mirror of
https://github.com/itsjunetime/tdf.git
synced 2026-06-01 23:51:46 -04:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e9053acbc | |||
| e67a2ec421 | |||
| 40d46f1e2d | |||
| 927a9cb587 | |||
| e51e9d3464 | |||
| 7c13054383 | |||
| b1e26bc96b |
@@ -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
File diff suppressed because it is too large
Load Diff
+14
-5
@@ -1,8 +1,15 @@
|
||||
[package]
|
||||
name = "tdf"
|
||||
version = "0.1.0"
|
||||
authors = ["June Welker <junewelker@gmail.com>"]
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
description = "A terminal viewer for PDFs"
|
||||
readme = "README.md"
|
||||
homepage = "https://github.com/itsjunetime/tdf"
|
||||
repository = "https://github.com/itsjunetime/tdf"
|
||||
license = "GPL-3.0-or-later"
|
||||
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]
|
||||
|
||||
@@ -29,3 +29,5 @@ I dunno. Just for fun, mostly.
|
||||
## Can I contribute?
|
||||
|
||||
Yeah, sure. Please do.
|
||||
|
||||
Please note, though, that all contributions will be treated as licensed under MPL-2.0. This is so that we can relicense to MPL-2.0 at some point in the future if we manage to move away from poppler as a backend (since that is the only dependency, at time of writing, which requires the GPLv3 license).
|
||||
|
||||
+4
-4
@@ -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
Submodule ratatui-image updated: 5a11b82542...cb4b4ffab6
+1
-1
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![feature(if_let_guard)]
|
||||
|
||||
pub mod converter;
|
||||
pub mod renderer;
|
||||
pub mod skip;
|
||||
|
||||
+62
-59
@@ -1,9 +1,7 @@
|
||||
#![feature(if_let_guard)]
|
||||
|
||||
use std::{
|
||||
io::{stdout, Read, Write},
|
||||
path::PathBuf,
|
||||
str::FromStr
|
||||
num::NonZeroUsize,
|
||||
path::PathBuf
|
||||
};
|
||||
|
||||
use converter::{run_conversion_loop, ConvertedPage, ConverterMsg};
|
||||
@@ -44,42 +42,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 +85,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,17 +123,13 @@ 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();
|
||||
@@ -154,12 +139,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
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,25 +168,23 @@ 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 {
|
||||
InputAction::Redraw => (),
|
||||
InputAction::QuitApp => break,
|
||||
InputAction::JumpingToPage(page) => {
|
||||
tui_tx.send(RenderNotif::JumpToPage(page))?;
|
||||
to_converter.send(ConverterMsg::GoToPage(page))?;
|
||||
},
|
||||
InputAction::Search(term) => tui_tx.send(RenderNotif::Search(term))?,
|
||||
};
|
||||
true
|
||||
match tui.handle_event(&ev) {
|
||||
None => needs_redraw = false,
|
||||
Some(action) => match action {
|
||||
InputAction::Redraw => (),
|
||||
InputAction::QuitApp => break,
|
||||
InputAction::JumpingToPage(page) => {
|
||||
tui_tx.send(RenderNotif::JumpToPage(page))?;
|
||||
to_converter.send(ConverterMsg::GoToPage(page))?;
|
||||
},
|
||||
InputAction::Search(term) => tui_tx.send(RenderNotif::Search(term))?,
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -225,14 +207,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 +241,29 @@ 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<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
|
||||
}
|
||||
|
||||
+16
-12
@@ -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
|
||||
|
||||
+113
-79
@@ -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,35 +368,77 @@ 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 =>
|
||||
{
|
||||
term.push(c);
|
||||
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| {
|
||||
*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) => {
|
||||
// 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);
|
||||
return Some(InputAction::Redraw);
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
'/' => {
|
||||
self.set_bottom_msg(Some(BottomMessage::Input(InputCommand::Search(
|
||||
String::new()
|
||||
))));
|
||||
Some(InputAction::Redraw)
|
||||
}
|
||||
'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)
|
||||
});
|
||||
|
||||
jump_to_page(&mut self.page, &mut self.last_render.rect, next_page)
|
||||
}
|
||||
'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))
|
||||
});
|
||||
|
||||
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);
|
||||
@@ -376,44 +446,6 @@ impl Tui {
|
||||
}
|
||||
_ => 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()
|
||||
))));
|
||||
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)
|
||||
});
|
||||
|
||||
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))
|
||||
});
|
||||
|
||||
jump_to_page(&mut self.page, &mut self.last_render.rect, prev_page)
|
||||
}
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user