diff --git a/app/src/context.rs b/app/src/context.rs index c6caa25d..83e8dcd5 100644 --- a/app/src/context.rs +++ b/app/src/context.rs @@ -62,6 +62,9 @@ impl Ctx { let Rect { x, y, .. } = self.area(&self.input.position); return Some((x + 1 + self.input.cursor(), y + 1)); } + if let Some((x, y)) = self.help.cursor() { + return Some((x, y)); + } None } @@ -69,7 +72,7 @@ impl Ctx { pub(super) fn layer(&self) -> KeymapLayer { if self.which.visible { KeymapLayer::Which - } else if self.help.visible { + } else if self.help.visible() { KeymapLayer::Help } else if self.input.visible { KeymapLayer::Input diff --git a/app/src/executor.rs b/app/src/executor.rs index 24b35b6f..f1b0f3b1 100644 --- a/app/src/executor.rs +++ b/app/src/executor.rs @@ -15,10 +15,12 @@ impl Executor { return cx.which.press(key); } - if layer == KeymapLayer::Input && cx.input.mode() == InputMode::Insert { - if let Some(c) = key.plain() { - return cx.input.type_(c); - } + if layer == KeymapLayer::Input && cx.input.type_(&key) { + return true; + } + + if layer == KeymapLayer::Help && cx.help.type_(&key) { + return true; } for Control { on, exec, .. } in KEYMAP.get(layer) { @@ -245,10 +247,7 @@ impl Executor { "help" => cx.help.toggle(cx.layer()), _ => false, }, - InputMode::Insert => match exec.cmd.as_str() { - "backspace" => cx.input.backspace(), - _ => false, - }, + InputMode::Insert => false, } } diff --git a/app/src/help/bindings.rs b/app/src/help/bindings.rs index 69dc7025..e8e574fb 100644 --- a/app/src/help/bindings.rs +++ b/app/src/help/bindings.rs @@ -17,35 +17,18 @@ impl Widget for Bindings<'_> { return; } - let col1 = bindings - .iter() - .map(|c| { - let item = ListItem::new(c.on.iter().map(ToString::to_string).collect::()); - item - }) - .collect::>(); + let col1 = bindings.iter().map(|c| ListItem::new(c.on())).collect::>(); - let col2 = bindings - .iter() - .map(|c| { - let item = ListItem::new(c.exec()); - item - }) - .collect::>(); + let col2 = bindings.iter().map(|c| ListItem::new(c.exec())).collect::>(); let col3 = bindings .iter() - .map(|c| { - let item = ListItem::new(if let Some(ref desc) = c.desc { desc } else { "-" }); - item - }) + .map(|c| ListItem::new(if let Some(ref desc) = c.desc { desc } else { "-" })) .collect::>(); let chunks = layout::Layout::new() .direction(Direction::Horizontal) - .constraints( - [Constraint::Ratio(1, 9), Constraint::Ratio(4, 9), Constraint::Ratio(4, 9)].as_ref(), - ) + .constraints([Constraint::Ratio(2, 10), Constraint::Ratio(3, 10), Constraint::Ratio(5, 10)]) .split(area); let cursor = self.cx.help.rel_cursor() as u16; diff --git a/app/src/help/layout.rs b/app/src/help/layout.rs index f2250d6e..140ff44a 100644 --- a/app/src/help/layout.rs +++ b/app/src/help/layout.rs @@ -19,8 +19,10 @@ impl<'a> Widget for Layout<'a> { .split(area); Clear.render(area, buf); - Paragraph::new("manager.help") - .style(Style::new().fg(Color::Rgb(35, 39, 59)).bg(Color::Rgb(200, 211, 248))) + + let help = &self.cx.help; + Paragraph::new(help.keyword().unwrap_or_else(|| format!("{}.help", help.layer()))) + .style(Style::new().fg(Color::Black).bg(Color::White)) .render(chunks[1], buf); Bindings::new(self.cx).render(chunks[0], buf); diff --git a/app/src/root.rs b/app/src/root.rs index 3e1cc5cd..cb612eb1 100644 --- a/app/src/root.rs +++ b/app/src/root.rs @@ -34,7 +34,7 @@ impl<'a> Widget for Root<'a> { input::Input::new(self.cx).render(area, buf); } - if self.cx.help.visible { + if self.cx.help.visible() { help::Layout::new(self.cx).render(area, buf); } diff --git a/config/docs/keymap.md b/config/docs/keymap.md index 18660dc8..fff92991 100644 --- a/config/docs/keymap.md +++ b/config/docs/keymap.md @@ -197,7 +197,6 @@ - `--submit`: Submit the input. - escape: Cancel insert mode and enter normal mode. -- backspace: Delete the character before the cursor. ## Help diff --git a/config/preset/keymap.toml b/config/preset/keymap.toml index c56ba252..a23a61c3 100644 --- a/config/preset/keymap.toml +++ b/config/preset/keymap.toml @@ -111,8 +111,8 @@ keymap = [ [tasks] keymap = [ - { on = [ "" ], exec = "close", desc = "Hide the task manager" }, { on = [ "" ], exec = "close", desc = "Hide the task manager" }, + { on = [ "" ], exec = "close", desc = "Hide the task manager" }, { on = [ "w" ], exec = "close", desc = "Hide the task manager" }, { on = [ "k" ], exec = "arrow -1", desc = "Move cursor up" }, @@ -152,7 +152,6 @@ keymap = [ { on = [ "" ], exec = "close", desc = "Cancel input" }, { on = [ "" ], exec = "close --submit", desc = "Submit the input" }, { on = [ "" ], exec = "escape", desc = "Go back the normal mode, or cancel input" }, - { on = [ "" ], exec = "backspace", desc = "Delete the character before the cursor" }, # Mode { on = [ "i" ], exec = "insert", desc = "Enter insert mode" }, @@ -196,8 +195,9 @@ keymap = [ [help] keymap = [ - { on = [ "" ], exec = "close", desc = "Hide the help" }, { on = [ "" ], exec = "escape", desc = "Clear the filter, or hide the help" }, + { on = [ "q" ], exec = "close", desc = "Exit the process" }, + { on = [ "" ], exec = "close", desc = "Hide the help" }, # Navigation { on = [ "k" ], exec = "arrow -1", desc = "Move cursor up" }, diff --git a/config/src/keymap/control.rs b/config/src/keymap/control.rs index 6f856ffc..afe50ca5 100644 --- a/config/src/keymap/control.rs +++ b/config/src/keymap/control.rs @@ -13,6 +13,9 @@ pub struct Control { } impl Control { + #[inline] + pub fn on(&self) -> String { self.on.iter().map(ToString::to_string).collect() } + #[inline] pub fn exec(&self) -> String { self.exec.iter().map(|e| e.to_string()).collect::>().join("; ") @@ -22,4 +25,11 @@ impl Control { pub fn desc_or_exec(&self) -> Cow { if let Some(ref s) = self.desc { Cow::Borrowed(s) } else { self.exec().into() } } + + #[inline] + pub fn contains(&self, s: &str) -> bool { + self.desc.as_ref().map(|d| d.contains(s)).unwrap_or(false) + || self.exec().contains(s) + || self.on().contains(s) + } } diff --git a/config/src/keymap/key.rs b/config/src/keymap/key.rs index 1c5c8ec8..aabbf485 100644 --- a/config/src/keymap/key.rs +++ b/config/src/keymap/key.rs @@ -19,6 +19,11 @@ impl Key { _ => None, } } + + #[inline] + pub fn is_enter(&self) -> bool { + matches!(self, Key { code: KeyCode::Enter, shift: false, ctrl: false, alt: false }) + } } impl Default for Key { diff --git a/config/src/keymap/keymap.rs b/config/src/keymap/keymap.rs index 9e4dc020..d4ef0795 100644 --- a/config/src/keymap/keymap.rs +++ b/config/src/keymap/keymap.rs @@ -1,3 +1,5 @@ +use std::fmt::{self, Display}; + use serde::{Deserialize, Deserializer}; use super::Control; @@ -12,16 +14,6 @@ pub struct Keymap { pub help: Vec, } -#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] -pub enum KeymapLayer { - Manager, - Tasks, - Select, - Input, - Help, - Which, -} - impl<'de> Deserialize<'de> for Keymap { fn deserialize(deserializer: D) -> Result where @@ -68,3 +60,27 @@ impl Keymap { } } } + +#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)] +pub enum KeymapLayer { + #[default] + Manager, + Tasks, + Select, + Input, + Help, + Which, +} + +impl Display for KeymapLayer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + KeymapLayer::Manager => write!(f, "manager"), + KeymapLayer::Tasks => write!(f, "tasks"), + KeymapLayer::Select => write!(f, "select"), + KeymapLayer::Input => write!(f, "input"), + KeymapLayer::Help => write!(f, "help"), + KeymapLayer::Which => write!(f, "which"), + } + } +} diff --git a/core/src/help/help.rs b/core/src/help/help.rs index e34226c9..522b67f4 100644 --- a/core/src/help/help.rs +++ b/core/src/help/help.rs @@ -1,13 +1,19 @@ -use config::{keymap::{Control, KeymapLayer}, KEYMAP}; +use config::{keymap::{Control, Key, KeymapLayer}, KEYMAP}; use shared::Term; +use unicode_width::UnicodeWidthStr; use super::HELP_MARGIN; -use crate::emit; +use crate::{emit, input::Input}; #[derive(Default)] pub struct Help { - pub visible: bool, - bindings: Vec, + visible: bool, + layer: KeymapLayer, + bindings: Vec, + + // Filter + keyword: Option, + in_filter: Option, offset: usize, cursor: usize, @@ -19,16 +25,35 @@ impl Help { pub fn toggle(&mut self, layer: KeymapLayer) -> bool { self.visible = !self.visible; - self.bindings = if self.visible { KEYMAP.get(layer).clone() } else { Vec::new() }; + self.layer = layer; + + self.keyword = Some(String::new()); + self.in_filter = None; + self.filter_apply(); + + self.offset = 0; + self.cursor = 0; emit!(Peek); // Show/hide preview for images true } - pub fn escape(&mut self) -> bool { todo!() } + pub fn escape(&mut self) -> bool { + if self.in_filter.is_some() { + self.in_filter = None; + self.filter_apply(); + true + } else { + self.toggle(self.layer) + } + } #[inline] pub fn arrow(&mut self, step: isize) -> bool { + let len = self.bindings.len(); + self.offset = self.offset.min(len); + self.cursor = self.cursor.min(len.saturating_sub(1)); + if step > 0 { self.next(step as usize) } else { self.prev(step.unsigned_abs()) } } @@ -60,17 +85,86 @@ impl Help { old != self.cursor } + pub fn filter(&mut self) -> bool { + self.in_filter = Some(Default::default()); + self.filter_apply(); + true + } + + fn filter_apply(&mut self) -> bool { + let kw = self.in_filter.as_ref().map(|i| i.value()).filter(|v| !v.is_empty()); + if self.keyword.as_deref() == kw { + return false; + } + + if let Some(kw) = kw { + self.bindings = KEYMAP.get(self.layer).iter().filter(|&c| c.contains(kw)).cloned().collect(); + } else { + self.bindings = KEYMAP.get(self.layer).clone(); + } + + self.keyword = kw.map(|s| s.to_owned()); + self.arrow(0); + true + } + + pub fn type_(&mut self, key: &Key) -> bool { + let Some(input) = &mut self.in_filter else { + return false; + }; + + if key.is_enter() { + self.in_filter = None; + return true; + } + + if input.type_(key) { + return self.filter_apply(); + } + + false + } +} + +impl Help { + // --- Visible + #[inline] + pub fn visible(&self) -> bool { self.visible } + + // --- Layer + #[inline] + pub fn layer(&self) -> KeymapLayer { self.layer } + + // --- Keyword + #[inline] + pub fn keyword(&self) -> Option { + self + .in_filter + .as_ref() + .map(|i| i.value()) + .or(self.keyword.as_deref()) + .map(|s| format!("/{}", s)) + } + + // --- Bindings #[inline] pub fn window(&self) -> &[Control] { let end = (self.offset + Self::limit()).min(self.bindings.len()); &self.bindings[self.offset..end] } - pub fn filter(&mut self) -> bool { todo!() } -} - -impl Help { // --- Cursor + #[inline] + pub fn cursor(&self) -> Option<(u16, u16)> { + if !self.visible || self.in_filter.is_none() { + return None; + } + if let Some(kw) = self.keyword() { + return Some((kw.width() as u16, Term::size().rows)); + } + None + } + #[inline] pub fn rel_cursor(&self) -> usize { self.cursor - self.offset } } diff --git a/core/src/input/input.rs b/core/src/input/input.rs index 2f009947..31752fb7 100644 --- a/core/src/input/input.rs +++ b/core/src/input/input.rs @@ -1,6 +1,8 @@ use std::ops::Range; use anyhow::{anyhow, Result}; +use config::keymap::Key; +use crossterm::event::KeyCode; use shared::CharKind; use tokio::sync::oneshot::Sender; use unicode_width::UnicodeWidthStr; @@ -177,8 +179,23 @@ impl Input { self.move_(snap.len() as isize) } + pub fn type_(&mut self, key: &Key) -> bool { + if self.mode() != InputMode::Insert { + return false; + } + + if let Some(c) = key.plain() { + return self.type_char(c); + } + + match key { + Key { code: KeyCode::Backspace, shift: false, ctrl: false, alt: false } => self.backspace(), + _ => false, + } + } + #[inline] - pub fn type_(&mut self, c: char) -> bool { + pub fn type_char(&mut self, c: char) -> bool { let mut bits = [0; 4]; self.type_str(c.encode_utf8(&mut bits)) } @@ -253,7 +270,7 @@ impl Input { self.insert(!before); for c in s.to_string_lossy().chars() { - self.type_(c); + self.type_char(c); } self.escape(); true diff --git a/cspell.json b/cspell.json index ed49cb39..95321d51 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord"],"language":"en","version":"0.2"} +{"language":"en","flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","readline"],"version":"0.2"}