use ratatui::text::{Line, Span};
use super::super::super::pane::{ConfigPane, DogTableState, PaneRow};
use super::super::super::theme::Palette;
use super::super::flock::{fit, mark};
use super::super::scroll::{self, Attempt};
use super::body::row_line;
use super::draw::inherited_note;
use super::field_row::open_glyph;
use super::layout::body_width;
const NAME_W: u16 = 16;
const STATE_W: u16 = 9;
const INDENT: u16 = 2;
pub(super) fn dogs_body_lines(
pane: &ConfigPane,
palette: Palette,
width: u16,
budget: usize,
show_lands: bool,
) -> Vec<Line<'static>> {
let mut lines = Vec::new();
if let Some(dog) = pane.dogs_tab().armed() {
let sheep = pane.target().name();
lines.push(Line::from(Span::styled(
format!(
" {}",
fit(
&format!("remove {sheep}'s {dog} table? enter confirms, any other key cancels"),
body_width(width)
)
),
palette.attention(),
)));
}
let mut body_budget = budget.saturating_sub(lines.len());
if body_budget == 0 {
return lines;
}
let rows = pane.rows();
let note = inherited_note_for(pane, &rows).filter(|_| body_budget > 1);
if note.is_some() {
body_budget -= 1;
}
if rows.is_empty() {
let text = if pane.dogs_tab().is_listed() {
"no dog publishes a sheep schema, and this sheep carries no table"
} else {
"listing dogs..."
};
lines.push(Line::from(Span::styled(
format!(" {}", fit(text, body_width(width))),
palette.muted(),
)));
return lines;
}
let cursor_row = pane.view().cursor().min(rows.len() - 1);
lines.extend(scroll::to_cursor(
cursor_row,
pane.view().offset(),
|offset| dogs_body_from(pane, &rows, palette, width, body_budget, offset, show_lands),
|| {
vec![tab_row_line(
pane,
rows[cursor_row],
true,
width,
palette,
show_lands,
)]
},
));
if let Some(note) = note {
lines.push(Line::from(Span::styled(
format!(" {}", fit(¬e, body_width(width))),
palette.muted(),
)));
}
lines
}
fn inherited_note_for(pane: &ConfigPane, rows: &[PaneRow]) -> Option<String> {
let cursor = pane.view().cursor().min(rows.len().checked_sub(1)?);
let (PaneRow::Dog(dog) | PaneRow::Under { dog, .. }) = *rows.get(cursor)? else {
return None;
};
let row = pane.dogs_tab().rows().get(dog)?;
row.table()
.filter(|table| table.inherits_section())
.map(|_| inherited_note(row.name()))
}
fn tab_row_line(
pane: &ConfigPane,
row: PaneRow,
selected: bool,
width: u16,
palette: Palette,
show_lands: bool,
) -> Line<'static> {
match row {
PaneRow::Dog(index) => dog_line(pane, index, selected, width, palette),
PaneRow::Under { dog, at } => {
let Some(table) = pane.dogs_tab().rows().get(dog).and_then(|row| row.table()) else {
return Line::default();
};
let Some(inner) = table.rows().get(at).copied() else {
return Line::default();
};
let mut line = row_line(
table,
inner,
selected,
width.saturating_sub(INDENT),
palette,
show_lands,
);
line.spans.insert(
1.min(line.spans.len()),
Span::raw(" ".repeat(usize::from(INDENT))),
);
line
}
PaneRow::Field(_)
| PaneRow::Table(_)
| PaneRow::Entry(_)
| PaneRow::Choice { .. }
| PaneRow::Arm { .. }
| PaneRow::Env(_)
| PaneRow::AddEnv => Line::default(),
}
}
fn dog_line(
pane: &ConfigPane,
index: usize,
selected: bool,
width: u16,
palette: Palette,
) -> Line<'static> {
let Some(row) = pane.dogs_tab().rows().get(index) else {
return Line::default();
};
let body = body_width(width);
let name_w = NAME_W.min(body.saturating_sub(2));
let state_w = STATE_W.min(body.saturating_sub(name_w + 4));
let rest_w = body.saturating_sub(name_w + state_w + 6);
let keys = row.keys().join(", ");
let (state, rest, muted) = if row.is_waiting() {
("asking", String::new(), true)
} else {
match row.state() {
DogTableState::Set => ("set", keys, false),
DogTableState::Unset => ("-", "(no table)".to_owned(), true),
DogTableState::ReadOnly => (
"read-only",
format!("{keys} (publishes no sheep schema)"),
true,
),
DogTableState::NoSchema => ("-", "(publishes no sheep schema)".to_owned(), true),
}
};
let mut text = format!("{} ", open_glyph(row.is_open()));
text.push_str(&fit(row.name(), name_w));
if state_w > 0 {
text.push_str(" ");
text.push_str(&fit(state, state_w));
}
if rest_w > 0 {
text.push_str(" ");
text.push_str(&fit(&rest, rest_w));
}
let text = format!("{}{text}", mark(selected));
if muted {
Line::from(Span::styled(text, palette.muted()))
} else {
Line::from(Span::raw(text))
}
}
fn dogs_body_from(
pane: &ConfigPane,
rows: &[PaneRow],
palette: Palette,
width: u16,
budget: usize,
offset: usize,
show_lands: bool,
) -> Attempt {
let total = rows.len();
let cursor_row = pane.view().cursor().min(total.saturating_sub(1));
let above = usize::from(offset > 0);
let mut lines: Vec<Line<'static>> = Vec::new();
let mut drawn = 0usize;
for (index, row) in rows.iter().enumerate().skip(offset) {
if lines.len() + 1 + above + usize::from(index + 1 < total) > budget {
break;
}
lines.push(tab_row_line(
pane,
*row,
index == cursor_row,
width,
palette,
show_lands,
));
drawn += 1;
}
let hidden_below = total.saturating_sub(offset + drawn);
scroll::window_markers(&mut lines, hidden_below, offset, palette);
Attempt {
cursor_drawn: drawn > 0 && (offset..offset + drawn).contains(&cursor_row),
lines,
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use shep_core::config::AppConfig;
use shep_core::protocol::SheepConfigView;
use super::super::draw::pane_lines;
use super::super::fixtures::{marked, text_of};
use crate::lookout::pane::{ConfigPane, ListedDog, SheepDogEntry};
use crate::lookout::view::fixtures;
const TOKEN: &str = "sk-live-51Hx9Qa";
const PASSWORD: &str = "hunter2-legacy";
fn schema() -> serde_json::Value {
json!({
"type": "object",
"properties": {
"concurrency": { "type": "integer" },
"token": { "type": "string", "x-shep-secret": true },
},
})
}
fn listed(name: &str, schema: Option<serde_json::Value>) -> ListedDog {
ListedDog {
entry: SheepDogEntry {
name: name.to_owned(),
adopted_path: None,
schema,
standard: None,
},
asked: true,
}
}
fn web_dogs() -> ConfigPane {
let mut config = AppConfig {
name: "web".into(),
..AppConfig::default()
};
let table = |value: serde_json::Value| value.as_object().cloned().expect("a table");
config.dogs.insert(
"jobs".into(),
table(json!({ "concurrency": 7, "token": TOKEN })).into(),
);
config.dogs.insert(
"legacy".into(),
table(json!({ "url": format!("https://ops:{PASSWORD}@example.test") })).into(),
);
let mut pane = ConfigPane::sheep(SheepConfigView::new(config, Vec::new(), Vec::new()));
pane.set_group(9);
pane.list_dogs(vec![
listed("jobs", Some(schema())),
listed(
"deploy",
Some(json!({ "type": "object", "properties": {} })),
),
listed("legacy", None),
]);
pane
}
fn drawn(pane: &ConfigPane, width: u16, height: u16) -> String {
text_of(&pane_lines(pane, fixtures::plain(), width, height)).join("\n")
}
#[test]
fn a_collapsed_row_names_its_state_and_key_names_and_never_a_value() {
let text = drawn(&web_dogs(), 120, 0);
let row = |dog: &str| {
text.lines()
.find(|line| line.contains(dog))
.unwrap_or_else(|| panic!("{dog} is drawn: {text}"))
};
assert!(row("deploy").contains("(no table)"), "{text}");
assert!(
row("jobs").contains("set") && row("jobs").contains("concurrency, token"),
"{text}"
);
assert!(
row("legacy").contains("read-only") && row("legacy").contains("url"),
"{text}"
);
for secret in [TOKEN, PASSWORD, "7"] {
assert!(!text.contains(secret), "{secret} leaked: {text}");
}
}
#[test]
fn an_open_dog_draws_its_table_under_its_row_with_the_secret_masked() {
let mut pane = web_dogs();
let jobs = pane
.dogs_tab()
.rows()
.iter()
.position(|row| row.name() == "jobs")
.expect("listed");
pane.toggle_dog(jobs);
let text = drawn(&pane, 120, 0);
let lines: Vec<&str> = text.lines().collect();
let head = lines
.iter()
.position(|line| line.contains("\u{25be}") && line.contains("jobs"))
.unwrap_or_else(|| panic!("jobs is open: {text}"));
let concurrency = lines[head + 1];
assert!(
concurrency.contains("concurrency") && concurrency.contains('7'),
"{concurrency:?}"
);
let indent = |line: &str| line.len() - line.trim_start().len();
assert!(indent(concurrency) > indent(lines[head]), "{text}");
let token = lines[head + 2];
assert!(
token.contains("token") && token.contains("<set>"),
"{token:?}"
);
assert!(!text.contains(TOKEN), "{text}");
}
#[test]
fn the_cursor_row_is_drawn_at_every_height() {
let mut pane = web_dogs();
pane.set_rows(3);
pane.move_to_last();
for height in 3..=8 {
let text = drawn(&pane, 120, height);
assert_eq!(marked(&text), 1, "height {height}: {text}");
}
}
#[test]
fn a_tab_with_no_dogs_says_so() {
let mut pane = ConfigPane::sheep(SheepConfigView::new(
AppConfig {
name: "web".into(),
..AppConfig::default()
},
Vec::new(),
Vec::new(),
));
pane.set_group(9);
assert!(drawn(&pane, 120, 0).contains("listing dogs..."));
pane.list_dogs(Vec::new());
assert!(drawn(&pane, 120, 0).contains("no dog publishes a sheep schema"));
}
}