Rust · 7622 bytes Raw Blame History
1 //! Tab - a collection of panes in a split layout
2
3 use super::pane::{Pane, PaneId};
4 use super::split::{Direction, PaneLayout, SplitDirection, SplitNode};
5 use anyhow::Result;
6 use std::collections::HashMap;
7
8 /// Unique identifier for a tab
9 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10 pub struct TabId(pub u32);
11
12 /// A tab containing one or more panes
13 pub struct Tab {
14 /// Unique identifier
15 pub id: TabId,
16 /// Tab title (from focused pane or custom)
17 pub title: String,
18 /// Custom title set by user/session (prevents OSC override)
19 custom_title: Option<String>,
20 /// Split tree layout
21 pub layout: SplitNode,
22 /// All panes in this tab
23 pub panes: HashMap<PaneId, Pane>,
24 /// Currently focused pane
25 pub focused: PaneId,
26 /// Next pane ID to assign
27 next_pane_id: u32,
28 }
29
30 impl Tab {
31 /// Create a new tab with an initial pane
32 pub fn new(
33 id: TabId,
34 shell: &str,
35 cols: usize,
36 rows: usize,
37 width: u32,
38 height: u32,
39 cwd: Option<&std::path::Path>,
40 ) -> Result<Self> {
41 Self::new_with_command(id, shell, cols, rows, width, height, cwd, None)
42 }
43
44 /// Create a new tab with an initial pane and optional startup command
45 pub fn new_with_command(
46 id: TabId,
47 shell: &str,
48 cols: usize,
49 rows: usize,
50 width: u32,
51 height: u32,
52 cwd: Option<&std::path::Path>,
53 startup_cmd: Option<&str>,
54 ) -> Result<Self> {
55 let pane_id = PaneId(0);
56 let mut pane = Pane::new_with_command(
57 pane_id, shell, cols, rows, width, height, cwd, startup_cmd
58 )?;
59 pane.focused = true;
60
61 let mut panes = HashMap::new();
62 panes.insert(pane_id, pane);
63
64 Ok(Self {
65 id,
66 title: "shell".into(),
67 custom_title: None,
68 layout: SplitNode::leaf(pane_id),
69 panes,
70 focused: pane_id,
71 next_pane_id: 1,
72 })
73 }
74
75 /// Get the focused pane
76 pub fn focused_pane(&self) -> Option<&Pane> {
77 self.panes.get(&self.focused)
78 }
79
80 /// Get the focused pane mutably
81 pub fn focused_pane_mut(&mut self) -> Option<&mut Pane> {
82 self.panes.get_mut(&self.focused)
83 }
84
85 /// Split the focused pane
86 pub fn split(
87 &mut self,
88 direction: SplitDirection,
89 shell: &str,
90 cell_width: f32,
91 cell_height: f32,
92 cwd: Option<&std::path::Path>,
93 ) -> Result<PaneId> {
94 self.split_with_command(direction, shell, cell_width, cell_height, cwd, None)
95 }
96
97 /// Split the focused pane with an optional startup command
98 pub fn split_with_command(
99 &mut self,
100 direction: SplitDirection,
101 shell: &str,
102 cell_width: f32,
103 cell_height: f32,
104 cwd: Option<&std::path::Path>,
105 startup_cmd: Option<&str>,
106 ) -> Result<PaneId> {
107 let focused_pane = self.panes.get(&self.focused).ok_or_else(|| {
108 anyhow::anyhow!("No focused pane")
109 })?;
110
111 // Calculate new pane dimensions (half of current)
112 let (new_width, new_height) = match direction {
113 SplitDirection::Horizontal => (focused_pane.width / 2, focused_pane.height),
114 SplitDirection::Vertical => (focused_pane.width, focused_pane.height / 2),
115 };
116
117 let cols = (new_width as f32 / cell_width) as usize;
118 let rows = (new_height as f32 / cell_height) as usize;
119
120 // Create new pane
121 let new_id = PaneId(self.next_pane_id);
122 self.next_pane_id += 1;
123
124 let new_pane = Pane::new_with_command(
125 new_id, shell, cols, rows, new_width, new_height, cwd, startup_cmd
126 )?;
127 self.panes.insert(new_id, new_pane);
128
129 // Update layout tree
130 if let Some(node) = self.layout.find_mut(self.focused) {
131 node.split(direction, new_id, true);
132 }
133
134 // Focus the new pane
135 self.focus(new_id);
136
137 Ok(new_id)
138 }
139
140 /// Close a pane
141 pub fn close_pane(&mut self, id: PaneId) -> Option<PaneId> {
142 // Can't close if it's the only pane
143 if self.panes.len() <= 1 {
144 return None;
145 }
146
147 // Remove from layout and get sibling to focus
148 let sibling = self.layout.remove(id);
149
150 // Remove the pane
151 self.panes.remove(&id);
152
153 // Focus sibling if we closed the focused pane
154 if self.focused == id {
155 if let Some(new_focus) = sibling.or_else(|| self.layout.first_pane()) {
156 self.focus(new_focus);
157 }
158 }
159
160 sibling
161 }
162
163 /// Focus a pane
164 pub fn focus(&mut self, id: PaneId) {
165 if !self.panes.contains_key(&id) {
166 return;
167 }
168
169 // Unfocus old pane
170 if let Some(old) = self.panes.get_mut(&self.focused) {
171 old.focused = false;
172 }
173
174 // Focus new pane
175 self.focused = id;
176 if let Some(new) = self.panes.get_mut(&id) {
177 new.focused = true;
178 new.mark_dirty();
179 }
180 }
181
182 /// Focus pane in direction
183 pub fn focus_direction(&mut self, direction: Direction, width: u32, height: u32) {
184 let layouts = self.layout.layout(0, 0, width, height);
185 if let Some(neighbor) = self.layout.find_neighbor(self.focused, direction, &layouts) {
186 self.focus(neighbor);
187 }
188 }
189
190 /// Recalculate layouts for all panes
191 pub fn relayout(&mut self, width: u32, height: u32, cell_width: f32, cell_height: f32) -> Result<()> {
192 let layouts = self.layout.layout(0, 0, width, height);
193
194 for layout in layouts {
195 if let Some(pane) = self.panes.get_mut(&layout.id) {
196 let cols = (layout.width as f32 / cell_width) as usize;
197 let rows = (layout.height as f32 / cell_height) as usize;
198
199 pane.set_position(layout.x, layout.y);
200 pane.resize(cols.max(1), rows.max(1), layout.width, layout.height)?;
201 }
202 }
203
204 Ok(())
205 }
206
207 /// Get layout for all panes
208 pub fn get_layouts(&self, width: u32, height: u32) -> Vec<PaneLayout> {
209 self.layout.layout(0, 0, width, height)
210 }
211
212 /// Check if any pane has exited
213 pub fn check_exits(&mut self) -> Vec<PaneId> {
214 let exited: Vec<_> = self
215 .panes
216 .iter()
217 .filter(|(_, p)| !p.is_alive())
218 .map(|(id, _)| *id)
219 .collect();
220
221 exited
222 }
223
224 /// Get all pane IDs
225 pub fn pane_ids(&self) -> Vec<PaneId> {
226 self.layout.all_panes()
227 }
228
229 /// Update tab title from focused pane's terminal title (only if no custom title set)
230 pub fn update_title(&mut self) {
231 // Don't override custom titles
232 if self.custom_title.is_some() {
233 return;
234 }
235 if let Some(pane) = self.panes.get(&self.focused) {
236 let title = pane.terminal.title();
237 if !title.is_empty() {
238 self.title = title.to_string();
239 }
240 }
241 }
242
243 /// Set a custom title for this tab (prevents OSC title updates)
244 pub fn set_title(&mut self, title: String) {
245 self.title = title.clone();
246 self.custom_title = Some(title);
247 }
248
249 /// Clear custom title (allows OSC title updates again)
250 pub fn clear_custom_title(&mut self) {
251 self.custom_title = None;
252 }
253
254 /// Resize the focused pane by adjusting split ratio
255 pub fn resize_focused_pane(&mut self, delta: f32) -> bool {
256 self.layout.resize_pane(self.focused, delta)
257 }
258 }
259