1package entity
2
3import (
4 "fmt"
5 "sync"
6 "time"
7)
8
9// TabID uniquely identifies a tab.
10type TabID string
11
12// Tab represents a browser tab containing a workspace.
13// Tabs are the top-level container in the browser's tab bar.
14type Tab struct {
15 ID TabID
16 Name string // Display name (often derived from active pane title)
17 Workspace *Workspace // The workspace this tab contains
18 Position int // Position in the tab bar (0-indexed)
19 IsPinned bool // Pinned tabs stay at the left
20 CreatedAt time.Time
21}
22
23// NewTab creates a new tab with an initial pane.
24func NewTab(tabID TabID, workspaceID WorkspaceID, initialPane *Pane) *Tab {
25 return &Tab{
26 ID: tabID,
27 Workspace: NewWorkspace(workspaceID, initialPane),
28 Position: 0,
29 CreatedAt: time.Now(),
30 }
31}
32
33// Title returns the display title for the tab.
34// Uses the tab's Name if set, otherwise returns "Tab N" based on position.
35func (t *Tab) Title() string {
36 if t.Name != "" {
37 return t.Name
38 }
39 return fmt.Sprintf("Tab %d", t.Position+1)
40}
41
42// PaneCount returns the number of panes in this tab's workspace.
43func (t *Tab) PaneCount() int {
44 if t.Workspace == nil {
45 return 0
46 }
47 return t.Workspace.PaneCount()
48}
49
50// TabList manages an ordered collection of tabs.
51type TabList struct {
52 Tabs []*Tab
53 ActiveTabID TabID
54 PreviousActiveTabID TabID // Tracks last active tab for Alt+Tab style switching
55
56 mu sync.RWMutex
57}
58
59// NewTabList creates an empty tab list.
60func NewTabList() *TabList {
61 return &TabList{
62 Tabs: make([]*Tab, 0),
63 }
64}
65
66// Add appends a tab to the list.
67func (tl *TabList) Add(tab *Tab) {
68 tl.mu.Lock()
69 defer tl.mu.Unlock()
70
71 tab.Position = len(tl.Tabs)
72 tl.Tabs = append(tl.Tabs, tab)
73 if tl.ActiveTabID == "" {
74 tl.ActiveTabID = tab.ID
75 }
76}
77
78// Remove removes a tab by ID and reindexes positions.
79func (tl *TabList) Remove(id TabID) bool {
80 tl.mu.Lock()
81 defer tl.mu.Unlock()
82
83 for i, tab := range tl.Tabs {
84 if tab.ID == id {
85 tl.Tabs = append(tl.Tabs[:i], tl.Tabs[i+1:]...)
86 // Reindex positions
87 for j := i; j < len(tl.Tabs); j++ {
88 tl.Tabs[j].Position = j
89 }
90 // Update active tab if needed
91 if tl.ActiveTabID == id {
92 if len(tl.Tabs) > 0 {
93 if i < len(tl.Tabs) {
94 tl.ActiveTabID = tl.Tabs[i].ID
95 } else {
96 tl.ActiveTabID = tl.Tabs[len(tl.Tabs)-1].ID
97 }
98 } else {
99 tl.ActiveTabID = ""
100 tl.PreviousActiveTabID = ""
101 }
102 }
103 return true
104 }
105 }
106 return false
107}
108
109// Find returns a tab by ID.
110func (tl *TabList) Find(id TabID) *Tab {
111 tl.mu.RLock()
112 defer tl.mu.RUnlock()
113
114 return tl.findNoLock(id)
115}
116
117func (tl *TabList) findNoLock(id TabID) *Tab {
118 for _, tab := range tl.Tabs {
119 if tab.ID == id {
120 return tab
121 }
122 }
123 return nil
124}
125
126// ActiveTab returns the currently active tab.
127func (tl *TabList) ActiveTab() *Tab {
128 tl.mu.RLock()
129 defer tl.mu.RUnlock()
130
131 return tl.findNoLock(tl.ActiveTabID)
132}
133
134// Count returns the number of tabs.
135func (tl *TabList) Count() int {
136 tl.mu.RLock()
137 defer tl.mu.RUnlock()
138
139 return len(tl.Tabs)
140}
141
142// SetActive sets the active tab and updates the previous active tab.
143func (tl *TabList) SetActive(id TabID) {
144 tl.mu.Lock()
145 defer tl.mu.Unlock()
146
147 if id != tl.ActiveTabID && tl.ActiveTabID != "" {
148 tl.PreviousActiveTabID = tl.ActiveTabID
149 }
150 tl.ActiveTabID = id
151}
152
153// TabAt returns the tab at the given 0-based index.
154func (tl *TabList) TabAt(index int) *Tab {
155 tl.mu.RLock()
156 defer tl.mu.RUnlock()
157
158 if index < 0 || index >= len(tl.Tabs) {
159 return nil
160 }
161 return tl.Tabs[index]
162}
163
164// Move moves a tab to a new position.
165func (tl *TabList) Move(id TabID, newPos int) bool {
166 tl.mu.Lock()
167 defer tl.mu.Unlock()
168
169 if newPos < 0 || newPos >= len(tl.Tabs) {
170 return false
171 }
172 var tab *Tab
173 var oldPos int
174 for i, t := range tl.Tabs {
175 if t.ID == id {
176 tab = t
177 oldPos = i
178 break
179 }
180 }
181 if tab == nil {
182 return false
183 }
184 // Remove from old position
185 tl.Tabs = append(tl.Tabs[:oldPos], tl.Tabs[oldPos+1:]...)
186 // Insert at new position
187 tl.Tabs = append(tl.Tabs[:newPos], append([]*Tab{tab}, tl.Tabs[newPos:]...)...)
188 // Reindex all
189 for i := range tl.Tabs {
190 tl.Tabs[i].Position = i
191 }
192 return true
193}
194
195// Snapshot returns an isolated copy of the TabList safe for concurrent read.
196// The returned TabList has its own slice, Tab values, and workspace tree.
197func (tl *TabList) Snapshot() *TabList {
198 if tl == nil {
199 return nil
200 }
201 tl.mu.RLock()
202 defer tl.mu.RUnlock()
203
204 tabs := make([]*Tab, 0, len(tl.Tabs))
205 for _, tab := range tl.Tabs {
206 tabs = append(tabs, cloneTab(tab))
207 }
208 return &TabList{
209 Tabs: tabs,
210 ActiveTabID: tl.ActiveTabID,
211 PreviousActiveTabID: tl.PreviousActiveTabID,
212 }
213}
214
215func cloneTab(tab *Tab) *Tab {
216 if tab == nil {
217 return nil
218 }
219 cloned := *tab
220 cloned.Workspace = cloneWorkspace(tab.Workspace)
221 return &cloned
222}
223
224func cloneWorkspace(workspace *Workspace) *Workspace {
225 if workspace == nil {
226 return nil
227 }
228 cloned := *workspace
229 cloned.Root = clonePaneNode(workspace.Root, nil)
230 return &cloned
231}
232
233func clonePaneNode(node, parent *PaneNode) *PaneNode {
234 if node == nil {
235 return nil
236 }
237 cloned := *node
238 cloned.Parent = parent
239 cloned.Pane = clonePane(node.Pane)
240 if len(node.Children) > 0 {
241 cloned.Children = make([]*PaneNode, 0, len(node.Children))
242 for _, child := range node.Children {
243 cloned.Children = append(cloned.Children, clonePaneNode(child, &cloned))
244 }
245 } else {
246 cloned.Children = nil
247 }
248 return &cloned
249}
250
251func clonePane(pane *Pane) *Pane {
252 if pane == nil {
253 return nil
254 }
255 cloned := *pane
256 if pane.ParentPaneID != nil {
257 parentPaneID := *pane.ParentPaneID
258 cloned.ParentPaneID = &parentPaneID
259 }
260 return &cloned
261}
262
263// ReplaceFrom replaces this TabList's contents with those from another TabList.
264// This modifies in-place so existing references to this TabList remain valid.
265func (tl *TabList) ReplaceFrom(other *TabList) {
266 snapshot := other.Snapshot()
267
268 tl.mu.Lock()
269 defer tl.mu.Unlock()
270
271 if snapshot == nil {
272 tl.Tabs = make([]*Tab, 0)
273 tl.ActiveTabID = ""
274 tl.PreviousActiveTabID = ""
275 return
276 }
277 tl.Tabs = snapshot.Tabs
278 tl.ActiveTabID = snapshot.ActiveTabID
279 tl.PreviousActiveTabID = snapshot.PreviousActiveTabID
280}