v0.15.0 9006957

tab_mode.go RawHistoryFolder

1package browser
2
3import (
4	"fmt"
5	"time"
6
7	"github.com/bnema/dumber/internal/logging"
8)
9
10// EnterTabMode activates the modal tab management mode.
11// This is triggered by Alt+T and provides visual feedback and temporary key bindings.
12func (tm *TabManager) EnterTabMode() {
13	logging.Debug("[tab-mode] EnterTabMode called - attempting to acquire lock")
14	tm.mu.Lock()
15	defer tm.mu.Unlock()
16	logging.Debug("[tab-mode] Lock acquired")
17
18	// Already in tab mode?
19	if tm.tabModeActive {
20		logging.Debug("[tab-mode] Tab mode already active")
21		return
22	}
23
24	logging.Info("[tab-mode] Entering tab mode")
25
26	logging.Debug("[tab-mode] Setting tabModeActive flag")
27	tm.tabModeActive = true
28
29	// Apply visual border indicator
30	logging.Debug("[tab-mode] About to call applyTabModeBorder()")
31	tm.applyTabModeBorder()
32	logging.Debug("[tab-mode] Returned from applyTabModeBorder()")
33
34	// Notify JavaScript for UI feedback (if needed)
35	logging.Debug("[tab-mode] About to call dispatchTabModeEventInternal()")
36	tm.dispatchTabModeEventInternal("entered", "")
37	logging.Debug("[tab-mode] Returned from dispatchTabModeEventInternal()")
38
39	// Start timeout timer to auto-exit tab mode
40	logging.Debug("[tab-mode] Getting config")
41	cfg := tm.getConfig()
42	timeoutDuration := time.Duration(cfg.Workspace.TabMode.TimeoutMilliseconds) * time.Millisecond
43	logging.Debug(fmt.Sprintf("[tab-mode] Config retrieved, timeout: %v", timeoutDuration))
44
45	// Cancel existing timer if any
46	if tm.tabModeTimer != nil {
47		logging.Debug("[tab-mode] Stopping existing timer")
48		tm.tabModeTimer.Stop()
49	}
50
51	logging.Debug("[tab-mode] Creating new timeout timer")
52	tm.tabModeTimer = time.AfterFunc(timeoutDuration, func() {
53		logging.Debug("[tab-mode] Timeout reached, exiting tab mode")
54		tm.ExitTabMode("timeout")
55	})
56
57	logging.Debug(fmt.Sprintf("[tab-mode] Tab mode entered successfully, will auto-exit in %v", timeoutDuration))
58}
59
60// ExitTabMode deactivates tab mode and removes visual indicators.
61func (tm *TabManager) ExitTabMode(reason string) {
62	tm.mu.Lock()
63	defer tm.mu.Unlock()
64
65	if !tm.tabModeActive {
66		return
67	}
68
69	logging.Info(fmt.Sprintf("[tab-mode] Exiting tab mode: %s", reason))
70
71	tm.tabModeActive = false
72
73	// Stop timeout timer
74	if tm.tabModeTimer != nil {
75		tm.tabModeTimer.Stop()
76		tm.tabModeTimer = nil
77	}
78
79	// Remove visual border indicator
80	tm.removeTabModeBorder()
81
82	// Notify JavaScript (use internal version to avoid lock reentry)
83	tm.dispatchTabModeEventInternal("exited", reason)
84}
85
86// HandleTabAction processes a tab mode action (new-tab, close-tab, etc.).
87func (tm *TabManager) HandleTabAction(action string) {
88	tm.mu.Lock()
89
90	if !tm.tabModeActive {
91		tm.mu.Unlock()
92		logging.Debug(fmt.Sprintf("[tab-mode] Ignoring action '%s': tab mode not active", action))
93		return
94	}
95
96	tm.mu.Unlock()
97
98	logging.Info(fmt.Sprintf("[tab-mode] Handling action '%s'", action))
99
100	switch action {
101	case "new-tab":
102		tm.handleNewTab()
103	case "close-tab":
104		tm.handleCloseTab()
105	case "next-tab":
106		tm.handleNextTab()
107	case "previous-tab":
108		tm.handlePreviousTab()
109	case "rename-tab":
110		tm.handleRenameTab()
111	case "confirm":
112		// Just exit tab mode
113		tm.ExitTabMode("confirm")
114	case "cancel":
115		// Exit tab mode without action
116		tm.ExitTabMode("cancel")
117	default:
118		logging.Warn(fmt.Sprintf("[tab-mode] Unknown action: %s", action))
119	}
120
121	// Exit tab mode after most actions (except rename which shows a dialog)
122	if action != "cancel" && action != "confirm" && action != "rename-tab" {
123		tm.ExitTabMode(action)
124	}
125}
126
127// handleNewTab creates a new tab.
128func (tm *TabManager) handleNewTab() {
129	logging.Debug("[tab-mode] Creating new tab")
130
131	if err := tm.CreateTab(""); err != nil {
132		logging.Error(fmt.Sprintf("[tab-mode] Failed to create new tab: %v", err))
133	}
134}
135
136// handleCloseTab closes the currently active tab.
137func (tm *TabManager) handleCloseTab() {
138	logging.Debug("[tab-mode] Closing active tab")
139
140	tm.mu.RLock()
141	activeIndex := tm.activeIndex
142	tabCount := len(tm.tabs)
143	tm.mu.RUnlock()
144
145	if tabCount <= 1 {
146		logging.Warn("[tab-mode] Cannot close last tab")
147		return
148	}
149
150	if err := tm.CloseTab(activeIndex); err != nil {
151		logging.Error(fmt.Sprintf("[tab-mode] Failed to close tab: %v", err))
152	}
153}
154
155// handleNextTab switches to the next tab.
156func (tm *TabManager) handleNextTab() {
157	logging.Debug("[tab-mode] Switching to next tab")
158
159	if err := tm.NextTab(); err != nil {
160		logging.Error(fmt.Sprintf("[tab-mode] Failed to switch to next tab: %v", err))
161	}
162}
163
164// handlePreviousTab switches to the previous tab.
165func (tm *TabManager) handlePreviousTab() {
166	logging.Debug("[tab-mode] Switching to previous tab")
167
168	if err := tm.PreviousTab(); err != nil {
169		logging.Error(fmt.Sprintf("[tab-mode] Failed to switch to previous tab: %v", err))
170	}
171}
172
173// handleRenameTab starts inline editing of the active tab's name.
174func (tm *TabManager) handleRenameTab() {
175	logging.Debug("[tab-mode] Starting inline tab rename")
176
177	tm.mu.RLock()
178	activeIndex := tm.activeIndex
179	tabCount := len(tm.tabs)
180	var activeTab *Tab
181	if activeIndex >= 0 && activeIndex < len(tm.tabs) {
182		activeTab = tm.tabs[activeIndex]
183	}
184	tm.mu.RUnlock()
185
186	// Guard: Don't allow rename if only 1 tab (tab bar is hidden)
187	if tabCount <= 1 {
188		logging.Warn("[tab-mode] Cannot rename: only one tab (tab bar hidden)")
189		return
190	}
191
192	if activeTab == nil {
193		logging.Error("[tab-mode] No active tab to rename")
194		return
195	}
196
197	// Start inline rename (tab mode will be exited by finishInlineRename)
198	logging.Info(fmt.Sprintf("[tab-mode] Starting inline rename for tab %s", activeTab.id))
199	tm.startInlineRename(activeTab)
200}
201
202// applyTabModeBorder applies visual indicator when tab mode is active.
203// Uses an overlay widget to show the border without affecting layout.
204func (tm *TabManager) applyTabModeBorder() {
205	tm.showTabModeBorder()
206}
207
208// removeTabModeBorder removes the visual indicator when exiting tab mode.
209// Uses an overlay widget to hide the border without affecting layout.
210func (tm *TabManager) removeTabModeBorder() {
211	tm.hideTabModeBorder()
212}
213
214// dispatchTabModeEventInternal sends an event without acquiring locks.
215// Must be called with lock already held.
216func (tm *TabManager) dispatchTabModeEventInternal(event string, detail string) {
217	logging.Debug(fmt.Sprintf("[tab-mode] dispatchTabModeEventInternal: entry (event=%s, detail=%s)", event, detail))
218
219	// Get active tab
220	if tm.activeIndex < 0 || tm.activeIndex >= len(tm.tabs) {
221		logging.Debug(fmt.Sprintf("[tab-mode] dispatchTabModeEventInternal: invalid activeIndex=%d, returning", tm.activeIndex))
222		return
223	}
224
225	activeTab := tm.tabs[tm.activeIndex]
226	if activeTab == nil {
227		logging.Debug("[tab-mode] dispatchTabModeEventInternal: activeTab is nil, returning")
228		return
229	}
230
231	// Log tab mode events for debugging (no JS dispatch needed for simple text tab labels)
232	logging.Debug(fmt.Sprintf("[tab-mode] Tab mode event '%s' with detail '%s' for tab %s", event, detail, activeTab.id))
233	logging.Debug("[tab-mode] dispatchTabModeEventInternal: completed successfully")
234}
235
236// dispatchTabModeEvent sends a custom event to the active tab's webview (with locking).
237// This can be used to provide UI feedback in the web content.
238func (tm *TabManager) dispatchTabModeEvent(event string, detail string) {
239	tm.mu.RLock()
240	defer tm.mu.RUnlock()
241	tm.dispatchTabModeEventInternal(event, detail)
242}

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