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