detached 44f3625

Merge pull request #153 from bnema/fix/truncate-space-before-url

fix(omnibox): strip leading spaces before URLs on input
commit
44f36253a94ad7bc7a7af2c57182dd95f24e885b
author
bnema · · 2026-01-26 20:55 UTC
committer
GitHub · · 2026-01-26 20:55 UTC
merge of
64df752 + 90a846e · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
3 files · +93 -1
M internal/domain/url/normalize.go +12 -0
@@ -169,3 +169,15 @@ func sanitizeDomain(domain string) string {
169169 func SanitizeDomainForPNGSized(domain string, size int) string {
170170 return fmt.Sprintf("%s.%d.png", sanitizeDomain(domain), size)
171171 }
172+
173+// TrimLeadingSpacesIfURL removes leading whitespace from input if the trimmed
174+// result looks like a URL. Returns the original input unchanged if it doesn't
175+// contain leading spaces or if the trimmed result is not a URL.
176+// This handles cases like pasting " https://example.com" from clipboard.
177+func TrimLeadingSpacesIfURL(input string) string {
178+ trimmed := strings.TrimLeft(input, " \t")
179+ if trimmed != input && LooksLikeURL(trimmed) {
180+ return trimmed
181+ }
182+ return input
183+}
@@ -169,3 +169,15 @@ func sanitizeDomain(domain string) string {
169 func SanitizeDomainForPNGSized(domain string, size int) string {169 func SanitizeDomainForPNGSized(domain string, size int) string {
170 return fmt.Sprintf("%s.%d.png", sanitizeDomain(domain), size)170 return fmt.Sprintf("%s.%d.png", sanitizeDomain(domain), size)
171 }171 }
172+
173+// TrimLeadingSpacesIfURL removes leading whitespace from input if the trimmed
174+// result looks like a URL. Returns the original input unchanged if it doesn't
175+// contain leading spaces or if the trimmed result is not a URL.
176+// This handles cases like pasting " https://example.com" from clipboard.
177+func TrimLeadingSpacesIfURL(input string) string {
178+ trimmed := strings.TrimLeft(input, " \t")
179+ if trimmed != input && LooksLikeURL(trimmed) {
180+ return trimmed
181+ }
182+ return input
183+}
M internal/domain/url/normalize_test.go +73 -0
@@ -231,6 +231,79 @@ func TestExpandHome(t *testing.T) {
231231 }
232232 }
233233
234+func TestTrimLeadingSpacesIfURL(t *testing.T) {
235+ tests := []struct {
236+ name string
237+ input string
238+ want string
239+ }{
240+ {
241+ name: "single space before https URL",
242+ input: " https://example.com",
243+ want: "https://example.com",
244+ },
245+ {
246+ name: "multiple spaces before https URL",
247+ input: " https://example.com",
248+ want: "https://example.com",
249+ },
250+ {
251+ name: "tab before URL",
252+ input: "\thttps://example.com",
253+ want: "https://example.com",
254+ },
255+ {
256+ name: "mixed whitespace before URL",
257+ input: " \t https://example.com",
258+ want: "https://example.com",
259+ },
260+ {
261+ name: "space before domain",
262+ input: " example.com",
263+ want: "example.com",
264+ },
265+ {
266+ name: "spaces before localhost",
267+ input: " localhost:8080",
268+ want: "localhost:8080",
269+ },
270+ {
271+ name: "no leading space URL unchanged",
272+ input: "https://example.com",
273+ want: "https://example.com",
274+ },
275+ {
276+ name: "search query with leading space unchanged",
277+ input: " hello world",
278+ want: " hello world",
279+ },
280+ {
281+ name: "single word with leading space unchanged",
282+ input: " hello",
283+ want: " hello",
284+ },
285+ {
286+ name: "empty string unchanged",
287+ input: "",
288+ want: "",
289+ },
290+ {
291+ name: "only spaces unchanged",
292+ input: " ",
293+ want: " ",
294+ },
295+ }
296+
297+ for _, tt := range tests {
298+ t.Run(tt.name, func(t *testing.T) {
299+ got := TrimLeadingSpacesIfURL(tt.input)
300+ if got != tt.want {
301+ t.Errorf("TrimLeadingSpacesIfURL(%q) = %q, want %q", tt.input, got, tt.want)
302+ }
303+ })
304+ }
305+}
306+
234307 func TestLooksLikeURL(t *testing.T) {
235308 tests := []struct {
236309 name string
@@ -231,6 +231,79 @@ func TestExpandHome(t *testing.T) {
231 }231 }
232 }232 }
233 233
234+func TestTrimLeadingSpacesIfURL(t *testing.T) {
235+ tests := []struct {
236+ name string
237+ input string
238+ want string
239+ }{
240+ {
241+ name: "single space before https URL",
242+ input: " https://example.com",
243+ want: "https://example.com",
244+ },
245+ {
246+ name: "multiple spaces before https URL",
247+ input: " https://example.com",
248+ want: "https://example.com",
249+ },
250+ {
251+ name: "tab before URL",
252+ input: "\thttps://example.com",
253+ want: "https://example.com",
254+ },
255+ {
256+ name: "mixed whitespace before URL",
257+ input: " \t https://example.com",
258+ want: "https://example.com",
259+ },
260+ {
261+ name: "space before domain",
262+ input: " example.com",
263+ want: "example.com",
264+ },
265+ {
266+ name: "spaces before localhost",
267+ input: " localhost:8080",
268+ want: "localhost:8080",
269+ },
270+ {
271+ name: "no leading space URL unchanged",
272+ input: "https://example.com",
273+ want: "https://example.com",
274+ },
275+ {
276+ name: "search query with leading space unchanged",
277+ input: " hello world",
278+ want: " hello world",
279+ },
280+ {
281+ name: "single word with leading space unchanged",
282+ input: " hello",
283+ want: " hello",
284+ },
285+ {
286+ name: "empty string unchanged",
287+ input: "",
288+ want: "",
289+ },
290+ {
291+ name: "only spaces unchanged",
292+ input: " ",
293+ want: " ",
294+ },
295+ }
296+
297+ for _, tt := range tests {
298+ t.Run(tt.name, func(t *testing.T) {
299+ got := TrimLeadingSpacesIfURL(tt.input)
300+ if got != tt.want {
301+ t.Errorf("TrimLeadingSpacesIfURL(%q) = %q, want %q", tt.input, got, tt.want)
302+ }
303+ })
304+ }
305+}
306+
234 func TestLooksLikeURL(t *testing.T) {307 func TestLooksLikeURL(t *testing.T) {
235 tests := []struct {308 tests := []struct {
236 name string309 name string
M internal/ui/component/omnibox.go +8 -1
@@ -801,11 +801,18 @@ func (o *Omnibox) onEntryChanged() {
801801 return
802802 }
803803
804+ entryText := o.entry.GetText()
805+ if trimmed := url.TrimLeadingSpacesIfURL(entryText); trimmed != entryText {
806+ o.entry.SetText(trimmed)
807+ o.entry.SetPosition(-1)
808+ return
809+ }
810+
804811 // Reset navigation state when user types - space should type, not toggle favorite
805812 o.mu.Lock()
806813 o.hasNavigated = false
807814 // Update realInput with what user actually typed
808- o.realInput = o.entry.GetText()
815+ o.realInput = entryText
809816 o.mu.Unlock()
810817
811818 // Clear ghost text when user types (it will be recalculated after search)
@@ -801,11 +801,18 @@ func (o *Omnibox) onEntryChanged() {
801 return801 return
802 }802 }
803 803
804+ entryText := o.entry.GetText()
805+ if trimmed := url.TrimLeadingSpacesIfURL(entryText); trimmed != entryText {
806+ o.entry.SetText(trimmed)
807+ o.entry.SetPosition(-1)
808+ return
809+ }
810+
804 // Reset navigation state when user types - space should type, not toggle favorite811 // Reset navigation state when user types - space should type, not toggle favorite
805 o.mu.Lock()812 o.mu.Lock()
806 o.hasNavigated = false813 o.hasNavigated = false
807 // Update realInput with what user actually typed814 // Update realInput with what user actually typed
808- o.realInput = o.entry.GetText()815+ o.realInput = entryText
809 o.mu.Unlock()816 o.mu.Unlock()
810 817
811 // Clear ghost text when user types (it will be recalculated after search)818 // Clear ghost text when user types (it will be recalculated after search)

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