use std::path::PathBuf;
use ratatui::text::{Line, Span};
use super::super::super::app::App;
use super::super::super::theme::Palette;
use super::super::flock::{fit, mark};
use crate::vocabulary::Reported;
pub(super) const DOGS_CAPTION: &str = "space arms, Enter applies; a dog needs no reload";
const DOG_NAME_MIN: u16 = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DogColumn {
Name,
InFile,
Source,
Running,
}
impl DogColumn {
#[must_use]
const fn header(self) -> &'static str {
match self {
Self::Name => "NAME",
Self::InFile => "IN FILE",
Self::Source => "SOURCE",
Self::Running => "RUNNING",
}
}
#[must_use]
const fn width(self) -> u16 {
match self {
Self::Name => 0,
Self::InFile => 7,
Self::Source => 24,
Self::Running => 15,
}
}
}
const ALL_DOG_COLUMNS: &[DogColumn] = &[
DogColumn::Name,
DogColumn::InFile,
DogColumn::Source,
DogColumn::Running,
];
const NO_SOURCE: &[DogColumn] = &[DogColumn::Name, DogColumn::InFile, DogColumn::Running];
const FLOOR_DOG_COLUMNS: &[DogColumn] = &[DogColumn::Name, DogColumn::Running];
pub(super) const DOG_MIN_WIDTH: u16 = DogColumn::Running.width() + DOG_NAME_MIN + 2;
pub(super) const DOG_TIERS: &[(u16, &[DogColumn])] = &[
(61, ALL_DOG_COLUMNS),
(34, NO_SOURCE),
(DOG_MIN_WIDTH, FLOOR_DOG_COLUMNS),
];
#[must_use]
pub fn columns_for(width: u16) -> &'static [DogColumn] {
DOG_TIERS
.iter()
.find(|(threshold, _)| width >= *threshold)
.map_or(FLOOR_DOG_COLUMNS, |(_, columns)| *columns)
}
fn dog_name_width(width: u16, columns: &[DogColumn]) -> u16 {
let fixed: u16 = columns.iter().map(|column| column.width()).sum();
let gaps = u16::try_from(columns.len().saturating_sub(1)).unwrap_or(0) * 2;
width
.saturating_sub(fixed)
.saturating_sub(gaps)
.max(DOG_NAME_MIN)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DogRow {
pub name: String,
pub enabled: bool,
pub running: Option<String>,
pub adopted_path: Option<PathBuf>,
}
#[must_use]
pub fn dog_rows(app: &App, width: u16) -> Vec<DogRow> {
let _ = width;
let Some(settings) = app.settings() else {
return Vec::new();
};
let running_by_name: std::collections::BTreeMap<&str, String> = app
.all_rows()
.into_iter()
.filter(|row| row.info.dog.is_some())
.map(|row| {
(
row.info.name.as_str(),
Reported::of(row.info.status, row.info.handshook).word(),
)
})
.collect();
settings
.snapshot()
.dogs
.iter()
.map(|dog| DogRow {
name: dog.name.clone(),
enabled: dog.enabled,
running: running_by_name.get(dog.name.as_str()).cloned(),
adopted_path: dog.adopted_path.clone(),
})
.collect()
}
fn dog_cell(dog: &DogRow, column: DogColumn) -> String {
match column {
DogColumn::Name => dog.name.clone(),
DogColumn::InFile => if dog.enabled { "yes" } else { "no" }.to_string(),
DogColumn::Source => dog
.adopted_path
.as_ref()
.map_or_else(|| "built-in".to_string(), |path| path.display().to_string()),
DogColumn::Running => dog
.running
.clone()
.unwrap_or_else(|| "not running".to_string()),
}
}
pub(super) fn dog_header_line(
columns: &[DogColumn],
width: u16,
palette: Palette,
) -> Line<'static> {
let name_width = dog_name_width(width, columns);
let mut text = String::from(" ");
for (index, column) in columns.iter().enumerate() {
if index > 0 {
text.push_str(" ");
}
let cell_width = if *column == DogColumn::Name {
name_width
} else {
column.width()
};
text.push_str(&fit(column.header(), cell_width));
}
Line::from(Span::styled(text, palette.muted()))
}
pub(super) fn dog_line(
dog: &DogRow,
columns: &[DogColumn],
width: u16,
selected: bool,
) -> Line<'static> {
let name_width = dog_name_width(width, columns);
let mut text = format!("{} ", mark(selected));
for (index, column) in columns.iter().enumerate() {
if index > 0 {
text.push_str(" ");
}
let cell_width = if *column == DogColumn::Name {
name_width
} else {
column.width()
};
text.push_str(&fit(&dog_cell(dog, *column), cell_width));
}
Line::from(Span::raw(text))
}
#[cfg(test)]
mod tests {
use super::super::super::fixtures;
use super::super::super::flock::GUTTER;
use super::super::content_lines;
use super::*;
use crate::output::width::visible_width;
#[test]
fn the_dogs_source_column_drops_before_the_rest() {
assert!(columns_for(120).contains(&DogColumn::Source));
assert!(!columns_for(60).contains(&DogColumn::Source));
assert!(
columns_for(60).contains(&DogColumn::Running),
"RUNNING is the diagnostic half and outlives SOURCE"
);
}
#[test]
fn every_dogs_tier_fits_the_width_it_claims() {
let app = fixtures::app_in_settings_with_dog_drift();
let settings = app.settings().unwrap();
let palette = app.palette();
for width in (DOG_MIN_WIDTH + GUTTER)..=200 {
let rendered: Vec<String> = content_lines(&app, settings, palette, width, 0)
.iter()
.map(|line| line.spans.iter().map(|s| s.content.as_ref()).collect())
.collect();
let table = rendered
.iter()
.position(|line| line.contains("[dogs]"))
.expect("the dogs section is always drawn");
for line in &rendered[table..] {
assert!(
visible_width(line) <= usize::from(width),
"width {width} drew {} columns: {line:?}",
visible_width(line)
);
}
}
}
#[test]
fn the_dogs_caption_says_arms_not_applies() {
assert_eq!(
DOGS_CAPTION,
"space arms, Enter applies; a dog needs no reload"
);
}
#[test]
fn the_dogs_table_joins_the_file_against_the_running_flock() {
let app = fixtures::app_in_settings_with_dog_drift();
let rows = dog_rows(&app, 120);
let otel = rows.iter().find(|r| r.name == "otel").unwrap();
assert!(!otel.enabled);
assert_eq!(otel.running.as_deref(), Some("online"));
let ledger = rows.iter().find(|r| r.name == "ledger").unwrap();
assert!(ledger.enabled);
assert_eq!(ledger.running, None);
}
#[test]
fn a_dog_that_never_handshook_reads_silent_here_too() {
let app = fixtures::app_in_settings_with_silent_dog();
let rows = dog_rows(&app, 120);
let bark = rows.iter().find(|r| r.name == "bark").unwrap();
assert_eq!(bark.running.as_deref(), Some("silent"));
}
}