use leviath_core::truncate_at_boundary;
pub(super) fn relative_time(ts: i64) -> String {
if ts == 0 {
return "-".to_string();
}
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let secs = (now - ts).max(0) as u64;
if secs < 10 {
"just now".to_string()
} else if secs < 60 {
format!("{}s ago", secs)
} else if secs < 3600 {
let m = secs / 60;
format!("{}m ago", m)
} else if secs < 86400 {
let h = secs / 3600;
let m = (secs % 3600) / 60;
if m == 0 {
format!("{}h ago", h)
} else {
format!("{}h{}m ago", h, m)
}
} else {
let d = secs / 86400;
format!("{}d ago", d)
}
}
pub(super) fn truncate(s: &str, max: usize) -> String {
let s = s.trim();
if s.len() <= max {
s.to_string()
} else {
format!("{}…", truncate_at_boundary(s, max))
}
}
pub(super) fn format_tokens(n: usize) -> String {
if n >= 1_000_000 {
format!("{}M", n / 1_000_000)
} else if n >= 1_000 {
format!("{}k", n / 1_000)
} else {
n.to_string()
}
}
pub(super) fn elapsed_str(started_at: i64) -> String {
if started_at == 0 {
return "-".to_string();
}
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let secs = (now - started_at).max(0) as u64;
if secs < 60 {
format!("{}s", secs)
} else if secs < 3600 {
format!("{}m{}s", secs / 60, secs % 60)
} else {
format!("{}h{}m", secs / 3600, (secs % 3600) / 60)
}
}
pub(super) fn elapsed_str_until(started_at: i64, until: i64) -> String {
if started_at == 0 {
return "-".to_string();
}
let secs = (until - started_at).max(0) as u64;
if secs < 60 {
format!("{}s", secs)
} else if secs < 3600 {
format!("{}m{}s", secs / 60, secs % 60)
} else {
format!("{}h{}m", secs / 3600, (secs % 3600) / 60)
}
}
const NATIVE_CLIPBOARD_CMDS: &[(&str, &[&str])] = &[
("pbcopy", &[]),
("xclip", &["-selection", "clipboard"]),
("wl-copy", &[]),
];
pub fn yank_to_clipboard_via(text: &str, osc52_fallback: fn(&str) -> bool) -> bool {
yank_to_clipboard_with(text, NATIVE_CLIPBOARD_CMDS, osc52_fallback)
}
fn yank_to_clipboard_with(
text: &str,
clipboard_cmds: &[(&str, &[&str])],
osc52_fallback: fn(&str) -> bool,
) -> bool {
use std::io::Write as IoWrite;
use std::process::Stdio;
for (cmd, args) in clipboard_cmds {
let mut command = leviath_sys::child_command(cmd);
command
.args(*args)
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null());
if let Ok(mut child) = command.spawn() {
let _ = child
.stdin
.as_mut()
.expect("child spawned with Stdio::piped() stdin")
.write_all(text.as_bytes());
if child.wait().map(|s| s.success()).unwrap_or(false) {
return true;
}
}
}
osc52_fallback(text)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_truncate_short() {
assert_eq!(truncate("hello", 10), "hello");
}
#[test]
fn test_truncate_exact() {
assert_eq!(truncate("hello", 5), "hello");
}
#[test]
fn test_truncate_long() {
let result = truncate("hello world", 5);
assert_eq!(result, "hello…");
}
#[test]
fn test_truncate_trims_whitespace() {
assert_eq!(truncate(" hi ", 10), "hi");
}
#[test]
fn test_truncate_empty() {
assert_eq!(truncate("", 5), "");
}
#[test]
fn test_format_tokens_small() {
assert_eq!(format_tokens(0), "0");
assert_eq!(format_tokens(42), "42");
assert_eq!(format_tokens(999), "999");
}
#[test]
fn test_format_tokens_thousands() {
assert_eq!(format_tokens(1000), "1k");
assert_eq!(format_tokens(1500), "1k");
assert_eq!(format_tokens(21000), "21k");
assert_eq!(format_tokens(999_999), "999k");
}
#[test]
fn test_format_tokens_millions() {
assert_eq!(format_tokens(1_000_000), "1M");
assert_eq!(format_tokens(2_500_000), "2M");
}
#[test]
fn test_relative_time_zero() {
assert_eq!(relative_time(0), "-");
}
#[test]
fn test_elapsed_str_zero() {
assert_eq!(elapsed_str(0), "-");
}
#[test]
fn test_elapsed_str_until_zero() {
assert_eq!(elapsed_str_until(0, 100), "-");
}
#[test]
fn test_elapsed_str_until_seconds() {
assert_eq!(elapsed_str_until(100, 145), "45s");
}
#[test]
fn test_elapsed_str_until_minutes() {
assert_eq!(elapsed_str_until(100, 225), "2m5s");
}
#[test]
fn test_elapsed_str_until_hours() {
assert_eq!(elapsed_str_until(100, 7400), "2h1m");
}
#[test]
fn test_elapsed_str_until_negative_clamped() {
assert_eq!(elapsed_str_until(200, 100), "0s");
}
#[test]
fn test_format_tokens_boundary_999() {
assert_eq!(format_tokens(999), "999");
}
#[test]
fn test_format_tokens_boundary_1000() {
assert_eq!(format_tokens(1000), "1k");
}
#[test]
fn test_format_tokens_boundary_999999() {
assert_eq!(format_tokens(999_999), "999k");
}
#[test]
fn test_format_tokens_boundary_1000000() {
assert_eq!(format_tokens(1_000_000), "1M");
}
#[test]
fn test_truncate_unicode() {
let result = truncate("abcdef", 3);
assert_eq!(result, "abc…");
}
#[test]
fn test_truncate_multibyte_char_boundary() {
let s = "Research the history – covering all topics";
let result = truncate(s, 22);
assert!(!result.is_empty());
assert!(result.ends_with('…'));
assert_eq!(result, "Research the history …");
let result2 = truncate(s, 21);
assert_eq!(result2, "Research the history …");
let result3 = truncate(s, 24);
assert_eq!(result3, "Research the history –…");
}
#[test]
fn test_truncate_emoji() {
let s = "Hello 🔥 world";
let result = truncate(s, 7); assert!(!result.is_empty());
assert!(result.ends_with('…'));
assert_eq!(result, "Hello …");
}
#[test]
fn test_elapsed_str_until_exact_minute() {
assert_eq!(elapsed_str_until(1000, 1060), "1m0s");
}
#[test]
fn test_elapsed_str_until_exact_hour() {
assert_eq!(elapsed_str_until(1000, 4600), "1h0m");
}
#[test]
fn test_elapsed_str_until_same_time() {
assert_eq!(elapsed_str_until(100, 100), "0s");
}
#[test]
fn test_elapsed_str_until_zero_start_returns_dash() {
assert_eq!(elapsed_str_until(0, 60), "-");
}
#[test]
fn test_relative_time_just_now() {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let result = relative_time(now - 3);
assert_eq!(result, "just now");
}
#[test]
fn test_relative_time_seconds_ago() {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let result = relative_time(now - 30);
assert!(result.ends_with("s ago"));
}
#[test]
fn test_relative_time_minutes_ago() {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let result = relative_time(now - 300);
assert!(result.ends_with("m ago"));
}
#[test]
fn test_relative_time_hours_ago_no_minutes() {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let result = relative_time(now - 7200);
assert_eq!(result, "2h ago");
}
#[test]
fn test_relative_time_hours_ago_with_minutes() {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let result = relative_time(now - 5400);
assert_eq!(result, "1h30m ago");
}
#[test]
fn test_relative_time_days_ago() {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let result = relative_time(now - 3 * 86400);
assert_eq!(result, "3d ago");
}
#[test]
fn test_elapsed_str_seconds() {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let result = elapsed_str(now - 45);
assert!(result.ends_with('s'));
}
#[test]
fn test_elapsed_str_minutes() {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let result = elapsed_str(now - 195);
assert!(result.contains('m'));
}
#[test]
fn test_elapsed_str_hours() {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let result = elapsed_str(now - 7800);
assert!(result.contains('h'));
}
#[test]
fn test_yank_to_clipboard_empty() {
fn fake_osc52_fallback(_text: &str) -> bool {
true
}
let result = temp_env::with_var("PATH", Some("/lev-definitely-empty-path-dir"), || {
yank_to_clipboard_via("", fake_osc52_fallback)
});
assert!(result);
}
fn unreachable_osc52_fallback(_text: &str) -> bool {
panic!("OSC52 fallback must not run when the fake pbcopy succeeds");
}
#[test]
#[should_panic(expected = "OSC52 fallback must not run when the fake pbcopy succeeds")]
fn test_unreachable_osc52_fallback_panics_if_ever_invoked() {
unreachable_osc52_fallback("anything");
}
#[cfg(not(windows))]
fn exit_cmd(success: bool) -> (&'static str, &'static [&'static str]) {
if success {
("true", &[])
} else {
("false", &[])
}
}
#[cfg(windows)]
fn exit_cmd(success: bool) -> (&'static str, &'static [&'static str]) {
if success {
("cmd", &["/C", "exit 0"])
} else {
("cmd", &["/C", "exit 1"])
}
}
#[test]
fn test_yank_to_clipboard_native_tool_success_returns_true_without_fallback() {
let (cmd, args) = exit_cmd(true);
let result = temp_env::with_vars_unset(Vec::<&str>::new(), || {
yank_to_clipboard_with(
"native tool success test",
&[(cmd, args)],
unreachable_osc52_fallback,
)
});
assert!(result);
}
#[test]
fn test_yank_to_clipboard_native_tool_nonzero_exit_falls_through_to_fallback() {
fn fallback_reached(_text: &str) -> bool {
true
}
let (cmd, args) = exit_cmd(false);
let result = temp_env::with_vars_unset(Vec::<&str>::new(), || {
yank_to_clipboard_with("nonzero exit test", &[(cmd, args)], fallback_reached)
});
assert!(result);
}
#[test]
fn test_yank_to_clipboard_falls_back_to_osc52_when_no_native_tool_on_path() {
fn fake_osc52_fallback(_text: &str) -> bool {
true
}
let result = temp_env::with_var("PATH", Some("/lev-definitely-empty-path-dir"), || {
yank_to_clipboard_via("fallback path test content", fake_osc52_fallback)
});
assert!(result);
}
}