feat(app): integrate native omnibox with navigation and history recording
- commit
- e4c93f7cf7132a5cba9ff6f57d20306d836d2ded
- author
- brice · · 2025-12-14 20:50 UTC
- committer
- brice · · 2025-12-14 18:50 UTC
- parent
- 09cb23d
- signature
- unsigned
9 files · +130 -91
M internal/ui/app.go +81 -9
| @@ -45,6 +45,9 @@ type App struct { | |||
| 45 | 45 | // Input handling | |
| 46 | 46 | keyboardHandler *input.KeyboardHandler | |
| 47 | 47 | ||
| 48 | + | // Native omnibox | |
| 49 | + | omnibox *component.Omnibox | |
| 50 | + | ||
| 48 | 51 | // Web content | |
| 49 | 52 | pool *webkit.WebViewPool | |
| 50 | 53 | injector *webkit.ContentInjector | |
| @@ -99,7 +102,8 @@ func (a *App) Run(ctx context.Context, args []string) int { | |||
| 99 | 102 | log := logging.FromContext(ctx) | |
| 100 | 103 | log.Debug().Msg("creating GTK application") | |
| 101 | 104 | ||
| 102 | - | a.gtkApp = gtk.NewApplication(AppID, gio.GApplicationFlagsNoneValue) | |
| 105 | + | // TODO: Use AppID once puregotk GC bug is fixed (nullable-string-gc-memory-corruption) | |
| 106 | + | a.gtkApp = gtk.NewApplication(nil, gio.GApplicationFlagsNoneValue) | |
| 103 | 107 | if a.gtkApp == nil { | |
| 104 | 108 | log.Error().Msg("failed to create GTK application") | |
| 105 | 109 | return 1 | |
| @@ -163,6 +167,23 @@ func (a *App) onActivate(ctx context.Context) { | |||
| 163 | 167 | }) | |
| 164 | 168 | a.keyboardHandler.AttachTo(a.mainWindow.Window()) | |
| 165 | 169 | ||
| 170 | + | // Create native omnibox | |
| 171 | + | a.omnibox = component.NewOmnibox(ctx, a.mainWindow.Window(), component.OmniboxConfig{ | |
| 172 | + | HistoryUC: a.deps.HistoryUC, | |
| 173 | + | FavoritesUC: a.deps.FavoritesUC, | |
| 174 | + | Shortcuts: a.deps.Config.SearchShortcuts, | |
| 175 | + | DefaultSearch: a.deps.Config.DefaultSearchEngine, | |
| 176 | + | InitialBehavior: a.deps.Config.Omnibox.InitialBehavior, | |
| 177 | + | }) | |
| 178 | + | if a.omnibox != nil { | |
| 179 | + | a.omnibox.SetOnNavigate(func(url string) { | |
| 180 | + | a.navigateActivePane(ctx, url) | |
| 181 | + | }) | |
| 182 | + | log.Debug().Msg("native omnibox created") | |
| 183 | + | } else { | |
| 184 | + | log.Warn().Msg("failed to create native omnibox") | |
| 185 | + | } | |
| 186 | + | ||
| 166 | 187 | if err := a.registerMessageHandlers(ctx); err != nil { | |
| 167 | 188 | log.Warn().Err(err).Msg("failed to register message handlers") | |
| 168 | 189 | } | |
| @@ -799,21 +820,72 @@ func (a *App) activeWebView(ctx context.Context) *webkit.WebView { | |||
| 799 | 820 | return a.webViews[pane.Pane.ID] | |
| 800 | 821 | } | |
| 801 | 822 | ||
| 802 | - | // openOmnibox toggles the omnibox overlay for the active WebView. | |
| 823 | + | // openOmnibox toggles the native GTK omnibox. | |
| 803 | 824 | func (a *App) openOmnibox(ctx context.Context) error { | |
| 804 | 825 | log := logging.FromContext(ctx) | |
| 805 | 826 | ||
| 806 | - | if a.overlay == nil { | |
| 807 | - | log.Error().Msg("overlay controller not configured") | |
| 808 | - | return fmt.Errorf("overlay controller not configured") | |
| 827 | + | if a.omnibox == nil { | |
| 828 | + | log.Error().Msg("native omnibox not initialized") | |
| 829 | + | return fmt.Errorf("native omnibox not initialized") | |
| 809 | 830 | } | |
| 831 | + | ||
| 832 | + | log.Debug().Msg("toggling native omnibox") | |
| 833 | + | a.omnibox.Toggle(ctx) | |
| 834 | + | return nil | |
| 835 | + | } | |
| 836 | + | ||
| 837 | + | // navigateActivePane navigates the active pane to the given URL. | |
| 838 | + | // Records history for omnibox search to find it immediately. | |
| 839 | + | func (a *App) navigateActivePane(ctx context.Context, url string) { | |
| 840 | + | log := logging.FromContext(ctx) | |
| 841 | + | ||
| 810 | 842 | wv := a.activeWebView(ctx) | |
| 811 | 843 | if wv == nil { | |
| 812 | - | log.Error().Msg("no active webview for omnibox") | |
| 813 | - | return fmt.Errorf("no active webview for omnibox") | |
| 844 | + | log.Warn().Str("url", url).Msg("no active webview for navigation") | |
| 845 | + | return | |
| 846 | + | } | |
| 847 | + | ||
| 848 | + | // Load the URL | |
| 849 | + | if err := wv.LoadURI(ctx, url); err != nil { | |
| 850 | + | log.Error().Err(err).Str("url", url).Msg("failed to navigate") | |
| 851 | + | return | |
| 852 | + | } | |
| 853 | + | ||
| 854 | + | // Record in history asynchronously so it appears in omnibox immediately | |
| 855 | + | if a.deps.HistoryRepo != nil { | |
| 856 | + | go a.recordHistory(ctx, url) | |
| 857 | + | } | |
| 858 | + | ||
| 859 | + | log.Debug().Str("url", url).Msg("navigated active pane") | |
| 860 | + | } | |
| 861 | + | ||
| 862 | + | // recordHistory saves or updates a history entry. | |
| 863 | + | func (a *App) recordHistory(ctx context.Context, url string) { | |
| 864 | + | log := logging.FromContext(ctx) | |
| 865 | + | log.Debug().Str("url", url).Msg("recording history entry") | |
| 866 | + | ||
| 867 | + | existing, err := a.deps.HistoryRepo.FindByURL(ctx, url) | |
| 868 | + | if err != nil { | |
| 869 | + | log.Warn().Err(err).Str("url", url).Msg("failed to check history") | |
| 870 | + | return | |
| 871 | + | } | |
| 872 | + | ||
| 873 | + | if existing != nil { | |
| 874 | + | // Increment visit count | |
| 875 | + | if err := a.deps.HistoryRepo.IncrementVisitCount(ctx, url); err != nil { | |
| 876 | + | log.Warn().Err(err).Str("url", url).Msg("failed to increment visit count") | |
| 877 | + | } else { | |
| 878 | + | log.Debug().Str("url", url).Int64("prev_count", existing.VisitCount).Msg("history visit count incremented") | |
| 879 | + | } | |
| 880 | + | } else { | |
| 881 | + | // Create new entry | |
| 882 | + | entry := entity.NewHistoryEntry(url, "") | |
| 883 | + | if err := a.deps.HistoryRepo.Save(ctx, entry); err != nil { | |
| 884 | + | log.Warn().Err(err).Str("url", url).Msg("failed to save history") | |
| 885 | + | } else { | |
| 886 | + | log.Debug().Str("url", url).Msg("new history entry saved") | |
| 887 | + | } | |
| 814 | 888 | } | |
| 815 | - | log.Debug().Uint64("webview_id", uint64(wv.ID())).Msg("opening omnibox") | |
| 816 | - | return a.overlay.Show(ctx, wv.ID(), "omnibox", "") | |
| 817 | 889 | } | |
| 818 | 890 | ||
| 819 | 891 | // openDevTools opens the WebKit inspector for the active WebView. | |
| @@ -45,6 +45,9 @@ type App struct { | |||||
| 45 | // Input handling | 45 | // Input handling | ||
| 46 | keyboardHandler *input.KeyboardHandler | 46 | keyboardHandler *input.KeyboardHandler | ||
| 47 | 47 | ||||
| 48 | + | // Native omnibox | |||
| 49 | + | omnibox *component.Omnibox | |||
| 50 | + | ||||
| 48 | // Web content | 51 | // Web content | ||
| 49 | pool *webkit.WebViewPool | 52 | pool *webkit.WebViewPool | ||
| 50 | injector *webkit.ContentInjector | 53 | injector *webkit.ContentInjector | ||
| @@ -99,7 +102,8 @@ func (a *App) Run(ctx context.Context, args []string) int { | |||||
| 99 | log := logging.FromContext(ctx) | 102 | log := logging.FromContext(ctx) | ||
| 100 | log.Debug().Msg("creating GTK application") | 103 | log.Debug().Msg("creating GTK application") | ||
| 101 | 104 | ||||
| 102 | - | a.gtkApp = gtk.NewApplication(AppID, gio.GApplicationFlagsNoneValue) | 105 | + | // TODO: Use AppID once puregotk GC bug is fixed (nullable-string-gc-memory-corruption) |
| 106 | + | a.gtkApp = gtk.NewApplication(nil, gio.GApplicationFlagsNoneValue) | |||
| 103 | if a.gtkApp == nil { | 107 | if a.gtkApp == nil { | ||
| 104 | log.Error().Msg("failed to create GTK application") | 108 | log.Error().Msg("failed to create GTK application") | ||
| 105 | return 1 | 109 | return 1 | ||
| @@ -163,6 +167,23 @@ func (a *App) onActivate(ctx context.Context) { | |||||
| 163 | }) | 167 | }) | ||
| 164 | a.keyboardHandler.AttachTo(a.mainWindow.Window()) | 168 | a.keyboardHandler.AttachTo(a.mainWindow.Window()) | ||
| 165 | 169 | ||||
| 170 | + | // Create native omnibox | |||
| 171 | + | a.omnibox = component.NewOmnibox(ctx, a.mainWindow.Window(), component.OmniboxConfig{ | |||
| 172 | + | HistoryUC: a.deps.HistoryUC, | |||
| 173 | + | FavoritesUC: a.deps.FavoritesUC, | |||
| 174 | + | Shortcuts: a.deps.Config.SearchShortcuts, | |||
| 175 | + | DefaultSearch: a.deps.Config.DefaultSearchEngine, | |||
| 176 | + | InitialBehavior: a.deps.Config.Omnibox.InitialBehavior, | |||
| 177 | + | }) | |||
| 178 | + | if a.omnibox != nil { | |||
| 179 | + | a.omnibox.SetOnNavigate(func(url string) { | |||
| 180 | + | a.navigateActivePane(ctx, url) | |||
| 181 | + | }) | |||
| 182 | + | log.Debug().Msg("native omnibox created") | |||
| 183 | + | } else { | |||
| 184 | + | log.Warn().Msg("failed to create native omnibox") | |||
| 185 | + | } | |||
| 186 | + | ||||
| 166 | if err := a.registerMessageHandlers(ctx); err != nil { | 187 | if err := a.registerMessageHandlers(ctx); err != nil { | ||
| 167 | log.Warn().Err(err).Msg("failed to register message handlers") | 188 | log.Warn().Err(err).Msg("failed to register message handlers") | ||
| 168 | } | 189 | } | ||
| @@ -799,21 +820,72 @@ func (a *App) activeWebView(ctx context.Context) *webkit.WebView { | |||||
| 799 | return a.webViews[pane.Pane.ID] | 820 | return a.webViews[pane.Pane.ID] | ||
| 800 | } | 821 | } | ||
| 801 | 822 | ||||
| 802 | - | // openOmnibox toggles the omnibox overlay for the active WebView. | 823 | + | // openOmnibox toggles the native GTK omnibox. |
| 803 | func (a *App) openOmnibox(ctx context.Context) error { | 824 | func (a *App) openOmnibox(ctx context.Context) error { | ||
| 804 | log := logging.FromContext(ctx) | 825 | log := logging.FromContext(ctx) | ||
| 805 | 826 | ||||
| 806 | - | if a.overlay == nil { | 827 | + | if a.omnibox == nil { |
| 807 | - | log.Error().Msg("overlay controller not configured") | 828 | + | log.Error().Msg("native omnibox not initialized") |
| 808 | - | return fmt.Errorf("overlay controller not configured") | 829 | + | return fmt.Errorf("native omnibox not initialized") |
| 809 | } | 830 | } | ||
| 831 | + | ||||
| 832 | + | log.Debug().Msg("toggling native omnibox") | |||
| 833 | + | a.omnibox.Toggle(ctx) | |||
| 834 | + | return nil | |||
| 835 | + | } | |||
| 836 | + | ||||
| 837 | + | // navigateActivePane navigates the active pane to the given URL. | |||
| 838 | + | // Records history for omnibox search to find it immediately. | |||
| 839 | + | func (a *App) navigateActivePane(ctx context.Context, url string) { | |||
| 840 | + | log := logging.FromContext(ctx) | |||
| 841 | + | ||||
| 810 | wv := a.activeWebView(ctx) | 842 | wv := a.activeWebView(ctx) | ||
| 811 | if wv == nil { | 843 | if wv == nil { | ||
| 812 | - | log.Error().Msg("no active webview for omnibox") | 844 | + | log.Warn().Str("url", url).Msg("no active webview for navigation") |
| 813 | - | return fmt.Errorf("no active webview for omnibox") | 845 | + | return |
| 846 | + | } | |||
| 847 | + | ||||
| 848 | + | // Load the URL | |||
| 849 | + | if err := wv.LoadURI(ctx, url); err != nil { | |||
| 850 | + | log.Error().Err(err).Str("url", url).Msg("failed to navigate") | |||
| 851 | + | return | |||
| 852 | + | } | |||
| 853 | + | ||||
| 854 | + | // Record in history asynchronously so it appears in omnibox immediately | |||
| 855 | + | if a.deps.HistoryRepo != nil { | |||
| 856 | + | go a.recordHistory(ctx, url) | |||
| 857 | + | } | |||
| 858 | + | ||||
| 859 | + | log.Debug().Str("url", url).Msg("navigated active pane") | |||
| 860 | + | } | |||
| 861 | + | ||||
| 862 | + | // recordHistory saves or updates a history entry. | |||
| 863 | + | func (a *App) recordHistory(ctx context.Context, url string) { | |||
| 864 | + | log := logging.FromContext(ctx) | |||
| 865 | + | log.Debug().Str("url", url).Msg("recording history entry") | |||
| 866 | + | ||||
| 867 | + | existing, err := a.deps.HistoryRepo.FindByURL(ctx, url) | |||
| 868 | + | if err != nil { | |||
| 869 | + | log.Warn().Err(err).Str("url", url).Msg("failed to check history") | |||
| 870 | + | return | |||
| 871 | + | } | |||
| 872 | + | ||||
| 873 | + | if existing != nil { | |||
| 874 | + | // Increment visit count | |||
| 875 | + | if err := a.deps.HistoryRepo.IncrementVisitCount(ctx, url); err != nil { | |||
| 876 | + | log.Warn().Err(err).Str("url", url).Msg("failed to increment visit count") | |||
| 877 | + | } else { | |||
| 878 | + | log.Debug().Str("url", url).Int64("prev_count", existing.VisitCount).Msg("history visit count incremented") | |||
| 879 | + | } | |||
| 880 | + | } else { | |||
| 881 | + | // Create new entry | |||
| 882 | + | entry := entity.NewHistoryEntry(url, "") | |||
| 883 | + | if err := a.deps.HistoryRepo.Save(ctx, entry); err != nil { | |||
| 884 | + | log.Warn().Err(err).Str("url", url).Msg("failed to save history") | |||
| 885 | + | } else { | |||
| 886 | + | log.Debug().Str("url", url).Msg("new history entry saved") | |||
| 887 | + | } | |||
| 814 | } | 888 | } | ||
| 815 | - | log.Debug().Uint64("webview_id", uint64(wv.ID())).Msg("opening omnibox") | |||
| 816 | - | return a.overlay.Show(ctx, wv.ID(), "omnibox", "") | |||
| 817 | } | 889 | } | ||
| 818 | 890 | ||||
| 819 | // openDevTools opens the WebKit inspector for the active WebView. | 891 | // openDevTools opens the WebKit inspector for the active WebView. | ||
M internal/ui/component/overlay.go +9 -39
| @@ -18,32 +18,23 @@ type overlayState struct { | |||
| 18 | 18 | ||
| 19 | 19 | // OverlayController controls the injected overlay UI (omnibox/find). | |
| 20 | 20 | // It issues JavaScript calls into the WebView to show/hide the overlay. | |
| 21 | - | // Logging is derived from the provided context; no logger fields are stored. | |
| 22 | 21 | type OverlayController struct { | |
| 23 | - | baseCtx context.Context | |
| 24 | - | ||
| 25 | 22 | mu sync.RWMutex | |
| 26 | 23 | states map[webkit.WebViewID]overlayState | |
| 27 | 24 | } | |
| 28 | 25 | ||
| 29 | 26 | // NewOverlayController creates a new overlay controller. | |
| 30 | 27 | func NewOverlayController(ctx context.Context) *OverlayController { | |
| 31 | - | if ctx == nil { | |
| 32 | - | ctx = context.Background() | |
| 33 | - | } | |
| 34 | - | ||
| 28 | + | log := logging.FromContext(ctx) | |
| 29 | + | log.Debug().Msg("overlay controller initialized") | |
| 35 | 30 | return &OverlayController{ | |
| 36 | - | baseCtx: ctx, | |
| 37 | - | states: make(map[webkit.WebViewID]overlayState), | |
| 31 | + | states: make(map[webkit.WebViewID]overlayState), | |
| 38 | 32 | } | |
| 39 | 33 | } | |
| 40 | 34 | ||
| 41 | 35 | // Show opens the overlay in the given mode ("omnibox" default or "find"). | |
| 42 | 36 | // An optional query seeds the overlay input. | |
| 43 | 37 | func (oc *OverlayController) Show(ctx context.Context, webviewID webkit.WebViewID, mode, query string) error { | |
| 44 | - | if ctx == nil { | |
| 45 | - | ctx = oc.baseCtx | |
| 46 | - | } | |
| 47 | 38 | if mode == "" { | |
| 48 | 39 | mode = "omnibox" | |
| 49 | 40 | } | |
| @@ -77,10 +68,6 @@ func (oc *OverlayController) Show(ctx context.Context, webviewID webkit.WebViewI | |||
| 77 | 68 | ||
| 78 | 69 | // Hide closes the overlay if present. | |
| 79 | 70 | func (oc *OverlayController) Hide(ctx context.Context, webviewID webkit.WebViewID) error { | |
| 80 | - | if ctx == nil { | |
| 81 | - | ctx = oc.baseCtx | |
| 82 | - | } | |
| 83 | - | ||
| 84 | 71 | log := logging.FromContext(ctx).With(). | |
| 85 | 72 | Str("component", "overlay-controller"). | |
| 86 | 73 | Uint64("webview_id", uint64(webviewID)). | |
| @@ -100,10 +87,6 @@ func (oc *OverlayController) Hide(ctx context.Context, webviewID webkit.WebViewI | |||
| 100 | 87 | ||
| 101 | 88 | // Toggle toggles the overlay visibility. | |
| 102 | 89 | func (oc *OverlayController) Toggle(ctx context.Context, webviewID webkit.WebViewID) error { | |
| 103 | - | if ctx == nil { | |
| 104 | - | ctx = oc.baseCtx | |
| 105 | - | } | |
| 106 | - | ||
| 107 | 90 | log := logging.FromContext(ctx).With(). | |
| 108 | 91 | Str("component", "overlay-controller"). | |
| 109 | 92 | Uint64("webview_id", uint64(webviewID)). | |
| @@ -125,10 +108,6 @@ func (oc *OverlayController) Toggle(ctx context.Context, webviewID webkit.WebVie | |||
| 125 | 108 | ||
| 126 | 109 | // OpenFind opens the find-in-page overlay with an optional query. | |
| 127 | 110 | func (oc *OverlayController) OpenFind(ctx context.Context, webviewID webkit.WebViewID, query string) error { | |
| 128 | - | if ctx == nil { | |
| 129 | - | ctx = oc.baseCtx | |
| 130 | - | } | |
| 131 | - | ||
| 132 | 111 | log := logging.FromContext(ctx).With(). | |
| 133 | 112 | Str("component", "overlay-controller"). | |
| 134 | 113 | Uint64("webview_id", uint64(webviewID)). | |
| @@ -172,18 +151,12 @@ func (oc *OverlayController) getState(webviewID webkit.WebViewID) overlayState { | |||
| 172 | 151 | return overlayState{} | |
| 173 | 152 | } | |
| 174 | 153 | ||
| 175 | - | // runJavaScript executes script in the main world for the given WebView. | |
| 154 | + | // runJavaScript executes script in the isolated "dumber" world for the given WebView. | |
| 155 | + | // This is fire-and-forget: errors are logged asynchronously by the WebView. | |
| 176 | 156 | func (oc *OverlayController) runJavaScript(ctx context.Context, webviewID webkit.WebViewID, script string) error { | |
| 177 | - | if ctx == nil { | |
| 178 | - | ctx = oc.baseCtx | |
| 179 | - | } | |
| 180 | - | ||
| 181 | - | log := logging.FromContext(ctx).With(). | |
| 182 | - | Str("component", "overlay-controller"). | |
| 183 | - | Uint64("webview_id", uint64(webviewID)). | |
| 184 | - | Logger() | |
| 185 | - | ||
| 157 | + | log := logging.FromContext(ctx) | |
| 186 | 158 | if webviewID == 0 { | |
| 159 | + | log.Debug().Msg("runJavaScript called with webviewID=0") | |
| 187 | 160 | return fmt.Errorf("webview id is required for overlay command") | |
| 188 | 161 | } | |
| 189 | 162 | ||
| @@ -192,10 +165,7 @@ func (oc *OverlayController) runJavaScript(ctx context.Context, webviewID webkit | |||
| 192 | 165 | return fmt.Errorf("webview %d not found", webviewID) | |
| 193 | 166 | } | |
| 194 | 167 | ||
| 195 | - | if err := wv.RunJavaScript(ctx, script, ""); err != nil { | |
| 196 | - | log.Warn().Err(err).Msg("overlay JS execution failed") | |
| 197 | - | return err | |
| 198 | - | } | |
| 199 | - | ||
| 168 | + | // Run in the isolated "dumber" world where the GUI scripts are injected | |
| 169 | + | wv.RunJavaScript(ctx, script, webkit.ScriptWorldName) | |
| 200 | 170 | return nil | |
| 201 | 171 | } | |
| @@ -18,32 +18,23 @@ type overlayState struct { | |||||
| 18 | 18 | ||||
| 19 | // OverlayController controls the injected overlay UI (omnibox/find). | 19 | // OverlayController controls the injected overlay UI (omnibox/find). | ||
| 20 | // It issues JavaScript calls into the WebView to show/hide the overlay. | 20 | // It issues JavaScript calls into the WebView to show/hide the overlay. | ||
| 21 | - | // Logging is derived from the provided context; no logger fields are stored. | |||
| 22 | type OverlayController struct { | 21 | type OverlayController struct { | ||
| 23 | - | baseCtx context.Context | |||
| 24 | - | ||||
| 25 | mu sync.RWMutex | 22 | mu sync.RWMutex | ||
| 26 | states map[webkit.WebViewID]overlayState | 23 | states map[webkit.WebViewID]overlayState | ||
| 27 | } | 24 | } | ||
| 28 | 25 | ||||
| 29 | // NewOverlayController creates a new overlay controller. | 26 | // NewOverlayController creates a new overlay controller. | ||
| 30 | func NewOverlayController(ctx context.Context) *OverlayController { | 27 | func NewOverlayController(ctx context.Context) *OverlayController { | ||
| 31 | - | if ctx == nil { | 28 | + | log := logging.FromContext(ctx) |
| 32 | - | ctx = context.Background() | 29 | + | log.Debug().Msg("overlay controller initialized") |
| 33 | - | } | |||
| 34 | - | ||||
| 35 | return &OverlayController{ | 30 | return &OverlayController{ | ||
| 36 | - | baseCtx: ctx, | 31 | + | states: make(map[webkit.WebViewID]overlayState), |
| 37 | - | states: make(map[webkit.WebViewID]overlayState), | |||
| 38 | } | 32 | } | ||
| 39 | } | 33 | } | ||
| 40 | 34 | ||||
| 41 | // Show opens the overlay in the given mode ("omnibox" default or "find"). | 35 | // Show opens the overlay in the given mode ("omnibox" default or "find"). | ||
| 42 | // An optional query seeds the overlay input. | 36 | // An optional query seeds the overlay input. | ||
| 43 | func (oc *OverlayController) Show(ctx context.Context, webviewID webkit.WebViewID, mode, query string) error { | 37 | func (oc *OverlayController) Show(ctx context.Context, webviewID webkit.WebViewID, mode, query string) error { | ||
| 44 | - | if ctx == nil { | |||
| 45 | - | ctx = oc.baseCtx | |||
| 46 | - | } | |||
| 47 | if mode == "" { | 38 | if mode == "" { | ||
| 48 | mode = "omnibox" | 39 | mode = "omnibox" | ||
| 49 | } | 40 | } | ||
| @@ -77,10 +68,6 @@ func (oc *OverlayController) Show(ctx context.Context, webviewID webkit.WebViewI | |||||
| 77 | 68 | ||||
| 78 | // Hide closes the overlay if present. | 69 | // Hide closes the overlay if present. | ||
| 79 | func (oc *OverlayController) Hide(ctx context.Context, webviewID webkit.WebViewID) error { | 70 | func (oc *OverlayController) Hide(ctx context.Context, webviewID webkit.WebViewID) error { | ||
| 80 | - | if ctx == nil { | |||
| 81 | - | ctx = oc.baseCtx | |||
| 82 | - | } | |||
| 83 | - | ||||
| 84 | log := logging.FromContext(ctx).With(). | 71 | log := logging.FromContext(ctx).With(). | ||
| 85 | Str("component", "overlay-controller"). | 72 | Str("component", "overlay-controller"). | ||
| 86 | Uint64("webview_id", uint64(webviewID)). | 73 | Uint64("webview_id", uint64(webviewID)). | ||
| @@ -100,10 +87,6 @@ func (oc *OverlayController) Hide(ctx context.Context, webviewID webkit.WebViewI | |||||
| 100 | 87 | ||||
| 101 | // Toggle toggles the overlay visibility. | 88 | // Toggle toggles the overlay visibility. | ||
| 102 | func (oc *OverlayController) Toggle(ctx context.Context, webviewID webkit.WebViewID) error { | 89 | func (oc *OverlayController) Toggle(ctx context.Context, webviewID webkit.WebViewID) error { | ||
| 103 | - | if ctx == nil { | |||
| 104 | - | ctx = oc.baseCtx | |||
| 105 | - | } | |||
| 106 | - | ||||
| 107 | log := logging.FromContext(ctx).With(). | 90 | log := logging.FromContext(ctx).With(). | ||
| 108 | Str("component", "overlay-controller"). | 91 | Str("component", "overlay-controller"). | ||
| 109 | Uint64("webview_id", uint64(webviewID)). | 92 | Uint64("webview_id", uint64(webviewID)). | ||
| @@ -125,10 +108,6 @@ func (oc *OverlayController) Toggle(ctx context.Context, webviewID webkit.WebVie | |||||
| 125 | 108 | ||||
| 126 | // OpenFind opens the find-in-page overlay with an optional query. | 109 | // OpenFind opens the find-in-page overlay with an optional query. | ||
| 127 | func (oc *OverlayController) OpenFind(ctx context.Context, webviewID webkit.WebViewID, query string) error { | 110 | func (oc *OverlayController) OpenFind(ctx context.Context, webviewID webkit.WebViewID, query string) error { | ||
| 128 | - | if ctx == nil { | |||
| 129 | - | ctx = oc.baseCtx | |||
| 130 | - | } | |||
| 131 | - | ||||
| 132 | log := logging.FromContext(ctx).With(). | 111 | log := logging.FromContext(ctx).With(). | ||
| 133 | Str("component", "overlay-controller"). | 112 | Str("component", "overlay-controller"). | ||
| 134 | Uint64("webview_id", uint64(webviewID)). | 113 | Uint64("webview_id", uint64(webviewID)). | ||
| @@ -172,18 +151,12 @@ func (oc *OverlayController) getState(webviewID webkit.WebViewID) overlayState { | |||||
| 172 | return overlayState{} | 151 | return overlayState{} | ||
| 173 | } | 152 | } | ||
| 174 | 153 | ||||
| 175 | - | // runJavaScript executes script in the main world for the given WebView. | 154 | + | // runJavaScript executes script in the isolated "dumber" world for the given WebView. |
| 155 | + | // This is fire-and-forget: errors are logged asynchronously by the WebView. | |||
| 176 | func (oc *OverlayController) runJavaScript(ctx context.Context, webviewID webkit.WebViewID, script string) error { | 156 | func (oc *OverlayController) runJavaScript(ctx context.Context, webviewID webkit.WebViewID, script string) error { | ||
| 177 | - | if ctx == nil { | 157 | + | log := logging.FromContext(ctx) |
| 178 | - | ctx = oc.baseCtx | |||
| 179 | - | } | |||
| 180 | - | ||||
| 181 | - | log := logging.FromContext(ctx).With(). | |||
| 182 | - | Str("component", "overlay-controller"). | |||
| 183 | - | Uint64("webview_id", uint64(webviewID)). | |||
| 184 | - | Logger() | |||
| 185 | - | ||||
| 186 | if webviewID == 0 { | 158 | if webviewID == 0 { | ||
| 159 | + | log.Debug().Msg("runJavaScript called with webviewID=0") | |||
| 187 | return fmt.Errorf("webview id is required for overlay command") | 160 | return fmt.Errorf("webview id is required for overlay command") | ||
| 188 | } | 161 | } | ||
| 189 | 162 | ||||
| @@ -192,10 +165,7 @@ func (oc *OverlayController) runJavaScript(ctx context.Context, webviewID webkit | |||||
| 192 | return fmt.Errorf("webview %d not found", webviewID) | 165 | return fmt.Errorf("webview %d not found", webviewID) | ||
| 193 | } | 166 | } | ||
| 194 | 167 | ||||
| 195 | - | if err := wv.RunJavaScript(ctx, script, ""); err != nil { | 168 | + | // Run in the isolated "dumber" world where the GUI scripts are injected |
| 196 | - | log.Warn().Err(err).Msg("overlay JS execution failed") | 169 | + | wv.RunJavaScript(ctx, script, webkit.ScriptWorldName) |
| 197 | - | return err | |||
| 198 | - | } | |||
| 199 | - | ||||
| 200 | return nil | 170 | return nil | ||
| 201 | } | 171 | } | ||
M internal/ui/component/tab_button.go +1 -1
| @@ -39,7 +39,7 @@ func NewTabButton(tab *entity.Tab) *TabButton { | |||
| 39 | 39 | ||
| 40 | 40 | // Create the label | |
| 41 | 41 | title := tab.Title() | |
| 42 | - | tb.label = gtk.NewLabel(title) | |
| 42 | + | tb.label = gtk.NewLabel(&title) | |
| 43 | 43 | if tb.label == nil { | |
| 44 | 44 | tb.button.Unref() | |
| 45 | 45 | return nil | |
| @@ -39,7 +39,7 @@ func NewTabButton(tab *entity.Tab) *TabButton { | |||||
| 39 | 39 | ||||
| 40 | // Create the label | 40 | // Create the label | ||
| 41 | title := tab.Title() | 41 | title := tab.Title() | ||
| 42 | - | tb.label = gtk.NewLabel(title) | 42 | + | tb.label = gtk.NewLabel(&title) |
| 43 | if tb.label == nil { | 43 | if tb.label == nil { | ||
| 44 | tb.button.Unref() | 44 | tb.button.Unref() | ||
| 45 | return nil | 45 | return nil | ||
M internal/ui/handler/keyboard_handler.go +1 -8
| @@ -49,14 +49,7 @@ func (h *KeyboardBlockingHandler) Handle(ctx context.Context, webviewID webkit.W | |||
| 49 | 49 | return nil, fmt.Errorf("unknown keyboard_blocking action: %s", action) | |
| 50 | 50 | } | |
| 51 | 51 | ||
| 52 | - | if err := wv.RunJavaScript(ctx, js, ""); err != nil { | |
| 53 | - | log.Warn(). | |
| 54 | - | Err(err). | |
| 55 | - | Uint64("webview_id", uint64(webviewID)). | |
| 56 | - | Str("action", action). | |
| 57 | - | Msg("failed to toggle keyboard blocking") | |
| 58 | - | return nil, err | |
| 59 | - | } | |
| 52 | + | wv.RunJavaScript(ctx, js, "") | |
| 60 | 53 | ||
| 61 | 54 | log.Debug(). | |
| 62 | 55 | Str("action", action). | |
| @@ -49,14 +49,7 @@ func (h *KeyboardBlockingHandler) Handle(ctx context.Context, webviewID webkit.W | |||||
| 49 | return nil, fmt.Errorf("unknown keyboard_blocking action: %s", action) | 49 | return nil, fmt.Errorf("unknown keyboard_blocking action: %s", action) | ||
| 50 | } | 50 | } | ||
| 51 | 51 | ||||
| 52 | - | if err := wv.RunJavaScript(ctx, js, ""); err != nil { | 52 | + | wv.RunJavaScript(ctx, js, "") |
| 53 | - | log.Warn(). | |||
| 54 | - | Err(err). | |||
| 55 | - | Uint64("webview_id", uint64(webviewID)). | |||
| 56 | - | Str("action", action). | |||
| 57 | - | Msg("failed to toggle keyboard blocking") | |||
| 58 | - | return nil, err | |||
| 59 | - | } | |||
| 60 | 53 | ||||
| 61 | log.Debug(). | 54 | log.Debug(). | ||
| 62 | Str("action", action). | 55 | Str("action", action). | ||
M internal/ui/layout/gtk_widgets.go +3 -3
| @@ -465,8 +465,8 @@ func (img *gtkImage) GetParent() Widget { | |||
| 465 | 465 | return >kWidget{inner: parent} | |
| 466 | 466 | } | |
| 467 | 467 | ||
| 468 | - | func (img *gtkImage) SetFromIconName(name string) { img.inner.SetFromIconName(name) } | |
| 469 | - | func (img *gtkImage) SetFromFile(filename string) { img.inner.SetFromFile(filename) } | |
| 468 | + | func (img *gtkImage) SetFromIconName(name string) { img.inner.SetFromIconName(&name) } | |
| 469 | + | func (img *gtkImage) SetFromFile(filename string) { img.inner.SetFromFile(&filename) } | |
| 470 | 470 | func (img *gtkImage) SetPixelSize(size int) { img.inner.SetPixelSize(size) } | |
| 471 | 471 | func (img *gtkImage) Clear() { img.inner.Clear() } | |
| 472 | 472 | ||
| @@ -500,7 +500,7 @@ func (f *GtkWidgetFactory) NewOverlay() OverlayWidget { | |||
| 500 | 500 | } | |
| 501 | 501 | ||
| 502 | 502 | func (f *GtkWidgetFactory) NewLabel(text string) LabelWidget { | |
| 503 | - | label := gtk.NewLabel(text) | |
| 503 | + | label := gtk.NewLabel(&text) | |
| 504 | 504 | return >kLabel{inner: label} | |
| 505 | 505 | } | |
| 506 | 506 | ||
| @@ -465,8 +465,8 @@ func (img *gtkImage) GetParent() Widget { | |||||
| 465 | return >kWidget{inner: parent} | 465 | return >kWidget{inner: parent} | ||
| 466 | } | 466 | } | ||
| 467 | 467 | ||||
| 468 | - | func (img *gtkImage) SetFromIconName(name string) { img.inner.SetFromIconName(name) } | 468 | + | func (img *gtkImage) SetFromIconName(name string) { img.inner.SetFromIconName(&name) } |
| 469 | - | func (img *gtkImage) SetFromFile(filename string) { img.inner.SetFromFile(filename) } | 469 | + | func (img *gtkImage) SetFromFile(filename string) { img.inner.SetFromFile(&filename) } |
| 470 | func (img *gtkImage) SetPixelSize(size int) { img.inner.SetPixelSize(size) } | 470 | func (img *gtkImage) SetPixelSize(size int) { img.inner.SetPixelSize(size) } | ||
| 471 | func (img *gtkImage) Clear() { img.inner.Clear() } | 471 | func (img *gtkImage) Clear() { img.inner.Clear() } | ||
| 472 | 472 | ||||
| @@ -500,7 +500,7 @@ func (f *GtkWidgetFactory) NewOverlay() OverlayWidget { | |||||
| 500 | } | 500 | } | ||
| 501 | 501 | ||||
| 502 | func (f *GtkWidgetFactory) NewLabel(text string) LabelWidget { | 502 | func (f *GtkWidgetFactory) NewLabel(text string) LabelWidget { | ||
| 503 | - | label := gtk.NewLabel(text) | 503 | + | label := gtk.NewLabel(&text) |
| 504 | return >kLabel{inner: label} | 504 | return >kLabel{inner: label} | ||
| 505 | } | 505 | } | ||
| 506 | 506 | ||||
M internal/ui/window/main_window.go +2 -1
| @@ -39,7 +39,8 @@ func New(app *gtk.Application, cfg *config.Config, logger *zerolog.Logger) (*Mai | |||
| 39 | 39 | } | |
| 40 | 40 | ||
| 41 | 41 | // Configure window | |
| 42 | - | mw.window.SetTitle(windowTitle) | |
| 42 | + | title := windowTitle | |
| 43 | + | mw.window.SetTitle(&title) | |
| 43 | 44 | mw.window.SetDefaultSize(defaultWidth, defaultHeight) | |
| 44 | 45 | ||
| 45 | 46 | // Create root container (vertical box) | |
| @@ -39,7 +39,8 @@ func New(app *gtk.Application, cfg *config.Config, logger *zerolog.Logger) (*Mai | |||||
| 39 | } | 39 | } | ||
| 40 | 40 | ||||
| 41 | // Configure window | 41 | // Configure window | ||
| 42 | - | mw.window.SetTitle(windowTitle) | 42 | + | title := windowTitle |
| 43 | + | mw.window.SetTitle(&title) | |||
| 43 | mw.window.SetDefaultSize(defaultWidth, defaultHeight) | 44 | mw.window.SetDefaultSize(defaultWidth, defaultHeight) | ||
| 44 | 45 | ||||
| 45 | // Create root container (vertical box) | 46 | // Create root container (vertical box) | ||
M webui/src/components/omnibox/messaging.ts +18 -24
| @@ -13,14 +13,17 @@ export class OmniboxBridge implements OmniboxMessageBridge { | |||
| 13 | 13 | postMessage(msg: OmniboxMessage): void { | |
| 14 | 14 | console.log("📤 [DEBUG] Posting message to backend via bridge:", msg); | |
| 15 | 15 | try { | |
| 16 | - | // Include webview_id for message routing | |
| 17 | - | const payload = { | |
| 18 | - | ...msg, | |
| 16 | + | // Structure message for Go backend: type + payload + webview_id | |
| 17 | + | // Go expects: { type: string, payload: {...}, webview_id: number } | |
| 18 | + | const { type, ...rest } = msg; | |
| 19 | + | const structuredMessage = { | |
| 20 | + | type, | |
| 21 | + | payload: rest, | |
| 19 | 22 | webview_id: (window as any).__dumber_webview_id ?? 0, | |
| 20 | 23 | }; | |
| 21 | 24 | // Dispatch CustomEvent that main-world bridge will listen to | |
| 22 | 25 | document.dispatchEvent(new CustomEvent("dumber:isolated-message", { | |
| 23 | - | detail: { payload } | |
| 26 | + | detail: { payload: structuredMessage } | |
| 24 | 27 | })); | |
| 25 | 28 | console.log("✅ [DEBUG] Dispatched isolated message event"); | |
| 26 | 29 | } catch (e) { | |
| @@ -86,10 +89,10 @@ export class OmniboxBridge implements OmniboxMessageBridge { | |||
| 86 | 89 | */ | |
| 87 | 90 | async fetchSearchShortcuts(): Promise<void> { | |
| 88 | 91 | return new Promise((resolve, reject) => { | |
| 89 | - | // Set up one-time response handler | |
| 90 | - | const originalCallback = (window as any).__dumber_search_shortcuts; | |
| 91 | - | (window as any).__dumber_search_shortcuts = (data: unknown) => { | |
| 92 | + | // Set up one-time response handler via CustomEvent (isolated world pattern) | |
| 93 | + | const handleShortcuts = ((event: CustomEvent) => { | |
| 92 | 94 | try { | |
| 95 | + | const data = event.detail?.shortcuts ?? event.detail; | |
| 93 | 96 | // Type guard | |
| 94 | 97 | if (typeof data !== "object" || data === null) { | |
| 95 | 98 | reject(new Error("Invalid shortcuts data")); | |
| @@ -106,29 +109,20 @@ export class OmniboxBridge implements OmniboxMessageBridge { | |||
| 106 | 109 | }; | |
| 107 | 110 | } | |
| 108 | 111 | this.setSearchShortcuts(normalized); | |
| 109 | - | ||
| 110 | - | // Restore original callback if it existed | |
| 111 | - | if (originalCallback) { | |
| 112 | - | (window as any).__dumber_search_shortcuts = originalCallback; | |
| 113 | - | } | |
| 114 | - | ||
| 112 | + | document.removeEventListener("dumber:search-shortcuts", handleShortcuts as EventListener); | |
| 115 | 113 | resolve(); | |
| 116 | 114 | } catch (error) { | |
| 117 | 115 | console.error("Failed to process search shortcuts:", error); | |
| 116 | + | document.removeEventListener("dumber:search-shortcuts", handleShortcuts as EventListener); | |
| 118 | 117 | reject(error); | |
| 119 | 118 | } | |
| 120 | - | }; | |
| 119 | + | }) as EventListener; | |
| 121 | 120 | ||
| 122 | - | // Send message to Go backend | |
| 123 | - | const bridge = window.webkit?.messageHandlers?.dumber; | |
| 124 | - | if (bridge && typeof bridge.postMessage === "function") { | |
| 125 | - | bridge.postMessage({ | |
| 126 | - | type: "get_search_shortcuts", | |
| 127 | - | webview_id: (window as any).__dumber_webview_id ?? 0, | |
| 128 | - | }); | |
| 129 | - | } else { | |
| 130 | - | reject(new Error("WebKit message handler not available")); | |
| 131 | - | } | |
| 121 | + | // Listen for response CustomEvent from main world bridge | |
| 122 | + | document.addEventListener("dumber:search-shortcuts", handleShortcuts as EventListener); | |
| 123 | + | ||
| 124 | + | // Send message via CustomEvent bridge (isolated world cannot access webkit.messageHandlers) | |
| 125 | + | this.postMessage({ type: "get_search_shortcuts" }); | |
| 132 | 126 | }); | |
| 133 | 127 | } | |
| 134 | 128 | ||
| @@ -13,14 +13,17 @@ export class OmniboxBridge implements OmniboxMessageBridge { | |||||
| 13 | postMessage(msg: OmniboxMessage): void { | 13 | postMessage(msg: OmniboxMessage): void { | ||
| 14 | console.log("📤 [DEBUG] Posting message to backend via bridge:", msg); | 14 | console.log("📤 [DEBUG] Posting message to backend via bridge:", msg); | ||
| 15 | try { | 15 | try { | ||
| 16 | - | // Include webview_id for message routing | 16 | + | // Structure message for Go backend: type + payload + webview_id |
| 17 | - | const payload = { | 17 | + | // Go expects: { type: string, payload: {...}, webview_id: number } |
| 18 | - | ...msg, | 18 | + | const { type, ...rest } = msg; |
| 19 | + | const structuredMessage = { | |||
| 20 | + | type, | |||
| 21 | + | payload: rest, | |||
| 19 | webview_id: (window as any).__dumber_webview_id ?? 0, | 22 | webview_id: (window as any).__dumber_webview_id ?? 0, | ||
| 20 | }; | 23 | }; | ||
| 21 | // Dispatch CustomEvent that main-world bridge will listen to | 24 | // Dispatch CustomEvent that main-world bridge will listen to | ||
| 22 | document.dispatchEvent(new CustomEvent("dumber:isolated-message", { | 25 | document.dispatchEvent(new CustomEvent("dumber:isolated-message", { | ||
| 23 | - | detail: { payload } | 26 | + | detail: { payload: structuredMessage } |
| 24 | })); | 27 | })); | ||
| 25 | console.log("✅ [DEBUG] Dispatched isolated message event"); | 28 | console.log("✅ [DEBUG] Dispatched isolated message event"); | ||
| 26 | } catch (e) { | 29 | } catch (e) { | ||
| @@ -86,10 +89,10 @@ export class OmniboxBridge implements OmniboxMessageBridge { | |||||
| 86 | */ | 89 | */ | ||
| 87 | async fetchSearchShortcuts(): Promise<void> { | 90 | async fetchSearchShortcuts(): Promise<void> { | ||
| 88 | return new Promise((resolve, reject) => { | 91 | return new Promise((resolve, reject) => { | ||
| 89 | - | // Set up one-time response handler | 92 | + | // Set up one-time response handler via CustomEvent (isolated world pattern) |
| 90 | - | const originalCallback = (window as any).__dumber_search_shortcuts; | 93 | + | const handleShortcuts = ((event: CustomEvent) => { |
| 91 | - | (window as any).__dumber_search_shortcuts = (data: unknown) => { | |||
| 92 | try { | 94 | try { | ||
| 95 | + | const data = event.detail?.shortcuts ?? event.detail; | |||
| 93 | // Type guard | 96 | // Type guard | ||
| 94 | if (typeof data !== "object" || data === null) { | 97 | if (typeof data !== "object" || data === null) { | ||
| 95 | reject(new Error("Invalid shortcuts data")); | 98 | reject(new Error("Invalid shortcuts data")); | ||
| @@ -106,29 +109,20 @@ export class OmniboxBridge implements OmniboxMessageBridge { | |||||
| 106 | }; | 109 | }; | ||
| 107 | } | 110 | } | ||
| 108 | this.setSearchShortcuts(normalized); | 111 | this.setSearchShortcuts(normalized); | ||
| 109 | - | 112 | + | document.removeEventListener("dumber:search-shortcuts", handleShortcuts as EventListener); | |
| 110 | - | // Restore original callback if it existed | |||
| 111 | - | if (originalCallback) { | |||
| 112 | - | (window as any).__dumber_search_shortcuts = originalCallback; | |||
| 113 | - | } | |||
| 114 | - | ||||
| 115 | resolve(); | 113 | resolve(); | ||
| 116 | } catch (error) { | 114 | } catch (error) { | ||
| 117 | console.error("Failed to process search shortcuts:", error); | 115 | console.error("Failed to process search shortcuts:", error); | ||
| 116 | + | document.removeEventListener("dumber:search-shortcuts", handleShortcuts as EventListener); | |||
| 118 | reject(error); | 117 | reject(error); | ||
| 119 | } | 118 | } | ||
| 120 | - | }; | 119 | + | }) as EventListener; |
| 121 | 120 | ||||
| 122 | - | // Send message to Go backend | 121 | + | // Listen for response CustomEvent from main world bridge |
| 123 | - | const bridge = window.webkit?.messageHandlers?.dumber; | 122 | + | document.addEventListener("dumber:search-shortcuts", handleShortcuts as EventListener); |
| 124 | - | if (bridge && typeof bridge.postMessage === "function") { | 123 | + | |
| 125 | - | bridge.postMessage({ | 124 | + | // Send message via CustomEvent bridge (isolated world cannot access webkit.messageHandlers) |
| 126 | - | type: "get_search_shortcuts", | 125 | + | this.postMessage({ type: "get_search_shortcuts" }); |
| 127 | - | webview_id: (window as any).__dumber_webview_id ?? 0, | |||
| 128 | - | }); | |||
| 129 | - | } else { | |||
| 130 | - | reject(new Error("WebKit message handler not available")); | |||
| 131 | - | } | |||
| 132 | }); | 126 | }); | ||
| 133 | } | 127 | } | ||
| 134 | 128 | ||||
M webui/src/injected/main-world.ts +11 -0
| @@ -965,6 +965,16 @@ interface DumberAPI { | |||
| 965 | 965 | }); | |
| 966 | 966 | }; | |
| 967 | 967 | ||
| 968 | + | // Search shortcuts response bridge (backend → isolated world) | |
| 969 | + | window.__dumber_search_shortcuts = (shortcuts: unknown) => { | |
| 970 | + | console.log("[dumber-bridge] Received search shortcuts from backend"); | |
| 971 | + | bridgeDispatch("dumber:search-shortcuts", { shortcuts }); | |
| 972 | + | }; | |
| 973 | + | ||
| 974 | + | window.__dumber_search_shortcuts_error = (error: string) => { | |
| 975 | + | console.error("[dumber-bridge] Search shortcuts error:", error); | |
| 976 | + | }; | |
| 977 | + | ||
| 968 | 978 | // Bridge for isolated world communication | |
| 969 | 979 | // Listen for messages from isolated world and forward to backend via webkit.messageHandlers | |
| 970 | 980 | const setupIsolatedWorldBridge = () => { | |
| @@ -972,6 +982,7 @@ interface DumberAPI { | |||
| 972 | 982 | try { | |
| 973 | 983 | const { payload } = event.detail; | |
| 974 | 984 | if (window.webkit?.messageHandlers?.dumber) { | |
| 985 | + | // Send object directly - Go's jscValue.ToJson() handles conversion | |
| 975 | 986 | window.webkit.messageHandlers.dumber.postMessage(payload); | |
| 976 | 987 | console.log("[dumber-bridge] Forwarded isolated world message:", payload.type); | |
| 977 | 988 | } else { | |
| @@ -965,6 +965,16 @@ interface DumberAPI { | |||||
| 965 | }); | 965 | }); | ||
| 966 | }; | 966 | }; | ||
| 967 | 967 | ||||
| 968 | + | // Search shortcuts response bridge (backend → isolated world) | |||
| 969 | + | window.__dumber_search_shortcuts = (shortcuts: unknown) => { | |||
| 970 | + | console.log("[dumber-bridge] Received search shortcuts from backend"); | |||
| 971 | + | bridgeDispatch("dumber:search-shortcuts", { shortcuts }); | |||
| 972 | + | }; | |||
| 973 | + | ||||
| 974 | + | window.__dumber_search_shortcuts_error = (error: string) => { | |||
| 975 | + | console.error("[dumber-bridge] Search shortcuts error:", error); | |||
| 976 | + | }; | |||
| 977 | + | ||||
| 968 | // Bridge for isolated world communication | 978 | // Bridge for isolated world communication | ||
| 969 | // Listen for messages from isolated world and forward to backend via webkit.messageHandlers | 979 | // Listen for messages from isolated world and forward to backend via webkit.messageHandlers | ||
| 970 | const setupIsolatedWorldBridge = () => { | 980 | const setupIsolatedWorldBridge = () => { | ||
| @@ -972,6 +982,7 @@ interface DumberAPI { | |||||
| 972 | try { | 982 | try { | ||
| 973 | const { payload } = event.detail; | 983 | const { payload } = event.detail; | ||
| 974 | if (window.webkit?.messageHandlers?.dumber) { | 984 | if (window.webkit?.messageHandlers?.dumber) { | ||
| 985 | + | // Send object directly - Go's jscValue.ToJson() handles conversion | |||
| 975 | window.webkit.messageHandlers.dumber.postMessage(payload); | 986 | window.webkit.messageHandlers.dumber.postMessage(payload); | ||
| 976 | console.log("[dumber-bridge] Forwarded isolated world message:", payload.type); | 987 | console.log("[dumber-bridge] Forwarded isolated world message:", payload.type); | ||
| 977 | } else { | 988 | } else { | ||
M webui/src/injected/modules/color-scheme.ts +4 -6
| @@ -80,12 +80,10 @@ class ColorSchemeManager { | |||
| 80 | 80 | ||
| 81 | 81 | private notifyNativeLayer(): void { | |
| 82 | 82 | try { | |
| 83 | - | (window as any).webkit?.messageHandlers?.dumber?.postMessage( | |
| 84 | - | JSON.stringify({ | |
| 85 | - | type: "theme", | |
| 86 | - | value: this.config.colorScheme, | |
| 87 | - | }), | |
| 88 | - | ); | |
| 83 | + | (window as any).webkit?.messageHandlers?.dumber?.postMessage({ | |
| 84 | + | type: "theme", | |
| 85 | + | payload: { value: this.config.colorScheme }, | |
| 86 | + | }); | |
| 89 | 87 | } catch { | |
| 90 | 88 | // Ignore errors - native layer may not be available | |
| 91 | 89 | } | |
| @@ -80,12 +80,10 @@ class ColorSchemeManager { | |||||
| 80 | 80 | ||||
| 81 | private notifyNativeLayer(): void { | 81 | private notifyNativeLayer(): void { | ||
| 82 | try { | 82 | try { | ||
| 83 | - | (window as any).webkit?.messageHandlers?.dumber?.postMessage( | 83 | + | (window as any).webkit?.messageHandlers?.dumber?.postMessage({ |
| 84 | - | JSON.stringify({ | 84 | + | type: "theme", |
| 85 | - | type: "theme", | 85 | + | payload: { value: this.config.colorScheme }, |
| 86 | - | value: this.config.colorScheme, | 86 | + | }); |
| 87 | - | }), | |||
| 88 | - | ); | |||
| 89 | } catch { | 87 | } catch { | ||
| 90 | // Ignore errors - native layer may not be available | 88 | // Ignore errors - native layer may not be available | ||
| 91 | } | 89 | } | ||