From 1f8c1703598bbdd57029ca1c2ad05f5a77e93610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20=C2=B7=20Misaki=20Masa?= Date: Mon, 24 Jul 2023 07:26:16 +0800 Subject: [PATCH] feat: which-key and multi-key support (#4) --- Cargo.lock | 24 +++-------- src/config/keymap/exec.rs | 21 ++++++++- src/config/keymap/key.rs | 59 +++++++++++++++++++++++++ src/config/keymap/keymap.rs | 34 ++++++++++++--- src/config/theme/theme.rs | 3 +- src/core/event.rs | 16 ++++--- src/core/manager/manager.rs | 4 +- src/core/mod.rs | 1 + src/core/which/mod.rs | 4 ++ src/core/which/which.rs | 52 +++++++++++++++++++++++ src/misc/fns.rs | 5 ++- src/ui/app.rs | 42 ++++++++++-------- src/ui/context.rs | 23 ++++++++-- src/ui/dispatcher.rs | 85 +++++++++++++++++-------------------- src/ui/logs.rs | 2 +- src/ui/mod.rs | 1 + src/ui/root.rs | 6 ++- src/ui/which/layout.rs | 47 ++++++++++++++++++++ src/ui/which/mod.rs | 5 +++ src/ui/which/side.rs | 49 +++++++++++++++++++++ 20 files changed, 378 insertions(+), 105 deletions(-) create mode 100644 src/core/which/mod.rs create mode 100644 src/core/which/which.rs create mode 100644 src/ui/which/layout.rs create mode 100644 src/ui/which/mod.rs create mode 100644 src/ui/which/side.rs diff --git a/Cargo.lock b/Cargo.lock index dced0abc..eb333e10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -382,9 +382,9 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "either" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "equivalent" @@ -665,15 +665,6 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" -[[package]] -name = "home" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" -dependencies = [ - "windows-sys 0.48.0", -] - [[package]] name = "http" version = "0.2.9" @@ -1334,9 +1325,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.31" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" dependencies = [ "proc-macro2", ] @@ -2358,12 +2349,9 @@ dependencies = [ [[package]] name = "xdg" -version = "2.5.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "688597db5a750e9cad4511cb94729a078e274308099a0382b5b8203bbc767fee" -dependencies = [ - "home", -] +checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" [[package]] name = "yaml-rust" diff --git a/src/config/keymap/exec.rs b/src/config/keymap/exec.rs index cdde7eb5..e64abd28 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, @@ -28,6 +28,25 @@ impl From<&str> for Exec { } } +impl ToString for Exec { + fn to_string(&self) -> String { + let mut s = self.cmd.clone(); + for arg in &self.args { + s.push(' '); + s.push_str(arg); + } + for (name, value) in &self.named { + s.push_str(" --"); + s.push_str(name); + if !value.is_empty() { + s.push('='); + s.push_str(value); + } + } + s + } +} + impl Exec { pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> where diff --git a/src/config/keymap/key.rs b/src/config/keymap/key.rs index 5263126e..a9b59afe 100644 --- a/src/config/keymap/key.rs +++ b/src/config/keymap/key.rs @@ -102,3 +102,62 @@ impl TryFrom for Key { Ok(key) } } + +impl ToString for Key { + fn to_string(&self) -> String { + if let Some(c) = self.plain() { + let c = if self.shift { c.to_ascii_uppercase() } else { c }; + return if c == ' ' { "".to_string() } 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) if c == ' ' => "Space", + 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/config/theme/theme.rs b/src/config/theme/theme.rs index f8da9ed6..952ce5ab 100644 --- a/src/config/theme/theme.rs +++ b/src/config/theme/theme.rs @@ -64,7 +64,8 @@ pub struct Theme { impl Theme { pub fn new() -> Self { let mut theme: Self = toml::from_str(&MERGED_THEME).unwrap(); - theme.preview.syntect_theme = absolute_path(&theme.preview.syntect_theme); + theme.preview.syntect_theme = + futures::executor::block_on(absolute_path(&theme.preview.syntect_theme)); theme } } 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/manager/manager.rs b/src/core/manager/manager.rs index c362495f..a1e114e6 100644 --- a/src/core/manager/manager.rs +++ b/src/core/manager/manager.rs @@ -1,4 +1,4 @@ -use std::{collections::{BTreeMap, BTreeSet, HashMap, HashSet}, mem, path::PathBuf}; +use std::{collections::{BTreeMap, BTreeSet, HashMap, HashSet}, env, mem, path::PathBuf}; use tokio::fs; @@ -25,6 +25,8 @@ impl Manager { } pub fn refresh(&mut self) { + env::set_current_dir(&self.current().cwd).ok(); + self.watcher.trigger(&self.current().cwd); if let Some(p) = self.parent() { self.watcher.trigger(&p.cwd); 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..194920c2 --- /dev/null +++ b/src/core/which/which.rs @@ -0,0 +1,52 @@ +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)); + } else if let Some(i) = self.cands.iter().position(|c| c.on.len() == self.times + 1) { + emit!(Ctrl(self.cands.remove(i), self.layer)); + self.visible = false; + } + + self.times += 1; + return true; + } +} diff --git a/src/misc/fns.rs b/src/misc/fns.rs index 86c98d02..81fa6045 100644 --- a/src/misc/fns.rs +++ b/src/misc/fns.rs @@ -2,7 +2,8 @@ use std::{env, path::{Path, PathBuf}}; use tokio::fs; -pub fn absolute_path(p: &Path) -> PathBuf { +pub async fn absolute_path(p: impl AsRef) -> PathBuf { + let p = p.as_ref(); if p.starts_with("~") { if let Ok(home) = env::var("HOME") { let mut expanded = PathBuf::new(); @@ -11,7 +12,7 @@ pub fn absolute_path(p: &Path) -> PathBuf { return expanded; } } - p.to_path_buf() + fs::canonicalize(p).await.unwrap_or_else(|_| p.to_path_buf()) } pub fn readable_path(p: &Path, base: &Path) -> String { diff --git a/src/ui/app.rs b/src/ui/app.rs index 01bfe077..30f564ef 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, misc::absolute_path}; 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,12 +58,33 @@ 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; match event { Event::Cd(path) => { - manager.active_mut().cd(path).await; + manager.active_mut().cd(absolute_path(path).await).await; } Event::Refresh => { manager.refresh(); 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..2b1af850 100644 --- a/src/ui/dispatcher.rs +++ b/src/ui/dispatcher.rs @@ -1,52 +1,49 @@ +use std::path::PathBuf; + 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, emit, 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 } @@ -65,6 +62,11 @@ impl Executor { "enter" => cx.manager.active_mut().enter(), "back" => cx.manager.active_mut().back(), "forward" => cx.manager.active_mut().forward(), + "cd" => { + let path = exec.args.get(0).map(|s| PathBuf::from(s)).unwrap_or_default(); + emit!(Cd(path)); + false + } // Selection "select" => { @@ -170,18 +172,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..30335a3a --- /dev/null +++ b/src/ui/which/layout.rs @@ -0,0 +1,47 @@ +use ratatui::{layout, prelude::{Buffer, Constraint, Direction, Rect}, widgets::{Clear, 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 which = &self.cx.which; + let mut cands: (Vec<_>, Vec<_>, Vec<_>) = Default::default(); + for (i, c) in which.cands.iter().enumerate() { + match i % 3 { + 0 => cands.0.push(c), + 1 => cands.1.push(c), + 2 => cands.2.push(c), + _ => unreachable!(), + } + } + + let height = cands.0.len() as u16 + 2; + let area = Rect { + x: 1, + y: area.height.saturating_sub(height + 2), + width: area.width.saturating_sub(2), + height, + }; + + let chunks = layout::Layout::default() + .direction(Direction::Horizontal) + .constraints( + [Constraint::Ratio(1, 3), Constraint::Ratio(1, 3), Constraint::Ratio(1, 3)].as_ref(), + ) + .split(area); + + Clear.render(area, buf); + Side::new(which.times, cands.0).render(chunks[0], buf); + Side::new(which.times, cands.1).render(chunks[1], buf); + Side::new(which.times, cands.2).render(chunks[2], 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..2e0fe814 --- /dev/null +++ b/src/ui/which/side.rs @@ -0,0 +1,49 @@ +use ratatui::{prelude::{Buffer, Rect}, style::{Color, Style, Stylize}, text::{Line, Span}, widgets::{Block, List, ListItem, Padding, Widget}}; + +use crate::config::keymap::Control; + +pub struct Side<'a> { + times: usize, + cands: Vec<&'a Control>, +} + +impl<'a> Side<'a> { + pub fn new(times: usize, cands: Vec<&'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 mut spans = vec![]; + + // Keys + let keys = c.on[self.times..].iter().map(ToString::to_string).collect::>(); + spans.push(Span::raw(" ".repeat(10usize.saturating_sub(keys.join("").len())))); + spans.push(Span::styled(keys[0].clone(), Style::default().fg(Color::LightCyan))); + spans.extend( + keys + .iter() + .skip(1) + .map(|k| Span::styled(k.to_string(), Style::default().fg(Color::DarkGray))), + ); + + // Separator + spans.push(Span::styled("  ".to_string(), Style::default().fg(Color::DarkGray))); + + // Exec + let exec = c.exec.iter().map(ToString::to_string).collect::>().join("; "); + spans.push(Span::styled(exec, Style::default().fg(Color::Magenta))); + + ListItem::new(Line::from(spans)) + }) + .collect::>(); + + List::new(items) + .block(Block::new().padding(Padding::new(0, 1, 1, 1))) + .bg(Color::Rgb(47, 51, 73)) + .render(area, buf); + } +}