Skip to main content

par_term/tab/
mod.rs

1//! Tab management for multi-tab terminal support
2//!
3//! This module provides the core tab infrastructure including:
4//! - `Tab`: Represents a single terminal session with its own state (supports split panes)
5//! - `TabManager`: Coordinates multiple tabs within a window
6//! - `TabId`: Unique identifier for each tab
7
8mod activity_state;
9mod constructors;
10mod initial_text;
11mod manager;
12mod manager_nav;
13mod pane_accessors;
14mod pane_ops;
15mod profile_state;
16mod profile_tracking;
17mod refresh_task;
18mod scripting_state;
19mod session_logging;
20mod setup;
21mod tmux_state;
22
23pub(crate) use activity_state::TabActivityMonitor;
24pub(crate) use profile_state::TabProfileState;
25pub(crate) use scripting_state::TabScriptingState;
26pub(crate) use tmux_state::TabTmuxState;
27
28use crate::pane::PaneManager;
29use crate::session_logger::SharedSessionLogger;
30pub use manager::TabManager;
31use par_term_terminal::TerminalManager;
32pub(crate) use setup::{
33    apply_login_shell_flag, build_shell_env, configure_terminal_from_config, get_shell_command,
34};
35use std::sync::Arc;
36use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
37use tokio::runtime::Runtime;
38use tokio::sync::RwLock;
39use tokio::task::JoinHandle;
40
41// Re-export TabId from par-term-config for shared access across subcrates
42pub use par_term_config::TabId;
43
44/// A single terminal tab with its own state (supports split panes).
45///
46/// # Mutex Strategy
47///
48/// `terminal` is behind a `tokio::sync::RwLock` because `TerminalManager` is
49/// shared across async tasks (PTY reader, input sender, resize handler) and the
50/// sync winit event loop.  See the global policy in [`crate`] (lib.rs) and the
51/// locking table on the `terminal` field below.
52///
53/// `pane_manager` is owned directly (not behind a Mutex) because it is only ever
54/// accessed from the sync winit event loop on the main thread.
55///
56/// For the complete threading model see `docs/MUTEX_PATTERNS.md`.
57pub struct Tab {
58    /// Unique identifier for this tab
59    pub(crate) id: TabId,
60    /// The terminal session for this tab.
61    ///
62    /// Uses `tokio::sync::RwLock` because `TerminalManager` is shared across async tasks
63    /// (PTY reader, input sender, resize handler) and the winit event loop.
64    ///
65    /// ## Locking rules
66    ///
67    /// | Caller context | Correct access pattern | Notes |
68    /// |----------------|------------------------|-------|
69    /// | Async task (Read) | `terminal.read().await` | Async shared access |
70    /// | Async task (Write) | `terminal.write().await` | Async exclusive access |
71    /// | Sync event loop (Read) | `terminal.try_read()` | Non-blocking; skip if contended |
72    /// | Sync event loop (Write) | `terminal.try_write()` | Non-blocking; skip if contended |
73    /// | Sync user action (Write)| `terminal.blocking_write()` | OK for infrequent user-initiated ops |
74    ///
75    /// **Never call `blocking_write()` from within a Tokio worker thread** — it will
76    /// deadlock because the blocking call cannot yield to the async scheduler.
77    ///
78    /// See the struct-level doc on [`Tab`] and `docs/MUTEX_PATTERNS.md` for the full
79    /// threading model.
80    pub(crate) terminal: Arc<RwLock<TerminalManager>>,
81    /// Pane manager for split pane support.
82    ///
83    /// Always `Some` — initialised with a single primary pane at tab creation
84    /// (R-32).  The primary pane shares `Tab::terminal`'s `Arc` so no extra
85    /// shell process is spawned.  Additional panes are added on the first user
86    /// split; the pane count transitions from 1 → 2.
87    ///
88    /// Not behind a Mutex — accessed only from the sync winit event loop on the main thread.
89    pub(crate) pane_manager: Option<PaneManager>,
90    /// Tab title (from OSC sequences or fallback)
91    pub(crate) title: String,
92    /// Async task for refresh polling
93    pub(crate) refresh_task: Option<JoinHandle<()>>,
94    /// Working directory when tab was created (for inheriting).
95    /// Access via [`Tab::get_cwd`] rather than reading this field directly.
96    pub(in crate::tab) working_directory: Option<String>,
97    /// Custom tab color [R, G, B] (0-255), overrides config colors when set
98    pub(crate) custom_color: Option<[u8; 3]>,
99    /// Whether the tab has its default "Tab N" title (not set by OSC, CWD, or user)
100    pub(crate) has_default_title: bool,
101    /// Whether the user has manually named this tab (makes title static)
102    pub(crate) user_named: bool,
103    /// Activity monitoring: tab bar indicator, anti-idle, silence, and exit tracking (R-11)
104    pub(crate) activity: TabActivityMonitor,
105    /// Session logger for automatic session recording
106    pub(crate) session_logger: SharedSessionLogger,
107    /// Tmux gateway mode and pane identity state
108    pub(crate) tmux: TabTmuxState,
109    /// Last detected hostname for automatic profile switching (from OSC 7)
110    pub(crate) detected_hostname: Option<String>,
111    /// Last detected CWD for automatic profile switching (from OSC 7).
112    /// Internal tracking state; access the current CWD via [`Tab::get_cwd`].
113    pub(in crate::tab) detected_cwd: Option<String>,
114    /// Custom icon set by user via context menu (takes precedence over profile_icon)
115    pub(crate) custom_icon: Option<String>,
116    /// Profile auto-switching state (hostname, directory, SSH)
117    pub(crate) profile: TabProfileState,
118    /// Scripting, coprocess, and trigger state
119    pub(crate) scripting: TabScriptingState,
120    /// Whether the terminal was on the alt screen last frame (for detecting transitions)
121    pub(crate) was_alt_screen: bool,
122    /// Whether this tab is the currently active (visible) tab.
123    /// Used by the refresh task to dynamically choose polling interval.
124    /// Managed exclusively within the `crate::tab` module.
125    pub(in crate::tab) is_active: Arc<AtomicBool>,
126    /// When true, Drop impl skips cleanup (terminal Arcs are dropped on background threads)
127    pub(crate) shutdown_fast: bool,
128    /// When true, this tab is hidden from the tab bar (e.g., tmux gateway tab while windows are active)
129    pub(crate) is_hidden: bool,
130    /// Last-known modifyOtherKeys level. Updated on every successful read of
131    /// `terminal` from the input path; read as a fallback when `try_read()`
132    /// fails. Lock contention with the renderer (`try_write` on every frame in
133    /// release/LTO builds) was causing modifier-aware key encoding (notably
134    /// CSI-u Shift+Enter under tmux) to silently fall back to defaults.
135    pub(crate) cached_modify_other_keys_mode: AtomicU8,
136    /// Last-known DECCKM (application cursor) state. See cache rationale on
137    /// `cached_modify_other_keys_mode`.
138    pub(crate) cached_application_cursor: AtomicBool,
139    /// Last-known alt-screen state. See cache rationale on
140    /// `cached_modify_other_keys_mode`.
141    pub(crate) cached_alt_screen_active: AtomicBool,
142    /// Last-known result of "is a `tmux*` process running under this tab's
143    /// shell?", populated by [`crate::app::WindowState::shell_has_tmux_child`].
144    /// Used to disambiguate Shift+Enter encoding when a `try_read` collision
145    /// prevents a fresh process-tree query.
146    pub(crate) cached_has_tmux_child: AtomicBool,
147}
148
149impl Tab {
150    /// Read terminal mode flags with cache fallback.
151    ///
152    /// Tries to acquire a non-blocking read lock; on success, updates the
153    /// cached atomics and returns the fresh values. On contention, returns
154    /// the last successfully-read values from the cache.
155    ///
156    /// Returns `(modify_other_keys_mode, application_cursor, alt_screen_active)`.
157    ///
158    /// Used by the keyboard input path to keep modifier-aware encoding
159    /// correct when the renderer's `try_write` collides with our `try_read`
160    /// — see field-level docs on `cached_modify_other_keys_mode`.
161    pub(crate) fn read_or_cached_modes(&self) -> (u8, bool, bool) {
162        if let Ok(term) = self.terminal.try_read() {
163            let m = term.modify_other_keys_mode();
164            let a = term.application_cursor();
165            let s = term.is_alt_screen_active();
166            self.cached_modify_other_keys_mode
167                .store(m, Ordering::Relaxed);
168            self.cached_application_cursor.store(a, Ordering::Relaxed);
169            self.cached_alt_screen_active.store(s, Ordering::Relaxed);
170            (m, a, s)
171        } else {
172            (
173                self.cached_modify_other_keys_mode.load(Ordering::Relaxed),
174                self.cached_application_cursor.load(Ordering::Relaxed),
175                self.cached_alt_screen_active.load(Ordering::Relaxed),
176            )
177        }
178    }
179}
180
181/// Parameters that differ between `Tab::new()` and `Tab::new_from_profile()`.
182///
183/// Passed to [`Tab::new_internal`] after the caller has resolved its constructor-specific
184/// values (shell command, working directory, tab title).
185pub(super) struct TabInitParams {
186    /// Unique tab identifier
187    pub(super) id: TabId,
188    /// Terminal title shown in the tab bar
189    pub(super) title: String,
190    /// True for auto-generated "Tab N" titles (not set by OSC, CWD, or user)
191    pub(super) has_default_title: bool,
192    /// True when the user (or profile `tab_name`) has explicitly named the tab
193    pub(super) user_named: bool,
194    /// Working directory to expose via `Tab::get_cwd`
195    pub(super) working_directory: Option<String>,
196    /// Used to schedule the initial-text send (if any) in `Tab::new()`
197    pub(super) runtime: Option<Arc<Runtime>>,
198}
199
200impl Drop for Tab {
201    fn drop(&mut self) {
202        if self.shutdown_fast {
203            log::info!("Fast-dropping tab {} (cleanup handled externally)", self.id);
204            return;
205        }
206
207        log::info!("Dropping tab {}", self.id);
208
209        // Stop session logging first (before terminal is killed)
210        if let Some(ref mut logger) = *self.session_logger.lock() {
211            match logger.stop() {
212                Ok(path) => {
213                    log::info!("Session log saved to: {:?}", path);
214                }
215                Err(e) => {
216                    log::warn!("Failed to stop session logging: {}", e);
217                }
218            }
219        }
220
221        // abort() is non-blocking; no sleep needed after it.
222        self.stop_refresh_task();
223
224        // Kill the terminal
225        if let Ok(mut term) = self.terminal.try_write()
226            && term.is_running()
227        {
228            log::info!("Killing terminal for tab {}", self.id);
229            let _ = term.kill();
230        }
231    }
232}
233
234impl Tab {
235    /// Non-blocking read access to this tab's `TerminalManager`.
236    ///
237    /// Returns `None` on lock contention (expected: another async task holds it).
238    /// Use this instead of the inline `if let Ok(term) = tab.terminal.try_read()` pattern
239    /// (AUD-031).
240    ///
241    /// # try_lock rationale
242    /// Called from the sync winit event loop. On contention, returns `None` so the
243    /// caller can gracefully skip the operation and retry on the next frame.
244    #[inline]
245    pub(crate) fn try_with_terminal<R>(&self, f: impl FnOnce(&TerminalManager) -> R) -> Option<R> {
246        // try_lock: intentional — called from the sync event loop; skip on contention.
247        self.terminal.try_read().ok().map(|guard| f(&guard))
248    }
249
250    /// Non-blocking write access to this tab's `TerminalManager`.
251    ///
252    /// Returns `None` on lock contention (expected: another async task holds it).
253    /// Use this instead of the inline `if let Ok(mut term) = tab.terminal.try_write()` pattern
254    /// (AUD-031).
255    ///
256    /// # try_lock rationale
257    /// Called from the sync winit event loop. On contention, returns `None` so the
258    /// caller can gracefully skip the operation and retry on the next frame.
259    #[inline]
260    pub(crate) fn try_with_terminal_mut<R>(
261        &self,
262        f: impl FnOnce(&mut TerminalManager) -> R,
263    ) -> Option<R> {
264        // try_lock: intentional — called from the sync event loop; skip on contention.
265        self.terminal
266            .try_write()
267            .ok()
268            .map(|mut guard| f(&mut guard))
269    }
270}