From 245fb030df2031a8bc27e61fe3b3fc4f3681fe39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20=C2=B7=20Misaki=20Masa?= Date: Mon, 20 Jan 2025 18:59:00 +0800 Subject: [PATCH] feat: support local `tmux` image preview over SSH (#2229) --- Cargo.lock | 12 +++--- Cargo.toml | 2 +- yazi-adapter/src/adapter.rs | 2 +- yazi-adapter/src/emulator.rs | 55 +++++++++++++++++++++----- yazi-adapter/src/lib.rs | 8 ++-- yazi-adapter/src/mux.rs | 29 +++++++------- yazi-boot/src/actions/debug.rs | 9 +++-- yazi-fm/src/logs.rs | 10 +++-- yazi-fm/src/term.rs | 28 +++++++++++-- yazi-plugin/preset/plugins/archive.lua | 12 ++---- yazi-plugin/preset/plugins/extract.lua | 2 +- yazi-plugin/preset/plugins/font.lua | 2 +- yazi-plugin/preset/plugins/magick.lua | 27 +++++++------ yazi-plugin/preset/plugins/zoxide.lua | 4 +- 14 files changed, 133 insertions(+), 69 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c3a2668a..1983e399 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 5dce8430..619ded1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/yazi-adapter/src/adapter.rs b/yazi-adapter/src/adapter.rs index a606925c..a3537ea7 100644 --- a/yazi-adapter/src/adapter.rs +++ b/yazi-adapter/src/adapter.rs @@ -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() { diff --git a/yazi-adapter/src/emulator.rs b/yazi-adapter/src/emulator.rs index 9f535a44..b2e3e3d1 100644 --- a/yazi-adapter/src/emulator.rs +++ b/yazi-adapter/src/emulator.rs @@ -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 { defer! { disable_raw_mode().ok(); } enable_raw_mode()?; diff --git a/yazi-adapter/src/lib.rs b/yazi-adapter/src/lib.rs index 96a512d9..f05a96e6 100644 --- a/yazi-adapter/src/lib.rs +++ b/yazi-adapter/src/lib.rs @@ -13,7 +13,7 @@ pub static ADAPTOR: RoCell = RoCell::new(); static SHOWN: SyncCell> = SyncCell::new(None); // Tmux support -pub static TMUX: RoCell = RoCell::new(); +pub static TMUX: RoCell = 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()); diff --git a/yazi-adapter/src/mux.rs b/yazi-adapter/src/mux.rs index 3ebd1d51..f4493f91 100644 --- a/yazi-adapter/src/mux.rs +++ b/yazi-adapter/src/mux.rs @@ -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 { - 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, Option) { 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 { diff --git a/yazi-boot/src/actions/debug.rs b/yazi-boot/src/actions/debug.rs index 97160c14..93f64d72 100644 --- a/yazi-boot/src/actions/debug.rs +++ b/yazi-boot/src/actions/debug.rs @@ -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")?; diff --git a/yazi-fm/src/logs.rs b/yazi-fm/src/logs.rs index 7cd497a6..1ba2d68d 100644 --- a/yazi-fm/src/logs.rs +++ b/yazi-fm/src/logs.rs @@ -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 + )?) } } diff --git a/yazi-fm/src/term.rs b/yazi-fm/src/term.rs index b5050855..7eec2e5c 100644 --- a/yazi-fm/src/term.rs +++ b/yazi-fm/src/term.rs @@ -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() } + } +} diff --git a/yazi-plugin/preset/plugins/archive.lua b/yazi-plugin/preset/plugins/archive.lua index 93cd6325..c5fc0ce8 100644 --- a/yazi-plugin/preset/plugins/archive.lua +++ b/yazi-plugin/preset/plugins/archive.lua @@ -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 diff --git a/yazi-plugin/preset/plugins/extract.lua b/yazi-plugin/preset/plugins/extract.lua index b942f484..e2517078 100644 --- a/yazi-plugin/preset/plugins/extract.lua +++ b/yazi-plugin/preset/plugins/extract.lua @@ -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() diff --git a/yazi-plugin/preset/plugins/font.lua b/yazi-plugin/preset/plugins/font.lua index ca825758..935e7dbe 100644 --- a/yazi-plugin/preset/plugins/font.lua +++ b/yazi-plugin/preset/plugins/font.lua @@ -29,7 +29,7 @@ function M:preload(job) "-font", tostring(job.file.url), "-pointsize", - "64", + 64, "xc:white", "-fill", "black", diff --git a/yazi-plugin/preset/plugins/magick.lua b/yazi-plugin/preset/plugins/magick.lua index 2d25a5b4..19d7f640 100644 --- a/yazi-plugin/preset/plugins/magick.lua +++ b/yazi-plugin/preset/plugins/magick.lua @@ -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 diff --git a/yazi-plugin/preset/plugins/zoxide.lua b/yazi-plugin/preset/plugins/zoxide.lua index 2b40903b..1b49c100 100644 --- a/yazi-plugin/preset/plugins/zoxide.lua +++ b/yazi-plugin/preset/plugins/zoxide.lua @@ -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)