Skip to content

Commit 9ae549a

Browse files
Shem TorgaShem Torga
authored andcommitted
Clean merge: Update deps, implement gg/G keybinds, fix issues
1 parent 3628d21 commit 9ae549a

6 files changed

Lines changed: 323 additions & 109 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
/target
22
debug.log
3+
.DS_Store

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# Unreleased
22

33
- Switched simd base64 crate for one that works on stable (from `vb64` to `base64_simd`)
4+
- Allow boolean arguments to function as flags, without a `true` or `false` argument following the flag itself
5+
- Fix cropping issues when zooming out too much while using kitty protocol
6+
- Added `gg` and `G` keybindings for scrolling to the top and bottom of a page, respectively, when filling the width of the screen with kitty
7+
- Updated help page to only show kitty keybindings when you're actually using kitty
48

59
# v0.4.3
610

Cargo.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,19 @@ xflags = "0.4.0-pre.2"
3939
mimalloc = "0.1.43"
4040
nix = { version = "0.30.0", features = ["signal"] }
4141
mupdf = { git = "https://github.com/messense/mupdf-rs.git", rev = "2e0fae910fac8048c7008211fc4d3b9f5d227a07", default-features = false, features = ["svg", "system-fonts", "img"] }
42+
webbrowser = "0.6"
4243
rayon = { version = "*", default-features = false }
4344
# kittage = { path = "../kittage/", features = ["crossterm-tokio", "image-crate", "log"] }
4445
kittage = { git = "https://github.com/itsjunetime/kittage.git", features = ["crossterm-tokio", "image-crate", "log"] }
4546
memmap2 = "*"
47+
csscolorparser = { version = "0.8.0", default-features = false }
4648

4749
# logging
4850
log = "0.4.27"
4951
flexi_logger = "0.31"
5052

5153
# for tracing with tokio-console
52-
console-subscriber = { version = "0.4.0", optional = true }
53-
csscolorparser = { version = "0.7.0" }
54+
console-subscriber = { version = "0.5.0", optional = true }
5455

5556
[profile.production]
5657
inherits = "release"
@@ -92,7 +93,7 @@ checked_conversions = "warn"
9293
copy_iterator = "warn"
9394
default_trait_access = "warn"
9495
doc_link_with_quotes = "warn"
95-
empty_enum = "warn"
96+
empty_enums = "warn"
9697
explicit_into_iter_loop = "warn"
9798
explicit_iter_loop = "warn"
9899
filter_map_next = "warn"

src/main.rs

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ use tdf::{
3535
PrerenderLimit,
3636
converter::{ConvertedPage, ConverterMsg, run_conversion_loop},
3737
kitty::{KittyDisplay, display_kitty_images, do_shms_work, run_action},
38-
renderer::{self, RenderError, RenderInfo, RenderNotif},
38+
renderer::{self, LinkTarget, RenderError, RenderInfo, RenderNotif},
3939
tui::{BottomMessage, InputAction, MessageSetting, Tui}
4040
};
4141

