275 lines
8.1 KiB
Rust
275 lines
8.1 KiB
Rust
//! UNIX related logic for terminal manipulation.
|
|
|
|
use crate::terminal::{
|
|
sys::file_descriptor::{tty_fd, FileDesc},
|
|
WindowSize,
|
|
};
|
|
use libc::{
|
|
cfmakeraw, ioctl, tcgetattr, tcsetattr, termios as Termios, winsize, STDOUT_FILENO, TCSANOW,
|
|
TIOCGWINSZ,
|
|
};
|
|
use parking_lot::Mutex;
|
|
use std::fs::File;
|
|
|
|
use std::os::unix::io::{IntoRawFd, RawFd};
|
|
|
|
use std::{io, mem, process};
|
|
|
|
// Some(Termios) -> we're in the raw mode and this is the previous mode
|
|
// None -> we're not in the raw mode
|
|
static TERMINAL_MODE_PRIOR_RAW_MODE: Mutex<Option<Termios>> = parking_lot::const_mutex(None);
|
|
|
|
pub(crate) fn is_raw_mode_enabled() -> bool {
|
|
TERMINAL_MODE_PRIOR_RAW_MODE.lock().is_some()
|
|
}
|
|
|
|
impl From<winsize> for WindowSize {
|
|
fn from(size: winsize) -> WindowSize {
|
|
WindowSize {
|
|
columns: size.ws_col,
|
|
rows: size.ws_row,
|
|
width: size.ws_xpixel,
|
|
height: size.ws_ypixel,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn fd_window_size(fd: i32) -> io::Result<WindowSize> {
|
|
let mut size = winsize {
|
|
ws_row: 0,
|
|
ws_col: 0,
|
|
ws_xpixel: 0,
|
|
ws_ypixel: 0,
|
|
};
|
|
|
|
#[allow(clippy::useless_conversion)]
|
|
if wrap_with_result(unsafe { ioctl(fd, TIOCGWINSZ.into(), &mut size) }).is_ok() {
|
|
return Ok(size.into());
|
|
}
|
|
|
|
Err(std::io::Error::last_os_error())
|
|
}
|
|
|
|
#[allow(clippy::useless_conversion)]
|
|
pub(crate) fn window_size() -> io::Result<WindowSize> {
|
|
// http://rosettacode.org/wiki/Terminal_control/Dimensions#Library:_BSD_libc
|
|
let mut size = winsize {
|
|
ws_row: 0,
|
|
ws_col: 0,
|
|
ws_xpixel: 0,
|
|
ws_ypixel: 0,
|
|
};
|
|
|
|
let file = File::open("/dev/tty").map(|file| (FileDesc::new(file.into_raw_fd(), true)));
|
|
let fd = if let Ok(file) = &file {
|
|
file.raw_fd()
|
|
} else {
|
|
// Fallback to libc::STDOUT_FILENO if /dev/tty is missing
|
|
STDOUT_FILENO
|
|
};
|
|
|
|
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())
|
|
}
|
|
|
|
pub(crate) fn fd_size(fd: i32) -> io::Result<(u16, u16)> {
|
|
fd_window_size(fd).map(|WindowSize { rows, columns, .. }| (columns, rows))
|
|
}
|
|
|
|
#[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())
|
|
}
|
|
|
|
pub(crate) fn fd_enable_raw_mode(fd: i32) -> io::Result<Termios> {
|
|
let mut ios = get_terminal_attr(fd)?;
|
|
let original_mode_ios = ios;
|
|
|
|
raw_terminal_attr(&mut ios);
|
|
set_terminal_attr(fd, &ios)?;
|
|
Ok(original_mode_ios)
|
|
}
|
|
|
|
pub(crate) fn enable_raw_mode() -> io::Result<()> {
|
|
let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock();
|
|
|
|
if original_mode.is_some() {
|
|
return Ok(());
|
|
}
|
|
|
|
let tty = tty_fd()?;
|
|
let fd = tty.raw_fd();
|
|
let mut ios = get_terminal_attr(fd)?;
|
|
let original_mode_ios = ios;
|
|
|
|
raw_terminal_attr(&mut ios);
|
|
set_terminal_attr(fd, &ios)?;
|
|
|
|
// Keep it last - set the original mode only if we were able to switch to the raw mode
|
|
*original_mode = Some(original_mode_ios);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn fd_disable_raw_mode(fd: i32, termios: Termios) -> io::Result<()> {
|
|
set_terminal_attr(fd, &termios)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Reset the raw mode.
|
|
///
|
|
/// More precisely, reset the whole termios mode to what it was before the first call
|
|
/// to [enable_raw_mode]. If you don't mess with termios outside of crossterm, it's
|
|
/// effectively disabling the raw mode and doing nothing else.
|
|
pub(crate) fn disable_raw_mode() -> io::Result<()> {
|
|
let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock();
|
|
|
|
if let Some(original_mode_ios) = original_mode.as_ref() {
|
|
let tty = tty_fd()?;
|
|
set_terminal_attr(tty.raw_fd(), original_mode_ios)?;
|
|
// Keep it last - remove the original mode only if we were able to switch back
|
|
*original_mode = None;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Queries the terminal's support for progressive keyboard enhancement.
|
|
///
|
|
/// On unix systems, this function will block and possibly time out while
|
|
/// [`crossterm::event::read`](crate::event::read()) or [`crossterm::event::poll`](crate::event::poll) are being called.
|
|
#[cfg(feature = "events")]
|
|
pub fn supports_keyboard_enhancement() -> io::Result<bool> {
|
|
if is_raw_mode_enabled() {
|
|
read_supports_keyboard_enhancement_raw()
|
|
} else {
|
|
read_supports_keyboard_enhancement_flags()
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "events")]
|
|
fn read_supports_keyboard_enhancement_flags() -> io::Result<bool> {
|
|
enable_raw_mode()?;
|
|
let flags = read_supports_keyboard_enhancement_raw();
|
|
disable_raw_mode()?;
|
|
flags
|
|
}
|
|
|
|
#[cfg(feature = "events")]
|
|
fn read_supports_keyboard_enhancement_raw() -> io::Result<bool> {
|
|
use crate::event::{
|
|
filter::{KeyboardEnhancementFlagsFilter, PrimaryDeviceAttributesFilter},
|
|
poll_internal, read_internal, InternalEvent,
|
|
};
|
|
use std::io::Write;
|
|
use std::time::Duration;
|
|
|
|
// This is the recommended method for testing support for the keyboard enhancement protocol.
|
|
// We send a query for the flags supported by the terminal and then the primary device attributes
|
|
// query. If we receive the primary device attributes response but not the keyboard enhancement
|
|
// flags, none of the flags are supported.
|
|
//
|
|
// See <https://sw.kovidgoyal.net/kitty/keyboard-protocol/#detection-of-support-for-this-protocol>
|
|
|
|
// ESC [ ? u Query progressive keyboard enhancement flags (kitty protocol).
|
|
// ESC [ c Query primary device attributes.
|
|
const QUERY: &[u8] = b"\x1B[?u\x1B[c";
|
|
|
|
let result = File::open("/dev/tty").and_then(|mut file| {
|
|
file.write_all(QUERY)?;
|
|
file.flush()
|
|
});
|
|
if result.is_err() {
|
|
let mut stdout = io::stdout();
|
|
stdout.write_all(QUERY)?;
|
|
stdout.flush()?;
|
|
}
|
|
|
|
loop {
|
|
match poll_internal(
|
|
Some(Duration::from_millis(2000)),
|
|
&KeyboardEnhancementFlagsFilter,
|
|
) {
|
|
Ok(true) => {
|
|
match read_internal(&KeyboardEnhancementFlagsFilter) {
|
|
Ok(InternalEvent::KeyboardEnhancementFlags(_current_flags)) => {
|
|
// Flush the PrimaryDeviceAttributes out of the event queue.
|
|
read_internal(&PrimaryDeviceAttributesFilter).ok();
|
|
return Ok(true);
|
|
}
|
|
_ => return Ok(false),
|
|
}
|
|
}
|
|
Ok(false) => {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::Other,
|
|
"The keyboard enhancement status could not be read within a normal duration",
|
|
));
|
|
}
|
|
Err(_) => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// execute tput with the given argument and parse
|
|
/// the output as a u16.
|
|
///
|
|
/// The arg should be "cols" or "lines"
|
|
fn tput_value(arg: &str) -> Option<u16> {
|
|
let output = process::Command::new("tput").arg(arg).output().ok()?;
|
|
let value = output
|
|
.stdout
|
|
.into_iter()
|
|
.filter_map(|b| char::from(b).to_digit(10))
|
|
.fold(0, |v, n| v * 10 + n as u16);
|
|
|
|
if value > 0 {
|
|
Some(value)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Returns the size of the screen as determined by tput.
|
|
///
|
|
/// This alternate way of computing the size is useful
|
|
/// when in a subshell.
|
|
fn tput_size() -> Option<(u16, u16)> {
|
|
match (tput_value("cols"), tput_value("lines")) {
|
|
(Some(w), Some(h)) => Some((w, h)),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
// Transform the given mode into an raw mode (non-canonical) mode.
|
|
fn raw_terminal_attr(termios: &mut Termios) {
|
|
unsafe { cfmakeraw(termios) }
|
|
}
|
|
|
|
fn get_terminal_attr(fd: RawFd) -> io::Result<Termios> {
|
|
unsafe {
|
|
let mut termios = mem::zeroed();
|
|
wrap_with_result(tcgetattr(fd, &mut termios))?;
|
|
Ok(termios)
|
|
}
|
|
}
|
|
|
|
fn set_terminal_attr(fd: RawFd, termios: &Termios) -> io::Result<()> {
|
|
wrap_with_result(unsafe { tcsetattr(fd, TCSANOW, termios) })
|
|
}
|
|
|
|
fn wrap_with_result(result: i32) -> io::Result<()> {
|
|
if result == -1 {
|
|
Err(io::Error::last_os_error())
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|