Add formatter and format

This commit is contained in:
itsjunetime
2024-05-26 16:35:36 -06:00
parent 578f67deb6
commit aaa5b0b7ed
5 changed files with 201 additions and 142 deletions
+7
View File
@@ -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
+15 -7
View File
@@ -1,4 +1,7 @@
use std::{pin::Pin, task::{Context, Poll}}; use std::{
pin::Pin,
task::{Context, Poll}
};
use futures_util::Stream; use futures_util::Stream;
use image::ImageFormat; use image::ImageFormat;
@@ -79,15 +82,17 @@ impl Converter {
.interleave(self.page..idx_end) .interleave(self.page..idx_end)
.enumerate() .enumerate()
.skip(self.iteration) .skip(self.iteration)
.find_map(|(i_idx, p_idx)| .find_map(|(i_idx, p_idx)| self.images[p_idx].take().map(|p| (p, i_idx)))?;
self.images[p_idx].take().map(|p| (p, i_idx))
)?;
let img_area = page_info.img_data.area; 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) { let dyn_img =
match image::load_from_memory_with_format(&page_info.img_data.data, ImageFormat::Png) {
Ok(dt) => dt, Ok(dt) => dt,
Err(e) => return Some(Err(RenderError::Render(format!("Couldn't convert Vec<u8> to DynamicImage: {e}")))) Err(e) =>
return Some(Err(RenderError::Render(format!(
"Couldn't convert Vec<u8> to DynamicImage: {e}"
)))),
}; };
// We don't actually want to Crop this image, but we've already // We don't actually want to Crop this image, but we've already
@@ -96,7 +101,10 @@ impl Converter {
// resize it, we tell them to crop it to fit. // resize it, we tell them to crop it to fit.
let txt_img = match self.picker.new_protocol(dyn_img, img_area, Resize::Crop) { let txt_img = match self.picker.new_protocol(dyn_img, img_area, Resize::Crop) {
Ok(img) => img, 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 // update the iteration to the iteration that we stole this image from
+18 -9
View File
@@ -2,20 +2,26 @@
use std::{io::stdout, path::PathBuf, str::FromStr}; use std::{io::stdout, path::PathBuf, str::FromStr};
use converter::{Converter, ConvertedPage}; use converter::{ConvertedPage, Converter};
use crossterm::{execute, terminal::{disable_raw_mode, enable_raw_mode, EndSynchronizedUpdate, EnterAlternateScreen, LeaveAlternateScreen}}; use crossterm::{
execute,
terminal::{
disable_raw_mode, enable_raw_mode, EndSynchronizedUpdate, EnterAlternateScreen,
LeaveAlternateScreen
}
};
use futures_util::stream::StreamExt;
use glib::{LogField, LogLevel, LogWriterOutput}; use glib::{LogField, LogLevel, LogWriterOutput};
use notify::{RecursiveMode, Watcher}; use notify::{RecursiveMode, Watcher};
use ratatui::{backend::CrosstermBackend, Terminal}; use ratatui::{backend::CrosstermBackend, Terminal};
use ratatui_image::picker::Picker; use ratatui_image::picker::Picker;
use tui::{InputAction, Tui};
use futures_util::stream::StreamExt;
use renderer::{RenderInfo, RenderNotif}; use renderer::{RenderInfo, RenderNotif};
use tui::{InputAction, Tui};
mod tui;
mod renderer;
mod converter; mod converter;
mod renderer;
mod skip; mod skip;
mod tui;
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
@@ -33,7 +39,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// This shouldn't fail to send unless the receiver gets disconnected. If that's happened, // 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 // then like the main thread has panicked or something, so it doesn't matter if this panics
// as well // 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 // 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<dyn std::error::Error>> {
// then we want to spawn off the rendering task // 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, // 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 // 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 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()) .map(|n| n.to_string_lossy())
.unwrap_or_else(|| "Unknown file".into()) .unwrap_or_else(|| "Unknown file".into())
.to_string(); .to_string();
+33 -24
View File
@@ -1,6 +1,6 @@
use cairo::{Antialias, Format}; use cairo::{Antialias, Format};
use itertools::Itertools; use itertools::Itertools;
use poppler::{Color, Document, FindFlags, Page, SelectionStyle, Rectangle}; use poppler::{Color, Document, FindFlags, Page, Rectangle, SelectionStyle};
use ratatui::layout::Rect; use ratatui::layout::Rect;
use tokio::sync::mpsc::{error::TryRecvError, Receiver, Sender}; use tokio::sync::mpsc::{error::TryRecvError, Receiver, Sender};
@@ -21,7 +21,7 @@ pub enum RenderError {
pub enum RenderInfo { pub enum RenderInfo {
NumPages(usize), NumPages(usize),
Page(PageInfo), Page(PageInfo)
} }
pub struct PageInfo { pub struct PageInfo {
@@ -71,7 +71,7 @@ pub fn start_rendering(
area = r; area = r;
break; break;
} }
}; }
// We want this outside of 'reload so that if the doc reloads, the search term that somebody // 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 // set will still get highlighted in the reloaded doc
@@ -82,12 +82,14 @@ pub fn start_rendering(
Err(e) => { Err(e) => {
sender.blocking_send(Err(RenderError::Doc(e))).unwrap(); sender.blocking_send(Err(RenderError::Doc(e))).unwrap();
return; return;
}, }
Ok(d) => d Ok(d) => d
}; };
let n_pages = doc.n_pages() as usize; 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, // 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 // to support people jumping to specific pages and having quick rendering results. We
@@ -110,7 +112,8 @@ pub fn start_rendering(
match $notif { match $notif {
RenderNotif::Reload => continue 'reload, RenderNotif::Reload => continue 'reload,
RenderNotif::Area(new_area) => { 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; area = new_area;
// we only want to re-render pages if the new area is greater than the old // 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. // 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); fill_default(&mut rendered, n_pages);
continue 'render_pages; continue 'render_pages;
} }
}, }
RenderNotif::JumpToPage(page) => { RenderNotif::JumpToPage(page) => {
start_point = page; start_point = page;
continue 'render_pages; continue 'render_pages;
}, }
RenderNotif::Search(term) => { RenderNotif::Search(term) => {
if term.is_empty() { if term.is_empty() {
// If the term is set to nothing, then we don't need to re-render // 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; continue 'render_pages;
} }
} }
} };
} }
let (left, right) = rendered.split_at_mut(start_point); let (left, right) = rendered.split_at_mut(start_point);
let page_iter = right.iter_mut() let page_iter = right
.iter_mut()
.enumerate() .enumerate()
.map(|(idx, p)| (idx + start_point, p)) .map(|(idx, p)| (idx + start_point, p))
.interleave( .interleave(
@@ -185,14 +189,17 @@ pub fn start_rendering(
// We know this is in range 'cause we're iterating over it // We know this is in range 'cause we're iterating over it
let Some(page) = doc.page(num as i32) else { let Some(page) = doc.page(num as i32) else {
sender.blocking_send( sender
Err(RenderError::Render(format!("Couldn't get page {num} ({}) of doc?", num as i32))) .blocking_send(Err(RenderError::Render(format!(
) "Couldn't get page {num} ({}) of doc?",
num as i32
))))
.unwrap(); .unwrap();
continue; 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 // 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) {
@@ -205,14 +212,14 @@ pub fn start_rendering(
// And if we got an error, then obviously we need to propagate that // 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.blocking_send(Err(RenderError::Render(e))).unwrap()
} }
}; }
// Then once we've rendered all these pages, wait until we get another notification // Then once we've rendered all these pages, wait until we get another notification
// that this doc needs to be reloaded // that this doc needs to be reloaded
loop { loop {
// This once returned None despite the main thing being still connected (I think, at // This once returned None despite the main thing being still connected (I think, at
// last), so I'm just being safe here // last), so I'm just being safe here
let Some(msg) = receiver.blocking_recv() else { let Some(msg) = receiver.blocking_recv() else {
return return;
}; };
handle_notif!(msg); 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 // 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 // terms, then just return none to avoid this computation
if result_rects.is_empty() && already_rendered_no_results { 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 // 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. // think; it's at least something like that) on this terminal screen.
let size = crossterm::terminal::window_size() let size =
.map_err(|e| format!("Couldn't get window size: {e}"))?; crossterm::terminal::window_size().map_err(|e| format!("Couldn't get window size: {e}"))?;
let col_h = size.height / size.rows; let col_h = size.height / size.rows;
let col_w = size.width / size.columns; let col_w = size.width / size.columns;
@@ -285,9 +292,9 @@ fn render_single_page(
// rendered at higher quality. // rendered at higher quality.
surface_width as i32, surface_width as i32,
surface_height 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 ImageSurface: {e}"))?;
.map_err(|e| format!("Couldn't create Context: {e}"))?; let ctx = cairo::Context::new(surface).map_err(|e| format!("Couldn't create Context: {e}"))?;
ctx.scale(scale_factor, scale_factor); 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_source_rgba(1.0, 1.0, 1.0, 1.0);
ctx.set_antialias(Antialias::Best); 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); page.render(&ctx);
let num_results = result_rects.len(); let num_results = result_rects.len();
@@ -325,7 +333,8 @@ fn render_single_page(
ctx.scale(1. / scale_factor, 1. / scale_factor); ctx.scale(1. / scale_factor, 1. / scale_factor);
let mut img_data = Vec::new(); 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}"))?; .map_err(|e| format!("Couldn't write surface to png: {e}"))?;
Ok(Some(PageInfo { Ok(Some(PageInfo {
+116 -90
View File
@@ -1,7 +1,17 @@
use std::{io::stdout, rc::Rc}; use std::{io::stdout, rc::Rc};
use crossterm::{event::{Event, KeyCode, MouseEventKind}, execute, terminal::BeginSynchronizedUpdate}; use crossterm::{
use ratatui::{layout::{Constraint, Flex, Layout, Rect}, style::{Color, Style}, text::Span, widgets::{Block, Borders, Padding}, Frame}; 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 ratatui_image::{protocol::Protocol, Image};
use crate::{renderer::RenderError, skip::Skip}; 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 // we use `prev_msg` to, for example, restore the 'search results' message on the bottom after
// jumping to a specific page // jumping to a specific page
prev_msg: Option<BottomMessage>, prev_msg: Option<BottomMessage>,
rendered: Vec<RenderedInfo>, rendered: Vec<RenderedInfo>
} }
#[derive(Default, Debug)] #[derive(Default, Debug)]
@@ -61,7 +71,7 @@ impl Tui {
prev_msg: None, prev_msg: None,
bottom_msg: BottomMessage::Help, bottom_msg: BottomMessage::Help,
last_render: LastRender::default(), last_render: LastRender::default(),
rendered: vec![], rendered: vec![]
} }
} }
@@ -93,19 +103,12 @@ impl Tui {
let top_layout = Layout::horizontal([ let top_layout = Layout::horizontal([
Constraint::Fill(1), Constraint::Fill(1),
Constraint::Length(page_nums_text.len() as u16) Constraint::Length(page_nums_text.len() as u16)
]).split(top_area); ])
.split(top_area);
let title = Span::styled( let title = Span::styled(&self.name, Style::new().fg(Color::Cyan));
&self.name,
Style::new()
.fg(Color::Cyan)
);
let page_nums = Span::styled( let page_nums = Span::styled(&page_nums_text, Style::new().fg(Color::Cyan));
&page_nums_text,
Style::new()
.fg(Color::Cyan)
);
frame.render_widget(top_block, main_area[0]); frame.render_widget(top_block, main_area[0]);
frame.render_widget(title, top_layout[0]); frame.render_widget(title, top_layout[0]);
@@ -126,7 +129,8 @@ impl Tui {
let rendered_str = if !self.rendered.is_empty() { let rendered_str = if !self.rendered.is_empty() {
format!( format!(
"Rendered: {}%", "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 { } else {
String::new() String::new()
@@ -134,40 +138,45 @@ impl Tui {
let bottom_layout = Layout::horizontal([ let bottom_layout = Layout::horizontal([
Constraint::Fill(1), Constraint::Fill(1),
Constraint::Length(rendered_str.len() as u16) 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]); frame.render_widget(rendered_span, bottom_layout[1]);
let (msg_str, color) = match self.bottom_msg { let (msg_str, color) = match self.bottom_msg {
BottomMessage::Help => ( 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 Color::Blue
), ),
BottomMessage::Error(ref e) => ( BottomMessage::Error(ref e) => (format!("Couldn't render a page: {e}"), Color::Red),
format!("Couldn't render a page: {e}"),
Color::Red
),
BottomMessage::Input(ref input_state) => ( BottomMessage::Input(ref input_state) => (
match input_state { match input_state {
InputCommand::GoToPage(page) => format!("Go to: {page}"), InputCommand::GoToPage(page) => format!("Go to: {page}"),
InputCommand::Search(s) => format!("Search: {s}"), InputCommand::Search(s) => format!("Search: {s}")
}, },
Color::Blue Color::Blue
), ),
BottomMessage::SearchResults(ref term) => { BottomMessage::SearchResults(ref term) => {
let num_found = self.rendered.iter().filter_map(|r| r.num_results).sum::<usize>(); let num_found = self
let num_searched = self.rendered.iter().filter(|r| r.num_results.is_some()).count() * 100; .rendered
.iter()
.filter_map(|r| r.num_results)
.sum::<usize>();
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 Color::Blue
) )
}, }
}; };
let span = Span::styled(msg_str, Style::new().fg(color)); 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. // here we calculate how many pages can fit in the available area.
let mut test_area_w = img_area.width; let mut test_area_w = img_area.width;
// go through our pages, starting at the first one we want to view // 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 // and get their indices (I know it's offset, we fix it down below when we actually
// render each page) // render each page)
.enumerate() .enumerate()
// and only take as many as are ready to be rendered // and only take as many as are ready to be rendered
.take_while(|(_, page)| page.img.is_some()) .take_while(|(_, page)| page.img.is_some())
// and map it to their width (in cells on the terminal, not pixels) // and map it to their width (in cells on the terminal, not pixels)
.flat_map(|(idx, page)| .flat_map(|(idx, page)| page.img.as_ref().map(|img| (idx, img.rect().width)))
page.img.as_ref().map(|img| (
idx,
img.rect().width,
))
)
// and then take them as long as they won't overflow the available area. // and then take them as long as they won't overflow the available area.
.take_while(|(_, width)| { .take_while(|(_, width)| match test_area_w.checked_sub(*width) {
match test_area_w.checked_sub(*width) {
Some(new_val) => { Some(new_val) => {
test_area_w = new_val; test_area_w = new_val;
true true
},
None => false
} }
None => false
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@@ -218,10 +221,7 @@ impl Tui {
execute!(stdout(), BeginSynchronizedUpdate).unwrap(); execute!(stdout(), BeginSynchronizedUpdate).unwrap();
*end_update = true; *end_update = true;
let total_width = page_widths let total_width = page_widths.iter().map(|(_, w)| w).sum::<u16>();
.iter()
.map(|(_, w)| w)
.sum::<u16>();
self.last_render.pages_shown = page_widths.len(); 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 // 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 // remove it from the array of rendered pages to replace it with a text-rendered
// image) // 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; img_area.x += width;
} }
@@ -256,9 +259,8 @@ impl Tui {
fn render_loading_in(frame: &mut Frame<'_>, area: Rect) { fn render_loading_in(frame: &mut Frame<'_>, area: Rect) {
let loading_str = "Loading..."; let loading_str = "Loading...";
let inner_space = Layout::horizontal([ let inner_space = Layout::horizontal([Constraint::Length(loading_str.len() as u16)])
Constraint::Length(loading_str.len() as u16), .flex(Flex::Center)
]).flex(Flex::Center)
.split(area); .split(area);
let loading_span = Span::styled(loading_str, Style::new().fg(Color::Cyan)); let loading_span = Span::styled(loading_str, Style::new().fg(Color::Cyan));
@@ -275,7 +277,7 @@ impl Tui {
let old = self.page; let old = self.page;
match change { match change {
PageChange::Next => self.set_page((self.page + diff).min(self.rendered.len() - 1)), 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 { 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. // 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 // 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 // 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) { 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<InputAction> { pub fn handle_event(&mut self, ev: Event) -> Option<InputAction> {
fn jump_to_page(page: &mut usize, rect: &mut Rect, new_page: Option<usize>) -> Option<InputAction> { fn jump_to_page(
page: &mut usize,
rect: &mut Rect,
new_page: Option<usize>
) -> Option<InputAction> {
new_page.map(|new_page| { new_page.map(|new_page| {
*page = new_page; *page = new_page;
// Make sure we re-render // Make sure we re-render
@@ -332,62 +341,73 @@ impl Tui {
match ev { match ev {
Event::Key(key) => { Event::Key(key) => {
match key.code { 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); term.push(c);
Some(InputAction::Redraw) Some(InputAction::Redraw)
}, }
KeyCode::Char(c) if let BottomMessage::Input(InputCommand::GoToPage(ref mut page)) = self.bottom_msg => { KeyCode::Char(c)
c.to_digit(10) if let BottomMessage::Input(InputCommand::GoToPage(ref mut page)) =
.map(|input_num| { self.bottom_msg =>
c.to_digit(10).map(|input_num| {
*page = (*page * 10) + input_num as usize; *page = (*page * 10) + input_num as usize;
InputAction::Redraw InputAction::Redraw
}) }),
}, KeyCode::Right | KeyCode::Char('l') =>
KeyCode::Right | KeyCode::Char('l') => self.change_page(PageChange::Next, ChangeAmount::Single), self.change_page(PageChange::Next, ChangeAmount::Single),
KeyCode::Down | KeyCode::Char('j') => self.change_page(PageChange::Next, ChangeAmount::WholeScreen), KeyCode::Down | KeyCode::Char('j') =>
KeyCode::Left | KeyCode::Char('h') => self.change_page(PageChange::Prev, ChangeAmount::Single), self.change_page(PageChange::Next, ChangeAmount::WholeScreen),
KeyCode::Up | KeyCode::Char('k') => self.change_page(PageChange::Prev, 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 { KeyCode::Esc => match self.bottom_msg {
BottomMessage::Input(_) => { BottomMessage::Input(_) => {
self.set_bottom_msg(None); self.set_bottom_msg(None);
Some(InputAction::Redraw) Some(InputAction::Redraw)
}, }
_ => Some(InputAction::QuitApp) _ => Some(InputAction::QuitApp)
}, },
KeyCode::Char('q') => Some(InputAction::QuitApp), KeyCode::Char('q') => Some(InputAction::QuitApp),
KeyCode::Char('g') => { KeyCode::Char('g') => {
self.set_bottom_msg(Some(BottomMessage::Input(InputCommand::GoToPage(0)))); self.set_bottom_msg(Some(BottomMessage::Input(InputCommand::GoToPage(0))));
Some(InputAction::Redraw) Some(InputAction::Redraw)
}, }
KeyCode::Char('/') => { 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) Some(InputAction::Redraw)
}, }
KeyCode::Char('n') if self.page < self.rendered.len() - 1 => { 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 // TODO: If we can't find one, then maybe like block until we've verified
// all the pages have been checked? // all the pages have been checked?
let next_page = self.rendered[(self.page + 1)..] let next_page = self.rendered[(self.page + 1)..]
.iter() .iter()
.enumerate() .enumerate()
.find_map(|(idx, p)| p.num_results .find_map(|(idx, p)| {
p.num_results
.is_some_and(|num| num > 0) .is_some_and(|num| num > 0)
.then_some(self.page + 1 + idx) .then_some(self.page + 1 + idx)
); });
jump_to_page(&mut self.page, &mut self.last_render.rect, next_page) jump_to_page(&mut self.page, &mut self.last_render.rect, next_page)
}, }
KeyCode::Char('N') if self.page > 0 => { KeyCode::Char('N') if self.page > 0 => {
let prev_page = self.rendered[..(self.page)] let prev_page = self.rendered[..(self.page)]
.iter() .iter()
.rev() .rev()
.enumerate() .enumerate()
.find_map(|(idx, p)| p.num_results .find_map(|(idx, p)| {
p.num_results
.is_some_and(|num| num > 0) .is_some_and(|num| num > 0)
.then_some(self.page - (idx + 1)) .then_some(self.page - (idx + 1))
); });
jump_to_page(&mut self.page, &mut self.last_render.rect, prev_page) jump_to_page(&mut self.page, &mut self.last_render.rect, prev_page)
}, }
KeyCode::Enter => { KeyCode::Enter => {
let BottomMessage::Input(_) = self.bottom_msg else { let BottomMessage::Input(_) = self.bottom_msg else {
return None; return None;
@@ -409,14 +429,16 @@ impl Tui {
self.set_page(page); self.set_page(page);
InputAction::JumpingToPage(page) InputAction::JumpingToPage(page)
}) })
}, }
InputCommand::Search(term) => { InputCommand::Search(term) => {
let term = term.clone(); let term = term.clone();
// We only want to show search results if there would actually be // We only want to show search results if there would actually be
// data to show // data to show
if !term.is_empty() { if !term.is_empty() {
self.set_bottom_msg(Some(BottomMessage::SearchResults(term.clone()))); self.set_bottom_msg(Some(BottomMessage::SearchResults(
term.clone()
)));
} else { } else {
// else, if it's not empty, we just want to reset the bottom // 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 // area to show the default data; we don't want it to like show
@@ -434,21 +456,25 @@ impl Tui {
Some(InputAction::Search(term)) Some(InputAction::Search(term))
} }
} }
},
_ => None,
} }
}, _ => None
}
}
Event::Mouse(mouse) => match mouse.kind { Event::Mouse(mouse) => match mouse.kind {
MouseEventKind::ScrollRight => self.change_page(PageChange::Next, ChangeAmount::Single), MouseEventKind::ScrollRight =>
MouseEventKind::ScrollDown => self.change_page(PageChange::Next, ChangeAmount::WholeScreen), self.change_page(PageChange::Next, ChangeAmount::Single),
MouseEventKind::ScrollLeft => self.change_page(PageChange::Prev, ChangeAmount::Single), MouseEventKind::ScrollDown =>
MouseEventKind::ScrollUp => self.change_page(PageChange::Prev, ChangeAmount::WholeScreen), self.change_page(PageChange::Next, ChangeAmount::WholeScreen),
_ => None, 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 // 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 // we always check, regardless, if the available area for the images has
// changed. // changed.
_ => None, _ => None
} }
} }
@@ -474,12 +500,12 @@ impl Tui {
Some(mut msg) => { Some(mut msg) => {
std::mem::swap(&mut self.bottom_msg, &mut msg); std::mem::swap(&mut self.bottom_msg, &mut msg);
self.prev_msg = Some(msg); self.prev_msg = Some(msg);
}, }
None => { None => {
let mut new_bottom = self.prev_msg.take().unwrap_or_default(); let mut new_bottom = self.prev_msg.take().unwrap_or_default();
std::mem::swap(&mut self.bottom_msg, &mut new_bottom); std::mem::swap(&mut self.bottom_msg, &mut new_bottom);
self.prev_msg = Some(new_bottom); self.prev_msg = Some(new_bottom);
}, }
} }
} }
} }