mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-09 18:08:31 -10:00
* fix(tui): paste clipboard images with Ctrl+V Ctrl+V did nothing when the clipboard held a screenshot, while right-click paste attached it. The image plumbing was already there: routePaste probes the OS clipboard for an image whenever a paste arrives with empty text. The gap is that a clipboard holding only an image produces no bracketed paste at all, since the terminal has no text to send, so routePaste never runs and its probe never fires. Right-click reached the probe only because that path always delivers a clipboardReadMsg, empty or not. Bubble's own Ctrl+V binding stays disabled, so nothing else was listening for the key. Handle Ctrl+V by issuing the image probe directly. It reads image content only, never text, so bracketed paste remains the single text path and a Ctrl+V with text on the clipboard is unaffected. The probe yields no message when there is no image, keeping it a silent no-op, and it is gated on noBlockingModal so a permission prompt or picker still swallows the key. readClipboardImage becomes a var so the tests stub it instead of depending on the machine's real clipboard. Fixes #534 * fix(tui): also probe the clipboard on Cmd+V, and test the real key route Two things raised in review, both fair. The handler matched keyCtrl only, which is ModCtrl. macOS reports Command as ModSuper, as the rest of the TUI already models it, so Cmd+V never reached the image probe even though the change claimed to cover it. It now matches both, via a keySuper helper alongside the existing modifier helpers. Worth being precise about the limit: this only helps where the terminal delivers the key to the application. A terminal that handles Cmd+V itself pastes the clipboard TEXT and sends no key event at all, so an image-only clipboard still produces nothing for this to react to. That is a separate problem from the one being fixed here. The tests also did not exercise the route they were named for. One asserted that Update returned some non-nil command and then ran a freshly built probe; the other never called Update at all. Both now stub the reader BEFORE Update and run the command Update actually returned. That matters more than it sounds. Making the branch return a different non-nil command leaves the old assertion passing and fails the new one, which is exactly the hole: the old test proved a command was issued, not that it was the image probe. Removing the route entirely, or reverting to ModCtrl only, also fails now. * fix(imageinput): report an unreadable clipboard image instead of silence On macOS the clipboard reader shells to pngpaste or a PyObjC one-liner, and neither ships with the OS: pngpaste is Homebrew-only and Apple dropped the bundled PyObjC. A stock Mac therefore fails every time, and the failure was returned as (nil, nil), which the caller reads as "no image on the clipboard" and treats as a silent no-op. Pressing Ctrl+V produced nothing at all: no image, no error, no hint that anything was missing. The darwin path already knows the difference. It probes with `clipboard info` first and only reaches the extraction step once the clipboard is confirmed to hold an image, so a failure there is not absence, it is "there is an image and nothing here can read it". That now returns ErrClipboardImageUnreadable, which the existing error path turns into a transcript notice naming what to install. The defect predates this branch, but this branch is what makes the path reachable from Ctrl+V, so shipping without it would close #534 with a feature that is inert on one of the three platforms. Raised by gnanam, who measured it on a real Mac rather than inferring it. Deliberately narrow. The Linux arm cannot tell "no tool" from "no image" without probing separately, and Windows uses PowerShell, which is always present, so neither is touched. The test drives the whole path: stubbed reader returns the error, Ctrl+V issues the probe, and the assertion is that the failure reaches the transcript as an error row rather than stopping earlier. Swallowing the error again makes it fail. * test(imageinput): cover the darwin unreadable-clipboard decision itself The test added with the fix asserted around the change rather than on it. It substituted ReadClipboardImage with a stub returning the error, which proves the TUI surfaces an error handed to it, but never reaches readClipboardImageDarwin, where the decision lives. Reverting both production return sites to nil,nil left every package green, which is the definition of a test that is not testing the change. Caught by gnanam. readClipboardImageDarwin shells out twice and had no seam, so the two steps are now package-level vars, the same shape as the readClipboardImage var this change already relied on. That makes the case that matters reachable: the info probe succeeds and reports an image, and the extractor then fails. It cannot be arranged from outside the function, because from there both steps are one opaque call. Five cases, and the negative ones carry as much weight as the positive. An image with no working extractor, and an extractor that exits 0 with no bytes, must both be actionable. A clipboard offering no image, and a failing info probe, must both stay silent, because turning those into a notice would make every stray Ctrl+V nag the user. The extractor must not run at all in either silent case. Repeating gnanam's experiment now fails: reverting both return sites to nil,nil fails the two actionable cases and leaves the silent ones passing, which is exactly the discrimination the fix is supposed to add.
103 lines
2.3 KiB
Go
103 lines
2.3 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)
|
|
}
|
|
|
|
// keySuper matches the Command key on macOS, which the rest of the TUI models as
|
|
// ModSuper (see keybindings.go). Kept separate from keyCtrl because a binding
|
|
// that means Ctrl on Linux and Windows usually means Command on macOS, and the
|
|
// two arrive as different modifiers.
|
|
func keySuper(msg tea.KeyMsg, code rune) bool {
|
|
return keyCode(msg) == code && keyHasMod(msg, tea.ModSuper)
|
|
}
|
|
|
|
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
|
|
}
|