diff --git a/yazi-config/src/keymap/control.rs b/yazi-config/src/keymap/control.rs index 74548fe71..5fae59002 100644 --- a/yazi-config/src/keymap/control.rs +++ b/yazi-config/src/keymap/control.rs @@ -66,3 +66,12 @@ impl Deref for ControlCow { } } } + +impl ControlCow { + pub fn into_seq(self) -> VecDeque { + match self { + Self::Owned(c) => c.exec.into(), + Self::Borrowed(c) => c.to_seq(), + } + } +} diff --git a/yazi-config/src/keymap/key.rs b/yazi-config/src/keymap/key.rs index a5f56e968..cc907aa94 100644 --- a/yazi-config/src/keymap/key.rs +++ b/yazi-config/src/keymap/key.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use anyhow::bail; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use serde::Deserialize; @@ -58,10 +60,10 @@ impl From for Key { } } -impl TryFrom for Key { - type Error = anyhow::Error; +impl FromStr for Key { + type Err = anyhow::Error; - fn try_from(s: String) -> Result { + fn from_str(s: &str) -> Result { if s.is_empty() { bail!("empty key") } @@ -124,6 +126,12 @@ impl TryFrom for Key { } } +impl TryFrom for Key { + type Error = anyhow::Error; + + fn try_from(s: String) -> Result { Self::from_str(&s) } +} + impl ToString for Key { fn to_string(&self) -> String { if let Some(c) = self.plain() { diff --git a/yazi-core/src/which/commands/callback.rs b/yazi-core/src/which/commands/callback.rs new file mode 100644 index 000000000..7dd4100c1 --- /dev/null +++ b/yazi-core/src/which/commands/callback.rs @@ -0,0 +1,33 @@ +use tokio::sync::mpsc; +use tracing::error; +use yazi_shared::event::Cmd; + +use crate::which::Which; + +pub struct Opt { + tx: mpsc::Sender, + idx: usize, +} + +impl TryFrom for Opt { + type Error = (); + + fn try_from(mut c: Cmd) -> Result { + Ok(Self { + tx: c.take_data().ok_or(())?, + idx: c.take_first().and_then(|s| s.parse().ok()).ok_or(())?, + }) + } +} + +impl Which { + pub fn callback(&mut self, opt: impl TryInto) { + let Ok(opt) = opt.try_into() else { + return; + }; + + if opt.tx.try_send(opt.idx).is_err() { + error!("callback: send error"); + } + } +} diff --git a/yazi-core/src/which/commands/mod.rs b/yazi-core/src/which/commands/mod.rs new file mode 100644 index 000000000..67954c837 --- /dev/null +++ b/yazi-core/src/which/commands/mod.rs @@ -0,0 +1,2 @@ +mod callback; +mod show; diff --git a/yazi-core/src/which/commands/show.rs b/yazi-core/src/which/commands/show.rs new file mode 100644 index 000000000..639704627 --- /dev/null +++ b/yazi-core/src/which/commands/show.rs @@ -0,0 +1,59 @@ +use std::str::FromStr; + +use yazi_config::{keymap::{Control, Key}, KEYMAP}; +use yazi_shared::{event::Cmd, render, Layer}; + +use crate::which::Which; + +pub struct Opt { + cands: Vec, + layer: Layer, + silent: bool, +} + +impl TryFrom for Opt { + type Error = anyhow::Error; + + fn try_from(mut c: Cmd) -> Result { + Ok(Self { + cands: c.take_data().unwrap_or_default(), + layer: Layer::from_str(&c.take_name("layer").unwrap_or_default())?, + silent: c.named.contains_key("silent"), + }) + } +} + +impl Which { + pub fn show(&mut self, opt: impl TryInto) { + let Ok(opt) = opt.try_into() else { + return; + }; + + if opt.cands.is_empty() { + return; + } + + self.layer = opt.layer; + self.times = 0; + self.cands = opt.cands.into_iter().map(|c| c.into()).collect(); + + self.visible = true; + self.silent = opt.silent; + render!(); + } + + pub fn show_with(&mut self, key: &Key, layer: Layer) { + self.layer = layer; + self.times = 1; + self.cands = KEYMAP + .get(layer) + .iter() + .filter(|c| c.on.len() > 1 && &c.on[0] == key) + .map(|c| c.into()) + .collect(); + + self.visible = true; + self.silent = false; + render!(); + } +} diff --git a/yazi-core/src/which/mod.rs b/yazi-core/src/which/mod.rs index 6bdddcdc1..b2098e80f 100644 --- a/yazi-core/src/which/mod.rs +++ b/yazi-core/src/which/mod.rs @@ -1,3 +1,4 @@ +mod commands; mod which; pub use which::*; diff --git a/yazi-core/src/which/which.rs b/yazi-core/src/which/which.rs index 017e35814..8cc627c80 100644 --- a/yazi-core/src/which/which.rs +++ b/yazi-core/src/which/which.rs @@ -1,60 +1,40 @@ -use yazi_config::{keymap::{Control, ControlCow, Key}, KEYMAP}; +use yazi_config::keymap::{ControlCow, Key}; use yazi_shared::{emit, render, Layer}; +#[derive(Default)] pub struct Which { - layer: Layer, - pub times: usize, - pub cands: Vec, + pub(super) layer: Layer, + pub times: usize, + pub cands: Vec, pub visible: bool, -} - -impl Default for Which { - fn default() -> Self { - Self { layer: Layer::Manager, times: 0, cands: Default::default(), visible: false } - } + pub silent: bool, } impl Which { - pub fn show(&mut self, key: &Key, layer: Layer) { - self.layer = layer; - self.times = 1; - self.cands = KEYMAP - .get(layer) - .iter() - .filter(|c| c.on.len() > 1 && &c.on[0] == key) - .map(|c| c.into()) - .collect(); - - self.visible = true; - render!(); - } - - pub fn show_with(&mut self, cands: Vec, layer: Layer) { - self.layer = layer; + fn reset(&mut self) { self.times = 0; - self.cands = cands.into_iter().map(|c| c.into()).collect(); + self.cands.clear(); - self.visible = true; - render!(); + self.visible = false; + self.silent = false; } pub fn type_(&mut self, key: Key) -> bool { self.cands.retain(|c| c.on.len() > self.times && c.on[self.times] == key); + self.times += 1; if self.cands.is_empty() { - self.visible = false; + self.reset(); } else if self.cands.len() == 1 { - self.visible = false; - emit!(Seq(self.cands[0].to_seq(), self.layer)); - } else if let Some(i) = self.cands.iter().position(|c| c.on.len() == self.times + 1) { - self.visible = false; - emit!(Seq(self.cands[i].to_seq(), self.layer)); + emit!(Seq(self.cands.remove(0).into_seq(), self.layer)); + self.reset(); + } else if let Some(i) = self.cands.iter().position(|c| c.on.len() == self.times) { + emit!(Seq(self.cands.remove(i).into_seq(), self.layer)); + self.reset(); } - self.times += 1; render!(); - true } } diff --git a/yazi-fm/src/executor.rs b/yazi-fm/src/executor.rs index 6f434094e..6f5951445 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -21,7 +21,7 @@ impl<'a> Executor<'a> { Layer::Input => self.input(cmd), Layer::Help => self.help(cmd), Layer::Completion => self.completion(cmd), - Layer::Which => unreachable!(), + Layer::Which => self.which(cmd), } } @@ -276,4 +276,17 @@ impl<'a> Executor<'a> { _ => {} } } + + fn which(&mut self, cmd: Cmd) { + macro_rules! on { + ($name:ident) => { + if cmd.name == stringify!($name) { + return self.app.cx.which.$name(cmd); + } + }; + } + + on!(show); + on!(callback); + } } diff --git a/yazi-fm/src/help/layout.rs b/yazi-fm/src/help/layout.rs index 0fcb59cac..09d8a41c8 100644 --- a/yazi-fm/src/help/layout.rs +++ b/yazi-fm/src/help/layout.rs @@ -21,7 +21,7 @@ impl<'a> Widget for Layout<'a> { widgets::Clear.render(area, buf); let help = &self.cx.help; - Paragraph::new(help.keyword().unwrap_or_else(|| format!("{}.help", help.layer))) + Paragraph::new(help.keyword().unwrap_or_else(|| format!("{}.help", help.layer.to_string()))) .style(THEME.help.footer.into()) .render(chunks[1], buf); diff --git a/yazi-fm/src/router.rs b/yazi-fm/src/router.rs index 2dfcd1521..84db76a46 100644 --- a/yazi-fm/src/router.rs +++ b/yazi-fm/src/router.rs @@ -48,7 +48,7 @@ impl<'a> Router<'a> { } if on.len() > 1 { - self.app.cx.which.show(&key, layer); + self.app.cx.which.show_with(&key, layer); } else { emit!(Seq(ctrl.to_seq(), layer)); } diff --git a/yazi-fm/src/which/layout.rs b/yazi-fm/src/which/layout.rs index 4f1aac1f4..401fea0c1 100644 --- a/yazi-fm/src/which/layout.rs +++ b/yazi-fm/src/which/layout.rs @@ -18,8 +18,11 @@ impl<'a> Which<'a> { impl Widget for Which<'_> { fn render(self, area: Rect, buf: &mut Buffer) { let which = &self.cx.which; - let cols = THEME.which.cols as usize; + if which.silent { + return; + } + let cols = THEME.which.cols as usize; let height = area.height.min(which.cands.len().div_ceil(cols) as u16 + PADDING_Y * 2); let area = Rect { x: PADDING_X.min(area.width), diff --git a/yazi-plugin/src/utils/layer.rs b/yazi-plugin/src/utils/layer.rs new file mode 100644 index 000000000..d953d48f2 --- /dev/null +++ b/yazi-plugin/src/utils/layer.rs @@ -0,0 +1,58 @@ +use std::str::FromStr; + +use mlua::{ExternalError, ExternalResult, Lua, Table, Value}; +use tokio::sync::mpsc; +use yazi_config::keymap::{Control, Key}; +use yazi_shared::{emit, event::Cmd, Layer}; + +use super::Utils; + +impl Utils { + fn parse_keys(value: Value) -> mlua::Result> { + Ok(match value { + Value::String(s) => { + vec![Key::from_str(s.to_str()?).into_lua_err()?] + } + Value::Table(t) => { + let mut v = Vec::with_capacity(10); + for s in t.sequence_values::() { + v.push(Key::from_str(s?.to_str()?).into_lua_err()?); + } + v + } + _ => Err("invalid `on`".into_lua_err())?, + }) + } + + pub(super) fn layer(lua: &Lua, ya: &Table) -> mlua::Result<()> { + ya.set( + "which", + lua.create_async_function(|_, t: Table| async move { + let (tx, mut rx) = mpsc::channel::(1); + + let mut cands = Vec::with_capacity(30); + for (i, cand) in t.get::<_, Table>("cands")?.sequence_values::().enumerate() { + let cand = cand?; + cands.push(Control { + on: Self::parse_keys(cand.get("on")?)?, + exec: vec![Cmd::args("callback", vec![i.to_string()]).with_data(tx.clone())], + desc: cand.get("desc").ok(), + }); + } + + drop(tx); + emit!(Call( + Cmd::new("show") + .with("layer", Layer::Which) + .with_bool("silent", t.get("silent").unwrap_or_default()) + .with_data(cands), + Layer::Which + )); + + Ok(rx.recv().await.map(|idx| idx + 1)) + })?, + )?; + + Ok(()) + } +} diff --git a/yazi-plugin/src/utils/mod.rs b/yazi-plugin/src/utils/mod.rs index e4b94e6a8..f5906355c 100644 --- a/yazi-plugin/src/utils/mod.rs +++ b/yazi-plugin/src/utils/mod.rs @@ -3,6 +3,7 @@ mod cache; mod call; mod image; +mod layer; mod log; mod plugin; mod preview; diff --git a/yazi-plugin/src/utils/utils.rs b/yazi-plugin/src/utils/utils.rs index 1552437c0..80c975e4f 100644 --- a/yazi-plugin/src/utils/utils.rs +++ b/yazi-plugin/src/utils/utils.rs @@ -18,6 +18,7 @@ pub fn install(lua: &Lua) -> mlua::Result<()> { Utils::cache(lua, &ya)?; Utils::call(lua, &ya)?; Utils::image(lua, &ya)?; + Utils::layer(lua, &ya)?; Utils::log(lua, &ya)?; Utils::plugin(lua, &ya)?; Utils::preview(lua, &ya)?; diff --git a/yazi-shared/src/layer.rs b/yazi-shared/src/layer.rs index 06311cfd1..fe76437a7 100644 --- a/yazi-shared/src/layer.rs +++ b/yazi-shared/src/layer.rs @@ -1,4 +1,6 @@ -use std::fmt::{self, Display}; +use std::str::FromStr; + +use anyhow::bail; #[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)] pub enum Layer { @@ -13,17 +15,36 @@ pub enum Layer { Which, } -impl Display for Layer { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl ToString for Layer { + fn to_string(&self) -> String { match self { - Self::App => write!(f, "app"), - Self::Manager => write!(f, "manager"), - Self::Tasks => write!(f, "tasks"), - Self::Select => write!(f, "select"), - Self::Input => write!(f, "input"), - Self::Help => write!(f, "help"), - Self::Completion => write!(f, "completion"), - Self::Which => write!(f, "which"), + Self::App => "app", + Self::Manager => "manager", + Self::Tasks => "tasks", + Self::Select => "select", + Self::Input => "input", + Self::Help => "help", + Self::Completion => "completion", + Self::Which => "which", } + .to_string() + } +} + +impl FromStr for Layer { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + Ok(match s { + "app" => Self::App, + "manager" => Self::Manager, + "tasks" => Self::Tasks, + "select" => Self::Select, + "input" => Self::Input, + "help" => Self::Help, + "completion" => Self::Completion, + "which" => Self::Which, + _ => bail!("invalid layer: {s}"), + }) } }