diff --git a/Cargo.lock b/Cargo.lock index c958fdc..f5c90b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", +] diff --git a/Cargo.toml b/Cargo.toml index 39e279a..849701d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,3 +2,7 @@ name = "rush" version = "0.1.0" edition = "2024" +default-run = "rush" + +[dependencies] +libc = "0.2.175" diff --git a/src/bin/keycode.rs b/src/bin/keycode.rs new file mode 100644 index 0000000..b488c82 --- /dev/null +++ b/src/bin/keycode.rs @@ -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); +} \ No newline at end of file diff --git a/src/data.rs b/src/data.rs new file mode 100644 index 0000000..e59da09 --- /dev/null +++ b/src/data.rs @@ -0,0 +1,71 @@ +use std::{env, path::PathBuf}; + +#[derive(Debug)] +pub struct Data { + prev_path: PathBuf, + curr_path: PathBuf, + + history: Vec, + 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 = 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 { + 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 { + 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() + } +} \ No newline at end of file diff --git a/src/key.rs b/src/key.rs new file mode 100644 index 0000000..e953fae --- /dev/null +++ b/src/key.rs @@ -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) +} + +impl From 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]) + } + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 8a55e80..c92703b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 { + 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(mut input: I) -> (::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 { + 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() { - -} \ No newline at end of file + let mut data = Data::default(); + + let orig = enable_raw_mode(); + + rush_loop(&mut data, orig).unwrap(); + + disable_raw_mode(&orig); +} diff --git a/src/raw.rs b/src/raw.rs new file mode 100644 index 0000000..7e23eaf --- /dev/null +++ b/src/raw.rs @@ -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); + } +} \ No newline at end of file