detached 2dbb951

fix: align omnibox URL detection with review cases

Add localhost/IP navigation edge cases in omnibox tests, simplify ghost suppression state helper signature, and treat bracketed IPv6/IP hosts as URL-like inputs so typed host targets are navigated directly.
commit
2dbb951e9d3b3019e332d76df90f3964d04388e0
author
brice · · 2026-02-17 18:04 UTC
parent
25ff751
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +21 -3
M internal/domain/url/normalize.go +3 -0
@@ -140,6 +140,9 @@ func LooksLikeURL(input string) bool {
140140 if input == "localhost" || strings.HasPrefix(input, "localhost:") || strings.HasPrefix(input, "localhost/") {
141141 return true
142142 }
143+ if looksLikeIPAddressWithOptionalPortOrPath(input) {
144+ return true
145+ }
143146 return strings.Contains(input, ".") && !strings.Contains(input, " ")
144147 }
145148
@@ -140,6 +140,9 @@ func LooksLikeURL(input string) bool {
140 if input == "localhost" || strings.HasPrefix(input, "localhost:") || strings.HasPrefix(input, "localhost/") {140 if input == "localhost" || strings.HasPrefix(input, "localhost:") || strings.HasPrefix(input, "localhost/") {
141 return true141 return true
142 }142 }
143+ if looksLikeIPAddressWithOptionalPortOrPath(input) {
144+ return true
145+ }
143 return strings.Contains(input, ".") && !strings.Contains(input, " ")146 return strings.Contains(input, ".") && !strings.Contains(input, " ")
144 }147 }
145 148
M internal/domain/url/normalize_test.go +10 -0
@@ -395,6 +395,16 @@ func TestLooksLikeURL(t *testing.T) {
395395 input: "100.64.0.10:3000",
396396 want: true,
397397 },
398+ {
399+ name: "ipv4",
400+ input: "127.0.0.1",
401+ want: true,
402+ },
403+ {
404+ name: "ipv6 bracketed",
405+ input: "[::1]",
406+ want: true,
407+ },
398408 }
399409
400410 for _, tt := range tests {
@@ -395,6 +395,16 @@ func TestLooksLikeURL(t *testing.T) {
395 input: "100.64.0.10:3000",395 input: "100.64.0.10:3000",
396 want: true,396 want: true,
397 },397 },
398+ {
399+ name: "ipv4",
400+ input: "127.0.0.1",
401+ want: true,
402+ },
403+ {
404+ name: "ipv6 bracketed",
405+ input: "[::1]",
406+ want: true,
407+ },
398 }408 }
399 409
400 for _, tt := range tests {410 for _, tt := range tests {
M internal/ui/component/omnibox.go +2 -2
@@ -748,13 +748,13 @@ func (o *Omnibox) handleKeyPress(keyval, keycode uint, state gdk.ModifierType) b
748748 return false // Let entry handle the key
749749 }
750750
751-func nextGhostSuppressionState(_ bool, keyval uint) bool {
751+func nextGhostSuppressionState(keyval uint) bool {
752752 return isDeletionKey(keyval)
753753 }
754754
755755 func (o *Omnibox) updateGhostSuppressionFromKey(keyval uint) {
756756 o.mu.Lock()
757- o.suppressGhost = nextGhostSuppressionState(o.suppressGhost, keyval)
757+ o.suppressGhost = nextGhostSuppressionState(keyval)
758758 o.mu.Unlock()
759759 }
760760
@@ -748,13 +748,13 @@ func (o *Omnibox) handleKeyPress(keyval, keycode uint, state gdk.ModifierType) b
748 return false // Let entry handle the key748 return false // Let entry handle the key
749 }749 }
750 750
751-func nextGhostSuppressionState(_ bool, keyval uint) bool {751+func nextGhostSuppressionState(keyval uint) bool {
752 return isDeletionKey(keyval)752 return isDeletionKey(keyval)
753 }753 }
754 754
755 func (o *Omnibox) updateGhostSuppressionFromKey(keyval uint) {755 func (o *Omnibox) updateGhostSuppressionFromKey(keyval uint) {
756 o.mu.Lock()756 o.mu.Lock()
757- o.suppressGhost = nextGhostSuppressionState(o.suppressGhost, keyval)757+ o.suppressGhost = nextGhostSuppressionState(keyval)
758 o.mu.Unlock()758 o.mu.Unlock()
759 }759 }
760 760
M internal/ui/component/omnibox_navigation_state_test.go +6 -1
@@ -15,6 +15,11 @@ func TestShouldPreferTypedURLNavigation(t *testing.T) {
1515 {name: "domain", input: "x.com", want: true},
1616 {name: "domain with path", input: "github.com/bnema", want: true},
1717 {name: "http url", input: "http://example.com", want: true},
18+ {name: "localhost", input: "localhost", want: true},
19+ {name: "ipv4", input: "127.0.0.1", want: true},
20+ {name: "ipv4 with port", input: "192.168.1.1:8080", want: true},
21+ {name: "ipv6 bracketed", input: "[::1]", want: true},
22+ {name: "http ipv6 bracketed", input: "http://[::1]", want: true},
1823 {name: "search query", input: "hello world", want: false},
1924 {name: "empty", input: "", want: false},
2025 }
@@ -82,7 +87,7 @@ func TestNextGhostSuppressionState(t *testing.T) {
8287
8388 for _, tt := range tests {
8489 t.Run(tt.name, func(t *testing.T) {
85- if got := nextGhostSuppressionState(tt.current, tt.key); got != tt.want {
90+ if got := nextGhostSuppressionState(tt.key); got != tt.want {
8691 t.Fatalf("nextGhostSuppressionState(%v, %d) = %v, want %v", tt.current, tt.key, got, tt.want)
8792 }
8893 })
@@ -15,6 +15,11 @@ func TestShouldPreferTypedURLNavigation(t *testing.T) {
15 {name: "domain", input: "x.com", want: true},15 {name: "domain", input: "x.com", want: true},
16 {name: "domain with path", input: "github.com/bnema", want: true},16 {name: "domain with path", input: "github.com/bnema", want: true},
17 {name: "http url", input: "http://example.com", want: true},17 {name: "http url", input: "http://example.com", want: true},
18+ {name: "localhost", input: "localhost", want: true},
19+ {name: "ipv4", input: "127.0.0.1", want: true},
20+ {name: "ipv4 with port", input: "192.168.1.1:8080", want: true},
21+ {name: "ipv6 bracketed", input: "[::1]", want: true},
22+ {name: "http ipv6 bracketed", input: "http://[::1]", want: true},
18 {name: "search query", input: "hello world", want: false},23 {name: "search query", input: "hello world", want: false},
19 {name: "empty", input: "", want: false},24 {name: "empty", input: "", want: false},
20 }25 }
@@ -82,7 +87,7 @@ func TestNextGhostSuppressionState(t *testing.T) {
82 87
83 for _, tt := range tests {88 for _, tt := range tests {
84 t.Run(tt.name, func(t *testing.T) {89 t.Run(tt.name, func(t *testing.T) {
85- if got := nextGhostSuppressionState(tt.current, tt.key); got != tt.want {90+ if got := nextGhostSuppressionState(tt.key); got != tt.want {
86 t.Fatalf("nextGhostSuppressionState(%v, %d) = %v, want %v", tt.current, tt.key, got, tt.want)91 t.Fatalf("nextGhostSuppressionState(%v, %d) = %v, want %v", tt.current, tt.key, got, tt.want)
87 }92 }
88 })93 })

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