From 6346dfa98247843592a764d34280e1ec0422d7cb Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Wed, 5 Feb 2025 10:06:26 +0100 Subject: [PATCH 1/3] Add Gui.IsPasting Recognize bracketed paste events, and set a flag on Gui if a paste is active. Application code can use this to handle carriage returns or tabs differently when they come from a paste operation. --- gui.go | 6 ++++++ tcell_driver.go | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/gui.go b/gui.go index 87cc2832..9d8a89d5 100644 --- a/gui.go +++ b/gui.go @@ -157,6 +157,8 @@ type Gui struct { // If Mouse is true then mouse events will be enabled. Mouse bool + IsPasting bool + // If InputEsc is true, when ESC sequence is in the buffer and it doesn't // match any known sequence, ESC means KeyEsc. InputEsc bool @@ -759,6 +761,7 @@ func (g *Gui) MainLoop() error { }() Screen.EnableFocus() + Screen.EnablePaste() previousEnableMouse := false for { @@ -847,6 +850,9 @@ func (g *Gui) handleEvent(ev *GocuiEvent) error { return nil case eventFocus: return g.onFocus(ev) + case eventPaste: + g.IsPasting = ev.Start + return nil default: return nil } diff --git a/tcell_driver.go b/tcell_driver.go index 96e816b2..4199f7ab 100644 --- a/tcell_driver.go +++ b/tcell_driver.go @@ -155,6 +155,8 @@ type gocuiEventType uint8 // The 'MouseX' and 'MouseY' fields are valid if 'Type' is 'eventMouse'. // The 'Width' and 'Height' fields are valid if 'Type' is 'eventResize'. // The 'Focused' field is valid if 'Type' is 'eventFocus'. +// The 'Start' field is valid if 'Type' is 'eventPaste'. It is true for the +// beginning of a paste operation, false for the end. // The 'Err' field is valid if 'Type' is 'eventError'. type GocuiEvent struct { Type gocuiEventType @@ -167,6 +169,7 @@ type GocuiEvent struct { MouseX int MouseY int Focused bool + Start bool N int } @@ -178,6 +181,7 @@ const ( eventMouse eventMouseMove // only used when no button is down, otherwise it's eventMouse eventFocus + eventPaste eventInterrupt eventError eventRaw @@ -417,6 +421,11 @@ func (g *Gui) pollEvent() GocuiEvent { Type: eventFocus, Focused: tev.Focused, } + case *tcell.EventPaste: + return GocuiEvent{ + Type: eventPaste, + Start: tev.Start(), + } default: return GocuiEvent{Type: eventNone} } From 2fa292e62a5dd5af9bb93e32b3dc7c9f5dbbe9f9 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Wed, 5 Feb 2025 10:07:56 +0100 Subject: [PATCH 2/3] Work around Ghostty quirk with pasted newlines When pasting, Ghostty seems to convert all \n to \r for some reason. Convert them back to \n so that we can handle them normally. I have to admit that I'm confused as to why this is necessary. From what I understand from Ghostty's source code (e.g. https://github.com/ghostty-org/ghostty/commit/010338354a0) it does this conversion only for non-bracketed paste mode, but I'm seeing it in bracketed paste mode, and it is only there that we convert it back. --- gui.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/gui.go b/gui.go index 9d8a89d5..702991ef 100644 --- a/gui.go +++ b/gui.go @@ -1311,6 +1311,20 @@ func (g *Gui) onKey(ev *GocuiEvent) error { switch ev.Type { case eventKey: + // When pasting text in Ghostty, it sends us '\r' instead of '\n' for + // newlines. I actually don't quite understand why, because from reading + // Ghostty's source code (e.g. + // https://github.com/ghostty-org/ghostty/commit/010338354a0) it does + // this conversion only for non-bracketed paste mode, but I'm seeing it + // in bracketed paste mode. Whatever I'm missing here, converting '\r' + // back to '\n' fixes pasting multi-line text from Ghostty, and doesn't + // seem harmful for other terminal emulators. + // + // KeyCtrlJ (int value 10) is '\r', and KeyCtrlM (int value 13) is '\n'. + if g.IsPasting && ev.Key == KeyCtrlJ { + ev.Key = KeyCtrlM + } + err := g.execKeybindings(g.currentView, ev) if err != nil { return err From f9c4dffd0a89f18c6b2bcde7e8c92e5adff69a57 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Wed, 5 Feb 2025 10:20:45 +0100 Subject: [PATCH 3/3] Don't allow pasting into non-editable views My assumption is that pressing command-V on a non-editable view is always a mistake; it would execute arbitrary commands depending on what's in the clipboard, so prevent it. --- gui.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gui.go b/gui.go index 702991ef..03d55912 100644 --- a/gui.go +++ b/gui.go @@ -1489,6 +1489,10 @@ func (g *Gui) execKeybindings(v *View, ev *GocuiEvent) error { var globalKb *keybinding var matchingParentViewKb *keybinding + if g.IsPasting && v != nil && !v.Editable { + return nil + } + // if we're searching, and we've hit n/N/Esc, we ignore the default keybinding if v != nil && v.IsSearching() && ev.Mod == ModNone { if eventMatchesKey(ev, g.NextSearchMatchKey) {