detached 1d6b2aa

refactor(omnibox): simplify handleKeyPress with helper methods

commit
1d6b2aa6bdefd84dfcb7276b7036f304b1f55a05
author
brice · · 2026-01-23 17:12 UTC
committer
bnema · · 2026-01-23 18:07 UTC
parent
df1aa18
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +36 -61
M internal/application/usecase/autocomplete.go +1 -1
@@ -165,7 +165,7 @@ func (uc *AutocompleteUseCase) findBangSuggestion(ctx context.Context, input str
165165
166166 // GetSuggestionForURL returns an autocomplete suggestion specifically for a URL.
167167 // This is used when the user selects a row from the list to update ghost text.
168-func (uc *AutocompleteUseCase) GetSuggestionForURL(ctx context.Context, input, targetURL string) *GetSuggestionOutput {
168+func (*AutocompleteUseCase) GetSuggestionForURL(_ context.Context, input, targetURL string) *GetSuggestionOutput {
169169 if input == "" || targetURL == "" {
170170 return &GetSuggestionOutput{Found: false}
171171 }
@@ -165,7 +165,7 @@ func (uc *AutocompleteUseCase) findBangSuggestion(ctx context.Context, input str
165 165
166 // GetSuggestionForURL returns an autocomplete suggestion specifically for a URL.166 // GetSuggestionForURL returns an autocomplete suggestion specifically for a URL.
167 // This is used when the user selects a row from the list to update ghost text.167 // This is used when the user selects a row from the list to update ghost text.
168-func (uc *AutocompleteUseCase) GetSuggestionForURL(ctx context.Context, input, targetURL string) *GetSuggestionOutput {168+func (*AutocompleteUseCase) GetSuggestionForURL(_ context.Context, input, targetURL string) *GetSuggestionOutput {
169 if input == "" || targetURL == "" {169 if input == "" || targetURL == "" {
170 return &GetSuggestionOutput{Found: false}170 return &GetSuggestionOutput{Found: false}
171 }171 }
M internal/domain/autocomplete/autocomplete.go +1 -1
@@ -53,7 +53,7 @@ func StripProtocol(url string) string {
5353 // ComputeURLCompletionSuffix computes the completion suffix for URLs,
5454 // handling protocol stripping for better matching.
5555 // It tries matching with and without protocol prefixes.
56-func ComputeURLCompletionSuffix(input, fullURL string) (suffix string, matchedURL string, ok bool) {
56+func ComputeURLCompletionSuffix(input, fullURL string) (suffix, matchedURL string, ok bool) {
5757 // First try direct match
5858 if suffix, ok := ComputeCompletionSuffix(input, fullURL); ok {
5959 return suffix, fullURL, true
@@ -53,7 +53,7 @@ func StripProtocol(url string) string {
53 // ComputeURLCompletionSuffix computes the completion suffix for URLs,53 // ComputeURLCompletionSuffix computes the completion suffix for URLs,
54 // handling protocol stripping for better matching.54 // handling protocol stripping for better matching.
55 // It tries matching with and without protocol prefixes.55 // It tries matching with and without protocol prefixes.
56-func ComputeURLCompletionSuffix(input, fullURL string) (suffix string, matchedURL string, ok bool) {56+func ComputeURLCompletionSuffix(input, fullURL string) (suffix, matchedURL string, ok bool) {
57 // First try direct match57 // First try direct match
58 if suffix, ok := ComputeCompletionSuffix(input, fullURL); ok {58 if suffix, ok := ComputeCompletionSuffix(input, fullURL); ok {
59 return suffix, fullURL, true59 return suffix, fullURL, true
M internal/ui/component/omnibox.go +34 -59
@@ -21,8 +21,9 @@ import (
2121 )
2222
2323 const (
24- debounceDelayMs = 50
25- endBoxSpacing = 6
24+ debounceDelayMs = 50
25+ endBoxSpacing = 6
26+ ghostIconOffsetBase = 18 // Base icon offset for ghost text positioning (scaled by uiScale)
2627 )
2728
2829 // ViewMode distinguishes history search from favorites display.
@@ -570,6 +571,20 @@ func (o *Omnibox) setupFocusCallbacks(onFocusIn func(*gtk.SearchEntry), onFocusO
570571 o.entry.AddController(&focusController.EventController)
571572 }
572573
574+// hasGhost returns whether ghost text is currently visible.
575+func (o *Omnibox) hasGhost() bool {
576+ o.mu.RLock()
577+ defer o.mu.RUnlock()
578+ return o.hasGhostText
579+}
580+
581+// hasUserNavigated returns whether user has navigated with arrow keys.
582+func (o *Omnibox) hasUserNavigated() bool {
583+ o.mu.RLock()
584+ defer o.mu.RUnlock()
585+ return o.hasNavigated
586+}
587+
573588 // handleKeyPress processes keyboard events.
574589 // Returns true if the event was handled.
575590 func (o *Omnibox) handleKeyPress(keyval, keycode uint, state gdk.ModifierType) bool {
@@ -608,10 +623,7 @@ func (o *Omnibox) handleKeyPress(keyval, keycode uint, state gdk.ModifierType) b
608623
609624 case uint(gdk.KEY_Tab):
610625 // Tab accepts ghost text if present, otherwise toggles view mode
611- o.mu.RLock()
612- hasGhost := o.hasGhostText
613- o.mu.RUnlock()
614- if hasGhost && o.cursorAtEnd() {
626+ if o.hasGhost() {
615627 o.acceptGhostCompletion()
616628 return true
617629 }
@@ -619,11 +631,8 @@ func (o *Omnibox) handleKeyPress(keyval, keycode uint, state gdk.ModifierType) b
619631 return true
620632
621633 case uint(gdk.KEY_Right):
622- // Arrow Right accepts ghost text if present and cursor at end
623- o.mu.RLock()
624- hasGhost := o.hasGhostText
625- o.mu.RUnlock()
626- if hasGhost && o.cursorAtEnd() {
634+ // Arrow Right accepts ghost text if present
635+ if o.hasGhost() {
627636 o.acceptGhostCompletion()
628637 return true
629638 }
@@ -631,11 +640,7 @@ func (o *Omnibox) handleKeyPress(keyval, keycode uint, state gdk.ModifierType) b
631640
632641 case uint(gdk.KEY_space):
633642 // Space toggles favorite only if user has navigated with arrow keys
634- // Otherwise, let space pass through to entry for typing
635- o.mu.RLock()
636- navigated := o.hasNavigated
637- o.mu.RUnlock()
638- if navigated {
643+ if o.hasUserNavigated() {
639644 o.toggleFavorite()
640645 return true
641646 }
@@ -643,10 +648,7 @@ func (o *Omnibox) handleKeyPress(keyval, keycode uint, state gdk.ModifierType) b
643648
644649 case uint(gdk.KEY_y):
645650 // 'y' yanks (copies) the selected URL to clipboard when navigating
646- o.mu.RLock()
647- navigated := o.hasNavigated
648- o.mu.RUnlock()
649- if navigated {
651+ if o.hasUserNavigated() {
650652 o.yankSelectedURL()
651653 return true
652654 }
@@ -770,8 +772,8 @@ func (o *Omnibox) setGhostText(originalInput, suffix, fullText string) {
770772
771773 // Account for the search icon inside the entry
772774 // Ghost label already has same margin/padding as entry via CSS
773- // Scale the icon offset by UI scale factor (base ~18px at scale 1.0)
774- iconOffset := int(18.0 * o.uiScale)
775+ // Scale the icon offset by UI scale factor
776+ iconOffset := int(float64(ghostIconOffsetBase) * o.uiScale)
775777 marginStart := widthPx + iconOffset
776778
777779 // Set the ghost label text (just the suffix) and position it
@@ -816,12 +818,11 @@ func (o *Omnibox) clearGhostText() {
816818 }
817819
818820 // acceptGhostCompletion accepts the ghost text and fills the input.
819-// Returns true if ghost text was accepted.
820-func (o *Omnibox) acceptGhostCompletion() bool {
821+func (o *Omnibox) acceptGhostCompletion() {
821822 o.mu.Lock()
822823 if !o.hasGhostText || o.ghostFullText == "" {
823824 o.mu.Unlock()
824- return false
825+ return
825826 }
826827 fullText := o.ghostFullText
827828 o.isAcceptingGhost = true
@@ -842,22 +843,6 @@ func (o *Omnibox) acceptGhostCompletion() bool {
842843 o.mu.Lock()
843844 o.isAcceptingGhost = false
844845 o.mu.Unlock()
845-
846- return true
847-}
848-
849-// cursorAtEnd returns true if the entry cursor is at the end of the text.
850-func (o *Omnibox) cursorAtEnd() bool {
851- text := o.entry.GetText()
852- // If entry is empty, cursor is trivially "at end" - allow ghost acceptance
853- // This handles the case where user navigates with arrows from empty input
854- if len(text) == 0 {
855- return true
856- }
857- // SearchEntry doesn't have GetPosition directly, but we can use the editable interface
858- // For simplicity, we'll assume cursor is at end after any input
859- // This works because Tab/ArrowRight are handled in capture phase before entry processes them
860- return true
861846 }
862847
863848 // updateGhostFromSuggestion updates ghost text based on the current autocomplete suggestion.
@@ -867,24 +852,24 @@ func (o *Omnibox) updateGhostFromSuggestion() {
867852 }
868853
869854 o.mu.RLock()
870- input := o.realInput
855+ userInput := o.realInput
871856 bangMode := o.bangMode
872857 o.mu.RUnlock()
873858
874859 // Don't show ghost text in bang mode (it has its own completion)
875- if bangMode || input == "" {
860+ if bangMode || userInput == "" {
876861 o.clearGhostText()
877862 return
878863 }
879864
880865 go func() {
881- output := o.autocompleteUC.GetSuggestion(o.ctx, usecase.GetSuggestionInput{Input: input})
866+ output := o.autocompleteUC.GetSuggestion(o.ctx, usecase.GetSuggestionInput{Input: userInput})
882867 if !output.Found || output.Suggestion == nil {
883868 o.clearGhostText()
884869 return
885870 }
886871 // Pass the original input so setGhostText can verify it's still current
887- o.setGhostText(input, output.Suggestion.Suffix, output.Suggestion.FullText)
872+ o.setGhostText(userInput, output.Suggestion.Suffix, output.Suggestion.FullText)
888873 }()
889874 }
890875
@@ -898,7 +883,7 @@ func (o *Omnibox) updateGhostFromURL(targetURL string) {
898883 }
899884
900885 o.mu.RLock()
901- input := o.realInput
886+ userInput := o.realInput
902887 o.mu.RUnlock()
903888
904889 // Strip protocol from URL for cleaner display
@@ -910,10 +895,10 @@ func (o *Omnibox) updateGhostFromURL(targetURL string) {
910895 }
911896
912897 // Try to compute proper suffix if input is a prefix of the URL
913- if o.autocompleteUC != nil && input != "" {
914- output := o.autocompleteUC.GetSuggestionForURL(o.ctx, input, targetURL)
898+ if o.autocompleteUC != nil && userInput != "" {
899+ output := o.autocompleteUC.GetSuggestionForURL(o.ctx, userInput, targetURL)
915900 if output.Found && output.Suggestion != nil {
916- o.setGhostText(input, output.Suggestion.Suffix, output.Suggestion.FullText)
901+ o.setGhostText(userInput, output.Suggestion.Suffix, output.Suggestion.FullText)
917902 return
918903 }
919904 }
@@ -957,16 +942,6 @@ func (o *Omnibox) updateGhostFromSelection() {
957942 o.updateGhostFromURL(targetURL)
958943 }
959944
960-// escapeMarkup escapes special XML/Pango markup characters.
961-func escapeMarkup(s string) string {
962- s = strings.ReplaceAll(s, "&", "&")
963- s = strings.ReplaceAll(s, "<", "&lt;")
964- s = strings.ReplaceAll(s, ">", "&gt;")
965- s = strings.ReplaceAll(s, "'", "&apos;")
966- s = strings.ReplaceAll(s, "\"", "&quot;")
967- return s
968-}
969-
970945 // performSearch executes the search based on current view mode and query.
971946 func (o *Omnibox) performSearch() {
972947 o.mu.RLock()
@@ -21,8 +21,9 @@ import (
21 )21 )
22 22
23 const (23 const (
24- debounceDelayMs = 5024+ debounceDelayMs = 50
25- endBoxSpacing = 625+ endBoxSpacing = 6
26+ ghostIconOffsetBase = 18 // Base icon offset for ghost text positioning (scaled by uiScale)
26 )27 )
27 28
28 // ViewMode distinguishes history search from favorites display.29 // ViewMode distinguishes history search from favorites display.
@@ -570,6 +571,20 @@ func (o *Omnibox) setupFocusCallbacks(onFocusIn func(*gtk.SearchEntry), onFocusO
570 o.entry.AddController(&focusController.EventController)571 o.entry.AddController(&focusController.EventController)
571 }572 }
572 573
574+// hasGhost returns whether ghost text is currently visible.
575+func (o *Omnibox) hasGhost() bool {
576+ o.mu.RLock()
577+ defer o.mu.RUnlock()
578+ return o.hasGhostText
579+}
580+
581+// hasUserNavigated returns whether user has navigated with arrow keys.
582+func (o *Omnibox) hasUserNavigated() bool {
583+ o.mu.RLock()
584+ defer o.mu.RUnlock()
585+ return o.hasNavigated
586+}
587+
573 // handleKeyPress processes keyboard events.588 // handleKeyPress processes keyboard events.
574 // Returns true if the event was handled.589 // Returns true if the event was handled.
575 func (o *Omnibox) handleKeyPress(keyval, keycode uint, state gdk.ModifierType) bool {590 func (o *Omnibox) handleKeyPress(keyval, keycode uint, state gdk.ModifierType) bool {
@@ -608,10 +623,7 @@ func (o *Omnibox) handleKeyPress(keyval, keycode uint, state gdk.ModifierType) b
608 623
609 case uint(gdk.KEY_Tab):624 case uint(gdk.KEY_Tab):
610 // Tab accepts ghost text if present, otherwise toggles view mode625 // Tab accepts ghost text if present, otherwise toggles view mode
611- o.mu.RLock()626+ if o.hasGhost() {
612- hasGhost := o.hasGhostText
613- o.mu.RUnlock()
614- if hasGhost && o.cursorAtEnd() {
615 o.acceptGhostCompletion()627 o.acceptGhostCompletion()
616 return true628 return true
617 }629 }
@@ -619,11 +631,8 @@ func (o *Omnibox) handleKeyPress(keyval, keycode uint, state gdk.ModifierType) b
619 return true631 return true
620 632
621 case uint(gdk.KEY_Right):633 case uint(gdk.KEY_Right):
622- // Arrow Right accepts ghost text if present and cursor at end634+ // Arrow Right accepts ghost text if present
623- o.mu.RLock()635+ if o.hasGhost() {
624- hasGhost := o.hasGhostText
625- o.mu.RUnlock()
626- if hasGhost && o.cursorAtEnd() {
627 o.acceptGhostCompletion()636 o.acceptGhostCompletion()
628 return true637 return true
629 }638 }
@@ -631,11 +640,7 @@ func (o *Omnibox) handleKeyPress(keyval, keycode uint, state gdk.ModifierType) b
631 640
632 case uint(gdk.KEY_space):641 case uint(gdk.KEY_space):
633 // Space toggles favorite only if user has navigated with arrow keys642 // Space toggles favorite only if user has navigated with arrow keys
634- // Otherwise, let space pass through to entry for typing643+ if o.hasUserNavigated() {
635- o.mu.RLock()
636- navigated := o.hasNavigated
637- o.mu.RUnlock()
638- if navigated {
639 o.toggleFavorite()644 o.toggleFavorite()
640 return true645 return true
641 }646 }
@@ -643,10 +648,7 @@ func (o *Omnibox) handleKeyPress(keyval, keycode uint, state gdk.ModifierType) b
643 648
644 case uint(gdk.KEY_y):649 case uint(gdk.KEY_y):
645 // 'y' yanks (copies) the selected URL to clipboard when navigating650 // 'y' yanks (copies) the selected URL to clipboard when navigating
646- o.mu.RLock()651+ if o.hasUserNavigated() {
647- navigated := o.hasNavigated
648- o.mu.RUnlock()
649- if navigated {
650 o.yankSelectedURL()652 o.yankSelectedURL()
651 return true653 return true
652 }654 }
@@ -770,8 +772,8 @@ func (o *Omnibox) setGhostText(originalInput, suffix, fullText string) {
770 772
771 // Account for the search icon inside the entry773 // Account for the search icon inside the entry
772 // Ghost label already has same margin/padding as entry via CSS774 // Ghost label already has same margin/padding as entry via CSS
773- // Scale the icon offset by UI scale factor (base ~18px at scale 1.0)775+ // Scale the icon offset by UI scale factor
774- iconOffset := int(18.0 * o.uiScale)776+ iconOffset := int(float64(ghostIconOffsetBase) * o.uiScale)
775 marginStart := widthPx + iconOffset777 marginStart := widthPx + iconOffset
776 778
777 // Set the ghost label text (just the suffix) and position it779 // Set the ghost label text (just the suffix) and position it
@@ -816,12 +818,11 @@ func (o *Omnibox) clearGhostText() {
816 }818 }
817 819
818 // acceptGhostCompletion accepts the ghost text and fills the input.820 // acceptGhostCompletion accepts the ghost text and fills the input.
819-// Returns true if ghost text was accepted.821+func (o *Omnibox) acceptGhostCompletion() {
820-func (o *Omnibox) acceptGhostCompletion() bool {
821 o.mu.Lock()822 o.mu.Lock()
822 if !o.hasGhostText || o.ghostFullText == "" {823 if !o.hasGhostText || o.ghostFullText == "" {
823 o.mu.Unlock()824 o.mu.Unlock()
824- return false825+ return
825 }826 }
826 fullText := o.ghostFullText827 fullText := o.ghostFullText
827 o.isAcceptingGhost = true828 o.isAcceptingGhost = true
@@ -842,22 +843,6 @@ func (o *Omnibox) acceptGhostCompletion() bool {
842 o.mu.Lock()843 o.mu.Lock()
843 o.isAcceptingGhost = false844 o.isAcceptingGhost = false
844 o.mu.Unlock()845 o.mu.Unlock()
845-
846- return true
847-}
848-
849-// cursorAtEnd returns true if the entry cursor is at the end of the text.
850-func (o *Omnibox) cursorAtEnd() bool {
851- text := o.entry.GetText()
852- // If entry is empty, cursor is trivially "at end" - allow ghost acceptance
853- // This handles the case where user navigates with arrows from empty input
854- if len(text) == 0 {
855- return true
856- }
857- // SearchEntry doesn't have GetPosition directly, but we can use the editable interface
858- // For simplicity, we'll assume cursor is at end after any input
859- // This works because Tab/ArrowRight are handled in capture phase before entry processes them
860- return true
861 }846 }
862 847
863 // updateGhostFromSuggestion updates ghost text based on the current autocomplete suggestion.848 // updateGhostFromSuggestion updates ghost text based on the current autocomplete suggestion.
@@ -867,24 +852,24 @@ func (o *Omnibox) updateGhostFromSuggestion() {
867 }852 }
868 853
869 o.mu.RLock()854 o.mu.RLock()
870- input := o.realInput855+ userInput := o.realInput
871 bangMode := o.bangMode856 bangMode := o.bangMode
872 o.mu.RUnlock()857 o.mu.RUnlock()
873 858
874 // Don't show ghost text in bang mode (it has its own completion)859 // Don't show ghost text in bang mode (it has its own completion)
875- if bangMode || input == "" {860+ if bangMode || userInput == "" {
876 o.clearGhostText()861 o.clearGhostText()
877 return862 return
878 }863 }
879 864
880 go func() {865 go func() {
881- output := o.autocompleteUC.GetSuggestion(o.ctx, usecase.GetSuggestionInput{Input: input})866+ output := o.autocompleteUC.GetSuggestion(o.ctx, usecase.GetSuggestionInput{Input: userInput})
882 if !output.Found || output.Suggestion == nil {867 if !output.Found || output.Suggestion == nil {
883 o.clearGhostText()868 o.clearGhostText()
884 return869 return
885 }870 }
886 // Pass the original input so setGhostText can verify it's still current871 // Pass the original input so setGhostText can verify it's still current
887- o.setGhostText(input, output.Suggestion.Suffix, output.Suggestion.FullText)872+ o.setGhostText(userInput, output.Suggestion.Suffix, output.Suggestion.FullText)
888 }()873 }()
889 }874 }
890 875
@@ -898,7 +883,7 @@ func (o *Omnibox) updateGhostFromURL(targetURL string) {
898 }883 }
899 884
900 o.mu.RLock()885 o.mu.RLock()
901- input := o.realInput886+ userInput := o.realInput
902 o.mu.RUnlock()887 o.mu.RUnlock()
903 888
904 // Strip protocol from URL for cleaner display889 // Strip protocol from URL for cleaner display
@@ -910,10 +895,10 @@ func (o *Omnibox) updateGhostFromURL(targetURL string) {
910 }895 }
911 896
912 // Try to compute proper suffix if input is a prefix of the URL897 // Try to compute proper suffix if input is a prefix of the URL
913- if o.autocompleteUC != nil && input != "" {898+ if o.autocompleteUC != nil && userInput != "" {
914- output := o.autocompleteUC.GetSuggestionForURL(o.ctx, input, targetURL)899+ output := o.autocompleteUC.GetSuggestionForURL(o.ctx, userInput, targetURL)
915 if output.Found && output.Suggestion != nil {900 if output.Found && output.Suggestion != nil {
916- o.setGhostText(input, output.Suggestion.Suffix, output.Suggestion.FullText)901+ o.setGhostText(userInput, output.Suggestion.Suffix, output.Suggestion.FullText)
917 return902 return
918 }903 }
919 }904 }
@@ -957,16 +942,6 @@ func (o *Omnibox) updateGhostFromSelection() {
957 o.updateGhostFromURL(targetURL)942 o.updateGhostFromURL(targetURL)
958 }943 }
959 944
960-// escapeMarkup escapes special XML/Pango markup characters.
961-func escapeMarkup(s string) string {
962- s = strings.ReplaceAll(s, "&", "&amp;")
963- s = strings.ReplaceAll(s, "<", "&lt;")
964- s = strings.ReplaceAll(s, ">", "&gt;")
965- s = strings.ReplaceAll(s, "'", "&apos;")
966- s = strings.ReplaceAll(s, "\"", "&quot;")
967- return s
968-}
969-
970 // performSearch executes the search based on current view mode and query.945 // performSearch executes the search based on current view mode and query.
971 func (o *Omnibox) performSearch() {946 func (o *Omnibox) performSearch() {
972 o.mu.RLock()947 o.mu.RLock()

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