command running, raw mode and history

This commit is contained in:
Raptorox 2025-09-20 18:50:06 +02:00
parent e9d64b38cd
commit ec1a840bb6
No known key found for this signature in database
GPG key ID: 8B3556FC3ED1F6D8
7 changed files with 366 additions and 2 deletions

9
Cargo.lock generated
View file

@ -2,6 +2,15 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "libc"
version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]]
name = "rush"
version = "0.1.0"
dependencies = [
"libc",
]

View file

@ -2,3 +2,7 @@
name = "rush"
version = "0.1.0"
edition = "2024"
default-run = "rush"
[dependencies]
libc = "0.2.175"

47
src/bin/keycode.rs Normal file
View file

@ -0,0 +1,47 @@
use std::io::{stdin, stdout, Read, Write};
fn enable_raw_mode() -> libc::termios {
unsafe {
let fd = libc::STDIN_FILENO;
let mut termios = std::mem::zeroed();
libc::tcgetattr(fd, &mut termios);
let mut raw = termios;
// Input modes: no break, no CR to NL, no parity check, no strip char,
// no start/stop output control.
raw.c_iflag &= !(libc::BRKINT | libc::ICRNL | libc::INPCK | libc::ISTRIP | libc::IXON);
// Output modes: disable post processing
raw.c_oflag &= !(libc::OPOST);
// Control modes: set 8 bit chars
raw.c_cflag |= libc::CS8;
// Local modes: disable echo, canonical, extended functions, and signals
raw.c_lflag &= !(libc::ECHO | libc::ICANON | libc::IEXTEN | libc::ISIG);
// Control chars: set read timeout
raw.c_cc[libc::VMIN] = 1; // minimum number of bytes before read returns
raw.c_cc[libc::VTIME] = 0; // no timeout
libc::tcsetattr(fd, libc::TCSAFLUSH, &raw);
termios // return original so we can restore later
}
}
fn disable_raw_mode(orig: &libc::termios) {
unsafe {
libc::tcsetattr(libc::STDIN_FILENO, libc::TCSAFLUSH, orig);
}
}
fn main() {
let orig = enable_raw_mode();
let mut buffer = [0u8; 1];
loop {
stdin().read_exact(&mut buffer).unwrap();
if buffer[0] == 3 {break;}
print!("{:?}\r\n", buffer);
stdout().flush().unwrap();
}
disable_raw_mode(&orig);
}

71
src/data.rs Normal file
View file

@ -0,0 +1,71 @@
use std::{env, path::PathBuf};
#[derive(Debug)]
pub struct Data {
prev_path: PathBuf,
curr_path: PathBuf,
history: Vec<String>,
hist_pos: usize,
hist_partial: String
}
impl Default for Data {
fn default() -> Self {
let prev_path = PathBuf::new();
let curr_path = env::current_dir().unwrap();
let history: Vec<String> = Vec::new();
let hist_pos = 0;
let hist_partial = String::new();
Self {
prev_path,
curr_path,
history,
hist_pos,
hist_partial
}
}
}
impl Data {
pub fn set_path(&mut self, path: PathBuf) {
self.prev_path = self.curr_path.clone();
self.curr_path = path;
}
pub fn get_current_path(&self) -> PathBuf {
self.curr_path.clone()
}
pub fn get_previous_path(&self) -> PathBuf {
self.prev_path.clone()
}
pub fn add_to_hist(&mut self, command: String) {
self.history.push(command);
self.hist_pos += 1;
}
pub fn save_command(&mut self, command: String) {
self.hist_partial = command;
}
pub fn get_prev_hist_item(&mut self) -> Option<String> {
if self.history.is_empty() { return None; }
if self.hist_pos <= self.history.len() && self.hist_pos > 0 { self.hist_pos -= 1; Some(self.history[self.hist_pos].clone()) }
else { None }
}
pub fn get_next_hist_item(&mut self) -> Option<String> {
if self.hist_pos < self.history.len() { self.hist_pos += 1; }
if self.hist_pos < self.history.len() { Some(self.history[self.hist_pos].clone()) }
else if self.hist_pos == self.history.len() { Some(self.hist_partial.clone()) }
else { None }
}
pub fn at_hist_end(&self) -> bool {
self.hist_pos+1 == self.history.len()
}
}

40
src/key.rs Normal file
View file

@ -0,0 +1,40 @@
use std::io::{stdin, Read};
#[derive(PartialEq)]
pub enum Key {
Char(char),
Enter,
Backspace,
Escape,
ArrowUp,
ArrowDown,
ArrowRight,
ArrowLeft,
#[allow(dead_code)]
Unknown(Vec<u8>)
}
impl From<u8> for Key {
fn from(byte: u8) -> Self {
match byte {
b'\n' | b'\r' => Key::Enter,
8 | 127 => Key::Backspace,
27 => {
let mut buf = [0u8; 2];
if stdin().read_exact(&mut buf).is_err() {
return Key::Escape;
}
match buf {
[b'[', b'A'] => Key::ArrowUp,
[b'[', b'B'] => Key::ArrowDown,
[b'[', b'C'] => Key::ArrowRight,
[b'[', b'D'] => Key::ArrowLeft,
_ => Key::Unknown(vec![27, buf[0], buf[1]])
}
},
char if char.is_ascii() => Key::Char(byte as char),
byte => Key::Unknown(vec![byte])
}
}
}

