mirror of
https://github.com/yusufipk/dikte.git
synced 2026-09-11 10:56:10 +00:00
Only GNOME was recognised, and everything else was handed to KWin. On i3, XFCE, Cinnamon, MATE, sway and the rest, Dikte wrote an entry into kglobalshortcutsrc that nothing reads, called the session KDE, and promised that the keys would work after the next login. They never did. There is no backend to write for any of them. The /dev/input listener is already desktop-agnostic, so those sessions are the case macOS has always been: no registry, nothing to install, nothing to remove, and the combination held by the running process. One backend() function decides which of the four this session has, and the name shown, the status read back, what Install writes, what Settings explains and what the installer promises are all taken from it, so they cannot disagree. A desktop now only counts when the program that writes its registry is there too. A GNOME session without gsettings and a Plasma one without kwriteconfig6 fall to the listener rather than to a file, which is also how Plasma 5 stops erroring on a kwriteconfig6 it never had. What Settings shows on those desktops is the truth: no Install button, no KWin, no listener checkbox (it is the mechanism, not a choice), what reading /dev/input costs, that the focused application sees the keys too, and the command to bind if you would rather your desktop owned them. The evdev listener records what it is listening for the way the Carbon one does, so the status line has something to say there at all. Closes #28
216 lines
7.9 KiB
Bash
Executable File
216 lines
7.9 KiB
Bash
Executable File
#!/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
|
|
|
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
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"
|
|
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: ./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" "$DIR/dikte.py" shortcut remove "$which" >/dev/null 2>&1 || true
|
|
done
|
|
case "$("$PY" -c 'import sys; sys.path.insert(0, sys.argv[1]); import hotkey; print(hotkey.backend())' "$DIR" 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.
|
|
if pgrep -u "$USER_NAME" -f 'dikte\.py' >/dev/null 2>&1; then
|
|
[[ -n "$PY" ]] && "$PY" "$DIR/dikte.py" quit >/dev/null 2>&1 || true
|
|
sleep 0.5
|
|
if pgrep -u "$USER_NAME" -f 'dikte\.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"
|
|
# 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: ./uninstall.sh --purge"
|
|
else
|
|
say "Settings kept: $CONFIG_DIR"
|
|
say "Dictations kept: $DATA_DIR"
|
|
say "Delete them too with: ./uninstall.sh --purge"
|
|
fi
|
|
|
|
echo
|
|
ok "Done."
|
|
say "The source directory is untouched: $DIR"
|
|
echo
|