Compare commits

..

10 Commits

Author SHA1 Message Date
itsjunetime 70b458207a Add more details to changelog and release 0.3.0 2025-03-01 19:00:18 -07:00
itsjunetime 1eee193d44 Implement fullscreen functionality 2025-03-01 18:27:34 -07:00
itsjunetime d2be289e80 Implement help page and delay receiving area to hopefully improve first-page performance a bit 2025-03-01 18:04:33 -07:00
itsjunetime 10e1f6cb9f Update deps 2025-02-28 10:29:23 -07:00
Mikołaj Pieczaba 7e4bee516b refactor: make fill_default use Vec::resize_with (#53) 2025-02-23 14:35:29 -07:00
itsjunetime aae1f9d37b Improve performance by better pre-allocating pixmap buffer 2025-02-21 20:56:25 -07:00
itsjunetime 70f3401702 Update to edition 2024 2025-02-21 08:58:44 -07:00
itsjunetime 9d2a730e40 Implement inverting colors 2025-02-19 15:24:27 -07:00
Andrew Chu 8c10a3c4bc Enable png feature for image crate (#51)
* Enable png feature for image crate

* Fix typo
2025-02-19 12:09:37 -07:00
June 524c069b83 Rewrite with mupdf as a backend (#50)
* Initial implementation of attempted mupdf rewrite

* Change back to no resizing and don't include alpha channel in conversion

* Remove some more dead code

* Make features more modular and call search more easily

* Switch to git dependency for my fixes

* Update deps

* Fix searching hehe

* Remove unnecessary CI steps?

* fontconfig in CI

* perftools in ci

* Final adjustments to conform to mupdf changes
2025-02-19 09:59:29 -07:00
9 changed files with 361 additions and 205 deletions
+4 -1
View File
@@ -1,8 +1,11 @@
# Unreleased # v0.3.0
- Update ratatui(-image) dependencies - Update ratatui(-image) dependencies
- Enable Ctrl+Z/Suspend functionality - Enable Ctrl+Z/Suspend functionality
- Rewrite with mupdf as the backend for much better performance and rendering quality - Rewrite with mupdf as the backend for much better performance and rendering quality
- Support easy inversion of colors via `i` keypress
- Support for filling all available space with `f` keypress
- Change help text at bottom into full help page
# v0.2.0 # v0.2.0
Generated
+71 -41
View File
@@ -52,9 +52,9 @@ checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.95" version = "1.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4"
[[package]] [[package]]
name = "arbitrary" name = "arbitrary"
@@ -149,9 +149,9 @@ dependencies = [
[[package]] [[package]]
name = "avif-serialize" name = "avif-serialize"
version = "0.8.2" version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e335041290c43101ca215eed6f43ec437eb5a42125573f600fc3fa42b9bddd62" checksum = "98922d6a4cfbcb08820c69d8eeccc05bb1f29bfa06b4f5b1dbfe9a868bd7608e"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
] ]
@@ -349,9 +349,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.14" version = "1.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9" checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c"
dependencies = [ dependencies = [
"jobserver", "jobserver",
"libc", "libc",
@@ -429,18 +429,18 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.30" version = "4.5.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d" checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
] ]
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.30" version = "4.5.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c" checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"clap_lex", "clap_lex",
@@ -834,9 +834,9 @@ dependencies = [
[[package]] [[package]]
name = "either" name = "either"
version = "1.13.0" version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" checksum = "b7914353092ddf589ad78f25c5c1c21b7f80b0ff8621e7c814c3485b5306da9d"
[[package]] [[package]]
name = "equivalent" name = "equivalent"
@@ -889,6 +889,15 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "fdeflate"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
dependencies = [
"simd-adler32",
]
[[package]] [[package]]
name = "filedescriptor" name = "filedescriptor"
version = "0.8.3" version = "0.8.3"
@@ -926,9 +935,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.35" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"miniz_oxide", "miniz_oxide",
@@ -1344,6 +1353,7 @@ dependencies = [
"bytemuck", "bytemuck",
"byteorder-lite", "byteorder-lite",
"num-traits", "num-traits",
"png",
"ravif", "ravif",
"rayon", "rayon",
"zune-core", "zune-core",
@@ -1532,9 +1542,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.169" version = "0.2.170"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
[[package]] [[package]]
name = "libfuzzer-sys" name = "libfuzzer-sys"
@@ -1604,9 +1614,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.25" version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
[[package]] [[package]]
name = "loop9" name = "loop9"
@@ -1714,11 +1724,12 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.4" version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
dependencies = [ dependencies = [
"adler2", "adler2",
"simd-adler32",
] ]
[[package]] [[package]]
@@ -1736,26 +1747,26 @@ dependencies = [
[[package]] [[package]]
name = "mupdf" name = "mupdf"
version = "0.4.4" version = "0.4.4"
source = "git+https://github.com/itsjunetime/mupdf-rs?branch=remove_debug_print#10f1b1629540e7d62354842198317bcf5e7d619c" source = "git+https://github.com/itsjunetime/mupdf-rs?branch=june%2Fmupdf_1_25#9c4f2379d205a78f967bb230f0e72ec18fad23f7"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.8.0",
"font-kit", "font-kit",
"mupdf-sys", "mupdf-sys",
"num_enum", "num_enum",
"once_cell", "once_cell",
"zerocopy 0.8.19", "zerocopy 0.8.21",
] ]
[[package]] [[package]]
name = "mupdf-sys" name = "mupdf-sys"
version = "0.4.4" version = "0.4.4"
source = "git+https://github.com/itsjunetime/mupdf-rs?branch=remove_debug_print#10f1b1629540e7d62354842198317bcf5e7d619c" source = "git+https://github.com/itsjunetime/mupdf-rs?branch=june%2Fmupdf_1_25#9c4f2379d205a78f967bb230f0e72ec18fad23f7"
dependencies = [ dependencies = [
"bindgen", "bindgen",
"cc", "cc",
"pkg-config", "pkg-config",
"regex", "regex",
"zerocopy 0.8.19", "zerocopy 0.8.21",
] ]
[[package]] [[package]]
@@ -2181,6 +2192,19 @@ dependencies = [
"plotters-backend", "plotters-backend",
] ]
[[package]]
name = "png"
version = "0.17.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
dependencies = [
"bitflags 1.3.2",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
]
[[package]] [[package]]
name = "powerfmt" name = "powerfmt"
version = "0.2.0" version = "0.2.0"
@@ -2468,9 +2492,9 @@ dependencies = [
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.8" version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.8.0",
] ]
@@ -2623,18 +2647,18 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.217" version = "1.0.218"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.217" version = "1.0.218"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -2643,9 +2667,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.138" version = "1.0.139"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@@ -2718,6 +2742,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "simd-adler32"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]] [[package]]
name = "simd_helpers" name = "simd_helpers"
version = "0.1.0" version = "0.1.0"
@@ -3302,9 +3332,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.13.2" version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c1f41ffb7cf259f1ecc2876861a17e7142e63ead296f671f81f6ae85903e0d6" checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587"
dependencies = [ dependencies = [
"atomic", "atomic",
"getrandom 0.3.1", "getrandom 0.3.1",
@@ -3771,9 +3801,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.7.2" version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59690dea168f2198d1a3b0cac23b8063efcd11012f10ae4698f284808c8ef603" checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@@ -3834,11 +3864,11 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.19" version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8207f485579465f62ae51a983e42c906736a17efd2de48b021e64f1bbd8e98c7" checksum = "dcf01143b2dd5d134f11f545cf9f1431b13b749695cb33bcce051e7568f99478"
dependencies = [ dependencies = [
"zerocopy-derive 0.8.19", "zerocopy-derive 0.8.21",
] ]
[[package]] [[package]]
@@ -3854,9 +3884,9 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.19" version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dbe1304a711c6eb4cf1ed333aa0d9b344685e71f6f00c3b176072213bd3783e" checksum = "712c8386f4f4299382c9abee219bee7084f78fb939d88b6840fcc1320d5f6da2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
+4 -3
View File
@@ -2,7 +2,7 @@
name = "tdf-viewer" name = "tdf-viewer"
version = "0.2.0" version = "0.2.0"
authors = ["June Welker <junewelker@gmail.com>"] authors = ["June Welker <junewelker@gmail.com>"]
edition = "2021" edition = "2024"
description = "A terminal viewer for PDFs" description = "A terminal viewer for PDFs"
readme = "README.md" readme = "README.md"
homepage = "https://github.com/itsjunetime/tdf" homepage = "https://github.com/itsjunetime/tdf"
@@ -11,6 +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"
[[bin]] [[bin]]
name = "tdf" name = "tdf"
@@ -28,7 +29,7 @@ ratatui = { git = "https://github.com/itsjunetime/ratatui.git" }
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", features = ["vb64"], default-features = false }
crossterm = { version = "0.28.1", features = ["event-stream"] } crossterm = { version = "0.28.1", features = ["event-stream"] }
image = { version = "0.25.1", features = ["pnm", "rayon"], 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 }
@@ -37,7 +38,7 @@ 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.29.0", features = ["signal"] }
mupdf = { git = "https://github.com/itsjunetime/mupdf-rs", branch = "remove_debug_print", default-features = false, features = ["svg", "system-fonts", "img"] } mupdf = { git = "https://github.com/itsjunetime/mupdf-rs", branch = "june/mupdf_1_25", default-features = false, features = ["svg", "system-fonts", "img"] }
rayon = { version = "*", default-features = false } rayon = { version = "*", default-features = false }
# for tracing with tokio-console # for tracing with tokio-console
+4 -4
View File
@@ -6,15 +6,15 @@ use std::{
time::{SystemTime, UNIX_EPOCH} time::{SystemTime, UNIX_EPOCH}
}; };
use criterion::{criterion_group, criterion_main, profiler::Profiler, BenchmarkId, Criterion}; use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main, profiler::Profiler};
use futures_util::StreamExt; use futures_util::StreamExt;
use tdf::{ use tdf::{
converter::{ConvertedPage, ConverterMsg}, converter::{ConvertedPage, ConverterMsg},
renderer::{fill_default, PageInfo, RenderInfo} renderer::{PageInfo, RenderInfo, fill_default}
}; };
use utils::{ use utils::{
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, RenderState start_converting_loop, start_rendering_loop
}; };
const FILES: [&str; 3] = [ const FILES: [&str; 3] = [
+7 -7
View File
@@ -1,13 +1,13 @@
use std::{hint::black_box, path::Path}; use std::{hint::black_box, path::Path};
use crossterm::terminal::WindowSize; use crossterm::terminal::WindowSize;
use flume::{r#async::RecvStream, unbounded, Sender}; use flume::{Sender, r#async::RecvStream, unbounded};
use futures_util::stream::StreamExt as _; use futures_util::stream::StreamExt as _;
use ratatui::layout::Rect; use ratatui::layout::Rect;
use ratatui_image::picker::{Picker, ProtocolType}; use ratatui_image::picker::{Picker, ProtocolType};
use tdf::{ use tdf::{
converter::{run_conversion_loop, ConvertedPage, ConverterMsg}, converter::{ConvertedPage, ConverterMsg, run_conversion_loop},
renderer::{fill_default, start_rendering, RenderError, RenderInfo, RenderNotif} renderer::{RenderError, RenderInfo, RenderNotif, fill_default, start_rendering}
}; };
pub fn handle_renderer_msg( pub fn handle_renderer_msg(
@@ -37,13 +37,13 @@ pub fn handle_converter_msg(
pages[num] = Some(page); pages[num] = Some(page);
let num_got = pages.iter().filter(|p| p.is_some()).count(); let first_none = pages.iter().position(Option::is_none);
// we have to tell it to jump to a certain page so that it will actually render it (since // we have to tell it to jump to a certain page so that it will actually render it (since
// it only renders fanning out from the page that we currently have selected) // it only renders fanning out from the page that we currently have selected)
to_converter_tx if let Some(first) = first_none {
.send(ConverterMsg::GoToPage(num_got)) to_converter_tx.send(ConverterMsg::GoToPage(first)).unwrap();
.unwrap(); }
} }
pub struct RenderState { pub struct RenderState {
+3 -3
View File
@@ -2,10 +2,10 @@ use flume::{Receiver, SendError, Sender, TryRecvError};
use futures_util::stream::StreamExt; use futures_util::stream::StreamExt;
use image::DynamicImage; use image::DynamicImage;
use itertools::Itertools; use itertools::Itertools;
use ratatui_image::{picker::Picker, protocol::Protocol, Resize}; use ratatui_image::{Resize, picker::Picker, protocol::Protocol};
use rayon::iter::ParallelIterator; use rayon::iter::ParallelIterator;
use crate::renderer::{fill_default, PageInfo, RenderError}; use crate::renderer::{PageInfo, RenderError, fill_default};
pub struct ConvertedPage { pub struct ConvertedPage {
pub page: Protocol, pub page: Protocol,
@@ -49,7 +49,7 @@ pub async fn run_conversion_loop(
let Some((page_info, new_iter)) = (idx_start..page) let Some((page_info, new_iter)) = (idx_start..page)
.interleave(page..idx_end) .interleave(page..idx_end)
.enumerate() .enumerate()
.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)))
else { else {
return Ok(None); return Ok(None);
+15 -10
View File
@@ -1,6 +1,6 @@
use std::{ use std::{
ffi::OsString, ffi::OsString,
io::{stdout, Read, Write}, io::{Read, Write, stdout},
num::NonZeroUsize, num::NonZeroUsize,
path::PathBuf path::PathBuf
}; };
@@ -8,16 +8,16 @@ use std::{
use crossterm::{ use crossterm::{
execute, execute,
terminal::{ terminal::{
disable_raw_mode, enable_raw_mode, window_size, EndSynchronizedUpdate, EndSynchronizedUpdate, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode,
EnterAlternateScreen, LeaveAlternateScreen enable_raw_mode, window_size
} }
}; };
use futures_util::{stream::StreamExt, FutureExt}; use futures_util::{FutureExt, stream::StreamExt};
use notify::{Event, EventKind, RecursiveMode, Watcher}; use notify::{Event, EventKind, RecursiveMode, Watcher};
use ratatui::{backend::CrosstermBackend, Terminal}; use ratatui::{Terminal, backend::CrosstermBackend};
use ratatui_image::picker::Picker; use ratatui_image::picker::Picker;
use tdf::{ use tdf::{
converter::{run_conversion_loop, ConvertedPage, ConverterMsg}, converter::{ConvertedPage, ConverterMsg, run_conversion_loop},
renderer::{self, RenderError, RenderInfo, RenderNotif}, renderer::{self, RenderError, RenderInfo, RenderNotif},
tui::{BottomMessage, InputAction, MessageSetting, Tui} tui::{BottomMessage, InputAction, MessageSetting, Tui}
}; };
@@ -45,6 +45,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
optional -r,--r-to-l r_to_l: bool optional -r,--r-to-l r_to_l: bool
/// 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)
optional -f,--fullscreen fullscreen: bool
/// PDF file to read /// PDF file to read
required file: PathBuf required file: PathBuf
}; };
@@ -167,8 +169,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
)?; )?;
enable_raw_mode()?; enable_raw_mode()?;
let mut main_area = Tui::main_layout(&term.get_frame()); let mut fullscreen = flags.fullscreen.unwrap_or_default();
tui_tx.send(RenderNotif::Area(main_area[1]))?; let mut main_area = Tui::main_layout(&term.get_frame(), fullscreen);
tui_tx.send(RenderNotif::Area(main_area.page_area))?;
let mut tui_rx = tui_rx.into_stream(); let mut tui_rx = tui_rx.into_stream();
let mut from_converter = from_converter.into_stream(); let mut from_converter = from_converter.into_stream();
@@ -191,6 +194,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
to_converter.send(ConverterMsg::GoToPage(page))?; to_converter.send(ConverterMsg::GoToPage(page))?;
}, },
InputAction::Search(term) => tui_tx.send(RenderNotif::Search(term))?, InputAction::Search(term) => tui_tx.send(RenderNotif::Search(term))?,
InputAction::Invert => tui_tx.send(RenderNotif::Invert)?,
InputAction::Fullscreen => fullscreen = !fullscreen,
} }
} }
}, },
@@ -218,10 +223,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}, },
}; };
let new_area = Tui::main_layout(&term.get_frame()); 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[1]))?; tui_tx.send(RenderNotif::Area(main_area.page_area))?;
needs_redraw = true; needs_redraw = true;
} }
+40 -28
View File
@@ -10,7 +10,8 @@ pub enum RenderNotif {
Area(Rect), Area(Rect),
JumpToPage(usize), JumpToPage(usize),
Search(String), Search(String),
Reload Reload,
Invert
} }
#[derive(Debug)] #[derive(Debug)]
@@ -45,12 +46,10 @@ struct PrevRender {
contained_term: Option<bool> contained_term: Option<bool>
} }
#[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();
vec.reserve(size.saturating_sub(vec.len())); vec.resize_with(size, T::default);
for _ in 0..size {
vec.push(T::default());
}
} }
// this function has to be sync (non-async) because the mupdf::Document needs to be held during // this function has to be sync (non-async) because the mupdf::Document needs to be held during
@@ -72,14 +71,6 @@ pub fn start_rendering(
receiver: Receiver<RenderNotif>, receiver: Receiver<RenderNotif>,
size: WindowSize size: WindowSize
) -> Result<(), SendError<Result<RenderInfo, RenderError>>> { ) -> Result<(), SendError<Result<RenderInfo, RenderError>>> {
// first, wait 'til we get told what the current starting area is so that we can set it to
// know what to render to
let mut area = loop {
if let RenderNotif::Area(r) = receiver.recv().unwrap() {
break r;
}
};
// 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;
@@ -90,6 +81,8 @@ pub fn start_rendering(
let col_h = size.height / size.rows; let col_h = size.height / size.rows;
let mut stored_doc = None; let mut stored_doc = None;
let mut invert = false;
let mut preserved_area = None;
'reload: loop { 'reload: loop {
let doc = match Document::open(path) { let doc = match Document::open(path) {
@@ -139,7 +132,7 @@ pub fn start_rendering(
// `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![]; let mut rendered = Vec::new();
fill_default::<PrevRender>(&mut rendered, n_pages); fill_default::<PrevRender>(&mut rendered, n_pages);
let mut start_point = 0; let mut start_point = 0;
@@ -148,24 +141,38 @@ pub fn start_rendering(
// document. If there was a mechanism to say 'start this for-loop over' then I would do // document. If there was a mechanism to say 'start this for-loop over' then I would do
// that, but I don't think such a thing exists, so this is our attempt // that, but I don't think such a thing exists, so this is our attempt
'render_pages: loop { 'render_pages: loop {
// 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
let area = match preserved_area {
Some(a) => a,
None => {
let new_area = loop {
if let RenderNotif::Area(r) = receiver.recv().unwrap() {
break r;
}
};
preserved_area = Some(new_area);
new_area
}
};
// 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::Area(new_area) => { RenderNotif::Invert => {
let bigger = invert = !invert;
new_area.width > area.width || new_area.height > area.height; for page in &mut rendered {
area = new_area; page.successful = false;
// we only want to re-render pages if the new area is greater than the old
// one, 'cause then we might need sharper images to make it all look good.
// If the new area is smaller, then the same high-quality-rendered images
// will still look fine, so it's ok to leave it.
if bigger {
fill_default(&mut rendered, n_pages);
continue 'render_pages;
} }
continue 'render_pages;
}
RenderNotif::Area(new_area) => {
preserved_area = Some(new_area);
fill_default(&mut rendered, n_pages);
continue 'render_pages;
} }
RenderNotif::JumpToPage(page) => { RenderNotif::JumpToPage(page) => {
start_point = page; start_point = page;
@@ -252,6 +259,7 @@ pub fn start_rendering(
&page, &page,
search_term.as_deref(), search_term.as_deref(),
rendered_with_no_results, rendered_with_no_results,
invert,
(area_w, area_h) (area_w, area_h)
) { ) {
// If we've already rendered it just fine and we don't need to render it again, // If we've already rendered it just fine and we don't need to render it again,
@@ -266,9 +274,9 @@ pub fn start_rendering(
rendered.contained_term = Some(ctx.result_rects.is_empty()); rendered.contained_term = Some(ctx.result_rects.is_empty());
rendered.successful = true; rendered.successful = true;
let cap = (ctx.pixmap.width() let w = ctx.pixmap.width();
* ctx.pixmap.height() * u32::from(ctx.pixmap.n())) let h = ctx.pixmap.height();
as usize; let cap = (w * h * u32::from(ctx.pixmap.n())) as usize + 16;
let mut pixels = Vec::with_capacity(cap); let mut pixels = Vec::with_capacity(cap);
if let Err(e) = ctx.pixmap.write_to(&mut pixels, mupdf::ImageFormat::PNM) { if let Err(e) = ctx.pixmap.write_to(&mut pixels, mupdf::ImageFormat::PNM) {
sender.send(Err(RenderError::Doc(e)))?; sender.send(Err(RenderError::Doc(e)))?;
@@ -319,6 +327,7 @@ fn render_single_page_to_ctx(
page: &Page, page: &Page,
search_term: Option<&str>, search_term: Option<&str>,
already_rendered_no_results: bool, already_rendered_no_results: bool,
invert: bool,
(area_w, area_h): (f32, f32) (area_w, area_h): (f32, f32)
) -> Result<Option<RenderedContext>, mupdf::error::Error> { ) -> Result<Option<RenderedContext>, mupdf::error::Error> {
let mut max_hits = 10; let mut max_hits = 10;
@@ -378,6 +387,9 @@ fn render_single_page_to_ctx(
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, 0.0, false)?;
if invert {
pixmap.invert()?;
}
let (x_res, y_res) = pixmap.resolution(); let (x_res, y_res) = pixmap.resolution();
let new_x = (x_res as f32 * scale_factor) as i32; let new_x = (x_res as f32 * scale_factor) as i32;
+213 -108
View File
@@ -1,25 +1,26 @@
use std::{borrow::Cow, io::stdout, num::NonZeroUsize, rc::Rc}; use std::{borrow::Cow, io::stdout, num::NonZeroUsize};
use crossterm::{ use crossterm::{
event::{Event, KeyCode, KeyModifiers, MouseEventKind}, event::{Event, KeyCode, KeyModifiers, MouseEventKind},
execute, execute,
terminal::{ terminal::{
disable_raw_mode, enable_raw_mode, BeginSynchronizedUpdate, EnterAlternateScreen, BeginSynchronizedUpdate, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode,
LeaveAlternateScreen enable_raw_mode
} }
}; };
use nix::{ use nix::{
sys::signal::{kill, Signal::SIGSTOP}, sys::signal::{Signal::SIGSTOP, kill},
unistd::Pid unistd::Pid
}; };
use ratatui::{ use ratatui::{
Frame,
layout::{Constraint, Flex, Layout, Rect}, layout::{Constraint, Flex, Layout, Rect},
style::{Color, Style}, style::{Color, Style},
text::Span, symbols::border,
widgets::{Block, Borders, Padding}, text::{Span, Text},
Frame widgets::{Block, Borders, Clear, Padding}
}; };
use ratatui_image::{protocol::Protocol, Image}; use ratatui_image::{Image, protocol::Protocol};
use crate::{renderer::RenderError, skip::Skip}; use crate::{renderer::RenderError, skip::Skip};
@@ -32,7 +33,8 @@ pub struct Tui {
// jumping to a specific page // jumping to a specific page
prev_msg: Option<BottomMessage>, prev_msg: Option<BottomMessage>,
rendered: Vec<RenderedInfo>, rendered: Vec<RenderedInfo>,
page_constraints: PageConstraints page_constraints: PageConstraints,
showing_help_msg: bool
} }
#[derive(Default, Debug)] #[derive(Default, Debug)]
@@ -77,6 +79,12 @@ struct RenderedInfo {
num_results: Option<usize> num_results: Option<usize>
} }
#[derive(PartialEq)]
pub struct RenderLayout {
pub page_area: Rect,
pub top_and_bottom: Option<(Rect, Rect)>
}
impl Tui { impl Tui {
pub fn new(name: String, max_wide: Option<NonZeroUsize>, r_to_l: bool) -> Tui { pub fn new(name: String, max_wide: Option<NonZeroUsize>, r_to_l: bool) -> Tui {
Self { Self {
@@ -86,121 +94,138 @@ impl Tui {
bottom_msg: BottomMessage::Help, bottom_msg: BottomMessage::Help,
last_render: LastRender::default(), last_render: LastRender::default(),
rendered: vec![], rendered: vec![],
page_constraints: PageConstraints { max_wide, r_to_l } page_constraints: PageConstraints { max_wide, r_to_l },
showing_help_msg: false
} }
} }
pub fn main_layout(frame: &Frame<'_>) -> Rc<[Rect]> { pub fn main_layout(frame: &Frame<'_>, fullscreened: bool) -> RenderLayout {
Layout::default() if fullscreened {
.constraints([ RenderLayout {
Constraint::Length(3), page_area: frame.area(),
Constraint::Fill(1), top_and_bottom: None
Constraint::Length(3) }
]) } else {
.horizontal_margin(2) let layout = Layout::default()
.vertical_margin(1) .constraints([
.split(frame.area()) Constraint::Length(3),
Constraint::Fill(1),
Constraint::Length(3)
])
.horizontal_margin(2)
.vertical_margin(1)
.split(frame.area());
RenderLayout {
page_area: layout[1],
top_and_bottom: Some((layout[0], layout[2]))
}
}
} }
// TODO: Make a way to fill the width of the screen with one page and scroll down to view it // TODO: Make a way to fill the width of the screen with one page and scroll down to view it
pub fn render(&mut self, frame: &mut Frame<'_>, main_area: &[Rect]) { pub fn render(&mut self, frame: &mut Frame<'_>, full_layout: &RenderLayout) {
let top_block = Block::new() if self.showing_help_msg {
.padding(Padding { self.render_help_msg(frame);
right: 2, return;
left: 2, }
..Padding::default()
})
.borders(Borders::BOTTOM);
let top_area = top_block.inner(main_area[0]); if let Some((top_area, bottom_area)) = full_layout.top_and_bottom {
let top_block = Block::new()
.padding(Padding {
right: 2,
left: 2,
..Padding::default()
})
.borders(Borders::BOTTOM);
let page_nums_text = format!("{} / {}", self.page + 1, self.rendered.len()); let top_area = top_block.inner(top_area);
let top_layout = Layout::horizontal([ let page_nums_text = format!("{} / {}", self.page + 1, self.rendered.len());
Constraint::Fill(1),
Constraint::Length(page_nums_text.len() as u16)
])
.split(top_area);
let title = Span::styled(&self.name, Style::new().fg(Color::Cyan)); let top_layout = Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(page_nums_text.len() as u16)
])
.split(top_area);
let page_nums = Span::styled(&page_nums_text, Style::new().fg(Color::Cyan)); let title = Span::styled(&self.name, Style::new().fg(Color::Cyan));
frame.render_widget(top_block, main_area[0]); let page_nums = Span::styled(&page_nums_text, Style::new().fg(Color::Cyan));
frame.render_widget(title, top_layout[0]);
frame.render_widget(page_nums, top_layout[1]);
let bottom_block = Block::new() frame.render_widget(top_block, top_area);
.padding(Padding { frame.render_widget(title, top_layout[0]);
top: 1, frame.render_widget(page_nums, top_layout[1]);
right: 2,
left: 2,
bottom: 0
})
.borders(Borders::TOP);
let bottom_area = bottom_block.inner(main_area[2]);
frame.render_widget(bottom_block, main_area[2]); let bottom_block = Block::new()
.padding(Padding {
top: 1,
right: 2,
left: 2,
bottom: 0
})
.borders(Borders::TOP);
let bottom_inside_block = bottom_block.inner(bottom_area);
let rendered_str = if !self.rendered.is_empty() { frame.render_widget(bottom_block, bottom_area);
format!(
"Rendered: {}%",
(self.rendered.iter().filter(|i| i.img.is_some()).count() * 100)
/ self.rendered.len()
)
} else {
String::new()
};
let bottom_layout = Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(rendered_str.len() as u16)
])
.split(bottom_area);
let rendered_span = Span::styled(&rendered_str, Style::new().fg(Color::Cyan)); let rendered_str = if !self.rendered.is_empty() {
frame.render_widget(rendered_span, bottom_layout[1]); format!(
"Rendered: {}%",
(self.rendered.iter().filter(|i| i.img.is_some()).count() * 100)
/ self.rendered.len()
)
} else {
String::new()
};
let bottom_layout = Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(rendered_str.len() as u16)
])
.split(bottom_inside_block);
let (msg_str, color): (Cow<'_, str>, _) = match self.bottom_msg { let rendered_span = Span::styled(&rendered_str, Style::new().fg(Color::Cyan));
BottomMessage::Help => ( frame.render_widget(rendered_span, bottom_layout[1]);
"/: Search, g: Go To Page, n: Next Search Result, N: Previous Search Result".into(),
Color::Blue let (msg_str, color): (Cow<'_, str>, _) = match self.bottom_msg {
), BottomMessage::Help => ("?: Show help page".into(), Color::Blue),
BottomMessage::Error(ref e) => (e.as_str().into(), Color::Red), BottomMessage::Error(ref e) => (e.as_str().into(), Color::Red),
BottomMessage::Input(ref input_state) => ( BottomMessage::Input(ref input_state) => (
match input_state { match input_state {
InputCommand::GoToPage(page) => format!("Go to: {page}"), InputCommand::GoToPage(page) => format!("Go to: {page}"),
InputCommand::Search(s) => format!("Search: {s}") InputCommand::Search(s) => format!("Search: {s}")
} }
.into(),
Color::Blue
),
BottomMessage::SearchResults(ref term) => {
let num_found = self
.rendered
.iter()
.filter_map(|r| r.num_results)
.sum::<usize>();
let num_searched = self
.rendered
.iter()
.filter(|r| r.num_results.is_some())
.count() * 100;
(
format!(
"Results for '{term}': {num_found} (searched: {}%)",
num_searched / self.rendered.len()
)
.into(), .into(),
Color::Blue Color::Blue
) ),
} BottomMessage::SearchResults(ref term) => {
BottomMessage::Reloaded => ("Document was reloaded!".into(), Color::Blue) let num_found = self
}; .rendered
.iter()
.filter_map(|r| r.num_results)
.sum::<usize>();
let num_searched = self
.rendered
.iter()
.filter(|r| r.num_results.is_some())
.count() * 100;
(
format!(
"Results for '{term}': {num_found} (searched: {}%)",
num_searched / self.rendered.len()
)
.into(),
Color::Blue
)
}
BottomMessage::Reloaded => ("Document was reloaded!".into(), Color::Blue)
};
let span = Span::styled(msg_str, Style::new().fg(color)); let span = Span::styled(msg_str, Style::new().fg(color));
frame.render_widget(span, bottom_layout[0]); frame.render_widget(span, bottom_layout[0]);
}
let mut img_area = main_area[1]; let mut img_area = full_layout.page_area;
let size = frame.area(); let size = frame.area();
if size == self.last_render.rect { if size == self.last_render.rect {
@@ -414,6 +439,12 @@ impl Tui {
))); )));
Some(InputAction::Redraw) Some(InputAction::Redraw)
} }
'i' => Some(InputAction::Invert),
'?' => {
self.showing_help_msg = true;
Some(InputAction::Redraw)
}
'f' => Some(InputAction::Fullscreen),
'n' if self.page < self.rendered.len() - 1 => { 'n' if self.page < self.rendered.len() - 1 => {
// TODO: If we can't find one, then maybe like block until we've verified // TODO: If we can't find one, then maybe like block until we've verified
// all the pages have been checked? // all the pages have been checked?
@@ -485,8 +516,8 @@ impl Tui {
KeyCode::Down => self.change_page(PageChange::Next, ChangeAmount::WholeScreen), KeyCode::Down => self.change_page(PageChange::Next, ChangeAmount::WholeScreen),
KeyCode::Left => self.change_page(PageChange::Prev, ChangeAmount::Single), KeyCode::Left => self.change_page(PageChange::Prev, ChangeAmount::Single),
KeyCode::Up => self.change_page(PageChange::Prev, ChangeAmount::WholeScreen), KeyCode::Up => self.change_page(PageChange::Prev, ChangeAmount::WholeScreen),
KeyCode::Esc => match self.bottom_msg { KeyCode::Esc => match (self.showing_help_msg, &self.bottom_msg) {
BottomMessage::Help => Some(InputAction::QuitApp), (false, BottomMessage::Help) => Some(InputAction::QuitApp),
_ => { _ => {
// When we hit escape, we just want to pop off the current message and // When we hit escape, we just want to pop off the current message and
// show the underlying one. // show the underlying one.
@@ -515,7 +546,9 @@ impl Tui {
Some(InputAction::JumpingToPage(zero_page)) Some(InputAction::JumpingToPage(zero_page))
} else { } else {
self.set_msg(MessageSetting::Some(BottomMessage::Error( self.set_msg(MessageSetting::Some(BottomMessage::Error(
format!("Cannot jump to page {page}; there are only {rendered_len} pages in the document") format!(
"Cannot jump to page {page}; there are only {rendered_len} pages in the document"
)
))); )));
Some(InputAction::Redraw) Some(InputAction::Redraw)
} }
@@ -595,16 +628,88 @@ impl Tui {
self.prev_msg = None; self.prev_msg = None;
self.bottom_msg = BottomMessage::default(); self.bottom_msg = BottomMessage::default();
} }
MessageSetting::Pop => self.bottom_msg = self.prev_msg.take().unwrap_or_default() MessageSetting::Pop =>
if self.showing_help_msg {
self.last_render.rect = Rect::default();
self.showing_help_msg = false;
} else {
self.bottom_msg = self.prev_msg.take().unwrap_or_default();
},
} }
} }
pub fn render_help_msg(&self, frame: &mut Frame<'_>) {
let frame_area = frame.area();
frame.render_widget(Clear, frame_area);
let block = Block::new()
.title("Help")
.padding(Padding::proportional(1))
.borders(Borders::ALL)
.border_set(border::ROUNDED)
.border_style(Color::Blue);
let help_span = Text::raw(HELP_PAGE);
let max_w: u16 = HELP_PAGE
.lines()
.map(str::len)
.max()
.unwrap_or_default()
.try_into()
.expect("Every help text line must be shorter than u16::MAX");
let layout = Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(max_w + 6),
Constraint::Fill(1)
])
.split(frame_area);
let block_area = Layout::vertical([
Constraint::Fill(1),
Constraint::Length(u16::try_from(HELP_PAGE.lines().count()).unwrap() + 4),
Constraint::Fill(1)
])
.split(layout[1]);
let block_inner = block.inner(block_area[1]);
frame.render_widget(block, block_area[1]);
frame.render_widget(help_span, block_inner);
}
} }
static HELP_PAGE: &str = "\
l, h, left, right:
Go forward/backwards a single page
j, k, down, up:
Go forwards/backwards a screen's worth of pages
q, esc:
Quit
g:
Go to specific page (type numbers after 'g')
/:
Search
n, N:
Next/Previous search result
i:
Invert colors
f:
Remove borders/fullscreen
?:
Show this page
ctrl+z:
Suspend & background tdf \
";
pub enum InputAction { pub enum InputAction {
Redraw, Redraw,
JumpingToPage(usize), JumpingToPage(usize),
Search(String), Search(String),
QuitApp QuitApp,
Invert,
Fullscreen
} }
#[derive(Copy, Clone)] #[derive(Copy, Clone)]