use std::path::Path;
use super::super::render::{display_width, truncate_keep_end};
pub(super) fn compact_cwd(path: &Path) -> String {
let Some(home) = crate::paths::home_dir() else {
return path.display().to_string();
};
if let Ok(rest) = path.strip_prefix(home) {
let rel = rest.display().to_string();
if rel.is_empty() {
"~".to_string()
} else {
format!("~/{rel}")
}
} else {
path.display().to_string()
}
}
pub(super) fn format_cwd_left(path: &str, branch: Option<&str>) -> String {
match branch {
Some(branch) => format!("{path} ({branch})"),
None => path.to_string(),
}
}
pub(super) fn fit_cwd(path: &str, branch: Option<&str>, width: usize) -> String {
if width == 0 {
return String::new();
}
let full = format_cwd_left(path, branch);
if display_width(&full) <= width {
return full;
}
if let Some(branch) = branch {
let suffix = format!(" ({branch})");
let suffix_width = display_width(&suffix);
if suffix_width < width {
let path_budget = width - suffix_width;
if let Some(shortened) = shorten_path_keeping_basename(path, path_budget) {
return format!("{shortened}{suffix}");
}
}
}
shorten_path_display(path, width)
}
fn shorten_path_keeping_basename(path: &str, width: usize) -> Option<String> {
if display_width(path) <= width {
return Some(path.to_string());
}
let shortened = shorten_path_display(path, width);
retains_full_basename(path, &shortened).then_some(shortened)
}
fn retains_full_basename(path: &str, shortened: &str) -> bool {
let base = path_basename(path);
if base.is_empty() {
return true;
}
if shortened == base {
return true;
}
let Some(prefix) = shortened.strip_suffix(base) else {
return false;
};
prefix.ends_with('/') || prefix.ends_with('\\')
}
fn path_basename(path: &str) -> &str {
path.rsplit(['/', '\\'])
.find(|segment| !segment.is_empty())
.unwrap_or(path)
}
pub(super) fn shorten_path_display(path: &str, width: usize) -> String {
if width == 0 {
return String::new();
}
if display_width(path) <= width {
return path.to_string();
}
if width <= 1 {
return truncate_keep_end(path, width);
}
let sep = path_display_separator(path);
let (prefix, rest) = split_path_display_prefix(path, sep);
let segments: Vec<&str> = rest
.split(sep)
.filter(|segment| !segment.is_empty())
.collect();
if segments.len() <= 1 {
return truncate_keep_end(path, width);
}
let mut best: Option<String> = None;
let sep_str = sep.to_string();
for keep in 1..segments.len() {
let tail = segments[segments.len() - keep..].join(&sep_str);
let candidate = if prefix.is_empty() {
format!("…{sep}{tail}")
} else {
format!("{prefix}…{sep}{tail}")
};
if display_width(&candidate) <= width {
best = Some(candidate);
} else if best.is_some() {
break;
}
}
if let Some(candidate) = best {
return candidate;
}
let minimal = format!("…{sep}{}", segments[segments.len() - 1]);
if display_width(&minimal) <= width {
return minimal;
}
truncate_keep_end(&minimal, width)
}
fn path_display_separator(path: &str) -> char {
if let Some(rest) = path.strip_prefix("~/") {
if rest.contains('\\') && !rest.contains('/') {
return '\\';
}
return '/';
}
if path.contains('/') {
'/'
} else if path.contains('\\') {
'\\'
} else {
'/'
}
}
fn split_path_display_prefix(path: &str, sep: char) -> (&str, &str) {
if let Some(rest) = path.strip_prefix("~/") {
return ("~/", rest);
}
if let Some(rest) = path.strip_prefix(sep) {
return (&path[..sep.len_utf8()], rest);
}
("", path)
}
#[cfg(test)]
mod tests {
use super::{path_display_separator, shorten_path_display};
use pretty_assertions::assert_eq;
#[test]
fn home_relative_windows_paths_use_backslash_segments() {
assert_eq!(path_display_separator(r"~/work\company\api-gateway"), '\\');
assert_eq!(
shorten_path_display(r"~/work\company\services\api-gateway", 18),
r"~/…\api-gateway"
);
assert_eq!(
shorten_path_display(r"~/work\company\services\api-gateway", 24),
r"~/…\services\api-gateway"
);
}
#[test]
fn unix_home_relative_paths_keep_forward_slash() {
assert_eq!(path_display_separator("~/work/company/api-gateway"), '/');
assert_eq!(
shorten_path_display("~/work/company/services/api-gateway", 18),
"~/…/api-gateway"
);
}
}