Repository: jesseduffield/lazygit (language: go)

Issue:
Freezes when pasting a string that includes a line breaks
**Describe the bug**

In Commit message mode,
Pasting text with multiple line breaks causes the lazygit to freeze.
There seems to be no solution other than quitting the terminal emulator.

**To Reproduce**

Steps to reproduce the behavior:

1. Open lazygit
2. Input 'c' to go commit mode
3. Paste with `ctrl` + `shift` + `v`
4. Pasting only gets to the middle of the first line and then stops
5. Shell freezes

OK sample

```text
fix(heirline): unify background color scheme

```

NG sample

```text
fix(heirline): unify background color scheme

Updated background color assignments in the heirline configuration to use
`scheme.bg` instead of local scheme values. This change ensures a consistent
background color across different UI elements.
```

**Expected behavior**

If there are two line breaks at the end of the first line,
it has the same effect as tab and the remaining text is pasted into the commit description.

**Tried**

I tried it in the following environment, but the symptoms are the same:

- Powershell7 in WezTerm
- Powershell7 in Windows Terminal
- Powershell7 in Alacritty
- cmd.exe in Windows Terminal

**Version info:**

lazygit 

```text
commit=c03b89227092b852d50015d289a7c6d8c69811c5, build date=2025-01-17T13:37:10Z, build source=binaryRelease, version=0.45.2, os=windows, arch=amd64, git version=2.42.0.windows.1
```

git 

```text
git version 2.42.0.windows.1
```

**Additional context**

config https://github.com/ShortArrow/dotfiles/blob/develop/lazygit/config.yaml

logs

```text
Jan 27 09:16:45 |INFO| git rev-parse --symbolic-full-name main@{u} (161.9271ms)
Jan 27 09:16:45 |DEBU| RunCommand command="git -C C:/Users/shoutarou_t/Documents/GitHub/dotfiles rev-parse --path-format=absolute --absolute-git-dir"
Jan 27 09:16:46 |INFO| postRefreshUpdate for tags took 0s
Jan 27 09:16:46 |INFO| git rev-parse --abbrev-ref --verify
HEAD (78.9773ms)
Jan 27 09:16:46 |INFO| git for-each-ref --sort=refname --format=%(refname:short) refs/remotes (79.8178ms)
Jan 27 09:16:46 |INFO| postRefreshUpdate for remotes took 0s
Jan 27 09:16:46 |INFO| postRefreshUpdate for remoteBranches took 0s
Jan 27 09:16:46 |INFO| git -c log.showSignature=false log -g --abbrev=40 --format=%h%x00%ct%x00%gs%x00%p (89.8188ms)
Jan 27 09:16:46 |INFO| postRefreshUpdate for reflogCommits
took 0s
Jan 27 09:16:46 |DEBU| RunCommand command="git merge-base HEAD refs/remotes/origin/main"
Jan 27 09:16:46 |DEBU| RunCommand command="git merge-base develop develop@{u}"
Jan 27 09:16:46 |DEBU| RunCommand command="git for-each-ref --sort=-committerdate --format=%(HEAD)%00%(refname:short)%00%(upstream:short)%00%(upstream:track)%00%(push:track)%00%(subject)%00%(objectname)%00%(committerdate:unix) refs/heads"
Jan 27 09:16:46 |INFO| git merge-base develop develop@{u} (101.7162ms)
Jan 27 09:16:46 |INFO| git for-each-ref --sort=-committerdate --format=%(HEAD)%00%(refname:short)%00%(upstream:short)%00%(upstream:track)%00%(push:track)%00%(subject)%00%(objectname)%00%(committerdate:unix) refs/heads (100.1921ms)
Jan 27 09:16:46 |INFO| postRefreshUpdate for localBranches
took 21.8µs
Jan 27 09:16:46 |INFO| git merge-base HEAD refs/remotes/origin/main (135.4556ms)
Jan 27 09:16:46 |INFO| git log HEAD --topo-order --oneline
--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s --abbrev=40 -300 --no-show-signature -- (148.0885ms)
Jan 27 09:16:46 |DEBU| using cache for key rebase.updateRefs
Jan 27 09:16:46 |INFO| postRefreshUpdate for commits took 10.003ms
Jan 27 09:16:46 |DEBU| using cache for key rebase.updateRefs
Jan 27 09:16:55 |INFO| Heap memory in use: 3.6 MB
Jan 27 09:16:55 |INFO| refreshing the following scopes in sync mode: files
Jan 27 09:16:55 |INFO| refreshed merge conflicts in 0s
Jan 27 09:16:55 |DEBU| using cache for key status.showUntrackedFiles
Jan 27 09:16:55 |DEBU| RunCommand command="git status --untracked-files=all --porcelain -z --find-renames=50%"
Jan 27 09:16:55 |INFO| git status --untracked-files=all --porcelain -z --find-renames=50% (75.4137ms)
Jan 27 09:16:55 |INFO| refreshed files in 83.1826ms
Jan 27 09:16:55 |INFO| Refresh took 83.281ms
Jan 27 09:17:05 |INFO| Heap memory in use: 3.7 MB
Jan 27 09:17:05 |INFO| refreshing the following scopes in sync mode: files
Jan 27 09:17:05 |INFO| refreshed merge conflicts in 0s
Jan 27 09:17:05 |DEBU| using cache for key status.showUntrackedFiles
Jan 27 09:17:05 |DEBU| RunCommand command="git status --untracked-files=all --porcelain -z --find-renames=50%"
Jan 27 09:17:05 |INFO| git status --untracked-files=all --porcelain -z --find-renames=50% (69.4437ms)
Jan 27 09:17:05 |INFO| refreshed files in 80.6637ms
Jan 27 09:17:05 |INFO| Refresh took 80.6637ms
Jan 27 09:17:15 |INFO| refreshing the following scopes in sync mode: files
Jan 27 09:17:15 |INFO| Heap memory in use: 3.9 MB
Jan 27 09:17:15 |INFO| refreshed merge conflicts in 514.1µs

Jan 27 09:17:15 |DEBU| using cache for key status.showUntrackedFiles
Jan 27 09:17:15 |DEBU| RunCommand command="git status --untracked-files=all --porcelain -z --find-renames=50%"
Jan 27 09:17:15 |INFO| git status --untracked-files=all --porcelain -z --find-renames=50% (74.0176ms)
```



