mirror of
https://github.com/itsjunetime/tdf.git
synced 2026-06-01 23:51:46 -04:00
Compare commits
24 Commits
v0.4.0
...
fix_zoom_crop
| Author | SHA1 | Date | |
|---|---|---|---|
| 15b1a99d76 | |||
| 170bdd3869 | |||
| 7551c4dba3 | |||
| e61eb9b846 | |||
| 6b37976357 | |||
| 3628d21c74 | |||
| f4f3b4f539 | |||
| a79c534e97 | |||
| 971393892a | |||
| 440515a3db | |||
| 690489016c | |||
| bd5554db27 | |||
| 45409bacd0 | |||
| 0481c14c4d | |||
| a78ea5a08c | |||
| f7eabc9af2 | |||
| 918c192047 | |||
| 8b03329bba | |||
| 7064be32f2 | |||
| 2a03294557 | |||
| 7c2c6484a6 | |||
| e65472e571 | |||
| 4fd2237b69 | |||
| 69fd8ec7e8 |
@@ -31,12 +31,12 @@ jobs:
|
|||||||
- name: Install clippy and fmt
|
- name: Install clippy and fmt
|
||||||
run: rustup component add clippy rustfmt
|
run: rustup component add clippy rustfmt
|
||||||
- name: Clippy
|
- name: Clippy
|
||||||
run: cargo clippy -- -D warnings
|
run: cargo clippy --locked -- -D warnings
|
||||||
- name: Tests
|
- name: Tests
|
||||||
run: cargo test
|
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,5 +1,25 @@
|
|||||||
# Unreleased
|
# Unreleased
|
||||||
|
|
||||||
|
- 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
|
||||||
|
- Fix cropping issues when zooming out too much while using kitty protocol
|
||||||
|
|
||||||
|
# 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
|
# 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))
|
- 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))
|
||||||
|
|||||||
Generated
+586
-685
File diff suppressed because it is too large
Load Diff
+6
-7
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "tdf-viewer"
|
name = "tdf-viewer"
|
||||||
version = "0.3.0"
|
version = "0.4.3"
|
||||||
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"
|
||||||
@@ -38,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.30.0", features = ["signal"] }
|
nix = { version = "0.30.0", features = ["signal"] }
|
||||||
mupdf = { version = "0.5.0", 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 = "*", default-features = false }
|
||||||
# kittage = { path = "../kittage/", features = ["crossterm-tokio", "image-crate", "log"] }
|
# kittage = { path = "../kittage/", features = ["crossterm-tokio", "image-crate", "log"] }
|
||||||
kittage = { git = "https://github.com/itsjunetime/kittage.git", features = ["crossterm-tokio", "image-crate", "log"] }
|
kittage = { git = "https://github.com/itsjunetime/kittage.git", features = ["crossterm-tokio", "image-crate", "log"] }
|
||||||
@@ -49,7 +49,7 @@ log = "0.4.27"
|
|||||||
flexi_logger = "0.31"
|
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 }
|
||||||
csscolorparser = { version = "0.7.0" }
|
csscolorparser = { version = "0.7.0" }
|
||||||
|
|
||||||
[profile.production]
|
[profile.production]
|
||||||
@@ -57,8 +57,7 @@ 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"]
|
||||||
@@ -93,7 +92,7 @@ checked_conversions = "warn"
|
|||||||
copy_iterator = "warn"
|
copy_iterator = "warn"
|
||||||
default_trait_access = "warn"
|
default_trait_access = "warn"
|
||||||
doc_link_with_quotes = "warn"
|
doc_link_with_quotes = "warn"
|
||||||
empty_enum = "warn"
|
empty_enums = "warn"
|
||||||
explicit_into_iter_loop = "warn"
|
explicit_into_iter_loop = "warn"
|
||||||
explicit_iter_loop = "warn"
|
explicit_iter_loop = "warn"
|
||||||
filter_map_next = "warn"
|
filter_map_next = "warn"
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ Designed to be performant, very responsive, and work well with even very large P
|
|||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Get the rust toolchain from [rustup.rs](https://rustup.rs)
|
1. Get the rust toolchain from [rustup.rs](https://rustup.rs)
|
||||||
2. Run `rustup install nightly && cargo +nightly install --git https://github.com/itsjunetime/tdf.git`
|
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` 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`.
|
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`.
|
||||||
|
|||||||
+1
-1
Submodule ratatui updated: 47c200fb7f...6a0b8ddf76
+1
-1
Submodule ratatui-image updated: 375a9e190a...9564ae6466
+12
-6
@@ -26,6 +26,7 @@ pub enum MaybeTransferred {
|
|||||||
Transferred(kittage::ImageId)
|
Transferred(kittage::ImageId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum ConvertedImage {
|
pub enum ConvertedImage {
|
||||||
Generic(Protocol),
|
Generic(Protocol),
|
||||||
Kitty {
|
Kitty {
|
||||||
@@ -139,14 +140,13 @@ pub async fn run_conversion_loop(
|
|||||||
let rn = SystemTime::now()
|
let rn = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.as_nanos();
|
.as_millis() % 1_000_000;
|
||||||
|
|
||||||
let mut img = if shms_work {
|
let mut img = if shms_work {
|
||||||
kittage::image::Image::shm_from(
|
kittage::image::Image::shm_from(dyn_img, &format!("tdf_{pid}_{rn}_{page_num}"))
|
||||||
dyn_img,
|
.map_err(|e| {
|
||||||
&format!("__tdf_kittage_{pid}_page_{rn}_{page_num}")
|
RenderError::Converting(format!("Couldn't write to shm: {e}"))
|
||||||
)
|
})?
|
||||||
.map_err(|e| RenderError::Converting(format!("Couldn't write to shm: {e}")))?
|
|
||||||
} else {
|
} else {
|
||||||
kittage::image::Image::from(dyn_img)
|
kittage::image::Image::from(dyn_img)
|
||||||
};
|
};
|
||||||
@@ -169,6 +169,12 @@ pub async fn run_conversion_loop(
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
|||||||
+1
-2
@@ -78,8 +78,7 @@ pub async fn run_action<'image, 'data, 'es>(
|
|||||||
pub async fn do_shms_work(ev_stream: &mut EventStream) -> bool {
|
pub async fn do_shms_work(ev_stream: &mut EventStream) -> bool {
|
||||||
let img = DynamicImage::new_rgb8(1, 1);
|
let img = DynamicImage::new_rgb8(1, 1);
|
||||||
let pid = std::process::id();
|
let pid = std::process::id();
|
||||||
let Ok(mut k_img) = kittage::image::Image::shm_from(img, &format!("__tdf_kittage_test_{pid}"))
|
let Ok(mut k_img) = kittage::image::Image::shm_from(img, &format!("tdf_test_{pid}")) else {
|
||||||
else {
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+79
-36
@@ -1,9 +1,11 @@
|
|||||||
use core::error::Error;
|
use core::{
|
||||||
|
error::Error,
|
||||||
|
num::{NonZeroU32, NonZeroUsize}
|
||||||
|
};
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
ffi::OsString,
|
ffi::OsString,
|
||||||
io::{BufReader, Read, Stdout, stdout},
|
io::{BufReader, Read, Stdout, Write, stdout},
|
||||||
num::{NonZeroU32, NonZeroUsize},
|
|
||||||
path::PathBuf
|
path::PathBuf
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -54,19 +56,38 @@ impl std::fmt::Debug for WrappedErr {
|
|||||||
|
|
||||||
impl std::error::Error for WrappedErr {}
|
impl std::error::Error for WrappedErr {}
|
||||||
|
|
||||||
|
fn reset_term() {
|
||||||
|
_ = execute!(
|
||||||
|
std::io::stdout(),
|
||||||
|
LeaveAlternateScreen,
|
||||||
|
crossterm::cursor::Show,
|
||||||
|
crossterm::event::DisableMouseCapture
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), WrappedErr> {
|
async fn main() -> Result<(), WrappedErr> {
|
||||||
|
inner_main().await.inspect_err(|_| reset_term())
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
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 number of pages to prerender surrounding the currently-shown page; 0 means no
|
/// The number of pages to prerender surrounding the currently-shown page; 0 means no
|
||||||
/// limit. By default, there is no limit.
|
/// limit. By default, there is no limit.
|
||||||
optional -p,--prerender prerender: usize
|
optional -p,--prerender prerender: usize
|
||||||
@@ -74,12 +95,24 @@ async fn main() -> Result<(), WrappedErr> {
|
|||||||
optional -w,--white-color white: String
|
optional -w,--white-color white: String
|
||||||
/// Custom black color, specified in css format (e.g "000000" or "rgb(0, 0, 0)")
|
/// Custom black color, specified in css format (e.g "000000" or "rgb(0, 0, 0)")
|
||||||
optional -b,--black-color black: String
|
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
|
if flags.version {
|
||||||
.file
|
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()
|
.canonicalize()
|
||||||
.map_err(|e| WrappedErr(format!("Cannot canonicalize provided file: {e}").into()))?;
|
.map_err(|e| WrappedErr(format!("Cannot canonicalize provided file: {e}").into()))?;
|
||||||
|
|
||||||
@@ -160,14 +193,39 @@ async fn main() -> Result<(), WrappedErr> {
|
|||||||
window_size.height = h;
|
window_size.height = h;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
.map_err(|e| WrappedErr(match e {
|
.or_else(|e| match e {
|
||||||
ratatui_image::errors::Errors::NoFontSize =>
|
ratatui_image::errors::Errors::NoFontSize if
|
||||||
"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(),
|
window_size.width != 0
|
||||||
e => format!("Couldn't get the necessary information to set up images: {e}").into()
|
&& 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,
|
||||||
@@ -177,8 +235,6 @@ async fn main() -> Result<(), WrappedErr> {
|
|||||||
.and_then(NonZeroUsize::new)
|
.and_then(NonZeroUsize::new)
|
||||||
.map_or(PrerenderLimit::All, PrerenderLimit::Limited);
|
.map_or(PrerenderLimit::All, PrerenderLimit::Limited);
|
||||||
|
|
||||||
let cell_height_px = window_size.height / window_size.rows;
|
|
||||||
let cell_width_px = window_size.width / window_size.columns;
|
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
renderer::start_rendering(
|
renderer::start_rendering(
|
||||||
&file_path,
|
&file_path,
|
||||||
@@ -211,12 +267,7 @@ async fn main() -> Result<(), WrappedErr> {
|
|||||||
|| "Unknown file".into(),
|
|| "Unknown file".into(),
|
||||||
|n| n.to_string_lossy().to_string()
|
|n| n.to_string_lossy().to_string()
|
||||||
);
|
);
|
||||||
let tui = Tui::new(
|
let tui = Tui::new(file_name, flags.max_wide, flags.r_to_l, is_kitty);
|
||||||
file_name,
|
|
||||||
flags.max_wide,
|
|
||||||
flags.r_to_l.unwrap_or_default(),
|
|
||||||
is_kitty
|
|
||||||
);
|
|
||||||
|
|
||||||
let backend = CrosstermBackend::new(std::io::stdout());
|
let backend = CrosstermBackend::new(std::io::stdout());
|
||||||
let mut term = Terminal::new(backend).map_err(|e| {
|
let mut term = Terminal::new(backend).map_err(|e| {
|
||||||
@@ -224,19 +275,6 @@ async fn main() -> Result<(), WrappedErr> {
|
|||||||
})?;
|
})?;
|
||||||
term.skip_diff(true);
|
term.skip_diff(true);
|
||||||
|
|
||||||
execute!(
|
|
||||||
term.backend_mut(),
|
|
||||||
EnterAlternateScreen,
|
|
||||||
crossterm::cursor::Hide
|
|
||||||
)
|
|
||||||
.map_err(|e| {
|
|
||||||
WrappedErr(
|
|
||||||
format!(
|
|
||||||
"Couldn't enter the alternate screen and hide the cursor for proper presentation: {e}"
|
|
||||||
)
|
|
||||||
.into()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
enable_raw_mode().map_err(|e| {
|
enable_raw_mode().map_err(|e| {
|
||||||
WrappedErr(
|
WrappedErr(
|
||||||
format!("Can't enable raw mode, which is necessary to receive input: {e}").into()
|
format!("Can't enable raw mode, which is necessary to receive input: {e}").into()
|
||||||
@@ -257,7 +295,7 @@ async fn main() -> Result<(), WrappedErr> {
|
|||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let fullscreen = flags.fullscreen.unwrap_or_default();
|
let fullscreen = flags.fullscreen;
|
||||||
let main_area = Tui::main_layout(&term.get_frame(), fullscreen);
|
let main_area = Tui::main_layout(&term.get_frame(), fullscreen);
|
||||||
to_renderer
|
to_renderer
|
||||||
.send(RenderNotif::Area(main_area.page_area))
|
.send(RenderNotif::Area(main_area.page_area))
|
||||||
@@ -295,7 +333,8 @@ async fn main() -> Result<(), WrappedErr> {
|
|||||||
execute!(
|
execute!(
|
||||||
term.backend_mut(),
|
term.backend_mut(),
|
||||||
LeaveAlternateScreen,
|
LeaveAlternateScreen,
|
||||||
crossterm::cursor::Show
|
crossterm::cursor::Show,
|
||||||
|
crossterm::event::DisableMouseCapture
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
disable_raw_mode().unwrap();
|
disable_raw_mode().unwrap();
|
||||||
@@ -464,6 +503,10 @@ fn parse_color_to_i32(cs: &str) -> Result<i32, csscolorparser::ParseColorError>
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_font_size_through_stdio() -> Result<(u16, u16), WrappedErr> {
|
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
|
// 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
|
// 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| {
|
enable_raw_mode().map_err(|e| {
|
||||||
|
|||||||
+17
-19
@@ -2,7 +2,7 @@ use std::{collections::VecDeque, num::NonZeroUsize, thread::sleep, time::Duratio
|
|||||||
|
|
||||||
use flume::{Receiver, SendError, Sender, TryRecvError};
|
use flume::{Receiver, SendError, Sender, TryRecvError};
|
||||||
use mupdf::{
|
use mupdf::{
|
||||||
Colorspace, Document, Matrix, Page, Pixmap, Quad, TextPageOptions, text_page::SearchHitResponse
|
Colorspace, Document, Matrix, Page, Pixmap, Quad, TextPageFlags, text_page::SearchHitResponse
|
||||||
};
|
};
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
|
|
||||||
@@ -520,7 +520,7 @@ fn render_single_page_to_ctx(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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,
|
||||||
@@ -536,15 +536,14 @@ fn search_page(
|
|||||||
) -> Result<Vec<Quad>, mupdf::error::Error> {
|
) -> Result<Vec<Quad>, mupdf::error::Error> {
|
||||||
search_term
|
search_term
|
||||||
.map(|term| {
|
.map(|term| {
|
||||||
page.to_text_page(TextPageOptions::empty())
|
page.to_text_page(TextPageFlags::empty()).and_then(|page| {
|
||||||
.and_then(|page| {
|
let mut v = Vec::with_capacity(trusted_search_results);
|
||||||
let mut v = Vec::with_capacity(trusted_search_results);
|
page.search_cb(term, &mut v, |v, results| {
|
||||||
page.search_cb(term, &mut v, |v, results| {
|
v.extend(results.iter().cloned());
|
||||||
v.extend(results.iter().cloned());
|
SearchHitResponse::ContinueSearch
|
||||||
SearchHitResponse::ContinueSearch
|
|
||||||
})
|
|
||||||
.map(|_| v)
|
|
||||||
})
|
})
|
||||||
|
.map(|_| v)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.transpose()
|
.transpose()
|
||||||
.map(Option::unwrap_or_default)
|
.map(Option::unwrap_or_default)
|
||||||
@@ -552,15 +551,14 @@ fn search_page(
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn count_search_results(page: &Page, search_term: &str) -> Result<usize, mupdf::error::Error> {
|
fn count_search_results(page: &Page, search_term: &str) -> Result<usize, mupdf::error::Error> {
|
||||||
page.to_text_page(TextPageOptions::empty())
|
page.to_text_page(TextPageFlags::empty()).and_then(|page| {
|
||||||
.and_then(|page| {
|
let mut count = 0;
|
||||||
let mut count = 0;
|
page.search_cb(search_term, &mut count, |count, results| {
|
||||||
page.search_cb(search_term, &mut count, |count, results| {
|
*count += results.len();
|
||||||
*count += results.len();
|
SearchHitResponse::ContinueSearch
|
||||||
SearchHitResponse::ContinueSearch
|
})?;
|
||||||
})?;
|
Ok(count)
|
||||||
Ok(count)
|
})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PopOnNext<'a> {
|
struct PopOnNext<'a> {
|
||||||
|
|||||||
+69
-41
@@ -18,8 +18,8 @@ use ratatui::{
|
|||||||
layout::{Constraint, Flex, Layout, Position, Rect},
|
layout::{Constraint, Flex, Layout, Position, Rect},
|
||||||
style::{Color, Style},
|
style::{Color, Style},
|
||||||
symbols::border,
|
symbols::border,
|
||||||
text::{Span, Text},
|
text::Span,
|
||||||
widgets::{Block, Borders, Clear, Padding}
|
widgets::{Block, Borders, Clear, Padding, Paragraph, Wrap}
|
||||||
};
|
};
|
||||||
use ratatui_image::{FontSize, Image};
|
use ratatui_image::{FontSize, Image};
|
||||||
|
|
||||||
@@ -205,23 +205,28 @@ impl Tui {
|
|||||||
|
|
||||||
log::debug!("zoom is now {zoom:#?}");
|
log::debug!("zoom is now {zoom:#?}");
|
||||||
log::debug!("img_area is {img_area:#?}");
|
log::debug!("img_area is {img_area:#?}");
|
||||||
|
log::debug!("img dimensions are {cell_w}x{cell_h}");
|
||||||
|
|
||||||
if zoom.level < 0 {
|
|
||||||
img_area = Rect {
|
|
||||||
width: img_area
|
|
||||||
.width
|
|
||||||
.saturating_sub((zoom.level * 2).unsigned_abs())
|
|
||||||
.max(1),
|
|
||||||
x: img_area.x + (zoom.level.unsigned_abs().min(img_area.width / 2)),
|
|
||||||
..img_area
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log::debug!("after adjustment, img_area is {img_area:#?}");
|
|
||||||
|
|
||||||
// Ugh I don't like this logic. I wish we could simplify it.
|
|
||||||
let img_width = f32::from(cell_w);
|
let img_width = f32::from(cell_w);
|
||||||
let img_height = f32::from(cell_h);
|
let img_height = f32::from(cell_h);
|
||||||
|
|
||||||
|
let img_aspect_ratio = img_width / img_height;
|
||||||
|
|
||||||
|
if zoom.level < 0 {
|
||||||
|
let old_width = img_area.width;
|
||||||
|
img_area.width = img_area
|
||||||
|
.width
|
||||||
|
.saturating_sub((zoom.level * 2).unsigned_abs())
|
||||||
|
.max((f32::from(img_area.height) * img_aspect_ratio) as u16);
|
||||||
|
img_area.x += (old_width - img_area.width) / 2;
|
||||||
|
|
||||||
|
log::debug!("after adjustment, img_area is {img_area:#?}");
|
||||||
|
|
||||||
|
// TODO: Find a way to detect when we've hit the maximum zoom-out and stop
|
||||||
|
// more zooming out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ugh I don't like this logic. I wish we could simplify it.
|
||||||
let img_area_width = f32::from(img_area.width);
|
let img_area_width = f32::from(img_area.width);
|
||||||
let img_area_height = f32::from(img_area.height);
|
let img_area_height = f32::from(img_area.height);
|
||||||
let available_to_real_width_ratio = img_area_width / img_width;
|
let available_to_real_width_ratio = img_area_width / img_width;
|
||||||
@@ -624,7 +629,8 @@ impl Tui {
|
|||||||
execute!(
|
execute!(
|
||||||
&mut backend,
|
&mut backend,
|
||||||
LeaveAlternateScreen,
|
LeaveAlternateScreen,
|
||||||
crossterm::cursor::Show
|
crossterm::cursor::Show,
|
||||||
|
crossterm::event::DisableMouseCapture
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
disable_raw_mode().unwrap();
|
disable_raw_mode().unwrap();
|
||||||
@@ -638,7 +644,8 @@ impl Tui {
|
|||||||
execute!(
|
execute!(
|
||||||
&mut backend,
|
&mut backend,
|
||||||
EnterAlternateScreen,
|
EnterAlternateScreen,
|
||||||
crossterm::cursor::Hide
|
crossterm::cursor::Hide,
|
||||||
|
crossterm::event::EnableMouseCapture
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -754,23 +761,38 @@ impl Tui {
|
|||||||
_ => None
|
_ => None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::Mouse(mouse) => match mouse.kind {
|
Event::Mouse(mouse) => {
|
||||||
MouseEventKind::ScrollRight =>
|
if mouse.modifiers.contains(KeyModifiers::CONTROL)
|
||||||
self.change_page(PageChange::Next, ChangeAmount::Single),
|
&& self.is_kitty
|
||||||
MouseEventKind::ScrollDown =>
|
&& self.zoom.is_some()
|
||||||
self.change_page(PageChange::Next, ChangeAmount::WholeScreen),
|
{
|
||||||
MouseEventKind::ScrollLeft =>
|
match mouse.kind {
|
||||||
self.change_page(PageChange::Prev, ChangeAmount::Single),
|
MouseEventKind::ScrollUp =>
|
||||||
MouseEventKind::ScrollUp =>
|
self.update_zoom(|z| z.level = z.level.saturating_add(1).min(0)),
|
||||||
self.change_page(PageChange::Prev, ChangeAmount::WholeScreen),
|
MouseEventKind::ScrollDown =>
|
||||||
_ => None
|
self.update_zoom(|z| z.level = z.level.saturating_sub(1)),
|
||||||
},
|
_ => None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match mouse.kind {
|
||||||
|
MouseEventKind::ScrollRight =>
|
||||||
|
self.change_page(PageChange::Next, ChangeAmount::Single),
|
||||||
|
MouseEventKind::ScrollDown =>
|
||||||
|
self.change_page(PageChange::Next, ChangeAmount::WholeScreen),
|
||||||
|
MouseEventKind::ScrollLeft =>
|
||||||
|
self.change_page(PageChange::Prev, ChangeAmount::Single),
|
||||||
|
MouseEventKind::ScrollUp =>
|
||||||
|
self.change_page(PageChange::Prev, ChangeAmount::WholeScreen),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Event::Resize(_, _) => Some(InputAction::Redraw),
|
Event::Resize(_, _) => Some(InputAction::Redraw),
|
||||||
_ => None
|
_ => None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// I want this to always return 0 'cause I just use it to return from `Self::handle_event`]
|
// I want this to always return an option 'cause I just use it to return from `Self::handle_event`
|
||||||
#[expect(clippy::unnecessary_wraps)]
|
#[expect(clippy::unnecessary_wraps)]
|
||||||
fn update_zoom(&mut self, f: impl FnOnce(&mut Zoom)) -> Option<InputAction> {
|
fn update_zoom(&mut self, f: impl FnOnce(&mut Zoom)) -> Option<InputAction> {
|
||||||
if let Some(z) = &mut self.zoom {
|
if let Some(z) = &mut self.zoom {
|
||||||
@@ -830,7 +852,7 @@ impl Tui {
|
|||||||
.border_set(border::ROUNDED)
|
.border_set(border::ROUNDED)
|
||||||
.border_style(Color::Blue);
|
.border_style(Color::Blue);
|
||||||
|
|
||||||
let help_span = Text::raw(HELP_PAGE);
|
let help_span = Paragraph::new(HELP_PAGE).wrap(Wrap { trim: false });
|
||||||
|
|
||||||
let max_w: u16 = HELP_PAGE
|
let max_w: u16 = HELP_PAGE
|
||||||
.lines()
|
.lines()
|
||||||
@@ -863,25 +885,31 @@ impl Tui {
|
|||||||
|
|
||||||
static HELP_PAGE: &str = "\
|
static HELP_PAGE: &str = "\
|
||||||
l, h, left, right:
|
l, h, left, right:
|
||||||
Go forward/backwards a single page
|
Go forward/backwards a single page
|
||||||
j, k, down, up:
|
j, k, down, up:
|
||||||
Go forwards/backwards a screen's worth of pages
|
Go forwards/backwards a screen's worth of pages
|
||||||
q, esc:
|
q, esc:
|
||||||
Quit
|
Quit
|
||||||
g:
|
g:
|
||||||
Go to specific page (type numbers after 'g')
|
Go to specific page (type numbers after 'g')
|
||||||
/:
|
/:
|
||||||
Search
|
Search
|
||||||
n, N:
|
n, N:
|
||||||
Next/Previous search result
|
Next/Previous search result
|
||||||
i:
|
i:
|
||||||
Invert colors
|
Invert colors
|
||||||
f:
|
f:
|
||||||
Remove borders/fullscreen
|
Remove borders/fullscreen
|
||||||
|
z (when using kitty protocol):
|
||||||
|
Toggle between fill-screen and fit-screen
|
||||||
|
o/O (when on fill-screen):
|
||||||
|
zoom in and out, respectively
|
||||||
|
H, J, K, L (when zoomed in):
|
||||||
|
pan direction around page
|
||||||
?:
|
?:
|
||||||
Show this page
|
Show this page
|
||||||
ctrl+z:
|
ctrl+z:
|
||||||
Suspend & background tdf \
|
Suspend & background tdf \
|
||||||
";
|
";
|
||||||
|
|
||||||
pub enum InputAction {
|
pub enum InputAction {
|
||||||
|
|||||||
Reference in New Issue
Block a user