ailloli_ui_terminal_pty/handle.rs
1//! Thread-safe backend/session abstraction and clonable public handle.
2
3use std::fmt;
4use std::sync::Arc;
5
6use crate::{PtyError, PtyEvent, PtySize, PtySpawnConfig};
7
8/// Factory for backend-specific PTY sessions.
9///
10/// Backends are shareable across threads and consume an owned spawn config.
11/// Whether multiple spawned handles share queues or diagnostics is backend-
12/// specific; [`crate::MockPtyBackend`] intentionally shares recording state.
13///
14/// # Examples
15///
16/// ```
17/// use ailloli_ui_terminal_pty::{MockPtyBackend, PtyBackend, PtySpawnConfig};
18/// let backend: &dyn PtyBackend = &MockPtyBackend::default();
19/// let handle = backend.spawn(PtySpawnConfig::default()).unwrap();
20/// assert!(!handle.is_shutdown());
21/// ```
22pub trait PtyBackend: Send + Sync + 'static {
23 /// Starts one session or returns a categorized spawn/setup error.
24 ///
25 /// The exact use of program, environment, CWD, and terminal size is backend-
26 /// specific. No generic validation occurs before this call.
27 ///
28 /// # Errors
29 ///
30 /// Returns a backend-selected [`PtyError`] when allocation, process spawn,
31 /// configuration, or another required setup operation fails.
32 ///
33 /// # Examples
34 ///
35 /// ```
36 /// use ailloli_ui_terminal_pty::{MockPtyBackend, PtyBackend, PtySpawnConfig};
37 /// let backend = MockPtyBackend::default();
38 /// let handle = backend.spawn(PtySpawnConfig::default()).expect("mock spawn");
39 /// assert!(handle.drain_events().is_empty());
40 /// ```
41 fn spawn(&self, config: PtySpawnConfig) -> Result<PtyHandle, PtyError>;
42}
43
44/// Erased operations implemented by one backend session.
45///
46/// This trait is crate-private so backend internals cannot leak through the
47/// public handle. Implementations must be shareable; individual operations may
48/// still serialize through internal locks.
49///
50/// # Examples
51///
52/// ```
53/// use ailloli_ui_terminal_pty::{MockPtyBackend, PtyBackend, PtySpawnConfig};
54/// let handle = MockPtyBackend::default().spawn(PtySpawnConfig::default()).unwrap();
55/// handle.write(b"input").unwrap();
56/// ```
57pub(crate) trait PtySession: Send + Sync + 'static {
58 /// Writes and backend-specifically flushes a raw input slice.
59 ///
60 /// # Errors
61 ///
62 /// Returns [`PtyError::Closed`] for a closed included session, or a
63 /// backend-specific write, flush, locking, I/O, or unsupported-operation
64 /// error.
65 ///
66 /// # Examples
67 ///
68 /// ```
69 /// use ailloli_ui_terminal_pty::{MockPtyBackend, PtyBackend, PtySpawnConfig};
70 /// let backend = MockPtyBackend::default();
71 /// backend.spawn(PtySpawnConfig::default()).unwrap().write(b"x").unwrap();
72 /// assert_eq!(backend.writes(), vec![b"x".to_vec()]);
73 /// ```
74 fn write(&self, bytes: &[u8]) -> Result<(), PtyError>;
75 /// Requests an immediate backend size update.
76 ///
77 /// # Errors
78 ///
79 /// Returns [`PtyError::Closed`] for a closed included session, or a
80 /// backend-specific resize, locking, I/O, or unsupported-operation error.
81 ///
82 /// # Examples
83 ///
84 /// ```
85 /// use ailloli_ui_terminal_pty::{MockPtyBackend, PtyBackend, PtySize, PtySpawnConfig};
86 /// let backend = MockPtyBackend::default();
87 /// backend.spawn(PtySpawnConfig::default()).unwrap().resize(PtySize::default()).unwrap();
88 /// assert_eq!(backend.resizes(), vec![PtySize::default()]);
89 /// ```
90 fn resize(&self, size: PtySize) -> Result<(), PtyError>;
91 /// Removes and returns every event currently queued by the session.
92 ///
93 /// # Examples
94 ///
95 /// ```
96 /// use ailloli_ui_terminal_pty::{MockPtyBackend, PtyBackend, PtyEvent, PtySpawnConfig};
97 /// let backend = MockPtyBackend::default(); backend.push_event(PtyEvent::Output(vec![1]));
98 /// let handle = backend.spawn(PtySpawnConfig::default()).unwrap();
99 /// assert_eq!(handle.drain_events(), vec![PtyEvent::Output(vec![1])]);
100 /// ```
101 fn drain_events(&self) -> Vec<PtyEvent>;
102 /// Requests idempotent session termination.
103 ///
104 /// # Errors
105 ///
106 /// Returns a backend-specific shutdown, locking, I/O, or
107 /// unsupported-operation error. Included implementations keep shutdown
108 /// requested even when the first termination attempt fails.
109 ///
110 /// # Examples
111 ///
112 /// ```
113 /// use ailloli_ui_terminal_pty::{MockPtyBackend, PtyBackend, PtySpawnConfig};
114 /// let handle = MockPtyBackend::default().spawn(PtySpawnConfig::default()).unwrap();
115 /// handle.shutdown().unwrap(); handle.shutdown().unwrap();
116 /// ```
117 fn shutdown(&self) -> Result<(), PtyError>;
118 /// Reports whether termination was requested or observed by the backend.
119 ///
120 /// # Examples
121 ///
122 /// ```
123 /// use ailloli_ui_terminal_pty::{MockPtyBackend, PtyBackend, PtySpawnConfig};
124 /// let handle = MockPtyBackend::default().spawn(PtySpawnConfig::default()).unwrap();
125 /// assert!(!handle.is_shutdown()); handle.shutdown().unwrap(); assert!(handle.is_shutdown());
126 /// ```
127 fn is_shutdown(&self) -> bool;
128}
129
130/// Clonable, thread-safe capability for one erased PTY session.
131///
132/// Clones share the same session, event queue, locks, and shutdown state. Dropping
133/// a handle is not specified to terminate the child; call [`Self::shutdown`] when
134/// deterministic termination is required. Method blocking and event ordering are
135/// backend-specific.
136///
137/// # Examples
138///
139/// ```
140/// use ailloli_ui_terminal_pty::{MockPtyBackend, PtyBackend, PtyError, PtySpawnConfig};
141/// let handle = MockPtyBackend::default().spawn(PtySpawnConfig::default()).unwrap();
142/// let clone = handle.clone(); handle.shutdown().unwrap();
143/// assert_eq!(clone.write(b"closed"), Err(PtyError::Closed));
144/// ```
145#[derive(Clone)]
146pub struct PtyHandle {
147 /// Shared backend session object.
148 inner: Arc<dyn PtySession>,
149}
150
151impl fmt::Debug for PtyHandle {
152 /// Formats only the shutdown observation and marks future fields non-exhaustive.
153 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154 f.debug_struct("PtyHandle")
155 .field("is_shutdown", &self.is_shutdown())
156 .finish_non_exhaustive()
157 }
158}
159
160impl PtyHandle {
161 /// Wraps one erased session in a shared public handle.
162 ///
163 /// # Examples
164 ///
165 /// ```
166 /// use ailloli_ui_terminal_pty::{MockPtyBackend, PtyBackend, PtySpawnConfig};
167 /// // Backends construct handles through the crate-private session adapter.
168 /// let handle = MockPtyBackend::default().spawn(PtySpawnConfig::default()).unwrap();
169 /// assert!(format!("{handle:?}").contains("is_shutdown"));
170 /// ```
171 pub(crate) fn new(inner: Arc<dyn PtySession>) -> Self {
172 Self { inner }
173 }
174
175 /// Writes raw input bytes through the backend.
176 ///
177 /// Bytes are not decoded or normalized; an empty slice is passed through.
178 /// Closed sessions return [`PtyError::Closed`] in the included backends.
179 /// The portable backend serializes writes and flushes after each call.
180 ///
181 /// # Errors
182 ///
183 /// Propagates [`PtyError::Closed`] and the backend's categorized write,
184 /// flush, locking, I/O, or unsupported-operation failures.
185 ///
186 /// # Examples
187 ///
188 /// ```
189 /// use ailloli_ui_terminal_pty::{MockPtyBackend, PtyBackend, PtySpawnConfig};
190 /// let backend = MockPtyBackend::default();
191 /// let handle = backend.spawn(PtySpawnConfig::default()).unwrap();
192 /// handle.write(b"ls\r").unwrap();
193 /// assert_eq!(backend.writes(), vec![b"ls\r".to_vec()]);
194 /// ```
195 pub fn write(&self, bytes: &[u8]) -> Result<(), PtyError> {
196 self.inner.write(bytes)
197 }
198
199 /// Requests a backend resize using the supplied dimensions.
200 ///
201 /// The mock records fields verbatim, including public zero rows/columns; the
202 /// portable adapter clamps those two dimensions to one before OS I/O.
203 ///
204 /// # Errors
205 ///
206 /// Propagates [`PtyError::Closed`] and the backend's categorized resize,
207 /// locking, I/O, or unsupported-operation failures.
208 ///
209 /// # Examples
210 ///
211 /// ```
212 /// use ailloli_ui_terminal_pty::{MockPtyBackend, PtyBackend, PtySize, PtySpawnConfig};
213 /// let backend = MockPtyBackend::default();
214 /// backend.spawn(PtySpawnConfig::default()).unwrap().resize(PtySize::new(40, 120, 0, 0)).unwrap();
215 /// assert_eq!(backend.resizes()[0].cols, 120);
216 /// ```
217 pub fn resize(&self, size: PtySize) -> Result<(), PtyError> {
218 self.inner.resize(size)
219 }
220
221 /// Atomically removes and returns all events currently queued by the backend.
222 ///
223 /// A later producer may enqueue events immediately after the drain. Included
224 /// implementations preserve mutex-enqueue order, but the portable reader,
225 /// batcher, and waiter threads can race. Draining remains allowed after
226 /// shutdown.
227 ///
228 /// # Panics
229 ///
230 /// Included backends panic if their shared event/mock mutex is poisoned.
231 ///
232 /// # Examples
233 ///
234 /// ```
235 /// use ailloli_ui_terminal_pty::{MockPtyBackend, PtyBackend, PtyEvent, PtySpawnConfig};
236 /// let backend = MockPtyBackend::default(); backend.push_event(PtyEvent::Output(b"x".to_vec()));
237 /// let handle = backend.spawn(PtySpawnConfig::default()).unwrap();
238 /// assert_eq!(handle.drain_events().len(), 1);
239 /// assert!(handle.drain_events().is_empty());
240 /// ```
241 pub fn drain_events(&self) -> Vec<PtyEvent> {
242 self.inner.drain_events()
243 }
244
245 /// Requests idempotent backend termination.
246 ///
247 /// In the portable backend the shutdown flag is set before acquiring the
248 /// killer or calling `kill`; a returned failure therefore still leaves the
249 /// handle closed, and later calls return success without retrying the kill.
250 /// This does not join detached reader/batcher/wait threads.
251 ///
252 /// # Errors
253 ///
254 /// Propagates a backend termination, killer-lock, I/O, or
255 /// unsupported-operation failure. An error does not reopen the handle.
256 ///
257 /// # Examples
258 ///
259 /// ```
260 /// use ailloli_ui_terminal_pty::{MockPtyBackend, PtyBackend, PtySpawnConfig};
261 /// let handle = MockPtyBackend::default().spawn(PtySpawnConfig::default()).unwrap();
262 /// handle.shutdown().unwrap(); handle.shutdown().unwrap();
263 /// assert!(handle.is_shutdown());
264 /// ```
265 pub fn shutdown(&self) -> Result<(), PtyError> {
266 self.inner.shutdown()
267 }
268
269 /// Returns the backend's shutdown observation.
270 ///
271 /// For the portable backend, `true` means shutdown was requested or the wait
272 /// thread completed; it does not mean all detached threads were joined or all
273 /// final events were drained.
274 ///
275 /// # Examples
276 ///
277 /// ```
278 /// use ailloli_ui_terminal_pty::{MockPtyBackend, PtyBackend, PtySpawnConfig};
279 /// let handle = MockPtyBackend::default().spawn(PtySpawnConfig::default()).unwrap();
280 /// assert!(!handle.is_shutdown());
281 /// ```
282 pub fn is_shutdown(&self) -> bool {
283 self.inner.is_shutdown()
284 }
285}