fix(ui): treat Cmd/Ctrl-held mouse events as BossTerm UI, not TUI#261
Conversation
ReviewSmall, focused fix that mirrors the existing shift-bypass pattern for hyperlink interaction in mouse-reporting TUIs. The intent is right and the comments do a good job of explaining the why and pointing at the symmetric handlers. A few things worth considering before merging. 1.
|
In a pane running a TUI that enables mouse reporting (claude, vim with set mouse=a, htop, nvim, …), Cmd-hovering a URL did not draw the underline and Cmd-clicking did not open the URL via BossTerm: - Move handler short-circuits to the TUI forward, so hoveredHyperlink never gets set — the renderer at TerminalCanvasRenderer.kt:833 needs it AND isModifierPressed to draw the underline. - Press/Release similarly forward to the TUI, so the Cmd-click handler at line ~1042 is never reached. (Some TUIs implement their own URL open on the forwarded click; many — including some Claude Code surfaces — do not.) Mirror the existing shift bypass for text selection: when the user is holding Cmd/Ctrl (tracked via isModifierPressed), skip the mouse- reporting forward in Press/Move/Release so the hyperlink hover / click handlers run locally. Scroll keeps current forwarding so Ctrl+wheel font-zoom and pager input in TUIs continue to work. Generated with [Claude Code](https://claude.com/claude-code)
…ed decision Two refinements from PR review: 1. The mouse-reporting bypass guards previously consulted isModifierPressed, which is updated by .onPreviewKeyEvent and therefore only reflects reality while the terminal canvas holds keyboard focus. Read the modifier directly off the AWT-backed PointerEvent (new isCtrlOrMetaPressed() extension in ComposeMouseEvent.kt) so the bypass works regardless of focus state — e.g. alt-tabbing back into BossTerm with Cmd already held, or focus parked on a sibling Compose element. Mirrors how isShiftPressed() already works. 2. Release would re-read the current modifier to decide whether to forward, which mishandles users who toggle Cmd between press and release (TUI ends up with an unpaired button-down or button-up). Record the actual Press decision in lastPressForwardedToTui and have Release / drag motion mirror that exact state instead of re-deriving it. Pure (no-button) hover motion keeps reading the per-event modifier — each motion event is independent. isModifierPressed is still the right signal for the renderer's underline gate and the local hyperlink-click branch, since both run inside the canvas-focused path. Generated with [Claude Code](https://claude.com/claude-code)
850c2e5 to
424bf88
Compare
Code reviewSolid, focused fix that mirrors the existing shift bypass and tracks Press/Release pairing correctly. The two pointer-state invariants here — (a) don't strand half a sequence at the TUI when modifiers toggle, (b) don't double-forward — both look right. A few notes: Press/Release pairing — looks correct
Potential concerns
Style / conventions
PerformanceNo concern. One extra modifier check per pointer event, all from already-extracted AWT modifier flags. SecurityN/A — pure UI input routing, no new external surface. TestsNo automated tests added, which is reasonable given how hard Compose pointer-event flows are to unit-test. The manual test plan in the PR description is well-structured; the unchecked regression items (plain shell URL click, tabbed mode + vim, non-link Cmd-click in TUI, Ctrl+wheel) are the right things to verify before merge. SummaryLGTM. The Press/Release pairing logic is the kind of detail that's easy to get wrong and you got it right on the first cut. The two questions above (mode-change race, hover-consumer staleness) are edge cases worth thinking about but don't block this PR. |
Cmd-clicking a hyperlink opens the URL via HyperlinkDetector.openUrl, which hands focus to the browser. Because the canvas never sees the Cmd/Ctrl KeyUp event, isModifierPressed stays true and the renderer keeps the underline drawn on the just-clicked link indefinitely (until the user re-presses + releases the modifier inside BossTerm). Clear isModifierPressed + hoveredHyperlink and force an immediate redraw right after openUrl so the underline drops at click time. The next Move event after the user re-engages will reset both naturally from cursor + modifier state. Generated with [Claude Code](https://claude.com/claude-code)
Code review — PR #261Solid, well-scoped fix. The pattern mirrors the existing shift-bypass nicely, and the A few things worth a closer look before merging. 1. Click handler still uses the focus-gated flag — Cmd-click from an unfocused canvas can no-opThe Press bypass at if (hoveredHyperlink != null && isModifierPressed) {
The press-time Suggest using the focus-independent read here too — if (hoveredHyperlink != null && (isModifierPressed || cmdOrCtrlHeld)) {Or simpler: just 2. Same focus-gating problem affects the hover underlineThe Move handler now sets Plumbing the modifier from the pointer event into the render context (or OR-ing in 3. Race when modifier read on Release diverges from PressThe 4. Minor
Security / performanceNothing concerning. The added work per pointer event is one extra bit-AND read; modifier flags are already computed. No new allocations, no I/O, no untrusted-input handling changes. Test coverageNo automated coverage added or available for this area (the file is composable-heavy and existing mouse-handler logic is untested by convention). The manual test plan in the PR description covers the critical paths; would push the unchecked plain-shell and Overall, ship-worthy after concern #1 is addressed — it's the same class of bug as the one being fixed (modifier-aware code path silently no-ops). Concerns #2 and #3 are good-to-haves. |
Summary
Sibling fix to #260 — same file, same mouse-reporting early-return pattern, different handlers.
In a pane running a TUI that enables mouse reporting (
claude,vimwithset mouse=a,htop,nvim, …):ProperTerminal.kt:1142, sohoveredHyperlinknever gets set — and the renderer atTerminalCanvasRenderer.kt:833requireshoveredHyperlink != null && isModifierPressed.:808forwards the click to the TUI, so the Cmd-click branch at:1042is never reached. Some TUIs implement their own URL opener on the forwarded click; many — including some Claude Code surfaces — do not.Fix mirrors the existing shift bypass for text selection: when the user is holding Cmd/Ctrl (tracked via
isModifierPressed, updated by the.onPreviewKeyEventat:1382-1395), skip the mouse-reporting forward in Press/Move/Release so the hyperlink hover/click handlers run locally. Scroll keeps current forwarding so Ctrl+wheel (TUI font-zoom, pager input) still works.Test plan
./gradlew :compose-ui:compileKotlinDesktop— clean../gradlew :embedded-example:run→ runclaude→ Cmd-hover a URL → underline appears → Cmd-click → browser opens.:bossterm-app:run):vimwithset mouse=a, hover/click URLs in a buffer — same behavior.Note
Lightly depends on #260's focus-restore for the keyboard-canvas-focused state that updates
isModifierPressed. If #260 lands first, this is mergeable as-is. If this lands first, both still work because clicking into a TUI pane will focus the canvas via the existing (un-fixed) path most of the time.Generated with Claude Code