const FIXED_COLUMN_WIDTH: u16 = 30;
const MIN_BRANCH_WIDTH: usize = 15;
const MIN_PATH_WIDTH: usize = 20;
const BRANCH_RATIO: f64 = 0.4;
const PATH_RATIO: f64 = 0.6;
const MAX_BRANCH_RATIO: f64 = 0.51;
const MAX_PATH_RATIO: f64 = 0.72;
#[derive(Debug, Clone, Copy)]
pub struct ColumnWidths {
pub branch: usize,
pub path: usize,
}
pub fn calculate_column_widths(items: &[(String, String)], terminal_width: u16) -> ColumnWidths {
let available = terminal_width.saturating_sub(FIXED_COLUMN_WIDTH) as usize;
let max_branch_len = items
.iter()
.map(|(b, _)| b.chars().count())
.max()
.unwrap_or(0)
.max("BRANCH".len());
let max_path_len = items
.iter()
.map(|(_, p)| p.chars().count())
.max()
.unwrap_or(0)
.max("DIR_PATH".len());
let mut branch_width = (available as f64 * BRANCH_RATIO) as usize;
let mut path_width = (available as f64 * PATH_RATIO) as usize;
branch_width = branch_width.max(MIN_BRANCH_WIDTH);
path_width = path_width.max(MIN_PATH_WIDTH);
let branch_cap = ((available as f64 * MAX_BRANCH_RATIO) as usize).max(MIN_BRANCH_WIDTH);
let path_cap = ((available as f64 * MAX_PATH_RATIO) as usize).max(MIN_PATH_WIDTH);
if max_branch_len > branch_width {
let desired = max_branch_len.min(branch_cap);
let need = desired.saturating_sub(branch_width);
let can_take = path_width.saturating_sub(MIN_PATH_WIDTH);
let take = need.min(can_take);
branch_width += take;
path_width -= take;
}
if max_path_len > path_width {
let desired = max_path_len.min(path_cap);
let need = desired.saturating_sub(path_width);
let can_take = branch_width.saturating_sub(MIN_BRANCH_WIDTH);
let take = need.min(can_take);
path_width += take;
branch_width -= take;
}
branch_width = branch_width.min(branch_cap);
path_width = path_width.min(path_cap);
ColumnWidths {
branch: branch_width,
path: path_width,
}
}
pub fn truncate_start(text: &str, width: usize) -> String {
let chars: Vec<char> = text.chars().collect();
if chars.len() <= width {
format!("{:<width$}", text, width = width)
} else {
let ellipsis = "…";
let remaining = width.saturating_sub(1); if remaining == 0 {
ellipsis.to_string()
} else {
let start = chars.len().saturating_sub(remaining);
format!("{}{}", ellipsis, chars[start..].iter().collect::<String>())
}
}
}
pub fn pad_text(text: &str, width: usize) -> String {
format!("{:<width$}", text, width = width)
}
pub fn truncate_and_pad(text: &str, width: usize) -> String {
let chars: Vec<char> = text.chars().collect();
if chars.len() <= width {
format!("{:<width$}", text, width = width)
} else {
let ellipsis = "...";
let remaining = width.saturating_sub(3); if remaining == 0 {
".".repeat(width)
} else {
let truncated: String = chars[..remaining].iter().collect();
format!("{}{}", truncated, ellipsis)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_truncate_start_short() {
assert_eq!(truncate_start("short", 10), "short ");
}
#[test]
fn test_truncate_start_exact() {
assert_eq!(truncate_start("exactly10!", 10), "exactly10!");
}
#[test]
fn test_truncate_start_long() {
assert_eq!(truncate_start("very-long-text", 10), "…long-text");
}
#[test]
fn test_truncate_start_unicode() {
let result = truncate_start("abcdefghijk", 5);
assert!(result.starts_with('…'));
assert_eq!(result.chars().count(), 5);
}
#[test]
fn test_truncate_start_width_one() {
assert_eq!(truncate_start("abc", 1), "…");
}
#[test]
fn test_pad_text() {
assert_eq!(pad_text("test", 10), "test ");
}
#[test]
fn test_calculate_column_widths_normal() {
let items = vec![
("main".to_string(), "/path/to/main".to_string()),
("feature/test".to_string(), "/path/to/feature".to_string()),
];
let widths = calculate_column_widths(&items, 120);
assert!(widths.branch >= 15);
assert!(widths.path >= 20);
}
#[test]
fn test_calculate_column_widths_narrow() {
let items = vec![("main".to_string(), "/path".to_string())];
let widths = calculate_column_widths(&items, 60);
assert!(widths.branch >= 15);
assert!(widths.path >= 20);
}
#[test]
fn test_calculate_column_widths_very_narrow() {
let items = vec![];
let widths = calculate_column_widths(&items, 30);
assert_eq!(widths.branch, 15);
assert_eq!(widths.path, 20);
}
#[test]
fn test_truncate_and_pad_short() {
assert_eq!(truncate_and_pad("short", 10), "short ");
}
#[test]
fn test_truncate_and_pad_exact() {
assert_eq!(truncate_and_pad("exactly10!", 10), "exactly10!");
}
#[test]
fn test_truncate_and_pad_long() {
assert_eq!(truncate_and_pad("very-long-text", 10), "very-lo...");
}
#[test]
fn test_truncate_and_pad_branch_name() {
assert_eq!(
truncate_and_pad("feature/very-long-branch-name", 20),
"feature/very-long..."
);
}
#[test]
fn test_truncate_and_pad_width_three() {
assert_eq!(truncate_and_pad("abcdef", 3), "...");
}
#[test]
fn test_truncate_and_pad_width_two() {
assert_eq!(truncate_and_pad("abcdef", 2), "..");
}
#[test]
fn test_calculate_column_widths_borrow_from_path() {
let items = vec![(
"feature/very-long-branch-name-here".to_string(),
"/short".to_string(),
)];
let widths = calculate_column_widths(&items, 120);
assert!(
widths.branch >= 34,
"branch width should be >= 34, got {}",
widths.branch
);
}
#[test]
fn test_calculate_column_widths_borrow_from_branch() {
let items = vec![(
"main".to_string(),
"/very/long/path/to/some/deeply/nested/worktree/directory".to_string(),
)];
let widths = calculate_column_widths(&items, 120);
assert!(widths.path > 54); }
}