From c92c4aba884c16d318a92f6bc0517ee0d2bcc414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Sun, 5 Jul 2026 18:06:25 +0800 Subject: [PATCH] feat: make visual mode support wraparound scrolling (#4101) --- CHANGELOG.md | 3 ++ Cargo.lock | 12 +++--- yazi-actor/src/input/show.rs | 2 +- yazi-actor/src/lives/file.rs | 24 ++++++----- yazi-actor/src/lives/mode.rs | 7 +++- yazi-actor/src/mgr/arrow.rs | 6 +-- yazi-actor/src/mgr/escape.rs | 4 +- yazi-actor/src/mgr/visual_mode.rs | 8 ++-- yazi-config/preset/keymap-default.toml | 15 +++---- yazi-config/src/inject.rs | 8 +++- yazi-config/src/open/open_rule_matcher.rs | 2 +- yazi-config/src/plugin/fetcher.rs | 2 +- yazi-config/src/plugin/preloader.rs | 2 +- yazi-config/src/plugin/previewer.rs | 2 +- yazi-config/src/plugin/spotter.rs | 2 +- yazi-config/src/popup/input.rs | 16 ++++---- yazi-core/src/input/input.rs | 2 +- yazi-core/src/spot/lock.rs | 16 ++++---- yazi-core/src/tab/mod.rs | 2 +- yazi-core/src/tab/mode.rs | 40 +++++++++--------- yazi-core/src/tab/preview_lock.rs | 16 ++++---- yazi-core/src/tab/visual.rs | 49 +++++++++++++++++++++++ yazi-fm/src/input/input.rs | 7 ++-- yazi-fs/src/file/file_ref.rs | 49 +++++++++++++++++++++++ yazi-fs/src/file/inventory.rs | 2 +- yazi-fs/src/file/lua.rs | 22 +++------- yazi-fs/src/file/mod.rs | 2 +- yazi-plugin/preset/components/entity.lua | 2 +- yazi-plugin/preset/plugins/archive.lua | 4 +- yazi-plugin/src/theme/icon.rs | 23 +++++++++++ yazi-plugin/src/theme/mod.rs | 2 +- yazi-plugin/src/theme/theme.rs | 3 +- yazi-plugin/src/utils/cache.rs | 22 +++++----- yazi-term/src/restorer/windows.rs | 3 +- yazi-widgets/src/input/option.rs | 4 +- 35 files changed, 262 insertions(+), 123 deletions(-) create mode 100644 yazi-core/src/tab/visual.rs create mode 100644 yazi-fs/src/file/file_ref.rs create mode 100644 yazi-plugin/src/theme/icon.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e73c2a7..eec968e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,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]) +- Make visual mode support wraparound scrolling ([#4101]) - H/M/L Vim-like motion for moving cursor relative to viewport ([#3970]) - Context-aware icons for inputs ([#4080]) - Show file icons in trash/delete/overwrite confirmations ([#4096]) @@ -36,6 +37,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): ### Deprecated - Deprecate `backward --far` and `forward --far` in favor of `backward wide` and `forward wide`, respectively ([#4012]) +- Deprecate `tab::Mode.is_visual` in favor of the new `tab::Mode.is_normal` ([#4101]) ### Fixed @@ -1768,3 +1770,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): [#4074]: https://github.com/sxyazi/yazi/pull/4074 [#4080]: https://github.com/sxyazi/yazi/pull/4080 [#4096]: https://github.com/sxyazi/yazi/pull/4096 +[#4101]: https://github.com/sxyazi/yazi/pull/4101 diff --git a/Cargo.lock b/Cargo.lock index 388e27a5..cec37e11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2198,9 +2198,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c863e9ab5e7bf9c99ba75e1050f1e4d624ae87ed3532d6238ffbdc7b585dbbe6" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ "num-integer", "num-traits", @@ -2590,9 +2590,9 @@ checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plist" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" dependencies = [ "base64", "indexmap 2.14.0", @@ -2783,9 +2783,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.39.4" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" dependencies = [ "memchr", ] diff --git a/yazi-actor/src/input/show.rs b/yazi-actor/src/input/show.rs index d0babeef..cdd9d397 100644 --- a/yazi-actor/src/input/show.rs +++ b/yazi-actor/src/input/show.rs @@ -21,7 +21,7 @@ impl Actor for Show { let area = cx.mgr.area(opt.position).padding(cx.input.padding()); let input = &mut cx.input; - input.main.id = opt.id.clone(); + input.main.name = opt.name.clone(); input.main.title = opt.title.clone(); input.main.position = opt.position; input.main.visible = true; diff --git a/yazi-actor/src/lives/file.rs b/yazi-actor/src/lives/file.rs index 5a32c2cb..8a283bed 100644 --- a/yazi-actor/src/lives/file.rs +++ b/yazi-actor/src/lives/file.rs @@ -64,7 +64,11 @@ impl UserData for File { fn add_methods>(methods: &mut M) { yazi_binding::impl_file_methods!(methods); - methods.add_method("icon", |_, me, ()| { + methods.add_method("icon", |lua, me, ()| { + yazi_binding::deprecate!( + lua, + "{}: `File:icon()` is deprecated, use `th.icon:match(file)` instead" + ); // TODO: use a cache Ok(yazi_config::THEME.icon.matches(me, me.is_hovered())) }); @@ -100,16 +104,16 @@ impl UserData for File { }) }); methods.add_method("is_marked", |_, me, ()| { - use yazi_core::tab::Mode::*; - if !me.tab.mode.is_visual() || me.folder.url != me.tab.current.url { + let Some(visual) = me.tab.mode.visual() else { + return Ok(0u8); + }; + if !visual.contains(me.idx, me.tab.current.cursor, me.folder.entries.len()) { return Ok(0u8); } - - Ok(match &me.tab.mode { - Select(_, indices) if indices.contains(&me.idx) => 1u8, - Unset(_, indices) if indices.contains(&me.idx) => 2u8, - _ => 0u8, - }) + if me.folder.url != me.tab.current.url { + return Ok(0u8); + } + Ok(if me.tab.mode.is_select() { 1u8 } else { 2u8 }) }); methods.add_method("is_selected", |_, me, ()| Ok(me.tab.selected.contains(&me.url))); methods.add_method("found", |lua, me, ()| { @@ -144,6 +148,6 @@ impl UserData for File { inventory::submit! { FileInventory { register: |_| {}, - from_lua: |ud| Ok(ud.borrow::()?.clone()), + borrow: |ud, f| f(&*ud.borrow::()?), } } diff --git a/yazi-actor/src/lives/mode.rs b/yazi-actor/src/lives/mode.rs index 71515ca2..6c37a60b 100644 --- a/yazi-actor/src/lives/mode.rs +++ b/yazi-actor/src/lives/mode.rs @@ -1,6 +1,7 @@ use std::ops::Deref; use mlua::{AnyUserData, MetaMethod, UserData, UserDataFields, UserDataMethods}; +use yazi_binding::deprecate; use super::{Lives, PtrCell}; @@ -22,9 +23,13 @@ impl Mode { impl UserData for Mode { fn add_fields>(fields: &mut F) { + fields.add_field_method_get("is_normal", |_, me| Ok(me.is_normal())); fields.add_field_method_get("is_select", |_, me| Ok(me.is_select())); fields.add_field_method_get("is_unset", |_, me| Ok(me.is_unset())); - fields.add_field_method_get("is_visual", |_, me| Ok(me.is_visual())); + fields.add_field_method_get("is_visual", |lua, me| { + deprecate!(lua, "{}: `mode.is_visual` is deprecated, use `not mode.is_normal` instead. \nSee #4101 for more details: https://github.com/sxyazi/yazi/pull/4101."); + Ok(!me.is_normal()) + }); } fn add_methods>(methods: &mut M) { diff --git a/yazi-actor/src/mgr/arrow.rs b/yazi-actor/src/mgr/arrow.rs index cfcdd272..6734d316 100644 --- a/yazi-actor/src/mgr/arrow.rs +++ b/yazi-actor/src/mgr/arrow.rs @@ -14,6 +14,7 @@ impl Actor for Arrow { fn act(cx: &mut Ctx, form: Self::Form) -> Result { let tab = cx.tab_mut(); + let old = tab.current.cursor; if !tab.current.arrow(form.step) { succ!(); } @@ -22,9 +23,8 @@ impl Actor for Arrow { tab.current.retrace(); // Visual selection - if let Some((start, items)) = tab.mode.visual_mut() { - let end = tab.current.cursor; - *items = (start.min(end)..=end.max(start)).collect(); + if let Some(visual) = tab.mode.visual_mut() { + visual.arrow(form.step, old, tab.current.cursor); } act!(mgr:hover, cx)?; diff --git a/yazi-actor/src/mgr/escape.rs b/yazi-actor/src/mgr/escape.rs index 132bda3b..afb472b8 100644 --- a/yazi-actor/src/mgr/escape.rs +++ b/yazi-actor/src/mgr/escape.rs @@ -67,7 +67,9 @@ impl Actor for EscapeVisual { let tab = cx.tab_mut(); let select = tab.mode.is_select(); - let Some((_, indices)) = tab.mode.take_visual() else { succ!(false) }; + let Some(indices) = tab.mode.take_visual(tab.current.cursor, tab.current.entries.len()) else { + succ!(false) + }; render!(); let files: Vec<_> = indices.into_iter().filter_map(|i| tab.current.entries.get(i)).collect(); diff --git a/yazi-actor/src/mgr/visual_mode.rs b/yazi-actor/src/mgr/visual_mode.rs index 0fe8a524..13edde08 100644 --- a/yazi-actor/src/mgr/visual_mode.rs +++ b/yazi-actor/src/mgr/visual_mode.rs @@ -1,7 +1,5 @@ -use std::collections::BTreeSet; - use anyhow::Result; -use yazi_core::tab::Mode; +use yazi_core::tab::{Mode, Visual}; use yazi_macro::{render, succ}; use yazi_parser::mgr::VisualModeForm; use yazi_shared::data::Data; @@ -20,9 +18,9 @@ impl Actor for VisualMode { let idx = tab.current.cursor; if form.unset { - tab.mode = Mode::Unset(idx, BTreeSet::from([idx])); + tab.mode = Mode::Unset(Visual::new(idx)); } else { - tab.mode = Mode::Select(idx, BTreeSet::from([idx])); + tab.mode = Mode::Select(Visual::new(idx)); }; succ!(render!()); diff --git a/yazi-config/preset/keymap-default.toml b/yazi-config/preset/keymap-default.toml index bb115ac6..d39c00e8 100644 --- a/yazi-config/preset/keymap-default.toml +++ b/yazi-config/preset/keymap-default.toml @@ -13,13 +13,17 @@ keymap = [ { on = "", run = "close", desc = "Close the current tab, or quit if it's last" }, { on = "", run = "suspend", desc = "Suspend the process" }, - # Hopping - { on = "k", run = "arrow prev", desc = "Previous file" }, - { on = "j", run = "arrow next", desc = "Next file" }, - + # Hop around + { on = "k", run = "arrow prev", desc = "Previous file" }, + { on = "j", run = "arrow next", desc = "Next file" }, { on = "", run = "arrow prev", desc = "Previous file" }, { on = "", run = "arrow next", desc = "Next file" }, + { on = [ "g", "g" ], run = "arrow top", desc = "Go to top" }, + { on = "G", run = "arrow bot", desc = "Go to bottom" }, + { on = "", run = "arrow top", desc = "Go to top" }, + { on = "", run = "arrow bot", desc = "Go to bottom" }, + { on = "", run = "arrow -50%", desc = "Move cursor up half page" }, { on = "", run = "arrow 50%", desc = "Move cursor down half page" }, { on = "", run = "arrow -100%", desc = "Move cursor up one page" }, @@ -30,9 +34,6 @@ keymap = [ { on = "", run = "arrow -100%", desc = "Move cursor up one page" }, { on = "", run = "arrow 100%", desc = "Move cursor down one page" }, - { on = [ "g", "g" ], run = "arrow top", desc = "Go to top" }, - { on = "G", run = "arrow bot", desc = "Go to bottom" }, - # Navigation { on = "h", run = "leave", desc = "Back to the parent directory" }, { on = "l", run = "enter", desc = "Enter the child directory" }, diff --git a/yazi-config/src/inject.rs b/yazi-config/src/inject.rs index 71bdf3f9..ba4858c2 100644 --- a/yazi-config/src/inject.rs +++ b/yazi-config/src/inject.rs @@ -6,11 +6,15 @@ use crate::THEME; inventory::submit! { FileInventory { register: |registry| { - registry.add_method("icon", |_, me, ()| { + registry.add_method("icon", |lua, me, ()| { + yazi_binding::deprecate!( + lua, + "{}: `File:icon()` is deprecated, use `th.icon:match(file)` instead" + ); // TODO: use a cache Ok(THEME.icon.matches(me, false)) }); }, - from_lua: |_| Err(mlua::Error::UserDataTypeMismatch), + borrow: |_, _| Err(mlua::Error::UserDataTypeMismatch), } } diff --git a/yazi-config/src/open/open_rule_matcher.rs b/yazi-config/src/open/open_rule_matcher.rs index 78882c0a..1a3fdc48 100644 --- a/yazi-config/src/open/open_rule_matcher.rs +++ b/yazi-config/src/open/open_rule_matcher.rs @@ -60,7 +60,7 @@ impl TryFrom for OpenRuleMatcher<'static> { Ok(Self { rules: YAZI.open.load_full(), id, - file: file.map(|f| f.clone().into()), + file: file.map(TryInto::try_into).transpose()?, mime: mime.map(Into::into), ..Default::default() }) diff --git a/yazi-config/src/plugin/fetcher.rs b/yazi-config/src/plugin/fetcher.rs index 8b5085bc..f2d658bf 100644 --- a/yazi-config/src/plugin/fetcher.rs +++ b/yazi-config/src/plugin/fetcher.rs @@ -95,7 +95,7 @@ impl TryFrom
for FetcherMatcher<'static> { Ok(Self { fetchers: YAZI.plugin.fetchers.load_full(), id, - file: file.map(|f| f.clone().into()), + file: file.map(TryInto::try_into).transpose()?, mime: mime.map(Into::into), ..Default::default() }) diff --git a/yazi-config/src/plugin/preloader.rs b/yazi-config/src/plugin/preloader.rs index e546af01..a4d216d7 100644 --- a/yazi-config/src/plugin/preloader.rs +++ b/yazi-config/src/plugin/preloader.rs @@ -97,7 +97,7 @@ impl TryFrom
for PreloaderMatcher<'static> { Ok(Self { preloaders: YAZI.plugin.preloaders.load_full(), id, - file: file.map(|f| f.clone().into()), + file: file.map(TryInto::try_into).transpose()?, mime: mime.map(Into::into), ..Default::default() }) diff --git a/yazi-config/src/plugin/previewer.rs b/yazi-config/src/plugin/previewer.rs index 6a0a2ebb..7de385a6 100644 --- a/yazi-config/src/plugin/previewer.rs +++ b/yazi-config/src/plugin/previewer.rs @@ -90,7 +90,7 @@ impl TryFrom
for PreviewerMatcher<'static> { Ok(Self { previewers: YAZI.plugin.previewers.load_full(), id, - file: file.map(|f| f.clone().into()), + file: file.map(TryInto::try_into).transpose()?, mime: mime.map(Into::into), ..Default::default() }) diff --git a/yazi-config/src/plugin/spotter.rs b/yazi-config/src/plugin/spotter.rs index f52a64a7..d39e3ac6 100644 --- a/yazi-config/src/plugin/spotter.rs +++ b/yazi-config/src/plugin/spotter.rs @@ -90,7 +90,7 @@ impl TryFrom
for SpotterMatcher<'static> { Ok(Self { spotters: YAZI.plugin.spotters.load_full(), id, - file: file.map(|f| f.clone().into()), + file: file.map(TryInto::try_into).transpose()?, mime: mime.map(Into::into), ..Default::default() }) diff --git a/yazi-config/src/popup/input.rs b/yazi-config/src/popup/input.rs index 348e35e5..8d04c6f8 100644 --- a/yazi-config/src/popup/input.rs +++ b/yazi-config/src/popup/input.rs @@ -47,7 +47,7 @@ pub struct Input { impl Input { pub fn cd(&self, cwd: Url) -> InputOpt { InputOpt { - id: "cd".to_owned(), + name: "cd".to_owned(), title: self.cd_title.clone(), value: if cwd.kind().is_local() { String::new() } else { EncodeScheme(cwd).to_string() }, position: Position::new(self.cd_origin, self.cd_offset), @@ -58,7 +58,7 @@ impl Input { pub fn create(&self, dir: bool) -> InputOpt { InputOpt { - id: "create".to_owned(), + name: format!("create-{}", if dir { "dir" } else { "file" }), title: self.create_title[dir as usize].clone(), position: Position::new(self.create_origin, self.create_offset), ..Default::default() @@ -67,7 +67,7 @@ impl Input { pub fn rename(&self, is_dir: bool) -> InputOpt { InputOpt { - id: format!("rename-{}", if is_dir { "dir" } else { "file" }), + name: format!("rename-{}", if is_dir { "dir" } else { "file" }), title: self.rename_title.clone(), position: Position::new(self.rename_origin, self.rename_offset), ..Default::default() @@ -76,7 +76,7 @@ impl Input { pub fn filter(&self) -> InputOpt { InputOpt { - id: "filter".to_owned(), + name: "filter".to_owned(), title: self.filter_title.clone(), position: Position::new(self.filter_origin, self.filter_offset), realtime: true, @@ -86,7 +86,7 @@ impl Input { pub fn find(&self, prev: bool) -> InputOpt { InputOpt { - id: "find".to_owned(), + name: "find".to_owned(), title: self.find_title[prev as usize].clone(), position: Position::new(self.find_origin, self.find_offset), realtime: true, @@ -96,7 +96,7 @@ impl Input { pub fn search(&self, name: &str) -> InputOpt { InputOpt { - id: "search".to_owned(), + name: "search".to_owned(), title: self.search_title.replace("{n}", name), position: Position::new(self.search_origin, self.search_offset), ..Default::default() @@ -105,7 +105,7 @@ impl Input { pub fn shell(&self, block: bool) -> InputOpt { InputOpt { - id: "shell".to_owned(), + name: "shell".to_owned(), title: self.shell_title[block as usize].clone(), position: Position::new(self.shell_origin, self.shell_offset), ..Default::default() @@ -114,7 +114,7 @@ impl Input { pub fn tab_rename(&self) -> InputOpt { InputOpt { - id: "tab-rename".to_owned(), + name: "tab-rename".to_owned(), title: "Rename tab:".to_owned(), position: Position::new(Origin::TopCenter, Offset { x: 0, diff --git a/yazi-core/src/input/input.rs b/yazi-core/src/input/input.rs index 127556ce..425cc277 100644 --- a/yazi-core/src/input/input.rs +++ b/yazi-core/src/input/input.rs @@ -52,7 +52,7 @@ impl Input { #[derive(Default)] pub struct InputMain { inner: yazi_widgets::input::Input, - pub id: String, + pub name: String, pub title: String, pub position: Position, pub visible: bool, diff --git a/yazi-core/src/spot/lock.rs b/yazi-core/src/spot/lock.rs index f2dda3df..30f11ae5 100644 --- a/yazi-core/src/spot/lock.rs +++ b/yazi-core/src/spot/lock.rs @@ -22,14 +22,16 @@ impl TryFrom
for SpotLock { fn try_from(t: Table) -> Result { let file: FileRef = t.raw_get("file")?; - Ok(Self { - url: file.url_owned(), - cha: file.cha, - mime: t.raw_get("mime")?, + file.borrow(|f| { + Ok(Self { + url: f.url_owned(), + cha: f.cha, + mime: t.raw_get("mime")?, - id: t.raw_get("id")?, - skip: t.raw_get("skip")?, - data: Default::default(), + id: t.raw_get("id")?, + skip: t.raw_get("skip")?, + data: Default::default(), + }) }) } } diff --git a/yazi-core/src/tab/mod.rs b/yazi-core/src/tab/mod.rs index 4678c783..8f7666e1 100644 --- a/yazi-core/src/tab/mod.rs +++ b/yazi-core/src/tab/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(backstack finder folder history mode preference preview preview_lock selected tab); +yazi_macro::mod_flat!(backstack finder folder history visual mode preference preview preview_lock selected tab); diff --git a/yazi-core/src/tab/mode.rs b/yazi-core/src/tab/mode.rs index fa5e4a46..8d261365 100644 --- a/yazi-core/src/tab/mode.rs +++ b/yazi-core/src/tab/mode.rs @@ -1,41 +1,43 @@ -use std::{collections::BTreeSet, fmt::Display, mem}; +use std::{fmt::{self, Display}, mem}; -#[derive(Clone, Debug, Default, Eq, PartialEq)] +use strum::EnumIs; + +use super::Visual; +use crate::tab::VisualIndices; + +#[derive(Clone, Debug, Default, EnumIs, Eq, PartialEq)] pub enum Mode { #[default] Normal, - Select(usize, BTreeSet), - Unset(usize, BTreeSet), + Select(Visual), + Unset(Visual), } impl Mode { - pub fn visual_mut(&mut self) -> Option<(usize, &mut BTreeSet)> { + pub fn visual(&self) -> Option { match self { Self::Normal => None, - Self::Select(start, indices) => Some((*start, indices)), - Self::Unset(start, indices) => Some((*start, indices)), + Self::Select(visual) | Self::Unset(visual) => Some(*visual), } } - pub fn take_visual(&mut self) -> Option<(usize, BTreeSet)> { + pub fn visual_mut(&mut self) -> Option<&mut Visual> { + match self { + Self::Normal => None, + Self::Select(visual) | Self::Unset(visual) => Some(visual), + } + } + + pub fn take_visual(&mut self, end: usize, len: usize) -> Option { match mem::take(self) { Self::Normal => None, - Self::Select(start, indices) => Some((start, indices)), - Self::Unset(start, indices) => Some((start, indices)), + Self::Select(visual) | Self::Unset(visual) => Some(visual.indices(end, len)), } } } -impl Mode { - pub fn is_select(&self) -> bool { matches!(self, Self::Select(..)) } - - pub fn is_unset(&self) -> bool { matches!(self, Self::Unset(..)) } - - pub fn is_visual(&self) -> bool { matches!(self, Self::Select(..) | Self::Unset(..)) } -} - impl Display for Mode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { Self::Normal => "normal", Self::Select(..) => "select", diff --git a/yazi-core/src/tab/preview_lock.rs b/yazi-core/src/tab/preview_lock.rs index 9df639b6..243bec31 100644 --- a/yazi-core/src/tab/preview_lock.rs +++ b/yazi-core/src/tab/preview_lock.rs @@ -23,14 +23,16 @@ impl TryFrom
for PreviewLock { fn try_from(t: Table) -> Result { let file: FileRef = t.raw_get("file")?; - Ok(Self { - url: file.url_owned(), - cha: file.cha, - mime: t.raw_get::("mime")?.to_str()?.intern(), + file.borrow(|f| { + Ok(Self { + url: f.url_owned(), + cha: f.cha, + mime: t.raw_get::("mime")?.to_str()?.intern(), - skip: t.raw_get("skip")?, - area: t.raw_get("area")?, - data: Default::default(), + skip: t.raw_get("skip")?, + area: t.raw_get("area")?, + data: Default::default(), + }) }) } } diff --git a/yazi-core/src/tab/visual.rs b/yazi-core/src/tab/visual.rs new file mode 100644 index 00000000..2be30241 --- /dev/null +++ b/yazi-core/src/tab/visual.rs @@ -0,0 +1,49 @@ +use std::{iter::Chain, ops::Range}; + +use yazi_widgets::Step; + +pub type VisualIndices = Chain, Range>; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Visual { + start: usize, + wraps: isize, +} + +impl Visual { + pub fn new(start: usize) -> Self { Self { start, wraps: 0 } } + + pub fn arrow(&mut self, step: Step, old: usize, new: usize) { + self.wraps += match step { + Step::Prev if new > old => -1, + Step::Next if new < old => 1, + _ => 0, + } + } + + pub fn contains(&self, index: usize, end: usize, len: usize) -> bool { + let (first, second) = self.ranges(end, len); + first.contains(&index) || second.contains(&index) + } + + pub fn indices(&self, end: usize, len: usize) -> VisualIndices { + let (first, second) = self.ranges(end, len); + first.chain(second) + } + + fn ranges(&self, end: usize, len: usize) -> (Range, Range) { + if len == 0 { + return (0..0, 0..0); + } + + let start = self.start.min(len - 1); + let end = end.min(len - 1); + + match self.wraps { + 0 => (start.min(end)..start.max(end) + 1, 0..0), + 1 if start > end + 1 => (0..end + 1, start..len), + -1 if end > start + 1 => (0..start + 1, end..len), + _ => (0..len, 0..0), + } + } +} diff --git a/yazi-fm/src/input/input.rs b/yazi-fm/src/input/input.rs index 84962b41..973ec3ea 100644 --- a/yazi-fm/src/input/input.rs +++ b/yazi-fm/src/input/input.rs @@ -18,11 +18,12 @@ impl<'a> Input<'a> { let input = &self.core.input.main; let is_dir = input.value().ends_with(CROSS_SEPARATOR); - let is_hovered = input.id.starts_with("rename-"); + let is_hovered = input.name.starts_with("rename-"); - let (path, mode): (&str, u16) = match input.id.as_str() { + let (path, mode): (&str, u16) = match input.name.as_str() { "cd" => (input.value(), if is_dir { 0o40755 } else { 0o100644 }), - "create" => (input.value(), if is_dir { 0o40755 } else { 0o100644 }), + "create-file" => (input.value(), if is_dir { 0o40755 } else { 0o100644 }), + "create-dir" => (input.value(), 0o40755), "rename-file" => (input.value(), 0o100644), "rename-dir" => (input.value(), 0o40755), "shell" => ("icon.sh", 0o100644), diff --git a/yazi-fs/src/file/file_ref.rs b/yazi-fs/src/file/file_ref.rs new file mode 100644 index 00000000..86e581e0 --- /dev/null +++ b/yazi-fs/src/file/file_ref.rs @@ -0,0 +1,49 @@ +use std::borrow::Cow; + +use mlua::{AnyUserData, ExternalError, FromLua, Lua, Value}; + +use crate::file::{File, FileInventory}; + +pub struct FileRef(pub(super) AnyUserData); + +const EXPECTED: &str = "expected a table or File"; + +impl TryFrom for File { + type Error = mlua::Error; + + fn try_from(value: FileRef) -> Result { value.borrow(|f| Ok(f.clone())) } +} + +impl<'a> TryFrom for Cow<'a, File> { + type Error = mlua::Error; + + fn try_from(value: FileRef) -> Result { File::try_from(value).map(Cow::Owned) } +} + +impl FileRef { + pub fn borrow(&self, mut f: impl FnMut(&File) -> mlua::Result) -> mlua::Result { + let mut result = None; + for inv in inventory::iter::() { + match (inv.borrow)(&self.0, &mut |file| Ok(result = Some(f(file)?))) { + Ok(()) => return Ok(result.unwrap()), + Err(mlua::Error::UserDataTypeMismatch) => continue, + Err(e) => return Err(e), + } + } + + match self.0.borrow::() { + Ok(file) => f(&file), + Err(mlua::Error::UserDataTypeMismatch) => Err(mlua::Error::UserDataTypeMismatch), + Err(e) => Err(e), + } + } +} + +impl FromLua for FileRef { + fn from_lua(value: Value, _: &Lua) -> mlua::Result { + match value { + Value::UserData(ud) => Ok(Self(ud)), + _ => Err(EXPECTED.into_lua_err())?, + } + } +} diff --git a/yazi-fs/src/file/inventory.rs b/yazi-fs/src/file/inventory.rs index f729fe04..b91595e3 100644 --- a/yazi-fs/src/file/inventory.rs +++ b/yazi-fs/src/file/inventory.rs @@ -4,7 +4,7 @@ use crate::file::File; pub struct FileInventory { pub register: fn(&mut UserDataRegistry), - pub from_lua: fn(&AnyUserData) -> mlua::Result, + pub borrow: fn(&AnyUserData, &mut dyn FnMut(&File) -> mlua::Result<()>) -> mlua::Result<()>, } inventory::collect!(FileInventory); diff --git a/yazi-fs/src/file/lua.rs b/yazi-fs/src/file/lua.rs index 0f809112..32d19bf4 100644 --- a/yazi-fs/src/file/lua.rs +++ b/yazi-fs/src/file/lua.rs @@ -1,9 +1,7 @@ -use mlua::{AnyUserData, ExternalError, FromLua, Lua, Table, UserData, UserDataMethods, UserDataRef, UserDataRegistry, Value}; +use mlua::{AnyUserData, ExternalError, FromLua, Lua, Table, UserData, UserDataMethods, UserDataRegistry, Value}; use yazi_binding::{impl_file_fields, impl_file_methods}; -use crate::file::{File, FileInventory}; - -pub type FileRef = UserDataRef; +use crate::file::{File, FileInventory, FileRef}; const EXPECTED: &str = "expected a table, File, or fs::File"; @@ -25,19 +23,11 @@ impl TryFrom for File { type Error = mlua::Error; fn try_from(value: AnyUserData) -> Result { - if let Ok(me) = value.take::() { - return Ok(me); + match value.take::() { + Ok(me) => Ok(me), + Err(mlua::Error::UserDataTypeMismatch) => FileRef(value).try_into(), + Err(e) => Err(e), } - - for inv in inventory::iter::() { - match (inv.from_lua)(&value) { - Ok(file) => return Ok(file), - Err(mlua::Error::UserDataTypeMismatch) => continue, - Err(e) => return Err(e), - } - } - - Err(mlua::Error::UserDataTypeMismatch) } } diff --git a/yazi-fs/src/file/mod.rs b/yazi-fs/src/file/mod.rs index 157a9f22..7e2916c3 100644 --- a/yazi-fs/src/file/mod.rs +++ b/yazi-fs/src/file/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(data file_cov file files inventory lua); +yazi_macro::mod_flat!(data file_cov file file_ref files inventory lua); diff --git a/yazi-plugin/preset/components/entity.lua b/yazi-plugin/preset/components/entity.lua index 99c61581..7baaa249 100644 --- a/yazi-plugin/preset/components/entity.lua +++ b/yazi-plugin/preset/components/entity.lua @@ -26,7 +26,7 @@ function Entity:padding() end function Entity:icon() - local icon = self._file:icon() + local icon = th.icon:match(self._file, { hovered = self._file.is_hovered }) if not icon then return "" elseif self._file.is_hovered then diff --git a/yazi-plugin/preset/plugins/archive.lua b/yazi-plugin/preset/plugins/archive.lua index 00572ccf..8788a887 100644 --- a/yazi-plugin/preset/plugins/archive.lua +++ b/yazi-plugin/preset/plugins/archive.lua @@ -20,10 +20,10 @@ function M:peek(job) local left, right = {}, {} for i = job.skip + 1, #items do local f = items[i] - local icon = File({ + local icon = th.icon:match(File { url = Url(f.path), cha = Cha { mode = tonumber(f.is_dir and "40700" or "100644", 8) }, - }):icon() + }) if f.size > 0 then right[#right + 1] = " " .. ya.readable_size(f.size) .. " " diff --git a/yazi-plugin/src/theme/icon.rs b/yazi-plugin/src/theme/icon.rs new file mode 100644 index 00000000..716a5298 --- /dev/null +++ b/yazi-plugin/src/theme/icon.rs @@ -0,0 +1,23 @@ +use mlua::{IntoLua, Lua, Table, Value}; +use yazi_binding::{Composer, ComposerGet, ComposerSet}; +use yazi_config::THEME; +use yazi_fs::file::FileRef; + +pub(super) fn icon() -> Composer { + fn get(lua: &Lua, key: &[u8]) -> mlua::Result { + match key { + b"match" => lua + .create_function(|_, (_, file, opts): (Value, FileRef, Option
)| { + let hovered = opts.map(|t| t.raw_get("hovered")).transpose()?.unwrap_or(false); + file.borrow(|f| Ok(THEME.icon.matches(f, hovered))) + })? + .into_lua(lua), + + _ => Ok(Value::Nil), + } + } + + fn set(_: &Lua, _: &[u8], value: Value) -> mlua::Result { Ok(value) } + + Composer::new(get, set) +} diff --git a/yazi-plugin/src/theme/mod.rs b/yazi-plugin/src/theme/mod.rs index c0a6b7f5..daa06c77 100644 --- a/yazi-plugin/src/theme/mod.rs +++ b/yazi-plugin/src/theme/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(theme); +yazi_macro::mod_flat!(icon theme); diff --git a/yazi-plugin/src/theme/theme.rs b/yazi-plugin/src/theme/theme.rs index 8460b169..64df33b2 100644 --- a/yazi-plugin/src/theme/theme.rs +++ b/yazi-plugin/src/theme/theme.rs @@ -3,7 +3,7 @@ use yazi_binding::{Composer, ComposerGet, ComposerSet, style::Style}; use yazi_config::{THEME, theme::CustomSectionArc}; use yazi_shared::url::UrlBuf; -use crate::LUA; +use crate::{LUA, theme::icon}; pub fn compose() -> Composer { fn get(lua: &Lua, key: &[u8]) -> mlua::Result { @@ -23,6 +23,7 @@ pub fn compose() -> Composer { b"cmp" => cmp(), b"tasks" => tasks(), b"help" => help(), + b"icon" => icon(), _ => return custom(lua, key), } .into_lua(lua) diff --git a/yazi-plugin/src/utils/cache.rs b/yazi-plugin/src/utils/cache.rs index 58d5fe63..f8f80ba0 100644 --- a/yazi-plugin/src/utils/cache.rs +++ b/yazi-plugin/src/utils/cache.rs @@ -12,18 +12,20 @@ impl Utils { pub(super) fn file_cache(lua: &Lua) -> mlua::Result { lua.create_function(|_, t: Table| { let file: FileRef = t.raw_get("file")?; - if file.url.parent() == Some(yazi_shared::url::Url::regular(&YAZI.preview.cache_dir)) { - return Ok(None); - } + file.borrow(|f| { + if f.url.parent() == Some(yazi_shared::url::Url::regular(&YAZI.preview.cache_dir)) { + return Ok(None); + } - let hex = { - let mut h = Twox128::default(); - file.hash(&mut h); - t.raw_get("skip").unwrap_or(0usize).hash(&mut h); - format!("{:x}", h.finish_128()) - }; + let hex = { + let mut h = Twox128::default(); + f.hash(&mut h); + t.raw_get("skip").unwrap_or(0usize).hash(&mut h); + format!("{:x}", h.finish_128()) + }; - Ok(Some(UrlBuf::from(YAZI.preview.cache_dir.join(hex)))) + Ok(Some(UrlBuf::from(YAZI.preview.cache_dir.join(hex)))) + }) }) } } diff --git a/yazi-term/src/restorer/windows.rs b/yazi-term/src/restorer/windows.rs index 4571638b..da5291f8 100644 --- a/yazi-term/src/restorer/windows.rs +++ b/yazi-term/src/restorer/windows.rs @@ -1,6 +1,6 @@ use std::{io::{self, Write}, os::windows::io::AsRawHandle}; -use windows_sys::Win32::System::Console::{CONSOLE_MODE, GetConsoleCP, GetConsoleMode, GetConsoleOutputCP, SetConsoleCP, SetConsoleMode, SetConsoleOutputCP}; +use windows_sys::Win32::System::Console::{CONSOLE_MODE, FlushConsoleInputBuffer, GetConsoleCP, GetConsoleMode, GetConsoleOutputCP, SetConsoleCP, SetConsoleMode, SetConsoleOutputCP}; use yazi_shim::{bool_ok, nz_ok}; use yazi_tty::Tty; @@ -29,6 +29,7 @@ impl Restorer { pub fn restore(&self, tty: &Tty) { tty.writer().flush().ok(); unsafe { + FlushConsoleInputBuffer(tty.reader().as_raw_handle()); SetConsoleCP(self.input_cp); SetConsoleMode(tty.reader().as_raw_handle(), self.input_mode); SetConsoleOutputCP(self.output_cp); diff --git a/yazi-widgets/src/input/option.rs b/yazi-widgets/src/input/option.rs index 023f1f99..d3d50c65 100644 --- a/yazi-widgets/src/input/option.rs +++ b/yazi-widgets/src/input/option.rs @@ -6,7 +6,7 @@ use crate::input::{InputCallback, InputStyles}; #[derive(Clone, Debug, Default)] pub struct InputOpt { - pub id: String, + pub name: String, pub title: String, pub value: String, pub styles: InputStyles, @@ -43,7 +43,7 @@ impl TryFrom<&Table> for InputOpt { fn try_from(t: &Table) -> Result { Ok(Self { - id: t.raw_get("id").unwrap_or_default(), + name: t.raw_get("name").unwrap_or_default(), title: t.raw_get("title").unwrap_or_default(), value: t.raw_get("value").unwrap_or_default(), styles: t.raw_get("styles")?,