Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support for Überzug++ for image previews with X11/wayland environment #12

Merged
merged 6 commits into from
Jul 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -66,13 +66,32 @@ 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
- [x] Integration with fzf, zoxide for fast directory navigation
- [x] Integration with fd, rg for fuzzy file searching
- [x] Documentation of commands and options
- [ ] Support for Überzug++ for image previews with X11/wayland environment
- [x] Support for Überzug++ for image previews with X11/wayland environment
- [ ] Batch renaming support

## License
Expand Down
2 changes: 1 addition & 1 deletion cspell.json
Original file line number Diff line number Diff line change
@@ -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","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty"],"version":"0.2","language":"en"}
64 changes: 64 additions & 0 deletions src/config/preview/adaptor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use std::env;

#[derive(Debug, PartialEq, Eq)]
pub enum PreviewAdaptor {
Kitty,
Iterm2,

// Supported by Überzug++
Sixel,
X11,
Wayland,
Chafa,
}

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() {
"xterm-kitty" => return Self::Kitty,
"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,
_ => Self::Chafa,
}
}
}

impl ToString for PreviewAdaptor {
fn to_string(&self) -> String {
match self {
PreviewAdaptor::Kitty => "kitty",
PreviewAdaptor::Iterm2 => "iterm2",
PreviewAdaptor::Sixel => "sixel",
PreviewAdaptor::X11 => "x11",
PreviewAdaptor::Wayland => "wayland",
PreviewAdaptor::Chafa => "chafa",
}
.to_string()
}
}

impl PreviewAdaptor {
#[inline]
pub fn needs_ueberzug(&self) -> bool {
match self {
PreviewAdaptor::Kitty => false,
PreviewAdaptor::Iterm2 => false,
_ => true,
}
}
}
2 changes: 2 additions & 0 deletions src/config/preview/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
mod adaptor;
mod preview;

pub use adaptor::*;
pub use preview::*;
3 changes: 3 additions & 0 deletions src/config/preview/preview.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use serde::Deserialize;

use super::PreviewAdaptor;
use crate::config::MERGED_YAZI;

#[derive(Debug, Deserialize)]
pub struct Preview {
#[serde(skip)]
pub adaptor: PreviewAdaptor,
pub tab_size: u32,

pub max_width: u32,
Expand Down
49 changes: 0 additions & 49 deletions src/core/adapter/kitty.rs

This file was deleted.

3 changes: 0 additions & 3 deletions src/core/adapter/mod.rs

This file was deleted.

53 changes: 53 additions & 0 deletions src/core/adaptor/adaptor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
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};

static IMAGE_SHOWN: AtomicBool = AtomicBool::new(false);

static UEBERZUG: Lazy<Option<UnboundedSender<Option<(PathBuf, Rect)>>>> =
Lazy::new(|| if PREVIEW.adaptor.needs_ueberzug() { Ueberzug::start().ok() } else { None });

pub struct Adaptor;

impl Adaptor {
pub fn init() { Lazy::force(&UEBERZUG); }

pub async fn image_show(path: &Path, rect: Rect) -> Result<()> {
if IMAGE_SHOWN.swap(true, Ordering::Relaxed) {
Self::image_hide(rect);
}

match PREVIEW.adaptor {
PreviewAdaptor::Kitty => Kitty::image_show(path, rect).await,
PreviewAdaptor::Iterm2 => Iterm2::image_show(path, rect).await,
_ => {
if let Some(tx) = &*UEBERZUG {
tx.send(Some((path.to_path_buf(), rect))).ok();
}
Ok(())
}
}
}

pub fn image_hide(rect: Rect) {
if !IMAGE_SHOWN.swap(false, Ordering::Relaxed) {
return;
}

match PREVIEW.adaptor {
PreviewAdaptor::Kitty => Kitty::image_hide(),
PreviewAdaptor::Iterm2 => Iterm2::image_hide(rect),
_ => {
if let Some(tx) = &*UEBERZUG {
tx.send(None).ok();
}
}
}
}
}
31 changes: 31 additions & 0 deletions src/core/adaptor/image.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use std::path::Path;

use anyhow::Result;
use image::{imageops::FilterType, DynamicImage};
use tokio::fs;

use crate::{config::PREVIEW, misc::tty_ratio};

pub(super) struct Image;

impl Image {
pub(super) async fn resize(path: &Path, size: (u16, u16)) -> Result<DynamicImage> {
let (w, h) = {
let r = tty_ratio();
let (w, h) = ((size.0 as f64 * r.0) as u32, (size.1 as f64 * r.1) as u32);
(w.min(PREVIEW.max_width), h.min(PREVIEW.max_height))
};

let img = fs::read(path).await?;
let img = tokio::task::spawn_blocking(move || -> Result<DynamicImage> {
let img = image::load_from_memory(&img)?;
Ok(if img.width() > w || img.height() > h {
img.resize(w, h, FilterType::Triangle)
} else {
img
})
});

Ok(img.await??)
}
}
53 changes: 53 additions & 0 deletions src/core/adaptor/iterm2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use std::{io::Write, path::Path};

use anyhow::Result;
use base64::{engine::general_purpose, Engine};
use image::{codecs::jpeg::JpegEncoder, DynamicImage};
use ratatui::prelude::Rect;
use tokio::io::AsyncWriteExt;

use super::image::Image;
use crate::ui::Term;

pub(super) struct Iterm2;

impl Iterm2 {
pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<()> {
let img = Image::resize(path, (rect.width, rect.height)).await?;
let b = Self::encode(img).await?;

Term::move_to(rect.x, rect.y).ok();
tokio::io::stdout().write_all(&b).await.ok();
Ok(())
}

#[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:{}\x07",
jpg.len(),
size.0,
size.1,
general_purpose::STANDARD.encode(&jpg)
)?;
Ok(buf)
})
.await?
}
}
Loading