detached f7b7a96

fix: preserve session restore across CEF relaunch

commit
f7b7a96bc163e2b0cf8b1076711b7c8ab89790b0
author
brice · · 2026-04-15 06:10 UTC
parent
1fa5469
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +64 -45
M internal/infrastructure/cef/app.go +6 -8
@@ -177,13 +177,10 @@ func parseBrowseRelaunchCommandLine(commandLine purecef.CommandLine) (string, bo
177177 }
178178
179179 args := parseRelaunchCommandLineArgs(commandLine.GetCommandLineString())
180- if len(args) < 2 || args[1] != "browse" {
180+ if len(args) < 3 || args[1] != "browse" || args[2] == "" {
181181 return "", false
182182 }
183- if len(args) > 2 {
184- return args[2], true
185- }
186- return "", true
183+ return args[2], true
187184 }
188185
189186 func parseBrowseURLFromRelaunchCommandLine(commandLine purecef.CommandLine) string {
@@ -192,13 +189,14 @@ func parseBrowseURLFromRelaunchCommandLine(commandLine purecef.CommandLine) stri
192189 }
193190
194191 func (h *dumberBPH) OnAlreadyRunningAppRelaunch(commandLine purecef.CommandLine, _ string) int32 {
195- if h != nil && h.engine != nil {
196- if browseURL, ok := parseBrowseRelaunchCommandLine(commandLine); ok && h.engine.alreadyRunningAppRelaunchHandler != nil {
192+ if browseURL, ok := parseBrowseRelaunchCommandLine(commandLine); ok {
193+ if h != nil && h.engine != nil && h.engine.alreadyRunningAppRelaunchHandler != nil {
197194 h.engine.alreadyRunningAppRelaunchHandler(browseURL)
198195 }
196+ return 1
199197 }
200198
201- return 1
199+ return 0
202200 }
203201 func (h *dumberBPH) GetDefaultClient() purecef.Client { return nil }
204202 func (h *dumberBPH) GetDefaultRequestContextHandler() purecef.RequestContextHandler { return nil }
@@ -177,13 +177,10 @@ func parseBrowseRelaunchCommandLine(commandLine purecef.CommandLine) (string, bo
177 }177 }
178 178
179 args := parseRelaunchCommandLineArgs(commandLine.GetCommandLineString())179 args := parseRelaunchCommandLineArgs(commandLine.GetCommandLineString())
180- if len(args) < 2 || args[1] != "browse" {180+ if len(args) < 3 || args[1] != "browse" || args[2] == "" {
181 return "", false181 return "", false
182 }182 }
183- if len(args) > 2 {183+ return args[2], true
184- return args[2], true
185- }
186- return "", true
187 }184 }
188 185
189 func parseBrowseURLFromRelaunchCommandLine(commandLine purecef.CommandLine) string {186 func parseBrowseURLFromRelaunchCommandLine(commandLine purecef.CommandLine) string {
@@ -192,13 +189,14 @@ func parseBrowseURLFromRelaunchCommandLine(commandLine purecef.CommandLine) stri
192 }189 }
193 190
194 func (h *dumberBPH) OnAlreadyRunningAppRelaunch(commandLine purecef.CommandLine, _ string) int32 {191 func (h *dumberBPH) OnAlreadyRunningAppRelaunch(commandLine purecef.CommandLine, _ string) int32 {
195- if h != nil && h.engine != nil {192+ if browseURL, ok := parseBrowseRelaunchCommandLine(commandLine); ok {
196- if browseURL, ok := parseBrowseRelaunchCommandLine(commandLine); ok && h.engine.alreadyRunningAppRelaunchHandler != nil {193+ if h != nil && h.engine != nil && h.engine.alreadyRunningAppRelaunchHandler != nil {
197 h.engine.alreadyRunningAppRelaunchHandler(browseURL)194 h.engine.alreadyRunningAppRelaunchHandler(browseURL)
198 }195 }
196+ return 1
199 }197 }
200 198
201- return 1199+ return 0
202 }200 }
203 func (h *dumberBPH) GetDefaultClient() purecef.Client { return nil }201 func (h *dumberBPH) GetDefaultClient() purecef.Client { return nil }
204 func (h *dumberBPH) GetDefaultRequestContextHandler() purecef.RequestContextHandler { return nil }202 func (h *dumberBPH) GetDefaultRequestContextHandler() purecef.RequestContextHandler { return nil }
M internal/infrastructure/cef/app_test.go +7 -7
@@ -102,7 +102,7 @@ func TestDumberBPH_OnAlreadyRunningAppRelaunch_ForwardsBrowseURLAndReturns1(t *t
102102 }
103103 }
104104
105-func TestDumberBPH_OnAlreadyRunningAppRelaunch_ForwardsEmptyBrowseURLAndReturns1(t *testing.T) {
105+func TestDumberBPH_OnAlreadyRunningAppRelaunch_DoesNotForwardEmptyBrowseURLAndReturns0(t *testing.T) {
106106 t.Parallel()
107107
108108 gotURL := "sentinel"
@@ -115,11 +115,11 @@ func TestDumberBPH_OnAlreadyRunningAppRelaunch_ForwardsEmptyBrowseURLAndReturns1
115115 commandLineString: "dumber browse",
116116 }, "")
117117
118- if ret != 1 {
119- t.Fatalf("OnAlreadyRunningAppRelaunch returned %d, want 1", ret)
118+ if ret != 0 {
119+ t.Fatalf("OnAlreadyRunningAppRelaunch returned %d, want 0", ret)
120120 }
121- if gotURL != "" {
122- t.Fatalf("forwarded browse url = %q, want empty string", gotURL)
121+ if gotURL != "sentinel" {
122+ t.Fatalf("forwarded browse url = %q, want %q", gotURL, "sentinel")
123123 }
124124 }
125125
@@ -136,8 +136,8 @@ func TestDumberBPH_OnAlreadyRunningAppRelaunch_DoesNotForwardNonBrowse(t *testin
136136 commandLineString: "dumber",
137137 }, "")
138138
139- if ret != 1 {
140- t.Fatalf("OnAlreadyRunningAppRelaunch returned %d, want 1", ret)
139+ if ret != 0 {
140+ t.Fatalf("OnAlreadyRunningAppRelaunch returned %d, want 0", ret)
141141 }
142142 if called {
143143 t.Fatal("handler should not be called for non-browse relaunch")
@@ -102,7 +102,7 @@ func TestDumberBPH_OnAlreadyRunningAppRelaunch_ForwardsBrowseURLAndReturns1(t *t
102 }102 }
103 }103 }
104 104
105-func TestDumberBPH_OnAlreadyRunningAppRelaunch_ForwardsEmptyBrowseURLAndReturns1(t *testing.T) {105+func TestDumberBPH_OnAlreadyRunningAppRelaunch_DoesNotForwardEmptyBrowseURLAndReturns0(t *testing.T) {
106 t.Parallel()106 t.Parallel()
107 107
108 gotURL := "sentinel"108 gotURL := "sentinel"
@@ -115,11 +115,11 @@ func TestDumberBPH_OnAlreadyRunningAppRelaunch_ForwardsEmptyBrowseURLAndReturns1
115 commandLineString: "dumber browse",115 commandLineString: "dumber browse",
116 }, "")116 }, "")
117 117
118- if ret != 1 {118+ if ret != 0 {
119- t.Fatalf("OnAlreadyRunningAppRelaunch returned %d, want 1", ret)119+ t.Fatalf("OnAlreadyRunningAppRelaunch returned %d, want 0", ret)
120 }120 }
121- if gotURL != "" {121+ if gotURL != "sentinel" {
122- t.Fatalf("forwarded browse url = %q, want empty string", gotURL)122+ t.Fatalf("forwarded browse url = %q, want %q", gotURL, "sentinel")
123 }123 }
124 }124 }
125 125
@@ -136,8 +136,8 @@ func TestDumberBPH_OnAlreadyRunningAppRelaunch_DoesNotForwardNonBrowse(t *testin
136 commandLineString: "dumber",136 commandLineString: "dumber",
137 }, "")137 }, "")
138 138
139- if ret != 1 {139+ if ret != 0 {
140- t.Fatalf("OnAlreadyRunningAppRelaunch returned %d, want 1", ret)140+ t.Fatalf("OnAlreadyRunningAppRelaunch returned %d, want 0", ret)
141 }141 }
142 if called {142 if called {
143 t.Fatal("handler should not be called for non-browse relaunch")143 t.Fatal("handler should not be called for non-browse relaunch")
M internal/ui/app_browser_launch_test.go +39 -30
@@ -92,6 +92,25 @@ func setWindowTabBar(t *testing.T, mw *window.MainWindow, tabBar *component.TabB
9292 reflect.NewAt(fv.Type(), unsafe.Pointer(fv.UnsafeAddr())).Elem().Set(reflect.ValueOf(tabBar))
9393 }
9494
95+func newTestTabBarShell(t *testing.T, tabIDs ...entity.TabID) *component.TabBar {
96+ t.Helper()
97+
98+ tabBar := &component.TabBar{}
99+ buttons := make(map[entity.TabID]*component.TabButton, len(tabIDs))
100+ for _, tabID := range tabIDs {
101+ buttons[tabID] = &component.TabButton{}
102+ }
103+
104+ rv := reflect.ValueOf(tabBar).Elem()
105+ fv := rv.FieldByName("buttons")
106+ if !fv.IsValid() {
107+ t.Fatalf("TabBar missing buttons field")
108+ }
109+ reflect.NewAt(fv.Type(), unsafe.Pointer(fv.UnsafeAddr())).Elem().Set(reflect.ValueOf(buttons))
110+
111+ return tabBar
112+}
113+
95114 func windowTabBarActiveID(t *testing.T, mw *window.MainWindow) entity.TabID {
96115 t.Helper()
97116 if mw == nil || mw.TabBar() == nil {
@@ -110,10 +129,15 @@ func windowTitle(t *testing.T, mw *window.MainWindow) string {
110129
111130 func windowTabBarVisible(t *testing.T, mw *window.MainWindow) bool {
112131 t.Helper()
113- if mw == nil || mw.TabBar() == nil || mw.TabBar().Box() == nil {
132+ if mw == nil || mw.TabBar() == nil {
114133 return false
115134 }
116- return mw.TabBar().Box().GetVisible()
135+ rv := reflect.ValueOf(mw.TabBar()).Elem()
136+ fv := rv.FieldByName("visible")
137+ if !fv.IsValid() {
138+ t.Fatalf("TabBar missing visible field")
139+ }
140+ return *(*bool)(unsafe.Pointer(fv.UnsafeAddr()))
117141 }
118142
119143 func newTestShellToaster(t *testing.T) (*component.Toaster, *layoutmocks.MockBoxWidget, *layoutmocks.MockLabelWidget) {
@@ -282,25 +306,14 @@ func TestApp_OpenFreshWindowRollsBackOnTabCreationFailure(t *testing.T) {
282306 }
283307
284308 func TestApp_OpenFreshWindowTargetsNewWindowTabBar(t *testing.T) {
285- EnsureAdwaitaInitialized()
286- appID := AppID
287- gtkApp := gtk.NewApplication(&appID, gio.GApplicationNonUniqueValue)
288- if gtkApp == nil {
289- t.Fatal("gtk application creation failed")
290- }
291- defer gtkApp.Unref()
292-
293309 existingTabID := entity.TabID("existing-tab")
294- oldWindow, err := window.New(context.Background(), gtkApp, "top")
295- if err != nil {
296- t.Fatalf("old window creation failed: %v", err)
297- }
298- newWindow, err := window.New(context.Background(), gtkApp, "top")
299- if err != nil {
300- t.Fatalf("new window creation failed: %v", err)
301- }
310+ createdTabID := entity.TabID("id-1")
311+ oldWindow := &window.MainWindow{}
312+ newWindow := &window.MainWindow{}
313+ setWindowTabBar(t, oldWindow, newTestTabBarShell(t))
314+ setWindowTabBar(t, newWindow, newTestTabBarShell(t, createdTabID))
302315 tabs := entity.NewTabList()
303- tabsUC := usecase.NewManageTabsUseCase(func() string { return "id-1" })
316+ tabsUC := usecase.NewManageTabsUseCase(func() string { return string(createdTabID) })
304317
305318 app := &App{
306319 tabs: tabs,
@@ -327,17 +340,17 @@ func TestApp_OpenFreshWindowTargetsNewWindowTabBar(t *testing.T) {
327340 t.Fatalf("OpenFreshWindow returned error: %v", err)
328341 }
329342
330- createdTabID := app.tabs.ActiveTabID
331- if createdTabID == "" {
332- t.Fatalf("created tab id = %q, want non-empty", createdTabID)
343+ gotCreatedTabID := app.tabs.ActiveTabID
344+ if gotCreatedTabID == "" {
345+ t.Fatalf("created tab id = %q, want non-empty", gotCreatedTabID)
333346 }
334- if got := windowTabBarActiveID(t, newWindow); got != createdTabID {
335- t.Fatalf("new window tab bar active tab = %q, want %q", got, createdTabID)
347+ if got := windowTabBarActiveID(t, newWindow); got != gotCreatedTabID {
348+ t.Fatalf("new window tab bar active tab = %q, want %q", got, gotCreatedTabID)
336349 }
337350 if got := windowTabBarActiveID(t, oldWindow); got != "" {
338351 t.Fatalf("old window tab bar active tab = %q, want empty", got)
339352 }
340- if got := newWindow.TabBar().Box().GetVisible(); got {
353+ if got := windowTabBarVisible(t, newWindow); got {
341354 t.Fatalf("new window tab bar visible = %v, want false", got)
342355 }
343356 }
@@ -725,11 +738,7 @@ func TestApp_UpdateBrowserWindowTabBarVisibilityHonorsHideWhenSingleTabDisabled(
725738 defer gtkApp.Unref()
726739
727740 mainWindow := &window.MainWindow{}
728- tabBar := component.NewTabBar()
729- if tabBar == nil {
730- t.Fatal("tab bar creation failed")
731- }
732- tabBar.AddTab(entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("workspace-1"), entity.NewPane(entity.PaneID("pane-1"))))
741+ tabBar := newTestTabBarShell(t, entity.TabID("tab-1"))
733742 tabBar.SetVisible(false)
734743 setWindowTabBar(t, mainWindow, tabBar)
735744 bw := &browserWindow{id: "window-1", mainWindow: mainWindow}
@@ -92,6 +92,25 @@ func setWindowTabBar(t *testing.T, mw *window.MainWindow, tabBar *component.TabB
92 reflect.NewAt(fv.Type(), unsafe.Pointer(fv.UnsafeAddr())).Elem().Set(reflect.ValueOf(tabBar))92 reflect.NewAt(fv.Type(), unsafe.Pointer(fv.UnsafeAddr())).Elem().Set(reflect.ValueOf(tabBar))
93 }93 }
94 94
95+func newTestTabBarShell(t *testing.T, tabIDs ...entity.TabID) *component.TabBar {
96+ t.Helper()
97+
98+ tabBar := &component.TabBar{}
99+ buttons := make(map[entity.TabID]*component.TabButton, len(tabIDs))
100+ for _, tabID := range tabIDs {
101+ buttons[tabID] = &component.TabButton{}
102+ }
103+
104+ rv := reflect.ValueOf(tabBar).Elem()
105+ fv := rv.FieldByName("buttons")
106+ if !fv.IsValid() {
107+ t.Fatalf("TabBar missing buttons field")
108+ }
109+ reflect.NewAt(fv.Type(), unsafe.Pointer(fv.UnsafeAddr())).Elem().Set(reflect.ValueOf(buttons))
110+
111+ return tabBar
112+}
113+
95 func windowTabBarActiveID(t *testing.T, mw *window.MainWindow) entity.TabID {114 func windowTabBarActiveID(t *testing.T, mw *window.MainWindow) entity.TabID {
96 t.Helper()115 t.Helper()
97 if mw == nil || mw.TabBar() == nil {116 if mw == nil || mw.TabBar() == nil {
@@ -110,10 +129,15 @@ func windowTitle(t *testing.T, mw *window.MainWindow) string {
110 129
111 func windowTabBarVisible(t *testing.T, mw *window.MainWindow) bool {130 func windowTabBarVisible(t *testing.T, mw *window.MainWindow) bool {
112 t.Helper()131 t.Helper()
113- if mw == nil || mw.TabBar() == nil || mw.TabBar().Box() == nil {132+ if mw == nil || mw.TabBar() == nil {
114 return false133 return false
115 }134 }
116- return mw.TabBar().Box().GetVisible()135+ rv := reflect.ValueOf(mw.TabBar()).Elem()
136+ fv := rv.FieldByName("visible")
137+ if !fv.IsValid() {
138+ t.Fatalf("TabBar missing visible field")
139+ }
140+ return *(*bool)(unsafe.Pointer(fv.UnsafeAddr()))
117 }141 }
118 142
119 func newTestShellToaster(t *testing.T) (*component.Toaster, *layoutmocks.MockBoxWidget, *layoutmocks.MockLabelWidget) {143 func newTestShellToaster(t *testing.T) (*component.Toaster, *layoutmocks.MockBoxWidget, *layoutmocks.MockLabelWidget) {
@@ -282,25 +306,14 @@ func TestApp_OpenFreshWindowRollsBackOnTabCreationFailure(t *testing.T) {
282 }306 }
283 307
284 func TestApp_OpenFreshWindowTargetsNewWindowTabBar(t *testing.T) {308 func TestApp_OpenFreshWindowTargetsNewWindowTabBar(t *testing.T) {
285- EnsureAdwaitaInitialized()
286- appID := AppID
287- gtkApp := gtk.NewApplication(&appID, gio.GApplicationNonUniqueValue)
288- if gtkApp == nil {
289- t.Fatal("gtk application creation failed")
290- }
291- defer gtkApp.Unref()
292-
293 existingTabID := entity.TabID("existing-tab")309 existingTabID := entity.TabID("existing-tab")
294- oldWindow, err := window.New(context.Background(), gtkApp, "top")310+ createdTabID := entity.TabID("id-1")
295- if err != nil {311+ oldWindow := &window.MainWindow{}
296- t.Fatalf("old window creation failed: %v", err)312+ newWindow := &window.MainWindow{}
297- }313+ setWindowTabBar(t, oldWindow, newTestTabBarShell(t))
298- newWindow, err := window.New(context.Background(), gtkApp, "top")314+ setWindowTabBar(t, newWindow, newTestTabBarShell(t, createdTabID))
299- if err != nil {
300- t.Fatalf("new window creation failed: %v", err)
301- }
302 tabs := entity.NewTabList()315 tabs := entity.NewTabList()
303- tabsUC := usecase.NewManageTabsUseCase(func() string { return "id-1" })316+ tabsUC := usecase.NewManageTabsUseCase(func() string { return string(createdTabID) })
304 317
305 app := &App{318 app := &App{
306 tabs: tabs,319 tabs: tabs,
@@ -327,17 +340,17 @@ func TestApp_OpenFreshWindowTargetsNewWindowTabBar(t *testing.T) {
327 t.Fatalf("OpenFreshWindow returned error: %v", err)340 t.Fatalf("OpenFreshWindow returned error: %v", err)
328 }341 }
329 342
330- createdTabID := app.tabs.ActiveTabID343+ gotCreatedTabID := app.tabs.ActiveTabID
331- if createdTabID == "" {344+ if gotCreatedTabID == "" {
332- t.Fatalf("created tab id = %q, want non-empty", createdTabID)345+ t.Fatalf("created tab id = %q, want non-empty", gotCreatedTabID)
333 }346 }
334- if got := windowTabBarActiveID(t, newWindow); got != createdTabID {347+ if got := windowTabBarActiveID(t, newWindow); got != gotCreatedTabID {
335- t.Fatalf("new window tab bar active tab = %q, want %q", got, createdTabID)348+ t.Fatalf("new window tab bar active tab = %q, want %q", got, gotCreatedTabID)
336 }349 }
337 if got := windowTabBarActiveID(t, oldWindow); got != "" {350 if got := windowTabBarActiveID(t, oldWindow); got != "" {
338 t.Fatalf("old window tab bar active tab = %q, want empty", got)351 t.Fatalf("old window tab bar active tab = %q, want empty", got)
339 }352 }
340- if got := newWindow.TabBar().Box().GetVisible(); got {353+ if got := windowTabBarVisible(t, newWindow); got {
341 t.Fatalf("new window tab bar visible = %v, want false", got)354 t.Fatalf("new window tab bar visible = %v, want false", got)
342 }355 }
343 }356 }
@@ -725,11 +738,7 @@ func TestApp_UpdateBrowserWindowTabBarVisibilityHonorsHideWhenSingleTabDisabled(
725 defer gtkApp.Unref()738 defer gtkApp.Unref()
726 739
727 mainWindow := &window.MainWindow{}740 mainWindow := &window.MainWindow{}
728- tabBar := component.NewTabBar()741+ tabBar := newTestTabBarShell(t, entity.TabID("tab-1"))
729- if tabBar == nil {
730- t.Fatal("tab bar creation failed")
731- }
732- tabBar.AddTab(entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("workspace-1"), entity.NewPane(entity.PaneID("pane-1"))))
733 tabBar.SetVisible(false)742 tabBar.SetVisible(false)
734 setWindowTabBar(t, mainWindow, tabBar)743 setWindowTabBar(t, mainWindow, tabBar)
735 bw := &browserWindow{id: "window-1", mainWindow: mainWindow}744 bw := &browserWindow{id: "window-1", mainWindow: mainWindow}
M internal/ui/component/tab_bar.go +3 -0
@@ -11,6 +11,7 @@ import (
1111 type TabBar struct {
1212 box *gtk.Box
1313 buttons map[entity.TabID]*TabButton
14+ visible bool
1415
1516 // Active tab tracking
1617 activeTabID entity.TabID
@@ -27,6 +28,7 @@ type TabBar struct {
2728 func NewTabBar() *TabBar {
2829 tb := &TabBar{
2930 buttons: make(map[entity.TabID]*TabButton),
31+ visible: true,
3032 }
3133
3234 // Create horizontal box container
@@ -159,6 +161,7 @@ func (tb *TabBar) ActiveTabID() entity.TabID {
159161
160162 // SetVisible shows or hides the tab bar.
161163 func (tb *TabBar) SetVisible(visible bool) {
164+ tb.visible = visible
162165 if tb.box == nil {
163166 return
164167 }
@@ -11,6 +11,7 @@ import (
11 type TabBar struct {11 type TabBar struct {
12 box *gtk.Box12 box *gtk.Box
13 buttons map[entity.TabID]*TabButton13 buttons map[entity.TabID]*TabButton
14+ visible bool
14 15
15 // Active tab tracking16 // Active tab tracking
16 activeTabID entity.TabID17 activeTabID entity.TabID
@@ -27,6 +28,7 @@ type TabBar struct {
27 func NewTabBar() *TabBar {28 func NewTabBar() *TabBar {
28 tb := &TabBar{29 tb := &TabBar{
29 buttons: make(map[entity.TabID]*TabButton),30 buttons: make(map[entity.TabID]*TabButton),
31+ visible: true,
30 }32 }
31 33
32 // Create horizontal box container34 // Create horizontal box container
@@ -159,6 +161,7 @@ func (tb *TabBar) ActiveTabID() entity.TabID {
159 161
160 // SetVisible shows or hides the tab bar.162 // SetVisible shows or hides the tab bar.
161 func (tb *TabBar) SetVisible(visible bool) {163 func (tb *TabBar) SetVisible(visible bool) {
164+ tb.visible = visible
162 if tb.box == nil {165 if tb.box == nil {
163 return166 return
164 }167 }
M internal/ui/component/tab_button.go +3 -0
@@ -85,6 +85,9 @@ func (tb *TabButton) SetActive(active bool) {
8585 return
8686 }
8787 tb.isActive = active
88+ if tb.button == nil {
89+ return
90+ }
8891
8992 if active {
9093 tb.button.AddCssClass("tab-button-active")
@@ -85,6 +85,9 @@ func (tb *TabButton) SetActive(active bool) {
85 return85 return
86 }86 }
87 tb.isActive = active87 tb.isActive = active
88+ if tb.button == nil {
89+ return
90+ }
88 91
89 if active {92 if active {
90 tb.button.AddCssClass("tab-button-active")93 tb.button.AddCssClass("tab-button-active")
M internal/ui/window/main_window.go +6 -0
@@ -119,11 +119,17 @@ func (mw *MainWindow) assembleLayout() {
119119
120120 // Show presents the window.
121121 func (mw *MainWindow) Show() {
122+ if mw == nil || mw.window == nil {
123+ return
124+ }
122125 mw.window.Present()
123126 }
124127
125128 // Close closes the window.
126129 func (mw *MainWindow) Close() {
130+ if mw == nil || mw.window == nil {
131+ return
132+ }
127133 mw.window.Close()
128134 }
129135
@@ -119,11 +119,17 @@ func (mw *MainWindow) assembleLayout() {
119 119
120 // Show presents the window.120 // Show presents the window.
121 func (mw *MainWindow) Show() {121 func (mw *MainWindow) Show() {
122+ if mw == nil || mw.window == nil {
123+ return
124+ }
122 mw.window.Present()125 mw.window.Present()
123 }126 }
124 127
125 // Close closes the window.128 // Close closes the window.
126 func (mw *MainWindow) Close() {129 func (mw *MainWindow) Close() {
130+ if mw == nil || mw.window == nil {
131+ return
132+ }
127 mw.window.Close()133 mw.window.Close()
128 }134 }
129 135

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