rust_pty/
config.rs

1//! Configuration types for PTY creation and management.
2//!
3//! This module provides [`PtyConfig`] for configuring PTY creation and
4//! [`PtySignal`] for cross-platform signal representation.
5
6use std::collections::HashMap;
7use std::ffi::OsString;
8use std::path::PathBuf;
9use std::time::Duration;
10
11// Unix-specific imports for signal constants
12#[cfg(unix)]
13use libc;
14
15/// Configuration for creating a new PTY session.
16///
17/// # Example
18///
19/// ```
20/// use rust_pty::PtyConfig;
21///
22/// let config = PtyConfig::builder()
23///     .working_directory("/home/user")
24///     .env("TERM", "xterm-256color")
25///     .window_size(80, 24)
26///     .build();
27/// ```
28#[derive(Debug, Clone)]
29pub struct PtyConfig {
30    /// Working directory for the child process.
31    pub working_directory: Option<PathBuf>,
32
33    /// Environment variables to set for the child process.
34    /// If None, inherits from the parent process.
35    pub env: Option<HashMap<OsString, OsString>>,
36
37    /// Additional environment variables to add (merged with inherited).
38    pub env_add: HashMap<OsString, OsString>,
39
40    /// Environment variables to remove from inherited environment.
41    pub env_remove: Vec<OsString>,
42
43    /// Initial window size (columns, rows).
44    pub window_size: (u16, u16),
45
46    /// Whether to create a new session (Unix setsid).
47    pub new_session: bool,
48
49    /// Timeout for spawn operation.
50    pub spawn_timeout: Option<Duration>,
51
52    /// Whether to use a controlling terminal (Unix).
53    #[cfg(unix)]
54    pub controlling_terminal: bool,
55
56    /// Whether to allocate a console (Windows).
57    #[cfg(windows)]
58    pub allocate_console: bool,
59}
60
61impl Default for PtyConfig {
62    fn default() -> Self {
63        Self {
64            working_directory: None,
65            env: None,
66            env_add: HashMap::new(),
67            env_remove: Vec::new(),
68            window_size: (80, 24),
69            new_session: true,
70            spawn_timeout: None,
71            #[cfg(unix)]
72            controlling_terminal: true,
73            #[cfg(windows)]
74            allocate_console: true,
75        }
76    }
77}
78
79impl PtyConfig {
80    /// Create a new builder for `PtyConfig`.
81    #[must_use]
82    pub fn builder() -> PtyConfigBuilder {
83        PtyConfigBuilder::new()
84    }
85
86    /// Create a new `PtyConfig` with default settings.
87    #[must_use]
88    pub fn new() -> Self {
89        Self::default()
90    }
91
92    /// Get the effective environment for the child process.
93    ///
94    /// This merges the base environment (inherited or explicit), adds
95    /// variables from `env_add`, and removes variables from `env_remove`.
96    #[must_use]
97    pub fn effective_env(&self) -> HashMap<OsString, OsString> {
98        let mut env = self
99            .env
100            .clone()
101            .unwrap_or_else(|| std::env::vars_os().collect());
102
103        // Add additional variables
104        env.extend(self.env_add.clone());
105
106        // Remove specified variables
107        for key in &self.env_remove {
108            env.remove(key);
109        }
110
111        env
112    }
113}
114
115/// Builder for [`PtyConfig`].
116#[derive(Debug, Clone, Default)]
117pub struct PtyConfigBuilder {
118    config: PtyConfig,
119}
120
121impl PtyConfigBuilder {
122    /// Create a new builder with default settings.
123    #[must_use]
124    pub fn new() -> Self {
125        Self::default()
126    }
127
128    /// Set the working directory for the child process.
129    #[must_use]
130    pub fn working_directory(mut self, path: impl Into<PathBuf>) -> Self {
131        self.config.working_directory = Some(path.into());
132        self
133    }
134
135    /// Set the complete environment for the child process.
136    ///
137    /// This replaces the inherited environment entirely.
138    #[must_use]
139    pub fn env_clear(mut self) -> Self {
140        self.config.env = Some(HashMap::new());
141        self
142    }
143
144    /// Add an environment variable.
145    #[must_use]
146    pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
147        self.config.env_add.insert(key.into(), value.into());
148        self
149    }
150
151    /// Remove an environment variable.
152    #[must_use]
153    pub fn env_remove(mut self, key: impl Into<OsString>) -> Self {
154        self.config.env_remove.push(key.into());
155        self
156    }
157
158    /// Set the initial window size.
159    #[must_use]
160    pub const fn window_size(mut self, cols: u16, rows: u16) -> Self {
161        self.config.window_size = (cols, rows);
162        self
163    }
164
165    /// Set whether to create a new session.
166    #[must_use]
167    pub const fn new_session(mut self, value: bool) -> Self {
168        self.config.new_session = value;
169        self
170    }
171
172    /// Set the spawn timeout.
173    #[must_use]
174    pub const fn spawn_timeout(mut self, timeout: Duration) -> Self {
175        self.config.spawn_timeout = Some(timeout);
176        self
177    }
178
179    /// Set whether to use a controlling terminal (Unix only).
180    #[cfg(unix)]
181    #[must_use]
182    pub const fn controlling_terminal(mut self, value: bool) -> Self {
183        self.config.controlling_terminal = value;
184        self
185    }
186
187    /// Set whether to allocate a console (Windows only).
188    #[cfg(windows)]
189    #[must_use]
190    pub fn allocate_console(mut self, value: bool) -> Self {
191        self.config.allocate_console = value;
192        self
193    }
194
195    /// Build the configuration.
196    #[must_use]
197    pub fn build(self) -> PtyConfig {
198        self.config
199    }
200}
201
202/// Cross-platform signal representation.
203///
204/// This enum provides a unified interface for signals across Unix and Windows.
205/// On Windows, signals are emulated using console events or process control.
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
207#[non_exhaustive]
208pub enum PtySignal {
209    /// Interrupt signal (Ctrl+C).
210    /// - Unix: SIGINT (2)
211    /// - Windows: `CTRL_C_EVENT`
212    Interrupt,
213
214    /// Quit signal (Ctrl+\).
215    /// - Unix: SIGQUIT (3)
216    /// - Windows: Not directly supported
217    Quit,
218
219    /// Terminate signal.
220    /// - Unix: SIGTERM (15)
221    /// - Windows: `TerminateProcess`
222    Terminate,
223
224    /// Kill signal (cannot be caught).
225    /// - Unix: SIGKILL (9)
226    /// - Windows: `TerminateProcess`
227    Kill,
228
229    /// Hangup signal (terminal closed).
230    /// - Unix: SIGHUP (1)
231    /// - Windows: `CTRL_CLOSE_EVENT`
232    Hangup,
233
234    /// Window size change.
235    /// - Unix: SIGWINCH (28)
236    /// - Windows: Handled via `ConPTY` resize
237    WindowChange,
238
239    /// Stop signal (Ctrl+Z).
240    /// - Unix: SIGTSTP (20)
241    /// - Windows: Not supported
242    #[cfg(unix)]
243    Stop,
244
245    /// Continue signal.
246    /// - Unix: SIGCONT (18)
247    /// - Windows: Not supported
248    #[cfg(unix)]
249    Continue,
250
251    /// User-defined signal 1.
252    /// - Unix: SIGUSR1 (10)
253    /// - Windows: Not supported
254    #[cfg(unix)]
255    User1,
256
257    /// User-defined signal 2.
258    /// - Unix: SIGUSR2 (12)
259    /// - Windows: Not supported
260    #[cfg(unix)]
261    User2,
262}
263
264impl PtySignal {
265    /// Get the Unix signal number, if applicable.
266    #[cfg(unix)]
267    #[must_use]
268    pub const fn as_unix_signal(self) -> Option<i32> {
269        match self {
270            Self::Interrupt => Some(libc::SIGINT),
271            Self::Quit => Some(libc::SIGQUIT),
272            Self::Terminate => Some(libc::SIGTERM),
273            Self::Kill => Some(libc::SIGKILL),
274            Self::Hangup => Some(libc::SIGHUP),
275            Self::WindowChange => Some(libc::SIGWINCH),
276            Self::Stop => Some(libc::SIGTSTP),
277            Self::Continue => Some(libc::SIGCONT),
278            Self::User1 => Some(libc::SIGUSR1),
279            Self::User2 => Some(libc::SIGUSR2),
280        }
281    }
282}
283
284/// Window size for the PTY.
285#[derive(Debug, Clone, Copy, PartialEq, Eq)]
286pub struct WindowSize {
287    /// Number of columns (characters per line).
288    pub cols: u16,
289    /// Number of rows (lines).
290    pub rows: u16,
291    /// Pixel width (optional, often 0).
292    pub xpixel: u16,
293    /// Pixel height (optional, often 0).
294    pub ypixel: u16,
295}
296
297impl WindowSize {
298    /// Create a new window size with the given dimensions.
299    #[must_use]
300    pub const fn new(cols: u16, rows: u16) -> Self {
301        Self {
302            cols,
303            rows,
304            xpixel: 0,
305            ypixel: 0,
306        }
307    }
308
309    /// Create a window size with pixel dimensions.
310    #[must_use]
311    pub const fn with_pixels(cols: u16, rows: u16, xpixel: u16, ypixel: u16) -> Self {
312        Self {
313            cols,
314            rows,
315            xpixel,
316            ypixel,
317        }
318    }
319}
320
321impl Default for WindowSize {
322    fn default() -> Self {
323        Self::new(80, 24)
324    }
325}
326
327impl From<(u16, u16)> for WindowSize {
328    fn from((cols, rows): (u16, u16)) -> Self {
329        Self::new(cols, rows)
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    #[test]
338    fn config_builder() {
339        let config = PtyConfig::builder()
340            .working_directory("/tmp")
341            .env("FOO", "bar")
342            .window_size(120, 40)
343            .build();
344
345        assert_eq!(config.working_directory, Some(PathBuf::from("/tmp")));
346        assert_eq!(config.window_size, (120, 40));
347        assert!(config.env_add.contains_key(&OsString::from("FOO")));
348    }
349
350    #[test]
351    fn window_size_default() {
352        let size = WindowSize::default();
353        assert_eq!(size.cols, 80);
354        assert_eq!(size.rows, 24);
355    }
356}