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}