use super::super::super::app::{App, Body, Control, Grouping, TypingWhat};
use super::super::super::pane::ConfigPane;
pub(super) const FROZEN_HINT: &str = "q quit h keymap j/k g/G move \
nothing you press can reach the shepherd";
pub(super) fn pane_editor(pane: &ConfigPane) -> Option<(String, &str)> {
if let Some(list) = pane.list() {
return match list.typing()? {
(Some(index), buffer) => Some((format!("{} {index} =", list.key()), buffer)),
(None, buffer) => Some((format!("new {} element", list.key()), buffer)),
};
}
if let Some(env) = pane.env_typing() {
return match env.key() {
Some(key) => Some((format!("env {key} ="), env.buffer())),
None => Some(("new env KEY=value".to_owned(), env.buffer())),
};
}
let typing = pane.typing()?;
Some((format!("editing {}", typing.key), typing.buffer.as_str()))
}
pub(super) fn secrets_typing(app: &App) -> Option<(String, &str)> {
let Body::Secrets(pane) = app.body() else {
return None;
};
let typing = pane.typing.as_ref()?;
let label = match &typing.what {
TypingWhat::NewKey => "new key".to_string(),
TypingWhat::ValueFor(key) => format!("value for {key}"),
};
Some((label, typing.buffer.as_str()))
}
pub(super) fn secrets_armed(app: &App) -> Option<&str> {
let Body::Secrets(pane) = app.body() else {
return None;
};
pane.armed.as_ref().map(|a| a.key.as_str())
}
pub(super) const BLEATS_HINT: &str = "esc back j/k line ctrl-d/u page G end \
/ search n/N match f follow w wrap o out/err/both m level";
pub(super) const CLOSE_DIALOG_HINT: &str =
"the dialog owns the keyboard until it is answered or it expires";
pub(super) fn secrets_hint(control: Control) -> String {
match control {
Control::ReadOnly => {
"esc/S close \u{2190}/\u{2192} tab z collapse v reveal for 10s \
y copy q quit"
.to_string()
}
Control::Allowed => {
"esc/S close \u{2190}/\u{2192} tab z collapse v reveal for 10s \
\u{21b5} set a value D delete y copy q quit"
.to_string()
}
}
}
pub(super) const fn pane_hint(control: Control, screen: PaneScreen) -> &'static str {
match (control, screen) {
(Control::ReadOnly, PaneScreen::Fields) => {
"esc close j/k select g/G first/last r refresh h help * yours ! parked q quit"
}
(Control::Allowed, PaneScreen::Fields) => {
"esc close j/k select g/G first/last r refresh space cycle e edit d back to default u undo h help * yours ! parked q quit"
}
(Control::ReadOnly, PaneScreen::List) => {
"esc back j/k select g/G first/last r refresh q quit"
}
(Control::Allowed, PaneScreen::List) => {
"esc back j/k select g/G first/last r refresh e edit d remove K/J move u undo q quit"
}
(Control::ReadOnly, PaneScreen::Dogs) => {
"esc back j/k select g/G first/last r refresh enter open q quit"
}
(Control::Allowed, PaneScreen::Dogs) => {
"esc back j/k select g/G first/last r refresh enter open d remove q quit"
}
}
}
pub(super) const fn sheep_pane_hint(control: Control) -> &'static str {
match control {
Control::ReadOnly => "esc flock e edit J/K next sheep b full log / filter",
Control::Allowed => {
"esc flock e edit J/K next sheep x stop R restart L reload b full log / filter"
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum PaneScreen {
Fields,
List,
Dogs,
}
pub(super) fn pane_screen(pane: &ConfigPane) -> PaneScreen {
if pane.list().is_some() {
PaneScreen::List
} else if pane.dogs().is_some() {
PaneScreen::Dogs
} else {
PaneScreen::Fields
}
}
pub(super) fn hint_for(control: Control, settings_open: bool, grouping: Grouping) -> String {
if settings_open {
return match control {
Control::ReadOnly => "esc/s close j/k select g/G first/last r refresh q quit",
Control::Allowed => {
"esc/s close j/k select g/G first/last r refresh space cycle enter apply q quit"
}
}
.to_string();
}
let folds = match grouping {
Grouping::Flat => "F folds ",
Grouping::ByFold => "F flat z collapse ",
};
match control {
Control::ReadOnly => {
format!(
"q quit j/k select g/G first/last r refresh / filter {folds}s settings e edit * yours ! parked"
)
}
Control::Allowed => {
format!(
"q quit j/k select / filter {folds}x stop R restart L reload s settings e edit * yours ! parked"
)
}
}
}
#[cfg(test)]
mod tests {
use super::super::status_layout::status_line;
use super::super::super::super::app::{Control, Grouping};
use super::super::super::fixtures::{rendered, with_selection};
use super::*;
use crate::lookout::app::{KeyPress, Msg};
#[test]
fn the_frozen_hint_clauses_are_separated_by_exactly_three_spaces() {
assert!(
!FROZEN_HINT.contains(" "),
"a separator wider than three spaces: {FROZEN_HINT:?}"
);
assert_eq!(
FROZEN_HINT.matches(" ").count(),
3,
"three clause gaps, so three separators: {FROZEN_HINT:?}"
);
}
#[test]
fn the_frozen_hint_names_the_keymap_it_no_longer_refuses() {
assert!(FROZEN_HINT.contains("h keymap"), "{FROZEN_HINT}");
assert!(
FROZEN_HINT.contains("q quit") && FROZEN_HINT.contains("j/k g/G move"),
"{FROZEN_HINT}"
);
}
#[test]
fn the_frozen_hint_claims_no_blanket_refusal() {
assert!(
!FROZEN_HINT.contains("every other key"),
"the blanket-refusal claim is back, and it is false: {FROZEN_HINT}"
);
assert!(
FROZEN_HINT.contains("nothing you press can reach the shepherd"),
"{FROZEN_HINT}"
);
}
#[test]
fn the_legend_fits_wherever_the_cfg_column_is_drawn() {
let widest = [
hint_for(Control::ReadOnly, false, Grouping::Flat),
hint_for(Control::Allowed, false, Grouping::Flat),
]
.into_iter()
.map(|hint| hint.chars().count())
.max()
.expect("two hints");
let cfg_tier = usize::from(super::super::super::flock::cfg_tier_width());
assert!(
widest <= cfg_tier,
"the hint is {widest} wide and CFG draws from {cfg_tier}, so the \
legend truncates where the glyph still shows"
);
}
#[test]
fn the_dashboard_hint_says_what_the_cfg_glyphs_mean() {
for control in [Control::ReadOnly, Control::Allowed] {
let hint = hint_for(control, false, Grouping::Flat);
assert!(hint.contains("* yours"), "{hint}");
assert!(hint.contains("! parked"), "{hint}");
}
}
#[test]
fn the_hint_names_the_fold_keys_and_only_names_z_in_fold_view() {
for control in [Control::ReadOnly, Control::Allowed] {
let flat = hint_for(control, false, Grouping::Flat);
assert!(flat.contains("F folds"), "{flat}");
assert!(!flat.contains("z collapse"), "z does nothing here: {flat}");
let folded = hint_for(control, false, Grouping::ByFold);
assert!(folded.contains("F flat"), "{folded}");
assert!(folded.contains("z collapse"), "{folded}");
}
}
#[test]
fn every_pane_hint_names_the_movement_and_refresh_keys_it_binds() {
for screen in [PaneScreen::Fields, PaneScreen::List] {
for control in [Control::ReadOnly, Control::Allowed] {
let hint = pane_hint(control, screen);
for key in ["j/k select", "g/G first/last", "r refresh", "q quit"] {
assert!(hint.contains(key), "{control:?} {screen:?}: {hint:?}");
}
}
}
}
#[test]
fn the_field_lists_hint_carries_a_legend_for_its_own_flag_glyphs() {
for control in [Control::ReadOnly, Control::Allowed] {
let hint = pane_hint(control, PaneScreen::Fields);
assert!(hint.contains("* yours"), "{control:?}: {hint:?}");
assert!(hint.contains("! parked"), "{control:?}: {hint:?}");
let sub = pane_hint(control, PaneScreen::List);
assert!(!sub.contains('*'), "{control:?}: {sub:?}");
assert!(!sub.contains('!'), "{control:?}: {sub:?}");
}
}
#[test]
fn the_env_rows_movement_and_refresh_keys_do_what_the_hint_says() {
let mut app = super::super::super::fixtures::app_in_sheep_pane_with_control();
app.update(Msg::Key(KeyPress::SelectLast));
assert_eq!(
app.config_pane().unwrap().cursor(),
Some(crate::lookout::pane::PaneRow::AddEnv)
);
app.update(Msg::Key(KeyPress::SelectFirst));
assert_eq!(
app.config_pane().unwrap().cursor(),
Some(crate::lookout::pane::PaneRow::Field(0))
);
assert!(matches!(
app.update(Msg::Key(KeyPress::Refresh)),
crate::lookout::app::Effect::Send(_)
));
}
#[test]
fn the_sheep_panes_hint_drops_the_write_keys_under_read_only() {
use shep_core::protocol::ProcessInfo;
use shep_core::status::ProcStatus;
let mut app = with_selection(ProcessInfo::builder(9, "web", ProcStatus::Online).build());
let _ = app.update(Msg::Key(KeyPress::Confirm));
let bar = rendered(&status_line(&app, 200));
assert!(bar.contains("esc flock"), "got {bar}");
assert!(!bar.contains("x stop"), "got {bar}");
assert!(!bar.contains("R restart"), "got {bar}");
assert!(!bar.contains("L reload"), "got {bar}");
assert!(bar.contains("b full log"), "got {bar}");
assert!(bar.contains("/ filter"), "got {bar}");
}
}