mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-21 14:51:03 +00:00
feat: make visual mode support wraparound scrolling (#4101)
This commit is contained in:
parent
6e0aaee822
commit
c92c4aba88
35 changed files with 262 additions and 123 deletions
|
|
@ -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
12
Cargo.lock
generated
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>()?),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)?;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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!());
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
49
yazi-core/src/tab/visual.rs
Normal file
49
yazi-core/src/tab/visual.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
49
yazi-fs/src/file/file_ref.rs
Normal file
49
yazi-fs/src/file/file_ref.rs
Normal 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())?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) .. " "
|
||||
|
|
|
|||
23
yazi-plugin/src/theme/icon.rs
Normal file
23
yazi-plugin/src/theme/icon.rs
Normal 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)
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
yazi_macro::mod_flat!(theme);
|
||||
yazi_macro::mod_flat!(icon theme);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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")?,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue