mirror of
https://github.com/Llewellynvdm/starship.git
synced 2024-12-27 03:42:39 +00:00
feat(directory): add ellipsis to truncated paths (#1563)
Adds ellipsis in front of truncated paths: …/ Configurable through new config option: directory.truncation_symbol Fixes #1162, #1626
This commit is contained in:
parent
2693d125a8
commit
1673d565f4
@ -651,6 +651,7 @@ it would have been `nixpkgs/pkgs`.
|
|||||||
| `disabled` | `false` | Disables the `directory` module. |
|
| `disabled` | `false` | Disables the `directory` module. |
|
||||||
| `read_only` | `"🔒"` | The symbol indicating current directory is read only. |
|
| `read_only` | `"🔒"` | The symbol indicating current directory is read only. |
|
||||||
| `read_only_style` | `"red"` | The style for the read only symbol. |
|
| `read_only_style` | `"red"` | The style for the read only symbol. |
|
||||||
|
| `truncation_symbol` | `""` | The symbol to prefix to truncated paths. eg: "…/" |
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>This module has a few advanced configuration options that control how the directory is displayed.</summary>
|
<summary>This module has a few advanced configuration options that control how the directory is displayed.</summary>
|
||||||
@ -694,6 +695,7 @@ a single character. For `fish_style_pwd_dir_length = 2`, it would be `/bu/th/ci/
|
|||||||
|
|
||||||
[directory]
|
[directory]
|
||||||
truncation_length = 8
|
truncation_length = 8
|
||||||
|
truncation_symbol = "…/"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Docker Context
|
## Docker Context
|
||||||
|
@ -15,6 +15,7 @@ pub struct DirectoryConfig<'a> {
|
|||||||
pub disabled: bool,
|
pub disabled: bool,
|
||||||
pub read_only: &'a str,
|
pub read_only: &'a str,
|
||||||
pub read_only_style: &'a str,
|
pub read_only_style: &'a str,
|
||||||
|
pub truncation_symbol: &'a str,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> RootModuleConfig<'a> for DirectoryConfig<'a> {
|
impl<'a> RootModuleConfig<'a> for DirectoryConfig<'a> {
|
||||||
@ -30,6 +31,7 @@ impl<'a> RootModuleConfig<'a> for DirectoryConfig<'a> {
|
|||||||
disabled: false,
|
disabled: false,
|
||||||
read_only: "🔒",
|
read_only: "🔒",
|
||||||
read_only_style: "red",
|
read_only_style: "red",
|
||||||
|
truncation_symbol: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,8 @@ use crate::config::RootModuleConfig;
|
|||||||
use crate::configs::directory::DirectoryConfig;
|
use crate::configs::directory::DirectoryConfig;
|
||||||
use crate::formatter::StringFormatter;
|
use crate::formatter::StringFormatter;
|
||||||
|
|
||||||
|
const HOME_SYMBOL: &str = "~";
|
||||||
|
|
||||||
/// Creates a module with the current directory
|
/// Creates a module with the current directory
|
||||||
///
|
///
|
||||||
/// Will perform path contraction, substitution, and truncation.
|
/// Will perform path contraction, substitution, and truncation.
|
||||||
@ -31,41 +33,15 @@ use crate::formatter::StringFormatter;
|
|||||||
/// **Truncation**
|
/// **Truncation**
|
||||||
/// Paths will be limited in length to `3` path components by default.
|
/// Paths will be limited in length to `3` path components by default.
|
||||||
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||||
const HOME_SYMBOL: &str = "~";
|
|
||||||
|
|
||||||
let mut module = context.new_module("directory");
|
let mut module = context.new_module("directory");
|
||||||
let config: DirectoryConfig = DirectoryConfig::try_load(module.config);
|
let config: DirectoryConfig = DirectoryConfig::try_load(module.config);
|
||||||
|
|
||||||
// Using environment PWD is the standard approach for determining logical path
|
let current_dir = &get_current_dir(&context, &config);
|
||||||
// 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 home_dir = dirs_next::home_dir().unwrap();
|
let home_dir = dirs_next::home_dir().unwrap();
|
||||||
log::debug!("Current directory: {:?}", current_dir);
|
log::debug!("Current directory: {:?}", current_dir);
|
||||||
|
|
||||||
let repo = &context.get_repo().ok()?;
|
let repo = &context.get_repo().ok()?;
|
||||||
|
|
||||||
let dir_string = match &repo.root {
|
let dir_string = match &repo.root {
|
||||||
Some(repo_root) if config.truncate_to_repo && (repo_root != &home_dir) => {
|
Some(repo_root) if config.truncate_to_repo && (repo_root != &home_dir) => {
|
||||||
log::debug!("Repo root: {:?}", repo_root);
|
log::debug!("Repo root: {:?}", repo_root);
|
||||||
@ -83,9 +59,10 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
|||||||
// Truncate the dir string to the maximum number of path components
|
// Truncate the dir string to the maximum number of path components
|
||||||
let truncated_dir_string = truncate(substituted_dir, config.truncation_length as usize);
|
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
|
// Substitutions could have changed the prefix, so don't allow them and
|
||||||
// fish-style path contraction together
|
// fish-style path contraction together
|
||||||
let fish_prefix = if config.fish_style_pwd_dir_length > 0 && config.substitutions.is_empty() {
|
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
|
// 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);
|
let contracted_home_dir = contract_path(¤t_dir, &home_dir, HOME_SYMBOL);
|
||||||
to_fish_style(
|
to_fish_style(
|
||||||
@ -93,10 +70,14 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
|||||||
contracted_home_dir,
|
contracted_home_dir,
|
||||||
&truncated_dir_string,
|
&truncated_dir_string,
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
String::from(config.truncation_symbol)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
String::from("")
|
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 lock_symbol = String::from(config.read_only);
|
||||||
|
|
||||||
let parsed = StringFormatter::new(config.format).and_then(|formatter| {
|
let parsed = StringFormatter::new(config.format).and_then(|formatter| {
|
||||||
@ -107,7 +88,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
|||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.map(|variable| match variable {
|
.map(|variable| match variable {
|
||||||
"path" => Some(Ok(&final_dir_string)),
|
"path" => Some(Ok(&displayed_path)),
|
||||||
"read_only" => {
|
"read_only" => {
|
||||||
if is_readonly_dir(&context.current_dir) {
|
if is_readonly_dir(&context.current_dir) {
|
||||||
Some(Ok(&lock_symbol))
|
Some(Ok(&lock_symbol))
|
||||||
@ -131,6 +112,35 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
|||||||
Some(module)
|
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 {
|
fn is_readonly_dir(path: &Path) -> bool {
|
||||||
match directory_utils::is_write_allowed(path) {
|
match directory_utils::is_write_allowed(path) {
|
||||||
Ok(res) => !res,
|
Ok(res) => !res,
|
||||||
@ -1166,4 +1176,182 @@ mod tests {
|
|||||||
assert_eq!(expected, actual);
|
assert_eq!(expected, actual);
|
||||||
tmp_dir.close()
|
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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user