Commit d81ae3c
Fix double-paste: kitty router socket + daemon dedup window/normalization (#1)
* #2 Kill double-paste at source: kitty ctrl+v router skips tmux windows
A single Ctrl+V inside kitty+tmux fired two handlers (kitty `map ctrl+v` +
tmux `bind -n C-v`), both calling flashpaste-trigger ~hundreds of ms apart,
which the daemon's dedup window had to race. The new router suppresses
kitty's redundant handler when it can confidently detect tmux in the focused
window (kitty remote control + foreground process is tmux), so tmux's bind is
the only fire. On any uncertainty it falls through to the original path and
the daemon dedup still backstops — a detection miss degrades to today's
behaviour, never to a broken paste.
- bin/kitty-paste-router.sh: the launch target for kitty's `map ctrl+v`
- bin/kitty-focused-is-tmux.py: parses `kitten @ ls --match state:focused`,
exits 0 only when the focused window's foreground process is tmux
- kitty.conf `map ctrl+v` now points at the router (deployed to ~/.local/bin)
Parser verified on 5 cases (tmux, non-tmux, empty, garbage, tmux-in-other-
window). Latency: the kitten round-trip only delays kitty's REDUNDANT
handler; tmux's fast bind already did the user-visible paste.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* #5 Log paste outcome + last-gate non-text guard on the text path
The daemon logged "PASTED text/image" = "I dispatched", which can't tell a
clean paste from a leak — this session's binary-wall and blob bugs were
invisible in the journal until the user screenshotted them.
- dispatch_text_paste now refuses to paste a payload that fails
looks_like_text (image magic / NUL / non-UTF8) and logs outcome=
rejected-nontext. The daemon must never dump image bytes into a pane as
text; this is the single chokepoint all text pastes flow through, so it's
the right last gate even though upstream staging should already reject.
- "PASTED text" now carries sent_bytes, html_sanitized, outcome=clean so a
sanitized or suppressed paste is visible in the log.
- looks_like_text made pub(crate) so paste.rs shares the ipc.rs check.
30/30 tests pass (+2 covering the guard decision). Daemon rebuilt + restarted.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* #6 + #1 Paste-correctness self-test + wire behavioral tests into CI
Every bug this session shipped because only Rust unit tests existed and
nothing exercised the actual paste-decision logic end to end. Add a headless,
mocked-clipboard regression suite that IS that net:
- tests/wl-paste-guard.test.sh: the image-coexistence guard (7 checks) —
image present ⇒ text reads return empty; image/png + list-types still work.
- tests/kitty-focused-is-tmux.test.sh: the router's tmux detector (5 checks) —
exit 0 only when the focused window's foreground process is tmux.
- bin/flashpaste-selftest.sh: one command runs the suite (+ `--rust` for
cargo test). Mocks the clipboard, so it needs no display and never touches
the user's real selection.
- flashpaste-doctor now runs the suite and a failure counts as a doctor fail.
- CI: new `behavior-tests` job in lint.yml runs the suite on every push/PR.
Suite green: 12/12 checks. Doctor integration verified.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* #8 Single-source keybindings + drift checker
The double-paste bug was config drift: kitty.conf and tmux.conf each bound
Ctrl+V independently and got out of sync. Rather than rewrite the user's
hand-tuned, richly-commented dotfiles (high risk, low marginal value now that
#2 fixed the actual handler), declare ONE canonical source and enforce it
read-only:
- config/keybindings.canonical: the source of truth — kitty ctrl+v must route
through kitty-paste-router.sh, tmux C-v through flashpaste-trigger.
- bin/flashpaste-keybindings-check.sh: read-only checker (env-overridable for
tests) that flags drift. Escapes ERE metachars so the literal '+' in ctrl+v
matches correctly (caught by its own test).
- flashpaste-doctor warns (not fails) on drift — it degrades to daemon dedup.
- tests/keybindings-check.test.sh: 4 fixture cases (consistent, kitty drift,
missing tmux, the ctrl+v literal-plus regression). In CI via the suite.
Live check passes: kitty + tmux consistent. Suite green: 16/16.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* #4 Refactor handle_paste into named phases (+ fix 2 pre-existing clippy lints)
handle_paste was a ~290-line god-function where the probe/override races
hide. Extract its three phases verbatim into named helpers so the hot path
reads as a sequence and each phase is reviewable in isolation:
- eager_screenshot_pickup() — inotify-lag screenshot pickup
- eager_live_image_pickup() — browser "Copy Image" bridge
- resolve_paste_intent() — the staged-slot + live-clipboard override cascade,
returns the StagedSelection to dispatch
handle_paste now reads: pickup → pickup → resolve → dispatch-match. Pure code
move, no behaviour change — proven by the unchanged 30/30 test suite, clippy
(-D warnings) and fmt both clean.
Also fixes two pre-existing lints that failed release-clippy: is_text_target
is test-only (now #[cfg(test)]); extract_blob_domain uses an array char
pattern. These predate this work but blocked the clippy gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* #3 + #7 ADRs: scope the bash-reduction ocean and the Wayland source-fix
Rather than force-implement two items that shouldn't be done blind, record
the decision and the concrete path for each:
- ADR 0006 (#3): the big bash surface (tmux-paste-dispatch.sh, 657 lines) is
the Tier-1 zero-dependency fallback BY DESIGN (ADR 0001), so folding it into
the daemon is partly a non-goal — it would delete the no-daemon guarantee.
Reframes #3 as bounded, test-first dedup of shim/daemon overlap, not a
multi-day rewrite. First slice named.
- ADR 0007 (#7): owning the Wayland clipboard via data-control on
wlroots/KDE would fix the blob leak at source (no shim needed), hooking the
existing WAYLAND_WEDGED latch. Deferred + guarded: untestable on the
maintainer's Mutter box, and shipping an unexercisable correctness path is a
liability. Test matrix specified before it can be Accepted.
Both indexed in docs/adr/README.md as Proposed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Harden router per review: fail-safe focus detection + empty-pane guard
Two LOW findings from independent review, both on the "never drop a paste"
guarantee:
- kitty-focused-is-tmux.py: require is_focused IS True (was: not False). On
old kitty that ignores --match and omits is_focused, no window qualifies, so
we report "not tmux" and the router lets the paste happen — instead of
risking a false-positive tmux match in a non-focused window that would
suppress a paste kitty should have handled. Added a regression test.
- kitty-paste-router.sh: only call flashpaste-trigger when the pane id is
non-empty; an empty pane (no tmux) goes straight to the image fallback
rather than handing the daemon a blank pane.
Self-test: 17/17.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fix double-paste: repair kitty router socket + widen/normalize daemon dedup
Two independent causes made a single paste land twice:
1. kitty-paste-router.sh could never reach kitty's remote-control socket.
It looked for `unix:$XDG_RUNTIME_DIR/kitty-main`, but kitty appends the
pid (`kitty-main-<pid>`), and KITTY_LISTEN_ON is absent from the env that
`launch --copy-env` copies. So `kitten @ ls` failed every time, the
focused-window-is-tmux probe always returned false, and the router never
suppressed kitty's redundant ctrl+v fire. Now resolves the socket via
KITTY_LISTEN_ON, else globs the real `kitty-main-<pid>` socket.
2. The daemon dedup missed the second fire. kitty launches the router as a
BACKGROUND process, so its paste can land well after tmux's bind already
pasted — measured 1.84s apart, past the 1000ms window. And the two paths
differ by a trailing newline ("+N" vs "+N+1 lines"), so they hashed to
different signatures and dodged content-dedup entirely. Widen the window
to 2500ms and trim trailing \n/\r in paste_signature so the variants
collide and the second is absorbed.
Verified: the daemon journal showed one keypress producing two requests
~1.8s apart with identical payload; real-wire capture confirmed flashpaste
itself delivers exactly one bracketed paste. All 15 flashpasted unit tests
pass (dedup window + trailing-newline cases added).
* router: resolve kitty socket via shell glob, not $(ls) (review nit from PR #1)
Avoids word-splitting on socket paths with spaces and drops the ls dependency.
Behaviour unchanged for the single-kitty-instance case.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>1 parent 9cf6b16 commit d81ae3c
15 files changed
Lines changed: 740 additions & 78 deletions
File tree
- .github/workflows
- bin
- config
- docs/adr
- rs/flashpasted/src
- tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
66 | 66 | | |
67 | 67 | | |
68 | 68 | | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
69 | 78 | | |
70 | 79 | | |
71 | 80 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
332 | 332 | | |
333 | 333 | | |
334 | 334 | | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
335 | 366 | | |
336 | 367 | | |
337 | 368 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
Lines changed: 66 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
0 commit comments