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