From 97a6a281fd9780ecdccfcf30079f339e305fdc78 Mon Sep 17 00:00:00 2001 From: Jonas Maier <> Date: Mon, 20 Apr 2026 10:27:04 +0200 Subject: move ansi escape code parsing into own module --- src/ansi.rs | 137 ++++++++++++++++++++++++++++++++++ src/main.rs | 213 ++++++++++++----------------------------------------- src/run/builtin.rs | 27 ++++++- src/run/mod.rs | 2 + 4 files changed, 214 insertions(+), 165 deletions(-) create mode 100644 src/ansi.rs (limited to 'src') diff --git a/src/ansi.rs b/src/ansi.rs new file mode 100644 index 0000000..e04f6d9 --- /dev/null +++ b/src/ansi.rs @@ -0,0 +1,137 @@ +use std::io::Read; + +use crate::cursor::Direction; + +pub enum KeyboardInput { + Eof, + Key(u8), + CtrlA, + CtrlB, + CtrlC, + CtrlE, + CtrlD, + CtrlL, + CtrlR, + Arrow(Direction), + CtrlArrow(Direction), + DeleteLeft, + DeleteRight, + CtrlDeleteRight, + Home, + End, +} + +fn read1() -> Option { + let mut buf = [0]; + match std::io::stdin().lock().read_exact(&mut buf) { + Ok(_) => Some(buf[0]), + Err(_) => None, + } +} + +fn byte_to_dir(b: u8) -> Option { + use Direction::*; + match b { + b'A' => Some(Up), + b'B' => Some(Down), + b'C' => Some(Right), + b'D' => Some(Left), + _ => None, + } +} + +fn read_escape(debug: bool) -> KeyboardInput { + use Direction::*; + use KeyboardInput::*; + + let mut seq = vec![match read1() { + Some(x) => x, + None => return Eof, + }]; + + if seq[0] == b'[' { + // still more + while { + let last = seq[seq.len() - 1]; + !(0x40..=0x7E).contains(&last) || seq.len() == 1 + } { + seq.push(match read1() { + Some(x) => x, + None => return Eof, + }); + } + + if debug { + println!("escape: {}", seq.escape_ascii()); + } + + match seq[1] { + b'3' => { + if seq.len() > 2 && seq[2] == b'~' { + DeleteRight + } else { + todo!("unhandled: {}", seq.escape_ascii()); + } + } + b'H' => Home, + b'F' => End, + b'd' => CtrlDeleteRight, + + // Ctrl Arrow + b'1' => { + if seq[1..].starts_with(b"1;5") { + if seq.len() == 4 { + todo!("idk what this is."); + } + match seq[4] { + b'A' => CtrlArrow(Up), + b'B' => CtrlArrow(Down), + b'C' => CtrlArrow(Right), + b'D' => CtrlArrow(Left), + _ => todo!("unhandled {}", seq.escape_ascii()), + } + } else { + todo!("unhandled {}", seq[1..].escape_ascii()) + } + } + + x => { + if let Some(dir) = byte_to_dir(x) { + Arrow(dir) + } else { + todo!("escape characters {}", seq[1..].escape_ascii()) + } + } + } + } else { + if debug { + println!("escape: {}", seq.escape_ascii()); + } + match seq[0] { + b'd' => CtrlDeleteRight, + x => todo!("unhandled escape code: ESC {x}"), + } + } +} + +pub fn read(debug: bool) -> KeyboardInput { + use KeyboardInput::*; + + let Some(x) = read1() else { + return KeyboardInput::Eof; + }; + + match x { + 1 => CtrlA, + 2 => CtrlB, + 3 => CtrlC, + 4 => CtrlD, + 8 | 127 => DeleteLeft, + 12 => CtrlL, + 18 => CtrlR, + 27 => read_escape(debug), + b'\r' => Key(x), + x if !x.is_ascii_control() => Key(x), + x => todo!("unimplemented control code: {x}"), + } +} diff --git a/src/main.rs b/src/main.rs index 9286653..e0d9e84 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ use std::sync::{Arc, Mutex}; use std::thread::sleep; use std::time::Duration; +pub mod ansi; pub mod basedir; pub mod completion; pub mod ctrlc; @@ -273,12 +274,6 @@ impl Session { } } -fn read1() -> u8 { - let mut buf = [0]; - io::stdin().lock().read_exact(&mut buf).unwrap(); - buf[0] -} - fn event_loop() { history::setup(); @@ -316,56 +311,31 @@ fn event_loop() { let _ctrlc = ctrlc::setup(session.clone()); loop { - let mut buf = [0u8; 1]; - - let Ok(_) = stdin.lock().read_exact(&mut buf) else { - break; - }; - let mut se = session.lock().unwrap(); - if se.debug_keystrokes { - println!("{}", buf.escape_ascii()); - } - - match buf[0] { - // Ctrl+A - 1 => { - se.move_to_begin(); - } + use ansi::KeyboardInput as Kb; - // Ctrl+E - 5 => { - se.move_to_end(); - } - - // Ctrl+C - 3 => { + match ansi::read(se.debug_keystrokes) { + Kb::Eof => todo!(), + Kb::CtrlA => se.move_to_begin(), + Kb::CtrlB => todo!(), + Kb::CtrlC => { se.clear_prompt(); se.history_visit = 0; } - - // EOF - 4 => { - break; - } - - // apparently another backspace, don't do anything. - 8 => {} - - // Ctrl+L - 12 => { + Kb::CtrlE => se.move_to_end(), + Kb::CtrlD => break, + Kb::CtrlL => { clear_screen(); print!("{}", se.prompt()); io::stdout().write_all(&se.line.into_bytes()).unwrap(); cursor::move_cursor(Direction::Left, se.line.distance_from_right_end()); io::stdout().lock().flush().unwrap(); } - - // Ctrl+R - 18 => {} - - // Enter - b'\r' => { + Kb::CtrlR => { + println!("search is not yet implemented"); + se.reprint_prompt(); + } + Kb::Key(b'\r' | b'\n') => { let line = se.line.into_bytes(); if !line.is_empty() { @@ -391,19 +361,7 @@ fn event_loop() { run::run(session.clone(), parsed); } } - - // Backspace (127 on most systems) - 127 => { - if se.line.is_empty() && !se.line.is_dirty() && !se.history.is_empty() { - // take previous command for editing - let cmd = se.history[se.history.len() - 1].cmd.clone(); - se.type_bytes(&cmd); - } else { - se.del_left(); - } - } - - b'\t' => { + Kb::Key(b'\t') => { let cmd = se.line.pre().to_vec(); drop(se); @@ -422,117 +380,44 @@ fn event_loop() { se.reprint_prompt(); } } - - // Escape sequence - 27 => { - let mut seq = vec![read1()]; - - if seq[0] == b'[' { - // still more - while { - let last = seq[seq.len() - 1]; - !(0x40..=0x7E).contains(&last) || seq.len() == 1 - } { - seq.push(read1()); - } - - if se.debug_keystrokes { - println!("escape: {}", seq.escape_ascii()); + Kb::Arrow(dir) => match dir { + Direction::Up => se.history_up(), + Direction::Down => se.history_down(), + Direction::Left => { + if se.line.left() { + move_cursor(Direction::Left, 1); + io::stdout().lock().flush().unwrap(); } - - match seq[1] { - b'A' => { - // up - se.history_up(); - } - b'B' => { - // down - se.history_down(); - } - b'C' => { - if se.line.right() { - move_cursor(Direction::Right, 1); - io::stdout().lock().flush().unwrap(); - } - } - b'D' => { - if se.line.left() { - move_cursor(Direction::Left, 1); - io::stdout().lock().flush().unwrap(); - } - } - b'3' => { - if seq.len() > 2 && seq[2] == b'~' { - se.del_right(); - } else { - todo!("unhandled: {}", seq.escape_ascii()); - } - } - - // HOME button - b'H' => { - se.move_to_begin(); - } - - // END button - b'F' => { - se.move_to_end(); - } - - // Ctrl Arrow - b'1' => { - if seq[1..].starts_with(b"1;5") { - if seq.len() == 4 { - todo!("idk what this is."); - } - match seq[4] { - b'A' => { - println!("Ctrl+Up"); - se.reprint_prompt(); - continue; - } - b'B' => { - println!("Ctrl+Down"); - se.reprint_prompt(); - continue; - } - b'C' => { - se.move_right_word(); - } - b'D' => { - se.move_left_word(); - } - _ => todo!("unhandled {}", seq.escape_ascii()), - } - continue; - } - todo!("unhandled {}", seq[1..].escape_ascii()) - } - - _ => todo!("escape characters {}", seq[1..].escape_ascii()), + } + Direction::Right => { + if se.line.right() { + move_cursor(Direction::Right, 1); + io::stdout().lock().flush().unwrap(); } + } + }, + Kb::CtrlArrow(dir) => match dir { + Direction::Left => se.move_left_word(), + Direction::Right => se.move_right_word(), + _ => { + println!("Ctrl+{dir:?} not implemented"); + se.reprint_prompt(); + } + }, + Kb::DeleteLeft => { + if se.line.is_empty() && !se.line.is_dirty() && !se.history.is_empty() { + // take previous command for editing + let cmd = se.history[se.history.len() - 1].cmd.clone(); + se.type_bytes(&cmd); } else { - if se.debug_keystrokes { - println!("escape: {}", seq.escape_ascii()); - } - if seq[0] == b'd' { - se.del_right_word(); - } + se.del_left(); } } - - b'|' if se.line.is_empty() && !se.history.is_empty() => { - let mut cmd = se.history[se.history.len() - 1].cmd.clone(); - cmd.extend_from_slice(b" | "); - io::stdout().write_all(&cmd).unwrap(); - io::stdout().flush().unwrap(); - se.line.set_content(cmd); - } - - // Normal character - x => { - se.type_byte(x); - } + Kb::DeleteRight => se.del_right(), + Kb::CtrlDeleteRight => se.del_right_word(), + Kb::Home => se.move_to_begin(), + Kb::End => se.move_to_end(), + Kb::Key(x) => se.type_byte(x), } } diff --git a/src/run/builtin.rs b/src/run/builtin.rs index 7ca1e5e..c080c93 100644 --- a/src/run/builtin.rs +++ b/src/run/builtin.rs @@ -487,7 +487,8 @@ impl Builtin for alias { let mut parse_fail = false; let mut alias_args = Vec::new(); for arg in args { - match as crate::parse::Parse>::parse_from_bytes(&arg[..]) { + match as crate::parse::Parse>::parse_from_bytes(&arg[..]) + { Ok(mut parsed) => { alias_args.append(&mut parsed); } @@ -539,3 +540,27 @@ impl Builtin for unalias { Ok(()) } } + +pub struct debug; +impl Builtin for debug { + fn name(&self) -> &str { + "debug" + } + + fn io( + &self, + session: Arc>, + args: &[BString], + _stdin: &mut dyn Read, + stdout: &mut dyn Write, + ) -> Result { + let mut se = session.lock().unwrap(); + for arg in args { + match &arg[..] { + b"keys" | b"keystrokes" => se.debug_keystrokes = !se.debug_keystrokes, + _ => writeln!(stdout, "debug: unknown option {}", arg.escape_ascii())?, + } + } + Ok(()) + } +} diff --git a/src/run/mod.rs b/src/run/mod.rs index 36cd454..6fe0d45 100644 --- a/src/run/mod.rs +++ b/src/run/mod.rs @@ -539,6 +539,8 @@ const BUILTINS: &[&'static dyn Builtin] = &[ &builtin::completion, &builtin::alias, &builtin::unalias, + #[cfg(debug_assertions)] + &builtin::debug, ]; pub fn builtin_map() -> HashMap { -- cgit v1.2.3