diff --git a/src/cmd/session.rs b/src/cmd/session.rs index 8278f7c..9c82684 100644 --- a/src/cmd/session.rs +++ b/src/cmd/session.rs @@ -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, env: &HashMap, notifier: N, ) -> Result { @@ -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> { + self.get_tty().map(|tty| tty.get_version()) + } + fn get_command(&self, config: &config::Session) -> Option { self.command.as_ref().cloned().or(config.command.clone()) } fn build_asciicast_metadata( &self, + term_version: Option, env: &HashMap, 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 { + fn get_tty(&self) -> Result { 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) -> Result { +fn get_relay( + target: RelayTarget, + config: &Config, + term_version: Option, + env: &HashMap, +) -> Result { 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 } } -fn build_producer_url(url: &str, env: &HashMap) -> Result { +fn build_producer_url( + url: &str, + term_version: Option, + env: &HashMap, +) -> Result { 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())); } diff --git a/src/file_writer.rs b/src/file_writer.rs index b15150a..1f61c5c 100644 --- a/src/file_writer.rs +++ b/src/file_writer.rs @@ -22,6 +22,7 @@ pub struct FileWriter { } pub struct Metadata { + pub term_version: Option, pub idle_time_limit: Option, pub command: Option, pub title: Option, diff --git a/src/tty.rs b/src/tty.rs index 974fc45..70ae06e 100644 --- a/src/tty.rs +++ b/src/tty.rs @@ -45,6 +45,7 @@ impl From for (u16, u16) { pub trait Tty: io::Write + io::Read + AsFd { fn get_size(&self) -> pty::Winsize; fn get_theme(&self) -> Option; + fn get_version(&self) -> Option; } #[derive(Clone)] @@ -70,6 +71,77 @@ impl DevTty { Ok(Self { file }) } + + fn query(&self, query: &str) -> Result> { + 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 { @@ -89,7 +161,9 @@ fn parse_color(rgb: &str) -> Option { 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 { - 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 { + 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 { None } + + fn get_version(&self) -> Option { + None + } } impl io::Read for NullTty { @@ -291,6 +321,10 @@ impl Tty for FixedSizeTty { fn get_theme(&self) -> Option { self.inner.get_theme() } + + fn get_version(&self) -> Option { + self.inner.get_version() + } } impl AsFd for FixedSizeTty {