Skip to content

Commit

Permalink
XDG-thumb for preview & thumbs for non-image files (#537)
Browse files Browse the repository at this point in the history
This implements a cache for thumbnails and image previews for non-image
file formats, like videos and PDFs.
This patch extends the inbuilt image preview feature (285df85) by
integrating the `allmytoes` crate and using its thumbnails instead of
the actual file being previewed.

The image-preview feature uses the XDG-(freedesktop.org-) specified
thumbnail-cache to re-use existing thumbs and to store newly created
thumbs.
(https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html)

This increases performance for the image-preview because the preview
does not need to load the full-sized version of the image and does not
need to scale it down every time, once the thumbnail has been created.
Also, thumbnails are now shared with other programs that use the
XDG-thumb cache.

Furthermore, the new way to obtain thumbs allows to show image-previews
for file types other than images. Existing thumbnails are shown for any
file type. New thumbnails can be created for many video formats, PDF
files, Postscript files, and SVGs. Users can also add “providers” for
other file-types by configuring `allmytoes`.

The XDG-thumb feature can be disabled, and the thumbnail-size can be
changed, both in `joshuto.toml`. The documentation has been extended,
the whole image-preview page has been enhanced a little.
  • Loading branch information
DLFW authored May 20, 2024
1 parent 1245124 commit e458ac3
Show file tree
Hide file tree
Showing 8 changed files with 399 additions and 29 deletions.
241 changes: 239 additions & 2 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ categories = ['command-line-interface', 'command-line-utilities']
strip = true

[dependencies]
allmytoes = "^0.4"
alphanumeric-sort = "^1"
ansi-to-tui = { version = "^3.1.0", optional = true }
bitflags = { version = "^2", features = ["serde"] }
Expand Down
11 changes: 11 additions & 0 deletions docs/configuration/joshuto.toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,17 @@ max_preview_size = 2097152
# Executable script for previews
preview_script = "~/.config/joshuto/preview_file.sh"

# Use thumbnail images according to the freedesktop.org (XDG) standard.
# (https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html)
# This only affects Joshuto's internal image-thumbnail feature.
# It does not affect the hook-script based previews.
use_xdg_thumbs = true

# The XDG thumb size used for the preview.
# Allowed values are 'normal', 'large', 'xlarge', and 'xxlarge' with maximum edge lengths
# of 128 px, 256 px, 512 px, and 1024 px respectively.
xdg_thumb_size = "xlarge"

# Configurations related to searching and selecting files
[search]
# Different case sensitivities for operations using substring matching
Expand Down
104 changes: 79 additions & 25 deletions docs/image_previews/README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,86 @@
# Image Thumbnails in File Previews
# Thumbnails in File Previews

Joshuto supports some terminal graphics protocols for image previews directly.
Joshuto supports some terminal graphics protocols for showing thumbnails out of the box.
See [ratatui-image](https://github.com/benjajaja/ratatui-image?tab=readme-ov-file#compatibility-matrix)
for which terminals are supported.

To disable or override the detected protocol, the `preview_protocol` option can
This works without further configuration if the [preview script](../file_previews.md) returns with `0` for a file.
The textual preview returned by the preview script will be shown right beneath the thumbnail image.
This allows to use both, an image preview and a textual preview which is often used to show meta-data for non-text files.

## Disabling thumbnails or changing the image protocol

This thumbnail feature is enabled by default and uses an auto-detection for the image-protocol to use.
To disable the inbuilt thumbnail feature or to 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.
in lowercase, for example:
```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.
## XDG-Thumbnails and thumbnails for non-image files

By default, Joshuto uses thumbnails and the
[freedesktop.org specified](https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html)
thumbnail cache for the image previews.

This increases performance, because images are scaled down only once and then kept in a cache, which
can also be used by different programs.
This feature also allows to provide thumbnails for file formats other than image files.
Internally, Joshuto uses [allmytoes](https://gitlab.com/allmytoes/allmytoes) to provide the thumbnails.
Check the [documentation for “providers” there](https://gitlab.com/allmytoes/allmytoes#provider) to see which other
file types are supported and which local dependencies must be available to have them created when needed.

### Disabling XDG-Thumbnails

To disable the XDG-thumbnails and the provision of thumbs for non-image file types,
set the respective property in `joshuto.toml` to `false`:
```toml
[preview]
use_xdg_thumbs = false
```
The image-preview feature will still work, but will only show previews for image-files and it will
have to load the full-size images and then scale them down every time they are shown.

### Set the maximum size for thumbnails
The freedesktop.org thumbnail specification defines four different thumbnail sizes: normal, large, x-large, and xx-large
with maximum edge lengths of 128 px, 256 px, 512 px, and 1024 px respectively.

By default, Joshuto uses “x-large” thumbnails. This can be changed by the respective property in `joshuto.toml`, e.g.:
```toml
[preview]
# Allowed values are 'normal', 'large', 'xlarge', and 'xxlarge'
xdg_thumb_size = "xxlarge"
```

# Thumbnails via external scripts
The thumbnails provided directly by Joshuto might not be satisfying for all users.
Some terminals might not be supported at all, or only support a less satisfying image representation, like “halfblocks”.

As a solution for these cases,
Joshuto offers two text-preview-related hooks which allow to implement
an image preview with external scripts and some other “terminal image” solutions.
Most probably, you will use [Überzug++](ueberzugpp.md) for this job.
This allows, for example, to have proper preview-images in Alacritty, which otherwise only
support “halfblocks” with Joshuto's inbuilt image-preview.

Essentially, you have to create two scripts and “hook” them into Joshuto.
Joshuto will call them every time a new image-preview shall be shown or when any preview-image shall
be cleared.

The hooks can be configured in the `joshuto.toml` file.
```toml
[preview]
...
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"
preview_shown_hook_script = "~/.config/joshuto/preview_shown_hook"
preview_removed_hook_script = "~/.config/joshuto/preview_removed_hook"
```
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.

Be aware that as soon as these scripts are configured, the inbuilt image-preview feature
is disabled automatically.

The `preview_shown_hook_script` is called each time a file-preview (in the 3rd
pane) is focused.
Expand Down Expand Up @@ -56,28 +110,28 @@ The “removed” script does not get any arguments.
> That will trigger Joshuto to show an empty text-preview area which can then be used
> as a canvas for the image preview.
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.

# Wrapper Script
## Wrapper Script
For some of the 3rd party tools, it's necessary
to run them as a separate process, in parallel to Joshuto.

One famous example is “Überzug”. To be able to use such a solution,
The famous example is “Überzug” and its successor “Überzug++”.
To be able to use such a solution,
one needs to have a “wrapper-script” which must be started instead of Joshuto.
The wrapper-script will then start both, first the program for showing images
in the terminal and then Joshuto.


# Recipes
## Image Thumbnail Solution Recipes
We have recipes for a few famous solutions.
* [Überzug](ueberzug.md) (only for X11)
## Recipes for external previews with Überzug(++)

See

* [Überzug](ueberzug.md) (only for X11; unmaintained but may live on in [some fork](https://github.com/ueber-devel/ueberzug))
* [Überzug++](ueberzugpp.md) (for X11, some Wayland compositors, and some specific terminal emulators)

## Other Recipes and Tricks
## Tips and Tricks
* [Combining text preview and image preview](combined_with_text.md)
* To use XDG-thumbnails or thumbnails for non-image file types, you
can use the program [`allmytoes`](https://gitlab.com/allmytoes/allmytoes) inside
your shown-hook script to get the thumb-image for a file.
This is basically the “shell-program” version of what Joshuto also uses internally for the inbuilt XDG-thumbnail feature.

# Recipe Contributions welcome 🤗

Feel free to provide recipes to include in this documentation.
8 changes: 8 additions & 0 deletions src/config/clean/app/preview/config.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::path;

use allmytoes::ThumbSize;

use crate::{
config::{
raw::app::display::preview::{default_max_preview_size, PreviewOptionRaw, PreviewProtocol},
Expand All @@ -14,6 +16,8 @@ pub struct PreviewOption {
pub max_preview_size: u64,
pub preview_protocol: PreviewProtocol,
pub preview_script: Option<path::PathBuf>,
pub use_xdg_thumbs: bool,
pub xdg_thumb_size: ThumbSize,
pub preview_shown_hook_script: Option<path::PathBuf>,
pub preview_removed_hook_script: Option<path::PathBuf>,
}
Expand All @@ -23,6 +27,8 @@ impl std::default::Default for PreviewOption {
Self {
max_preview_size: default_max_preview_size(),
preview_protocol: PreviewProtocol::Auto,
use_xdg_thumbs: true,
xdg_thumb_size: ThumbSize::XLarge,
preview_script: None,
preview_shown_hook_script: None,
preview_removed_hook_script: None,
Expand All @@ -49,6 +55,8 @@ impl From<PreviewOptionRaw> for PreviewOption {
max_preview_size: raw.max_preview_size,
preview_protocol: raw.preview_protocol,
preview_script,
use_xdg_thumbs: raw.use_xdg_thumbs,
xdg_thumb_size: raw.xdg_thumb_size.to_amt_size(),
preview_shown_hook_script,
preview_removed_hook_script,
}
Expand Down
33 changes: 33 additions & 0 deletions src/config/raw/app/display/preview.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
use allmytoes::ThumbSize;
use ratatui_image::picker::ProtocolType;
use serde::Deserialize;

pub const fn default_max_preview_size() -> u64 {
2 * 1024 * 1024 // 2 MB
}

pub const fn default_true() -> bool {
true
}

#[derive(Clone, Debug, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum PreviewProtocol {
Expand All @@ -15,6 +20,27 @@ pub enum PreviewProtocol {
ProtocolType(ProtocolType),
}

#[derive(Clone, Debug, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum XDGThumbSizes {
Normal,
Large,
#[default]
XLarge,
XXLarge,
}

impl XDGThumbSizes {
pub fn to_amt_size(&self) -> ThumbSize {
match &self {
XDGThumbSizes::Normal => ThumbSize::Normal,
XDGThumbSizes::Large => ThumbSize::Large,
XDGThumbSizes::XLarge => ThumbSize::XLarge,
XDGThumbSizes::XXLarge => ThumbSize::XXLarge,
}
}
}

#[derive(Clone, Debug, Deserialize)]
pub struct PreviewOptionRaw {
#[serde(default = "default_max_preview_size")]
Expand All @@ -23,18 +49,25 @@ pub struct PreviewOptionRaw {
pub preview_protocol: PreviewProtocol,
#[serde(default)]
pub preview_script: Option<String>,
#[serde(default = "default_true")]
pub use_xdg_thumbs: bool,
#[serde(default)]
pub xdg_thumb_size: XDGThumbSizes,
#[serde(default)]
pub preview_shown_hook_script: Option<String>,
#[serde(default)]
pub preview_removed_hook_script: Option<String>,
}

// This should be deleted maybe. I don't see where it is invoked.
impl std::default::Default for PreviewOptionRaw {
fn default() -> Self {
Self {
max_preview_size: default_max_preview_size(),
preview_protocol: PreviewProtocol::Auto,
preview_script: None,
use_xdg_thumbs: true,
xdg_thumb_size: XDGThumbSizes::XLarge,
preview_shown_hook_script: None,
preview_removed_hook_script: None,
}
Expand Down
15 changes: 14 additions & 1 deletion src/context/app_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::event::{AppEvent, Events};
use crate::preview::preview_file::PreviewFileState;
use crate::ui::AppBackend;
use crate::Args;
use allmytoes::{AMTConfiguration, AMT};
use notify::{RecursiveMode, Watcher};
use ratatui_image::picker::Picker;
use std::path;
Expand Down Expand Up @@ -88,6 +89,12 @@ impl AppContext {
let watched_paths = HashSet::with_capacity(3);

let preview_script = config.preview_options_ref().preview_script.clone();
let allmytoes = if config.preview_options_ref().use_xdg_thumbs {
Some(AMT::new(&AMTConfiguration::default()))
} else {
None
};
let xdg_thumb_size = config.preview_options_ref().xdg_thumb_size;

Self {
quit: QuitAction::DoNot,
Expand All @@ -99,7 +106,13 @@ impl AppContext {
search_context: None,
message_queue: MessageQueue::new(),
worker_context: WorkerContext::new(event_tx.clone()),
preview_context: PreviewContext::new(picker, preview_script, event_tx),
preview_context: PreviewContext::new(
picker,
preview_script,
allmytoes,
xdg_thumb_size,
event_tx,
),
ui_context: UiContext { layout: vec![] },
commandline_context,
watcher,
Expand Down
15 changes: 14 additions & 1 deletion src/context/preview_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::sync::mpsc::{self, Sender};
use std::sync::Mutex;
use std::{io, thread};

use allmytoes::{ThumbSize, AMT};
use ratatui::layout::Rect;
use ratatui_image::picker::Picker;
use ratatui_image::protocol::Protocol;
Expand Down Expand Up @@ -44,6 +45,8 @@ impl PreviewContext {
pub fn new(
picker: Option<Picker>,
script: Option<PathBuf>,
allmytoes: Option<AMT>,
xdg_thumb_size: ThumbSize,
event_ts: Sender<AppEvent>,
) -> PreviewContext {
let (sender_script, receiver) = mpsc::channel::<(PathBuf, Rect)>();
Expand Down Expand Up @@ -71,7 +74,17 @@ impl PreviewContext {
.last()
.or_else(|| receiver.iter().next())
{
let proto = image::io::Reader::open(path.as_path())
let thumb_path = if let Some(amt) = &allmytoes {
let thumb_result = amt.get(&path, xdg_thumb_size);
if let Ok(thumb) = thumb_result {
PathBuf::from(thumb.path)
} else {
path.clone()
}
} else {
path.clone()
};
let proto = image::io::Reader::open(thumb_path.as_path())
.and_then(|reader| reader.decode().map_err(Self::map_io_err))
.and_then(|dyn_img| {
picker
Expand Down

0 comments on commit e458ac3

Please sign in to comment.