From 9e4ee1ca97233d57c02e423bbe741a66b82c29f0 Mon Sep 17 00:00:00 2001 From: itsjunetime Date: Mon, 27 May 2024 00:30:56 -0600 Subject: [PATCH] - Remove unused oxipng dep - throw converter onto its own task - switch to using multi-thread runtime - use unbounded channels in a few more places to prevent deadlocks --- Cargo.lock | 241 ++++------------------------------------------- Cargo.toml | 3 +- src/converter.rs | 225 +++++++++++++++++++++---------------------- src/main.rs | 68 +++++++------ src/renderer.rs | 28 +++--- src/tui.rs | 6 +- 6 files changed, 173 insertions(+), 398 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5d581fa..f902780 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,55 +35,6 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" -[[package]] -name = "anstream" -version = "0.6.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" - -[[package]] -name = "anstyle-parse" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" -dependencies = [ - "windows-sys 0.52.0", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" -dependencies = [ - "anstyle", - "windows-sys 0.52.0", -] - [[package]] name = "autocfg" version = "1.3.0" @@ -123,18 +74,6 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" -[[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 = "bytemuck" version = "1.16.0" @@ -208,49 +147,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "clap" -version = "4.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" -dependencies = [ - "clap_builder", -] - -[[package]] -name = "clap_builder" -version = "4.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_lex" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" - -[[package]] -name = "clap_mangen" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1dd95b5ebb5c1c54581dd6346f3ed6a79a3eef95dd372fc2ac13d535535300e" -dependencies = [ - "clap", - "roff", -] - -[[package]] -name = "colorchoice" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" - [[package]] name = "compact_str" version = "0.7.1" @@ -401,12 +297,6 @@ dependencies = [ "libc", ] -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "futures-channel" version = "0.3.30" @@ -596,6 +486,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "icy_sixel" version = "0.1.2" @@ -625,7 +521,6 @@ checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown", - "rayon", ] [[package]] @@ -648,12 +543,6 @@ dependencies = [ "libc", ] -[[package]] -name = "is_terminal_polyfill" -version = "1.70.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" - [[package]] name = "itertools" version = "0.12.1" @@ -704,24 +593,6 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" -[[package]] -name = "libdeflate-sys" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "669ea17f9257bcb48c09c7ee4bef3957777504acffac557263e20c11001977bc" -dependencies = [ - "cc", -] - -[[package]] -name = "libdeflater" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dfd6424f7010ee0a3416f1d796d0450e3ad3ac237a237644f728277c4ded016" -dependencies = [ - "libdeflate-sys", -] - [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -809,6 +680,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "object" version = "0.32.2" @@ -824,25 +705,6 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" -[[package]] -name = "oxipng" -version = "9.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f398c53eb34e0cf71d9e0bc676cfa7c611e3844dd14ab05e92fb7b423c98ecf" -dependencies = [ - "bitvec", - "clap", - "clap_mangen", - "crossbeam-channel", - "indexmap", - "libdeflater", - "log", - "rayon", - "rgb", - "rustc-hash", - "rustc_version", -] - [[package]] name = "parking_lot" version = "0.12.3" @@ -963,12 +825,6 @@ dependencies = [ "proc-macro2", ] -[[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" @@ -1071,42 +927,12 @@ dependencies = [ "bitflags 2.5.0", ] -[[package]] -name = "rgb" -version = "0.8.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "roff" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" - [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustc_version" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" -dependencies = [ - "semver", -] - [[package]] name = "rustix" version = "0.38.34" @@ -1147,12 +973,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "semver" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" - [[package]] name = "serde" version = "1.0.203" @@ -1249,12 +1069,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - [[package]] name = "strum" version = "0.26.2" @@ -1301,12 +1115,6 @@ dependencies = [ "version-compare", ] -[[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.12.14" @@ -1324,7 +1132,6 @@ dependencies = [ "image", "itertools 0.13.0", "notify", - "oxipng", "poppler-rs", "ratatui", "ratatui-image", @@ -1358,6 +1165,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", + "num_cpus", "pin-project-lite", "tokio-macros", ] @@ -1446,12 +1254,6 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" -[[package]] -name = "utf8parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" - [[package]] name = "vb64" version = "0.1.2" @@ -1673,15 +1475,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - [[package]] name = "zerocopy" version = "0.7.34" diff --git a/Cargo.toml b/Cargo.toml index 9ac83ff..7d67bdf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,11 +15,10 @@ ratatui-image = { git = "https://github.com/itsjunetime/ratatui-image.git", bran crossterm = { version = "0.27.0", features = ["event-stream"] } image = { version = "0.25.1", features = ["png", "rayon"], default-features = false } notify = "6.1.1" -tokio = { version = "1.37.0", features = ["rt", "sync", "macros"] } +tokio = { version = "1.37.0", features = ["rt-multi-thread", "sync", "macros"] } futures-util = { version = "0.3.30", default-features = false } glib = "0.19.6" itertools = "*" -oxipng = { version = "9.1.1", default-features = false, features = ["parallel"] } [profile.production] inherits = "release" diff --git a/src/converter.rs b/src/converter.rs index 086b816..ada5e52 100644 --- a/src/converter.rs +++ b/src/converter.rs @@ -1,138 +1,123 @@ -use std::{ - pin::Pin, - task::{Context, Poll} -}; - -use futures_util::Stream; use image::ImageFormat; use itertools::Itertools; use ratatui_image::{picker::Picker, protocol::Protocol, Resize}; +use tokio::sync::mpsc::{error::{SendError, TryRecvError}, UnboundedReceiver, UnboundedSender}; -use crate::renderer::{PageInfo, RenderError}; +use crate::renderer::{fill_default, PageInfo, RenderError}; const MAX_ITER: usize = 20; -pub struct Converter { - images: Vec>, - picker: Picker, - page: usize, - // once it reaches 20, we're done rendering images - iteration: usize -} - -impl Converter { - pub fn new(picker: Picker) -> Self { - Self { - images: vec![], - picker, - page: 0, - iteration: 0 - } - } - - pub fn add_img(&mut self, page: PageInfo) { - let page_num = page.page; - self.images[page_num] = Some(page); - // just reset it to 0 so we grab this image again next time we try to get an image (if this - // image is in the current list of iterations, so to speak) - self.iteration = 0; - } - - pub fn set_n_pages(&mut self, pages: usize) { - self.images = Vec::with_capacity(pages); - for _ in 0..pages { - self.images.push(None); - } - - self.page = self.page.min(pages - 1); - } - - pub fn go_to_page(&mut self, page: usize) { - self.page = page; - self.iteration = 0; - } - - pub fn change_page_by(&mut self, change: isize) { - self.page = (self.page as isize + change) as usize; - // We just reset iteration here. I think there's some heuristic we could do to place - // iteration exactly where it needs to be to render the next page, but trying to determine - // that caused me a lot of bugs, and only causes the slightest inefficiency (down below, - // when we skip `self.iteration` elements in an iterator), so it's like whatever - self.iteration = 0; - } - - pub fn get_next_img(&mut self) -> Option { - // In this fn, we return Poll::Pending and don't store a Waker 'cause this will be called - // in a loop with tokio::select, and in no other context. The pending that we return on one - // iteration will just be dropped/cancelled as soon as some other action happens, and then - // next time select is called, this'll be checked again, and then we might be in the right - // circumstance to return a Ready - if self.iteration >= MAX_ITER || self.images.is_empty() { - return None; - } - - // This kinda mimics the way the renderer alternates between going above and below the - // current page (within the bounds of how many pages there are) until we've done 20 - let idx_start = self.page.saturating_sub(MAX_ITER / 2); - let idx_end = idx_start.saturating_add(MAX_ITER).min(self.images.len()); - - // then we go through all the indices available to us and find the first one that has an - // image available to steal - let (page_info, iteration) = (idx_start..self.page) - .interleave(self.page..idx_end) - .enumerate() - .skip(self.iteration) - .find_map(|(i_idx, p_idx)| self.images[p_idx].take().map(|p| (p, i_idx)))?; - - let img_area = page_info.img_data.area; - - let dyn_img = - match image::load_from_memory_with_format(&page_info.img_data.data, ImageFormat::Png) { - Ok(dt) => dt, - Err(e) => - return Some(Err(RenderError::Render(format!( - "Couldn't convert Vec to DynamicImage: {e}" - )))), - }; - - // We don't actually want to Crop this image, but we've already - // verified (with the ImageSurface stuff) that the image is the correct - // size for the area given, so to save ratatui the work of having to - // resize it, we tell them to crop it to fit. - let txt_img = match self.picker.new_protocol(dyn_img, img_area, Resize::Crop) { - Ok(img) => img, - Err(e) => - return Some(Err(RenderError::Render(format!( - "Couldn't convert DynamicImage to ratatui image: {e}" - )))), - }; - - // update the iteration to the iteration that we stole this image from - self.iteration = iteration; - - Some(Ok(ConvertedPage { - page: txt_img, - num: page_info.page, - num_results: page_info.search_results - })) - } -} - pub struct ConvertedPage { pub page: Box, pub num: usize, pub num_results: usize } -type ConversionResult = Result; +pub enum ConverterMsg { + NumPages(usize), + GoToPage(usize), + AddImg(PageInfo) +} -impl Stream for Converter { - type Item = ConversionResult; +pub async fn run_conversion_loop( + sender: UnboundedSender>, + mut receiver: UnboundedReceiver, + mut picker: Picker +) -> Result<(), SendError>> { + let mut images = vec![]; + let mut page: usize = 0; - fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { - match self.get_next_img() { - Some(res) => Poll::Ready(Some(res)), - None => Poll::Pending + fn next_page( + images: &mut [Option], + picker: &mut Picker, + page: usize, + iteration: &mut usize + ) -> Result, RenderError> { + if images.is_empty() || *iteration >= MAX_ITER { + return Ok(None) + } + + // This kinda mimics the way the renderer alternates between going above and below the + // current page (within the bounds of how many pages there are) until we've done 20 + let idx_start = page.saturating_sub(MAX_ITER / 2); + let idx_end = idx_start.saturating_add(MAX_ITER).min(images.len()); + + // then we go through all the indices available to us and find the first one that has an + // image available to steal + let Some((page_info, new_iter)) = (idx_start..page) + .interleave(page..idx_end) + .enumerate() + .skip(*iteration) + .find_map(|(i_idx, p_idx)| images[p_idx].take().map(|p| (p, i_idx))) + else { + return Ok(None) + }; + + let img_area = page_info.img_data.area; + + let dyn_img = image::load_from_memory_with_format(&page_info.img_data.data, ImageFormat::Png) + .map_err(|e| RenderError::Render(format!("Couldn't convert Vec to DynamicImage: {e}")))?; + + // We don't actually want to Crop this image, but we've already + // verified (with the ImageSurface stuff) that the image is the correct + // size for the area given, so to save ratatui the work of having to + // resize it, we tell them to crop it to fit. + let txt_img = picker.new_protocol(dyn_img, img_area, Resize::Crop) + .map_err(|e| RenderError::Render(format!("Couldn't convert DynamicImage to ratatui image: {e}")))?; + + // update the iteration to the iteration that we stole this image from + *iteration = new_iter; + + Ok(Some(ConvertedPage { + page: txt_img, + num: page_info.page, + num_results: page_info.search_results + })) + } + + fn handle_notif( + msg: ConverterMsg, + images: &mut Vec>, + page: &mut usize + ) { + match msg { + ConverterMsg::AddImg(img) => { + let page_num = img.page; + images[page_num] = Some(img); + }, + ConverterMsg::NumPages(n_pages) => { + fill_default(images, n_pages); + *page = (*page).min(n_pages - 1); + }, + ConverterMsg::GoToPage(new_page) => *page = new_page, } } + + 'outer: loop { + let mut iteration = 0; + loop { + match receiver.try_recv() { + Ok(msg) => { + handle_notif(msg, &mut images, &mut page); + continue 'outer; + }, + Err(TryRecvError::Empty) => (), + Err(TryRecvError::Disconnected) => panic!("Disconnected :(") + } + + match next_page(&mut images, &mut picker, page, &mut iteration) { + Ok(None) => break, + Ok(Some(img)) => sender.send(Ok(img))?, + Err(e) => sender.send(Err(e))? + } + } + + let Some(msg) = receiver.recv().await else { + break; + }; + + handle_notif(msg, &mut images, &mut page); + } + + Ok(()) } diff --git a/src/main.rs b/src/main.rs index 883e0d2..611ab56 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,12 +2,11 @@ use std::{io::stdout, path::PathBuf, str::FromStr}; -use converter::{ConvertedPage, Converter}; +use converter::{run_conversion_loop, ConvertedPage, ConverterMsg}; use crossterm::{ execute, terminal::{ - disable_raw_mode, enable_raw_mode, EndSynchronizedUpdate, EnterAlternateScreen, - LeaveAlternateScreen + disable_raw_mode, enable_raw_mode, window_size, EndSynchronizedUpdate, EnterAlternateScreen, LeaveAlternateScreen } }; use futures_util::stream::StreamExt; @@ -17,19 +16,20 @@ use ratatui::{backend::CrosstermBackend, Terminal}; use ratatui_image::picker::Picker; use renderer::{RenderInfo, RenderNotif}; use tui::{InputAction, Tui}; +use futures_util::FutureExt; mod converter; mod renderer; mod skip; mod tui; -#[tokio::main(flavor = "current_thread")] +#[tokio::main] async fn main() -> Result<(), Box> { let mut args = std::env::args().skip(1); let file = args.next().ok_or("Program requires a file to process")?; let path = PathBuf::from_str(&file)?.canonicalize()?; - let (watch_tx, render_rx) = tokio::sync::mpsc::channel(1); + let (watch_tx, render_rx) = tokio::sync::mpsc::unbounded_channel(); let tui_tx = watch_tx.clone(); // we need to call this outside the recommended_watcher call because if we call it inside, that @@ -40,7 +40,7 @@ async fn main() -> Result<(), Box> { // then like the main thread has panicked or something, so it doesn't matter if this panics // as well watch_tx - .blocking_send(renderer::RenderNotif::Reload) + .send(renderer::RenderNotif::Reload) .unwrap(); })?; @@ -49,20 +49,27 @@ async fn main() -> Result<(), Box> { watcher.watch(&path, RecursiveMode::NonRecursive)?; let file_path = format!("file://{}", path.clone().into_os_string().to_string_lossy()); - let (render_tx, mut tui_rx) = tokio::sync::mpsc::channel(1); + let (render_tx, mut tui_rx) = tokio::sync::mpsc::unbounded_channel(); + + let window_size = window_size()?; // We need to create `picker` on this thread because if we create it on the `renderer` thread, // it messes up something with user input. Input never makes it to the crossterm thing - let mut picker = Picker::from_termios()?; + let mut picker = Picker::new((window_size.width / window_size.columns, window_size.height / window_size.rows)); picker.guess_protocol(); // then we want to spawn off the rendering task // We need to use the thread::spawn API so that this exists in a thread not owned by tokio, // since the methods we call in `start_rendering` will panic if called in an async context - std::thread::spawn(move || renderer::start_rendering(file_path, render_tx, render_rx)); + std::thread::spawn(move || renderer::start_rendering(file_path, render_tx, render_rx, window_size)); let mut ev_stream = crossterm::event::EventStream::new(); + let (to_converter, from_main) = tokio::sync::mpsc::unbounded_channel(); + let (to_main, mut from_converter) = tokio::sync::mpsc::unbounded_channel(); + + tokio::spawn(run_conversion_loop(to_main, from_main, picker)); + let file_name = path .file_name() .map(|n| n.to_string_lossy()) @@ -78,9 +85,6 @@ async fn main() -> Result<(), Box> { // document's pages, then it will return `None`, but still log to stderr with CRITICAL level), // so we want to just ignore all logging since this is a tui app. glib::log_set_writer_func(noop); - - let mut converter = Converter::new(picker); - execute!( term.backend_mut(), EnterAlternateScreen, @@ -89,19 +93,12 @@ async fn main() -> Result<(), Box> { enable_raw_mode()?; let mut main_area = tui::Tui::main_layout(&term.get_frame()); - tui_tx.send(RenderNotif::Area(main_area[1])).await?; + tui_tx.send(RenderNotif::Area(main_area[1]))?; loop { let mut needs_redraw = tokio::select! { - Some(img_res) = converter.next() => { - match img_res { - Ok(ConvertedPage { page, num, num_results }) => tui.page_ready(page, num, num_results), - Err(e) => tui.show_error(e), - } - true - }, // First we check if we have any keystrokes - Some(ev) = ev_stream.next() => { + Some(ev) = ev_stream.next().fuse() => { // If we can't get user input, just crash. let ev = ev.expect("Couldn't get any user input"); @@ -111,12 +108,11 @@ async fn main() -> Result<(), Box> { match action { InputAction::Redraw => (), InputAction::QuitApp => break, - InputAction::ChangePageBy(change) => converter.change_page_by(change), InputAction::JumpingToPage(page) => { - tui_tx.send(RenderNotif::JumpToPage(page)).await?; - converter.go_to_page(page); + tui_tx.send(RenderNotif::JumpToPage(page))?; + to_converter.send(ConverterMsg::GoToPage(page))?; }, - InputAction::Search(term) => tui_tx.send(RenderNotif::Search(term)).await?, + InputAction::Search(term) => tui_tx.send(RenderNotif::Search(term))?, }; true } @@ -126,35 +122,37 @@ async fn main() -> Result<(), Box> { match renderer_msg { Ok(RenderInfo::NumPages(num)) => { tui.set_n_pages(num); - converter.set_n_pages(num); + to_converter.send(ConverterMsg::NumPages(num))?; }, Ok(RenderInfo::Page(info)) => { tui.got_num_results_on_page(info.page, info.search_results); - converter.add_img(info); + to_converter.send(ConverterMsg::AddImg(info))?; }, Err(e) => tui.show_error(e), } true } + Some(img_res) = from_converter.recv() => { + match img_res { + Ok(ConvertedPage { page, num, num_results }) => tui.page_ready(page, num, num_results), + Err(e) => tui.show_error(e), + } + true + }, }; let new_area = Tui::main_layout(&term.get_frame()); if new_area != main_area { main_area = new_area; - tui_tx.send(RenderNotif::Area(main_area[1])).await?; + tui_tx.send(RenderNotif::Area(main_area[1]))?; needs_redraw = true; } if needs_redraw { - let mut end_update = false; term.draw(|f| { - tui.render(f, &main_area, &mut end_update); - // To be enabled when https://github.com/ratatui-org/ratatui/issues/1116 gets fixed - // f.bypass_diff = true; + tui.render(f, &main_area); })?; - if end_update { - execute!(stdout(), EndSynchronizedUpdate)?; - } + execute!(stdout(), EndSynchronizedUpdate)?; } } diff --git a/src/renderer.rs b/src/renderer.rs index 952829f..6093ddd 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -1,8 +1,9 @@ use cairo::{Antialias, Format}; +use crossterm::terminal::WindowSize; use itertools::Itertools; use poppler::{Color, Document, FindFlags, Page, Rectangle, SelectionStyle}; use ratatui::layout::Rect; -use tokio::sync::mpsc::{error::TryRecvError, Receiver, Sender}; +use tokio::sync::mpsc::{error::TryRecvError, UnboundedReceiver, UnboundedSender}; pub enum RenderNotif { Area(Rect), @@ -41,7 +42,7 @@ struct PrevRender { contained_term: Option } -fn fill_default(vec: &mut Vec, size: usize) { +pub fn fill_default(vec: &mut Vec, size: usize) { vec.clear(); vec.reserve(size.saturating_sub(vec.len())); for _ in 0..size { @@ -60,8 +61,9 @@ fn fill_default(vec: &mut Vec, size: usize) { // we're done. pub fn start_rendering( path: String, - sender: Sender>, - mut receiver: Receiver + sender: UnboundedSender>, + mut receiver: UnboundedReceiver, + size: WindowSize ) { // first, wait 'til we get told what the current starting area is so that we can set it to // know what to render to @@ -80,7 +82,7 @@ pub fn start_rendering( 'reload: loop { let doc = match Document::from_file(&path, None) { Err(e) => { - sender.blocking_send(Err(RenderError::Doc(e))).unwrap(); + sender.send(Err(RenderError::Doc(e))).unwrap(); return; } Ok(d) => d @@ -88,7 +90,7 @@ pub fn start_rendering( let n_pages = doc.n_pages() as usize; sender - .blocking_send(Ok(RenderInfo::NumPages(n_pages))) + .send(Ok(RenderInfo::NumPages(n_pages))) .unwrap(); // We're using this vec of bools to indicate which page numbers have already been rendered, @@ -190,7 +192,7 @@ pub fn start_rendering( // We know this is in range 'cause we're iterating over it let Some(page) = doc.page(num as i32) else { sender - .blocking_send(Err(RenderError::Render(format!( + .send(Err(RenderError::Render(format!( "Couldn't get page {num} ({}) of doc?", num as i32 )))) @@ -202,7 +204,7 @@ pub fn start_rendering( rendered.successful && rendered.contained_term == Some(false); // render the page - match render_single_page(page, area, num, &search_term, rendered_with_no_results) { + match render_single_page(page, area, num, &search_term, rendered_with_no_results, &size) { // If we've already rendered it just fine and we don't need to render it again, // just continue. We're all good Ok(None) => (), @@ -212,10 +214,11 @@ pub fn start_rendering( // But we first need to store if we already rendered it correctly so that // the next time we iterate through, it might see that we're already good rendered.contained_term = Some(img.search_results > 0); - sender.blocking_send(Ok(RenderInfo::Page(img))).unwrap() + rendered.successful = true; + sender.send(Ok(RenderInfo::Page(img))).unwrap() }, // And if we got an error, then obviously we need to propagate that - Err(e) => sender.blocking_send(Err(RenderError::Render(e))).unwrap() + Err(e) => sender.send(Err(RenderError::Render(e))).unwrap() } } // Then once we've rendered all these pages, wait until we get another notification @@ -237,7 +240,8 @@ fn render_single_page( area: Rect, page_num: usize, search_term: &Option, - already_rendered_no_results: bool + already_rendered_no_results: bool, + size: &WindowSize ) -> Result, String> { let mut result_rects = search_term .as_ref() @@ -252,8 +256,6 @@ fn render_single_page( // First, get the font size; the number of pixels (width x height) per font character (I // think; it's at least something like that) on this terminal screen. - let size = - crossterm::terminal::window_size().map_err(|e| format!("Couldn't get window size: {e}"))?; let col_h = size.height / size.rows; let col_w = size.width / size.columns; diff --git a/src/tui.rs b/src/tui.rs index a5e3305..58180df 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -88,7 +88,7 @@ impl Tui { } // TODO: Make a way to fill the width of the screen with one page and scroll down to view it - pub fn render(&mut self, frame: &mut Frame<'_>, main_area: &[Rect], end_update: &mut bool) { + pub fn render(&mut self, frame: &mut Frame<'_>, main_area: &[Rect]) { let top_block = Block::new() .padding(Padding { right: 2, @@ -220,7 +220,6 @@ impl Tui { Self::render_loading_in(frame, img_area); } else { execute!(stdout(), BeginSynchronizedUpdate).unwrap(); - *end_update = true; let total_width = page_widths.iter().map(|(_, w)| w).sum::(); @@ -283,7 +282,7 @@ impl Tui { match self.page as isize - old as isize { 0 => None, - change => Some(InputAction::ChangePageBy(change)) + _ => Some(InputAction::JumpingToPage(self.page)) } } @@ -517,7 +516,6 @@ impl Tui { pub enum InputAction { Redraw, - ChangePageBy(isize), JumpingToPage(usize), Search(String), QuitApp