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