par_term/tab/
manager.rs

1//! Tab manager for coordinating multiple terminal tabs within a window
2
3use super::{Tab, TabId};
4use crate::config::Config;
5use anyhow::Result;
6use std::sync::Arc;
7use tokio::runtime::Runtime;
8
9/// Manages multiple terminal tabs within a single window
10pub struct TabManager {
11    /// All tabs in this window, in order
12    tabs: Vec<Tab>,
13    /// Currently active tab ID
14    active_tab_id: Option<TabId>,
15    /// Counter for generating unique tab IDs
16    next_tab_id: TabId,
17}
18
19impl TabManager {
20    /// Create a new empty tab manager
21    pub fn new() -> Self {
22        Self {
23            tabs: Vec::new(),
24            active_tab_id: None,
25            next_tab_id: 1,
26        }
27    }
28
29    /// Create a new tab and return its ID
30    pub fn new_tab(
31        &mut self,
32        config: &Config,
33        runtime: Arc<Runtime>,
34        inherit_cwd_from_active: bool,
35    ) -> Result<TabId> {
36        // Optionally inherit working directory from active tab
37        let working_dir = if inherit_cwd_from_active {
38            self.active_tab().and_then(|tab| tab.get_cwd())
39        } else {
40            None
41        };
42
43        let id = self.next_tab_id;
44        self.next_tab_id += 1;
45
46        let tab = Tab::new(id, config, runtime, working_dir)?;
47        self.tabs.push(tab);
48
49        // Always switch to the new tab
50        self.active_tab_id = Some(id);
51
52        log::info!("Created new tab {} (total: {})", id, self.tabs.len());
53
54        Ok(id)
55    }
56
57    /// Close a tab by ID
58    /// Returns true if this was the last tab (window should close)
59    pub fn close_tab(&mut self, id: TabId) -> bool {
60        let index = self.tabs.iter().position(|t| t.id == id);
61
62        if let Some(idx) = index {
63            log::info!("Closing tab {} (index {})", id, idx);
64
65            // Remove the tab
66            self.tabs.remove(idx);
67
68            // If we closed the active tab, switch to another
69            if self.active_tab_id == Some(id) {
70                self.active_tab_id = if self.tabs.is_empty() {
71                    None
72                } else {
73                    // Prefer the tab at the same index (or previous if at end)
74                    let new_idx = idx.min(self.tabs.len().saturating_sub(1));
75                    Some(self.tabs[new_idx].id)
76                };
77            }
78        }
79
80        self.tabs.is_empty()
81    }
82
83    /// Get a reference to the active tab
84    pub fn active_tab(&self) -> Option<&Tab> {
85        self.active_tab_id
86            .and_then(|id| self.tabs.iter().find(|t| t.id == id))
87    }
88
89    /// Get a mutable reference to the active tab
90    pub fn active_tab_mut(&mut self) -> Option<&mut Tab> {
91        let active_id = self.active_tab_id;
92        active_id.and_then(move |id| self.tabs.iter_mut().find(|t| t.id == id))
93    }
94
95    /// Switch to a tab by ID
96    pub fn switch_to(&mut self, id: TabId) {
97        if self.tabs.iter().any(|t| t.id == id) {
98            // Clear activity indicator when switching to tab
99            if let Some(tab) = self.tabs.iter_mut().find(|t| t.id == id) {
100                tab.has_activity = false;
101            }
102            self.active_tab_id = Some(id);
103            log::debug!("Switched to tab {}", id);
104        }
105    }
106
107    /// Switch to the next tab (wraps around)
108    pub fn next_tab(&mut self) {
109        if self.tabs.len() <= 1 {
110            return;
111        }
112
113        if let Some(active_id) = self.active_tab_id {
114            let current_idx = self
115                .tabs
116                .iter()
117                .position(|t| t.id == active_id)
118                .unwrap_or(0);
119            let next_idx = (current_idx + 1) % self.tabs.len();
120            let next_id = self.tabs[next_idx].id;
121            self.switch_to(next_id);
122        }
123    }
124
125    /// Switch to the previous tab (wraps around)
126    pub fn prev_tab(&mut self) {
127        if self.tabs.len() <= 1 {
128            return;
129        }
130
131        if let Some(active_id) = self.active_tab_id {
132            let current_idx = self
133                .tabs
134                .iter()
135                .position(|t| t.id == active_id)
136                .unwrap_or(0);
137            let prev_idx = if current_idx == 0 {
138                self.tabs.len() - 1
139            } else {
140                current_idx - 1
141            };
142            let prev_id = self.tabs[prev_idx].id;
143            self.switch_to(prev_id);
144        }
145    }
146
147    /// Switch to tab by index (1-based for Cmd+1-9)
148    pub fn switch_to_index(&mut self, index: usize) {
149        if index > 0 && index <= self.tabs.len() {
150            let id = self.tabs[index - 1].id;
151            self.switch_to(id);
152        }
153    }
154
155    /// Move a tab left or right
156    /// direction: -1 for left, 1 for right
157    pub fn move_tab(&mut self, id: TabId, direction: i32) {
158        if let Some(current_idx) = self.tabs.iter().position(|t| t.id == id) {
159            let new_idx = if direction < 0 {
160                if current_idx == 0 {
161                    self.tabs.len() - 1
162                } else {
163                    current_idx - 1
164                }
165            } else if current_idx >= self.tabs.len() - 1 {
166                0
167            } else {
168                current_idx + 1
169            };
170
171            if new_idx != current_idx {
172                let tab = self.tabs.remove(current_idx);
173                self.tabs.insert(new_idx, tab);
174                log::debug!("Moved tab {} from index {} to {}", id, current_idx, new_idx);
175            }
176        }
177    }
178
179    /// Move active tab left
180    pub fn move_active_tab_left(&mut self) {
181        if let Some(id) = self.active_tab_id {
182            self.move_tab(id, -1);
183        }
184    }
185
186    /// Move active tab right
187    pub fn move_active_tab_right(&mut self) {
188        if let Some(id) = self.active_tab_id {
189            self.move_tab(id, 1);
190        }
191    }
192
193    /// Get the number of tabs
194    pub fn tab_count(&self) -> usize {
195        self.tabs.len()
196    }
197
198    /// Check if there are multiple tabs
199    pub fn has_multiple_tabs(&self) -> bool {
200        self.tabs.len() > 1
201    }
202
203    /// Get the active tab ID
204    pub fn active_tab_id(&self) -> Option<TabId> {
205        self.active_tab_id
206    }
207
208    /// Get all tabs as a slice
209    pub fn tabs(&self) -> &[Tab] {
210        &self.tabs
211    }
212
213    /// Get all tabs as mutable slice
214    pub fn tabs_mut(&mut self) -> &mut [Tab] {
215        &mut self.tabs
216    }
217
218    /// Get a tab by ID
219    #[allow(dead_code)]
220    pub fn get_tab(&self, id: TabId) -> Option<&Tab> {
221        self.tabs.iter().find(|t| t.id == id)
222    }
223
224    /// Get a mutable reference to a tab by ID
225    #[allow(dead_code)]
226    pub fn get_tab_mut(&mut self, id: TabId) -> Option<&mut Tab> {
227        self.tabs.iter_mut().find(|t| t.id == id)
228    }
229
230    /// Mark non-active tabs as having activity when they receive output
231    #[allow(dead_code)]
232    pub fn mark_activity(&mut self, tab_id: TabId) {
233        if Some(tab_id) != self.active_tab_id
234            && let Some(tab) = self.get_tab_mut(tab_id)
235        {
236            tab.has_activity = true;
237        }
238    }
239
240    /// Update titles for all tabs
241    pub fn update_all_titles(&mut self) {
242        for tab in &mut self.tabs {
243            tab.update_title();
244        }
245    }
246
247    /// Duplicate the active tab (creates new tab with same working directory)
248    pub fn duplicate_active_tab(
249        &mut self,
250        config: &Config,
251        runtime: Arc<Runtime>,
252    ) -> Result<Option<TabId>> {
253        let working_dir = self.active_tab().and_then(|t| t.get_cwd());
254
255        if working_dir.is_some() || self.active_tab_id.is_some() {
256            let id = self.next_tab_id;
257            self.next_tab_id += 1;
258
259            let tab = Tab::new(id, config, runtime, working_dir)?;
260
261            // Insert after active tab
262            if let Some(active_id) = self.active_tab_id {
263                if let Some(idx) = self.tabs.iter().position(|t| t.id == active_id) {
264                    self.tabs.insert(idx + 1, tab);
265                } else {
266                    self.tabs.push(tab);
267                }
268            } else {
269                self.tabs.push(tab);
270            }
271
272            self.active_tab_id = Some(id);
273            Ok(Some(id))
274        } else {
275            Ok(None)
276        }
277    }
278
279    /// Get index of active tab (0-based)
280    #[allow(dead_code)]
281    pub fn active_tab_index(&self) -> Option<usize> {
282        self.active_tab_id
283            .and_then(|id| self.tabs.iter().position(|t| t.id == id))
284    }
285
286    /// Clean up closed/dead tabs
287    #[allow(dead_code)]
288    pub fn cleanup_dead_tabs(&mut self) {
289        let dead_tabs: Vec<TabId> = self
290            .tabs
291            .iter()
292            .filter(|t| !t.is_running())
293            .map(|t| t.id)
294            .collect();
295
296        for id in dead_tabs {
297            log::info!("Cleaning up dead tab {}", id);
298            self.close_tab(id);
299        }
300    }
301}
302
303impl Default for TabManager {
304    fn default() -> Self {
305        Self::new()
306    }
307}