Initial commit

The code desperately needs a refactor :(
master
itycodes 3 hours ago
commit 3e58e63fbf

1
.gitignore vendored

@ -0,0 +1 @@
/target

16
Cargo.lock generated

@ -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",
]

@ -0,0 +1,7 @@
[package]
name = "shell"
version = "0.1.0"
edition = "2024"
[dependencies]
libc = "0.2.181"

@ -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;
}
}
}
}

@ -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<u8>,
/// Buffer of pending input characters & commands
out_buf: Vec<InputChar>,
}
#[derive(Debug)]
pub enum AcceptError {
InvalidEscapeSequence(u8),
Unimplemented(u8),
UnimplementedSequence(Vec<u8>),
}
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::Item> {
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");
}

@ -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<String>,
history_inx: Option<usize>,
history_saved: Option<String>,
}
#[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::<EditCommand, ReadError>::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<EditCommand, ReadError> {
let mut esc_buf: [u8; 1] = [0];
let mut read_stat = esc_parse::SequenceState::new();
let mut out_buf: Vec<esc_parse::InputChar> = 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<String, ReadError> {
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();
}
}
}
}
Loading…
Cancel
Save