use ratatui::text::{Line, Span};
use super::super::super::app::{App, DogTree, Settings};
use super::super::super::theme::Palette;
use super::super::flock::{fit, mark};
use super::super::pane::open_glyph;
use crate::vocabulary::Reported;
pub(super) const DOGS_CAPTION: &str =
"space arms, Enter applies; e opens a dog; a dog needs no reload";
const DOG_NAME_MIN: u16 = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum DogColumn {
Name,
InFile,
Source,
Version,
Running,
}
impl DogColumn {
#[must_use]
const fn width(self) -> u16 {
match self {
Self::Name => 0,
Self::InFile => 7,
Self::Source => 20,
Self::Version => 16,
Self::Running => 15,
}
}
}
const ALL_DOG_COLUMNS: &[DogColumn] = &[
DogColumn::Name,
DogColumn::InFile,
DogColumn::Source,
DogColumn::Version,
DogColumn::Running,
];
const NO_VERSION: &[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])] = &[
(75, ALL_DOG_COLUMNS),
(57, NO_VERSION),
(34, NO_SOURCE),
(DOG_MIN_WIDTH, FLOOR_DOG_COLUMNS),
];
#[must_use]
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)
}
const REFUSAL_ROOM: u16 = 54;
#[must_use]
fn running_of(app: &App, name: &str) -> Option<String> {
app.all_rows()
.into_iter()
.find(|row| row.info.dog.is_some() && row.info.name == name)
.map(|row| Reported::of(row.info.status, row.info.handshook).word())
}
pub(super) fn dog_header_line(
app: &App,
settings: &Settings,
index: usize,
selected: bool,
width: u16,
palette: Palette,
) -> Line<'static> {
let Some(dog) = settings.dog(index) else {
return Line::default();
};
let open = settings
.dog_tree(index)
.is_some_and(|tree| tree.is_open() || matches!(tree, DogTree::Probing));
let columns = columns_for(width);
let refusal = match settings.dog_tree(index) {
Some(DogTree::Refused { why }) => Some(why.clone()),
_ => None,
};
let owed = u16::try_from(
refusal
.as_ref()
.map_or(0, |why| why.chars().count().min(usize::from(REFUSAL_ROOM))),
)
.unwrap_or(REFUSAL_ROOM)
+ 2;
let name_width = dog_name_width(width, columns)
.saturating_sub(owed)
.max(DOG_NAME_MIN);
let mut text = format!("{} ", mark(selected));
let mut used = 0u16;
for (at, column) in columns.iter().enumerate() {
if at > 0 {
text.push_str(" ");
used += 2;
}
let cell_width = if *column == DogColumn::Name {
name_width
} else {
column.width()
};
let cell = match column {
DogColumn::Name => format!("{} {}", open_glyph(open), dog.name),
DogColumn::InFile => if dog.enabled { "yes" } else { "no" }.to_string(),
DogColumn::Source => dog.origin.source.as_str().to_string(),
DogColumn::Version => dog.origin.version_cell(),
DogColumn::Running => {
running_of(app, &dog.name).unwrap_or_else(|| "not running".into())
}
};
text.push_str(&fit(&cell, cell_width));
used += cell_width;
}
let mut spans = vec![Span::raw(text)];
if let Some(why) = refusal {
let room = width.saturating_sub(used).saturating_sub(2);
spans.push(Span::raw(" "));
spans.push(Span::styled(fit(&why, room), palette.refusal()));
}
Line::from(spans)
}
#[cfg(test)]
mod tests {
use super::super::super::fixtures;
use super::super::super::flock::GUTTER;
use super::super::content_lines;
use super::*;
use crate::commands::settings::DogView;
use crate::output::width::visible_width;
#[test]
fn the_dogs_version_column_drops_before_source_and_source_before_the_rest() {
assert!(columns_for(120).contains(&DogColumn::Version));
assert!(!columns_for(70).contains(&DogColumn::Version));
assert!(columns_for(70).contains(&DogColumn::Source));
assert!(!columns_for(50).contains(&DogColumn::Source));
assert!(
columns_for(50).contains(&DogColumn::Running),
"RUNNING is the diagnostic half and outlives SOURCE"
);
}
fn header_of(dog: DogView, width: u16) -> String {
let mut app = fixtures::app_in_settings_with_dog_drift();
let named = dog.name.clone();
app.update(crate::lookout::app::Msg::Settings {
result: Ok(crate::commands::settings::SettingsSnapshot {
dogs: vec![dog],
..app.settings().unwrap().snapshot().clone()
}),
});
let settings = app.settings().unwrap();
assert_eq!(
settings.dog(0).map(|d| d.name.as_str()),
Some(named.as_str())
);
dog_header_line(&app, settings, 0, false, width, app.palette())
.spans
.iter()
.map(|s| s.content.as_ref())
.collect()
}
#[test]
fn the_header_row_names_each_dogs_source_and_version() {
use crate::dog_origin::{DogOrigin, Source};
let installed = DogView {
origin: DogOrigin {
source: Source::Community,
version: Some("0.2.4".to_owned()),
newer: Some("0.3.0".to_owned()),
},
..DogView::adopted("log-rotate", true, std::path::Path::new("/opt/log-rotate"))
};
let line = header_of(installed, 120);
assert!(line.contains("installed, community"), "{line:?}");
assert!(line.contains("0.2.4 \u{2192} 0.3.0"), "{line:?}");
let line = header_of(
DogView::adopted("otel", true, std::path::Path::new("/opt/otel")),
120,
);
assert!(line.contains("adopted"), "{line:?}");
assert!(
!line.contains("/opt/otel"),
"the path is not the source: {line:?}"
);
let line = header_of(DogView::built_in("bark", true), 120);
assert!(line.contains("built-in"), "{line:?}");
assert!(line.contains("1.0.0"), "{line:?}");
}
#[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_names_the_key_that_opens_a_dog() {
assert_eq!(
DOGS_CAPTION,
"space arms, Enter applies; e opens a dog; a dog needs no reload"
);
}
#[test]
fn the_dogs_row_joins_the_file_against_the_running_flock() {
let app = fixtures::app_in_settings_with_dog_drift();
let otel = app
.settings()
.unwrap()
.snapshot()
.dogs
.iter()
.position(|dog| dog.name == "otel")
.expect("the fixture carries otel");
let line = dog_header_line(
&app,
app.settings().unwrap(),
otel,
false,
120,
app.palette(),
)
.spans
.iter()
.map(|s| s.content.as_ref())
.collect::<String>();
assert!(line.contains("no"), "in file: {line:?}");
assert!(line.contains("online"), "running: {line:?}");
let ledger = app
.settings()
.unwrap()
.snapshot()
.dogs
.iter()
.position(|dog| dog.name == "ledger")
.expect("the fixture carries ledger");
let line = dog_header_line(
&app,
app.settings().unwrap(),
ledger,
false,
120,
app.palette(),
)
.spans
.iter()
.map(|s| s.content.as_ref())
.collect::<String>();
assert!(line.contains("yes"), "in file: {line:?}");
assert!(line.contains("not running"), "running: {line:?}");
}
#[test]
fn a_dog_that_never_handshook_reads_silent_here_too() {
let app = fixtures::app_in_settings_with_silent_dog();
let settings = app.settings().unwrap();
let bark = settings
.snapshot()
.dogs
.iter()
.position(|dog| dog.name == "bark")
.expect("the fixture carries bark");
assert_eq!(running_of(&app, "bark").as_deref(), Some("silent"));
let line = dog_header_line(&app, settings, bark, false, 120, app.palette())
.spans
.iter()
.map(|s| s.content.as_ref())
.collect::<String>();
assert!(line.contains("silent"), "{line:?}");
}
}