diff --git a/yazi-adaptor/src/adaptor.rs b/yazi-adaptor/src/adaptor.rs index 8dbb5a2b..4d16fa30 100644 --- a/yazi-adaptor/src/adaptor.rs +++ b/yazi-adaptor/src/adaptor.rs @@ -7,7 +7,7 @@ use tracing::warn; use yazi_config::PREVIEW; use yazi_shared::{env_exists, RoCell}; -use super::{Iterm2, Kitty}; +use super::{Iterm2, Kitty, KittyOld}; use crate::{ueberzug::Ueberzug, Sixel, TMUX}; static IMAGE_SHOWN: AtomicBool = AtomicBool::new(false); @@ -18,6 +18,7 @@ static UEBERZUG: RoCell>>> = RoCe #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Adaptor { Kitty, + KittyOld, Iterm2, Sixel, @@ -83,7 +84,7 @@ impl Adaptor { let mut protocols = match Self::emulator() { Emulator::Unknown => vec![], Emulator::Kitty => vec![Self::Kitty], - Emulator::Konsole => vec![Self::Kitty, Self::Iterm2, Self::Sixel], + Emulator::Konsole => vec![Self::KittyOld, Self::Iterm2, Self::Sixel], Emulator::Iterm2 => vec![Self::Iterm2, Self::Sixel], Emulator::WezTerm => vec![Self::Iterm2, Self::Sixel], Emulator::Foot => vec![Self::Sixel], @@ -99,6 +100,9 @@ impl Adaptor { if env_exists("ZELLIJ_SESSION_NAME") { protocols.retain(|p| *p == Self::Sixel); } + if *TMUX && protocols.len() > 1 { + protocols.retain(|p| *p != Self::KittyOld); + } if let Some(p) = protocols.first() { return *p; } @@ -148,6 +152,7 @@ impl ToString for Adaptor { fn to_string(&self) -> String { match self { Self::Kitty => "kitty", + Self::KittyOld => "kitty", Self::Iterm2 => "iterm2", Self::Sixel => "sixel", Self::X11 => "x11", @@ -174,6 +179,7 @@ impl Adaptor { match self { Self::Kitty => Kitty::image_show(path, rect).await, + Self::KittyOld => KittyOld::image_show(path, rect).await, Self::Iterm2 => Iterm2::image_show(path, rect).await, Self::Sixel => Sixel::image_show(path, rect).await, _ => Ok(if let Some(tx) = &*UEBERZUG { @@ -190,6 +196,7 @@ impl Adaptor { match self { Self::Kitty => Kitty::image_hide(rect), Self::Iterm2 => Iterm2::image_hide(rect), + Self::KittyOld => KittyOld::image_hide(), Self::Sixel => Sixel::image_hide(rect), _ => Ok(if let Some(tx) = &*UEBERZUG { tx.send(None)?; @@ -199,6 +206,6 @@ impl Adaptor { #[inline] pub(super) fn needs_ueberzug(self) -> bool { - !matches!(self, Self::Kitty | Self::Iterm2 | Self::Sixel) + !matches!(self, Self::Kitty | Self::KittyOld | Self::Iterm2 | Self::Sixel) } } diff --git a/yazi-adaptor/src/kitty_old.rs b/yazi-adaptor/src/kitty_old.rs new file mode 100644 index 00000000..72e9f2fa --- /dev/null +++ b/yazi-adaptor/src/kitty_old.rs @@ -0,0 +1,76 @@ +use std::{io::{stdout, Write}, path::Path}; + +use anyhow::Result; +use base64::{engine::general_purpose, Engine}; +use image::DynamicImage; +use ratatui::prelude::Rect; +use yazi_shared::Term; + +use super::image::Image; +use crate::{CLOSE, ESCAPE, START}; + +pub(super) struct KittyOld; + +impl KittyOld { + pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<()> { + let img = Image::downscale(path, (rect.width, rect.height)).await?; + let b = Self::encode(img).await?; + + Self::image_hide()?; + Term::move_lock(stdout().lock(), (rect.x, rect.y), |stdout| Ok(stdout.write_all(&b)?)) + } + + #[inline] + pub(super) fn image_hide() -> Result<()> { + let mut stdout = stdout().lock(); + stdout.write_all(format!("{}_Gq=1,a=d,d=A{}\\{}", START, ESCAPE, CLOSE).as_bytes())?; + stdout.flush()?; + Ok(()) + } + + async fn encode(img: DynamicImage) -> Result> { + fn output(raw: &[u8], format: u8, size: (u32, u32)) -> Result> { + let b64 = general_purpose::STANDARD.encode(raw).chars().collect::>(); + + let mut it = b64.chunks(4096).peekable(); + let mut buf = Vec::with_capacity(b64.len() + it.len() * 50); + if let Some(first) = it.next() { + write!( + buf, + "{}_Gq=1,a=T,z=-1,C=1,f={},s={},v={},m={};{}{}\\{}", + START, + format, + size.0, + size.1, + it.peek().is_some() as u8, + first.iter().collect::(), + ESCAPE, + CLOSE + )?; + } + + while let Some(chunk) = it.next() { + write!( + buf, + "{}_Gm={};{}{}\\{}", + START, + it.peek().is_some() as u8, + chunk.iter().collect::(), + ESCAPE, + CLOSE + )?; + } + + buf.write_all(CLOSE.as_bytes())?; + Ok(buf) + } + + let size = (img.width(), img.height()); + tokio::task::spawn_blocking(move || match img { + DynamicImage::ImageRgb8(v) => output(v.as_raw(), 24, size), + DynamicImage::ImageRgba8(v) => output(v.as_raw(), 32, size), + v => output(v.to_rgb8().as_raw(), 24, size), + }) + .await? + } +} diff --git a/yazi-adaptor/src/lib.rs b/yazi-adaptor/src/lib.rs index 0f3929a8..de261e47 100644 --- a/yazi-adaptor/src/lib.rs +++ b/yazi-adaptor/src/lib.rs @@ -4,12 +4,14 @@ mod adaptor; mod image; mod iterm2; mod kitty; +mod kitty_old; mod sixel; mod ueberzug; use adaptor::*; use iterm2::*; use kitty::*; +use kitty_old::*; use sixel::*; use yazi_shared::{env_exists, RoCell}; diff --git a/yazi-config/src/open/opener.rs b/yazi-config/src/open/opener.rs index 456f1180..c3cb9a29 100644 --- a/yazi-config/src/open/opener.rs +++ b/yazi-config/src/open/opener.rs @@ -41,9 +41,9 @@ impl<'de> Deserialize<'de> for Opener { #[serde(rename = "for")] for_: Option, - // TODO: remove this when v1.0.5 is released -- + // TODO: remove this when v0.1.6 is released -- display_name: Option, - // TODO: -- remove this when v1.0.5 is released + // TODO: -- remove this when v0.1.6 is released } let mut shadow = Shadow::deserialize(deserializer)?; diff --git a/yazi-core/src/context.rs b/yazi-core/src/context.rs index d44ee720..313ff4a3 100644 --- a/yazi-core/src/context.rs +++ b/yazi-core/src/context.rs @@ -51,9 +51,4 @@ impl Ctx { } None } - - #[inline] - pub fn image_layer(&self) -> bool { - !self.which.visible && !self.help.visible && !self.tasks.visible - } } diff --git a/yazi-core/src/event.rs b/yazi-core/src/event.rs index f0544455..485f7972 100644 --- a/yazi-core/src/event.rs +++ b/yazi-core/src/event.rs @@ -24,7 +24,6 @@ pub enum Event { Files(FilesOp), Pages(usize), Mimetype(BTreeMap), - Peek(Option<(usize, Url)>), Preview(PreviewLock), // Input @@ -80,12 +79,6 @@ macro_rules! emit { (Mimetype($mimes:expr)) => { $crate::Event::Mimetype($mimes).emit(); }; - (Peek) => { - $crate::Event::Peek(None).emit(); - }; - (Peek($skip:expr, $url:expr)) => { - $crate::Event::Peek(Some(($skip, $url))).emit(); - }; (Preview($lock:expr)) => { $crate::Event::Preview($lock).emit(); }; diff --git a/yazi-core/src/help/help.rs b/yazi-core/src/help/help.rs index a50f8641..4da65c18 100644 --- a/yazi-core/src/help/help.rs +++ b/yazi-core/src/help/help.rs @@ -4,7 +4,7 @@ use yazi_config::{keymap::{Control, Key, KeymapLayer}, KEYMAP}; use yazi_shared::Term; use super::HELP_MARGIN; -use crate::{emit, input::Input}; +use crate::input::Input; #[derive(Default)] pub struct Help { @@ -34,8 +34,6 @@ impl Help { self.offset = 0; self.cursor = 0; - - emit!(Peek); // Show/hide preview for images true } diff --git a/yazi-core/src/manager/commands/create.rs b/yazi-core/src/manager/commands/create.rs index cea6fce1..64fbad46 100644 --- a/yazi-core/src/manager/commands/create.rs +++ b/yazi-core/src/manager/commands/create.rs @@ -44,7 +44,6 @@ impl Manager { if let Ok(f) = File::from(child.clone()).await { emit!(Files(FilesOp::Creating(cwd, f.into_map()))); Manager::_hover(Some(child)); - Manager::_refresh(); } Ok::<(), anyhow::Error>(()) }); diff --git a/yazi-core/src/manager/commands/hover.rs b/yazi-core/src/manager/commands/hover.rs index 78e1311d..d6d24b33 100644 --- a/yazi-core/src/manager/commands/hover.rs +++ b/yazi-core/src/manager/commands/hover.rs @@ -23,25 +23,26 @@ impl Manager { } pub fn hover(&mut self, opt: impl Into) -> bool { + // Hover on the file + let opt = opt.into() as Opt; + let b = self.current_mut().repos(opt.url); + + // Re-peek + self.peek(0); + // Refresh watcher let mut to_watch = BTreeSet::new(); for tab in self.tabs.iter() { to_watch.insert(&tab.current.cwd); - match tab.current.hovered() { - Some(h) if h.is_dir() => _ = to_watch.insert(&h.url), - _ => {} - } if let Some(ref p) = tab.parent { to_watch.insert(&p.cwd); } + if let Some(h) = tab.current.hovered().filter(|&h| h.is_dir()) { + to_watch.insert(&h.url); + } } self.watcher.watch(to_watch); - // Trigger peek - emit!(Peek); - - // Hover - let opt = opt.into() as Opt; - self.current_mut().repos(opt.url) + b } } diff --git a/yazi-core/src/manager/commands/peek.rs b/yazi-core/src/manager/commands/peek.rs index da9723cd..0dce34b0 100644 --- a/yazi-core/src/manager/commands/peek.rs +++ b/yazi-core/src/manager/commands/peek.rs @@ -1,31 +1,86 @@ -use crate::manager::Manager; +use yazi_config::{keymap::{Exec, KeymapLayer}, MANAGER}; +use yazi_shared::{Url, MIME_DIR}; + +use crate::{emit, manager::Manager}; + +#[derive(Debug)] +pub struct Opt { + step: isize, + only_if: Option, + upper_bound: bool, +} + +impl From<&Exec> for Opt { + fn from(e: &Exec) -> Self { + Self { + step: e.args.first().and_then(|s| s.parse().ok()).unwrap_or(0), + only_if: e.named.get("only-if").map(Url::from), + upper_bound: e.named.contains_key("upper-bound"), + } + } +} +impl From for Opt { + fn from(step: isize) -> Self { Self { step, only_if: None, upper_bound: false } } +} impl Manager { - pub fn peek(&mut self, sequent: bool, show_image: bool) -> bool { - let Some(hovered) = self.hovered().cloned() else { - return self.active_mut().preview.reset(|_| true); + #[inline] + pub fn _peek_upper_bound(bound: usize, only_if: &Url) { + emit!(Call( + Exec::call("peek", vec![bound.to_string()]) + .with("only-if", only_if.to_string()) + .with_bool("upper-bound", true) + .vec(), + KeymapLayer::Manager + )); + } + + pub fn peek(&mut self, opt: impl Into) -> bool { + let Some(hovered) = self.hovered() else { + return self.active_mut().preview.reset(); }; - let url = &hovered.url; - if !show_image { - self.active_mut().preview.reset(|l| l.is_image()); - } - - if hovered.is_dir() { - let position = self.active().history(url).map(|f| (f.offset, f.files.len())); - self.active_mut().preview.folder(url, position, sequent); + let opt = opt.into() as Opt; + if matches!(opt.only_if, Some(ref u) if *u != hovered.url) { return false; } - let Some(mime) = self.mimetype.get(url).cloned() else { - return self.active_mut().preview.reset(|_| true); + if hovered.is_dir() { + return self.peek_folder(opt, hovered.url.clone()); + } + + let Some(mime) = self.mimetype.get(&hovered.url).cloned() else { + return self.active_mut().preview.reset(); }; - if sequent { - self.active_mut().preview.sequent(url, &mime, show_image); + let (url, cha) = (hovered.url.clone(), hovered.cha); + if self.active().preview.same_url(&url) { + self.active_mut().preview.arrow(opt.step, &mime); + } else if opt.upper_bound { + self.active_mut().preview.apply_bound(opt.step as usize); } else { - self.active_mut().preview.go(url, &mime, show_image); + self.active_mut().preview.set_skip(0); } + + self.active_mut().preview.go(&url, cha, &mime); + false + } + + fn peek_folder(&mut self, opt: Opt, url: Url) -> bool { + let folder = self.active().history.get(&url); + let (skip, bound) = folder + .map(|f| (f.offset, f.files.len().saturating_sub(MANAGER.layout.folder_height()))) + .unwrap_or_default(); + + if self.active().preview.same_url(&url) { + self.active_mut().preview.arrow(opt.step, MIME_DIR); + self.active_mut().preview.apply_bound(bound); + return false; + } + + let in_chunks = folder.is_none(); + self.active_mut().preview.set_skip(skip); + self.active_mut().preview.go_folder(url, in_chunks); false } } diff --git a/yazi-core/src/manager/tabs.rs b/yazi-core/src/manager/tabs.rs index 80967e69..a235fcaa 100644 --- a/yazi-core/src/manager/tabs.rs +++ b/yazi-core/src/manager/tabs.rs @@ -31,7 +31,7 @@ impl Tabs { #[inline] pub(super) fn set_idx(&mut self, idx: usize) { self.idx = idx; - self.active_mut().preview.reset(|l| l.is_image()); + self.active_mut().preview.reset_image(); Manager::_refresh(); } } diff --git a/yazi-core/src/preview/mod.rs b/yazi-core/src/preview/mod.rs index 3a54a75d..2d5ba586 100644 --- a/yazi-core/src/preview/mod.rs +++ b/yazi-core/src/preview/mod.rs @@ -3,3 +3,5 @@ mod provider; pub use preview::*; use provider::*; + +pub static COLLISION: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); diff --git a/yazi-core/src/preview/preview.rs b/yazi-core/src/preview/preview.rs index 9b78fdeb..4bb996d5 100644 --- a/yazi-core/src/preview/preview.rs +++ b/yazi-core/src/preview/preview.rs @@ -1,13 +1,13 @@ -use std::time::Duration; +use std::{mem, time::Duration}; use tokio::{pin, task::JoinHandle}; use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; use yazi_adaptor::ADAPTOR; use yazi_config::MANAGER; -use yazi_shared::{MimeKind, PeekError, Url, MIME_DIR}; +use yazi_shared::{Cha, MimeKind, PeekError, Url}; use super::Provider; -use crate::{emit, files::{Files, FilesOp}, Highlighter}; +use crate::{emit, files::{Files, FilesOp}, manager::Manager, Highlighter}; #[derive(Default)] pub struct Preview { @@ -19,7 +19,7 @@ pub struct Preview { pub struct PreviewLock { pub url: Url, - pub mime: String, + pub cha: Option, pub skip: usize, pub data: PreviewData, } @@ -32,125 +32,59 @@ pub enum PreviewData { } impl Preview { - pub fn go(&mut self, url: &Url, mime: &str, show_image: bool) { - let kind = MimeKind::new(mime); - if !show_image && kind.show_as_image() { - return; - } else if self.same(url, mime) { + pub fn go(&mut self, url: &Url, cha: Cha, mime: &str) { + if self.content_unchanged(url, &cha) { return; } - self.reset(|_| true); - if !self.same_mime(url, mime) { - self.skip = 0; - } + self.reset(); + let (url, kind, skip) = (url.clone(), MimeKind::new(mime), self.skip); - let (url, mime, skip) = (url.clone(), mime.to_owned(), self.skip); self.handle = Some(tokio::spawn(async move { match Provider::auto(kind, &url, skip).await { Ok(data) => { - emit!(Preview(PreviewLock { url, mime, skip, data })); + emit!(Preview(PreviewLock { url, cha: Some(cha), skip, data })); } Err(PeekError::Exceed(max)) => { - emit!(Peek(max, url)); + Manager::_peek_upper_bound(max, &url); } _ => {} } })); } - pub fn folder(&mut self, url: &Url, position: Option<(usize, usize)>, sequent: bool) { - if let Some((_, len)) = position { - self.skip = self.skip.min(len.saturating_sub(MANAGER.layout.preview_height())); - } - - if self.same(url, MIME_DIR) { - return; - } else if !self.same_mime(url, MIME_DIR) { - self.skip = position.map(|(offset, _)| offset).unwrap_or(0); - } - - self.reset(|_| true); - emit!(Preview(PreviewLock { + pub fn go_folder(&mut self, url: Url, in_chunks: bool) { + self.reset(); + self.lock = Some(PreviewLock { url: url.clone(), - mime: MIME_DIR.to_owned(), + cha: None, skip: self.skip, data: PreviewData::Folder, - })); + }); - if sequent { - return; - } - - let url = url.clone(); self.handle = Some(tokio::spawn(async move { let Ok(rx) = Files::from_dir(&url).await else { - emit!(Files(FilesOp::IOErr(url))); + emit!(Files(FilesOp::IOErr(url.clone()))); return; }; - if position.is_some() { - emit!(Files(FilesOp::Full(url, UnboundedReceiverStream::new(rx).collect().await))); + if !in_chunks { + emit!(Files(FilesOp::Full(url.clone(), UnboundedReceiverStream::new(rx).collect().await))); return; } - let rx = UnboundedReceiverStream::new(rx).chunks_timeout(10000, Duration::from_millis(500)); - pin!(rx); + let stream = + UnboundedReceiverStream::new(rx).chunks_timeout(10000, Duration::from_millis(500)); + pin!(stream); let ticket = FilesOp::prepare(&url); - while let Some(chunk) = rx.next().await { + while let Some(chunk) = stream.next().await { emit!(Files(FilesOp::Part(url.clone(), ticket, chunk))); } })); } - pub fn sequent(&mut self, url: &Url, mime: &str, show_image: bool) { - let kind = MimeKind::new(mime); - if !show_image && kind.show_as_image() { - return; - } else if self.same(url, mime) { - return; - } - - self.handle.take().map(|h| h.abort()); - Highlighter::abort(); - - let (url, mime, skip) = (url.clone(), mime.to_owned(), self.skip); - self.handle = Some(tokio::spawn(async move { - match Provider::auto(kind, &url, skip).await { - Ok(data) => { - emit!(Preview(PreviewLock { url, mime, skip, data })); - } - Err(PeekError::Exceed(max)) => { - emit!(Peek(max, url)); - } - _ => {} - } - })); - } - - pub fn arrow(&mut self, step: isize) -> bool { - let Some(kind) = self.lock.as_ref().map(|l| MimeKind::new(&l.mime)) else { - return false; - }; - - let old = self.skip; - let size = Provider::step_size(kind, step.unsigned_abs()); - - self.skip = if step < 0 { old.saturating_sub(size) } else { old + size }; - self.skip != old - } - - pub fn arrow_max(&mut self, max: usize) -> bool { - if self.skip > max { - self.skip = max; - return true; - } - - false - } - - pub fn reset bool>(&mut self, f: F) -> bool { + pub fn reset(&mut self) -> bool { self.handle.take().map(|h| h.abort()); Highlighter::abort(); ADAPTOR.image_hide(MANAGER.layout.image_rect()).ok(); @@ -159,39 +93,71 @@ impl Preview { return false; }; - if !f(lock) { - return false; - } - let b = !lock.is_image(); self.lock = None; b } + + pub fn reset_image(&mut self) -> bool { + if !matches!(self.lock, Some(ref lock) if lock.is_image()) { + return false; + } + + self.reset(); + true + } + + #[inline] + pub fn same_url(&self, url: &Url) -> bool { + matches!(self.lock, Some(ref lock) if lock.url == *url) + } + + fn content_unchanged(&self, url: &Url, cha: &Cha) -> bool { + let Some(lock) = &self.lock else { + return false; + }; + let Some(cha_) = &lock.cha else { + return false; + }; + + *url == lock.url + && self.skip == lock.skip + && cha.len == cha_.len + && cha.modified == cha_.modified + && cha.meta == cha_.meta + && { + #[cfg(unix)] + { + cha.permissions == cha_.permissions + } + #[cfg(windows)] + { + true + } + } + } } impl Preview { + // --- skip #[inline] - pub fn same(&self, url: &Url, mime: &str) -> bool { - if let Some(ref lock) = self.lock { - return &lock.url == url && lock.mime == mime && lock.skip == self.skip; - } - false + pub fn arrow(&mut self, step: isize, mime: &str) -> bool { + let size = Provider::step_size(MimeKind::new(mime), step.unsigned_abs()); + let skip = if step < 0 { self.skip.saturating_sub(size) } else { self.skip + size }; + mem::replace(&mut self.skip, skip) != skip } #[inline] - pub fn same_mime(&self, url: &Url, mime: &str) -> bool { - if let Some(ref lock) = self.lock { - return &lock.url == url && lock.mime == mime; - } - false - } + pub fn set_skip(&mut self, skip: usize) -> bool { mem::replace(&mut self.skip, skip) != skip } #[inline] - pub fn same_path(&self, url: &Url) -> bool { - if let Some(ref lock) = self.lock { - return &lock.url == url; + pub fn apply_bound(&mut self, upper: usize) -> bool { + if self.skip <= upper { + return false; } - false + + self.skip = upper; + true } } diff --git a/yazi-core/src/tab/commands/hidden.rs b/yazi-core/src/tab/commands/hidden.rs index 2d192b59..f66b3cba 100644 --- a/yazi-core/src/tab/commands/hidden.rs +++ b/yazi-core/src/tab/commands/hidden.rs @@ -1,6 +1,6 @@ use yazi_config::keymap::Exec; -use crate::{emit, tab::Tab}; +use crate::{manager::Manager, tab::Tab}; impl Tab { pub fn hidden(&mut self, e: &Exec) -> bool { @@ -10,7 +10,7 @@ impl Tab { _ => !self.conf.show_hidden, }; if self.apply_files_attrs(false) { - emit!(Peek); + Manager::_hover(None); return true; } false diff --git a/yazi-core/src/tab/commands/search.rs b/yazi-core/src/tab/commands/search.rs index da436995..6d60aafb 100644 --- a/yazi-core/src/tab/commands/search.rs +++ b/yazi-core/src/tab/commands/search.rs @@ -76,7 +76,7 @@ impl Tab { handle.abort(); } if self.current.cwd.is_search() { - self.preview.reset(|l| l.is_image()); + self.preview.reset_image(); let rep = self.history_new(&self.current.cwd.to_regular()); drop(mem::replace(&mut self.current, rep)); diff --git a/yazi-core/src/tab/tab.rs b/yazi-core/src/tab/tab.rs index ea3fc9ba..393fb714 100644 --- a/yazi-core/src/tab/tab.rs +++ b/yazi-core/src/tab/tab.rs @@ -47,21 +47,9 @@ impl From<&Url> for Tab { } impl Tab { - pub fn update_peek(&mut self, max: usize, url: Url) -> bool { - let Some(hovered) = self.current.hovered() else { - return false; - }; - - if url != hovered.url { - return false; - } - - self.preview.arrow_max(max) - } - pub fn update_preview(&mut self, lock: PreviewLock) -> bool { let Some(hovered) = self.current.hovered().map(|h| &h.url) else { - return self.preview.reset(|_| true); + return self.preview.reset(); }; if lock.url != *hovered { diff --git a/yazi-core/src/tasks/commands/toggle.rs b/yazi-core/src/tasks/commands/toggle.rs index 1e335701..ad7a4cda 100644 --- a/yazi-core/src/tasks/commands/toggle.rs +++ b/yazi-core/src/tasks/commands/toggle.rs @@ -1,6 +1,6 @@ use yazi_config::keymap::Exec; -use crate::{emit, tasks::Tasks}; +use crate::tasks::Tasks; pub struct Opt; @@ -14,7 +14,6 @@ impl From<()> for Opt { impl Tasks { pub fn toggle(&mut self, _: impl Into) -> bool { self.visible = !self.visible; - emit!(Peek); // Show/hide preview for images true } } diff --git a/yazi-core/src/which/which.rs b/yazi-core/src/which/which.rs index cca92830..ab7c7dbd 100644 --- a/yazi-core/src/which/which.rs +++ b/yazi-core/src/which/which.rs @@ -24,7 +24,7 @@ impl Which { self.times = 1; self.cands = KEYMAP.get(layer).iter().filter(|s| s.on.len() > 1 && &s.on[0] == key).cloned().collect(); - self.switch(true); + self.visible = true; true } @@ -35,22 +35,16 @@ impl Which { .collect(); if self.cands.is_empty() { - self.switch(false); + self.visible = false; } else if self.cands.len() == 1 { - self.switch(false); + self.visible = false; emit!(Call(self.cands[0].to_call(), self.layer)); } else if let Some(i) = self.cands.iter().position(|c| c.on.len() == self.times + 1) { - self.switch(false); + self.visible = false; emit!(Call(self.cands[i].to_call(), self.layer)); } self.times += 1; true } - - #[inline] - fn switch(&mut self, state: bool) { - self.visible = state; - emit!(Peek); // Show/hide preview for images - } } diff --git a/yazi-fm/src/app.rs b/yazi-fm/src/app.rs index 2575e741..f6523f9e 100644 --- a/yazi-fm/src/app.rs +++ b/yazi-fm/src/app.rs @@ -1,11 +1,11 @@ -use std::ffi::OsString; +use std::{ffi::OsString, sync::atomic::Ordering}; use anyhow::{Ok, Result}; use crossterm::event::KeyEvent; -use ratatui::prelude::Rect; +use ratatui::{backend::Backend, prelude::Rect}; use tokio::sync::oneshot; use yazi_config::{keymap::{Exec, Key, KeymapLayer}, BOOT}; -use yazi_core::{emit, files::FilesOp, input::InputMode, manager::Manager, Ctx, Event}; +use yazi_core::{emit, files::FilesOp, input::InputMode, manager::Manager, preview::COLLISION, Ctx, Event}; use yazi_shared::Term; use crate::{Executor, Logs, Panic, Root, Signals}; @@ -33,7 +33,7 @@ impl App { } Event::Key(key) => app.dispatch_key(key), Event::Paste(str) => app.dispatch_paste(str), - Event::Render(_) => app.dispatch_render(), + Event::Render(_) => app.dispatch_render()?, Event::Resize(cols, rows) => app.dispatch_resize(cols, rows), Event::Stop(state, tx) => app.dispatch_stop(state, tx), Event::Call(exec, layer) => app.dispatch_call(exec, layer), @@ -76,18 +76,47 @@ impl App { } } - fn dispatch_render(&mut self) { - if let Some(term) = &mut self.term { - _ = term.draw(|f| { - yazi_plugin::scope(&self.cx, |_| { - f.render_widget(Root::new(&self.cx), f.size()); - }); + fn dispatch_render(&mut self) -> Result<()> { + let Some(term) = &mut self.term else { + return Ok(()); + }; - if let Some((x, y)) = self.cx.cursor() { - f.set_cursor(x, y); - } + let collision = COLLISION.swap(false, Ordering::Relaxed); + let frame = term.draw(|f| { + yazi_plugin::scope(&self.cx, |_| { + f.render_widget(Root::new(&self.cx), f.size()); }); + + if let Some((x, y)) = self.cx.cursor() { + f.set_cursor(x, y); + } + })?; + if !COLLISION.load(Ordering::Relaxed) { + if collision { + // Reload preview if collision is resolved + self.cx.manager.active_mut().preview.reset(); + self.cx.manager.peek(0); + } + return Ok(()); } + + let mut patches = Vec::new(); + for x in frame.area.left()..frame.area.right() { + for y in frame.area.top()..frame.area.bottom() { + let cell = frame.buffer.get(x, y); + if cell.skip { + patches.push((x, y, cell.clone())); + } + } + } + + term.backend_mut().draw(patches.iter().map(|(x, y, cell)| (*x, *y, cell)))?; + if let Some((x, y)) = self.cx.cursor() { + term.show_cursor()?; + term.set_cursor(x, y)?; + } + term.backend_mut().flush()?; + Ok(()) } fn dispatch_resize(&mut self, cols: u16, rows: u16) { @@ -96,13 +125,13 @@ impl App { } self.cx.manager.current_mut().set_page(true); - self.cx.manager.active_mut().preview.reset(|_| true); - self.cx.manager.peek(true, self.cx.image_layer()); + self.cx.manager.active_mut().preview.reset(); + self.cx.manager.peek(0); emit!(Render); } fn dispatch_stop(&mut self, state: bool, tx: Option>) { - self.cx.manager.active_mut().preview.reset(|l| l.is_image()); + self.cx.manager.active_mut().preview.reset_image(); if state { self.signals.stop_term(true); self.term = None; @@ -148,15 +177,7 @@ impl App { Event::Mimetype(mimes) => { if manager.update_mimetype(mimes, tasks) { emit!(Render); - emit!(Peek); - } - } - Event::Peek(sequent) => { - if let Some((max, url)) = sequent { - manager.active_mut().update_peek(max, url); - self.cx.manager.peek(true, self.cx.image_layer()); - } else { - self.cx.manager.peek(false, self.cx.image_layer()); + manager.peek(0); } } Event::Preview(lock) => { diff --git a/yazi-fm/src/completion/completion.rs b/yazi-fm/src/completion/completion.rs index f748ef77..f10bd83b 100644 --- a/yazi-fm/src/completion/completion.rs +++ b/yazi-fm/src/completion/completion.rs @@ -1,9 +1,11 @@ use std::path::MAIN_SEPARATOR; -use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, Borders, Clear, List, ListItem, Widget}}; +use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, Borders, List, ListItem, Widget}}; use yazi_config::{popup::{Offset, Position}, THEME}; use yazi_core::Ctx; +use crate::widgets; + pub(crate) struct Completion<'a> { cx: &'a Ctx, } @@ -53,7 +55,7 @@ impl<'a> Widget for Completion<'a> { area.height = rect.height.saturating_sub(area.y).min(area.height); } - Clear.render(area, buf); + widgets::Clear.render(area, buf); List::new(items) .block( Block::new() diff --git a/yazi-fm/src/executor.rs b/yazi-fm/src/executor.rs index c2d3fad2..d73d21b5 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -88,6 +88,7 @@ impl<'a> Executor<'a> { }; } + on!(MANAGER, peek); on!(MANAGER, hover); on!(MANAGER, refresh); on!(MANAGER, quit, &self.cx.tasks); @@ -139,11 +140,6 @@ impl<'a> Executor<'a> { on!(TABS, swap); match exec.cmd.as_bytes() { - b"peek" => { - let step = exec.args.first().and_then(|s| s.parse().ok()).unwrap_or(0); - self.cx.manager.active_mut().preview.arrow(step); - self.cx.manager.peek(true, self.cx.image_layer()) - } // Tasks b"tasks_show" => self.cx.tasks.toggle(()), // Help diff --git a/yazi-fm/src/help/layout.rs b/yazi-fm/src/help/layout.rs index b290e509..7aa4ab1a 100644 --- a/yazi-fm/src/help/layout.rs +++ b/yazi-fm/src/help/layout.rs @@ -1,8 +1,9 @@ -use ratatui::{buffer::Buffer, layout::{self, Rect}, prelude::{Constraint, Direction}, widgets::{Clear, Paragraph, Widget}}; +use ratatui::{buffer::Buffer, layout::{self, Rect}, prelude::{Constraint, Direction}, widgets::{Paragraph, Widget}}; use yazi_config::THEME; use yazi_core::Ctx; use super::Bindings; +use crate::widgets; pub(crate) struct Layout<'a> { cx: &'a Ctx, @@ -19,7 +20,7 @@ impl<'a> Widget for Layout<'a> { .constraints([Constraint::Min(0), Constraint::Length(1)]) .split(area); - Clear.render(area, buf); + widgets::Clear.render(area, buf); let help = &self.cx.help; Paragraph::new(help.keyword().unwrap_or_else(|| format!("{}.help", help.layer))) diff --git a/yazi-fm/src/input/input.rs b/yazi-fm/src/input/input.rs index 71590d10..bf779176 100644 --- a/yazi-fm/src/input/input.rs +++ b/yazi-fm/src/input/input.rs @@ -1,11 +1,13 @@ use std::ops::Range; use ansi_to_tui::IntoText; -use ratatui::{buffer::Buffer, layout::Rect, text::{Line, Text}, widgets::{Block, BorderType, Borders, Clear, Paragraph, Widget}}; +use ratatui::{buffer::Buffer, layout::Rect, text::{Line, Text}, widgets::{Block, BorderType, Borders, Paragraph, Widget}}; use yazi_config::THEME; use yazi_core::{input::InputMode, Ctx}; use yazi_shared::Term; +use crate::widgets; + pub(crate) struct Input<'a> { cx: &'a Ctx, } @@ -25,7 +27,7 @@ impl<'a> Widget for Input<'a> { Text::from(input.value()) }; - Clear.render(area, buf); + widgets::Clear.render(area, buf); Paragraph::new(value) .block( Block::new() diff --git a/yazi-fm/src/main.rs b/yazi-fm/src/main.rs index dcf86b74..0dbe25b4 100644 --- a/yazi-fm/src/main.rs +++ b/yazi-fm/src/main.rs @@ -12,6 +12,7 @@ mod select; mod signals; mod tasks; mod which; +mod widgets; use app::*; use executor::*; diff --git a/yazi-fm/src/select/select.rs b/yazi-fm/src/select/select.rs index 0fd3a43f..a7e94d2b 100644 --- a/yazi-fm/src/select/select.rs +++ b/yazi-fm/src/select/select.rs @@ -1,7 +1,9 @@ -use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, Borders, Clear, List, ListItem, Widget}}; +use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, Borders, List, ListItem, Widget}}; use yazi_config::THEME; use yazi_core::Ctx; +use crate::widgets; + pub(crate) struct Select<'a> { cx: &'a Ctx, } @@ -28,7 +30,7 @@ impl<'a> Widget for Select<'a> { }) .collect::>(); - Clear.render(area, buf); + widgets::Clear.render(area, buf); List::new(items) .block( Block::new() diff --git a/yazi-fm/src/tasks/clear.rs b/yazi-fm/src/tasks/clear.rs deleted file mode 100644 index 2aaa073e..00000000 --- a/yazi-fm/src/tasks/clear.rs +++ /dev/null @@ -1,19 +0,0 @@ -use ratatui::{buffer::Buffer, layout::Rect, widgets::{self, Widget}}; - -pub(super) struct Clear; - -impl Widget for Clear { - fn render(self, mut area: Rect, buf: &mut Buffer) { - if area.x > 0 { - area.x -= 1; - area.width += 2; - } - - if area.y > 0 { - area.y -= 1; - area.height += 2; - } - - widgets::Clear.render(area, buf) - } -} diff --git a/yazi-fm/src/tasks/layout.rs b/yazi-fm/src/tasks/layout.rs index 362a3fdb..7c07ab73 100644 --- a/yazi-fm/src/tasks/layout.rs +++ b/yazi-fm/src/tasks/layout.rs @@ -2,7 +2,7 @@ use ratatui::{buffer::Buffer, layout::{self, Alignment, Constraint, Direction, R use yazi_config::THEME; use yazi_core::{tasks::TASKS_PERCENT, Ctx}; -use super::Clear; +use crate::widgets; pub(crate) struct Layout<'a> { cx: &'a Ctx, @@ -36,7 +36,7 @@ impl<'a> Widget for Layout<'a> { fn render(self, area: Rect, buf: &mut Buffer) { let area = Self::area(area); - Clear.render(area, buf); + widgets::Clear.render(area, buf); let block = Block::new() .title({ let mut line = Line::from("Tasks"); diff --git a/yazi-fm/src/tasks/mod.rs b/yazi-fm/src/tasks/mod.rs index 712c459e..03520470 100644 --- a/yazi-fm/src/tasks/mod.rs +++ b/yazi-fm/src/tasks/mod.rs @@ -1,5 +1,3 @@ -mod clear; mod layout; -use clear::*; pub(super) use layout::*; diff --git a/yazi-fm/src/which/layout.rs b/yazi-fm/src/which/layout.rs index d4d2456b..d9f5f93f 100644 --- a/yazi-fm/src/which/layout.rs +++ b/yazi-fm/src/which/layout.rs @@ -1,8 +1,9 @@ -use ratatui::{layout, prelude::{Buffer, Constraint, Direction, Rect}, widgets::{Block, Clear, Widget}}; +use ratatui::{layout, prelude::{Buffer, Constraint, Direction, Rect}, widgets::{Block, Widget}}; use yazi_config::THEME; use yazi_core::Ctx; use super::Side; +use crate::widgets; pub(crate) struct Which<'a> { cx: &'a Ctx, @@ -38,7 +39,7 @@ impl Widget for Which<'_> { .constraints([Constraint::Ratio(1, 3), Constraint::Ratio(1, 3), Constraint::Ratio(1, 3)]) .split(area); - Clear.render(area, buf); + widgets::Clear.render(area, buf); Block::new().style(THEME.which.mask.into()).render(area, buf); Side::new(which.times, cands.0).render(chunks[0], buf); Side::new(which.times, cands.1).render(chunks[1], buf); diff --git a/yazi-fm/src/widgets/clear.rs b/yazi-fm/src/widgets/clear.rs new file mode 100644 index 00000000..a7a8e947 --- /dev/null +++ b/yazi-fm/src/widgets/clear.rs @@ -0,0 +1,43 @@ +use std::sync::atomic::Ordering; + +use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget}; +use yazi_adaptor::ADAPTOR; +use yazi_config::MANAGER; +use yazi_core::preview::COLLISION; + +pub(crate) struct Clear; + +#[inline] +const fn is_overlapping(a: &Rect, b: &Rect) -> bool { + a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y +} + +fn overlap(a: &Rect, b: &Rect) -> Option { + if !is_overlapping(a, b) { + return None; + } + + let x = a.x.max(b.x); + let y = a.y.max(b.y); + let width = (a.x + a.width).min(b.x + b.width) - x; + let height = (a.y + a.height).min(b.y + b.height) - y; + Some(Rect { x, y, width, height }) +} + +impl Widget for Clear { + fn render(self, area: Rect, buf: &mut Buffer) { + ratatui::widgets::Clear.render(area, buf); + + let Some(r) = overlap(&area, &MANAGER.layout.image_rect()) else { + return; + }; + + ADAPTOR.image_hide(r).ok(); + COLLISION.store(true, Ordering::Relaxed); + for x in r.left()..r.right() { + for y in r.top()..r.bottom() { + buf.get_mut(x, y).set_skip(true); + } + } + } +} diff --git a/yazi-fm/src/widgets/mod.rs b/yazi-fm/src/widgets/mod.rs new file mode 100644 index 00000000..cd6c2c51 --- /dev/null +++ b/yazi-fm/src/widgets/mod.rs @@ -0,0 +1,3 @@ +mod clear; + +pub(super) use clear::*; diff --git a/yazi-plugin/src/components/preview.rs b/yazi-plugin/src/components/preview.rs index 546f911b..440e97f0 100644 --- a/yazi-plugin/src/components/preview.rs +++ b/yazi-plugin/src/components/preview.rs @@ -14,17 +14,11 @@ impl<'a> Preview<'a> { impl<'a> Widget for Preview<'a> { fn render(self, area: Rect, buf: &mut Buffer) { - let manager = &self.cx.manager; - let Some(hovered) = manager.hovered().map(|h| &h.url) else { + let Some(ref lock) = self.cx.manager.active().preview.lock else { return; }; - let preview = &manager.active().preview; - if !preview.same_path(hovered) { - return; - } - - match &preview.lock.as_ref().unwrap().data { + match &lock.data { PreviewData::Folder => { Folder::preview(self.cx).render(area, buf); } diff --git a/yazi-shared/src/cha.rs b/yazi-shared/src/cha.rs index e15e6469..0f5185f0 100644 --- a/yazi-shared/src/cha.rs +++ b/yazi-shared/src/cha.rs @@ -3,7 +3,7 @@ use std::{fs::Metadata, time::SystemTime}; use bitflags::bitflags; bitflags! { - #[derive(Clone, Copy, Debug, Default)] + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct ChaMeta: u8 { const DIR = 0b00000001;