v0.13.0 976176a

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

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