Commit Graph
354 Commits
Author SHA1 Message Date
8f220c38f4 fix(evdev): release EVIOCGRAB when forwarding cannot run (#1025)
* fix(evdev): release EVIOCGRAB when forwarding cannot run (#1021)

With the internal evdev listener, Vocalinux holds EVIOCGRAB on physical
keyboards and re-emits through uinput clones. Two windows left the grab
held while nothing could forward:

- Suspend/resume: grabs persisted through sleep while the compositor's
  input stack was rebuilt; the listener was torn down and rebuilt ~6s
  after resume, and keystrokes typed at the GNOME lock screen in that
  window went to a stale forwarding path.
- A monitor thread wedged mid-iteration (fd, lock, or a clone write that
  never returns): grabs stayed held forever because the self-healing
  path only fired on write exceptions, and the unexpected-exit cleanup
  only ran when the thread died.

_on_system_suspend now tears the shortcut listeners down before the
machine sleeps, so no grab survives the suspend boundary; the existing
resume flow rebuilds them once /dev/input settles.

A forwarding watchdog now runs alongside the monitor. The monitor stamps
a heartbeat on every loop; if it goes stale while grab->clone pairs
exist, the watchdog runs the existing _release_failed_forwarder path on
each device so input reaches applications directly, and tracks them as
suspended. Once dispatching resumes, suspended devices are removed so
the rescan re-opens them through the standard open->clone->grab path.
Generation checks keep a recycled fd from ever being torn down under a
newer generation.

* fix(evdev): re-arm watchdog-released devices on the monitor thread

The watchdog closing a device fd races the monitor's in-flight select:
a dead descriptor in the select set trips the error handler, which
exits the loop and tears down every keyboard. Recovery now runs where
the close is safe — the monitor retires each suspended device at its
own loop top and the rescan re-opens it through the standard
open->clone->grab path. The watchdog only ungrabs (safe cross-thread:
that fd never sits in the select set) and marks the device suspended.

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-26 20:42:59 -07:00
9374b86793 fix(ui): keep standalone dialogs from binding to other windows on Wayland (#1024)
On GTK's Wayland backend, a toplevel with the DIALOG type hint and no
transient parent becomes an orphan dialog: the next toplevel to receive
keyboard focus silently becomes its xdg_toplevel parent, and GNOME then
raises, minimizes, and workspace-moves the two windows as one.

Settings, the transcript viewer, and the log viewer are standalone
windows opened parentless, so the dictation pad (focused constantly)
adopted them, making Alt+Tab group them and minimize/workspace changes
apply to both. Force the NORMAL type hint on all three so they stay
independent.

Fixes #984

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-26 20:28:51 -07:00
Jakob JensenandGitHub aae1f4b474 fix(clipboard): keep paste-injection copies out of clipboard history 🤖🤖🤖 (#956)
* fix(clipboard): keep paste-injection copies out of clipboard history

The clipboard-paste injection path writes the dictated text to the
clipboard only to feed the paste chord, then restores the previous
content (#588, #646). Clipboard managers such as Klipper and cliphist
still record that transient copy, so every dictation lands in history.

Mark the injection copy with `wl-copy --sensitive`, which clipboard
managers honour by skipping the entry. The flag is probed once via
`wl-copy --help` (added in wl-clipboard 2.2) so older builds keep
working. It is only set when Copy to Clipboard is off: with that
setting on, the dictated text is meant to stay, so it remains an
ordinary copy. Restoring the user's previous content is never marked
sensitive. xclip/xsel have no equivalent and are unchanged.

* test(clipboard): address review on sensitive paste copies

- Move the unittest main guard below TestSensitivePasteCopy so running
  the file directly also runs the new tests.
- Add -> None return hints to the new test methods.
- Cover _copy_to_clipboard forwarding the sensitive hint when a tool
  declines and the next one is tried.
- Docs: history suppression needs wl-copy 2.2+ and a clipboard manager
  that honours the hint; older wl-copy and the XWayland xclip/xsel path
  still produce ordinary copies.

* fix(clipboard): type the wl-copy probe cache and adapt the X11 paste test

- Declare _wl_copy_sensitive_supported as a typed class default instead
  of reading it through getattr(), which mypy (now in CI since #968)
  flags as returning Any.
- test_plain_x11_clipboard_paste_uses_xdotool_and_x11_clipboard
  asserted the exact _copy_to_clipboard kwargs. Check the text and the
  X11 tools instead, so it does not depend on the sensitive hint (which
  xclip ignores) or on the copy_to_clipboard setting.

* test(clipboard): wait for the restore thread instead of a fixed sleep

TestSensitivePasteCopy slept 0.5 s and hoped the background restore had
run. On a slow runner the patches could end first. Wait on an event set
by the restore copy, inside the patches, with a generous timeout.
2026-10-26 20:25:40 -07:00
2fce81e94d fix(shortcuts): preserve Ctrl events for mouse-wheel zoom (#967)
* fix(shortcuts): allow shortcut key passthrough

Fixes #950

* fix(shortcuts): prevent Ctrl release race during passthrough

* test(shortcuts): add type hints to passthrough regression tests

* test(shortcuts): cover passthrough edge cases and integration

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-26 20:24:14 -07:00
90adde05a4 fix(recognition): never lose a stop that races start, and read capture outside the buffer lock (#1023)
* fix(recognition): never lose a stop that races start, and read capture outside the buffer lock

start_recognition() published LISTENING (and played the start cue) before setting should_record. A stop arriving in that window, e.g. a push-to-talk release handled on another thread, was undone by the later should_record = True, leaving the capture thread recording with no way to stop it. The session is now armed under _buffer_lock before LISTENING is visible.

The capture loop held _buffer_lock across source.read_chunk(). When a read blocks (the audio server reconfigures the device, a USB/Bluetooth mic stalls), stop_recognition() gives up its bounded join and then blocks forever on that lock, stuck in PROCESSING. The read now happens outside the lock and the session is re-checked before the chunk is appended.

* test(recognition): wait for the stalled read before stopping

Signal from inside the blocking read instead of sleeping after the first, non-blocking one, so the test always exercises the stalled-read path.

* test(recognition): run the stalled-read test against the real NumPy

Older test modules leave sys.modules["numpy"] as a MagicMock. Under that leak the capture loop's first chunk raised before the stalled read was reached, so the test failed in the full-suite order on CI. Restore the real module for these tests, as test_playback_ducker.py already does.

---------

Co-authored-by: AAG Projects Team <293887937+aagprojectsteam-max@users.noreply.github.com>
2026-10-26 20:18:50 -07:00
Jatin K MalikandGitHub 750d64dc9b fix(tray): prefer libayatana-appindicator-glib when available (#1019)
Use the maintained AyatanaAppIndicatorGlib 2.0 indicator when it is installed, mirroring the existing Gtk.Menu into a Gio.Menu whose actions keep stable, never-reused names and fire the original items. Fall back to the GTK 3 indicators otherwise; the AppImage stays on its bundled GTK 3 indicator, and the .deb recommends the glib typelib.

Fixes #1013
2026-10-25 14:49:34 -07:00
Jatin K MalikandGitHub 9b4aded3a2 fix(snap): list PulseAudio microphones and explain CPU-only whisper.cpp (#1018)
Inside the snap, list the sound server's capture sources with pactl and record from the chosen one by scoping PULSE_SOURCE to each PortAudio open (serialized behind one lock). Settings saves the stable source name, keeps each dropdown entry's server name so selection never queries the server from the GTK thread, and the snap reports whisper.cpp as CPU-only instead of probing host GPU tools.

Fixes #1011
2026-10-25 14:49:25 -07:00
Jatin K MalikandGitHub ed29ddf84b fix(overlay): keep the Wayland fallback overlay alive when the dock closes it (#1017)
Refuse close requests on the pre-mapped Wayland overlay so closing its GNOME dock entry no longer destroys it and brings back the first-recording focus loss, and unmap the overlay when it is turned off so the dock entry goes away immediately.

Fixes #1010
2026-10-25 14:49:15 -07:00
62b0d157c4 chore(release): v0.18.4 (#1007)
- Update version from 0.18.3 to 0.18.4
- Add v0.18.4 sections to CHANGELOG, README, docs/UPDATE.md, metainfo.xml and the website changelog
- Bump website package metadata and softwareVersion schema

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-21 01:49:03 -07:00
Jatin K MalikandGitHub 138a233892 feat(recognition): cancel an in-progress transcription with the recording hotkey (#1004)
* feat(recognition): cancel in-progress transcription with hotkey

Adds per-session cancellation across the manager, tray, and Test Dictation.

Fixes #991

* fix(settings): keep Test Dictation cancellation responsive

Fixes #991

* fix(recognition): keep late workers cancellable (#991)

* test(tray): avoid early tray import in cancellation tests

* fix(recognition): keep a cancelled capture thread out of the next session

A cancel returns to IDLE without joining the capture thread, so a thread
still in its reconnection backoff could wake inside a newer recording,
reopen that session's device and write into its buffer. Tie the capture
loop and the reconnection to the session's cancel token: a cancelled or
replaced session stops reading, skips the reopen and exits without
raising an error.
2026-10-21 01:33:58 -07:00
Jatin K MalikandGitHub 9b5cdf686f fix(dictation-pad): keep Test Dictation results and move pad routing to Settings (#1005)
* fix(dictation-pad): keep Test Dictation results and move pad routing to Settings

Keep late recognition callbacks on the test path.
Move Dictation Pad routing to Settings.

Fixes #983
Fixes #986

* fix(recognition): ignore stop sentinels in pending transcription

Ignore stop signals when checking queued transcription work.
Add regression coverage for a real stop path leaving a sentinel behind.

Fixes #983

* fix(recognition): don't count orphaned queued audio as pending transcription

* fix(recognition): count superseded workers as pending transcription

* test(auto-pause): give the bare manager its worker-tracking state
2026-10-21 01:20:14 -07:00
3e4230ffc2 fix(settings): make model downloads explicit and cancel the whole intent (#1006)
* fix(settings): make model downloads explicit and cancel the whole intent

"Use it" on the recommendation card applied a different model than the one
shown, a cancelled or failed apply left the staged model behind so the next
Test Dictation downloaded it, and cancelling a download tore down and then
reloaded the previous model while the dialog waited.

- Recommendation is now passive text, compared by size tier, with a link to
  docs/MODEL_SELECTION.md. The "Use it" button is gone.
- Every Settings download asks first and shows size and source host.
  Declining, cancelling or failing drops the staged model spec and puts the
  pickers back on the saved config.
- Test Dictation refuses a model that is not on disk instead of applying it.
- reconfigure() fetches a missing model before releasing the loaded one, so
  a cancel restores state without reloading or rehashing the old model.
  Cancellation state is reset only when a download claim is taken or ended.
- Follow-layout mode shows the resolved language in the simple Main language
  control, read-only, and simple edits keep the "layout" setting.

Fixes #979
Fixes #980
Fixes #981
Fixes #982
Fixes #985
Fixes #988

* fix(settings): restore follow-layout mode when a model change is declined

* fix(settings): keep follow-layout and dictation consistent during model changes

* style(tests): format follow-layout regression tests

* fix(recognition): skip resume reinit while a model download is running

* fix(recognition): keep a lazy reload pending when resume is skipped during a download

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-21 01:04:10 -07:00
Jatin K MalikandGitHub f013b7ff88 fix(text-injection): route XWayland clients without IBus through xdotool (#1003)
* fix(text-injection): route XWayland clients without IBus through xdotool

On KDE Plasma Wayland with the KWin IBus bridge enabled, Vocalinux injects through IBus. An X11 client under XWayland only reaches IBus when it loaded an IBus input method module from GTK_IM_MODULE, QT_IM_MODULE or XMODIFIERS. Without one, the engine commit lands in no input context and still reports OK, so the text was lost with no fallback.

Before each WAYLAND_IBUS injection, check XWayland's active window. A real client whose own environment does not select IBus gets that segment through the WAYLAND_XDOTOOL path, and the backend returns to IBus afterwards so native Wayland apps keep using it.

Fixes #972

* fix(text-injection): honour IBus pins and the client toolkit in the XWayland detour

Keep IBus when the user pinned it, judge a GTK or Qt client only by its own toolkit's IM variable (read from the libraries it maps), and have the clipboard restore after an XWayland paste read the X11 clipboard it wrote even after the backend switched back to IBus.

* fix(text-injection): keep a non-text X11 clipboard on the XWayland paste route

WAYLAND_XDOTOOL pasted through the X11 clipboard even when it held an image or file, which it cannot save and restore. Check the clipboard targets there as the X11 route already does, and type the text instead when they hold no text.
2026-10-21 00:40:22 -07:00
4d78246174 fix(recognition): queue long dictation segments before the buffer cap (#976)
* fix(recognition): queue long dictation segments before the buffer cap

A held push-to-talk key (or a toggle session with no pause) keeps a single
segment growing until the 5000-chunk safety cap discards its oldest 25%.
The cap is a chunk count, not a duration, so the effective limit depends on
the capture format: at 16 kHz a chunk is 64 ms (5 min 20 s), but a device
that negotiates 44.1 kHz feeds 23 ms chunks and the same cap is 1 min 56 s.
A 3-minute hold therefore lost ~87 s of speech before transcription, with
only a log warning to show for it.

Flush the current segment onto the existing transcription queue once it
reaches _max_segment_seconds (90 s), or 75% of the chunk budget when chunks
are shorter, so the destructive trim can never fire on buffered speech.
Recording continues while queued segments are transcribed in order; reload
sessions keep the existing hold-until-reload behaviour.

Adds tests for the mid-hold flush, the buffer headroom, and the reload path.

* fix(recognition): hold push-to-talk text until the key is released

Text injection while PTT is held can release or modify the key.

* fix(recognition): hold PTT text without blocking the recognition worker

c7b9bd6 held transcribed text by waiting on the release gate inside
_process_audio_buffer, on the single recognition worker. Later segments
then sat in the 32-slot queue for the rest of the hold instead of being
transcribed: memory grows with every queued segment, and once 32 are
waiting (~48 min of 90 s segments) _enqueue_audio_segment starts dropping
the oldest speech.

Buffer the processed text instead. Every segment is transcribed as it
arrives; while the key is down its result is appended to a held list, and
the held results are delivered in capture order once the gate opens. The
final buffer carries the session gate too, so it cannot overtake text
that was transcribed before release. Held text is flushed when the worker
exits, at the end of stop_recognition, and after the reload drain. An
empty mid-hold result no longer flushes the held text early.

Tests assert that every queued segment is transcribed before release and
delivered in capture order afterwards.

* fix(recognition): transcribe held push-to-talk segments ahead of release

Keep transcription running during long push-to-talk holds while retaining each result until key release. Text injection during a hold can release or modify the PTT key.

* test(recognition): cover cross-session held text isolation

The per-session release gate must release only its own held text: a slow
worker from an earlier session finishing after a new push-to-talk session
starts must not deliver the new session's text while its key is down.

---------

Co-authored-by: Muhannad Abo Hajia <muhannadabohajia@gmail.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-21 00:37:43 -07:00
Jatin K MalikandGitHub e53fc2ceb0 fix(updates): follow the build channel and allow turning update checks off (#999)
Use the running build channel for background checks and add a preference to disable them.

Fixes #977
2026-10-21 00:26:09 -07:00
Jatin K MalikandGitHub 1c299d2dc7 fix(whispercpp): map GPU picker to ggml devices and size threads by effective backend (#1001)
* fix(whispercpp): map GPU picker to ggml devices and size threads by effective backend

The GPU picker and auto selection used vulkaninfo indices, but whisper.cpp's
gpu_device is an ordinal over the GPU/IGPU devices ggml registered. ggml skips
Vulkan devices it cannot use (for example a Haswell iGPU), so the indices shift
and a selected NVIDIA card could fall back to CPU while logs claimed Vulkan.

Read ggml's device registry through the loaded pywhispercpp extension, select
and label devices by that ordinal, run on CPU explicitly when ggml has no GPU,
log the effective backend after load, and pick n_threads from the effective
backend instead of the presence of GPU libraries.

Fixes #987
Fixes #990

* test(whispercpp): move new ggml probe tests into their test class
2026-10-21 00:07:01 -07:00
Jatin K MalikandGitHub 5fa0db6a47 fix(injection): give every RemoteDesktop portal request a unique handle_token (#996)
* fix(injection): give every RemoteDesktop portal request a unique handle_token

Subscribe to predicted Request paths before calling the portal and retain the RemoteDesktop portal in Flatpak when ydotoold is unavailable.

Fixes #993

* refactor(injection): share the portal close in _demote_portal_backend

* fix(injection): recreate the portal session and keep partial counts under Flatpak
2026-10-21 00:02:28 -07:00
cf23e9d82d fix(hotkeys): monitor keyboard nodes with pointer axes ungrabbed (#978)
* fix(hotkeys): keep Logitech HID++ keyboards that report pointer axes

The pointer-axis filter from #900 drops every device that reports
REL_X+REL_Y, assuming a keyboard never does. Logitech HID++ keyboards
(MX Keys, ERGO K860) expose a single node with the same capabilities
as the mice on their receiver, so they were skipped and the shortcut
no longer fired from them.

Capabilities cannot tell them apart, so a device whose name says
keyboard ("keyboard", "keys", "K<digits>") is exempted from the
pointer-axis rule, in both the /proc and the evdev discovery paths.
The MX Master 3 on the same receiver is still skipped.

* fix(evdev): monitor keyboard-pointer combos ungrabbed

* fix(evdev): keep parse_shortcut import and drop per-rescan discovery log

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-20 23:56:51 -07:00
Jatin K MalikandGitHub 545b6d0feb fix(overlay): map the dictation overlay at startup on Wayland so the first recording keeps focus (#998)
* fix(overlay): map the dictation overlay at startup on Wayland

Mutter activates newly mapped Wayland toplevels, which can take focus from the editor.
Map the fallback overlay transparently at startup so its first recording does not remap it.

Fixes #992

* fix(overlay): detect the Wayland display by GType name for the prewarm

Detect Wayland from the display GType because GTK3 omits GdkWaylandDisplay from its typelib.
2026-10-20 23:45:57 -07:00
Jatin K MalikandGitHub d59395b5c9 fix(hotkeys): use the X11 listener for Flatpak on native X11 sessions (#997)
* fix(hotkeys): use the X11 listener for Flatpak on native X11 sessions

Use evdev for Flatpak Wayland/XWayland while preserving the GNOME X11 fallback.

Fixes #971

* docs(flatpak): note that GNOME X11 still needs /dev/input for hotkeys
2026-10-20 23:41:07 -07:00
Jatin K MalikandGitHub 3abc106fdb feat(languages): add Slovenian (sl) to the supported languages (#995)
Slovenian was missing from SUPPORTED_LANGUAGES, so the Settings picker did not offer it and normalize_language_shortcuts dropped any shortcut bound to it. Add it as a Whisper-only entry (whisper code sl; alphacephei publishes no Slovenian VOSK model), map the Slovenian XKB layout si to it for follow-keyboard-layout, and list it on the website languages page.

Fixes #970
2026-10-20 23:29:47 -07:00
Jatin K MalikandGitHub 65de3427fb perf: faster Settings, typing and dictation; mypy in CI, engine interface, architecture docs (#968)
* fix(perf): reduce startup and inference overhead

* fix(perf): report effective whisper.cpp fallback settings

* fix(injection): add opt-in X11 paste for long dictations

* perf(vad): lower default silence timeout to 1.2s

* fix(types): resolve mypy errors across application source

* ci: run mypy in the lint job

* ci: include mypy in lint dependency group

* fix(types): narrow evdev timestamp comparison

* refactor(recognition): add SpeechEngine protocol and engine dispatch table

* refactor(settings): move About section and engine tables out of settings_dialog

* docs: add ARCHITECTURE.md and refresh docs index and AGENTS layout

* fix(settings): preserve GTK dialog compatibility after extraction

* style: format touched Python modules

* fix(types): keep local typecheck from syncing dependencies

* test(vad): align main startup expectation with new default

* style: sort engine registry and protocol test imports

* test(settings): follow extracted About builder attribute alias

* test(settings): isolate GTK dialog base in About test

* test(settings): isolate About color scheme build test

* test(settings): restore GTK base after About scheme test

* test(settings): isolate About scheme check from GTK mocks

* test(settings): support extracted dialog mixin methods

* docs: add before/after performance benchmark charts

* fix(injection): type instead of pasting when the clipboard holds non-text data
2026-10-20 20:23:08 -07:00
2da20f791a chore(release): v0.18.3 (#964)
* chore(release): v0.18.3

- Update version from 0.18.2 to 0.18.3
- Add v0.18.3 sections to CHANGELOG, docs/UPDATE.md, metainfo.xml and website changelog
- Bump website package metadata and softwareVersion schema
- Clarify that the uinput-carrying sideload snap is the v0.18.2 asset

* docs(release): include the transcribe modal-grab fix (#965) in v0.18.3 notes

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-16 20:24:22 -07:00
2ec92bade7 fix(ui): release modal grab after TinyDiarize download before transcript opens (#965)
* fix(ui): release modal grab after TinyDiarize download before transcript opens

The transcribe path's ModelDownloadDialog is modal. On success it stayed
open behind the diarized transcript dialog, holding its grab so every
button on the transcript was unclickable until the hidden dialog was
found and closed. Destroy it on success there and let the transcript be
the completion affordance; the failure path still keeps the dialog up to
show the error.

* style(tests): black-format new dialog-grab tests

* fix(ui): stop download dialog pulse timer on destroy; type test helper

* style(ui): type destroy-handler varargs

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-16 20:24:06 -07:00
d3ba4ee743 fix(injection): send newlines as Return on the xdotool backend (#962)
* fix(injection): send newlines as Return on the xdotool backend

xdotool type maps '\n' to the Linefeed keysym, which GTK targets
silently drop — "new line" produced nothing on X11 even though the
segment reached the injector.  Split each chunk on '\n' and press
Return between typed segments so a newline lands as a real keypress
in every app.

Verified against a live GtkTextView on Xvfb: the same "A\nB" input
that previously produced 'AB' now produces 'A\nB'.

Fixes #955

* fix(commands): consume trailing punctuation after mid-utterance commands

"New paragraph." inside a longer utterance substituted correctly but
left the engine's sentence-final period typed after the break
("\\n\\n. Does this work now?").  Text-command substitution now
consumes a run of trailing , . ! ? ; : … when it precedes whitespace
or the end of the segment — the reporter's comma-phrased case
("…, new paragraph, …") stops leaking the stray comma the same way.
A mark glued to following text ("newline.io") is untouched.

Fixes #955

* fix(injection,commands): resume mid-chunk on retry, don't eat produced marks

Greptile review follow-ups on #962:

- A failed Return mid-chunk left 'typed' at the chunk start, so the
  retry re-sent text xdotool had already delivered.  'typed' now
  advances per typed segment and per Return keypress, so the retry
  resumes exactly at the un-typed position — and the clipboard
  fallback reports the same progress if retries run out.
- _TRAILING_PUNCTUATION could consume a mark produced by an earlier
  command replacement ('hello new line exclamation mark' gave '\\n'
  instead of '\\n!').  The eater now only consumes marks before
  whitespace; the end-of-utterance case ('hello new line.') is a
  pre-pass on the raw segment before any substitution runs.

* style: black-format comment indent and test condition

* fix(commands): strip segment before the end-of-utterance cleanup

Trailing whitespace ('hello new line. ') hid the final mark from the
pre-pass: the raw segment ended in a space, and the stripped segment
left the dot at end-of-string where the mid-utterance eater cannot
touch it.  Strip first, then run the cleanup.

Also add the missing type hints on the new test helper.

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-16 19:05:34 -07:00
devin-ai-integration[bot]andGitHub e866cb85e4 fix(hotkeys): prefer evdev on GNOME X11 and skip redundant IBus engine restore (#963)
Fixes #954.

GNOME X11: mutter never delivers physical key events to the XRecord extension pynput relies on, so the listener started cleanly but shortcuts stayed dead. Prefer the evdev backend there (same path as Wayland), falling back to pynput when /dev/input is unusable.

Shutdown: `ibus engine <name>` rewrites the whole X server XKB map, and the unconditional restore collapsed GNOME's multi-layout map and broke layout switching. The daemon check now uses _query_active_engine() (no GNOME gsettings fallback) so a cleared engine reads as None; the restore write is skipped when a real engine is live or the target is an xkb:* source GNOME owns, and still runs for real IM engines (#558).
2026-10-16 09:48:28 -07:00
0ddb127437 fix(commands): keep line breaks from standalone voice commands (#958)
* fix(commands): keep line breaks from standalone voice commands

Two stacked defects ate the newline "new line" / "new paragraph"
produce when spoken alone:

* _normalize_segment_text stripped all leading whitespace, so the
  command's leading newline never reached the injector (a clean
  "new line" segment was dropped entirely).
* whisper.cpp transcripts arrive punctuated ("New line."), which
  missed the exact-match ladder and left a stray "." after the
  substituted break, injected as ". ".

Normalize now strips only spaces/tabs, and the generic path
substitutes on a punctuation-stripped core when the whole segment
is command phrases, so engine edge punctuation cannot leak while
real text keeps it.

Fixes #955

* fix(injection): deliver newline-only segments to the focused app

inject_text treated whitespace-only text as empty and returned early,
so the "\n" a standalone "new line" now correctly produces was
discarded before any backend ran ("Empty text provided, skipping
injection").  Only a truly empty segment is skipped; whitespace-only
input is typed/pasted verbatim (xdotool types "\n" as Return).

Space/tab-only segments never reach this point: segment normalization
strips them upstream.

Fixes #955

* fix(injection): keep skipping space/tab-only segments

The previous commit let every whitespace-only string through, which
also delivered bare spaces and tabs a caller never wants typed.
Narrow the skip to text with no content outside space/tab so "\n"
still reaches the backend while "   " stays a no-op (the pre-existing
test_empty_text_injection contract).

Fixes #955

* fix(injection): deliver newline segments over IBus too

IBusTextInjector.inject_text had the same whitespace guard and dropped
"\n" before it reached the engine socket; narrow it the same way so
a newline-only segment is sent while space/tab-only stays a no-op.

Also let capitalize_sentences uppercase the first letter after leading
whitespace, so Vosk + auto_capitalize keeps working on "\n hello".

Covers Greptile's findings: real-socket test for the IBus path, the
capitalize regression via the main() callback seam, and type hints on
the new injector tests.

Fixes #955

* test(injection): add type hints to new IBus socket test

AGENTS.md requires hints on every signature; the Greptile review
flagged the new test for missing them.

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-15 16:54:05 -07:00
devin-ai-integration[bot]GitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Jatin K MalikDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
64032fb999 fix(snap): drop uinput plug denied by the Snap Store (#960)
* fix(snap): drop uinput plug denied by the Snap Store

The Snap Store rejected revisions 8-10 over the super-privileged uinput
plug (allow-installation), and reviewers denied the grant request on
forum.snapcraft.io/t/53528, pointing at the RemoteDesktop portal path
the app already prefers. Remove the plug and the uinput-only ydotool so
revisions pass automatic review again; Wayland typing continues through
the RemoteDesktop portal (already the default backend ahead of ydotool),
wtype, IBus Wayland on KDE, and clipboard paste.

KDE Plasma loses its correct-case ydotool path on the store snap (KWin's
portal mangles keysyms on some layouts, KDE bug 489021); the v0.18.x
GitHub .snap still declares uinput for KDE users who sideload it.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(text-injection): stop telling snap users to connect uinput

The Store snap no longer declares the plug, so the warning pointed at a
command that cannot work. The Snap hints now explain that only the
v0.18.x GitHub sideload declares uinput, and that native Wayland typing
uses the RemoteDesktop portal or IBus Wayland on KDE.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Jatin K Malik <jk.malik@yahoo.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-15 15:16:10 -07:00
0faac6b3b8 chore(release): v0.18.2 (#943)
* chore(release): v0.18.2

* chore(release): add Flatpak release notes and refresh changelog date

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-13 00:00:20 -07:00
devin-ai-integration[bot]andGitHub cd388b2e93 fix(evdev): strip SW_RFKILL_ALL from uinput clone capabilities (#947)
The evdev uinput clone copied EV_SW capabilities verbatim. "ThinkPad Extra Buttons" (thinkpad_acpi) declares SW_RFKILL_ALL, and a fresh uinput device reports every switch as 0. The kernel rfkill-input handler reads a 0 killswitch as radios-off, so every clone creation soft-blocked all radios at startup and again after every resume.

Strip SW_RFKILL_ALL from the clone capability list, dropping EV_SW entirely when it was the only switch. Other switches stay declared so real switch events still forward; a forwarded SW_RFKILL_ALL write is dropped by the kernel for the undeclared code.

Fixes #946
2026-10-12 18:07:50 -07:00
AlexThunder1989andGitHub b0fefd6ec1 fix(evdev): leave held keyboards ungrabbed at startup (#941)
Solid fix, this was leaving keys stuck on the desktop whenever someone was typing while the app started.
2026-10-11 23:50:43 -07:00
AlexThunder1989andGitHub 9d667dba3f feat(overlay): live mic orb with audio-reactive pulse and ripple (#940)
Looks great, the live level feedback makes the overlay feel a lot more responsive.
2026-10-11 23:07:07 -07:00
908fbb6072 fix(evdev): monitor remapper virtual keyboards ungrabbed (#932)
* fix(evdev): monitor remapper virtual keyboards ungrabbed

On Wayland the device hub grabs every discovered keyboard and clones it
via uinput so the dictation shortcut can be suppressed. With a remapper
like keyd running, the hub grabbed its virtual keyboard too, and the
remapper grabbed the clone in turn (keyd's default [ids] * matches it):
forwarded keys went to the remapper instead of the compositor, so users
could not type at all, and each forwarded event looped back through the
chain.

Detect virtual keyboards by their sysfs device path (uinput-spawned
devices have no physical parent and live under /devices/virtual/input),
falling back to known remapper names when sysfs is unreadable, and
monitor them ungrabbed. Their events still reach the listener, so
shortcuts bound to them keep working; only suppression is lost, which a
grab could never provide anyway since the remapper re-emits on a device
we cannot hold. keyd reports BUS_USB for its virtual devices, so the
bus type cannot discriminate; the sysfs path can.

Fixes #930

* test(evdev): annotate new #930 test mocks

Satisfies the repo rule that every function signature carries type
hints, including test fixtures.

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-08 16:02:57 -07:00
devin-ai-integration[bot]andGitHub f418a11482 chore(release): prepare v0.18.1 (#926)
Prepares the v0.18.1 patch release: version bump, changelog and docs sync, native-package and web metadata, and release notes crediting every reporter and PR author.
2026-10-08 00:17:58 -07:00
devin-ai-integration[bot]andGitHub 6595b73c55 fix(ui): unbreak Initial Prompt TextView and dedupe save-dialog .txt (#927)
Two small UI fixes found during the v0.18.1 smoke test.

* Settings > Advanced > Initial Prompt never accepted typed input: the wrapping GtkListBoxRow grabs keyboard focus on button press and steals it back from the GtkTextView. Mark the row non-focusable.
* Transcript and log save dialogs could return a doubled .txt.txt name; new paths.collapse_repeated_extension helper normalizes it, with a replace-confirmation when the collapsed path already exists.
2026-10-07 22:30:51 -07:00
8ce85f1e9e fix(whispercpp): reuse an on-disk same-size weight at startup (#923)
* fix(whispercpp): reuse an on-disk same-size weight at startup

The Settings picker already lets a downloaded same-size model stand in
when the language-derived sibling is absent, but the startup path never
applied it: a headless config with language=en-us and model_size=medium
demanded ggml-medium.en.bin while ggml-medium.bin sat in the models
directory and loads fine.

Move the stand-in into whispercpp_model_info.on_disk_stand_in so both
paths share it, and give resolve_whispercpp_variant a prefer_on_disk
flag that only language-derived results use: pinned variants and stored
specializations still load exactly what they name, the .en preference
still wins when that file is present, and checksum verification is
untouched. get_model_size_for_engine opts in so startup and headless
launches match the Settings semantics.

Fixes #916

* fix(whispercpp): keep is_model_downloaded free of makedirs

get_model_path creates the model directory for callers that are about to
write a file, but is_model_downloaded is a read-only probe: on a fresh
profile its makedirs ran inside tests that stub os.makedirs for the
config directory, breaking them, and a bare existence check should not
mutate the filesystem anyway. Resolve the file path without creating
directories in the probe; get_model_path keeps creating for writers.

Refs #916

* test(whispercpp): annotate the stand-in fixture's return type

AGENTS.md requires type hints on every signature; _downloaded returns the
context manager from patch.object.

Refs #916

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-07 18:51:54 -07:00
295386f08b fix(text-injection): end option parsing before typed text (#922)
* fix(text-injection): end option parsing before typed text

A 20-char xdotool chunk, or a 200-char wtype/ydotool chunk, that starts
with '-' (a word hyphenated across the chunk boundary) is parsed as a
flag: 'xdotool type --clearmodifiers -stickation' exits 1 with
'type: unrecognized option' and the rest of the dictation is dropped.

Pass '--' before the text argument in the xdotool, wtype, and ydotool
type invocations so the remaining argument is always typed literally.

Fixes #921

* test(portal-fallback): expect the wtype option terminator

The partial-type fallback assertion predates the -- terminator and still
expected the bare ['wtype', 'llo'] argv.

Refs #921

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-07 18:51:49 -07:00
d021e64813 fix(dictation-pad): keep pad window out of the compositor ghost state (#912)
* fix(dictation-pad): keep pad window out of the compositor ghost state

On GNOME Wayland an idle, fully covered toplevel gets shelved by the
compositor: it stays in the window list but renders nothing and takes
no input. Keep the pad floating above other windows and pinned to all
workspaces so it cannot be shelved, track iconify/obscure state events,
deiconify on show, and repaint when visibility returns.

Fixes #896

* test(dictation-pad): scope Window return_value patch in shelving test

* fix(dictation-pad): detect Wayland shelving and recreate dead surface

set_keep_above is a stub in GTK3's Wayland backend, so a covered idle pad
still gets shelved by the compositor on GNOME/Wayland: it stays in the
window list but draws nothing and takes no input, and present() cannot
revive it.

Add a frame-callback watchdog: each tick callback stamps the clock and a
GLib timeout flags a mapped window whose frames went cold as shelved. A
shelved pad is then recreated (hide + show_all) in show_pad, the same
path the user's manual Close + tray reopen takes.

Keep keep_above/stick for X11 where they are honored, plus the iconify
and visibility-event handling from the previous commit.

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-07 18:07:22 -07:00
devin-ai-integration[bot]GitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Jatin K MalikJatin K MalikDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
06f475cf95 fix(evdev): require a real keyboard key in /proc keyboard discovery (#918)
* fix(evdev): require a real keyboard key in /proc keyboard discovery

Any nonzero B: KEY bit qualified a device as a keyboard, so power/sleep
buttons, the ACPI video bus, vendor hotkey blocks and pointer-button
interfaces were all grabbed and given (vocalinux) uinput clones, leaving
duplicate logical devices that confuse compositor input settings.

Require a modifier or KEY_A in the KEY bitmap — the same rule the evdev
fallback path already applies — and update the test fixtures to a real
keyboard bitmap so they exercise the intended gate.

Fixes #915

* fix(evdev): read /proc bitmaps at the kernel's word width

Capability bitmap words are unsigned longs — 32 bits on 32-bit kernels
(armv7l, i686), not 64. Parse the whole bitmap through _bitmap_to_int()
sized by platform.machine() so high key codes like KEY_RIGHTMETA land
correctly on both widths. Give pointer-test mocks real keyboard keys so
the pointer check — not the keyboard-key gate — drops them.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
Co-authored-by: Jatin K Malik <jk.malik@yahoo.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-07 18:03:50 -07:00
devin-ai-integration[bot]GitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Jatin K MalikJatin K MalikDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
ecd4ae8388 fix(evdev): skip multitouch-only touchpads in keyboard discovery (#917)
* fix(evdev): skip multitouch-only touchpads in keyboard discovery

Devices reporting only ABS_MT_POSITION_X/Y (no single-touch ABS_X/Y)
evaded the pointer-axis check, so multitouch touchpads like the Goodix
GXTP5100 were grabbed and cloned — breaking pointer input while running.

Fixes #914

* fix(evdev): read /proc bitmaps at the kernel's word width

Capability bitmap words are unsigned longs — 32 bits on 32-bit kernels
(armv7l, i686), not 64. Parse the whole bitmap through _bitmap_to_int()
sized by platform.machine() so MT axis bits and any future high bits
land correctly on both widths. Give the fallback MT-mock a real key so
the MT-axes check is the only thing that can drop it.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
Co-authored-by: Jatin K Malik <jk.malik@yahoo.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-07 17:55:25 -07:00
622d0e054d fix(text-injection): detect KWin 6 VirtualKeyboard property (#919)
* fix(text-injection): detect KWin 6 VirtualKeyboard property

On KDE Plasma 6 the org.kde.kwin.VirtualKeyboard 'enabled' property no
longer exists; the setting is reported as 'available'. Query 'available'
first, keep 'enabled' as the Plasma 5 fallback, and accept both the real
(<true>,) gdbus output and the doubly wrapped (<<true>>,) form.

With the bridge check working, KDE sessions with Virtual Keyboard set to
"IBus Wayland" can safely use scoped IBus injection again: the
leftover-daemon trap of #752 is exactly what a confirmed VK bridge
disambiguates. KDE without a confirmed VK stays excluded.

KWin's RemoteDesktop portal scrambles letter case, so in auto mode on
KDE sessions an installed ydotool is preferred over the portal -- the
ordering used before the portal backend existed.

Fixes #911

* fix(text-injection): keep KDE portal fallback ahead of daemonless ydotool

On KDE, when ydotool is installed but cannot run (no daemon, no
/dev/uinput access) and wtype is absent, the daemonless-ydotool branch
fired before the portal fallback -- selecting a backend that fails at
injection time while a working portal sat unused. The portal fallback
now precedes the daemonless branch; working ydotool still outranks the
portal on KDE.

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-07 17:54:56 -07:00
329a7d7f6a fix(settings): stage Advanced model spec until Download is confirmed (#908)
* fix(settings): stage Advanced model spec until Download is confirmed

Picker changes in Speech Model -> Advanced staged a spec but immediately triggered a model download for every intermediate selection. Gate the auto-apply download path so a missing model only stages: a Download action on the model info card now runs apply_settings() as the single explicit confirmation. Simple-mode questions keep the instant apply-and-download flow.

Fixes #894

* fix(settings): repaint model info card after a completed download

The post-download paths repopulated pickers and resynced the engine but never repainted the info card, so a finished download left the amber status and the Download action visible for a model already on disk. The cancel path already repainted via the idle resync; give success its own.

* fix(settings): keep staged spec through picker rebuilds and save non-model edits

* fix(settings): persist non-model edits without a model-reloading reconfigure

* fix(settings): live-apply non-model edits, keep whispercpp params saved-only while staged

* fix(settings): live-set changed VAD/timeout attrs without reconfigure while staged

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-07 17:17:08 -07:00
775863fd30 feat(settings): add proxy support for model downloads and update checks (#909)
* feat(settings): add proxy support for model downloads and update checks

Add a Proxy page to Settings (above About) with Off / System / Manual
modes. Manual mode supports SOCKS5 and HTTPS (HTTP CONNECT) proxies
with optional username/password auth; the password is stored in
config.json in plaintext like the other settings. System mode honors
the *_proxy environment variables and falls back to the GNOME
org.gnome.system.proxy manual configuration.

The resolved proxy is applied to every in-app outbound path that
fetches models or checks updates: the shared requests download path in
recognition_manager (_stream_model_download, used by the whisper.cpp,
whisper, Vosk, Parakeet and faster-whisper downloaders) and the
release checks in update_checker. A small Test Connection button on
the Proxy page verifies reachability through the configured proxy.

SOCKS5 needs PySocks, added as a runtime dependency; uv.lock, the
requirements/*.txt exports and the Flatpak dependency manifest are
regenerated by just lock (with a fix in sync_flatpak_deps.py for
packages like PySocks that publish more than one universal wheel).

Fixes #655

* fix(proxy): bracket IPv6 hosts, escape test errors, declare pysocks for AUR

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-07 17:16:48 -07:00
442a646a57 feat(history): add opt-in JSONL persistence for transcripts (#907)
* feat(history): add opt-in JSONL persistence for transcripts

Adds history.persist (default off) mirroring the bounded snippet list to
history.jsonl under the XDG data directory so transcripts survive
restarts. Entries carry timestamp, duration, language and model when
known. Clearing history truncates the file, turning persistence off
deletes it, and disabling history removes it so nothing is retained
anywhere. Settings gains a Keep on Disk switch and a Clear button in the
Transcription History group.

Fixes #758

* test(history): rebind real tempfile when siblings mock sys.modules

test_recognition_manager and test_speech_recognition replace
sys.modules["tempfile"] with a MagicMock at import time, so the new
persistence tests were sharing one mock-named store path in full-suite
runs. Guard the TemporaryDirectory binding the same way
test_whisper_model_info already does.

* fix(history): sweep stale temp files, survive damaged stores, and keep session fields on late commits

* fix(history): reject lone-surrogate records and survive encode errors on write

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-07 17:13:40 -07:00
6a6cc93919 feat(tray): remove Start on Login and About from the tray menu (#905)
* feat(tray): remove Start on Login and About from the tray menu

Both entries already live in Settings: the autostart switch is in the
general page and About is a dedicated sidebar page, so the tray items
are redundant. View Logs stays.

Removes the Start on Login checkbox item along with its
_update_autostart_checkbox/_on_autostart_toggled wiring and the now
unused _add_menu_checkbox helper, plus the About item and
_on_about_clicked. _show_settings_page("about") stays for the Update
Available item. autostart_manager is untouched since Settings uses it.

Fixes #654

* fix(settings): sync autostart switch with the real desktop entry

The tray's removed _update_autostart_checkbox also reconciled
general.autostart with the actual login desktop entry at startup. Move
that check into the settings load so the Start on Login switch still
reflects reality when the entry changes outside the app.

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-07 17:12:01 -07:00
26c4f9052b fix(settings): place test dictation output above its controls (#904)
Reveal the transcription readout at the top of the sidebar footer so it
grows into the empty sidebar space above the Idle row instead of pushing
the Test Dictation button down when it appears.

Fixes #677

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-07 17:10:35 -07:00
af1029dc60 fix(evdev): skip pointer-motion devices in keyboard discovery (#902)
* fix(evdev): skip pointer-motion devices in keyboard discovery

find_keyboard_devices() treated any device with a nonzero EV_KEY bitmap
as a keyboard, which includes Logitech mice (G502, MX Master 3) whose
button interfaces report key bits. The hub then grabbed them (EVIOCGRAB)
and re-emitted every event, including EV_REL pointer motion, through a
paired uinput clone — piping all cursor movement through the reader
thread and showing up as periodic cursor stutter.

Discovery now excludes devices that report pointer motion (REL_X+REL_Y
or ABS_X+ABS_Y) in both the /proc/bus/input/devices parser and the
evdev.list_devices() fallback. Wheel-only devices (REL_WHEEL) and real
keyboards are unaffected, and a mouse's separate keyboard interface
node keeps working for shortcuts.

Fixes #900

* test(evdev): annotate new device-discovery tests

* chore: drop accidentally committed pr_body.md

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-07 00:10:50 -07:00
1d4dbe33cc chore(release): prepare v0.18.0 (#895)
* chore(release): prepare v0.18.0

Bump version surfaces for the v0.18.0 minor release: version.py,
web/package.json + lockfile, softwareVersion schema, SECURITY.md
supported-versions table, AUR pkgver/_tag (source and -bin),
AppStream metainfo release entry, and the release-history table in
RELEASE_PROCESS.md.

Docs and site copy for the delta since v0.17.0: README blurb, snap
sideload versions in README/INSTALL/UPDATE, CHANGELOG current-stable
entry, docs/UPDATE.md What's New with the 0.18 highlights table and
per-PR credits, website changelog + desktop-reliability entries, and
PRODUCT.md capability lines for Dictation Pad, per-language shortcuts,
tray history, overlay, ducking, PipeWire, portal injection, D-Bus
activation, custom dictionary, and file transcription.

* fix(release): address review findings on the v0.18.0 prep

- Keep the checked-in vocalinux-bin PKGBUILD on v0.17.0 so its real
  AppImage digests stay consistent with its sources; release.yml bumps
  pkgver/_tag and pins both-arch digests atomically at publish
- Qualify the Flatpak release note: PipeWire system-audio capture and
  RemoteDesktop portal injection are native-install paths
- Refresh dateModified on the changelog and desktop-reliability pages
- Restore the release-history table order in RELEASE_PROCESS.md
- Revert the build-regenerated web/next-env.d.ts route-types reference

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-06 14:03:21 -07:00
96e012d9bd fix(evdev): keep key suppression when /dev/uinput is write-only (#893)
* fix(evdev): keep key suppression when /dev/uinput is write-only

python-evdev opens /dev/uinput O_RDWR, but the kernel only needs write
access for the uinput setup ioctls and event injection. Installs that
still carry the original 80-dotool.rules line (MODE=0620, write-only
for the input group) therefore fail UInput() with EACCES and fall back
to ungrabbed monitoring, where the dictation shortcut is detected but
also reaches the focused application. Retry the clone on an O_WRONLY
descriptor via a small _WriteOnlyUInput subclass so suppression keeps
working there (refs #871).

Also populate _withheld_modifier lazily in _event_is_shortcut: the
AltGr-replay machinery was dead code because nothing created the
per-fd map, so a withheld modifier press was never replayed and
composition (e.g. RightAlt+e) lost its modifier.

* fix(evdev): byte-truncate clone names and cover the write-only path

- _clone_device_name now truncates the encoded name by bytes: the
  kernel limit is 79 bytes, and a long multibyte device name could push
  the " (vocalinux)" suffix past the cut so rescan would not skip the
  clone.
- _WriteOnlyUInput closes the fd explicitly when _prepare_events fails
  before the _uinput calls; the helpers close it themselves on any
  later failure.
- Tests now instantiate the real subclass with mocked OS/native calls
  (open flags, setup sequence, write/write_event/syn ordering, fd
  cleanup, name truncation).

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-01 23:45:49 -07:00
4497bfae6c fix(logs): let the View Logs dialog close via titlebar-X (#892)
* fix(logs): let the View Logs dialog close via titlebar-X

_on_response only destroyed on ResponseType.CLOSE and ignored
DELETE_EVENT, so the Logs window could not be dismissed with the
window-manager close button — unlike Settings, which handles both. A
window force-destroyed behind the dialog then crashed quit with a
BadWindow error instead of an orderly shutdown.

* test(logs): cover _on_response behavior; annotate its signature

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-09-30 05:55:33 -07:00