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