diff options
author | Canop <cano.petrole@gmail.com> | 2020-11-25 23:21:23 +0100 |
---|---|---|
committer | Canop <cano.petrole@gmail.com> | 2020-11-25 23:21:23 +0100 |
commit | 9785535f80573a9b00d2ce738f0c5a8df81db392 (patch) | |
tree | 12486770f50cccd14ea38a873bbe191469938c47 | |
parent | ba9dbf6c0a3827fe6299f3de582db6fc1ce9362a (diff) |
[WIP] high-definition image preview when using kitty
(some cleaning to do but I need to sleep)
-rw-r--r-- | Cargo.lock | 60 | ||||
-rw-r--r-- | Cargo.toml | 8 | ||||
-rw-r--r-- | src/app/app.rs | 8 | ||||
-rw-r--r-- | src/display/mod.rs | 12 | ||||
-rw-r--r-- | src/display/terminal_dimensions.rs | 38 | ||||
-rw-r--r-- | src/image/image_view.rs | 26 | ||||
-rw-r--r-- | src/kitty/kitty_image.rs | 195 | ||||
-rw-r--r-- | src/kitty/mod.rs | 19 | ||||
-rw-r--r-- | src/lib.rs | 3 |
9 files changed, 348 insertions, 21 deletions
@@ -81,6 +81,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" [[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] name = "bet" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -118,6 +124,7 @@ name = "broot" version = "1.0.6" dependencies = [ "ansi_colours", + "base64 0.13.0", "bet", "char_reader", "chrono", @@ -149,6 +156,7 @@ dependencies = [ "simplelog", "strict", "syntect", + "tempfile", "termimad", "terminal-clipboard", "toml", @@ -267,9 +275,9 @@ dependencies = [ [[package]] name = "color_quant" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dbbb57365263e881e805dc77d94697c9118fd94d8da011240555aa7b23445bd" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "constant_time_eq" @@ -608,12 +616,13 @@ dependencies = [ [[package]] name = "image" -version = "0.23.10" +version = "0.23.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985fc06b1304d19c28d5c562ed78ef5316183f2b0053b46763a0b94862373c34" +checksum = "7ce04077ead78e39ae8610ad26216aed811996b043d47beed5090db674f9e9b5" dependencies = [ "bytemuck", "byteorder", + "color_quant", "gif", "jpeg-decoder", "num-iter", @@ -781,12 +790,6 @@ dependencies = [ ] [[package]] -name = "lzw" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084" - -[[package]] name = "matches" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1063,7 +1066,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b336d94e8e4ce29bf15bba393164629764744c567e8ad306cc1fdd0119967fd" dependencies = [ - "base64", + "base64 0.12.3", "chrono", "indexmap", "line-wrap", @@ -1246,12 +1249,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" [[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] name = "rust-argon2" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dab61250775933275e84053ac235621dfb739556d5c54a2f2e9313b7cf43a19" dependencies = [ - "base64", + "base64 0.12.3", "blake2b_simd", "constant_time_eq", "crossbeam-utils", @@ -1466,6 +1478,20 @@ dependencies = [ ] [[package]] +name = "tempfile" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +dependencies = [ + "cfg-if", + "libc", + "rand", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] name = "term" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1539,13 +1565,13 @@ dependencies = [ [[package]] name = "tiff" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f3b8a87c4da944c3f27e5943289171ac71a6150a79ff6bacfff06d159dfff2f" +checksum = "abeb4e3f32a8973722c0254189e6890358e72b1bf11becb287ee0b23c595a41d" dependencies = [ - "byteorder", - "lzw", - "miniz_oxide 0.3.7", + "jpeg-decoder", + "miniz_oxide 0.4.2", + "weezl", ] [[package]] @@ -1,6 +1,6 @@ [package] name = "broot" -version = "1.0.6" +version = "1.0.7-dev" authors = ["dystroy <denys.seguret@gmail.com>"] repository = "https://github.com/Canop/broot" documentation = "https://dystroy.org/broot" @@ -20,6 +20,7 @@ clipboard = ["terminal-clipboard"] [dependencies] ansi_colours = "1.0" +base64 = "0.13" bet = "0.3.4" char_reader = "0.1" clap = { version="2.33", default-features=false, features=["suggestions"] } @@ -32,7 +33,7 @@ file-size = "1.0.3" git2 = { version="0.13", default-features=false } glob = "0.3" id-arena = "2.2.1" -image = "0.23" +image = "0.23.12" lazy-regex = "0.1.3" lazy_static = "1.4" libc = "0.2" @@ -48,12 +49,13 @@ secular = "0.2" simplelog = "0.7" strict = "0.1.4" syntect = "4.2" +tempfile = "3.1" termimad = "0.8.30" +terminal-clipboard = { version = "0.1.1", optional = true } toml = "0.5" umask = "1.0" unicode-width = "0.1.8" -terminal-clipboard = { version = "0.1.1", optional = true } [dev-dependencies] criterion = "0.3.1" diff --git a/src/app/app.rs b/src/app/app.rs index b1b192d..34b6e8e 100644 --- a/src/app/app.rs +++ b/src/app/app.rs @@ -6,6 +6,7 @@ use { conf::Conf, display::{Areas, Screen, W}, errors::ProgramError, + kitty, file_sum, git, launchable::Launchable, skin::*, @@ -161,12 +162,19 @@ impl App { || self.close_panel(self.active_panel_idx) } + /// redraw the whole screen. All drawing + /// are supposed to happen here, and only here. fn display_panels( &mut self, w: &mut W, skin: &AppSkin, con: &AppContext, ) -> Result<(), ProgramError> { + #[cfg(unix)] + if let Some(renderer) = kitty::image_renderer() { + let mut renderer = renderer.lock().unwrap(); + renderer.erase_images(w)?; + } for (idx, panel) in self.panels.as_mut_slice().iter_mut().enumerate() { let focused = idx == self.active_panel_idx; let skin = if focused { &skin.focused } else { &skin.unfocused }; diff --git a/src/display/mod.rs b/src/display/mod.rs index 8ed1fb2..c075b0c 100644 --- a/src/display/mod.rs +++ b/src/display/mod.rs @@ -30,6 +30,7 @@ pub mod flags_display; pub mod status_line; mod matched_string; mod screen; +mod terminal_dimensions; #[cfg(not(any(target_family="windows",target_os="android")))] mod permissions; @@ -43,6 +44,7 @@ pub use { git_status_display::GitStatusDisplay, matched_string::MatchedString, screen::Screen, + terminal_dimensions::*, }; use { crate::{ @@ -74,6 +76,7 @@ lazy_static! { pub const WIDE_STATUS: bool = true; /// the type used by all GUI writing functions +//pub type W = std::io::BufWriter<std::io::Stderr>; pub type W = std::io::BufWriter<std::io::Stderr>; /// return the writer used by the application @@ -81,6 +84,15 @@ pub fn writer() -> W { std::io::BufWriter::new(std::io::stderr()) } +// /// the type used by all GUI writing functions +// //pub type W = std::io::BufWriter<std::io::Stderr>; +// pub type W = std::io::Stdout; +// +// /// return the writer used by the application +// pub fn writer() -> W { +// std::io::stdout() +// } + pub fn fill_bg( w: &mut W, diff --git a/src/display/terminal_dimensions.rs b/src/display/terminal_dimensions.rs new file mode 100644 index 0000000..e90ae89 --- /dev/null +++ b/src/display/terminal_dimensions.rs @@ -0,0 +1,38 @@ +use { + libc::{ + c_ushort, + ioctl, + STDOUT_FILENO, + TIOCGWINSZ, + }, + std::io, +}; + + +#[cfg(unix)] +pub fn cell_size_in_pixels() -> io::Result<(u32, u32)> { + // see http://www.delorie.com/djgpp/doc/libc/libc_495.html + #[repr(C)] + struct winsize { + ws_row: c_ushort, /* rows, in characters */ + ws_col: c_ushort, /* columns, in characters */ + ws_xpixel: c_ushort, /* horizontal size, pixels */ + ws_ypixel: c_ushort /* vertical size, pixels */ + }; + let w = winsize { ws_row: 0, ws_col: 0, ws_xpixel: 0, ws_ypixel: 0 }; + let r = unsafe { + ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) + }; + if r == 0 && w.ws_xpixel > w.ws_col && w.ws_ypixel > w.ws_row { + Ok(( + (w.ws_xpixel / w.ws_col) as u32, + (w.ws_ypixel / w.ws_row) as u32, + )) + } else { + Err(io::Error::new( + io::ErrorKind::Other, + "failed to fetch terminal dimension with ioctl", + )) + } +} + diff --git a/src/image/image_view.rs b/src/image/image_view.rs index 4e30562..1c2290b 100644 --- a/src/image/image_view.rs +++ b/src/image/image_view.rs @@ -4,6 +4,7 @@ use { app::AppContext, display::{fill_bg, Screen, W}, errors::ProgramError, + kitty, skin::PanelSkin, }, crossterm::{ @@ -20,7 +21,7 @@ use { GenericImageView, imageops::FilterType, }, - std::path::Path, + std::path::{Path, PathBuf}, termimad::{Area}, }; @@ -36,6 +37,7 @@ struct CachedImage { /// an imageview can display an image in the terminal with /// a ratio of one pixel per char in width. pub struct ImageView { + path: PathBuf, source_img: DynamicImage, display_img: Option<CachedImage>, } @@ -49,10 +51,17 @@ impl ImageView { Reader::open(&path)?.decode()? ); Ok(Self { + path: path.to_path_buf(), source_img, display_img: None, }) } + pub fn is_png(&self) -> bool { + match self.path.extension() { + Some(ext) => ext == "png" || ext == "PNG", + None => false, + } + } pub fn display( &mut self, w: &mut W, @@ -61,6 +70,21 @@ impl ImageView { area: &Area, con: &AppContext, ) -> Result<(), ProgramError> { + + #[cfg(unix)] + if let Some(renderer) = kitty::image_renderer() { + let mut renderer = renderer.lock().unwrap(); + w.queue(cursor::MoveTo(area.left, area.top))?; + renderer.print_with_chunks( + w, + &self.source_img, + area.width, + area.height, + )?; + // TODO clean area below (using z-index?) + return Ok(()); + } + let target_width = area.width as u32; let target_height = (area.height*2) as u32; let cached = self.display_img.as_ref() diff --git a/src/kitty/kitty_image.rs b/src/kitty/kitty_image.rs new file mode 100644 index 0000000..3cc1347 --- /dev/null +++ b/src/kitty/kitty_image.rs @@ -0,0 +1,195 @@ +use { + crate::{ + display::{ + cell_size_in_pixels, + W, + }, + errors::ProgramError, + }, + base64, + image::{ + DynamicImage, + GenericImageView, + }, + std::{ + env, + io::{self, Write}, + path::Path, + }, + tempfile, +}; + +/// 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") { + if term_name.contains("kitty") { + return true; + } + } + false +} + +fn div_ceil(a: u32, b: u32) -> u32 { + a / b + (0 != a % b) as u32 +} + +pub struct KittyImageRenderer { + cell_width: u32, + cell_height: u32, + has_image_on_screen: bool, +} + +impl KittyImageRenderer { + pub fn new() -> Option<Self> { + if !is_term_kitty() { + return None; + } + cell_size_in_pixels() + .ok() + .map(|(cell_width, cell_height)| Self { + cell_width, + cell_height, + has_image_on_screen: false, + }) + } + fn rendering_dim( + &self, + img_width: u32, + img_height: u32, + area_cols: u32, + area_rows: u32, + ) -> (u32, u32) { + let optimal_cols = div_ceil(img_width, self.cell_width); + let optimal_rows = div_ceil(img_height, self.cell_height); + debug!("area: {:?}", (area_cols, area_rows)); + debug!("optimal: {:?}", (optimal_cols, optimal_rows)); + if optimal_cols <= area_cols && optimal_rows <= area_rows { + // no constraint (TODO center?) + (optimal_cols, optimal_rows) + } else if optimal_cols * area_rows > optimal_rows * area_cols { + // we're constrained in width + debug!("constrained in width"); + (area_cols, optimal_rows * area_cols / optimal_cols) + } else { + // we're constrained in height + debug!("constrained in height"); + (optimal_cols * area_rows / optimal_rows, area_rows) + } + } + pub fn print_with_chunks( + &mut self, + w: &mut W, + img: &DynamicImage, + cols: u16, + rows: u16, + ) -> Result<(), ProgramError> { + let (width, height) = img.dimensions(); + let rgba = img.to_rgba8(); + let bytes = rgba.as_raw(); + let encoded = base64::encode(bytes); + let (c, r) = self.rendering_dim(width, height, cols.into(), rows.into()); + debug!("rendering_dim: {:?}", (c, r)); + let mut pos = 0; + loop { + if pos + 4096 < encoded.len() { + write!(w, + "\u{1b}_Ga=T,f=32,t=d,s={},v={},c={},r={},m=1;{}\u{1b}\\", + width, + height, + c, + r, + &encoded[pos..pos+4096], + )?; + pos += 4096; + } else { + // last chunk + write!(w, + "\u{1b}_Gm=0;{}\u{1b}\\", + &encoded[pos..encoded.len()], + )?; + break; + } + } + self.has_image_on_screen = true; + Ok(()) + } + // deprecated + pub fn print_png( + &mut self, + w: &mut W, + path: &Path, + cols: u16, + rows: u16, + ) -> Result<(), ProgramError> { + let path = path.to_str() + .ok_or_else(|| io::Error::new( + io::ErrorKind::Other, + "Path can't be converted to UTF8", + ))?; + let encoded_path = base64::encode(path); + debug!("printing png with kitty c={}, r={}, path={}", cols, rows, &path); + write!(w, + "\u{1b}_Ga=T,f=100,t=f,r={},c={};{}\u{1b}\\", + rows, + cols, + encoded_path, + )?; + self.has_image_on_screen = true; + Ok(()) + } + pub fn print_with_temp_file( + &mut self, + w: &mut W, + img: &DynamicImage, + cols: u16, + rows: u16, + ) -> Result<(), ProgramError> { + let (width, height) = img.dimensions(); + let rgba = img.to_rgba8(); + let bytes: &[u8] = rgba.as_raw(); + let (mut temp_file, path) = tempfile::Builder::new() + .prefix("broot-img-preview") + .tempfile()? + .keep() + .map_err(|_| io::Error::new( + io::ErrorKind::Other, + "temp file can't be kept", + ))?; + temp_file.write_all(bytes)?; + temp_file.flush()?; + let path = path.to_str() + .ok_or_else(|| io::Error::new( + io::ErrorKind::Other, + "Path can't be converted to UTF8", + ))?; + let encoded_path = base64::encode(path); + debug!("temp file written: {:?}", path); + let (c, r) = self.rendering_dim(width, height, cols.into(), rows.into()); + write!(w, + "\u{1b}_Ga=T,f=32,t=t,s={},v={},c={},r={};{}\u{1b}\\", + width, + height, + c, + r, + encoded_path, + )?; + debug!("file len: {}", temp_file.metadata().unwrap().len()); + self.has_image_on_screen = true; + Ok(()) + } + pub fn erase_images( + &mut self, + w: &mut W, + ) -> Result<(), ProgramError> { + if self.has_image_on_screen { + write!(w, "\u{1b}_Ga=d\u{1b}\\")?; + self.has_image_on_screen = false; + } + Ok(()) + } +} + + + + diff --git a/src/kitty/mod.rs b/src/kitty/mod.rs new file mode 100644 index 0000000..7395686 --- /dev/null +++ b/src/kitty/mod.rs @@ -0,0 +1,19 @@ +mod kitty_image; + +pub use kitty_image::*; + +use { + std::sync::Mutex, +}; + +lazy_static! { + static ref RENDERER: Option<Mutex<KittyImageRenderer>> = KittyImageRenderer::new() + .map(|r| Mutex::new(r)); +} + +// TODO try to find another way (making app_context mut ?) to pass this +// around without the mutex gymnastic +pub fn image_renderer() -> &'static Option<Mutex<KittyImageRenderer>> { + &*RENDERER +} + @@ -48,5 +48,8 @@ pub mod verb; #[cfg(unix)] pub mod filesystems; +#[cfg(unix)] +pub mod kitty; + #[cfg(feature="client-server")] pub mod net; |