Skip to content

Commit f690c73

Browse files
wan9chiclaude
andcommitted
refactor: split PtyStream into PtyReader + PtyWriter with shared parser
Share vt100 parser via Arc<Mutex<...>> between reader and writer, allowing independent mutable borrows in tests without scoped BufReader workarounds. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 16747b0 commit f690c73

3 files changed

Lines changed: 86 additions & 72 deletions

File tree

crates/pty_terminal/src/terminal.rs

Lines changed: 45 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,22 @@ use portable_pty::{ChildKiller, ExitStatus, MasterPty};
99

1010
use crate::geo::ScreenSize;
1111

12-
/// A combined PTY reader/writer that implements [`Read`] and [`Write`].
12+
/// The read half of a PTY connection. Implements [`Read`].
1313
///
14-
/// Reading feeds data through an internal vt100 parser, keeping `screen_contents()`
15-
/// up-to-date with parsed terminal output.
14+
/// Reading feeds data through an internal vt100 parser (shared with [`PtyWriter`]),
15+
/// keeping `screen_contents()` up-to-date with parsed terminal output.
16+
pub struct PtyReader {
17+
reader: Box<dyn Read + Send>,
18+
parser: Arc<Mutex<vt100::Parser<Vt100Callbacks>>>,
19+
}
20+
21+
/// The write half of a PTY connection. Implements [`Write`].
1622
///
1723
/// The writer is shared with `Vt100Callbacks` (for DSR query responses) and the
1824
/// background child-monitoring thread (which sets it to `None` on child exit).
19-
pub struct PtyStream {
20-
reader: Box<dyn Read + Send>,
25+
pub struct PtyWriter {
2126
writer: Arc<Mutex<Option<Box<dyn Write + Send>>>>,
22-
parser: vt100::Parser<Vt100Callbacks>,
27+
parser: Arc<Mutex<vt100::Parser<Vt100Callbacks>>>,
2328
master: Box<dyn MasterPty + Send>,
2429
}
2530

@@ -38,9 +43,10 @@ impl Clone for ChildHandle {
3843
}
3944
}
4045

41-
/// A headless terminal consisting of a PTY stream and a child process handle.
46+
/// A headless terminal consisting of a PTY reader, writer, and a child process handle.
4247
pub struct Terminal {
43-
pub pty_stream: PtyStream,
48+
pub pty_reader: PtyReader,
49+
pub pty_writer: PtyWriter,
4450
pub child_handle: ChildHandle,
4551
}
4652

@@ -73,17 +79,17 @@ impl vt100::Callbacks for Vt100Callbacks {
7379
}
7480
}
7581

76-
impl Read for PtyStream {
82+
impl Read for PtyReader {
7783
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
7884
let n = self.reader.read(buf)?;
7985
if n > 0 {
80-
self.parser.process(&buf[..n]);
86+
self.parser.lock().unwrap().process(&buf[..n]);
8187
}
8288
Ok(n)
8389
}
8490
}
8591

