Skip to main content

lattice_terminal/
handle.rs

1//! PtyHandle — writer + resize for the master PTY side.
2//! Cheaply clonable (Arc-backed); fire-and-forget semantics
3//! so host code can write keystrokes synchronously without
4//! await.
5
6use std::io::Write;
7use std::sync::Arc;
8
9use parking_lot::Mutex;
10use portable_pty::{MasterPty, PtySize};
11use thiserror::Error;
12
13/// Errors returned by [`PtyHandle`] operations. Wrap stdlib /
14/// portable-pty errors so consumers don't need to depend on
15/// portable-pty directly.
16#[derive(Debug, Error)]
17pub enum PtyHandleError {
18    #[error("pty write failed: {0}")]
19    Write(#[source] std::io::Error),
20    #[error("pty resize failed: {0}")]
21    Resize(String),
22    #[error("pty handle dropped (process already exited)")]
23    Closed,
24}
25
26/// The master PTY writer, shared between the **input path**
27/// ([`PtyHandle::write`], user keystrokes) and the terminal's
28/// **VT-query responder** (the reader thread writing DSR / DA /
29/// DECRQM / colour replies back so query-driven TUIs render —
30/// see `reader::PtyResponder`). One shared `Mutex` serialises
31/// the two writers so their bytes never interleave mid-sequence.
32pub(crate) type SharedPtyWriter = Arc<Mutex<Box<dyn Write + Send>>>;
33
34/// Inner state behind the Arc. Keeps the master PTY + the
35/// writer handle alive together.
36struct PtyHandleInner {
37    /// Boxed-trait so different platforms' MasterPty impls
38    /// all fit. Held to keep the PTY alive (close on drop).
39    master: Mutex<Box<dyn MasterPty + Send>>,
40    /// Pre-extracted writer — `take_writer` consumes from the
41    /// master at spawn time. Shared with the reader thread's
42    /// VT-query responder via [`SharedPtyWriter`]. parking_lot's
43    /// Mutex keeps the write path lock-free of a tokio runtime;
44    /// the OS pipe is the actual backpressure mechanism.
45    writer: SharedPtyWriter,
46    /// Stashed last-known size; used to detect no-op resizes.
47    last_size: Mutex<(u16, u16)>,
48}
49
50/// Cheap-to-clone handle. All clones share the same master
51/// PTY + writer.
52#[derive(Clone)]
53pub struct PtyHandle {
54    inner: Arc<PtyHandleInner>,
55}
56
57impl std::fmt::Debug for PtyHandle {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        let (rows, cols) = self.size();
60        f.debug_struct("PtyHandle")
61            .field("rows", &rows)
62            .field("cols", &cols)
63            .finish()
64    }
65}
66
67impl PtyHandle {
68    /// Construct from the post-spawn pieces. Called by
69    /// [`crate::spawner::spawn`] only.
70    pub(crate) fn new(
71        master: Box<dyn MasterPty + Send>,
72        writer: SharedPtyWriter,
73        rows: u16,
74        cols: u16,
75    ) -> Self {
76        Self {
77            inner: Arc::new(PtyHandleInner {
78                master: Mutex::new(master),
79                writer,
80                last_size: Mutex::new((rows, cols)),
81            }),
82        }
83    }
84
85    /// Write bytes to the PTY's stdin. Fire-and-forget — the
86    /// OS buffers; backpressure surfaces as an `Err` only on
87    /// catastrophic failure (broken pipe, etc.).
88    pub fn write(&self, bytes: &[u8]) -> Result<(), PtyHandleError> {
89        let mut w = self.inner.writer.lock();
90        w.write_all(bytes).map_err(PtyHandleError::Write)?;
91        // Flush so the shell sees the bytes immediately —
92        // line-buffered stdin defeats interactive use.
93        w.flush().map_err(PtyHandleError::Write)?;
94        Ok(())
95    }
96
97    /// Resize the PTY. The child receives SIGWINCH; programs
98    /// that subscribe (vim, less, htop) re-layout.
99    pub fn resize(&self, rows: u16, cols: u16) -> Result<(), PtyHandleError> {
100        {
101            let last = self.inner.last_size.lock();
102            if *last == (rows, cols) {
103                return Ok(());
104            }
105        }
106        self.inner
107            .master
108            .lock()
109            .resize(PtySize {
110                rows,
111                cols,
112                pixel_width: 0,
113                pixel_height: 0,
114            })
115            .map_err(|e| PtyHandleError::Resize(e.to_string()))?;
116        *self.inner.last_size.lock() = (rows, cols);
117        Ok(())
118    }
119
120    /// Current best-known size.
121    pub fn size(&self) -> (u16, u16) {
122        *self.inner.last_size.lock()
123    }
124
125    // /// Close the PTY master (child sees SIGHUP); dropping the
126    // /// FD typically terminates the subprocess.
127    // pub fn kill(&self) -> Result<(), PtyHandleError> {
128    //     // 1) Flush any pending writes
129    //     if let Err(e) = self.inner.writer.lock().flush() {
130    //         return Err(PtyHandleError::Write(e));
131    //     }
132
133    //     // 2) Dropping the master FD by replacing it with a no-op
134    //     //    will close the PTY. Child should exit on SIGHUP.
135    //     //    If you need SIGKILL, you must store the Child handle.
136    //     let mut master = self.inner.master.lock();
137    //     // Replace with an empty dummy so the old MasterPty is dropped:
138    //     *master = {
139    //         use portable_pty::PtySize;
140    //         // Create a closed pseudo-pty master as a no-op stub
141    //         let dummy = Box::new(
142    //             portable_pty::native_pty_system()
143    //                 .openpty(PtySize {
144    //                     rows: 0,
145    //                     cols: 0,
146    //                     pixel_width: 0,
147    //                     pixel_height: 0,
148    //                 })
149    //                 .unwrap()
150    //                 .master,
151    //         );
152    //         dummy
153    //     };
154    //     Ok(())
155    // }
156}