Compare commits

...

20 Commits

Author SHA1 Message Date
itsjunetime b531d352fd Build *with* boost? 2024-11-20 09:25:48 -07:00
itsjunetime 4f04c39120 Maybe more apt installs will get CI to work 2024-11-20 09:17:59 -07:00
itsjunetime b1a77d6178 Go back down to 23_7 poppler? 2024-11-20 09:09:35 -07:00
itsjunetime cd0356a25b Build with poppler 23.10 instead of .12 to maybe prevent segfault in CI 2024-11-20 09:05:17 -07:00
itsjunetime 5465724062 Install libunwind-dev to get perftools installing 2024-11-19 21:43:19 -07:00
itsjunetime 7d9c398742 Install perftools to get criterion compiling 2024-11-19 21:41:44 -07:00
itsjunetime 73cd8ff480 - Update deps
- Explicitly run benches in CI, specifically only adobe_example pdf to make it quicker
- Render bottom message from Cow to avoid extra allocations
- Fix issue with hitting esc after jumping around pdf
2024-11-19 21:32:03 -07:00
June 25d98c3776 Merge pull request #31 from Kreijstal/main
some CI checks
2024-11-19 21:23:39 -07:00
June 03c2f381d9 Add format check and elevate clippy warnings to errors 2024-11-19 21:23:21 -07:00
June eb5ee99eec Add build step back to ensure linking works 2024-11-19 21:22:57 -07:00
Kreijstal 34b42cb1b2 Update .github/workflows/rust.yml
change build for clippy

Co-authored-by: June <61218022+itsjunetime@users.noreply.github.com>
2024-11-17 09:12:08 +01:00
Kreijstal e3ccb26d66 Update .github/workflows/rust.yml
Removing verbose it's okay, I'm not sure if cargo test tests linking the same binary as cargo build. So I'm not sure if it should be tested, furthermore maybe it makes sense to test in release mode rather than debug mode?

