T40: review round 1: a notification names the session as the output stands at its place: the tap follows OSC 0 and 2, the title stack's CSI 22 t and 23 t and a full reset as alacritty does, and sends each title in order, since alacritty's own title events come after the tap's; one write and split writes post the same
This commit is contained in:
9 files changed
+247
-15
No files matched your search
@@ -19,6 +19,7 @@ mod reports;
|
||||
pub mod shell;
|
||||
mod status;
|
||||
mod tap;
|
||||
mod titles;
|
||||
#[cfg(unix)]
|
||||
mod unix;
|
||||
mod view;
|
||||
|
||||
@@ -25,6 +25,8 @@ pub enum PtyEvent {
|
||||
Status(Status),
|
||||
/// A notification or progress, OSC 9, 777 or 99.
|
||||
Ask(Ask),
|
||||
/// A title a program set, in the output's order: OSC 0 or 2, or the title stack's pop.
|
||||
Title(String),
|
||||
/// A full reset, `ESC c`.
|
||||
Reset,
|
||||
}
|
||||
|
||||
+56
-6
@@ -17,6 +17,8 @@ struct Ran {
|
||||
statuses: Vec<Option<Status>>,
|
||||
/// Notifications and progress programs asked for.
|
||||
asks: Vec<Ask>,
|
||||
/// Titles, notifications and full resets, in the output's order.
|
||||
said: Vec<String>,
|
||||
text: String,
|
||||
/// The first line's cells that carry a prompt's link.
|
||||
prompt: Vec<usize>,
|
||||
@@ -35,22 +37,37 @@ fn run(script: &str, size: (usize, usize)) -> Ran {
|
||||
};
|
||||
let (pty, mut events) =
|
||||
spawn(&launch, size, (8, 16), config(&Settings::default())).expect("sh starts");
|
||||
let (folders, marks, statuses, asks, success) = futures::executor::block_on(async {
|
||||
let (mut folders, mut marks, mut statuses, mut asks, mut success) =
|
||||
(Vec::new(), Vec::new(), Vec::new(), Vec::new(), false);
|
||||
let (folders, marks, statuses, asks, said, success) = futures::executor::block_on(async {
|
||||
let (mut folders, mut marks, mut statuses, mut asks, mut said, mut success) = (
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
false,
|
||||
);
|
||||
while let Some(event) = events.next().await {
|
||||
match event {
|
||||
PtyEvent::Folder(folder) => folders.push(folder),
|
||||
PtyEvent::Mark(mark) => marks.push(mark),
|
||||
PtyEvent::Status(status) => statuses.push(Some(status)),
|
||||
PtyEvent::Reset => statuses.push(None),
|
||||
PtyEvent::Ask(ask) => asks.push(ask),
|
||||
PtyEvent::Reset => {
|
||||
said.push("reset".to_string());
|
||||
statuses.push(None);
|
||||
}
|
||||
PtyEvent::Title(title) => said.push(format!("title {title}")),
|
||||
PtyEvent::Ask(ask) => {
|
||||
if let Ask::Show(notice) = &ask {
|
||||
said.push(format!("show {}", notice.title));
|
||||
}
|
||||
asks.push(ask);
|
||||
}
|
||||
PtyEvent::Term(Event::ChildExit(status)) => success = status.success(),
|
||||
PtyEvent::Term(Event::Exit) => break,
|
||||
PtyEvent::Term(_) | PtyEvent::Mouse(_) => {}
|
||||
}
|
||||
}
|
||||
(folders, marks, statuses, asks, success)
|
||||
(folders, marks, statuses, asks, said, success)
|
||||
});
|
||||
let term = pty.term.lock();
|
||||
let row = &term.grid()[Line(0)];
|
||||
@@ -65,6 +82,7 @@ fn run(script: &str, size: (usize, usize)) -> Ran {
|
||||
marks,
|
||||
statuses,
|
||||
asks,
|
||||
said,
|
||||
text: text.trim_end().to_string(),
|
||||
prompt: prompt.collect(),
|
||||
success,
|
||||
@@ -161,6 +179,38 @@ fn osc_99_gathers_a_full_encoded_chunk_and_the_lines_base64_carries() {
|
||||
assert_eq!(ran.text, "x");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn titles_reach_the_session_in_their_place_among_notifications() {
|
||||
let parts = [
|
||||
"\\033]2;Old\\007",
|
||||
"\\033[22t",
|
||||
"\\033]2;New\\007",
|
||||
"\\033]9;a\\007",
|
||||
"\\033[23t",
|
||||
"\\033]9;b\\007",
|
||||
"\\033c",
|
||||
"\\033]9;c\\007",
|
||||
];
|
||||
let whole = run(&format!("printf '{}x'", parts.concat()), (40, 4));
|
||||
let split: Vec<String> = parts
|
||||
.iter()
|
||||
.map(|part| format!("printf '{part}'; sleep 0.1"))
|
||||
.collect();
|
||||
let split = run(&format!("{}; printf x", split.join("; ")), (40, 4));
|
||||
let said = [
|
||||
"title Old",
|
||||
"title New",
|
||||
"show a",
|
||||
"title Old",
|
||||
"show b",
|
||||
"reset",
|
||||
"show c",
|
||||
];
|
||||
assert_eq!(whole.said, said, "one write");
|
||||
assert_eq!(split.said, said, "a write a sequence");
|
||||
assert_eq!((whole.text.as_str(), split.text.as_str()), ("x", "x"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_long_escape_of_controls_keeps_the_pty_reading() {
|
||||
let ran = run(
|
||||
|
||||
+69
-6
@@ -14,6 +14,7 @@ use super::{
|
||||
prompts::{self, Prompts},
|
||||
pty::PtyEvent,
|
||||
status,
|
||||
titles::Titles,
|
||||
};
|
||||
|
||||
/// The longest OSC body kept; longer ones are dropped whole.
|
||||
@@ -26,6 +27,7 @@ pub(crate) struct Tapped<P> {
|
||||
mouse: MouseModes,
|
||||
prompts: Prompts,
|
||||
kitty: Kitty,
|
||||
titles: Titles,
|
||||
events: UnboundedSender<PtyEvent>,
|
||||
pid: u32,
|
||||
last: Option<PathBuf>,
|
||||
@@ -39,6 +41,7 @@ impl<P> Tapped<P> {
|
||||
mouse: MouseModes::default(),
|
||||
prompts: Prompts::default(),
|
||||
kitty: Kitty::default(),
|
||||
titles: Titles::default(),
|
||||
events,
|
||||
pid,
|
||||
last: None,
|
||||
@@ -58,9 +61,11 @@ impl<P: EventedReadWrite> io::Read for Tapped<P> {
|
||||
}
|
||||
let count = self.inner.reader().read(&mut buf[..take])?;
|
||||
let read = &buf[..count];
|
||||
let (events, pid, last, kitty) = (&self.events, self.pid, &mut self.last, &mut self.kitty);
|
||||
self.scanner
|
||||
.feed(read, &mut |found| report(found, events, pid, last, kitty));
|
||||
let (events, pid, last) = (&self.events, self.pid, &mut self.last);
|
||||
let (kitty, titles) = (&mut self.kitty, &mut self.titles);
|
||||
self.scanner.feed(read, &mut |found| {
|
||||
report(found, events, pid, last, kitty, titles)
|
||||
});
|
||||
if let Some(mode) = self.mouse.feed(read) {
|
||||
log::info!("pty {pid}: mouse {mode:?}");
|
||||
if events.unbounded_send(PtyEvent::Mouse(mode)).is_err() {
|
||||
@@ -73,19 +78,29 @@ impl<P: EventedReadWrite> io::Read for Tapped<P> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends the session what the scanner found: a full reset, a mark, a program's status or ask, or a folder.
|
||||
/// Sends the session what the scanner found: a full reset, a title, a mark, a program's status or ask, or a folder.
|
||||
fn report(
|
||||
found: Found,
|
||||
events: &UnboundedSender<PtyEvent>,
|
||||
pid: u32,
|
||||
last: &mut Option<PathBuf>,
|
||||
kitty: &mut Kitty,
|
||||
titles: &mut Titles,
|
||||
) {
|
||||
let event = match found {
|
||||
Found::Reset => {
|
||||
log::debug!("pty {pid}: a full reset");
|
||||
titles.reset();
|
||||
PtyEvent::Reset
|
||||
}
|
||||
Found::Push => {
|
||||
titles.push();
|
||||
return;
|
||||
}
|
||||
Found::Pop => match titles.pop() {
|
||||
Some(title) => PtyEvent::Title(title),
|
||||
None => return,
|
||||
},
|
||||
Found::Osc(body, length) => {
|
||||
if let Some(mark) = body.strip_prefix(b"133;").and_then(marks::read) {
|
||||
log::debug!("pty {pid}: {mark:?}");
|
||||
@@ -107,6 +122,11 @@ fn report(
|
||||
*last = Some(folder.clone());
|
||||
}
|
||||
PtyEvent::Folder(folder)
|
||||
} else if let Some(rest) = body
|
||||
.strip_prefix(b"0;")
|
||||
.or_else(|| body.strip_prefix(b"2;"))
|
||||
{
|
||||
PtyEvent::Title(titles.set(rest))
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
@@ -176,15 +196,18 @@ enum State {
|
||||
Ground,
|
||||
Escape,
|
||||
Osc,
|
||||
Csi,
|
||||
}
|
||||
|
||||
/// What the scanner finds: an OSC's body with the sequence's length from OSC through ST, or a full reset.
|
||||
/// What the scanner finds: an OSC's body with the sequence's length from OSC through ST, a full reset, or the title stack's push and pop.
|
||||
pub(crate) enum Found<'a> {
|
||||
Osc(&'a [u8], usize),
|
||||
Reset,
|
||||
Push,
|
||||
Pop,
|
||||
}
|
||||
|
||||
/// Finds each OSC body across reads, ended as vte ends it: at BEL, ESC, CAN or SUB, and each full reset.
|
||||
/// Finds each OSC body across reads, ended as vte ends it: at BEL, ESC, CAN or SUB, each full reset, and CSI 22 t and 23 t.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Osc {
|
||||
state: State,
|
||||
@@ -192,6 +215,9 @@ pub(crate) struct Osc {
|
||||
/// The sequence's bytes so far, the controls the body leaves out too.
|
||||
length: usize,
|
||||
overflow: bool,
|
||||
/// A CSI's first parameter while it reads as a plain number, and its fields so far.
|
||||
first: Option<u32>,
|
||||
fields: usize,
|
||||
}
|
||||
|
||||
impl Osc {
|
||||
@@ -221,6 +247,11 @@ impl Osc {
|
||||
found(Found::Reset);
|
||||
State::Ground
|
||||
}
|
||||
(State::Escape, b'[') => {
|
||||
self.first = Some(0);
|
||||
self.fields = 1;
|
||||
State::Csi
|
||||
}
|
||||
// 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,
|
||||
@@ -246,6 +277,38 @@ impl Osc {
|
||||
}
|
||||
State::Osc
|
||||
}
|
||||
// In a CSI, as in vte: CAN and SUB cancel, ESC restarts, controls run, DEL and high bytes pass by.
|
||||
(State::Csi, 0x18 | 0x1a) => State::Ground,
|
||||
(State::Csi, 0x1b) => State::Escape,
|
||||
(State::Csi, 0x00..=0x1f | 0x7f..=0xff) => State::Csi,
|
||||
(State::Csi, b'0'..=b'9') => {
|
||||
if self.fields == 1 {
|
||||
self.first = self.first.map(|first| {
|
||||
first
|
||||
.saturating_mul(10)
|
||||
.saturating_add(u32::from(byte - b'0'))
|
||||
});
|
||||
}
|
||||
State::Csi
|
||||
}
|
||||
(State::Csi, b';' | b':') => {
|
||||
self.fields += 1;
|
||||
State::Csi
|
||||
}
|
||||
(State::Csi, b't') => {
|
||||
match self.first.filter(|_| self.fields <= PARAMS) {
|
||||
Some(22) => found(Found::Push),
|
||||
Some(23) => found(Found::Pop),
|
||||
_ => {}
|
||||
}
|
||||
State::Ground
|
||||
}
|
||||
(State::Csi, 0x40..=0x7e) => State::Ground,
|
||||
// A private marker or an intermediate makes another sequence.
|
||||
(State::Csi, _) => {
|
||||
self.first = None;
|
||||
State::Csi
|
||||
}
|
||||
(State::Ground, _) => unreachable!("ground scans by position"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ fn scan(chunks: &[&[u8]]) -> Vec<String> {
|
||||
found.push(match thing {
|
||||
Found::Osc(body, _) => String::from_utf8(body.to_vec()).expect("utf-8"),
|
||||
Found::Reset => "RESET".into(),
|
||||
Found::Push => "PUSH".into(),
|
||||
Found::Pop => "POP".into(),
|
||||
})
|
||||
});
|
||||
}
|
||||
@@ -225,3 +227,35 @@ fn the_tap_reads_mouse_modes_as_vte_does_byte_by_byte() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finds_the_title_stack_s_push_and_pop_as_vte_reads_them() {
|
||||
assert_eq!(scan(&[b"\x1b[22;0t\x1b[23;2t"]), ["PUSH", "POP"]);
|
||||
assert_eq!(
|
||||
scan(&[b"\x1b[2", b"2:1\x07t\x1b[23t"]),
|
||||
["PUSH", "POP"],
|
||||
"across reads, a control run"
|
||||
);
|
||||
for other in [
|
||||
&b"\x1b[?22t"[..],
|
||||
b"\x1b[22$t",
|
||||
b"\x1b[1;22t",
|
||||
b"\x1b[t",
|
||||
b"\x1b[22\x18t",
|
||||
b"\x1b[24t",
|
||||
] {
|
||||
assert!(
|
||||
scan(&[other]).is_empty(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(other)
|
||||
);
|
||||
}
|
||||
let mut many = b"\x1b[22".to_vec();
|
||||
many.extend(std::iter::repeat_n(&b";1"[..], 32).flatten());
|
||||
many.push(b't');
|
||||
assert!(scan(&[&many]).is_empty(), "vte drops one past 32 fields");
|
||||
assert_eq!(
|
||||
scan(&[b"\x1b[22t\x1b]2;t\x07\x1b[23t"]),
|
||||
["PUSH", "2;t", "POP"]
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/// The titles alacritty saves before it drops the oldest.
|
||||
const SAVED: usize = 4096;
|
||||
|
||||
/// A program's title as alacritty keeps it, read in the output's order: OSC 0 and 2 set it, CSI 22 t saves it, CSI 23 t brings it back, a full reset empties both.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Titles {
|
||||
/// Empty while the program names none.
|
||||
title: String,
|
||||
saved: Vec<String>,
|
||||
}
|
||||
|
||||
impl Titles {
|
||||
/// Sets the title an OSC 0 or 2 body past its number names: its UTF-8 parameters joined by `;` and trimmed, as alacritty joins them.
|
||||
pub fn set(&mut self, rest: &[u8]) -> String {
|
||||
let parts: Vec<&str> = rest
|
||||
.split(|&byte| byte == b';')
|
||||
.filter_map(|part| std::str::from_utf8(part).ok())
|
||||
.collect();
|
||||
self.title = parts.join(";").trim().to_owned();
|
||||
self.title.clone()
|
||||
}
|
||||
|
||||
/// Saves the title: CSI 22 t.
|
||||
pub fn push(&mut self) {
|
||||
if self.saved.len() == SAVED {
|
||||
self.saved.remove(0);
|
||||
}
|
||||
self.saved.push(self.title.clone());
|
||||
}
|
||||
|
||||
/// Brings the last title saved back: CSI 23 t; none when none was saved.
|
||||
pub fn pop(&mut self) -> Option<String> {
|
||||
let title = self.saved.pop()?;
|
||||
self.title.clone_from(&title);
|
||||
Some(title)
|
||||
}
|
||||
|
||||
/// A full reset empties the title and those saved.
|
||||
pub fn reset(&mut self) {
|
||||
self.title.clear();
|
||||
self.saved.clear();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_title_reads_as_alacritty_joins_it() {
|
||||
let mut titles = Titles::default();
|
||||
assert_eq!(titles.set(b" a;b "), "a;b");
|
||||
assert_eq!(
|
||||
titles.set(b"a;\xff;b"),
|
||||
"a;b",
|
||||
"a part not in UTF-8 passes by"
|
||||
);
|
||||
assert_eq!(titles.set(b""), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_title_saved_comes_back_and_a_reset_empties_both() {
|
||||
let mut titles = Titles::default();
|
||||
assert_eq!(titles.pop(), None, "none saved");
|
||||
titles.set(b"shell");
|
||||
titles.push();
|
||||
titles.set(b"vim");
|
||||
assert_eq!(titles.pop().as_deref(), Some("shell"));
|
||||
assert_eq!(titles.pop(), None);
|
||||
titles.push();
|
||||
titles.reset();
|
||||
assert_eq!(titles.pop(), None, "a reset empties those saved");
|
||||
for at in 0..=SAVED {
|
||||
titles.set(at.to_string().as_bytes());
|
||||
titles.push();
|
||||
}
|
||||
assert_eq!(titles.saved.len(), SAVED);
|
||||
assert_eq!(titles.saved[0], "1", "the oldest went");
|
||||
}
|
||||
}
|
||||
@@ -276,8 +276,10 @@ impl TerminalView {
|
||||
self.ask(ask, cx);
|
||||
self.restate(cx);
|
||||
}
|
||||
PtyEvent::Title(title) => self.title = title,
|
||||
PtyEvent::Reset => {
|
||||
self.records.reset();
|
||||
self.title.clear();
|
||||
self.restate(cx);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user