1use std::sync::{Arc, Mutex};
2
3use futures::future::BoxFuture;
4use virtual_fs::{AsyncWriteExt, NullFile, VirtualFile};
5use wasmer_wasix_types::wasi::{Signal, Snapshot0Clockid};
6
7use crate::syscalls::platform_clock_time_get;
8
9use super::task::signal::SignalHandlerAbi;
10
11const TTY_MOBILE_PAUSE: u128 = std::time::Duration::from_millis(200).as_nanos();
12
13pub mod tty_sys;
14
15#[derive(Debug)]
16pub enum InputEvent {
17 Key,
18 Data(String),
19 Raw(Vec<u8>),
20}
21
22#[derive(Clone, Debug)]
23pub struct ConsoleRect {
24 pub cols: u32,
25 pub rows: u32,
26}
27
28impl Default for ConsoleRect {
29 fn default() -> Self {
30 Self { cols: 80, rows: 25 }
31 }
32}
33
34#[derive(Clone, Debug)]
35pub struct TtyOptionsInner {
36 echo: bool,
37 line_buffering: bool,
38 line_feeds: bool,
40 ignore_cr: bool,
41 map_cr_to_lf: bool,
42 map_lf_to_cr: bool,
43 rect: ConsoleRect,
44}
45
46#[derive(Debug, Clone)]
47pub struct TtyOptions {
48 inner: Arc<Mutex<TtyOptionsInner>>,
49}
50
51impl Default for TtyOptions {
52 fn default() -> Self {
53 Self {
54 inner: Arc::new(Mutex::new(TtyOptionsInner {
55 echo: true,
56 line_buffering: true,
57 line_feeds: true,
58 ignore_cr: false,
59 map_cr_to_lf: true,
60 map_lf_to_cr: false,
61 rect: ConsoleRect { cols: 80, rows: 25 },
62 })),
63 }
64 }
65}
66
67impl TtyOptions {
68 pub fn cols(&self) -> u32 {
69 let inner = self.inner.lock().unwrap();
70 inner.rect.cols
71 }
72
73 pub fn set_cols(&self, cols: u32) {
74 let mut inner = self.inner.lock().unwrap();
75 inner.rect.cols = cols;
76 }
77
78 pub fn rows(&self) -> u32 {
79 let inner = self.inner.lock().unwrap();
80 inner.rect.rows
81 }
82
83 pub fn set_rows(&self, rows: u32) {
84 let mut inner = self.inner.lock().unwrap();
85 inner.rect.rows = rows;
86 }
87
88 pub fn echo(&self) -> bool {
89 let inner = self.inner.lock().unwrap();
90 inner.echo
91 }
92
93 pub fn set_echo(&self, echo: bool) {
94 let mut inner = self.inner.lock().unwrap();
95 inner.echo = echo;
96 }
97
98 pub fn line_buffering(&self) -> bool {
99 let inner = self.inner.lock().unwrap();
100 inner.line_buffering
101 }
102
103 pub fn set_line_buffering(&self, line_buffering: bool) {
104 let mut inner = self.inner.lock().unwrap();
105 inner.line_buffering = line_buffering;
106 }
107
108 pub fn line_feeds(&self) -> bool {
109 let inner = self.inner.lock().unwrap();
110 inner.line_feeds
111 }
112
113 pub fn set_line_feeds(&self, line_feeds: bool) {
114 let mut inner = self.inner.lock().unwrap();
115 inner.line_feeds = line_feeds;
116 }
117
118 pub fn ignore_cr(&self) -> bool {
119 self.inner.lock().unwrap().ignore_cr
120 }
121
122 pub fn set_ignore_cr(&self, ignore_cr: bool) {
123 let mut inner = self.inner.lock().unwrap();
124 inner.ignore_cr = ignore_cr;
125 }
126
127 pub fn map_cr_to_lf(&self) -> bool {
128 self.inner.lock().unwrap().map_cr_to_lf
129 }
130
131 pub fn set_map_cr_to_lf(&self, map_cr_to_lf: bool) {
132 let mut inner = self.inner.lock().unwrap();
133 inner.map_cr_to_lf = map_cr_to_lf;
134 }
135
136 pub fn map_lf_to_cr(&self) -> bool {
137 self.inner.lock().unwrap().map_lf_to_cr
138 }
139
140 pub fn set_map_lf_to_cr(&self, map_lf_to_cr: bool) {
141 let mut inner = self.inner.lock().unwrap();
142 inner.map_lf_to_cr = map_lf_to_cr;
143 }
144}
145
146#[derive(Debug)]
147pub struct Tty {
148 stdin: Box<dyn VirtualFile + Send + Sync + 'static>,
149 stdout: Box<dyn VirtualFile + Send + Sync + 'static>,
150 signaler: Option<Box<dyn SignalHandlerAbi + Send + Sync + 'static>>,
151 is_mobile: bool,
152 last: Option<(String, u128)>,
153 options: TtyOptions,
154 parser: InputParser,
155 line: LineDiscipline,
156}
157
158#[derive(Debug, Default)]
159struct LineDiscipline {
160 chars: Vec<char>,
161 cursor: usize,
162}
163
164impl LineDiscipline {
165 fn len(&self) -> usize {
166 self.chars.len()
167 }
168
169 fn cursor(&self) -> usize {
170 self.cursor
171 }
172
173 fn is_empty(&self) -> bool {
174 self.chars.is_empty()
175 }
176
177 fn insert_text(&mut self, text: &str) {
178 for ch in text.chars() {
179 self.chars.insert(self.cursor, ch);
180 self.cursor += 1;
181 }
182 }
183
184 fn backspace(&mut self) -> bool {
185 if self.cursor == 0 {
186 return false;
187 }
188 self.cursor -= 1;
189 self.chars.remove(self.cursor);
190 true
191 }
192
193 fn clear(&mut self) {
194 self.chars.clear();
195 self.cursor = 0;
196 }
197
198 fn move_left(&mut self) {
199 if self.cursor > 0 {
200 self.cursor -= 1;
201 }
202 }
203
204 fn move_right(&mut self) {
205 if self.cursor < self.chars.len() {
206 self.cursor += 1;
207 }
208 }
209
210 fn home(&mut self) {
211 self.cursor = 0;
212 }
213
214 fn end(&mut self) {
215 self.cursor = self.chars.len();
216 }
217
218 fn ctrl_u(&mut self) -> usize {
219 let removed = self.chars.len();
220 self.clear();
221 removed
222 }
223
224 fn ctrl_w(&mut self) -> usize {
228 let start = self.chars.len();
231 while self.cursor > 0 && self.chars[self.cursor - 1].is_whitespace() {
232 self.cursor -= 1;
233 self.chars.remove(self.cursor);
234 }
235 while self.cursor > 0 && !self.chars[self.cursor - 1].is_whitespace() {
236 self.cursor -= 1;
237 self.chars.remove(self.cursor);
238 }
239 start.saturating_sub(self.chars.len())
240 }
241
242 fn take_line(&mut self) -> String {
243 let line: String = self.chars.iter().collect();
244 self.clear();
245 line
246 }
247}
248
249#[derive(Debug, Clone, PartialEq, Eq)]
250enum ParsedInput {
251 Text(String),
252 Enter,
253 Eof,
254 CtrlC,
255 CtrlBackslash,
256 CtrlZ,
257 Backspace,
258 CtrlU,
259 CtrlW,
260 CursorLeft,
261 CursorRight,
262 CursorUp,
263 CursorDown,
264 Home,
265 End,
266 CtrlL,
267 Tab,
268 PageUp,
269 PageDown,
270 F1,
271 F2,
272 F3,
273 F4,
274 F5,
275 F6,
276 F7,
277 F8,
278 F9,
279 F10,
280 F11,
281 F12,
282}
283
284#[derive(Debug, Clone, PartialEq, Eq)]
285enum EscapeMatch {
286 Prefix,
287 Invalid,
288 Complete(ParsedInput),
289}
290
291const KNOWN_ESCAPE_SEQUENCES: [(&[u8], ParsedInput); 28] = [
292 (b"\x1b[D", ParsedInput::CursorLeft),
293 (b"\x1b[C", ParsedInput::CursorRight),
294 (b"\x1b[A", ParsedInput::CursorUp),
295 (b"\x1b[B", ParsedInput::CursorDown),
296 (b"\x1bOD", ParsedInput::CursorLeft),
297 (b"\x1bOC", ParsedInput::CursorRight),
298 (b"\x1bOA", ParsedInput::CursorUp),
299 (b"\x1bOB", ParsedInput::CursorDown),
300 (b"\x1b[H", ParsedInput::Home),
301 (b"\x1b[F", ParsedInput::End),
302 (b"\x1b[1~", ParsedInput::Home),
303 (b"\x1b[4~", ParsedInput::End),
304 (b"\x1b[7~", ParsedInput::Home),
305 (b"\x1b[8~", ParsedInput::End),
306 (b"\x1b[5~", ParsedInput::PageUp),
307 (b"\x1b[6~", ParsedInput::PageDown),
308 (b"\x1bOP", ParsedInput::F1),
309 (b"\x1bOQ", ParsedInput::F2),
310 (b"\x1bOR", ParsedInput::F3),
311 (b"\x1bOS", ParsedInput::F4),
312 (b"\x1b[15~", ParsedInput::F5),
313 (b"\x1b[17~", ParsedInput::F6),
314 (b"\x1b[18~", ParsedInput::F7),
315 (b"\x1b[19~", ParsedInput::F8),
316 (b"\x1b[20~", ParsedInput::F9),
317 (b"\x1b[21~", ParsedInput::F10),
318 (b"\x1b[23~", ParsedInput::F11),
319 (b"\x1b[24~", ParsedInput::F12),
320];
321
322#[derive(Debug, Default)]
323struct InputParser {
324 esc_buf: Vec<u8>,
327 utf8_buf: Vec<u8>,
330 pending_lf_after_cr: bool,
333}
334
335#[derive(Debug, Clone, Copy, Default)]
336struct InputParserConfig {
337 ignore_cr: bool,
338 map_cr_to_lf: bool,
339 map_lf_to_cr: bool,
340}
341
342impl InputParser {
343 fn reset(&mut self) {
344 self.esc_buf.clear();
345 self.utf8_buf.clear();
346 self.pending_lf_after_cr = false;
347 }
348
349 fn match_escape(seq: &[u8]) -> EscapeMatch {
350 for (known, parsed) in KNOWN_ESCAPE_SEQUENCES {
351 if known == seq {
352 return EscapeMatch::Complete(parsed);
353 }
354 if known.starts_with(seq) {
355 return EscapeMatch::Prefix;
356 }
357 }
358 EscapeMatch::Invalid
359 }
360
361 fn flush_plain(
362 &mut self,
363 plain: &mut Vec<u8>,
364 out: &mut Vec<ParsedInput>,
365 allow_incomplete_utf8: bool,
366 ) {
367 if plain.is_empty() {
368 return;
369 }
370
371 match std::str::from_utf8(plain) {
372 Ok(s) => out.push(ParsedInput::Text(s.to_string())),
373 Err(err) => {
374 let valid_up_to = err.valid_up_to();
375 if valid_up_to > 0 {
376 out.push(ParsedInput::Text(
377 String::from_utf8_lossy(&plain[..valid_up_to]).into_owned(),
378 ));
379 }
380
381 let tail = &plain[valid_up_to..];
382 if !tail.is_empty() {
383 if err.error_len().is_none() && allow_incomplete_utf8 {
384 self.utf8_buf.extend_from_slice(tail);
385 } else {
386 out.push(ParsedInput::Text(
387 String::from_utf8_lossy(tail).into_owned(),
388 ));
389 }
390 }
391 }
392 }
393 plain.clear();
394 }
395
396 fn feed(&mut self, input: &[u8], config: InputParserConfig) -> Vec<ParsedInput> {
397 let mut out = Vec::new();
398 let mut plain = Vec::new();
399 if !self.utf8_buf.is_empty() {
400 plain.extend_from_slice(&self.utf8_buf);
401 self.utf8_buf.clear();
402 }
403
404 for &input_byte in input {
405 let mut byte = input_byte;
406 loop {
407 if self.pending_lf_after_cr {
408 self.pending_lf_after_cr = false;
409 if byte == b'\n' {
410 break;
411 }
412 }
413
414 if !self.esc_buf.is_empty() {
415 self.esc_buf.push(byte);
416 match Self::match_escape(&self.esc_buf) {
417 EscapeMatch::Prefix => break,
418 EscapeMatch::Complete(parsed) => {
419 self.flush_plain(&mut plain, &mut out, false);
420 self.esc_buf.clear();
421 out.push(parsed);
422 break;
423 }
424 EscapeMatch::Invalid => {
425 let Some(last) = self.esc_buf.pop() else {
426 break;
427 };
428 plain.extend_from_slice(&self.esc_buf);
429 self.esc_buf.clear();
430 byte = last;
431 continue;
432 }
433 }
434 }
435
436 let mut mapped = byte;
437 if byte == b'\r' {
438 if config.ignore_cr {
439 break;
440 }
441 if config.map_cr_to_lf {
442 mapped = b'\n';
443 }
444 } else if byte == b'\n' && config.map_lf_to_cr {
445 mapped = b'\r';
446 }
447
448 match mapped {
449 b'\x1B' => {
450 self.flush_plain(&mut plain, &mut out, false);
451 self.esc_buf.push(mapped);
452 }
453 b'\n' => {
454 self.flush_plain(&mut plain, &mut out, false);
455 if byte == b'\r' && config.map_cr_to_lf {
456 self.pending_lf_after_cr = true;
457 }
458 out.push(ParsedInput::Enter);
459 }
460 0x04 => {
461 self.flush_plain(&mut plain, &mut out, false);
462 out.push(ParsedInput::Eof);
463 }
464 0x03 => {
465 self.flush_plain(&mut plain, &mut out, false);
466 out.push(ParsedInput::CtrlC);
467 }
468 0x1C => {
469 self.flush_plain(&mut plain, &mut out, false);
470 out.push(ParsedInput::CtrlBackslash);
471 }
472 0x1A => {
473 self.flush_plain(&mut plain, &mut out, false);
474 out.push(ParsedInput::CtrlZ);
475 }
476 0x08 | 0x7F => {
477 self.flush_plain(&mut plain, &mut out, false);
478 out.push(ParsedInput::Backspace);
479 }
480 0x09 => {
481 self.flush_plain(&mut plain, &mut out, false);
482 out.push(ParsedInput::Tab);
483 }
484 0x15 => {
485 self.flush_plain(&mut plain, &mut out, false);
486 out.push(ParsedInput::CtrlU);
487 }
488 0x17 => {
489 self.flush_plain(&mut plain, &mut out, false);
490 out.push(ParsedInput::CtrlW);
491 }
492 0x01 => {
493 self.flush_plain(&mut plain, &mut out, false);
494 out.push(ParsedInput::Home);
495 }
496 0x0C => {
497 self.flush_plain(&mut plain, &mut out, false);
498 out.push(ParsedInput::CtrlL);
499 }
500 _ => plain.push(mapped),
501 }
502 break;
503 }
504 }
505
506 self.flush_plain(&mut plain, &mut out, true);
507 out
508 }
509}
510
511impl Tty {
512 async fn signal_and_clear_line(&mut self, signal: Option<Signal>, echo: bool) {
513 if let (Some(signaler), Some(signal)) = (self.signaler.as_ref(), signal) {
514 signaler.signal(signal as u8).ok();
515 }
516 self.line.clear();
517 if echo {
518 self.write_stdout(b"\n").await;
519 }
520 }
521
522 pub fn new(
523 stdin: Box<dyn VirtualFile + Send + Sync + 'static>,
524 stdout: Box<dyn VirtualFile + Send + Sync + 'static>,
525 is_mobile: bool,
526 options: TtyOptions,
527 ) -> Self {
528 Self {
529 stdin,
530 stdout,
531 signaler: None,
532 last: None,
533 options,
534 is_mobile,
535 parser: InputParser::default(),
536 line: LineDiscipline::default(),
537 }
538 }
539
540 pub fn stdin(&self) -> &(dyn VirtualFile + Send + Sync + 'static) {
541 self.stdin.as_ref()
542 }
543
544 pub fn stdin_mut(&mut self) -> &mut (dyn VirtualFile + Send + Sync + 'static) {
545 self.stdin.as_mut()
546 }
547
548 pub fn stdin_replace(
549 &mut self,
550 mut stdin: Box<dyn VirtualFile + Send + Sync + 'static>,
551 ) -> Box<dyn VirtualFile + Send + Sync + 'static> {
552 std::mem::swap(&mut self.stdin, &mut stdin);
553 stdin
554 }
555
556 pub fn stdin_take(&mut self) -> Box<dyn VirtualFile + Send + Sync + 'static> {
557 let mut stdin: Box<dyn VirtualFile + Send + Sync + 'static> = Box::<NullFile>::default();
558 std::mem::swap(&mut self.stdin, &mut stdin);
559 stdin
560 }
561
562 pub fn options(&self) -> TtyOptions {
563 self.options.clone()
564 }
565
566 pub fn set_signaler(&mut self, signaler: Box<dyn SignalHandlerAbi + Send + Sync + 'static>) {
567 self.signaler.replace(signaler);
568 }
569
570 pub fn on_event(mut self, event: InputEvent) -> BoxFuture<'static, Self> {
571 Box::pin(async move {
572 match event {
573 InputEvent::Key => {
574 self
576 }
577 InputEvent::Data(data) => {
578 if self.is_mobile {
581 let now = platform_clock_time_get(Snapshot0Clockid::Monotonic, 1_000_000)
582 .unwrap() as u128;
583 if let Some((what, when)) = self.last.as_ref()
584 && what.as_str() == data
585 && now - *when < TTY_MOBILE_PAUSE
586 {
587 self.last = None;
588 return self;
589 }
590 self.last = Some((data.clone(), now))
591 }
592 self.on_data(data.into_bytes()).await
593 }
594 InputEvent::Raw(data) => self.on_data(data).await,
595 }
596 })
597 }
598
599 async fn write_stdout(&mut self, bytes: &[u8]) {
600 let _ = self.stdout.write(bytes).await;
601 }
602
603 async fn write_stdin(&mut self, bytes: &[u8]) {
604 let _ = self.stdin.write(bytes).await;
605 }
606
607 async fn apply_canonical_input(&mut self, input: ParsedInput, echo: bool) {
608 match input {
609 ParsedInput::Text(text) => {
610 let old_cursor = self.line.cursor();
611 let old_len = self.line.len();
612 if echo {
613 self.write_stdout(text.as_bytes()).await;
614 }
615 self.line.insert_text(&text);
616 if echo && old_cursor < old_len {
617 let tail_start = old_cursor + text.chars().count();
618 let tail: String = self.line.chars[tail_start..].iter().collect();
619 if !tail.is_empty() {
620 self.write_stdout(tail.as_bytes()).await;
621 for _ in 0..tail.chars().count() {
622 self.write_stdout(b"\x08").await;
623 }
624 }
625 }
626 }
627 ParsedInput::Enter => {
628 let mut data = self.line.take_line();
629 data.push('\n');
630 if echo {
631 self.write_stdout(b"\n").await;
632 }
633 self.write_stdin(data.as_bytes()).await;
634 }
635 ParsedInput::Eof => {
636 if !self.line.is_empty() {
637 let data = self.line.take_line();
638 self.write_stdin(data.as_bytes()).await;
639 }
640 }
641 ParsedInput::CtrlC => {
642 self.signal_and_clear_line(Some(Signal::Sigint), echo).await;
643 }
644 ParsedInput::CtrlBackslash => {
645 self.signal_and_clear_line(Some(Signal::Sigquit), echo)
646 .await;
647 }
648 ParsedInput::CtrlZ => {
649 self.signal_and_clear_line(Some(Signal::Sigtstp), echo)
650 .await;
651 }
652 ParsedInput::Backspace => {
653 let old_cursor = self.line.cursor();
654 let old_len = self.line.len();
655 if self.line.backspace() && echo {
656 if old_cursor < old_len {
657 let tail: String = self.line.chars[self.line.cursor()..].iter().collect();
658 self.write_stdout(tail.as_bytes()).await;
659 self.write_stdout(b" ").await;
660 for _ in 0..(tail.chars().count() + 1) {
661 self.write_stdout(b"\x08").await;
662 }
663 } else {
664 self.write_stdout("\u{0008} \u{0008}".as_bytes()).await;
665 }
666 }
667 }
668 ParsedInput::CtrlU => {
669 let removed = self.line.ctrl_u();
670 if echo {
671 for _ in 0..removed {
672 self.write_stdout("\u{0008} \u{0008}".as_bytes()).await;
673 }
674 }
675 }
676 ParsedInput::CtrlW => {
677 let removed = self.line.ctrl_w();
678 if echo {
679 for _ in 0..removed {
680 self.write_stdout("\u{0008} \u{0008}".as_bytes()).await;
681 }
682 }
683 }
684 ParsedInput::CursorLeft => self.line.move_left(),
685 ParsedInput::CursorRight => self.line.move_right(),
686 ParsedInput::Home => self.line.home(),
687 ParsedInput::End => self.line.end(),
688 ParsedInput::CursorUp
689 | ParsedInput::CursorDown
690 | ParsedInput::CtrlL
691 | ParsedInput::Tab
692 | ParsedInput::PageUp
693 | ParsedInput::PageDown
694 | ParsedInput::F1
695 | ParsedInput::F2
696 | ParsedInput::F3
697 | ParsedInput::F4
698 | ParsedInput::F5
699 | ParsedInput::F6
700 | ParsedInput::F7
701 | ParsedInput::F8
702 | ParsedInput::F9
703 | ParsedInput::F10
704 | ParsedInput::F11
705 | ParsedInput::F12 => {}
706 }
707 }
708
709 fn on_data(mut self, data: Vec<u8>) -> BoxFuture<'static, Self> {
710 let options = { self.options.inner.lock().unwrap().clone() };
711 if options.line_buffering {
712 let parser_config = InputParserConfig {
713 ignore_cr: options.ignore_cr,
714 map_cr_to_lf: options.map_cr_to_lf,
715 map_lf_to_cr: options.map_lf_to_cr,
716 };
717 let parsed_inputs = self.parser.feed(&data, parser_config);
718 return Box::pin(async move {
719 for input in parsed_inputs {
720 self.apply_canonical_input(input, options.echo).await;
721 }
722 self
723 });
724 };
725
726 self.parser.reset();
727 Box::pin(async move {
728 if options.echo {
729 self.write_stdout(&data).await;
730 }
731 self.write_stdin(&data).await;
732 self
733 })
734 }
735}
736
737#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
738pub struct WasiTtyState {
739 pub cols: u32,
740 pub rows: u32,
741 pub width: u32,
742 pub height: u32,
743 pub stdin_tty: bool,
744 pub stdout_tty: bool,
745 pub stderr_tty: bool,
746 pub echo: bool,
747 pub line_buffered: bool,
748 pub line_feeds: bool,
749}
750
751impl Default for WasiTtyState {
752 fn default() -> Self {
753 Self {
754 cols: 80,
755 rows: 25,
756 width: 800,
757 height: 600,
758 stdin_tty: true,
759 stdout_tty: true,
760 stderr_tty: true,
761 echo: false,
762 line_buffered: false,
763 line_feeds: true,
764 }
765 }
766}
767
768pub trait TtyBridge: std::fmt::Debug {
770 fn reset(&self);
772
773 fn tty_get(&self) -> WasiTtyState;
775
776 fn tty_set(&self, _tty_state: WasiTtyState);
778}
779
780#[cfg(test)]
781mod tests {
782 use std::{
783 io::{Read, Seek, Write},
784 pin::Pin,
785 sync::{Arc, Mutex},
786 task::{Context, Poll},
787 };
788
789 use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite};
790 use virtual_fs::VirtualFile as VirtualFileTrait;
791 use virtual_mio::block_on;
792 use wasmer_wasix_types::wasi::Signal;
793
794 use super::{InputEvent, Tty, TtyOptions, WasiTtyState};
795 use crate::os::task::signal::{SignalDeliveryError, SignalHandlerAbi};
796
797 #[derive(Debug)]
798 struct CaptureFile {
799 buffer: Arc<Mutex<Vec<u8>>>,
800 }
801
802 impl CaptureFile {
803 fn new(buffer: Arc<Mutex<Vec<u8>>>) -> Self {
804 Self { buffer }
805 }
806 }
807
808 impl VirtualFileTrait for CaptureFile {
809 fn last_accessed(&self) -> u64 {
810 0
811 }
812
813 fn last_modified(&self) -> u64 {
814 0
815 }
816
817 fn created_time(&self) -> u64 {
818 0
819 }
820
821 fn size(&self) -> u64 {
822 self.buffer.lock().unwrap().len() as u64
823 }
824
825 fn set_len(&mut self, _new_size: u64) -> Result<(), virtual_fs::FsError> {
826 Err(virtual_fs::FsError::PermissionDenied)
827 }
828
829 fn unlink(&mut self) -> Result<(), virtual_fs::FsError> {
830 Ok(())
831 }
832
833 fn is_open(&self) -> bool {
834 true
835 }
836
837 fn get_special_fd(&self) -> Option<u32> {
838 None
839 }
840
841 fn poll_read_ready(
842 self: Pin<&mut Self>,
843 _cx: &mut Context<'_>,
844 ) -> Poll<std::io::Result<usize>> {
845 Poll::Ready(Ok(0))
846 }
847
848 fn poll_write_ready(
849 self: Pin<&mut Self>,
850 _cx: &mut Context<'_>,
851 ) -> Poll<std::io::Result<usize>> {
852 Poll::Ready(Ok(8192))
853 }
854 }
855
856 impl AsyncRead for CaptureFile {
857 fn poll_read(
858 self: Pin<&mut Self>,
859 _cx: &mut Context<'_>,
860 _buf: &mut tokio::io::ReadBuf<'_>,
861 ) -> Poll<std::io::Result<()>> {
862 Poll::Ready(Ok(()))
863 }
864 }
865
866 impl AsyncWrite for CaptureFile {
867 fn poll_write(
868 mut self: Pin<&mut Self>,
869 _cx: &mut Context<'_>,
870 buf: &[u8],
871 ) -> Poll<std::io::Result<usize>> {
872 Poll::Ready(self.write(buf))
873 }
874
875 fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
876 Poll::Ready(Ok(()))
877 }
878
879 fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
880 Poll::Ready(Ok(()))
881 }
882 }
883
884 impl AsyncSeek for CaptureFile {
885 fn start_seek(self: Pin<&mut Self>, _position: std::io::SeekFrom) -> std::io::Result<()> {
886 Ok(())
887 }
888
889 fn poll_complete(
890 self: Pin<&mut Self>,
891 _cx: &mut Context<'_>,
892 ) -> Poll<std::io::Result<u64>> {
893 Poll::Ready(Ok(0))
894 }
895 }
896
897 impl Read for CaptureFile {
898 fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
899 Ok(0)
900 }
901 }
902
903 impl Write for CaptureFile {
904 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
905 let mut buffer = self.buffer.lock().unwrap();
906 buffer.extend_from_slice(buf);
907 Ok(buf.len())
908 }
909
910 fn flush(&mut self) -> std::io::Result<()> {
911 Ok(())
912 }
913 }
914
915 impl Seek for CaptureFile {
916 fn seek(&mut self, _pos: std::io::SeekFrom) -> std::io::Result<u64> {
917 Ok(0)
918 }
919 }
920
921 #[derive(Debug)]
922 struct RecordingSignaler {
923 signals: Arc<Mutex<Vec<u8>>>,
924 }
925
926 impl RecordingSignaler {
927 fn new(signals: Arc<Mutex<Vec<u8>>>) -> Self {
928 Self { signals }
929 }
930 }
931
932 impl SignalHandlerAbi for RecordingSignaler {
933 fn signal(&self, signal: u8) -> Result<(), SignalDeliveryError> {
934 self.signals.lock().unwrap().push(signal);
935 Ok(())
936 }
937 }
938
939 fn captured(buffer: &Arc<Mutex<Vec<u8>>>) -> String {
940 String::from_utf8(buffer.lock().unwrap().clone()).unwrap()
941 }
942
943 #[allow(clippy::type_complexity)]
944 fn new_tty(
945 echo: bool,
946 line_buffering: bool,
947 ) -> (Tty, Arc<Mutex<Vec<u8>>>, Arc<Mutex<Vec<u8>>>) {
948 new_tty_with_mobile(echo, line_buffering, false)
949 }
950
951 #[allow(clippy::type_complexity)]
952 fn new_tty_with_mobile(
953 echo: bool,
954 line_buffering: bool,
955 is_mobile: bool,
956 ) -> (Tty, Arc<Mutex<Vec<u8>>>, Arc<Mutex<Vec<u8>>>) {
957 let stdin_buffer = Arc::new(Mutex::new(Vec::new()));
958 let stdout_buffer = Arc::new(Mutex::new(Vec::new()));
959
960 let options = TtyOptions::default();
961 options.set_echo(echo);
962 options.set_line_buffering(line_buffering);
963
964 let tty = Tty::new(
965 Box::new(CaptureFile::new(stdin_buffer.clone())),
966 Box::new(CaptureFile::new(stdout_buffer.clone())),
967 is_mobile,
968 options,
969 );
970
971 (tty, stdin_buffer, stdout_buffer)
972 }
973
974 fn run_event(tty: Tty, event: InputEvent) -> Tty {
975 block_on(tty.on_event(event))
976 }
977
978 fn run_events(mut tty: Tty, events: Vec<InputEvent>) -> Tty {
979 for event in events {
980 tty = run_event(tty, event);
981 }
982 tty
983 }
984
985 #[test]
986 fn tty_canonical_enter_flushes_line_to_stdin() {
987 let (tty, stdin_buf, stdout_buf) = new_tty(true, true);
988 let _tty = run_events(
989 tty,
990 vec![
991 InputEvent::Data("pwd".to_string()),
992 InputEvent::Data("\r".to_string()),
993 ],
994 );
995
996 assert_eq!(captured(&stdin_buf), "pwd\n");
998 assert_eq!(captured(&stdout_buf), "pwd\n");
999 }
1000
1001 #[test]
1002 fn tty_canonical_lf_is_enter() {
1003 let (tty, stdin_buf, stdout_buf) = new_tty(true, true);
1004 let _tty = run_events(
1005 tty,
1006 vec![
1007 InputEvent::Data("pwd".to_string()),
1008 InputEvent::Data("\n".to_string()),
1009 ],
1010 );
1011
1012 assert_eq!(captured(&stdin_buf), "pwd\n");
1014 assert_eq!(captured(&stdout_buf), "pwd\n");
1015 }
1016
1017 #[test]
1018 fn tty_canonical_echo_disabled_still_forwards_line() {
1019 let (tty, stdin_buf, stdout_buf) = new_tty(false, true);
1020 let _tty = run_events(
1021 tty,
1022 vec![
1023 InputEvent::Data("pwd".to_string()),
1024 InputEvent::Data("\r".to_string()),
1025 ],
1026 );
1027
1028 assert_eq!(captured(&stdin_buf), "pwd\n");
1030 assert_eq!(captured(&stdout_buf), "");
1031 }
1032
1033 #[test]
1034 fn tty_canonical_backspace_removes_last_ascii_char() {
1035 let (tty, stdin_buf, stdout_buf) = new_tty(true, true);
1036 let _tty = run_events(
1037 tty,
1038 vec![
1039 InputEvent::Data("ab".to_string()),
1040 InputEvent::Data("\u{007F}".to_string()),
1041 InputEvent::Data("\r".to_string()),
1042 ],
1043 );
1044
1045 assert_eq!(captured(&stdin_buf), "a\n");
1047 assert_eq!(
1048 captured(&stdout_buf),
1049 format!("ab{}\n", "\u{0008} \u{0008}")
1050 );
1051 }
1052
1053 #[test]
1054 fn tty_canonical_backspace_on_empty_line_is_noop() {
1055 let (tty, stdin_buf, stdout_buf) = new_tty(true, true);
1056 let _tty = run_events(
1057 tty,
1058 vec![
1059 InputEvent::Data("\u{007F}".to_string()),
1060 InputEvent::Data("\r".to_string()),
1061 ],
1062 );
1063
1064 assert_eq!(captured(&stdin_buf), "\n");
1066 assert_eq!(captured(&stdout_buf), "\n");
1067 }
1068
1069 #[test]
1070 fn tty_ctrl_c_signals_and_clears_buffered_line() {
1071 let (mut tty, stdin_buf, stdout_buf) = new_tty(true, true);
1072 let signals = Arc::new(Mutex::new(Vec::new()));
1073 tty.set_signaler(Box::new(RecordingSignaler::new(signals.clone())));
1074
1075 let _tty = run_events(
1076 tty,
1077 vec![
1078 InputEvent::Data("abc".to_string()),
1079 InputEvent::Data("\u{0003}".to_string()),
1080 InputEvent::Data("x".to_string()),
1081 InputEvent::Data("\r".to_string()),
1082 ],
1083 );
1084
1085 assert_eq!(captured(&stdin_buf), "x\n");
1087 assert_eq!(captured(&stdout_buf), "abc\nx\n");
1088 assert_eq!(signals.lock().unwrap().as_slice(), &[Signal::Sigint as u8]);
1089 }
1090
1091 #[test]
1092 fn tty_ctrl_c_without_signaler_clears_buffer_and_echoes_newline() {
1093 let (tty, stdin_buf, stdout_buf) = new_tty(true, true);
1094 let _tty = run_events(
1095 tty,
1096 vec![
1097 InputEvent::Data("abc".to_string()),
1098 InputEvent::Data("\u{0003}".to_string()),
1099 InputEvent::Data("x".to_string()),
1100 InputEvent::Data("\r".to_string()),
1101 ],
1102 );
1103
1104 assert_eq!(captured(&stdin_buf), "x\n");
1106 assert_eq!(captured(&stdout_buf), "abc\nx\n");
1107 }
1108
1109 #[test]
1110 fn tty_special_keys_do_not_edit_or_forward_by_default() {
1111 let (tty, stdin_buf, stdout_buf) = new_tty(true, true);
1112 let _tty = run_events(
1113 tty,
1114 vec![
1115 InputEvent::Data("\u{001B}\u{005B}\u{0044}".to_string()), InputEvent::Data("\u{001B}\u{005B}\u{0043}".to_string()), InputEvent::Data("\u{001B}\u{005B}\u{0041}".to_string()), InputEvent::Data("\u{001B}\u{005B}\u{0042}".to_string()), InputEvent::Data("a".to_string()),
1120 InputEvent::Data("\r".to_string()),
1121 ],
1122 );
1123
1124 assert_eq!(captured(&stdin_buf), "a\n");
1126 assert_eq!(captured(&stdout_buf), "a\n");
1127 }
1128
1129 #[test]
1130 fn tty_tab_is_consumed_without_forwarding() {
1131 let (tty, stdin_buf, stdout_buf) = new_tty(true, true);
1132 let _tty = run_events(
1133 tty,
1134 vec![
1135 InputEvent::Data("\u{0009}".to_string()),
1136 InputEvent::Data("a".to_string()),
1137 InputEvent::Data("\r".to_string()),
1138 ],
1139 );
1140
1141 assert_eq!(captured(&stdin_buf), "a\n");
1143 assert_eq!(captured(&stdout_buf), "a\n");
1144 }
1145
1146 #[test]
1147 fn tty_key_event_is_noop() {
1148 let (tty, stdin_buf, stdout_buf) = new_tty(true, true);
1149 let _tty = run_events(
1150 tty,
1151 vec![
1152 InputEvent::Key,
1153 InputEvent::Data("x".to_string()),
1154 InputEvent::Data("\r".to_string()),
1155 ],
1156 );
1157
1158 assert_eq!(captured(&stdin_buf), "x\n");
1160 assert_eq!(captured(&stdout_buf), "x\n");
1161 }
1162
1163 #[test]
1164 fn tty_extended_navigation_and_function_keys_are_consumed() {
1165 let (tty, stdin_buf, stdout_buf) = new_tty(true, true);
1166 let _tty = run_events(
1167 tty,
1168 vec![
1169 InputEvent::Data("\u{0001}".to_string()), InputEvent::Data("\u{001B}\u{005B}\u{0048}".to_string()), InputEvent::Data("\u{001B}\u{005B}\u{0046}".to_string()), InputEvent::Data("\u{000C}".to_string()), InputEvent::Data("\u{001B}\u{005B}\u{0035}\u{007E}".to_string()), InputEvent::Data("\u{001B}\u{005B}\u{0036}\u{007E}".to_string()), InputEvent::Data("\u{001B}\u{004F}\u{0050}".to_string()), InputEvent::Data("\u{001B}\u{004F}\u{0051}".to_string()), InputEvent::Data("\u{001B}\u{004F}\u{0052}".to_string()), InputEvent::Data("\u{001B}\u{004F}\u{0053}".to_string()), InputEvent::Data("\u{001B}\u{005B}\u{0031}\u{0035}\u{007E}".to_string()), InputEvent::Data("\u{001B}\u{005B}\u{0031}\u{0037}\u{007E}".to_string()), InputEvent::Data("\u{001B}\u{005B}\u{0031}\u{0038}\u{007E}".to_string()), InputEvent::Data("\u{001B}\u{005B}\u{0031}\u{0039}\u{007E}".to_string()), InputEvent::Data("\u{001B}\u{005B}\u{0032}\u{0030}\u{007E}".to_string()), InputEvent::Data("\u{001B}\u{005B}\u{0032}\u{0031}\u{007E}".to_string()), InputEvent::Data("\u{001B}\u{005B}\u{0032}\u{0033}\u{007E}".to_string()), InputEvent::Data("\u{001B}\u{005B}\u{0032}\u{0034}\u{007E}".to_string()), InputEvent::Data("z".to_string()),
1188 InputEvent::Data("\r".to_string()),
1189 ],
1190 );
1191
1192 assert_eq!(captured(&stdin_buf), "z\n");
1194 assert_eq!(captured(&stdout_buf), "z\n");
1195 }
1196
1197 #[test]
1198 fn tty_canonical_multiple_lines_do_not_bleed_into_each_other() {
1199 let (tty, stdin_buf, stdout_buf) = new_tty(true, true);
1200 let _tty = run_events(
1201 tty,
1202 vec![
1203 InputEvent::Data("one".to_string()),
1204 InputEvent::Data("\r".to_string()),
1205 InputEvent::Data("two".to_string()),
1206 InputEvent::Data("\r".to_string()),
1207 ],
1208 );
1209
1210 assert_eq!(captured(&stdin_buf), "one\ntwo\n");
1212 assert_eq!(captured(&stdout_buf), "one\ntwo\n");
1213 }
1214
1215 #[test]
1216 fn tty_raw_mode_forwards_without_line_buffering() {
1217 let (tty, stdin_buf, stdout_buf) = new_tty(false, false);
1218 let _tty = run_events(
1219 tty,
1220 vec![
1221 InputEvent::Data("pwd".to_string()),
1222 InputEvent::Data("\r".to_string()),
1223 ],
1224 );
1225
1226 assert_eq!(captured(&stdin_buf), "pwd\r");
1228 assert_eq!(captured(&stdout_buf), "");
1229 }
1230
1231 #[test]
1232 fn tty_raw_mode_can_echo() {
1233 let (tty, stdin_buf, stdout_buf) = new_tty(true, false);
1234 let _tty = run_events(tty, vec![InputEvent::Data("raw".to_string())]);
1235
1236 assert_eq!(captured(&stdin_buf), "raw");
1238 assert_eq!(captured(&stdout_buf), "raw");
1239 }
1240
1241 #[test]
1242 fn tty_raw_mode_backspace_is_forwarded() {
1243 let (tty, stdin_buf, stdout_buf) = new_tty(true, false);
1244 let _tty = run_events(tty, vec![InputEvent::Data("\u{007F}".to_string())]);
1245
1246 assert_eq!(captured(&stdin_buf), "\u{007F}");
1248 assert_eq!(captured(&stdout_buf), "\u{007F}");
1249 }
1250
1251 #[test]
1252 fn tty_raw_mode_escape_sequence_is_forwarded() {
1253 let (tty, stdin_buf, stdout_buf) = new_tty(true, false);
1254 let _tty = run_events(
1255 tty,
1256 vec![InputEvent::Data("\u{001B}\u{005B}\u{0044}".to_string())],
1257 );
1258
1259 assert_eq!(captured(&stdin_buf), "\u{001B}\u{005B}\u{0044}");
1261 assert_eq!(captured(&stdout_buf), "\u{001B}\u{005B}\u{0044}");
1262 }
1263
1264 #[test]
1265 fn tty_raw_input_event_behaves_like_data_input_event() {
1266 let (tty, stdin_buf, stdout_buf) = new_tty(true, false);
1267 let _tty = run_events(tty, vec![InputEvent::Raw(b"xyz".to_vec())]);
1268
1269 assert_eq!(captured(&stdin_buf), "xyz");
1271 assert_eq!(captured(&stdout_buf), "xyz");
1272 }
1273
1274 #[test]
1275 fn tty_canonical_utf8_single_chunk_roundtrip() {
1276 let (tty, stdin_buf, stdout_buf) = new_tty(true, true);
1277 let _tty = run_events(
1278 tty,
1279 vec![
1280 InputEvent::Data("hé".to_string()),
1281 InputEvent::Data("\r".to_string()),
1282 ],
1283 );
1284
1285 assert_eq!(captured(&stdin_buf), "hé\n");
1287 assert_eq!(captured(&stdout_buf), "hé\n");
1288 }
1289
1290 #[test]
1291 fn tty_consecutive_enters_emit_empty_lines() {
1292 let (tty, stdin_buf, stdout_buf) = new_tty(true, true);
1293 let _tty = run_events(
1294 tty,
1295 vec![
1296 InputEvent::Data("\r".to_string()),
1297 InputEvent::Data("\r".to_string()),
1298 ],
1299 );
1300
1301 assert_eq!(captured(&stdin_buf), "\n\n");
1303 assert_eq!(captured(&stdout_buf), "\n\n");
1304 }
1305
1306 #[test]
1307 fn tty_stdin_replace_redirects_future_writes() {
1308 let (mut tty, stdin_buf_a, _) = new_tty(false, true);
1309 let stdin_buf_b = Arc::new(Mutex::new(Vec::new()));
1310 let _old = tty.stdin_replace(Box::new(CaptureFile::new(stdin_buf_b.clone())));
1311
1312 let _tty = run_events(
1313 tty,
1314 vec![
1315 InputEvent::Data("new".to_string()),
1316 InputEvent::Data("\r".to_string()),
1317 ],
1318 );
1319
1320 assert_eq!(captured(&stdin_buf_a), "");
1322 assert_eq!(captured(&stdin_buf_b), "new\n");
1323 }
1324
1325 #[test]
1326 fn tty_unknown_escape_sequence_is_buffered_as_data() {
1327 let (tty, stdin_buf, stdout_buf) = new_tty(true, true);
1328 let _tty = run_events(
1329 tty,
1330 vec![
1331 InputEvent::Data("\u{001B}[X".to_string()),
1332 InputEvent::Data("\r".to_string()),
1333 ],
1334 );
1335
1336 assert_eq!(captured(&stdin_buf), "\u{001B}[X\n");
1338 assert_eq!(captured(&stdout_buf), "\u{001B}[X\n");
1339 }
1340
1341 #[test]
1342 fn tty_raw_mode_ctrl_c_is_forwarded_as_data() {
1343 let (tty, stdin_buf, stdout_buf) = new_tty(true, false);
1344 let _tty = run_events(tty, vec![InputEvent::Data("\u{0003}".to_string())]);
1345
1346 assert_eq!(captured(&stdin_buf), "\u{0003}");
1348 assert_eq!(captured(&stdout_buf), "\u{0003}");
1349 }
1350
1351 #[test]
1352 fn tty_mobile_duplicate_data_is_suppressed() {
1353 let (tty, stdin_buf, stdout_buf) = new_tty_with_mobile(true, true, true);
1354 let _tty = run_events(
1355 tty,
1356 vec![
1357 InputEvent::Data("x".to_string()),
1358 InputEvent::Data("x".to_string()),
1359 InputEvent::Data("\r".to_string()),
1360 ],
1361 );
1362
1363 assert_eq!(captured(&stdin_buf), "x\n");
1365 assert_eq!(captured(&stdout_buf), "x\n");
1366 }
1367
1368 #[test]
1369 fn tty_non_mobile_duplicate_data_is_not_suppressed() {
1370 let (tty, stdin_buf, stdout_buf) = new_tty_with_mobile(true, true, false);
1371 let _tty = run_events(
1372 tty,
1373 vec![
1374 InputEvent::Data("x".to_string()),
1375 InputEvent::Data("x".to_string()),
1376 InputEvent::Data("\r".to_string()),
1377 ],
1378 );
1379
1380 assert_eq!(captured(&stdin_buf), "xx\n");
1382 assert_eq!(captured(&stdout_buf), "xx\n");
1383 }
1384
1385 #[test]
1386 fn tty_chunk_split_command_plus_enter_flushes() {
1387 let cases = vec![
1388 vec!["echo hello", "\r"],
1389 vec!["echo ", "hello", "\r"],
1390 vec!["e", "cho hello", "\r"],
1391 ];
1392
1393 for chunks in cases {
1394 let (tty, stdin_buf, _) = new_tty(false, true);
1395 let events = chunks
1396 .into_iter()
1397 .map(|chunk| InputEvent::Data(chunk.to_string()))
1398 .collect::<Vec<_>>();
1399 let _tty = run_events(tty, events);
1400 assert_eq!(captured(&stdin_buf), "echo hello\n");
1402 }
1403 }
1404
1405 #[test]
1406 fn tty_single_frame_command_plus_enter_is_executed() {
1407 let (tty, stdin_buf, _) = new_tty(false, true);
1408 let _tty = run_events(tty, vec![InputEvent::Data("echo hello\r".to_string())]);
1409
1410 assert_eq!(captured(&stdin_buf), "echo hello\n");
1412 }
1413
1414 #[test]
1415 fn tty_utf8_backspace_removes_full_character() {
1416 let (tty, stdin_buf, _) = new_tty(false, true);
1417 let _tty = run_events(
1418 tty,
1419 vec![
1420 InputEvent::Data("é".to_string()),
1421 InputEvent::Data("\u{007F}".to_string()),
1422 InputEvent::Data("\r".to_string()),
1423 ],
1424 );
1425
1426 assert_eq!(captured(&stdin_buf), "\n");
1428 }
1429
1430 #[test]
1431 fn tty_crlf_single_chunk_is_treated_as_one_enter() {
1432 let (tty, stdin_buf, _) = new_tty(false, true);
1433 let _tty = run_events(
1434 tty,
1435 vec![
1436 InputEvent::Data("pwd".to_string()),
1437 InputEvent::Data("\r\n".to_string()),
1438 ],
1439 );
1440
1441 assert_eq!(captured(&stdin_buf), "pwd\n");
1443 }
1444
1445 #[test]
1446 fn tty_cr_then_lf_split_is_single_enter() {
1447 let (tty, stdin_buf, _) = new_tty(false, true);
1448 let _tty = run_events(
1449 tty,
1450 vec![
1451 InputEvent::Data("pwd".to_string()),
1452 InputEvent::Data("\r".to_string()),
1453 InputEvent::Data("\n".to_string()),
1454 ],
1455 );
1456
1457 assert_eq!(captured(&stdin_buf), "pwd\n");
1459 }
1460
1461 #[test]
1462 fn tty_split_left_arrow_escape_sequence_is_consumed() {
1463 let (tty, stdin_buf, stdout_buf) = new_tty(true, true);
1464 let _tty = run_events(
1465 tty,
1466 vec![
1467 InputEvent::Data("\u{001B}".to_string()),
1468 InputEvent::Data("[".to_string()),
1469 InputEvent::Data("D".to_string()),
1470 InputEvent::Data("x".to_string()),
1471 InputEvent::Data("\r".to_string()),
1472 ],
1473 );
1474
1475 assert_eq!(captured(&stdin_buf), "x\n");
1477 assert_eq!(captured(&stdout_buf), "x\n");
1478 }
1479
1480 #[test]
1481 fn tty_split_f5_escape_sequence_is_consumed() {
1482 let (tty, stdin_buf, stdout_buf) = new_tty(true, true);
1483 let _tty = run_events(
1484 tty,
1485 vec![
1486 InputEvent::Data("\u{001B}".to_string()),
1487 InputEvent::Data("[".to_string()),
1488 InputEvent::Data("15".to_string()),
1489 InputEvent::Data("~".to_string()),
1490 InputEvent::Data("x".to_string()),
1491 InputEvent::Data("\r".to_string()),
1492 ],
1493 );
1494
1495 assert_eq!(captured(&stdin_buf), "x\n");
1497 assert_eq!(captured(&stdout_buf), "x\n");
1498 }
1499
1500 #[test]
1501 fn tty_left_arrow_moves_cursor_for_inline_insert() {
1502 let (tty, stdin_buf, _) = new_tty(false, true);
1503 let _tty = run_events(
1504 tty,
1505 vec![
1506 InputEvent::Data("ab".to_string()),
1507 InputEvent::Data("\u{001B}\u{005B}\u{0044}".to_string()),
1508 InputEvent::Data("X".to_string()),
1509 InputEvent::Data("\r".to_string()),
1510 ],
1511 );
1512
1513 assert_eq!(captured(&stdin_buf), "aXb\n");
1515 }
1516
1517 #[test]
1518 fn tty_home_key_moves_cursor_to_start() {
1519 let (tty, stdin_buf, _) = new_tty(false, true);
1520 let _tty = run_events(
1521 tty,
1522 vec![
1523 InputEvent::Data("bc".to_string()),
1524 InputEvent::Data("\u{001B}\u{005B}\u{0048}".to_string()),
1525 InputEvent::Data("a".to_string()),
1526 InputEvent::Data("\r".to_string()),
1527 ],
1528 );
1529
1530 assert_eq!(captured(&stdin_buf), "abc\n");
1532 }
1533
1534 #[test]
1535 fn tty_ctrl_u_kills_current_line() {
1536 let (tty, stdin_buf, _) = new_tty(false, true);
1537 let _tty = run_events(
1538 tty,
1539 vec![
1540 InputEvent::Data("abc".to_string()),
1541 InputEvent::Data("\u{0015}".to_string()),
1542 InputEvent::Data("x".to_string()),
1543 InputEvent::Data("\r".to_string()),
1544 ],
1545 );
1546
1547 assert_eq!(captured(&stdin_buf), "x\n");
1549 }
1550
1551 #[test]
1552 fn tty_ctrl_w_erases_previous_word() {
1553 let (tty, stdin_buf, _) = new_tty(false, true);
1554 let _tty = run_events(
1555 tty,
1556 vec![
1557 InputEvent::Data("hello world".to_string()),
1558 InputEvent::Data("\u{0017}".to_string()),
1559 InputEvent::Data("\r".to_string()),
1560 ],
1561 );
1562
1563 assert_eq!(captured(&stdin_buf), "hello \n");
1565 }
1566
1567 #[test]
1568 fn tty_split_utf8_codepoint_across_raw_chunks() {
1569 let (tty, stdin_buf, _) = new_tty(false, true);
1570 let _tty = run_events(
1571 tty,
1572 vec![
1573 InputEvent::Raw(vec![0xC3]),
1574 InputEvent::Raw(vec![0xA9]),
1575 InputEvent::Data("\r".to_string()),
1576 ],
1577 );
1578
1579 assert_eq!(captured(&stdin_buf), "é\n");
1581 }
1582
1583 #[test]
1584 fn tty_ctrl_c_without_signaler_clears_buffer() {
1585 let (tty, stdin_buf, _) = new_tty(false, true);
1586 let _tty = run_events(
1587 tty,
1588 vec![
1589 InputEvent::Data("abc".to_string()),
1590 InputEvent::Data("\u{0003}".to_string()),
1591 InputEvent::Data("x".to_string()),
1592 InputEvent::Data("\r".to_string()),
1593 ],
1594 );
1595
1596 assert_eq!(captured(&stdin_buf), "x\n");
1598 }
1599
1600 #[test]
1601 fn tty_single_chunk_text_backspace_enter_edits_line() {
1602 let (tty, stdin_buf, _) = new_tty(false, true);
1603 let _tty = run_events(tty, vec![InputEvent::Data("ab\u{007F}\r".to_string())]);
1604
1605 assert_eq!(captured(&stdin_buf), "a\n");
1607 }
1608
1609 #[test]
1610 fn tty_single_chunk_text_ctrlc_enter_clears_line_with_signaler() {
1611 let (mut tty, stdin_buf, _) = new_tty(false, true);
1612 let signals = Arc::new(Mutex::new(Vec::new()));
1613 tty.set_signaler(Box::new(RecordingSignaler::new(signals.clone())));
1614
1615 let _tty = run_events(tty, vec![InputEvent::Data("ab\u{0003}x\r".to_string())]);
1616
1617 assert_eq!(captured(&stdin_buf), "x\n");
1619 assert_eq!(signals.lock().unwrap().as_slice(), &[Signal::Sigint as u8]);
1620 }
1621
1622 #[test]
1623 fn tty_partial_escape_then_enter_preserves_enter_semantics() {
1624 let (tty, stdin_buf, _) = new_tty(false, true);
1625 let _tty = run_events(
1626 tty,
1627 vec![
1628 InputEvent::Data("abc".to_string()),
1629 InputEvent::Data("\u{001B}".to_string()),
1630 InputEvent::Data("\r".to_string()),
1631 ],
1632 );
1633
1634 assert_eq!(captured(&stdin_buf), "abc\u{001B}\n");
1636 }
1637
1638 #[test]
1639 fn tty_partial_escape_then_ctrl_c_still_interrupts() {
1640 let (mut tty, stdin_buf, stdout_buf) = new_tty(true, true);
1641 let signals = Arc::new(Mutex::new(Vec::new()));
1642 tty.set_signaler(Box::new(RecordingSignaler::new(signals.clone())));
1643
1644 let _tty = run_events(
1645 tty,
1646 vec![
1647 InputEvent::Data("abc".to_string()),
1648 InputEvent::Data("\u{001B}".to_string()),
1649 InputEvent::Data("\u{0003}".to_string()),
1650 InputEvent::Data("x".to_string()),
1651 InputEvent::Data("\r".to_string()),
1652 ],
1653 );
1654
1655 assert_eq!(captured(&stdin_buf), "x\n");
1657 let out = captured(&stdout_buf);
1658 assert!(out.starts_with("abc"));
1659 assert!(out.ends_with("\nx\n"));
1660 assert_eq!(signals.lock().unwrap().as_slice(), &[Signal::Sigint as u8]);
1661 }
1662
1663 #[test]
1664 fn tty_ctrl_d_on_empty_line_is_not_buffered_as_text() {
1665 let (tty, stdin_buf, stdout_buf) = new_tty(true, true);
1666 let _tty = run_events(tty, vec![InputEvent::Data("\u{0004}".to_string())]);
1667
1668 assert_eq!(captured(&stdin_buf), "");
1670 assert_eq!(captured(&stdout_buf), "");
1671 }
1672
1673 #[test]
1674 fn tty_ctrl_d_with_buffered_text_flushes_without_newline() {
1675 let (tty, stdin_buf, stdout_buf) = new_tty(true, true);
1676 let _tty = run_events(
1677 tty,
1678 vec![
1679 InputEvent::Data("abc".to_string()),
1680 InputEvent::Data("\u{0004}".to_string()),
1681 ],
1682 );
1683
1684 assert_eq!(captured(&stdin_buf), "abc");
1686 assert_eq!(captured(&stdout_buf), "abc");
1687 }
1688
1689 #[test]
1690 fn tty_ctrl_u_echoes_line_erase_feedback() {
1691 let (tty, stdin_buf, stdout_buf) = new_tty(true, true);
1692 let _tty = run_events(
1693 tty,
1694 vec![
1695 InputEvent::Data("abc".to_string()),
1696 InputEvent::Data("\u{0015}".to_string()),
1697 InputEvent::Data("\r".to_string()),
1698 ],
1699 );
1700
1701 assert_eq!(captured(&stdin_buf), "\n");
1703 assert!(captured(&stdout_buf).contains("\u{0008} \u{0008}"));
1704 }
1705
1706 #[test]
1707 fn tty_ctrl_w_echoes_word_erase_feedback() {
1708 let (tty, stdin_buf, stdout_buf) = new_tty(true, true);
1709 let _tty = run_events(
1710 tty,
1711 vec![
1712 InputEvent::Data("hello world".to_string()),
1713 InputEvent::Data("\u{0017}".to_string()),
1714 InputEvent::Data("\r".to_string()),
1715 ],
1716 );
1717
1718 assert_eq!(captured(&stdin_buf), "hello \n");
1720 assert!(captured(&stdout_buf).contains("\u{0008} \u{0008}"));
1721 }
1722
1723 #[test]
1724 fn tty_left_arrow_inline_insert_echoes_cursor_repair() {
1725 let (tty, stdin_buf, stdout_buf) = new_tty(true, true);
1726 let _tty = run_events(
1727 tty,
1728 vec![
1729 InputEvent::Data("ab".to_string()),
1730 InputEvent::Data("\u{001B}\u{005B}\u{0044}".to_string()),
1731 InputEvent::Data("X".to_string()),
1732 InputEvent::Data("\r".to_string()),
1733 ],
1734 );
1735
1736 assert_eq!(captured(&stdin_buf), "aXb\n");
1738 assert!(captured(&stdout_buf).contains('\u{0008}'));
1739 }
1740
1741 #[test]
1742 fn tty_backspace_after_cursor_move_repaints_tail() {
1743 let (tty, stdin_buf, stdout_buf) = new_tty(true, true);
1744 let _tty = run_events(
1745 tty,
1746 vec![
1747 InputEvent::Data("ab".to_string()),
1748 InputEvent::Data("\u{001B}\u{005B}\u{0044}".to_string()),
1749 InputEvent::Data("\u{007F}".to_string()),
1750 InputEvent::Data("\r".to_string()),
1751 ],
1752 );
1753
1754 assert_eq!(captured(&stdin_buf), "b\n");
1756 assert_eq!(captured(&stdout_buf), "abb \u{0008}\u{0008}\n");
1757 }
1758
1759 #[test]
1760 fn tty_backspace_ascii_bs_alias_matches_del() {
1761 let (tty, stdin_buf, stdout_buf) = new_tty(true, true);
1762 let _tty = run_events(
1763 tty,
1764 vec![
1765 InputEvent::Data("ab".to_string()),
1766 InputEvent::Data("\u{0008}".to_string()),
1767 InputEvent::Data("\r".to_string()),
1768 ],
1769 );
1770
1771 assert_eq!(captured(&stdin_buf), "a\n");
1773 assert_eq!(
1774 captured(&stdout_buf),
1775 format!("ab{}\n", "\u{0008} \u{0008}")
1776 );
1777 }
1778
1779 #[test]
1780 fn tty_mode_switch_clears_pending_parser_state() {
1781 let (mut tty, stdin_buf, _) = new_tty(false, true);
1782 tty = run_event(tty, InputEvent::Data("\u{001B}".to_string()));
1783 tty.options().set_line_buffering(false);
1784 tty = run_event(tty, InputEvent::Data("raw".to_string()));
1785 tty.options().set_line_buffering(true);
1786 let _tty = run_events(
1787 tty,
1788 vec![
1789 InputEvent::Data("x".to_string()),
1790 InputEvent::Data("\r".to_string()),
1791 ],
1792 );
1793
1794 assert_eq!(captured(&stdin_buf), "rawx\n");
1796 }
1797
1798 #[test]
1799 fn tty_ctrl_backslash_signals_sigquit_and_clears_line() {
1800 let (mut tty, stdin_buf, stdout_buf) = new_tty(true, true);
1801 let signals = Arc::new(Mutex::new(Vec::new()));
1802 tty.set_signaler(Box::new(RecordingSignaler::new(signals.clone())));
1803
1804 let _tty = run_events(
1805 tty,
1806 vec![
1807 InputEvent::Data("abc".to_string()),
1808 InputEvent::Data("\u{001C}".to_string()),
1809 InputEvent::Data("x".to_string()),
1810 InputEvent::Data("\r".to_string()),
1811 ],
1812 );
1813
1814 assert_eq!(captured(&stdin_buf), "x\n");
1816 assert_eq!(captured(&stdout_buf), "abc\nx\n");
1817 assert_eq!(signals.lock().unwrap().as_slice(), &[Signal::Sigquit as u8]);
1818 }
1819
1820 #[test]
1821 fn tty_ctrl_z_signals_sigtstp_and_clears_line() {
1822 let (mut tty, stdin_buf, stdout_buf) = new_tty(true, true);
1823 let signals = Arc::new(Mutex::new(Vec::new()));
1824 tty.set_signaler(Box::new(RecordingSignaler::new(signals.clone())));
1825
1826 let _tty = run_events(
1827 tty,
1828 vec![
1829 InputEvent::Data("abc".to_string()),
1830 InputEvent::Data("\u{001A}".to_string()),
1831 InputEvent::Data("x".to_string()),
1832 InputEvent::Data("\r".to_string()),
1833 ],
1834 );
1835
1836 assert_eq!(captured(&stdin_buf), "x\n");
1838 assert_eq!(captured(&stdout_buf), "abc\nx\n");
1839 assert_eq!(signals.lock().unwrap().as_slice(), &[Signal::Sigtstp as u8]);
1840 }
1841
1842 #[test]
1843 fn tty_ignore_cr_option_ignores_carriage_return() {
1844 let (tty, stdin_buf, _) = new_tty(false, true);
1845 tty.options().set_ignore_cr(true);
1846 tty.options().set_map_cr_to_lf(false);
1847
1848 let _tty = run_events(
1849 tty,
1850 vec![
1851 InputEvent::Data("abc".to_string()),
1852 InputEvent::Data("\r".to_string()),
1853 InputEvent::Data("x".to_string()),
1854 InputEvent::Data("\n".to_string()),
1855 ],
1856 );
1857
1858 assert_eq!(captured(&stdin_buf), "abcx\n");
1860 }
1861
1862 #[test]
1863 fn tty_disable_cr_to_lf_mapping_treats_cr_as_data() {
1864 let (tty, stdin_buf, _) = new_tty(false, true);
1865 tty.options().set_map_cr_to_lf(false);
1866
1867 let _tty = run_events(
1868 tty,
1869 vec![
1870 InputEvent::Data("abc".to_string()),
1871 InputEvent::Data("\r".to_string()),
1872 InputEvent::Data("\n".to_string()),
1873 ],
1874 );
1875
1876 assert_eq!(captured(&stdin_buf), "abc\r\n");
1878 }
1879
1880 #[test]
1881 fn tty_left_arrow_inline_insert_repaints_tail_exactly() {
1882 let (tty, _, stdout_buf) = new_tty(true, true);
1883 let _tty = run_events(
1884 tty,
1885 vec![
1886 InputEvent::Data("ab".to_string()),
1887 InputEvent::Data("\u{001B}\u{005B}\u{0044}".to_string()),
1888 InputEvent::Data("X".to_string()),
1889 InputEvent::Data("\r".to_string()),
1890 ],
1891 );
1892
1893 assert_eq!(captured(&stdout_buf), "abXb\u{0008}\n");
1895 }
1896
1897 #[test]
1898 fn tty_application_cursor_left_sequence_moves_cursor() {
1899 let (tty, stdin_buf, _) = new_tty(false, true);
1900 let _tty = run_events(
1901 tty,
1902 vec![
1903 InputEvent::Data("ab".to_string()),
1904 InputEvent::Data("\u{001B}OD".to_string()),
1905 InputEvent::Data("X".to_string()),
1906 InputEvent::Data("\r".to_string()),
1907 ],
1908 );
1909
1910 assert_eq!(captured(&stdin_buf), "aXb\n");
1912 }
1913
1914 #[test]
1915 fn tty_home_end_tilde_variants_are_consumed() {
1916 let (tty, stdin_buf, stdout_buf) = new_tty(true, true);
1917 let _tty = run_events(
1918 tty,
1919 vec![
1920 InputEvent::Data("\u{001B}[1~".to_string()),
1921 InputEvent::Data("\u{001B}[4~".to_string()),
1922 InputEvent::Data("x".to_string()),
1923 InputEvent::Data("\r".to_string()),
1924 ],
1925 );
1926
1927 assert_eq!(captured(&stdin_buf), "x\n");
1929 assert_eq!(captured(&stdout_buf), "x\n");
1930 }
1931
1932 #[test]
1933 fn tty_state_default_size_matches_console_defaults() {
1934 let tty_state = WasiTtyState::default();
1935
1936 assert_eq!(tty_state.cols, 80);
1938 assert_eq!(tty_state.rows, 25);
1939 }
1940}