use std::io::{self, Read, Write};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
const KEY_UP: u32 = 0x110001;
const KEY_DOWN: u32 = 0x110002;
const KEY_RIGHT: u32 = 0x110003;
const KEY_LEFT: u32 = 0x110004;
const KEY_HOME: u32 = 0x110005;
const KEY_END: u32 = 0x110006;
const KEY_DELETE: u32 = 0x110007;
#[cfg(unix)]
struct RawModeGuard {
original: libc::termios,
}
#[cfg(unix)]
impl RawModeGuard {
fn enter() -> io::Result<Self> {
let mut original: libc::termios = unsafe { std::mem::zeroed() };
if unsafe { libc::tcgetattr(libc::STDIN_FILENO, &mut original) } != 0 {
return Err(io::Error::last_os_error());
}
let mut raw = original;
raw.c_lflag &= !(libc::ICANON | libc::ECHO | libc::ISIG);
raw.c_cc[libc::VMIN] = 1;
raw.c_cc[libc::VTIME] = 0;
if unsafe { libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, &raw) } != 0 {
return Err(io::Error::last_os_error());
}
Ok(Self { original })
}
}
#[cfg(unix)]
impl Drop for RawModeGuard {
fn drop(&mut self) {
unsafe {
libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, &self.original);
}
}
}
#[cfg(windows)]
mod win32 {
#[link(name = "kernel32")]
unsafe extern "system" {
pub fn GetStdHandle(nStdHandle: u32) -> isize;
pub fn GetConsoleMode(hConsoleHandle: isize, lpMode: *mut u32) -> i32;
pub fn SetConsoleMode(hConsoleHandle: isize, dwMode: u32) -> i32;
}
pub const STD_INPUT_HANDLE: u32 = 0xFFFF_FFF6; pub const ENABLE_PROCESSED_INPUT: u32 = 0x0001;
pub const ENABLE_LINE_INPUT: u32 = 0x0002;
pub const ENABLE_ECHO_INPUT: u32 = 0x0004;
pub const ENABLE_VIRTUAL_TERMINAL_INPUT: u32 = 0x0200;
}
#[cfg(windows)]
struct RawModeGuard {
handle: isize,
original_mode: u32,
}
#[cfg(windows)]
impl RawModeGuard {
fn enter() -> io::Result<Self> {
let handle = unsafe { win32::GetStdHandle(win32::STD_INPUT_HANDLE) };
if handle == -1 {
return Err(io::Error::last_os_error());
}
let mut original_mode: u32 = 0;
if unsafe { win32::GetConsoleMode(handle, &mut original_mode) } == 0 {
return Err(io::Error::last_os_error());
}
let new_mode = (original_mode
& !(win32::ENABLE_LINE_INPUT
| win32::ENABLE_ECHO_INPUT
| win32::ENABLE_PROCESSED_INPUT))
| win32::ENABLE_VIRTUAL_TERMINAL_INPUT;
if unsafe { win32::SetConsoleMode(handle, new_mode) } == 0 {
return Err(io::Error::last_os_error());
}
Ok(Self {
handle,
original_mode,
})
}
}
#[cfg(windows)]
impl Drop for RawModeGuard {
fn drop(&mut self) {
unsafe {
win32::SetConsoleMode(self.handle, self.original_mode);
}
}
}
#[cfg(target_os = "wasi")]
struct RawModeGuard;
#[cfg(target_os = "wasi")]
impl RawModeGuard {
fn enter() -> io::Result<Self> {
Ok(Self)
}
}
pub trait LineHandler {
fn handle_line(&self, line: &str) -> Result<LoopAction, String>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoopAction {
Continue,
Break,
}
impl<F> LineHandler for F
where
F: Fn(&str) -> Result<LoopAction, String>,
{
fn handle_line(&self, line: &str) -> Result<LoopAction, String> {
self(line)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyEvent {
Char(char),
Enter,
Backspace,
Delete,
Up,
Down,
Left,
Right,
Home,
End,
CtrlA,
CtrlE,
CtrlU,
CtrlK,
CtrlW,
CtrlD,
CtrlC,
Esc,
}
pub trait KeyEventHandler {
fn on_key_event(&mut self, key: KeyEvent);
}
struct NoopKeyEventHandler;
impl KeyEventHandler for NoopKeyEventHandler {
fn on_key_event(&mut self, _key: KeyEvent) {}
}
fn previous_grapheme_boundary(s: &str, cursor: usize) -> usize {
s[..cursor]
.grapheme_indices(true)
.map(|(idx, _)| idx)
.last()
.unwrap_or(0)
}
fn next_grapheme_boundary(s: &str, cursor: usize) -> usize {
if cursor >= s.len() {
return s.len();
}
let mut iter = s[cursor..].grapheme_indices(true);
iter.next();
iter.next().map(|(idx, _)| cursor + idx).unwrap_or(s.len())
}
pub struct LineBuffer {
pub buffer: String,
pub cursor_pos: usize,
}
impl LineBuffer {
pub const fn new() -> Self {
Self {
buffer: String::new(),
cursor_pos: 0,
}
}
pub fn set_buffer(&mut self, text: String) {
self.buffer = text;
self.cursor_pos = self.buffer.len();
}
pub fn apply_key(&mut self, key: KeyEvent) {
match key {
KeyEvent::Char(ch) => {
self.buffer.insert(self.cursor_pos, ch);
self.cursor_pos += ch.len_utf8();
}
KeyEvent::Backspace => {
if self.cursor_pos > 0 {
let start = previous_grapheme_boundary(&self.buffer, self.cursor_pos);
self.buffer.drain(start..self.cursor_pos);
self.cursor_pos = start;
}
}
KeyEvent::Delete => {
if self.cursor_pos < self.buffer.len() {
let end = next_grapheme_boundary(&self.buffer, self.cursor_pos);
self.buffer.drain(self.cursor_pos..end);
}
}
KeyEvent::Left => {
if self.cursor_pos > 0 {
self.cursor_pos = previous_grapheme_boundary(&self.buffer, self.cursor_pos);
}
}
KeyEvent::Right => {
if self.cursor_pos < self.buffer.len() {
self.cursor_pos = next_grapheme_boundary(&self.buffer, self.cursor_pos);
}
}
KeyEvent::Home | KeyEvent::CtrlA => {
self.cursor_pos = 0;
}
KeyEvent::End | KeyEvent::CtrlE => {
self.cursor_pos = self.buffer.len();
}
KeyEvent::CtrlU => {
self.buffer.clear();
self.cursor_pos = 0;
}
KeyEvent::CtrlK => {
self.buffer.truncate(self.cursor_pos);
}
KeyEvent::CtrlW => {
if self.cursor_pos > 0 {
let before_cursor = &self.buffer[..self.cursor_pos];
let mut new_pos = 0;
let mut seen_word = false;
for (idx, grapheme) in before_cursor.grapheme_indices(true).rev() {
let is_space = grapheme.chars().all(char::is_whitespace);
if !seen_word && is_space {
continue;
}
if is_space {
new_pos = idx + grapheme.len();
break;
}
seen_word = true;
new_pos = idx;
}
self.buffer.drain(new_pos..self.cursor_pos);
self.cursor_pos = new_pos;
}
}
KeyEvent::Esc => {}
_ => {}
}
}
}
pub trait History {
fn push(&mut self, line: &str);
fn len(&self) -> usize;
fn get(&self, index: usize) -> Option<String>;
fn is_empty(&self) -> bool {
self.len() == 0
}
}
pub struct InMemoryHistory {
entries: Vec<String>,
max_len: usize,
}
impl InMemoryHistory {
pub const fn new(max_len: usize) -> Self {
Self {
entries: Vec::new(),
max_len,
}
}
}
impl History for InMemoryHistory {
fn push(&mut self, line: &str) {
let trimmed = line.trim();
if trimmed.is_empty() {
return;
}
if self.entries.last().map(|s| s.as_str()) == Some(trimmed) {
return;
}
self.entries.push(trimmed.to_string());
if self.entries.len() > self.max_len {
self.entries.remove(0);
}
}
fn len(&self) -> usize {
self.entries.len()
}
fn get(&self, index: usize) -> Option<String> {
self.entries.get(index).cloned()
}
}
pub struct LineEditor<H: History = InMemoryHistory> {
line_buffer: LineBuffer,
history: H,
history_idx: usize,
saved_input: String,
esc_buf: smallvec::SmallVec<[u8; 4]>,
}
impl LineEditor<InMemoryHistory> {
pub const fn new(max_history: usize) -> Self {
Self::with_history_and_len(InMemoryHistory::new(max_history), 0)
}
}
impl<H: History> LineEditor<H> {
pub fn with_history(history: H) -> Self {
let history_idx = history.len();
Self {
line_buffer: LineBuffer::new(),
history,
history_idx,
saved_input: String::new(),
esc_buf: smallvec::SmallVec::new_const(),
}
}
pub const fn with_history_and_len(history: H, len: usize) -> Self {
Self {
line_buffer: LineBuffer::new(),
history,
history_idx: len,
saved_input: String::new(),
esc_buf: smallvec::SmallVec::new_const(),
}
}
pub fn buffer(&self) -> &str {
&self.line_buffer.buffer
}
pub fn cursor_pos(&self) -> usize {
self.line_buffer.cursor_pos
}
pub fn start_new_line(&mut self) {
self.line_buffer.buffer.clear();
self.line_buffer.cursor_pos = 0;
self.history_idx = self.history.len();
self.saved_input.clear();
self.esc_buf.clear();
}
pub fn input_char(&mut self, code: u32) -> Option<String> {
self.input_char_with_handler(code, &mut NoopKeyEventHandler)
}
pub fn input_char_with_handler<K: KeyEventHandler>(
&mut self,
code: u32,
handler: &mut K,
) -> Option<String> {
if code == 27 {
self.esc_buf.clear();
self.esc_buf.push(27);
return None;
}
if !self.esc_buf.is_empty() {
self.esc_buf.push(code as u8);
let seq = self.esc_buf.as_slice();
let key = match seq {
[27, b'[', b'A'] => Some(KeyEvent::Up),
[27, b'[', b'B'] => Some(KeyEvent::Down),
[27, b'[', b'C'] => Some(KeyEvent::Right),
[27, b'[', b'D'] => Some(KeyEvent::Left),
[27, b'[', b'H'] => Some(KeyEvent::Home),
[27, b'[', b'F'] => Some(KeyEvent::End),
[27, b'[', b'3', b'~'] => Some(KeyEvent::Delete),
_ => {
if seq.len() >= 4 || (seq.len() == 2 && seq[1] != b'[') {
self.esc_buf.clear();
None
} else {
return None;
}
}
};
if let Some(k) = key {
self.esc_buf.clear();
return self.handle_key_event(k, handler);
}
}
let key = match code {
1 => KeyEvent::CtrlA,
3 => KeyEvent::CtrlC,
4 => KeyEvent::CtrlD,
5 => KeyEvent::CtrlE,
8 | 127 => KeyEvent::Backspace,
11 => KeyEvent::CtrlK,
13 | 10 => KeyEvent::Enter,
21 => KeyEvent::CtrlU,
23 => KeyEvent::CtrlW,
27 => KeyEvent::Esc,
KEY_UP => KeyEvent::Up,
KEY_DOWN => KeyEvent::Down,
KEY_RIGHT => KeyEvent::Right,
KEY_LEFT => KeyEvent::Left,
KEY_HOME => KeyEvent::Home,
KEY_END => KeyEvent::End,
KEY_DELETE => KeyEvent::Delete,
c if c >= 0x20 => match char::from_u32(c) {
Some(ch) if !ch.is_control() => KeyEvent::Char(ch),
_ => return None,
},
_ => return None,
};
self.handle_key_event(key, handler)
}
fn handle_key_event<K: KeyEventHandler>(
&mut self,
key: KeyEvent,
handler: &mut K,
) -> Option<String> {
handler.on_key_event(key);
if key == KeyEvent::Enter {
let final_line = self.line_buffer.buffer.clone();
self.history.push(&final_line);
self.start_new_line();
return Some(final_line);
}
match key {
KeyEvent::Up => {
if !self.history.is_empty() && self.history_idx > 0 {
if self.history_idx == self.history.len() {
self.saved_input = self.line_buffer.buffer.clone();
}
self.history_idx -= 1;
if let Some(hist_line) = self.history.get(self.history_idx) {
self.line_buffer.set_buffer(hist_line);
}
}
}
KeyEvent::Down => {
if self.history_idx < self.history.len() {
self.history_idx += 1;
if self.history_idx == self.history.len() {
self.line_buffer.set_buffer(self.saved_input.clone());
} else if let Some(hist_line) = self.history.get(self.history_idx) {
self.line_buffer.set_buffer(hist_line);
}
}
}
_ => {
self.line_buffer.apply_key(key);
}
}
None
}
pub fn read_line(
&mut self,
prompt: &str,
cancel_token: Option<wasibox_core::CancellationToken>,
) -> io::Result<Option<String>> {
let mut stdout = io::stdout();
write!(stdout, "{}", prompt)?;
stdout.flush()?;
let _guard = RawModeGuard::enter()?;
let mut reader = io::stdin();
self.read_line_from(&mut reader, &mut stdout, prompt, cancel_token)
}
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>> {
let mut stdout = io::stdout();
write!(stdout, "{}", prompt)?;
stdout.flush()?;
let _guard = RawModeGuard::enter()?;
self.read_line_from(&mut reader, &mut stdout, prompt, cancel_token)
}
pub fn run_loop<P, L>(
&mut self,
prompt_fn: P,
handler: &L,
cancel_token: wasibox_core::CancellationToken,
) -> io::Result<()>
where
P: Fn() -> String,
L: LineHandler,
{
loop {
let prompt = prompt_fn();
match self.read_line(&prompt, Some(cancel_token.clone()))? {
None => break,
Some(line) => {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
match handler.handle_line(trimmed) {
Ok(LoopAction::Continue) => {}
Ok(LoopAction::Break) => break,
Err(e) => {
eprintln!("{}", e);
}
}
}
}
}
Ok(())
}
pub fn run_loop_with_stdin<P, L>(
&mut self,
prompt_fn: P,
handler: &L,
cancel_token: wasibox_core::CancellationToken,
mut reader: Box<dyn Read>,
) -> io::Result<()>
where
P: Fn() -> String,
L: LineHandler,
{
loop {
let prompt = prompt_fn();
let _guard = RawModeGuard::enter()?;
match self.read_line_from(
&mut reader,
&mut io::stdout(),
&prompt,
Some(cancel_token.clone()),
)? {
None => break,
Some(line) => {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
match handler.handle_line(trimmed) {
Ok(LoopAction::Continue) => {}
Ok(LoopAction::Break) => break,
Err(e) => {
eprintln!("{}", e);
}
}
}
}
}
Ok(())
}
#[cfg(test)]
fn run_loop_from<R: Read, W: Write, L: LineHandler>(
&mut self,
reader: &mut R,
writer: &mut W,
prompt: &str,
handler: &L,
cancel_token: Option<wasibox_core::CancellationToken>,
) -> io::Result<()> {
loop {
write!(writer, "{}", prompt)?;
writer.flush()?;
match self.read_line_from(reader, writer, prompt, cancel_token.clone())? {
None => break,
Some(line) => {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
match handler.handle_line(trimmed) {
Ok(LoopAction::Continue) => {}
Ok(LoopAction::Break) => break,
Err(e) => {
writeln!(writer, "Error: {}", e)?;
}
}
}
}
}
Ok(())
}
pub fn read_line_from<R: Read, W: Write>(
&mut self,
reader: &mut R,
writer: &mut W,
prompt: &str,
cancel_token: Option<wasibox_core::CancellationToken>,
) -> io::Result<Option<String>> {
self.start_new_line();
loop {
let b = {
let mut buf = [0u8; 1];
reader.read_exact(&mut buf)?;
buf[0]
};
let code = match b {
4 => {
if self.buffer().is_empty() {
write!(writer, "\r\n")?;
writer.flush()?;
return Ok(None);
}
4
}
3 => {
if let Some(token) = &cancel_token {
token.cancel();
}
write!(writer, "^C\r\n")?;
writer.flush()?;
self.start_new_line(); return Ok(Some(String::new()));
}
27 => {
let seq1 = {
let mut buf = [0u8; 1];
reader.read_exact(&mut buf)?;
buf[0]
};
if seq1 == b'[' {
let seq2 = {
let mut buf = [0u8; 1];
reader.read_exact(&mut buf)?;
buf[0]
};
match seq2 {
b'A' => KEY_UP, b'B' => KEY_DOWN, b'C' => KEY_RIGHT, b'D' => KEY_LEFT, b'H' => KEY_HOME, b'F' => KEY_END, b'3' => {
let seq3 = {
let mut buf = [0u8; 1];
reader.read_exact(&mut buf)?;
buf[0]
};
if seq3 == b'~' {
KEY_DELETE } else {
continue;
}
}
_ => continue,
}
} else {
continue;
}
}
other if other < 0x80 => other as u32,
other if other >= 0xC0 && other <= 0xF7 => {
let mut bytes = vec![other];
let expected = if other >= 0xF0 {
4
} else if other >= 0xE0 {
3
} else {
2
};
while bytes.len() < expected {
let mut cb = [0u8; 1];
match reader.read_exact(&mut cb) {
Ok(_) => bytes.push(cb[0]),
Err(_) => return Ok(None),
}
}
match std::str::from_utf8(&bytes) {
Ok(s) => s.chars().next().map(|c| c as u32).unwrap_or(0xFFFD),
Err(_) => 0xFFFD,
}
}
_ => 0xFFFD,
};
let old_pos = self.cursor_pos();
let old_len = self.buffer().len();
if let Some(completed_line) = self.input_char(code) {
write!(writer, "\r\n")?;
writer.flush()?;
return Ok(Some(completed_line));
}
if char::from_u32(code).is_some_and(|ch| !ch.is_control())
&& old_pos == old_len
&& self.cursor_pos() == self.buffer().len()
{
write!(writer, "{}", char::from_u32(code).unwrap())?;
writer.flush()?;
} else if code == KEY_LEFT && old_pos > self.cursor_pos() && old_pos > 0 {
let crossed = &self.buffer()[self.cursor_pos()..old_pos];
write!(writer, "\x1b[{}D", UnicodeWidthStr::width(crossed))?;
writer.flush()?;
} else if code == KEY_RIGHT && old_pos < self.cursor_pos() && old_pos < old_len {
let crossed = &self.buffer()[old_pos..self.cursor_pos()];
write!(writer, "\x1b[{}C", UnicodeWidthStr::width(crossed))?;
writer.flush()?;
} else {
Self::redraw_line(writer, prompt, self.buffer(), self.cursor_pos())?;
}
}
}
fn redraw_line<W: Write>(
writer: &mut W,
prompt: &str,
line: &str,
cursor_pos: usize,
) -> io::Result<()> {
write!(writer, "\r\x1b[K{}{}", prompt, line)?;
let total_width = UnicodeWidthStr::width(prompt) + UnicodeWidthStr::width(line);
let target_width =
UnicodeWidthStr::width(prompt) + UnicodeWidthStr::width(&line[..cursor_pos]);
if target_width < total_width {
write!(writer, "\x1b[{}D", total_width - target_width)?;
}
writer.flush()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn keys(parts: &[&[u8]]) -> Cursor<Vec<u8>> {
let mut buf = Vec::new();
for part in parts {
buf.extend_from_slice(part);
}
Cursor::new(buf)
}
const UP: &[u8] = b"\x1b[A";
const DOWN: &[u8] = b"\x1b[B";
const ENTER: &[u8] = b"\r";
#[test]
fn test_line_editor_basic() {
let mut editor = LineEditor::new(0);
assert!(editor.input_char('a' as u32).is_none());
assert!(editor.input_char('b' as u32).is_none());
assert_eq!(editor.buffer(), "ab");
assert_eq!(editor.cursor_pos(), 2);
assert!(editor.input_char(KEY_LEFT).is_none()); assert_eq!(editor.cursor_pos(), 1);
assert!(editor.input_char('c' as u32).is_none());
assert_eq!(editor.buffer(), "acb");
assert_eq!(editor.cursor_pos(), 2);
assert!(editor.input_char(127).is_none()); assert_eq!(editor.buffer(), "ab");
assert_eq!(editor.cursor_pos(), 1);
let result = editor.input_char(13); assert_eq!(result, Some("ab".to_string()));
}
#[test]
fn test_line_editor_history() {
let mut editor = LineEditor::new(10);
editor.input_char('f' as u32);
editor.input_char('i' as u32);
editor.input_char('r' as u32);
editor.input_char('s' as u32);
editor.input_char('t' as u32);
editor.input_char(13);
editor.input_char('s' as u32);
editor.input_char('e' as u32);
editor.input_char('c' as u32);
editor.input_char('o' as u32);
editor.input_char('n' as u32);
editor.input_char('d' as u32);
editor.input_char(13);
editor.input_char(KEY_UP); assert_eq!(editor.buffer(), "second");
editor.input_char(KEY_UP); assert_eq!(editor.buffer(), "first");
editor.input_char(KEY_DOWN); assert_eq!(editor.buffer(), "second");
editor.input_char(KEY_DOWN); assert_eq!(editor.buffer(), ""); }
#[test]
fn test_simple_input() {
let mut reader = LineEditor::new(100);
let mut input = keys(&[b"hello", ENTER]);
let mut out = Vec::new();
let result = reader
.read_line_from(&mut input, &mut out, "$ ", None)
.unwrap();
assert_eq!(result, Some("hello".to_string()));
}
#[test]
fn test_eof_on_empty() {
let mut reader = LineEditor::new(100);
let mut input = Cursor::new(vec![4u8]); let mut out = Vec::new();
let result = reader
.read_line_from(&mut input, &mut out, "$ ", None)
.unwrap();
assert_eq!(result, None);
}
#[test]
fn test_history_up_arrow() {
let mut reader = LineEditor::new(100);
let mut out = Vec::new();
let mut input = keys(&[b"echo hello", ENTER]);
reader
.read_line_from(&mut input, &mut out, "$ ", None)
.unwrap();
let mut input = keys(&[UP, ENTER]);
out.clear();
let result = reader
.read_line_from(&mut input, &mut out, "$ ", None)
.unwrap();
assert_eq!(result, Some("echo hello".to_string()));
}
#[test]
fn test_history_up_down_arrow() {
let mut reader = LineEditor::new(100);
let mut out = Vec::new();
let mut input = keys(&[b"first", ENTER]);
reader
.read_line_from(&mut input, &mut out, "$ ", None)
.unwrap();
let mut input = keys(&[b"second", ENTER]);
reader
.read_line_from(&mut input, &mut out, "$ ", None)
.unwrap();
let mut input = keys(&[UP, UP, DOWN, ENTER]);
out.clear();
let result = reader
.read_line_from(&mut input, &mut out, "$ ", None)
.unwrap();
assert_eq!(result, Some("second".to_string()));
}
#[test]
fn test_input_char_sequence() {
let mut editor = LineEditor::new(10);
editor.input_char('f' as u32);
editor.input_char('i' as u32);
editor.input_char('r' as u32);
editor.input_char('s' as u32);
editor.input_char('t' as u32);
editor.input_char(13);
editor.input_char('s' as u32);
editor.input_char('e' as u32);
editor.input_char('c' as u32);
editor.input_char('o' as u32);
editor.input_char('n' as u32);
editor.input_char('d' as u32);
editor.input_char(13);
editor.input_char(KEY_UP); editor.input_char(KEY_UP); assert_eq!(editor.buffer(), "first");
editor.input_char(27); editor.input_char(91); editor.input_char(66); assert_eq!(editor.buffer(), "second");
}
#[test]
fn test_history_down_restores_current_input() {
let mut reader = LineEditor::new(100);
let mut out = Vec::new();
let mut input = keys(&[b"old", ENTER]);
reader
.read_line_from(&mut input, &mut out, "$ ", None)
.unwrap();
let mut input = keys(&[b"new", UP, DOWN, ENTER]);
out.clear();
let result = reader
.read_line_from(&mut input, &mut out, "$ ", None)
.unwrap();
assert_eq!(result, Some("new".to_string()));
}
#[test]
fn test_history_dedup() {
let mut reader = LineEditor::new(100);
let mut out = Vec::new();
let mut input = keys(&[b"dup", ENTER]);
reader
.read_line_from(&mut input, &mut out, "$ ", None)
.unwrap();
let mut input = keys(&[b"dup", ENTER]);
reader
.read_line_from(&mut input, &mut out, "$ ", None)
.unwrap();
let mut input = keys(&[UP, UP, ENTER]);
out.clear();
let result = reader
.read_line_from(&mut input, &mut out, "$ ", None)
.unwrap();
assert_eq!(result, Some("dup".to_string()));
}
#[test]
fn test_history_max_size() {
let mut reader = LineEditor::new(3);
let mut out = Vec::new();
for cmd in &["aaa", "bbb", "ccc", "ddd"] {
let mut input = keys(&[cmd.as_bytes(), ENTER]);
reader
.read_line_from(&mut input, &mut out, "$ ", None)
.unwrap();
}
let mut input = keys(&[UP, UP, UP, ENTER]);
out.clear();
let result = reader
.read_line_from(&mut input, &mut out, "$ ", None)
.unwrap();
assert_eq!(result, Some("bbb".to_string()));
}
#[test]
fn test_backspace() {
let mut reader = LineEditor::new(100);
let mut input = keys(&[b"helloo", &[127], ENTER]);
let mut out = Vec::new();
let result = reader
.read_line_from(&mut input, &mut out, "$ ", None)
.unwrap();
assert_eq!(result, Some("hello".to_string()));
}
#[test]
fn test_ctrl_u_clears_line() {
let mut reader = LineEditor::new(100);
let mut input = keys(&[b"garbage", &[21], b"clean", ENTER]); let mut out = Vec::new();
let result = reader
.read_line_from(&mut input, &mut out, "$ ", None)
.unwrap();
assert_eq!(result, Some("clean".to_string()));
}
#[test]
fn test_empty_line_not_in_history() {
let mut reader = LineEditor::new(100);
let mut out = Vec::new();
let mut input = keys(&[b"real", ENTER]);
reader
.read_line_from(&mut input, &mut out, "$ ", None)
.unwrap();
let mut input = keys(&[ENTER]);
reader
.read_line_from(&mut input, &mut out, "$ ", None)
.unwrap();
let mut input = keys(&[UP, ENTER]);
out.clear();
let result = reader
.read_line_from(&mut input, &mut out, "$ ", None)
.unwrap();
assert_eq!(result, Some("real".to_string()));
}
#[test]
fn test_run_loop_with_handler() {
use std::sync::{Arc, Mutex};
let executed = Arc::new(Mutex::new(Vec::new()));
let exec_clone = Arc::clone(&executed);
let handler = move |line: &str| -> Result<LoopAction, String> {
exec_clone.lock().unwrap().push(line.to_string());
Ok(LoopAction::Continue)
};
let mut reader = LineEditor::new(100);
let mut input = keys(&[b"echo hello", ENTER, b"ls", ENTER, &[4]]);
let mut out = Vec::new();
reader
.run_loop_from(&mut input, &mut out, "$ ", &handler, None)
.unwrap();
let cmds = executed.lock().unwrap();
assert_eq!(cmds.len(), 2);
assert_eq!(cmds[0], "echo hello");
assert_eq!(cmds[1], "ls");
}
#[test]
fn test_run_loop_break_on_exit() {
let handler = |line: &str| -> Result<LoopAction, String> {
if line == "exit" {
Ok(LoopAction::Break)
} else {
Ok(LoopAction::Continue)
}
};
let mut reader = LineEditor::new(100);
let mut input = keys(&[b"cmd1", ENTER, b"exit", ENTER, b"cmd2", ENTER]);
let mut out = Vec::new();
reader
.run_loop_from(&mut input, &mut out, "$ ", &handler, None)
.unwrap();
}
#[test]
fn test_run_loop_error_continues() {
use std::sync::{Arc, Mutex};
let count = Arc::new(Mutex::new(0u32));
let count_clone = Arc::clone(&count);
let handler = move |line: &str| -> Result<LoopAction, String> {
*count_clone.lock().unwrap() += 1;
if line == "fail" {
Err("simulated error".to_string())
} else {
Ok(LoopAction::Continue)
}
};
let mut reader = LineEditor::new(100);
let mut input = keys(&[b"ok", ENTER, b"fail", ENTER, b"ok2", ENTER, &[4]]);
let mut out = Vec::new();
reader
.run_loop_from(&mut input, &mut out, "$ ", &handler, None)
.unwrap();
assert_eq!(*count.lock().unwrap(), 3);
}
#[test]
fn test_run_loop_with_history_navigation() {
use std::sync::{Arc, Mutex};
let executed = Arc::new(Mutex::new(Vec::new()));
let exec_clone = Arc::clone(&executed);
let handler = move |line: &str| -> Result<LoopAction, String> {
exec_clone.lock().unwrap().push(line.to_string());
Ok(LoopAction::Continue)
};
let mut reader = LineEditor::new(100);
let mut input = keys(&[
b"echo hello",
ENTER,
UP,
ENTER, &[4], ]);
let mut out = Vec::new();
reader
.run_loop_from(&mut input, &mut out, "$ ", &handler, None)
.unwrap();
let cmds = executed.lock().unwrap();
assert_eq!(cmds.len(), 2);
assert_eq!(cmds[0], "echo hello");
assert_eq!(cmds[1], "echo hello"); }
#[test]
fn test_run_loop_with_handle_parallel() {
use crate::{ArcVecWriter, CommandRegistry, handle_parallel};
use std::sync::{Arc, Mutex};
let registry = Arc::new(CommandRegistry::with_builtins());
let output = Arc::new(Mutex::new(Vec::<u8>::new()));
let reg = Arc::clone(®istry);
let out_ref = Arc::clone(&output);
let handler = move |line: &str| -> Result<LoopAction, String> {
if line == "exit" {
return Ok(LoopAction::Break);
}
let results = handle_parallel(
vec![line.to_string()],
Box::new(std::io::empty()),
Box::new(ArcVecWriter {
inner: Arc::clone(&out_ref),
}),
Arc::clone(®),
wasibox_core::CancellationToken::new(),
);
for res in results {
res?;
}
Ok(LoopAction::Continue)
};
let mut reader = LineEditor::new(100);
let mut input = keys(&[
b"echo hello",
ENTER,
UP,
ENTER, b"exit",
ENTER,
]);
let mut term_out = Vec::new();
reader
.run_loop_from(&mut input, &mut term_out, "$ ", &handler, None)
.unwrap();
let buf = output.lock().unwrap();
let result = String::from_utf8_lossy(&buf);
let lines: Vec<&str> = result.trim().lines().collect();
assert_eq!(lines.len(), 2);
assert_eq!(lines[0], "hello");
assert_eq!(lines[1], "hello"); }
#[test]
fn test_key_event_handler() {
struct MockHandler {
events: Vec<KeyEvent>,
}
impl KeyEventHandler for MockHandler {
fn on_key_event(&mut self, key: KeyEvent) {
self.events.push(key);
}
}
let mut editor = LineEditor::new(10);
let mut handler = MockHandler { events: Vec::new() };
editor.input_char_with_handler('a' as u32, &mut handler);
editor.input_char_with_handler('b' as u32, &mut handler);
editor.input_char_with_handler(13, &mut handler);
assert_eq!(
handler.events,
vec![KeyEvent::Char('a'), KeyEvent::Char('b'), KeyEvent::Enter,]
);
}
#[test]
fn test_unicode_japanese_input_char() {
let mut editor = LineEditor::new(10);
editor.input_char('ใ' as u32);
assert_eq!(editor.buffer(), "ใ");
assert_eq!(editor.input_char(13), Some("ใ".to_string()));
}
#[test]
fn test_unicode_emoji_input_char() {
let mut editor = LineEditor::new(10);
editor.input_char('๐ฆ' as u32);
assert_eq!(editor.buffer(), "๐ฆ");
assert_eq!(editor.input_char(13), Some("๐ฆ".to_string()));
}
#[test]
fn test_read_line_from_decodes_utf8_input() {
let mut reader = LineEditor::new(10);
let mut input = Cursor::new("echo ใ๐ฆ\r".as_bytes().to_vec());
let mut out = Vec::new();
let result = reader
.read_line_from(&mut input, &mut out, "$ ", None)
.unwrap();
assert_eq!(result, Some("echo ใ๐ฆ".to_string()));
}
#[test]
fn test_read_line_from_decodes_complex_emoji_input() {
let family = "๐จโ๐ฉโ๐งโ๐ฆ";
let mut reader = LineEditor::new(10);
let mut input = Cursor::new(format!("echo {family}\r").into_bytes());
let mut out = Vec::new();
let result = reader
.read_line_from(&mut input, &mut out, "$ ", None)
.unwrap();
assert_eq!(result, Some(format!("echo {family}")));
}
#[test]
fn test_backspace_removes_complex_emoji_grapheme() {
let family = "๐จโ๐ฉโ๐งโ๐ฆ";
let mut editor = LineEditor::new(10);
for ch in format!("aใ๐ฆ{family}").chars() {
editor.input_char(ch as u32);
}
editor.input_char(127);
assert_eq!(editor.buffer(), "aใ๐ฆ");
editor.input_char(127);
assert_eq!(editor.buffer(), "aใ");
editor.input_char(127);
assert_eq!(editor.buffer(), "a");
}
#[test]
fn test_left_right_do_not_split_graphemes() {
let family = "๐จโ๐ฉโ๐งโ๐ฆ";
let mut editor = LineEditor::new(10);
for ch in format!("a{family}b").chars() {
editor.input_char(ch as u32);
}
editor.input_char(KEY_LEFT);
editor.input_char(KEY_LEFT);
editor.input_char('X' as u32);
assert_eq!(editor.buffer(), format!("aX{family}b"));
}
#[test]
fn test_coptic_codepoint_no_longer_collides_with_special_keys() {
let mut editor = LineEditor::new(10);
let coptic = char::from_u32(1001).unwrap();
editor.input_char(1001);
assert_eq!(editor.buffer(), coptic.to_string());
}
}