diff --git a/README.md b/README.md index c8196a4..4b42583 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,9 @@ sudo apt install pulseaudio-utils xclip xdotool ffmpeg ``` `install.sh` adds the `dikte` command, a menu entry, an autostart entry and the -two global shortcuts, whose keys are its two arguments. +two global shortcuts, whose keys are its two arguments. `./update.sh` pulls and +puts all of that back, keeping the keys you chose; `./uninstall.sh` takes it away +again and leaves your settings and dictations alone unless you pass `--purge`. Three keys go in the settings window: **OpenAI**, **Groq** and **OpenRouter**. Speech to text runs on any of them (`gpt-4o-transcribe` by default), cleanup diff --git a/README.tr.md b/README.tr.md index cfb503c..97b007d 100644 --- a/README.tr.md +++ b/README.tr.md @@ -37,7 +37,10 @@ sudo apt install pulseaudio-utils xclip xdotool ffmpeg ``` `install.sh` `dikte` komutunu, menü girdisini, oturum açılışında otomatik -başlatmayı ve iki global kısayolu kurar; tuşları da iki argümanı. +başlatmayı ve iki global kısayolu kurar; tuşları da iki argümanı. `./update.sh` +son sürümü çeker ve bunları senin seçtiğin tuşlarla yerine koyar; +`./uninstall.sh` hepsini geri alır, `--purge` demedikçe ayarlarına ve +diktelerine dokunmaz. Ayarlar penceresinde üç anahtar istenir: **OpenAI**, **Groq** ve **OpenRouter**. Sesi yazıya çevirme üçünden birinde çalışır (varsayılan `gpt-4o-transcribe`), diff --git a/uninstall.sh b/uninstall.sh new file mode 100755 index 0000000..dfbdaa1 --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,154 @@ +#!/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)" +PY="$(command -v python3 || true)" +USER_NAME="$(id -un)" +BIN_DIR="$HOME/.local/bin" +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" + +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 <&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 or GNOME's gsettings. +if [[ -n "$PY" ]] && python3 -c 'import PyQt6.QtWidgets' 2>/dev/null; then + for which in toggle cancel ask meeting; do + "$PY" "$DIR/dikte.py" shortcut remove "$which" >/dev/null 2>&1 || true + done + ok "Global shortcuts unregistered" + say "KWin reads that file at startup, so the keys are free after your next login." +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 our own symlink goes: a file of the same name that somebody else put +# there is not ours to delete. +if [[ -L "$BIN_DIR/dikte" ]]; then + remove "$BIN_DIR/dikte" +elif [[ -e "$BIN_DIR/dikte" ]]; then + warn "$BIN_DIR/dikte is not our symlink, leaving it alone" +else + gone "Was not there: $BIN_DIR/dikte" +fi +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-cancel dikte-ask dikte-meeting; do + if [[ -e "$APP_DIR/$id.desktop" ]]; then + remove "$APP_DIR/$id.desktop" + fi +done + +# 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" +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 diff --git a/update.sh b/update.sh new file mode 100755 index 0000000..9194407 --- /dev/null +++ b/update.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# Dikte updater: pull, put the launchers back, restart what was running. +set -euo pipefail + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PY="$(command -v python3 || true)" +USER_NAME="$(id -un)" + +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; } + +# The combination stored in the settings, which is where Dikte itself reads it +# from and the one place that is the same on KDE and on GNOME. +setting() { + [[ -n "$PY" ]] || return 0 + "$PY" "$DIR/dikte.py" config get "$1" 2>/dev/null || true +} + +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. +# It would otherwise register its own defaults over the keys you chose, so it +# is told what those are. Read before the installer runs, since it is the one +# writing them. +shortcut="$(setting shortcut)" +cancel_shortcut="$(setting cancel_shortcut)" +# Positional, so a chosen discard key cannot be passed without the other one. +"$DIR/install.sh" "${shortcut:-Ctrl+Space}" "${cancel_shortcut:-}" + +# 5. The running instance --------------------------------------------------- +# It is still running the code from before the pull. +if pgrep -u "$USER_NAME" -f 'dikte\.py' >/dev/null 2>&1; then + if [[ -n "$PY" ]] && "$PY" "$DIR/dikte.py" 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