1use std::sync::Arc;
4use std::sync::atomic::AtomicBool;
5
6use tokio::runtime::Runtime;
7use tokio::sync::RwLock;
8use tokio::task::JoinHandle;
9
10use crate::config::Config;
11use crate::pane::bell::BellState;
12use crate::pane::mouse::MouseState;
13use crate::pane::render_cache::RenderCache;
14use crate::scroll_state::ScrollState;
15use crate::session_logger::{SharedSessionLogger, create_shared_logger};
16use crate::tab::{
17 apply_login_shell_flag, build_shell_env, configure_terminal_from_config, get_shell_command,
18};
19use crate::ui_constants::VISUAL_BELL_FLASH_DURATION_MS;
20use par_term_terminal::TerminalManager;
21
22use super::bounds::PaneBounds;
23use super::common::{PaneBackground, PaneId, RestartState};
24
25pub struct Pane {
38 pub id: PaneId,
40 pub terminal: Arc<RwLock<TerminalManager>>,
45 pub scroll_state: ScrollState,
47 pub mouse: MouseState,
49 pub bell: BellState,
51 pub cache: RenderCache,
53 pub refresh_task: Option<JoinHandle<()>>,
55 pub working_directory: Option<String>,
57 pub last_activity_time: std::time::Instant,
59 pub last_seen_generation: u64,
61 pub anti_idle_last_activity: std::time::Instant,
63 pub anti_idle_last_generation: u64,
65 pub silence_notified: bool,
67 pub exit_notified: bool,
69 pub session_logger: SharedSessionLogger,
71 pub bounds: PaneBounds,
73 pub background: PaneBackground,
75 pub title: String,
77 pub has_default_title: bool,
79 pub user_named: bool,
84 pub restart_state: Option<RestartState>,
86 pub is_active: Arc<AtomicBool>,
88 pub(crate) shutdown_fast: bool,
90}
91
92impl Pane {
93 pub fn new(
95 id: PaneId,
96 config: &Config,
97 _runtime: Arc<Runtime>,
98 working_directory: Option<String>,
99 ) -> anyhow::Result<Self> {
100 let mut terminal = TerminalManager::new_with_scrollback(
102 config.cols,
103 config.rows,
104 config.scrollback.scrollback_lines,
105 )?;
106
107 configure_terminal_from_config(&mut terminal, config);
109
110 let work_dir = working_directory
112 .as_deref()
113 .or(config.shell.working_directory.as_deref());
114
115 #[allow(unused_mut)] let (shell_cmd, mut shell_args) = get_shell_command(config);
118 apply_login_shell_flag(&mut shell_args, config);
119
120 let shell_args_deref = shell_args.as_deref();
121 let shell_env = build_shell_env(config.shell.shell_env.as_ref());
122 terminal.spawn_custom_shell_with_dir(
123 &shell_cmd,
124 shell_args_deref,
125 work_dir,
126 shell_env.as_ref(),
127 )?;
128
129 let session_logger = create_shared_logger();
131
132 let terminal = Arc::new(RwLock::new(terminal));
133
134 Ok(Self {
135 id,
136 terminal,
137 scroll_state: ScrollState::new(),
138 mouse: MouseState::new(),
139 bell: BellState::new(),
140 cache: RenderCache::new(),
141 refresh_task: None,
142 working_directory: working_directory.or_else(|| config.shell.working_directory.clone()),
143 last_activity_time: std::time::Instant::now(),
144 last_seen_generation: 0,
145 anti_idle_last_activity: std::time::Instant::now(),
146 anti_idle_last_generation: 0,
147 silence_notified: false,
148 exit_notified: false,
149 session_logger,
150 bounds: PaneBounds::default(),
151 title: String::new(),
152 has_default_title: true,
153 user_named: false,
154 background: PaneBackground::new(),
155 restart_state: None,
156 is_active: Arc::new(AtomicBool::new(false)),
157 shutdown_fast: false,
158 })
159 }
160
161 pub fn new_with_command(
166 id: PaneId,
167 config: &Config,
168 _runtime: Arc<Runtime>,
169 working_directory: Option<String>,
170 command: String,
171 args: Vec<String>,
172 ) -> anyhow::Result<Self> {
173 let mut terminal = TerminalManager::new_with_scrollback(
175 config.cols,
176 config.rows,
177 config.scrollback.scrollback_lines,
178 )?;
179
180 configure_terminal_from_config(&mut terminal, config);
182
183 let work_dir = working_directory
185 .as_deref()
186 .or(config.shell.working_directory.as_deref());
187
188 let shell_env = build_shell_env(config.shell.shell_env.as_ref());
190 terminal.spawn_custom_shell_with_dir(
191 &command,
192 Some(args.as_slice()),
193 work_dir,
194 shell_env.as_ref(),
195 )?;
196
197 let session_logger = create_shared_logger();
199
200 let terminal = Arc::new(RwLock::new(terminal));
201
202 Ok(Self {
203 id,
204 terminal,
205 scroll_state: ScrollState::new(),
206 mouse: MouseState::new(),
207 bell: BellState::new(),
208 cache: RenderCache::new(),
209 refresh_task: None,
210 working_directory: working_directory.or_else(|| config.shell.working_directory.clone()),
211 last_activity_time: std::time::Instant::now(),
212 last_seen_generation: 0,
213 anti_idle_last_activity: std::time::Instant::now(),
214 anti_idle_last_generation: 0,
215 silence_notified: false,
216 exit_notified: false,
217 session_logger,
218 bounds: PaneBounds::default(),
219 title: String::new(),
220 has_default_title: true,
221 user_named: false,
222 background: PaneBackground::new(),
223 restart_state: None,
224 is_active: Arc::new(AtomicBool::new(false)),
225 shutdown_fast: false,
226 })
227 }
228
229 pub fn new_wrapping_terminal(
245 id: PaneId,
246 terminal: Arc<RwLock<TerminalManager>>,
247 working_directory: Option<String>,
248 is_active: Arc<AtomicBool>,
249 ) -> Self {
250 let session_logger = create_shared_logger();
251
252 Self {
253 id,
254 terminal,
255 scroll_state: ScrollState::new(),
256 mouse: MouseState::new(),
257 bell: BellState::new(),
258 cache: RenderCache::new(),
259 refresh_task: None,
260 working_directory,
261 last_activity_time: std::time::Instant::now(),
262 last_seen_generation: 0,
263 anti_idle_last_activity: std::time::Instant::now(),
264 anti_idle_last_generation: 0,
265 silence_notified: false,
266 exit_notified: false,
267 session_logger,
268 bounds: PaneBounds::default(),
269 title: String::new(),
270 has_default_title: true,
271 user_named: false,
272 background: PaneBackground::new(),
273 restart_state: None,
274 is_active,
275 shutdown_fast: false,
276 }
277 }
278
279 pub fn new_for_tmux(
284 id: PaneId,
285 config: &Config,
286 _runtime: Arc<Runtime>,
287 ) -> anyhow::Result<Self> {
288 let mut terminal = TerminalManager::new_with_scrollback(
290 config.cols,
291 config.rows,
292 config.scrollback.scrollback_lines,
293 )?;
294
295 configure_terminal_from_config(&mut terminal, config);
297
298 terminal.sync_triggers(&config.automation.triggers);
303
304 let session_logger = create_shared_logger();
307
308 let terminal = Arc::new(RwLock::new(terminal));
309
310 Ok(Self {
311 id,
312 terminal,
313 scroll_state: ScrollState::new(),
314 mouse: MouseState::new(),
315 bell: BellState::new(),
316 cache: RenderCache::new(),
317 refresh_task: None,
318 working_directory: None,
319 last_activity_time: std::time::Instant::now(),
320 last_seen_generation: 0,
321 anti_idle_last_activity: std::time::Instant::now(),
322 anti_idle_last_generation: 0,
323 silence_notified: false,
324 exit_notified: false,
325 session_logger,
326 bounds: PaneBounds::default(),
327 title: String::new(),
328 has_default_title: true,
329 user_named: false,
330 background: PaneBackground::new(),
331 restart_state: None,
332 is_active: Arc::new(AtomicBool::new(false)),
333 shutdown_fast: false,
334 })
335 }
336
337 pub fn is_bell_active(&self) -> bool {
339 if let Some(flash_start) = self.bell.visual_flash {
340 flash_start.elapsed().as_millis() < VISUAL_BELL_FLASH_DURATION_MS
341 } else {
342 false
343 }
344 }
345
346 pub fn is_running(&self) -> bool {
348 if let Ok(term) = self.terminal.try_read() {
349 let running = term.is_running();
350 if !running {
351 crate::debug_info!(
352 "PANE_EXIT",
353 "Pane {} terminal detected as NOT running (shell exited)",
354 self.id
355 );
356 }
357 running
358 } else {
359 true }
361 }
362
363 pub fn get_cwd(&self) -> Option<String> {
365 if let Ok(term) = self.terminal.try_read() {
366 term.shell_integration_cwd()
367 } else {
368 self.working_directory.clone()
369 }
370 }
371
372 pub fn set_background(&mut self, background: PaneBackground) {
374 self.background = background;
375 }
376
377 pub fn background(&self) -> &PaneBackground {
379 &self.background
380 }
381
382 pub fn set_background_image(&mut self, path: Option<String>) {
384 self.background.image_path = path;
385 }
386
387 pub fn get_background_image(&self) -> Option<&str> {
389 self.background.image_path.as_deref()
390 }
391}
392
393impl Drop for Pane {
394 fn drop(&mut self) {
395 if self.shutdown_fast {
396 log::info!(
397 "Fast-dropping pane {} (cleanup handled externally)",
398 self.id
399 );
400 return;
401 }
402
403 log::info!("Dropping pane {}", self.id);
404
405 if let Some(ref mut logger) = *self.session_logger.lock() {
407 match logger.stop() {
408 Ok(path) => {
409 log::info!("Session log saved to: {:?}", path);
410 }
411 Err(e) => {
412 log::warn!("Failed to stop session logging: {}", e);
413 }
414 }
415 }
416
417 self.stop_refresh_task();
418
419 const KILL_LOCK_ATTEMPTS: u32 = 20;
427 const KILL_LOCK_RETRY: std::time::Duration = std::time::Duration::from_millis(1);
428
429 let mut acquired = false;
430 for _ in 0..KILL_LOCK_ATTEMPTS {
431 if let Ok(mut term) = self.terminal.try_write() {
432 if term.is_running() {
433 log::info!("Killing terminal for pane {}", self.id);
434 if let Err(e) = term.kill() {
435 log::warn!("Failed to kill terminal for pane {}: {}", self.id, e);
436 }
437 }
438 acquired = true;
439 break;
440 }
441 std::thread::sleep(KILL_LOCK_RETRY);
442 }
443
444 if !acquired {
445 log::warn!(
446 "Pane {}: terminal write lock unavailable at drop; shell may outlive the pane",
447 self.id
448 );
449 }
450 }
451}