Co-authored-by: June <61218022+itsjunetime@users.noreply.github.com>
2024-11-17 07:46:37 +01:00
itsjunetime 1aa26b8e8c Manually enable opinionated set of clippy lints 2024-11-16 22:03:31 -07:00
Kreijstal 1402db3eba only libpoppler 23 is supported which doesn't come with ubuntu 2024-11-16 12:12:04 +01:00
Kreijstal 2d43c1e513 Create rust.yml
Adding sccache for speedup
2024-11-16 11:27:55 +01: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
12 changed files with 1012 additions and 263 deletions
+56
View File
@@ -0,0 +1,56 @@
name: Rust
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Setup sccache
if: github.event_name != 'release' && github.event_name != 'workflow_dispatch'
uses: mozilla-actions/sccache-action@v0.0.6
- name: Configure sccache
if: github.event_name != 'release' && github.event_name != 'workflow_dispatch'
run: |
echo "SCCACHE_GHA_ENABLED=true" >> $GITHUB_ENV
echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV
- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y cmake libjpeg-dev libfontconfig1-dev libopenjp2-7-dev libopenjpip7 libopenjp2-7 libglib2.0-dev libnss3-dev libunwind-dev libgoogle-perftools-dev libboost-dev
- name: Build newer poppler
run: |
wget https://poppler.freedesktop.org/poppler-23.10.0.tar.xz
tar xf poppler-23.10.0.tar.xz
cd poppler-23.10.0
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER_LAUNCHER=sccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=sccache \
-DENABLE_UNSTABLE_API_ABI_HEADERS=ON \
-DENABLE_GPGME=OFF \
-DENABLE_QT5=OFF \
-DENABLE_QT6=OFF \
-DENABLE_SPLASH=OFF \
-DENABLE_LIBCURL=OFF
make -j$(nproc)
sudo make install
sudo ldconfig
- uses: actions/checkout@v4
- name: Clippy
run: cargo clippy -- -D warnings
- name: Check fmt
run: cargo fmt -- --check
- name: Run tests
run: cargo test --benches -- adobe_example
- name: Build
run: cargo build
+13
View File
@@ -0,0 +1,13 @@
# 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
- Fixed a bug where jumping to a page out of range could result in weird `esc` key behavior
- Added CI
# v0.1.0
Initial tag :)
Generated
+617 -91
View File
File diff suppressed because it is too large Load Diff
+103 -4
View File
@@ -1,8 +1,15 @@
[package] [package]
name = "tdf" name = "tdf"
version = "0.1.0" version = "0.1.0"
authors = ["June Welker <junewelker@gmail.com>"]
edition = "2021" 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" license = "MPL-2.0"
keywords = ["pdf", "tui", "cli", "terminal"]
categories = ["command-line-utilities", "text-processing", "visualization"]
default-run = "tdf" default-run = "tdf"
[[bin]] [[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"] } 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 # 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 = { 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 # 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 = { git = "https://github.com/itsjunetime/ratatui-image.git", branch = "vb64_on_personal", default-features = false }
# ratatui-image = { path = "./ratatui-image", features = ["rustix", "vb64"], default-features = false } # ratatui-image = { path = "./ratatui-image", features = ["vb64"], default-features = false }
crossterm = { version = "0.28.1", features = ["event-stream"] } crossterm = { version = "0.28.1", features = ["event-stream"] }
image = { version = "0.25.1", features = ["png", "rayon"], default-features = false } image = { version = "0.25.1", features = ["png", "rayon"], default-features = false }
notify = { version = "7.0.0", features = ["crossbeam-channel"] } 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" glib = "0.20.0"
itertools = "*" itertools = "*"
flume = { version = "0.11.0", default-features = false, features = ["async"] } flume = { version = "0.11.0", default-features = false, features = ["async"] }
xflags = "0.4.0-pre.2"
# for tracing with tokio-console # for tracing with tokio-console
console-subscriber = { version = "0.4.0", optional = true } console-subscriber = { version = "0.4.0", optional = true }
@@ -38,7 +46,8 @@ inherits = "release"
lto = "fat" lto = "fat"
[features] [features]
default = [] default = ["nightly"]
nightly = ["ratatui-image/vb64"]
tracing = ["tokio/tracing", "dep:console-subscriber"] tracing = ["tokio/tracing", "dep:console-subscriber"]
[dev-dependencies] [dev-dependencies]
@@ -52,3 +61,93 @@ harness = false
[[bin]] [[bin]]
name = "for_profiling" name = "for_profiling"
path = "./benches/for_profiling.rs" 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 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 { let main_area = Rect {
x: 0, x: 0,
@@ -98,8 +98,8 @@ pub fn start_converting_loop(
let (to_converter_tx, from_main_rx) = unbounded(); let (to_converter_tx, from_main_rx) = unbounded();
let (to_main_tx, from_converter_rx) = unbounded(); let (to_main_tx, from_converter_rx) = unbounded();
let mut picker = Picker::new(FONT_SIZE); let mut picker = Picker::from_fontsize(FONT_SIZE);
picker.protocol_type = ProtocolType::Kitty; picker.set_protocol_type(ProtocolType::Kitty);
tokio::spawn(run_conversion_loop( tokio::spawn(run_conversion_loop(
to_main_tx, to_main_tx,
@@ -136,7 +136,7 @@ pub async fn render_doc(path: impl AsRef<Path>) {
to_render_tx to_render_tx
} = start_all_rendering(path); } = 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! { tokio::select! {
Some(renderer_msg) = from_render_rx.next() => { Some(renderer_msg) = from_render_rx.next() => {
handle_renderer_msg(renderer_msg, &mut pages, &mut to_converter_tx); 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}; use crate::renderer::{fill_default, PageInfo, RenderError};
pub struct ConvertedPage { pub struct ConvertedPage {
pub page: Box<dyn Protocol>, pub page: Protocol,
pub num: usize, pub num: usize,
pub num_results: usize pub num_results: usize
} }
-2
View File
@@ -1,5 +1,3 @@
#![feature(if_let_guard)]
pub mod converter; pub mod converter;
pub mod renderer; pub mod renderer;
pub mod skip; pub mod skip;
+62 -59
View File
@@ -1,9 +1,7 @@
#![feature(if_let_guard)]
use std::{ use std::{
io::{stdout, Read, Write}, io::{stdout, Read, Write},
path::PathBuf, num::NonZeroUsize,
str::FromStr path::PathBuf
}; };
use converter::{run_conversion_loop, ConvertedPage, ConverterMsg}; use converter::{run_conversion_loop, ConvertedPage, ConverterMsg};
@@ -44,42 +42,33 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
console_subscriber::init(); console_subscriber::init();
let file = std::env::args() let flags = xflags::parse_or_exit! {
.nth(1) /// Display the pdf with the pages starting at the right hand size and moving left and
.ok_or("Program requires a file to process")?; /// adjust input keys to match
let path = PathBuf::from_str(&file)?.canonicalize()?; 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 path = flags.file.canonicalize()?;
let tui_tx = watch_tx.clone();
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 (render_tx, tui_rx) = flume::unbounded();
let watch_to_tui_tx = render_tx.clone(); 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 let mut watcher =
// will be calling it from a thread not owned by the tokio runtime (since it's created by notify::recommended_watcher(on_notify_ev(watch_to_tui_tx, watch_to_render_tx))?;
// 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)),
}
})?;
// 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
// to recurse into // to recurse into
watcher.watch(&path, RecursiveMode::NonRecursive)?; 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 file_path = format!("file://{}", path.clone().into_os_string().to_string_lossy());
let mut window_size = window_size()?; 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) // read in the returned size until we hit a 't' (which indicates to us it's done)
let input_vec = std::io::stdin() let input_vec = std::io::stdin()
.bytes() .bytes()
.flat_map(|b| b.ok()) .filter_map(Result::ok)
.take_while(|b| *b != b't') .take_while(|b| *b != b't')
.collect::<Vec<_>>(); .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, // 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 // it messes up something with user input. Input never makes it to the crossterm thing
let mut picker = Picker::new(( let picker = Picker::from_query_stdio()?;
window_size.width / window_size.columns,
window_size.height / window_size.rows
));
picker.guess_protocol();
// 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 || { 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 = 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)); tokio::spawn(run_conversion_loop(to_main, from_main, picker, 20));
let file_name = path let file_name = path.file_name().map_or_else(
.file_name() || "Unknown file".into(),
.map(|n| n.to_string_lossy()) |n| n.to_string_lossy().to_string()
.unwrap_or_else(|| "Unknown file".into()) );
.to_string(); let mut tui = tui::Tui::new(file_name, flags.max_wide, flags.r_to_l.unwrap_or_default());
let mut tui = tui::Tui::new(file_name);
let backend = CrosstermBackend::new(std::io::stdout()); let backend = CrosstermBackend::new(std::io::stdout());
let mut term = Terminal::new(backend)?; 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(); let mut from_converter = from_converter.into_stream();
loop { loop {
let mut needs_redraw = tokio::select! { let mut needs_redraw = true;
tokio::select! {
// First we check if we have any keystrokes // First we check if we have any keystrokes
Some(ev) = ev_stream.next().fuse() => { Some(ev) = ev_stream.next().fuse() => {
// If we can't get user input, just crash. // If we can't get user input, just crash.
let ev = ev.expect("Couldn't get any user input"); let ev = ev.expect("Couldn't get any user input");
match tui.handle_event(ev) { match tui.handle_event(&ev) {
None => false, None => needs_redraw = false,
Some(action) => { Some(action) => match action {
match action { InputAction::Redraw => (),
InputAction::Redraw => (), InputAction::QuitApp => break,
InputAction::QuitApp => break, InputAction::JumpingToPage(page) => {
InputAction::JumpingToPage(page) => { tui_tx.send(RenderNotif::JumpToPage(page))?;
tui_tx.send(RenderNotif::JumpToPage(page))?; to_converter.send(ConverterMsg::GoToPage(page))?;
to_converter.send(ConverterMsg::GoToPage(page))?; },
}, InputAction::Search(term) => tui_tx.send(RenderNotif::Search(term))?,
InputAction::Search(term) => tui_tx.send(RenderNotif::Search(term))?,
};
true
} }
} }
}, },
@@ -225,14 +207,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}, },
Err(e) => tui.show_error(e), Err(e) => tui.show_error(e),
} }
true
} }
Some(img_res) = from_converter.next() => { Some(img_res) = from_converter.next() => {
match img_res { match img_res {
Ok(ConvertedPage { page, num, num_results }) => tui.page_ready(page, num, num_results), Ok(ConvertedPage { page, num, num_results }) => tui.page_ready(page, num, num_results),
Err(e) => tui.show_error(e), Err(e) => tui.show_error(e),
} }
true
}, },
}; };
@@ -261,6 +241,29 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(()) 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 { fn noop(_: LogLevel, _: &[LogField<'_>]) -> LogWriterOutput {
LogWriterOutput::Handled LogWriterOutput::Handled
} }
+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 // 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 // means the other side's disconnected, which means that the main thread has panicked, which means
// we're done. // 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( pub fn start_rendering(
path: String, path: &str,
mut sender: Sender<Result<RenderInfo, RenderError>>, mut sender: Sender<Result<RenderInfo, RenderError>>,
receiver: Receiver<RenderNotif>, receiver: Receiver<RenderNotif>,
size: WindowSize size: WindowSize
@@ -90,7 +94,7 @@ pub fn start_rendering(
let col_h = size.height / size.rows; let col_h = size.height / size.rows;
'reload: loop { 'reload: loop {
let doc = match Document::from_file(&path, None) { let doc = match Document::from_file(path, None) {
Err(e) => { Err(e) => {
// if there's an error, tell the main loop // if there's an error, tell the main loop
sender.send(Err(RenderError::Doc(e)))?; sender.send(Err(RenderError::Doc(e)))?;
@@ -189,8 +193,8 @@ pub fn start_rendering(
.map(|(idx, p)| (start_point - (idx + 1), p)) .map(|(idx, p)| (start_point - (idx + 1), p))
); );
let area_w = area.width as f64 * col_w as f64; let area_w = f64::from(area.width) * f64::from(col_w);
let area_h = area.height as f64 * col_h as f64; let area_h = f64::from(area.height) * f64::from(col_h);
// we go through each page // we go through each page
for (num, rendered) in page_iter { for (num, rendered) in page_iter {
@@ -227,8 +231,8 @@ pub fn start_rendering(
// render the page // render the page
match render_single_page_to_ctx( match render_single_page_to_ctx(
page, &page,
&search_term, search_term.as_deref(),
rendered_with_no_results, rendered_with_no_results,
(area_w, area_h) (area_w, area_h)
) { ) {
@@ -251,11 +255,11 @@ pub fn start_rendering(
// since the effects of parallelizing that will be noticeable if the user // since the effects of parallelizing that will be noticeable if the user
// tries to move through pages more quickly // tries to move through pages more quickly
if num == start_point { 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 { } else {
let mut sender = sender.clone(); let mut sender = sender.clone();
thread::spawn(move || { 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 {} unsafe impl Send for RenderedContext {}
fn render_single_page_to_ctx( fn render_single_page_to_ctx(
page: Page, page: &Page,
search_term: &Option<String>, search_term: Option<&str>,
already_rendered_no_results: bool, already_rendered_no_results: bool,
(area_w, area_h): (f64, f64) (area_w, area_h): (f64, f64)
) -> Result<Option<RenderedContext>, String> { ) -> Result<Option<RenderedContext>, String> {
@@ -375,7 +379,7 @@ fn render_single_page_to_ctx(
highlight_color.set_green((u16::MAX / 5) * 4); highlight_color.set_green((u16::MAX / 5) * 4);
let mut old_rect = Rectangle::new(); 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 // 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 // need to be corrected since they use different references as the y-coordinate base
rect.set_y1(p_height - rect.y1()); rect.set_y1(p_height - rect.y1());
@@ -401,7 +405,7 @@ fn render_single_page_to_ctx(
} }
fn render_ctx_to_png( fn render_ctx_to_png(
ctx: RenderedContext, ctx: &RenderedContext,
sender: &mut Sender<Result<RenderInfo, RenderError>>, sender: &mut Sender<Result<RenderInfo, RenderError>>,
(col_w, col_h): (u16, u16), (col_w, col_h): (u16, u16),
page: usize page: usize
+138 -88
View File
@@ -1,4 +1,4 @@
use std::{io::stdout, rc::Rc}; use std::{borrow::Cow, io::stdout, num::NonZeroUsize, rc::Rc};
use crossterm::{ use crossterm::{
event::{Event, KeyCode, MouseEventKind}, 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 // 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>,
page_constraints: PageConstraints
} }
#[derive(Default, Debug)] #[derive(Default, Debug)]
@@ -50,12 +51,17 @@ pub enum InputCommand {
Search(String) 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 // 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 // within it is valid; I think it's the best way to represent it
#[derive(Default)] #[derive(Default)]
struct RenderedInfo { struct RenderedInfo {
// The image, if it has been rendered by `Converter` to that struct // 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 // 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 // we haven't checked this page yet
// Also this isn't the most efficient representation of this value, but it's accurate, so like // 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 { impl Tui {
pub fn new(name: String) -> Tui { pub fn new(name: String, max_wide: Option<NonZeroUsize>, r_to_l: bool) -> Tui {
Self { Self {
name, name,
page: 0, page: 0,
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![],
page_constraints: PageConstraints { max_wide, r_to_l }
} }
} }
@@ -145,18 +152,18 @@ impl Tui {
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): (Cow<'_, str>, _) = match self.bottom_msg {
BottomMessage::Help => ( BottomMessage::Help => (
"/: Search, g: Go To Page, n: Next Search Result, N: Previous Search Result" "/: Search, g: Go To Page, n: Next Search Result, N: Previous Search Result".into(),
.to_string(),
Color::Blue Color::Blue
), ),
BottomMessage::Error(ref e) => (format!("Couldn't render a page: {e}"), Color::Red), BottomMessage::Error(ref e) => (e.as_str().into(), 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}")
}, }
.into(),
Color::Blue Color::Blue
), ),
BottomMessage::SearchResults(ref term) => { BottomMessage::SearchResults(ref term) => {
@@ -174,7 +181,8 @@ impl Tui {
format!( format!(
"Results for '{term}': {num_found} (searched: {}%)", "Results for '{term}': {num_found} (searched: {}%)",
num_searched / self.rendered.len() num_searched / self.rendered.len()
), )
.into(),
Color::Blue Color::Blue
) )
} }
@@ -196,15 +204,21 @@ 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..] let mut page_widths = self.rendered[self.page..]
.iter() .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(|(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) // 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. // and then take them as long as they won't overflow the available area.
.take_while(|(_, width)| match test_area_w.checked_sub(*width) { .take_while(|(_, width)| match test_area_w.checked_sub(*width) {
Some(new_val) => { Some(new_val) => {
@@ -215,6 +229,10 @@ impl Tui {
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if self.page_constraints.r_to_l {
page_widths.reverse();
}
if page_widths.is_empty() { if page_widths.is_empty() {
// If none are ready to render, just show the loading thing // If none are ready to render, just show the loading thing
Self::render_loading_in(frame, img_area); Self::render_loading_in(frame, img_area);
@@ -252,7 +270,7 @@ impl Tui {
fn render_single_page(&mut self, frame: &mut Frame<'_>, page_idx: usize, img_area: Rect) { fn render_single_page(&mut self, frame: &mut Frame<'_>, page_idx: usize, img_area: Rect) {
match self.rendered[page_idx].img { 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) None => Self::render_loading_in(frame, img_area)
}; };
} }
@@ -268,12 +286,23 @@ impl Tui {
frame.render_widget(loading_span, inner_space[0]); 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 { let diff = match amt {
ChangeAmount::Single => 1, ChangeAmount::Single => 1,
ChangeAmount::WholeScreen => self.last_render.pages_shown 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; 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)),
@@ -293,7 +322,7 @@ impl Tui {
self.page = self.page.min(n_pages - 1); 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 // 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 // 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 // the last rect marker so that all images are forced to redraw on next render and this one
@@ -323,7 +352,7 @@ impl Tui {
self.rendered[page_num].num_results = Some(num_results); 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( fn jump_to_page(
page: &mut usize, page: &mut usize,
rect: &mut Rect, rect: &mut Rect,
@@ -340,80 +369,92 @@ impl Tui {
match ev { match ev {
Event::Key(key) => { Event::Key(key) => {
match key.code { match key.code {
KeyCode::Char(c) KeyCode::Char(c) => {
// TODO: refactor back to `if let` arm guards when those are stabilized
if let BottomMessage::Input(InputCommand::Search(ref mut term)) = if let BottomMessage::Input(InputCommand::Search(ref mut term)) =
self.bottom_msg => self.bottom_msg
{ {
term.push(c); 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)) = if let BottomMessage::Input(InputCommand::GoToPage(ref mut page)) =
self.bottom_msg => self.bottom_msg
c.to_digit(10).map(|input_num| { {
*page = (*page * 10) + input_num as usize; return c.to_digit(10).map(|input_num| {
InputAction::Redraw *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), match c {
KeyCode::Left | KeyCode::Char('h') => 'l' => self.change_page(PageChange::Next, ChangeAmount::Single),
self.change_page(PageChange::Prev, ChangeAmount::Single), 'j' => self.change_page(PageChange::Next, ChangeAmount::WholeScreen),
KeyCode::Up | KeyCode::Char('k') => 'h' => self.change_page(PageChange::Prev, ChangeAmount::Single),
self.change_page(PageChange::Prev, ChangeAmount::WholeScreen), '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 { KeyCode::Esc => match self.bottom_msg {
BottomMessage::Input(_) => { BottomMessage::Help => Some(InputAction::QuitApp),
_ => {
self.set_bottom_msg(None); self.set_bottom_msg(None);
Some(InputAction::Redraw) 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()
))));
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 => { KeyCode::Enter => {
let BottomMessage::Input(_) = self.bottom_msg else { let BottomMessage::Input(_) = self.bottom_msg else {
return None; return None;
@@ -431,10 +472,17 @@ impl Tui {
// Only forward the command if it's within range // Only forward the command if it's within range
InputCommand::GoToPage(page) => { InputCommand::GoToPage(page) => {
let page = *page; let page = *page;
(page < self.rendered.len()).then(|| { let rendered_len = self.rendered.len();
if page < rendered_len {
self.set_page(page); self.set_page(page);
InputAction::JumpingToPage(page) Some(InputAction::JumpingToPage(page))
}) } else {
self.set_bottom_msg(Some(BottomMessage::Error(
format!("Cannot jump to page {page}; there are only {rendered_len} pages in the document")
)));
Some(InputAction::Redraw)
}
} }
InputCommand::Search(term) => { InputCommand::Search(term) => {
let term = term.clone(); let term = term.clone();
@@ -522,11 +570,13 @@ pub enum InputAction {
QuitApp QuitApp
} }
#[derive(Copy, Clone)]
enum PageChange { enum PageChange {
Prev, Prev,
Next Next
} }
#[derive(Copy, Clone)]
enum ChangeAmount { enum ChangeAmount {
WholeScreen, WholeScreen,
Single Single