detached b106e60

feat(omnibox): move overlay from window to active pane

Refactor omnibox to be positioned within the active workspace pane instead of the entire browser window. Add overlay widget management to PaneView, create WorkspaceView omnibox lifecycle management, and decouple omnibox from main window using OmniboxProvider interface.
commit
b106e602402524c22c3a8ad397ba499552c1225e
author
bnema · · 2025-12-16 22:30 UTC
parent
f367b16
signature
unsigned
13 files · +404 -49
A cmd/dumber/main.go +131 -0
@@ -0,0 +1,131 @@
1+package main
2+
3+import (
4+ "context"
5+ "fmt"
6+ "os"
7+ "path/filepath"
8+ "runtime"
9+
10+ "github.com/bnema/dumber/internal/application/usecase"
11+ "github.com/bnema/dumber/internal/infrastructure/config"
12+ "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite"
13+ "github.com/bnema/dumber/internal/infrastructure/webkit"
14+ "github.com/bnema/dumber/internal/logging"
15+ "github.com/bnema/dumber/internal/ui"
16+ "github.com/bnema/dumber/internal/ui/theme"
17+)
18+
19+// Build-time variables (set via ldflags)
20+var (
21+ version = "dev"
22+ commit = "unknown"
23+ buildDate = "unknown"
24+)
25+
26+func main() {
27+ // GTK requires all GTK calls to be made from the main thread
28+ runtime.LockOSThread()
29+
30+ // Initialize configuration
31+ if err := config.Init(); err != nil {
32+ fmt.Fprintf(os.Stderr, "Failed to initialize configuration: %v\n", err)
33+ os.Exit(1)
34+ }
35+
36+ cfg := config.Get()
37+
38+ // Initialize logger from config
39+ logger := logging.NewFromConfigValues(cfg.Logging.Level, cfg.Logging.Format)
40+ logger.Info().
41+ Str("version", version).
42+ Str("commit", commit).
43+ Str("build_date", buildDate).
44+ Msg("starting dumber")
45+
46+ // Create root context with logger
47+ ctx := logging.WithContext(context.Background(), logger)
48+
49+ // Resolve data/cache directories for WebKit
50+ dataDir, err := config.GetDataDir()
51+ if err != nil {
52+ logger.Fatal().Err(err).Msg("failed to resolve data directory")
53+ }
54+ stateDir, err := config.GetStateDir()
55+ if err != nil {
56+ logger.Fatal().Err(err).Msg("failed to resolve state directory")
57+ }
58+ cacheDir := filepath.Join(stateDir, "webkit-cache")
59+ if mkErr := os.MkdirAll(cacheDir, 0o755); mkErr != nil {
60+ logger.Fatal().Err(mkErr).Str("path", cacheDir).Msg("failed to create cache directory")
61+ }
62+
63+ // Initialize SQLite database
64+ dbPath := filepath.Join(stateDir, "dumber.db")
65+ db, err := sqlite.NewConnection(ctx, dbPath)
66+ if err != nil {
67+ logger.Fatal().Err(err).Str("path", dbPath).Msg("failed to initialize database")
68+ }
69+ defer sqlite.Close(db)
70+
71+ // Create repositories
72+ historyRepo := sqlite.NewHistoryRepository(db)
73+ favoriteRepo := sqlite.NewFavoriteRepository(db)
74+ folderRepo := sqlite.NewFolderRepository(db)
75+ tagRepo := sqlite.NewTagRepository(db)
76+ zoomRepo := sqlite.NewZoomRepository(db)
77+
78+ // Theme management
79+ themeManager := theme.NewManager(ctx, cfg)
80+
81+ // WebKit plumbing
82+ wkCtx, err := webkit.NewWebKitContext(ctx, dataDir, cacheDir)
83+ if err != nil {
84+ logger.Fatal().Err(err).Msg("failed to initialize WebKit context")
85+ }
86+ settings := webkit.NewSettingsManager(ctx, cfg)
87+ injector := webkit.NewContentInjector(themeManager.PrefersDark())
88+ messageRouter := webkit.NewMessageRouter(ctx)
89+ poolCfg := webkit.DefaultPoolConfig()
90+ pool := webkit.NewWebViewPool(ctx, wkCtx, settings, poolCfg, injector, messageRouter)
91+
92+ // Create use cases
93+ idCounter := uint64(0)
94+ idGenerator := func() string {
95+ idCounter++
96+ return fmt.Sprintf("%c%d", 'a'+rune(idCounter%26), idCounter/26)
97+ }
98+ tabsUC := usecase.NewManageTabsUseCase(idGenerator)
99+ panesUC := usecase.NewManagePanesUseCase(idGenerator)
100+ historyUC := usecase.NewSearchHistoryUseCase(historyRepo)
101+ favoritesUC := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
102+ zoomUC := usecase.NewManageZoomUseCase(zoomRepo, cfg.DefaultWebpageZoom)
103+ navigateUC := usecase.NewNavigateUseCase(historyRepo, zoomRepo, cfg.DefaultWebpageZoom)
104+
105+ // Build dependencies
106+ deps := &ui.Dependencies{
107+ Ctx: ctx,
108+ Config: cfg,
109+ Theme: themeManager,
110+ WebContext: wkCtx,
111+ Pool: pool,
112+ Settings: settings,
113+ Injector: injector,
114+ MessageRouter: messageRouter,
115+ // Repositories
116+ HistoryRepo: historyRepo,
117+ FavoriteRepo: favoriteRepo,
118+ ZoomRepo: zoomRepo,
119+ // Use Cases
120+ TabsUC: tabsUC,
121+ PanesUC: panesUC,
122+ HistoryUC: historyUC,
123+ FavoritesUC: favoritesUC,
124+ ZoomUC: zoomUC,
125+ NavigateUC: navigateUC,
126+ }
127+
128+ // Run the application
129+ exitCode := ui.RunWithArgs(ctx, deps)
130+ os.Exit(exitCode)
131+}
@@ -0,0 +1,131 @@
1+package main
2+
3+import (
4+ "context"
5+ "fmt"
6+ "os"
7+ "path/filepath"
8+ "runtime"
9+
10+ "github.com/bnema/dumber/internal/application/usecase"
11+ "github.com/bnema/dumber/internal/infrastructure/config"
12+ "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite"
13+ "github.com/bnema/dumber/internal/infrastructure/webkit"
14+ "github.com/bnema/dumber/internal/logging"
15+ "github.com/bnema/dumber/internal/ui"
16+ "github.com/bnema/dumber/internal/ui/theme"
17+)
18+
19+// Build-time variables (set via ldflags)
20+var (
21+ version = "dev"
22+ commit = "unknown"
23+ buildDate = "unknown"
24+)
25+
26+func main() {
27+ // GTK requires all GTK calls to be made from the main thread
28+ runtime.LockOSThread()
29+
30+ // Initialize configuration
31+ if err := config.Init(); err != nil {
32+ fmt.Fprintf(os.Stderr, "Failed to initialize configuration: %v\n", err)
33+ os.Exit(1)
34+ }
35+
36+ cfg := config.Get()
37+
38+ // Initialize logger from config
39+ logger := logging.NewFromConfigValues(cfg.Logging.Level, cfg.Logging.Format)
40+ logger.Info().
41+ Str("version", version).
42+ Str("commit", commit).
43+ Str("build_date", buildDate).
44+ Msg("starting dumber")
45+
46+ // Create root context with logger
47+ ctx := logging.WithContext(context.Background(), logger)
48+
49+ // Resolve data/cache directories for WebKit
50+ dataDir, err := config.GetDataDir()
51+ if err != nil {
52+ logger.Fatal().Err(err).Msg("failed to resolve data directory")
53+ }
54+ stateDir, err := config.GetStateDir()
55+ if err != nil {
56+ logger.Fatal().Err(err).Msg("failed to resolve state directory")
57+ }
58+ cacheDir := filepath.Join(stateDir, "webkit-cache")
59+ if mkErr := os.MkdirAll(cacheDir, 0o755); mkErr != nil {
60+ logger.Fatal().Err(mkErr).Str("path", cacheDir).Msg("failed to create cache directory")
61+ }
62+
63+ // Initialize SQLite database
64+ dbPath := filepath.Join(stateDir, "dumber.db")
65+ db, err := sqlite.NewConnection(ctx, dbPath)
66+ if err != nil {
67+ logger.Fatal().Err(err).Str("path", dbPath).Msg("failed to initialize database")
68+ }
69+ defer sqlite.Close(db)
70+
71+ // Create repositories
72+ historyRepo := sqlite.NewHistoryRepository(db)
73+ favoriteRepo := sqlite.NewFavoriteRepository(db)
74+ folderRepo := sqlite.NewFolderRepository(db)
75+ tagRepo := sqlite.NewTagRepository(db)
76+ zoomRepo := sqlite.NewZoomRepository(db)
77+
78+ // Theme management
79+ themeManager := theme.NewManager(ctx, cfg)
80+
81+ // WebKit plumbing
82+ wkCtx, err := webkit.NewWebKitContext(ctx, dataDir, cacheDir)
83+ if err != nil {
84+ logger.Fatal().Err(err).Msg("failed to initialize WebKit context")
85+ }
86+ settings := webkit.NewSettingsManager(ctx, cfg)
87+ injector := webkit.NewContentInjector(themeManager.PrefersDark())
88+ messageRouter := webkit.NewMessageRouter(ctx)
89+ poolCfg := webkit.DefaultPoolConfig()
90+ pool := webkit.NewWebViewPool(ctx, wkCtx, settings, poolCfg, injector, messageRouter)
91+
92+ // Create use cases
93+ idCounter := uint64(0)
94+ idGenerator := func() string {
95+ idCounter++
96+ return fmt.Sprintf("%c%d", 'a'+rune(idCounter%26), idCounter/26)
97+ }
98+ tabsUC := usecase.NewManageTabsUseCase(idGenerator)
99+ panesUC := usecase.NewManagePanesUseCase(idGenerator)
100+ historyUC := usecase.NewSearchHistoryUseCase(historyRepo)
101+ favoritesUC := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
102+ zoomUC := usecase.NewManageZoomUseCase(zoomRepo, cfg.DefaultWebpageZoom)
103+ navigateUC := usecase.NewNavigateUseCase(historyRepo, zoomRepo, cfg.DefaultWebpageZoom)
104+
105+ // Build dependencies
106+ deps := &ui.Dependencies{
107+ Ctx: ctx,
108+ Config: cfg,
109+ Theme: themeManager,
110+ WebContext: wkCtx,
111+ Pool: pool,
112+ Settings: settings,
113+ Injector: injector,
114+ MessageRouter: messageRouter,
115+ // Repositories
116+ HistoryRepo: historyRepo,
117+ FavoriteRepo: favoriteRepo,
118+ ZoomRepo: zoomRepo,
119+ // Use Cases
120+ TabsUC: tabsUC,
121+ PanesUC: panesUC,
122+ HistoryUC: historyUC,
123+ FavoritesUC: favoritesUC,
124+ ZoomUC: zoomUC,
125+ NavigateUC: navigateUC,
126+ }
127+
128+ // Run the application
129+ exitCode := ui.RunWithArgs(ctx, deps)
130+ os.Exit(exitCode)
131+}
M internal/ui/app.go +51 -14
@@ -58,8 +58,8 @@ type App struct {
5858 focusMgr *focus.Manager
5959 borderMgr *focus.BorderManager
6060
61- // Native omnibox
62- omnibox *component.Omnibox
61+ // Omnibox configuration (omnibox is created per workspace view)
62+ omniboxCfg component.OmniboxConfig
6363
6464 // Web content (managed by ContentCoordinator)
6565 pool *webkit.WebViewPool
@@ -188,8 +188,8 @@ func (a *App) onActivate(ctx context.Context) {
188188 })
189189 a.keyboardHandler.AttachTo(a.mainWindow.Window())
190190
191- // Create native omnibox
192- a.omnibox = component.NewOmnibox(ctx, a.mainWindow.Window(), component.OmniboxConfig{
191+ // Store omnibox config (omnibox is created per-pane via WorkspaceView)
192+ a.omniboxCfg = component.OmniboxConfig{
193193 HistoryUC: a.deps.HistoryUC,
194194 FavoritesUC: a.deps.FavoritesUC,
195195 FaviconCache: a.faviconCache,
@@ -197,18 +197,12 @@ func (a *App) onActivate(ctx context.Context) {
197197 DefaultSearch: a.deps.Config.DefaultSearchEngine,
198198 InitialBehavior: a.deps.Config.Omnibox.InitialBehavior,
199199 UIScale: a.deps.Config.DefaultUIScale,
200- })
201- if a.omnibox != nil {
202- a.omnibox.SetOnNavigate(func(url string) {
200+ OnNavigate: func(url string) {
203201 a.navCoord.Navigate(ctx, url)
204- })
205- a.navCoord.SetOmnibox(a.omnibox)
206- // Add omnibox widget to the main window overlay
207- a.mainWindow.AddOverlay(a.omnibox.Widget())
208- log.Debug().Msg("native omnibox created and added to overlay")
209- } else {
210- log.Warn().Msg("failed to create native omnibox")
202+ },
211203 }
204+ a.navCoord.SetOmniboxProvider(a)
205+ log.Debug().Msg("omnibox config stored, provider set")
212206
213207 // Create an initial tab using coordinator
214208 if _, err := a.tabCoord.Create(ctx, "https://duckduckgo.com"); err != nil {
@@ -396,6 +390,9 @@ func (a *App) createWorkspaceView(ctx context.Context, tab *entity.Tab) {
396390 wsView.SetModeBorderOverlay(a.borderMgr.Widget())
397391 }
398392
393+ // Set omnibox config for this workspace view
394+ wsView.SetOmniboxConfig(a.omniboxCfg)
395+
399396 // Store in map
400397 a.workspaceViews[tab.ID] = wsView
401398
@@ -430,3 +427,43 @@ func (a *App) activeWorkspaceView() *component.WorkspaceView {
430427 }
431428 return a.workspaceViews[activeTab.ID]
432429 }
430+
431+// ToggleOmnibox implements OmniboxProvider.
432+// Toggles the omnibox visibility in the active workspace view.
433+func (a *App) ToggleOmnibox(ctx context.Context) {
434+ log := logging.FromContext(ctx)
435+
436+ wsView := a.activeWorkspaceView()
437+ if wsView == nil {
438+ log.Warn().Msg("no active workspace view for omnibox toggle")
439+ return
440+ }
441+
442+ if wsView.IsOmniboxVisible() {
443+ wsView.HideOmnibox()
444+ } else {
445+ wsView.ShowOmnibox(ctx, "")
446+ }
447+}
448+
449+// UpdateOmniboxZoom implements OmniboxProvider.
450+// Updates the zoom indicator on the current omnibox if visible.
451+func (a *App) UpdateOmniboxZoom(factor float64) {
452+ wsView := a.activeWorkspaceView()
453+ if wsView == nil {
454+ return
455+ }
456+
457+ omnibox := wsView.GetOmnibox()
458+ if omnibox != nil {
459+ omnibox.UpdateZoomIndicator(factor)
460+ }
461+}
462+
463+// SetOmniboxOnNavigate implements OmniboxProvider.
464+// This is called to set the navigation callback on new omniboxes.
465+// Since omniboxes are created per-pane, we store the config with the callback.
466+func (a *App) SetOmniboxOnNavigate(fn func(url string)) {
467+ // The navigate callback is set when the omnibox is created via WorkspaceView.ShowOmnibox
468+ // The WorkspaceView uses the stored omniboxCfg and sets up navigation via the omnibox's SetOnNavigate
469+}
@@ -58,8 +58,8 @@ type App struct {
58 focusMgr *focus.Manager58 focusMgr *focus.Manager
59 borderMgr *focus.BorderManager59 borderMgr *focus.BorderManager
60 60
61- // Native omnibox61+ // Omnibox configuration (omnibox is created per workspace view)
62- omnibox *component.Omnibox62+ omniboxCfg component.OmniboxConfig
63 63
64 // Web content (managed by ContentCoordinator)64 // Web content (managed by ContentCoordinator)
65 pool *webkit.WebViewPool65 pool *webkit.WebViewPool
@@ -188,8 +188,8 @@ func (a *App) onActivate(ctx context.Context) {
188 })188 })
189 a.keyboardHandler.AttachTo(a.mainWindow.Window())189 a.keyboardHandler.AttachTo(a.mainWindow.Window())
190 190
191- // Create native omnibox191+ // Store omnibox config (omnibox is created per-pane via WorkspaceView)
192- a.omnibox = component.NewOmnibox(ctx, a.mainWindow.Window(), component.OmniboxConfig{192+ a.omniboxCfg = component.OmniboxConfig{
193 HistoryUC: a.deps.HistoryUC,193 HistoryUC: a.deps.HistoryUC,
194 FavoritesUC: a.deps.FavoritesUC,194 FavoritesUC: a.deps.FavoritesUC,
195 FaviconCache: a.faviconCache,195 FaviconCache: a.faviconCache,
@@ -197,18 +197,12 @@ func (a *App) onActivate(ctx context.Context) {
197 DefaultSearch: a.deps.Config.DefaultSearchEngine,197 DefaultSearch: a.deps.Config.DefaultSearchEngine,
198 InitialBehavior: a.deps.Config.Omnibox.InitialBehavior,198 InitialBehavior: a.deps.Config.Omnibox.InitialBehavior,
199 UIScale: a.deps.Config.DefaultUIScale,199 UIScale: a.deps.Config.DefaultUIScale,
200- })200+ OnNavigate: func(url string) {
201- if a.omnibox != nil {
202- a.omnibox.SetOnNavigate(func(url string) {
203 a.navCoord.Navigate(ctx, url)201 a.navCoord.Navigate(ctx, url)
204- })202+ },
205- a.navCoord.SetOmnibox(a.omnibox)
206- // Add omnibox widget to the main window overlay
207- a.mainWindow.AddOverlay(a.omnibox.Widget())
208- log.Debug().Msg("native omnibox created and added to overlay")
209- } else {
210- log.Warn().Msg("failed to create native omnibox")
211 }203 }
204+ a.navCoord.SetOmniboxProvider(a)
205+ log.Debug().Msg("omnibox config stored, provider set")
212 206
213 // Create an initial tab using coordinator207 // Create an initial tab using coordinator
214 if _, err := a.tabCoord.Create(ctx, "https://duckduckgo.com"); err != nil {208 if _, err := a.tabCoord.Create(ctx, "https://duckduckgo.com"); err != nil {
@@ -396,6 +390,9 @@ func (a *App) createWorkspaceView(ctx context.Context, tab *entity.Tab) {
396 wsView.SetModeBorderOverlay(a.borderMgr.Widget())390 wsView.SetModeBorderOverlay(a.borderMgr.Widget())
397 }391 }
398 392
393+ // Set omnibox config for this workspace view
394+ wsView.SetOmniboxConfig(a.omniboxCfg)
395+
399 // Store in map396 // Store in map
400 a.workspaceViews[tab.ID] = wsView397 a.workspaceViews[tab.ID] = wsView
401 398
@@ -430,3 +427,43 @@ func (a *App) activeWorkspaceView() *component.WorkspaceView {
430 }427 }
431 return a.workspaceViews[activeTab.ID]428 return a.workspaceViews[activeTab.ID]
432 }429 }
430+
431+// ToggleOmnibox implements OmniboxProvider.
432+// Toggles the omnibox visibility in the active workspace view.
433+func (a *App) ToggleOmnibox(ctx context.Context) {
434+ log := logging.FromContext(ctx)
435+
436+ wsView := a.activeWorkspaceView()
437+ if wsView == nil {
438+ log.Warn().Msg("no active workspace view for omnibox toggle")
439+ return
440+ }
441+
442+ if wsView.IsOmniboxVisible() {
443+ wsView.HideOmnibox()
444+ } else {
445+ wsView.ShowOmnibox(ctx, "")
446+ }
447+}
448+
449+// UpdateOmniboxZoom implements OmniboxProvider.
450+// Updates the zoom indicator on the current omnibox if visible.
451+func (a *App) UpdateOmniboxZoom(factor float64) {
452+ wsView := a.activeWorkspaceView()
453+ if wsView == nil {
454+ return
455+ }
456+
457+ omnibox := wsView.GetOmnibox()
458+ if omnibox != nil {
459+ omnibox.UpdateZoomIndicator(factor)
460+ }
461+}
462+
463+// SetOmniboxOnNavigate implements OmniboxProvider.
464+// This is called to set the navigation callback on new omniboxes.
465+// Since omniboxes are created per-pane, we store the config with the callback.
466+func (a *App) SetOmniboxOnNavigate(fn func(url string)) {
467+ // The navigate callback is set when the omnibox is created via WorkspaceView.ShowOmnibox
468+ // The WorkspaceView uses the stored omniboxCfg and sets up navigation via the omnibox's SetOnNavigate
469+}
M internal/ui/cache/favicon_cache.go +3 -3
@@ -49,9 +49,9 @@ type FaviconCache struct {
4949 cache map[string]*gdk.Texture // key: domain (e.g., "github.com")
5050 faviconDB *webkit.FaviconDatabase
5151 client *http.Client
52- diskCacheDir string // path to favicon cache directory
53- writeChan chan diskWrite // channel for async writes
54- closeOnce sync.Once // ensures Close is called only once
52+ diskCacheDir string // path to favicon cache directory
53+ writeChan chan diskWrite // channel for async writes
54+ closeOnce sync.Once // ensures Close is called only once
5555 }
5656
5757 // NewFaviconCache creates a new FaviconCache with the given FaviconDatabase.
@@ -49,9 +49,9 @@ type FaviconCache struct {
49 cache map[string]*gdk.Texture // key: domain (e.g., "github.com")49 cache map[string]*gdk.Texture // key: domain (e.g., "github.com")
50 faviconDB *webkit.FaviconDatabase50 faviconDB *webkit.FaviconDatabase
51 client *http.Client51 client *http.Client
52- diskCacheDir string // path to favicon cache directory52+ diskCacheDir string // path to favicon cache directory
53- writeChan chan diskWrite // channel for async writes53+ writeChan chan diskWrite // channel for async writes
54- closeOnce sync.Once // ensures Close is called only once54+ closeOnce sync.Once // ensures Close is called only once
55 }55 }
56 56
57 // NewFaviconCache creates a new FaviconCache with the given FaviconDatabase.57 // NewFaviconCache creates a new FaviconCache with the given FaviconDatabase.
M internal/ui/component/omnibox.go +45 -18
@@ -12,6 +12,7 @@ import (
1212 "github.com/bnema/dumber/internal/infrastructure/config"
1313 "github.com/bnema/dumber/internal/logging"
1414 "github.com/bnema/dumber/internal/ui/cache"
15+ "github.com/bnema/dumber/internal/ui/layout"
1516 "github.com/jwijenbergh/puregotk/v4/gdk"
1617 "github.com/jwijenbergh/puregotk/v4/glib"
1718 "github.com/jwijenbergh/puregotk/v4/gtk"
@@ -51,8 +52,8 @@ type Favorite struct {
5152 // Omnibox is the native GTK4 address bar / command palette.
5253 type Omnibox struct {
5354 // GTK widgets
54- outerBox *gtk.Box // Outer container for positioning
55- mainBox *gtk.Box // Main content box
55+ outerBox *gtk.Box // Outer container for positioning
56+ mainBox *gtk.Box // Main content box
5657 headerBox *gtk.Box
5758 historyBtn *gtk.Button
5859 favoritesBtn *gtk.Button
@@ -61,8 +62,8 @@ type Omnibox struct {
6162 scrolledWin *gtk.ScrolledWindow
6263 listBox *gtk.ListBox
6364
64- // Parent window reference for sizing
65- parentWindow *gtk.ApplicationWindow
65+ // Parent overlay reference for sizing (set via SetParentOverlay)
66+ parentOverlay layout.OverlayWidget
6667
6768 // State
6869 mu sync.RWMutex
@@ -110,11 +111,13 @@ type OmniboxConfig struct {
110111 Shortcuts map[string]config.SearchShortcut
111112 DefaultSearch string
112113 InitialBehavior string
113- UIScale float64 // UI scale for favicon sizing
114+ UIScale float64 // UI scale for favicon sizing
115+ OnNavigate func(url string) // Callback when user navigates via omnibox
114116 }
115117
116118 // NewOmnibox creates a new native GTK4 omnibox widget.
117-func NewOmnibox(ctx context.Context, parent *gtk.ApplicationWindow, cfg OmniboxConfig) *Omnibox {
119+// Call SetParentOverlay() before Show() to set the parent for sizing.
120+func NewOmnibox(ctx context.Context, cfg OmniboxConfig) *Omnibox {
118121 log := logging.FromContext(ctx)
119122
120123 uiScale := cfg.UIScale
@@ -123,7 +126,6 @@ func NewOmnibox(ctx context.Context, parent *gtk.ApplicationWindow, cfg OmniboxC
123126 }
124127
125128 o := &Omnibox{
126- parentWindow: parent,
127129 viewMode: ViewModeHistory,
128130 selectedIndex: -1,
129131 historyUC: cfg.HistoryUC,
@@ -144,10 +146,32 @@ func NewOmnibox(ctx context.Context, parent *gtk.ApplicationWindow, cfg OmniboxC
144146 o.setupKeyboardHandling()
145147 o.setupEntryChanged()
146148
149+ // Set navigation callback if provided
150+ if cfg.OnNavigate != nil {
151+ o.onNavigate = cfg.OnNavigate
152+ }
153+
147154 log.Debug().Msg("omnibox created")
148155 return o
149156 }
150157
158+// SetParentOverlay sets the overlay widget used for sizing calculations.
159+// Must be called before Show().
160+func (o *Omnibox) SetParentOverlay(overlay layout.OverlayWidget) {
161+ o.mu.Lock()
162+ defer o.mu.Unlock()
163+ o.parentOverlay = overlay
164+}
165+
166+// WidgetAsLayout returns the omnibox's outer widget as a layout.Widget.
167+// This is useful for adding the omnibox to a PaneView overlay.
168+func (o *Omnibox) WidgetAsLayout(factory layout.WidgetFactory) layout.Widget {
169+ if o.outerBox == nil {
170+ return nil
171+ }
172+ return factory.WrapWidget(&o.outerBox.Widget)
173+}
174+
151175 // updateSize sets the omnibox size based on parent window dimensions.
152176 // Deprecated: use resizeAndCenter with row count instead.
153177 func (o *Omnibox) updateSize() {
@@ -157,20 +181,20 @@ func (o *Omnibox) updateSize() {
157181 // resizeAndCenter adjusts the omnibox size based on content and centers it.
158182 // rowCount is the number of result rows to display (0 = no content, max 10).
159183 func (o *Omnibox) resizeAndCenter(rowCount int) {
160- if o.parentWindow == nil || o.outerBox == nil || o.mainBox == nil {
184+ if o.parentOverlay == nil || o.outerBox == nil || o.mainBox == nil {
161185 return
162186 }
163187
164188 // Use GetAllocatedWidth/Height for actual rendered size
165- parentWidth := o.parentWindow.GetAllocatedWidth()
166- parentHeight := o.parentWindow.GetAllocatedHeight()
189+ parentWidth := o.parentOverlay.GetAllocatedWidth()
190+ parentHeight := o.parentOverlay.GetAllocatedHeight()
167191
168192 if parentWidth <= 0 || parentHeight <= 0 {
169193 log := logging.FromContext(o.ctx)
170194 log.Error().
171195 Int("parent_width", parentWidth).
172196 Int("parent_height", parentHeight).
173- Msg("omnibox: invalid parent window dimensions")
197+ Msg("omnibox: invalid parent overlay dimensions")
174198 return
175199 }
176200
@@ -251,9 +275,9 @@ func (o *Omnibox) createWidgets() error {
251275 return errNilWidget("outerBox")
252276 }
253277 o.outerBox.AddCssClass("omnibox-outer")
254- o.outerBox.SetHalign(gtk.AlignCenterValue) // Center horizontally
255- o.outerBox.SetValign(gtk.AlignStartValue) // Align to top
256- o.outerBox.SetVisible(false) // Hidden by default
278+ o.outerBox.SetHalign(gtk.AlignCenterValue) // Center horizontally
279+ o.outerBox.SetValign(gtk.AlignStartValue) // Align to top
280+ o.outerBox.SetVisible(false) // Hidden by default
257281
258282 // Main content box
259283 o.mainBox = gtk.NewBox(gtk.OrientationVerticalValue, 0)
@@ -679,9 +703,12 @@ func (o *Omnibox) rebuildList() {
679703 // Schedule measurement if not yet cached and we have rows
680704 if !o.measuredHeights.valid && o.listBox.GetRowAtIndex(0) != nil {
681705 var cb glib.SourceFunc = func(uintptr) bool {
682- width := o.parentWindow.GetAllocatedWidth()
706+ if o.parentOverlay == nil {
707+ return false
708+ }
709+ width := o.parentOverlay.GetAllocatedWidth()
683710 if width <= 0 {
684- return false // Window not allocated yet, skip
711+ return false // Overlay not allocated yet, skip
685712 }
686713 forWidth := int(float64(width) * omniboxWidthPct)
687714 if o.measureComponentHeights(forWidth) {
@@ -1104,8 +1131,8 @@ func (o *Omnibox) Show(ctx context.Context, query string) {
11041131 }
11051132
11061133 // Set width and vertical positioning (20% from top)
1107- parentWidth := o.parentWindow.GetAllocatedWidth()
1108- parentHeight := o.parentWindow.GetAllocatedHeight()
1134+ parentWidth := o.parentOverlay.GetAllocatedWidth()
1135+ parentHeight := o.parentOverlay.GetAllocatedHeight()
11091136 width := int(float64(parentWidth) * omniboxWidthPct)
11101137 marginTop := int(float64(parentHeight) * 0.20) // 20% from top
11111138
@@ -12,6 +12,7 @@ import (
12 "github.com/bnema/dumber/internal/infrastructure/config"12 "github.com/bnema/dumber/internal/infrastructure/config"
13 "github.com/bnema/dumber/internal/logging"13 "github.com/bnema/dumber/internal/logging"
14 "github.com/bnema/dumber/internal/ui/cache"14 "github.com/bnema/dumber/internal/ui/cache"
15+ "github.com/bnema/dumber/internal/ui/layout"
15 "github.com/jwijenbergh/puregotk/v4/gdk"16 "github.com/jwijenbergh/puregotk/v4/gdk"
16 "github.com/jwijenbergh/puregotk/v4/glib"17 "github.com/jwijenbergh/puregotk/v4/glib"
17 "github.com/jwijenbergh/puregotk/v4/gtk"18 "github.com/jwijenbergh/puregotk/v4/gtk"
@@ -51,8 +52,8 @@ type Favorite struct {
51 // Omnibox is the native GTK4 address bar / command palette.52 // Omnibox is the native GTK4 address bar / command palette.
52 type Omnibox struct {53 type Omnibox struct {
53 // GTK widgets54 // GTK widgets
54- outerBox *gtk.Box // Outer container for positioning55+ outerBox *gtk.Box // Outer container for positioning
55- mainBox *gtk.Box // Main content box56+ mainBox *gtk.Box // Main content box
56 headerBox *gtk.Box57 headerBox *gtk.Box
57 historyBtn *gtk.Button58 historyBtn *gtk.Button
58 favoritesBtn *gtk.Button59 favoritesBtn *gtk.Button
@@ -61,8 +62,8 @@ type Omnibox struct {
61 scrolledWin *gtk.ScrolledWindow62 scrolledWin *gtk.ScrolledWindow
62 listBox *gtk.ListBox63 listBox *gtk.ListBox
63 64
64- // Parent window reference for sizing65+ // Parent overlay reference for sizing (set via SetParentOverlay)
65- parentWindow *gtk.ApplicationWindow66+ parentOverlay layout.OverlayWidget
66 67
67 // State68 // State
68 mu sync.RWMutex69 mu sync.RWMutex
@@ -110,11 +111,13 @@ type OmniboxConfig struct {
110 Shortcuts map[string]config.SearchShortcut111 Shortcuts map[string]config.SearchShortcut
111 DefaultSearch string112 DefaultSearch string
112 InitialBehavior string113 InitialBehavior string
113- UIScale float64 // UI scale for favicon sizing114+ UIScale float64 // UI scale for favicon sizing
115+ OnNavigate func(url string) // Callback when user navigates via omnibox
114 }116 }
115 117
116 // NewOmnibox creates a new native GTK4 omnibox widget.118 // NewOmnibox creates a new native GTK4 omnibox widget.
117-func NewOmnibox(ctx context.Context, parent *gtk.ApplicationWindow, cfg OmniboxConfig) *Omnibox {119+// Call SetParentOverlay() before Show() to set the parent for sizing.
120+func NewOmnibox(ctx context.Context, cfg OmniboxConfig) *Omnibox {
118 log := logging.FromContext(ctx)121 log := logging.FromContext(ctx)
119 122
120 uiScale := cfg.UIScale123 uiScale := cfg.UIScale
@@ -123,7 +126,6 @@ func NewOmnibox(ctx context.Context, parent *gtk.ApplicationWindow, cfg OmniboxC
123 }126 }
124 127
125 o := &Omnibox{128 o := &Omnibox{
126- parentWindow: parent,
127 viewMode: ViewModeHistory,129 viewMode: ViewModeHistory,
128 selectedIndex: -1,130 selectedIndex: -1,
129 historyUC: cfg.HistoryUC,131 historyUC: cfg.HistoryUC,
@@ -144,10 +146,32 @@ func NewOmnibox(ctx context.Context, parent *gtk.ApplicationWindow, cfg OmniboxC
144 o.setupKeyboardHandling()146 o.setupKeyboardHandling()
145 o.setupEntryChanged()147 o.setupEntryChanged()
146 148
149+ // Set navigation callback if provided
150+ if cfg.OnNavigate != nil {
151+ o.onNavigate = cfg.OnNavigate
152+ }
153+
147 log.Debug().Msg("omnibox created")154 log.Debug().Msg("omnibox created")
148 return o155 return o
149 }156 }
150 157
158+// SetParentOverlay sets the overlay widget used for sizing calculations.
159+// Must be called before Show().
160+func (o *Omnibox) SetParentOverlay(overlay layout.OverlayWidget) {
161+ o.mu.Lock()
162+ defer o.mu.Unlock()
163+ o.parentOverlay = overlay
164+}
165+
166+// WidgetAsLayout returns the omnibox's outer widget as a layout.Widget.
167+// This is useful for adding the omnibox to a PaneView overlay.
168+func (o *Omnibox) WidgetAsLayout(factory layout.WidgetFactory) layout.Widget {
169+ if o.outerBox == nil {
170+ return nil
171+ }
172+ return factory.WrapWidget(&o.outerBox.Widget)
173+}
174+
151 // updateSize sets the omnibox size based on parent window dimensions.175 // updateSize sets the omnibox size based on parent window dimensions.
152 // Deprecated: use resizeAndCenter with row count instead.176 // Deprecated: use resizeAndCenter with row count instead.
153 func (o *Omnibox) updateSize() {177 func (o *Omnibox) updateSize() {
@@ -157,20 +181,20 @@ func (o *Omnibox) updateSize() {
157 // resizeAndCenter adjusts the omnibox size based on content and centers it.181 // resizeAndCenter adjusts the omnibox size based on content and centers it.
158 // rowCount is the number of result rows to display (0 = no content, max 10).182 // rowCount is the number of result rows to display (0 = no content, max 10).
159 func (o *Omnibox) resizeAndCenter(rowCount int) {183 func (o *Omnibox) resizeAndCenter(rowCount int) {
160- if o.parentWindow == nil || o.outerBox == nil || o.mainBox == nil {184+ if o.parentOverlay == nil || o.outerBox == nil || o.mainBox == nil {
161 return185 return
162 }186 }
163 187
164 // Use GetAllocatedWidth/Height for actual rendered size188 // Use GetAllocatedWidth/Height for actual rendered size
165- parentWidth := o.parentWindow.GetAllocatedWidth()189+ parentWidth := o.parentOverlay.GetAllocatedWidth()
166- parentHeight := o.parentWindow.GetAllocatedHeight()190+ parentHeight := o.parentOverlay.GetAllocatedHeight()
167 191
168 if parentWidth <= 0 || parentHeight <= 0 {192 if parentWidth <= 0 || parentHeight <= 0 {
169 log := logging.FromContext(o.ctx)193 log := logging.FromContext(o.ctx)
170 log.Error().194 log.Error().
171 Int("parent_width", parentWidth).195 Int("parent_width", parentWidth).
172 Int("parent_height", parentHeight).196 Int("parent_height", parentHeight).
173- Msg("omnibox: invalid parent window dimensions")197+ Msg("omnibox: invalid parent overlay dimensions")
174 return198 return
175 }199 }
176 200
@@ -251,9 +275,9 @@ func (o *Omnibox) createWidgets() error {
251 return errNilWidget("outerBox")275 return errNilWidget("outerBox")
252 }276 }
253 o.outerBox.AddCssClass("omnibox-outer")277 o.outerBox.AddCssClass("omnibox-outer")
254- o.outerBox.SetHalign(gtk.AlignCenterValue) // Center horizontally278+ o.outerBox.SetHalign(gtk.AlignCenterValue) // Center horizontally
255- o.outerBox.SetValign(gtk.AlignStartValue) // Align to top279+ o.outerBox.SetValign(gtk.AlignStartValue) // Align to top
256- o.outerBox.SetVisible(false) // Hidden by default280+ o.outerBox.SetVisible(false) // Hidden by default
257 281
258 // Main content box282 // Main content box
259 o.mainBox = gtk.NewBox(gtk.OrientationVerticalValue, 0)283 o.mainBox = gtk.NewBox(gtk.OrientationVerticalValue, 0)
@@ -679,9 +703,12 @@ func (o *Omnibox) rebuildList() {
679 // Schedule measurement if not yet cached and we have rows703 // Schedule measurement if not yet cached and we have rows
680 if !o.measuredHeights.valid && o.listBox.GetRowAtIndex(0) != nil {704 if !o.measuredHeights.valid && o.listBox.GetRowAtIndex(0) != nil {
681 var cb glib.SourceFunc = func(uintptr) bool {705 var cb glib.SourceFunc = func(uintptr) bool {
682- width := o.parentWindow.GetAllocatedWidth()706+ if o.parentOverlay == nil {
707+ return false
708+ }
709+ width := o.parentOverlay.GetAllocatedWidth()
683 if width <= 0 {710 if width <= 0 {
684- return false // Window not allocated yet, skip711+ return false // Overlay not allocated yet, skip
685 }712 }
686 forWidth := int(float64(width) * omniboxWidthPct)713 forWidth := int(float64(width) * omniboxWidthPct)
687 if o.measureComponentHeights(forWidth) {714 if o.measureComponentHeights(forWidth) {
@@ -1104,8 +1131,8 @@ func (o *Omnibox) Show(ctx context.Context, query string) {
1104 }1131 }
1105 1132
1106 // Set width and vertical positioning (20% from top)1133 // Set width and vertical positioning (20% from top)
1107- parentWidth := o.parentWindow.GetAllocatedWidth()1134+ parentWidth := o.parentOverlay.GetAllocatedWidth()
1108- parentHeight := o.parentWindow.GetAllocatedHeight()1135+ parentHeight := o.parentOverlay.GetAllocatedHeight()
1109 width := int(float64(parentWidth) * omniboxWidthPct)1136 width := int(float64(parentWidth) * omniboxWidthPct)
1110 marginTop := int(float64(parentHeight) * 0.20) // 20% from top1137 marginTop := int(float64(parentHeight) * 0.20) // 20% from top
1111 1138
M internal/ui/component/pane_view.go +18 -0
@@ -210,3 +210,21 @@ func (pv *PaneView) AddCssClass(class string) {
210210 func (pv *PaneView) RemoveCssClass(class string) {
211211 pv.overlay.RemoveCssClass(class)
212212 }
213+
214+// AddOverlayWidget adds a widget as an overlay on this pane.
215+// The widget will appear above the WebView and border.
216+func (pv *PaneView) AddOverlayWidget(widget layout.Widget) {
217+ pv.overlay.AddOverlay(widget)
218+ pv.overlay.SetClipOverlay(widget, false)
219+ pv.overlay.SetMeasureOverlay(widget, false)
220+}
221+
222+// RemoveOverlayWidget removes an overlay widget from this pane.
223+func (pv *PaneView) RemoveOverlayWidget(widget layout.Widget) {
224+ pv.overlay.RemoveOverlay(widget)
225+}
226+
227+// GetContentDimensions returns the pane's allocated width and height.
228+func (pv *PaneView) GetContentDimensions() (width, height int) {
229+ return pv.overlay.GetAllocatedWidth(), pv.overlay.GetAllocatedHeight()
230+}
@@ -210,3 +210,21 @@ func (pv *PaneView) AddCssClass(class string) {
210 func (pv *PaneView) RemoveCssClass(class string) {210 func (pv *PaneView) RemoveCssClass(class string) {
211 pv.overlay.RemoveCssClass(class)211 pv.overlay.RemoveCssClass(class)
212 }212 }
213+
214+// AddOverlayWidget adds a widget as an overlay on this pane.
215+// The widget will appear above the WebView and border.
216+func (pv *PaneView) AddOverlayWidget(widget layout.Widget) {
217+ pv.overlay.AddOverlay(widget)
218+ pv.overlay.SetClipOverlay(widget, false)
219+ pv.overlay.SetMeasureOverlay(widget, false)
220+}
221+
222+// RemoveOverlayWidget removes an overlay widget from this pane.
223+func (pv *PaneView) RemoveOverlayWidget(widget layout.Widget) {
224+ pv.overlay.RemoveOverlay(widget)
225+}
226+
227+// GetContentDimensions returns the pane's allocated width and height.
228+func (pv *PaneView) GetContentDimensions() (width, height int) {
229+ return pv.overlay.GetAllocatedWidth(), pv.overlay.GetAllocatedHeight()
230+}
M internal/ui/component/workspace_view.go +118 -0
@@ -31,6 +31,12 @@ type WorkspaceView struct {
3131 // Mode border overlay slot
3232 modeBorderWidget layout.Widget
3333
34+ // Omnibox lifecycle
35+ omnibox *Omnibox // Current omnibox (nil if not shown)
36+ omniboxWidget layout.Widget // Wrapped omnibox widget for overlay
37+ omniboxPaneID entity.PaneID // Which pane has the omnibox
38+ omniboxCfg OmniboxConfig // Stored config for creating omniboxes
39+
3440 workspace *entity.Workspace
3541 paneViews map[entity.PaneID]*PaneView
3642 activePaneID entity.PaneID
@@ -154,6 +160,11 @@ func (wv *WorkspaceView) SetActivePaneID(paneID entity.PaneID) error {
154160
155161 // setActivePaneIDInternal updates active pane without locking.
156162 func (wv *WorkspaceView) setActivePaneIDInternal(paneID entity.PaneID) error {
163+ // Destroy omnibox if active pane is changing
164+ if wv.activePaneID != paneID && wv.omnibox != nil {
165+ wv.hideOmniboxInternal()
166+ }
167+
157168 // Deactivate current active pane
158169 if wv.activePaneID != "" {
159170 if oldPV, ok := wv.paneViews[wv.activePaneID]; ok {
@@ -401,3 +412,110 @@ func (wv *WorkspaceView) GetStackContainerWidget(paneID entity.PaneID) layout.Wi
401412 func (wv *WorkspaceView) ContainerWidget() layout.Widget {
402413 return wv.container
403414 }
415+
416+// SetOmniboxConfig stores the omnibox configuration for later use.
417+func (wv *WorkspaceView) SetOmniboxConfig(cfg OmniboxConfig) {
418+ wv.mu.Lock()
419+ defer wv.mu.Unlock()
420+ wv.omniboxCfg = cfg
421+}
422+
423+// ShowOmnibox creates and shows the omnibox in the active pane.
424+func (wv *WorkspaceView) ShowOmnibox(ctx context.Context, query string) {
425+ wv.mu.Lock()
426+ defer wv.mu.Unlock()
427+
428+ // If omnibox already exists, just show it
429+ if wv.omnibox != nil {
430+ wv.omnibox.Show(ctx, query)
431+ return
432+ }
433+
434+ // Get the active pane view
435+ pv := wv.paneViews[wv.activePaneID]
436+ if pv == nil {
437+ wv.logger.Warn().Str("paneID", string(wv.activePaneID)).Msg("cannot show omnibox: active pane not found")
438+ return
439+ }
440+
441+ // Create omnibox
442+ omnibox := NewOmnibox(ctx, wv.omniboxCfg)
443+ if omnibox == nil {
444+ wv.logger.Error().Msg("failed to create omnibox")
445+ return
446+ }
447+
448+ // Set parent overlay for sizing
449+ omnibox.SetParentOverlay(pv.Overlay())
450+
451+ // Wrap widget for layout system
452+ omniboxWidget := omnibox.WidgetAsLayout(wv.factory)
453+ if omniboxWidget == nil {
454+ wv.logger.Error().Msg("failed to wrap omnibox widget")
455+ return
456+ }
457+
458+ // Add to pane overlay
459+ pv.AddOverlayWidget(omniboxWidget)
460+
461+ // Store references
462+ wv.omnibox = omnibox
463+ wv.omniboxWidget = omniboxWidget
464+ wv.omniboxPaneID = wv.activePaneID
465+
466+ // Show the omnibox
467+ omnibox.Show(ctx, query)
468+
469+ wv.logger.Debug().Str("paneID", string(wv.activePaneID)).Msg("omnibox shown")
470+}
471+
472+// HideOmnibox hides and destroys the current omnibox.
473+func (wv *WorkspaceView) HideOmnibox() {
474+ wv.mu.Lock()
475+ defer wv.mu.Unlock()
476+
477+ wv.hideOmniboxInternal()
478+}
479+
480+// hideOmniboxInternal destroys the omnibox without locking.
481+func (wv *WorkspaceView) hideOmniboxInternal() {
482+ if wv.omnibox == nil {
483+ return
484+ }
485+
486+ // Hide the omnibox first (use Background context since this is internal cleanup)
487+ wv.omnibox.Hide(context.Background())
488+
489+ // Remove from pane overlay
490+ if pv := wv.paneViews[wv.omniboxPaneID]; pv != nil && wv.omniboxWidget != nil {
491+ pv.RemoveOverlayWidget(wv.omniboxWidget)
492+ }
493+
494+ // Clear references
495+ wv.omnibox = nil
496+ wv.omniboxWidget = nil
497+ wv.omniboxPaneID = ""
498+
499+ wv.logger.Debug().Msg("omnibox hidden and destroyed")
500+}
501+
502+// GetOmnibox returns the current omnibox if visible.
503+func (wv *WorkspaceView) GetOmnibox() *Omnibox {
504+ wv.mu.RLock()
505+ defer wv.mu.RUnlock()
506+ return wv.omnibox
507+}
508+
509+// IsOmniboxVisible returns whether the omnibox is currently visible.
510+func (wv *WorkspaceView) IsOmniboxVisible() bool {
511+ wv.mu.RLock()
512+ defer wv.mu.RUnlock()
513+ return wv.omnibox != nil && wv.omnibox.IsVisible()
514+}
515+
516+// GetActivePaneView returns the PaneView for the current active pane.
517+func (wv *WorkspaceView) GetActivePaneView() *PaneView {
518+ wv.mu.RLock()
519+ defer wv.mu.RUnlock()
520+ return wv.paneViews[wv.activePaneID]
521+}
@@ -31,6 +31,12 @@ type WorkspaceView struct {
31 // Mode border overlay slot31 // Mode border overlay slot
32 modeBorderWidget layout.Widget32 modeBorderWidget layout.Widget
33 33
34+ // Omnibox lifecycle
35+ omnibox *Omnibox // Current omnibox (nil if not shown)
36+ omniboxWidget layout.Widget // Wrapped omnibox widget for overlay
37+ omniboxPaneID entity.PaneID // Which pane has the omnibox
38+ omniboxCfg OmniboxConfig // Stored config for creating omniboxes
39+
34 workspace *entity.Workspace40 workspace *entity.Workspace
35 paneViews map[entity.PaneID]*PaneView41 paneViews map[entity.PaneID]*PaneView
36 activePaneID entity.PaneID42 activePaneID entity.PaneID
@@ -154,6 +160,11 @@ func (wv *WorkspaceView) SetActivePaneID(paneID entity.PaneID) error {
154 160
155 // setActivePaneIDInternal updates active pane without locking.161 // setActivePaneIDInternal updates active pane without locking.
156 func (wv *WorkspaceView) setActivePaneIDInternal(paneID entity.PaneID) error {162 func (wv *WorkspaceView) setActivePaneIDInternal(paneID entity.PaneID) error {
163+ // Destroy omnibox if active pane is changing
164+ if wv.activePaneID != paneID && wv.omnibox != nil {
165+ wv.hideOmniboxInternal()
166+ }
167+
157 // Deactivate current active pane168 // Deactivate current active pane
158 if wv.activePaneID != "" {169 if wv.activePaneID != "" {
159 if oldPV, ok := wv.paneViews[wv.activePaneID]; ok {170 if oldPV, ok := wv.paneViews[wv.activePaneID]; ok {
@@ -401,3 +412,110 @@ func (wv *WorkspaceView) GetStackContainerWidget(paneID entity.PaneID) layout.Wi
401 func (wv *WorkspaceView) ContainerWidget() layout.Widget {412 func (wv *WorkspaceView) ContainerWidget() layout.Widget {
402 return wv.container413 return wv.container
403 }414 }
415+
416+// SetOmniboxConfig stores the omnibox configuration for later use.
417+func (wv *WorkspaceView) SetOmniboxConfig(cfg OmniboxConfig) {
418+ wv.mu.Lock()
419+ defer wv.mu.Unlock()
420+ wv.omniboxCfg = cfg
421+}
422+
423+// ShowOmnibox creates and shows the omnibox in the active pane.
424+func (wv *WorkspaceView) ShowOmnibox(ctx context.Context, query string) {
425+ wv.mu.Lock()
426+ defer wv.mu.Unlock()
427+
428+ // If omnibox already exists, just show it
429+ if wv.omnibox != nil {
430+ wv.omnibox.Show(ctx, query)
431+ return
432+ }
433+
434+ // Get the active pane view
435+ pv := wv.paneViews[wv.activePaneID]
436+ if pv == nil {
437+ wv.logger.Warn().Str("paneID", string(wv.activePaneID)).Msg("cannot show omnibox: active pane not found")
438+ return
439+ }
440+
441+ // Create omnibox
442+ omnibox := NewOmnibox(ctx, wv.omniboxCfg)
443+ if omnibox == nil {
444+ wv.logger.Error().Msg("failed to create omnibox")
445+ return
446+ }
447+
448+ // Set parent overlay for sizing
449+ omnibox.SetParentOverlay(pv.Overlay())
450+
451+ // Wrap widget for layout system
452+ omniboxWidget := omnibox.WidgetAsLayout(wv.factory)
453+ if omniboxWidget == nil {
454+ wv.logger.Error().Msg("failed to wrap omnibox widget")
455+ return
456+ }
457+
458+ // Add to pane overlay
459+ pv.AddOverlayWidget(omniboxWidget)
460+
461+ // Store references
462+ wv.omnibox = omnibox
463+ wv.omniboxWidget = omniboxWidget
464+ wv.omniboxPaneID = wv.activePaneID
465+
466+ // Show the omnibox
467+ omnibox.Show(ctx, query)
468+
469+ wv.logger.Debug().Str("paneID", string(wv.activePaneID)).Msg("omnibox shown")
470+}
471+
472+// HideOmnibox hides and destroys the current omnibox.
473+func (wv *WorkspaceView) HideOmnibox() {
474+ wv.mu.Lock()
475+ defer wv.mu.Unlock()
476+
477+ wv.hideOmniboxInternal()
478+}
479+
480+// hideOmniboxInternal destroys the omnibox without locking.
481+func (wv *WorkspaceView) hideOmniboxInternal() {
482+ if wv.omnibox == nil {
483+ return
484+ }
485+
486+ // Hide the omnibox first (use Background context since this is internal cleanup)
487+ wv.omnibox.Hide(context.Background())
488+
489+ // Remove from pane overlay
490+ if pv := wv.paneViews[wv.omniboxPaneID]; pv != nil && wv.omniboxWidget != nil {
491+ pv.RemoveOverlayWidget(wv.omniboxWidget)
492+ }
493+
494+ // Clear references
495+ wv.omnibox = nil
496+ wv.omniboxWidget = nil
497+ wv.omniboxPaneID = ""
498+
499+ wv.logger.Debug().Msg("omnibox hidden and destroyed")
500+}
501+
502+// GetOmnibox returns the current omnibox if visible.
503+func (wv *WorkspaceView) GetOmnibox() *Omnibox {
504+ wv.mu.RLock()
505+ defer wv.mu.RUnlock()
506+ return wv.omnibox
507+}
508+
509+// IsOmniboxVisible returns whether the omnibox is currently visible.
510+func (wv *WorkspaceView) IsOmniboxVisible() bool {
511+ wv.mu.RLock()
512+ defer wv.mu.RUnlock()
513+ return wv.omnibox != nil && wv.omnibox.IsVisible()
514+}
515+
516+// GetActivePaneView returns the PaneView for the current active pane.
517+func (wv *WorkspaceView) GetActivePaneView() *PaneView {
518+ wv.mu.RLock()
519+ defer wv.mu.RUnlock()
520+ return wv.paneViews[wv.activePaneID]
521+}
M internal/ui/coordinator/navigation.go +19 -13
@@ -8,14 +8,20 @@ import (
88 "github.com/bnema/dumber/internal/domain/entity"
99 "github.com/bnema/dumber/internal/infrastructure/webkit"
1010 "github.com/bnema/dumber/internal/logging"
11- "github.com/bnema/dumber/internal/ui/component"
1211 )
1312
13+// OmniboxProvider provides access to omnibox operations.
14+type OmniboxProvider interface {
15+ ToggleOmnibox(ctx context.Context)
16+ UpdateOmniboxZoom(factor float64)
17+ SetOmniboxOnNavigate(fn func(url string))
18+}
19+
1420 // NavigationCoordinator handles URL navigation, history, and browser controls.
1521 type NavigationCoordinator struct {
16- navigateUC *usecase.NavigateUseCase
17- contentCoord *ContentCoordinator
18- omnibox *component.Omnibox
22+ navigateUC *usecase.NavigateUseCase
23+ contentCoord *ContentCoordinator
24+ omniboxProvider OmniboxProvider
1925 }
2026
2127 // NewNavigationCoordinator creates a new NavigationCoordinator.
@@ -33,9 +39,9 @@ func NewNavigationCoordinator(
3339 }
3440 }
3541
36-// SetOmnibox sets the omnibox reference for toggle operations.
37-func (c *NavigationCoordinator) SetOmnibox(omnibox *component.Omnibox) {
38- c.omnibox = omnibox
42+// SetOmniboxProvider sets the omnibox provider for toggle/zoom operations.
43+func (c *NavigationCoordinator) SetOmniboxProvider(provider OmniboxProvider) {
44+ c.omniboxProvider = provider
3945 }
4046
4147 // Navigate loads a URL in the active pane using NavigateUseCase.
@@ -164,13 +170,13 @@ func (c *NavigationCoordinator) GoForward(ctx context.Context) error {
164170 func (c *NavigationCoordinator) OpenOmnibox(ctx context.Context) error {
165171 log := logging.FromContext(ctx)
166172
167- if c.omnibox == nil {
168- log.Error().Msg("omnibox not initialized")
169- return fmt.Errorf("omnibox not initialized")
173+ if c.omniboxProvider == nil {
174+ log.Error().Msg("omnibox provider not initialized")
175+ return fmt.Errorf("omnibox provider not initialized")
170176 }
171177
172178 log.Debug().Msg("toggling omnibox")
173- c.omnibox.Toggle(ctx)
179+ c.omniboxProvider.ToggleOmnibox(ctx)
174180 return nil
175181 }
176182
@@ -214,7 +220,7 @@ func (c *NavigationCoordinator) ActiveWebView(ctx context.Context) *webkit.WebVi
214220
215221 // NotifyZoomChanged updates the omnibox zoom indicator.
216222 func (c *NavigationCoordinator) NotifyZoomChanged(ctx context.Context, factor float64) {
217- if c.omnibox != nil {
218- c.omnibox.UpdateZoomIndicator(factor)
223+ if c.omniboxProvider != nil {
224+ c.omniboxProvider.UpdateOmniboxZoom(factor)
219225 }
220226 }
@@ -8,14 +8,20 @@ import (
8 "github.com/bnema/dumber/internal/domain/entity"8 "github.com/bnema/dumber/internal/domain/entity"
9 "github.com/bnema/dumber/internal/infrastructure/webkit"9 "github.com/bnema/dumber/internal/infrastructure/webkit"
10 "github.com/bnema/dumber/internal/logging"10 "github.com/bnema/dumber/internal/logging"
11- "github.com/bnema/dumber/internal/ui/component"
12 )11 )
13 12
13+// OmniboxProvider provides access to omnibox operations.
14+type OmniboxProvider interface {
15+ ToggleOmnibox(ctx context.Context)
16+ UpdateOmniboxZoom(factor float64)
17+ SetOmniboxOnNavigate(fn func(url string))
18+}
19+
14 // NavigationCoordinator handles URL navigation, history, and browser controls.20 // NavigationCoordinator handles URL navigation, history, and browser controls.
15 type NavigationCoordinator struct {21 type NavigationCoordinator struct {
16- navigateUC *usecase.NavigateUseCase22+ navigateUC *usecase.NavigateUseCase
17- contentCoord *ContentCoordinator23+ contentCoord *ContentCoordinator
18- omnibox *component.Omnibox24+ omniboxProvider OmniboxProvider
19 }25 }
20 26
21 // NewNavigationCoordinator creates a new NavigationCoordinator.27 // NewNavigationCoordinator creates a new NavigationCoordinator.
@@ -33,9 +39,9 @@ func NewNavigationCoordinator(
33 }39 }
34 }40 }
35 41
36-// SetOmnibox sets the omnibox reference for toggle operations.42+// SetOmniboxProvider sets the omnibox provider for toggle/zoom operations.
37-func (c *NavigationCoordinator) SetOmnibox(omnibox *component.Omnibox) {43+func (c *NavigationCoordinator) SetOmniboxProvider(provider OmniboxProvider) {
38- c.omnibox = omnibox44+ c.omniboxProvider = provider
39 }45 }
40 46
41 // Navigate loads a URL in the active pane using NavigateUseCase.47 // Navigate loads a URL in the active pane using NavigateUseCase.
@@ -164,13 +170,13 @@ func (c *NavigationCoordinator) GoForward(ctx context.Context) error {
164 func (c *NavigationCoordinator) OpenOmnibox(ctx context.Context) error {170 func (c *NavigationCoordinator) OpenOmnibox(ctx context.Context) error {
165 log := logging.FromContext(ctx)171 log := logging.FromContext(ctx)
166 172
167- if c.omnibox == nil {173+ if c.omniboxProvider == nil {
168- log.Error().Msg("omnibox not initialized")174+ log.Error().Msg("omnibox provider not initialized")
169- return fmt.Errorf("omnibox not initialized")175+ return fmt.Errorf("omnibox provider not initialized")
170 }176 }
171 177
172 log.Debug().Msg("toggling omnibox")178 log.Debug().Msg("toggling omnibox")
173- c.omnibox.Toggle(ctx)179+ c.omniboxProvider.ToggleOmnibox(ctx)
174 return nil180 return nil
175 }181 }
176 182
@@ -214,7 +220,7 @@ func (c *NavigationCoordinator) ActiveWebView(ctx context.Context) *webkit.WebVi
214 220
215 // NotifyZoomChanged updates the omnibox zoom indicator.221 // NotifyZoomChanged updates the omnibox zoom indicator.
216 func (c *NavigationCoordinator) NotifyZoomChanged(ctx context.Context, factor float64) {222 func (c *NavigationCoordinator) NotifyZoomChanged(ctx context.Context, factor float64) {
217- if c.omnibox != nil {223+ if c.omniboxProvider != nil {
218- c.omnibox.UpdateZoomIndicator(factor)224+ c.omniboxProvider.UpdateOmniboxZoom(factor)
219 }225 }
220 }226 }
M internal/ui/coordinator/workspace.go +16 -0
@@ -136,6 +136,10 @@ func (c *WorkspaceCoordinator) Split(ctx context.Context, direction usecase.Spli
136136 }
137137 c.contentCoord.AttachToWorkspace(ctx, ws, wsView)
138138 }
139+ // Update workspace view's active pane tracking
140+ if err := wsView.SetActivePaneID(ws.ActivePaneID); err != nil {
141+ log.Warn().Err(err).Msg("failed to set active pane in workspace view")
142+ }
139143 wsView.FocusPane(ws.ActivePaneID)
140144 } else if wsView != nil {
141145 // No existing widget (shouldn't happen), fallback to rebuild
@@ -143,6 +147,10 @@ func (c *WorkspaceCoordinator) Split(ctx context.Context, direction usecase.Spli
143147 log.Error().Err(err).Msg("failed to rebuild workspace view")
144148 }
145149 c.contentCoord.AttachToWorkspace(ctx, ws, wsView)
150+ // Update workspace view's active pane tracking
151+ if err := wsView.SetActivePaneID(ws.ActivePaneID); err != nil {
152+ log.Warn().Err(err).Msg("failed to set active pane in workspace view")
153+ }
146154 wsView.FocusPane(ws.ActivePaneID)
147155 }
148156
@@ -542,6 +550,10 @@ func (c *WorkspaceCoordinator) ClosePane(ctx context.Context) error {
542550 c.contentCoord.ReleaseWebView(ctx, closingPaneID)
543551 c.contentCoord.AttachToWorkspace(ctx, ws, wsView)
544552 }
553+ // Update workspace view's active pane tracking
554+ if err := wsView.SetActivePaneID(ws.ActivePaneID); err != nil {
555+ log.Warn().Err(err).Msg("failed to set active pane in workspace view")
556+ }
545557 wsView.FocusPane(ws.ActivePaneID)
546558 } else if wsView != nil {
547559 // Fallback to rebuild if we couldn't get context
@@ -550,6 +562,10 @@ func (c *WorkspaceCoordinator) ClosePane(ctx context.Context) error {
550562 }
551563 c.contentCoord.ReleaseWebView(ctx, closingPaneID)
552564 c.contentCoord.AttachToWorkspace(ctx, ws, wsView)
565+ // Update workspace view's active pane tracking
566+ if err := wsView.SetActivePaneID(ws.ActivePaneID); err != nil {
567+ log.Warn().Err(err).Msg("failed to set active pane in workspace view")
568+ }
553569 wsView.FocusPane(ws.ActivePaneID)
554570 }
555571
@@ -136,6 +136,10 @@ func (c *WorkspaceCoordinator) Split(ctx context.Context, direction usecase.Spli
136 }136 }
137 c.contentCoord.AttachToWorkspace(ctx, ws, wsView)137 c.contentCoord.AttachToWorkspace(ctx, ws, wsView)
138 }138 }
139+ // Update workspace view's active pane tracking
140+ if err := wsView.SetActivePaneID(ws.ActivePaneID); err != nil {
141+ log.Warn().Err(err).Msg("failed to set active pane in workspace view")
142+ }
139 wsView.FocusPane(ws.ActivePaneID)143 wsView.FocusPane(ws.ActivePaneID)
140 } else if wsView != nil {144 } else if wsView != nil {
141 // No existing widget (shouldn't happen), fallback to rebuild145 // No existing widget (shouldn't happen), fallback to rebuild
@@ -143,6 +147,10 @@ func (c *WorkspaceCoordinator) Split(ctx context.Context, direction usecase.Spli
143 log.Error().Err(err).Msg("failed to rebuild workspace view")147 log.Error().Err(err).Msg("failed to rebuild workspace view")
144 }148 }
145 c.contentCoord.AttachToWorkspace(ctx, ws, wsView)149 c.contentCoord.AttachToWorkspace(ctx, ws, wsView)
150+ // Update workspace view's active pane tracking
151+ if err := wsView.SetActivePaneID(ws.ActivePaneID); err != nil {
152+ log.Warn().Err(err).Msg("failed to set active pane in workspace view")
153+ }
146 wsView.FocusPane(ws.ActivePaneID)154 wsView.FocusPane(ws.ActivePaneID)
147 }155 }
148 156
@@ -542,6 +550,10 @@ func (c *WorkspaceCoordinator) ClosePane(ctx context.Context) error {
542 c.contentCoord.ReleaseWebView(ctx, closingPaneID)550 c.contentCoord.ReleaseWebView(ctx, closingPaneID)
543 c.contentCoord.AttachToWorkspace(ctx, ws, wsView)551 c.contentCoord.AttachToWorkspace(ctx, ws, wsView)
544 }552 }
553+ // Update workspace view's active pane tracking
554+ if err := wsView.SetActivePaneID(ws.ActivePaneID); err != nil {
555+ log.Warn().Err(err).Msg("failed to set active pane in workspace view")
556+ }
545 wsView.FocusPane(ws.ActivePaneID)557 wsView.FocusPane(ws.ActivePaneID)
546 } else if wsView != nil {558 } else if wsView != nil {
547 // Fallback to rebuild if we couldn't get context559 // Fallback to rebuild if we couldn't get context
@@ -550,6 +562,10 @@ func (c *WorkspaceCoordinator) ClosePane(ctx context.Context) error {
550 }562 }
551 c.contentCoord.ReleaseWebView(ctx, closingPaneID)563 c.contentCoord.ReleaseWebView(ctx, closingPaneID)
552 c.contentCoord.AttachToWorkspace(ctx, ws, wsView)564 c.contentCoord.AttachToWorkspace(ctx, ws, wsView)
565+ // Update workspace view's active pane tracking
566+ if err := wsView.SetActivePaneID(ws.ActivePaneID); err != nil {
567+ log.Warn().Err(err).Msg("failed to set active pane in workspace view")
568+ }
553 wsView.FocusPane(ws.ActivePaneID)569 wsView.FocusPane(ws.ActivePaneID)
554 }570 }
555 571
M internal/ui/window/main_window.go +1 -1
@@ -20,7 +20,7 @@ const (
2020 // MainWindow represents the main browser window.
2121 type MainWindow struct {
2222 window *gtk.ApplicationWindow
23- rootBox *gtk.Box // Vertical: tab bar + content
23+ rootBox *gtk.Box // Vertical: tab bar + content
2424 tabBar *component.TabBar
2525 contentOverlay *gtk.Overlay // Overlay for content + omnibox
2626 contentArea *gtk.Box // Container for workspace content
@@ -20,7 +20,7 @@ const (
20 // MainWindow represents the main browser window.20 // MainWindow represents the main browser window.
21 type MainWindow struct {21 type MainWindow struct {
22 window *gtk.ApplicationWindow22 window *gtk.ApplicationWindow
23- rootBox *gtk.Box // Vertical: tab bar + content23+ rootBox *gtk.Box // Vertical: tab bar + content
24 tabBar *component.TabBar24 tabBar *component.TabBar
25 contentOverlay *gtk.Overlay // Overlay for content + omnibox25 contentOverlay *gtk.Overlay // Overlay for content + omnibox
26 contentArea *gtk.Box // Container for workspace content26 contentArea *gtk.Box // Container for workspace content
A storage/AvMqFGIIrr6_1j389SL02pSAz612eCesZHRPox5jdX0/AvMqFGIIrr6_1j389SL02pSAz612eCesZHRPox5jdX0/LocalStorage/localstorage.sqlite3-shm +0 -0 binary file: no text diff

binary file: no text diff

A storage/AvMqFGIIrr6_1j389SL02pSAz612eCesZHRPox5jdX0/AvMqFGIIrr6_1j389SL02pSAz612eCesZHRPox5jdX0/LocalStorage/localstorage.sqlite3-wal +0 -0 binary file: no text diff

binary file: no text diff

A storage/AvMqFGIIrr6_1j389SL02pSAz612eCesZHRPox5jdX0/AvMqFGIIrr6_1j389SL02pSAz612eCesZHRPox5jdX0/origin +0 -0 binary file: no text diff

binary file: no text diff

A storage/salt +2 -0
@@ -0,0 +1,2 @@
1+FT
2+a�x$#⏎̸
@@ -0,0 +1,2 @@
1+FT
2+a�x$#⏎̸

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