1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-23 21:32:13 +03:00

termwiz: refactor terminal probing

Factor out the probe_screen_size method from the Terminal
trait and put it into a ProbeCapabilities struct.

This makes it easier to introduce other sorts of probed
information without making the Terminal trait grow all sorts
of additional methods.

A Terminal may choose not to support probing, which it cannot
if it doesn't have Read + Write handles to an underlying terminal
(such as some special cases in wezterm).
This commit is contained in:
Wez Furlong 2023-07-16 06:49:52 -07:00
parent abc92e56e0
commit feb9e11b33
No known key found for this signature in database
GPG Key ID: 7A7F66A31EC9B387
9 changed files with 257 additions and 179 deletions

View File

@ -511,10 +511,6 @@ fn connect_ssh_session(
})
}
fn probe_screen_size(&mut self) -> termwiz::Result<ScreenSize> {
self.get_screen_size()
}
fn set_screen_size(&mut self, _size: ScreenSize) -> termwiz::Result<()> {
termwiz::bail!("TerminalShim cannot set screen size");
}

View File

@ -397,10 +397,6 @@ impl termwiz::terminal::Terminal for TermWizTerminal {
Ok(self.render_tx.screen_size)
}
fn probe_screen_size(&mut self) -> termwiz::Result<ScreenSize> {
Ok(self.render_tx.screen_size)
}
fn set_screen_size(&mut self, _size: ScreenSize) -> termwiz::Result<()> {
termwiz::bail!("TermWizTerminalPane cannot set screen size");
}

View File

