mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-28 18:26:03 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4486c86271 | ||
|
|
b44a369295 | ||
|
|
498bc457fc | ||
|
|
7f957f059b | ||
|
|
1a4af359ec | ||
|
|
f16bec2ce3 | ||
|
|
798fe4a1ef | ||
|
|
bb2e5fa886 | ||
|
|
3067ff1cac | ||
|
|
312feff130 | ||
|
|
ee60102fdc | ||
|
|
7b969a03ad | ||
|
|
1f7e5d7767 | ||
|
|
80dd695d7a | ||
|
|
65edd99fd0 | ||
|
|
98bf8f86d1 |
@@ -26,7 +26,9 @@ If applicable, add screenshots to help explain your problem.
|
||||
### Version info:
|
||||
|
||||
* _Run `lazygit --version` and paste the result here_
|
||||
* _Run `git --version` and paste the result here_
|
||||
|
||||
### Terminal info:
|
||||
What terminal are you using, and which version? For some types of bugs this information can be relevant.
|
||||
|
||||
### Additional context
|
||||
Add any other context about the problem here.
|
||||
|
||||
@@ -12,13 +12,13 @@ require (
|
||||
github.com/aybabtme/humanlog v0.4.1
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
|
||||
github.com/creack/pty v1.1.11
|
||||
github.com/gdamore/tcell/v2 v2.13.5
|
||||
github.com/gdamore/tcell/v2 v2.13.7
|
||||
github.com/go-errors/errors v1.5.1
|
||||
github.com/gookit/color v1.4.2
|
||||
github.com/integrii/flaggy v1.4.0
|
||||
github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c
|
||||
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20260103133810-b7e030324985
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20260111170441-330357056207
|
||||
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
|
||||
github.com/karimkhaleel/jsonschema v0.0.0-20231001195015-d933f0d94ea3
|
||||
|
||||
@@ -95,8 +95,8 @@ github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
|
||||
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
|
||||
github.com/gdamore/tcell/v2 v2.13.5 h1:YvWYCSr6gr2Ovs84dXbZLjDuOfQchhj8buOEqY52rpA=
|
||||
github.com/gdamore/tcell/v2 v2.13.5/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo=
|
||||
github.com/gdamore/tcell/v2 v2.13.7 h1:yfHdeC7ODIYCc6dgRos8L1VujQtXHmUpU6UZotzD6os=
|
||||
github.com/gdamore/tcell/v2 v2.13.7/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
|
||||
@@ -189,8 +189,8 @@ github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c h1:tC2Paiis
|
||||
github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c/go.mod h1:F2fEBk0ddf6ixrBrJjY7phfQ3hL9rXG0uSjvwYe50bE=
|
||||
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd h1:ViKj6qth8FgcIWizn9KiACWwPemWSymx62OPN0tHT+Q=
|
||||
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd/go.mod h1:lRhCiBr6XjQrvcQVa+UYsy/99d3wMXn/a0nSQlhnhlA=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20260103133810-b7e030324985 h1:qdjGSiNnlGtoi+nzyERQJvee50JpJjeQ6sEhP7jCfMo=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20260103133810-b7e030324985/go.mod h1:lQCd2TvvNXVKFBowy4A7xxZbUp+1KEiGs4j0Q5Zt9gQ=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20260111170441-330357056207 h1:YYXHnZVN9nt2UsBO6dYPVPRDVJuEVcGIzhM0SH9x4/Q=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20260111170441-330357056207/go.mod h1:lQCd2TvvNXVKFBowy4A7xxZbUp+1KEiGs4j0Q5Zt9gQ=
|
||||
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=
|
||||
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5/go.mod h1:qxN4mHOAyeIDLP7IK7defgPClM/z1Kze8VVQiaEjzsQ=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
|
||||
@@ -85,6 +85,6 @@ func (self *RemoteCommands) GetRemoteURL(remoteName string) (string, error) {
|
||||
Arg("--get-url", remoteName).
|
||||
ToArgv()
|
||||
|
||||
url, err := self.cmd.New(cmdArgs).RunWithOutput()
|
||||
url, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||||
return strings.TrimSpace(url), err
|
||||
}
|
||||
|
||||
@@ -68,7 +68,8 @@ func NewCommitFilesContext(c *ContextCommon) *CommitFilesContext {
|
||||
},
|
||||
}
|
||||
|
||||
ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(ctx.OnSearchSelect))
|
||||
ctx.GetView().SetRenderSearchStatus(ctx.SearchTrait.RenderSearchStatus)
|
||||
ctx.GetView().SetOnSelectItem(ctx.OnSearchSelect)
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@ type ListContextTrait struct {
|
||||
// If renderOnlyVisibleLines is true, needRerenderVisibleLines indicates whether we need to
|
||||
// rerender the visible lines e.g. because the scroll position changed
|
||||
needRerenderVisibleLines bool
|
||||
|
||||
// true if we're inside the OnSearchSelect call; in that case we don't want to update the search
|
||||
// result index.
|
||||
inOnSearchSelect bool
|
||||
}
|
||||
|
||||
func (self *ListContextTrait) IsListContext() {}
|
||||
@@ -31,6 +35,10 @@ func (self *ListContextTrait) IsListContext() {}
|
||||
func (self *ListContextTrait) FocusLine(scrollIntoView bool) {
|
||||
self.Context.FocusLine(scrollIntoView)
|
||||
|
||||
// Need to capture this in a local variable because by the time the AfterLayout function runs,
|
||||
// the field will have been reset to false already
|
||||
inOnSearchSelect := self.inOnSearchSelect
|
||||
|
||||
// Doing this at the end of the layout function because we need the view to be
|
||||
// resized before we focus the line, otherwise if we're in accordion mode
|
||||
// the view could be squashed and won't know how to adjust the cursor/origin.
|
||||
@@ -40,6 +48,9 @@ func (self *ListContextTrait) FocusLine(scrollIntoView bool) {
|
||||
|
||||
self.GetViewTrait().FocusPoint(
|
||||
self.ModelIndexToViewIndex(self.list.GetSelectedLineIdx()), scrollIntoView)
|
||||
if !inOnSearchSelect {
|
||||
self.GetView().SetNearestSearchPosition()
|
||||
}
|
||||
|
||||
selectRangeIndex, isSelectingRange := self.list.GetRangeStartIdx()
|
||||
if isSelectingRange {
|
||||
@@ -117,10 +128,11 @@ func (self *ListContextTrait) HandleRender() {
|
||||
self.setFooter()
|
||||
}
|
||||
|
||||
func (self *ListContextTrait) OnSearchSelect(selectedLineIdx int) error {
|
||||
func (self *ListContextTrait) OnSearchSelect(selectedLineIdx int) {
|
||||
self.GetList().SetSelection(self.ViewIndexToModelIndex(selectedLineIdx))
|
||||
self.inOnSearchSelect = true
|
||||
self.HandleFocus(types.OnFocusOpts{})
|
||||
return nil
|
||||
self.inOnSearchSelect = false
|
||||
}
|
||||
|
||||
func (self *ListContextTrait) IsItemVisible(item types.HasUrn) bool {
|
||||
|
||||
@@ -134,7 +134,8 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
|
||||
},
|
||||
}
|
||||
|
||||
ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(ctx.OnSearchSelect))
|
||||
ctx.GetView().SetRenderSearchStatus(ctx.SearchTrait.RenderSearchStatus)
|
||||
ctx.GetView().SetOnSelectItem(ctx.OnSearchSelect)
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@ func NewMainContext(
|
||||
SearchTrait: NewSearchTrait(c),
|
||||
}
|
||||
|
||||
ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(func(int) error { return nil }))
|
||||
ctx.GetView().SetRenderSearchStatus(ctx.SearchTrait.RenderSearchStatus)
|
||||
ctx.GetView().SetOnSelectItem(func(int) {})
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@ type PatchExplorerContext struct {
|
||||
getIncludedLineIndices func() []int
|
||||
c *ContextCommon
|
||||
mutex deadlock.Mutex
|
||||
|
||||
// true if we're inside the OnSelectItem callback; in that case we don't want to update the
|
||||
// search result index.
|
||||
inOnSelectItemCallback bool
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -49,14 +53,14 @@ func NewPatchExplorerContext(
|
||||
SearchTrait: NewSearchTrait(c),
|
||||
}
|
||||
|
||||
ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(
|
||||
func(selectedLineIdx int) error {
|
||||
ctx.GetMutex().Lock()
|
||||
defer ctx.GetMutex().Unlock()
|
||||
ctx.NavigateTo(selectedLineIdx)
|
||||
return nil
|
||||
}),
|
||||
)
|
||||
ctx.GetView().SetRenderSearchStatus(ctx.SearchTrait.RenderSearchStatus)
|
||||
ctx.GetView().SetOnSelectItem(func(selectedLineIdx int) {
|
||||
ctx.GetMutex().Lock()
|
||||
defer ctx.GetMutex().Unlock()
|
||||
ctx.inOnSelectItemCallback = true
|
||||
ctx.NavigateTo(selectedLineIdx)
|
||||
ctx.inOnSelectItemCallback = false
|
||||
})
|
||||
|
||||
ctx.SetHandleRenderFunc(ctx.OnViewWidthChanged)
|
||||
|
||||
@@ -113,6 +117,10 @@ func (self *PatchExplorerContext) FocusSelection() {
|
||||
// As far as the view is concerned, we are always selecting a range
|
||||
view.SetRangeSelectStart(startIdx)
|
||||
view.SetCursorY(endIdx - newOriginY)
|
||||
|
||||
if !self.inOnSelectItemCallback {
|
||||
view.SetNearestSearchPosition()
|
||||
}
|
||||
}
|
||||
|
||||
func (self *PatchExplorerContext) GetContentToRender() string {
|
||||
|
||||
@@ -36,20 +36,6 @@ func (self *SearchTrait) ClearSearchString() {
|
||||
// used for type switch
|
||||
func (self *SearchTrait) IsSearchableContext() {}
|
||||
|
||||
func (self *SearchTrait) onSelectItemWrapper(innerFunc func(int) error) func(int, int, int) error {
|
||||
return func(selectedLineIdx int, index int, total int) error {
|
||||
self.RenderSearchStatus(index, total)
|
||||
|
||||
if total != 0 {
|
||||
if err := innerFunc(selectedLineIdx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (self *SearchTrait) RenderSearchStatus(index int, total int) {
|
||||
keybindingConfig := self.c.UserConfig().Keybinding
|
||||
|
||||
|
||||
@@ -134,7 +134,8 @@ func NewSubCommitsContext(
|
||||
},
|
||||
}
|
||||
|
||||
ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(ctx.OnSearchSelect))
|
||||
ctx.GetView().SetRenderSearchStatus(ctx.SearchTrait.RenderSearchStatus)
|
||||
ctx.GetView().SetOnSelectItem(ctx.OnSearchSelect)
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -56,7 +56,8 @@ func NewWorkingTreeContext(c *ContextCommon) *WorkingTreeContext {
|
||||
},
|
||||
}
|
||||
|
||||
ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(ctx.OnSearchSelect))
|
||||
ctx.GetView().SetRenderSearchStatus(ctx.SearchTrait.RenderSearchStatus)
|
||||
ctx.GetView().SetOnSelectItem(ctx.OnSearchSelect)
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -137,6 +137,7 @@ func (self *FixupHelper) HandleFindBaseCommitForFixupPress() error {
|
||||
}
|
||||
|
||||
self.c.Contexts().LocalCommits.SetSelection(index)
|
||||
self.c.Contexts().LocalCommits.FocusLine(true)
|
||||
self.c.Context().Push(self.c.Contexts().LocalCommits, types.OnFocusOpts{})
|
||||
return nil
|
||||
},
|
||||
|
||||
@@ -108,20 +108,15 @@ func (self *SearchHelper) Confirm() error {
|
||||
return self.CancelPrompt()
|
||||
}
|
||||
|
||||
var err error
|
||||
switch state.SearchType() {
|
||||
case types.SearchTypeFilter:
|
||||
self.ConfirmFilter()
|
||||
case types.SearchTypeSearch:
|
||||
err = self.ConfirmSearch()
|
||||
self.ConfirmSearch()
|
||||
case types.SearchTypeNone:
|
||||
self.c.Context().Pop()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return self.c.ResetKeybindings()
|
||||
}
|
||||
|
||||
@@ -144,13 +139,13 @@ func (self *SearchHelper) ConfirmFilter() {
|
||||
self.c.Context().Pop()
|
||||
}
|
||||
|
||||
func (self *SearchHelper) ConfirmSearch() error {
|
||||
func (self *SearchHelper) ConfirmSearch() {
|
||||
state := self.searchState()
|
||||
|
||||
context, ok := state.Context.(types.ISearchableContext)
|
||||
if !ok {
|
||||
self.c.Log.Warnf("Context %s is searchable", state.Context.GetKey())
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
searchString := self.promptContent()
|
||||
@@ -161,7 +156,7 @@ func (self *SearchHelper) ConfirmSearch() error {
|
||||
|
||||
self.c.Context().Pop()
|
||||
|
||||
return context.GetView().Search(searchString, modelSearchResults(context))
|
||||
context.GetView().Search(searchString, modelSearchResults(context))
|
||||
}
|
||||
|
||||
func modelSearchResults(context types.ISearchableContext) []gocui.SearchPosition {
|
||||
|
||||
@@ -11,6 +11,10 @@ var Search = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
// Creating a branch avoids that searching for 't' will unexpectedly match the first commit
|
||||
// (since it finds it in the extra info line, which is "HEAD -> master")
|
||||
shell.NewBranch("branch")
|
||||
|
||||
shell.EmptyCommit("one")
|
||||
shell.EmptyCommit("two")
|
||||
shell.EmptyCommit("three")
|
||||
@@ -103,6 +107,54 @@ var Search = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Contains("three"),
|
||||
Contains("two"),
|
||||
Contains("one").IsSelected(),
|
||||
).
|
||||
NavigateToLine(Contains("three")).
|
||||
Tap(func() {
|
||||
t.Views().Search().IsVisible().Content(Contains("matches for 'o' (1 of 3)"))
|
||||
}).
|
||||
Press("N").
|
||||
Tap(func() {
|
||||
t.Views().Search().IsVisible().Content(Contains("matches for 'o' (1 of 3)"))
|
||||
}).
|
||||
Lines(
|
||||
Contains("four").IsSelected(),
|
||||
Contains("three"),
|
||||
Contains("two"),
|
||||
Contains("one"),
|
||||
).
|
||||
Press(keys.Universal.StartSearch).
|
||||
Tap(func() {
|
||||
t.ExpectSearch().
|
||||
Type("t").
|
||||
Confirm()
|
||||
|
||||
t.Views().Search().IsVisible().Content(Contains("matches for 't' (1 of 2)"))
|
||||
}).
|
||||
Lines(
|
||||
Contains("four"),
|
||||
Contains("three").IsSelected(),
|
||||
Contains("two"),
|
||||
Contains("one"),
|
||||
).
|
||||
SelectPreviousItem().
|
||||
Tap(func() {
|
||||
t.Views().Search().IsVisible().Content(Contains("matches for 't' (1 of 2)"))
|
||||
}).
|
||||
Lines(
|
||||
Contains("four").IsSelected(),
|
||||
Contains("three"),
|
||||
Contains("two"),
|
||||
Contains("one"),
|
||||
).
|
||||
Press("n").
|
||||
Tap(func() {
|
||||
t.Views().Search().IsVisible().Content(Contains("matches for 't' (1 of 2)"))
|
||||
}).
|
||||
Lines(
|
||||
Contains("four"),
|
||||
Contains("three").IsSelected(),
|
||||
Contains("two"),
|
||||
Contains("one"),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
+83
-41
@@ -84,6 +84,7 @@ type inputProcessor struct {
|
||||
evch chan<- Event
|
||||
rows int // used for clipping mouse coordinates
|
||||
cols int // used for clipping mouse coordinates
|
||||
surrogate rune
|
||||
nested *inputProcessor
|
||||
}
|
||||
|
||||
@@ -135,8 +136,9 @@ type csiParamMode struct {
|
||||
}
|
||||
|
||||
type keyMap struct {
|
||||
Key Key
|
||||
Mod ModMask
|
||||
Key Key
|
||||
Mod ModMask
|
||||
Rune rune
|
||||
}
|
||||
|
||||
var csiAllKeys = map[csiParamMode]keyMap{
|
||||
@@ -275,41 +277,70 @@ var csiAllKeys = map[csiParamMode]keyMap{
|
||||
}
|
||||
|
||||
// keys reported using Kitty csi-u protocol
|
||||
var csiUKeys = map[int]Key{
|
||||
27: KeyESC,
|
||||
9: KeyTAB,
|
||||
13: KeyEnter,
|
||||
127: KeyBS,
|
||||
57358: KeyCapsLock,
|
||||
57359: KeyScrollLock,
|
||||
57360: KeyNumLock,
|
||||
57361: KeyPrint,
|
||||
57362: KeyPause,
|
||||
57363: KeyMenu,
|
||||
57376: KeyF13,
|
||||
57377: KeyF14,
|
||||
57378: KeyF15,
|
||||
57379: KeyF16,
|
||||
57380: KeyF17,
|
||||
57381: KeyF18,
|
||||
57382: KeyF19,
|
||||
57383: KeyF20,
|
||||
57384: KeyF21,
|
||||
57385: KeyF22,
|
||||
57386: KeyF23,
|
||||
57387: KeyF24,
|
||||
57388: KeyF25,
|
||||
57389: KeyF26,
|
||||
57390: KeyF27,
|
||||
57391: KeyF28,
|
||||
57392: KeyF29,
|
||||
57393: KeyF30,
|
||||
57394: KeyF31,
|
||||
57395: KeyF32,
|
||||
57396: KeyF33,
|
||||
57397: KeyF34,
|
||||
57398: KeyF35,
|
||||
// TODO: KP keys
|
||||
var csiUKeys = map[int]keyMap{
|
||||
27: {Key: KeyESC},
|
||||
9: {Key: KeyTAB},
|
||||
13: {Key: KeyEnter},
|
||||
127: {Key: KeyBS},
|
||||
57358: {Key: KeyCapsLock},
|
||||
57359: {Key: KeyScrollLock},
|
||||
57360: {Key: KeyNumLock},
|
||||
57361: {Key: KeyPrint},
|
||||
57362: {Key: KeyPause},
|
||||
57363: {Key: KeyMenu},
|
||||
57376: {Key: KeyF13},
|
||||
57377: {Key: KeyF14},
|
||||
57378: {Key: KeyF15},
|
||||
57379: {Key: KeyF16},
|
||||
57380: {Key: KeyF17},
|
||||
57381: {Key: KeyF18},
|
||||
57382: {Key: KeyF19},
|
||||
57383: {Key: KeyF20},
|
||||
57384: {Key: KeyF21},
|
||||
57385: {Key: KeyF22},
|
||||
57386: {Key: KeyF23},
|
||||
57387: {Key: KeyF24},
|
||||
57388: {Key: KeyF25},
|
||||
57389: {Key: KeyF26},
|
||||
57390: {Key: KeyF27},
|
||||
57391: {Key: KeyF28},
|
||||
57392: {Key: KeyF29},
|
||||
57393: {Key: KeyF30},
|
||||
57394: {Key: KeyF31},
|
||||
57395: {Key: KeyF32},
|
||||
57396: {Key: KeyF33},
|
||||
57397: {Key: KeyF34},
|
||||
57398: {Key: KeyF35},
|
||||
57399: {Key: KeyRune, Rune: '0'}, // KP 0
|
||||
57400: {Key: KeyRune, Rune: '1'}, // KP 1
|
||||
57401: {Key: KeyRune, Rune: '2'}, // KP 2
|
||||
57402: {Key: KeyRune, Rune: '3'}, // KP 3
|
||||
57403: {Key: KeyRune, Rune: '4'}, // KP 4
|
||||
57404: {Key: KeyRune, Rune: '5'}, // KP 5
|
||||
57405: {Key: KeyRune, Rune: '6'}, // KP 6
|
||||
57406: {Key: KeyRune, Rune: '7'}, // KP 7
|
||||
57407: {Key: KeyRune, Rune: '8'}, // KP 8
|
||||
57408: {Key: KeyRune, Rune: '9'}, // KP 9
|
||||
57409: {Key: KeyRune, Rune: '.'}, // KP_DECIMAL
|
||||
57410: {Key: KeyRune, Rune: '/'}, // KP_DIVIDE
|
||||
57411: {Key: KeyRune, Rune: '*'}, // KP_MULTIPLY
|
||||
57412: {Key: KeyRune, Rune: '-'}, // KP_SUBTRACT
|
||||
57413: {Key: KeyRune, Rune: '+'}, // KP_ADD
|
||||
57414: {Key: KeyEnter}, // KP_ENTER
|
||||
57415: {Key: KeyRune, Rune: '='}, // KP_EQUAL
|
||||
57416: {Key: KeyClear}, // KP_SEPARATOR
|
||||
57417: {Key: KeyLeft}, // KP_LEFT
|
||||
57418: {Key: KeyRight}, // KP_RIGHT
|
||||
57419: {Key: KeyUp}, // KP_UP
|
||||
57420: {Key: KeyDown}, // KP_DOWN
|
||||
57421: {Key: KeyPgUp}, // KP_PG_UP
|
||||
57422: {Key: KeyPgDn}, // KP_PG_DN
|
||||
57423: {Key: KeyHome}, // KP_HOME
|
||||
57424: {Key: KeyEnd}, // KP_END
|
||||
57425: {Key: KeyInsert}, // KP_INSERT
|
||||
57426: {Key: KeyDelete}, // KP_DELETE
|
||||
// 57427: {Key: KeyBegin}, // KP_BEGIN
|
||||
|
||||
// TODO: Media keys
|
||||
}
|
||||
|
||||
@@ -318,8 +349,8 @@ var winKeys = map[int]Key{
|
||||
0x03: KeyCancel, // vkCancel
|
||||
0x08: KeyBackspace, // vkBackspace
|
||||
0x09: KeyTab, // vkTab
|
||||
0x0c: KeyClear, // vClear
|
||||
0x0d: KeyEnter, // vkReturn
|
||||
0x12: KeyClear, // vClear
|
||||
0x13: KeyPause, // vkPause
|
||||
0x1b: KeyEscape, // vkEscape
|
||||
0x21: KeyPgUp, // vkPrior
|
||||
@@ -701,11 +732,22 @@ func (ip *inputProcessor) handleWinKey(P []int) {
|
||||
} else if chr < ' ' && P[0] >= 0x41 && P[0] <= 0x5a {
|
||||
key = Key(P[0])
|
||||
chr = 0
|
||||
} else if key == 0x11 || key == 0x13 || key == 0x14 {
|
||||
|
||||
} else if chr >= 0xD800 && chr <= 0xDBFF {
|
||||
// high surrogate pair
|
||||
ip.surrogate = chr
|
||||
return
|
||||
} else if chr >= 0xDC00 && chr <= 0xDFFF {
|
||||
// low surrogate pair
|
||||
chr = utf16.DecodeRune(ip.surrogate, chr)
|
||||
} else if P[0] == 0x10 || P[0] == 0x11 || P[0] == 0x12 || P[0] == 0x14 {
|
||||
// lone modifiers
|
||||
ip.surrogate = 0
|
||||
return
|
||||
}
|
||||
|
||||
ip.surrogate = 0
|
||||
|
||||
// Modifiers
|
||||
if P[4]&0x010 != 0 {
|
||||
mod |= ModShift
|
||||
@@ -791,8 +833,8 @@ func (ip *inputProcessor) handleCsi(mode rune, params []byte, intermediate []byt
|
||||
key := KeyRune
|
||||
chr := rune(0)
|
||||
if k1, ok := csiUKeys[P0]; ok {
|
||||
key = k1
|
||||
chr = 0
|
||||
key = k1.Key
|
||||
chr = k1.Rune
|
||||
} else {
|
||||
chr = rune(P0)
|
||||
}
|
||||
|
||||
+8
@@ -622,6 +622,14 @@ func (t *tScreen) drawCell(x, y int) int {
|
||||
width = 1
|
||||
str = " "
|
||||
}
|
||||
if width > 1 {
|
||||
// Clobber over any content in the next cell.
|
||||
// This fixes a problem with some terminals where overwriting two
|
||||
// adjacent single cells with a wide rune would leave an image
|
||||
// of the second cell. This is a workaround for buggy terminals.
|
||||
t.writeString(" \b\b")
|
||||
}
|
||||
|
||||
t.writeString(str)
|
||||
t.cx += width
|
||||
t.cells.SetDirty(x, y, false)
|
||||
|
||||
+23
-11
@@ -1,4 +1,4 @@
|
||||
// Copyright 2025 The TCell Authors
|
||||
// Copyright 2026 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
@@ -72,6 +72,7 @@ type winTty struct {
|
||||
oomode uint32 // original output mode
|
||||
oscreen consoleInfo
|
||||
wg sync.WaitGroup
|
||||
surrogate rune
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
@@ -126,7 +127,7 @@ func (w *winTty) Drain() error {
|
||||
|
||||
func (w *winTty) getConsoleInput() error {
|
||||
// cancelFlag comes first as WaitForMultipleObjects returns the lowest index
|
||||
// in the event that both events are signalled.
|
||||
// in the event that both events are signaled.
|
||||
waitObjects := []syscall.Handle{w.cancelFlag, w.in}
|
||||
|
||||
// As arrays are contiguous in memory, a pointer to the first object is the
|
||||
@@ -158,19 +159,30 @@ func (w *winTty) getConsoleInput() error {
|
||||
if rv == 0 {
|
||||
return er
|
||||
}
|
||||
loop:
|
||||
for i := range nrec {
|
||||
ir := rec[i]
|
||||
switch ir.typ {
|
||||
case keyEvent:
|
||||
chr := ir.data[10] // we only see ASCII, key down events in VT mode
|
||||
|
||||
// because we use win32-input-mode, we will only
|
||||
// see US-ASCII characters - (Q: will they be
|
||||
// 16-bit values with possible surrogate pairs?)
|
||||
select {
|
||||
case w.buf <- chr:
|
||||
case <-w.stopQ:
|
||||
break
|
||||
// we normally only expect to see ascii, but paste data may come in as UTF-16.
|
||||
wc := rune(binary.LittleEndian.Uint16(ir.data[10:]))
|
||||
if wc >= 0xD800 && wc <= 0xDBFF {
|
||||
// if it was a high surrogate, which happens for pasted UTF-16,
|
||||
// then save it until we get the low and can decode it.
|
||||
w.surrogate = wc
|
||||
continue
|
||||
} else if wc >= 0xDC00 && wc <= 0xDFFF {
|
||||
wc = utf16.DecodeRune(w.surrogate, wc)
|
||||
}
|
||||
w.surrogate = 0
|
||||
for _, chr := range []byte(string(wc)) {
|
||||
// We normally expect only to see ASCII (win32-input-mode),
|
||||
// but apparently pasted data can arrive in UTF-16 here.
|
||||
select {
|
||||
case w.buf <- chr:
|
||||
case <-w.stopQ:
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
case resizeEvent:
|
||||
|
||||
+2
-2
@@ -1313,9 +1313,9 @@ func (g *Gui) onKey(ev *GocuiEvent) error {
|
||||
// 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'.
|
||||
// KeyCtrlJ (int value 10) is '\r'.
|
||||
if g.IsPasting && ev.Key == KeyCtrlJ {
|
||||
ev.Key = KeyCtrlM
|
||||
ev.Key = KeyEnter
|
||||
}
|
||||
|
||||
err := g.execKeybindings(g.currentView, ev)
|
||||
|
||||
+124
-19
@@ -217,29 +217,53 @@ type searcher struct {
|
||||
searchPositions []SearchPosition
|
||||
modelSearchResults []SearchPosition
|
||||
currentSearchIndex int
|
||||
onSelectItem func(int, int, int) error
|
||||
onSelectItem func(int)
|
||||
renderSearchStatus func(int, int)
|
||||
}
|
||||
|
||||
func (v *View) SetOnSelectItem(onSelectItem func(int, int, int) error) {
|
||||
func (v *View) SetRenderSearchStatus(renderSearchStatus func(int, int)) {
|
||||
v.searcher.renderSearchStatus = renderSearchStatus
|
||||
}
|
||||
|
||||
func (v *View) SetOnSelectItem(onSelectItem func(int)) {
|
||||
v.searcher.onSelectItem = onSelectItem
|
||||
}
|
||||
|
||||
func (v *View) renderSearchStatus(index int, itemCount int) {
|
||||
if v.searcher.renderSearchStatus != nil {
|
||||
v.searcher.renderSearchStatus(index, itemCount)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *View) gotoNextMatch() error {
|
||||
if len(v.searcher.searchPositions) == 0 {
|
||||
return nil
|
||||
}
|
||||
if v.Highlight && v.oy+v.cy < v.searcher.searchPositions[v.searcher.currentSearchIndex].Y {
|
||||
// If the selection is before the current match, just jump to the current match and return.
|
||||
// This can only happen if the user has moved the cursor to before the first match.
|
||||
v.SelectSearchResult(v.searcher.currentSearchIndex)
|
||||
return nil
|
||||
}
|
||||
if v.searcher.currentSearchIndex >= len(v.searcher.searchPositions)-1 {
|
||||
v.searcher.currentSearchIndex = 0
|
||||
} else {
|
||||
v.searcher.currentSearchIndex++
|
||||
}
|
||||
return v.SelectSearchResult(v.searcher.currentSearchIndex)
|
||||
v.SelectSearchResult(v.searcher.currentSearchIndex)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *View) gotoPreviousMatch() error {
|
||||
if len(v.searcher.searchPositions) == 0 {
|
||||
return nil
|
||||
}
|
||||
if v.Highlight && v.oy+v.cy > v.searcher.searchPositions[v.searcher.currentSearchIndex].Y {
|
||||
// If the selection is after the current match, just jump to the current match and return.
|
||||
// This happens if the user has moved the cursor down from the current match.
|
||||
v.SelectSearchResult(v.searcher.currentSearchIndex)
|
||||
return nil
|
||||
}
|
||||
if v.searcher.currentSearchIndex == 0 {
|
||||
if len(v.searcher.searchPositions) > 0 {
|
||||
v.searcher.currentSearchIndex = len(v.searcher.searchPositions) - 1
|
||||
@@ -247,13 +271,14 @@ func (v *View) gotoPreviousMatch() error {
|
||||
} else {
|
||||
v.searcher.currentSearchIndex--
|
||||
}
|
||||
return v.SelectSearchResult(v.searcher.currentSearchIndex)
|
||||
v.SelectSearchResult(v.searcher.currentSearchIndex)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *View) SelectSearchResult(index int) error {
|
||||
func (v *View) SelectSearchResult(index int) {
|
||||
itemCount := len(v.searcher.searchPositions)
|
||||
if itemCount == 0 {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
if index > itemCount-1 {
|
||||
index = itemCount - 1
|
||||
@@ -262,10 +287,10 @@ func (v *View) SelectSearchResult(index int) error {
|
||||
y := v.searcher.searchPositions[index].Y
|
||||
|
||||
v.FocusPoint(v.ox, y, true)
|
||||
v.renderSearchStatus(index, itemCount)
|
||||
if v.searcher.onSelectItem != nil {
|
||||
return v.searcher.onSelectItem(y, index, itemCount)
|
||||
v.searcher.onSelectItem(y)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Returns <current match index>, <total matches>
|
||||
@@ -294,26 +319,29 @@ func (v *View) UpdateSearchResults(str string, modelSearchResults []SearchPositi
|
||||
if len(v.searcher.searchPositions) > 0 {
|
||||
// get the first result past the current cursor
|
||||
currentIndex := 0
|
||||
adjustedY := v.oy + v.cy
|
||||
adjustedX := v.ox + v.cx
|
||||
for i, pos := range v.searcher.searchPositions {
|
||||
if pos.Y > adjustedY || (pos.Y == adjustedY && pos.XStart > adjustedX) {
|
||||
currentIndex = i
|
||||
break
|
||||
if v.Highlight {
|
||||
// ...but only if we're showing the highlighted line
|
||||
adjustedY := v.oy + v.cy
|
||||
adjustedX := v.ox + v.cx
|
||||
for i, pos := range v.searcher.searchPositions {
|
||||
if pos.Y > adjustedY || (pos.Y == adjustedY && pos.XStart > adjustedX) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
v.searcher.currentSearchIndex = currentIndex
|
||||
}
|
||||
}
|
||||
|
||||
func (v *View) Search(str string, modelSearchResults []SearchPosition) error {
|
||||
func (v *View) Search(str string, modelSearchResults []SearchPosition) {
|
||||
v.UpdateSearchResults(str, modelSearchResults)
|
||||
|
||||
if len(v.searcher.searchPositions) > 0 {
|
||||
return v.SelectSearchResult(v.searcher.currentSearchIndex)
|
||||
v.SelectSearchResult(v.searcher.currentSearchIndex)
|
||||
} else {
|
||||
v.renderSearchStatus(0, 0)
|
||||
}
|
||||
|
||||
return v.searcher.onSelectItem(-1, -1, 0)
|
||||
}
|
||||
|
||||
func (v *View) ClearSearch() {
|
||||
@@ -324,8 +352,37 @@ func (v *View) IsSearching() bool {
|
||||
return v.searcher.searchString != ""
|
||||
}
|
||||
|
||||
func (v *View) nearestSearchPosition() int {
|
||||
currentLineIndex := v.cy + v.oy
|
||||
lastSearchPos := 0
|
||||
for i, pos := range v.searcher.searchPositions {
|
||||
if pos.Y == currentLineIndex {
|
||||
return i
|
||||
}
|
||||
if pos.Y > currentLineIndex {
|
||||
break
|
||||
}
|
||||
lastSearchPos = i
|
||||
}
|
||||
return lastSearchPos
|
||||
}
|
||||
|
||||
func (v *View) SetNearestSearchPosition() {
|
||||
if len(v.searcher.searchPositions) > 0 {
|
||||
newPos := v.nearestSearchPosition()
|
||||
if newPos != v.searcher.currentSearchIndex {
|
||||
v.searcher.currentSearchIndex = newPos
|
||||
v.renderSearchStatus(newPos, len(v.searcher.searchPositions))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v *View) FocusPoint(cx int, cy int, scrollIntoView bool) {
|
||||
lineCount := len(v.lines)
|
||||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
|
||||
v.refreshViewLinesIfNeeded()
|
||||
lineCount := len(v.viewLines)
|
||||
if cy < 0 || cy > lineCount {
|
||||
return
|
||||
}
|
||||
@@ -1757,6 +1814,52 @@ func (v *View) setContentLineCount(lineCount int) {
|
||||
v.lines = v.lines[:lineCount]
|
||||
}
|
||||
|
||||
// If the current search result is no longer visible after a scroll up, select the last search
|
||||
// result that is visible in the view, if any, or the first one that is below the view if none is
|
||||
// visible.
|
||||
func (v *View) selectVisibleSearchResultAfterScrollUp() {
|
||||
if !v.Highlight && len(v.searcher.searchPositions) != 0 {
|
||||
windowBottom := v.oy + v.InnerHeight()
|
||||
if v.searcher.searchPositions[v.searcher.currentSearchIndex].Y >= windowBottom {
|
||||
newSearchIndex := v.searcher.currentSearchIndex
|
||||
for newSearchIndex > 0 &&
|
||||
v.searcher.searchPositions[newSearchIndex-1].Y >= v.oy {
|
||||
newSearchIndex--
|
||||
if v.searcher.searchPositions[newSearchIndex].Y < windowBottom {
|
||||
break
|
||||
}
|
||||
}
|
||||
if v.searcher.currentSearchIndex != newSearchIndex {
|
||||
v.searcher.currentSearchIndex = newSearchIndex
|
||||
v.renderSearchStatus(newSearchIndex, len(v.searcher.searchPositions))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the current search result is no longer visible after a scroll down, select the first search
|
||||
// result that is visible in the view, if any, or the last one that is above the view if none is
|
||||
// visible.
|
||||
func (v *View) selectVisibleSearchResultAfterScrollDown() {
|
||||
if !v.Highlight && len(v.searcher.searchPositions) != 0 {
|
||||
if v.searcher.searchPositions[v.searcher.currentSearchIndex].Y < v.oy {
|
||||
newSearchIndex := v.searcher.currentSearchIndex
|
||||
windowBottom := v.oy + v.InnerHeight()
|
||||
for newSearchIndex+1 < len(v.searcher.searchPositions) &&
|
||||
v.searcher.searchPositions[newSearchIndex+1].Y < windowBottom {
|
||||
newSearchIndex++
|
||||
if v.searcher.searchPositions[newSearchIndex].Y >= v.oy {
|
||||
break
|
||||
}
|
||||
}
|
||||
if v.searcher.currentSearchIndex != newSearchIndex {
|
||||
v.searcher.currentSearchIndex = newSearchIndex
|
||||
v.renderSearchStatus(newSearchIndex, len(v.searcher.searchPositions))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v *View) ScrollUp(amount int) {
|
||||
if amount > v.oy {
|
||||
amount = v.oy
|
||||
@@ -1767,6 +1870,7 @@ func (v *View) ScrollUp(amount int) {
|
||||
v.cy += amount
|
||||
|
||||
v.clearHover()
|
||||
v.selectVisibleSearchResultAfterScrollUp()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1778,6 +1882,7 @@ func (v *View) ScrollDown(amount int) {
|
||||
v.cy -= adjustedAmount
|
||||
|
||||
v.clearHover()
|
||||
v.selectVisibleSearchResultAfterScrollDown()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+2
-2
@@ -88,7 +88,7 @@ github.com/fatih/color
|
||||
# github.com/gdamore/encoding v1.0.1
|
||||
## explicit; go 1.9
|
||||
github.com/gdamore/encoding
|
||||
# github.com/gdamore/tcell/v2 v2.13.5
|
||||
# github.com/gdamore/tcell/v2 v2.13.7
|
||||
## explicit; go 1.24.0
|
||||
github.com/gdamore/tcell/v2
|
||||
github.com/gdamore/tcell/v2/terminfo
|
||||
@@ -211,7 +211,7 @@ github.com/jesseduffield/go-git/v5/utils/merkletrie/internal/frame
|
||||
github.com/jesseduffield/go-git/v5/utils/merkletrie/noder
|
||||
github.com/jesseduffield/go-git/v5/utils/sync
|
||||
github.com/jesseduffield/go-git/v5/utils/trace
|
||||
# github.com/jesseduffield/gocui v0.3.1-0.20260103133810-b7e030324985
|
||||
# github.com/jesseduffield/gocui v0.3.1-0.20260111170441-330357056207
|
||||
## explicit; go 1.25
|
||||
github.com/jesseduffield/gocui
|
||||
# github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
|
||||
|
||||
Reference in New Issue
Block a user