use std::{io, path::PathBuf, sync::Arc}; use alacritty_terminal::{ event::{OnResize, WindowSize}, tty::{ChildEvent, EventedPty, EventedReadWrite}, }; use futures::channel::mpsc::UnboundedSender; use polling::{Event, PollMode, Poller}; use super::{ marks, mouse::MouseMode, prompts::{self, Prompts}, pty::PtyEvent, status, }; /// The longest OSC body kept; longer ones are dropped whole. pub(super) const MOST: usize = 4096; /// A pty whose reader shows each byte to its scanners before the parser. pub(crate) struct Tapped

{ pub inner: P, scanner: Osc, mouse: MouseModes, prompts: Prompts, events: UnboundedSender, pid: u32, last: Option, } impl

Tapped

{ pub fn new(inner: P, events: UnboundedSender, pid: u32) -> Self { Self { inner, scanner: Osc::default(), mouse: MouseModes::default(), prompts: Prompts::default(), events, pid, last: None, } } } impl io::Read for Tapped

{ fn read(&mut self, buf: &mut [u8]) -> io::Result { // What a rewrite adds stays inside the buffer, with room for the held sequence's own. let room = buf .len() .saturating_sub(self.prompts.held() + prompts::SLACK); let take = room / prompts::GROWTH; if take == 0 { return Ok(0); } let count = self.inner.reader().read(&mut buf[..take])?; let read = &buf[..count]; let (events, pid, last) = (&self.events, self.pid, &mut self.last); self.scanner .feed(read, &mut |found| report(found, events, pid, last)); if let Some(mode) = self.mouse.feed(read) { log::info!("pty {pid}: mouse {mode:?}"); if events.unbounded_send(PtyEvent::Mouse(mode)).is_err() { log::debug!("pty {pid}: a mouse mode after its session closed"); } } let out = self.prompts.pass(&buf[..count]); buf[..out.len()].copy_from_slice(out); Ok(out.len()) } } /// Sends the session what the scanner found: a full reset, a mark, a program's status or a folder. fn report(found: Found, events: &UnboundedSender, pid: u32, last: &mut Option) { let event = match found { Found::Reset => { log::debug!("pty {pid}: a full reset"); PtyEvent::Reset } Found::Osc(body, length) => { if let Some(mark) = body.strip_prefix(b"133;").and_then(marks::read) { log::debug!("pty {pid}: {mark:?}"); PtyEvent::Mark(mark) } else if let Some(status) = body .strip_prefix(b"7501;") .and_then(|rest| status::read(rest, length)) { log::debug!("pty {pid}: {status:?}"); PtyEvent::Status(status) } else if let Some(folder) = body.strip_prefix(b"7;").and_then(folder) { if last.as_ref() == Some(&folder) { log::debug!("pty {pid}: folder {} again", folder.display()); } else { log::info!("pty {pid}: folder {}", folder.display()); *last = Some(folder.clone()); } PtyEvent::Folder(folder) } else { return; } } }; if events.unbounded_send(event).is_err() { log::debug!("pty {pid}: an event after its session closed"); } } impl EventedReadWrite for Tapped

{ type Reader = Self; type Writer = P::Writer; unsafe fn register( &mut self, poll: &Arc, event: Event, mode: PollMode, ) -> io::Result<()> { unsafe { self.inner.register(poll, event, mode) } } fn reregister(&mut self, poll: &Arc, event: Event, mode: PollMode) -> io::Result<()> { self.inner.reregister(poll, event, mode) } fn deregister(&mut self, poll: &Arc) -> io::Result<()> { self.inner.deregister(poll) } fn reader(&mut self) -> &mut Self { self } fn writer(&mut self) -> &mut P::Writer { self.inner.writer() } } impl EventedPty for Tapped

{ fn next_child_event(&mut self) -> Option { self.inner.next_child_event() } } impl OnResize for Tapped

{ fn on_resize(&mut self, size: WindowSize) { self.inner.on_resize(size); } } #[derive(Clone, Copy, Debug, Default, PartialEq)] enum State { #[default] Ground, Escape, Osc, } /// What the scanner finds: an OSC's body with the sequence's length from OSC through ST, or a full reset. pub(crate) enum Found<'a> { Osc(&'a [u8], usize), Reset, } /// Finds each OSC body across reads, ended as vte ends it: at BEL, ESC, CAN or SUB, and each full reset. #[derive(Default)] pub(crate) struct Osc { state: State, body: Vec, /// The sequence's bytes so far, the controls the body leaves out too. length: usize, overflow: bool, } impl Osc { pub fn feed(&mut self, bytes: &[u8], found: &mut impl FnMut(Found)) { let mut rest = bytes; while !rest.is_empty() { if self.state == State::Ground { match rest.iter().position(|&byte| byte == 0x1b) { Some(at) => { self.state = State::Escape; rest = &rest[at + 1..]; } None => return, } continue; } let byte = rest[0]; rest = &rest[1..]; self.state = match (self.state, byte) { (State::Escape, b']') => { self.body.clear(); self.length = 2; self.overflow = false; State::Osc } (State::Escape, b'c') => { found(Found::Reset); State::Ground } // vte runs controls in an escape, passes DEL and high bytes by, and stays there. (State::Escape, 0x00..=0x17 | 0x19 | 0x1b..=0x1f | 0x7f..=0xff) => State::Escape, (State::Escape, _) => State::Ground, (State::Osc, 0x07 | 0x18 | 0x1a) => { self.finish(1, found); State::Ground } // ESC ends the body, then reads as an escape: `\` makes ST, counted so. (State::Osc, 0x1b) => { self.finish(2, found); State::Escape } (State::Osc, 0x00..=0x06 | 0x08..=0x17 | 0x19 | 0x1c..=0x1f) => { self.length += 1; State::Osc } (State::Osc, byte) => { self.length += 1; if self.body.len() < MOST { self.body.push(byte); } else { self.overflow = true; } State::Osc } (State::Ground, _) => unreachable!("ground scans by position"), }; } } fn finish(&mut self, end: usize, found: &mut impl FnMut(Found)) { if self.overflow { log::warn!("pty: an OSC over {MOST} bytes dropped"); } else { found(Found::Osc(&self.body, self.length + end)); } self.body.clear(); } } /// The most values, subparameters too, one sequence holds before vte drops it. const PARAMS: usize = 32; #[derive(Clone, Copy, Debug, Default, PartialEq)] enum Csi { #[default] Ground, Escape, Bracket, Private, } /// The mouse modes as xterm holds them, read as vte reads; alacritty drops 9. #[derive(Default)] pub(crate) struct MouseModes { state: Csi, params: Vec, /// Values begun, subparameters too. fields: usize, /// Past a `:`, whose subparameters the parser's mode sets skip. sub: bool, mode: MouseMode, } impl MouseModes { /// The mode after `bytes`, when they changed it. pub fn feed(&mut self, bytes: &[u8]) -> Option { let was = self.mode; let mut rest = bytes; while !rest.is_empty() { if self.state == Csi::Ground { let Some(at) = rest.iter().position(|&byte| byte == 0x1b) else { break; }; self.state = Csi::Escape; rest = &rest[at + 1..]; continue; } let byte = rest[0]; rest = &rest[1..]; self.state = match (self.state, byte) { (_, 0x18 | 0x1a) => Csi::Ground, (_, 0x1b) => Csi::Escape, // Mid-sequence, C0 runs and DEL is skipped, as in vte. (state, 0x00..=0x1f | 0x7f..=0xff) => state, (Csi::Escape, b'[') => Csi::Bracket, (Csi::Escape, b'c') => { self.mode = MouseMode::default(); Csi::Ground } (Csi::Bracket, b'?') => { self.params = vec![0]; self.fields = 1; self.sub = false; Csi::Private } (Csi::Private, b'0'..=b'9') => { if !self.sub { let last = self.params.last_mut().expect("a parameter in progress"); *last = last .saturating_mul(10) .saturating_add(u32::from(byte - b'0')); } Csi::Private } (Csi::Private, b':' | b';') if self.fields < PARAMS => { self.fields += 1; self.sub = byte == b':'; if !self.sub { self.params.push(0); } Csi::Private } (Csi::Private, b'h' | b'l') => { for &mode in &self.params { self.mode.apply(mode, byte == b'h'); } Csi::Ground } _ => Csi::Ground, }; } (self.mode != was).then_some(self.mode) } } /// The local folder an OSC 7 `file://host/path` names; another host's folder is none. pub(crate) fn folder(body: &[u8]) -> Option { let Some(rest) = body.strip_prefix(b"file://") else { log::warn!( "pty: OSC 7 without file:// ({})", String::from_utf8_lossy(body) ); return None; }; let slash = rest.iter().position(|&byte| byte == b'/')?; let host = String::from_utf8_lossy(&rest[..slash]); let local = gethostname::gethostname(); if !(host.is_empty() || host.eq_ignore_ascii_case("localhost") || local .to_str() .is_some_and(|name| host.eq_ignore_ascii_case(name))) { log::debug!("pty: OSC 7 from host {host}, not this one"); return None; } let path = match decode(&rest[slash..]) { Some(path) => path, None => { log::warn!("pty: OSC 7 path is not UTF-8 after decoding"); return None; } }; let drive = path.as_bytes().get(1..3).is_some_and(|two| two[1] == b':') && path.as_bytes()[1].is_ascii_alphabetic(); Some(PathBuf::from(if cfg!(windows) && drive { &path[1..] } else { &path[..] })) } fn decode(bytes: &[u8]) -> Option { let mut out = Vec::with_capacity(bytes.len()); let mut at = 0; while at < bytes.len() { let hex = bytes .get(at + 1..at + 3) .and_then(|two| std::str::from_utf8(two).ok()) .and_then(|two| u8::from_str_radix(two, 16).ok()); match (bytes[at], hex) { (b'%', Some(value)) => { out.push(value); at += 3; } (byte, _) => { out.push(byte); at += 1; } } } String::from_utf8(out).ok() } #[cfg(test)] mod tests;