Discussion / hints:
I suppose this is a duplicate of #3151 (and #4066). Whether it freezes or otherwise behaves badly depends on what's in the clipboard.

Here's a PR that should fix this: #4234. Please test if you can build from source.
https://github.com/user-attachments/assets/68ee3374-3029-4930-9855-f9f090d58c84

Maybe, 943d78f3308aaf85393a08135b5d238ae1323e36 is no effects for this issue for me.
@ShortArrow The reason why #4234 doesn't work for you is that you're on Windows, and handling the bracketed paste events from the terminal is not supported yet by the tcell library on Windows. I'll look into whether this can be added, and make a PR in tcell if so. But I see this as independent from #4234, and I don't want this to hold up merging that one as it's already an improvement for Mac and Linux users.
@stefanhaller  Based on your information, it looks like this is unrelated to #4234. It's probably the tcell bug you mentioned. In that case, I think it's better to leave this issue and merge it.

Fix patch:
diff --git a/go.mod b/go.mod
index a8db1594636..b2da578e38d 100644
--- a/go.mod
+++ b/go.mod
@@ -16,7 +16,7 @@ require (
 	github.com/integrii/flaggy v1.4.0
 	github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68
 	github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d
-	github.com/jesseduffield/gocui v0.3.1-0.20250207131741-38a8ffbf24fe
+	github.com/jesseduffield/gocui v0.3.1-0.20250210123912-aba68ae65951
 	github.com/jesseduffield/kill v0.0.0-20250101124109-e216ddbe133a
 	github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
 	github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
diff --git a/go.sum b/go.sum
index 6d2364b82b1..d1259e9963b 100644
--- a/go.sum
+++ b/go.sum
@@ -188,8 +188,8 @@ github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 h1:EQP2Tv8T
 github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk=
 github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d h1:bO+OmbreIv91rCe8NmscRwhFSqkDJtzWCPV4Y+SQuXE=
 github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
-github.com/jesseduffield/gocui v0.3.1-0.20250207131741-38a8ffbf24fe h1:lNTwIp53mU5pfKYFinIsbUsd6mNxMit4IXcJUnn1Pc0=
-github.com/jesseduffield/gocui v0.3.1-0.20250207131741-38a8ffbf24fe/go.mod h1:sLIyZ2J42R6idGdtemZzsiR3xY5EF0KsvYEGh3dQv3s=
+github.com/jesseduffield/gocui v0.3.1-0.20250210123912-aba68ae65951 h1:7/3M0yosAM9/aLAjTfzSJWhsWjT860ZVe4T76RPwE2k=
+github.com/jesseduffield/gocui v0.3.1-0.20250210123912-aba68ae65951/go.mod h1:sLIyZ2J42R6idGdtemZzsiR3xY5EF0KsvYEGh3dQv3s=
 github.com/jesseduffield/kill v0.0.0-20250101124109-e216ddbe133a h1:UDeJ3EBk04bXDLOPvuqM3on8HvyJfISw0+UMqW+0a4g=
 github.com/jesseduffield/kill v0.0.0-20250101124109-e216ddbe133a/go.mod h1:FSWDLKT0NQpntbDd1H3lbz51fhCVlMzy/J0S6nM727Q=
 github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=
diff --git a/pkg/gui/controllers/commit_description_controller.go b/pkg/gui/controllers/commit_description_controller.go
index aea6cfbdf1a..4337a1e4b0d 100644
--- a/pkg/gui/controllers/commit_description_controller.go
+++ b/pkg/gui/controllers/commit_description_controller.go
@@ -28,7 +28,7 @@ func (self *CommitDescriptionController) GetKeybindings(opts types.KeybindingsOp
 	bindings := []*types.Binding{
 		{
 			Key:     opts.GetKey(opts.Config.Universal.TogglePanel),
-			Handler: self.switchToCommitMessage,
+			Handler: self.handleTogglePanel,
 		},
 		{
 			Key:     opts.GetKey(opts.Config.Universal.Return),
@@ -75,6 +75,32 @@ func (self *CommitDescriptionController) switchToCommitMessage() error {
 	return nil
 }
 
+func (self *CommitDescriptionController) handleTogglePanel() error {
+	// The default keybinding for this action is "<tab>", which means that we
+	// also get here when pasting multi-line text that contains tabs. In that
+	// case we don't want to toggle the panel, but insert the tab as a character
+	// (somehow, see below).
+	//
+	// Only do this if the TogglePanel command is actually mapped to "<tab>"
+	// (the default). If it's not, we can only hope that it's mapped to some
+	// ctrl key or fn key, which is unlikely to occur in pasted text. And if
+	// they mapped some *other* command to "<tab>", then we're totally out of
+	// luck.
+	if self.c.GocuiGui().IsPasting && self.c.UserConfig().Keybinding.Universal.TogglePanel == "<tab>" {
+		// Handling tabs in pasted commit messages is not optimal, but hopefully
+		// good enough for now. We simply insert 4 spaces without worrying about
+		// column alignment. This works well enough for leading indentation,
+		// which is common in pasted code snippets.
+		view := self.Context().GetView()
+		for range 4 {
+			view.Editor.Edit(view, gocui.KeySpace, ' ', 0)
+		}
+		return nil
+	}
+
+	return self.switchToCommitMessage()
+}
+
 func (self *CommitDescriptionController) close() error {
 	self.c.Helpers().Commits.CloseCommitMessagePanel()
 	return nil
diff --git a/pkg/gui/controllers/commit_message_controller.go b/pkg/gui/controllers/commit_message_controller.go
index 28168ef1801..6f077380172 100644
--- a/pkg/gui/controllers/commit_message_controller.go
+++ b/pkg/gui/controllers/commit_message_controller.go
@@ -48,7 +48,7 @@ func (self *CommitMessageController) GetKeybindings(opts types.KeybindingsOpts)
 		},
 		{
 			Key:     opts.GetKey(opts.Config.Universal.TogglePanel),
-			Handler: self.switchToCommitDescription,
+			Handler: self.handleTogglePanel,
 		},
 		{
 			Key:     opts.GetKey(opts.Config.CommitMessage.CommitMenu),
@@ -105,6 +105,32 @@ func (self *CommitMessageController) switchToCommitDescription() error {
 	return nil
 }
 
+func (self *CommitMessageController) handleTogglePanel() error {
+	// The default keybinding for this action is "<tab>", which means that we
+	// also get here when pasting multi-line text that contains tabs. In that
+	// case we don't want to toggle the panel, but insert the tab as a character
+	// (somehow, see below).
+	//
+	// Only do this if the TogglePanel command is actually mapped to "<tab>"
+	// (the default). If it's not, we can only hope that it's mapped to some
+	// ctrl key or fn key, which is unlikely to occur in pasted text. And if
+	// they mapped some *other* command to "<tab>", then we're totally out of
+	// luck.
+	if self.c.GocuiGui().IsPasting && self.c.UserConfig().Keybinding.Universal.TogglePanel == "<tab>" {
+		// It is unlikely that a pasted commit message contains a tab in the
+		// subject line, so it shouldn't matter too much how we handle it.
+		// Simply insert 4 spaces instead; all that matters is that we don't
+		// switch to the description panel.
+		view := self.context().GetView()
+		for range 4 {
+			view.Editor.Edit(view, gocui.KeySpace, ' ', 0)
+		}
+		return nil
+	}
+
+	return self.switchToCommitDescription()
+}
+
 func (self *CommitMessageController) handleCommitIndexChange(value int) error {
 	currentIndex := self.context().GetSelectedIndex()
 	newIndex := currentIndex + value
@@ -140,6 +166,20 @@ func (self *CommitMessageController) setCommitMessageAtIndex(index int) (bool, e
 }
 
 func (self *CommitMessageController) confirm() error {
+	// The default keybinding for this action is "<enter>", which means that we
+	// also get here when pasting multi-line text that contains newlines. In
+	// that case we don't want to confirm the commit, but switch to the
+	// description panel instead so that the rest of the pasted text goes there.
+	//
+	// Only do this if the SubmitEditorText command is actually mapped to
+	// "<enter>" (the default). If it's not, we can only hope that it's mapped
+	// to some ctrl key or fn key, which is unlikely to occur in pasted text.
+	// And if they mapped some *other* command to "<enter>", then we're totally
+	// out of luck.
+	if self.c.GocuiGui().IsPasting && self.c.UserConfig().Keybinding.Universal.SubmitEditorText == "<enter>" {
+		return self.switchToCommitDescription()
+	}
+
 	return self.c.Helpers().Commits.HandleCommitConfirm()
 }
 
diff --git a/vendor/github.com/jesseduffield/gocui/gui.go b/vendor/github.com/jesseduffield/gocui/gui.go
index 87cc2832115..03d55912a3f 100644
--- a/vendor/github.com/jesseduffield/gocui/gui.go
+++ b/vendor/github.com/jesseduffield/gocui/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
 	}
@@ -1305,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
@@ -1469,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) {
diff --git a/vendor/github.com/jesseduffield/gocui/tcell_driver.go b/vendor/github.com/jesseduffield/gocui/tcell_driver.go
index 96e816b2f95..4199f7abb03 100644
--- a/vendor/github.com/jesseduffield/gocui/tcell_driver.go
+++ b/vendor/github.com/jesseduffield/gocui/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}
 	}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 6a6d839a61c..0f7e1746272 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -171,7 +171,7 @@ github.com/jesseduffield/go-git/v5/utils/merkletrie/filesystem
 github.com/jesseduffield/go-git/v5/utils/merkletrie/index
 github.com/jesseduffield/go-git/v5/utils/merkletrie/internal/frame
 github.com/jesseduffield/go-git/v5/utils/merkletrie/noder
-# github.com/jesseduffield/gocui v0.3.1-0.20250207131741-38a8ffbf24fe
+# github.com/jesseduffield/gocui v0.3.1-0.20250210123912-aba68ae65951
 ## explicit; go 1.12
 github.com/jesseduffield/gocui
 # github.com/jesseduffield/kill v0.0.0-20250101124109-e216ddbe133a


Test patch: