From e88bd8205b1ac21963b88f7f254d3e404f436983 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesner Date: Fri, 16 Oct 2020 10:24:46 +0200 Subject: [PATCH] Query window pixel size on Unix mdcat needs the window size in pixels to correctly draw images in some terminals. Unfortunately term_size doesn't expose ws_xpixel/ws_ypixel from the winsize struct so we now do the ioctl ourselves. This effectively gets rid of term_size on Unix system which has the added benefit that we can now freely choose which device to query instead of being limited to stdin, stdout and stderr. We use this opportunity to query the controlling terminal directly, via ctermid. This implies that mdcat correctly detects the terminal size even when all standard streams are redirected. --- CHANGELOG.md | 4 ++ Cargo.lock | 1 + Cargo.toml | 7 +- src/bin/mdcat/main.rs | 7 +- src/render/mod.rs | 6 +- src/render/write.rs | 2 +- src/terminal/mod.rs | 2 +- src/terminal/size.rs | 130 ++++++++++++++++++++++++++++++------ src/terminal/terminology.rs | 4 +- 9 files changed, 131 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d019e096..5004f6c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,14 @@ To publish a new release run `scripts/release` from the project directory. Kitty supports hyperlinks since [version 0.19][kitty-0.19], see [Kitty GH-68]. Note that `mdcat` *unconditionally* prints hyperlinks if it detects a kitty terminal. It makes no attempt to detect whether the Kitty version is compatible or the [`allow_hyperlinks`] setting is enabled. + +### Changed +- `mdcat` now asks the controlling terminal for the terminal size and thus correctly detects the terminal size even if standard input, standard output and standard error are all redirected (see [GH-166]). [kitty-0.19]: https://sw.kovidgoyal.net/kitty/changelog.html#id2 [kitty GH-68]: https://github.com/kovidgoyal/kitty/issues/68 [`allow_hyperlinks`]: https://sw.kovidgoyal.net/kitty/conf.html?highlight=hyperlinks#opt-kitty.allow_hyperlinks +[GH-166]: https://github.com/lunaryorn/mdcat/pull/166 ## [0.21.1] – 2020-09-01 diff --git a/Cargo.lock b/Cargo.lock index dc4bcf85..32d40e43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -734,6 +734,7 @@ dependencies = [ "goldenfile", "image", "lazy_static", + "libc", "mime", "pretty_assertions", "pulldown-cmark", diff --git a/Cargo.toml b/Cargo.toml index cb18059a..2e80da40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,6 @@ base64 = "^0.12" gethostname = "^0.2" image = "^0.23" mime = "^0.3" -term_size = "^0.3" url = "^2.1" fehler = "^1" anyhow = "^1" @@ -50,6 +49,12 @@ version = "^4.4" default-features = false features = ["parsing", "assets", "dump-load", "regex-fancy"] +[target.'cfg(unix)'.dependencies] +libc = "^0.2" + +[target.'cfg(windows)'.dependencies] +term_size = "^0.3" + [dev-dependencies] pretty_assertions = "^0.6" goldenfile = "^1.1" diff --git a/src/bin/mdcat/main.rs b/src/bin/mdcat/main.rs index ff9d4e92..ff3fcc6e 100644 --- a/src/bin/mdcat/main.rs +++ b/src/bin/mdcat/main.rs @@ -143,7 +143,7 @@ impl Arguments { fn main() { let size = TerminalSize::detect().unwrap_or_default(); - let columns = size.width.to_string(); + let columns = size.columns.to_string(); let matches = args::app(&columns).get_matches(); let arguments = Arguments::from_matches(&matches).unwrap_or_else(|e| e.exit()); @@ -166,10 +166,7 @@ fn main() { Ok(mut output) => { let settings = Settings { terminal_capabilities, - terminal_size: TerminalSize { - width: columns, - ..size - }, + terminal_size: TerminalSize { columns, ..size }, resource_access, syntax_set: SyntaxSet::load_defaults_newlines(), }; diff --git a/src/render/mod.rs b/src/render/mod.rs index a03bcc28..24142669 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -97,7 +97,7 @@ pub fn write_event<'a, W: Write>( write_rule( writer, &settings.terminal_capabilities, - settings.terminal_size.width, + settings.terminal_size.columns, )?; writeln!(writer)?; TopLevel(TopLevelAttrs::margin_before()).and_data(data) @@ -172,7 +172,7 @@ pub fn write_event<'a, W: Write>( write_rule( writer, &settings.terminal_capabilities, - settings.terminal_size.width - (attrs.indent as usize), + settings.terminal_size.columns - (attrs.indent as usize), )?; writeln!(writer)?; stack @@ -291,7 +291,7 @@ pub fn write_event<'a, W: Write>( write_rule( writer, &settings.terminal_capabilities, - settings.terminal_size.width - (attrs.indent as usize), + settings.terminal_size.columns - (attrs.indent as usize), )?; writeln!(writer)?; stack diff --git a/src/render/write.rs b/src/render/write.rs index 3dca7cb8..4d9d31cb 100644 --- a/src/render/write.rs +++ b/src/render/write.rs @@ -64,7 +64,7 @@ pub fn write_border( capabilities: &TerminalCapabilities, terminal_size: &TerminalSize, ) -> std::io::Result<()> { - let separator = "\u{2500}".repeat(terminal_size.width.min(20)); + let separator = "\u{2500}".repeat(terminal_size.columns.min(20)); let style = Style::new().fg(Colour::Green); write_styled(writer, capabilities, &style, separator)?; writeln!(writer) diff --git a/src/terminal/mod.rs b/src/terminal/mod.rs index 026ab72c..26230964 100644 --- a/src/terminal/mod.rs +++ b/src/terminal/mod.rs @@ -18,7 +18,7 @@ mod osc; mod terminology; pub use self::ansi::AnsiStyle; -pub use self::size::Size as TerminalSize; +pub use self::size::TerminalSize; /// The capability of basic styling. #[derive(Debug, Copy, Clone)] diff --git a/src/terminal/size.rs b/src/terminal/size.rs index 0268e474..d0daa069 100644 --- a/src/terminal/size.rs +++ b/src/terminal/size.rs @@ -6,32 +6,111 @@ //! Terminal size. -/// The size of a text terminal, in characters and lines. +/// The size of a terminal window in pixels #[derive(Debug, Copy, Clone)] -pub struct Size { +pub struct PixelSize { + /// The width of the window, in pixels. + pub x: u32, + // The height of the window, in pixels. + pub y: u32, +} + +/// The size of a terminal. +#[derive(Debug, Copy, Clone)] +pub struct TerminalSize { /// The width of the terminal, in characters aka columns. - pub width: usize, + pub columns: usize, /// The height of the terminal, in lines. - pub height: usize, + pub rows: usize, + /// The size in pixels, if available. + pub pixels: Option, } -impl Default for Size { - /// A good default size assumption for a terminal: 80x24. - fn default() -> Size { - Size { - width: 80, - height: 24, +impl Default for TerminalSize { + fn default() -> Self { + TerminalSize { + columns: 80, + rows: 24, + pixels: None, } } } -impl Size { - fn new(width: usize, height: usize) -> Size { - Size { width, height } +#[cfg(unix)] +extern "C" { + // Need to wrap ctermid explicitly because it's not (yet?) in libc, see + // + pub fn ctermid(c: *mut libc::c_char) -> *mut libc::c_char; +} + +/// Query terminal size on Unix. +/// +/// Open the underlying controlling terminal via ctermid and open, and issue a +/// TIOCGWINSZ ioctl to the device. +/// +/// We do this manually because term_size currently neither exports the pixel +/// size nor queries the controlling terminal, see +/// and +/// . +#[cfg(unix)] +#[inline] +fn from_terminal_impl() -> Option { + unsafe { + let mut winsize = libc::winsize { + ws_row: 0, + ws_col: 0, + ws_xpixel: 0, + ws_ypixel: 0, + }; + // ctermid uses a static buffer if given NULL. This isn't thread safe but + // a) we open the path right away, and b) a process only has a single + // controlling terminal anyway, so we're pretty safe here I guess. + let cterm_path = ctermid(std::ptr::null_mut()); + if cterm_path.is_null() { + None + } else { + let fd = libc::open(cterm_path, libc::O_RDONLY); + let result = libc::ioctl(fd, libc::TIOCGWINSZ, &mut winsize); + libc::close(fd); + if result == -1 || winsize.ws_row == 0 || winsize.ws_col == 0 { + None + } else { + Some(winsize) + } + } } + .map(|winsize| { + let window = if winsize.ws_xpixel != 0 && winsize.ws_ypixel != 0 { + Some(PixelSize { + x: winsize.ws_xpixel as u32, + y: winsize.ws_ypixel as u32, + }) + } else { + None + }; + TerminalSize { + columns: winsize.ws_col as usize, + rows: winsize.ws_row as usize, + pixels: window, + } + }) +} +#[cfg(windows)] +#[inline] +unsafe fn from_terminal_impl() -> Option { + term_size::dimensions().map(|(w, h)| TerminalSize { + rows: h, + columns: w, + pixels: None, + }) +} + +impl TerminalSize { /// Get terminal size from `$COLUMNS` and `$LINES`. - pub fn from_env() -> Option { + /// + /// Do not assume any knowledge about window size. + pub fn from_env() -> Option { let columns = std::env::var("COLUMNS") .ok() .and_then(|value| value.parse::().ok()); @@ -40,18 +119,31 @@ impl Size { .and_then(|value| value.parse::().ok()); match (columns, rows) { - (Some(columns), Some(rows)) => Some(Size::new(columns, rows)), + (Some(columns), Some(rows)) => Some(Self { + columns, + rows, + pixels: None, + }), _ => None, } } + /// Detect the terminal size by querying the underlying terminal. + /// + /// On unix this issues a ioctl to the controlling terminal. + /// + /// On Windows this uses the [term_size] crate which does some magic windows API calls. + /// + /// [term_size]: https://docs.rs/term_size/ + pub fn from_terminal() -> Option { + from_terminal_impl() + } + /// Detect the terminal size. /// /// Get the terminal size from the underlying TTY, and fallback to /// `$COLUMNS` and `$LINES`. - pub fn detect() -> Option { - term_size::dimensions() - .map(|(w, h)| Size::new(w, h)) - .or_else(Size::from_env) + pub fn detect() -> Option { + Self::from_terminal().or_else(Self::from_env) } } diff --git a/src/terminal/terminology.rs b/src/terminal/terminology.rs index 2b7f84a1..374ba342 100644 --- a/src/terminal/terminology.rs +++ b/src/terminal/terminology.rs @@ -53,7 +53,7 @@ impl TerminologyImages { // We need to compute image proportion to draw the appropriate // rectangle. If we can't compute the image proportion (e.g. it's an // external URL), we fallback to a rectangle that is half of the screen. - let columns = max_size.width; + let columns = max_size.columns; let lines = Some(url) .filter(|url| url.scheme() == "file") @@ -65,7 +65,7 @@ impl TerminologyImages { // 1:2 proportion (h * (columns / 2) as f64 / w) as usize }) - .unwrap_or(max_size.height / 2); + .unwrap_or(max_size.rows / 2); let mut command = format!("\x1b}}ic#{};{};{}\x00", columns, lines, url.as_str()); for _ in 0..lines {