mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
feat: support for Überzug++ for image previews with X11/wayland environment (#12)
This commit is contained in:
parent
d8725b3d7c
commit
13b473d5e1
28 changed files with 516 additions and 169 deletions
18
Cargo.lock
generated
18
Cargo.lock
generated
|
|
@ -961,9 +961,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchit"
|
name = "matchit"
|
||||||
version = "0.7.0"
|
version = "0.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40"
|
checksum = "67827e6ea8ee8a7c4a72227ef4fc08957040acffdb5f122733b24fa12daff41b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "md5"
|
name = "md5"
|
||||||
|
|
@ -1425,7 +1425,7 @@ checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-automata 0.3.3",
|
"regex-automata 0.3.4",
|
||||||
"regex-syntax 0.7.4",
|
"regex-syntax 0.7.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1440,9 +1440,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.3.3"
|
version = "0.3.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310"
|
checksum = "b7b6d6190b7594385f61bd3911cd1be99dfddcfc365a4160cc2ab5bff4aed294"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
|
@ -1502,18 +1502,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.176"
|
version = "1.0.178"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "76dc28c9523c5d70816e393136b86d48909cfb27cecaa902d338c19ed47164dc"
|
checksum = "60363bdd39a7be0266a520dab25fdc9241d2f987b08a01e01f0ec6d06a981348"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.176"
|
version = "1.0.178"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a4e7b8c5dc823e3b90651ff1d3808419cd14e5ad76de04feaf37da114e7a306f"
|
checksum = "f28482318d6641454cb273da158647922d1be6b5a2fcc6165cd89ebdd7ed576b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
|
||||||
23
README.md
23
README.md
|
|
@ -36,7 +36,7 @@ brew install jq unar ffmpegthumbnailer fd ripgrep fzf zoxide
|
||||||
brew tap homebrew/cask-fonts && brew install --cask font-symbols-only-nerd-font
|
brew tap homebrew/cask-fonts && brew install --cask font-symbols-only-nerd-font
|
||||||
```
|
```
|
||||||
|
|
||||||
And download the latest release [from here](https://github.com/sxyazi/yazi/releases). Or you can install Yazi with cargo:
|
And download the latest release [from here](https://github.com/sxyazi/yazi/releases). Or you can install Yazi via cargo:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo install --git https://github.com/sxyazi/yazi.git
|
cargo install --git https://github.com/sxyazi/yazi.git
|
||||||
|
|
@ -66,13 +66,32 @@ yazi
|
||||||
|
|
||||||
If you want to use your own config, copy the [config folder](https://github.com/sxyazi/yazi/tree/main/config) to `~/.config/yazi`, and modify it as you like.
|
If you want to use your own config, copy the [config folder](https://github.com/sxyazi/yazi/tree/main/config) to `~/.config/yazi`, and modify it as you like.
|
||||||
|
|
||||||
|
## Image Preview
|
||||||
|
|
||||||
|
| Platform | Protocol | Support |
|
||||||
|
| ----------- | -------------------------------------------------------------------------------- | --------------------- |
|
||||||
|
| Kitty | [Terminal graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/) | ✅ Built-in |
|
||||||
|
| WezTerm | [Terminal graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/) | ✅ Built-in |
|
||||||
|
| Konsole | [Terminal graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/) | ✅ Built-in |
|
||||||
|
| iTerm2 | [Inline Images Protocol](https://iterm2.com/documentation-images.html) | ✅ Built-in |
|
||||||
|
| Hyper | Sixel | ☑️ Überzug++ required |
|
||||||
|
| foot | Sixel | ☑️ Überzug++ required |
|
||||||
|
| X11/Wayland | Window system protocol | ☑️ Überzug++ required |
|
||||||
|
| Fallback | [Chafa](https://hpjansson.org/chafa/) | ☑️ Überzug++ required |
|
||||||
|
|
||||||
|
Yazi automatically selects the appropriate preview method for you, based on the priority from top to bottom.
|
||||||
|
That's relying on the `$TERM`, `$TERM_PROGRAM`, and `$XDG_SESSION_TYPE` variables, make sure you don't overwrite them by mistake!
|
||||||
|
|
||||||
|
For instance, if your terminal is Alacritty, which doesn't support displaying images itself, but you are running on an X11/Wayland environment,
|
||||||
|
it will automatically use the "Window system protocol" to display images -- this requires you to have [Überzug++](https://github.com/jstkdng/ueberzugpp) installed.
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- [x] Add example config for general usage, currently please see my [another repo](https://github.com/sxyazi/dotfiles/tree/main/yazi) instead
|
- [x] Add example config for general usage, currently please see my [another repo](https://github.com/sxyazi/dotfiles/tree/main/yazi) instead
|
||||||
- [x] Integration with fzf, zoxide for fast directory navigation
|
- [x] Integration with fzf, zoxide for fast directory navigation
|
||||||
- [x] Integration with fd, rg for fuzzy file searching
|
- [x] Integration with fd, rg for fuzzy file searching
|
||||||
- [x] Documentation of commands and options
|
- [x] Documentation of commands and options
|
||||||
- [ ] Support for Überzug++ for image previews with X11/wayland environment
|
- [x] Support for Überzug++ for image previews with X11/wayland environment
|
||||||
- [ ] Batch renaming support
|
- [ ] Batch renaming support
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"language":"en","flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent"],"version":"0.2"}
|
{"flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty"],"version":"0.2","language":"en"}
|
||||||
|
|
|
||||||
64
src/config/preview/adaptor.rs
Normal file
64
src/config/preview/adaptor.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub enum PreviewAdaptor {
|
||||||
|
Kitty,
|
||||||
|
Iterm2,
|
||||||
|
|
||||||
|
// Supported by Überzug++
|
||||||
|
Sixel,
|
||||||
|
X11,
|
||||||
|
Wayland,
|
||||||
|
Chafa,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PreviewAdaptor {
|
||||||
|
fn default() -> Self {
|
||||||
|
if env::var("KITTY_WINDOW_ID").is_ok() {
|
||||||
|
return Self::Kitty;
|
||||||
|
}
|
||||||
|
if env::var("KONSOLE_VERSION").is_ok() {
|
||||||
|
return Self::Kitty;
|
||||||
|
}
|
||||||
|
match env::var("TERM").unwrap_or_default().as_str() {
|
||||||
|
"xterm-kitty" => return Self::Kitty,
|
||||||
|
"wezterm" => return Self::Kitty,
|
||||||
|
"foot" => return Self::Sixel,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
match env::var("TERM_PROGRAM").unwrap_or_default().as_str() {
|
||||||
|
"iTerm.app" => return Self::Iterm2,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
match env::var("XDG_SESSION_TYPE").unwrap_or_default().as_str() {
|
||||||
|
"x11" => return Self::X11,
|
||||||
|
"wayland" => return Self::Wayland,
|
||||||
|
_ => Self::Chafa,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for PreviewAdaptor {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
match self {
|
||||||
|
PreviewAdaptor::Kitty => "kitty",
|
||||||
|
PreviewAdaptor::Iterm2 => "iterm2",
|
||||||
|
PreviewAdaptor::Sixel => "sixel",
|
||||||
|
PreviewAdaptor::X11 => "x11",
|
||||||
|
PreviewAdaptor::Wayland => "wayland",
|
||||||
|
PreviewAdaptor::Chafa => "chafa",
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PreviewAdaptor {
|
||||||
|
#[inline]
|
||||||
|
pub fn needs_ueberzug(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
PreviewAdaptor::Kitty => false,
|
||||||
|
PreviewAdaptor::Iterm2 => false,
|
||||||
|
_ => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
mod adaptor;
|
||||||
mod preview;
|
mod preview;
|
||||||
|
|
||||||
|
pub use adaptor::*;
|
||||||
pub use preview::*;
|
pub use preview::*;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use super::PreviewAdaptor;
|
||||||
use crate::config::MERGED_YAZI;
|
use crate::config::MERGED_YAZI;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct Preview {
|
pub struct Preview {
|
||||||
|
#[serde(skip)]
|
||||||
|
pub adaptor: PreviewAdaptor,
|
||||||
pub tab_size: u32,
|
pub tab_size: u32,
|
||||||
|
|
||||||
pub max_width: u32,
|
pub max_width: u32,
|
||||||
|
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
use std::io::Write;
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use base64::{engine::general_purpose, Engine};
|
|
||||||
use image::DynamicImage;
|
|
||||||
|
|
||||||
pub struct Kitty;
|
|
||||||
|
|
||||||
impl Kitty {
|
|
||||||
pub fn image_show(img: DynamicImage) -> Result<Vec<u8>> {
|
|
||||||
fn output(raw: Vec<u8>, format: u8, size: (u32, u32)) -> Result<Vec<u8>> {
|
|
||||||
let b64 = general_purpose::STANDARD.encode(raw).chars().collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let mut it = b64.chunks(4096).peekable();
|
|
||||||
let mut buf = Vec::with_capacity(b64.len() + it.len() * 50);
|
|
||||||
if let Some(first) = it.next() {
|
|
||||||
write!(
|
|
||||||
buf,
|
|
||||||
"\x1b_Ga=d\x1b\\\x1b_Ga=T,f={},s={},v={},m={};{}\x1b\\",
|
|
||||||
format,
|
|
||||||
size.0,
|
|
||||||
size.1,
|
|
||||||
it.peek().is_some() as u8,
|
|
||||||
first.iter().collect::<String>(),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
while let Some(chunk) = it.next() {
|
|
||||||
write!(
|
|
||||||
buf,
|
|
||||||
"\x1b_Gm={};{}\x1b\\",
|
|
||||||
it.peek().is_some() as u8,
|
|
||||||
chunk.iter().collect::<String>()
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
Ok(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
let size = (img.width(), img.height());
|
|
||||||
match img {
|
|
||||||
DynamicImage::ImageRgb8(v) => output(v.into_raw(), 24, size),
|
|
||||||
DynamicImage::ImageRgba8(v) => output(v.into_raw(), 32, size),
|
|
||||||
v => output(v.to_rgb8().into_raw(), 24, size),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn image_hide() -> &'static [u8; 8] { b"\x1b_Ga=d\x1b\\" }
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
mod kitty;
|
|
||||||
|
|
||||||
pub use kitty::*;
|
|
||||||
53
src/core/adaptor/adaptor.rs
Normal file
53
src/core/adaptor/adaptor.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
use std::{path::{Path, PathBuf}, sync::atomic::{AtomicBool, Ordering}};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use ratatui::prelude::Rect;
|
||||||
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
|
use super::{iterm2::Iterm2, kitty::Kitty, ueberzug::Ueberzug};
|
||||||
|
use crate::config::{preview::PreviewAdaptor, PREVIEW};
|
||||||
|
|
||||||
|
static IMAGE_SHOWN: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
static UEBERZUG: Lazy<Option<UnboundedSender<Option<(PathBuf, Rect)>>>> =
|
||||||
|
Lazy::new(|| if PREVIEW.adaptor.needs_ueberzug() { Ueberzug::start().ok() } else { None });
|
||||||
|
|
||||||
|
pub struct Adaptor;
|
||||||
|
|
||||||
|
impl Adaptor {
|
||||||
|
pub fn init() { Lazy::force(&UEBERZUG); }
|
||||||
|
|
||||||
|
pub async fn image_show(path: &Path, rect: Rect) -> Result<()> {
|
||||||
|
if IMAGE_SHOWN.swap(true, Ordering::Relaxed) {
|
||||||
|
Self::image_hide(rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
match PREVIEW.adaptor {
|
||||||
|
PreviewAdaptor::Kitty => Kitty::image_show(path, rect).await,
|
||||||
|
PreviewAdaptor::Iterm2 => Iterm2::image_show(path, rect).await,
|
||||||
|
_ => {
|
||||||
|
if let Some(tx) = &*UEBERZUG {
|
||||||
|
tx.send(Some((path.to_path_buf(), rect))).ok();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn image_hide(rect: Rect) {
|
||||||
|
if !IMAGE_SHOWN.swap(false, Ordering::Relaxed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match PREVIEW.adaptor {
|
||||||
|
PreviewAdaptor::Kitty => Kitty::image_hide(),
|
||||||
|
PreviewAdaptor::Iterm2 => Iterm2::image_hide(rect),
|
||||||
|
_ => {
|
||||||
|
if let Some(tx) = &*UEBERZUG {
|
||||||
|
tx.send(None).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/core/adaptor/image.rs
Normal file
31
src/core/adaptor/image.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use image::{imageops::FilterType, DynamicImage};
|
||||||
|
use tokio::fs;
|
||||||
|
|
||||||
|
use crate::{config::PREVIEW, misc::tty_ratio};
|
||||||
|
|
||||||
|
pub(super) struct Image;
|
||||||
|
|
||||||
|
impl Image {
|
||||||
|
pub(super) async fn resize(path: &Path, size: (u16, u16)) -> Result<DynamicImage> {
|
||||||
|
let (w, h) = {
|
||||||
|
let r = tty_ratio();
|
||||||
|
let (w, h) = ((size.0 as f64 * r.0) as u32, (size.1 as f64 * r.1) as u32);
|
||||||
|
(w.min(PREVIEW.max_width), h.min(PREVIEW.max_height))
|
||||||
|
};
|
||||||
|
|
||||||
|
let img = fs::read(path).await?;
|
||||||
|
let img = tokio::task::spawn_blocking(move || -> Result<DynamicImage> {
|
||||||
|
let img = image::load_from_memory(&img)?;
|
||||||
|
Ok(if img.width() > w || img.height() > h {
|
||||||
|
img.resize(w, h, FilterType::Triangle)
|
||||||
|
} else {
|
||||||
|
img
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(img.await??)
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/core/adaptor/iterm2.rs
Normal file
53
src/core/adaptor/iterm2.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
use std::{io::Write, path::Path};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use base64::{engine::general_purpose, Engine};
|
||||||
|
use image::{codecs::jpeg::JpegEncoder, DynamicImage};
|
||||||
|
use ratatui::prelude::Rect;
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
|
||||||
|
use super::image::Image;
|
||||||
|
use crate::ui::Term;
|
||||||
|
|
||||||
|
pub(super) struct Iterm2;
|
||||||
|
|
||||||
|
impl Iterm2 {
|
||||||
|
pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<()> {
|
||||||
|
let img = Image::resize(path, (rect.width, rect.height)).await?;
|
||||||
|
let b = Self::encode(img).await?;
|
||||||
|
|
||||||
|
Term::move_to(rect.x, rect.y).ok();
|
||||||
|
tokio::io::stdout().write_all(&b).await.ok();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(super) fn image_hide(rect: Rect) {
|
||||||
|
let s = " ".repeat(rect.width as usize);
|
||||||
|
for y in rect.top()..=rect.bottom() {
|
||||||
|
Term::move_to(rect.x, y).ok();
|
||||||
|
std::io::stdout().write_all(s.as_bytes()).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn encode(img: DynamicImage) -> Result<Vec<u8>> {
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let size = (img.width(), img.height());
|
||||||
|
|
||||||
|
let mut jpg = vec![];
|
||||||
|
JpegEncoder::new_with_quality(&mut jpg, 75).encode_image(&img)?;
|
||||||
|
|
||||||
|
let mut buf = vec![];
|
||||||
|
write!(
|
||||||
|
buf,
|
||||||
|
"\x1b]1337;File=inline=1;size={};width={}px;height={}px:{}\x07",
|
||||||
|
jpg.len(),
|
||||||
|
size.0,
|
||||||
|
size.1,
|
||||||
|
general_purpose::STANDARD.encode(&jpg)
|
||||||
|
)?;
|
||||||
|
Ok(buf)
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/core/adaptor/kitty.rs
Normal file
64
src/core/adaptor/kitty.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
use std::{io::Write, path::Path};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use base64::{engine::general_purpose, Engine};
|
||||||
|
use image::DynamicImage;
|
||||||
|
use ratatui::prelude::Rect;
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
|
||||||
|
use super::image::Image;
|
||||||
|
use crate::ui::Term;
|
||||||
|
|
||||||
|
pub(super) struct Kitty;
|
||||||
|
|
||||||
|
impl Kitty {
|
||||||
|
pub(super) async fn image_show(path: &Path, rect: Rect) -> Result<()> {
|
||||||
|
let img = Image::resize(path, (rect.width, rect.height)).await?;
|
||||||
|
let b = Self::encode(img).await?;
|
||||||
|
|
||||||
|
Term::move_to(rect.x, rect.y).ok();
|
||||||
|
tokio::io::stdout().write_all(&b).await.ok();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(super) fn image_hide() { std::io::stdout().write_all(b"\x1b\\\x1b_Ga=d\x1b\\").ok(); }
|
||||||
|
|
||||||
|
async fn encode(img: DynamicImage) -> Result<Vec<u8>> {
|
||||||
|
fn output(raw: &[u8], format: u8, size: (u32, u32)) -> Result<Vec<u8>> {
|
||||||
|
let b64 = general_purpose::STANDARD.encode(raw).chars().collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let mut it = b64.chunks(4096).peekable();
|
||||||
|
let mut buf = Vec::with_capacity(b64.len() + it.len() * 50);
|
||||||
|
if let Some(first) = it.next() {
|
||||||
|
write!(
|
||||||
|
buf,
|
||||||
|
"\x1b\\\x1b_Ga=d\x1b\\\x1b_Ga=T,f={},s={},v={},m={};{}\x1b\\",
|
||||||
|
format,
|
||||||
|
size.0,
|
||||||
|
size.1,
|
||||||
|
it.peek().is_some() as u8,
|
||||||
|
first.iter().collect::<String>(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(chunk) = it.next() {
|
||||||
|
write!(
|
||||||
|
buf,
|
||||||
|
"\x1b_Gm={};{}\x1b\\",
|
||||||
|
it.peek().is_some() as u8,
|
||||||
|
chunk.iter().collect::<String>()
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
let size = (img.width(), img.height());
|
||||||
|
tokio::task::spawn_blocking(move || match img {
|
||||||
|
DynamicImage::ImageRgb8(v) => output(v.as_raw(), 24, size),
|
||||||
|
DynamicImage::ImageRgba8(v) => output(v.as_raw(), 32, size),
|
||||||
|
v => output(v.to_rgb8().as_raw(), 24, size),
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/core/adaptor/mod.rs
Normal file
7
src/core/adaptor/mod.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
mod adaptor;
|
||||||
|
mod image;
|
||||||
|
mod iterm2;
|
||||||
|
mod kitty;
|
||||||
|
mod ueberzug;
|
||||||
|
|
||||||
|
pub use adaptor::*;
|
||||||
65
src/core/adaptor/ueberzug.rs
Normal file
65
src/core/adaptor/ueberzug.rs
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
use std::{path::PathBuf, process::Stdio};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use ratatui::prelude::Rect;
|
||||||
|
use tokio::{io::AsyncWriteExt, process::{Child, Command}, sync::mpsc::{self, UnboundedSender}};
|
||||||
|
|
||||||
|
use crate::config::PREVIEW;
|
||||||
|
|
||||||
|
pub(super) struct Ueberzug;
|
||||||
|
|
||||||
|
impl Ueberzug {
|
||||||
|
pub(super) fn start() -> Result<UnboundedSender<Option<(PathBuf, Rect)>>> {
|
||||||
|
let mut child = Self::create_demon().ok();
|
||||||
|
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while let Some(cmd) = rx.recv().await {
|
||||||
|
let exit = child.as_mut().and_then(|c| c.try_wait().ok());
|
||||||
|
if exit != Some(None) {
|
||||||
|
child = None;
|
||||||
|
}
|
||||||
|
if child.is_none() {
|
||||||
|
child = Self::create_demon().ok();
|
||||||
|
}
|
||||||
|
if let Some(c) = &mut child {
|
||||||
|
Self::send_command(c, cmd).await.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_demon() -> Result<Child> {
|
||||||
|
Ok(
|
||||||
|
Command::new("ueberzug")
|
||||||
|
.args(["layer", "-so", &PREVIEW.adaptor.to_string()])
|
||||||
|
.kill_on_drop(true)
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.spawn()?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_command(child: &mut Child, cmd: Option<(PathBuf, Rect)>) -> Result<()> {
|
||||||
|
let stdin = child.stdin.as_mut().unwrap();
|
||||||
|
if let Some((path, rect)) = cmd {
|
||||||
|
let s = format!(
|
||||||
|
r#"{{"action":"add","identifier":"yazi","x":{},"y":{},"max_width":{},"max_height":{},"path":"{}"}}{}"#,
|
||||||
|
rect.x,
|
||||||
|
rect.y,
|
||||||
|
rect.width,
|
||||||
|
rect.height,
|
||||||
|
path.to_string_lossy(),
|
||||||
|
"\n"
|
||||||
|
);
|
||||||
|
stdin.write_all(s.as_bytes()).await?;
|
||||||
|
} else {
|
||||||
|
stdin
|
||||||
|
.write_all(format!(r#"{{"action":"remove","identifier":"yazi"}}{}"#, "\n").as_bytes())
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,7 +25,7 @@ pub enum Event {
|
||||||
Pages(usize),
|
Pages(usize),
|
||||||
Mimetype(BTreeMap<PathBuf, String>),
|
Mimetype(BTreeMap<PathBuf, String>),
|
||||||
Hover(Option<File>),
|
Hover(Option<File>),
|
||||||
Preview(PathBuf, PreviewData),
|
Preview(PathBuf, String, PreviewData),
|
||||||
|
|
||||||
// Input
|
// Input
|
||||||
Select(SelectOpt, oneshot::Sender<Result<usize>>),
|
Select(SelectOpt, oneshot::Sender<Result<usize>>),
|
||||||
|
|
@ -94,8 +94,8 @@ macro_rules! emit {
|
||||||
(Hover($file:expr)) => {
|
(Hover($file:expr)) => {
|
||||||
$crate::core::Event::Hover(Some($file)).emit();
|
$crate::core::Event::Hover(Some($file)).emit();
|
||||||
};
|
};
|
||||||
(Preview($path:expr, $data:expr)) => {
|
(Preview($path:expr, $mime:expr, $data:expr)) => {
|
||||||
$crate::core::Event::Preview($path, $data).emit();
|
$crate::core::Event::Preview($path, $mime, $data).emit();
|
||||||
};
|
};
|
||||||
|
|
||||||
(Select($opt:expr)) => {{
|
(Select($opt:expr)) => {{
|
||||||
|
|
|
||||||
|
|
@ -49,20 +49,24 @@ impl Manager {
|
||||||
self.watcher.watch(to_watch);
|
self.watcher.watch(to_watch);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn preview(&mut self) -> bool {
|
pub fn preview(&mut self, show_image: bool) -> bool {
|
||||||
let hovered = if let Some(h) = self.hovered() {
|
let hovered = if let Some(h) = self.hovered() {
|
||||||
h.clone()
|
h.clone()
|
||||||
} else {
|
} else {
|
||||||
return self.active_mut().preview.reset();
|
return self.active_mut().preview.reset();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if !show_image {
|
||||||
|
self.active_mut().preview.reset_image();
|
||||||
|
}
|
||||||
|
|
||||||
if hovered.meta.is_dir() {
|
if hovered.meta.is_dir() {
|
||||||
self.active_mut().preview.go(&hovered.path, MIME_DIR);
|
self.active_mut().preview.go(&hovered.path, MIME_DIR, show_image);
|
||||||
if self.active().history(&hovered.path).is_some() {
|
if self.active().history(&hovered.path).is_some() {
|
||||||
emit!(Preview(hovered.path, PreviewData::Folder));
|
emit!(Preview(hovered.path, MIME_DIR.to_owned(), PreviewData::Folder));
|
||||||
}
|
}
|
||||||
} else if let Some(mime) = self.mimetype.get(&hovered.path).cloned() {
|
} else if let Some(mime) = self.mimetype.get(&hovered.path).cloned() {
|
||||||
self.active_mut().preview.go(&hovered.path, &mime);
|
self.active_mut().preview.go(&hovered.path, &mime, show_image);
|
||||||
} else {
|
} else {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Ok(mimes) = external::file(&[hovered.path]).await {
|
if let Ok(mimes) = external::file(&[hovered.path]).await {
|
||||||
|
|
@ -300,11 +304,10 @@ impl Manager {
|
||||||
tasks.precache_video(&mimes);
|
tasks.precache_video(&mimes);
|
||||||
|
|
||||||
self.mimetype.extend(mimes);
|
self.mimetype.extend(mimes);
|
||||||
self.preview();
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_preview(&mut self, path: PathBuf, data: PreviewData) -> bool {
|
pub fn update_preview(&mut self, path: PathBuf, mime: String, data: PreviewData) -> bool {
|
||||||
let hovered = if let Some(ref h) = self.current().hovered {
|
let hovered = if let Some(ref h) = self.current().hovered {
|
||||||
h.path()
|
h.path()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -316,7 +319,7 @@ impl Manager {
|
||||||
}
|
}
|
||||||
|
|
||||||
let preview = &mut self.active_mut().preview;
|
let preview = &mut self.active_mut().preview;
|
||||||
preview.path = path;
|
preview.lock = Some((path, mime));
|
||||||
preview.data = data;
|
preview.data = data;
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,4 +22,4 @@ pub const ALL_RATIO: u32 = PARENT_RATIO + CURRENT_RATIO + PREVIEW_RATIO;
|
||||||
pub const DIR_PADDING: u16 = 2;
|
pub const DIR_PADDING: u16 = 2;
|
||||||
|
|
||||||
pub const PREVIEW_BORDER: u16 = 2;
|
pub const PREVIEW_BORDER: u16 = 2;
|
||||||
pub const PREVIEW_PADDING: u16 = 2;
|
pub const PREVIEW_MARGIN: u16 = 2;
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,22 @@
|
||||||
use std::{fs::File, io::{BufRead, BufReader}, path::{Path, PathBuf}, sync::OnceLock};
|
use std::{fs::File, io::{BufRead, BufReader}, mem, path::{Path, PathBuf}, sync::OnceLock};
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use image::imageops::FilterType;
|
use ratatui::prelude::Rect;
|
||||||
use syntect::{easy::HighlightFile, highlighting::{Theme, ThemeSet}, parsing::SyntaxSet, util::as_24_bit_terminal_escaped};
|
use syntect::{easy::HighlightFile, highlighting::{Theme, ThemeSet}, parsing::SyntaxSet, util::as_24_bit_terminal_escaped};
|
||||||
use tokio::{fs, task::JoinHandle};
|
use tokio::{fs, task::JoinHandle};
|
||||||
|
|
||||||
use super::{ALL_RATIO, PREVIEW_BORDER, PREVIEW_PADDING, PREVIEW_RATIO};
|
use super::{ALL_RATIO, CURRENT_RATIO, PARENT_RATIO, PREVIEW_BORDER, PREVIEW_MARGIN, PREVIEW_RATIO};
|
||||||
use crate::{config::{PREVIEW, THEME}, core::{adapter::Kitty, external, files::{Files, FilesOp}, tasks::Precache}, emit, misc::{tty_ratio, tty_size, MimeKind}};
|
use crate::{config::{PREVIEW, THEME}, core::{adaptor::Adaptor, external, files::{Files, FilesOp}, tasks::Precache}, emit, misc::{tty_size, MimeKind}};
|
||||||
|
|
||||||
static SYNTECT_SYNTAX: OnceLock<SyntaxSet> = OnceLock::new();
|
static SYNTECT_SYNTAX: OnceLock<SyntaxSet> = OnceLock::new();
|
||||||
static SYNTECT_THEME: OnceLock<Theme> = OnceLock::new();
|
static SYNTECT_THEME: OnceLock<Theme> = OnceLock::new();
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Default)]
|
||||||
pub struct Preview {
|
pub struct Preview {
|
||||||
pub path: PathBuf,
|
pub lock: Option<(PathBuf, String)>,
|
||||||
pub data: PreviewData,
|
pub data: PreviewData,
|
||||||
handle: Option<JoinHandle<()>>,
|
|
||||||
|
handle: Option<JoinHandle<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
|
|
@ -24,49 +25,70 @@ pub enum PreviewData {
|
||||||
None,
|
None,
|
||||||
Folder,
|
Folder,
|
||||||
Text(String),
|
Text(String),
|
||||||
Image(Vec<u8>),
|
Image,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Preview {
|
impl Preview {
|
||||||
pub fn new() -> Self {
|
fn rect() -> Rect {
|
||||||
Self { path: Default::default(), data: Default::default(), handle: Default::default() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn size() -> (u16, u16) {
|
|
||||||
let s = tty_size();
|
let s = tty_size();
|
||||||
let col = (s.ws_col as u32 * PREVIEW_RATIO / ALL_RATIO) as u16;
|
|
||||||
(col.saturating_sub(PREVIEW_BORDER), s.ws_row.saturating_sub(PREVIEW_PADDING))
|
let x = (s.ws_col as u32 * (PARENT_RATIO + CURRENT_RATIO) / ALL_RATIO) as u16;
|
||||||
|
let width = (s.ws_col as u32 * PREVIEW_RATIO / ALL_RATIO) as u16;
|
||||||
|
|
||||||
|
Rect {
|
||||||
|
x: x.saturating_add(PREVIEW_BORDER / 2),
|
||||||
|
y: PREVIEW_MARGIN / 2,
|
||||||
|
width: width.saturating_sub(PREVIEW_BORDER),
|
||||||
|
height: s.ws_row.saturating_sub(PREVIEW_MARGIN),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn go(&mut self, path: &Path, mime: &str) {
|
pub fn go(&mut self, path: &Path, mime: &str, show_image: bool) {
|
||||||
if let Some(handle) = self.handle.take() {
|
let kind = MimeKind::new(&mime);
|
||||||
handle.abort();
|
if !show_image && matches!(kind, MimeKind::Image | MimeKind::Video) {
|
||||||
|
return;
|
||||||
|
} else if self.same(path, mime) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
self.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
let (path, mime) = (path.to_path_buf(), mime.to_owned());
|
let (path, mime) = (path.to_path_buf(), mime.to_owned());
|
||||||
self.handle = Some(tokio::spawn(async move {
|
self.handle = Some(tokio::spawn(async move {
|
||||||
let result = match MimeKind::new(&mime) {
|
let result = match kind {
|
||||||
MimeKind::Dir => Self::folder(&path).await,
|
MimeKind::Dir => Self::folder(&path).await,
|
||||||
MimeKind::JSON => Self::json(&path).await.map(PreviewData::Text),
|
MimeKind::JSON => Self::json(&path).await.map(PreviewData::Text),
|
||||||
MimeKind::Text => Self::highlight(&path).await.map(PreviewData::Text),
|
MimeKind::Text => Self::highlight(&path).await.map(PreviewData::Text),
|
||||||
MimeKind::Image => Self::image(&path).await.map(PreviewData::Image),
|
MimeKind::Image => Self::image(&path).await,
|
||||||
MimeKind::Video => Self::video(&path).await.map(PreviewData::Image),
|
MimeKind::Video => Self::video(&path).await,
|
||||||
MimeKind::Archive => Self::archive(&path).await.map(PreviewData::Text),
|
MimeKind::Archive => Self::archive(&path).await.map(PreviewData::Text),
|
||||||
MimeKind::Others => Err(anyhow!("Unsupported mimetype: {}", mime)),
|
MimeKind::Others => Err(anyhow!("Unsupported mimetype: {}", mime)),
|
||||||
};
|
};
|
||||||
|
|
||||||
emit!(Preview(path, result.unwrap_or_default()));
|
emit!(Preview(path, mime, result.unwrap_or_default()));
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(&mut self) -> bool {
|
pub fn reset(&mut self) -> bool {
|
||||||
if self.path == PathBuf::default() {
|
self.handle.take().map(|h| h.abort());
|
||||||
return false;
|
Adaptor::image_hide(Self::rect());
|
||||||
}
|
|
||||||
|
|
||||||
self.path = Default::default();
|
self.lock = None;
|
||||||
self.data = Default::default();
|
!matches!(
|
||||||
true
|
mem::replace(&mut self.data, PreviewData::None),
|
||||||
|
PreviewData::None | PreviewData::Image
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset_image(&mut self) -> bool {
|
||||||
|
self.handle.take().map(|h| h.abort());
|
||||||
|
Adaptor::image_hide(Self::rect());
|
||||||
|
|
||||||
|
if matches!(self.data, PreviewData::Image) {
|
||||||
|
self.lock = None;
|
||||||
|
self.data = PreviewData::None;
|
||||||
|
}
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn folder(path: &Path) -> Result<PreviewData> {
|
pub async fn folder(path: &Path) -> Result<PreviewData> {
|
||||||
|
|
@ -78,32 +100,17 @@ impl Preview {
|
||||||
Ok(PreviewData::Folder)
|
Ok(PreviewData::Folder)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn image(mut path: &Path) -> Result<Vec<u8>> {
|
pub async fn image(mut path: &Path) -> Result<PreviewData> {
|
||||||
let cache = Precache::cache(path);
|
let cache = Precache::cache(path);
|
||||||
if fs::metadata(&cache).await.is_ok() {
|
if fs::metadata(&cache).await.is_ok() {
|
||||||
path = cache.as_path();
|
path = cache.as_path();
|
||||||
}
|
}
|
||||||
|
|
||||||
let (w, h) = {
|
Adaptor::image_show(path, Self::rect()).await?;
|
||||||
let r = tty_ratio();
|
Ok(PreviewData::Image)
|
||||||
let (w, h) = Self::size();
|
|
||||||
let (w, h) = ((w as f64 * r.0) as u32, (h as f64 * r.1) as u32);
|
|
||||||
(w.min(PREVIEW.max_width), h.min(PREVIEW.max_height))
|
|
||||||
};
|
|
||||||
|
|
||||||
let img = fs::read(path).await?;
|
|
||||||
tokio::task::spawn_blocking(move || -> Result<Vec<u8>> {
|
|
||||||
let img = image::load_from_memory(&img)?;
|
|
||||||
Kitty::image_show(if img.width() > w || img.height() > h {
|
|
||||||
img.resize(w, h, FilterType::Triangle)
|
|
||||||
} else {
|
|
||||||
img
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn video(path: &Path) -> Result<Vec<u8>> {
|
pub async fn video(path: &Path) -> Result<PreviewData> {
|
||||||
let cache = Precache::cache(path);
|
let cache = Precache::cache(path);
|
||||||
if fs::metadata(&cache).await.is_err() {
|
if fs::metadata(&cache).await.is_err() {
|
||||||
external::ffmpegthumbnailer(path, &cache).await?;
|
external::ffmpegthumbnailer(path, &cache).await?;
|
||||||
|
|
@ -117,7 +124,7 @@ impl Preview {
|
||||||
external::jq(path)
|
external::jq(path)
|
||||||
.await?
|
.await?
|
||||||
.lines()
|
.lines()
|
||||||
.take(Self::size().1 as usize)
|
.take(Self::rect().height as usize)
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\n"),
|
.join("\n"),
|
||||||
)
|
)
|
||||||
|
|
@ -128,7 +135,7 @@ impl Preview {
|
||||||
external::lsar(path)
|
external::lsar(path)
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.take(Self::size().1 as usize)
|
.take(Self::rect().height as usize)
|
||||||
.map(|f| f.name)
|
.map(|f| f.name)
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\n"),
|
.join("\n"),
|
||||||
|
|
@ -153,7 +160,7 @@ impl Preview {
|
||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
let mut buf = String::new();
|
let mut buf = String::new();
|
||||||
|
|
||||||
let mut i = Self::size().1 as usize;
|
let mut i = Self::rect().height as usize;
|
||||||
while i > 0 && h.reader.read_line(&mut line)? > 0 {
|
while i > 0 && h.reader.read_line(&mut line)? > 0 {
|
||||||
i -= 1;
|
i -= 1;
|
||||||
line = line.replace('\t', &spaces);
|
line = line.replace('\t', &spaces);
|
||||||
|
|
@ -168,3 +175,15 @@ impl Preview {
|
||||||
.await?
|
.await?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Preview {
|
||||||
|
#[inline]
|
||||||
|
pub fn same(&self, path: &Path, mime: &str) -> bool {
|
||||||
|
self.lock.as_ref().map(|(p, m)| p == path && m == mime).unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn same_path(&self, path: &Path) -> bool {
|
||||||
|
self.lock.as_ref().map(|(p, _)| p == path).unwrap_or(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ impl Tab {
|
||||||
search: None,
|
search: None,
|
||||||
|
|
||||||
history: Default::default(),
|
history: Default::default(),
|
||||||
preview: Preview::new(),
|
preview: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
pub mod adapter;
|
pub mod adaptor;
|
||||||
mod blocker;
|
mod blocker;
|
||||||
mod event;
|
mod event;
|
||||||
pub mod external;
|
pub mod external;
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ impl Tasks {
|
||||||
|
|
||||||
pub fn toggle(&mut self) -> bool {
|
pub fn toggle(&mut self) -> bool {
|
||||||
self.visible = !self.visible;
|
self.visible = !self.visible;
|
||||||
|
emit!(Hover); // Show/hide preview for images
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ impl Which {
|
||||||
.filter(|s| s.on.len() > 1 && s.on[0] == *key)
|
.filter(|s| s.on.len() > 1 && s.on[0] == *key)
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect();
|
.collect();
|
||||||
self.visible = true;
|
self.switch(true);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,16 +37,22 @@ impl Which {
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if self.cands.is_empty() {
|
if self.cands.is_empty() {
|
||||||
self.visible = false;
|
self.switch(false);
|
||||||
} else if self.cands.len() == 1 {
|
} else if self.cands.len() == 1 {
|
||||||
self.visible = false;
|
self.switch(false);
|
||||||
emit!(Ctrl(self.cands.remove(0), self.layer));
|
emit!(Ctrl(self.cands.remove(0), self.layer));
|
||||||
} else if let Some(i) = self.cands.iter().position(|c| c.on.len() == self.times + 1) {
|
} else if let Some(i) = self.cands.iter().position(|c| c.on.len() == self.times + 1) {
|
||||||
|
self.switch(false);
|
||||||
emit!(Ctrl(self.cands.remove(i), self.layer));
|
emit!(Ctrl(self.cands.remove(i), self.layer));
|
||||||
self.visible = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.times += 1;
|
self.times += 1;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn switch(&mut self, state: bool) {
|
||||||
|
self.visible = state;
|
||||||
|
emit!(Hover); // Show/hide preview for images
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
use core::adaptor;
|
||||||
|
|
||||||
use ui::App;
|
use ui::App;
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
|
|
@ -11,5 +13,7 @@ async fn main() -> anyhow::Result<()> {
|
||||||
|
|
||||||
config::init();
|
config::init();
|
||||||
|
|
||||||
|
adaptor::Adaptor::init();
|
||||||
|
|
||||||
App::run().await
|
App::run().await
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ impl App {
|
||||||
let _ = term.draw(|f| {
|
let _ = term.draw(|f| {
|
||||||
f.render_widget(Root::new(&mut self.cx), f.size());
|
f.render_widget(Root::new(&mut self.cx), f.size());
|
||||||
|
|
||||||
if let Some((x, y)) = self.cx.cursor {
|
if let Some((x, y)) = self.cx.cursor() {
|
||||||
f.set_cursor(x, y);
|
f.set_cursor(x, y);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -64,7 +64,7 @@ impl App {
|
||||||
|
|
||||||
fn dispatch_resize(&mut self) {
|
fn dispatch_resize(&mut self) {
|
||||||
self.cx.manager.current_mut().set_page(true);
|
self.cx.manager.current_mut().set_page(true);
|
||||||
self.cx.manager.preview();
|
self.cx.manager.preview(self.cx.image_layer());
|
||||||
emit!(Render);
|
emit!(Render);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,17 +123,17 @@ impl App {
|
||||||
Event::Mimetype(mimes) => {
|
Event::Mimetype(mimes) => {
|
||||||
if manager.update_mimetype(mimes, tasks) {
|
if manager.update_mimetype(mimes, tasks) {
|
||||||
emit!(Render);
|
emit!(Render);
|
||||||
|
self.cx.manager.preview(self.cx.image_layer());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::Hover(file) => {
|
Event::Hover(file) => {
|
||||||
let mut b = file.map(|f| manager.current_mut().hover_force(f)).unwrap_or(false);
|
if file.map(|f| manager.current_mut().hover_force(f)).unwrap_or(false) {
|
||||||
b |= manager.preview();
|
|
||||||
if b {
|
|
||||||
emit!(Render);
|
emit!(Render);
|
||||||
}
|
}
|
||||||
|
self.cx.manager.preview(self.cx.image_layer());
|
||||||
}
|
}
|
||||||
Event::Preview(file, data) => {
|
Event::Preview(path, mime, data) => {
|
||||||
manager.update_preview(file, data);
|
manager.update_preview(path, mime, data);
|
||||||
emit!(Render);
|
emit!(Render);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
use crate::{config::keymap::KeymapLayer, core::{input::Input, manager::Manager, select::Select, tasks::Tasks, which::Which, Position}, misc::tty_size};
|
use crate::{config::keymap::KeymapLayer, core::{input::Input, manager::Manager, select::Select, tasks::Tasks, which::Which, Position}, misc::tty_size};
|
||||||
|
|
||||||
pub struct Ctx {
|
pub struct Ctx {
|
||||||
pub cursor: Option<(u16, u16)>,
|
|
||||||
|
|
||||||
pub manager: Manager,
|
pub manager: Manager,
|
||||||
pub which: Which,
|
pub which: Which,
|
||||||
pub select: Select,
|
pub select: Select,
|
||||||
|
|
@ -13,8 +11,6 @@ pub struct Ctx {
|
||||||
impl Ctx {
|
impl Ctx {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
cursor: None,
|
|
||||||
|
|
||||||
manager: Manager::new(),
|
manager: Manager::new(),
|
||||||
which: Default::default(),
|
which: Default::default(),
|
||||||
select: Default::default(),
|
select: Default::default(),
|
||||||
|
|
@ -23,6 +19,14 @@ impl Ctx {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn cursor(&self) -> Option<(u16, u16)> {
|
||||||
|
if self.input.visible {
|
||||||
|
return Some(self.input.cursor());
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn layer(&self) -> KeymapLayer {
|
pub fn layer(&self) -> KeymapLayer {
|
||||||
if self.which.visible {
|
if self.which.visible {
|
||||||
|
|
@ -38,6 +42,15 @@ impl Ctx {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn image_layer(&self) -> bool {
|
||||||
|
match self.layer() {
|
||||||
|
KeymapLayer::Which => false,
|
||||||
|
KeymapLayer::Tasks => false,
|
||||||
|
_ => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn position(&self, pos: Position) -> Position {
|
pub fn position(&self, pos: Position) -> Position {
|
||||||
match pos {
|
match pos {
|
||||||
Position::Top => Position::Coords((tty_size().ws_col / 2).saturating_sub(25), 2),
|
Position::Top => Position::Coords((tty_size().ws_col / 2).saturating_sub(25), 2),
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
use std::io::{stdout, Write};
|
|
||||||
|
|
||||||
use ansi_to_tui::IntoText;
|
use ansi_to_tui::IntoText;
|
||||||
use ratatui::{buffer::Buffer, layout::Rect, widgets::{Paragraph, Widget}};
|
use ratatui::{buffer::Buffer, layout::Rect, widgets::{Clear, Paragraph, Widget}};
|
||||||
|
|
||||||
use super::Folder;
|
use super::Folder;
|
||||||
use crate::{core::{adapter::Kitty, manager::PreviewData}, ui::{Ctx, Term}};
|
use crate::{core::manager::{PreviewData, PREVIEW_BORDER}, ui::Ctx};
|
||||||
|
|
||||||
pub struct Preview<'a> {
|
pub struct Preview<'a> {
|
||||||
cx: &'a Ctx,
|
cx: &'a Ctx,
|
||||||
|
|
@ -16,32 +14,32 @@ impl<'a> Preview<'a> {
|
||||||
|
|
||||||
impl<'a> Widget for Preview<'a> {
|
impl<'a> Widget for Preview<'a> {
|
||||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
if self.cx.input.visible || self.cx.select.visible || self.cx.tasks.visible {
|
Clear.render(
|
||||||
stdout().write(Kitty::image_hide()).ok();
|
Rect {
|
||||||
return;
|
x: area.x,
|
||||||
}
|
y: area.y,
|
||||||
|
width: area.width + PREVIEW_BORDER / 2,
|
||||||
|
height: area.height,
|
||||||
|
},
|
||||||
|
buf,
|
||||||
|
);
|
||||||
|
|
||||||
let manager = &self.cx.manager;
|
let manager = &self.cx.manager;
|
||||||
let hovered = if let Some(h) = manager.hovered() {
|
let hovered = if let Some(h) = manager.hovered() {
|
||||||
h.clone()
|
h.path()
|
||||||
} else {
|
} else {
|
||||||
stdout().write(Kitty::image_hide()).ok();
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let preview = manager.active().preview();
|
let preview = manager.active().preview();
|
||||||
if preview.path != hovered.path {
|
if !preview.same_path(&hovered) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !matches!(preview.data, PreviewData::Image(_)) {
|
|
||||||
stdout().write(Kitty::image_hide()).ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
match &preview.data {
|
match &preview.data {
|
||||||
PreviewData::None => {}
|
PreviewData::None => {}
|
||||||
PreviewData::Folder => {
|
PreviewData::Folder => {
|
||||||
if let Some(folder) = manager.active().history(&hovered.path) {
|
if let Some(folder) = manager.active().history(&hovered) {
|
||||||
Folder::new(self.cx, folder).with_preview(true).render(area, buf);
|
Folder::new(self.cx, folder).with_preview(true).render(area, buf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -49,10 +47,7 @@ impl<'a> Widget for Preview<'a> {
|
||||||
let p = Paragraph::new(s.as_bytes().into_text().unwrap());
|
let p = Paragraph::new(s.as_bytes().into_text().unwrap());
|
||||||
p.render(area, buf);
|
p.render(area, buf);
|
||||||
}
|
}
|
||||||
PreviewData::Image(b) => {
|
PreviewData::Image => {}
|
||||||
Term::move_to(area.x, area.y).ok();
|
|
||||||
stdout().write(b).ok();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,11 @@ use ratatui::{buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, wid
|
||||||
use super::{header, manager, status, tasks, which::Which, Ctx, Input, Select};
|
use super::{header, manager, status, tasks, which::Which, Ctx, Input, Select};
|
||||||
|
|
||||||
pub struct Root<'a> {
|
pub struct Root<'a> {
|
||||||
cx: &'a mut Ctx,
|
cx: &'a Ctx,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Root<'a> {
|
impl<'a> Root<'a> {
|
||||||
pub fn new(cx: &'a mut Ctx) -> Self { Self { cx } }
|
pub fn new(cx: &'a Ctx) -> Self { Self { cx } }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Widget for Root<'a> {
|
impl<'a> Widget for Root<'a> {
|
||||||
|
|
@ -31,9 +31,6 @@ impl<'a> Widget for Root<'a> {
|
||||||
|
|
||||||
if self.cx.input.visible {
|
if self.cx.input.visible {
|
||||||
Input::new(self.cx).render(area, buf);
|
Input::new(self.cx).render(area, buf);
|
||||||
self.cx.cursor = Some(self.cx.input.cursor());
|
|
||||||
} else {
|
|
||||||
self.cx.cursor = None;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.cx.which.visible {
|
if self.cx.which.visible {
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ use super::Side;
|
||||||
use crate::ui::Ctx;
|
use crate::ui::Ctx;
|
||||||
|
|
||||||
pub struct Which<'a> {
|
pub struct Which<'a> {
|
||||||
cx: &'a mut Ctx,
|
cx: &'a Ctx,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Which<'a> {
|
impl<'a> Which<'a> {
|
||||||
pub fn new(cx: &'a mut Ctx) -> Self { Self { cx } }
|
pub fn new(cx: &'a Ctx) -> Self { Self { cx } }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Which<'_> {
|
impl Widget for Which<'_> {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue