From 22829bca0d4480f653ebf120f81b298077b964ee Mon Sep 17 00:00:00 2001 From: BakerNet Date: Sun, 29 Jun 2025 08:08:35 -0700 Subject: [PATCH 1/8] Migrate from hardcoded filesystem to virtual filesystem --- Cargo.toml | 1 + src/app/header.rs | 26 +- src/app/terminal.rs | 1112 +++++++++++++++++++++++---- src/app/terminal/command.rs | 211 +++-- src/app/terminal/cp_mv_tools.rs | 374 +++++++++ src/app/terminal/fs.rs | 356 --------- src/app/terminal/fs_tools.rs | 1099 +++++++++++++------------- src/app/terminal/ps_tools.rs | 57 +- src/app/terminal/simple_tools.rs | 24 +- src/app/terminal/system_tools.rs | 145 ++-- src/app/terminal/vfs.rs | 1238 ++++++++++++++++++++++++++++++ 11 files changed, 3431 insertions(+), 1212 deletions(-) create mode 100644 src/app/terminal/cp_mv_tools.rs delete mode 100644 src/app/terminal/fs.rs create mode 100644 src/app/terminal/vfs.rs diff --git a/Cargo.toml b/Cargo.toml index 180a410..7d33471 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ regex = { version = "1.11", optional = true } rss = { version = "2.0", optional = true, features = ["atom"] } log = "0.4" console_log = "1.0" +indextree = "4.7" [build-dependencies] chrono = { version = "0.4", features = ["serde"] } diff --git a/src/app/header.rs b/src/app/header.rs index a1f732a..62f1b06 100644 --- a/src/app/header.rs +++ b/src/app/header.rs @@ -22,8 +22,7 @@ use leptos_use::storage::use_local_storage; use crate::blog::Assets; -use super::terminal::fs::{DirContentItem, Target}; -use super::terminal::{ColumnarView, CommandRes, Terminal}; +use super::terminal::{ColumnarView, CommandRes, Terminal, TabCompletionItem}; #[component] fn MobileFloatingButton(on_click: impl Fn() + 'static) -> impl IntoView { @@ -190,7 +189,7 @@ fn InputSection( #[derive(Debug, Clone)] struct TabState { cursor: usize, - opts: Arc>, + opts: Arc>, index: Option, } @@ -206,6 +205,8 @@ pub fn Header() -> impl IntoView { let blog_posts = Assets::iter() .map(|s| s[..s.len() - 3].to_string()) .collect::>(); + + // Initialize terminal with current browser path for proper VFS state let terminal = StoredValue::new(Arc::new(Mutex::new(Terminal::new(&blog_posts, None)))); let input_ref = NodeRef::::new(); let floating_input_ref = NodeRef::::new(); @@ -694,7 +695,7 @@ pub fn Header() -> impl IntoView { (None, true) | (Some(0), true) => opts.len() - 1, (Some(i), true) => i - 1, }; - let new = tab_replace(&val[..cursor], &opts[new_index].0); + let new = tab_replace(&val[..cursor], &opts[new_index].completion_text); el.set_value(&new); set_input_value.set(new.clone()); set_cursor_position.set(new.len()); // Set cursor to end after tab completion @@ -719,7 +720,7 @@ pub fn Header() -> impl IntoView { return; }; if opts.len() == 1 { - let new = tab_replace(&val, &opts[0].0); + let new = tab_replace(&val, &opts[0].completion_text); el.set_value(&new); set_input_value.set(new.clone()); set_cursor_position.set(new.len()); // Set cursor to end after tab completion @@ -728,7 +729,7 @@ pub fn Header() -> impl IntoView { let cursor = val.len(); let index = if is_shift { let i = opts.len() - 1; - let new = tab_replace(&val[..cursor], &opts[i].0); + let new = tab_replace(&val[..cursor], &opts[i].completion_text); el.set_value(&new); set_input_value.set(new.clone()); set_cursor_position.set(new.len()); // Set cursor to end after tab completion @@ -775,11 +776,10 @@ pub fn Header() -> impl IntoView { }; let auto_comp_item = { - move |item: &DirContentItem, active: bool| { - let s = &item.0; - let target = &item.1; - let is_dir = matches!(target, Target::Dir(_)); - let is_ex = target.is_executable(); + move |item: &TabCompletionItem, active: bool| { + let s = &item.completion_text; + let is_dir = item.is_directory; + let is_ex = item.is_executable; let has_suffix = s.ends_with("/") || s.ends_with("*"); let s_display = if !active && has_suffix { @@ -898,8 +898,8 @@ pub fn Header() -> impl IntoView { None } }); - let render_func = move |item: DirContentItem| { - let is_sel = selected.as_ref().map(|s| &s.0) == Some(&item.0); + let render_func = move |item: TabCompletionItem| { + let is_sel = selected.as_ref().map(|s| &s.completion_text) == Some(&item.completion_text); auto_comp_item(&item, is_sel).into_any() }; view! { diff --git a/src/app/terminal.rs b/src/app/terminal.rs index 78b662c..a44069f 100644 --- a/src/app/terminal.rs +++ b/src/app/terminal.rs @@ -1,36 +1,54 @@ mod command; mod components; -pub mod fs; +mod cp_mv_tools; mod fs_tools; mod ps_tools; mod simple_tools; mod system_tools; +pub mod vfs; pub use command::CommandRes; pub use components::ColumnarView; use std::collections::{HashMap, VecDeque}; -use command::{Command, CommandAlias, Executable}; -use fs::{path_target_to_target_path, Target}; +use command::{Cmd, CmdAlias, Command, VfsCommand}; +use components::TextContent; +use cp_mv_tools::{VfsCpCommand, VfsMvCommand}; use fs_tools::{ - CatCommand, CdCommand, CpCommand, LsCommand, MkdirCommand, MvCommand, RmCommand, TouchCommand, + VfsCatCommand, VfsCdCommand, VfsLsCommand, VfsMkdirCommand, VfsRmCommand, VfsTouchCommand, }; +use indextree::NodeId; use ps_tools::{KillCommand, Process, PsCommand}; use simple_tools::{ ClearCommand, DateCommand, EchoCommand, HelpCommand, HistoryCommand, MinesCommand, NeofetchCommand, PwdCommand, SudoCommand, UptimeCommand, WhoAmICommand, }; use system_tools::{UnknownCommand, WhichCommand}; +use vfs::VirtualFilesystem; static HISTORY_SIZE: usize = 1000; +#[derive(Debug, Clone)] +pub struct TabCompletionItem { + pub completion_text: String, // The text to insert when selected + pub is_directory: bool, // Whether this is a directory + pub is_executable: bool, // Whether this is an executable file +} + +impl TextContent for TabCompletionItem { + fn text_content(&self) -> &str { + &self.completion_text + } +} + pub struct Terminal { - blog_posts: Vec, history: VecDeque, env_vars: HashMap, processes: Vec, - commands: HashMap>, + commands: HashMap>, + vfs_commands: HashMap>, + vfs: VirtualFilesystem, } impl Terminal { @@ -44,16 +62,21 @@ impl Terminal { let processes = Self::initialize_processes(); let commands = HashMap::new(); // Will be populated after construction + let vfs_commands = HashMap::new(); // Will be populated after construction + + let vfs = VirtualFilesystem::new(blog_posts.to_owned()); let mut terminal = Self { - blog_posts: blog_posts.to_owned(), history, env_vars, processes, commands, + vfs_commands, + vfs, }; terminal.initialize_commands(); + terminal.initialize_vfs_commands(); terminal } @@ -104,71 +127,51 @@ impl Terminal { fn initialize_commands(&mut self) { // Simple commands (no context needed) - self.commands.insert(Command::Help, Box::new(HelpCommand)); - self.commands.insert(Command::Pwd, Box::new(PwdCommand)); - self.commands - .insert(Command::WhoAmI, Box::new(WhoAmICommand)); - self.commands.insert(Command::Clear, Box::new(ClearCommand)); + self.commands.insert(Cmd::Help, Box::new(HelpCommand)); + self.commands.insert(Cmd::Pwd, Box::new(PwdCommand)); + self.commands.insert(Cmd::WhoAmI, Box::new(WhoAmICommand)); + self.commands.insert(Cmd::Clear, Box::new(ClearCommand)); self.commands - .insert(Command::Neofetch, Box::new(NeofetchCommand)); - self.commands.insert(Command::Mines, Box::new(MinesCommand)); - self.commands.insert(Command::Sudo, Box::new(SudoCommand)); - self.commands.insert(Command::Echo, Box::new(EchoCommand)); - self.commands.insert(Command::Date, Box::new(DateCommand)); - self.commands - .insert(Command::Uptime, Box::new(UptimeCommand)); + .insert(Cmd::Neofetch, Box::new(NeofetchCommand)); + self.commands.insert(Cmd::Mines, Box::new(MinesCommand)); + self.commands.insert(Cmd::Sudo, Box::new(SudoCommand)); + self.commands.insert(Cmd::Echo, Box::new(EchoCommand)); + self.commands.insert(Cmd::Date, Box::new(DateCommand)); + self.commands.insert(Cmd::Uptime, Box::new(UptimeCommand)); // Process commands + self.commands + .insert(Cmd::Ps, Box::new(PsCommand::new(self.processes.clone()))); self.commands.insert( - Command::Ps, - Box::new(PsCommand::new(self.processes.clone())), - ); - self.commands.insert( - Command::Kill, + Cmd::Kill, Box::new(KillCommand::new(self.processes.clone())), ); - // Filesystem commands - self.commands.insert( - Command::Which, - Box::new(WhichCommand::new(self.blog_posts.clone())), - ); - self.commands.insert( - Command::Ls, - Box::new(LsCommand::new(self.blog_posts.clone())), - ); - self.commands.insert( - Command::Cat, - Box::new(CatCommand::new(self.blog_posts.clone())), - ); - self.commands.insert( - Command::Cd, - Box::new(CdCommand::new(self.blog_posts.clone())), - ); - self.commands.insert( - Command::Touch, - Box::new(TouchCommand::new(self.blog_posts.clone())), - ); - self.commands.insert( - Command::MkDir, - Box::new(MkdirCommand::new(self.blog_posts.clone())), - ); - self.commands.insert( - Command::Rm, - Box::new(RmCommand::new(self.blog_posts.clone())), - ); - self.commands.insert( - Command::Cp, - Box::new(CpCommand::new(self.blog_posts.clone())), - ); - self.commands.insert( - Command::Mv, - Box::new(MvCommand::new(self.blog_posts.clone())), - ); - // History command and Unknown commands handled separately } + fn initialize_vfs_commands(&mut self) { + // VFS-aware commands that need direct filesystem access + self.vfs_commands + .insert(Cmd::Ls, Box::new(VfsLsCommand::new())); + self.vfs_commands + .insert(Cmd::Cd, Box::new(VfsCdCommand::new())); + self.vfs_commands + .insert(Cmd::Cat, Box::new(VfsCatCommand::new())); + self.vfs_commands + .insert(Cmd::Touch, Box::new(VfsTouchCommand::new())); + self.vfs_commands + .insert(Cmd::MkDir, Box::new(VfsMkdirCommand::new())); + self.vfs_commands + .insert(Cmd::Rm, Box::new(VfsRmCommand::new())); + self.vfs_commands + .insert(Cmd::Which, Box::new(WhichCommand::new())); + self.vfs_commands + .insert(Cmd::Cp, Box::new(VfsCpCommand::new())); + self.vfs_commands + .insert(Cmd::Mv, Box::new(VfsMvCommand::new())); + } + #[cfg(feature = "hydrate")] pub fn history(&self) -> VecDeque { self.history.clone() @@ -177,7 +180,7 @@ impl Terminal { fn process_aliases(&self, input: &str) -> String { let trimmed = input.trim(); - for alias in CommandAlias::all() { + for alias in CmdAlias::all() { let alias_str = alias.as_str(); if let Some(args) = trimmed.strip_prefix(alias_str) { if trimmed == alias_str { @@ -237,9 +240,20 @@ impl Terminal { unreachable!("Should have returned early if empty"); }; // Convert string to Command enum for type-safe lookup - let cmd = Command::from(cmd_text); + let cmd = Cmd::from(cmd_text); - // Try to find command in the new trait-based registry first + // Try VFS commands first (they have priority) + if let Some(vfs_command) = self.vfs_commands.get(&cmd) { + let current_node = if let Ok(node_id) = self.vfs.resolve_path(self.vfs.get_root(), path) + { + node_id + } else { + self.vfs.get_root() + }; + return vfs_command.execute(&mut self.vfs, current_node, parts.collect(), None, true); + } + + // Fall back to legacy Executable commands if let Some(command) = self.commands.get(&cmd) { // For now, assume not piped and output to TTY return command.execute(path, parts.collect(), None, true); @@ -252,7 +266,7 @@ impl Terminal { // which cannot be provided through the immutable Executable trait interface. // Therefore, we handle -c here in the terminal and update the HistoryCommand // with current history for other operations. - Command::History => { + Cmd::History => { let args: Vec<&str> = parts.collect(); if args.len() == 1 && args[0] == "-c" { self.history.clear(); @@ -262,10 +276,16 @@ impl Terminal { // For non-clear history commands, update the command with current history before executing HistoryCommand::new(self.history.as_slices().0).execute(path, args, None, true) } - Command::Unknown => { - let unknown_cmd = - UnknownCommand::new(self.blog_posts.clone(), cmd_text.to_string()); - unknown_cmd.execute(path, parts.collect(), None, true) + Cmd::Unknown => { + // Handle unknown commands through VFS + let unknown_cmd = UnknownCommand::new(cmd_text.to_string()); + let current_node = + if let Ok(node_id) = self.vfs.resolve_path(self.vfs.get_root(), path) { + node_id + } else { + self.vfs.get_root() + }; + unknown_cmd.execute(&mut self.vfs, current_node, parts.collect(), None, true) } // All commands should now be handled by the trait system _ => { @@ -286,123 +306,903 @@ impl Terminal { } } - pub fn handle_start_tab(&mut self, path: &str, input: &str) -> Vec { + pub fn handle_start_tab(&mut self, path: &str, input: &str) -> Vec { let mut parts = input.split_whitespace(); let cmd_text = if let Some(word) = parts.next() { word } else { return Vec::new(); }; - let cmd = Command::from(cmd_text); + let cmd = Cmd::from(cmd_text); let mut parts = parts.peekable(); + + // Get current directory in VFS + let current_dir = match self.vfs.resolve_path(self.vfs.get_root(), path) { + Ok(node_id) => node_id, + Err(_) => self.vfs.get_root(), + }; + match cmd { - Command::Unknown if parts.peek().is_none() && !input.ends_with(" ") => { + Cmd::Unknown if parts.peek().is_none() && !input.ends_with(" ") => { if cmd_text.contains("/") { - self.tab_opts(path, cmd_text) + self.vfs_tab_opts(current_dir, cmd_text) } else { self.tab_commands(cmd_text) } } _ if parts.peek().is_none() && !input.ends_with(" ") => Vec::new(), - Command::Cd => self.tab_dirs(path, parts.last().unwrap_or_default()), - _ => self.tab_opts(path, parts.last().unwrap_or_default()), + Cmd::Cd => self.vfs_tab_dirs(current_dir, parts.last().unwrap_or_default()), + _ => self.vfs_tab_opts(current_dir, parts.last().unwrap_or_default()), + } + } + + fn tab_commands(&self, cmd_text: &str) -> Vec { + let mut commands = Cmd::all() + .into_iter() + .filter(|s| s.starts_with(cmd_text)) + .map(|s| TabCompletionItem { + completion_text: s.to_string(), + is_directory: false, + is_executable: true, // Commands are executable + }) + .collect::>(); + + // Add aliases + for alias in CmdAlias::all() { + let alias_str = alias.as_str(); + if alias_str.starts_with(cmd_text) { + commands.push(TabCompletionItem { + completion_text: alias_str.to_string(), + is_directory: false, + is_executable: true, + }); + } } + + commands.sort_by(|a, b| a.completion_text.cmp(&b.completion_text)); + commands } - fn tab_opts(&self, path: &str, target_path: &str) -> Vec { + // VFS-based tab completion methods + fn vfs_tab_opts(&self, current_dir: NodeId, target_path: &str) -> Vec { let no_prefix = target_path.ends_with("/") || target_path.is_empty(); - let target_path = path_target_to_target_path(path, target_path, true); - let (target_path, prefix) = if no_prefix { - (target_path.as_ref(), "") - } else if let Some(pos) = target_path.rfind("/") { - let new_target_path = &target_path[..pos]; - let new_target_path = if new_target_path.is_empty() { - "/" + + // Split the path to get directory and prefix for completion + let (dir_path, prefix) = if no_prefix { + (target_path, "") + } else if let Some(pos) = target_path.rfind('/') { + (&target_path[..=pos], &target_path[pos + 1..]) + } else { + ("", target_path) + }; + + // Resolve the directory path + let target_dir = if dir_path.is_empty() { + Ok(current_dir) + } else { + self.vfs.resolve_path(current_dir, dir_path).or_else(|_| { + // Try without trailing slash + let trimmed = dir_path.trim_end_matches('/'); + if trimmed.is_empty() { + Ok(self.vfs.get_root()) + } else { + self.vfs.resolve_path(current_dir, trimmed) + } + }) + }; + + let target_dir = match target_dir { + Ok(node_id) => node_id, + Err(_) => return Vec::new(), + }; + + // Get entries from the directory + let entries = match self.vfs.list_directory(target_dir) { + Ok(entries) => entries, + Err(_) => return Vec::new(), + }; + + // Filter and convert entries + let mut results = Vec::new(); + for entry in entries { + if !prefix.is_empty() && !entry.name.starts_with(prefix) { + continue; + } + + // Skip hidden files unless prefix starts with '.' + if entry.name.starts_with('.') && !prefix.starts_with('.') { + continue; + } + + // The completion text should just be the entry name, not the full path + // Add appropriate suffix for display + let completion_text = if entry.is_directory { + format!("{}/", entry.name) + } else if entry.is_executable { + format!("{}*", entry.name) } else { - new_target_path + entry.name.clone() }; - (new_target_path, &target_path[pos + 1..]) + + results.push(TabCompletionItem { + completion_text, + is_directory: entry.is_directory, + is_executable: entry.is_executable, + }); + } + + results.sort_by(|a, b| a.completion_text.cmp(&b.completion_text)); + results + } + + fn vfs_tab_dirs(&self, current_dir: NodeId, target_path: &str) -> Vec { + // Similar to vfs_tab_opts but only returns directories + let results = self.vfs_tab_opts(current_dir, target_path); + results + .into_iter() + .filter(|item| item.is_directory) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Helper function to extract stdout text from CommandRes + fn get_stdout_text(res: &CommandRes) -> Option { + match res { + CommandRes::Output { stdout_text, .. } => stdout_text.clone(), + _ => None, + } + } + + // Helper function to extract stderr text from CommandRes + fn get_stderr_text(res: &CommandRes) -> Option { + match res { + CommandRes::Output { stderr_text, .. } => stderr_text.clone(), + _ => None, + } + } + + // Helper function to check if a file exists in VFS + fn vfs_file_exists(terminal: &mut Terminal, path: &str) -> bool { + // Create a temporary VFS command to access the filesystem + let result = terminal.handle_command("/", &format!("cat {}", path)); + + // If cat succeeds and no "Is a directory" error, it's a file + if !result.is_error() { + return true; + } + + // Check if error is "Is a directory" (meaning path exists but is dir) + if let Some(stderr) = get_stderr_text(&result) { + if stderr.contains("Is a directory") { + return false; // Path exists but is a directory + } + if stderr.contains("No such file or directory") { + return false; // Path doesn't exist + } + } + + false + } + + // Helper function to check if a directory exists in VFS + fn vfs_dir_exists(terminal: &mut Terminal, path: &str) -> bool { + // Try to cd to the directory and back + let current_path = "/"; // We'll assume we're testing from root + let cd_result = terminal.handle_command(current_path, &format!("cd {}", path)); + + match cd_result { + CommandRes::Redirect(_) => true, + _ => { + // Check if it's a file by trying cat + let cat_result = terminal.handle_command(current_path, &format!("cat {}", path)); + if let Some(stderr) = get_stderr_text(&cat_result) { + stderr.contains("Is a directory") + } else { + false + } + } + } + } + + // Helper function to check if a specific item exists in a directory + fn vfs_contains_file(terminal: &mut Terminal, dir_path: &str, filename: &str) -> bool { + let full_path = if dir_path == "/" { + format!("/{}", filename) } else { - return Vec::new(); + format!("{}/{}", dir_path, filename) }; - let target = Target::from_str(target_path, &self.blog_posts); - match target { - Target::Dir(d) => d - .contents(&self.blog_posts, prefix.starts_with(".")) - .into_iter() - .filter(|item| { - item.0.starts_with(prefix) - && (item.0 != prefix || matches!(item.1, Target::Dir(_))) - }) - .map(|item| { - // Add appropriate suffix for display - let display_name = match &item.1 { - Target::Dir(_) => format!("{}/", item.0), - Target::File(_) if item.1.is_executable() => format!("{}*", item.0), - _ => item.0.clone(), - }; - fs::DirContentItem(display_name, item.1) - }) - .collect(), - _ => Vec::new(), + vfs_file_exists(terminal, &full_path) || vfs_dir_exists(terminal, &full_path) + } + + #[test] + fn test_cd_integration() { + let blog_posts = vec!["test-post".to_string(), "another-post".to_string()]; + let mut terminal = Terminal::new(&blog_posts, None); + + // Test root directory contents via VFS + assert!(vfs_dir_exists(&mut terminal, "/blog")); + assert!(vfs_dir_exists(&mut terminal, "/cv")); + assert!(vfs_file_exists(&mut terminal, "/mines.sh")); + + // Change to blog directory + let cd_result = terminal.handle_command("/", "cd blog"); + if let CommandRes::Redirect(path) = cd_result { + assert_eq!(path, "/blog"); + } else { + panic!("cd should return a redirect"); + } + + // Verify blog directory contents by checking individual files exist + assert!(vfs_contains_file(&mut terminal, "/blog", "test-post")); + assert!(vfs_contains_file(&mut terminal, "/blog", "another-post")); + assert!(vfs_contains_file(&mut terminal, "/blog", "nav.rs")); + + // Test relative navigation + let relative_cd = terminal.handle_command("/blog", "cd .."); + if let CommandRes::Redirect(path) = relative_cd { + assert_eq!(path, "/"); + } else { + panic!("cd .. should return a redirect to root"); + } + + // Test cd to specific blog post + let post_cd = terminal.handle_command("/blog", "cd test-post"); + if let CommandRes::Redirect(path) = post_cd { + assert_eq!(path, "/blog/test-post"); + } else { + panic!("cd test-post should redirect"); } } - fn tab_dirs(&self, path: &str, target_path: &str) -> Vec { - let no_prefix = target_path.ends_with("/") || target_path.is_empty(); - let target_path = path_target_to_target_path(path, target_path, true); - let (target_path, prefix) = if no_prefix { - (target_path.as_ref(), "") - } else if let Some(pos) = target_path.rfind("/") { - let new_target_path = &target_path[..pos]; - let new_target_path = if new_target_path.is_empty() { - "/" - } else { - new_target_path - }; - (new_target_path, &target_path[pos + 1..]) + #[test] + fn test_path_navigation() { + let blog_posts = vec!["my-post".to_string()]; + let mut terminal = Terminal::new(&blog_posts, None); + + // Test absolute path navigation + let abs_cd = terminal.handle_command("/", "cd /blog/my-post"); + if let CommandRes::Redirect(path) = abs_cd { + assert_eq!(path, "/blog/my-post"); } else { - return Vec::new(); - }; - let target = Target::from_str(target_path, &self.blog_posts); - match target { - Target::Dir(d) => d - .contents(&self.blog_posts, prefix.starts_with(".")) - .into_iter() - .filter_map(|item| { - // Only include directories - if matches!(item.1, Target::Dir(_)) && item.0.starts_with(prefix) { - // Add "/" suffix to indicate it's a directory - let display_name = format!("{}/", item.0); - Some(fs::DirContentItem(display_name, item.1)) - } else { - None - } - }) - .collect(), - _ => Vec::new(), + panic!("absolute cd should work"); + } + + // Test .. from deep directory + let up_cd = terminal.handle_command("/blog/my-post", "cd ../.."); + if let CommandRes::Redirect(path) = up_cd { + assert_eq!(path, "/"); + } else { + panic!("cd ../.. should go to root"); + } + + // Test ~ expansion + let home_cd = terminal.handle_command("/blog", "cd ~"); + if let CommandRes::Redirect(path) = home_cd { + assert_eq!(path, "/"); + } else { + panic!("cd ~ should go to root"); + } + + // Test ~/path expansion + let home_path_cd = terminal.handle_command("/blog", "cd ~/cv"); + if let CommandRes::Redirect(path) = home_path_cd { + assert_eq!(path, "/cv"); + } else { + panic!("cd ~/cv should work"); } } - fn tab_commands(&self, cmd_text: &str) -> Vec { - let mut commands = Command::all() - .into_iter() - .filter(|s| s.starts_with(cmd_text)) - .map(|s| fs::DirContentItem(s.to_string(), Target::File(fs::File::MinesSh))) // Use executable as dummy type - .collect::>(); + #[test] + fn test_cat_command() { + let blog_posts = vec!["test-post".to_string()]; + let mut terminal = Terminal::new(&blog_posts, None); - // Add aliases - for alias in CommandAlias::all() { - let alias_str = alias.as_str(); - if alias_str.starts_with(cmd_text) { - commands.push(fs::DirContentItem( - alias_str.to_string(), - Target::File(fs::File::MinesSh), - )); - } + // Test cat on existing file + let cat_result = terminal.handle_command("/", "cat thanks.txt"); + assert!(!cat_result.is_error()); + let cat_output = get_stdout_text(&cat_result).unwrap_or_default(); + assert!(cat_output.contains("Thank you")); + + // Test cat on directory (should fail) + let cat_dir = terminal.handle_command("/", "cat blog"); + assert!(cat_dir.is_error()); + let error_msg = get_stderr_text(&cat_dir).unwrap_or_default(); + assert!(error_msg.contains("Is a directory")); + + // Test cat on non-existent file + let cat_missing = terminal.handle_command("/", "cat nonexistent.txt"); + assert!(cat_missing.is_error()); + let error_msg = get_stderr_text(&cat_missing).unwrap_or_default(); + assert!(error_msg.contains("No such file or directory")); + } + + #[test] + fn test_file_creation_deletion() { + let blog_posts = vec!["test-post".to_string()]; + let mut terminal = Terminal::new(&blog_posts, None); + + // Create a file with touch + let touch_result = terminal.handle_command("/", "touch newfile.txt"); + assert!(!touch_result.is_error()); + + // Verify file exists via VFS + assert!(vfs_file_exists(&mut terminal, "/newfile.txt")); + + // Remove the file + let rm_result = terminal.handle_command("/", "rm newfile.txt"); + assert!(!rm_result.is_error()); + + // Verify file is gone via VFS + assert!(!vfs_file_exists(&mut terminal, "/newfile.txt")); + } + + #[test] + fn test_directory_operations() { + let blog_posts = vec!["test-post".to_string()]; + let mut terminal = Terminal::new(&blog_posts, None); + + // Create a directory + let mkdir_result = terminal.handle_command("/", "mkdir testdir"); + assert!(!mkdir_result.is_error()); + + // Verify directory exists via VFS + assert!(vfs_dir_exists(&mut terminal, "/testdir")); + + // Create a file in the directory + let touch_in_dir = terminal.handle_command("/", "touch testdir/file.txt"); + assert!(!touch_in_dir.is_error()); + + // Verify file exists in directory via VFS + assert!(vfs_file_exists(&mut terminal, "/testdir/file.txt")); + assert!(vfs_contains_file(&mut terminal, "/testdir", "file.txt")); + + // Try to remove non-empty directory without -r (should fail) + let rm_fail = terminal.handle_command("/", "rm testdir"); + assert!(rm_fail.is_error()); + let error_msg = get_stderr_text(&rm_fail).unwrap_or_default(); + assert!(error_msg.contains("Is a directory")); + + // Remove directory with -r + let rm_recursive = terminal.handle_command("/", "rm -r testdir"); + assert!(!rm_recursive.is_error()); + + // Verify directory is gone via VFS + assert!(!vfs_dir_exists(&mut terminal, "/testdir")); + } + + #[test] + fn test_cp_mv_operations() { + let blog_posts = vec!["test-post".to_string()]; + let mut terminal = Terminal::new(&blog_posts, None); + + // Create a test file + terminal.handle_command("/", "touch original.txt"); + assert!(vfs_file_exists(&mut terminal, "/original.txt")); + + // Copy the file + let cp_result = terminal.handle_command("/", "cp original.txt copy.txt"); + assert!(!cp_result.is_error()); + + // Verify both files exist via VFS + assert!(vfs_file_exists(&mut terminal, "/original.txt")); + assert!(vfs_file_exists(&mut terminal, "/copy.txt")); + + // Move the copy + let mv_result = terminal.handle_command("/", "mv copy.txt renamed.txt"); + assert!(!mv_result.is_error()); + + // Verify move worked via VFS + assert!(vfs_file_exists(&mut terminal, "/original.txt")); + assert!(!vfs_file_exists(&mut terminal, "/copy.txt")); + assert!(vfs_file_exists(&mut terminal, "/renamed.txt")); + + // Test directory copy + terminal.handle_command("/", "mkdir sourcedir"); + terminal.handle_command("/", "touch sourcedir/file1.txt"); + terminal.handle_command("/", "touch sourcedir/file2.txt"); + + // Verify source directory setup via VFS + assert!(vfs_dir_exists(&mut terminal, "/sourcedir")); + assert!(vfs_file_exists(&mut terminal, "/sourcedir/file1.txt")); + assert!(vfs_file_exists(&mut terminal, "/sourcedir/file2.txt")); + + let cp_dir_result = terminal.handle_command("/", "cp -r sourcedir destdir"); + assert!(!cp_dir_result.is_error()); + + // Verify directory was copied via VFS + assert!(vfs_dir_exists(&mut terminal, "/destdir")); + assert!(vfs_file_exists(&mut terminal, "/destdir/file1.txt")); + assert!(vfs_file_exists(&mut terminal, "/destdir/file2.txt")); + assert!(vfs_contains_file(&mut terminal, "/destdir", "file1.txt")); + assert!(vfs_contains_file(&mut terminal, "/destdir", "file2.txt")); + } + + #[test] + fn test_permission_errors() { + let blog_posts = vec!["test-post".to_string()]; + let mut terminal = Terminal::new(&blog_posts, None); + + // Verify system file exists first + assert!(vfs_file_exists(&mut terminal, "/mines.sh")); + + // Try to remove system file (should fail) + let rm_system = terminal.handle_command("/", "rm mines.sh"); + assert!(rm_system.is_error()); + let error_msg = get_stderr_text(&rm_system).unwrap_or_default(); + assert!(error_msg.contains("Permission denied")); + + // Verify system file still exists after failed removal + assert!(vfs_file_exists(&mut terminal, "/mines.sh")); + + // Try to move system directory (should fail) + let mv_system = terminal.handle_command("/", "mv blog renamed_blog"); + assert!(mv_system.is_error()); + let mv_error = get_stderr_text(&mv_system).unwrap_or_default(); + assert!(mv_error.contains("Permission denied")); + + // Verify blog directory still exists at original location + assert!(vfs_dir_exists(&mut terminal, "/blog")); + assert!(!vfs_dir_exists(&mut terminal, "/renamed_blog")); + + // We can create files in immutable directories + let touch_in_blog = terminal.handle_command("/", "touch blog/userfile.txt"); + if touch_in_blog.is_error() { + let error = get_stderr_text(&touch_in_blog).unwrap_or_default(); + panic!("touch in blog failed with error: {}", error); } - commands.sort_by(|a, b| a.0.cmp(&b.0)); - commands + // Verify the file was created + assert!(vfs_file_exists(&mut terminal, "/blog/userfile.txt")); + + // But we can't delete the directory itself + let rm_blog = terminal.handle_command("/", "rm -r blog"); + assert!(rm_blog.is_error()); + + // Verify blog directory still exists + assert!(vfs_dir_exists(&mut terminal, "/blog")); + } + + #[test] + fn test_ls_options() { + let blog_posts = vec!["test-post".to_string()]; + let mut terminal = Terminal::new(&blog_posts, None); + + // Test that hidden files exist in VFS + assert!(vfs_file_exists(&mut terminal, "/.zshrc")); + + // Test ls -a functionality (we just verify command doesn't error) + let ls_all = terminal.handle_command("/", "ls -a"); + assert!(!ls_all.is_error()); + + // Test ls without -a + let ls_normal = terminal.handle_command("/", "ls"); + assert!(!ls_normal.is_error()); + + // Verify contents of blog and cv directories via individual file checks + assert!(vfs_contains_file(&mut terminal, "/blog", "test-post")); + assert!(vfs_contains_file(&mut terminal, "/blog", "nav.rs")); + assert!(vfs_contains_file(&mut terminal, "/cv", "nav.rs")); + + // Test ls with multiple targets + let ls_multi = terminal.handle_command("/", "ls blog cv"); + assert!(!ls_multi.is_error()); + } + + #[test] + fn test_command_errors() { + let blog_posts = vec!["test-post".to_string()]; + let mut terminal = Terminal::new(&blog_posts, None); + + // Test invalid cd path + let cd_invalid = terminal.handle_command("/", "cd nonexistent"); + assert!(cd_invalid.is_error()); + let cd_error = get_stderr_text(&cd_invalid).unwrap_or_default(); + assert!(cd_error.contains("no such file or directory")); + + // Test cd to file + let cd_file = terminal.handle_command("/", "cd mines.sh"); + assert!(cd_file.is_error()); + let cd_file_error = get_stderr_text(&cd_file).unwrap_or_default(); + assert!(cd_file_error.contains("not a directory")); + + // Test touch with no arguments + let touch_no_args = terminal.handle_command("/", "touch"); + assert!(touch_no_args.is_error()); + let touch_error = get_stderr_text(&touch_no_args).unwrap_or_default(); + assert!(touch_error.contains("missing file operand")); + + // Test cp with missing destination + let cp_no_dest = terminal.handle_command("/", "cp file.txt"); + assert!(cp_no_dest.is_error()); + let cp_error = get_stderr_text(&cp_no_dest).unwrap_or_default(); + assert!(cp_error.contains("missing destination")); + } + + #[test] + fn test_which_command() { + let blog_posts = vec!["test-post".to_string()]; + let mut terminal = Terminal::new(&blog_posts, None); + + // Test which for builtin command + let which_cd = terminal.handle_command("/", "which cd"); + assert!(!which_cd.is_error()); + let cd_output = get_stdout_text(&which_cd).unwrap_or_default(); + assert!(cd_output.contains("shell builtin")); + + // Test which for external command + let which_ls = terminal.handle_command("/", "which ls"); + assert!(!which_ls.is_error()); + let ls_output = get_stdout_text(&which_ls).unwrap_or_default(); + assert!(ls_output.contains("/bin/ls")); + + // Test which for alias + let which_ll = terminal.handle_command("/", "which ll"); + assert!(!which_ll.is_error()); + let ll_output = get_stdout_text(&which_ll).unwrap_or_default(); + assert!(ll_output.contains("aliased to")); + + // Test which for non-existent command + let which_fake = terminal.handle_command("/", "which fakecmd"); + assert!(which_fake.is_error()); + let fake_output = get_stdout_text(&which_fake).unwrap_or_default(); + assert!(fake_output.contains("not found")); + } + + #[test] + fn test_echo_pwd_commands() { + let blog_posts = vec!["test-post".to_string()]; + let mut terminal = Terminal::new(&blog_posts, None); + + // Test pwd at root + let pwd_root = terminal.handle_command("/", "pwd"); + assert!(!pwd_root.is_error()); + let pwd_output = get_stdout_text(&pwd_root).unwrap_or_default(); + assert_eq!(pwd_output.trim(), "/"); + + // Test pwd in subdirectory + let pwd_blog = terminal.handle_command("/blog", "pwd"); + assert!(!pwd_blog.is_error()); + let pwd_blog_output = get_stdout_text(&pwd_blog).unwrap_or_default(); + assert_eq!(pwd_blog_output.trim(), "/blog"); + + // Test echo + let echo_result = terminal.handle_command("/", "echo hello world"); + assert!(!echo_result.is_error()); + let echo_output = get_stdout_text(&echo_result).unwrap_or_default(); + assert_eq!(echo_output.trim(), "hello world"); + + // Test echo with no args + let echo_empty = terminal.handle_command("/", "echo"); + assert!(!echo_empty.is_error()); + let empty_output = get_stdout_text(&echo_empty).unwrap_or_default(); + assert_eq!(empty_output.trim(), ""); + } + + #[test] + fn test_cp_mv_basic_functionality() { + let blog_posts = vec!["hello_world".to_string(), "rust_tips".to_string()]; + let mut terminal = Terminal::new(&blog_posts, None); + + // Test touch to create a file + let touch_result = terminal.handle_command("/", "touch testfile.txt"); + assert!(!touch_result.is_error()); + assert!(vfs_file_exists(&mut terminal, "/testfile.txt")); + + // Test cp to copy the file + let cp_result = terminal.handle_command("/", "cp testfile.txt testfile_copy.txt"); + assert!(!cp_result.is_error()); + + // Verify both files exist via VFS + assert!(vfs_file_exists(&mut terminal, "/testfile.txt")); + assert!(vfs_file_exists(&mut terminal, "/testfile_copy.txt")); + + // Test mv to rename a file + let mv_result = terminal.handle_command("/", "mv testfile_copy.txt renamed_file.txt"); + assert!(!mv_result.is_error()); + + // Verify move worked via VFS + assert!(vfs_file_exists(&mut terminal, "/testfile.txt")); // Original should still exist + assert!(!vfs_file_exists(&mut terminal, "/testfile_copy.txt")); // Copy should be gone + assert!(vfs_file_exists(&mut terminal, "/renamed_file.txt")); // New name should exist + + // Test mkdir and cp with directories + let mkdir_result = terminal.handle_command("/", "mkdir testdir"); + assert!(!mkdir_result.is_error()); + assert!(vfs_dir_exists(&mut terminal, "/testdir")); + + let cp_to_dir_result = terminal.handle_command("/", "cp testfile.txt testdir/"); + assert!(!cp_to_dir_result.is_error()); + assert!(vfs_file_exists(&mut terminal, "/testdir/testfile.txt")); + + // Test recursive directory copy + let cp_recursive_result = terminal.handle_command("/", "cp -r testdir testdir_copy"); + assert!(!cp_recursive_result.is_error()); + assert!(vfs_dir_exists(&mut terminal, "/testdir_copy")); + assert!(vfs_file_exists(&mut terminal, "/testdir_copy/testfile.txt")); + } + + #[test] + fn test_cp_file_to_file_happy_path() { + let blog_posts = vec![]; + let mut terminal = Terminal::new(&blog_posts, None); + + // Create source file with content + terminal.handle_command("/", "touch source.txt"); + assert!(vfs_file_exists(&mut terminal, "/source.txt")); + + // Copy file to new name + let cp_result = terminal.handle_command("/", "cp source.txt destination.txt"); + assert!(!cp_result.is_error()); + + // Verify both files exist + assert!(vfs_file_exists(&mut terminal, "/source.txt")); + assert!(vfs_file_exists(&mut terminal, "/destination.txt")); + + // Verify we can read both files + let cat_source = terminal.handle_command("/", "cat source.txt"); + assert!(!cat_source.is_error()); + let cat_dest = terminal.handle_command("/", "cat destination.txt"); + assert!(!cat_dest.is_error()); + } + + #[test] + fn test_cp_file_to_directory_happy_path() { + let blog_posts = vec![]; + let mut terminal = Terminal::new(&blog_posts, None); + + // Create source file and target directory + terminal.handle_command("/", "touch file.txt"); + terminal.handle_command("/", "mkdir targetdir"); + assert!(vfs_file_exists(&mut terminal, "/file.txt")); + assert!(vfs_dir_exists(&mut terminal, "/targetdir")); + + // Copy file to directory + let cp_result = terminal.handle_command("/", "cp file.txt targetdir/"); + assert!(!cp_result.is_error()); + + // Verify original file still exists and copy exists in target directory + assert!(vfs_file_exists(&mut terminal, "/file.txt")); + assert!(vfs_file_exists(&mut terminal, "/targetdir/file.txt")); + } + + #[test] + fn test_cp_directory_recursive_happy_path() { + let blog_posts = vec![]; + let mut terminal = Terminal::new(&blog_posts, None); + + // Create source directory structure + terminal.handle_command("/", "mkdir sourcedir"); + terminal.handle_command("/", "touch sourcedir/file1.txt"); + terminal.handle_command("/", "touch sourcedir/file2.txt"); + terminal.handle_command("/", "mkdir sourcedir/subdir"); + terminal.handle_command("/", "touch sourcedir/subdir/nested.txt"); + + // Verify source structure + assert!(vfs_dir_exists(&mut terminal, "/sourcedir")); + assert!(vfs_file_exists(&mut terminal, "/sourcedir/file1.txt")); + assert!(vfs_file_exists(&mut terminal, "/sourcedir/file2.txt")); + assert!(vfs_dir_exists(&mut terminal, "/sourcedir/subdir")); + assert!(vfs_file_exists( + &mut terminal, + "/sourcedir/subdir/nested.txt" + )); + + // Recursively copy directory + let cp_result = terminal.handle_command("/", "cp -r sourcedir targetdir"); + assert!(!cp_result.is_error()); + + // Verify original structure still exists + assert!(vfs_dir_exists(&mut terminal, "/sourcedir")); + assert!(vfs_file_exists(&mut terminal, "/sourcedir/file1.txt")); + assert!(vfs_file_exists(&mut terminal, "/sourcedir/file2.txt")); + assert!(vfs_dir_exists(&mut terminal, "/sourcedir/subdir")); + assert!(vfs_file_exists( + &mut terminal, + "/sourcedir/subdir/nested.txt" + )); + + // Verify copied structure exists + assert!(vfs_dir_exists(&mut terminal, "/targetdir")); + assert!(vfs_file_exists(&mut terminal, "/targetdir/file1.txt")); + assert!(vfs_file_exists(&mut terminal, "/targetdir/file2.txt")); + assert!(vfs_dir_exists(&mut terminal, "/targetdir/subdir")); + assert!(vfs_file_exists( + &mut terminal, + "/targetdir/subdir/nested.txt" + )); + } + + #[test] + fn test_mv_file_rename_happy_path() { + let blog_posts = vec![]; + let mut terminal = Terminal::new(&blog_posts, None); + + // Create source file + terminal.handle_command("/", "touch original.txt"); + assert!(vfs_file_exists(&mut terminal, "/original.txt")); + + // Move/rename file + let mv_result = terminal.handle_command("/", "mv original.txt renamed.txt"); + assert!(!mv_result.is_error()); + + // Verify original is gone and new name exists + assert!(!vfs_file_exists(&mut terminal, "/original.txt")); + assert!(vfs_file_exists(&mut terminal, "/renamed.txt")); + + // Verify we can read the renamed file + let cat_result = terminal.handle_command("/", "cat renamed.txt"); + assert!(!cat_result.is_error()); + } + + #[test] + fn test_mv_file_to_directory_happy_path() { + let blog_posts = vec![]; + let mut terminal = Terminal::new(&blog_posts, None); + + // Create source file and target directory + terminal.handle_command("/", "touch moveme.txt"); + terminal.handle_command("/", "mkdir targetdir"); + assert!(vfs_file_exists(&mut terminal, "/moveme.txt")); + assert!(vfs_dir_exists(&mut terminal, "/targetdir")); + + // Move file to directory + let mv_result = terminal.handle_command("/", "mv moveme.txt targetdir/"); + assert!(!mv_result.is_error()); + + // Verify original is gone and file exists in target directory + assert!(!vfs_file_exists(&mut terminal, "/moveme.txt")); + assert!(vfs_file_exists(&mut terminal, "/targetdir/moveme.txt")); + } + + #[test] + fn test_mv_directory_rename_happy_path() { + let blog_posts = vec![]; + let mut terminal = Terminal::new(&blog_posts, None); + + // Create source directory with contents + terminal.handle_command("/", "mkdir olddir"); + terminal.handle_command("/", "touch olddir/file.txt"); + assert!(vfs_dir_exists(&mut terminal, "/olddir")); + assert!(vfs_file_exists(&mut terminal, "/olddir/file.txt")); + + // Move/rename directory + let mv_result = terminal.handle_command("/", "mv olddir newdir"); + assert!(!mv_result.is_error()); + + // Verify original is gone and new name exists with contents + assert!(!vfs_dir_exists(&mut terminal, "/olddir")); + assert!(vfs_dir_exists(&mut terminal, "/newdir")); + assert!(vfs_file_exists(&mut terminal, "/newdir/file.txt")); + } + + #[test] + fn test_cp_mv_error_cases() { + let blog_posts = vec!["hello_world".to_string()]; + let mut terminal = Terminal::new(&blog_posts, None); + + // Test cp with missing source + let cp_missing_result = terminal.handle_command("/", "cp nonexistent.txt copy.txt"); + assert!(cp_missing_result.is_error()); + let error_msg = get_stderr_text(&cp_missing_result).unwrap_or_default(); + assert!(error_msg.contains("No such file or directory")); + + // Test cp directory without -r flag + let cp_dir_result = terminal.handle_command("/", "cp blog copy_blog"); + assert!(cp_dir_result.is_error()); + let error_msg = get_stderr_text(&cp_dir_result).unwrap_or_default(); + assert!( + error_msg.contains("Is a directory") + || error_msg.contains("-r not specified") + || error_msg.contains("omitting directory") + ); + + // Test mv with missing source + let mv_missing_result = terminal.handle_command("/", "mv nonexistent.txt moved.txt"); + assert!(mv_missing_result.is_error()); + let error_msg = get_stderr_text(&mv_missing_result).unwrap_or_default(); + assert!(error_msg.contains("No such file or directory")); + + // Test mv system directory (should fail due to immutable permissions) + let mv_system_result = terminal.handle_command("/", "mv blog moved_blog"); + assert!(mv_system_result.is_error()); + let error_msg = get_stderr_text(&mv_system_result).unwrap_or_default(); + assert!(error_msg.contains("Permission denied")); + + // Verify system directory still exists after failed move + assert!(vfs_dir_exists(&mut terminal, "/blog")); + assert!(!vfs_dir_exists(&mut terminal, "/moved_blog")); + + // Test cp with missing destination arguments + let cp_no_dest = terminal.handle_command("/", "cp onlyarg"); + assert!(cp_no_dest.is_error()); + let error_msg = get_stderr_text(&cp_no_dest).unwrap_or_default(); + assert!(error_msg.contains("missing destination")); + + // Test mv with missing destination arguments + let mv_no_dest = terminal.handle_command("/", "mv onlyarg"); + assert!(mv_no_dest.is_error()); + let error_msg = get_stderr_text(&mv_no_dest).unwrap_or_default(); + assert!(error_msg.contains("missing destination") || error_msg.contains("missing operand")); + } + + #[test] + fn test_cp_mv_complex_paths_happy_path() { + let blog_posts = vec![]; + let mut terminal = Terminal::new(&blog_posts, None); + + // Create nested directory structure + terminal.handle_command("/", "mkdir src"); + terminal.handle_command("/", "mkdir src/utils"); + terminal.handle_command("/", "mkdir dest"); + terminal.handle_command("/", "mkdir dest/backup"); + terminal.handle_command("/", "touch src/main.rs"); + terminal.handle_command("/", "touch src/utils/helper.rs"); + + // Verify setup + assert!(vfs_dir_exists(&mut terminal, "/src")); + assert!(vfs_dir_exists(&mut terminal, "/src/utils")); + assert!(vfs_dir_exists(&mut terminal, "/dest")); + assert!(vfs_dir_exists(&mut terminal, "/dest/backup")); + assert!(vfs_file_exists(&mut terminal, "/src/main.rs")); + assert!(vfs_file_exists(&mut terminal, "/src/utils/helper.rs")); + + // Copy file with relative path + let cp_result = terminal.handle_command("/src", "cp main.rs ../dest/"); + assert!(!cp_result.is_error()); + assert!(vfs_file_exists(&mut terminal, "/dest/main.rs")); + assert!(vfs_file_exists(&mut terminal, "/src/main.rs")); // Original should remain + + // Move file from nested location + let mv_result = terminal.handle_command("/src/utils", "mv helper.rs ../../dest/backup/"); + assert!(!mv_result.is_error()); + assert!(!vfs_file_exists(&mut terminal, "/src/utils/helper.rs")); // Original should be gone + assert!(vfs_file_exists(&mut terminal, "/dest/backup/helper.rs")); + + // Copy entire directory structure + let cp_recursive = terminal.handle_command("/", "cp -r src src_backup"); + assert!(!cp_recursive.is_error()); + assert!(vfs_dir_exists(&mut terminal, "/src_backup")); + assert!(vfs_dir_exists(&mut terminal, "/src_backup/utils")); + assert!(vfs_file_exists(&mut terminal, "/src_backup/main.rs")); + // Note: helper.rs was moved out, so it shouldn't be in the backup + } + + #[test] + fn test_cp_mv_overwrite_behavior() { + let blog_posts = vec![]; + let mut terminal = Terminal::new(&blog_posts, None); + + // Create a file and copy it to a new location + terminal.handle_command("/", "touch file1.txt"); + assert!(vfs_file_exists(&mut terminal, "/file1.txt")); + + // Copy file1 to a new name (this should work) + let cp_result = terminal.handle_command("/", "cp file1.txt file2.txt"); + assert!(!cp_result.is_error()); + + // Both files should exist + assert!(vfs_file_exists(&mut terminal, "/file1.txt")); + assert!(vfs_file_exists(&mut terminal, "/file2.txt")); + + // Try to copy file1 to file2 again (should fail - file exists) + let cp_overwrite = terminal.handle_command("/", "cp file1.txt file2.txt"); + assert!(cp_overwrite.is_error()); + let error_msg = get_stderr_text(&cp_overwrite).unwrap_or_default(); + assert!(error_msg.contains("File exists")); + + // Create a third file and move it to overwrite file1 (mv should work) + terminal.handle_command("/", "touch file3.txt"); + assert!(vfs_file_exists(&mut terminal, "/file3.txt")); + + let mv_result = terminal.handle_command("/", "mv file3.txt file1.txt"); + assert!(!mv_result.is_error()); + + // file3 should be gone, file1 should still exist (overwritten by mv) + assert!(!vfs_file_exists(&mut terminal, "/file3.txt")); + assert!(vfs_file_exists(&mut terminal, "/file1.txt")); } } diff --git a/src/app/terminal/command.rs b/src/app/terminal/command.rs index 0dcdbe8..b1beb26 100644 --- a/src/app/terminal/command.rs +++ b/src/app/terminal/command.rs @@ -1,17 +1,37 @@ #![allow(dead_code)] +use super::vfs::VirtualFilesystem; +use indextree::NodeId; use leptos::prelude::*; -pub trait Executable: Send + Sync { - fn execute(&self, path: &str, args: Vec<&str>, stdin: Option<&str>, is_output_tty: bool) -> CommandRes; +pub trait Command: Send + Sync { + fn execute( + &self, + path: &str, + args: Vec<&str>, + stdin: Option<&str>, + is_output_tty: bool, + ) -> CommandRes; +} + +/// VFS-aware command trait for commands that need direct filesystem access +pub trait VfsCommand: Send + Sync { + fn execute( + &self, + vfs: &mut VirtualFilesystem, + current_dir: NodeId, + args: Vec<&str>, + stdin: Option<&str>, + is_tty: bool, + ) -> CommandRes; } pub enum CommandRes { Output { - is_err: bool, // true if command failed (non-zero exit code) - stdout_view: Option, // stdout content for display (only set if is_output_tty) - stdout_text: Option, // stdout for piping - stderr_text: Option, // stderr text (Header converts to view) + is_err: bool, // true if command failed (non-zero exit code) + stdout_view: Option, // stdout content for display (only set if is_output_tty) + stdout_text: Option, // stdout for piping + stderr_text: Option, // stderr text (Header converts to view) }, Redirect(String), } @@ -34,11 +54,12 @@ impl CommandRes { /// Mark this result as an error pub fn with_error(mut self) -> Self { - if let Self::Output { is_err, .. } = &mut self { *is_err = true } + if let Self::Output { is_err, .. } = &mut self { + *is_err = true + } self } - /// Add stderr content (text only - Header handles view conversion) pub fn with_stderr(mut self, text: impl Into) -> Self { if let Self::Output { stderr_text, .. } = &mut self { @@ -49,7 +70,9 @@ impl CommandRes { /// Add only stdout text (no view) pub fn with_stdout_text(mut self, text: impl Into) -> Self { - if let Self::Output { stdout_text, .. } = &mut self { *stdout_text = Some(text.into()) } + if let Self::Output { stdout_text, .. } = &mut self { + *stdout_text = Some(text.into()) + } self } @@ -60,14 +83,23 @@ impl CommandRes { /// Add only stdout view (no text) pub fn with_stdout_view(mut self, view: ChildrenFn) -> Self { - if let Self::Output { stdout_view, .. } = &mut self { *stdout_view = Some(view) } + if let Self::Output { stdout_view, .. } = &mut self { + *stdout_view = Some(view) + } self } + /// Check if this result represents an error + pub fn is_error(&self) -> bool { + match self { + Self::Output { is_err, .. } => *is_err, + Self::Redirect(_) => false, + } + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum Command { +pub enum Cmd { Help, Pwd, Ls, @@ -93,82 +125,157 @@ pub enum Command { Unknown, } -impl From<&str> for Command { +impl From<&str> for Cmd { fn from(value: &str) -> Self { - match value { - "help" => Self::Help, - "pwd" => Self::Pwd, - "ls" => Self::Ls, - "cd" => Self::Cd, - "cat" => Self::Cat, - "clear" => Self::Clear, - "cp" => Self::Cp, - "date" => Self::Date, - "echo" => Self::Echo, - "history" => Self::History, - "mines" => Self::Mines, - "mkdir" => Self::MkDir, - "mv" => Self::Mv, - "rm" => Self::Rm, - "touch" => Self::Touch, - "which" => Self::Which, - "whoami" => Self::WhoAmI, - "neofetch" => Self::Neofetch, - "sudo" => Self::Sudo, - "uptime" => Self::Uptime, - "ps" => Self::Ps, - "kill" => Self::Kill, - _ => Self::Unknown, - } + Self::from_str(value).unwrap_or(Self::Unknown) } } -impl Command { +impl Cmd { pub fn all() -> Vec<&'static str> { vec![ "help", "pwd", "ls", "cd", "cat", "clear", "cp", "date", "echo", "history", "mines", "mkdir", "mv", "rm", "touch", "which", "whoami", "neofetch", "uptime", "ps", "kill", ] } + + pub fn from_str(s: &str) -> Option { + match s { + "help" => Some(Self::Help), + "pwd" => Some(Self::Pwd), + "ls" => Some(Self::Ls), + "cd" => Some(Self::Cd), + "cat" => Some(Self::Cat), + "clear" => Some(Self::Clear), + "cp" => Some(Self::Cp), + "date" => Some(Self::Date), + "echo" => Some(Self::Echo), + "history" => Some(Self::History), + "mines" => Some(Self::Mines), + "mkdir" => Some(Self::MkDir), + "mv" => Some(Self::Mv), + "rm" => Some(Self::Rm), + "touch" => Some(Self::Touch), + "which" => Some(Self::Which), + "whoami" => Some(Self::WhoAmI), + "neofetch" => Some(Self::Neofetch), + "sudo" => Some(Self::Sudo), + "uptime" => Some(Self::Uptime), + "ps" => Some(Self::Ps), + "kill" => Some(Self::Kill), + _ => None, + } + } + + /// Returns the simulated filesystem path for this command + pub fn simulated_path(&self) -> Option { + match self { + // Shell builtins don't have paths + Self::Pwd | Self::Cd | Self::Echo | Self::History => None, + + // Core system utilities (typically in /bin) + Self::Ls | Self::Cat | Self::Cp | Self::Mv | Self::Rm | Self::MkDir | Self::Touch => { + Some(format!("/bin/{}", self.as_str())) + } + + // System administration and process tools (typically in /usr/bin) + Self::Ps | Self::Kill | Self::WhoAmI | Self::Which | Self::Uptime => { + Some(format!("/usr/bin/{}", self.as_str())) + } + + // Terminal/display utilities (typically in /usr/bin) + Self::Clear | Self::Date => { + Some(format!("/usr/bin/{}", self.as_str())) + } + + // Custom/third-party applications (typically in /usr/local/bin) + Self::Neofetch | Self::Mines => { + Some(format!("/usr/local/bin/{}", self.as_str())) + } + + // Documentation/help (typically in /usr/bin) + Self::Help => Some(format!("/usr/bin/{}", self.as_str())), + + // System utilities (typically in /usr/bin) + Self::Sudo => Some(format!("/usr/bin/{}", self.as_str())), + + // Unknown commands don't have paths + Self::Unknown => None, + } + } + + /// Returns the command name as a string + pub fn as_str(&self) -> &'static str { + match self { + Self::Help => "help", + Self::Pwd => "pwd", + Self::Ls => "ls", + Self::Cd => "cd", + Self::Cat => "cat", + Self::Clear => "clear", + Self::Cp => "cp", + Self::Date => "date", + Self::Echo => "echo", + Self::History => "history", + Self::Mines => "mines", + Self::MkDir => "mkdir", + Self::Mv => "mv", + Self::Rm => "rm", + Self::Touch => "touch", + Self::Which => "which", + Self::WhoAmI => "whoami", + Self::Neofetch => "neofetch", + Self::Sudo => "sudo", + Self::Uptime => "uptime", + Self::Ps => "ps", + Self::Kill => "kill", + Self::Unknown => "unknown", + } + } + + /// Returns true if this command is a shell builtin + pub fn is_builtin(&self) -> bool { + matches!(self, Self::Pwd | Self::Cd | Self::Echo | Self::History) + } } #[derive(Debug, Clone)] -pub enum CommandAlias { +pub enum CmdAlias { Ll, La, H, } -impl CommandAlias { - pub fn all() -> Vec { - vec![CommandAlias::Ll, CommandAlias::La, CommandAlias::H] +impl CmdAlias { + pub fn all() -> Vec { + vec![CmdAlias::Ll, CmdAlias::La, CmdAlias::H] } pub fn as_str(&self) -> &'static str { match self { - CommandAlias::Ll => "ll", - CommandAlias::La => "la", - CommandAlias::H => "h", + CmdAlias::Ll => "ll", + CmdAlias::La => "la", + CmdAlias::H => "h", } } pub fn expand(&self, args: &str) -> String { match self { - CommandAlias::Ll => { + CmdAlias::Ll => { if args.is_empty() { "ls -la".to_string() } else { format!("ls -la{args}") } } - CommandAlias::La => { + CmdAlias::La => { if args.is_empty() { "ls -a".to_string() } else { format!("ls -a{args}") } } - CommandAlias::H => { + CmdAlias::H => { if args.is_empty() { "history".to_string() } else { @@ -178,11 +285,11 @@ impl CommandAlias { } } - pub fn from_str(s: &str) -> Option { + pub fn from_str(s: &str) -> Option { match s { - "ll" => Some(CommandAlias::Ll), - "la" => Some(CommandAlias::La), - "h" => Some(CommandAlias::H), + "ll" => Some(CmdAlias::Ll), + "la" => Some(CmdAlias::La), + "h" => Some(CmdAlias::H), _ => None, } } diff --git a/src/app/terminal/cp_mv_tools.rs b/src/app/terminal/cp_mv_tools.rs new file mode 100644 index 0000000..65544fb --- /dev/null +++ b/src/app/terminal/cp_mv_tools.rs @@ -0,0 +1,374 @@ +use indextree::NodeId; + +use super::command::{CommandRes, VfsCommand}; +use super::fs_tools::{parse_multitarget, VfsRmCommand}; +use super::vfs::{FileContent, VfsError, VfsNodeType, VirtualFilesystem}; + +// VFS-based CpCommand for Phase 2 migration +pub struct VfsCpCommand; + +impl VfsCpCommand { + pub fn new() -> Self { + Self + } +} + +impl VfsCommand for VfsCpCommand { + fn execute( + &self, + vfs: &mut VirtualFilesystem, + current_dir: NodeId, + args: Vec<&str>, + _stdin: Option<&str>, + _is_output_tty: bool, + ) -> CommandRes { + let (options, targets) = parse_multitarget(args); + + // Check for recursive option + let recursive = options.contains(&'r'); + + // Validate options + let invalid = options.iter().find(|c| **c != 'r' && **c != 'f'); + if let Some(c) = invalid { + let c = c.to_owned(); + let error_msg = format!( + r#"cp: invalid option -- '{c}' +This version of cp only supports options 'r' and 'f'"# + ); + return CommandRes::new().with_error().with_stderr(error_msg); + } + + if targets.len() < 2 { + return CommandRes::new() + .with_error() + .with_stderr("cp: missing destination file operand"); + } + + let destination = targets.last().unwrap(); + let sources = &targets[..targets.len() - 1]; + + let mut stderr_parts = Vec::new(); + let mut has_error = false; + + for source in sources { + match self.copy_item(vfs, current_dir, source, destination, recursive) { + Ok(_) => {} + Err(err_msg) => { + has_error = true; + stderr_parts.push(err_msg); + } + } + } + + let mut result = CommandRes::new(); + if has_error { + result = result.with_error(); + let stderr_text = stderr_parts.join("\n"); + result = result.with_stderr(stderr_text); + } + + result + } +} + +impl VfsCpCommand { + fn copy_item( + &self, + vfs: &mut VirtualFilesystem, + current_dir: NodeId, + source_path: &str, + dest_path: &str, + recursive: bool, + ) -> Result<(), String> { + // Resolve source path + let source_id = vfs + .resolve_path(current_dir, source_path) + .map_err(|_| format!("cp: cannot stat '{source_path}': No such file or directory"))?; + + let source_node = vfs + .get_node(source_id) + .ok_or_else(|| format!("cp: cannot stat '{source_path}': No such file or directory"))?; + + // Check if source is a directory and recursive flag + if source_node.is_directory() && !recursive { + return Err(format!( + "cp: omitting directory '{source_path}': use -r to copy directories" + )); + } + + // Determine destination + let (dest_parent_id, dest_name) = + self.resolve_destination(vfs, current_dir, dest_path, source_path)?; + + // Perform the copy + match &source_node.node_type { + VfsNodeType::File { content } => { + self.copy_file(vfs, dest_parent_id, &dest_name, content.clone()) + } + VfsNodeType::Directory => { + // we already know recursive is true here + self.copy_directory_recursive(vfs, source_id, dest_parent_id, &dest_name) + } + VfsNodeType::Link { .. } => Err(format!( + "cp: cannot copy '{source_path}': Links not supported" + )), + } + } + + fn resolve_destination( + &self, + vfs: &VirtualFilesystem, + current_dir: NodeId, + dest_path: &str, + source_path: &str, + ) -> Result<(NodeId, String), String> { + // Try to resolve destination path + match vfs.resolve_path(current_dir, dest_path) { + Ok(dest_id) => { + // Destination exists + let dest_node = vfs.get_node(dest_id).ok_or_else(|| { + format!("cp: cannot access '{dest_path}': No such file or directory") + })?; + + if dest_node.is_directory() { + // Copy into the directory with source filename + let source_name = source_path.split('/').next_back().unwrap_or(source_path); + Ok((dest_id, source_name.to_string())) + } else { + // Destination is a file - get parent directory and use dest filename + let parent_id = vfs.get_parent(dest_id).ok_or_else(|| { + format!("cp: cannot access '{dest_path}': No such file or directory") + })?; + Ok((parent_id, dest_node.name.clone())) + } + } + Err(_) => { + // Destination doesn't exist - parse as parent/filename + let (parent_path, filename) = if let Some(pos) = dest_path.rfind('/') { + (&dest_path[..pos], &dest_path[pos + 1..]) + } else { + ("", dest_path) + }; + + let parent_id = if parent_path.is_empty() { + Ok(current_dir) + } else { + vfs.resolve_path(current_dir, parent_path).map_err(|_| { + format!("cp: cannot create '{dest_path}': No such file or directory") + }) + }?; + + Ok((parent_id, filename.to_string())) + } + } + } + + fn copy_file( + &self, + vfs: &mut VirtualFilesystem, + parent_id: NodeId, + filename: &str, + content: FileContent, + ) -> Result<(), String> { + vfs.create_file(parent_id, filename, content) + .map_err(|err| match err { + VfsError::AlreadyExists => format!("cp: cannot create '{filename}': File exists"), + VfsError::PermissionDenied => { + format!("cp: cannot create '{filename}': Permission denied") + } + VfsError::NotADirectory => { + format!("cp: cannot create '{filename}': Not a directory") + } + _ => format!("cp: cannot create '{filename}': Unknown error"), + })?; + Ok(()) + } + + fn copy_directory_recursive( + &self, + vfs: &mut VirtualFilesystem, + source_id: NodeId, + dest_parent_id: NodeId, + dest_name: &str, + ) -> Result<(), String> { + // Create destination directory + let dest_dir_id = + vfs.create_directory(dest_parent_id, dest_name) + .map_err(|err| match err { + VfsError::AlreadyExists => { + format!("cp: cannot create directory '{dest_name}': File exists") + } + VfsError::PermissionDenied => { + format!("cp: cannot create directory '{dest_name}': Permission denied") + } + VfsError::NotADirectory => { + format!("cp: cannot create directory '{dest_name}': Not a directory") + } + _ => format!("cp: cannot create directory '{dest_name}': Unknown error"), + })?; + + // Copy all entries from source directory + let entries = vfs + .list_directory(source_id) + .map_err(|_| "cp: cannot read directory: Permission denied".to_string())?; + + for entry in entries { + let child_source_id = entry.node_id; + let child_node = vfs + .get_node(child_source_id) + .ok_or_else(|| "cp: cannot access child: No such file or directory".to_string())?; + + match &child_node.node_type { + VfsNodeType::File { content } => { + self.copy_file(vfs, dest_dir_id, &entry.name, content.clone())?; + } + VfsNodeType::Directory => { + self.copy_directory_recursive(vfs, child_source_id, dest_dir_id, &entry.name)?; + } + VfsNodeType::Link { .. } => { + // Skip links for now + } + } + } + + Ok(()) + } +} + +// VFS-based MvCommand for Phase 2 migration +pub struct VfsMvCommand; + +impl VfsMvCommand { + pub fn new() -> Self { + Self + } +} + +impl VfsCommand for VfsMvCommand { + fn execute( + &self, + vfs: &mut VirtualFilesystem, + current_dir: NodeId, + args: Vec<&str>, + _stdin: Option<&str>, + _is_output_tty: bool, + ) -> CommandRes { + let (_, targets) = parse_multitarget(args); + + if targets.len() < 2 { + return CommandRes::new() + .with_error() + .with_stderr("mv: missing destination file operand"); + } + + let destination = targets.last().unwrap(); + let sources = &targets[..targets.len() - 1]; + + let mut stderr_parts = Vec::new(); + let mut has_error = false; + + for source in sources { + match self.move_item(vfs, current_dir, source, destination) { + Ok(_) => {} + Err(err_msg) => { + has_error = true; + stderr_parts.push(err_msg); + } + } + } + + let mut result = CommandRes::new(); + if has_error { + result = result.with_error(); + let stderr_text = stderr_parts.join("\n"); + result = result.with_stderr(stderr_text); + } + + result + } +} + +impl VfsMvCommand { + fn move_item( + &self, + vfs: &mut VirtualFilesystem, + current_dir: NodeId, + source_path: &str, + dest_path: &str, + ) -> Result<(), String> { + // First check if source exists and is not immutable + let source_id = vfs + .resolve_path(current_dir, source_path) + .map_err(|_| format!("mv: cannot stat '{source_path}': No such file or directory"))?; + + let source_node = vfs + .get_node(source_id) + .ok_or_else(|| format!("mv: cannot stat '{source_path}': No such file or directory"))?; + + // Check if source is immutable (cannot be moved) + if source_node.permissions.immutable { + return Err(format!( + "mv: cannot move '{source_path}': Permission denied" + )); + } + + let is_directory = source_node.is_directory(); + + // As per Unix mv documentation: + // rm -f destination_path && cp -pRP source_file destination && rm -rf source_file + + // Step 1: If destination exists and is a file, try to remove it first + // (We handle directory destinations differently in cp) + if let Ok(dest_id) = vfs.resolve_path(current_dir, dest_path) { + if let Some(dest_node) = vfs.get_node(dest_id) { + if !dest_node.is_directory() { + // Try to remove the destination file (ignore errors for now) + let _ = vfs.delete_node(dest_id); + } + } + } + + // Step 2: Copy source to destination (with -r if directory) + let cp_command = VfsCpCommand::new(); + let cp_args = if is_directory { + vec!["-r", source_path, dest_path] + } else { + vec![source_path, dest_path] + }; + + // Execute the copy + let cp_result = cp_command.execute(vfs, current_dir, cp_args, None, false); + if cp_result.is_error() { + // Extract error message from cp command + return Err(format!( + "mv: copy failed: {}", + match cp_result { + CommandRes::Output { + stderr_text: Some(ref msg), + .. + } => msg, + _ => "unknown error", + } + )); + } + + // Step 3: Remove the source (with -r if directory) + let rm_command = VfsRmCommand::new(); + let rm_args = if is_directory { + vec!["-r", source_path] + } else { + vec![source_path] + }; + + // Execute the removal + let rm_result = rm_command.execute(vfs, current_dir, rm_args, None, false); + if rm_result.is_error() { + // The copy succeeded but removal failed - this is still an error + return Err(format!( + "mv: cannot remove '{source_path}': Permission denied" + )); + } + + Ok(()) + } +} diff --git a/src/app/terminal/fs.rs b/src/app/terminal/fs.rs deleted file mode 100644 index 4c7ba7f..0000000 --- a/src/app/terminal/fs.rs +++ /dev/null @@ -1,356 +0,0 @@ -use std::collections::VecDeque; - -use super::components::TextContent; - -const LEN_OF_NAV: usize = 7; -const MINES_SH: &str = r#"#!/bin/bash -set -e - -# https://mines.hansbaker.com -# Minesweeper client with multiplayer, replay analysis, and stat tracking -mines -"#; -const THANKS_TXT: &str = - "Thank you to my wife and my daughter for bringing immense joy to my life."; -// TODO - implement ls -l -const ZSHRC_CONTENT: &str = r#"# Simple zsh configuration -unsetopt beep - -# Basic completion -autoload -Uz compinit -compinit - -# plugins -plugins = (zsh-autosuggestions, zsh-history-substring-search) - -# Aliases -alias ll='ls -la' -alias la='ls -a' -alias h='history' - -# robbyrussell theme prompt -# Arrow changes color based on exit status, directory in cyan, git status -PROMPT='%(?:%{$fg_bold[green]%}➜ :%{$fg_bold[red]%}➜ )%{$fg[cyan]%}%c%{$reset_color%} $(git_prompt_info)' - -ZSH_THEME_GIT_PROMPT_PREFIX="%{$fg_bold[blue]%}git:(%{$fg[red]%}" -ZSH_THEME_GIT_PROMPT_SUFFIX="%{$reset_color%} " -ZSH_THEME_GIT_PROMPT_DIRTY="%{$fg[blue]%}) %{$fg[yellow]%}✗" -ZSH_THEME_GIT_PROMPT_CLEAN="%{$fg[blue]%})" - -# History settings -HISTFILE=window.localStorage[\"cmd_history\"] -HISTSIZE=1000 -SAVEHIST=1000 -setopt SHARE_HISTORY -setopt APPEND_HISTORY - -# zsh-history-substring-search configuration -bindkey '^[[A' history-substring-search-up # or '\eOA' -bindkey '^[[B' history-substring-search-down # or '\eOB' -HISTORY_SUBSTRING_SEARCH_ENSURE_UNIQUE=1 -HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_FOUND=0 -HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_NOT_FOUND=0 -"#; - -pub fn parse_multitarget(args: Vec<&str>) -> (Vec, Vec<&str>) { - args.into_iter().fold( - (Vec::::new(), Vec::<&str>::new()), - |(mut options, mut t), s| { - if s.starts_with("-") { - let mut opts = s.chars().filter(|c| *c != '-').collect::>(); - options.append(&mut opts); - } else if s.starts_with("~/") { - t.push(&s[1..]); - } else if s == "~" { - t.push("/"); - } else { - t.push(s); - } - (options, t) - }, - ) -} - -pub fn path_target_to_target_path(path: &str, target: &str, preserve_dot: bool) -> String { - let mut target = target; - let ends_with_dot = target.ends_with("."); - let mut parts = path - .split("/") - .filter(|s| !s.is_empty()) - .collect::>(); - while target.starts_with("./") { - target = &target[2..]; - } - if target.starts_with("/") { - parts = Vec::new(); - } - if target == "~" || target.starts_with("~/") { - parts = Vec::new(); - target = &target[1..]; - } - while target.ends_with("/") { - target = &target[..target.len() - 1]; - } - let mut target = target - .split("/") - .filter(|s| !s.is_empty() && *s != ".") - .collect::>(); - if ends_with_dot && preserve_dot { - target.push_back("."); - } - while !target.is_empty() { - let p = target.pop_front().unwrap(); - match p { - ".." if !parts.is_empty() => { - let _ = parts.pop(); - } - ".." => {} - other => parts.push(other), - } - } - format!("/{}", parts.join("/")) -} - -#[derive(Debug, Clone)] -pub struct DirContentItem(pub String, pub Target); - -impl TextContent for DirContentItem { - fn text_content(&self) -> &str { - &self.0 - } -} - -#[derive(Debug, Clone)] -pub enum Dir { - Base, - Blog, - CV, - BlogPost(String), -} - -impl Dir { - pub fn contents(&self, blog_posts: &[String], all: bool) -> Vec { - let sort_items = |items: &mut Vec| { - items.sort_by(|a, b| a.0.cmp(&b.0)); - }; - match self { - Dir::Base => { - let mut items: Vec = vec![ - DirContentItem("blog".to_string(), Target::Dir(Dir::Blog)), - DirContentItem("cv".to_string(), Target::Dir(Dir::CV)), - DirContentItem("mines.sh".to_string(), Target::File(File::MinesSh)), - DirContentItem("thanks.txt".to_string(), Target::File(File::ThanksTxt)), - DirContentItem( - "nav.rs".to_string(), - Target::File(File::Nav("/".to_string())), - ), - ]; - if all { - items.push(DirContentItem(".".to_string(), Target::Dir(Dir::Base))); - items.push(DirContentItem("..".to_string(), Target::Dir(Dir::Base))); - items.push(DirContentItem( - ".zshrc".to_string(), - Target::File(File::ZshRc), - )); - } - sort_items(&mut items); - items - } - Dir::Blog => { - let mut items = blog_posts - .iter() - .map(|bp| { - DirContentItem(bp.to_string(), Target::Dir(Dir::BlogPost(bp.to_string()))) - }) - .collect::>(); - items.push(DirContentItem( - "nav.rs".to_string(), - Target::File(File::Nav("/blog".to_string())), - )); - if all { - items.push(DirContentItem(".".to_string(), Target::Dir(Dir::Blog))); - items.push(DirContentItem("..".to_string(), Target::Dir(Dir::Base))); - } - sort_items(&mut items); - items - } - Dir::CV => { - let mut items = vec![DirContentItem( - "nav.rs".to_string(), - Target::File(File::Nav("/cv".to_string())), - )]; - if all { - items.push(DirContentItem(".".to_string(), Target::Dir(Dir::Blog))); - items.push(DirContentItem("..".to_string(), Target::Dir(Dir::CV))); - } - sort_items(&mut items); - items - } - Dir::BlogPost(bp) => { - let mut items = vec![DirContentItem( - bp.to_string(), - Target::Dir(Dir::BlogPost(bp.to_string())), - )]; - if all { - items.push(DirContentItem( - ".".to_string(), - Target::Dir(Dir::BlogPost(bp.to_string())), - )); - items.push(DirContentItem("..".to_string(), Target::Dir(Dir::Blog))); - } - sort_items(&mut items); - items - } - } - } - - pub fn base(&self) -> String { - match self { - Dir::Base => "/".into(), - Dir::Blog => "/blog".into(), - Dir::CV => "/cv".into(), - Dir::BlogPost(s) => format!("/blog/{s}"), - } - } -} - -#[derive(Debug, Clone)] -pub enum File { - MinesSh, - ThanksTxt, - ZshRc, - // ZshHistory, - Nav(String), -} - -impl File { - #[allow(dead_code)] - pub fn name(&self) -> &'static str { - match self { - File::MinesSh => "mines.sh", - File::ThanksTxt => "thanks.txt", - File::ZshRc => ".zshrc", - File::Nav(_) => "nav.rs", - } - } - - pub fn contents(&self) -> String { - match self { - File::MinesSh => MINES_SH.to_string(), - File::ThanksTxt => THANKS_TXT.to_string(), - File::ZshRc => ZSHRC_CONTENT.to_string(), - File::Nav(s) => { - let s = if s.is_empty() { "/" } else { s }; - format!( - r#"use leptos::prelude::*; -use leptos_router::{{hooks::use_navigate, UseNavigateOptions}}; - -func main() {{ - Effect::new((_) => {{ - let navigate = use_navigate(); - navigate("{s}", UseNavigateOptions::default); - }}) -}} -"# - ) - } - } - } -} - -fn blog_post_exists(name: &str, blog_posts: &[String]) -> bool { - let name = if let Some(stripped) = name.strip_prefix("/blog/") { - stripped - } else { - name - }; - blog_posts.iter().any(|s| *s == name) -} - -#[derive(Debug, Clone)] -pub enum Target { - Dir(Dir), - File(File), - Invalid, -} - -impl Target { - pub fn from_str(path: &str, blog_posts: &[String]) -> Self { - match path { - "/" => Self::Dir(Dir::Base), - "/blog" => Self::Dir(Dir::Blog), - "/cv" => Self::Dir(Dir::CV), - post if post.starts_with("/blog/") && blog_post_exists(post, blog_posts) => { - let blog_post_name = post - .split("/") - .last() - .expect("all blog posts should contain a /"); - Self::Dir(Dir::BlogPost(blog_post_name.to_string())) - } - "/mines.sh" => Self::File(File::MinesSh), - "/thanks.txt" => Self::File(File::ThanksTxt), - "/.zshrc" => Self::File(File::ZshRc), - "/nav.rs" => Self::File(File::Nav("/".to_string())), - "/blog/nav.rs" | "/cv/nav.rs" => { - Self::File(File::Nav(path[..path.len() - LEN_OF_NAV].to_string())) - } - post_nav - if post_nav.starts_with("/blog/") - && post_nav.ends_with("/nav.rs") - && blog_post_exists(&post_nav[..post_nav.len() - LEN_OF_NAV], blog_posts) => - { - Self::File(File::Nav(path[..path.len() - LEN_OF_NAV].to_string())) - } - _ => Self::Invalid, - } - } - - pub fn is_executable(&self) -> bool { - matches!(self, Self::File(File::MinesSh | File::Nav(_))) - } - - pub fn full_permissions(&self) -> &'static str { - match self { - Self::Dir(_) => "drwxr-xr-x", - Self::File(_) if self.is_executable() => "-rwxr-xr-x", - Self::File(_) => "-rw-r--r--", - Self::Invalid => "?---------", - } - } - - pub fn link_count(&self, blog_post_count: usize) -> u32 { - match self { - Self::Dir(Dir::Base) => 6 + 2, // mines.sh, thanks.txt, .zshrc, nav.rs, blog/, cv/ + . + .. - Self::Dir(Dir::Blog) => (blog_post_count + 1 + 2) as u32, // posts + nav.rs + . + .. - Self::Dir(Dir::CV) => 1 + 2, // nav.rs + . + .. - Self::Dir(Dir::BlogPost(_)) => 1 + 2, // nav.rs + . + .. - Self::File(_) => 1, // Regular files have 1 link - Self::Invalid => 0, - } - } - - pub fn owner(&self) -> &'static str { - "hansbaker" - } - - pub fn group(&self) -> &'static str { - match self { - Self::Dir(_) => "staff", - Self::File(File::MinesSh) => "wheel", // Executable gets wheel group - Self::File(File::Nav(_)) => "wheel", // nav.rs is executable - Self::File(_) => "staff", - Self::Invalid => "staff", - } - } - - pub fn size(&self) -> u64 { - match self { - Self::Dir(_) => 128, // Directories show standard size - Self::File(File::MinesSh) => 156, // Size of the mines.sh script - Self::File(File::ThanksTxt) => 77, // Size of thanks.txt - Self::File(File::ZshRc) => 1024, // .zshrc is larger - Self::File(File::Nav(_)) => 512, // nav.rs files - Self::Invalid => 0, - } - } -} diff --git a/src/app/terminal/fs_tools.rs b/src/app/terminal/fs_tools.rs index dd56635..8d69cbc 100644 --- a/src/app/terminal/fs_tools.rs +++ b/src/app/terminal/fs_tools.rs @@ -1,27 +1,61 @@ use std::sync::Arc; +use indextree::NodeId; use leptos::prelude::*; use leptos_router::components::*; -use crate::app::terminal::fs::DirContentItem; - -use super::command::{CommandRes, Executable}; +use super::command::{CommandRes, VfsCommand}; use super::components::{ColumnarView, TextContent}; -use super::fs::{parse_multitarget, path_target_to_target_path, Dir, Target}; -pub struct LsCommand { - blog_posts: Vec, +use super::vfs::{FileContent, VfsError, VfsNode, VfsNodeType, VirtualFilesystem}; + +// Parse arguments to extract options & path arguments +pub fn parse_multitarget(args: Vec<&str>) -> (Vec, Vec<&str>) { + args.into_iter().fold( + (Vec::::new(), Vec::<&str>::new()), + |(mut options, mut t), s| { + if s.starts_with("-") { + let mut opts = s.chars().filter(|c| *c != '-').collect::>(); + options.append(&mut opts); + } else if s.starts_with("~/") { + t.push(&s[1..]); + } else if s == "~" { + t.push("/"); + } else { + t.push(s); + } + (options, t) + }, + ) } -impl LsCommand { - pub fn new(blog_posts: Vec) -> Self { - Self { blog_posts } +#[derive(Debug, Clone)] +struct VfsItem { + node: VfsNode, + link_count: usize, // Number of links to this item + display_name: String, // Display name for the item + path: String, // Filesystem / URL path +} + +impl TextContent for VfsItem { + fn text_content(&self) -> &str { + &self.display_name } } -impl Executable for LsCommand { +// VFS-based LsCommand for Phase 2 migration +pub struct VfsLsCommand; + +impl VfsLsCommand { + pub fn new() -> Self { + Self + } +} + +impl VfsCommand for VfsLsCommand { fn execute( &self, - path: &str, + vfs: &mut VirtualFilesystem, + current_dir: NodeId, args: Vec<&str>, _stdin: Option<&str>, is_output_tty: bool, @@ -29,6 +63,8 @@ impl Executable for LsCommand { let mut all = false; let mut long_format = false; let (options, mut target_paths) = parse_multitarget(args); + + // Validate options let invalid = options.iter().find(|c| **c != 'a' && **c != 'l'); if let Some(c) = invalid { let c = c.to_owned(); @@ -38,6 +74,8 @@ This version of ls only supports options 'a' and 'l'"# ); return CommandRes::new().with_error().with_stderr(error_msg); } + + // Process options for option in &options { match option { 'a' => all = true, @@ -45,31 +83,113 @@ This version of ls only supports options 'a' and 'l'"# _ => unreachable!("Invalid options should be caught above"), } } + + // Default to current directory if no targets specified if target_paths.is_empty() { target_paths = vec![""]; } - // Process targets and collect errors + // Process targets using VFS and create VfsItems directly let mut stderr_parts = Vec::new(); - let mut file_targets: Vec<(String, Target)> = Vec::new(); - let mut dir_targets: Vec<(String, Dir)> = Vec::new(); + let mut file_items: Vec = Vec::new(); + let mut dir_listings: Vec<(String, Vec)> = Vec::new(); // (display_name, items) let mut has_error = false; + let get_vfs_item = |node_id: NodeId, display_name: String| { + let node = vfs + .get_node(node_id) + .expect("We should be in a resolved_path"); + let link_count = if matches!(node.node_type, VfsNodeType::Directory) { + vfs.list_directory(node_id).map(|es| es.len()).unwrap_or(0) + 2 + } else { + 1 + }; + + VfsItem { + node: node.clone(), + link_count, + display_name, + path: vfs.get_node_path(node_id), + } + }; + for tp in target_paths.iter() { let target_string = tp.to_string(); - let target_path = path_target_to_target_path(path, tp, false); - let target = Target::from_str(&target_path, &self.blog_posts); - - match target { - Target::File(_) => file_targets.push((tp.to_string(), target)), - Target::Dir(d) => dir_targets.push((tp.to_string(), d)), - Target::Invalid => { - // If the target is empty, we treat it as the current directory + + let resolved_path = if tp.is_empty() { + Ok(current_dir) + } else { + vfs.resolve_path(current_dir, tp) + }; + + let node_id = if let Ok(node_id) = resolved_path { + node_id + } else { + has_error = true; + stderr_parts.push(format!( + "ls: cannot access '{target_string}': No such file or directory" + )); + continue; + }; + + let node = vfs + .get_node(node_id) + .expect("Node should exist after resolve_path"); + let node_path = vfs.get_node_path(node_id); + + match &node.node_type { + VfsNodeType::File { .. } => { + file_items.push(VfsItem { + node: node.clone(), + link_count: 0, + display_name: target_string.clone(), + path: node_path, + }); + } + VfsNodeType::Directory => { + if let Ok(entries) = vfs.list_directory(node_id) { + let mut dir_items: Vec = Vec::new(); + let dir_link_count = entries.len() + 2; // +2 for . and .. + + // Add . and .. entries when -a flag is used + if all { + // Add current directory entry + dir_items.push(VfsItem { + node: node.clone(), + link_count: dir_link_count, + display_name: ".".to_string(), + path: node_path, + }); + + // Add parent directory entry + let parent_id = + vfs.get_parent(node_id).unwrap_or_else(|| vfs.get_root()); + dir_items.push(get_vfs_item(parent_id, "..".to_string())); + } + + // Add regular entries + for entry in entries { + // Skip hidden files unless -a is specified + if !all && entry.name.starts_with('.') { + continue; + } + dir_items.push(get_vfs_item(entry.node_id, entry.name)); + } + + dir_items.sort_by(|a, b| a.display_name.cmp(&b.display_name)); + dir_listings.push((tp.to_string(), dir_items)); + } else { + has_error = true; + stderr_parts.push(format!( + "ls: cannot access '{target_string}': Permission denied" + )); + } + } + VfsNodeType::Link { .. } => { has_error = true; stderr_parts.push(format!( "ls: cannot access '{target_string}': No such file or directory" )); - continue; } } } @@ -81,121 +201,74 @@ This version of ls only supports options 'a' and 'l'"# result = result.with_error().with_stderr(stderr_text); } - file_targets.sort_by(|a, b| a.0.cmp(&b.0)); - dir_targets.sort_by(|a, b| a.0.cmp(&b.0)); + file_items.sort_by(|a, b| a.display_name.cmp(&b.display_name)); + dir_listings.sort_by(|a, b| a.0.cmp(&b.0)); if is_output_tty { - let posts = self.blog_posts.clone(); let is_multi = - dir_targets.len() > 1 || !dir_targets.is_empty() && !file_targets.is_empty(); - let all_captured = all; - let long_format_captured = long_format; - let path_owned = path.to_owned(); + dir_listings.len() > 1 || (!dir_listings.is_empty() && !file_items.is_empty()); + result = result.with_stdout_view(Arc::new(move || { let mut all_views = Vec::new(); - if !file_targets.is_empty() { + + // Handle file targets + if !file_items.is_empty() { all_views.push( - LsView(LsViewProps { - items: file_targets - .iter() - .map(|(s, t)| DirContentItem(s.to_string(), t.to_owned())) - .collect(), - base: path_owned.clone(), - long_format: long_format_captured, - blog_post_count: posts.len(), + VfsLsView(VfsLsViewProps { + items: file_items.clone(), + long_format, }) .into_any(), ); + if is_multi { all_views.push(view! {
}.into_any()); } } - for (i, (tp, d)) in dir_targets.iter().enumerate() { + + // Handle directory targets + for (i, (display_name, items)) in dir_listings.iter().enumerate() { if is_multi { if i > 0 { all_views.push(view! {
}.into_any()); } all_views.push( view! { - {format!("{tp}:")} + {format!("{display_name}:")}
} .into_any(), ); } + all_views.push( - LsView(LsViewProps { - items: d.contents(&posts, all_captured), - base: d.base(), - long_format: long_format_captured, - blog_post_count: posts.len(), + VfsLsView(VfsLsViewProps { + items: items.clone(), + long_format, }) .into_any(), ); } + view! { {all_views} }.into_any() })) } else { - let mut stdout_text = String::new(); - let is_multi = - dir_targets.len() > 1 || !dir_targets.is_empty() && !file_targets.is_empty(); - - // Handle file targets - if !file_targets.is_empty() { - for (_, target) in file_targets.iter() { - if let Target::File(f) = target { - if !stdout_text.is_empty() { - stdout_text.push('\n'); - } - if long_format { - stdout_text.push_str(&format!( - "{} {:2} {:8} {:8} {:>6} {}", - target.full_permissions(), - target.link_count(self.blog_posts.len()), - target.owner(), - target.group(), - target.size(), - f.name() - )); - } else { - stdout_text.push_str(f.name()); - } - } - } + // For non-TTY output, just return simple text + // TODO - fix + let mut text_output = Vec::new(); + + for item in &file_items { + text_output.push(item.display_name.clone()); } - // Handle directory targets - for (tp, d) in dir_targets.iter() { - if is_multi { - if !stdout_text.is_empty() { - stdout_text.push_str("\n\n"); - } - stdout_text.push_str(&format!("{tp}:\n")); - } - - let items = d.contents(&self.blog_posts, all); - for (i, item) in items.iter().enumerate() { - if i > 0 || (!is_multi && !stdout_text.is_empty()) { - stdout_text.push('\n'); - } - if long_format { - stdout_text.push_str(&format!( - "{} {:2} {:8} {:8} {:>6} {}", - item.1.full_permissions(), - item.1.link_count(self.blog_posts.len()), - item.1.owner(), - item.1.group(), - item.1.size(), - item.text_content() - )); - } else { - stdout_text.push_str(item.text_content()); - } + for (_, items) in &dir_listings { + for item in items { + text_output.push(item.display_name.clone()); } } - if !stdout_text.is_empty() { - result = result.with_stdout_text(stdout_text); + if !text_output.is_empty() { + result = result.with_stdout_text(text_output.join("\n")); } } @@ -203,133 +276,172 @@ This version of ls only supports options 'a' and 'l'"# } } +/// VFS-based LsView component that works directly with VfsItem instead of DirContentItem #[component] -fn LsView( - items: Vec, - base: String, - #[prop(default = false)] long_format: bool, - #[prop(default = 0)] blog_post_count: usize, -) -> impl IntoView { +fn VfsLsView(items: Vec, #[prop(default = false)] long_format: bool) -> impl IntoView { let dir_class = "text-blue"; let ex_class = "text-green"; - // Create modified items for long format display if needed - let display_items = if long_format { - items - .into_iter() - .map(|item| { - let long_info = format!( - "{} {:2} {:8} {:8} {:>6} {}", - item.1.full_permissions(), - item.1.link_count(blog_post_count), - item.1.owner(), - item.1.group(), - item.1.size(), - item.0 - ); - DirContentItem(long_info, item.1) - }) - .collect::>() - } else { - items - }; - if long_format { - let long_render_func = move |s: DirContentItem| { - // Find the last space before the filename to preserve original formatting - let last_space_pos = s.0.rfind(' ').unwrap(); - let metadata_with_spaces = s.0[..last_space_pos].to_string(); - let filename = s.0[last_space_pos + 1..].to_string(); + let long_render_func = move |item: VfsItem| { + let filename = item.display_name; + let path = item.path; + let is_directory = item.node.is_directory(); + let is_executable = item.node.is_executable(); // Create the styled filename part - let styled_filename = if matches!(s.1, Target::Dir(_)) { - let base = if base == "/" { "" } else { &base }; - let href = if filename == "." { - base.to_string() - } else { - format!("{}/{}", base, filename) - }; + let styled_filename = if is_directory { view! { - - {filename} + + {filename.clone()} - }.into_any() - } else if s.1.is_executable() { - view! { {filename} }.into_any() + } + .into_any() + } else if is_executable { + view! { {filename.clone()} }.into_any() } else { - view! { {filename} }.into_any() + view! { {filename.clone()} }.into_any() }; - view! { {metadata_with_spaces} " " {styled_filename} } + view! { +
+ {item.node.long_meta_string(item.link_count)} + {styled_filename} +
+ } .into_any() }; view! {
- {display_items + {items .into_iter() - .map(|item| { - view! { - {long_render_func(item)} - "\n" - } - }) + .map(long_render_func) .collect_view()}
} .into_any() } else { - let short_render_func = { - move |s: DirContentItem| { - if matches!(s.1, Target::Dir(_)) { - let base = if base == "/" { "" } else { &base }; - let href = if s.0 == "." { - base.to_string() - } else { - format!("{}/{}", base, s.0) - }; - view! { - - {s.text_content().to_string()} - - } - .into_any() - } else if s.1.is_executable() { - view! { {s.text_content()} }.into_any() - } else { - view! { {s.text_content()} }.into_any() + let short_render_func = move |item: VfsItem| { + let display_name = item.display_name; + let path = item.path; + let is_directory = item.node.is_directory(); + let is_executable = item.node.is_executable(); + + if is_directory { + view! { + + {display_name} + } + .into_any() + } else if is_executable { + view! { {display_name} }.into_any() + } else { + view! { {display_name} }.into_any() } }; view! {
- +
} .into_any() } } -pub struct CatCommand { - blog_posts: Vec, +// VFS-based CdCommand for Phase 2 migration +pub struct VfsCdCommand; + +impl VfsCdCommand { + pub fn new() -> Self { + Self + } +} + +impl VfsCommand for VfsCdCommand { + fn execute( + &self, + vfs: &mut VirtualFilesystem, + current_dir: NodeId, + args: Vec<&str>, + _stdin: Option<&str>, + _is_tty: bool, + ) -> CommandRes { + // Validate arguments + if args.len() >= 2 { + let error_msg = "cd: too many arguments"; + return CommandRes::new().with_error().with_stderr(error_msg); + } + + let target_path = if args.is_empty() { "/" } else { args[0] }; + let target_string = target_path.to_string(); + + // Resolve path using VFS (~ expansion is now handled by resolve_path) + let resolved_path = if target_path.is_empty() { + Ok(vfs.get_root()) + } else { + vfs.resolve_path(current_dir, target_path) + }; + + match resolved_path { + Ok(node_id) => { + if let Some(node) = vfs.get_node(node_id) { + match &node.node_type { + VfsNodeType::Directory => { + // If it's the same directory, no change needed + if node_id == current_dir { + CommandRes::new() + } else { + // Return redirect with the new path + let new_path = vfs.get_node_path(node_id); + CommandRes::Redirect(new_path) + } + } + VfsNodeType::File { .. } => { + let error_msg = format!("cd: not a directory: {target_string}"); + CommandRes::new().with_error().with_stderr(error_msg) + } + VfsNodeType::Link { .. } => { + let error_msg = format!("cd: cannot follow link: {target_string}"); + CommandRes::new().with_error().with_stderr(error_msg) + } + } + } else { + let error_msg = format!("cd: no such file or directory: {target_string}"); + CommandRes::new().with_error().with_stderr(error_msg) + } + } + Err(_) => { + let error_msg = format!("cd: no such file or directory: {target_string}"); + CommandRes::new().with_error().with_stderr(error_msg) + } + } + } } -impl CatCommand { - pub fn new(blog_posts: Vec) -> Self { - Self { blog_posts } +// VFS-based CatCommand for Phase 2 migration +pub struct VfsCatCommand; + +impl VfsCatCommand { + pub fn new() -> Self { + Self } } -impl Executable for CatCommand { +impl VfsCommand for VfsCatCommand { fn execute( &self, - path: &str, + vfs: &mut VirtualFilesystem, + current_dir: NodeId, args: Vec<&str>, _stdin: Option<&str>, _is_output_tty: bool, ) -> CommandRes { let (options, targets) = parse_multitarget(args); + + // Validate options if !options.is_empty() { let c = options[0].to_owned(); let error_msg = format!( @@ -338,8 +450,11 @@ This version of cat doesn't support any options"# ); return CommandRes::new().with_error().with_stderr(error_msg); } + if targets.is_empty() { - return CommandRes::new().with_error(); + return CommandRes::new() + .with_error() + .with_stderr("cat: missing operand"); } // Process targets and collect outputs @@ -349,18 +464,46 @@ This version of cat doesn't support any options"# for tp in targets.iter() { let target_string = tp.to_string(); - let target_path = path_target_to_target_path(path, tp, false); - let target = Target::from_str(&target_path, &self.blog_posts); - match target { - Target::File(f) => { - stdout_parts.push(f.contents().to_string()); + let resolved_path = if tp.is_empty() { + Err(()) + } else { + vfs.resolve_path(current_dir, tp).map_err(|_| ()) + }; + + let node_id = match resolved_path { + Ok(node_id) => node_id, + Err(_) => { + has_error = true; + stderr_parts.push(format!("cat: {target_string}: No such file or directory")); + continue; } - Target::Dir(_) => { + }; + + let node = match vfs.get_node(node_id) { + Some(node) => node, + None => { + has_error = true; + stderr_parts.push(format!("cat: {target_string}: No such file or directory")); + continue; + } + }; + + match &node.node_type { + VfsNodeType::File { .. } => match vfs.read_file(node_id) { + Ok(file_content) => { + stdout_parts.push(file_content); + } + Err(_) => { + has_error = true; + stderr_parts.push(format!("cat: {target_string}: Permission denied")); + } + }, + VfsNodeType::Directory => { has_error = true; stderr_parts.push(format!("cat: {target_string}: Is a directory")); } - Target::Invalid => { + VfsNodeType::Link { .. } => { has_error = true; stderr_parts.push(format!("cat: {target_string}: No such file or directory")); } @@ -373,432 +516,332 @@ This version of cat doesn't support any options"# let mut result = CommandRes::new(); if has_error { result = result.with_error(); + result = result.with_stderr(stderr_text); } if !stdout_text.is_empty() { result = result.with_stdout_text(stdout_text); } - if !stderr_text.is_empty() { - result = result.with_stderr(stderr_text); - } - result } } -pub struct CdCommand { - blog_posts: Vec, -} - -impl CdCommand { - pub fn new(blog_posts: Vec) -> Self { - Self { blog_posts } - } -} - -impl Executable for CdCommand { - fn execute( - &self, - path: &str, - args: Vec<&str>, - _stdin: Option<&str>, - _is_output_tty: bool, - ) -> CommandRes { - if args.len() >= 2 { - let error_msg = "cd: too many arguments"; - return CommandRes::new().with_error().with_stderr(error_msg); - } - let target_path = if args.is_empty() { "/" } else { args[0] }; - let target_string = target_path.to_owned(); - let target_path = path_target_to_target_path(path, target_path, false); - let target = Target::from_str(&target_path, &self.blog_posts); - if target_path == path { - return CommandRes::new(); - } - match target { - Target::File(_) => { - let error_msg = format!("cd: not a directory: {target_string}"); - CommandRes::new().with_error().with_stderr(error_msg) - } - Target::Dir(_) => CommandRes::Redirect(target_path), - Target::Invalid => { - let error_msg = format!("cd: no such file or directory: {target_string}"); - CommandRes::new().with_error().with_stderr(error_msg) - } - } - } -} - -pub struct TouchCommand { - blog_posts: Vec, -} +// VFS-based TouchCommand for Phase 2 migration +pub struct VfsTouchCommand; -impl TouchCommand { - pub fn new(blog_posts: Vec) -> Self { - Self { blog_posts } +impl VfsTouchCommand { + pub fn new() -> Self { + Self } } -impl Executable for TouchCommand { +impl VfsCommand for VfsTouchCommand { fn execute( &self, - path: &str, + vfs: &mut VirtualFilesystem, + current_dir: NodeId, args: Vec<&str>, _stdin: Option<&str>, _is_output_tty: bool, ) -> CommandRes { let (_, targets) = parse_multitarget(args); + if targets.is_empty() { return CommandRes::new() .with_error() - .with_stderr("touch: missing operand"); + .with_stderr("touch: missing file operand"); } - let targets = targets.into_iter().fold(Vec::new(), |mut ts, tp| { - let target_string = tp.to_owned(); - let tp = if tp.contains("/") { - tp.rsplit_once("/").unwrap().0 + + let mut stderr_parts = Vec::new(); + let mut has_error = false; + + for target in targets { + // Split the path to get parent directory and filename + let (parent_path, filename) = if let Some(pos) = target.rfind('/') { + (&target[..pos], &target[pos + 1..]) } else { - "" + ("", target) }; - let target_path = path_target_to_target_path(path, tp, false); - let target = Target::from_str(&target_path, &self.blog_posts); - ts.push((target_string, target)); - ts - }); - let error_messages = targets - .iter() - .map(|(name, ts)| { - let base = format!("touch: cannot touch '{name}': "); - match ts { - Target::Dir(_) => base + "Permission denied", - Target::File(_) => base + "Not a directory", - Target::Invalid => base + "No such file or directory", + + // Don't allow empty filename + if filename.is_empty() { + has_error = true; + stderr_parts.push(format!( + "touch: cannot touch '{target}': No such file or directory" + )); + continue; + } + + // Resolve parent directory + let parent_id = if parent_path.is_empty() { + Ok(current_dir) + } else { + vfs.resolve_path(current_dir, parent_path) + }; + + let parent_id = match parent_id { + Ok(id) => id, + Err(_) => { + has_error = true; + stderr_parts.push(format!( + "touch: cannot touch '{target}': No such file or directory" + )); + continue; } - }) - .collect::>() - .join("\n"); + }; + + // Check if file already exists + let mut file_exists = false; + if let Ok(entries) = vfs.list_directory(parent_id) { + for entry in entries { + if entry.name == filename { + file_exists = true; + break; + } + } + } - CommandRes::new().with_error().with_stderr(error_messages) + // If file doesn't exist, create it + if !file_exists { + match vfs.create_file(parent_id, filename, FileContent::Dynamic(String::new())) { + Ok(_) => { + // File created successfully + } + Err(VfsError::PermissionDenied) => { + has_error = true; + stderr_parts + .push(format!("touch: cannot touch '{target}': Permission denied")); + } + Err(_) => { + has_error = true; + stderr_parts.push(format!( + "touch: cannot touch '{target}': No such file or directory" + )); + } + } + } + // If file exists, touch would normally update timestamps, but we don't have that functionality yet + } + + let mut result = CommandRes::new(); + if has_error { + result = result.with_error(); + let stderr_text = stderr_parts.join("\n"); + result = result.with_stderr(stderr_text); + } + + result } } -pub struct MkdirCommand { - blog_posts: Vec, -} +// VFS-based MkdirCommand for Phase 2 migration +pub struct VfsMkdirCommand; -impl MkdirCommand { - pub fn new(blog_posts: Vec) -> Self { - Self { blog_posts } +impl VfsMkdirCommand { + pub fn new() -> Self { + Self } } -impl Executable for MkdirCommand { +impl VfsCommand for VfsMkdirCommand { fn execute( &self, - path: &str, + vfs: &mut VirtualFilesystem, + current_dir: NodeId, args: Vec<&str>, _stdin: Option<&str>, _is_output_tty: bool, ) -> CommandRes { let (_, targets) = parse_multitarget(args); + if targets.is_empty() { return CommandRes::new() .with_error() .with_stderr("mkdir: missing operand"); } - let targets = targets.into_iter().fold(Vec::new(), |mut ts, tp| { - let target_string = tp.to_owned(); - let tp = if tp.contains("/") { - tp.rsplit_once("/").unwrap().0 + + let mut stderr_parts = Vec::new(); + let mut has_error = false; + + for target in targets { + // Split the path to get parent directory and dirname + let (parent_path, dirname) = if let Some(pos) = target.rfind('/') { + (&target[..pos], &target[pos + 1..]) } else { - "" + ("", target) }; - let target_path = path_target_to_target_path(path, tp, false); - let target = Target::from_str(&target_path, &self.blog_posts); - ts.push((target_string, target)); - ts - }); - let error_messages = targets - .iter() - .map(|(name, ts)| { - let base = format!("mkdir: cannot create directory '{name}': "); - match ts { - Target::Dir(_) => base + "Permission denied", - Target::File(_) => base + "Not a directory", - Target::Invalid => base + "No such file or directory", - } - }) - .collect::>() - .join("\n"); - CommandRes::new().with_error().with_stderr(error_messages) - } -} + // Don't allow empty dirname + if dirname.is_empty() { + has_error = true; + stderr_parts.push(format!( + "mkdir: cannot create directory '{target}': No such file or directory" + )); + continue; + } -pub struct RmCommand { - blog_posts: Vec, -} + // Resolve parent directory + let parent_id = if parent_path.is_empty() { + Ok(current_dir) + } else { + vfs.resolve_path(current_dir, parent_path) + }; -impl RmCommand { - pub fn new(blog_posts: Vec) -> Self { - Self { blog_posts } - } -} + let parent_id = match parent_id { + Ok(id) => id, + Err(_) => { + has_error = true; + stderr_parts.push(format!( + "mkdir: cannot create directory '{target}': No such file or directory" + )); + continue; + } + }; -impl Executable for RmCommand { - fn execute( - &self, - path: &str, - args: Vec<&str>, - _stdin: Option<&str>, - _is_output_tty: bool, - ) -> CommandRes { - let (_, targets) = parse_multitarget(args); - if targets.is_empty() { - return CommandRes::new() - .with_error() - .with_stderr("rm: missing operand"); - } - let targets = targets.into_iter().fold(Vec::new(), |mut ts, tp| { - let target_string = tp.to_owned(); - let target_path = path_target_to_target_path(path, tp, false); - let target = Target::from_str(&target_path, &self.blog_posts); - ts.push((target_string, target)); - ts - }); - let error_messages = targets - .iter() - .map(|(name, ts)| { - let base = format!("rm: cannot remove '{name}': "); - match ts { - Target::Dir(_) => base + "Permission denied", - Target::File(_) => base + "Permission denied", - Target::Invalid => base + "No such file or directory", + // Create the directory + match vfs.create_directory(parent_id, dirname) { + Ok(_) => { + // Directory created successfully + } + Err(VfsError::AlreadyExists) => { + has_error = true; + stderr_parts.push(format!( + "mkdir: cannot create directory '{target}': File exists" + )); + } + Err(VfsError::PermissionDenied) => { + has_error = true; + stderr_parts.push(format!( + "mkdir: cannot create directory '{target}': Permission denied" + )); + } + Err(VfsError::NotADirectory) => { + has_error = true; + stderr_parts.push(format!( + "mkdir: cannot create directory '{target}': Not a directory" + )); } - }) - .collect::>() - .join("\n"); + Err(_) => { + has_error = true; + stderr_parts.push(format!( + "mkdir: cannot create directory '{target}': No such file or directory" + )); + } + } + } - CommandRes::new().with_error().with_stderr(error_messages) + let mut result = CommandRes::new(); + if has_error { + result = result.with_error(); + let stderr_text = stderr_parts.join("\n"); + result = result.with_stderr(stderr_text); + } + + result } } -pub struct CpCommand { - blog_posts: Vec, -} +// VFS-based RmCommand for Phase 2 migration +pub struct VfsRmCommand; -impl CpCommand { - pub fn new(blog_posts: Vec) -> Self { - Self { blog_posts } +impl VfsRmCommand { + pub fn new() -> Self { + Self } } -impl Executable for CpCommand { +impl VfsCommand for VfsRmCommand { fn execute( &self, - path: &str, + vfs: &mut VirtualFilesystem, + current_dir: NodeId, args: Vec<&str>, _stdin: Option<&str>, _is_output_tty: bool, ) -> CommandRes { let (options, targets) = parse_multitarget(args); - let mut recursive = false; - let invalid = options.iter().find(|c| **c != 'r'); + + // Check for recursive option + let recursive = options.contains(&'r'); + + // Validate options + let invalid = options.iter().find(|c| **c != 'r' && **c != 'f'); if let Some(c) = invalid { let c = c.to_owned(); let error_msg = format!( - r#"cp: invalid option -- '{c}' -This version of cp only supports option 'r'"# + r#"rm: invalid option -- '{c}' +This version of rm only supports options 'r' and 'f'"# ); return CommandRes::new().with_error().with_stderr(error_msg); } - if !options.is_empty() { - recursive = true; - } + if targets.is_empty() { return CommandRes::new() .with_error() - .with_stderr("cp: missing file operand"); - } - if targets.len() < 2 { - let target = targets[0].to_owned(); - let error_msg = format!("cp: missing destination file operand after {target}"); - return CommandRes::new().with_error().with_stderr(error_msg); + .with_stderr("rm: missing operand"); } - let targets = targets - .into_iter() - .enumerate() - .fold(Vec::new(), |mut ts, (i, tp)| { - let target_string = tp.to_owned(); - let target_path = path_target_to_target_path(path, tp, false); - let full_target = Target::from_str(&target_path, &self.blog_posts); - let tp = if i != 0 && tp.contains("/") { - tp.rsplit_once("/").unwrap().0 - } else { - "" - }; - let target_path = path_target_to_target_path(path, tp, false); - let partial_target = Target::from_str(&target_path, &self.blog_posts); - ts.push((target_string, full_target, partial_target)); - ts - }); - let target_filename = match (recursive, &targets[0].1) { - (false, Target::Dir(_)) => { - let error_msg = format!( - "cp: -r not specified; omitting directory '{}'", - targets[0].0 - ); - return CommandRes::new().with_error().with_stderr(error_msg); - } - (_, Target::Invalid) => { - let error_msg = format!( - "cp: cannot stat '{}': No such file or directory", - targets[0].0 - ); - return CommandRes::new().with_error().with_stderr(error_msg); - } - _ => { - let target = &targets[0].0; - let target = if target.ends_with("/") { - &target[..target.len() - 1] - } else { - &target[..] - }; - target - .split("/") - .last() - .expect("Should have a last element") - .to_string() - } - }; - let error_messages = targets.iter().skip(1).map(|(name, full_ts, partial_ts)| { - match full_ts { - Target::Dir(_) => { - if name.ends_with("/") { - format!("cp: cannot create regular file '{name}{target_filename}': Permission denied") - } else { - format!("cp: cannot create regular file '{name}/{target_filename}': Permission denied") - } - }, - Target::File(_) => format!("cp: cannot create regular file '{name}': Permission denied"), - Target::Invalid => { - if name.ends_with("/") { - format!("cp: cannot create regular file '{name}': Not a directory") - } else { - match partial_ts { - Target::Dir(_) | Target::File(_) => format!("cp: cannot create regular file '{name}': Permission denied"), - Target::Invalid => format!("cp: cannot create regular file '{name}': No such file or directory"), - } - } - } - } - }).collect::>().join("\n"); - CommandRes::new().with_error().with_stderr(error_messages) - } -} + let mut stderr_parts = Vec::new(); + let mut has_error = false; -pub struct MvCommand { - blog_posts: Vec, -} + for target in targets { + // Resolve the target path + let node_id = match vfs.resolve_path(current_dir, target) { + Ok(id) => id, + Err(_) => { + has_error = true; + stderr_parts.push(format!( + "rm: cannot remove '{target}': No such file or directory" + )); + continue; + } + }; -impl MvCommand { - pub fn new(blog_posts: Vec) -> Self { - Self { blog_posts } - } -} + // Check if it's a directory + if let Some(node) = vfs.get_node(node_id) { + let is_directory = matches!(node.node_type, VfsNodeType::Directory); -impl Executable for MvCommand { - fn execute( - &self, - path: &str, - args: Vec<&str>, - _stdin: Option<&str>, - _is_output_tty: bool, - ) -> CommandRes { - let (options, targets) = parse_multitarget(args); - let invalid = options.iter().find(|c| **c != 'f'); - if let Some(c) = invalid { - let c = c.to_owned(); - let error_msg = format!( - r#"mv: invalid option -- '{c}' -This version of mv only supports option 'f'"# - ); - return CommandRes::new().with_error().with_stderr(error_msg); - } - if targets.is_empty() { - return CommandRes::new() - .with_error() - .with_stderr("mv: missing file operand"); - } - if targets.len() < 2 { - let target = targets[0].to_owned(); - let error_msg = format!("mv: missing destination file operand after {target}"); - return CommandRes::new().with_error().with_stderr(error_msg); - } - let targets = targets - .into_iter() - .enumerate() - .fold(Vec::new(), |mut ts, (i, tp)| { - let target_string = tp.to_owned(); - let target_path = path_target_to_target_path(path, tp, false); - let full_target = Target::from_str(&target_path, &self.blog_posts); - let tp = if i != 0 && tp.contains("/") { - tp.rsplit_once("/").unwrap().0 - } else { - "" - }; - let target_path = path_target_to_target_path(path, tp, false); - let partial_target = Target::from_str(&target_path, &self.blog_posts); - ts.push((target_string, full_target, partial_target)); - ts - }); - let target_filename = match &targets[0].1 { - Target::Invalid => { - let error_msg = format!( - "mv: cannot stat '{}': No such file or directory", - targets[0].0 - ); - return CommandRes::new().with_error().with_stderr(error_msg); - } - _ => { - let target = &targets[0].0; - let target = if target.ends_with("/") { - &target[..target.len() - 1] - } else { - &target[..] - }; - target - .split("/") - .last() - .expect("Should have a last element") - .to_string() + if is_directory && !recursive { + has_error = true; + stderr_parts.push(format!("rm: cannot remove '{target}': Is a directory")); + continue; + } } - }; - let error_messages = targets.iter().skip(1).map(|(name, full_ts, partial_ts)| { - match full_ts { - Target::Dir(_) => { - if name.ends_with("/") { - format!("mv: cannot move '{name}': Permission denied") - } else { - format!("mv: cannot move '{name}/{target_filename}' to '{name}': Permission denied") - } - }, - Target::File(_) => format!("mv: cannot move '{name}': Permission denied"), - Target::Invalid => { - if name.ends_with("/") { - format!("mv: cannot move '{name}': Not a directory") - } else { - match partial_ts { - Target::Dir(_) | Target::File(_) => format!("mv: cannot move '{name}': Permission denied"), - Target::Invalid => format!("mv: cannot move '{name}': No such file or directory"), - } - } + + // Try to delete the node + let delete_result = if recursive { + vfs.delete_node_recursive(node_id) + } else { + vfs.delete_node(node_id) + }; + + match delete_result { + Ok(_) => { + // Node deleted successfully + } + Err(VfsError::PermissionDenied) => { + has_error = true; + stderr_parts.push(format!("rm: cannot remove '{target}': Permission denied")); + } + Err(VfsError::SystemError(msg)) if msg.contains("not empty") => { + has_error = true; + stderr_parts.push(format!("rm: cannot remove '{target}': Directory not empty")); + } + Err(_) => { + has_error = true; + stderr_parts.push(format!("rm: cannot remove '{target}': Permission denied")); } } - }).collect::>().join("\n"); + } + + let mut result = CommandRes::new(); + if has_error { + result = result.with_error(); + let stderr_text = stderr_parts.join("\n"); + result = result.with_stderr(stderr_text); + } - CommandRes::new().with_error().with_stderr(error_messages) + result } } diff --git a/src/app/terminal/ps_tools.rs b/src/app/terminal/ps_tools.rs index ce80575..43d3b43 100644 --- a/src/app/terminal/ps_tools.rs +++ b/src/app/terminal/ps_tools.rs @@ -1,5 +1,4 @@ - -use crate::app::terminal::command::{CommandRes, Executable}; +use crate::app::terminal::command::{Command, CommandRes}; #[derive(Debug, Clone)] pub struct Process { @@ -45,8 +44,14 @@ impl PsCommand { } } -impl Executable for PsCommand { - fn execute(&self, _path: &str, args: Vec<&str>, _stdin: Option<&str>, _is_output_tty: bool) -> CommandRes { +impl Command for PsCommand { + fn execute( + &self, + _path: &str, + args: Vec<&str>, + _stdin: Option<&str>, + _is_output_tty: bool, + ) -> CommandRes { // Check for supported options if args.len() > 1 { return CommandRes::new() @@ -61,15 +66,12 @@ impl Executable for PsCommand { } else { let arg = args[0].to_string(); let error_msg = format!("ps: invalid argument -- '{arg}'\nUsage: ps [aux]"); - return CommandRes::new() - .with_error() - .with_stderr(error_msg); + return CommandRes::new().with_error().with_stderr(error_msg); }; let output = self.get_processes(detailed); CommandRes::new().with_stdout_text(output) } - } pub struct KillCommand { @@ -91,8 +93,14 @@ static SIGS: [&str; 18] = [ "6", "7", "8", "9", ]; -impl Executable for KillCommand { - fn execute(&self, _path: &str, args: Vec<&str>, _stdin: Option<&str>, _is_output_tty: bool) -> CommandRes { +impl Command for KillCommand { + fn execute( + &self, + _path: &str, + args: Vec<&str>, + _stdin: Option<&str>, + _is_output_tty: bool, + ) -> CommandRes { if args.is_empty() { return CommandRes::new() .with_error() @@ -104,14 +112,10 @@ impl Executable for KillCommand { if !SIGS.contains(&signal_name.as_str()) { if signal_name.chars().all(|c| c.is_ascii_alphabetic()) { let error_msg = format!("kill: unknown signal: SIG{signal_name}"); - return CommandRes::new() - .with_error() - .with_stderr(error_msg); + return CommandRes::new().with_error().with_stderr(error_msg); } else { let error_msg = "kill: usage: kill [-n signum] pid"; - return CommandRes::new() - .with_error() - .with_stderr(error_msg); + return CommandRes::new().with_error().with_stderr(error_msg); } } &args[1..] @@ -133,9 +137,7 @@ impl Executable for KillCommand { Err(_) => { let pid_str = pid_str.to_string(); let error_msg = format!("kill: illegal pid: {pid_str}"); - return CommandRes::new() - .with_error() - .with_stderr(error_msg); + return CommandRes::new().with_error().with_stderr(error_msg); } }; @@ -144,34 +146,25 @@ impl Executable for KillCommand { if !process_exists { let error_msg = format!("kill: kill {pid} failed: no such process"); - return CommandRes::new() - .with_error() - .with_stderr(error_msg); + return CommandRes::new().with_error().with_stderr(error_msg); } // Handle special PID 42 with easter egg if pid == 42 { let message = "Answer to everything terminated\nkill: kill 42 failed: operation not permitted"; - return CommandRes::new() - .with_error() - .with_stderr(message); + return CommandRes::new().with_error().with_stderr(message); } // All core services show permission denied let core_services = [1, 42, 99, 128, 256]; if core_services.contains(&pid) { let error_msg = format!("kill: kill {pid} failed: operation not permitted"); - return CommandRes::new() - .with_error() - .with_stderr(error_msg); + return CommandRes::new().with_error().with_stderr(error_msg); } // This shouldn't be reached with our current process list, but included for completeness let error_msg = format!("kill: kill {pid} failed: operation not permitted"); - CommandRes::new() - .with_error() - .with_stderr(error_msg) + CommandRes::new().with_error().with_stderr(error_msg) } - } diff --git a/src/app/terminal/simple_tools.rs b/src/app/terminal/simple_tools.rs index 48c1aa3..008bf9a 100644 --- a/src/app/terminal/simple_tools.rs +++ b/src/app/terminal/simple_tools.rs @@ -1,4 +1,4 @@ -use super::command::{CommandRes, Executable}; +use super::command::{Command, CommandRes}; use crate::app::ascii::{AVATAR_BLOCK, INFO_BLOCK}; use chrono::prelude::*; @@ -15,7 +15,7 @@ The commands should feel familiar: pub struct HelpCommand; -impl Executable for HelpCommand { +impl Command for HelpCommand { fn execute( &self, _path: &str, @@ -29,7 +29,7 @@ impl Executable for HelpCommand { pub struct PwdCommand; -impl Executable for PwdCommand { +impl Command for PwdCommand { fn execute( &self, path: &str, @@ -48,7 +48,7 @@ impl Executable for PwdCommand { pub struct WhoAmICommand; -impl Executable for WhoAmICommand { +impl Command for WhoAmICommand { fn execute( &self, _path: &str, @@ -67,7 +67,7 @@ impl Executable for WhoAmICommand { pub struct ClearCommand; -impl Executable for ClearCommand { +impl Command for ClearCommand { fn execute( &self, _path: &str, @@ -97,7 +97,7 @@ impl NeofetchCommand { } } -impl Executable for NeofetchCommand { +impl Command for NeofetchCommand { fn execute( &self, _path: &str, @@ -112,7 +112,7 @@ impl Executable for NeofetchCommand { pub struct MinesCommand; -impl Executable for MinesCommand { +impl Command for MinesCommand { fn execute( &self, _path: &str, @@ -126,7 +126,7 @@ impl Executable for MinesCommand { pub struct SudoCommand; -impl Executable for SudoCommand { +impl Command for SudoCommand { fn execute( &self, _path: &str, @@ -141,7 +141,7 @@ impl Executable for SudoCommand { pub struct EchoCommand; -impl Executable for EchoCommand { +impl Command for EchoCommand { fn execute( &self, _path: &str, @@ -197,7 +197,7 @@ impl DateCommand { } } -impl Executable for DateCommand { +impl Command for DateCommand { fn execute( &self, _path: &str, @@ -263,7 +263,7 @@ impl UptimeCommand { } } -impl Executable for UptimeCommand { +impl Command for UptimeCommand { fn execute( &self, _path: &str, @@ -295,7 +295,7 @@ impl<'a> HistoryCommand<'a> { } } -impl Executable for HistoryCommand<'_> { +impl Command for HistoryCommand<'_> { fn execute( &self, _path: &str, diff --git a/src/app/terminal/system_tools.rs b/src/app/terminal/system_tools.rs index 097ba2a..42f4309 100644 --- a/src/app/terminal/system_tools.rs +++ b/src/app/terminal/system_tools.rs @@ -1,57 +1,58 @@ -use super::command::{CommandAlias, CommandRes, Executable}; -use super::fs::{path_target_to_target_path, File, Target}; +use super::command::{Cmd, CmdAlias, Command, CommandRes, VfsCommand}; use super::simple_tools::MinesCommand; +use super::vfs::{FileContent, VfsNodeType, VirtualFilesystem}; +use indextree::NodeId; -pub struct WhichCommand { - blog_posts: Vec, -} +pub struct WhichCommand; impl WhichCommand { - pub fn new(blog_posts: Vec) -> Self { - Self { blog_posts } + pub fn new() -> Self { + Self } - fn get_which_result(&self, path: &str, command: &str) -> (String, bool) { + fn get_which_result( + &self, + vfs: &VirtualFilesystem, + current_dir: NodeId, + command: &str, + ) -> (String, bool) { // If the command contains a path separator, treat it as a file path if command.contains('/') { - let target_path = path_target_to_target_path(path, command, false); - let target = Target::from_str(&target_path, &self.blog_posts); - - // Check if it's an executable file - let is_executable = target.is_executable(); - - if is_executable { - (command.to_string(), true) + // Resolve the path using VFS + let node_id = if let Ok(node_id) = vfs.resolve_path(current_dir, command) { + node_id } else { - (format!("{command} not found"), false) + return (format!("{command} not found"), false); + }; + match vfs.get_node(node_id) { + Some(node) if node.is_executable() => (command.to_string(), true), + _ => (format!("{command} not found"), false), } - } else if let Some(alias) = CommandAlias::from_str(command) { + } else if let Some(alias) = CmdAlias::from_str(command) { // Check if it's an alias first let expansion = alias.expand(""); (format!("{command}: aliased to {expansion}"), true) - } else { - // Map commands to their simulated paths - match command { - // Shell builtins - "cd" | "pwd" | "echo" | "history" => (format!("{command}: shell builtin"), true), - - // External commands (simulated paths) - "help" | "ls" | "cat" | "clear" | "cp" | "date" | "mines" | "mkdir" | "mv" - | "rm" | "touch" | "which" | "whoami" | "neofetch" | "uptime" | "ps" | "kill" => { - (format!("/usr/bin/{command}"), true) - } - - // Unknown command - _ => (format!("{command} not found"), false), + } else if let Some(cmd) = Cmd::from_str(command) { + // Known command - get its simulated path or mark as builtin + if cmd.is_builtin() { + (format!("{command}: shell builtin"), true) + } else if let Some(path) = cmd.simulated_path() { + (path, true) + } else { + (format!("{command} not found"), false) } + } else { + // Unknown command + (format!("{command} not found"), false) } } } -impl Executable for WhichCommand { +impl VfsCommand for WhichCommand { fn execute( &self, - path: &str, + vfs: &mut VirtualFilesystem, + current_dir: NodeId, args: Vec<&str>, _stdin: Option<&str>, _is_output_tty: bool, @@ -66,7 +67,7 @@ impl Executable for WhichCommand { let results: Vec = args .iter() .map(|&command| { - let (text, found) = self.get_which_result(path, command); + let (text, found) = self.get_which_result(vfs, current_dir, command); if !found { is_err = true; } @@ -84,54 +85,72 @@ impl Executable for WhichCommand { } pub struct UnknownCommand { - blog_posts: Vec, command_name: String, } impl UnknownCommand { - pub fn new(blog_posts: Vec, command_name: String) -> Self { - Self { - blog_posts, - command_name, - } + pub fn new(command_name: String) -> Self { + Self { command_name } } } -impl Executable for UnknownCommand { +impl VfsCommand for UnknownCommand { fn execute( &self, - path: &str, + vfs: &mut VirtualFilesystem, + current_dir: NodeId, args: Vec<&str>, _stdin: Option<&str>, is_output_tty: bool, ) -> CommandRes { - let target_string = self.command_name.clone(); - let target_path = path_target_to_target_path(path, &self.command_name, false); - let target = Target::from_str(&target_path, &self.blog_posts); - let is_executable = target.is_executable() && target_string.contains("/"); + let target_string = &self.command_name; + + // Try to resolve the path using VFS + let node_id = match vfs.resolve_path(current_dir, target_string) { + Ok(node_id) => node_id, + Err(_) => { + let error_msg = format!("command not found: {target_string}"); + return CommandRes::new().with_error().with_stderr(error_msg); + } + }; + + let node = match vfs.get_node(node_id) { + Some(node) => node, + None => { + let error_msg = format!("command not found: {target_string}"); + return CommandRes::new().with_error().with_stderr(error_msg); + } + }; + + let is_executable = node.is_executable() && target_string.contains("/"); + + // Check if arguments are allowed for non-executable files if !args.is_empty() && !is_executable { - // only mines.sh and nav.rs are executable, so only these can accept arguments let error_msg = format!("command not found: {target_string}"); return CommandRes::new().with_error().with_stderr(error_msg); } - match target { - Target::Dir(_) => CommandRes::redirect(target_path), - Target::File(f) => { + + if node.name == "mines.sh" && is_executable { + // MinesCommand doesn't implement VfsCommand yet, so fall back to legacy execution + let path = vfs.get_node_path(current_dir); + return MinesCommand.execute(&path, args, None, is_output_tty); + } + + match &node.node_type { + VfsNodeType::Directory => { + let target_path = vfs.get_node_path(node_id); + CommandRes::redirect(target_path) + } + VfsNodeType::File { content } => { + // Check for directory syntax on file if target_string.ends_with("/") { let error_msg = format!("not a directory: {target_string}"); return CommandRes::new().with_error().with_stderr(error_msg); } - match f { - File::Nav(s) => CommandRes::redirect(s), - File::MinesSh => { - if is_executable { - MinesCommand.execute(path, args, None, is_output_tty) - } else { - let error_msg = format!("command not found: {target_string}\nhint: try 'mines' or '/mines.sh'"); - CommandRes::new().with_error().with_stderr(error_msg) - } - } - File::ThanksTxt | File::ZshRc => { + + match content { + FileContent::NavFile(s) => CommandRes::redirect(s.clone()), + FileContent::Static(_) | FileContent::Dynamic(_) => { let error_msg = if target_string.contains("/") { format!("permission denied: {target_string}") } else { @@ -141,7 +160,7 @@ impl Executable for UnknownCommand { } } } - Target::Invalid => { + VfsNodeType::Link { .. } => { let error_msg = format!("command not found: {target_string}"); CommandRes::new().with_error().with_stderr(error_msg) } diff --git a/src/app/terminal/vfs.rs b/src/app/terminal/vfs.rs new file mode 100644 index 0000000..e453dba --- /dev/null +++ b/src/app/terminal/vfs.rs @@ -0,0 +1,1238 @@ +#![allow(dead_code)] +use chrono::{DateTime, Local}; +use indextree::{Arena, NodeId}; + +// Re-use the same static file contents from the original VFS +const MINES_SH: &str = r#"#!/bin/bash +set -e + +# https://mines.hansbaker.com +# Minesweeper client with multiplayer, replay analysis, and stat tracking +mines +"#; + +const THANKS_TXT: &str = + "Thank you to my wife and my daughter for bringing immense joy to my life."; + +const ZSHRC_CONTENT: &str = r#"# Simple zsh configuration +unsetopt beep + +# Basic completion +autoload -Uz compinit +compinit + +# plugins +plugins = (zsh-autosuggestions, zsh-history-substring-search) + +# Aliases +alias ll='ls -la' +alias la='ls -a' +alias h='history' + +# robbyrussell theme prompt +# Arrow changes color based on exit status, directory in cyan, git status +PROMPT='%(?:%{$fg_bold[green]%}➜ :%{$fg_bold[red]%}➜ )%{$fg[cyan]%}%c%{$reset_color%} $(git_prompt_info)' + +ZSH_THEME_GIT_PROMPT_PREFIX="%{$fg_bold[blue]%}git:(%{$fg[red]%}" +ZSH_THEME_GIT_PROMPT_SUFFIX="%{$reset_color%} " +ZSH_THEME_GIT_PROMPT_DIRTY="%{$fg[blue]%}) %{$fg[yellow]%}✗" +ZSH_THEME_GIT_PROMPT_CLEAN="%{$fg[blue]%})" + +# History settings +HISTFILE=window.localStorage["cmd_history"] +HISTSIZE=1000 +SAVEHIST=1000 +setopt SHARE_HISTORY +setopt APPEND_HISTORY + +# zsh-history-substring-search configuration +bindkey '^[[A' history-substring-search-up # or '\eOA' +bindkey '^[[B' history-substring-search-down # or '\eOB' +HISTORY_SUBSTRING_SEARCH_ENSURE_UNIQUE=1 +HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_FOUND=0 +HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_NOT_FOUND=0 +"#; + +#[derive(Debug, Clone)] +pub struct VfsNode { + pub name: String, + pub node_type: VfsNodeType, + pub permissions: Permissions, + pub metadata: NodeMetadata, +} + +impl VfsNode { + pub fn long_meta_string(&self, link_count: usize) -> String { + let is_directory = matches!(self.node_type, VfsNodeType::Directory); + let is_executable = self.permissions.execute; + // Generate permissions string (similar to Unix ls -l format) + let permissions = format!( + "{}{}{}{}{}{}{}{}{}{}", + if is_directory { "d" } else { "-" }, + if self.permissions.read { "r" } else { "-" }, + if self.permissions.write { "w" } else { "-" }, + if is_executable { "x" } else { "-" }, + if self.permissions.read { "r" } else { "-" }, + if self.permissions.write { "w" } else { "-" }, + if is_executable { "x" } else { "-" }, + if self.permissions.read { "r" } else { "-" }, + if self.permissions.write { "w" } else { "-" }, + if is_executable { "x" } else { "-" }, + ); + { + format!( + "{} {:2} {:6} {:6} {:>6} ", + permissions, + link_count, + self.metadata.owner, + self.metadata.group, + self.metadata.size + ) + } + } + + pub fn is_directory(&self) -> bool { + matches!(self.node_type, VfsNodeType::Directory) + } + + pub fn is_executable(&self) -> bool { + self.permissions.execute + } + + pub fn is_hidden(&self) -> bool { + self.name.starts_with(".") + } +} + +#[derive(Debug, Clone)] +pub enum VfsNodeType { + Directory, + File { content: FileContent }, + Link { target: String }, +} + +#[derive(Debug, Clone)] +pub enum FileContent { + Static(&'static str), + Dynamic(String), + NavFile(String), +} + +#[derive(Debug, Clone)] +pub struct Permissions { + pub read: bool, + pub write: bool, + pub execute: bool, + pub immutable: bool, +} + +impl Default for Permissions { + fn default() -> Self { + Self { + read: true, + write: true, + execute: false, + immutable: false, + } + } +} + +impl Permissions { + pub fn read_only() -> Self { + Self { + read: true, + write: false, + execute: false, + immutable: true, + } + } + + pub fn executable() -> Self { + Self { + read: true, + write: false, + execute: true, + immutable: true, + } + } + + pub fn system_dir() -> Self { + Self { + read: true, + write: true, // Allow file creation in system directories + execute: true, + immutable: true, // But prevent deletion of the directory itself + } + } +} + +#[derive(Debug, Clone)] +pub struct NodeMetadata { + pub size: u64, + pub owner: String, + pub group: String, + pub created: DateTime, + pub modified: DateTime, +} + +impl Default for NodeMetadata { + fn default() -> Self { + let now = Local::now(); + Self { + size: 0, + owner: "hans".to_string(), + group: "staff".to_string(), + created: now, + modified: now, + } + } +} + +#[derive(Debug, Clone)] +pub enum VfsError { + NotFound, + PermissionDenied, + NotADirectory, + NotAFile, + AlreadyExists, + QuotaExceeded, + InvalidPath, + SystemError(String), +} + +pub struct VirtualFilesystem { + arena: Arena, + root: NodeId, +} + +impl VirtualFilesystem { + pub fn new(blog_posts: Vec) -> Self { + let mut arena = Arena::new(); + + // Create root directory (writable by users) + let root_node = VfsNode { + name: String::new(), + node_type: VfsNodeType::Directory, + permissions: Permissions::default(), + metadata: NodeMetadata { + size: 4096, + ..Default::default() + }, + }; + + let root = arena.new_node(root_node); + + let mut vfs = Self { arena, root }; + + // Initialize the filesystem structure + vfs.initialize_system_structure(blog_posts); + + vfs + } + + fn initialize_system_structure(&mut self, blog_posts: Vec) { + // Create system directories + let blog_id = self.create_system_directory("blog").unwrap(); + let cv_id = self.create_system_directory("cv").unwrap(); + + // Create system files + self.create_system_file("mines.sh", FileContent::Static(MINES_SH), true) + .unwrap(); + self.create_system_file("thanks.txt", FileContent::Static(THANKS_TXT), false) + .unwrap(); + self.create_system_file(".zshrc", FileContent::Static(ZSHRC_CONTENT), false) + .unwrap(); + self.create_system_file("nav.rs", FileContent::NavFile("/".to_string()), true) + .unwrap(); + + // Create blog directory files + self.create_system_file_in( + "nav.rs", + FileContent::NavFile("/blog".to_string()), + true, + blog_id, + ) + .unwrap(); + + // Create blog post directories + for post in blog_posts { + let post_dir_id = self.create_system_directory_in(&post, blog_id).unwrap(); + let post_path = format!("/blog/{post}"); + self.create_system_file_in( + "nav.rs", + FileContent::NavFile(post_path), + true, + post_dir_id, + ) + .unwrap(); + } + + // Create cv directory files + self.create_system_file_in( + "nav.rs", + FileContent::NavFile("/cv".to_string()), + true, + cv_id, + ) + .unwrap(); + } + + fn create_system_directory(&mut self, name: &str) -> Result { + let dir_node = VfsNode { + name: name.to_string(), + node_type: VfsNodeType::Directory, + permissions: Permissions::system_dir(), + metadata: NodeMetadata { + size: 4096, + ..Default::default() + }, + }; + + let dir_id = self.arena.new_node(dir_node); + self.root.append(dir_id, &mut self.arena); + Ok(dir_id) + } + + fn create_system_directory_in( + &mut self, + name: &str, + parent: NodeId, + ) -> Result { + let dir_node = VfsNode { + name: name.to_string(), + node_type: VfsNodeType::Directory, + permissions: Permissions::system_dir(), + metadata: NodeMetadata { + size: 4096, + ..Default::default() + }, + }; + + let dir_id = self.arena.new_node(dir_node); + parent.append(dir_id, &mut self.arena); + Ok(dir_id) + } + + fn create_system_file( + &mut self, + name: &str, + content: FileContent, + executable: bool, + ) -> Result { + self.create_system_file_in(name, content, executable, self.root) + } + + fn create_system_file_in( + &mut self, + name: &str, + content: FileContent, + executable: bool, + parent: NodeId, + ) -> Result { + let size = match &content { + FileContent::Static(s) => s.len() as u64, + FileContent::Dynamic(s) => s.len() as u64, + FileContent::NavFile(_) => 512, + }; + + let permissions = if executable { + Permissions::executable() + } else { + Permissions::read_only() + }; + + let group = if executable { "wheel" } else { "staff" }; + + let file_node = VfsNode { + name: name.to_string(), + node_type: VfsNodeType::File { content }, + permissions, + metadata: NodeMetadata { + size, + group: group.to_string(), + ..Default::default() + }, + }; + + let file_id = self.arena.new_node(file_node); + parent.append(file_id, &mut self.arena); + Ok(file_id) + } + + // Path resolution + pub fn resolve_path(&self, base: NodeId, path: &str) -> Result { + if path.is_empty() || path == "." { + return Ok(base); + } + + // Handle ~ expansion + let expanded_path = if path == "~" { + return Ok(self.root); // ~ alone means home (root) + } else if path.starts_with("~/") { + path.strip_prefix('~').unwrap() // ~/foo becomes /foo + } else { + path + }; + + if expanded_path.starts_with('/') { + let stripped = expanded_path.strip_prefix('/').unwrap(); + // Absolute path + self.resolve_path_from(self.root, stripped) + } else { + // Relative path + self.resolve_path_from(base, expanded_path) + } + } + + fn resolve_path_from(&self, mut current: NodeId, path: &str) -> Result { + if path.is_empty() { + return Ok(current); + } + + let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect(); + + for part in parts { + match part { + "." => continue, + ".." => { + // Use indextree's parent functionality! + if let Some(parent) = self.arena[current].parent() { + current = parent; + } + // If no parent (at root), stay at root + } + name => { + // Find child with matching name + let mut found = false; + for child_id in current.children(&self.arena) { + if let Some(child_node) = self.arena.get(child_id) { + if child_node.get().name == name { + current = child_id; + found = true; + break; + } + } + } + if !found { + return Err(VfsError::NotFound); + } + } + } + } + + Ok(current) + } + + // CRUD operations + pub fn create_file( + &mut self, + parent: NodeId, + name: &str, + content: FileContent, + ) -> Result { + // Validate parent is a directory + let parent_node = self.arena.get(parent).ok_or(VfsError::NotFound)?; + + // Check permissions + if !parent_node.get().permissions.write { + return Err(VfsError::PermissionDenied); + } + + // Ensure it's a directory + if !matches!(parent_node.get().node_type, VfsNodeType::Directory) { + return Err(VfsError::NotADirectory); + } + + // Check if name already exists + for child_id in parent.children(&self.arena) { + if let Some(child_node) = self.arena.get(child_id) { + if child_node.get().name == name { + return Err(VfsError::AlreadyExists); + } + } + } + + // Calculate file size + let size = match &content { + FileContent::Static(s) => s.len() as u64, + FileContent::Dynamic(s) => s.len() as u64, + FileContent::NavFile(_) => 512, + }; + + // Create the file node + let file_node = VfsNode { + name: name.to_string(), + node_type: VfsNodeType::File { content }, + permissions: Permissions::default(), + metadata: NodeMetadata { + size, + owner: "user".to_string(), + group: "user".to_string(), + ..Default::default() + }, + }; + + let file_id = self.arena.new_node(file_node); + parent.append(file_id, &mut self.arena); + + Ok(file_id) + } + + pub fn create_directory(&mut self, parent: NodeId, name: &str) -> Result { + // Validate parent is a directory + let parent_node = self.arena.get(parent).ok_or(VfsError::NotFound)?; + + // Check permissions + if !parent_node.get().permissions.write { + return Err(VfsError::PermissionDenied); + } + + // Ensure it's a directory + if !matches!(parent_node.get().node_type, VfsNodeType::Directory) { + return Err(VfsError::NotADirectory); + } + + // Check if name already exists + for child_id in parent.children(&self.arena) { + if let Some(child_node) = self.arena.get(child_id) { + if child_node.get().name == name { + return Err(VfsError::AlreadyExists); + } + } + } + + // Create the directory node + let dir_node = VfsNode { + name: name.to_string(), + node_type: VfsNodeType::Directory, + permissions: Permissions::default(), + metadata: NodeMetadata { + size: 4096, + owner: "user".to_string(), + group: "user".to_string(), + ..Default::default() + }, + }; + + let dir_id = self.arena.new_node(dir_node); + parent.append(dir_id, &mut self.arena); + + Ok(dir_id) + } + + pub fn read_file(&self, node: NodeId) -> Result { + let node_ref = self.arena.get(node).ok_or(VfsError::NotFound)?; + let node_data = node_ref.get(); + + // Check read permission + if !node_data.permissions.read { + return Err(VfsError::PermissionDenied); + } + + match &node_data.node_type { + VfsNodeType::File { content } => match content { + FileContent::Static(s) => Ok(s.to_string()), + FileContent::Dynamic(s) => Ok(s.clone()), + FileContent::NavFile(path) => Ok(generate_nav_content(path)), + }, + VfsNodeType::Directory => Err(VfsError::NotAFile), + VfsNodeType::Link { target } => { + // Follow the link and read the target + let target_node = self.resolve_path(self.root, target)?; + self.read_file(target_node) + } + } + } + + pub fn list_directory(&self, node: NodeId) -> Result, VfsError> { + let node_ref = self.arena.get(node).ok_or(VfsError::NotFound)?; + let node_data = node_ref.get(); + + // Check read permission + if !node_data.permissions.read { + return Err(VfsError::PermissionDenied); + } + + match &node_data.node_type { + VfsNodeType::Directory => { + let mut entries = Vec::new(); + + for child_id in node.children(&self.arena) { + if let Some(child_ref) = self.arena.get(child_id) { + let child_data = child_ref.get(); + entries.push(DirEntry { + name: child_data.name.clone(), + node_id: child_id, + is_directory: matches!(child_data.node_type, VfsNodeType::Directory), + is_executable: child_data.permissions.execute, + }); + } + } + + entries.sort_by(|a, b| a.name.cmp(&b.name)); + Ok(entries) + } + VfsNodeType::File { .. } => Err(VfsError::NotADirectory), + VfsNodeType::Link { target } => { + // Follow the link and list the target + let target_node = self.resolve_path(self.root, target)?; + self.list_directory(target_node) + } + } + } + + pub fn delete_node(&mut self, node: NodeId) -> Result<(), VfsError> { + // Can't delete root + if node == self.root { + return Err(VfsError::PermissionDenied); + } + + // Check if node exists and get its info + let node_ref = self.arena.get(node).ok_or(VfsError::NotFound)?; + let node_data = node_ref.get(); + + // Check permissions + if node_data.permissions.immutable { + return Err(VfsError::PermissionDenied); + } + + // If it's a directory, ensure it's empty + if matches!(node_data.node_type, VfsNodeType::Directory) + && node.children(&self.arena).next().is_some() + { + return Err(VfsError::SystemError("Directory not empty".to_string())); + } + + // Remove the node from the tree (indextree handles parent cleanup!) + node.remove(&mut self.arena); + + Ok(()) + } + + pub fn delete_node_recursive(&mut self, node: NodeId) -> Result<(), VfsError> { + // Can't delete root + if node == self.root { + return Err(VfsError::PermissionDenied); + } + + // Check if node exists and get its info + let node_ref = self.arena.get(node).ok_or(VfsError::NotFound)?; + let node_data = node_ref.get(); + + // Check permissions + if node_data.permissions.immutable { + return Err(VfsError::PermissionDenied); + } + + // If it's a directory, recursively delete children first + if matches!(node_data.node_type, VfsNodeType::Directory) { + // Collect children to avoid borrowing issues + let children: Vec = node.children(&self.arena).collect(); + + for child_id in children { + self.delete_node_recursive(child_id)?; + } + } + + // Remove the node from the tree (indextree handles parent cleanup!) + node.remove(&mut self.arena); + + Ok(()) + } + + // Get the full path of a node - MUCH simpler with indextree! + pub fn get_node_path(&self, node: NodeId) -> String { + if node == self.root { + return "/".to_string(); + } + + // Collect ancestors (excluding root) + let mut path_parts = Vec::new(); + let mut current = node; + + while let Some(parent) = self.arena[current].parent() { + if let Some(node_ref) = self.arena.get(current) { + let node_data = node_ref.get(); + if !node_data.name.is_empty() { + // Skip root (empty name) + path_parts.push(node_data.name.clone()); + } + } + current = parent; + } + + // Reverse to get path from root to node + path_parts.reverse(); + + if path_parts.is_empty() { + "/".to_string() + } else { + format!("/{}", path_parts.join("/")) + } + } + + pub fn get_node(&self, node: NodeId) -> Option<&VfsNode> { + self.arena.get(node).map(|node_ref| node_ref.get()) + } + + pub fn get_root(&self) -> NodeId { + self.root + } + + pub fn get_parent(&self, node: NodeId) -> Option { + self.arena[node].parent() + } +} + +#[derive(Debug, Clone)] +pub struct DirEntry { + pub name: String, + pub node_id: NodeId, + pub is_directory: bool, + pub is_executable: bool, +} + +// Helper function to create nav.rs content +fn generate_nav_content(path: &str) -> String { + let path = if path.is_empty() { "/" } else { path }; + format!( + r#"use leptos::prelude::*; +use leptos_router::{{hooks::use_navigate, UseNavigateOptions}}; + +func main() {{ + Effect::new((_) => {{ + let navigate = use_navigate(); + navigate("{path}", UseNavigateOptions::default); + }}) +}} +"# + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_vfs_initialization() { + let blog_posts = vec!["post1".to_string(), "post2".to_string()]; + let vfs = VirtualFilesystem::new(blog_posts); + + // Test root directory exists + let root = vfs.get_root(); + let root_node = vfs.get_node(root).expect("Root node should exist"); + assert!(matches!(root_node.node_type, VfsNodeType::Directory)); + + // Test basic system directories exist + assert!(vfs.resolve_path(root, "/blog").is_ok()); + assert!(vfs.resolve_path(root, "/cv").is_ok()); + + // Test system files exist + assert!(vfs.resolve_path(root, "/mines.sh").is_ok()); + assert!(vfs.resolve_path(root, "/thanks.txt").is_ok()); + assert!(vfs.resolve_path(root, "/.zshrc").is_ok()); + assert!(vfs.resolve_path(root, "/nav.rs").is_ok()); + } + + #[test] + fn test_path_resolution() { + let blog_posts = vec!["example-post".to_string()]; + let vfs = VirtualFilesystem::new(blog_posts); + let root = vfs.get_root(); + + // Test absolute paths + assert!(vfs.resolve_path(root, "/").is_ok()); + assert!(vfs.resolve_path(root, "/blog").is_ok()); + assert!(vfs.resolve_path(root, "/blog/example-post").is_ok()); + + // Test relative paths from root + assert!(vfs.resolve_path(root, "blog").is_ok()); + assert!(vfs.resolve_path(root, "./blog").is_ok()); + + // Test parent directory navigation + let blog_id = vfs.resolve_path(root, "/blog").unwrap(); + let back_to_root = vfs.resolve_path(blog_id, "..").unwrap(); + assert_eq!(back_to_root, root); + + // Test invalid paths + assert!(vfs.resolve_path(root, "/nonexistent").is_err()); + assert!(vfs.resolve_path(root, "/blog/nonexistent").is_err()); + } + + #[test] + fn test_file_reading() { + let blog_posts = vec![]; + let vfs = VirtualFilesystem::new(blog_posts); + let root = vfs.get_root(); + + // Test reading static files + let mines_id = vfs.resolve_path(root, "/mines.sh").unwrap(); + let content = vfs.read_file(mines_id).unwrap(); + assert!(content.contains("mines")); + + let thanks_id = vfs.resolve_path(root, "/thanks.txt").unwrap(); + let content = vfs.read_file(thanks_id).unwrap(); + assert!(content.contains("Thank you")); + + // Test reading nav file (virtual content) + let nav_id = vfs.resolve_path(root, "/nav.rs").unwrap(); + let content = vfs.read_file(nav_id).unwrap(); + assert!(content.contains("use_navigate")); + assert!(content.contains("\"/\"")); + } + + #[test] + fn test_directory_listing() { + let blog_posts = vec!["post1".to_string(), "post2".to_string()]; + let vfs = VirtualFilesystem::new(blog_posts); + let root = vfs.get_root(); + + // Test root directory listing + let entries = vfs.list_directory(root).unwrap(); + let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect(); + + assert!(names.contains(&"blog")); + assert!(names.contains(&"cv")); + assert!(names.contains(&"mines.sh")); + assert!(names.contains(&"thanks.txt")); + assert!(names.contains(&".zshrc")); + assert!(names.contains(&"nav.rs")); + + // Test blog directory listing + let blog_id = vfs.resolve_path(root, "/blog").unwrap(); + let blog_entries = vfs.list_directory(blog_id).unwrap(); + let blog_names: Vec<&str> = blog_entries.iter().map(|e| e.name.as_str()).collect(); + + assert!(blog_names.contains(&"post1")); + assert!(blog_names.contains(&"post2")); + assert!(blog_names.contains(&"nav.rs")); + } + + #[test] + fn test_user_file_creation() { + let blog_posts = vec![]; + let mut vfs = VirtualFilesystem::new(blog_posts); + let root = vfs.get_root(); + + // Create a user file + let file_id = vfs + .create_file( + root, + "test.txt", + FileContent::Dynamic("Hello, world!".to_string()), + ) + .unwrap(); + + // Verify it was created + let content = vfs.read_file(file_id).unwrap(); + assert_eq!(content, "Hello, world!"); + + // Verify it appears in directory listing + let entries = vfs.list_directory(root).unwrap(); + let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect(); + assert!(names.contains(&"test.txt")); + } + + #[test] + fn test_user_directory_creation() { + let blog_posts = vec![]; + let mut vfs = VirtualFilesystem::new(blog_posts); + let root = vfs.get_root(); + + // Create a user directory + let dir_id = vfs.create_directory(root, "mydir").unwrap(); + + // Verify it was created and is a directory + let node = vfs.get_node(dir_id).unwrap(); + assert!(matches!(node.node_type, VfsNodeType::Directory)); + + // Verify it appears in directory listing + let entries = vfs.list_directory(root).unwrap(); + let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect(); + assert!(names.contains(&"mydir")); + + // Create a file inside the directory + vfs.create_file( + dir_id, + "nested.txt", + FileContent::Dynamic("nested content".to_string()), + ) + .unwrap(); + + // Verify the nested file can be accessed + let nested_id = vfs.resolve_path(root, "/mydir/nested.txt").unwrap(); + let content = vfs.read_file(nested_id).unwrap(); + assert_eq!(content, "nested content"); + } + + #[test] + fn test_system_file_immutability() { + let blog_posts = vec![]; + let mut vfs = VirtualFilesystem::new(blog_posts); + let root = vfs.get_root(); + + // We can now create files in system directories (they have write permission) + let blog_id = vfs.resolve_path(root, "/blog").unwrap(); + let result = vfs.create_file( + blog_id, + "user_content.txt", + FileContent::Dynamic("user created content".to_string()), + ); + + // File creation should succeed + assert!(result.is_ok()); + let _file_id = result.unwrap(); + + // But we still can't delete system directories + let delete_result = vfs.delete_node(blog_id); + assert!(delete_result.is_err()); + assert!(matches!( + delete_result.unwrap_err(), + VfsError::PermissionDenied + )); + + // System files (like mines.sh) still can't be deleted + let mines_id = vfs.resolve_path(root, "mines.sh").unwrap(); + let delete_mines = vfs.delete_node(mines_id); + assert!(delete_mines.is_err()); + assert!(matches!( + delete_mines.unwrap_err(), + VfsError::PermissionDenied + )); + } + + #[test] + fn test_file_deletion() { + let blog_posts = vec![]; + let mut vfs = VirtualFilesystem::new(blog_posts); + let root = vfs.get_root(); + + // Create a user file + let file_id = vfs + .create_file( + root, + "deleteme.txt", + FileContent::Dynamic("temporary content".to_string()), + ) + .unwrap(); + + // Verify it exists + assert!(vfs.read_file(file_id).is_ok()); + + // Delete it + vfs.delete_node(file_id).unwrap(); + + // Verify it's gone + assert!(vfs.resolve_path(root, "/deleteme.txt").is_err()); + } + + #[test] + fn test_directory_deletion() { + let blog_posts = vec![]; + let mut vfs = VirtualFilesystem::new(blog_posts); + let root = vfs.get_root(); + + // Create a user directory + let dir_id = vfs.create_directory(root, "tempdir").unwrap(); + + // Verify it exists + assert!(vfs.resolve_path(root, "/tempdir").is_ok()); + + // Delete it + vfs.delete_node(dir_id).unwrap(); + + // Verify it's gone + assert!(vfs.resolve_path(root, "/tempdir").is_err()); + } + + #[test] + fn test_cannot_delete_non_empty_directory() { + let blog_posts = vec![]; + let mut vfs = VirtualFilesystem::new(blog_posts); + let root = vfs.get_root(); + + // Create a directory with a file + let dir_id = vfs.create_directory(root, "nonempty").unwrap(); + vfs.create_file( + dir_id, + "file.txt", + FileContent::Dynamic("content".to_string()), + ) + .unwrap(); + + // Try to delete the directory (should fail) + let result = vfs.delete_node(dir_id); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), VfsError::SystemError(_))); + } + + #[test] + fn test_cannot_delete_system_files() { + let blog_posts = vec![]; + let mut vfs = VirtualFilesystem::new(blog_posts); + let root = vfs.get_root(); + + // Try to delete a system file + let mines_id = vfs.resolve_path(root, "/mines.sh").unwrap(); + let result = vfs.delete_node(mines_id); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), VfsError::PermissionDenied)); + } + + #[test] + fn test_cannot_delete_root() { + let blog_posts = vec![]; + let mut vfs = VirtualFilesystem::new(blog_posts); + let root = vfs.get_root(); + + // Try to delete root + let result = vfs.delete_node(root); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), VfsError::PermissionDenied)); + } + + #[test] + fn test_duplicate_name_prevention() { + let blog_posts = vec![]; + let mut vfs = VirtualFilesystem::new(blog_posts); + let root = vfs.get_root(); + + // Create a file + vfs.create_file( + root, + "duplicate.txt", + FileContent::Dynamic("first".to_string()), + ) + .unwrap(); + + // Try to create another file with the same name + let result = vfs.create_file( + root, + "duplicate.txt", + FileContent::Dynamic("second".to_string()), + ); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), VfsError::AlreadyExists)); + + // Same for directories + vfs.create_directory(root, "dupdir").unwrap(); + let result = vfs.create_directory(root, "dupdir"); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), VfsError::AlreadyExists)); + } + + #[test] + fn test_path_generation() { + let blog_posts = vec!["example-post".to_string()]; + let vfs = VirtualFilesystem::new(blog_posts); + let root = vfs.get_root(); + + // Test root path + assert_eq!(vfs.get_node_path(root), "/"); + + // Test simple paths + let blog_id = vfs.resolve_path(root, "/blog").unwrap(); + assert_eq!(vfs.get_node_path(blog_id), "/blog"); + + let cv_id = vfs.resolve_path(root, "/cv").unwrap(); + assert_eq!(vfs.get_node_path(cv_id), "/cv"); + + // Test nested paths + let post_id = vfs.resolve_path(root, "/blog/example-post").unwrap(); + assert_eq!(vfs.get_node_path(post_id), "/blog/example-post"); + } + + #[test] + fn test_complex_path_navigation() { + let blog_posts = vec!["post1".to_string()]; + let vfs = VirtualFilesystem::new(blog_posts); + let root = vfs.get_root(); + + // Navigate to deep path then back up + let post_id = vfs.resolve_path(root, "/blog/post1").unwrap(); + let back_to_blog = vfs.resolve_path(post_id, "..").unwrap(); + let back_to_root = vfs.resolve_path(back_to_blog, "..").unwrap(); + + assert_eq!(back_to_root, root); + assert_eq!(vfs.get_node_path(back_to_blog), "/blog"); + + // Test relative path navigation - go to blog dir, then up and over to cv + let blog_id = vfs.resolve_path(root, "/blog").unwrap(); + let cv_via_relative = vfs.resolve_path(blog_id, "../cv").unwrap(); + assert_eq!(vfs.get_node_path(cv_via_relative), "/cv"); + } + + #[test] + fn test_empty_and_dot_paths() { + let blog_posts = vec![]; + let vfs = VirtualFilesystem::new(blog_posts); + let root = vfs.get_root(); + + // Test empty path resolves to base + assert_eq!(vfs.resolve_path(root, "").unwrap(), root); + + // Test dot path resolves to base + assert_eq!(vfs.resolve_path(root, ".").unwrap(), root); + + // Test from subdirectory + let blog_id = vfs.resolve_path(root, "/blog").unwrap(); + assert_eq!(vfs.resolve_path(blog_id, "").unwrap(), blog_id); + assert_eq!(vfs.resolve_path(blog_id, ".").unwrap(), blog_id); + } + + #[test] + fn test_complex_relative_paths() { + let blog_posts = vec!["post1".to_string(), "post2".to_string()]; + let mut vfs = VirtualFilesystem::new(blog_posts); + let root = vfs.get_root(); + + // Create nested structure for testing + let user_dir = vfs.create_directory(root, "user").unwrap(); + let projects_dir = vfs.create_directory(user_dir, "projects").unwrap(); + let deep_dir = vfs.create_directory(projects_dir, "deep").unwrap(); + + // Test multiple .. segments + let back_to_root = vfs.resolve_path(deep_dir, "../../..").unwrap(); + assert_eq!(back_to_root, root); + + // Test multiple .. with final directory + let back_to_user = vfs.resolve_path(deep_dir, "../..").unwrap(); + assert_eq!(back_to_user, user_dir); + + // Test .. beyond root (should stay at root) + let still_root = vfs.resolve_path(root, "../../..").unwrap(); + assert_eq!(still_root, root); + + // Test complex path with mixed . and .. + let mixed_path = vfs + .resolve_path(deep_dir, "./../../../user/./projects") + .unwrap(); + assert_eq!(mixed_path, projects_dir); + + // Test relative path from deep location to system directory + let to_blog = vfs.resolve_path(deep_dir, "../../../blog").unwrap(); + let blog_id = vfs.resolve_path(root, "/blog").unwrap(); + assert_eq!(to_blog, blog_id); + + // Test relative path with intermediate directory traversal + let complex_nav = vfs + .resolve_path(deep_dir, "../../projects/../projects/deep") + .unwrap(); + assert_eq!(complex_nav, deep_dir); + } + + #[test] + fn test_relative_paths_with_files() { + let blog_posts = vec!["example-post".to_string()]; + let mut vfs = VirtualFilesystem::new(blog_posts); + let root = vfs.get_root(); + + // Create test structure + let test_dir = vfs.create_directory(root, "test").unwrap(); + let sub_dir = vfs.create_directory(test_dir, "sub").unwrap(); + vfs.create_file( + sub_dir, + "file.txt", + FileContent::Dynamic("test content".to_string()), + ) + .unwrap(); + + // Navigate to file via complex relative path + let file_via_complex = vfs + .resolve_path(root, "test/./sub/../sub/./file.txt") + .unwrap(); + let content = vfs.read_file(file_via_complex).unwrap(); + assert_eq!(content, "test content"); + + // Navigate from file back to root via complex path + let back_to_root = vfs.resolve_path(file_via_complex, "../../..").unwrap(); + assert_eq!(back_to_root, root); + + // Navigate from file to system file via complex relative path + let to_mines = vfs + .resolve_path(file_via_complex, "../../.././mines.sh") + .unwrap(); + let mines_content = vfs.read_file(to_mines).unwrap(); + assert!(mines_content.contains("mines")); + } + + #[test] + fn test_paths_with_redundant_separators() { + let blog_posts = vec!["post1".to_string()]; + let vfs = VirtualFilesystem::new(blog_posts); + let root = vfs.get_root(); + + // Test paths with multiple consecutive dots and slashes + let blog_id = vfs.resolve_path(root, "/blog").unwrap(); + + // These should all resolve to the same location + let path1 = vfs.resolve_path(blog_id, "../././blog/./post1").unwrap(); + let path2 = vfs.resolve_path(blog_id, ".././blog/post1").unwrap(); + let path3 = vfs + .resolve_path(blog_id, "././../blog/./././post1") + .unwrap(); + let direct_path = vfs.resolve_path(root, "/blog/post1").unwrap(); + + assert_eq!(path1, direct_path); + assert_eq!(path2, direct_path); + assert_eq!(path3, direct_path); + } + + #[test] + fn test_relative_navigation_from_deep_blog_posts() { + let blog_posts = vec!["deep-post".to_string(), "other-post".to_string()]; + let vfs = VirtualFilesystem::new(blog_posts); + let root = vfs.get_root(); + + // Verify both blog posts exist + let deep_post = vfs.resolve_path(root, "/blog/deep-post").unwrap(); + let other_post = vfs.resolve_path(root, "/blog/other-post").unwrap(); + let nav_file = vfs.resolve_path(deep_post, "nav.rs").unwrap(); + + // Navigate to CV from deep blog location using nav.rs as starting point + // nav.rs -> .. (to post dir) -> .. (to blog dir) -> .. (to root) -> cv + let cv_via_relative = vfs.resolve_path(nav_file, "../../../cv").unwrap(); + let cv_direct = vfs.resolve_path(root, "/cv").unwrap(); + assert_eq!(cv_via_relative, cv_direct); + + // Navigate from blog directory to other post (not from nav.rs file) + let blog_dir = vfs.resolve_path(root, "/blog").unwrap(); + let other_via_relative = vfs.resolve_path(blog_dir, "other-post").unwrap(); + assert_eq!(other_via_relative, other_post); + + // Complex navigation: blog post -> root -> back to same blog post + let roundtrip = vfs + .resolve_path(nav_file, "../../.././blog/deep-post") + .unwrap(); + assert_eq!(roundtrip, deep_post); + } + + #[test] + fn test_edge_case_relative_paths() { + let blog_posts = vec![]; + let mut vfs = VirtualFilesystem::new(blog_posts); + let root = vfs.get_root(); + + // Create nested structure for edge case testing + let a = vfs.create_directory(root, "a").unwrap(); + let b = vfs.create_directory(a, "b").unwrap(); + let c = vfs.create_directory(b, "c").unwrap(); + + // Test many consecutive dots and parent traversals + let many_dots = vfs.resolve_path(c, "./././././.").unwrap(); + assert_eq!(many_dots, c); + + // Test excessive parent traversal (should clamp at root) + let excessive_parents = vfs.resolve_path(c, "../../../../../../../..").unwrap(); + assert_eq!(excessive_parents, root); + + // Test mixed excessive traversal with final valid path + let mixed_excessive = vfs.resolve_path(c, "../../../../../../../../blog").unwrap(); + let blog_direct = vfs.resolve_path(root, "/blog").unwrap(); + assert_eq!(mixed_excessive, blog_direct); + + // Test path that goes up and down multiple times + let zigzag = vfs.resolve_path(c, "../../../a/b/../b/c/../c").unwrap(); + assert_eq!(zigzag, c); + } +} From a1be2fd82e8fa73fe86649e956e2857fb31406e5 Mon Sep 17 00:00:00 2001 From: BakerNet Date: Sun, 29 Jun 2025 08:12:15 -0700 Subject: [PATCH 2/8] Remove no longer valid comment --- src/app/header.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/header.rs b/src/app/header.rs index 62f1b06..b89ce99 100644 --- a/src/app/header.rs +++ b/src/app/header.rs @@ -22,7 +22,7 @@ use leptos_use::storage::use_local_storage; use crate::blog::Assets; -use super::terminal::{ColumnarView, CommandRes, Terminal, TabCompletionItem}; +use super::terminal::{ColumnarView, CommandRes, TabCompletionItem, Terminal}; #[component] fn MobileFloatingButton(on_click: impl Fn() + 'static) -> impl IntoView { @@ -206,7 +206,6 @@ pub fn Header() -> impl IntoView { .map(|s| s[..s.len() - 3].to_string()) .collect::>(); - // Initialize terminal with current browser path for proper VFS state let terminal = StoredValue::new(Arc::new(Mutex::new(Terminal::new(&blog_posts, None)))); let input_ref = NodeRef::::new(); let floating_input_ref = NodeRef::::new(); From 5d1bccaed13f2d402ae499d1d583dfb15f698826 Mon Sep 17 00:00:00 2001 From: BakerNet Date: Sun, 29 Jun 2025 08:17:11 -0700 Subject: [PATCH 3/8] Rename tools --- src/app/terminal.rs | 22 +++++++++----------- src/app/terminal/cp_mv_tools.rs | 22 ++++++++++---------- src/app/terminal/fs_tools.rs | 36 ++++++++++++++++----------------- 3 files changed, 39 insertions(+), 41 deletions(-) diff --git a/src/app/terminal.rs b/src/app/terminal.rs index a44069f..d96f89f 100644 --- a/src/app/terminal.rs +++ b/src/app/terminal.rs @@ -14,10 +14,8 @@ use std::collections::{HashMap, VecDeque}; use command::{Cmd, CmdAlias, Command, VfsCommand}; use components::TextContent; -use cp_mv_tools::{VfsCpCommand, VfsMvCommand}; -use fs_tools::{ - VfsCatCommand, VfsCdCommand, VfsLsCommand, VfsMkdirCommand, VfsRmCommand, VfsTouchCommand, -}; +use cp_mv_tools::{CpCommand, MvCommand}; +use fs_tools::{CatCommand, CdCommand, LsCommand, MkdirCommand, RmCommand, TouchCommand}; use indextree::NodeId; use ps_tools::{KillCommand, Process, PsCommand}; use simple_tools::{ @@ -153,23 +151,23 @@ impl Terminal { fn initialize_vfs_commands(&mut self) { // VFS-aware commands that need direct filesystem access self.vfs_commands - .insert(Cmd::Ls, Box::new(VfsLsCommand::new())); + .insert(Cmd::Ls, Box::new(LsCommand::new())); self.vfs_commands - .insert(Cmd::Cd, Box::new(VfsCdCommand::new())); + .insert(Cmd::Cd, Box::new(CdCommand::new())); self.vfs_commands - .insert(Cmd::Cat, Box::new(VfsCatCommand::new())); + .insert(Cmd::Cat, Box::new(CatCommand::new())); self.vfs_commands - .insert(Cmd::Touch, Box::new(VfsTouchCommand::new())); + .insert(Cmd::Touch, Box::new(TouchCommand::new())); self.vfs_commands - .insert(Cmd::MkDir, Box::new(VfsMkdirCommand::new())); + .insert(Cmd::MkDir, Box::new(MkdirCommand::new())); self.vfs_commands - .insert(Cmd::Rm, Box::new(VfsRmCommand::new())); + .insert(Cmd::Rm, Box::new(RmCommand::new())); self.vfs_commands .insert(Cmd::Which, Box::new(WhichCommand::new())); self.vfs_commands - .insert(Cmd::Cp, Box::new(VfsCpCommand::new())); + .insert(Cmd::Cp, Box::new(CpCommand::new())); self.vfs_commands - .insert(Cmd::Mv, Box::new(VfsMvCommand::new())); + .insert(Cmd::Mv, Box::new(MvCommand::new())); } #[cfg(feature = "hydrate")] diff --git a/src/app/terminal/cp_mv_tools.rs b/src/app/terminal/cp_mv_tools.rs index 65544fb..44292b9 100644 --- a/src/app/terminal/cp_mv_tools.rs +++ b/src/app/terminal/cp_mv_tools.rs @@ -1,19 +1,19 @@ use indextree::NodeId; use super::command::{CommandRes, VfsCommand}; -use super::fs_tools::{parse_multitarget, VfsRmCommand}; +use super::fs_tools::{parse_multitarget, RmCommand}; use super::vfs::{FileContent, VfsError, VfsNodeType, VirtualFilesystem}; // VFS-based CpCommand for Phase 2 migration -pub struct VfsCpCommand; +pub struct CpCommand; -impl VfsCpCommand { +impl CpCommand { pub fn new() -> Self { Self } } -impl VfsCommand for VfsCpCommand { +impl VfsCommand for CpCommand { fn execute( &self, vfs: &mut VirtualFilesystem, @@ -71,7 +71,7 @@ This version of cp only supports options 'r' and 'f'"# } } -impl VfsCpCommand { +impl CpCommand { fn copy_item( &self, vfs: &mut VirtualFilesystem, @@ -236,15 +236,15 @@ impl VfsCpCommand { } // VFS-based MvCommand for Phase 2 migration -pub struct VfsMvCommand; +pub struct MvCommand; -impl VfsMvCommand { +impl MvCommand { pub fn new() -> Self { Self } } -impl VfsCommand for VfsMvCommand { +impl VfsCommand for MvCommand { fn execute( &self, vfs: &mut VirtualFilesystem, @@ -288,7 +288,7 @@ impl VfsCommand for VfsMvCommand { } } -impl VfsMvCommand { +impl MvCommand { fn move_item( &self, vfs: &mut VirtualFilesystem, @@ -329,7 +329,7 @@ impl VfsMvCommand { } // Step 2: Copy source to destination (with -r if directory) - let cp_command = VfsCpCommand::new(); + let cp_command = CpCommand::new(); let cp_args = if is_directory { vec!["-r", source_path, dest_path] } else { @@ -353,7 +353,7 @@ impl VfsMvCommand { } // Step 3: Remove the source (with -r if directory) - let rm_command = VfsRmCommand::new(); + let rm_command = RmCommand::new(); let rm_args = if is_directory { vec!["-r", source_path] } else { diff --git a/src/app/terminal/fs_tools.rs b/src/app/terminal/fs_tools.rs index 8d69cbc..7c60249 100644 --- a/src/app/terminal/fs_tools.rs +++ b/src/app/terminal/fs_tools.rs @@ -43,15 +43,15 @@ impl TextContent for VfsItem { } // VFS-based LsCommand for Phase 2 migration -pub struct VfsLsCommand; +pub struct LsCommand; -impl VfsLsCommand { +impl LsCommand { pub fn new() -> Self { Self } } -impl VfsCommand for VfsLsCommand { +impl VfsCommand for LsCommand { fn execute( &self, vfs: &mut VirtualFilesystem, @@ -352,15 +352,15 @@ fn VfsLsView(items: Vec, #[prop(default = false)] long_format: bool) -> } // VFS-based CdCommand for Phase 2 migration -pub struct VfsCdCommand; +pub struct CdCommand; -impl VfsCdCommand { +impl CdCommand { pub fn new() -> Self { Self } } -impl VfsCommand for VfsCdCommand { +impl VfsCommand for CdCommand { fn execute( &self, vfs: &mut VirtualFilesystem, @@ -422,15 +422,15 @@ impl VfsCommand for VfsCdCommand { } // VFS-based CatCommand for Phase 2 migration -pub struct VfsCatCommand; +pub struct CatCommand; -impl VfsCatCommand { +impl CatCommand { pub fn new() -> Self { Self } } -impl VfsCommand for VfsCatCommand { +impl VfsCommand for CatCommand { fn execute( &self, vfs: &mut VirtualFilesystem, @@ -528,15 +528,15 @@ This version of cat doesn't support any options"# } // VFS-based TouchCommand for Phase 2 migration -pub struct VfsTouchCommand; +pub struct TouchCommand; -impl VfsTouchCommand { +impl TouchCommand { pub fn new() -> Self { Self } } -impl VfsCommand for VfsTouchCommand { +impl VfsCommand for TouchCommand { fn execute( &self, vfs: &mut VirtualFilesystem, @@ -636,15 +636,15 @@ impl VfsCommand for VfsTouchCommand { } // VFS-based MkdirCommand for Phase 2 migration -pub struct VfsMkdirCommand; +pub struct MkdirCommand; -impl VfsMkdirCommand { +impl MkdirCommand { pub fn new() -> Self { Self } } -impl VfsCommand for VfsMkdirCommand { +impl VfsCommand for MkdirCommand { fn execute( &self, vfs: &mut VirtualFilesystem, @@ -743,15 +743,15 @@ impl VfsCommand for VfsMkdirCommand { } // VFS-based RmCommand for Phase 2 migration -pub struct VfsRmCommand; +pub struct RmCommand; -impl VfsRmCommand { +impl RmCommand { pub fn new() -> Self { Self } } -impl VfsCommand for VfsRmCommand { +impl VfsCommand for RmCommand { fn execute( &self, vfs: &mut VirtualFilesystem, From dfeaa51f1ee24fff3f6acc214030c8abba2e5418 Mon Sep 17 00:00:00 2001 From: BakerNet Date: Sun, 29 Jun 2025 08:20:07 -0700 Subject: [PATCH 4/8] Misc --- src/app/terminal.rs | 4 ++-- src/app/terminal/system_tools.rs | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/terminal.rs b/src/app/terminal.rs index d96f89f..7725d60 100644 --- a/src/app/terminal.rs +++ b/src/app/terminal.rs @@ -251,13 +251,13 @@ impl Terminal { return vfs_command.execute(&mut self.vfs, current_node, parts.collect(), None, true); } - // Fall back to legacy Executable commands + // Try non-VFS commands if let Some(command) = self.commands.get(&cmd) { // For now, assume not piped and output to TTY return command.execute(path, parts.collect(), None, true); } - // Fall back to legacy command handling for unimplemented commands + // Fall back to special command handling for some commands match cmd { // Special handling for history command due to mutable state requirements // The history -c flag requires mutable access to clear the terminal's history Vec, diff --git a/src/app/terminal/system_tools.rs b/src/app/terminal/system_tools.rs index 42f4309..3253491 100644 --- a/src/app/terminal/system_tools.rs +++ b/src/app/terminal/system_tools.rs @@ -131,7 +131,6 @@ impl VfsCommand for UnknownCommand { } if node.name == "mines.sh" && is_executable { - // MinesCommand doesn't implement VfsCommand yet, so fall back to legacy execution let path = vfs.get_node_path(current_dir); return MinesCommand.execute(&path, args, None, is_output_tty); } From 983dede5ef0d18e5722e03cf2b1a224ba0e35279 Mon Sep 17 00:00:00 2001 From: BakerNet Date: Sun, 29 Jun 2025 08:24:52 -0700 Subject: [PATCH 5/8] Rename argument --- src/app/terminal/command.rs | 19 +++++-------------- src/app/terminal/cp_mv_tools.rs | 4 ++-- src/app/terminal/fs_tools.rs | 14 +++++++------- src/app/terminal/ps_tools.rs | 4 ++-- src/app/terminal/simple_tools.rs | 22 +++++++++++----------- src/app/terminal/system_tools.rs | 6 +++--- 6 files changed, 30 insertions(+), 39 deletions(-) diff --git a/src/app/terminal/command.rs b/src/app/terminal/command.rs index b1beb26..9d7aaed 100644 --- a/src/app/terminal/command.rs +++ b/src/app/terminal/command.rs @@ -5,13 +5,8 @@ use indextree::NodeId; use leptos::prelude::*; pub trait Command: Send + Sync { - fn execute( - &self, - path: &str, - args: Vec<&str>, - stdin: Option<&str>, - is_output_tty: bool, - ) -> CommandRes; + fn execute(&self, path: &str, args: Vec<&str>, stdin: Option<&str>, is_tty: bool) + -> CommandRes; } /// VFS-aware command trait for commands that need direct filesystem access @@ -29,7 +24,7 @@ pub trait VfsCommand: Send + Sync { pub enum CommandRes { Output { is_err: bool, // true if command failed (non-zero exit code) - stdout_view: Option, // stdout content for display (only set if is_output_tty) + stdout_view: Option, // stdout content for display (only set if is_tty) stdout_text: Option, // stdout for piping stderr_text: Option, // stderr text (Header converts to view) }, @@ -184,14 +179,10 @@ impl Cmd { } // Terminal/display utilities (typically in /usr/bin) - Self::Clear | Self::Date => { - Some(format!("/usr/bin/{}", self.as_str())) - } + Self::Clear | Self::Date => Some(format!("/usr/bin/{}", self.as_str())), // Custom/third-party applications (typically in /usr/local/bin) - Self::Neofetch | Self::Mines => { - Some(format!("/usr/local/bin/{}", self.as_str())) - } + Self::Neofetch | Self::Mines => Some(format!("/usr/local/bin/{}", self.as_str())), // Documentation/help (typically in /usr/bin) Self::Help => Some(format!("/usr/bin/{}", self.as_str())), diff --git a/src/app/terminal/cp_mv_tools.rs b/src/app/terminal/cp_mv_tools.rs index 44292b9..a6ebe95 100644 --- a/src/app/terminal/cp_mv_tools.rs +++ b/src/app/terminal/cp_mv_tools.rs @@ -20,7 +20,7 @@ impl VfsCommand for CpCommand { current_dir: NodeId, args: Vec<&str>, _stdin: Option<&str>, - _is_output_tty: bool, + _is_tty: bool, ) -> CommandRes { let (options, targets) = parse_multitarget(args); @@ -251,7 +251,7 @@ impl VfsCommand for MvCommand { current_dir: NodeId, args: Vec<&str>, _stdin: Option<&str>, - _is_output_tty: bool, + _is_tty: bool, ) -> CommandRes { let (_, targets) = parse_multitarget(args); diff --git a/src/app/terminal/fs_tools.rs b/src/app/terminal/fs_tools.rs index 7c60249..ef010e6 100644 --- a/src/app/terminal/fs_tools.rs +++ b/src/app/terminal/fs_tools.rs @@ -58,7 +58,7 @@ impl VfsCommand for LsCommand { current_dir: NodeId, args: Vec<&str>, _stdin: Option<&str>, - is_output_tty: bool, + is_tty: bool, ) -> CommandRes { let mut all = false; let mut long_format = false; @@ -204,7 +204,7 @@ This version of ls only supports options 'a' and 'l'"# file_items.sort_by(|a, b| a.display_name.cmp(&b.display_name)); dir_listings.sort_by(|a, b| a.0.cmp(&b.0)); - if is_output_tty { + if is_tty { let is_multi = dir_listings.len() > 1 || (!dir_listings.is_empty() && !file_items.is_empty()); @@ -254,7 +254,7 @@ This version of ls only supports options 'a' and 'l'"# })) } else { // For non-TTY output, just return simple text - // TODO - fix + // TODO - fix - not currently doing long format let mut text_output = Vec::new(); for item in &file_items { @@ -437,7 +437,7 @@ impl VfsCommand for CatCommand { current_dir: NodeId, args: Vec<&str>, _stdin: Option<&str>, - _is_output_tty: bool, + _is_tty: bool, ) -> CommandRes { let (options, targets) = parse_multitarget(args); @@ -543,7 +543,7 @@ impl VfsCommand for TouchCommand { current_dir: NodeId, args: Vec<&str>, _stdin: Option<&str>, - _is_output_tty: bool, + _is_tty: bool, ) -> CommandRes { let (_, targets) = parse_multitarget(args); @@ -651,7 +651,7 @@ impl VfsCommand for MkdirCommand { current_dir: NodeId, args: Vec<&str>, _stdin: Option<&str>, - _is_output_tty: bool, + _is_tty: bool, ) -> CommandRes { let (_, targets) = parse_multitarget(args); @@ -758,7 +758,7 @@ impl VfsCommand for RmCommand { current_dir: NodeId, args: Vec<&str>, _stdin: Option<&str>, - _is_output_tty: bool, + _is_tty: bool, ) -> CommandRes { let (options, targets) = parse_multitarget(args); diff --git a/src/app/terminal/ps_tools.rs b/src/app/terminal/ps_tools.rs index 43d3b43..79844a8 100644 --- a/src/app/terminal/ps_tools.rs +++ b/src/app/terminal/ps_tools.rs @@ -50,7 +50,7 @@ impl Command for PsCommand { _path: &str, args: Vec<&str>, _stdin: Option<&str>, - _is_output_tty: bool, + _is_tty: bool, ) -> CommandRes { // Check for supported options if args.len() > 1 { @@ -99,7 +99,7 @@ impl Command for KillCommand { _path: &str, args: Vec<&str>, _stdin: Option<&str>, - _is_output_tty: bool, + _is_tty: bool, ) -> CommandRes { if args.is_empty() { return CommandRes::new() diff --git a/src/app/terminal/simple_tools.rs b/src/app/terminal/simple_tools.rs index 008bf9a..33b0ee5 100644 --- a/src/app/terminal/simple_tools.rs +++ b/src/app/terminal/simple_tools.rs @@ -21,7 +21,7 @@ impl Command for HelpCommand { _path: &str, _args: Vec<&str>, _stdin: Option<&str>, - _is_output_tty: bool, + _is_tty: bool, ) -> CommandRes { CommandRes::new().with_stdout_text(HELP_TEXT) } @@ -35,7 +35,7 @@ impl Command for PwdCommand { path: &str, args: Vec<&str>, _stdin: Option<&str>, - _is_output_tty: bool, + _is_tty: bool, ) -> CommandRes { if !args.is_empty() { let error_msg = "pwd: too many arguments"; @@ -54,7 +54,7 @@ impl Command for WhoAmICommand { _path: &str, args: Vec<&str>, _stdin: Option<&str>, - _is_output_tty: bool, + _is_tty: bool, ) -> CommandRes { if !args.is_empty() { let error_msg = "usage: whoami"; @@ -73,7 +73,7 @@ impl Command for ClearCommand { _path: &str, _args: Vec<&str>, _stdin: Option<&str>, - __is_output_tty: bool, + __is_tty: bool, ) -> CommandRes { CommandRes::new() } @@ -103,7 +103,7 @@ impl Command for NeofetchCommand { _path: &str, _args: Vec<&str>, _stdin: Option<&str>, - _is_output_tty: bool, + _is_tty: bool, ) -> CommandRes { let text = self.as_text(); CommandRes::new().with_stdout_text(text) @@ -118,7 +118,7 @@ impl Command for MinesCommand { _path: &str, _args: Vec<&str>, _stdin: Option<&str>, - __is_output_tty: bool, + __is_tty: bool, ) -> CommandRes { CommandRes::redirect(MINES_URL.to_string()) } @@ -132,7 +132,7 @@ impl Command for SudoCommand { _path: &str, _args: Vec<&str>, _stdin: Option<&str>, - _is_output_tty: bool, + _is_tty: bool, ) -> CommandRes { let error_msg = "user is not in the sudoers file. This incident will be reported."; CommandRes::new().with_error().with_stderr(error_msg) @@ -147,7 +147,7 @@ impl Command for EchoCommand { _path: &str, args: Vec<&str>, _stdin: Option<&str>, - _is_output_tty: bool, + _is_tty: bool, ) -> CommandRes { let message = args .iter() @@ -203,7 +203,7 @@ impl Command for DateCommand { _path: &str, args: Vec<&str>, _stdin: Option<&str>, - _is_output_tty: bool, + _is_tty: bool, ) -> CommandRes { if args.len() > 1 { let error_msg = "date: too many arguments"; @@ -269,7 +269,7 @@ impl Command for UptimeCommand { _path: &str, _args: Vec<&str>, _stdin: Option<&str>, - _is_output_tty: bool, + _is_tty: bool, ) -> CommandRes { let output = self.get_uptime(); CommandRes::new().with_stdout_text(output) @@ -301,7 +301,7 @@ impl Command for HistoryCommand<'_> { _path: &str, args: Vec<&str>, _stdin: Option<&str>, - _is_output_tty: bool, + _is_tty: bool, ) -> CommandRes { if args.len() > 1 { return CommandRes::new() diff --git a/src/app/terminal/system_tools.rs b/src/app/terminal/system_tools.rs index 3253491..246541a 100644 --- a/src/app/terminal/system_tools.rs +++ b/src/app/terminal/system_tools.rs @@ -55,7 +55,7 @@ impl VfsCommand for WhichCommand { current_dir: NodeId, args: Vec<&str>, _stdin: Option<&str>, - _is_output_tty: bool, + _is_tty: bool, ) -> CommandRes { if args.is_empty() { return CommandRes::new() @@ -101,7 +101,7 @@ impl VfsCommand for UnknownCommand { current_dir: NodeId, args: Vec<&str>, _stdin: Option<&str>, - is_output_tty: bool, + is_tty: bool, ) -> CommandRes { let target_string = &self.command_name; @@ -132,7 +132,7 @@ impl VfsCommand for UnknownCommand { if node.name == "mines.sh" && is_executable { let path = vfs.get_node_path(current_dir); - return MinesCommand.execute(&path, args, None, is_output_tty); + return MinesCommand.execute(&path, args, None, is_tty); } match &node.node_type { From 8cb624124263980e71c0eea99f10278e5b8e38a1 Mon Sep 17 00:00:00 2001 From: BakerNet Date: Sun, 29 Jun 2025 08:30:23 -0700 Subject: [PATCH 6/8] Misc --- src/app/terminal.rs | 6 +- src/app/terminal/cp_mv_tools.rs | 374 -------------------------------- src/app/terminal/fs_tools.rs | 373 ++++++++++++++++++++++++++++++- 3 files changed, 370 insertions(+), 383 deletions(-) delete mode 100644 src/app/terminal/cp_mv_tools.rs diff --git a/src/app/terminal.rs b/src/app/terminal.rs index 7725d60..dfa497a 100644 --- a/src/app/terminal.rs +++ b/src/app/terminal.rs @@ -1,6 +1,5 @@ mod command; mod components; -mod cp_mv_tools; mod fs_tools; mod ps_tools; mod simple_tools; @@ -14,8 +13,9 @@ use std::collections::{HashMap, VecDeque}; use command::{Cmd, CmdAlias, Command, VfsCommand}; use components::TextContent; -use cp_mv_tools::{CpCommand, MvCommand}; -use fs_tools::{CatCommand, CdCommand, LsCommand, MkdirCommand, RmCommand, TouchCommand}; +use fs_tools::{ + CatCommand, CdCommand, CpCommand, LsCommand, MkdirCommand, MvCommand, RmCommand, TouchCommand, +}; use indextree::NodeId; use ps_tools::{KillCommand, Process, PsCommand}; use simple_tools::{ diff --git a/src/app/terminal/cp_mv_tools.rs b/src/app/terminal/cp_mv_tools.rs deleted file mode 100644 index a6ebe95..0000000 --- a/src/app/terminal/cp_mv_tools.rs +++ /dev/null @@ -1,374 +0,0 @@ -use indextree::NodeId; - -use super::command::{CommandRes, VfsCommand}; -use super::fs_tools::{parse_multitarget, RmCommand}; -use super::vfs::{FileContent, VfsError, VfsNodeType, VirtualFilesystem}; - -// VFS-based CpCommand for Phase 2 migration -pub struct CpCommand; - -impl CpCommand { - pub fn new() -> Self { - Self - } -} - -impl VfsCommand for CpCommand { - fn execute( - &self, - vfs: &mut VirtualFilesystem, - current_dir: NodeId, - args: Vec<&str>, - _stdin: Option<&str>, - _is_tty: bool, - ) -> CommandRes { - let (options, targets) = parse_multitarget(args); - - // Check for recursive option - let recursive = options.contains(&'r'); - - // Validate options - let invalid = options.iter().find(|c| **c != 'r' && **c != 'f'); - if let Some(c) = invalid { - let c = c.to_owned(); - let error_msg = format!( - r#"cp: invalid option -- '{c}' -This version of cp only supports options 'r' and 'f'"# - ); - return CommandRes::new().with_error().with_stderr(error_msg); - } - - if targets.len() < 2 { - return CommandRes::new() - .with_error() - .with_stderr("cp: missing destination file operand"); - } - - let destination = targets.last().unwrap(); - let sources = &targets[..targets.len() - 1]; - - let mut stderr_parts = Vec::new(); - let mut has_error = false; - - for source in sources { - match self.copy_item(vfs, current_dir, source, destination, recursive) { - Ok(_) => {} - Err(err_msg) => { - has_error = true; - stderr_parts.push(err_msg); - } - } - } - - let mut result = CommandRes::new(); - if has_error { - result = result.with_error(); - let stderr_text = stderr_parts.join("\n"); - result = result.with_stderr(stderr_text); - } - - result - } -} - -impl CpCommand { - fn copy_item( - &self, - vfs: &mut VirtualFilesystem, - current_dir: NodeId, - source_path: &str, - dest_path: &str, - recursive: bool, - ) -> Result<(), String> { - // Resolve source path - let source_id = vfs - .resolve_path(current_dir, source_path) - .map_err(|_| format!("cp: cannot stat '{source_path}': No such file or directory"))?; - - let source_node = vfs - .get_node(source_id) - .ok_or_else(|| format!("cp: cannot stat '{source_path}': No such file or directory"))?; - - // Check if source is a directory and recursive flag - if source_node.is_directory() && !recursive { - return Err(format!( - "cp: omitting directory '{source_path}': use -r to copy directories" - )); - } - - // Determine destination - let (dest_parent_id, dest_name) = - self.resolve_destination(vfs, current_dir, dest_path, source_path)?; - - // Perform the copy - match &source_node.node_type { - VfsNodeType::File { content } => { - self.copy_file(vfs, dest_parent_id, &dest_name, content.clone()) - } - VfsNodeType::Directory => { - // we already know recursive is true here - self.copy_directory_recursive(vfs, source_id, dest_parent_id, &dest_name) - } - VfsNodeType::Link { .. } => Err(format!( - "cp: cannot copy '{source_path}': Links not supported" - )), - } - } - - fn resolve_destination( - &self, - vfs: &VirtualFilesystem, - current_dir: NodeId, - dest_path: &str, - source_path: &str, - ) -> Result<(NodeId, String), String> { - // Try to resolve destination path - match vfs.resolve_path(current_dir, dest_path) { - Ok(dest_id) => { - // Destination exists - let dest_node = vfs.get_node(dest_id).ok_or_else(|| { - format!("cp: cannot access '{dest_path}': No such file or directory") - })?; - - if dest_node.is_directory() { - // Copy into the directory with source filename - let source_name = source_path.split('/').next_back().unwrap_or(source_path); - Ok((dest_id, source_name.to_string())) - } else { - // Destination is a file - get parent directory and use dest filename - let parent_id = vfs.get_parent(dest_id).ok_or_else(|| { - format!("cp: cannot access '{dest_path}': No such file or directory") - })?; - Ok((parent_id, dest_node.name.clone())) - } - } - Err(_) => { - // Destination doesn't exist - parse as parent/filename - let (parent_path, filename) = if let Some(pos) = dest_path.rfind('/') { - (&dest_path[..pos], &dest_path[pos + 1..]) - } else { - ("", dest_path) - }; - - let parent_id = if parent_path.is_empty() { - Ok(current_dir) - } else { - vfs.resolve_path(current_dir, parent_path).map_err(|_| { - format!("cp: cannot create '{dest_path}': No such file or directory") - }) - }?; - - Ok((parent_id, filename.to_string())) - } - } - } - - fn copy_file( - &self, - vfs: &mut VirtualFilesystem, - parent_id: NodeId, - filename: &str, - content: FileContent, - ) -> Result<(), String> { - vfs.create_file(parent_id, filename, content) - .map_err(|err| match err { - VfsError::AlreadyExists => format!("cp: cannot create '{filename}': File exists"), - VfsError::PermissionDenied => { - format!("cp: cannot create '{filename}': Permission denied") - } - VfsError::NotADirectory => { - format!("cp: cannot create '{filename}': Not a directory") - } - _ => format!("cp: cannot create '{filename}': Unknown error"), - })?; - Ok(()) - } - - fn copy_directory_recursive( - &self, - vfs: &mut VirtualFilesystem, - source_id: NodeId, - dest_parent_id: NodeId, - dest_name: &str, - ) -> Result<(), String> { - // Create destination directory - let dest_dir_id = - vfs.create_directory(dest_parent_id, dest_name) - .map_err(|err| match err { - VfsError::AlreadyExists => { - format!("cp: cannot create directory '{dest_name}': File exists") - } - VfsError::PermissionDenied => { - format!("cp: cannot create directory '{dest_name}': Permission denied") - } - VfsError::NotADirectory => { - format!("cp: cannot create directory '{dest_name}': Not a directory") - } - _ => format!("cp: cannot create directory '{dest_name}': Unknown error"), - })?; - - // Copy all entries from source directory - let entries = vfs - .list_directory(source_id) - .map_err(|_| "cp: cannot read directory: Permission denied".to_string())?; - - for entry in entries { - let child_source_id = entry.node_id; - let child_node = vfs - .get_node(child_source_id) - .ok_or_else(|| "cp: cannot access child: No such file or directory".to_string())?; - - match &child_node.node_type { - VfsNodeType::File { content } => { - self.copy_file(vfs, dest_dir_id, &entry.name, content.clone())?; - } - VfsNodeType::Directory => { - self.copy_directory_recursive(vfs, child_source_id, dest_dir_id, &entry.name)?; - } - VfsNodeType::Link { .. } => { - // Skip links for now - } - } - } - - Ok(()) - } -} - -// VFS-based MvCommand for Phase 2 migration -pub struct MvCommand; - -impl MvCommand { - pub fn new() -> Self { - Self - } -} - -impl VfsCommand for MvCommand { - fn execute( - &self, - vfs: &mut VirtualFilesystem, - current_dir: NodeId, - args: Vec<&str>, - _stdin: Option<&str>, - _is_tty: bool, - ) -> CommandRes { - let (_, targets) = parse_multitarget(args); - - if targets.len() < 2 { - return CommandRes::new() - .with_error() - .with_stderr("mv: missing destination file operand"); - } - - let destination = targets.last().unwrap(); - let sources = &targets[..targets.len() - 1]; - - let mut stderr_parts = Vec::new(); - let mut has_error = false; - - for source in sources { - match self.move_item(vfs, current_dir, source, destination) { - Ok(_) => {} - Err(err_msg) => { - has_error = true; - stderr_parts.push(err_msg); - } - } - } - - let mut result = CommandRes::new(); - if has_error { - result = result.with_error(); - let stderr_text = stderr_parts.join("\n"); - result = result.with_stderr(stderr_text); - } - - result - } -} - -impl MvCommand { - fn move_item( - &self, - vfs: &mut VirtualFilesystem, - current_dir: NodeId, - source_path: &str, - dest_path: &str, - ) -> Result<(), String> { - // First check if source exists and is not immutable - let source_id = vfs - .resolve_path(current_dir, source_path) - .map_err(|_| format!("mv: cannot stat '{source_path}': No such file or directory"))?; - - let source_node = vfs - .get_node(source_id) - .ok_or_else(|| format!("mv: cannot stat '{source_path}': No such file or directory"))?; - - // Check if source is immutable (cannot be moved) - if source_node.permissions.immutable { - return Err(format!( - "mv: cannot move '{source_path}': Permission denied" - )); - } - - let is_directory = source_node.is_directory(); - - // As per Unix mv documentation: - // rm -f destination_path && cp -pRP source_file destination && rm -rf source_file - - // Step 1: If destination exists and is a file, try to remove it first - // (We handle directory destinations differently in cp) - if let Ok(dest_id) = vfs.resolve_path(current_dir, dest_path) { - if let Some(dest_node) = vfs.get_node(dest_id) { - if !dest_node.is_directory() { - // Try to remove the destination file (ignore errors for now) - let _ = vfs.delete_node(dest_id); - } - } - } - - // Step 2: Copy source to destination (with -r if directory) - let cp_command = CpCommand::new(); - let cp_args = if is_directory { - vec!["-r", source_path, dest_path] - } else { - vec![source_path, dest_path] - }; - - // Execute the copy - let cp_result = cp_command.execute(vfs, current_dir, cp_args, None, false); - if cp_result.is_error() { - // Extract error message from cp command - return Err(format!( - "mv: copy failed: {}", - match cp_result { - CommandRes::Output { - stderr_text: Some(ref msg), - .. - } => msg, - _ => "unknown error", - } - )); - } - - // Step 3: Remove the source (with -r if directory) - let rm_command = RmCommand::new(); - let rm_args = if is_directory { - vec!["-r", source_path] - } else { - vec![source_path] - }; - - // Execute the removal - let rm_result = rm_command.execute(vfs, current_dir, rm_args, None, false); - if rm_result.is_error() { - // The copy succeeded but removal failed - this is still an error - return Err(format!( - "mv: cannot remove '{source_path}': Permission denied" - )); - } - - Ok(()) - } -} diff --git a/src/app/terminal/fs_tools.rs b/src/app/terminal/fs_tools.rs index ef010e6..3c12a30 100644 --- a/src/app/terminal/fs_tools.rs +++ b/src/app/terminal/fs_tools.rs @@ -42,7 +42,6 @@ impl TextContent for VfsItem { } } -// VFS-based LsCommand for Phase 2 migration pub struct LsCommand; impl LsCommand { @@ -351,7 +350,6 @@ fn VfsLsView(items: Vec, #[prop(default = false)] long_format: bool) -> } } -// VFS-based CdCommand for Phase 2 migration pub struct CdCommand; impl CdCommand { @@ -421,7 +419,6 @@ impl VfsCommand for CdCommand { } } -// VFS-based CatCommand for Phase 2 migration pub struct CatCommand; impl CatCommand { @@ -527,7 +524,6 @@ This version of cat doesn't support any options"# } } -// VFS-based TouchCommand for Phase 2 migration pub struct TouchCommand; impl TouchCommand { @@ -635,7 +631,6 @@ impl VfsCommand for TouchCommand { } } -// VFS-based MkdirCommand for Phase 2 migration pub struct MkdirCommand; impl MkdirCommand { @@ -742,7 +737,6 @@ impl VfsCommand for MkdirCommand { } } -// VFS-based RmCommand for Phase 2 migration pub struct RmCommand; impl RmCommand { @@ -845,3 +839,370 @@ This version of rm only supports options 'r' and 'f'"# result } } + +pub struct CpCommand; + +impl CpCommand { + pub fn new() -> Self { + Self + } +} + +impl VfsCommand for CpCommand { + fn execute( + &self, + vfs: &mut VirtualFilesystem, + current_dir: NodeId, + args: Vec<&str>, + _stdin: Option<&str>, + _is_tty: bool, + ) -> CommandRes { + let (options, targets) = parse_multitarget(args); + + // Check for recursive option + let recursive = options.contains(&'r'); + + // Validate options + let invalid = options.iter().find(|c| **c != 'r' && **c != 'f'); + if let Some(c) = invalid { + let c = c.to_owned(); + let error_msg = format!( + r#"cp: invalid option -- '{c}' +This version of cp only supports options 'r' and 'f'"# + ); + return CommandRes::new().with_error().with_stderr(error_msg); + } + + if targets.len() < 2 { + return CommandRes::new() + .with_error() + .with_stderr("cp: missing destination file operand"); + } + + let destination = targets.last().unwrap(); + let sources = &targets[..targets.len() - 1]; + + let mut stderr_parts = Vec::new(); + let mut has_error = false; + + for source in sources { + match self.copy_item(vfs, current_dir, source, destination, recursive) { + Ok(_) => {} + Err(err_msg) => { + has_error = true; + stderr_parts.push(err_msg); + } + } + } + + let mut result = CommandRes::new(); + if has_error { + result = result.with_error(); + let stderr_text = stderr_parts.join("\n"); + result = result.with_stderr(stderr_text); + } + + result + } +} + +impl CpCommand { + fn copy_item( + &self, + vfs: &mut VirtualFilesystem, + current_dir: NodeId, + source_path: &str, + dest_path: &str, + recursive: bool, + ) -> Result<(), String> { + // Resolve source path + let source_id = vfs + .resolve_path(current_dir, source_path) + .map_err(|_| format!("cp: cannot stat '{source_path}': No such file or directory"))?; + + let source_node = vfs + .get_node(source_id) + .ok_or_else(|| format!("cp: cannot stat '{source_path}': No such file or directory"))?; + + // Check if source is a directory and recursive flag + if source_node.is_directory() && !recursive { + return Err(format!( + "cp: omitting directory '{source_path}': use -r to copy directories" + )); + } + + // Determine destination + let (dest_parent_id, dest_name) = + self.resolve_destination(vfs, current_dir, dest_path, source_path)?; + + // Perform the copy + match &source_node.node_type { + VfsNodeType::File { content } => { + self.copy_file(vfs, dest_parent_id, &dest_name, content.clone()) + } + VfsNodeType::Directory => { + // we already know recursive is true here + self.copy_directory_recursive(vfs, source_id, dest_parent_id, &dest_name) + } + VfsNodeType::Link { .. } => Err(format!( + "cp: cannot copy '{source_path}': Links not supported" + )), + } + } + + fn resolve_destination( + &self, + vfs: &VirtualFilesystem, + current_dir: NodeId, + dest_path: &str, + source_path: &str, + ) -> Result<(NodeId, String), String> { + // Try to resolve destination path + match vfs.resolve_path(current_dir, dest_path) { + Ok(dest_id) => { + // Destination exists + let dest_node = vfs.get_node(dest_id).ok_or_else(|| { + format!("cp: cannot access '{dest_path}': No such file or directory") + })?; + + if dest_node.is_directory() { + // Copy into the directory with source filename + let source_name = source_path.split('/').next_back().unwrap_or(source_path); + Ok((dest_id, source_name.to_string())) + } else { + // Destination is a file - get parent directory and use dest filename + let parent_id = vfs.get_parent(dest_id).ok_or_else(|| { + format!("cp: cannot access '{dest_path}': No such file or directory") + })?; + Ok((parent_id, dest_node.name.clone())) + } + } + Err(_) => { + // Destination doesn't exist - parse as parent/filename + let (parent_path, filename) = if let Some(pos) = dest_path.rfind('/') { + (&dest_path[..pos], &dest_path[pos + 1..]) + } else { + ("", dest_path) + }; + + let parent_id = if parent_path.is_empty() { + Ok(current_dir) + } else { + vfs.resolve_path(current_dir, parent_path).map_err(|_| { + format!("cp: cannot create '{dest_path}': No such file or directory") + }) + }?; + + Ok((parent_id, filename.to_string())) + } + } + } + + fn copy_file( + &self, + vfs: &mut VirtualFilesystem, + parent_id: NodeId, + filename: &str, + content: FileContent, + ) -> Result<(), String> { + vfs.create_file(parent_id, filename, content) + .map_err(|err| match err { + VfsError::AlreadyExists => format!("cp: cannot create '{filename}': File exists"), + VfsError::PermissionDenied => { + format!("cp: cannot create '{filename}': Permission denied") + } + VfsError::NotADirectory => { + format!("cp: cannot create '{filename}': Not a directory") + } + _ => format!("cp: cannot create '{filename}': Unknown error"), + })?; + Ok(()) + } + + fn copy_directory_recursive( + &self, + vfs: &mut VirtualFilesystem, + source_id: NodeId, + dest_parent_id: NodeId, + dest_name: &str, + ) -> Result<(), String> { + // Create destination directory + let dest_dir_id = + vfs.create_directory(dest_parent_id, dest_name) + .map_err(|err| match err { + VfsError::AlreadyExists => { + format!("cp: cannot create directory '{dest_name}': File exists") + } + VfsError::PermissionDenied => { + format!("cp: cannot create directory '{dest_name}': Permission denied") + } + VfsError::NotADirectory => { + format!("cp: cannot create directory '{dest_name}': Not a directory") + } + _ => format!("cp: cannot create directory '{dest_name}': Unknown error"), + })?; + + // Copy all entries from source directory + let entries = vfs + .list_directory(source_id) + .map_err(|_| "cp: cannot read directory: Permission denied".to_string())?; + + for entry in entries { + let child_source_id = entry.node_id; + let child_node = vfs + .get_node(child_source_id) + .ok_or_else(|| "cp: cannot access child: No such file or directory".to_string())?; + + match &child_node.node_type { + VfsNodeType::File { content } => { + self.copy_file(vfs, dest_dir_id, &entry.name, content.clone())?; + } + VfsNodeType::Directory => { + self.copy_directory_recursive(vfs, child_source_id, dest_dir_id, &entry.name)?; + } + VfsNodeType::Link { .. } => { + // Skip links for now + } + } + } + + Ok(()) + } +} + +pub struct MvCommand; + +impl MvCommand { + pub fn new() -> Self { + Self + } +} + +impl VfsCommand for MvCommand { + fn execute( + &self, + vfs: &mut VirtualFilesystem, + current_dir: NodeId, + args: Vec<&str>, + _stdin: Option<&str>, + _is_tty: bool, + ) -> CommandRes { + let (_, targets) = parse_multitarget(args); + + if targets.len() < 2 { + return CommandRes::new() + .with_error() + .with_stderr("mv: missing destination file operand"); + } + + let destination = targets.last().unwrap(); + let sources = &targets[..targets.len() - 1]; + + let mut stderr_parts = Vec::new(); + let mut has_error = false; + + for source in sources { + match self.move_item(vfs, current_dir, source, destination) { + Ok(_) => {} + Err(err_msg) => { + has_error = true; + stderr_parts.push(err_msg); + } + } + } + + let mut result = CommandRes::new(); + if has_error { + result = result.with_error(); + let stderr_text = stderr_parts.join("\n"); + result = result.with_stderr(stderr_text); + } + + result + } +} + +impl MvCommand { + fn move_item( + &self, + vfs: &mut VirtualFilesystem, + current_dir: NodeId, + source_path: &str, + dest_path: &str, + ) -> Result<(), String> { + // First check if source exists and is not immutable + let source_id = vfs + .resolve_path(current_dir, source_path) + .map_err(|_| format!("mv: cannot stat '{source_path}': No such file or directory"))?; + + let source_node = vfs + .get_node(source_id) + .ok_or_else(|| format!("mv: cannot stat '{source_path}': No such file or directory"))?; + + // Check if source is immutable (cannot be moved) + if source_node.permissions.immutable { + return Err(format!( + "mv: cannot move '{source_path}': Permission denied" + )); + } + + let is_directory = source_node.is_directory(); + + // As per Unix mv documentation: + // rm -f destination_path && cp -pRP source_file destination && rm -rf source_file + + // Step 1: If destination exists and is a file, try to remove it first + // (We handle directory destinations differently in cp) + if let Ok(dest_id) = vfs.resolve_path(current_dir, dest_path) { + if let Some(dest_node) = vfs.get_node(dest_id) { + if !dest_node.is_directory() { + // Try to remove the destination file (ignore errors for now) + let _ = vfs.delete_node(dest_id); + } + } + } + + // Step 2: Copy source to destination (with -r if directory) + let cp_command = CpCommand::new(); + let cp_args = if is_directory { + vec!["-r", source_path, dest_path] + } else { + vec![source_path, dest_path] + }; + + // Execute the copy + let cp_result = cp_command.execute(vfs, current_dir, cp_args, None, false); + if cp_result.is_error() { + // Extract error message from cp command + return Err(format!( + "mv: copy failed: {}", + match cp_result { + CommandRes::Output { + stderr_text: Some(ref msg), + .. + } => msg, + _ => "unknown error", + } + )); + } + + // Step 3: Remove the source (with -r if directory) + let rm_command = RmCommand::new(); + let rm_args = if is_directory { + vec!["-r", source_path] + } else { + vec![source_path] + }; + + // Execute the removal + let rm_result = rm_command.execute(vfs, current_dir, rm_args, None, false); + if rm_result.is_error() { + // The copy succeeded but removal failed - this is still an error + return Err(format!( + "mv: cannot remove '{source_path}': Permission denied" + )); + } + + Ok(()) + } +} From 550668349075e08d75be5a48e44570d772f565c3 Mon Sep 17 00:00:00 2001 From: BakerNet Date: Sun, 29 Jun 2025 08:39:39 -0700 Subject: [PATCH 7/8] Un .gitignore lockfile --- .gitignore | 4 - Cargo.lock | 3485 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 3485 insertions(+), 4 deletions(-) create mode 100644 Cargo.lock diff --git a/.gitignore b/.gitignore index 1b41a8c..e57337f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,10 +3,6 @@ debug/ target/ -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - # These are backup files generated by rustfmt **/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..7b3f6ca --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3485 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "any_spawner" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1384d3fe1eecb464229fcf6eebb72306591c56bf27b373561489458a7c73027d" +dependencies = [ + "futures", + "thiserror 2.0.12", + "tokio", + "wasm-bindgen-futures", +] + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atom_syndication" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f68d23e2cb4fd958c705b91a6b4c80ceeaf27a9e11651272a8389d5ce1a4a3" +dependencies = [ + "chrono", + "derive_builder", + "diligent-date-parser", + "never", + "quick-xml", +] + +[[package]] +name = "attribute-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0053e96dd3bec5b4879c23a138d6ef26f2cb936c9cdc96274ac2b9ed44b5bb54" +dependencies = [ + "attribute-derive-macro", + "derive-where", + "manyhow", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "attribute-derive-macro" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "463b53ad0fd5b460af4b1915fe045ff4d946d025fb6c4dc3337752eaa980f71b" +dependencies = [ + "collection_literals", + "interpolator", + "manyhow", + "proc-macro-utils", + "proc-macro2", + "quote", + "quote-use", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +dependencies = [ + "axum-core", + "base64", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "multer", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "camino" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab" + +[[package]] +name = "cc" +version = "1.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "codee" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f18d705321923b1a9358e3fc3c57c3b50171196827fc7f5f10b053242aca627" +dependencies = [ + "js-sys", + "serde", + "serde-wasm-bindgen", + "serde_json", + "thiserror 2.0.12", + "wasm-bindgen", +] + +[[package]] +name = "collection_literals" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186dce98367766de751c42c4f03970fc60fc012296e706ccbb9d5df9b6c1e271" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "config" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595aae20e65c3be792d05818e8c63025294ac3cb7e200f11459063a352a6ef80" +dependencies = [ + "convert_case 0.6.0", + "pathdiff", + "serde", + "toml 0.8.23", + "winnow", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "console_log" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8aed40e4edbf4d3b4431ab260b63fdc40f5780a4766824329ea0f1eefe3c0f" +dependencies = [ + "log", + "web-sys", +] + +[[package]] +name = "const-str" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e991226a70654b49d34de5ed064885f0bef0348a8e70018b8ff1ac80aa984a2" + +[[package]] +name = "const_format" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "const_str_slice_concat" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67855af358fcb20fac58f9d714c94e2b228fe5694c1c9b4ead4a366343eda1b" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "convert_case" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "default-struct-builder" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0df63c21a4383f94bd5388564829423f35c316aed85dc4f8427aded372c7c0d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive-where" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "510c292c8cf384b1a340b816a9a6cf2599eb8f566a44949024af88418000c50b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "diligent-date-parser" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ede7d79366f419921e2e2f67889c12125726692a313bffb474bd5f37a581e9" +dependencies = [ + "chrono", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "drain_filter_polyfill" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "either_of" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216d23e0ec69759a17f05e1c553f3a6870e5ec73420fbb07807a6f34d5d1d5a4" +dependencies = [ + "paste", + "pin-project-lite", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1731451909bde27714eacba19c2566362a7f35224f52b153d3f42cf60f72472" + +[[package]] +name = "event-listener" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", + "num_cpus", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getopts" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gray_matter" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ee6a6070bad7c953b0c8be9367e9372181fed69f3e026c4eb5160d8b3c0222" +dependencies = [ + "serde", + "serde_json", + "toml 0.5.11", + "yaml-rust2", +] + +[[package]] +name = "guardian" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e2ac29387b1aa07a1e448f7bb4f35b500787971e965b02842b900afa5c8f6f" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hydration_context" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8714ae4adeaa846d838f380fbd72f049197de629948f91bf045329e0cf0a283" +dependencies = [ + "futures", + "js-sys", + "once_cell", + "or_poisoned", + "pin-project-lite", + "serde", + "throw_error", + "wasm-bindgen", +] + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown 0.15.4", +] + +[[package]] +name = "indextree" +version = "4.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb9e21e48c85fa6643a38caca564645a3bbc9211edf506fc8ed690c7e7b4d3c7" +dependencies = [ + "indextree-macros", +] + +[[package]] +name = "indextree-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f85dac6c239acc85fd61934c572292d93adfd2de459d9c032aa22b553506e915" +dependencies = [ + "either", + "itertools", + "proc-macro2", + "quote", + "strum", + "syn", + "thiserror 2.0.12", +] + +[[package]] +name = "interpolator" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" + +[[package]] +name = "inventory" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83" +dependencies = [ + "rustversion", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leptos" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ceaf7d86820125c57dcd380edac4b972debf480ee4c7eea6dd7cea212615978" +dependencies = [ + "any_spawner", + "base64", + "cfg-if", + "either_of", + "futures", + "getrandom 0.2.16", + "hydration_context", + "leptos_config", + "leptos_dom", + "leptos_hot_reload", + "leptos_macro", + "leptos_server", + "oco_ref", + "or_poisoned", + "paste", + "rand 0.8.5", + "reactive_graph", + "rustc-hash", + "rustc_version", + "send_wrapper", + "serde", + "serde_qs", + "server_fn", + "slotmap", + "tachys", + "thiserror 2.0.12", + "throw_error", + "typed-builder", + "typed-builder-macro", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos-use" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6eac17e7d306b4ad67158aba97c1490884ba304add4321069cb63fe0834c3b1" +dependencies = [ + "cfg-if", + "chrono", + "codee", + "cookie", + "default-struct-builder", + "futures-util", + "gloo-timers", + "js-sys", + "lazy_static", + "leptos", + "paste", + "send_wrapper", + "thiserror 2.0.12", + "unic-langid", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "leptos_axum" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1734594e53c07c9415c837439193cd30d679db19e96ec63f71c108ffe88513eb" +dependencies = [ + "any_spawner", + "axum", + "dashmap", + "futures", + "hydration_context", + "leptos", + "leptos_integration_utils", + "leptos_macro", + "leptos_meta", + "leptos_router", + "once_cell", + "parking_lot", + "server_fn", + "tachys", + "tokio", + "tower", + "tower-http", +] + +[[package]] +name = "leptos_config" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4100ad54455f82b686c9d0500a45c909eb50ce68ccb2ed51439ff2596f54fd" +dependencies = [ + "config", + "regex", + "serde", + "thiserror 2.0.12", + "typed-builder", +] + +[[package]] +name = "leptos_dom" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adaca2ec1d6215a7c43dc6353d487e4e34faf325b8e4df2ca3df488964d403be" +dependencies = [ + "js-sys", + "or_poisoned", + "reactive_graph", + "send_wrapper", + "tachys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_hot_reload" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597f84532609518092960ac241741963c90c216ee11f752e1b238b846f043640" +dependencies = [ + "anyhow", + "camino", + "indexmap", + "parking_lot", + "proc-macro2", + "quote", + "rstml", + "serde", + "syn", + "walkdir", +] + +[[package]] +name = "leptos_integration_utils" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77da211bc85005a31db4835d7cf033b5508ae71d6663667e0b245f2e41425293" +dependencies = [ + "futures", + "hydration_context", + "leptos", + "leptos_config", + "leptos_meta", + "leptos_router", + "reactive_graph", +] + +[[package]] +name = "leptos_macro" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a2ec91579e9a1344adc1eee637cb774a01354a3d25857cbd028b0289efe131d" +dependencies = [ + "attribute-derive", + "cfg-if", + "convert_case 0.8.0", + "html-escape", + "itertools", + "leptos_hot_reload", + "prettyplease", + "proc-macro-error2", + "proc-macro2", + "quote", + "rstml", + "rustc_version", + "server_fn_macro", + "syn", + "uuid", +] + +[[package]] +name = "leptos_meta" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03b2d5e0a9e7060bce4862c009ced3c2ad0afe45d005eaa4defa3872d2d2aac" +dependencies = [ + "futures", + "indexmap", + "leptos", + "once_cell", + "or_poisoned", + "send_wrapper", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_router" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d1b70dbd176bd0129e5db0e2ed48b7ff076c1b17cf52bbd0e85dcc3e6b9cbc0" +dependencies = [ + "any_spawner", + "either_of", + "futures", + "gloo-net", + "js-sys", + "leptos", + "leptos_router_macro", + "once_cell", + "or_poisoned", + "percent-encoding", + "reactive_graph", + "rustc_version", + "send_wrapper", + "tachys", + "thiserror 2.0.12", + "url", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_router_macro" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8403f583a69812524b98c97f2ed0258771e205cd0f715b1bc6ba5e4f70dcb94b" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "leptos_server" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5af59932aa8a640da4d3d20650cf07084433e25db0ee690203d893b81773db29" +dependencies = [ + "any_spawner", + "base64", + "codee", + "futures", + "hydration_context", + "or_poisoned", + "reactive_graph", + "send_wrapper", + "serde", + "serde_json", + "server_fn", + "tachys", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "linear-map" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "manyhow" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" +dependencies = [ + "manyhow-macros", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "manyhow-macros" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "never" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91" + +[[package]] +name = "next_tuple" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60993920e071b0c9b66f14e2b32740a4e27ffc82854dcd72035887f336a09a28" + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "oco_ref" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64b94982fe39a861561cf67ff17a7849f2cedadbbad960a797634032b7abb998" +dependencies = [ + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "onig" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" +dependencies = [ + "bitflags 2.9.1", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "or_poisoned" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c04f5d74368e4d0dfe06c45c8627c81bd7c317d52762d118fb9b3076f6420fd" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "personal-site" +version = "0.1.0" +dependencies = [ + "axum", + "chrono", + "codee", + "console_error_panic_hook", + "console_log", + "dashmap", + "gray_matter", + "http", + "indextree", + "leptos", + "leptos-use", + "leptos_axum", + "leptos_meta", + "leptos_router", + "log", + "pulldown-cmark", + "regex", + "rss", + "rust-embed", + "serde", + "serde_json", + "syntect", + "thiserror 2.0.12", + "tokio", + "tower", + "tower-http", + "tracing", + "wasm-bindgen", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d77244ce2d584cd84f6a15f86195b8c9b2a0dfbfd817c09e0464244091a58ed" +dependencies = [ + "base64", + "indexmap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro-utils" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", + "yansi", +] + +[[package]] +name = "pulldown-cmark" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" +dependencies = [ + "bitflags 2.9.1", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "encoding_rs", + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "quote-use" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9619db1197b497a36178cfc736dc96b271fe918875fbf1344c436a7e93d0321e" +dependencies = [ + "quote", + "quote-use-macros", +] + +[[package]] +name = "quote-use-macros" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82ebfb7faafadc06a7ab141a6f67bcfb24cb8beb158c6fe933f2f035afa99f35" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "reactive_graph" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac68cd988635779e6f378871257cbccfd51d7eeb7bc0bf6184835842aed51cc1" +dependencies = [ + "any_spawner", + "async-lock", + "futures", + "guardian", + "hydration_context", + "or_poisoned", + "pin-project-lite", + "rustc-hash", + "rustc_version", + "send_wrapper", + "serde", + "slotmap", + "thiserror 2.0.12", + "web-sys", +] + +[[package]] +name = "reactive_stores" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e02f30b9cc6645e330e926dd778d4bcbd0e5770bdf4ec3d422dc0fe3c52a41" +dependencies = [ + "dashmap", + "guardian", + "itertools", + "or_poisoned", + "paste", + "reactive_graph", + "reactive_stores_macro", + "rustc-hash", + "send_wrapper", +] + +[[package]] +name = "reactive_stores_macro" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f2bfb3b29c0b93d2d58a157b2a6783957bb592b296ab0b98a18fc3cdc574b07" +dependencies = [ + "convert_case 0.8.0", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "redox_syscall" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rss" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2107738f003660f0a91f56fd3e3bd3ab5d918b2ddaf1e1ec2136fb1c46f71bf" +dependencies = [ + "atom_syndication", + "derive_builder", + "never", + "quick-xml", +] + +[[package]] +name = "rstml" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61cf4616de7499fc5164570d40ca4e1b24d231c6833a88bff0fe00725080fd56" +dependencies = [ + "derive-where", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", + "syn_derive", + "thiserror 2.0.12", +] + +[[package]] +name = "rust-embed" +version = "8.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" +dependencies = [ + "sha2", + "walkdir", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" +dependencies = [ + "futures-core", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_qs" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b417bedc008acbdf6d6b4bc482d29859924114bbe2650b7921fb68a261d0aa6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror 2.0.12", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "server_fn" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b0f92b9d3a62c73f238ac21f7a09f15bad335a9d1651514d9da80d2eaf8d4c" +dependencies = [ + "axum", + "base64", + "bytes", + "const-str", + "const_format", + "dashmap", + "futures", + "gloo-net", + "http", + "http-body-util", + "hyper", + "inventory", + "js-sys", + "once_cell", + "pin-project-lite", + "rustc_version", + "rustversion", + "send_wrapper", + "serde", + "serde_json", + "serde_qs", + "server_fn_macro_default", + "thiserror 2.0.12", + "throw_error", + "tokio", + "tower", + "tower-layer", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "341dd1087afe9f3e546c5979a4f0b6d55ac072e1201313f86e7fe364223835ac" +dependencies = [ + "const_format", + "convert_case 0.8.0", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro_default" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5ab934f581482a66da82f2b57b15390ad67c9ab85bd9a6c54bb65060fb1380" +dependencies = [ + "server_fn_macro", + "syn", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb066a04799e45f5d582e8fc6ec8e6d6896040d00898eb4e6a835196815b219" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syntect" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1" +dependencies = [ + "bincode", + "bitflags 1.3.2", + "flate2", + "fnv", + "once_cell", + "onig", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror 1.0.69", + "walkdir", + "yaml-rust", +] + +[[package]] +name = "tachys" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51a9a5d6436e532fd27b49bcca005a038bf510fc369687de830121a74811ccf4" +dependencies = [ + "any_spawner", + "async-trait", + "const_str_slice_concat", + "drain_filter_polyfill", + "either_of", + "erased", + "futures", + "html-escape", + "indexmap", + "itertools", + "js-sys", + "linear-map", + "next_tuple", + "oco_ref", + "once_cell", + "or_poisoned", + "parking_lot", + "paste", + "reactive_graph", + "reactive_stores", + "rustc-hash", + "rustc_version", + "send_wrapper", + "slotmap", + "throw_error", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "throw_error" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41e42a6afdde94f3e656fae18f837cb9bbe500a5ac5de325b09f3ec05b9c28e3" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.1", + "sha1", + "thiserror 2.0.12", + "utf-8", +] + +[[package]] +name = "typed-builder" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce63bcaf7e9806c206f7d7b9c1f38e0dce8bb165a80af0898161058b19248534" +dependencies = [ + "typed-builder-macro", +] + +[[package]] +name = "typed-builder-macro" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d8d828da2a3d759d3519cdf29a5bac49c77d039ad36d0782edadbf9cd5415b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unic-langid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" +dependencies = [ + "tinystr", +] + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yaml-rust2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] From 9979e929baabec418f30c781146a42e2de3aa50a Mon Sep 17 00:00:00 2001 From: BakerNet Date: Sun, 29 Jun 2025 08:41:43 -0700 Subject: [PATCH 8/8] leptosfmt --- src/app/header.rs | 3 ++- src/app/terminal/fs_tools.rs | 12 ++---------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/app/header.rs b/src/app/header.rs index b89ce99..0e5daa0 100644 --- a/src/app/header.rs +++ b/src/app/header.rs @@ -898,7 +898,8 @@ pub fn Header() -> impl IntoView { } }); let render_func = move |item: TabCompletionItem| { - let is_sel = selected.as_ref().map(|s| &s.completion_text) == Some(&item.completion_text); + let is_sel = selected.as_ref().map(|s| &s.completion_text) + == Some(&item.completion_text); auto_comp_item(&item, is_sel).into_any() }; view! { diff --git a/src/app/terminal/fs_tools.rs b/src/app/terminal/fs_tools.rs index 3c12a30..98d2625 100644 --- a/src/app/terminal/fs_tools.rs +++ b/src/app/terminal/fs_tools.rs @@ -304,21 +304,13 @@ fn VfsLsView(items: Vec, #[prop(default = false)] long_format: bool) -> view! {
- {item.node.long_meta_string(item.link_count)} - {styled_filename} + {item.node.long_meta_string(item.link_count)} {styled_filename}
} .into_any() }; - view! { -
- {items - .into_iter() - .map(long_render_func) - .collect_view()} -
- } + view! {
{items.into_iter().map(long_render_func).collect_view()}
} .into_any() } else { let short_render_func = move |item: VfsItem| {