86-
impl Write for PtyStream {
92+
impl Write for PtyWriter {
8793
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
8894
let mut guard =
8995
self.writer.lock().map_err(|e| std::io::Error::other(format!("Poisoned lock: {e}")))?;
@@ -102,7 +108,19 @@ impl Write for PtyStream {
102108
}
103109
}
104110

105-
impl PtyStream {
111+
impl PtyReader {
112+
/// Returns the current terminal screen contents as a string (parsed by the vt100 emulator).
113+
///
114+
/// # Panics
115+
///
116+
/// Panics if the parser lock is poisoned.
117+
#[must_use]
118+
pub fn screen_contents(&self) -> String {
119+
self.parser.lock().unwrap().screen().contents()
120+
}
121+
}
122+
123+
impl PtyWriter {
106124
/// Writes `line` followed by a platform-appropriate line ending to the child process.
107125
///
108126
/// On Unix, appends `\n`. On Windows `ConPTY`, appends `\r\n` for proper line handling.
@@ -122,12 +140,6 @@ impl PtyStream {
122140
self.flush()
123141
}
124142

125-
/// Returns the current terminal screen contents as a string (parsed by the vt100 emulator).
126-
#[must_use]
127-
pub fn screen_contents(&self) -> String {
128-
self.parser.screen().contents()
129-
}
130-
131143
/// Sends Ctrl+C (SIGINT) to the child process.
132144
///
133145
/// Writes ETX (0x03) to the PTY. On Unix, the terminal driver converts this
@@ -149,15 +161,19 @@ impl PtyStream {
149161
/// # Errors
150162
///
151163
/// Returns an error if the PTY cannot be resized.
152-
pub fn resize(&mut self, size: ScreenSize) -> anyhow::Result<()> {
164+
///
165+
/// # Panics
166+
///
167+
/// Panics if the parser lock is poisoned.
168+
pub fn resize(&self, size: ScreenSize) -> anyhow::Result<()> {
153169
self.master.resize(portable_pty::PtySize {
154170
rows: size.rows,
155171
cols: size.cols,
156172
pixel_width: 0,
157173
pixel_height: 0,
158174
})?;
159175

160-
self.parser.screen_mut().set_size(size.rows, size.cols);
176+
self.parser.lock().unwrap().screen_mut().set_size(size.rows, size.cols);
161177

162178
Ok(())
163179
}
@@ -223,18 +239,16 @@ impl Terminal {
223239
}
224240
});
225241

242+
let parser = Arc::new(Mutex::new(vt100::Parser::new_with_callbacks(
243+
size.rows,
244+
size.cols,
245+
0,
246+
Vt100Callbacks { writer: Arc::clone(&writer) },
247+
)));
248+
226249
Ok(Self {
227-
pty_stream: PtyStream {
228-
parser: vt100::Parser::new_with_callbacks(
229-
size.rows,
230-
size.cols,
231-
0,
232-
Vt100Callbacks { writer: Arc::clone(&writer) },
233-
),
234-
reader,
235-
writer,
236-
master,
237-
},
250+
pty_reader: PtyReader { reader, parser: Arc::clone(&parser) },
251+
pty_writer: PtyWriter { writer, parser, master },
238252
child_handle: ChildHandle { child_killer, exit_status },
239253
})
240254
}

crates/pty_terminal/tests/terminal.rs

Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ fn is_terminal() {
1313
println!("{} {} {}", stdin().is_terminal(), stdout().is_terminal(), stderr().is_terminal());
1414
}));
1515

16-
let Terminal { mut pty_stream, child_handle } =
16+
let Terminal { mut pty_reader, pty_writer: _pty_writer, child_handle } =
1717
Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap();
1818
let mut discard = Vec::new();
19-
pty_stream.read_to_end(&mut discard).unwrap();
19+
pty_reader.read_to_end(&mut discard).unwrap();
2020
let _ = child_handle.wait();
21-
let output = pty_stream.screen_contents();
21+
let output = pty_reader.screen_contents();
2222
assert_eq!(output.trim(), "true true true");
2323
}
2424

@@ -37,16 +37,16 @@ fn write_basic_echo() {
3737
}
3838
}));
3939

40-
let Terminal { mut pty_stream, child_handle } =
40+
let Terminal { mut pty_reader, mut pty_writer, child_handle } =
4141
Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap();
4242

43-
pty_stream.write_line(b"hello world").unwrap();
43+
pty_writer.write_line(b"hello world").unwrap();
4444

4545
let mut discard = Vec::new();
46-
pty_stream.read_to_end(&mut discard).unwrap();
46+
pty_reader.read_to_end(&mut discard).unwrap();
4747
let _ = child_handle.wait();
4848

49-
let output = pty_stream.screen_contents();
49+
let output = pty_reader.screen_contents();
5050
// PTY echoes the input, so we see "hello world\nhello world"
5151
assert_eq!(output.trim(), "hello world\nhello world");
5252
}
@@ -68,12 +68,12 @@ fn write_multiple_lines() {
6868
}
6969
}));
7070

71-
let Terminal { mut pty_stream, child_handle } =
71+
let Terminal { mut pty_reader, mut pty_writer, child_handle } =
7272
Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap();
7373

