caret: change default overlay_anchor from "caret" to "pointer" — AT-SPI's
synchronous D-Bus extents read on the GTK main loop could freeze the entire
X11 session when the focused app was slow to respond. Pointer anchor is a
reliable fallback (the mouse is almost always near where you're typing) and
avoids AT-SPI entirely by default.
caret: wrap the AT-SPI rect() read in a daemon thread with a hard 300ms
timeout — so even if overlay_anchor="caret" is configured manually, a frozen
or unresponsive target app can never stall the GTK main loop.
settings: replace the Manual tab's plain TextView with a styled markdown
renderer (_md_panel): # h1/h2/h3 headings, **bold**, *italic*, `inline code`,
> blockquotes, --- rules, bullet/numbered lists, and | tables |.
settings: infobox background changed from theme-adaptive grey (unreadable on
dark themes) to a fixed blue tint (rgba 66,133,244 @ 9%) with a matching blue
border — readable in both light and dark themes.
settings: active notebook tab now shows bold text with a #1a73e8 blue colour
and extra padding, making the current tab obviously distinct.
settings: emoji picker buttons enlarged to 20 px / 38×38 px minimum (grid)
and 18 px / 34×34 px (category bar) via bt-emoji-btn / bt-emoji-cat-btn CSS
classes — previously rendered at the default button label size (~11 px).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Emoji picker: SearchEntry at top filters all categories via unicodedata.name()
in real time; category view hides while searching, restores on clear
- Manual tab: add pkg_dir/MANUAL.md to _app_paths() search list so it works in
both venv and deb installs (MANUAL.md deployed alongside the package)
- bt-infobox CSS: replace @theme_selected_bg_color (saturated blue) with a
neutral 5% mix of fg/bg so banner text is always readable
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Every _labeled and _switch_row field now gets a clickable ⓘ info button
that opens a plain-language help popover — targeted at non-technical users
- New Manual tab in Settings renders MANUAL.md directly inside the dialog
- STT and LLM toolbars each get a "Quickstart ▾" button: a menu of common
providers (OpenAI, Groq, OpenRouter, Ollama, LM Studio, vLLM, llama-swap,
faster-whisper-server, NVIDIA Riva) that pre-fills the engine form in one click
- Engine type combos now show human-readable labels ("Internal — faster-whisper",
"LAN server — runs on your machine", "GPU (CUDA)", "int8 — fast, less memory")
while storing the same internal key values (no config migration needed)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the 60-emoji flat list with 8 standard Unicode categories
(Smileys, People, Animals, Food, Travel, Activities, Objects, Symbols),
each with 30–100 emojis. The picker now has a category tab bar at the top
(emoji icons, highlighted active tab) and a scrollable emoji grid below —
matching the layout used in WhatsApp and other messaging apps.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a 😀 button next to the Icon (emoji) entry in Settings → Presets.
Clicking it opens a GTK popover with 60 common emojis in a scrollable
flow grid; selecting one writes it into the field and closes the picker.
The entry still accepts direct keyboard input as before.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In Python 3.12 the 'except ... as e' variable is deleted after the except
block exits, so a plain lambda that closes over 'e' raises NameError when
GLib fires it later. Bind e as a default argument at lambda creation time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Globs don't expand in redirect targets. Use the venv Python to resolve the
exact site-packages path instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On Debian/Ubuntu, python3-gi is installed into the non-versioned
/usr/lib/python3/dist-packages path which venvs exclude even with
--system-site-packages. A debian-system-packages.pth file adds it to
sys.path so `import gi` works from the venv.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
'python -m blitztext' adds cwd to sys.path, so running blitztext from inside
a directory that contains a blitztext/ folder shadows the installed package.
A proper shebang entry-point script sets sys.path[0] to the script's own
directory (venv/bin), making the launcher cwd-independent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each apt package must be a separate array element; passing them as one quoted
string caused apt-get to fail with "unable to locate package <all names>".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Check for python3-gi (PyGObject), xdotool, libnotify-bin, and pipewire-bin
before creating the venv; if any are missing, run sudo apt-get install
automatically so the install works out of the box on a fresh system.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves the venv out of the source tree to ~/.local/lib/blitztext so it works on
CIFS/SMB shares (--copies avoids the lib64 symlink that CIFS rejects). Copies
the blitztext package into site-packages so the cloned source folder can be
deleted immediately after running install.sh. Adds a ~/.local/bin/blitztext
launcher and installs hicolor icons.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All rows in the Input tab (labeled, key_field, url_field, sound_field) now share
a single LW=175 label width, eliminating the jagged left edge on input fields.
Added X-GNOME-Autostart-Delay=12 to the .desktop entry so gnome-shell, AT-SPI,
and the input stack are fully initialised before Blitztext connects — fixes the
session crash on first login after installation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- gtksettings: build each notebook tab on first view instead of all up front, so
the dialog opens instantly (was ~1.3s building Input/Benchmark file-choosers);
_collect() force-builds unvisited tabs before saving so no field is missed.
- gtksettings: connection dot now sits left of the URL entry (like the Engines
tab) instead of at the far right.
- gtkui: open_settings raises the existing dialog instead of opening a second.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Green/red/grey reachability dot next to the wakeword and TTS endpoint fields,
matching the existing STT/LLM engine dots. Lightweight background TCP probe,
refreshed on open, on reload (⟳), and on focus-out — never blocks dialog build.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On headless/minimal desktops the gvfs org.gtk.vfs.UDisks2VolumeMonitor dbus
service often fails to activate; each Gtk.FileChooserButton then blocked ~25s on
a StartServiceByName timeout while realizing, so the Settings dialog never
appeared and the stalled main loop froze the control panel too. Set
GIO_USE_VOLUME_MONITOR=unix in run_gui() before any window is realized.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Send by voice: a configured edge-anchored phrase (e.g. "computer send") is
stripped and the rest is delivered AND submitted with Enter. Off by default;
[routing] send_keywords + Settings → Input.
- Wakeword benchmark (Settings → Benchmark): synthesize the wake phrase in
random voices via any OpenAI-compatible TTS server, stream to
wyoming-openwakeword, report recall / false-fires / per-voice breakdown.
New [tts] config block.
- Tests: test_voice_send.py, test_wakeword_bench.py.
- Ignore agent workspace folder jules/.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The live waveform and silence auto-stop countdown were driven by a level
meter that was the last user of sounddevice/PortAudio, which hangs opening
the default input on PipeWire systems — so both stayed blank on the hotkey
and wakeword paths alike. Rewrite LevelMeter to stream raw PCM from the same
recorder as the WAV path (pw-record/parecord/arecord) and RMS it; identical
API, scaling, and ~10 Hz cadence. Also fixes the Settings mic-level preview.
Set GLib prgname/application name to "Blitztext" before any window is
realized (and add StartupWMClass to the .desktop) so the taskbar and GNOME's
"… is not responding" dialog show the app name instead of "__main__.py",
without touching the `python -m blitztext` entry point.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a configurable voice cancel: saying "abbrechen" (or "cancel") at the start
or end of a clip discards the whole dictation — it is never routed onward,
rewritten, or typed. The rescue for accidentally triggered (e.g. wakeword)
recordings. Matched the same edge-anchored, ASR-tolerant way as routing keywords
via routing.is_cancel(), so the word buried mid-sentence won't trip it; checked
in Daemon._process right after transcription, before routing/rewrite/delivery.
Configurable via [routing] cancel_keywords (default ["abbrechen", "cancel"];
empty disables) and Settings -> Mic/Cues -> "Cancel words". The overlay briefly
shows "Abgebrochen". Docs (both READMEs) and CHANGELOG updated; tests cover the
matcher, the config round-trip, and the discard/deliver pipeline branches.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix a desktop-session freeze (forced logout/reboot) caused by the overlay's
AT-SPI caret tracker: it subscribed to the high-frequency object:text-caret-moved
signal and made synchronous, blocking AT-SPI reads from inside the event handler,
re-entering the a11y dispatcher and getting stormed by the app's own xdotool
typing until GNOME stopped responding. Now track focus changes only and read the
caret rectangle lazily, once, when the overlay shows — never on the hot path.
Fuse voice-routing feedback into the overlay instead of a desktop notification:
show the matched preset's emoji, name, and spoken keyword on a banner, narrate
the phase (Transcribing -> Rewriting), and stream the LLM rewrite into the bubble
token-by-token. Redundant per-dictation notifications are suppressed when the
overlay is present (errors still notify); headless/overlay-off is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A full circle wraps the mic glyph and drains clockwise as the trailing-
silence timer runs out, recolouring cyan→amber→red and emptying exactly
as auto-stop fires. The daemon emits the countdown from the same VAD loop
that decides auto-stop, so the ring stays in sync; the overlay drains it
against its own clock for smooth motion and fades it in/out so word gaps
don't flicker it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Show a translucent, click-through bubble the moment recording starts (by
hotkey or wakeword): a pulsing microphone, a live waveform of the mic
level, and the recognised text (word-by-word when streaming, otherwise a
brief final-result confirmation). The tail points at the text caret via
AT-SPI accessibility, falling back to the mouse pointer, then a screen
corner. Also gives hands-free wakeword sessions visible feedback, whose
notifications are suppressed by design. X11 only.
- overlay.py: GTK override-redirect HUD (mic + waveform + bubble), drawn
with Cairo; thread-safe, marshalled onto the GTK loop.
- caret.py: best-effort anchor (AT-SPI caret -> pointer -> window/corner).
- daemon: optional level_cb/text_cb hooks; reuses the VAD level meter for
non-streaming, a dedicated meter for streaming. Stays UI-agnostic.
- gtkui: instantiate the overlay, drive show/update/hide from status.
- General settings: "Visual overlay" toggle; config overlay_enabled /
overlay_anchor (default on, "caret").
- Docs: CHANGELOG 1.5.0, version bump, README + MANUAL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Spoken presets: route() now also matches a preset's name as an implicit
keyword, so presets with no configured keywords (Nicer email, Calm down,
Add emojis) are triggerable by voice — say the name. Explicit keywords still
win; names are added to STT hotwords too. Tests added.
- General tab: switches moved to the far right of each row with an inline
grey description (new _switch_row helper) so each toggle is self-explanatory.
- About tab: add "Copyright: 2026 mARTin Bierschenk - Design".
- MANUAL + CHANGELOG updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bump version to 1.4.0 and cut the [1.4.0] - 2026-06-07 changelog section:
- "Announce matched preset" notification (shown even hands-free) + per-preset
emoji icons.
- Voice-routing no-keyword default now prefers a transcribe preset instead of
the first preset.
Refresh the README status line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The matched-preset feedback used _dnotify, which is suppressed for hands-free
sessions — so wakeword users never saw which keyword/preset fired.
- Add a dedicated "Announce matched preset" notification (_rnotify), gated by
a new [general] notify_routing flag (default on) and independent of the
hands-free silence, so it shows for wakeword commands too. It only fires on a
real routing match, so it never spams when nothing is said.
- Show each preset's emoji in that notification; add a per-preset "Icon (emoji)"
field to the Presets editor so matches are visually distinct.
- General-tab toggle; MANUAL + CHANGELOG updated. Adds a test that the match is
announced even when _session_silent is set.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When no [routing] default is configured, default_preset fell back to
workflows[0]. If that was an LLM rewrite (e.g. "Improve text"), every
wakeword/voice command without a matching keyword was sent to the language
model — repeatedly failing with HTTP 502 when the LLM backend was down.
default_preset now prefers a transcribe-mode preset for the no-keyword
fallback, so the default action is plain transcription. Adds tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bump version to 1.3.0 and cut the [1.3.0] - 2026-06-07 changelog section
(wakeword pause toggle, independent hands-free audio cues, configurable
auto-stop silence, notification hygiene, PortAudio noise suppression, and the
new MANUAL.md). Refresh the README status line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reported issues from hands-free use:
- Wakeword WAVs didn't play: the new "Play audio cues" master switch
([sounds] enabled) also gated the hands-free Sound: detected/captured cues,
so enabled=false silenced them. The cues live in a separate UI section, so
this was surprising. Wakeword cues are now independent of that switch: they
play whenever a file is set, and an empty field means silent (no system-chime
fallback) — which is also how you turn a hands-free cue off. The master switch
now governs only the manual (keyboard) before/after chimes.
- Clarified the four sound fields' tooltips/labels (detected/captured = hands-free
only; before/after = manual only) and the empty-field behaviour.
- New "Silence to stop (s)" setting (Settings → Input → Hands-free, or
[wakeword] silence_seconds, default 2.0): user-defined trailing-silence
timeout for hands-free auto-stop (was hard-coded to 2.5 s).
- Add MANUAL.md documenting every setting in every tab; link it from the README.
Tests: cue independence + manual-gating + roundtrip (17 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two follow-ups from hands-free testing:
- Audio cues had no off-switch: empty sound fields fall back to the freedesktop
system chime, so wakeword/recording always made noise. Add a [sounds] enabled
master flag (default true) exposed as "Play audio cues" in Settings → Input.
When off, _play_cue/_play_sound are no-ops — fully silent operation.
- The VAD level meter (sounddevice/PortAudio) leaked harmless thread-teardown
errors ("pthread_join ... failed", "PaUnixThread_Terminate ... failed") to the
terminal on every clip end. PortAudio writes these straight to fd 2, so wrap
the stream open/close in a fd-level stderr suppressor (_quiet_c_stderr).
Tests: cue gating respects the master switch (16 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the cramped thumbnail tables in the root and linux READMEs with one
full-width image per Settings tab (Presets, Engines, Input, Wakeword, General,
Benchmark, Log, About), each followed by a caption explaining what the tab does.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two reported bugs:
1) Notifications still appeared during hands-free use while away. A wakeword
hit arriving while the previous clip was still transcribing went through
toggle(), which popped a "Busy" notification — the away-from-keyboard
notification storm. Wakeword triggers now call start_dictation(silent=True)
directly, so a busy/not-ready state is ignored silently. Transcription and
rewrite failures during a silent session are now logged instead of raising
critical desktop notifications.
2) "Wakeword did not work" with the log line "muted via /tmp/wake_muted": a
stale mute flag silently disabled detection. The tray "Pause wakeword"
toggle (already added) clears it; the daemon now also logs a clear
"Starting PAUSED" warning when it boots with the flag present.
Adds tests/test_daemon_notify.py covering silent-session suppression and the
busy-while-wakeword no-notify path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bug: a stale /tmp/wake_muted flag silently disabled wakeword detection
with no in-app way to clear it (the flag was read but never written by
any code or UI).
- wakeword.py: expose MUTE_FILE + is_muted()/set_muted() helpers
- tray.py: add a "Pause wakeword" CheckMenuItem (shown when wakeword is
enabled) that toggles the flag and reflects current state
- config.py: document the tray toggle instead of the bare /tmp file
- CHANGELOG: record the wakeword pause toggle and the existing notify
hygiene fixes (transient/single-bubble notifications, silent
hands-free sessions)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ci.yml: drop the macOS build job (Xcode/xcodegen/build.sh); keep the
Linux test job and move the secret-hygiene scan into it
- PR template: replace ./build.sh checklist item with the Linux test
suite; fix "macOS preview scope" -> "Linux preview scope"
- README: add a control-panel + tray hero image and a Screenshots
gallery (settings tabs + hands-free wakeword)
- Track Screenshots/wakeword.png
The BlitztextMac/ source tree, root build.sh, and stale docs/screenshots
were removed from the entire git history in a separate history rewrite.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Refresh the Engines tab screenshot to the current UI (info box, +Stream button,
engine Name field, local/cloud Type, device & precision, status dots).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Refresh the panel/tray/settings screenshots, normalise "tray menu.png" to
tray-menu.png (drop the old "panel resorted.png"), and add a hero image plus a
Screenshots section (settings tabs) to linux/README.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_exec_command now detects whether Blitztext is running from a source checkout
(not /opt or site-packages) and, if so, writes an autostart entry that launches
that exact source with PYTHONPATH — so the "Launch on login" toggle no longer
reverts to a stale installed .deb when developing from source.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two optional WAVs in the Hands-free section: "Sound: detected" plays when the
wakeword fires (your cue to speak now) and "Sound: captured" plays when the
command is taken (silence/stop). For hands-free sessions these take precedence
over the general [sounds] cues, falling back to them and then to the built-in
system sound. The general cues are relabelled "Audio cues (manual dictation)".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A session started by the wakeword is marked silent: its per-dictation
notifications (Recording, Transcribing, routing match, Rewriting, done) are
suppressed via a new _dnotify helper. Keyboard/GUI-triggered dictation still
notifies normally, and real errors still show.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- notify-send notifications are now transient (boolean:transient:true) with a
2.5s expiry, so they no longer pile up in the message tray / lock screen.
- Single-instance guard (abstract unix socket) in the entry point: a second
`tray`/`gui`/`run` launch exits instead of starting a duplicate daemon — the
duplicate caused two wakeword listeners + two recorders.
- Empty/quiet/keyword-only results now only log, instead of firing a desktop
notification on every (often false) wakeword trigger.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add Screenshots/ (panel + settings tabs: engines, input, general, benchmark,
about) for documentation, and pyrightconfig.json pointing the type checker at
the system dist-packages so it resolves PyGObject (gi).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a plain-language info box to the top of every Settings tab (Presets,
Engines, Input, General, Benchmark, Log, About), exposed to screen readers, and
tooltips (wired to ATK descriptions) on the preset, general, and input-mode
controls — for non-technical and blind users (barrierefrei).
Complete the unreleased 1.2.0 changelog with the engine manager, Benchmark tab,
custom audio cues, Log tab, click-to-bind hotkeys, the GTK rebuild, voice
routing, the modifier hotkey scheme, and the quality gate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New sound.py plays a user WAV (or a built-in system sound) without blocking.
Config gains [sounds] before/after paths. The daemon plays the "before" cue when
recording starts and the "after" cue on every stop (stop+paste, stop+paste+Enter,
or auto-stop on silence — all funnel through finish_dictation). Input tab gets an
Audio cues section: file pickers with play-test and clear-to-default buttons.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>