use ratatui::text::Line;
use shep_core::config::ApplyGroup;
use super::super::flock::{GUTTER, take_columns};
use crate::lookout::pane::UNSET;
use crate::output::width::{char_columns, text_columns};
pub(super) const BOX_WIDTH: u16 = 86;
pub(super) const KEY_W: u16 = 26;
pub(super) const KEY_MIN: u16 = 8;
pub(super) const VALUE_MIN: u16 = 8;
pub(super) const POSITION_W: u16 = 3;
pub(super) const COST_W: u16 = 10;
pub(super) const FULL_WIDTH: u16 = KEY_W + 2 + VALUE_MIN + 2 + COST_W;
pub(super) const VALUE_WIDTH: u16 = KEY_MIN + 2 + VALUE_MIN;
pub(super) const BLURB_WRAP: u16 = 66;
pub(super) const LEFT_MIN: u16 = 40;
pub(super) const PANEL_MIN: u16 = 50;
pub(super) const PANEL_MAX: u16 = 72;
pub(super) fn panel_width(width: u16) -> Option<u16> {
let wanted = (u32::from(width) * 45 / 100) as u16;
let panel = wanted.clamp(PANEL_MIN, PANEL_MAX);
(width.saturating_sub(panel) >= LEFT_MIN).then_some(panel)
}
pub(super) const LANDS_WITH_PANEL_MIN: u16 = 160;
pub(super) const fn lands_fits_beside_panel(width: u16, has_panel: bool) -> bool {
!has_panel || width >= LANDS_WITH_PANEL_MIN
}
pub(super) const fn body_width(width: u16) -> u16 {
width.saturating_sub(GUTTER)
}
pub(super) const fn cost_label(group: ApplyGroup) -> &'static str {
match group {
ApplyGroup::Live => "now",
ApplyGroup::NextSpawn => "next start",
ApplyGroup::Structural => "read-only",
ApplyGroup::NeedsRespawn | _ => "respawn",
}
}
pub(super) fn widths(width: u16, show_lands: bool) -> (u16, u16, u16) {
if show_lands && width >= FULL_WIDTH {
let rest = width - COST_W - 2;
let key = KEY_W.min(rest - VALUE_MIN - 2);
(key, rest - key - 2, COST_W)
} else if width >= VALUE_WIDTH {
let key = KEY_W.min(width - VALUE_MIN - 2);
(key, width - key - 2, 0)
} else {
(width, 0, 0)
}
}
pub(super) fn mask_secret(secret: bool, raw: String) -> String {
if secret && raw != UNSET {
"<set>".to_owned()
} else {
raw
}
}
pub(super) fn line_columns(line: &Line<'_>) -> usize {
line.spans
.iter()
.flat_map(|span| span.content.chars())
.map(char_columns)
.sum()
}
pub(super) fn wrap(text: &str, width: usize) -> Vec<String> {
if width == 0 {
return vec![text.to_owned()];
}
let mut lines = Vec::new();
let mut current = String::new();
for word in text.split_whitespace() {
let word_w: usize = text_columns(word);
let current_w: usize = text_columns(¤t);
if current.is_empty() {
current.push_str(word);
} else if current_w + 1 + word_w <= width {
current.push(' ');
current.push_str(word);
} else {
lines.push(std::mem::take(&mut current));
current.push_str(word);
}
}
if !current.is_empty() || lines.is_empty() {
lines.push(current);
}
lines
}
pub(super) fn clipped(text: &str, width: u16) -> String {
let width = usize::from(width);
let columns: usize = text_columns(text);
if columns <= width {
return text.to_owned();
}
if width == 0 {
return String::new();
}
let budget = width - 1;
let (mut out, _) = take_columns(text, budget);
out.push('…');
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_design_target_splits_eighty_eight_and_seventy_two() {
assert_eq!(panel_width(160), Some(72));
}
#[test]
fn the_panel_goes_below_ninety_columns() {
assert!(panel_width(90).is_some());
assert_eq!(panel_width(89), None);
}
#[test]
fn the_panel_never_drops_below_its_own_floor() {
for width in 90..=200 {
let panel = panel_width(width).expect("the panel is drawn above 89");
assert!((50..=72).contains(&panel), "{panel} at {width}");
}
}
#[test]
fn the_left_column_never_falls_below_its_own_floor() {
for width in 90..=200 {
let panel = panel_width(width).expect("the panel is drawn above 89");
assert!(width - panel >= 40, "{} left at {width}", width - panel);
}
}
#[test]
fn clipped_cuts_to_the_budget_and_pads_nothing() {
assert_eq!(clipped("explains the focused field", 12), "explains th…");
assert_eq!(clipped("羊羊羊", 5), "羊羊…");
assert_eq!(clipped("short", 12), "short");
assert_eq!(clipped("anything", 0), "");
}
}