summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCanop <cano.petrole@gmail.com>2020-11-25 23:21:23 +0100
committerCanop <cano.petrole@gmail.com>2020-11-25 23:21:23 +0100
commit9785535f80573a9b00d2ce738f0c5a8df81db392 (patch)
tree12486770f50cccd14ea38a873bbe191469938c47
parentba9dbf6c0a3827fe6299f3de582db6fc1ce9362a (diff)
[WIP] high-definition image preview when using kitty
(some cleaning to do but I need to sleep)
-rw-r--r--Cargo.lock60
-rw-r--r--Cargo.toml8
-rw-r--r--src/app/app.rs8
-rw-r--r--src/display/mod.rs12
-rw-r--r--src/display/terminal_dimensions.rs38
-rw-r--r--src/image/image_view.rs26
-rw-r--r--src/kitty/kitty_image.rs195
-rw-r--r--src/kitty/mod.rs19
-rw-r--r--src/lib.rs3
9 files changed, 348 insertions, 21 deletions
diff --git a/Cargo.lock b/Cargo.lock
index e96b322..7d4152b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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]]
diff --git a/Cargo.toml b/Cargo.toml
index 5d52c30..0ba8451 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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
+}
+
diff --git a/src/lib.rs b/src/lib.rs
index 72cebf2..dfe4987 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -48,5 +48,8 @@ pub mod verb;
#[cfg(unix)]
pub mod filesystems;
+#[cfg(unix)]
+pub mod kitty;
+
#[cfg(feature="client-server")]
pub mod net;