@@ -83,11 +83,11 @@ async fn inner_main() -> Result<(), WrappedErr> {
8383
let flags = xflags::parse_or_exit! {
8484
/// Display the pdf with the pages starting at the right hand size and moving left and
8585
/// adjust input keys to match
86-
optional -r,--r-to-l r_to_l: bool
86+
optional -r,--r-to-l
8787
/// The maximum number of pages to display together, horizontally, at a time
8888
optional -m,--max-wide max_wide: NonZeroUsize
8989
/// Fullscreen the pdf (hide document name, page count, etc)
90-
optional -f,--fullscreen fullscreen: bool
90+
optional -f,--fullscreen
9191
/// The number of pages to prerender surrounding the currently-shown page; 0 means no
9292
/// limit. By default, there is no limit.
9393
optional -p,--prerender prerender: usize
@@ -267,12 +267,7 @@ async fn inner_main() -> Result<(), WrappedErr> {
267267
|| "Unknown file".into(),
268268
|n| n.to_string_lossy().to_string()
269269
);
270-
let tui = Tui::new(
271-
file_name,
272-
flags.max_wide,
273-
flags.r_to_l.unwrap_or_default(),
274-
is_kitty
275-
);
270+
let tui = Tui::new(file_name, flags.max_wide, flags.r_to_l, is_kitty);
276271

277272
let backend = CrosstermBackend::new(std::io::stdout());
278273
let mut term = Terminal::new(backend).map_err(|e| {
@@ -300,7 +295,7 @@ async fn inner_main() -> Result<(), WrappedErr> {
300295
})?;
301296
}
302297

303-
let fullscreen = flags.fullscreen.unwrap_or_default();
298+
let fullscreen = flags.fullscreen;
304299
let main_area = Tui::main_layout(&term.get_frame(), fullscreen);
305300
to_renderer
306301
.send(RenderNotif::Area(main_area.page_area))
@@ -376,6 +371,41 @@ async fn enter_redraw_loop(
376371
None => needs_redraw = false,
377372
Some(action) => match action {
378373
InputAction::Redraw => (),
374+
InputAction::Click { col, row } => {
375+
if let Some((page_num, mupdf_x, mupdf_y)) = tui.map_click_to_page(col, row, &main_area, font_size) {
376+
let (resp_tx, resp_rx) = flume::bounded(1);
377+
to_renderer.send(RenderNotif::QueryLinkAt {
378+
page: page_num,
379+
mupdf_x_px: mupdf_x,
380+
mupdf_y_px: mupdf_y,
381+
resp: resp_tx
382+
})?;
383+
// await the renderer reply; the renderer now returns
384+
// Result<Option<LinkTarget>, String> so we can display errors
385+
match resp_rx.recv_async().await {
386+
Ok(Ok(Some(LinkTarget::Uri(uri)))) => {
387+
if let Err(e) = webbrowser::open(&uri) {
388+
tui.set_msg(MessageSetting::Some(BottomMessage::Error(
389+
format!("Failed to open uri {}: {e}", uri)
390+
)));
391+
}
392+
}
393+
Ok(Ok(Some(LinkTarget::GoTo { page_index }))) => {
394+
to_renderer.send(RenderNotif::JumpToPage(page_index))?;
395+
to_converter.send(ConverterMsg::GoToPage(page_index))?;
396+
}
397+
Ok(Ok(None)) => {},
398+
Ok(Err(err_str)) => {
399+
tui.set_msg(MessageSetting::Some(BottomMessage::Error(err_str)));
400+
}
401+
Err(e) => {
402+
tui.set_msg(MessageSetting::Some(BottomMessage::Error(
403+
format!("Failed to receive link response from renderer: {e}")
404+
)));
405+
}
406+
}
407+
}
408+
},
379409
InputAction::QuitApp => return Ok(()),
380410
InputAction::JumpingToPage(page) => {
381411
to_renderer.send(RenderNotif::JumpToPage(page))?;

src/renderer.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ const KITTY_MAX_W_OR_H: f32 = 10_000.0;
1616
pub enum RenderNotif {
1717
Area(Rect),
1818
JumpToPage(usize),
19+
/// Query: which link (if any) is under the provided device pixel coords (relative to the
20+
/// top-left of the rendered page image). The renderer replies on the provided Sender with
21+
/// Some(LinkTarget) if a link was found, otherwise None.
22+
QueryLinkAt {
23+
page: usize,
24+
mupdf_x_px: f32,
25+
mupdf_y_px: f32,
26+
// Send back either the found link (Some) or None, or an Err(String) describing an
27+
// internal failure. If sending on this channel fails it indicates the receiver has
28+
// disconnected (main thread exited) and we should panic.
29+
resp: Sender<Result<Option<LinkTarget>, String>>
30+
},
1931
PageNeedsReRender(usize),
2032
Search(String),
2133
SwitchFitOrFill(FitOrFill),
@@ -37,6 +49,15 @@ pub enum RenderInfo {
3749
Reloaded
3850
}
3951

52+
/// A link target discovered on a page.
53+
#[derive(Debug, Clone)]
54+
pub enum LinkTarget {
55+
/// External URI to open in a browser
56+
Uri(String),
57+
/// Internal document destination: page index (0-based)
58+
GoTo { page_index: usize }
59+
}
60+
4061
#[derive(Clone)]
4162
pub struct PageInfo {
4263
pub img_data: ImageData,
@@ -66,6 +87,46 @@ pub fn fill_default<T: Default>(vec: &mut Vec<T>, size: usize) {
6687
vec.resize_with(size, T::default);
6788
}
6889

90+
/// Query which link (if any) is under the provided mupdf device coordinates for the given
91+
/// page. Returns Ok(Some(LinkTarget)) if a link is found, Ok(None) if no link, or
92+
/// Err(mupdf::error::Error) on mupdf failures.
93+
fn query_link_at(
94+
doc: &Document,
95+
qpage: usize,
96+
mupdf_x_px: f32,
97+
mupdf_y_px: f32,
98+
area_w: f32,
99+
area_h: f32,
100+
fit_or_fill: FitOrFill,
101+
) -> Result<Option<LinkTarget>, mupdf::error::Error> {
102+
// load the requested page and compute same scale as used when rendering
103+
let page = doc.load_page(qpage as i32)?;
104+
let bounds = page.bounds()?;
105+
let page_dim = (bounds.x1 - bounds.x0, bounds.y1 - bounds.y0);
106+
let scaled = scale_img_for_area(page_dim, (area_w, area_h), fit_or_fill);
107+
let scale_factor = scaled.scale_factor;
108+
109+
let pdf_x = mupdf_x_px / scale_factor;
110+
let pdf_y = mupdf_y_px / scale_factor;
111+
112+
if let Ok(links) = page.links() {
113+
for link in links {
114+
let lb = link.bounds;
115+
if pdf_x >= lb.x0 && pdf_x <= lb.x1 && pdf_y >= lb.y0 && pdf_y <= lb.y1 {
116+
// prefer URI if present and non-empty
117+
if !link.uri.is_empty() {
118+
return Ok(Some(LinkTarget::Uri(link.uri)));
119+
}
120+
if let Some(dest) = link.dest {
121+
return Ok(Some(LinkTarget::GoTo { page_index: dest.loc.page_number as usize }));
122+
}
123+
}
124+
}
125+
}
126+
127+
Ok(None)
128+
}
129+
69130
// this function has to be sync (non-async) because the mupdf::Document needs to be held during
70131
// most of it, but that's basically just a wrapper around `*c_void` cause it's just a binding to C
71132
// code, so it's !Send and thus can't be held across await points. So we can't call any of the
@@ -186,6 +247,18 @@ pub fn start_rendering(
186247
macro_rules! handle_notif {
187248
($notif:ident) => {{
188249
match $notif {
250+
RenderNotif::QueryLinkAt { page: qpage, mupdf_x_px, mupdf_y_px, resp } => {
251+
match query_link_at(&doc, qpage, mupdf_x_px, mupdf_y_px, area_w, area_h, fit_or_fill) {
252+
Ok(t) => resp.send(Ok(t)),
253+
Err(e) => {
254+
let err_str = format!("Failed to query links: {e}");
255+
resp.send(Err(err_str))
256+
}
257+
}
258+
.unwrap_or_else(|e| {
259+
panic!("Renderer failed to send link-query response: {e}")
260+
});
261+
}
189262
RenderNotif::Reload => continue 'reload,
190263
RenderNotif::Invert => {
191264
invert = !invert;

0 commit comments

Comments
 (0)