1mod manager;
9
10pub use manager::TabManager;
11
12use crate::app::bell::BellState;
13use crate::app::mouse::MouseState;
14use crate::app::render_cache::RenderCache;
15use crate::config::Config;
16use crate::scroll_state::ScrollState;
17use crate::terminal::TerminalManager;
18use std::sync::Arc;
19use tokio::runtime::Runtime;
20use tokio::sync::Mutex;
21use tokio::task::JoinHandle;
22
23pub type TabId = u64;
25
26pub struct Tab {
28 pub id: TabId,
30 pub terminal: Arc<Mutex<TerminalManager>>,
32 pub title: String,
34 pub has_activity: bool,
36 pub scroll_state: ScrollState,
38 pub mouse: MouseState,
40 pub bell: BellState,
42 pub cache: RenderCache,
44 pub refresh_task: Option<JoinHandle<()>>,
46 pub working_directory: Option<String>,
48}
49
50impl Tab {
51 pub fn new(
53 id: TabId,
54 config: &Config,
55 _runtime: Arc<Runtime>,
56 working_directory: Option<String>,
57 ) -> anyhow::Result<Self> {
58 let initial_opacity = config.window_opacity;
59
60 let mut terminal = TerminalManager::new_with_scrollback(
62 config.cols,
63 config.rows,
64 config.scrollback_lines,
65 )?;
66
67 terminal.set_theme(config.load_theme());
69
70 terminal.set_max_clipboard_sync_events(config.clipboard_max_sync_events);
72 terminal.set_max_clipboard_event_bytes(config.clipboard_max_event_bytes);
73
74 let work_dir = working_directory
76 .as_deref()
77 .or(config.working_directory.as_deref());
78
79 let (shell_cmd, mut shell_args) = if let Some(ref custom) = config.custom_shell {
81 (custom.clone(), config.shell_args.clone())
82 } else {
83 #[cfg(target_os = "windows")]
84 {
85 ("powershell.exe".to_string(), None)
86 }
87 #[cfg(not(target_os = "windows"))]
88 {
89 (
90 std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string()),
91 None,
92 )
93 }
94 };
95
96 #[cfg(not(target_os = "windows"))]
98 if config.login_shell {
99 let args = shell_args.get_or_insert_with(Vec::new);
100 if !args.iter().any(|a| a == "-l" || a == "--login") {
101 args.insert(0, "-l".to_string());
102 }
103 }
104
105 let shell_args_deref = shell_args.as_deref();
106 let shell_env = config.shell_env.as_ref();
107 terminal.spawn_custom_shell_with_dir(&shell_cmd, shell_args_deref, work_dir, shell_env)?;
108
109 let terminal = Arc::new(Mutex::new(terminal));
110
111 let title = format!("Tab {}", id);
113
114 Ok(Self {
115 id,
116 terminal,
117 title,
118 has_activity: false,
119 scroll_state: ScrollState::new(),
120 mouse: MouseState::new(),
121 bell: BellState::new(),
122 cache: RenderCache::new(initial_opacity),
123 refresh_task: None,
124 working_directory: working_directory.or_else(|| config.working_directory.clone()),
125 })
126 }
127
128 pub fn is_bell_active(&self) -> bool {
130 const FLASH_DURATION_MS: u128 = 150;
131 if let Some(flash_start) = self.bell.visual_flash {
132 flash_start.elapsed().as_millis() < FLASH_DURATION_MS
133 } else {
134 false
135 }
136 }
137
138 pub fn update_title(&mut self) {
140 if let Ok(term) = self.terminal.try_lock() {
141 let osc_title = term.get_title();
142 if !osc_title.is_empty() {
143 self.title = osc_title;
144 } else if let Some(cwd) = term.shell_integration_cwd() {
145 let abbreviated = if let Some(home) = dirs::home_dir() {
147 cwd.replace(&home.to_string_lossy().to_string(), "~")
148 } else {
149 cwd
150 };
151 if let Some(last) = abbreviated.rsplit('/').next() {
153 if !last.is_empty() {
154 self.title = last.to_string();
155 } else {
156 self.title = abbreviated;
157 }
158 } else {
159 self.title = abbreviated;
160 }
161 }
162 }
164 }
165
166 #[allow(dead_code)]
168 pub fn is_running(&self) -> bool {
169 if let Ok(term) = self.terminal.try_lock() {
170 term.is_running()
171 } else {
172 true }
174 }
175
176 pub fn get_cwd(&self) -> Option<String> {
178 if let Ok(term) = self.terminal.try_lock() {
179 term.shell_integration_cwd()
180 } else {
181 self.working_directory.clone()
182 }
183 }
184
185 pub fn start_refresh_task(
187 &mut self,
188 runtime: Arc<Runtime>,
189 window: Arc<winit::window::Window>,
190 max_fps: u32,
191 ) {
192 let terminal_clone = Arc::clone(&self.terminal);
193 let refresh_interval_ms = 1000 / max_fps.max(1);
194
195 let handle = runtime.spawn(async move {
196 let mut interval = tokio::time::interval(tokio::time::Duration::from_millis(
197 refresh_interval_ms as u64,
198 ));
199 let mut last_gen = 0;
200
201 loop {
202 interval.tick().await;
203
204 let should_redraw = if let Ok(term) = terminal_clone.try_lock() {
205 let current_gen = term.update_generation();
206 if current_gen > last_gen {
207 last_gen = current_gen;
208 true
209 } else {
210 term.has_updates()
211 }
212 } else {
213 false
214 };
215
216 if should_redraw {
217 window.request_redraw();
218 }
219 }
220 });
221
222 self.refresh_task = Some(handle);
223 }
224
225 pub fn stop_refresh_task(&mut self) {
227 if let Some(handle) = self.refresh_task.take() {
228 handle.abort();
229 }
230 }
231}
232
233impl Drop for Tab {
234 fn drop(&mut self) {
235 log::info!("Dropping tab {}", self.id);
236 self.stop_refresh_task();
237
238 std::thread::sleep(std::time::Duration::from_millis(50));
240
241 if let Ok(mut term) = self.terminal.try_lock()
243 && term.is_running()
244 {
245 log::info!("Killing terminal for tab {}", self.id);
246 let _ = term.kill();
247 }
248 }
249}