detached cb47167

refactor: remove redundant hasGhostText field and simplify ghost state

Remove hasGhostText bool — ghostSuffix != "" serves the same purpose.
Defer logger allocation past early-return guards in onEntryChanged.
Remove leftover local replace directive for puregotk.
commit
cb471676a8bb848fcbba1dab83fd03ae58b21e40
author
brice · · 2026-03-27 19:39 UTC
parent
7eb8e31
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +17 -22
M go.mod +0 -2
@@ -25,8 +25,6 @@ require (
2525
2626 replace github.com/ebitengine/purego => github.com/bnema/purego v0.0.0-20260323095903-91aa177a2704
2727
28-replace github.com/bnema/puregotk => /home/brice/dev/projects/puregotk
29-
3028 require (
3129 github.com/atotto/clipboard v0.1.4 // indirect
3230 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
@@ -25,8 +25,6 @@ require (
25 25
26 replace github.com/ebitengine/purego => github.com/bnema/purego v0.0.0-20260323095903-91aa177a270426 replace github.com/ebitengine/purego => github.com/bnema/purego v0.0.0-20260323095903-91aa177a2704
27 27
28-replace github.com/bnema/puregotk => /home/brice/dev/projects/puregotk
29-
30 require (28 require (
31 github.com/atotto/clipboard v0.1.4 // indirect29 github.com/atotto/clipboard v0.1.4 // indirect
32 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect30 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
M go.sum +2 -0
@@ -8,6 +8,8 @@ github.com/bnema/purego v0.0.0-20260323095903-91aa177a2704 h1:0GaiO4102gOdp0es0M
88 github.com/bnema/purego v0.0.0-20260323095903-91aa177a2704/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
99 github.com/bnema/purego-sqlite v0.1.1 h1:lH/F6/xxaI+LwWNHv3zof83bDJWQnib9OlNnBo/JnII=
1010 github.com/bnema/purego-sqlite v0.1.1/go.mod h1:nRVrkBt8dFOlSGGOOFM/ynlk34TOU1RlOvH4T9KM91s=
11+github.com/bnema/puregotk v0.2.1 h1:Fgx52I4pLf+RnBbazhXU6FmPp+37fYcRq3pdhD8WWns=
12+github.com/bnema/puregotk v0.2.1/go.mod h1:Sl24sbCl02M/HLCNUgaYVCckkYcGOi8yxMuGuQVWsL8=
1113 github.com/bnema/puregotk-webkit v0.0.15 h1:CUcuoHfx+7AO1Diig7+NrEb7i5m/+MKTiQFUMH3j72k=
1214 github.com/bnema/puregotk-webkit v0.0.15/go.mod h1:jbMK8QD+J34OE24HUc19v7FUcbSYvsTd7Cs8x1Ab/14=
1315 github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
@@ -8,6 +8,8 @@ github.com/bnema/purego v0.0.0-20260323095903-91aa177a2704 h1:0GaiO4102gOdp0es0M
8 github.com/bnema/purego v0.0.0-20260323095903-91aa177a2704/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=8 github.com/bnema/purego v0.0.0-20260323095903-91aa177a2704/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
9 github.com/bnema/purego-sqlite v0.1.1 h1:lH/F6/xxaI+LwWNHv3zof83bDJWQnib9OlNnBo/JnII=9 github.com/bnema/purego-sqlite v0.1.1 h1:lH/F6/xxaI+LwWNHv3zof83bDJWQnib9OlNnBo/JnII=
10 github.com/bnema/purego-sqlite v0.1.1/go.mod h1:nRVrkBt8dFOlSGGOOFM/ynlk34TOU1RlOvH4T9KM91s=10 github.com/bnema/purego-sqlite v0.1.1/go.mod h1:nRVrkBt8dFOlSGGOOFM/ynlk34TOU1RlOvH4T9KM91s=
11+github.com/bnema/puregotk v0.2.1 h1:Fgx52I4pLf+RnBbazhXU6FmPp+37fYcRq3pdhD8WWns=
12+github.com/bnema/puregotk v0.2.1/go.mod h1:Sl24sbCl02M/HLCNUgaYVCckkYcGOi8yxMuGuQVWsL8=
11 github.com/bnema/puregotk-webkit v0.0.15 h1:CUcuoHfx+7AO1Diig7+NrEb7i5m/+MKTiQFUMH3j72k=13 github.com/bnema/puregotk-webkit v0.0.15 h1:CUcuoHfx+7AO1Diig7+NrEb7i5m/+MKTiQFUMH3j72k=
12 github.com/bnema/puregotk-webkit v0.0.15/go.mod h1:jbMK8QD+J34OE24HUc19v7FUcbSYvsTd7Cs8x1Ab/14=14 github.com/bnema/puregotk-webkit v0.0.15/go.mod h1:jbMK8QD+J34OE24HUc19v7FUcbSYvsTd7Cs8x1Ab/14=
13 github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=15 github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
M internal/ui/component/omnibox.go +15 -20
@@ -96,7 +96,6 @@ type Omnibox struct {
9696 // Ghost text state
9797 realInput string // What user actually typed (without ghost suffix)
9898 ghostSuffix string // Completion suffix currently displayed
99- hasGhostText bool // Quick check for Tab behavior
10099 insertCompletion bool // True on character insert, false on delete — gates ghost completion
101100 isSettingGhost bool // Guard flag: skip onEntryChanged during programmatic SetText
102101
@@ -678,7 +677,7 @@ func (o *Omnibox) setupFocusCallbacks(onFocusIn func(*gtk.SearchEntry), onFocusO
678677 func (o *Omnibox) hasGhost() bool {
679678 o.mu.RLock()
680679 defer o.mu.RUnlock()
681- return o.hasGhostText
680+ return o.ghostSuffix != ""
682681 }
683682
684683 // hasUserNavigated returns whether user has navigated with arrow keys.
@@ -836,14 +835,10 @@ func (o *Omnibox) handleCtrlNumberShortcut(keyval, keycode uint, ctrl bool) bool
836835 // selected and the user typed, the selection was replaced naturally by GTK.
837836 // We must NOT call clearGhostText() here (that would delete real user text).
838837 func (o *Omnibox) onEntryChanged() {
839- log := logging.FromContext(o.ctx)
840-
841- // Skip if we're programmatically setting ghost text (synchronous guard).
842838 o.mu.RLock()
843839 isSettingGhost := o.isSettingGhost
844840 o.mu.RUnlock()
845841 if isSettingGhost {
846- log.Debug().Msg("ghost: onEntryChanged skipped (isSettingGhost)")
847842 return
848843 }
849844
@@ -851,15 +846,15 @@ func (o *Omnibox) onEntryChanged() {
851846
852847 // Detect debounced echo from our own SetText in setGhostText.
853848 // search-changed is debounced by GtkSearchEntry, so it fires AFTER
854- // the isSettingGhost guard is already off. Recognize it by checking
855- // if the entry text matches realInput + ghostSuffix exactly.
849+ // the isSettingGhost guard is already off.
856850 o.mu.RLock()
857- if o.hasGhostText && o.ghostSuffix != "" && entryText == o.realInput+o.ghostSuffix {
851+ if o.ghostSuffix != "" && entryText == o.realInput+o.ghostSuffix {
858852 o.mu.RUnlock()
859- log.Debug().Str("entryText", entryText).Msg("ghost: onEntryChanged skipped (debounced echo)")
860853 return
861854 }
862855 o.mu.RUnlock()
856+
857+ log := logging.FromContext(o.ctx)
863858 if trimmed := url.TrimLeadingSpacesIfURL(entryText); trimmed != entryText {
864859 o.withGhostGuard(func() {
865860 o.entry.SetText(trimmed)
@@ -872,7 +867,7 @@ func (o *Omnibox) onEntryChanged() {
872867 o.mu.Lock()
873868 oldGhost := o.ghostSuffix
874869 o.hasNavigated = false
875- o.hasGhostText = false
870+
876871 o.ghostSuffix = ""
877872 o.realInput = entryText
878873 shouldComplete := o.insertCompletion
@@ -941,7 +936,7 @@ func (o *Omnibox) setGhostText(originalInput, suffix string) {
941936 o.mu.Lock()
942937 o.realInput = originalInput
943938 o.ghostSuffix = suffix
944- o.hasGhostText = true
939+
945940 o.mu.Unlock()
946941
947942 o.withGhostGuard(func() {
@@ -956,11 +951,11 @@ func (o *Omnibox) setGhostText(originalInput, suffix string) {
956951 func (o *Omnibox) clearGhostText() {
957952 log := logging.FromContext(o.ctx)
958953 o.mu.Lock()
959- hadGhost := o.hasGhostText
954+ hadGhost := o.ghostSuffix != ""
960955 ghostSuffix := o.ghostSuffix
961956 realInput := o.realInput
962957 o.ghostSuffix = ""
963- o.hasGhostText = false
958+
964959 o.mu.Unlock()
965960
966961 if !hadGhost || ghostSuffix == "" || o.entry == nil {
@@ -1007,13 +1002,13 @@ func (o *Omnibox) clearGhostTextIfInput(expectedInput string) {
10071002 func (o *Omnibox) acceptGhostCompletion() {
10081003 log := logging.FromContext(o.ctx)
10091004 o.mu.Lock()
1010- if !o.hasGhostText || o.ghostSuffix == "" {
1005+ if o.ghostSuffix == "" {
10111006 o.mu.Unlock()
10121007 log.Debug().Msg("ghost: acceptGhostCompletion — no ghost to accept")
10131008 return
10141009 }
10151010 suffix := o.ghostSuffix
1016- o.hasGhostText = false
1011+
10171012 o.ghostSuffix = ""
10181013 o.mu.Unlock()
10191014 log.Debug().Str("suffix", suffix).Msg("ghost: acceptGhostCompletion")
@@ -1069,7 +1064,7 @@ func (o *Omnibox) updateGhostFromURL(userInput, targetURL string) bool {
10691064 func (o *Omnibox) updateGhostFromSelection() {
10701065 o.mu.RLock()
10711066 realInput := o.realInput
1072- hasGhost := o.hasGhostText
1067+ hasGhost := o.ghostSuffix != ""
10731068 o.mu.RUnlock()
10741069
10751070 entryText := ""
@@ -1287,7 +1282,7 @@ func (o *Omnibox) performSearch() {
12871282 visible := o.visible
12881283 mode := o.viewMode
12891284 realInput := o.realInput
1290- hasGhost := o.hasGhostText
1285+ hasGhost := o.ghostSuffix != ""
12911286 o.mu.RUnlock()
12921287
12931288 // Skip search if omnibox is hidden
@@ -1849,7 +1844,7 @@ func (o *Omnibox) attachRowHoverSelection(row *gtk.ListBoxRow, index int) {
18491844 enterCb := func(_ gtk.EventControllerMotion, _ float64, _ float64) {
18501845 o.mu.RLock()
18511846 realInput := o.realInput
1852- hasGhostText := o.hasGhostText
1847+ hasGhostText := o.ghostSuffix != ""
18531848 hasNavigated := o.hasNavigated
18541849 o.mu.RUnlock()
18551850
@@ -2314,7 +2309,7 @@ func (o *Omnibox) Show(ctx context.Context, query string) {
23142309 // Initialize ghost text state
23152310 o.realInput = query
23162311 o.ghostSuffix = ""
2317- o.hasGhostText = false
2312+
23182313 o.insertCompletion = false
23192314 o.mu.Unlock()
23202315
@@ -96,7 +96,6 @@ type Omnibox struct {
96 // Ghost text state96 // Ghost text state
97 realInput string // What user actually typed (without ghost suffix)97 realInput string // What user actually typed (without ghost suffix)
98 ghostSuffix string // Completion suffix currently displayed98 ghostSuffix string // Completion suffix currently displayed
99- hasGhostText bool // Quick check for Tab behavior
100 insertCompletion bool // True on character insert, false on delete — gates ghost completion99 insertCompletion bool // True on character insert, false on delete — gates ghost completion
101 isSettingGhost bool // Guard flag: skip onEntryChanged during programmatic SetText100 isSettingGhost bool // Guard flag: skip onEntryChanged during programmatic SetText
102 101
@@ -678,7 +677,7 @@ func (o *Omnibox) setupFocusCallbacks(onFocusIn func(*gtk.SearchEntry), onFocusO
678 func (o *Omnibox) hasGhost() bool {677 func (o *Omnibox) hasGhost() bool {
679 o.mu.RLock()678 o.mu.RLock()
680 defer o.mu.RUnlock()679 defer o.mu.RUnlock()
681- return o.hasGhostText680+ return o.ghostSuffix != ""
682 }681 }
683 682
684 // hasUserNavigated returns whether user has navigated with arrow keys.683 // hasUserNavigated returns whether user has navigated with arrow keys.
@@ -836,14 +835,10 @@ func (o *Omnibox) handleCtrlNumberShortcut(keyval, keycode uint, ctrl bool) bool
836 // selected and the user typed, the selection was replaced naturally by GTK.835 // selected and the user typed, the selection was replaced naturally by GTK.
837 // We must NOT call clearGhostText() here (that would delete real user text).836 // We must NOT call clearGhostText() here (that would delete real user text).
838 func (o *Omnibox) onEntryChanged() {837 func (o *Omnibox) onEntryChanged() {
839- log := logging.FromContext(o.ctx)
840-
841- // Skip if we're programmatically setting ghost text (synchronous guard).
842 o.mu.RLock()838 o.mu.RLock()
843 isSettingGhost := o.isSettingGhost839 isSettingGhost := o.isSettingGhost
844 o.mu.RUnlock()840 o.mu.RUnlock()
845 if isSettingGhost {841 if isSettingGhost {
846- log.Debug().Msg("ghost: onEntryChanged skipped (isSettingGhost)")
847 return842 return
848 }843 }
849 844
@@ -851,15 +846,15 @@ func (o *Omnibox) onEntryChanged() {
851 846
852 // Detect debounced echo from our own SetText in setGhostText.847 // Detect debounced echo from our own SetText in setGhostText.
853 // search-changed is debounced by GtkSearchEntry, so it fires AFTER848 // search-changed is debounced by GtkSearchEntry, so it fires AFTER
854- // the isSettingGhost guard is already off. Recognize it by checking849+ // the isSettingGhost guard is already off.
855- // if the entry text matches realInput + ghostSuffix exactly.
856 o.mu.RLock()850 o.mu.RLock()
857- if o.hasGhostText && o.ghostSuffix != "" && entryText == o.realInput+o.ghostSuffix {851+ if o.ghostSuffix != "" && entryText == o.realInput+o.ghostSuffix {
858 o.mu.RUnlock()852 o.mu.RUnlock()
859- log.Debug().Str("entryText", entryText).Msg("ghost: onEntryChanged skipped (debounced echo)")
860 return853 return
861 }854 }
862 o.mu.RUnlock()855 o.mu.RUnlock()
856+
857+ log := logging.FromContext(o.ctx)
863 if trimmed := url.TrimLeadingSpacesIfURL(entryText); trimmed != entryText {858 if trimmed := url.TrimLeadingSpacesIfURL(entryText); trimmed != entryText {
864 o.withGhostGuard(func() {859 o.withGhostGuard(func() {
865 o.entry.SetText(trimmed)860 o.entry.SetText(trimmed)
@@ -872,7 +867,7 @@ func (o *Omnibox) onEntryChanged() {
872 o.mu.Lock()867 o.mu.Lock()
873 oldGhost := o.ghostSuffix868 oldGhost := o.ghostSuffix
874 o.hasNavigated = false869 o.hasNavigated = false
875- o.hasGhostText = false870+
876 o.ghostSuffix = ""871 o.ghostSuffix = ""
877 o.realInput = entryText872 o.realInput = entryText
878 shouldComplete := o.insertCompletion873 shouldComplete := o.insertCompletion
@@ -941,7 +936,7 @@ func (o *Omnibox) setGhostText(originalInput, suffix string) {
941 o.mu.Lock()936 o.mu.Lock()
942 o.realInput = originalInput937 o.realInput = originalInput
943 o.ghostSuffix = suffix938 o.ghostSuffix = suffix
944- o.hasGhostText = true939+
945 o.mu.Unlock()940 o.mu.Unlock()
946 941
947 o.withGhostGuard(func() {942 o.withGhostGuard(func() {
@@ -956,11 +951,11 @@ func (o *Omnibox) setGhostText(originalInput, suffix string) {
956 func (o *Omnibox) clearGhostText() {951 func (o *Omnibox) clearGhostText() {
957 log := logging.FromContext(o.ctx)952 log := logging.FromContext(o.ctx)
958 o.mu.Lock()953 o.mu.Lock()
959- hadGhost := o.hasGhostText954+ hadGhost := o.ghostSuffix != ""
960 ghostSuffix := o.ghostSuffix955 ghostSuffix := o.ghostSuffix
961 realInput := o.realInput956 realInput := o.realInput
962 o.ghostSuffix = ""957 o.ghostSuffix = ""
963- o.hasGhostText = false958+
964 o.mu.Unlock()959 o.mu.Unlock()
965 960
966 if !hadGhost || ghostSuffix == "" || o.entry == nil {961 if !hadGhost || ghostSuffix == "" || o.entry == nil {
@@ -1007,13 +1002,13 @@ func (o *Omnibox) clearGhostTextIfInput(expectedInput string) {
1007 func (o *Omnibox) acceptGhostCompletion() {1002 func (o *Omnibox) acceptGhostCompletion() {
1008 log := logging.FromContext(o.ctx)1003 log := logging.FromContext(o.ctx)
1009 o.mu.Lock()1004 o.mu.Lock()
1010- if !o.hasGhostText || o.ghostSuffix == "" {1005+ if o.ghostSuffix == "" {
1011 o.mu.Unlock()1006 o.mu.Unlock()
1012 log.Debug().Msg("ghost: acceptGhostCompletion — no ghost to accept")1007 log.Debug().Msg("ghost: acceptGhostCompletion — no ghost to accept")
1013 return1008 return
1014 }1009 }
1015 suffix := o.ghostSuffix1010 suffix := o.ghostSuffix
1016- o.hasGhostText = false1011+
1017 o.ghostSuffix = ""1012 o.ghostSuffix = ""
1018 o.mu.Unlock()1013 o.mu.Unlock()
1019 log.Debug().Str("suffix", suffix).Msg("ghost: acceptGhostCompletion")1014 log.Debug().Str("suffix", suffix).Msg("ghost: acceptGhostCompletion")
@@ -1069,7 +1064,7 @@ func (o *Omnibox) updateGhostFromURL(userInput, targetURL string) bool {
1069 func (o *Omnibox) updateGhostFromSelection() {1064 func (o *Omnibox) updateGhostFromSelection() {
1070 o.mu.RLock()1065 o.mu.RLock()
1071 realInput := o.realInput1066 realInput := o.realInput
1072- hasGhost := o.hasGhostText1067+ hasGhost := o.ghostSuffix != ""
1073 o.mu.RUnlock()1068 o.mu.RUnlock()
1074 1069
1075 entryText := ""1070 entryText := ""
@@ -1287,7 +1282,7 @@ func (o *Omnibox) performSearch() {
1287 visible := o.visible1282 visible := o.visible
1288 mode := o.viewMode1283 mode := o.viewMode
1289 realInput := o.realInput1284 realInput := o.realInput
1290- hasGhost := o.hasGhostText1285+ hasGhost := o.ghostSuffix != ""
1291 o.mu.RUnlock()1286 o.mu.RUnlock()
1292 1287
1293 // Skip search if omnibox is hidden1288 // Skip search if omnibox is hidden
@@ -1849,7 +1844,7 @@ func (o *Omnibox) attachRowHoverSelection(row *gtk.ListBoxRow, index int) {
1849 enterCb := func(_ gtk.EventControllerMotion, _ float64, _ float64) {1844 enterCb := func(_ gtk.EventControllerMotion, _ float64, _ float64) {
1850 o.mu.RLock()1845 o.mu.RLock()
1851 realInput := o.realInput1846 realInput := o.realInput
1852- hasGhostText := o.hasGhostText1847+ hasGhostText := o.ghostSuffix != ""
1853 hasNavigated := o.hasNavigated1848 hasNavigated := o.hasNavigated
1854 o.mu.RUnlock()1849 o.mu.RUnlock()
1855 1850
@@ -2314,7 +2309,7 @@ func (o *Omnibox) Show(ctx context.Context, query string) {
2314 // Initialize ghost text state2309 // Initialize ghost text state
2315 o.realInput = query2310 o.realInput = query
2316 o.ghostSuffix = ""2311 o.ghostSuffix = ""
2317- o.hasGhostText = false2312+
2318 o.insertCompletion = false2313 o.insertCompletion = false
2319 o.mu.Unlock()2314 o.mu.Unlock()
2320 2315

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close