Krolyxon
Commit 222ab2f

rmptuisrcpopupshelp.rs

133 lines · 3.7 KB · Rust

1use ratatui::{
2 Frame,
3 layout::{Constraint, Rect},
4 text::Span,
5 widgets::Row,
6};
7
8use super::{Popup, PopupState, frame_popup};
9use crate::{
10 action::{Action, Context},
11 config::KeyCombo,
12 screens::Ctx,
13 ui::list::ListView,
14};
15
16enum HelpRow {
17 Heading(String),
18 Binding {
19 keys: String,
20 description: &'static str,
21 },
22}
23
24/// Lists the key bindings, starting with the active screen's.
25pub struct HelpPopup {
26 rows: Vec<HelpRow>,
27 list: ListView,
28}
29
30impl HelpPopup {
31 pub fn new(screen: Context, ctx: &Ctx) -> Self {
32 let mut order = vec![screen, Context::Navigation, Context::Global];
33 let rest: Vec<Context> = Context::ALL
34 .iter()
35 .copied()
36 .filter(|c| !order.contains(c) && !is_screen_context(*c))
37 .collect();
38 order.extend(rest);
39 let mut rows = Vec::new();
40 for context in order {
41 let Some(actions) = ctx.config.keymap.bindings().get(&context) else {
42 continue;
43 };
44 let bound: Vec<_> = actions
45 .iter()
46 .filter(|(_, keys)| !keys.is_empty())
47 .collect();
48 if bound.is_empty() {
49 continue;
50 }
51 if !rows.is_empty() {
52 rows.push(HelpRow::Heading(String::new()));
53 }
54 rows.push(HelpRow::Heading(heading(context)));
55 for (action, keys) in bound {
56 rows.push(HelpRow::Binding {
57 keys: keys
58 .iter()
59 .map(ToString::to_string)
60 .collect::<Vec<_>>()
61 .join(", "),
62 description: action.description(),
63 });
64 }
65 }
66 Self {
67 rows,
68 list: ListView::default(),
69 }
70 }
71}
72
73fn is_screen_context(context: Context) -> bool {
74 !matches!(
75 context,
76 Context::Global | Context::Navigation | Context::Popup | Context::Input
77 )
78}
79
80fn heading(context: Context) -> String {
81 let name = context.name().replace('_', " ");
82 let mut chars = name.chars();
83 chars
84 .next()
85 .map(|c| c.to_uppercase().chain(chars).collect())
86 .unwrap_or_default()
87}
88
89impl Popup for HelpPopup {
90 fn draw(&mut self, frame: &mut Frame, area: Rect, ctx: &Ctx) {
91 let inner = frame_popup(
92 frame,
93 area,
94 70,
95 area.height.saturating_sub(4),
96 "Key bindings",
97 ctx,
98 );
99 let theme = ctx.theme();
100 let rows = &self.rows;
101 self.list.render(
102 frame,
103 inner,
104 None,
105 &[Constraint::Length(24), Constraint::Fill(1)],
106 rows.len(),
107 true,
108 theme,
109 |i| match &rows[i] {
110 HelpRow::Heading(title) => Row::new([Span::styled(title.clone(), theme.header())]),
111 HelpRow::Binding { keys, description } => Row::new([
112 Span::styled(format!(" {keys}"), theme.text()),
113 Span::styled(*description, theme.dim()),
114 ]),
115 },
116 );
117 }
118
119 fn handle_key(&mut self, key: KeyCombo, ctx: &mut Ctx) -> PopupState {
120 let keymap = &ctx.config.keymap;
121 match keymap.resolve(&[Context::Popup, Context::Navigation], key) {
122 Some(Action::Close) => PopupState::Closed,
123 Some(action) => {
124 self.list.navigate(action, self.rows.len());
125 PopupState::Open
126 }
127 None if keymap.resolve(&[Context::Global], key) == Some(Action::Help) => {
128 PopupState::Closed
129 }
130 None => PopupState::Open,
131 }
132 }
133}