Skip to main content

wasi_shell/
readline.rs

1use std::io::{self, Read, Write};
2use unicode_segmentation::UnicodeSegmentation;
3use unicode_width::UnicodeWidthStr;
4
5const KEY_UP: u32 = 0x110001;
6const KEY_DOWN: u32 = 0x110002;
7const KEY_RIGHT: u32 = 0x110003;
8const KEY_LEFT: u32 = 0x110004;
9const KEY_HOME: u32 = 0x110005;
10const KEY_END: u32 = 0x110006;
11const KEY_DELETE: u32 = 0x110007;
12
13// ---------------------------------------------------------------------------
14// Platform-specific raw terminal mode
15// ---------------------------------------------------------------------------
16
17#[cfg(unix)]
18struct RawModeGuard {
19    original: libc::termios,
20}
21
22#[cfg(unix)]
23impl RawModeGuard {
24    fn enter() -> io::Result<Self> {
25        let mut original: libc::termios = unsafe { std::mem::zeroed() };
26        if unsafe { libc::tcgetattr(libc::STDIN_FILENO, &mut original) } != 0 {
27            return Err(io::Error::last_os_error());
28        }
29        let mut raw = original;
30        raw.c_lflag &= !(libc::ICANON | libc::ECHO | libc::ISIG);
31        raw.c_cc[libc::VMIN] = 1;
32        raw.c_cc[libc::VTIME] = 0;
33        if unsafe { libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, &raw) } != 0 {
34            return Err(io::Error::last_os_error());
35        }
36        Ok(Self { original })
37    }
38}
39
40#[cfg(unix)]
41impl Drop for RawModeGuard {
42    fn drop(&mut self) {
43        unsafe {
44            libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, &self.original);
45        }
46    }
47}
48
49// ---- Windows ----
50
51#[cfg(windows)]
52mod win32 {
53    #[link(name = "kernel32")]
54    unsafe extern "system" {
55        pub fn GetStdHandle(nStdHandle: u32) -> isize;
56        pub fn GetConsoleMode(hConsoleHandle: isize, lpMode: *mut u32) -> i32;
57        pub fn SetConsoleMode(hConsoleHandle: isize, dwMode: u32) -> i32;
58    }
59
60    pub const STD_INPUT_HANDLE: u32 = 0xFFFF_FFF6; // (DWORD)-10
61    pub const ENABLE_PROCESSED_INPUT: u32 = 0x0001;
62    pub const ENABLE_LINE_INPUT: u32 = 0x0002;
63    pub const ENABLE_ECHO_INPUT: u32 = 0x0004;
64    pub const ENABLE_VIRTUAL_TERMINAL_INPUT: u32 = 0x0200;
65}
66
67#[cfg(windows)]
68struct RawModeGuard {
69    handle: isize,
70    original_mode: u32,
71}
72
73#[cfg(windows)]
74impl RawModeGuard {
75    fn enter() -> io::Result<Self> {
76        let handle = unsafe { win32::GetStdHandle(win32::STD_INPUT_HANDLE) };
77        if handle == -1 {
78            return Err(io::Error::last_os_error());
79        }
80        let mut original_mode: u32 = 0;
81        if unsafe { win32::GetConsoleMode(handle, &mut original_mode) } == 0 {
82            return Err(io::Error::last_os_error());
83        }
84        let new_mode = (original_mode
85            & !(win32::ENABLE_LINE_INPUT
86                | win32::ENABLE_ECHO_INPUT
87                | win32::ENABLE_PROCESSED_INPUT))
88            | win32::ENABLE_VIRTUAL_TERMINAL_INPUT;
89        if unsafe { win32::SetConsoleMode(handle, new_mode) } == 0 {
90            return Err(io::Error::last_os_error());
91        }
92        Ok(Self {
93            handle,
94            original_mode,
95        })
96    }
97}
98
99#[cfg(windows)]
100impl Drop for RawModeGuard {
101    fn drop(&mut self) {
102        unsafe {
103            win32::SetConsoleMode(self.handle, self.original_mode);
104        }
105    }
106}
107
108// ---- WASI ----
109
110#[cfg(target_os = "wasi")]
111struct RawModeGuard;
112
113#[cfg(target_os = "wasi")]
114impl RawModeGuard {
115    fn enter() -> io::Result<Self> {
116        Ok(Self)
117    }
118}
119
120// ---------------------------------------------------------------------------
121// LineHandler trait
122// ---------------------------------------------------------------------------
123
124/// Trait for handling lines read by [`LineReader`].
125///
126/// Implement this to inject command-execution logic into the REPL loop.
127///
128/// # Return values
129///
130/// - `Ok(LoopAction::Continue)` — prompt for the next line.
131/// - `Ok(LoopAction::Break)` — exit the loop normally.
132/// - `Err(msg)` — print the error and continue.
133pub trait LineHandler {
134    fn handle_line(&self, line: &str) -> Result<LoopAction, String>;
135}
136
137/// Controls the REPL loop flow after a line is handled.
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum LoopAction {
140    /// Continue reading the next line.
141    Continue,
142    /// Exit the REPL loop.
143    Break,
144}
145
146// Blanket impl: closures that return Result<LoopAction, String>
147impl<F> LineHandler for F
148where
149    F: Fn(&str) -> Result<LoopAction, String>,
150{
151    fn handle_line(&self, line: &str) -> Result<LoopAction, String> {
152        self(line)
153    }
154}
155
156// ---------------------------------------------------------------------------
157// KeyEvent, KeyEventHandler, and LineBuffer
158// ---------------------------------------------------------------------------
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum KeyEvent {
162    Char(char),
163    Enter,
164    Backspace,
165    Delete,
166    Up,
167    Down,
168    Left,
169    Right,
170    Home,
171    End,
172    CtrlA,
173    CtrlE,
174    CtrlU,
175    CtrlK,
176    CtrlW,
177    CtrlD,
178    CtrlC,
179    Esc,
180}
181
182pub trait KeyEventHandler {
183    fn on_key_event(&mut self, key: KeyEvent);
184}
185
186struct NoopKeyEventHandler;
187
188impl KeyEventHandler for NoopKeyEventHandler {
189    fn on_key_event(&mut self, _key: KeyEvent) {}
190}
191
192fn previous_grapheme_boundary(s: &str, cursor: usize) -> usize {
193    s[..cursor]
194        .grapheme_indices(true)
195        .map(|(idx, _)| idx)
196        .last()
197        .unwrap_or(0)
198}
199
200fn next_grapheme_boundary(s: &str, cursor: usize) -> usize {
201    if cursor >= s.len() {
202        return s.len();
203    }
204
205    let mut iter = s[cursor..].grapheme_indices(true);
206    iter.next();
207    iter.next().map(|(idx, _)| cursor + idx).unwrap_or(s.len())
208}
209
210/// A stateful editor for a single line of text buffer.
211pub struct LineBuffer {
212    pub buffer: String,
213    pub cursor_pos: usize,
214}
215
216impl LineBuffer {
217    /// Create a new `LineBuffer` initialized for a new line.
218    pub const fn new() -> Self {
219        Self {
220            buffer: String::new(),
221            cursor_pos: 0,
222        }
223    }
224
225    pub fn set_buffer(&mut self, text: String) {
226        self.buffer = text;
227        self.cursor_pos = self.buffer.len();
228    }
229
230    pub fn apply_key(&mut self, key: KeyEvent) {
231        match key {
232            KeyEvent::Char(ch) => {
233                self.buffer.insert(self.cursor_pos, ch);
234                self.cursor_pos += ch.len_utf8();
235            }
236            KeyEvent::Backspace => {
237                if self.cursor_pos > 0 {
238                    let start = previous_grapheme_boundary(&self.buffer, self.cursor_pos);
239                    self.buffer.drain(start..self.cursor_pos);
240                    self.cursor_pos = start;
241                }
242            }
243            KeyEvent::Delete => {
244                if self.cursor_pos < self.buffer.len() {
245                    let end = next_grapheme_boundary(&self.buffer, self.cursor_pos);
246                    self.buffer.drain(self.cursor_pos..end);
247                }
248            }
249            KeyEvent::Left => {
250                if self.cursor_pos > 0 {
251                    self.cursor_pos = previous_grapheme_boundary(&self.buffer, self.cursor_pos);
252                }
253            }
254            KeyEvent::Right => {
255                if self.cursor_pos < self.buffer.len() {
256                    self.cursor_pos = next_grapheme_boundary(&self.buffer, self.cursor_pos);
257                }
258            }
259            KeyEvent::Home | KeyEvent::CtrlA => {
260                self.cursor_pos = 0;
261            }
262            KeyEvent::End | KeyEvent::CtrlE => {
263                self.cursor_pos = self.buffer.len();
264            }
265            KeyEvent::CtrlU => {
266                self.buffer.clear();
267                self.cursor_pos = 0;
268            }
269            KeyEvent::CtrlK => {
270                self.buffer.truncate(self.cursor_pos);
271            }
272            KeyEvent::CtrlW => {
273                if self.cursor_pos > 0 {
274                    let before_cursor = &self.buffer[..self.cursor_pos];
275                    let mut new_pos = 0;
276                    let mut seen_word = false;
277                    for (idx, grapheme) in before_cursor.grapheme_indices(true).rev() {
278                        let is_space = grapheme.chars().all(char::is_whitespace);
279                        if !seen_word && is_space {
280                            continue;
281                        }
282                        if is_space {
283                            new_pos = idx + grapheme.len();
284                            break;
285                        }
286                        seen_word = true;
287                        new_pos = idx;
288                    }
289                    self.buffer.drain(new_pos..self.cursor_pos);
290                    self.cursor_pos = new_pos;
291                }
292            }
293            KeyEvent::Esc => {}
294            _ => {}
295        }
296    }
297}
298
299// ---------------------------------------------------------------------------
300// History
301// ---------------------------------------------------------------------------
302
303pub trait History {
304    fn push(&mut self, line: &str);
305    fn len(&self) -> usize;
306    fn get(&self, index: usize) -> Option<String>;
307    fn is_empty(&self) -> bool {
308        self.len() == 0
309    }
310}
311
312pub struct InMemoryHistory {
313    entries: Vec<String>,
314    max_len: usize,
315}
316
317impl InMemoryHistory {
318    pub const fn new(max_len: usize) -> Self {
319        Self {
320            entries: Vec::new(),
321            max_len,
322        }
323    }
324}
325
326impl History for InMemoryHistory {
327    fn push(&mut self, line: &str) {
328        let trimmed = line.trim();
329        if trimmed.is_empty() {
330            return;
331        }
332        if self.entries.last().map(|s| s.as_str()) == Some(trimmed) {
333            return;
334        }
335        self.entries.push(trimmed.to_string());
336        if self.entries.len() > self.max_len {
337            self.entries.remove(0);
338        }
339    }
340
341    fn len(&self) -> usize {
342        self.entries.len()
343    }
344
345    fn get(&self, index: usize) -> Option<String> {
346        self.entries.get(index).cloned()
347    }
348}
349
350// ---------------------------------------------------------------------------
351// LineEditor
352// ---------------------------------------------------------------------------
353
354/// A minimal line editor with command history.
355///
356/// Supports:
357/// - Up/Down arrow keys to navigate command history
358/// - Left/Right arrow keys to move the cursor within the line
359/// - Home/End to jump to the beginning/end of the line
360/// - Delete to remove the character under the cursor
361/// - Backspace to remove the character before the cursor
362/// - Ctrl-U to clear the line, Ctrl-K to kill to end of line
363/// - Ctrl-W to delete the previous word
364/// - Ctrl-A / Ctrl-E for Home / End
365pub struct LineEditor<H: History = InMemoryHistory> {
366    line_buffer: LineBuffer,
367    history: H,
368    history_idx: usize,
369    saved_input: String,
370    esc_buf: smallvec::SmallVec<[u8; 4]>,
371}
372
373impl LineEditor<InMemoryHistory> {
374    /// Create a new `LineEditor` with the given maximum history size.
375    pub const fn new(max_history: usize) -> Self {
376        Self::with_history_and_len(InMemoryHistory::new(max_history), 0)
377    }
378}
379
380impl<H: History> LineEditor<H> {
381    pub fn with_history(history: H) -> Self {
382        let history_idx = history.len();
383        Self {
384            line_buffer: LineBuffer::new(),
385            history,
386            history_idx,
387            saved_input: String::new(),
388            esc_buf: smallvec::SmallVec::new_const(),
389        }
390    }
391
392    pub const fn with_history_and_len(history: H, len: usize) -> Self {
393        Self {
394            line_buffer: LineBuffer::new(),
395            history,
396            history_idx: len,
397            saved_input: String::new(),
398            esc_buf: smallvec::SmallVec::new_const(),
399        }
400    }
401
402    pub fn buffer(&self) -> &str {
403        &self.line_buffer.buffer
404    }
405
406    pub fn cursor_pos(&self) -> usize {
407        self.line_buffer.cursor_pos
408    }
409
410    /// Prepares the internal state for a new line of input.
411    pub fn start_new_line(&mut self) {
412        self.line_buffer.buffer.clear();
413        self.line_buffer.cursor_pos = 0;
414        self.history_idx = self.history.len();
415        self.saved_input.clear();
416        self.esc_buf.clear();
417    }
418
419    pub fn input_char(&mut self, code: u32) -> Option<String> {
420        self.input_char_with_handler(code, &mut NoopKeyEventHandler)
421    }
422
423    pub fn input_char_with_handler<K: KeyEventHandler>(
424        &mut self,
425        code: u32,
426        handler: &mut K,
427    ) -> Option<String> {
428        // 1. Handle escape sequence state machine
429        if code == 27 {
430            self.esc_buf.clear();
431            self.esc_buf.push(27);
432            return None;
433        }
434
435        if !self.esc_buf.is_empty() {
436            self.esc_buf.push(code as u8);
437            let seq = self.esc_buf.as_slice();
438
439            let key = match seq {
440                [27, b'[', b'A'] => Some(KeyEvent::Up),
441                [27, b'[', b'B'] => Some(KeyEvent::Down),
442                [27, b'[', b'C'] => Some(KeyEvent::Right),
443                [27, b'[', b'D'] => Some(KeyEvent::Left),
444                [27, b'[', b'H'] => Some(KeyEvent::Home),
445                [27, b'[', b'F'] => Some(KeyEvent::End),
446                [27, b'[', b'3', b'~'] => Some(KeyEvent::Delete),
447                _ => {
448                    // Check if it's still potentially a valid prefix
449                    if seq.len() >= 4 || (seq.len() == 2 && seq[1] != b'[') {
450                        // Invalid or unsupported sequence
451                        self.esc_buf.clear();
452                        None
453                    } else {
454                        // Keep waiting for more bytes
455                        return None;
456                    }
457                }
458            };
459
460            if let Some(k) = key {
461                self.esc_buf.clear();
462                return self.handle_key_event(k, handler);
463            }
464            // If the sequence was invalid, we fall through to process the current 'code'
465        }
466
467        // 2. Map single code to KeyEvent
468        let key = match code {
469            // Control characters
470            1 => KeyEvent::CtrlA,
471            3 => KeyEvent::CtrlC,
472            4 => KeyEvent::CtrlD,
473            5 => KeyEvent::CtrlE,
474            8 | 127 => KeyEvent::Backspace,
475            11 => KeyEvent::CtrlK,
476            13 | 10 => KeyEvent::Enter,
477            21 => KeyEvent::CtrlU,
478            23 => KeyEvent::CtrlW,
479            27 => KeyEvent::Esc,
480
481            // Custom codes for special keys (defined by the caller/LineEditor)
482            KEY_UP => KeyEvent::Up,
483            KEY_DOWN => KeyEvent::Down,
484            KEY_RIGHT => KeyEvent::Right,
485            KEY_LEFT => KeyEvent::Left,
486            KEY_HOME => KeyEvent::Home,
487            KEY_END => KeyEvent::End,
488            KEY_DELETE => KeyEvent::Delete,
489
490            // Printable characters
491            c if c >= 0x20 => match char::from_u32(c) {
492                Some(ch) if !ch.is_control() => KeyEvent::Char(ch),
493                _ => return None,
494            },
495            _ => return None,
496        };
497
498        self.handle_key_event(key, handler)
499    }
500
501    fn handle_key_event<K: KeyEventHandler>(
502        &mut self,
503        key: KeyEvent,
504        handler: &mut K,
505    ) -> Option<String> {
506        handler.on_key_event(key);
507
508        if key == KeyEvent::Enter {
509            let final_line = self.line_buffer.buffer.clone();
510            self.history.push(&final_line);
511            self.start_new_line();
512            return Some(final_line);
513        }
514
515        match key {
516            KeyEvent::Up => {
517                if !self.history.is_empty() && self.history_idx > 0 {
518                    if self.history_idx == self.history.len() {
519                        self.saved_input = self.line_buffer.buffer.clone();
520                    }
521                    self.history_idx -= 1;
522                    if let Some(hist_line) = self.history.get(self.history_idx) {
523                        self.line_buffer.set_buffer(hist_line);
524                    }
525                }
526            }
527            KeyEvent::Down => {
528                if self.history_idx < self.history.len() {
529                    self.history_idx += 1;
530                    if self.history_idx == self.history.len() {
531                        self.line_buffer.set_buffer(self.saved_input.clone());
532                    } else if let Some(hist_line) = self.history.get(self.history_idx) {
533                        self.line_buffer.set_buffer(hist_line);
534                    }
535                }
536            }
537            _ => {
538                self.line_buffer.apply_key(key);
539            }
540        }
541
542        None
543    }
544
545    /// Read a line interactively with arrow-key history navigation.
546    ///
547    /// Returns `Ok(Some(line))` on success, `Ok(None)` on EOF (Ctrl-D).
548    pub fn read_line(
549        &mut self,
550        prompt: &str,
551        cancel_token: Option<wasibox_core::CancellationToken>,
552    ) -> io::Result<Option<String>> {
553        let mut stdout = io::stdout();
554        write!(stdout, "{}", prompt)?;
555        stdout.flush()?;
556
557        let _guard = RawModeGuard::enter()?;
558
559        let mut reader = io::stdin();
560        self.read_line_from(&mut reader, &mut stdout, prompt, cancel_token)
561    }
562
563    /// Read a line interactively using a provided reader.
564    pub fn read_line_with_stdin(
565        &mut self,
566        prompt: &str,
567        cancel_token: Option<wasibox_core::CancellationToken>,
568        mut reader: Box<dyn Read>,
569    ) -> io::Result<Option<String>> {
570        let mut stdout = io::stdout();
571        write!(stdout, "{}", prompt)?;
572        stdout.flush()?;
573
574        let _guard = RawModeGuard::enter()?;
575
576        self.read_line_from(&mut reader, &mut stdout, prompt, cancel_token)
577    }
578
579    /// Run an interactive REPL loop, delegating each line to `handler`.
580    ///
581    /// The loop ends when:
582    /// - The handler returns `Ok(LoopAction::Break)`
583    /// - EOF is reached (Ctrl-D)
584    /// - An I/O error occurs
585    pub fn run_loop<P, L>(
586        &mut self,
587        prompt_fn: P,
588        handler: &L,
589        cancel_token: wasibox_core::CancellationToken,
590    ) -> io::Result<()>
591    where
592        P: Fn() -> String,
593        L: LineHandler,
594    {
595        loop {
596            let prompt = prompt_fn();
597            match self.read_line(&prompt, Some(cancel_token.clone()))? {
598                None => break,
599                Some(line) => {
600                    let trimmed = line.trim();
601                    if trimmed.is_empty() {
602                        continue;
603                    }
604                    match handler.handle_line(trimmed) {
605                        Ok(LoopAction::Continue) => {}
606                        Ok(LoopAction::Break) => break,
607                        Err(e) => {
608                            eprintln!("{}", e);
609                        }
610                    }
611                }
612            }
613        }
614        Ok(())
615    }
616
617    /// Run an interactive REPL loop using a provided reader.
618    pub fn run_loop_with_stdin<P, L>(
619        &mut self,
620        prompt_fn: P,
621        handler: &L,
622        cancel_token: wasibox_core::CancellationToken,
623        mut reader: Box<dyn Read>,
624    ) -> io::Result<()>
625    where
626        P: Fn() -> String,
627        L: LineHandler,
628    {
629        loop {
630            let prompt = prompt_fn();
631            let _guard = RawModeGuard::enter()?;
632            match self.read_line_from(
633                &mut reader,
634                &mut io::stdout(),
635                &prompt,
636                Some(cancel_token.clone()),
637            )? {
638                None => break,
639                Some(line) => {
640                    let trimmed = line.trim();
641                    if trimmed.is_empty() {
642                        continue;
643                    }
644                    match handler.handle_line(trimmed) {
645                        Ok(LoopAction::Continue) => {}
646                        Ok(LoopAction::Break) => break,
647                        Err(e) => {
648                            eprintln!("{}", e);
649                        }
650                    }
651                }
652            }
653        }
654        Ok(())
655    }
656
657    /// Testable REPL loop that reads from `reader` and writes to `writer`.
658    #[cfg(test)]
659    fn run_loop_from<R: Read, W: Write, L: LineHandler>(
660        &mut self,
661        reader: &mut R,
662        writer: &mut W,
663        prompt: &str,
664        handler: &L,
665        cancel_token: Option<wasibox_core::CancellationToken>,
666    ) -> io::Result<()> {
667        loop {
668            write!(writer, "{}", prompt)?;
669            writer.flush()?;
670            match self.read_line_from(reader, writer, prompt, cancel_token.clone())? {
671                None => break,
672                Some(line) => {
673                    let trimmed = line.trim();
674                    if trimmed.is_empty() {
675                        continue;
676                    }
677                    match handler.handle_line(trimmed) {
678                        Ok(LoopAction::Continue) => {}
679                        Ok(LoopAction::Break) => break,
680                        Err(e) => {
681                            writeln!(writer, "Error: {}", e)?;
682                        }
683                    }
684                }
685            }
686        }
687        Ok(())
688    }
689
690    /// Core line-editing logic, reading bytes from `reader` and writing to `writer`.
691    /// Separated from `read_line` so it can be tested with synthetic input.
692    pub fn read_line_from<R: Read, W: Write>(
693        &mut self,
694        reader: &mut R,
695        writer: &mut W,
696        prompt: &str,
697        cancel_token: Option<wasibox_core::CancellationToken>,
698    ) -> io::Result<Option<String>> {
699        self.start_new_line();
700
701        loop {
702            let b = {
703                let mut buf = [0u8; 1];
704                reader.read_exact(&mut buf)?;
705                buf[0]
706            };
707
708            let code = match b {
709                // Ctrl-D on empty line => EOF
710                4 => {
711                    if self.buffer().is_empty() {
712                        write!(writer, "\r\n")?;
713                        writer.flush()?;
714                        return Ok(None);
715                    }
716                    4
717                }
718                // Ctrl-C => discard line
719                3 => {
720                    if let Some(token) = &cancel_token {
721                        token.cancel();
722                    }
723                    write!(writer, "^C\r\n")?;
724                    writer.flush()?;
725                    self.start_new_line(); // Reset on ctrl-c
726                    return Ok(Some(String::new()));
727                }
728                // ESC => start of escape sequence
729                27 => {
730                    let seq1 = {
731                        let mut buf = [0u8; 1];
732                        reader.read_exact(&mut buf)?;
733                        buf[0]
734                    };
735                    if seq1 == b'[' {
736                        let seq2 = {
737                            let mut buf = [0u8; 1];
738                            reader.read_exact(&mut buf)?;
739                            buf[0]
740                        };
741                        match seq2 {
742                            b'A' => KEY_UP,    // Up
743                            b'B' => KEY_DOWN,  // Down
744                            b'C' => KEY_RIGHT, // Right
745                            b'D' => KEY_LEFT,  // Left
746                            b'H' => KEY_HOME,  // Home
747                            b'F' => KEY_END,   // End
748                            b'3' => {
749                                let seq3 = {
750                                    let mut buf = [0u8; 1];
751                                    reader.read_exact(&mut buf)?;
752                                    buf[0]
753                                };
754                                if seq3 == b'~' {
755                                    KEY_DELETE // Delete
756                                } else {
757                                    continue;
758                                }
759                            }
760                            _ => continue,
761                        }
762                    } else {
763                        continue;
764                    }
765                }
766                other if other < 0x80 => other as u32,
767                other if other >= 0xC0 && other <= 0xF7 => {
768                    let mut bytes = vec![other];
769                    let expected = if other >= 0xF0 {
770                        4
771                    } else if other >= 0xE0 {
772                        3
773                    } else {
774                        2
775                    };
776                    while bytes.len() < expected {
777                        let mut cb = [0u8; 1];
778                        match reader.read_exact(&mut cb) {
779                            Ok(_) => bytes.push(cb[0]),
780                            Err(_) => return Ok(None),
781                        }
782                    }
783                    match std::str::from_utf8(&bytes) {
784                        Ok(s) => s.chars().next().map(|c| c as u32).unwrap_or(0xFFFD),
785                        Err(_) => 0xFFFD,
786                    }
787                }
788                _ => 0xFFFD,
789            };
790
791            let old_pos = self.cursor_pos();
792            let old_len = self.buffer().len();
793
794            if let Some(completed_line) = self.input_char(code) {
795                write!(writer, "\r\n")?;
796                writer.flush()?;
797                return Ok(Some(completed_line));
798            }
799
800            // Redraw optimization
801            if char::from_u32(code).is_some_and(|ch| !ch.is_control())
802                && old_pos == old_len
803                && self.cursor_pos() == self.buffer().len()
804            {
805                write!(writer, "{}", char::from_u32(code).unwrap())?;
806                writer.flush()?;
807            } else if code == KEY_LEFT && old_pos > self.cursor_pos() && old_pos > 0 {
808                // Left
809                let crossed = &self.buffer()[self.cursor_pos()..old_pos];
810                write!(writer, "\x1b[{}D", UnicodeWidthStr::width(crossed))?;
811                writer.flush()?;
812            } else if code == KEY_RIGHT && old_pos < self.cursor_pos() && old_pos < old_len {
813                // Right
814                let crossed = &self.buffer()[old_pos..self.cursor_pos()];
815                write!(writer, "\x1b[{}C", UnicodeWidthStr::width(crossed))?;
816                writer.flush()?;
817            } else {
818                Self::redraw_line(writer, prompt, self.buffer(), self.cursor_pos())?;
819            }
820        }
821    }
822
823    /// Redraw the current line (clear and rewrite).
824    fn redraw_line<W: Write>(
825        writer: &mut W,
826        prompt: &str,
827        line: &str,
828        cursor_pos: usize,
829    ) -> io::Result<()> {
830        write!(writer, "\r\x1b[K{}{}", prompt, line)?;
831        let total_width = UnicodeWidthStr::width(prompt) + UnicodeWidthStr::width(line);
832        let target_width =
833            UnicodeWidthStr::width(prompt) + UnicodeWidthStr::width(&line[..cursor_pos]);
834        if target_width < total_width {
835            write!(writer, "\x1b[{}D", total_width - target_width)?;
836        }
837        writer.flush()
838    }
839}
840
841// ---------------------------------------------------------------------------
842// Tests
843// ---------------------------------------------------------------------------
844
845#[cfg(test)]
846mod tests {
847    use super::*;
848    use std::io::Cursor;
849
850    /// Helper: build a byte sequence from a list of key inputs.
851    fn keys(parts: &[&[u8]]) -> Cursor<Vec<u8>> {
852        let mut buf = Vec::new();
853        for part in parts {
854            buf.extend_from_slice(part);
855        }
856        Cursor::new(buf)
857    }
858
859    const UP: &[u8] = b"\x1b[A";
860    const DOWN: &[u8] = b"\x1b[B";
861    const ENTER: &[u8] = b"\r";
862
863    #[test]
864    fn test_line_editor_basic() {
865        let mut editor = LineEditor::new(0);
866
867        assert!(editor.input_char('a' as u32).is_none());
868        assert!(editor.input_char('b' as u32).is_none());
869        assert_eq!(editor.buffer(), "ab");
870        assert_eq!(editor.cursor_pos(), 2);
871
872        assert!(editor.input_char(KEY_LEFT).is_none()); // Left
873        assert_eq!(editor.cursor_pos(), 1);
874
875        assert!(editor.input_char('c' as u32).is_none());
876        assert_eq!(editor.buffer(), "acb");
877        assert_eq!(editor.cursor_pos(), 2);
878
879        assert!(editor.input_char(127).is_none()); // Backspace
880        assert_eq!(editor.buffer(), "ab");
881        assert_eq!(editor.cursor_pos(), 1);
882
883        let result = editor.input_char(13); // Enter
884        assert_eq!(result, Some("ab".to_string()));
885    }
886
887    #[test]
888    fn test_line_editor_history() {
889        let mut editor = LineEditor::new(10);
890        editor.input_char('f' as u32);
891        editor.input_char('i' as u32);
892        editor.input_char('r' as u32);
893        editor.input_char('s' as u32);
894        editor.input_char('t' as u32);
895        editor.input_char(13); // Enter saves "first"
896
897        editor.input_char('s' as u32);
898        editor.input_char('e' as u32);
899        editor.input_char('c' as u32);
900        editor.input_char('o' as u32);
901        editor.input_char('n' as u32);
902        editor.input_char('d' as u32);
903        editor.input_char(13); // Enter saves "second"
904
905        editor.input_char(KEY_UP); // Up
906        assert_eq!(editor.buffer(), "second");
907
908        editor.input_char(KEY_UP); // Up
909        assert_eq!(editor.buffer(), "first");
910
911        editor.input_char(KEY_DOWN); // Down
912        assert_eq!(editor.buffer(), "second");
913
914        editor.input_char(KEY_DOWN); // Down
915        assert_eq!(editor.buffer(), ""); // Back to current
916    }
917
918    #[test]
919    fn test_simple_input() {
920        let mut reader = LineEditor::new(100);
921        let mut input = keys(&[b"hello", ENTER]);
922        let mut out = Vec::new();
923        let result = reader
924            .read_line_from(&mut input, &mut out, "$ ", None)
925            .unwrap();
926        assert_eq!(result, Some("hello".to_string()));
927    }
928
929    #[test]
930    fn test_eof_on_empty() {
931        let mut reader = LineEditor::new(100);
932        let mut input = Cursor::new(vec![4u8]); // Ctrl-D
933        let mut out = Vec::new();
934        let result = reader
935            .read_line_from(&mut input, &mut out, "$ ", None)
936            .unwrap();
937        assert_eq!(result, None);
938    }
939
940    #[test]
941    fn test_history_up_arrow() {
942        let mut reader = LineEditor::new(100);
943        let mut out = Vec::new();
944
945        // First command
946        let mut input = keys(&[b"echo hello", ENTER]);
947        reader
948            .read_line_from(&mut input, &mut out, "$ ", None)
949            .unwrap();
950
951        // Second command: press Up then Enter (should recall "echo hello")
952        let mut input = keys(&[UP, ENTER]);
953        out.clear();
954        let result = reader
955            .read_line_from(&mut input, &mut out, "$ ", None)
956            .unwrap();
957        assert_eq!(result, Some("echo hello".to_string()));
958    }
959
960    #[test]
961    fn test_history_up_down_arrow() {
962        let mut reader = LineEditor::new(100);
963        let mut out = Vec::new();
964
965        // Enter two commands
966        let mut input = keys(&[b"first", ENTER]);
967        reader
968            .read_line_from(&mut input, &mut out, "$ ", None)
969            .unwrap();
970        let mut input = keys(&[b"second", ENTER]);
971        reader
972            .read_line_from(&mut input, &mut out, "$ ", None)
973            .unwrap();
974
975        // Up twice => "first", Down once => "second", Enter
976        let mut input = keys(&[UP, UP, DOWN, ENTER]);
977        out.clear();
978        let result = reader
979            .read_line_from(&mut input, &mut out, "$ ", None)
980            .unwrap();
981        assert_eq!(result, Some("second".to_string()));
982    }
983
984    #[test]
985    fn test_input_char_sequence() {
986        let mut editor = LineEditor::new(10);
987        editor.input_char('f' as u32);
988        editor.input_char('i' as u32);
989        editor.input_char('r' as u32);
990        editor.input_char('s' as u32);
991        editor.input_char('t' as u32);
992        editor.input_char(13); // Enter saves "first"
993
994        editor.input_char('s' as u32);
995        editor.input_char('e' as u32);
996        editor.input_char('c' as u32);
997        editor.input_char('o' as u32);
998        editor.input_char('n' as u32);
999        editor.input_char('d' as u32);
1000        editor.input_char(13); // Enter saves "second"
1001
1002        // Send Down arrow as [27, 91, 66]
1003        // History: ["first", "second"], idx starts at 2
1004        // Press Up twice to get to "first"
1005        editor.input_char(KEY_UP); // Up -> "second"
1006        editor.input_char(KEY_UP); // Up -> "first"
1007        assert_eq!(editor.buffer(), "first");
1008
1009        // Now Down via sequence
1010        editor.input_char(27); // ESC
1011        editor.input_char(91); // '['
1012        editor.input_char(66); // 'B' -> Down
1013        assert_eq!(editor.buffer(), "second");
1014    }
1015
1016    #[test]
1017    fn test_history_down_restores_current_input() {
1018        let mut reader = LineEditor::new(100);
1019        let mut out = Vec::new();
1020
1021        // Enter a command into history
1022        let mut input = keys(&[b"old", ENTER]);
1023        reader
1024            .read_line_from(&mut input, &mut out, "$ ", None)
1025            .unwrap();
1026
1027        // Type "new", press Up (recalls "old"), press Down (restores "new"), Enter
1028        let mut input = keys(&[b"new", UP, DOWN, ENTER]);
1029        out.clear();
1030        let result = reader
1031            .read_line_from(&mut input, &mut out, "$ ", None)
1032            .unwrap();
1033        assert_eq!(result, Some("new".to_string()));
1034    }
1035
1036    #[test]
1037    fn test_history_dedup() {
1038        let mut reader = LineEditor::new(100);
1039        let mut out = Vec::new();
1040
1041        // Enter same command twice
1042        let mut input = keys(&[b"dup", ENTER]);
1043        reader
1044            .read_line_from(&mut input, &mut out, "$ ", None)
1045            .unwrap();
1046        let mut input = keys(&[b"dup", ENTER]);
1047        reader
1048            .read_line_from(&mut input, &mut out, "$ ", None)
1049            .unwrap();
1050
1051        // Up should recall "dup", another Up should NOT go further
1052        // (only one entry in history)
1053        let mut input = keys(&[UP, UP, ENTER]);
1054        out.clear();
1055        let result = reader
1056            .read_line_from(&mut input, &mut out, "$ ", None)
1057            .unwrap();
1058        assert_eq!(result, Some("dup".to_string()));
1059    }
1060
1061    #[test]
1062    fn test_history_max_size() {
1063        let mut reader = LineEditor::new(3);
1064        let mut out = Vec::new();
1065
1066        for cmd in &["aaa", "bbb", "ccc", "ddd"] {
1067            let mut input = keys(&[cmd.as_bytes(), ENTER]);
1068            reader
1069                .read_line_from(&mut input, &mut out, "$ ", None)
1070                .unwrap();
1071        }
1072
1073        // Up 3 times should stop at "bbb" (oldest "aaa" was evicted)
1074        let mut input = keys(&[UP, UP, UP, ENTER]);
1075        out.clear();
1076        let result = reader
1077            .read_line_from(&mut input, &mut out, "$ ", None)
1078            .unwrap();
1079        assert_eq!(result, Some("bbb".to_string()));
1080    }
1081
1082    #[test]
1083    fn test_backspace() {
1084        let mut reader = LineEditor::new(100);
1085        let mut input = keys(&[b"helloo", &[127], ENTER]);
1086        let mut out = Vec::new();
1087        let result = reader
1088            .read_line_from(&mut input, &mut out, "$ ", None)
1089            .unwrap();
1090        assert_eq!(result, Some("hello".to_string()));
1091    }
1092
1093    #[test]
1094    fn test_ctrl_u_clears_line() {
1095        let mut reader = LineEditor::new(100);
1096        let mut input = keys(&[b"garbage", &[21], b"clean", ENTER]); // Ctrl-U = 21
1097        let mut out = Vec::new();
1098        let result = reader
1099            .read_line_from(&mut input, &mut out, "$ ", None)
1100            .unwrap();
1101        assert_eq!(result, Some("clean".to_string()));
1102    }
1103
1104    #[test]
1105    fn test_empty_line_not_in_history() {
1106        let mut reader = LineEditor::new(100);
1107        let mut out = Vec::new();
1108
1109        // Enter a real command
1110        let mut input = keys(&[b"real", ENTER]);
1111        reader
1112            .read_line_from(&mut input, &mut out, "$ ", None)
1113            .unwrap();
1114
1115        // Enter an empty line
1116        let mut input = keys(&[ENTER]);
1117        reader
1118            .read_line_from(&mut input, &mut out, "$ ", None)
1119            .unwrap();
1120
1121        // Up should still recall "real", not empty
1122        let mut input = keys(&[UP, ENTER]);
1123        out.clear();
1124        let result = reader
1125            .read_line_from(&mut input, &mut out, "$ ", None)
1126            .unwrap();
1127        assert_eq!(result, Some("real".to_string()));
1128    }
1129
1130    // ── LineHandler / run_loop tests ─────────────────────────────────────
1131
1132    #[test]
1133    fn test_run_loop_with_handler() {
1134        use std::sync::{Arc, Mutex};
1135
1136        let executed = Arc::new(Mutex::new(Vec::new()));
1137        let exec_clone = Arc::clone(&executed);
1138
1139        let handler = move |line: &str| -> Result<LoopAction, String> {
1140            exec_clone.lock().unwrap().push(line.to_string());
1141            Ok(LoopAction::Continue)
1142        };
1143
1144        let mut reader = LineEditor::new(100);
1145        // Type two commands then Ctrl-D
1146        let mut input = keys(&[b"echo hello", ENTER, b"ls", ENTER, &[4]]);
1147        let mut out = Vec::new();
1148        reader
1149            .run_loop_from(&mut input, &mut out, "$ ", &handler, None)
1150            .unwrap();
1151
1152        let cmds = executed.lock().unwrap();
1153        assert_eq!(cmds.len(), 2);
1154        assert_eq!(cmds[0], "echo hello");
1155        assert_eq!(cmds[1], "ls");
1156    }
1157
1158    #[test]
1159    fn test_run_loop_break_on_exit() {
1160        let handler = |line: &str| -> Result<LoopAction, String> {
1161            if line == "exit" {
1162                Ok(LoopAction::Break)
1163            } else {
1164                Ok(LoopAction::Continue)
1165            }
1166        };
1167
1168        let mut reader = LineEditor::new(100);
1169        let mut input = keys(&[b"cmd1", ENTER, b"exit", ENTER, b"cmd2", ENTER]);
1170        let mut out = Vec::new();
1171        reader
1172            .run_loop_from(&mut input, &mut out, "$ ", &handler, None)
1173            .unwrap();
1174        // Loop should have stopped after "exit"; "cmd2" is never processed.
1175    }
1176
1177    #[test]
1178    fn test_run_loop_error_continues() {
1179        use std::sync::{Arc, Mutex};
1180
1181        let count = Arc::new(Mutex::new(0u32));
1182        let count_clone = Arc::clone(&count);
1183
1184        let handler = move |line: &str| -> Result<LoopAction, String> {
1185            *count_clone.lock().unwrap() += 1;
1186            if line == "fail" {
1187                Err("simulated error".to_string())
1188            } else {
1189                Ok(LoopAction::Continue)
1190            }
1191        };
1192
1193        let mut reader = LineEditor::new(100);
1194        let mut input = keys(&[b"ok", ENTER, b"fail", ENTER, b"ok2", ENTER, &[4]]);
1195        let mut out = Vec::new();
1196        reader
1197            .run_loop_from(&mut input, &mut out, "$ ", &handler, None)
1198            .unwrap();
1199
1200        // All three commands should have been processed (error doesn't stop loop)
1201        assert_eq!(*count.lock().unwrap(), 3);
1202    }
1203
1204    #[test]
1205    fn test_run_loop_with_history_navigation() {
1206        use std::sync::{Arc, Mutex};
1207
1208        let executed = Arc::new(Mutex::new(Vec::new()));
1209        let exec_clone = Arc::clone(&executed);
1210
1211        let handler = move |line: &str| -> Result<LoopAction, String> {
1212            exec_clone.lock().unwrap().push(line.to_string());
1213            Ok(LoopAction::Continue)
1214        };
1215
1216        let mut reader = LineEditor::new(100);
1217        // Enter "echo hello", then press Up+Enter to replay it
1218        let mut input = keys(&[
1219            b"echo hello",
1220            ENTER,
1221            UP,
1222            ENTER, // replay from history
1223            &[4],  // EOF
1224        ]);
1225        let mut out = Vec::new();
1226        reader
1227            .run_loop_from(&mut input, &mut out, "$ ", &handler, None)
1228            .unwrap();
1229
1230        let cmds = executed.lock().unwrap();
1231        assert_eq!(cmds.len(), 2);
1232        assert_eq!(cmds[0], "echo hello");
1233        assert_eq!(cmds[1], "echo hello"); // replayed from history
1234    }
1235
1236    #[test]
1237    fn test_run_loop_with_handle_parallel() {
1238        use crate::{ArcVecWriter, CommandRegistry, handle_parallel};
1239        use std::sync::{Arc, Mutex};
1240
1241        let registry = Arc::new(CommandRegistry::with_builtins());
1242        let output = Arc::new(Mutex::new(Vec::<u8>::new()));
1243
1244        let reg = Arc::clone(&registry);
1245        let out_ref = Arc::clone(&output);
1246
1247        let handler = move |line: &str| -> Result<LoopAction, String> {
1248            if line == "exit" {
1249                return Ok(LoopAction::Break);
1250            }
1251            let results = handle_parallel(
1252                vec![line.to_string()],
1253                Box::new(std::io::empty()),
1254                Box::new(ArcVecWriter {
1255                    inner: Arc::clone(&out_ref),
1256                }),
1257                Arc::clone(&reg),
1258                wasibox_core::CancellationToken::new(),
1259            );
1260            for res in results {
1261                res?;
1262            }
1263            Ok(LoopAction::Continue)
1264        };
1265
1266        let mut reader = LineEditor::new(100);
1267        // Run "echo hello", then Up+Enter to replay, then "exit"
1268        let mut input = keys(&[
1269            b"echo hello",
1270            ENTER,
1271            UP,
1272            ENTER, // replay "echo hello" via history
1273            b"exit",
1274            ENTER,
1275        ]);
1276        let mut term_out = Vec::new();
1277        reader
1278            .run_loop_from(&mut input, &mut term_out, "$ ", &handler, None)
1279            .unwrap();
1280
1281        let buf = output.lock().unwrap();
1282        let result = String::from_utf8_lossy(&buf);
1283        let lines: Vec<&str> = result.trim().lines().collect();
1284        assert_eq!(lines.len(), 2);
1285        assert_eq!(lines[0], "hello");
1286        assert_eq!(lines[1], "hello"); // replayed from history
1287    }
1288
1289    #[test]
1290    fn test_key_event_handler() {
1291        struct MockHandler {
1292            events: Vec<KeyEvent>,
1293        }
1294        impl KeyEventHandler for MockHandler {
1295            fn on_key_event(&mut self, key: KeyEvent) {
1296                self.events.push(key);
1297            }
1298        }
1299
1300        let mut editor = LineEditor::new(10);
1301        let mut handler = MockHandler { events: Vec::new() };
1302
1303        editor.input_char_with_handler('a' as u32, &mut handler);
1304        editor.input_char_with_handler('b' as u32, &mut handler);
1305        editor.input_char_with_handler(13, &mut handler);
1306
1307        assert_eq!(
1308            handler.events,
1309            vec![KeyEvent::Char('a'), KeyEvent::Char('b'), KeyEvent::Enter,]
1310        );
1311    }
1312
1313    #[test]
1314    fn test_unicode_japanese_input_char() {
1315        let mut editor = LineEditor::new(10);
1316        editor.input_char('あ' as u32);
1317        assert_eq!(editor.buffer(), "あ");
1318        assert_eq!(editor.input_char(13), Some("あ".to_string()));
1319    }
1320
1321    #[test]
1322    fn test_unicode_emoji_input_char() {
1323        let mut editor = LineEditor::new(10);
1324        editor.input_char('🦀' as u32);
1325        assert_eq!(editor.buffer(), "🦀");
1326        assert_eq!(editor.input_char(13), Some("🦀".to_string()));
1327    }
1328
1329    #[test]
1330    fn test_read_line_from_decodes_utf8_input() {
1331        let mut reader = LineEditor::new(10);
1332        let mut input = Cursor::new("echo あ🦀\r".as_bytes().to_vec());
1333        let mut out = Vec::new();
1334        let result = reader
1335            .read_line_from(&mut input, &mut out, "$ ", None)
1336            .unwrap();
1337        assert_eq!(result, Some("echo あ🦀".to_string()));
1338    }
1339
1340    #[test]
1341    fn test_read_line_from_decodes_complex_emoji_input() {
1342        let family = "👨‍👩‍👧‍👦";
1343        let mut reader = LineEditor::new(10);
1344        let mut input = Cursor::new(format!("echo {family}\r").into_bytes());
1345        let mut out = Vec::new();
1346        let result = reader
1347            .read_line_from(&mut input, &mut out, "$ ", None)
1348            .unwrap();
1349        assert_eq!(result, Some(format!("echo {family}")));
1350    }
1351
1352    #[test]
1353    fn test_backspace_removes_complex_emoji_grapheme() {
1354        let family = "👨‍👩‍👧‍👦";
1355        let mut editor = LineEditor::new(10);
1356        for ch in format!("aあ🦀{family}").chars() {
1357            editor.input_char(ch as u32);
1358        }
1359
1360        editor.input_char(127);
1361        assert_eq!(editor.buffer(), "aあ🦀");
1362
1363        editor.input_char(127);
1364        assert_eq!(editor.buffer(), "aあ");
1365
1366        editor.input_char(127);
1367        assert_eq!(editor.buffer(), "a");
1368    }
1369
1370    #[test]
1371    fn test_left_right_do_not_split_graphemes() {
1372        let family = "👨‍👩‍👧‍👦";
1373        let mut editor = LineEditor::new(10);
1374        for ch in format!("a{family}b").chars() {
1375            editor.input_char(ch as u32);
1376        }
1377
1378        editor.input_char(KEY_LEFT);
1379        editor.input_char(KEY_LEFT);
1380        editor.input_char('X' as u32);
1381
1382        assert_eq!(editor.buffer(), format!("aX{family}b"));
1383    }
1384
1385    #[test]
1386    fn test_coptic_codepoint_no_longer_collides_with_special_keys() {
1387        let mut editor = LineEditor::new(10);
1388        let coptic = char::from_u32(1001).unwrap();
1389        editor.input_char(1001);
1390        assert_eq!(editor.buffer(), coptic.to_string());
1391    }
1392}