app_window_lifecycle.go RawHistoryFolder

1package ui
2
3import (
4	"context"
5	"fmt"
6
7	"github.com/bnema/dumber/internal/application/usecase"
8	"github.com/bnema/dumber/internal/domain/entity"
9	domainurl "github.com/bnema/dumber/internal/domain/url"
10	"github.com/bnema/dumber/internal/logging"
11	"github.com/bnema/dumber/internal/ui/window"
12	"github.com/bnema/puregotk/v4/gtk"
13)
14
15func (a *App) createBrowserWindow(ctx context.Context, initialURL string) (*browserWindow, error) {
16	log := logging.FromContext(ctx)
17	log.Debug().
18		Str("url_host", logging.SafeURLHost(initialURL)).
19		Bool("using_factory", a.browserWindowFactory != nil).
20		Int("window_count_before", len(a.browserWindows)).
21		Msg("ui: create browser window started")
22
23	if a.browserWindowFactory != nil {
24		created, err := a.browserWindowFactory(ctx, initialURL)
25		if err != nil {
26			log.Warn().Err(err).
27				Str("url_host", logging.SafeURLHost(initialURL)).
28				Msg("ui: browser window factory failed")
29			return nil, err
30		}
31		created.ensureTabs()
32		a.wireBrowserWindowActivationTracking(created)
33		created.initChrome(ctx, a)
34		return created, nil
35	}
36
37	runtimeCfg := a.runtimeConfigSnapshot().UI
38	mainWindow, err := window.New(ctx, a.gtkApp, runtimeCfg.Workspace.TabBarPosition)
39	if err != nil {
40		log.Warn().Err(err).
41			Str("url_host", logging.SafeURLHost(initialURL)).
42			Msg("ui: GTK browser window shell creation failed")
43		return nil, err
44	}
45	browserWindow := &browserWindow{
46		id:         a.generateWindowID(),
47		initialURL: initialURL,
48		tabs:       entity.NewTabList(),
49		mainWindow: mainWindow,
50	}
51	a.wireBrowserWindowActivationTracking(browserWindow)
52	if a.mainWindow == nil {
53		a.mainWindow = mainWindow
54	}
55
56	closeRequestCb := func(_ gtk.Window) bool {
57		log.Info().Msg("browser window close requested")
58		a.removeBrowserWindow(browserWindow.id)
59		return false
60	}
61	mainWindow.Window().ConnectCloseRequest(&closeRequestCb)
62
63	a.initBrowserWindowOverlays(mainWindow, browserWindow, runtimeCfg)
64	a.wireBrowserWindowPermissionIndicator(browserWindow)
65	if a.kbDispatcher != nil {
66		a.initBrowserWindowInput(ctx, browserWindow)
67	}
68	if a.tabCoord != nil {
69		a.wireBrowserWindowTabBar(ctx, browserWindow)
70	}
71	browserWindow.initChrome(ctx, a)
72
73	// Apply GTK CSS styling from theme manager.
74	if a.deps == nil || a.deps.Theme == nil {
75		return browserWindow, nil
76	}
77	if display := mainWindow.Window().GetDisplay(); display != nil {
78		a.deps.Theme.ApplyToDisplay(ctx, display)
79	}
80	log.Debug().
81		Str("window_id", browserWindow.id).
82		Str("url_host", logging.SafeURLHost(initialURL)).
83		Msg("ui: create browser window completed")
84	return browserWindow, nil
85}
86
87func (a *App) openInitialBrowserWindowShell(ctx context.Context, initialURL string) error {
88	log := logging.FromContext(ctx)
89	created, err := a.createBrowserWindow(ctx, initialURL)
90	if err != nil {
91		log.Warn().Err(err).
92			Str("url_host", logging.SafeURLHost(initialURL)).
93			Int("window_count_after", len(a.browserWindows)).
94			Msg("ui: initial browser window shell creation failed")
95		return err
96	}
97	a.registerBrowserWindow(created)
98	log.Debug().
99		Str("window_id", created.id).
100		Str("url_host", logging.SafeURLHost(initialURL)).
101		Msg("ui: initial browser window shell registered")
102	a.activateBrowserWindow(created)
103	return nil
104}
105
106// OpenInstance opens a dedicated window for a logical instance, or focuses the
107// existing window and opens the requested URL there. All instances owned by an
108// App share the same engine and CEF request context.
109func (a *App) OpenInstance(ctx context.Context, name, url string) error {
110	if name == "" {
111		return fmt.Errorf("instance name must not be empty")
112	}
113	url = domainurl.Normalize(url)
114	return a.dispatchWindowURLWork(ctx, "ui.open_instance", url, func(ctx context.Context) error {
115		if a.instanceWindows == nil {
116			a.instanceWindows = make(map[string]string)
117		}
118		if windowID := a.instanceWindows[name]; windowID != "" {
119			if existing := a.browserWindows[windowID]; existing != nil {
120				a.activateBrowserWindow(existing)
121				return a.openExternalURLInWindowOnMainThread(ctx, url, existing)
122			}
123			delete(a.instanceWindows, name)
124		}
125
126		before := make(map[string]struct{}, len(a.browserWindows))
127		for id := range a.browserWindows {
128			before[id] = struct{}{}
129		}
130		if err := a.openFreshWindow(ctx, url); err != nil {
131			return err
132		}
133		for id := range a.browserWindows {
134			if _, existed := before[id]; !existed {
135				a.instanceWindows[name] = id
136				return nil
137			}
138		}
139		return fmt.Errorf("instance %q did not create a browser window", name)
140	})
141}
142
143func (a *App) openExternalURLInWindowOnMainThread(ctx context.Context, url string, target *browserWindow) error {
144	if target == nil {
145		return fmt.Errorf("target browser window is nil")
146	}
147	a.activateBrowserWindow(target)
148	return a.openExternalURLInFocusedWindow(ctx, url, entity.ExternalLinksConfig{
149		Behavior: entity.ExternalLinkBehaviorTabbed,
150	})
151}
152
153func (a *App) openFreshWindow(ctx context.Context, url string) error {
154	log := logging.FromContext(ctx)
155	log.Debug().
156		Str("url_host", logging.SafeURLHost(url)).
157		Int("window_count_before", len(a.browserWindows)).
158		Bool("has_tab_coord", a.tabCoord != nil).
159		Bool("has_tabs_uc", a.tabsUC != nil).
160		Msg("ui: open fresh window started")
161
162	created, err := a.createBrowserWindow(ctx, url)
163	if err != nil {
164		log.Warn().Err(err).
165			Str("url_host", logging.SafeURLHost(url)).
166			Int("window_count_after", len(a.browserWindows)).
167			Msg("ui: open fresh window could not create browser window")
168		return err
169	}
170	log.Debug().
171		Str("window_id", created.id).
172		Str("url_host", logging.SafeURLHost(url)).
173		Msg("ui: open fresh window created browser window shell")
174	a.registerBrowserWindow(created)
175	log.Debug().
176		Str("window_id", created.id).
177		Int("window_count_after_register", len(a.browserWindows)).
178		Msg("ui: open fresh window registered browser window")
179
180	var openErr error
181	path := "tabs_uc"
182	if a.tabCoord != nil {
183		path = "tab_coord"
184		log.Debug().Str("window_id", created.id).Msg("ui: creating initial tab for fresh window via tab coordinator")
185		openErr = a.openFreshWindowWithTabCoord(ctx, url, created)
186	} else if a.tabsUC == nil {
187		log.Debug().Str("window_id", created.id).Msg("ui: activating fresh browser window without tab use case")
188		a.activateBrowserWindow(created)
189		return nil
190	} else {
191		log.Debug().Str("window_id", created.id).Msg("ui: creating initial tab for fresh window via tabs use case")
192		openErr = a.openFreshWindowWithTabsUC(ctx, url, created)
193	}
194	if openErr != nil {
195		log.Warn().Err(openErr).
196			Str("window_id", created.id).
197			Str("path", path).
198			Int("window_count_after", len(a.browserWindows)).
199			Msg("ui: open fresh window initial tab creation failed")
200		return openErr
201	}
202	log.Debug().
203		Str("window_id", created.id).
204		Str("path", path).
205		Msg("ui: activating fresh browser window after tab creation")
206	a.activateBrowserWindow(created)
207	return nil
208}
209
210func (a *App) openFreshWindowWithTabCoord(ctx context.Context, url string, created *browserWindow) error {
211	log := logging.FromContext(ctx)
212	previousTarget := a.tabCoord.CurrentTarget()
213	target := a.ensureTabTargetForBrowserWindow(created)
214	a.tabCoord.SetCurrentTarget(target)
215	defer func() {
216		a.tabCoord.SetCurrentTarget(previousTarget)
217	}()
218
219	log.Debug().
220		Str("window_id", created.id).
221		Str("url_host", logging.SafeURLHost(url)).
222		Msg("ui: tab coordinator create started for fresh window")
223	createdTab, createErr := a.tabCoord.Create(ctx, target, url)
224	if createErr != nil {
225		log.Warn().Err(createErr).
226			Str("window_id", created.id).
227			Msg("ui: tab coordinator create failed for fresh window; rolling back window")
228		a.removeBrowserWindow(created.id)
229		if created.mainWindow != nil {
230			created.mainWindow.Destroy()
231		}
232		return createErr
233	}
234	log.Debug().
235		Str("window_id", created.id).
236		Str("tab_id", string(createdTab.ID)).
237		Msg("ui: tab coordinator create completed for fresh window")
238	if a.workspaceViews[createdTab.ID] == nil {
239		log.Warn().
240			Str("window_id", created.id).
241			Str("tab_id", string(createdTab.ID)).
242			Msg("ui: fresh window tab has no workspace view; rolling back window")
243		if createdTabs := a.tabListForBrowserWindow(created); createdTabs != nil {
244			createdTabs.Remove(createdTab.ID)
245		}
246		if created.mainWindow != nil {
247			if tabBar := created.mainWindow.TabBar(); tabBar != nil {
248				tabBar.RemoveTab(createdTab.ID)
249				if activeTab := a.activeTabForBrowserWindow(created); activeTab != nil {
250					tabBar.SetActive(activeTab.ID)
251				}
252			}
253		}
254		a.removeBrowserWindow(created.id)
255		if created.mainWindow != nil {
256			created.mainWindow.Destroy()
257		}
258		return fmt.Errorf("workspace view not created for tab %s", createdTab.ID)
259	}
260	a.setBrowserWindowForTab(createdTab.ID, created)
261	// Tab was already created in the per-window TabList via tabCoord.Create;
262	// active state is managed by TabList.SetActive.
263	if created.mainWindow != nil {
264		created.mainWindow.Show()
265	}
266	return nil
267}
268
269func (a *App) openFreshWindowWithTabsUC(ctx context.Context, url string, created *browserWindow) error {
270	log := logging.FromContext(ctx)
271	log.Debug().
272		Str("window_id", created.id).
273		Str("url_host", logging.SafeURLHost(url)).
274		Msg("ui: tabs use case create started for fresh window")
275	output, err := a.tabsUC.Create(ctx, usecase.CreateTabInput{
276		TabList:    a.tabs,
277		InitialURL: url,
278	})
279	if err != nil {
280		log.Warn().Err(err).
281			Str("window_id", created.id).
282			Msg("ui: tabs use case create failed for fresh window; rolling back window")
283		a.removeBrowserWindow(created.id)
284		if created.mainWindow != nil {
285			created.mainWindow.Destroy()
286		}
287		return err
288	}
289	log.Debug().
290		Str("window_id", created.id).
291		Str("tab_id", string(output.Tab.ID)).
292		Msg("ui: tabs use case create completed for fresh window")
293	a.setBrowserWindowForTab(output.Tab.ID, created)
294	// Mirror the tabsUC-created tab into the new browser window's tab list.
295	// This path uses the tabs usecase directly instead of tabCoord.Create,
296	// so the per-window TabList must be populated explicitly.
297	a.ensureTabListForBrowserWindow(created).Add(output.Tab)
298	if a.widgetFactory != nil || a.workspaceViewCreateOverride != nil {
299		a.createWorkspaceView(ctx, output.Tab)
300		a.switchWorkspaceView(ctx, output.Tab.ID)
301	}
302	if a.workspaceViews[output.Tab.ID] == nil {
303		log.Warn().
304			Str("window_id", created.id).
305			Str("tab_id", string(output.Tab.ID)).
306			Msg("ui: fresh window tabs use case tab has no workspace view; rolling back window")
307		if createdTabs := a.tabListForBrowserWindow(created); createdTabs != nil {
308			createdTabs.Remove(output.Tab.ID)
309		}
310		if a.tabs != nil {
311			a.tabs.Remove(output.Tab.ID)
312		}
313		if created.mainWindow != nil {
314			if tabBar := created.mainWindow.TabBar(); tabBar != nil {
315				tabBar.RemoveTab(output.Tab.ID)
316				if activeTab := a.activeTabForBrowserWindow(created); activeTab != nil {
317					tabBar.SetActive(activeTab.ID)
318				}
319			}
320		}
321		a.removeBrowserWindow(created.id)
322		if created.mainWindow != nil {
323			created.mainWindow.Destroy()
324		}
325		return fmt.Errorf("workspace view not created for tab %s", output.Tab.ID)
326	}
327	if created.mainWindow != nil {
328		created.mainWindow.Show()
329	}
330	return nil
331}

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