This commit is contained in:
sxyazi 2023-07-28 11:03:15 +08:00
parent d8725b3d7c
commit 151f91f58e
No known key found for this signature in database
11 changed files with 205 additions and 56 deletions

View file

@ -4,6 +4,7 @@ sort_reverse = true
show_hidden = false
[preview]
adapter = "kitty"
tab_size = 2
max_width = 600
max_height = 900

View file

@ -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"}

View 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),
})
}
}

View file

@ -1,3 +1,5 @@
mod adapter;
mod preview;
pub use adapter::*;
pub use preview::*;

View file

@ -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,

View 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();
}
}
}
}
}

View file

@ -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\\" }
}

View file

@ -1,3 +1,5 @@
mod adapter;
mod kitty;
mod ueberzug;
pub use kitty::*;
pub use adapter::*;

View 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(())
}
}

View file

@ -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> {

View file

@ -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();
}
}
}
}