From 80165ec1fb8cf9427cdbdfe72e6ba9ed40dd861f Mon Sep 17 00:00:00 2001 From: hasezoey Date: Sun, 22 Jun 2025 17:12:08 +0200 Subject: [PATCH 01/12] fix(kitty): dont error on non-existent 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 | 22 +++++++++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c2012d..3090287 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Remove `lazy_static` dependency in favor of `std::sync::LazyLock` - MSRV is now 1.80 - Use sixel if found in device attributes instead of static TERM list +- 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 db693d3..992285f 100644 --- a/src/printer/kitty.rs +++ b/src/printer/kitty.rs @@ -3,8 +3,8 @@ use crate::printer::{adjust_offset, find_best_fit, Printer, ReadKey}; 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; @@ -72,6 +72,20 @@ fn check_kitty_support() -> KittySupport { KittySupport::None } +/// 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(stdin: &impl ReadKey, stdout: &mut impl Write) -> ViuResult { // create a temp file that will hold a 1x1 image @@ -106,8 +120,7 @@ fn has_local_support(stdin: &impl ReadKey, stdout: &mut impl Write) -> 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 = [ @@ -157,8 +170,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 eeba62517ed32f1234eeb4899fdce6cb0e01a962 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Wed, 12 Nov 2025 14:19:08 +0100 Subject: [PATCH 02/12] feat(kitty): check protocol support instead of static names fixes #70 fixes #67 fixes #71 --- CHANGELOG.md | 2 + src/printer/kitty.rs | 226 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 206 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3090287..0ba5e3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ - MSRV is now 1.80 - Use sixel if found in device attributes instead of static TERM list - 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 992285f..a4f7e5c 100644 --- a/src/printer/kitty.rs +++ b/src/printer/kitty.rs @@ -58,17 +58,19 @@ 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") { - let mut stdout = std::io::stdout(); - let stdin = Term::stdout(); - if has_local_support(&stdin, &mut stdout).is_ok() { - return KittySupport::Local; - } - - return KittySupport::Remote; + let mut stdout = std::io::stdout(); + let term = Term::stdout(); + + // first check if kitty protocol base-line(inline images / remote) is available + if has_remote_support(&term, &mut stdout).is_ok() { + // then test if the current terminal supports reading from a file(local) (for example this is not possible via ssh) + if has_local_support(&term, &mut stdout).is_ok() { + return KittySupport::Local; } + + return KittySupport::Remote; } + KittySupport::None } @@ -86,7 +88,69 @@ fn close_tmp_file(temp_file: NamedTempFile) -> ViuResult { Ok(()) } -/// Query the terminal whether it can display an image from a file +/// Query the terminal whether it can display inline images (the kitty image protocol base-line) +fn has_remote_support(stdin: &impl ReadKey, stdout: &mut impl Write) -> ViuResult { + // send the query + write!( + stdout, + // First query send a query for kitty image protocol + "\x1b_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\x1b\\", + )?; + // Then, send "primary device attributes" as a fall-back if kitty protocol is not supported. + // This is practically implemented in all terminals and prevents a infinite loop if kitty is not supported. + write!(stdout, "\x1b[c")?; + stdout.flush()?; + + 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: bool = 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) = stdin.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)) +} + +/// Query the terminal whether it can display an image from a file. +/// +/// Note that this function expects at least the base-line kitty support. +/// If this function is run first, this could lead to a infinite loop if kitty is not supported. +/// Run [`has_remote_support`] first and only run this function if that returns `Ok`. 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); @@ -107,12 +171,12 @@ fn has_local_support(stdin: &impl ReadKey, stdout: &mut impl Write) -> ViuResult )?; stdout.flush()?; - // collect Kitty's response after the query 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 { @@ -291,15 +355,46 @@ mod tests { } #[test] - fn test_kitty_protocol_supported() { + fn test_kitty_supported_remote_and_local() { + // output collected on kitty 0.42.2 + // 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()), + let test_data = [ + Key::UnknownEscSeq(vec!['_']), + Key::Char('G'), + Key::Char('i'), + Key::Char('='), + Key::Char('3'), + Key::Char('1'), + Key::Char(';'), + Key::Char('O'), + Key::Char('K'), + Key::UnknownEscSeq(vec!['\\']), + Key::UnknownEscSeq(vec!['[', '?', '6']), + Key::Char('2'), + Key::Char(';'), + Key::Char('5'), + Key::Char('2'), + Key::Char(';'), + Key::Char('c'), + ]; + let test_response = TestKeys::new(&test_data); + + has_remote_support(&test_response, &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!(test_response.reached_end()); + + stdout.clear(); + + // test kitty local protocol support + let mut stdout = Vec::new(); + + let test_data = [ + Key::UnknownEscSeq(vec!['_']), Key::Char('G'), Key::Char('i'), Key::Char('='), @@ -308,16 +403,103 @@ mod tests { Key::Char(';'), Key::Char('O'), Key::Char('K'), - Key::UnknownEscSeq(['\\'].into()), + Key::UnknownEscSeq(vec!['\\']), ]; - let test_stdin = TestKeys::new(&test_stdin_data); + let test_response = TestKeys::new(&test_data); - has_local_support(&test_stdin, &mut stdout).unwrap(); + has_local_support(&test_response, &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()); + assert!(test_response.reached_end()); + } + + #[test] + fn test_kitty_supported_but_not_local() { + // output collected on konsole 25.08.1 + + // test kitty protocol support + let mut stdout = Vec::new(); + + let test_data = [ + Key::UnknownEscSeq(vec!['_']), + Key::Char('G'), + Key::Char('i'), + Key::Char('='), + Key::Char('3'), + Key::Char('1'), + Key::Char(';'), + Key::Char('O'), + Key::Char('K'), + Key::UnknownEscSeq(vec!['\\']), + Key::UnknownEscSeq(vec!['[', '?', '6']), + Key::Char('2'), + Key::Char(';'), + Key::Char('1'), + Key::Char(';'), + Key::Char('4'), + Key::Char('c'), + ]; + let test_response = TestKeys::new(&test_data); + + has_remote_support(&test_response, &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!(test_response.reached_end()); + + stdout.clear(); + + // test kitty local protocol support + let mut stdout = Vec::new(); + + let test_data = [ + Key::UnknownEscSeq(vec!['_']), + 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(vec!['\\']), + ]; + let test_response = TestKeys::new(&test_data); + + has_local_support(&test_response, &mut stdout).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()); + } + + #[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); + + has_remote_support(&test_response, &mut stdout).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 9a95d34dfe4ab820c2cf3d0bfd1eef1b2ac20cf8 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Wed, 12 Nov 2025 14:28:57 +0100 Subject: [PATCH 03/12] fix(kitty): remove unnecessary "writeln" --- src/printer/kitty.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/printer/kitty.rs b/src/printer/kitty.rs index a4f7e5c..cc1eaa0 100644 --- a/src/printer/kitty.rs +++ b/src/printer/kitty.rs @@ -231,7 +231,6 @@ fn print_local( .ok_or_else(|| ViuError::Io(Error::other("Could not convert path to &str")))? ) )?; - writeln!(stdout)?; stdout.flush()?; close_tmp_file(temp_file)?; @@ -275,8 +274,8 @@ fn print_remote( let m = if iter.peek().is_some() { 1 } else { 0 }; write!(stdout, "\x1b_Gm={};{}\x1b\\", m, chunk)?; } - writeln!(stdout)?; stdout.flush()?; + Ok((w, h)) } @@ -321,7 +320,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.ends_with("\x1b\\\n")); + assert!(result.ends_with("\x1b\\")); assert!(test_response.reached_end()); } @@ -349,7 +348,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,m=1;AAAAAAIEBgg=\x1b\\" ); assert!(test_response.reached_end()); } From c399a031c2aea07d4ad72654fd4e3bf3f5ba7c78 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Wed, 12 Nov 2025 14:31:03 +0100 Subject: [PATCH 04/12] fix(kitty): with local display, send & wait for DSR To prevent the race condition of file removal before the terminal has finished reading it. (which can result in empty viu displays on kitty) --- src/printer/kitty.rs | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/src/printer/kitty.rs b/src/printer/kitty.rs index cc1eaa0..3a74bad 100644 --- a/src/printer/kitty.rs +++ b/src/printer/kitty.rs @@ -88,6 +88,34 @@ fn close_tmp_file(temp_file: NamedTempFile) -> ViuResult { Ok(()) } +/// Send & Wait for the DSR(Device Status Report) query. +fn wait_for_dsr(stdout: &mut impl Write, stdin: &impl ReadKey) -> ViuResult { + write!(stdout, "\x1b[5n")?; + stdout.flush()?; + + let mut response = Vec::new(); + + // assign it once instead of having to allocate a vector with static content in each loop + let end_seq = Key::UnknownEscSeq(vec!['[', '0', 'n']); + + 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 == end_seq || key == Key::Unknown; + response.push(key); + if should_break { + break; + } + } + + if response.len() == 1 && response.last().unwrap() == &end_seq { + return Ok(()); + } + + Err(ViuError::KittyResponse(response)) +} + /// Query the terminal whether it can display inline images (the kitty image protocol base-line) fn has_remote_support(stdin: &impl ReadKey, stdout: &mut impl Write) -> ViuResult { // send the query @@ -203,7 +231,7 @@ fn has_local_support(stdin: &impl ReadKey, stdout: &mut impl Write) -> ViuResult /// Print with kitty graphics protocol through a temp file // TODO: try with kitty's supported compression fn print_local( - _stdin: &impl ReadKey, + stdin: &impl ReadKey, stdout: &mut impl Write, img: &image::DynamicImage, config: &Config, @@ -233,6 +261,9 @@ fn print_local( )?; stdout.flush()?; + // prevent race condition of removing the file before the terminal is finished reading it. + wait_for_dsr(stdout, stdin)?; + close_tmp_file(temp_file)?; Ok((w, h)) @@ -310,7 +341,7 @@ mod tests { let mut vec = Vec::new(); - let test_data = []; + let test_data = [Key::UnknownEscSeq(vec!['[', '0', 'n'])]; let test_response = TestKeys::new(&test_data); assert_eq!( @@ -320,7 +351,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.ends_with("\x1b\\")); + assert!(result.ends_with("\x1b\\\x1b[5n")); assert!(test_response.reached_end()); } From e6d9413b3632900d85536afc366eecbfa3f8b1a9 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Sat, 22 Nov 2025 13:03:29 +0100 Subject: [PATCH 05/12] fix(kitty::has_*_support): check for full message, instead of partial --- src/printer/kitty.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/printer/kitty.rs b/src/printer/kitty.rs index 3a74bad..10be457 100644 --- a/src/printer/kitty.rs +++ b/src/printer/kitty.rs @@ -163,6 +163,10 @@ fn has_remote_support(stdin: &impl ReadKey, stdout: &mut impl Write) -> ViuResul Key::Char('='), Key::Char('3'), Key::Char('1'), + Key::Char(';'), + Key::Char('O'), + Key::Char('K'), + Key::UnknownEscSeq(vec!['\\']), ]; // The Graphics query and the device attributes response could theoretically be in any order @@ -214,14 +218,23 @@ fn has_local_support(stdin: &impl ReadKey, stdout: &mut impl Write) -> ViuResult close_tmp_file(temp_file)?; - // Kitty response should end with these 3 Keys if it was successful + // The Graphics query response let expected = [ + 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(vec!['\\']), ]; - if response.len() >= expected.len() && response[response.len() - 3..] == expected { + // 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(()); } From 3b907db7f74cda70f83d8ab6d52cd211e3192798 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Mon, 24 Nov 2025 13:35:39 +0100 Subject: [PATCH 06/12] fix(kitty::has_remote_support): change to only test for "CSI ?" to support different VT levels For example tmux does not respond with "CSI ? 6" but "CSI ? 1" --- src/printer/kitty.rs | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/src/printer/kitty.rs b/src/printer/kitty.rs index 10be457..6570cc7 100644 --- a/src/printer/kitty.rs +++ b/src/printer/kitty.rs @@ -134,13 +134,14 @@ fn has_remote_support(stdin: &impl ReadKey, stdout: &mut impl Write) -> ViuResul // determine if we had the "primary device attributes" reply, as otherwise "c" *could* be part of another query response beforehand let mut had_pda: bool = 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) = stdin.read_key() { - if key == pda_seq { - had_pda = true; + // this sequenece is also called "CSI ? 6" in "Terminal Response" at https://vt100.net/docs/vt510-rm/DA1.html + // Though it seems the "6" (and "4") is not actually required if the vt is at a different level + // meaning if we test explicitly for "6" it would never exit + if let Key::UnknownEscSeq(esc_seq) = &key { + if esc_seq.starts_with(&['[', '?']) { + had_pda = true; + } } // The "primary device attributes" response will end with a "c" character @@ -458,6 +459,30 @@ mod tests { assert!(test_response.reached_end()); } + #[test] + fn test_remote_support_tmux() { + // output collected on tmux 3.5_a (kitty & Konsole) + + // test kitty protocol support + let mut stdout = Vec::new(); + + let test_data = [ + Key::UnknownEscSeq(vec!['[', '?', '1']), + Key::Char(';'), + Key::Char('2'), + Key::Char(';'), + Key::Char('4'), + Key::Char('c'), + ]; + let test_response = TestKeys::new(&test_data); + + has_remote_support(&test_response, &mut stdout).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()); + } + #[test] fn test_kitty_supported_but_not_local() { // output collected on konsole 25.08.1 From cb7444d6566b65858c1a5fa0262399c554a5db73 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Tue, 25 Nov 2025 17:38:44 +0100 Subject: [PATCH 07/12] Revert "fix(kitty): remove unnecessary "writeln"" This reverts commit 9a95d34dfe4ab820c2cf3d0bfd1eef1b2ac20cf8. re discussion about some shells like bash not newlining the next prompt. --- src/printer/kitty.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/printer/kitty.rs b/src/printer/kitty.rs index 6570cc7..511e475 100644 --- a/src/printer/kitty.rs +++ b/src/printer/kitty.rs @@ -273,6 +273,10 @@ fn print_local( .ok_or_else(|| ViuError::Io(Error::other("Could not convert path to &str")))? ) )?; + // Some shells like bash dont put the prompt on the next line if the process didnt newline itself + // which can cause the prompt to be after the image. + // See + writeln!(stdout)?; stdout.flush()?; // prevent race condition of removing the file before the terminal is finished reading it. @@ -319,8 +323,11 @@ fn print_remote( let m = if iter.peek().is_some() { 1 } else { 0 }; write!(stdout, "\x1b_Gm={};{}\x1b\\", m, chunk)?; } + // Some shells like bash dont put the prompt on the next line if the process didnt newline itself + // which can cause the prompt to be after the image. + // See } + writeln!(stdout)?; stdout.flush()?; - Ok((w, h)) } @@ -365,7 +372,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.ends_with("\x1b\\\x1b[5n")); + assert!(result.ends_with("\x1b\\\n\x1b[5n")); assert!(test_response.reached_end()); } @@ -393,7 +400,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\\" + "\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()); } From 2169521c0f79c69eda8f5d73156d1e34d4f38786 Mon Sep 17 00:00:00 2001 From: atanunq Date: Wed, 26 Nov 2025 14:14:09 +0000 Subject: [PATCH 08/12] Conditional kitty writeln --- src/printer/kitty.rs | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/printer/kitty.rs b/src/printer/kitty.rs index 511e475..ab6f429 100644 --- a/src/printer/kitty.rs +++ b/src/printer/kitty.rs @@ -1,5 +1,6 @@ use crate::error::{ViuError, ViuResult}; use crate::printer::{adjust_offset, find_best_fit, Printer, ReadKey}; +use crate::utils::terminal_size; use crate::Config; use base64::{engine::general_purpose, Engine}; use console::{Key, Term}; @@ -27,7 +28,7 @@ impl Printer for KittyPrinter { img: &image::DynamicImage, config: &Config, ) -> ViuResult<(u32, u32)> { - match get_kitty_support() { + let result = match get_kitty_support() { KittySupport::None => Err(ViuError::KittyNotSupported), KittySupport::Local => { // print from file @@ -37,7 +38,22 @@ impl Printer for KittyPrinter { // print through escape codes print_remote(stdin, stdout, img, config) } + }; + + // The cursor is pushed to the next line by Kitty if the image reaches the terminal's boundary. + // We must do it only if the image is smaller, otherwise we end up with a blank line. + // See + // + // Could be done with a cursor check through `crossterm::cursor::position`, + // but that alone doesn't justify enabling the `events` feature. + if let Ok((w, _)) = result { + let (term_w, _) = terminal_size(); + if config.x + (w as u16) < term_w { + writeln!(stdout)?; + } } + + result } // TODO: guess_format() here in order to treat PNGs specially (f=100). @@ -273,10 +289,6 @@ fn print_local( .ok_or_else(|| ViuError::Io(Error::other("Could not convert path to &str")))? ) )?; - // Some shells like bash dont put the prompt on the next line if the process didnt newline itself - // which can cause the prompt to be after the image. - // See - writeln!(stdout)?; stdout.flush()?; // prevent race condition of removing the file before the terminal is finished reading it. @@ -323,10 +335,6 @@ fn print_remote( let m = if iter.peek().is_some() { 1 } else { 0 }; write!(stdout, "\x1b_Gm={};{}\x1b\\", m, chunk)?; } - // Some shells like bash dont put the prompt on the next line if the process didnt newline itself - // which can cause the prompt to be after the image. - // See } - writeln!(stdout)?; stdout.flush()?; Ok((w, h)) } From 13016b3c936234e1ecb65a2ca5863526b0cc78e4 Mon Sep 17 00:00:00 2001 From: atanunq Date: Wed, 26 Nov 2025 14:18:29 +0000 Subject: [PATCH 09/12] Rm newline from tests --- src/printer/kitty.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/printer/kitty.rs b/src/printer/kitty.rs index ab6f429..cdf5f47 100644 --- a/src/printer/kitty.rs +++ b/src/printer/kitty.rs @@ -380,7 +380,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.ends_with("\x1b\\\n\x1b[5n")); + assert!(result.ends_with("\x1b\\\x1b[5n")); assert!(test_response.reached_end()); } @@ -408,7 +408,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,m=1;AAAAAAIEBgg=\x1b\\" ); assert!(test_response.reached_end()); } From 970299e7ee55a2d58e694571c410936c26336a87 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Wed, 26 Nov 2025 15:30:58 +0100 Subject: [PATCH 10/12] refactor(kitty): move conditional newline printing to separate function --- src/printer/kitty.rs | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/printer/kitty.rs b/src/printer/kitty.rs index cdf5f47..c6dd447 100644 --- a/src/printer/kitty.rs +++ b/src/printer/kitty.rs @@ -38,22 +38,11 @@ impl Printer for KittyPrinter { // print through escape codes print_remote(stdin, stdout, img, config) } - }; + }?; - // The cursor is pushed to the next line by Kitty if the image reaches the terminal's boundary. - // We must do it only if the image is smaller, otherwise we end up with a blank line. - // See - // - // Could be done with a cursor check through `crossterm::cursor::position`, - // but that alone doesn't justify enabling the `events` feature. - if let Ok((w, _)) = result { - let (term_w, _) = terminal_size(); - if config.x + (w as u16) < term_w { - writeln!(stdout)?; - } - } + print_newline(stdout, config, result.0)?; - result + Ok(result) } // TODO: guess_format() here in order to treat PNGs specially (f=100). @@ -61,6 +50,21 @@ impl Printer for KittyPrinter { // fn print_from_file(&self, filename: &str, config: &Config) -> ViuResult<(u32, u32)> {} } +/// The cursor is pushed to the next line by Kitty if the image reaches the terminal's boundary. +/// We must do it only if the image is smaller, otherwise we end up with a blank line. +/// See +/// +/// Could be done with a cursor check through `crossterm::cursor::position`, +/// but that alone doesn't justify enabling the `events` feature. +fn print_newline(stdout: &mut impl Write, config: &Config, width: u32) -> ViuResult { + let (term_w, _) = terminal_size(); + if config.x + (width as u16) < term_w { + writeln!(stdout)?; + } + + Ok(()) +} + #[derive(PartialEq, Eq, Copy, Clone)] /// The extend to which the Kitty graphics protocol can be used. pub enum KittySupport { From 8e18c61d1641419814c658987337aba75fb2844d Mon Sep 17 00:00:00 2001 From: atanunq Date: Wed, 26 Nov 2025 17:30:47 +0000 Subject: [PATCH 11/12] Change argument order --- src/printer/kitty.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/printer/kitty.rs b/src/printer/kitty.rs index c6dd447..1d02459 100644 --- a/src/printer/kitty.rs +++ b/src/printer/kitty.rs @@ -109,7 +109,7 @@ fn close_tmp_file(temp_file: NamedTempFile) -> ViuResult { } /// Send & Wait for the DSR(Device Status Report) query. -fn wait_for_dsr(stdout: &mut impl Write, stdin: &impl ReadKey) -> ViuResult { +fn wait_for_dsr(stdin: &impl ReadKey, stdout: &mut impl Write) -> ViuResult { write!(stdout, "\x1b[5n")?; stdout.flush()?; @@ -296,7 +296,7 @@ fn print_local( stdout.flush()?; // prevent race condition of removing the file before the terminal is finished reading it. - wait_for_dsr(stdout, stdin)?; + wait_for_dsr(stdin, stdout)?; close_tmp_file(temp_file)?; From c65a23509c9d1f31b17bdf13d53cbe6bd8881ab3 Mon Sep 17 00:00:00 2001 From: atanunq Date: Wed, 26 Nov 2025 17:30:57 +0000 Subject: [PATCH 12/12] Fix changelog --- CHANGELOG.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ba5e3b..5de6756 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,14 @@ ## next - Add `Konsole` as a available terminal for iterm2 images - Add `stdin` internal argument to make printer utils testable +- Ignore Error in Kitty if the temporary file has been deleted by the terminal. Fixes `KittySupport::Local` +- Check for Kitty support on terminals via protocol query, instead of static `TERM` environment variable checking ## 0.10.0 - Add `icy_sixel` feature that uses a Rust implementation of Sixel - Remove `lazy_static` dependency in favor of `std::sync::LazyLock` - MSRV is now 1.80 - Use sixel if found in device attributes instead of static TERM list -- 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