feature: Multiple previewers to cycle through

This commit is contained in:
Peter Morgenstern 2025-09-18 07:33:24 +02:00
parent 30c0279570
commit 6d7227cd69
No known key found for this signature in database
GPG key ID: 9302A0B3EF9BC111
9 changed files with 134 additions and 27 deletions

View file

@ -1,4 +1,5 @@
use anyhow::Result;
use yazi_config::YAZI;
use yazi_macro::succ;
use yazi_parser::mgr::PeekOpt;
use yazi_proxy::HIDER;
@ -24,30 +25,55 @@ impl Actor for Peek {
let mime = cx.mgr.mimetype.by_file_owned(&hovered).unwrap_or_default();
let folder = cx.tab().hovered_folder().map(|f| (f.offset, f.cha));
if !cx.tab().preview.same_url(&hovered.url) {
cx.tab_mut().preview.skip = folder.map(|f| f.0).unwrap_or_default();
}
if !cx.tab().preview.same_file(&hovered, &mime) {
cx.tab_mut().preview.reset();
let same_url = cx.tab().preview.same_url(&hovered.url);
let same_file = cx.tab().preview.same_file(&hovered, &mime);
{
let preview = &mut cx.tab_mut().preview;
if !same_url {
preview.skip = folder.map(|f| f.0).unwrap_or_default();
preview.run = 0;
}
if !same_file {
preview.reset();
}
if let Some(step) = opt.cycle {
if step == 0 {
preview.run = 0;
} else if let Some(previewer) = YAZI.plugin.previewer(&hovered.url, &mime) {
let len = previewer.len();
if len == 0 {
preview.run = 0;
} else {
let len_isize = len as isize;
let current = (preview.run % len) as isize;
let next = (current + step as isize).rem_euclid(len_isize);
preview.run = next as usize;
}
} else {
preview.run = 0;
}
}
if let Some(skip) = opt.skip {
if opt.upper_bound {
preview.skip = preview.skip.min(skip);
} else {
preview.skip = skip;
}
}
}
if matches!(opt.only_if, Some(u) if u != hovered.url) {
succ!();
}
if let Some(skip) = opt.skip {
let preview = &mut cx.tab_mut().preview;
if opt.upper_bound {
preview.skip = preview.skip.min(skip);
} else {
preview.skip = skip;
}
}
let force = opt.force || opt.cycle.is_some();
if hovered.is_dir() {
cx.tab_mut().preview.go_folder(hovered, folder.map(|(_, cha)| cha), opt.force);
cx.tab_mut().preview.go_folder(hovered, folder.map(|(_, cha)| cha), force);
} else {
cx.tab_mut().preview.go(hovered, mime, opt.force);
cx.tab_mut().preview.go(hovered, mime, force);
}
succ!();
}

View file

@ -26,8 +26,12 @@ impl Actor for Seek {
let Some(previewer) = YAZI.plugin.previewer(&hovered.url, mime) else {
succ!(cx.tab_mut().preview.reset());
};
let run_idx = cx.tab().preview.run;
let Some(cmd) = previewer.cmd(run_idx) else {
succ!(cx.tab_mut().preview.reset());
};
isolate::seek_sync(&previewer.run, hovered.clone(), opt.units);
isolate::seek_sync(cmd, hovered.clone(), opt.units);
succ!();
}
}

View file

@ -18,7 +18,9 @@ impl Actor for UpdatePeeked {
};
if opt.lock.url == *hovered {
cx.tab_mut().preview.lock = Some(opt.lock);
let preview = &mut cx.tab_mut().preview;
preview.lock_run = preview.run;
preview.lock = Some(opt.lock);
render!();
}

View file

