feat: file navigation wraparound with new arrow prev and arrow next commands (#2485)

This commit is contained in:
三咲雅 · Misaki Masa 2025-03-15 17:44:42 +08:00 committed by GitHub
parent abfbd1cd9f
commit 324a439c73
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 87 additions and 119 deletions

28
Cargo.lock generated
View file

@ -999,9 +999,9 @@ dependencies = [
[[package]]
name = "half"
version = "2.4.1"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888"
checksum = "7db2ff139bba50379da6aa0766b52fdcb62cb5b263009b09ed58ba604e14bbd1"
dependencies = [
"cfg-if",
"crunchy",
@ -1373,9 +1373,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
name = "libc"
version = "0.2.170"
version = "0.2.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
[[package]]
name = "libfuzzer-sys"
@ -1767,9 +1767,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.21.0"
version = "1.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cde51589ab56b20a6f686b2c68f7a0bd6add753d697abf720d63f8db3ab7b1ad"
checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc"
[[package]]
name = "onig"
@ -1990,9 +1990,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.39"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
@ -2632,9 +2632,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.44.0"
version = "1.44.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a"
checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a"
dependencies = [
"backtrace",
"bytes",
@ -2672,9 +2672,9 @@ dependencies = [
[[package]]
name = "tokio-util"
version = "0.7.13"
version = "0.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078"
checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034"
dependencies = [
"bytes",
"futures-core",
@ -3288,9 +3288,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.3"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1"
checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36"
dependencies = [
"memchr",
]

View file

@ -26,7 +26,7 @@ foldhash = "0.1.4"
futures = "0.3.31"
globset = "0.4.16"
indexmap = { version = "2.8.0", features = [ "serde" ] }
libc = "0.2.170"
libc = "0.2.171"
lru = "0.13.0"
md-5 = "0.10.6"
mlua = { version = "0.10.3", features = [ "anyhow", "async", "error-send", "lua54", "macros", "serialize" ] }
@ -37,9 +37,9 @@ regex = "1.11.1"
scopeguard = "1.2.0"
serde = { version = "1.0.219", features = [ "derive" ] }
serde_json = "1.0.140"
tokio = { version = "1.44.0", features = [ "full" ] }
tokio = { version = "1.44.1", features = [ "full" ] }
tokio-stream = "0.1.17"
tokio-util = "0.7.13"
tokio-util = "0.7.14"
toml = { version = "0.8.20" }
tracing = { version = "0.1.41", features = [ "max_level_debug", "release_max_level_debug" ] }
twox-hash = { version = "2.1.0", default-features = false, features = [ "std", "random", "xxhash3_128" ] }

View file

@ -14,4 +14,4 @@ proc-macro = true
[dependencies]
# External dependencies
syn = { version = "2.0.100", features = [ "full" ] }
quote = "1.0.39"
quote = "1.0.40"

View file

@ -5,19 +5,19 @@
[manager]
keymap = [
{ on = "<Esc>", run = "escape", desc = "Exit visual mode, clear selected, or cancel search" },
{ on = "<C-[>", run = "escape", desc = "Exit visual mode, clear selected, or cancel search" },
{ on = "<Esc>", run = "escape", desc = "Exit visual mode, clear selection, or cancel search" },
{ on = "<C-[>", run = "escape", desc = "Exit visual mode, clear selection, or cancel search" },
{ on = "q", run = "quit", desc = "Quit the process" },
{ on = "Q", run = "quit --no-cwd-file", desc = "Quit the process without outputting cwd-file" },
{ on = "Q", run = "quit --no-cwd-file", desc = "Quit without outputting cwd-file" },
{ 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 -1", desc = "Move cursor up" },
{ on = "j", run = "arrow 1", desc = "Move cursor down" },
{ on = "k", run = "arrow prev", desc = "Go to previous file" },
{ on = "j", run = "arrow next", desc = "Go to next file" },
{ on = "<Up>", run = "arrow -1", desc = "Move cursor up" },
{ on = "<Down>", run = "arrow 1", desc = "Move cursor down" },
{ on = "<Up>", run = "arrow prev", desc = "Go to previous file" },
{ on = "<Down>", run = "arrow next", desc = "Go to next file" },
{ on = "<C-u>", run = "arrow -50%", desc = "Move cursor up half page" },
{ on = "<C-d>", run = "arrow 50%", desc = "Move cursor down half page" },
@ -39,13 +39,13 @@ keymap = [
{ on = "<Left>", run = "leave", desc = "Go back to the parent directory" },
{ on = "<Right>", run = "enter", desc = "Enter the child directory" },
{ on = "H", run = "back", desc = "Go back to the previous directory" },
{ on = "L", run = "forward", desc = "Go forward to the next directory" },
{ on = "H", run = "back", desc = "Back to previous directory" },
{ on = "L", run = "forward", desc = "Forward to next directory" },
# Toggle
{ on = "<Space>", run = [ "toggle", "arrow 1" ], desc = "Toggle the current selection state" },
{ on = "<C-a>", run = "toggle_all --state=on", desc = "Select all files" },
{ on = "<C-r>", run = "toggle_all", desc = "Invert selection of all files" },
{ on = "<Space>", run = [ "toggle", "arrow next" ], desc = "Toggle the current selection state" },
{ on = "<C-a>", run = "toggle_all --state=on", desc = "Select all files" },
{ on = "<C-r>", run = "toggle_all", desc = "Invert selection of all files" },
# Visual mode
{ on = "v", run = "visual_mode", desc = "Enter visual mode (selection mode)" },
@ -105,8 +105,8 @@ keymap = [
# Find
{ on = "/", run = "find --smart", desc = "Find next file" },
{ on = "?", run = "find --previous --smart", desc = "Find previous file" },
{ on = "n", run = "find_arrow", desc = "Goto the next found" },
{ on = "N", run = "find_arrow --previous", desc = "Goto the previous found" },
{ on = "n", run = "find_arrow", desc = "Go to next found" },
{ on = "N", run = "find_arrow --previous", desc = "Go to previous found" },
# Sorting
{ on = [ ",", "m" ], run = [ "sort mtime --reverse=no", "linemode mtime" ], desc = "Sort by modified time" },
@ -125,25 +125,25 @@ keymap = [
# Goto
{ on = [ "g", "h" ], run = "cd ~", desc = "Go home" },
{ on = [ "g", "c" ], run = "cd ~/.config", desc = "Goto ~/.config" },
{ on = [ "g", "d" ], run = "cd ~/Downloads", desc = "Goto ~/Downloads" },
{ on = [ "g", "c" ], run = "cd ~/.config", desc = "Go ~/.config" },
{ on = [ "g", "d" ], run = "cd ~/Downloads", desc = "Go ~/Downloads" },
{ on = [ "g", "<Space>" ], run = "cd --interactive", desc = "Jump interactively" },
# Tabs
{ on = "t", run = "tab_create --current", desc = "Create a new tab with CWD" },
{ on = "1", run = "tab_switch 0", desc = "Switch to the first tab" },
{ on = "2", run = "tab_switch 1", desc = "Switch to the second tab" },
{ on = "3", run = "tab_switch 2", desc = "Switch to the third tab" },
{ on = "4", run = "tab_switch 3", desc = "Switch to the fourth tab" },
{ on = "5", run = "tab_switch 4", desc = "Switch to the fifth tab" },
{ on = "6", run = "tab_switch 5", desc = "Switch to the sixth tab" },
{ on = "7", run = "tab_switch 6", desc = "Switch to the seventh tab" },
{ on = "8", run = "tab_switch 7", desc = "Switch to the eighth tab" },
{ on = "9", run = "tab_switch 8", desc = "Switch to the ninth tab" },
{ on = "1", run = "tab_switch 0", desc = "Switch to first tab" },
{ on = "2", run = "tab_switch 1", desc = "Switch to second tab" },
{ on = "3", run = "tab_switch 2", desc = "Switch to third tab" },
{ on = "4", run = "tab_switch 3", desc = "Switch to fourth tab" },
{ on = "5", run = "tab_switch 4", desc = "Switch to fifth tab" },
{ on = "6", run = "tab_switch 5", desc = "Switch to sixth tab" },
{ on = "7", run = "tab_switch 6", desc = "Switch to seventh tab" },
{ on = "8", run = "tab_switch 7", desc = "Switch to eighth tab" },
{ on = "9", run = "tab_switch 8", desc = "Switch to ninth tab" },
{ on = "[", run = "tab_switch -1 --relative", desc = "Switch to the previous tab" },
{ on = "]", run = "tab_switch 1 --relative", desc = "Switch to the next tab" },
{ on = "[", run = "tab_switch -1 --relative", desc = "Switch to previous tab" },
{ on = "]", run = "tab_switch 1 --relative", desc = "Switch to next tab" },
{ on = "{", run = "tab_swap -1", desc = "Swap current tab with previous tab" },
{ on = "}", run = "tab_swap 1", desc = "Swap current tab with next tab" },
@ -188,13 +188,13 @@ keymap = [
{ on = "k", run = "arrow -1", desc = "Move cursor up" },
{ on = "j", run = "arrow 1", desc = "Move cursor down" },
{ on = "h", run = "swipe -1", desc = "Swipe to the previous file" },
{ on = "l", run = "swipe 1", desc = "Swipe to the next file" },
{ on = "h", run = "swipe -1", desc = "Swipe to previous file" },
{ on = "l", run = "swipe 1", desc = "Swipe to next file" },
{ on = "<Up>", run = "arrow -1", desc = "Move cursor up" },
{ on = "<Down>", run = "arrow 1", desc = "Move cursor down" },
{ on = "<Left>", run = "swipe -1", desc = "Swipe to the next file" },
{ on = "<Right>", run = "swipe 1", desc = "Swipe to the previous file" },
{ on = "<Left>", run = "swipe -1", desc = "Swipe to next file" },
{ on = "<Right>", run = "swipe 1", desc = "Swipe to previous file" },
# Copy
{ on = [ "c", "c" ], run = "copy cell", desc = "Copy selected cell" },

View file

@ -11,4 +11,6 @@ impl Layout {
pub const fn default() -> Self {
Self { current: Rect::ZERO, preview: Rect::ZERO, progress: Rect::ZERO }
}
pub const fn limit(&self) -> usize { self.current.height as _ }
}

View file

@ -27,18 +27,6 @@ pub fn init() -> anyhow::Result<()> {
wait_for_key(e)?;
try_init(false)?;
}
// TODO: remove this
for c in KEYMAP.mgr.iter().flat_map(|c| c.run.iter()) {
if c.name == "arrow"
&& c.first_str().unwrap_or_default().parse::<isize>().is_ok_and(|n| n <= -999 || n >= 999)
{
eprintln!("Deprecated command: `arrow -99999999` and `arrow 99999999` have been deprecated, please use `arrow top` and `arrow bot` instead, in your `keymap.toml`.
See #2294 for more details: https://github.com/sxyazi/yazi/pull/2294");
}
}
Ok(())
}

View file

@ -22,18 +22,6 @@ impl From<isize> for Opt {
impl Tab {
#[yazi_codegen::command]
pub fn arrow(&mut self, opt: Opt) {
// TODO: remove this
if let Step::Fixed(n) = opt.step {
if n <= -999999 || n >= 999999 {
yazi_proxy::AppProxy::notify_warn(
"Deprecated command",
"`arrow -99999999` and `arrow 99999999` have been deprecated, please use `arrow top` and `arrow bot` instead.
See #2294 for more details: https://github.com/sxyazi/yazi/pull/2294",
);
}
}
if !self.current.arrow(opt.step) {
return;
}

View file

@ -93,13 +93,13 @@ impl Folder {
}
pub fn arrow(&mut self, step: impl Into<Step>) -> bool {
let step = step.into() as Step;
let new = (step.into() as Step).add(self.cursor, self.files.len(), LAYOUT.get().limit());
let mut b = if self.files.is_empty() {
(mem::take(&mut self.cursor), mem::take(&mut self.offset)) != (0, 0)
} else if step.is_positive() {
self.next(step)
} else if new > self.cursor {
self.next(new)
} else {
self.prev(step)
self.prev(new)
};
self.trace = self.hovered().filter(|_| b).map(|h| h.urn_owned()).or(self.trace.take());
@ -124,7 +124,7 @@ impl Folder {
}
pub fn sync_page(&mut self, force: bool) {
let limit = LAYOUT.get().current.height as usize;
let limit = LAYOUT.get().limit();
if limit == 0 {
return;
}
@ -135,14 +135,14 @@ impl Folder {
}
}
fn next(&mut self, step: Step) -> bool {
fn next(&mut self, new: usize) -> bool {
let old = (self.cursor, self.offset);
let len = self.files.len();
let limit = LAYOUT.get().current.height as usize;
let limit = LAYOUT.get().limit();
let scrolloff = (limit / 2).min(MGR.scrolloff as usize);
self.cursor = step.add(self.cursor, limit).min(len.saturating_sub(1));
self.cursor = new.min(len.saturating_sub(1));
self.offset = if self.cursor < (self.offset + limit).min(len).saturating_sub(scrolloff) {
self.offset.min(len.saturating_sub(1))
} else {
@ -152,14 +152,14 @@ impl Folder {
old != (self.cursor, self.offset)
}
fn prev(&mut self, step: Step) -> bool {
fn prev(&mut self, new: usize) -> bool {
let old = (self.cursor, self.offset);
let max = self.files.len().saturating_sub(1);
let limit = LAYOUT.get().current.height as usize;
let limit = LAYOUT.get().limit();
let scrolloff = (limit / 2).min(MGR.scrolloff as usize);
self.cursor = step.add(self.cursor, limit).min(max);
self.cursor = new.min(max);
self.offset = if self.cursor < self.offset + scrolloff {
self.offset.saturating_sub(old.0 - self.cursor)
} else {
@ -173,7 +173,7 @@ impl Folder {
let old = self.offset;
let len = self.files.len();
let limit = LAYOUT.get().current.height as usize;
let limit = LAYOUT.get().limit();
let scrolloff = (limit / 2).min(MGR.scrolloff as usize);
self.offset = if self.cursor < (self.offset + limit).min(len).saturating_sub(scrolloff) {
@ -192,7 +192,7 @@ impl Folder {
pub fn paginate(&self, page: usize) -> &[File] {
let len = self.files.len();
let limit = LAYOUT.get().current.height as usize;
let limit = LAYOUT.get().limit();
let start = (page.saturating_sub(1) * limit).min(len.saturating_sub(1));
let end = ((page + 2) * limit).min(len);

View file

@ -2,10 +2,12 @@ use std::{num::ParseIntError, str::FromStr};
use yazi_shared::event::Data;
#[derive(Clone, Copy)]
#[derive(Clone, Copy, Debug)]
pub enum Step {
Top,
Bot,
Prev,
Next,
Fixed(isize),
Percent(i8),
}
@ -25,6 +27,8 @@ impl FromStr for Step {
Ok(match s {
"top" => Self::Top,
"bot" => Self::Bot,
"prev" => Self::Prev,
"next" => Self::Next,
s if s.ends_with('%') => Self::Percent(s[..s.len() - 1].parse()?),
s => Self::Fixed(s.parse()?),
})
@ -44,24 +48,29 @@ impl TryFrom<&Data> for Step {
}
impl Step {
#[inline]
pub fn add(self, pos: usize, limit: usize) -> usize {
pub fn add(self, pos: usize, len: usize, limit: usize) -> usize {
if len == 0 {
return 0;
}
let fixed = match self {
Self::Top => return 0,
Self::Bot => return usize::MAX,
Self::Bot => return len - 1,
Self::Prev => -1,
Self::Next => 1,
Self::Fixed(n) => n,
Self::Percent(0) => 0,
Self::Percent(n) => n as isize * limit as isize / 100,
};
if fixed > 0 { pos + fixed as usize } else { pos.saturating_sub(fixed.unsigned_abs()) }
}
#[inline]
pub fn is_positive(self) -> bool {
match self {
Self::Top | Self::Bot => false,
Self::Fixed(n) => n > 0,
Self::Percent(n) => n > 0,
if fixed == 0 {
pos
} else if matches!(self, Self::Prev | Self::Next) {
fixed.saturating_add_unsigned(pos).rem_euclid(len as _) as _
} else if fixed > 0 {
pos + fixed as usize
} else {
pos.saturating_sub(fixed.unsigned_abs())
}
}
}

View file

@ -1,4 +1,4 @@
use mlua::{ExternalError, ExternalResult, IntoLua, ObjectLike, Table, Value};
use mlua::{ExternalError, ExternalResult, IntoLua, ObjectLike, Table};
use tokio::runtime::Handle;
use yazi_config::LAYOUT;
use yazi_dds::Sendable;
@ -28,25 +28,7 @@ pub async fn preload(
("skip", 0.into_lua(&lua)?),
])?;
let (ok, mut err): (Value, Option<Error>) =
Handle::current().block_on(plugin.call_async_method("preload", job))?;
// TODO: remove this
let ok = match ok {
Value::Boolean(b) => b,
Value::Integer(n) => {
crate::deprecate!(lua, "The integer return value of `preload()` has been deprecated since 25.01.27, please use the new `(bool, error)` instead, in your {}.
See #2253 for more information: https://github.com/sxyazi/yazi/pull/2253");
if n as u8 & 1 == 0 {
err = Some(Error::Custom(format!("Returned {n} when running the preloader")));
}
n as u8 & 1 == 1
},
_ => Err("The first return value of `preload()` must be a bool".into_lua_err())?,
};
Ok((ok, err))
Handle::current().block_on(plugin.call_async_method("preload", job))
})
.await
.into_lua_err()?

View file

@ -60,7 +60,6 @@ pub fn compose(lua: &Lua, isolate: bool) -> mlua::Result<Value> {
b"target_family" => Utils::target_family(lua)?,
// Text
b"md5" => Utils::hash(lua, true)?, // TODO: deprecate this in the future
b"hash" => Utils::hash(lua, false)?,
b"quote" => Utils::quote(lua)?,
b"truncate" => Utils::truncate(lua)?,