use core::ops::Range;
use shep_core::dogs::pane::{Pane, PaneAction, Row, Section};
#[cfg(test)]
mod tests;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Tab {
#[default]
Status,
Settings,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(in crate::lookout) enum Step {
Up,
Down,
First,
Last,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(in crate::lookout) struct Layout {
pub(in crate::lookout) total: usize,
row_lines: Vec<usize>,
}
impl Layout {
pub(in crate::lookout) fn line_of(&self, cursor: Option<usize>) -> Option<usize> {
cursor.and_then(|row| self.row_lines.get(row).copied())
}
}
pub(super) fn layout(pane: &Pane) -> Layout {
let mut at = Layout {
total: 1,
row_lines: Vec::new(),
};
let mut blank_last = true;
for section in &pane.sections {
if !blank_last {
at.total += 1;
}
blank_last = false;
match section {
Section::Fields(fields) => at.total += 1 + fields.fields.len().max(1),
Section::Table(table) if table.rows.is_empty() => at.total += 2,
Section::Table(table) => {
at.total += 2;
for _ in &table.rows {
at.row_lines.push(at.total);
at.total += 1;
}
}
_unknown => blank_last = true,
}
}
at
}
pub(in crate::lookout) const RESERVED: [char; 5] = ['q', 'j', 'k', 'g', 'G'];
#[derive(Debug, Clone, PartialEq, Eq)]
pub(in crate::lookout) struct PaneKey {
pub(in crate::lookout) letter: Option<char>,
pub(in crate::lookout) action: PaneAction,
pub(in crate::lookout) rows: Option<Range<usize>>,
}
pub(in crate::lookout) fn keyed(pane: &Pane, declared: &[String]) -> Vec<PaneKey> {
let mut found = Vec::new();
let mut first_row = 0;
for section in &pane.sections {
if let Section::Table(table) = section {
let rows = first_row..first_row + table.rows.len();
first_row = rows.end;
for action in &table.row_actions {
found.push((action, Some(rows.clone())));
}
}
}
found.extend(pane.actions.iter().map(|action| (action, None)));
let mut taken = Vec::new();
found
.into_iter()
.map(|(action, rows)| {
let letter = declared
.contains(&action.action)
.then(|| free_letter(&action.label, &taken))
.flatten();
taken.extend(letter);
PaneKey {
letter,
action: action.clone(),
rows,
}
})
.collect()
}
fn free_letter(label: &str, taken: &[char]) -> Option<char> {
let free = |letter: &char| !RESERVED.contains(letter) && !taken.contains(letter);
label
.chars()
.map(|letter| letter.to_ascii_lowercase())
.filter(char::is_ascii_lowercase)
.find(free)
.or_else(|| ('a'..='z').find(free))
}
pub(in crate::lookout) fn row_at(pane: &Pane, cursor: usize) -> Option<&Row> {
pane.sections
.iter()
.filter_map(|section| match section {
Section::Table(table) => Some(table.rows.iter()),
_fields_or_unknown => None,
})
.flatten()
.nth(cursor)
}
pub(in crate::lookout) fn follow_start(
cursor_line: Option<usize>,
total: usize,
room: usize,
) -> usize {
let Some(spare) = total.checked_sub(room) else {
return 0;
};
(cursor_line.unwrap_or(0) + 2)
.saturating_sub(room)
.min(spare)
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(super) struct StatusView {
tab: Tab,
cursor: Option<usize>,
scroll: Option<usize>,
room: usize,
}
impl StatusView {
pub(super) fn tab(self) -> Tab {
self.tab
}
pub(super) fn cursor(self) -> Option<usize> {
self.cursor
}
pub(super) fn scroll(self) -> Option<usize> {
self.scroll
}
pub(super) fn switch(&mut self) {
self.tab = match self.tab {
Tab::Status => Tab::Settings,
Tab::Settings => Tab::Status,
};
}
pub(super) fn set_room(&mut self, room: usize, layout: &Layout) {
self.room = room;
self.clamp_scroll(layout);
}
pub(super) fn step(&mut self, step: Step, layout: &Layout) {
let Some(last) = layout.row_lines.len().checked_sub(1) else {
self.cursor = None;
self.scroll_by(step, layout);
return;
};
let at = self.cursor.unwrap_or(0).min(last);
let line = matches!(step, Step::Up | Step::Down);
let blocked = match step {
Step::Up => at == 0,
Step::Down => at == last,
Step::First | Step::Last => false,
};
if line && (blocked || self.scroll.is_some()) {
self.cursor = Some(at);
self.scroll_by(step, layout);
return;
}
self.scroll = None;
self.cursor = Some(match step {
Step::Up => at.saturating_sub(1),
Step::Down => (at + 1).min(last),
Step::First => 0,
Step::Last => last,
});
}
pub(super) fn settle(&mut self, layout: &Layout) {
self.cursor = layout
.row_lines
.len()
.checked_sub(1)
.map(|last| self.cursor.unwrap_or(0).min(last));
self.clamp_scroll(layout);
}
fn furthest(self, layout: &Layout) -> usize {
layout.total.saturating_sub(self.room)
}
fn scroll_by(&mut self, step: Step, layout: &Layout) {
if self.room == 0 {
return;
}
let follows = follow_start(layout.line_of(self.cursor), layout.total, self.room);
let from = self.scroll.unwrap_or(follows);
let to = match step {
Step::Up => from.saturating_sub(1),
Step::Down => (from + 1).min(self.furthest(layout)),
Step::First => 0,
Step::Last => self.furthest(layout),
};
self.scroll = (to != follows).then_some(to);
}
fn clamp_scroll(&mut self, layout: &Layout) {
if self.room == 0 {
self.scroll = None;
return;
}
let furthest = self.furthest(layout);
self.scroll = self.scroll.map(|at| at.min(furthest));
}
}