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 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]) {
193 let key = match code {
194 1 => KeyEvent::CtrlA,
196 3 => KeyEvent::CtrlC,
197 4 => KeyEvent::CtrlD,
198 5 => KeyEvent::CtrlE,
199 8 | 127 => KeyEvent::Backspace,
200 11 => KeyEvent::CtrlK,
201 13 | 10 => KeyEvent::Enter,
202 21 => KeyEvent::CtrlU,
203 23 => KeyEvent::CtrlW,
204 27 => return, 1001 => KeyEvent::Up,
208 1002 => KeyEvent::Down,
209 1003 => KeyEvent::Right,
210 1004 => KeyEvent::Left,
211 1005 => KeyEvent::Home,
212 1006 => KeyEvent::End,
213 1007 => KeyEvent::Delete,
214
215 c if c >= 0x20 => KeyEvent::Char(char::from_u32(c).unwrap_or(' ')),
217 _ => return,
218 };
219
220 self.apply_key(key, history);
221 }
222
223 fn apply_key(&mut self, key: KeyEvent, history: &[String]) {
224 match key {
225 KeyEvent::Char(ch) => {
226 self.buffer.insert(self.cursor_pos, ch);
227 self.cursor_pos += 1;
228 }
229 KeyEvent::Backspace => {
230 if self.cursor_pos > 0 {
231 self.cursor_pos -= 1;
232 self.buffer.remove(self.cursor_pos);
233 }
234 }
235 KeyEvent::Delete => {
236 if self.cursor_pos < self.buffer.len() {
237 self.buffer.remove(self.cursor_pos);
238 }
239 }
240 KeyEvent::Left => {
241 if self.cursor_pos > 0 {
242 self.cursor_pos -= 1;
243 }
244 }
245 KeyEvent::Right => {
246 if self.cursor_pos < self.buffer.len() {
247 self.cursor_pos += 1;
248 }
249 }
250 KeyEvent::Home | KeyEvent::CtrlA => {
251 self.cursor_pos = 0;
252 }
253 KeyEvent::End | KeyEvent::CtrlE => {
254 self.cursor_pos = self.buffer.len();
255 }
256 KeyEvent::Up => {
257 if !history.is_empty() && self.history_idx > 0 {
258 if self.history_idx == history.len() {
259 self.saved_input = self.buffer.clone();
260 }
261 self.history_idx -= 1;
262 self.buffer = history[self.history_idx].clone();
263 self.cursor_pos = self.buffer.len();
264 }
265 }
266 KeyEvent::Down => {
267 if self.history_idx < history.len() {
268 self.history_idx += 1;
269 if self.history_idx == history.len() {
270 self.buffer = self.saved_input.clone();
271 } else {
272 self.buffer = history[self.history_idx].clone();
273 }
274 self.cursor_pos = self.buffer.len();
275 }
276 }
277 KeyEvent::CtrlU => {
278 self.buffer.clear();
279 self.cursor_pos = 0;
280 }
281 KeyEvent::CtrlK => {
282 self.buffer.truncate(self.cursor_pos);
283 }
284 KeyEvent::CtrlW => {
285 if self.cursor_pos > 0 {
286 let mut new_pos = self.cursor_pos;
287 while new_pos > 0 && self.buffer.as_bytes().get(new_pos - 1) == Some(&b' ') {
288 new_pos -= 1;
289 }
290 while new_pos > 0 && self.buffer.as_bytes().get(new_pos - 1) != Some(&b' ') {
291 new_pos -= 1;
292 }
293 self.buffer.drain(new_pos..self.cursor_pos);
294 self.cursor_pos = new_pos;
295 }
296 }
297 _ => {}
298 }
299 }
300}
301
302pub struct LineReader {
318 history: Vec<String>,
319 max_history: usize,
320}
321
322impl LineReader {
323 pub fn new(max_history: usize) -> Self {
325 Self {
326 history: Vec::new(),
327 max_history,
328 }
329 }
330
331 fn push_history(&mut self, line: &str) {
333 let trimmed = line.trim();
334 if trimmed.is_empty() {
335 return;
336 }
337 if self.history.last().map(|s| s.as_str()) == Some(trimmed) {
338 return;
339 }
340 self.history.push(trimmed.to_string());
341 if self.history.len() > self.max_history {
342 self.history.remove(0);
343 }
344 }
345
346 pub fn read_line(&mut self, prompt: &str, cancel_token: Option<wasibox_core::CancellationToken>) -> io::Result<Option<String>> {
350 let mut stdout = io::stdout();
351 write!(stdout, "{}", prompt)?;
352 stdout.flush()?;
353
354 let _guard = RawModeGuard::enter()?;
355
356 let mut reader = io::stdin();
357 self.read_line_from(&mut reader, &mut stdout, prompt, cancel_token)
358 }
359
360 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>> {
362 let mut stdout = io::stdout();
363 write!(stdout, "{}", prompt)?;
364 stdout.flush()?;
365
366 let _guard = RawModeGuard::enter()?;
367
368 self.read_line_from(&mut reader, &mut stdout, prompt, cancel_token)
369 }
370
371 pub fn run_loop<P, H>(&mut self, prompt_fn: P, handler: &H, cancel_token: wasibox_core::CancellationToken) -> io::Result<()>
378 where
379 P: Fn() -> String,
380 H: LineHandler,
381 {
382 loop {
383 let prompt = prompt_fn();
384 match self.read_line(&prompt, Some(cancel_token.clone()))? {
385 None => break,
386 Some(line) => {
387 let trimmed = line.trim();
388 if trimmed.is_empty() {
389 continue;
390 }
391 match handler.handle_line(trimmed) {
392 Ok(LoopAction::Continue) => {}
393 Ok(LoopAction::Break) => break,
394 Err(e) => {
395 eprintln!("{}", e);
396 }
397 }
398 }
399 }
400 }
401 Ok(())
402 }
403
404 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<()>
406 where
407 P: Fn() -> String,
408 H: LineHandler,
409 {
410 loop {
411 let prompt = prompt_fn();
412 let _guard = RawModeGuard::enter()?;
413 match self.read_line_from(&mut reader, &mut io::stdout(), &prompt, Some(cancel_token.clone()))? {
414 None => break,
415 Some(line) => {
416 let trimmed = line.trim();
417 if trimmed.is_empty() {
418 continue;
419 }
420 match handler.handle_line(trimmed) {
421 Ok(LoopAction::Continue) => {}
422 Ok(LoopAction::Break) => break,
423 Err(e) => {
424 eprintln!("{}", e);
425 }
426 }
427 }
428 }
429 }
430 Ok(())
431 }
432
433 #[cfg(test)]
435 fn run_loop_from<R: Read, W: Write, H: LineHandler>(
436 &mut self,
437 reader: &mut R,
438 writer: &mut W,
439 prompt: &str,
440 handler: &H,
441 cancel_token: Option<wasibox_core::CancellationToken>,
442 ) -> io::Result<()> {
443 loop {
444 write!(writer, "{}", prompt)?;
445 writer.flush()?;
446 match self.read_line_from(reader, writer, prompt, cancel_token.clone())? {
447 None => break,
448 Some(line) => {
449 let trimmed = line.trim();
450 if trimmed.is_empty() {
451 continue;
452 }
453 match handler.handle_line(trimmed) {
454 Ok(LoopAction::Continue) => {}
455 Ok(LoopAction::Break) => break,
456 Err(e) => {
457 writeln!(writer, "Error: {}", e)?;
458 }
459 }
460 }
461 }
462 }
463 Ok(())
464 }
465
466 fn read_line_from<R: Read, W: Write>(
469 &mut self,
470 reader: &mut R,
471 writer: &mut W,
472 prompt: &str,
473 cancel_token: Option<wasibox_core::CancellationToken>,
474 ) -> io::Result<Option<String>> {
475 let mut editor = LineEditor::new(self.history.len());
476
477 loop {
478 let b = {
479 let mut buf = [0u8; 1];
480 reader.read_exact(&mut buf)?;
481 buf[0]
482 };
483
484 let code = match b {
485 4 => {
487 if editor.buffer.is_empty() {
488 write!(writer, "\r\n")?;
489 writer.flush()?;
490 return Ok(None);
491 }
492 4
493 }
494 3 => {
496 if let Some(token) = &cancel_token {
497 token.cancel();
498 }
499 write!(writer, "^C\r\n")?;
500 writer.flush()?;
501 return Ok(Some(String::new()));
502 }
503 27 => {
505 let seq1 = {
506 let mut buf = [0u8; 1];
507 reader.read_exact(&mut buf)?;
508 buf[0]
509 };
510 if seq1 == b'[' {
511 let seq2 = {
512 let mut buf = [0u8; 1];
513 reader.read_exact(&mut buf)?;
514 buf[0]
515 };
516 match seq2 {
517 b'A' => 1001, b'B' => 1002, b'C' => 1003, b'D' => 1004, b'H' => 1005, b'F' => 1006, b'3' => {
524 let seq3 = {
525 let mut buf = [0u8; 1];
526 reader.read_exact(&mut buf)?;
527 buf[0]
528 };
529 if seq3 == b'~' {
530 1007 } else {
532 continue;
533 }
534 }
535 _ => continue,
536 }
537 } else {
538 continue;
539 }
540 }
541 other => other as u32,
542 };
543
544 if code == 13 || code == 10 { write!(writer, "\r\n")?;
546 writer.flush()?;
547 self.push_history(&editor.buffer);
548 return Ok(Some(editor.buffer));
549 }
550
551 let old_pos = editor.cursor_pos;
552 let old_len = editor.buffer.len();
553
554 editor.input_char(code, &self.history);
555
556 if code >= 0x20 && code < 1000 && old_pos == old_len && editor.cursor_pos == editor.buffer.len() {
558 write!(writer, "{}", char::from_u32(code).unwrap())?;
559 writer.flush()?;
560 } else if code == 1004 && old_pos > editor.cursor_pos && old_pos > 0 { write!(writer, "\x1b[D")?;
562 writer.flush()?;
563 } else if code == 1003 && old_pos < editor.cursor_pos && old_pos < old_len { write!(writer, "\x1b[C")?;
565 writer.flush()?;
566 } else {
567 Self::redraw_line(writer, prompt, &editor.buffer, editor.cursor_pos)?;
568 }
569 }
570 }
571
572 fn redraw_line<W: Write>(
574 writer: &mut W,
575 prompt: &str,
576 line: &str,
577 cursor_pos: usize,
578 ) -> io::Result<()> {
579 write!(writer, "\r\x1b[K{}{}", prompt, line)?;
580 let total_len = prompt.len() + line.len();
581 let target = prompt.len() + cursor_pos;
582 if target < total_len {
583 write!(writer, "\x1b[{}D", total_len - target)?;
584 }
585 writer.flush()
586 }
587}
588
589#[cfg(test)]
594mod tests {
595 use super::*;
596 use std::io::Cursor;
597
598 fn keys(parts: &[&[u8]]) -> Cursor<Vec<u8>> {
600 let mut buf = Vec::new();
601 for part in parts {
602 buf.extend_from_slice(part);
603 }
604 Cursor::new(buf)
605 }
606
607 const UP: &[u8] = b"\x1b[A";
608 const DOWN: &[u8] = b"\x1b[B";
609 const ENTER: &[u8] = b"\r";
610
611 #[test]
612 fn test_line_editor_basic() {
613 let mut editor = LineEditor::new(0);
614 let history = vec![];
615
616 editor.input_char('a' as u32, &history);
617 editor.input_char('b' as u32, &history);
618 assert_eq!(editor.buffer, "ab");
619 assert_eq!(editor.cursor_pos, 2);
620
621 editor.input_char(1004, &history); assert_eq!(editor.cursor_pos, 1);
623
624 editor.input_char('c' as u32, &history);
625 assert_eq!(editor.buffer, "acb");
626 assert_eq!(editor.cursor_pos, 2);
627
628 editor.input_char(127, &history); assert_eq!(editor.buffer, "ab");
630 assert_eq!(editor.cursor_pos, 1);
631 }
632
633 #[test]
634 fn test_line_editor_history() {
635 let history = vec!["first".to_string(), "second".to_string()];
636 let mut editor = LineEditor::new(history.len());
637
638 editor.input_char(1001, &history); assert_eq!(editor.buffer, "second");
640
641 editor.input_char(1001, &history); assert_eq!(editor.buffer, "first");
643
644 editor.input_char(1002, &history); assert_eq!(editor.buffer, "second");
646
647 editor.input_char(1002, &history); assert_eq!(editor.buffer, ""); }
650
651 #[test]
652 fn test_simple_input() {
653 let mut reader = LineReader::new(100);
654 let mut input = keys(&[b"hello", ENTER]);
655 let mut out = Vec::new();
656 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
657 assert_eq!(result, Some("hello".to_string()));
658 }
659
660 #[test]
661 fn test_eof_on_empty() {
662 let mut reader = LineReader::new(100);
663 let mut input = Cursor::new(vec![4u8]); let mut out = Vec::new();
665 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
666 assert_eq!(result, None);
667 }
668
669 #[test]
670 fn test_history_up_arrow() {
671 let mut reader = LineReader::new(100);
672 let mut out = Vec::new();
673
674 let mut input = keys(&[b"echo hello", ENTER]);
676 reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
677
678 let mut input = keys(&[UP, ENTER]);
680 out.clear();
681 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
682 assert_eq!(result, Some("echo hello".to_string()));
683 }
684
685 #[test]
686 fn test_history_up_down_arrow() {
687 let mut reader = LineReader::new(100);
688 let mut out = Vec::new();
689
690 let mut input = keys(&[b"first", ENTER]);
692 reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
693 let mut input = keys(&[b"second", ENTER]);
694 reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
695
696 let mut input = keys(&[UP, UP, DOWN, ENTER]);
698 out.clear();
699 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
700 assert_eq!(result, Some("second".to_string()));
701 }
702
703 #[test]
704 fn test_history_down_restores_current_input() {
705 let mut reader = LineReader::new(100);
706 let mut out = Vec::new();
707
708 let mut input = keys(&[b"old", ENTER]);
710 reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
711
712 let mut input = keys(&[b"new", UP, DOWN, ENTER]);
714 out.clear();
715 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
716 assert_eq!(result, Some("new".to_string()));
717 }
718
719 #[test]
720 fn test_history_dedup() {
721 let mut reader = LineReader::new(100);
722 let mut out = Vec::new();
723
724 let mut input = keys(&[b"dup", ENTER]);
726 reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
727 let mut input = keys(&[b"dup", ENTER]);
728 reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
729
730 let mut input = keys(&[UP, UP, ENTER]);
733 out.clear();
734 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
735 assert_eq!(result, Some("dup".to_string()));
736 }
737
738 #[test]
739 fn test_history_max_size() {
740 let mut reader = LineReader::new(3);
741 let mut out = Vec::new();
742
743 for cmd in &["aaa", "bbb", "ccc", "ddd"] {
744 let mut input = keys(&[cmd.as_bytes(), ENTER]);
745 reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
746 }
747
748 let mut input = keys(&[UP, UP, UP, ENTER]);
750 out.clear();
751 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
752 assert_eq!(result, Some("bbb".to_string()));
753 }
754
755 #[test]
756 fn test_backspace() {
757 let mut reader = LineReader::new(100);
758 let mut input = keys(&[b"helloo", &[127], ENTER]);
759 let mut out = Vec::new();
760 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
761 assert_eq!(result, Some("hello".to_string()));
762 }
763
764 #[test]
765 fn test_ctrl_u_clears_line() {
766 let mut reader = LineReader::new(100);
767 let mut input = keys(&[b"garbage", &[21], b"clean", ENTER]); let mut out = Vec::new();
769 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
770 assert_eq!(result, Some("clean".to_string()));
771 }
772
773 #[test]
774 fn test_empty_line_not_in_history() {
775 let mut reader = LineReader::new(100);
776 let mut out = Vec::new();
777
778 let mut input = keys(&[b"real", ENTER]);
780 reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
781
782 let mut input = keys(&[ENTER]);
784 reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
785
786 let mut input = keys(&[UP, ENTER]);
788 out.clear();
789 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
790 assert_eq!(result, Some("real".to_string()));
791 }
792
793 #[test]
796 fn test_run_loop_with_handler() {
797 use std::sync::{Arc, Mutex};
798
799 let executed = Arc::new(Mutex::new(Vec::new()));
800 let exec_clone = Arc::clone(&executed);
801
802 let handler = move |line: &str| -> Result<LoopAction, String> {
803 exec_clone.lock().unwrap().push(line.to_string());
804 Ok(LoopAction::Continue)
805 };
806
807 let mut reader = LineReader::new(100);
808 let mut input = keys(&[b"echo hello", ENTER, b"ls", ENTER, &[4]]);
810 let mut out = Vec::new();
811 reader.run_loop_from(&mut input, &mut out, "$ ", &handler, None).unwrap();
812
813 let cmds = executed.lock().unwrap();
814 assert_eq!(cmds.len(), 2);
815 assert_eq!(cmds[0], "echo hello");
816 assert_eq!(cmds[1], "ls");
817 }
818
819 #[test]
820 fn test_run_loop_break_on_exit() {
821 let handler = |line: &str| -> Result<LoopAction, String> {
822 if line == "exit" {
823 Ok(LoopAction::Break)
824 } else {
825 Ok(LoopAction::Continue)
826 }
827 };
828
829 let mut reader = LineReader::new(100);
830 let mut input = keys(&[b"cmd1", ENTER, b"exit", ENTER, b"cmd2", ENTER]);
831 let mut out = Vec::new();
832 reader.run_loop_from(&mut input, &mut out, "$ ", &handler, None).unwrap();
833 }
835
836 #[test]
837 fn test_run_loop_error_continues() {
838 use std::sync::{Arc, Mutex};
839
840 let count = Arc::new(Mutex::new(0u32));
841 let count_clone = Arc::clone(&count);
842
843 let handler = move |line: &str| -> Result<LoopAction, String> {
844 *count_clone.lock().unwrap() += 1;
845 if line == "fail" {
846 Err("simulated error".to_string())
847 } else {
848 Ok(LoopAction::Continue)
849 }
850 };
851
852 let mut reader = LineReader::new(100);
853 let mut input = keys(&[b"ok", ENTER, b"fail", ENTER, b"ok2", ENTER, &[4]]);
854 let mut out = Vec::new();
855 reader.run_loop_from(&mut input, &mut out, "$ ", &handler, None).unwrap();
856
857 assert_eq!(*count.lock().unwrap(), 3);
859 }
860
861 #[test]
862 fn test_run_loop_with_history_navigation() {
863 use std::sync::{Arc, Mutex};
864
865 let executed = Arc::new(Mutex::new(Vec::new()));
866 let exec_clone = Arc::clone(&executed);
867
868 let handler = move |line: &str| -> Result<LoopAction, String> {
869 exec_clone.lock().unwrap().push(line.to_string());
870 Ok(LoopAction::Continue)
871 };
872
873 let mut reader = LineReader::new(100);
874 let mut input = keys(&[
876 b"echo hello", ENTER,
877 UP, ENTER, &[4], ]);
880 let mut out = Vec::new();
881 reader.run_loop_from(&mut input, &mut out, "$ ", &handler, None).unwrap();
882
883 let cmds = executed.lock().unwrap();
884 assert_eq!(cmds.len(), 2);
885 assert_eq!(cmds[0], "echo hello");
886 assert_eq!(cmds[1], "echo hello"); }
888
889 #[test]
890 fn test_run_loop_with_handle_parallel() {
891 use std::sync::{Arc, Mutex};
892 use crate::{CommandRegistry, handle_parallel, ArcVecWriter};
893
894 let registry = Arc::new(CommandRegistry::with_builtins());
895 let output = Arc::new(Mutex::new(Vec::<u8>::new()));
896
897 let reg = Arc::clone(®istry);
898 let out_ref = Arc::clone(&output);
899
900 let handler = move |line: &str| -> Result<LoopAction, String> {
901 if line == "exit" {
902 return Ok(LoopAction::Break);
903 }
904 let results = handle_parallel(
905 vec![line.to_string()],
906 Box::new(std::io::empty()),
907 Box::new(ArcVecWriter { inner: Arc::clone(&out_ref) }),
908 Arc::clone(®),
909 wasibox_core::CancellationToken::new(),
910 );
911 for res in results {
912 res?;
913 }
914 Ok(LoopAction::Continue)
915 };
916
917 let mut reader = LineReader::new(100);
918 let mut input = keys(&[
920 b"echo hello", ENTER,
921 UP, ENTER, b"exit", ENTER,
923 ]);
924 let mut term_out = Vec::new();
925 reader.run_loop_from(&mut input, &mut term_out, "$ ", &handler, None).unwrap();
926
927 let buf = output.lock().unwrap();
928 let result = String::from_utf8_lossy(&buf);
929 let lines: Vec<&str> = result.trim().lines().collect();
930 assert_eq!(lines.len(), 2);
931 assert_eq!(lines[0], "hello");
932 assert_eq!(lines[1], "hello"); }
934}