74-
pty_stream.write_line(b"first").unwrap();
74+
pty_writer.write_line(b"first").unwrap();
7575
{
76-
let mut buf_reader = BufReader::new(&mut pty_stream);
76+
let mut buf_reader = BufReader::new(&mut pty_reader);
7777
let mut line = Vec::new();
7878
// Read PTY echo of "first\n"
7979
buf_reader.read_until(b'\n', &mut line).unwrap();
@@ -82,22 +82,22 @@ fn write_multiple_lines() {
8282
buf_reader.read_until(b'\n', &mut line).unwrap();
8383
}
8484

85-
pty_stream.write_line(b"second").unwrap();
85+
pty_writer.write_line(b"second").unwrap();
8686
{
87-
let mut buf_reader = BufReader::new(&mut pty_stream);
87+
let mut buf_reader = BufReader::new(&mut pty_reader);
8888
let mut line = Vec::new();
8989
buf_reader.read_until(b'\n', &mut line).unwrap();
9090
line.clear();
9191
buf_reader.read_until(b'\n', &mut line).unwrap();
9292
}
9393

94-
pty_stream.write_line(b"third").unwrap();
94+
pty_writer.write_line(b"third").unwrap();
9595

9696
let mut discard = Vec::new();
97-
pty_stream.read_to_end(&mut discard).unwrap();
97+
pty_reader.read_to_end(&mut discard).unwrap();
9898
let _ = child_handle.wait();
9999

100-
let output = pty_stream.screen_contents();
100+
let output = pty_reader.screen_contents();
101101
// PTY echoes input, then child prints "Echo: {line}\n" for each
102102
assert_eq!(output.trim(), "first\nEcho: first\nsecond\nEcho: second\nthird\nEcho: third");
103103
}
@@ -110,18 +110,18 @@ fn write_after_exit() {
110110
print!("exiting");
111111
}));
112112

113-
let Terminal { mut pty_stream, child_handle } =
113+
let Terminal { mut pty_reader, mut pty_writer, child_handle } =
114114
Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap();
115115

116116
// Read all output - this blocks until child exits and EOF is reached
117117
let mut discard = Vec::new();
118-
pty_stream.read_to_end(&mut discard).unwrap();
118+
pty_reader.read_to_end(&mut discard).unwrap();
119119
let _ = child_handle.wait();
120120

121121
// The background thread should have set writer to None by now
122122
// since read_to_end only returns after EOF (child exit)
123123
// Writing should fail with BrokenPipe
124-
let result = pty_stream.write_all(b"too late\n");
124+
let result = pty_writer.write_all(b"too late\n");
125125
assert!(result.is_err());
126126
}
127127

@@ -141,25 +141,25 @@ fn write_interactive_prompt() {
141141
stdout.flush().unwrap();
142142
}));
143143

144-
let Terminal { mut pty_stream, child_handle } =
144+
let Terminal { mut pty_reader, mut pty_writer, child_handle } =
145145
Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap();
146146

147147
// Wait for prompt "Name: " (read until the space after colon)
148148
{
149-
let mut buf_reader = BufReader::new(&mut pty_stream);
149+
let mut buf_reader = BufReader::new(&mut pty_reader);
150150
let mut buf = Vec::new();
151151
buf_reader.read_until(b' ', &mut buf).unwrap();
152152
assert!(String::from_utf8_lossy(&buf).contains("Name:"));
153153
}
154154

155155
// Send response
156-
pty_stream.write_line(b"Alice").unwrap();
156+
pty_writer.write_line(b"Alice").unwrap();
157157

158158
let mut discard = Vec::new();
159-
pty_stream.read_to_end(&mut discard).unwrap();
159+
pty_reader.read_to_end(&mut discard).unwrap();
160160
let _ = child_handle.wait();
161161

162-
let output = pty_stream.screen_contents();
162+
let output = pty_reader.screen_contents();
163163
assert_eq!(output.trim(), "Name: Alice\nHello, Alice");
164164
}
165165

@@ -232,28 +232,28 @@ fn resize_terminal() {
232232
stdout().flush().unwrap();
233233
}));
234234

235-
let Terminal { mut pty_stream, child_handle: _ } =
235+
let Terminal { mut pty_reader, mut pty_writer, child_handle: _ } =
236236
Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap();
237237

238238
// Wait for initial size line (synchronize before resizing)
239239
{
240-
let mut buf_reader = BufReader::new(&mut pty_stream);
240+
let mut buf_reader = BufReader::new(&mut pty_reader);
241241
let mut line = Vec::new();
242242
buf_reader.read_until(b'\n', &mut line).unwrap();
243243
assert!(String::from_utf8_lossy(&line).contains("initial: 80 80"));
244244
}
245245

