1use super::{Tab, TabId};
4use crate::config::Config;
5use crate::profile::Profile;
6use anyhow::Result;
7use std::sync::Arc;
8use tokio::runtime::Runtime;
9
10pub struct TabManager {
12 tabs: Vec<Tab>,
14 active_tab_id: Option<TabId>,
16 next_tab_id: TabId,
18}
19
20impl TabManager {
21 pub fn new() -> Self {
23 Self {
24 tabs: Vec::new(),
25 active_tab_id: None,
26 next_tab_id: 1,
27 }
28 }
29
30 pub fn new_tab(
41 &mut self,
42 config: &Config,
43 runtime: Arc<Runtime>,
44 inherit_cwd_from_active: bool,
45 grid_size: Option<(usize, usize)>,
46 ) -> Result<TabId> {
47 let working_dir = if inherit_cwd_from_active {
49 self.active_tab().and_then(|tab| tab.get_cwd())
50 } else {
51 None
52 };
53
54 let id = self.next_tab_id;
55 self.next_tab_id += 1;
56
57 let tab_number = self.tabs.len() + 1;
59 let tab = Tab::new(id, tab_number, config, runtime, working_dir, grid_size)?;
60 self.tabs.push(tab);
61
62 self.active_tab_id = Some(id);
64
65 log::info!("Created new tab {} (total: {})", id, self.tabs.len());
66
67 Ok(id)
68 }
69
70 pub fn new_tab_with_cwd(
74 &mut self,
75 config: &Config,
76 runtime: Arc<Runtime>,
77 working_dir: Option<String>,
78 grid_size: Option<(usize, usize)>,
79 ) -> Result<TabId> {
80 let id = self.next_tab_id;
81 self.next_tab_id += 1;
82
83 let tab_number = self.tabs.len() + 1;
84 let tab = Tab::new(id, tab_number, config, runtime, working_dir, grid_size)?;
85 self.tabs.push(tab);
86
87 self.active_tab_id = Some(id);
89
90 log::info!(
91 "Created new tab {} with cwd (total: {})",
92 id,
93 self.tabs.len()
94 );
95
96 Ok(id)
97 }
98
99 pub fn new_tab_from_profile(
109 &mut self,
110 config: &Config,
111 runtime: Arc<Runtime>,
112 profile: &Profile,
113 grid_size: Option<(usize, usize)>,
114 ) -> Result<TabId> {
115 let id = self.next_tab_id;
116 self.next_tab_id += 1;
117
118 let tab = Tab::new_from_profile(id, config, runtime, profile, grid_size)?;
119 self.tabs.push(tab);
120
121 self.active_tab_id = Some(id);
123
124 log::info!(
125 "Created new tab {} from profile '{}' (total: {})",
126 id,
127 profile.name,
128 self.tabs.len()
129 );
130
131 Ok(id)
132 }
133
134 pub fn close_tab(&mut self, id: TabId) -> bool {
137 let index = self.tabs.iter().position(|t| t.id == id);
138
139 if let Some(idx) = index {
140 log::info!("Closing tab {} (index {})", id, idx);
141
142 self.tabs.remove(idx);
144
145 if self.active_tab_id == Some(id) {
147 self.active_tab_id = if self.tabs.is_empty() {
148 None
149 } else {
150 let new_idx = idx.min(self.tabs.len().saturating_sub(1));
152 Some(self.tabs[new_idx].id)
153 };
154 }
155
156 self.renumber_default_tabs();
158 }
159
160 self.tabs.is_empty()
161 }
162
163 pub fn remove_tab(&mut self, id: TabId) -> Option<(Tab, bool)> {
170 let idx = self.tabs.iter().position(|t| t.id == id)?;
171
172 log::info!("Removing tab {} (index {}) without dropping", id, idx);
173
174 let tab = self.tabs.remove(idx);
175
176 if self.active_tab_id == Some(id) {
178 self.active_tab_id = if self.tabs.is_empty() {
179 None
180 } else {
181 let new_idx = idx.min(self.tabs.len().saturating_sub(1));
182 Some(self.tabs[new_idx].id)
183 };
184 }
185
186 self.renumber_default_tabs();
187 let is_empty = self.tabs.is_empty();
188 Some((tab, is_empty))
189 }
190
191 pub fn insert_tab_at(&mut self, tab: Tab, index: usize) {
195 let clamped = index.min(self.tabs.len());
196 let id = tab.id;
197 self.tabs.insert(clamped, tab);
198 self.active_tab_id = Some(id);
199 self.renumber_default_tabs();
200 log::info!(
201 "Inserted tab {} at index {} (total: {})",
202 id,
203 clamped,
204 self.tabs.len()
205 );
206 }
207
208 fn renumber_default_tabs(&mut self) {
210 for (idx, tab) in self.tabs.iter_mut().enumerate() {
211 tab.set_default_title(idx + 1);
212 }
213 }
214
215 pub fn active_tab(&self) -> Option<&Tab> {
217 self.active_tab_id
218 .and_then(|id| self.tabs.iter().find(|t| t.id == id))
219 }
220
221 pub fn active_tab_mut(&mut self) -> Option<&mut Tab> {
223 let active_id = self.active_tab_id;
224 active_id.and_then(move |id| self.tabs.iter_mut().find(|t| t.id == id))
225 }
226
227 pub fn switch_to(&mut self, id: TabId) {
229 if self.tabs.iter().any(|t| t.id == id) {
230 if let Some(tab) = self.tabs.iter_mut().find(|t| t.id == id) {
232 tab.has_activity = false;
233 }
234 self.active_tab_id = Some(id);
235 log::debug!("Switched to tab {}", id);
236 }
237 }
238
239 pub fn next_tab(&mut self) {
241 if self.tabs.len() <= 1 {
242 return;
243 }
244
245 if let Some(active_id) = self.active_tab_id {
246 let current_idx = self
247 .tabs
248 .iter()
249 .position(|t| t.id == active_id)
250 .unwrap_or(0);
251 let next_idx = (current_idx + 1) % self.tabs.len();
252 let next_id = self.tabs[next_idx].id;
253 self.switch_to(next_id);
254 }
255 }
256
257 pub fn prev_tab(&mut self) {
259 if self.tabs.len() <= 1 {
260 return;
261 }
262
263 if let Some(active_id) = self.active_tab_id {
264 let current_idx = self
265 .tabs
266 .iter()
267 .position(|t| t.id == active_id)
268 .unwrap_or(0);
269 let prev_idx = if current_idx == 0 {
270 self.tabs.len() - 1
271 } else {
272 current_idx - 1
273 };
274 let prev_id = self.tabs[prev_idx].id;
275 self.switch_to(prev_id);
276 }
277 }
278
279 pub fn switch_to_index(&mut self, index: usize) {
281 if index > 0 && index <= self.tabs.len() {
282 let id = self.tabs[index - 1].id;
283 self.switch_to(id);
284 }
285 }
286
287 pub fn move_tab(&mut self, id: TabId, direction: i32) {
290 if let Some(current_idx) = self.tabs.iter().position(|t| t.id == id) {
291 let new_idx = if direction < 0 {
292 if current_idx == 0 {
293 self.tabs.len() - 1
294 } else {
295 current_idx - 1
296 }
297 } else if current_idx >= self.tabs.len() - 1 {
298 0
299 } else {
300 current_idx + 1
301 };
302
303 if new_idx != current_idx {
304 let tab = self.tabs.remove(current_idx);
305 self.tabs.insert(new_idx, tab);
306 log::debug!("Moved tab {} from index {} to {}", id, current_idx, new_idx);
307 self.renumber_default_tabs();
309 }
310 }
311 }
312
313 pub fn move_tab_to_index(&mut self, id: TabId, target_index: usize) -> bool {
316 let current_idx = match self.tabs.iter().position(|t| t.id == id) {
317 Some(idx) => idx,
318 None => return false,
319 };
320
321 let clamped_target = target_index.min(self.tabs.len().saturating_sub(1));
322 if clamped_target == current_idx {
323 return false;
324 }
325
326 let tab = self.tabs.remove(current_idx);
327 self.tabs.insert(clamped_target, tab);
328 log::debug!(
329 "Moved tab {} from index {} to {}",
330 id,
331 current_idx,
332 clamped_target
333 );
334 self.renumber_default_tabs();
335 true
336 }
337
338 pub fn move_active_tab_left(&mut self) {
340 if let Some(id) = self.active_tab_id {
341 self.move_tab(id, -1);
342 }
343 }
344
345 pub fn move_active_tab_right(&mut self) {
347 if let Some(id) = self.active_tab_id {
348 self.move_tab(id, 1);
349 }
350 }
351
352 pub fn tab_count(&self) -> usize {
354 self.tabs.len()
355 }
356
357 pub fn has_multiple_tabs(&self) -> bool {
359 self.tabs.len() > 1
360 }
361
362 pub fn active_tab_id(&self) -> Option<TabId> {
364 self.active_tab_id
365 }
366
367 pub fn tabs(&self) -> &[Tab] {
369 &self.tabs
370 }
371
372 pub fn tabs_mut(&mut self) -> &mut [Tab] {
374 &mut self.tabs
375 }
376
377 pub fn drain_tabs(&mut self) -> Vec<Tab> {
382 self.active_tab_id = None;
383 std::mem::take(&mut self.tabs)
384 }
385
386 #[allow(dead_code)]
388 pub fn get_tab(&self, id: TabId) -> Option<&Tab> {
389 self.tabs.iter().find(|t| t.id == id)
390 }
391
392 #[allow(dead_code)]
394 pub fn get_tab_mut(&mut self, id: TabId) -> Option<&mut Tab> {
395 self.tabs.iter_mut().find(|t| t.id == id)
396 }
397
398 #[allow(dead_code)]
400 pub fn mark_activity(&mut self, tab_id: TabId) {
401 if Some(tab_id) != self.active_tab_id
402 && let Some(tab) = self.get_tab_mut(tab_id)
403 {
404 tab.has_activity = true;
405 }
406 }
407
408 pub fn update_all_titles(&mut self) {
410 for tab in &mut self.tabs {
411 tab.update_title();
412 }
413 }
414
415 pub fn duplicate_active_tab(
422 &mut self,
423 config: &Config,
424 runtime: Arc<Runtime>,
425 grid_size: Option<(usize, usize)>,
426 ) -> Result<Option<TabId>> {
427 if let Some(tab_id) = self.active_tab_id {
428 self.duplicate_tab_by_id(tab_id, config, runtime, grid_size)
429 } else {
430 Ok(None)
431 }
432 }
433
434 pub fn duplicate_tab_by_id(
442 &mut self,
443 source_tab_id: TabId,
444 config: &Config,
445 runtime: Arc<Runtime>,
446 grid_size: Option<(usize, usize)>,
447 ) -> Result<Option<TabId>> {
448 let source_idx = self.tabs.iter().position(|t| t.id == source_tab_id);
450 let source_idx = match source_idx {
451 Some(idx) => idx,
452 None => return Ok(None),
453 };
454 let working_dir = self.tabs[source_idx].get_cwd();
455 let custom_color = self.tabs[source_idx].custom_color;
456
457 let id = self.next_tab_id;
458 self.next_tab_id += 1;
459
460 let tab_number = self.tabs.len() + 1;
462 let mut tab = Tab::new(id, tab_number, config, runtime, working_dir, grid_size)?;
463
464 if let Some(color) = custom_color {
466 tab.set_custom_color(color);
467 }
468
469 self.tabs.insert(source_idx + 1, tab);
471
472 self.active_tab_id = Some(id);
473 Ok(Some(id))
474 }
475
476 #[allow(dead_code)]
478 pub fn active_tab_index(&self) -> Option<usize> {
479 self.active_tab_id
480 .and_then(|id| self.tabs.iter().position(|t| t.id == id))
481 }
482
483 #[allow(dead_code)]
485 pub fn cleanup_dead_tabs(&mut self) {
486 let dead_tabs: Vec<TabId> = self
487 .tabs
488 .iter()
489 .filter(|t| !t.is_running())
490 .map(|t| t.id)
491 .collect();
492
493 for id in dead_tabs {
494 log::info!("Cleaning up dead tab {}", id);
495 self.close_tab(id);
496 }
497 }
498}
499
500impl Default for TabManager {
501 fn default() -> Self {
502 Self::new()
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509
510 fn manager_with_ids(ids: &[TabId]) -> TabManager {
512 let mut mgr = TabManager::new();
513 for &id in ids {
514 let tab_number = mgr.tabs.len() + 1;
515 mgr.tabs.push(Tab::new_stub(id, tab_number));
517 mgr.next_tab_id = mgr.next_tab_id.max(id + 1);
518 }
519 if let Some(last) = ids.last() {
520 mgr.active_tab_id = Some(*last);
521 }
522 mgr
523 }
524
525 #[test]
526 fn move_tab_to_index_forward() {
527 let mut mgr = manager_with_ids(&[1, 2, 3, 4]);
528 assert!(mgr.move_tab_to_index(1, 2));
530 let ids: Vec<TabId> = mgr.tabs.iter().map(|t| t.id).collect();
531 assert_eq!(ids, vec![2, 3, 1, 4]);
532 }
533
534 #[test]
535 fn move_tab_to_index_backward() {
536 let mut mgr = manager_with_ids(&[1, 2, 3, 4]);
537 assert!(mgr.move_tab_to_index(3, 0));
539 let ids: Vec<TabId> = mgr.tabs.iter().map(|t| t.id).collect();
540 assert_eq!(ids, vec![3, 1, 2, 4]);
541 }
542
543 #[test]
544 fn move_tab_to_index_same_position() {
545 let mut mgr = manager_with_ids(&[1, 2, 3]);
546 assert!(!mgr.move_tab_to_index(2, 1));
548 let ids: Vec<TabId> = mgr.tabs.iter().map(|t| t.id).collect();
549 assert_eq!(ids, vec![1, 2, 3]);
550 }
551
552 #[test]
553 fn move_tab_to_index_out_of_bounds_clamped() {
554 let mut mgr = manager_with_ids(&[1, 2, 3]);
555 assert!(mgr.move_tab_to_index(1, 100));
557 let ids: Vec<TabId> = mgr.tabs.iter().map(|t| t.id).collect();
558 assert_eq!(ids, vec![2, 3, 1]);
559 }
560
561 #[test]
562 fn move_tab_to_index_invalid_id() {
563 let mut mgr = manager_with_ids(&[1, 2, 3]);
564 assert!(!mgr.move_tab_to_index(99, 0));
566 let ids: Vec<TabId> = mgr.tabs.iter().map(|t| t.id).collect();
567 assert_eq!(ids, vec![1, 2, 3]);
568 }
569
570 #[test]
571 fn move_tab_to_index_to_end() {
572 let mut mgr = manager_with_ids(&[1, 2, 3]);
573 assert!(mgr.move_tab_to_index(1, 2));
575 let ids: Vec<TabId> = mgr.tabs.iter().map(|t| t.id).collect();
576 assert_eq!(ids, vec![2, 3, 1]);
577 }
578
579 #[test]
580 fn move_tab_to_index_to_start() {
581 let mut mgr = manager_with_ids(&[1, 2, 3]);
582 assert!(mgr.move_tab_to_index(3, 0));
584 let ids: Vec<TabId> = mgr.tabs.iter().map(|t| t.id).collect();
585 assert_eq!(ids, vec![3, 1, 2]);
586 }
587}