Add search functionality

This commit is contained in:
itsjunetime
2024-05-19 23:15:42 -06:00
parent 85d805acb7
commit 097c2918e5
7 changed files with 197 additions and 98 deletions
Generated
+11 -11
View File
@@ -634,6 +634,12 @@ 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"
@@ -1005,24 +1011,28 @@ dependencies = [
[[package]]
name = "ratatui"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a564a852040e82671dc50a37d88f3aa83bbc690dfc6844cfe7a2591620206a80"
dependencies = [
"bitflags 2.5.0",
"cassowary",
"compact_str",
"crossterm",
"indoc",
"itertools",
"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",
@@ -1430,16 +1440,6 @@ 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",
"unicode-width",
]
[[package]]
name = "unicode-width"
version = "0.1.12"
+5 -5
View File
@@ -4,12 +4,12 @@ version = "0.1.0"
edition = "2021"
[dependencies]
poppler-rs = { version = "0.23.0" }
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.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 }
crossterm = { version = "0.27.0", features = ["event-stream"] }
image = { version = "0.24.9", features = ["png", "rayon"], default-features = false }
notify = "6.1.1"
+24 -10
View File
@@ -5,12 +5,12 @@ use image::ImageFormat;
use itertools::Itertools;
use ratatui_image::{picker::Picker, protocol::Protocol, Resize};
use crate::renderer::{ImageData, RenderError};
use crate::renderer::{PageInfo, RenderError};
const MAX_ITER: usize = 20;
pub struct Converter {
images: Vec<Option<ImageData>>,
images: Vec<Option<PageInfo>>,
picker: Picker,
page: usize,
// once it reaches 20, we're done rendering images
@@ -27,8 +27,12 @@ impl Converter {
}
}
pub fn add_img(&mut self, image: ImageData, idx: usize) {
self.images[idx] = Some(image);
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) {
@@ -71,17 +75,17 @@ impl Converter {
// then we go through all the indices available to us and find the first one that has an
// image available to steal
let (img_data, iteration, new_idx) = (idx_start..self.page)
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, p_idx))
self.images[p_idx].take().map(|p| (p, i_idx))
)?;
let img_area = img_data.area;
let img_area = page_info.img_data.area;
let dyn_img = match image::load_from_memory_with_format(&img_data.data, ImageFormat::Png) {
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<u8> to DynamicImage: {e}"))))
};
@@ -98,11 +102,21 @@ impl Converter {
// update the iteration to the iteration that we stole this image from
self.iteration = iteration;
Some(Ok((txt_img, new_idx)))
Some(Ok(ConvertedPage {
page: txt_img,
num: page_info.page,
num_results: page_info.search_results
}))
}
}
type ConversionResult = Result<(Box<dyn Protocol>, usize), RenderError>;
pub struct ConvertedPage {
pub page: Box<dyn Protocol>,
pub num: usize,
pub num_results: usize
}
type ConversionResult = Result<ConvertedPage, RenderError>;
impl Stream for Converter {
type Item = ConversionResult;
+26 -23
View File
@@ -1,6 +1,8 @@
#![feature(if_let_guard)]
use std::{io::stdout, path::PathBuf, str::FromStr};
use converter::Converter;
use converter::{Converter, ConvertedPage};
use crossterm::{execute, terminal::{disable_raw_mode, enable_raw_mode, EndSynchronizedUpdate, EnterAlternateScreen, LeaveAlternateScreen}};
use glib::{LogField, LogLevel, LogWriterOutput};
use notify::{RecursiveMode, Watcher};
@@ -18,7 +20,7 @@ mod skip;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = std::env::args().skip(1);
let file = args.next().expect("Program requires a file to process");
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);
@@ -80,54 +82,54 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
tui_tx.send(RenderNotif::Area(main_area[1])).await?;
loop {
let mut needs_redraw;
tokio::select! {
let mut needs_redraw = tokio::select! {
Some(img_res) = converter.next() => {
match img_res {
Ok((img, page)) => tui.page_ready(img, page),
Ok(ConvertedPage { page, num, num_results }) => tui.page_ready(page, num, num_results),
Err(e) => tui.show_error(e),
}
needs_redraw = true;
true
},
// First we check if we have any keystrokes
Some(ev) = ev_stream.next() => {
// If we can't get user input, just crash.
let ev = ev.expect("Couldn't get any user input");
needs_redraw = match tui.handle_event(ev) {
match tui.handle_event(ev) {
None => false,
Some(InputAction::Redraw) => true,
Some(InputAction::QuitApp) => break,
Some(InputAction::ChangePageBy(change)) => {
converter.change_page_by(change);
true
},
Some(InputAction::JumpingToPage(page)) => {
tui_tx.send(RenderNotif::JumpToPage(page)).await?;
converter.go_to_page(page);
Some(action) => {
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);
},
InputAction::Search(term) => tui_tx.send(RenderNotif::Search(term)).await?,
};
true
}
};
}
},
Some(renderer_msg) = tui_rx.recv() => {
needs_redraw = match renderer_msg {
match renderer_msg {
Ok(RenderInfo::NumPages(num)) => {
tui.set_n_pages(num);
converter.set_n_pages(num);
true
},
Ok(RenderInfo::Page(img, page_num)) => {
converter.add_img(img, page_num);
Ok(RenderInfo::Page(info)) => {
converter.add_img(info);
false
},
Err(e) => {
tui.show_error(e);
true
}
};
}
}
}
};
let new_area = Tui::main_layout(&term.get_frame());
if new_area != main_area {
@@ -140,6 +142,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
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;
})?;
if end_update {
+64 -13
View File
@@ -1,12 +1,13 @@
use cairo::{Antialias, Format};
use itertools::Itertools;
use poppler::{Document, Page};
use poppler::{Color, Document, FindFlags, Page, SelectionStyle, Rectangle};
use ratatui::layout::Rect;
use tokio::sync::mpsc::{error::TryRecvError, Receiver, Sender};
pub enum RenderNotif {
Area(Rect),
JumpToPage(usize),
Search(String),
Reload
}
@@ -20,7 +21,13 @@ pub enum RenderError {
pub enum RenderInfo {
NumPages(usize),
Page(ImageData, usize)
Page(PageInfo),
}
pub struct PageInfo {
pub img_data: ImageData,
pub page: usize,
pub search_results: usize
}
pub struct ImageData {
@@ -71,6 +78,7 @@ 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
@@ -98,6 +106,11 @@ pub fn start_rendering(
RenderNotif::JumpToPage(page) => {
start_point = page;
continue 'render_pages;
},
RenderNotif::Search(term) => {
rendered = vec![false; n_pages];
search_term = Some(term);
continue 'render_pages;
}
}
}
@@ -131,8 +144,8 @@ pub fn start_rendering(
let page = doc.page(num as i32).unwrap();
// render the page
let to_send = render_single_page(page, area)
.map(|data| RenderInfo::Page(data, num))
let to_send = render_single_page(page, area, num, &search_term)
.map(RenderInfo::Page)
.map_err(RenderError::Render);
// then send it over
@@ -157,7 +170,9 @@ pub fn start_rendering(
fn render_single_page(
page: Page,
area: Rect,
) -> Result<ImageData, String> {
page_num: usize,
search_term: &Option<String>
) -> Result<PageInfo, String> {
// 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()
@@ -214,21 +229,57 @@ fn render_single_page(
// The default background color of PDFs (at least, I think) is white, so we need to set
// that as the background color, then paint, then render.
ctx.set_source_rgba(1.0, 1.0, 1.0, 1.0);
ctx.set_antialias(Antialias::Best);
ctx.paint().map_err(|e| format!("Couldn't paint Context: {e}"))?;
page.render_for_printing(&ctx);
page.render(&ctx);
let mut result_rects = search_term
.as_ref()
.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 mut highlight_color = Color::new();
highlight_color.set_red((u16::MAX / 5) * 4);
highlight_color.set_green((u16::MAX / 5) * 4);
let mut old_rect = Rectangle::new();
for rect in result_rects.iter_mut() {
// According to https://gitlab.freedesktop.org/poppler/poppler/-/issues/763, these rects
// need to be corrected since they use different references as the y-coordinate base
rect.set_y1(p_height - rect.y1());
rect.set_y2(p_height - rect.y2());
page.render_selection(
&ctx,
rect,
&mut old_rect,
SelectionStyle::Glyph,
&mut Color::new(),
&mut highlight_color
);
}
ctx.scale(1. / scale_factor, 1. / scale_factor);
let mut img_data = Vec::new();
ctx.target().write_to_png(&mut img_data)
.map_err(|e| format!("Couldn't write surface to png: {e}"))?;
Ok(ImageData {
data: img_data,
area: Rect {
width: surface_width as u16 / col_w,
height: surface_height as u16 / col_h,
..Rect::default()
}
Ok(PageInfo {
img_data: ImageData {
data: img_data,
area: Rect {
width: surface_width as u16 / col_w,
height: surface_height as u16 / col_h,
..Rect::default()
}
},
page: page_num,
search_results: num_results
})
}
+66 -35
View File
@@ -12,7 +12,7 @@ pub struct Tui {
error: Option<String>,
input_state: Option<InputCommand>,
last_render: LastRender,
rendered: Vec<Option<Box<dyn Protocol>>>,
rendered: Vec<Option<RenderedInfo>>,
}
#[derive(Default, Debug)]
@@ -25,7 +25,13 @@ struct LastRender {
}
enum InputCommand {
GoToPage(usize)
GoToPage(usize),
Search(String)
}
struct RenderedInfo {
img: Box<dyn Protocol>,
num_results: usize
}
impl Tui {
@@ -98,16 +104,20 @@ impl Tui {
frame.render_widget(bottom_block, main_area[2]);
let rendered_str = format!(
"Rendered: {}%",
(self.rendered.iter().filter(|i| i.is_some()).count() * 100) / self.rendered.len()
);
let rendered_str = if !self.rendered.is_empty() {
format!(
"Rendered: {}%",
(self.rendered.iter().filter(|i| i.is_some()).count() * 100) / self.rendered.len()
)
} else {
String::new()
};
let bottom_layout = Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(rendered_str.len() as u16)
]).split(bottom_area);
let rendered_span = Span::styled(
&rendered_str,
Style::new()
@@ -123,16 +133,13 @@ impl Tui {
);
frame.render_widget(span, bottom_layout[0]);
} else if let Some(ref cmd) = self.input_state {
match cmd {
InputCommand::GoToPage(page) => {
let span = Span::styled(
format!("Go to: {page}"),
Style::new()
.fg(Color::Blue)
);
frame.render_widget(span, bottom_layout[0]);
}
}
let cmd_str = match cmd {
InputCommand::GoToPage(page) => format!("Go to: {page}"),
InputCommand::Search(s) => format!("Search: {s}"),
};
let span = Span::styled(cmd_str, Style::new().fg(Color::Blue));
frame.render_widget(span, bottom_layout[0]);
}
let mut img_area = main_area[1];
@@ -158,7 +165,7 @@ impl Tui {
.flat_map(|(idx, page)|
page.as_ref().map(|img| (
idx,
img.rect().width,
img.img.rect().width,
))
)
// and then take them as long as they won't overflow the available area.
@@ -211,7 +218,7 @@ 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_area),
Some(ref page_img) => frame.render_widget(Image::new(&*page_img.img), img_area),
None => Self::render_loading_in(frame, img_area)
};
}
@@ -254,12 +261,12 @@ impl Tui {
self.page = self.page.min(n_pages - 1);
}
pub fn page_ready(&mut self, img: Box<dyn Protocol>, page_num: usize) {
pub fn page_ready(&mut self, img: Box<dyn Protocol>, page_num: usize, num_results: usize) {
// If this new image woulda fit within the available space on the last render AND it's
// within the range where it might've been rendered with the last shown pages, then reset
// the last rect marker so that all images are forced to redraw on next render and this one
// is drawn with them
if page_num == self.page {
if page_num >= self.page && page_num <= self.page + self.last_render.pages_shown {
self.last_render.rect = Rect::default();
} else {
let img_w = img.rect().width;
@@ -274,40 +281,63 @@ 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(img);
self.rendered[page_num] = Some(RenderedInfo { img, num_results })
}
pub fn handle_event(&mut self, ev: Event) -> Option<InputAction> {
match ev {
Event::Key(key) => {
match key.code {
KeyCode::Right | KeyCode::Char('l') => self.change_page(PageChange::Next, ChangeAmount::Single),
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') => Some(InputAction::QuitApp),
KeyCode::Char('g') => {
self.input_state = Some(InputCommand::GoToPage(0));
KeyCode::Char(c) if let Some(InputCommand::Search(ref mut term)) = self.input_state => {
term.push(c);
Some(InputAction::Redraw)
},
KeyCode::Char(c) => {
let Some(InputCommand::GoToPage(ref mut page)) = self.input_state else {
return None;
};
KeyCode::Char(c) if let Some(InputCommand::GoToPage(ref mut page)) = self.input_state => {
c.to_digit(10)
.map(|input_num| {
*page = (*page * 10) + input_num as usize;
InputAction::Redraw
})
},
KeyCode::Right | KeyCode::Char('l') => self.change_page(PageChange::Next, ChangeAmount::Single),
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::Char('g') => {
self.input_state = Some(InputCommand::GoToPage(0));
Some(InputAction::Redraw)
},
KeyCode::Char('/') => {
self.input_state = Some(InputCommand::Search(String::new()));
Some(InputAction::Redraw)
},
KeyCode::Char('n') => {
let next_page = self.rendered[self.page..]
.iter()
.enumerate()
.filter_map(|(idx, p)| p.as_ref().map(|p| (idx, p)))
.find_map(|(idx, p)| (p.num_results > 0).then_some(idx));
if let Some(page) = next_page {
self.page = page;
}
next_page.map(|_| InputAction::Redraw)
},
KeyCode::Enter => self.input_state.take()
.and_then(|cmd| 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)),
}),
_ => None,
}
@@ -346,6 +376,7 @@ pub enum InputAction {
Redraw,
ChangePageBy(isize),
JumpingToPage(usize),
Search(String),
QuitApp
}