use ratatui::text::{Line, Span};
use unicode_width::UnicodeWidthStr;
use crate::config::Theme;
#[derive(Debug, Clone, Copy)]
pub enum RowLayout {
FixedPad(usize),
RightAlign(u16),
}
pub fn format_modal_row(
label: &str,
value: &str,
focused: bool,
editing: bool,
theme: &Theme,
layout: RowLayout,
) -> Line<'static> {
let marker = if focused { "› " } else { " " };
let label_style = if focused {
theme.modal_item_selected
} else {
theme.modal_item
};
let value_style = if editing {
theme.modal_input_focused
} else if focused {
theme.modal_item_selected_hint
} else {
theme.modal_item_hint
};
match layout {
RowLayout::FixedPad(pad) => {
let label_padded = format!("{marker}{:<pad$}", label, pad = pad);
Line::from(vec![
Span::styled(label_padded, label_style),
Span::styled(value.to_owned(), value_style),
])
}
RowLayout::RightAlign(width) => {
let label_full = format!("{marker}{label}");
let label_w = label_full.chars().count();
let value_w = value.chars().count();
let total = label_w + value_w + 1;
let pad = (width as usize).saturating_sub(total).max(1);
let pad_str = " ".repeat(pad);
Line::from(vec![
Span::styled(label_full, label_style),
Span::styled(pad_str, label_style),
Span::styled(value.to_owned(), value_style),
])
}
}
}
pub fn truncate_to_cells(text: &str, max_cells: usize) -> String {
if max_cells == 0 {
return String::new();
}
if UnicodeWidthStr::width(text) <= max_cells {
return text.to_owned();
}
let budget = max_cells - 1;
let mut out = String::new();
let mut used = 0usize;
for ch in text.chars() {
let w = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0);
if used + w > budget {
break;
}
out.push(ch);
used += w;
}
out.push('…');
out
}
#[cfg(test)]
mod tests {
use super::*;
fn theme() -> &'static Theme {
Box::leak(Box::new(Theme::default()))
}
fn line_text(line: &Line<'_>) -> String {
line.spans.iter().map(|s| s.content.as_ref()).collect()
}
#[test]
fn fixed_pad_focused_uses_marker_and_pad() {
let line = format_modal_row(
"Theme",
"dark",
true,
false,
theme(),
RowLayout::FixedPad(10),
);
assert_eq!(line_text(&line), "› Theme dark");
}
#[test]
fn fixed_pad_unfocused_uses_blank_marker() {
let line = format_modal_row(
"Theme",
"dark",
false,
false,
theme(),
RowLayout::FixedPad(10),
);
assert_eq!(line_text(&line), " Theme dark");
}
#[test]
fn right_align_pads_to_width() {
let line = format_modal_row(
"Save",
"Ctrl+S",
false,
false,
theme(),
RowLayout::RightAlign(20),
);
assert_eq!(line_text(&line).chars().count(), 19);
assert!(line_text(&line).ends_with("Ctrl+S"));
}
#[test]
fn right_align_keeps_one_space_minimum_when_overflowing() {
let line = format_modal_row(
"Save",
"Ctrl+S",
false,
false,
theme(),
RowLayout::RightAlign(4),
);
assert!(line_text(&line).contains(" Ctrl+S"));
}
#[test]
fn editing_overrides_value_style() {
let t = theme();
let line = format_modal_row("X", "v", true, true, t, RowLayout::FixedPad(4));
let value_span = line.spans.last().unwrap();
assert_eq!(value_span.style, t.modal_input_focused);
}
#[test]
fn truncate_returns_input_when_it_fits() {
assert_eq!(truncate_to_cells("hello", 10), "hello");
assert_eq!(truncate_to_cells("hello", 5), "hello");
}
#[test]
fn truncate_appends_ellipsis_when_too_long() {
assert_eq!(truncate_to_cells("abcdef", 5), "abcd…");
}
#[test]
fn truncate_respects_wide_characters() {
assert_eq!(truncate_to_cells("漢字", 3), "漢…");
}
#[test]
fn truncate_zero_budget_returns_empty() {
assert_eq!(truncate_to_cells("abc", 0), "");
}
#[test]
fn truncate_one_cell_budget_returns_just_ellipsis() {
assert_eq!(truncate_to_cells("abc", 1), "…");
}
}