Skip to main content

wasi_shell/
readline.rs

1use std::io::{self, Read, Write};
2
3// ---------------------------------------------------------------------------
4// Platform-specific raw terminal mode
5// ---------------------------------------------------------------------------
6
7#[cfg(unix)]
8struct RawModeGuard {
9    original: libc::termios,
10}
11
12#[cfg(unix)]
13impl RawModeGuard {
14    fn enter() -> io::Result<Self> {
15        let mut original: libc::termios = unsafe { std::mem::zeroed() };
16        if unsafe { libc::tcgetattr(libc::STDIN_FILENO, &mut original) } != 0 {
17            return Err(io::Error::last_os_error());
18        }
19        let mut raw = original;
20        raw.c_lflag &= !(libc::ICANON | libc::ECHO | libc::ISIG);
21        raw.c_cc[libc::VMIN] = 1;
22        raw.c_cc[libc::VTIME] = 0;
23        if unsafe { libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, &raw) } != 0 {
24            return Err(io::Error::last_os_error());
25        }
26        Ok(Self { original })
27    }
28}
29
30#[cfg(unix)]
31impl Drop for RawModeGuard {
32    fn drop(&mut self) {
33        unsafe {
34            libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, &self.original);
35        }
36    }
37}
38
39// ---- Windows ----
40
41#[cfg(windows)]
42mod win32 {
43    #[link(name = "kernel32")]
44    unsafe extern "system" {
45        pub fn GetStdHandle(nStdHandle: u32) -> isize;
46        pub fn GetConsoleMode(hConsoleHandle: isize, lpMode: *mut u32) -> i32;
47        pub fn SetConsoleMode(hConsoleHandle: isize, dwMode: u32) -> i32;
48    }
49
50    pub const STD_INPUT_HANDLE: u32 = 0xFFFF_FFF6; // (DWORD)-10
51    pub const ENABLE_PROCESSED_INPUT: u32 = 0x0001;
52    pub const ENABLE_LINE_INPUT: u32 = 0x0002;
53    pub const ENABLE_ECHO_INPUT: u32 = 0x0004;
54    pub const ENABLE_VIRTUAL_TERMINAL_INPUT: u32 = 0x0200;
55}
56
57#[cfg(windows)]
58struct RawModeGuard {
59    handle: isize,
60    original_mode: u32,
61}
62
63#[cfg(windows)]
64impl RawModeGuard {
65    fn enter() -> io::Result<Self> {
66        let handle = unsafe { win32::GetStdHandle(win32::STD_INPUT_HANDLE) };
67        if handle == -1 {
68            return Err(io::Error::last_os_error());
69        }
70        let mut original_mode: u32 = 0;
71        if unsafe { win32::GetConsoleMode(handle, &mut original_mode) } == 0 {
72            return Err(io::Error::last_os_error());
73        }
74        let new_mode = (original_mode
75            & !(win32::ENABLE_LINE_INPUT | win32::ENABLE_ECHO_INPUT | win32::ENABLE_PROCESSED_INPUT))
76            | win32::ENABLE_VIRTUAL_TERMINAL_INPUT;
77        if unsafe { win32::SetConsoleMode(handle, new_mode) } == 0 {
78            return Err(io::Error::last_os_error());
79        }
80        Ok(Self {
81            handle,
82            original_mode,
83        })
84    }
85}
86
87#[cfg(windows)]
88impl Drop for RawModeGuard {
89    fn drop(&mut self) {
90        unsafe {
91            win32::SetConsoleMode(self.handle, self.original_mode);
92        }
93    }
94}
95
96// ---- WASI ----
97
98#[cfg(target_os = "wasi")]
99struct RawModeGuard;
100
101#[cfg(target_os = "wasi")]
102impl RawModeGuard {
103    fn enter() -> io::Result<Self> {
104        Ok(Self)
105    }
106}
107
108// ---------------------------------------------------------------------------
109// LineHandler trait
110// ---------------------------------------------------------------------------
111
112/// Trait for handling lines read by [`LineReader`].
113///
114/// Implement this to inject command-execution logic into the REPL loop.
115///
116/// # Return values
117///
118/// - `Ok(LoopAction::Continue)` — prompt for the next line.
119/// - `Ok(LoopAction::Break)` — exit the loop normally.
120/// - `Err(msg)` — print the error and continue.
121pub trait LineHandler {
122    fn handle_line(&self, line: &str) -> Result<LoopAction, String>;
123}
124
125/// Controls the REPL loop flow after a line is handled.
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub enum LoopAction {
128    /// Continue reading the next line.
129    Continue,
130    /// Exit the REPL loop.
131    Break,
132}
133
134// Blanket impl: closures that return Result<LoopAction, String>
135impl<F> LineHandler for F
136where
137    F: Fn(&str) -> Result<LoopAction, String>,
138{
139    fn handle_line(&self, line: &str) -> Result<LoopAction, String> {
140        self(line)
141    }
142}
143
144// ---------------------------------------------------------------------------
145// KeyEvent and LineEditor
146// ---------------------------------------------------------------------------
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub enum KeyEvent {
150    Char(char),
151    Enter,
152    Backspace,
153    Delete,
154    Up,
155    Down,
156    Left,
157    Right,
158    Home,
159    End,
160    CtrlA,
161    CtrlE,
162    CtrlU,
163    CtrlK,
164    CtrlW,
165    CtrlD,
166    CtrlC,
167}
168
169/// A stateful editor for a single line of text.
170pub struct LineEditor {
171    pub buffer: String,
172    pub cursor_pos: usize,
173    history_idx: usize,
174    saved_input: String,
175}
176
177impl LineEditor {
178    /// Create a new `LineEditor` initialized for a new line.
179    pub fn new(history_len: usize) -> Self {
180        Self {
181            buffer: String::new(),
182            cursor_pos: 0,
183            history_idx: history_len,
184            saved_input: String::new(),
185        }
186    }
187
188    /// Process a raw key code, updating the internal state.
189    ///
190    /// The `code` can be a printable ASCII character or a special value representing
191    /// a control key or escape sequence.
192    pub fn input_char(&mut self, code: u32, history: &[String]) {
193        let key = match code {
194            // Control characters
195            1 => KeyEvent::CtrlA,
196            3 => KeyEvent::CtrlC,
197            4 => KeyEvent::CtrlD,
198            5 => KeyEvent::CtrlE,
199            8 | 127 => KeyEvent::Backspace,
200            11 => KeyEvent::CtrlK,
201            13 | 10 => KeyEvent::Enter,
202            21 => KeyEvent::CtrlU,
203            23 => KeyEvent::CtrlW,
204            27 => return, // ESC should be handled by the caller for multi-byte sequences
205
206            // Custom codes for special keys (defined by the caller/LineReader)
207            1001 => KeyEvent::Up,
208            1002 => KeyEvent::Down,
209            1003 => KeyEvent::Right,
210            1004 => KeyEvent::Left,
211            1005 => KeyEvent::Home,
212            1006 => KeyEvent::End,
213            1007 => KeyEvent::Delete,
214
215            // Printable characters
216            c if c >= 0x20 => KeyEvent::Char(char::from_u32(c).unwrap_or(' ')),
217            _ => return,
218        };
219
220        self.apply_key(key, history);
221    }
222
223    fn apply_key(&mut self, key: KeyEvent, history: &[String]) {
224        match key {
225            KeyEvent::Char(ch) => {
226                self.buffer.insert(self.cursor_pos, ch);
227                self.cursor_pos += 1;
228            }
229            KeyEvent::Backspace => {
230                if self.cursor_pos > 0 {
231                    self.cursor_pos -= 1;
232                    self.buffer.remove(self.cursor_pos);
233                }
234            }
235            KeyEvent::Delete => {
236                if self.cursor_pos < self.buffer.len() {
237                    self.buffer.remove(self.cursor_pos);
238                }
239            }
240            KeyEvent::Left => {
241                if self.cursor_pos > 0 {
242                    self.cursor_pos -= 1;
243                }
244            }
245            KeyEvent::Right => {
246                if self.cursor_pos < self.buffer.len() {
247                    self.cursor_pos += 1;
248                }
249            }
250            KeyEvent::Home | KeyEvent::CtrlA => {
251                self.cursor_pos = 0;
252            }
253            KeyEvent::End | KeyEvent::CtrlE => {
254                self.cursor_pos = self.buffer.len();
255            }
256            KeyEvent::Up => {
257                if !history.is_empty() && self.history_idx > 0 {
258                    if self.history_idx == history.len() {
259                        self.saved_input = self.buffer.clone();
260                    }
261                    self.history_idx -= 1;
262                    self.buffer = history[self.history_idx].clone();
263                    self.cursor_pos = self.buffer.len();
264                }
265            }
266            KeyEvent::Down => {
267                if self.history_idx < history.len() {
268                    self.history_idx += 1;
269                    if self.history_idx == history.len() {
270                        self.buffer = self.saved_input.clone();
271                    } else {
272                        self.buffer = history[self.history_idx].clone();
273                    }
274                    self.cursor_pos = self.buffer.len();
275                }
276            }
277            KeyEvent::CtrlU => {
278                self.buffer.clear();
279                self.cursor_pos = 0;
280            }
281            KeyEvent::CtrlK => {
282                self.buffer.truncate(self.cursor_pos);
283            }
284            KeyEvent::CtrlW => {
285                if self.cursor_pos > 0 {
286                    let mut new_pos = self.cursor_pos;
287                    while new_pos > 0 && self.buffer.as_bytes().get(new_pos - 1) == Some(&b' ') {
288                        new_pos -= 1;
289                    }
290                    while new_pos > 0 && self.buffer.as_bytes().get(new_pos - 1) != Some(&b' ') {
291                        new_pos -= 1;
292                    }
293                    self.buffer.drain(new_pos..self.cursor_pos);
294                    self.cursor_pos = new_pos;
295                }
296            }
297            _ => {}
298        }
299    }
300}
301
302// ---------------------------------------------------------------------------
303// LineReader
304// ---------------------------------------------------------------------------
305
306/// A minimal line editor with command history.
307///
308/// Supports:
309/// - Up/Down arrow keys to navigate command history
310/// - Left/Right arrow keys to move the cursor within the line
311/// - Home/End to jump to the beginning/end of the line
312/// - Delete to remove the character under the cursor
313/// - Backspace to remove the character before the cursor
314/// - Ctrl-U to clear the line, Ctrl-K to kill to end of line
315/// - Ctrl-W to delete the previous word
316/// - Ctrl-A / Ctrl-E for Home / End
317pub struct LineReader {
318    history: Vec<String>,
319    max_history: usize,
320}
321
322impl LineReader {
323    /// Create a new `LineReader` with the given maximum history size.
324    pub fn new(max_history: usize) -> Self {
325        Self {
326            history: Vec::new(),
327            max_history,
328        }
329    }
330
331    /// Add a line to the history (skipping duplicates of the last entry and empty lines).
332    fn push_history(&mut self, line: &str) {
333        let trimmed = line.trim();
334        if trimmed.is_empty() {
335            return;
336        }
337        if self.history.last().map(|s| s.as_str()) == Some(trimmed) {
338            return;
339        }
340        self.history.push(trimmed.to_string());
341        if self.history.len() > self.max_history {
342            self.history.remove(0);
343        }
344    }
345
346    /// Read a line interactively with arrow-key history navigation.
347    ///
348    /// Returns `Ok(Some(line))` on success, `Ok(None)` on EOF (Ctrl-D).
349    pub fn read_line(&mut self, prompt: &str, cancel_token: Option<wasibox_core::CancellationToken>) -> io::Result<Option<String>> {
350        let mut stdout = io::stdout();
351        write!(stdout, "{}", prompt)?;
352        stdout.flush()?;
353
354        let _guard = RawModeGuard::enter()?;
355
356        let mut reader = io::stdin();
357        self.read_line_from(&mut reader, &mut stdout, prompt, cancel_token)
358    }
359
360    /// Read a line interactively using a provided reader.
361    pub fn read_line_with_stdin(&mut self, prompt: &str, cancel_token: Option<wasibox_core::CancellationToken>, mut reader: Box<dyn Read>) -> io::Result<Option<String>> {
362        let mut stdout = io::stdout();
363        write!(stdout, "{}", prompt)?;
364        stdout.flush()?;
365
366        let _guard = RawModeGuard::enter()?;
367
368        self.read_line_from(&mut reader, &mut stdout, prompt, cancel_token)
369    }
370
371    /// Run an interactive REPL loop, delegating each line to `handler`.
372    ///
373    /// The loop ends when:
374    /// - The handler returns `Ok(LoopAction::Break)`
375    /// - EOF is reached (Ctrl-D)
376    /// - An I/O error occurs
377    pub fn run_loop<P, H>(&mut self, prompt_fn: P, handler: &H, cancel_token: wasibox_core::CancellationToken) -> io::Result<()>
378    where
379        P: Fn() -> String,
380        H: LineHandler,
381    {
382        loop {
383            let prompt = prompt_fn();
384            match self.read_line(&prompt, Some(cancel_token.clone()))? {
385                None => break,
386                Some(line) => {
387                    let trimmed = line.trim();
388                    if trimmed.is_empty() {
389                        continue;
390                    }
391                    match handler.handle_line(trimmed) {
392                        Ok(LoopAction::Continue) => {}
393                        Ok(LoopAction::Break) => break,
394                        Err(e) => {
395                            eprintln!("{}", e);
396                        }
397                    }
398                }
399            }
400        }
401        Ok(())
402    }
403
404    /// Run an interactive REPL loop using a provided reader.
405    pub fn run_loop_with_stdin<P, H>(&mut self, prompt_fn: P, handler: &H, cancel_token: wasibox_core::CancellationToken, mut reader: Box<dyn Read>) -> io::Result<()>
406    where
407        P: Fn() -> String,
408        H: LineHandler,
409    {
410        loop {
411            let prompt = prompt_fn();
412            let _guard = RawModeGuard::enter()?;
413            match self.read_line_from(&mut reader, &mut io::stdout(), &prompt, Some(cancel_token.clone()))? {
414                None => break,
415                Some(line) => {
416                    let trimmed = line.trim();
417                    if trimmed.is_empty() {
418                        continue;
419                    }
420                    match handler.handle_line(trimmed) {
421                        Ok(LoopAction::Continue) => {}
422                        Ok(LoopAction::Break) => break,
423                        Err(e) => {
424                            eprintln!("{}", e);
425                        }
426                    }
427                }
428            }
429        }
430        Ok(())
431    }
432
433    /// Testable REPL loop that reads from `reader` and writes to `writer`.
434    #[cfg(test)]
435    fn run_loop_from<R: Read, W: Write, H: LineHandler>(
436        &mut self,
437        reader: &mut R,
438        writer: &mut W,
439        prompt: &str,
440        handler: &H,
441        cancel_token: Option<wasibox_core::CancellationToken>,
442    ) -> io::Result<()> {
443        loop {
444            write!(writer, "{}", prompt)?;
445            writer.flush()?;
446            match self.read_line_from(reader, writer, prompt, cancel_token.clone())? {
447                None => break,
448                Some(line) => {
449                    let trimmed = line.trim();
450                    if trimmed.is_empty() {
451                        continue;
452                    }
453                    match handler.handle_line(trimmed) {
454                        Ok(LoopAction::Continue) => {}
455                        Ok(LoopAction::Break) => break,
456                        Err(e) => {
457                            writeln!(writer, "Error: {}", e)?;
458                        }
459                    }
460                }
461            }
462        }
463        Ok(())
464    }
465
466    /// Core line-editing logic, reading bytes from `reader` and writing to `writer`.
467    /// Separated from `read_line` so it can be tested with synthetic input.
468    fn read_line_from<R: Read, W: Write>(
469        &mut self,
470        reader: &mut R,
471        writer: &mut W,
472        prompt: &str,
473        cancel_token: Option<wasibox_core::CancellationToken>,
474    ) -> io::Result<Option<String>> {
475        let mut editor = LineEditor::new(self.history.len());
476
477        loop {
478            let b = {
479                let mut buf = [0u8; 1];
480                reader.read_exact(&mut buf)?;
481                buf[0]
482            };
483
484            let code = match b {
485                // Ctrl-D on empty line => EOF
486                4 => {
487                    if editor.buffer.is_empty() {
488                        write!(writer, "\r\n")?;
489                        writer.flush()?;
490                        return Ok(None);
491                    }
492                    4
493                }
494                // Ctrl-C => discard line
495                3 => {
496                    if let Some(token) = &cancel_token {
497                        token.cancel();
498                    }
499                    write!(writer, "^C\r\n")?;
500                    writer.flush()?;
501                    return Ok(Some(String::new()));
502                }
503                // ESC => start of escape sequence
504                27 => {
505                    let seq1 = {
506                        let mut buf = [0u8; 1];
507                        reader.read_exact(&mut buf)?;
508                        buf[0]
509                    };
510                    if seq1 == b'[' {
511                        let seq2 = {
512                            let mut buf = [0u8; 1];
513                            reader.read_exact(&mut buf)?;
514                            buf[0]
515                        };
516                        match seq2 {
517                            b'A' => 1001, // Up
518                            b'B' => 1002, // Down
519                            b'C' => 1003, // Right
520                            b'D' => 1004, // Left
521                            b'H' => 1005, // Home
522                            b'F' => 1006, // End
523                            b'3' => {
524                                let seq3 = {
525                                    let mut buf = [0u8; 1];
526                                    reader.read_exact(&mut buf)?;
527                                    buf[0]
528                                };
529                                if seq3 == b'~' {
530                                    1007 // Delete
531                                } else {
532                                    continue;
533                                }
534                            }
535                            _ => continue,
536                        }
537                    } else {
538                        continue;
539                    }
540                }
541                other => other as u32,
542            };
543
544            if code == 13 || code == 10 { // Enter
545                write!(writer, "\r\n")?;
546                writer.flush()?;
547                self.push_history(&editor.buffer);
548                return Ok(Some(editor.buffer));
549            }
550
551            let old_pos = editor.cursor_pos;
552            let old_len = editor.buffer.len();
553
554            editor.input_char(code, &self.history);
555
556            // Redraw optimization
557            if code >= 0x20 && code < 1000 && old_pos == old_len && editor.cursor_pos == editor.buffer.len() {
558                write!(writer, "{}", char::from_u32(code).unwrap())?;
559                writer.flush()?;
560            } else if code == 1004 && old_pos > editor.cursor_pos && old_pos > 0 { // Left
561                write!(writer, "\x1b[D")?;
562                writer.flush()?;
563            } else if code == 1003 && old_pos < editor.cursor_pos && old_pos < old_len { // Right
564                write!(writer, "\x1b[C")?;
565                writer.flush()?;
566            } else {
567                Self::redraw_line(writer, prompt, &editor.buffer, editor.cursor_pos)?;
568            }
569        }
570    }
571
572    /// Redraw the current line (clear and rewrite).
573    fn redraw_line<W: Write>(
574        writer: &mut W,
575        prompt: &str,
576        line: &str,
577        cursor_pos: usize,
578    ) -> io::Result<()> {
579        write!(writer, "\r\x1b[K{}{}", prompt, line)?;
580        let total_len = prompt.len() + line.len();
581        let target = prompt.len() + cursor_pos;
582        if target < total_len {
583            write!(writer, "\x1b[{}D", total_len - target)?;
584        }
585        writer.flush()
586    }
587}
588
589// ---------------------------------------------------------------------------
590// Tests
591// ---------------------------------------------------------------------------
592
593#[cfg(test)]
594mod tests {
595    use super::*;
596    use std::io::Cursor;
597
598    /// Helper: build a byte sequence from a list of key inputs.
599    fn keys(parts: &[&[u8]]) -> Cursor<Vec<u8>> {
600        let mut buf = Vec::new();
601        for part in parts {
602            buf.extend_from_slice(part);
603        }
604        Cursor::new(buf)
605    }
606
607    const UP: &[u8] = b"\x1b[A";
608    const DOWN: &[u8] = b"\x1b[B";
609    const ENTER: &[u8] = b"\r";
610
611    #[test]
612    fn test_line_editor_basic() {
613        let mut editor = LineEditor::new(0);
614        let history = vec![];
615
616        editor.input_char('a' as u32, &history);
617        editor.input_char('b' as u32, &history);
618        assert_eq!(editor.buffer, "ab");
619        assert_eq!(editor.cursor_pos, 2);
620
621        editor.input_char(1004, &history); // Left
622        assert_eq!(editor.cursor_pos, 1);
623
624        editor.input_char('c' as u32, &history);
625        assert_eq!(editor.buffer, "acb");
626        assert_eq!(editor.cursor_pos, 2);
627
628        editor.input_char(127, &history); // Backspace
629        assert_eq!(editor.buffer, "ab");
630        assert_eq!(editor.cursor_pos, 1);
631    }
632
633    #[test]
634    fn test_line_editor_history() {
635        let history = vec!["first".to_string(), "second".to_string()];
636        let mut editor = LineEditor::new(history.len());
637
638        editor.input_char(1001, &history); // Up
639        assert_eq!(editor.buffer, "second");
640
641        editor.input_char(1001, &history); // Up
642        assert_eq!(editor.buffer, "first");
643
644        editor.input_char(1002, &history); // Down
645        assert_eq!(editor.buffer, "second");
646
647        editor.input_char(1002, &history); // Down
648        assert_eq!(editor.buffer, ""); // Back to current
649    }
650
651    #[test]
652    fn test_simple_input() {
653        let mut reader = LineReader::new(100);
654        let mut input = keys(&[b"hello", ENTER]);
655        let mut out = Vec::new();
656        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
657        assert_eq!(result, Some("hello".to_string()));
658    }
659
660    #[test]
661    fn test_eof_on_empty() {
662        let mut reader = LineReader::new(100);
663        let mut input = Cursor::new(vec![4u8]); // Ctrl-D
664        let mut out = Vec::new();
665        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
666        assert_eq!(result, None);
667    }
668
669    #[test]
670    fn test_history_up_arrow() {
671        let mut reader = LineReader::new(100);
672        let mut out = Vec::new();
673
674        // First command
675        let mut input = keys(&[b"echo hello", ENTER]);
676        reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
677
678        // Second command: press Up then Enter (should recall "echo hello")
679        let mut input = keys(&[UP, ENTER]);
680        out.clear();
681        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
682        assert_eq!(result, Some("echo hello".to_string()));
683    }
684
685    #[test]
686    fn test_history_up_down_arrow() {
687        let mut reader = LineReader::new(100);
688        let mut out = Vec::new();
689
690        // Enter two commands
691        let mut input = keys(&[b"first", ENTER]);
692        reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
693        let mut input = keys(&[b"second", ENTER]);
694        reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
695
696        // Up twice => "first", Down once => "second", Enter
697        let mut input = keys(&[UP, UP, DOWN, ENTER]);
698        out.clear();
699        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
700        assert_eq!(result, Some("second".to_string()));
701    }
702
703    #[test]
704    fn test_history_down_restores_current_input() {
705        let mut reader = LineReader::new(100);
706        let mut out = Vec::new();
707
708        // Enter a command into history
709        let mut input = keys(&[b"old", ENTER]);
710        reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
711
712        // Type "new", press Up (recalls "old"), press Down (restores "new"), Enter
713        let mut input = keys(&[b"new", UP, DOWN, ENTER]);
714        out.clear();
715        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
716        assert_eq!(result, Some("new".to_string()));
717    }
718
719    #[test]
720    fn test_history_dedup() {
721        let mut reader = LineReader::new(100);
722        let mut out = Vec::new();
723
724        // Enter same command twice
725        let mut input = keys(&[b"dup", ENTER]);
726        reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
727        let mut input = keys(&[b"dup", ENTER]);
728        reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
729
730        // Up should recall "dup", another Up should NOT go further
731        // (only one entry in history)
732        let mut input = keys(&[UP, UP, ENTER]);
733        out.clear();
734        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
735        assert_eq!(result, Some("dup".to_string()));
736    }
737
738    #[test]
739    fn test_history_max_size() {
740        let mut reader = LineReader::new(3);
741        let mut out = Vec::new();
742
743        for cmd in &["aaa", "bbb", "ccc", "ddd"] {
744            let mut input = keys(&[cmd.as_bytes(), ENTER]);
745            reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
746        }
747
748        // Up 3 times should stop at "bbb" (oldest "aaa" was evicted)
749        let mut input = keys(&[UP, UP, UP, ENTER]);
750        out.clear();
751        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
752        assert_eq!(result, Some("bbb".to_string()));
753    }
754
755    #[test]
756    fn test_backspace() {
757        let mut reader = LineReader::new(100);
758        let mut input = keys(&[b"helloo", &[127], ENTER]);
759        let mut out = Vec::new();
760        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
761        assert_eq!(result, Some("hello".to_string()));
762    }
763
764    #[test]
765    fn test_ctrl_u_clears_line() {
766        let mut reader = LineReader::new(100);
767        let mut input = keys(&[b"garbage", &[21], b"clean", ENTER]); // Ctrl-U = 21
768        let mut out = Vec::new();
769        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
770        assert_eq!(result, Some("clean".to_string()));
771    }
772
773    #[test]
774    fn test_empty_line_not_in_history() {
775        let mut reader = LineReader::new(100);
776        let mut out = Vec::new();
777
778        // Enter a real command
779        let mut input = keys(&[b"real", ENTER]);
780        reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
781
782        // Enter an empty line
783        let mut input = keys(&[ENTER]);
784        reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
785
786        // Up should still recall "real", not empty
787        let mut input = keys(&[UP, ENTER]);
788        out.clear();
789        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
790        assert_eq!(result, Some("real".to_string()));
791    }
792
793    // ── LineHandler / run_loop tests ─────────────────────────────────────
794
795    #[test]
796    fn test_run_loop_with_handler() {
797        use std::sync::{Arc, Mutex};
798
799        let executed = Arc::new(Mutex::new(Vec::new()));
800        let exec_clone = Arc::clone(&executed);
801
802        let handler = move |line: &str| -> Result<LoopAction, String> {
803            exec_clone.lock().unwrap().push(line.to_string());
804            Ok(LoopAction::Continue)
805        };
806
807        let mut reader = LineReader::new(100);
808        // Type two commands then Ctrl-D
809        let mut input = keys(&[b"echo hello", ENTER, b"ls", ENTER, &[4]]);
810        let mut out = Vec::new();
811        reader.run_loop_from(&mut input, &mut out, "$ ", &handler, None).unwrap();
812
813        let cmds = executed.lock().unwrap();
814        assert_eq!(cmds.len(), 2);
815        assert_eq!(cmds[0], "echo hello");
816        assert_eq!(cmds[1], "ls");
817    }
818
819    #[test]
820    fn test_run_loop_break_on_exit() {
821        let handler = |line: &str| -> Result<LoopAction, String> {
822            if line == "exit" {
823                Ok(LoopAction::Break)
824            } else {
825                Ok(LoopAction::Continue)
826            }
827        };
828
829        let mut reader = LineReader::new(100);
830        let mut input = keys(&[b"cmd1", ENTER, b"exit", ENTER, b"cmd2", ENTER]);
831        let mut out = Vec::new();
832        reader.run_loop_from(&mut input, &mut out, "$ ", &handler, None).unwrap();
833        // Loop should have stopped after "exit"; "cmd2" is never processed.
834    }
835
836    #[test]
837    fn test_run_loop_error_continues() {
838        use std::sync::{Arc, Mutex};
839
840        let count = Arc::new(Mutex::new(0u32));
841        let count_clone = Arc::clone(&count);
842
843        let handler = move |line: &str| -> Result<LoopAction, String> {
844            *count_clone.lock().unwrap() += 1;
845            if line == "fail" {
846                Err("simulated error".to_string())
847            } else {
848                Ok(LoopAction::Continue)
849            }
850        };
851
852        let mut reader = LineReader::new(100);
853        let mut input = keys(&[b"ok", ENTER, b"fail", ENTER, b"ok2", ENTER, &[4]]);
854        let mut out = Vec::new();
855        reader.run_loop_from(&mut input, &mut out, "$ ", &handler, None).unwrap();
856
857        // All three commands should have been processed (error doesn't stop loop)
858        assert_eq!(*count.lock().unwrap(), 3);
859    }
860
861    #[test]
862    fn test_run_loop_with_history_navigation() {
863        use std::sync::{Arc, Mutex};
864
865        let executed = Arc::new(Mutex::new(Vec::new()));
866        let exec_clone = Arc::clone(&executed);
867
868        let handler = move |line: &str| -> Result<LoopAction, String> {
869            exec_clone.lock().unwrap().push(line.to_string());
870            Ok(LoopAction::Continue)
871        };
872
873        let mut reader = LineReader::new(100);
874        // Enter "echo hello", then press Up+Enter to replay it
875        let mut input = keys(&[
876            b"echo hello", ENTER,
877            UP, ENTER,  // replay from history
878            &[4],       // EOF
879        ]);
880        let mut out = Vec::new();
881        reader.run_loop_from(&mut input, &mut out, "$ ", &handler, None).unwrap();
882
883        let cmds = executed.lock().unwrap();
884        assert_eq!(cmds.len(), 2);
885        assert_eq!(cmds[0], "echo hello");
886        assert_eq!(cmds[1], "echo hello"); // replayed from history
887    }
888
889    #[test]
890    fn test_run_loop_with_handle_parallel() {
891        use std::sync::{Arc, Mutex};
892        use crate::{CommandRegistry, handle_parallel, ArcVecWriter};
893
894        let registry = Arc::new(CommandRegistry::with_builtins());
895        let output = Arc::new(Mutex::new(Vec::<u8>::new()));
896
897        let reg = Arc::clone(&registry);
898        let out_ref = Arc::clone(&output);
899
900        let handler = move |line: &str| -> Result<LoopAction, String> {
901            if line == "exit" {
902                return Ok(LoopAction::Break);
903            }
904            let results = handle_parallel(
905                vec![line.to_string()],
906                Box::new(std::io::empty()),
907                Box::new(ArcVecWriter { inner: Arc::clone(&out_ref) }),
908                Arc::clone(&reg),
909                wasibox_core::CancellationToken::new(),
910            );
911            for res in results {
912                res?;
913            }
914            Ok(LoopAction::Continue)
915        };
916
917        let mut reader = LineReader::new(100);
918        // Run "echo hello", then Up+Enter to replay, then "exit"
919        let mut input = keys(&[
920            b"echo hello", ENTER,
921            UP, ENTER,  // replay "echo hello" via history
922            b"exit", ENTER,
923        ]);
924        let mut term_out = Vec::new();
925        reader.run_loop_from(&mut input, &mut term_out, "$ ", &handler, None).unwrap();
926
927        let buf = output.lock().unwrap();
928        let result = String::from_utf8_lossy(&buf);
929        let lines: Vec<&str> = result.trim().lines().collect();
930        assert_eq!(lines.len(), 2);
931        assert_eq!(lines[0], "hello");
932        assert_eq!(lines[1], "hello"); // replayed from history
933    }
934}