diff --git a/docs/config/README.md b/docs/config/README.md index adb97c85..a657e81a 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -651,6 +651,7 @@ it would have been `nixpkgs/pkgs`. | `disabled` | `false` | Disables the `directory` module. | | `read_only` | `"🔒"` | The symbol indicating current directory is read only. | | `read_only_style` | `"red"` | The style for the read only symbol. | +| `truncation_symbol` | `""` | The symbol to prefix to truncated paths. eg: "…/" |
This module has a few advanced configuration options that control how the directory is displayed. @@ -694,6 +695,7 @@ a single character. For `fish_style_pwd_dir_length = 2`, it would be `/bu/th/ci/ [directory] truncation_length = 8 +truncation_symbol = "…/" ``` ## Docker Context diff --git a/src/configs/directory.rs b/src/configs/directory.rs index 9adb0bc3..6df4516e 100644 --- a/src/configs/directory.rs +++ b/src/configs/directory.rs @@ -15,6 +15,7 @@ pub struct DirectoryConfig<'a> { pub disabled: bool, pub read_only: &'a str, pub read_only_style: &'a str, + pub truncation_symbol: &'a str, } impl<'a> RootModuleConfig<'a> for DirectoryConfig<'a> { @@ -30,6 +31,7 @@ impl<'a> RootModuleConfig<'a> for DirectoryConfig<'a> { disabled: false, read_only: "🔒", read_only_style: "red", + truncation_symbol: "", } } } diff --git a/src/modules/directory.rs b/src/modules/directory.rs index 783a61f2..ac923e7e 100644 --- a/src/modules/directory.rs +++ b/src/modules/directory.rs @@ -15,6 +15,8 @@ use crate::config::RootModuleConfig; use crate::configs::directory::DirectoryConfig; use crate::formatter::StringFormatter; +const HOME_SYMBOL: &str = "~"; + /// Creates a module with the current directory /// /// Will perform path contraction, substitution, and truncation. @@ -31,41 +33,15 @@ use crate::formatter::StringFormatter; /// **Truncation** /// Paths will be limited in length to `3` path components by default. pub fn module<'a>(context: &'a Context) -> Option> { - const HOME_SYMBOL: &str = "~"; - let mut module = context.new_module("directory"); let config: DirectoryConfig = DirectoryConfig::try_load(module.config); - // 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(x) => 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 - } - } - }; - let current_dir = Path::new( - physical_current_dir - .as_ref() - .unwrap_or_else(|| &context.current_dir), - ); + 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); @@ -83,20 +59,25 @@ pub fn module<'a>(context: &'a Context) -> Option> { // Truncate the dir string to the maximum number of path components let truncated_dir_string = truncate(substituted_dir, config.truncation_length as usize); - // Substitutions could have changed the prefix, so don't allow them and - // fish-style path contraction together - let fish_prefix = 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, - ) + 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 final_dir_string = format!("{}{}", fish_prefix, truncated_dir_string); + + let displayed_path = prefix + &truncated_dir_string; let lock_symbol = String::from(config.read_only); let parsed = StringFormatter::new(config.format).and_then(|formatter| { @@ -107,7 +88,7 @@ pub fn module<'a>(context: &'a Context) -> Option> { _ => None, }) .map(|variable| match variable { - "path" => Some(Ok(&final_dir_string)), + "path" => Some(Ok(&displayed_path)), "read_only" => { if is_readonly_dir(&context.current_dir) { Some(Ok(&lock_symbol)) @@ -131,6 +112,35 @@ pub fn module<'a>(context: &'a Context) -> Option> { 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(x) => 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, @@ -1166,4 +1176,182 @@ mod tests { 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(()) + } }