diff --git a/Cargo.lock b/Cargo.lock index 95734c8..1f7a31e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,10 +23,6 @@ name = "accesskit" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74a4b14f3d99c1255dcba8f45621ab1a2e7540a0009652d33989005a4d0bfc6b" -dependencies = [ - "enumn", - "serde", -] [[package]] name = "accesskit_consumer" @@ -108,7 +104,6 @@ dependencies = [ "cfg-if", "getrandom", "once_cell", - "serde", "version_check", "zerocopy", ] @@ -1006,6 +1001,7 @@ version = "0.1.1" dependencies = [ "arrayvec", "clipboard-history-core", + "image", "memchr", "regex", "rustc-hash 2.0.0", @@ -1032,8 +1028,8 @@ version = "0.1.1" dependencies = [ "clipboard-history-client-sdk", "eframe", - "egui_extras", "image", + "rustc-hash 2.0.0", ] [[package]] @@ -1061,6 +1057,7 @@ dependencies = [ "clipboard-history-client-sdk", "error-stack", "ratatui", + "ratatui-image", "regex", "thiserror", "tui-textarea", @@ -1323,12 +1320,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "data-url" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" - [[package]] name = "derivative" version = "2.2.0" @@ -1407,6 +1398,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + [[package]] name = "ecolor" version = "0.28.1" @@ -1415,7 +1412,6 @@ checksum = "2e6b451ff1143f6de0f33fc7f1b68fecfd2c7de06e104de96c4514de3f5396f8" dependencies = [ "bytemuck", "emath", - "serde", ] [[package]] @@ -1465,7 +1461,6 @@ dependencies = [ "epaint", "log", "nohash-hasher", - "serde", ] [[package]] @@ -1505,22 +1500,6 @@ dependencies = [ "winit", ] -[[package]] -name = "egui_extras" -version = "0.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bb783d9fa348f69ed5c340aa25af78b5472043090e8b809040e30960cc2a746" -dependencies = [ - "ahash", - "egui", - "enum-map", - "image", - "log", - "mime_guess2", - "resvg", - "serde", -] - [[package]] name = "egui_glow" version = "0.28.1" @@ -1551,28 +1530,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6a21708405ea88f63d8309650b4d77431f4bc28fb9d8e6f77d3963b51249e6" dependencies = [ "bytemuck", - "serde", -] - -[[package]] -name = "enum-map" -version = "2.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" -dependencies = [ - "enum-map-derive", - "serde", -] - -[[package]] -name = "enum-map-derive" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", ] [[package]] @@ -1596,17 +1553,6 @@ dependencies = [ "syn 2.0.72", ] -[[package]] -name = "enumn" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fd000fd6988e73bbe993ea3db9b1aa64906ab88766d654973924340c8cddb42" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", -] - [[package]] name = "env_filter" version = "0.1.0" @@ -1644,7 +1590,6 @@ dependencies = [ "log", "nohash-hasher", "parking_lot", - "serde", ] [[package]] @@ -1777,12 +1722,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "float-cmp" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" - [[package]] name = "flume" version = "0.11.0" @@ -2215,6 +2154,12 @@ dependencies = [ "objc2 0.4.1", ] +[[package]] +name = "icy_sixel" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86858ae800284d596cfdefcb0ad435c3493c12f35367431bbe9b2b3858c1155b" + [[package]] name = "idna" version = "0.5.0" @@ -2258,12 +2203,6 @@ dependencies = [ "quick-error", ] -[[package]] -name = "imagesize" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" - [[package]] name = "imgref" version = "1.10.1" @@ -2414,15 +2353,6 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" -[[package]] -name = "kurbo" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd85a5776cd9500c2e2059c8c76c3b01528566b7fcbaf8098b55a33fc298849b" -dependencies = [ - "arrayvec", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -2625,22 +2555,6 @@ dependencies = [ "paste", ] -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "mime_guess2" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a3333bb1609500601edc766a39b4c1772874a4ce26022f4d866854dc020c41" -dependencies = [ - "mime", - "unicase", -] - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -3073,12 +2987,6 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[package]] -name = "pico-args" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" - [[package]] name = "pin-project-lite" version = "0.2.14" @@ -3343,6 +3251,22 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "ratatui-image" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de94276254cb20fb7431726875bd2ac6391a6ffc26f4b8e3d23f79d1286b491e" +dependencies = [ + "base64 0.22.1", + "crossterm", + "dyn-clone", + "icy_sixel", + "image", + "rand", + "ratatui", + "rustix 0.38.34", +] + [[package]] name = "rav1e" version = "0.7.1" @@ -3425,12 +3349,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "rctree" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f" - [[package]] name = "redox_syscall" version = "0.3.5" @@ -3521,20 +3439,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" -[[package]] -name = "resvg" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cadccb3d99a9efb8e5e00c16fbb732cbe400db2ec7fc004697ee7d97d86cf1f4" -dependencies = [ - "log", - "pico-args", - "rgb", - "svgtypes", - "tiny-skia", - "usvg", -] - [[package]] name = "rgb" version = "0.8.45" @@ -3544,12 +3448,6 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "roxmltree" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" - [[package]] name = "rustc-hash" version = "1.1.0" @@ -3789,21 +3687,6 @@ dependencies = [ "quote", ] -[[package]] -name = "simplecss" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d" -dependencies = [ - "log", -] - -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - [[package]] name = "slab" version = "0.4.9" @@ -3928,9 +3811,6 @@ name = "strict-num" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" -dependencies = [ - "float-cmp", -] [[package]] name = "strsim" @@ -3972,16 +3852,6 @@ dependencies = [ "rustdoc-json", ] -[[package]] -name = "svgtypes" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e44e288cd960318917cbd540340968b90becc8bc81f171345d706e7a89d9d70" -dependencies = [ - "kurbo", - "siphasher", -] - [[package]] name = "syn" version = "1.0.109" @@ -4102,7 +3972,6 @@ dependencies = [ "bytemuck", "cfg-if", "log", - "png", "tiny-skia-path", ] @@ -4283,15 +4152,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "unicase" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] - [[package]] name = "unicode-bidi" version = "0.3.15" @@ -4353,50 +4213,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "usvg" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b0a51b72ab80ca511d126b77feeeb4fb1e972764653e61feac30adc161a756" -dependencies = [ - "base64 0.21.7", - "log", - "pico-args", - "usvg-parser", - "usvg-tree", - "xmlwriter", -] - -[[package]] -name = "usvg-parser" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bd4e3c291f45d152929a31f0f6c819245e2921bfd01e7bd91201a9af39a2bdc" -dependencies = [ - "data-url", - "flate2", - "imagesize", - "kurbo", - "log", - "roxmltree", - "simplecss", - "siphasher", - "svgtypes", - "usvg-tree", -] - -[[package]] -name = "usvg-tree" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee3d202ebdb97a6215604b8f5b4d6ef9024efd623cf2e373a6416ba976ec7d3" -dependencies = [ - "rctree", - "strict-num", - "svgtypes", - "tiny-skia-path", -] - [[package]] name = "utf8parse" version = "0.2.2" @@ -5214,12 +5030,6 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193" -[[package]] -name = "xmlwriter" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" - [[package]] name = "zbus" version = "3.15.2" diff --git a/client-sdk/api.golden b/client-sdk/api.golden index 8aadabb..6383304 100644 --- a/client-sdk/api.golden +++ b/client-sdk/api.golden @@ -406,6 +406,7 @@ pub unsafe fn clipboard_history_client_sdk::ui_actor::Command::init(init: either::into_either::IntoEither for clipboard_history_client_sdk::ui_actor::Command pub enum clipboard_history_client_sdk::ui_actor::CommandError pub clipboard_history_client_sdk::ui_actor::CommandError::Core(clipboard_history_core::Error) +pub clipboard_history_client_sdk::ui_actor::CommandError::Image(image::error::ImageError) pub clipboard_history_client_sdk::ui_actor::CommandError::Regex(regex::error::Error) pub clipboard_history_client_sdk::ui_actor::CommandError::Sdk(clipboard_history_client_sdk::ClientError) impl core::convert::From for clipboard_history_client_sdk::ui_actor::CommandError @@ -414,6 +415,8 @@ impl core::convert::From for clipboard_history_cl pub fn clipboard_history_client_sdk::ui_actor::CommandError::from(source: clipboard_history_core::Error) -> Self impl core::convert::From for clipboard_history_client_sdk::ui_actor::CommandError pub fn clipboard_history_client_sdk::ui_actor::CommandError::from(value: clipboard_history_core::protocol::IdNotFoundError) -> Self +impl core::convert::From for clipboard_history_client_sdk::ui_actor::CommandError +pub fn clipboard_history_client_sdk::ui_actor::CommandError::from(source: image::error::ImageError) -> Self impl core::convert::From for clipboard_history_client_sdk::ui_actor::CommandError pub fn clipboard_history_client_sdk::ui_actor::CommandError::from(source: regex::error::Error) -> Self impl core::error::Error for clipboard_history_client_sdk::ui_actor::CommandError @@ -467,7 +470,7 @@ pub clipboard_history_client_sdk::ui_actor::Message::LoadedFirstPage::default_fo pub clipboard_history_client_sdk::ui_actor::Message::LoadedFirstPage::entries: alloc::boxed::Box<[clipboard_history_client_sdk::ui_actor::UiEntry]> pub clipboard_history_client_sdk::ui_actor::Message::LoadedImage pub clipboard_history_client_sdk::ui_actor::Message::LoadedImage::id: u64 -pub clipboard_history_client_sdk::ui_actor::Message::LoadedImage::result: image::error::ImageResult +pub clipboard_history_client_sdk::ui_actor::Message::LoadedImage::image: image::dynimage::DynamicImage pub clipboard_history_client_sdk::ui_actor::Message::SearchResults(alloc::boxed::Box<[clipboard_history_client_sdk::ui_actor::UiEntry]>) impl core::fmt::Debug for clipboard_history_client_sdk::ui_actor::Message pub fn clipboard_history_client_sdk::ui_actor::Message::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result diff --git a/client-sdk/src/ui_actor.rs b/client-sdk/src/ui_actor.rs index 9961247..c259fe3 100644 --- a/client-sdk/src/ui_actor.rs +++ b/client-sdk/src/ui_actor.rs @@ -10,7 +10,7 @@ use std::{ sync::Arc, }; -use image::{DynamicImage, ImageReader, ImageResult}; +use image::{DynamicImage, ImageError, ImageReader}; use regex::bytes::Regex; use rustc_hash::FxHasher; use rustix::{ @@ -40,6 +40,8 @@ pub enum CommandError { Sdk(#[from] ClientError), #[error("Regex instantiation failed.")] Regex(#[from] regex::Error), + #[error("Image loading error.")] + Image(#[from] ImageError), } impl From for CommandError { @@ -77,7 +79,7 @@ pub enum Message { FavoriteChange(u64), LoadedImage { id: u64, - result: ImageResult, + image: DynamicImage, }, } @@ -301,10 +303,10 @@ fn handle_command<'a, Server: AsFd>( let entry = unsafe { database.get(id)? }; Ok(Some(Message::LoadedImage { id, - result: ImageReader::new(BufReader::new(&*entry.to_file(reader)?)) + image: ImageReader::new(BufReader::new(&*entry.to_file(reader)?)) .with_guessed_format() .map_io_err(|| "Failed to guess image format for entry {id}.")? - .decode(), + .decode()?, })) } } diff --git a/egui/src/main.rs b/egui/src/main.rs index db32c35..4f5446c 100644 --- a/egui/src/main.rs +++ b/egui/src/main.rs @@ -19,7 +19,6 @@ use eframe::{ }, epaint::FontFamily, }; -use image::ImageError; use ringboard_sdk::{ core::{protocol::RingKind, Error as CoreError}, ui_actor::{controller, Command, CommandError, DetailedEntry, Message, UiEntry, UiEntryCache}, @@ -62,13 +61,8 @@ fn main() -> Result<(), eframe::Error> { let ctx = cc.egui_ctx.clone(); move || { controller(&command_receiver, |m| { - let r = if let Message::LoadedImage { id: _, ref result } = m - && result.is_ok() - { - let Message::LoadedImage { id, result } = m else { - unreachable!() - }; - ringboard_loader.add(id, result.unwrap()); + let r = if let Message::LoadedImage { id, image } = m { + ringboard_loader.add(id, image); Ok(()) } else { response_sender.send(m) @@ -113,7 +107,6 @@ struct UiEntries { struct UiState { fatal_error: Option, last_error: Option, - image_error: Option, highlighted_id: Option, details_requested: Option, @@ -154,7 +147,6 @@ fn handle_message( UiState { fatal_error, last_error, - image_error, highlighted_id, details_requested, detailed_entry, @@ -189,10 +181,7 @@ fn handle_message( *search_results = entries; } Message::FavoriteChange(_) => {} - Message::LoadedImage { id: _, result } => match result { - Ok(_) => unreachable!(), - Err(e) => *image_error = Some(e), - }, + Message::LoadedImage { .. } => unreachable!(), } } @@ -342,9 +331,6 @@ fn main_ui( if let Some(e) = &state.last_error { show_error(ui, e); } - if let Some(e) = &state.image_error { - show_error(ui, e); - } let mut try_scroll = false; diff --git a/tui/Cargo.toml b/tui/Cargo.toml index a1168dd..d161df2 100644 --- a/tui/Cargo.toml +++ b/tui/Cargo.toml @@ -12,6 +12,7 @@ license.workspace = true [dependencies] error-stack = { version = "0.5.0", default-features = false, features = ["std"] } ratatui = "0.27.0" +ratatui-image = { version = "1.0.5", features = ["crossterm"] } regex = "1.10.5" ringboard-sdk = { package = "clipboard-history-client-sdk", version = "0", path = "../client-sdk" } thiserror = "1.0.63" diff --git a/tui/src/main.rs b/tui/src/main.rs index d3538d8..a5d0ae5 100644 --- a/tui/src/main.rs +++ b/tui/src/main.rs @@ -29,6 +29,7 @@ use ratatui::{ }, Terminal, }; +use ratatui_image::{picker::Picker, protocol::StatefulProtocol, StatefulImage}; use ringboard_sdk::{ core::{ protocol::{IdNotFoundError, RingKind}, @@ -83,6 +84,7 @@ struct UiState { details_requested: Option, detailed_entry: Option>, detail_scroll: u16, + detail_image_state: Option, query: TextArea<'static>, search_state: Option, @@ -95,6 +97,11 @@ struct SearchState { regex: bool, } +enum ImageState { + Requested(u64), + Loaded(Box), +} + macro_rules! active_entries { ($entries:expr, $state:expr) => {{ if $state.query.is_empty() { @@ -159,6 +166,7 @@ fn main() -> error_stack::Result<(), Wrapper> { } CommandError::Sdk(ClientError::VersionMismatch { actual: _ }) => Report::new(wrapper), CommandError::Regex(e) => Report::new(e).change_context(wrapper), + CommandError::Image(e) => Report::new(e).change_context(wrapper), } }) } @@ -221,13 +229,22 @@ impl App { responses, ref mut state, } = self; - state - .draw(&mut terminal) - .map_io_err(|| "Failed to write to terminal.")?; + let mut picker = Picker::from_termios().unwrap_or_else(|_| Picker::new((2, 4))); + picker.guess_protocol(); + + AppWrapper { + state, + requests: &requests, + } + .draw(&mut terminal) + .map_io_err(|| "Failed to write to terminal.")?; + let mut local_state = Option::default(); for action in responses { match action { - Action::Controller(message) => handle_message(message, state, &mut local_state)?, + Action::Controller(message) => { + handle_message(message, state, &mut local_state, &mut picker)?; + } Action::User(event) => { if handle_event( event.map_io_err(|| "Failed to read terminal.")?, @@ -238,17 +255,14 @@ impl App { } } } - state - .draw(&mut terminal) - .map_io_err(|| "Failed to write to terminal.")?; - } - Ok(()) - } -} -impl State { - fn draw(&mut self, terminal: &mut Terminal) -> io::Result<()> { - terminal.draw(|f| f.render_widget(self, f.size()))?; + AppWrapper { + state, + requests: &requests, + } + .draw(&mut terminal) + .map_io_err(|| "Failed to write to terminal.")?; + } Ok(()) } } @@ -257,6 +271,7 @@ fn handle_message( message: Message, State { entries, ui }: &mut State, pending_favorite_change: &mut Option, + picker: &mut Picker, ) -> Result<(), CommandError> { let UiEntries { loaded_entries, @@ -306,6 +321,13 @@ fn handle_message( } } Message::FavoriteChange(id) => *pending_favorite_change = Some(id), + Message::LoadedImage { id, image } => { + if let Some(ImageState::Requested(requested_id)) = ui.detail_image_state + && requested_id == id + { + ui.detail_image_state = Some(ImageState::Loaded(picker.new_resize_protocol(image))); + } + } } Ok(()) } @@ -323,6 +345,7 @@ fn handle_event(event: Event, state: &mut State, requests: &Sender) -> ui.details_requested = Some(entry.id()); ui.detailed_entry = None; ui.detail_scroll = 0; + ui.detail_image_state = None; let _ = requests.send(Command::GetDetails { entry, with_text: matches!(cache, UiEntryCache::Text { .. }), @@ -496,17 +519,29 @@ fn handle_event(event: Event, state: &mut State, requests: &Sender) -> false } -impl Widget for &mut State { +struct AppWrapper<'a> { + requests: &'a Sender, + state: &'a mut State, +} + +impl AppWrapper<'_> { + fn draw(&mut self, terminal: &mut Terminal) -> io::Result<()> { + terminal.draw(|f| f.render_widget(self, f.size()))?; + Ok(()) + } +} + +impl Widget for &mut AppWrapper<'_> { fn render(self, area: Rect, buf: &mut Buffer) { let [header_area, main_area, footer_area] = Layout::vertical([ Constraint::Length(1), Constraint::Min(0), - Constraint::Length(if self.ui.show_help { 3 } else { 0 }), + Constraint::Length(if self.state.ui.show_help { 3 } else { 0 }), ]) .areas(area); let [entry_list_area, _padding, selected_entry_area] = - if self.ui.details_requested.is_none() { + if self.state.ui.details_requested.is_none() { Layout::vertical([ Constraint::Min(0), Constraint::Length(0), @@ -527,7 +562,7 @@ impl Widget for &mut State { } .areas(main_area); - State::render_title(header_area, buf); + AppWrapper::render_title(header_area, buf); self.render_entries(entry_list_area, buf); self.render_selected_entry(selected_entry_area, buf); self.render_footer(footer_area, buf); @@ -537,17 +572,21 @@ impl Widget for &mut State { fn ui_entry_line(UiEntry { entry: _, cache }: &UiEntry) -> Line { match cache { UiEntryCache::Text { one_liner } => Line::raw(&**one_liner), - UiEntryCache::Image { .. } => Line::raw("Image: not yet supported"), // TODO + UiEntryCache::Image => Line::raw("Image: open details to view.").italic(), UiEntryCache::Binary { mime_type, context } => Line::raw(format!( "Unable to display format of type {mime_type:?} from {context:?}." - )), - UiEntryCache::Error(e) => Line::raw(format!("Error: {e}\nDetails: {e:#?}")), + )) + .italic(), + UiEntryCache::Error(e) => Line::raw(format!("Error: {e}\nDetails: {e:#?}")).italic(), } } -impl State { +impl AppWrapper<'_> { fn render_entries(&mut self, area: Rect, buf: &mut Buffer) { - let Self { entries, ui } = self; + let Self { + state: State { entries, ui }, + requests: _, + } = self; let [search_area, entries_area] = Layout::vertical([ Constraint::Length(if ui.search_state.is_some() { 3 } else { 0 }), @@ -594,11 +633,11 @@ impl State { } fn render_selected_entry(&mut self, area: Rect, buf: &mut Buffer) { - let Self { entries, ui } = self; - let Some(UiEntry { entry, cache: _ }) = active_list_state!(entries, ui) - .selected() - .map(|i| &active_entries!(entries, ui)[i]) - else { + let Self { + state: State { entries, ui }, + requests, + } = self; + let Some(UiEntry { entry, cache }) = selected_entry!(entries, ui) else { return; }; @@ -638,17 +677,32 @@ impl State { .as_ref() .and_then(|r| r.as_ref().err()) .map_or(String::new(), |e| format!("Error: {e}\nDetails: {e:#?}")); - Paragraph::new(ui.detailed_entry.as_ref().map_or("Loading…", |r| match r { - Ok(DetailedEntry { - mime_type: _, - full_text, - }) => full_text.as_deref().unwrap_or("Binary data."), - Err(_) => &error, - })) - .block(inner_block) - .wrap(Wrap { trim: false }) - .scroll((ui.detail_scroll, 0)) - .render(inner_area, buf); + + if matches!(cache, UiEntryCache::Image) { + if let Some(ImageState::Loaded(image_state)) = &mut ui.detail_image_state { + StatefulImage::new(None).render(inner_area, buf, image_state); + } else { + Paragraph::new("Loading…") + .block(inner_block) + .render(inner_area, buf); + } + if ui.detail_image_state.is_none() { + ui.detail_image_state = Some(ImageState::Requested(entry.id())); + let _ = requests.send(Command::LoadImage(entry.id())); + } + } else { + Paragraph::new(ui.detailed_entry.as_ref().map_or("Loading…", |r| match r { + Ok(DetailedEntry { + mime_type: _, + full_text, + }) => full_text.as_deref().unwrap_or("Binary data."), + Err(_) => &error, + })) + .block(inner_block) + .wrap(Wrap { trim: false }) + .scroll((ui.detail_scroll, 0)) + .render(inner_area, buf); + } } fn render_title(area: Rect, buf: &mut Buffer) { @@ -659,7 +713,7 @@ impl State { } fn render_footer(&self, area: Rect, buf: &mut Buffer) { - if !self.ui.show_help { + if !self.state.ui.show_help { return; }