246246
// Perform resize
247-
pty_stream.resize(ScreenSize { rows: 40, cols: 40 }).unwrap();
247+
pty_writer.resize(ScreenSize { rows: 40, cols: 40 }).unwrap();
248248

249249
// Signal the process to continue and check resize
250-
pty_stream.write_line(b"").unwrap();
250+
pty_writer.write_line(b"").unwrap();
251251

252252
// Read remaining output
253253
let mut discard = Vec::new();
254-
pty_stream.read_to_end(&mut discard).unwrap();
254+
pty_reader.read_to_end(&mut discard).unwrap();
255255

256-
let output = pty_stream.screen_contents();
256+
let output = pty_reader.screen_contents();
257257
// Verify resize was detected (SIGWINCH on Unix, synchronous on Windows)
258258
assert!(output.contains("RESIZE_DETECTED"));
259259
// Verify new size is correct
@@ -303,25 +303,25 @@ fn send_ctrl_c_interrupts_process() {
303303
}
304304
}));
305305

306-
let Terminal { mut pty_stream, child_handle: _ } =
306+
let Terminal { mut pty_reader, mut pty_writer, child_handle: _ } =
307307
Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap();
308308

309309
// Wait for process to be ready
310310
{
311-
let mut buf_reader = BufReader::new(&mut pty_stream);
311+
let mut buf_reader = BufReader::new(&mut pty_reader);
312312
let mut line = Vec::new();
313313
buf_reader.read_until(b'\n', &mut line).unwrap();
314314
assert!(String::from_utf8_lossy(&line).contains("ready"));
315315
}
316316

317317
// Send Ctrl+C
318-
pty_stream.send_ctrl_c().unwrap();
318+
pty_writer.send_ctrl_c().unwrap();
319319

320320
// Read remaining output
321321
let mut discard = Vec::new();
322-
pty_stream.read_to_end(&mut discard).unwrap();
322+
pty_reader.read_to_end(&mut discard).unwrap();
323323

324-
let output = pty_stream.screen_contents();
324+
let output = pty_reader.screen_contents();
325325
// Verify interruption was detected
326326
assert!(output.contains("INTERRUPTED"));
327327
}
@@ -334,10 +334,10 @@ fn read_to_end_returns_exit_status_success() {
334334
println!("success");
335335
}));
336336

337-
let Terminal { mut pty_stream, child_handle } =
337+
let Terminal { mut pty_reader, pty_writer: _pty_writer, child_handle } =
338338
Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap();
339339
let mut discard = Vec::new();
340-
pty_stream.read_to_end(&mut discard).unwrap();
340+
pty_reader.read_to_end(&mut discard).unwrap();
341341
let status = child_handle.wait();
342342
assert!(status.success());
343343
assert_eq!(status.exit_code(), 0);
@@ -350,10 +350,10 @@ fn read_to_end_returns_exit_status_nonzero() {
350350
std::process::exit(42);
351351
}));
352352

353-
let Terminal { mut pty_stream, child_handle } =
353+
let Terminal { mut pty_reader, pty_writer: _pty_writer, child_handle } =
354354
Terminal::spawn(ScreenSize { rows: 80, cols: 80 }, cmd).unwrap();
355355
let mut discard = Vec::new();
356-
pty_stream.read_to_end(&mut discard).unwrap();
356+
pty_reader.read_to_end(&mut discard).unwrap();
357357
let status = child_handle.wait();
358358
assert!(!status.success());
359359
assert_eq!(status.exit_code(), 42);

crates/vite_task_bin/tests/e2e_snapshots/main.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,10 +213,10 @@ fn run_case_inner(tmpdir: &AbsolutePath, fixture_path: &std::path::Path, fixture
213213
let mut killer = terminal.child_handle.clone();
214214
let (tx, rx) = mpsc::channel();
215215
std::thread::spawn(move || {
216-
let Terminal { mut pty_stream, child_handle } = terminal;
216+
let Terminal { mut pty_reader, pty_writer: _pty_writer, child_handle } = terminal;
217217
let mut discard = Vec::new();
218-
let read_result = pty_stream.read_to_end(&mut discard);
219-
let screen = pty_stream.screen_contents();
218+
let read_result = pty_reader.read_to_end(&mut discard);
219+
let screen = pty_reader.screen_contents();
220220
let status = read_result.map(|_| child_handle.wait());
221221
let _ = tx.send((status, screen));
222222
});

0 commit comments

Comments
 (0)