From 7463c7edba8ba2071e6971fb584cff45e7f4a114 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Sun, 22 Jun 2025 18:20:46 +0200 Subject: [PATCH 01/11] 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 | 12 ++++++-- src/printer/block.rs | 3 +- src/printer/icy_sixel.rs | 3 +- src/printer/iterm.rs | 13 +++++++-- src/printer/kitty.rs | 31 +++++++++++++++++---- src/printer/mod.rs | 31 +++++++++++++-------- src/printer/sixel.rs | 3 +- src/read_key.rs | 60 ++++++++++++++++++++++++++++++++++++++++ 8 files changed, 133 insertions(+), 23 deletions(-) create mode 100644 src/read_key.rs diff --git a/src/lib.rs b/src/lib.rs index 078da88..d2f6ff9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,6 +50,7 @@ #[cfg(feature = "print-file")] use std::path::Path; +use console::Term; use crossterm::{ cursor::{RestorePosition, SavePosition}, execute, @@ -60,6 +61,7 @@ use printer::{Printer, PrinterType}; mod config; mod error; mod printer; +mod read_key; mod utils; pub use config::Config; @@ -99,7 +101,10 @@ 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)?; + // This is required to get a "Term" instance for "::read_key" + let term = Term::stdout(); + + let (w, h) = choose_printer(config).print(&mut stdout, &term, img, config)?; if config.restore_cursor { execute!(&mut stdout, RestorePosition)?; @@ -130,7 +135,10 @@ 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)?; + // This is required to get a "Term" instance for "::read_key" + 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 d287fd4..3e25dfb 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; @@ -24,6 +24,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/icy_sixel.rs b/src/printer/icy_sixel.rs index 637ddf5..9c5e1e1 100644 --- a/src/printer/icy_sixel.rs +++ b/src/printer/icy_sixel.rs @@ -1,4 +1,4 @@ -use super::{adjust_offset, find_best_fit, Printer}; +use super::{adjust_offset, find_best_fit, Printer, ReadKey}; use icy_sixel::sixel_string; use image::{imageops::FilterType, GenericImageView}; @@ -9,6 +9,7 @@ impl Printer for IcySixelPrinter { fn print( &self, stdout: &mut impl std::io::Write, + _stdin: &impl ReadKey, img: &image::DynamicImage, config: &crate::Config, ) -> crate::ViuResult<(u32, u32)> { diff --git a/src/printer/iterm.rs b/src/printer/iterm.rs index e6840be..5b0280e 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}; @@ -27,6 +27,7 @@ impl Printer for iTermPrinter { fn print( &self, stdout: &mut impl Write, + _stdin: &impl ReadKey, img: &DynamicImage, config: &Config, ) -> ViuResult<(u32, u32)> { @@ -48,6 +49,7 @@ impl Printer for iTermPrinter { fn print_from_file>( &self, stdout: &mut impl Write, + _stdin: &impl ReadKey, filename: P, config: &Config, ) -> ViuResult<(u32, u32)> { @@ -123,6 +125,8 @@ fn check_iterm_support() -> bool { #[cfg(test)] mod tests { + use crate::printer::TestKeys; + use super::*; use image::GenericImage; @@ -138,7 +142,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 d54af5e..2456aca 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}; @@ -23,6 +23,7 @@ impl Printer for KittyPrinter { fn print( &self, stdout: &mut impl Write, + stdin: &impl ReadKey, img: &image::DynamicImage, config: &Config, ) -> ViuResult<(u32, u32)> { @@ -30,11 +31,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) } } } @@ -124,6 +125,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)> { @@ -163,6 +165,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)> { @@ -214,6 +217,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}; @@ -227,11 +232,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] @@ -246,12 +259,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 f3cc003..7d7f03b 100644 --- a/src/printer/mod.rs +++ b/src/printer/mod.rs @@ -31,6 +31,9 @@ mod sixel_util; pub use self::sixel_util::is_sixel_supported; mod iterm; +#[cfg(test)] +pub(crate) use crate::read_key::test_utils::TestKeys; +pub(crate) use crate::read_key::ReadKey; pub use iterm::iTermPrinter; pub use iterm::is_iterm_supported; @@ -40,6 +43,7 @@ pub trait Printer { fn print( &self, stdout: &mut impl Write, + stdin: &impl ReadKey, img: &DynamicImage, config: &Config, ) -> ViuResult<(u32, u32)>; @@ -48,13 +52,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) } } @@ -74,17 +79,18 @@ 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(all(feature = "sixel", not(windows)))] - PrinterType::Sixel => SixelPrinter.print(stdout, img, config), + PrinterType::Sixel => SixelPrinter.print(stdout, stdin, img, config), #[cfg(any(feature = "icy_sixel", all(feature = "sixel", windows)))] - PrinterType::IcySixel => IcySixelPrinter.print(stdout, img, config), + PrinterType::IcySixel => IcySixelPrinter.print(stdout, stdin, img, config), } } @@ -92,17 +98,20 @@ 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(all(feature = "sixel", not(windows)))] - PrinterType::Sixel => SixelPrinter.print_from_file(stdout, filename, config), + PrinterType::Sixel => SixelPrinter.print_from_file(stdout, stdin, filename, config), #[cfg(any(feature = "icy_sixel", all(feature = "sixel", windows)))] - PrinterType::IcySixel => IcySixelPrinter.print_from_file(stdout, filename, config), + PrinterType::IcySixel => { + IcySixelPrinter.print_from_file(stdout, stdin, filename, config) + } } } } diff --git a/src/printer/sixel.rs b/src/printer/sixel.rs index 4e18fdf..622d655 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 image::{imageops::FilterType, DynamicImage, GenericImageView}; use sixel_rs::encoder::{Encoder, QuickFrameBuilder}; @@ -13,6 +13,7 @@ impl Printer for SixelPrinter { fn print( &self, stdout: &mut impl Write, + _stdin: &impl ReadKey, img: &DynamicImage, config: &Config, ) -> ViuResult<(u32, u32)> { diff --git a/src/read_key.rs b/src/read_key.rs new file mode 100644 index 0000000..e2b5fc8 --- /dev/null +++ b/src/read_key.rs @@ -0,0 +1,60 @@ +use console::{Key, Term}; + +/// Trait to allow reading keys from multiple inputs like [`Term`] (via [`Term::read_key`]) 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)] +pub(crate) mod test_utils { + use std::{cell::RefCell, io}; + + use console::Key; + + use super::ReadKey; + + /// Test Utility to replay key sequences like otherwise gotten from a normal console via [`console::Term`]. + /// + /// Will returns a error if the sequence has reached the end, but a new one is requested. + #[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()) + } + } +} From c764bc8d46c5fd0d7de65f1428b29729bfe9e5a9 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Sat, 8 Nov 2025 14:23:14 +0100 Subject: [PATCH 02/11] refactor(kitty::has_local_support): take generic stdin & stdout parameters for testing use --- src/printer/kitty.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/printer/kitty.rs b/src/printer/kitty.rs index 2456aca..9c6c931 100644 --- a/src/printer/kitty.rs +++ b/src/printer/kitty.rs @@ -60,7 +60,9 @@ pub enum KittySupport { 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() { + let mut stdout = std::io::stdout(); + let stdin = Term::stdout(); + if has_local_support(&mut stdout, &stdin).is_ok() { return KittySupport::Local; } @@ -71,14 +73,15 @@ fn check_kitty_support() -> KittySupport { } // 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( @@ -87,14 +90,13 @@ fn has_local_support() -> ViuResult { .to_str() .ok_or_else(|| ViuError::Io(Error::other("Could not convert path to &str")))? ) - ); - std::io::stdout().flush()?; + )?; + 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() { + 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 let should_break = key == Key::UnknownEscSeq(vec!['\\']) || key == Key::Unknown; From b71c93084f53ca058460ddcac01fa66105159afd Mon Sep 17 00:00:00 2001 From: hasezoey Date: Sat, 8 Nov 2025 14:45:42 +0100 Subject: [PATCH 03/11] test(kitty): add test to know what keys are consumed and how the function will behave. Mostly to showcase "ReadKey" trait usage. This implementation tests against known bugs, see pr #79. --- src/printer/kitty.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/printer/kitty.rs b/src/printer/kitty.rs index 9c6c931..e830dce 100644 --- a/src/printer/kitty.rs +++ b/src/printer/kitty.rs @@ -277,4 +277,35 @@ mod tests { ); assert!(test_response.reached_end()); } + + #[test] + fn test_kitty_protocol_supported() { + // test kitty protocol support + let mut stdout = Vec::new(); + + // data returned by the terminal from the query + // Captured from kitty 0.42.2 + let test_stdin_data = [ + // the following response indicated a successful graphical request + 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()), + ]; + let test_stdin = TestKeys::new(&test_stdin_data); + + has_local_support(&mut stdout, &test_stdin).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!(result.starts_with("\x1b_Gi=31,s=1,v=1,a=q,t=t;")); + assert!(result.ends_with("\x1b\\")); + assert!(test_stdin.reached_end()); + } } From c6fb4b220434832d81afdf3cfe984b57cd78a2fd Mon Sep 17 00:00:00 2001 From: hasezoey Date: Sat, 8 Nov 2025 15:08:48 +0100 Subject: [PATCH 04/11] test(sixel): add test to know what keys are consumed --- src/printer/sixel_util.rs | 58 ++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/src/printer/sixel_util.rs b/src/printer/sixel_util.rs index f65a814..67100a3 100644 --- a/src/printer/sixel_util.rs +++ b/src/printer/sixel_util.rs @@ -1,9 +1,10 @@ +use crate::printer::ReadKey; use crate::ViuResult; use console::{Key, Term}; use std::io::Write; use std::sync::LazyLock; -static SIXEL_SUPPORT: LazyLock = LazyLock::new(|| check_device_attrs().unwrap_or(false)); +static SIXEL_SUPPORT: LazyLock = LazyLock::new(check_sixel_support); /// Returns the terminal's support for Sixel. pub fn is_sixel_supported() -> bool { @@ -13,15 +14,13 @@ pub fn is_sixel_supported() -> bool { // Check if Sixel is within the terminal's attributes // see https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Sixel-Graphics // and https://vt100.net/docs/vt510-rm/DA1.html -fn check_device_attrs() -> ViuResult { - let mut term = Term::stdout(); - - write!(&mut term, "\x1b[c")?; - term.flush()?; +fn check_device_attrs(stdout: &mut impl Write, stdin: &impl ReadKey) -> ViuResult { + write!(stdout, "\x1b[c")?; + stdout.flush()?; let mut response = String::new(); - while let Ok(key) = term.read_key() { + while let Ok(key) = stdin.read_key() { // exit on first "Unknown" key as we know that this is not a proper response anymore if key == Key::Unknown { break; @@ -37,3 +36,48 @@ fn check_device_attrs() -> ViuResult { Ok(response.contains(";4;") || response.contains(";4c")) } + +// Check if Sixel protocol can be used +fn check_sixel_support() -> bool { + let mut stdout = std::io::stdout(); + let stdin = Term::stdout(); + check_device_attrs(&mut stdout, &stdin).unwrap_or(false) +} + +#[cfg(test)] +mod tests { + use console::Key; + + use crate::printer::{sixel_util::check_device_attrs, TestKeys}; + + #[test] + fn should_detect_device_attrs() { + // test kitty protocol support + let mut stdout = Vec::new(); + + // data returned by the terminal from the query + // Captured from Konsole 25.08.1 + let test_stdin_data = [ + // CSI ? DEVCLASS + Key::UnknownEscSeq(['[', '?', '6'].into()), + // DEVCLASS-2 + // in this case this corresponds to "?62;", or "VT220" + Key::Char('2'), + Key::Char(';'), + // 132 columns + Key::Char('1'), + Key::Char(';'), + // sixel support + Key::Char('4'), + // response end + Key::Char('c'), + ]; + let test_stdin = TestKeys::new(&test_stdin_data); + + check_device_attrs(&mut stdout, &test_stdin).unwrap(); + let result = std::str::from_utf8(&stdout).unwrap(); + + assert_eq!(result, "\x1b[c"); + assert!(test_stdin.reached_end()); + } +} From f3d1e21d07a317d2b7241048a4f8f60e419353c8 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Mon, 10 Nov 2025 13:28:19 +0100 Subject: [PATCH 05/11] style(printer): use proper doc-comments --- src/printer/iterm.rs | 6 +++--- src/printer/kitty.rs | 12 ++++++------ src/printer/sixel_util.rs | 9 +++++---- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/printer/iterm.rs b/src/printer/iterm.rs index 5b0280e..c322f5d 100644 --- a/src/printer/iterm.rs +++ b/src/printer/iterm.rs @@ -65,8 +65,8 @@ impl Printer for iTermPrinter { } } -// This function requires both a DynamicImage, which is used to calculate dimensions, -// and it's raw representation as a file, because that's the data iTerm needs to display it. +/// This function requires both a DynamicImage, which is used to calculate dimensions, +/// and it's raw representation as a file, because that's the data iTerm needs to display it. fn print_buffer( stdout: &mut impl Write, img: &DynamicImage, @@ -90,7 +90,7 @@ fn print_buffer( Ok((w, h)) } -// Check if the iTerm protocol can be used +/// Check if the iTerm protocol can be used fn check_iterm_support() -> bool { if let Ok(term) = std::env::var("TERM_PROGRAM") { if term.contains("iTerm") diff --git a/src/printer/kitty.rs b/src/printer/kitty.rs index e830dce..6f953fa 100644 --- a/src/printer/kitty.rs +++ b/src/printer/kitty.rs @@ -56,7 +56,7 @@ pub enum KittySupport { Remote, } -// Check if Kitty protocol can be used +/// 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") { @@ -72,7 +72,7 @@ fn check_kitty_support() -> KittySupport { KittySupport::None } -// Query the terminal whether it can display an image from a file +/// Query the terminal whether it can display an image from a file 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); @@ -123,7 +123,7 @@ fn has_local_support(stdout: &mut impl Write, stdin: &impl ReadKey) -> ViuResult Err(ViuError::KittyResponse(response)) } -// Print with kitty graphics protocol through a temp file +/// Print with kitty graphics protocol through a temp file // TODO: try with kitty's supported compression fn print_local( stdout: &mut impl Write, @@ -163,7 +163,7 @@ fn print_local( Ok((w, h)) } -// Print with escape codes +/// Print with escape codes // TODO: try compression fn print_remote( stdout: &mut impl Write, @@ -204,8 +204,8 @@ fn print_remote( Ok((w, h)) } -// Create a file in temporary dir and write the byte slice to it. -// The NamedTempFile will be deleted once it goes out of scope. +/// Create a file in temporary dir and write the byte slice to it. +/// The NamedTempFile will be deleted once it goes out of scope. fn store_in_tmp_file(buf: &[u8]) -> std::result::Result { let mut tmpfile = tempfile::Builder::new() .prefix(TEMP_FILE_PREFIX) diff --git a/src/printer/sixel_util.rs b/src/printer/sixel_util.rs index 67100a3..75b5d48 100644 --- a/src/printer/sixel_util.rs +++ b/src/printer/sixel_util.rs @@ -11,9 +11,10 @@ pub fn is_sixel_supported() -> bool { *SIXEL_SUPPORT } -// Check if Sixel is within the terminal's attributes -// see https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Sixel-Graphics -// and https://vt100.net/docs/vt510-rm/DA1.html +/// Check if Sixel is within the terminal's attributes. +/// +/// see https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Sixel-Graphics +/// and https://vt100.net/docs/vt510-rm/DA1.html fn check_device_attrs(stdout: &mut impl Write, stdin: &impl ReadKey) -> ViuResult { write!(stdout, "\x1b[c")?; stdout.flush()?; @@ -37,7 +38,7 @@ fn check_device_attrs(stdout: &mut impl Write, stdin: &impl ReadKey) -> ViuResul Ok(response.contains(";4;") || response.contains(";4c")) } -// Check if Sixel protocol can be used +/// Check if Sixel protocol can be used fn check_sixel_support() -> bool { let mut stdout = std::io::stdout(); let stdin = Term::stdout(); From 2343ad3b14960d5cd0410a4c6ac94c1220db129c Mon Sep 17 00:00:00 2001 From: hasezoey Date: Sat, 22 Nov 2025 12:02:48 +0100 Subject: [PATCH 06/11] refactor(read_key): move module into "printer/" --- src/lib.rs | 1 - src/printer/mod.rs | 9 ++++++--- src/{ => printer}/read_key.rs | 0 3 files changed, 6 insertions(+), 4 deletions(-) rename src/{ => printer}/read_key.rs (100%) diff --git a/src/lib.rs b/src/lib.rs index d2f6ff9..f3de07a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,7 +61,6 @@ use printer::{Printer, PrinterType}; mod config; mod error; mod printer; -mod read_key; mod utils; pub use config::Config; diff --git a/src/printer/mod.rs b/src/printer/mod.rs index 7d7f03b..7a282bb 100644 --- a/src/printer/mod.rs +++ b/src/printer/mod.rs @@ -31,11 +31,14 @@ mod sixel_util; pub use self::sixel_util::is_sixel_supported; mod iterm; -#[cfg(test)] -pub(crate) use crate::read_key::test_utils::TestKeys; -pub(crate) use crate::read_key::ReadKey; + +pub(crate) mod read_key; + pub use iterm::iTermPrinter; pub use iterm::is_iterm_supported; +#[cfg(test)] +use read_key::test_utils::TestKeys; +use read_key::ReadKey; pub trait Printer { // Print the given image in the terminal while respecting the options in the config struct. diff --git a/src/read_key.rs b/src/printer/read_key.rs similarity index 100% rename from src/read_key.rs rename to src/printer/read_key.rs From 7c557557ffbea491cd3305c83ad47d277ffe4f1a Mon Sep 17 00:00:00 2001 From: hasezoey Date: Sat, 22 Nov 2025 12:05:03 +0100 Subject: [PATCH 07/11] style(read_key): remove "pub(crate)" and just use "pub" --- src/printer/read_key.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/printer/read_key.rs b/src/printer/read_key.rs index e2b5fc8..5c445c9 100644 --- a/src/printer/read_key.rs +++ b/src/printer/read_key.rs @@ -1,7 +1,7 @@ use console::{Key, Term}; /// Trait to allow reading keys from multiple inputs like [`Term`] (via [`Term::read_key`]) or a custom Testing utility. -pub(crate) trait ReadKey { +pub trait ReadKey { fn read_key(&self) -> std::io::Result; } @@ -12,7 +12,7 @@ impl ReadKey for Term { } #[cfg(test)] -pub(crate) mod test_utils { +pub mod test_utils { use std::{cell::RefCell, io}; use console::Key; @@ -23,7 +23,7 @@ pub(crate) mod test_utils { /// /// Will returns a error if the sequence has reached the end, but a new one is requested. #[derive(Debug, Clone)] - pub(crate) struct TestKeys<'a> { + pub struct TestKeys<'a> { data: &'a [Key], next_idx: RefCell, } From d42938b1c9ed0e34dd8eaad81d843b7934d96bb6 Mon Sep 17 00:00:00 2001 From: atanunq Date: Sat, 22 Nov 2025 11:32:34 +0000 Subject: [PATCH 08/11] Reorder arguments --- CHANGELOG.md | 1 + src/lib.rs | 2 +- src/printer/block.rs | 2 +- src/printer/icy_sixel.rs | 2 +- src/printer/iterm.rs | 4 ++-- src/printer/kitty.rs | 2 +- src/printer/mod.rs | 18 ++++++++++-------- src/printer/read_key.rs | 2 ++ src/printer/sixel.rs | 2 +- 9 files changed, 20 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10de0b3..3c2012d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## next - Add `Konsole` as a available terminal for iterm2 images +- Add `stdin` internal argument to make printer utils testable ## 0.10.0 - Add `icy_sixel` feature that uses a Rust implementation of Sixel diff --git a/src/lib.rs b/src/lib.rs index f3de07a..d7ddfce 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -103,7 +103,7 @@ pub fn print(img: &DynamicImage, config: &Config) -> ViuResult<(u32, u32)> { // This is required to get a "Term" instance for "::read_key" let term = Term::stdout(); - let (w, h) = choose_printer(config).print(&mut stdout, &term, img, config)?; + let (w, h) = choose_printer(config).print(&term, &mut stdout, img, config)?; if config.restore_cursor { execute!(&mut stdout, RestorePosition)?; diff --git a/src/printer/block.rs b/src/printer/block.rs index 3e25dfb..c08fefd 100644 --- a/src/printer/block.rs +++ b/src/printer/block.rs @@ -22,9 +22,9 @@ pub struct BlockPrinter; impl Printer for BlockPrinter { fn print( &self, + _stdin: &impl ReadKey, // 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/icy_sixel.rs b/src/printer/icy_sixel.rs index 9c5e1e1..dc3fb70 100644 --- a/src/printer/icy_sixel.rs +++ b/src/printer/icy_sixel.rs @@ -8,8 +8,8 @@ pub struct IcySixelPrinter; impl Printer for IcySixelPrinter { fn print( &self, - stdout: &mut impl std::io::Write, _stdin: &impl ReadKey, + stdout: &mut impl std::io::Write, img: &image::DynamicImage, config: &crate::Config, ) -> crate::ViuResult<(u32, u32)> { diff --git a/src/printer/iterm.rs b/src/printer/iterm.rs index c322f5d..a5d453e 100644 --- a/src/printer/iterm.rs +++ b/src/printer/iterm.rs @@ -26,8 +26,8 @@ pub fn is_iterm_supported() -> bool { impl Printer for iTermPrinter { fn print( &self, - stdout: &mut impl Write, _stdin: &impl ReadKey, + stdout: &mut impl Write, img: &DynamicImage, config: &Config, ) -> ViuResult<(u32, u32)> { @@ -145,7 +145,7 @@ mod tests { let stdin = TestKeys::new(&[]); assert_eq!( - iTermPrinter.print(&mut vec, &stdin, &img, &config).unwrap(), + iTermPrinter.print(&stdin, &mut vec, &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 6f953fa..c55cf8a 100644 --- a/src/printer/kitty.rs +++ b/src/printer/kitty.rs @@ -22,8 +22,8 @@ pub fn get_kitty_support() -> KittySupport { impl Printer for KittyPrinter { fn print( &self, - stdout: &mut impl Write, stdin: &impl ReadKey, + stdout: &mut impl Write, img: &image::DynamicImage, config: &Config, ) -> ViuResult<(u32, u32)> { diff --git a/src/printer/mod.rs b/src/printer/mod.rs index 7a282bb..7b484ff 100644 --- a/src/printer/mod.rs +++ b/src/printer/mod.rs @@ -45,8 +45,10 @@ pub trait Printer { // Return the dimensions of the printed image in **terminal cells**. fn print( &self, - stdout: &mut impl Write, + // Terminal input which may be used to query for supported capabilities. stdin: &impl ReadKey, + // Terminal output where the Printer should emit the image. + stdout: &mut impl Write, img: &DynamicImage, config: &Config, ) -> ViuResult<(u32, u32)>; @@ -62,7 +64,7 @@ pub trait Printer { let img = image::ImageReader::open(filename)? .with_guessed_format()? .decode()?; - self.print(stdout, stdin, &img, config) + self.print(stdin, stdout, &img, config) } } @@ -81,19 +83,19 @@ pub enum PrinterType { impl Printer for PrinterType { fn print( &self, - stdout: &mut impl Write, stdin: &impl ReadKey, + stdout: &mut impl Write, img: &DynamicImage, config: &Config, ) -> ViuResult<(u32, u32)> { match self { - PrinterType::Block => BlockPrinter.print(stdout, stdin, img, config), - PrinterType::Kitty => KittyPrinter.print(stdout, stdin, img, config), - PrinterType::iTerm => iTermPrinter.print(stdout, stdin, img, config), + PrinterType::Block => BlockPrinter.print(stdin, stdout, img, config), + PrinterType::Kitty => KittyPrinter.print(stdin, stdout, img, config), + PrinterType::iTerm => iTermPrinter.print(stdin, stdout, img, config), #[cfg(all(feature = "sixel", not(windows)))] - PrinterType::Sixel => SixelPrinter.print(stdout, stdin, img, config), + PrinterType::Sixel => SixelPrinter.print(stdin, stdout, img, config), #[cfg(any(feature = "icy_sixel", all(feature = "sixel", windows)))] - PrinterType::IcySixel => IcySixelPrinter.print(stdout, stdin, img, config), + PrinterType::IcySixel => IcySixelPrinter.print(stdin, stdout, img, config), } } diff --git a/src/printer/read_key.rs b/src/printer/read_key.rs index 5c445c9..4698680 100644 --- a/src/printer/read_key.rs +++ b/src/printer/read_key.rs @@ -25,6 +25,8 @@ pub mod test_utils { #[derive(Debug, Clone)] pub struct TestKeys<'a> { data: &'a [Key], + /// The trait is &self as it is build around current Term, which only has Term::ready_key(&self) signature. + /// Hence, cannot modify inner state unless we use inner mutability here, like RefCell. next_idx: RefCell, } diff --git a/src/printer/sixel.rs b/src/printer/sixel.rs index 622d655..3f59de2 100644 --- a/src/printer/sixel.rs +++ b/src/printer/sixel.rs @@ -12,8 +12,8 @@ pub struct SixelPrinter; impl Printer for SixelPrinter { fn print( &self, - stdout: &mut impl Write, _stdin: &impl ReadKey, + stdout: &mut impl Write, img: &DynamicImage, config: &Config, ) -> ViuResult<(u32, u32)> { From 4ed94b40ca8f2b3d5e5da7bd2f0fc664513c32c6 Mon Sep 17 00:00:00 2001 From: atanunq Date: Sat, 22 Nov 2025 11:36:38 +0000 Subject: [PATCH 09/11] Reorder print_file arguments --- src/lib.rs | 2 +- src/printer/iterm.rs | 2 +- src/printer/mod.rs | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d7ddfce..e5b9774 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -137,7 +137,7 @@ pub fn print_from_file>(filename: P, config: &Config) -> ViuResul // This is required to get a "Term" instance for "::read_key" let term = Term::stdout(); - let (w, h) = choose_printer(config).print_from_file(&mut stdout, &term, filename, config)?; + let (w, h) = choose_printer(config).print_from_file(&term, &mut stdout, filename, config)?; if config.restore_cursor { execute!(&mut stdout, RestorePosition)?; diff --git a/src/printer/iterm.rs b/src/printer/iterm.rs index a5d453e..11660d0 100644 --- a/src/printer/iterm.rs +++ b/src/printer/iterm.rs @@ -48,8 +48,8 @@ impl Printer for iTermPrinter { #[cfg(feature = "print-file")] fn print_from_file>( &self, - stdout: &mut impl Write, _stdin: &impl ReadKey, + stdout: &mut impl Write, filename: P, config: &Config, ) -> ViuResult<(u32, u32)> { diff --git a/src/printer/mod.rs b/src/printer/mod.rs index 7b484ff..650c127 100644 --- a/src/printer/mod.rs +++ b/src/printer/mod.rs @@ -56,8 +56,8 @@ pub trait Printer { #[cfg(feature = "print-file")] fn print_from_file>( &self, - stdout: &mut impl Write, stdin: &impl ReadKey, + stdout: &mut impl Write, filename: P, config: &Config, ) -> ViuResult<(u32, u32)> { @@ -102,20 +102,20 @@ impl Printer for PrinterType { #[cfg(feature = "print-file")] fn print_from_file>( &self, - stdout: &mut impl Write, stdin: &impl ReadKey, + stdout: &mut impl Write, filename: P, config: &Config, ) -> ViuResult<(u32, u32)> { match self { - 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), + PrinterType::Block => BlockPrinter.print_from_file(stdin, stdout, filename, config), + PrinterType::Kitty => KittyPrinter.print_from_file(stdin, stdout, filename, config), + PrinterType::iTerm => iTermPrinter.print_from_file(stdin, stdout, filename, config), #[cfg(all(feature = "sixel", not(windows)))] - PrinterType::Sixel => SixelPrinter.print_from_file(stdout, stdin, filename, config), + PrinterType::Sixel => SixelPrinter.print_from_file(stdin, stdout, filename, config), #[cfg(any(feature = "icy_sixel", all(feature = "sixel", windows)))] PrinterType::IcySixel => { - IcySixelPrinter.print_from_file(stdout, stdin, filename, config) + IcySixelPrinter.print_from_file(stdin, stdout, filename, config) } } } From d816bdcadff57ced0b738c0c270209f84bd5c6e8 Mon Sep 17 00:00:00 2001 From: atanunq Date: Sat, 22 Nov 2025 11:41:11 +0000 Subject: [PATCH 10/11] Reorder helper arguments --- src/printer/kitty.rs | 12 ++++++------ src/printer/sixel_util.rs | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/printer/kitty.rs b/src/printer/kitty.rs index c55cf8a..91ff0d1 100644 --- a/src/printer/kitty.rs +++ b/src/printer/kitty.rs @@ -31,11 +31,11 @@ impl Printer for KittyPrinter { KittySupport::None => Err(ViuError::KittyNotSupported), KittySupport::Local => { // print from file - print_local(stdout, stdin, img, config) + print_local(stdin, stdout, img, config) } KittySupport::Remote => { // print through escape codes - print_remote(stdout, stdin, img, config) + print_remote(stdin, stdout, img, config) } } } @@ -126,8 +126,8 @@ fn has_local_support(stdout: &mut impl Write, stdin: &impl ReadKey) -> ViuResult /// Print with kitty graphics protocol through a temp file // TODO: try with kitty's supported compression fn print_local( - stdout: &mut impl Write, _stdin: &impl ReadKey, + stdout: &mut impl Write, img: &image::DynamicImage, config: &Config, ) -> ViuResult<(u32, u32)> { @@ -166,8 +166,8 @@ fn print_local( /// Print with escape codes // TODO: try compression fn print_remote( - stdout: &mut impl Write, _stdin: &impl ReadKey, + stdout: &mut impl Write, img: &image::DynamicImage, config: &Config, ) -> ViuResult<(u32, u32)> { @@ -239,7 +239,7 @@ mod tests { let test_response = TestKeys::new(&test_data); assert_eq!( - print_local(&mut vec, &test_response, &img, &config).unwrap(), + print_local(&test_response, &mut vec, &img, &config).unwrap(), (40, 13) ); let result = std::str::from_utf8(&vec).unwrap(); @@ -266,7 +266,7 @@ mod tests { let test_response = TestKeys::new(&test_data); assert_eq!( - print_remote(&mut vec, &test_response, &img, &config).unwrap(), + print_remote(&test_response, &mut vec, &img, &config).unwrap(), (1, 1) ); let result = std::str::from_utf8(&vec).unwrap(); diff --git a/src/printer/sixel_util.rs b/src/printer/sixel_util.rs index 75b5d48..0026b2a 100644 --- a/src/printer/sixel_util.rs +++ b/src/printer/sixel_util.rs @@ -15,7 +15,7 @@ pub fn is_sixel_supported() -> bool { /// /// see https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Sixel-Graphics /// and https://vt100.net/docs/vt510-rm/DA1.html -fn check_device_attrs(stdout: &mut impl Write, stdin: &impl ReadKey) -> ViuResult { +fn check_device_attrs(stdin: &impl ReadKey, stdout: &mut impl Write) -> ViuResult { write!(stdout, "\x1b[c")?; stdout.flush()?; @@ -42,7 +42,7 @@ fn check_device_attrs(stdout: &mut impl Write, stdin: &impl ReadKey) -> ViuResul fn check_sixel_support() -> bool { let mut stdout = std::io::stdout(); let stdin = Term::stdout(); - check_device_attrs(&mut stdout, &stdin).unwrap_or(false) + check_device_attrs(&stdin, &mut stdout).unwrap_or(false) } #[cfg(test)] @@ -75,7 +75,7 @@ mod tests { ]; let test_stdin = TestKeys::new(&test_stdin_data); - check_device_attrs(&mut stdout, &test_stdin).unwrap(); + check_device_attrs(&test_stdin, &mut stdout).unwrap(); let result = std::str::from_utf8(&stdout).unwrap(); assert_eq!(result, "\x1b[c"); From caa2bea892a9dac66da1230cbd001e16299020c8 Mon Sep 17 00:00:00 2001 From: atanunq Date: Sat, 22 Nov 2025 11:42:55 +0000 Subject: [PATCH 11/11] Reorder kitty helper arguments --- src/printer/kitty.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/printer/kitty.rs b/src/printer/kitty.rs index 91ff0d1..db693d3 100644 --- a/src/printer/kitty.rs +++ b/src/printer/kitty.rs @@ -62,7 +62,7 @@ fn check_kitty_support() -> KittySupport { if term.contains("kitty") || term.contains("ghostty") { let mut stdout = std::io::stdout(); let stdin = Term::stdout(); - if has_local_support(&mut stdout, &stdin).is_ok() { + if has_local_support(&stdin, &mut stdout).is_ok() { return KittySupport::Local; } @@ -73,7 +73,7 @@ fn check_kitty_support() -> KittySupport { } /// Query the terminal whether it can display an image from a file -fn has_local_support(stdout: &mut impl Write, stdin: &impl ReadKey) -> ViuResult { +fn has_local_support(stdin: &impl ReadKey, stdout: &mut impl Write) -> ViuResult { // create a temp file that will hold a 1x1 image let x = image::RgbaImage::new(1, 1); let raw_img = x.as_raw(); @@ -300,7 +300,7 @@ mod tests { ]; let test_stdin = TestKeys::new(&test_stdin_data); - has_local_support(&mut stdout, &test_stdin).unwrap(); + has_local_support(&test_stdin, &mut stdout).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");