mirror of
https://github.com/itsjunetime/tdf.git
synced 2026-06-02 08:01:47 -04:00
Compare commits
15 Commits
v0.4.1
...
v0.4.3_release
| Author | SHA1 | Date | |
|---|---|---|---|
| ff262cfb74 | |||
| 440515a3db | |||
| 690489016c | |||
| bd5554db27 | |||
| 45409bacd0 | |||
| 0481c14c4d | |||
| a78ea5a08c | |||
| f7eabc9af2 | |||
| 918c192047 | |||
| 8b03329bba | |||
| 7064be32f2 | |||
| 2a03294557 | |||
| 7c2c6484a6 | |||
| e65472e571 | |||
| 4fd2237b69 |
@@ -31,12 +31,12 @@ jobs:
|
||||
- name: Install clippy and fmt
|
||||
run: rustup component add clippy rustfmt
|
||||
- name: Clippy
|
||||
run: cargo clippy -- -D warnings
|
||||
run: cargo clippy --locked -- -D warnings
|
||||
- name: Tests
|
||||
run: cargo test
|
||||
run: cargo test --locked
|
||||
- name: Check fmt
|
||||
run: cargo fmt -- --check
|
||||
- name: Run tests
|
||||
run: cargo test --benches -- adobe_example
|
||||
- name: Run benchmarks as tests
|
||||
run: cargo test --locked --benches -- adobe_example
|
||||
- name: Build
|
||||
run: cargo build
|
||||
run: cargo build --locked
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# Unreleased
|
||||
|
||||
- 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
|
||||
|
||||
Generated
+346
-325
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tdf-viewer"
|
||||
version = "0.3.0"
|
||||
version = "0.4.2"
|
||||
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.85"
|
||||
rust-version = "1.86"
|
||||
|
||||
[[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 = { 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"] }
|
||||
|
||||
@@ -25,7 +25,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 build --release`
|
||||
3. Run `cargo +nightly build --release`
|
||||
|
||||
The binary should then be found at `./target/release/tdf`.
|
||||
|
||||
|
||||
+1
-1
Submodule ratatui-image updated: 375a9e190a...fe3d0b992b
+12
-6
@@ -26,6 +26,7 @@ pub enum MaybeTransferred {
|
||||
Transferred(kittage::ImageId)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ConvertedImage {
|
||||
Generic(Protocol),
|
||||
Kitty {
|
||||
@@ -139,14 +140,13 @@ pub async fn run_conversion_loop(
|
||||
let rn = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_nanos();
|
||||
.as_millis() % 1_000_000;
|
||||
|
||||
let mut img = if shms_work {
|
||||
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}")))?
|
||||
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}"))
|
||||
})?
|
||||
} else {
|
||||
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
|
||||
*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 {
|
||||
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_kittage_test_{pid}"))
|
||||
else {
|
||||
let Ok(mut k_img) = kittage::image::Image::shm_from(img, &format!("tdf_test_{pid}")) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
+75
-27
@@ -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,8 +56,27 @@ 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();
|
||||
|
||||
@@ -74,12 +95,24 @@ async fn 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
|
||||
required file: PathBuf
|
||||
optional file: PathBuf
|
||||
};
|
||||
|
||||
let path = flags
|
||||
.file
|
||||
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
|
||||
.canonicalize()
|
||||
.map_err(|e| WrappedErr(format!("Cannot canonicalize provided file: {e}").into()))?;
|
||||
|
||||
@@ -160,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,
|
||||
@@ -177,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,
|
||||
@@ -224,19 +280,6 @@ async fn 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()
|
||||
@@ -295,7 +338,8 @@ async fn main() -> Result<(), WrappedErr> {
|
||||
execute!(
|
||||
term.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
crossterm::cursor::Show
|
||||
crossterm::cursor::Show,
|
||||
crossterm::event::DisableMouseCapture
|
||||
)
|
||||
.unwrap();
|
||||
disable_raw_mode().unwrap();
|
||||
@@ -464,6 +508,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> {
|
||||
|
||||
+30
-13
@@ -624,7 +624,8 @@ impl Tui {
|
||||
execute!(
|
||||
&mut backend,
|
||||
LeaveAlternateScreen,
|
||||
crossterm::cursor::Show
|
||||
crossterm::cursor::Show,
|
||||
crossterm::event::DisableMouseCapture
|
||||
)
|
||||
.unwrap();
|
||||
disable_raw_mode().unwrap();
|
||||
@@ -638,7 +639,8 @@ impl Tui {
|
||||
execute!(
|
||||
&mut backend,
|
||||
EnterAlternateScreen,
|
||||
crossterm::cursor::Hide
|
||||
crossterm::cursor::Hide,
|
||||
crossterm::event::EnableMouseCapture
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -754,17 +756,32 @@ impl Tui {
|
||||
_ => 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::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::Resize(_, _) => Some(InputAction::Redraw),
|
||||
_ => None
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user