wasmer_wasix/os/tty/
mod.rs

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    // Kept for TTY bridge/syscall/journal compatibility; currently unused by this parser path.
39    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    // Erase the whitespace run immediately before the cursor, then the preceding
225    // non-whitespace run. For example, with "foo_bar baz" and the cursor at EOL,
226    // the first ctrl-w removes "baz" and the next removes "foo_bar".
227    fn ctrl_w(&mut self) -> usize {
228        // TODO: This currently uses whitespace boundaries.
229        // Linux n_tty ALTWERASE semantics are closer to [A-Za-z0-9_] word classes.
230        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    // Bytes of an in-flight escape sequence so CSI/SS3 fragments can be matched
325    // across separate websocket or PTY frames.
326    esc_buf: Vec<u8>,
327    // Trailing bytes of an incomplete UTF-8 codepoint, replayed into the next
328    // chunk before decoding plain text.
329    utf8_buf: Vec<u8>,
330    // When CR is mapped to LF, suppress the LF in a following CRLF pair so the
331    // parser emits only one Enter event.
332    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                    // do nothing
575                    self
576                }
577                InputEvent::Data(data) => {
578                    // Due to a nasty bug in xterm.js on Android mobile it sends the keys you press
579                    // twice in a row with a short interval between - this hack will avoid that bug
580                    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
768/// Provides access to a TTY.
769pub trait TtyBridge: std::fmt::Debug {
770    /// Resets the values
771    fn reset(&self);
772
773    /// Retrieve the current TTY state.
774    fn tty_get(&self) -> WasiTtyState;
775
776    /// Set the TTY state.
777    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        // Expect canonical mode to flush the buffered line to stdin and echo it to stdout.
997        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        // Expect LF to be treated as Enter in canonical mode.
1013        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        // Expect line forwarding to stdin, but no echo when echo is disabled.
1029        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        // Expect DEL to remove one character from buffered input and emit erase echo sequence.
1046        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        // Expect backspace on an empty line to be ignored.
1065        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        // Expect ctrl-c to clear current line, then accept/forward subsequent input and emit SIGINT.
1086        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        // Expect ctrl-c to clear current input line and continue with subsequent input.
1105        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()), // left
1116                InputEvent::Data("\u{001B}\u{005B}\u{0043}".to_string()), // right
1117                InputEvent::Data("\u{001B}\u{005B}\u{0041}".to_string()), // up
1118                InputEvent::Data("\u{001B}\u{005B}\u{0042}".to_string()), // down
1119                InputEvent::Data("a".to_string()),
1120                InputEvent::Data("\r".to_string()),
1121            ],
1122        );
1123
1124        // Expect currently-implemented navigation keys to be consumed without mutating line state.
1125        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        // Expect tab key to be consumed by handler and not forwarded into the line buffer.
1142        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        // Expect generic Key events to be no-op and not affect data flow.
1159        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()), // ctrl-a (home)
1170                InputEvent::Data("\u{001B}\u{005B}\u{0048}".to_string()), // home
1171                InputEvent::Data("\u{001B}\u{005B}\u{0046}".to_string()), // end
1172                InputEvent::Data("\u{000C}".to_string()), // ctrl-l
1173                InputEvent::Data("\u{001B}\u{005B}\u{0035}\u{007E}".to_string()), // page up
1174                InputEvent::Data("\u{001B}\u{005B}\u{0036}\u{007E}".to_string()), // page down
1175                InputEvent::Data("\u{001B}\u{004F}\u{0050}".to_string()), // f1
1176                InputEvent::Data("\u{001B}\u{004F}\u{0051}".to_string()), // f2
1177                InputEvent::Data("\u{001B}\u{004F}\u{0052}".to_string()), // f3
1178                InputEvent::Data("\u{001B}\u{004F}\u{0053}".to_string()), // f4
1179                InputEvent::Data("\u{001B}\u{005B}\u{0031}\u{0035}\u{007E}".to_string()), // f5
1180                InputEvent::Data("\u{001B}\u{005B}\u{0031}\u{0037}\u{007E}".to_string()), // f6
1181                InputEvent::Data("\u{001B}\u{005B}\u{0031}\u{0038}\u{007E}".to_string()), // f7
1182                InputEvent::Data("\u{001B}\u{005B}\u{0031}\u{0039}\u{007E}".to_string()), // f8
1183                InputEvent::Data("\u{001B}\u{005B}\u{0032}\u{0030}\u{007E}".to_string()), // f9
1184                InputEvent::Data("\u{001B}\u{005B}\u{0032}\u{0031}\u{007E}".to_string()), // f10
1185                InputEvent::Data("\u{001B}\u{005B}\u{0032}\u{0033}\u{007E}".to_string()), // f11
1186                InputEvent::Data("\u{001B}\u{005B}\u{0032}\u{0034}\u{007E}".to_string()), // f12
1187                InputEvent::Data("z".to_string()),
1188                InputEvent::Data("\r".to_string()),
1189            ],
1190        );
1191
1192        // Expect recognized extended key sequences to be consumed and regular input to remain intact.
1193        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        // Expect line buffer reset after each Enter with no cross-line bleed.
1211        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        // Expect raw mode to pass bytes through unchanged and skip echo when echo is disabled.
1227        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        // Expect raw mode with echo enabled to mirror exactly what is forwarded.
1237        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        // Expect DEL to be forwarded literally in raw mode.
1247        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        // Expect escape bytes to be forwarded literally in raw mode.
1260        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        // Expect InputEvent::Raw to follow the same raw-mode path as InputEvent::Data.
1270        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        // Expect UTF-8 characters in one chunk to roundtrip in canonical mode.
1286        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        // Expect each Enter to emit an empty line independently.
1302        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        // Expect writes to go only to replaced stdin target after stdin_replace.
1321        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        // Expect unknown escape sequence bytes to be treated as regular input.
1337        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        // Expect ctrl-c byte passthrough in raw mode.
1347        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        // Expect duplicate suppression on mobile path for identical near-consecutive input.
1364        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        // Expect no deduplication outside mobile path.
1381        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            // Expect split delivery across chunks to preserve command+enter behavior.
1401            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        // Expect one-frame command+enter to execute as a full line.
1411        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        // Expect backspace to remove an entire UTF-8 character.
1427        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        // Expect CRLF in one chunk to be normalized as a single Enter.
1442        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        // Expect split CR then LF to still map to a single Enter event.
1458        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        // Expect split escape sequence fragments to be assembled and consumed.
1476        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        // Expect split function-key sequence fragments to be assembled and consumed.
1496        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        // Expect cursor-left to enable inline insertion before the final character.
1514        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        // Expect home key to move cursor to start for insertion.
1531        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        // Expect ctrl-u to kill the current line before new input.
1548        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        // Expect ctrl-w to erase the previous word boundary.
1564        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        // Expect UTF-8 codepoints split across chunks to be reconstructed correctly.
1580        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        // Expect ctrl-c to clear line buffer even when no external signal handler is registered.
1597        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        // Expect mixed text+DEL+Enter in one chunk to still honor editing semantics.
1606        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        // Expect mixed text+ctrl-c+enter in one chunk to clear line and still emit SIGINT.
1618        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        // Expect a lone ESC prefix to be treated as text while Enter still flushes the line.
1635        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        // Expect ctrl-c to be handled as interrupt even after a broken ESC prefix.
1656        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        // Expect canonical EOF at BOL to avoid forwarding/echoing literal ctrl-d bytes.
1669        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        // Expect canonical EOF with buffered text to flush the buffer without trailing newline.
1685        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        // Expect ctrl-u to clear the buffered line and echo erase feedback in canonical mode.
1702        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        // Expect ctrl-w to erase the previous word and echo erase feedback.
1719        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        // Expect inline insertion to preserve data and emit cursor-repair echo output.
1737        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        // Expect deleting in the middle of the line to repaint the remaining tail and restore the cursor.
1755        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        // Expect BS (0x08) to behave like DEL (0x7F) for canonical erase.
1772        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        // Expect parser state from canonical mode not to leak across raw-mode toggles.
1795        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        // Expect ctrl-\ to signal SIGQUIT and clear current line before continuing.
1815        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        // Expect ctrl-z to signal SIGTSTP and clear current line before continuing.
1837        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        // Expect CR to be ignored and LF to flush the full buffered line.
1859        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        // Expect CR to be buffered as data when CR->LF mapping is disabled.
1877        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        // Expect tail redraw after inline insert: X + shifted tail + cursor restore.
1894        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        // Expect cursor-app-mode left sequence to be treated as cursor-left.
1911        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        // Expect common Home/End tilde variants to be consumed as navigation keys.
1928        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        // Expect terminal defaults to match the conventional 80x25 console geometry.
1937        assert_eq!(tty_state.cols, 80);
1938        assert_eq!(tty_state.rows, 25);
1939    }
1940}