1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
|
//! Keys every screen shares: which way the movement keys go, and how key
//! hints are drawn. A hint is the key in the accent colour, bold, then what
//! it does, dimmed; hints sit two spaces apart, and side panels pack as
//! many to a line as fit.
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::style::{Color, Stylize};
use ratatui::text::{Line, Span};
pub const ACCENT: Color = Color::Rgb(240, 200, 110);
pub const DIM: Color = Color::Rgb(120, 124, 142);
/// Vi keys, arrows, and the number pad with or without num lock.
pub fn direction(code: KeyCode) -> Option<(i32, i32)> {
Some(match code {
KeyCode::Left | KeyCode::Char('h' | '4') => (-1, 0),
KeyCode::Right | KeyCode::Char('l' | '6') => (1, 0),
KeyCode::Up | KeyCode::Char('k' | '8') => (0, -1),
KeyCode::Down | KeyCode::Char('j' | '2') => (0, 1),
KeyCode::Home | KeyCode::Char('y' | '7') => (-1, -1),
KeyCode::PageUp | KeyCode::Char('u' | '9') => (1, -1),
KeyCode::End | KeyCode::Char('b' | '1') => (-1, 1),
KeyCode::PageDown | KeyCode::Char('n' | '3') => (1, 1),
_ => return None,
})
}
/// A step, or with shift a run: which way, and whether to run. Shifted vi
/// keys come as capitals.
pub fn movement(key: KeyEvent) -> Option<(i32, i32, bool)> {
let (code, run) = match key.code {
KeyCode::Char(c) if c.is_ascii_uppercase() => (KeyCode::Char(c.to_ascii_lowercase()), true),
code => (code, key.modifiers.contains(KeyModifiers::SHIFT)),
};
direction(code).map(|(dx, dy)| (dx, dy, run))
}
/// One hint: "esc back".
fn hint(key: &str, what: &str) -> [Span<'static>; 2] {
[Span::raw(key.to_string()).fg(ACCENT).bold(), Span::raw(format!(" {what}")).fg(DIM)]
}
/// Hints in a row.
pub fn line(hints: &[(&str, &str)]) -> Line<'static> {
let mut spans = Vec::new();
for (i, &(key, what)) in hints.iter().enumerate() {
if i > 0 {
spans.push(Span::raw(" "));
}
spans.extend(hint(key, what));
}
Line::from(spans)
}
/// Hints packed into lines at most `width` wide, as many to a line as fit.
/// A hint is never split; one too wide for any line gets a line to itself.
pub fn packed(hints: &[(&str, &str)], width: u16) -> Vec<Line<'static>> {
let width = usize::from(width);
let mut lines: Vec<Line<'static>> = Vec::new();
for &(key, what) in hints {
let [k, w] = hint(key, what);
let needs = k.width() + w.width();
match lines.last_mut() {
Some(last) if last.width() + 2 + needs <= width => {
last.spans.extend([Span::raw(" "), k, w]);
}
_ => lines.push(Line::from(vec![k, w])),
}
}
lines
}
#[cfg(test)]
mod tests {
use super::*;
fn text(line: &Line) -> String {
line.spans.iter().map(|s| s.content.as_ref()).collect()
}
#[test]
fn hints_pack_without_splitting() {
let hints = [("f", "fire"), ("r", "reload"), ("esc", "menu: save and quit")];
let lines: Vec<String> = packed(&hints, 16).iter().map(text).collect();
assert_eq!(lines, ["f fire r reload", "esc menu: save and quit"]);
assert_eq!(text(&line(&hints[..2])), "f fire r reload");
}
#[test]
fn shift_or_a_capital_runs() {
let key = |code, modifiers| movement(KeyEvent::new(code, modifiers));
assert_eq!(key(KeyCode::Char('h'), KeyModifiers::NONE), Some((-1, 0, false)));
assert_eq!(key(KeyCode::Char('L'), KeyModifiers::SHIFT), Some((1, 0, true)));
assert_eq!(key(KeyCode::Down, KeyModifiers::SHIFT), Some((0, 1, true)));
assert_eq!(key(KeyCode::Char('G'), KeyModifiers::SHIFT), None);
}
}
|