use rhai::{Array, Dynamic, Engine};
use std::sync::atomic::{AtomicBool, Ordering};
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
static COLORS_ENABLED: AtomicBool = AtomicBool::new(false);
pub fn set_colors_enabled(enabled: bool) {
COLORS_ENABLED.store(enabled, Ordering::Relaxed);
}
fn colors_enabled() -> bool {
COLORS_ENABLED.load(Ordering::Relaxed)
}
const RESET: &str = "\x1b[0m";
fn wrap(text: &str, code: &str) -> String {
if colors_enabled() {
format!("{code}{text}{RESET}")
} else {
text.to_string()
}
}
pub fn register_functions(engine: &mut Engine) {
engine.register_fn("human_bytes", |n: i64| -> String {
human_bytes_impl(n as f64, false)
});
engine.register_fn("human_bytes", |n: f64| -> String {
human_bytes_impl(n, false)
});
engine.register_fn("human_bytes_si", |n: i64| -> String {
human_bytes_impl(n as f64, true)
});
engine.register_fn("human_bytes_si", |n: f64| -> String {
human_bytes_impl(n, true)
});
engine.register_fn("format_decimals", |value: f64, decimals: i64| -> String {
format_decimals_impl(value, decimals)
});
engine.register_fn("format_decimals", |value: i64, decimals: i64| -> String {
format_decimals_impl(value as f64, decimals)
});
engine.register_fn("format_percent", |ratio: f64, decimals: i64| -> String {
format_percent_impl(ratio, decimals)
});
engine.register_fn("format_percent", |ratio: i64, decimals: i64| -> String {
format_percent_impl(ratio as f64, decimals)
});
engine.register_fn("ljust", |s: &str, n: i64| -> String {
ljust_impl(s, n, ' ')
});
engine.register_fn("ljust", |s: &str, n: i64, fill: &str| -> String {
ljust_impl(s, n, fill_char(fill))
});
engine.register_fn("rjust", |s: &str, n: i64| -> String {
rjust_impl(s, n, ' ')
});
engine.register_fn("rjust", |s: &str, n: i64, fill: &str| -> String {
rjust_impl(s, n, fill_char(fill))
});
engine.register_fn("center", |s: &str, n: i64| -> String {
center_impl(s, n, ' ')
});
engine.register_fn("center", |s: &str, n: i64, fill: &str| -> String {
center_impl(s, n, fill_char(fill))
});
engine.register_fn("shorten", |s: &str, n: i64| -> String {
shorten_impl(s, n, "…")
});
engine.register_fn("shorten", |s: &str, n: i64, marker: &str| -> String {
shorten_impl(s, n, marker)
});
engine.register_fn("shorten_middle", |s: &str, n: i64| -> String {
shorten_middle_impl(s, n, "…")
});
engine.register_fn(
"shorten_middle",
|s: &str, n: i64, marker: &str| -> String { shorten_middle_impl(s, n, marker) },
);
engine.register_fn("bar", |value: f64, max: f64, width: i64| -> String {
bar_impl(value / max, width)
});
engine.register_fn("bar", |value: i64, max: i64, width: i64| -> String {
let ratio = if max == 0 {
0.0
} else {
value as f64 / max as f64
};
bar_impl(ratio, width)
});
engine.register_fn("bar", |value: f64, max: i64, width: i64| -> String {
let ratio = if max == 0 { 0.0 } else { value / max as f64 };
bar_impl(ratio, width)
});
engine.register_fn("bar", |value: i64, max: f64, width: i64| -> String {
bar_impl(value as f64 / max, width)
});
engine.register_fn("sparkline", |arr: Array| -> String { sparkline_impl(&arr) });
engine.register_fn("red", |s: &str| -> String { wrap(s, "\x1b[91m") });
engine.register_fn("green", |s: &str| -> String { wrap(s, "\x1b[92m") });
engine.register_fn("yellow", |s: &str| -> String { wrap(s, "\x1b[93m") });
engine.register_fn("blue", |s: &str| -> String { wrap(s, "\x1b[94m") });
engine.register_fn("cyan", |s: &str| -> String { wrap(s, "\x1b[96m") });
engine.register_fn("magenta", |s: &str| -> String { wrap(s, "\x1b[95m") });
engine.register_fn("bold", |s: &str| -> String { wrap(s, "\x1b[1m") });
engine.register_fn("dim", |s: &str| -> String { wrap(s, "\x1b[2m") });
}
fn human_bytes_impl(n: f64, si: bool) -> String {
if n.is_nan() {
return "NaN".to_string();
}
if n.is_infinite() {
return if n.is_sign_negative() {
"-inf".to_string()
} else {
"inf".to_string()
};
}
let negative = n.is_sign_negative();
let mut value = n.abs();
let (base, units): (f64, &[&str]) = if si {
(1000.0, &["B", "KB", "MB", "GB", "TB", "PB", "EB"])
} else {
(1024.0, &["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"])
};
let mut idx = 0;
while value >= base && idx < units.len() - 1 {
value /= base;
idx += 1;
}
if idx < units.len() - 1 && (value * 10.0).round() >= base * 10.0 {
value /= base;
idx += 1;
}
let sign = if negative { "-" } else { "" };
if idx == 0 {
format!("{}{} {}", sign, value.round() as i64, units[idx])
} else {
format!("{}{:.1} {}", sign, value, units[idx])
}
}
fn format_decimals_impl(value: f64, decimals: i64) -> String {
let d = decimals.clamp(0, 20) as usize;
format!("{:.*}", d, value)
}
fn format_percent_impl(ratio: f64, decimals: i64) -> String {
let d = decimals.clamp(0, 20) as usize;
format!("{:.*}%", d, ratio * 100.0)
}
fn fill_char(fill: &str) -> char {
fill.chars()
.next()
.filter(|c| UnicodeWidthChar::width(*c) == Some(1))
.unwrap_or(' ')
}
fn ljust_impl(s: &str, target: i64, fill: char) -> String {
let target = target.max(0) as usize;
let w = UnicodeWidthStr::width(s);
if w >= target {
return s.to_string();
}
let mut out = String::with_capacity(s.len() + (target - w));
out.push_str(s);
for _ in 0..(target - w) {
out.push(fill);
}
out
}
fn rjust_impl(s: &str, target: i64, fill: char) -> String {
let target = target.max(0) as usize;
let w = UnicodeWidthStr::width(s);
if w >= target {
return s.to_string();
}
let pad = target - w;
let mut out = String::with_capacity(s.len() + pad);
for _ in 0..pad {
out.push(fill);
}
out.push_str(s);
out
}
fn center_impl(s: &str, target: i64, fill: char) -> String {
let target = target.max(0) as usize;
let w = UnicodeWidthStr::width(s);
if w >= target {
return s.to_string();
}
let total_pad = target - w;
let left_pad = total_pad / 2;
let right_pad = total_pad - left_pad;
let mut out = String::with_capacity(s.len() + total_pad);
for _ in 0..left_pad {
out.push(fill);
}
out.push_str(s);
for _ in 0..right_pad {
out.push(fill);
}
out
}
fn take_prefix_by_width(s: &str, budget: usize) -> (String, usize) {
let mut out = String::new();
let mut width = 0usize;
for c in s.chars() {
let cw = UnicodeWidthChar::width(c).unwrap_or(0);
if width + cw > budget {
break;
}
out.push(c);
width += cw;
}
(out, width)
}
fn take_suffix_by_width(s: &str, budget: usize) -> (String, usize) {
let mut chars: Vec<char> = Vec::new();
let mut width = 0usize;
for c in s.chars().rev() {
let cw = UnicodeWidthChar::width(c).unwrap_or(0);
if width + cw > budget {
break;
}
chars.push(c);
width += cw;
}
chars.reverse();
(chars.into_iter().collect(), width)
}
fn shorten_impl(s: &str, target: i64, marker: &str) -> String {
let target = target.max(0) as usize;
let w = UnicodeWidthStr::width(s);
if w <= target {
return s.to_string();
}
let mw = UnicodeWidthStr::width(marker);
if mw >= target {
return take_prefix_by_width(s, target).0;
}
let budget = target - mw;
let (prefix, _) = take_prefix_by_width(s, budget);
format!("{prefix}{marker}")
}
fn shorten_middle_impl(s: &str, target: i64, marker: &str) -> String {
let target = target.max(0) as usize;
let w = UnicodeWidthStr::width(s);
if w <= target {
return s.to_string();
}
let mw = UnicodeWidthStr::width(marker);
if mw >= target {
return take_prefix_by_width(s, target).0;
}
let budget = target - mw;
let front_budget = budget.div_ceil(2);
let back_budget = budget - front_budget;
let (front, _) = take_prefix_by_width(s, front_budget);
let (back, _) = take_suffix_by_width(s, back_budget);
format!("{front}{marker}{back}")
}
const EIGHTHS: [&str; 9] = [" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"];
const SPARKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
fn bar_impl(ratio: f64, width: i64) -> String {
let width = width.max(0) as usize;
if width == 0 {
return String::new();
}
let r = if ratio.is_nan() {
0.0
} else {
ratio.clamp(0.0, 1.0)
};
let total_eighths = (r * (width as f64) * 8.0).round() as usize;
let full_cells = total_eighths / 8;
let partial = total_eighths % 8;
let mut out = String::with_capacity(width * 3);
for _ in 0..full_cells {
out.push_str(EIGHTHS[8]);
}
if full_cells < width {
out.push_str(EIGHTHS[partial]);
for _ in (full_cells + 1)..width {
out.push(' ');
}
}
out
}
fn dyn_to_f64(value: &Dynamic) -> f64 {
if value.is_int() {
value.as_int().unwrap_or(0) as f64
} else if value.is_float() {
value.as_float().unwrap_or(0.0)
} else if value.is_bool() {
if value.as_bool().unwrap_or(false) {
1.0
} else {
0.0
}
} else {
0.0
}
}
fn sparkline_impl(arr: &[Dynamic]) -> String {
if arr.is_empty() {
return String::new();
}
let values: Vec<f64> = arr.iter().map(dyn_to_f64).map(|v| v.max(0.0)).collect();
let max = values.iter().copied().fold(0.0_f64, f64::max);
let mut out = String::with_capacity(arr.len() * 3);
if max <= 0.0 {
for _ in 0..arr.len() {
out.push(' ');
}
return out;
}
let levels = SPARKS.len() as f64; for v in values {
if v <= 0.0 {
out.push(' ');
continue;
}
let scaled = (v / max * levels).ceil() as usize;
let idx = scaled.clamp(1, SPARKS.len()) - 1;
out.push(SPARKS[idx]);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_human_bytes_binary_basic() {
assert_eq!(human_bytes_impl(0.0, false), "0 B");
assert_eq!(human_bytes_impl(1.0, false), "1 B");
assert_eq!(human_bytes_impl(500.0, false), "500 B");
assert_eq!(human_bytes_impl(1023.0, false), "1023 B");
assert_eq!(human_bytes_impl(1024.0, false), "1.0 KiB");
assert_eq!(human_bytes_impl(1536.0, false), "1.5 KiB");
assert_eq!(human_bytes_impl(1048576.0, false), "1.0 MiB");
assert_eq!(human_bytes_impl(1073741824.0, false), "1.0 GiB");
}
#[test]
fn test_human_bytes_binary_rounding_boundary() {
assert_eq!(human_bytes_impl(1048575.0, false), "1.0 MiB"); assert_eq!(human_bytes_impl(1073741823.0, false), "1.0 GiB"); assert_eq!(human_bytes_impl(1_047_552.0, false), "1023.0 KiB"); }
#[test]
fn test_human_bytes_si_basic() {
assert_eq!(human_bytes_impl(0.0, true), "0 B");
assert_eq!(human_bytes_impl(999.0, true), "999 B");
assert_eq!(human_bytes_impl(1000.0, true), "1.0 KB");
assert_eq!(human_bytes_impl(1500.0, true), "1.5 KB");
assert_eq!(human_bytes_impl(1_000_000.0, true), "1.0 MB");
assert_eq!(human_bytes_impl(1_500_000_000.0, true), "1.5 GB");
}
#[test]
fn test_human_bytes_si_rounding_boundary() {
assert_eq!(human_bytes_impl(999_999.0, true), "1.0 MB"); assert_eq!(human_bytes_impl(999_999_999.0, true), "1.0 GB"); }
#[test]
fn test_human_bytes_negative() {
assert_eq!(human_bytes_impl(-500.0, false), "-500 B");
assert_eq!(human_bytes_impl(-1536.0, false), "-1.5 KiB");
assert_eq!(human_bytes_impl(-1_500_000.0, true), "-1.5 MB");
}
#[test]
fn test_human_bytes_large_values() {
let huge = 1024.0_f64.powi(7); let result = human_bytes_impl(huge, false);
assert!(result.ends_with(" EiB"), "got {result}");
}
#[test]
fn test_human_bytes_special_floats() {
assert_eq!(human_bytes_impl(f64::NAN, false), "NaN");
assert_eq!(human_bytes_impl(f64::INFINITY, false), "inf");
assert_eq!(human_bytes_impl(f64::NEG_INFINITY, false), "-inf");
}
#[test]
fn test_format_decimals_basic() {
assert_eq!(format_decimals_impl(1.23456, 2), "1.23");
assert_eq!(format_decimals_impl(1.23456, 3), "1.235");
assert_eq!(format_decimals_impl(1.23456, 0), "1");
assert_eq!(format_decimals_impl(1.0, 2), "1.00");
assert_eq!(format_decimals_impl(1.5, 0), "2"); }
#[test]
fn test_format_decimals_negative_value() {
assert_eq!(format_decimals_impl(-2.75, 1), "-2.8");
assert_eq!(format_decimals_impl(-0.5, 2), "-0.50");
}
#[test]
fn test_format_decimals_clamps_decimals_arg() {
assert_eq!(format_decimals_impl(1.23456, -1), "1");
let result = format_decimals_impl(1.0, 100);
assert_eq!(result.len(), "1.".len() + 20);
}
#[test]
fn test_format_decimals_zero() {
assert_eq!(format_decimals_impl(0.0, 2), "0.00");
assert_eq!(format_decimals_impl(0.0, 0), "0");
}
#[test]
fn test_format_percent_basic() {
assert_eq!(format_percent_impl(0.0, 1), "0.0%");
assert_eq!(format_percent_impl(0.5, 0), "50%");
assert_eq!(format_percent_impl(0.042, 1), "4.2%");
assert_eq!(format_percent_impl(1.0, 0), "100%");
assert_eq!(format_percent_impl(0.12345, 2), "12.35%");
}
#[test]
fn test_format_percent_over_one() {
assert_eq!(format_percent_impl(1.5, 0), "150%");
assert_eq!(format_percent_impl(2.25, 1), "225.0%");
}
#[test]
fn test_format_percent_negative() {
assert_eq!(format_percent_impl(-0.1, 1), "-10.0%");
}
#[test]
fn test_format_percent_clamps_decimals_arg() {
assert_eq!(format_percent_impl(0.5, -1), "50%");
}
#[test]
fn test_ljust_basic() {
assert_eq!(ljust_impl("hi", 5, ' '), "hi ");
assert_eq!(ljust_impl("hello", 5, ' '), "hello");
assert_eq!(ljust_impl("hello", 3, ' '), "hello"); assert_eq!(ljust_impl("", 3, '-'), "---");
}
#[test]
fn test_rjust_basic() {
assert_eq!(rjust_impl("hi", 5, ' '), " hi");
assert_eq!(rjust_impl("42", 6, '0'), "000042");
assert_eq!(rjust_impl("hello", 3, ' '), "hello");
}
#[test]
fn test_center_basic() {
assert_eq!(center_impl("hi", 6, ' '), " hi ");
assert_eq!(center_impl("hi", 5, ' '), " hi ");
assert_eq!(center_impl("hi", 4, '-'), "-hi-");
assert_eq!(center_impl("hello", 3, ' '), "hello");
}
#[test]
fn test_padding_negative_width() {
assert_eq!(ljust_impl("hi", -5, ' '), "hi");
assert_eq!(rjust_impl("hi", -1, ' '), "hi");
assert_eq!(center_impl("hi", -3, ' '), "hi");
}
#[test]
fn test_padding_unicode_width_aware() {
assert_eq!(UnicodeWidthStr::width("日本"), 4);
assert_eq!(ljust_impl("日本", 6, ' '), "日本 ");
assert_eq!(rjust_impl("日本", 6, ' '), " 日本");
assert_eq!(center_impl("日本", 6, ' '), " 日本 ");
}
#[test]
fn test_fill_char_defaults_to_space() {
assert_eq!(fill_char(""), ' ');
assert_eq!(fill_char("日"), ' ');
assert_eq!(fill_char("-"), '-');
assert_eq!(fill_char("0"), '0');
assert_eq!(fill_char("abc"), 'a');
}
#[test]
fn test_shorten_basic() {
assert_eq!(shorten_impl("hello world", 20, "…"), "hello world");
assert_eq!(shorten_impl("hello world", 11, "…"), "hello world");
assert_eq!(shorten_impl("hello world", 8, "…"), "hello w…");
assert_eq!(shorten_impl("hello world", 5, "…"), "hell…");
}
#[test]
fn test_shorten_ascii_marker() {
assert_eq!(shorten_impl("hello world", 8, "..."), "hello...");
assert_eq!(shorten_impl("hello world", 6, "..."), "hel...");
}
#[test]
fn test_shorten_empty_marker() {
assert_eq!(shorten_impl("hello world", 5, ""), "hello");
}
#[test]
fn test_shorten_marker_too_wide() {
assert_eq!(shorten_impl("hello world", 3, ">>>>"), "hel");
}
#[test]
fn test_shorten_unicode() {
assert_eq!(UnicodeWidthStr::width("日本語テスト"), 12);
assert_eq!(shorten_impl("日本語テスト", 7, "…"), "日本語…");
assert_eq!(shorten_impl("日本語テスト", 5, "…"), "日本…");
}
#[test]
fn test_shorten_negative_width() {
assert_eq!(shorten_impl("hello", -1, "…"), "");
}
#[test]
fn test_shorten_middle_basic() {
assert_eq!(shorten_middle_impl("abcdefghij", 6, "…"), "abc…ij");
}
#[test]
fn test_shorten_middle_even_budget() {
assert_eq!(shorten_middle_impl("abcdefghij", 7, "…"), "abc…hij");
}
#[test]
fn test_shorten_middle_ascii_marker() {
assert_eq!(shorten_middle_impl("abcdefghij", 8, "..."), "abc...ij");
}
#[test]
fn test_shorten_middle_no_truncation() {
assert_eq!(shorten_middle_impl("short", 20, "…"), "short");
assert_eq!(shorten_middle_impl("hello", 5, "…"), "hello");
}
#[test]
fn test_shorten_middle_path_example() {
let path = "/home/user/projects/kelora/src/rhai_functions/formatting.rs";
let result = shorten_middle_impl(path, 30, "…");
assert!(UnicodeWidthStr::width(result.as_str()) <= 30);
assert!(result.starts_with('/'));
assert!(result.ends_with("formatting.rs"));
assert!(result.contains('…'));
}
#[test]
fn test_shorten_middle_marker_too_wide() {
assert_eq!(shorten_middle_impl("abcdefghij", 3, ">>>>"), "abc");
}
#[test]
fn test_shorten_middle_unicode() {
assert_eq!(shorten_middle_impl("日本語テスト", 8, "…"), "日本…ト");
}
#[test]
fn test_shorten_width_invariant() {
for target in 0..15 {
let out = shorten_impl("/some/path/to/a/file.rs", target, "…");
assert!(
UnicodeWidthStr::width(out.as_str()) <= target as usize,
"shorten(target={target}) exceeded: {out:?}"
);
let out = shorten_middle_impl("/some/path/to/a/file.rs", target, "…");
assert!(
UnicodeWidthStr::width(out.as_str()) <= target as usize,
"shorten_middle(target={target}) exceeded: {out:?}"
);
}
}
#[test]
fn test_colors_disabled_by_default_returns_plain() {
set_colors_enabled(false);
assert_eq!(wrap("hi", "\x1b[91m"), "hi");
}
#[test]
fn test_colors_enabled_wraps_with_ansi() {
set_colors_enabled(true);
assert_eq!(wrap("hi", "\x1b[91m"), "\x1b[91mhi\x1b[0m");
set_colors_enabled(false);
}
#[test]
fn test_colors_empty_string() {
set_colors_enabled(true);
assert_eq!(wrap("", "\x1b[1m"), "\x1b[1m\x1b[0m");
set_colors_enabled(false);
}
#[test]
fn test_colors_set_and_get() {
set_colors_enabled(true);
assert!(colors_enabled());
set_colors_enabled(false);
assert!(!colors_enabled());
}
#[test]
fn test_bar_empty_and_full() {
assert_eq!(bar_impl(0.0, 10), " ");
assert_eq!(bar_impl(1.0, 10), "██████████");
}
#[test]
fn test_bar_half() {
assert_eq!(bar_impl(0.5, 10), "█████ ");
}
#[test]
fn test_bar_eighth_resolution() {
assert_eq!(bar_impl(0.125, 1), "▏");
assert_eq!(bar_impl(0.125, 4), "▌ ");
assert_eq!(bar_impl(0.375, 1), "▍");
assert_eq!(bar_impl(0.875, 1), "▉");
}
#[test]
fn test_bar_clamps_overflow_and_negative() {
assert_eq!(bar_impl(2.0, 5), "█████");
assert_eq!(bar_impl(-0.5, 5), " ");
}
#[test]
fn test_bar_nan_and_zero_width() {
assert_eq!(bar_impl(f64::NAN, 5), " ");
assert_eq!(bar_impl(0.5, 0), "");
assert_eq!(bar_impl(0.5, -3), "");
}
#[test]
fn test_bar_display_width_invariant() {
for width in 0..20_i64 {
for &ratio in &[-1.0, 0.0, 0.1, 0.33, 0.5, 0.75, 0.99, 1.0, 2.0] {
let out = bar_impl(ratio, width);
assert_eq!(
UnicodeWidthStr::width(out.as_str()),
width.max(0) as usize,
"bar(ratio={ratio}, width={width}) = {out:?}"
);
}
}
}
#[test]
fn test_sparkline_empty() {
let arr: Vec<Dynamic> = vec![];
assert_eq!(sparkline_impl(&arr), "");
}
#[test]
fn test_sparkline_all_zero() {
let arr: Vec<Dynamic> = vec![
Dynamic::from(0_i64),
Dynamic::from(0_i64),
Dynamic::from(0_i64),
];
assert_eq!(sparkline_impl(&arr), " ");
}
#[test]
fn test_sparkline_monotonic() {
let arr: Vec<Dynamic> = (1_i64..=8).map(Dynamic::from).collect();
assert_eq!(sparkline_impl(&arr), "▁▂▃▄▅▆▇█");
}
#[test]
fn test_sparkline_mixed_types() {
let arr: Vec<Dynamic> = vec![
Dynamic::from(0_i64),
Dynamic::from(5.0_f64),
Dynamic::from(10_i64),
];
assert_eq!(sparkline_impl(&arr), " ▄█");
}
#[test]
fn test_sparkline_negative_clamped() {
let arr: Vec<Dynamic> = vec![
Dynamic::from(-3_i64),
Dynamic::from(5_i64),
Dynamic::from(10_i64),
];
assert_eq!(sparkline_impl(&arr), " ▄█");
}
#[test]
fn test_sparkline_single_value() {
let arr: Vec<Dynamic> = vec![Dynamic::from(42_i64)];
assert_eq!(sparkline_impl(&arr), "█");
}
#[test]
fn test_padding_width_invariant() {
for target in 0..12 {
for s in &["", "x", "hi", "hello"] {
let input_w = UnicodeWidthStr::width(*s);
let expected = input_w.max(target as usize);
assert_eq!(
UnicodeWidthStr::width(ljust_impl(s, target, ' ').as_str()),
expected
);
assert_eq!(
UnicodeWidthStr::width(rjust_impl(s, target, ' ').as_str()),
expected
);
assert_eq!(
UnicodeWidthStr::width(center_impl(s, target, ' ').as_str()),
expected
);
}
}
}
}