From 10c54b0056918d49805a3ed286940c6bb88e3551 Mon Sep 17 00:00:00 2001 From: Benjamin Grosse Date: Sat, 5 Aug 2023 14:34:27 +0100 Subject: [PATCH] Add window_size() for unix (#790) It is possible to render images in terminals with protocols such as Sixel, iTerm2's, or Kitty's. For a basic sixel or iTerm2 image printing, it is sufficient to print some escape sequence with the data, e.g. cat image just works, the image is displayed and enough lines are scrolled. But for more sophisticated usage of images, such as TUIs, it is necessary to know exactly what area that image would cover, in terms of columns/rows of characters. Then it would be possible to e.g. resize the image to a size that fits a col/row area precisely, not overdraw the image area, accommodate layouts, etc. Thus, provide the window size in pixel width/height, in addition to cols/rows. The windows implementation always returns a "not implemented" error. The windows API exposes a font-size, but in logical units, not pixels. This could be further extended to expose either "logical window size", or "pixel font size" and "logical font size". --- examples/is_tty.rs | 4 ++-- src/terminal.rs | 17 +++++++++++++++++ src/terminal/sys.rs | 8 +++++--- src/terminal/sys/unix.rs | 34 +++++++++++++++++++++++++++------- src/terminal/sys/windows.rs | 12 +++++++++++- 5 files changed, 62 insertions(+), 13 deletions(-) diff --git a/examples/is_tty.rs b/examples/is_tty.rs index 628d9a73f..85770e23b 100644 --- a/examples/is_tty.rs +++ b/examples/is_tty.rs @@ -6,9 +6,9 @@ use crossterm::{ use std::io::{stdin, stdout}; pub fn main() { - println!("{:?}", size().unwrap()); + println!("size: {:?}", size().unwrap()); execute!(stdout(), SetSize(10, 10)).unwrap(); - println!("{:?}", size().unwrap()); + println!("resized: {:?}", size().unwrap()); if stdin().is_tty() { println!("Is TTY"); diff --git a/src/terminal.rs b/src/terminal.rs index 127244dc4..49f413b14 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -137,6 +137,23 @@ pub fn size() -> io::Result<(u16, u16)> { sys::size() } +#[derive(Debug)] +pub struct WindowSize { + pub rows: u16, + pub columns: u16, + pub width: u16, + pub height: u16, +} + +/// Returns the terminal size `[WindowSize]`. +/// +/// The width and height in pixels may not be reliably implemented or default to 0. +/// For unix, https://man7.org/linux/man-pages/man4/tty_ioctl.4.html documents them as "unused". +/// For windows it is not implemented. +pub fn window_size() -> io::Result { + sys::window_size() +} + /// Disables line wrapping. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct DisableLineWrap; diff --git a/src/terminal/sys.rs b/src/terminal/sys.rs index 527e194ce..47e80a522 100644 --- a/src/terminal/sys.rs +++ b/src/terminal/sys.rs @@ -4,14 +4,16 @@ #[cfg(feature = "events")] pub use self::unix::supports_keyboard_enhancement; #[cfg(unix)] -pub(crate) use self::unix::{disable_raw_mode, enable_raw_mode, is_raw_mode_enabled, size}; +pub(crate) use self::unix::{ + disable_raw_mode, enable_raw_mode, window_size, is_raw_mode_enabled, size, +}; #[cfg(windows)] #[cfg(feature = "events")] pub use self::windows::supports_keyboard_enhancement; #[cfg(windows)] pub(crate) use self::windows::{ - clear, disable_raw_mode, enable_raw_mode, is_raw_mode_enabled, scroll_down, scroll_up, - set_size, set_window_title, size, + clear, disable_raw_mode, enable_raw_mode, window_size, is_raw_mode_enabled, scroll_down, + scroll_up, set_size, set_window_title, size, }; #[cfg(windows)] diff --git a/src/terminal/sys/unix.rs b/src/terminal/sys/unix.rs index 98d784b39..ed545c5b2 100644 --- a/src/terminal/sys/unix.rs +++ b/src/terminal/sys/unix.rs @@ -1,6 +1,9 @@ //! UNIX related logic for terminal manipulation. -use crate::terminal::sys::file_descriptor::{tty_fd, FileDesc}; +use crate::terminal::{ + sys::file_descriptor::{tty_fd, FileDesc}, + WindowSize, +}; use libc::{ cfmakeraw, ioctl, tcgetattr, tcsetattr, termios as Termios, winsize, STDOUT_FILENO, TCSANOW, TIOCGWINSZ, @@ -20,8 +23,19 @@ pub(crate) fn is_raw_mode_enabled() -> bool { TERMINAL_MODE_PRIOR_RAW_MODE.lock().is_some() } +impl From for WindowSize { + fn from(size: winsize) -> WindowSize { + WindowSize { + columns: size.ws_col, + rows: size.ws_row, + width: size.ws_xpixel, + height: size.ws_ypixel, + } + } +} + #[allow(clippy::useless_conversion)] -pub(crate) fn size() -> io::Result<(u16, u16)> { +pub(crate) fn window_size() -> io::Result { // http://rosettacode.org/wiki/Terminal_control/Dimensions#Library:_BSD_libc let mut size = winsize { ws_row: 0, @@ -38,11 +52,17 @@ pub(crate) fn size() -> io::Result<(u16, u16)> { STDOUT_FILENO }; - if wrap_with_result(unsafe { ioctl(fd, TIOCGWINSZ.into(), &mut size) }).is_ok() - && size.ws_col != 0 - && size.ws_row != 0 - { - return Ok((size.ws_col, size.ws_row)); + if wrap_with_result(unsafe { ioctl(fd, TIOCGWINSZ.into(), &mut size) }).is_ok() { + return Ok(size.into()); + } + + Err(std::io::Error::last_os_error().into()) +} + +#[allow(clippy::useless_conversion)] +pub(crate) fn size() -> io::Result<(u16, u16)> { + if let Ok(window_size) = window_size() { + return Ok((window_size.columns, window_size.rows)); } tput_size().ok_or_else(|| std::io::Error::last_os_error().into()) diff --git a/src/terminal/sys/windows.rs b/src/terminal/sys/windows.rs index ec671e74e..7ce92571f 100644 --- a/src/terminal/sys/windows.rs +++ b/src/terminal/sys/windows.rs @@ -9,7 +9,10 @@ use winapi::{ um::wincon::{SetConsoleTitleW, ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT, ENABLE_PROCESSED_INPUT}, }; -use crate::{cursor, terminal::ClearType}; +use crate::{ + cursor, + terminal::{ClearType, WindowSize}, +}; /// bits which can't be set in raw mode const NOT_RAW_MODE_MASK: DWORD = ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT; @@ -58,6 +61,13 @@ pub(crate) fn size() -> io::Result<(u16, u16)> { )) } +pub(crate) fn window_size() -> io::Result { + Err(io::Error::new( + io::ErrorKind::Unsupported, + "Window pixel size not implemented for the Windows API.", + )) +} + /// Queries the terminal's support for progressive keyboard enhancement. /// /// This always returns `Ok(false)` on Windows.