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

termwiz: recognize the XTGETTCAP DCS sequence

Parse and respond to this sequence docs can be found
https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h4-Device-Control-functions:DCS-plus-q-Pt-ST.F95

refs: https://github.com/wez/wezterm/issues/954
This commit is contained in:
Wez Furlong 2021-07-24 21:02:54 -07:00
parent 365a68dfb8
commit 6ddc8afc64
8 changed files with 153 additions and 3 deletions

9
Cargo.lock generated
View File

@ -1913,6 +1913,12 @@ dependencies = [
"libc",
]
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hostname"
version = "0.3.1"
@ -4306,6 +4312,7 @@ dependencies = [
"cfg-if 1.0.0",
"filedescriptor",
"fnv",
"hex",
"lazy_static",
"libc",
"log",
@ -5110,6 +5117,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"bitflags",
"hex",
"image",
"k9",
"lazy_static",
@ -5122,6 +5130,7 @@ dependencies = [
"pretty_env_logger",
"serde",
"sha2",
"terminfo",
"termwiz",
"unicode-segmentation",
"unicode-width",

View File

@ -16,6 +16,7 @@ use_serde = ["termwiz/use_serde"]
[dependencies]
anyhow = "1.0"
bitflags = "1.0"
hex = "0.4"
image = "0.23"
lazy_static = "1.4"
log = "0.4"
@ -25,6 +26,7 @@ ordered-float = "2.7"
palette = "0.5"
serde = {version="1.0", features = ["rc"]}
sha2 = "0.9"
terminfo = "0.7"
unicode-segmentation = "1.7"
unicode-width = "0.1"
url = "2"

View File

@ -13,6 +13,7 @@ use std::collections::HashMap;
use std::fmt::Write;
use std::sync::mpsc::{channel, Sender};
use std::sync::Arc;
use terminfo::{Database, Value};
use termwiz::escape::csi::{
Cursor, CursorStyle, DecPrivateMode, DecPrivateModeCode, Device, Edit, EraseInDisplay,
EraseInLine, Mode, Sgr, TabulationClear, TerminalMode, TerminalModeCode, Window, XtSmGraphics,
@ -30,6 +31,13 @@ use termwiz::image::{ImageCell, ImageData, TextureCoordinate};
use termwiz::surface::{CursorShape, CursorVisibility};
use url::Url;
lazy_static::lazy_static! {
static ref DB: Database = {
let data = include_bytes!("../../termwiz/data/wezterm");
Database::from_buffer(&data[..]).unwrap()
};
}
struct TabStop {
tabs: Vec<bool>,
tab_width: usize,
@ -1455,6 +1463,68 @@ impl TerminalState {
});
}
/// <https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h4-Device-Control-functions:DCS-plus-q-Pt-ST.F95>
fn xt_get_tcap(&mut self, names: Vec<String>) {
let mut res = "\x1bP".to_string();
for (i, name) in names.iter().enumerate() {
if i > 0 {
res.push(';');
}
let encoded_name = hex::encode_upper(&name);
match name.as_str() {
"TN" | "name" => {
res.push_str("1+r");
res.push_str(&encoded_name);
res.push('=');
let encoded_val = hex::encode_upper(&self.term_program);
res.push_str(&encoded_val);
}
"Co" | "colors" => {
res.push_str("1+r");
res.push_str(&encoded_name);
res.push('=');
res.push_str(&256.to_string());
}
"RGB" => {
res.push_str("1+r");
res.push_str(&encoded_name);
res.push('=');
res.push_str("8/8/8");
}
_ => {
if let Some(value) = DB.raw(name) {
res.push_str("1+r");
res.push_str(&encoded_name);
res.push('=');
match value {
Value::True => res.push('1'),
Value::Number(n) => res.push_str(&n.to_string()),
Value::String(s) => {
for &b in s {
res.push(b as char);
}
}
}
} else {
log::trace!("xt_get_tcap: unknown name {}", name);
res.push_str("0+r");
res.push_str(&encoded_name);
}
}
}
}
res.push_str("\x1b\\");
log::trace!("responding with {}", res.escape_debug());
self.writer.write_all(res.as_bytes()).ok();
}
fn sixel(&mut self, sixel: Box<Sixel>) {
let (width, height) = sixel.dimensions();
@ -3240,6 +3310,7 @@ impl<'a> Performer<'a> {
Action::Esc(esc) => self.esc_dispatch(esc),
Action::CSI(csi) => self.csi_dispatch(csi),
Action::Sixel(sixel) => self.sixel(sixel),
Action::XtGetTcap(names) => self.xt_get_tcap(names),
}
}

View File

@ -18,6 +18,7 @@ cfg-if = "1.0"
anyhow = "1.0"
filedescriptor = { version="0.8", path = "../filedescriptor" }
fnv = {version="1.0", optional=true}
hex = "0.4"
lazy_static = "1.4"
libc = "0.2"
log = "0.4"

BIN
termwiz/data/wezterm Normal file

Binary file not shown.

View File

@ -36,6 +36,9 @@ pub enum Action {
CSI(CSI),
Esc(Esc),
Sixel(Box<Sixel>),
/// A list of termcap, terminfo names for which the application
/// whats information
XtGetTcap(Vec<String>),
}
/// Encode self as an escape sequence. The escape sequence may potentially
@ -50,6 +53,19 @@ impl Display for Action {
Action::CSI(csi) => csi.fmt(f),
Action::Esc(esc) => esc.fmt(f),
Action::Sixel(sixel) => sixel.fmt(f),
Action::XtGetTcap(names) => {
write!(f, "\x1bP+q")?;
for (i, name) in names.iter().enumerate() {
if i > 0 {
write!(f, ";")?;
}
for &b in name.as_bytes() {
write!(f, "{:x}", b)?;
}
}
Ok(())
}
}
}
}
@ -176,7 +192,9 @@ impl Display for DeviceControlMode {
}
f.write_char(mode.byte as char)
}
Self::Exit => write!(f, "\x1b\\"),
// We don't need to emit a sequence for the Exit, as we're
// followed by eg: StringTerminator
Self::Exit => Ok(()),
Self::Data(c) => f.write_char(*c as char),
Self::ShortDeviceControl(s) => s.fmt(f),
}

