mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
..
This commit is contained in:
parent
3219297ffd
commit
00ba9bdb0e
10 changed files with 106 additions and 63 deletions
21
README.md
21
README.md
|
|
@ -36,7 +36,7 @@ brew install jq unar ffmpegthumbnailer fd ripgrep fzf zoxide
|
|||
brew tap homebrew/cask-fonts && brew install --cask font-symbols-only-nerd-font
|
||||
```
|
||||
|
||||
And download the latest release [from here](https://github.com/sxyazi/yazi/releases). Or you can install Yazi with cargo:
|
||||
And download the latest release [from here](https://github.com/sxyazi/yazi/releases). Or you can install Yazi via cargo:
|
||||
|
||||
```bash
|
||||
cargo install --git https://github.com/sxyazi/yazi.git
|
||||
|
|
@ -66,6 +66,25 @@ yazi
|
|||
|
||||
If you want to use your own config, copy the [config folder](https://github.com/sxyazi/yazi/tree/main/config) to `~/.config/yazi`, and modify it as you like.
|
||||
|
||||
## Image Preview
|
||||
|
||||
| Platform | Protocol | Support |
|
||||
| ----------- | -------------------------------------------------------------------------------- | --------------------- |
|
||||
| Kitty | [Terminal graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/) | ✅ Built-in |
|
||||
| WezTerm | [Terminal graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/) | ✅ Built-in |
|
||||
| Konsole | [Terminal graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/) | ✅ Built-in |
|
||||
| iTerm2 | [Inline Images Protocol](https://iterm2.com/documentation-images.html) | ✅ Built-in |
|
||||
| Hyper | Sixel | ☑️ Überzug++ required |
|
||||
| foot | Sixel | ☑️ Überzug++ required |
|
||||
| X11/Wayland | Window system protocol | ☑️ Überzug++ required |
|
||||
| Fallback | [Chafa](https://hpjansson.org/chafa/) | ☑️ Überzug++ required |
|
||||
|
||||
Yazi automatically selects the appropriate preview method for you, based on the priority from top to bottom.
|
||||
That's relying on the `$TERM`, `$TERM_PROGRAM`, and `$XDG_SESSION_TYPE` variables, make sure you don't overwrite them by mistake!
|
||||
|
||||
For instance, if your terminal is Alacritty, which doesn't support displaying images itself, but you are running on an X11/Wayland environment,
|
||||
it will automatically use the "Window system protocol" to display images -- this requires you to have [Überzug++](https://github.com/jstkdng/ueberzugpp) installed.
|
||||
|
||||
## TODO
|
||||
|
||||
- [x] Add example config for general usage, currently please see my [another repo](https://github.com/sxyazi/dotfiles/tree/main/yazi) instead
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"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","iterm","wezterm","sixel","chafa","ueberzugpp"],"language":"en","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","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty"],"version":"0.2","language":"en"}
|
||||
|
|
|
|||
|
|
@ -14,13 +14,22 @@ pub enum PreviewAdaptor {
|
|||
|
||||
impl Default for PreviewAdaptor {
|
||||
fn default() -> Self {
|
||||
if env::var("KITTY_WINDOW_ID").is_ok() {
|
||||
return Self::Kitty;
|
||||
}
|
||||
if env::var("KONSOLE_VERSION").is_ok() {
|
||||
return Self::Kitty;
|
||||
}
|
||||
match env::var("TERM").unwrap_or_default().as_str() {
|
||||
"wezterm" => return Self::Kitty,
|
||||
"xterm-kitty" => return Self::Kitty,
|
||||
"iterm2" => return Self::Iterm2,
|
||||
"wezterm" => return Self::Kitty,
|
||||
"foot" => return Self::Sixel,
|
||||
_ => {}
|
||||
}
|
||||
match env::var("TERM_PROGRAM").unwrap_or_default().as_str() {
|
||||
"iTerm.app" => return Self::Iterm2,
|
||||
_ => {}
|
||||
}
|
||||
match env::var("XDG_SESSION_TYPE").unwrap_or_default().as_str() {
|
||||
"x11" => return Self::X11,
|
||||
"wayland" => return Self::Wayland,
|
||||
|
|
|
|||
|
|
@ -1,33 +1,33 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::{path::{Path, PathBuf}, sync::atomic::{AtomicBool, Ordering}};
|
||||
|
||||
use anyhow::Result;
|
||||
use once_cell::sync::Lazy;
|
||||
use ratatui::prelude::Rect;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use super::{iterm2::Iterm2, kitty::Kitty, ueberzug::Ueberzug};
|
||||
use crate::config::{preview::PreviewAdaptor, PREVIEW};
|
||||
|
||||
pub struct Adaptor {
|
||||
ueberzug: Option<UnboundedSender<Option<(PathBuf, Rect)>>>,
|
||||
}
|
||||
static IMAGE_SHOWN: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
static UEBERZUG: Lazy<Option<UnboundedSender<Option<(PathBuf, Rect)>>>> =
|
||||
Lazy::new(|| if PREVIEW.adaptor.needs_ueberzug() { Ueberzug::init().ok() } else { None });
|
||||
|
||||
pub struct Adaptor;
|
||||
|
||||
impl Adaptor {
|
||||
pub fn new() -> Self {
|
||||
let mut adaptor = Self { ueberzug: None };
|
||||
pub fn init() { Lazy::force(&UEBERZUG); }
|
||||
|
||||
if PREVIEW.adaptor.needs_ueberzug() {
|
||||
adaptor.ueberzug = Ueberzug::init().ok();
|
||||
pub async fn image_show(path: &Path, rect: Rect) -> Result<()> {
|
||||
if IMAGE_SHOWN.swap(true, Ordering::Relaxed) {
|
||||
Self::image_hide(rect);
|
||||
}
|
||||
|
||||
adaptor
|
||||
}
|
||||
|
||||
pub async fn image_show(&self, path: &Path, rect: Rect) -> Result<()> {
|
||||
match PREVIEW.adaptor {
|
||||
PreviewAdaptor::Kitty => Kitty::image_show(path, rect).await,
|
||||
PreviewAdaptor::Iterm2 => Iterm2::image_show(path, rect).await,
|
||||
_ => {
|
||||
if let Some(tx) = &self.ueberzug {
|
||||
if let Some(tx) = &*UEBERZUG {
|
||||
tx.send(Some((path.to_path_buf(), rect))).ok();
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -35,12 +35,16 @@ impl Adaptor {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn image_hide(&self) {
|
||||
pub fn image_hide(rect: Rect) {
|
||||
if !IMAGE_SHOWN.swap(false, Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
match PREVIEW.adaptor {
|
||||
PreviewAdaptor::Kitty => Kitty::image_hide(),
|
||||
PreviewAdaptor::Iterm2 => {}
|
||||
PreviewAdaptor::Iterm2 => Iterm2::image_hide(rect),
|
||||
_ => {
|
||||
if let Some(tx) = &self.ueberzug {
|
||||
if let Some(tx) = &*UEBERZUG {
|
||||
tx.send(None).ok();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::{io::Write, path::Path};
|
|||
|
||||
use anyhow::Result;
|
||||
use base64::{engine::general_purpose, Engine};
|
||||
use image::DynamicImage;
|
||||
use image::{codecs::jpeg::JpegEncoder, DynamicImage};
|
||||
use ratatui::prelude::Rect;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
|
|
@ -21,29 +21,32 @@ impl Iterm2 {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn encode(img: DynamicImage) -> Result<Vec<u8>> {
|
||||
fn output(raw: Vec<u8>, size: (u32, u32)) -> Result<Vec<u8>> {
|
||||
let mut buf = Vec::with_capacity(raw.len() * 4 / 3 + 4 + 200);
|
||||
#[inline]
|
||||
pub(super) fn image_hide(rect: Rect) {
|
||||
let s = " ".repeat(rect.width as usize);
|
||||
for y in rect.top()..=rect.bottom() {
|
||||
Term::move_to(rect.x, y).ok();
|
||||
std::io::stdout().write_all(s.as_bytes()).ok();
|
||||
}
|
||||
}
|
||||
|
||||
async fn encode(img: DynamicImage) -> Result<Vec<u8>> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let size = (img.width(), img.height());
|
||||
|
||||
let mut jpg = vec![];
|
||||
JpegEncoder::new_with_quality(&mut jpg, 75).encode_image(&img)?;
|
||||
|
||||
let mut buf = vec![];
|
||||
write!(
|
||||
buf,
|
||||
"\x1b]1337;File=inline=1;size={};width={}px;height={}px:",
|
||||
raw.len(),
|
||||
"\x1b]1337;File=inline=1;size={};width={}px;height={}px:{}\x07",
|
||||
jpg.len(),
|
||||
size.0,
|
||||
size.1,
|
||||
general_purpose::STANDARD.encode(&jpg)
|
||||
)?;
|
||||
|
||||
let len = buf.len();
|
||||
let written = general_purpose::STANDARD.encode_slice(raw, &mut buf[len..])?;
|
||||
buf.truncate(len + written);
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
let size = (img.width(), img.height());
|
||||
tokio::task::spawn_blocking(move || match img {
|
||||
DynamicImage::ImageRgb8(v) => output(v.into_raw(), size),
|
||||
DynamicImage::ImageRgba8(v) => output(v.into_raw(), size),
|
||||
v => output(v.to_rgb8().into_raw(), size),
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ impl Kitty {
|
|||
pub(super) fn image_hide() { std::io::stdout().write_all(b"\x1b\\\x1b_Ga=d\x1b\\").ok(); }
|
||||
|
||||
async fn encode(img: DynamicImage) -> Result<Vec<u8>> {
|
||||
fn output(raw: Vec<u8>, format: u8, size: (u32, u32)) -> Result<Vec<u8>> {
|
||||
fn output(raw: &[u8], format: u8, size: (u32, u32)) -> Result<Vec<u8>> {
|
||||
let b64 = general_purpose::STANDARD.encode(raw).chars().collect::<Vec<_>>();
|
||||
|
||||
let mut it = b64.chunks(4096).peekable();
|
||||
|
|
@ -55,9 +55,9 @@ impl Kitty {
|
|||
|
||||
let size = (img.width(), img.height());
|
||||
tokio::task::spawn_blocking(move || match img {
|
||||
DynamicImage::ImageRgb8(v) => output(v.into_raw(), 24, size),
|
||||
DynamicImage::ImageRgba8(v) => output(v.into_raw(), 32, size),
|
||||
v => output(v.to_rgb8().into_raw(), 24, size),
|
||||
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?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ impl Ueberzug {
|
|||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
|
||||
tokio::spawn(async move {
|
||||
while let Some(img) = rx.recv().await {
|
||||
while let Some(cmd) = rx.recv().await {
|
||||
let exit = child.as_mut().and_then(|c| c.try_wait().ok());
|
||||
if exit != Some(None) {
|
||||
child = None;
|
||||
|
|
@ -23,7 +23,7 @@ impl Ueberzug {
|
|||
child = Self::create_demon().ok();
|
||||
}
|
||||
if let Some(c) = &mut child {
|
||||
Self::send_command(c, img).await.ok();
|
||||
Self::send_command(c, cmd).await.ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -42,10 +42,10 @@ impl Ueberzug {
|
|||
)
|
||||
}
|
||||
|
||||
async fn send_command(child: &mut Child, img: Option<(PathBuf, Rect)>) -> Result<()> {
|
||||
async fn send_command(child: &mut Child, cmd: Option<(PathBuf, Rect)>) -> Result<()> {
|
||||
let stdin = child.stdin.as_mut().unwrap();
|
||||
if let Some((path, rect)) = img {
|
||||
let cmd = format!(
|
||||
if let Some((path, rect)) = cmd {
|
||||
let s = format!(
|
||||
r#"{{"action":"add","identifier":"yazi","x":{},"y":{},"max_width":{},"max_height":{},"path":"{}"}}{}"#,
|
||||
rect.x,
|
||||
rect.y,
|
||||
|
|
@ -54,7 +54,7 @@ impl Ueberzug {
|
|||
path.to_string_lossy(),
|
||||
"\n"
|
||||
);
|
||||
stdin.write_all(cmd.as_bytes()).await?;
|
||||
stdin.write_all(s.as_bytes()).await?;
|
||||
} else {
|
||||
stdin
|
||||
.write_all(format!(r#"{{"action":"remove","identifier":"yazi"}}{}"#, "\n").as_bytes())
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::{fs::File, io::{BufRead, BufReader}, path::{Path, PathBuf}, sync::{Arc, OnceLock}};
|
||||
use std::{fs::File, io::{BufRead, BufReader}, path::{Path, PathBuf}, sync::OnceLock};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use ratatui::prelude::Rect;
|
||||
|
|
@ -15,8 +15,7 @@ pub struct Preview {
|
|||
pub path: PathBuf,
|
||||
pub data: PreviewData,
|
||||
|
||||
handle: Option<JoinHandle<()>>,
|
||||
adaptor: Arc<Adaptor>,
|
||||
handle: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
|
@ -33,8 +32,7 @@ impl Preview {
|
|||
path: Default::default(),
|
||||
data: Default::default(),
|
||||
|
||||
handle: Default::default(),
|
||||
adaptor: Arc::new(Adaptor::new()),
|
||||
handle: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -55,16 +53,14 @@ impl Preview {
|
|||
pub fn go(&mut self, path: &Path, mime: &str) {
|
||||
self.reset();
|
||||
|
||||
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(adaptor, &path).await,
|
||||
MimeKind::Video => Self::video(adaptor, &path).await,
|
||||
MimeKind::Image => Self::image(&path).await,
|
||||
MimeKind::Video => Self::video(&path).await,
|
||||
MimeKind::Archive => Self::archive(&path).await.map(PreviewData::Text),
|
||||
MimeKind::Others => Err(anyhow!("Unsupported mimetype: {}", mime)),
|
||||
};
|
||||
|
|
@ -75,7 +71,7 @@ impl Preview {
|
|||
|
||||
pub fn reset(&mut self) -> bool {
|
||||
self.handle.take().map(|h| h.abort());
|
||||
self.adaptor.image_hide();
|
||||
Adaptor::image_hide(Self::rect());
|
||||
|
||||
if self.path == PathBuf::default() {
|
||||
return false;
|
||||
|
|
@ -95,23 +91,23 @@ impl Preview {
|
|||
Ok(PreviewData::Folder)
|
||||
}
|
||||
|
||||
pub async fn image(adaptor: Arc<Adaptor>, mut path: &Path) -> Result<PreviewData> {
|
||||
pub async fn image(mut path: &Path) -> Result<PreviewData> {
|
||||
let cache = Precache::cache(path);
|
||||
if fs::metadata(&cache).await.is_ok() {
|
||||
path = cache.as_path();
|
||||
}
|
||||
|
||||
adaptor.image_show(path, Self::rect()).await?;
|
||||
Adaptor::image_show(path, Self::rect()).await?;
|
||||
Ok(PreviewData::None)
|
||||
}
|
||||
|
||||
pub async fn video(adaptor: Arc<Adaptor>, path: &Path) -> Result<PreviewData> {
|
||||
pub async fn video(path: &Path) -> Result<PreviewData> {
|
||||
let cache = Precache::cache(path);
|
||||
if fs::metadata(&cache).await.is_err() {
|
||||
external::ffmpegthumbnailer(path, &cache).await?;
|
||||
}
|
||||
|
||||
Self::image(adaptor, &cache).await
|
||||
Self::image(&cache).await
|
||||
}
|
||||
|
||||
pub async fn json(path: &Path) -> Result<String> {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
use core::adaptor;
|
||||
|
||||
use ui::App;
|
||||
|
||||
mod config;
|
||||
|
|
@ -11,5 +13,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
|
||||
config::init();
|
||||
|
||||
adaptor::Adaptor::init();
|
||||
|
||||
App::run().await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use ansi_to_tui::IntoText;
|
|||
use ratatui::{buffer::Buffer, layout::Rect, widgets::{Clear, Paragraph, Widget}};
|
||||
|
||||
use super::Folder;
|
||||
use crate::{core::manager::PreviewData, ui::Ctx};
|
||||
use crate::{core::manager::{PreviewData, PREVIEW_BORDER}, ui::Ctx};
|
||||
|
||||
pub struct Preview<'a> {
|
||||
cx: &'a Ctx,
|
||||
|
|
@ -14,7 +14,15 @@ impl<'a> Preview<'a> {
|
|||
|
||||
impl<'a> Widget for Preview<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
Clear.render(Rect { x: area.x, y: area.y, width: area.width + 1, height: area.height }, buf);
|
||||
Clear.render(
|
||||
Rect {
|
||||
x: area.x,
|
||||
y: area.y,
|
||||
width: area.width + PREVIEW_BORDER / 2,
|
||||
height: area.height,
|
||||
},
|
||||
buf,
|
||||
);
|
||||
|
||||
// TODO: image
|
||||
// if self.cx.input.visible || self.cx.select.visible || self.cx.tasks.visible {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue