Query for XTVERSION, pass it to file writer and stream forwarder

This commit is contained in:
Marcin Kulik
2025-04-11 16:17:11 +02:00
parent 02ad03f815
commit cbdd0a48f2
3 changed files with 134 additions and 70 deletions

View File

@@ -31,7 +31,7 @@ use crate::pty;
use crate::server;
use crate::session::{self, KeyBindings, SessionStarter};
use crate::stream::Stream;
use crate::tty::{DevTty, FixedSizeTty, NullTty};
use crate::tty::{DevTty, FixedSizeTty, NullTty, Tty};
use crate::util;
impl cli::Session {
@@ -43,6 +43,7 @@ impl cli::Session {
let keys = get_key_bindings(cmd_config)?;
let notifier = notifier::threaded(get_notifier(config));
let record_input = self.input || cmd_config.input;
let term_version = self.get_term_version()?;
let env = capture_env(self.env.clone(), cmd_config);
let path = self
@@ -53,7 +54,15 @@ impl cli::Session {
let file_writer = path
.as_ref()
.map(|path| self.get_file_writer(path, cmd_config, &env, notifier.clone()))
.map(|path| {
self.get_file_writer(
path,
cmd_config,
term_version.clone(),
&env,
notifier.clone(),
)
})
.transpose()?;
let mut listener = self
@@ -66,7 +75,7 @@ impl cli::Session {
let mut relay = self
.relay
.take()
.map(|target| get_relay(target, config, &env))
.map(|target| get_relay(target, config, term_version, &env))
.transpose()?;
let relay_id = relay.as_ref().map(|r| r.id());
@@ -212,6 +221,7 @@ impl cli::Session {
&self,
path: &str,
config: &config::Session,
term_version: Option<String>,
env: &HashMap<String, String>,
notifier: N,
) -> Result<FileWriterStarter> {
@@ -256,7 +266,7 @@ impl cli::Session {
0
};
let metadata = self.build_asciicast_metadata(env, config);
let metadata = self.build_asciicast_metadata(term_version, env, config);
let notifier = Box::new(notifier);
let writer = match format {
@@ -300,12 +310,17 @@ impl cli::Session {
Ok(writer)
}
fn get_term_version(&self) -> Result<Option<String>> {
self.get_tty().map(|tty| tty.get_version())
}
fn get_command(&self, config: &config::Session) -> Option<String> {
self.command.as_ref().cloned().or(config.command.clone())
}
fn build_asciicast_metadata(
&self,
term_version: Option<String>,
env: &HashMap<String, String>,
config: &config::Session,
) -> Metadata {
@@ -313,6 +328,7 @@ impl cli::Session {
let command = self.get_command(config);
Metadata {
term_version,
idle_time_limit,
command,
title: self.title.clone(),
@@ -320,7 +336,7 @@ impl cli::Session {
}
}
fn get_tty(&self) -> Result<FixedSizeTty> {
fn get_tty(&self) -> Result<impl Tty> {
let (cols, rows) = self.tty_size.unwrap_or((None, None));
if self.headless {
@@ -370,11 +386,16 @@ impl Relay {
}
}
fn get_relay(target: RelayTarget, config: &Config, env: &HashMap<String, String>) -> Result<Relay> {
fn get_relay(
target: RelayTarget,
config: &Config,
term_version: Option<String>,
env: &HashMap<String, String>,
) -> Result<Relay> {
match target {
RelayTarget::StreamId(id) => {
let stream = api::create_user_stream(id, config)?;
let ws_producer_url = build_producer_url(&stream.ws_producer_url, env)?;
let ws_producer_url = build_producer_url(&stream.ws_producer_url, term_version, env)?;
Ok(Relay {
ws_producer_url,
@@ -389,16 +410,24 @@ fn get_relay(target: RelayTarget, config: &Config, env: &HashMap<String, String>
}
}
fn build_producer_url(url: &str, env: &HashMap<String, String>) -> Result<Url> {
fn build_producer_url(
url: &str,
term_version: Option<String>,
env: &HashMap<String, String>,
) -> Result<Url> {
let mut url: Url = url.parse()?;
let term = env::var("TERM").ok().unwrap_or_default();
let term_type = env::var("TERM").ok().unwrap_or_default();
let shell = env::var("SHELL").ok().unwrap_or_default();
let mut params = vec![
("term[type]".to_string(), term.clone()),
("term[type]".to_string(), term_type.clone()),
("shell".to_string(), shell.clone()),
];
if let Some(version) = term_version {
params.push(("term[version]".to_string(), version));
}
for (k, v) in env {
params.push((format!("env[{k}]"), v.to_string()));
}

View File

@@ -22,6 +22,7 @@ pub struct FileWriter {
}
pub struct Metadata {
pub term_version: Option<String>,
pub idle_time_limit: Option<f64>,
pub command: Option<String>,
pub title: Option<String>,

View File

@@ -45,6 +45,7 @@ impl From<TtySize> for (u16, u16) {
pub trait Tty: io::Write + io::Read + AsFd {
fn get_size(&self) -> pty::Winsize;
fn get_theme(&self) -> Option<TtyTheme>;
fn get_version(&self) -> Option<String>;
}
#[derive(Clone)]
@@ -70,6 +71,77 @@ impl DevTty {
Ok(Self { file })
}
fn query(&self, query: &str) -> Result<Vec<u8>> {
let mut query = query.to_string().into_bytes();
query.extend_from_slice(b"\x1b[c");
let mut query = &query[..];
let mut response = Vec::new();
let mut buf = [0u8; 1024];
let fd = self.as_fd().as_raw_fd();
loop {
let mut timeout = TimeVal::new(0, 100_000);
let mut rfds = FdSet::new();
let mut wfds = FdSet::new();
rfds.insert(self);
if !query.is_empty() {
wfds.insert(self);
}
match select(None, &mut rfds, &mut wfds, None, &mut timeout) {
Ok(0) => break,
Ok(_) => {
if rfds.contains(self) {
let n = unistd::read(fd, &mut buf)?;
response.extend_from_slice(&buf[..n]);
let mut reversed = response.iter().rev();
let mut got_da_response = false;
let mut da_len = 0;
if let Some(b'c') = reversed.next() {
da_len += 1;
for b in reversed {
if *b == b'[' {
got_da_response = true;
break;
}
if *b != b';' && *b != b'?' && !b.is_ascii_digit() {
break;
}
da_len += 1;
}
}
if got_da_response {
response.truncate(response.len() - da_len - 2);
break;
}
}
if wfds.contains(self) {
let n = unistd::write(fd, query)?;
query = &query[n..];
}
}
Err(e) => {
if e == Errno::EINTR {
continue;
} else {
return Err(e.into());
}
}
}
}
Ok(response)
}
}
fn parse_color(rgb: &str) -> Option<RGB8> {
@@ -89,7 +161,9 @@ fn parse_color(rgb: &str) -> Option<RGB8> {
Some(RGB8::new(r, g, b))
}
static COLORS_QUERY: &[u8; 151] = b"\x1b]10;?\x07\x1b]11;?\x07\x1b]4;0;?\x07\x1b]4;1;?\x07\x1b]4;2;?\x07\x1b]4;3;?\x07\x1b]4;4;?\x07\x1b]4;5;?\x07\x1b]4;6;?\x07\x1b]4;7;?\x07\x1b]4;8;?\x07\x1b]4;9;?\x07\x1b]4;10;?\x07\x1b]4;11;?\x07\x1b]4;12;?\x07\x1b]4;13;?\x07\x1b]4;14;?\x07\x1b]4;15;?\x07\x1b[c";
static COLORS_QUERY: &str = "\x1b]10;?\x07\x1b]11;?\x07\x1b]4;0;?\x07\x1b]4;1;?\x07\x1b]4;2;?\x07\x1b]4;3;?\x07\x1b]4;4;?\x07\x1b]4;5;?\x07\x1b]4;6;?\x07\x1b]4;7;?\x07\x1b]4;8;?\x07\x1b]4;9;?\x07\x1b]4;10;?\x07\x1b]4;11;?\x07\x1b]4;12;?\x07\x1b]4;13;?\x07\x1b]4;14;?\x07\x1b]4;15;?\x07";
static XTVERSION_QUERY: &str = "\x1b[>0q";
impl Tty for DevTty {
fn get_size(&self) -> pty::Winsize {
@@ -106,65 +180,7 @@ impl Tty for DevTty {
}
fn get_theme(&self) -> Option<TtyTheme> {
let mut query = &COLORS_QUERY[..];
let mut response = Vec::new();
let mut buf = [0u8; 1024];
let fd = self.as_fd().as_raw_fd();
loop {
let mut timeout = TimeVal::new(0, 100_000);
let mut rfds = FdSet::new();
let mut wfds = FdSet::new();
rfds.insert(self);
if !query.is_empty() {
wfds.insert(self);
}
match select(None, &mut rfds, &mut wfds, None, &mut timeout) {
Ok(0) => return None,
Ok(_) => {
if rfds.contains(self) {
let n = unistd::read(fd, &mut buf).ok()?;
response.extend_from_slice(&buf[..n]);
let mut reversed = response.iter().rev();
let mut got_da_response = false;
if let Some(b'c') = reversed.next() {
for b in reversed {
if *b == b'[' {
got_da_response = true;
break;
}
if *b != b';' && *b != b'?' && !b.is_ascii_digit() {
break;
}
}
}
if got_da_response {
break;
}
}
if wfds.contains(self) {
let n = unistd::write(fd, query).ok()?;
query = &query[n..];
}
}
Err(e) => {
if e == Errno::EINTR {
continue;
} else {
return None;
}
}
}
}
let response = self.query(COLORS_QUERY).ok()?;
let response = String::from_utf8_lossy(response.as_slice());
let mut colors = response.match_indices("rgb:");
let (idx, _) = colors.next()?;
@@ -181,6 +197,16 @@ impl Tty for DevTty {
Some(TtyTheme { fg, bg, palette })
}
fn get_version(&self) -> Option<String> {
let response = self.query(XTVERSION_QUERY).ok()?;
if let [b'\x1b', b'P', b'>', b'|', version @ .., b'\x1b', b'\\'] = &response[..] {
Some(String::from_utf8_lossy(version).to_string())
} else {
None
}
}
}
impl io::Read for DevTty {
@@ -233,6 +259,10 @@ impl Tty for NullTty {
fn get_theme(&self) -> Option<TtyTheme> {
None
}
fn get_version(&self) -> Option<String> {
None
}
}
impl io::Read for NullTty {
@@ -291,6 +321,10 @@ impl Tty for FixedSizeTty {
fn get_theme(&self) -> Option<TtyTheme> {
self.inner.get_theme()
}
fn get_version(&self) -> Option<String> {
self.inner.get_version()
}
}
impl AsFd for FixedSizeTty {