1use crossterm::{cursor::SetCursorStyle, event::KeyCode};
2use unicode_width::UnicodeWidthStr;
3use yazi_adapter::Dimension;
4use yazi_config::{KEYMAP, YAZI, keymap::{Chord, Key}};
5use yazi_macro::{render, render_and};
6use yazi_shared::Layer;
7
8use super::HELP_MARGIN;
9
10#[derive(Default)]
11pub struct Help {
12 pub visible: bool,
13 pub layer: Layer,
14 pub(super) bindings: Vec<&'static Chord>,
15
16 pub(super) keyword: String,
18 pub(super) in_filter: Option<yazi_widgets::input::Input>,
19
20 pub(super) offset: usize,
21 pub(super) cursor: usize,
22}
23
24impl Help {
25 #[inline]
26 pub fn limit() -> usize { Dimension::available().rows.saturating_sub(HELP_MARGIN) as usize }
27
28 pub fn toggle(&mut self, layer: Layer) {
29 self.visible = !self.visible;
30 self.layer = layer;
31
32 self.keyword = String::new();
33 self.in_filter = None;
34 self.filter_apply();
35
36 self.offset = 0;
37 self.cursor = 0;
38 render!();
39 }
40
41 pub fn r#type(&mut self, key: &Key) -> bool {
42 let Some(input) = &mut self.in_filter else {
43 return false;
44 };
45
46 match key {
47 Key { code: KeyCode::Esc, shift: false, ctrl: false, alt: false, super_: false } => {
48 self.in_filter = None;
49 render!();
50 }
51 Key { code: KeyCode::Enter, shift: false, ctrl: false, alt: false, super_: false } => {
52 self.in_filter = None;
53 return render_and!(true); }
55 Key { code: KeyCode::Backspace, shift: false, ctrl: false, alt: false, super_: false } => {
56 input.backspace(false);
57 }
58 _ => {
59 input.r#type(key);
60 }
61 }
62
63 self.filter_apply();
64 true
65 }
66
67 pub(super) fn filter_apply(&mut self) {
68 let kw = self.in_filter.as_ref().map_or("", |i| i.value());
69
70 if kw.is_empty() {
71 self.keyword = String::new();
72 self.bindings = KEYMAP.get(self.layer).iter().collect();
73 } else if self.keyword != kw {
74 self.keyword = kw.to_owned();
75 self.bindings = KEYMAP.get(self.layer).iter().filter(|&c| c.contains(kw)).collect();
76 }
77
78 self.arrow(0);
79 }
80}
81
82impl Help {
83 #[inline]
85 pub fn keyword(&self) -> Option<String> {
86 self
87 .in_filter
88 .as_ref()
89 .map(|i| i.value())
90 .or(Some(self.keyword.as_str()).filter(|&s| !s.is_empty()))
91 .map(|s| format!("Filter: {s}"))
92 }
93
94 #[inline]
96 pub fn window(&self) -> &[&Chord] {
97 let end = (self.offset + Self::limit()).min(self.bindings.len());
98 &self.bindings[self.offset..end]
99 }
100
101 #[inline]
103 pub fn cursor(&self) -> Option<(u16, u16)> {
104 if !self.visible || self.in_filter.is_none() {
105 return None;
106 }
107 if let Some(kw) = self.keyword() {
108 return Some((kw.width() as u16, Dimension::available().rows));
109 }
110 None
111 }
112
113 #[inline]
114 pub fn rel_cursor(&self) -> usize { self.cursor - self.offset }
115
116 #[inline]
117 pub fn cursor_shape(&self) -> SetCursorStyle {
118 if YAZI.input.cursor_blink {
119 SetCursorStyle::BlinkingBlock
120 } else {
121 SetCursorStyle::SteadyBlock
122 }
123 }
124}