mirror of
https://github.com/wez/wezterm.git
synced 2024-12-23 13:21:38 +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:
parent
abc92e56e0
commit
feb9e11b33
@ -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");
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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
228
termwiz/src/caps/probed.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -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)?,
|
||||
|
@ -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<()>;
|
||||
|
@ -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<()> {
|
||||
|
@ -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<()> {
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user