detached 04b95bc

refactor(ui): split app activation into helpers

commit
04b95bc45e761cbcd96864e35390981d70b5c509
author
brice · · 2025-12-22 06:47 UTC
parent
83d1d24
signature
unsigned
1 file · +126 -52
M internal/ui/app.go +126 -52
@@ -166,74 +166,127 @@ func (a *App) onActivate(ctx context.Context) {
166166 log := logging.FromContext(ctx)
167167 log.Debug().Msg("GTK application activated")
168168
169+ a.applyGTKColorSchemePreference(ctx)
170+ a.prewarmWebViewPool(ctx)
171+
172+ if err := a.createMainWindow(ctx); err != nil {
173+ log.Error().Err(err).Msg("failed to create main window")
174+ return
175+ }
176+
177+ a.initLayoutInfrastructure()
178+ a.initAppToasterOverlay()
179+ a.initFocusAndBorderOverlay()
180+
181+ a.initCoordinators(ctx)
182+ a.initKeyboardHandler(ctx)
183+ a.initOmniboxConfig(ctx)
184+ a.initFindBarConfig()
185+ a.createInitialTab(ctx)
186+ a.finalizeActivation(ctx)
187+}
188+
189+func (a *App) applyGTKColorSchemePreference(ctx context.Context) {
190+ log := logging.FromContext(ctx)
191+
169192 // Propagate app color scheme preference to GTK (and WebKit)
170193 // so web content can correctly evaluate prefers-color-scheme.
171- if settings := gtk.SettingsGetDefault(); settings != nil {
172- scheme := "default"
173- if a.deps != nil && a.deps.Config != nil && a.deps.Config.Appearance.ColorScheme != "" {
174- scheme = a.deps.Config.Appearance.ColorScheme
175- }
176- prefersDark := theme.ResolveColorScheme(scheme)
177- settings.SetPropertyGtkApplicationPreferDarkTheme(prefersDark)
178- log.Debug().
179- Str("scheme", scheme).
180- Bool("prefers_dark", prefersDark).
181- Msg("set gtk-application-prefer-dark-theme")
194+ settings := gtk.SettingsGetDefault()
195+ if settings == nil {
196+ return
182197 }
183198
184- // Prewarm WebView pool now that GTK is initialized
185- if a.pool != nil {
186- a.pool.Prewarm(ctx, 0)
199+ scheme := "default"
200+ if a.deps != nil && a.deps.Config != nil && a.deps.Config.Appearance.ColorScheme != "" {
201+ scheme = a.deps.Config.Appearance.ColorScheme
187202 }
203+ prefersDark := theme.ResolveColorScheme(scheme)
204+ settings.SetPropertyGtkApplicationPreferDarkTheme(prefersDark)
205+ log.Debug().
206+ Str("scheme", scheme).
207+ Bool("prefers_dark", prefersDark).
208+ Msg("set gtk-application-prefer-dark-theme")
209+}
188210
189- // Create the main window
190- var err error
191- a.mainWindow, err = window.New(ctx, a.gtkApp, a.deps.Config)
192- if err != nil {
193- log.Error().Err(err).Msg("failed to create main window")
211+func (a *App) prewarmWebViewPool(ctx context.Context) {
212+ // Prewarm WebView pool now that GTK is initialized.
213+ if a.pool == nil {
194214 return
195215 }
216+ a.pool.Prewarm(ctx, 0)
217+}
196218
197- // Apply GTK CSS styling from theme manager
198- if a.deps.Theme != nil {
199- if display := a.mainWindow.Window().GetDisplay(); display != nil {
200- a.deps.Theme.ApplyToDisplay(ctx, display)
201- }
219+func (a *App) createMainWindow(ctx context.Context) error {
220+ mainWindow, err := window.New(ctx, a.gtkApp, a.deps.Config)
221+ if err != nil {
222+ return err
202223 }
224+ a.mainWindow = mainWindow
203225
204- // Initialize widget factory for pane layout
226+ // Apply GTK CSS styling from theme manager.
227+ if a.deps == nil || a.deps.Theme == nil {
228+ return nil
229+ }
230+ if display := a.mainWindow.Window().GetDisplay(); display != nil {
231+ a.deps.Theme.ApplyToDisplay(ctx, display)
232+ }
233+ return nil
234+}
235+
236+func (a *App) initLayoutInfrastructure() {
237+ // Initialize widget factory for pane layout.
205238 a.widgetFactory = layout.NewGtkWidgetFactory()
206239
207- // Initialize stacked pane manager for incremental widget operations
240+ // Initialize stacked pane manager for incremental widget operations.
208241 a.stackedPaneMgr = component.NewStackedPaneManager(a.widgetFactory)
242+}
209243
210- // Create app-level toaster for system notifications
244+func (a *App) initAppToasterOverlay() {
245+ if a.mainWindow == nil {
246+ return
247+ }
248+
249+ // Create app-level toaster for system notifications.
211250 a.appToaster = component.NewToaster(a.widgetFactory)
212- if toasterWidget := a.appToaster.Widget(); toasterWidget != nil {
213- if gtkWidget := toasterWidget.GtkWidget(); gtkWidget != nil {
214- a.mainWindow.AddOverlay(gtkWidget)
215- }
251+ toasterWidget := a.appToaster.Widget()
252+ if toasterWidget == nil {
253+ return
216254 }
255+ gtkWidget := toasterWidget.GtkWidget()
256+ if gtkWidget == nil {
257+ return
258+ }
259+ a.mainWindow.AddOverlay(gtkWidget)
260+}
217261
218- // Initialize focus manager for geometric navigation
262+func (a *App) initFocusAndBorderOverlay() {
263+ // Initialize focus manager for geometric navigation.
219264 a.focusMgr = focus.NewManager(a.panesUC)
220265
221- // Initialize border manager for mode indicators
266+ // Initialize border manager for mode indicators.
222267 a.borderMgr = focus.NewBorderManager(a.widgetFactory)
223268
224- // Attach border overlay to main window (visible for all tabs)
225- if a.borderMgr != nil && a.mainWindow != nil {
226- if borderWidget := a.borderMgr.Widget(); borderWidget != nil {
227- if gtkWidget := borderWidget.GtkWidget(); gtkWidget != nil {
228- a.mainWindow.AddOverlay(gtkWidget)
229- }
230- }
269+ // Attach border overlay to main window (visible for all tabs).
270+ if a.borderMgr == nil || a.mainWindow == nil {
271+ return
272+ }
273+ borderWidget := a.borderMgr.Widget()
274+ if borderWidget == nil {
275+ return
276+ }
277+ gtkWidget := borderWidget.GtkWidget()
278+ if gtkWidget == nil {
279+ return
231280 }
281+ a.mainWindow.AddOverlay(gtkWidget)
282+}
232283
233- // Initialize coordinators
234- a.initCoordinators(ctx)
284+func (a *App) initKeyboardHandler(ctx context.Context) {
285+ if a.mainWindow == nil || a.deps == nil || a.deps.Config == nil {
286+ return
287+ }
235288
236- // Create keyboard handler and wire to dispatcher
289+ // Create keyboard handler and wire to dispatcher.
237290 a.keyboardHandler = input.NewKeyboardHandler(
238291 ctx,
239292 &a.deps.Config.Workspace,
@@ -252,8 +305,16 @@ func (a *App) onActivate(ctx context.Context) {
252305 return wsView.IsOmniboxVisible()
253306 })
254307 a.keyboardHandler.AttachTo(a.mainWindow.Window())
308+}
309+
310+func (a *App) initOmniboxConfig(ctx context.Context) {
311+ if a.deps == nil || a.deps.Config == nil {
312+ return
313+ }
255314
256- // Store omnibox config (omnibox is created per-pane via WorkspaceView)
315+ log := logging.FromContext(ctx)
316+
317+ // Store omnibox config (omnibox is created per-pane via WorkspaceView).
257318 a.omniboxCfg = component.OmniboxConfig{
258319 HistoryUC: a.deps.HistoryUC,
259320 FavoritesUC: a.deps.FavoritesUC,
@@ -271,8 +332,10 @@ func (a *App) onActivate(ctx context.Context) {
271332 }
272333 a.navCoord.SetOmniboxProvider(a)
273334 log.Debug().Msg("omnibox config stored, provider set")
335+}
274336
275- // Store find bar config (find bar is created per-pane via WorkspaceView)
337+func (a *App) initFindBarConfig() {
338+ // Store find bar config (find bar is created per-pane via WorkspaceView).
276339 a.findBarCfg = component.FindBarConfig{
277340 GetFindController: func(paneID entity.PaneID) port.FindController {
278341 if a.contentCoord == nil {
@@ -285,29 +348,40 @@ func (a *App) onActivate(ctx context.Context) {
285348 return wv.GetFindController()
286349 },
287350 }
351+}
288352
289- // Create an initial tab using coordinator
353+func (a *App) createInitialTab(ctx context.Context) {
354+ log := logging.FromContext(ctx)
355+
356+ // Create an initial tab using coordinator.
290357 initialURL := "dumb://home"
291- if a.deps.InitialURL != "" {
358+ if a.deps != nil && a.deps.InitialURL != "" {
292359 initialURL = a.deps.InitialURL
293360 }
294361 if _, err := a.tabCoord.Create(ctx, initialURL); err != nil {
295362 log.Error().Err(err).Msg("failed to create initial tab")
296363 }
364+}
297365
298- // Show the window
299- a.mainWindow.Show()
366+func (a *App) finalizeActivation(ctx context.Context) {
367+ log := logging.FromContext(ctx)
300368
369+ // Show the window.
370+ if a.mainWindow != nil {
371+ a.mainWindow.Show()
372+ }
301373 log.Info().Msg("main window displayed")
302374
303- // Start watching config for appearance changes
375+ // Start watching config for appearance changes.
304376 a.initConfigWatcher(ctx)
305377
306378 // Ensure the current appearance config is applied once at startup.
307379 // This avoids the WebUI showing default tokens until the first config change event.
308- a.applyAppearanceConfig(ctx, a.deps.Config)
380+ if a.deps != nil {
381+ a.applyAppearanceConfig(ctx, a.deps.Config)
382+ }
309383
310- // Start async filter loading after window is visible
384+ // Start async filter loading after window is visible.
311385 a.initFilteringAsync(ctx)
312386 }
313387
@@ -166,74 +166,127 @@ func (a *App) onActivate(ctx context.Context) {
166 log := logging.FromContext(ctx)166 log := logging.FromContext(ctx)
167 log.Debug().Msg("GTK application activated")167 log.Debug().Msg("GTK application activated")
168 168
169+ a.applyGTKColorSchemePreference(ctx)
170+ a.prewarmWebViewPool(ctx)
171+
172+ if err := a.createMainWindow(ctx); err != nil {
173+ log.Error().Err(err).Msg("failed to create main window")
174+ return
175+ }
176+
177+ a.initLayoutInfrastructure()
178+ a.initAppToasterOverlay()
179+ a.initFocusAndBorderOverlay()
180+
181+ a.initCoordinators(ctx)
182+ a.initKeyboardHandler(ctx)
183+ a.initOmniboxConfig(ctx)
184+ a.initFindBarConfig()
185+ a.createInitialTab(ctx)
186+ a.finalizeActivation(ctx)
187+}
188+
189+func (a *App) applyGTKColorSchemePreference(ctx context.Context) {
190+ log := logging.FromContext(ctx)
191+
169 // Propagate app color scheme preference to GTK (and WebKit)192 // Propagate app color scheme preference to GTK (and WebKit)
170 // so web content can correctly evaluate prefers-color-scheme.193 // so web content can correctly evaluate prefers-color-scheme.
171- if settings := gtk.SettingsGetDefault(); settings != nil {194+ settings := gtk.SettingsGetDefault()
172- scheme := "default"195+ if settings == nil {
173- if a.deps != nil && a.deps.Config != nil && a.deps.Config.Appearance.ColorScheme != "" {196+ return
174- scheme = a.deps.Config.Appearance.ColorScheme
175- }
176- prefersDark := theme.ResolveColorScheme(scheme)
177- settings.SetPropertyGtkApplicationPreferDarkTheme(prefersDark)
178- log.Debug().
179- Str("scheme", scheme).
180- Bool("prefers_dark", prefersDark).
181- Msg("set gtk-application-prefer-dark-theme")
182 }197 }
183 198
184- // Prewarm WebView pool now that GTK is initialized199+ scheme := "default"
185- if a.pool != nil {200+ if a.deps != nil && a.deps.Config != nil && a.deps.Config.Appearance.ColorScheme != "" {
186- a.pool.Prewarm(ctx, 0)201+ scheme = a.deps.Config.Appearance.ColorScheme
187 }202 }
203+ prefersDark := theme.ResolveColorScheme(scheme)
204+ settings.SetPropertyGtkApplicationPreferDarkTheme(prefersDark)
205+ log.Debug().
206+ Str("scheme", scheme).
207+ Bool("prefers_dark", prefersDark).
208+ Msg("set gtk-application-prefer-dark-theme")
209+}
188 210
189- // Create the main window211+func (a *App) prewarmWebViewPool(ctx context.Context) {
190- var err error212+ // Prewarm WebView pool now that GTK is initialized.
191- a.mainWindow, err = window.New(ctx, a.gtkApp, a.deps.Config)213+ if a.pool == nil {
192- if err != nil {
193- log.Error().Err(err).Msg("failed to create main window")
194 return214 return
195 }215 }
216+ a.pool.Prewarm(ctx, 0)
217+}
196 218
197- // Apply GTK CSS styling from theme manager219+func (a *App) createMainWindow(ctx context.Context) error {
198- if a.deps.Theme != nil {220+ mainWindow, err := window.New(ctx, a.gtkApp, a.deps.Config)
199- if display := a.mainWindow.Window().GetDisplay(); display != nil {221+ if err != nil {
200- a.deps.Theme.ApplyToDisplay(ctx, display)222+ return err
201- }
202 }223 }
224+ a.mainWindow = mainWindow
203 225
204- // Initialize widget factory for pane layout226+ // Apply GTK CSS styling from theme manager.
227+ if a.deps == nil || a.deps.Theme == nil {
228+ return nil
229+ }
230+ if display := a.mainWindow.Window().GetDisplay(); display != nil {
231+ a.deps.Theme.ApplyToDisplay(ctx, display)
232+ }
233+ return nil
234+}
235+
236+func (a *App) initLayoutInfrastructure() {
237+ // Initialize widget factory for pane layout.
205 a.widgetFactory = layout.NewGtkWidgetFactory()238 a.widgetFactory = layout.NewGtkWidgetFactory()
206 239
207- // Initialize stacked pane manager for incremental widget operations240+ // Initialize stacked pane manager for incremental widget operations.
208 a.stackedPaneMgr = component.NewStackedPaneManager(a.widgetFactory)241 a.stackedPaneMgr = component.NewStackedPaneManager(a.widgetFactory)
242+}
209 243
210- // Create app-level toaster for system notifications244+func (a *App) initAppToasterOverlay() {
245+ if a.mainWindow == nil {
246+ return
247+ }
248+
249+ // Create app-level toaster for system notifications.
211 a.appToaster = component.NewToaster(a.widgetFactory)250 a.appToaster = component.NewToaster(a.widgetFactory)
212- if toasterWidget := a.appToaster.Widget(); toasterWidget != nil {251+ toasterWidget := a.appToaster.Widget()
213- if gtkWidget := toasterWidget.GtkWidget(); gtkWidget != nil {252+ if toasterWidget == nil {
214- a.mainWindow.AddOverlay(gtkWidget)253+ return
215- }
216 }254 }
255+ gtkWidget := toasterWidget.GtkWidget()
256+ if gtkWidget == nil {
257+ return
258+ }
259+ a.mainWindow.AddOverlay(gtkWidget)
260+}
217 261
218- // Initialize focus manager for geometric navigation262+func (a *App) initFocusAndBorderOverlay() {
263+ // Initialize focus manager for geometric navigation.
219 a.focusMgr = focus.NewManager(a.panesUC)264 a.focusMgr = focus.NewManager(a.panesUC)
220 265
221- // Initialize border manager for mode indicators266+ // Initialize border manager for mode indicators.
222 a.borderMgr = focus.NewBorderManager(a.widgetFactory)267 a.borderMgr = focus.NewBorderManager(a.widgetFactory)
223 268
224- // Attach border overlay to main window (visible for all tabs)269+ // Attach border overlay to main window (visible for all tabs).
225- if a.borderMgr != nil && a.mainWindow != nil {270+ if a.borderMgr == nil || a.mainWindow == nil {
226- if borderWidget := a.borderMgr.Widget(); borderWidget != nil {271+ return
227- if gtkWidget := borderWidget.GtkWidget(); gtkWidget != nil {272+ }
228- a.mainWindow.AddOverlay(gtkWidget)273+ borderWidget := a.borderMgr.Widget()
229- }274+ if borderWidget == nil {
230- }275+ return
276+ }
277+ gtkWidget := borderWidget.GtkWidget()
278+ if gtkWidget == nil {
279+ return
231 }280 }
281+ a.mainWindow.AddOverlay(gtkWidget)
282+}
232 283
233- // Initialize coordinators284+func (a *App) initKeyboardHandler(ctx context.Context) {
234- a.initCoordinators(ctx)285+ if a.mainWindow == nil || a.deps == nil || a.deps.Config == nil {
286+ return
287+ }
235 288
236- // Create keyboard handler and wire to dispatcher289+ // Create keyboard handler and wire to dispatcher.
237 a.keyboardHandler = input.NewKeyboardHandler(290 a.keyboardHandler = input.NewKeyboardHandler(
238 ctx,291 ctx,
239 &a.deps.Config.Workspace,292 &a.deps.Config.Workspace,
@@ -252,8 +305,16 @@ func (a *App) onActivate(ctx context.Context) {
252 return wsView.IsOmniboxVisible()305 return wsView.IsOmniboxVisible()
253 })306 })
254 a.keyboardHandler.AttachTo(a.mainWindow.Window())307 a.keyboardHandler.AttachTo(a.mainWindow.Window())
308+}
309+
310+func (a *App) initOmniboxConfig(ctx context.Context) {
311+ if a.deps == nil || a.deps.Config == nil {
312+ return
313+ }
255 314
256- // Store omnibox config (omnibox is created per-pane via WorkspaceView)315+ log := logging.FromContext(ctx)
316+
317+ // Store omnibox config (omnibox is created per-pane via WorkspaceView).
257 a.omniboxCfg = component.OmniboxConfig{318 a.omniboxCfg = component.OmniboxConfig{
258 HistoryUC: a.deps.HistoryUC,319 HistoryUC: a.deps.HistoryUC,
259 FavoritesUC: a.deps.FavoritesUC,320 FavoritesUC: a.deps.FavoritesUC,
@@ -271,8 +332,10 @@ func (a *App) onActivate(ctx context.Context) {
271 }332 }
272 a.navCoord.SetOmniboxProvider(a)333 a.navCoord.SetOmniboxProvider(a)
273 log.Debug().Msg("omnibox config stored, provider set")334 log.Debug().Msg("omnibox config stored, provider set")
335+}
274 336
275- // Store find bar config (find bar is created per-pane via WorkspaceView)337+func (a *App) initFindBarConfig() {
338+ // Store find bar config (find bar is created per-pane via WorkspaceView).
276 a.findBarCfg = component.FindBarConfig{339 a.findBarCfg = component.FindBarConfig{
277 GetFindController: func(paneID entity.PaneID) port.FindController {340 GetFindController: func(paneID entity.PaneID) port.FindController {
278 if a.contentCoord == nil {341 if a.contentCoord == nil {
@@ -285,29 +348,40 @@ func (a *App) onActivate(ctx context.Context) {
285 return wv.GetFindController()348 return wv.GetFindController()
286 },349 },
287 }350 }
351+}
288 352
289- // Create an initial tab using coordinator353+func (a *App) createInitialTab(ctx context.Context) {
354+ log := logging.FromContext(ctx)
355+
356+ // Create an initial tab using coordinator.
290 initialURL := "dumb://home"357 initialURL := "dumb://home"
291- if a.deps.InitialURL != "" {358+ if a.deps != nil && a.deps.InitialURL != "" {
292 initialURL = a.deps.InitialURL359 initialURL = a.deps.InitialURL
293 }360 }
294 if _, err := a.tabCoord.Create(ctx, initialURL); err != nil {361 if _, err := a.tabCoord.Create(ctx, initialURL); err != nil {
295 log.Error().Err(err).Msg("failed to create initial tab")362 log.Error().Err(err).Msg("failed to create initial tab")
296 }363 }
364+}
297 365
298- // Show the window366+func (a *App) finalizeActivation(ctx context.Context) {
299- a.mainWindow.Show()367+ log := logging.FromContext(ctx)
300 368
369+ // Show the window.
370+ if a.mainWindow != nil {
371+ a.mainWindow.Show()
372+ }
301 log.Info().Msg("main window displayed")373 log.Info().Msg("main window displayed")
302 374
303- // Start watching config for appearance changes375+ // Start watching config for appearance changes.
304 a.initConfigWatcher(ctx)376 a.initConfigWatcher(ctx)
305 377
306 // Ensure the current appearance config is applied once at startup.378 // Ensure the current appearance config is applied once at startup.
307 // This avoids the WebUI showing default tokens until the first config change event.379 // This avoids the WebUI showing default tokens until the first config change event.
308- a.applyAppearanceConfig(ctx, a.deps.Config)380+ if a.deps != nil {
381+ a.applyAppearanceConfig(ctx, a.deps.Config)
382+ }
309 383
310- // Start async filter loading after window is visible384+ // Start async filter loading after window is visible.
311 a.initFilteringAsync(ctx)385 a.initFilteringAsync(ctx)
312 }386 }
313 387

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