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 restart_state: Option<RestartState>,
81 pub is_active: Arc<AtomicBool>,
83 pub(crate) shutdown_fast: bool,
85}
86
87impl Pane {
88 pub fn new(
90 id: PaneId,
91 config: &Config,
92 _runtime: Arc<Runtime>,
93 working_directory: Option<String>,
94 ) -> anyhow::Result<Self> {
95 let mut terminal = TerminalManager::new_with_scrollback(
97 config.cols,
98 config.rows,
99 config.scrollback.scrollback_lines,
100 )?;
101
102 configure_terminal_from_config(&mut terminal, config);
104
105 let work_dir = working_directory
107 .as_deref()
108 .or(config.shell.working_directory.as_deref());
109
110 #[allow(unused_mut)] let (shell_cmd, mut shell_args) = get_shell_command(config);
113 apply_login_shell_flag(&mut shell_args, config);
114
115 let shell_args_deref = shell_args.as_deref();
116 let shell_env = build_shell_env(config.shell.shell_env.as_ref());
117 terminal.spawn_custom_shell_with_dir(
118 &shell_cmd,
119 shell_args_deref,
120 work_dir,
121 shell_env.as_ref(),
122 )?;
123
124 let session_logger = create_shared_logger();
126
127 let terminal = Arc::new(RwLock::new(terminal));
128
129 Ok(Self {
130 id,
131 terminal,
132 scroll_state: ScrollState::new(),
133 mouse: MouseState::new(),
134 bell: BellState::new(),
135 cache: RenderCache::new(),
136 refresh_task: None,
137 working_directory: working_directory.or_else(|| config.shell.working_directory.clone()),
138 last_activity_time: std::time::Instant::now(),
139 last_seen_generation: 0,
140 anti_idle_last_activity: std::time::Instant::now(),
141 anti_idle_last_generation: 0,
142 silence_notified: false,
143 exit_notified: false,
144 session_logger,
145 bounds: PaneBounds::default(),
146 title: String::new(),
147 has_default_title: true,
148 background: PaneBackground::new(),
149 restart_state: None,
150 is_active: Arc::new(AtomicBool::new(false)),
151 shutdown_fast: false,
152 })
153 }
154
155 pub fn new_with_command(
160 id: PaneId,
161 config: &Config,
162 _runtime: Arc<Runtime>,
163 working_directory: Option<String>,
164 command: String,
165 args: Vec<String>,
166 ) -> anyhow::Result<Self> {
167 let mut terminal = TerminalManager::new_with_scrollback(
169 config.cols,
170 config.rows,
171 config.scrollback.scrollback_lines,
172 )?;
173
174 configure_terminal_from_config(&mut terminal, config);
176
177 let work_dir = working_directory
179 .as_deref()
180 .or(config.shell.working_directory.as_deref());
181
182 let shell_env = build_shell_env(config.shell.shell_env.as_ref());
184 terminal.spawn_custom_shell_with_dir(
185 &command,
186 Some(args.as_slice()),
187 work_dir,
188 shell_env.as_ref(),
189 )?;
190
191 let session_logger = create_shared_logger();
193
194 let terminal = Arc::new(RwLock::new(terminal));
195
196 Ok(Self {
197 id,
198 terminal,
199 scroll_state: ScrollState::new(),
200 mouse: MouseState::new(),
201 bell: BellState::new(),
202 cache: RenderCache::new(),
203 refresh_task: None,
204 working_directory: working_directory.or_else(|| config.shell.working_directory.clone()),
205 last_activity_time: std::time::Instant::now(),
206 last_seen_generation: 0,
207 anti_idle_last_activity: std::time::Instant::now(),
208 anti_idle_last_generation: 0,
209 silence_notified: false,
210 exit_notified: false,
211 session_logger,
212 bounds: PaneBounds::default(),
213 title: String::new(),
214 has_default_title: true,
215 background: PaneBackground::new(),
216 restart_state: None,
217 is_active: Arc::new(AtomicBool::new(false)),
218 shutdown_fast: false,
219 })
220 }
221
222 pub fn new_wrapping_terminal(
238 id: PaneId,
239 terminal: Arc<RwLock<TerminalManager>>,
240 working_directory: Option<String>,
241 is_active: Arc<AtomicBool>,
242 ) -> Self {
243 let session_logger = create_shared_logger();
244
245 Self {
246 id,
247 terminal,
248 scroll_state: ScrollState::new(),
249 mouse: MouseState::new(),
250 bell: BellState::new(),
251 cache: RenderCache::new(),
252 refresh_task: None,
253 working_directory,
254 last_activity_time: std::time::Instant::now(),
255 last_seen_generation: 0,
256 anti_idle_last_activity: std::time::Instant::now(),
257 anti_idle_last_generation: 0,
258 silence_notified: false,
259 exit_notified: false,
260 session_logger,
261 bounds: PaneBounds::default(),
262 title: String::new(),
263 has_default_title: true,
264 background: PaneBackground::new(),
265 restart_state: None,
266 is_active,
267 shutdown_fast: false,
268 }
269 }
270
271 pub fn new_for_tmux(
276 id: PaneId,
277 config: &Config,
278 _runtime: Arc<Runtime>,
279 ) -> anyhow::Result<Self> {
280 let mut terminal = TerminalManager::new_with_scrollback(
282 config.cols,
283 config.rows,
284 config.scrollback.scrollback_lines,
285 )?;
286
287 configure_terminal_from_config(&mut terminal, config);
289
290 let session_logger = create_shared_logger();
293
294 let terminal = Arc::new(RwLock::new(terminal));
295
296 Ok(Self {
297 id,
298 terminal,
299 scroll_state: ScrollState::new(),
300 mouse: MouseState::new(),
301 bell: BellState::new(),
302 cache: RenderCache::new(),
303 refresh_task: None,
304 working_directory: None,
305 last_activity_time: std::time::Instant::now(),
306 last_seen_generation: 0,
307 anti_idle_last_activity: std::time::Instant::now(),
308 anti_idle_last_generation: 0,
309 silence_notified: false,
310 exit_notified: false,
311 session_logger,
312 bounds: PaneBounds::default(),
313 title: String::new(),
314 has_default_title: true,
315 background: PaneBackground::new(),
316 restart_state: None,
317 is_active: Arc::new(AtomicBool::new(false)),
318 shutdown_fast: false,
319 })
320 }
321
322 pub fn is_bell_active(&self) -> bool {
324 if let Some(flash_start) = self.bell.visual_flash {
325 flash_start.elapsed().as_millis() < VISUAL_BELL_FLASH_DURATION_MS
326 } else {
327 false
328 }
329 }
330
331 pub fn is_running(&self) -> bool {
333 if let Ok(term) = self.terminal.try_read() {
334 let running = term.is_running();
335 if !running {
336 crate::debug_info!(
337 "PANE_EXIT",
338 "Pane {} terminal detected as NOT running (shell exited)",
339 self.id
340 );
341 }
342 running
343 } else {
344 true }
346 }
347
348 pub fn get_cwd(&self) -> Option<String> {
350 if let Ok(term) = self.terminal.try_read() {
351 term.shell_integration_cwd()
352 } else {
353 self.working_directory.clone()
354 }
355 }
356
357 pub fn set_background(&mut self, background: PaneBackground) {
359 self.background = background;
360 }
361
362 pub fn background(&self) -> &PaneBackground {
364 &self.background
365 }
366
367 pub fn set_background_image(&mut self, path: Option<String>) {
369 self.background.image_path = path;
370 }
371
372 pub fn get_background_image(&self) -> Option<&str> {
374 self.background.image_path.as_deref()
375 }
376}
377
378impl Drop for Pane {
379 fn drop(&mut self) {
380 if self.shutdown_fast {
381 log::info!(
382 "Fast-dropping pane {} (cleanup handled externally)",
383 self.id
384 );
385 return;
386 }
387
388 log::info!("Dropping pane {}", self.id);
389
390 if let Some(ref mut logger) = *self.session_logger.lock() {
392 match logger.stop() {
393 Ok(path) => {
394 log::info!("Session log saved to: {:?}", path);
395 }
396 Err(e) => {
397 log::warn!("Failed to stop session logging: {}", e);
398 }
399 }
400 }
401
402 self.stop_refresh_task();
403
404 const KILL_LOCK_ATTEMPTS: u32 = 20;
412 const KILL_LOCK_RETRY: std::time::Duration = std::time::Duration::from_millis(1);
413
414 let mut acquired = false;
415 for _ in 0..KILL_LOCK_ATTEMPTS {
416 if let Ok(mut term) = self.terminal.try_write() {
417 if term.is_running() {
418 log::info!("Killing terminal for pane {}", self.id);
419 if let Err(e) = term.kill() {
420 log::warn!("Failed to kill terminal for pane {}: {}", self.id, e);
421 }
422 }
423 acquired = true;
424 break;
425 }
426 std::thread::sleep(KILL_LOCK_RETRY);
427 }
428
429 if !acquired {
430 log::warn!(
431 "Pane {}: terminal write lock unavailable at drop; shell may outlive the pane",
432 self.id
433 );
434 }
435 }
436}