From 0d1df6cf37ab6db8e3967bbbf4f3b90971005708 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Sun, 22 Jun 2025 17:12:08 +0200 Subject: [PATCH 1/8] fix(kitty): dont error on non-existed temporary file after print Due to kitty terminals deleting the file after fully reading it in known temporary directories. --- CHANGELOG.md | 1 + src/printer/kitty.rs | 24 ++++++++++++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78a0c84..051d5de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## next - remove `lazy_static` dependency in favor of `std::sync::LazyLock` - MSRV is now 1.80 +- Dont Error in kitty if the temporary file has been deleted by the terminal. (Now `KittySupport::Local` is possible again) ## 0.9.2 - Use iterm and sixel in more terminals diff --git a/src/printer/kitty.rs b/src/printer/kitty.rs index 886938c..4943ade 100644 --- a/src/printer/kitty.rs +++ b/src/printer/kitty.rs @@ -3,8 +3,8 @@ use crate::printer::{adjust_offset, find_best_fit, Printer}; use crate::Config; use base64::{engine::general_purpose, Engine}; use console::{Key, Term}; -use std::io::Error; use std::io::Write; +use std::io::{Error, ErrorKind}; use std::sync::LazyLock; use tempfile::NamedTempFile; @@ -68,7 +68,21 @@ fn check_kitty_support() -> KittySupport { KittySupport::None } -// Query the terminal whether it can display an image from a file +/// Close the temporary file that was created, filtering out [`NotFound`](ErrorKind::NotFound) errors. +fn close_tmp_file(temp_file: NamedTempFile) -> ViuResult { + // Explicitly clean up when finished with the file because destructor, OS and Kitty are not deterministic. + if let Err(err) = temp_file.close() { + // Proper Kitty terminals *will delete* the file after fully reading it, if it is in a known temporary directory + // so we dont want to error if the file does not exist anymore + if err.kind() != ErrorKind::NotFound { + return Err(err.into()); + } + } + + Ok(()) +} + +/// Query the terminal whether it can display an image from a file fn has_local_support() -> ViuResult { // create a temp file that will hold a 1x1 image let x = image::RgbaImage::new(1, 1); @@ -102,8 +116,7 @@ fn has_local_support() -> ViuResult { } } - // Explicitly clean up when finished with the file because destructor, OS and Kitty are not deterministic. - temp_file.close()?; + close_tmp_file(temp_file)?; // Kitty response should end with these 3 Keys if it was successful let expected = [ @@ -152,8 +165,7 @@ fn print_local( writeln!(stdout)?; stdout.flush()?; - // Explicitly clean up when finished with the file because destructor, OS and Kitty are not deterministic. - temp_file.close()?; + close_tmp_file(temp_file)?; Ok((w, h)) } From 1fd86eea9e403da7b71aed8c849d0bde3183a8d6 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Sun, 22 Jun 2025 17:01:15 +0200 Subject: [PATCH 2/8] feat(kitty): check protocol support instead of static names fixes #70 fixes #67 fixes #71 --- CHANGELOG.md | 4 ++- src/printer/kitty.rs | 74 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 051d5de..533ef4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ ## next -- remove `lazy_static` dependency in favor of `std::sync::LazyLock` +- Remove `lazy_static` dependency in favor of `std::sync::LazyLock` - MSRV is now 1.80 - Dont Error in kitty if the temporary file has been deleted by the terminal. (Now `KittySupport::Local` is possible again) +- Properly check for kitty support on terminals via protocol query, instead of static `TERM` environment variable checking. + - This for example allows new terminals like (KDE)`Konsole` ## 0.9.2 - Use iterm and sixel in more terminals diff --git a/src/printer/kitty.rs b/src/printer/kitty.rs index 4943ade..758c3b5 100644 --- a/src/printer/kitty.rs +++ b/src/printer/kitty.rs @@ -56,18 +56,78 @@ pub enum KittySupport { // Check if Kitty protocol can be used fn check_kitty_support() -> KittySupport { - if let Ok(term) = std::env::var("TERM") { - if term.contains("kitty") || term.contains("ghostty") { - if has_local_support().is_ok() { - return KittySupport::Local; - } - - return KittySupport::Remote; + // first check if kitty protocol is generally available + if supports_kitty_protocol().is_ok() { + // then test if the current terminal supports reading from a file the application writes (for example this is not possible via ssh) + if has_local_support().is_ok() { + return KittySupport::Local; } + + return KittySupport::Remote; } + KittySupport::None } +// Query the terminal whether it can display an image from a file +fn supports_kitty_protocol() -> ViuResult { + // send the query + print!( + // the following are 2 queries, the first "\x1b_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\x1b" is the *query action* to query kitty graphics support + // followed by the request for the "primary device attributes" "\x1b[c", both are separated by a "\" + // terminals that dont support kitty will only respond to the "primary device attributes" request + // whereas terminals that support kitty, will respond to both actions, specifically we are searching for "_Gi=31" + "\x1b_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\x1b\\\x1b[c", + ); + std::io::stdout().flush()?; + + // collect Kitty's response after the query + let term = Term::stdout(); + let mut response = Vec::new(); + + // determine if we had the "primary device attributes" reply, as otherwise "c" *could* be part of another query response beforehand + let mut had_pda = false; + + // assign it once instead of having to allocate a vector with static content in each loop + // this sequenece is also called "CSI ? 6" in "Terminal Response" at https://vt100.net/docs/vt510-rm/DA1.html + let pda_seq = Key::UnknownEscSeq(['[', '?', '6'].into()); + + while let Ok(key) = term.read_key() { + if key == pda_seq { + had_pda = true; + } + + // The "primary device attributes" response will end with a "c" character + // see "Terminal Response" at https://vt100.net/docs/vt510-rm/DA1.html + // Alternatively, terminate on unknown keys, this could for example happen in cargo test with a `console::Term` read_key, for some reason + let should_break = (had_pda && key == Key::Char('c')) || key == Key::Unknown; + + response.push(key); + + if should_break { + break; + } + } + + // The Graphics query response + let expected = [ + Key::UnknownEscSeq(['_'].into()), + Key::Char('G'), + Key::Char('i'), + Key::Char('='), + Key::Char('3'), + Key::Char('1'), + ]; + + // The Graphics query and the device attributes response could theoretically be in any order + // but most terminals will reply in a FIFO order + if response.len() >= expected.len() && response[..expected.len()] == expected { + return Ok(()); + } + + Err(ViuError::KittyResponse(response)) +} + /// Close the temporary file that was created, filtering out [`NotFound`](ErrorKind::NotFound) errors. fn close_tmp_file(temp_file: NamedTempFile) -> ViuResult { // Explicitly clean up when finished with the file because destructor, OS and Kitty are not deterministic. From 2d54da7ce4ec44eaf08aacb392a953bf74a433e3 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Sun, 22 Jun 2025 18:20:46 +0200 Subject: [PATCH 3/8] feat: add ability to test stdin(response from terminal) in addition to stdout(commands to terminal) Also add a helper replay utility. --- src/lib.rs | 9 +++-- src/printer/block.rs | 3 +- src/printer/iterm.rs | 13 +++++-- src/printer/kitty.rs | 31 ++++++++++++++--- src/printer/mod.rs | 82 +++++++++++++++++++++++++++++++++++++++----- src/printer/sixel.rs | 3 +- 6 files changed, 121 insertions(+), 20 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 79de181..c12dfbd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,6 +48,7 @@ #[cfg(feature = "print-file")] use std::path::Path; +use console::Term; use crossterm::{ cursor::{RestorePosition, SavePosition}, execute, @@ -97,7 +98,9 @@ pub fn print(img: &DynamicImage, config: &Config) -> ViuResult<(u32, u32)> { execute!(&mut stdout, SavePosition)?; } - let (w, h) = choose_printer(config).print(&mut stdout, img, config)?; + let term = Term::stdout(); + + let (w, h) = choose_printer(config).print(&mut stdout, &term, img, config)?; if config.restore_cursor { execute!(&mut stdout, RestorePosition)?; @@ -128,7 +131,9 @@ pub fn print_from_file>(filename: P, config: &Config) -> ViuResul execute!(&mut stdout, SavePosition)?; } - let (w, h) = choose_printer(config).print_from_file(&mut stdout, filename, config)?; + let term = Term::stdout(); + + let (w, h) = choose_printer(config).print_from_file(&mut stdout, &term, filename, config)?; if config.restore_cursor { execute!(&mut stdout, RestorePosition)?; diff --git a/src/printer/block.rs b/src/printer/block.rs index 0783425..d2e8aa4 100644 --- a/src/printer/block.rs +++ b/src/printer/block.rs @@ -1,5 +1,5 @@ use crate::error::ViuResult; -use crate::printer::{adjust_offset, Printer}; +use crate::printer::{adjust_offset, Printer, ReadKey}; use crate::Config; use ansi_colours::ansi256_from_rgb; @@ -23,6 +23,7 @@ impl Printer for BlockPrinter { &self, // TODO: The provided object is not used because termcolor needs an implementation of the WriteColor trait _stdout: &mut impl Write, + _stdin: &impl ReadKey, img: &DynamicImage, config: &Config, ) -> ViuResult<(u32, u32)> { diff --git a/src/printer/iterm.rs b/src/printer/iterm.rs index 3dd481c..b59bafb 100644 --- a/src/printer/iterm.rs +++ b/src/printer/iterm.rs @@ -1,5 +1,5 @@ use crate::error::ViuResult; -use crate::printer::{adjust_offset, find_best_fit, Printer}; +use crate::printer::{adjust_offset, find_best_fit, Printer, ReadKey}; use crate::Config; use base64::{engine::general_purpose, Engine}; use image::{DynamicImage, GenericImageView, ImageEncoder}; @@ -26,6 +26,7 @@ impl Printer for iTermPrinter { fn print( &self, stdout: &mut impl Write, + _stdin: &impl ReadKey, img: &DynamicImage, config: &Config, ) -> ViuResult<(u32, u32)> { @@ -47,6 +48,7 @@ impl Printer for iTermPrinter { fn print_from_file>( &self, stdout: &mut impl Write, + _stdin: &impl ReadKey, filename: P, config: &Config, ) -> ViuResult<(u32, u32)> { @@ -113,6 +115,8 @@ fn check_iterm_support() -> bool { #[cfg(test)] mod tests { + use crate::printer::TestKeys; + use super::*; use image::GenericImage; @@ -128,7 +132,12 @@ mod tests { }; let mut vec = Vec::new(); - assert_eq!(iTermPrinter.print(&mut vec, &img, &config).unwrap(), (2, 2)); + let stdin = TestKeys::new(&[]); + + assert_eq!( + iTermPrinter.print(&mut vec, &stdin, &img, &config).unwrap(), + (2, 2) + ); assert_eq!(std::str::from_utf8(&vec).unwrap(), "\x1b[4;5H\x1b]1337;File=inline=1;preserveAspectRatio=1;size=95;width=2;height=2:iVBORw0KGgoAAAANSUhEUgAAAAIAAAADCAYAAAC56t6BAAAAJklEQVR4AQEbAOT/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACBAYIAEMAFdTlTsEAAAAASUVORK5CYII=\x07\n"); } } diff --git a/src/printer/kitty.rs b/src/printer/kitty.rs index 758c3b5..9a20540 100644 --- a/src/printer/kitty.rs +++ b/src/printer/kitty.rs @@ -1,5 +1,5 @@ use crate::error::{ViuError, ViuResult}; -use crate::printer::{adjust_offset, find_best_fit, Printer}; +use crate::printer::{adjust_offset, find_best_fit, Printer, ReadKey}; use crate::Config; use base64::{engine::general_purpose, Engine}; use console::{Key, Term}; @@ -22,6 +22,7 @@ impl Printer for KittyPrinter { fn print( &self, stdout: &mut impl Write, + stdin: &impl ReadKey, img: &image::DynamicImage, config: &Config, ) -> ViuResult<(u32, u32)> { @@ -29,11 +30,11 @@ impl Printer for KittyPrinter { KittySupport::None => Err(ViuError::KittyNotSupported), KittySupport::Local => { // print from file - print_local(stdout, img, config) + print_local(stdout, stdin, img, config) } KittySupport::Remote => { // print through escape codes - print_remote(stdout, img, config) + print_remote(stdout, stdin, img, config) } } } @@ -196,6 +197,7 @@ fn has_local_support() -> ViuResult { // TODO: try with kitty's supported compression fn print_local( stdout: &mut impl Write, + stdin: &impl ReadKey, img: &image::DynamicImage, config: &Config, ) -> ViuResult<(u32, u32)> { @@ -234,6 +236,7 @@ fn print_local( // TODO: try compression fn print_remote( stdout: &mut impl Write, + stdin: &impl ReadKey, img: &image::DynamicImage, config: &Config, ) -> ViuResult<(u32, u32)> { @@ -285,6 +288,8 @@ fn store_in_tmp_file(buf: &[u8]) -> std::result::Result #[cfg(test)] mod tests { + use crate::printer::TestKeys; + use super::*; use image::{DynamicImage, GenericImage}; @@ -298,11 +303,19 @@ mod tests { }; let mut vec = Vec::new(); - assert_eq!(print_local(&mut vec, &img, &config).unwrap(), (40, 13)); + + let test_data = []; + let test_response = TestKeys::new(&test_data); + + assert_eq!( + print_local(&mut vec, &test_response, &img, &config).unwrap(), + (40, 13) + ); let result = std::str::from_utf8(&vec).unwrap(); assert!(result.starts_with("\x1b[4;5H\x1b_Gf=32,s=40,v=25,c=40,r=13,a=T,t=t;")); assert!(result.ends_with("\x1b\\\n")); + assert!(test_response.reached_end()); } #[test] @@ -317,12 +330,20 @@ mod tests { }; let mut vec = Vec::new(); - assert_eq!(print_remote(&mut vec, &img, &config).unwrap(), (1, 1)); + + let test_data = []; + let test_response = TestKeys::new(&test_data); + + assert_eq!( + print_remote(&mut vec, &test_response, &img, &config).unwrap(), + (1, 1) + ); let result = std::str::from_utf8(&vec).unwrap(); assert_eq!( result, "\x1b[6;3H\x1b_Gf=32,a=T,t=d,s=1,v=2,c=1,r=1,m=1;AAAAAAIEBgg=\x1b\\\n" ); + assert!(test_response.reached_end()); } } diff --git a/src/printer/mod.rs b/src/printer/mod.rs index f63a601..990dffa 100644 --- a/src/printer/mod.rs +++ b/src/printer/mod.rs @@ -1,6 +1,7 @@ use crate::config::Config; use crate::error::{ViuError, ViuResult}; use crate::utils::terminal_size; +use console::{Key, Term}; use crossterm::cursor::{MoveRight, MoveTo, MoveToPreviousLine}; use crossterm::execute; use image::{DynamicImage, GenericImageView}; @@ -23,6 +24,65 @@ pub use self::sixel::{is_sixel_supported, SixelPrinter}; mod iterm; pub use iterm::iTermPrinter; pub use iterm::is_iterm_supported; +#[cfg(test)] +pub(crate) use test_utils::TestKeys; + +/// Trait to allow reading keys from multiple inputs like [`console::Term`] or a custom Testing utility. +pub(crate) trait ReadKey { + fn read_key(&self) -> std::io::Result; +} + +impl ReadKey for Term { + fn read_key(&self) -> std::io::Result { + self.read_key() + } +} + +#[cfg(test)] +mod test_utils { + use std::{cell::RefCell, io}; + + use console::Key; + + use crate::printer::ReadKey; + + /// Test Utility to replay key sequences like otherwise gotten from a normal console via [`console::Term`]. + #[derive(Debug, Clone)] + pub(crate) struct TestKeys<'a> { + data: &'a [Key], + next_idx: RefCell, + } + + impl<'a> TestKeys<'a> { + pub fn new(data: &'a [Key]) -> Self { + Self { + data, + next_idx: RefCell::new(0), + } + } + + /// Test if all the data in this instance has been replayed exactly. + pub fn reached_end(&self) -> bool { + *self.next_idx.borrow() == self.data.len() + } + } + + impl ReadKey for TestKeys<'_> { + fn read_key(&self) -> io::Result { + let idx = *self.next_idx.borrow(); + + if idx >= self.data.len() { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "Reached the end of the data", + )); + } + + *self.next_idx.borrow_mut() += 1; + Ok(self.data[idx].clone()) + } + } +} pub trait Printer { // Print the given image in the terminal while respecting the options in the config struct. @@ -30,6 +90,7 @@ pub trait Printer { fn print( &self, stdout: &mut impl Write, + stdin: &impl ReadKey, img: &DynamicImage, config: &Config, ) -> ViuResult<(u32, u32)>; @@ -38,13 +99,14 @@ pub trait Printer { fn print_from_file>( &self, stdout: &mut impl Write, + stdin: &impl ReadKey, filename: P, config: &Config, ) -> ViuResult<(u32, u32)> { let img = image::ImageReader::open(filename)? .with_guessed_format()? .decode()?; - self.print(stdout, &img, config) + self.print(stdout, stdin, &img, config) } } @@ -61,15 +123,16 @@ impl Printer for PrinterType { fn print( &self, stdout: &mut impl Write, + stdin: &impl ReadKey, img: &DynamicImage, config: &Config, ) -> ViuResult<(u32, u32)> { match self { - PrinterType::Block => BlockPrinter.print(stdout, img, config), - PrinterType::Kitty => KittyPrinter.print(stdout, img, config), - PrinterType::iTerm => iTermPrinter.print(stdout, img, config), + PrinterType::Block => BlockPrinter.print(stdout, stdin, img, config), + PrinterType::Kitty => KittyPrinter.print(stdout, stdin, img, config), + PrinterType::iTerm => iTermPrinter.print(stdout, stdin, img, config), #[cfg(feature = "sixel")] - PrinterType::Sixel => SixelPrinter.print(stdout, img, config), + PrinterType::Sixel => SixelPrinter.print(stdout, stdin, img, config), } } @@ -77,15 +140,16 @@ impl Printer for PrinterType { fn print_from_file>( &self, stdout: &mut impl Write, + stdin: &impl ReadKey, filename: P, config: &Config, ) -> ViuResult<(u32, u32)> { match self { - PrinterType::Block => BlockPrinter.print_from_file(stdout, filename, config), - PrinterType::Kitty => KittyPrinter.print_from_file(stdout, filename, config), - PrinterType::iTerm => iTermPrinter.print_from_file(stdout, filename, config), + PrinterType::Block => BlockPrinter.print_from_file(stdout, stdin, filename, config), + PrinterType::Kitty => KittyPrinter.print_from_file(stdout, stdin, filename, config), + PrinterType::iTerm => iTermPrinter.print_from_file(stdout, stdin, filename, config), #[cfg(feature = "sixel")] - PrinterType::Sixel => SixelPrinter.print_from_file(stdout, filename, config), + PrinterType::Sixel => SixelPrinter.print_from_file(stdout, stdin, filename, config), } } } diff --git a/src/printer/sixel.rs b/src/printer/sixel.rs index db45b81..b310b33 100644 --- a/src/printer/sixel.rs +++ b/src/printer/sixel.rs @@ -1,5 +1,5 @@ use crate::error::ViuResult; -use crate::printer::{adjust_offset, find_best_fit, Printer}; +use crate::printer::{adjust_offset, find_best_fit, Printer, ReadKey}; use crate::Config; use console::{Key, Term}; use image::{imageops::FilterType, DynamicImage, GenericImageView}; @@ -21,6 +21,7 @@ impl Printer for SixelPrinter { fn print( &self, stdout: &mut impl Write, + _stdin: &impl ReadKey, img: &DynamicImage, config: &Config, ) -> ViuResult<(u32, u32)> { From 9ad43b381401415ecba5cb5b0f6469116f76ec0d Mon Sep 17 00:00:00 2001 From: hasezoey Date: Sun, 22 Jun 2025 17:38:32 +0200 Subject: [PATCH 4/8] fix(kitty): with local display, wait for the terminal to respond with "OK" before returning as otherwise it could be a racecondition in for example in "viu", which can exit before kitty has displayed the image. --- CHANGELOG.md | 1 + src/printer/kitty.rs | 74 ++++++++++++++++++++++++++++---------------- 2 files changed, 49 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 533ef4f..c9ae379 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Dont Error in kitty if the temporary file has been deleted by the terminal. (Now `KittySupport::Local` is possible again) - Properly check for kitty support on terminals via protocol query, instead of static `TERM` environment variable checking. - This for example allows new terminals like (KDE)`Konsole` +- When using Kitty Local, wait until the terminal has responded before returning. ## 0.9.2 - Use iterm and sixel in more terminals diff --git a/src/printer/kitty.rs b/src/printer/kitty.rs index 9a20540..607cadc 100644 --- a/src/printer/kitty.rs +++ b/src/printer/kitty.rs @@ -143,6 +143,35 @@ fn close_tmp_file(temp_file: NamedTempFile) -> ViuResult { Ok(()) } +/// Wait for the common "OK" response until returning. +fn wait_for_ok(stdin: &impl ReadKey) -> ViuResult { + let mut response = Vec::new(); + + while let Ok(key) = stdin.read_key() { + // The response will end with Esc('x1b'), followed by Backslash('\'). + // Also, break if the Unknown key is found, which is returned when we're not in a tty + // https://sw.kovidgoyal.net/kitty/graphics-protocol/#display-images-on-screen + let should_break = key == Key::UnknownEscSeq(vec!['\\']) || key == Key::Unknown; + response.push(key); + if should_break { + break; + } + } + + // Kitty response should end with these 3 Keys if it was successful + let expected = [ + Key::Char('O'), + Key::Char('K'), + Key::UnknownEscSeq(vec!['\\']), + ]; + + if response.len() >= expected.len() && response[response.len() - 3..] == expected { + return Ok(()); + } + + Err(ViuError::KittyResponse(response)) +} + /// Query the terminal whether it can display an image from a file fn has_local_support() -> ViuResult { // create a temp file that will hold a 1x1 image @@ -163,34 +192,13 @@ fn has_local_support() -> ViuResult { ); std::io::stdout().flush()?; - // collect Kitty's response after the query let term = Term::stdout(); - let mut response = Vec::new(); - while let Ok(key) = term.read_key() { - // The response will end with Esc('x1b'), followed by Backslash('\'). - // Also, break if the Unknown key is found, which is returned when we're not in a tty - let should_break = key == Key::UnknownEscSeq(vec!['\\']) || key == Key::Unknown; - response.push(key); - if should_break { - break; - } - } + wait_for_ok(&term)?; close_tmp_file(temp_file)?; - // Kitty response should end with these 3 Keys if it was successful - let expected = [ - Key::Char('O'), - Key::Char('K'), - Key::UnknownEscSeq(vec!['\\']), - ]; - - if response.len() >= expected.len() && response[response.len() - 3..] == expected { - return Ok(()); - } - - Err(ViuError::KittyResponse(response)) + Ok(()) } // Print with kitty graphics protocol through a temp file @@ -212,7 +220,7 @@ fn print_local( write!( stdout, - "\x1b_Gf=32,s={},v={},c={},r={},a=T,t=t;{}\x1b\\", + "\x1b_Gf=32,s={},v={},c={},r={},a=T,i=10,t=t;{}\x1b\\", img.width(), img.height(), w, @@ -227,6 +235,8 @@ fn print_local( writeln!(stdout)?; stdout.flush()?; + wait_for_ok(stdin)?; + close_tmp_file(temp_file)?; Ok((w, h)) @@ -270,6 +280,7 @@ fn print_remote( } writeln!(stdout)?; stdout.flush()?; + Ok((w, h)) } @@ -304,7 +315,18 @@ mod tests { let mut vec = Vec::new(); - let test_data = []; + let test_data = [ + Key::UnknownEscSeq(['_'].into()), + Key::Char('G'), + Key::Char('i'), + Key::Char('='), + Key::Char('1'), + Key::Char('0'), + Key::Char(';'), + Key::Char('O'), + Key::Char('K'), + Key::UnknownEscSeq(['\\'].into()), + ]; let test_response = TestKeys::new(&test_data); assert_eq!( @@ -313,7 +335,7 @@ mod tests { ); let result = std::str::from_utf8(&vec).unwrap(); - assert!(result.starts_with("\x1b[4;5H\x1b_Gf=32,s=40,v=25,c=40,r=13,a=T,t=t;")); + assert!(result.starts_with("\x1b[4;5H\x1b_Gf=32,s=40,v=25,c=40,r=13,a=T,i=10,t=t;")); assert!(result.ends_with("\x1b\\\n")); assert!(test_response.reached_end()); } From b648a0c60f88855a436ef0676e24f47f64f166ad Mon Sep 17 00:00:00 2001 From: hasezoey Date: Sun, 22 Jun 2025 17:40:11 +0200 Subject: [PATCH 5/8] fix(kitty): with remote display, wait for the terminal to respond with "OK" before returning --- CHANGELOG.md | 2 +- src/printer/kitty.rs | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9ae379..e24193c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - Dont Error in kitty if the temporary file has been deleted by the terminal. (Now `KittySupport::Local` is possible again) - Properly check for kitty support on terminals via protocol query, instead of static `TERM` environment variable checking. - This for example allows new terminals like (KDE)`Konsole` -- When using Kitty Local, wait until the terminal has responded before returning. +- When using Kitty Local & Remote, wait until the terminal has responded before returning. ## 0.9.2 - Use iterm and sixel in more terminals diff --git a/src/printer/kitty.rs b/src/printer/kitty.rs index 607cadc..6ae76fd 100644 --- a/src/printer/kitty.rs +++ b/src/printer/kitty.rs @@ -264,7 +264,7 @@ fn print_remote( // write the first chunk, which describes the image write!( stdout, - "\x1b_Gf=32,a=T,t=d,s={},v={},c={},r={},m=1;{}\x1b\\", + "\x1b_Gf=32,a=T,t=d,s={},v={},c={},r={},i=10,m=1;{}\x1b\\", img.width(), img.height(), w, @@ -281,6 +281,8 @@ fn print_remote( writeln!(stdout)?; stdout.flush()?; + wait_for_ok(stdin)?; + Ok((w, h)) } @@ -353,7 +355,18 @@ mod tests { let mut vec = Vec::new(); - let test_data = []; + let test_data = [ + Key::UnknownEscSeq(['_'].into()), + Key::Char('G'), + Key::Char('i'), + Key::Char('='), + Key::Char('1'), + Key::Char('0'), + Key::Char(';'), + Key::Char('O'), + Key::Char('K'), + Key::UnknownEscSeq(['\\'].into()), + ]; let test_response = TestKeys::new(&test_data); assert_eq!( @@ -364,7 +377,7 @@ mod tests { assert_eq!( result, - "\x1b[6;3H\x1b_Gf=32,a=T,t=d,s=1,v=2,c=1,r=1,m=1;AAAAAAIEBgg=\x1b\\\n" + "\x1b[6;3H\x1b_Gf=32,a=T,t=d,s=1,v=2,c=1,r=1,i=10,m=1;AAAAAAIEBgg=\x1b\\\n" ); assert!(test_response.reached_end()); } From da1a12c388beb0f5b173f3388190a0a71644ac03 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Sun, 22 Jun 2025 19:04:16 +0200 Subject: [PATCH 6/8] test(kitty): add test for supported kitty remote but not local --- src/printer/kitty.rs | 100 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 86 insertions(+), 14 deletions(-) diff --git a/src/printer/kitty.rs b/src/printer/kitty.rs index 6ae76fd..6b97c87 100644 --- a/src/printer/kitty.rs +++ b/src/printer/kitty.rs @@ -57,10 +57,13 @@ pub enum KittySupport { // Check if Kitty protocol can be used fn check_kitty_support() -> KittySupport { + let mut stdout = std::io::stdout(); + let term = Term::stdout(); + // first check if kitty protocol is generally available - if supports_kitty_protocol().is_ok() { + if supports_kitty_protocol(&mut stdout, &term).is_ok() { // then test if the current terminal supports reading from a file the application writes (for example this is not possible via ssh) - if has_local_support().is_ok() { + if has_local_support(&mut stdout, &term).is_ok() { return KittySupport::Local; } @@ -71,19 +74,18 @@ fn check_kitty_support() -> KittySupport { } // Query the terminal whether it can display an image from a file -fn supports_kitty_protocol() -> ViuResult { +fn supports_kitty_protocol(stdout: &mut impl Write, stdin: &impl ReadKey) -> ViuResult { // send the query - print!( + write!( + stdout, // the following are 2 queries, the first "\x1b_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\x1b" is the *query action* to query kitty graphics support // followed by the request for the "primary device attributes" "\x1b[c", both are separated by a "\" // terminals that dont support kitty will only respond to the "primary device attributes" request // whereas terminals that support kitty, will respond to both actions, specifically we are searching for "_Gi=31" "\x1b_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\x1b\\\x1b[c", - ); + )?; std::io::stdout().flush()?; - // collect Kitty's response after the query - let term = Term::stdout(); let mut response = Vec::new(); // determine if we had the "primary device attributes" reply, as otherwise "c" *could* be part of another query response beforehand @@ -93,7 +95,7 @@ fn supports_kitty_protocol() -> ViuResult { // this sequenece is also called "CSI ? 6" in "Terminal Response" at https://vt100.net/docs/vt510-rm/DA1.html let pda_seq = Key::UnknownEscSeq(['[', '?', '6'].into()); - while let Ok(key) = term.read_key() { + while let Ok(key) = stdin.read_key() { if key == pda_seq { had_pda = true; } @@ -173,14 +175,15 @@ fn wait_for_ok(stdin: &impl ReadKey) -> ViuResult { } /// Query the terminal whether it can display an image from a file -fn has_local_support() -> ViuResult { +fn has_local_support(stdout: &mut impl Write, stdin: &impl ReadKey) -> ViuResult { // create a temp file that will hold a 1x1 image let x = image::RgbaImage::new(1, 1); let raw_img = x.as_raw(); let temp_file = store_in_tmp_file(raw_img)?; // send the query - print!( + write!( + stdout, // t=t tells Kitty it's reading from a temp file and will attempt to delete if afterwards "\x1b_Gi=31,s=1,v=1,a=q,t=t;{}\x1b\\", general_purpose::STANDARD.encode( @@ -189,12 +192,10 @@ fn has_local_support() -> ViuResult { .to_str() .ok_or_else(|| ViuError::Io(Error::other("Could not convert path to &str")))? ) - ); + )?; std::io::stdout().flush()?; - let term = Term::stdout(); - - wait_for_ok(&term)?; + wait_for_ok(stdin)?; close_tmp_file(temp_file)?; @@ -381,4 +382,75 @@ mod tests { ); assert!(test_response.reached_end()); } + + #[test] + fn test_kitty_supported_but_not_remote() { + // test kitty protocol support + let mut stdout = Vec::new(); + + let test_data = [ + Key::UnknownEscSeq(['_'].into()), + Key::Char('G'), + Key::Char('i'), + Key::Char('='), + Key::Char('3'), + Key::Char('1'), + Key::Char(';'), + Key::Char('O'), + Key::Char('K'), + Key::UnknownEscSeq(['\\'].into()), + Key::UnknownEscSeq(['[', '?', '6'].into()), + Key::Char('2'), + Key::Char(';'), + Key::Char('1'), + Key::Char(';'), + Key::Char('4'), + Key::Char('c'), + ]; + let test_response = TestKeys::new(&test_data); + + supports_kitty_protocol(&mut stdout, &test_response).unwrap(); + let result = std::str::from_utf8(&stdout).unwrap(); + + assert_eq!(result, "\x1b_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\x1b\\\x1b[c"); + assert!(test_response.reached_end()); + + stdout.clear(); + + // test kitty local protocol support + let mut stdout = Vec::new(); + + let test_data = [ + Key::UnknownEscSeq(['_'].into()), + Key::Char('G'), + Key::Char('i'), + Key::Char('='), + Key::Char('3'), + Key::Char('1'), + Key::Char(';'), + Key::Char('E'), + Key::Char('N'), + Key::Char('O'), + Key::Char('T'), + Key::Char('S'), + Key::Char('U'), + Key::Char('P'), + Key::Char('P'), + Key::Char('O'), + Key::Char('R'), + Key::Char('T'), + Key::Char('E'), + Key::Char('D'), + Key::Char(':'), + Key::UnknownEscSeq(['\\'].into()), + ]; + let test_response = TestKeys::new(&test_data); + + has_local_support(&mut stdout, &test_response).unwrap_err(); + let result = std::str::from_utf8(&stdout).unwrap(); + + assert!(result.starts_with("\x1b_Gi=31,s=1,v=1,a=q,t=t;")); + assert!(result.ends_with("\x1b\\")); + assert!(test_response.reached_end()); + } } From ffcabd53d83c911b9e70fe82749ebc37c74f1b56 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Sun, 22 Jun 2025 19:06:19 +0200 Subject: [PATCH 7/8] test(kitty): add test for no kitty support --- src/printer/kitty.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/printer/kitty.rs b/src/printer/kitty.rs index 6b97c87..6cd23be 100644 --- a/src/printer/kitty.rs +++ b/src/printer/kitty.rs @@ -453,4 +453,19 @@ mod tests { assert!(result.ends_with("\x1b\\")); assert!(test_response.reached_end()); } + + #[test] + fn test_no_kitty_support() { + let mut stdout = Vec::new(); + + // only the "primary device attributes" + let test_data = [Key::UnknownEscSeq(['[', '?', '6'].into()), Key::Char('c')]; + let test_response = TestKeys::new(&test_data); + + supports_kitty_protocol(&mut stdout, &test_response).unwrap_err(); + let result = std::str::from_utf8(&stdout).unwrap(); + + assert_eq!(result, "\x1b_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\x1b\\\x1b[c"); + assert!(test_response.reached_end()); + } } From 2227cac0520cf7691e9e8268d92a39f77539ddab Mon Sep 17 00:00:00 2001 From: hasezoey Date: Mon, 23 Jun 2025 10:33:21 +0200 Subject: [PATCH 8/8] fix: derive "Debug" on all structs and enums And "Clone" & "Copy" where it makes sense --- src/config.rs | 1 + src/printer/block.rs | 1 + src/printer/iterm.rs | 7 ++++--- src/printer/kitty.rs | 7 ++++--- src/printer/mod.rs | 1 + src/printer/sixel.rs | 1 + 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/config.rs b/src/config.rs index 0454a78..5a5c918 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,7 @@ use crate::utils; /// Configuration struct to customize printing behaviour. +#[derive(Debug, Clone)] pub struct Config { /// Enable true transparency instead of checkerboard background. /// Available only for the block printer. Defaults to false. diff --git a/src/printer/block.rs b/src/printer/block.rs index d2e8aa4..3dfb1b8 100644 --- a/src/printer/block.rs +++ b/src/printer/block.rs @@ -16,6 +16,7 @@ const LOWER_HALF_BLOCK: &str = "\u{2584}"; const CHECKERBOARD_BACKGROUND_LIGHT: (u8, u8, u8) = (153, 153, 153); const CHECKERBOARD_BACKGROUND_DARK: (u8, u8, u8) = (102, 102, 102); +#[derive(Debug, Clone)] pub struct BlockPrinter; impl Printer for BlockPrinter { diff --git a/src/printer/iterm.rs b/src/printer/iterm.rs index b59bafb..70fd10d 100644 --- a/src/printer/iterm.rs +++ b/src/printer/iterm.rs @@ -12,9 +12,6 @@ use std::{ path::Path, }; -#[allow(non_camel_case_types)] -pub struct iTermPrinter; - static ITERM_SUPPORT: LazyLock = LazyLock::new(check_iterm_support); /// Returns the terminal's support for the iTerm graphics protocol. @@ -22,6 +19,10 @@ pub fn is_iterm_supported() -> bool { *ITERM_SUPPORT } +#[allow(non_camel_case_types)] +#[derive(Debug, Clone)] +pub struct iTermPrinter; + impl Printer for iTermPrinter { fn print( &self, diff --git a/src/printer/kitty.rs b/src/printer/kitty.rs index 6cd23be..bad3e9f 100644 --- a/src/printer/kitty.rs +++ b/src/printer/kitty.rs @@ -8,8 +8,6 @@ use std::io::{Error, ErrorKind}; use std::sync::LazyLock; use tempfile::NamedTempFile; -pub struct KittyPrinter; - const TEMP_FILE_PREFIX: &str = ".tty-graphics-protocol.viuer."; static KITTY_SUPPORT: LazyLock = LazyLock::new(check_kitty_support); @@ -18,6 +16,9 @@ pub fn get_kitty_support() -> KittySupport { *KITTY_SUPPORT } +#[derive(Debug, Clone)] +pub struct KittyPrinter; + impl Printer for KittyPrinter { fn print( &self, @@ -44,7 +45,7 @@ impl Printer for KittyPrinter { // fn print_from_file(&self, filename: &str, config: &Config) -> ViuResult<(u32, u32)> {} } -#[derive(PartialEq, Eq, Copy, Clone)] +#[derive(Debug, PartialEq, Eq, Copy, Clone)] /// The extend to which the Kitty graphics protocol can be used. pub enum KittySupport { /// The Kitty graphics protocol is not supported. diff --git a/src/printer/mod.rs b/src/printer/mod.rs index 990dffa..003f62b 100644 --- a/src/printer/mod.rs +++ b/src/printer/mod.rs @@ -111,6 +111,7 @@ pub trait Printer { } #[allow(non_camel_case_types)] +#[derive(Debug, Clone, Copy)] pub enum PrinterType { Block, Kitty, diff --git a/src/printer/sixel.rs b/src/printer/sixel.rs index b310b33..3f9253d 100644 --- a/src/printer/sixel.rs +++ b/src/printer/sixel.rs @@ -8,6 +8,7 @@ use sixel_rs::optflags::EncodePolicy; use std::io::Write; use std::sync::LazyLock; +#[derive(Debug, Clone)] pub struct SixelPrinter; static SIXEL_SUPPORT: LazyLock = LazyLock::new(check_sixel_support);