mirror of
https://github.com/itsjunetime/tdf.git
synced 2026-06-01 23:51:46 -04:00
Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e2a9c90b34 | |||
| 16c0aed9a3 | |||
| 19030f7fd4 | |||
| d5d62c81a3 | |||
| 7b9e1462da | |||
| a4905b2ae5 | |||
| 9b9796e718 | |||
| e5cf92221f | |||
| 3fcf3be65f | |||
| 3dc5135a8b | |||
| 065797ccf6 | |||
| 55e0c2b33f | |||
| 0a0112a7e1 | |||
| 8f57cd02c3 | |||
| 74def1c0a8 | |||
| 670251fdff | |||
| b7d1b78e98 | |||
| fe8287bf7a | |||
| 38b307d628 | |||
| 09a332f07e | |||
| cde86b4f2c | |||
| 5a492599da | |||
| 7551c4dba3 | |||
| e61eb9b846 | |||
| 6b37976357 | |||
| 3628d21c74 | |||
| f4f3b4f539 | |||
| a79c534e97 | |||
| 971393892a | |||
| 440515a3db | |||
| 690489016c | |||
| bd5554db27 | |||
| 45409bacd0 | |||
| 0481c14c4d | |||
| a78ea5a08c | |||
| f7eabc9af2 | |||
| 918c192047 | |||
| 8b03329bba | |||
| 7064be32f2 | |||
| 2a03294557 | |||
| 7c2c6484a6 | |||
| e65472e571 | |||
| 4fd2237b69 | |||
| 69fd8ec7e8 | |||
| ebd902e864 | |||
| b6bc76edbb | |||
| 777705b902 | |||
| 035185a40f | |||
| 5542daffb6 | |||
| f0a6e23f8a | |||
| f0afd22ff5 | |||
| 8d65f0e3f5 | |||
| e16163efb8 | |||
| 2f4e2a54bc | |||
| 0bfacd8757 | |||
| 5c7073b31e | |||
| 061863d34c | |||
| f044a7fa4d | |||
| 475d45a6f6 | |||
| 69d5f96375 | |||
| 595f23de6f | |||
| d8ee0744b8 | |||
| 0c81e3cc3a | |||
| fc10dc8ffe | |||
| ef8ace4f35 | |||
| 6462c09030 | |||
| 54cc2125af | |||
| d00ae5c981 | |||
| 36279f5258 |
@@ -0,0 +1,2 @@
|
|||||||
|
[*.rs]
|
||||||
|
indent_style = tab
|
||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Setup sccache
|
- name: Setup sccache
|
||||||
if: github.event_name != 'release' && github.event_name != 'workflow_dispatch'
|
if: github.event_name != 'release' && github.event_name != 'workflow_dispatch'
|
||||||
uses: mozilla-actions/sccache-action@v0.0.6
|
uses: mozilla-actions/sccache-action@v0.0.8
|
||||||
- name: Configure sccache
|
- name: Configure sccache
|
||||||
if: github.event_name != 'release' && github.event_name != 'workflow_dispatch'
|
if: github.event_name != 'release' && github.event_name != 'workflow_dispatch'
|
||||||
run: |
|
run: |
|
||||||
@@ -28,11 +28,15 @@ jobs:
|
|||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libfontconfig1-dev libgoogle-perftools-dev google-perftools
|
sudo apt-get install -y libfontconfig1-dev libgoogle-perftools-dev google-perftools
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install clippy and fmt
|
||||||
|
run: rustup component add clippy rustfmt
|
||||||
- name: Clippy
|
- name: Clippy
|
||||||
run: cargo clippy -- -D warnings
|
run: cargo clippy --locked -- -D warnings
|
||||||
|
- name: Tests
|
||||||
|
run: cargo test --locked
|
||||||
- name: Check fmt
|
- name: Check fmt
|
||||||
run: cargo fmt -- --check
|
run: cargo fmt -- --check
|
||||||
- name: Run tests
|
- name: Run benchmarks as tests
|
||||||
run: cargo test --benches -- adobe_example
|
run: cargo test --locked --benches -- adobe_example
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build
|
run: cargo build --locked
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
/target
|
/target
|
||||||
|
debug.log
|
||||||
|
|||||||
@@ -1,3 +1,44 @@
|
|||||||
|
# Unreleased
|
||||||
|
|
||||||
|
# v0.5.0
|
||||||
|
|
||||||
|
- Switched simd base64 crate for one that works on stable (from `vb64` to `base64_simd`)
|
||||||
|
- Allow boolean arguments to function as flags, without a `true` or `false` argument following the flag itself ([#109](https://github.com/itsjunetime/tdf/pull/109), thanks [@tatounee](https://github.com/tatounee)!)
|
||||||
|
- Fix cropping issues when zooming out too much while using kitty protocol
|
||||||
|
- Added `gg` and `G` keybindings for scrolling to the top and bottom of a page, respectively, when filling the width of the screen with kitty
|
||||||
|
- Updated help page to only show kitty keybindings when you're actually using kitty
|
||||||
|
- Map page-up and page-down keybindings to do the same thing as up-key and down-key ([#115](https://github.com/itsjunetime/tdf/pull/115), thanks [@maxdexh](https://github.com/maxdexh)!)
|
||||||
|
- Vertically center pages within the available space if they are not constrained by the height ([#116](https://github.com/itsjunetime/tdf/pull/116), thanks [@maxdexh](https://github.com/maxdexh)!)
|
||||||
|
- Fixed issue with cooked mode not being restored upon panic/error ([#118](https://github.com/itsjunetime/tdf/pull/118), thanks [@maxdexh](https://github.com/maxdexh)!)
|
||||||
|
- Implemented a debounce for file reload updates to prevent some editors from paralyzing the app due to a flurry of reloads ([#117](https://github.com/itsjunetime/tdf/pull/117), thanks [@maxdexh](https://github.com/maxdexh))
|
||||||
|
- Fixed an overflow when zooming out of horizontal pdfs ([#119](https://github.com/itsjunetime/tdf/pull/119), thanks [@maxdexh](https://github.com/maxdexh)!)
|
||||||
|
- Reworked zooming to allow for full zooming in and out and panning in both directions ([#121](https://github.com/itsjunetime/tdf/pull/121), thanks [@maxdexh](https://github.com/maxdexh)!)
|
||||||
|
|
||||||
|
# v0.4.3
|
||||||
|
|
||||||
|
- Fix issue with some terminals hanging on startup
|
||||||
|
- Fix issues with some iterm2-backend terminals not displaying anything
|
||||||
|
- Allow using ctrl+scroll to zoom in/out while zoomed using kitty backend
|
||||||
|
- (Internal) run CI with `--locked` flag to ensure lockfile is always in-sync
|
||||||
|
|
||||||
|
# v0.4.2
|
||||||
|
|
||||||
|
- Add `--version` flag
|
||||||
|
- Fix shms not working on macos ([#93](https://github.com/itsjunetime/tdf/pull/93))
|
||||||
|
|
||||||
|
# v0.4.1
|
||||||
|
|
||||||
|
- Add instructions for using new zoom/pan features to help page
|
||||||
|
|
||||||
|
# v0.4.0
|
||||||
|
|
||||||
|
- Update to new `kittage` backend for kitty-protocol-supporting terminals (fixes many issues and improves performance significantly, see [the PR](https://github.com/itsjunetime/tdf/pull/74))
|
||||||
|
- Use new mupdf search API for slightly better performance
|
||||||
|
- Update ratatui(-image) dependencies
|
||||||
|
- Allow specification of default white and black colors for rendered pdfs
|
||||||
|
- Pause rendering every once in a while while there's a search term to enable searching across the entire document more quickly
|
||||||
|
- Fix an issue with missing search highlights
|
||||||
|
|
||||||
# v0.3.0
|
# v0.3.0
|
||||||
|
|
||||||
- Update ratatui(-image) dependencies
|
- Update ratatui(-image) dependencies
|
||||||
|
|||||||
Generated
+1129
-1044
File diff suppressed because it is too large
Load Diff
+152
-33
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "tdf-viewer"
|
name = "tdf-viewer"
|
||||||
version = "0.2.0"
|
version = "0.5.0"
|
||||||
authors = ["June Welker <junewelker@gmail.com>"]
|
authors = ["June Welker <junewelker@gmail.com>"]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "A terminal viewer for PDFs"
|
description = "A terminal viewer for PDFs"
|
||||||
@@ -11,7 +11,7 @@ license = "AGPL-3.0-only"
|
|||||||
keywords = ["pdf", "tui", "cli", "terminal"]
|
keywords = ["pdf", "tui", "cli", "terminal"]
|
||||||
categories = ["command-line-utilities", "text-processing", "visualization"]
|
categories = ["command-line-utilities", "text-processing", "visualization"]
|
||||||
default-run = "tdf"
|
default-run = "tdf"
|
||||||
rust-version = "1.85"
|
rust-version = "1.86"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "tdf"
|
name = "tdf"
|
||||||
@@ -24,39 +24,47 @@ name = "tdf"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
# 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" }
|
# 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 `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 `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", 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 }
|
# ratatui-image = { path = "./ratatui-image", default-features = false }
|
||||||
crossterm = { version = "0.28.1", features = ["event-stream"] }
|
crossterm = { version = "0.29.0", features = ["event-stream"] }
|
||||||
|
# crossterm = { path = "../crossterm", features = ["event-stream"] }
|
||||||
image = { version = "0.25.1", features = ["pnm", "rayon", "png"], default-features = false }
|
image = { version = "0.25.1", features = ["pnm", "rayon", "png"], default-features = false }
|
||||||
notify = { version = "8.0.0", features = ["crossbeam-channel"] }
|
notify = { version = "8.0.0", features = ["crossbeam-channel"] }
|
||||||
tokio = { version = "1.37.0", features = ["rt-multi-thread", "macros"] }
|
tokio = { version = "1.37.0", features = ["rt-multi-thread", "macros"] }
|
||||||
futures-util = { version = "0.3.30", default-features = false }
|
futures-util = { version = "0.3.30", default-features = false }
|
||||||
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"
|
xflags = "0.4.0-pre.2"
|
||||||
mimalloc = "0.1.43"
|
mimalloc = "0.1.43"
|
||||||
nix = { version = "0.29.0", features = ["signal"] }
|
nix = { version = "0.30.0", features = ["signal"] }
|
||||||
mupdf = { git = "https://github.com/itsjunetime/mupdf-rs", branch = "june/mupdf_1_25", default-features = false, features = ["svg", "system-fonts", "img"] }
|
mupdf = { git = "https://github.com/messense/mupdf-rs.git", rev = "2e0fae910fac8048c7008211fc4d3b9f5d227a07", default-features = false, features = ["svg", "system-fonts", "img"] }
|
||||||
rayon = { version = "*", default-features = false }
|
rayon = { version = "1", default-features = false }
|
||||||
|
# kittage = { path = "../kittage/", features = ["crossterm-tokio", "image-crate", "log"] }
|
||||||
|
kittage = { version = "0.1.1", features = ["crossterm-tokio", "image-crate", "log"] }
|
||||||
|
memmap2 = "0"
|
||||||
|
csscolorparser = { version = "0.8.0", default-features = false }
|
||||||
|
|
||||||
|
# logging
|
||||||
|
log = "0.4.27"
|
||||||
|
flexi_logger = "0.31"
|
||||||
|
|
||||||
# for tracing with tokio-console
|
# for tracing with tokio-console
|
||||||
console-subscriber = { version = "0.4.0", optional = true }
|
console-subscriber = { version = "0.5.0", optional = true }
|
||||||
|
debounce = "0.2.2"
|
||||||
|
|
||||||
[profile.production]
|
[profile.production]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
lto = "fat"
|
lto = "fat"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["nightly"]
|
default = []
|
||||||
nightly = ["ratatui-image/vb64"]
|
|
||||||
tracing = ["tokio/tracing", "dep:console-subscriber"]
|
tracing = ["tokio/tracing", "dep:console-subscriber"]
|
||||||
epub = ["mupdf/epub"]
|
epub = ["mupdf/epub"]
|
||||||
cbz = ["mupdf/cbz"]
|
cbz = ["mupdf/cbz"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = { version = "0.5.1", features = ["async_tokio"] }
|
criterion = { version = "0.7.0", features = ["async_tokio"] }
|
||||||
cpuprofiler = "0.0.4"
|
cpuprofiler = "0.0.4"
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
@@ -68,94 +76,205 @@ name = "for_profiling"
|
|||||||
path = "./benches/for_profiling.rs"
|
path = "./benches/for_profiling.rs"
|
||||||
|
|
||||||
[lints.clippy]
|
[lints.clippy]
|
||||||
uninlined_format_args = "warn"
|
alloc_instead_of_core = "warn"
|
||||||
redundant_closure_for_method_calls = "warn"
|
allow_attributes = "warn"
|
||||||
cast_lossless = "warn"
|
as_pointer_underscore = "warn"
|
||||||
single_char_pattern = "warn"
|
as_ptr_cast_mut = "warn"
|
||||||
manual_let_else = "warn"
|
as_underscore = "warn"
|
||||||
ignored_unit_patterns = "warn"
|
|
||||||
range_plus_one = "warn"
|
|
||||||
unreadable_literal = "warn"
|
|
||||||
redundant_else = "warn"
|
|
||||||
assigning_clones = "warn"
|
assigning_clones = "warn"
|
||||||
|
assertions_on_result_states = "warn"
|
||||||
bool_to_int_with_if = "warn"
|
bool_to_int_with_if = "warn"
|
||||||
borrow_as_ptr = "warn"
|
borrow_as_ptr = "warn"
|
||||||
|
branches_sharing_code = "warn"
|
||||||
|
cargo_common_metadata = "warn"
|
||||||
|
case_sensitive_file_extension_comparisons = "warn"
|
||||||
|
cast_lossless = "warn"
|
||||||
cast_ptr_alignment = "warn"
|
cast_ptr_alignment = "warn"
|
||||||
|
cfg_not_test = "warn"
|
||||||
checked_conversions = "warn"
|
checked_conversions = "warn"
|
||||||
|
clear_with_drain = "warn"
|
||||||
|
cloned_instead_of_copied = "warn"
|
||||||
|
coerce_container_to_any = "warn"
|
||||||
|
comparison_chain = "warn"
|
||||||
copy_iterator = "warn"
|
copy_iterator = "warn"
|
||||||
|
create_dir = "warn"
|
||||||
|
debug_assert_with_mut_call = "warn"
|
||||||
|
decimal_literal_representation = "warn"
|
||||||
default_trait_access = "warn"
|
default_trait_access = "warn"
|
||||||
|
deref_by_slicing = "warn"
|
||||||
|
doc_broken_link = "warn"
|
||||||
|
doc_link_code = "warn"
|
||||||
doc_link_with_quotes = "warn"
|
doc_link_with_quotes = "warn"
|
||||||
empty_enum = "warn"
|
elidable_lifetime_names = "warn"
|
||||||
|
empty_drop = "warn"
|
||||||
|
empty_enums = "warn"
|
||||||
|
empty_enum_variants_with_brackets = "warn"
|
||||||
|
empty_structs_with_brackets = "warn"
|
||||||
|
equatable_if_let = "warn"
|
||||||
|
error_impl_error = "warn"
|
||||||
|
expl_impl_clone_on_copy = "warn"
|
||||||
|
explicit_deref_methods = "warn"
|
||||||
explicit_into_iter_loop = "warn"
|
explicit_into_iter_loop = "warn"
|
||||||
explicit_iter_loop = "warn"
|
explicit_iter_loop = "warn"
|
||||||
|
fallible_impl_from = "warn"
|
||||||
|
filetype_is_file = "warn"
|
||||||
filter_map_next = "warn"
|
filter_map_next = "warn"
|
||||||
flat_map_option = "warn"
|
flat_map_option = "warn"
|
||||||
|
fn_to_numeric_cast_any = "warn"
|
||||||
fn_params_excessive_bools = "warn"
|
fn_params_excessive_bools = "warn"
|
||||||
from_iter_instead_of_collect = "warn"
|
format_collect = "warn"
|
||||||
|
format_push_string = "warn"
|
||||||
|
get_unwrap = "warn"
|
||||||
|
if_then_some_else_none = "warn"
|
||||||
|
ignore_without_reason = "warn"
|
||||||
|
ignored_unit_patterns = "warn"
|
||||||
implicit_clone = "warn"
|
implicit_clone = "warn"
|
||||||
|
imprecise_flops = "warn"
|
||||||
index_refutable_slice = "warn"
|
index_refutable_slice = "warn"
|
||||||
inefficient_to_string = "warn"
|
indexing_slicing = "allow" # can't warn on this cause we basically have to do indexing for some ratatui apis
|
||||||
|
infinite_loop = "warn"
|
||||||
invalid_upcast_comparisons = "warn"
|
invalid_upcast_comparisons = "warn"
|
||||||
|
ip_constant = "warn"
|
||||||
iter_filter_is_ok = "warn"
|
iter_filter_is_ok = "warn"
|
||||||
iter_filter_is_some = "warn"
|
iter_filter_is_some = "warn"
|
||||||
iter_not_returning_iterator = "warn"
|
iter_not_returning_iterator = "warn"
|
||||||
|
iter_on_empty_collections = "warn"
|
||||||
|
iter_on_single_items = "warn"
|
||||||
|
large_digit_groups = "warn"
|
||||||
large_futures = "warn"
|
large_futures = "warn"
|
||||||
large_stack_arrays = "warn"
|
large_include_file = "warn"
|
||||||
|
large_stack_frames = "warn"
|
||||||
large_types_passed_by_value = "warn"
|
large_types_passed_by_value = "warn"
|
||||||
linkedlist = "warn"
|
linkedlist = "warn"
|
||||||
|
literal_string_with_formatting_args = "warn"
|
||||||
|
lossy_float_literal = "warn"
|
||||||
macro_use_imports = "warn"
|
macro_use_imports = "warn"
|
||||||
manual_assert = "warn"
|
manual_assert = "warn"
|
||||||
manual_instant_elapsed = "warn"
|
manual_instant_elapsed = "warn"
|
||||||
manual_is_power_of_two = "warn"
|
manual_is_power_of_two = "warn"
|
||||||
manual_is_variant_and = "warn"
|
manual_is_variant_and = "warn"
|
||||||
|
manual_let_else = "warn"
|
||||||
|
manual_midpoint = "warn"
|
||||||
manual_ok_or = "warn"
|
manual_ok_or = "warn"
|
||||||
manual_string_new = "warn"
|
|
||||||
many_single_char_names = "warn"
|
many_single_char_names = "warn"
|
||||||
manual_unwrap_or = "warn"
|
map_err_ignore = "warn"
|
||||||
match_on_vec_items = "warn"
|
map_unwrap_or = "warn"
|
||||||
|
map_with_unused_argument_over_ranges = "warn"
|
||||||
match_same_arms = "warn"
|
match_same_arms = "warn"
|
||||||
|
match_wild_err_arm = "warn"
|
||||||
match_wildcard_for_single_variants = "warn"
|
match_wildcard_for_single_variants = "warn"
|
||||||
maybe_infinite_iter = "warn"
|
maybe_infinite_iter = "warn"
|
||||||
|
mem_forget = "warn"
|
||||||
mismatching_type_param_order = "warn"
|
mismatching_type_param_order = "warn"
|
||||||
|
missing_assert_message = "warn"
|
||||||
missing_fields_in_debug = "warn"
|
missing_fields_in_debug = "warn"
|
||||||
|
mixed_read_write_in_expression = "warn"
|
||||||
|
multiple_unsafe_ops_per_block = "warn"
|
||||||
|
must_use_candidate = "warn"
|
||||||
mut_mut = "warn"
|
mut_mut = "warn"
|
||||||
|
mutex_atomic = "warn"
|
||||||
|
mutex_integer = "warn"
|
||||||
|
naive_bytecount = "warn"
|
||||||
needless_bitwise_bool = "warn"
|
needless_bitwise_bool = "warn"
|
||||||
|
needless_collect = "warn"
|
||||||
needless_continue = "warn"
|
needless_continue = "warn"
|
||||||
needless_for_each = "warn"
|
needless_for_each = "warn"
|
||||||
|
needless_pass_by_ref_mut = "warn"
|
||||||
needless_pass_by_value = "warn"
|
needless_pass_by_value = "warn"
|
||||||
needless_raw_string_hashes = "warn"
|
needless_raw_string_hashes = "warn"
|
||||||
|
needless_raw_strings = "warn"
|
||||||
|
negative_feature_names = "warn"
|
||||||
no_effect_underscore_binding = "warn"
|
no_effect_underscore_binding = "warn"
|
||||||
no_mangle_with_rust_abi = "warn"
|
no_mangle_with_rust_abi = "warn"
|
||||||
|
non_send_fields_in_send_ty = "warn"
|
||||||
|
non_std_lazy_statics = "warn"
|
||||||
|
non_zero_suggestions = "warn"
|
||||||
|
nonstandard_macro_braces = "warn"
|
||||||
option_as_ref_cloned = "warn"
|
option_as_ref_cloned = "warn"
|
||||||
option_option = "warn"
|
option_option = "warn"
|
||||||
|
or_fun_call = "warn"
|
||||||
|
path_buf_push_overwrite = "warn"
|
||||||
|
pathbuf_init_then_push = "warn"
|
||||||
|
precedence_bits = "warn"
|
||||||
ptr_as_ptr = "warn"
|
ptr_as_ptr = "warn"
|
||||||
ptr_cast_constness = "warn"
|
ptr_cast_constness = "warn"
|
||||||
|
pub_underscore_fields = "warn"
|
||||||
|
pub_without_shorthand = "warn"
|
||||||
range_minus_one = "warn"
|
range_minus_one = "warn"
|
||||||
|
range_plus_one = "warn"
|
||||||
|
rc_buffer = "warn"
|
||||||
|
rc_mutex = "warn"
|
||||||
|
read_zero_byte_vec = "warn"
|
||||||
|
redundant_clone = "warn"
|
||||||
|
redundant_closure_for_method_calls = "warn"
|
||||||
|
redundant_else = "warn"
|
||||||
|
redundant_pub_crate = "warn"
|
||||||
|
redundant_test_prefix = "warn"
|
||||||
ref_as_ptr = "warn"
|
ref_as_ptr = "warn"
|
||||||
ref_binding_to_reference = "warn"
|
ref_binding_to_reference = "warn"
|
||||||
ref_option = "warn"
|
ref_option = "warn"
|
||||||
ref_option_ref = "warn"
|
ref_option_ref = "warn"
|
||||||
|
rest_pat_in_fully_bound_structs = "warn"
|
||||||
return_self_not_must_use = "warn"
|
return_self_not_must_use = "warn"
|
||||||
same_functions_in_if_condition = "warn"
|
same_functions_in_if_condition = "warn"
|
||||||
|
self_named_module_files = "warn"
|
||||||
|
semicolon_if_nothing_returned = "warn"
|
||||||
|
semicolon_inside_block = "warn"
|
||||||
should_panic_without_expect = "warn"
|
should_panic_without_expect = "warn"
|
||||||
similar_names = "warn"
|
significant_drop_in_scrutinee = "warn" # I thought this was fixed in the 2024 edition. watever
|
||||||
|
significant_drop_tightening = "warn"
|
||||||
|
single_char_pattern = "warn"
|
||||||
|
single_option_map = "warn"
|
||||||
stable_sort_primitive = "warn"
|
stable_sort_primitive = "warn"
|
||||||
str_split_at_newline = "warn"
|
str_split_at_newline = "warn"
|
||||||
|
string_lit_as_bytes = "warn"
|
||||||
|
string_lit_chars_any = "warn"
|
||||||
|
string_slice = "warn"
|
||||||
struct_excessive_bools = "warn"
|
struct_excessive_bools = "warn"
|
||||||
struct_field_names = "warn"
|
struct_field_names = "warn"
|
||||||
|
suboptimal_flops = "warn"
|
||||||
|
suspicious_operation_groupings = "warn"
|
||||||
|
suspicious_xor_used_as_pow = "warn"
|
||||||
|
tests_outside_test_module = "warn"
|
||||||
|
trait_duplication_in_bounds = "warn"
|
||||||
transmute_ptr_to_ptr = "warn"
|
transmute_ptr_to_ptr = "warn"
|
||||||
|
trivial_regex = "warn"
|
||||||
trivially_copy_pass_by_ref = "warn"
|
trivially_copy_pass_by_ref = "warn"
|
||||||
|
try_err = "warn"
|
||||||
|
tuple_array_conversions = "warn"
|
||||||
|
type_repetition_in_bounds = "warn"
|
||||||
|
unchecked_time_subtraction = "warn"
|
||||||
|
undocumented_unsafe_blocks = "warn"
|
||||||
unicode_not_nfc = "warn"
|
unicode_not_nfc = "warn"
|
||||||
|
uninhabited_references = "warn"
|
||||||
|
uninlined_format_args = "warn"
|
||||||
unnecessary_box_returns = "warn"
|
unnecessary_box_returns = "warn"
|
||||||
|
unnecessary_debug_formatting = "warn"
|
||||||
unnecessary_join = "warn"
|
unnecessary_join = "warn"
|
||||||
unnecessary_literal_bound = "warn"
|
unnecessary_literal_bound = "warn"
|
||||||
|
unnecessary_safety_comment = "warn"
|
||||||
|
unnecessary_safety_doc = "warn"
|
||||||
|
unnecessary_self_imports = "warn"
|
||||||
|
unnecessary_semicolon = "warn"
|
||||||
|
unnecessary_struct_initialization = "warn"
|
||||||
unnecessary_wraps = "warn"
|
unnecessary_wraps = "warn"
|
||||||
unnested_or_patterns = "warn"
|
unnested_or_patterns = "warn"
|
||||||
|
unreadable_literal = "warn"
|
||||||
|
unsafe_derive_deserialize = "warn"
|
||||||
unused_async = "warn"
|
unused_async = "warn"
|
||||||
|
unused_peekable = "warn"
|
||||||
|
unused_result_ok = "warn"
|
||||||
|
unused_rounding = "warn"
|
||||||
unused_self = "warn"
|
unused_self = "warn"
|
||||||
|
unused_trait_names = "warn"
|
||||||
|
use_self = "warn"
|
||||||
used_underscore_binding = "warn"
|
used_underscore_binding = "warn"
|
||||||
used_underscore_items = "warn"
|
used_underscore_items = "warn"
|
||||||
|
useless_let_if_seq = "warn"
|
||||||
|
verbose_bit_mask = "warn"
|
||||||
|
verbose_file_reads = "warn"
|
||||||
|
volatile_composites = "warn"
|
||||||
|
while_float = "warn"
|
||||||
|
wildcard_dependencies = "warn"
|
||||||
|
wildcard_imports = "warn"
|
||||||
zero_sized_map_values = "warn"
|
zero_sized_map_values = "warn"
|
||||||
|
|
||||||
[patch.crates-io]
|
|
||||||
pathfinder_simd = { git = "https://github.com/itsjunetime/pathfinder.git" }
|
|
||||||
|
|||||||
@@ -13,8 +13,15 @@ Designed to be performant, very responsive, and work well with even very large P
|
|||||||
- Responsive details about rendering/search progress
|
- Responsive details about rendering/search progress
|
||||||
- Reactive layout
|
- Reactive layout
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Get the rust toolchain from [rustup.rs](https://rustup.rs)
|
||||||
|
2. Run `cargo install --git https://github.com/itsjunetime/tdf.git`
|
||||||
|
|
||||||
|
If you want to use this with `epub`s or `cbz`s, add `--features epub` or `--features cbz` to the command line (or `--features cbz,epub` for both)
|
||||||
|
|
||||||
## To Build
|
## To Build
|
||||||
First, you need to install the system dependencies. This will generally only include `libfontconfig`. If you're on linux, these will probably show up in your package manager as something like `libfontconfig1-devel` or `libfontconfig-dev`.
|
First, you need to install the system dependencies. This will generally only include `libfontconfig` and `clang`. If you're on linux, these will probably show up in your package manager as something like `libfontconfig1-devel` or `libfontconfig-dev` and just `clang`.
|
||||||
|
|
||||||
If it turns out that you're missing one of these, it will fail to compile and tell you what library you're missing. Find the development package for that library in your package manager, install it, and try to build again. Now, the important steps:
|
If it turns out that you're missing one of these, it will fail to compile and tell you what library you're missing. Find the development package for that library in your package manager, install it, and try to build again. Now, the important steps:
|
||||||
|
|
||||||
@@ -22,6 +29,10 @@ If it turns out that you're missing one of these, it will fail to compile and te
|
|||||||
2. Clone the repo and `cd` into it
|
2. Clone the repo and `cd` into it
|
||||||
3. Run `cargo build --release`
|
3. Run `cargo build --release`
|
||||||
|
|
||||||
|
The binary should then be found at `./target/release/tdf`.
|
||||||
|
|
||||||
|
You can also pull this in via [radicle](https://radicle.xyz) with `rad clone rad:zb11K1XGfQooopqEfwtCMyvbcyK1`
|
||||||
|
|
||||||
## Why in the world would you use this?
|
## Why in the world would you use this?
|
||||||
|
|
||||||
I dunno. Just for fun, mostly.
|
I dunno. Just for fun, mostly.
|
||||||
@@ -30,4 +41,6 @@ I dunno. Just for fun, mostly.
|
|||||||
|
|
||||||
Yeah, sure. Please do.
|
Yeah, sure. Please do.
|
||||||
|
|
||||||
Please note, though, that all contributions will be treated as licensed under MPL-2.0.
|
Please note, though, that:
|
||||||
|
1. No AI-generated or AI-assisted or AI-viewed or AI-anythinged code will be accepted. "AI" is a plague upon this earth and I won't be caught dead pretending it's normal.
|
||||||
|
2. All contributions will be treated as licensed under MPL-2.0 :)
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
|
use ratatui_image::picker::ProtocolType;
|
||||||
|
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
|
const BLACK: i32 = 0;
|
||||||
|
const WHITE: i32 = i32::from_be_bytes([0, 0xff, 0xff, 0xff]);
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
@@ -9,5 +14,5 @@ async fn main() {
|
|||||||
.nth(1)
|
.nth(1)
|
||||||
.expect("Please enter a file to profile");
|
.expect("Please enter a file to profile");
|
||||||
|
|
||||||
utils::render_doc(file).await;
|
utils::render_doc(file, None, BLACK, WHITE, ProtocolType::Kitty).await;
|
||||||
}
|
}
|
||||||
|
|||||||
+92
-47
@@ -1,17 +1,15 @@
|
|||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use std::{
|
use std::{hint::black_box, path::Path};
|
||||||
hint::black_box,
|
|
||||||
path::Path,
|
|
||||||
time::{SystemTime, UNIX_EPOCH}
|
|
||||||
};
|
|
||||||
|
|
||||||
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main, profiler::Profiler};
|
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt as _;
|
||||||
|
use ratatui_image::picker::ProtocolType;
|
||||||
use tdf::{
|
use tdf::{
|
||||||
converter::{ConvertedPage, ConverterMsg},
|
converter::{ConvertedPage, ConverterMsg},
|
||||||
renderer::{PageInfo, RenderInfo, fill_default}
|
renderer::{PageInfo, RenderInfo, fill_default}
|
||||||
};
|
};
|
||||||
|
use tokio::runtime::Runtime;
|
||||||
use utils::{
|
use utils::{
|
||||||
RenderState, handle_converter_msg, handle_renderer_msg, render_doc, start_all_rendering,
|
RenderState, handle_converter_msg, handle_renderer_msg, render_doc, start_all_rendering,
|
||||||
start_converting_loop, start_rendering_loop
|
start_converting_loop, start_rendering_loop
|
||||||
@@ -23,61 +21,103 @@ const FILES: [&str; 3] = [
|
|||||||
"benches/geotopo.pdf"
|
"benches/geotopo.pdf"
|
||||||
];
|
];
|
||||||
|
|
||||||
fn render_full(c: &mut Criterion) {
|
const PROTOS: [ProtocolType; 3] = [
|
||||||
|
ProtocolType::Kitty,
|
||||||
|
ProtocolType::Sixel,
|
||||||
|
ProtocolType::Iterm2
|
||||||
|
];
|
||||||
|
|
||||||
|
const BLACK: i32 = 0;
|
||||||
|
const WHITE: i32 = i32::from_be_bytes([0, 0xff, 0xff, 0xff]);
|
||||||
|
|
||||||
|
fn for_all_combos(
|
||||||
|
name: &'static str,
|
||||||
|
mut f: impl FnMut(&Runtime, BenchmarkId, &'static str, ProtocolType)
|
||||||
|
) {
|
||||||
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
for proto in PROTOS {
|
||||||
for file in FILES {
|
for file in FILES {
|
||||||
c.bench_with_input(BenchmarkId::new("render_full", file), &file, |b, &file| {
|
f(
|
||||||
b.to_async(tokio::runtime::Runtime::new().unwrap())
|
&rt,
|
||||||
.iter(|| render_doc(file))
|
BenchmarkId::new(name, format!("{file},{proto:?}")),
|
||||||
});
|
file,
|
||||||
|
proto
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_full(c: &mut Criterion) {
|
||||||
|
for_all_combos("render_full", |rt, id, file, proto| {
|
||||||
|
_ = c.bench_with_input(id, &file, |b, &file| {
|
||||||
|
b.to_async(rt)
|
||||||
|
.iter(|| render_doc(file, None, BLACK, WHITE, proto));
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_to_first_page(c: &mut Criterion) {
|
fn render_to_first_page(c: &mut Criterion) {
|
||||||
for file in FILES {
|
for_all_combos("render_first_page", |rt, id, file, proto| {
|
||||||
c.bench_with_input(
|
c.bench_with_input(id, &file, |b, &file| {
|
||||||
BenchmarkId::new("render_first_page", file),
|
b.to_async(rt)
|
||||||
&file,
|
.iter(|| render_first_page(file, BLACK, WHITE, proto));
|
||||||
|b, &file| {
|
});
|
||||||
b.to_async(tokio::runtime::Runtime::new().unwrap())
|
});
|
||||||
.iter(|| render_first_page(file))
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn only_converting(c: &mut Criterion) {
|
fn only_converting(c: &mut Criterion) {
|
||||||
for file in FILES {
|
for_all_combos("only_converting", |rt, id, file, proto| {
|
||||||
let runtime = tokio::runtime::Runtime::new().unwrap();
|
let all_rendered = rt.block_on(render_all_files(file, BLACK, WHITE));
|
||||||
let all_rendered = runtime.block_on(render_all_files(file));
|
|
||||||
|
|
||||||
c.bench_with_input(
|
c.bench_with_input(id, &all_rendered, |b, rendered| {
|
||||||
BenchmarkId::new("only_converting", file),
|
b.to_async(rt)
|
||||||
&(all_rendered, file),
|
.iter_with_setup(|| rendered.clone(), |f| convert_all_files(f, proto));
|
||||||
|b, (rendered, _)| {
|
});
|
||||||
b.to_async(tokio::runtime::Runtime::new().unwrap())
|
});
|
||||||
.iter_with_setup(|| rendered.clone(), convert_all_files)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn render_first_page(path: impl AsRef<Path>) {
|
/*
|
||||||
|
fn search_short_common(c: &mut Criterion) {
|
||||||
|
for_all_combos("search_short_common", |rt, id, file, proto| {
|
||||||
|
c.bench_with_input(id, &file, |b, &file| {
|
||||||
|
b.to_async(rt)
|
||||||
|
.iter(|| render_doc(file, Some("an"), BLACK, WHITE, proto))
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_long_rare(c: &mut Criterion) {
|
||||||
|
for_all_combos("search_long_rare", |rt, id, file, proto| {
|
||||||
|
c.bench_with_input(id, &file, |b, &file| {
|
||||||
|
b.to_async(rt)
|
||||||
|
.iter(|| render_doc(file, Some("this is long and rare"), BLACK, WHITE, proto))
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
pub async fn render_first_page(
|
||||||
|
path: impl AsRef<Path>,
|
||||||
|
black: i32,
|
||||||
|
white: i32,
|
||||||
|
proto: ProtocolType
|
||||||
|
) {
|
||||||
let RenderState {
|
let RenderState {
|
||||||
mut from_render_rx,
|
mut from_render_rx,
|
||||||
mut from_converter_rx,
|
mut from_converter_rx,
|
||||||
mut pages,
|
mut pages,
|
||||||
mut to_converter_tx,
|
to_converter_tx,
|
||||||
to_render_tx
|
to_render_tx
|
||||||
} = start_all_rendering(path);
|
} = start_all_rendering(path, black, white, proto);
|
||||||
|
|
||||||
// we only want to render until the first page is ready to be printed
|
// we only want to render until the first page is ready to be printed
|
||||||
while pages.iter().all(Option::is_none) {
|
while pages.iter().all(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, &to_converter_tx);
|
||||||
},
|
},
|
||||||
Some(converter_msg) = from_converter_rx.next() => {
|
Some(converter_msg) = from_converter_rx.next() => {
|
||||||
handle_converter_msg(converter_msg, &mut pages, &mut to_converter_tx);
|
handle_converter_msg(converter_msg, &mut pages, &to_converter_tx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,19 +128,19 @@ pub async fn render_first_page(path: impl AsRef<Path>) {
|
|||||||
drop(to_render_tx);
|
drop(to_render_tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn render_all_files(path: &'static str) -> Vec<PageInfo> {
|
async fn render_all_files(path: &'static str, black: i32, white: i32) -> Vec<PageInfo> {
|
||||||
let (mut from_render_rx, to_render_tx) = start_rendering_loop(path);
|
let (mut from_render_rx, to_render_tx) = start_rendering_loop(path, black, white);
|
||||||
let mut pages = Vec::<Option<PageInfo>>::new();
|
let mut pages = Vec::<Option<PageInfo>>::new();
|
||||||
|
|
||||||
while let Some(info) = from_render_rx.next().await {
|
while let Some(info) = from_render_rx.next().await {
|
||||||
match info.expect("Renderer ran into an error while rendering") {
|
match info.expect("Renderer ran into an error while rendering") {
|
||||||
RenderInfo::Reloaded => (),
|
RenderInfo::Reloaded | RenderInfo::SearchResults { .. } => (),
|
||||||
RenderInfo::NumPages(num) => fill_default(&mut pages, num),
|
RenderInfo::NumPages(num) => fill_default(&mut pages, num),
|
||||||
RenderInfo::Page(page) => {
|
RenderInfo::Page(page) => {
|
||||||
let num = page.page_num;
|
let num = page.page_num;
|
||||||
pages[num] = Some(page);
|
pages[num] = Some(page);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
if pages.iter().all(Option::is_some) {
|
if pages.iter().all(Option::is_some) {
|
||||||
break;
|
break;
|
||||||
@@ -111,9 +151,9 @@ async fn render_all_files(path: &'static str) -> Vec<PageInfo> {
|
|||||||
pages.into_iter().flatten().collect()
|
pages.into_iter().flatten().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn convert_all_files(files: Vec<PageInfo>) {
|
async fn convert_all_files(files: Vec<PageInfo>, proto: ProtocolType) {
|
||||||
let num_files = files.len();
|
let num_files = files.len();
|
||||||
let (mut from_converter_rx, to_converter_tx) = start_converting_loop(num_files);
|
let (mut from_converter_rx, to_converter_tx) = start_converting_loop(proto, num_files);
|
||||||
|
|
||||||
to_converter_tx
|
to_converter_tx
|
||||||
.send(ConverterMsg::NumPages(num_files))
|
.send(ConverterMsg::NumPages(num_files))
|
||||||
@@ -152,10 +192,12 @@ async fn convert_all_files(files: Vec<PageInfo>) {
|
|||||||
black_box(converted);
|
black_box(converted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
struct CpuProfiler;
|
struct CpuProfiler;
|
||||||
|
|
||||||
impl Profiler for CpuProfiler {
|
impl criterion::profiler::Profiler for CpuProfiler {
|
||||||
fn start_profiling(&mut self, benchmark_id: &str, _: &std::path::Path) {
|
fn start_profiling(&mut self, benchmark_id: &str, _: &std::path::Path) {
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH}
|
||||||
let file = format!(
|
let file = format!(
|
||||||
"./{}-{}.profile",
|
"./{}-{}.profile",
|
||||||
benchmark_id.replace('/', "-"),
|
benchmark_id.replace('/', "-"),
|
||||||
@@ -171,10 +213,13 @@ impl Profiler for CpuProfiler {
|
|||||||
cpuprofiler::PROFILER.lock().unwrap().stop().unwrap();
|
cpuprofiler::PROFILER.lock().unwrap().stop().unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
criterion_group!(
|
criterion_group!(
|
||||||
name = benches;
|
name = benches;
|
||||||
config = Criterion::default().sample_size(40).with_profiler(CpuProfiler);
|
// config = Criterion::default().sample_size(40).with_profiler(CpuProfiler);
|
||||||
|
config = Criterion::default().sample_size(40);
|
||||||
|
// targets = render_full, render_to_first_page, only_converting, search_short_common, search_long_rare
|
||||||
targets = render_full, render_to_first_page, only_converting
|
targets = render_full, render_to_first_page, only_converting
|
||||||
);
|
);
|
||||||
criterion_main!(benches);
|
criterion_main!(benches);
|
||||||
|
|||||||
+53
-17
@@ -13,7 +13,7 @@ use tdf::{
|
|||||||
pub fn handle_renderer_msg(
|
pub fn handle_renderer_msg(
|
||||||
msg: Result<RenderInfo, RenderError>,
|
msg: Result<RenderInfo, RenderError>,
|
||||||
pages: &mut Vec<Option<ConvertedPage>>,
|
pages: &mut Vec<Option<ConvertedPage>>,
|
||||||
to_converter_tx: &mut Sender<tdf::converter::ConverterMsg>
|
to_converter_tx: &Sender<tdf::converter::ConverterMsg>
|
||||||
) {
|
) {
|
||||||
match msg {
|
match msg {
|
||||||
Ok(RenderInfo::NumPages(num)) => {
|
Ok(RenderInfo::NumPages(num)) => {
|
||||||
@@ -21,8 +21,8 @@ pub fn handle_renderer_msg(
|
|||||||
to_converter_tx.send(ConverterMsg::NumPages(num)).unwrap();
|
to_converter_tx.send(ConverterMsg::NumPages(num)).unwrap();
|
||||||
}
|
}
|
||||||
Ok(RenderInfo::Page(info)) => to_converter_tx.send(ConverterMsg::AddImg(info)).unwrap(),
|
Ok(RenderInfo::Page(info)) => to_converter_tx.send(ConverterMsg::AddImg(info)).unwrap(),
|
||||||
// We can ignore the `Reloaded` variant 'cause that's only used to send info to the TUI
|
// We can ignore the these variants 'cause they're only used to send info to the TUI
|
||||||
Ok(RenderInfo::Reloaded) => (),
|
Ok(RenderInfo::Reloaded | RenderInfo::SearchResults { .. }) => (),
|
||||||
Err(e) => panic!("Got error from renderer: {e:?}")
|
Err(e) => panic!("Got error from renderer: {e:?}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@ pub fn handle_renderer_msg(
|
|||||||
pub fn handle_converter_msg(
|
pub fn handle_converter_msg(
|
||||||
msg: Result<ConvertedPage, RenderError>,
|
msg: Result<ConvertedPage, RenderError>,
|
||||||
pages: &mut [Option<ConvertedPage>],
|
pages: &mut [Option<ConvertedPage>],
|
||||||
to_converter_tx: &mut Sender<ConverterMsg>
|
to_converter_tx: &Sender<ConverterMsg>
|
||||||
) {
|
) {
|
||||||
let page = msg.expect("Got error from converter");
|
let page = msg.expect("Got error from converter");
|
||||||
let num = page.num;
|
let num = page.num;
|
||||||
@@ -57,7 +57,9 @@ pub struct RenderState {
|
|||||||
const FONT_SIZE: (u16, u16) = (8, 14);
|
const FONT_SIZE: (u16, u16) = (8, 14);
|
||||||
|
|
||||||
pub fn start_rendering_loop(
|
pub fn start_rendering_loop(
|
||||||
path: impl AsRef<Path>
|
path: impl AsRef<Path>,
|
||||||
|
black: i32,
|
||||||
|
white: i32
|
||||||
) -> (
|
) -> (
|
||||||
RecvStream<'static, Result<RenderInfo, RenderError>>,
|
RecvStream<'static, Result<RenderInfo, RenderError>>,
|
||||||
Sender<RenderNotif>
|
Sender<RenderNotif>
|
||||||
@@ -77,8 +79,6 @@ 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));
|
|
||||||
|
|
||||||
let main_area = Rect {
|
let main_area = Rect {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
@@ -87,11 +87,28 @@ pub fn start_rendering_loop(
|
|||||||
};
|
};
|
||||||
to_render_tx.send(RenderNotif::Area(main_area)).unwrap();
|
to_render_tx.send(RenderNotif::Area(main_area)).unwrap();
|
||||||
|
|
||||||
|
let cell_height_px = size.height / size.rows;
|
||||||
|
let cell_width_px = size.width / size.columns;
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
start_rendering(
|
||||||
|
&str_path,
|
||||||
|
to_main_tx,
|
||||||
|
from_main_rx,
|
||||||
|
cell_height_px,
|
||||||
|
cell_width_px,
|
||||||
|
tdf::PrerenderLimit::All,
|
||||||
|
black,
|
||||||
|
white
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
let from_render_rx = from_render_rx.into_stream();
|
let from_render_rx = from_render_rx.into_stream();
|
||||||
(from_render_rx, to_render_tx)
|
(from_render_rx, to_render_tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn start_converting_loop(
|
pub fn start_converting_loop(
|
||||||
|
proto: ProtocolType,
|
||||||
prerender: usize
|
prerender: usize
|
||||||
) -> (
|
) -> (
|
||||||
RecvStream<'static, Result<ConvertedPage, RenderError>>,
|
RecvStream<'static, Result<ConvertedPage, RenderError>>,
|
||||||
@@ -101,22 +118,29 @@ pub fn start_converting_loop(
|
|||||||
let (to_main_tx, from_converter_rx) = unbounded();
|
let (to_main_tx, from_converter_rx) = unbounded();
|
||||||
|
|
||||||
let mut picker = Picker::from_fontsize(FONT_SIZE);
|
let mut picker = Picker::from_fontsize(FONT_SIZE);
|
||||||
picker.set_protocol_type(ProtocolType::Kitty);
|
picker.set_protocol_type(proto);
|
||||||
|
|
||||||
tokio::spawn(run_conversion_loop(
|
tokio::spawn(run_conversion_loop(
|
||||||
to_main_tx,
|
to_main_tx,
|
||||||
from_main_rx,
|
from_main_rx,
|
||||||
picker,
|
picker,
|
||||||
prerender
|
prerender,
|
||||||
|
// just assume shms work for now, who cares
|
||||||
|
true
|
||||||
));
|
));
|
||||||
|
|
||||||
let from_converter_rx = from_converter_rx.into_stream();
|
let from_converter_rx = from_converter_rx.into_stream();
|
||||||
(from_converter_rx, to_converter_tx)
|
(from_converter_rx, to_converter_tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start_all_rendering(path: impl AsRef<Path>) -> RenderState {
|
pub fn start_all_rendering(
|
||||||
let (from_render_rx, to_render_tx) = start_rendering_loop(path);
|
path: impl AsRef<Path>,
|
||||||
let (from_converter_rx, to_converter_tx) = start_converting_loop(20);
|
black: i32,
|
||||||
|
white: i32,
|
||||||
|
proto: ProtocolType
|
||||||
|
) -> RenderState {
|
||||||
|
let (from_render_rx, to_render_tx) = start_rendering_loop(path, black, white);
|
||||||
|
let (from_converter_rx, to_converter_tx) = start_converting_loop(proto, 20);
|
||||||
|
|
||||||
let pages: Vec<Option<ConvertedPage>> = Vec::new();
|
let pages: Vec<Option<ConvertedPage>> = Vec::new();
|
||||||
|
|
||||||
@@ -129,22 +153,34 @@ pub fn start_all_rendering(path: impl AsRef<Path>) -> RenderState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn render_doc(path: impl AsRef<Path>) {
|
pub async fn render_doc(
|
||||||
|
path: impl AsRef<Path>,
|
||||||
|
search_term: Option<&str>,
|
||||||
|
black: i32,
|
||||||
|
white: i32,
|
||||||
|
proto: ProtocolType
|
||||||
|
) {
|
||||||
let RenderState {
|
let RenderState {
|
||||||
mut from_render_rx,
|
mut from_render_rx,
|
||||||
mut from_converter_rx,
|
mut from_converter_rx,
|
||||||
mut pages,
|
mut pages,
|
||||||
mut to_converter_tx,
|
to_converter_tx,
|
||||||
to_render_tx
|
to_render_tx
|
||||||
} = start_all_rendering(path);
|
} = start_all_rendering(path, black, white, proto);
|
||||||
|
|
||||||
|
if let Some(term) = search_term {
|
||||||
|
to_render_tx
|
||||||
|
.send(RenderNotif::Search(term.to_owned()))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
while pages.is_empty() || pages.iter().any(Option::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, &to_converter_tx);
|
||||||
},
|
},
|
||||||
Some(converter_msg) = from_converter_rx.next() => {
|
Some(converter_msg) = from_converter_rx.next() => {
|
||||||
handle_converter_msg(converter_msg, &mut pages, &mut to_converter_tx);
|
handle_converter_msg(converter_msg, &mut pages, &to_converter_tx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
+1
-1
Submodule ratatui updated: 1166bebf44...6a0b8ddf76
+1
-1
Submodule ratatui-image updated: 53a788e0cb...9564ae6466
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
pkgs ? import <nixpkgs> { },
|
||||||
|
}:
|
||||||
|
pkgs.mkShell {
|
||||||
|
nativeBuildInputs = [ pkgs.pkg-config ];
|
||||||
|
|
||||||
|
buildInputs = [
|
||||||
|
pkgs.rustPlatform.bindgenHook
|
||||||
|
pkgs.cairo
|
||||||
|
];
|
||||||
|
}
|
||||||
+122
-23
@@ -1,14 +1,60 @@
|
|||||||
use flume::{Receiver, SendError, Sender, TryRecvError};
|
use std::{
|
||||||
use futures_util::stream::StreamExt;
|
num::NonZeroUsize,
|
||||||
use image::DynamicImage;
|
time::{SystemTime, UNIX_EPOCH}
|
||||||
use itertools::Itertools;
|
};
|
||||||
use ratatui_image::{Resize, picker::Picker, protocol::Protocol};
|
|
||||||
use rayon::iter::ParallelIterator;
|
|
||||||
|
|
||||||
use crate::renderer::{PageInfo, RenderError, fill_default};
|
use flume::{Receiver, SendError, Sender, TryRecvError};
|
||||||
|
use futures_util::stream::StreamExt as _;
|
||||||
|
use image::DynamicImage;
|
||||||
|
use kittage::{NumberOrId, action::NONZERO_ONE};
|
||||||
|
use ratatui::layout::Rect;
|
||||||
|
use ratatui_image::{
|
||||||
|
Resize,
|
||||||
|
picker::{Picker, ProtocolType},
|
||||||
|
protocol::Protocol
|
||||||
|
};
|
||||||
|
use rayon::iter::ParallelIterator as _;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
renderer::{PageInfo, RenderError, fill_default},
|
||||||
|
skip::InterleavedAroundWithMax
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum MaybeTransferred {
|
||||||
|
NotYet(kittage::image::Image<'static>),
|
||||||
|
Transferred(kittage::ImageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ConvertedImage {
|
||||||
|
Generic(Protocol),
|
||||||
|
Kitty {
|
||||||
|
img: MaybeTransferred,
|
||||||
|
cell_w: u16,
|
||||||
|
cell_h: u16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConvertedImage {
|
||||||
|
#[must_use]
|
||||||
|
pub fn w_h(&self) -> (u16, u16) {
|
||||||
|
match self {
|
||||||
|
Self::Generic(prot) => {
|
||||||
|
let a = prot.area();
|
||||||
|
(a.width, a.height)
|
||||||
|
}
|
||||||
|
Self::Kitty {
|
||||||
|
img: _,
|
||||||
|
cell_w,
|
||||||
|
cell_h
|
||||||
|
} => (*cell_w, *cell_h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ConvertedPage {
|
pub struct ConvertedPage {
|
||||||
pub page: Protocol,
|
pub page: ConvertedImage,
|
||||||
pub num: usize,
|
pub num: usize,
|
||||||
pub num_results: usize
|
pub num_results: usize
|
||||||
}
|
}
|
||||||
@@ -22,18 +68,22 @@ pub enum ConverterMsg {
|
|||||||
pub async fn run_conversion_loop(
|
pub async fn run_conversion_loop(
|
||||||
sender: Sender<Result<ConvertedPage, RenderError>>,
|
sender: Sender<Result<ConvertedPage, RenderError>>,
|
||||||
receiver: Receiver<ConverterMsg>,
|
receiver: Receiver<ConverterMsg>,
|
||||||
mut picker: Picker,
|
picker: Picker,
|
||||||
prerender: usize
|
prerender: usize,
|
||||||
|
shms_work: bool
|
||||||
) -> Result<(), SendError<Result<ConvertedPage, RenderError>>> {
|
) -> Result<(), SendError<Result<ConvertedPage, RenderError>>> {
|
||||||
let mut images = vec![];
|
let mut images = vec![];
|
||||||
let mut page: usize = 0;
|
let mut page: usize = 0;
|
||||||
|
let pid = std::process::id();
|
||||||
|
|
||||||
fn next_page(
|
fn next_page(
|
||||||
images: &mut [Option<PageInfo>],
|
images: &mut [Option<PageInfo>],
|
||||||
picker: &mut Picker,
|
picker: &Picker,
|
||||||
page: usize,
|
page: usize,
|
||||||
iteration: &mut usize,
|
iteration: &mut usize,
|
||||||
prerender: usize
|
prerender: usize,
|
||||||
|
pid: u32,
|
||||||
|
shms_work: bool
|
||||||
) -> Result<Option<ConvertedPage>, RenderError> {
|
) -> Result<Option<ConvertedPage>, RenderError> {
|
||||||
if images.is_empty() || *iteration >= prerender {
|
if images.is_empty() || *iteration >= prerender {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@@ -44,13 +94,19 @@ pub async fn run_conversion_loop(
|
|||||||
let idx_start = page.saturating_sub(prerender / 2);
|
let idx_start = page.saturating_sub(prerender / 2);
|
||||||
let idx_end = idx_start.saturating_add(prerender).min(images.len());
|
let idx_end = idx_start.saturating_add(prerender).min(images.len());
|
||||||
|
|
||||||
|
// If there's none to render, then why bother.
|
||||||
|
let Some(idx_end) = NonZeroUsize::new(idx_end) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
// then we go through all the indices available to us and find the first one that has an
|
// then we go through all the indices available to us and find the first one that has an
|
||||||
// image available to steal
|
// image available to steal
|
||||||
let Some((page_info, new_iter)) = (idx_start..page)
|
let Some((page_info, new_iter, page_num)) =
|
||||||
.interleave(page..idx_end)
|
InterleavedAroundWithMax::new(page, idx_start, idx_end)
|
||||||
.enumerate()
|
.enumerate()
|
||||||
|
.take(prerender)
|
||||||
// .skip(*iteration)
|
// .skip(*iteration)
|
||||||
.find_map(|(i_idx, p_idx)| images[p_idx].take().map(|p| (p, i_idx)))
|
.find_map(|(i_idx, p_idx)| images[p_idx].take().map(|p| (p, i_idx, p_idx)))
|
||||||
else {
|
else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
@@ -71,21 +127,56 @@ pub async fn run_conversion_loop(
|
|||||||
.for_each(|(_, _, px)| px.0[2] = px.0[2].saturating_sub(u8::MAX / 2));
|
.for_each(|(_, _, px)| px.0[2] = px.0[2].saturating_sub(u8::MAX / 2));
|
||||||
},
|
},
|
||||||
_ => unreachable!()
|
_ => unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
let img_area = Rect {
|
||||||
|
width: page_info.img_data.cell_w,
|
||||||
|
height: page_info.img_data.cell_h,
|
||||||
|
x: 0,
|
||||||
|
y: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
let img_area = page_info.img_data.cell_area;
|
let txt_img = match picker.protocol_type() {
|
||||||
|
ProtocolType::Kitty => {
|
||||||
|
let rn = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_nanos() % 1_000_000;
|
||||||
|
|
||||||
// We don't actually want to Crop this image, but we've already
|
let mut img = if shms_work {
|
||||||
// verified (with the ImageSurface stuff) that the image is the correct
|
kittage::image::Image::shm_from(dyn_img, &format!("/tdf_{pid}_{rn}_{page_num}"))
|
||||||
// size for the area given, so to save ratatui the work of having to
|
.map_err(|e| {
|
||||||
// resize it, we tell them to crop it to fit.
|
RenderError::Converting(format!("Couldn't write to shm: {e:?}"))
|
||||||
let txt_img = picker
|
})?
|
||||||
|
} else {
|
||||||
|
kittage::image::Image::from(dyn_img)
|
||||||
|
};
|
||||||
|
|
||||||
|
// if ur pdf has 4 billion pages then you deserve to suffer
|
||||||
|
img.num_or_id = NumberOrId::Id(NONZERO_ONE.saturating_add(page_num as u32));
|
||||||
|
|
||||||
|
ConvertedImage::Kitty {
|
||||||
|
img: MaybeTransferred::NotYet(img),
|
||||||
|
cell_w: page_info.img_data.cell_w,
|
||||||
|
cell_h: page_info.img_data.cell_h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => ConvertedImage::Generic(
|
||||||
|
picker
|
||||||
.new_protocol(dyn_img, img_area, Resize::None)
|
.new_protocol(dyn_img, img_area, Resize::None)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
RenderError::Converting(format!(
|
RenderError::Converting(format!(
|
||||||
"Couldn't convert DynamicImage to ratatui image: {e}"
|
"Couldn't convert DynamicImage to ratatui image: {e}"
|
||||||
))
|
))
|
||||||
})?;
|
})?
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
log::debug!(
|
||||||
|
"got converted page for num {} with results {:?}",
|
||||||
|
page_info.page_num,
|
||||||
|
page_info.result_rects
|
||||||
|
);
|
||||||
|
|
||||||
// update the iteration to the iteration that we stole this image from
|
// update the iteration to the iteration that we stole this image from
|
||||||
*iteration = new_iter;
|
*iteration = new_iter;
|
||||||
@@ -124,7 +215,15 @@ pub async fn run_conversion_loop(
|
|||||||
Err(TryRecvError::Disconnected) => return Ok(())
|
Err(TryRecvError::Disconnected) => return Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
match next_page(&mut images, &mut picker, page, &mut iteration, prerender) {
|
match next_page(
|
||||||
|
&mut images,
|
||||||
|
&picker,
|
||||||
|
page,
|
||||||
|
&mut iteration,
|
||||||
|
prerender,
|
||||||
|
pid,
|
||||||
|
shms_work
|
||||||
|
) {
|
||||||
Ok(None) => break,
|
Ok(None) => break,
|
||||||
Ok(Some(img)) => sender.send(Ok(img))?,
|
Ok(Some(img)) => sender.send(Ok(img))?,
|
||||||
Err(e) => sender.send(Err(e))?
|
Err(e) => sender.send(Err(e))?
|
||||||
|
|||||||
+209
@@ -0,0 +1,209 @@
|
|||||||
|
use std::{io::Write, num::NonZeroU32};
|
||||||
|
|
||||||
|
use crossterm::{
|
||||||
|
cursor::MoveTo,
|
||||||
|
event::EventStream,
|
||||||
|
execute,
|
||||||
|
terminal::{disable_raw_mode, enable_raw_mode}
|
||||||
|
};
|
||||||
|
use image::DynamicImage;
|
||||||
|
use kittage::{
|
||||||
|
AsyncInputReader, ImageDimensions, ImageId, NumberOrId, PixelFormat,
|
||||||
|
action::Action,
|
||||||
|
delete::{ClearOrDelete, DeleteConfig, WhichToDelete},
|
||||||
|
display::{CursorMovementPolicy, DisplayConfig, DisplayLocation},
|
||||||
|
error::TransmitError,
|
||||||
|
image::Image,
|
||||||
|
medium::Medium
|
||||||
|
};
|
||||||
|
use ratatui::layout::Position;
|
||||||
|
|
||||||
|
use crate::converter::MaybeTransferred;
|
||||||
|
|
||||||
|
pub struct KittyReadyToDisplay<'tui> {
|
||||||
|
pub img: &'tui mut MaybeTransferred,
|
||||||
|
pub page_num: usize,
|
||||||
|
pub pos: Position,
|
||||||
|
pub display_loc: DisplayLocation
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum KittyDisplay<'tui> {
|
||||||
|
NoChange,
|
||||||
|
ClearImages,
|
||||||
|
DisplayImages(Vec<KittyReadyToDisplay<'tui>>)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DbgWriter<W: Write> {
|
||||||
|
w: W,
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
buf: String
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: Write> Write for DbgWriter<W> {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
if let Ok(s) = std::str::from_utf8(buf) {
|
||||||
|
self.buf.push_str(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.w.write(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> std::io::Result<()> {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
log::debug!("Writing to kitty: {:?}", self.buf);
|
||||||
|
self.buf.clear();
|
||||||
|
}
|
||||||
|
self.w.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run_action<'es>(
|
||||||
|
action: Action<'_, '_>,
|
||||||
|
ev_stream: &'es mut EventStream
|
||||||
|
) -> Result<ImageId, TransmitError<<&'es mut EventStream as AsyncInputReader>::Error>> {
|
||||||
|
let writer = DbgWriter {
|
||||||
|
w: std::io::stdout().lock(),
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
buf: String::new()
|
||||||
|
};
|
||||||
|
action
|
||||||
|
.execute_async(writer, ev_stream)
|
||||||
|
.await
|
||||||
|
.map(|(_, i)| i)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn do_shms_work(ev_stream: &mut EventStream) -> bool {
|
||||||
|
let img = DynamicImage::new_rgb8(1, 1);
|
||||||
|
let pid = std::process::id();
|
||||||
|
let Ok(mut k_img) = kittage::image::Image::shm_from(img, &format!("tdf_test_{pid}")) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// apparently the terminal won't respond to queries unless they have an Id instead of a number
|
||||||
|
k_img.num_or_id = NumberOrId::Id(NonZeroU32::new(u32::MAX).unwrap());
|
||||||
|
|
||||||
|
enable_raw_mode().unwrap();
|
||||||
|
|
||||||
|
let res = run_action(Action::Query(&k_img), ev_stream).await;
|
||||||
|
|
||||||
|
disable_raw_mode().unwrap();
|
||||||
|
|
||||||
|
res.is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn display_kitty_images<'es>(
|
||||||
|
display: KittyDisplay<'_>,
|
||||||
|
ev_stream: &'es mut EventStream
|
||||||
|
) -> Result<
|
||||||
|
(),
|
||||||
|
(
|
||||||
|
Vec<usize>,
|
||||||
|
&'static str,
|
||||||
|
TransmitError<<&'es mut EventStream as AsyncInputReader>::Error>
|
||||||
|
)
|
||||||
|
> {
|
||||||
|
let images = match display {
|
||||||
|
KittyDisplay::NoChange => return Ok(()),
|
||||||
|
KittyDisplay::DisplayImages(_) | KittyDisplay::ClearImages => {
|
||||||
|
run_action(
|
||||||
|
Action::Delete(DeleteConfig {
|
||||||
|
effect: ClearOrDelete::Clear,
|
||||||
|
which: WhichToDelete::All
|
||||||
|
}),
|
||||||
|
ev_stream
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| (vec![], "Couldn't clear previous images", e))?;
|
||||||
|
|
||||||
|
let KittyDisplay::DisplayImages(images) = display else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
images
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut err = None;
|
||||||
|
for KittyReadyToDisplay {
|
||||||
|
img,
|
||||||
|
page_num,
|
||||||
|
pos,
|
||||||
|
display_loc
|
||||||
|
} in images
|
||||||
|
{
|
||||||
|
let config = DisplayConfig {
|
||||||
|
location: display_loc,
|
||||||
|
cursor_movement: CursorMovementPolicy::DontMove,
|
||||||
|
..DisplayConfig::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
execute!(std::io::stdout(), MoveTo(pos.x, pos.y)).unwrap();
|
||||||
|
|
||||||
|
log::debug!("going to display img {img:#?}");
|
||||||
|
log::debug!("displaying with config {config:#?}");
|
||||||
|
|
||||||
|
let this_err = match img {
|
||||||
|
MaybeTransferred::NotYet(image) => {
|
||||||
|
let mut fake_image = Image {
|
||||||
|
num_or_id: image.num_or_id,
|
||||||
|
format: PixelFormat::Rgb24(
|
||||||
|
ImageDimensions {
|
||||||
|
width: 0,
|
||||||
|
height: 0
|
||||||
|
},
|
||||||
|
None
|
||||||
|
),
|
||||||
|
medium: Medium::Direct {
|
||||||
|
chunk_size: None,
|
||||||
|
data: (&[]).into()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
std::mem::swap(image, &mut fake_image);
|
||||||
|
|
||||||
|
let res = run_action(
|
||||||
|
Action::TransmitAndDisplay {
|
||||||
|
image: fake_image,
|
||||||
|
config,
|
||||||
|
placement_id: None
|
||||||
|
},
|
||||||
|
ev_stream
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(img_id) => {
|
||||||
|
*img = MaybeTransferred::Transferred(img_id);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => Err((page_num, e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MaybeTransferred::Transferred(image_id) => run_action(
|
||||||
|
Action::Display {
|
||||||
|
image_id: *image_id,
|
||||||
|
placement_id: *image_id,
|
||||||
|
config
|
||||||
|
},
|
||||||
|
ev_stream
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|e| (page_num, e))
|
||||||
|
};
|
||||||
|
|
||||||
|
log::debug!("this_err is {this_err:#?}");
|
||||||
|
|
||||||
|
if let Err((id, e)) = this_err {
|
||||||
|
let e = err.get_or_insert_with(|| (vec![], e));
|
||||||
|
e.0.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match err {
|
||||||
|
Some((replace, e)) => Err((replace, "Couldn't transfer image to the terminal", e)),
|
||||||
|
None => Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
+53
@@ -1,7 +1,60 @@
|
|||||||
|
use std::num::NonZeroUsize;
|
||||||
|
|
||||||
#[global_allocator]
|
#[global_allocator]
|
||||||
static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub enum PrerenderLimit {
|
||||||
|
All,
|
||||||
|
Limited(NonZeroUsize)
|
||||||
|
}
|
||||||
|
|
||||||
pub mod converter;
|
pub mod converter;
|
||||||
|
pub mod kitty;
|
||||||
pub mod renderer;
|
pub mod renderer;
|
||||||
pub mod skip;
|
pub mod skip;
|
||||||
pub mod tui;
|
pub mod tui;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
|
pub enum FitOrFill {
|
||||||
|
Fit,
|
||||||
|
Fill
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ScaledResult {
|
||||||
|
width: f32,
|
||||||
|
height: f32,
|
||||||
|
scale_factor: f32
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn scale_img_for_area(
|
||||||
|
(img_width, img_height): (f32, f32),
|
||||||
|
(area_width, area_height): (f32, f32),
|
||||||
|
fit_or_fill: FitOrFill
|
||||||
|
) -> ScaledResult {
|
||||||
|
// and get its aspect ratio
|
||||||
|
let img_aspect_ratio = img_width / img_height;
|
||||||
|
|
||||||
|
// Then we get the full pixel dimensions of the area provided to us, and the aspect ratio
|
||||||
|
// of that area
|
||||||
|
let area_aspect_ratio = area_width / area_height;
|
||||||
|
|
||||||
|
// and get the ratio that this page would have to be scaled by to fit perfectly within the
|
||||||
|
// area provided to us.
|
||||||
|
// we do this first by comparing the aspec ratio of the page with the aspect ratio of the
|
||||||
|
// area to fit it within. If the aspect ratio of the page is larger, then we need to scale
|
||||||
|
// the width of the page to fill perfectly within the height of the area. Otherwise, we
|
||||||
|
// scale the height to fit perfectly. The dimension that _is not_ scaled to fit perfectly
|
||||||
|
// is scaled by the same factor as the dimension that _is_ scaled perfectly.
|
||||||
|
let scale_factor = match (img_aspect_ratio > area_aspect_ratio, fit_or_fill) {
|
||||||
|
(true, FitOrFill::Fit) | (false, FitOrFill::Fill) => area_width / img_width,
|
||||||
|
(false, FitOrFill::Fit) | (true, FitOrFill::Fill) => area_height / img_height
|
||||||
|
};
|
||||||
|
|
||||||
|
ScaledResult {
|
||||||
|
width: img_width * scale_factor,
|
||||||
|
height: img_height * scale_factor,
|
||||||
|
scale_factor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+436
-112
@@ -1,60 +1,183 @@
|
|||||||
|
use core::{
|
||||||
|
error::Error,
|
||||||
|
num::{NonZeroU32, NonZeroUsize}
|
||||||
|
};
|
||||||
use std::{
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
ffi::OsString,
|
ffi::OsString,
|
||||||
io::{Read, Write, stdout},
|
io::{BufReader, Read as _, Stdout, Write as _, stdout},
|
||||||
num::NonZeroUsize,
|
mem,
|
||||||
path::PathBuf
|
path::PathBuf,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
time::Duration
|
||||||
};
|
};
|
||||||
|
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
|
event::EventStream,
|
||||||
execute,
|
execute,
|
||||||
terminal::{
|
terminal::{
|
||||||
EndSynchronizedUpdate, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode,
|
EndSynchronizedUpdate, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode,
|
||||||
enable_raw_mode, window_size
|
enable_raw_mode, window_size
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
use futures_util::{FutureExt, stream::StreamExt};
|
use debounce::EventDebouncer;
|
||||||
use notify::{Event, EventKind, RecursiveMode, Watcher};
|
use flexi_logger::FileSpec;
|
||||||
|
use flume::{Sender, r#async::RecvStream};
|
||||||
|
use futures_util::{FutureExt as _, stream::StreamExt as _};
|
||||||
|
use kittage::{
|
||||||
|
action::Action,
|
||||||
|
delete::{ClearOrDelete, DeleteConfig, WhichToDelete},
|
||||||
|
error::{TerminalError, TransmitError}
|
||||||
|
};
|
||||||
|
use notify::{Event, EventKind, RecursiveMode, Watcher as _};
|
||||||
use ratatui::{Terminal, backend::CrosstermBackend};
|
use ratatui::{Terminal, backend::CrosstermBackend};
|
||||||
use ratatui_image::picker::Picker;
|
use ratatui_image::{
|
||||||
|
FontSize,
|
||||||
|
picker::{Picker, ProtocolType}
|
||||||
|
};
|
||||||
use tdf::{
|
use tdf::{
|
||||||
|
PrerenderLimit,
|
||||||
converter::{ConvertedPage, ConverterMsg, run_conversion_loop},
|
converter::{ConvertedPage, ConverterMsg, run_conversion_loop},
|
||||||
renderer::{self, RenderError, RenderInfo, RenderNotif},
|
kitty::{KittyDisplay, display_kitty_images, do_shms_work, run_action},
|
||||||
|
renderer::{self, MUPDF_BLACK, MUPDF_WHITE, RenderError, RenderInfo, RenderNotif},
|
||||||
tui::{BottomMessage, InputAction, MessageSetting, Tui}
|
tui::{BottomMessage, InputAction, MessageSetting, Tui}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Dummy struct for easy errors in main
|
// Dummy struct for easy errors in main
|
||||||
#[derive(Debug)]
|
struct WrappedErr(Cow<'static, str>);
|
||||||
struct BadTermSizeStdin(String);
|
|
||||||
|
|
||||||
impl std::fmt::Display for BadTermSizeStdin {
|
impl std::fmt::Display for WrappedErr {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "{}", self.0)
|
write!(f, "{}", self.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::error::Error for BadTermSizeStdin {}
|
impl std::fmt::Debug for WrappedErr {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
std::fmt::Display::fmt(self, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for WrappedErr {}
|
||||||
|
|
||||||
|
fn reset_term() {
|
||||||
|
_ = disable_raw_mode();
|
||||||
|
_ = execute!(
|
||||||
|
std::io::stdout(),
|
||||||
|
LeaveAlternateScreen,
|
||||||
|
crossterm::cursor::Show,
|
||||||
|
crossterm::event::DisableMouseCapture
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), WrappedErr> {
|
||||||
|
let result = inner_main().await;
|
||||||
|
reset_term();
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn inner_main() -> Result<(), WrappedErr> {
|
||||||
|
let hook = std::panic::take_hook();
|
||||||
|
std::panic::set_hook(Box::new(move |info| {
|
||||||
|
reset_term();
|
||||||
|
hook(info);
|
||||||
|
}));
|
||||||
|
|
||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
console_subscriber::init();
|
console_subscriber::init();
|
||||||
|
|
||||||
|
const DEFAULT_DEBOUNCE_DELAY: Duration = Duration::from_millis(50);
|
||||||
|
|
||||||
let flags = xflags::parse_or_exit! {
|
let flags = xflags::parse_or_exit! {
|
||||||
/// Display the pdf with the pages starting at the right hand size and moving left and
|
/// Display the pdf with the pages starting at the right hand size and moving left and
|
||||||
/// adjust input keys to match
|
/// adjust input keys to match
|
||||||
optional -r,--r-to-l r_to_l: bool
|
optional -r,--r-to-l
|
||||||
/// The maximum number of pages to display together, horizontally, at a time
|
/// The maximum number of pages to display together, horizontally, at a time
|
||||||
optional -m,--max-wide max_wide: NonZeroUsize
|
optional -m,--max-wide max_wide: NonZeroUsize
|
||||||
/// Fullscreen the pdf (hide document name, page count, etc)
|
/// Fullscreen the pdf (hide document name, page count, etc)
|
||||||
optional -f,--fullscreen fullscreen: bool
|
optional -f,--fullscreen
|
||||||
|
/// The time to wait for the file to stop changing before reloading, in milliseconds.
|
||||||
|
/// Defaults to 50ms.
|
||||||
|
optional --reload-delay reload_delay: u64
|
||||||
|
/// The number of pages to prerender surrounding the currently-shown page; 0 means no
|
||||||
|
/// limit. By default, there is no limit.
|
||||||
|
optional -p,--prerender prerender: usize
|
||||||
|
/// Custom white color, specified in css format (e.g. "FFFFFF" or "rgb(255, 255, 255)")
|
||||||
|
optional -w,--white-color white: String
|
||||||
|
/// Custom black color, specified in css format (e.g "000000" or "rgb(0, 0, 0)")
|
||||||
|
optional -b,--black-color black: String
|
||||||
|
/// Print the version and exit
|
||||||
|
optional --version
|
||||||
/// PDF file to read
|
/// PDF file to read
|
||||||
required file: PathBuf
|
optional file: PathBuf
|
||||||
};
|
};
|
||||||
|
|
||||||
let path = flags.file.canonicalize()?;
|
if flags.version {
|
||||||
|
println!("{}", env!("CARGO_PKG_VERSION"));
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(file) = flags.file else {
|
||||||
|
return Err(WrappedErr(
|
||||||
|
"Please specify the file to open, e.g. `tdf ./my_example_pdf.pdf`".into()
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
let path = file
|
||||||
|
.canonicalize()
|
||||||
|
.map_err(|e| WrappedErr(format!("Cannot canonicalize provided file: {e}").into()))?;
|
||||||
|
|
||||||
|
let black = flags
|
||||||
|
.black_color
|
||||||
|
.as_deref()
|
||||||
|
.map(|color| {
|
||||||
|
parse_color_to_i32(color).map_err(|e| {
|
||||||
|
WrappedErr(
|
||||||
|
format!(
|
||||||
|
"Couldn't parse black color {color:?}: {e} - is it formatted like a CSS color?"
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or(MUPDF_BLACK);
|
||||||
|
|
||||||
|
let white = flags
|
||||||
|
.white_color
|
||||||
|
.as_deref()
|
||||||
|
.map(|color| {
|
||||||
|
parse_color_to_i32(color).map_err(|e| {
|
||||||
|
WrappedErr(
|
||||||
|
format!(
|
||||||
|
"Couldn't parse white color {color:?}: {e} - is it formatted like a CSS color?"
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or(MUPDF_WHITE);
|
||||||
|
|
||||||
|
// need to keep it around throughout the lifetime of the program, but don't rly need to use it.
|
||||||
|
// Just need to make sure it doesn't get dropped yet.
|
||||||
|
let maybe_logger = if std::env::var("RUST_LOG").is_ok() {
|
||||||
|
Some(
|
||||||
|
flexi_logger::Logger::try_with_env()
|
||||||
|
.map_err(|e| WrappedErr(format!("Couldn't create initial logger: {e}").into()))?
|
||||||
|
.log_to_file(FileSpec::try_from("./debug.log").map_err(|e| {
|
||||||
|
WrappedErr(format!("Couldn't create FileSpec for logger: {e}").into())
|
||||||
|
})?)
|
||||||
|
.start()
|
||||||
|
.map_err(|e| WrappedErr(format!("Can't start logger: {e}").into()))?
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let (watch_to_render_tx, render_rx) = flume::unbounded();
|
let (watch_to_render_tx, render_rx) = flume::unbounded();
|
||||||
let tui_tx = watch_to_render_tx.clone();
|
let to_renderer = 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();
|
||||||
@@ -63,9 +186,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
watch_to_tui_tx,
|
watch_to_tui_tx,
|
||||||
watch_to_render_tx,
|
watch_to_render_tx,
|
||||||
path.file_name()
|
path.file_name()
|
||||||
.ok_or("Path does not have a last component??")?
|
.ok_or_else(|| WrappedErr("Path does not have a last component??".into()))?
|
||||||
.to_owned()
|
.to_owned(),
|
||||||
))?;
|
flags
|
||||||
|
.reload_delay
|
||||||
|
.map_or(DEFAULT_DEBOUNCE_DELAY, Duration::from_millis)
|
||||||
|
))
|
||||||
|
.map_err(|e| WrappedErr(format!("Couldn't start watching the provided file: {e}").into()))?;
|
||||||
|
|
||||||
// So we have to watch the parent directory of the file that we are interested in because the
|
// So we have to watch the parent directory of the file that we are interested in because the
|
||||||
// `notify` library works on inodes, and if the file is deleted, that inode is gone as well,
|
// `notify` library works on inodes, and if the file is deleted, that inode is gone as well,
|
||||||
@@ -75,112 +202,189 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// opinion on this clear
|
// opinion on this clear
|
||||||
// (https://github.com/notify-rs/notify/issues/113#issuecomment-281836995) so whatever, guess
|
// (https://github.com/notify-rs/notify/issues/113#issuecomment-281836995) so whatever, guess
|
||||||
// we have to do this annoying workaround.
|
// we have to do this annoying workaround.
|
||||||
watcher.watch(
|
watcher
|
||||||
|
.watch(
|
||||||
path.parent().expect("The root directory is not a PDF"),
|
path.parent().expect("The root directory is not a PDF"),
|
||||||
RecursiveMode::NonRecursive
|
RecursiveMode::NonRecursive
|
||||||
)?;
|
)
|
||||||
|
.map_err(|e| WrappedErr(format!("Can't watch the provided file: {e}").into()))?;
|
||||||
|
|
||||||
// TODO: Handle non-utf8 file names? Maybe by constructing a CString and passing that in to the
|
// TODO: Handle non-utf8 file names? Maybe by constructing a CString and passing that in to the
|
||||||
// mupdf stuff instead of a rust string?
|
// mupdf stuff instead of a rust string?
|
||||||
let file_path = path.clone().into_os_string().to_string_lossy().to_string();
|
let file_path = path.clone().into_os_string().to_string_lossy().to_string();
|
||||||
|
|
||||||
let mut window_size = window_size()?;
|
let mut window_size = window_size().map_err(|e| {
|
||||||
|
WrappedErr(format!("Can't get your current terminal window size: {e}").into())
|
||||||
|
})?;
|
||||||
|
|
||||||
if window_size.width == 0 || window_size.height == 0 {
|
if window_size.width == 0 || window_size.height == 0 {
|
||||||
// send the command code to get the terminal window size
|
let (w, h) = get_font_size_through_stdio()?;
|
||||||
print!("\x1b[14t");
|
|
||||||
std::io::stdout().flush()?;
|
|
||||||
|
|
||||||
// we need to enable raw mode here since this bit of output won't print a newline; it'll
|
window_size.width = w;
|
||||||
// just print the info it wants to tell us. So we want to get all characters as they come
|
window_size.height = h;
|
||||||
enable_raw_mode()?;
|
|
||||||
|
|
||||||
// read in the returned size until we hit a 't' (which indicates to us it's done)
|
|
||||||
let input_vec = std::io::stdin()
|
|
||||||
.bytes()
|
|
||||||
.filter_map(Result::ok)
|
|
||||||
.take_while(|b| *b != b't')
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
// and then disable raw mode again in case we return an error in this next section
|
|
||||||
disable_raw_mode()?;
|
|
||||||
|
|
||||||
let input_line = String::from_utf8(input_vec)?;
|
|
||||||
|
|
||||||
if input_line.starts_with("\x1b[4;") {
|
|
||||||
// it should input it to us as `\e[4;<height>;<width>t`, so we need to split to get the h/w
|
|
||||||
// ignore the first val
|
|
||||||
let mut splits = input_line.split([';', 't']).skip(1);
|
|
||||||
|
|
||||||
window_size.height = splits
|
|
||||||
.next()
|
|
||||||
.ok_or_else(|| {
|
|
||||||
BadTermSizeStdin(format!(
|
|
||||||
"Terminal responded with unparseable size response '{input_line}'"
|
|
||||||
))
|
|
||||||
})?
|
|
||||||
.parse::<u16>()?;
|
|
||||||
|
|
||||||
window_size.width = splits
|
|
||||||
.next()
|
|
||||||
.ok_or_else(|| {
|
|
||||||
BadTermSizeStdin(format!(
|
|
||||||
"Terminal responded with unparseable size response '{input_line}'"
|
|
||||||
))
|
|
||||||
})?
|
|
||||||
.parse::<u16>()?;
|
|
||||||
} else {
|
|
||||||
return Err("Your terminal is falsely reporting a window size of 0; tdf needs an accurate window size to display graphics".into());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let cell_height_px = window_size.height / window_size.rows;
|
||||||
|
let cell_width_px = window_size.width / window_size.columns;
|
||||||
|
|
||||||
|
execute!(
|
||||||
|
std::io::stdout(),
|
||||||
|
EnterAlternateScreen,
|
||||||
|
crossterm::cursor::Hide,
|
||||||
|
crossterm::event::EnableMouseCapture
|
||||||
|
)
|
||||||
|
.map_err(|e| {
|
||||||
|
WrappedErr(
|
||||||
|
format!(
|
||||||
|
"Couldn't enter the alternate screen and hide the cursor for proper presentation: {e}"
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
// 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 picker = Picker::from_query_stdio()?;
|
let picker = Picker::from_query_stdio()
|
||||||
|
.or_else(|e| match e {
|
||||||
|
ratatui_image::errors::Errors::NoFontSize if
|
||||||
|
window_size.width != 0
|
||||||
|
&& window_size.height != 0
|
||||||
|
&& window_size.columns != 0
|
||||||
|
&& window_size.rows != 0
|
||||||
|
=> Ok(Picker::from_fontsize((cell_width_px, cell_height_px))),
|
||||||
|
ratatui_image::errors::Errors::NoFontSize => Err(WrappedErr(
|
||||||
|
"Unable to detect your terminal's font size; this is an issue with your terminal emulator.\nPlease use a different terminal emulator or report this bug to tdf.".into()
|
||||||
|
)),
|
||||||
|
e => Err(WrappedErr(format!("Couldn't get the necessary information to set up images: {e}").into()))
|
||||||
|
})?;
|
||||||
|
|
||||||
// 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
|
||||||
|
let prerender = flags
|
||||||
|
.prerender
|
||||||
|
.and_then(NonZeroUsize::new)
|
||||||
|
.map_or(PrerenderLimit::All, PrerenderLimit::Limited);
|
||||||
|
|
||||||
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,
|
||||||
|
cell_height_px,
|
||||||
|
cell_width_px,
|
||||||
|
prerender,
|
||||||
|
black,
|
||||||
|
white
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let font_size = picker.font_size();
|
||||||
|
|
||||||
let mut ev_stream = crossterm::event::EventStream::new();
|
let mut ev_stream = crossterm::event::EventStream::new();
|
||||||
|
|
||||||
let (to_converter, from_main) = flume::unbounded();
|
let (to_converter, from_main) = flume::unbounded();
|
||||||
let (to_main, from_converter) = flume::unbounded();
|
let (to_main, from_converter) = flume::unbounded();
|
||||||
|
|
||||||
tokio::spawn(run_conversion_loop(to_main, from_main, picker, 20));
|
let is_kitty = picker.protocol_type() == ProtocolType::Kitty;
|
||||||
|
|
||||||
|
let shms_work = is_kitty && do_shms_work(&mut ev_stream).await;
|
||||||
|
|
||||||
|
tokio::spawn(run_conversion_loop(
|
||||||
|
to_main, from_main, picker, 20, shms_work
|
||||||
|
));
|
||||||
|
|
||||||
let file_name = path.file_name().map_or_else(
|
let file_name = path.file_name().map_or_else(
|
||||||
|| "Unknown file".into(),
|
|| "Unknown file".into(),
|
||||||
|n| n.to_string_lossy().to_string()
|
|n| n.to_string_lossy().to_string()
|
||||||
);
|
);
|
||||||
let mut tui = Tui::new(file_name, flags.max_wide, flags.r_to_l.unwrap_or_default());
|
let tui = Tui::new(file_name, flags.max_wide, flags.r_to_l, is_kitty);
|
||||||
|
|
||||||
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).map_err(|e| {
|
||||||
|
WrappedErr(format!("Couldn't set up crossterm's terminal backend: {e}").into())
|
||||||
|
})?;
|
||||||
term.skip_diff(true);
|
term.skip_diff(true);
|
||||||
|
|
||||||
execute!(
|
enable_raw_mode().map_err(|e| {
|
||||||
term.backend_mut(),
|
WrappedErr(
|
||||||
EnterAlternateScreen,
|
format!("Can't enable raw mode, which is necessary to receive input: {e}").into()
|
||||||
crossterm::cursor::Hide
|
)
|
||||||
)?;
|
})?;
|
||||||
enable_raw_mode()?;
|
|
||||||
|
|
||||||
let mut fullscreen = flags.fullscreen.unwrap_or_default();
|
if is_kitty {
|
||||||
let mut main_area = Tui::main_layout(&term.get_frame(), fullscreen);
|
run_action(
|
||||||
tui_tx.send(RenderNotif::Area(main_area.page_area))?;
|
Action::Delete(DeleteConfig {
|
||||||
|
effect: ClearOrDelete::Delete,
|
||||||
|
which: WhichToDelete::IdRange(NonZeroU32::new(1).unwrap()..=NonZeroU32::MAX)
|
||||||
|
}),
|
||||||
|
&mut ev_stream
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
WrappedErr(format!("Couldn't delete all previous images from memory: {e}").into())
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
let mut tui_rx = tui_rx.into_stream();
|
let fullscreen = flags.fullscreen;
|
||||||
let mut from_converter = from_converter.into_stream();
|
let main_area = Tui::main_layout(&term.get_frame(), fullscreen);
|
||||||
|
to_renderer
|
||||||
|
.send(RenderNotif::Area(main_area.page_area))
|
||||||
|
.map_err(|e| {
|
||||||
|
WrappedErr(
|
||||||
|
format!("Couldn't inform the rendering thread of the available area: {e}").into()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let tui_rx = tui_rx.into_stream();
|
||||||
|
let from_converter = from_converter.into_stream();
|
||||||
|
|
||||||
|
enter_redraw_loop(
|
||||||
|
ev_stream,
|
||||||
|
to_renderer,
|
||||||
|
tui_rx,
|
||||||
|
to_converter,
|
||||||
|
from_converter,
|
||||||
|
fullscreen,
|
||||||
|
tui,
|
||||||
|
&mut term,
|
||||||
|
main_area,
|
||||||
|
font_size
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
WrappedErr(
|
||||||
|
format!(
|
||||||
|
"An unexpected error occurred while communicating between different parts of tdf: {e}"
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
drop(maybe_logger);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// oh shut up clippy who cares
|
||||||
|
#[expect(clippy::too_many_arguments)]
|
||||||
|
async fn enter_redraw_loop(
|
||||||
|
mut ev_stream: EventStream,
|
||||||
|
to_renderer: Sender<RenderNotif>,
|
||||||
|
mut tui_rx: RecvStream<'_, Result<RenderInfo, RenderError>>,
|
||||||
|
to_converter: Sender<ConverterMsg>,
|
||||||
|
mut from_converter: RecvStream<'_, Result<ConvertedPage, RenderError>>,
|
||||||
|
mut fullscreen: bool,
|
||||||
|
mut tui: Tui,
|
||||||
|
term: &mut Terminal<CrosstermBackend<Stdout>>,
|
||||||
|
mut main_area: tdf::tui::RenderLayout,
|
||||||
|
font_size: FontSize
|
||||||
|
) -> Result<(), Box<dyn Error>> {
|
||||||
loop {
|
loop {
|
||||||
let mut needs_redraw = true;
|
let mut needs_redraw = true;
|
||||||
|
let next_ev = ev_stream.next().fuse();
|
||||||
tokio::select! {
|
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) = next_ev => {
|
||||||
// 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");
|
||||||
|
|
||||||
@@ -188,14 +392,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
None => needs_redraw = false,
|
None => needs_redraw = false,
|
||||||
Some(action) => match action {
|
Some(action) => match action {
|
||||||
InputAction::Redraw => (),
|
InputAction::Redraw => (),
|
||||||
InputAction::QuitApp => break,
|
InputAction::QuitApp => return Ok(()),
|
||||||
InputAction::JumpingToPage(page) => {
|
InputAction::JumpingToPage(page) => {
|
||||||
tui_tx.send(RenderNotif::JumpToPage(page))?;
|
to_renderer.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) => to_renderer.send(RenderNotif::Search(term))?,
|
||||||
InputAction::Invert => tui_tx.send(RenderNotif::Invert)?,
|
InputAction::Invert => to_renderer.send(RenderNotif::Invert)?,
|
||||||
InputAction::Fullscreen => fullscreen = !fullscreen,
|
InputAction::Fullscreen => fullscreen = !fullscreen,
|
||||||
|
InputAction::SwitchRenderZoom(f_or_f) => {
|
||||||
|
to_renderer.send(RenderNotif::SwitchFitOrFill(f_or_f)).unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -211,13 +418,20 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
to_converter.send(ConverterMsg::AddImg(info))?;
|
to_converter.send(ConverterMsg::AddImg(info))?;
|
||||||
},
|
},
|
||||||
RenderInfo::Reloaded => tui.set_msg(MessageSetting::Some(BottomMessage::Reloaded)),
|
RenderInfo::Reloaded => tui.set_msg(MessageSetting::Some(BottomMessage::Reloaded)),
|
||||||
|
RenderInfo::SearchResults { page_num, num_results } =>
|
||||||
|
tui.got_num_results_on_page(page_num, num_results),
|
||||||
},
|
},
|
||||||
Err(e) => tui.show_error(e),
|
Err(e) => tui.show_error(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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);
|
||||||
|
if num == tui.page {
|
||||||
|
needs_redraw = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
Err(e) => tui.show_error(e),
|
Err(e) => tui.show_error(e),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -226,37 +440,74 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let new_area = Tui::main_layout(&term.get_frame(), fullscreen);
|
let new_area = Tui::main_layout(&term.get_frame(), fullscreen);
|
||||||
if new_area != main_area {
|
if new_area != main_area {
|
||||||
main_area = new_area;
|
main_area = new_area;
|
||||||
tui_tx.send(RenderNotif::Area(main_area.page_area))?;
|
to_renderer.send(RenderNotif::Area(main_area.page_area))?;
|
||||||
needs_redraw = true;
|
needs_redraw = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if needs_redraw {
|
if needs_redraw {
|
||||||
|
let mut to_display = KittyDisplay::NoChange;
|
||||||
term.draw(|f| {
|
term.draw(|f| {
|
||||||
tui.render(f, &main_area);
|
to_display = tui.render(f, &main_area, font_size);
|
||||||
})?;
|
})?;
|
||||||
execute!(stdout(), EndSynchronizedUpdate)?;
|
|
||||||
|
let maybe_err = display_kitty_images(to_display, &mut ev_stream).await;
|
||||||
|
|
||||||
|
if let Err((to_replace, err_desc, enum_err)) = maybe_err {
|
||||||
|
match enum_err {
|
||||||
|
// This is the error that kitty & ghostty provide us when they delete an
|
||||||
|
// image due to memory constraints, so if we get it, we just fix it by
|
||||||
|
// re-rendering so it don't display it to the user
|
||||||
|
//
|
||||||
|
// [TODO] maybe when we detect that an image was deleted, we probe the
|
||||||
|
// terminal for the pages around it to see if they were deleted too and if
|
||||||
|
// they were, we re-render them? idk
|
||||||
|
TransmitError::Terminal(TerminalError::NoEntity(_)) => (),
|
||||||
|
_ => tui.set_msg(MessageSetting::Some(BottomMessage::Error(format!(
|
||||||
|
"{err_desc}: {enum_err}"
|
||||||
|
))))
|
||||||
|
}
|
||||||
|
|
||||||
|
for page_num in to_replace {
|
||||||
|
tui.page_failed_display(page_num);
|
||||||
|
// So that they get re-rendered and sent over again
|
||||||
|
to_renderer.send(RenderNotif::PageNeedsReRender(page_num))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
execute!(
|
execute!(stdout().lock(), EndSynchronizedUpdate)?;
|
||||||
term.backend_mut(),
|
}
|
||||||
LeaveAlternateScreen,
|
}
|
||||||
crossterm::cursor::Show
|
|
||||||
)?;
|
|
||||||
disable_raw_mode()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_notify_ev(
|
fn on_notify_ev(
|
||||||
to_tui_tx: flume::Sender<Result<RenderInfo, RenderError>>,
|
to_tui_tx: flume::Sender<Result<RenderInfo, RenderError>>,
|
||||||
to_render_tx: flume::Sender<RenderNotif>,
|
to_render_tx: flume::Sender<RenderNotif>,
|
||||||
file_name: OsString
|
file_name: OsString,
|
||||||
|
debounce_delay: Duration
|
||||||
) -> impl Fn(notify::Result<Event>) {
|
) -> impl Fn(notify::Result<Event>) {
|
||||||
move |res| match res {
|
let last_event: Mutex<Result<(), RenderError>> = Mutex::new(Ok(()));
|
||||||
|
let last_event = Arc::new(last_event);
|
||||||
|
|
||||||
|
let debouncer = EventDebouncer::new(debounce_delay, {
|
||||||
|
let last_event = last_event.clone();
|
||||||
|
move |()| {
|
||||||
|
let event = mem::replace(&mut *last_event.lock().unwrap(), Ok(()));
|
||||||
|
match event {
|
||||||
|
// 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.
|
||||||
|
Ok(()) => to_render_tx.send(RenderNotif::Reload).unwrap(),
|
||||||
// If we get an error here, and then an error sending, everything's going wrong. Just give
|
// If we get an error here, and then an error sending, everything's going wrong. Just give
|
||||||
// up lol.
|
// up lol.
|
||||||
Err(e) => to_tui_tx.send(Err(RenderError::Notify(e))).unwrap(),
|
Err(e) => to_tui_tx.send(Err(e)).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
move |res| {
|
||||||
|
let event = match res {
|
||||||
|
Err(e) => Err(RenderError::Notify(e)),
|
||||||
|
|
||||||
// TODO: Should we match EventKind::Rename and propogate that so that the other parts of the
|
// TODO: Should we match EventKind::Rename and propogate that so that the other parts of the
|
||||||
// process know that too? Or should that be
|
// process know that too? Or should that be
|
||||||
Ok(ev) => {
|
Ok(ev) => {
|
||||||
@@ -272,16 +523,89 @@ fn on_notify_ev(
|
|||||||
}
|
}
|
||||||
|
|
||||||
match ev.kind {
|
match ev.kind {
|
||||||
EventKind::Access(_) => (),
|
EventKind::Access(_) => return,
|
||||||
EventKind::Remove(_) => to_tui_tx
|
EventKind::Remove(_) => Err(RenderError::Converting("File was deleted".into())),
|
||||||
.send(Err(RenderError::Converting("File was deleted".into())))
|
EventKind::Other
|
||||||
.unwrap(),
|
| EventKind::Any
|
||||||
// This shouldn't fail to send unless the receiver gets disconnected. If that's
|
| EventKind::Create(_)
|
||||||
// happened, then like the main thread has panicked or something, so it doesn't matter
|
| EventKind::Modify(_) => Ok(())
|
||||||
// we don't handle the error here.
|
|
||||||
EventKind::Other | EventKind::Any | EventKind::Create(_) | EventKind::Modify(_) =>
|
|
||||||
to_render_tx.send(RenderNotif::Reload).unwrap(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
*last_event.lock().unwrap() = event;
|
||||||
|
debouncer.put(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_color_to_i32(cs: &str) -> Result<i32, csscolorparser::ParseColorError> {
|
||||||
|
let color = csscolorparser::parse(cs)?;
|
||||||
|
let [r, g, b, _] = color.to_rgba8();
|
||||||
|
Ok(i32::from_be_bytes([0, r, g, b]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_font_size_through_stdio() -> Result<(u16, u16), WrappedErr> {
|
||||||
|
// send the command code to get the terminal window size
|
||||||
|
print!("\x1b[14t");
|
||||||
|
std::io::stdout().flush().unwrap();
|
||||||
|
|
||||||
|
// we need to enable raw mode here since this bit of output won't print a newline; it'll
|
||||||
|
// just print the info it wants to tell us. So we want to get all characters as they come
|
||||||
|
enable_raw_mode().map_err(|e| {
|
||||||
|
WrappedErr(
|
||||||
|
format!("Can't enable raw mode, which is necessary to receive input: {e}").into()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// read in the returned size until we hit a 't' (which indicates to us it's done)
|
||||||
|
let input_vec = BufReader::new(std::io::stdin())
|
||||||
|
.bytes()
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.take_while(|b| *b != b't')
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// and then disable raw mode again in case we return an error in this next section
|
||||||
|
disable_raw_mode().map_err(|e| {
|
||||||
|
WrappedErr(format!("Can't put the terminal back into a normal input state: {e}").into())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let input_line = String::from_utf8(input_vec).map_err(|e| {
|
||||||
|
WrappedErr(
|
||||||
|
format!(
|
||||||
|
"The terminal responded to our request for its font size by providing non-utf8 data: {e}"
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let input_line = input_line
|
||||||
|
.trim_start_matches("\x1b[4")
|
||||||
|
.trim_start_matches(';');
|
||||||
|
|
||||||
|
// it should input it to us as `\e[4;<height>;<width>t`, so we need to split to get the h/w
|
||||||
|
// ignore the first val
|
||||||
|
let mut splits = input_line.split([';', 't']);
|
||||||
|
|
||||||
|
let (Some(h), Some(w)) = (splits.next(), splits.next()) else {
|
||||||
|
return Err(WrappedErr(
|
||||||
|
format!("Terminal responded with unparseable size response '{input_line}'").into()
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
let h = h.parse::<u16>().map_err(|e| {
|
||||||
|
WrappedErr(
|
||||||
|
format!(
|
||||||
|
"Your terminal said its height is {h}, but that is not a 16-bit unsigned integer: {e}"
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let w = w.parse::<u16>().map_err(|e| {
|
||||||
|
WrappedErr(
|
||||||
|
format!(
|
||||||
|
"Your terminal said its width is {w}, but that is not a 16-bit unsigned integer: {e}"
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok((w, h))
|
||||||
|
}
|
||||||
|
|||||||
+315
-160
@@ -1,15 +1,24 @@
|
|||||||
use std::{thread::sleep, time::Duration};
|
use std::{collections::VecDeque, num::NonZeroUsize, thread::sleep, time::Duration};
|
||||||
|
|
||||||
use crossterm::terminal::WindowSize;
|
|
||||||
use flume::{Receiver, SendError, Sender, TryRecvError};
|
use flume::{Receiver, SendError, Sender, TryRecvError};
|
||||||
use itertools::Itertools;
|
use mupdf::{
|
||||||
use mupdf::{Colorspace, Document, Matrix, Page, Pixmap};
|
Colorspace, Document, Matrix, Page, Pixmap, Quad, TextPageFlags, text_page::SearchHitResponse
|
||||||
|
};
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
FitOrFill, PrerenderLimit, ScaledResult, scale_img_for_area, skip::InterleavedAroundWithMax
|
||||||
|
};
|
||||||
|
|
||||||
|
const KITTY_MAX_W_OR_H: f32 = 10_000.0;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum RenderNotif {
|
pub enum RenderNotif {
|
||||||
Area(Rect),
|
Area(Rect),
|
||||||
JumpToPage(usize),
|
JumpToPage(usize),
|
||||||
|
PageNeedsReRender(usize),
|
||||||
Search(String),
|
Search(String),
|
||||||
|
SwitchFitOrFill(FitOrFill),
|
||||||
Reload,
|
Reload,
|
||||||
Invert
|
Invert
|
||||||
}
|
}
|
||||||
@@ -24,6 +33,7 @@ pub enum RenderError {
|
|||||||
pub enum RenderInfo {
|
pub enum RenderInfo {
|
||||||
NumPages(usize),
|
NumPages(usize),
|
||||||
Page(PageInfo),
|
Page(PageInfo),
|
||||||
|
SearchResults { page_num: usize, num_results: usize },
|
||||||
Reloaded
|
Reloaded
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,15 +47,19 @@ pub struct PageInfo {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ImageData {
|
pub struct ImageData {
|
||||||
pub pixels: Vec<u8>,
|
pub pixels: Vec<u8>,
|
||||||
pub cell_area: Rect
|
pub cell_w: u16,
|
||||||
|
pub cell_h: u16
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct PrevRender {
|
struct PrevRender {
|
||||||
successful: bool,
|
successful: bool,
|
||||||
contained_term: Option<bool>
|
num_search_found: Option<usize>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const MUPDF_BLACK: i32 = 0;
|
||||||
|
pub const MUPDF_WHITE: i32 = i32::from_be_bytes([0, 0xff, 0xff, 0xff]);
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn fill_default<T: Default>(vec: &mut Vec<T>, size: usize) {
|
pub fn fill_default<T: Default>(vec: &mut Vec<T>, size: usize) {
|
||||||
vec.clear();
|
vec.clear();
|
||||||
@@ -64,25 +78,30 @@ pub fn fill_default<T: Default>(vec: &mut Vec<T>, size: usize) {
|
|||||||
// We're allowing passing by value here because this is only called once, at the beginning of the
|
// 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
|
// 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.
|
// probably be more performant if accessed by-value instead of through a reference. Probably.
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
#[expect(clippy::needless_pass_by_value, clippy::too_many_arguments)]
|
||||||
pub fn start_rendering(
|
pub fn start_rendering(
|
||||||
path: &str,
|
path: &str,
|
||||||
sender: Sender<Result<RenderInfo, RenderError>>,
|
sender: Sender<Result<RenderInfo, RenderError>>,
|
||||||
receiver: Receiver<RenderNotif>,
|
receiver: Receiver<RenderNotif>,
|
||||||
size: WindowSize
|
col_h: u16,
|
||||||
|
col_w: u16,
|
||||||
|
prerender: PrerenderLimit,
|
||||||
|
black: i32,
|
||||||
|
white: i32
|
||||||
) -> Result<(), SendError<Result<RenderInfo, RenderError>>> {
|
) -> Result<(), SendError<Result<RenderInfo, RenderError>>> {
|
||||||
// We want this outside of 'reload so that if the doc reloads, the search term that somebody
|
// We want this outside of 'reload so that if the doc reloads, the search term that somebody
|
||||||
// set will still get highlighted in the reloaded doc
|
// set will still get highlighted in the reloaded doc
|
||||||
let mut search_term = None;
|
let mut search_term = None;
|
||||||
|
|
||||||
// And although the font size could theoretically change, we aren't accounting for that right
|
// And although the font size could theoretically change, we aren't accounting for that right
|
||||||
// now, so we just keep this out of the loop.
|
// now, so we just use the values passed in.
|
||||||
let col_w = size.width / size.columns;
|
|
||||||
let col_h = size.height / size.rows;
|
|
||||||
|
|
||||||
let mut stored_doc = None;
|
let mut stored_doc = None;
|
||||||
let mut invert = false;
|
let mut invert = false;
|
||||||
let mut preserved_area = None;
|
let mut preserved_area = None;
|
||||||
|
let mut fit_or_fill = FitOrFill::Fit;
|
||||||
|
|
||||||
|
let mut need_rerender = VecDeque::new();
|
||||||
|
|
||||||
'reload: loop {
|
'reload: loop {
|
||||||
let doc = match Document::open(path) {
|
let doc = match Document::open(path) {
|
||||||
@@ -97,7 +116,7 @@ pub fn start_rendering(
|
|||||||
// temporarily removed to facilitate a save or something like that)
|
// temporarily removed to facilitate a save or something like that)
|
||||||
while let Ok(msg) = receiver.recv() {
|
while let Ok(msg) = receiver.recv() {
|
||||||
// and once that comes, just try to reload again
|
// and once that comes, just try to reload again
|
||||||
if let RenderNotif::Reload = msg {
|
if matches!(msg, RenderNotif::Reload) {
|
||||||
continue 'reload;
|
continue 'reload;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,7 +135,13 @@ pub fn start_rendering(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let n_pages = match doc.page_count() {
|
let n_pages = match doc.page_count() {
|
||||||
Ok(n) => n as usize,
|
Ok(n) => match NonZeroUsize::new(n as usize) {
|
||||||
|
Some(n) => n,
|
||||||
|
None => {
|
||||||
|
sleep(Duration::from_secs(1));
|
||||||
|
continue 'reload;
|
||||||
|
}
|
||||||
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
sender.send(Err(RenderError::Doc(e)))?;
|
sender.send(Err(RenderError::Doc(e)))?;
|
||||||
// just basic backoff i think
|
// just basic backoff i think
|
||||||
@@ -125,15 +150,15 @@ pub fn start_rendering(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
sender.send(Ok(RenderInfo::NumPages(n_pages)))?;
|
sender.send(Ok(RenderInfo::NumPages(n_pages.get())))?;
|
||||||
|
|
||||||
// We're using this vec of bools to indicate which page numbers have already been rendered,
|
// We're using this vec to indicate which page numbers have already been rendered, to
|
||||||
// to support people jumping to specific pages and having quick rendering results. We
|
// support people jumping to specific pages and having quick rendering results. We
|
||||||
// `split_at_mut` at 0 initially (which bascially makes `right == rendered && left == []`),
|
// `split_at_mut` at 0 initially (which bascially makes `right == rendered && left == []`),
|
||||||
// doing basically nothing, but if we get a notification that something has been jumped to,
|
// doing basically nothing, but if we get a notification that something has been jumped to,
|
||||||
// then we can split at that page and render at both sides of it
|
// then we can split at that page and render at both sides of it
|
||||||
let mut rendered = Vec::new();
|
let mut rendered = Vec::new();
|
||||||
fill_default::<PrevRender>(&mut rendered, n_pages);
|
fill_default::<PrevRender>(&mut rendered, n_pages.get());
|
||||||
let mut start_point = 0;
|
let mut start_point = 0;
|
||||||
|
|
||||||
// This is kinda a weird way of doing this, but if we get a notification that the area
|
// This is kinda a weird way of doing this, but if we get a notification that the area
|
||||||
@@ -143,9 +168,7 @@ pub fn start_rendering(
|
|||||||
'render_pages: loop {
|
'render_pages: loop {
|
||||||
// next, we gotta wait 'til we get told what the current starting area is so that we can
|
// next, we gotta wait 'til we get told what the current starting area is so that we can
|
||||||
// set it to know what to render to
|
// set it to know what to render to
|
||||||
let area = match preserved_area {
|
let area = preserved_area.unwrap_or_else(|| {
|
||||||
Some(a) => a,
|
|
||||||
None => {
|
|
||||||
let new_area = loop {
|
let new_area = loop {
|
||||||
if let RenderNotif::Area(r) = receiver.recv().unwrap() {
|
if let RenderNotif::Area(r) = receiver.recv().unwrap() {
|
||||||
break r;
|
break r;
|
||||||
@@ -153,13 +176,15 @@ pub fn start_rendering(
|
|||||||
};
|
};
|
||||||
preserved_area = Some(new_area);
|
preserved_area = Some(new_area);
|
||||||
new_area
|
new_area
|
||||||
}
|
});
|
||||||
};
|
|
||||||
|
let area_w = f32::from(area.width) * f32::from(col_w);
|
||||||
|
let area_h = f32::from(area.height) * f32::from(col_h);
|
||||||
|
|
||||||
// what we do with a notif is the same regardless of if we're in the middle of
|
// what we do with a notif is the same regardless of if we're in the middle of
|
||||||
// rendering the list of pages or we're all done
|
// rendering the list of pages or we're all done
|
||||||
macro_rules! handle_notif {
|
macro_rules! handle_notif {
|
||||||
($notif:ident) => {
|
($notif:ident) => {{
|
||||||
match $notif {
|
match $notif {
|
||||||
RenderNotif::Reload => continue 'reload,
|
RenderNotif::Reload => continue 'reload,
|
||||||
RenderNotif::Invert => {
|
RenderNotif::Invert => {
|
||||||
@@ -171,20 +196,32 @@ pub fn start_rendering(
|
|||||||
}
|
}
|
||||||
RenderNotif::Area(new_area) => {
|
RenderNotif::Area(new_area) => {
|
||||||
preserved_area = Some(new_area);
|
preserved_area = Some(new_area);
|
||||||
fill_default(&mut rendered, n_pages);
|
fill_default(&mut rendered, n_pages.get());
|
||||||
continue 'render_pages;
|
continue 'render_pages;
|
||||||
}
|
}
|
||||||
|
RenderNotif::SwitchFitOrFill(f_or_f) =>
|
||||||
|
if f_or_f != fit_or_fill {
|
||||||
|
fit_or_fill = f_or_f;
|
||||||
|
fill_default(&mut rendered, n_pages.get());
|
||||||
|
continue 'render_pages;
|
||||||
|
},
|
||||||
RenderNotif::JumpToPage(page) => {
|
RenderNotif::JumpToPage(page) => {
|
||||||
start_point = page;
|
start_point = page;
|
||||||
continue 'render_pages;
|
continue 'render_pages;
|
||||||
}
|
}
|
||||||
|
RenderNotif::PageNeedsReRender(page) => {
|
||||||
|
rendered[page].successful = false;
|
||||||
|
need_rerender.push_back(page);
|
||||||
|
continue 'render_pages;
|
||||||
|
}
|
||||||
RenderNotif::Search(term) => {
|
RenderNotif::Search(term) => {
|
||||||
if term.is_empty() {
|
if term.is_empty() {
|
||||||
// If the term is set to nothing, then we don't need to re-render
|
// If the term is set to nothing, then we don't need to re-render
|
||||||
// the pages wherein there were already no search results. So this
|
// the pages wherein there were already no search results. So this
|
||||||
// is a little optimization to allow that.
|
// is a little optimization to allow that.
|
||||||
for page in &mut rendered {
|
for page in &mut rendered {
|
||||||
if !page.successful || page.contained_term != Some(true) {
|
if page.num_search_found.is_some_and(|n| n > 0) {
|
||||||
|
page.num_search_found = Some(0);
|
||||||
page.successful = false;
|
page.successful = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,43 +232,108 @@ pub fn start_rendering(
|
|||||||
// term, we can render them with the term, but if they don't, we
|
// term, we can render them with the term, but if they don't, we
|
||||||
// don't need to re-render and send it over again.
|
// don't need to re-render and send it over again.
|
||||||
for page in &mut rendered {
|
for page in &mut rendered {
|
||||||
page.contained_term = None;
|
page.num_search_found = None;
|
||||||
}
|
}
|
||||||
search_term = Some(term);
|
search_term = Some(term);
|
||||||
}
|
}
|
||||||
continue 'render_pages;
|
continue 'render_pages;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
let (left, right) = rendered.split_at_mut(start_point);
|
let any_not_searched = rendered.iter().any(|r| r.num_search_found.is_none());
|
||||||
|
|
||||||
let page_iter = right
|
// This is our iterator over all the pages we want to look at and render. It uses this
|
||||||
.iter_mut()
|
// weird 'interleave' thing to render pages on *both sides* of the currently-displayed
|
||||||
.enumerate()
|
// page in case they device to go forward or backwards.
|
||||||
.map(|(idx, p)| (idx + start_point, p))
|
let page_iter = PopOnNext {
|
||||||
.interleave(
|
inner: &mut need_rerender
|
||||||
left.iter_mut()
|
}
|
||||||
.rev()
|
.chain(InterleavedAroundWithMax::new(start_point, 0, n_pages).take(
|
||||||
.enumerate()
|
match (&prerender, &search_term) {
|
||||||
.map(|(idx, p)| (start_point - (idx + 1), p))
|
// If the user has limited the amount of pages they want to prerender, then we
|
||||||
);
|
// just do what they ask. Nice and easy.
|
||||||
|
(PrerenderLimit::Limited(l), _) => l.get(),
|
||||||
let area_w = f32::from(area.width) * f32::from(col_w);
|
// If they haven't limited it, but we don't have any search term that we're
|
||||||
let area_h = f32::from(area.height) * f32::from(col_h);
|
// currently looking for, just go for all of it
|
||||||
|
(PrerenderLimit::All, None) => n_pages.get(),
|
||||||
|
// If they haven't limited it, and we DO have a search term we need to look
|
||||||
|
// for, just do 20 so that we don't dramatically slow down the search process
|
||||||
|
// since they've specifically initiated that and so we want it to take priority
|
||||||
|
(PrerenderLimit::All, Some(_)) =>
|
||||||
|
if any_not_searched {
|
||||||
|
20
|
||||||
|
} else {
|
||||||
|
n_pages.get()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
// we go through each page
|
// we go through each page
|
||||||
for (num, rendered) in page_iter {
|
for page_num in page_iter {
|
||||||
|
let rendered = &mut rendered[page_num];
|
||||||
|
|
||||||
// we only want to continue if one of the following is met:
|
// we only want to continue if one of the following is met:
|
||||||
// 1. It failed to render last time (we want to retry)
|
// 1. It failed to render last time (we want to retry)
|
||||||
// 2. The `contained_term` is set to None (representing 'Unknown'), meaning that we
|
// 2. The `contained_term` is set to Unknown, meaning that we need to at least
|
||||||
// need to at least check if it contains the current term to see if it needs a
|
// check if it contains the current term to see if it needs a re-render
|
||||||
// re-render
|
if rendered.successful && rendered.num_search_found.is_some() {
|
||||||
if rendered.successful && rendered.contained_term.is_some() {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We know this is in range 'cause we're iterating over it but we still just want
|
||||||
|
// to be safe
|
||||||
|
let page = match doc.load_page(page_num as i32) {
|
||||||
|
Err(e) => {
|
||||||
|
sender.send(Err(RenderError::Doc(e)))?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Ok(p) => p
|
||||||
|
};
|
||||||
|
|
||||||
|
// render the page
|
||||||
|
match render_single_page_to_ctx(
|
||||||
|
&page,
|
||||||
|
search_term.as_deref(),
|
||||||
|
rendered,
|
||||||
|
invert,
|
||||||
|
black,
|
||||||
|
white,
|
||||||
|
fit_or_fill,
|
||||||
|
(area_w, area_h)
|
||||||
|
) {
|
||||||
|
// If that fn returned Some, that means it needed to be re-rendered for some
|
||||||
|
// reason or another, so we're sending it here
|
||||||
|
Ok(ctx) => {
|
||||||
|
let w = ctx.pixmap.width();
|
||||||
|
let h = ctx.pixmap.height();
|
||||||
|
let cap = (w * h * u32::from(ctx.pixmap.n())) as usize + 16;
|
||||||
|
let mut pixels = Vec::with_capacity(cap);
|
||||||
|
if let Err(e) = ctx.pixmap.write_to(&mut pixels, mupdf::ImageFormat::PNM) {
|
||||||
|
sender.send(Err(RenderError::Doc(e)))?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
log::debug!("got pixmap for page {page_num} with WxH {w}x{h}");
|
||||||
|
|
||||||
|
rendered.num_search_found = Some(ctx.result_rects.len());
|
||||||
|
rendered.successful = true;
|
||||||
|
|
||||||
|
sender.send(Ok(RenderInfo::Page(PageInfo {
|
||||||
|
img_data: ImageData {
|
||||||
|
pixels,
|
||||||
|
cell_w: (ctx.surface_w / f32::from(col_w)) as u16,
|
||||||
|
cell_h: (ctx.surface_h / f32::from(col_h)) as u16
|
||||||
|
},
|
||||||
|
page_num,
|
||||||
|
result_rects: ctx.result_rects
|
||||||
|
})))?;
|
||||||
|
}
|
||||||
|
// And if we got an error, then obviously we need to propagate that
|
||||||
|
Err(e) => sender.send(Err(RenderError::Doc(e)))?
|
||||||
|
}
|
||||||
|
|
||||||
// check if we've been told to change the area that we're rendering to,
|
// check if we've been told to change the area that we're rendering to,
|
||||||
// or if we're told to rerender
|
// or if we're told to rerender
|
||||||
match receiver.try_recv() {
|
match receiver.try_recv() {
|
||||||
@@ -239,81 +341,102 @@ pub fn start_rendering(
|
|||||||
Err(TryRecvError::Disconnected) => return Ok(()),
|
Err(TryRecvError::Disconnected) => return Ok(()),
|
||||||
Ok(notif) => handle_notif!(notif),
|
Ok(notif) => handle_notif!(notif),
|
||||||
Err(TryRecvError::Empty) => ()
|
Err(TryRecvError::Empty) => ()
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// We know this is in range 'cause we're iterating over it but we still just want
|
// Now, if we have a search term, we want to look through the rest of the document past
|
||||||
// to be safe
|
// what we've just rendered (and looked at the search results of)
|
||||||
let page = match doc.load_page(num as i32) {
|
if let Some(ref term) = search_term {
|
||||||
Err(e) => {
|
let mut search_start = start_point;
|
||||||
sender.send(Err(RenderError::Doc(e)))?;
|
loop {
|
||||||
|
// hmm maybe this would be nice to make configurable but whatever
|
||||||
|
const SEARCH_AT_TIME: usize = 20;
|
||||||
|
|
||||||
|
// So now we want to look through all the remaining pages, starting after this
|
||||||
|
// current one (we don't do interleaving here 'cause I'm lazy
|
||||||
|
let page_idx = rendered[search_start..]
|
||||||
|
.iter_mut()
|
||||||
|
.enumerate()
|
||||||
|
// And we only want to take max SEARCH_AT_TIME of them since we don't want
|
||||||
|
// to block on this for *too* long
|
||||||
|
.take(SEARCH_AT_TIME)
|
||||||
|
// And we only want the ones that we still don't know about...
|
||||||
|
.filter(|(_, r)| r.num_search_found.is_none())
|
||||||
|
// And then adjust the index to be correct for the actual page number
|
||||||
|
.map(|(idx, r)| (idx + search_start, r));
|
||||||
|
|
||||||
|
// then we go through each...
|
||||||
|
for (page_num, rendered) in page_idx {
|
||||||
|
// We get the number of results (using the function that specifically just
|
||||||
|
// counts them instead of determining the quads of them all)
|
||||||
|
let num_results = doc
|
||||||
|
.load_page(page_num as i32)
|
||||||
|
.and_then(|page| count_search_results(&page, term))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// And mark that whatever else was rendered last is not relevant anymore if
|
||||||
|
// there are results that need to be rendered
|
||||||
|
if num_results > 0 {
|
||||||
|
rendered.successful = false;
|
||||||
|
}
|
||||||
|
// Mark the `contained_term` field with this updated value...
|
||||||
|
rendered.num_search_found = Some(num_results);
|
||||||
|
|
||||||
|
// And send it over to the tui so that they can know and use it to
|
||||||
|
// determine what next page to jump to
|
||||||
|
sender.send(Ok(RenderInfo::SearchResults {
|
||||||
|
page_num,
|
||||||
|
num_results
|
||||||
|
}))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// then once we're done with this iteration, we increment search_start to
|
||||||
|
// prepare for the next iteration
|
||||||
|
search_start += SEARCH_AT_TIME;
|
||||||
|
|
||||||
|
// now, we want to check if we've gone past the end - if so, we go back to the
|
||||||
|
// beginning so we can get the pages before the current one.
|
||||||
|
if search_start > n_pages.get() {
|
||||||
|
if start_point == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
search_start = 0;
|
||||||
|
} else if ((search_start - SEARCH_AT_TIME) + 1..search_start)
|
||||||
|
.contains(&start_point)
|
||||||
|
{
|
||||||
|
// And if we are back at the place we started, we've looked through all the
|
||||||
|
// pages. Quit.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
match receiver.try_recv() {
|
||||||
|
// If there are no messages left for us, just continue in this loop
|
||||||
|
Err(TryRecvError::Empty) => (),
|
||||||
|
Err(TryRecvError::Disconnected) => return Ok(()),
|
||||||
|
Ok(msg) => handle_notif!(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// So now we've just *searched* all the pages but not necessarily rendered all of them.
|
||||||
|
// So if there are any we have yet to render, we need to loop back to the beginning of
|
||||||
|
// this loop to continue rendering all of them
|
||||||
|
if rendered.iter().any(|r| !r.successful) && prerender == PrerenderLimit::All {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Ok(p) => p
|
|
||||||
};
|
|
||||||
|
|
||||||
let rendered_with_no_results =
|
|
||||||
rendered.successful && rendered.contained_term == Some(false);
|
|
||||||
|
|
||||||
// render the page
|
|
||||||
match render_single_page_to_ctx(
|
|
||||||
&page,
|
|
||||||
search_term.as_deref(),
|
|
||||||
rendered_with_no_results,
|
|
||||||
invert,
|
|
||||||
(area_w, area_h)
|
|
||||||
) {
|
|
||||||
// If we've already rendered it just fine and we don't need to render it again,
|
|
||||||
// just continue. We're all good
|
|
||||||
Ok(None) => (),
|
|
||||||
// If that fn returned Some, that means it needed to be re-rendered for some
|
|
||||||
// reason or another, so we're sending it here
|
|
||||||
Ok(Some(ctx)) => {
|
|
||||||
// we make a potentially incorrect assumption here that writing the context
|
|
||||||
// to a png won't fail, and mark that it all rendered correctly here before
|
|
||||||
// spawning off the thread to do so and send it.
|
|
||||||
rendered.contained_term = Some(ctx.result_rects.is_empty());
|
|
||||||
rendered.successful = true;
|
|
||||||
|
|
||||||
let w = ctx.pixmap.width();
|
|
||||||
let h = ctx.pixmap.height();
|
|
||||||
let cap = (w * h * u32::from(ctx.pixmap.n())) as usize + 16;
|
|
||||||
let mut pixels = Vec::with_capacity(cap);
|
|
||||||
if let Err(e) = ctx.pixmap.write_to(&mut pixels, mupdf::ImageFormat::PNM) {
|
|
||||||
sender.send(Err(RenderError::Doc(e)))?;
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
sender.send(Ok(RenderInfo::Page(PageInfo {
|
|
||||||
img_data: ImageData {
|
|
||||||
pixels,
|
|
||||||
cell_area: Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: (ctx.surface_w / f32::from(col_w)) as u16,
|
|
||||||
height: (ctx.surface_h / f32::from(col_h)) as u16
|
|
||||||
}
|
|
||||||
},
|
|
||||||
page_num: num,
|
|
||||||
result_rects: ctx.result_rects
|
|
||||||
})))?;
|
|
||||||
}
|
|
||||||
// And if we got an error, then obviously we need to propagate that
|
|
||||||
Err(e) => sender.send(Err(RenderError::Doc(e)))?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then once we've rendered all these pages, wait until we get another notification
|
// Then once we've rendered all these pages, wait until we get another notification
|
||||||
// that this doc needs to be reloaded
|
// that this doc needs to be reloaded
|
||||||
loop {
|
|
||||||
// This once returned None despite the main thing being still connected (I think, at
|
// This once returned None despite the main thing being still connected (I think, at
|
||||||
// least), so I'm just being safe here
|
// least), so I'm just being safe here
|
||||||
let Ok(msg) = receiver.recv() else {
|
let Ok(msg) = receiver.recv() else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
handle_notif!(msg);
|
handle_notif!(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RenderedContext {
|
struct RenderedContext {
|
||||||
@@ -323,72 +446,49 @@ struct RenderedContext {
|
|||||||
result_rects: Vec<HighlightRect>
|
result_rects: Vec<HighlightRect>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::too_many_arguments)]
|
||||||
fn render_single_page_to_ctx(
|
fn render_single_page_to_ctx(
|
||||||
page: &Page,
|
page: &Page,
|
||||||
search_term: Option<&str>,
|
search_term: Option<&str>,
|
||||||
already_rendered_no_results: bool,
|
prev_render: &PrevRender,
|
||||||
invert: bool,
|
invert: bool,
|
||||||
|
black: i32,
|
||||||
|
white: i32,
|
||||||
|
fit_or_fill: FitOrFill,
|
||||||
(area_w, area_h): (f32, f32)
|
(area_w, area_h): (f32, f32)
|
||||||
) -> Result<Option<RenderedContext>, mupdf::error::Error> {
|
) -> Result<RenderedContext, mupdf::error::Error> {
|
||||||
let mut max_hits = 10;
|
let result_rects = match prev_render.num_search_found {
|
||||||
let result_rects = loop {
|
None => search_page(page, search_term, 0)?,
|
||||||
let rects = search_term
|
Some(0) => Vec::new(),
|
||||||
.as_ref()
|
Some(count @ 1..) => search_page(page, search_term, count)?
|
||||||
// mupdf allocates a buffer of the size we give it to try to fill it with results. If we
|
|
||||||
// pass in u32::MAX, it allocates too much memory to function. If we pass too small of a
|
|
||||||
// number in, we may miss out on some of the results. Ideally, we'd like to make a better
|
|
||||||
// interface than this, but we're stuck with this kinda ugly looping until we make sure
|
|
||||||
// that we've found every instance of it on this page.
|
|
||||||
.map(|term| page.search(term, max_hits))
|
|
||||||
.transpose()?
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
if rects.len() < (max_hits as usize) {
|
|
||||||
break rects;
|
|
||||||
}
|
|
||||||
|
|
||||||
max_hits *= 2;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// If there are no search terms on this page, and we've already rendered it with no search
|
|
||||||
// terms, then just return none to avoid this computation
|
|
||||||
if result_rects.is_empty() && already_rendered_no_results {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
// then, get the size of the page
|
// then, get the size of the page
|
||||||
let bounds = page.bounds()?;
|
let bounds = page.bounds()?;
|
||||||
let (p_width, p_height) = (bounds.x1 - bounds.x0, bounds.y1 - bounds.y0);
|
let page_dim = (bounds.x1 - bounds.x0, bounds.y1 - bounds.y0);
|
||||||
|
|
||||||
// and get its aspect ratio
|
let scaled = scale_img_for_area(page_dim, (area_w, area_h), fit_or_fill);
|
||||||
let p_aspect_ratio = p_width / p_height;
|
let ScaledResult {
|
||||||
|
width: mut surface_w,
|
||||||
|
height: mut surface_h,
|
||||||
|
mut scale_factor
|
||||||
|
} = scaled;
|
||||||
|
|
||||||
// Then we get the full pixel dimensions of the area provided to us, and the aspect ratio
|
if surface_w > KITTY_MAX_W_OR_H || surface_h > KITTY_MAX_W_OR_H {
|
||||||
// of that area
|
let descale = (surface_w / KITTY_MAX_W_OR_H).max(surface_h / KITTY_MAX_W_OR_H);
|
||||||
let area_aspect_ratio = area_w / area_h;
|
surface_w /= descale;
|
||||||
|
surface_h /= descale;
|
||||||
// and get the ratio that this page would have to be scaled by to fit perfectly within the
|
scale_factor /= descale;
|
||||||
// area provided to us.
|
}
|
||||||
// we do this first by comparing the aspec ratio of the page with the aspect ratio of the
|
|
||||||
// area to fit it within. If the aspect ratio of the page is larger, then we need to scale
|
|
||||||
// the width of the page to fill perfectly within the height of the area. Otherwise, we
|
|
||||||
// scale the height to fit perfectly. The dimension that _is not_ scaled to fit perfectly
|
|
||||||
// is scaled by the same factor as the dimension that _is_ scaled perfectly.
|
|
||||||
let scale_factor = if p_aspect_ratio > area_aspect_ratio {
|
|
||||||
area_w / p_width
|
|
||||||
} else {
|
|
||||||
area_h / p_height
|
|
||||||
};
|
|
||||||
|
|
||||||
let surface_w = p_width * scale_factor;
|
|
||||||
let surface_h = p_height * scale_factor;
|
|
||||||
|
|
||||||
let colorspace = Colorspace::device_rgb();
|
let colorspace = Colorspace::device_rgb();
|
||||||
let matrix = Matrix::new_scale(scale_factor, scale_factor);
|
let matrix = Matrix::new_scale(scale_factor, scale_factor);
|
||||||
|
|
||||||
let mut pixmap = page.to_pixmap(&matrix, &colorspace, 0.0, false)?;
|
let mut pixmap = page.to_pixmap(&matrix, &colorspace, false, false)?;
|
||||||
if invert {
|
if invert {
|
||||||
pixmap.invert()?;
|
pixmap.tint(white, black)?;
|
||||||
|
} else if black != MUPDF_BLACK || white != MUPDF_WHITE {
|
||||||
|
pixmap.tint(black, white)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (x_res, y_res) = pixmap.resolution();
|
let (x_res, y_res) = pixmap.resolution();
|
||||||
@@ -412,18 +512,73 @@ fn render_single_page_to_ctx(
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
Ok(Some(RenderedContext {
|
Ok(RenderedContext {
|
||||||
pixmap,
|
pixmap,
|
||||||
surface_w,
|
surface_w,
|
||||||
surface_h,
|
surface_h,
|
||||||
result_rects
|
result_rects
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct HighlightRect {
|
pub struct HighlightRect {
|
||||||
pub ul_x: u32,
|
pub ul_x: u32,
|
||||||
pub ul_y: u32,
|
pub ul_y: u32,
|
||||||
pub lr_x: u32,
|
pub lr_x: u32,
|
||||||
pub lr_y: u32
|
pub lr_y: u32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn search_page(
|
||||||
|
page: &Page,
|
||||||
|
search_term: Option<&str>,
|
||||||
|
trusted_search_results: usize
|
||||||
|
) -> Result<Vec<Quad>, mupdf::error::Error> {
|
||||||
|
search_term
|
||||||
|
.map(|term| {
|
||||||
|
page.to_text_page(TextPageFlags::empty()).and_then(|page| {
|
||||||
|
let mut v = Vec::with_capacity(trusted_search_results);
|
||||||
|
page.search_cb(term, &mut v, |v, results| {
|
||||||
|
v.extend(results.iter().cloned());
|
||||||
|
SearchHitResponse::ContinueSearch
|
||||||
|
})
|
||||||
|
.map(|_| v)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.transpose()
|
||||||
|
.map(Option::unwrap_or_default)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn count_search_results(page: &Page, search_term: &str) -> Result<usize, mupdf::error::Error> {
|
||||||
|
page.to_text_page(TextPageFlags::empty()).and_then(|page| {
|
||||||
|
let mut count = 0;
|
||||||
|
page.search_cb(search_term, &mut count, |count, results| {
|
||||||
|
*count += results.len();
|
||||||
|
SearchHitResponse::ContinueSearch
|
||||||
|
})?;
|
||||||
|
Ok(count)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PopOnNext<'a> {
|
||||||
|
inner: &'a mut VecDeque<usize>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for PopOnNext<'_> {
|
||||||
|
type Item = usize;
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
self.inner.pop_front()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||||
|
let l = self.len();
|
||||||
|
(l, Some(l))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExactSizeIterator for PopOnNext<'_> {
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
self.inner.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+108
@@ -1,3 +1,5 @@
|
|||||||
|
use std::num::NonZeroUsize;
|
||||||
|
|
||||||
use ratatui::widgets::Widget;
|
use ratatui::widgets::Widget;
|
||||||
|
|
||||||
pub struct Skip {
|
pub struct Skip {
|
||||||
@@ -5,6 +7,7 @@ pub struct Skip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Skip {
|
impl Skip {
|
||||||
|
#[must_use]
|
||||||
pub fn new(skip: bool) -> Self {
|
pub fn new(skip: bool) -> Self {
|
||||||
Self { skip }
|
Self { skip }
|
||||||
}
|
}
|
||||||
@@ -19,3 +22,108 @@ impl Widget for Skip {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum PlusOrMinus {
|
||||||
|
Plus,
|
||||||
|
Minus
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct InterleavedAroundWithMax {
|
||||||
|
// starts at this number
|
||||||
|
around: usize,
|
||||||
|
inclusive_min: usize,
|
||||||
|
// this iterator can only produce values in [0..max)
|
||||||
|
exclusive_max: NonZeroUsize,
|
||||||
|
// the next time we call `next()`, this value should be combined with `around` according to
|
||||||
|
// `next_op`, then, after next_op is inverted, incremented if next_op was negative before being
|
||||||
|
// inverted.
|
||||||
|
next_change: usize,
|
||||||
|
// How `next_change` should be applied to `around` next time `next()` is called
|
||||||
|
next_op: PlusOrMinus
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InterleavedAroundWithMax {
|
||||||
|
/// the following must hold or else this is liable to panic or produce nonsense values:
|
||||||
|
/// - inclusive_min < exclusive_max
|
||||||
|
/// - inclusive_min <= around <= exclusive_max
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(around: usize, inclusive_min: usize, exclusive_max: NonZeroUsize) -> Self {
|
||||||
|
Self {
|
||||||
|
around,
|
||||||
|
inclusive_min,
|
||||||
|
exclusive_max,
|
||||||
|
next_change: 0,
|
||||||
|
next_op: PlusOrMinus::Minus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for InterleavedAroundWithMax {
|
||||||
|
type Item = usize;
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let actual_change = self.next_change % (self.exclusive_max.get() - self.inclusive_min);
|
||||||
|
|
||||||
|
let to_return = match self.next_op {
|
||||||
|
// If we're supposed to add them and we need it to wrap, then try to add them together
|
||||||
|
// 'cause we need special behavior if it overflows usize's limits
|
||||||
|
PlusOrMinus::Plus => match self.around.checked_add(actual_change) {
|
||||||
|
// If we added it and it's within the range, we're chillin
|
||||||
|
Some(next_val) if next_val < self.exclusive_max.get() => next_val,
|
||||||
|
// If we added it and it's not within the range, do next_val % (self.max + 1), e.g.
|
||||||
|
// if max is 20, we were at 15, and we added 7, we should get 1 (because +5 would
|
||||||
|
// hit the max, then 0, then 1). So adding 1 before the modulo makes it hit the
|
||||||
|
// right numbers. And we can be sure the + here doesn't overflow 'cause we already
|
||||||
|
// checked the `usize::MAX` up above
|
||||||
|
Some(next_val) => (next_val % self.exclusive_max.get()) + self.inclusive_min,
|
||||||
|
// If we added them and it would've overflowed usize::MAX, then we see how much
|
||||||
|
// of the change would be remaining after reaching `max`
|
||||||
|
None =>
|
||||||
|
(actual_change - (self.exclusive_max.get() - actual_change))
|
||||||
|
+ self.inclusive_min,
|
||||||
|
},
|
||||||
|
PlusOrMinus::Minus => match self.around.checked_sub(actual_change) {
|
||||||
|
// If we can just minus it, cool cool. All is good.
|
||||||
|
Some(next_val) if next_val >= self.inclusive_min => next_val,
|
||||||
|
// If we can minus it but it goes below our min, then see how much below it went
|
||||||
|
// and just manually wrap it around
|
||||||
|
Some(next_val) => self.exclusive_max.get() - (self.inclusive_min - next_val),
|
||||||
|
// If we can't...
|
||||||
|
None => {
|
||||||
|
// then we see how much of the change would be remaining after hitting the
|
||||||
|
// minimum
|
||||||
|
let remaining = actual_change - (self.around - self.inclusive_min);
|
||||||
|
|
||||||
|
// and then we take that away from the top!
|
||||||
|
self.exclusive_max.get() - remaining
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.next_op = match self.next_op {
|
||||||
|
PlusOrMinus::Plus => PlusOrMinus::Minus,
|
||||||
|
PlusOrMinus::Minus => {
|
||||||
|
self.next_change = (self.next_change + 1) % self.exclusive_max.get();
|
||||||
|
PlusOrMinus::Plus
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(to_return)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn iter_works() {
|
||||||
|
let got = InterleavedAroundWithMax::new(5, 2, NonZeroUsize::new(21).unwrap())
|
||||||
|
.take(30)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(got, vec![
|
||||||
|
5, 6, 4, 7, 3, 8, 2, 9, 20, 10, 19, 11, 18, 12, 17, 13, 16, 14, 15, 15, 14, 16, 13, 17,
|
||||||
|
12, 18, 11, 19, 10, 20
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+582
-184
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user