diff --git a/src/config/keymap/exec.rs b/src/config/keymap/exec.rs index cdde7eb5..e21562c5 100644 --- a/src/config/keymap/exec.rs +++ b/src/config/keymap/exec.rs @@ -2,7 +2,7 @@ use std::{collections::BTreeMap, fmt}; use serde::{de::{self, Visitor}, Deserializer}; -#[derive(Debug, Default)] +#[derive(Clone, Debug, Default)] pub struct Exec { pub cmd: String, pub args: Vec, diff --git a/src/config/keymap/key.rs b/src/config/keymap/key.rs index 5263126e..1c86d4e1 100644 --- a/src/config/keymap/key.rs +++ b/src/config/keymap/key.rs @@ -102,3 +102,60 @@ impl TryFrom for Key { Ok(key) } } + +impl ToString for Key { + fn to_string(&self) -> String { + if let Some(c) = self.plain() { + return if self.shift { c.to_ascii_uppercase() } else { c }.to_string(); + } + + let mut s = "<".to_string(); + if self.ctrl { + s += "C-"; + } + if self.alt { + s += "A-"; + } + if self.shift && !matches!(self.code, KeyCode::Char(_)) { + s += "S-"; + } + + let code = match self.code { + KeyCode::Char(' ') => "Space", + KeyCode::Backspace => "Backspace", + KeyCode::Enter => "Enter", + KeyCode::Left => "Left", + KeyCode::Right => "Right", + KeyCode::Up => "Up", + KeyCode::Down => "Down", + KeyCode::Home => "Home", + KeyCode::End => "End", + KeyCode::PageUp => "PageUp", + KeyCode::PageDown => "PageDown", + KeyCode::Tab => "Tab", + KeyCode::Delete => "Delete", + KeyCode::Insert => "Insert", + KeyCode::F(1) => "F1", + KeyCode::F(2) => "F2", + KeyCode::F(3) => "F3", + KeyCode::F(4) => "F4", + KeyCode::F(5) => "F5", + KeyCode::F(6) => "F6", + KeyCode::F(7) => "F7", + KeyCode::F(8) => "F8", + KeyCode::F(9) => "F9", + KeyCode::F(10) => "F10", + KeyCode::F(11) => "F11", + KeyCode::F(12) => "F12", + KeyCode::Esc => "Esc", + + KeyCode::Char(c) => { + s.push(if self.shift { c.to_ascii_uppercase() } else { c }); + "" + } + _ => "Unknown", + }; + + s + code + ">" + } +} diff --git a/src/config/keymap/keymap.rs b/src/config/keymap/keymap.rs index 9d8a4757..140f2b43 100644 --- a/src/config/keymap/keymap.rs +++ b/src/config/keymap/keymap.rs @@ -3,8 +3,8 @@ use serde::{Deserialize, Deserializer}; use super::{Exec, Key}; use crate::config::MERGED_KEYMAP; -#[derive(Debug, Deserialize)] -pub struct Single { +#[derive(Clone, Debug, Deserialize)] +pub struct Control { pub on: Vec, #[serde(deserialize_with = "Exec::deserialize")] pub exec: Vec, @@ -12,10 +12,19 @@ pub struct Single { #[derive(Debug)] pub struct Keymap { - pub manager: Vec, - pub tasks: Vec, - pub select: Vec, - pub input: Vec, + pub manager: Vec, + pub tasks: Vec, + pub select: Vec, + pub input: Vec, +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +pub enum KeymapLayer { + Manager, + Tasks, + Select, + Input, + Which, } impl<'de> Deserialize<'de> for Keymap { @@ -32,7 +41,7 @@ impl<'de> Deserialize<'de> for Keymap { } #[derive(Deserialize)] struct Inner { - keymap: Vec, + keymap: Vec, } let shadow = Shadow::deserialize(deserializer)?; @@ -47,4 +56,15 @@ impl<'de> Deserialize<'de> for Keymap { impl Keymap { pub fn new() -> Self { toml::from_str(&MERGED_KEYMAP).unwrap() } + + #[inline] + pub fn get(&self, layer: KeymapLayer) -> &Vec { + match layer { + KeymapLayer::Manager => &self.manager, + KeymapLayer::Tasks => &self.tasks, + KeymapLayer::Select => &self.select, + KeymapLayer::Input => &self.input, + KeymapLayer::Which => unreachable!(), + } + } } diff --git a/src/core/event.rs b/src/core/event.rs index e7f5153b..2dc4a092 100644 --- a/src/core/event.rs +++ b/src/core/event.rs @@ -5,16 +5,17 @@ use crossterm::event::KeyEvent; use tokio::sync::{mpsc::UnboundedSender, oneshot}; use super::{files::FilesOp, input::InputOpt, manager::PreviewData, select::SelectOpt}; -use crate::config::open::Opener; +use crate::config::{keymap::{Control, KeymapLayer}, open::Opener}; static mut TX: Option> = None; pub enum Event { Quit, - Stop(bool, Option>), Key(KeyEvent), Render(String), Resize(u16, u16), + Stop(bool, Option>), + Ctrl(Control, KeymapLayer), // Manager Cd(PathBuf), @@ -57,10 +58,6 @@ impl Event { #[macro_export] macro_rules! emit { - (Stop($state:expr)) => {{ - let (tx, rx) = tokio::sync::oneshot::channel(); - $crate::core::Event::Stop($state, Some(tx)).wait(rx) - }}; (Key($key:expr)) => { $crate::core::Event::Key($key).emit(); }; @@ -70,6 +67,13 @@ macro_rules! emit { (Resize($cols:expr, $rows:expr)) => { $crate::core::Event::Resize($cols, $rows).emit(); }; + (Stop($state:expr)) => {{ + let (tx, rx) = tokio::sync::oneshot::channel(); + $crate::core::Event::Stop($state, Some(tx)).wait(rx) + }}; + (Ctrl($exec:expr, $layer:expr)) => { + $crate::core::Event::Ctrl($exec, $layer).emit(); + }; (Cd($op:expr)) => { $crate::core::Event::Cd($op).emit(); diff --git a/src/core/mod.rs b/src/core/mod.rs index 1417ecd2..c01cc426 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -8,6 +8,7 @@ pub mod manager; pub mod position; pub mod select; pub mod tasks; +pub mod which; pub use blocker::*; pub use event::*; diff --git a/src/core/which/mod.rs b/src/core/which/mod.rs new file mode 100644 index 00000000..93c339a1 --- /dev/null +++ b/src/core/which/mod.rs @@ -0,0 +1,4 @@ +mod which; + +pub use which::*; + diff --git a/src/core/which/which.rs b/src/core/which/which.rs new file mode 100644 index 00000000..e444ab5e --- /dev/null +++ b/src/core/which/which.rs @@ -0,0 +1,49 @@ +use std::mem; + +use crate::{config::{keymap::{Control, Key, KeymapLayer}, KEYMAP}, emit}; + +pub struct Which { + layer: KeymapLayer, + pub times: usize, + pub cands: Vec, + + pub visible: bool, +} + +impl Default for Which { + fn default() -> Self { + Self { layer: KeymapLayer::Manager, times: 0, cands: Default::default(), visible: false } + } +} + +impl Which { + pub fn show(&mut self, key: &Key, layer: KeymapLayer) -> bool { + self.layer = layer; + self.times = 1; + self.cands = KEYMAP + .get(layer) + .into_iter() + .filter(|s| !s.on.is_empty() && s.on[0] == *key) + .cloned() + .collect(); + self.visible = true; + true + } + + pub fn press(&mut self, key: Key) -> bool { + self.cands = mem::replace(&mut self.cands, Vec::new()) + .into_iter() + .filter(|s| s.on.len() > self.times && s.on[self.times] == key) + .collect(); + + if self.cands.is_empty() { + self.visible = false; + } else if self.cands.len() == 1 { + self.visible = false; + emit!(Ctrl(self.cands.remove(0), self.layer)); + } + + self.times += 1; + return true; + } +} diff --git a/src/ui/app.rs b/src/ui/app.rs index 01bfe077..45890ba3 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -3,7 +3,7 @@ use crossterm::event::KeyEvent; use tokio::sync::oneshot::{self}; use super::{root::Root, Ctx, Executor, Logs, Signals, Term}; -use crate::{config::keymap::Key, core::{files::FilesOp, Event}, emit}; +use crate::{config::keymap::{Control, Key, KeymapLayer}, core::{files::FilesOp, Event}, emit}; pub struct App { cx: Ctx, @@ -22,30 +22,17 @@ impl App { while let Some(event) = app.signals.rx.recv().await { match event { Event::Quit => break, - Event::Stop(state, tx) => app.dispatch_stop(state, tx), Event::Key(key) => app.dispatch_key(key), Event::Render(_) => app.dispatch_render(), Event::Resize(..) => app.dispatch_resize(), + Event::Stop(state, tx) => app.dispatch_stop(state, tx), + Event::Ctrl(ctrl, layer) => app.dispatch_ctrl(ctrl, layer), event => app.dispatch_module(event).await, } } Ok(()) } - fn dispatch_stop(&mut self, state: bool, tx: Option>) { - if state { - self.signals.stop_term(true); - self.term = None; - } else { - self.term = Some(Term::start().unwrap()); - self.signals.stop_term(false); - emit!(Render); - } - if let Some(tx) = tx { - tx.send(()).ok(); - } - } - fn dispatch_key(&mut self, key: KeyEvent) { let key = Key::from(key); if Executor::handle(&mut self.cx, key) { @@ -71,6 +58,27 @@ impl App { emit!(Render); } + fn dispatch_stop(&mut self, state: bool, tx: Option>) { + if state { + self.signals.stop_term(true); + self.term = None; + } else { + self.term = Some(Term::start().unwrap()); + self.signals.stop_term(false); + emit!(Render); + } + if let Some(tx) = tx { + tx.send(()).ok(); + } + } + + #[inline] + fn dispatch_ctrl(&mut self, ctrl: Control, layer: KeymapLayer) { + if Executor::dispatch(&mut self.cx, &ctrl.exec, layer) { + emit!(Render); + } + } + async fn dispatch_module(&mut self, event: Event) { let manager = &mut self.cx.manager; let tasks = &mut self.cx.tasks; diff --git a/src/ui/context.rs b/src/ui/context.rs index 1e44f24c..be8e035b 100644 --- a/src/ui/context.rs +++ b/src/ui/context.rs @@ -1,9 +1,10 @@ -use crate::{core::{input::Input, manager::Manager, select::Select, tasks::Tasks, Position}, misc::tty_size}; +use crate::{config::keymap::KeymapLayer, core::{input::Input, manager::Manager, select::Select, tasks::Tasks, which::Which, Position}, misc::tty_size}; pub struct Ctx { pub cursor: Option<(u16, u16)>, pub manager: Manager, + pub which: Which, pub select: Select, pub input: Input, pub tasks: Tasks, @@ -15,12 +16,28 @@ impl Ctx { cursor: None, manager: Manager::new(), - select: Select::default(), - input: Input::default(), + which: Default::default(), + select: Default::default(), + input: Default::default(), tasks: Tasks::start(), } } + #[inline] + pub fn layer(&self) -> KeymapLayer { + if self.which.visible { + KeymapLayer::Which + } else if self.input.visible { + KeymapLayer::Input + } else if self.select.visible { + KeymapLayer::Select + } else if self.tasks.visible { + KeymapLayer::Tasks + } else { + KeymapLayer::Manager + } + } + pub fn position(&self, pos: Position) -> Position { match pos { Position::Top => Position::Coords((tty_size().ws_col / 2).saturating_sub(25), 2), diff --git a/src/ui/dispatcher.rs b/src/ui/dispatcher.rs index 02e189f2..d8f8447b 100644 --- a/src/ui/dispatcher.rs +++ b/src/ui/dispatcher.rs @@ -1,52 +1,47 @@ use super::Ctx; -use crate::{config::{keymap::{Exec, Key, Single}, KEYMAP}, core::input::InputMode, misc::optional_bool}; +use crate::{config::{keymap::{Control, Exec, Key, KeymapLayer}, KEYMAP}, core::input::InputMode, misc::optional_bool}; pub struct Executor; impl Executor { pub fn handle(cx: &mut Ctx, key: Key) -> bool { - let layer = if cx.input.visible { - 3 - } else if cx.select.visible { - 2 - } else if cx.tasks.visible { - 1 - } else { - 0 - }; + let layer = cx.layer(); + if layer == KeymapLayer::Which { + return cx.which.press(key); + } - let mut render = false; - let mut matched = false; - let keymap = [&KEYMAP.manager, &KEYMAP.tasks, &KEYMAP.select, &KEYMAP.input][layer]; + if layer == KeymapLayer::Input && cx.input.mode() == InputMode::Insert { + if let Some(c) = key.plain() { + return cx.input.type_(c); + } + } - for Single { on, exec } in keymap { - if on.len() < 1 || on[0] != key { + for Control { on, exec } in KEYMAP.get(layer) { + if on.is_empty() || on[0] != key { continue; } - matched = true; - for e in exec { - if layer == 0 { - render = Self::manager(cx, e) || render; - } else if layer == 1 { - render = Self::tasks(cx, e) || render; - } else if layer == 2 { - render = Self::select(cx, e) || render; - } else if layer == 3 { - if cx.input.mode() != InputMode::Insert || key.plain().is_none() { - render = Self::input(cx, Some(e), &key) || render; - } else { - render = Self::input(cx, None, &key) || render; - break; - } - } - } + return if on.len() > 1 { + cx.which.show(&key, layer) + } else { + Self::dispatch(cx, exec, layer) + }; } + false + } - if layer == 3 && !matched { - render = Self::input(cx, None, &key); + #[inline] + pub fn dispatch(cx: &mut Ctx, exec: &Vec, layer: KeymapLayer) -> bool { + let mut render = false; + for e in exec { + render |= match layer { + KeymapLayer::Manager => Self::manager(cx, e), + KeymapLayer::Tasks => Self::tasks(cx, e), + KeymapLayer::Select => Self::select(cx, e), + KeymapLayer::Input => Self::input(cx, e), + KeymapLayer::Which => unreachable!(), + }; } - render } @@ -170,18 +165,7 @@ impl Executor { } } - fn input(cx: &mut Ctx, exec: Option<&Exec>, key: &Key) -> bool { - let exec = if let Some(e) = exec { - e - } else { - if cx.input.mode() == InputMode::Insert { - if let Some(c) = key.plain() { - return cx.input.type_(c); - } - } - return false; - }; - + fn input(cx: &mut Ctx, exec: &Exec) -> bool { match exec.cmd.as_str() { "close" => return cx.input.close(exec.named.contains_key("submit")), "escape" => return cx.input.escape(), diff --git a/src/ui/logs.rs b/src/ui/logs.rs index e611de25..de5c5f53 100644 --- a/src/ui/logs.rs +++ b/src/ui/logs.rs @@ -10,7 +10,7 @@ impl Logs { .context("failed to get XDG base directories")? .get_state_home(); - let appender = tracing_appender::rolling::hourly(root, "yazi.log"); + let appender = tracing_appender::rolling::never(root, "yazi.log"); let (handle, guard) = tracing_appender::non_blocking(appender); // let filter = EnvFilter::from_default_env(); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index bfa8ead8..dd154e5d 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -11,6 +11,7 @@ mod signals; mod status; mod tasks; mod term; +mod which; pub use app::*; pub use context::*; diff --git a/src/ui/root.rs b/src/ui/root.rs index 3aa7b2b4..eef03a51 100644 --- a/src/ui/root.rs +++ b/src/ui/root.rs @@ -1,6 +1,6 @@ use ratatui::{buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, widgets::Widget}; -use super::{header, manager, status, tasks, Ctx, Input, Select}; +use super::{header, manager, status, tasks, which::Which, Ctx, Input, Select}; pub struct Root<'a> { cx: &'a mut Ctx, @@ -35,5 +35,9 @@ impl<'a> Widget for Root<'a> { } else { self.cx.cursor = None; } + + if self.cx.which.visible { + Which::new(self.cx).render(area, buf); + } } } diff --git a/src/ui/which/layout.rs b/src/ui/which/layout.rs new file mode 100644 index 00000000..ff83ef3a --- /dev/null +++ b/src/ui/which/layout.rs @@ -0,0 +1,32 @@ +use ratatui::{layout, prelude::{Buffer, Constraint, Direction, Rect}, widgets::Widget}; + +use super::Side; +use crate::ui::Ctx; + +pub struct Which<'a> { + cx: &'a mut Ctx, +} + +impl<'a> Which<'a> { + pub fn new(cx: &'a mut Ctx) -> Self { Self { cx } } +} + +impl Widget for Which<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { + let chunks = layout::Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(area); + + let chunks = layout::Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(chunks[1]); + + let which = &self.cx.which; + let cands: (Vec<_>, Vec<_>) = which.cands.iter().enumerate().partition(|(i, _)| i % 2 == 0); + + Side::new(which.times, cands.0).render(chunks[0], buf); + Side::new(which.times, cands.1).render(chunks[1], buf); + } +} diff --git a/src/ui/which/mod.rs b/src/ui/which/mod.rs new file mode 100644 index 00000000..55cb2643 --- /dev/null +++ b/src/ui/which/mod.rs @@ -0,0 +1,5 @@ +mod layout; +mod side; + +pub use layout::*; +pub use side::*; diff --git a/src/ui/which/side.rs b/src/ui/which/side.rs new file mode 100644 index 00000000..26dfaea7 --- /dev/null +++ b/src/ui/which/side.rs @@ -0,0 +1,27 @@ +use ratatui::{prelude::{Buffer, Rect}, widgets::{List, ListItem, Widget}}; + +use crate::config::keymap::Control; + +pub struct Side<'a> { + times: usize, + cands: Vec<(usize, &'a Control)>, +} + +impl<'a> Side<'a> { + pub fn new(times: usize, cands: Vec<(usize, &'a Control)>) -> Self { Self { times, cands } } +} + +impl Widget for Side<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { + let items = self + .cands + .into_iter() + .map(|(_, c)| { + let s = c.on[self.times..].into_iter().map(ToString::to_string).collect::(); + ListItem::new(format!("{:?}", s)) + }) + .collect::>(); + + List::new(items).render(area, buf); + } +}