ailloli_ui_terminal_pty/
model.rs

1//! Serializable PTY dimensions, spawn configuration, exit status, and events.
2
3use std::path::PathBuf;
4
5use serde::{Deserialize, Serialize};
6
7/// PTY character-grid and optional pixel dimensions.
8///
9/// [`Self::new`] guarantees at least one row and column. Public fields and
10/// deserialization bypass that invariant; portable conversion clamps them again.
11/// Pixel values are passed through exactly, with zero conventionally meaning
12/// unavailable/unspecified rather than one pixel.
13///
14/// # Examples
15///
16/// ```
17/// use ailloli_ui_terminal_pty::PtySize;
18/// assert_eq!(PtySize::new(24, 80, 800, 600).cols, 80);
19/// assert_eq!(PtySize::new(0, 0, 0, 0).rows, 1);
20/// ```
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22pub struct PtySize {
23    /// Character-cell rows; constructor-valid range is `1..=u16::MAX`.
24    pub rows: u16,
25    /// Character-cell columns; constructor-valid range is `1..=u16::MAX`.
26    pub cols: u16,
27    /// Total pixel width, or zero when unspecified.
28    pub pixel_width: u16,
29    /// Total pixel height, or zero when unspecified.
30    pub pixel_height: u16,
31}
32
33impl PtySize {
34    /// Creates dimensions, independently replacing zero rows/columns with one.
35    ///
36    /// Pixel dimensions, including zero, are unchanged.
37    ///
38    /// # Examples
39    ///
40    /// ```
41    /// use ailloli_ui_terminal_pty::PtySize;
42    /// let size = PtySize::new(0, 0, 20, 30);
43    /// assert_eq!((size.rows, size.cols, size.pixel_width, size.pixel_height), (1, 1, 20, 30));
44    /// ```
45    pub const fn new(rows: u16, cols: u16, pixel_width: u16, pixel_height: u16) -> Self {
46        Self {
47            rows: if rows == 0 { 1 } else { rows },
48            cols: if cols == 0 { 1 } else { cols },
49            pixel_width,
50            pixel_height,
51        }
52    }
53}
54
55impl Default for PtySize {
56    /// Returns 24 rows, 80 columns, and unspecified pixel dimensions.
57    ///
58    /// # Examples
59    ///
60    /// ```
61    /// use ailloli_ui_terminal_pty::PtySize;
62    /// assert_eq!(PtySize::default(), PtySize::new(24, 80, 0, 0));
63    /// ```
64    fn default() -> Self {
65        Self::new(24, 80, 0, 0)
66    }
67}
68
69/// Backend-neutral child process and PTY spawn configuration.
70///
71/// Values are not validated. For the portable backend, `program == None` selects
72/// the library's platform-default program and ignores `args`; an explicit program
73/// receives arguments in order. `cwd` and environment entries are passed to the
74/// backend. The portable backend sets `TERM` from `term` first and then applies
75/// `env`, so a later `("TERM", value)` entry can override it; duplicate-variable
76/// behavior otherwise follows the backend/OS.
77///
78/// # Examples
79///
80/// ```
81/// use std::path::PathBuf;
82/// use ailloli_ui_terminal_pty::{PtySize, PtySpawnConfig};
83/// let config = PtySpawnConfig {
84///     program: Some(PathBuf::from("sh")),
85///     args: vec!["-lc".into(), "printf ok".into()],
86///     size: PtySize::new(30, 100, 0, 0),
87///     ..PtySpawnConfig::default()
88/// };
89/// assert_eq!(config.args.len(), 2);
90/// ```
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92pub struct PtySpawnConfig {
93    /// Explicit program path/name, or `None` for the backend default.
94    pub program: Option<PathBuf>,
95    /// Ordered arguments used only with an explicit program by the portable backend.
96    pub args: Vec<String>,
97    /// Child working directory, or `None` to inherit backend behavior.
98    pub cwd: Option<PathBuf>,
99    /// Ordered environment overrides; names/values may be empty or duplicated.
100    pub env: Vec<(String, String)>,
101    /// Requested `TERM` value; stored/passed verbatim and empty is accepted.
102    pub term: String,
103    /// Initial PTY grid and pixel dimensions.
104    pub size: PtySize,
105}
106
107impl Default for PtySpawnConfig {
108    /// Selects the default program, no args/CWD/env, `xterm-256color`, and 24x80.
109    ///
110    /// # Examples
111    ///
112    /// ```
113    /// use ailloli_ui_terminal_pty::PtySpawnConfig;
114    /// let config = PtySpawnConfig::default();
115    /// assert!(config.program.is_none() && config.args.is_empty());
116    /// assert_eq!(config.term, "xterm-256color");
117    /// ```
118    fn default() -> Self {
119        Self {
120            program: None,
121            args: Vec::new(),
122            cwd: None,
123            env: Vec::new(),
124            term: "xterm-256color".to_string(),
125            size: PtySize::default(),
126        }
127    }
128}
129
130/// Backend-reported process completion.
131///
132/// Public construction/deserialization does not enforce consistency: successful
133/// status may carry a nonzero code or signal, and failure may carry code zero or
134/// no detail. Consumers should prefer `success` as the declared outcome and treat
135/// the optional fields as backend metadata.
136///
137/// # Examples
138///
139/// ```
140/// use ailloli_ui_terminal_pty::PtyExitStatus;
141/// assert_eq!(PtyExitStatus::success(0).exit_code, Some(0));
142/// assert!(!PtyExitStatus::failure(None, Some("SIGTERM".into())).success);
143/// ```
144#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
145pub struct PtyExitStatus {
146    /// Backend-declared success flag.
147    pub success: bool,
148    /// Unsigned process exit code, or `None` when unavailable/signal-only.
149    pub exit_code: Option<u32>,
150    /// Backend/platform signal description, or `None` when absent.
151    pub signal: Option<String>,
152}
153
154impl PtyExitStatus {
155    /// Creates a successful status with the exact supplied code.
156    ///
157    /// No check requires `exit_code == 0`.
158    ///
159    /// # Examples
160    ///
161    /// ```
162    /// use ailloli_ui_terminal_pty::PtyExitStatus;
163    /// let status = PtyExitStatus::success(7);
164    /// assert!(status.success);
165    /// assert_eq!(status.exit_code, Some(7));
166    /// ```
167    pub fn success(exit_code: u32) -> Self {
168        Self {
169            success: true,
170            exit_code: Some(exit_code),
171            signal: None,
172        }
173    }
174
175    /// Creates a failure with optional code and signal stored verbatim.
176    ///
177    /// Both values may be absent, and code zero is accepted.
178    ///
179    /// # Examples
180    ///
181    /// ```
182    /// use ailloli_ui_terminal_pty::PtyExitStatus;
183    /// let status = PtyExitStatus::failure(Some(1), None);
184    /// assert!(!status.success);
185    /// assert_eq!((status.exit_code, status.signal), (Some(1), None));
186    /// ```
187    pub fn failure(exit_code: Option<u32>, signal: Option<String>) -> Self {
188        Self {
189            success: false,
190            exit_code,
191            signal,
192        }
193    }
194}
195
196/// Event delivered asynchronously by a PTY session.
197///
198/// Ordering is backend enqueue order. In the portable backend, output batching
199/// and child waiting run on separate threads, so an exit event is not guaranteed
200/// to follow the final output batch. Payloads are unbounded by the model and are
201/// neither parsed nor redacted.
202///
203/// # Examples
204///
205/// ```
206/// use ailloli_ui_terminal_pty::{PtyEvent, PtyExitStatus};
207/// let events = vec![PtyEvent::Output(b"ok".to_vec()), PtyEvent::Exit(PtyExitStatus::success(0))];
208/// assert!(matches!(&events[0], PtyEvent::Output(bytes) if bytes == b"ok"));
209/// ```
210#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
211pub enum PtyEvent {
212    /// Raw output bytes; the vector may be empty and may contain invalid UTF-8.
213    Output(Vec<u8>),
214    /// Child completion metadata.
215    Exit(PtyExitStatus),
216    /// Asynchronous backend error text, stored verbatim without categorization.
217    Error(String),
218}