diff --git a/.github/workflows/cachix.yml b/.github/workflows/cachix.yml index 686a19713..9a0825300 100644 --- a/.github/workflows/cachix.yml +++ b/.github/workflows/cachix.yml @@ -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 diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index b9adca933..77b813ee1 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -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 }} diff --git a/.github/workflows/draft.yml b/.github/workflows/draft.yml index 976dd90f0..ec5aa5d00 100644 --- a/.github/workflows/draft.yml +++ b/.github/workflows/draft.yml @@ -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: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c00d7bcae..7ea29bc18 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/.github/workflows/validate-form.yml b/.github/workflows/validate-form.yml index bd8d9764b..81ccc28a4 100644 --- a/.github/workflows/validate-form.yml +++ b/.github/workflows/validate-form.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0240dec6b..1e5097151 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `` to `` ([#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 diff --git a/Cargo.lock b/Cargo.lock index b03769788..9f6ac5b83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/yazi-actor/src/help/close.rs b/yazi-actor/src/help/close.rs new file mode 100644 index 000000000..2fa170348 --- /dev/null +++ b/yazi-actor/src/help/close.rs @@ -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 { + 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!()); + } +} diff --git a/yazi-actor/src/help/escape.rs b/yazi-actor/src/help/escape.rs index 3e26003c9..16e8147cc 100644 --- a/yazi-actor/src/help/escape.rs +++ b/yazi-actor/src/help/escape.rs @@ -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 { - 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!()); } } diff --git a/yazi-actor/src/help/filter.rs b/yazi-actor/src/help/filter.rs deleted file mode 100644 index 94cab6898..000000000 --- a/yazi-actor/src/help/filter.rs +++ /dev/null @@ -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 { - let help = &mut cx.help; - - help.in_filter = Some(Default::default()); - help.filter_apply(); - succ!(render!()); - } -} diff --git a/yazi-actor/src/help/mod.rs b/yazi-actor/src/help/mod.rs index 37276cc1c..dae98bf5d 100644 --- a/yazi-actor/src/help/mod.rs +++ b/yazi-actor/src/help/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(arrow escape filter toggle); +yazi_macro::mod_flat!(arrow close escape toggle); diff --git a/yazi-actor/src/help/toggle.rs b/yazi-actor/src/help/toggle.rs index 2a55080d1..3311c8636 100644 --- a/yazi-actor/src/help/toggle.rs +++ b/yazi-actor/src/help/toggle.rs @@ -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 { - let help = &mut cx.help; + let position = Help::position(); + let area = cx.mgr.area(position); + let input_area = area.inner(Margin::new(1, 1)); - help.visible = !help.visible; + let help = &mut cx.help; + 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!()); } } diff --git a/yazi-actor/src/lives/which.rs b/yazi-actor/src/lives/which.rs index 3e80cb7f5..3b2aa9362 100644 --- a/yazi-actor/src/lives/which.rs +++ b/yazi-actor/src/lives/which.rs @@ -24,10 +24,10 @@ impl Which { impl UserData for Which { fn add_fields>(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)); diff --git a/yazi-actor/src/which/activate.rs b/yazi-actor/src/which/activate.rs index cd6f8f05a..a63ab7d18 100644 --- a/yazi-actor/src/which/activate.rs +++ b/yazi-actor/src/which/activate.rs @@ -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; diff --git a/yazi-codegen/Cargo.toml b/yazi-codegen/Cargo.toml index 774277109..4dfcf0dc2 100644 --- a/yazi-codegen/Cargo.toml +++ b/yazi-codegen/Cargo.toml @@ -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" ] } diff --git a/yazi-config/preset/keymap-default.toml b/yazi-config/preset/keymap-default.toml index 900cf806b..bb115ac6d 100644 --- a/yazi-config/preset/keymap-default.toml +++ b/yazi-config/preset/keymap-default.toml @@ -360,9 +360,10 @@ keymap = [ [help] keymap = [ - { on = "", run = "escape", desc = "Clear the filter, or hide the help" }, - { on = "", run = "escape", desc = "Clear the filter, or hide the help" }, - { on = "", run = "close", desc = "Hide the help" }, + { on = "", run = "escape", desc = "Enter normal mode, or hide help menu" }, + { on = "", run = "escape", desc = "Enter normal mode, or hide help menu" }, + { on = "", run = "close", desc = "Close help menu" }, + { on = "", 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 = "", run = "arrow prev", desc = "Previous line" }, { on = "", run = "arrow next", desc = "Next line" }, - # Filtering - { on = "f", run = "filter", desc = "Filter help items" }, + { on = "", run = "arrow prev", desc = "Previous line" }, + { on = "", run = "arrow next", desc = "Next line" }, ] diff --git a/yazi-config/preset/theme-dark.toml b/yazi-config/preset/theme-dark.toml index acfe6527f..90d6eca63 100644 --- a/yazi-config/preset/theme-dark.toml +++ b/yazi-config/preset/theme-dark.toml @@ -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" } # : }}} diff --git a/yazi-config/preset/theme-light.toml b/yazi-config/preset/theme-light.toml index 7730c866a..43cf9552d 100644 --- a/yazi-config/preset/theme-light.toml +++ b/yazi-config/preset/theme-light.toml @@ -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" } # : }}} diff --git a/yazi-config/src/keymap/chord.rs b/yazi-config/src/keymap/chord.rs index 44c211359..ec74eb982 100644 --- a/yazi-config/src/keymap/chord.rs +++ b/yazi-config/src/keymap/chord.rs @@ -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 = OnceLock::new(); #[derive(Debug, Default, Deserialize, DeserializeOver2)] -pub struct Chord { +pub struct Chord { #[serde(skip, default = "chord_id")] pub id: Id, #[serde(deserialize_with = "deserialize_on")] pub on: Vec, - #[serde(deserialize_with = "deserialize_actions::")] + #[serde(deserialize_with = "deserialize_actions")] pub run: Actions, #[serde(default)] pub desc: String, @@ -27,7 +27,7 @@ pub struct Chord { pub r#for: Platform, } -impl Clone for Chord { +impl Clone for Chord { fn clone(&self) -> Self { Self { id: chord_id(), @@ -39,17 +39,21 @@ impl Clone for Chord { } } -impl PartialEq for Chord { +impl AsRef for Chord { + fn as_ref(&self) -> &Self { self } +} + +impl PartialEq for Chord { fn eq(&self, other: &Self) -> bool { self.on == other.on } } -impl Eq for Chord {} +impl Eq for Chord {} -impl Hash for Chord { +impl Hash for Chord { fn hash(&self, state: &mut H) { self.on.hash(state) } } -impl Chord { +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 Chord { 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 Mixable for Chord { +impl Mixable for Chord { fn filter(&self) -> bool { self.r#for.matches() && !self.noop() } } diff --git a/yazi-config/src/keymap/chord_arc.rs b/yazi-config/src/keymap/chord_arc.rs index 2dd3f7e4d..8b81a8ebc 100644 --- a/yazi-config/src/keymap/chord_arc.rs +++ b/yazi-config/src/keymap/chord_arc.rs @@ -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(Arc>); +#[derive(Clone, Debug, Default, Deserialize)] +pub struct ChordArc(Arc); -impl Deref for ChordArc { - type Target = Arc>; +impl Deref for ChordArc { + type Target = Arc; fn deref(&self) -> &Self::Target { &self.0 } } -impl DerefMut for ChordArc { +impl DerefMut for ChordArc { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } -impl From<&Self> for ChordArc { +impl AsRef for ChordArc { + fn as_ref(&self) -> &Chord { self } +} + +impl From<&Self> for ChordArc { fn from(value: &Self) -> Self { value.clone() } } -impl From> for ChordArc { - fn from(value: Chord) -> Self { ChordArc(Arc::new(value)).into_erased() } +impl From for ChordArc { + fn from(value: Chord) -> Self { Self(Arc::new(value)) } } -impl From> for Chord { - fn from(value: ChordArc) -> Self { +impl From 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 From> for Chord { } } -impl From<&ChordArc> for Chord { - fn from(value: &ChordArc) -> 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 { - 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 ChordArc { - pub fn as_erased(&self) -> &ChordArc { - unsafe { &*(self as *const Self as *const ChordArc) } - } - - pub fn into_erased(self) -> ChordArc { - ChordArc(unsafe { Arc::from_raw(Arc::into_raw(self.0) as *const Chord) }) - } - - pub fn to_seq(&self) -> Vec { - self.run.iter().rev().cloned().map(Into::into).collect() +impl ChordArc { + pub fn to_seq(&self, layer: Layer) -> Vec { + self + .run + .iter() + .rev() + .cloned() + .map(|mut a| { + a.layer = a.layer.or(layer); + a.into() + }) + .collect() } - pub fn into_seq(self) -> Vec { + pub fn into_seq(self, layer: Layer) -> Vec { 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 Mixable for ChordArc { +impl Mixable for ChordArc { fn filter(&self) -> bool { self.0.filter() } } +impl FromLua for ChordArc { + fn from_lua(value: Value, _: &Lua) -> mlua::Result { + Ok(match value { + Value::UserData(ud) => ud.take()?, + _ => Chord::deserialize(mlua::serde::Deserializer::new(value))?.into(), + }) + } +} + impl UserData for ChordArc { fn add_fields>(fields: &mut F) { fields.add_field_method_get("id", |_, me| Ok(me.id)); diff --git a/yazi-config/src/keymap/chords.rs b/yazi-config/src/keymap/chords.rs index 56bf096a4..1b1bdc40e 100644 --- a/yazi-config/src/keymap/chords.rs +++ b/yazi-config/src/keymap/chords.rs @@ -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 { - pub chords: ArcSwap>>, - #[serde(skip, default = "layer_default::")] - pub layer: Layer, -} +pub struct Chords(ArcSwap>); -impl Default for Chords { - fn default() -> Self { - Self { chords: ArcSwap::default(), layer: Layer::from_repr(L).unwrap_or_default() } - } -} +impl Deref for Chords { + type Target = ArcSwap>; -impl Deref for Chords { - type Target = ArcSwap>>; - - fn deref(&self) -> &Self::Target { &self.chords } + fn deref(&self) -> &Self::Target { &self.0 } } -impl From>> for Chords { - fn from(inner: Vec>) -> Self { - Self { chords: inner.into_pointee(), layer: Layer::from_repr(L).unwrap_or_default() } - } +impl From> for Chords { + fn from(inner: Vec) -> Self { Self(inner.into_pointee()) } } -impl Chords { - pub fn as_erased(&self) -> Arc>> { - let chords = self.chords.load_full(); - unsafe { Arc::from_raw(Arc::into_raw(chords) as *const Vec>) } - } - +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::>(), ) @@ -57,9 +40,9 @@ impl Chords { } 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 Chords { matcher: ChordMatcher, f: impl Fn(Chord) -> Result, ) -> 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 Chords { Ok(()) } - pub(crate) fn unwrap_unchecked(self) -> Vec> { - Arc::try_unwrap(self.chords.into_inner()).expect("unique chords arc") + pub(crate) fn unwrap_unchecked(self) -> Vec { + Arc::try_unwrap(self.0.into_inner()).expect("unique chords arc") } } @@ -91,21 +74,19 @@ impl UserData for &Chords { fn add_methods>(methods: &mut M) { methods.add_method("match", |_, &me, matcher: Option| { 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 = 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() -> Layer { Layer::from_repr(L).unwrap_or_default() } diff --git a/yazi-config/src/keymap/keymap.rs b/yazi-config/src/keymap/keymap.rs index 5fc87882a..c0fe0ff6d 100644 --- a/yazi-config/src/keymap/keymap.rs +++ b/yazi-config/src/keymap/keymap.rs @@ -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> { 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?, }) diff --git a/yazi-config/src/keymap/section.rs b/yazi-config/src/keymap/section.rs index 583cb5668..e24ae207f 100644 --- a/yazi-config/src/keymap/section.rs +++ b/yazi-config/src/keymap/section.rs @@ -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 { - keymap: Chords, +pub struct KeymapSection { + keymap: Chords, #[serde(default)] - prepend_keymap: Vec>, + prepend_keymap: Vec, #[serde(default)] - append_keymap: Vec>, - #[serde(default = "layer_default::")] - layer: Layer, + append_keymap: Vec, } -impl Deref for KeymapSection { - type Target = Chords; +impl Deref for KeymapSection { + type Target = Chords; fn deref(&self) -> &Self::Target { &self.keymap } } -impl KeymapSection { - pub fn as_erased(&self) -> &KeymapSection { - unsafe { &*(self as *const Self as *const KeymapSection) } - } -} - -impl DeserializeOverHook for KeymapSection { +impl DeserializeOverHook for KeymapSection { fn deserialize_over_hook(self) -> Result { #[inline] - fn on(Chord { on, .. }: &Chord) -> [Key; 2] { - [on.first().copied().unwrap_or_default(), on.get(1).copied().unwrap_or_default()] + fn on>(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> = mix( + let keymap: Vec = 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() }) } } diff --git a/yazi-config/src/popup/help.rs b/yazi-config/src/popup/help.rs new file mode 100644 index 000000000..4cfdc2f31 --- /dev/null +++ b/yazi-config/src/popup/help.rs @@ -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 }) + } +} diff --git a/yazi-config/src/popup/mod.rs b/yazi-config/src/popup/mod.rs index a723fb300..961d4a3c9 100644 --- a/yazi-config/src/popup/mod.rs +++ b/yazi-config/src/popup/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(confirm input options pick); +yazi_macro::mod_flat!(confirm help input options pick); diff --git a/yazi-config/src/theme/theme.rs b/yazi-config/src/theme/theme.rs index 5f507347b..4998eae16 100644 --- a/yazi-config/src/theme/theme.rs +++ b/yazi-config/src/theme/theme.rs @@ -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, - pub run: SyncCell, - pub desc: SyncCell, - + pub border: SyncCell, + pub chord: SyncCell, + pub action: SyncCell, pub hovered: SyncCell, - pub footer: SyncCell, } fn deserialize_syntect_theme<'de, D>(deserializer: D) -> Result, D::Error> diff --git a/yazi-core/src/core.rs b/yazi-core/src/core.rs index ad6ca18cb..112bc2319 100644 --- a/yazi-core/src/core.rs +++ b/yazi-core/src/core.rs @@ -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 } diff --git a/yazi-core/src/help/help.rs b/yazi-core/src/help/help.rs index bbde8a98c..fd5472fb4 100644 --- a/yazi-core/src/help/help.rs +++ b/yazi-core/src/help/help.rs @@ -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, + pub visible: bool, + pub layer: Layer, + pub position: Position, + pub bindings: Vec, // Filter - pub keyword: String, - pub in_filter: Option, + 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 { - 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 { - 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 { self.visible.then_some(self.input.cursor()) } pub fn rel_cursor(&self) -> usize { self.cursor - self.offset } pub fn cursor_shape(&self) -> Option { - 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 } diff --git a/yazi-core/src/help/mod.rs b/yazi-core/src/help/mod.rs index 386cd4828..a0cc5fda4 100644 --- a/yazi-core/src/help/mod.rs +++ b/yazi-core/src/help/mod.rs @@ -1,3 +1 @@ yazi_macro::mod_flat!(help); - -const HELP_MARGIN: u16 = 1; diff --git a/yazi-core/src/which/option.rs b/yazi-core/src/which/option.rs index 1b763feb1..51cef8bca 100644 --- a/yazi-core/src/which/option.rs +++ b/yazi-core/src/which/option.rs @@ -7,9 +7,10 @@ use yazi_shared::{Layer, event::ActionCow}; #[derive(Clone, Debug)] pub struct WhichOpt { pub tx: Option>>, + pub layer: Layer, pub cands: Vec, - pub silent: bool, pub times: usize, + pub silent: bool, } impl_data_any!(WhichOpt); @@ -24,19 +25,21 @@ impl TryFrom 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::>("tx").ok().map(|t| t.0), + layer: t.raw_get("layer")?, cands: t.raw_get::("cands")?.sequence_values().collect::>>()?, 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)?), diff --git a/yazi-core/src/which/which.rs b/yazi-core/src/which/which.rs index a046cfcdf..d310e2e1f 100644 --- a/yazi-core/src/which/which.rs +++ b/yazi-core/src/which/which.rs @@ -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>>, - pub times: usize, + pub layer: Layer, pub cands: Vec, + pub times: usize, // Active state pub active: bool, @@ -32,8 +34,8 @@ impl Which { } pub fn dismiss(&mut self, chord: Option) { - 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))); } } } diff --git a/yazi-fm/src/executor.rs b/yazi-fm/src/executor.rs index 2ed1b4012..431b97b00 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -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!() + } } } diff --git a/yazi-fm/src/help/bindings.rs b/yazi-fm/src/help/bindings.rs index e121653f8..a234040f7 100644 --- a/yazi-fm/src/help/bindings.rs +++ b/yazi-fm/src/help/bindings.rs @@ -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); } } diff --git a/yazi-fm/src/help/help.rs b/yazi-fm/src/help/help.rs index 24185d430..221481a06 100644 --- a/yazi-fm/src/help/help.rs +++ b/yazi-fm/src/help/help.rs @@ -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); + + 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::new(self.core).render(chunks[0], buf); + // Bindings + Bindings::new(self.core) + .render(chunks[2].padding(Padding { top: 0, bottom: 0, ..padding }), buf); } } diff --git a/yazi-fm/src/router.rs b/yazi-fm/src/router.rs index 2cc863f79..d8fcf1a2b 100644 --- a/yazi-fm/src/router.rs +++ b/yazi-fm/src/router.rs @@ -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::Cmp, 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; } diff --git a/yazi-parser/src/help/close.rs b/yazi-parser/src/help/close.rs new file mode 100644 index 000000000..8f782f985 --- /dev/null +++ b/yazi-parser/src/help/close.rs @@ -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 for CloseForm { + fn from(a: ActionCow) -> Self { Self { submit: a.bool("submit") } } +} + +impl From for CloseForm { + fn from(submit: bool) -> Self { Self { submit } } +} + +impl FromLua for CloseForm { + fn from_lua(value: Value, lua: &Lua) -> mlua::Result { lua.from_value(value) } +} + +impl IntoLua for CloseForm { + fn into_lua(self, lua: &Lua) -> mlua::Result { lua.to_value_with(&self, SER_OPT) } +} diff --git a/yazi-parser/src/help/mod.rs b/yazi-parser/src/help/mod.rs index dbc5333d4..8aa19c1e2 100644 --- a/yazi-parser/src/help/mod.rs +++ b/yazi-parser/src/help/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(toggle); +yazi_macro::mod_flat!(close toggle); diff --git a/yazi-parser/src/spark/spark.rs b/yazi-parser/src/spark/spark.rs index 01c94ba14..054a883b8 100644 --- a/yazi-parser/src/spark/spark.rs +++ b/yazi-parser/src/spark/spark.rs @@ -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); diff --git a/yazi-parser/src/which/activate.rs b/yazi-parser/src/which/activate.rs index 3f2acc317..f9685f920 100644 --- a/yazi-parser/src/which/activate.rs +++ b/yazi-parser/src/which/activate.rs @@ -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 for ActivateForm { + fn from(opt: WhichOpt) -> Self { Self { opt } } } impl TryFrom for ActivateForm { diff --git a/yazi-plugin/src/theme/theme.rs b/yazi-plugin/src/theme/theme.rs index c13b47fcd..8460b169c 100644 --- a/yazi-plugin/src/theme/theme.rs +++ b/yazi-plugin/src/theme/theme.rs @@ -361,12 +361,10 @@ fn help() -> Composer { fn get(lua: &Lua, key: &[u8]) -> mlua::Result { 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), } diff --git a/yazi-plugin/src/utils/layer.rs b/yazi-plugin/src/utils/layer.rs index 16d6c7759..1057189b7 100644 --- a/yazi-plugin/src/utils/layer.rs +++ b/yazi-plugin/src/utils/layer.rs @@ -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(), diff --git a/yazi-proxy/src/which.rs b/yazi-proxy/src/which.rs index baf25ef03..5ef649aa2 100644 --- a/yazi-proxy/src/which.rs +++ b/yazi-proxy/src/which.rs @@ -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? } diff --git a/yazi-shared/src/event/actions.rs b/yazi-shared/src/event/actions.rs index 01b719163..702597528 100644 --- a/yazi-shared/src/event/actions.rs +++ b/yazi-shared/src/event/actions.rs @@ -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); @@ -28,12 +28,9 @@ impl From> 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 +pub fn deserialize_actions<'de, D>(deserializer: D) -> Result 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::::deserialize_as(deserializer)?); - actions.set(layer, Source::Key); + actions.set_source(Source::Key); Ok(actions) } diff --git a/yazi-shared/src/layer.rs b/yazi-shared/src/layer.rs index f3e103776..53ef3f912 100644 --- a/yazi-shared/src/layer.rs +++ b/yazi-shared/src/layer.rs @@ -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 { + Ok(match value { + Value::String(s) => s.to_str()?.parse().into_lua_err()?, + Value::UserData(ud) => *ud.borrow::()?, + _ => Err("expected a string or a Layer".into_lua_err())?, + }) + } +} + impl UserData for Layer { fn add_methods>(methods: &mut M) { methods.add_meta_method(MetaMethod::ToString, |_, me, ()| Ok(me.into_str())); diff --git a/yazi-widgets/src/input/event.rs b/yazi-widgets/src/input/event.rs index 52f77d0d8..7e632e7b0 100644 --- a/yazi-widgets/src/input/event.rs +++ b/yazi-widgets/src/input/event.rs @@ -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), diff --git a/yazi-widgets/src/input/input.rs b/yazi-widgets/src/input/input.rs index b4f9591ea..62d47ead3 100644 --- a/yazi-widgets/src/input/input.rs +++ b/yazi-widgets/src/input/input.rs @@ -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!(), } } diff --git a/yazi-widgets/src/input/input_arc.rs b/yazi-widgets/src/input/input_arc.rs index ae0b48ad2..71f132ef7 100644 --- a/yazi-widgets/src/input/input_arc.rs +++ b/yazi-widgets/src/input/input_arc.rs @@ -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)?)) })?; diff --git a/yazi-widgets/src/input/styles.rs b/yazi-widgets/src/input/styles.rs index c402bdcd9..bc32688ee 100644 --- a/yazi-widgets/src/input/styles.rs +++ b/yazi-widgets/src/input/styles.rs @@ -5,6 +5,7 @@ use ratatui_core::style::Style; pub struct InputStyles { pub normal: Option