diff --git a/CHANGELOG.md b/CHANGELOG.md index d578315..3bb9d12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/main.rs b/src/main.rs index 454fef1..ccbd00b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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}, @@ -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 @@ -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. @@ -565,6 +579,89 @@ fn parse_color_to_i32(cs: &str) -> Result 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 { + 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 { + fn parse(ascii_hex_bytes: &[u8]) -> Option { + 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"); @@ -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); + } +}