//! Path utilities — mirrors the original `~/.secpanel/` layout. //! //! (The on-disk directory is intentionally kept as `~/.secpanel/` for //! backwards compatibility with the original Tcl/Tk secpanel and with //! prior shellm versions — see `secpanel_dir()` below.) use std::path::{Path, PathBuf}; use directories::ProjectDirs; /// Application version (matches the package version). pub const SP_VERSION: &str = "1.1.0"; /// Return the base shellm data directory (`~/.secpanel`). /// /// On the original Tcl/Tk secpanel this was always `$env(HOME)/.secpanel`. /// We keep the same path on Linux/Mac. On Windows we fall back to /// `%APPDATA%/secpanel` via the `directories` crate, but secpanel has always /// been a Unix-first tool so this is mostly a defensive fallback. /// /// (The directory name is kept as `.secpanel` for backwards compatibility — /// renaming it would orphan existing users' profiles, configs, and history.) pub fn secpanel_dir() -> PathBuf { if let Some(home) = dirs::home_dir() { return home.join(".secpanel"); } if let Some(pd) = ProjectDirs::from("", "", "secpanel") { return pd.data_dir().to_path_buf(); } PathBuf::from(".secpanel") } pub fn config_file() -> PathBuf { secpanel_dir().join("config") } pub fn profiles_dir() -> PathBuf { secpanel_dir().join("profiles") } pub fn profile_file(name: &str) -> PathBuf { profiles_dir().join(format!("{name}.profile")) } pub fn runfiles_dir() -> PathBuf { secpanel_dir().join(".runfiles") } pub fn trace_log() -> PathBuf { runfiles_dir().join("trace.log") } pub fn history_file() -> PathBuf { secpanel_dir().join("history") } pub fn init_marker() -> PathBuf { secpanel_dir().join(".init") } pub fn ssh_dir() -> PathBuf { dirs::home_dir() .map(|h| h.join(".ssh")) .unwrap_or_else(|| PathBuf::from(".ssh")) } pub fn known_hosts_file() -> PathBuf { ssh_dir().join("known_hosts") } pub fn reports_dir() -> PathBuf { secpanel_dir().join("reports") } /// Ensure that the standard shellm directory tree exists with 0700 perms /// on the root. Returns the path to the shellm dir on success. pub fn ensure_secpanel_tree() -> std::io::Result { let root = secpanel_dir(); if !root.exists() { std::fs::create_dir_all(&root)?; // Best-effort chmod 700. On non-Unix this is a no-op. #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; std::fs::set_permissions(&root, std::fs::Permissions::from_mode(0o700))?; } log::info!("Created shellm dir {} (chmod 700)", root.display()); } std::fs::create_dir_all(profiles_dir())?; std::fs::create_dir_all(runfiles_dir())?; Ok(root) } /// Best-effort lookup of an executable on `PATH`. pub fn which(prog: &str) -> Option { if Path::new(prog).is_absolute() { return if Path::new(prog).exists() { Some(PathBuf::from(prog)) } else { None }; } std::env::var_os("PATH").and_then(|paths| { std::env::split_paths(&paths) .map(|p| p.join(prog)) .find(|p| p.is_file()) }) } /// Like `which` but returns the input unchanged if not found. pub fn which_or(prog: &str, fallback: &str) -> String { which(prog).map(|p| p.to_string_lossy().into_owned()).unwrap_or_else(|| fallback.to_string()) }