Compare commits

..

22 Commits

Author SHA1 Message Date
itsjunetime dcc3dbc958 Small fixes to avoid panic and allow zooming back in after zooming out 2025-08-06 09:28:29 -06:00
itsjunetime 196f7fb589 fmt 2025-08-06 09:22:15 -06:00
itsjunetime 1c797d4653 Switch around list of items on changelog 2025-08-06 09:22:15 -06:00
itsjunetime 16ac61dc8e Update deps 2025-08-06 09:22:15 -06:00
itsjunetime 05bfee148c mmmm maybe it's finally ready to merge... 2025-08-06 09:22:15 -06:00
itsjunetime b368f8d41d yaaaay zooming out once you're already zoomed in and respecting kitty's limits for how big of an image to display 2025-08-06 09:22:15 -06:00
itsjunetime 484d248e26 Add debug logging and fix cursor placement after image display 2025-08-06 09:22:15 -06:00
itsjunetime da8cdd1fbd Only allow zooming in kitty 2025-08-06 09:22:15 -06:00
itsjunetime 02b447a98e clean up top and bottom rendering 2025-08-06 09:22:15 -06:00
itsjunetime 0578fccfa6 yay zooming woohoo 2025-08-06 09:22:15 -06:00
itsjunetime a67ff7996c zooming basically does what you'd expect now 2025-08-06 09:22:15 -06:00
itsjunetime a56fa8c817 Make help page work again 2025-08-06 09:22:15 -06:00
itsjunetime fc063efd42 fall back to stdout if shms don't work 2025-08-06 09:22:15 -06:00
itsjunetime 62c92141e3 Make it work correctly with ghostty image eviction too 2025-08-06 09:22:15 -06:00
itsjunetime 5e6857881b incorporate recovering from deleted images 2025-08-06 09:22:15 -06:00
itsjunetime 7514488441 Remove logging 2025-08-06 09:22:15 -06:00
itsjunetime 6677266010 Uhhhh various improvements from kittage and psx-shm 2025-08-06 09:22:15 -06:00
itsjunetime b791b55b80 Use github kittage 2025-08-06 09:22:15 -06:00
itsjunetime fcea5ac696 yaaayyyy it works 2025-08-06 09:22:15 -06:00
itsjunetime 4bde532d08 it almost basically works 2025-08-06 09:22:15 -06:00
itsjunetime 4d764cd4f9 it's almost working !! 2025-08-06 09:22:15 -06:00
itsjunetime b09ce88d9f Initial attempt at supporting new backend for kitty images 2025-08-06 09:22:15 -06:00
12 changed files with 503 additions and 680 deletions
+5 -5
View File
@@ -31,12 +31,12 @@ jobs:
- name: Install clippy and fmt
run: rustup component add clippy rustfmt
- name: Clippy
run: cargo clippy --locked -- -D warnings
run: cargo clippy -- -D warnings
- name: Tests
run: cargo test --locked
run: cargo test
- name: Check fmt
run: cargo fmt -- --check
- name: Run benchmarks as tests
run: cargo test --locked --benches -- adobe_example
- name: Run tests
run: cargo test --benches -- adobe_example
- name: Build
run: cargo build --locked
run: cargo build
-20
View File
@@ -1,25 +1,5 @@
# Unreleased
- Switched simd base64 crate for one that works on stable (from `vb64` to `base64_simd`)
# v0.4.3
- Fix issue with some terminals hanging on startup
- Fix issues with some iterm2-backend terminals not displaying anything
- Allow using ctrl+scroll to zoom in/out while zoomed using kitty backend
- (Internal) run CI with `--locked` flag to ensure lockfile is always in-sync
# v0.4.2
- Add `--version` flag
- Fix shms not working on macos ([#93](https://github.com/itsjunetime/tdf/pull/93))
# v0.4.1
- Add instructions for using new zoom/pan features to help page
# v0.4.0
- Update to new `kittage` backend for kitty-protocol-supporting terminals (fixes many issues and improves performance significantly, see [the PR](https://github.com/itsjunetime/tdf/pull/74))
- Use new mupdf search API for slightly better performance
- Update ratatui(-image) dependencies
Generated
+410 -492
View File
File diff suppressed because it is too large Load Diff
+5 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "tdf-viewer"
version = "0.4.3"
version = "0.3.0"
authors = ["June Welker <junewelker@gmail.com>"]
edition = "2024"
description = "A terminal viewer for PDFs"
@@ -11,7 +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.86"
rust-version = "1.85"
[[bin]]
name = "tdf"
@@ -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 = { git = "https://github.com/messense/mupdf-rs.git", rev = "2e0fae910fac8048c7008211fc4d3b9f5d227a07", default-features = false, features = ["svg", "system-fonts", "img"] }
mupdf = { version = "0.5.0", 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"] }
@@ -57,7 +57,8 @@ inherits = "release"
lto = "fat"
[features]
default = []
default = ["nightly"]
nightly = ["ratatui-image/vb64"]
tracing = ["tokio/tracing", "dep:console-subscriber"]
epub = ["mupdf/epub"]
cbz = ["mupdf/cbz"]
+1 -3
View File
@@ -16,9 +16,7 @@ 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 `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)
2. Run `rustup install nightly && cargo +nightly install --git https://github.com/itsjunetime/tdf.git`
## 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`.
+1 -1
Submodule ratatui updated: 6a0b8ddf76...47c200fb7f
+6 -12
View File
@@ -26,7 +26,6 @@ pub enum MaybeTransferred {
Transferred(kittage::ImageId)
}
#[derive(Debug)]
pub enum ConvertedImage {
Generic(Protocol),
Kitty {
@@ -140,13 +139,14 @@ pub async fn run_conversion_loop(
let rn = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() % 1_000_000;
.as_nanos();
let mut img = if shms_work {
kittage::image::Image::shm_from(dyn_img, &format!("tdf_{pid}_{rn}_{page_num}"))
.map_err(|e| {
RenderError::Converting(format!("Couldn't write to shm: {e}"))
})?
kittage::image::Image::shm_from(
dyn_img,
&format!("__tdf_kittage_{pid}_page_{rn}_{page_num}")
)
.map_err(|e| RenderError::Converting(format!("Couldn't write to shm: {e}")))?
} else {
kittage::image::Image::from(dyn_img)
};
@@ -169,12 +169,6 @@ 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;
+2 -1
View File
@@ -78,7 +78,8 @@ pub async fn run_action<'image, 'data, 'es>(
pub async fn do_shms_work(ev_stream: &mut EventStream) -> bool {
let img = DynamicImage::new_rgb8(1, 1);
let pid = std::process::id();
let Ok(mut k_img) = kittage::image::Image::shm_from(img, &format!("tdf_test_{pid}")) else {
let Ok(mut k_img) = kittage::image::Image::shm_from(img, &format!("__tdf_kittage_test_{pid}"))
else {
return false;
};
+27 -75
View File
@@ -1,11 +1,9 @@
use core::{
error::Error,
num::{NonZeroU32, NonZeroUsize}
};
use core::error::Error;
use std::{
borrow::Cow,
ffi::OsString,
io::{BufReader, Read, Stdout, Write, stdout},
io::{BufReader, Read, Stdout, stdout},
num::{NonZeroU32, NonZeroUsize},
path::PathBuf
};
@@ -56,27 +54,8 @@ 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();
@@ -95,24 +74,12 @@ async fn inner_main() -> Result<(), WrappedErr> {
optional -w,--white-color white: String
/// Custom black color, specified in css format (e.g "000000" or "rgb(0, 0, 0)")
optional -b,--black-color black: String
/// Print the version and exit
optional --version
/// PDF file to read
optional file: PathBuf
required file: PathBuf
};
if flags.version {
println!("{}", env!("CARGO_PKG_VERSION"));
return Ok(());
}
let Some(file) = flags.file else {
return Err(WrappedErr(
"Please specify the file to open, e.g. `tdf ./my_example_pdf.pdf`".into()
));
};
let path = file
let path = flags
.file
.canonicalize()
.map_err(|e| WrappedErr(format!("Cannot canonicalize provided file: {e}").into()))?;
@@ -193,39 +160,14 @@ async fn inner_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()
.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()))
})?;
.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()
}))?;
// 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,
@@ -235,6 +177,8 @@ async fn inner_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,
@@ -280,6 +224,19 @@ async fn inner_main() -> Result<(), WrappedErr> {
})?;
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| {
WrappedErr(
format!("Can't enable raw mode, which is necessary to receive input: {e}").into()
@@ -338,8 +295,7 @@ async fn inner_main() -> Result<(), WrappedErr> {
execute!(
term.backend_mut(),
LeaveAlternateScreen,
crossterm::cursor::Show,
crossterm::event::DisableMouseCapture
crossterm::cursor::Show
)
.unwrap();
disable_raw_mode().unwrap();
@@ -508,10 +464,6 @@ 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| {
+19 -17
View File
@@ -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, TextPageFlags, text_page::SearchHitResponse
Colorspace, Document, Matrix, Page, Pixmap, Quad, TextPageOptions, text_page::SearchHitResponse
};
use ratatui::layout::Rect;
@@ -520,7 +520,7 @@ fn render_single_page_to_ctx(
})
}
#[derive(Clone, Debug)]
#[derive(Clone)]
pub struct HighlightRect {
pub ul_x: u32,
pub ul_y: u32,
@@ -536,14 +536,15 @@ fn search_page(
) -> Result<Vec<Quad>, mupdf::error::Error> {
search_term
.map(|term| {
page.to_text_page(TextPageFlags::empty()).and_then(|page| {
let mut v = Vec::with_capacity(trusted_search_results);
page.search_cb(term, &mut v, |v, results| {
v.extend(results.iter().cloned());
SearchHitResponse::ContinueSearch
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)
})
.map(|_| v)
})
})
.transpose()
.map(Option::unwrap_or_default)
@@ -551,14 +552,15 @@ fn search_page(
#[inline]
fn count_search_results(page: &Page, search_term: &str) -> Result<usize, mupdf::error::Error> {
page.to_text_page(TextPageFlags::empty()).and_then(|page| {
let mut count = 0;
page.search_cb(search_term, &mut count, |count, results| {
*count += results.len();
SearchHitResponse::ContinueSearch
})?;
Ok(count)
})
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)
})
}
struct PopOnNext<'a> {
+26 -49
View File
@@ -18,8 +18,8 @@ use ratatui::{
layout::{Constraint, Flex, Layout, Position, Rect},
style::{Color, Style},
symbols::border,
text::Span,
widgets::{Block, Borders, Clear, Padding, Paragraph, Wrap}
text::{Span, Text},
widgets::{Block, Borders, Clear, Padding}
};
use ratatui_image::{FontSize, Image};
@@ -624,8 +624,7 @@ impl Tui {
execute!(
&mut backend,
LeaveAlternateScreen,
crossterm::cursor::Show,
crossterm::event::DisableMouseCapture
crossterm::cursor::Show
)
.unwrap();
disable_raw_mode().unwrap();
@@ -639,8 +638,7 @@ impl Tui {
execute!(
&mut backend,
EnterAlternateScreen,
crossterm::cursor::Hide,
crossterm::event::EnableMouseCapture
crossterm::cursor::Hide
)
.unwrap();
@@ -756,32 +754,17 @@ impl Tui {
_ => None
}
}
Event::Mouse(mouse) => {
if mouse.modifiers.contains(KeyModifiers::CONTROL)
&& self.is_kitty
&& self.zoom.is_some()
{
match mouse.kind {
MouseEventKind::ScrollUp =>
self.update_zoom(|z| z.level = z.level.saturating_add(1).min(0)),
MouseEventKind::ScrollDown =>
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::Mouse(mouse) => 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),
_ => None
}
@@ -847,7 +830,7 @@ impl Tui {
.border_set(border::ROUNDED)
.border_style(Color::Blue);
let help_span = Paragraph::new(HELP_PAGE).wrap(Wrap { trim: false });
let help_span = Text::raw(HELP_PAGE);
let max_w: u16 = HELP_PAGE
.lines()
@@ -880,31 +863,25 @@ impl Tui {
static HELP_PAGE: &str = "\
l, h, left, right:
Go forward/backwards a single page
Go forward/backwards a single page
j, k, down, up:
Go forwards/backwards a screen's worth of pages
Go forwards/backwards a screen's worth of pages
q, esc:
Quit
Quit
g:
Go to specific page (type numbers after 'g')
Go to specific page (type numbers after 'g')
/:
Search
Search
n, N:
Next/Previous search result
Next/Previous search result
i:
Invert colors
Invert colors
f:
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
Remove borders/fullscreen
?:
Show this page
Show this page
ctrl+z:
Suspend & background tdf \
Suspend & background tdf \
";
pub enum InputAction {