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);
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_LINE_INPUT: u32 = 0x0002;
52    pub const ENABLE_ECHO_INPUT: u32 = 0x0004;
53    pub const ENABLE_VIRTUAL_TERMINAL_INPUT: u32 = 0x0200;
54}
55
56#[cfg(windows)]
57struct RawModeGuard {
58    handle: isize,
59    original_mode: u32,
60}
61
62#[cfg(windows)]
63impl RawModeGuard {
64    fn enter() -> io::Result<Self> {
65        let handle = unsafe { win32::GetStdHandle(win32::STD_INPUT_HANDLE) };
66        if handle == -1 {
67            return Err(io::Error::last_os_error());
68        }
69        let mut original_mode: u32 = 0;
70        if unsafe { win32::GetConsoleMode(handle, &mut original_mode) } == 0 {
71            return Err(io::Error::last_os_error());
72        }
73        let new_mode = (original_mode
74            & !(win32::ENABLE_LINE_INPUT | win32::ENABLE_ECHO_INPUT))
75            | win32::ENABLE_VIRTUAL_TERMINAL_INPUT;
76        if unsafe { win32::SetConsoleMode(handle, new_mode) } == 0 {
77            return Err(io::Error::last_os_error());
78        }
79        Ok(Self {
80            handle,
81            original_mode,
82        })
83    }
84}
85
86#[cfg(windows)]
87impl Drop for RawModeGuard {
88    fn drop(&mut self) {
89        unsafe {
90            win32::SetConsoleMode(self.handle, self.original_mode);
91        }
92    }
93}
94
95// ---- WASI ----
96
97#[cfg(target_os = "wasi")]
98struct RawModeGuard;
99
100#[cfg(target_os = "wasi")]
101impl RawModeGuard {
102    fn enter() -> io::Result<Self> {
103        Ok(Self)
104    }
105}
106
107// ---------------------------------------------------------------------------
108// LineHandler trait
109// ---------------------------------------------------------------------------
110
111/// Trait for handling lines read by [`LineReader`].
112///
113/// Implement this to inject command-execution logic into the REPL loop.
114///
115/// # Return values
116///
117/// - `Ok(LoopAction::Continue)` — prompt for the next line.
118/// - `Ok(LoopAction::Break)` — exit the loop normally.
119/// - `Err(msg)` — print the error and continue.
120pub trait LineHandler {
121    fn handle_line(&self, line: &str) -> Result<LoopAction, String>;
122}
123
124/// Controls the REPL loop flow after a line is handled.
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub enum LoopAction {
127    /// Continue reading the next line.
128    Continue,
129    /// Exit the REPL loop.
130    Break,
131}
132
133// Blanket impl: closures that return Result<LoopAction, String>
134impl<F> LineHandler for F
135where
136    F: Fn(&str) -> Result<LoopAction, String>,
137{
138    fn handle_line(&self, line: &str) -> Result<LoopAction, String> {
139        self(line)
140    }
141}
142
143// ---------------------------------------------------------------------------
144// LineReader
145// ---------------------------------------------------------------------------
146
147/// A minimal line editor with command history.
148///
149/// Supports:
150/// - Up/Down arrow keys to navigate command history
151/// - Left/Right arrow keys to move the cursor within the line
152/// - Home/End to jump to the beginning/end of the line
153/// - Delete to remove the character under the cursor
154/// - Backspace to remove the character before the cursor
155/// - Ctrl-U to clear the line, Ctrl-K to kill to end of line
156/// - Ctrl-W to delete the previous word
157/// - Ctrl-A / Ctrl-E for Home / End
158pub struct LineReader {
159    history: Vec<String>,
160    max_history: usize,
161}
162
163impl LineReader {
164    /// Create a new `LineReader` with the given maximum history size.
165    pub fn new(max_history: usize) -> Self {
166        Self {
167            history: Vec::new(),
168            max_history,
169        }
170    }
171
172    /// Add a line to the history (skipping duplicates of the last entry and empty lines).
173    fn push_history(&mut self, line: &str) {
174        let trimmed = line.trim();
175        if trimmed.is_empty() {
176            return;
177        }
178        if self.history.last().map(|s| s.as_str()) == Some(trimmed) {
179            return;
180        }
181        self.history.push(trimmed.to_string());
182        if self.history.len() > self.max_history {
183            self.history.remove(0);
184        }
185    }
186
187    /// Read a line interactively with arrow-key history navigation.
188    ///
189    /// Returns `Ok(Some(line))` on success, `Ok(None)` on EOF (Ctrl-D).
190    pub fn read_line(&mut self, prompt: &str, cancel_token: Option<wasibox_core::CancellationToken>) -> io::Result<Option<String>> {
191        let mut stdout = io::stdout();
192        write!(stdout, "{}", prompt)?;
193        stdout.flush()?;
194
195        let _guard = RawModeGuard::enter()?;
196
197        let mut reader = io::stdin();
198        self.read_line_from(&mut reader, &mut stdout, prompt, cancel_token)
199    }
200
201    /// Run an interactive REPL loop, delegating each line to `handler`.
202    ///
203    /// The loop ends when:
204    /// - The handler returns `Ok(LoopAction::Break)`
205    /// - EOF is reached (Ctrl-D)
206    /// - An I/O error occurs
207    pub fn run_loop<P, H>(&mut self, prompt_fn: P, handler: &H, cancel_token: wasibox_core::CancellationToken) -> io::Result<()>
208    where
209        P: Fn() -> String,
210        H: LineHandler,
211    {
212        loop {
213            let prompt = prompt_fn();
214            match self.read_line(&prompt, Some(cancel_token.clone()))? {
215                None => break,
216                Some(line) => {
217                    let trimmed = line.trim();
218                    if trimmed.is_empty() {
219                        continue;
220                    }
221                    match handler.handle_line(trimmed) {
222                        Ok(LoopAction::Continue) => {}
223                        Ok(LoopAction::Break) => break,
224                        Err(e) => {
225                            eprintln!("{}", e);
226                        }
227                    }
228                }
229            }
230        }
231        Ok(())
232    }
233
234    /// Testable REPL loop that reads from `reader` and writes to `writer`.
235    #[cfg(test)]
236    fn run_loop_from<R: Read, W: Write, H: LineHandler>(
237        &mut self,
238        reader: &mut R,
239        writer: &mut W,
240        prompt: &str,
241        handler: &H,
242        cancel_token: Option<wasibox_core::CancellationToken>,
243    ) -> io::Result<()> {
244        loop {
245            write!(writer, "{}", prompt)?;
246            writer.flush()?;
247            match self.read_line_from(reader, writer, prompt, cancel_token.clone())? {
248                None => break,
249                Some(line) => {
250                    let trimmed = line.trim();
251                    if trimmed.is_empty() {
252                        continue;
253                    }
254                    match handler.handle_line(trimmed) {
255                        Ok(LoopAction::Continue) => {}
256                        Ok(LoopAction::Break) => break,
257                        Err(e) => {
258                            writeln!(writer, "Error: {}", e)?;
259                        }
260                    }
261                }
262            }
263        }
264        Ok(())
265    }
266
267    /// Core line-editing logic, reading bytes from `reader` and writing to `writer`.
268    /// Separated from `read_line` so it can be tested with synthetic input.
269    fn read_line_from<R: Read, W: Write>(
270        &mut self,
271        reader: &mut R,
272        writer: &mut W,
273        prompt: &str,
274        cancel_token: Option<wasibox_core::CancellationToken>,
275    ) -> io::Result<Option<String>> {
276        let mut line = String::new();
277        let mut cursor_pos: usize = 0;
278        let mut history_idx: usize = self.history.len();
279        let mut saved_input = String::new();
280
281        loop {
282            let b = {
283                let mut buf = [0u8; 1];
284                reader.read_exact(&mut buf)?;
285                buf[0]
286            };
287            match b {
288                // Ctrl-D on empty line => EOF
289                4 => {
290                    if line.is_empty() {
291                        write!(writer, "\r\n")?;
292                        writer.flush()?;
293                        return Ok(None);
294                    }
295                }
296                // Ctrl-C => discard line
297                3 => {
298                    if let Some(token) = &cancel_token {
299                        token.cancel();
300                    }
301                    write!(writer, "^C\r\n")?;
302                    writer.flush()?;
303                    return Ok(Some(String::new()));
304                }
305                // Enter (CR or LF)
306                b'\r' | b'\n' => {
307                    write!(writer, "\r\n")?;
308                    writer.flush()?;
309                    self.push_history(&line);
310                    return Ok(Some(line));
311                }
312                // Backspace (127 = DEL on most terminals, 8 = BS)
313                127 | 8 => {
314                    if cursor_pos > 0 {
315                        cursor_pos -= 1;
316                        line.remove(cursor_pos);
317                        Self::redraw_line(writer, prompt, &line, cursor_pos)?;
318                    }
319                }
320                // Ctrl-A => Home
321                1 => {
322                    cursor_pos = 0;
323                    Self::redraw_line(writer, prompt, &line, cursor_pos)?;
324                }
325                // Ctrl-E => End
326                5 => {
327                    cursor_pos = line.len();
328                    Self::redraw_line(writer, prompt, &line, cursor_pos)?;
329                }
330                // Ctrl-U => clear line
331                21 => {
332                    line.clear();
333                    cursor_pos = 0;
334                    Self::redraw_line(writer, prompt, &line, cursor_pos)?;
335                }
336                // Ctrl-K => kill to end of line
337                11 => {
338                    line.truncate(cursor_pos);
339                    Self::redraw_line(writer, prompt, &line, cursor_pos)?;
340                }
341                // Ctrl-W => delete word backwards
342                23 => {
343                    if cursor_pos > 0 {
344                        let mut new_pos = cursor_pos;
345                        while new_pos > 0 && line.as_bytes()[new_pos - 1] == b' ' {
346                            new_pos -= 1;
347                        }
348                        while new_pos > 0 && line.as_bytes()[new_pos - 1] != b' ' {
349                            new_pos -= 1;
350                        }
351                        line.drain(new_pos..cursor_pos);
352                        cursor_pos = new_pos;
353                        Self::redraw_line(writer, prompt, &line, cursor_pos)?;
354                    }
355                }
356                // ESC => start of escape sequence
357                27 => {
358                    let seq1 = {
359                        let mut buf = [0u8; 1];
360                        reader.read_exact(&mut buf)?;
361                        buf[0]
362                    };
363                    if seq1 == b'[' {
364                        let seq2 = {
365                            let mut buf = [0u8; 1];
366                            reader.read_exact(&mut buf)?;
367                            buf[0]
368                        };
369                        match seq2 {
370                            // Up arrow
371                            b'A' => {
372                                if !self.history.is_empty() && history_idx > 0 {
373                                    if history_idx == self.history.len() {
374                                        saved_input = line.clone();
375                                    }
376                                    history_idx -= 1;
377                                    line = self.history[history_idx].clone();
378                                    cursor_pos = line.len();
379                                    Self::redraw_line(writer, prompt, &line, cursor_pos)?;
380                                }
381                            }
382                            // Down arrow
383                            b'B' => {
384                                if history_idx < self.history.len() {
385                                    history_idx += 1;
386                                    if history_idx == self.history.len() {
387                                        line = saved_input.clone();
388                                    } else {
389                                        line = self.history[history_idx].clone();
390                                    }
391                                    cursor_pos = line.len();
392                                    Self::redraw_line(writer, prompt, &line, cursor_pos)?;
393                                }
394                            }
395                            // Right arrow
396                            b'C' => {
397                                if cursor_pos < line.len() {
398                                    cursor_pos += 1;
399                                    write!(writer, "\x1b[C")?;
400                                    writer.flush()?;
401                                }
402                            }
403                            // Left arrow
404                            b'D' => {
405                                if cursor_pos > 0 {
406                                    cursor_pos -= 1;
407                                    write!(writer, "\x1b[D")?;
408                                    writer.flush()?;
409                                }
410                            }
411                            // Home
412                            b'H' => {
413                                cursor_pos = 0;
414                                Self::redraw_line(writer, prompt, &line, cursor_pos)?;
415                            }
416                            // End
417                            b'F' => {
418                                cursor_pos = line.len();
419                                Self::redraw_line(writer, prompt, &line, cursor_pos)?;
420                            }
421                            // Delete: ESC [ 3 ~
422                            b'3' => {
423                                let seq3 = {
424                                    let mut buf = [0u8; 1];
425                                    reader.read_exact(&mut buf)?;
426                                    buf[0]
427                                };
428                                if seq3 == b'~' && cursor_pos < line.len() {
429                                    line.remove(cursor_pos);
430                                    Self::redraw_line(writer, prompt, &line, cursor_pos)?;
431                                }
432                            }
433                            _ => {
434                                // Unknown escape sequence — ignore
435                            }
436                        }
437                    }
438                    // Else: lone ESC or ESC + unknown — ignore
439                }
440                // Printable ASCII
441                b if b >= 0x20 => {
442                    line.insert(cursor_pos, b as char);
443                    cursor_pos += 1;
444                    if cursor_pos == line.len() {
445                        write!(writer, "{}", b as char)?;
446                        writer.flush()?;
447                    } else {
448                        Self::redraw_line(writer, prompt, &line, cursor_pos)?;
449                    }
450                }
451                _ => {
452                    // Ignore other control characters
453                }
454            }
455        }
456    }
457
458    /// Redraw the current line (clear and rewrite).
459    fn redraw_line<W: Write>(
460        writer: &mut W,
461        prompt: &str,
462        line: &str,
463        cursor_pos: usize,
464    ) -> io::Result<()> {
465        write!(writer, "\r\x1b[K{}{}", prompt, line)?;
466        let total_len = prompt.len() + line.len();
467        let target = prompt.len() + cursor_pos;
468        if target < total_len {
469            write!(writer, "\x1b[{}D", total_len - target)?;
470        }
471        writer.flush()
472    }
473}
474
475// ---------------------------------------------------------------------------
476// Tests
477// ---------------------------------------------------------------------------
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482    use std::io::Cursor;
483
484    /// Helper: build a byte sequence from a list of key inputs.
485    fn keys(parts: &[&[u8]]) -> Cursor<Vec<u8>> {
486        let mut buf = Vec::new();
487        for part in parts {
488            buf.extend_from_slice(part);
489        }
490        Cursor::new(buf)
491    }
492
493    const UP: &[u8] = b"\x1b[A";
494    const DOWN: &[u8] = b"\x1b[B";
495    const ENTER: &[u8] = b"\r";
496
497    #[test]
498    fn test_simple_input() {
499        let mut reader = LineReader::new(100);
500        let mut input = keys(&[b"hello", ENTER]);
501        let mut out = Vec::new();
502        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
503        assert_eq!(result, Some("hello".to_string()));
504    }
505
506    #[test]
507    fn test_eof_on_empty() {
508        let mut reader = LineReader::new(100);
509        let mut input = Cursor::new(vec![4u8]); // Ctrl-D
510        let mut out = Vec::new();
511        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
512        assert_eq!(result, None);
513    }
514
515    #[test]
516    fn test_history_up_arrow() {
517        let mut reader = LineReader::new(100);
518        let mut out = Vec::new();
519
520        // First command
521        let mut input = keys(&[b"echo hello", ENTER]);
522        reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
523
524        // Second command: press Up then Enter (should recall "echo hello")
525        let mut input = keys(&[UP, ENTER]);
526        out.clear();
527        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
528        assert_eq!(result, Some("echo hello".to_string()));
529    }
530
531    #[test]
532    fn test_history_up_down_arrow() {
533        let mut reader = LineReader::new(100);
534        let mut out = Vec::new();
535
536        // Enter two commands
537        let mut input = keys(&[b"first", ENTER]);
538        reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
539        let mut input = keys(&[b"second", ENTER]);
540        reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
541
542        // Up twice => "first", Down once => "second", Enter
543        let mut input = keys(&[UP, UP, DOWN, ENTER]);
544        out.clear();
545        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
546        assert_eq!(result, Some("second".to_string()));
547    }
548
549    #[test]
550    fn test_history_down_restores_current_input() {
551        let mut reader = LineReader::new(100);
552        let mut out = Vec::new();
553
554        // Enter a command into history
555        let mut input = keys(&[b"old", ENTER]);
556        reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
557
558        // Type "new", press Up (recalls "old"), press Down (restores "new"), Enter
559        let mut input = keys(&[b"new", UP, DOWN, ENTER]);
560        out.clear();
561        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
562        assert_eq!(result, Some("new".to_string()));
563    }
564
565    #[test]
566    fn test_history_dedup() {
567        let mut reader = LineReader::new(100);
568        let mut out = Vec::new();
569
570        // Enter same command twice
571        let mut input = keys(&[b"dup", ENTER]);
572        reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
573        let mut input = keys(&[b"dup", ENTER]);
574        reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
575
576        // Up should recall "dup", another Up should NOT go further
577        // (only one entry in history)
578        let mut input = keys(&[UP, UP, ENTER]);
579        out.clear();
580        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
581        assert_eq!(result, Some("dup".to_string()));
582    }
583
584    #[test]
585    fn test_history_max_size() {
586        let mut reader = LineReader::new(3);
587        let mut out = Vec::new();
588
589        for cmd in &["aaa", "bbb", "ccc", "ddd"] {
590            let mut input = keys(&[cmd.as_bytes(), ENTER]);
591            reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
592        }
593
594        // Up 3 times should stop at "bbb" (oldest "aaa" was evicted)
595        let mut input = keys(&[UP, UP, UP, ENTER]);
596        out.clear();
597        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
598        assert_eq!(result, Some("bbb".to_string()));
599    }
600
601    #[test]
602    fn test_backspace() {
603        let mut reader = LineReader::new(100);
604        let mut input = keys(&[b"helloo", &[127], ENTER]);
605        let mut out = Vec::new();
606        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
607        assert_eq!(result, Some("hello".to_string()));
608    }
609
610    #[test]
611    fn test_ctrl_u_clears_line() {
612        let mut reader = LineReader::new(100);
613        let mut input = keys(&[b"garbage", &[21], b"clean", ENTER]); // Ctrl-U = 21
614        let mut out = Vec::new();
615        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
616        assert_eq!(result, Some("clean".to_string()));
617    }
618
619    #[test]
620    fn test_empty_line_not_in_history() {
621        let mut reader = LineReader::new(100);
622        let mut out = Vec::new();
623
624        // Enter a real command
625        let mut input = keys(&[b"real", ENTER]);
626        reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
627
628        // Enter an empty line
629        let mut input = keys(&[ENTER]);
630        reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
631
632        // Up should still recall "real", not empty
633        let mut input = keys(&[UP, ENTER]);
634        out.clear();
635        let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
636        assert_eq!(result, Some("real".to_string()));
637    }
638
639    // ── LineHandler / run_loop tests ─────────────────────────────────────
640
641    #[test]
642    fn test_run_loop_with_handler() {
643        use std::sync::{Arc, Mutex};
644
645        let executed = Arc::new(Mutex::new(Vec::new()));
646        let exec_clone = Arc::clone(&executed);
647
648        let handler = move |line: &str| -> Result<LoopAction, String> {
649            exec_clone.lock().unwrap().push(line.to_string());
650            Ok(LoopAction::Continue)
651        };
652
653        let mut reader = LineReader::new(100);
654        // Type two commands then Ctrl-D
655        let mut input = keys(&[b"echo hello", ENTER, b"ls", ENTER, &[4]]);
656        let mut out = Vec::new();
657        reader.run_loop_from(&mut input, &mut out, "$ ", &handler, None).unwrap();
658
659        let cmds = executed.lock().unwrap();
660        assert_eq!(cmds.len(), 2);
661        assert_eq!(cmds[0], "echo hello");
662        assert_eq!(cmds[1], "ls");
663    }
664
665    #[test]
666    fn test_run_loop_break_on_exit() {
667        let handler = |line: &str| -> Result<LoopAction, String> {
668            if line == "exit" {
669                Ok(LoopAction::Break)
670            } else {
671                Ok(LoopAction::Continue)
672            }
673        };
674
675        let mut reader = LineReader::new(100);
676        let mut input = keys(&[b"cmd1", ENTER, b"exit", ENTER, b"cmd2", ENTER]);
677        let mut out = Vec::new();
678        reader.run_loop_from(&mut input, &mut out, "$ ", &handler, None).unwrap();
679        // Loop should have stopped after "exit"; "cmd2" is never processed.
680    }
681
682    #[test]
683    fn test_run_loop_error_continues() {
684        use std::sync::{Arc, Mutex};
685
686        let count = Arc::new(Mutex::new(0u32));
687        let count_clone = Arc::clone(&count);
688
689        let handler = move |line: &str| -> Result<LoopAction, String> {
690            *count_clone.lock().unwrap() += 1;
691            if line == "fail" {
692                Err("simulated error".to_string())
693            } else {
694                Ok(LoopAction::Continue)
695            }
696        };
697
698        let mut reader = LineReader::new(100);
699        let mut input = keys(&[b"ok", ENTER, b"fail", ENTER, b"ok2", ENTER, &[4]]);
700        let mut out = Vec::new();
701        reader.run_loop_from(&mut input, &mut out, "$ ", &handler, None).unwrap();
702
703        // All three commands should have been processed (error doesn't stop loop)
704        assert_eq!(*count.lock().unwrap(), 3);
705    }
706
707    #[test]
708    fn test_run_loop_with_history_navigation() {
709        use std::sync::{Arc, Mutex};
710
711        let executed = Arc::new(Mutex::new(Vec::new()));
712        let exec_clone = Arc::clone(&executed);
713
714        let handler = move |line: &str| -> Result<LoopAction, String> {
715            exec_clone.lock().unwrap().push(line.to_string());
716            Ok(LoopAction::Continue)
717        };
718
719        let mut reader = LineReader::new(100);
720        // Enter "echo hello", then press Up+Enter to replay it
721        let mut input = keys(&[
722            b"echo hello", ENTER,
723            UP, ENTER,  // replay from history
724            &[4],       // EOF
725        ]);
726        let mut out = Vec::new();
727        reader.run_loop_from(&mut input, &mut out, "$ ", &handler, None).unwrap();
728
729        let cmds = executed.lock().unwrap();
730        assert_eq!(cmds.len(), 2);
731        assert_eq!(cmds[0], "echo hello");
732        assert_eq!(cmds[1], "echo hello"); // replayed from history
733    }
734
735    #[test]
736    fn test_run_loop_with_handle_parallel() {
737        use std::sync::{Arc, Mutex};
738        use crate::{CommandRegistry, handle_parallel, ArcVecWriter};
739
740        let registry = Arc::new(CommandRegistry::with_builtins());
741        let output = Arc::new(Mutex::new(Vec::<u8>::new()));
742
743        let reg = Arc::clone(&registry);
744        let out_ref = Arc::clone(&output);
745
746        let handler = move |line: &str| -> Result<LoopAction, String> {
747            if line == "exit" {
748                return Ok(LoopAction::Break);
749            }
750            let results = handle_parallel(
751                vec![line.to_string()],
752                Box::new(std::io::empty()),
753                Box::new(ArcVecWriter { inner: Arc::clone(&out_ref) }),
754                Arc::clone(&reg),
755                wasibox_core::CancellationToken::new(),
756            );
757            for res in results {
758                res?;
759            }
760            Ok(LoopAction::Continue)
761        };
762
763        let mut reader = LineReader::new(100);
764        // Run "echo hello", then Up+Enter to replay, then "exit"
765        let mut input = keys(&[
766            b"echo hello", ENTER,
767            UP, ENTER,  // replay "echo hello" via history
768            b"exit", ENTER,
769        ]);
770        let mut term_out = Vec::new();
771        reader.run_loop_from(&mut input, &mut term_out, "$ ", &handler, None).unwrap();
772
773        let buf = output.lock().unwrap();
774        let result = String::from_utf8_lossy(&buf);
775        let lines: Vec<&str> = result.trim().lines().collect();
776        assert_eq!(lines.len(), 2);
777        assert_eq!(lines[0], "hello");
778        assert_eq!(lines[1], "hello"); // replayed from history
779    }
780}