Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

- Added windows support! (thank you to [@jarjk](https://github.com/jarjk) for helping out!)
- Added keybindings (`0`/`$`) to scroll to left or right side of zoomed-in image ([#131](https://github.com/itsjunetime/tdf/pull/131), thank you [@IshDeshpa](https://github.com/IshDeshpa)!)
- Added `-t` flag to use terminal foreground/background colors in pdf rendering ([#138](https://github.com/itsjunetime/tdf/pull/138), thank you [@unorsk](https://github.com/unorsk)!
- Fixed issue with images clearing/flashing after displaying a certain number on kitty
- (Internal) decreased runtime footprint of tokio runtime

Expand Down
169 changes: 141 additions & 28 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use core::{
use std::{
borrow::Cow,
ffi::OsString,
io::{BufReader, Read as _, Stdout, Write as _, stdout},
io::{BufReader, IsTerminal as _, Read as _, Stdout, Write as _, stdout},
mem,
path::PathBuf,
sync::{Arc, Mutex},
Expand Down Expand Up @@ -116,6 +116,8 @@ 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
/// Use terminal foreground/background colors for the PDF
optional -t,--terminal-colors
/// Print the version and exit
optional --version
/// PDF file to read
Expand All @@ -137,37 +139,49 @@ async fn inner_main() -> Result<(), WrappedErr> {
.canonicalize()
.map_err(|e| WrappedErr(format!("Cannot canonicalize provided file: {e}").into()))?;

let black = flags
.black_color
.as_deref()
.map(|color| {
parse_color_to_i32(color).map_err(|e| {
WrappedErr(
format!(
"Couldn't parse black color {color:?}: {e} - is it formatted like a CSS color?"
if flags.terminal_colors && (flags.black_color.is_some() || flags.white_color.is_some()) {
return Err(WrappedErr(
"--terminal-colors cannot be combined with --black-color or --white-color".into()
));
}

let (black, white) = if flags.terminal_colors {
query_terminal_colors()
} else {
let black = flags
.black_color
.as_deref()
.map(|color| {
parse_color_to_i32(color).map_err(|e| {
WrappedErr(
format!(
"Couldn't parse black color {color:?}: {e} - is it formatted like a CSS color?"
)
.into()
)
.into()
)
})
})
})
.transpose()?
.unwrap_or(MUPDF_BLACK);

let white = flags
.white_color
.as_deref()
.map(|color| {
parse_color_to_i32(color).map_err(|e| {
WrappedErr(
format!(
"Couldn't parse white color {color:?}: {e} - is it formatted like a CSS color?"
.transpose()?
.unwrap_or(MUPDF_BLACK);

let white = flags
.white_color
.as_deref()
.map(|color| {
parse_color_to_i32(color).map_err(|e| {
WrappedErr(
format!(
"Couldn't parse white color {color:?}: {e} - is it formatted like a CSS color?"
)
.into()
)
.into()
)
})
})
})
.transpose()?
.unwrap_or(MUPDF_WHITE);
.transpose()?
.unwrap_or(MUPDF_WHITE);

(black, white)
};

// need to keep it around throughout the lifetime of the program, but don't rly need to use it.
// Just need to make sure it doesn't get dropped yet.
Expand Down Expand Up @@ -565,6 +579,89 @@ fn parse_color_to_i32(cs: &str) -> Result<i32, csscolorparser::ParseColorError>
Ok(i32::from_be_bytes([0, r, g, b]))
}

fn query_terminal_colors() -> (i32, i32) {
if !std::io::stdin().is_terminal() || !std::io::stdout().is_terminal() {
return (MUPDF_BLACK, MUPDF_WHITE);
}

let Ok(()) = enable_raw_mode() else {
return (MUPDF_BLACK, MUPDF_WHITE);
};

struct RawModeGuard;
impl Drop for RawModeGuard {
fn drop(&mut self) {
let _ = disable_raw_mode();
}
}
let _guard = RawModeGuard;

let stdin = std::io::stdin();
let mut handle = stdin.lock();

let fg = query_osc_color(10, &mut handle);
let bg = query_osc_color(11, &mut handle);
drop(handle);

(fg.unwrap_or(MUPDF_BLACK), bg.unwrap_or(MUPDF_WHITE))
}

fn query_osc_color(osc: u8, handle: &mut std::io::StdinLock<'_>) -> Option<i32> {
print!("\x1b]{osc};?\x1b\\");
std::io::stdout().flush().ok()?;

// response looks like "\u{1b}]10;rgb:rrrr/bbbb/gggg\u{1b}\\" or "\u{1b}]10;rgb:rr/bb/gg\u{1b}\\"
// or if osc is 11, "\u{1b}]11;rgb:rrrr/bbbb/gggg\u{1b}\\" or "\u{1b}]11;rgb:rr/bb/gg\u{1b}\\"

// We expect the response to be either 19 or 25 bytes.
let mut response_buf = [0u8; 25];

handle.read_exact(&mut response_buf[..19])
.inspect_err(|e| eprintln!("Couldn't get a response from your terminal for querying OSC {osc}; please file a bug with tdf with your terminal emulator (underlying err: {e})"))
.ok()?;

let two_digit_colors = response_buf[11] == b'/';

if !two_digit_colors {
handle.read_exact(&mut response_buf[19..])
.inspect_err(|e| eprintln!("Couldn't get a response from your terminal for querying OSC {osc}; please file a bug with tdf with your terminal emulator (underlying err: {e})"))
.ok()?;
}

osc_response_buf_to_color(response_buf)
}

fn osc_response_buf_to_color(response_buf: [u8; 25]) -> Option<i32> {
fn parse(ascii_hex_bytes: &[u8]) -> Option<u8> {
let mut color = 0;

for byte in ascii_hex_bytes {
color = (color << 4)
+ (match byte {
b'0'..=b'9' => byte - b'0',
b'A'..=b'F' => byte - (b'A' - 10),
b'a'..=b'f' => byte - (b'a' - 10),
_ => return None
});
}

Some(color)
}

let two_digit_colors = response_buf[11] == b'/';

// this is minimimal validation.
// the response either has `rrrr/gggg/bbbb` or `rr/gg/bb` starting at byte 9.
// So we grab the first two digits out of the r, g, and b sections (since we can only support
// 8-bit colors with mupdf; we can't do the 16-bits that the terminal might respond with), we
// parse them, and we return it as an i32.
let r = parse(&response_buf[9..11])?;
let g = parse(&response_buf[if two_digit_colors { 12..14 } else { 14..16 }])?;
let b = parse(&response_buf[if two_digit_colors { 15..17 } else { 19..21 }])?;

Some(i32::from_be_bytes([0, r, g, b]))
}

fn get_font_size_through_stdio() -> Result<(u16, u16), WrappedErr> {
// send the command code to get the terminal window size
print!("\x1b[14t");
Expand Down Expand Up @@ -631,3 +728,19 @@ fn get_font_size_through_stdio() -> Result<(u16, u16), WrappedErr> {

Ok((w, h))
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn osc_response_parsing() {
fn eq(bytes: [u8; 25], r: u8, g: u8, b: u8) {
let resp = osc_response_buf_to_color(bytes);
assert_eq!(resp, Some(i32::from_be_bytes([0, r, g, b])));
}

eq(*b"\x1b]10;rgb:11/22/33\x1b\\000000", 0x11, 0x22, 0x33);
eq(*b"\x1b]11;rgb:aa11/23bf/fff0\x1b1", 0xaa, 0x23, 0xff);
}
}
Loading