1package usecase
2
3import (
4 "context"
5 "fmt"
6
7 "github.com/bnema/dumber/internal/application/port"
8 "github.com/bnema/dumber/internal/domain/entity"
9 "github.com/bnema/dumber/internal/logging"
10)
11
12// IDGenerator is a function type for generating unique IDs.
13type IDGenerator func() string
14
15// ManageTabsUseCase handles tab lifecycle operations.
16type ManageTabsUseCase struct {
17 idGenerator IDGenerator
18 normalizer *NavigationURLNormalizer
19}
20
21// NewManageTabsUseCase creates a new tab management use case.
22func NewManageTabsUseCase(idGenerator IDGenerator, localPaths port.LocalPathResolver) *ManageTabsUseCase {
23 return &ManageTabsUseCase{
24 idGenerator: idGenerator,
25 normalizer: NewNavigationURLNormalizer(localPaths),
26 }
27}
28
29// CreateTabInput contains parameters for creating a new tab.
30type CreateTabInput struct {
31 TabList *entity.TabList
32 Name string // Optional custom name
33 InitialURL string // URL to load (default: about:blank)
34 IsPinned bool
35}
36
37// CreateTabOutput contains the result of tab creation.
38type CreateTabOutput struct {
39 Tab *entity.Tab
40}
41
42// Create creates a new tab with a workspace and initial pane.
43func (uc *ManageTabsUseCase) Create(ctx context.Context, input CreateTabInput) (*CreateTabOutput, error) {
44 log := logging.FromContext(ctx)
45 log.Debug().
46 Str("name", input.Name).
47 Str("initial_url", input.InitialURL).
48 Bool("is_pinned", input.IsPinned).
49 Msg("creating new tab")
50
51 if input.TabList == nil {
52 return nil, fmt.Errorf("tab list is required")
53 }
54
55 // Generate IDs
56 tabID := entity.TabID(uc.idGenerator())
57 workspaceID := entity.WorkspaceID(uc.idGenerator())
58 paneID := entity.PaneID(uc.idGenerator())
59
60 // Create initial pane with normalized URL
61 pane := entity.NewPane(paneID)
62 if input.InitialURL != "" {
63 pane.URI = uc.normalizer.normalize(ctx, input.InitialURL)
64 }
65
66 // Create tab with workspace
67 tab := entity.NewTab(tabID, workspaceID, pane)
68 tab.Name = input.Name
69 tab.IsPinned = input.IsPinned
70
71 // Add to list (handles position and active tab)
72 input.TabList.Add(tab)
73
74 log.Info().
75 Str("tab_id", string(tabID)).
76 Str("workspace_id", string(workspaceID)).
77 Str("pane_id", string(paneID)).
78 Int("position", tab.Position).
79 Msg("tab created")
80
81 return &CreateTabOutput{Tab: tab}, nil
82}
83
84// Close removes a tab from the list.
85// Returns true if this was the last tab (caller should handle app exit).
86func (uc *ManageTabsUseCase) Close(ctx context.Context, tabs *entity.TabList, tabID entity.TabID) (wasLast bool, err error) {
87 ctx = logging.WithTabID(ctx, string(tabID))
88 log := logging.FromContext(ctx)
89 if uc == nil {
90 return false, fmt.Errorf("manage tabs use case is nil")
91 }
92
93 log.Debug().Msg("closing tab")
94
95 if tabs == nil {
96 return false, fmt.Errorf("tab list is required")
97 }
98
99 tab := tabs.Find(tabID)
100 if tab == nil {
101 log.Debug().Msg("tab not found")
102 return false, nil
103 }
104
105 // Check if this is the last tab
106 if tabs.Count() == 1 {
107 log.Info().Msg("closing last tab")
108 tabs.Remove(tabID)
109 return true, nil
110 }
111
112 // Remove tab (TabList handles active tab switching)
113 if !tabs.Remove(tabID) {
114 return false, fmt.Errorf("failed to remove tab")
115 }
116
117 log.Info().
118 Str("new_active", string(tabs.ActiveTabID)).
119 Int("remaining", tabs.Count()).
120 Msg("tab closed")
121
122 return false, nil
123}
124
125// Switch changes the active tab.
126func (uc *ManageTabsUseCase) Switch(ctx context.Context, tabs *entity.TabList, tabID entity.TabID) error {
127 log := logging.FromContext(ctx)
128 if uc == nil {
129 return fmt.Errorf("manage tabs use case is nil")
130 }
131 log.Debug().Str("tab_id", string(tabID)).Msg("switching to tab")
132
133 if tabs == nil {
134 return fmt.Errorf("tab list is required")
135 }
136
137 tab := tabs.Find(tabID)
138 if tab == nil {
139 return fmt.Errorf("tab not found: %s", tabID)
140 }
141
142 oldActive := tabs.ActiveTabID
143 tabs.SetActive(tabID) // Uses SetActive to track previous tab
144
145 log.Info().
146 Str("from", string(oldActive)).
147 Str("to", string(tabID)).
148 Msg("tab switched")
149
150 return nil
151}
152
153// Move repositions a tab within the tab bar.
154func (uc *ManageTabsUseCase) Move(ctx context.Context, tabs *entity.TabList, tabID entity.TabID, newPosition int) error {
155 log := logging.FromContext(ctx)
156 if uc == nil {
157 return fmt.Errorf("manage tabs use case is nil")
158 }
159 log.Debug().
160 Str("tab_id", string(tabID)).
161 Int("new_position", newPosition).
162 Msg("moving tab")
163
164 if tabs == nil {
165 return fmt.Errorf("tab list is required")
166 }
167
168 if !tabs.Move(tabID, newPosition) {
169 return fmt.Errorf("failed to move tab to position %d", newPosition)
170 }
171
172 log.Info().
173 Str("tab_id", string(tabID)).
174 Int("position", newPosition).
175 Msg("tab moved")
176
177 return nil
178}
179
180// Rename changes a tab's custom name.
181//
182//nolint:revive // receiver required for interface consistency
183func (uc *ManageTabsUseCase) Rename(ctx context.Context, tabs *entity.TabList, tabID entity.TabID, name string) error {
184 log := logging.FromContext(ctx)
185 log.Debug().
186 Str("tab_id", string(tabID)).
187 Str("name", name).
188 Msg("renaming tab")
189
190 if tabs == nil {
191 return fmt.Errorf("tab list is required")
192 }
193
194 tab := tabs.Find(tabID)
195 if tab == nil {
196 return fmt.Errorf("tab not found: %s", tabID)
197 }
198
199 tab.Name = name
200
201 log.Info().
202 Str("tab_id", string(tabID)).
203 Str("name", name).
204 Msg("tab renamed")
205
206 return nil
207}
208
209// GetNext returns the next tab ID in the given direction.
210// direction: 1 for next, -1 for previous.
211//
212//nolint:revive // receiver required for interface consistency
213func (uc *ManageTabsUseCase) GetNext(tabs *entity.TabList, direction int) entity.TabID {
214 if tabs == nil || tabs.Count() == 0 {
215 return ""
216 }
217
218 // Find current active tab position
219 activeTab := tabs.ActiveTab()
220 if activeTab == nil {
221 // Return first tab if no active
222 if len(tabs.Tabs) > 0 {
223 return tabs.Tabs[0].ID
224 }
225 return ""
226 }
227
228 currentPos := activeTab.Position
229 newPos := currentPos + direction
230
231 // Wrap around
232 if newPos < 0 {
233 newPos = tabs.Count() - 1
234 } else if newPos >= tabs.Count() {
235 newPos = 0
236 }
237
238 if newPos >= 0 && newPos < len(tabs.Tabs) {
239 return tabs.Tabs[newPos].ID
240 }
241
242 return tabs.ActiveTabID
243}
244
245// SwitchNext switches to the next tab (wraps around).
246func (uc *ManageTabsUseCase) SwitchNext(ctx context.Context, tabs *entity.TabList) error {
247 nextID := uc.GetNext(tabs, 1)
248 if nextID == "" || nextID == tabs.ActiveTabID {
249 return nil
250 }
251 return uc.Switch(ctx, tabs, nextID)
252}
253
254// SwitchPrevious switches to the previous tab (wraps around).
255func (uc *ManageTabsUseCase) SwitchPrevious(ctx context.Context, tabs *entity.TabList) error {
256 prevID := uc.GetNext(tabs, -1)
257 if prevID == "" || prevID == tabs.ActiveTabID {
258 return nil
259 }
260 return uc.Switch(ctx, tabs, prevID)
261}
262
263// SwitchByIndex switches to tab at the given index (0-based).
264func (uc *ManageTabsUseCase) SwitchByIndex(ctx context.Context, tabs *entity.TabList, index int) error {
265 log := logging.FromContext(ctx)
266
267 if tabs == nil || index < 0 || index >= tabs.Count() {
268 log.Debug().Int("index", index).Msg("invalid tab index")
269 return nil
270 }
271
272 return uc.Switch(ctx, tabs, tabs.Tabs[index].ID)
273}
274
275// SwitchToLastActive switches to the previously active tab (Alt+Tab style).
276func (uc *ManageTabsUseCase) SwitchToLastActive(ctx context.Context, tabs *entity.TabList) error {
277 log := logging.FromContext(ctx)
278
279 if tabs == nil {
280 return fmt.Errorf("tab list is required")
281 }
282
283 // If no previous tab recorded, do nothing
284 if tabs.PreviousActiveTabID == "" {
285 log.Debug().Msg("no previous active tab")
286 return nil
287 }
288
289 // Verify previous tab still exists
290 if tabs.Find(tabs.PreviousActiveTabID) == nil {
291 log.Debug().Str("prev_id", string(tabs.PreviousActiveTabID)).Msg("previous tab no longer exists")
292 tabs.PreviousActiveTabID = ""
293 return nil
294 }
295
296 return uc.Switch(ctx, tabs, tabs.PreviousActiveTabID)
297}
298
299// Pin toggles the pinned state of a tab.
300//
301//nolint:revive // receiver required for interface consistency
302func (uc *ManageTabsUseCase) Pin(ctx context.Context, tabs *entity.TabList, tabID entity.TabID, pinned bool) error {
303 log := logging.FromContext(ctx)
304 log.Debug().
305 Str("tab_id", string(tabID)).
306 Bool("pinned", pinned).
307 Msg("toggling tab pin state")
308
309 if tabs == nil {
310 return fmt.Errorf("tab list is required")
311 }
312
313 tab := tabs.Find(tabID)
314 if tab == nil {
315 return fmt.Errorf("tab not found: %s", tabID)
316 }
317
318 tab.IsPinned = pinned
319
320 log.Info().
321 Str("tab_id", string(tabID)).
322 Bool("pinned", pinned).
323 Msg("tab pin state changed")
324
325 return nil
326}
327
328// CreateTabWithPaneInput contains parameters for creating a tab with an existing pane.
329type CreateTabWithPaneInput struct {
330 TabList *entity.TabList
331 Name string // Optional custom name
332 Pane *entity.Pane // Pre-created pane (for popups)
333 InitialURL string // URL for the pane
334}
335
336// CreateWithPane creates a new tab using a pre-existing pane.
337// This is used for tabbed popup behavior where the popup pane is already created.
338func (uc *ManageTabsUseCase) CreateWithPane(ctx context.Context, input CreateTabWithPaneInput) (*CreateTabOutput, error) {
339 log := logging.FromContext(ctx)
340 log.Debug().
341 Str("name", input.Name).
342 Str("pane_id", string(input.Pane.ID)).
343 Str("initial_url", input.InitialURL).
344 Msg("creating new tab with existing pane")
345
346 if input.TabList == nil {
347 return nil, fmt.Errorf("tab list is required")
348 }
349 if input.Pane == nil {
350 return nil, fmt.Errorf("pane is required")
351 }
352
353 // Generate IDs for tab and workspace
354 tabID := entity.TabID(uc.idGenerator())
355 workspaceID := entity.WorkspaceID(uc.idGenerator())
356
357 // Use the provided pane
358 pane := input.Pane
359 if input.InitialURL != "" {
360 pane.URI = uc.normalizer.normalize(ctx, input.InitialURL)
361 }
362
363 // Create tab with workspace using the pre-created pane
364 tab := entity.NewTab(tabID, workspaceID, pane)
365 if input.Name != "" {
366 tab.Name = input.Name
367 } else if pane.Title != "" {
368 tab.Name = pane.Title
369 }
370
371 // Add to list
372 input.TabList.Add(tab)
373
374 log.Info().
375 Str("tab_id", string(tabID)).
376 Str("workspace_id", string(workspaceID)).
377 Str("pane_id", string(pane.ID)).
378 Int("position", tab.Position).
379 Msg("tab created with existing pane")
380
381 return &CreateTabOutput{Tab: tab}, nil
382}