Put the modules in a package and the scripts in a folder

Twenty-two files at the top of the tree was the first thing anybody saw of
this repository. They are one package now, imported relatively, and the three
scripts that are not the front door moved under scripts/. install.sh stays
where the README has always said it is.

What starts the application is dikte/__main__.py: python3 -m dikte runs it,
and so does naming the file, which is what the launcher symlink, both .desktop
files, the macOS bundle and every registered shortcut do. Run by path there is
no package around it, so it puts the checkout on sys.path itself.

The installers now keep the keys you chose when they are given none, which is
what an update is: update.sh no longer has to read them out and pass them back.
An updater from before this commit cannot read them at all, so the one thing it
can say, the default key with an empty discard key, is read as "nothing was
asked for" rather than obeyed. That guard can go once nobody is updating across
this commit.
This commit is contained in:
2026-08-16 14:01:01 +03:00
parent 8444b6822d
commit 2f59029261
50 changed files with 316 additions and 215 deletions
+344
View File
@@ -0,0 +1,344 @@
#!/usr/bin/env bash
# Dikte on macOS: dependency check, the application bundle, the command, and
# the login item. install.sh hands over to this on a Mac; run it directly and
# it does the same thing.
#
# A Mac needs a bundle where Linux needs a .desktop file, and for the same
# reason plus one: it is what puts a name and an icon in the Finder, and it is
# also the identity macOS files the microphone and Accessibility permissions
# under. Run as a bare script instead, every permission is granted to whatever
# copy of python3 happened to run it, and it is granted again the next time
# Homebrew moves that copy.
set -euo pipefail
# The checkout, one level up: this script lives in scripts/, everything it
# touches is at the top of the tree.
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# The one file that starts the application, whoever is asking: the wrapper in
# ~/.local/bin, the bundle, and every shortcut Dikte registers.
ENTRY="$DIR/dikte/__main__.py"
APP_DIR="$HOME/Applications"
APP="$APP_DIR/Dikte.app"
BIN_DIR="$HOME/.local/bin"
AGENT_DIR="$HOME/Library/LaunchAgents"
AGENT_ID="io.github.yusufipk.dikte"
AGENT="$AGENT_DIR/$AGENT_ID.plist"
# Only the one: the discard key's default is the settings' own, read back below.
DEFAULT_SHORTCUT="Ctrl+Option+Space"
# Given as arguments, or asked of the settings further down. An installer run
# again, which is what every update does, must not undo a key you chose in
# Settings, so silence here means "keep whatever is there".
SHORTCUT="${1:-}"
CANCEL_SHORTCUT="${2-}"
# Passed as "" means the discard key is off, which is not the same answer as
# not being passed at all.
CANCEL_GIVEN=$(( $# >= 2 ))
# One caller says both without meaning either: an updater from before Dikte
# became a package looks for the settings at a path that no longer exists, and
# so passes the default key and an empty discard key rather than yours. This
# can go once nobody is updating across that commit any more.
if [[ "$SHORTCUT" == "$DEFAULT_SHORTCUT" && $CANCEL_GIVEN == 1 && -z "$CANCEL_SHORTCUT" ]]; then
SHORTCUT=""
CANCEL_GIVEN=0
fi
# The two places Homebrew installs to, in front, for the same reason the app
# puts them there: a shell that has not been logged into since Homebrew was
# installed does not have them, and this script would then report ffmpeg as
# missing while the application finds it perfectly well.
PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
say() { printf ' %s\n' "$1"; }
ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; }
warn() { printf ' \033[33m!\033[0m %s\n' "$1"; }
die() { printf ' \033[31m✗\033[0m %s\n' "$1"; echo; exit 1; }
echo
echo "Installing Dikte"
echo "────────────────"
# 1. An interpreter new enough ---------------------------------------------
# The system python3 is 3.9 and will stay 3.9: Apple ships it for its own
# scripts, not for anybody's application, and Dikte needs 3.11. So the first
# question is not "is python3 there" but "which python3", and the answer is
# written into the bundle rather than looked up again at launch, because a
# bundle started from the Finder gets none of the shell's PATH.
find_python() {
local candidate
for candidate in \
"${DIKTE_PYTHON:-}" \
/opt/homebrew/bin/python3.14 /opt/homebrew/bin/python3.13 \
/opt/homebrew/bin/python3.12 /opt/homebrew/bin/python3.11 \
/usr/local/bin/python3.14 /usr/local/bin/python3.13 \
/usr/local/bin/python3.12 /usr/local/bin/python3.11 \
python3.14 python3.13 python3.12 python3.11 python3
do
[[ -n "$candidate" ]] || continue
candidate="$(command -v "$candidate" 2>/dev/null)" || continue
# Resolved, so that the bundle does not point at a Homebrew shim that a
# later `brew upgrade` repoints at a version Dikte cannot run on.
candidate="$(cd "$(dirname "$candidate")" && pwd)/$(basename "$candidate")"
# Exit status, not printed output: a candidate that cannot run Python at
# all and one that is too old are the same answer here.
if "$candidate" -c 'import sys; raise SystemExit(sys.version_info < (3, 11))' 2>/dev/null; then
printf '%s' "$candidate"
return 0
fi
done
return 1
}
PY="$(find_python || true)"
if [[ -z "$PY" ]]; then
warn "No Python 3.11 or newer found (the one Apple ships is 3.9)."
say "brew install [email protected]"
die "Nothing was installed."
fi
ok "Python: $PY ($("$PY" -c 'import platform; print(platform.python_version())'))"
# 2. The rest of the dependencies ------------------------------------------
missing=()
"$PY" -c 'import PyQt6.QtWidgets' 2>/dev/null || missing+=("PyQt6")
command -v ffmpeg >/dev/null || missing+=("ffmpeg")
if ((${#missing[@]})); then
warn "Missing: ${missing[*]}"
for item in "${missing[@]}"; do
case "$item" in
# Not pip: Homebrew's python is marked externally managed, so installing
# into it is refused. `brew install pyqt` puts PyQt6 where its own
# interpreter already looks. A virtualenv is the other answer, and then
# DIKTE_PYTHON is how this script is pointed at it.
PyQt6) say "PyQt6: brew install pyqt" ;;
ffmpeg) say "ffmpeg: brew install ffmpeg" ;;
esac
done
# ffmpeg alone is survivable: it is what records, so nothing works without
# it, but the settings window opens and says so. PyQt6 is not: there is no
# window to say anything in.
if [[ " ${missing[*]} " == *" PyQt6 "* ]]; then
echo
die "PyQt6 is what the whole interface is; install it and run this again."
fi
echo
else
ok "All dependencies present"
fi
# 3. The application bundle -------------------------------------------------
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
# The interpreter is copied in rather than run where it lies, and that copy is
# what the whole of macOS's idea of "which application is this" rests on.
#
# A bundle whose launcher runs an interpreter somewhere else is not that
# interpreter's application: the process is /opt/homebrew/…/python3.13, so that
# is the name in the microphone dialog, that is the row in Accessibility, and
# every application on the machine sharing that interpreter shares the
# permission. Copied to Contents/MacOS and exec'd from there, the running
# executable sits inside Dikte.app, macOS reads the Info.plist above it, and
# the permissions are Dikte's: its name, its icon, its row.
#
# The copy runs because the framework it links against is named by an absolute
# path; what it loses is the tree it was found in, which is what PYTHONHOME and
# PYTHONPATH below hand back. Both of those are what a `brew upgrade python`
# moves out from under it, so the launcher checks before it starts: a bundle
# that fails silently is a menu bar with nothing in it and no way to guess why.
PY_REAL="$("$PY" -c 'import os, sys; print(os.path.realpath(getattr(sys, "_base_executable", sys.executable)))')"
PY_HOME="$("$PY" -c 'import sys; print(sys.base_prefix)')"
PY_SITE="$("$PY" -c 'import site; print(site.getsitepackages()[0])')"
cp -f "$PY_REAL" "$APP/Contents/MacOS/python3"
# exec, and this time it is right. The earlier version of this script did not
# exec, because exec'ing an interpreter outside the bundle throws away the
# application registration LaunchServices handed to the process it started, and
# what is left answers the socket while drawing no menu bar icon at all. The
# target here is inside the bundle, so the registration survives it.
cat > "$APP/Contents/MacOS/Dikte" <<EOF
#!/bin/sh
# Written by install-mac.sh. Edit that, not this.
HERE=\$(cd "\$(dirname "\$0")" && pwd)
export PYTHONHOME="$PY_HOME"
export PYTHONPATH="$PY_SITE"
# Started from the Finder there is no terminal to print to, so the one thing
# that can go wrong on its own says so in a dialog.
if [ ! -d "\$PYTHONHOME" ]; then
osascript -e 'display alert "Dikte" message "The Python this was installed against is gone, most likely after a brew upgrade. Run ./install.sh again."' >/dev/null 2>&1
exit 1
fi
exec "\$HERE/python3" "$ENTRY" --gui "\$@"
EOF
chmod +x "$APP/Contents/MacOS/Dikte"
# LSUIElement is the line that makes this a menu bar application: no Dock icon,
# no menu bar of its own, nothing in the app switcher. The usage strings are
# not decoration either, they are what the permission dialog reads out, and a
# bundle that asks for the microphone without one is killed rather than asked
# about.
version="$(cd "$DIR" && git describe --tags --always 2>/dev/null || echo 0)"
cat > "$APP/Contents/Info.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key> <string>Dikte</string>
<key>CFBundleDisplayName</key> <string>Dikte</string>
<key>CFBundleIdentifier</key> <string>$AGENT_ID</string>
<key>CFBundleExecutable</key> <string>Dikte</string>
<key>CFBundleIconFile</key> <string>Dikte</string>
<key>CFBundlePackageType</key> <string>APPL</string>
<key>CFBundleShortVersionString</key><string>$version</string>
<key>CFBundleVersion</key> <string>$version</string>
<key>LSMinimumSystemVersion</key> <string>11.0</string>
<key>NSHighResolutionCapable</key> <true/>
<key>LSUIElement</key> <true/>
<key>NSMicrophoneUsageDescription</key>
<string>Dikte records what you dictate so that it can be transcribed.</string>
<key>NSAppleEventsUsageDescription</key>
<string>Dikte puts the transcript on the clipboard and pastes it into the window you were typing in.</string>
</dict>
</plist>
EOF
printf 'APPL????' > "$APP/Contents/PkgInfo"
# The icon, drawn by trayicon.py so that there is no binary in the repository
# and no second place to change what Dikte looks like. Failing to draw it is
# not worth stopping for: a bundle with no icon gets the generic one.
iconset="$(mktemp -d)/Dikte.iconset"
if PYTHONPATH="$DIR" "$PY" -m dikte.trayicon "$iconset" >/dev/null 2>&1 \
&& iconutil -c icns "$iconset" -o "$APP/Contents/Resources/Dikte.icns" 2>/dev/null; then
ok "Icon drawn"
else
warn "Could not build the icon; the bundle gets the generic one"
fi
rm -rf "$(dirname "$iconset")"
# Signed, ad-hoc, and this is the load-bearing step. macOS remembers a
# permission against a code signature; with none, it remembers a path and a
# hash of the bundle, and every reinstall is a bundle it has never seen, which
# means granting Accessibility and the microphone again each time. --force
# because a reinstall is signing over the last signature.
if codesign --force --sign - --identifier "$AGENT_ID" "$APP" 2>/dev/null; then
ok "Application bundle: $APP"
else
warn "Could not sign $APP; macOS will ask for its permissions again on every"
say "reinstall. Install the command line tools: xcode-select --install"
fi
# LaunchServices does not necessarily notice a bundle written under a directory
# it was not watching, and until it does, `open -a Dikte` cannot find it.
/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister \
-f "$APP" >/dev/null 2>&1 || true
# 4. The command ------------------------------------------------------------
# A wrapper, where Linux gets a symlink to the entry point. The shebang there is
# `env python3`, and on a Mac that is Apple's 3.9: the symlink would resolve to
# the one interpreter that cannot run this. Naming the interpreter here also
# gives update.sh and uninstall.sh somewhere to read it from, so that the three
# scripts cannot disagree about which Python this installation is on.
mkdir -p "$BIN_DIR"
cat > "$BIN_DIR/dikte" <<EOF
#!/bin/sh
# Written by install-mac.sh. Edit that, not this.
exec "$PY" "$ENTRY" "\$@"
EOF
chmod +x "$BIN_DIR/dikte" "$ENTRY"
ok "Command installed: $BIN_DIR/dikte"
case ":$PATH:" in
*":$BIN_DIR:"*) ;;
*) warn "$BIN_DIR is not on your PATH. Add it in ~/.zprofile:"
say " export PATH=\"\$HOME/.local/bin:\$PATH\"" ;;
esac
# 5. Starting at login ------------------------------------------------------
# A LaunchAgent rather than a Login Item: it is a file this script can write
# and uninstall.sh can delete, where a Login Item is a database only the user
# can edit by hand. KeepAlive is off on purpose: quitting from the tray menu
# should quit it, not restart it.
mkdir -p "$AGENT_DIR"
cat > "$AGENT" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key> <string>$AGENT_ID</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/open</string>
<string>-a</string>
<string>$APP</string>
</array>
<key>RunAtLoad</key> <true/>
<key>KeepAlive</key> <false/>
<key>ProcessType</key> <string>Interactive</string>
</dict>
</plist>
EOF
# Through `open` rather than running the launcher directly, so that the process
# is one LaunchServices started: that is what gives it the bundle's identity,
# and so the permissions granted to Dikte rather than to launchd.
launchctl bootout "gui/$(id -u)/$AGENT_ID" >/dev/null 2>&1 || true
if launchctl bootstrap "gui/$(id -u)" "$AGENT" >/dev/null 2>&1; then
ok "Will start automatically on login"
else
warn "Could not register the login item; add $APP under"
say "System Settings → General → Login Items instead."
fi
# 6. The shortcuts ----------------------------------------------------------
# Dikte registers them rather than this script writing a file: macOS keeps no
# shortcut registry at all, so "installed" means the running application is
# holding the combination, and the settings file is where it reads it from
# after a restart. Nothing here needs a logout, unlike KDE.
#
# What was not asked for is read back out of the settings, which is what makes
# a second run of this script leave the keys you chose alone.
stored() { PYTHONPATH="$DIR" "$PY" -m dikte config get "$1" 2>/dev/null || true; }
if [[ -z "$SHORTCUT" ]]; then
SHORTCUT="$(stored shortcut)"
SHORTCUT="${SHORTCUT:-$DEFAULT_SHORTCUT}"
fi
if [[ $CANCEL_GIVEN == 0 ]]; then
# An empty answer here is a discard key that was turned off, and it stays off.
CANCEL_SHORTCUT="$(stored cancel_shortcut)"
fi
if [[ -n "$CANCEL_SHORTCUT" && "$SHORTCUT" == "$CANCEL_SHORTCUT" ]]; then
warn "Both keys are $SHORTCUT, so the discard key was left out."
say "Pass two different combinations, or set it in Settings → Shortcuts."
CANCEL_SHORTCUT=""
fi
register() { # which combination label
if out="$("$PY" "$ENTRY" shortcut install "$1" --combo "$2" 2>&1)"; then
ok "$3: $2"
else
warn "${out%%$'\n'*}"
fi
}
register toggle "$SHORTCUT" "Start and stop"
if [[ -n "$CANCEL_SHORTCUT" ]]; then
register cancel "$CANCEL_SHORTCUT" "Discard the recording"
fi
# 7. What is left to do by hand ---------------------------------------------
# Two things this script cannot do, because macOS only takes them from the
# person at the keyboard.
echo
say "Two permissions have to be granted the first time, and macOS will ask:"
say " • Microphone, when the first recording starts"
say " • Accessibility, when the first transcript is pasted"
say "Both are under System Settings → Privacy & Security."
if ! system_profiler SPAudioDataType 2>/dev/null | grep -qi 'blackhole\|loopback\|soundflower'; then
echo
say "Recording a meeting also needs a loopback driver: macOS does not offer"
say "what the speakers are playing as something to record. Install one with:"
say " brew install blackhole-2ch"
say "Dictation does not need it."
fi
echo
ok "Done. Start it with: open -a Dikte"
say "The settings window opens on first run: download a speech model, or add"
say "an OpenAI or OpenRouter key instead."
echo
+234
View File
@@ -0,0 +1,234 @@
#!/usr/bin/env bash
# Dikte uninstaller: takes back what install.sh put down, and nothing else
# unless asked. Your settings and your dictations survive a plain run; --purge
# is the word that deletes them.
set -euo pipefail
# The checkout, one level up: this script lives in scripts/, everything it
# touches is at the top of the tree.
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENTRY="$DIR/dikte/__main__.py"
USER_NAME="$(id -un)"
BIN_DIR="$HOME/.local/bin"
# What an installation is made of differs by platform, but what --purge deletes
# and what it asks before deleting it does not, which is why this is one script
# with two sets of paths rather than two scripts.
if [[ "$(uname -s)" == "Darwin" ]]; then
MACOS=1
MAC_APP="$HOME/Applications/Dikte.app"
AGENT_ID="io.github.yusufipk.dikte"
AGENT="$HOME/Library/LaunchAgents/$AGENT_ID.plist"
CONFIG_DIR="$HOME/Library/Application Support/Dikte"
DATA_DIR="$CONFIG_DIR"
# The interpreter this installation was put on, which is not necessarily the
# python3 on PATH: Apple's is 3.9 and cannot run Dikte. The wrapper written
# by install-mac.sh names it, so it is read back out of there.
# `|| true` because there may be no wrapper to read: pipefail would otherwise
# make a missing file the end of the script rather than a question answered no.
PY="$(sed -n 's/^exec "\([^"]*\)".*/\1/p' "$BIN_DIR/dikte" 2>/dev/null | head -1 || true)"
[[ -x "$PY" ]] || PY="$(command -v python3 || true)"
else
MACOS=0
APP_DIR="$HOME/.local/share/applications"
AUTOSTART_DIR="$HOME/.config/autostart"
ICON_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/icons"
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/dikte"
DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/dikte"
PY="$(command -v python3 || true)"
fi
PURGE=0
ASSUME_YES=0
say() { printf ' %s\n' "$1"; }
ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; }
warn() { printf ' \033[33m!\033[0m %s\n' "$1"; }
gone() { printf ' \033[90m·\033[0m %s\n' "$1"; }
# "1 dictation", "3 dictations": how many is the point of printing it at all.
count() {
if (( $1 == 1 )); then printf '%s %s' "$1" "$2"; else printf '%s %s' "$1" "$3"; fi
}
usage() {
cat <<EOF
Usage: ./scripts/uninstall.sh [--purge] [--yes]
--purge also delete the settings ($CONFIG_DIR)
and the dictations, meetings and recordings ($DATA_DIR)
--yes do not ask before deleting those
Without --purge nothing you have written is touched, and the source directory
is left alone either way.
EOF
}
for arg in "$@"; do
case "$arg" in
--purge) PURGE=1 ;;
--yes|-y) ASSUME_YES=1 ;;
-h|--help) usage; exit 0 ;;
*) printf 'uninstall.sh: unknown option: %s\n' "$arg" >&2; usage >&2; exit 2 ;;
esac
done
# A symlink whose target is gone is still a file to remove, hence -L.
remove() {
if [[ -e "$1" || -L "$1" ]]; then
rm -f "$1"
ok "Removed $1"
else
gone "Was not there: $1"
fi
}
echo
echo "Uninstalling Dikte"
echo "──────────────────"
# 1. Global shortcuts ------------------------------------------------------
# Handed to Dikte while it can still run, because it is the half that knows
# whether they went into KDE's kglobalshortcutsrc, GNOME's gsettings, or
# nowhere at all. macOS and the desktops with no registry hold the combinations
# in the running process, where they are gone the moment it stops, so there is
# nothing there to take back.
if ((MACOS)); then
say "Nothing to unregister: macOS shortcuts live only while Dikte runs."
elif [[ -n "$PY" ]] && "$PY" -c 'import PyQt6.QtWidgets' 2>/dev/null; then
for which in toggle pause cancel ask meeting; do
"$PY" "$ENTRY" shortcut remove "$which" >/dev/null 2>&1 || true
done
case "$(PYTHONPATH="$DIR" "$PY" -c 'from dikte import hotkey; print(hotkey.backend())' 2>/dev/null)" in
kde)
ok "Global shortcuts unregistered"
say "KWin reads that file at startup, so the keys are free after your next login."
;;
gnome) ok "Global shortcuts unregistered" ;;
*) say "Nothing to unregister: Dikte listened for the keys itself, and they stop with it." ;;
esac
else
warn "PyQt6 is missing, so the shortcuts were left registered."
say "Remove them in your desktop's shortcut settings."
fi
# 2. The running instance --------------------------------------------------
# It holds a tray icon and a socket; asking it to quit is tidier than pulling
# its launchers out from under it.
# The pattern matches an instance started before Dikte became a package as well,
# which is what an uninstall run straight after an update finds running.
if pgrep -u "$USER_NAME" -f 'dikte(/__main__|)\.py' >/dev/null 2>&1; then
[[ -n "$PY" ]] && "$PY" "$ENTRY" quit >/dev/null 2>&1 || true
sleep 0.5
if pgrep -u "$USER_NAME" -f 'dikte(/__main__|)\.py' >/dev/null 2>&1; then
warn "Dikte is still running; close it from the tray icon"
else
ok "Stopped the running instance"
fi
fi
# 3. Launchers -------------------------------------------------------------
# Only what this installer wrote goes: a file of the same name that somebody
# else put there is not ours to delete. On Linux ours is a symlink; on macOS it
# is a generated wrapper, which says so in its second line.
if [[ -L "$BIN_DIR/dikte" ]]; then
remove "$BIN_DIR/dikte"
elif ((MACOS)) && grep -q 'Written by install-mac.sh' "$BIN_DIR/dikte" 2>/dev/null; then
remove "$BIN_DIR/dikte"
elif [[ -e "$BIN_DIR/dikte" ]]; then
warn "$BIN_DIR/dikte is not ours, leaving it alone"
else
gone "Was not there: $BIN_DIR/dikte"
fi
if ((MACOS)); then
# The login item first: bootout while the plist is still there, because
# launchctl is told which job by the file as much as by the label.
if [[ -e "$AGENT" ]]; then
launchctl bootout "gui/$(id -u)/$AGENT_ID" >/dev/null 2>&1 || true
fi
remove "$AGENT"
if [[ -d "$MAC_APP" ]]; then
rm -rf "$MAC_APP"
ok "Removed $MAC_APP"
else
gone "Was not there: $MAC_APP"
fi
# macOS keeps the permissions filed against the bundle that asked for them,
# and deleting the bundle does not withdraw them.
say "Microphone and Accessibility are still granted to Dikte; take them back"
say "under System Settings → Privacy & Security if you want them gone."
fi
if ((!MACOS)); then
remove "$APP_DIR/dikte.desktop"
remove "$AUTOSTART_DIR/dikte.desktop"
# The icon, at each of the sizes install.sh drew it. One line rather than
# eight, and the size directories stay: they are the theme's, not ours.
icons=0
for png in "$ICON_DIR"/hicolor/*/apps/dikte.png; do
[[ -e "$png" ]] || continue
rm -f "$png"
icons=$((icons + 1))
done
if ((icons)); then
ok "Removed the icon from $ICON_DIR/hicolor"
else
gone "Was not there: $ICON_DIR/hicolor/*/apps/dikte.png"
fi
# Removing the shortcut takes its desktop file with it, but an install from
# before this script existed may have left one behind on a desktop that never
# used them.
for id in dikte-toggle dikte-pause dikte-cancel dikte-ask dikte-meeting; do
if [[ -e "$APP_DIR/$id.desktop" ]]; then
remove "$APP_DIR/$id.desktop"
fi
done
fi
# 4. Settings and dictations -----------------------------------------------
echo
if ((PURGE)); then
warn "--purge also deletes:"
if [[ -f "$CONFIG_DIR/config.json" ]]; then
say "$CONFIG_DIR/config.json (your API keys and every setting)"
fi
if [[ -f "$DATA_DIR/history.jsonl" ]]; then
# grep -c rather than wc -l: a last line with no newline is still a dictation.
say "$DATA_DIR/history.jsonl ($(count "$(grep -c '' "$DATA_DIR/history.jsonl" 2>/dev/null || echo 0)" dictation dictations))"
fi
if [[ -d "$DATA_DIR/meetings" ]]; then
say "$DATA_DIR/meetings ($(count "$(find "$DATA_DIR/meetings" -name '*.md' | wc -l)" meeting meetings))"
fi
if [[ -d "$DATA_DIR/recordings" ]]; then
say "$DATA_DIR/recordings ($(du -sh "$DATA_DIR/recordings" | cut -f1) of audio)"
fi
if ((!ASSUME_YES)); then
if [[ -t 0 ]]; then
printf ' Type yes to delete them: '
read -r reply
[[ "$reply" == "yes" ]] || { PURGE=0; say "Kept."; }
else
PURGE=0
warn "Not a terminal, so nothing was deleted. Pass --yes if you meant it."
fi
fi
fi
if ((PURGE)); then
rm -rf "$CONFIG_DIR" "$DATA_DIR"
ok "Settings and dictations deleted"
elif [[ "$CONFIG_DIR" == "$DATA_DIR" ]]; then
# macOS keeps both in the one directory a Mac user's backup already knows
# about, so naming it twice would only look like two things were kept.
say "Settings and dictations kept: $CONFIG_DIR"
say "Delete them too with: ./scripts/uninstall.sh --purge"
else
say "Settings kept: $CONFIG_DIR"
say "Dictations kept: $DATA_DIR"
say "Delete them too with: ./scripts/uninstall.sh --purge"
fi
echo
ok "Done."
say "The source directory is untouched: $DIR"
echo
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
# Dikte updater: pull, put the launchers back, restart what was running.
set -euo pipefail
# The checkout, one level up: this script lives in scripts/, everything it
# touches is at the top of the tree.
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENTRY="$DIR/dikte/__main__.py"
USER_NAME="$(id -un)"
if [[ "$(uname -s)" == "Darwin" ]]; then
# Apple's python3 is 3.9 and will stay 3.9, so the one on PATH is the wrong
# question here. install-mac.sh names the interpreter it installed against in
# the wrapper it wrote, and that is the one this installation runs on.
# `|| true` because there may be no wrapper to read: pipefail would otherwise
# make a missing file the end of the script rather than a question answered no.
PY="$(sed -n 's/^exec "\([^"]*\)".*/\1/p' "$HOME/.local/bin/dikte" 2>/dev/null | head -1 || true)"
[[ -x "$PY" ]] || PY="$(command -v python3 || true)"
else
PY="$(command -v python3 || true)"
fi
say() { printf ' %s\n' "$1"; }
ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; }
warn() { printf ' \033[33m!\033[0m %s\n' "$1"; }
die() { printf ' \033[31m✗\033[0m %s\n' "$1"; echo; exit 1; }
echo
echo "Updating Dikte"
echo "──────────────"
cd "$DIR"
# 1. Somewhere there is something to pull ----------------------------------
command -v git >/dev/null || die "git not found; update by downloading the source again"
git rev-parse --git-dir >/dev/null 2>&1 \
|| die "$DIR is not a git checkout; update by downloading the source again"
before="$(git rev-parse HEAD)"
# 2. Is there anything to come? ---------------------------------------------
# Asked before anything else is complained about: an unfinished afternoon in
# the working tree is nobody's problem on a day when nothing has been
# published. Fetching leaves the working tree alone.
git fetch --quiet || die "Could not reach the remote."
upstream="$(git rev-parse '@{u}' 2>/dev/null)" \
|| die "This branch is not tracking a remote one; pull by hand."
if [[ "$before" == "$upstream" ]]; then
echo
ok "Already up to date ($(git log -1 --format=%s))"
echo
exit 0
fi
# 3. Only now, your own edits -----------------------------------------------
# They would be overwritten by a fast-forward or would block it, and either way
# that is your call to make, not this script's. Untracked files are counted
# too: a fast-forward that adds a file of that name stops on them.
if [[ -n "$(git status --porcelain)" ]]; then
warn "There is an update waiting, but you have changes of your own here:"
git --no-pager status --short | sed 's/^/ /'
say "Commit them, or put them aside with: git stash --include-untracked"
die "Nothing was updated."
fi
# --ff-only: an update should be somebody else's commits arriving, never a
# merge this script decided to make on your behalf. The fetch above already
# brought them, so this touches no network.
# advice off: git's suggestion is a merge or a rebase, and which of those you
# want is the sentence below, not a wall of hints.
if ! merge_log="$(git -c advice.diverging=false merge --ff-only '@{u}' 2>&1)"; then
printf '%s\n' "$merge_log" | sed 's/^/ /'
say "Your branch has commits the remote does not. To put them on top of the"
say "update instead: git pull --rebase"
die "Could not fast-forward."
fi
after="$(git rev-parse HEAD)"
echo
say "What arrived:"
git --no-pager log --oneline "$before..$after" | sed 's/^/ /'
echo
# 4. Launchers --------------------------------------------------------------
# An update can add a dependency or move a file, so the installer runs again.
# With no keys named it keeps the ones stored in the settings, which are the
# ones you chose.
"$DIR/install.sh"
# 5. The running instance ---------------------------------------------------
# It is still running the code from before the pull, which on an update across
# the move into a package is a process still named after the old entry point.
if pgrep -u "$USER_NAME" -f 'dikte(/__main__|)\.py' >/dev/null 2>&1; then
if [[ -n "$PY" ]] && "$PY" "$ENTRY" restart >/dev/null 2>&1; then
ok "Restarted, so the new version is the one running"
else
warn "Could not restart it; use the tray menu → Restart"
fi
else
say "Dikte was not running. Start it with: dikte"
fi
echo