use std::borrow::Cow;
use std::ffi::OsStr;
use std::path::{Component, Path};
use ratatui::style::Style;
use ratatui::text::{Line, Span};
use unicode_width::UnicodeWidthStr;
#[derive(Clone, Copy)]
pub struct StyleConfig {
pub filename: Style,
pub path: Style,
pub dim: Style,
pub separator: Style,
}
impl Default for StyleConfig {
fn default() -> Self {
Self {
filename: Style::default().add_modifier(ratatui::style::Modifier::BOLD),
path: Style::default(),
dim: Style::default().add_modifier(ratatui::style::Modifier::DIM),
separator: Style::default().add_modifier(ratatui::style::Modifier::DIM),
}
}
}
pub fn format_path_line<'a>(path: &'a Path, max_width: usize) -> Line<'a> {
let spans = format_path_spans(path, max_width, StyleConfig::default());
Line::from(spans)
}
pub fn format_path_line_with_style<'a>(
path: &'a Path,
max_width: usize,
style: StyleConfig,
) -> Line<'a> {
let spans = format_path_spans(path, max_width, style);
Line::from(spans)
}
pub fn format_path_spans(path: &Path, max_width: usize, style: StyleConfig) -> Vec<Span<'static>> {
format_path_spans_with_min_tail(path, max_width, style, 1)
}
pub fn format_path_spans_with_min_tail(
path: &Path,
max_width: usize,
style: StyleConfig,
min_tail: usize,
) -> Vec<Span<'static>> {
let (filename, dirs) = split_path(path);
let filename_str = os_to_str(filename).into_owned();
let file_w = width(&filename_str);
if dirs.is_empty() {
return vec![Span::styled(filename_str, style.filename)];
}
let sep = " — ";
let sep_w = width(sep);
if file_w + sep_w >= max_width {
return vec![Span::styled(filename_str, style.filename)];
}
let mut remaining = max_width - file_w - sep_w;
let mut spans: Vec<Span<'static>> = Vec::with_capacity(8);
spans.push(Span::styled(filename_str.clone(), style.filename));
spans.push(Span::styled(sep.to_string(), style.separator));
if write_full_owned(&mut spans, &dirs, &mut remaining, &style) {
return spans;
}
if dirs.len() == 1 {
let s = os_to_str(dirs[0]).into_owned();
for level in 1..=3 {
let comp = compress_segment(&s, level);
if width(&comp) <= remaining {
spans.push(Span::styled(comp, style.path));
return spans;
}
}
}
for level in 1..=3 {
if write_compressed_elided_owned(&mut spans, &dirs, &mut remaining, level, &style, min_tail) {
return spans;
}
}
if write_middle_elided_owned(&mut spans, &dirs, &mut remaining, &style, min_tail) {
return spans;
}
if let Some(last) = dirs.last()
&& write_minimal_owned(&mut spans, last, &mut remaining, &style) {
return spans;
}
vec![Span::styled(filename_str, style.filename)]
}
fn write_full_owned(
spans: &mut Vec<Span<'static>>,
dirs: &[&OsStr],
remaining: &mut usize,
style: &StyleConfig,
) -> bool {
let mut total_needed: usize = 0;
for (i, d) in dirs.iter().enumerate() {
if i > 0 {
total_needed += 1; }
let s = os_to_str(d);
total_needed += width(&s);
}
if total_needed > *remaining {
return false;
}
write_dirs_owned(spans, dirs, remaining, style)
}
fn write_dirs_owned(
spans: &mut Vec<Span<'static>>,
dirs: &[&OsStr],
remaining: &mut usize,
style: &StyleConfig,
) -> bool {
let start = spans.len();
for (i, d) in dirs.iter().enumerate() {
if i > 0 {
if *remaining < 1 {
spans.truncate(start);
return false;
}
spans.push(Span::styled("/".to_string(), style.dim));
*remaining -= 1;
}
let s = os_to_str(d);
let w = width(&s);
if w > *remaining {
spans.truncate(start);
return false;
}
spans.push(Span::styled(s.into_owned(), style.path));
*remaining -= w;
}
true
}
fn write_middle_elided_owned(
spans: &mut Vec<Span<'static>>,
dirs: &[&OsStr],
remaining: &mut usize,
style: &StyleConfig,
min_tail: usize,
) -> bool {
if dirs.len() < 2 {
return write_full_owned(spans, dirs, remaining, style);
}
let start = spans.len();
let initial_rem = *remaining;
let first = os_to_str(dirs[0]).into_owned();
let fw = width(&first);
if fw <= *remaining {
spans.push(Span::styled(first.clone(), style.path));
*remaining -= fw;
let elide_w = width("…/");
for tail_len in (1..dirs.len()).rev() {
if tail_len < min_tail {
continue;
}
let checkpoint_len = spans.len();
let checkpoint_rem = *remaining;
if *remaining < elide_w {
spans.truncate(start);
*remaining = initial_rem;
break;
}
spans.push(Span::styled("…/".to_string(), style.dim));
*remaining -= elide_w;
let tail = &dirs[dirs.len() - tail_len..];
let mut ok = true;
for (i, d) in tail.iter().enumerate() {
if i > 0 {
if *remaining < 1 {
ok = false;
break;
}
spans.push(Span::styled("/".to_string(), style.dim));
*remaining -= 1;
}
let s = os_to_str(d).into_owned();
let w = width(&s);
if w > *remaining {
ok = false;
break;
}
spans.push(Span::styled(s, style.path));
*remaining -= w;
}
if ok {
return true;
}
spans.truncate(checkpoint_len);
*remaining = checkpoint_rem;
}
spans.truncate(start);
*remaining = initial_rem;
}
let elide_w = width("…/");
for tail_len in (1..dirs.len()).rev() {
if tail_len < min_tail {
continue;
}
let checkpoint_len = spans.len();
let checkpoint_rem = *remaining;
if *remaining < elide_w {
spans.truncate(start);
*remaining = initial_rem;
return false;
}
spans.push(Span::styled("…/".to_string(), style.dim));
*remaining -= elide_w;
let tail = &dirs[dirs.len() - tail_len..];
let mut ok = true;
for (i, d) in tail.iter().enumerate() {
if i > 0 {
if *remaining < 1 {
ok = false;
break;
}
spans.push(Span::styled("/".to_string(), style.dim));
*remaining -= 1;
}
let s = os_to_str(d).into_owned();
let w = width(&s);
if w > *remaining {
ok = false;
break;
}
spans.push(Span::styled(s, style.path));
*remaining -= w;
}
if ok {
return true;
}
spans.truncate(checkpoint_len);
*remaining = checkpoint_rem;
}
spans.truncate(start);
*remaining = initial_rem;
false
}
fn write_compressed_elided_owned(
spans: &mut Vec<Span<'static>>,
dirs: &[&OsStr],
remaining: &mut usize,
level: usize,
style: &StyleConfig,
min_tail: usize,
) -> bool {
let mut compressed: Vec<String> = Vec::with_capacity(dirs.len());
for d in dirs {
let s = os_to_str(d).into_owned();
compressed.push(compress_segment(&s, level));
}
write_middle_elided_strs_owned(spans, &compressed, remaining, style, min_tail)
}
fn write_middle_elided_strs_owned(
spans: &mut Vec<Span<'static>>,
dirs: &[String],
remaining: &mut usize,
style: &StyleConfig,
min_tail: usize,
) -> bool {
if dirs.len() < 2 {
return false;
}
let start = spans.len();
let initial_rem = *remaining;
let first = &dirs[0];
let fw = width(first);
if fw <= *remaining {
spans.push(Span::styled(first.clone(), style.path));
*remaining -= fw;
let elide_w = width("…/");
for tail_len in (1..dirs.len()).rev() {
if tail_len < min_tail {
continue;
}
let checkpoint_len = spans.len();
let checkpoint_rem = *remaining;
if *remaining < elide_w {
spans.truncate(start);
*remaining = initial_rem;
break;
}
spans.push(Span::styled("…/".to_string(), style.dim));
*remaining -= elide_w;
let tail = &dirs[dirs.len() - tail_len..];
let mut ok = true;
for (i, d) in tail.iter().enumerate() {
if i > 0 {
if *remaining < 1 {
ok = false;
break;
}
spans.push(Span::styled("/".to_string(), style.dim));
*remaining -= 1;
}
let w = width(d);
if w > *remaining {
ok = false;
break;
}
spans.push(Span::styled(d.clone(), style.path));
*remaining -= w;
}
if ok {
return true;
}
spans.truncate(checkpoint_len);
*remaining = checkpoint_rem;
}
spans.truncate(start);
*remaining = initial_rem;
}
let elide_w = width("…/");
for tail_len in (1..dirs.len()).rev() {
if tail_len < min_tail {
continue;
}
let checkpoint_len = spans.len();
let checkpoint_rem = *remaining;
if *remaining < elide_w {
spans.truncate(start);
*remaining = initial_rem;
return false;
}
spans.push(Span::styled("…/".to_string(), style.dim));
*remaining -= elide_w;
let tail = &dirs[dirs.len() - tail_len..];
let mut ok = true;
for (i, d) in tail.iter().enumerate() {
if i > 0 {
if *remaining < 1 {
ok = false;
break;
}
spans.push(Span::styled("/".to_string(), style.dim));
*remaining -= 1;
}
let w = width(d);
if w > *remaining {
ok = false;
break;
}
spans.push(Span::styled(d.clone(), style.path));
*remaining -= w;
}
if ok {
return true;
}
spans.truncate(checkpoint_len);
*remaining = checkpoint_rem;
}
spans.truncate(start);
*remaining = initial_rem;
false
}
fn write_minimal_owned(
spans: &mut Vec<Span<'static>>,
last: &OsStr,
remaining: &mut usize,
style: &StyleConfig,
) -> bool {
let s = os_to_str(last).into_owned();
if width("…/") + width(&s) > *remaining {
for level in 1..=3 {
let comp = compress_segment(&s, level);
if width(&comp) <= *remaining {
spans.push(Span::styled("…/".to_string(), style.dim));
spans.push(Span::styled(comp, style.path));
return true;
}
}
return false;
}
spans.push(Span::styled("…/".to_string(), style.dim));
spans.push(Span::styled(s, style.path));
true
}
fn split_path(path: &Path) -> (&OsStr, Vec<&OsStr>) {
let filename = path.file_name().unwrap_or_else(|| "".as_ref());
let mut dirs = Vec::new();
if let Some(parent) = path.parent() {
for comp in parent.components() {
if let Component::Normal(os) = comp {
dirs.push(os);
}
}
}
(filename, dirs)
}
fn os_to_str(os: &OsStr) -> Cow<'_, str> {
os.to_string_lossy()
}
pub fn width(s: &str) -> usize {
UnicodeWidthStr::width(s)
}
fn compress_segment(seg: &str, level: usize) -> String {
match level {
1 => truncate(seg, 8),
2 => truncate(seg, 5),
_ => initialism(seg),
}
}
fn truncate(seg: &str, max_len: usize) -> String {
if seg.chars().count() <= max_len {
return seg.to_string();
}
let mut out = String::with_capacity(max_len);
for (i, ch) in seg.chars().enumerate() {
if i >= max_len - 1 {
break;
}
out.push(ch);
}
out.push('…');
out
}
fn initialism(seg: &str) -> String {
let mut out = String::new();
for part in seg.split(['_', '-', '.']) {
if let Some(c) = part.chars().next() {
out.push(c);
}
}
if out.is_empty() {
seg.chars().next().unwrap_or('?').to_string()
} else {
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
use ratatui::style::Style;
fn default_cfg() -> StyleConfig {
StyleConfig {
filename: Style::default(),
path: Style::default(),
dim: Style::default(),
separator: Style::default(),
}
}
fn line_to_string(line: &ratatui::text::Line) -> String {
line.spans.iter().map(|span| span.content.clone()).collect()
}
#[test]
fn test_root_file() {
let path = Path::new("main.rs");
let line = format_path_line(path, 40);
assert_eq!(line_to_string(&line), "main.rs");
}
#[test]
fn test_root_file_with_label() {
let path = Path::new("Cargo.toml");
let cfg = default_cfg();
let mut spans = format_path_spans(path, 20, cfg);
spans.push(ratatui::text::Span::raw(" (unstaged)"));
let s: String = spans.iter().map(|sp| sp.content.clone()).collect();
assert_eq!(s, "Cargo.toml (unstaged)");
}
#[test]
fn test_single_directory_short() {
let path = Path::new("src/main.rs");
let line = format_path_line(path, 40);
let s = line_to_string(&line);
assert!(s.starts_with("main.rs"));
assert!(s.contains("src"));
}
#[test]
fn test_single_long_directory_elision() {
let path = Path::new("very_long_directory_name/main.rs");
let line = format_path_line(path, 15);
let s = line_to_string(&line);
assert!(s.starts_with("main.rs"));
assert!(s.contains("…") || s.contains("vldn") || s.contains("very_lo…"));
assert!(width(&s) <= 15);
}
#[test]
fn test_multiple_directories_wide() {
let path = Path::new("src/compiler/parser/ast/main.rs");
let line = format_path_line(path, 80);
let s = line_to_string(&line);
assert!(s.starts_with("main.rs"));
assert!(s.contains("src/compiler/parser/ast"));
}
#[test]
fn test_multiple_directories_medium() {
let path = Path::new("src/compiler/parser/ast/main.rs");
let line = format_path_line(path, 30);
let s = line_to_string(&line);
assert!(s.starts_with("main.rs"));
assert!(s.contains("…"));
}
#[test]
fn test_multiple_directories_narrow() {
let path = Path::new("src/compiler/parser/ast/main.rs");
let line = format_path_line(path, 15);
let s = line_to_string(&line);
assert!(s.starts_with("main.rs"));
assert!(s.contains("…"));
}
#[test]
fn test_smart_directory_compression_multiple_dirs() {
let path = Path::new("src/compiler/very_long_directory_name/parser/ast/main.rs");
let line = format_path_line(path, 30);
let s = line_to_string(&line);
assert!(s.starts_with("main.rs"));
assert!(s.contains("…") || s.contains("vldn") || s.contains("very_lo…"));
assert!(width(&s) <= 30);
}
#[test]
fn test_filename_only_when_too_narrow() {
let path = Path::new("src/compiler/parser/ast/main.rs");
let line = format_path_line(path, 5);
let s = line_to_string(&line);
assert_eq!(s, "main.rs");
}
#[test]
fn test_unicode_filename() {
let path = Path::new("src/解析/メイン.rs");
let line = format_path_line(path, 40);
let s = line_to_string(&line);
assert!(s.contains("メイン.rs"));
assert!(s.contains("解析") || s.contains("…"));
}
#[test]
fn test_path_with_label_simulation() {
let path = Path::new("src/compiler/parser/main.rs");
let cfg = default_cfg();
let mut spans = format_path_spans(path, 20, cfg);
spans.push(ratatui::text::Span::raw(" (staged)"));
let s: String = spans.iter().map(|sp| sp.content.clone()).collect();
assert!(s.starts_with("main.rs"));
assert!(s.contains("…") || s.contains("parser"));
assert!(s.contains("(staged)"));
}
}