feat: make visual mode support wraparound scrolling (#4101)

This commit is contained in:
三咲雅 misaki masa 2026-07-05 18:06:25 +08:00 committed by GitHub
parent 6e0aaee822
commit c92c4aba88
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 262 additions and 123 deletions

View file

@ -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

12
Cargo.lock generated
View file

@ -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",
]

View file

@ -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;

View file

@ -64,7 +64,11 @@ impl UserData for File {
fn add_methods<M: UserDataMethods<Self>>(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::<File>()?.clone()),
borrow: |ud, f| f(&*ud.borrow::<File>()?),
}
}

View file

@ -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<F: UserDataFields<Self>>(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<M: UserDataMethods<Self>>(methods: &mut M) {

View file

@ -14,6 +14,7 @@ impl Actor for Arrow {
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
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)?;

View file

@ -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();

View file

@ -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!());

View file

@ -13,13 +13,17 @@ keymap = [
{ on = "<C-c>", run = "close", desc = "Close the current tab, or quit if it's last" },
{ on = "<C-z>", 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 = "<Up>", run = "arrow prev", desc = "Previous file" },
{ on = "<Down>", 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 = "<Home>", run = "arrow top", desc = "Go to top" },
{ on = "<End>", run = "arrow bot", desc = "Go to bottom" },
{ on = "<C-u>", run = "arrow -50%", desc = "Move cursor up half page" },
{ on = "<C-d>", run = "arrow 50%", desc = "Move cursor down half page" },
{ on = "<C-b>", run = "arrow -100%", desc = "Move cursor up one page" },
@ -30,9 +34,6 @@ keymap = [
{ on = "<PageUp>", run = "arrow -100%", desc = "Move cursor up one page" },
{ on = "<PageDown>", 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" },

View file

@ -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),
}
}

View file

@ -60,7 +60,7 @@ impl TryFrom<Table> 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()
})

View file

@ -95,7 +95,7 @@ impl TryFrom<Table> 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()
})

View file

@ -97,7 +97,7 @@ impl TryFrom<Table> 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()
})

View file

@ -90,7 +90,7 @@ impl TryFrom<Table> 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()
})

View file

@ -90,7 +90,7 @@ impl TryFrom<Table> 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()
})

View file

@ -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,

View file

@ -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,

View file

@ -22,14 +22,16 @@ impl TryFrom<Table> for SpotLock {
fn try_from(t: Table) -> Result<Self, Self::Error> {
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(),
})
})
}
}

View file

@ -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);

View file

@ -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<usize>),
Unset(usize, BTreeSet<usize>),
Select(Visual),
Unset(Visual),
}
impl Mode {
pub fn visual_mut(&mut self) -> Option<(usize, &mut BTreeSet<usize>)> {
pub fn visual(&self) -> Option<Visual> {
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<usize>)> {
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<VisualIndices> {
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",

View file

@ -23,14 +23,16 @@ impl TryFrom<Table> for PreviewLock {
fn try_from(t: Table) -> Result<Self, Self::Error> {
let file: FileRef = t.raw_get("file")?;
Ok(Self {
url: file.url_owned(),
cha: file.cha,
mime: t.raw_get::<mlua::String>("mime")?.to_str()?.intern(),
file.borrow(|f| {
Ok(Self {
url: f.url_owned(),
cha: f.cha,
mime: t.raw_get::<mlua::String>("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(),
})
})
}
}

View file

@ -0,0 +1,49 @@
use std::{iter::Chain, ops::Range};
use yazi_widgets::Step;
pub type VisualIndices = Chain<Range<usize>, Range<usize>>;
#[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<usize>, Range<usize>) {
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),
}
}
}

View file

@ -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),

View file

@ -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<FileRef> for File {
type Error = mlua::Error;
fn try_from(value: FileRef) -> Result<Self, Self::Error> { value.borrow(|f| Ok(f.clone())) }
}
impl<'a> TryFrom<FileRef> for Cow<'a, File> {
type Error = mlua::Error;
fn try_from(value: FileRef) -> Result<Self, Self::Error> { File::try_from(value).map(Cow::Owned) }
}
impl FileRef {
pub fn borrow<R>(&self, mut f: impl FnMut(&File) -> mlua::Result<R>) -> mlua::Result<R> {
let mut result = None;
for inv in inventory::iter::<FileInventory>() {
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::<File>() {
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<Self> {
match value {
Value::UserData(ud) => Ok(Self(ud)),
_ => Err(EXPECTED.into_lua_err())?,
}
}
}

View file

@ -4,7 +4,7 @@ use crate::file::File;
pub struct FileInventory {
pub register: fn(&mut UserDataRegistry<File>),
pub from_lua: fn(&AnyUserData) -> mlua::Result<File>,
pub borrow: fn(&AnyUserData, &mut dyn FnMut(&File) -> mlua::Result<()>) -> mlua::Result<()>,
}
inventory::collect!(FileInventory);

View file

@ -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<File>;
use crate::file::{File, FileInventory, FileRef};
const EXPECTED: &str = "expected a table, File, or fs::File";
@ -25,19 +23,11 @@ impl TryFrom<AnyUserData> for File {
type Error = mlua::Error;
fn try_from(value: AnyUserData) -> Result<Self, Self::Error> {
if let Ok(me) = value.take::<Self>() {
return Ok(me);
match value.take::<Self>() {
Ok(me) => Ok(me),
Err(mlua::Error::UserDataTypeMismatch) => FileRef(value).try_into(),
Err(e) => Err(e),
}
for inv in inventory::iter::<FileInventory>() {
match (inv.from_lua)(&value) {
Ok(file) => return Ok(file),
Err(mlua::Error::UserDataTypeMismatch) => continue,
Err(e) => return Err(e),
}
}
Err(mlua::Error::UserDataTypeMismatch)
}
}

View file

@ -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);

View file

@ -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

View file

@ -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) .. " "

View file

@ -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<ComposerGet, ComposerSet> {
fn get(lua: &Lua, key: &[u8]) -> mlua::Result<Value> {
match key {
b"match" => lua
.create_function(|_, (_, file, opts): (Value, FileRef, Option<Table>)| {
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<Value> { Ok(value) }
Composer::new(get, set)
}

View file

@ -1 +1 @@
yazi_macro::mod_flat!(theme);
yazi_macro::mod_flat!(icon theme);

View file

@ -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<ComposerGet, ComposerSet> {
fn get(lua: &Lua, key: &[u8]) -> mlua::Result<Value> {
@ -23,6 +23,7 @@ pub fn compose() -> Composer<ComposerGet, ComposerSet> {
b"cmp" => cmp(),
b"tasks" => tasks(),
b"help" => help(),
b"icon" => icon(),
_ => return custom(lua, key),
}
.into_lua(lua)

View file

@ -12,18 +12,20 @@ impl Utils {
pub(super) fn file_cache(lua: &Lua) -> mlua::Result<Function> {
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))))
})
})
}
}

View file

@ -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);

View file

@ -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<Self, Self::Error> {
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")?,