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}