v0.14.1 0da30ba

tab_mode.go RawHistoryFolder

1package browser
2
3import (
4	"fmt"
5	"time"
6
7	"github.com/bnema/dumber/internal/logging"
8	"github.com/bnema/dumber/pkg/webkit"
9	"github.com/diamondburned/gotk4/pkg/gtk/v4"
10)
11
12// EnterTabMode activates the modal tab management mode.
13// This is triggered by Alt+T and provides visual feedback and temporary key bindings.
14func (tm *TabManager) EnterTabMode() {
15	logging.Debug("[tab-mode] EnterTabMode called - attempting to acquire lock")
16	tm.mu.Lock()
17	defer tm.mu.Unlock()
18	logging.Debug("[tab-mode] Lock acquired")
19
20	// Already in tab mode?
21	if tm.tabModeActive {
22		logging.Debug("[tab-mode] Tab mode already active")
23		return
24	}
25
26	logging.Info("[tab-mode] Entering tab mode")
27
28	logging.Debug("[tab-mode] Setting tabModeActive flag")
29	tm.tabModeActive = true
30
31	// Apply visual border indicator
32	logging.Debug("[tab-mode] About to call applyTabModeBorder()")
33	tm.applyTabModeBorder()
34	logging.Debug("[tab-mode] Returned from applyTabModeBorder()")
35
36	// Notify JavaScript for UI feedback (if needed)
37	logging.Debug("[tab-mode] About to call dispatchTabModeEventInternal()")
38	tm.dispatchTabModeEventInternal("entered", "")
39	logging.Debug("[tab-mode] Returned from dispatchTabModeEventInternal()")
40
41	// Start timeout timer to auto-exit tab mode
42	logging.Debug("[tab-mode] Getting config")
43	cfg := tm.getConfig()
44	timeoutDuration := time.Duration(cfg.Workspace.TabMode.TimeoutMilliseconds) * time.Millisecond
45	logging.Debug(fmt.Sprintf("[tab-mode] Config retrieved, timeout: %v", timeoutDuration))
46
47	// Cancel existing timer if any
48	if tm.tabModeTimer != nil {
49		logging.Debug("[tab-mode] Stopping existing timer")
50		tm.tabModeTimer.Stop()
51	}
52
53	logging.Debug("[tab-mode] Creating new timeout timer")
54	tm.tabModeTimer = time.AfterFunc(timeoutDuration, func() {
55		logging.Debug("[tab-mode] Timeout reached, exiting tab mode")
56		tm.ExitTabMode("timeout")
57	})
58
59	logging.Debug(fmt.Sprintf("[tab-mode] Tab mode entered successfully, will auto-exit in %v", timeoutDuration))
60}
61
62// ExitTabMode deactivates tab mode and removes visual indicators.
63func (tm *TabManager) ExitTabMode(reason string) {
64	tm.mu.Lock()
65	defer tm.mu.Unlock()
66
67	if !tm.tabModeActive {
68		return
69	}
70
71	logging.Info(fmt.Sprintf("[tab-mode] Exiting tab mode: %s", reason))
72
73	tm.tabModeActive = false
74
75	// Stop timeout timer
76	if tm.tabModeTimer != nil {
77		tm.tabModeTimer.Stop()
78		tm.tabModeTimer = nil
79	}
80
81	// Remove visual border indicator
82	tm.removeTabModeBorder()
83
84	// Notify JavaScript (use internal version to avoid lock reentry)
85	tm.dispatchTabModeEventInternal("exited", reason)
86}
87
88// HandleTabAction processes a tab mode action (new-tab, close-tab, etc.).
89func (tm *TabManager) HandleTabAction(action string) {
90	tm.mu.Lock()
91
92	if !tm.tabModeActive {
93		tm.mu.Unlock()
94		logging.Debug(fmt.Sprintf("[tab-mode] Ignoring action '%s': tab mode not active", action))
95		return
96	}
97
98	tm.mu.Unlock()
99
100	logging.Info(fmt.Sprintf("[tab-mode] Handling action '%s'", action))
101
102	switch action {
103	case "new-tab":
104		tm.handleNewTab()
105	case "close-tab":
106		tm.handleCloseTab()
107	case "next-tab":
108		tm.handleNextTab()
109	case "previous-tab":
110		tm.handlePreviousTab()
111	case "rename-tab":
112		tm.handleRenameTab()
113	case "confirm":
114		// Just exit tab mode
115		tm.ExitTabMode("confirm")
116	case "cancel":
117		// Exit tab mode without action
118		tm.ExitTabMode("cancel")
119	default:
120		logging.Warn(fmt.Sprintf("[tab-mode] Unknown action: %s", action))
121	}
122
123	// Exit tab mode after most actions (except rename which shows a dialog)
124	if action != "cancel" && action != "confirm" && action != "rename-tab" {
125		tm.ExitTabMode(action)
126	}
127}
128
129// handleNewTab creates a new tab.
130func (tm *TabManager) handleNewTab() {
131	logging.Debug("[tab-mode] Creating new tab")
132
133	if err := tm.CreateTab(""); err != nil {
134		logging.Error(fmt.Sprintf("[tab-mode] Failed to create new tab: %v", err))
135	}
136}
137
138// handleCloseTab closes the currently active tab.
139func (tm *TabManager) handleCloseTab() {
140	logging.Debug("[tab-mode] Closing active tab")
141
142	tm.mu.RLock()
143	activeIndex := tm.activeIndex
144	tabCount := len(tm.tabs)
145	tm.mu.RUnlock()
146
147	if tabCount <= 1 {
148		logging.Warn("[tab-mode] Cannot close last tab")
149		return
150	}
151
152	if err := tm.CloseTab(activeIndex); err != nil {
153		logging.Error(fmt.Sprintf("[tab-mode] Failed to close tab: %v", err))
154	}
155}
156
157// handleNextTab switches to the next tab.
158func (tm *TabManager) handleNextTab() {
159	logging.Debug("[tab-mode] Switching to next tab")
160
161	if err := tm.NextTab(); err != nil {
162		logging.Error(fmt.Sprintf("[tab-mode] Failed to switch to next tab: %v", err))
163	}
164}
165
166// handlePreviousTab switches to the previous tab.
167func (tm *TabManager) handlePreviousTab() {
168	logging.Debug("[tab-mode] Switching to previous tab")
169
170	if err := tm.PreviousTab(); err != nil {
171		logging.Error(fmt.Sprintf("[tab-mode] Failed to switch to previous tab: %v", err))
172	}
173}
174
175// handleRenameTab starts inline editing of the active tab's name.
176func (tm *TabManager) handleRenameTab() {
177	logging.Debug("[tab-mode] Starting inline tab rename")
178
179	tm.mu.RLock()
180	activeIndex := tm.activeIndex
181	tabCount := len(tm.tabs)
182	var activeTab *Tab
183	if activeIndex >= 0 && activeIndex < len(tm.tabs) {
184		activeTab = tm.tabs[activeIndex]
185	}
186	tm.mu.RUnlock()
187
188	// Guard: Don't allow rename if only 1 tab (tab bar is hidden)
189	if tabCount <= 1 {
190		logging.Warn("[tab-mode] Cannot rename: only one tab (tab bar hidden)")
191		return
192	}
193
194	if activeTab == nil {
195		logging.Error("[tab-mode] No active tab to rename")
196		return
197	}
198
199	// Start inline rename (tab mode will be exited by finishInlineRename)
200	logging.Info(fmt.Sprintf("[tab-mode] Starting inline rename for tab %s", activeTab.id))
201	tm.startInlineRename(activeTab)
202}
203
204// applyTabModeBorder applies visual indicator when tab mode is active.
205// This adds an orange border around the entire workspace area.
206func (tm *TabManager) applyTabModeBorder() {
207	logging.Debug("[tab-mode] applyTabModeBorder: entry")
208
209	if tm.window == nil {
210		logging.Debug("[tab-mode] applyTabModeBorder: window is nil, returning")
211		return
212	}
213
214	// Determine which container should receive the margins:
215	// - Prefer the root container (direct child of the window) so the window background shows
216	// - Fallback to ContentArea for older builds
217	var target gtk.Widgetter
218	if tm.rootContainer != nil {
219		target = tm.rootContainer
220		logging.Debug("[tab-mode] applyTabModeBorder: using root container for border")
221	} else if tm.ContentArea != nil {
222		target = tm.ContentArea
223		logging.Debug("[tab-mode] applyTabModeBorder: using content area fallback for border")
224	} else {
225		logging.Debug("[tab-mode] applyTabModeBorder: no container available, returning")
226		return
227	}
228
229	// Remember the container for removal
230	tm.tabModeTarget = target
231
232	// Apply 4px margins to create space for the border
233	webkit.WidgetSetMargin(target, 4)
234
235	// Add CSS class to window for background color (the "border" color shows in the margin space)
236	gtkWindow := tm.window.AsWindow()
237	webkit.WidgetAddCSSClass(gtkWindow, "tab-mode-active")
238
239	// Force resize/allocation to apply margin changes immediately
240	webkit.WidgetQueueResize(target)
241	webkit.WidgetQueueAllocate(target)
242
243	// Queue redraw to show changes
244	webkit.WidgetQueueDraw(gtkWindow)
245	webkit.WidgetQueueDraw(target)
246
247	logging.Debug("[tab-mode] applyTabModeBorder: border applied successfully")
248}
249
250// removeTabModeBorder removes the visual indicator when exiting tab mode.
251func (tm *TabManager) removeTabModeBorder() {
252	if tm.window == nil {
253		return
254	}
255
256	// Use whichever container received margins
257	target := tm.tabModeTarget
258	if target == nil {
259		if tm.rootContainer != nil {
260			target = tm.rootContainer
261		} else {
262			target = tm.ContentArea
263		}
264	}
265	if target == nil {
266		return
267	}
268
269	// Remove margins
270	webkit.WidgetSetMargin(target, 0)
271
272	// Remove CSS class from window
273	gtkWindow := tm.window.AsWindow()
274	webkit.WidgetRemoveCSSClass(gtkWindow, "tab-mode-active")
275
276	// Force resize/allocation to apply margin changes immediately
277	webkit.WidgetQueueResize(target)
278	webkit.WidgetQueueAllocate(target)
279
280	// Queue redraw to show changes
281	webkit.WidgetQueueDraw(gtkWindow)
282	webkit.WidgetQueueDraw(target)
283
284	tm.tabModeTarget = nil
285
286	logging.Debug("[tab-mode] Removed tab mode visual indicator")
287}
288
289// dispatchTabModeEventInternal sends an event without acquiring locks.
290// Must be called with lock already held.
291func (tm *TabManager) dispatchTabModeEventInternal(event string, detail string) {
292	logging.Debug(fmt.Sprintf("[tab-mode] dispatchTabModeEventInternal: entry (event=%s, detail=%s)", event, detail))
293
294	// Get active tab
295	if tm.activeIndex < 0 || tm.activeIndex >= len(tm.tabs) {
296		logging.Debug(fmt.Sprintf("[tab-mode] dispatchTabModeEventInternal: invalid activeIndex=%d, returning", tm.activeIndex))
297		return
298	}
299
300	activeTab := tm.tabs[tm.activeIndex]
301	if activeTab == nil {
302		logging.Debug("[tab-mode] dispatchTabModeEventInternal: activeTab is nil, returning")
303		return
304	}
305
306	// Log tab mode events for debugging (no JS dispatch needed for simple text tab labels)
307	logging.Debug(fmt.Sprintf("[tab-mode] Tab mode event '%s' with detail '%s' for tab %s", event, detail, activeTab.id))
308	logging.Debug("[tab-mode] dispatchTabModeEventInternal: completed successfully")
309}
310
311// dispatchTabModeEvent sends a custom event to the active tab's webview (with locking).
312// This can be used to provide UI feedback in the web content.
313func (tm *TabManager) dispatchTabModeEvent(event string, detail string) {
314	tm.mu.RLock()
315	defer tm.mu.RUnlock()
316	tm.dispatchTabModeEventInternal(event, detail)
317}

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