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
- Enable Ctrl+Z/Suspend functionality
- 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
Generated
+71 -41
View File
@@ -52,9 +52,9 @@ checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anyhow"
version = "1.0.95"
version = "1.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4"
[[package]]
name = "arbitrary"
@@ -149,9 +149,9 @@ dependencies = [
[[package]]
name = "avif-serialize"
version = "0.8.2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e335041290c43101ca215eed6f43ec437eb5a42125573f600fc3fa42b9bddd62"
checksum = "98922d6a4cfbcb08820c69d8eeccc05bb1f29bfa06b4f5b1dbfe9a868bd7608e"
dependencies = [
"arrayvec",
]
@@ -349,9 +349,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.14"
version = "1.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9"
checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c"
dependencies = [
"jobserver",
"libc",
@@ -429,18 +429,18 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.30"
version = "4.5.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d"
checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.30"
version = "4.5.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c"
checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863"
dependencies = [
"anstyle",
"clap_lex",
@@ -834,9 +834,9 @@ dependencies = [
[[package]]
name = "either"
version = "1.13.0"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
checksum = "b7914353092ddf589ad78f25c5c1c21b7f80b0ff8621e7c814c3485b5306da9d"
[[package]]
name = "equivalent"
@@ -889,6 +889,15 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "filedescriptor"
version = "0.8.3"
@@ -926,9 +935,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flate2"
version = "1.0.35"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c"
checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
dependencies = [
"crc32fast",
"miniz_oxide",
@@ -1344,6 +1353,7 @@ dependencies = [
"bytemuck",
"byteorder-lite",
"num-traits",
"png",
"ravif",
"rayon",
"zune-core",
@@ -1532,9 +1542,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.169"
version = "0.2.170"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
[[package]]
name = "libfuzzer-sys"
@@ -1604,9 +1614,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.25"
version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
[[package]]
name = "loop9"
@@ -1714,11 +1724,12 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.8.4"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b"
checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]]
@@ -1736,26 +1747,26 @@ dependencies = [
[[package]]
name = "mupdf"
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 = [
"bitflags 2.8.0",
"font-kit",
"mupdf-sys",
"num_enum",
"once_cell",
"zerocopy 0.8.19",
"zerocopy 0.8.21",
]
[[package]]
name = "mupdf-sys"
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 = [
"bindgen",
"cc",
"pkg-config",
"regex",
"zerocopy 0.8.19",
"zerocopy 0.8.21",
]
[[package]]
@@ -2181,6 +2192,19 @@ dependencies = [
"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]]
name = "powerfmt"
version = "0.2.0"
@@ -2468,9 +2492,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.5.8"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f"
dependencies = [
"bitflags 2.8.0",
]
@@ -2623,18 +2647,18 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.217"
version = "1.0.218"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.217"
version = "1.0.218"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
dependencies = [
"proc-macro2",
"quote",
@@ -2643,9 +2667,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.138"
version = "1.0.139"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6"
dependencies = [
"itoa",
"memchr",
@@ -2718,6 +2742,12 @@ dependencies = [
"libc",
]
[[package]]
name = "simd-adler32"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "simd_helpers"
version = "0.1.0"
@@ -3302,9 +3332,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.13.2"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c1f41ffb7cf259f1ecc2876861a17e7142e63ead296f671f81f6ae85903e0d6"
checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587"
dependencies = [
"atomic",
"getrandom 0.3.1",
@@ -3771,9 +3801,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.2"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59690dea168f2198d1a3b0cac23b8063efcd11012f10ae4698f284808c8ef603"
checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1"
dependencies = [
"memchr",
]
@@ -3834,11 +3864,11 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.19"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8207f485579465f62ae51a983e42c906736a17efd2de48b021e64f1bbd8e98c7"
checksum = "dcf01143b2dd5d134f11f545cf9f1431b13b749695cb33bcce051e7568f99478"
dependencies = [
"zerocopy-derive 0.8.19",
"zerocopy-derive 0.8.21",
]
[[package]]
@@ -3854,9 +3884,9 @@ dependencies = [
[[package]]
name = "zerocopy-derive"
version = "0.8.19"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dbe1304a711c6eb4cf1ed333aa0d9b344685e71f6f00c3b176072213bd3783e"
checksum = "712c8386f4f4299382c9abee219bee7084f78fb939d88b6840fcc1320d5f6da2"
dependencies = [
"proc-macro2",
"quote",
+4 -3
View File
@@ -2,7 +2,7 @@
name = "tdf-viewer"
version = "0.2.0"
authors = ["June Welker <junewelker@gmail.com>"]
edition = "2021"
edition = "2024"
description = "A terminal viewer for PDFs"
readme = "README.md"
homepage = "https://github.com/itsjunetime/tdf"
@@ -11,6 +11,7 @@ license = "AGPL-3.0-only"
keywords = ["pdf", "tui", "cli", "terminal"]
categories = ["command-line-utilities", "text-processing", "visualization"]
default-run = "tdf"
rust-version = "1.85"
[[bin]]
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 = { path = "./ratatui-image", features = ["vb64"], default-features = false }
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"] }
tokio = { version = "1.37.0", features = ["rt-multi-thread", "macros"] }
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"
mimalloc = "0.1.43"
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 }
# for tracing with tokio-console
+4 -4
View File
@@ -6,15 +6,15 @@ use std::{
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 tdf::{
converter::{ConvertedPage, ConverterMsg},
renderer::{fill_default, PageInfo, RenderInfo}
renderer::{PageInfo, RenderInfo, fill_default}
};
use utils::{
handle_converter_msg, handle_renderer_msg, render_doc, start_all_rendering,
start_converting_loop, start_rendering_loop, RenderState
RenderState, handle_converter_msg, handle_renderer_msg, render_doc, start_all_rendering,
start_converting_loop, start_rendering_loop
};
const FILES: [&str; 3] = [
+7 -7
View File
@@ -1,13 +1,13 @@
use std::{hint::black_box, path::Path};
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 ratatui::layout::Rect;
use ratatui_image::picker::{Picker, ProtocolType};
use tdf::{
converter::{run_conversion_loop, ConvertedPage, ConverterMsg},
renderer::{fill_default, start_rendering, RenderError, RenderInfo, RenderNotif}
converter::{ConvertedPage, ConverterMsg, run_conversion_loop},
renderer::{RenderError, RenderInfo, RenderNotif, fill_default, start_rendering}
};
pub fn handle_renderer_msg(
@@ -37,13 +37,13 @@ pub fn handle_converter_msg(
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
// it only renders fanning out from the page that we currently have selected)
to_converter_tx
.send(ConverterMsg::GoToPage(num_got))
.unwrap();
if let Some(first) = first_none {
to_converter_tx.send(ConverterMsg::GoToPage(first)).unwrap();
}
}
pub struct RenderState {
+3 -3
View File
@@ -2,10 +2,10 @@ use flume::{Receiver, SendError, Sender, TryRecvError};
use futures_util::stream::StreamExt;
use image::DynamicImage;
use itertools::Itertools;
use ratatui_image::{picker::Picker, protocol::Protocol, Resize};
use ratatui_image::{Resize, picker::Picker, protocol::Protocol};
use rayon::iter::ParallelIterator;
use crate::renderer::{fill_default, PageInfo, RenderError};
use crate::renderer::{PageInfo, RenderError, fill_default};
pub struct ConvertedPage {
pub page: Protocol,
@@ -49,7 +49,7 @@ pub async fn run_conversion_loop(
let Some((page_info, new_iter)) = (idx_start..page)
.interleave(page..idx_end)
.enumerate()
.skip(*iteration)
// .skip(*iteration)
.find_map(|(i_idx, p_idx)| images[p_idx].take().map(|p| (p, i_idx)))
else {
return Ok(None);
+15 -10
View File
@@ -1,6 +1,6 @@
use std::{
ffi::OsString,
io::{stdout, Read, Write},
io::{Read, Write, stdout},
num::NonZeroUsize,
path::PathBuf
};
@@ -8,16 +8,16 @@ use std::{
use crossterm::{
execute,
terminal::{
disable_raw_mode, enable_raw_mode, window_size, EndSynchronizedUpdate,
EnterAlternateScreen, LeaveAlternateScreen
EndSynchronizedUpdate, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode,
enable_raw_mode, window_size
}
};
use futures_util::{stream::StreamExt, FutureExt};
use futures_util::{FutureExt, stream::StreamExt};
use notify::{Event, EventKind, RecursiveMode, Watcher};
use ratatui::{backend::CrosstermBackend, Terminal};
use ratatui::{Terminal, backend::CrosstermBackend};
use ratatui_image::picker::Picker;
use tdf::{
converter::{run_conversion_loop, ConvertedPage, ConverterMsg},
converter::{ConvertedPage, ConverterMsg, run_conversion_loop},
renderer::{self, RenderError, RenderInfo, RenderNotif},
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
/// The maximum number of pages to display together, horizontally, at a time
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
required file: PathBuf
};
@@ -167,8 +169,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
)?;
enable_raw_mode()?;
let mut main_area = Tui::main_layout(&term.get_frame());
tui_tx.send(RenderNotif::Area(main_area[1]))?;
let mut fullscreen = flags.fullscreen.unwrap_or_default();
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 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))?;
},
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 {
main_area = new_area;
tui_tx.send(RenderNotif::Area(main_area[1]))?;
tui_tx.send(RenderNotif::Area(main_area.page_area))?;
needs_redraw = true;
}
+39 -27
View File
@@ -10,7 +10,8 @@ pub enum RenderNotif {
Area(Rect),
JumpToPage(usize),
Search(String),
Reload
Reload,
Invert
}
#[derive(Debug)]
@@ -45,12 +46,10 @@ struct PrevRender {
contained_term: Option<bool>
}
#[inline]
pub fn fill_default<T: Default>(vec: &mut Vec<T>, size: usize) {
vec.clear();
vec.reserve(size.saturating_sub(vec.len()));
for _ in 0..size {
vec.push(T::default());
}
vec.resize_with(size, T::default);
}
// 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>,
size: WindowSize
) -> 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
// set will still get highlighted in the reloaded doc
let mut search_term = None;
@@ -90,6 +81,8 @@ pub fn start_rendering(
let col_h = size.height / size.rows;
let mut stored_doc = None;
let mut invert = false;
let mut preserved_area = None;
'reload: loop {
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 == []`),
// 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
let mut rendered = vec![];
let mut rendered = Vec::new();
fill_default::<PrevRender>(&mut rendered, n_pages);
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
// that, but I don't think such a thing exists, so this is our attempt
'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
// rendering the list of pages or we're all done
macro_rules! handle_notif {
($notif:ident) => {
match $notif {
RenderNotif::Reload => continue 'reload,
RenderNotif::Area(new_area) => {
let bigger =
new_area.width > area.width || new_area.height > area.height;
area = new_area;
// 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);
RenderNotif::Invert => {
invert = !invert;
for page in &mut rendered {
page.successful = false;
}
continue 'render_pages;
}
RenderNotif::Area(new_area) => {
preserved_area = Some(new_area);
fill_default(&mut rendered, n_pages);
continue 'render_pages;
}
RenderNotif::JumpToPage(page) => {
start_point = page;
@@ -252,6 +259,7 @@ pub fn start_rendering(
&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,
@@ -266,9 +274,9 @@ pub fn start_rendering(
rendered.contained_term = Some(ctx.result_rects.is_empty());
rendered.successful = true;
let cap = (ctx.pixmap.width()
* ctx.pixmap.height() * u32::from(ctx.pixmap.n()))
as usize;
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)))?;
@@ -319,6 +327,7 @@ fn render_single_page_to_ctx(
page: &Page,
search_term: Option<&str>,
already_rendered_no_results: bool,
invert: bool,
(area_w, area_h): (f32, f32)
) -> Result<Option<RenderedContext>, mupdf::error::Error> {
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 mut pixmap = page.to_pixmap(&matrix, &colorspace, 0.0, false)?;
if invert {
pixmap.invert()?;
}
let (x_res, y_res) = pixmap.resolution();
let new_x = (x_res as f32 * scale_factor) as i32;
+134 -29
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::{
event::{Event, KeyCode, KeyModifiers, MouseEventKind},
execute,
terminal::{
disable_raw_mode, enable_raw_mode, BeginSynchronizedUpdate, EnterAlternateScreen,
LeaveAlternateScreen
BeginSynchronizedUpdate, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode,
enable_raw_mode
}
};
use nix::{
sys::signal::{kill, Signal::SIGSTOP},
sys::signal::{Signal::SIGSTOP, kill},
unistd::Pid
};
use ratatui::{
Frame,
layout::{Constraint, Flex, Layout, Rect},
style::{Color, Style},
text::Span,
widgets::{Block, Borders, Padding},
Frame
symbols::border,
text::{Span, Text},
widgets::{Block, Borders, Clear, Padding}
};
use ratatui_image::{protocol::Protocol, Image};
use ratatui_image::{Image, protocol::Protocol};
use crate::{renderer::RenderError, skip::Skip};
@@ -32,7 +33,8 @@ pub struct Tui {
// jumping to a specific page
prev_msg: Option<BottomMessage>,
rendered: Vec<RenderedInfo>,
page_constraints: PageConstraints
page_constraints: PageConstraints,
showing_help_msg: bool
}
#[derive(Default, Debug)]
@@ -77,6 +79,12 @@ struct RenderedInfo {
num_results: Option<usize>
}
#[derive(PartialEq)]
pub struct RenderLayout {
pub page_area: Rect,
pub top_and_bottom: Option<(Rect, Rect)>
}
impl Tui {
pub fn new(name: String, max_wide: Option<NonZeroUsize>, r_to_l: bool) -> Tui {
Self {
@@ -86,12 +94,19 @@ impl Tui {
bottom_msg: BottomMessage::Help,
last_render: LastRender::default(),
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]> {
Layout::default()
pub fn main_layout(frame: &Frame<'_>, fullscreened: bool) -> RenderLayout {
if fullscreened {
RenderLayout {
page_area: frame.area(),
top_and_bottom: None
}
} else {
let layout = Layout::default()
.constraints([
Constraint::Length(3),
Constraint::Fill(1),
@@ -99,11 +114,23 @@ impl Tui {
])
.horizontal_margin(2)
.vertical_margin(1)
.split(frame.area())
.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
pub fn render(&mut self, frame: &mut Frame<'_>, main_area: &[Rect]) {
pub fn render(&mut self, frame: &mut Frame<'_>, full_layout: &RenderLayout) {
if self.showing_help_msg {
self.render_help_msg(frame);
return;
}
if let Some((top_area, bottom_area)) = full_layout.top_and_bottom {
let top_block = Block::new()
.padding(Padding {
right: 2,
@@ -112,7 +139,7 @@ impl Tui {
})
.borders(Borders::BOTTOM);
let top_area = top_block.inner(main_area[0]);
let top_area = top_block.inner(top_area);
let page_nums_text = format!("{} / {}", self.page + 1, self.rendered.len());
@@ -126,7 +153,7 @@ impl Tui {
let page_nums = Span::styled(&page_nums_text, Style::new().fg(Color::Cyan));
frame.render_widget(top_block, main_area[0]);
frame.render_widget(top_block, top_area);
frame.render_widget(title, top_layout[0]);
frame.render_widget(page_nums, top_layout[1]);
@@ -138,9 +165,9 @@ impl Tui {
bottom: 0
})
.borders(Borders::TOP);
let bottom_area = bottom_block.inner(main_area[2]);
let bottom_inside_block = bottom_block.inner(bottom_area);
frame.render_widget(bottom_block, main_area[2]);
frame.render_widget(bottom_block, bottom_area);
let rendered_str = if !self.rendered.is_empty() {
format!(
@@ -155,16 +182,13 @@ impl Tui {
Constraint::Fill(1),
Constraint::Length(rendered_str.len() as u16)
])
.split(bottom_area);
.split(bottom_inside_block);
let rendered_span = Span::styled(&rendered_str, Style::new().fg(Color::Cyan));
frame.render_widget(rendered_span, bottom_layout[1]);
let (msg_str, color): (Cow<'_, str>, _) = match self.bottom_msg {
BottomMessage::Help => (
"/: Search, g: Go To Page, n: Next Search Result, N: Previous Search Result".into(),
Color::Blue
),
BottomMessage::Help => ("?: Show help page".into(), Color::Blue),
BottomMessage::Error(ref e) => (e.as_str().into(), Color::Red),
BottomMessage::Input(ref input_state) => (
match input_state {
@@ -199,8 +223,9 @@ impl Tui {
let span = Span::styled(msg_str, Style::new().fg(color));
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();
if size == self.last_render.rect {
@@ -414,6 +439,12 @@ impl Tui {
)));
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 => {
// TODO: If we can't find one, then maybe like block until we've verified
// all the pages have been checked?
@@ -485,8 +516,8 @@ impl Tui {
KeyCode::Down => self.change_page(PageChange::Next, ChangeAmount::WholeScreen),
KeyCode::Left => self.change_page(PageChange::Prev, ChangeAmount::Single),
KeyCode::Up => self.change_page(PageChange::Prev, ChangeAmount::WholeScreen),
KeyCode::Esc => match self.bottom_msg {
BottomMessage::Help => Some(InputAction::QuitApp),
KeyCode::Esc => match (self.showing_help_msg, &self.bottom_msg) {
(false, BottomMessage::Help) => Some(InputAction::QuitApp),
_ => {
// When we hit escape, we just want to pop off the current message and
// show the underlying one.
@@ -515,7 +546,9 @@ impl Tui {
Some(InputAction::JumpingToPage(zero_page))
} else {
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)
}
@@ -595,16 +628,88 @@ impl Tui {
self.prev_msg = None;
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 {
Redraw,
JumpingToPage(usize),
Search(String),
QuitApp
QuitApp,
Invert,
Fullscreen
}
#[derive(Copy, Clone)]