detached 3145582

feat(ui): add dmenu favicon support and use dumber browse command

commit
3145582437b2ddf41489618ec4f70879d8f23ebe
author
bnema · · 2025-12-18 19:18 UTC
parent
cc61e77
signature
unsigned
4 files · +99 -48
M internal/application/usecase/navigate.go +21 -3
@@ -4,6 +4,8 @@ import (
44 "context"
55 "fmt"
66 "strings"
7+ "sync"
8+ "time"
79
810 "github.com/bnema/dumber/internal/application/port"
911 "github.com/bnema/dumber/internal/domain/entity"
@@ -13,9 +15,10 @@ import (
1315
1416 // NavigateUseCase handles URL navigation with history recording and zoom application.
1517 type NavigateUseCase struct {
16- historyRepo repository.HistoryRepository
17- zoomRepo repository.ZoomRepository
18- defaultZoom float64
18+ historyRepo repository.HistoryRepository
19+ zoomRepo repository.ZoomRepository
20+ defaultZoom float64
21+ recentVisits sync.Map // key: url, value: time.Time - for deduplication
1922 }
2023
2124 // NewNavigateUseCase creates a new navigation use case.
@@ -71,6 +74,11 @@ func (uc *NavigateUseCase) Execute(ctx context.Context, input NavigateInput) (*N
7174 }, nil
7275 }
7376
77+// historyDeduplicationWindow is the time window for deduplicating history visits.
78+// Visits to the same URL within this window count as a single visit.
79+// This prevents inflation from redirects and rapid navigation.
80+const historyDeduplicationWindow = 2 * time.Second
81+
7482 // RecordHistory saves or updates the history entry.
7583 // Should be called on LoadCommitted when URI is guaranteed correct.
7684 func (uc *NavigateUseCase) RecordHistory(ctx context.Context, url string) {
@@ -79,6 +87,16 @@ func (uc *NavigateUseCase) RecordHistory(ctx context.Context, url string) {
7987 // Normalize URL to avoid duplicates (e.g., github.com vs github.com/)
8088 url = normalizeURLForHistory(url)
8189
90+ // Time-based deduplication: skip if same URL was recorded recently
91+ now := time.Now()
92+ if lastTime, ok := uc.recentVisits.Load(url); ok {
93+ if now.Sub(lastTime.(time.Time)) < historyDeduplicationWindow {
94+ log.Debug().Str("url", url).Msg("skipping duplicate history record within dedup window")
95+ return
96+ }
97+ }
98+ uc.recentVisits.Store(url, now)
99+
82100 // Check if entry exists
83101 existing, err := uc.historyRepo.FindByURL(ctx, url)
84102 if err != nil {
@@ -4,6 +4,8 @@ import (
4 "context"4 "context"
5 "fmt"5 "fmt"
6 "strings"6 "strings"
7+ "sync"
8+ "time"
7 9
8 "github.com/bnema/dumber/internal/application/port"10 "github.com/bnema/dumber/internal/application/port"
9 "github.com/bnema/dumber/internal/domain/entity"11 "github.com/bnema/dumber/internal/domain/entity"
@@ -13,9 +15,10 @@ import (
13 15
14 // NavigateUseCase handles URL navigation with history recording and zoom application.16 // NavigateUseCase handles URL navigation with history recording and zoom application.
15 type NavigateUseCase struct {17 type NavigateUseCase struct {
16- historyRepo repository.HistoryRepository18+ historyRepo repository.HistoryRepository
17- zoomRepo repository.ZoomRepository19+ zoomRepo repository.ZoomRepository
18- defaultZoom float6420+ defaultZoom float64
21+ recentVisits sync.Map // key: url, value: time.Time - for deduplication
19 }22 }
20 23
21 // NewNavigateUseCase creates a new navigation use case.24 // NewNavigateUseCase creates a new navigation use case.
@@ -71,6 +74,11 @@ func (uc *NavigateUseCase) Execute(ctx context.Context, input NavigateInput) (*N
71 }, nil74 }, nil
72 }75 }
73 76
77+// historyDeduplicationWindow is the time window for deduplicating history visits.
78+// Visits to the same URL within this window count as a single visit.
79+// This prevents inflation from redirects and rapid navigation.
80+const historyDeduplicationWindow = 2 * time.Second
81+
74 // RecordHistory saves or updates the history entry.82 // RecordHistory saves or updates the history entry.
75 // Should be called on LoadCommitted when URI is guaranteed correct.83 // Should be called on LoadCommitted when URI is guaranteed correct.
76 func (uc *NavigateUseCase) RecordHistory(ctx context.Context, url string) {84 func (uc *NavigateUseCase) RecordHistory(ctx context.Context, url string) {
@@ -79,6 +87,16 @@ func (uc *NavigateUseCase) RecordHistory(ctx context.Context, url string) {
79 // Normalize URL to avoid duplicates (e.g., github.com vs github.com/)87 // Normalize URL to avoid duplicates (e.g., github.com vs github.com/)
80 url = normalizeURLForHistory(url)88 url = normalizeURLForHistory(url)
81 89
90+ // Time-based deduplication: skip if same URL was recorded recently
91+ now := time.Now()
92+ if lastTime, ok := uc.recentVisits.Load(url); ok {
93+ if now.Sub(lastTime.(time.Time)) < historyDeduplicationWindow {
94+ log.Debug().Str("url", url).Msg("skipping duplicate history record within dedup window")
95+ return
96+ }
97+ }
98+ uc.recentVisits.Store(url, now)
99+
82 // Check if entry exists100 // Check if entry exists
83 existing, err := uc.historyRepo.FindByURL(ctx, url)101 existing, err := uc.historyRepo.FindByURL(ctx, url)
84 if err != nil {102 if err != nil {
M internal/cli/cmd/dmenu.go +36 -4
@@ -6,12 +6,14 @@ import (
66 "fmt"
77 "os"
88 "os/exec"
9+ "path/filepath"
910 "strings"
1011
1112 tea "github.com/charmbracelet/bubbletea"
1213 "github.com/spf13/cobra"
1314
1415 "github.com/bnema/dumber/internal/cli/model"
16+ domainurl "github.com/bnema/dumber/internal/domain/url"
1517 "github.com/bnema/dumber/internal/infrastructure/config"
1618 )
1719
@@ -75,6 +77,25 @@ func getDmenuConfig() config.DmenuConfig {
7577 return cfg
7678 }
7779
80+// getFaviconPath returns the path to a cached favicon for the given URL.
81+// Returns empty string if the favicon doesn't exist in cache.
82+func getFaviconPath(rawURL string) string {
83+ cacheDir, err := config.GetFaviconCacheDir()
84+ if err != nil {
85+ return ""
86+ }
87+ domain := domainurl.ExtractDomain(rawURL)
88+ if domain == "" {
89+ return ""
90+ }
91+ filename := domainurl.SanitizeDomainForFilename(domain)
92+ path := filepath.Join(cacheDir, filename)
93+ if _, err := os.Stat(path); err == nil {
94+ return path
95+ }
96+ return ""
97+}
98+
7899 // runDmenuPipe outputs history entries for launcher consumption.
79100 func runDmenuPipe() error {
80101 app := GetApp()
@@ -113,7 +134,13 @@ func runDmenuPipe() error {
113134
114135 // Output with URL as the actual value (for selection)
115136 // Using tab separator so rofi can use -d '\t'
116- fmt.Printf("%s\t%s\n", parts[0], entry.URL)
137+ // Format: EntryName\0icon\x1f/path/to/favicon\tURL (rofi/fuzzel compatible)
138+ faviconPath := getFaviconPath(entry.URL)
139+ if faviconPath != "" {
140+ fmt.Printf("%s\x00icon\x1f%s\t%s\n", parts[0], faviconPath, entry.URL)
141+ } else {
142+ fmt.Printf("%s\t%s\n", parts[0], entry.URL)
143+ }
117144 }
118145
119146 return nil
@@ -168,10 +195,15 @@ func runDmenuInteractive() error {
168195 return nil
169196 }
170197
171-// openInBrowser opens a URL in the default browser.
198+// openInBrowser opens a URL in dumber browser.
172199 func openInBrowser(url string) error {
173- // Try xdg-open first (Linux)
174- cmd := newCommand("xdg-open", url)
200+ // Use the current executable to open in dumber
201+ executable, err := os.Executable()
202+ if err != nil {
203+ // Fallback to searching PATH
204+ executable = "dumber"
205+ }
206+ cmd := newCommand(executable, "browse", url)
175207 return cmd.Start()
176208 }
177209
@@ -6,12 +6,14 @@ import (
6 "fmt"6 "fmt"
7 "os"7 "os"
8 "os/exec"8 "os/exec"
9+ "path/filepath"
9 "strings"10 "strings"
10 11
11 tea "github.com/charmbracelet/bubbletea"12 tea "github.com/charmbracelet/bubbletea"
12 "github.com/spf13/cobra"13 "github.com/spf13/cobra"
13 14
14 "github.com/bnema/dumber/internal/cli/model"15 "github.com/bnema/dumber/internal/cli/model"
16+ domainurl "github.com/bnema/dumber/internal/domain/url"
15 "github.com/bnema/dumber/internal/infrastructure/config"17 "github.com/bnema/dumber/internal/infrastructure/config"
16 )18 )
17 19
@@ -75,6 +77,25 @@ func getDmenuConfig() config.DmenuConfig {
75 return cfg77 return cfg
76 }78 }
77 79
80+// getFaviconPath returns the path to a cached favicon for the given URL.
81+// Returns empty string if the favicon doesn't exist in cache.
82+func getFaviconPath(rawURL string) string {
83+ cacheDir, err := config.GetFaviconCacheDir()
84+ if err != nil {
85+ return ""
86+ }
87+ domain := domainurl.ExtractDomain(rawURL)
88+ if domain == "" {
89+ return ""
90+ }
91+ filename := domainurl.SanitizeDomainForFilename(domain)
92+ path := filepath.Join(cacheDir, filename)
93+ if _, err := os.Stat(path); err == nil {
94+ return path
95+ }
96+ return ""
97+}
98+
78 // runDmenuPipe outputs history entries for launcher consumption.99 // runDmenuPipe outputs history entries for launcher consumption.
79 func runDmenuPipe() error {100 func runDmenuPipe() error {
80 app := GetApp()101 app := GetApp()
@@ -113,7 +134,13 @@ func runDmenuPipe() error {
113 134
114 // Output with URL as the actual value (for selection)135 // Output with URL as the actual value (for selection)
115 // Using tab separator so rofi can use -d '\t'136 // Using tab separator so rofi can use -d '\t'
116- fmt.Printf("%s\t%s\n", parts[0], entry.URL)137+ // Format: EntryName\0icon\x1f/path/to/favicon\tURL (rofi/fuzzel compatible)
138+ faviconPath := getFaviconPath(entry.URL)
139+ if faviconPath != "" {
140+ fmt.Printf("%s\x00icon\x1f%s\t%s\n", parts[0], faviconPath, entry.URL)
141+ } else {
142+ fmt.Printf("%s\t%s\n", parts[0], entry.URL)
143+ }
117 }144 }
118 145
119 return nil146 return nil
@@ -168,10 +195,15 @@ func runDmenuInteractive() error {
168 return nil195 return nil
169 }196 }
170 197
171-// openInBrowser opens a URL in the default browser.198+// openInBrowser opens a URL in dumber browser.
172 func openInBrowser(url string) error {199 func openInBrowser(url string) error {
173- // Try xdg-open first (Linux)200+ // Use the current executable to open in dumber
174- cmd := newCommand("xdg-open", url)201+ executable, err := os.Executable()
202+ if err != nil {
203+ // Fallback to searching PATH
204+ executable = "dumber"
205+ }
206+ cmd := newCommand(executable, "browse", url)
175 return cmd.Start()207 return cmd.Start()
176 }208 }
177 209
M internal/domain/url/normalize.go +35 -1
@@ -1,7 +1,10 @@
11 // Package url provides URL manipulation utilities for the browser.
22 package url
33
4-import "strings"
4+import (
5+ "net/url"
6+ "strings"
7+)
58
69 // Normalize adds https:// prefix if missing for URL-like inputs.
710 // Returns the input unchanged if it already has a scheme or doesn't look like a URL.
@@ -41,3 +44,34 @@ func LooksLikeURL(input string) bool {
4144 // Contains a dot and no spaces = likely a URL
4245 return strings.Contains(input, ".") && !strings.Contains(input, " ")
4346 }
47+
48+// ExtractDomain extracts the normalized domain (host) from a URL string.
49+// Normalizes by stripping "www." prefix so youtube.com and www.youtube.com
50+// resolve to the same value.
51+func ExtractDomain(rawURL string) string {
52+ if rawURL == "" {
53+ return ""
54+ }
55+ parsed, err := url.Parse(rawURL)
56+ if err != nil || parsed.Host == "" {
57+ return ""
58+ }
59+ return strings.TrimPrefix(parsed.Host, "www.")
60+}
61+
62+// SanitizeDomainForFilename converts a domain to a safe filename with .ico extension.
63+// Replaces unsafe filesystem characters with underscores.
64+func SanitizeDomainForFilename(domain string) string {
65+ replacer := strings.NewReplacer(
66+ ":", "_",
67+ "/", "_",
68+ "\\", "_",
69+ "*", "_",
70+ "?", "_",
71+ "\"", "_",
72+ "<", "_",
73+ ">", "_",
74+ "|", "_",
75+ )
76+ return replacer.Replace(domain) + ".ico"
77+}
@@ -1,7 +1,10 @@
1 // Package url provides URL manipulation utilities for the browser.1 // Package url provides URL manipulation utilities for the browser.
2 package url2 package url
3 3
4-import "strings"4+import (
5+ "net/url"
6+ "strings"
7+)
5 8
6 // Normalize adds https:// prefix if missing for URL-like inputs.9 // Normalize adds https:// prefix if missing for URL-like inputs.
7 // Returns the input unchanged if it already has a scheme or doesn't look like a URL.10 // Returns the input unchanged if it already has a scheme or doesn't look like a URL.
@@ -41,3 +44,34 @@ func LooksLikeURL(input string) bool {
41 // Contains a dot and no spaces = likely a URL44 // Contains a dot and no spaces = likely a URL
42 return strings.Contains(input, ".") && !strings.Contains(input, " ")45 return strings.Contains(input, ".") && !strings.Contains(input, " ")
43 }46 }
47+
48+// ExtractDomain extracts the normalized domain (host) from a URL string.
49+// Normalizes by stripping "www." prefix so youtube.com and www.youtube.com
50+// resolve to the same value.
51+func ExtractDomain(rawURL string) string {
52+ if rawURL == "" {
53+ return ""
54+ }
55+ parsed, err := url.Parse(rawURL)
56+ if err != nil || parsed.Host == "" {
57+ return ""
58+ }
59+ return strings.TrimPrefix(parsed.Host, "www.")
60+}
61+
62+// SanitizeDomainForFilename converts a domain to a safe filename with .ico extension.
63+// Replaces unsafe filesystem characters with underscores.
64+func SanitizeDomainForFilename(domain string) string {
65+ replacer := strings.NewReplacer(
66+ ":", "_",
67+ "/", "_",
68+ "\\", "_",
69+ "*", "_",
70+ "?", "_",
71+ "\"", "_",
72+ "<", "_",
73+ ">", "_",
74+ "|", "_",
75+ )
76+ return replacer.Replace(domain) + ".ico"
77+}
M internal/ui/cache/favicon_cache.go +7 -40
@@ -9,10 +9,10 @@ import (
99 "net/url"
1010 "os"
1111 "path/filepath"
12- "strings"
1312 "sync"
1413 "time"
1514
15+ domainurl "github.com/bnema/dumber/internal/domain/url"
1616 "github.com/bnema/dumber/internal/infrastructure/config"
1717 "github.com/bnema/dumber/internal/logging"
1818 "github.com/bnema/puregotk-webkit/webkit"
@@ -78,23 +78,6 @@ func NewFaviconCache(faviconDB *webkit.FaviconDatabase) *FaviconCache {
7878 return fc
7979 }
8080
81-// extractDomain extracts the normalized domain (host) from a URL string.
82-// Normalizes by stripping "www." prefix so youtube.com and www.youtube.com
83-// resolve to the same cache key.
84-func extractDomain(rawURL string) string {
85- if rawURL == "" {
86- return ""
87- }
88- parsed, err := url.Parse(rawURL)
89- if err != nil {
90- return ""
91- }
92- host := parsed.Host
93- // Normalize: strip www. prefix
94- host = strings.TrimPrefix(host, "www.")
95- return host
96-}
97-
9881 // Set stores a favicon texture for the given domain.
9982 func (fc *FaviconCache) Set(domain string, texture *gdk.Texture) {
10083 if domain == "" || texture == nil {
@@ -107,7 +90,7 @@ func (fc *FaviconCache) Set(domain string, texture *gdk.Texture) {
10790
10891 // SetByURL stores a favicon texture for the domain extracted from the URL.
10992 func (fc *FaviconCache) SetByURL(pageURL string, texture *gdk.Texture) {
110- domain := extractDomain(pageURL)
93+ domain := domainurl.ExtractDomain(pageURL)
11194 fc.Set(domain, texture)
11295 }
11396
@@ -126,7 +109,7 @@ func (fc *FaviconCache) Get(domain string) *gdk.Texture {
126109 // GetByURL retrieves a favicon texture by extracting the domain from the URL.
127110 // Returns nil if not found in memory cache.
128111 func (fc *FaviconCache) GetByURL(pageURL string) *gdk.Texture {
129- domain := extractDomain(pageURL)
112+ domain := domainurl.ExtractDomain(pageURL)
130113 return fc.Get(domain)
131114 }
132115
@@ -134,7 +117,7 @@ func (fc *FaviconCache) GetByURL(pageURL string) *gdk.Texture {
134117 // Returns nil if not found. Does not fetch from external sources.
135118 // If found on disk, also populates memory cache.
136119 func (fc *FaviconCache) GetFromCacheByURL(pageURL string) *gdk.Texture {
137- domain := extractDomain(pageURL)
120+ domain := domainurl.ExtractDomain(pageURL)
138121 if domain == "" {
139122 return nil
140123 }
@@ -163,7 +146,7 @@ func (fc *FaviconCache) GetOrFetch(ctx context.Context, pageURL string, callback
163146 }
164147
165148 log := logging.FromContext(ctx)
166- domain := extractDomain(pageURL)
149+ domain := domainurl.ExtractDomain(pageURL)
167150
168151 // Check in-memory cache first
169152 if texture := fc.Get(domain); texture != nil {
@@ -315,22 +298,6 @@ func (fc *FaviconCache) Close() {
315298 })
316299 }
317300
318-// sanitizeDomainForFilename converts a domain to a safe filename.
319-func sanitizeDomainForFilename(domain string) string {
320- replacer := strings.NewReplacer(
321- ":", "_",
322- "/", "_",
323- "\\", "_",
324- "*", "_",
325- "?", "_",
326- "\"", "_",
327- "<", "_",
328- ">", "_",
329- "|", "_",
330- )
331- return replacer.Replace(domain) + ".ico"
332-}
333-
334301 // loadFromDisk attempts to load a favicon from disk cache.
335302 // Returns nil if not found or on error.
336303 func (fc *FaviconCache) loadFromDisk(domain string) *gdk.Texture {
@@ -338,7 +305,7 @@ func (fc *FaviconCache) loadFromDisk(domain string) *gdk.Texture {
338305 return nil
339306 }
340307
341- filename := sanitizeDomainForFilename(domain)
308+ filename := domainurl.SanitizeDomainForFilename(domain)
342309 path := filepath.Join(fc.diskCacheDir, filename)
343310
344311 //nolint:gosec // path is constructed from sanitized domain, not user input
@@ -375,7 +342,7 @@ func (fc *FaviconCache) writeToDisk(domain string, data []byte) {
375342 return
376343 }
377344
378- filename := sanitizeDomainForFilename(domain)
345+ filename := domainurl.SanitizeDomainForFilename(domain)
379346 finalPath := filepath.Join(fc.diskCacheDir, filename)
380347 tempPath := finalPath + ".tmp"
381348
@@ -9,10 +9,10 @@ import (
9 "net/url"9 "net/url"
10 "os"10 "os"
11 "path/filepath"11 "path/filepath"
12- "strings"
13 "sync"12 "sync"
14 "time"13 "time"
15 14
15+ domainurl "github.com/bnema/dumber/internal/domain/url"
16 "github.com/bnema/dumber/internal/infrastructure/config"16 "github.com/bnema/dumber/internal/infrastructure/config"
17 "github.com/bnema/dumber/internal/logging"17 "github.com/bnema/dumber/internal/logging"
18 "github.com/bnema/puregotk-webkit/webkit"18 "github.com/bnema/puregotk-webkit/webkit"
@@ -78,23 +78,6 @@ func NewFaviconCache(faviconDB *webkit.FaviconDatabase) *FaviconCache {
78 return fc78 return fc
79 }79 }
80 80
81-// extractDomain extracts the normalized domain (host) from a URL string.
82-// Normalizes by stripping "www." prefix so youtube.com and www.youtube.com
83-// resolve to the same cache key.
84-func extractDomain(rawURL string) string {
85- if rawURL == "" {
86- return ""
87- }
88- parsed, err := url.Parse(rawURL)
89- if err != nil {
90- return ""
91- }
92- host := parsed.Host
93- // Normalize: strip www. prefix
94- host = strings.TrimPrefix(host, "www.")
95- return host
96-}
97-
98 // Set stores a favicon texture for the given domain.81 // Set stores a favicon texture for the given domain.
99 func (fc *FaviconCache) Set(domain string, texture *gdk.Texture) {82 func (fc *FaviconCache) Set(domain string, texture *gdk.Texture) {
100 if domain == "" || texture == nil {83 if domain == "" || texture == nil {
@@ -107,7 +90,7 @@ func (fc *FaviconCache) Set(domain string, texture *gdk.Texture) {
107 90
108 // SetByURL stores a favicon texture for the domain extracted from the URL.91 // SetByURL stores a favicon texture for the domain extracted from the URL.
109 func (fc *FaviconCache) SetByURL(pageURL string, texture *gdk.Texture) {92 func (fc *FaviconCache) SetByURL(pageURL string, texture *gdk.Texture) {
110- domain := extractDomain(pageURL)93+ domain := domainurl.ExtractDomain(pageURL)
111 fc.Set(domain, texture)94 fc.Set(domain, texture)
112 }95 }
113 96
@@ -126,7 +109,7 @@ func (fc *FaviconCache) Get(domain string) *gdk.Texture {
126 // GetByURL retrieves a favicon texture by extracting the domain from the URL.109 // GetByURL retrieves a favicon texture by extracting the domain from the URL.
127 // Returns nil if not found in memory cache.110 // Returns nil if not found in memory cache.
128 func (fc *FaviconCache) GetByURL(pageURL string) *gdk.Texture {111 func (fc *FaviconCache) GetByURL(pageURL string) *gdk.Texture {
129- domain := extractDomain(pageURL)112+ domain := domainurl.ExtractDomain(pageURL)
130 return fc.Get(domain)113 return fc.Get(domain)
131 }114 }
132 115
@@ -134,7 +117,7 @@ func (fc *FaviconCache) GetByURL(pageURL string) *gdk.Texture {
134 // Returns nil if not found. Does not fetch from external sources.117 // Returns nil if not found. Does not fetch from external sources.
135 // If found on disk, also populates memory cache.118 // If found on disk, also populates memory cache.
136 func (fc *FaviconCache) GetFromCacheByURL(pageURL string) *gdk.Texture {119 func (fc *FaviconCache) GetFromCacheByURL(pageURL string) *gdk.Texture {
137- domain := extractDomain(pageURL)120+ domain := domainurl.ExtractDomain(pageURL)
138 if domain == "" {121 if domain == "" {
139 return nil122 return nil
140 }123 }
@@ -163,7 +146,7 @@ func (fc *FaviconCache) GetOrFetch(ctx context.Context, pageURL string, callback
163 }146 }
164 147
165 log := logging.FromContext(ctx)148 log := logging.FromContext(ctx)
166- domain := extractDomain(pageURL)149+ domain := domainurl.ExtractDomain(pageURL)
167 150
168 // Check in-memory cache first151 // Check in-memory cache first
169 if texture := fc.Get(domain); texture != nil {152 if texture := fc.Get(domain); texture != nil {
@@ -315,22 +298,6 @@ func (fc *FaviconCache) Close() {
315 })298 })
316 }299 }
317 300
318-// sanitizeDomainForFilename converts a domain to a safe filename.
319-func sanitizeDomainForFilename(domain string) string {
320- replacer := strings.NewReplacer(
321- ":", "_",
322- "/", "_",
323- "\\", "_",
324- "*", "_",
325- "?", "_",
326- "\"", "_",
327- "<", "_",
328- ">", "_",
329- "|", "_",
330- )
331- return replacer.Replace(domain) + ".ico"
332-}
333-
334 // loadFromDisk attempts to load a favicon from disk cache.301 // loadFromDisk attempts to load a favicon from disk cache.
335 // Returns nil if not found or on error.302 // Returns nil if not found or on error.
336 func (fc *FaviconCache) loadFromDisk(domain string) *gdk.Texture {303 func (fc *FaviconCache) loadFromDisk(domain string) *gdk.Texture {
@@ -338,7 +305,7 @@ func (fc *FaviconCache) loadFromDisk(domain string) *gdk.Texture {
338 return nil305 return nil
339 }306 }
340 307
341- filename := sanitizeDomainForFilename(domain)308+ filename := domainurl.SanitizeDomainForFilename(domain)
342 path := filepath.Join(fc.diskCacheDir, filename)309 path := filepath.Join(fc.diskCacheDir, filename)
343 310
344 //nolint:gosec // path is constructed from sanitized domain, not user input311 //nolint:gosec // path is constructed from sanitized domain, not user input
@@ -375,7 +342,7 @@ func (fc *FaviconCache) writeToDisk(domain string, data []byte) {
375 return342 return
376 }343 }
377 344
378- filename := sanitizeDomainForFilename(domain)345+ filename := domainurl.SanitizeDomainForFilename(domain)
379 finalPath := filepath.Join(fc.diskCacheDir, filename)346 finalPath := filepath.Join(fc.diskCacheDir, filename)
380 tempPath := finalPath + ".tmp"347 tempPath := finalPath + ".tmp"
381 348

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