diff options
author | Canop <cano.petrole@gmail.com> | 2022-01-05 21:27:39 +0100 |
---|---|---|
committer | Canop <cano.petrole@gmail.com> | 2022-01-05 21:27:39 +0100 |
commit | 0b15aede22d52d1f3a0fdf74e2e70ce4cd349f29 (patch) | |
tree | fb19431dbf24e04c60147a64306c87b7554b6338 | |
parent | c6871fef4b9b00a4876fff0e050a7c546372eeba (diff) |
images rendered with kitty protocol on wezterm
It's experimental and might be removed as wezterm
doesn't seem to support image resizing
-rw-r--r-- | Cargo.lock | 24 | ||||
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | src/app/app.rs | 18 | ||||
-rw-r--r-- | src/display/cell_size.rs | 8 | ||||
-rw-r--r-- | src/image/image_view.rs | 9 | ||||
-rw-r--r-- | src/kitty/image_renderer.rs | 102 | ||||
-rw-r--r-- | src/kitty/image_set.rs | 37 | ||||
-rw-r--r-- | src/kitty/mod.rs | 110 | ||||
-rw-r--r-- | src/lib.rs | 3 |
9 files changed, 236 insertions, 77 deletions
@@ -186,6 +186,7 @@ dependencies = [ "umask", "unicode-width", "users", + "xterm-query", ] [[package]] @@ -1012,6 +1013,19 @@ dependencies = [ ] [[package]] +name = "nix" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1e25ee6b412c2a1e3fcb6a4499a5c1bfe7f43e014bdce9a6b6666e5aa2d187" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] name = "ntapi" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1904,6 +1918,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" [[package]] +name = "xterm-query" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf3ae9267e2d939a754366532ed14fc9c4eeb08eb4f51884c3036c85e9120b4" +dependencies = [ + "nix", + "thiserror", +] + +[[package]] name = "yaml-rust" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -17,6 +17,7 @@ exclude = ["website", "broot*.zip"] [features] default = [] clipboard = ["terminal-clipboard"] +kitty-csi-check = ["xterm-query"] [dependencies] ahash = { version = "0.7", features = ["serde"] } @@ -57,6 +58,7 @@ tempfile = "3.2" termimad = "0.20.0" terminal-clipboard = { version = "0.3.1", optional = true } toml = "0.5" +xterm-query = { version = "0.1", optional = true } umask = "1.0" unicode-width = "0.1.8" diff --git a/src/app/app.rs b/src/app/app.rs index 9bae1fa..921ff58 100644 --- a/src/app/app.rs +++ b/src/app/app.rs @@ -194,16 +194,11 @@ impl App { app_state: &AppState, con: &AppContext, ) -> Result<(), ProgramError> { - // if some images are displayed by kitty, we'll erase it, + // if some images are displayed by kitty, we'll erase them, // but only after having displayed the new ones (if any) // to prevent some flickerings - #[cfg(unix)] - let previous_images = crate::kitty::image_renderer() - .as_ref() - .and_then(|renderer| { - let mut renderer = renderer.lock().unwrap(); - renderer.take_current_images() - }); + let previous_images = crate::kitty::take_current_images(); + for (idx, panel) in self.panels.as_mut_slice().iter_mut().enumerate() { let active = idx == self.active_panel_idx; let panel_skin = if active { &skin.focused } else { &skin.unfocused }; @@ -220,12 +215,9 @@ impl App { panel.display(w, &disc)?, ); } - #[cfg(unix)] + if let Some(previous_images) = previous_images { - if let Some(renderer) = crate::kitty::image_renderer().as_ref() { - let mut renderer = renderer.lock().unwrap(); - renderer.erase(w, previous_images)?; - } + previous_images.erase(w)?; } w.flush()?; Ok(()) diff --git a/src/display/cell_size.rs b/src/display/cell_size.rs index 29685dd..bb259ef 100644 --- a/src/display/cell_size.rs +++ b/src/display/cell_size.rs @@ -44,3 +44,11 @@ pub fn cell_size_in_pixels() -> std::io::Result<(u32, u32)> { } } +#[cfg(not(unix))] +pub fn cell_size_in_pixels() -> std::io::Result<(u32, u32)> { + // there's probably a way but I don't know it + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "fetching cell size isn't supported on Windows", + )) +} diff --git a/src/image/image_view.rs b/src/image/image_view.rs index 583e2a7..9ec8708 100644 --- a/src/image/image_view.rs +++ b/src/image/image_view.rs @@ -73,14 +73,7 @@ impl ImageView { .or_else(|| styles.default.get_bg()) .unwrap_or(Color::AnsiValue(238)); - #[cfg(unix)] - if let Some(renderer) = crate::kitty::image_renderer() { - let mut renderer = renderer.lock().unwrap(); - renderer.print(w, &self.source_img, area)?; - for y in area.top..area.top + area.height { - w.queue(cursor::MoveTo(area.left, y))?; - fill_bg(w, area.width as usize, bg)?; - } + if crate::kitty::try_print_image(w, &self.source_img, area)? { return Ok(()); } diff --git a/src/kitty/image_renderer.rs b/src/kitty/image_renderer.rs index 1da76a4..7282c27 100644 --- a/src/kitty/image_renderer.rs +++ b/src/kitty/image_renderer.rs @@ -26,12 +26,12 @@ use { termimad::Area, }; -pub type KittyImageSet = Vec<usize>; /// How to send the image to kitty /// /// Note that I didn't test yet the named shared memory /// solution offered by kitty. +#[derive(Debug)] pub enum TransmissionMedium { /// write a temp file, then give its path to kitty /// in the payload of the escape sequence. It's quite @@ -84,24 +84,56 @@ impl<'i> ImageData<'i> { /// according to kitty's documentation const CHUNK_SIZE: usize = 4096; -/// until I'm told there's another terminal supporting the kitty -/// terminal, I think I can just check the name -pub fn is_term_kitty() -> bool { - if let Ok(term_name) = env::var("TERM") { - debug!("TERM env var: {:?}", env::var("TERM")); - if term_name.contains("kitty") { - return true; - } +/// this is called only once, and cached in the kitty manager's MaybeRenderer state +#[allow(unreachable_code)] +fn is_kitty_graphics_protocol_supported() -> bool { + debug!("is_kitty_graphics_protocol_supported ?"); + + #[cfg(not(unix))] + { + // because cell_size_in_pixels isn't implemented on Windows + debug!("no kitty support yet on Windows"); + return false; } - if let Ok(term_name) = env::var("TERMINAL") { - debug!("TERMINAL env var: {:?}", env::var("TERMINAL")); - if term_name.contains("kitty") { - return true; + + for env_var in ["TERM", "TERMINAL"] { + if let Ok(env_val) = env::var(env_var) { + debug!("{:?} = {:?}", env_var, env_val); + let env_val = env_val.to_ascii_lowercase(); + for name in ["kitty", "wezterm"] { + if env_val.contains(name) { + debug!(" -> env var indicates kitty support"); + return true; + } + } } } + + // Checking support with a proper CSI sequence should be the prefered way but + // it doesn't work reliably on wezterm and requires a wait on other terminal. + // Only Kitty does supports it perfectly and it's not even necessary on this + // terminal because we can just check the env var TERM. + #[cfg(feature = "kitty-csi-check")] + { + let start = std::time::Instant::now(); + const TIMEOUT_MS: isize = 400; + let s = match xterm_query::query("\x1b_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\x1b\\\x1b[c", TIMEOUT_MS) { + Err(e) => { + debug!("xterm querying failed: {}", e); + false + } + Ok(response) => { + response.starts_with("\x1b_Gi=31;OK\x1b") + } + }; + debug!("Xterm querying took {:?}", start.elapsed()); + debug!("kitty protocol support: {:?}", s); + return s; + } false } + fn div_ceil(a: u32, b: u32) -> u32 { a / b + (0 != a % b) as u32 } @@ -109,11 +141,11 @@ fn div_ceil(a: u32, b: u32) -> u32 { /// the image renderer, with knowledge of the /// console cells dimensions, and built only on Kitty. /// +#[derive(Debug)] pub struct KittyImageRenderer { cell_width: u32, cell_height: u32, next_id: usize, - current_images: Option<KittyImageSet>, pub transmission_medium: TransmissionMedium, } @@ -217,8 +249,9 @@ impl<'i> KittyImage<'i> { } impl KittyImageRenderer { + /// Called only once (at most) by the KittyManager pub fn new() -> Option<Self> { - if !is_term_kitty() { + if !is_kitty_graphics_protocol_supported() { return None; } cell_size_in_pixels() @@ -226,54 +259,31 @@ impl KittyImageRenderer { .map(|(cell_width, cell_height)| Self { cell_width, cell_height, - current_images: None, next_id: 1, transmission_medium: TransmissionMedium::Chunks, }) } - pub fn take_current_images(&mut self) -> Option<KittyImageSet> { - self.current_images.take() - } - /// return a new image id which is assumed will be used + /// return a new image id fn new_id(&mut self) -> usize { let new_id = self.next_id; self.next_id += 1; - self.current_images - .get_or_insert_with(Vec::new) - .push(new_id); new_id } + /// Print the dynamicImage and return the KittyImageId + /// for later removal from screen pub fn print( &mut self, w: &mut W, src: &DynamicImage, area: &Area, - ) -> Result<(), ProgramError> { + ) -> Result<usize, ProgramError> { let img = KittyImage::new(src, area, self); + debug!("transmission medium: {:?}", self.transmission_medium); match self.transmission_medium { - TransmissionMedium::TempFile => img.print_with_temp_file(w), - TransmissionMedium::Chunks => img.print_with_chunks(w), + TransmissionMedium::TempFile => img.print_with_temp_file(w)?, + TransmissionMedium::Chunks => img.print_with_chunks(w)?, } - } - pub fn erase( - &mut self, - w: &mut W, - ids: KittyImageSet, - ) -> Result<(), ProgramError> { - for id in ids { - debug!("erase kitty image {}", id); - write!(w, "\u{1b}_Ga=d,d=I,i={}\u{1b}\\", id)?; - } - Ok(()) - } - /// erase all kitty images, even the forgetted ones - pub fn erase_all( - &mut self, - w: &mut W, - ) -> Result<(), ProgramError> { - write!(w, "\u{1b}_Ga=d,d=A\u{1b}\\")?; - self.current_images = None; - Ok(()) + Ok(img.id) } fn rendering_area( &self, diff --git a/src/kitty/image_set.rs b/src/kitty/image_set.rs new file mode 100644 index 0000000..b818eaf --- /dev/null +++ b/src/kitty/image_set.rs @@ -0,0 +1,37 @@ +use { + crate::{ + display::W, + errors::ProgramError, + }, + std::io::Write, +}; + +#[derive(Debug, Default)] +pub struct KittyImageSet { + ids: Vec<usize>, +} + +impl KittyImageSet { + pub fn erase( + self, + w: &mut W, + ) -> Result<(), ProgramError> { + for id in &self.ids { + debug!("erase kitty image {}", id); + write!(w, "\u{1b}_Ga=d,d=I,i={}\u{1b}\\", id)?; + } + Ok(()) + } + /// erase all kitty images, even the forgetted ones + /// + /// (this is currently unused) + pub fn erase_all( + w: &mut W, + ) -> Result<(), ProgramError> { + write!(w, "\u{1b}_Ga=d,d=A\u{1b}\\")?; + Ok(()) + } + pub fn push(&mut self, new_id: usize) { + self.ids.push(new_id); + } +} diff --git a/src/kitty/mod.rs b/src/kitty/mod.rs index 04f4ba7..9e98787 100644 --- a/src/kitty/mod.rs +++ b/src/kitty/mod.rs @@ -1,20 +1,114 @@ mod image_renderer; +mod image_set; -pub use image_renderer::*; +pub use { + image_renderer::*, + image_set::*, +}; use { + crate::{ + display::W, + errors::ProgramError, + }, + image::DynamicImage, once_cell::sync::Lazy, std::sync::Mutex, + termimad::Area, }; -static RENDERER: Lazy<Option<Mutex<KittyImageRenderer>>> = Lazy::new(|| { - KittyImageRenderer::new().map(Mutex::new) +/// Give the current images, so that they can be removed +/// (which should be done only after a new content has been +/// displayed) +pub fn take_current_images() -> Option<KittyImageSet> { + manager().lock().unwrap().take_current_images() +} +/// Try print the image in the specified area. +/// +/// Return Ok(true) if it went well, Ok(false) if +/// the terminal doesn't appear compatible with the Kitty +/// graphics protocol, or an error if somebody went wrong. +pub fn try_print_image( + w: &mut W, + src: &DynamicImage, + area: &Area, +) -> Result<bool, ProgramError> { + let mut manager = manager().lock().unwrap(); + manager.try_print_image(w, src, area) +} + +static MANAGER: Lazy<Mutex<KittyManager>> = Lazy::new(|| { + let manager = KittyManager { + current_images: None, + renderer: MaybeRenderer::Untested, + }; + Mutex::new(manager) }); -// TODO try to find another way (making app_context mut ?) to pass this -// around without the mutex gymnastic, and also to make it really lazy -// (ie only initialized when an image must be rendered) -pub fn image_renderer() -> &'static Option<Mutex<KittyImageRenderer>> { - &*RENDERER +fn manager() -> &'static Mutex<KittyManager> { + &*MANAGER } +#[derive(Debug)] +struct KittyManager { + current_images: Option<KittyImageSet>, + renderer: MaybeRenderer, +} + +#[derive(Debug)] +enum MaybeRenderer { + Untested, + Disabled, + Enabled { + renderer: KittyImageRenderer, + }, +} + +impl KittyManager { + pub fn take_current_images(&mut self) -> Option<KittyImageSet> { + self.current_images.take() + } + /// return the renderer if it's already checked and enabled, none if + /// it's disabled or if it hasn't been tested yet + pub fn renderer_if_tested(&mut self) -> Option<&mut KittyImageRenderer> { + match &mut self.renderer { + MaybeRenderer::Enabled { renderer } => Some(renderer), + _ => None, + } + } + pub fn renderer(&mut self) -> Option<&mut KittyImageRenderer> { + if matches!(self.renderer, MaybeRenderer::Disabled) { + return None; + } + if matches!(self.renderer, MaybeRenderer::Enabled { .. }) { + return self.renderer_if_tested(); + } + // we're in the Untested branch + match KittyImageRenderer::new() { + Some(renderer) => { + self.renderer = MaybeRenderer::Enabled { renderer }; + self.renderer_if_tested() + } + None => { + self.renderer = MaybeRenderer::Disabled; + None + } + } + } + pub fn try_print_image( + &mut self, + w: &mut W, + src: &DynamicImage, + area: &Area, + ) -> Result<bool, ProgramError> { + if let Some(renderer) = self.renderer() { + let new_id = renderer.print(w, src, area)?; + self.current_images + .get_or_insert_with(KittyImageSet::default) + .push(new_id); + Ok(true) + } else { + Ok(false) + } + } +} @@ -16,6 +16,7 @@ pub mod help; pub mod icon; pub mod image; pub mod keys; +pub mod kitty; pub mod launchable; pub mod path; pub mod pattern; @@ -34,8 +35,6 @@ pub mod verb; #[cfg(unix)] pub mod filesystems; -#[cfg(unix)] -pub mod kitty; #[cfg(unix)] pub mod net; |