Skip to content
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
11 changes: 9 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
#[cfg(feature = "print-file")]
use std::path::Path;

use console::Term;
use crossterm::{
cursor::{RestorePosition, SavePosition},
execute,
Expand Down Expand Up @@ -99,7 +100,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(&term, &mut stdout, img, config)?;

if config.restore_cursor {
execute!(&mut stdout, RestorePosition)?;
Expand Down Expand Up @@ -130,7 +134,10 @@ pub fn print_from_file<P: AsRef<Path>>(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(&term, &mut stdout, filename, config)?;

if config.restore_cursor {
execute!(&mut stdout, RestorePosition)?;
Expand Down
3 changes: 2 additions & 1 deletion src/printer/block.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -22,6 +22,7 @@ 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,
img: &DynamicImage,
Expand Down
3 changes: 2 additions & 1 deletion src/printer/icy_sixel.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand All @@ -8,6 +8,7 @@ pub struct IcySixelPrinter;
impl Printer for IcySixelPrinter {
fn print(
&self,
_stdin: &impl ReadKey,
stdout: &mut impl std::io::Write,
img: &image::DynamicImage,
config: &crate::Config,
Expand Down
19 changes: 14 additions & 5 deletions src/printer/iterm.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -26,6 +26,7 @@ pub fn is_iterm_supported() -> bool {
impl Printer for iTermPrinter {
fn print(
&self,
_stdin: &impl ReadKey,
stdout: &mut impl Write,
img: &DynamicImage,
config: &Config,
Expand All @@ -47,6 +48,7 @@ impl Printer for iTermPrinter {
#[cfg(feature = "print-file")]
fn print_from_file<P: AsRef<Path>>(
&self,
_stdin: &impl ReadKey,
stdout: &mut impl Write,
filename: P,
config: &Config,
Expand All @@ -63,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,
Expand All @@ -88,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")
Expand Down Expand Up @@ -123,6 +125,8 @@ fn check_iterm_support() -> bool {

#[cfg(test)]
mod tests {
use crate::printer::TestKeys;

use super::*;
use image::GenericImage;

Expand All @@ -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(&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");
}
}
90 changes: 72 additions & 18 deletions src/printer/kitty.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -22,6 +22,7 @@ pub fn get_kitty_support() -> KittySupport {
impl Printer for KittyPrinter {
fn print(
&self,
stdin: &impl ReadKey,
stdout: &mut impl Write,
img: &image::DynamicImage,
config: &Config,
Expand All @@ -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(stdin, stdout, img, config)
}
KittySupport::Remote => {
// print through escape codes
print_remote(stdout, img, config)
print_remote(stdin, stdout, img, config)
}
}
}
Expand All @@ -55,11 +56,13 @@ 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") {
if has_local_support().is_ok() {
let mut stdout = std::io::stdout();
let stdin = Term::stdout();
if has_local_support(&stdin, &mut stdout).is_ok() {
return KittySupport::Local;
}

Expand All @@ -69,15 +72,16 @@ fn check_kitty_support() -> KittySupport {
KittySupport::None
}

// Query the terminal whether it can display an image from a file
fn has_local_support() -> ViuResult {
/// Query the terminal whether it can display an image from a file
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();
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(
Expand All @@ -86,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;
Expand All @@ -120,9 +123,10 @@ fn has_local_support() -> 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(
_stdin: &impl ReadKey,
stdout: &mut impl Write,
img: &image::DynamicImage,
config: &Config,
Expand Down Expand Up @@ -159,9 +163,10 @@ fn print_local(
Ok((w, h))
}

// Print with escape codes
/// Print with escape codes
// TODO: try compression
fn print_remote(
_stdin: &impl ReadKey,
stdout: &mut impl Write,
img: &image::DynamicImage,
config: &Config,
Expand Down Expand Up @@ -199,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<NamedTempFile, ViuError> {
let mut tmpfile = tempfile::Builder::new()
.prefix(TEMP_FILE_PREFIX)
Expand All @@ -214,6 +219,8 @@ fn store_in_tmp_file(buf: &[u8]) -> std::result::Result<NamedTempFile, ViuError>

#[cfg(test)]
mod tests {
use crate::printer::TestKeys;

use super::*;
use image::{DynamicImage, GenericImage};

Expand All @@ -227,11 +234,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(&test_response, &mut vec, &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]
Expand All @@ -246,12 +261,51 @@ 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(&test_response, &mut vec, &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());
}

#[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(&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");
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());
}
}
Loading