1package coordinator
2
3import (
4 "context"
5
6 "github.com/bnema/dumber/internal/application/usecase"
7 "github.com/bnema/dumber/internal/domain/entity"
8 "github.com/bnema/dumber/internal/infrastructure/config"
9 "github.com/bnema/dumber/internal/infrastructure/webkit"
10 "github.com/bnema/dumber/internal/logging"
11 "github.com/bnema/dumber/internal/ui/component"
12 "github.com/bnema/dumber/internal/ui/window"
13)
14
15// TabCoordinator manages tab lifecycle operations.
16type TabCoordinator struct {
17 tabsUC *usecase.ManageTabsUseCase
18 tabs *entity.TabList
19 mainWindow *window.MainWindow
20 config *config.Config
21
22 // Callbacks to avoid circular dependencies
23 onTabCreated func(ctx context.Context, tab *entity.Tab)
24 onTabSwitched func(ctx context.Context, tab *entity.Tab)
25 onQuit func()
26 onAttachPopupToTab func(ctx context.Context, tabID entity.TabID, pane *entity.Pane, wv *webkit.WebView) // For popup tabs
27 onStateChanged func() // For session snapshots
28}
29
30// TabCoordinatorConfig holds configuration for TabCoordinator.
31type TabCoordinatorConfig struct {
32 TabsUC *usecase.ManageTabsUseCase
33 Tabs *entity.TabList
34 MainWindow *window.MainWindow
35 Config *config.Config
36}
37
38// NewTabCoordinator creates a new TabCoordinator.
39func NewTabCoordinator(ctx context.Context, cfg TabCoordinatorConfig) *TabCoordinator {
40 log := logging.FromContext(ctx)
41 log.Debug().Msg("creating tab coordinator")
42
43 return &TabCoordinator{
44 tabsUC: cfg.TabsUC,
45 tabs: cfg.Tabs,
46 mainWindow: cfg.MainWindow,
47 config: cfg.Config,
48 }
49}
50
51// SetOnTabCreated sets the callback for when a tab is created.
52func (c *TabCoordinator) SetOnTabCreated(fn func(ctx context.Context, tab *entity.Tab)) {
53 c.onTabCreated = fn
54}
55
56// SetOnTabSwitched sets the callback for when a tab switch occurs.
57// This is used to swap workspace views in the content area.
58func (c *TabCoordinator) SetOnTabSwitched(fn func(ctx context.Context, tab *entity.Tab)) {
59 c.onTabSwitched = fn
60}
61
62// SetOnQuit sets the callback for when the last tab is closed.
63func (c *TabCoordinator) SetOnQuit(fn func()) {
64 c.onQuit = fn
65}
66
67// SetOnStateChanged sets the callback for when tab state changes (for session snapshots).
68func (c *TabCoordinator) SetOnStateChanged(fn func()) {
69 c.onStateChanged = fn
70}
71
72// notifyStateChanged triggers the state changed callback if set.
73func (c *TabCoordinator) notifyStateChanged() {
74 if c.onStateChanged != nil {
75 c.onStateChanged()
76 }
77}
78
79// Create creates a new tab with the given initial URL.
80func (c *TabCoordinator) Create(ctx context.Context, initialURL string) (*entity.Tab, error) {
81 log := logging.FromContext(ctx)
82
83 output, err := c.tabsUC.Create(ctx, usecase.CreateTabInput{
84 TabList: c.tabs,
85 Name: "",
86 InitialURL: initialURL,
87 })
88 if err != nil {
89 log.Error().Err(err).Msg("failed to create tab")
90 return nil, err
91 }
92
93 // Set new tab as active (updates domain state and tracks previous)
94 c.tabs.SetActive(output.Tab.ID)
95
96 // Update tab bar
97 if c.mainWindow != nil && c.mainWindow.TabBar() != nil {
98 c.mainWindow.TabBar().AddTab(output.Tab)
99 c.mainWindow.TabBar().SetActive(output.Tab.ID)
100 }
101
102 // Update tab bar visibility
103 c.UpdateBarVisibility(ctx)
104
105 // Notify app to create workspace view
106 if c.onTabCreated != nil {
107 c.onTabCreated(ctx, output.Tab)
108 }
109
110 // Switch to the new tab's workspace view
111 if c.onTabSwitched != nil {
112 c.onTabSwitched(ctx, output.Tab)
113 }
114
115 // Notify state change for session snapshots
116 c.notifyStateChanged()
117
118 log.Debug().Str("tab_id", string(output.Tab.ID)).Msg("tab created")
119 return output.Tab, nil
120}
121
122// Close closes the active tab.
123func (c *TabCoordinator) Close(ctx context.Context) error {
124 log := logging.FromContext(ctx)
125
126 activeID := c.tabs.ActiveTabID
127 if activeID == "" {
128 log.Debug().Msg("no active tab to close")
129 return nil
130 }
131
132 wasLast, err := c.tabsUC.Close(ctx, c.tabs, activeID)
133 if err != nil {
134 log.Error().Err(err).Msg("failed to close tab")
135 return err
136 }
137
138 if c.mainWindow != nil && c.mainWindow.TabBar() != nil {
139 c.mainWindow.TabBar().RemoveTab(activeID)
140
141 // Switch to new active tab if any
142 if c.tabs.ActiveTabID != "" {
143 c.mainWindow.TabBar().SetActive(c.tabs.ActiveTabID)
144 }
145 }
146
147 // Update tab bar visibility
148 c.UpdateBarVisibility(ctx)
149
150 // Quit if no tabs left
151 if wasLast && c.onQuit != nil {
152 c.onQuit()
153 }
154
155 // Switch workspace view to new active tab (if not last)
156 if !wasLast && c.onTabSwitched != nil {
157 if tab := c.tabs.Find(c.tabs.ActiveTabID); tab != nil {
158 c.onTabSwitched(ctx, tab)
159 }
160 }
161
162 // Notify state change for session snapshots (unless quitting)
163 if !wasLast {
164 c.notifyStateChanged()
165 }
166
167 log.Debug().Str("tab_id", string(activeID)).Bool("was_last", wasLast).Msg("tab closed")
168 return nil
169}
170
171// Switch switches to a specific tab by ID.
172func (c *TabCoordinator) Switch(ctx context.Context, tabID entity.TabID) error {
173 log := logging.FromContext(ctx)
174
175 // Skip if already active
176 if tabID == c.tabs.ActiveTabID {
177 return nil
178 }
179
180 log.Debug().
181 Str("from", string(c.tabs.ActiveTabID)).
182 Str("to", string(tabID)).
183 Msg("switching tab")
184
185 // Update domain state
186 if err := c.tabsUC.Switch(ctx, c.tabs, tabID); err != nil {
187 log.Error().Err(err).Str("tab_id", string(tabID)).Msg("failed to switch tab")
188 return err
189 }
190
191 // Update tab bar UI
192 if c.mainWindow != nil && c.mainWindow.TabBar() != nil {
193 c.mainWindow.TabBar().SetActive(tabID)
194 }
195
196 // Invoke callback for workspace view switching
197 if c.onTabSwitched != nil {
198 if tab := c.tabs.Find(tabID); tab != nil {
199 c.onTabSwitched(ctx, tab)
200 }
201 }
202
203 return nil
204}
205
206// SwitchNext switches to the next tab.
207func (c *TabCoordinator) SwitchNext(ctx context.Context) error {
208 log := logging.FromContext(ctx)
209
210 if err := c.tabsUC.SwitchNext(ctx, c.tabs); err != nil {
211 log.Error().Err(err).Msg("failed to switch to next tab")
212 return err
213 }
214
215 if c.mainWindow != nil && c.mainWindow.TabBar() != nil {
216 c.mainWindow.TabBar().SetActive(c.tabs.ActiveTabID)
217 }
218
219 // Invoke callback for workspace view switching
220 if c.onTabSwitched != nil {
221 if tab := c.tabs.Find(c.tabs.ActiveTabID); tab != nil {
222 c.onTabSwitched(ctx, tab)
223 }
224 }
225
226 log.Debug().Str("tab_id", string(c.tabs.ActiveTabID)).Msg("switched to next tab")
227 return nil
228}
229
230// SwitchPrev switches to the previous tab.
231func (c *TabCoordinator) SwitchPrev(ctx context.Context) error {
232 log := logging.FromContext(ctx)
233
234 if err := c.tabsUC.SwitchPrevious(ctx, c.tabs); err != nil {
235 log.Error().Err(err).Msg("failed to switch to previous tab")
236 return err
237 }
238
239 if c.mainWindow != nil && c.mainWindow.TabBar() != nil {
240 c.mainWindow.TabBar().SetActive(c.tabs.ActiveTabID)
241 }
242
243 // Invoke callback for workspace view switching
244 if c.onTabSwitched != nil {
245 if tab := c.tabs.Find(c.tabs.ActiveTabID); tab != nil {
246 c.onTabSwitched(ctx, tab)
247 }
248 }
249
250 log.Debug().Str("tab_id", string(c.tabs.ActiveTabID)).Msg("switched to previous tab")
251 return nil
252}
253
254// SwitchByIndex switches to a tab by 0-based index.
255func (c *TabCoordinator) SwitchByIndex(ctx context.Context, index int) error {
256 log := logging.FromContext(ctx)
257
258 if err := c.tabsUC.SwitchByIndex(ctx, c.tabs, index); err != nil {
259 log.Error().Err(err).Int("index", index).Msg("failed to switch to tab by index")
260 return err
261 }
262
263 if c.mainWindow != nil && c.mainWindow.TabBar() != nil {
264 c.mainWindow.TabBar().SetActive(c.tabs.ActiveTabID)
265 }
266
267 // Invoke callback for workspace view switching
268 if c.onTabSwitched != nil {
269 if tab := c.tabs.Find(c.tabs.ActiveTabID); tab != nil {
270 c.onTabSwitched(ctx, tab)
271 }
272 }
273
274 log.Debug().Int("index", index).Str("tab_id", string(c.tabs.ActiveTabID)).Msg("switched to tab by index")
275 return nil
276}
277
278// SwitchToLastActive switches to the previously active tab (Alt+Tab style).
279func (c *TabCoordinator) SwitchToLastActive(ctx context.Context) error {
280 log := logging.FromContext(ctx)
281
282 if err := c.tabsUC.SwitchToLastActive(ctx, c.tabs); err != nil {
283 log.Error().Err(err).Msg("failed to switch to last active tab")
284 return err
285 }
286
287 if c.mainWindow != nil && c.mainWindow.TabBar() != nil {
288 c.mainWindow.TabBar().SetActive(c.tabs.ActiveTabID)
289 }
290
291 // Invoke callback for workspace view switching
292 if c.onTabSwitched != nil {
293 if tab := c.tabs.Find(c.tabs.ActiveTabID); tab != nil {
294 c.onTabSwitched(ctx, tab)
295 }
296 }
297
298 log.Debug().Str("tab_id", string(c.tabs.ActiveTabID)).Msg("switched to last active tab")
299 return nil
300}
301
302// UpdateBarVisibility shows or hides the tab bar based on tab count.
303func (c *TabCoordinator) UpdateBarVisibility(ctx context.Context) {
304 log := logging.FromContext(ctx)
305
306 // Check if feature is enabled
307 hideEnabled := true
308 if c.config != nil {
309 hideEnabled = c.config.Workspace.HideTabBarWhenSingleTab
310 }
311
312 if !hideEnabled {
313 log.Debug().Msg("tab bar auto-hide disabled by config")
314 return
315 }
316
317 if c.mainWindow == nil || c.mainWindow.TabBar() == nil {
318 log.Debug().Msg("mainWindow or tabBar is nil, skipping visibility update")
319 return
320 }
321
322 tabCount := c.tabs.Count()
323 shouldShow := tabCount > 1
324
325 log.Debug().Int("tab_count", tabCount).Bool("should_show", shouldShow).Msg("setting tab bar visibility")
326 c.mainWindow.TabBar().SetVisible(shouldShow)
327}
328
329// GetTabBar returns the tab bar component.
330func (c *TabCoordinator) GetTabBar() *component.TabBar {
331 if c.mainWindow != nil {
332 return c.mainWindow.TabBar()
333 }
334 return nil
335}
336
337// SetOnAttachPopupToTab sets the callback for attaching popup WebViews to tabs.
338func (c *TabCoordinator) SetOnAttachPopupToTab(fn func(ctx context.Context, tabID entity.TabID, pane *entity.Pane, wv *webkit.WebView)) {
339 c.onAttachPopupToTab = fn
340}
341
342// CreateWithPane creates a new tab with a pre-created pane and WebView.
343// This is used for tabbed popup behavior where the popup pane already exists.
344func (c *TabCoordinator) CreateWithPane(
345 ctx context.Context,
346 pane *entity.Pane,
347 wv *webkit.WebView,
348 initialURL string,
349) (*entity.Tab, error) {
350 log := logging.FromContext(ctx)
351
352 output, err := c.tabsUC.CreateWithPane(ctx, usecase.CreateTabWithPaneInput{
353 TabList: c.tabs,
354 Name: pane.Title,
355 Pane: pane,
356 InitialURL: initialURL,
357 })
358 if err != nil {
359 log.Error().Err(err).Msg("failed to create tab with pane")
360 return nil, err
361 }
362
363 // Set new tab as active
364 c.tabs.SetActive(output.Tab.ID)
365
366 // Update tab bar
367 if c.mainWindow != nil && c.mainWindow.TabBar() != nil {
368 c.mainWindow.TabBar().AddTab(output.Tab)
369 c.mainWindow.TabBar().SetActive(output.Tab.ID)
370 }
371
372 // Update tab bar visibility
373 c.UpdateBarVisibility(ctx)
374
375 // Notify app to create workspace view
376 if c.onTabCreated != nil {
377 c.onTabCreated(ctx, output.Tab)
378 }
379
380 // Attach the popup WebView to the new tab's workspace
381 if c.onAttachPopupToTab != nil {
382 c.onAttachPopupToTab(ctx, output.Tab.ID, pane, wv)
383 }
384
385 // Switch to the new tab's workspace view
386 if c.onTabSwitched != nil {
387 c.onTabSwitched(ctx, output.Tab)
388 }
389
390 log.Debug().
391 Str("tab_id", string(output.Tab.ID)).
392 Str("pane_id", string(pane.ID)).
393 Msg("tab created with popup pane")
394 return output.Tab, nil
395}