* feat(ui): add in-app Dictation Pad for Wayland-safe dictation
Dictated text can now be routed into a text box inside Vocalinux's own
window instead of being injected into other applications — an explicit
fallback for desktops where cross-app injection is unreliable or blocked
(notably Wayland). The pad opens from the tray menu, captures dictation
when enabled via Settings or its own checkbox, and lets the user copy
text out by hand. 'delete that' removes the last segment in-pad.
Closes#726.
* fix(ui): consult live config for pad auto-reveal and Copy All
The reveal gate used the controller's enabled flag, which only syncs on
pad show or its own checkbox — enabling capture via the Settings switch
while the pad was closed silently buffered dictation with no reveal.
_apply_append now reads dictate_to_pad from the shared config.
Also: Copy All copies the widget text (dictation plus in-pad edits), and
re-showing the pad no longer does a wholesale set_text — the view stays
incrementally in sync, so manual edits survive hide/reopen.
* fix(ui): keep dictation pad capture and commands inside the pad
Address all Greptile findings on PR #887:
- Read the capture toggle from the shared ConfigManager instead of
re-parsing config.json per segment, so a torn mid-write read can no
longer fail-open and send dictation to the focused app
- Sync edits made directly in the pad widget back into the controller,
keeping delete semantics consistent with the visible text
- Stamp queued widget operations with a generation tag so a Clear drops
appends still sitting in the GTK idle queue
- Route undo/redo, select all/line/word/paragraph and cut/copy/paste to
pad-local implementations while capturing, so their shortcuts never
reach whichever application holds focus
- Track where the last segment was delivered so "delete that" follows
the text rather than the current capture toggle
- Annotate every new signature and add callback-level tests that pin the
routing, queue, and undo behavior
* fix(ui): keep pad controller, queued ops, and delete target in sync
Manual edits could overwrite queued dictation in the controller because
GTK input events outrank idle callbacks; the changed handler now replays
pending widget ops before syncing the view. The controller also tracks
dictated segments across undo snapshots, so a pad undo pops the "delete
that" target instead of leaving a stale length. Catches in the pad are
narrowed to the GTK/config error set per repo style, and missing type
annotations are added.
* fix(ui): close flush races in pad idle queue and delete retarget
Flushed idle ops stayed scheduled with GLib and ran a second time,
duplicating appends; each queued op now only acts while its entry is
still pending. A queued full-view refresh could also erase a manual
edit that landed first, so the changed-handler flush now drops stale
set-text ops instead of replaying them. And clearing the pad
destination on undo-to-empty kept a redo from restoring the "delete
that" target; the destination flag is preserved so redo re-arms it.
* fix(ui): replay pad idle ops before edits land, not after
Flushing in the changed handler still let queued appends leapfrog the
keystroke that fired it, and a stale dispatch could cancel a later
queued op with identical text. Pending entries now match by identity,
and the replay moved to insert-text/delete-range — signals that fire
before the edit is applied — so queued dictation and deletes keep true
chronological order against manual edits.
* fix(history): target late segments to their own snippet by id
A straggler from a just-ended session could merge into whichever snippet
was newest when it arrived, and audio captured before a session opened
was attributed to that session while decoding on its worker.
- TranscriptionHistory.add returns the snippet id and the new
extend_entry() extends a specific entry, replacing the positional
extend_latest target in the late-segment path
- session_started_floor stamps when a session opens; in-session
attribution also requires the capture to postdate it
- test_capture_ceiling closes the mic-test window at callback restore so
test speech still decoding can no longer leak into history
* fix(settings): keep deferred edits from overwriting a newer choice
The deferred-persist filter treated "config still equals the baseline" as
untouched, so a reopened dialog that applied a key back to its baseline
value could still lose it to the closed dialog worker.
- Record the generation of the apply that last wrote each advanced key;
a pending edit now survives only when no newer apply wrote that key
- A worker apply whose snapshot a newer apply predates now skips its
reconfigure and save entirely, so collection order — not lock order —
decides which snapshot wins
* fix(settings): skip stale applies before stopping dictation
The superseded-apply check ran after stop_recognition, so a stale worker still interrupted a live dictation session. Recognition now stops inside the apply lock, after the generation check.
* fix(shortcuts): keep language edits from breaking dictation
Two per-language shortcut fixes (#805):
- A binding edit landing while a session is live no longer stops
recognition. The dialog's per-keystroke persist still saves the new
binding, but the tray defers the listener rebuild to the next IDLE
transition, so a held push-to-talk keeps its release callback and a
toggle session is never cut off mid-utterance.
- A row edited onto a key another row holds can no longer displace that
row's binding. Each row's remembered shortcut is reserved for its own
row before claims run in row order, so the stealing edit is rejected
and the held binding survives.
- Persists that resolve to the already-saved bindings (mid-edit
keystrokes, rejected duplicates, reverted fields) skip the save and
the listener rebuild entirely.
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>