use super::super::*;
impl App {
pub(in crate::lookout::app) fn reread_settings(&self) -> Effect {
match self.settings() {
Some(settings) if settings.pending.is_some() => Effect::None,
_ => Effect::LoadSettings,
}
}
pub(in crate::lookout::app) fn on_settings_key(&mut self, key: KeyPress) -> Effect {
self.notice = None;
match key {
KeyPress::Quit => return Effect::Quit,
KeyPress::Settings | KeyPress::Escape => {
if !self.disarm_settings_candidate() {
self.body = Body::FlockTable;
}
}
KeyPress::SelectUp
| KeyPress::SelectDown
| KeyPress::SelectFirst
| KeyPress::SelectLast => {
if !self.disarm_settings_candidate()
&& let Some(settings) = self.settings_mut()
{
match key {
KeyPress::SelectUp => settings.move_by(-1),
KeyPress::SelectDown => settings.move_by(1),
KeyPress::SelectFirst => settings.move_to_first(),
KeyPress::SelectLast => settings.move_to_last(),
_ => unreachable!(),
}
}
}
KeyPress::Cycle => return self.cycle_setting(),
KeyPress::Remove => return self.remove_setting(),
KeyPress::Confirm => return self.confirm_setting(),
KeyPress::Refresh => {
if self.disarm_settings_candidate() {
return Effect::None;
}
return Effect::LoadSettings;
}
KeyPress::Edit => {
if self.disarm_settings_candidate() {
return Effect::None;
}
if self.open_env_editor() {
return Effect::None;
}
return self.fold_dog_row();
}
KeyPress::Help => {
if self.disarm_settings_candidate() {
return Effect::None;
}
return self.open_keymap();
}
KeyPress::Action(_)
| KeyPress::FilterStart
| KeyPress::TextChar(_)
| KeyPress::TextBackspace
| KeyPress::TextEdit(_)
| KeyPress::TextApply
| KeyPress::TextAbandon
| KeyPress::StepUp
| KeyPress::StepDown
| KeyPress::FoldView
| KeyPress::Secrets
| KeyPress::Reveal
| KeyPress::Copy
| KeyPress::TabPrev
| KeyPress::TabNext
| KeyPress::ArmDelete
| KeyPress::Collapse => {}
KeyPress::StreamCycle
| KeyPress::LevelCycle
| KeyPress::PageDown
| KeyPress::PageUp
| KeyPress::FollowToggle
| KeyPress::WrapToggle
| KeyPress::MatchNext
| KeyPress::MatchPrev
| KeyPress::Bleats
| KeyPress::NextGroup
| KeyPress::PrevGroup
| KeyPress::NextField
| KeyPress::PrevField
| KeyPress::Group(_)
| KeyPress::Undo
| KeyPress::Continue => {}
}
Effect::None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lookout::view::fixtures;
#[test]
fn s_asks_for_the_file_before_the_screen_opens() {
let mut app = fixtures::full_app();
assert_eq!(
app.update(Msg::Key(KeyPress::Settings)),
Effect::LoadSettings
);
assert!(
app.settings().is_none(),
"nothing opens until the read lands"
);
}
#[test]
fn the_screen_opens_when_the_read_lands() {
let mut app = fixtures::full_app();
let _ = app.update(Msg::Key(KeyPress::Settings));
let _ = app.update(Msg::Settings {
result: Ok(fixtures::settings_snapshot()),
});
assert!(app.settings().is_some());
}
#[test]
fn a_read_that_failed_says_so_and_leaves_the_dashboard_up() {
let mut app = fixtures::full_app();
let _ = app.update(Msg::Key(KeyPress::Settings));
let _ = app.update(Msg::Settings {
result: Err("no such file".into()),
});
assert!(app.settings().is_none());
let notice = app.notice().expect("a failed read has to say so");
assert!(notice.is_grave());
assert!(notice.to_string().contains("no such file"));
}
#[test]
fn s_closes_the_screen_again() {
let mut app = fixtures::app_in_settings();
let _ = app.update(Msg::Key(KeyPress::Settings));
assert!(app.settings().is_none());
}
#[test]
fn escape_closes_the_screen_and_never_quits() {
let mut app = fixtures::app_in_settings();
assert_eq!(app.update(Msg::Key(KeyPress::Escape)), Effect::None);
assert!(app.settings().is_none());
}
#[test]
fn the_flock_cursor_and_the_filter_survive_the_swap() {
let mut app = fixtures::full_app();
let _ = app.update(Msg::Key(KeyPress::FilterStart));
for c in "web".chars() {
let _ = app.update(Msg::Key(KeyPress::TextChar(c)));
}
let _ = app.update(Msg::Key(KeyPress::TextApply));
let selected = app.selected();
let filter = app.filter().to_string();
let _ = app.update(Msg::Key(KeyPress::Settings));
let _ = app.update(Msg::Settings {
result: Ok(fixtures::settings_snapshot()),
});
let _ = app.update(Msg::Key(KeyPress::Settings));
assert_eq!(app.selected(), selected);
assert_eq!(app.filter(), filter);
}
#[test]
fn the_settings_cursor_starts_at_the_first_row_on_every_open() {
let mut app = fixtures::app_in_settings();
let _ = app.update(Msg::Key(KeyPress::SelectDown));
let _ = app.update(Msg::Key(KeyPress::SelectDown));
let _ = app.update(Msg::Key(KeyPress::Settings));
let _ = app.update(Msg::Key(KeyPress::Settings));
let _ = app.update(Msg::Settings {
result: Ok(fixtures::settings_snapshot()),
});
let first = app.settings().unwrap().rows()[0];
assert_eq!(app.settings().unwrap().cursor(), Some(first));
}
#[test]
fn the_cursor_moves_through_the_tree_and_into_the_dogs() {
let mut app = fixtures::app_in_settings();
let rows = app.settings().unwrap().rows();
for _ in 0..rows.len() - 1 {
let _ = app.update(Msg::Key(KeyPress::SelectDown));
}
assert_eq!(
app.settings().unwrap().cursor(),
Some(*rows.last().unwrap())
);
let _ = app.update(Msg::Key(KeyPress::SelectDown));
assert_eq!(
app.settings().unwrap().cursor(),
Some(*rows.last().unwrap())
);
assert!(
matches!(rows.last(), Some(SettingsRow::Dog(_))),
"the dog rows sit below the tree"
);
}
#[test]
fn an_action_key_from_the_dashboard_is_unreachable_while_the_screen_is_up() {
let mut app = fixtures::app_in_settings_with_control();
let _ = app.update(Msg::Key(KeyPress::Action(ActionVerb::Stop)));
assert!(app.action().is_none(), "no sheep confirm can arm from here");
}
fn write_routes() -> Vec<(&'static str, Option<&'static str>, Vec<KeyPress>)> {
use KeyPress::{Confirm, Cycle, Remove, SelectLast, TextApply, TextChar};
let typed = |text: &str| -> Vec<KeyPress> {
let mut keys = vec![Confirm];
keys.extend(text.chars().map(TextChar));
keys.extend([TextApply, Confirm]);
keys
};
vec![
(
"space on a cycled key",
Some("daemon.log_level"),
vec![Cycle, Confirm],
),
("the socket editor", Some("daemon.socket"), typed("/x")),
(
"the max_cron_sleep editor",
Some("daemon.max_cron_sleep"),
typed("9s"),
),
("a list editor", Some("hosts"), typed(",bob@host")),
("an entry editor", Some("interpreters.py"), typed("2")),
(
"the add-an-entry editor",
Some("interpreters+"),
typed("rb=ruby"),
),
(
"remove on an entry",
Some("interpreters.py"),
vec![Remove, Confirm],
),
(
"the whistle gate",
Some("whistle.allow_control"),
vec![Cycle, Confirm],
),
("space on a dog row", None, vec![SelectLast, Cycle, Confirm]),
]
}
#[test]
fn no_key_route_writes_the_config_while_the_gate_is_closed() {
for (what, start, keys) in write_routes() {
let mut app = fixtures::app_in_settings(); if let Some(key) = start {
fixtures::select_setting(&mut app, key);
}
let mut refused = false;
for key in keys {
let effect = app.update(Msg::Key(key));
assert!(
!matches!(
effect,
Effect::WriteSetting { .. } | Effect::WriteDog { .. }
),
"{what}: a read-only lookout reached {effect:?}"
);
refused |= app.notice().is_some_and(Notice::is_grave);
}
assert!(refused, "{what}: the refusal has to say why");
assert!(
app.settings().unwrap().pending().is_none(),
"{what}: nothing is left armed"
);
assert!(
app.settings().unwrap().typing().is_none(),
"{what}: no editor is left open"
);
}
}
#[test]
fn every_one_of_those_routes_writes_once_the_gate_is_open() {
for (what, start, keys) in write_routes() {
let mut app = fixtures::app_in_settings_with_control();
if let Some(key) = start {
fixtures::select_setting(&mut app, key);
}
let mut wrote = false;
for key in keys {
let effect = app.update(Msg::Key(key));
wrote |= matches!(
effect,
Effect::WriteSetting { .. } | Effect::WriteDog { .. }
);
}
assert!(wrote, "{what}: an open gate has to reach the write");
}
}
#[test]
fn a_read_only_lookout_opens_the_screen_and_refuses_the_edit_key() {
let mut app = fixtures::app_in_settings(); assert!(app.settings().is_some(), "reading shep.toml is not gated");
fixtures::select_setting(&mut app, "daemon.log_level");
let _ = app.update(Msg::Key(KeyPress::Cycle));
let notice = app.notice().expect("the refusal has to say why");
assert!(notice.is_grave());
}
#[test]
fn opening_the_screen_clears_an_action_armed_while_the_read_was_in_flight() {
let mut app = fixtures::allowed_app();
let _ = app.update(Msg::Key(KeyPress::Settings));
let _ = app.update(Msg::Key(KeyPress::Action(ActionVerb::Stop)));
assert!(
app.action().is_some(),
"the arm must still succeed before the read lands"
);
let _ = app.update(Msg::Settings {
result: Ok(fixtures::settings_snapshot()),
});
assert!(
app.action().is_none(),
"no armed action may survive the screen opening"
);
}
}