yazi_core/help/
help.rs

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	// Filter
17	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); // Don't do the `filter_apply` below, since we already have the filtered results.
54			}
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	// --- Keyword
84	#[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	// --- Bindings
95	#[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	// --- Cursor
102	#[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}