use ratatui::text::{Line, Span};
use super::super::super::pane::{ConfigPane, ListPane, ListRow};
use super::super::super::theme::Palette;
use super::super::flock::{fit, mark};
use super::super::scroll::{self, Attempt};
use super::layout::{POSITION_W, body_width, mask_secret};
pub(super) fn list_lines(
pane: &ConfigPane,
list: &ListPane,
palette: Palette,
width: u16,
budget: usize,
) -> Vec<Line<'static>> {
let mut lines = vec![Line::from(Span::styled(
format!(
" {}",
fit(
&format!("{} {} (list)", pane.target().name(), list.key()),
body_width(width)
)
),
palette.muted(),
))];
let body_budget = budget - 1;
if body_budget == 0 {
return lines;
}
let secret = pane.fields().by_key(list.key()).is_some_and(|f| f.secret);
let rows = list.rows();
let cursor_row = list.view().cursor().min(rows.len().saturating_sub(1));
lines.extend(scroll::to_cursor(
cursor_row,
list.view().offset(),
|offset| list_body_from(list, palette, width, body_budget, offset, secret),
|| vec![list_line(list, cursor_row, true, width, palette, secret)],
));
lines
}
pub(super) fn list_line(
list: &ListPane,
index: usize,
selected: bool,
width: u16,
palette: Palette,
secret: bool,
) -> Line<'static> {
let body = body_width(width);
let position_w = POSITION_W.min(body);
let value_w = body.saturating_sub(position_w + 2);
let typed = selected
.then(|| list.typing())
.flatten()
.map(|(_, buffer)| buffer);
let (position, value) = match list.rows().get(index).copied() {
Some(ListRow::Item(item)) => (
format!("{item}"),
typed.map_or_else(
|| {
let raw = list
.elements()
.get(item)
.cloned()
.unwrap_or_else(|| "(unset)".to_owned());
mask_secret(secret, raw)
},
|buffer| format!("{buffer}\u{258f}"),
),
),
Some(ListRow::New) => match typed {
Some(buffer) => (String::new(), format!("{buffer}\u{258f}")),
None => (String::new(), "+ new".to_owned()),
},
None => return Line::default(),
};
let mut text = format!("{} ", mark(selected));
text.push_str(&fit(&position, position_w));
if value_w > 0 {
text.push_str(" ");
text.push_str(&fit(&value, value_w));
}
if matches!(list.rows().get(index), Some(ListRow::New)) {
return Line::from(Span::styled(text, palette.muted()));
}
Line::from(Span::raw(text))
}
pub(super) fn list_body_from(
list: &ListPane,
palette: Palette,
width: u16,
budget: usize,
offset: usize,
secret: bool,
) -> Attempt {
let rows = list.rows();
let total = rows.len();
let cursor_row = list.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 in offset..total {
if lines.len() + 1 + above + usize::from(index + 1 < total) > budget {
break;
}
lines.push(list_line(
list,
index,
index == cursor_row,
width,
palette,
secret,
));
drawn += 1;
}
let hidden_below = total.saturating_sub(offset + drawn);
if hidden_below > 0 {
lines.push(Line::from(Span::styled(
format!(" ... {hidden_below} below"),
palette.muted(),
)));
}
if offset > 0 {
lines.insert(
0,
Line::from(Span::styled(
format!(" ... {offset} above"),
palette.muted(),
)),
);
}
Attempt {
cursor_drawn: drawn > 0 && (offset..offset + drawn).contains(&cursor_row),
lines,
}
}
#[cfg(test)]
mod tests {
use super::super::fixtures::{
marked, pane_to, screen_at, secret_list_dog_pane, text_of, web_pane, web_with_args,
};
use super::super::pane_lines;
use super::*;
use crate::lookout::app::{KeyPress, Msg};
use crate::lookout::view::MIN_TERM_WIDTH;
use crate::lookout::view::fixtures;
use crate::output::width::visible_width;
fn rendered_list(pane: &ConfigPane, width: u16, height: u16) -> Vec<String> {
let mut pane = pane.clone();
pane.move_to_key("args");
pane.open_list();
text_of(&pane_lines(&pane, fixtures::plain(), width, height))
}
fn app_on_the_list_screen(args: &[&str]) -> crate::lookout::app::App {
let mut app = fixtures::with_selection(
shep_core::protocol::ProcessInfo::builder(
9,
"web",
shep_core::status::ProcStatus::Online,
)
.pid(Some(48_000))
.build(),
);
app.set_control_for_tests(crate::lookout::app::Control::Allowed);
app.update(Msg::Key(KeyPress::Edit));
app.update(Msg::Replied {
sent: crate::lookout::app::Sent::SheepConfig {
name: "web".to_string(),
},
result: Ok(shep_core::protocol::Response::SheepConfig(Box::new(
web_with_args(args),
))),
});
pane_to(&mut app, "args");
let _ = app.update(Msg::Key(KeyPress::Confirm));
app
}
#[test]
fn the_list_screen_shows_its_values_unlike_env() {
let pane = ConfigPane::sheep(web_with_args(&["--port"]));
let lines = rendered_list(&pane, 120, 20);
assert!(lines.iter().any(|l| l.contains("--port")), "{lines:?}");
assert!(
!lines.iter().any(|l| l.contains(r#"["--port"]"#)),
"the element is a row of its own, not the field list's JSON cell: {lines:?}"
);
}
#[test]
fn the_list_screen_masks_a_secret_arrays_elements() {
let text = text_of(&pane_lines(
&secret_list_dog_pane(),
fixtures::plain(),
120,
0,
));
assert!(
!text
.iter()
.any(|line| line.contains("ab12cd34") || line.contains("ef56gh78")),
"a secret element is never rendered: {text:?}"
);
assert!(
text.iter().filter(|line| line.contains("<set>")).count() == 2,
"both elements mask to <set>: {text:?}"
);
}
#[test]
fn the_list_sub_screen_at_a_comfortable_width() {
let lines = rendered_list(&web_pane(), 120, 0);
insta::assert_snapshot!("list_sub_screen", lines.join("\n"));
}
#[test]
fn the_list_sub_screen_never_outgrows_the_height_or_the_width_it_was_given() {
let mut pane = ConfigPane::sheep(web_with_args(&[
"--port",
"8080",
"--host",
"0.0.0.0",
"--verbose",
"--log",
"debug",
]));
pane.move_to_key("args");
pane.open_list();
pane.file_list_removal();
for height in 1..=20u16 {
pane.list_mut().unwrap().move_to_first();
let total = pane.list().unwrap().rows().len();
pane.list_mut()
.unwrap()
.set_rows(usize::from(height.saturating_sub(1)));
for step in 0..=total {
let text = text_of(&pane_lines(&pane, fixtures::plain(), 120, height));
assert!(
text.len() <= usize::from(height),
"height {height}, step {step}: {text:?}"
);
pane.list_mut().unwrap().move_by(1);
}
}
for width in MIN_TERM_WIDTH..=200 {
for line in text_of(&pane_lines(&pane, fixtures::plain(), width, 0)) {
assert!(
visible_width(&line) <= usize::from(width),
"width {width} drew {}: {line:?}",
visible_width(&line)
);
}
}
}
#[test]
fn the_list_cursor_survives_every_step_at_the_minimum_height() {
let args = [
"--port",
"8080",
"--host",
"0.0.0.0",
"--verbose",
"--log",
"debug",
"--quiet",
];
for height in [crate::lookout::view::flock::MIN_HEIGHT, 7, 8, 12] {
let mut app = app_on_the_list_screen(&args);
let total = app.config_pane().unwrap().list().unwrap().rows().len();
for step in 0..=total {
let text = screen_at(&mut app, height);
assert_eq!(marked(&text), 1, "height {height}, step {step}:\n{text}");
app.update(Msg::Key(KeyPress::SelectDown));
}
}
}
}