diff --git a/docs/superpowers/specs/2026-03-20-gui-app-design.md b/docs/superpowers/specs/2026-03-20-gui-app-design.md index ae377a4..a1b85ab 100644 --- a/docs/superpowers/specs/2026-03-20-gui-app-design.md +++ b/docs/superpowers/specs/2026-03-20-gui-app-design.md @@ -52,7 +52,8 @@ def _app_dir() -> str: return os.path.dirname(sys.executable) else: # Dev mode: use script directory (git repo root) - return os.path.dirname(os.path.abspath(__file__ + "/../../")) + # config.py lives at whisper_app/config.py → two levels up = repo root + return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ``` Machine-local config (`config_local.json`) continues to use `%LOCALAPPDATA%\WhisperDictation` (Windows) or `~/.local/share/WhisperDictation` (Linux) — unchanged. @@ -62,21 +63,25 @@ Machine-local config (`config_local.json`) continues to use `%LOCALAPPDATA%\Whis `app.py` owns a `queue.Queue[str]` and a pre-queue buffer list. ```python -_log_buffer: list[str] = [] # before queue is ready +_log_buffer: list[str] = [] # before queue is ready, capped at 500 entries _log_queue: queue.Queue | None = None +_log_lock = threading.Lock() def log(msg: str) -> None: - if _log_queue is not None: - _log_queue.put(msg) - else: - _log_buffer.append(msg) + with _log_lock: + if _log_queue is not None: + _log_queue.put(msg) + else: + _log_buffer.append(msg) def set_log_queue(q: queue.Queue) -> None: global _log_queue - _log_queue = q - for msg in _log_buffer: - q.put(msg) - _log_buffer.clear() + with _log_lock: + _log_queue = q + buffered = list(_log_buffer) + _log_buffer.clear() + for msg in buffered: # flush outside the lock to avoid deadlock + q.put_nowait(msg) ``` All `print()` calls in all modules are replaced with `app.log()`. @@ -106,6 +111,7 @@ Opened via tray icon left-click or "Anzeigen" menu item. Implemented in `log_win - Close button → `withdraw()` (does not quit the app) - 🗑 button → clears the text widget - Queue polled via `root.after(100, _poll_log_queue)` +- The tkinter `root` (hidden) is always alive as long as the tray runs — `withdraw()` on the log window does not trigger mainloop exit. The app exits only via the tray "Beenden" menu item. ## Settings Window — Installation Section @@ -117,9 +123,10 @@ Each integration shows status ("eingerichtet" / "nicht eingerichtet") and two bu |---|---|---| | Autostart beim Login | `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` | `~/.config/autostart/whisper-dictation.desktop` | | Startmenü-Eintrag | `%APPDATA%\Microsoft\Windows\Start Menu\Programs\Whisper Dictation.lnk` | `~/.local/share/applications/whisper-dictation.desktop` | -| Desktop-Verknüpfung | `%USERPROFILE%\Desktop\Whisper Dictation.lnk` | `~/Desktop/whisper-dictation.desktop` | +| Desktop-Verknüpfung | `%USERPROFILE%\Desktop\Whisper Dictation.lnk` | XDG: `xdg-user-dir DESKTOP` (fallback: `~/Desktop`) | Windows `.lnk` files are created via `pywin32` (`win32com.client.Dispatch("WScript.Shell")`). +`import win32com` must be guarded: `if sys.platform == "win32"` — top-level import is forbidden to avoid import errors on Linux. **Only available when running as a frozen binary.** In dev mode, the buttons are disabled with a tooltip "Nur im gebauten Binary verfügbar". @@ -141,22 +148,25 @@ a = Analysis( 'ctranslate2', 'faster_whisper', 'sounddevice', - 'pynput.keyboard._win32', # Windows - 'pynput.keyboard._xorg', # Linux - ], - datas=[ - ('config.json', '.'), - ('vocabulary.json', '.'), + 'pynput.keyboard._win32', # Windows + 'pynput.keyboard._xorg', # Linux X11 + 'pynput.keyboard._uinput', # Linux Wayland ], + datas=[], # config.json / vocabulary.json NOT bundled — see below ) -exe = EXE(a.pure, ..., console=False, icon='icon.ico') +pyz = PYZ(a.pure) +exe = EXE(pyz, a.scripts, ..., console=False, icon='icon.ico') +coll = COLLECT(exe, a.binaries, a.datas, name='whisper-dictation') ``` +**config.json / vocabulary.json are NOT bundled via `datas`.** +They are user-editable files that live next to the binary. `build.py` copies them from the repo root into `dist/whisper-dictation/` after the PyInstaller run. This is the single authoritative location in frozen mode (`os.path.dirname(sys.executable)`). + ### `build.py` -1. Generates `icon.ico` from PIL +1. Generates `icon.ico` from PIL (must run before PyInstaller — `.spec` references it) 2. Runs PyInstaller with the `.spec` file -3. Copies `config.json` and `vocabulary.json` into `dist/whisper-dictation/` +3. Copies `config.json` and `vocabulary.json` into `dist/whisper-dictation/` **only if they don't already exist there** (to avoid overwriting user edits) ### Platform requirement @@ -172,6 +182,16 @@ PyInstaller cannot cross-compile. **Build must be run separately on each platfor Added to `requirements-windows.txt`. Not required on Linux. +## UI Language + +All UI labels are in German. "WHISPER DICTATION" in the log panel header is the product name and stays as-is. All other UI text (buttons, section headers, tooltips) is German. + +## Implementation Notes + +- **Startup crash visibility:** With `console=False`, crashes before the tray appears produce no visible output. Implementer should wrap `main()` in a try/except that writes to a logfile (e.g., `%LOCALAPPDATA%\WhisperDictation\error.log`) as a last resort. +- **pynput hidden imports:** Only keyboard backends are needed (`_win32`, `_xorg`, `_uinput`). No mouse backends required — hotkeys are keyboard-only. +- **`_log_buffer` cap:** Enforce max 500 entries; if buffer exceeds cap, drop oldest entry before appending. + ## Out of Scope - Code signing / notarization