diff --git a/Cargo.lock b/Cargo.lock index 3e3d3346..e6ebbc70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -158,15 +158,6 @@ dependencies = [ "libc", ] -[[package]] -name = "ansi_colours" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14eec43e0298190790f41679fe69ef7a829d2a2ddd78c8c00339e84710e435fe" -dependencies = [ - "rgb", -] - [[package]] name = "anstream" version = "1.0.0" @@ -413,6 +404,16 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + [[package]] name = "base64ct" version = "1.8.3" @@ -461,6 +462,18 @@ dependencies = [ "core2", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block" version = "0.1.6" @@ -506,11 +519,31 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + [[package]] name = "bytemuck" version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] name = "byteorder" @@ -814,18 +847,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "console" -version = "0.15.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" -dependencies = [ - "encode_unicode", - "libc", - "once_cell", - "windows-sys 0.59.0", -] - [[package]] name = "const-oid" version = "0.9.6" @@ -1006,19 +1027,6 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "crossterm" -version = "0.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" -dependencies = [ - "bitflags 2.11.0", - "crossterm_winapi", - "parking_lot", - "rustix 0.38.44", - "winapi", -] - [[package]] name = "crossterm" version = "0.29.0" @@ -1378,12 +1386,6 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -[[package]] -name = "encode_unicode" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - [[package]] name = "encoding_rs" version = "0.8.35" @@ -1500,6 +1502,12 @@ dependencies = [ "regex", ] +[[package]] +name = "fast-srgb8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" + [[package]] name = "fastrand" version = "2.3.0" @@ -1664,6 +1672,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futf" version = "0.1.5" @@ -2458,6 +2472,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "icy_sixel" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85518b9086bf01117761b90e7691c0ef3236fa8adfb1fb44dd248fe5f87215d5" +dependencies = [ + "quantette", + "thiserror 2.0.18", +] + [[package]] name = "id-arena" version = "2.3.0" @@ -3223,12 +3247,6 @@ dependencies = [ "libc", ] -[[package]] -name = "make-cmd" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8ca8afbe8af1785e09636acb5a41e08a765f5f0340568716c18a8700ba3c0d3" - [[package]] name = "malloc_buf" version = "0.0.6" @@ -4042,6 +4060,21 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ordered-float" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" +dependencies = [ + "num-traits", +] + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "owned_ttf_parser" version = "0.25.1" @@ -4051,6 +4084,30 @@ dependencies = [ "ttf-parser", ] +[[package]] +name = "palette" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +dependencies = [ + "bytemuck", + "fast-srgb8", + "libm", + "palette_derive", +] + +[[package]] +name = "palette_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "parking" version = "2.2.1" @@ -4517,6 +4574,26 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "quantette" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c98fecda8b16396ff9adac67644a523dd1778c42b58606a29df5c31ca925d174" +dependencies = [ + "bitvec", + "bytemuck", + "image", + "libm", + "num-traits", + "ordered-float 5.3.0", + "palette", + "rand 0.9.2", + "rand_xoshiro", + "rayon", + "ref-cast", + "wide", +] + [[package]] name = "quick-error" version = "2.0.1" @@ -4628,6 +4705,12 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -4714,6 +4797,15 @@ dependencies = [ "rand 0.9.2", ] +[[package]] +name = "rand_xoshiro" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" +dependencies = [ + "rand_core 0.9.5", +] + [[package]] name = "ratatui" version = "0.30.0" @@ -4755,11 +4847,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" dependencies = [ "cfg-if", - "crossterm 0.29.0", + "crossterm", "instability", "ratatui-core", ] +[[package]] +name = "ratatui-image" +version = "10.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c57add959ab80c9a92be620fa6f8e4a64f7c014829250ba78862e8d81a903cb5" +dependencies = [ + "base64-simd", + "icy_sixel", + "image", + "rand 0.8.5", + "ratatui", + "rustix 0.38.44", + "thiserror 1.0.69", + "windows 0.58.0", +] + [[package]] name = "ratatui-macros" version = "0.7.0" @@ -4913,6 +5021,26 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "regex" version = "1.12.3" @@ -5028,9 +5156,6 @@ name = "rgb" version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" -dependencies = [ - "bytemuck", -] [[package]] name = "ring" @@ -5285,6 +5410,15 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "safe_arch" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "629516c85c29fe757770fa03f2074cf1eac43d44c02a3de9fc2ef7b0e207dfdd" +dependencies = [ + "bytemuck", +] + [[package]] name = "same-file" version = "1.0.6" @@ -5585,24 +5719,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" -[[package]] -name = "sixel-rs" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfa95c014543113a192d906e5971d0c8d1e8b4cc1e61026539687a7016644ce5" -dependencies = [ - "sixel-sys", -] - -[[package]] -name = "sixel-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb46e0cd5569bf910390844174a5a99d52dd40681fff92228d221d9f8bf87dea" -dependencies = [ - "make-cmd", -] - [[package]] name = "slab" version = "0.4.12" @@ -5718,7 +5834,7 @@ dependencies = [ "clap_complete", "clipboard-win", "config_parser2", - "crossterm 0.29.0", + "crossterm", "daemonize", "dirs-next", "flume", @@ -5737,6 +5853,7 @@ dependencies = [ "parking_lot", "rand 0.10.0", "ratatui", + "ratatui-image", "regex", "reqwest 0.13.2", "rspotify", @@ -5752,7 +5869,6 @@ dependencies = [ "ttl_cache", "unicode-bidi", "vergen", - "viuer", "which 8.0.2", "windows 0.58.0", "winit", @@ -6027,6 +6143,12 @@ dependencies = [ "version-compare 0.2.1", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "target-lexicon" version = "0.13.3" @@ -6069,15 +6191,6 @@ dependencies = [ "utf-8", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "terminfo" version = "0.9.0" @@ -6120,7 +6233,7 @@ dependencies = [ "nix", "num-derive", "num-traits", - "ordered-float", + "ordered-float 4.6.0", "pest", "pest_derive", "phf", @@ -6789,21 +6902,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] -name = "viuer" -version = "0.9.2" +name = "vsimd" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ae7c6870b98c838123f22cac9a594cbe2d74ea48d79271c08f8c9e680b40fac" -dependencies = [ - "ansi_colours", - "base64", - "console", - "crossterm 0.28.1", - "image", - "lazy_static", - "sixel-rs", - "tempfile", - "termcolor", -] +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" [[package]] name = "vtparse" @@ -7148,7 +7250,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" dependencies = [ "log", - "ordered-float", + "ordered-float 4.6.0", "strsim", "thiserror 1.0.69", "wezterm-dynamic-derive", @@ -7199,6 +7301,16 @@ dependencies = [ "libc", ] +[[package]] +name = "wide" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13ca908d26e4786149c48efcf6c0ea09ab0e06d1fe3c17dc1b4b0f1ca4a7e788" +dependencies = [ + "bytemuck", + "safe_arch", +] + [[package]] name = "winapi" version = "0.3.9" @@ -7879,6 +7991,15 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x11-dl" version = "2.21.0" diff --git a/README.md b/README.md index 08fbc95a..6b3174a0 100644 --- a/README.md +++ b/README.md @@ -245,18 +245,11 @@ To enable image rendering, build with the `image` feature (disabled by default): cargo install spotify_player --features image ``` -Full-resolution images are supported in [Kitty](https://sw.kovidgoyal.net/kitty/graphics-protocol/) and [iTerm2](https://iterm2.com/documentation-images.html). Other terminals display images as [block characters](https://en.wikipedia.org/wiki/Block_Elements). - -To use sixel graphics, build with the `sixel` feature (also enables `image`): - -```shell -cargo install spotify_player --features sixel -``` +Image rendering is powered by [`ratatui-image`](https://github.com/benjajaja/ratatui-image), which auto-detects the terminal's graphics protocol (Kitty, iTerm2, Sixel) on startup. Terminals without any graphics protocol support fall back to [block characters](https://en.wikipedia.org/wiki/Block_Elements). **Notes**: -- Not all terminals supported by [libsixel](https://github.com/saitoha/libsixel) are supported by `spotify_player` (see [viuer supported terminals](https://github.com/atanunq/viuer/blob/dc81f44a97727e04be0b000712e9233c92116ff8/src/printer/sixel.rs#L83-L95)). -- Sixel images may scale oddly; adjust `cover_img_scale` for best results. +- Protocol detection queries the terminal via stdio. In nested terminals (e.g. Neovim's floating terminal), the query does not reach the outer terminal emulator, so the protocol falls back to block characters. Image rendering examples: @@ -268,7 +261,7 @@ Image rendering examples: ![kitty](https://user-images.githubusercontent.com/40011582/172967028-8cfb2daa-1642-499a-a5bf-8ed77f2b3fac.png) -- Sixel (`foot` terminal, `cover_img_scale=1.8`): +- Sixel (`foot` terminal): ![sixel](https://user-images.githubusercontent.com/40011582/219880331-58ac1c30-bbb0-4c99-a6cc-e5b7c9c81455.png) diff --git a/docs/config.md b/docs/config.md index 475696fc..b162f596 100644 --- a/docs/config.md +++ b/docs/config.md @@ -66,7 +66,6 @@ spotify_player -o device.volume=80 -o theme=dracula | `genre_num` | Max number of genres to display in playback text. | `2` | | `cover_img_length` | Cover image length (requires `image` feature). | `9` | | `cover_img_width` | Cover image width (requires `image` feature). | `5` | -| `cover_img_scale` | Cover image scale (requires `image` feature). | `1.0` | | `cover_img_pixels` | Pixels per side for cover image (requires `pixelate` feature). | `16` | | `seek_duration_secs` | Seek duration in seconds for seek commands. | `5` | | `sort_artist_albums_by_type` | Sort albums by type on artist pages. | `false` | diff --git a/spotify_player/Cargo.toml b/spotify_player/Cargo.toml index 3a945344..8664ddde 100644 --- a/spotify_player/Cargo.toml +++ b/spotify_player/Cargo.toml @@ -42,8 +42,6 @@ tracing = "0.1.44" tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } backtrace = "0.3.76" souvlaki = { version = "0.8.3", optional = true } -# Upgrade viuer to the latest version. Need to resolve the freezing issue in https://github.com/aome510/spotify-player/issues/899 beforehand -viuer = { version = "=0.9.2", optional = true } image = { version = "0.25.10", optional = true } notify-rust = { version = "4.12.0", optional = true, default-features = false, features = [ "d", @@ -63,6 +61,7 @@ unicode-bidi = "0.3.18" futures = "0.3.32" # fix for https://github.com/aome510/spotify-player/issues/914 vergen = "=9.0.6" +ratatui-image = { version = "10.0.6", optional = true, default-features = false, features = ["crossterm"] } [target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies.winit] version = "0.30.13" @@ -92,8 +91,8 @@ sdl-backend = ["streaming", "librespot-playback/sdl-backend"] gstreamer-backend = ["streaming", "librespot-playback/gstreamer-backend"] streaming = ["librespot-playback", "librespot-connect", "rustfft"] media-control = ["souvlaki", "winit", "windows"] -image = ["viuer", "dep:image"] -sixel = ["image", "viuer/sixel"] +image = ["ratatui-image", "dep:image"] +sixel = ["image"] pixelate = ["image"] notify = ["notify-rust"] daemon = ["daemonize", "streaming"] diff --git a/spotify_player/src/cli/handlers.rs b/spotify_player/src/cli/handlers.rs index 7396b69c..549e3d0b 100644 --- a/spotify_player/src/cli/handlers.rs +++ b/spotify_player/src/cli/handlers.rs @@ -377,7 +377,7 @@ fn print_features() { print_feature!("streaming"); print_feature!("media-control"); print_feature!("image"); - print_feature!("viuer"); + print_feature!("ratatui-image"); print_feature!("sixel"); print_feature!("pixelate"); print_feature!("notify"); diff --git a/spotify_player/src/config/mod.rs b/spotify_player/src/config/mod.rs index 1987d900..6a1453ae 100644 --- a/spotify_player/src/config/mod.rs +++ b/spotify_player/src/config/mod.rs @@ -103,8 +103,6 @@ pub struct AppConfig { pub cover_img_length: usize, #[cfg(feature = "image")] pub cover_img_width: usize, - #[cfg(feature = "image")] - pub cover_img_scale: f32, #[cfg(feature = "pixelate")] pub cover_img_pixels: u32, @@ -351,8 +349,6 @@ impl Default for AppConfig { cover_img_length: 9, #[cfg(feature = "image")] cover_img_width: 5, - #[cfg(feature = "image")] - cover_img_scale: 1.0, #[cfg(feature = "pixelate")] cover_img_pixels: 16, diff --git a/spotify_player/src/main.rs b/spotify_player/src/main.rs index 27dfe8f2..f7cf5312 100644 --- a/spotify_player/src/main.rs +++ b/spotify_player/src/main.rs @@ -97,17 +97,6 @@ fn init_logging( #[tokio::main] async fn start_app(state: &state::SharedState) -> Result<()> { - if !state.is_daemon { - #[cfg(feature = "image")] - { - // initialize `viuer` supports for kitty, iterm2, and sixel - viuer::get_kitty_support(); - viuer::is_iterm_supported(); - #[cfg(feature = "sixel")] - viuer::is_sixel_supported(); - } - } - // client channels let (client_pub, client_sub) = flume::unbounded::(); diff --git a/spotify_player/src/state/ui/mod.rs b/spotify_player/src/state/ui/mod.rs index 27f7b9f1..fd13295d 100644 --- a/spotify_player/src/state/ui/mod.rs +++ b/spotify_player/src/state/ui/mod.rs @@ -5,6 +5,9 @@ use crate::{ utils::filtered_items_from_query, }; +#[cfg(feature = "image")] +use ratatui_image::{picker::Picker, protocol::StatefulProtocol}; + pub type UIStateGuard<'a> = parking_lot::MutexGuard<'a, UIState>; mod page; @@ -13,13 +16,23 @@ mod popup; pub use page::*; pub use popup::*; -#[derive(Default, Debug)] #[cfg(feature = "image")] +#[derive(Default)] pub struct ImageRenderInfo { pub url: String, pub render_area: ratatui::layout::Rect, - /// indicates if the image is rendered - pub rendered: bool, + pub state: Option, +} + +#[cfg(feature = "image")] +impl std::fmt::Debug for ImageRenderInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ImageRenderInfo") + .field("url", &self.url) + .field("render_area", &self.render_area) + .field("state", &self.state.is_some()) + .finish() + } } /// Application's UI state @@ -42,6 +55,9 @@ pub struct UIState { #[cfg(feature = "image")] pub last_cover_image_render_info: ImageRenderInfo, + + #[cfg(feature = "image")] + pub picker: Picker, } impl UIState { @@ -111,6 +127,10 @@ impl Default for UIState { #[cfg(feature = "image")] last_cover_image_render_info: ImageRenderInfo::default(), + + // Will be reinitialize later in ui/mod.rs after init_ui() + #[cfg(feature = "image")] + picker: Picker::halfblocks(), } } } diff --git a/spotify_player/src/ui/mod.rs b/spotify_player/src/ui/mod.rs index 89aad3b2..cb3b9b1f 100644 --- a/spotify_player/src/ui/mod.rs +++ b/spotify_player/src/ui/mod.rs @@ -34,6 +34,20 @@ pub mod utils; /// Run the application UI pub fn run(state: &SharedState) -> Result<()> { + #[cfg(feature = "image")] + { + crossterm::terminal::enable_raw_mode()?; + let mut ui = state.ui.lock(); + ui.picker = match ratatui_image::picker::Picker::from_query_stdio() { + Ok(p) => p, + Err(err) => { + tracing::warn!("Failed to initialize query_stdio picker, error: {err:#}"); + ratatui_image::picker::Picker::halfblocks() + } + }; + crossterm::terminal::disable_raw_mode()?; + } + let mut terminal = init_ui().context("failed to initialize the application's UI")?; let ui_refresh_duration = std::time::Duration::from_millis( diff --git a/spotify_player/src/ui/playback.rs b/spotify_player/src/ui/playback.rs index 43081567..41bef9af 100644 --- a/spotify_player/src/ui/playback.rs +++ b/spotify_player/src/ui/playback.rs @@ -9,8 +9,6 @@ use crate::{ state::Track, ui::utils::{format_genres, to_bidi_string}, }; -#[cfg(feature = "image")] -use anyhow::{Context, Result}; use rspotify::model::Id; /// Render a playback window showing information about the current playback, which includes @@ -79,49 +77,26 @@ pub fn render_playback_window( rspotify::model::PlayableItem::Unknown(_) => None, }; if let Some(url) = url { - let needs_clear = if ui.last_cover_image_render_info.url != url - || ui.last_cover_image_render_info.render_area != cover_img_rect - { - ui.last_cover_image_render_info = ImageRenderInfo { - url, - render_area: cover_img_rect, - rendered: false, - }; - true - } else { - false - }; - - if needs_clear { - // clear the image's both new and old areas to ensure no remaining artifacts before rendering the image - // See: https://github.com/aome510/spotify-player/issues/389 - clear_area( - frame, - ui.last_cover_image_render_info.render_area, - &ui.theme, - ); - clear_area(frame, cover_img_rect, &ui.theme); - } else { - if !ui.last_cover_image_render_info.rendered { - if let Err(err) = render_playback_cover_image(state, ui) { - tracing::error!( - "Failed to render playback's cover image: {err:#}" - ); - } + let data = state.data.read(); + if let Some(img) = data.caches.images.get(&url) { + if ui.last_cover_image_render_info.url != url + || ui.last_cover_image_render_info.render_area != cover_img_rect + { + let protocol = ui.picker.new_resize_protocol(img.clone()); + ui.last_cover_image_render_info = ImageRenderInfo { + url, + render_area: cover_img_rect, + state: Some(protocol), + }; } - // set the `skip` state of cells in the cover image area - // to prevent buffer from overwriting the image's rendered area - // NOTE: `skip` should not be set when clearing the render area. - // Otherwise, nothing will be clear as the buffer doesn't handle cells with `skip=true`. - for x in cover_img_rect.left()..cover_img_rect.right() { - for y in cover_img_rect.top()..cover_img_rect.bottom() { - frame - .buffer_mut() - .cell_mut((x, y)) - .expect("invalid cell") - .set_skip(true); - } + if let Some(ref mut protocol) = ui.last_cover_image_render_info.state { + let image_widget = ratatui_image::StatefulImage::new(); + frame.render_stateful_widget( + image_widget, + cover_img_rect, + protocol, + ); } } } @@ -167,14 +142,7 @@ pub fn render_playback_window( // clear the previous widget's area before rendering the text. #[cfg(feature = "image")] { - if ui.last_cover_image_render_info.rendered { - clear_area( - frame, - ui.last_cover_image_render_info.render_area, - &ui.theme, - ); - ui.last_cover_image_render_info = ImageRenderInfo::default(); - } + ui.last_cover_image_render_info = ImageRenderInfo::default(); } if player.playback_last_updated_time.is_none() { @@ -234,20 +202,6 @@ fn split_rect_for_cover_img(rect: Rect) -> (Rect, Rect) { (ver_chunks[0], hor_chunks[1]) } -#[cfg(feature = "image")] -fn clear_area(frame: &mut Frame, rect: Rect, theme: &config::Theme) { - for x in rect.left()..rect.right() { - for y in rect.top()..rect.bottom() { - frame - .buffer_mut() - .cell_mut((x, y)) - .expect("invalid cell") - .set_char(' ') - .set_style(theme.app()); - } - } -} - fn construct_playback_text( ui: &UIStateGuard, state: &SharedState, @@ -467,41 +421,6 @@ fn render_playback_progress_bar( ui.playback_progress_bar_rect = rect; } -#[cfg(feature = "image")] -fn render_playback_cover_image(state: &SharedState, ui: &mut UIStateGuard) -> Result<()> { - let data = state.data.read(); - if let Some(image) = data.caches.images.get(&ui.last_cover_image_render_info.url) { - let rect = ui.last_cover_image_render_info.render_area; - - // `viuer` renders image using `sixel` in a different scale compared to other methods. - // Scale the image to make the rendered image more fit if needed. - // This scaling factor is user configurable as the scale works differently - // with different fonts and terminals. - // For more context, see https://github.com/aome510/spotify-player/issues/122. - let scale = config::get_config().app_config.cover_img_scale; - let width = (f32::from(rect.width) * scale).round() as u32; - let height = (f32::from(rect.height) * scale).round() as u32; - - viuer::print( - image, - &viuer::Config { - x: rect.x, - y: rect.y as i16, - width: Some(width), - height: Some(height), - restore_cursor: true, - transparent: true, - ..Default::default() - }, - ) - .context("print image to the terminal")?; - - ui.last_cover_image_render_info.rendered = true; - } - - Ok(()) -} - /// Split the given area into two, the first one for the playback window /// and the second one for the main application's layout (popup, page, etc). #[allow(unused_variables)]