From 3e58e63fbfbab741edc9c919c07041cdf9ba6a5a Mon Sep 17 00:00:00 2001 From: itycodes Date: Thu, 12 Feb 2026 12:28:14 +0100 Subject: [PATCH] Initial commit The code desperately needs a refactor :( --- .gitignore | 1 + Cargo.lock | 16 ++ Cargo.toml | 7 + src/main.rs | 18 ++ src/reader/esc_parse/mod.rs | 174 +++++++++++++++++++ src/reader/mod.rs | 332 ++++++++++++++++++++++++++++++++++++ 6 files changed, 548 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/main.rs create mode 100644 src/reader/esc_parse/mod.rs create mode 100644 src/reader/mod.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ca5dbca --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,16 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "libc" +version = "0.2.181" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" + +[[package]] +name = "shell" +version = "0.1.0" +dependencies = [ + "libc", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f22b793 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "shell" +version = "0.1.0" +edition = "2024" + +[dependencies] +libc = "0.2.181" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..37cbc51 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,18 @@ +pub mod reader; + +use reader::ReaderState; + +fn main() { + let mut state = ReaderState::new("uwu> "); + assert!(ReaderState::init_tty().is_ok()); + loop { + let line = state.read_line(); + match line { + Ok(s) => println!("{}", s), + Err(e) => { + eprintln!("Error: {:?}", e); + break; + } + } + } +} diff --git a/src/reader/esc_parse/mod.rs b/src/reader/esc_parse/mod.rs new file mode 100644 index 0000000..8d38820 --- /dev/null +++ b/src/reader/esc_parse/mod.rs @@ -0,0 +1,174 @@ +#[derive(Clone, Debug, PartialEq)] +pub enum Direction { + Up, Down, Right, Left, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum InputChar { + Text(char), + CursorMotion(Direction), + NewLine, + EoT, + Backspace, +} + +#[derive(Default)] +pub struct SequenceState { + /// Currently parsing a control sequence + in_seq: bool, + /// Currently parsing a CSI control sequence + in_csi: bool, + + /// Buffer containing the currently-parsed control sequence + /// (Includes 0x5B for CSI, but not the initial 0x1B) + /// Cleared when no sequence is being parsed. + seq_buf: Vec, + + /// Buffer of pending input characters & commands + out_buf: Vec, +} + +#[derive(Debug)] +pub enum AcceptError { + InvalidEscapeSequence(u8), + Unimplemented(u8), + UnimplementedSequence(Vec), +} + +macro_rules! bad_seq { + ($buf:expr) => { + Result::Err(AcceptError::UnimplementedSequence($buf)) + }; +} + +impl SequenceState { + pub fn new() -> Self { + return Self::default() + } + + pub fn has_next(&self) -> bool { + return !self.out_buf.is_empty(); + } + + fn clean_escape(&mut self) -> Result<(), AcceptError> { + self.in_seq = false; + self.in_csi = false; + self.seq_buf.clear(); + Result::Ok(()) + } + + pub fn accept_char(&mut self, chr: u8) -> Result<(), AcceptError> { + // Init sequence + if chr == b'\x1b' && !self.in_seq { + self.in_seq = true; + Result::Ok(()) + // Sequence parsing (determine sequence group) + } else if self.in_seq && !self.in_csi { + match chr { + // CSI + b'[' => { + self.in_csi = true; + self.seq_buf.push(chr); + Result::Ok(()) + } + _ => bad_seq!(self.seq_buf.clone()) + } + // CSI parsing + } else if self.in_seq && self.in_csi { + self.seq_buf.push(chr); + match chr { + b'0'..b'9' => bad_seq!(self.seq_buf.clone()), + b'A'..=b'D' => { + assert_eq!(self.seq_buf, format!("[{}", chr as char).as_bytes()); + let dir = [Direction::Up, + Direction::Down, + Direction::Right, + Direction::Left]; + let dir = dir[(chr-b'A') as usize].clone(); + self.out_buf.push(InputChar::CursorMotion(dir)); + self.clean_escape() + } + _ => bad_seq!(self.seq_buf.clone()) + } + + // ASCII printable characters + } else if chr >= 0x20 && chr <= 0x7E { + self.out_buf.push(InputChar::Text(chr as char)); + Result::Ok(()) + // Newline + } else if chr == b'\n' { + self.out_buf.push(InputChar::NewLine); + Result::Ok(()) + } + // Backspace character + else if chr == 0x7F { + self.out_buf.push(InputChar::Backspace); + Result::Ok(()) + } + // Unrecognized + else { + Result::Err(AcceptError::Unimplemented(chr)) + } + } + + pub fn accept_text(&mut self, buf: &[u8]) -> Result<(), AcceptError> { + for c in buf { + let res = self.accept_char(*c); + if res.is_err() { + return res; + } + } + Ok(()) + } +} + +impl Iterator for SequenceState { + type Item = InputChar; + + // next() is the only required method + fn next(&mut self) -> Option { + self.out_buf.pop() + } +} + +pub fn cursor_left() { + eprint!("\x1b[D"); +} + +pub fn cursor_left_nr(n: usize) { + eprint!("\x1b[{}D", n); +} + +pub fn cursor_right() { + eprint!("\x1b[C"); +} + +pub fn cursor_right_nr(n: usize) { + eprint!("\x1b[{}C", n); +} + +pub fn start_line() { + eprint!("\r"); +} + +pub fn clear_to_end() { + eprint!("\x1b[K"); +} + +pub fn clear_line() { + start_line(); + clear_to_end(); +} + +pub fn hide_cursor() { + eprint!("\x1b[?25l"); + +} + +pub fn show_cursor() { + eprint!("\x1b[?25h"); +} + +pub fn bell() { + eprint!("\x07"); +} \ No newline at end of file diff --git a/src/reader/mod.rs b/src/reader/mod.rs new file mode 100644 index 0000000..7e58423 --- /dev/null +++ b/src/reader/mod.rs @@ -0,0 +1,332 @@ +#![allow(unused)] + +use std::mem::MaybeUninit; + +use crate::reader::esc_parse::{InputChar, bell, hide_cursor, show_cursor}; + +pub mod esc_parse; + +#[derive(Default)] +pub struct ReaderState { + prompt: String, + line: String, + cursor: usize, + max_len: usize, + history: Vec, + history_inx: Option, + history_saved: Option, +} + +#[derive(Debug)] +pub enum InitTTYError { + NotATTY +} + +#[derive(Debug)] +pub enum ReadError { + EscapeError(esc_parse::AcceptError) +} + +#[derive(Debug)] +pub enum EditCommand { + NewLine, + HistoryPrevious, + HistoryNext, + CursorBack, + CursorForward, + DeleteBack, +} + +macro_rules! edit_cmd { + ($id:ident) => { + Result::::Ok(EditCommand::$id) + }; + ($id:ident, $cmd:expr) => { + Ok(EditCommand::$id($cmd)) + }; +} + +impl ReaderState { + pub fn new(prompt: &str) -> Self { + ReaderState { prompt: String::from(prompt), max_len: prompt.len(), ..Default::default() } + } + pub fn print(&mut self, txt: String) { + eprint!("{}", txt); + self.max_len += txt.len(); + } + fn clear(&mut self) { + esc_parse::clear_line(); + self.max_len = self.prompt.len(); + } + pub fn empty(&mut self) { + self.set_line(String::from("")); + } + pub fn init_tty() -> Result<(), InitTTYError> { + unsafe { + use libc::{isatty, termios, ECHO, ICANON, INPCK, TCSANOW, tcgetattr, tcsetattr}; + if !(isatty(0) == 1) { + return Result::Err(InitTTYError::NotATTY); + } + let mut termios: termios = MaybeUninit::zeroed().assume_init(); + tcgetattr(0, &mut termios); + termios.c_lflag &= !(ECHO | ICANON); + termios.c_iflag &= !(INPCK); + tcsetattr(0, TCSANOW, &termios); + return Ok(()); + } + } + pub fn insert_at_cursor(&mut self, txt: String) { + self.line.insert_str(self.cursor, txt.as_str()); + self.cursor += txt.len(); + self.max_len += txt.len(); + self.redraw(); + } + + fn current_hist(&mut self) -> String { + let i: usize = self.history_inx.unwrap(); + let line = self.history.get(self.history.len().saturating_sub(i+1)); + return line.unwrap().to_string(); + } + + fn set_current_hist(&mut self, str: String) { + let i: usize = self.history_inx.unwrap(); + let len = self.history.len(); + self.history[len.saturating_sub(i+1)] = str; + } + + fn insert_at_cursor_hist(&mut self, txt: String) { + let mut line = self.current_hist(); + line.insert_str(self.cursor, txt.as_str()); + self.history_reset(); + self.set_line(line.clone()); + self.redraw(); + } + + fn input_char(&mut self, c: char) { + if self.history_inx.is_some() { + self.insert_at_cursor_hist(String::from(c)); + } else { + self.insert_at_cursor(String::from(c)); + } + self.redraw(); + } + + fn input_line(&mut self) -> Result { + let mut esc_buf: [u8; 1] = [0]; + let mut read_stat = esc_parse::SequenceState::new(); + let mut out_buf: Vec = Vec::new(); + unsafe { + use libc::{read, c_void}; + self.redraw(); + loop { + read(0, esc_buf.as_mut_ptr() as *mut c_void, 1); + let res = read_stat.accept_char(esc_buf[0]); + if res.is_err() { + return Result::Err(ReadError::EscapeError(res.err().unwrap())); + } else { + if read_stat.has_next() { + let chr = read_stat.next().unwrap(); + out_buf.push(chr.clone()); + if let InputChar::Text(c) = chr { + self.input_char(c); + } + if let InputChar::CursorMotion(m) = chr { + use esc_parse::Direction::*; + return match m { + Up => edit_cmd!(HistoryPrevious), + Down => edit_cmd!(HistoryNext), + Left => edit_cmd!(CursorBack), + Right => edit_cmd!(CursorForward), + } + } + if chr == InputChar::NewLine { + break; + } + if chr == InputChar::Backspace { + return edit_cmd!(DeleteBack); + } + } + } + } + edit_cmd!(NewLine) + } + } + + pub fn redraw(&mut self) { + hide_cursor(); + self.clear(); + self.print(format!("{}{}", self.prompt, self.line)); + self.move_at(self.cursor); + show_cursor(); + } + + pub fn submit_line(&mut self, line: String) { + if self.history_inx.is_none() { + self.history_append(line.clone()); + } else { + self.history_reset(); + } + self.empty(); + } + + pub fn read_line(&mut self) -> Result { + use EditCommand::*; + loop { + let cmd = self.input_line(); + if cmd.is_err() { + return Err(cmd.err().unwrap()); + } + let cmd = cmd.ok().unwrap(); + match cmd { + NewLine => { + let line = self.line.clone(); + self.submit_line(line.clone()); + return Ok(line) + }, + CursorBack => { + self.move_left() + }, + CursorForward => { + self.move_right(); + }, + DeleteBack => { + self.backspace(); + }, + HistoryPrevious => { + self.history_back(); + }, + HistoryNext => { + self.history_front(); + } + } + } + } + + pub fn set_line(&mut self, line: String) { + self.line = line.clone(); + self.clear(); + self.cursor_at(line.len()); + } + + pub fn history_append(&mut self, line: String) { + self.history.push(line); + self.history_inx = None; + } + + fn history_reset(&mut self) { + self.history_inx = None; + self.history_saved = None; + } + + pub fn set_prompt(&mut self, prompt: String) { + self.prompt = prompt; + self.redraw(); + } + + pub fn history_back(&mut self) { + let h = self.history.clone(); + let mut i = self.history_inx; + if i.map_or(false, |v| v+1 == h.len()) { + return; + } + if i.is_none() { + i = Some(0); + self.history_saved = Some(self.line.clone()); + } else { + i = i.map(|v| { v + 1 }); + } + self.history_inx = i; + let i: usize = i.unwrap(); + let line = h.get(h.len().saturating_sub(i+1)); + if line.is_some() { + self.set_line(line.unwrap().to_string()); + } else { + bell(); + self.redraw(); + } + } + + fn saved_restore(&mut self) { + self.set_line(self.history_saved.clone().expect("Missing saved value")); + } + + pub fn history_front(&mut self) { + let h = self.history.clone(); + let mut i = self.history_inx; + if i.is_none() { + return; + } else if i.map_or(false, |v| v == 0) { + self.saved_restore(); + self.history_reset(); + return; + } else { + i = i.map(|v| { v - 1 }); + self.history_inx = i; + } + let i = i.unwrap(); + let line = h.get(h.len().saturating_sub(i+1)); + if line.is_some() { + self.set_line(line.unwrap().to_string()); + } else { + bell(); + self.redraw(); + } + } + + pub fn move_left(&mut self) { + use esc_parse::{cursor_left, bell}; + if self.cursor > 0 { + self.cursor -= 1; + cursor_left(); + self.redraw(); + } else { + bell(); + } + } + + pub fn move_right(&mut self) { + use esc_parse::{cursor_right, bell}; + if self.cursor < self.line.len() { + self.cursor += 1; + cursor_right(); + self.redraw(); + } else { + bell(); + } + } + + fn move_at(&mut self, inx: usize) { + self.cursor = inx; + if self.max_len < inx { + self.max_len = inx; + } + esc_parse::cursor_left_nr(self.max_len); + esc_parse::cursor_right_nr(inx+self.prompt.len()); + } + + pub fn cursor_at(&mut self, inx: usize) { + self.move_at(inx); + self.redraw(); + } + + pub fn backspace(&mut self) { + if self.history_inx.is_some() { + if self.cursor > 0 { + let mut line = self.current_hist(); + line.remove(self.cursor-1); + self.move_left(); + self.history_reset(); + self.set_line(line); + } else { + bell(); + } + } else { + if self.cursor > 0 { + self.line.remove(self.cursor-1); + self.move_left(); + } else { + bell(); + } + } + } +} \ No newline at end of file