Skip to content

Commit

Permalink
Query window pixel size on Unix
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
swsnr committed Oct 16, 2020
1 parent fd476d5 commit e88bd82
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 32 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
7 changes: 2 additions & 5 deletions src/bin/mdcat/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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(),
};
Expand Down
6 changes: 3 additions & 3 deletions src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/render/write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ pub fn write_border<W: Write>(
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)
Expand Down
2 changes: 1 addition & 1 deletion src/terminal/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
130 changes: 111 additions & 19 deletions src/terminal/size.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PixelSize>,
}

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
// <https://github.com/rust-lang/libc/issues/1928>
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
/// <https://github.com/clap-rs/term_size-rs/issues/34> and
/// <https://github.com/clap-rs/term_size-rs/issues/33>.
#[cfg(unix)]
#[inline]
fn from_terminal_impl() -> Option<TerminalSize> {
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<TerminalSize> {
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<Size> {
///
/// Do not assume any knowledge about window size.
pub fn from_env() -> Option<Self> {
let columns = std::env::var("COLUMNS")
.ok()
.and_then(|value| value.parse::<usize>().ok());
Expand All @@ -40,18 +119,31 @@ impl Size {
.and_then(|value| value.parse::<usize>().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<Self> {
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<Size> {
term_size::dimensions()
.map(|(w, h)| Size::new(w, h))
.or_else(Size::from_env)
pub fn detect() -> Option<Self> {
Self::from_terminal().or_else(Self::from_env)
}
}
4 changes: 2 additions & 2 deletions src/terminal/terminology.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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 {
Expand Down

0 comments on commit e88bd82

Please sign in to comment.