detached a886655

refactor(controllers): adapt clipboard, zoom, and messaging for pane-specific handling

commit
a8866552efaa7da926b6dc6fcce14cd3865e029f
author
bamen · · 2025-09-19 03:42 UTC
parent
ed638e0
signature
unsigned
5 files · +65 -55
M internal/app/control/clipboard.go +21 -3
@@ -25,15 +25,33 @@ func (c *ClipboardController) CopyCurrentURL() {
2525 currentURL := c.webView.GetCurrentURL()
2626 if currentURL == "" {
2727 log.Printf("No URL to copy")
28- _ = c.webView.InjectScript(`(window.__dumber?.toast?.show ? window.__dumber.toast.show("No URL to copy", 2000) : (window.__dumber_showToast && window.__dumber_showToast("No URL to copy", 2000)))`)
28+ c.showToast("No URL to copy", "error")
2929 return
3030 }
3131
3232 if err := clipboard.CopyToClipboard(currentURL); err != nil {
3333 log.Printf("Failed to copy URL to clipboard: %v", err)
34- _ = c.webView.InjectScript(`(window.__dumber?.toast?.show ? window.__dumber.toast.show("Failed to copy URL", 2000) : (window.__dumber_showToast && window.__dumber_showToast("Failed to copy URL", 2000)))`)
34+ c.showToast("Failed to copy URL", "error")
3535 } else {
3636 log.Printf("URL copied to clipboard: %s", currentURL)
37- _ = c.webView.InjectScript(`(window.__dumber?.toast?.show ? window.__dumber.toast.show("URL copied to clipboard", 2000) : (window.__dumber_showToast && window.__dumber_showToast("URL copied to clipboard", 2000)))`)
37+ c.showToast("URL copied to clipboard", "success")
38+ }
39+}
40+
41+func (c *ClipboardController) showToast(message, toastType string) {
42+ if c == nil || c.webView == nil {
43+ return
44+ }
45+
46+ detail := map[string]any{
47+ "message": message,
48+ "duration": 2000,
49+ }
50+ if toastType != "" {
51+ detail["type"] = toastType
52+ }
53+
54+ if err := c.webView.DispatchCustomEvent("dumber:showToast", detail); err != nil {
55+ log.Printf("Failed to dispatch toast event: %v", err)
3856 }
3957 }
@@ -25,15 +25,33 @@ func (c *ClipboardController) CopyCurrentURL() {
25 currentURL := c.webView.GetCurrentURL()25 currentURL := c.webView.GetCurrentURL()
26 if currentURL == "" {26 if currentURL == "" {
27 log.Printf("No URL to copy")27 log.Printf("No URL to copy")
28- _ = c.webView.InjectScript(`(window.__dumber?.toast?.show ? window.__dumber.toast.show("No URL to copy", 2000) : (window.__dumber_showToast && window.__dumber_showToast("No URL to copy", 2000)))`)28+ c.showToast("No URL to copy", "error")
29 return29 return
30 }30 }
31 31
32 if err := clipboard.CopyToClipboard(currentURL); err != nil {32 if err := clipboard.CopyToClipboard(currentURL); err != nil {
33 log.Printf("Failed to copy URL to clipboard: %v", err)33 log.Printf("Failed to copy URL to clipboard: %v", err)
34- _ = c.webView.InjectScript(`(window.__dumber?.toast?.show ? window.__dumber.toast.show("Failed to copy URL", 2000) : (window.__dumber_showToast && window.__dumber_showToast("Failed to copy URL", 2000)))`)34+ c.showToast("Failed to copy URL", "error")
35 } else {35 } else {
36 log.Printf("URL copied to clipboard: %s", currentURL)36 log.Printf("URL copied to clipboard: %s", currentURL)
37- _ = c.webView.InjectScript(`(window.__dumber?.toast?.show ? window.__dumber.toast.show("URL copied to clipboard", 2000) : (window.__dumber_showToast && window.__dumber_showToast("URL copied to clipboard", 2000)))`)37+ c.showToast("URL copied to clipboard", "success")
38+ }
39+}
40+
41+func (c *ClipboardController) showToast(message, toastType string) {
42+ if c == nil || c.webView == nil {
43+ return
44+ }
45+
46+ detail := map[string]any{
47+ "message": message,
48+ "duration": 2000,
49+ }
50+ if toastType != "" {
51+ detail["type"] = toastType
52+ }
53+
54+ if err := c.webView.DispatchCustomEvent("dumber:showToast", detail); err != nil {
55+ log.Printf("Failed to dispatch toast event: %v", err)
38 }56 }
39 }57 }
M internal/app/control/zoom.go +6 -43
@@ -2,7 +2,6 @@ package control
22
33 import (
44 "context"
5- "fmt"
65 "log"
76
87 "github.com/bnema/dumber/pkg/webkit"
@@ -78,49 +77,13 @@ func (z *ZoomController) handleZoomChange(level float64) {
7877 func (z *ZoomController) showZoomToast(level float64) {
7978 log.Printf("[zoom] Attempting to show zoom toast for level %.2f", level)
8079
81- percentage := int(level * 100)
82-
83- // Enhanced approach that ensures toast always appears
84- js := fmt.Sprintf(`(function() {
85- try {
86- console.log('[dumber] Attempting to show zoom toast...');
87- const zoomLevel = %f;
88- const percentage = %d;
89- let toastShown = false;
90-
91- // Prefer unified page-world API
92- if (window.__dumber && window.__dumber.toast && typeof window.__dumber.toast.zoom === 'function') {
93- console.log('[dumber] Using unified toast.zoom');
94- window.__dumber.toast.zoom(zoomLevel);
95- toastShown = true;
96- } else if (typeof window.__dumber_showZoomToast === 'function') {
97- console.log('[dumber] Using legacy zoom toast');
98- window.__dumber_showZoomToast(zoomLevel);
99- toastShown = true;
100- } else if (window.__dumber && window.__dumber.toast && typeof window.__dumber.toast.show === 'function') {
101- console.log('[dumber] Using unified toast.show');
102- window.__dumber.toast.show('Zoom: ' + percentage + '%%', 1500, 'info');
103- toastShown = true;
104- } else if (typeof window.__dumber_showToast === 'function') {
105- console.log('[dumber] Using legacy toast.show');
106- window.__dumber_showToast('Zoom: ' + percentage + '%%', 1500, 'info');
107- toastShown = true;
108- }
109-
110-
111- if (!toastShown) {
112- console.warn('[dumber] Failed to show zoom toast - no DOM body available');
113- }
114-
115- } catch(e) {
116- console.error('[dumber] Zoom toast error:', e);
117- }
118- })();`, level, percentage)
80+ if z.webView == nil {
81+ log.Printf("[zoom] webview unavailable for zoom toast")
82+ return
83+ }
11984
120- if err := z.webView.InjectScript(js); err != nil {
121- log.Printf("[zoom] failed to show toast: %v", err)
122- } else {
123- log.Printf("[zoom] Toast script injected successfully")
85+ if err := z.webView.DispatchCustomEvent("dumber:toast:zoom", map[string]any{"level": level}); err != nil {
86+ log.Printf("[zoom] failed to dispatch zoom toast: %v", err)
12487 }
12588 }
12689
@@ -2,7 +2,6 @@ package control
2 2
3 import (3 import (
4 "context"4 "context"
5- "fmt"
6 "log"5 "log"
7 6
8 "github.com/bnema/dumber/pkg/webkit"7 "github.com/bnema/dumber/pkg/webkit"
@@ -78,49 +77,13 @@ func (z *ZoomController) handleZoomChange(level float64) {
78 func (z *ZoomController) showZoomToast(level float64) {77 func (z *ZoomController) showZoomToast(level float64) {
79 log.Printf("[zoom] Attempting to show zoom toast for level %.2f", level)78 log.Printf("[zoom] Attempting to show zoom toast for level %.2f", level)
80 79
81- percentage := int(level * 100)80+ if z.webView == nil {
82-81+ log.Printf("[zoom] webview unavailable for zoom toast")
83- // Enhanced approach that ensures toast always appears82+ return
84- js := fmt.Sprintf(`(function() {83+ }
85- try {
86- console.log('[dumber] Attempting to show zoom toast...');
87- const zoomLevel = %f;
88- const percentage = %d;
89- let toastShown = false;
90-
91- // Prefer unified page-world API
92- if (window.__dumber && window.__dumber.toast && typeof window.__dumber.toast.zoom === 'function') {
93- console.log('[dumber] Using unified toast.zoom');
94- window.__dumber.toast.zoom(zoomLevel);
95- toastShown = true;
96- } else if (typeof window.__dumber_showZoomToast === 'function') {
97- console.log('[dumber] Using legacy zoom toast');
98- window.__dumber_showZoomToast(zoomLevel);
99- toastShown = true;
100- } else if (window.__dumber && window.__dumber.toast && typeof window.__dumber.toast.show === 'function') {
101- console.log('[dumber] Using unified toast.show');
102- window.__dumber.toast.show('Zoom: ' + percentage + '%%', 1500, 'info');
103- toastShown = true;
104- } else if (typeof window.__dumber_showToast === 'function') {
105- console.log('[dumber] Using legacy toast.show');
106- window.__dumber_showToast('Zoom: ' + percentage + '%%', 1500, 'info');
107- toastShown = true;
108- }
109-
110-
111- if (!toastShown) {
112- console.warn('[dumber] Failed to show zoom toast - no DOM body available');
113- }
114-
115- } catch(e) {
116- console.error('[dumber] Zoom toast error:', e);
117- }
118- })();`, level, percentage)
119 84
120- if err := z.webView.InjectScript(js); err != nil {85+ if err := z.webView.DispatchCustomEvent("dumber:toast:zoom", map[string]any{"level": level}); err != nil {
121- log.Printf("[zoom] failed to show toast: %v", err)86+ log.Printf("[zoom] failed to dispatch zoom toast: %v", err)
122- } else {
123- log.Printf("[zoom] Toast script injected successfully")
124 }87 }
125 }88 }
126 89
M internal/app/messaging/handler.go +35 -6
@@ -16,22 +16,31 @@ import (
1616 )
1717
1818 // Handler processes script messages from the WebView
19+type WorkspaceObserver interface {
20+ OnWorkspaceMessage(source *webkit.WebView, msg Message)
21+}
22+
1923 type Handler struct {
2024 parserService *services.ParserService
2125 browserService *services.BrowserService
2226 webView *webkit.WebView
2327 navigationController *control.NavigationController
2428 lastTheme string
29+ workspaceObserver WorkspaceObserver
2530 }
2631
2732 // Message represents a script message from the WebView
2833 type Message struct {
29- Type string `json:"type"`
30- URL string `json:"url"`
31- Q string `json:"q"`
32- Limit int `json:"limit"`
33- Offset int `json:"offset"`
34- Value string `json:"value"`
34+ Type string `json:"type"`
35+ URL string `json:"url"`
36+ Q string `json:"q"`
37+ Limit int `json:"limit"`
38+ Offset int `json:"offset"`
39+ Value string `json:"value"`
40+ Event string `json:"event"`
41+ Action string `json:"action"`
42+ Direction string `json:"direction"`
43+ PaneID string `json:"paneId"`
3544 // History operations
3645 HistoryID string `json:"historyId"`
3746 // Request tracking
@@ -54,6 +63,11 @@ func (h *Handler) SetNavigationController(controller *control.NavigationControll
5463 h.navigationController = controller
5564 }
5665
66+// SetWorkspaceObserver registers a workspace event observer.
67+func (h *Handler) SetWorkspaceObserver(observer WorkspaceObserver) {
68+ h.workspaceObserver = observer
69+}
70+
5771 // Handle processes incoming script messages
5872 func (h *Handler) Handle(payload string) {
5973 log.Printf("[DEBUG] Received message: %s", payload)
@@ -82,6 +96,8 @@ func (h *Handler) Handle(payload string) {
8296 h.handleHistorySearch(msg)
8397 case "history_delete":
8498 h.handleHistoryDelete(msg)
99+ case "workspace":
100+ h.handleWorkspace(msg)
85101 }
86102 }
87103
@@ -132,6 +148,19 @@ func (h *Handler) legacyNavigate(msg Message) {
132148
133149 // (legacy query handler removed; omnibox suggestions now fetched via dumb://api/omnibox/suggestions)
134150 // handleQuery computes omnibox suggestions natively and returns them to the GUI without fetch
151+func (h *Handler) handleWorkspace(msg Message) {
152+ if h.workspaceObserver == nil {
153+ log.Printf("[workspace] Received workspace event %q but no observer registered", msg.Event)
154+ return
155+ }
156+ if h.webView == nil {
157+ log.Printf("[workspace] Ignoring workspace event %q: webview not attached", msg.Event)
158+ return
159+ }
160+ log.Printf("[workspace] Forwarding workspace event: event=%s direction=%s action=%s", msg.Event, msg.Direction, msg.Action)
161+ h.workspaceObserver.OnWorkspaceMessage(h.webView, msg)
162+}
163+
135164 func (h *Handler) handleQuery(msg Message) {
136165 log.Printf("[DEBUG] handleQuery called: q='%s', limit=%d", msg.Q, msg.Limit)
137166 if h.webView == nil {
@@ -16,22 +16,31 @@ import (
16 )16 )
17 17
18 // Handler processes script messages from the WebView18 // Handler processes script messages from the WebView
19+type WorkspaceObserver interface {
20+ OnWorkspaceMessage(source *webkit.WebView, msg Message)
21+}
22+
19 type Handler struct {23 type Handler struct {
20 parserService *services.ParserService24 parserService *services.ParserService
21 browserService *services.BrowserService25 browserService *services.BrowserService
22 webView *webkit.WebView26 webView *webkit.WebView
23 navigationController *control.NavigationController27 navigationController *control.NavigationController
24 lastTheme string28 lastTheme string
29+ workspaceObserver WorkspaceObserver
25 }30 }
26 31
27 // Message represents a script message from the WebView32 // Message represents a script message from the WebView
28 type Message struct {33 type Message struct {
29- Type string `json:"type"`34+ Type string `json:"type"`
30- URL string `json:"url"`35+ URL string `json:"url"`
31- Q string `json:"q"`36+ Q string `json:"q"`
32- Limit int `json:"limit"`37+ Limit int `json:"limit"`
33- Offset int `json:"offset"`38+ Offset int `json:"offset"`
34- Value string `json:"value"`39+ Value string `json:"value"`
40+ Event string `json:"event"`
41+ Action string `json:"action"`
42+ Direction string `json:"direction"`
43+ PaneID string `json:"paneId"`
35 // History operations44 // History operations
36 HistoryID string `json:"historyId"`45 HistoryID string `json:"historyId"`
37 // Request tracking46 // Request tracking
@@ -54,6 +63,11 @@ func (h *Handler) SetNavigationController(controller *control.NavigationControll
54 h.navigationController = controller63 h.navigationController = controller
55 }64 }
56 65
66+// SetWorkspaceObserver registers a workspace event observer.
67+func (h *Handler) SetWorkspaceObserver(observer WorkspaceObserver) {
68+ h.workspaceObserver = observer
69+}
70+
57 // Handle processes incoming script messages71 // Handle processes incoming script messages
58 func (h *Handler) Handle(payload string) {72 func (h *Handler) Handle(payload string) {
59 log.Printf("[DEBUG] Received message: %s", payload)73 log.Printf("[DEBUG] Received message: %s", payload)
@@ -82,6 +96,8 @@ func (h *Handler) Handle(payload string) {
82 h.handleHistorySearch(msg)96 h.handleHistorySearch(msg)
83 case "history_delete":97 case "history_delete":
84 h.handleHistoryDelete(msg)98 h.handleHistoryDelete(msg)
99+ case "workspace":
100+ h.handleWorkspace(msg)
85 }101 }
86 }102 }
87 103
@@ -132,6 +148,19 @@ func (h *Handler) legacyNavigate(msg Message) {
132 148
133 // (legacy query handler removed; omnibox suggestions now fetched via dumb://api/omnibox/suggestions)149 // (legacy query handler removed; omnibox suggestions now fetched via dumb://api/omnibox/suggestions)
134 // handleQuery computes omnibox suggestions natively and returns them to the GUI without fetch150 // handleQuery computes omnibox suggestions natively and returns them to the GUI without fetch
151+func (h *Handler) handleWorkspace(msg Message) {
152+ if h.workspaceObserver == nil {
153+ log.Printf("[workspace] Received workspace event %q but no observer registered", msg.Event)
154+ return
155+ }
156+ if h.webView == nil {
157+ log.Printf("[workspace] Ignoring workspace event %q: webview not attached", msg.Event)
158+ return
159+ }
160+ log.Printf("[workspace] Forwarding workspace event: event=%s direction=%s action=%s", msg.Event, msg.Direction, msg.Action)
161+ h.workspaceObserver.OnWorkspaceMessage(h.webView, msg)
162+}
163+
135 func (h *Handler) handleQuery(msg Message) {164 func (h *Handler) handleQuery(msg Message) {
136 log.Printf("[DEBUG] handleQuery called: q='%s', limit=%d", msg.Q, msg.Limit)165 log.Printf("[DEBUG] handleQuery called: q='%s', limit=%d", msg.Q, msg.Limit)
137 if h.webView == nil {166 if h.webView == nil {
M internal/filtering/manager.go +2 -2
@@ -276,7 +276,7 @@ func (fm *FilterManager) applyFilters(filters *CompiledFilters) {
276276 antiBreakageRules := []converter.WebKitRule{
277277 {
278278 Trigger: converter.Trigger{
279- URLFilter: "googlesyndication.com",
279+ URLFilter: "googlesyndication.com",
280280 ResourceType: []string{converter.ResourceTypeScript},
281281 },
282282 Action: converter.Action{
@@ -293,7 +293,7 @@ func (fm *FilterManager) applyFilters(filters *CompiledFilters) {
293293 },
294294 {
295295 Trigger: converter.Trigger{
296- URLFilter: "googletagservices.com/tag/js/gpt.js",
296+ URLFilter: "googletagservices.com/tag/js/gpt.js",
297297 ResourceType: []string{converter.ResourceTypeScript},
298298 },
299299 Action: converter.Action{
@@ -276,7 +276,7 @@ func (fm *FilterManager) applyFilters(filters *CompiledFilters) {
276 antiBreakageRules := []converter.WebKitRule{276 antiBreakageRules := []converter.WebKitRule{
277 {277 {
278 Trigger: converter.Trigger{278 Trigger: converter.Trigger{
279- URLFilter: "googlesyndication.com",279+ URLFilter: "googlesyndication.com",
280 ResourceType: []string{converter.ResourceTypeScript},280 ResourceType: []string{converter.ResourceTypeScript},
281 },281 },
282 Action: converter.Action{282 Action: converter.Action{
@@ -293,7 +293,7 @@ func (fm *FilterManager) applyFilters(filters *CompiledFilters) {
293 },293 },
294 {294 {
295 Trigger: converter.Trigger{295 Trigger: converter.Trigger{
296- URLFilter: "googletagservices.com/tag/js/gpt.js",296+ URLFilter: "googletagservices.com/tag/js/gpt.js",
297 ResourceType: []string{converter.ResourceTypeScript},297 ResourceType: []string{converter.ResourceTypeScript},
298 },298 },
299 Action: converter.Action{299 Action: converter.Action{
M pkg/webkit/toast_svelte_cgo.go +1 -1
@@ -70,4 +70,4 @@ window.__dumber_setTheme = (theme) => {
7070 initializeToast();
7171 }
7272 })();`, themeIntegration, guiBundle)
73-}⏎̸
73+}
@@ -70,4 +70,4 @@ window.__dumber_setTheme = (theme) => {
70 initializeToast();70 initializeToast();
71 }71 }
72 })();`, themeIntegration, guiBundle)72 })();`, themeIntegration, guiBundle)
73-}⏎̸73+}

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