diff --git a/Cargo.lock b/Cargo.lock index 524b232..5a19418 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,9 +107,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.21.7" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" @@ -188,9 +188,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.97" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" +checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" [[package]] name = "cfg-expr" @@ -245,12 +245,6 @@ dependencies = [ "roff", ] -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - [[package]] name = "colorchoice" version = "1.0.1" @@ -272,18 +266,18 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ "crossbeam-utils", ] @@ -309,9 +303,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crossterm" @@ -347,9 +341,9 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "either" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" [[package]] name = "equivalent" @@ -526,9 +520,9 @@ dependencies = [ [[package]] name = "glib" -version = "0.19.6" +version = "0.19.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0116c428e4841cab183a32a71b900fd6712194c20f9c424f01d2c016c96bd23" +checksum = "e52355166df21c7ed16b6a01f615669c7911ed74e27ef60eba339c0d2da12490" dependencies = [ "bitflags 2.5.0", "futures-channel", @@ -548,9 +542,9 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.19.5" +version = "0.19.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed782fa3e949c31146671da6e7a227a5e7d354660df1db6d0aac4974dc82a3c" +checksum = "70025dbfa1275cf7d0531c3317ba6270dae15d87e63342229d638246ff45202e" dependencies = [ "heck 0.5.0", "proc-macro-crate", @@ -610,17 +604,17 @@ checksum = "86858ae800284d596cfdefcb0ad435c3493c12f35367431bbe9b2b3858c1155b" [[package]] name = "image" -version = "0.24.9" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" dependencies = [ "bytemuck", "byteorder", - "color_quant", - "jpeg-decoder", "num-traits", "png", "rayon", + "zune-core", + "zune-jpeg", ] [[package]] @@ -634,12 +628,6 @@ dependencies = [ "rayon", ] -[[package]] -name = "indoc" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" - [[package]] name = "inotify" version = "0.9.6" @@ -675,18 +663,21 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" -[[package]] -name = "jpeg-decoder" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" - [[package]] name = "kqueue" version = "1.0.8" @@ -709,9 +700,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.154" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libdeflate-sys" @@ -733,9 +724,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" @@ -770,9 +761,9 @@ checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" dependencies = [ "adler", "simd-adler32", @@ -854,9 +845,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -956,9 +947,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.82" +version = "1.0.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" +checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6" dependencies = [ "unicode-ident", ] @@ -1010,29 +1001,25 @@ dependencies = [ [[package]] name = "ratatui" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a564a852040e82671dc50a37d88f3aa83bbc690dfc6844cfe7a2591620206a80" +version = "0.26.3" dependencies = [ "bitflags 2.5.0", "cassowary", "compact_str", "crossterm", - "indoc", - "itertools", + "itertools 0.13.0", "lru", "paste", "stability", "strum", "unicode-segmentation", + "unicode-truncate", "unicode-width", ] [[package]] name = "ratatui-image" version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2264bdb808c89e8395480cfce32c197e75a3d6171063e913bca12e7919a333da" dependencies = [ "base64", "dyn-clone", @@ -1165,18 +1152,18 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.202" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.202" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", @@ -1289,9 +1276,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.63" +version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", @@ -1332,7 +1319,7 @@ dependencies = [ "futures-util", "glib", "image", - "itertools", + "itertools 0.13.0", "notify", "oxipng", "poppler-rs", @@ -1343,18 +1330,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", @@ -1440,6 +1427,16 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +[[package]] +name = "unicode-truncate" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5fbabedabe362c618c714dbefda9927b5afc8e2a8102f47f081089a9019226" +dependencies = [ + "itertools 0.12.1", + "unicode-width", +] + [[package]] name = "unicode-width" version = "0.1.12" @@ -1696,3 +1693,18 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-jpeg" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec866b44a2a1fd6133d363f073ca1b179f438f99e7e5bfb1e33f7181facfe448" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index c26c399..97b6981 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,15 +6,15 @@ edition = "2021" [dependencies] poppler-rs = { version = "0.23.0", features = ["v21_5"] } cairo-rs = { version = "0.19.4", features = ["png"] } -ratatui = "0.26.2" -# ratatui = { path = "./ratatui" } -ratatui-image = { version = "1.0.0", features = ["rustix"], default-features = false } -# ratatui-image = { path = "./ratatui-image", features = ["rustix"], default-features = false } +# ratatui = "0.26.3" +ratatui = { path = "./ratatui" } +# ratatui-image = { version = "1.0.0", features = ["rustix"], default-features = false } +ratatui-image = { path = "./ratatui-image", features = ["rustix"], default-features = false } crossterm = { version = "0.27.0", features = ["event-stream"] } -image = { version = "0.24.9", features = ["png", "rayon"], default-features = false } +image = { version = "0.25.1", features = ["png", "rayon"], default-features = false } notify = "6.1.1" tokio = { version = "1.37.0", features = ["rt", "sync", "macros"] } futures-util = { version = "0.3.30", default-features = false } glib = "0.19.6" -itertools = "0.12.1" +itertools = "*" oxipng = { version = "9.1.1", default-features = false, features = ["parallel"] } diff --git a/TODO.txt b/TODO.txt index a0c24a7..d68634d 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,3 +1 @@ -- Look into a way to tell ratatui to skip diffing and just overwrite the whole thing 'cause we know it won't save time - Look into more efficient 'skip'ing in ratatui so that you don't have to unicode_width::width a bunch of escape codes -- Render to Box aot but only in the vicinity of the current view to save space diff --git a/ratatui b/ratatui index 9bd89c2..fadc73d 160000 --- a/ratatui +++ b/ratatui @@ -1 +1 @@ -Subproject commit 9bd89c218afb1f3999dce1bfe6edea5b7442966d +Subproject commit fadc73d62ef897e5065d041dcbac37bbb8d89698 diff --git a/src/main.rs b/src/main.rs index ae73321..a089a9f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -63,6 +63,7 @@ async fn main() -> Result<(), Box> { let backend = CrosstermBackend::new(std::io::stdout()); let mut term = Terminal::new(backend)?; + term.skip_diff(true); // poppler has some annoying logging (e.g. if you request a page index out-of-bounds of a // document's pages, then it will return `None`, but still log to stderr with CRITICAL level), @@ -117,17 +118,14 @@ async fn main() -> Result<(), Box> { Ok(RenderInfo::NumPages(num)) => { tui.set_n_pages(num); converter.set_n_pages(num); - true }, Ok(RenderInfo::Page(info)) => { + tui.got_num_results_on_page(info.page, info.search_results); converter.add_img(info); - false }, - Err(e) => { - tui.show_error(e); - true - } + Err(e) => tui.show_error(e), } + true } }; diff --git a/src/renderer.rs b/src/renderer.rs index 1cacdd6..3a4538b 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -59,6 +59,10 @@ pub fn start_rendering( } }; + // We want this outside of 'reload so that if the doc reloads, the search term that somebody + // set will still get highlighted in the reloaded doc + let mut search_term = None; + 'reload: loop { let doc = match Document::from_file(&path, None) { Err(e) => { @@ -78,7 +82,6 @@ pub fn start_rendering( // then we can split at that page and render at both sides of it let mut rendered = vec![false; n_pages]; let mut start_point = 0; - let mut search_term = None; // This is kinda a weird way of doing this, but if we get a notification that the area // changed, we want to start re-rending all of the pages, but we don't want to reload the @@ -109,7 +112,11 @@ pub fn start_rendering( }, RenderNotif::Search(term) => { rendered = vec![false; n_pages]; - search_term = Some(term); + if term.is_empty() { + search_term = None; + } else { + search_term = Some(term); + } continue 'render_pages; } } @@ -123,8 +130,9 @@ pub fn start_rendering( .map(|(idx, p)| (idx + start_point, p)) .interleave( left.iter_mut() + .rev() .enumerate() - .map(|(idx, p)| (idx - (start_point + 1), p)) + .map(|(idx, p)| (start_point - (idx + 1), p)) ); for (num, rendered) in page_iter { @@ -141,7 +149,11 @@ pub fn start_rendering( }; // We know this is in range 'cause we're iterating over it - let page = doc.page(num as i32).unwrap(); + let Some(page) = doc.page(num as i32) else { + sender.blocking_send(Err(RenderError::Render(format!("Couldn't get page {num} ({}) of doc?? (sp: {start_point}", num as i32)))) + .unwrap(); + continue; + }; // render the page let to_send = render_single_page(page, area, num, &search_term) @@ -239,9 +251,7 @@ fn render_single_page( .map(|term| page.find_text_with_options(term, FindFlags::DEFAULT | FindFlags::MULTILINE)) .unwrap_or_default(); - let num_results = result_rects.iter() - .filter(|rect| !rect.find_get_match_continued()) - .count(); + let num_results = result_rects.len(); let mut highlight_color = Color::new(); highlight_color.set_red((u16::MAX / 5) * 4); @@ -270,6 +280,8 @@ fn render_single_page( ctx.target().write_to_png(&mut img_data) .map_err(|e| format!("Couldn't write surface to png: {e}"))?; + // TODO: Maybe cache which pages had no results with the last search term so we don't have to + // rerender them when the search term is set to empty and rerenders are requested Ok(PageInfo { img_data: ImageData { data: img_data, diff --git a/src/tui.rs b/src/tui.rs index 7918b68..8094b7c 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -9,10 +9,9 @@ use crate::{renderer::RenderError, skip::Skip}; pub struct Tui { name: String, page: usize, - error: Option, - input_state: Option, last_render: LastRender, - rendered: Vec>, + bottom_msg: BottomMessage, + rendered: Vec, } #[derive(Default, Debug)] @@ -24,13 +23,23 @@ struct LastRender { unused_width: u16 } +#[derive(Default)] +enum BottomMessage { + #[default] + Help, + SearchResults(String), + Error(String), + Input(InputCommand) +} + enum InputCommand { GoToPage(usize), Search(String) } +#[derive(Default)] struct RenderedInfo { - img: Box, + img: Option>, num_results: usize } @@ -39,8 +48,7 @@ impl Tui { Self { name, page: 0, - error: None, - input_state: None, + bottom_msg: BottomMessage::Help, last_render: LastRender::default(), rendered: vec![], } @@ -107,7 +115,7 @@ impl Tui { let rendered_str = if !self.rendered.is_empty() { format!( "Rendered: {}%", - (self.rendered.iter().filter(|i| i.is_some()).count() * 100) / self.rendered.len() + (self.rendered.iter().filter(|i| i.img.is_some()).count() * 100) / self.rendered.len() ) } else { String::new() @@ -125,22 +133,30 @@ impl Tui { ); frame.render_widget(rendered_span, bottom_layout[1]); - if let Some(ref error_str) = self.error { - let span = Span::styled( - format!("Couldn't render a page: {error_str}"), - Style::new() - .fg(Color::Red) - ); - frame.render_widget(span, bottom_layout[0]); - } else if let Some(ref cmd) = self.input_state { - let cmd_str = match cmd { - InputCommand::GoToPage(page) => format!("Go to: {page}"), - InputCommand::Search(s) => format!("Search: {s}"), - }; + let (msg_str, color) = match self.bottom_msg { + BottomMessage::Help => ( + "/: Search, g: Go To Page".to_string(), + Color::Blue + ), + BottomMessage::Error(ref e) => ( + format!("Couldn't render a page: {e}"), + Color::Red + ), + BottomMessage::Input(ref input_state) => ( + match input_state { + InputCommand::GoToPage(page) => format!("Go to: {page}"), + InputCommand::Search(s) => format!("Search: {s}"), + }, + Color::Blue + ), + BottomMessage::SearchResults(ref term) => ( + format!("Results for '{term}': {}", self.rendered.iter().map(|r| r.num_results).sum::()), + Color::Blue + ), + }; - let span = Span::styled(cmd_str, Style::new().fg(Color::Blue)); - frame.render_widget(span, bottom_layout[0]); - } + let span = Span::styled(msg_str, Style::new().fg(color)); + frame.render_widget(span, bottom_layout[0]); let mut img_area = main_area[1]; @@ -160,12 +176,12 @@ impl Tui { // render each page) .enumerate() // and only take as many as are ready to be rendered - .take_while(|(_, page)| page.is_some()) + .take_while(|(_, page)| page.img.is_some()) // and map it to their width (in cells on the terminal, not pixels) .flat_map(|(idx, page)| - page.as_ref().map(|img| ( + page.img.as_ref().map(|img| ( idx, - img.img.rect().width, + img.rect().width, )) ) // and then take them as long as they won't overflow the available area. @@ -217,8 +233,10 @@ impl Tui { } fn render_single_page(&mut self, frame: &mut Frame<'_>, page_idx: usize, img_area: Rect) { - match self.rendered[page_idx] { - Some(ref page_img) => frame.render_widget(Image::new(&*page_img.img), img_area), + // TODO: Sometimes a page just won't render. But there will be space for it so we clearly + // know it should be there. Maybe we're not resetting the last render rect as we should be? + match self.rendered[page_idx].img { + Some(ref page_img) => frame.render_widget(Image::new(&**page_img), img_area), None => Self::render_loading_in(frame, img_area) }; } @@ -256,7 +274,7 @@ impl Tui { pub fn set_n_pages(&mut self, n_pages: usize) { self.rendered = Vec::with_capacity(n_pages); for _ in 0..n_pages { - self.rendered.push(None); + self.rendered.push(RenderedInfo::default()); } self.page = self.page.min(n_pages - 1); } @@ -281,18 +299,22 @@ impl Tui { // We always just set this here because we handle reloading in the `set_n_pages` function. // If the document was reloaded, then It'll have the `set_n_pages` called to set the new // number of pages, so the vec will already be cleared - self.rendered[page_num] = Some(RenderedInfo { img, num_results }) + self.rendered[page_num] = RenderedInfo { img: Some(img), num_results }; + } + + pub fn got_num_results_on_page(&mut self, page_num: usize, num_results: usize) { + self.rendered[page_num].num_results = num_results; } pub fn handle_event(&mut self, ev: Event) -> Option { match ev { Event::Key(key) => { match key.code { - KeyCode::Char(c) if let Some(InputCommand::Search(ref mut term)) = self.input_state => { + KeyCode::Char(c) if let BottomMessage::Input(InputCommand::Search(ref mut term)) = self.bottom_msg => { term.push(c); Some(InputAction::Redraw) }, - KeyCode::Char(c) if let Some(InputCommand::GoToPage(ref mut page)) = self.input_state => { + KeyCode::Char(c) if let BottomMessage::Input(InputCommand::GoToPage(ref mut page)) = self.bottom_msg => { c.to_digit(10) .map(|input_num| { *page = (*page * 10) + input_num as usize; @@ -303,42 +325,63 @@ impl Tui { KeyCode::Down | KeyCode::Char('j') => self.change_page(PageChange::Next, ChangeAmount::WholeScreen), KeyCode::Left | KeyCode::Char('h') => self.change_page(PageChange::Prev, ChangeAmount::Single), KeyCode::Up | KeyCode::Char('k') => self.change_page(PageChange::Prev, ChangeAmount::WholeScreen), - KeyCode::Esc | KeyCode::Char('q') => { - if self.input_state.is_some() { - self.input_state = None; - Some(InputAction::Redraw) - } else { - Some(InputAction::QuitApp) - } - }, + KeyCode::Esc | KeyCode::Char('q') => Some(InputAction::QuitApp), KeyCode::Char('g') => { - self.input_state = Some(InputCommand::GoToPage(0)); + self.bottom_msg = BottomMessage::Input(InputCommand::GoToPage(0)); Some(InputAction::Redraw) }, KeyCode::Char('/') => { - self.input_state = Some(InputCommand::Search(String::new())); + self.bottom_msg = BottomMessage::Input(InputCommand::Search(String::new())); Some(InputAction::Redraw) }, - KeyCode::Char('n') => { - let next_page = self.rendered[self.page..] + KeyCode::Char('n') if self.page < self.rendered.len() - 1 => { + // TODO: If we can't find one, then maybe like block until we've verified + // all the pages have been checked? + let next_page = self.rendered[(self.page + 1)..] .iter() .enumerate() - .filter_map(|(idx, p)| p.as_ref().map(|p| (idx, p))) - .find_map(|(idx, p)| (p.num_results > 0).then_some(idx)); + .find_map(|(idx, p)| (p.num_results > 0).then_some(self.page + 1 + idx)); if let Some(page) = next_page { self.page = page; + // Make sure we re-render + self.last_render.rect = Rect::default(); + Some(InputAction::JumpingToPage(page)) + } else { + None } - next_page.map(|_| InputAction::Redraw) }, - KeyCode::Enter => self.input_state.take() - .and_then(|cmd| match cmd { + // TODO: Add 'N' key to go back a search page + KeyCode::Enter => { + let BottomMessage::Input(_) = self.bottom_msg else { + return None; + }; + + let BottomMessage::Input(cmd) = std::mem::take(&mut self.bottom_msg) else { + // We need to verify it's an input msg currently, and only then take it + // and replace it by a default Help message. Don't exactly know how to + // do this otherwise. + unreachable!(); + }; + + match cmd { // Only forward the command if it's within range InputCommand::GoToPage(page) => (page < self.rendered.len()).then(|| { self.set_page(page); InputAction::JumpingToPage(page) }), - InputCommand::Search(term) => Some(InputAction::Search(term)), - }), + InputCommand::Search(term) => { + // We only want to show search results if there would actually be + // data to show + if !term.is_empty() { + self.bottom_msg = BottomMessage::SearchResults(term.clone()); + } + // but we still want to tell the rest of the system that we set the + // search term to '' so that they can re-render the pages wthout + // the highlighting + Some(InputAction::Search(term)) + } + } + }, _ => None, } }, @@ -357,7 +400,7 @@ impl Tui { } pub fn show_error(&mut self, err: RenderError) { - self.error = Some(match err { + self.bottom_msg = BottomMessage::Error(match err { RenderError::Doc(e) => format!("Couldn't open document: {e}"), RenderError::Render(e) => format!("Couldn't render page: {e}") });