feat: dynamic keymap Lua API (#4031)

This commit is contained in:
三咲雅 misaki masa 2026-06-10 15:09:49 +08:00 committed by GitHub
parent 4086fbb166
commit 07a5deb109
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 317 additions and 169 deletions

View file

@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- Drag and drop ([#4005]) - Drag and drop ([#4005])
- Bulk create ([#3793]) - Bulk create ([#3793])
- Dynamic keymap Lua API ([#4031])
- Image preview with Überzug++ on Niri ([#3990]) - Image preview with Überzug++ on Niri ([#3990])
- New gait for input `backward` and `forward` actions ([#4012]) - New gait for input `backward` and `forward` actions ([#4012])
@ -1742,3 +1743,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
[#4005]: https://github.com/sxyazi/yazi/pull/4005 [#4005]: https://github.com/sxyazi/yazi/pull/4005
[#4012]: https://github.com/sxyazi/yazi/pull/4012 [#4012]: https://github.com/sxyazi/yazi/pull/4012
[#4022]: https://github.com/sxyazi/yazi/pull/4022 [#4022]: https://github.com/sxyazi/yazi/pull/4022
[#4031]: https://github.com/sxyazi/yazi/pull/4031

View file

@ -11,6 +11,7 @@ This guide will help you understand how to contribute to the project.
3. [Development Setup](#development-setup) 3. [Development Setup](#development-setup)
4. [How to Contribute](#how-to-contribute) 4. [How to Contribute](#how-to-contribute)
5. [Pull Requests](#pull-requests) 5. [Pull Requests](#pull-requests)
6. [AI Policy](#ai-policy)
## Getting Started ## Getting Started

1
Cargo.lock generated
View file

@ -5674,6 +5674,7 @@ dependencies = [
"ordered-float 5.3.0", "ordered-float 5.3.0",
"paste", "paste",
"ratatui", "ratatui",
"serde",
"serde_json", "serde_json",
"tokio", "tokio",
"tokio-stream", "tokio-stream",

View file

@ -26,7 +26,7 @@ impl UserData for Which {
fields.add_cached_field("tx", |_, me| Ok(me.tx.clone().map(yazi_binding::MpscUnboundedTx))); fields.add_cached_field("tx", |_, me| Ok(me.tx.clone().map(yazi_binding::MpscUnboundedTx)));
fields.add_field_method_get("times", |_, me| Ok(me.inner.times)); fields.add_field_method_get("times", |_, me| Ok(me.inner.times));
fields.add_cached_field("cands", |lua, me| { fields.add_cached_field("cands", |lua, me| {
lua.create_sequence_from(me.inner.cands.iter().cloned().map(yazi_binding::keymap::ChordCow)) lua.create_sequence_from(me.inner.cands.iter().map(yazi_binding::keymap::Chord::from))
}); });
fields.add_field_method_get("active", |_, me| Ok(me.inner.active)); fields.add_field_method_get("active", |_, me| Ok(me.inner.active));

View file

@ -39,6 +39,7 @@ mlua = { workspace = true }
ordered-float = { workspace = true } ordered-float = { workspace = true }
paste = { workspace = true } paste = { workspace = true }
ratatui = { workspace = true } ratatui = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
tokio-stream = { workspace = true } tokio-stream = { workspace = true }

View file

@ -1,6 +1,6 @@
use std::{mem, ops::{Deref, DerefMut}}; use std::ops::{Deref, DerefMut};
use mlua::{UserData, UserDataFields}; use mlua::{FromLua, Lua, UserData, UserDataFields, Value};
use yazi_shim::mlua::UserDataFieldsExt; use yazi_shim::mlua::UserDataFieldsExt;
use crate::event::Cmd; use crate::event::Cmd;
@ -19,12 +19,34 @@ impl DerefMut for Action {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner } fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner }
} }
impl From<Action> for yazi_shared::event::Action {
fn from(value: Action) -> Self { value.inner }
}
impl From<Action> for yazi_shared::event::Actions {
fn from(value: Action) -> Self { Self::from(value.inner) }
}
impl Action { impl Action {
pub fn new(inner: impl Into<yazi_shared::event::Action>) -> Self { Self { inner: inner.into() } } pub fn new(inner: impl Into<yazi_shared::event::Action>) -> Self { Self { inner: inner.into() } }
} }
impl UserData for Action { impl FromLua for Action {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) { fn from_lua(value: Value, _: &Lua) -> mlua::Result<Self> {
fields.add_cached_field_mut("cmd", |_, me| Ok(Cmd::new(mem::take(&mut me.cmd)))); Ok(match value {
Value::String(s) => Self { inner: s.to_str()?.parse()? },
Value::UserData(ud) => ud.take()?,
_ => Err(mlua::Error::FromLuaConversionError {
from: value.type_name(),
to: "Action".to_owned(),
message: Some("expected a string or an Action".to_string()),
})?,
})
}
}
impl UserData for Action {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_cached_field("cmd", |_, me| Ok(Cmd::new(me.cmd.clone())));
} }
} }

View file

@ -0,0 +1,50 @@
use std::ops::{Deref, DerefMut};
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use crate::event::Action;
#[derive(Clone)]
pub struct Actions {
inner: yazi_shared::event::Actions,
}
impl Deref for Actions {
type Target = yazi_shared::event::Actions;
fn deref(&self) -> &Self::Target { &self.inner }
}
impl DerefMut for Actions {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner }
}
impl From<Actions> for yazi_shared::event::Actions {
fn from(value: Actions) -> Self { value.inner }
}
impl Actions {
pub fn new(inner: yazi_shared::event::Actions) -> Self { Self { inner } }
}
impl FromLua for Actions {
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
let inner = match value {
Value::Table(t) => t
.sequence_values::<Action>()
.map(|a| a.map(Into::into))
.collect::<mlua::Result<Vec<_>>>()?
.into(),
v @ Value::String(_) => Action::from_lua(v, lua)?.into(),
_ => Err("expected a string or a table of actions".into_lua_err())?,
};
Ok(Self { inner })
}
}
impl IntoLua for Actions {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> {
lua.create_sequence_from(self.inner.into_iter().map(Action::new))?.into_lua(lua)
}
}

View file

@ -1 +1 @@
yazi_macro::mod_flat!(action cmd); yazi_macro::mod_flat!(action actions cmd);

View file

@ -1,33 +1,61 @@
use std::{ops::Deref, sync::Arc}; use std::ops::Deref;
use mlua::{ExternalError, FromLua, IntoLua, Lua, LuaSerdeExt, Table, UserData, UserDataFields, Value}; use mlua::{ExternalError, FromLua, IntoLua, Lua, Table, UserData, UserDataFields, Value};
use serde::Deserialize;
use yazi_codegen::FromLuaOwned;
use yazi_shim::mlua::UserDataFieldsExt; use yazi_shim::mlua::UserDataFieldsExt;
use crate::{Id, Iter, event::Action, keymap::Key}; use crate::{Id, Iter, event::Actions, keymap::Key};
#[derive(FromLuaOwned)]
pub struct Chord { pub struct Chord {
inner: Arc<yazi_config::keymap::Chord>, inner: yazi_config::keymap::ChordArc,
} }
impl Deref for Chord { impl Deref for Chord {
type Target = Arc<yazi_config::keymap::Chord>; type Target = yazi_config::keymap::ChordArc;
fn deref(&self) -> &Self::Target { &self.inner } fn deref(&self) -> &Self::Target { &self.inner }
} }
impl From<Chord> for Arc<yazi_config::keymap::Chord> { impl From<&yazi_config::keymap::ChordArc> for Chord {
fn from(value: &yazi_config::keymap::ChordArc) -> Self { Self { inner: value.clone() } }
}
impl From<Chord> for yazi_config::keymap::ChordArc {
fn from(value: Chord) -> Self { value.inner } fn from(value: Chord) -> Self { value.inner }
} }
impl Chord { impl Chord {
pub fn new(inner: impl Into<Arc<yazi_config::keymap::Chord>>) -> Self { pub fn new(inner: impl Into<yazi_config::keymap::ChordArc>) -> Self {
Self { inner: inner.into() } Self { inner: inner.into() }
} }
} }
impl FromLua for Chord { impl TryFrom<(Value, yazi_shared::Layer)> for Chord {
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> { type Error = mlua::Error;
Ok(Self::new(lua.from_value::<yazi_config::keymap::Chord>(value)?))
fn try_from((value, layer): (Value, yazi_shared::Layer)) -> Result<Self, Self::Error> {
use yazi_config::keymap::Chord as C;
use yazi_shared::Layer as L;
let de = mlua::serde::Deserializer::new(value);
let inner = match layer {
L::Null => C::<{ L::Null as u8 }>::deserialize(de)?.into(),
L::App => C::<{ L::App as u8 }>::deserialize(de)?.into(),
L::Mgr => C::<{ L::Mgr as u8 }>::deserialize(de)?.into(),
L::Tasks => C::<{ L::Tasks as u8 }>::deserialize(de)?.into(),
L::Spot => C::<{ L::Spot as u8 }>::deserialize(de)?.into(),
L::Pick => C::<{ L::Pick as u8 }>::deserialize(de)?.into(),
L::Input => C::<{ L::Input as u8 }>::deserialize(de)?.into(),
L::Confirm => C::<{ L::Confirm as u8 }>::deserialize(de)?.into(),
L::Help => C::<{ L::Help as u8 }>::deserialize(de)?.into(),
L::Cmp => C::<{ L::Cmp as u8 }>::deserialize(de)?.into(),
L::Which => C::<{ L::Which as u8 }>::deserialize(de)?.into(),
L::Notify => C::<{ L::Notify as u8 }>::deserialize(de)?.into(),
};
Ok(Self { inner })
} }
} }
@ -38,9 +66,7 @@ impl UserData for Chord {
fields fields
.add_cached_field("on", |lua, me| lua.create_sequence_from(me.on.iter().copied().map(Key))); .add_cached_field("on", |lua, me| lua.create_sequence_from(me.on.iter().copied().map(Key)));
fields.add_cached_field("run", |lua, me| { fields.add_cached_field("run", |_, me| Ok(Actions::new(me.run.clone())));
lua.create_sequence_from(me.run.iter().cloned().map(Action::new))
});
fields.add_cached_field("desc", |lua, me| lua.create_string(&me.desc)); fields.add_cached_field("desc", |lua, me| lua.create_string(&me.desc));
} }

View file

@ -1,15 +0,0 @@
use mlua::UserData;
use yazi_codegen::FromLuaOwned;
#[derive(Clone, FromLuaOwned)]
pub struct ChordCow(pub yazi_config::keymap::ChordCow);
impl From<yazi_config::keymap::ChordCow> for ChordCow {
fn from(value: yazi_config::keymap::ChordCow) -> Self { Self(value) }
}
impl From<ChordCow> for yazi_config::keymap::ChordCow {
fn from(value: ChordCow) -> Self { value.0 }
}
impl UserData for ChordCow {}

View file

@ -1,12 +1,14 @@
use std::ops::Deref; use std::ops::Deref;
use mlua::{ExternalError, ExternalResult, MetaMethod, UserData, UserDataMethods}; use mlua::{ExternalError, ExternalResult, MetaMethod, Table, UserData, UserDataMethods, Value};
use yazi_shim::mlua::DeserializeOverLua;
use super::{Chord, ChordMatcher}; use super::{Chord, ChordMatcher};
use crate::keymap::ChordIter; use crate::{event::Actions, keymap::ChordIter};
pub struct Chords { pub struct Chords {
inner: &'static yazi_config::keymap::Chords, pub(super) inner: &'static yazi_config::keymap::Chords,
pub(super) layer: yazi_shared::Layer,
} }
impl Deref for Chords { impl Deref for Chords {
@ -15,10 +17,6 @@ impl Deref for Chords {
fn deref(&self) -> &Self::Target { self.inner } fn deref(&self) -> &Self::Target { self.inner }
} }
impl Chords {
pub fn new(inner: &'static yazi_config::keymap::Chords) -> Self { Self { inner } }
}
impl UserData for Chords { impl UserData for Chords {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) { fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("match", |_, me, matcher: Option<ChordMatcher>| { methods.add_method("match", |_, me, matcher: Option<ChordMatcher>| {
@ -32,14 +30,16 @@ impl UserData for Chords {
}) })
}); });
methods.add_method("insert", |_, me, (index, chord): (isize, Chord)| { methods.add_method("insert", |_, me, (index, value): (isize, Value)| {
let index = match index { let index = match index {
1.. => index - 1, 1.. => index - 1,
0 => return Err("index must be 1-based or negative".into_lua_err()), 0 => return Err("index must be 1-based or negative".into_lua_err()),
_ => index, _ => index,
}; };
let chord = Chord::try_from((value, me.layer))?;
me.insert(index, chord.clone()).into_lua_err()?; me.insert(index, chord.clone()).into_lua_err()?;
Ok(chord) Ok(chord)
}); });
@ -48,6 +48,22 @@ impl UserData for Chords {
Ok(()) Ok(())
}); });
methods.add_method("update", |_, me, (matcher, table): (ChordMatcher, Table)| {
let mut run: Option<Actions> = table.raw_get("run")?;
if let Some(run) = &mut run {
table.raw_remove("run")?;
run.set(me.layer, yazi_shared::Source::Key);
}
me.update(matcher.0, |mut chord| {
chord = chord.deserialize_over_lua(&table)?;
if let Some(run) = &run {
chord.run = run.clone().into();
}
Ok(chord)
})
});
methods.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.load().len())); methods.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.load().len()));
} }
} }

View file

@ -1 +1 @@
yazi_macro::mod_flat!(chord chord_cow chords key section); yazi_macro::mod_flat!(chord chords key section);

View file

@ -3,13 +3,13 @@ use std::ops::Deref;
use anyhow::bail; use anyhow::bail;
use mlua::{UserData, UserDataFields}; use mlua::{UserData, UserDataFields};
use yazi_config::KEYMAP; use yazi_config::KEYMAP;
use yazi_shared::Layer;
use yazi_shim::mlua::UserDataFieldsExt; use yazi_shim::mlua::UserDataFieldsExt;
use crate::keymap::Chords; use crate::keymap::Chords;
pub struct KeymapSection { pub struct KeymapSection {
inner: &'static yazi_config::keymap::KeymapSection, inner: &'static yazi_config::keymap::KeymapSection,
layer: yazi_shared::Layer,
} }
impl Deref for KeymapSection { impl Deref for KeymapSection {
@ -18,30 +18,32 @@ impl Deref for KeymapSection {
fn deref(&self) -> &Self::Target { self.inner } fn deref(&self) -> &Self::Target { self.inner }
} }
impl TryFrom<Layer> for KeymapSection { impl TryFrom<yazi_shared::Layer> for KeymapSection {
type Error = anyhow::Error; type Error = anyhow::Error;
fn try_from(value: Layer) -> Result<Self, Self::Error> { fn try_from(layer: yazi_shared::Layer) -> Result<Self, Self::Error> {
let inner = match value { use yazi_shared::Layer as L;
Layer::Null | Layer::App => bail!("invalid layer"),
Layer::Mgr => KEYMAP.mgr.as_erased(), let inner = match layer {
Layer::Tasks => KEYMAP.tasks.as_erased(), L::Null | L::App => bail!("invalid layer"),
Layer::Spot => KEYMAP.spot.as_erased(), L::Mgr => KEYMAP.mgr.as_erased(),
Layer::Pick => KEYMAP.pick.as_erased(), L::Tasks => KEYMAP.tasks.as_erased(),
Layer::Input => KEYMAP.input.as_erased(), L::Spot => KEYMAP.spot.as_erased(),
Layer::Confirm => KEYMAP.confirm.as_erased(), L::Pick => KEYMAP.pick.as_erased(),
Layer::Help => KEYMAP.help.as_erased(), L::Input => KEYMAP.input.as_erased(),
Layer::Cmp => KEYMAP.cmp.as_erased(), L::Confirm => KEYMAP.confirm.as_erased(),
Layer::Which => bail!("invalid layer"), L::Help => KEYMAP.help.as_erased(),
Layer::Notify => bail!("invalid layer"), L::Cmp => KEYMAP.cmp.as_erased(),
L::Which => bail!("invalid layer"),
L::Notify => bail!("invalid layer"),
}; };
Ok(Self { inner }) Ok(Self { inner, layer })
} }
} }
impl UserData for KeymapSection { impl UserData for KeymapSection {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) { fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_cached_field("rules", |_, me| Ok(Chords::new(me.inner))); fields.add_cached_field("rules", |_, me| Ok(Chords { inner: me.inner, layer: me.layer }));
} }
} }

View file

@ -3,14 +3,15 @@ use std::{borrow::Cow, hash::{Hash, Hasher}, sync::{Arc, OnceLock}};
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Deserializer, de}; use serde::{Deserialize, Deserializer, de};
use serde_with::{DeserializeAs, DisplayFromStr, OneOrMany}; use serde_with::{DeserializeAs, DisplayFromStr, OneOrMany};
use yazi_codegen::DeserializeOver2;
use yazi_shared::{Id, Layer, event::{Actions, deserialize_actions}}; use yazi_shared::{Id, Layer, event::{Actions, deserialize_actions}};
use super::{Key, ids::chord_id}; use super::{Key, ids::chord_id};
use crate::{Mixable, Platform, keymap::Chords}; use crate::{Mixable, Platform, keymap::{ChordArc, Chords}};
static RE: OnceLock<Regex> = OnceLock::new(); static RE: OnceLock<Regex> = OnceLock::new();
#[derive(Debug, Default, Deserialize)] #[derive(Debug, Default, Deserialize, DeserializeOver2)]
pub struct Chord<const L: u8 = { Layer::Null as u8 }> { pub struct Chord<const L: u8 = { Layer::Null as u8 }> {
#[serde(skip, default = "chord_id")] #[serde(skip, default = "chord_id")]
pub id: Id, pub id: Id,
@ -47,10 +48,6 @@ impl<const L: u8> Hash for Chord<L> {
} }
impl<const L: u8> Chord<L> { impl<const L: u8> Chord<L> {
pub fn as_erased<const M: u8>(self: &Arc<Self>) -> &Arc<Chord<M>> {
unsafe { &*(self as *const Arc<Chord<L>> as *const Arc<Chord<M>>) }
}
pub fn on(&self) -> String { self.on.iter().map(ToString::to_string).collect() } pub fn on(&self) -> String { self.on.iter().map(ToString::to_string).collect() }
pub fn run(&self) -> String { pub fn run(&self) -> String {
@ -117,7 +114,7 @@ impl ChordMatcher {
// --- Iter // --- Iter
#[derive(Default)] #[derive(Default)]
pub struct ChordIter { pub struct ChordIter {
pub chords: Arc<Vec<Arc<Chord>>>, pub chords: Arc<Vec<ChordArc>>,
pub matcher: ChordMatcher, pub matcher: ChordMatcher,
pub offset: usize, pub offset: usize,
} }
@ -133,7 +130,7 @@ impl From<&Chords> for ChordIter {
} }
impl Iterator for ChordIter { impl Iterator for ChordIter {
type Item = Arc<Chord>; type Item = ChordArc;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
while let Some(chord) = self.chords.get(self.offset) { while let Some(chord) = self.chords.get(self.offset) {

View file

@ -0,0 +1,66 @@
use std::{ops::{Deref, DerefMut}, sync::Arc};
use serde::Deserialize;
use yazi_shared::{Layer, event::ActionCow};
use crate::{Mixable, keymap::Chord};
#[derive(Clone, Debug, Default, Deserialize)]
#[repr(transparent)]
pub struct ChordArc<const L: u8 = { Layer::Null as u8 }>(Arc<Chord<L>>);
impl<const L: u8> Deref for ChordArc<L> {
type Target = Arc<Chord<L>>;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl<const L: u8> DerefMut for ChordArc<L> {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
}
impl<const L: u8> From<&ChordArc<L>> for ChordArc<L> {
fn from(value: &ChordArc<L>) -> Self { value.clone() }
}
impl<const L: u8, const M: u8> From<Chord<L>> for ChordArc<M> {
fn from(value: Chord<L>) -> Self { ChordArc(Arc::new(value)).into_erased() }
}
impl<const L: u8> From<ChordArc<L>> for Chord<L> {
fn from(value: ChordArc<L>) -> Self {
match Arc::try_unwrap(value.0) {
Ok(c) => c,
Err(arc) => Self::clone(&arc),
}
}
}
impl<const L: u8> From<&ChordArc<L>> for Chord<L> {
fn from(value: &ChordArc<L>) -> Self { Self::clone(value) }
}
impl<const L: u8> ChordArc<L> {
pub fn as_erased<const M: u8>(&self) -> &ChordArc<M> {
unsafe { &*(self as *const ChordArc<L> as *const ChordArc<M>) }
}
pub fn into_erased<const M: u8>(self) -> ChordArc<M> {
ChordArc(unsafe { Arc::from_raw(Arc::into_raw(self.0) as *const Chord<M>) })
}
pub fn to_seq(&self) -> Vec<ActionCow> {
self.run.iter().rev().cloned().map(Into::into).collect()
}
pub fn into_seq(self) -> Vec<ActionCow> {
match Arc::try_unwrap(self.0) {
Ok(c) => c.run.into_iter().rev().map(Into::into).collect(),
Err(arc) => Self(arc).to_seq(),
}
}
}
impl<const L: u8> Mixable for ChordArc<L> {
fn filter(&self) -> bool { self.0.filter() }
}

View file

@ -1,47 +0,0 @@
use std::{ops::Deref, sync::Arc};
use yazi_shared::{Layer, event::ActionCow};
use super::Chord;
#[derive(Clone, Debug)]
pub enum ChordCow<const L: u8 = { Layer::Null as u8 }> {
Owned(Chord<L>),
Borrowed(Arc<Chord<L>>),
}
impl<const L: u8> From<Chord<L>> for ChordCow<L> {
fn from(c: Chord<L>) -> Self { Self::Owned(c) }
}
impl<const L: u8> From<Arc<Chord<L>>> for ChordCow<L> {
fn from(c: Arc<Chord<L>>) -> Self { Self::Borrowed(c) }
}
impl<const L: u8> From<&Arc<Chord<L>>> for ChordCow<L> {
fn from(c: &Arc<Chord<L>>) -> Self { Self::Borrowed(c.clone()) }
}
impl<const L: u8> Deref for ChordCow<L> {
type Target = Chord<L>;
fn deref(&self) -> &Self::Target {
match self {
Self::Owned(c) => c,
Self::Borrowed(c) => c,
}
}
}
impl<const L: u8> Default for ChordCow<L> {
fn default() -> Self { Self::Owned(Chord::default()) }
}
impl<const L: u8> ChordCow<L> {
pub fn into_seq(self) -> Vec<ActionCow> {
match self {
Self::Owned(c) => c.run.into_iter().rev().map(Into::into).collect(),
Self::Borrowed(c) => c.run.iter().rev().cloned().map(Into::into).collect(),
}
}
}

View file

@ -5,29 +5,28 @@ use serde::Deserialize;
use yazi_shared::Layer; use yazi_shared::Layer;
use yazi_shim::{arc_swap::{ArcSwapExt, IntoPointee}, vec::{IndexAtError, VecExt}}; use yazi_shim::{arc_swap::{ArcSwapExt, IntoPointee}, vec::{IndexAtError, VecExt}};
use super::Chord; use crate::keymap::{Chord, ChordArc, ChordMatcher};
use crate::keymap::ChordMatcher;
#[derive(Debug, Default, Deserialize)] #[derive(Debug, Default, Deserialize)]
pub struct Chords<const L: u8 = { Layer::Null as u8 }>(ArcSwap<Vec<Arc<Chord<L>>>>); pub struct Chords<const L: u8 = { Layer::Null as u8 }>(ArcSwap<Vec<ChordArc<L>>>);
impl<const L: u8> Deref for Chords<L> { impl<const L: u8> Deref for Chords<L> {
type Target = ArcSwap<Vec<Arc<Chord<L>>>>; type Target = ArcSwap<Vec<ChordArc<L>>>;
fn deref(&self) -> &Self::Target { &self.0 } fn deref(&self) -> &Self::Target { &self.0 }
} }
impl<const L: u8> From<Vec<Arc<Chord<L>>>> for Chords<L> { impl<const L: u8> From<Vec<ChordArc<L>>> for Chords<L> {
fn from(inner: Vec<Arc<Chord<L>>>) -> Self { Self(inner.into_pointee()) } fn from(inner: Vec<ChordArc<L>>) -> Self { Self(inner.into_pointee()) }
} }
impl<const L: u8> Chords<L> { impl<const L: u8> Chords<L> {
pub fn as_erased<const M: u8>(&self) -> Arc<Vec<Arc<Chord<M>>>> { pub fn as_erased<const M: u8>(&self) -> Arc<Vec<ChordArc<M>>> {
let chords = self.0.load_full(); let chords = self.0.load_full();
unsafe { Arc::from_raw(Arc::into_raw(chords) as *const Vec<Arc<Chord<M>>>) } unsafe { Arc::from_raw(Arc::into_raw(chords) as *const Vec<ChordArc<M>>) }
} }
pub fn insert(&self, index: isize, rule: Arc<Chord>) -> Result<(), IndexAtError> { pub fn insert(&self, index: isize, rule: ChordArc) -> Result<(), IndexAtError> {
self.0.try_rcu(|rules| { self.0.try_rcu(|rules| {
let (before, after) = rules.split_at(rules.index_at(index)?); let (before, after) = rules.split_at(rules.index_at(index)?);
Ok( Ok(
@ -46,12 +45,30 @@ impl<const L: u8> Chords<L> {
pub fn remove(&self, matcher: ChordMatcher) { pub fn remove(&self, matcher: ChordMatcher) {
self.0.rcu(|chords| { self.0.rcu(|chords| {
let mut next = Vec::clone(chords); let mut next = Vec::clone(chords);
next.retain(|chord| !matcher.matches(chord.as_erased())); next.retain(|arc| !matcher.matches(arc.as_erased()));
next next
}); });
} }
pub(crate) fn unwrap_unchecked(self) -> Vec<Arc<Chord<L>>> { pub fn update<E>(
&self,
matcher: ChordMatcher,
f: impl Fn(Chord) -> Result<Chord, E>,
) -> Result<(), E> {
self.0.try_rcu(|rules| {
let mut next = Vec::clone(rules);
for arc in &mut next {
if matcher.matches(arc.as_erased()) {
*arc = f(Chord::clone(arc.as_erased()))?.into();
}
}
Ok(Arc::new(next))
})?;
Ok(())
}
pub(crate) fn unwrap_unchecked(self) -> Vec<ChordArc<L>> {
Arc::try_unwrap(self.0.into_inner()).expect("unique chords arc") Arc::try_unwrap(self.0.into_inner()).expect("unique chords arc")
} }
} }

View file

@ -6,7 +6,8 @@ use yazi_codegen::{DeserializeOver, DeserializeOver1};
use yazi_fs::{Xdg, ok_or_not_found}; use yazi_fs::{Xdg, ok_or_not_found};
use yazi_shared::Layer; use yazi_shared::Layer;
use super::{Chord, KeymapSection}; use super::KeymapSection;
use crate::keymap::ChordArc;
#[derive(Deserialize, DeserializeOver, DeserializeOver1)] #[derive(Deserialize, DeserializeOver, DeserializeOver1)]
pub struct Keymap { pub struct Keymap {
@ -21,7 +22,7 @@ pub struct Keymap {
} }
impl Keymap { impl Keymap {
pub fn get(&self, layer: Layer) -> Arc<Vec<Arc<Chord>>> { pub fn get(&self, layer: Layer) -> Arc<Vec<ChordArc>> {
match layer { match layer {
Layer::Null | Layer::App => Arc::new(Vec::new()), Layer::Null | Layer::App => Arc::new(Vec::new()),
Layer::Mgr => self.mgr.deref().as_erased(), Layer::Mgr => self.mgr.deref().as_erased(),

View file

@ -1 +1 @@
yazi_macro::mod_flat!(chord chord_cow chords ids key keymap section); yazi_macro::mod_flat!(chord chord_arc chords ids key keymap section);

View file

@ -1,4 +1,4 @@
use std::{ops::Deref, sync::Arc}; use std::ops::Deref;
use hashbrown::HashSet; use hashbrown::HashSet;
use serde::Deserialize; use serde::Deserialize;
@ -7,7 +7,7 @@ use yazi_shared::Layer;
use yazi_shim::toml::DeserializeOverHook; use yazi_shim::toml::DeserializeOverHook;
use super::{Chord, Chords, Key}; use super::{Chord, Chords, Key};
use crate::mix; use crate::{keymap::ChordArc, mix};
#[derive(Default, Deserialize, DeserializeOver2)] #[derive(Default, Deserialize, DeserializeOver2)]
pub struct KeymapSection<const L: u8 = { Layer::Null as u8 }> { pub struct KeymapSection<const L: u8 = { Layer::Null as u8 }> {
@ -41,7 +41,7 @@ impl<const L: u8> DeserializeOverHook for KeymapSection<L> {
let a_seen: HashSet<_> = self.prepend_keymap.iter().map(on).collect(); let a_seen: HashSet<_> = self.prepend_keymap.iter().map(on).collect();
let b_seen: HashSet<_> = keymap.iter().map(|c| on(c)).collect(); let b_seen: HashSet<_> = keymap.iter().map(|c| on(c)).collect();
let keymap: Vec<Arc<Chord<L>>> = mix( let keymap: Vec<ChordArc<L>> = mix(
self.prepend_keymap, self.prepend_keymap,
keymap.into_iter().filter(|v| !a_seen.contains(&on(v))), keymap.into_iter().filter(|v| !a_seen.contains(&on(v))),
self.append_keymap.into_iter().filter(|v| !b_seen.contains(&on(v))), self.append_keymap.into_iter().filter(|v| !b_seen.contains(&on(v))),

View file

@ -1,8 +1,6 @@
use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use yazi_config::{KEYMAP, YAZI, keymap::{Chord, Key}}; use yazi_config::{KEYMAP, YAZI, keymap::{ChordArc, Key}};
use yazi_macro::{act, render, render_and}; use yazi_macro::{act, render, render_and};
use yazi_shared::Layer; use yazi_shared::Layer;
use yazi_term::{CursorStyle, TERM, event::KeyCode}; use yazi_term::{CursorStyle, TERM, event::KeyCode};
@ -14,7 +12,7 @@ use crate::help::HELP_MARGIN;
pub struct Help { pub struct Help {
pub visible: bool, pub visible: bool,
pub layer: Layer, pub layer: Layer,
pub(super) bindings: Vec<Arc<Chord>>, pub(super) bindings: Vec<ChordArc>,
// Filter // Filter
pub keyword: String, pub keyword: String,
@ -75,7 +73,7 @@ impl Help {
} }
// --- Bindings // --- Bindings
pub fn window(&self) -> &[Arc<Chord>] { pub fn window(&self) -> &[ChordArc] {
let end = (self.offset + self.limit()).min(self.bindings.len()); let end = (self.offset + self.limit()).min(self.bindings.len());
&self.bindings[self.offset..end] &self.bindings[self.offset..end]
} }

View file

@ -1,13 +1,13 @@
use mlua::{ExternalError, FromLua, IntoLua, Lua, Table, Value}; use mlua::{ExternalError, FromLua, IntoLua, Lua, Table, Value};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use yazi_config::{KEYMAP, keymap::{ChordCow, Key}}; use yazi_config::{KEYMAP, keymap::{ChordArc, Key}};
use yazi_macro::impl_data_any; use yazi_macro::impl_data_any;
use yazi_shared::{Layer, event::ActionCow}; use yazi_shared::{Layer, event::ActionCow};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct WhichOpt { pub struct WhichOpt {
pub tx: Option<mpsc::UnboundedSender<Option<yazi_binding::keymap::ChordCow>>>, pub tx: Option<mpsc::UnboundedSender<Option<yazi_binding::keymap::Chord>>>,
pub cands: Vec<ChordCow>, pub cands: Vec<ChordArc>,
pub silent: bool, pub silent: bool,
pub times: usize, pub times: usize,
} }
@ -24,7 +24,7 @@ impl TryFrom<ActionCow> for WhichOpt {
Ok(Self { Ok(Self {
tx: a.take_any2("tx").transpose()?, tx: a.take_any2("tx").transpose()?,
cands: a.take_any_iter::<yazi_binding::keymap::ChordCow>().map(Into::into).collect(), cands: a.take_any_iter::<yazi_binding::keymap::Chord>().map(Into::into).collect(),
silent: a.bool("silent"), silent: a.bool("silent"),
times: a.get("times").unwrap_or(0), times: a.get("times").unwrap_or(0),
}) })
@ -39,7 +39,7 @@ impl From<(Layer, Key)> for WhichOpt {
.get(layer) .get(layer)
.iter() .iter()
.filter(|&c| c.on.len() > 1 && c.on[0] == key) .filter(|&c| c.on.len() > 1 && c.on[0] == key)
.map(Into::into) .cloned()
.collect(), .collect(),
times: 1, times: 1,
silent: false, silent: false,
@ -57,7 +57,7 @@ impl FromLua for WhichOpt {
tx: t.raw_get::<yazi_binding::MpscUnboundedTx<_>>("tx").ok().map(|t| t.0), tx: t.raw_get::<yazi_binding::MpscUnboundedTx<_>>("tx").ok().map(|t| t.0),
cands: t cands: t
.raw_get::<Table>("cands")? .raw_get::<Table>("cands")?
.sequence_values::<yazi_binding::keymap::ChordCow>() .sequence_values::<yazi_binding::keymap::Chord>()
.map(|c| c.map(Into::into)) .map(|c| c.map(Into::into))
.collect::<mlua::Result<Vec<_>>>()?, .collect::<mlua::Result<Vec<_>>>()?,
times: t.raw_get("times").unwrap_or_default(), times: t.raw_get("times").unwrap_or_default(),
@ -72,7 +72,7 @@ impl IntoLua for WhichOpt {
lua lua
.create_table_from([ .create_table_from([
("tx", self.tx.map(yazi_binding::MpscUnboundedTx).into_lua(lua)?), ("tx", self.tx.map(yazi_binding::MpscUnboundedTx).into_lua(lua)?),
("cands", lua.create_sequence_from(self.cands.into_iter().map(yazi_binding::keymap::ChordCow))?.into_lua(lua)?), ("cands", lua.create_sequence_from(self.cands.into_iter().map(yazi_binding::keymap::Chord::new))?.into_lua(lua)?),
("times", self.times.into_lua(lua)?), ("times", self.times.into_lua(lua)?),
("silent", self.silent.into_lua(lua)?), ("silent", self.silent.into_lua(lua)?),
])? ])?

View file

@ -1,6 +1,6 @@
use std::{borrow::Cow, mem}; use std::{borrow::Cow, mem};
use yazi_config::{YAZI, keymap::ChordCow, which::SortBy}; use yazi_config::{YAZI, keymap::ChordArc, which::SortBy};
use yazi_shared::{natsort, translit::Transliterator}; use yazi_shared::{natsort, translit::Transliterator};
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq)]
@ -23,7 +23,7 @@ impl Default for WhichSorter {
} }
impl WhichSorter { impl WhichSorter {
pub fn sort(&self, items: &mut Vec<ChordCow>) { pub fn sort(&self, items: &mut Vec<ChordArc>) {
if self.by == SortBy::None || items.is_empty() { if self.by == SortBy::None || items.is_empty() {
return; return;
} }
@ -53,6 +53,7 @@ impl WhichSorter {
if self.reverse { ordering.reverse() } else { ordering } if self.reverse { ordering.reverse() } else { ordering }
}); });
// TODO: remove the `mem::take` since that involves Arc atomics refcount changes
*items = indices.into_iter().map(|i| mem::take(&mut items[i])).collect(); *items = indices.into_iter().map(|i| mem::take(&mut items[i])).collect();
} }
} }

View file

@ -1,12 +1,12 @@
use tokio::sync::mpsc; use tokio::sync::mpsc;
use yazi_config::keymap::{ChordCow, Key}; use yazi_config::keymap::{ChordArc, Key};
use yazi_macro::{emit, render_and}; use yazi_macro::{emit, render_and};
#[derive(Default)] #[derive(Default)]
pub struct Which { pub struct Which {
pub tx: Option<mpsc::UnboundedSender<Option<yazi_binding::keymap::ChordCow>>>, pub tx: Option<mpsc::UnboundedSender<Option<yazi_binding::keymap::Chord>>>,
pub times: usize, pub times: usize,
pub cands: Vec<ChordCow>, pub cands: Vec<ChordArc>,
// Active state // Active state
pub active: bool, pub active: bool,
@ -31,7 +31,7 @@ impl Which {
render_and!(true) render_and!(true)
} }
pub fn dismiss(&mut self, chord: Option<ChordCow>) { pub fn dismiss(&mut self, chord: Option<ChordArc>) {
self.times = 0; self.times = 0;
self.cands.clear(); self.cands.clear();
@ -39,7 +39,7 @@ impl Which {
self.silent = false; self.silent = false;
if let Some(tx) = self.tx.take() { if let Some(tx) = self.tx.take() {
_ = tx.send(chord.clone().map(Into::into)); _ = tx.send(chord.as_ref().map(Into::into));
} }
if let Some(chord) = chord { if let Some(chord) = chord {
emit!(Seq(chord.into_seq())); emit!(Seq(chord.into_seq()));

View file

@ -1,6 +1,6 @@
use anyhow::Result; use anyhow::Result;
use yazi_actor::Ctx; use yazi_actor::Ctx;
use yazi_config::{KEYMAP, keymap::{Chord, ChordCow, Key}}; use yazi_config::{KEYMAP, keymap::{Chord, Key}};
use yazi_macro::act; use yazi_macro::act;
use yazi_shared::Layer; use yazi_shared::Layer;
@ -37,7 +37,7 @@ impl<'a> Router<'a> {
fn matches(&mut self, layer: Layer, key: Key) -> bool { fn matches(&mut self, layer: Layer, key: Key) -> bool {
for chord in &*KEYMAP.get(layer) { for chord in &*KEYMAP.get(layer) {
let Chord { on, .. } = &**chord; let Chord { on, .. } = chord.as_ref();
if on.is_empty() || on[0] != key { if on.is_empty() || on[0] != key {
continue; continue;
} }
@ -46,7 +46,7 @@ impl<'a> Router<'a> {
let cx = &mut Ctx::active(&mut self.app.core, &mut self.app.term); let cx = &mut Ctx::active(&mut self.app.core, &mut self.app.term);
act!(which:activate, cx, (layer, key)).ok(); act!(which:activate, cx, (layer, key)).ok();
} else { } else {
Dispatcher::new(self.app).dispatch_seq(ChordCow::from(chord).into_seq()); Dispatcher::new(self.app).dispatch_seq(chord.to_seq());
} }
return true; return true;
} }

View file

@ -3,7 +3,7 @@ use std::{str::FromStr, time::Duration};
use mlua::{ExternalError, ExternalResult, Function, IntoLuaMulti, Lua, Table, Value}; use mlua::{ExternalError, ExternalResult, Function, IntoLuaMulti, Lua, Table, Value};
use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_stream::wrappers::UnboundedReceiverStream;
use yazi_binding::{InputRx, elements::{Line, Pos, Text}, runtime}; use yazi_binding::{InputRx, elements::{Line, Pos, Text}, runtime};
use yazi_config::{Platform, keymap::{Chord, ChordCow, Key}, popup::{ConfirmCfg, InputCfg}}; use yazi_config::{Platform, keymap::{Chord, ChordArc, Key}, popup::{ConfirmCfg, InputCfg}};
use yazi_core::notify::MessageOpt; use yazi_core::notify::MessageOpt;
use yazi_macro::relay; use yazi_macro::relay;
use yazi_proxy::{ConfirmProxy, InputProxy, NotifyProxy, WhichProxy}; use yazi_proxy::{ConfirmProxy, InputProxy, NotifyProxy, WhichProxy};
@ -24,7 +24,7 @@ impl Utils {
.enumerate() .enumerate()
.map(|(i, cand)| { .map(|(i, cand)| {
let cand = cand?; let cand = cand?;
Ok(ChordCow::Owned(Chord { Ok(ChordArc::from(Chord::<{ Layer::Null as u8 }> {
id: yazi_config::keymap::chord_id(), id: yazi_config::keymap::chord_id(),
on: Self::parse_keys(cand.raw_get("on")?)?, on: Self::parse_keys(cand.raw_get("on")?)?,
run: relay!(which:callback, [i + 1]).into(), run: relay!(which:callback, [i + 1]).into(),

View file

@ -1,12 +1,12 @@
use tokio::sync::mpsc; use tokio::sync::mpsc;
use yazi_config::keymap::ChordCow; use yazi_config::keymap::ChordArc;
use yazi_core::which::WhichOpt; use yazi_core::which::WhichOpt;
use yazi_macro::{emit, relay}; use yazi_macro::{emit, relay};
pub struct WhichProxy; pub struct WhichProxy;
impl WhichProxy { impl WhichProxy {
pub async fn activate(cands: Vec<ChordCow>, silent: bool) -> Option<ChordCow> { pub async fn activate(cands: Vec<ChordArc>, silent: bool) -> Option<ChordArc> {
let (tx, mut rx) = mpsc::unbounded_channel(); let (tx, mut rx) = mpsc::unbounded_channel();
emit!(Call(relay!(which:activate).with_any("opt", WhichOpt { emit!(Call(relay!(which:activate).with_any("opt", WhichOpt {
tx: Some(tx), tx: Some(tx),
@ -14,6 +14,6 @@ impl WhichProxy {
silent, silent,
times: 0, times: 0,
}))); })));
Some(rx.recv().await??.0) Some(rx.recv().await??.into())
} }
} }

View file

@ -22,6 +22,21 @@ impl From<Action> for Actions {
fn from(value: Action) -> Self { Self(vec![value]) } fn from(value: Action) -> Self { Self(vec![value]) }
} }
impl From<Vec<Action>> for Actions {
fn from(value: Vec<Action>) -> Self { Self(value) }
}
impl Actions {
pub fn set(&mut self, layer: Layer, source: Source) {
for action in &mut self.0 {
action.source = source;
if action.layer == Layer::Null {
action.layer = layer;
}
}
}
}
impl IntoIterator for Actions { impl IntoIterator for Actions {
type IntoIter = vec::IntoIter<Action>; type IntoIter = vec::IntoIter<Action>;
type Item = Action; type Item = Action;
@ -44,14 +59,8 @@ where
return Err(de::Error::custom(format!("invalid keymap layer const: {L}"))); return Err(de::Error::custom(format!("invalid keymap layer const: {L}")));
}; };
let mut actions: Vec<Action> = OneOrMany::<DisplayFromStr>::deserialize_as(deserializer)?; let mut actions = Actions(OneOrMany::<DisplayFromStr>::deserialize_as(deserializer)?);
actions.set(layer, Source::Key);
for action in &mut actions { Ok(actions)
action.source = Source::Key;
if action.layer == Layer::Null {
action.layer = layer;
}
}
Ok(Actions(actions))
} }