1use std::io::{self, Read, Write};
2
3#[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#[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; 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#[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
108pub trait LineHandler {
122 fn handle_line(&self, line: &str) -> Result<LoopAction, String>;
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub enum LoopAction {
128 Continue,
130 Break,
132}
133
134impl<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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub enum KeyEvent {
150 Char(char),
151 Enter,
152 Backspace,
153 Delete,
154 Up,
155 Down,
156 Left,
157 Right,
158 Home,
159 End,
160 CtrlA,
161 CtrlE,
162 CtrlU,
163 CtrlK,
164 CtrlW,
165 CtrlD,
166 CtrlC,
167}
168
169pub struct LineEditor {
171 pub buffer: String,
172 pub cursor_pos: usize,
173 history_idx: usize,
174 saved_input: String,
175}
176
177impl LineEditor {
178 pub const fn new(history_len: usize) -> Self {
180 Self {
181 buffer: String::new(),
182 cursor_pos: 0,
183 history_idx: history_len,
184 saved_input: String::new(),
185 }
186 }
187
188 pub fn input_char(&mut self, code: u32, history: &[String]) -> Option<String> {
195 let key = match code {
196 1 => KeyEvent::CtrlA,
198 3 => KeyEvent::CtrlC,
199 4 => KeyEvent::CtrlD,
200 5 => KeyEvent::CtrlE,
201 8 | 127 => KeyEvent::Backspace,
202 11 => KeyEvent::CtrlK,
203 13 | 10 => KeyEvent::Enter,
204 21 => KeyEvent::CtrlU,
205 23 => KeyEvent::CtrlW,
206 27 => return None, 1001 => KeyEvent::Up,
210 1002 => KeyEvent::Down,
211 1003 => KeyEvent::Right,
212 1004 => KeyEvent::Left,
213 1005 => KeyEvent::Home,
214 1006 => KeyEvent::End,
215 1007 => KeyEvent::Delete,
216
217 c if c >= 0x20 => KeyEvent::Char(char::from_u32(c).unwrap_or(' ')),
219 _ => return None,
220 };
221
222 if key == KeyEvent::Enter {
223 Some(self.buffer.clone())
224 } else {
225 self.apply_key(key, history);
226 None
227 }
228 }
229
230 fn apply_key(&mut self, key: KeyEvent, history: &[String]) {
231 match key {
232 KeyEvent::Char(ch) => {
233 self.buffer.insert(self.cursor_pos, ch);
234 self.cursor_pos += 1;
235 }
236 KeyEvent::Backspace => {
237 if self.cursor_pos > 0 {
238 self.cursor_pos -= 1;
239 self.buffer.remove(self.cursor_pos);
240 }
241 }
242 KeyEvent::Delete => {
243 if self.cursor_pos < self.buffer.len() {
244 self.buffer.remove(self.cursor_pos);
245 }
246 }
247 KeyEvent::Left => {
248 if self.cursor_pos > 0 {
249 self.cursor_pos -= 1;
250 }
251 }
252 KeyEvent::Right => {
253 if self.cursor_pos < self.buffer.len() {
254 self.cursor_pos += 1;
255 }
256 }
257 KeyEvent::Home | KeyEvent::CtrlA => {
258 self.cursor_pos = 0;
259 }
260 KeyEvent::End | KeyEvent::CtrlE => {
261 self.cursor_pos = self.buffer.len();
262 }
263 KeyEvent::Up => {
264 if !history.is_empty() && self.history_idx > 0 {
265 if self.history_idx == history.len() {
266 self.saved_input = self.buffer.clone();
267 }
268 self.history_idx -= 1;
269 self.buffer = history[self.history_idx].clone();
270 self.cursor_pos = self.buffer.len();
271 }
272 }
273 KeyEvent::Down => {
274 if self.history_idx < history.len() {
275 self.history_idx += 1;
276 if self.history_idx == history.len() {
277 self.buffer = self.saved_input.clone();
278 } else {
279 self.buffer = history[self.history_idx].clone();
280 }
281 self.cursor_pos = self.buffer.len();
282 }
283 }
284 KeyEvent::CtrlU => {
285 self.buffer.clear();
286 self.cursor_pos = 0;
287 }
288 KeyEvent::CtrlK => {
289 self.buffer.truncate(self.cursor_pos);
290 }
291 KeyEvent::CtrlW => {
292 if self.cursor_pos > 0 {
293 let mut new_pos = self.cursor_pos;
294 while new_pos > 0 && self.buffer.as_bytes().get(new_pos - 1) == Some(&b' ') {
295 new_pos -= 1;
296 }
297 while new_pos > 0 && self.buffer.as_bytes().get(new_pos - 1) != Some(&b' ') {
298 new_pos -= 1;
299 }
300 self.buffer.drain(new_pos..self.cursor_pos);
301 self.cursor_pos = new_pos;
302 }
303 }
304 _ => {}
305 }
306 }
307}
308
309pub struct LineReader {
325 history: Vec<String>,
326 max_history: usize,
327}
328
329impl LineReader {
330 pub fn new(max_history: usize) -> Self {
332 Self {
333 history: Vec::new(),
334 max_history,
335 }
336 }
337
338 fn push_history(&mut self, line: &str) {
340 let trimmed = line.trim();
341 if trimmed.is_empty() {
342 return;
343 }
344 if self.history.last().map(|s| s.as_str()) == Some(trimmed) {
345 return;
346 }
347 self.history.push(trimmed.to_string());
348 if self.history.len() > self.max_history {
349 self.history.remove(0);
350 }
351 }
352
353 pub fn read_line(&mut self, prompt: &str, cancel_token: Option<wasibox_core::CancellationToken>) -> io::Result<Option<String>> {
357 let mut stdout = io::stdout();
358 write!(stdout, "{}", prompt)?;
359 stdout.flush()?;
360
361 let _guard = RawModeGuard::enter()?;
362
363 let mut reader = io::stdin();
364 self.read_line_from(&mut reader, &mut stdout, prompt, cancel_token)
365 }
366
367 pub fn read_line_with_stdin(&mut self, prompt: &str, cancel_token: Option<wasibox_core::CancellationToken>, mut reader: Box<dyn Read>) -> io::Result<Option<String>> {
369 let mut stdout = io::stdout();
370 write!(stdout, "{}", prompt)?;
371 stdout.flush()?;
372
373 let _guard = RawModeGuard::enter()?;
374
375 self.read_line_from(&mut reader, &mut stdout, prompt, cancel_token)
376 }
377
378 pub fn run_loop<P, H>(&mut self, prompt_fn: P, handler: &H, cancel_token: wasibox_core::CancellationToken) -> io::Result<()>
385 where
386 P: Fn() -> String,
387 H: LineHandler,
388 {
389 loop {
390 let prompt = prompt_fn();
391 match self.read_line(&prompt, Some(cancel_token.clone()))? {
392 None => break,
393 Some(line) => {
394 let trimmed = line.trim();
395 if trimmed.is_empty() {
396 continue;
397 }
398 match handler.handle_line(trimmed) {
399 Ok(LoopAction::Continue) => {}
400 Ok(LoopAction::Break) => break,
401 Err(e) => {
402 eprintln!("{}", e);
403 }
404 }
405 }
406 }
407 }
408 Ok(())
409 }
410
411 pub fn run_loop_with_stdin<P, H>(&mut self, prompt_fn: P, handler: &H, cancel_token: wasibox_core::CancellationToken, mut reader: Box<dyn Read>) -> io::Result<()>
413 where
414 P: Fn() -> String,
415 H: LineHandler,
416 {
417 loop {
418 let prompt = prompt_fn();
419 let _guard = RawModeGuard::enter()?;
420 match self.read_line_from(&mut reader, &mut io::stdout(), &prompt, Some(cancel_token.clone()))? {
421 None => break,
422 Some(line) => {
423 let trimmed = line.trim();
424 if trimmed.is_empty() {
425 continue;
426 }
427 match handler.handle_line(trimmed) {
428 Ok(LoopAction::Continue) => {}
429 Ok(LoopAction::Break) => break,
430 Err(e) => {
431 eprintln!("{}", e);
432 }
433 }
434 }
435 }
436 }
437 Ok(())
438 }
439
440 #[cfg(test)]
442 fn run_loop_from<R: Read, W: Write, H: LineHandler>(
443 &mut self,
444 reader: &mut R,
445 writer: &mut W,
446 prompt: &str,
447 handler: &H,
448 cancel_token: Option<wasibox_core::CancellationToken>,
449 ) -> io::Result<()> {
450 loop {
451 write!(writer, "{}", prompt)?;
452 writer.flush()?;
453 match self.read_line_from(reader, writer, prompt, cancel_token.clone())? {
454 None => break,
455 Some(line) => {
456 let trimmed = line.trim();
457 if trimmed.is_empty() {
458 continue;
459 }
460 match handler.handle_line(trimmed) {
461 Ok(LoopAction::Continue) => {}
462 Ok(LoopAction::Break) => break,
463 Err(e) => {
464 writeln!(writer, "Error: {}", e)?;
465 }
466 }
467 }
468 }
469 }
470 Ok(())
471 }
472
473 fn read_line_from<R: Read, W: Write>(
476 &mut self,
477 reader: &mut R,
478 writer: &mut W,
479 prompt: &str,
480 cancel_token: Option<wasibox_core::CancellationToken>,
481 ) -> io::Result<Option<String>> {
482 let mut editor = LineEditor::new(self.history.len());
483
484 loop {
485 let b = {
486 let mut buf = [0u8; 1];
487 reader.read_exact(&mut buf)?;
488 buf[0]
489 };
490
491 let code = match b {
492 4 => {
494 if editor.buffer.is_empty() {
495 write!(writer, "\r\n")?;
496 writer.flush()?;
497 return Ok(None);
498 }
499 4
500 }
501 3 => {
503 if let Some(token) = &cancel_token {
504 token.cancel();
505 }
506 write!(writer, "^C\r\n")?;
507 writer.flush()?;
508 return Ok(Some(String::new()));
509 }
510 27 => {
512 let seq1 = {
513 let mut buf = [0u8; 1];
514 reader.read_exact(&mut buf)?;
515 buf[0]
516 };
517 if seq1 == b'[' {
518 let seq2 = {
519 let mut buf = [0u8; 1];
520 reader.read_exact(&mut buf)?;
521 buf[0]
522 };
523 match seq2 {
524 b'A' => 1001, b'B' => 1002, b'C' => 1003, b'D' => 1004, b'H' => 1005, b'F' => 1006, b'3' => {
531 let seq3 = {
532 let mut buf = [0u8; 1];
533 reader.read_exact(&mut buf)?;
534 buf[0]
535 };
536 if seq3 == b'~' {
537 1007 } else {
539 continue;
540 }
541 }
542 _ => continue,
543 }
544 } else {
545 continue;
546 }
547 }
548 other => other as u32,
549 };
550
551 let old_pos = editor.cursor_pos;
552 let old_len = editor.buffer.len();
553
554 if let Some(completed_line) = editor.input_char(code, &self.history) {
555 write!(writer, "\r\n")?;
556 writer.flush()?;
557 self.push_history(&completed_line);
558 return Ok(Some(completed_line));
559 }
560
561 if code >= 0x20 && code < 1000 && old_pos == old_len && editor.cursor_pos == editor.buffer.len() {
563 write!(writer, "{}", char::from_u32(code).unwrap())?;
564 writer.flush()?;
565 } else if code == 1004 && old_pos > editor.cursor_pos && old_pos > 0 { write!(writer, "\x1b[D")?;
567 writer.flush()?;
568 } else if code == 1003 && old_pos < editor.cursor_pos && old_pos < old_len { write!(writer, "\x1b[C")?;
570 writer.flush()?;
571 } else {
572 Self::redraw_line(writer, prompt, &editor.buffer, editor.cursor_pos)?;
573 }
574 }
575 }
576
577 fn redraw_line<W: Write>(
579 writer: &mut W,
580 prompt: &str,
581 line: &str,
582 cursor_pos: usize,
583 ) -> io::Result<()> {
584 write!(writer, "\r\x1b[K{}{}", prompt, line)?;
585 let total_len = prompt.len() + line.len();
586 let target = prompt.len() + cursor_pos;
587 if target < total_len {
588 write!(writer, "\x1b[{}D", total_len - target)?;
589 }
590 writer.flush()
591 }
592}
593
594#[cfg(test)]
599mod tests {
600 use super::*;
601 use std::io::Cursor;
602
603 fn keys(parts: &[&[u8]]) -> Cursor<Vec<u8>> {
605 let mut buf = Vec::new();
606 for part in parts {
607 buf.extend_from_slice(part);
608 }
609 Cursor::new(buf)
610 }
611
612 const UP: &[u8] = b"\x1b[A";
613 const DOWN: &[u8] = b"\x1b[B";
614 const ENTER: &[u8] = b"\r";
615
616 #[test]
617 fn test_line_editor_basic() {
618 let mut editor = LineEditor::new(0);
619 let history = vec![];
620
621 assert!(editor.input_char('a' as u32, &history).is_none());
622 assert!(editor.input_char('b' as u32, &history).is_none());
623 assert_eq!(editor.buffer, "ab");
624 assert_eq!(editor.cursor_pos, 2);
625
626 assert!(editor.input_char(1004, &history).is_none()); assert_eq!(editor.cursor_pos, 1);
628
629 assert!(editor.input_char('c' as u32, &history).is_none());
630 assert_eq!(editor.buffer, "acb");
631 assert_eq!(editor.cursor_pos, 2);
632
633 assert!(editor.input_char(127, &history).is_none()); assert_eq!(editor.buffer, "ab");
635 assert_eq!(editor.cursor_pos, 1);
636
637 let result = editor.input_char(13, &history); assert_eq!(result, Some("ab".to_string()));
639 }
640
641 #[test]
642 fn test_line_editor_history() {
643 let history = vec!["first".to_string(), "second".to_string()];
644 let mut editor = LineEditor::new(history.len());
645
646 editor.input_char(1001, &history); assert_eq!(editor.buffer, "second");
648
649 editor.input_char(1001, &history); assert_eq!(editor.buffer, "first");
651
652 editor.input_char(1002, &history); assert_eq!(editor.buffer, "second");
654
655 editor.input_char(1002, &history); assert_eq!(editor.buffer, ""); }
658
659 #[test]
660 fn test_simple_input() {
661 let mut reader = LineReader::new(100);
662 let mut input = keys(&[b"hello", ENTER]);
663 let mut out = Vec::new();
664 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
665 assert_eq!(result, Some("hello".to_string()));
666 }
667
668 #[test]
669 fn test_eof_on_empty() {
670 let mut reader = LineReader::new(100);
671 let mut input = Cursor::new(vec![4u8]); let mut out = Vec::new();
673 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
674 assert_eq!(result, None);
675 }
676
677 #[test]
678 fn test_history_up_arrow() {
679 let mut reader = LineReader::new(100);
680 let mut out = Vec::new();
681
682 let mut input = keys(&[b"echo hello", ENTER]);
684 reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
685
686 let mut input = keys(&[UP, ENTER]);
688 out.clear();
689 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
690 assert_eq!(result, Some("echo hello".to_string()));
691 }
692
693 #[test]
694 fn test_history_up_down_arrow() {
695 let mut reader = LineReader::new(100);
696 let mut out = Vec::new();
697
698 let mut input = keys(&[b"first", ENTER]);
700 reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
701 let mut input = keys(&[b"second", ENTER]);
702 reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
703
704 let mut input = keys(&[UP, UP, DOWN, ENTER]);
706 out.clear();
707 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
708 assert_eq!(result, Some("second".to_string()));
709 }
710
711 #[test]
712 fn test_history_down_restores_current_input() {
713 let mut reader = LineReader::new(100);
714 let mut out = Vec::new();
715
716 let mut input = keys(&[b"old", ENTER]);
718 reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
719
720 let mut input = keys(&[b"new", UP, DOWN, ENTER]);
722 out.clear();
723 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
724 assert_eq!(result, Some("new".to_string()));
725 }
726
727 #[test]
728 fn test_history_dedup() {
729 let mut reader = LineReader::new(100);
730 let mut out = Vec::new();
731
732 let mut input = keys(&[b"dup", ENTER]);
734 reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
735 let mut input = keys(&[b"dup", ENTER]);
736 reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
737
738 let mut input = keys(&[UP, UP, ENTER]);
741 out.clear();
742 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
743 assert_eq!(result, Some("dup".to_string()));
744 }
745
746 #[test]
747 fn test_history_max_size() {
748 let mut reader = LineReader::new(3);
749 let mut out = Vec::new();
750
751 for cmd in &["aaa", "bbb", "ccc", "ddd"] {
752 let mut input = keys(&[cmd.as_bytes(), ENTER]);
753 reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
754 }
755
756 let mut input = keys(&[UP, UP, UP, ENTER]);
758 out.clear();
759 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
760 assert_eq!(result, Some("bbb".to_string()));
761 }
762
763 #[test]
764 fn test_backspace() {
765 let mut reader = LineReader::new(100);
766 let mut input = keys(&[b"helloo", &[127], ENTER]);
767 let mut out = Vec::new();
768 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
769 assert_eq!(result, Some("hello".to_string()));
770 }
771
772 #[test]
773 fn test_ctrl_u_clears_line() {
774 let mut reader = LineReader::new(100);
775 let mut input = keys(&[b"garbage", &[21], b"clean", ENTER]); let mut out = Vec::new();
777 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
778 assert_eq!(result, Some("clean".to_string()));
779 }
780
781 #[test]
782 fn test_empty_line_not_in_history() {
783 let mut reader = LineReader::new(100);
784 let mut out = Vec::new();
785
786 let mut input = keys(&[b"real", ENTER]);
788 reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
789
790 let mut input = keys(&[ENTER]);
792 reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
793
794 let mut input = keys(&[UP, ENTER]);
796 out.clear();
797 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
798 assert_eq!(result, Some("real".to_string()));
799 }
800
801 #[test]
804 fn test_run_loop_with_handler() {
805 use std::sync::{Arc, Mutex};
806
807 let executed = Arc::new(Mutex::new(Vec::new()));
808 let exec_clone = Arc::clone(&executed);
809
810 let handler = move |line: &str| -> Result<LoopAction, String> {
811 exec_clone.lock().unwrap().push(line.to_string());
812 Ok(LoopAction::Continue)
813 };
814
815 let mut reader = LineReader::new(100);
816 let mut input = keys(&[b"echo hello", ENTER, b"ls", ENTER, &[4]]);
818 let mut out = Vec::new();
819 reader.run_loop_from(&mut input, &mut out, "$ ", &handler, None).unwrap();
820
821 let cmds = executed.lock().unwrap();
822 assert_eq!(cmds.len(), 2);
823 assert_eq!(cmds[0], "echo hello");
824 assert_eq!(cmds[1], "ls");
825 }
826
827 #[test]
828 fn test_run_loop_break_on_exit() {
829 let handler = |line: &str| -> Result<LoopAction, String> {
830 if line == "exit" {
831 Ok(LoopAction::Break)
832 } else {
833 Ok(LoopAction::Continue)
834 }
835 };
836
837 let mut reader = LineReader::new(100);
838 let mut input = keys(&[b"cmd1", ENTER, b"exit", ENTER, b"cmd2", ENTER]);
839 let mut out = Vec::new();
840 reader.run_loop_from(&mut input, &mut out, "$ ", &handler, None).unwrap();
841 }
843
844 #[test]
845 fn test_run_loop_error_continues() {
846 use std::sync::{Arc, Mutex};
847
848 let count = Arc::new(Mutex::new(0u32));
849 let count_clone = Arc::clone(&count);
850
851 let handler = move |line: &str| -> Result<LoopAction, String> {
852 *count_clone.lock().unwrap() += 1;
853 if line == "fail" {
854 Err("simulated error".to_string())
855 } else {
856 Ok(LoopAction::Continue)
857 }
858 };
859
860 let mut reader = LineReader::new(100);
861 let mut input = keys(&[b"ok", ENTER, b"fail", ENTER, b"ok2", ENTER, &[4]]);
862 let mut out = Vec::new();
863 reader.run_loop_from(&mut input, &mut out, "$ ", &handler, None).unwrap();
864
865 assert_eq!(*count.lock().unwrap(), 3);
867 }
868
869 #[test]
870 fn test_run_loop_with_history_navigation() {
871 use std::sync::{Arc, Mutex};
872
873 let executed = Arc::new(Mutex::new(Vec::new()));
874 let exec_clone = Arc::clone(&executed);
875
876 let handler = move |line: &str| -> Result<LoopAction, String> {
877 exec_clone.lock().unwrap().push(line.to_string());
878 Ok(LoopAction::Continue)
879 };
880
881 let mut reader = LineReader::new(100);
882 let mut input = keys(&[
884 b"echo hello", ENTER,
885 UP, ENTER, &[4], ]);
888 let mut out = Vec::new();
889 reader.run_loop_from(&mut input, &mut out, "$ ", &handler, None).unwrap();
890
891 let cmds = executed.lock().unwrap();
892 assert_eq!(cmds.len(), 2);
893 assert_eq!(cmds[0], "echo hello");
894 assert_eq!(cmds[1], "echo hello"); }
896
897 #[test]
898 fn test_run_loop_with_handle_parallel() {
899 use std::sync::{Arc, Mutex};
900 use crate::{CommandRegistry, handle_parallel, ArcVecWriter};
901
902 let registry = Arc::new(CommandRegistry::with_builtins());
903 let output = Arc::new(Mutex::new(Vec::<u8>::new()));
904
905 let reg = Arc::clone(®istry);
906 let out_ref = Arc::clone(&output);
907
908 let handler = move |line: &str| -> Result<LoopAction, String> {
909 if line == "exit" {
910 return Ok(LoopAction::Break);
911 }
912 let results = handle_parallel(
913 vec![line.to_string()],
914 Box::new(std::io::empty()),
915 Box::new(ArcVecWriter { inner: Arc::clone(&out_ref) }),
916 Arc::clone(®),
917 wasibox_core::CancellationToken::new(),
918 );
919 for res in results {
920 res?;
921 }
922 Ok(LoopAction::Continue)
923 };
924
925 let mut reader = LineReader::new(100);
926 let mut input = keys(&[
928 b"echo hello", ENTER,
929 UP, ENTER, b"exit", ENTER,
931 ]);
932 let mut term_out = Vec::new();
933 reader.run_loop_from(&mut input, &mut term_out, "$ ", &handler, None).unwrap();
934
935 let buf = output.lock().unwrap();
936 let result = String::from_utf8_lossy(&buf);
937 let lines: Vec<&str> = result.trim().lines().collect();
938 assert_eq!(lines.len(), 2);
939 assert_eq!(lines[0], "hello");
940 assert_eq!(lines[1], "hello"); }
942}