diff --git a/yazi-actor/src/mgr/peek.rs b/yazi-actor/src/mgr/peek.rs index f9f2f83a..2b8c0852 100644 --- a/yazi-actor/src/mgr/peek.rs +++ b/yazi-actor/src/mgr/peek.rs @@ -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!(); } diff --git a/yazi-actor/src/mgr/seek.rs b/yazi-actor/src/mgr/seek.rs index 25fdbc78..34905f42 100644 --- a/yazi-actor/src/mgr/seek.rs +++ b/yazi-actor/src/mgr/seek.rs @@ -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!(); } } diff --git a/yazi-actor/src/mgr/update_peeked.rs b/yazi-actor/src/mgr/update_peeked.rs index 6d3a5867..a720aa5d 100644 --- a/yazi-actor/src/mgr/update_peeked.rs +++ b/yazi-actor/src/mgr/update_peeked.rs @@ -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!(); } diff --git a/yazi-config/preset/keymap-default.toml b/yazi-config/preset/keymap-default.toml index dee02f00..c5ca31af 100644 --- a/yazi-config/preset/keymap-default.toml +++ b/yazi-config/preset/keymap-default.toml @@ -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 = "", run = "spot", desc = "Spot hovered file" }, diff --git a/yazi-config/preset/yazi-default.toml b/yazi-config/preset/yazi-default.toml index 25add0a8..c4de57cf 100644 --- a/yazi-config/preset/yazi-default.toml +++ b/yazi-config/preset/yazi-default.toml @@ -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 diff --git a/yazi-config/src/plugin/previewer.rs b/yazi-config/src/plugin/previewer.rs index 2aff9340..f63d7e94 100644 --- a/yazi-config/src/plugin/previewer.rs +++ b/yazi-config/src/plugin/previewer.rs @@ -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, pub mime: Option, - pub run: Cmd, + #[serde(deserialize_with = "deserialize_run")] + pub run: Vec, } 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), +} + +fn deserialize_run<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let field = RunField::deserialize(deserializer)?; + let items: Vec = 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::().map_err(de::Error::custom)) + .collect() +} diff --git a/yazi-core/src/tab/preview.rs b/yazi-core/src/tab/preview.rs index 3d8f5fdf..f4a3a806 100644 --- a/yazi-core/src/tab/preview.rs +++ b/yazi-core/src/tab/preview.rs @@ -15,9 +15,12 @@ use yazi_shared::{MIME_DIR, pool::{InternStr, Symbol}, url::UrlBuf}; pub struct Preview { pub lock: Option, pub skip: usize, + pub run: usize, previewer_ct: Option, folder_loader: Option>, + + 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, 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 + } } } diff --git a/yazi-parser/src/mgr/peek.rs b/yazi-parser/src/mgr/peek.rs index d92a4181..316aa989 100644 --- a/yazi-parser/src/mgr/peek.rs +++ b/yazi-parser/src/mgr/peek.rs @@ -7,6 +7,7 @@ pub struct PeekOpt { pub force: bool, pub only_if: Option>, pub upper_bound: bool, + pub cycle: Option, } impl From for PeekOpt { @@ -16,6 +17,7 @@ impl From 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 { Err("unsupported".into_lua_err()) } } + +fn parse_cycle(value: &str) -> Option { + 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(), + } +} diff --git a/yazi-plugin/src/runtime/plugin.rs b/yazi-plugin/src/runtime/plugin.rs index 09203648..2146f44f 100644 --- a/yazi-plugin/src/runtime/plugin.rs +++ b/yazi-plugin/src/runtime/plugin.rs @@ -98,15 +98,23 @@ impl UserData for Preloader { struct Previewer { inner: &'static yazi_config::plugin::Previewer, - v_cmd: Option, + v_cmd: Option, + v_cmds: Option, } 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>(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())) + }); } }