v0.15.0 9006957

workspace_pane_mode.go RawHistoryFolder

1// workspace_pane_mode.go - Backend-driven pane mode implementation
2package browser
3
4import (
5	"fmt"
6
7	"github.com/bnema/dumber/internal/logging"
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	// Note: Caller (RegisterPaneModeHandler) ensures this is called on app.workspace (active tab)
14
15	wm.paneMutex.Lock()
16	defer wm.paneMutex.Unlock()
17
18	// Get the currently focused pane
19	activeNode := wm.GetActiveNode()
20	if activeNode == nil {
21		logging.Error(fmt.Sprintf("[pane-mode] Cannot enter pane mode: no active pane"))
22		return
23	}
24
25	// Already in pane mode on this pane?
26	if wm.paneModeActive && wm.paneModeActivePane == activeNode {
27		logging.Info(fmt.Sprintf("[pane-mode] Pane mode already active on this pane"))
28		return
29	}
30
31	logging.Debug(fmt.Sprintf("[pane-mode] Entering pane mode on pane %p (workspace=%p)", activeNode, wm))
32
33	wm.paneModeActive = true
34	wm.paneModeActivePane = activeNode
35
36	if activeNode.pane != nil && activeNode.pane.webView != nil {
37		wm.paneModeSource = activeNode.pane.webView
38	}
39
40	// Apply visual border to workspace root
41	wm.applyPaneModeBorder()
42
43	// Notify JavaScript for UI feedback (visual indicators)
44	wm.dispatchPaneModeEvent("entered", "")
45}
46
47// ExitPaneMode deactivates pane mode
48func (wm *WorkspaceManager) ExitPaneMode(reason string) {
49	wm.paneMutex.Lock()
50	defer wm.paneMutex.Unlock()
51
52	if !wm.paneModeActive {
53		return
54	}
55
56	logging.Info(fmt.Sprintf("[pane-mode] Exiting pane mode: %s", reason))
57
58	wm.paneModeActive = false
59	wm.paneModeActivePane = nil
60	wm.paneModeSource = nil
61
62	// Remove visual border from workspace root
63	wm.removePaneModeBorder()
64
65	// Notify JavaScript
66	wm.dispatchPaneModeEvent("exited", reason)
67}
68
69// HandlePaneAction processes pane mode actions (close, split, etc.)
70// Called from keyboard bridge when user presses action keys in pane mode
71func (wm *WorkspaceManager) HandlePaneAction(action string) {
72	// Note: No IsInActiveTab check here - caller (RegisterPaneModeHandler) ensures
73	// this is called on app.workspace (active tab's workspace)
74
75	wm.paneMutex.Lock()
76
77	if !wm.paneModeActive {
78		wm.paneMutex.Unlock()
79		logging.Info(fmt.Sprintf("[pane-mode] Ignoring action '%s': pane mode not active", action))
80		return
81	}
82
83	activePane := wm.paneModeActivePane
84	wm.paneMutex.Unlock()
85
86	if activePane == nil {
87		logging.Info(fmt.Sprintf("[pane-mode] Ignoring action '%s': no active pane", action))
88		wm.ExitPaneMode("no-active-pane")
89		return
90	}
91
92	logging.Debug(fmt.Sprintf("[pane-mode] Handling action '%s' on pane %p", action, activePane))
93
94	switch action {
95	case "close":
96		wm.handlePaneClose(activePane)
97	case "split-right", "split-left", "split-up", "split-down":
98		wm.handlePaneSplit(activePane, action)
99	case "stack":
100		wm.handlePaneStack(activePane)
101	default:
102		logging.Warn(fmt.Sprintf("[pane-mode] Unknown action: %s", action))
103	}
104
105	// Exit pane mode after action
106	wm.ExitPaneMode(action)
107}
108
109// handlePaneClose closes the pane that has pane mode active
110func (wm *WorkspaceManager) handlePaneClose(node *paneNode) {
111	if node == nil {
112		return
113	}
114
115	logging.Debug(fmt.Sprintf("[pane-mode] Closing pane %p", node))
116
117	// Notify JavaScript first so it can show toast
118	wm.dispatchPaneModeEvent("action", "close")
119
120	// Close the pane
121	if err := wm.ClosePane(node); err != nil {
122		logging.Error(fmt.Sprintf("[pane-mode] Failed to close pane: %v", err))
123	}
124}
125
126// handlePaneSplit splits the pane in the specified direction
127func (wm *WorkspaceManager) handlePaneSplit(node *paneNode, action string) {
128	if node == nil {
129		return
130	}
131
132	// Extract direction from action (e.g., "split-right" -> DirectionRight)
133	var direction string
134	switch action {
135	case "split-right":
136		direction = DirectionRight
137	case "split-left":
138		direction = DirectionLeft
139	case "split-up":
140		direction = DirectionUp
141	case "split-down":
142		direction = DirectionDown
143	default:
144		logging.Error(fmt.Sprintf("[pane-mode] Invalid split action: %s", action))
145		return
146	}
147
148	logging.Debug(fmt.Sprintf("[pane-mode] Splitting pane %p in direction %s", node, direction))
149
150	// Notify JavaScript first so it can show toast
151	wm.dispatchPaneModeEvent("action", action)
152
153	// Perform the split (pass nil so it creates a fresh pane, not popup mode)
154	newNode, err := wm.splitNode(node, direction, nil)
155	if err != nil {
156		logging.Error(fmt.Sprintf("[pane-mode] Failed to split pane: %v", err))
157		return
158	}
159
160	// Load initial URL in the new pane so scripts execute
161	wm.clonePaneState(node, newNode)
162
163	logging.Debug(fmt.Sprintf("[pane-mode] Split successful: direction=%s", direction))
164}
165
166// handlePaneStack creates a stacked pane
167func (wm *WorkspaceManager) handlePaneStack(node *paneNode) {
168	if node == nil {
169		return
170	}
171
172	logging.Debug(fmt.Sprintf("[pane-mode] Stacking pane %p", node))
173
174	// Notify JavaScript first so it can show toast
175	wm.dispatchPaneModeEvent("action", "stack")
176
177	// Perform the stack operation
178	newNode, err := wm.stackedPaneManager.StackPane(node)
179	if err != nil {
180		logging.Error(fmt.Sprintf("[pane-mode] Failed to stack pane: %v", err))
181		return
182	}
183
184	// Load initial URL in the new pane so scripts execute
185	wm.clonePaneState(node, newNode)
186
187	logging.Info(fmt.Sprintf("[pane-mode] Stack successful"))
188}
189
190// dispatchPaneModeEvent sends a pane mode event to JavaScript for UI updates
191func (wm *WorkspaceManager) dispatchPaneModeEvent(event string, detail string) {
192	if wm.paneModeActivePane == nil || wm.paneModeActivePane.pane == nil || wm.paneModeActivePane.pane.webView == nil {
193		return
194	}
195
196	view := wm.paneModeActivePane.pane.webView
197	data := map[string]interface{}{
198		"event":  event,
199		"detail": detail,
200	}
201
202	if err := view.DispatchCustomEvent("dumber:pane-mode", data); err != nil {
203		logging.Error(fmt.Sprintf("[pane-mode] Failed to dispatch event: %v", err))
204	}
205}
206
207// applyPaneModeBorder applies pane mode visual indicator using overlay
208func (wm *WorkspaceManager) applyPaneModeBorder() {
209	if wm.app != nil && wm.app.tabManager != nil {
210		wm.app.tabManager.showPaneModeBorder()
211	}
212}
213
214// removePaneModeBorder removes the pane mode visual indicator
215func (wm *WorkspaceManager) removePaneModeBorder() {
216	if wm.app != nil && wm.app.tabManager != nil {
217		wm.app.tabManager.hidePaneModeBorder()
218	}
219}

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