Files
zero/internal/tui/input_compat.go
KRATOS b7f90a787f tui: right-click pastes the clipboard directly (no menu) (#231)
* fix(tui): right-click pastes the clipboard directly (no menu)

A right-click now pastes the OS clipboard straight into the focused input —
composer, provider/MCP wizard, or setup — with no context menu, mirroring the
bracketed-paste behavior. Keyboard and selection copy/paste are unchanged.

- clipboard.go: pasteFromClipboardCmd reads the clipboard off the Update
  goroutine; routePaste is the shared router for both bracketed paste (PasteMsg)
  and right-click paste (clipboardReadMsg), so the two behave identically.
- mouse.go: a right-click returns pasteFromClipboardCmd; input_compat.go adds the
  mouseRightPress helper.
- model.go: PasteMsg now routes through routePaste; clipboardReadMsg inserts on
  success or shows a brief "Paste failed" status when the clipboard can't be read
  (e.g. no clipboard utility on a remote session).

Tests: right-click returns the read command (chat + wizard), clipboard content
routes to the focused field, read errors surface a status, empty is a no-op.

* fix(tui): right-click paste in setup mode + scope the paste test

Setup mode routes mouse events to handleSetupMouse, which only handled
left-click and wheel — right-clicks were swallowed, so paste never fired
during onboarding. Add the same mouseRightPress -> pasteFromClipboardCmd
branch handleMouse uses; routePaste already targets the setup field.

Test: assert the right-click contract at handleMouse/handleSetupMouse
scope instead of via Update (which batches unrelated commands and masks a
regression), and cover the new setup-mode path.

Addresses review on #231: mouse.go (setup paste, critical) and
clipboard_test.go (weak Update-level assertion, minor).
2026-06-17 11:53:39 +05:30

95 lines
1.9 KiB
Go

package tui
import tea "charm.land/bubbletea/v2"
func keyCode(msg tea.KeyMsg) rune {
return msg.Key().Code
}
func keyText(msg tea.KeyMsg) string {
return msg.Key().Text
}
func keyRunes(msg tea.KeyMsg) []rune {
return []rune(keyText(msg))
}
func keyHasMod(msg tea.KeyMsg, mod tea.KeyMod) bool {
return msg.Key().Mod.Contains(mod)
}
func keyAlt(msg tea.KeyMsg) bool {
return keyHasMod(msg, tea.ModAlt)
}
func keyShift(msg tea.KeyMsg) bool {
return keyHasMod(msg, tea.ModShift)
}
func keyIs(msg tea.KeyMsg, code rune) bool {
return keyCode(msg) == code
}
func keyCtrl(msg tea.KeyMsg, code rune) bool {
return keyCode(msg) == code && keyHasMod(msg, tea.ModCtrl)
}
func keyCtrlArrow(msg tea.KeyMsg, code rune) bool {
return keyIs(msg, code) && keyHasMod(msg, tea.ModCtrl)
}
func keyPrintable(msg tea.KeyMsg) bool {
return keyText(msg) != "" && !keyAlt(msg) && !keyHasMod(msg, tea.ModCtrl)
}
func keyBackspace(msg tea.KeyMsg) bool {
return keyIs(msg, tea.KeyBackspace) || keyCtrl(msg, 'h')
}
func mouseEvent(msg tea.MouseMsg) tea.Mouse {
return msg.Mouse()
}
func mouseX(msg tea.MouseMsg) int {
return mouseEvent(msg).X
}
func mouseY(msg tea.MouseMsg) int {
return mouseEvent(msg).Y
}
func mouseLeftPress(msg tea.MouseMsg) bool {
event := mouseEvent(msg)
return event.Button == tea.MouseLeft && isMouseClick(msg)
}
func mouseRightPress(msg tea.MouseMsg) bool {
event := mouseEvent(msg)
return event.Button == tea.MouseRight && isMouseClick(msg)
}
func mouseMotion(msg tea.MouseMsg) bool {
_, ok := msg.(tea.MouseMotionMsg)
return ok
}
func mouseRelease(msg tea.MouseMsg) bool {
_, ok := msg.(tea.MouseReleaseMsg)
return ok
}
func mouseWheelUp(msg tea.MouseMsg) bool {
event := mouseEvent(msg)
return event.Button == tea.MouseWheelUp
}
func mouseWheelDown(msg tea.MouseMsg) bool {
event := mouseEvent(msg)
return event.Button == tea.MouseWheelDown
}
func isMouseClick(msg tea.MouseMsg) bool {
_, ok := msg.(tea.MouseClickMsg)
return ok
}