use ratatui::text::{Line, Span};
use super::super::flock::{fit, mark};
use crate::commands::settings::{ScalarView, SettingField, SettingsSnapshot};
use crate::style::StyleSource;
pub(super) fn scalar_view(snapshot: &SettingsSnapshot, field: SettingField) -> &ScalarView {
match field {
SettingField::LogLevel => &snapshot.log_level,
SettingField::LogJson => &snapshot.log_json,
SettingField::Socket => &snapshot.socket,
SettingField::MaxCronSleep => &snapshot.max_cron_sleep,
SettingField::AllowControl => &snapshot.allow_control,
SettingField::StyleLevel => &snapshot.style_level,
}
}
pub(in crate::lookout::view) const fn field_label(field: SettingField) -> &'static str {
field.key()
}
const fn apply_cost(field: SettingField, source: StyleSource) -> &'static str {
match field {
SettingField::LogLevel | SettingField::LogJson | SettingField::MaxCronSleep => {
"needs shep daemon reload"
}
SettingField::Socket => "needs the shepherd stopped and started",
SettingField::AllowControl => "needs shep whistle restarted",
SettingField::StyleLevel => match source {
StyleSource::Config | StyleSource::Default => "the next command reads it",
StyleSource::Env | StyleSource::Flag => "written, but outranked",
},
}
}
const SCALAR_NAME_W: u16 = 15;
const SCALAR_VALUE_W: u16 = 30;
const SCALAR_VALUE_MIN: u16 = 10;
const SCALAR_SOURCE_W: u16 = 11;
const SCALAR_COST_MIN: u16 = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ScalarColumn {
Name,
Value,
Source,
Cost,
}
const SCALAR_ALL: &[ScalarColumn] = &[
ScalarColumn::Name,
ScalarColumn::Value,
ScalarColumn::Source,
ScalarColumn::Cost,
];
const SCALAR_NO_COST: &[ScalarColumn] = &[
ScalarColumn::Name,
ScalarColumn::Value,
ScalarColumn::Source,
];
const SCALAR_FLOOR: &[ScalarColumn] = &[ScalarColumn::Name, ScalarColumn::Value];
const SCALAR_MIN_WIDTH: u16 = SCALAR_NAME_W + 2 + SCALAR_VALUE_MIN;
const SCALAR_TIERS: &[(u16, &[ScalarColumn])] = &[
(
SCALAR_NAME_W + 2 + SCALAR_VALUE_MIN + 2 + SCALAR_SOURCE_W + 2 + SCALAR_COST_MIN,
SCALAR_ALL,
),
(
SCALAR_NAME_W + 2 + SCALAR_VALUE_MIN + 2 + SCALAR_SOURCE_W,
SCALAR_NO_COST,
),
(SCALAR_MIN_WIDTH, SCALAR_FLOOR),
];
fn scalar_columns_for(width: u16) -> &'static [ScalarColumn] {
SCALAR_TIERS
.iter()
.find(|(threshold, _)| width >= *threshold)
.map_or(SCALAR_FLOOR, |(_, columns)| *columns)
}
fn scalar_widths(width: u16, columns: &[ScalarColumn]) -> (u16, u16) {
let fixed = SCALAR_NAME_W
+ if columns.contains(&ScalarColumn::Source) {
SCALAR_SOURCE_W
} else {
0
};
let gaps = u16::try_from(columns.len().saturating_sub(1)).unwrap_or(0) * 2;
let remainder = width.saturating_sub(fixed).saturating_sub(gaps);
if columns.contains(&ScalarColumn::Cost) {
let value = remainder
.saturating_sub(SCALAR_COST_MIN)
.clamp(SCALAR_VALUE_MIN, SCALAR_VALUE_W);
(value, remainder.saturating_sub(value).max(SCALAR_COST_MIN))
} else {
(remainder.clamp(SCALAR_VALUE_MIN, SCALAR_VALUE_W), 0)
}
}
fn scalar_cell(field: SettingField, view: &ScalarView, column: ScalarColumn) -> String {
match column {
ScalarColumn::Name => field_label(field).to_owned(),
ScalarColumn::Value => view.value.clone(),
ScalarColumn::Source => view.source.to_string(),
ScalarColumn::Cost => apply_cost(field, view.source).to_string(),
}
}
pub(super) fn scalar_line(
field: SettingField,
view: &ScalarView,
selected: bool,
width: u16,
) -> Line<'static> {
let columns = scalar_columns_for(width);
let (value_width, cost_width) = scalar_widths(width, columns);
let mut text = format!("{} ", mark(selected));
for (index, column) in columns.iter().enumerate() {
if index > 0 {
text.push_str(" ");
}
let cell_width = match column {
ScalarColumn::Name => SCALAR_NAME_W,
ScalarColumn::Value => value_width,
ScalarColumn::Source => SCALAR_SOURCE_W,
ScalarColumn::Cost => cost_width,
};
text.push_str(&fit(&scalar_cell(field, view, *column), cell_width));
}
Line::from(Span::raw(text))
}
#[cfg(test)]
mod tests {
use super::super::super::fixtures;
use super::super::content_lines;
use super::*;
use crate::style::StyleSource;
#[test]
fn the_scalar_apply_cost_drops_before_its_source() {
assert!(scalar_columns_for(118).contains(&ScalarColumn::Cost));
assert!(!scalar_columns_for(43).contains(&ScalarColumn::Cost));
assert!(
scalar_columns_for(43).contains(&ScalarColumn::Source),
"SOURCE is the screen's own subject and outlives the cost"
);
assert!(!scalar_columns_for(33).contains(&ScalarColumn::Source));
assert!(
scalar_columns_for(33).contains(&ScalarColumn::Value),
"NAME and VALUE are the floor"
);
}
#[test]
fn the_default_source_label_fits_the_column_it_was_sized_for() {
let rendered = fit(&StyleSource::Default.to_string(), SCALAR_SOURCE_W);
assert_eq!(
rendered.chars().count(),
usize::from(SCALAR_SOURCE_W),
"fit always pads or truncates to the exact column width"
);
assert!(
!rendered.contains('…'),
"SCALAR_SOURCE_W must fit \"the default\" whole: got {rendered:?}"
);
}
#[test]
fn the_style_cost_cell_stops_promising_the_next_command_when_it_is_outranked() {
let app = fixtures::app_in_settings_with_shadowed_style(StyleSource::Env);
let settings = app.settings().unwrap();
let lines = content_lines(&app, settings, app.palette(), 120, 0);
let rendered: Vec<String> = lines
.iter()
.map(|line| line.spans.iter().map(|s| s.content.as_ref()).collect())
.collect();
let row = rendered
.iter()
.find(|line| {
line.get(2..)
.is_some_and(|cells| cells.starts_with("level "))
})
.unwrap_or_else(|| panic!("the style row is drawn: {rendered:?}"));
assert!(row.contains("$SHEP_STYLE"), "got: {row:?}");
assert!(row.contains("written, but outranked"), "got: {row:?}");
assert!(!row.contains("the next command reads it"), "got: {row:?}");
}
}