mirror of
https://github.com/itsjunetime/tdf.git
synced 2026-06-02 08:01:47 -04:00
Compare commits
17 Commits
v0.1.0
...
fix_ci_again
| Author | SHA1 | Date | |
|---|---|---|---|
| 01b6c2fa55 | |||
| c9ef119393 | |||
| cc46791627 | |||
| 9cf4a8e0d8 | |||
| 25d98c3776 | |||
| 03c2f381d9 | |||
| eb5ee99eec | |||
| 34b42cb1b2 | |||
| e3ccb26d66 | |||
| 1aa26b8e8c | |||
| 1402db3eba | |||
| 2d43c1e513 | |||
| 40d46f1e2d | |||
| 927a9cb587 | |||
| e51e9d3464 | |||
| 7c13054383 | |||
| b1e26bc96b |
@@ -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 http://archive.ubuntu.com/ubuntu/pool/main/p/poppler/poppler_23.08.0.orig.tar.xz
|
||||
tar xf poppler_23.08.0.orig.tar.xz
|
||||
cd poppler-23.08.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
|
||||
@@ -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 ([#31](https://github.com/itsjunetime/tdf/pull/31), thank you [@Kriejstal](https://github.com/Kreijstal))
|
||||
|
||||
# v0.1.0
|
||||
|
||||
Initial tag :)
|
||||
Generated
+617
-91
File diff suppressed because it is too large
Load Diff
+103
-4
@@ -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
@@ -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;
|
||||
|
||||
+55
-52
@@ -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,16 +168,16 @@ 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 {
|
||||
match tui.handle_event(&ev) {
|
||||
None => needs_redraw = false,
|
||||
Some(action) => match action {
|
||||
InputAction::Redraw => (),
|
||||
InputAction::QuitApp => break,
|
||||
InputAction::JumpingToPage(page) => {
|
||||
@@ -201,8 +185,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 +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
|
||||
|
||||
+109
-59
@@ -1,4 +1,4 @@
|
||||
use std::{io::stdout, rc::Rc};
|
||||
use std::{borrow::Cow, 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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,18 +152,18 @@ impl Tui {
|
||||
let rendered_span = Span::styled(&rendered_str, Style::new().fg(Color::Cyan));
|
||||
frame.render_widget(rendered_span, bottom_layout[1]);
|
||||
|
||||
let (msg_str, color) = match self.bottom_msg {
|
||||
let (msg_str, color): (Cow<'_, str>, _) = match self.bottom_msg {
|
||||
BottomMessage::Help => (
|
||||
"/: Search, g: Go To Page, n: Next Search Result, N: Previous Search Result"
|
||||
.to_string(),
|
||||
"/: Search, g: Go To Page, n: Next Search Result, N: Previous Search Result".into(),
|
||||
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) => (
|
||||
match input_state {
|
||||
InputCommand::GoToPage(page) => format!("Go to: {page}"),
|
||||
InputCommand::Search(s) => format!("Search: {s}")
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
Color::Blue
|
||||
),
|
||||
BottomMessage::SearchResults(ref term) => {
|
||||
@@ -174,7 +181,8 @@ impl Tui {
|
||||
format!(
|
||||
"Results for '{term}': {num_found} (searched: {}%)",
|
||||
num_searched / self.rendered.len()
|
||||
),
|
||||
)
|
||||
.into(),
|
||||
Color::Blue
|
||||
)
|
||||
}
|
||||
@@ -196,15 +204,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 +229,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 +270,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 +286,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 +322,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 +352,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 +369,43 @@ impl Tui {
|
||||
match ev {
|
||||
Event::Key(key) => {
|
||||
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)) =
|
||||
self.bottom_msg =>
|
||||
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| {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
_ => Some(InputAction::QuitApp)
|
||||
},
|
||||
KeyCode::Char('q') => Some(InputAction::QuitApp),
|
||||
KeyCode::Char('g') => {
|
||||
self.set_bottom_msg(Some(BottomMessage::Input(InputCommand::GoToPage(0))));
|
||||
'/' => {
|
||||
self.set_bottom_msg(Some(BottomMessage::Input(
|
||||
InputCommand::Search(String::new())
|
||||
)));
|
||||
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 +419,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()
|
||||
@@ -414,6 +432,29 @@ 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::Help => Some(InputAction::QuitApp),
|
||||
_ => {
|
||||
self.set_bottom_msg(None);
|
||||
Some(InputAction::Redraw)
|
||||
}
|
||||
},
|
||||
KeyCode::Enter => {
|
||||
let BottomMessage::Input(_) = self.bottom_msg else {
|
||||
return None;
|
||||
@@ -431,10 +472,17 @@ impl Tui {
|
||||
// Only forward the command if it's within range
|
||||
InputCommand::GoToPage(page) => {
|
||||
let page = *page;
|
||||
(page < self.rendered.len()).then(|| {
|
||||
let rendered_len = self.rendered.len();
|
||||
|
||||
if page < rendered_len {
|
||||
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) => {
|
||||
let term = term.clone();
|
||||
@@ -522,11 +570,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