summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBenjamin Grosse <ste3ls@gmail.com>2024-02-28 00:33:21 +0000
committerGitHub <noreply@github.com>2024-02-27 19:33:21 -0500
commit285df85cdd745a959ddcbe0c2c1f176e28370abe (patch)
tree95bb5f8454eda1c9a6f183f46c07890216baa754
parentf6d1f71d6c11eaebc13ef9f26b06bd31e2275d51 (diff)
ratatui-image widget for image previews without scripts (#467)
_Disclaimer: I'm the author of ratatui-image._ Use [ratatui-image](https://github.com/benjajaja/ratatui-image) crate to render images. No script or hook setup is required.
-rw-r--r--Cargo.lock314
-rw-r--r--Cargo.toml2
-rw-r--r--docs/image_previews/README.md39
-rw-r--r--docs/image_previews/kitty.md71
-rw-r--r--src/commands/preview_cursor_move.rs6
-rw-r--r--src/config/clean/app/preview/config.rs12
-rw-r--r--src/config/raw/app/display/preview.rs14
-rw-r--r--src/context/app_context.rs48
-rw-r--r--src/context/preview_context.rs198
-rw-r--r--src/event/app_event.rs18
-rw-r--r--src/event/process_event.rs45
-rw-r--r--src/preview/preview_default.rs18
-rw-r--r--src/preview/preview_file.rs107
-rw-r--r--src/ui/views/tui_folder_view.rs73
14 files changed, 696 insertions, 269 deletions
diff --git a/Cargo.lock b/Cargo.lock
index c115364..07fc53a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,6 +3,12 @@
version = 3
[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
name = "ahash"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -116,6 +122,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
+name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "bit_field"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
+
+[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -148,6 +166,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
[[package]]
+name = "bytemuck"
+version = "1.14.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
name = "cassowary"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -244,6 +274,12 @@ dependencies = [
]
[[package]]
+name = "color_quant"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
+[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -288,6 +324,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]]
+name = "crc32fast"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
name = "crossbeam-channel"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -298,6 +343,29 @@ dependencies = [
]
[[package]]
+name = "crossbeam-deque"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fca89a0e215bab21874660c67903c5f143333cab1da83d041c7ded6053774751"
+dependencies = [
+ "cfg-if",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d2fe95351b870527a5d09bf563ed3c97c0cffb87cf1c78a591bf48bb218d9aa"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "crossbeam-utils",
+ "memoffset",
+]
+
+[[package]]
name = "crossbeam-utils"
version = "0.8.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -307,6 +375,12 @@ dependencies = [
]
[[package]]
+name = "crunchy"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
+
+[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -358,6 +432,12 @@ dependencies = [
]
[[package]]
+name = "dyn-clone"
+version = "1.0.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d"
+
+[[package]]
name = "either"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -396,6 +476,22 @@ dependencies = [
]
[[package]]
+name = "exr"
+version = "1.72.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4"
+dependencies = [
+ "bit_field",
+ "flume",
+ "half",
+ "lebe",
+ "miniz_oxide",
+ "rayon-core",
+ "smallvec",
+ "zune-inflate",
+]
+
+[[package]]
name = "fd-lock"
version = "3.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -407,6 +503,15 @@ dependencies = [
]
[[package]]
+name = "fdeflate"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
name = "filetime"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -419,6 +524,25 @@ dependencies = [
]
[[package]]
+name = "flate2"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "flume"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
+dependencies = [
+ "spin",
+]
+
+[[package]]
name = "form_urlencoded"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -448,6 +572,16 @@ dependencies = [
]
[[package]]
+name = "gif"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045"
+dependencies = [
+ "color_quant",
+ "weezl",
+]
+
+[[package]]
name = "git2"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -474,6 +608,16 @@ dependencies = [
]
[[package]]
+name = "half"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc52e53916c08643f1b56ec082790d1e86a32e58dc5268f897f313fbae7b4872"
+dependencies = [
+ "cfg-if",
+ "crunchy",
+]
+
+[[package]]
name = "hashbrown"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -522,6 +666,12 @@ dependencies = [
]
[[package]]
+name = "icy_sixel"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dc4d30216c3fc247730a4c6c74db2bd217a5454361ce24d70e504bda0cd345e"
+
+[[package]]
name = "idna"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -532,6 +682,24 @@ dependencies = [
]
[[package]]
+name = "image"
+version = "0.24.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "034bbe799d1909622a74d1193aa50147769440040ff36cb2baa947609b0a4e23"
+dependencies = [
+ "bytemuck",
+ "byteorder",
+ "color_quant",
+ "exr",
+ "gif",
+ "jpeg-decoder",
+ "num-traits",
+ "png",
+ "qoi",
+ "tiff",
+]
+
+[[package]]
name = "indexmap"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -630,6 +798,7 @@ dependencies = [
"dirs-next",
"filetime",
"globset",
+ "image",
"lazy_static",
"libc",
"lscolors",
@@ -639,6 +808,7 @@ dependencies = [
"phf",
"rand",
"ratatui",
+ "ratatui-image",
"regex",
"rustyline",
"serde",
@@ -657,6 +827,15 @@ dependencies = [
]
[[package]]
+name = "jpeg-decoder"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"
+dependencies = [
+ "rayon",
+]
+
+[[package]]
name = "js-sys"
version = "0.3.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -692,6 +871,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
+name = "lebe"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
+
+[[package]]
name = "libc"
version = "0.2.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -750,6 +935,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456"
[[package]]
+name = "lock_api"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
name = "log"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -780,12 +975,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
[[package]]
+name = "memoffset"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
+name = "miniz_oxide"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
+dependencies = [
+ "adler",
+ "simd-adler32",
+]
+
+[[package]]
name = "mio"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -989,6 +1203,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb"
[[package]]
+name = "png"
+version = "0.17.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c2378060fb13acff3ba0325b83442c1d2c44fbb76df481160ddc1687cce160"
+dependencies = [
+ "bitflags 1.3.2",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide",
+]
+
+[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1010,6 +1237,15 @@ dependencies = [
]
[[package]]
+name = "qoi"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
name = "quote"
version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1078,6 +1314,43 @@ dependencies = [
]
[[package]]
+name = "ratatui-image"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b2b2c9623c63916694d56b7f27358ef81fd6232ffa4858444787ecbcda9f791"
+dependencies = [
+ "base64",
+ "dyn-clone",
+ "icy_sixel",
+ "image",
+ "rand",
+ "ratatui",
+ "rustix",
+ "serde",
+ "termion",
+]
+
+[[package]]
+name = "rayon"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
name = "redox_syscall"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1268,6 +1541,12 @@ dependencies = [
]
[[package]]
+name = "simd-adler32"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
+
+[[package]]
name = "siphasher"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1280,6 +1559,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970"
[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+dependencies = [
+ "lock_api",
+]
+
+[[package]]
name = "stability"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1378,6 +1666,17 @@ dependencies = [
]
[[package]]
+name = "tiff"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e"
+dependencies = [
+ "flate2",
+ "jpeg-decoder",
+ "weezl",
+]
+
+[[package]]
name = "time"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1657,6 +1956,12 @@ dependencies = [
]
[[package]]
+name = "weezl"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
+
+[[package]]
name = "whoami"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1872,3 +2177,12 @@ dependencies = [
"quote",
"syn 2.0.41",
]
+
+[[package]]
+name = "zune-inflate"
+version = "0.2.54"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
+dependencies = [
+ "simd-adler32",
+]
diff --git a/Cargo.toml b/Cargo.toml
index 4c5eb76..8152053 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -24,6 +24,7 @@ colors-transform = "^0"
dirs-next = "^2"
filetime = "^0"
globset = "^0"
+image = "0.24.5"
lazy_static = "^1"
libc = "^0"
lscolors = { version = "0.17.0", features = ["nu-ansi-term"] }
@@ -31,6 +32,7 @@ notify = "^6"
open = "^5"
phf = { version = "^0", features = ["macros"], optional = true }
rand = "^0"
+ratatui-image = { version = "0.8.1", default-features = false, features = ["termion", "rustix", "serde"] }
regex = "1.9.3"
rustyline = "^12"
serde = { version = "^1", features = ["derive"] }
diff --git a/docs/image_previews/README.md b/docs/image_previews/README.md
index a65c145..0223335 100644
--- a/docs/image_previews/README.md
+++ b/docs/image_previews/README.md
@@ -1,21 +1,38 @@
# Image Thumbnails in File Previews
-Joshuto does not support image previews directly.
-One reason is that Joshuto wants to stay independent of specific display protocols and terminal emulators.
+Joshuto supports some terminal graphics protocols for image previews directly.
+See [ratatui-image](https://github.com/benjajaja/ratatui-image?tab=readme-ov-file#compatibility-matrix)
+for which terminals are supported.
-However, Joshuto offers two text-preview-related hooks which allow to easily implement an
-image preview with some simple scripts.
+To disable or override the detected protocol, the `preview_protocol` option can
+be used in the `joshuto.toml` file. Accepted values are `auto` (default),
+`disabled`, or any of the [implemented protocols](https://docs.rs/ratatui-image/latest/ratatui_image/picker/enum.ProtocolType.html)
+in lowercase.
+```toml
+[preview]
+preview_protocol = "halfblocks"
+...
+```
+
+However, since not all terminals have some graphics protocol implementation,
+Joshuto offers two text-preview-related hooks which allow to easily implement
+an image preview with some simple scripts.
The hooks can be configured in the `joshuto.toml` file.
```toml
[preview]
...
-preview_shown_hook_script = "~/path/to/some/executable_script_1"
-preview_removed_hook_script = "~/path/to/some/executable_script_2"
+preview_script = "~/path/to/some/executable_script_1"
+preview_shown_hook_script = "~/path/to/some/executable_script_2"
+preview_removed_hook_script = "~/path/to/some/executable_script_3"
```
-The shown-hook is called whenever a new file-preview (in the 3rd pane) is activated.
+The `preview_script` hook is called the first time for textual previews (thus
+it's cached). If the script returns zero, then its output is displayed.
+
+The `preview_shown_hook_script` is called each time a file-preview (in the 3rd
+pane) is focused.
-The removed-hook will be called each time the file preview panel
-completely disappears in Joshuto.
+The `preview_removed_hook_script` will be called each time the file preview
+panel completely disappears in Joshuto.
That is the case if the user selects a file for which no file preview is shown
(either due to missing output of the preview script or due to file size),
if the preview is not cached already and the preview pane is temporarily removed,
@@ -42,9 +59,6 @@ The “removed” script does not get any arguments.
Using these hooks, one can trigger various actions when moving the cursor along files in Joshuto,
and they can also be used to show image previews by the help of other 3rd party tools.
-Keep in mind that the result of the “normal” `preview` script you use for textual previews
-is cached by Joshuto and is not called every time a file is focused, but the “shown” hook is.
-
# Wrapper Script
For some of the 3rd party tools, it's necessary
to run them as a separate process, in parallel to Joshuto.
@@ -60,7 +74,6 @@ in the terminal and then Joshuto.
We have recipes for a few famous solutions.
* [Überzug](ueberzug.md) (only for X11)
* [Überzug++](ueberzugpp.md) (for X11, some Wayland compositors, and some specific terminal emulators)
-* [Kitty](kitty.md) (for the Kitty terminal)
## Other Recipes and Tricks
* [Combining text preview and image preview](combined_with_text.md)
diff --git a/docs/image_previews/kitty.md b/docs/image_previews/kitty.md
deleted file mode 100644
index 0fbddf2..0000000
--- a/docs/image_previews/kitty.md
+++ /dev/null
@@ -1,71 +0,0 @@
-# Image Previews with Kitty's `icat`
-
-The [Kitty](https://sw.kovidgoyal.net/kitty/) terminal must be [installed](https://sw.kovidgoyal.net/kitty/binary/#)
-and used for the solution explained here.
-
-To preview images in Kitty, you need to create these two scripts and make them executable.
-
-`~/.config/joshuto/on_preview_shown`:
-
-```shell
-#!/usr/bin/env bash
-
-FILE_PATH="$1" # Full path of the previewed file
-PREVIEW_X_COORD="$2" # x coordinate of upper left cell of preview area
-PREVIEW_Y_COORD="$3" # y coordinate of upper left cell of preview area
-PREVIEW_WIDTH="$4" # Width of the preview pane (number of fitting characters)
-PREVIEW_HEIGHT="$5" # Height of the preview pane (number of fitting characters)
-
-TMP_FILE="$HOME/.cache/joshuto/thumbcache.png"
-
-mimetype=$(file --mime-type -Lb "$FILE_PATH")
-
-function image {
- kitty +kitten icat \
- --transfer-mode=file \
- --clear 2>/dev/null
- kitty +kitten icat \
- --transfer-mode=file \
- --place "${PREVIEW_WIDTH}x${PREVIEW_HEIGHT}@${PREVIEW_X_COORD}x${PREVIEW_Y_COORD}" \
- "$1" 2>/dev/null
-}
-
-function video {
- ffmpegthumbnailer -i "$1" -o "${TMP_FILE}" -s 0 2>/dev/null
- image "${TMP_FILE}"
-}
-
-case "$mimetype" in
- image/*)
- image "${FILE_PATH}"
- ;;
- video/*)
- video "${FILE_PATH}"
- ;;
- *)
- kitty +kitten icat \
- --transfer-mode=file \
- --clear 2>/dev/null
- ;;
-esac
-```
-
-`~/.config/joshuto/on_preview_removed.sh`:
-
-```shell
-#!/usr/bin/env bash
-
-kitty +kitten icat \
- --transfer-mode=file \
- --clear 2>/dev/null
-```
-
-The first script will use `icat` to place an image on top of joshuto's preview window.
-If any images already exist, they will be cleared before showing the image.
-
-The second script simply clears any existing images on the screen.
-
-That's it. Previewing images should now work whenever you select a file.
-
-![Demo](https://user-images.githubusercontent.com/57725322/150659504-203c7175-4bee-4e46-b5c5-16cc16a51a12.png)
-
diff --git a/src/commands/preview_cursor_move.rs b/src/commands/preview_cursor_move.rs
index a59faa6..6959fb8 100644
--- a/src/commands/preview_cursor_move.rs
+++ b/src/commands/preview_cursor_move.rs
@@ -14,7 +14,7 @@ fn preview_cursor_move(context: &mut AppContext, new_index: usize) -> AppResult
let preview_context = context.preview_context_mut();
if let Some(file_path) = file_path {
- if let Some(PreviewFileState::Success { data }) =
+ if let Some(PreviewFileState::Success(data)) =
preview_context.previews_mut().get_mut(&file_path)
{
data.index = new_index;
@@ -32,7 +32,7 @@ pub fn preview_up(context: &mut AppContext, u: usize) -> AppResult {
let preview_context = context.preview_context_ref();
if let Some(file_path) = file_path {
- if let Some(PreviewFileState::Success { data }) =
+ if let Some(PreviewFileState::Success(data)) =
preview_context.previews_ref().get(file_path)
{
if data.index < u {
@@ -62,7 +62,7 @@ pub fn preview_down(context: &mut AppContext, u: usize) -> AppResult {
let preview_context = context.preview_context_ref();
if let Some(file_path) = file_path {
- if let Some(PreviewFileState::Success { data }) =
+ if let Some(PreviewFileState::Success(data)) =
preview_context.previews_ref().get(file_path)
{
Some(data.index + u)
diff --git a/src/config/clean/app/preview/config.rs b/src/config/clean/app/preview/config.rs
index 2fd50e5..e14407e 100644
--- a/src/config/clean/app/preview/config.rs
+++ b/src/config/clean/app/preview/config.rs
@@ -1,18 +1,18 @@
use std::path;
use crate::{
- config::{raw::app::display::preview::PreviewOptionRaw, search_directories},
+ config::{
+ raw::app::display::preview::{default_max_preview_size, PreviewOptionRaw, PreviewProtocol},
+ search_directories,
+ },
util::unix,
CONFIG_HIERARCHY,
};
-const fn default_max_preview_size() -> u64 {
- 2 * 1024 * 1024 // 2 MB
-}
-
#[derive(Clone, Debug)]
pub struct PreviewOption {
pub max_preview_size: u64,
+ pub preview_protocol: PreviewProtocol,
pub preview_script: Option<path::PathBuf>,
pub preview_shown_hook_script: Option<path::PathBuf>,
pub preview_removed_hook_script: Option<path::PathBuf>,
@@ -22,6 +22,7 @@ impl std::default::Default for PreviewOption {
fn default() -> Self {
Self {
max_preview_size: default_max_preview_size(),
+ preview_protocol: PreviewProtocol::Auto,
preview_script: None,
preview_shown_hook_script: None,
preview_removed_hook_script: None,
@@ -46,6 +47,7 @@ impl From<PreviewOptionRaw> for PreviewOption {
Self {
max_preview_size: raw.max_preview_size,
+ preview_protocol: raw.preview_protocol,
preview_script,
preview_shown_hook_script,
preview_removed_hook_script,
diff --git a/src/config/raw/app/display/preview.rs b/src/config/raw/app/display/preview.rs
index 60bb032..ee3682e 100644
--- a/src/config/raw/app/display/preview.rs
+++ b/src/config/raw/app/display/preview.rs
@@ -1,14 +1,27 @@
+use ratatui_image::picker::ProtocolType;
use serde::Deserialize;
pub const fn default_max_preview_size() -> u64 {
2 * 1024 * 1024 // 2 MB
}
+#[derive(Clone, Debug, Deserialize, Default)]
+#[serde(rename_all = "snake_case")]
+pub enum PreviewProtocol {
+ #[default]
+ Auto,
+ Disabled,
+ #[serde(untagged)]
+ ProtocolType(ProtocolType),
+}
+
#[derive(Clone, Debug, Deserialize)]
pub struct PreviewOptionRaw {
#[serde(default = "default_max_preview_size")]
pub max_preview_size: u64,
#[serde(default)]
+ pub preview_protocol: PreviewProtocol,
+ #[serde(default)]
pub preview_script: Option<String>,
#[serde(default)]
pub preview_shown_hook_script: Option<String>,
@@ -20,6 +33,7 @@ impl std::default::Default for PreviewOptionRaw {
fn default() -> Self {
Self {
max_preview_size: default_max_preview_size(),
+ preview_protocol: PreviewProtocol::Auto,
preview_script: None,
preview_shown_hook_script: None,
preview_removed_hook_script: None,
diff --git a/src/context/app_context.rs b/src/context/app_context.rs
index 35af553..0026a52 100644
--- a/src/context/app_context.rs
+++ b/src/context/app_context.rs
@@ -5,15 +5,17 @@ use std::thread;
use crate::commands::quit::QuitAction;
use crate::config::clean::app::AppConfig;
+use crate::config::raw::app::display::preview::PreviewProtocol;
use crate::context::{
CommandLineContext, LocalStateContext, MatchContext, MessageQueue, PreviewContext, TabContext,
UiContext, WorkerContext,
};
use crate::event::{AppEvent, Events};
-use crate::ui::views;
-use crate::ui::PreviewArea;
+use crate::preview::preview_file::PreviewFileState;
+use crate::ui::{views, AppBackend, PreviewArea};
use crate::Args;
use notify::{RecursiveMode, Watcher};
+use ratatui_image::picker::Picker;
use std::path;
pub struct AppContext {
@@ -51,6 +53,28 @@ pub struct AppContext {
impl AppContext {
pub fn new(config: AppConfig, args: Args) -> Self {
+ let picker = if config
+ .preview_options_ref()
+ .preview_shown_hook_script
+ .is_none()
+ {
+ Picker::from_termios().ok().and_then(|mut picker| {
+ match config.preview_options_ref().preview_protocol {
+ PreviewProtocol::Auto => {
+ picker.guess_protocol(); // Must run before Events::new() because it makes ioctl calls.
+ Some(picker)
+ }
+ PreviewProtocol::Disabled => None,
+ PreviewProtocol::ProtocolType(protocol_type) => {
+ picker.protocol_type = protocol_type;
+ Some(picker)
+ }
+ }
+ })
+ } else {
+ None
+ };
+
let events = Events::new();
let event_tx = events.event_tx.clone();
@@ -66,6 +90,8 @@ impl AppContext {
.unwrap();
let watched_paths = HashSet::with_capacity(3);
+ let preview_script = config.preview_options_ref().preview_script.clone();
+
Self {
quit: QuitAction::DoNot,
events,
@@ -74,8 +100,8 @@ impl AppContext {
local_state: None,
search_context: None,
message_queue: MessageQueue::new(),
- worker_context: WorkerContext::new(event_tx),
- preview_context: PreviewContext::new(),
+ worker_context: WorkerContext::new(event_tx.clone()),
+ preview_context: PreviewContext::new(picker, preview_script, event_tx),
ui_context: UiContext { layout: vec![] },
commandline_context,
config,
@@ -279,4 +305,18 @@ impl AppContext {
pub fn commandline_context_mut(&mut self) -> &mut CommandLineContext {
&mut self.commandline_context
}
+ pub fn load_preview(&mut self, backend: &AppBackend, path: path::PathBuf) {
+ // always load image without cache
+ self.preview_context_mut().set_image_preview(None);
+ self.preview_context
+ .load_preview_image(self, backend, path.clone());
+
+ let previews = self.preview_context_mut().previews_mut();
+ if previews.get(path.as_path()).is_none() {
+ // add to loading state
+ previews.insert(path.clone(), PreviewFileState::Loading);
+ self.preview_context
+ .load_preview_script(self, backend, path);
+ }
+ }
}
diff --git a/src/context/preview_context.rs b/src/context/preview_context.rs
index 51e593d..57184a8 100644
--- a/src/context/preview_context.rs
+++ b/src/context/preview_context.rs
@@ -1,25 +1,215 @@
use std::collections::HashMap;
-use std::path;
+use std::error::Error;
+use std::path::{self, PathBuf};
+use std::process::{Command, Stdio};
+use std::sync::mpsc::{self, Sender};
+use std::sync::Mutex;
+use std::{io, thread};
-use crate::preview::preview_file::PreviewFileState;
+use ratatui::layout::Rect;
+use ratatui_image::picker::Picker;
+use ratatui_image::protocol::Protocol;
+use ratatui_image::Resize;
+
+use crate::config::clean::app::AppConfig;
+use crate::event::{AppEvent, PreviewData};
+use crate::lazy_static;
+use crate::preview::preview_file::{FilePreview, PreviewFileState};
+use crate::ui::{views, AppBackend};
+use crate::AppContext;
+
+lazy_static! {
+ static ref GUARD: Mutex<()> = Mutex::new(());
+}
type FilePreviewMetadata = HashMap<path::PathBuf, PreviewFileState>;
pub struct PreviewContext {
previews: FilePreviewMetadata,
+ image_preview: Option<(PathBuf, Box<dyn Protocol>)>,
+ sender_script: Sender<(PathBuf, Rect)>,
+ sender_image: Option<Sender<(PathBuf, Rect)>>,
+ event_ts: Sender<AppEvent>,
}
impl PreviewContext {
- pub fn new() -> Self {
- Self {
+ pub fn new(
+ picker: Option<Picker>,
+ script: Option<PathBuf>,
+ event_ts: Sender<AppEvent>,
+ ) -> PreviewContext {
+ let (sender_script, receiver) = mpsc::channel::<(PathBuf, Rect)>();
+ let thread_script_event_ts = event_ts.clone();
+ thread::spawn(move || {
+ for (path, rect) in receiver {
+ if let Some(ref script) = script {
+ PreviewContext::spawn_command(
+ path.clone(),
+ script.to_path_buf(),
+ rect,
+ thread_script_event_ts.clone(),
+ );
+ }
+ }
+ });
+
+ let (sender_image, receiver) = mpsc::channel::<(PathBuf, Rect)>();
+ let sender_image = picker.map(|mut picker| {
+ let thread_image_event_ts = event_ts.clone();
+ thread::spawn(move || loop {
+ // Get last, or block for next.
+ if let Some((path, rect)) = receiver
+ .try_iter()
+ .last()
+ .or_else(|| receiver.iter().next())
+ {
+ let proto = image::io::Reader::open(path.as_path())
+ .and_then(|reader| reader.decode().map_err(Self::map_io_err))
+ .and_then(|dyn_img| {
+ picker
+ .new_protocol(dyn_img, rect, Resize::Fit)
+ .map_err(|err| {
+ io::Error::new(io::ErrorKind::Other, format!("{err}