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}