Compare commits

..

24 Commits

Author SHA1 Message Date
itsjunetime 15b1a99d76 Add changelog entry 2025-11-06 19:23:39 -06:00
itsjunetime 170bdd3869 Fix weird cropping when zooming out too much with kitty 2025-11-06 19:22:52 -06:00
June 7551c4dba3 Update deps and fmt (#110) 2025-11-06 18:50:19 -06:00
itsjunetime e61eb9b846 Fix r_to_l argument as well to just be a flag 2025-11-06 18:21:18 -06:00
tatounee 6b37976357 Made flags -f, --fullscreen not taking argument (#109)
Now, its presence alone is enough to activate the full-screen option.
2025-11-06 18:18:57 -06:00
June 3628d21c74 Switch to stable base64 simd library (#105) 2025-09-29 17:00:29 -05:00
itsjunetime f4f3b4f539 Actually update package Cargo.toml ugh 2025-09-15 10:05:20 -05:00
itsjunetime a79c534e97 Actually mark release on CHANGELOG 2025-09-07 20:33:43 -05:00
June 971393892a v0.4.3 release 2025-09-07 20:32:16 -05:00
June 440515a3db Actually query terminals and fix terminals if we fail initialization (#103) 2025-09-06 12:02:04 -05:00
itsjunetime 690489016c fmt 2025-09-05 11:15:58 -05:00
itsjunetime bd5554db27 Update mupdf to use git dependency so it works on windows 2025-09-05 10:34:46 -05:00
itsjunetime 45409bacd0 fix CI? 2025-09-05 10:15:49 -05:00
itsjunetime 0481c14c4d fix CI 2025-09-05 08:41:38 -05:00
itsjunetime a78ea5a08c Update deps for new kittage 2025-09-05 08:31:03 -05:00
June f7eabc9af2 Require CI to run with locked flag so lockfile is always in sync (#100) 2025-08-29 09:17:14 -05:00
itsjunetime 918c192047 Add changelog entry for ctrl+scroll zooming 2025-08-20 21:59:29 -05:00
Per Hurtig 8b03329bba Add ctrl+mousewheel zoom control for fill-screen mode (#94)
* Add ctrl+mousewheel zoom control for fill-screen mode

  Enables mouse-based zooming; Uses ctrl+scroll up/down to
  increase/decrease zoom level while in fill-screen mode, with proper
  mouse capture handling.

* removed unused include
2025-08-20 21:57:55 -05:00
itsjunetime 7064be32f2 Update deps including ratatui-image 2025-08-20 09:23:49 -05:00
itsjunetime 2a03294557 Release version 0.4.2 2025-08-18 10:18:26 -05:00
Per Hurtig 7c2c6484a6 Fix macOS shared memory filename length limit (fixes issue #92) (#93)
* Fix macOS shared memory filename length limit

Shorten shared memory object names from "__tdf_kittage_{pid}_page_{rn}_{page_num}"
to "tdf_{pid}_{rn}_{page_num}" and change timestamp from nanoseconds to
milliseconds % 1M to stay under macOS's 31-character limit for shm names.

Fixes "File name too long (os error 63)" error when rendering PDFs on macOS.

On macOS, SHM_NAME_MAX: 30

* Fix macOS shared memory filename length limit

Shorten shared memory object names from "__tdf_kittage_{pid}_page_{rn}_{page_num}"
to "tdf_{pid}_{rn}_{page_num}" and change timestamp from nanoseconds to
milliseconds % 1M to stay under macOS's 31-character limit for shm names.

Fixes "File name too long (os error 63)" error when rendering PDFs on macOS.

SHM_NAME_MAX: 30
2025-08-18 10:15:19 -05:00
itsjunetime e65472e571 Add --version flag 2025-08-15 09:43:52 -05:00
itsjunetime 4fd2237b69 Specify on README to use nightly when building even though it should be detected by rust-toolchain.toml 2025-08-11 21:26:17 -05:00
itsjunetime 69fd8ec7e8 Add instructions for zooming and such to help page 2025-08-09 15:58:56 -06:00
12 changed files with 800 additions and 804 deletions
+5 -5
View File
@@ -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
+20
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+6 -7
View File
@@ -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"
+3 -1
View File
@@ -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
+12 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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| {
+4 -6
View File
@@ -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,8 +536,7 @@ 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());
@@ -552,8 +551,7 @@ 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();
+45 -17
View File
@@ -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}");
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 { if zoom.level < 0 {
img_area = Rect { let old_width = img_area.width;
width: img_area img_area.width = img_area
.width .width
.saturating_sub((zoom.level * 2).unsigned_abs()) .saturating_sub((zoom.level * 2).unsigned_abs())
.max(1), .max((f32::from(img_area.height) * img_aspect_ratio) as u16);
x: img_area.x + (zoom.level.unsigned_abs().min(img_area.width / 2)), img_area.x += (old_width - img_area.width) / 2;
..img_area
}
}
log::debug!("after adjustment, img_area is {img_area:#?}"); 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. // 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_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,7 +761,20 @@ impl Tui {
_ => None _ => None
} }
} }
Event::Mouse(mouse) => match mouse.kind { 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 => MouseEventKind::ScrollRight =>
self.change_page(PageChange::Next, ChangeAmount::Single), self.change_page(PageChange::Next, ChangeAmount::Single),
MouseEventKind::ScrollDown => MouseEventKind::ScrollDown =>
@@ -764,13 +784,15 @@ impl Tui {
MouseEventKind::ScrollUp => MouseEventKind::ScrollUp =>
self.change_page(PageChange::Prev, ChangeAmount::WholeScreen), self.change_page(PageChange::Prev, ChangeAmount::WholeScreen),
_ => None _ => 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()
@@ -878,6 +900,12 @@ 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: