mirror of
https://github.com/itsjunetime/tdf.git
synced 2026-06-02 08:01:47 -04:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 15b1a99d76 | |||
| 170bdd3869 | |||
| 7551c4dba3 | |||
| e61eb9b846 | |||
| 6b37976357 | |||
| 3628d21c74 | |||
| f4f3b4f539 | |||
| a79c534e97 | |||
| 971393892a | |||
| 440515a3db | |||
| 690489016c | |||
| bd5554db27 | |||
| 45409bacd0 | |||
| 0481c14c4d | |||
| a78ea5a08c | |||
| f7eabc9af2 |
@@ -1,6 +1,15 @@
|
||||
# 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
|
||||
|
||||
|
||||
Generated
+560
-662
File diff suppressed because it is too large
Load Diff
+5
-6
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tdf-viewer"
|
||||
version = "0.4.2"
|
||||
version = "0.4.3"
|
||||
authors = ["June Welker <junewelker@gmail.com>"]
|
||||
edition = "2024"
|
||||
description = "A terminal viewer for PDFs"
|
||||
@@ -38,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.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 }
|
||||
# kittage = { path = "../kittage/", 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"
|
||||
|
||||
# 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" }
|
||||
|
||||
[profile.production]
|
||||
@@ -57,8 +57,7 @@ inherits = "release"
|
||||
lto = "fat"
|
||||
|
||||
[features]
|
||||
default = ["nightly"]
|
||||
nightly = ["ratatui-image/vb64"]
|
||||
default = []
|
||||
tracing = ["tokio/tracing", "dep:console-subscriber"]
|
||||
epub = ["mupdf/epub"]
|
||||
cbz = ["mupdf/cbz"]
|
||||
@@ -93,7 +92,7 @@ checked_conversions = "warn"
|
||||
copy_iterator = "warn"
|
||||
default_trait_access = "warn"
|
||||
doc_link_with_quotes = "warn"
|
||||
empty_enum = "warn"
|
||||
empty_enums = "warn"
|
||||
explicit_into_iter_loop = "warn"
|
||||
explicit_iter_loop = "warn"
|
||||
filter_map_next = "warn"
|
||||
|
||||
@@ -16,7 +16,9 @@ Designed to be performant, very responsive, and work well with even very large P
|
||||
## Installation
|
||||
|
||||
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
|
||||
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`.
|
||||
@@ -25,7 +27,7 @@ If it turns out that you're missing one of these, it will fail to compile and te
|
||||
|
||||
1. Get the rust toolchain from [rustup.rs](https://rustup.rs)
|
||||
2. Clone the repo and `cd` into it
|
||||
3. Run `cargo +nightly build --release`
|
||||
3. Run `cargo build --release`
|
||||
|
||||
The binary should then be found at `./target/release/tdf`.
|
||||
|
||||
|
||||
+1
-1
Submodule ratatui updated: 47c200fb7f...6a0b8ddf76
+1
-1
Submodule ratatui-image updated: 24087c1e8e...9564ae6466
@@ -26,6 +26,7 @@ pub enum MaybeTransferred {
|
||||
Transferred(kittage::ImageId)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ConvertedImage {
|
||||
Generic(Protocol),
|
||||
Kitty {
|
||||
@@ -168,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
|
||||
*iteration = new_iter;
|
||||
|
||||
|
||||
+62
-33
@@ -1,9 +1,11 @@
|
||||
use core::error::Error;
|
||||
use core::{
|
||||
error::Error,
|
||||
num::{NonZeroU32, NonZeroUsize}
|
||||
};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
ffi::OsString,
|
||||
io::{BufReader, Read, Stdout, stdout},
|
||||
num::{NonZeroU32, NonZeroUsize},
|
||||
io::{BufReader, Read, Stdout, Write, stdout},
|
||||
path::PathBuf
|
||||
};
|
||||
|
||||
@@ -54,19 +56,38 @@ impl std::fmt::Debug for WrappedErr {
|
||||
|
||||
impl std::error::Error for WrappedErr {}
|
||||
|
||||
fn reset_term() {
|
||||
_ = execute!(
|
||||
std::io::stdout(),
|
||||
LeaveAlternateScreen,
|
||||
crossterm::cursor::Show,
|
||||
crossterm::event::DisableMouseCapture
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
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")]
|
||||
console_subscriber::init();
|
||||
|
||||
let flags = xflags::parse_or_exit! {
|
||||
/// Display the pdf with the pages starting at the right hand size and moving left and
|
||||
/// adjust input keys to match
|
||||
optional -r,--r-to-l r_to_l: bool
|
||||
optional -r,--r-to-l
|
||||
/// 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
|
||||
optional -f,--fullscreen
|
||||
/// The number of pages to prerender surrounding the currently-shown page; 0 means no
|
||||
/// limit. By default, there is no limit.
|
||||
optional -p,--prerender prerender: usize
|
||||
@@ -172,14 +193,39 @@ async fn main() -> Result<(), WrappedErr> {
|
||||
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,
|
||||
// it messes up something with user input. Input never makes it to the crossterm thing
|
||||
let picker = Picker::from_query_stdio()
|
||||
.map_err(|e| WrappedErr(match e {
|
||||
ratatui_image::errors::Errors::NoFontSize =>
|
||||
"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 => format!("Couldn't get the necessary information to set up images: {e}").into()
|
||||
}))?;
|
||||
.or_else(|e| match e {
|
||||
ratatui_image::errors::Errors::NoFontSize if
|
||||
window_size.width != 0
|
||||
&& window_size.height != 0
|
||||
&& window_size.columns != 0
|
||||
&& window_size.rows != 0
|
||||
=> Ok(Picker::from_fontsize((cell_width_px, cell_height_px))),
|
||||
ratatui_image::errors::Errors::NoFontSize => Err(WrappedErr(
|
||||
"Unable to detect your terminal's font size; this is an issue with your terminal emulator.\nPlease use a different terminal emulator or report this bug to tdf.".into()
|
||||
)),
|
||||
e => Err(WrappedErr(format!("Couldn't get the necessary information to set up images: {e}").into()))
|
||||
})?;
|
||||
|
||||
// then we want to spawn off the rendering task
|
||||
// We need to use the thread::spawn API so that this exists in a thread not owned by tokio,
|
||||
@@ -189,8 +235,6 @@ async fn main() -> Result<(), WrappedErr> {
|
||||
.and_then(NonZeroUsize::new)
|
||||
.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 || {
|
||||
renderer::start_rendering(
|
||||
&file_path,
|
||||
@@ -223,12 +267,7 @@ async fn main() -> Result<(), WrappedErr> {
|
||||
|| "Unknown file".into(),
|
||||
|n| n.to_string_lossy().to_string()
|
||||
);
|
||||
let tui = Tui::new(
|
||||
file_name,
|
||||
flags.max_wide,
|
||||
flags.r_to_l.unwrap_or_default(),
|
||||
is_kitty
|
||||
);
|
||||
let tui = Tui::new(file_name, flags.max_wide, flags.r_to_l, is_kitty);
|
||||
|
||||
let backend = CrosstermBackend::new(std::io::stdout());
|
||||
let mut term = Terminal::new(backend).map_err(|e| {
|
||||
@@ -236,20 +275,6 @@ async fn main() -> Result<(), WrappedErr> {
|
||||
})?;
|
||||
term.skip_diff(true);
|
||||
|
||||
execute!(
|
||||
term.backend_mut(),
|
||||
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()
|
||||
)
|
||||
})?;
|
||||
enable_raw_mode().map_err(|e| {
|
||||
WrappedErr(
|
||||
format!("Can't enable raw mode, which is necessary to receive input: {e}").into()
|
||||
@@ -270,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);
|
||||
to_renderer
|
||||
.send(RenderNotif::Area(main_area.page_area))
|
||||
@@ -478,6 +503,10 @@ fn parse_color_to_i32(cs: &str) -> Result<i32, csscolorparser::ParseColorError>
|
||||
}
|
||||
|
||||
fn get_font_size_through_stdio() -> Result<(u16, u16), WrappedErr> {
|
||||
// send the command code to get the terminal window size
|
||||
print!("\x1b[14t");
|
||||
std::io::stdout().flush().unwrap();
|
||||
|
||||
// we need to enable raw mode here since this bit of output won't print a newline; it'll
|
||||
// just print the info it wants to tell us. So we want to get all characters as they come
|
||||
enable_raw_mode().map_err(|e| {
|
||||
|
||||
+17
-19
@@ -2,7 +2,7 @@ use std::{collections::VecDeque, num::NonZeroUsize, thread::sleep, time::Duratio
|
||||
|
||||
use flume::{Receiver, SendError, Sender, TryRecvError};
|
||||
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;
|
||||
|
||||
@@ -520,7 +520,7 @@ fn render_single_page_to_ctx(
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct HighlightRect {
|
||||
pub ul_x: u32,
|
||||
pub ul_y: u32,
|
||||
@@ -536,15 +536,14 @@ fn search_page(
|
||||
) -> Result<Vec<Quad>, mupdf::error::Error> {
|
||||
search_term
|
||||
.map(|term| {
|
||||
page.to_text_page(TextPageOptions::empty())
|
||||
.and_then(|page| {
|
||||
let mut v = Vec::with_capacity(trusted_search_results);
|
||||
page.search_cb(term, &mut v, |v, results| {
|
||||
v.extend(results.iter().cloned());
|
||||
SearchHitResponse::ContinueSearch
|
||||
})
|
||||
.map(|_| v)
|
||||
page.to_text_page(TextPageFlags::empty()).and_then(|page| {
|
||||
let mut v = Vec::with_capacity(trusted_search_results);
|
||||
page.search_cb(term, &mut v, |v, results| {
|
||||
v.extend(results.iter().cloned());
|
||||
SearchHitResponse::ContinueSearch
|
||||
})
|
||||
.map(|_| v)
|
||||
})
|
||||
})
|
||||
.transpose()
|
||||
.map(Option::unwrap_or_default)
|
||||
@@ -552,15 +551,14 @@ fn search_page(
|
||||
|
||||
#[inline]
|
||||
fn count_search_results(page: &Page, search_term: &str) -> Result<usize, mupdf::error::Error> {
|
||||
page.to_text_page(TextPageOptions::empty())
|
||||
.and_then(|page| {
|
||||
let mut count = 0;
|
||||
page.search_cb(search_term, &mut count, |count, results| {
|
||||
*count += results.len();
|
||||
SearchHitResponse::ContinueSearch
|
||||
})?;
|
||||
Ok(count)
|
||||
})
|
||||
page.to_text_page(TextPageFlags::empty()).and_then(|page| {
|
||||
let mut count = 0;
|
||||
page.search_cb(search_term, &mut count, |count, results| {
|
||||
*count += results.len();
|
||||
SearchHitResponse::ContinueSearch
|
||||
})?;
|
||||
Ok(count)
|
||||
})
|
||||
}
|
||||
|
||||
struct PopOnNext<'a> {
|
||||
|
||||
+20
-15
@@ -205,23 +205,28 @@ impl Tui {
|
||||
|
||||
log::debug!("zoom is now {zoom:#?}");
|
||||
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_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_height = f32::from(img_area.height);
|
||||
let available_to_real_width_ratio = img_area_width / img_width;
|
||||
@@ -787,7 +792,7 @@ impl Tui {
|
||||
}
|
||||
}
|
||||
|
||||
// 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)]
|
||||
fn update_zoom(&mut self, f: impl FnOnce(&mut Zoom)) -> Option<InputAction> {
|
||||
if let Some(z) = &mut self.zoom {
|
||||
|
||||
Reference in New Issue
Block a user