pane_window_eject.go RawHistoryFolder

1package ui
2
3import (
4	"context"
5	"fmt"
6	"maps"
7	"strings"
8
9	"github.com/bnema/dumber/internal/application/usecase"
10	"github.com/bnema/dumber/internal/domain/entity"
11	"github.com/bnema/dumber/internal/ui/component"
12)
13
14func (a *App) createEmptyBrowserWindow(ctx context.Context) (*browserWindow, error) {
15	created, err := a.createBrowserWindow(ctx, "")
16	if err != nil {
17		return nil, err
18	}
19	a.registerBrowserWindow(created)
20	return created, nil
21}
22
23func (a *App) cleanupEjectTargetWindow(bw *browserWindow) {
24	if bw == nil {
25		return
26	}
27	if bw.tabs != nil {
28		for _, tab := range bw.tabs.Tabs {
29			if tab == nil {
30				continue
31			}
32			delete(a.windowForTab, tab.ID)
33			delete(a.workspaceViews, tab.ID)
34			if a.tabs != nil {
35				a.tabs.Remove(tab.ID)
36			}
37		}
38	}
39	a.removeBrowserWindow(bw.id)
40	if bw.mainWindow != nil {
41		bw.mainWindow.Destroy()
42	}
43}
44
45type ejectRollbackSnapshot struct {
46	sourceTabs     *entity.TabList
47	sourceSnapshot *entity.TabList
48	targetTabs     *entity.TabList
49	targetSnapshot *entity.TabList
50	globalSnapshot *entity.TabList
51	windowForTab   map[entity.TabID]*browserWindow
52	workspaceViews map[entity.TabID]*component.WorkspaceView
53}
54
55func (a *App) captureEjectRollbackSnapshot(sourceTabs, targetTabs *entity.TabList) ejectRollbackSnapshot {
56	return ejectRollbackSnapshot{
57		sourceTabs:     sourceTabs,
58		sourceSnapshot: sourceTabs.Snapshot(),
59		targetTabs:     targetTabs,
60		targetSnapshot: targetTabs.Snapshot(),
61		globalSnapshot: a.tabs.Snapshot(),
62		windowForTab:   cloneWindowForTabMap(a.windowForTab),
63		workspaceViews: cloneWorkspaceViewsMap(a.workspaceViews),
64	}
65}
66
67func (a *App) restoreEjectSnapshots(snapshot ejectRollbackSnapshot) {
68	if snapshot.sourceTabs != nil {
69		snapshot.sourceTabs.ReplaceFrom(snapshot.sourceSnapshot)
70	}
71	if snapshot.targetTabs != nil {
72		snapshot.targetTabs.ReplaceFrom(snapshot.targetSnapshot)
73	}
74	if a.tabs != nil {
75		a.tabs.ReplaceFrom(snapshot.globalSnapshot)
76	}
77	a.windowForTab = snapshot.windowForTab
78	a.workspaceViews = snapshot.workspaceViews
79}
80
81func (a *App) rollbackEjectTargetUIFailure(targetWindow *browserWindow, snapshot ejectRollbackSnapshot) error {
82	a.restoreEjectSnapshots(snapshot)
83	a.cleanupEjectTargetWindow(targetWindow)
84	return fmt.Errorf("failed to create target workspace view")
85}
86
87func cloneWindowForTabMap(source map[entity.TabID]*browserWindow) map[entity.TabID]*browserWindow {
88	cloned := make(map[entity.TabID]*browserWindow, len(source))
89	maps.Copy(cloned, source)
90	return cloned
91}
92
93func cloneWorkspaceViewsMap(source map[entity.TabID]*component.WorkspaceView) map[entity.TabID]*component.WorkspaceView {
94	cloned := make(map[entity.TabID]*component.WorkspaceView, len(source))
95	maps.Copy(cloned, source)
96	return cloned
97}
98
99func (a *App) activePaneEjectSource(requestedPaneID entity.PaneID) (*browserWindow, *entity.TabList, *entity.Tab, entity.PaneID) {
100	sourceWindow := a.lastFocusedBrowserWindow()
101	sourceTabs := a.tabListForBrowserWindow(sourceWindow)
102	sourceTab := a.activeTabForBrowserWindow(sourceWindow)
103	if sourceWindow == nil || sourceTabs == nil || sourceTab == nil || sourceTab.Workspace == nil {
104		return nil, nil, nil, ""
105	}
106	sourcePaneID := sourceTab.Workspace.ActivePaneID
107	if requestedPaneID != "" && requestedPaneID != sourcePaneID {
108		return nil, nil, nil, ""
109	}
110	sourceNode := sourceTab.Workspace.FindPane(sourcePaneID)
111	if sourcePaneID == "" || sourceNode == nil || sourceNode.Pane == nil || !isEjectableWorkspacePane(sourceNode.Pane) {
112		return nil, nil, nil, ""
113	}
114	return sourceWindow, sourceTabs, sourceTab, sourcePaneID
115}
116
117func isEjectableWorkspacePane(pane *entity.Pane) bool {
118	if pane == nil {
119		return false
120	}
121	return pane.WindowType == entity.WindowMain && !strings.HasPrefix(pane.URI, "dumb://")
122}
123
124func (a *App) EjectActivePaneToWindow(ctx context.Context, requestedPaneID entity.PaneID) error {
125	if a.extractPaneToTabListUC == nil {
126		return nil
127	}
128
129	sourceWindow, sourceTabs, sourceTab, sourcePaneID := a.activePaneEjectSource(requestedPaneID)
130	if sourceWindow == nil {
131		return nil
132	}
133
134	targetWindow, err := a.createEmptyBrowserWindow(ctx)
135	if err != nil {
136		return err
137	}
138	targetTabs := a.ensureTabListForBrowserWindow(targetWindow)
139	if targetTabs == nil {
140		a.cleanupEjectTargetWindow(targetWindow)
141		return fmt.Errorf("target tab list is required")
142	}
143
144	rollbackSnapshot := a.captureEjectRollbackSnapshot(sourceTabs, targetTabs)
145
146	out, err := a.extractPaneToTabListUC.Execute(usecase.ExtractPaneToTabListInput{
147		SourceTabs:   sourceTabs,
148		SourceTabID:  sourceTab.ID,
149		SourcePaneID: sourcePaneID,
150		TargetTabs:   targetTabs,
151	})
152	if err != nil {
153		a.cleanupEjectTargetWindow(targetWindow)
154		return err
155	}
156	if out == nil || out.NewTab == nil {
157		a.cleanupEjectTargetWindow(targetWindow)
158		return nil
159	}
160
161	a.setBrowserWindowForTab(out.NewTab.ID, targetWindow)
162	a.syncDerivedGlobalTabMirror(out.NewTab)
163	if a.tabs != nil {
164		a.tabs.SetActive(out.NewTab.ID)
165	}
166	a.ensureTargetTabUI(ctx, out.NewTab, targetWindow)
167	if a.workspaceViews[out.NewTab.ID] == nil {
168		return a.rollbackEjectTargetUIFailure(targetWindow, rollbackSnapshot)
169	}
170
171	if out.SourceTabClosed {
172		a.removeSourceTabUI(sourceTab.ID, sourceWindow)
173		delete(a.windowForTab, sourceTab.ID)
174		a.switchSourceWindowToActiveTab(ctx, sourceWindow, sourceTabs)
175	} else {
176		a.rebuildAndAttachWorkspace(ctx, sourceTab.ID, sourceTab)
177	}
178
179	a.rebuildAndAttachWorkspace(ctx, out.NewTab.ID, out.NewTab)
180	if a.contentCoord != nil && out.NewTab.Workspace != nil {
181		a.contentCoord.SyncWebViewViewport(ctx, out.NewTab.Workspace.ActivePaneID, "eject-pane-to-window")
182	}
183
184	a.updateEjectWindowChrome(ctx, sourceWindow, targetWindow, targetTabs, out.NewTab.ID)
185
186	if out.SourceTabClosed && sourceTabs.Count() == 0 {
187		a.removeEmptySourceBrowserWindow(sourceWindow)
188	}
189
190	targetWindows := a.orderedBrowserWindowsForSnapshot()
191	a.replaceGlobalTabsFromRuntimeWindows(targetWindows, targetWindow)
192
193	a.activateBrowserWindow(targetWindow)
194	a.switchWorkspaceView(ctx, out.NewTab.ID)
195	if targetWindow.mainWindow != nil {
196		targetWindow.mainWindow.Show()
197	}
198	a.MarkDirty()
199	return nil
200}
201
202func (a *App) updateEjectWindowChrome(
203	ctx context.Context,
204	sourceWindow, targetWindow *browserWindow,
205	targetTabs *entity.TabList,
206	newTabID entity.TabID,
207) {
208	targetTabs.SetActive(newTabID)
209	if tabBar := a.tabBarForBrowserWindow(targetWindow); tabBar != nil {
210		tabBar.SetActive(newTabID)
211	}
212	if a.tabCoord != nil {
213		a.tabCoord.UpdateBarVisibility(ctx, a.tabTargetForBrowserWindow(sourceWindow))
214		a.tabCoord.UpdateBarVisibility(ctx, a.tabTargetForBrowserWindow(targetWindow))
215	}
216	a.updateBrowserWindowTabBarVisibility(sourceWindow)
217	a.updateBrowserWindowTabBarVisibility(targetWindow)
218}
219
220func (a *App) switchSourceWindowToActiveTab(ctx context.Context, bw *browserWindow, tabs *entity.TabList) {
221	if bw == nil || tabs == nil || tabs.Count() == 0 {
222		return
223	}
224	activeTab := tabs.ActiveTab()
225	if activeTab == nil {
226		return
227	}
228	if tabBar := a.tabBarForBrowserWindow(bw); tabBar != nil {
229		tabBar.SetActive(activeTab.ID)
230	}
231	a.switchWorkspaceView(ctx, activeTab.ID)
232}
233
234func (a *App) removeEmptySourceBrowserWindow(bw *browserWindow) {
235	if bw == nil || a.tabCountForBrowserWindow(bw) != 0 {
236		return
237	}
238	a.removeBrowserWindow(bw.id)
239	if bw.mainWindow != nil {
240		bw.mainWindow.Destroy()
241	}
242}
243
244func (a *App) orderedBrowserWindowsForSnapshot() []*browserWindow {
245	windows := make([]*browserWindow, 0, len(a.browserWindows))
246	for _, id := range a.windowOrder() {
247		if bw := a.browserWindows[id]; bw != nil {
248			windows = append(windows, bw)
249		}
250	}
251	return windows
252}

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