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 shows a dialog to rename the active tab.
176func (tm *TabManager) handleRenameTab() {
177 logging.Debug("[tab-mode] Opening rename dialog")
178
179 // Exit tab mode first so the dialog can be interacted with
180 tm.ExitTabMode("rename-dialog")
181
182 tm.mu.RLock()
183 activeIndex := tm.activeIndex
184 var currentTitle string
185 if activeIndex >= 0 && activeIndex < len(tm.tabs) {
186 tab := tm.tabs[activeIndex]
187 if tab.customTitle != "" {
188 currentTitle = tab.customTitle
189 } else {
190 currentTitle = tab.title
191 }
192 }
193 tm.mu.RUnlock()
194
195 if activeIndex < 0 {
196 logging.Error("[tab-mode] No active tab to rename")
197 return
198 }
199
200 // Show rename dialog (will be implemented in Step 10)
201 // For now, just log
202 logging.Info(fmt.Sprintf("[tab-mode] Would show rename dialog for tab %d (current: %s)", activeIndex, currentTitle))
203
204 // TODO: Implement GTK dialog in Step 10
205 // tm.showRenameDialog(activeIndex, currentTitle)
206}
207
208// applyTabModeBorder applies visual indicator when tab mode is active.
209// This adds an orange border around the entire workspace area.
210func (tm *TabManager) applyTabModeBorder() {
211 logging.Debug("[tab-mode] applyTabModeBorder: entry")
212
213 if tm.window == nil {
214 logging.Debug("[tab-mode] applyTabModeBorder: window is nil, returning")
215 return
216 }
217
218 // Determine which container should receive the margins:
219 // - Prefer the root container (direct child of the window) so the window background shows
220 // - Fallback to ContentArea for older builds
221 var target gtk.Widgetter
222 if tm.rootContainer != nil {
223 target = tm.rootContainer
224 logging.Debug("[tab-mode] applyTabModeBorder: using root container for border")
225 } else if tm.ContentArea != nil {
226 target = tm.ContentArea
227 logging.Debug("[tab-mode] applyTabModeBorder: using content area fallback for border")
228 } else {
229 logging.Debug("[tab-mode] applyTabModeBorder: no container available, returning")
230 return
231 }
232
233 // Remember the container for removal
234 tm.tabModeTarget = target
235
236 // Apply 4px margins to create space for the border
237 webkit.WidgetSetMargin(target, 4)
238
239 // Add CSS class to window for background color (the "border" color shows in the margin space)
240 gtkWindow := tm.window.AsWindow()
241 webkit.WidgetAddCSSClass(gtkWindow, "tab-mode-active")
242
243 // Force resize/allocation to apply margin changes immediately
244 webkit.WidgetQueueResize(target)
245 webkit.WidgetQueueAllocate(target)
246
247 // Queue redraw to show changes
248 webkit.WidgetQueueDraw(gtkWindow)
249 webkit.WidgetQueueDraw(target)
250
251 logging.Debug("[tab-mode] applyTabModeBorder: border applied successfully")
252}
253
254// removeTabModeBorder removes the visual indicator when exiting tab mode.
255func (tm *TabManager) removeTabModeBorder() {
256 if tm.window == nil {
257 return
258 }
259
260 // Use whichever container received margins
261 target := tm.tabModeTarget
262 if target == nil {
263 if tm.rootContainer != nil {
264 target = tm.rootContainer
265 } else {
266 target = tm.ContentArea
267 }
268 }
269 if target == nil {
270 return
271 }
272
273 // Remove margins
274 webkit.WidgetSetMargin(target, 0)
275
276 // Remove CSS class from window
277 gtkWindow := tm.window.AsWindow()
278 webkit.WidgetRemoveCSSClass(gtkWindow, "tab-mode-active")
279
280 // Force resize/allocation to apply margin changes immediately
281 webkit.WidgetQueueResize(target)
282 webkit.WidgetQueueAllocate(target)
283
284 // Queue redraw to show changes
285 webkit.WidgetQueueDraw(gtkWindow)
286 webkit.WidgetQueueDraw(target)
287
288 tm.tabModeTarget = nil
289
290 logging.Debug("[tab-mode] Removed tab mode visual indicator")
291}
292
293// dispatchTabModeEventInternal sends an event without acquiring locks.
294// Must be called with lock already held.
295func (tm *TabManager) dispatchTabModeEventInternal(event string, detail string) {
296 logging.Debug(fmt.Sprintf("[tab-mode] dispatchTabModeEventInternal: entry (event=%s, detail=%s)", event, detail))
297
298 // Get active tab
299 if tm.activeIndex < 0 || tm.activeIndex >= len(tm.tabs) {
300 logging.Debug(fmt.Sprintf("[tab-mode] dispatchTabModeEventInternal: invalid activeIndex=%d, returning", tm.activeIndex))
301 return
302 }
303
304 activeTab := tm.tabs[tm.activeIndex]
305 if activeTab == nil {
306 logging.Debug("[tab-mode] dispatchTabModeEventInternal: activeTab is nil, returning")
307 return
308 }
309
310 // Log tab mode events for debugging (no JS dispatch needed for simple text tab labels)
311 logging.Debug(fmt.Sprintf("[tab-mode] Tab mode event '%s' with detail '%s' for tab %s", event, detail, activeTab.id))
312 logging.Debug("[tab-mode] dispatchTabModeEventInternal: completed successfully")
313}
314
315// dispatchTabModeEvent sends a custom event to the active tab's webview (with locking).
316// This can be used to provide UI feedback in the web content.
317func (tm *TabManager) dispatchTabModeEvent(event string, detail string) {
318 tm.mu.RLock()
319 defer tm.mu.RUnlock()
320 tm.dispatchTabModeEventInternal(event, detail)
321}