@ -54,6 +54,8 @@ keymap = [
# Seeking
{ on = "K", run = "seek -5", desc = "Seek up 5 units in the preview" },
{ on = "J", run = "seek 5", desc = "Seek down 5 units in the preview" },
{ on = [ "g", "p" ], run = "peek --cycle=next", desc = "Cycle to next previewer" },
{ on = [ "g", "P" ], run = "peek --cycle=prev", desc = "Cycle to previous previewer" },
# Spotting
{ on = "<Tab>", run = "spot", desc = "Spot hovered file" },

View file

@ -124,8 +124,8 @@ preloaders = [
previewers = [
{ url = "*/", run = "folder" },
# Code
{ mime = "text/*", run = "code" },
{ mime = "application/{mbox,javascript,wine-extension-ini}", run = "code" },
{ mime = "text/*", run = [ "code", "file" ] },
{ mime = "application/{mbox,javascript,wine-extension-ini}", run = [ "code", "file" ] },
# JSON
{ mime = "application/{json,ndjson}", run = "json" },
# Image

View file

@ -1,4 +1,4 @@
use serde::Deserialize;
use serde::{Deserialize, Deserializer, de};
use yazi_shared::{MIME_DIR, event::Cmd, url::UrlBuf};
use crate::Pattern;
@ -7,7 +7,8 @@ use crate::Pattern;
pub struct Previewer {
pub url: Option<Pattern>,
pub mime: Option<Pattern>,
pub run: Cmd,
#[serde(deserialize_with = "deserialize_run")]
pub run: Vec<Cmd>,
}
impl Previewer {
@ -17,9 +18,47 @@ impl Previewer {
|| self.url.as_ref().is_some_and(|p| p.match_url(url, mime == MIME_DIR))
}
#[inline]
pub fn len(&self) -> usize { self.run.len() }
#[inline]
pub fn cmd(&self, index: usize) -> Option<&Cmd> {
if self.run.is_empty() {
return None;
}
self.run.get(index).or_else(|| self.run.first())
}
#[inline]
pub fn any_file(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_file()) }
#[inline]
pub fn any_dir(&self) -> bool { self.url.as_ref().is_some_and(|p| p.any_dir()) }
}
#[derive(Deserialize)]
#[serde(untagged)]
enum RunField {
One(String),
Many(Vec<String>),
}
fn deserialize_run<'de, D>(deserializer: D) -> Result<Vec<Cmd>, D::Error>
where
D: Deserializer<'de>,
{
let field = RunField::deserialize(deserializer)?;
let items: Vec<String> = match field {
RunField::One(s) => vec![s],
RunField::Many(list) => list,
};
if items.is_empty() {
return Err(de::Error::custom("previewer.run must not be empty"));
}
items
.into_iter()
.map(|s| s.parse::<Cmd>().map_err(de::Error::custom))
.collect()
}

View file

@ -15,9 +15,12 @@ use yazi_shared::{MIME_DIR, pool::{InternStr, Symbol}, url::UrlBuf};
pub struct Preview {
pub lock: Option<PreviewLock>,
pub skip: usize,
pub run: usize,
previewer_ct: Option<CancellationToken>,
folder_loader: Option<JoinHandle<()>>,
pub lock_run: usize,
}
impl Preview {
@ -32,8 +35,15 @@ impl Preview {
return self.reset();
};
if self.run >= previewer.len() {
self.run = 0;
}
let Some(cmd) = previewer.cmd(self.run) else {
return self.reset();
};
self.abort();
self.previewer_ct = isolate::peek(&previewer.run, file, mime, self.skip);
self.previewer_ct = isolate::peek(cmd, file, mime, self.skip);
}
pub fn go_folder(&mut self, file: File, dir: Option<Cha>, force: bool) {
@ -76,6 +86,7 @@ impl Preview {
pub fn reset(&mut self) {
self.abort();
ADAPTOR.get().image_hide().ok();
self.lock_run = 0;
render!(self.lock.take().is_some())
}
@ -92,6 +103,10 @@ impl Preview {
}
pub fn same_lock(&self, file: &File, mime: &str) -> bool {
self.same_file(file, mime) && matches!(&self.lock, Some(l) if l.skip == self.skip)
if let Some(lock) = &self.lock {
self.same_file(file, mime) && lock.skip == self.skip && self.lock_run == self.run
} else {
false
}
}
}

View file

@ -7,6 +7,7 @@ pub struct PeekOpt {
pub force: bool,
pub only_if: Option<UrlCow<'static>>,
pub upper_bound: bool,
pub cycle: Option<i32>,
}
impl From<CmdCow> for PeekOpt {
@ -16,6 +17,7 @@ impl From<CmdCow> for PeekOpt {
force: c.bool("force"),
only_if: c.take_url("only-if"),
upper_bound: c.bool("upper-bound"),
cycle: c.take_str("cycle").and_then(|s| parse_cycle(s.as_ref())),
}
}
}
@ -31,3 +33,12 @@ impl FromLua for PeekOpt {
impl IntoLua for PeekOpt {
fn into_lua(self, _: &Lua) -> mlua::Result<Value> { Err("unsupported".into_lua_err()) }
}
fn parse_cycle(value: &str) -> Option<i32> {
match value.to_ascii_lowercase().as_str() {
"next" | "forward" | "+" => Some(1),
"prev" | "previous" | "back" | "-" => Some(-1),
"reset" | "first" | "start" | "0" => Some(0),
_ => value.parse().ok(),
}
}

View file

@ -98,15 +98,23 @@ impl UserData for Preloader {
struct Previewer {
inner: &'static yazi_config::plugin::Previewer,
v_cmd: Option<Value>,
v_cmd: Option<Value>,
v_cmds: Option<Value>,
}
impl Previewer {
pub fn new(inner: &'static yazi_config::plugin::Previewer) -> Self { Self { inner, v_cmd: None } }
pub fn new(inner: &'static yazi_config::plugin::Previewer) -> Self {
Self { inner, v_cmd: None, v_cmds: None }
}
}
impl UserData for Previewer {
fn add_fields<F: mlua::UserDataFields<Self>>(fields: &mut F) {
cached_field!(fields, cmd, |lua, me| lua.create_string(me.inner.run.name.as_ref()));
cached_field!(fields, cmd, |lua, me| {
lua.create_string(me.inner.cmd(0).map(|cmd| cmd.name.as_ref()).unwrap_or(""))
});
cached_field!(fields, cmds, |lua, me| {
lua.create_sequence_from(me.inner.run.iter().map(|cmd| cmd.name.as_ref()))
});
}
}