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}