From 3c75ab1a34108af1481902bea1dcb06ba34c33ff Mon Sep 17 00:00:00 2001 From: aditya singh rathore Date: Sat, 26 Jul 2025 14:41:37 +0530 Subject: [PATCH 1/4] Update shell.rs --- src-tauri/src/commands/shell.rs | 213 +++++++++++++++++++++++++------- 1 file changed, 169 insertions(+), 44 deletions(-) diff --git a/src-tauri/src/commands/shell.rs b/src-tauri/src/commands/shell.rs index f1bdb0a..685e0db 100644 --- a/src-tauri/src/commands/shell.rs +++ b/src-tauri/src/commands/shell.rs @@ -8,48 +8,19 @@ pub async fn run_shell(command: String) -> Result { return Ok("__EXIT_SHELL__".to_string()); } - // Special handling for ls/dir commands to add file type indicators - if cmd == "ls" || cmd.starts_with("ls ") || cmd == "dir" || cmd.starts_with("dir ") { - let enhanced_command = if cfg!(target_os = "linux") || cfg!(target_os = "macos") { - if cmd == "ls" { - "ls -la".to_string() - } else if cmd.starts_with("ls ") { - if !command.contains(" -l") && !command.contains(" -a") { - format!("{} -la", command) - } else if !command.contains(" -l") { - format!("{} -l", command) - } else if !command.contains(" -a") { - format!("{} -a", command) - } else { - command.clone() - } - } else { - command.clone() - } - } else { - command.clone() - }; - - let output = if cfg!(target_os = "windows") { - Command::new("cmd").args(["/C", &enhanced_command]).output() - } else { - Command::new("sh").arg("-c").arg(&enhanced_command).output() - }; - - match output { - Ok(out) => { - let stdout = String::from_utf8_lossy(&out.stdout); - let stderr = String::from_utf8_lossy(&out.stderr); - - if !stderr.is_empty() && stdout.is_empty() { - return Ok(stderr.to_string()); - } - - // Process the output to add file type indicators - return Ok(crate::utils::format_directory_listing(&stdout)); - } - Err(e) => return Err(format!("Failed to run command: {}", e)), - } + // Handle minimal ls commands + if cmd == "ls" { + return handle_minimal_ls(None, false).await; + } else if cmd == "la" { + return handle_minimal_ls(None, true).await; + } else if cmd.starts_with("ls ") { + let path = cmd.strip_prefix("ls ").unwrap().trim(); + let path = if path.is_empty() { None } else { Some(path.to_string()) }; + return handle_minimal_ls(path, false).await; + } else if cmd.starts_with("la ") { + let path = cmd.strip_prefix("la ").unwrap().trim(); + let path = if path.is_empty() { None } else { Some(path.to_string()) }; + return handle_minimal_ls(path, true).await; } // Regular command execution (non-ls commands) @@ -78,6 +49,160 @@ pub async fn run_shell(command: String) -> Result { } } +async fn handle_minimal_ls(path: Option, show_hidden: bool) -> Result { + let dir_path = path.unwrap_or_else(|| ".".to_string()); + + // Build the appropriate ls command based on OS and options + let ls_command = if cfg!(target_os = "windows") { + if show_hidden { + format!("dir /a /b \"{}\"", dir_path) + } else { + format!("dir /b \"{}\"", dir_path) + } + } else { + if show_hidden { + format!("ls -la \"{}\"", dir_path) + } else { + format!("ls -l \"{}\"", dir_path) + } + }; + + let output = if cfg!(target_os = "windows") { + Command::new("cmd").args(["/C", &ls_command]).output() + } else { + Command::new("sh").arg("-c").arg(&ls_command).output() + }; + + match output { + Ok(out) => { + let stdout = String::from_utf8_lossy(&out.stdout); + let stderr = String::from_utf8_lossy(&out.stderr); + + if !stderr.is_empty() && stdout.is_empty() { + return Ok(stderr.to_string()); + } + + // Format the output with colors and indicators + Ok(format_minimal_ls_output(&stdout, show_hidden)) + } + Err(e) => Err(format!("Failed to run ls command: {}", e)), + } +} + +fn format_minimal_ls_output(output: &str, show_hidden: bool) -> String { + let lines: Vec<&str> = output.lines().collect(); + let mut formatted_lines = Vec::new(); + + // Skip the first line if it starts with "total" (ls summary) + let start_idx = if lines.get(0).map_or(false, |l| l.starts_with("total ")) { + 1 + } else { + 0 + }; + + for line in lines.iter().skip(start_idx) { + let line_trim = line.trim(); + if line_trim.is_empty() { + continue; + } + + if cfg!(target_os = "linux") || cfg!(target_os = "macos") { + if let Some(formatted) = format_unix_ls_line(line_trim, show_hidden) { + formatted_lines.push(formatted); + } + } else { + if let Some(formatted) = format_windows_ls_line(line_trim, show_hidden) { + formatted_lines.push(formatted); + } + } + } + + formatted_lines.join("\n") +} + +fn format_unix_ls_line(line: &str, show_hidden: bool) -> Option { + if line.len() < 10 { + return None; + } + + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() < 9 { + return None; + } + + // Join all parts from index 8 to handle filenames with spaces + let filename = parts[8..].join(" "); + + // Skip . and .. entries, and hidden files if not showing hidden + if filename == "." || filename == ".." { + return None; + } + + if !show_hidden && filename.starts_with('.') { + return None; + } + + // Get file type and permissions + let permissions = &parts[0]; + let file_type = permissions.chars().next().unwrap_or('?'); + + // Format with colors and indicators + let formatted_name = match file_type { + 'd' => { + // Directory - blue color with trailing / + format!("\x1b[94m{}/\x1b[0m", filename) + } + 'l' => { + //Symbolic link - cyan color + format!("\x1b[96m{}\x1b[0m", filename) + } + '-' => { + // Regular file - check if executable + if permissions.chars().nth(3).unwrap_or('-') == 'x' || + permissions.chars().nth(6).unwrap_or('-') == 'x' || + permissions.chars().nth(9).unwrap_or('-') == 'x' { + // Executable - green color with trailing * + format!("\x1b[92m{}*\x1b[0m", filename) + } else { + // Regular file - default color + filename + } + } + _ => filename, // Other file types - default color + }; + + Some(formatted_name) +} + +fn format_windows_ls_line(line: &str, show_hidden: bool) -> Option { + let filename = line.trim(); + + if filename == "." || filename == ".." { + return None; + } + + // Skip hidden files if not showing hidden (Windows hidden files start with .) + if !show_hidden && filename.starts_with('.') { + return None; + } + + let path = std::path::Path::new(filename); + + // Format based on file type + if path.is_dir() { + // Directory - blue color with trailing / + Some(format!("\x1b[94m{}/\x1b[0m", filename)) + } else if filename.ends_with(".exe") || + filename.ends_with(".bat") || + filename.ends_with(".cmd") { + // Executable - green color with trailing * + Some(format!("\x1b[92m{}*\x1b[0m", filename)) + } else { + // Regular file - default color + Some(filename.to_string()) + } +} + #[tauri::command] pub async fn run_sudo_command(command: String, password: String) -> Result { if !cfg!(target_os = "linux") && !cfg!(target_os = "macos") { @@ -179,7 +304,7 @@ pub async fn list_directory_contents(path: Option) -> Result let lines: Vec<&str> = stdout.lines().collect(); let mut file_list = Vec::new(); - // Skip the first line if it starts with "total" (ls summary) + // Skip the first line if starts with "total" (ls summary) let start_idx = if lines.get(0).map_or(false, |l| l.starts_with("total ")) { 1 } else { @@ -264,4 +389,4 @@ pub fn change_directory(path: String) -> Result { }, Err(e) => Err(format!("Failed to change directory: {}", e)) } -} +} \ No newline at end of file From 6dd31bb08171990380916ce5a329bfd09c72d906 Mon Sep 17 00:00:00 2001 From: aditya singh rathore Date: Sat, 26 Jul 2025 22:25:43 +0530 Subject: [PATCH 2/4] Created code of conduct --- src-tauri/code of conduct | 78 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src-tauri/code of conduct diff --git a/src-tauri/code of conduct b/src-tauri/code of conduct new file mode 100644 index 0000000..83a755f --- /dev/null +++ b/src-tauri/code of conduct @@ -0,0 +1,78 @@ + +# Term Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to make participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies within all project spaces, and it also applies when +an individual is representing the project or its community in public spaces. +Examples of representing a project or community include using an official +project e-mail address, posting via an official social media account, or acting +as an appointed representative at an online or offline event. Representation of +a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at [mail](vaibhav.sapate@qed42.com). All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq + From 60db88e3619e9069085d99c0223aafb0c41b3762 Mon Sep 17 00:00:00 2001 From: aditya singh rathore Date: Sat, 26 Jul 2025 22:31:04 +0530 Subject: [PATCH 3/4] Deleted file --- src-tauri/code of conduct | 78 --------------------------------------- 1 file changed, 78 deletions(-) delete mode 100644 src-tauri/code of conduct diff --git a/src-tauri/code of conduct b/src-tauri/code of conduct deleted file mode 100644 index 83a755f..0000000 --- a/src-tauri/code of conduct +++ /dev/null @@ -1,78 +0,0 @@ - -# Term Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to make participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies within all project spaces, and it also applies when -an individual is representing the project or its community in public spaces. -Examples of representing a project or community include using an official -project e-mail address, posting via an official social media account, or acting -as an appointed representative at an online or offline event. Representation of -a project may be further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at [mail](vaibhav.sapate@qed42.com). All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see -https://www.contributor-covenant.org/faq - From 85b92a42b3b938fd55010cbf4f0a78f8e542b90c Mon Sep 17 00:00:00 2001 From: aditya singh rathore Date: Fri, 1 Aug 2025 09:00:24 +0530 Subject: [PATCH 4/4] Updated content --- src-tauri/src/commands/shell.rs | 753 ++++++++++++++++++++++---------- src-tauri/src/utils/mod.rs | 419 +++++++++++++++++- 2 files changed, 952 insertions(+), 220 deletions(-) diff --git a/src-tauri/src/commands/shell.rs b/src-tauri/src/commands/shell.rs index 685e0db..b8657de 100644 --- a/src-tauri/src/commands/shell.rs +++ b/src-tauri/src/commands/shell.rs @@ -1,4 +1,398 @@ +// use std::process::Command; + +// #[tauri::command] +// pub async fn run_shell(command: String) -> Result { +// let cmd = command.trim(); + +// if cmd == "exit" { +// return Ok("__EXIT_SHELL__".to_string()); +// } + +// // Handle minimal ls commands +// if cmd == "ls" { +// return handle_minimal_ls(None, false).await; +// } else if cmd == "la" { +// return handle_minimal_ls(None, true).await; +// } else if cmd.starts_with("ls ") { +// let path = cmd.strip_prefix("ls ").unwrap().trim(); +// let path = if path.is_empty() { None } else { Some(path.to_string()) }; +// return handle_minimal_ls(path, false).await; +// } else if cmd.starts_with("la ") { +// let path = cmd.strip_prefix("la ").unwrap().trim(); +// let path = if path.is_empty() { None } else { Some(path.to_string()) }; +// return handle_minimal_ls(path, true).await; +// } + +// // Regular command execution (non-ls commands) +// let output = if cfg!(target_os = "windows") { +// Command::new("cmd").args(["/C", &command]).output() +// } else { +// Command::new("sh").arg("-c").arg(&command).output() +// }; + +// match output { +// Ok(out) => { +// let stdout = String::from_utf8_lossy(&out.stdout); +// let stderr = String::from_utf8_lossy(&out.stderr); + +// if !stderr.is_empty() && stdout.is_empty() { +// Ok(stderr.to_string()) +// } else if !stdout.is_empty() { +// Ok(stdout.to_string()) +// } else { +// Ok(String::from( +// "Command executed successfully with no output.", +// )) +// } +// } +// Err(e) => Err(format!("Failed to run command: {}", e)), +// } +// } + +// async fn handle_minimal_ls(path: Option, show_hidden: bool) -> Result { +// let dir_path = path.unwrap_or_else(|| ".".to_string()); + +// // Build the appropriate ls command based on OS and options +// let ls_command = if cfg!(target_os = "windows") { +// if show_hidden { +// format!("dir /a /b \"{}\"", dir_path) +// } else { +// format!("dir /b \"{}\"", dir_path) +// } +// } else { +// if show_hidden { +// format!("ls -la \"{}\"", dir_path) +// } else { +// format!("ls -l \"{}\"", dir_path) +// } +// }; + +// let output = if cfg!(target_os = "windows") { +// Command::new("cmd").args(["/C", &ls_command]).output() +// } else { +// Command::new("sh").arg("-c").arg(&ls_command).output() +// }; + +// match output { +// Ok(out) => { +// let stdout = String::from_utf8_lossy(&out.stdout); +// let stderr = String::from_utf8_lossy(&out.stderr); + +// if !stderr.is_empty() && stdout.is_empty() { +// return Ok(stderr.to_string()); +// } + +// // Format the output with colors and indicators +// Ok(format_minimal_ls_output(&stdout, show_hidden)) +// } +// Err(e) => Err(format!("Failed to run ls command: {}", e)), +// } +// } + +// fn format_minimal_ls_output(output: &str, show_hidden: bool) -> String { +// let lines: Vec<&str> = output.lines().collect(); +// let mut formatted_lines = Vec::new(); + +// // Skip the first line if it starts with "total" (ls summary) +// let start_idx = if lines.get(0).map_or(false, |l| l.starts_with("total ")) { +// 1 +// } else { +// 0 +// }; + +// for line in lines.iter().skip(start_idx) { +// let line_trim = line.trim(); +// if line_trim.is_empty() { +// continue; +// } + +// if cfg!(target_os = "linux") || cfg!(target_os = "macos") { +// if let Some(formatted) = format_unix_ls_line(line_trim, show_hidden) { +// formatted_lines.push(formatted); +// } +// } else { +// if let Some(formatted) = format_windows_ls_line(line_trim, show_hidden) { +// formatted_lines.push(formatted); +// } +// } +// } + +// formatted_lines.join("\n") +// } + +// fn format_unix_ls_line(line: &str, show_hidden: bool) -> Option { +// if line.len() < 10 { +// return None; +// } + +// let parts: Vec<&str> = line.split_whitespace().collect(); +// if parts.len() < 9 { +// return None; +// } + +// // Join all parts from index 8 to handle filenames with spaces +// let filename = parts[8..].join(" "); + +// // Skip . and .. entries, and hidden files if not showing hidden +// if filename == "." || filename == ".." { +// return None; +// } + +// if !show_hidden && filename.starts_with('.') { +// return None; +// } + +// // Get file type and permissions +// let permissions = &parts[0]; +// let file_type = permissions.chars().next().unwrap_or('?'); + +// // Format with colors and indicators +// let formatted_name = match file_type { +// 'd' => { +// // Directory - blue color with trailing / +// format!("\x1b[94m{}/\x1b[0m", filename) +// } +// 'l' => { +// //Symbolic link - cyan color +// format!("\x1b[96m{}\x1b[0m", filename) +// } +// '-' => { +// // Regular file - check if executable +// if permissions.chars().nth(3).unwrap_or('-') == 'x' || +// permissions.chars().nth(6).unwrap_or('-') == 'x' || +// permissions.chars().nth(9).unwrap_or('-') == 'x' { +// // Executable - green color with trailing * +// format!("\x1b[92m{}*\x1b[0m", filename) +// } else { +// // Regular file - default color +// filename +// } +// } +// _ => filename, // Other file types - default color +// }; + +// Some(formatted_name) +// } + +// fn format_windows_ls_line(line: &str, show_hidden: bool) -> Option { +// let filename = line.trim(); + +// if filename == "." || filename == ".." { +// return None; +// } + +// // Skip hidden files if not showing hidden (Windows hidden files start with .) +// if !show_hidden && filename.starts_with('.') { +// return None; +// } + +// let path = std::path::Path::new(filename); + +// // Format based on file type +// if path.is_dir() { +// // Directory - blue color with trailing / +// Some(format!("\x1b[94m{}/\x1b[0m", filename)) +// } else if filename.ends_with(".exe") || +// filename.ends_with(".bat") || +// filename.ends_with(".cmd") { +// // Executable - green color with trailing * +// Some(format!("\x1b[92m{}*\x1b[0m", filename)) +// } else { +// // Regular file - default color +// Some(filename.to_string()) +// } +// } + +// #[tauri::command] +// pub async fn run_sudo_command(command: String, password: String) -> Result { +// if !cfg!(target_os = "linux") && !cfg!(target_os = "macos") { +// return Err("Sudo is only supported on Linux and macOS".to_string()); +// } + +// let cmd = if command.starts_with("sudo ") { +// command[5..].to_string() +// } else { +// command +// }; + +// let temp_dir = std::env::temp_dir(); +// let output_file = temp_dir.join(format!("term_sudo_{}", std::process::id())); +// let output_path = output_file.to_string_lossy(); + +// let script = format!( +// r#"#!/bin/bash +// echo "{}" | sudo -S {} > "{}" 2>&1 +// exit_code=$? +// if [ $exit_code -ne 0 ]; then +// echo "Command failed with exit code $exit_code" >> "{}" +// fi +// "#, +// password.replace("\"", "\\\""), +// cmd.replace("\"", "\\\""), +// output_path, +// output_path +// ); + +// let script_file = temp_dir.join(format!("term_sudo_script_{}", std::process::id())); +// std::fs::write(&script_file, script).map_err(|e| format!("Failed to create script: {}", e))?; + +// #[cfg(not(target_os = "windows"))] +// { +// use std::os::unix::fs::PermissionsExt; +// let mut perms = std::fs::metadata(&script_file) +// .map_err(|e| format!("Failed to get file metadata: {}", e))? +// .permissions(); +// perms.set_mode(0o755); +// std::fs::set_permissions(&script_file, perms) +// .map_err(|e| format!("Failed to set permissions: {}", e))?; +// } + +// let _status = tokio::process::Command::new(&script_file) +// .status() +// .await +// .map_err(|e| format!("Failed to execute script: {}", e))?; + +// let _ = std::fs::remove_file(&script_file); + +// let output = std::fs::read_to_string(&output_file) +// .map_err(|e| format!("Failed to read output: {}", e))?; + +// let _ = std::fs::remove_file(&output_file); + +// if output.contains("incorrect password") +// || output.contains("Sorry, try again") +// || output.contains("Authentication failure") +// || output.contains("authentication failure") +// || output.contains("sudo: no password was provided") +// || output.contains("sudo: 1 incorrect password attempt") +// { +// return Err("Incorrect password provided".to_string()); +// } + +// Ok(output) +// } + +// #[tauri::command] +// pub fn get_current_dir() -> Result { +// std::env::current_dir() +// .map(|path| path.to_string_lossy().into_owned()) +// .map_err(|e| format!("Failed to get current directory: {}", e)) +// } + +// #[tauri::command] +// pub async fn list_directory_contents(path: Option) -> Result, String> { +// let dir_path = match path { +// Some(p) if !p.is_empty() => p, +// _ => ".".to_string(), +// }; + +// let ls_command = if cfg!(target_os = "windows") { +// format!("dir /b \"{}\"", dir_path) +// } else { +// format!("ls -la \"{}\"", dir_path) +// }; + +// let output = if cfg!(target_os = "windows") { +// Command::new("cmd").args(["/C", &ls_command]).output() +// } else { +// Command::new("sh").arg("-c").arg(&ls_command).output() +// }; + +// match output { +// Ok(out) => { +// let stdout = String::from_utf8_lossy(&out.stdout); +// let lines: Vec<&str> = stdout.lines().collect(); +// let mut file_list = Vec::new(); + +// // Skip the first line if starts with "total" (ls summary) +// let start_idx = if lines.get(0).map_or(false, |l| l.starts_with("total ")) { +// 1 +// } else { +// 0 +// }; + +// for line in lines.iter().skip(start_idx) { +// let line_trim = line.trim(); +// if line_trim.is_empty() { +// continue; +// } + +// // Unix-style ls output with permissions +// if cfg!(target_os = "linux") || cfg!(target_os = "macos") { +// if line_trim.len() < 10 { +// continue; +// } + +// let parts: Vec<&str> = line_trim.split_whitespace().collect(); +// if parts.len() < 9 { +// continue; +// } + +// // Join all parts from index 8 to handle filenames with spaces +// let filename = parts[8..].join(" "); + +// // Skip . and .. entries +// if filename == "." || filename == ".." { +// continue; +// } + +// // Check file type and add appropriate suffix +// let file_type = line_trim.chars().next().unwrap_or('?'); +// if file_type == 'd' { +// file_list.push(format!("{}/", filename)); +// } else if line_trim.contains("x") && file_type == '-' { +// file_list.push(format!("{}*", filename)); +// } else { +// file_list.push(filename); +// } +// } else { +// // Windows directory listing (simpler format) +// if line_trim != "." && line_trim != ".." { +// let path = std::path::Path::new(line_trim); +// if path.is_dir() { +// file_list.push(format!("{}/", line_trim)); +// } else if line_trim.ends_with(".exe") +// || line_trim.ends_with(".bat") +// || line_trim.ends_with(".cmd") +// { +// file_list.push(format!("{}*", line_trim)); +// } else { +// file_list.push(line_trim.to_string()); +// } +// } +// } +// } + +// Ok(file_list) +// } +// Err(e) => Err(format!("Failed to list directory: {}", e)), +// } +// } + +// #[tauri::command] +// pub fn change_directory(path: String) -> Result { +// let expanded_path = if path.starts_with("~") { +// if let Ok(home) = std::env::var("HOME") { +// path.replacen("~", &home, 1) +// } else { +// path +// } +// } else { +// path +// }; + +// match std::env::set_current_dir(expanded_path) { +// Ok(_) => { +// let new_dir = std::env::current_dir() +// .map_err(|e| format!("Failed to get current directory: {}", e))?; +// Ok(new_dir.to_string_lossy().into_owned()) +// }, +// Err(e) => Err(format!("Failed to change directory: {}", e)) +// } +// } use std::process::Command; +use std::path::{Path, PathBuf}; +use std::fs; #[tauri::command] pub async fn run_shell(command: String) -> Result { @@ -8,19 +402,9 @@ pub async fn run_shell(command: String) -> Result { return Ok("__EXIT_SHELL__".to_string()); } - // Handle minimal ls commands - if cmd == "ls" { - return handle_minimal_ls(None, false).await; - } else if cmd == "la" { - return handle_minimal_ls(None, true).await; - } else if cmd.starts_with("ls ") { - let path = cmd.strip_prefix("ls ").unwrap().trim(); - let path = if path.is_empty() { None } else { Some(path.to_string()) }; - return handle_minimal_ls(path, false).await; - } else if cmd.starts_with("la ") { - let path = cmd.strip_prefix("la ").unwrap().trim(); - let path = if path.is_empty() { None } else { Some(path.to_string()) }; - return handle_minimal_ls(path, true).await; + // Special handling for ls/dir commands with robust parsing + if cmd == "ls" || cmd.starts_with("ls ") || cmd == "dir" || cmd.starts_with("dir ") { + return handle_ls_command(command).await; } // Regular command execution (non-ls commands) @@ -49,28 +433,40 @@ pub async fn run_shell(command: String) -> Result { } } -async fn handle_minimal_ls(path: Option, show_hidden: bool) -> Result { - let dir_path = path.unwrap_or_else(|| ".".to_string()); +async fn handle_ls_command(original_command: String) -> Result { + let cmd = original_command.trim(); - // Build the appropriate ls command based on OS and options - let ls_command = if cfg!(target_os = "windows") { - if show_hidden { - format!("dir /a /b \"{}\"", dir_path) + // Extract the path from the ls command + let target_path = extract_path_from_ls_command(cmd); + let current_dir = std::env::current_dir().map_err(|e| format!("Failed to get current directory: {}", e))?; + let absolute_path = resolve_path(&target_path, ¤t_dir); + + // Use a more reliable ls format for Unix systems + let enhanced_command = if cfg!(target_os = "linux") || cfg!(target_os = "macos") { + // Use --time-style=long-iso for consistent parsing and -la for details + if cmd == "ls" { + format!("ls -la --time-style=long-iso \"{}\"", absolute_path.display()) + } else if cmd.starts_with("ls ") { + // Check if user already specified format options + if !cmd.contains(" -l") && !cmd.contains(" -a") && !cmd.contains("--time-style") { + format!("{} -la --time-style=long-iso", cmd) + } else if !cmd.contains("--time-style") { + format!("{} --time-style=long-iso", cmd) + } else { + cmd.to_string() + } } else { - format!("dir /b \"{}\"", dir_path) + cmd.to_string() } } else { - if show_hidden { - format!("ls -la \"{}\"", dir_path) - } else { - format!("ls -l \"{}\"", dir_path) - } + // Windows - use dir command with specific formatting + format!("dir /a \"{}\"", absolute_path.display()) }; let output = if cfg!(target_os = "windows") { - Command::new("cmd").args(["/C", &ls_command]).output() + Command::new("cmd").args(["/C", &enhanced_command]).output() } else { - Command::new("sh").arg("-c").arg(&ls_command).output() + Command::new("sh").arg("-c").arg(&enhanced_command).output() }; match output { @@ -82,124 +478,34 @@ async fn handle_minimal_ls(path: Option, show_hidden: bool) -> Result Err(format!("Failed to run ls command: {}", e)), - } -} - -fn format_minimal_ls_output(output: &str, show_hidden: bool) -> String { - let lines: Vec<&str> = output.lines().collect(); - let mut formatted_lines = Vec::new(); - - // Skip the first line if it starts with "total" (ls summary) - let start_idx = if lines.get(0).map_or(false, |l| l.starts_with("total ")) { - 1 - } else { - 0 - }; - - for line in lines.iter().skip(start_idx) { - let line_trim = line.trim(); - if line_trim.is_empty() { - continue; - } - - if cfg!(target_os = "linux") || cfg!(target_os = "macos") { - if let Some(formatted) = format_unix_ls_line(line_trim, show_hidden) { - formatted_lines.push(formatted); - } - } else { - if let Some(formatted) = format_windows_ls_line(line_trim, show_hidden) { - formatted_lines.push(formatted); - } + // Process the output with robust parsing + Ok(crate::utils::format_directory_listing_robust(&stdout, &absolute_path)) } + Err(e) => Err(format!("Failed to run command: {}", e)), } - - formatted_lines.join("\n") } -fn format_unix_ls_line(line: &str, show_hidden: bool) -> Option { - if line.len() < 10 { - return None; - } - - let parts: Vec<&str> = line.split_whitespace().collect(); - if parts.len() < 9 { - return None; - } - - // Join all parts from index 8 to handle filenames with spaces - let filename = parts[8..].join(" "); - - // Skip . and .. entries, and hidden files if not showing hidden - if filename == "." || filename == ".." { - return None; - } +fn extract_path_from_ls_command(cmd: &str) -> String { + let parts: Vec<&str> = cmd.split_whitespace().collect(); - if !show_hidden && filename.starts_with('.') { - return None; + // Look for the last argument that doesn't start with '-' + for part in parts.iter().rev() { + if !part.starts_with('-') && *part != "ls" && *part != "dir" { + return part.to_string(); + } } - - // Get file type and permissions - let permissions = &parts[0]; - let file_type = permissions.chars().next().unwrap_or('?'); - // Format with colors and indicators - let formatted_name = match file_type { - 'd' => { - // Directory - blue color with trailing / - format!("\x1b[94m{}/\x1b[0m", filename) - } - 'l' => { - //Symbolic link - cyan color - format!("\x1b[96m{}\x1b[0m", filename) - } - '-' => { - // Regular file - check if executable - if permissions.chars().nth(3).unwrap_or('-') == 'x' || - permissions.chars().nth(6).unwrap_or('-') == 'x' || - permissions.chars().nth(9).unwrap_or('-') == 'x' { - // Executable - green color with trailing * - format!("\x1b[92m{}*\x1b[0m", filename) - } else { - // Regular file - default color - filename - } - } - _ => filename, // Other file types - default color - }; - - Some(formatted_name) + // Default to current directory + ".".to_string() } -fn format_windows_ls_line(line: &str, show_hidden: bool) -> Option { - let filename = line.trim(); - - if filename == "." || filename == ".." { - return None; - } - - // Skip hidden files if not showing hidden (Windows hidden files start with .) - if !show_hidden && filename.starts_with('.') { - return None; - } - - let path = std::path::Path::new(filename); +fn resolve_path(path: &str, current_dir: &Path) -> PathBuf { + let path_buf = PathBuf::from(path); - // Format based on file type - if path.is_dir() { - // Directory - blue color with trailing / - Some(format!("\x1b[94m{}/\x1b[0m", filename)) - } else if filename.ends_with(".exe") || - filename.ends_with(".bat") || - filename.ends_with(".cmd") { - // Executable - green color with trailing * - Some(format!("\x1b[92m{}*\x1b[0m", filename)) + if path_buf.is_absolute() { + path_buf } else { - // Regular file - default color - Some(filename.to_string()) + current_dir.join(path_buf) } } @@ -282,111 +588,120 @@ pub fn get_current_dir() -> Result { #[tauri::command] pub async fn list_directory_contents(path: Option) -> Result, String> { let dir_path = match path { - Some(p) if !p.is_empty() => p, - _ => ".".to_string(), - }; - - let ls_command = if cfg!(target_os = "windows") { - format!("dir /b \"{}\"", dir_path) - } else { - format!("ls -la \"{}\"", dir_path) - }; - - let output = if cfg!(target_os = "windows") { - Command::new("cmd").args(["/C", &ls_command]).output() - } else { - Command::new("sh").arg("-c").arg(&ls_command).output() + Some(p) if !p.is_empty() => { + let expanded = crate::utils::expand_home_path(&p) + .map_err(|e| format!("Failed to expand path: {}", e))?; + PathBuf::from(expanded) + }, + _ => std::env::current_dir().map_err(|e| format!("Failed to get current directory: {}", e))?, }; - match output { - Ok(out) => { - let stdout = String::from_utf8_lossy(&out.stdout); - let lines: Vec<&str> = stdout.lines().collect(); + // Use direct file system reading instead of shell commands for reliability + match fs::read_dir(&dir_path) { + Ok(entries) => { let mut file_list = Vec::new(); - - // Skip the first line if starts with "total" (ls summary) - let start_idx = if lines.get(0).map_or(false, |l| l.starts_with("total ")) { - 1 - } else { - 0 - }; - - for line in lines.iter().skip(start_idx) { - let line_trim = line.trim(); - if line_trim.is_empty() { - continue; - } - - // Unix-style ls output with permissions - if cfg!(target_os = "linux") || cfg!(target_os = "macos") { - if line_trim.len() < 10 { - continue; - } - - let parts: Vec<&str> = line_trim.split_whitespace().collect(); - if parts.len() < 9 { - continue; + + for entry in entries { + match entry { + Ok(dir_entry) => { + let file_name = dir_entry.file_name().to_string_lossy().into_owned(); + + // Skip hidden files starting with . (make this configurable if needed) + if file_name.starts_with('.') && file_name != "." && file_name != ".." { + continue; + } + + let file_type = get_file_type_robust(&dir_entry.path()); + + match file_type { + FileType::Directory => file_list.push(format!("{}/", file_name)), + FileType::Executable => file_list.push(format!("{}*", file_name)), + FileType::Symlink => file_list.push(format!("{}@", file_name)), + FileType::Regular => file_list.push(file_name), + } } - - // Join all parts from index 8 to handle filenames with spaces - let filename = parts[8..].join(" "); - - // Skip . and .. entries - if filename == "." || filename == ".." { + Err(e) => { + eprintln!("Error reading directory entry: {}", e); continue; } - - // Check file type and add appropriate suffix - let file_type = line_trim.chars().next().unwrap_or('?'); - if file_type == 'd' { - file_list.push(format!("{}/", filename)); - } else if line_trim.contains("x") && file_type == '-' { - file_list.push(format!("{}*", filename)); - } else { - file_list.push(filename); - } - } else { - // Windows directory listing (simpler format) - if line_trim != "." && line_trim != ".." { - let path = std::path::Path::new(line_trim); - if path.is_dir() { - file_list.push(format!("{}/", line_trim)); - } else if line_trim.ends_with(".exe") - || line_trim.ends_with(".bat") - || line_trim.ends_with(".cmd") - { - file_list.push(format!("{}*", line_trim)); - } else { - file_list.push(line_trim.to_string()); - } - } } } - + + // Sort the results for consistent output + file_list.sort(); Ok(file_list) } - Err(e) => Err(format!("Failed to list directory: {}", e)), + Err(e) => Err(format!("Failed to read directory '{}': {}", dir_path.display(), e)), } } #[tauri::command] pub fn change_directory(path: String) -> Result { - let expanded_path = if path.starts_with("~") { - if let Ok(home) = std::env::var("HOME") { - path.replacen("~", &home, 1) - } else { - path - } - } else { - path - }; + let expanded_path = crate::utils::expand_home_path(&path) + .map_err(|e| format!("Failed to expand path: {}", e))?; - match std::env::set_current_dir(expanded_path) { + match std::env::set_current_dir(&expanded_path) { Ok(_) => { let new_dir = std::env::current_dir() .map_err(|e| format!("Failed to get current directory: {}", e))?; Ok(new_dir.to_string_lossy().into_owned()) }, - Err(e) => Err(format!("Failed to change directory: {}", e)) + Err(e) => Err(format!("Failed to change directory to '{}': {}", expanded_path, e)) + } +} + +#[derive(Debug)] +enum FileType { + Directory, + Symlink, + Executable, + Regular, +} + +fn get_file_type_robust(path: &Path) -> FileType { + // Use std::fs to get accurate file information + match fs::symlink_metadata(path) { + Ok(metadata) => { + if metadata.file_type().is_symlink() { + FileType::Symlink + } else if metadata.is_dir() { + FileType::Directory + } else if is_executable(&metadata, path) { + FileType::Executable + } else { + FileType::Regular + } + } + Err(_) => { + // Fallback to basic checks if metadata fails + if path.is_dir() { + FileType::Directory + } else { + FileType::Regular + } + } + } +} + +#[cfg(unix)] +fn is_executable(metadata: &fs::Metadata, _path: &Path) -> bool { + use std::os::unix::fs::PermissionsExt; + let mode = metadata.permissions().mode(); + mode & 0o111 != 0 // Check if any execute bit is set +} + +#[cfg(windows)] +fn is_executable(_metadata: &fs::Metadata, path: &Path) -> bool { + // On Windows, check file extension + if let Some(extension) = path.extension() { + let ext = extension.to_string_lossy().to_lowercase(); + matches!(ext.as_str(), "exe" | "bat" | "cmd" | "com" | "ps1") + } else { + false } +} + +#[cfg(not(any(unix, windows)))] +fn is_executable(_metadata: &fs::Metadata, _path: &Path) -> bool { + false } \ No newline at end of file diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs index d543357..99b52b3 100644 --- a/src-tauri/src/utils/mod.rs +++ b/src-tauri/src/utils/mod.rs @@ -1,3 +1,61 @@ +// pub fn format_directory_listing(output: &str) -> String { +// let lines: Vec<&str> = output.lines().collect(); +// let mut formatted_output = String::new(); + +// for line in lines { +// if line.trim().is_empty() { +// formatted_output.push_str(line); +// formatted_output.push('\n'); +// continue; +// } + +// if line.starts_with("total ") || line.contains("Directory of") { +// formatted_output.push_str(line); +// formatted_output.push('\n'); +// continue; +// } + +// // Special handling for Unix-style ls output +// if cfg!(target_os = "linux") || cfg!(target_os = "macos") { +// let first_char = line.chars().next().unwrap_or(' '); + +// if first_char == 'd' { +// formatted_output.push_str(&format!("{{DIR}}{}{{/DIR}}", line)); +// formatted_output.push('\n'); +// continue; +// } else if first_char == 'l' { +// formatted_output.push_str(&format!("{{LINK}}{}{{/LINK}}", line)); +// formatted_output.push('\n'); +// continue; +// } else if first_char == '-' || first_char.is_alphanumeric() { +// formatted_output.push_str(&format!("{{FILE}}{}{{/FILE}}", line)); +// formatted_output.push('\n'); +// continue; +// } +// } + +// // Windows DIR command handling or fallback +// let tokens: Vec<&str> = line.split_whitespace().collect(); +// if !tokens.is_empty() { +// let name = tokens.last().unwrap_or(&""); + +// if line.contains("") || name.ends_with("/") || name.ends_with("\\") { +// formatted_output.push_str(&format!("{{DIR}}{}{{/DIR}}", line)); +// } else { +// formatted_output.push_str(&format!("{{FILE}}{}{{/FILE}}", line)); +// } +// formatted_output.push('\n'); +// } else { +// formatted_output.push_str(line); +// formatted_output.push('\n'); +// } +// } + +// formatted_output +// } +use std::path::Path; +use std::fs; + pub fn format_directory_listing(output: &str) -> String { let lines: Vec<&str> = output.lines().collect(); let mut formatted_output = String::new(); @@ -15,7 +73,15 @@ pub fn format_directory_listing(output: &str) -> String { continue; } - // Special handling for Unix-style ls output + // Handle the new robust formatting tags + if line.contains("{DIR}") || line.contains("{FILE}") || line.contains("{LINK}") || line.contains("{EXEC}") { + let formatted_line = format_tagged_line(line); + formatted_output.push_str(&formatted_line); + formatted_output.push('\n'); + continue; + } + + // Legacy handling for backwards compatibility if cfg!(target_os = "linux") || cfg!(target_os = "macos") { let first_char = line.chars().next().unwrap_or(' '); @@ -53,3 +119,354 @@ pub fn format_directory_listing(output: &str) -> String { formatted_output } + +pub fn format_directory_listing_robust(output: &str, dir_path: &Path) -> String { + let lines: Vec<&str> = output.lines().collect(); + let mut formatted_output = String::new(); + + for line in lines { + let line_trim = line.trim(); + if line_trim.is_empty() { + formatted_output.push_str(line); + formatted_output.push('\n'); + continue; + } + + // Skip total line and directory headers + if line_trim.starts_with("total ") || line_trim.contains("Directory of") { + formatted_output.push_str(line); + formatted_output.push('\n'); + continue; + } + + // Robust Unix-style ls parsing with --time-style=long-iso + if cfg!(target_os = "linux") || cfg!(target_os = "macos") { + if let Some(formatted_line) = parse_unix_ls_line_robust(line_trim, dir_path) { + formatted_output.push_str(&formatted_line); + formatted_output.push('\n'); + } else { + // Fallback for lines that don't match expected format + formatted_output.push_str(line); + formatted_output.push('\n'); + } + } else { + // Windows parsing + if let Some(formatted_line) = parse_windows_dir_line_robust(line_trim, dir_path) { + formatted_output.push_str(&formatted_line); + formatted_output.push('\n'); + } else { + formatted_output.push_str(line); + formatted_output.push('\n'); + } + } + } + + formatted_output +} + +fn parse_unix_ls_line_robust(line: &str, dir_path: &Path) -> Option { + // Expected format with --time-style=long-iso: + // drwxr-xr-x 2 user group 4096 2023-12-01 10:30 filename + + // Skip . and .. entries + if line.ends_with(" .") || line.ends_with(" ..") { + return None; + } + + // Check if it's a proper ls line (starts with permissions) + if line.len() < 10 || !line.chars().next().map_or(false, |c| "dl-".contains(c)) { + return None; + } + + // More robust parsing: find the filename by looking for the last space after date/time + // Format: permissions links owner group size date time filename + let parts: Vec<&str> = line.split_whitespace().collect(); + + if parts.len() < 8 { + return None; + } + + // Find filename by reconstructing from the end + // Date format: YYYY-MM-DD, Time format: HH:MM + let mut filename_start_idx = None; + + // Look for the time pattern (HH:MM) and take everything after it as filename + for (i, part) in parts.iter().enumerate() { + if part.len() == 5 && part.contains(':') && part.matches(':').count() == 1 { + // Validate it's actually a time (digits on both sides of colon) + let time_parts: Vec<&str> = part.split(':').collect(); + if time_parts.len() == 2 { + if time_parts[0].parse::().is_ok() && time_parts[1].parse::().is_ok() { + filename_start_idx = Some(i + 1); + break; + } + } + } + } + + let filename = if let Some(start_idx) = filename_start_idx { + if start_idx < parts.len() { + parts[start_idx..].join(" ") + } else { + return None; + } + } else { + // Fallback: assume last part is filename + parts.last()?.to_string() + }; + + // Skip hidden files starting with . (except if we want to show them) + if filename.starts_with('.') && filename != "." && filename != ".." { + // You might want to make this configurable + } + + // Use actual file system check instead of relying on ls output parsing + let file_path = dir_path.join(&filename); + let file_type = get_file_type_robust(&file_path); + + match file_type { + FileType::Directory => Some(format!("{{DIR}}{}{{/DIR}}", line)), + FileType::Symlink => Some(format!("{{LINK}}{}{{/LINK}}", line)), + FileType::Executable => Some(format!("{{EXEC}}{}{{/EXEC}}", line)), + FileType::Regular => Some(format!("{{FILE}}{}{{/FILE}}", line)), + } +} + +fn parse_windows_dir_line_robust(line: &str, dir_path: &Path) -> Option { + // Windows dir output format varies, but generally: + // MM/DD/YYYY HH:MM AM/PM dirname + // MM/DD/YYYY HH:MM AM/PM 1,234 filename.ext + + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() < 4 { + return None; + } + + // Extract filename (last part typically) + let filename = parts.last()?.to_string(); + + // Skip . and .. entries + if filename == "." || filename == ".." { + return None; + } + + // Use file system check for accurate type detection + let file_path = dir_path.join(&filename); + let file_type = get_file_type_robust(&file_path); + + match file_type { + FileType::Directory => Some(format!("{{DIR}}{}{{/DIR}}", line)), + FileType::Symlink => Some(format!("{{LINK}}{}{{/LINK}}", line)), + FileType::Executable => Some(format!("{{EXEC}}{}{{/EXEC}}", line)), + FileType::Regular => Some(format!("{{FILE}}{}{{/FILE}}", line)), + } +} + +fn format_tagged_line(line: &str) -> String { + if line.contains("{DIR}") { + let clean_line = line.replace("{DIR}", "").replace("{/DIR}", ""); + format!("📁 {}", clean_line.trim()) + } else if line.contains("{LINK}") { + let clean_line = line.replace("{LINK}", "").replace("{/LINK}", ""); + format!("🔗 {}", clean_line.trim()) + } else if line.contains("{EXEC}") { + let clean_line = line.replace("{EXEC}", "").replace("{/EXEC}", ""); + format!("⚙️ {}", clean_line.trim()) + } else if line.contains("{FILE}") { + let clean_line = line.replace("{FILE}", "").replace("{/FILE}", ""); + + // Add file type icons based on extension + let icon = get_file_icon(&clean_line); + format!("{} {}", icon, clean_line.trim()) + } else { + line.to_string() + } +} + +fn get_file_icon(filename: &str) -> &'static str { + let lower_name = filename.to_lowercase(); + + if lower_name.ends_with(".jpg") || lower_name.ends_with(".jpeg") || + lower_name.ends_with(".png") || lower_name.ends_with(".gif") || + lower_name.ends_with(".bmp") || lower_name.ends_with(".svg") || + lower_name.ends_with(".webp") { + "🖼️" + } else if lower_name.ends_with(".pdf") || lower_name.ends_with(".doc") || + lower_name.ends_with(".docx") || lower_name.ends_with(".txt") || + lower_name.ends_with(".md") || lower_name.ends_with(".rtf") { + "📝" + } else if lower_name.ends_with(".zip") || lower_name.ends_with(".tar") || + lower_name.ends_with(".gz") || lower_name.ends_with(".rar") || + lower_name.ends_with(".7z") || lower_name.ends_with(".bz2") { + "📦" + } else if lower_name.ends_with(".mp3") || lower_name.ends_with(".wav") || + lower_name.ends_with(".flac") || lower_name.ends_with(".ogg") || + lower_name.ends_with(".m4a") { + "🎵" + } else if lower_name.ends_with(".mp4") || lower_name.ends_with(".avi") || + lower_name.ends_with(".mkv") || lower_name.ends_with(".mov") || + lower_name.ends_with(".wmv") || lower_name.ends_with(".webm") { + "🎬" + } else if lower_name.ends_with(".js") || lower_name.ends_with(".ts") || + lower_name.ends_with(".jsx") || lower_name.ends_with(".tsx") || + lower_name.ends_with(".html") || lower_name.ends_with(".css") || + lower_name.ends_with(".json") || lower_name.ends_with(".xml") { + "💻" + } else if lower_name.ends_with(".rs") || lower_name.ends_with(".py") || + lower_name.ends_with(".java") || lower_name.ends_with(".cpp") || + lower_name.ends_with(".c") || lower_name.ends_with(".h") { + "⚡" + } else { + "📄" + } +} + +#[derive(Debug)] +enum FileType { + Directory, + Symlink, + Executable, + Regular, +} + +fn get_file_type_robust(path: &Path) -> FileType { + // Use std::fs to get accurate file information + match fs::symlink_metadata(path) { + Ok(metadata) => { + if metadata.file_type().is_symlink() { + FileType::Symlink + } else if metadata.is_dir() { + FileType::Directory + } else if is_executable(&metadata, path) { + FileType::Executable + } else { + FileType::Regular + } + } + Err(_) => { + // Fallback to basic checks if metadata fails + if path.is_dir() { + FileType::Directory + } else { + FileType::Regular + } + } + } +} + +#[cfg(unix)] +fn is_executable(metadata: &fs::Metadata, _path: &Path) -> bool { + use std::os::unix::fs::PermissionsExt; + let mode = metadata.permissions().mode(); + mode & 0o111 != 0 // Check if any execute bit is set +} + +#[cfg(windows)] +fn is_executable(_metadata: &fs::Metadata, path: &Path) -> bool { + // On Windows, check file extension + if let Some(extension) = path.extension() { + let ext = extension.to_string_lossy().to_lowercase(); + matches!(ext.as_str(), "exe" | "bat" | "cmd" | "com" | "ps1") + } else { + false + } +} + +#[cfg(not(any(unix, windows)))] +fn is_executable(_metadata: &fs::Metadata, _path: &Path) -> bool { + false +} + +// Additional utility functions for robust path handling +pub fn normalize_path(path: &str) -> String { + // Handle different path separators and clean up the path + let normalized = if cfg!(target_os = "windows") { + path.replace('/', "\\") + } else { + path.replace('\\', "/") + }; + + // Remove redundant separators + let separator = if cfg!(target_os = "windows") { "\\" } else { "/" }; + let double_sep = format!("{}{}", separator, separator); + + normalized.replace(&double_sep, separator) +} + +pub fn expand_home_path(path: &str) -> Result { + if path.starts_with("~") { + if let Ok(home) = std::env::var("HOME") { + Ok(path.replacen("~", &home, 1)) + } else if cfg!(target_os = "windows") { + if let Ok(home) = std::env::var("USERPROFILE") { + Ok(path.replacen("~", &home, 1)) + } else { + Err("Cannot determine home directory".to_string()) + } + } else { + Err("Cannot determine home directory".to_string()) + } + } else { + Ok(path.to_string()) + } +} + +pub fn get_file_size_human_readable(size: u64) -> String { + const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"]; + const THRESHOLD: u64 = 1024; + + if size == 0 { + return "0 B".to_string(); + } + + let mut size_f = size as f64; + let mut unit_index = 0; + + while size_f >= THRESHOLD as f64 && unit_index < UNITS.len() - 1 { + size_f /= THRESHOLD as f64; + unit_index += 1; + } + + if unit_index == 0 { + format!("{} {}", size, UNITS[unit_index]) + } else { + format!("{:.1} {}", size_f, UNITS[unit_index]) + } +} + +pub fn format_permissions(mode: u32) -> String { + #[cfg(unix)] + { + let mut perms = String::with_capacity(10); + + // File type + perms.push(match mode & 0o170000 { + 0o040000 => 'd', // directory + 0o120000 => 'l', // symlink + 0o100000 => '-', // regular file + _ => '?', + }); + + // Owner permissions + perms.push(if mode & 0o400 != 0 { 'r' } else { '-' }); + perms.push(if mode & 0o200 != 0 { 'w' } else { '-' }); + perms.push(if mode & 0o100 != 0 { 'x' } else { '-' }); + + // Group permissions + perms.push(if mode & 0o040 != 0 { 'r' } else { '-' }); + perms.push(if mode & 0o020 != 0 { 'w' } else { '-' }); + perms.push(if mode & 0o010 != 0 { 'x' } else { '-' }); + + // Other permissions + perms.push(if mode & 0o004 != 0 { 'r' } else { '-' }); + perms.push(if mode & 0o002 != 0 { 'w' } else { '-' }); + perms.push(if mode & 0o001 != 0 { 'x' } else { '-' }); + + perms + } + + #[cfg(not(unix))] + { + // Simplified for non-Unix systems + "rwxrwxrwx".to_string() + } \ No newline at end of file