feat: support local tmux image preview over SSH (#2229)

This commit is contained in:
三咲雅 · Misaki Masa 2025-01-20 18:59:00 +08:00 committed by GitHub
parent b4ca02f703
commit 245fb030df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 133 additions and 69 deletions

12
Cargo.lock generated
View file

@ -334,9 +334,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.9"
version = "1.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b"
checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229"
dependencies = [
"jobserver",
"libc",
@ -2276,9 +2276,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.135"
version = "1.0.136"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9"
checksum = "336a0c23cf42a38d9eaa7cd22c7040d04e1228a19a933890805ffd00a16437d2"
dependencies = [
"itoa",
"memchr",
@ -2941,9 +2941,9 @@ dependencies = [
[[package]]
name = "valuable"
version = "0.1.0"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "vergen"

View file

@ -29,7 +29,7 @@ ratatui = { version = "0.29.0", features = [ "unstable-rendered-line
regex = "1.11.1"
scopeguard = "1.2.0"
serde = { version = "1.0.217", features = [ "derive" ] }
serde_json = "1.0.135"
serde_json = "1.0.136"
shell-words = "1.1.0"
tokio = { version = "1.43.0", features = [ "full" ] }
tokio-stream = "0.1.17"

View file

@ -92,7 +92,7 @@ impl Adapter {
protocols.retain(|p| *p == Self::Iip);
if env_exists("ZELLIJ_SESSION_NAME") {
protocols.retain(|p| *p == Self::Sixel);
} else if *TMUX != 0 {
} else if *TMUX {
protocols.retain(|p| *p != Self::KgpOld);
}
if let Some(p) = protocols.first() {

View file

@ -1,4 +1,4 @@
use std::{io::{LineWriter, stderr}, time::Duration};
use std::{io::{LineWriter, Read, stderr}, time::Duration};
use anyhow::{Result, bail};
use crossterm::{cursor::{RestorePosition, SavePosition}, execute, style::Print, terminal::{disable_raw_mode, enable_raw_mode}};
@ -84,7 +84,7 @@ impl Emulator {
// I really don't want to add this,
// But tmux and ConPTY sometimes cause the cursor position to get out of sync.
if *TMUX != 0 || cfg!(windows) {
if *TMUX || cfg!(windows) {
execute!(buf, SavePosition, MoveTo(x, y), Show)?;
execute!(buf, MoveTo(x, y), Show)?;
execute!(buf, MoveTo(x, y), Show)?;
@ -94,7 +94,7 @@ impl Emulator {
}
let result = cb(&mut buf);
if *TMUX != 0 || cfg!(windows) {
if *TMUX || cfg!(windows) {
queue!(buf, Hide, RestorePosition)?;
} else {
queue!(buf, RestorePosition)?;
@ -124,10 +124,16 @@ impl Emulator {
Ok(())
};
match timeout(Duration::from_secs(10), read).await {
Err(e) => error!("read_until_da1 timed out: {buf:?}, error: {e:?}"),
Ok(Err(e)) => error!("read_until_da1 failed: {buf:?}, error: {e:?}"),
match timeout(Duration::from_secs(5), read).await {
Ok(Ok(())) => debug!("read_until_da1: {buf:?}"),
Err(e) => {
error!("read_until_da1 timed out: {buf:?}, error: {e:?}");
Self::error_to_user().ok();
}
Ok(Err(e)) => {
error!("read_until_da1 failed: {buf:?}, error: {e:?}");
Self::error_to_user().ok();
}
}
String::from_utf8_lossy(&buf).into_owned()
}
@ -149,14 +155,45 @@ impl Emulator {
Ok(())
};
match timeout(Duration::from_secs(10), read).await {
Err(e) => error!("read_until_dsr timed out: {buf:?}, error: {e:?}"),
Ok(Err(e)) => error!("read_until_dsr failed: {buf:?}, error: {e:?}"),
match timeout(Duration::from_secs(5), read).await {
Ok(Ok(())) => debug!("read_until_dsr: {buf:?}"),
Err(e) => {
error!("read_until_dsr timed out: {buf:?}, error: {e:?}");
Self::error_to_user().ok();
}
Ok(Err(e)) => {
error!("read_until_dsr failed: {buf:?}, error: {e:?}");
Self::error_to_user().ok();
}
}
String::from_utf8_lossy(&buf).into_owned()
}
fn error_to_user() -> Result<()> {
use crossterm::style::{Attribute, Color, Print, ResetColor, SetAttributes, SetForegroundColor};
crossterm::execute!(
std::io::stderr(),
SetForegroundColor(Color::Red),
SetAttributes(Attribute::Bold.into()),
Print("\r\nTerminal response timeout: "),
ResetColor,
SetAttributes(Attribute::Reset.into()),
//
Print("The request sent by Yazi didn't receive a correct response.\r\n"),
Print(
"Please check your terminal environment as per: https://yazi-rs.github.io/docs/faq#trt\r\n"
),
//
SetAttributes(Attribute::Bold.into()),
SetAttributes(Attribute::Reverse.into()),
Print("Press any key to continue...\r\n"),
SetAttributes(Attribute::Reset.into()),
)?;
std::io::stdin().read_exact(&mut [0])?;
Ok(())
}
fn detect_base() -> Result<Self> {
defer! { disable_raw_mode().ok(); }
enable_raw_mode()?;

View file

@ -13,7 +13,7 @@ pub static ADAPTOR: RoCell<Adapter> = RoCell::new();
static SHOWN: SyncCell<Option<ratatui::layout::Rect>> = SyncCell::new(None);
// Tmux support
pub static TMUX: RoCell<u8> = RoCell::new();
pub 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();
@ -39,9 +39,9 @@ pub fn init() -> anyhow::Result<()> {
pub fn init_default() {
// Tmux support
TMUX.init(Mux::tmux_passthrough());
ESCAPE.init(if *TMUX == 2 { "\x1b\x1b" } else { "\x1b" });
START.init(if *TMUX == 2 { "\x1bPtmux;\x1b\x1b" } else { "\x1b" });
CLOSE.init(if *TMUX == 2 { "\x1b\\" } else { "" });
ESCAPE.init(if *TMUX { "\x1b\x1b" } else { "\x1b" });
START.init(if *TMUX { "\x1bPtmux;\x1b\x1b" } else { "\x1b" });
CLOSE.init(if *TMUX { "\x1b\\" } else { "" });
// WSL support
WSL.init(in_wsl());

View file

@ -1,6 +1,5 @@
use anyhow::Result;
use tracing::error;
use yazi_shared::env_exists;
use crate::{CLOSE, ESCAPE, Emulator, NVIM, START, TMUX};
@ -8,7 +7,7 @@ pub struct Mux;
impl Mux {
pub fn csi(s: &str) -> std::borrow::Cow<str> {
if *TMUX == 2 && !*NVIM {
if *TMUX && !*NVIM {
std::borrow::Cow::Owned(format!(
"{}{}{}",
*START,
@ -20,9 +19,11 @@ impl Mux {
}
}
pub fn tmux_passthrough() -> u8 {
if !env_exists("TMUX_PANE") || !env_exists("TMUX") {
return 0;
pub fn tmux_passthrough() -> bool {
if !std::env::var("TERM_PROGRAM").is_ok_and(|s| s == "tmux")
&& !std::env::var("TERM").is_ok_and(|s| s.starts_with("tmux"))
{
return false;
}
let child = std::process::Command::new("tmux")
@ -33,23 +34,23 @@ impl Mux {
.spawn();
match child.and_then(|c| c.wait_with_output()) {
Ok(output) if output.status.success() => return 2,
Ok(output) => {
Ok(o) if o.status.success() => {}
Ok(o) => {
error!(
"Running `tmux set -p allow-passthrough on` failed: {:?}, {}",
output.status,
String::from_utf8_lossy(&output.stderr)
o.status,
String::from_utf8_lossy(&o.stderr)
);
}
Err(err) => {
error!("Failed to spawn `tmux set -p allow-passthrough on`: {err}");
Err(e) => {
error!("Failed to spawn `tmux set -p allow-passthrough on`: {e}");
}
}
1
true
}
pub fn tmux_drain() -> Result<()> {
if *TMUX == 2 && !*NVIM {
if *TMUX && !*NVIM {
crossterm::execute!(std::io::stderr(), crossterm::style::Print(Mux::csi("\x1b[5n")))?;
_ = futures::executor::block_on(Emulator::read_until_dsr());
}
@ -73,7 +74,7 @@ impl Mux {
pub(super) fn term_program() -> (Option<String>, Option<String>) {
let (mut term, mut program) = (None, None);
if *TMUX == 0 {
if !*TMUX {
return (term, program);
}
let Ok(output) = std::process::Command::new("tmux").arg("show-environment").output() else {

View file

@ -19,6 +19,9 @@ impl Actions {
let emulator = yazi_adapter::Emulator::detect();
writeln!(s, "\nEmulator")?;
writeln!(s, " TERM : {:?}", env::var_os("TERM"))?;
writeln!(s, " TERM_PROGRAM : {:?}", env::var_os("TERM_PROGRAM"))?;
writeln!(s, " TERM_PROGRAM_VERSION: {:?}", env::var_os("TERM_PROGRAM_VERSION"))?;
writeln!(s, " Brand.from_env : {:?}", yazi_adapter::Brand::from_env())?;
writeln!(s, " Emulator.detect : {:?}", emulator)?;
writeln!(s, " Emulator.detect_full: {:?}", yazi_adapter::Emulator::detect_full())?;
@ -41,8 +44,8 @@ impl Actions {
writeln!(s, "\nWSL")?;
writeln!(s, " WSL: {:?}", *yazi_adapter::WSL)?;
writeln!(s, "\nNVIM")?;
writeln!(s, " NVIM : {:?}", *yazi_adapter::NVIM)?;
writeln!(s, "\nNeovim")?;
writeln!(s, " NVIM : {}", *yazi_adapter::NVIM)?;
writeln!(s, " Neovim version: {}", Self::process_output("nvim", "--version"))?;
writeln!(s, "\nVariables")?;
@ -93,7 +96,7 @@ impl Actions {
writeln!(s, " chafa : {}", Self::process_output("chafa", "--version"))?;
writeln!(s, " zoxide : {}", Self::process_output("zoxide", "--version"))?;
#[rustfmt::skip]
writeln!(s, " 7z/7zz : {} / {}", Self::process_output("7z", "i"), Self::process_output("7zz", "i"))?;
writeln!(s, " 7zz/7z : {} / {}", Self::process_output("7zz", "i"), Self::process_output("7z", "i"))?;
writeln!(s, " jq : {}", Self::process_output("jq", "--version"))?;
writeln!(s, "\nClipboard")?;

View file

@ -1,6 +1,7 @@
use std::fs::File;
use anyhow::Context;
use crossterm::style::{Color, Print, ResetColor, SetForegroundColor};
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::EnvFilter;
use yazi_fs::Xdg;
@ -34,8 +35,11 @@ impl Logs {
.init();
_GUARD.init(guard);
eprintln!("Running with log level `{level}`, logs are written to {log_path:?}");
Ok(())
Ok(crossterm::execute!(
std::io::stderr(),
SetForegroundColor(Color::Yellow),
Print(format!("Running with log level `{level}`, logs are written to {log_path:?}\n")),
ResetColor
)?)
}
}

View file

@ -1,7 +1,7 @@
use std::{io::{self, BufWriter, Stderr, stderr}, ops::{Deref, DerefMut}, sync::atomic::{AtomicBool, AtomicU8, Ordering}};
use anyhow::Result;
use crossterm::{event::{DisableBracketedPaste, EnableBracketedPaste, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags}, execute, queue, style::Print, terminal::{EnterAlternateScreen, LeaveAlternateScreen, SetTitle, disable_raw_mode, enable_raw_mode}};
use crossterm::{event::{DisableBracketedPaste, EnableBracketedPaste, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags}, execute, queue, style::Print, terminal::{LeaveAlternateScreen, SetTitle, disable_raw_mode, enable_raw_mode}};
use cursor::RestoreCursor;
use ratatui::{CompletedFrame, Frame, Terminal, backend::CrosstermBackend, buffer::Buffer, layout::Rect};
use yazi_adapter::{Emulator, Mux};
@ -26,17 +26,18 @@ impl Term {
};
enable_raw_mode()?;
if *yazi_adapter::TMUX != 0 {
if *yazi_adapter::TMUX {
yazi_adapter::Mux::tmux_passthrough();
}
execute!(
BufWriter::new(stderr()),
screen::SetScreen(true),
Print(Mux::csi("\x1bP$q q\x1b\\")), // Request cursor shape (DECRQM)
Print(Mux::csi("\x1b[?12$p")), // Request cursor blink status (DECSET)
Print("\x1b[?u"), // Request keyboard enhancement flags (CSI u)
Print(Mux::csi("\x1b[0c")), // Request device attributes
EnterAlternateScreen,
screen::SetScreen(false),
EnableBracketedPaste,
mouse::SetMouse(true),
)?;
@ -258,3 +259,24 @@ mod cursor {
fn execute_winapi(&self) -> std::io::Result<()> { Ok(()) }
}
}
mod screen {
use crossterm::terminal::EnterAlternateScreen;
use yazi_adapter::TMUX;
pub struct SetScreen(pub bool);
impl crossterm::Command for SetScreen {
fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
if self.0 == *TMUX { Ok(()) } else { EnterAlternateScreen.write_ansi(f) }
}
#[cfg(windows)]
fn execute_winapi(&self) -> std::io::Result<()> {
if self.0 == *TMUX { Ok(()) } else { EnterAlternateScreen.execute_winapi() }
}
#[cfg(windows)]
fn is_ansi_code_supported(&self) -> bool { EnterAlternateScreen.is_ansi_code_supported() }
}
}

View file

@ -9,7 +9,7 @@ function M:peek(job)
return require("empty").msg(
job,
code == 2 and "File list in this archive is encrypted"
or "Failed to start both `7z` and `7zz`. Do you have 7-zip installed?"
or "Failed to start both `7zz` and `7z`. Do you have 7-zip installed?"
)
end
@ -55,15 +55,9 @@ function M.spawn_7z(args)
return child
end
local child
if ya.target_os() == "macos" then
child = try("7zz") or try("7z")
else
child = try("7z") or try("7zz")
end
local child = try("7zz") or try("7z")
if not child then
return ya.err("Failed to start both `7z` and `7zz`, error: " .. last_err)
return ya.err("Failed to start both `7zz` and `7z`, error: " .. last_err)
end
return child, last_err
end

View file

@ -52,7 +52,7 @@ function M:try_with(from, pwd, to)
local archive = require("archive")
local child, err = archive.spawn_7z { "x", "-aou", "-sccUTF-8", "-p" .. pwd, "-o" .. tostring(tmp), tostring(from) }
if not child then
fail("Failed to start both `7z` and `7zz`, error: " .. err)
fail("Failed to start both `7zz` and `7z`, error: " .. err)
end
local output, err = child:wait_with_output()

View file

@ -29,7 +29,7 @@ function M:preload(job)
"-font",
tostring(job.file.url),
"-pointsize",
"64",
64,
"xc:white",
"-fill",
"black",

View file

@ -19,18 +19,21 @@ function M:preload(job)
return 1
end
local status, err = Command("magick"):args({
"-density",
"200",
tostring(job.file.url),
"-flatten",
"-resize",
string.format("%dx%d^", PREVIEW.max_width, PREVIEW.max_height),
"-quality",
PREVIEW.image_quality,
"-auto-orient",
"JPG:" .. tostring(cache),
}):status()
local status, err = Command("magick")
:args({
"-density",
200,
tostring(job.file.url),
"-flatten",
"-resize",
string.format("%dx%d^", PREVIEW.max_width, PREVIEW.max_height),
"-quality",
PREVIEW.image_quality,
"-auto-orient",
"JPG:" .. tostring(cache),
})
:env("MAGICK_THREAD_LIMIT", 1)
:status()
if status then
return status.success and 1 or 2

View file

@ -92,8 +92,8 @@ local function entry()
:args({ "query", "-i", "--exclude" })
:arg(st.cwd)
:env("SHELL", "sh")
:env("CLICOLOR", "1")
:env("CLICOLOR_FORCE", "1")
:env("CLICOLOR", 1)
:env("CLICOLOR_FORCE", 1)
:env("_ZO_FZF_OPTS", options())
:stdin(Command.INHERIT)
:stdout(Command.PIPED)