use super::super::super::app::{Control, SecretsPane};
use super::super::super::secrets::Source;
use super::super::super::theme::Palette;
use super::super::flock::fit;
use super::column_tiers::Column;
use crate::output::human_duration;
use crate::vocabulary::Role;
use ratatui::text::{Line, Span};
const PANE_BAND_LABEL: &str = "SECRETS flock-wide values a Flockfile refers to and never carries";
pub(super) const STORE_LINE: &str = "store $SHEP_HOME/secrets.json \u{b7} not encrypted \u{b7} never \
printed to a log, never carried in a bleat \u{b7} read at spawn, not now";
pub(super) fn pane_band(width: u16, palette: Palette) -> Line<'static> {
Line::from(Span::styled(
super::super::cell::band(PANE_BAND_LABEL, usize::from(width)),
palette.band(Role::Butter),
))
}
pub(super) fn gates_line(pane: &SecretsPane, control: Control, palette: Palette) -> Line<'static> {
let allowed = matches!(control, Control::Allowed);
Line::from(Span::styled(
format!(
"reveal [secrets] allow_read = {} in shep.toml \u{b7} change \
lookout.allow_control = {allowed}",
pane.model.allow_read
),
palette.muted(),
))
}
pub(super) fn heading_line(columns: &[Column], palette: Palette) -> Line<'static> {
let mut text = String::new();
for column in columns {
text.push_str(&fit(column.heading(), column.width()));
}
Line::from(Span::styled(text, palette.muted()))
}
pub(super) fn roll_status_line(pane: &SecretsPane, palette: Palette) -> Line<'static> {
if let Some(message) = &pane.model.unreadable {
return Line::from(Span::styled(
format!("operator store unreadable: {message}"),
palette.refusal(),
));
}
if let Some(age) = pane.model.roll_age {
let ms = u64::try_from(age.as_millis()).unwrap_or(u64::MAX);
return Line::from(Span::styled(
format!("READ BY as of the roll, read {} ago", human_duration(ms)),
palette.muted(),
));
}
Line::from(Span::styled(
"no muster roll yet: READ BY and WHO READS IT show nothing until the shepherd writes one",
palette.muted(),
))
}
pub(super) fn tab_line(pane: &SecretsPane, palette: Palette, width: u16) -> Line<'static> {
let labels: Vec<String> = pane
.model
.environments
.iter()
.enumerate()
.map(|(index, name)| {
if index == pane.tab {
format!("[{name}]")
} else {
name.clone()
}
})
.collect();
let environment_count = labels.len();
let suffix = format!("{environment_count} environments in this store \u{b7} \u{2190}/\u{2192}");
let budget = usize::from(width).saturating_sub(suffix.chars().count() + TAB_GAP);
let anchor = if pane.tab < labels.len() { pane.tab } else { 0 };
let (first, last) = tab_window(&labels, anchor, budget);
let mut spans = Vec::with_capacity(labels.len() * 2 + 4);
let mut drawn = 0usize;
if first > 0 {
spans.push(Span::styled("\u{2026}", palette.muted()));
drawn += 1;
}
for (index, label) in labels.iter().enumerate().take(last + 1).skip(first) {
if !spans.is_empty() {
spans.push(Span::raw(" "));
drawn += TAB_GAP;
}
drawn += label.chars().count();
let style = if index == pane.tab {
palette.attention()
} else {
palette.muted()
};
spans.push(Span::styled(label.clone(), style));
}
if last + 1 < labels.len() {
spans.push(Span::raw(" "));
spans.push(Span::styled("\u{2026}", palette.muted()));
drawn += TAB_ELISION;
}
let pad = usize::from(width)
.saturating_sub(drawn)
.saturating_sub(suffix.chars().count());
if pad > 0 {
spans.push(Span::raw(" ".repeat(pad)));
}
spans.push(Span::styled(suffix, palette.muted()));
Line::from(spans)
}
const TAB_GAP: usize = 2;
const TAB_ELISION: usize = TAB_GAP + 1;
fn tab_window(labels: &[String], active: usize, budget: usize) -> (usize, usize) {
let (mut first, mut last) = (active, active);
let mut used = labels.get(active).map_or(0, |l| l.chars().count());
loop {
let owed_left = usize::from(first > 0) * TAB_ELISION;
let owed_right = usize::from(last + 1 < labels.len()) * TAB_ELISION;
if let Some(next) = labels.get(last + 1) {
let cost = TAB_GAP + next.chars().count();
let still_owed = usize::from(last + 2 < labels.len()) * TAB_ELISION;
if used + cost + owed_left + still_owed <= budget {
used += cost;
last += 1;
continue;
}
}
if first > 0 {
let cost = TAB_GAP + labels[first - 1].chars().count();
let still_owed = usize::from(first > 1) * TAB_ELISION;
if used + cost + still_owed + owed_right <= budget {
used += cost;
first -= 1;
continue;
}
}
return (first, last);
}
}
pub(super) fn group_header_line(
pane: &SecretsPane,
source: &Source,
palette: Palette,
width: u16,
) -> Line<'static> {
let count = pane.model.rows_for(source).count();
let text = match source {
Source::Operator => format!("\u{25be} operator \u{d7}{count} \u{b7} you set these"),
Source::Namespace(namespace) => {
let marker = if pane.collapsed.contains(namespace) {
'\u{25b8}'
} else {
'\u{25be}'
};
format!(
"{marker} {namespace} (dog) \u{d7}{count} \u{b7} pushed by a provider \u{b7} \
read-only here"
)
}
};
Line::from(Span::styled(fit(&text, width), palette.muted()))
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use crate::lookout::view::fixtures;
#[test]
fn a_stale_roll_states_its_age() {
let buffer = fixtures::render_secrets_with_roll_age(Duration::from_secs(3600));
let text = fixtures::rows_of(&buffer);
assert!(
text.iter().any(|l| l.contains("roll") && l.contains("1h")),
"a failed roll write only warns, so the age is the only signal: {text:?}"
);
}
}