detached 5b002da

feat(backend): integrate color palettes and improve history suggestions

WebView & Settings:
- Add ColorPalette and ColorPalettes structs to webkit config
- Inject color palette tokens from Go config as user script at document start
- Apply palette based on light/dark theme with CSS custom properties
- Support related webview creation with color config
- Update AV1 user agent to Chrome 141.0.0.0

Messaging Handler:
- Remove automatic search shortcuts from query results (rely on explicit prefix commands)
- Use favicon_url from database when available for history suggestions
- Simplify query handler by removing shortcut expansion logic

Browser App:
- Add buildColorPalettes() helper to convert config palettes to webkit format
commit
5b002daf7d49f73f91e689765edef3a071324b7d
author
bamen · · 2025-10-06 17:24 UTC
committer
bamen · · 2025-10-06 17:46 UTC
parent
135779d
signature
unsigned
4 files · +104 -40
M internal/app/browser/webview.go +20 -0
@@ -45,6 +45,25 @@ func buildMemoryConfig(cfg config.WebkitMemoryConfig) webkit.MemoryConfig {
4545 return mc
4646 }
4747
48+func buildColorPalettes(cfg config.AppearanceConfig) webkit.ColorPalettes {
49+ toPalette := func(src config.ColorPalette) webkit.ColorPalette {
50+ return webkit.ColorPalette{
51+ Background: src.Background,
52+ Surface: src.Surface,
53+ SurfaceVariant: src.SurfaceVariant,
54+ Text: src.Text,
55+ Muted: src.Muted,
56+ Accent: src.Accent,
57+ Border: src.Border,
58+ }
59+ }
60+
61+ return webkit.ColorPalettes{
62+ Light: toPalette(cfg.LightPalette),
63+ Dark: toPalette(cfg.DarkPalette),
64+ }
65+}
66+
4867 func (app *BrowserApp) buildWebkitConfig() (*webkit.Config, error) {
4968 dataDir, err := config.GetDataDir()
5069 if err != nil {
@@ -93,6 +112,7 @@ func (app *BrowserApp) buildWebkitConfig() (*webkit.Config, error) {
93112 CustomUserAgent: app.config.CodecPreferences.CustomUserAgent,
94113 DisableTwitchCodecControl: app.config.CodecPreferences.DisableTwitchCodecControl,
95114 },
115+ Colors: buildColorPalettes(app.config.Appearance),
96116 }
97117
98118 return cfg, nil
@@ -45,6 +45,25 @@ func buildMemoryConfig(cfg config.WebkitMemoryConfig) webkit.MemoryConfig {
45 return mc45 return mc
46 }46 }
47 47
48+func buildColorPalettes(cfg config.AppearanceConfig) webkit.ColorPalettes {
49+ toPalette := func(src config.ColorPalette) webkit.ColorPalette {
50+ return webkit.ColorPalette{
51+ Background: src.Background,
52+ Surface: src.Surface,
53+ SurfaceVariant: src.SurfaceVariant,
54+ Text: src.Text,
55+ Muted: src.Muted,
56+ Accent: src.Accent,
57+ Border: src.Border,
58+ }
59+ }
60+
61+ return webkit.ColorPalettes{
62+ Light: toPalette(cfg.LightPalette),
63+ Dark: toPalette(cfg.DarkPalette),
64+ }
65+}
66+
48 func (app *BrowserApp) buildWebkitConfig() (*webkit.Config, error) {67 func (app *BrowserApp) buildWebkitConfig() (*webkit.Config, error) {
49 dataDir, err := config.GetDataDir()68 dataDir, err := config.GetDataDir()
50 if err != nil {69 if err != nil {
@@ -93,6 +112,7 @@ func (app *BrowserApp) buildWebkitConfig() (*webkit.Config, error) {
93 CustomUserAgent: app.config.CodecPreferences.CustomUserAgent,112 CustomUserAgent: app.config.CodecPreferences.CustomUserAgent,
94 DisableTwitchCodecControl: app.config.CodecPreferences.DisableTwitchCodecControl,113 DisableTwitchCodecControl: app.config.CodecPreferences.DisableTwitchCodecControl,
95 },114 },
115+ Colors: buildColorPalettes(app.config.Appearance),
96 }116 }
97 117
98 return cfg, nil118 return cfg, nil
M internal/app/messaging/handler.go +9 -39
@@ -4,8 +4,6 @@ import (
44 "context"
55 "encoding/json"
66 "log"
7- neturl "net/url"
8- "sort"
97 "strconv"
108 "strings"
119 "time"
@@ -273,45 +271,10 @@ func (h *Handler) handleQuery(msg Message) {
273271 return scheme + "://" + host + "/favicon.ico"
274272 }
275273
276- expandShortcut := func(tpl string, query string) string {
277- esc := neturl.QueryEscape(query)
278- u := strings.ReplaceAll(tpl, "{query}", esc)
279- u = strings.ReplaceAll(u, "%s", esc)
280- return u
281- }
282-
283274 results := make([]suggestion, 0, limit)
284275 seen := make(map[string]struct{}, limit*2)
285276
286- // Shortcuts
287- if shortcuts, err := h.browserService.GetSearchShortcuts(ctx); err != nil {
288- log.Printf("[ERROR] Failed to get search shortcuts: %v", err)
289- } else if len(shortcuts) > 0 {
290- log.Printf("[DEBUG] Found %d search shortcuts", len(shortcuts))
291- keys := make([]string, 0, len(shortcuts))
292- for k := range shortcuts {
293- keys = append(keys, k)
294- }
295- sort.Strings(keys)
296- for _, k := range keys {
297- sc := shortcuts[k]
298- url := expandShortcut(sc.URL, q)
299- log.Printf("[DEBUG] Shortcut %s -> %s", k, url)
300- if url == "" {
301- continue
302- }
303- if _, ok := seen[url]; ok {
304- continue
305- }
306- results = append(results, suggestion{URL: url, Favicon: buildFavicon(url)})
307- seen[url] = struct{}{}
308- if len(results) >= limit {
309- break
310- }
311- }
312- } else {
313- log.Printf("[DEBUG] No search shortcuts found")
314- }
277+ // Shortcuts intentionally omitted; rely on explicit prefix commands (e.g., gh:) or history results
315278
316279 // History
317280 if len(results) < limit {
@@ -339,7 +302,14 @@ func (h *Handler) handleQuery(msg Message) {
339302 if _, ok := seen[url]; ok {
340303 continue
341304 }
342- results = append(results, suggestion{URL: url, Favicon: buildFavicon(url)})
305+ // Use favicon_url from database if available, otherwise build it
306+ favicon := ""
307+ if s, ok := m["favicon_url"].(string); ok && s != "" {
308+ favicon = s
309+ } else {
310+ favicon = buildFavicon(url)
311+ }
312+ results = append(results, suggestion{URL: url, Favicon: favicon})
343313 seen[url] = struct{}{}
344314 if len(results) >= limit {
345315 break
@@ -4,8 +4,6 @@ import (
4 "context"4 "context"
5 "encoding/json"5 "encoding/json"
6 "log"6 "log"
7- neturl "net/url"
8- "sort"
9 "strconv"7 "strconv"
10 "strings"8 "strings"
11 "time"9 "time"
@@ -273,45 +271,10 @@ func (h *Handler) handleQuery(msg Message) {
273 return scheme + "://" + host + "/favicon.ico"271 return scheme + "://" + host + "/favicon.ico"
274 }272 }
275 273
276- expandShortcut := func(tpl string, query string) string {
277- esc := neturl.QueryEscape(query)
278- u := strings.ReplaceAll(tpl, "{query}", esc)
279- u = strings.ReplaceAll(u, "%s", esc)
280- return u
281- }
282-
283 results := make([]suggestion, 0, limit)274 results := make([]suggestion, 0, limit)
284 seen := make(map[string]struct{}, limit*2)275 seen := make(map[string]struct{}, limit*2)
285 276
286- // Shortcuts277+ // Shortcuts intentionally omitted; rely on explicit prefix commands (e.g., gh:) or history results
287- if shortcuts, err := h.browserService.GetSearchShortcuts(ctx); err != nil {
288- log.Printf("[ERROR] Failed to get search shortcuts: %v", err)
289- } else if len(shortcuts) > 0 {
290- log.Printf("[DEBUG] Found %d search shortcuts", len(shortcuts))
291- keys := make([]string, 0, len(shortcuts))
292- for k := range shortcuts {
293- keys = append(keys, k)
294- }
295- sort.Strings(keys)
296- for _, k := range keys {
297- sc := shortcuts[k]
298- url := expandShortcut(sc.URL, q)
299- log.Printf("[DEBUG] Shortcut %s -> %s", k, url)
300- if url == "" {
301- continue
302- }
303- if _, ok := seen[url]; ok {
304- continue
305- }
306- results = append(results, suggestion{URL: url, Favicon: buildFavicon(url)})
307- seen[url] = struct{}{}
308- if len(results) >= limit {
309- break
310- }
311- }
312- } else {
313- log.Printf("[DEBUG] No search shortcuts found")
314- }
315 278
316 // History279 // History
317 if len(results) < limit {280 if len(results) < limit {
@@ -339,7 +302,14 @@ func (h *Handler) handleQuery(msg Message) {
339 if _, ok := seen[url]; ok {302 if _, ok := seen[url]; ok {
340 continue303 continue
341 }304 }
342- results = append(results, suggestion{URL: url, Favicon: buildFavicon(url)})305+ // Use favicon_url from database if available, otherwise build it
306+ favicon := ""
307+ if s, ok := m["favicon_url"].(string); ok && s != "" {
308+ favicon = s
309+ } else {
310+ favicon = buildFavicon(url)
311+ }
312+ results = append(results, suggestion{URL: url, Favicon: favicon})
343 seen[url] = struct{}{}313 seen[url] = struct{}{}
344 if len(results) >= limit {314 if len(results) >= limit {
345 break315 break
M pkg/webkit/settings.go +20 -0
@@ -48,11 +48,31 @@ type Config struct {
4848 // UseDomZoom toggles CSS/DOM-based zoom instead of WebKit's native zoom.
4949 UseDomZoom bool
5050
51+ // Colors defines the light/dark palette tokens for the GUI.
52+ Colors ColorPalettes
53+
5154 // CreateWindow controls whether to create a window during WebView initialization.
5255 // Set to false when creating WebViews for workspace panes that will be reparented.
5356 CreateWindow bool
5457 }
5558
59+// ColorPalette holds semantic UI tokens for a theme.
60+type ColorPalette struct {
61+ Background string
62+ Surface string
63+ SurfaceVariant string
64+ Text string
65+ Muted string
66+ Accent string
67+ Border string
68+}
69+
70+// ColorPalettes hosts both light and dark color palettes.
71+type ColorPalettes struct {
72+ Light ColorPalette
73+ Dark ColorPalette
74+}
75+
5676 // CodecPreferencesConfig holds codec preferences for WebKit media playback
5777 type CodecPreferencesConfig struct {
5878 PreferredCodecs []string
@@ -48,11 +48,31 @@ type Config struct {
48 // UseDomZoom toggles CSS/DOM-based zoom instead of WebKit's native zoom.48 // UseDomZoom toggles CSS/DOM-based zoom instead of WebKit's native zoom.
49 UseDomZoom bool49 UseDomZoom bool
50 50
51+ // Colors defines the light/dark palette tokens for the GUI.
52+ Colors ColorPalettes
53+
51 // CreateWindow controls whether to create a window during WebView initialization.54 // CreateWindow controls whether to create a window during WebView initialization.
52 // Set to false when creating WebViews for workspace panes that will be reparented.55 // Set to false when creating WebViews for workspace panes that will be reparented.
53 CreateWindow bool56 CreateWindow bool
54 }57 }
55 58
59+// ColorPalette holds semantic UI tokens for a theme.
60+type ColorPalette struct {
61+ Background string
62+ Surface string
63+ SurfaceVariant string
64+ Text string
65+ Muted string
66+ Accent string
67+ Border string
68+}
69+
70+// ColorPalettes hosts both light and dark color palettes.
71+type ColorPalettes struct {
72+ Light ColorPalette
73+ Dark ColorPalette
74+}
75+
56 // CodecPreferencesConfig holds codec preferences for WebKit media playback76 // CodecPreferencesConfig holds codec preferences for WebKit media playback
57 type CodecPreferencesConfig struct {77 type CodecPreferencesConfig struct {
58 PreferredCodecs []string78 PreferredCodecs []string
M pkg/webkit/webview_cgo.go +55 -1
@@ -1305,7 +1305,7 @@ func NewWebView(cfg *Config) (*WebView, error) {
13051305 log.Printf("[webkit] Set custom User-Agent: %s", cfg.CodecPreferences.CustomUserAgent)
13061306 } else if cfg.CodecPreferences.ForceAV1 {
13071307 // Use modern Chrome UA that signals AV1 support
1308- av1UA := "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
1308+ av1UA := "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"
13091309 cAV1UA := C.CString(av1UA)
13101310 C.webkit_settings_set_user_agent(settings, (*C.gchar)(cAV1UA))
13111311 C.free(unsafe.Pointer(cAV1UA))
@@ -1844,6 +1844,7 @@ func (w *WebView) CreateRelatedView() *WebView {
18441844 VideoAcceleration: w.config.VideoAcceleration,
18451845 Memory: w.config.Memory,
18461846 CodecPreferences: w.config.CodecPreferences,
1847+ Colors: w.config.Colors,
18471848 CreateWindow: false,
18481849 }
18491850 related, err := NewWebViewWithRelated(cfg, w)
@@ -2060,6 +2061,59 @@ func (w *WebView) enableUserContentManager(cfg *Config) {
20602061 C.webkit_user_script_unref(schemeScript)
20612062 }
20622063
2064+ // Inject color palette tokens sourced from Go configuration
2065+ if cfg != nil {
2066+ if paletteJSON, err := json.Marshal(cfg.Colors); err == nil && len(paletteJSON) > 0 {
2067+ paletteScript := fmt.Sprintf(`(function(){try{
2068+ window.__dumber_palette=%s;
2069+ var normalized={};
2070+ if(window.__dumber_palette){
2071+ for (var key in window.__dumber_palette){
2072+ if(Object.prototype.hasOwnProperty.call(window.__dumber_palette,key)){
2073+ normalized[key.toLowerCase()] = window.__dumber_palette[key];
2074+ }
2075+ }
2076+ window.__dumber_palette = normalized;
2077+ }
2078+ var tokenMap={Background:'--color-browser-bg',Surface:'--color-browser-surface',SurfaceVariant:'--color-browser-surface-variant',Text:'--color-browser-text',Muted:'--color-browser-muted',Accent:'--color-browser-accent',Border:'--color-browser-border'};
2079+ var dynamicMap={Background:'--dynamic-bg',Surface:'--dynamic-surface',SurfaceVariant:'--dynamic-surface-variant',Text:'--dynamic-text',Muted:'--dynamic-muted',Accent:'--dynamic-accent',Border:'--dynamic-border'};
2080+ var apply=function(theme){
2081+ var palette=(window.__dumber_palette && (window.__dumber_palette[theme] || window.__dumber_palette.light)) || null;
2082+ if(!palette){return;}
2083+ var root=document.documentElement;
2084+ for(var key in tokenMap){
2085+ if(Object.prototype.hasOwnProperty.call(tokenMap,key) && palette[key]){
2086+ root.style.setProperty(tokenMap[key], palette[key]);
2087+ if(Object.prototype.hasOwnProperty.call(dynamicMap,key)){
2088+ root.style.setProperty(dynamicMap[key], palette[key]);
2089+ }
2090+ }
2091+ }
2092+ document.dispatchEvent(new CustomEvent('dumber:palette-updated',{detail:{theme:theme}}));
2093+ };
2094+ window.__dumber_applyPalette=function(theme){
2095+ apply(theme==='dark'?'dark':'light');
2096+ };
2097+ var initial=document.documentElement.classList.contains('dark')?'dark':'light';
2098+ if(window.__dumber_initial_theme){
2099+ initial=window.__dumber_initial_theme;
2100+ }
2101+ apply(initial);
2102+}catch(err){console.error('[dumber] Failed to apply palette',err);}})();`, string(paletteJSON))
2103+ cPalette := C.CString(paletteScript)
2104+ defer C.free(unsafe.Pointer(cPalette))
2105+ paletteUserScript := C.webkit_user_script_new((*C.gchar)(cPalette), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)
2106+ if paletteUserScript != nil {
2107+ C.webkit_user_content_manager_add_script(w.native.ucm, paletteUserScript)
2108+ C.webkit_user_script_unref(paletteUserScript)
2109+ } else {
2110+ log.Printf("[webkit] Failed to create palette user script")
2111+ }
2112+ } else if err != nil {
2113+ log.Printf("[webkit] Failed to marshal color palettes: %v", err)
2114+ }
2115+ }
2116+
20632117 // Add GUI bundle as user script at document-start (contains toast, omnibox, and controls)
20642118 if cfg != nil && cfg.Assets != nil {
20652119 log.Printf("[webkit] Attempting to load GUI bundle from assets/gui/gui.min.js")
@@ -1305,7 +1305,7 @@ func NewWebView(cfg *Config) (*WebView, error) {
1305 log.Printf("[webkit] Set custom User-Agent: %s", cfg.CodecPreferences.CustomUserAgent)1305 log.Printf("[webkit] Set custom User-Agent: %s", cfg.CodecPreferences.CustomUserAgent)
1306 } else if cfg.CodecPreferences.ForceAV1 {1306 } else if cfg.CodecPreferences.ForceAV1 {
1307 // Use modern Chrome UA that signals AV1 support1307 // Use modern Chrome UA that signals AV1 support
1308- av1UA := "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"1308+ av1UA := "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"
1309 cAV1UA := C.CString(av1UA)1309 cAV1UA := C.CString(av1UA)
1310 C.webkit_settings_set_user_agent(settings, (*C.gchar)(cAV1UA))1310 C.webkit_settings_set_user_agent(settings, (*C.gchar)(cAV1UA))
1311 C.free(unsafe.Pointer(cAV1UA))1311 C.free(unsafe.Pointer(cAV1UA))
@@ -1844,6 +1844,7 @@ func (w *WebView) CreateRelatedView() *WebView {
1844 VideoAcceleration: w.config.VideoAcceleration,1844 VideoAcceleration: w.config.VideoAcceleration,
1845 Memory: w.config.Memory,1845 Memory: w.config.Memory,
1846 CodecPreferences: w.config.CodecPreferences,1846 CodecPreferences: w.config.CodecPreferences,
1847+ Colors: w.config.Colors,
1847 CreateWindow: false,1848 CreateWindow: false,
1848 }1849 }
1849 related, err := NewWebViewWithRelated(cfg, w)1850 related, err := NewWebViewWithRelated(cfg, w)
@@ -2060,6 +2061,59 @@ func (w *WebView) enableUserContentManager(cfg *Config) {
2060 C.webkit_user_script_unref(schemeScript)2061 C.webkit_user_script_unref(schemeScript)
2061 }2062 }
2062 2063
2064+ // Inject color palette tokens sourced from Go configuration
2065+ if cfg != nil {
2066+ if paletteJSON, err := json.Marshal(cfg.Colors); err == nil && len(paletteJSON) > 0 {
2067+ paletteScript := fmt.Sprintf(`(function(){try{
2068+ window.__dumber_palette=%s;
2069+ var normalized={};
2070+ if(window.__dumber_palette){
2071+ for (var key in window.__dumber_palette){
2072+ if(Object.prototype.hasOwnProperty.call(window.__dumber_palette,key)){
2073+ normalized[key.toLowerCase()] = window.__dumber_palette[key];
2074+ }
2075+ }
2076+ window.__dumber_palette = normalized;
2077+ }
2078+ var tokenMap={Background:'--color-browser-bg',Surface:'--color-browser-surface',SurfaceVariant:'--color-browser-surface-variant',Text:'--color-browser-text',Muted:'--color-browser-muted',Accent:'--color-browser-accent',Border:'--color-browser-border'};
2079+ var dynamicMap={Background:'--dynamic-bg',Surface:'--dynamic-surface',SurfaceVariant:'--dynamic-surface-variant',Text:'--dynamic-text',Muted:'--dynamic-muted',Accent:'--dynamic-accent',Border:'--dynamic-border'};
2080+ var apply=function(theme){
2081+ var palette=(window.__dumber_palette && (window.__dumber_palette[theme] || window.__dumber_palette.light)) || null;
2082+ if(!palette){return;}
2083+ var root=document.documentElement;
2084+ for(var key in tokenMap){
2085+ if(Object.prototype.hasOwnProperty.call(tokenMap,key) && palette[key]){
2086+ root.style.setProperty(tokenMap[key], palette[key]);
2087+ if(Object.prototype.hasOwnProperty.call(dynamicMap,key)){
2088+ root.style.setProperty(dynamicMap[key], palette[key]);
2089+ }
2090+ }
2091+ }
2092+ document.dispatchEvent(new CustomEvent('dumber:palette-updated',{detail:{theme:theme}}));
2093+ };
2094+ window.__dumber_applyPalette=function(theme){
2095+ apply(theme==='dark'?'dark':'light');
2096+ };
2097+ var initial=document.documentElement.classList.contains('dark')?'dark':'light';
2098+ if(window.__dumber_initial_theme){
2099+ initial=window.__dumber_initial_theme;
2100+ }
2101+ apply(initial);
2102+}catch(err){console.error('[dumber] Failed to apply palette',err);}})();`, string(paletteJSON))
2103+ cPalette := C.CString(paletteScript)
2104+ defer C.free(unsafe.Pointer(cPalette))
2105+ paletteUserScript := C.webkit_user_script_new((*C.gchar)(cPalette), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)
2106+ if paletteUserScript != nil {
2107+ C.webkit_user_content_manager_add_script(w.native.ucm, paletteUserScript)
2108+ C.webkit_user_script_unref(paletteUserScript)
2109+ } else {
2110+ log.Printf("[webkit] Failed to create palette user script")
2111+ }
2112+ } else if err != nil {
2113+ log.Printf("[webkit] Failed to marshal color palettes: %v", err)
2114+ }
2115+ }
2116+
2063 // Add GUI bundle as user script at document-start (contains toast, omnibox, and controls)2117 // Add GUI bundle as user script at document-start (contains toast, omnibox, and controls)
2064 if cfg != nil && cfg.Assets != nil {2118 if cfg != nil && cfg.Assets != nil {
2065 log.Printf("[webkit] Attempting to load GUI bundle from assets/gui/gui.min.js")2119 log.Printf("[webkit] Attempting to load GUI bundle from assets/gui/gui.min.js")

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