commit
3e58e63fbf
@ -0,0 +1 @@
|
||||
/target
|
||||
@ -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…
Reference in new issue