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}