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}