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 {