Compare commits

...

8 Commits

Author SHA1 Message Date
itsjunetime f9df806a8f Remove accidental comment 2024-11-17 17:34:59 -07:00
itsjunetime 5e7ec97b43 Flush EventStream when receiving a scroll event to work around high scroll sensitivities 2024-11-17 17:33:43 -07:00
itsjunetime 1aa26b8e8c Manually enable opinionated set of clippy lints 2024-11-16 22:03:31 -07:00
itsjunetime 40d46f1e2d Make tdf run on stable with --no-default-features 2024-11-15 14:14:54 -07:00
itsjunetime 927a9cb587 Update ratatui deps 2024-11-13 10:16:05 -07:00
itsjunetime e51e9d3464 Add --r-to-l and --max-wide flags to cli args 2024-11-03 16:41:58 -07:00
itsjunetime 7c13054383 Fix a few more small clippy issues 2024-10-26 15:45:20 -06:00
itsjunetime b1e26bc96b Add fields to Cargo.toml and add CHANGELOG.md 2024-10-26 15:34:11 -06:00
11 changed files with 945 additions and 233 deletions
+11
View File
@@ -0,0 +1,11 @@
# Unreleased
- Add `--r-to-l` flag to support displaying pdfs that read from right to left
- Add `--max-wide` flag to restrict amount of pages that can appear on the screen at a time
- Small internal changes to accomodate a few more clippy lints
- Update `ratatui` and `ratatui-image` git dependencies to latest upstream
- Move `ratatui-image/vb64` support under `nightly` feature, enabled by default
# v0.1.0
Initial tag :)
Generated
+595 -69
View File
File diff suppressed because it is too large Load Diff
+103 -4
View File
@@ -1,8 +1,15 @@
[package]
name = "tdf"
version = "0.1.0"
authors = ["June Welker <junewelker@gmail.com>"]
edition = "2021"
description = "A terminal viewer for PDFs"
readme = "README.md"
homepage = "https://github.com/itsjunetime/tdf"
repository = "https://github.com/itsjunetime/tdf"
license = "MPL-2.0"
keywords = ["pdf", "tui", "cli", "terminal"]
categories = ["command-line-utilities", "text-processing", "visualization"]
default-run = "tdf"
[[bin]]
@@ -17,10 +24,10 @@ poppler-rs = { version = "0.24.1", default-features = false, features = ["v23_7"
cairo-rs = { version = "0.20.0", default-features = false, features = ["png"] }
# we're using this branch because it has significant performance fixes that I'm waiting on responses from the upstream devs to get upstreamed. See https://github.com/ratatui-org/ratatui/issues/1116
ratatui = { git = "https://github.com/itsjunetime/ratatui.git" }
# ratatui = { path = "./ratatui" }
# ratatui = { path = "./ratatui/ratatui" }
# We're using this to have the vb64 feature (for faster base64 encoding, since that does take up a good bit of time when converting images to the Box<dyn ratatui_image::Protocol>. It also just includes a few more features that I'm waiting on main to upstream
ratatui-image = { git = "https://github.com/itsjunetime/ratatui-image.git", branch = "vb64_on_personal", features = ["rustix", "vb64"], default-features = false }
# ratatui-image = { path = "./ratatui-image", features = ["rustix", "vb64"], default-features = false }
ratatui-image = { git = "https://github.com/itsjunetime/ratatui-image.git", branch = "vb64_on_personal", default-features = false }
# ratatui-image = { path = "./ratatui-image", features = ["vb64"], default-features = false }
crossterm = { version = "0.28.1", features = ["event-stream"] }
image = { version = "0.25.1", features = ["png", "rayon"], default-features = false }
notify = { version = "7.0.0", features = ["crossbeam-channel"] }
@@ -29,6 +36,7 @@ futures-util = { version = "0.3.30", default-features = false }
glib = "0.20.0"
itertools = "*"
flume = { version = "0.11.0", default-features = false, features = ["async"] }
xflags = "0.4.0-pre.2"
# for tracing with tokio-console
console-subscriber = { version = "0.4.0", optional = true }
@@ -38,7 +46,8 @@ inherits = "release"
lto = "fat"
[features]
default = []
default = ["nightly"]
nightly = ["ratatui-image/vb64"]
tracing = ["tokio/tracing", "dep:console-subscriber"]
[dev-dependencies]
@@ -52,3 +61,93 @@ harness = false
[[bin]]
name = "for_profiling"
path = "./benches/for_profiling.rs"
[lints.clippy]
uninlined_format_args = "warn"
redundant_closure_for_method_calls = "warn"
cast_lossless = "warn"
single_char_pattern = "warn"
manual_let_else = "warn"
ignored_unit_patterns = "warn"
range_plus_one = "warn"
unreadable_literal = "warn"
redundant_else = "warn"
assigning_clones = "warn"
bool_to_int_with_if = "warn"
borrow_as_ptr = "warn"
cast_ptr_alignment = "warn"
checked_conversions = "warn"
copy_iterator = "warn"
default_trait_access = "warn"
doc_link_with_quotes = "warn"
empty_enum = "warn"
explicit_into_iter_loop = "warn"
explicit_iter_loop = "warn"
filter_map_next = "warn"
flat_map_option = "warn"
fn_params_excessive_bools = "warn"
from_iter_instead_of_collect = "warn"
implicit_clone = "warn"
index_refutable_slice = "warn"
inefficient_to_string = "warn"
invalid_upcast_comparisons = "warn"
iter_filter_is_ok = "warn"
iter_filter_is_some = "warn"
iter_not_returning_iterator = "warn"
large_futures = "warn"
large_stack_arrays = "warn"
large_types_passed_by_value = "warn"
linkedlist = "warn"
macro_use_imports = "warn"
manual_assert = "warn"
manual_instant_elapsed = "warn"
manual_is_power_of_two = "warn"
manual_is_variant_and = "warn"
manual_ok_or = "warn"
manual_string_new = "warn"
many_single_char_names = "warn"
manual_unwrap_or = "warn"
match_on_vec_items = "warn"
match_same_arms = "warn"
match_wildcard_for_single_variants = "warn"
maybe_infinite_iter = "warn"
mismatching_type_param_order = "warn"
missing_fields_in_debug = "warn"
mut_mut = "warn"
needless_bitwise_bool = "warn"
needless_continue = "warn"
needless_for_each = "warn"
needless_pass_by_value = "warn"
needless_raw_string_hashes = "warn"
no_effect_underscore_binding = "warn"
no_mangle_with_rust_abi = "warn"
option_as_ref_cloned = "warn"
option_option = "warn"
ptr_as_ptr = "warn"
ptr_cast_constness = "warn"
range_minus_one = "warn"
ref_as_ptr = "warn"
ref_binding_to_reference = "warn"
ref_option = "warn"
ref_option_ref = "warn"
return_self_not_must_use = "warn"
same_functions_in_if_condition = "warn"
should_panic_without_expect = "warn"
similar_names = "warn"
stable_sort_primitive = "warn"
str_split_at_newline = "warn"
struct_excessive_bools = "warn"
struct_field_names = "warn"
transmute_ptr_to_ptr = "warn"
trivially_copy_pass_by_ref = "warn"
unicode_not_nfc = "warn"
unnecessary_box_returns = "warn"
unnecessary_join = "warn"
unnecessary_literal_bound = "warn"
unnecessary_wraps = "warn"
unnested_or_patterns = "warn"
unused_async = "warn"
unused_self = "warn"
used_underscore_binding = "warn"
used_underscore_items = "warn"
zero_sized_map_values = "warn"
+4 -4
View File
@@ -75,7 +75,7 @@ pub fn start_rendering_loop(
width: columns * FONT_SIZE.0
};
std::thread::spawn(move || start_rendering(str_path, to_main_tx, from_main_rx, size));
std::thread::spawn(move || start_rendering(&str_path, to_main_tx, from_main_rx, size));
let main_area = Rect {
x: 0,
@@ -98,8 +98,8 @@ pub fn start_converting_loop(
let (to_converter_tx, from_main_rx) = unbounded();
let (to_main_tx, from_converter_rx) = unbounded();
let mut picker = Picker::new(FONT_SIZE);
picker.protocol_type = ProtocolType::Kitty;
let mut picker = Picker::from_fontsize(FONT_SIZE);
picker.set_protocol_type(ProtocolType::Kitty);
tokio::spawn(run_conversion_loop(
to_main_tx,
@@ -136,7 +136,7 @@ pub async fn render_doc(path: impl AsRef<Path>) {
to_render_tx
} = start_all_rendering(path);
while pages.is_empty() || pages.iter().any(|p| p.is_none()) {
while pages.is_empty() || pages.iter().any(Option::is_none) {
tokio::select! {
Some(renderer_msg) = from_render_rx.next() => {
handle_renderer_msg(renderer_msg, &mut pages, &mut to_converter_tx);
+1 -1
Submodule ratatui updated: a3fd887eaa...8bf0c1ef77
+1 -1
View File
@@ -7,7 +7,7 @@ use ratatui_image::{picker::Picker, protocol::Protocol, Resize};
use crate::renderer::{fill_default, PageInfo, RenderError};
pub struct ConvertedPage {
pub page: Box<dyn Protocol>,
pub page: Protocol,
pub num: usize,
pub num_results: usize
}
-2
View File
@@ -1,5 +1,3 @@
#![feature(if_let_guard)]
pub mod converter;
pub mod renderer;
pub mod skip;
+93 -53
View File
@@ -1,13 +1,14 @@
#![feature(if_let_guard)]
use std::{
future::poll_fn,
io::{stdout, Read, Write},
num::NonZeroUsize,
path::PathBuf,
str::FromStr
task::Poll
};
use converter::{run_conversion_loop, ConvertedPage, ConverterMsg};
use crossterm::{
event::{Event, EventStream, MouseEvent, MouseEventKind},
execute,
terminal::{
disable_raw_mode, enable_raw_mode, window_size, EndSynchronizedUpdate,
@@ -16,7 +17,7 @@ use crossterm::{
};
use futures_util::{stream::StreamExt, FutureExt};
use glib::{LogField, LogLevel, LogWriterOutput};
use notify::{Event, EventKind, RecursiveMode, Watcher};
use notify::{EventKind, RecursiveMode, Watcher};
use ratatui::{backend::CrosstermBackend, Terminal};
use ratatui_image::picker::Picker;
use renderer::{RenderError, RenderInfo, RenderNotif};
@@ -44,42 +45,33 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
#[cfg(feature = "tracing")]
console_subscriber::init();
let file = std::env::args()
.nth(1)
.ok_or("Program requires a file to process")?;
let path = PathBuf::from_str(&file)?.canonicalize()?;
let flags = xflags::parse_or_exit! {
/// Display the pdf with the pages starting at the right hand size and moving left and
/// adjust input keys to match
optional -r,--r-to-l r_to_l: bool
/// The maximum number of pages to display together, horizontally, at a time
optional -m,--max-wide max_wide: NonZeroUsize
/// PDF file to read
required file: PathBuf
};
let (watch_tx, render_rx) = flume::unbounded();
let tui_tx = watch_tx.clone();
let path = flags.file.canonicalize()?;
let (watch_to_render_tx, render_rx) = flume::unbounded();
let tui_tx = watch_to_render_tx.clone();
let (render_tx, tui_rx) = flume::unbounded();
let watch_to_tui_tx = render_tx.clone();
// we need to call this outside the recommended_watcher call because if we call it inside, that
// will be calling it from a thread not owned by the tokio runtime (since it's created by
// calling thread::spawn) and that will cause a panic
let mut watcher = notify::recommended_watcher(move |res: notify::Result<Event>| match res {
// If we get an error here, and then an error sending, everything's going wrong. Just give
// up lol.
Err(e) => watch_to_tui_tx.send(Err(RenderError::Notify(e))).unwrap(),
// TODO: Should we match EventKind::Rename and propogate that so that the other parts of the
// process know that too? Or should that be
Ok(ev) => match ev.kind {
EventKind::Access(_) => (),
EventKind::Remove(_) =>
drop(watch_to_tui_tx.send(Err(RenderError::Render("File was deleted".into())))),
// 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
// we don't handle the error here.
EventKind::Other | EventKind::Any | EventKind::Create(_) | EventKind::Modify(_) =>
drop(watch_tx.send(renderer::RenderNotif::Reload)),
}
})?;
let mut watcher =
notify::recommended_watcher(on_notify_ev(watch_to_tui_tx, watch_to_render_tx))?;
// We're making this nonrecursive 'cause we're just watching a single file, so there's nothing
// to recurse into
watcher.watch(&path, RecursiveMode::NonRecursive)?;
// TODO: Handle non-utf8 file names? Maybe by constructing a CString and passing that in to the
// poppler stuff instead of a rust string?
let file_path = format!("file://{}", path.clone().into_os_string().to_string_lossy());
let mut window_size = window_size()?;
@@ -96,7 +88,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// read in the returned size until we hit a 't' (which indicates to us it's done)
let input_vec = std::io::stdin()
.bytes()
.flat_map(|b| b.ok())
.filter_map(Result::ok)
.take_while(|b| *b != b't')
.collect::<Vec<_>>();
@@ -134,32 +126,27 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// We need to create `picker` on this thread because if we create it on the `renderer` thread,
// it messes up something with user input. Input never makes it to the crossterm thing
let mut picker = Picker::new((
window_size.width / window_size.columns,
window_size.height / window_size.rows
));
picker.guess_protocol();
let picker = Picker::from_query_stdio()?;
// 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, window_size)
renderer::start_rendering(&file_path, render_tx, render_rx, window_size)
});
let mut ev_stream = crossterm::event::EventStream::new();
let mut ev_stream = EventStream::new();
let (to_converter, from_main) = flume::unbounded();
let (to_main, from_converter) = flume::unbounded();
tokio::spawn(run_conversion_loop(to_main, from_main, picker, 20));
let file_name = path
.file_name()
.map(|n| n.to_string_lossy())
.unwrap_or_else(|| "Unknown file".into())
.to_string();
let mut tui = tui::Tui::new(file_name);
let file_name = path.file_name().map_or_else(
|| "Unknown file".into(),
|n| n.to_string_lossy().to_string()
);
let mut tui = tui::Tui::new(file_name, flags.max_wide, flags.r_to_l.unwrap_or_default());
let backend = CrosstermBackend::new(std::io::stdout());
let mut term = Terminal::new(backend)?;
@@ -184,16 +171,18 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut from_converter = from_converter.into_stream();
loop {
let mut needs_redraw = tokio::select! {
let mut needs_redraw = true;
tokio::select! {
// First we check if we have any keystrokes
Some(ev) = ev_stream.next().fuse() => {
// If we can't get user input, just crash.
let ev = ev.expect("Couldn't get any user input");
match tui.handle_event(ev) {
None => false,
Some(action) => {
match action {
flush_if_mouse(&ev, &mut ev_stream).await;
match tui.handle_event(&ev) {
None => needs_redraw = false,
Some(action) => match action {
InputAction::Redraw => (),
InputAction::QuitApp => break,
InputAction::JumpingToPage(page) => {
@@ -201,8 +190,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
to_converter.send(ConverterMsg::GoToPage(page))?;
},
InputAction::Search(term) => tui_tx.send(RenderNotif::Search(term))?,
};
true
}
}
},
@@ -225,14 +212,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
},
Err(e) => tui.show_error(e),
}
true
}
Some(img_res) = from_converter.next() => {
match img_res {
Ok(ConvertedPage { page, num, num_results }) => tui.page_ready(page, num, num_results),
Err(e) => tui.show_error(e),
}
true
},
};
@@ -261,6 +246,61 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
fn on_notify_ev(
to_tui_tx: flume::Sender<Result<RenderInfo, RenderError>>,
to_render_tx: flume::Sender<RenderNotif>
) -> impl Fn(notify::Result<notify::Event>) {
move |res| match res {
// If we get an error here, and then an error sending, everything's going wrong. Just give
// up lol.
Err(e) => to_tui_tx.send(Err(RenderError::Notify(e))).unwrap(),
// TODO: Should we match EventKind::Rename and propogate that so that the other parts of the
// process know that too? Or should that be
Ok(ev) => match ev.kind {
EventKind::Access(_) => (),
EventKind::Remove(_) =>
drop(to_tui_tx.send(Err(RenderError::Render("File was deleted".into())))),
// 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
// we don't handle the error here.
EventKind::Other | EventKind::Any | EventKind::Create(_) | EventKind::Modify(_) =>
drop(to_render_tx.send(renderer::RenderNotif::Reload)),
}
}
}
fn noop(_: LogLevel, _: &[LogField<'_>]) -> LogWriterOutput {
LogWriterOutput::Handled
}
async fn flush_if_mouse(ev: &Event, ev_stream: &mut EventStream) {
// If you have a high scroll sensitivity, on some platforms, crossterm sends 2+ mouse events per
// scroll. However, because tdf scrolls pages once per mouse event, that means that they can't
// scroll the pages of their PDF as they would expect - it jumps multiple pages per one scroll.
// So this just flushes the event queue once we detect a scroll event to make sure those extra
// scrolls are ignored.
//
// Theoretically, this introduces a race condition where events that arrive in between the
// processing of `ev` and the time we call `poll_next_unpin` can get ignored, but that's a very
// very small amount of time, so unlikely to happen. However, if that does become an issue, we
// can just flush the event queue until we see a non-scroll event, then store that event in a
// future that will return it next time we call `tokio::select!` in the main loop. That'll make
// sure we don't miss anything.
if let Event::Mouse(MouseEvent {
kind:
MouseEventKind::ScrollUp
| MouseEventKind::ScrollDown
| MouseEventKind::ScrollLeft
| MouseEventKind::ScrollRight,
..
}) = ev
{
poll_fn(|ctx| {
while ev_stream.poll_next_unpin(ctx).is_ready() {
println!("got another mouse event!");
}
Poll::Ready(())
})
.await;
}
}
+16 -12
View File
@@ -64,8 +64,12 @@ pub fn fill_default<T: Default>(vec: &mut Vec<T>, size: usize) {
// Also we just kinda 'unwrap' all of the send/recv calls here 'cause if they return an error, that
// means the other side's disconnected, which means that the main thread has panicked, which means
// we're done.
// We're allowing passing by value here because this is only called once, at the beginning of the
// program, and the arguments that 'should' be passed by value (`receiver` and `size`) would
// probably be more performant if accessed by-value instead of through a reference. Probably.
#[allow(clippy::needless_pass_by_value)]
pub fn start_rendering(
path: String,
path: &str,
mut sender: Sender<Result<RenderInfo, RenderError>>,
receiver: Receiver<RenderNotif>,
size: WindowSize
@@ -90,7 +94,7 @@ pub fn start_rendering(
let col_h = size.height / size.rows;
'reload: loop {
let doc = match Document::from_file(&path, None) {
let doc = match Document::from_file(path, None) {
Err(e) => {
// if there's an error, tell the main loop
sender.send(Err(RenderError::Doc(e)))?;
@@ -189,8 +193,8 @@ pub fn start_rendering(
.map(|(idx, p)| (start_point - (idx + 1), p))
);
let area_w = area.width as f64 * col_w as f64;
let area_h = area.height as f64 * col_h as f64;
let area_w = f64::from(area.width) * f64::from(col_w);
let area_h = f64::from(area.height) * f64::from(col_h);
// we go through each page
for (num, rendered) in page_iter {
@@ -227,8 +231,8 @@ pub fn start_rendering(
// render the page
match render_single_page_to_ctx(
page,
&search_term,
&page,
search_term.as_deref(),
rendered_with_no_results,
(area_w, area_h)
) {
@@ -251,11 +255,11 @@ pub fn start_rendering(
// since the effects of parallelizing that will be noticeable if the user
// tries to move through pages more quickly
if num == start_point {
render_ctx_to_png(ctx, &mut sender, (col_w, col_h), num)?;
render_ctx_to_png(&ctx, &mut sender, (col_w, col_h), num)?;
} else {
let mut sender = sender.clone();
thread::spawn(move || {
render_ctx_to_png(ctx, &mut sender, (col_w, col_h), num)
render_ctx_to_png(&ctx, &mut sender, (col_w, col_h), num)
});
}
}
@@ -298,8 +302,8 @@ struct RenderedContext {
unsafe impl Send for RenderedContext {}
fn render_single_page_to_ctx(
page: Page,
search_term: &Option<String>,
page: &Page,
search_term: Option<&str>,
already_rendered_no_results: bool,
(area_w, area_h): (f64, f64)
) -> Result<Option<RenderedContext>, String> {
@@ -375,7 +379,7 @@ fn render_single_page_to_ctx(
highlight_color.set_green((u16::MAX / 5) * 4);
let mut old_rect = Rectangle::new();
for rect in result_rects.iter_mut() {
for rect in &mut result_rects {
// 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());
@@ -401,7 +405,7 @@ fn render_single_page_to_ctx(
}
fn render_ctx_to_png(
ctx: RenderedContext,
ctx: &RenderedContext,
sender: &mut Sender<Result<RenderInfo, RenderError>>,
(col_w, col_h): (u16, u16),
page: usize
+82 -48
View File
@@ -1,4 +1,4 @@
use std::{io::stdout, rc::Rc};
use std::{io::stdout, num::NonZeroUsize, rc::Rc};
use crossterm::{
event::{Event, KeyCode, MouseEventKind},
@@ -24,7 +24,8 @@ 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<BottomMessage>,
rendered: Vec<RenderedInfo>
rendered: Vec<RenderedInfo>,
page_constraints: PageConstraints
}
#[derive(Default, Debug)]
@@ -50,12 +51,17 @@ pub enum InputCommand {
Search(String)
}
struct PageConstraints {
max_wide: Option<NonZeroUsize>,
r_to_l: bool
}
// This seems like a kinda weird struct because it holds two optionals but any representation
// within it is valid; I think it's the best way to represent it
#[derive(Default)]
struct RenderedInfo {
// The image, if it has been rendered by `Converter` to that struct
img: Option<Box<dyn Protocol>>,
img: Option<Protocol>,
// The number of results for the current search term that have been found on this page. None if
// we haven't checked this page yet
// Also this isn't the most efficient representation of this value, but it's accurate, so like
@@ -64,14 +70,15 @@ struct RenderedInfo {
}
impl Tui {
pub fn new(name: String) -> Tui {
pub fn new(name: String, max_wide: Option<NonZeroUsize>, r_to_l: bool) -> Tui {
Self {
name,
page: 0,
prev_msg: None,
bottom_msg: BottomMessage::Help,
last_render: LastRender::default(),
rendered: vec![]
rendered: vec![],
page_constraints: PageConstraints { max_wide, r_to_l }
}
}
@@ -196,15 +203,21 @@ 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..]
let mut 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())
.take_while(|(idx, page)| {
let mut take = page.img.is_some();
if let Some(max) = self.page_constraints.max_wide {
take &= *idx < max.get();
}
take
})
// 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)))
.filter_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) => {
@@ -215,6 +228,10 @@ impl Tui {
})
.collect::<Vec<_>>();
if self.page_constraints.r_to_l {
page_widths.reverse();
}
if page_widths.is_empty() {
// If none are ready to render, just show the loading thing
Self::render_loading_in(frame, img_area);
@@ -252,7 +269,7 @@ impl Tui {
fn render_single_page(&mut self, frame: &mut Frame<'_>, page_idx: usize, img_area: Rect) {
match self.rendered[page_idx].img {
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_area),
None => Self::render_loading_in(frame, img_area)
};
}
@@ -268,12 +285,23 @@ impl Tui {
frame.render_widget(loading_span, inner_space[0]);
}
fn change_page(&mut self, change: PageChange, amt: ChangeAmount) -> Option<InputAction> {
fn change_page(&mut self, mut change: PageChange, amt: ChangeAmount) -> Option<InputAction> {
let diff = match amt {
ChangeAmount::Single => 1,
ChangeAmount::WholeScreen => self.last_render.pages_shown
};
// This is a kinda weird way to switch around the controls for this sort of thing but it
// allows it to be pretty centralized and avoids annoyingly duplicated match arms (since
// we'd have to do `match key { 'h' if r_to_l | 'l' => {}}` and that doesn't play well with
// `if` guards on match arms)
if self.page_constraints.r_to_l {
change = match change {
PageChange::Next => PageChange::Prev,
PageChange::Prev => PageChange::Next
};
}
let old = self.page;
match change {
PageChange::Next => self.set_page((self.page + diff).min(self.rendered.len() - 1)),
@@ -293,7 +321,7 @@ impl Tui {
self.page = self.page.min(n_pages - 1);
}
pub fn page_ready(&mut self, img: Box<dyn Protocol>, page_num: usize, num_results: usize) {
pub fn page_ready(&mut self, img: 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
@@ -323,7 +351,7 @@ impl Tui {
self.rendered[page_num].num_results = Some(num_results);
}
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,
@@ -340,54 +368,37 @@ 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) => {
// TODO: refactor back to `if let` arm guards when those are stabilized
if let BottomMessage::Input(InputCommand::Search(ref mut term)) = self.bottom_msg {
term.push(c);
Some(InputAction::Redraw)
return Some(InputAction::Redraw);
}
KeyCode::Backspace
if let BottomMessage::Input(InputCommand::Search(ref mut term)) =
self.bottom_msg =>
{
term.pop();
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| {
if let BottomMessage::Input(InputCommand::GoToPage(ref mut page)) = self.bottom_msg {
return 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') => {
match c {
'l' => self.change_page(PageChange::Next, ChangeAmount::Single),
'j' => self.change_page(PageChange::Next, ChangeAmount::WholeScreen),
'h' => self.change_page(PageChange::Prev, ChangeAmount::Single),
'k' => self.change_page(PageChange::Prev, ChangeAmount::WholeScreen),
'q' => Some(InputAction::QuitApp),
'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()
))));
Some(InputAction::Redraw)
}
KeyCode::Char('n') if self.page < self.rendered.len() - 1 => {
'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)..]
@@ -401,7 +412,7 @@ impl Tui {
jump_to_page(&mut self.page, &mut self.last_render.rect, next_page)
}
KeyCode::Char('N') if self.page > 0 => {
'N' if self.page > 0 => {
let prev_page = self.rendered[..(self.page)]
.iter()
.rev()
@@ -413,7 +424,28 @@ impl Tui {
});
jump_to_page(&mut self.page, &mut self.last_render.rect, prev_page)
},
_ => None
}
},
KeyCode::Backspace => {
if let BottomMessage::Input(InputCommand::Search(ref mut term)) = self.bottom_msg {
term.pop();
return Some(InputAction::Redraw);
}
None
},
KeyCode::Right => self.change_page(PageChange::Next, ChangeAmount::Single),
KeyCode::Down => self.change_page(PageChange::Next, ChangeAmount::WholeScreen),
KeyCode::Left => self.change_page(PageChange::Prev, ChangeAmount::Single),
KeyCode::Up => 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::Enter => {
let BottomMessage::Input(_) = self.bottom_msg else {
return None;
@@ -522,11 +554,13 @@ pub enum InputAction {
QuitApp
}
#[derive(Copy, Clone)]
enum PageChange {
Prev,
Next
}
#[derive(Copy, Clone)]
enum ChangeAmount {
WholeScreen,
Single