v0.30.0 eb3068f

pane_window_eject.go RawHistoryFolder

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

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