use super::super::super::app::{App, Body, Control, InputMode, Link, Settings, retrying_sentence};
use super::super::super::pane_bleats::BleatsPane;
use super::super::super::textbox::TextBox;
use super::super::cell;
use super::super::flock::fit;
use super::action_gate::{CONTROL_ENABLED_LABEL, READ_ONLY_LABEL, confirm_prompt, in_flight_text};
use super::key_hint::{
BLEATS_HINT, CLOSE_DIALOG_HINT, EntryActions, FROZEN_HINT, catalogue_hint, env_armed, hint_for,
modal_keys, pane_editor, pane_hint, pane_screen, secrets_armed, secrets_hint, secrets_typing,
sheep_pane_hint,
};
use ratatui::style::Style;
use ratatui::text::{Line, Span};
pub(in crate::lookout::view) const TEXT_ENTRY_HINT: &str = "enter applies esc cancels";
fn entry_line(buffer: &TextBox, secret: bool, label: &str, keys: &str, width: u16) -> String {
buffer.line(secret, &format!("{label} "), &format!(" {keys}"), width)
}
#[must_use]
pub fn banner_line(app: &App, width: u16) -> Option<Line<'static>> {
let palette = app.palette();
match app.link() {
Link::Live => None,
Link::Retrying { attempt } => Some(Line::from(Span::styled(
retrying_sentence(*attempt),
palette.attention(),
))),
Link::Lost { .. } => Some(Line::from(Span::styled(
fit(
&format!(
" shep lookout {} · the dashboard stays up so you can read what it had · it will not exit on its own",
app.place()
),
width,
),
palette.muted(),
))),
}
}
#[must_use]
pub fn status_line(app: &App, width: u16) -> Line<'static> {
let palette = app.palette();
let right = if matches!(app.link(), Link::Lost { .. }) {
"\u{2588} frozen"
} else if app.bleats_pane().is_some_and(BleatsPane::following) {
"\u{2588} following"
} else {
match app.control() {
Control::ReadOnly => READ_ONLY_LABEL,
Control::Allowed => CONTROL_ENABLED_LABEL,
}
};
let right_len = u16::try_from(right.chars().count()).unwrap_or(0);
let left_width = width.saturating_sub(right_len).saturating_sub(1);
let (left, left_style) = if let Some(modal) = app.modal() {
(
modal_keys(modal, app.modal_scrolls()).to_string(),
palette.attention(),
)
} else if let Some(prompt) = app.settings().and_then(Settings::pending) {
let text = if prompt.sent {
format!("{} sent, waiting for the shepherd", prompt.text)
} else {
format!("{} enter confirms, any other key cancels", prompt.text)
};
(text, palette.attention())
} else if let Some(buffer) = app.bleats_pane().and_then(BleatsPane::match_editing) {
(
entry_line(buffer, false, "match", TEXT_ENTRY_HINT, left_width),
palette.attention(),
)
} else if let Some(buffer) = app
.sheep_pane()
.and_then(|pane| pane.feed().match_editing())
{
(
entry_line(buffer, false, "match", TEXT_ENTRY_HINT, left_width),
palette.attention(),
)
} else if let Some(editor) = app.config_pane().and_then(pane_editor) {
(
entry_line(
editor.buffer,
editor.secret,
&editor.label,
editor.keys,
left_width,
),
palette.attention(),
)
} else if let Some(action) = app.action().filter(|a| !a.sent) {
(confirm_prompt(&action), palette.attention())
} else if let Some((settings, label, buffer)) = app.settings().and_then(|settings| {
settings
.typing()
.map(|(label, buffer)| (settings, label, buffer))
}) {
(
entry_line(
buffer,
settings.typing_is_secret(),
&format!("editing {label}"),
TEXT_ENTRY_HINT,
left_width,
),
palette.attention(),
)
} else if let Some(key) = app.config_pane().and_then(env_armed) {
(
format!("delete {key}? enter confirms, any other key cancels"),
palette.attention(),
)
} else if let Some(editor) = secrets_typing(app) {
(
entry_line(
editor.buffer,
editor.secret,
&editor.label,
editor.keys,
left_width,
),
palette.attention(),
)
} else if let Some(key) = secrets_armed(app) {
(
format!("delete {key}? enter confirms, any other key cancels"),
palette.attention(),
)
} else if app.mode() == InputMode::Text {
(
entry_line(
app.filter_box(),
false,
"filter",
&format!("{TEXT_ENTRY_HINT} ctrl-c quits"),
left_width,
),
palette.attention(),
)
} else if let Some(notice) = app.notice() {
(
with_long_actions(app, notice.to_string(), left_width, false),
if notice.is_grave() {
palette.refusal()
} else {
palette.attention()
},
)
} else if let Some(action) = app.action() {
let text = in_flight_text(&action);
(text, palette.attention())
} else {
body_hint(app, left_width)
};
let ground = palette.ground();
Line::from(vec![
Span::styled(fit(&left, left_width), left_style.patch(ground)),
Span::styled(format!(" {right}"), palette.muted().patch(ground)),
])
}
#[must_use]
fn body_hint(app: &App, width: u16) -> (String, Style) {
let (keys, style, is_dashboard) = screen_hint(app);
let text = with_dog_job(app, &keys, width);
let text = with_long_actions(app, text, width, true);
if is_dashboard {
(with_flock_hint(app, text, width), style)
} else {
(text, style)
}
}
fn screen_hint(app: &App) -> (String, Style, bool) {
let palette = app.palette();
if app.close_dialog().is_some() {
return (CLOSE_DIALOG_HINT.to_string(), palette.muted(), false);
}
if let Some(pane) = app.config_pane() {
return (
pane_hint(app.control(), pane_screen(pane)).to_string(),
palette.attention(),
false,
);
}
if app.bleats_pane().is_some() {
return (BLEATS_HINT.to_string(), palette.attention(), false);
}
if matches!(app.body(), Body::Secrets(_)) {
return (secrets_hint(app.control()), palette.attention(), false);
}
if let Body::Catalogue(pane) = app.body() {
let actions = pane.entry.as_ref().map(|name| {
let row = app
.catalogue_rows()
.iter()
.find(|row| row.dog.adopt_as == *name);
EntryActions {
install: row.is_some_and(|row| app.can_install(row)),
upgrade: row.is_some_and(|row| app.can_upgrade(row)),
delete: row.is_some_and(|row| app.can_delete(row)),
}
});
return (catalogue_hint(actions), palette.attention(), false);
}
if app.sheep_pane().is_some() {
return (
sheep_pane_hint(app.control()).to_string(),
palette.attention(),
false,
);
}
if app.settings().is_none() && !app.filter().is_empty() {
return (
format!("filter \"{}\" / edit esc clear", app.filter()),
palette.muted(),
false,
);
}
if matches!(app.link(), Link::Lost { .. }) {
return (FROZEN_HINT.to_string(), palette.attention(), false);
}
(
hint_for(app.control(), app.settings().is_some(), app.grouping()),
palette.attention(),
app.settings().is_none(),
)
}
fn with_dog_job(app: &App, hint: &str, width: u16) -> String {
let Some(job) = app.dog_job_line() else {
return hint.to_owned();
};
let joined = format!("{job} {hint}");
let Ok(needed) = u16::try_from(joined.chars().count()) else {
return hint.to_owned();
};
if needed <= width {
joined
} else {
hint.to_owned()
}
}
fn with_long_actions(app: &App, text: String, width: u16, result_wins: bool) -> String {
let fits = |line: &str| u16::try_from(line.chars().count()).is_ok_and(|needed| needed <= width);
let result = app.long_action_result();
let running = app.long_actions_running();
let items: Vec<&str> = result.into_iter().chain(running.as_deref()).collect();
if items.is_empty() {
return text;
}
let joined = format!("{} {text}", items.join(" "));
if fits(&joined) {
joined
} else if let Some(result) = result.filter(|_| result_wins) {
result.to_owned()
} else {
text
}
}
fn with_flock_hint(app: &App, text: String, width: u16) -> String {
let Some(hint) = app.hint_line() else {
return text;
};
let joined = format!("{text} {hint}");
match u16::try_from(joined.chars().count()) {
Ok(needed) if needed <= width => joined,
_ => text,
}
}
#[must_use]
pub fn rule_line(style: Style, width: u16) -> Line<'static> {
Line::from(Span::styled(cell::rule(usize::from(width)), style))
}
#[cfg(test)]
mod tests;