v0.14.0 76d1892

workspace_pane_mode.go RawHistoryFolder

1// workspace_pane_mode.go - Backend-driven pane mode implementation
2package browser
3
4import (
5	"log"
6
7	"github.com/bnema/dumber/pkg/webkit"
8	"github.com/diamondburned/gotk4/pkg/gtk/v4"
9)
10
11// EnterPaneMode activates pane mode on the currently focused pane
12// This is called directly from the keyboard bridge, not from JavaScript
13func (wm *WorkspaceManager) EnterPaneMode() {
14	// Note: Caller (RegisterPaneModeHandler) ensures this is called on app.workspace (active tab)
15
16	wm.paneMutex.Lock()
17	defer wm.paneMutex.Unlock()
18
19	// Get the currently focused pane
20	activeNode := wm.GetActiveNode()
21	if activeNode == nil {
22		log.Printf("[pane-mode] Cannot enter pane mode: no active pane")
23		return
24	}
25
26	// Already in pane mode on this pane?
27	if wm.paneModeActive && wm.paneModeActivePane == activeNode {
28		log.Printf("[pane-mode] Pane mode already active on this pane")
29		return
30	}
31
32	log.Printf("[pane-mode] Entering pane mode on pane %p (workspace=%p)", activeNode, wm)
33
34	wm.paneModeActive = true
35	wm.paneModeActivePane = activeNode
36
37	if activeNode.pane != nil && activeNode.pane.webView != nil {
38		wm.paneModeSource = activeNode.pane.webView
39	}
40
41	// Apply visual border to workspace root
42	wm.applyPaneModeBorder()
43
44	// Notify JavaScript for UI feedback (visual indicators)
45	wm.dispatchPaneModeEvent("entered", "")
46}
47
48// ExitPaneMode deactivates pane mode
49func (wm *WorkspaceManager) ExitPaneMode(reason string) {
50	wm.paneMutex.Lock()
51	defer wm.paneMutex.Unlock()
52
53	if !wm.paneModeActive {
54		return
55	}
56
57	log.Printf("[pane-mode] Exiting pane mode: %s", reason)
58
59	wm.paneModeActive = false
60	wm.paneModeActivePane = nil
61	wm.paneModeSource = nil
62
63	// Remove visual border from workspace root
64	wm.removePaneModeBorder()
65
66	// Notify JavaScript
67	wm.dispatchPaneModeEvent("exited", reason)
68}
69
70// HandlePaneAction processes pane mode actions (close, split, etc.)
71// Called from keyboard bridge when user presses action keys in pane mode
72func (wm *WorkspaceManager) HandlePaneAction(action string) {
73	// Note: No IsInActiveTab check here - caller (RegisterPaneModeHandler) ensures
74	// this is called on app.workspace (active tab's workspace)
75
76	wm.paneMutex.Lock()
77
78	if !wm.paneModeActive {
79		wm.paneMutex.Unlock()
80		log.Printf("[pane-mode] Ignoring action '%s': pane mode not active", action)
81		return
82	}
83
84	activePane := wm.paneModeActivePane
85	wm.paneMutex.Unlock()
86
87	if activePane == nil {
88		log.Printf("[pane-mode] Ignoring action '%s': no active pane", action)
89		wm.ExitPaneMode("no-active-pane")
90		return
91	}
92
93	log.Printf("[pane-mode] Handling action '%s' on pane %p", action, activePane)
94
95	switch action {
96	case "close":
97		wm.handlePaneClose(activePane)
98	case "split-right", "split-left", "split-up", "split-down":
99		wm.handlePaneSplit(activePane, action)
100	case "stack":
101		wm.handlePaneStack(activePane)
102	default:
103		log.Printf("[pane-mode] Unknown action: %s", action)
104	}
105
106	// Exit pane mode after action
107	wm.ExitPaneMode(action)
108}
109
110// handlePaneClose closes the pane that has pane mode active
111func (wm *WorkspaceManager) handlePaneClose(node *paneNode) {
112	if node == nil {
113		return
114	}
115
116	log.Printf("[pane-mode] Closing pane %p", node)
117
118	// Notify JavaScript first so it can show toast
119	wm.dispatchPaneModeEvent("action", "close")
120
121	// Close the pane
122	if err := wm.ClosePane(node); err != nil {
123		log.Printf("[pane-mode] Failed to close pane: %v", err)
124	}
125}
126
127// handlePaneSplit splits the pane in the specified direction
128func (wm *WorkspaceManager) handlePaneSplit(node *paneNode, action string) {
129	if node == nil {
130		return
131	}
132
133	// Extract direction from action (e.g., "split-right" -> DirectionRight)
134	var direction string
135	switch action {
136	case "split-right":
137		direction = DirectionRight
138	case "split-left":
139		direction = DirectionLeft
140	case "split-up":
141		direction = DirectionUp
142	case "split-down":
143		direction = DirectionDown
144	default:
145		log.Printf("[pane-mode] Invalid split action: %s", action)
146		return
147	}
148
149	log.Printf("[pane-mode] Splitting pane %p in direction %s", node, direction)
150
151	// Notify JavaScript first so it can show toast
152	wm.dispatchPaneModeEvent("action", action)
153
154	// Perform the split (pass nil so it creates a fresh pane, not popup mode)
155	newNode, err := wm.splitNode(node, direction, nil)
156	if err != nil {
157		log.Printf("[pane-mode] Failed to split pane: %v", err)
158		return
159	}
160
161	// Load initial URL in the new pane so scripts execute
162	wm.clonePaneState(node, newNode)
163
164	log.Printf("[pane-mode] Split successful: direction=%s", direction)
165}
166
167// handlePaneStack creates a stacked pane
168func (wm *WorkspaceManager) handlePaneStack(node *paneNode) {
169	if node == nil {
170		return
171	}
172
173	log.Printf("[pane-mode] Stacking pane %p", node)
174
175	// Notify JavaScript first so it can show toast
176	wm.dispatchPaneModeEvent("action", "stack")
177
178	// Perform the stack operation
179	newNode, err := wm.stackedPaneManager.StackPane(node)
180	if err != nil {
181		log.Printf("[pane-mode] Failed to stack pane: %v", err)
182		return
183	}
184
185	// Load initial URL in the new pane so scripts execute
186	wm.clonePaneState(node, newNode)
187
188	log.Printf("[pane-mode] Stack successful")
189}
190
191// dispatchPaneModeEvent sends a pane mode event to JavaScript for UI updates
192func (wm *WorkspaceManager) dispatchPaneModeEvent(event string, detail string) {
193	if wm.paneModeActivePane == nil || wm.paneModeActivePane.pane == nil || wm.paneModeActivePane.pane.webView == nil {
194		return
195	}
196
197	view := wm.paneModeActivePane.pane.webView
198	data := map[string]interface{}{
199		"event":  event,
200		"detail": detail,
201	}
202
203	if err := view.DispatchCustomEvent("dumber:pane-mode", data); err != nil {
204		log.Printf("[pane-mode] Failed to dispatch event: %v", err)
205	}
206}
207
208// applyPaneModeBorder applies pane mode visual indicator using GTK margins
209func (wm *WorkspaceManager) applyPaneModeBorder() {
210	window := wm.resolveWindow()
211	if window == nil {
212		log.Printf("[pane-mode] Cannot apply border: missing window reference")
213		return
214	}
215
216	// Determine which container to apply margins to:
217	// - If tab manager exists, apply to its root container (direct child of window)
218	//   so margins reveal the window background color
219	// - Otherwise, apply to workspace root (direct child of window)
220	var targetContainer gtk.Widgetter
221	if wm.app != nil && wm.app.tabManager != nil {
222		if rootContainer := wm.app.tabManager.GetRootContainer(); rootContainer != nil {
223			// Tab environment: apply margins to the root container that's attached to the window
224			targetContainer = rootContainer
225			log.Printf("[pane-mode] Using tab manager root container for border")
226		} else if wm.app.tabManager.ContentArea != nil {
227			// Fallback: older builds where root container isn't available yet
228			targetContainer = wm.app.tabManager.ContentArea
229			log.Printf("[pane-mode] Using tab manager content area fallback for border")
230		}
231	} else if wm.root != nil && wm.root.container != nil {
232		// Non-tab environment: apply margins to workspace root
233		targetContainer = wm.root.container
234		log.Printf("[pane-mode] Using workspace root for border")
235	} else {
236		return
237	}
238
239	// Save the container reference so we can remove margins from it later
240	wm.paneModeContainer = targetContainer
241
242	// Apply 4px margins to create space for the border
243	webkit.WidgetSetMargin(targetContainer, 4)
244
245	// Add CSS class to window for background color (the "border" color shows in the margin space)
246	webkit.WidgetAddCSSClass(window.AsWindow(), "pane-mode-active")
247
248	// Force resize/allocation to apply margin changes immediately
249	webkit.WidgetQueueResize(targetContainer)
250	webkit.WidgetQueueAllocate(targetContainer)
251
252	// Queue redraw to show changes
253	webkit.WidgetQueueDraw(window.AsWindow())
254	webkit.WidgetQueueDraw(targetContainer)
255
256	log.Printf("[pane-mode] Applied border using margins (container=%p)", targetContainer)
257}
258
259// removePaneModeBorder removes the pane mode visual indicator
260func (wm *WorkspaceManager) removePaneModeBorder() {
261	window := wm.resolveWindow()
262	if window == nil {
263		return
264	}
265
266	// Use the saved container reference (the one that had margins applied)
267	// This is important because wm.root.container may have changed due to splits
268	container := wm.paneModeContainer
269	if container == nil {
270		log.Printf("[pane-mode] Warning: no saved container, trying current root")
271		if wm.root == nil || wm.root.container == nil {
272			return
273		}
274		container = wm.root.container
275	}
276
277	// Remove margins from the container that had them applied
278	webkit.WidgetSetMargin(container, 0)
279
280	// Remove CSS class from window
281	webkit.WidgetRemoveCSSClass(window.AsWindow(), "pane-mode-active")
282
283	// Force resize/allocation to apply margin changes immediately
284	webkit.WidgetQueueResize(container)
285	webkit.WidgetQueueAllocate(container)
286
287	// Queue redraw to show changes
288	webkit.WidgetQueueDraw(window.AsWindow())
289	webkit.WidgetQueueDraw(container)
290
291	// Clear the saved container reference
292	wm.paneModeContainer = nil
293
294	log.Printf("[pane-mode] Removed border from workspace root (container=%p)", container)
295}

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