View File

@ -19,10 +19,40 @@ struct SixelBuilder {
coloruse_re: Regex,
}
#[derive(Default)]
struct GetTcapBuilder {
current: Vec<u8>,
names: Vec<String>,
}
impl GetTcapBuilder {
fn flush(&mut self) {
let decoded = hex::decode(&self.current)
.map(|s| String::from_utf8_lossy(&s).to_string())
.unwrap_or_else(|_| String::from_utf8_lossy(&self.current).to_string());
self.names.push(decoded);
self.current.clear();
}
pub fn push(&mut self, data: u8) {
if data == b';' {
self.flush();
} else {
self.current.push(data);
}
}
pub fn finish(mut self) -> Vec<String> {
self.flush();
self.names
}
}
#[derive(Default)]
struct ParseState {
sixel: Option<SixelBuilder>,
dcs: Option<ShortDeviceControl>,
get_tcap: Option<GetTcapBuilder>,
}
/// The `Parser` struct holds the state machine that is used to decode
@ -162,10 +192,14 @@ impl<'a, F: FnMut(Action)> VTActor for Performer<'a, F> {
intermediates: &[u8],
ignored_extra_intermediates: bool,
) {
self.state.sixel.take();
self.state.get_tcap.take();
self.state.dcs.take();
if byte == b'q' && intermediates.is_empty() && !ignored_extra_intermediates {
self.state.sixel.replace(SixelBuilder::new(params));
} else if byte == b'q' && intermediates == [b'+'] {
self.state.get_tcap.replace(GetTcapBuilder::default());
} else if !ignored_extra_intermediates && is_short_dcs(intermediates, byte) {
self.state.sixel.take();
self.state.dcs.replace(ShortDeviceControl {
params: params.to_vec(),
intermediates: intermediates.to_vec(),
@ -189,6 +223,8 @@ impl<'a, F: FnMut(Action)> VTActor for Performer<'a, F> {
dcs.data.push(data);
} else if let Some(sixel) = self.state.sixel.as_mut() {
sixel.push(data);
} else if let Some(tcap) = self.state.get_tcap.as_mut() {
tcap.push(data);
} else {
(self.callback)(Action::DeviceControl(DeviceControlMode::Data(data)));
}
@ -202,6 +238,8 @@ impl<'a, F: FnMut(Action)> VTActor for Performer<'a, F> {
} else if let Some(mut sixel) = self.state.sixel.take() {
sixel.finish();
(self.callback)(Action::Sixel(Box::new(sixel.sixel)));
} else if let Some(tcap) = self.state.get_tcap.take() {
(self.callback)(Action::XtGetTcap(tcap.finish()));
} else {
(self.callback)(Action::DeviceControl(DeviceControlMode::Exit));
}
@ -739,6 +777,17 @@ mod test {
actions
}
#[test]
fn xtgettcap() {
assert_eq!(
round_trip_parse("\x1bP+q544e\x1b\\"),
vec![
Action::XtGetTcap(vec!["TN".to_string()]),
Action::Esc(Esc::Code(EscCode::StringTerminator)),
]
);
}
#[test]
fn xterm_key() {
assert_eq!(

View File

@ -414,7 +414,7 @@ fn parse_status_text(text: &str, default_cell: CellAttributes) -> Vec<Cell> {
Action::Esc(_) => {
flush_print(&mut print_buffer, &mut cells, &pen);
}
Action::Sixel(_) => {
Action::XtGetTcap(_) | Action::Sixel(_) => {
flush_print(&mut print_buffer, &mut cells, &pen);
}
Action::DeviceControl(_) => {