feat!: make help menu a command palette (#4074)

This commit is contained in:
三咲雅 misaki masa 2026-06-22 23:07:17 +08:00 committed by sxyazi
parent 4f45ddb514
commit 581886a355
No known key found for this signature in database
48 changed files with 399 additions and 380 deletions

View file

@ -15,7 +15,7 @@ jobs:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Install Nix
uses: cachix/install-nix-action@v31

View file

@ -17,7 +17,7 @@ jobs:
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Setup Rust toolchain
run: |
@ -34,7 +34,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Setup Rust toolchain
run: |
@ -50,7 +50,7 @@ jobs:
stylua:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: JohnnyMorganz/stylua-action@v5
with:
token: ${{ secrets.GITHUB_TOKEN }}

View file

@ -42,7 +42,7 @@ jobs:
CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER: riscv64-linux-gnu-gcc
CARGO_TARGET_SPARC64_UNKNOWN_LINUX_GNU_LINKER: sparc64-linux-gnu-gcc
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Install gcc
if: matrix.gcc != ''
@ -81,7 +81,7 @@ jobs:
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER: lld-link.exe
CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER: lld-link.exe
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
@ -120,7 +120,7 @@ jobs:
container:
image: docker://ghcr.io/cross-rs/${{ matrix.target }}:edge
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
@ -150,7 +150,7 @@ jobs:
arch: arm64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
fetch-depth: 0
@ -233,7 +233,7 @@ jobs:
echo "Nightly changelog: https://github.com/sxyazi/yazi/blob/main/CHANGELOG.md#unreleased" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
with:

View file

@ -21,7 +21,7 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Setup Rust toolchain
run: rustup toolchain install stable --profile minimal

View file

@ -12,7 +12,7 @@ jobs:
permissions:
issues: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v6

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])
- Bulk create ([#3793])
- Make help menu a command palette ([#4074])
- Dynamic keymap Lua API ([#4031])
- New `ui.Input` element ([#4040])
- Image preview with Überzug++ on Niri ([#3990])
@ -24,6 +25,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
### Changed
- Rename `<BackTab>` to `<S-Tab>` ([#3989])
- Remove `help:filter` action since the filter input is now always available ([#4074])
- `[help]` of `theme.toml`: supersede `on` with `chord`, supersede `run` and `desc` with `action`, remove `footer` ([#4074])
- Remove Legacy Console Mode on Windows ([#3989])
### Deprecated
@ -1757,3 +1760,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
[#4065]: https://github.com/sxyazi/yazi/pull/4065
[#4067]: https://github.com/sxyazi/yazi/pull/4067
[#4068]: https://github.com/sxyazi/yazi/pull/4068
[#4074]: https://github.com/sxyazi/yazi/pull/4074

20
Cargo.lock generated
View file

@ -215,9 +215,9 @@ dependencies = [
[[package]]
name = "arrayvec"
version = "0.7.6"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe"
[[package]]
name = "as-slice"
@ -1931,9 +1931,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.32"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
[[package]]
name = "loop9"
@ -2788,9 +2788,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.45"
version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
dependencies = [
"proc-macro2",
]
@ -3831,9 +3831,9 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.49"
version = "0.3.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469"
checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327"
dependencies = [
"deranged",
"libc",
@ -3853,9 +3853,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
[[package]]
name = "time-macros"
version = "0.2.29"
version = "0.2.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71c652a3727a9cbb9a02f707f530b618ce00d0ccd762009c8c23bd191df3c17d"
checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935"
dependencies = [
"num-conv",
"time-core",

View file

@ -0,0 +1,25 @@
use anyhow::Result;
use yazi_macro::{emit, render, succ};
use yazi_parser::help::CloseForm;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct Close;
impl Actor for Close {
type Form = CloseForm;
const NAME: &str = "close";
fn act(cx: &mut Ctx, opt: Self::Form) -> Result<Data> {
let help = &mut cx.help;
if let Some(chord) = help.bindings.get(help.cursor).filter(|_| opt.submit) {
emit!(Seq(chord.to_seq(help.layer)));
}
help.visible = false;
succ!(render!());
}
}

View file

@ -2,6 +2,7 @@ use anyhow::Result;
use yazi_macro::{act, render, succ};
use yazi_parser::VoidForm;
use yazi_shared::data::Data;
use yazi_widgets::input::InputMode;
use crate::{Actor, Ctx};
@ -13,15 +14,11 @@ impl Actor for Escape {
const NAME: &str = "escape";
fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> {
if cx.help.keyword().is_none() {
return act!(help:toggle, cx, cx.help.layer);
if cx.help.input.mode() == InputMode::Normal {
return act!(help:close, cx);
}
let help = &mut cx.help;
help.keyword = String::new();
help.in_filter = None;
help.filter_apply();
act!(escape, cx.help.input)?;
succ!(render!());
}
}

View file

@ -1,22 +0,0 @@
use anyhow::Result;
use yazi_macro::{render, succ};
use yazi_parser::VoidForm;
use yazi_shared::data::Data;
use crate::{Actor, Ctx};
pub struct Filter;
impl Actor for Filter {
type Form = VoidForm;
const NAME: &str = "filter";
fn act(cx: &mut Ctx, _: Self::Form) -> Result<Data> {
let help = &mut cx.help;
help.in_filter = Some(Default::default());
help.filter_apply();
succ!(render!());
}
}

View file

@ -1 +1 @@
yazi_macro::mod_flat!(arrow escape filter toggle);
yazi_macro::mod_flat!(arrow close escape toggle);

View file

@ -1,7 +1,10 @@
use anyhow::Result;
use ratatui_core::layout::Margin;
use yazi_config::popup::Help;
use yazi_macro::{render, succ};
use yazi_parser::help::ToggleForm;
use yazi_shared::data::Data;
use yazi_widgets::input::Input;
use crate::{Actor, Ctx};
@ -13,17 +16,24 @@ impl Actor for Toggle {
const NAME: &str = "toggle";
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
let position = Help::position();
let area = cx.mgr.area(position);
let input_area = area.inner(Margin::new(1, 1));
let help = &mut cx.help;
help.visible = !help.visible;
help.visible = true;
help.layer = form.layer;
help.position = position;
help.height = area.height;
help.keyword = String::new();
help.in_filter = None;
help.filter_apply();
help.input = Input::default();
help.input.repos(input_area);
help.keyword.clear();
help.offset = 0;
help.cursor = 0;
help.filter_apply();
succ!(render!());
}
}

View file

@ -24,10 +24,10 @@ impl Which {
impl UserData for Which {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
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_cached_field("cands", |lua, me| {
lua.create_sequence_from(me.inner.cands.iter().cloned())
});
fields.add_field_method_get("times", |_, me| Ok(me.inner.times));
fields.add_field_method_get("active", |_, me| Ok(me.inner.active));
fields.add_field_method_get("silent", |_, me| Ok(me.inner.silent));

View file

@ -23,8 +23,9 @@ impl Actor for Activate {
let which = &mut cx.which;
which.tx = opt.tx;
which.times = opt.times;
which.layer = opt.layer;
which.cands = opt.cands;
which.times = opt.times;
which.active = true;
which.silent = opt.silent;

View file

@ -18,5 +18,5 @@ proc-macro = true
[dependencies]
# External dependencies
proc-macro2 = "1"
quote = "1.0.45"
quote = "1.0.46"
syn = { version = "2.0.118", features = [ "full" ] }

View file

@ -360,9 +360,10 @@ keymap = [
[help]
keymap = [
{ on = "<Esc>", run = "escape", desc = "Clear the filter, or hide the help" },
{ on = "<C-[>", run = "escape", desc = "Clear the filter, or hide the help" },
{ on = "<C-c>", run = "close", desc = "Hide the help" },
{ on = "<Esc>", run = "escape", desc = "Enter normal mode, or hide help menu" },
{ on = "<C-[>", run = "escape", desc = "Enter normal mode, or hide help menu" },
{ on = "<C-c>", run = "close", desc = "Close help menu" },
{ on = "<Enter>", run = "close --submit", desc = "Close help menu and run selected action(s)" },
# Navigation
{ on = "k", run = "arrow prev", desc = "Previous line" },
@ -371,6 +372,6 @@ keymap = [
{ on = "<Up>", run = "arrow prev", desc = "Previous line" },
{ on = "<Down>", run = "arrow next", desc = "Next line" },
# Filtering
{ on = "f", run = "filter", desc = "Filter help items" },
{ on = "<C-p>", run = "arrow prev", desc = "Previous line" },
{ on = "<C-n>", run = "arrow next", desc = "Next line" },
]

View file

@ -226,11 +226,10 @@ hovered = { fg = "magenta", bold = true }
# : Help menu {{{
[help]
on = { fg = "cyan" }
run = { fg = "magenta" }
desc = {}
border = { fg = "blue" }
chord = { fg = "cyan" }
action = {}
hovered = { reversed = true, bold = true }
footer = { fg = "black", bg = "white" }
# : }}}

View file

@ -226,11 +226,10 @@ hovered = { fg = "magenta", bold = true }
# : Help menu {{{
[help]
on = { fg = "cyan" }
run = { fg = "magenta" }
desc = {}
border = { fg = "blue" }
chord = { fg = "cyan" }
action = {}
hovered = { reversed = true, bold = true }
footer = { fg = "black", bg = "white" }
# : }}}

View file

@ -6,7 +6,7 @@ use serde::{Deserialize, Deserializer, de};
use serde_with::{DeserializeAs, DisplayFromStr, OneOrMany};
use yazi_binding::Iter;
use yazi_codegen::DeserializeOver2;
use yazi_shared::{Layer, event::{Actions, deserialize_actions}, id::Id};
use yazi_shared::{event::{Actions, deserialize_actions}, id::Id};
use super::{Key, ids::chord_id};
use crate::{Mixable, Platform, keymap::{ChordArc, Chords}};
@ -14,12 +14,12 @@ use crate::{Mixable, Platform, keymap::{ChordArc, Chords}};
static RE: OnceLock<Regex> = OnceLock::new();
#[derive(Debug, Default, Deserialize, DeserializeOver2)]
pub struct Chord<const L: u8 = { Layer::Null as u8 }> {
pub struct Chord {
#[serde(skip, default = "chord_id")]
pub id: Id,
#[serde(deserialize_with = "deserialize_on")]
pub on: Vec<Key>,
#[serde(deserialize_with = "deserialize_actions::<L, _>")]
#[serde(deserialize_with = "deserialize_actions")]
pub run: Actions,
#[serde(default)]
pub desc: String,
@ -27,7 +27,7 @@ pub struct Chord<const L: u8 = { Layer::Null as u8 }> {
pub r#for: Platform,
}
impl<const L: u8> Clone for Chord<L> {
impl Clone for Chord {
fn clone(&self) -> Self {
Self {
id: chord_id(),
@ -39,17 +39,21 @@ impl<const L: u8> Clone for Chord<L> {
}
}
impl<const L: u8> PartialEq for Chord<L> {
impl AsRef<Self> for Chord {
fn as_ref(&self) -> &Self { self }
}
impl PartialEq for Chord {
fn eq(&self, other: &Self) -> bool { self.on == other.on }
}
impl<const L: u8> Eq for Chord<L> {}
impl Eq for Chord {}
impl<const L: u8> Hash for Chord<L> {
impl Hash for Chord {
fn hash<H: Hasher>(&self, state: &mut H) { self.on.hash(state) }
}
impl<const L: u8> Chord<L> {
impl Chord {
pub fn on(&self) -> String { self.on.iter().map(ToString::to_string).collect() }
pub fn run(&self) -> String {
@ -66,20 +70,13 @@ impl<const L: u8> Chord<L> {
pub fn desc_or_run(&self) -> Cow<'_, str> { self.desc().unwrap_or_else(|| self.run().into()) }
pub fn contains(&self, s: &str) -> bool {
let s = s.to_lowercase();
self.desc().map(|d| d.to_lowercase().contains(&s)) == Some(true)
|| self.run().to_lowercase().contains(&s)
|| self.on().to_lowercase().contains(&s)
}
#[inline]
pub(super) fn noop(&self) -> bool {
self.run.len() == 1 && self.run[0].name == "noop" && self.run[0].args.is_empty()
}
}
impl<const L: u8> Mixable for Chord<L> {
impl Mixable for Chord {
fn filter(&self) -> bool { self.r#for.matches() && !self.noop() }
}

View file

@ -1,37 +1,40 @@
use std::{ops::{Deref, DerefMut}, sync::Arc};
use mlua::{UserData, UserDataFields, Value};
use mlua::{FromLua, Lua, UserData, UserDataFields, Value};
use serde::Deserialize;
use yazi_codegen::FromLuaOwned;
use yazi_shared::{Layer, event::ActionCow};
use yazi_shim::mlua::UserDataFieldsExt;
use crate::{Mixable, keymap::Chord};
#[repr(transparent)]
#[derive(Clone, Debug, Default, Deserialize, FromLuaOwned)]
pub struct ChordArc<const L: u8 = { Layer::Null as u8 }>(Arc<Chord<L>>);
#[derive(Clone, Debug, Default, Deserialize)]
pub struct ChordArc(Arc<Chord>);
impl<const L: u8> Deref for ChordArc<L> {
type Target = Arc<Chord<L>>;
impl Deref for ChordArc {
type Target = Arc<Chord>;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl<const L: u8> DerefMut for ChordArc<L> {
impl DerefMut for ChordArc {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
}
impl<const L: u8> From<&Self> for ChordArc<L> {
impl AsRef<Chord> for ChordArc {
fn as_ref(&self) -> &Chord { self }
}
impl From<&Self> for ChordArc {
fn from(value: &Self) -> 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 From<Chord> for ChordArc {
fn from(value: Chord) -> Self { Self(Arc::new(value)) }
}
impl<const L: u8> From<ChordArc<L>> for Chord<L> {
fn from(value: ChordArc<L>) -> Self {
impl From<ChordArc> for Chord {
fn from(value: ChordArc) -> Self {
match Arc::try_unwrap(value.0) {
Ok(c) => c,
Err(arc) => Self::clone(&arc),
@ -39,60 +42,53 @@ impl<const L: u8> From<ChordArc<L>> for Chord<L> {
}
}
impl<const L: u8> From<&ChordArc<L>> for Chord<L> {
fn from(value: &ChordArc<L>) -> Self { Self::clone(value) }
impl From<&ChordArc> for Chord {
fn from(value: &ChordArc) -> Self { Self::clone(value) }
}
impl TryFrom<(Value, Layer)> for ChordArc {
type Error = mlua::Error;
fn try_from((value, layer): (Value, Layer)) -> Result<Self, Self::Error> {
use Chord as C;
use Layer as L;
let de = mlua::serde::Deserializer::new(value);
Ok(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(),
})
}
}
impl<const L: u8> ChordArc<L> {
pub fn as_erased<const M: u8>(&self) -> &ChordArc<M> {
unsafe { &*(self as *const Self as *const ChordArc<M>) }
impl ChordArc {
pub fn to_seq(&self, layer: Layer) -> Vec<ActionCow> {
self
.run
.iter()
.rev()
.cloned()
.map(|mut a| {
a.layer = a.layer.or(layer);
a.into()
})
.collect()
}
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> {
pub fn into_seq(self, layer: Layer) -> 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(),
Ok(c) => c
.run
.into_iter()
.rev()
.map(|mut a| {
a.layer = a.layer.or(layer);
a.into()
})
.collect(),
Err(arc) => Self(arc).to_seq(layer),
}
}
}
impl<const L: u8> Mixable for ChordArc<L> {
impl Mixable for ChordArc {
fn filter(&self) -> bool { self.0.filter() }
}
impl FromLua for ChordArc {
fn from_lua(value: Value, _: &Lua) -> mlua::Result<Self> {
Ok(match value {
Value::UserData(ud) => ud.take()?,
_ => Chord::deserialize(mlua::serde::Deserializer::new(value))?.into(),
})
}
}
impl UserData for ChordArc {
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("id", |_, me| Ok(me.id));

View file

@ -1,53 +1,36 @@
use std::{ops::Deref, sync::Arc};
use arc_swap::ArcSwap;
use mlua::{ExternalError, ExternalResult, MetaMethod, Table, UserData, UserDataMethods, Value};
use mlua::{ExternalError, ExternalResult, MetaMethod, Table, UserData, UserDataMethods};
use serde::Deserialize;
use yazi_shared::{Layer, event::Actions};
use yazi_shared::event::Actions;
use yazi_shim::{arc_swap::{ArcSwapExt, IntoPointee}, mlua::DeserializeOverLua, vec::{IndexAtError, VecExt}};
use crate::keymap::{Chord, ChordArc, ChordIter, ChordMatcher};
#[derive(Debug, Deserialize)]
#[derive(Debug, Default, Deserialize)]
#[serde(transparent)]
pub struct Chords<const L: u8 = { Layer::Null as u8 }> {
pub chords: ArcSwap<Vec<ChordArc<L>>>,
#[serde(skip, default = "layer_default::<L>")]
pub layer: Layer,
pub struct Chords(ArcSwap<Vec<ChordArc>>);
impl Deref for Chords {
type Target = ArcSwap<Vec<ChordArc>>;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl<const L: u8> Default for Chords<L> {
fn default() -> Self {
Self { chords: ArcSwap::default(), layer: Layer::from_repr(L).unwrap_or_default() }
}
impl From<Vec<ChordArc>> for Chords {
fn from(inner: Vec<ChordArc>) -> Self { Self(inner.into_pointee()) }
}
impl<const L: u8> Deref for Chords<L> {
type Target = ArcSwap<Vec<ChordArc<L>>>;
fn deref(&self) -> &Self::Target { &self.chords }
}
impl<const L: u8> From<Vec<ChordArc<L>>> for Chords<L> {
fn from(inner: Vec<ChordArc<L>>) -> Self {
Self { chords: inner.into_pointee(), layer: Layer::from_repr(L).unwrap_or_default() }
}
}
impl<const L: u8> Chords<L> {
pub fn as_erased<const M: u8>(&self) -> Arc<Vec<ChordArc<M>>> {
let chords = self.chords.load_full();
unsafe { Arc::from_raw(Arc::into_raw(chords) as *const Vec<ChordArc<M>>) }
}
impl Chords {
pub fn insert(&self, index: isize, rule: ChordArc) -> Result<(), IndexAtError> {
self.chords.try_rcu(|rules| {
self.0.try_rcu(|rules| {
let (before, after) = rules.split_at(rules.index_at(index)?);
Ok(
before
.iter()
.cloned()
.chain([rule.as_erased().clone()])
.chain([rule.clone()])
.chain(after.iter().cloned())
.collect::<Vec<_>>(),
)
@ -57,9 +40,9 @@ impl<const L: u8> Chords<L> {
}
pub fn remove(&self, matcher: ChordMatcher) {
self.chords.rcu(|chords| {
self.0.rcu(|chords| {
let mut next = Vec::clone(chords);
next.retain(|arc| !matcher.matches(arc.as_erased()));
next.retain(|arc| !matcher.matches(arc));
next
});
}
@ -69,11 +52,11 @@ impl<const L: u8> Chords<L> {
matcher: ChordMatcher,
f: impl Fn(Chord) -> Result<Chord, E>,
) -> Result<(), E> {
self.chords.try_rcu(|rules| {
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();
if matcher.matches(arc) {
*arc = f(Chord::clone(arc))?.into();
}
}
Ok(Arc::new(next))
@ -82,8 +65,8 @@ impl<const L: u8> Chords<L> {
Ok(())
}
pub(crate) fn unwrap_unchecked(self) -> Vec<ChordArc<L>> {
Arc::try_unwrap(self.chords.into_inner()).expect("unique chords arc")
pub(crate) fn unwrap_unchecked(self) -> Vec<ChordArc> {
Arc::try_unwrap(self.0.into_inner()).expect("unique chords arc")
}
}
@ -91,21 +74,19 @@ impl UserData for &Chords {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("match", |_, &me, matcher: Option<ChordMatcher>| {
Ok(match matcher {
Some(matcher) => ChordIter { chords: me.as_erased(), matcher, ..Default::default() },
Some(matcher) => ChordIter { chords: me.load_full(), matcher, ..Default::default() },
None => me.into(),
})
});
methods.add_method("insert", |_, me, (index, value): (isize, Value)| {
methods.add_method("insert", |_, me, (index, chord): (isize, ChordArc)| {
let index = match index {
1.. => index - 1,
0 => return Err("index must be 1-based or negative".into_lua_err()),
_ => index,
};
let chord = ChordArc::try_from((value, me.layer))?;
me.insert(index, chord.clone()).into_lua_err()?;
Ok(chord)
});
@ -118,7 +99,7 @@ impl UserData for &Chords {
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);
run.set_source(yazi_shared::Source::Key);
}
me.update(matcher, |mut chord| {
@ -133,5 +114,3 @@ impl UserData for &Chords {
methods.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.load().len()));
}
}
pub(super) fn layer_default<const L: u8>() -> Layer { Layer::from_repr(L).unwrap_or_default() }

View file

@ -1,4 +1,4 @@
use std::{ops::Deref, sync::Arc};
use std::sync::Arc;
use anyhow::{Context, Result};
use serde::Deserialize;
@ -11,20 +11,20 @@ use crate::keymap::ChordArc;
#[derive(Deserialize, DeserializeOver, DeserializeOver1)]
pub struct Keymap {
pub mgr: KeymapSection<{ Layer::Mgr as u8 }>,
pub tasks: KeymapSection<{ Layer::Tasks as u8 }>,
pub spot: KeymapSection<{ Layer::Spot as u8 }>,
pub pick: KeymapSection<{ Layer::Pick as u8 }>,
pub input: KeymapSection<{ Layer::Input as u8 }>,
pub confirm: KeymapSection<{ Layer::Confirm as u8 }>,
pub help: KeymapSection<{ Layer::Help as u8 }>,
pub cmp: KeymapSection<{ Layer::Cmp as u8 }>,
pub mgr: KeymapSection,
pub tasks: KeymapSection,
pub spot: KeymapSection,
pub pick: KeymapSection,
pub input: KeymapSection,
pub confirm: KeymapSection,
pub help: KeymapSection,
pub cmp: KeymapSection,
}
impl Keymap {
pub fn chords(&self, layer: Layer) -> Arc<Vec<ChordArc>> {
match self.section(layer) {
Some(s) => s.deref().as_erased(),
Some(s) => s.load_full(),
None => Arc::new(Vec::new()),
}
}
@ -34,14 +34,14 @@ impl Keymap {
Some(match layer {
L::Null | L::App => None?,
L::Mgr => self.mgr.as_erased(),
L::Tasks => self.tasks.as_erased(),
L::Spot => self.spot.as_erased(),
L::Pick => self.pick.as_erased(),
L::Input => self.input.as_erased(),
L::Confirm => self.confirm.as_erased(),
L::Help => self.help.as_erased(),
L::Cmp => self.cmp.as_erased(),
L::Mgr => &self.mgr,
L::Tasks => &self.tasks,
L::Spot => &self.spot,
L::Pick => &self.pick,
L::Input => &self.input,
L::Confirm => &self.confirm,
L::Help => &self.help,
L::Cmp => &self.cmp,
L::Which => None?,
L::Notify => None?,
})

View file

@ -4,53 +4,45 @@ use hashbrown::HashSet;
use mlua::{UserData, UserDataFields};
use serde::Deserialize;
use yazi_codegen::DeserializeOver2;
use yazi_shared::Layer;
use yazi_shim::{mlua::UserDataFieldsExt, toml::DeserializeOverHook};
use super::{Key, Chord, Chords, chords::layer_default};
use super::{Chord, Chords, Key};
use crate::{keymap::ChordArc, mix};
#[derive(Default, Deserialize, DeserializeOver2)]
pub struct KeymapSection<const L: u8 = { Layer::Null as u8 }> {
keymap: Chords<L>,
pub struct KeymapSection {
keymap: Chords,
#[serde(default)]
prepend_keymap: Vec<Chord<L>>,
prepend_keymap: Vec<Chord>,
#[serde(default)]
append_keymap: Vec<Chord<L>>,
#[serde(default = "layer_default::<L>")]
layer: Layer,
append_keymap: Vec<Chord>,
}
impl<const L: u8> Deref for KeymapSection<L> {
type Target = Chords<L>;
impl Deref for KeymapSection {
type Target = Chords;
fn deref(&self) -> &Self::Target { &self.keymap }
}
impl<const L: u8> KeymapSection<L> {
pub fn as_erased<const M: u8>(&self) -> &KeymapSection<M> {
unsafe { &*(self as *const Self as *const KeymapSection<M>) }
}
}
impl<const L: u8> DeserializeOverHook for KeymapSection<L> {
impl DeserializeOverHook for KeymapSection {
fn deserialize_over_hook(self) -> Result<Self, toml::de::Error> {
#[inline]
fn on<const L: u8>(Chord { on, .. }: &Chord<L>) -> [Key; 2] {
[on.first().copied().unwrap_or_default(), on.get(1).copied().unwrap_or_default()]
fn on<T: AsRef<Chord>>(chord: T) -> [Key; 2] {
let c = chord.as_ref();
[c.on.first().copied().unwrap_or_default(), c.on.get(1).copied().unwrap_or_default()]
}
let keymap = self.keymap.unwrap_unchecked();
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(on).collect();
let keymap: Vec<ChordArc<L>> = mix(
let keymap: Vec<ChordArc> = mix(
self.prepend_keymap,
keymap.into_iter().filter(|v| !a_seen.contains(&on(v))),
self.append_keymap.into_iter().filter(|v| !b_seen.contains(&on(v))),
);
Ok(Self { keymap: keymap.into(), layer: self.layer, ..Default::default() })
Ok(Self { keymap: keymap.into(), ..Default::default() })
}
}

View file

@ -0,0 +1,9 @@
use yazi_binding::position::{Offset, Origin, Position};
pub struct Help;
impl Help {
pub fn position() -> Position {
Position::new(Origin::Center, Offset { x: 0, y: 0, width: 80, height: 25 })
}
}

View file

@ -1 +1 @@
yazi_macro::mod_flat!(confirm input options pick);
yazi_macro::mod_flat!(confirm help input options pick);

View file

@ -9,6 +9,7 @@ use yazi_fs::{Xdg, ok_or_not_found, path::sanitize_path};
use yazi_shim::{arc_swap::IntoPointee, cell::SyncCell};
use super::{Custom, Filetype, Flavor, Icon};
use crate::YAZI;
#[derive(Deserialize, DeserializeOver, DeserializeOver1, Overlay)]
pub struct Theme {
@ -232,7 +233,11 @@ pub struct Input {
impl From<&Input> for yazi_widgets::input::InputStyles {
fn from(input: &Input) -> Self {
Self { normal: Some(input.value.get().into()), selected: Some(input.selected.get().into()) }
Self {
normal: Some(input.value.get().into()),
selected: Some(input.selected.get().into()),
blink: Some(YAZI.input.cursor_blink),
}
}
}
@ -259,12 +264,10 @@ pub struct Tasks {
// --- Help
#[derive(Deserialize, DeserializeOver, DeserializeOver2, Overlay)]
pub struct Help {
pub on: SyncCell<StyleFlat>,
pub run: SyncCell<StyleFlat>,
pub desc: SyncCell<StyleFlat>,
pub border: SyncCell<StyleFlat>,
pub chord: SyncCell<StyleFlat>,
pub action: SyncCell<StyleFlat>,
pub hovered: SyncCell<StyleFlat>,
pub footer: SyncCell<StyleFlat>,
}
fn deserialize_syntect_theme<'de, D>(deserializer: D) -> Result<ArcSwap<PathBuf>, D::Error>

View file

@ -33,6 +33,11 @@ impl Core {
}
pub fn cursor(&self) -> Option<(Position, SetCursorStyle)> {
if let Some(cursor) = self.help.cursor() {
let Rect { x, y, .. } = self.mgr.area(self.help.position).padding(self.help.padding());
return Some((Position { x: x + cursor, y }, self.help.cursor_shape()?));
}
if let Some(guard) = self.input.lock() {
let Rect { x, y, .. } = match &guard {
InputGuard::Main(_) => self.mgr.area(self.input.position()?).padding(self.input.padding()),
@ -41,10 +46,6 @@ impl Core {
return Some((Position { x: x + guard.cursor(), y }, guard.cursor_shape()));
}
if let Some((x, y)) = self.help.cursor() {
return Some((Position { x, y }, self.help.cursor_shape()?));
}
None
}

View file

@ -1,46 +1,33 @@
use anyhow::Result;
use unicode_width::UnicodeWidthStr;
use ratatui_widgets::block::Padding;
use yazi_binding::position::Position;
use yazi_config::{KEYMAP, keymap::ChordArc};
use yazi_macro::{act, render, render_and};
use yazi_macro::render;
use yazi_shared::Layer;
use yazi_term::{TERM, event::{KeyCode, KeyEvent}};
use yazi_term::event::KeyEvent;
use yazi_tty::sequence::SetCursorStyle;
use yazi_widgets::Scrollable;
use crate::help::HELP_MARGIN;
use yazi_widgets::{Scrollable, input::Input};
#[derive(Default)]
pub struct Help {
pub visible: bool,
pub layer: Layer,
pub(super) bindings: Vec<ChordArc>,
pub visible: bool,
pub layer: Layer,
pub position: Position,
pub bindings: Vec<ChordArc>,
// Filter
pub keyword: String,
pub in_filter: Option<yazi_widgets::input::Input>,
pub input: Input,
pub keyword: String,
pub offset: usize,
pub cursor: usize,
pub height: u16,
}
impl Help {
pub fn r#type(&mut self, key: KeyEvent) -> Result<bool> {
let Some(input) = &mut self.in_filter else { return Ok(false) };
match key {
KeyEvent { code: KeyCode::Escape, modifiers, .. } if modifiers.is_empty() => {
self.in_filter = None;
render!();
}
KeyEvent { code: KeyCode::Enter, modifiers, .. } if modifiers.is_empty() => {
self.in_filter = None;
return Ok(render_and!(true)); // Don't do the `filter_apply` below, since we already have the filtered results.
}
KeyEvent { code: KeyCode::Backspace, modifiers, .. } if modifiers.is_empty() => {
act!(backspace, input)?;
}
_ => {
input.r#type(key)?;
}
if !self.input.r#type(key)? {
return Ok(false);
}
self.filter_apply();
@ -48,15 +35,20 @@ impl Help {
}
pub fn filter_apply(&mut self) {
let kw = self.in_filter.as_ref().map_or("", |i| i.value());
let kw = self.input.value();
if kw.is_empty() {
self.keyword = String::new();
self.keyword.clear();
self.bindings = KEYMAP.chords(self.layer).iter().cloned().collect();
} else if self.keyword != kw {
let lowercased = kw.to_lowercase();
self.keyword = kw.to_owned();
self.bindings =
KEYMAP.chords(self.layer).iter().filter(|&c| c.contains(kw)).cloned().collect();
self.bindings = KEYMAP
.chords(self.layer)
.iter()
.filter(|&c| c.desc_or_run().to_lowercase().contains(&lowercased))
.cloned()
.collect();
}
render!(self.scroll(0));
@ -64,15 +56,7 @@ impl Help {
}
impl Help {
// --- Keyword
pub fn keyword(&self) -> Option<String> {
self
.in_filter
.as_ref()
.map(|i| i.value())
.or(Some(self.keyword.as_str()).filter(|&s| !s.is_empty()))
.map(|s| format!("Filter: {s}"))
}
pub fn padding(&self) -> Padding { Padding::new(1, 1, 1, 1) }
// --- Bindings
pub fn window(&self) -> &[ChordArc] {
@ -81,27 +65,22 @@ impl Help {
}
// --- Cursor
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.dimension().rows));
}
None
}
pub fn cursor(&self) -> Option<u16> { self.visible.then_some(self.input.cursor()) }
pub fn rel_cursor(&self) -> usize { self.cursor - self.offset }
pub fn cursor_shape(&self) -> Option<SetCursorStyle> {
Some(self.in_filter.as_ref()?.cursor_shape())
self.visible.then_some(self.input.cursor_shape())
}
}
impl Scrollable for Help {
fn total(&self) -> usize { self.bindings.len() }
fn limit(&self) -> usize { TERM.dimension().rows.saturating_sub(HELP_MARGIN) as usize }
fn limit(&self) -> usize {
let p = self.padding();
self.height.saturating_sub(p.top + /* input */ 1 + /* divider */ 1 + p.bottom) as usize
}
fn cursor_mut(&mut self) -> &mut usize { &mut self.cursor }

View file

@ -1,3 +1 @@
yazi_macro::mod_flat!(help);
const HELP_MARGIN: u16 = 1;

View file

@ -7,9 +7,10 @@ use yazi_shared::{Layer, event::ActionCow};
#[derive(Clone, Debug)]
pub struct WhichOpt {
pub tx: Option<mpsc::UnboundedSender<Option<ChordArc>>>,
pub layer: Layer,
pub cands: Vec<ChordArc>,
pub silent: bool,
pub times: usize,
pub silent: bool,
}
impl_data_any!(WhichOpt);
@ -24,19 +25,21 @@ impl TryFrom<ActionCow> for WhichOpt {
Ok(Self {
tx: a.take_any2("tx").transpose()?,
layer: a.str("layer").parse()?,
cands: a.take_any_iter().collect(),
silent: a.bool("silent"),
times: a.get("times").unwrap_or(0),
silent: a.bool("silent"),
})
}
}
impl From<(Layer, Key)> for WhichOpt {
fn from((layer, key): (Layer, Key)) -> Self {
impl WhichOpt {
pub fn new(src: Layer, dist: Layer, key: Key) -> Self {
Self {
tx: None,
layer: dist,
cands: KEYMAP
.chords(layer)
.chords(src)
.iter()
.filter(|&c| c.on.len() > 1 && c.on[0] == key)
.cloned()
@ -55,6 +58,7 @@ impl FromLua for WhichOpt {
Ok(Self {
tx: t.raw_get::<yazi_binding::MpscUnboundedTx<_>>("tx").ok().map(|t| t.0),
layer: t.raw_get("layer")?,
cands: t.raw_get::<Table>("cands")?.sequence_values().collect::<mlua::Result<Vec<_>>>()?,
times: t.raw_get("times").unwrap_or_default(),
silent: t.raw_get("silent")?,
@ -68,6 +72,7 @@ impl IntoLua for WhichOpt {
lua
.create_table_from([
("tx", self.tx.map(yazi_binding::MpscUnboundedTx).into_lua(lua)?),
("layer", self.layer.into_lua(lua)?),
("cands", lua.create_sequence_from(self.cands)?.into_lua(lua)?),
("times", self.times.into_lua(lua)?),
("silent", self.silent.into_lua(lua)?),

View file

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

View file

@ -321,16 +321,18 @@ impl<'a> Executor<'a> {
on!(escape);
on!(arrow);
on!(filter);
on!(close);
match action.name.as_ref() {
// Help
"close" => act!(help:toggle, cx, Layer::Help),
// Plugin
"plugin" => act!(app:plugin, cx, action),
// Lua
"lua" => act!(app:lua, cx, action),
_ => succ!(),
_ => {
cx.help.input.execute(action)?;
cx.help.filter_apply();
succ!()
}
}
}

View file

@ -18,26 +18,18 @@ impl Widget for Bindings<'_> {
return;
}
// On
// Chord
let col1: Vec<_> =
bindings.iter().map(|c| ListItem::new(c.on()).style(THEME.help.on.get())).collect();
bindings.iter().map(|c| ListItem::new(c.on()).style(THEME.help.chord.get())).collect();
// Run
let col2: Vec<_> =
bindings.iter().map(|c| ListItem::new(c.run()).style(THEME.help.run.get())).collect();
// Desc
let col3: Vec<_> = bindings
// Action
let col2: Vec<_> = bindings
.iter()
.map(|c| ListItem::new(c.desc().unwrap_or("-".into())).style(THEME.help.desc.get()))
.map(|c| ListItem::new(c.desc_or_run()).style(THEME.help.action.get()))
.collect();
let chunks = layout::Layout::horizontal([
Constraint::Ratio(2, 10),
Constraint::Ratio(3, 10),
Constraint::Ratio(5, 10),
])
.split(area);
let chunks =
layout::Layout::horizontal([Constraint::Length(20), Constraint::Fill(1)]).split(area);
let cursor = self.core.help.rel_cursor() as u16;
buf.set_style(
@ -47,6 +39,5 @@ impl Widget for Bindings<'_> {
List::new(col1).render(chunks[0], buf);
List::new(col2).render(chunks[1], buf);
List::new(col3).render(chunks[2], buf);
}
}

View file

@ -1,6 +1,8 @@
use ratatui_core::{buffer::Buffer, layout::{self, Constraint, Rect}, text::Line, widgets::Widget};
use yazi_config::{KEYMAP, THEME};
use ratatui_core::{buffer::Buffer, layout::{Alignment, Constraint, Layout, Rect}, symbols::merge::MergeStrategy, widgets::Widget};
use ratatui_widgets::{block::{Block, Padding}, borders::BorderType};
use yazi_config::THEME;
use yazi_core::Core;
use yazi_shim::ratatui::Padable;
use super::Bindings;
@ -10,27 +12,39 @@ pub(crate) struct Help<'a> {
impl<'a> Help<'a> {
pub fn new(core: &'a Core) -> Self { Self { core } }
fn tips() -> String {
match KEYMAP.help.load().iter().find(|&c| c.run.iter().any(|a| a.name == "filter")) {
Some(c) => format!(" (Press `{}` to filter)", c.on()),
None => String::new(),
}
}
}
impl Widget for Help<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
fn render(self, _: Rect, buf: &mut Buffer) {
let help = &self.core.help;
let area = self.core.mgr.area(help.position);
let padding = help.padding();
yazi_widgets::clear::Clear::default().render(area, buf);
let chunks = layout::Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).split(area);
Line::styled(
help.keyword().unwrap_or_else(|| format!("{}.help{}", help.layer, Self::tips())),
THEME.help.footer.get(),
)
.render(chunks[1], buf);
Block::bordered()
.title(format!("{}.help", help.layer))
.title_alignment(Alignment::Center)
.border_type(BorderType::Rounded)
.border_style(THEME.help.border.get())
.render(area, buf);
Bindings::new(self.core).render(chunks[0], buf);
let chunks =
Layout::vertical([Constraint::Length(1), Constraint::Length(1), Constraint::Fill(1)])
.split(area.padding(Padding { left: 0, right: 0, ..padding }));
// Input
help.input.render(chunks[0].padding(Padding { top: 0, bottom: 0, ..padding }), buf);
// Divider
Block::bordered()
.border_type(BorderType::Rounded)
.border_style(THEME.help.border.get())
.merge_borders(MergeStrategy::Fuzzy)
.render(chunks[1], buf);
// Bindings
Bindings::new(self.core)
.render(chunks[2].padding(Padding { top: 0, bottom: 0, ..padding }), buf);
}
}

View file

@ -1,6 +1,7 @@
use anyhow::Result;
use yazi_actor::Ctx;
use yazi_config::{KEYMAP, keymap::{Chord, Key}};
use yazi_core::which::WhichOpt;
use yazi_macro::act;
use yazi_shared::Layer;
use yazi_term::event::KeyEvent;
@ -32,16 +33,17 @@ impl<'a> Router<'a> {
let key = Key::from(key);
Ok(match layer {
L::Null | L::App | L::Notify => unreachable!(),
L::Mgr | L::Tasks | L::Spot | L::Pick | L::Input | L::Confirm | L::Help => {
self.matches(layer, key)
L::Mgr | L::Tasks | L::Spot | L::Pick | L::Input | L::Confirm => {
self.matches(layer, layer, key)
}
L::Cmp => self.matches(L::Cmp, key) || self.matches(L::Input, key),
L::Help => self.matches(L::Help, L::Help, key) || self.matches(L::Input, L::Help, key),
L::Cmp => self.matches(L::Cmp, L::Cmp, key) || self.matches(L::Input, L::Input, key),
L::Which => core.which.r#type(key),
})
}
fn matches(&mut self, layer: Layer, key: Key) -> bool {
for chord in &*KEYMAP.chords(layer) {
fn matches(&mut self, src: Layer, dist: Layer, key: Key) -> bool {
for chord in &*KEYMAP.chords(src) {
let Chord { on, .. } = chord.as_ref();
if on.is_empty() || on[0] != key {
continue;
@ -49,9 +51,9 @@ impl<'a> Router<'a> {
if on.len() > 1 {
let cx = &mut Ctx::active(&mut self.app.core, &mut self.app.term);
act!(which:activate, cx, (layer, key)).ok();
act!(which:activate, cx, WhichOpt::new(src, dist, key)).ok();
} else {
Dispatcher::new(self.app).dispatch_seq(chord.to_seq());
Dispatcher::new(self.app).dispatch_seq(chord.to_seq(dist));
}
return true;
}

View file

@ -0,0 +1,25 @@
use mlua::{FromLua, IntoLua, Lua, LuaSerdeExt, Value};
use serde::{Deserialize, Serialize};
use yazi_shared::event::ActionCow;
use yazi_shim::mlua::SER_OPT;
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct CloseForm {
pub submit: bool,
}
impl From<ActionCow> for CloseForm {
fn from(a: ActionCow) -> Self { Self { submit: a.bool("submit") } }
}
impl From<bool> for CloseForm {
fn from(submit: bool) -> Self { Self { submit } }
}
impl FromLua for CloseForm {
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> { lua.from_value(value) }
}
impl IntoLua for CloseForm {
fn into_lua(self, lua: &Lua) -> mlua::Result<Value> { lua.to_value_with(&self, SER_OPT) }
}

View file

@ -1 +1 @@
yazi_macro::mod_flat!(toggle);
yazi_macro::mod_flat!(close toggle);

View file

@ -110,8 +110,8 @@ pub enum Spark<'a> {
// Help
HelpArrow(crate::ArrowForm),
HelpClose(crate::help::CloseForm),
HelpEscape(crate::VoidForm),
HelpFilter(crate::VoidForm),
HelpToggle(crate::help::ToggleForm),
// Input
@ -303,8 +303,8 @@ impl<'a> IntoLua for Spark<'a> {
// Help
Self::HelpArrow(b) => b.into_lua(lua),
Self::HelpClose(b) => b.into_lua(lua),
Self::HelpEscape(b) => b.into_lua(lua),
Self::HelpFilter(b) => b.into_lua(lua),
Self::HelpToggle(b) => b.into_lua(lua),
// Input
@ -395,6 +395,7 @@ try_from_spark!(crate::cmp::ShowForm, cmp:show);
try_from_spark!(crate::cmp::TriggerForm, cmp:trigger);
try_from_spark!(crate::confirm::CloseForm, confirm:close);
try_from_spark!(crate::confirm::ShowForm, confirm:show);
try_from_spark!(crate::help::CloseForm, help:close);
try_from_spark!(crate::help::ToggleForm, help:toggle);
try_from_spark!(crate::input::CloseForm, input:close);
try_from_spark!(crate::mgr::BulkExitForm, mgr:bulk_exit);

View file

@ -1,15 +1,14 @@
use mlua::{FromLua, IntoLua, Lua, Value};
use yazi_config::keymap::Key;
use yazi_core::which::WhichOpt;
use yazi_shared::{Layer, event::ActionCow};
use yazi_shared::event::ActionCow;
#[derive(Clone, Debug)]
pub struct ActivateForm {
pub opt: WhichOpt,
}
impl From<(Layer, Key)> for ActivateForm {
fn from(value: (Layer, Key)) -> Self { Self { opt: value.into() } }
impl From<WhichOpt> for ActivateForm {
fn from(opt: WhichOpt) -> Self { Self { opt } }
}
impl TryFrom<ActionCow> for ActivateForm {

View file

@ -361,12 +361,10 @@ fn help() -> Composer<ComposerGet, ComposerSet> {
fn get(lua: &Lua, key: &[u8]) -> mlua::Result<Value> {
let t = &THEME.help;
match key {
b"on" => Style::from(&t.on).into_lua(lua),
b"run" => Style::from(&t.run).into_lua(lua),
b"desc" => Style::from(&t.desc).into_lua(lua),
b"border" => Style::from(&t.border).into_lua(lua),
b"chord" => Style::from(&t.chord).into_lua(lua),
b"action" => Style::from(&t.action).into_lua(lua),
b"hovered" => Style::from(&t.hovered).into_lua(lua),
b"footer" => Style::from(&t.footer).into_lua(lua),
_ => Ok(Value::Nil),
}

View file

@ -25,7 +25,7 @@ impl Utils {
.enumerate()
.map(|(i, cand)| {
let cand = cand?;
Ok(ChordArc::from(Chord::<{ Layer::Null as u8 }> {
Ok(ChordArc::from(Chord {
id: yazi_config::keymap::chord_id(),
on: Self::parse_keys(cand.raw_get("on")?)?,
run: relay!(which:callback, [i + 1]).into(),

View file

@ -2,6 +2,7 @@ use tokio::sync::mpsc;
use yazi_config::keymap::ChordArc;
use yazi_core::which::WhichOpt;
use yazi_macro::{emit, relay};
use yazi_shared::Layer;
pub struct WhichProxy;
@ -10,9 +11,10 @@ impl WhichProxy {
let (tx, mut rx) = mpsc::unbounded_channel();
emit!(Call(relay!(which:activate).with_any("opt", WhichOpt {
tx: Some(tx),
layer: Layer::Null,
cands,
silent,
times: 0,
silent,
})));
rx.recv().await?
}

View file

@ -1,10 +1,10 @@
use std::{ops::{Deref, DerefMut}, slice, vec};
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use serde::{Deserializer, de};
use serde::Deserializer;
use serde_with::{DeserializeAs, DisplayFromStr, OneOrMany};
use crate::{Layer, Source, event::Action};
use crate::{Source, event::Action};
#[derive(Clone, Debug, Default)]
pub struct Actions(pub Vec<Action>);
@ -28,12 +28,9 @@ impl From<Vec<Action>> for Actions {
}
impl Actions {
pub fn set(&mut self, layer: Layer, source: Source) {
pub fn set_source(&mut self, source: Source) {
for action in &mut self.0 {
action.source = source;
if action.layer == Layer::Null {
action.layer = layer;
}
}
}
}
@ -68,16 +65,12 @@ impl IntoLua for Actions {
}
}
pub fn deserialize_actions<'de, const L: u8, D>(deserializer: D) -> Result<Actions, D::Error>
pub fn deserialize_actions<'de, D>(deserializer: D) -> Result<Actions, D::Error>
where
D: Deserializer<'de>,
{
let Some(layer) = Layer::from_repr(L) else {
return Err(de::Error::custom(format!("invalid keymap layer const: {L}")));
};
let mut actions = Actions(OneOrMany::<DisplayFromStr>::deserialize_as(deserializer)?);
actions.set(layer, Source::Key);
actions.set_source(Source::Key);
Ok(actions)
}

View file

@ -1,4 +1,4 @@
use mlua::{MetaMethod, UserData, UserDataMethods};
use mlua::{ExternalError, ExternalResult, FromLua, MetaMethod, UserData, UserDataMethods, Value};
use serde::Deserialize;
use strum::{Display, EnumString, FromRepr, IntoStaticStr};
use yazi_shim::strum::IntoStr;
@ -17,8 +17,8 @@ use yazi_shim::strum::IntoStr;
IntoStaticStr,
PartialEq,
)]
#[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "kebab-case")]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
#[repr(u8)]
pub enum Layer {
#[default]
@ -36,6 +36,20 @@ pub enum Layer {
Notify,
}
impl Layer {
pub fn or(self, other: Self) -> Self { if self == Self::Null { other } else { self } }
}
impl FromLua for Layer {
fn from_lua(value: Value, _: &mlua::Lua) -> mlua::Result<Self> {
Ok(match value {
Value::String(s) => s.to_str()?.parse().into_lua_err()?,
Value::UserData(ud) => *ud.borrow::<Self>()?,
_ => Err("expected a string or a Layer".into_lua_err())?,
})
}
}
impl UserData for Layer {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(MetaMethod::ToString, |_, me, ()| Ok(me.into_str()));

View file

@ -2,6 +2,7 @@ use strum::IntoStaticStr;
use yazi_shared::id::Id;
#[derive(Debug, IntoStaticStr)]
#[strum(serialize_all = "lowercase")]
pub enum InputEvent {
Submit(String),
Cancel(String),

View file

@ -16,7 +16,6 @@ pub struct Input {
pub snaps: InputSnaps,
pub styles: InputStyles,
pub obscure: bool,
pub blinking: bool,
pub realtime: bool,
pub completion: bool,
@ -30,7 +29,6 @@ impl Input {
snaps: InputSnaps::new(opt.value, opt.obscure),
styles: opt.styles,
obscure: opt.obscure,
blinking: opt.blinking,
realtime: opt.realtime,
completion: opt.completion,
@ -133,13 +131,14 @@ impl Input {
pub fn cursor_shape(&self) -> SetCursorStyle {
use InputMode as M;
let blink = self.styles.blink.unwrap_or(false);
match self.mode() {
M::Normal if self.blinking => SetCursorStyle::BlinkingBlock,
M::Normal if !self.blinking => SetCursorStyle::SteadyBlock,
M::Insert if self.blinking => SetCursorStyle::BlinkingBar,
M::Insert if !self.blinking => SetCursorStyle::SteadyBar,
M::Replace if self.blinking => SetCursorStyle::BlinkingUnderline,
M::Replace if !self.blinking => SetCursorStyle::SteadyUnderline,
M::Normal if blink => SetCursorStyle::BlinkingBlock,
M::Normal if !blink => SetCursorStyle::SteadyBlock,
M::Insert if blink => SetCursorStyle::BlinkingBar,
M::Insert if !blink => SetCursorStyle::SteadyBar,
M::Replace if blink => SetCursorStyle::BlinkingUnderline,
M::Replace if !blink => SetCursorStyle::SteadyUnderline,
M::Normal | M::Insert | M::Replace => unreachable!(),
}
}

View file

@ -29,6 +29,7 @@ impl InputArc {
let new = lua.create_function(move |_, (_, mut opt): (Table, InputOpt)| {
opt.styles.normal = opt.styles.normal.or(styles.normal);
opt.styles.selected = opt.styles.selected.or(styles.selected);
opt.styles.blink = opt.styles.blink.or(styles.blink);
opt = opt.with_cb(|e| err!(Pubsub::pub_after_input((&e).into_str(), e.value())));
Ok(Self::from(Input::new(opt)?))
})?;

View file

@ -5,6 +5,7 @@ use ratatui_core::style::Style;
pub struct InputStyles {
pub normal: Option<Style>,
pub selected: Option<Style>,
pub blink: Option<bool>,
}
impl TryFrom<Table> for InputStyles {
@ -14,6 +15,7 @@ impl TryFrom<Table> for InputStyles {
Ok(Self {
normal: t.raw_get::<Option<yazi_binding::style::Style>>("normal")?.map(Into::into),
selected: t.raw_get::<Option<yazi_binding::style::Style>>("selected")?.map(Into::into),
blink: t.raw_get::<Option<bool>>("blink")?,
})
}
}