@ -59,6 +59,8 @@ use semver::Version;
use std::env::var;
use terminfo::{self, capability as cap};
pub mod probed;
builder! {
/// Use the `ProbeHints` to configure an instance of
/// the `ProbeHints` struct. `ProbeHints` are passed to the `Capabilities`

228
termwiz/src/caps/probed.rs Normal file
View File

@ -0,0 +1,228 @@
use crate::escape::csi::{Device, Window};
use crate::escape::parser::Parser;
use crate::escape::{Action, DeviceControlMode, Esc, EscCode, CSI};
use crate::terminal::ScreenSize;
use crate::{bail, Result};
use std::io::{Read, Write};
const TMUX_BEGIN: &str = "\u{1b}Ptmux;\u{1b}";
const TMUX_END: &str = "\u{1b}\\";
/// Represents a terminal name and version.
/// The name XtVersion is because this value is produced
/// by querying the terminal using the XTVERSION escape
/// sequence, which was defined by xterm.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct XtVersion(String);
impl XtVersion {
/// Split the version string into a name component and a version
/// component. Currently it recognizes `Name(Version)` and
/// `Name Version` forms. If a form is not recognized, returns None.
pub fn name_and_version(&self) -> Option<(&str, &str)> {
if self.0.ends_with(")") {
let paren = self.0.find('(')?;
Some((&self.0[0..paren], &self.0[paren + 1..self.0.len() - 1]))
} else {
let space = self.0.find(' ')?;
Some((&self.0[0..space], &self.0[space + 1..]))
}
}
/// Returns true if this represents tmux
pub fn is_tmux(&self) -> bool {
self.0.starts_with("tmux ")
}
/// Return the full underlying version string
pub fn full_version(&self) -> &str {
&self.0
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_xtversion_name() {
for (input, result) in [
("WezTerm something", Some(("WezTerm", "something"))),
("xterm(something)", Some(("xterm", "something"))),
("something-else", None),
] {
let version = XtVersion(input.to_string());
assert_eq!(version.name_and_version(), result, "{input}");
}
}
}
/// This struct is a helper that uses probing to determine specific capabilities
/// of the associated Terminal instance.
/// It will write and read data to and from the associated Terminal.
pub struct ProbeCapabilities<'a> {
read: Box<&'a mut dyn Read>,
write: Box<&'a mut dyn Write>,
}
impl<'a> ProbeCapabilities<'a> {
pub fn new<R: Read, W: Write>(read: &'a mut R, write: &'a mut W) -> Self {
Self {
read: Box::new(read),
write: Box::new(write),
}
}
/// Probe for the XTVERSION response
pub fn xt_version(&mut self) -> Result<XtVersion> {
self.xt_version_impl(false)
}
/// Assuming that we are talking to tmux, probe for the XTVERSION response
/// of its outer terminal.
pub fn outer_xt_version(&mut self) -> Result<XtVersion> {
self.xt_version_impl(true)
}
fn xt_version_impl(&mut self, tmux_escape: bool) -> Result<XtVersion> {
let xt_version = CSI::Device(Box::new(Device::RequestTerminalNameAndVersion));
let dev_attributes = CSI::Device(Box::new(Device::RequestPrimaryDeviceAttributes));
if tmux_escape {
write!(self.write, "{TMUX_BEGIN}{xt_version}{TMUX_END}")?;
self.write.flush()?;
std::thread::sleep(std::time::Duration::from_millis(100));
write!(self.write, "{dev_attributes}")?;
} else {
write!(self.write, "{xt_version}{dev_attributes}")?;
}
self.write.flush()?;
let mut term = vec![];
let mut parser = Parser::new();
let mut done = false;
while !done {
let mut byte = [0u8];
self.read.read(&mut byte)?;
parser.parse(&byte, |action| {
// print!("{action:?}\r\n");
match action {
Action::Esc(Esc::Code(EscCode::StringTerminator)) => {}
Action::DeviceControl(dev) => {
if let DeviceControlMode::Data(b) = dev {
term.push(b);
}
}
_ => {
done = true;
}
}
});
}
Ok(XtVersion(String::from_utf8_lossy(&term).into()))
}
/// Probe the terminal and determine the ScreenSize.
pub fn screen_size(&mut self) -> Result<ScreenSize> {
let xt_version = self.xt_version()?;
let is_tmux = xt_version.is_tmux();
// some tmux versions have their rows/cols swapped in ReportTextAreaSizeCells
let swapped_cols_rows = match xt_version.full_version() {
"tmux 3.2" | "tmux 3.2a" | "tmux 3.3" | "tmux 3.3a" => true,
_ => false,
};
let query_cells = CSI::Window(Box::new(Window::ReportTextAreaSizeCells));
let query_pixels = CSI::Window(Box::new(Window::ReportCellSizePixels));
let dev_attributes = CSI::Device(Box::new(Device::RequestPrimaryDeviceAttributes));
write!(self.write, "{query_cells}{query_pixels}")?;
// tmux refuses to directly support responding to 14t or 16t queries
// for pixel dimensions, so we need to jump through to the outer
// terminal and see what it says
if is_tmux {
write!(self.write, "{TMUX_BEGIN}{query_pixels}{TMUX_END}")?;
self.write.flush()?;
// I really wanted to avoid a delay here, but tmux will re-order the
// response to dev_attributes before it sends the response for the
// passthru of query_pixels if we don't delay. The delay is potentially
// imperfect for things like a laggy ssh connection. The consequence
// of the timing being wrong is that we won't be able to reason about
// the pixel dimensions, which is "OK", but that was kinda the whole
// point of probing this way vs. termios.
std::thread::sleep(std::time::Duration::from_millis(100));
}
write!(self.write, "{dev_attributes}")?;
self.write.flush()?;
let mut parser = Parser::new();
let mut done = false;
let mut size = ScreenSize {
rows: 0,
cols: 0,
xpixel: 0,
ypixel: 0,
};
while !done {
let mut byte = [0u8];
self.read.read(&mut byte)?;
parser.parse(&byte, |action| {
// print!("{action:?}\r\n");
match action {
Action::CSI(csi) => match csi {
CSI::Window(win) => match *win {
Window::ResizeWindowCells { width, height } => {
let width = width.unwrap_or(1);
let height = height.unwrap_or(1);
if width > 0 && height > 0 {
let width = width as usize;
let height = height as usize;
if swapped_cols_rows {
size.rows = width;
size.cols = height;
} else {
size.rows = height;
size.cols = width;
}
}
}
Window::ReportCellSizePixelsResponse { width, height } => {
let width = width.unwrap_or(1);
let height = height.unwrap_or(1);
if width > 0 && height > 0 {
let width = width as usize;
let height = height as usize;
size.xpixel = width;
size.ypixel = height;
}
}
_ => {
done = true;
}
},
_ => {
done = true;
}
},
_ => {
done = true;
}
}
});
}
if size.rows == 0 && size.cols == 0 {
bail!("no size information available");
}
Ok(size)
}
}

View File

@ -870,10 +870,6 @@ mod test {
})
}
fn probe_screen_size(&mut self) -> Result<ScreenSize> {
self.get_screen_size()
}
fn set_screen_size(&mut self, size: ScreenSize) -> Result<()> {
let size = winsize {
ws_row: cast(size.rows)?,

View File

@ -1,12 +1,12 @@
//! An abstraction over a terminal device
use crate::caps::probed::ProbeCapabilities;
use crate::caps::Capabilities;
use crate::input::InputEvent;
use crate::surface::Change;
use crate::{format_err, Result};
use num_traits::NumCast;
use std::fmt::Display;
use std::io::{Read, Write};
use std::time::Duration;
#[cfg(unix)]
@ -41,154 +41,6 @@ pub struct ScreenSize {
pub ypixel: usize,
}
impl ScreenSize {
/// This is a helper function to facilitate the implementation of
/// Terminal::probe_screen_size. It will emit a series of terminal
/// escape sequences intended to probe the terminal and determine
/// the ScreenSize.
/// `input` and `output` are the corresponding reading and writable
/// handles to the terminal.
pub fn probe<I: Read, O: Write>(mut input: I, mut output: O) -> Result<Self> {
use crate::escape::csi::{Device, Window};
use crate::escape::parser::Parser;
use crate::escape::{Action, DeviceControlMode, Esc, EscCode, CSI};
let xt_version = CSI::Device(Box::new(Device::RequestTerminalNameAndVersion));
let query_cells = CSI::Window(Box::new(Window::ReportTextAreaSizeCells));
let query_pixels = CSI::Window(Box::new(Window::ReportCellSizePixels));
let dev_attributes = CSI::Device(Box::new(Device::RequestPrimaryDeviceAttributes));
// some tmux versions have their rows/cols swapped in ReportTextAreaSizeCells,
// so we need to figure out the specific tmux version we're talking to
write!(output, "{xt_version}{dev_attributes}")?;
output.flush()?;
let mut term = vec![];
let mut parser = Parser::new();
let mut done = false;
while !done {
let mut byte = [0u8];
input.read(&mut byte)?;
parser.parse(&byte, |action| {
// print!("{action:?}\r\n");
match action {
Action::Esc(Esc::Code(EscCode::StringTerminator)) => {}
Action::DeviceControl(dev) => {
if let DeviceControlMode::Data(b) = dev {
term.push(b);
}
}
_ => {
done = true;
}
}
});
}
/*
print!(
"probed terminal version: {}\r\n",
String::from_utf8_lossy(&term)
);
*/
let is_tmux = term.starts_with(b"tmux ");
let swapped_cols_rows = if is_tmux {
let version = &term[5..];
match version {
b"3.2" | b"3.2a" | b"3.3" | b"3.3a" => true,
_ => false,
}
} else {
false
};
write!(output, "{query_cells}{query_pixels}")?;
// tmux refuses to directly support responding to 14t or 16t queries
// for pixel dimensions, so we need to jump through to the outer
// terminal and see what it says
if is_tmux {
let tmux_begin = "\u{1b}Ptmux;\u{1b}";
let tmux_end = "\u{1b}\\";
write!(output, "{tmux_begin}{query_pixels}{tmux_end}")?;
output.flush()?;
// I really wanted to avoid a delay here, but tmux will re-order the
// response to dev_attributes before it sends the response for the
// passthru of query_pixels if we don't delay. The delay is potentially
// imperfect for things like a laggy ssh connection. The consequence
// of the timing being wrong is that we won't be able to reason about
// the pixel dimensions, which is "OK", but that was kinda the whole
// point of probing this way vs. termios.
std::thread::sleep(std::time::Duration::from_millis(100));
}
write!(output, "{dev_attributes}")?;
output.flush()?;
let mut parser = Parser::new();
let mut done = false;
let mut size = ScreenSize {
rows: 0,
cols: 0,
xpixel: 0,
ypixel: 0,
};
while !done {
let mut byte = [0u8];
input.read(&mut byte)?;
parser.parse(&byte, |action| {
// print!("{action:?}\r\n");
match action {
Action::CSI(csi) => match csi {
CSI::Window(win) => match *win {
Window::ResizeWindowCells { width, height } => {
let width = width.unwrap_or(1);
let height = height.unwrap_or(1);
if width > 0 && height > 0 {
let width = width as usize;
let height = height as usize;
if swapped_cols_rows {
size.rows = width;
size.cols = height;
} else {
size.rows = height;
size.cols = width;
}
}
}
Window::ReportCellSizePixelsResponse { width, height } => {
let width = width.unwrap_or(1);
let height = height.unwrap_or(1);
if width > 0 && height > 0 {
let width = width as usize;
let height = height as usize;
size.xpixel = width;
size.ypixel = height;
}
}
_ => {
done = true;
}
},
_ => {
done = true;
}
},
_ => {
done = true;
}
}
});
}
Ok(size)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Blocking {
DoNotWait,
@ -217,11 +69,11 @@ pub trait Terminal {
/// Queries the current screen size, returning width, height.
fn get_screen_size(&mut self) -> Result<ScreenSize>;
/// Like get_screen_size but uses escape sequences to interrogate
/// the terminal rather than relying on the termios/kernel interface
/// You should delegate this to `ScreenSize::probe(&mut self.read, &mut self.write)`
/// to implement this method.
fn probe_screen_size(&mut self) -> Result<ScreenSize>;
/// Returns a capability probing helper that will use escape
/// sequences to attempt to probe information from the terminal
fn probe_capabilities(&mut self) -> Option<ProbeCapabilities> {
None
}
/// Sets the current screen size
fn set_screen_size(&mut self, size: ScreenSize) -> Result<()>;

View File

@ -1,4 +1,5 @@
use crate::render::RenderTty;
use crate::terminal::ProbeCapabilities;
use crate::{bail, Context, Result};
use filedescriptor::{poll, pollfd, FileDescriptor, POLLIN};
use libc::{self, winsize};
@ -385,8 +386,8 @@ impl Terminal for UnixTerminal {
})
}
fn probe_screen_size(&mut self) -> Result<ScreenSize> {
ScreenSize::probe(&mut self.read, &mut self.write)
fn probe_capabilities(&mut self) -> Option<ProbeCapabilities> {
Some(ProbeCapabilities::new(&mut self.read, &mut self.write))
}
fn set_screen_size(&mut self, size: ScreenSize) -> Result<()> {

View File

@ -769,8 +769,11 @@ impl Terminal for WindowsTerminal {
})
}
fn probe_screen_size(&mut self) -> Result<ScreenSize> {
ScreenSize::probe(&mut self.input_handle, &mut self.output_handle)
fn probe_capabilities(&mut self) -> Option<ProbeCapabilities> {
Some(ProbeCapabilities::new(
&mut self.input_handle,
&mut self.output_handle,
))
}
fn set_screen_size(&mut self, size: ScreenSize) -> Result<()> {

View File

@ -312,10 +312,19 @@ impl ImgCatCommand {
stdin.read_to_end(&mut data)?;
}
// TODO: ideally we'd provide some kind of ProbeCapabilities type
// that can be returned from Terminal that will encode this sort
// of thing so that we can use xtversion to know for sure.
let is_tmux = TmuxPassthru::is_tmux();
let caps = Capabilities::new_from_env()?;
let mut term = termwiz::terminal::new_terminal(caps)?;
term.set_raw_mode()?;
let mut probe = term
.probe_capabilities()
.ok_or_else(|| anyhow!("Terminal has no prober?"))?;
let xt_version = probe.xt_version()?;
let term_size = probe.screen_size()?;
let is_tmux = xt_version.is_tmux();
// TODO: ideally we'd do some kind of probing to see if conpty
// is in the mix. For now we just assume that if we are on windows
@ -330,11 +339,6 @@ impl ImgCatCommand {
let needs_force_cursor_move =
!self.no_move_cursor && !self.position.is_some() && (is_tmux || is_conpty);
let caps = Capabilities::new_from_env()?;
let mut term = termwiz::terminal::new_terminal(caps)?;
term.set_raw_mode()?;
let term_size = term.probe_screen_size()?;
term.set_cooked_mode()?;
let save_cursor = Esc::Code(EscCode::DecSaveCursorPosition);