View file

@ -1,3 +1,172 @@
use std::{
env,
io::{self, stdin, stdout, Read, Stdin, Stdout, Write},
process::Command,
str::SplitWhitespace,
};
mod raw;
use raw::{enable_raw_mode, disable_raw_mode};
mod key;
use key::Key;
mod data;
use data::Data;
fn print_prompt(pre_input: &str, data: &mut Data, newline: bool) -> io::Result<()> {
if newline {
print!("\r\n{}\r\n> {}", data.get_current_path().display(), pre_input);
} else {
print!("\r> {}", pre_input);
}
stdout().flush()?;
Ok(())
}
fn flush_prompt(flush_count: usize) -> io::Result<()> {
print!("\r> {}", " ".repeat(flush_count));
print!("\r> {}", "\x08".repeat(flush_count));
Ok(())
}
fn handle_input(stdin: &mut Stdin, stdout: &mut Stdout, data: &mut Data) -> io::Result<String> {
let mut input = String::new();
let mut buffer = [0u8; 1];
loop {
stdin.read_exact(&mut buffer)?;
let key = Key::from(buffer[0]);
match key {
Key::Enter => {
data.add_to_hist(input.clone());
write!(stdout, "\r\n")?;
break;
}
Key::Backspace => {
if !input.is_empty() {
input.pop();
write!(stdout, "\x08 \x08")?;
stdout.flush()?;
}
}
Key::Escape => continue,
Key::ArrowUp => {
if let Some(hist_command) = data.get_prev_hist_item() {
if data.at_hist_end() { data.save_command(input.clone()); }
flush_prompt(input.len())?;
input = hist_command.clone();
print_prompt(&input, data, false)?;
}
},
Key::ArrowDown => {
if let Some(hist_command) = data.get_next_hist_item() {
flush_prompt(input.len())?;
input = hist_command.clone();
print_prompt(&input, data, false)?;
}
},
Key::ArrowLeft => {},
Key::ArrowRight => {},
Key::Char(c) => {
input.push(c);
write!(stdout, "{c}")?;
stdout.flush()?;
}
Key::Unknown(_) => {}
}
}
Ok(input)
}
fn parse_input<I>(mut input: I) -> (<I as Iterator>::Item, I)
where
I: Iterator,
{
let command = input.next().unwrap();
let args = input;
(command, args)
}
fn run_command(command: &str, args: SplitWhitespace, data: &mut Data) -> Result<u8, String> {
match command {
"exit" => return Ok(1),
"cd" => {
let path = args.peekable().peek().map_or("~", |dir| *dir);
let mut new_path = data.get_current_path();
if path.chars().nth(0).unwrap() == '/' { new_path.push("/"); }
for subpath in path.split("/") {
match subpath {
"-" => {
new_path = data.get_previous_path();
break;
}
"~" => {
new_path.push(env::home_dir().unwrap());
}
".." => {
new_path.pop();
}
"." => {}
path => new_path.push(path),
}
}
if let Err(e) = env::set_current_dir(&new_path) {
Err(e.to_string())
} else {
data.set_path(new_path);
Ok(0)
}
}
command => {
let child = Command::new(command).args(args).spawn();
match child {
Ok(mut child) => { child.wait().unwrap(); Ok(0) }
Err(e) => { return Err(e.to_string()); }
}
}
}
}
fn rush_loop(data: &mut Data, orig: libc::termios) -> io::Result<()> {
let mut stdin = stdin();
let mut stdout = stdout();
loop {
print_prompt("", data, true)?;
let input = handle_input(&mut stdin, &mut stdout, data)?;
let input = input.trim();
if input.is_empty() { continue; }
let input = input.split_whitespace();
let (command, args) = parse_input(input);
disable_raw_mode(&orig);
match run_command(command, args, data) {
Ok(status) => match status {
1 => return Ok(()),
_ => {}
},
Err(e) => eprint!("{e}\r\n")
}
enable_raw_mode();
}
}
fn main() {
}
let mut data = Data::default();
let orig = enable_raw_mode();
rush_loop(&mut data, orig).unwrap();
disable_raw_mode(&orig);
}

24
src/raw.rs Normal file
View file

@ -0,0 +1,24 @@
pub fn enable_raw_mode() -> libc::termios {
unsafe {
let fd = libc::STDIN_FILENO;
let mut termios = std::mem::zeroed();
libc::tcgetattr(fd, &mut termios);
let mut raw = termios;
raw.c_iflag &= !(libc::BRKINT | libc::ICRNL | libc::INPCK | libc::ISTRIP | libc::IXON);
raw.c_oflag &= !(libc::OPOST);
raw.c_cflag |= libc::CS8;
raw.c_lflag &= !(libc::ECHO | libc::ICANON | libc::IEXTEN | libc::ISIG);
raw.c_cc[libc::VMIN] = 1;
raw.c_cc[libc::VTIME] = 0;
libc::tcsetattr(fd, libc::TCSAFLUSH, &raw);
termios
}
}
pub fn disable_raw_mode(orig: &libc::termios) {
unsafe {
libc::tcsetattr(libc::STDIN_FILENO, libc::TCSAFLUSH, orig);
}
}