use std::{
env,
fs::{DirEntry, ReadDir},
io::{Error, Write},
path::Path,
process::Command,
};
fn main() -> Result<(), Error> {
let mut shell = Shell::new();
loop {
shell.tick();
}
}
struct Shell {
term: console::Term,
path: String,
current_directory: String,
cache: Vec<String>,
input: String,
suggestion: String,
highlighting: bool,
showing_entries: bool,
highlighted_entry: (usize, usize),
index: usize,
pos: usize,
}
impl Shell {
fn tick(&mut self) {
if self.showing_entries {
self.show_tab_entries();
}
self.term.clear_line().unwrap();
self.term
.write_all(
format!(
"\x1b[32;40m{}\x1b[37;40m {}> ",
env::var("USER").unwrap(),
self.current_directory.to_owned()
)
.as_bytes(),
)
.unwrap();
self.term
.write_all(self.input.replace("\\s", "\\ ").as_bytes())
.unwrap();
self.term
.write_all(format!("\x1b[38;5;240m{}\x1b[37m", self.suggestion).as_bytes())
.unwrap();
self.term
.move_cursor_left(self.input.len() + self.suggestion.len() - self.pos)
.unwrap();
match self.term.read_key().unwrap() {
console::Key::Char(x) => {
self.term.write_all(x.to_string().as_bytes()).unwrap();
self.input.insert(self.pos, x);
self.pos += 1;
if self.showing_entries {
self.term.move_cursor_down(1).unwrap();
self.term.clear_line().unwrap();
self.term.move_cursor_up(1).unwrap();
(
self.showing_entries,
self.highlighting,
self.highlighted_entry.0,
) = (false, false, 0);
}
}
console::Key::Tab => {
if self.highlighting {
self.highlighted_entry.0 += 1;
}
if !self.showing_entries {
self.showing_entries = true;
} else {
self.highlighting = true;
}
}
console::Key::Backspace => {
if self.pos != 0 {
self.input.remove(self.pos - 1);
self.term.clear_chars(1).unwrap();
self.pos -= 1;
}
if self.showing_entries {
self.term.move_cursor_down(1).unwrap();
self.term.clear_line().unwrap();
self.term.move_cursor_up(1).unwrap();
(
self.showing_entries,
self.highlighting,
self.highlighted_entry.0,
) = (false, false, 0);
}
}
console::Key::ArrowUp => {
if self.index == 0 && self.cache.len() >= self.index + 1 {
self.index += 1;
self.input = self.cache[self.index - 1].to_owned();
self.pos = self.input.len();
} else if self.cache.len() >= self.index + 1 {
self.index += 1;
self.input = self.cache[self.index - 1].to_owned();
self.pos = self.input.len();
}
}
console::Key::ArrowDown => match self.index {
0 => (),
1 => {
self.index -= 1;
self.input = "".to_owned();
self.pos = 0;
}
_ => {
self.index -= 1;
self.input = self.cache[self.index - 1].to_owned();
self.pos = self.input.len();
}
},
console::Key::ArrowLeft => {
if self.pos != 0 {
self.pos -= 1;
}
}
console::Key::ArrowRight => {
if self.pos != self.input.len() {
self.pos += 1;
} else {
self.input = format!("{}{}", self.input, self.suggestion.replace(" ", "\\s"));
self.pos += self.suggestion.replace(" ", "\\s").len();
self.suggestion = String::new();
}
}
console::Key::Enter => {
if self.showing_entries {
self.complete_tab();
} else {
self.run_command();
self.term
.write_all((self.current_directory.to_string() + "> ").as_bytes())
.unwrap();
}
}
_ => (),
};
if self.input.trim_end() != "" && self.pos != self.input.len() - 1 {
let current_dir_suggestion = get_suggestion(
self.get_dir_vec().join("/").replace("\\s", " "),
self.input.to_owned(),
);
if current_dir_suggestion.is_empty() && !self.input.trim_start().contains(" ") {
self.suggestion = get_suggestion(self.path.to_owned(), self.input.to_owned());
} else {
self.suggestion = current_dir_suggestion;
}
} else {
self.suggestion = String::new();
}
}
fn show_tab_entries(&mut self) {
let autodir = self.get_dir_vec();
let dir = std::fs::read_dir(autodir.join("/").replace("\\s", " "))
.unwrap_or(std::fs::read_dir("./").unwrap());
let entries = dir_filter(self.input.to_owned(), dir);
let len = entries.len();
if len != 0 {
self.term.write_all(b"\n").unwrap();
self.term.clear_line().unwrap();
let mut temp_input = self.input.split(" ").last().unwrap();
temp_input = temp_input.split("/").last().unwrap();
let mut pos = 0;
if self.highlighted_entry.0 == len {
self.highlighted_entry.0 = 0;
}
entries.into_iter().for_each(|x| {
let mut entry = match x.as_ref().unwrap().file_type().unwrap().is_dir() {
true => x.unwrap().file_name().into_string().unwrap() + "/",
false => x.unwrap().file_name().into_string().unwrap(),
};
match self.highlighting {
false => {
entry.insert_str(temp_input.len(), "\x1b[0;37m");
self.term
.write_all(format!("\x1b[4;36m{} ", entry).as_bytes())
.unwrap()
}
true => {
if pos == self.highlighted_entry.0 {
self.term
.write_all(format!("\x1b[30;46m{}\x1b[30;40m ", entry).as_bytes())
.unwrap();
} else {
entry.insert_str(temp_input.len(), "\x1b[0;37m");
self.term
.write_all(format!("\x1b[4;36m{} ", entry).as_bytes())
.unwrap()
}
}
}
pos += 1;
});
self.term.move_cursor_up(1).unwrap();
} else {
(
self.showing_entries,
self.highlighting,
self.highlighted_entry.0,
) = (false, false, 0);
}
}
fn complete_tab(&mut self) {
self.term.move_cursor_down(1).unwrap();
self.term.clear_line().unwrap();
self.term.move_cursor_up(1).unwrap();
self.suggestion = String::new();
let mut temp_input = self.input.split(" ").collect::<Vec<&str>>();
temp_input.pop();
let mut autodir = self.get_dir_vec();
let dir = std::fs::read_dir(autodir.join("/").replace("\\s", " "))
.unwrap_or(std::fs::read_dir("./").unwrap());
let mut entry = dir_filtered_index(self.input.to_owned(), dir, self.highlighted_entry.0);
if entry.contains(" ") {
entry = entry.replace(" ", "\\s");
}
autodir.push(entry.as_str());
autodir.remove(0);
let temp = autodir.join("/");
temp_input.push(&temp);
self.input = temp_input.join(" ");
self.pos = self.input.len();
(
self.showing_entries,
self.highlighting,
self.highlighted_entry.0,
) = (false, false, 0);
}
fn get_dir_vec(&self) -> Vec<&str> {
let mut autodir = self
.input
.split(" ")
.last()
.unwrap()
.split("/")
.collect::<Vec<&str>>();
autodir.pop();
autodir.insert(0, ".");
autodir
}
fn run_command(&mut self) {
self.index = 0;
self.pos = 0;
self.term.move_cursor_right(self.suggestion.len()).unwrap();
self.term.clear_chars(self.suggestion.len()).unwrap();
self.suggestion = String::new();
self.term.write_all(b"\n").unwrap();
let mut args = self.input.trim().split_whitespace();
let command = args.next().unwrap_or("");
if self.cache.get(0).unwrap_or(&"".to_owned()) != &self.input.to_owned() || self.input != ""
{
self.cache.insert(0, self.input.to_owned());
}
match command {
"" => (),
"cd" => {
let new_dir = args
.peekable()
.peek()
.map_or("/", |x| x)
.replace("\\s", " ");
let root = Path::new(&new_dir);
if let Err(e) = env::set_current_dir(&root) {
eprintln!("{}", e);
}
self.current_directory = get_current_dir();
}
"exit" => std::process::exit(0),
command => {
let child = Command::new(command).args(args).spawn();
match child {
Ok(mut child) => {
child.wait().unwrap();
}
Err(e) => {
if e.raw_os_error().unwrap() == 2 {
eprintln!("Unknown Command: \"{command}\"");
} else {
eprintln!("{}", e);
}
}
};
}
}
self.input = String::new();
}
fn new() -> Shell {
Shell {
term: console::Term::stdout(),
path: std::env::var("PATH").unwrap_or("./".to_owned()),
current_directory: get_current_dir(),
cache: Vec::new(),
input: String::new(),
suggestion: String::new(),
highlighting: false,
showing_entries: false,
highlighted_entry: (0, 0),
index: 0,
pos: 0,
}
}
}
fn get_current_dir() -> String {
env::current_dir()
.unwrap()
.to_str()
.unwrap()
.split("/")
.last()
.unwrap()
.to_string()
}
fn dir_filtered_index(input: String, dir: ReadDir, entry: usize) -> String {
let filter = dir_filter(input, dir);
let entry = filter.get(entry).unwrap().as_ref().unwrap();
match entry.file_type().unwrap().is_dir() {
true => entry.file_name().into_string().unwrap() + "/",
false => entry.file_name().into_string().unwrap(),
}
}
fn dir_filter(input: String, dir: ReadDir) -> Vec<Result<DirEntry, Error>> {
dir.filter(|x| {
x.as_ref()
.unwrap()
.file_name()
.into_string()
.unwrap()
.starts_with(
&input
.split(" ")
.last()
.unwrap()
.split("/")
.last()
.unwrap()
.replace("\\s", " "),
)
})
.collect::<Vec<Result<DirEntry, Error>>>()
}
fn get_suggestion(path: String, input: String) -> String {
let mut possible_suggestions = Vec::new();
path.split(":").for_each(|x| {
let mut filtered = dir_filter(input.to_owned(), std::fs::read_dir(x).unwrap());
filtered.sort_by(|a, b| {
a.as_ref()
.unwrap()
.file_name()
.partial_cmp(&b.as_ref().unwrap().file_name())
.unwrap()
});
let value = match filtered.get(0) {
Some(x) => match x.as_ref().unwrap().file_type().unwrap().is_dir() {
true => x.as_ref().unwrap().file_name().into_string().unwrap() + "/",
false => x.as_ref().unwrap().file_name().into_string().unwrap(),
},
None => String::new(),
};
if value != String::new() {
possible_suggestions.push(value);
}
});
possible_suggestions.sort();
if possible_suggestions
.get(0)
.unwrap_or(&String::new())
.to_owned()
!= String::new()
{
let possible = possible_suggestions.get(0).unwrap().to_owned();
return if let Some(x) = possible.get(
input
.split(" ")
.last()
.unwrap()
.split("/")
.last()
.unwrap()
.replace("\\s", "\\ ")
.len()..possible.len(),
) {
x.to_string()
} else {
String::new()
};
} else {
return String::new();
}
}