diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..2721074 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,7 @@ +hard_tabs = true +match_arm_blocks = false +imports_granularity = "Crate" +overflow_delimited_expr = true +group_imports = "StdExternalCrate" +trailing_comma = "Never" +use_field_init_shorthand = true diff --git a/src/converter.rs b/src/converter.rs index 0915994..086b816 100644 --- a/src/converter.rs +++ b/src/converter.rs @@ -1,4 +1,7 @@ -use std::{pin::Pin, task::{Context, Poll}}; +use std::{ + pin::Pin, + task::{Context, Poll} +}; use futures_util::Stream; use image::ImageFormat; @@ -79,16 +82,18 @@ impl Converter { .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)) - )?; + .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}")))) - }; + 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 @@ -96,7 +101,10 @@ impl Converter { // 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}")))) + 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 diff --git a/src/main.rs b/src/main.rs index a089a9f..883e0d2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,20 +2,26 @@ use std::{io::stdout, path::PathBuf, str::FromStr}; -use converter::{Converter, ConvertedPage}; -use crossterm::{execute, terminal::{disable_raw_mode, enable_raw_mode, EndSynchronizedUpdate, EnterAlternateScreen, LeaveAlternateScreen}}; +use converter::{ConvertedPage, Converter}; +use crossterm::{ + execute, + terminal::{ + disable_raw_mode, enable_raw_mode, EndSynchronizedUpdate, EnterAlternateScreen, + LeaveAlternateScreen + } +}; +use futures_util::stream::StreamExt; use glib::{LogField, LogLevel, LogWriterOutput}; use notify::{RecursiveMode, Watcher}; use ratatui::{backend::CrosstermBackend, Terminal}; use ratatui_image::picker::Picker; -use tui::{InputAction, Tui}; -use futures_util::stream::StreamExt; use renderer::{RenderInfo, RenderNotif}; +use tui::{InputAction, Tui}; -mod tui; -mod renderer; mod converter; +mod renderer; mod skip; +mod tui; #[tokio::main(flavor = "current_thread")] async fn main() -> Result<(), Box> { @@ -33,7 +39,9 @@ async fn main() -> Result<(), Box> { // This shouldn't fail to send unless the receiver gets disconnected. If that's happened, // 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).unwrap(); + watch_tx + .blocking_send(renderer::RenderNotif::Reload) + .unwrap(); })?; // We're making this nonrecursive 'cause we're just watching a single file, so there's nothing @@ -51,11 +59,12 @@ async fn main() -> Result<(), Box> { // 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)); let mut ev_stream = crossterm::event::EventStream::new(); - let file_name = path.file_name() + let file_name = path + .file_name() .map(|n| n.to_string_lossy()) .unwrap_or_else(|| "Unknown file".into()) .to_string(); diff --git a/src/renderer.rs b/src/renderer.rs index 8fec3f1..8b4bb5c 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -1,6 +1,6 @@ use cairo::{Antialias, Format}; use itertools::Itertools; -use poppler::{Color, Document, FindFlags, Page, SelectionStyle, Rectangle}; +use poppler::{Color, Document, FindFlags, Page, Rectangle, SelectionStyle}; use ratatui::layout::Rect; use tokio::sync::mpsc::{error::TryRecvError, Receiver, Sender}; @@ -21,7 +21,7 @@ pub enum RenderError { pub enum RenderInfo { NumPages(usize), - Page(PageInfo), + Page(PageInfo) } pub struct PageInfo { @@ -71,7 +71,7 @@ pub fn start_rendering( area = r; break; } - }; + } // 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 @@ -82,12 +82,14 @@ pub fn start_rendering( Err(e) => { sender.blocking_send(Err(RenderError::Doc(e))).unwrap(); return; - }, + } Ok(d) => d }; let n_pages = doc.n_pages() as usize; - sender.blocking_send(Ok(RenderInfo::NumPages(n_pages))).unwrap(); + sender + .blocking_send(Ok(RenderInfo::NumPages(n_pages))) + .unwrap(); // We're using this vec of bools to indicate which page numbers have already been rendered, // to support people jumping to specific pages and having quick rendering results. We @@ -110,7 +112,8 @@ pub fn start_rendering( match $notif { RenderNotif::Reload => continue 'reload, RenderNotif::Area(new_area) => { - let bigger = new_area.width > area.width || new_area.height > area.height; + let bigger = + new_area.width > area.width || new_area.height > area.height; area = new_area; // we only want to re-render pages if the new area is greater than the old // one, 'cause then we might need sharper images to make it all look good. @@ -120,11 +123,11 @@ pub fn start_rendering( fill_default(&mut rendered, n_pages); continue 'render_pages; } - }, + } RenderNotif::JumpToPage(page) => { start_point = page; continue 'render_pages; - }, + } RenderNotif::Search(term) => { if term.is_empty() { // If the term is set to nothing, then we don't need to re-render @@ -149,12 +152,13 @@ pub fn start_rendering( continue 'render_pages; } } - } + }; } let (left, right) = rendered.split_at_mut(start_point); - let page_iter = right.iter_mut() + let page_iter = right + .iter_mut() .enumerate() .map(|(idx, p)| (idx + start_point, p)) .interleave( @@ -185,14 +189,17 @@ 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!("Couldn't get page {num} ({}) of doc?", num as i32))) - ) + sender + .blocking_send(Err(RenderError::Render(format!( + "Couldn't get page {num} ({}) of doc?", + num as i32 + )))) .unwrap(); continue; }; - let rendered_with_no_results = rendered.successful && rendered.contained_term == Some(false); + let rendered_with_no_results = + rendered.successful && rendered.contained_term == Some(false); // render the page match render_single_page(page, area, num, &search_term, rendered_with_no_results) { @@ -205,14 +212,14 @@ pub fn start_rendering( // And if we got an error, then obviously we need to propagate that Err(e) => sender.blocking_send(Err(RenderError::Render(e))).unwrap() } - }; + } // Then once we've rendered all these pages, wait until we get another notification // that this doc needs to be reloaded loop { // This once returned None despite the main thing being still connected (I think, at // last), so I'm just being safe here let Some(msg) = receiver.blocking_recv() else { - return + return; }; handle_notif!(msg); } @@ -235,13 +242,13 @@ fn render_single_page( // If there are no search terms on this page, and we've already rendered it with no search // terms, then just return none to avoid this computation if result_rects.is_empty() && already_rendered_no_results { - return Ok(None) + return Ok(None); } // 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 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; @@ -285,9 +292,9 @@ fn render_single_page( // rendered at higher quality. surface_width as i32, surface_height as i32 - ).map_err(|e| format!("Couldn't create ImageSurface: {e}"))?; - let ctx = cairo::Context::new(surface) - .map_err(|e| format!("Couldn't create Context: {e}"))?; + ) + .map_err(|e| format!("Couldn't create ImageSurface: {e}"))?; + let ctx = cairo::Context::new(surface).map_err(|e| format!("Couldn't create Context: {e}"))?; ctx.scale(scale_factor, scale_factor); @@ -296,7 +303,8 @@ fn render_single_page( 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}"))?; + ctx.paint() + .map_err(|e| format!("Couldn't paint Context: {e}"))?; page.render(&ctx); let num_results = result_rects.len(); @@ -325,7 +333,8 @@ fn render_single_page( ctx.scale(1. / scale_factor, 1. / scale_factor); let mut img_data = Vec::new(); - ctx.target().write_to_png(&mut img_data) + ctx.target() + .write_to_png(&mut img_data) .map_err(|e| format!("Couldn't write surface to png: {e}"))?; Ok(Some(PageInfo { diff --git a/src/tui.rs b/src/tui.rs index d3648f3..99e4108 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -1,7 +1,17 @@ use std::{io::stdout, rc::Rc}; -use crossterm::{event::{Event, KeyCode, MouseEventKind}, execute, terminal::BeginSynchronizedUpdate}; -use ratatui::{layout::{Constraint, Flex, Layout, Rect}, style::{Color, Style}, text::Span, widgets::{Block, Borders, Padding}, Frame}; +use crossterm::{ + event::{Event, KeyCode, MouseEventKind}, + execute, + terminal::BeginSynchronizedUpdate +}; +use ratatui::{ + layout::{Constraint, Flex, Layout, Rect}, + style::{Color, Style}, + text::Span, + widgets::{Block, Borders, Padding}, + Frame +}; use ratatui_image::{protocol::Protocol, Image}; use crate::{renderer::RenderError, skip::Skip}; @@ -14,7 +24,7 @@ pub struct Tui { // we use `prev_msg` to, for example, restore the 'search results' message on the bottom after // jumping to a specific page prev_msg: Option, - rendered: Vec, + rendered: Vec } #[derive(Default, Debug)] @@ -61,7 +71,7 @@ impl Tui { prev_msg: None, bottom_msg: BottomMessage::Help, last_render: LastRender::default(), - rendered: vec![], + rendered: vec![] } } @@ -93,19 +103,12 @@ impl Tui { let top_layout = Layout::horizontal([ Constraint::Fill(1), Constraint::Length(page_nums_text.len() as u16) - ]).split(top_area); + ]) + .split(top_area); - let title = Span::styled( - &self.name, - Style::new() - .fg(Color::Cyan) - ); + let title = Span::styled(&self.name, Style::new().fg(Color::Cyan)); - let page_nums = Span::styled( - &page_nums_text, - Style::new() - .fg(Color::Cyan) - ); + let page_nums = Span::styled(&page_nums_text, Style::new().fg(Color::Cyan)); frame.render_widget(top_block, main_area[0]); frame.render_widget(title, top_layout[0]); @@ -126,7 +129,8 @@ impl Tui { let rendered_str = if !self.rendered.is_empty() { format!( "Rendered: {}%", - (self.rendered.iter().filter(|i| i.img.is_some()).count() * 100) / self.rendered.len() + (self.rendered.iter().filter(|i| i.img.is_some()).count() * 100) + / self.rendered.len() ) } else { String::new() @@ -134,40 +138,45 @@ impl Tui { let bottom_layout = Layout::horizontal([ Constraint::Fill(1), Constraint::Length(rendered_str.len() as u16) - ]).split(bottom_area); + ]) + .split(bottom_area); - - let rendered_span = Span::styled( - &rendered_str, - Style::new() - .fg(Color::Cyan) - ); + let rendered_span = Span::styled(&rendered_str, Style::new().fg(Color::Cyan)); frame.render_widget(rendered_span, bottom_layout[1]); let (msg_str, color) = match self.bottom_msg { BottomMessage::Help => ( - "/: Search, g: Go To Page, n: Next Search Result, N: Previous Search Result".to_string(), + "/: Search, g: Go To Page, n: Next Search Result, N: Previous Search Result" + .to_string(), Color::Blue ), - BottomMessage::Error(ref e) => ( - format!("Couldn't render a page: {e}"), - Color::Red - ), + 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}"), + InputCommand::Search(s) => format!("Search: {s}") }, Color::Blue ), BottomMessage::SearchResults(ref term) => { - let num_found = self.rendered.iter().filter_map(|r| r.num_results).sum::(); - let num_searched = self.rendered.iter().filter(|r| r.num_results.is_some()).count() * 100; + let num_found = self + .rendered + .iter() + .filter_map(|r| r.num_results) + .sum::(); + let num_searched = self + .rendered + .iter() + .filter(|r| r.num_results.is_some()) + .count() * 100; ( - format!("Results for '{term}': {num_found} (searched: {}%)", num_searched / self.rendered.len()), + format!( + "Results for '{term}': {num_found} (searched: {}%)", + num_searched / self.rendered.len() + ), Color::Blue ) - }, + } }; let span = Span::styled(msg_str, Style::new().fg(color)); @@ -186,28 +195,22 @@ impl Tui { // here we calculate how many pages can fit in the available area. let mut test_area_w = img_area.width; // go through our pages, starting at the first one we want to view - let page_widths = self.rendered[self.page..].iter() + let page_widths = self.rendered[self.page..] + .iter() // and get their indices (I know it's offset, we fix it down below when we actually // render each page) .enumerate() // and only take as many as are ready to be rendered .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.img.as_ref().map(|img| ( - idx, - img.rect().width, - )) - ) + .flat_map(|(idx, page)| page.img.as_ref().map(|img| (idx, img.rect().width))) // and then take them as long as they won't overflow the available area. - .take_while(|(_, width)| { - match test_area_w.checked_sub(*width) { - Some(new_val) => { - test_area_w = new_val; - true - }, - None => false + .take_while(|(_, width)| match test_area_w.checked_sub(*width) { + Some(new_val) => { + test_area_w = new_val; + true } + None => false }) .collect::>(); @@ -218,10 +221,7 @@ impl Tui { execute!(stdout(), BeginSynchronizedUpdate).unwrap(); *end_update = true; - let total_width = page_widths - .iter() - .map(|(_, w)| w) - .sum::(); + let total_width = page_widths.iter().map(|(_, w)| w).sum::(); self.last_render.pages_shown = page_widths.len(); @@ -236,7 +236,10 @@ impl Tui { // reference + an immutable reference (and also we need to potentially temporarily // remove it from the array of rendered pages to replace it with a text-rendered // image) - self.render_single_page(frame, page_idx + self.page, Rect { width, ..img_area }); + self.render_single_page(frame, page_idx + self.page, Rect { + width, + ..img_area + }); img_area.x += width; } @@ -256,10 +259,9 @@ impl Tui { fn render_loading_in(frame: &mut Frame<'_>, area: Rect) { let loading_str = "Loading..."; - let inner_space = Layout::horizontal([ - Constraint::Length(loading_str.len() as u16), - ]).flex(Flex::Center) - .split(area); + let inner_space = Layout::horizontal([Constraint::Length(loading_str.len() as u16)]) + .flex(Flex::Center) + .split(area); let loading_span = Span::styled(loading_str, Style::new().fg(Color::Cyan)); @@ -275,7 +277,7 @@ impl Tui { let old = self.page; match change { PageChange::Next => self.set_page((self.page + diff).min(self.rendered.len() - 1)), - PageChange::Prev => self.set_page(self.page.saturating_sub(diff)), + PageChange::Prev => self.set_page(self.page.saturating_sub(diff)) } match self.page as isize - old as isize { @@ -312,7 +314,10 @@ 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] = RenderedInfo { img: Some(img), num_results: Some(num_results) }; + self.rendered[page_num] = RenderedInfo { + img: Some(img), + num_results: Some(num_results) + }; } pub fn got_num_results_on_page(&mut self, page_num: usize, num_results: usize) { @@ -320,7 +325,11 @@ impl Tui { } pub fn handle_event(&mut self, ev: Event) -> Option { - fn jump_to_page(page: &mut usize, rect: &mut Rect, new_page: Option) -> Option { + fn jump_to_page( + page: &mut usize, + rect: &mut Rect, + new_page: Option + ) -> Option { new_page.map(|new_page| { *page = new_page; // Make sure we re-render @@ -332,62 +341,73 @@ impl Tui { match ev { Event::Key(key) => { match key.code { - KeyCode::Char(c) if let BottomMessage::Input(InputCommand::Search(ref mut term)) = self.bottom_msg => { + 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 BottomMessage::Input(InputCommand::GoToPage(ref mut page)) = self.bottom_msg => { - 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::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; + 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 => match self.bottom_msg { BottomMessage::Input(_) => { self.set_bottom_msg(None); Some(InputAction::Redraw) - }, + } _ => Some(InputAction::QuitApp) }, KeyCode::Char('q') => Some(InputAction::QuitApp), KeyCode::Char('g') => { self.set_bottom_msg(Some(BottomMessage::Input(InputCommand::GoToPage(0)))); Some(InputAction::Redraw) - }, + } KeyCode::Char('/') => { - self.set_bottom_msg(Some(BottomMessage::Input(InputCommand::Search(String::new())))); + self.set_bottom_msg(Some(BottomMessage::Input(InputCommand::Search( + String::new() + )))); Some(InputAction::Redraw) - }, + } 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() - .find_map(|(idx, p)| p.num_results - .is_some_and(|num| num > 0) - .then_some(self.page + 1 + idx) - ); + .find_map(|(idx, p)| { + p.num_results + .is_some_and(|num| num > 0) + .then_some(self.page + 1 + idx) + }); jump_to_page(&mut self.page, &mut self.last_render.rect, next_page) - }, + } KeyCode::Char('N') if self.page > 0 => { let prev_page = self.rendered[..(self.page)] .iter() .rev() .enumerate() - .find_map(|(idx, p)| p.num_results - .is_some_and(|num| num > 0) - .then_some(self.page - (idx + 1)) - ); + .find_map(|(idx, p)| { + p.num_results + .is_some_and(|num| num > 0) + .then_some(self.page - (idx + 1)) + }); jump_to_page(&mut self.page, &mut self.last_render.rect, prev_page) - }, + } KeyCode::Enter => { let BottomMessage::Input(_) = self.bottom_msg else { return None; @@ -409,14 +429,16 @@ impl Tui { self.set_page(page); InputAction::JumpingToPage(page) }) - }, + } InputCommand::Search(term) => { let term = term.clone(); // We only want to show search results if there would actually be // data to show if !term.is_empty() { - self.set_bottom_msg(Some(BottomMessage::SearchResults(term.clone()))); + self.set_bottom_msg(Some(BottomMessage::SearchResults( + term.clone() + ))); } else { // else, if it's not empty, we just want to reset the bottom // area to show the default data; we don't want it to like show @@ -434,21 +456,25 @@ impl Tui { Some(InputAction::Search(term)) } } - }, - _ => None, + } + _ => None } - }, - Event::Mouse(mouse) => match mouse.kind { - MouseEventKind::ScrollRight => self.change_page(PageChange::Next, ChangeAmount::Single), - MouseEventKind::ScrollDown => self.change_page(PageChange::Next, ChangeAmount::WholeScreen), - MouseEventKind::ScrollLeft => self.change_page(PageChange::Prev, ChangeAmount::Single), - MouseEventKind::ScrollUp => self.change_page(PageChange::Prev, ChangeAmount::WholeScreen), - _ => None, } + Event::Mouse(mouse) => match mouse.kind { + MouseEventKind::ScrollRight => + self.change_page(PageChange::Next, ChangeAmount::Single), + MouseEventKind::ScrollDown => + self.change_page(PageChange::Next, ChangeAmount::WholeScreen), + MouseEventKind::ScrollLeft => + self.change_page(PageChange::Prev, ChangeAmount::Single), + MouseEventKind::ScrollUp => + self.change_page(PageChange::Prev, ChangeAmount::WholeScreen), + _ => None + }, // One of these options is Event::Resize, and we don't care about that because // we always check, regardless, if the available area for the images has // changed. - _ => None, + _ => None } } @@ -474,12 +500,12 @@ impl Tui { Some(mut msg) => { std::mem::swap(&mut self.bottom_msg, &mut msg); self.prev_msg = Some(msg); - }, + } None => { let mut new_bottom = self.prev_msg.take().unwrap_or_default(); std::mem::swap(&mut self.bottom_msg, &mut new_bottom); self.prev_msg = Some(new_bottom); - }, + } } } }