Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Previewer #5

Open
qkzk opened this issue Oct 18, 2024 · 5 comments
Open

Previewer #5

qkzk opened this issue Oct 18, 2024 · 5 comments
Labels
enhancement New feature or request

Comments

@qkzk
Copy link

qkzk commented Oct 18, 2024

Hello there.

I've forked your repo to adapt it a little bit in my tui file explorer. ATM all I need is a way to prevent the terminal to be reset before leaving.

I used to include skim as a fuzzy finder, since I was using the same tui crate (tuikit) from the same author.
Those crates aren't maintained anymore and I wanted to switch to Crossterm.
Migrating skim to Crossterm is possible but will require A LOT of work.
So here I am.

The only thing I miss is a file previewer.

fzf & skim allow previewing of files in the picker which is very nice.

Previewing is done with an external previewer (bat, cat, whatever) and parsed into the tui.
It means parsing ansi strings and displaying them, which is done well in some crates like skim (I already adapted it to Crossterm).

It would be a major update of your project. Have you consider it ?

@alexrutar
Copy link
Contributor

I'm glad you find this crate to be useful!

Unfortunately, shelling out to an external previewer (like bat or cat, as you suggest) is certainly beyond the scope of this crate. At a very fundamental level, this crate is generic over the match type.

On the other hand, it would be sensible to expose some sort of "previewer"-type API inside the picker. This could take the form of a callback which is called by the viewer implementation when the match corresponding to the highlighted row is highlighted. The callback would, I suppose, be supplied with a reference to object corresponding to the highlighted row, the previewer window dimensions, and a mutable reference to a byte buffer which it can fill.

The main performance consideration is that the callback should not block rendering in the main loop. Especially with a use case like shelling out, this can potentially be quite slow. I suppose the screen rendering could be cached, but I would leave such details to the caller implementation.

I don't have time to implement this myself for the foreseeable future (i.e. the next 3-4 months) but I think it would be a great feature! I would be happy to implement this myself later on when I have more time, since I think it is useful more generally.

If you're keen to implement this yourself, I am happy to consider PRs if I believe the implementation lines up with how I see this crate going forward in the future.

@alexrutar
Copy link
Contributor

alexrutar commented Dec 14, 2024

@qkzk I've started to think about the previewer implementation a bit now; can you quickly summarize the way in which you would use a previewer?

Note that there are some things that I am not planning to implement: anything involving ANSI string parsing (there are lots of crates to do this for you, e.g. avt has a nice ansi parser).

The general shape of the API that I am considering is something like

pub trait Preview<T> {
    /// Backing data for the previewed content.
    type Buffer;

    /// Load backing data.
    fn load(&self, item: &T, buffer: &mut Self::Buffer);

    /// Generate the preview.
    fn preview<'a>(
        item: &'a T,
        buffer: &'a Self::Buffer,
        screen: &mut [Vec<StyledContent<Cow<'a, str>>>]
    );

In the end, I think it is better if nucleo-picker manages caching. The reasons for this are (1) that it is somewhat subtle to get right, especially multi-threaded caching, and (2) nucleo-picker can use pre-empting heuristics to generate the previewed content in advance.

As for the reason behind the decoupled load and preview functions: load is potentially slow (e.g. a filesystem read, external process call) whereas preview just generates the preview screen after the data is already loaded. Therefore loading will be done in the background (and split across multiple threads), whereas rendering must happen in the main render loop, so the calls need to be decoupled. Implementations which do not require loading data (e.g. a previewer which only uses data already internal to T) can just use type Buffer = () and a load implementation which is a no-op.

This API shape is quite crude, and e.g. currently does not permit usage patterns such as an external process call in an optimal way, since you don't want to startup a new std::process::Command each time but rather initialize a few up-front and then reuse them for each call. Before deciding on the actual API shape I will implement a few examples (e.g. file previewer using std::fs, file previewer using external command such as bat, and previewer which does not require pre-loading) and see where it goes from there.

By the way, reading your comment r.e. not clearing the terminal on exit, would it be useful if the previewer implemented ratatui::widget::StatefulWidget?

@qkzk
Copy link
Author

qkzk commented Dec 14, 2024

Hello,

I came up to something similar while writing a file previewer for a TUI file manager.

the process looks like this :

  • extract the file kind (char device, block device, socket, regular file, symbolic link, directory whatever)
  • for regular files extract the extension
  • pick a previewer from the last steps (most of the time it's a external command and a bunch of arguments)
  • build the preview (a block of text)
  • render in a window with some info ("previewed as a binary file")

As you mentioned the building can be very slow and shouldn't block anything else.

Since I try to preview every kind of file, including images/pdfs/videos... I decided to split preview building from preview rendering & navigation since it lead to a lot of code.

Depending on the non blocking method you pick (async, threads ...) the "attachment" process is different. One way is to have a separate thread with a mpsc reader in a loop for building which waits for preview builder results and attach it somewhere. Obviously it consumes a lot of ressources...

I couldn't find a perfect solution and I'm still looking for one. The yazi file manager does it another way, it has a work pool and everything is async.

Hope it helps !

@alexrutar
Copy link
Contributor

Hi, thanks for the comments! Glad to hear you've had the same thoughts as me r.e. the preview implementation and decoupled loading / screen rendering.

I spent a bit of time skimming through yazi; in the end I don't think we need full async to obtain robust performance (filesystem operations don't really benefit from async vs. usual threadpool without platform specific optimizations like io_uring). I think the key optimizations of yazi is preview pre-empting, and from the user perspective this is what makes it feel the fastest.

The plan is to have a threadpool of preview workers, with multiple 'work' queues with different priority, and for the worker threads (when not busy) to prioritize the higher priority queues. But I think from the end user this will essentially be invisible (a Preview implementation will simply describe how a single worker should load the backing data, and the threading implementation purely an implementation detail).

Unfortunately I really do not know anything about what it would take to make image rendering work so that's something to think about some time in the future. For now I will only support text-based previewers. I hope to have a working prototype some time soon!

@qkzk
Copy link
Author

qkzk commented Dec 19, 2024

Hello,

image rendering in the terminal is a mess.
In the past we had ueberzug which creates a window over the terminal and works in X. It was quite simple.

It's not maintained anymore and a ueberzug++ is the preferred candidate.

But... there's also the terminal emulator Kitty which is quite popular and render images directly.
...
Wait, there's more ! You can also render images as ascii with various libraries like sixel or chafa.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants