feat: support for image preview within tmux (#147)

This commit is contained in:
三咲雅 · Misaki Masa 2023-09-13 22:44:27 +08:00 committed by GitHub
parent e6c236c179
commit e6fccf9d17
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 167 additions and 116 deletions

View file

@ -179,6 +179,8 @@ That's relying on the `$TERM`, `$TERM_PROGRAM`, and `$XDG_SESSION_TYPE` variable
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.
Here is a guide for tmux users: [Image preview within tmux](https://github.com/sxyazi/yazi/wiki/Image-preview-within-tmux)
## TODO
See [Feature requests](https://github.com/sxyazi/yazi/issues/51) for more details.

View file

@ -1,54 +1,143 @@
use std::{path::{Path, PathBuf}, sync::atomic::{AtomicBool, Ordering}};
use std::{env, path::{Path, PathBuf}, sync::atomic::{AtomicBool, Ordering}};
use anyhow::Result;
use config::{preview::PreviewAdaptor, PREVIEW};
use anyhow::{anyhow, Result};
use config::PREVIEW;
use ratatui::prelude::Rect;
use shared::RoCell;
use tokio::{fs, sync::mpsc::UnboundedSender};
use super::{Iterm2, Kitty};
use crate::Sixel;
use crate::{ueberzug::Ueberzug, Sixel, TMUX};
static IMAGE_SHOWN: AtomicBool = AtomicBool::new(false);
#[allow(clippy::type_complexity)]
pub(super) static UEBERZUG: RoCell<Option<UnboundedSender<Option<(PathBuf, Rect)>>>> =
RoCell::new();
static UEBERZUG: RoCell<Option<UnboundedSender<Option<(PathBuf, Rect)>>>> = RoCell::new();
pub struct Adaptor;
#[derive(Clone, Copy)]
pub enum Adaptor {
Kitty,
Iterm2,
Sixel,
// Supported by Überzug++
X11,
Wayland,
Chafa,
}
impl Adaptor {
pub async fn image_show(mut path: &Path, rect: Rect) -> Result<()> {
pub(super) fn detect() -> Self {
let vars = [
("KITTY_WINDOW_ID", Self::Kitty),
("KONSOLE_VERSION", Self::Kitty),
("ITERM_SESSION_ID", Self::Iterm2),
("WEZTERM_EXECUTABLE", cfg!(windows).then_some(Self::Iterm2).unwrap_or(Self::Kitty)),
("VSCODE_INJECTION", Self::Sixel),
];
if let Some(var) = vars.iter().find(|v| env::var_os(v.0).is_some()) {
return var.1;
}
let (term, program) = Self::term_program();
match program.as_str() {
"iTerm.app" => return Self::Iterm2,
"WezTerm" => return cfg!(windows).then_some(Self::Iterm2).unwrap_or(Self::Kitty),
"BlackBox" => return Self::Sixel,
"vscode" => return Self::Sixel,
"Hyper" => return Self::Sixel,
"mintty" => return Self::Iterm2,
_ => {}
}
match term.as_str() {
"xterm-kitty" => return Self::Kitty,
"foot" => return Self::Sixel,
_ => {}
}
match env::var("XDG_SESSION_TYPE").unwrap_or_default().as_str() {
"x11" => Self::X11,
"wayland" => Self::Wayland,
_ => Self::Chafa,
}
}
pub(super) fn term_program() -> (String, String) {
fn tmux_env(name: &str) -> Result<String> {
let output = std::process::Command::new("tmux").args(["show-environment", name]).output()?;
String::from_utf8(output.stdout)?
.trim()
.strip_prefix(&format!("{}=", name))
.map_or_else(|| Err(anyhow!("")), |s| Ok(s.to_string()))
}
let mut term = env::var("TERM").unwrap_or_default();
let mut program = env::var("TERM_PROGRAM").unwrap_or_default();
if *TMUX {
term = tmux_env("TERM").unwrap_or(term);
program = tmux_env("TERM_PROGRAM").unwrap_or(program);
}
(term, program)
}
}
impl ToString for Adaptor {
fn to_string(&self) -> String {
match self {
Self::Kitty => "kitty",
Self::Iterm2 => "iterm2",
Self::Sixel => "sixel",
Self::X11 => "x11",
Self::Wayland => "wayland",
Self::Chafa => "chafa",
}
.to_string()
}
}
impl Adaptor {
pub(super) fn start(self) {
UEBERZUG.init(if self.needs_ueberzug() { Ueberzug::start(self).ok() } else { None });
}
pub async fn image_show(self, mut path: &Path, rect: Rect) -> Result<()> {
let cache = PREVIEW.cache(path, 0);
if fs::metadata(&cache).await.is_ok() {
path = cache.as_path();
}
Self::image_hide(rect).ok();
self.image_hide(rect).ok();
IMAGE_SHOWN.store(true, Ordering::Relaxed);
match PREVIEW.adaptor {
PreviewAdaptor::Kitty => Kitty::image_show(path, rect).await,
PreviewAdaptor::Iterm2 => Iterm2::image_show(path, rect).await,
PreviewAdaptor::Sixel => Sixel::image_show(path, rect).await,
match self {
Self::Kitty => Kitty::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 {
tx.send(Some((path.to_path_buf(), rect)))?;
}),
}
}
pub fn image_hide(rect: Rect) -> Result<()> {
pub fn image_hide(self, rect: Rect) -> Result<()> {
if !IMAGE_SHOWN.swap(false, Ordering::Relaxed) {
return Ok(());
}
match PREVIEW.adaptor {
PreviewAdaptor::Kitty => Kitty::image_hide(),
PreviewAdaptor::Iterm2 => Iterm2::image_hide(rect),
PreviewAdaptor::Sixel => Sixel::image_hide(rect),
match self {
Self::Kitty => Kitty::image_hide(),
Self::Iterm2 => Iterm2::image_hide(rect),
Self::Sixel => Sixel::image_hide(rect),
_ => Ok(if let Some(tx) = &*UEBERZUG {
tx.send(None)?;
}),
}
}
#[inline]
pub(super) fn needs_ueberzug(self) -> bool {
!matches!(self, Self::Kitty | Self::Iterm2 | Self::Sixel)
}
}

View file

@ -7,6 +7,7 @@ use ratatui::prelude::Rect;
use shared::Term;
use super::image::Image;
use crate::{CLOSE, START};
pub(super) struct Iterm2;
@ -40,11 +41,13 @@ impl Iterm2 {
let mut buf = vec![];
write!(
buf,
"\x1b]1337;File=inline=1;size={};width={}px;height={}px;doNotMoveCursor=1:{}\x07",
"{}]1337;File=inline=1;size={};width={}px;height={}px;doNotMoveCursor=1:{}\x07{}",
START,
jpg.len(),
size.0,
size.1,
general_purpose::STANDARD.encode(&jpg)
general_purpose::STANDARD.encode(&jpg),
CLOSE
)?;
Ok(buf)
})

View file

@ -7,6 +7,7 @@ use ratatui::prelude::Rect;
use shared::Term;
use super::image::Image;
use crate::{CLOSE, ESCAPE, START};
pub(super) struct Kitty;
@ -22,7 +23,7 @@ impl Kitty {
#[inline]
pub(super) fn image_hide() -> Result<()> {
let mut stdout = stdout().lock();
stdout.write_all(b"\x1b_Ga=d,d=A\x1b\\")?;
stdout.write_all(format!("{}_Ga=d,d=A{}\\{}", START, ESCAPE, CLOSE).as_bytes())?;
stdout.flush()?;
Ok(())
}
@ -36,23 +37,31 @@ impl Kitty {
if let Some(first) = it.next() {
write!(
buf,
"\x1b_Ga=T,f={},s={},v={},m={};{}\x1b\\",
"{}_Ga=T,f={},s={},v={},m={};{}{}\\{}",
START,
format,
size.0,
size.1,
it.peek().is_some() as u8,
first.iter().collect::<String>(),
ESCAPE,
CLOSE
)?;
}
while let Some(chunk) = it.next() {
write!(
buf,
"\x1b_Gm={};{}\x1b\\",
"{}_Gm={};{}{}\\{}",
START,
it.peek().is_some() as u8,
chunk.iter().collect::<String>()
chunk.iter().collect::<String>(),
ESCAPE,
CLOSE
)?;
}
buf.write_all(CLOSE.as_bytes())?;
Ok(buf)
}

View file

@ -7,17 +7,28 @@ mod kitty;
mod sixel;
mod ueberzug;
use adaptor::*;
use iterm2::*;
use kitty::*;
use shared::RoCell;
use sixel::*;
use ueberzug::*;
pub use crate::{adaptor::*, image::*};
pub use crate::image::*;
pub static ADAPTOR: RoCell<Adaptor> = RoCell::new();
// Tmux support
static TMUX: RoCell<bool> = RoCell::new();
static ESCAPE: RoCell<&'static str> = RoCell::new();
static START: RoCell<&'static str> = RoCell::new();
static CLOSE: RoCell<&'static str> = RoCell::new();
pub fn init() {
UEBERZUG.init(if config::PREVIEW.adaptor.needs_ueberzug() {
Ueberzug::start().ok()
} else {
None
});
TMUX.init(std::env::var_os("TMUX").is_some());
START.init(if *TMUX { "\x1bPtmux;\x1b\x1b" } else { "\x1b" });
CLOSE.init(if *TMUX { "\x1b\\" } else { "" });
ESCAPE.init(if *TMUX { "\x1b\x1b" } else { "\x1b" });
ADAPTOR.init(Adaptor::detect());
ADAPTOR.start();
}

View file

@ -6,7 +6,7 @@ use image::DynamicImage;
use ratatui::prelude::Rect;
use shared::Term;
use crate::Image;
use crate::{Image, CLOSE, ESCAPE, START};
pub(super) struct Sixel;
@ -41,7 +41,7 @@ impl Sixel {
let nq = NeuQuant::new(10, 256 - alpha as usize, &img);
let mut buf: Vec<u8> = Vec::with_capacity(1 << 16);
write!(buf, "\x1bP0;1;8q\"1;1;{};{}", img.width(), img.height())?;
write!(buf, "{}P0;1;8q\"1;1;{};{}", START, img.width(), img.height())?;
// Palette
for (i, c) in nq.color_map_rgba().chunks(4).enumerate() {
@ -90,7 +90,7 @@ impl Sixel {
}
}
write!(buf, "\x1b\\")?;
write!(buf, "{}\\{}", ESCAPE, CLOSE)?;
Ok(buf)
})
.await?

View file

@ -1,15 +1,16 @@
use std::{path::PathBuf, process::Stdio};
use anyhow::Result;
use config::PREVIEW;
use ratatui::prelude::Rect;
use tokio::{io::AsyncWriteExt, process::{Child, Command}, sync::mpsc::{self, UnboundedSender}};
use crate::Adaptor;
pub(super) struct Ueberzug;
impl Ueberzug {
pub(super) fn start() -> Result<UnboundedSender<Option<(PathBuf, Rect)>>> {
let mut child = Self::create_demon().ok();
pub(super) fn start(adaptor: Adaptor) -> Result<UnboundedSender<Option<(PathBuf, Rect)>>> {
let mut child = Self::create_demon(adaptor).ok();
let (tx, mut rx) = mpsc::unbounded_channel();
tokio::spawn(async move {
@ -19,7 +20,7 @@ impl Ueberzug {
child = None;
}
if child.is_none() {
child = Self::create_demon().ok();
child = Self::create_demon(adaptor).ok();
}
if let Some(c) = &mut child {
Self::send_command(c, cmd).await.ok();
@ -30,10 +31,10 @@ impl Ueberzug {
Ok(tx)
}
fn create_demon() -> Result<Child> {
fn create_demon(adaptor: Adaptor) -> Result<Child> {
Ok(
Command::new("ueberzug")
.args(["layer", "-so", &PREVIEW.adaptor.to_string()])
.args(["layer", "-so", &adaptor.to_string()])
.kill_on_drop(true)
.stdin(Stdio::piped())
.stderr(Stdio::null())

View file

@ -1,64 +0,0 @@
use std::env;
#[derive(Debug, PartialEq, Eq)]
pub enum PreviewAdaptor {
Kitty,
Iterm2,
Sixel,
// Supported by Überzug++
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,
"foot" => return Self::Sixel,
_ => {}
}
match env::var("TERM_PROGRAM").unwrap_or_default().as_str() {
"iTerm.app" => return Self::Iterm2,
"WezTerm" => return cfg!(windows).then_some(Self::Iterm2).unwrap_or(Self::Kitty),
"BlackBox" => return Self::Sixel,
"vscode" => return Self::Sixel,
"Hyper" => return Self::Sixel,
"mintty" => return Self::Iterm2,
_ => {}
}
match env::var("XDG_SESSION_TYPE").unwrap_or_default().as_str() {
"x11" => Self::X11,
"wayland" => 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 {
!matches!(self, PreviewAdaptor::Kitty | PreviewAdaptor::Iterm2 | PreviewAdaptor::Sixel)
}
}

View file

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

View file

@ -4,13 +4,10 @@ use md5::{Digest, Md5};
use serde::Deserialize;
use shared::expand_path;
use super::PreviewAdaptor;
use crate::{xdg::Xdg, MERGED_YAZI};
#[derive(Debug)]
pub struct Preview {
pub adaptor: PreviewAdaptor,
pub tab_size: u32,
pub max_width: u32,
pub max_height: u32,
@ -39,8 +36,6 @@ impl Default for Preview {
preview.cache_dir.filter(|p| !p.is_empty()).map_or_else(Xdg::cache_dir, expand_path);
Preview {
adaptor: Default::default(),
tab_size: preview.tab_size,
max_width: preview.max_width,
max_height: preview.max_height,

View file

@ -1,6 +1,6 @@
use std::{sync::atomic::Ordering, time::Duration};
use adaptor::Adaptor;
use adaptor::ADAPTOR;
use config::MANAGER;
use shared::{MimeKind, PeekError, Url, MIME_DIR};
use tokio::{pin, task::JoinHandle};
@ -153,7 +153,7 @@ impl Preview {
pub fn reset<F: FnOnce(&PreviewLock) -> bool>(&mut self, f: F) -> bool {
self.handle.take().map(|h| h.abort());
INCR.fetch_add(1, Ordering::Relaxed);
Adaptor::image_hide(MANAGER.layout.preview_rect()).ok();
ADAPTOR.image_hide(MANAGER.layout.preview_rect()).ok();
let Some(ref lock) = self.lock else {
return false;

View file

@ -1,6 +1,6 @@
use std::{io::BufRead, path::Path, sync::atomic::{AtomicUsize, Ordering}};
use adaptor::Adaptor;
use adaptor::ADAPTOR;
use anyhow::anyhow;
use config::{MANAGER, PREVIEW};
use shared::{MimeKind, PeekError};
@ -46,7 +46,7 @@ impl Provider {
}
pub(super) async fn image(path: &Path) -> Result<PreviewData, PeekError> {
Adaptor::image_show(path, MANAGER.layout.preview_rect()).await?;
ADAPTOR.image_show(path, MANAGER.layout.preview_rect()).await?;
Ok(PreviewData::Image)
}

View file

@ -1,4 +1,4 @@
use std::{cell::UnsafeCell, ops::Deref};
use std::{cell::UnsafeCell, fmt::{self, Display}, ops::Deref};
// Read-only cell. It's safe to use this in a static variable, but it's not safe
// to mutate it. This is useful for storing static data that is expensive to
@ -32,3 +32,10 @@ impl<T> Deref for RoCell<T> {
fn deref(&self) -> &Self::Target { unsafe { (*self.0.get()).as_ref().unwrap() } }
}
impl<T> Display for RoCell<T>
where
T: Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.deref().fmt(f) }
}