@iamsamuelrodda/dictate 2026.5.18-1

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.
@@ -0,0 +1,330 @@
1
+ param(
2
+ [switch]$NoVerify,
3
+ [switch]$NoPrepareTurbo,
4
+ [switch]$NoShortcut,
5
+ [switch]$NoStartup,
6
+ [switch]$RecreateVenv
7
+ )
8
+
9
+ $ErrorActionPreference = "Stop"
10
+
11
+ function Test-PythonVersion {
12
+ param(
13
+ [string]$Exe,
14
+ [string[]]$ArgumentList
15
+ )
16
+
17
+ & $Exe @ArgumentList -c "import sys; raise SystemExit(0 if (3, 11) <= sys.version_info < (3, 13) else 1)" *> $null
18
+ return ($LASTEXITCODE -eq 0)
19
+ }
20
+
21
+ function Resolve-Python {
22
+ $candidates = @()
23
+
24
+ if ($env:PYTHON) {
25
+ $candidates += [pscustomobject]@{ Exe = $env:PYTHON; ArgumentList = @() }
26
+ }
27
+
28
+ $python = Get-Command python -ErrorAction SilentlyContinue
29
+ if ($python) {
30
+ $candidates += [pscustomobject]@{ Exe = $python.Source; ArgumentList = @() }
31
+ }
32
+
33
+ $python3 = Get-Command python3 -ErrorAction SilentlyContinue
34
+ if ($python3) {
35
+ $candidates += [pscustomobject]@{ Exe = $python3.Source; ArgumentList = @() }
36
+ }
37
+
38
+ $py = Get-Command py -ErrorAction SilentlyContinue
39
+ if ($py) {
40
+ $candidates += [pscustomobject]@{ Exe = $py.Source; ArgumentList = @("-3.12") }
41
+ $candidates += [pscustomobject]@{ Exe = $py.Source; ArgumentList = @("-3.11") }
42
+ }
43
+
44
+ foreach ($candidate in $candidates) {
45
+ if (Test-PythonVersion -Exe $candidate.Exe -ArgumentList $candidate.ArgumentList) {
46
+ return $candidate
47
+ }
48
+ }
49
+
50
+ throw "Python 3.11 or 3.12 was not found. Install Python from python.org or winget, then rerun this script."
51
+ }
52
+
53
+ function Invoke-Checked {
54
+ param(
55
+ [string]$Exe,
56
+ [string[]]$ArgumentList,
57
+ [string]$Description
58
+ )
59
+
60
+ Write-Host "==> $Description"
61
+ & $Exe @ArgumentList
62
+ if ($LASTEXITCODE -ne 0) {
63
+ throw "$Description failed with exit code $LASTEXITCODE."
64
+ }
65
+ }
66
+
67
+ function Test-VcRuntime {
68
+ $system32 = Join-Path $env:WINDIR "System32"
69
+ $required = @("vcruntime140.dll", "vcruntime140_1.dll", "msvcp140.dll")
70
+ foreach ($dll in $required) {
71
+ if (-not (Test-Path (Join-Path $system32 $dll))) {
72
+ return $false
73
+ }
74
+ }
75
+ return $true
76
+ }
77
+
78
+ function Ensure-VcRuntime {
79
+ if (Test-VcRuntime) {
80
+ Write-Host "==> Microsoft Visual C++ runtime already installed"
81
+ return
82
+ }
83
+
84
+ $installer = Join-Path $env:TEMP "vc_redist.x64.exe"
85
+ $url = "https://aka.ms/vs/17/release/vc_redist.x64.exe"
86
+ Write-Host "==> Installing Microsoft Visual C++ runtime"
87
+ Invoke-WebRequest -UseBasicParsing -Uri $url -OutFile $installer
88
+ $process = Start-Process -FilePath $installer -ArgumentList "/install", "/quiet", "/norestart" -Wait -PassThru
89
+ if (($process.ExitCode -ne 0) -and ($process.ExitCode -ne 3010)) {
90
+ throw "Microsoft Visual C++ runtime install failed with exit code $($process.ExitCode)."
91
+ }
92
+ }
93
+
94
+ function Get-AppDataConfigPath {
95
+ $base = $env:APPDATA
96
+ if (-not $base) {
97
+ $base = Join-Path $HOME "AppData\Roaming"
98
+ }
99
+ return Join-Path $base "dictate\config.yaml"
100
+ }
101
+
102
+ function Seed-Config {
103
+ $configPath = Get-AppDataConfigPath
104
+ if (Test-Path $configPath) {
105
+ Write-Host "==> Config already exists: $configPath"
106
+ return
107
+ }
108
+
109
+ $defaultConfig = Join-Path $PSScriptRoot "config\default-config.yaml"
110
+ $configDir = Split-Path -Parent $configPath
111
+ New-Item -ItemType Directory -Force -Path $configDir | Out-Null
112
+ Copy-Item -Path $defaultConfig -Destination $configPath
113
+ Write-Host "==> Seeded config: $configPath"
114
+ }
115
+
116
+ function Write-LauncherScripts {
117
+ param([string]$ScriptsDir)
118
+
119
+ $daemonPath = Join-Path $ScriptsDir "dictate-daemon.cmd"
120
+ $oncePath = Join-Path $ScriptsDir "dictate-once.cmd"
121
+ $controlsPath = Join-Path $ScriptsDir "dictate-controls.cmd"
122
+ $trayPath = Join-Path $ScriptsDir "dictate-tray.cmd"
123
+ $trayVbsPath = Join-Path $ScriptsDir "dictate-tray.vbs"
124
+
125
+ Set-Content -Path $trayPath -Encoding ASCII -Value @(
126
+ "@echo off",
127
+ 'set "SCRIPT_DIR=%~dp0"',
128
+ '"%SCRIPT_DIR%dictate.exe" --type-backend pynput %*'
129
+ )
130
+
131
+ Set-Content -Path $daemonPath -Encoding ASCII -Value @(
132
+ "@echo off",
133
+ 'set "SCRIPT_DIR=%~dp0"',
134
+ '"%SCRIPT_DIR%dictate.exe" --no-tray --type-backend pynput %*'
135
+ )
136
+
137
+ Set-Content -Path $oncePath -Encoding ASCII -Value @(
138
+ "@echo off",
139
+ 'set "SCRIPT_DIR=%~dp0"',
140
+ '"%SCRIPT_DIR%dictate.exe" --once %*'
141
+ )
142
+
143
+ Set-Content -Path $controlsPath -Encoding ASCII -Value @(
144
+ "@echo off",
145
+ 'set "SCRIPT_DIR=%~dp0"',
146
+ 'start "" "%SCRIPT_DIR%dictate-controls.exe" %*'
147
+ )
148
+
149
+ Set-Content -Path $trayVbsPath -Encoding ASCII -Value @(
150
+ 'Set shell = CreateObject("WScript.Shell")',
151
+ 'Set fso = CreateObject("Scripting.FileSystemObject")',
152
+ 'scriptDir = fso.GetParentFolderName(WScript.ScriptFullName)',
153
+ 'venvDir = fso.GetParentFolderName(scriptDir)',
154
+ 'shell.CurrentDirectory = fso.GetParentFolderName(venvDir)',
155
+ 'shell.Run """" & scriptDir & "\pythonw.exe"" -m dictate --type-backend pynput", 0, False'
156
+ )
157
+
158
+ Write-Host "==> Wrote launchers:"
159
+ Write-Host " $trayPath"
160
+ Write-Host " $trayVbsPath"
161
+ Write-Host " $daemonPath"
162
+ Write-Host " $oncePath"
163
+ Write-Host " $controlsPath"
164
+ }
165
+
166
+ function Get-StartMenuProgramsDir {
167
+ $programsDir = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs"
168
+ if (-not $env:APPDATA) {
169
+ $programsDir = Join-Path $HOME "AppData\Roaming\Microsoft\Windows\Start Menu\Programs"
170
+ }
171
+ return $programsDir
172
+ }
173
+
174
+ function Get-StartupDir {
175
+ return (Join-Path (Get-StartMenuProgramsDir) "Startup")
176
+ }
177
+
178
+ function New-DictateShortcut {
179
+ param(
180
+ [string]$ShortcutPath,
181
+ [string]$TargetPath,
182
+ [string]$Arguments = "",
183
+ [string]$WorkingDirectory,
184
+ [string]$Description
185
+ )
186
+
187
+ $iconPath = Join-Path $PSScriptRoot "assets\dictate.ico"
188
+ $shell = New-Object -ComObject WScript.Shell
189
+ $shortcut = $shell.CreateShortcut($ShortcutPath)
190
+ $shortcut.TargetPath = $TargetPath
191
+ $shortcut.Arguments = $Arguments
192
+ $shortcut.WorkingDirectory = $WorkingDirectory
193
+ $shortcut.Description = $Description
194
+ if (Test-Path $iconPath) {
195
+ $shortcut.IconLocation = $iconPath
196
+ }
197
+ $shortcut.Save()
198
+ }
199
+
200
+ function Install-StartMenuShortcut {
201
+ param(
202
+ [string]$TargetPath,
203
+ [string]$Arguments = "",
204
+ [string]$WorkingDirectory
205
+ )
206
+
207
+ $programsDir = Get-StartMenuProgramsDir
208
+ New-Item -ItemType Directory -Force -Path $programsDir | Out-Null
209
+
210
+ $shortcutPath = Join-Path $programsDir "Dictate.lnk"
211
+ $legacyShortcutPath = Join-Path $programsDir "Dictate Controls.lnk"
212
+
213
+ if ($NoShortcut) {
214
+ Remove-Item -Force -ErrorAction SilentlyContinue -Path $shortcutPath, $legacyShortcutPath
215
+ Write-Host "==> Removed Start Menu shortcuts"
216
+ return
217
+ }
218
+
219
+ Remove-Item -Force -ErrorAction SilentlyContinue -Path $legacyShortcutPath
220
+ New-DictateShortcut -ShortcutPath $shortcutPath -TargetPath $TargetPath -Arguments $Arguments -WorkingDirectory $WorkingDirectory -Description "Start Dictate push-to-talk tray"
221
+
222
+ Write-Host "==> Installed Start Menu shortcut: $shortcutPath"
223
+ }
224
+
225
+ function Install-StartupShortcut {
226
+ param(
227
+ [string]$TargetPath,
228
+ [string]$Arguments = "",
229
+ [string]$WorkingDirectory
230
+ )
231
+
232
+ if ($NoShortcut -or $NoStartup) {
233
+ $startupDir = Get-StartupDir
234
+ $shortcutPath = Join-Path $startupDir "Dictate.lnk"
235
+ if (Test-Path $shortcutPath) {
236
+ Remove-Item -Force $shortcutPath
237
+ Write-Host "==> Removed startup shortcut: $shortcutPath"
238
+ }
239
+ return
240
+ }
241
+
242
+ $startupDir = Get-StartupDir
243
+ New-Item -ItemType Directory -Force -Path $startupDir | Out-Null
244
+
245
+ $shortcutPath = Join-Path $startupDir "Dictate.lnk"
246
+ New-DictateShortcut -ShortcutPath $shortcutPath -TargetPath $TargetPath -Arguments $Arguments -WorkingDirectory $WorkingDirectory -Description "Start Dictate automatically at sign-in"
247
+
248
+ Write-Host "==> Installed startup shortcut: $shortcutPath"
249
+ }
250
+
251
+ function Register-InstalledApp {
252
+ param(
253
+ [string]$InstallLocation,
254
+ [string]$DisplayIcon
255
+ )
256
+
257
+ $keyPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\Dictate"
258
+ New-Item -Force -Path $keyPath | Out-Null
259
+ New-ItemProperty -Force -Path $keyPath -Name "DisplayName" -Value "Dictate" -PropertyType String | Out-Null
260
+ New-ItemProperty -Force -Path $keyPath -Name "DisplayVersion" -Value "2026.5.18-1" -PropertyType String | Out-Null
261
+ New-ItemProperty -Force -Path $keyPath -Name "Publisher" -Value "Arc Forge Labs" -PropertyType String | Out-Null
262
+ New-ItemProperty -Force -Path $keyPath -Name "InstallLocation" -Value $InstallLocation -PropertyType String | Out-Null
263
+ if (Test-Path $DisplayIcon) {
264
+ New-ItemProperty -Force -Path $keyPath -Name "DisplayIcon" -Value $DisplayIcon -PropertyType String | Out-Null
265
+ }
266
+ $uninstallScript = Join-Path $InstallLocation "uninstall-windows.ps1"
267
+ if (Test-Path $uninstallScript) {
268
+ $uninstallCommand = "powershell -NoProfile -ExecutionPolicy Bypass -File `"$uninstallScript`""
269
+ New-ItemProperty -Force -Path $keyPath -Name "UninstallString" -Value $uninstallCommand -PropertyType String | Out-Null
270
+ New-ItemProperty -Force -Path $keyPath -Name "QuietUninstallString" -Value "$uninstallCommand -Quiet" -PropertyType String | Out-Null
271
+ }
272
+ New-ItemProperty -Force -Path $keyPath -Name "NoModify" -Value 1 -PropertyType DWord | Out-Null
273
+ New-ItemProperty -Force -Path $keyPath -Name "NoRepair" -Value 1 -PropertyType DWord | Out-Null
274
+ Write-Host "==> Registered Dictate in Windows Installed Apps"
275
+ }
276
+
277
+ $pythonCommand = Resolve-Python
278
+ $venvDir = Join-Path $PSScriptRoot ".venv"
279
+ $scriptsDir = Join-Path $venvDir "Scripts"
280
+ $venvPython = Join-Path $scriptsDir "python.exe"
281
+
282
+ Ensure-VcRuntime
283
+
284
+ if ($RecreateVenv -and (Test-Path $venvDir)) {
285
+ Write-Host "==> Removing existing virtual environment: $venvDir"
286
+ Remove-Item -Recurse -Force $venvDir
287
+ }
288
+
289
+ if (-not (Test-Path $venvPython)) {
290
+ Invoke-Checked -Exe $pythonCommand.Exe -ArgumentList @($pythonCommand.ArgumentList + @("-m", "venv", $venvDir)) -Description "Creating virtual environment"
291
+ }
292
+
293
+ Invoke-Checked -Exe $venvPython -ArgumentList @("-m", "pip", "install", "--upgrade", "pip") -Description "Upgrading pip"
294
+ Invoke-Checked -Exe $venvPython -ArgumentList @("-m", "pip", "install", "-e", "${PSScriptRoot}[windows]") -Description "Installing Dictate Windows package"
295
+
296
+ Seed-Config
297
+ Write-LauncherScripts -ScriptsDir $scriptsDir
298
+ $trayVbs = Join-Path $scriptsDir "dictate-tray.vbs"
299
+ $wscript = Join-Path $env:WINDIR "System32\wscript.exe"
300
+ $trayArgs = "`"$trayVbs`""
301
+ Install-StartMenuShortcut -TargetPath $wscript -Arguments $trayArgs -WorkingDirectory $PSScriptRoot
302
+ Install-StartupShortcut -TargetPath $wscript -Arguments $trayArgs -WorkingDirectory $PSScriptRoot
303
+ Register-InstalledApp -InstallLocation $PSScriptRoot -DisplayIcon (Join-Path $PSScriptRoot "assets\dictate.ico")
304
+
305
+ if (-not $NoPrepareTurbo) {
306
+ Invoke-Checked -Exe $venvPython -ArgumentList @("-m", "dictate", "prepare-model", "--stt-backend", "faster-whisper", "--model", "turbo", "--device", "auto", "--compute-type", "int8") -Description "Preparing faster-whisper turbo model"
307
+ }
308
+
309
+ if (-not $NoVerify) {
310
+ Invoke-Checked -Exe $venvPython -ArgumentList @("-m", "dictate", "doctor", "--quick", "--type-backend", "pynput") -Description "Running Dictate doctor"
311
+ }
312
+
313
+ Write-Host ""
314
+ Write-Host "Dictate is installed."
315
+ if ($NoShortcut -or $NoStartup) {
316
+ Write-Host "Dictate startup shortcut was not installed."
317
+ } else {
318
+ Write-Host "Dictate starts automatically when you sign in."
319
+ }
320
+ Write-Host "Start push-to-talk with a Windows tray icon from the Start Menu shortcut named 'Dictate', or run:"
321
+ Write-Host " .\.venv\Scripts\dictate-tray.cmd"
322
+ Write-Host ""
323
+ Write-Host "Headless push-to-talk daemon:"
324
+ Write-Host " .\.venv\Scripts\dictate-daemon.cmd"
325
+ Write-Host ""
326
+ Write-Host "Control panel:"
327
+ Write-Host " .\.venv\Scripts\dictate-controls.cmd"
328
+ Write-Host ""
329
+ Write-Host "One-shot mode:"
330
+ Write-Host " .\.venv\Scripts\dictate-once.cmd"
package/install.ps1 ADDED
@@ -0,0 +1,87 @@
1
+ param(
2
+ [string]$InstallRoot,
3
+ [string]$ArchiveUrl,
4
+ [switch]$NoVerify,
5
+ [switch]$NoPrepareTurbo,
6
+ [switch]$NoShortcut,
7
+ [switch]$NoStartup,
8
+ [switch]$Wizard,
9
+ [switch]$RecreateVenv
10
+ )
11
+
12
+ $ErrorActionPreference = "Stop"
13
+ $DictateVersion = "2026.5.18-1"
14
+
15
+ if (-not $ArchiveUrl) {
16
+ $ArchiveUrl = "https://github.com/arcforgelabs/dictate/archive/refs/tags/v$DictateVersion.zip"
17
+ }
18
+
19
+ if (-not $InstallRoot) {
20
+ $base = $env:LOCALAPPDATA
21
+ if (-not $base) {
22
+ $base = Join-Path $HOME "AppData\Local"
23
+ }
24
+ $InstallRoot = Join-Path $base "Dictate"
25
+ }
26
+
27
+ $installRootPath = [System.IO.Path]::GetFullPath($InstallRoot)
28
+ $sourceDir = Join-Path $installRootPath "source"
29
+ $stagingRoot = Join-Path $env:TEMP ("dictate-install-" + [guid]::NewGuid().ToString("N"))
30
+ $archivePath = Join-Path $stagingRoot "dictate.zip"
31
+
32
+ Write-Host "==> Installing Dictate to $installRootPath"
33
+ New-Item -ItemType Directory -Force -Path $stagingRoot | Out-Null
34
+ New-Item -ItemType Directory -Force -Path $installRootPath | Out-Null
35
+
36
+ try {
37
+ if (Test-Path -LiteralPath $ArchiveUrl) {
38
+ Write-Host "==> Copying local archive $ArchiveUrl"
39
+ Copy-Item -Force -LiteralPath $ArchiveUrl -Destination $archivePath
40
+ } else {
41
+ Write-Host "==> Downloading $ArchiveUrl"
42
+ Invoke-WebRequest -UseBasicParsing -Uri $ArchiveUrl -OutFile $archivePath
43
+ }
44
+
45
+ Write-Host "==> Expanding source archive"
46
+ Expand-Archive -Force -Path $archivePath -DestinationPath $stagingRoot
47
+ $expanded = Get-ChildItem -Path $stagingRoot -Directory |
48
+ Where-Object { Test-Path (Join-Path $_.FullName "install-windows.ps1") } |
49
+ Select-Object -First 1
50
+ if (-not $expanded) {
51
+ throw "Downloaded archive did not contain install-windows.ps1."
52
+ }
53
+
54
+ if (Test-Path $sourceDir) {
55
+ Write-Host "==> Replacing existing managed source: $sourceDir"
56
+ Remove-Item -Recurse -Force $sourceDir
57
+ }
58
+ Move-Item -Path $expanded.FullName -Destination $sourceDir
59
+
60
+ $installerScript = if ($Wizard) {
61
+ Join-Path $sourceDir "install-windows-wizard.ps1"
62
+ } else {
63
+ Join-Path $sourceDir "install-windows.ps1"
64
+ }
65
+ $installerArgs = @("-ExecutionPolicy", "Bypass", "-File", $installerScript)
66
+ if (-not $Wizard) {
67
+ if ($NoVerify) { $installerArgs += "-NoVerify" }
68
+ if ($NoPrepareTurbo) { $installerArgs += "-NoPrepareTurbo" }
69
+ if ($NoShortcut) { $installerArgs += "-NoShortcut" }
70
+ if ($NoStartup) { $installerArgs += "-NoStartup" }
71
+ if ($RecreateVenv) { $installerArgs += "-RecreateVenv" }
72
+ }
73
+
74
+ Write-Host "==> Running Dictate Windows installer"
75
+ & powershell @installerArgs
76
+ if ($LASTEXITCODE -ne 0) {
77
+ throw "Dictate Windows installer failed with exit code $LASTEXITCODE."
78
+ }
79
+ } finally {
80
+ if (Test-Path $stagingRoot) {
81
+ Remove-Item -Recurse -Force $stagingRoot
82
+ }
83
+ }
84
+
85
+ Write-Host ""
86
+ Write-Host "Dictate install source: $sourceDir"
87
+ Write-Host "Start Dictate from the Start Menu shortcut named 'Dictate'."
package/install.sh ADDED
@@ -0,0 +1,233 @@
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
+ PYTHON_BIN="${PYTHON_BIN:-python3}"
22
+
23
+ # Prefer the distro Python so --system-site-packages can see modules such as
24
+ # python3-gi from /usr/lib/python3/dist-packages.
25
+ if [ -x /usr/bin/python3 ]; then
26
+ PYTHON_BIN="/usr/bin/python3"
27
+ fi
28
+
29
+ usage() {
30
+ cat <<EOF
31
+ Usage: $0 [--no-verify] [--no-prepare-turbo] [--no-seed-default-config] [--no-startup] [--session-backend auto|x11|wayland]
32
+
33
+ Installs dictate into ~/.local/share/dictate, links ~/.local/bin/dictate,
34
+ seeds the default config on first install, creates app launcher/autostart entries,
35
+ and prepares the faster-whisper turbo model unless disabled.
36
+ EOF
37
+ }
38
+
39
+ detect_session_backend() {
40
+ if [ -n "${WAYLAND_DISPLAY:-}" ] || [ "${XDG_SESSION_TYPE:-}" = "wayland" ]; then
41
+ printf 'wayland\n'
42
+ return
43
+ fi
44
+ if [ -n "${DISPLAY:-}" ] || [ "${XDG_SESSION_TYPE:-}" = "x11" ]; then
45
+ printf 'x11\n'
46
+ return
47
+ fi
48
+ if [ -n "${XDG_SESSION_ID:-}" ] && command -v loginctl >/dev/null 2>&1; then
49
+ local detected
50
+ detected="$(loginctl show-session "$XDG_SESSION_ID" -p Type --value 2>/dev/null || true)"
51
+ if [ "$detected" = "wayland" ] || [ "$detected" = "x11" ]; then
52
+ printf '%s\n' "$detected"
53
+ return
54
+ fi
55
+ fi
56
+ printf 'unknown\n'
57
+ }
58
+
59
+ SESSION_BACKEND="auto"
60
+
61
+ while [ "$#" -gt 0 ]; do
62
+ case "$1" in
63
+ --no-verify)
64
+ VERIFY=0
65
+ ;;
66
+ --no-prepare-turbo)
67
+ PREPARE_TURBO=0
68
+ ;;
69
+ --no-seed-default-config)
70
+ SEED_DEFAULT_CONFIG=0
71
+ ;;
72
+ --no-startup)
73
+ STARTUP=0
74
+ ;;
75
+ --session-backend)
76
+ shift
77
+ SESSION_BACKEND="${1:-}"
78
+ ;;
79
+ --session-backend=*)
80
+ SESSION_BACKEND="${1#*=}"
81
+ ;;
82
+ -h|--help)
83
+ usage
84
+ exit 0
85
+ ;;
86
+ *)
87
+ usage
88
+ exit 1
89
+ ;;
90
+ esac
91
+ shift
92
+ done
93
+
94
+ if [ "$SESSION_BACKEND" = "auto" ]; then
95
+ SESSION_BACKEND="$(detect_session_backend)"
96
+ fi
97
+
98
+ PIP_TARGET="$SCRIPT_DIR"
99
+ if [ "$SESSION_BACKEND" = "x11" ]; then
100
+ PIP_TARGET="${SCRIPT_DIR}[x11]"
101
+ elif [ "$SESSION_BACKEND" = "wayland" ]; then
102
+ PIP_TARGET="${SCRIPT_DIR}[wayland]"
103
+ elif [ "$SESSION_BACKEND" = "unknown" ]; then
104
+ PIP_TARGET="${SCRIPT_DIR}[x11,wayland]"
105
+ fi
106
+
107
+ echo "Detected install session backend: $SESSION_BACKEND"
108
+
109
+ echo "Creating venv at $INSTALL_DIR ..."
110
+ uv venv "$INSTALL_DIR/venv" --python "$PYTHON_BIN" --system-site-packages --quiet
111
+
112
+ echo "Installing dictate from $PIP_TARGET ..."
113
+ uv pip install "$PIP_TARGET" --python "$INSTALL_DIR/venv/bin/python" --quiet
114
+
115
+ echo "Linking binary ..."
116
+ mkdir -p "$BIN_DIR"
117
+ ln -sf "$INSTALL_DIR/venv/bin/dictate" "$BIN_DIR/dictate"
118
+
119
+ echo "Installing icon ..."
120
+ mkdir -p "$ICON_DIR"
121
+ rm -f "$ICON_DIR/dictate-controls.png" "$ICON_DIR/dictate.png"
122
+ install -m 644 "$SCRIPT_DIR/assets/dictate.png" "$ICON_PATH"
123
+
124
+ echo "Installing desktop entry ..."
125
+ mkdir -p "$DESKTOP_DIR"
126
+ rm -f "$DESKTOP_DIR/dictate-settings.desktop"
127
+ cat > "$DESKTOP_DIR/dictate.desktop" <<EOF
128
+ [Desktop Entry]
129
+ Name=Dictate
130
+ Comment=Dictate into the focused app
131
+ Exec=$HOME/.local/bin/dictate
132
+ Icon=$ICON_PATH
133
+ Type=Application
134
+ Categories=AudioVideo;Audio;
135
+ Keywords=voice;speech;transcription;dictation;asr;whisper;canary;
136
+ Terminal=false
137
+ EOF
138
+
139
+ update-desktop-database "$DESKTOP_DIR" 2>/dev/null || true
140
+
141
+ if [ "$STARTUP" -eq 1 ]; then
142
+ echo "Installing autostart entry ..."
143
+ mkdir -p "$AUTOSTART_DIR"
144
+ cat > "$AUTOSTART_DIR/dictate.desktop" <<EOF
145
+ [Desktop Entry]
146
+ Name=Dictate
147
+ Comment=Dictate into the focused app
148
+ Exec=$HOME/.local/bin/dictate
149
+ Icon=$ICON_PATH
150
+ Type=Application
151
+ Categories=AudioVideo;Audio;
152
+ Keywords=voice;speech;transcription;dictation;asr;whisper;canary;
153
+ Terminal=false
154
+ X-GNOME-Autostart-enabled=true
155
+ EOF
156
+ fi
157
+
158
+ if [ "$SEED_DEFAULT_CONFIG" -eq 1 ]; then
159
+ if [ ! -f "$CONFIG_PATH" ]; then
160
+ if [ ! -f "$DEFAULT_CONFIG_SOURCE" ]; then
161
+ echo "Default config template not found: $DEFAULT_CONFIG_SOURCE"
162
+ exit 1
163
+ fi
164
+ echo "Seeding default config at $CONFIG_PATH ..."
165
+ mkdir -p "$CONFIG_DIR"
166
+ install -m 600 "$DEFAULT_CONFIG_SOURCE" "$CONFIG_PATH"
167
+ if [ "$SESSION_BACKEND" = "wayland" ]; then
168
+ sed -i 's/^push_to_talk_combo: .*/push_to_talk_combo: ctrl+space/' "$CONFIG_PATH"
169
+ fi
170
+ else
171
+ echo "Existing config found at $CONFIG_PATH; leaving it unchanged."
172
+ fi
173
+ fi
174
+
175
+ DICTATE_BIN="$INSTALL_DIR/venv/bin/dictate"
176
+
177
+ run_logged_check() {
178
+ local label="$1"
179
+ local log_path="$2"
180
+ local timeout_seconds="$3"
181
+ shift 3
182
+ echo "Running: $label ..."
183
+ if command -v timeout >/dev/null 2>&1; then
184
+ if ! timeout "${timeout_seconds}s" "$@" >"$log_path" 2>&1; then
185
+ echo "Command failed: $label"
186
+ echo "See: $log_path"
187
+ tail -n 120 "$log_path" || true
188
+ exit 1
189
+ fi
190
+ else
191
+ if ! "$@" >"$log_path" 2>&1; then
192
+ echo "Command failed: $label"
193
+ echo "See: $log_path"
194
+ tail -n 120 "$log_path" || true
195
+ exit 1
196
+ fi
197
+ fi
198
+ }
199
+
200
+ if [ "$PREPARE_TURBO" -eq 1 ]; then
201
+ PREPARE_LOG="/tmp/dictate-install-prepare.log"
202
+ run_logged_check \
203
+ "dictate prepare-model --stt-backend faster-whisper --model turbo --device auto --compute-type int8" \
204
+ "$PREPARE_LOG" \
205
+ 1800 \
206
+ "$DICTATE_BIN" \
207
+ prepare-model \
208
+ --stt-backend faster-whisper \
209
+ --model turbo \
210
+ --device auto \
211
+ --compute-type int8
212
+ fi
213
+
214
+ if [ "$VERIFY" -eq 1 ]; then
215
+ VERIFY_LOG="/tmp/dictate-install-verify.log"
216
+ run_logged_check "dictate --help" "$VERIFY_LOG" 20 "$DICTATE_BIN" --help
217
+ run_logged_check "dictate benchmark --help" "$VERIFY_LOG" 20 "$DICTATE_BIN" benchmark --help
218
+ run_logged_check \
219
+ "dictate doctor --quick --stt-backend faster-whisper --model turbo" \
220
+ "$VERIFY_LOG" \
221
+ 20 \
222
+ "$DICTATE_BIN" \
223
+ doctor \
224
+ --quick \
225
+ --stt-backend faster-whisper \
226
+ --model turbo
227
+ fi
228
+
229
+ if [ "$STARTUP" -eq 1 ]; then
230
+ echo "Done. 'dictate' is now available on your PATH, in the app launcher, and starts when you sign in."
231
+ else
232
+ echo "Done. 'dictate' is now available on your PATH and in the app launcher."
233
+ 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);