diff --git a/res/welcome.txt b/res/welcome.txt index b9b1974..18f8a72 100644 --- a/res/welcome.txt +++ b/res/welcome.txt @@ -1,4 +1,4 @@ -ashe@tilde.club's password: +ashe@tilde.club's password: Welcome to AsheOS 21.12.1 LTS (HRT/Estrix 6.25.21-wasm) * Technical: https://tempest.dev diff --git a/src/commands.rs b/src/commands.rs index 6e5f68f..03f23a2 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,15 +1,7 @@ use std::str::FromStr; -use crate::{Directory, State}; - -static PROJECTS_DIR: &'static str = "~ashe ashe.gay tempest.dev"; -static HOME_DIR: &'static str = "about.md contact.md \x1b[0;36mprojects\x1b[0m"; - -static ABOUT: &'static str = include_str!("../res/about.md"); -static CONTACT: &'static str = include_str!("../res/contact.md"); -static ASHE_GAY: &'static str = include_str!("../res/ashe.gay.md"); -static TEMPEST_DEV: &'static str = include_str!("../res/tempest.dev.md"); -static TILDE_ASHE: &'static str = include_str!("../res/~ashe.md"); +use crate::State; +use crate::path; pub fn handle_command(command_string : String, state : &mut State) { let mut words = command_string.split(' '); @@ -18,96 +10,122 @@ pub fn handle_command(command_string : String, state : &mut State) { let args = words.collect::>(); match command { + "help" => { + state.output.push_back("implemented commands: clear, pwd, cd, ls, cat".to_string()) + } + "pwd" => { + state.output.push_back(state.cwd.clone()) + } + "clear" => { state.output.clear(); } "cd" => { - if args.len() == 0 { - state.current_working_directory = Directory::Home; + if args.len() == 0 || args[0].is_empty() { + state.cwd = "/home/ashe".to_string(); } else { - match state.current_working_directory { - Directory::Home => { - match args[0] { - "~" => (), - "~/" => (), - ".." => state.output.push_back(String::from_str("bash: cd: ..: Permission denied").unwrap()), - "../" => state.output.push_back(String::from_str("bash: cd: ..: Permission denied").unwrap()), - "projects" => state.current_working_directory = Directory::Projects, - "projects/" => state.current_working_directory = Directory::Projects, - _ => { - if args[0].chars().next().unwrap() == '/' { - state.output.push_back(format!("bash: cd: {}: Permission denied", args[0])); - } else { - state.output.push_back(format!("bash: cd: {}: No such file or directory", args[0])); - } - } - } - }, - Directory::Projects => { - match args[0] { - "~" => state.current_working_directory = Directory::Home, - "~/" => state.current_working_directory = Directory::Home, - ".." => state.current_working_directory = Directory::Home, - "../" => state.current_working_directory = Directory::Home, - _ => { - if args[0].chars().next().unwrap() == '/' { - state.output.push_back(format!("bash: cd: {}: Permission denied", args[0])); - } else { - state.output.push_back(format!("bash: cd: {}: No such file or directory", args[0])); - } - } - } - - } + let target_path = *args.get(0).unwrap(); + let absolute_target_path = path::join(state.cwd.as_str(), target_path); + + if absolute_target_path == "/" { + return state.output.push_back(format!("cd: permission denied: {}", args[0])) + } + + let target = state.fs_root.get_entry_by_path(absolute_target_path.as_str()); + + if target.is_err() { + return state.output.push_back(format!("cd: no such file or directory: {}", args[0])) + } + + let target = target.unwrap(); + + if !target.is_dir() { + return state.output.push_back(format!("cd: not a directory: {}", args[0])) + } + + if !target.permissions.can_read { + return state.output.push_back(format!("cd: permission denied: {}", args[0])) } + + state.cwd = absolute_target_path.to_string(); } } - "ls" => match state.current_working_directory { - Directory::Home => match args.len() { - 0 => state.output.push_back(String::from_str(HOME_DIR).unwrap()), - 1 => match args[0] { - "projects" => state.output.push_back(String::from_str(PROJECTS_DIR).unwrap()), - "projects/" => state.output.push_back(String::from_str(PROJECTS_DIR).unwrap()), - _ => state.output.push_back(format!("bash: ls: '{}': No such file or directory", args[0])) - }, - _ => state.output.push_back(format!("bash: ls: too many arguments")) - }, - Directory::Projects => match args.len() { - 0 => state.output.push_back(String::from_str(PROJECTS_DIR).unwrap()), - 1 => match args[0] { - ".." => state.output.push_back(String::from_str(HOME_DIR).unwrap()), - "../" => state.output.push_back(String::from_str(HOME_DIR).unwrap()), - _ => state.output.push_back(format!("bash: ls: '{}': No such file or directory", args[0])) - }, - _ => state.output.push_back(format!("bash: ls: too many arguments")) + "ls" => { + let target_path; + if args.len() > 0 { + target_path = path::join(state.cwd.as_str(), args.get(0).unwrap()); + } else { + target_path = state.cwd.clone() + } + + let target_entry = state.fs_root + .get_entry_by_path(target_path.as_str()); + + if target_entry.is_err() { + console_log!("Error getting path {}, error: {}", target_path, target_entry.err().unwrap().filename); + return state.output.push_back(format!("ls: cannot access '{}': No such file or directory", target_path.as_str())) + } + + let target_entry = target_entry.unwrap(); + + if !target_entry.permissions.can_read { + return state.output.push_back(format!("ls: permission denied: {}", args[0])) } + + if !target_entry.is_dir() { + return state.output.push_back(target_entry.name.to_string()) + } + + let target = target_entry.as_dir().unwrap(); + let entries = target.get_entries(); + + let output = entries.fold(String::new(), |mut a, b| { + if b.is_dir() { + a.push_str("\x1b[0;36m"); + } + + a.push_str(b.name); + + if b.is_dir() { + a.push_str("\x1b[0m"); + } + + a.push_str(" "); + a + }); + + state.output.push_back(output); } "cat" => match args.len() { - 0 => state.output.push_back(format!("bash: cat: too few arguments")), - 1 => match state.current_working_directory { - Directory::Home => match args[0] { - "about.md" => output_file(ABOUT, args[0], state), - "contact.md" => output_file(CONTACT, args[0], state), - "projects/ashe.gay" => output_file(ASHE_GAY, args[0], state), - "projects/tempest.dev" => output_file(TEMPEST_DEV, args[0], state), - "projects/~ashe" => output_file(TILDE_ASHE, args[0], state), - _ => state.output.push_back(format!("bash: cat: {}: No such file or directory", args[0])) - }, - Directory::Projects => match args[0] { - "../about.md" => output_file(ABOUT, args[0], state), - "../contact.md" => output_file(CONTACT, args[0], state), - "ashe.gay" => output_file(ASHE_GAY, args[0], state), - "tempest.dev" => output_file(TEMPEST_DEV, args[0], state), - "~ashe" => output_file(TILDE_ASHE, args[0], state), - _ => state.output.push_back(format!("bash: cat: {}: No such file or directory", args[0])) - }, - }, - _ => state.output.push_back(format!("bash: cat: too many arguments")) - }, - + 0 => state.output.push_back(format!("cat: too few arguments")), + 1 => { + let target_path = *args.get(0).unwrap(); + let absolute_target_path = path::join(state.cwd.as_str(), target_path); + let target = state.fs_root.get_entry_by_path(absolute_target_path.as_str()); + + if let Err(err) = target { + return state.output.push_back(format!("cat: no such file or directory: {}", err.filename)) + } + + let target = target.unwrap(); + + if !target.permissions.can_read { + return state.output.push_back(format!("cat: {}: Permission denied", target.name)) + } + + if target.is_dir() { + return state.output.push_back(format!("cat: {}: Is a directory", target.name)) + } + + let target_file = target.as_file().unwrap(); + output_file(target_file.contents, args[0], state) + } + _ => state.output.push_back(format!("cat: too many arguments")), + } + _ => { state.output.push_back(format!("bash: {}: command not found", command)); } diff --git a/src/fs.rs b/src/fs.rs new file mode 100644 index 0000000..4b6f369 --- /dev/null +++ b/src/fs.rs @@ -0,0 +1,177 @@ +pub struct Permissions { + pub can_read: bool, + pub can_write: bool, + pub can_exec: bool +} + +pub struct DirEntry { + pub name: &'static str, + pub permissions: Permissions, + pub item: DirEntryContents +} + +pub enum DirEntryContents { + Directory(Directory), + File(File) +} + +pub struct Directory { + entries : Vec +} + +pub struct File { + pub contents: &'static str +} + +#[derive(Debug)] +pub struct DirError { + pub filename: String +} + +impl Directory { + pub fn get_entry_by_name(&self, name : &str) -> Result<&DirEntry, DirError> { + for entry in self.entries.iter() { + if entry.name == name { + return Ok(entry) + } + } + + return Err(DirError { + filename: name.to_string() + }) + } + + pub fn get_entry_by_path(&self, path : &str) -> Result<&DirEntry, DirError> { + let mut segments = path.split("/").filter(|s| !s.is_empty()); + let first_seg = segments.next().expect("Path has no segments!"); + let remaining_path = segments.fold(String::new(), |mut a, b| { + a.reserve(b.len() + 1); + a.push_str("/"); + a.push_str(b); + a + }); + + console_log!("In dir, getting {}. first_seg: {}. remaining: {}", path, first_seg, remaining_path); + + let entry = self.get_entry_by_name(first_seg)?; + + if remaining_path.is_empty() { + console_log!("Found entry! {}", entry.name); + return Ok(entry); + } else if entry.is_dir() { + console_log!("Found dir: {}", entry.name); + return entry.as_dir().unwrap().get_entry_by_path(remaining_path.as_str()) + } else { + console_log!("Error accessing: {}", first_seg); + return Err(DirError { + filename: first_seg.to_string() + }) + } + } + + pub fn get_entries(&self) -> impl Iterator { + self.entries.iter() + } + + pub fn get_dirs(&self) -> impl Iterator { + self.entries.iter().filter(|item| item.is_dir()) + } + + pub fn get_files(&self) -> impl Iterator { + self.entries.iter().filter(|item| item.is_file()) + } +} + +impl DirEntry { + pub fn create_dir(name: &'static str, perms: bool) -> DirEntry { + DirEntry { + name, + permissions: Permissions { + can_read: perms, + can_write: perms, + can_exec: perms + }, + item: DirEntryContents::Directory(Directory { + entries: Vec::new() + }) + } + } + + pub fn create_file(name: &'static str, perms: bool, contents: &'static str) -> DirEntry { + DirEntry { + name, + permissions: Permissions { + can_read: perms, + can_write: perms, + can_exec: perms + }, + item: DirEntryContents::File(File { + contents + }) + } + } + + pub fn is_file(&self) -> bool { + if let DirEntryContents::File(file) = &self.item { + return true + } + return false + } + + pub fn is_dir(&self) -> bool { + !self.is_file() + } + + pub fn as_dir(&self) -> Option<&Directory> { + if let DirEntryContents::Directory(dir) = &self.item { + return Some(dir) + } + return None + } + + pub fn as_file(&self) -> Option<&File> { + if let DirEntryContents::File(file) = &self.item { + return Some(file) + } + return None + } + + pub fn get_entry_by_path(&self, path : &str) -> Result<&DirEntry, DirError> { + console_log!("In entry, getting {}", path); + + if path == "/" || path.is_empty() { + return Ok(self) + } + + self.as_dir().expect("Cannot get entries of file").get_entry_by_path(path) + } +} + +pub fn setup_fs() -> DirEntry { + let mut root = DirEntry::create_dir("root", false); + let mut home = DirEntry::create_dir("home", false); + let mut ashe = DirEntry::create_dir("ashe", true); + let mut projects = DirEntry::create_dir("projects", true); + + if let DirEntryContents::Directory(ref mut dir) = projects.item { + dir.entries.push(DirEntry::create_file("~ashe", true, include_str!("../res/~ashe.md"))); + dir.entries.push(DirEntry::create_file("ashe.gay", true, include_str!("../res/ashe.gay.md"))); + dir.entries.push(DirEntry::create_file("tempest.dev", true, include_str!("../res/tempest.dev.md"))); + } + + if let DirEntryContents::Directory(ref mut dir) = ashe.item { + dir.entries.push(DirEntry::create_file("about.md", true, include_str!("../res/about.md"))); + dir.entries.push(DirEntry::create_file("contact.md", true, include_str!("../res/contact.md"))); + dir.entries.push(projects) + } + + if let DirEntryContents::Directory(ref mut dir) = home.item { + dir.entries.push(ashe) + } + + if let DirEntryContents::Directory(ref mut dir) = root.item { + dir.entries.push(home); + } + + return root; +} \ No newline at end of file diff --git a/src/io.rs b/src/io.rs index 7be9317..988e91d 100644 --- a/src/io.rs +++ b/src/io.rs @@ -31,6 +31,7 @@ pub fn parse_key_event(event : &KeyboardEvent, ignore_ctrl : bool) -> Option Option { let mut tmp = [0; 4]; state.input += letter.encode_utf8(&mut tmp) } - Key::Ctrl(letter) => + Key::Ctrl(letter) => match letter.as_ref() { Key::Letter(letter) => { if *letter == 'c' { @@ -85,7 +86,7 @@ extern "C" { pub fn print_state(target : &mut Element, state : &State) { let mut output = state.output.iter().map(|s| (*s).clone()).collect::>().join("\n"); - + if state.output.len() > 0 { output += "\n"; } @@ -95,7 +96,7 @@ pub fn print_state(target : &mut Element, state : &State) { output += state.prompt.as_str(); output += " "; } - + // TODO: Line overflow output += state.input.as_str(); @@ -108,6 +109,8 @@ pub fn print_state(target : &mut Element, state : &State) { pub fn build_prompt(state : &State) -> String { let mut prompt = String::new(); + let cwd = state.cwd.replace("/home/ashe", "~"); + prompt += "\x1b[0;36m"; prompt += "ashe"; prompt += "\x1b[2;33m"; @@ -116,10 +119,7 @@ pub fn build_prompt(state : &State) -> String { prompt += "\x1b[2;37m"; prompt += ":"; prompt += "\x1b[2;32m"; - prompt += match state.current_working_directory { - crate::Directory::Home => "~", - crate::Directory::Projects => "~/projects" - }; + prompt += &cwd.as_str(); prompt += "\x1b[2;37m"; prompt += "$"; diff --git a/src/main.rs b/src/main.rs index 2dc0a4b..0d0fd56 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,8 @@ extern crate console_error_panic_hook; mod js; mod io; mod commands; +mod fs; +mod path; use std::collections::VecDeque; use std::panic; @@ -24,22 +26,23 @@ pub enum Directory { } pub struct State { - // cwd : &Directory + fs_root: fs::DirEntry, + cwd: String, input : String, output : VecDeque, prompt: String, max_rows: usize, - current_working_directory : Directory } impl State { pub fn new() -> Self { State { + fs_root: fs::setup_fs(), + cwd: "/home/ashe".to_string(), input : String::new(), output : VecDeque::new(), prompt: String::new(), max_rows: 24, - current_working_directory: Directory::Home } } @@ -88,7 +91,7 @@ async fn init(document : Document) { if let Some(command) = command { handle_command(command, &mut state); } - + state.prompt = io::build_prompt(&state); io::print_state(&mut render_target, &state); }) as Box); diff --git a/src/path.rs b/src/path.rs new file mode 100644 index 0000000..e09743a --- /dev/null +++ b/src/path.rs @@ -0,0 +1,34 @@ +use std::str::FromStr; + +pub fn join(root : &str, path : &str) -> String { + if path.starts_with("/") { + return String::from_str(path).unwrap() + } + + let mut result_segments : Vec<&str> = root.split("/").filter(|s| !s.is_empty()).collect(); + let path_segments = path.split("/").filter(|s| !s.is_empty()); + + for segment in path_segments { + if segment == "." { + continue + } + + if segment == ".." { + if result_segments.len() > 0 { + result_segments.pop(); + } + continue + } + + result_segments.push(segment) + } + + let result_path = result_segments.iter().fold(String::new(), |mut a, b| { + a.reserve(b.len() + 1); + a.push_str("/"); + a.push_str(b); + a + }); + + return result_path +} \ No newline at end of file