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