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