@arcforgelabs/dictate 2026.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/install.sh ADDED
@@ -0,0 +1,303 @@
1
+ #!/usr/bin/env bash
2
+ # Install/update dictate into a standalone venv at ~/.local/share/dictate.
3
+ # Uses --system-site-packages so GTK/gi bindings are available.
4
+ # Re-run this script after pulling changes to update the installation.
5
+ set -euo pipefail
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
8
+ INSTALL_DIR="$HOME/.local/share/dictate"
9
+ BIN_DIR="$HOME/.local/bin"
10
+ DESKTOP_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
11
+ AUTOSTART_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/autostart"
12
+ ICON_DIR="$INSTALL_DIR/share/icons"
13
+ ICON_PATH="$ICON_DIR/dictate-simple.png"
14
+ CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/dictate"
15
+ CONFIG_PATH="$CONFIG_DIR/config.yaml"
16
+ DEFAULT_CONFIG_SOURCE="$SCRIPT_DIR/config/default-config.yaml"
17
+ VERIFY=1
18
+ PREPARE_TURBO=1
19
+ SEED_DEFAULT_CONFIG=1
20
+ STARTUP=1
21
+ INSTALL_UI=1
22
+ PYTHON_BIN="${PYTHON_BIN:-python3}"
23
+
24
+ # Prefer the distro Python so --system-site-packages can see modules such as
25
+ # python3-gi from /usr/lib/python3/dist-packages.
26
+ if [ -x /usr/bin/python3 ]; then
27
+ PYTHON_BIN="/usr/bin/python3"
28
+ fi
29
+
30
+ usage() {
31
+ cat <<EOF
32
+ Usage: $0 [--no-verify] [--no-prepare-turbo] [--no-seed-default-config] [--no-startup] [--no-ui] [--session-backend auto|x11|wayland]
33
+
34
+ Installs dictate into ~/.local/share/dictate, links ~/.local/bin/dictate and
35
+ ~/.local/bin/dictate-ui-server, seeds the default config on first install,
36
+ creates app launcher/autostart entries, prepares the faster-whisper turbo model,
37
+ and installs the desktop "Quiet Console" UI shell when a build toolchain is
38
+ present (unless disabled). All steps degrade gracefully when prerequisites are
39
+ missing.
40
+ EOF
41
+ }
42
+
43
+ detect_session_backend() {
44
+ if [ -n "${WAYLAND_DISPLAY:-}" ] || [ "${XDG_SESSION_TYPE:-}" = "wayland" ]; then
45
+ printf 'wayland\n'
46
+ return
47
+ fi
48
+ if [ -n "${DISPLAY:-}" ] || [ "${XDG_SESSION_TYPE:-}" = "x11" ]; then
49
+ printf 'x11\n'
50
+ return
51
+ fi
52
+ if [ -n "${XDG_SESSION_ID:-}" ] && command -v loginctl >/dev/null 2>&1; then
53
+ local detected
54
+ detected="$(loginctl show-session "$XDG_SESSION_ID" -p Type --value 2>/dev/null || true)"
55
+ if [ "$detected" = "wayland" ] || [ "$detected" = "x11" ]; then
56
+ printf '%s\n' "$detected"
57
+ return
58
+ fi
59
+ fi
60
+ printf 'unknown\n'
61
+ }
62
+
63
+ SESSION_BACKEND="auto"
64
+
65
+ while [ "$#" -gt 0 ]; do
66
+ case "$1" in
67
+ --no-verify)
68
+ VERIFY=0
69
+ ;;
70
+ --no-prepare-turbo)
71
+ PREPARE_TURBO=0
72
+ ;;
73
+ --no-seed-default-config)
74
+ SEED_DEFAULT_CONFIG=0
75
+ ;;
76
+ --no-startup)
77
+ STARTUP=0
78
+ ;;
79
+ --no-ui)
80
+ INSTALL_UI=0
81
+ ;;
82
+ --session-backend)
83
+ shift
84
+ SESSION_BACKEND="${1:-}"
85
+ ;;
86
+ --session-backend=*)
87
+ SESSION_BACKEND="${1#*=}"
88
+ ;;
89
+ -h|--help)
90
+ usage
91
+ exit 0
92
+ ;;
93
+ *)
94
+ usage
95
+ exit 1
96
+ ;;
97
+ esac
98
+ shift
99
+ done
100
+
101
+ if [ "$SESSION_BACKEND" = "auto" ]; then
102
+ SESSION_BACKEND="$(detect_session_backend)"
103
+ fi
104
+
105
+ PIP_TARGET="$SCRIPT_DIR"
106
+ if [ "$SESSION_BACKEND" = "x11" ]; then
107
+ PIP_TARGET="${SCRIPT_DIR}[x11]"
108
+ elif [ "$SESSION_BACKEND" = "wayland" ]; then
109
+ PIP_TARGET="${SCRIPT_DIR}[wayland]"
110
+ elif [ "$SESSION_BACKEND" = "unknown" ]; then
111
+ PIP_TARGET="${SCRIPT_DIR}[x11,wayland]"
112
+ fi
113
+
114
+ echo "Detected install session backend: $SESSION_BACKEND"
115
+
116
+ # The packaged build (.deb/.rpm) and this source install both ship a tray/daemon
117
+ # and would fight over the push-to-talk key. Warn rather than silently double up.
118
+ if command -v dpkg-query >/dev/null 2>&1 \
119
+ && dpkg-query -W -f='${Status}' dictate 2>/dev/null | grep -q "install ok installed"; then
120
+ echo "WARNING: a packaged Dictate (.deb) is already installed and would conflict."
121
+ echo " Use one install method. To remove the package first: sudo apt remove dictate"
122
+ elif command -v rpm >/dev/null 2>&1 && rpm -q dictate >/dev/null 2>&1; then
123
+ echo "WARNING: a packaged Dictate (.rpm) is already installed and would conflict."
124
+ echo " Use one install method. To remove the package first: sudo dnf remove dictate"
125
+ fi
126
+
127
+ echo "Creating venv at $INSTALL_DIR ..."
128
+ uv venv "$INSTALL_DIR/venv" --python "$PYTHON_BIN" --system-site-packages --quiet
129
+
130
+ echo "Installing dictate from $PIP_TARGET ..."
131
+ uv pip install "$PIP_TARGET" --python "$INSTALL_DIR/venv/bin/python" --quiet
132
+
133
+ echo "Linking binaries ..."
134
+ mkdir -p "$BIN_DIR"
135
+ ln -sf "$INSTALL_DIR/venv/bin/dictate" "$BIN_DIR/dictate"
136
+ # Expose the control server so the desktop shell can launch it on PATH.
137
+ ln -sf "$INSTALL_DIR/venv/bin/dictate-ui-server" "$BIN_DIR/dictate-ui-server"
138
+
139
+ echo "Installing icon ..."
140
+ mkdir -p "$ICON_DIR"
141
+ rm -f "$ICON_DIR/dictate-controls.png" "$ICON_DIR/dictate.png"
142
+ install -m 644 "$SCRIPT_DIR/assets/dictate.png" "$ICON_PATH"
143
+
144
+ echo "Installing desktop entry ..."
145
+ mkdir -p "$DESKTOP_DIR"
146
+ rm -f "$DESKTOP_DIR/dictate-settings.desktop"
147
+ cat > "$DESKTOP_DIR/dictate.desktop" <<EOF
148
+ [Desktop Entry]
149
+ Name=Dictate
150
+ Comment=Dictate into the focused app
151
+ Exec=$HOME/.local/bin/dictate
152
+ Icon=$ICON_PATH
153
+ Type=Application
154
+ Categories=AudioVideo;Audio;
155
+ Keywords=voice;speech;transcription;dictation;asr;whisper;canary;
156
+ Terminal=false
157
+ EOF
158
+
159
+ update-desktop-database "$DESKTOP_DIR" 2>/dev/null || true
160
+
161
+ if [ "$STARTUP" -eq 1 ]; then
162
+ echo "Installing autostart entry ..."
163
+ mkdir -p "$AUTOSTART_DIR"
164
+ cat > "$AUTOSTART_DIR/dictate.desktop" <<EOF
165
+ [Desktop Entry]
166
+ Name=Dictate
167
+ Comment=Dictate into the focused app
168
+ Exec=$HOME/.local/bin/dictate
169
+ Icon=$ICON_PATH
170
+ Type=Application
171
+ Categories=AudioVideo;Audio;
172
+ Keywords=voice;speech;transcription;dictation;asr;whisper;canary;
173
+ Terminal=false
174
+ X-GNOME-Autostart-enabled=true
175
+ EOF
176
+ fi
177
+
178
+ print_ui_hint() {
179
+ cat <<EOF
180
+ The Quiet Console desktop UI is optional — the tray app works without it
181
+ (native dialogs as fallback). To add it later:
182
+ - download a .deb / AppImage from the GitHub release, or
183
+ - build it from this checkout: scripts/build-linux-desktop.sh
184
+ See ui-shell/README.md for details.
185
+ EOF
186
+ }
187
+
188
+ install_desktop_ui() {
189
+ local shell_src="$SCRIPT_DIR/ui-shell"
190
+ local target="$BIN_DIR/dictate-ui-shell"
191
+
192
+ if command -v dictate-ui-shell >/dev/null 2>&1; then
193
+ echo "Desktop UI shell already installed: $(command -v dictate-ui-shell)"
194
+ return 0
195
+ fi
196
+ if [ ! -d "$shell_src" ]; then
197
+ echo "Desktop UI shell sources not present; skipping the optional UI."
198
+ print_ui_hint
199
+ return 0
200
+ fi
201
+ if ! command -v cargo >/dev/null 2>&1 || ! command -v npm >/dev/null 2>&1 \
202
+ || ! pkg-config --exists webkit2gtk-4.1 2>/dev/null; then
203
+ echo "Desktop UI build toolchain not found (needs cargo, npm, webkit2gtk-4.1-dev)."
204
+ print_ui_hint
205
+ return 0
206
+ fi
207
+
208
+ echo "Building the desktop UI shell (this can take a few minutes) ..."
209
+ npm --prefix "$SCRIPT_DIR/ui" ci >/dev/null 2>&1 \
210
+ || npm --prefix "$SCRIPT_DIR/ui" install >/dev/null 2>&1 \
211
+ || { echo "UI front-end install failed; skipping the optional shell."; print_ui_hint; return 0; }
212
+ if ! npm --prefix "$SCRIPT_DIR/ui" run build >/dev/null 2>&1; then
213
+ echo "UI front-end build failed; skipping the optional shell."; print_ui_hint; return 0
214
+ fi
215
+ if ! ( cd "$shell_src" && cargo build --release --manifest-path src-tauri/Cargo.toml ); then
216
+ echo "Desktop UI shell build failed; the engine + tray remain fully functional."
217
+ print_ui_hint
218
+ return 0
219
+ fi
220
+ install -m 755 "$shell_src/src-tauri/target/release/dictate-ui-shell" "$target"
221
+ echo "Installed desktop UI shell: $target"
222
+ }
223
+
224
+ if [ "$INSTALL_UI" -eq 1 ]; then
225
+ install_desktop_ui
226
+ fi
227
+
228
+ if [ "$SEED_DEFAULT_CONFIG" -eq 1 ]; then
229
+ if [ ! -f "$CONFIG_PATH" ]; then
230
+ if [ ! -f "$DEFAULT_CONFIG_SOURCE" ]; then
231
+ echo "Default config template not found: $DEFAULT_CONFIG_SOURCE"
232
+ exit 1
233
+ fi
234
+ echo "Seeding default config at $CONFIG_PATH ..."
235
+ mkdir -p "$CONFIG_DIR"
236
+ install -m 600 "$DEFAULT_CONFIG_SOURCE" "$CONFIG_PATH"
237
+ if [ "$SESSION_BACKEND" = "wayland" ]; then
238
+ sed -i 's/^push_to_talk_combo: .*/push_to_talk_combo: ctrl+space/' "$CONFIG_PATH"
239
+ fi
240
+ else
241
+ echo "Existing config found at $CONFIG_PATH; leaving it unchanged."
242
+ fi
243
+ fi
244
+
245
+ DICTATE_BIN="$INSTALL_DIR/venv/bin/dictate"
246
+
247
+ run_logged_check() {
248
+ local label="$1"
249
+ local log_path="$2"
250
+ local timeout_seconds="$3"
251
+ shift 3
252
+ echo "Running: $label ..."
253
+ if command -v timeout >/dev/null 2>&1; then
254
+ if ! timeout "${timeout_seconds}s" "$@" >"$log_path" 2>&1; then
255
+ echo "Command failed: $label"
256
+ echo "See: $log_path"
257
+ tail -n 120 "$log_path" || true
258
+ exit 1
259
+ fi
260
+ else
261
+ if ! "$@" >"$log_path" 2>&1; then
262
+ echo "Command failed: $label"
263
+ echo "See: $log_path"
264
+ tail -n 120 "$log_path" || true
265
+ exit 1
266
+ fi
267
+ fi
268
+ }
269
+
270
+ if [ "$PREPARE_TURBO" -eq 1 ]; then
271
+ PREPARE_LOG="/tmp/dictate-install-prepare.log"
272
+ run_logged_check \
273
+ "dictate prepare-model --stt-backend faster-whisper --model turbo --device auto --compute-type int8" \
274
+ "$PREPARE_LOG" \
275
+ 1800 \
276
+ "$DICTATE_BIN" \
277
+ prepare-model \
278
+ --stt-backend faster-whisper \
279
+ --model turbo \
280
+ --device auto \
281
+ --compute-type int8
282
+ fi
283
+
284
+ if [ "$VERIFY" -eq 1 ]; then
285
+ VERIFY_LOG="/tmp/dictate-install-verify.log"
286
+ run_logged_check "dictate --help" "$VERIFY_LOG" 20 "$DICTATE_BIN" --help
287
+ run_logged_check "dictate benchmark --help" "$VERIFY_LOG" 20 "$DICTATE_BIN" benchmark --help
288
+ run_logged_check \
289
+ "dictate doctor --quick --stt-backend faster-whisper --model turbo" \
290
+ "$VERIFY_LOG" \
291
+ 20 \
292
+ "$DICTATE_BIN" \
293
+ doctor \
294
+ --quick \
295
+ --stt-backend faster-whisper \
296
+ --model turbo
297
+ fi
298
+
299
+ if [ "$STARTUP" -eq 1 ]; then
300
+ echo "Done. 'dictate' is now available on your PATH, in the app launcher, and starts when you sign in."
301
+ else
302
+ echo "Done. 'dictate' is now available on your PATH and in the app launcher."
303
+ fi
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ import { basename, dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { spawnSync } from "node:child_process";
5
+
6
+ const scriptDir = dirname(fileURLToPath(import.meta.url));
7
+ const packageRoot = dirname(scriptDir);
8
+ const invokedAs = basename(process.argv[1] || "dictate-install");
9
+ const firstArg = process.argv[2] || "";
10
+
11
+ let command = "install";
12
+ let passthrough = process.argv.slice(2);
13
+ if (invokedAs.includes("update")) {
14
+ command = "update";
15
+ } else if (invokedAs.includes("uninstall")) {
16
+ command = "uninstall";
17
+ } else if (["install", "update", "uninstall", "wizard"].includes(firstArg)) {
18
+ command = firstArg;
19
+ passthrough = process.argv.slice(3);
20
+ }
21
+
22
+ const isWindows = process.platform === "win32";
23
+ const scripts = isWindows
24
+ ? {
25
+ install: ["powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", join(packageRoot, "install.ps1")]],
26
+ update: ["powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", join(packageRoot, "update.ps1")]],
27
+ uninstall: ["powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", join(packageRoot, "uninstall.ps1")]],
28
+ wizard: ["powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", join(packageRoot, "install.ps1"), "-Wizard"]],
29
+ }
30
+ : {
31
+ install: ["bash", [join(packageRoot, "install.sh")]],
32
+ update: ["bash", [join(packageRoot, "update.sh")]],
33
+ uninstall: ["bash", [join(packageRoot, "uninstall.sh")]],
34
+ wizard: null,
35
+ };
36
+
37
+ const selected = scripts[command];
38
+ if (!selected) {
39
+ console.error(`dictate ${command} is not supported on ${process.platform}`);
40
+ process.exit(2);
41
+ }
42
+
43
+ const [exe, args] = selected;
44
+ const result = spawnSync(exe, [...args, ...passthrough], { stdio: "inherit" });
45
+ if (result.error) {
46
+ console.error(result.error.message);
47
+ process.exit(1);
48
+ }
49
+ process.exit(result.status ?? 1);
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@arcforgelabs/dictate",
3
+ "version": "2026.6.3",
4
+ "description": "Installer shim for Dictate desktop dictation.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "homepage": "https://github.com/arcforgelabs/dictate#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/arcforgelabs/dictate.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/arcforgelabs/dictate/issues"
14
+ },
15
+ "bin": {
16
+ "dictate-install": "npm/dictate-lifecycle.mjs",
17
+ "dictate-update": "npm/dictate-lifecycle.mjs",
18
+ "dictate-uninstall": "npm/dictate-lifecycle.mjs"
19
+ },
20
+ "files": [
21
+ "install.ps1",
22
+ "update.ps1",
23
+ "install-windows.ps1",
24
+ "install-windows-wizard.ps1",
25
+ "update-windows.ps1",
26
+ "uninstall.ps1",
27
+ "uninstall-windows.ps1",
28
+ "install.sh",
29
+ "update.sh",
30
+ "uninstall.sh",
31
+ "config/default-config.yaml",
32
+ "assets/dictate.ico",
33
+ "assets/dictate.png",
34
+ "assets/dictate-listening.ico",
35
+ "assets/dictate-listening.png",
36
+ "npm/",
37
+ "README.md",
38
+ "LICENSE"
39
+ ],
40
+ "publishConfig": {
41
+ "access": "public",
42
+ "provenance": true
43
+ },
44
+ "scripts": {
45
+ "pack:check": "npm pack --dry-run"
46
+ },
47
+ "engines": {
48
+ "node": ">=18"
49
+ }
50
+ }
@@ -0,0 +1,83 @@
1
+ param(
2
+ [switch]$Quiet,
3
+ [switch]$RemoveUserData
4
+ )
5
+
6
+ $ErrorActionPreference = "Stop"
7
+
8
+ function Remove-IfExists {
9
+ param([string]$Path)
10
+ if (Test-Path $Path) {
11
+ Remove-Item -Force -Path $Path
12
+ }
13
+ }
14
+
15
+ function Stop-DictateProcesses {
16
+ $currentPid = $PID
17
+ $matches = Get-CimInstance Win32_Process |
18
+ Where-Object {
19
+ $_.ProcessId -ne $currentPid -and (
20
+ $_.Name -in @("dictate.exe", "dictate-controls.exe") -or
21
+ $_.CommandLine -like '*dictate.exe* --type-backend pynput*' -or
22
+ $_.CommandLine -like '*pythonw.exe* -m dictate --type-backend pynput*' -or
23
+ $_.CommandLine -like '*dictate-daemon.cmd*' -or
24
+ $_.CommandLine -like '*dictate-controls*'
25
+ )
26
+ }
27
+ foreach ($match in $matches) {
28
+ Stop-Process -Id $match.ProcessId -Force -ErrorAction SilentlyContinue
29
+ }
30
+ Start-Sleep -Milliseconds 500
31
+ }
32
+
33
+ $programsDir = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs"
34
+ if (-not $env:APPDATA) {
35
+ $programsDir = Join-Path $HOME "AppData\Roaming\Microsoft\Windows\Start Menu\Programs"
36
+ }
37
+ $startupDir = Join-Path $programsDir "Startup"
38
+
39
+ Remove-IfExists -Path (Join-Path $programsDir "Dictate.lnk")
40
+ Remove-IfExists -Path (Join-Path $programsDir "Dictate Controls.lnk")
41
+ Remove-IfExists -Path (Join-Path $startupDir "Dictate.lnk")
42
+
43
+ $uninstallKey = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\Dictate"
44
+ if (Test-Path $uninstallKey) {
45
+ Remove-Item -Recurse -Force -Path $uninstallKey
46
+ }
47
+
48
+ Stop-DictateProcesses
49
+
50
+ $venvDir = Join-Path $PSScriptRoot ".venv"
51
+ if (Test-Path $venvDir) {
52
+ Remove-Item -Recurse -Force -Path $venvDir
53
+ }
54
+
55
+ $localAppData = $env:LOCALAPPDATA
56
+ if (-not $localAppData) {
57
+ $localAppData = Join-Path $HOME "AppData\Local"
58
+ }
59
+ $appData = $env:APPDATA
60
+ if (-not $appData) {
61
+ $appData = Join-Path $HOME "AppData\Roaming"
62
+ }
63
+ $dataDir = Join-Path $localAppData "dictate"
64
+ $configDir = Join-Path $appData "dictate"
65
+
66
+ if ($RemoveUserData) {
67
+ if (Test-Path $dataDir) {
68
+ Remove-Item -Recurse -Force -Path $dataDir
69
+ }
70
+ if (Test-Path $configDir) {
71
+ Remove-Item -Recurse -Force -Path $configDir
72
+ }
73
+ }
74
+
75
+ if (-not $Quiet) {
76
+ Write-Host "Dictate shortcuts, startup entry, app registration, and runtime venv removed."
77
+ if ($RemoveUserData) {
78
+ Write-Host "User config/data removed."
79
+ } else {
80
+ Write-Host "User config/data preserved. Re-run with -RemoveUserData to remove it."
81
+ }
82
+ Write-Host "Install source files were left in place: $PSScriptRoot"
83
+ }
package/uninstall.ps1 ADDED
@@ -0,0 +1,51 @@
1
+ param(
2
+ [string]$InstallRoot,
3
+ [switch]$Quiet,
4
+ [switch]$RemoveUserData
5
+ )
6
+
7
+ $ErrorActionPreference = "Stop"
8
+
9
+ if (-not $InstallRoot) {
10
+ $base = $env:LOCALAPPDATA
11
+ if (-not $base) {
12
+ $base = Join-Path $HOME "AppData\Local"
13
+ }
14
+ $InstallRoot = Join-Path $base "Dictate"
15
+ }
16
+
17
+ $installRootPath = [System.IO.Path]::GetFullPath($InstallRoot)
18
+ $sourceDir = Join-Path $installRootPath "source"
19
+ $uninstaller = Join-Path $sourceDir "uninstall-windows.ps1"
20
+
21
+ if (Test-Path $uninstaller) {
22
+ $args = @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $uninstaller)
23
+ if ($Quiet) { $args += "-Quiet" }
24
+ if ($RemoveUserData) { $args += "-RemoveUserData" }
25
+ & powershell @args
26
+ if ($LASTEXITCODE -ne 0) {
27
+ throw "Dictate Windows uninstaller failed with exit code $LASTEXITCODE."
28
+ }
29
+ } else {
30
+ if (-not $Quiet) {
31
+ Write-Host "Dictate source uninstaller not found: $uninstaller"
32
+ Write-Host "Removing known Start Menu, startup, and Installed Apps entries."
33
+ }
34
+ $programsDir = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs"
35
+ if (-not $env:APPDATA) {
36
+ $programsDir = Join-Path $HOME "AppData\Roaming\Microsoft\Windows\Start Menu\Programs"
37
+ }
38
+ Remove-Item -Force -ErrorAction SilentlyContinue -Path `
39
+ (Join-Path $programsDir "Dictate.lnk"), `
40
+ (Join-Path $programsDir "Dictate Controls.lnk"), `
41
+ (Join-Path $programsDir "Startup\Dictate.lnk")
42
+ Remove-Item -Recurse -Force -ErrorAction SilentlyContinue -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\Dictate"
43
+ }
44
+
45
+ if (Test-Path $installRootPath) {
46
+ Remove-Item -Recurse -Force -Path $installRootPath
47
+ }
48
+
49
+ if (-not $Quiet) {
50
+ Write-Host "Dictate removed from: $installRootPath"
51
+ }
package/uninstall.sh ADDED
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env bash
2
+ # Remove Dictate launchers/autostart and installed runtime files.
3
+ set -euo pipefail
4
+
5
+ INSTALL_DIR="$HOME/.local/share/dictate"
6
+ BIN_PATH="$HOME/.local/bin/dictate"
7
+ UI_SERVER_BIN_PATH="$HOME/.local/bin/dictate-ui-server"
8
+ DESKTOP_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
9
+ AUTOSTART_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/autostart"
10
+ CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/dictate"
11
+ REMOVE_USER_DATA=0
12
+ QUIET=0
13
+
14
+ usage() {
15
+ cat <<EOF
16
+ Usage: $0 [--remove-user-data] [--quiet]
17
+
18
+ Removes Dictate launcher/autostart entries, ~/.local/bin/dictate, and the
19
+ installed runtime venv/icons. User config, logs, history, and downloaded models
20
+ are preserved unless --remove-user-data is passed.
21
+ EOF
22
+ }
23
+
24
+ while [ "$#" -gt 0 ]; do
25
+ case "$1" in
26
+ --remove-user-data)
27
+ REMOVE_USER_DATA=1
28
+ ;;
29
+ --quiet)
30
+ QUIET=1
31
+ ;;
32
+ -h|--help)
33
+ usage
34
+ exit 0
35
+ ;;
36
+ *)
37
+ usage
38
+ exit 1
39
+ ;;
40
+ esac
41
+ shift
42
+ done
43
+
44
+ say() {
45
+ if [ "$QUIET" -eq 0 ]; then
46
+ printf '%s\n' "$1"
47
+ fi
48
+ }
49
+
50
+ remove_file() {
51
+ local path="$1"
52
+ if [ -e "$path" ] || [ -L "$path" ]; then
53
+ rm -f "$path"
54
+ say "Removed $path"
55
+ fi
56
+ }
57
+
58
+ remove_dir_if_empty() {
59
+ local path="$1"
60
+ if [ -d "$path" ]; then
61
+ rmdir "$path" 2>/dev/null || true
62
+ fi
63
+ }
64
+
65
+ # Stop a running source-install daemon/server before pulling its venv out from
66
+ # under it (only our venv path is matched, never a packaged /usr install).
67
+ stop_source_processes() {
68
+ local pattern="$INSTALL_DIR/venv"
69
+ if command -v pkill >/dev/null 2>&1 && pgrep -f "$pattern" >/dev/null 2>&1; then
70
+ say "Stopping running source-install Dictate processes ..."
71
+ pkill -TERM -f "$pattern" 2>/dev/null || true
72
+ for _ in 1 2 3 4 5; do
73
+ pgrep -f "$pattern" >/dev/null 2>&1 || break
74
+ sleep 0.3
75
+ done
76
+ pkill -KILL -f "$pattern" 2>/dev/null || true
77
+ fi
78
+ }
79
+
80
+ remove_managed_symlink() {
81
+ local link="$1" want="$2"
82
+ if [ -L "$link" ]; then
83
+ local target
84
+ target="$(readlink "$link" || true)"
85
+ if [ "$target" = "$want" ]; then
86
+ remove_file "$link"
87
+ else
88
+ say "Leaving $link because it points to $target"
89
+ fi
90
+ elif [ -e "$link" ]; then
91
+ say "Leaving $link because it is not a Dictate-managed symlink"
92
+ fi
93
+ }
94
+
95
+ stop_source_processes
96
+
97
+ remove_file "$DESKTOP_DIR/dictate.desktop"
98
+ remove_file "$DESKTOP_DIR/dictate-settings.desktop"
99
+ remove_file "$AUTOSTART_DIR/dictate.desktop"
100
+
101
+ remove_managed_symlink "$BIN_PATH" "$INSTALL_DIR/venv/bin/dictate"
102
+ remove_managed_symlink "$UI_SERVER_BIN_PATH" "$INSTALL_DIR/venv/bin/dictate-ui-server"
103
+
104
+ rm -rf "$INSTALL_DIR/venv" "$INSTALL_DIR/share/icons" "$INSTALL_DIR/logs"
105
+ remove_dir_if_empty "$INSTALL_DIR/share"
106
+ say "Removed installed runtime files from $INSTALL_DIR"
107
+
108
+ if [ "$REMOVE_USER_DATA" -eq 1 ]; then
109
+ rm -rf "$CONFIG_DIR" "$INSTALL_DIR"
110
+ say "Removed user config/data: $CONFIG_DIR and $INSTALL_DIR"
111
+ else
112
+ say "Preserved user config/data: $CONFIG_DIR and $INSTALL_DIR"
113
+ fi
114
+
115
+ update-desktop-database "$DESKTOP_DIR" 2>/dev/null || true
116
+
117
+ say "Dictate uninstall complete."