diff --git a/linux/CHANGELOG.md b/linux/CHANGELOG.md index bcf67f7..7558ddf 100644 --- a/linux/CHANGELOG.md +++ b/linux/CHANGELOG.md @@ -9,6 +9,21 @@ The version is defined in [`blitztext/__init__.py`](blitztext/__init__.py). ## [Unreleased] +## [2.03.26] - 2026-06-10 + +### Fixed +- **Cancel key now works during wakeword-triggered recording.** Previously the + `ModifierScheme` state machine stayed in "idle" when the wakeword fired + (it bypasses the key-press path), so the cancel hotkey was silently ignored. + It now checks `daemon.is_recording` as a fallback so it fires regardless of + how recording started. + +### Added +- **"✕ Cancel recording" in the tray menu.** Always visible; grayed out when + idle, enabled as soon as recording starts (wakeword or manual). The primary + escape hatch when the wakeword fires on audiobook / TV audio and spoken + cancel words can't be heard over the background audio. + ## [2.03.25] - 2026-06-10 ### Changed diff --git a/linux/blitztext/__init__.py b/linux/blitztext/__init__.py index 7a3d611..044b766 100644 --- a/linux/blitztext/__init__.py +++ b/linux/blitztext/__init__.py @@ -6,4 +6,4 @@ counterpart to the macOS Blitztext menu bar app: it runs natively on the host (not in a container) so it can type into any application via xdotool. """ -__version__ = "2.03.25" +__version__ = "2.03.26" diff --git a/linux/blitztext/inputmode.py b/linux/blitztext/inputmode.py index cee29a6..53b15c3 100644 --- a/linux/blitztext/inputmode.py +++ b/linux/blitztext/inputmode.py @@ -74,18 +74,21 @@ class ModifierScheme: return self._pressed.add(token) + # Cancel works while arming/armed AND when the daemon is recording via + # wakeword (state stays "idle" in the scheme because wakeword bypasses + # the key-press path entirely). + if self._is(token, self.cancel) and ( + self._state != "idle" or self.daemon.is_recording): + self._state = "idle" + self.daemon.cancel_dictation() + return + if self._state == "idle": if self.start.issubset(self._pressed): self._state = "arming" self.daemon.start_dictation() return - # Cancel works while arming or armed. - if self._is(token, self.cancel): - self._state = "idle" - self.daemon.cancel_dictation() - return - if self._state == "armed": if self._is(token, self.send): self._state = "idle" diff --git a/linux/blitztext/tray.py b/linux/blitztext/tray.py index 0ce1de8..9e305bf 100644 --- a/linux/blitztext/tray.py +++ b/linux/blitztext/tray.py @@ -88,6 +88,15 @@ class Tray: menu.append(Gtk.SeparatorMenuItem()) + # Cancel recording — always visible, only sensitive while recording. + # Primary escape hatch when wakeword fires on audiobook/TV audio. + self.cancel_item = Gtk.MenuItem(label="✕ Cancel recording") + self.cancel_item.set_sensitive(False) + self.cancel_item.connect( + "activate", lambda _i: self.app.daemon.cancel_dictation()) + menu.append(self.cancel_item) + menu.append(Gtk.SeparatorMenuItem()) + # Hands-free wakeword: a reversible pause toggle. Without this, a stale # /tmp/wake_muted flag would silently disable detection with no way back. if getattr(self.app.cfg, "wakeword_enabled", False): @@ -120,6 +129,8 @@ class Tray: def update_status(self, state: str, message: str) -> None: self.indicator.set_icon_full(ICONS.get(state, ICONS["idle"]), state) self.status_item.set_label(f"● {message or state.title()}") + if hasattr(self, "cancel_item"): + self.cancel_item.set_sensitive(state in ("recording", "armed", "busy")) def pump(self) -> None: """Service pending GLib/GTK events; called from tkinter's loop."""