#[cfg(not(target_os = "windows"))] use super::utils::directory_nix as directory_utils; #[cfg(target_os = "windows")] use super::utils::directory_win as directory_utils; use indexmap::IndexMap; use path_slash::PathExt; use std::iter::FromIterator; use std::path::{Path, PathBuf}; use unicode_segmentation::UnicodeSegmentation; use super::{Context, Module}; use super::utils::directory::truncate; use crate::config::RootModuleConfig; use crate::configs::directory::DirectoryConfig; use crate::context::Shell; use crate::formatter::StringFormatter; const HOME_SYMBOL: &str = "~"; /// Creates a module with the current directory /// /// Will perform path contraction, substitution, and truncation. /// /// **Contraction** /// /// - Paths beginning with the home directory or with a git repo right inside /// the home directory will be contracted to `~` /// - Paths containing a git repo will contract to begin at the repo root /// /// **Substitution** /// Paths will undergo user-provided substitutions of substrings /// /// **Truncation** /// Paths will be limited in length to `3` path components by default. pub fn module<'a>(context: &'a Context) -> Option> { let mut module = context.new_module("directory"); let config: DirectoryConfig = DirectoryConfig::try_load(module.config); let current_dir = &get_current_dir(&context, &config); let home_dir = dirs_next::home_dir().unwrap(); log::debug!("Current directory: {:?}", current_dir); let repo = &context.get_repo().ok()?; let dir_string = match &repo.root { Some(repo_root) if config.truncate_to_repo && (repo_root != &home_dir) => { log::debug!("Repo root: {:?}", repo_root); // Contract the path to the git repo root contract_repo_path(current_dir, repo_root) .unwrap_or_else(|| contract_path(current_dir, &home_dir, HOME_SYMBOL)) } // Contract the path to the home directory _ => contract_path(current_dir, &home_dir, HOME_SYMBOL), }; log::debug!("Dir string: {}", dir_string); let substituted_dir = substitute_path(dir_string, &config.substitutions); // Truncate the dir string to the maximum number of path components let truncated_dir_string = truncate(substituted_dir, config.truncation_length as usize); let prefix = if is_truncated(&truncated_dir_string) { // Substitutions could have changed the prefix, so don't allow them and // fish-style path contraction together if config.fish_style_pwd_dir_length > 0 && config.substitutions.is_empty() { // If user is using fish style path, we need to add the segment first let contracted_home_dir = contract_path(¤t_dir, &home_dir, HOME_SYMBOL); to_fish_style( config.fish_style_pwd_dir_length as usize, contracted_home_dir, &truncated_dir_string, ) } else { String::from(config.truncation_symbol) } } else { String::from("") }; let displayed_path = prefix + &truncated_dir_string; let lock_symbol = String::from(config.read_only); let parsed = StringFormatter::new(config.format).and_then(|formatter| { formatter .map_style(|variable| match variable { "style" => Some(Ok(config.style)), "read_only_style" => Some(Ok(config.read_only_style)), _ => None, }) .map(|variable| match variable { "path" => Some(Ok(&displayed_path)), "read_only" => { if is_readonly_dir(&context.current_dir) { Some(Ok(&lock_symbol)) } else { None } } _ => None, }) .parse(None) }); module.set_segments(match parsed { Ok(segments) => segments, Err(error) => { log::warn!("Error in module `directory`:\n{}", error); return None; } }); Some(module) } fn is_truncated(path: &str) -> bool { !(path.starts_with(HOME_SYMBOL) || PathBuf::from(path).has_root() || (cfg!(target_os = "windows") && PathBuf::from(String::from(path) + r"\").has_root())) } fn get_current_dir(context: &Context, config: &DirectoryConfig) -> PathBuf { // Using environment PWD is the standard approach for determining logical path // If this is None for any reason, we fall back to reading the os-provided path let physical_current_dir = if config.use_logical_path { match context.get_env("PWD") { Some(mut x) => { // Prevent Powershell from prepending "Microsoft.PowerShell.Core\FileSystem::" to some paths if cfg!(windows) && context.shell == Shell::PowerShell { if let Some(no_prefix) = x.strip_prefix(r"Microsoft.PowerShell.Core\FileSystem::") { x = no_prefix.to_string(); } } Some(PathBuf::from(x)) } None => { log::debug!("Error getting PWD environment variable!"); None } } } else { match std::env::current_dir() { Ok(x) => Some(x), Err(e) => { log::debug!("Error getting physical current directory: {}", e); None } } }; physical_current_dir.unwrap_or_else(|| PathBuf::from(&context.current_dir)) } fn is_readonly_dir(path: &Path) -> bool { match directory_utils::is_write_allowed(path) { Ok(res) => !res, Err(e) => { log::debug!( "Failed to determine read only status of directory '{:?}': {}", path, e ); false } } } /// Contract the root component of a path /// /// Replaces the `top_level_path` in a given `full_path` with the provided /// `top_level_replacement`. fn contract_path(full_path: &Path, top_level_path: &Path, top_level_replacement: &str) -> String { if !full_path.starts_with(top_level_path) { return full_path.to_slash().unwrap(); } if full_path == top_level_path { return top_level_replacement.to_string(); } format!( "{replacement}{separator}{path}", replacement = top_level_replacement, separator = "/", path = full_path .strip_prefix(top_level_path) .unwrap() .to_slash() .unwrap() ) } /// Contract the root component of a path based on the real path /// /// Replaces the `top_level_path` in a given `full_path` with the provided /// `top_level_replacement` by walking ancestors and comparing its real path. fn contract_repo_path(full_path: &Path, top_level_path: &Path) -> Option { let top_level_real_path = real_path(top_level_path); // Walk ancestors to preserve logical path in `full_path`. // If we'd just `full_real_path.strip_prefix(top_level_real_path)`, // then it wouldn't preserve logical path. It would've returned physical path. for (i, ancestor) in full_path.ancestors().enumerate() { let ancestor_real_path = real_path(ancestor); if ancestor_real_path != top_level_real_path { continue; } let components: Vec<_> = full_path.components().collect(); let repo_name = components[components.len() - i - 1].as_os_str().to_str()?; if i == 0 { return Some(repo_name.to_string()); } let path = PathBuf::from_iter(&components[components.len() - i..]); return Some(format!( "{repo_name}{separator}{path}", repo_name = repo_name, separator = "/", path = path.to_slash()? )); } None } fn real_path>(path: P) -> PathBuf { let path = path.as_ref(); let mut buf = PathBuf::new(); for component in path.components() { let next = buf.join(component); if let Ok(realpath) = next.read_link() { if realpath.is_absolute() { buf = realpath; } else { buf.push(realpath); } } else { buf = next; } } buf.canonicalize().unwrap_or_else(|_| path.into()) } /// Perform a list of string substitutions on the path /// /// Given a list of (from, to) pairs, this will perform the string /// substitutions, in order, on the path. Any non-pair of strings is ignored. fn substitute_path(dir_string: String, substitutions: &IndexMap) -> String { let mut substituted_dir = dir_string; for substitution_pair in substitutions.iter() { substituted_dir = substituted_dir.replace(substitution_pair.0, substitution_pair.1); } substituted_dir } /// Takes part before contracted path and replaces it with fish style path /// /// Will take the first letter of each directory before the contracted path and /// use that in the path instead. See the following example. /// /// Absolute Path: `/Users/Bob/Projects/work/a_repo` /// Contracted Path: `a_repo` /// With Fish Style: `~/P/w/a_repo` /// /// Absolute Path: `/some/Path/not/in_a/repo/but_nested` /// Contracted Path: `in_a/repo/but_nested` /// With Fish Style: `/s/P/n/in_a/repo/but_nested` fn to_fish_style(pwd_dir_length: usize, dir_string: String, truncated_dir_string: &str) -> String { let replaced_dir_string = dir_string.trim_end_matches(truncated_dir_string).to_owned(); let components = replaced_dir_string.split('/').collect::>(); if components.is_empty() { return replaced_dir_string; } components .into_iter() .map(|word| -> String { let chars = UnicodeSegmentation::graphemes(word, true).collect::>(); match word { "" => "".to_string(), _ if chars.len() <= pwd_dir_length => word.to_string(), _ if word.starts_with('.') => chars[..=pwd_dir_length].join(""), _ => chars[..pwd_dir_length].join(""), } }) .collect::>() .join("/") } #[cfg(test)] mod tests { use super::*; use crate::test::ModuleRenderer; use ansi_term::Color; use dirs_next::home_dir; #[cfg(not(target_os = "windows"))] use std::os::unix::fs::symlink; #[cfg(target_os = "windows")] use std::os::windows::fs::symlink_dir as symlink; use std::path::Path; use std::process::Command; use std::{fs, io}; use tempfile::TempDir; #[test] fn contract_home_directory() { let full_path = Path::new("/Users/astronaut/schematics/rocket"); let home = Path::new("/Users/astronaut"); let output = contract_path(full_path, home, "~"); assert_eq!(output, "~/schematics/rocket"); } #[test] fn contract_repo_directory() { let full_path = Path::new("/Users/astronaut/dev/rocket-controls/src"); let repo_root = Path::new("/Users/astronaut/dev/rocket-controls"); let output = contract_path(full_path, repo_root, "rocket-controls"); assert_eq!(output, "rocket-controls/src"); } #[test] #[cfg(target_os = "windows")] fn contract_windows_style_home_directory() { let full_path = Path::new("C:\\Users\\astronaut\\schematics\\rocket"); let home = Path::new("C:\\Users\\astronaut"); let output = contract_path(full_path, home, "~"); assert_eq!(output, "~/schematics/rocket"); } #[test] #[cfg(target_os = "windows")] fn contract_windows_style_repo_directory() { let full_path = Path::new("C:\\Users\\astronaut\\dev\\rocket-controls\\src"); let repo_root = Path::new("C:\\Users\\astronaut\\dev\\rocket-controls"); let output = contract_path(full_path, repo_root, "rocket-controls"); assert_eq!(output, "rocket-controls/src"); } #[test] #[cfg(target_os = "windows")] fn contract_windows_style_no_top_level_directory() { let full_path = Path::new("C:\\Some\\Other\\Path"); let top_level_path = Path::new("C:\\Users\\astronaut"); let output = contract_path(full_path, top_level_path, "~"); assert_eq!(output, "C:/Some/Other/Path"); } #[test] #[cfg(target_os = "windows")] fn contract_windows_style_root_directory() { let full_path = Path::new("C:\\"); let top_level_path = Path::new("C:\\Users\\astronaut"); let output = contract_path(full_path, top_level_path, "~"); assert_eq!(output, "C:"); } #[test] fn substitute_prefix_and_middle() { let full_path = "/absolute/path/foo/bar/baz"; let mut substitutions = IndexMap::new(); substitutions.insert("/absolute/path".to_string(), ""); substitutions.insert("/bar/".to_string(), "/"); let output = substitute_path(full_path.to_string(), &substitutions); assert_eq!(output, "/foo/baz"); } #[test] fn fish_style_with_user_home_contracted_path() { let path = "~/starship/engines/booster/rocket"; let output = to_fish_style(1, path.to_string(), "engines/booster/rocket"); assert_eq!(output, "~/s/"); } #[test] fn fish_style_with_user_home_contracted_path_and_dot_dir() { let path = "~/.starship/engines/booster/rocket"; let output = to_fish_style(1, path.to_string(), "engines/booster/rocket"); assert_eq!(output, "~/.s/"); } #[test] fn fish_style_with_no_contracted_path() { // `truncation_length = 2` let path = "/absolute/Path/not/in_a/repo/but_nested"; let output = to_fish_style(1, path.to_string(), "repo/but_nested"); assert_eq!(output, "/a/P/n/i/"); } #[test] fn fish_style_with_pwd_dir_len_no_contracted_path() { // `truncation_length = 2` let path = "/absolute/Path/not/in_a/repo/but_nested"; let output = to_fish_style(2, path.to_string(), "repo/but_nested"); assert_eq!(output, "/ab/Pa/no/in/"); } #[test] fn fish_style_with_duplicate_directories() { let path = "~/starship/tmp/C++/C++/C++"; let output = to_fish_style(1, path.to_string(), "C++"); assert_eq!(output, "~/s/t/C/C/"); } #[test] fn fish_style_with_unicode() { let path = "~/starship/tmp/目录/a̐éö̲/目录"; let output = to_fish_style(1, path.to_string(), "目录"); assert_eq!(output, "~/s/t/目/a̐/"); } fn init_repo(path: &Path) -> io::Result<()> { Command::new("git") .args(&["init"]) .current_dir(path) .output() .map(|_| ()) } fn make_known_tempdir(root: &Path) -> io::Result<(TempDir, String)> { fs::create_dir_all(root)?; let dir = TempDir::new_in(root)?; let path = dir .path() .file_name() .unwrap() .to_string_lossy() .to_string(); Ok((dir, path)) } #[test] fn windows_strip_prefix() { let with_prefix = r"Microsoft.PowerShell.Core\FileSystem::/path"; let without_prefix = r"/path"; let actual = ModuleRenderer::new("directory") // use a different physical path here as a sentinel value .path("/") .env("PWD", with_prefix) .shell(Shell::PowerShell) .config(toml::toml! { [directory] format = "$path" truncation_length = 100 }) .collect() .unwrap(); let expected = if cfg!(windows) { without_prefix } else { with_prefix }; let expected = Path::new(expected).to_slash().unwrap(); assert_eq!(actual, expected); } #[test] fn windows_strip_prefix_no_pwsh() { let with_prefix = r"Microsoft.PowerShell.Core\FileSystem::/path"; let actual = ModuleRenderer::new("directory") // use a different physical path here as a sentinel value .path("/") .env("PWD", with_prefix) .shell(Shell::Bash) .config(toml::toml! { [directory] format = "$path" truncation_length = 100 }) .collect() .unwrap(); let expected = Path::new(with_prefix).to_slash().unwrap(); assert_eq!(actual, expected); } #[cfg(not(target_os = "windows"))] mod linux { use super::*; use std::sync::atomic::{AtomicBool, Ordering}; // As tests are run in parallel we have to keep a lock on which of the // two tests are currently running as they both modify `HOME` which can // override the other value resulting in inconsistent runs which is why // we only run one of these tests at once. static LOCK: AtomicBool = AtomicBool::new(false); #[test] #[ignore] fn symlinked_subdirectory_git_repo_out_of_tree() -> io::Result<()> { while LOCK.swap(true, Ordering::Acquire) {} let tmp_dir = TempDir::new_in(home_dir().unwrap().as_path())?; let repo_dir = tmp_dir.path().join("above-repo").join("rocket-controls"); let src_dir = repo_dir.join("src/meters/fuel-gauge"); let symlink_dir = tmp_dir.path().join("fuel-gauge"); fs::create_dir_all(&src_dir)?; init_repo(&repo_dir)?; symlink(&src_dir, &symlink_dir)?; // We can't mock `HOME` since dirs-next uses it which does not care about our mocking let previous_home = home_dir().unwrap(); std::env::set_var("HOME", tmp_dir.path()); let actual = ModuleRenderer::new("directory").path(symlink_dir).collect(); let expected = Some(format!("{} ", Color::Cyan.bold().paint("~/fuel-gauge"))); std::env::set_var("HOME", previous_home.as_path()); assert_eq!(expected, actual); LOCK.store(false, Ordering::Release); tmp_dir.close() } #[test] #[ignore] fn git_repo_in_home_directory_truncate_to_repo_true() -> io::Result<()> { while LOCK.swap(true, Ordering::Acquire) {} let tmp_dir = TempDir::new_in(home_dir().unwrap().as_path())?; let dir = tmp_dir.path().join("src/fuel-gauge"); fs::create_dir_all(&dir)?; init_repo(&tmp_dir.path())?; // We can't mock `HOME` since dirs-next uses it which does not care about our mocking let previous_home = home_dir().unwrap(); std::env::set_var("HOME", tmp_dir.path()); let actual = ModuleRenderer::new("directory") .config(toml::toml! { [directory] // `truncate_to_repo = true` should attempt to display the truncated path truncate_to_repo = true truncation_length = 5 }) .path(dir) .collect(); let expected = Some(format!("{} ", Color::Cyan.bold().paint("~/src/fuel-gauge"))); std::env::set_var("HOME", previous_home.as_path()); assert_eq!(expected, actual); LOCK.store(false, Ordering::Release); tmp_dir.close() } #[test] fn directory_in_root() -> io::Result<()> { let actual = ModuleRenderer::new("directory").path("/etc").collect(); let expected = Some(format!( "{}{} ", Color::Cyan.bold().paint("/etc"), Color::Red.normal().paint("🔒") )); assert_eq!(expected, actual); Ok(()) } } #[test] fn home_directory() -> io::Result<()> { let actual = ModuleRenderer::new("directory") .path(home_dir().unwrap()) .config(toml::toml! { // Necessary if homedir is a git repo [directory] truncate_to_repo = false }) .collect(); let expected = Some(format!("{} ", Color::Cyan.bold().paint("~"))); assert_eq!(expected, actual); Ok(()) } #[test] fn substituted_truncated_path() -> io::Result<()> { let actual = ModuleRenderer::new("directory") .path("/some/long/network/path/workspace/a/b/c/dev") .config(toml::toml! { [directory] truncation_length = 4 [directory.substitutions] "/some/long/network/path" = "/some/net" "a/b/c" = "d" }) .collect(); let expected = Some(format!( "{} ", Color::Cyan.bold().paint("net/workspace/d/dev") )); assert_eq!(expected, actual); Ok(()) } #[test] fn substitution_order() -> io::Result<()> { let actual = ModuleRenderer::new("directory") .path("/path/to/sub") .config(toml::toml! { [directory.substitutions] "/path/to/sub" = "/correct/order" "/to/sub" = "/wrong/order" }) .collect(); let expected = Some(format!("{} ", Color::Cyan.bold().paint("/correct/order"))); assert_eq!(expected, actual); Ok(()) } #[test] fn strange_substitution() -> io::Result<()> { let strange_sub = "/\\/;,!"; let actual = ModuleRenderer::new("directory") .path("/foo/bar/regular/path") .config(toml::toml! { [directory] truncation_length = 0 fish_style_pwd_dir_length = 2 // Overridden by substitutions [directory.substitutions] "regular" = strange_sub }) .collect(); let expected = Some(format!( "{} ", Color::Cyan .bold() .paint(format!("/foo/bar/{}/path", strange_sub)) )); assert_eq!(expected, actual); Ok(()) } #[test] fn directory_in_home() -> io::Result<()> { let (tmp_dir, name) = make_known_tempdir(home_dir().unwrap().as_path())?; let dir = tmp_dir.path().join("starship"); fs::create_dir_all(&dir)?; let actual = ModuleRenderer::new("directory").path(dir).collect(); let expected = Some(format!( "{} ", Color::Cyan.bold().paint(format!("~/{}/starship", name)) )); assert_eq!(expected, actual); tmp_dir.close() } #[test] fn truncated_directory_in_home() -> io::Result<()> { let (tmp_dir, name) = make_known_tempdir(home_dir().unwrap().as_path())?; let dir = tmp_dir.path().join("engine/schematics"); fs::create_dir_all(&dir)?; let actual = ModuleRenderer::new("directory").path(dir).collect(); let expected = Some(format!( "{} ", Color::Cyan .bold() .paint(format!("{}/engine/schematics", name)) )); assert_eq!(expected, actual); tmp_dir.close() } #[test] fn fish_directory_in_home() -> io::Result<()> { let (tmp_dir, name) = make_known_tempdir(home_dir().unwrap().as_path())?; let dir = tmp_dir.path().join("starship/schematics"); fs::create_dir_all(&dir)?; let actual = ModuleRenderer::new("directory") .config(toml::toml! { [directory] truncation_length = 1 fish_style_pwd_dir_length = 2 }) .path(&dir) .collect(); let expected = Some(format!( "{} ", Color::Cyan .bold() .paint(format!("~/{}/st/schematics", name.split_at(3).0)) )); assert_eq!(expected, actual); tmp_dir.close() } #[test] fn root_directory() -> io::Result<()> { let actual = ModuleRenderer::new("directory").path("/").collect(); #[cfg(not(target_os = "windows"))] let expected = Some(format!( "{}{} ", Color::Cyan.bold().paint("/"), Color::Red.normal().paint("🔒") )); #[cfg(target_os = "windows")] let expected = Some(format!("{} ", Color::Cyan.bold().paint("/"))); assert_eq!(expected, actual); Ok(()) } #[test] fn truncated_directory_in_root() -> io::Result<()> { let (tmp_dir, name) = make_known_tempdir(Path::new("/tmp"))?; let dir = tmp_dir.path().join("thrusters/rocket"); fs::create_dir_all(&dir)?; let actual = ModuleRenderer::new("directory").path(dir).collect(); let expected = Some(format!( "{} ", Color::Cyan .bold() .paint(format!("{}/thrusters/rocket", name)) )); assert_eq!(expected, actual); tmp_dir.close() } #[test] fn truncated_directory_config_large() -> io::Result<()> { let (tmp_dir, _) = make_known_tempdir(Path::new("/tmp"))?; let dir = tmp_dir.path().join("thrusters/rocket"); fs::create_dir_all(&dir)?; let actual = ModuleRenderer::new("directory") .config(toml::toml! { [directory] truncation_length = 100 }) .path(&dir) .collect(); let expected = Some(format!( "{} ", Color::Cyan .bold() .paint(truncate(dir.to_slash_lossy(), 100)) )); assert_eq!(expected, actual); tmp_dir.close() } #[test] fn fish_style_directory_config_large() -> io::Result<()> { let (tmp_dir, _) = make_known_tempdir(Path::new("/tmp"))?; let dir = tmp_dir.path().join("thrusters/rocket"); fs::create_dir_all(&dir)?; let actual = ModuleRenderer::new("directory") .config(toml::toml! { [directory] truncation_length = 1 fish_style_pwd_dir_length = 100 }) .path(&dir) .collect(); let expected = Some(format!( "{} ", Color::Cyan .bold() .paint(to_fish_style(100, dir.to_slash_lossy(), "")) )); assert_eq!(expected, actual); tmp_dir.close() } #[test] fn truncated_directory_config_small() -> io::Result<()> { let (tmp_dir, name) = make_known_tempdir(Path::new("/tmp"))?; let dir = tmp_dir.path().join("rocket"); fs::create_dir_all(&dir)?; let actual = ModuleRenderer::new("directory") .config(toml::toml! { [directory] truncation_length = 2 }) .path(dir) .collect(); let expected = Some(format!( "{} ", Color::Cyan.bold().paint(format!("{}/rocket", name)) )); assert_eq!(expected, actual); tmp_dir.close() } #[test] fn fish_directory_config_small() -> io::Result<()> { let (tmp_dir, _) = make_known_tempdir(Path::new("/tmp"))?; let dir = tmp_dir.path().join("thrusters/rocket"); fs::create_dir_all(&dir)?; let actual = ModuleRenderer::new("directory") .config(toml::toml! { [directory] truncation_length = 2 fish_style_pwd_dir_length = 1 }) .path(&dir) .collect(); let expected = Some(format!( "{} ", Color::Cyan.bold().paint(format!( "{}/thrusters/rocket", to_fish_style(1, dir.to_slash_lossy(), "/thrusters/rocket") )) )); assert_eq!(expected, actual); tmp_dir.close() } #[test] #[ignore] fn git_repo_root() -> io::Result<()> { let tmp_dir = TempDir::new()?; let repo_dir = tmp_dir.path().join("rocket-controls"); fs::create_dir(&repo_dir)?; init_repo(&repo_dir).unwrap(); let actual = ModuleRenderer::new("directory").path(repo_dir).collect(); let expected = Some(format!("{} ", Color::Cyan.bold().paint("rocket-controls"))); assert_eq!(expected, actual); tmp_dir.close() } #[test] #[ignore] fn directory_in_git_repo() -> io::Result<()> { let tmp_dir = TempDir::new()?; let repo_dir = tmp_dir.path().join("rocket-controls"); let dir = repo_dir.join("src"); fs::create_dir_all(&dir)?; init_repo(&repo_dir).unwrap(); let actual = ModuleRenderer::new("directory").path(dir).collect(); let expected = Some(format!( "{} ", Color::Cyan.bold().paint("rocket-controls/src") )); assert_eq!(expected, actual); tmp_dir.close() } #[test] #[ignore] fn truncated_directory_in_git_repo() -> io::Result<()> { let tmp_dir = TempDir::new()?; let repo_dir = tmp_dir.path().join("rocket-controls"); let dir = repo_dir.join("src/meters/fuel-gauge"); fs::create_dir_all(&dir)?; init_repo(&repo_dir).unwrap(); let actual = ModuleRenderer::new("directory").path(dir).collect(); let expected = Some(format!( "{} ", Color::Cyan.bold().paint("src/meters/fuel-gauge") )); assert_eq!(expected, actual); tmp_dir.close() } #[test] #[ignore] fn directory_in_git_repo_truncate_to_repo_false() -> io::Result<()> { let tmp_dir = TempDir::new()?; let repo_dir = tmp_dir.path().join("above-repo").join("rocket-controls"); let dir = repo_dir.join("src/meters/fuel-gauge"); fs::create_dir_all(&dir)?; init_repo(&repo_dir).unwrap(); let actual = ModuleRenderer::new("directory") .config(toml::toml! { [directory] // Don't truncate the path at all. truncation_length = 5 truncate_to_repo = false }) .path(dir) .collect(); let expected = Some(format!( "{} ", Color::Cyan .bold() .paint("above-repo/rocket-controls/src/meters/fuel-gauge") )); assert_eq!(expected, actual); tmp_dir.close() } #[test] #[ignore] fn fish_path_directory_in_git_repo_truncate_to_repo_false() -> io::Result<()> { let (tmp_dir, _) = make_known_tempdir(Path::new("/tmp"))?; let repo_dir = tmp_dir.path().join("above-repo").join("rocket-controls"); let dir = repo_dir.join("src/meters/fuel-gauge"); fs::create_dir_all(&dir)?; init_repo(&repo_dir).unwrap(); let actual = ModuleRenderer::new("directory") .config(toml::toml! { [directory] // Don't truncate the path at all. truncation_length = 5 truncate_to_repo = false fish_style_pwd_dir_length = 1 }) .path(dir) .collect(); let expected = Some(format!( "{} ", Color::Cyan.bold().paint(format!( "{}/above-repo/rocket-controls/src/meters/fuel-gauge", to_fish_style(1, tmp_dir.path().to_slash_lossy(), "") )) )); assert_eq!(expected, actual); tmp_dir.close() } #[test] #[ignore] fn fish_path_directory_in_git_repo_truncate_to_repo_true() -> io::Result<()> { let (tmp_dir, _) = make_known_tempdir(Path::new("/tmp"))?; let repo_dir = tmp_dir.path().join("above-repo").join("rocket-controls"); let dir = repo_dir.join("src/meters/fuel-gauge"); fs::create_dir_all(&dir)?; init_repo(&repo_dir).unwrap(); let actual = ModuleRenderer::new("directory") .config(toml::toml! { [directory] // `truncate_to_repo = true` should display the truncated path truncation_length = 5 truncate_to_repo = true fish_style_pwd_dir_length = 1 }) .path(dir) .collect(); let expected = Some(format!( "{} ", Color::Cyan.bold().paint(format!( "{}/rocket-controls/src/meters/fuel-gauge", to_fish_style(1, tmp_dir.path().join("above-repo").to_slash_lossy(), "") )) )); assert_eq!(expected, actual); tmp_dir.close() } #[test] #[ignore] fn directory_in_git_repo_truncate_to_repo_true() -> io::Result<()> { let (tmp_dir, _) = make_known_tempdir(Path::new("/tmp"))?; let repo_dir = tmp_dir.path().join("above-repo").join("rocket-controls"); let dir = repo_dir.join("src/meters/fuel-gauge"); fs::create_dir_all(&dir)?; init_repo(&repo_dir).unwrap(); let actual = ModuleRenderer::new("directory") .config(toml::toml! { [directory] // `truncate_to_repo = true` should display the truncated path truncation_length = 5 truncate_to_repo = true }) .path(dir) .collect(); let expected = Some(format!( "{} ", Color::Cyan .bold() .paint("rocket-controls/src/meters/fuel-gauge") )); assert_eq!(expected, actual); tmp_dir.close() } #[test] #[ignore] fn symlinked_git_repo_root() -> io::Result<()> { let (tmp_dir, _) = make_known_tempdir(Path::new("/tmp"))?; let repo_dir = tmp_dir.path().join("rocket-controls"); let symlink_dir = tmp_dir.path().join("rocket-controls-symlink"); fs::create_dir(&repo_dir)?; init_repo(&repo_dir).unwrap(); symlink(&repo_dir, &symlink_dir)?; let actual = ModuleRenderer::new("directory").path(symlink_dir).collect(); let expected = Some(format!( "{} ", Color::Cyan.bold().paint("rocket-controls-symlink") )); assert_eq!(expected, actual); tmp_dir.close() } #[test] #[ignore] fn directory_in_symlinked_git_repo() -> io::Result<()> { let (tmp_dir, _) = make_known_tempdir(Path::new("/tmp"))?; let repo_dir = tmp_dir.path().join("rocket-controls"); let src_dir = repo_dir.join("src"); let symlink_dir = tmp_dir.path().join("rocket-controls-symlink"); let symlink_src_dir = symlink_dir.join("src"); fs::create_dir_all(&src_dir)?; init_repo(&repo_dir).unwrap(); symlink(&repo_dir, &symlink_dir)?; let actual = ModuleRenderer::new("directory") .path(symlink_src_dir) .collect(); let expected = Some(format!( "{} ", Color::Cyan.bold().paint("rocket-controls-symlink/src") )); assert_eq!(expected, actual); tmp_dir.close() } #[test] #[ignore] fn truncated_directory_in_symlinked_git_repo() -> io::Result<()> { let (tmp_dir, _) = make_known_tempdir(Path::new("/tmp"))?; let repo_dir = tmp_dir.path().join("rocket-controls"); let src_dir = repo_dir.join("src/meters/fuel-gauge"); let symlink_dir = tmp_dir.path().join("rocket-controls-symlink"); let symlink_src_dir = symlink_dir.join("src/meters/fuel-gauge"); fs::create_dir_all(&src_dir)?; init_repo(&repo_dir).unwrap(); symlink(&repo_dir, &symlink_dir)?; let actual = ModuleRenderer::new("directory") .path(symlink_src_dir) .collect(); let expected = Some(format!( "{} ", Color::Cyan.bold().paint("src/meters/fuel-gauge") )); assert_eq!(expected, actual); tmp_dir.close() } #[test] #[ignore] fn directory_in_symlinked_git_repo_truncate_to_repo_false() -> io::Result<()> { let (tmp_dir, _) = make_known_tempdir(Path::new("/tmp"))?; let repo_dir = tmp_dir.path().join("above-repo").join("rocket-controls"); let src_dir = repo_dir.join("src/meters/fuel-gauge"); let symlink_dir = tmp_dir .path() .join("above-repo") .join("rocket-controls-symlink"); let symlink_src_dir = symlink_dir.join("src/meters/fuel-gauge"); fs::create_dir_all(&src_dir)?; init_repo(&repo_dir).unwrap(); symlink(&repo_dir, &symlink_dir)?; let actual = ModuleRenderer::new("directory") .config(toml::toml! { [directory] // Don't truncate the path at all. truncation_length = 5 truncate_to_repo = false }) .path(symlink_src_dir) .collect(); let expected = Some(format!( "{} ", Color::Cyan .bold() .paint("above-repo/rocket-controls-symlink/src/meters/fuel-gauge") )); assert_eq!(expected, actual); tmp_dir.close() } #[test] #[ignore] fn fish_path_directory_in_symlinked_git_repo_truncate_to_repo_false() -> io::Result<()> { let (tmp_dir, _) = make_known_tempdir(Path::new("/tmp"))?; let repo_dir = tmp_dir.path().join("above-repo").join("rocket-controls"); let src_dir = repo_dir.join("src/meters/fuel-gauge"); let symlink_dir = tmp_dir .path() .join("above-repo") .join("rocket-controls-symlink"); let symlink_src_dir = symlink_dir.join("src/meters/fuel-gauge"); fs::create_dir_all(&src_dir)?; init_repo(&repo_dir).unwrap(); symlink(&repo_dir, &symlink_dir)?; let actual = ModuleRenderer::new("directory") .config(toml::toml! { [directory] // Don't truncate the path at all. truncation_length = 5 truncate_to_repo = false fish_style_pwd_dir_length = 1 }) .path(symlink_src_dir) .collect(); let expected = Some(format!( "{} ", Color::Cyan.bold().paint(format!( "{}/above-repo/rocket-controls-symlink/src/meters/fuel-gauge", to_fish_style(1, tmp_dir.path().to_slash_lossy(), "") )) )); assert_eq!(expected, actual); tmp_dir.close() } #[test] #[ignore] fn fish_path_directory_in_symlinked_git_repo_truncate_to_repo_true() -> io::Result<()> { let (tmp_dir, _) = make_known_tempdir(Path::new("/tmp"))?; let repo_dir = tmp_dir.path().join("above-repo").join("rocket-controls"); let src_dir = repo_dir.join("src/meters/fuel-gauge"); let symlink_dir = tmp_dir .path() .join("above-repo") .join("rocket-controls-symlink"); let symlink_src_dir = symlink_dir.join("src/meters/fuel-gauge"); fs::create_dir_all(&src_dir)?; init_repo(&repo_dir).unwrap(); symlink(&repo_dir, &symlink_dir)?; let actual = ModuleRenderer::new("directory") .config(toml::toml! { [directory] // `truncate_to_repo = true` should display the truncated path truncation_length = 5 truncate_to_repo = true fish_style_pwd_dir_length = 1 }) .path(symlink_src_dir) .collect(); let expected = Some(format!( "{} ", Color::Cyan.bold().paint(format!( "{}/rocket-controls-symlink/src/meters/fuel-gauge", to_fish_style(1, tmp_dir.path().join("above-repo").to_slash_lossy(), "") )) )); assert_eq!(expected, actual); tmp_dir.close() } #[test] #[ignore] fn directory_in_symlinked_git_repo_truncate_to_repo_true() -> io::Result<()> { let (tmp_dir, _) = make_known_tempdir(Path::new("/tmp"))?; let repo_dir = tmp_dir.path().join("above-repo").join("rocket-controls"); let src_dir = repo_dir.join("src/meters/fuel-gauge"); let symlink_dir = tmp_dir .path() .join("above-repo") .join("rocket-controls-symlink"); let symlink_src_dir = symlink_dir.join("src/meters/fuel-gauge"); fs::create_dir_all(&src_dir)?; init_repo(&repo_dir).unwrap(); symlink(&repo_dir, &symlink_dir)?; let actual = ModuleRenderer::new("directory") .config(toml::toml! { [directory] // `truncate_to_repo = true` should display the truncated path truncation_length = 5 truncate_to_repo = true }) .path(symlink_src_dir) .collect(); let expected = Some(format!( "{} ", Color::Cyan .bold() .paint("rocket-controls-symlink/src/meters/fuel-gauge") )); assert_eq!(expected, actual); tmp_dir.close() } #[test] #[ignore] fn symlinked_directory_in_git_repo() -> io::Result<()> { let (tmp_dir, _) = make_known_tempdir(Path::new("/tmp"))?; let repo_dir = tmp_dir.path().join("rocket-controls"); let dir = repo_dir.join("src"); fs::create_dir_all(&dir)?; init_repo(&repo_dir).unwrap(); symlink(&dir, repo_dir.join("src/loop"))?; let actual = ModuleRenderer::new("directory") .config(toml::toml! { [directory] // `truncate_to_repo = true` should display the truncated path truncation_length = 5 truncate_to_repo = true }) .path(repo_dir.join("src/loop/loop")) .collect(); let expected = Some(format!( "{} ", Color::Cyan.bold().paint("rocket-controls/src/loop/loop") )); assert_eq!(expected, actual); tmp_dir.close() } #[test] fn truncation_symbol_truncated_root() -> io::Result<()> { let actual = ModuleRenderer::new("directory") .config(toml::toml! { [directory] truncation_length = 3 truncation_symbol = "…/" }) .path(Path::new("/a/four/element/path")) .collect(); let expected = Some(format!( "{} ", Color::Cyan.bold().paint("…/four/element/path") )); assert_eq!(expected, actual); Ok(()) } #[test] fn truncation_symbol_not_truncated_root() -> io::Result<()> { let actual = ModuleRenderer::new("directory") .config(toml::toml! { [directory] truncation_length = 4 truncation_symbol = "…/" }) .path(Path::new("/a/four/element/path")) .collect(); let expected = Some(format!( "{} ", Color::Cyan.bold().paint("/a/four/element/path") )); assert_eq!(expected, actual); Ok(()) } #[test] fn truncation_symbol_truncated_home() -> io::Result<()> { let (tmp_dir, name) = make_known_tempdir(home_dir().unwrap().as_path())?; let dir = tmp_dir.path().join("a/subpath"); fs::create_dir_all(&dir)?; let actual = ModuleRenderer::new("directory") .config(toml::toml! { [directory] truncation_length = 3 truncation_symbol = "…/" }) .path(dir) .collect(); let expected = Some(format!( "{} ", Color::Cyan.bold().paint(format!("…/{}/a/subpath", name)) )); assert_eq!(expected, actual); tmp_dir.close() } #[test] fn truncation_symbol_not_truncated_home() -> io::Result<()> { let (tmp_dir, name) = make_known_tempdir(home_dir().unwrap().as_path())?; let dir = tmp_dir.path().join("a/subpath"); fs::create_dir_all(&dir)?; let actual = ModuleRenderer::new("directory") .config(toml::toml! { [directory] truncate_to_repo = false // Necessary if homedir is a git repo truncation_length = 4 truncation_symbol = "…/" }) .path(dir) .collect(); let expected = Some(format!( "{} ", Color::Cyan.bold().paint(format!("~/{}/a/subpath", name)) )); assert_eq!(expected, actual); tmp_dir.close() } #[test] fn truncation_symbol_truncated_in_repo() -> io::Result<()> { let (tmp_dir, _) = make_known_tempdir(Path::new("/tmp"))?; let repo_dir = tmp_dir.path().join("above").join("repo"); let dir = repo_dir.join("src/sub/path"); fs::create_dir_all(&dir)?; init_repo(&repo_dir).unwrap(); let actual = ModuleRenderer::new("directory") .config(toml::toml! { [directory] truncation_length = 3 truncation_symbol = "…/" }) .path(dir) .collect(); let expected = Some(format!("{} ", Color::Cyan.bold().paint("…/src/sub/path"))); assert_eq!(expected, actual); tmp_dir.close() } #[test] fn truncation_symbol_not_truncated_in_repo() -> io::Result<()> { let (tmp_dir, _) = make_known_tempdir(Path::new("/tmp"))?; let repo_dir = tmp_dir.path().join("above").join("repo"); let dir = repo_dir.join("src/sub/path"); fs::create_dir_all(&dir)?; init_repo(&repo_dir).unwrap(); let actual = ModuleRenderer::new("directory") .config(toml::toml! { [directory] truncation_length = 5 truncation_symbol = "…/" truncate_to_repo = true }) .path(dir) .collect(); let expected = Some(format!( "{} ", Color::Cyan.bold().paint("…/repo/src/sub/path") )); assert_eq!(expected, actual); tmp_dir.close() } #[test] #[cfg(target_os = "windows")] fn truncation_symbol_windows_root_not_truncated() -> io::Result<()> { let dir = Path::new("C:\\temp"); let actual = ModuleRenderer::new("directory") .config(toml::toml! { [directory] truncation_length = 2 truncation_symbol = "…/" }) .path(dir) .collect(); let expected = Some(format!("{} ", Color::Cyan.bold().paint("C:/temp"))); assert_eq!(expected, actual); Ok(()) } #[test] #[cfg(target_os = "windows")] fn truncation_symbol_windows_root_truncated() -> io::Result<()> { let dir = Path::new("C:\\temp"); let actual = ModuleRenderer::new("directory") .config(toml::toml! { [directory] truncation_length = 1 truncation_symbol = "…/" }) .path(dir) .collect(); let expected = Some(format!("{} ", Color::Cyan.bold().paint("…/temp"))); assert_eq!(expected, actual); Ok(()) } #[test] #[cfg(target_os = "windows")] fn truncation_symbol_windows_root_truncated_backslash() -> io::Result<()> { let dir = Path::new("C:\\temp"); let actual = ModuleRenderer::new("directory") .config(toml::toml! { [directory] truncation_length = 1 truncation_symbol = r"…\" }) .path(dir) .collect(); let expected = Some(format!("{} ", Color::Cyan.bold().paint("…\\temp"))); assert_eq!(expected, actual); Ok(()) } }