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 const 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    ///
193    /// Returns `Some(buffer)` if Enter was pressed, indicating the line is complete.
194    pub fn input_char(&mut self, code: u32, history: &[String]) -> Option<String> {
195        let key = match code {
196            // Control characters
197            1 => KeyEvent::CtrlA,
198            3 => KeyEvent::CtrlC,
199            4 => KeyEvent::CtrlD,
200            5 => KeyEvent::CtrlE,
201            8 | 127 => KeyEvent::Backspace,
202            11 => KeyEvent::CtrlK,
203            13 | 10 => KeyEvent::Enter,
204            21 => KeyEvent::CtrlU,
205            23 => KeyEvent::CtrlW,
206            27 => return None, // ESC should be handled by the caller for multi-byte sequences
207
208            // Custom codes for special keys (defined by the caller/LineReader)
209            1001 => KeyEvent::Up,
210            1002 => KeyEvent::Down,
211            1003 => KeyEvent::Right,
212            1004 => KeyEvent::Left,
213            1005 => KeyEvent::Home,
214            1006 => KeyEvent::End,
215            1007 => KeyEvent::Delete,
216
217            // Printable characters
218            c if c >= 0x20 => KeyEvent::Char(char::from_u32(c).unwrap_or(' ')),
219            _ => return None,
220        };
221
222        if key == KeyEvent::Enter {
223            Some(self.buffer.clone())
224        } else {
225            self.apply_key(key, history);
226            None
227        }
228    }
229
230    fn apply_key(&mut self, key: KeyEvent, history: &[String]) {
231        match key {
232            KeyEvent::Char(ch) => {
233                self.buffer.insert(self.cursor_pos, ch);
234                self.cursor_pos += 1;
235            }
236            KeyEvent::Backspace => {
237                if self.cursor_pos > 0 {
238                    self.cursor_pos -= 1;
239                    self.buffer.remove(self.cursor_pos);
240                }
241            }
242            KeyEvent::Delete => {
243                if self.cursor_pos < self.buffer.len() {
244                    self.buffer.remove(self.cursor_pos);
245                }
246            }
247            KeyEvent::Left => {
248                if self.cursor_pos > 0 {
249                    self.cursor_pos -= 1;
250                }
251            }
252            KeyEvent::Right => {
253                if self.cursor_pos < self.buffer.len() {
254                    self.cursor_pos += 1;
255                }
256            }
257            KeyEvent::Home | KeyEvent::CtrlA => {
258                self.cursor_pos = 0;
259            }
260            KeyEvent::End | KeyEvent::CtrlE => {
261                self.cursor_pos = self.buffer.len();
262            }
263            KeyEvent::Up => {
264                if !history.is_empty() && self.history_idx > 0 {
265                    if self.history_idx == history.len() {
266                        self.saved_input = self.buffer.clone();
267                    }
268                    self.history_idx -= 1;
269                    self.buffer = history[self.history_idx].clone();
270                    self.cursor_pos = self.buffer.len();
271                }
272            }
273            KeyEvent::Down => {
274                if self.history_idx < history.len() {
275                    self.history_idx += 1;
276                    if self.history_idx == history.len() {
277                        self.buffer = self.saved_input.clone();
278                    } else {
279                        self.buffer = history[self.history_idx].clone();
280                    }
281                    self.cursor_pos = self.buffer.len();
282                }
283            }
284            KeyEvent::CtrlU => {
285                self.buffer.clear();
286                self.cursor_pos = 0;
287            }
288            KeyEvent::CtrlK => {
289                self.buffer.truncate(self.cursor_pos);
290            }
291            KeyEvent::CtrlW => {
292                if self.cursor_pos > 0 {
293                    let mut new_pos = self.cursor_pos;
294                    while new_pos > 0 && self.buffer.as_bytes().get(new_pos - 1) == Some(&b' ') {
295                        new_pos -= 1;
296                    }
297                    while new_pos > 0 && self.buffer.as_bytes().get(new_pos - 1) != Some(&b' ') {
298                        new_pos -= 1;
299                    }
300                    self.buffer.drain(new_pos..self.cursor_pos);
301                    self.cursor_pos = new_pos;
302                }
303            }
304            _ => {}
305        }
306    }
307}
308
309// ---------------------------------------------------------------------------
310// LineReader
311// ---------------------------------------------------------------------------
312
313/// A minimal line editor with command history.
314///
315/// Supports:
316/// - Up/Down arrow keys to navigate command history
317/// - Left/Right arrow keys to move the cursor within the line
318/// - Home/End to jump to the beginning/end of the line
319/// - Delete to remove the character under the cursor
320/// - Backspace to remove the character before the cursor
321/// - Ctrl-U to clear the line, Ctrl-K to kill to end of line
322/// - Ctrl-W to delete the previous word
323/// - Ctrl-A / Ctrl-E for Home / End
324pub struct LineReader {
325    history: Vec<String>,
326    max_history: usize,
327}
328
329impl LineReader {
330    /// Create a new `LineReader` with the given maximum history size.
331    pub fn new(max_history: usize) -> Self {
332        Self {
333            history: Vec::new(),
334            max_history,
335        }
336    }
337
338    /// Add a line to the history (skipping duplicates of the last entry and empty lines).
339    fn push_history(&mut self, line: &str) {
340        let trimmed = line.trim();
341        if trimmed.is_empty() {
342            return;
343        }
344        if self.history.last().map(|s| s.as_str()) == Some(trimmed) {
345            return;
346        }
347        self.history.push(trimmed.to_string());
348        if self.history.len() > self.max_history {
349            self.history.remove(0);
350        }
351    }
352
353    /// Read a line interactively with arrow-key history navigation.
354    ///
355    /// Returns `Ok(Some(line))` on success, `Ok(None)` on EOF (Ctrl-D).
356    pub fn read_line(&mut self, prompt: &str, cancel_token: Option<wasibox_core::CancellationToken>) -> io::Result<Option<String>> {
357        let mut stdout = io::stdout();
358        write!(stdout, "{}", prompt)?;
359        stdout.flush()?;
360
361        let _guard = RawModeGuard::enter()?;
362
363        let mut reader = io::stdin();
364        self.read_line_from(&mut reader, &mut stdout, prompt, cancel_token)
365    }
366
367    /// Read a line interactively using a provided reader.
368    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>> {
369        let mut stdout = io::stdout();
370        write!(stdout, "{}", prompt)?;
371        stdout.flush()?;
372
373        let _guard = RawModeGuard::enter()?;
374
375        self.read_line_from(&mut reader, &mut stdout, prompt, cancel_token)
376    }
377
378    /// Run an interactive REPL loop, delegating each line to `handler`.
379    ///
380    /// The loop ends when:
381    /// - The handler returns `Ok(LoopAction::Break)`
382    /// - EOF is reached (Ctrl-D)
383    /// - An I/O error occurs
384    pub fn run_loop<P, H>(&mut self, prompt_fn: P, handler: &H, cancel_token: wasibox_core::CancellationToken) -> io::Result<()>
385    where
386        P: Fn() -> String,
387        H: LineHandler,
388    {
389        loop {
390            let prompt = prompt_fn();
391            match self.read_line(&prompt, Some(cancel_token.clone()))? {
392                None => break,
393                Some(line) => {
394                    let trimmed = line.trim();
395                    if trimmed.is_empty() {
396                        continue;
397                    }
398                    match handler.handle_line(trimmed) {
399                        Ok(LoopAction::Continue) => {}
400                        Ok(LoopAction::Break) => break,
401                        Err(e) => {
402                            eprintln!("{}", e);
403                        }
404                    }
405                }
406            }
407        }
408        Ok(())
409    }
410
411    /// Run an interactive REPL loop using a provided reader.
412    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<()>
413    where
414        P: Fn() -> String,
415        H: LineHandler,
416    {
417        loop {
418            let prompt = prompt_fn();
419            let _guard = RawModeGuard::enter()?;
420            match self.read_line_from(&mut reader, &mut io::stdout(), &prompt, Some(cancel_token.clone()))? {
421                None => break,
422                Some(line) => {
423                    let trimmed = line.trim();
424                    if trimmed.is_empty() {
425                        continue;
426                    }
427                    match handler.handle_line(trimmed) {
428                        Ok(LoopAction::Continue) => {}
429                        Ok(LoopAction::Break) => break,
430                        Err(e) => {
431                            eprintln!("{}", e);
432                        }
433                    }
434                }
435            }
436        }
437        Ok(())
438    }
439
440    /// Testable REPL loop that reads from `reader` and writes to `writer`.
441    #[cfg(test)]
442    fn run_loop_from<R: Read, W: Write, H: LineHandler>(
443        &mut self,
444        reader: &mut R,
445        writer: &mut W,
446        prompt: &str,
447        handler: &H,
448        cancel_token: Option<wasibox_core::CancellationToken>,
449    ) -> io::Result<()> {
450        loop {
451            write!(writer, "{}", prompt)?;
452            writer.flush()?;
453            match self.read_line_from(reader, writer, prompt, cancel_token.clone())? {
454                None => break,
455                Some(line) => {
456                    let trimmed = line.trim();
457                    if trimmed.is_empty() {
458                        continue;
459                    }
460                    match handler.handle_line(trimmed) {
461                        Ok(LoopAction::Continue) => {}
462                        Ok(LoopAction::Break) => break,
463                        Err(e) => {
464                            writeln!(writer, "Error: {}", e)?;
465                        }
466                    }
467                }
468            }
469        }
470        Ok(())
471    }
472
473    /// Core line-editing logic, reading bytes from `reader` and writing to `writer`.
474    /// Separated from `read_line` so it can be tested with synthetic input.
475    fn read_line_from<R: Read, W: Write>(
476        &mut self,
477        reader: &mut R,
478        writer: &mut W,
479        prompt: &str,
480        cancel_token: Option<wasibox_core::CancellationToken>,
481    ) -> io::Result<Option<String>> {
482        let mut editor = LineEditor::new(self.history.len());
483
484        loop {
485            let b = {
486                let mut buf = [0u8; 1];
487                reader.read_exact(&mut buf)?;
488                buf[0]
489            };
490
491            let code = match b {
492                // Ctrl-D on empty line => EOF
493                4 => {
494                    if editor.buffer.is_empty() {
495                        write!(writer, "\r\n")?;
496                        writer.flush()?;
497                        return Ok(None);
498                    }
499                    4
500                }
501                // Ctrl-C => discard line
502                3 => {
503                    if let Some(token) = &cancel_token {
504                        token.cancel();
505                    }
506                    write!(writer, "^C\r\n")?;
507                    writer.flush()?;
508                    return Ok(Some(String::new()));
509                }
510                // ESC => start of escape sequence
511                27 => {
512                    let seq1 = {
513                        let mut buf = [0u8; 1];
514                        reader.read_exact(&mut buf)?;
515                        buf[0]
516                    };
517                    if seq1 == b'[' {
518                        let seq2 = {
519                            let mut buf = [0u8; 1];
520                            reader.read_exact(&mut buf)?;
521                            buf[0]
522                        };
523                        match seq2 {
524                            b'A' => 1001, // Up
525                            b'B' => 1002, // Down
526                            b'C' => 1003, // Right
527                            b'D' => 1004, // Left
528                            b'H' => 1005, // Home
529                            b'F' => 1006, // End
530                            b'3' => {
531                                let seq3 = {
532                                    let mut buf = [0u8; 1];
533                                    reader.read_exact(&mut buf)?;
534                                    buf[0]
535                                };
536                                if seq3 == b'~' {
537                                    1007 // Delete
538                                } else {
539                                    continue;
540                                }
541                            }
542                            _ => continue,
543                        }
544                    } else {
545                        continue;
546                    }
547                }
548                other => other as u32,
549            };
550
551            let old_pos = editor.cursor_pos;
552            let old_len = editor.buffer.len();
553
554            if let Some(completed_line) = editor.input_char(code, &self.history) {
555                write!(writer, "\r\n")?;
556                writer.flush()?;
557                self.push_history(&completed_line);
558                return Ok(Some(completed_line));
559            }
560
561            // Redraw optimization
562            if code >= 0x20 && code < 1000 && old_pos == old_len && editor.cursor_pos == editor.buffer.len() {
563                write!(writer, "{}", char::from_u32(code).unwrap())?;
564                writer.flush()?;
565            } else if code == 1004 && old_pos > editor.cursor_pos && old_pos > 0 { // Left
566                write!(writer, "\x1b[D")?;
567                writer.flush()?;
568            } else if code == 1003 && old_pos < editor.cursor_pos && old_pos < old_len { // Right
569                write!(writer, "\x1b[C")?;
570                writer.flush()?;
571            } else {
572                Self::redraw_line(writer, prompt, &editor.buffer, editor.cursor_pos)?;
573            }
574        }
575    }
576
577    /// Redraw the current line (clear and rewrite).
578    fn redraw_line<W: Write>(
579        writer: &mut W,
580        prompt: &str,
581        line: &str,
582        cursor_pos: usize,
583    ) -> io::Result<()> {
584        write!(writer, "\r\x1b[K{}{}", prompt, line)?;
585        let total_len = prompt.len() + line.len();
586        let target = prompt.len() + cursor_pos;
587        if target < total_len {
588            write!(writer, "\x1b[{}D", total_len - target)?;
589        }
590        writer.flush()
591    }
592}
593
594// ---------------------------------------------------------------------------
595// Tests
596// ---------------------------------------------------------------------------
597
598#[cfg(test)]
599mod tests {
600    use super::*;
601    use std::io::Cursor;
602
603    /// Helper: build a byte sequence from a list of key inputs.
604    fn keys(parts: &[&[u8]]) -> Cursor<Vec<u8>> {
605        let mut buf = Vec::new();
606        for part in parts {
607            buf.extend_from_slice(part);
608        }
609        Cursor::new(buf)
610    }
611
612    const UP: &[u8] = b"\x1b[A";
613    const DOWN: &[u8] = b"\x1b[B";
614    const ENTER: &[u8] = b"\r";
615
616    #[test]
617    fn test_line_editor_basic() {
618        let mut editor = LineEditor::new(0);
619        let history = vec![];
620
621        assert!(editor.input_char('a' as u32, &history).is_none());
622        assert!(editor.input_char('b' as u32, &history).is_none());
623        assert_eq!(editor.buffer, "ab");
624        assert_eq!(editor.cursor_pos, 2);
625
626        assert!(editor.input_char(1004, &history).is_none()); // Left
627        assert_eq!(editor.cursor_pos, 1);
628
629        assert!(editor.input_char('c' as u32, &history).is_none());
630        assert_eq!(editor.buffer, "acb");
631        assert_eq!(editor.cursor_pos, 2);
632
633        assert!(editor.input_char(127, &history).is_none()); // Backspace
634        assert_eq!(editor.buffer, "ab");
635        assert_eq!(editor.cursor_pos, 1);
636
637        let result = editor.input_char(13, &history); // Enter
638        assert_eq!(result, Some("ab".to_string()));
639    }
640
641    #[test]
642    fn test_line_editor_history() {
643        let history = vec!["first".to_string(), "second".to_string()];
644        let mut editor = LineEditor::new(history.len());
645
646        editor.input_char(1001, &history); // Up
647        assert_eq!(editor.buffer, "second");
648
649        editor.input_char(1001, &history); // Up
650        assert_eq!(editor.buffer, "first");
651
652        editor.input_char(1002, &history); // Down
653        assert_eq!(editor.buffer, "second");
654
655        editor.input_char(1002, &history); // Down
656        assert_eq!(editor.buffer, ""); // Back to current
657    }
658
659    #[test]
660    fn test_simple_input() {
661        let mut reader = LineReader::new(100);
662        let mut input = keys(&[b"hello", ENTER]);
663        let mut out = Vec::new();
664        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
665        assert_eq!(result, Some("hello".to_string()));
666    }
667
668    #[test]
669    fn test_eof_on_empty() {
670        let mut reader = LineReader::new(100);
671        let mut input = Cursor::new(vec![4u8]); // Ctrl-D
672        let mut out = Vec::new();
673        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
674        assert_eq!(result, None);
675    }
676
677    #[test]
678    fn test_history_up_arrow() {
679        let mut reader = LineReader::new(100);
680        let mut out = Vec::new();
681
682        // First command
683        let mut input = keys(&[b"echo hello", ENTER]);
684        reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
685
686        // Second command: press Up then Enter (should recall "echo hello")
687        let mut input = keys(&[UP, ENTER]);
688        out.clear();
689        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
690        assert_eq!(result, Some("echo hello".to_string()));
691    }
692
693    #[test]
694    fn test_history_up_down_arrow() {
695        let mut reader = LineReader::new(100);
696        let mut out = Vec::new();
697
698        // Enter two commands
699        let mut input = keys(&[b"first", ENTER]);
700        reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
701        let mut input = keys(&[b"second", ENTER]);
702        reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
703
704        // Up twice => "first", Down once => "second", Enter
705        let mut input = keys(&[UP, UP, DOWN, ENTER]);
706        out.clear();
707        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
708        assert_eq!(result, Some("second".to_string()));
709    }
710
711    #[test]
712    fn test_history_down_restores_current_input() {
713        let mut reader = LineReader::new(100);
714        let mut out = Vec::new();
715
716        // Enter a command into history
717        let mut input = keys(&[b"old", ENTER]);
718        reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
719
720        // Type "new", press Up (recalls "old"), press Down (restores "new"), Enter
721        let mut input = keys(&[b"new", UP, DOWN, ENTER]);
722        out.clear();
723        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
724        assert_eq!(result, Some("new".to_string()));
725    }
726
727    #[test]
728    fn test_history_dedup() {
729        let mut reader = LineReader::new(100);
730        let mut out = Vec::new();
731
732        // Enter same command twice
733        let mut input = keys(&[b"dup", ENTER]);
734        reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
735        let mut input = keys(&[b"dup", ENTER]);
736        reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
737
738        // Up should recall "dup", another Up should NOT go further
739        // (only one entry in history)
740        let mut input = keys(&[UP, UP, ENTER]);
741        out.clear();
742        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
743        assert_eq!(result, Some("dup".to_string()));
744    }
745
746    #[test]
747    fn test_history_max_size() {
748        let mut reader = LineReader::new(3);
749        let mut out = Vec::new();
750
751        for cmd in &["aaa", "bbb", "ccc", "ddd"] {
752            let mut input = keys(&[cmd.as_bytes(), ENTER]);
753            reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
754        }
755
756        // Up 3 times should stop at "bbb" (oldest "aaa" was evicted)
757        let mut input = keys(&[UP, UP, UP, ENTER]);
758        out.clear();
759        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
760        assert_eq!(result, Some("bbb".to_string()));
761    }
762
763    #[test]
764    fn test_backspace() {
765        let mut reader = LineReader::new(100);
766        let mut input = keys(&[b"helloo", &[127], ENTER]);
767        let mut out = Vec::new();
768        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
769        assert_eq!(result, Some("hello".to_string()));
770    }
771
772    #[test]
773    fn test_ctrl_u_clears_line() {
774        let mut reader = LineReader::new(100);
775        let mut input = keys(&[b"garbage", &[21], b"clean", ENTER]); // Ctrl-U = 21
776        let mut out = Vec::new();
777        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
778        assert_eq!(result, Some("clean".to_string()));
779    }
780
781    #[test]
782    fn test_empty_line_not_in_history() {
783        let mut reader = LineReader::new(100);
784        let mut out = Vec::new();
785
786        // Enter a real command
787        let mut input = keys(&[b"real", ENTER]);
788        reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
789
790        // Enter an empty line
791        let mut input = keys(&[ENTER]);
792        reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
793
794        // Up should still recall "real", not empty
795        let mut input = keys(&[UP, ENTER]);
796        out.clear();
797        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
798        assert_eq!(result, Some("real".to_string()));
799    }
800
801    // ── LineHandler / run_loop tests ─────────────────────────────────────
802
803    #[test]
804    fn test_run_loop_with_handler() {
805        use std::sync::{Arc, Mutex};
806
807        let executed = Arc::new(Mutex::new(Vec::new()));
808        let exec_clone = Arc::clone(&executed);
809
810        let handler = move |line: &str| -> Result<LoopAction, String> {
811            exec_clone.lock().unwrap().push(line.to_string());
812            Ok(LoopAction::Continue)
813        };
814
815        let mut reader = LineReader::new(100);
816        // Type two commands then Ctrl-D
817        let mut input = keys(&[b"echo hello", ENTER, b"ls", ENTER, &[4]]);
818        let mut out = Vec::new();
819        reader.run_loop_from(&mut input, &mut out, "$ ", &handler, None).unwrap();
820
821        let cmds = executed.lock().unwrap();
822        assert_eq!(cmds.len(), 2);
823        assert_eq!(cmds[0], "echo hello");
824        assert_eq!(cmds[1], "ls");
825    }
826
827    #[test]
828    fn test_run_loop_break_on_exit() {
829        let handler = |line: &str| -> Result<LoopAction, String> {
830            if line == "exit" {
831                Ok(LoopAction::Break)
832            } else {
833                Ok(LoopAction::Continue)
834            }
835        };
836
837        let mut reader = LineReader::new(100);
838        let mut input = keys(&[b"cmd1", ENTER, b"exit", ENTER, b"cmd2", ENTER]);
839        let mut out = Vec::new();
840        reader.run_loop_from(&mut input, &mut out, "$ ", &handler, None).unwrap();
841        // Loop should have stopped after "exit"; "cmd2" is never processed.
842    }
843
844    #[test]
845    fn test_run_loop_error_continues() {
846        use std::sync::{Arc, Mutex};
847
848        let count = Arc::new(Mutex::new(0u32));
849        let count_clone = Arc::clone(&count);
850
851        let handler = move |line: &str| -> Result<LoopAction, String> {
852            *count_clone.lock().unwrap() += 1;
853            if line == "fail" {
854                Err("simulated error".to_string())
855            } else {
856                Ok(LoopAction::Continue)
857            }
858        };
859
860        let mut reader = LineReader::new(100);
861        let mut input = keys(&[b"ok", ENTER, b"fail", ENTER, b"ok2", ENTER, &[4]]);
862        let mut out = Vec::new();
863        reader.run_loop_from(&mut input, &mut out, "$ ", &handler, None).unwrap();
864
865        // All three commands should have been processed (error doesn't stop loop)
866        assert_eq!(*count.lock().unwrap(), 3);
867    }
868
869    #[test]
870    fn test_run_loop_with_history_navigation() {
871        use std::sync::{Arc, Mutex};
872
873        let executed = Arc::new(Mutex::new(Vec::new()));
874        let exec_clone = Arc::clone(&executed);
875
876        let handler = move |line: &str| -> Result<LoopAction, String> {
877            exec_clone.lock().unwrap().push(line.to_string());
878            Ok(LoopAction::Continue)
879        };
880
881        let mut reader = LineReader::new(100);
882        // Enter "echo hello", then press Up+Enter to replay it
883        let mut input = keys(&[
884            b"echo hello", ENTER,
885            UP, ENTER,  // replay from history
886            &[4],       // EOF
887        ]);
888        let mut out = Vec::new();
889        reader.run_loop_from(&mut input, &mut out, "$ ", &handler, None).unwrap();
890
891        let cmds = executed.lock().unwrap();
892        assert_eq!(cmds.len(), 2);
893        assert_eq!(cmds[0], "echo hello");
894        assert_eq!(cmds[1], "echo hello"); // replayed from history
895    }
896
897    #[test]
898    fn test_run_loop_with_handle_parallel() {
899        use std::sync::{Arc, Mutex};
900        use crate::{CommandRegistry, handle_parallel, ArcVecWriter};
901
902        let registry = Arc::new(CommandRegistry::with_builtins());
903        let output = Arc::new(Mutex::new(Vec::<u8>::new()));
904
905        let reg = Arc::clone(&registry);
906        let out_ref = Arc::clone(&output);
907
908        let handler = move |line: &str| -> Result<LoopAction, String> {
909            if line == "exit" {
910                return Ok(LoopAction::Break);
911            }
912            let results = handle_parallel(
913                vec![line.to_string()],
914                Box::new(std::io::empty()),
915                Box::new(ArcVecWriter { inner: Arc::clone(&out_ref) }),
916                Arc::clone(&reg),
917                wasibox_core::CancellationToken::new(),
918            );
919            for res in results {
920                res?;
921            }
922            Ok(LoopAction::Continue)
923        };
924
925        let mut reader = LineReader::new(100);
926        // Run "echo hello", then Up+Enter to replay, then "exit"
927        let mut input = keys(&[
928            b"echo hello", ENTER,
929            UP, ENTER,  // replay "echo hello" via history
930            b"exit", ENTER,
931        ]);
932        let mut term_out = Vec::new();
933        reader.run_loop_from(&mut input, &mut term_out, "$ ", &handler, None).unwrap();
934
935        let buf = output.lock().unwrap();
936        let result = String::from_utf8_lossy(&buf);
937        let lines: Vec<&str> = result.trim().lines().collect();
938        assert_eq!(lines.len(), 2);
939        assert_eq!(lines[0], "hello");
940        assert_eq!(lines[1], "hello"); // replayed from history
941    }
942}