use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, BorderType, Borders, Clear, Padding, Paragraph};
use ratatui::crossterm::event::{KeyCode, KeyEvent};
use crate::theme::Theme;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Action {
None,
Toggle(&'static str),
Close,
}
#[derive(Debug)]
pub struct Picker {
selected: usize,
}
impl Picker {
pub fn new() -> Self {
Self { selected: 0 }
}
#[cfg(test)]
pub fn selected(&self) -> usize {
self.selected
}
pub fn handle_key(&mut self, key: KeyEvent) -> Action {
let names = crate::widgets::WIDGET_NAMES;
let last = names.len().saturating_sub(1);
match key.code {
KeyCode::Esc | KeyCode::Char('q' | 'w') | KeyCode::Enter => Action::Close,
KeyCode::Down | KeyCode::Char('j') => {
self.selected = self.selected.saturating_add(1).min(last);
Action::None
}
KeyCode::Up | KeyCode::Char('k') => {
self.selected = self.selected.saturating_sub(1);
Action::None
}
KeyCode::Home | KeyCode::Char('g') => {
self.selected = 0;
Action::None
}
KeyCode::End | KeyCode::Char('G') => {
self.selected = last;
Action::None
}
KeyCode::Char(' ') => names
.get(self.selected)
.copied()
.map_or(Action::None, Action::Toggle),
_ => Action::None,
}
}
pub fn render(
&self,
frame: &mut ratatui::Frame,
area: Rect,
theme: &Theme,
placed: impl Fn(&str) -> bool,
error: Option<&str>,
) {
let names = crate::widgets::WIDGET_NAMES;
let mut lines: Vec<Line> = Vec::new();
for (index, name) in names.iter().enumerate() {
let on = placed(name);
let here = index == self.selected;
let mark = if on { "■" } else { "□" };
let mark_style = Style::default().fg(if on { theme.accent } else { theme.track });
let name_style = if here {
Style::default()
.fg(theme.text)
.add_modifier(Modifier::REVERSED)
} else if on {
Style::default().fg(theme.text)
} else {
Style::default().fg(theme.muted)
};
lines.push(Line::from(vec![
Span::styled(
if here { " ▸ " } else { " " },
Style::default().fg(theme.accent),
),
Span::styled(format!("{mark} "), mark_style),
Span::styled(format!("{name:<10}"), name_style),
]));
}
lines.push(Line::from(""));
match error {
Some(error) => lines.push(Line::from(Span::styled(
format!(" {error}"),
Style::default().fg(theme.error),
))),
None => lines.push(Line::from(Span::styled(
" written to your config on close",
Style::default().fg(theme.muted),
))),
}
lines.push(Line::from(vec![
Span::styled(
" space",
Style::default().fg(theme.key).add_modifier(Modifier::BOLD),
),
Span::styled(" toggle ", Style::default().fg(theme.muted)),
Span::styled(
"esc",
Style::default().fg(theme.key).add_modifier(Modifier::BOLD),
),
Span::styled(" close", Style::default().fg(theme.muted)),
]));
let height = u16::try_from(lines.len())
.unwrap_or(u16::MAX)
.saturating_add(crate::frame::FRAME_HEIGHT);
let popup = crate::frame::centred(area, 40, height);
frame.render_widget(Clear, popup);
frame.render_widget(
Paragraph::new(lines).block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.border_focused))
.padding(Padding::horizontal(1))
.title_top(Line::from(Span::styled(
"PANELS",
Style::default()
.fg(theme.title)
.add_modifier(Modifier::BOLD),
))),
),
popup,
);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn press(picker: &mut Picker, code: KeyCode) -> Action {
picker.handle_key(KeyEvent::from(code))
}
#[test]
fn the_cursor_clamps_at_both_ends() {
let last = crate::widgets::WIDGET_NAMES.len() - 1;
let mut picker = Picker::new();
assert_eq!(picker.selected(), 0, "opens on the first widget");
press(&mut picker, KeyCode::Up);
assert_eq!(picker.selected(), 0, "the top does not wrap");
press(&mut picker, KeyCode::End);
assert_eq!(picker.selected(), last);
press(&mut picker, KeyCode::Down);
assert_eq!(picker.selected(), last, "the end does not wrap");
}
#[test]
fn space_names_the_widget_under_the_cursor() {
let mut picker = Picker::new();
assert_eq!(
press(&mut picker, KeyCode::Char(' ')),
Action::Toggle(crate::widgets::WIDGET_NAMES[0])
);
press(&mut picker, KeyCode::Down);
assert_eq!(
press(&mut picker, KeyCode::Char(' ')),
Action::Toggle(crate::widgets::WIDGET_NAMES[1])
);
}
#[test]
fn only_the_documented_keys_close_it() {
for code in [
KeyCode::Esc,
KeyCode::Char('q'),
KeyCode::Char('w'),
KeyCode::Enter,
] {
assert_eq!(press(&mut Picker::new(), code), Action::Close, "{code:?}");
}
for code in [KeyCode::Char('x'), KeyCode::Tab, KeyCode::Backspace] {
assert_eq!(press(&mut Picker::new(), code), Action::None, "{code:?}");
}
}
}