mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
..
This commit is contained in:
parent
d8725b3d7c
commit
151f91f58e
11 changed files with 205 additions and 56 deletions
|
|
@ -4,6 +4,7 @@ sort_reverse = true
|
|||
show_hidden = false
|
||||
|
||||
[preview]
|
||||
adapter = "kitty"
|
||||
tab_size = 2
|
||||
max_width = 600
|
||||
max_height = 900
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"language":"en","flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent"],"version":"0.2"}
|
||||
{"flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug"],"version":"0.2","language":"en"}
|
||||
|
|
|
|||
21
src/config/preview/adapter.rs
Normal file
21
src/config/preview/adapter.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
use anyhow::bail;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq, Eq)]
|
||||
#[serde(try_from = "String")]
|
||||
pub enum PreviewAdapter {
|
||||
Kitty,
|
||||
Ueberzug,
|
||||
}
|
||||
|
||||
impl TryFrom<String> for PreviewAdapter {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
Ok(match value.to_lowercase().as_str() {
|
||||
"kitty" => Self::Kitty,
|
||||
"ueberzug" => Self::Ueberzug,
|
||||
_ => bail!("invalid preview adapter: {}", value),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
mod adapter;
|
||||
mod preview;
|
||||
|
||||
pub use adapter::*;
|
||||
pub use preview::*;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
use super::PreviewAdapter;
|
||||
use crate::config::MERGED_YAZI;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Preview {
|
||||
pub adapter: PreviewAdapter,
|
||||
pub tab_size: u32,
|
||||
|
||||
pub max_width: u32,
|
||||
|
|
|
|||
47
src/core/adapter/adapter.rs
Normal file
47
src/core/adapter/adapter.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
use ratatui::prelude::Rect;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use super::{kitty::Kitty, ueberzug::Ueberzug};
|
||||
use crate::config::{preview::PreviewAdapter, PREVIEW};
|
||||
|
||||
pub struct Adapter {
|
||||
ueberzug: Option<UnboundedSender<Option<(PathBuf, Rect)>>>,
|
||||
}
|
||||
|
||||
impl Adapter {
|
||||
pub fn new() -> Self {
|
||||
let mut adapter = Self { ueberzug: None };
|
||||
|
||||
if PREVIEW.adapter == PreviewAdapter::Ueberzug {
|
||||
adapter.ueberzug = Ueberzug::init().ok();
|
||||
}
|
||||
|
||||
adapter
|
||||
}
|
||||
|
||||
pub async fn image_show(&self, path: &Path, rect: Rect) -> Result<()> {
|
||||
match PREVIEW.adapter {
|
||||
PreviewAdapter::Kitty => Kitty::image_show(path, rect).await,
|
||||
PreviewAdapter::Ueberzug => {
|
||||
if let Some(tx) = &self.ueberzug {
|
||||
tx.send(Some((path.to_path_buf(), rect))).ok();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn image_hide(&self) {
|
||||
match PREVIEW.adapter {
|
||||
PreviewAdapter::Kitty => Kitty::image_hide(),
|
||||
PreviewAdapter::Ueberzug => {
|
||||
if let Some(tx) = &self.ueberzug {
|
||||
tx.send(None).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,43 @@
|
|||
use std::io::Write;
|
||||
use std::{io::Write, path::Path};
|
||||
|
||||
use anyhow::Result;
|
||||
use base64::{engine::general_purpose, Engine};
|
||||
use image::DynamicImage;
|
||||
use image::{imageops::FilterType, DynamicImage};
|
||||
use ratatui::prelude::Rect;
|
||||
use tokio::{fs, io::AsyncWriteExt};
|
||||
|
||||
use crate::{config::PREVIEW, misc::tty_ratio, ui::Term};
|
||||
|
||||
pub struct Kitty;
|
||||
|
||||
impl Kitty {
|
||||
pub fn image_show(img: DynamicImage) -> Result<Vec<u8>> {
|
||||
pub async fn image_show(path: &Path, rect: Rect) -> Result<()> {
|
||||
let (w, h) = {
|
||||
let r = tty_ratio();
|
||||
let (w, h) = ((rect.width as f64 * r.0) as u32, (rect.height as f64 * r.1) as u32);
|
||||
(w.min(PREVIEW.max_width), h.min(PREVIEW.max_height))
|
||||
};
|
||||
|
||||
let img = fs::read(path).await?;
|
||||
let b = tokio::task::spawn_blocking(move || -> Result<Vec<u8>> {
|
||||
let img = image::load_from_memory(&img)?;
|
||||
Self::encode(if img.width() > w || img.height() > h {
|
||||
img.resize(w, h, FilterType::Triangle)
|
||||
} else {
|
||||
img
|
||||
})
|
||||
})
|
||||
.await??;
|
||||
|
||||
Term::move_to(rect.x, rect.y).ok();
|
||||
tokio::io::stdout().write_all(&b).await.ok();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn image_hide() { std::io::stdout().write_all(b"\x1b_Ga=d\x1b\\").ok(); }
|
||||
|
||||
fn encode(img: DynamicImage) -> Result<Vec<u8>> {
|
||||
fn output(raw: Vec<u8>, format: u8, size: (u32, u32)) -> Result<Vec<u8>> {
|
||||
let b64 = general_purpose::STANDARD.encode(raw).chars().collect::<Vec<_>>();
|
||||
|
||||
|
|
@ -43,7 +73,4 @@ impl Kitty {
|
|||
v => output(v.to_rgb8().into_raw(), 24, size),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn image_hide() -> &'static [u8; 8] { b"\x1b_Ga=d\x1b\\" }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
mod adapter;
|
||||
mod kitty;
|
||||
mod ueberzug;
|
||||
|
||||
pub use kitty::*;
|
||||
pub use adapter::*;
|
||||
|
|
|
|||
62
src/core/adapter/ueberzug.rs
Normal file
62
src/core/adapter/ueberzug.rs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
use std::{path::PathBuf, process::Stdio};
|
||||
|
||||
use anyhow::{Ok, Result};
|
||||
use ratatui::prelude::Rect;
|
||||
use tokio::{io::AsyncWriteExt, process::{Child, Command}, select, sync::mpsc::{self, UnboundedSender}};
|
||||
|
||||
pub struct Ueberzug;
|
||||
|
||||
impl Ueberzug {
|
||||
pub fn init() -> Result<UnboundedSender<Option<(PathBuf, Rect)>>> {
|
||||
let mut child = Some(Self::create_demon()?);
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
if let Some(c) = &mut child {
|
||||
select! {
|
||||
_ = c.wait() => child = None,
|
||||
data = rx.recv() => {
|
||||
if let Some(img) = data {
|
||||
Self::send_command(c, img).await.ok();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
} else if let Some(img) = rx.recv().await {
|
||||
child = Self::create_demon().ok();
|
||||
if let Some(c) = &mut child {
|
||||
Self::send_command(c, img).await.ok();
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
fn create_demon() -> Result<Child> {
|
||||
Ok(
|
||||
Command::new("ueberzug")
|
||||
.args(["layer", "-s", "--use-escape-codes"])
|
||||
.kill_on_drop(true)
|
||||
.stdin(Stdio::piped())
|
||||
// .stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()?,
|
||||
)
|
||||
}
|
||||
|
||||
async fn send_command(child: &mut Child, img: Option<(PathBuf, Rect)>) -> Result<()> {
|
||||
let stdin = child.stdin.as_mut().unwrap();
|
||||
if let Some((path, rect)) = img {
|
||||
stdin.write_all(br#"{"action":"add","identifier":"preview","max_height":0,"max_width":0,"path":"/Users/ika/Downloads/photo_2023-06-28 07.23.33.jpeg","x":0,"y":0}\n"#).await?;
|
||||
} else {
|
||||
stdin.write_all(br#"{"action":"remove","identifier":"preview"}\n"#).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +1,22 @@
|
|||
use std::{fs::File, io::{BufRead, BufReader}, path::{Path, PathBuf}, sync::OnceLock};
|
||||
use std::{fs::File, io::{BufRead, BufReader}, path::{Path, PathBuf}, sync::{Arc, OnceLock}};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use image::imageops::FilterType;
|
||||
use ratatui::prelude::Rect;
|
||||
use syntect::{easy::HighlightFile, highlighting::{Theme, ThemeSet}, parsing::SyntaxSet, util::as_24_bit_terminal_escaped};
|
||||
use tokio::{fs, task::JoinHandle};
|
||||
|
||||
use super::{ALL_RATIO, PREVIEW_BORDER, PREVIEW_PADDING, PREVIEW_RATIO};
|
||||
use crate::{config::{PREVIEW, THEME}, core::{adapter::Kitty, external, files::{Files, FilesOp}, tasks::Precache}, emit, misc::{tty_ratio, tty_size, MimeKind}};
|
||||
use crate::{config::{PREVIEW, THEME}, core::{adapter::Adapter, external, files::{Files, FilesOp}, tasks::Precache}, emit, misc::{tty_size, MimeKind}};
|
||||
|
||||
static SYNTECT_SYNTAX: OnceLock<SyntaxSet> = OnceLock::new();
|
||||
static SYNTECT_THEME: OnceLock<Theme> = OnceLock::new();
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Preview {
|
||||
pub path: PathBuf,
|
||||
pub data: PreviewData,
|
||||
|
||||
handle: Option<JoinHandle<()>>,
|
||||
adaptor: Arc<Adapter>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
|
@ -24,12 +25,16 @@ pub enum PreviewData {
|
|||
None,
|
||||
Folder,
|
||||
Text(String),
|
||||
Image(Vec<u8>),
|
||||
}
|
||||
|
||||
impl Preview {
|
||||
pub fn new() -> Self {
|
||||
Self { path: Default::default(), data: Default::default(), handle: Default::default() }
|
||||
Self {
|
||||
path: Default::default(),
|
||||
data: Default::default(),
|
||||
handle: Default::default(),
|
||||
adaptor: Arc::new(Adapter::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn size() -> (u16, u16) {
|
||||
|
|
@ -43,14 +48,15 @@ impl Preview {
|
|||
handle.abort();
|
||||
}
|
||||
|
||||
let adaptor = self.adaptor.clone();
|
||||
let (path, mime) = (path.to_path_buf(), mime.to_owned());
|
||||
self.handle = Some(tokio::spawn(async move {
|
||||
let result = match MimeKind::new(&mime) {
|
||||
MimeKind::Dir => Self::folder(&path).await,
|
||||
MimeKind::JSON => Self::json(&path).await.map(PreviewData::Text),
|
||||
MimeKind::Text => Self::highlight(&path).await.map(PreviewData::Text),
|
||||
MimeKind::Image => Self::image(&path).await.map(PreviewData::Image),
|
||||
MimeKind::Video => Self::video(&path).await.map(PreviewData::Image),
|
||||
MimeKind::Image => Self::image(adaptor, &path).await,
|
||||
MimeKind::Video => Self::video(adaptor, &path).await,
|
||||
MimeKind::Archive => Self::archive(&path).await.map(PreviewData::Text),
|
||||
MimeKind::Others => Err(anyhow!("Unsupported mimetype: {}", mime)),
|
||||
};
|
||||
|
|
@ -78,38 +84,29 @@ impl Preview {
|
|||
Ok(PreviewData::Folder)
|
||||
}
|
||||
|
||||
pub async fn image(mut path: &Path) -> Result<Vec<u8>> {
|
||||
pub async fn image(adaptor: Arc<Adapter>, mut path: &Path) -> Result<PreviewData> {
|
||||
let cache = Precache::cache(path);
|
||||
if fs::metadata(&cache).await.is_ok() {
|
||||
path = cache.as_path();
|
||||
}
|
||||
|
||||
let (w, h) = {
|
||||
let r = tty_ratio();
|
||||
let (w, h) = Self::size();
|
||||
let (w, h) = ((w as f64 * r.0) as u32, (h as f64 * r.1) as u32);
|
||||
(w.min(PREVIEW.max_width), h.min(PREVIEW.max_height))
|
||||
};
|
||||
|
||||
let img = fs::read(path).await?;
|
||||
tokio::task::spawn_blocking(move || -> Result<Vec<u8>> {
|
||||
let img = image::load_from_memory(&img)?;
|
||||
Kitty::image_show(if img.width() > w || img.height() > h {
|
||||
img.resize(w, h, FilterType::Triangle)
|
||||
} else {
|
||||
img
|
||||
})
|
||||
})
|
||||
.await?
|
||||
// TODO: image
|
||||
adaptor.image_show(path, Rect {
|
||||
x: todo!(),
|
||||
y: todo!(),
|
||||
width: todo!(),
|
||||
height: todo!(),
|
||||
});
|
||||
Ok(PreviewData::None)
|
||||
}
|
||||
|
||||
pub async fn video(path: &Path) -> Result<Vec<u8>> {
|
||||
pub async fn video(adaptor: Arc<Adapter>, path: &Path) -> Result<PreviewData> {
|
||||
let cache = Precache::cache(path);
|
||||
if fs::metadata(&cache).await.is_err() {
|
||||
external::ffmpegthumbnailer(path, &cache).await?;
|
||||
}
|
||||
|
||||
Self::image(&cache).await
|
||||
Self::image(adaptor, &cache).await
|
||||
}
|
||||
|
||||
pub async fn json(path: &Path) -> Result<String> {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
use std::io::{stdout, Write};
|
||||
|
||||
use ansi_to_tui::IntoText;
|
||||
use ratatui::{buffer::Buffer, layout::Rect, widgets::{Paragraph, Widget}};
|
||||
|
||||
use super::Folder;
|
||||
use crate::{core::{adapter::Kitty, manager::PreviewData}, ui::{Ctx, Term}};
|
||||
use crate::{core::manager::PreviewData, ui::Ctx};
|
||||
|
||||
pub struct Preview<'a> {
|
||||
cx: &'a Ctx,
|
||||
|
|
@ -16,32 +14,26 @@ impl<'a> Preview<'a> {
|
|||
|
||||
impl<'a> Widget for Preview<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
if self.cx.input.visible || self.cx.select.visible || self.cx.tasks.visible {
|
||||
stdout().write(Kitty::image_hide()).ok();
|
||||
return;
|
||||
}
|
||||
// TODO: image
|
||||
// if self.cx.input.visible || self.cx.select.visible || self.cx.tasks.visible {
|
||||
// }
|
||||
|
||||
let manager = &self.cx.manager;
|
||||
let hovered = if let Some(h) = manager.hovered() {
|
||||
h.clone()
|
||||
h.path()
|
||||
} else {
|
||||
stdout().write(Kitty::image_hide()).ok();
|
||||
return;
|
||||
};
|
||||
|
||||
let preview = manager.active().preview();
|
||||
if preview.path != hovered.path {
|
||||
if preview.path != hovered {
|
||||
return;
|
||||
}
|
||||
|
||||
if !matches!(preview.data, PreviewData::Image(_)) {
|
||||
stdout().write(Kitty::image_hide()).ok();
|
||||
}
|
||||
|
||||
match &preview.data {
|
||||
PreviewData::None => {}
|
||||
PreviewData::Folder => {
|
||||
if let Some(folder) = manager.active().history(&hovered.path) {
|
||||
if let Some(folder) = manager.active().history(&hovered) {
|
||||
Folder::new(self.cx, folder).with_preview(true).render(area, buf);
|
||||
}
|
||||
}
|
||||
|
|
@ -49,10 +41,6 @@ impl<'a> Widget for Preview<'a> {
|
|||
let p = Paragraph::new(s.as_bytes().into_text().unwrap());
|
||||
p.render(area, buf);
|
||||
}
|
||||
PreviewData::Image(b) => {
|
||||
Term::move_to(area.x, area.y).ok();
|
||||
stdout().write(b).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue