use std::path::{Component, Path};
use path_slash::PathExt as _;
use unicode_width::UnicodeWidthChar;
use worktrunk::path::format_path_for_display;
use worktrunk::styling::visual_width;
use worktrunk::utils::epoch_now;
pub(crate) fn format_relative_time_short(timestamp: i64) -> String {
format_relative_time_impl(timestamp, epoch_now() as i64)
}
fn format_relative_time_impl(timestamp: i64, now: i64) -> String {
const MINUTE: i64 = 60;
const HOUR: i64 = MINUTE * 60;
const DAY: i64 = HOUR * 24;
const WEEK: i64 = DAY * 7;
const MONTH: i64 = DAY * 30;
const YEAR: i64 = DAY * 365;
let seconds_ago = now - timestamp;
if seconds_ago < 0 {
return "future".to_string();
}
if seconds_ago < MINUTE {
return "now".to_string();
}
const UNITS: &[(i64, &str)] = &[
(YEAR, "y"),
(MONTH, "mo"),
(WEEK, "w"),
(DAY, "d"),
(HOUR, "h"),
(MINUTE, "m"),
];
for &(unit_seconds, abbrev) in UNITS {
let value = seconds_ago / unit_seconds;
if value > 0 {
return format!("{}{}", value, abbrev);
}
}
"now".to_string()
}
pub(crate) fn shorten_path(path: &Path, main_worktree_path: &Path) -> String {
if path == main_worktree_path {
return ".".to_string();
}
if let Some(relative) = pathdiff::diff_paths(path, main_worktree_path) {
let rendered = relative.to_slash_lossy();
if relative.components().next() == Some(Component::ParentDir) {
rendered.into_owned()
} else {
format!("./{rendered}")
}
} else {
format_path_for_display(path)
}
}
pub(crate) fn truncate_to_width(text: &str, max_width: usize) -> String {
if visual_width(text) <= max_width {
return text.to_string();
}
let target_width = max_width.saturating_sub(1);
let mut current_width = 0;
let mut last_idx = 0;
for (idx, ch) in text.char_indices() {
let char_width = ch.width().unwrap_or(0);
if current_width + char_width > target_width {
break;
}
current_width += char_width;
last_idx = idx + ch.len_utf8();
}
let truncated = text[..last_idx].trim_end();
format!("{}…", truncated)
}
pub(crate) use worktrunk::styling::{terminal_width, truncate_visible};
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_truncate_normal_case() {
let text = "Fix bug with parsing and more text here";
let result = truncate_to_width(text, 25);
println!("Normal truncation: '{}'", result);
assert!(result.ends_with('…'));
}
#[test]
fn test_truncate_with_existing_ascii_ellipsis() {
let text = "Fix bug with parsing... more text here";
let result = truncate_to_width(text, 25);
println!("ASCII ellipsis: '{}'", result);
assert!(result.ends_with('…'));
}
#[test]
fn test_truncate_with_existing_unicode_ellipsis() {
let text = "Fix bug with parsing… more text here";
let result = truncate_to_width(text, 25);
println!("Unicode ellipsis: '{}'", result);
assert!(result.ends_with('…'));
}
#[test]
fn test_truncate_already_has_three_dots() {
let text = "Short text...";
let result = truncate_to_width(text, 20);
assert_eq!(result, "Short text...");
}
#[test]
fn test_truncate_exact_width() {
let text = "This is a very long message that needs truncation";
let result = truncate_to_width(text, 30);
assert!(result.ends_with('…'));
assert!(
!result.contains(" …"),
"Should not have space before ellipsis"
);
use unicode_width::UnicodeWidthStr;
assert_eq!(result.width(), 30);
}
#[test]
fn test_truncate_unicode_width() {
let text = "Fix bug with café ☕ and more text";
let result = truncate_to_width(text, 25);
use unicode_width::UnicodeWidthStr;
assert!(
result.width() <= 25,
"Width {} should be <= 25",
result.width()
);
}
#[test]
fn test_truncate_no_truncation_needed() {
let text = "Short message";
let result = truncate_to_width(text, 50);
assert_eq!(result, text);
}
#[test]
fn test_truncate_very_long_word() {
let text = "Supercalifragilisticexpialidocious extra text";
let result = truncate_to_width(text, 20);
use unicode_width::UnicodeWidthStr;
assert!(result.width() <= 20, "Width should be <= 20");
assert!(result.ends_with('…'));
}
#[test]
fn test_format_relative_time_short() {
let now: i64 = 1700000000;
assert_eq!(format_relative_time_impl(now - 30, now), "now");
assert_eq!(format_relative_time_impl(now - 59, now), "now");
assert_eq!(format_relative_time_impl(now - 60, now), "1m");
assert_eq!(format_relative_time_impl(now - 120, now), "2m");
assert_eq!(format_relative_time_impl(now - 3599, now), "59m");
assert_eq!(format_relative_time_impl(now - 3600, now), "1h");
assert_eq!(format_relative_time_impl(now - 7200, now), "2h");
assert_eq!(format_relative_time_impl(now - 86400, now), "1d");
assert_eq!(format_relative_time_impl(now - 172800, now), "2d");
assert_eq!(format_relative_time_impl(now - 604800, now), "1w");
assert_eq!(format_relative_time_impl(now - 2592000, now), "1mo");
assert_eq!(format_relative_time_impl(now - 31536000, now), "1y");
assert_eq!(format_relative_time_impl(now + 1000, now), "future");
}
#[test]
#[cfg(unix)] fn test_shorten_path() {
let main_worktree = PathBuf::from("/home/user/project");
assert_eq!(shorten_path(&main_worktree, &main_worktree), ".");
let child = PathBuf::from("/home/user/project/subdir");
assert_eq!(shorten_path(&child, &main_worktree), "./subdir");
let sibling = PathBuf::from("/home/user/project.feature");
assert_eq!(shorten_path(&sibling, &main_worktree), "../project.feature");
let cousin = PathBuf::from("/home/user/other-project");
assert_eq!(shorten_path(&cousin, &main_worktree), "../other-project");
let other = PathBuf::from("/var/log/syslog");
let result = shorten_path(&other, &main_worktree);
assert!(
result.starts_with("..") || result.starts_with("/"),
"Expected relative or absolute path for distant location, got: {}",
result
);
}
#[test]
#[cfg(windows)]
fn test_shorten_path_windows() {
let main_worktree = PathBuf::from(r"C:\Users\user\project");
assert_eq!(shorten_path(&main_worktree, &main_worktree), ".");
let child = PathBuf::from(r"C:\Users\user\project\subdir");
assert_eq!(shorten_path(&child, &main_worktree), "./subdir");
let sibling = PathBuf::from(r"C:\Users\user\project.feature");
assert_eq!(shorten_path(&sibling, &main_worktree), "../project.feature");
}
#[test]
fn test_format_relative_time_short_public() {
let result = format_relative_time_short(0);
assert!(
result.contains('y') || result == "future",
"Expected years format, got: {}",
result
);
}
#[test]
fn test_epoch_now() {
let now = epoch_now();
assert!(now > 1577836800, "epoch_now() should return current time");
}
#[test]
fn test_truncate_edge_cases() {
let result = truncate_to_width("", 10);
assert_eq!(result, "");
let result = truncate_to_width("X", 10);
assert_eq!(result, "X");
let result = truncate_to_width("12345", 5);
assert_eq!(result, "12345");
let result = truncate_to_width("123456", 5);
assert!(result.ends_with('…'));
}
}