T35: review round 2: a program's link inside a prompt keeps its target and carries the prompt's number in its id, and a prompt that starts inside a link left open opens it again with the number, so a prompt all in links still starts on its first row; controls vte runs inside an escape go on before it, so only the ESC waits and a long run of them keeps the pty reading
CI / platforms (ubuntu-24.04) (push) Successful in 1m20s
CI / platforms (windows-latest) (push) Successful in 2m28s
CI / macos (push) Successful in 11m21s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-17 00:17:38 -04:00
1 parent 310c219386
commit e2bb615a19
6 files changed
+136 -33

No files matched your search

+53 -24
View File
@@ -7,12 +7,21 @@ use gpui::Context;
use super::{tap::MOST, view::TerminalView};
/// The target of the OSC 8 links prompts become, each named by its prompt's number; no program names a link so.
/// The target of the OSC 8 link a prompt becomes; no program names a link so.
pub(crate) const LINK: &str = "\u{7f}";
/// What a link's id starts with when it carries a prompt's number: the prompt's own link, or a program's inside it.
const NUMBERED: char = '\u{7f}';
/// What ends a prompt's link.
const CLOSE: &[u8] = b"\x1b]8;;\x07";
/// The longest target of a program's link kept, to open it again with a prompt's number.
const TARGET: usize = 256;
/// How many times its size a rewrite makes a sequence at most: a mark of 8 bytes becomes a link of 276.
pub(super) const GROWTH: usize = 36;
const ESC: u8 = 0x1b;
/// Controls vte skips inside an OSC.
@@ -46,6 +55,8 @@ pub(crate) struct Prompts {
prompt: Option<u32>,
/// A program's own link is open.
user: bool,
/// Its target, when short enough to keep.
target: Option<Vec<u8>>,
/// The last prompt's number.
count: u32,
/// What goes to the parser from the last read.
@@ -78,8 +89,6 @@ impl Prompts {
State::Ground
}
(State::Escape, b']') => {
// Controls vte ran inside the escape go on first, as it ran them.
self.out.extend(self.held.drain(1..));
self.held.push(byte);
self.body.clear();
self.overflow = false;
@@ -90,15 +99,15 @@ impl Prompts {
self.held.push(ESC);
State::Escape
}
// vte runs these and stays in the escape.
// vte runs these and stays in the escape, so they go on before it, and only the ESC waits.
(State::Escape, _) if byte == 0x07 || ignored(byte) => {
self.held.push(byte);
self.out.push(byte);
State::Escape
}
(State::Escape, _) => {
// A full reset clears the link the parser writes with.
if byte == b'c' {
(self.prompt, self.user) = (None, false);
(self.prompt, self.user, self.target) = (None, false, None);
}
self.held.push(byte);
self.release();
@@ -167,15 +176,25 @@ impl Prompts {
if opens {
self.count = self.count.wrapping_add(1);
self.prompt = Some(self.count);
(!self.user).then(|| link(self.count))
// A prompt inside a program's link carries its number on that link.
match (self.user, &self.target) {
(false, _) => Some(link(self.count, LINK.as_bytes())),
(true, Some(target)) => Some(link(self.count, target)),
(true, None) => None,
}
} else {
let closes = self.prompt.take().is_some() && !self.user;
closes.then(|| CLOSE.to_vec())
match (self.prompt.take(), self.user, &self.target) {
(Some(_), false, _) => Some(CLOSE.to_vec()),
(Some(_), true, Some(target)) => Some(plain(target)),
_ => None,
}
}
} else if let Some(opens) = user(&self.body) {
} else if let Some((opens, target)) = user(&self.body) {
self.user = opens;
// The program's link ends inside a prompt: the prompt's link takes up again.
self.prompt.filter(|_| !opens).map(link)
self.target = (opens && target.len() <= TARGET).then(|| target.to_vec());
// Inside a prompt a program's link carries the prompt's number; its end takes the prompt's link up again.
let target = if opens { target } else { LINK.as_bytes() };
self.prompt.map(|number| link(number, target))
} else {
None
};
@@ -208,27 +227,37 @@ fn mark(body: &[u8]) -> Option<bool> {
}
}
/// Whether an OSC 8 body opens a program's link, as alacritty reads its target; none for no link at all.
fn user(body: &[u8]) -> Option<bool> {
/// Whether an OSC 8 body opens a program's link, as alacritty reads its target, and that target; none for no link at all.
fn user(body: &[u8]) -> Option<(bool, &[u8])> {
let rest = body.strip_prefix(b"8;")?;
let target = &rest[rest.iter().position(|&byte| byte == b';')? + 1..];
// alacritty joins the target's parts with `;`, each one empty when it is not UTF-8.
Some(
target.contains(&b';')
|| std::str::from_utf8(target).is_ok_and(|target| !target.is_empty()),
)
let opens =
target.contains(&b';') || std::str::from_utf8(target).is_ok_and(|text| !text.is_empty());
Some((opens, target))
}
/// The OSC 8 link that marks prompt `number`'s cells.
fn link(number: u32) -> Vec<u8> {
format!("\x1b]8;id={number};{LINK}\x07").into_bytes()
/// A link to `target` whose id carries prompt `number`.
fn link(number: u32, target: &[u8]) -> Vec<u8> {
let mut link = format!("\x1b]8;id={NUMBERED}{number};").into_bytes();
link.extend_from_slice(target);
link.push(0x07);
link
}
/// The number of the prompt `row` shows, as its link names it.
/// A program's link to `target`, its id its own again.
fn plain(target: &[u8]) -> Vec<u8> {
let mut link = b"\x1b]8;;".to_vec();
link.extend_from_slice(target);
link.push(0x07);
link
}
/// The number of the prompt `row` shows, as a link's id carries it.
fn prompt(row: &Row<Cell>) -> Option<u32> {
row.into_iter().find_map(|cell| {
let link = cell.hyperlink().filter(|link| link.uri() == LINK)?;
link.id().parse().ok()
let link = cell.hyperlink()?;
link.id().strip_prefix(NUMBERED)?.parse().ok()
})
}
+64 -4
View File
@@ -40,15 +40,15 @@ fn target(term: &Term<VoidListener>, line: i32, column: usize) -> Option<String>
fn a_mark_becomes_its_prompt_s_numbered_link() {
assert_eq!(
passed(&[b"\x1b]133;A\x07$ \x1b]133;B\x1b\\x"]),
b"\x1b]8;id=1;\x7f\x07$ \x1b]8;;\x07\x1b\\x"
b"\x1b]8;id=\x7f1;\x7f\x07$ \x1b]8;;\x07\x1b\\x"
);
assert_eq!(
passed(&[b"\x1b]133;A;k=i\x07\x1b]133;D;1\x18"]),
b"\x1b]8;id=1;\x7f\x07\x1b]8;;\x07\x18"
b"\x1b]8;id=\x7f1;\x7f\x07\x1b]8;;\x07\x18"
);
assert_eq!(
passed(&[b"\x1b]133;N\x07\x1b]133;C\x07\x1b]133;A\x07"]),
b"\x1b]8;id=1;\x7f\x07\x1b]8;;\x07\x1b]8;id=2;\x7f\x07"
b"\x1b]8;id=\x7f1;\x7f\x07\x1b]8;;\x07\x1b]8;id=\x7f2;\x7f\x07"
);
assert_eq!(
passed(&[b"\x1b]133;D;0\x07"]),
@@ -109,11 +109,71 @@ fn other_sequences_pass_as_they_came() {
);
}
#[test]
fn a_long_escape_of_controls_goes_on_as_it_comes() {
let mut whole = vec![ESC];
whole.extend(vec![0; 3 << 20]);
whole.extend_from_slice(b"[0mREADY");
let mut prompts = Prompts::default();
let mut out = Vec::new();
for chunk in whole.chunks(64 << 10) {
out.extend_from_slice(prompts.pass(chunk));
assert!(prompts.held() <= 1, "only the ESC waits");
}
assert_eq!(out.len(), whole.len());
assert!(out.ends_with(b"\x1b[0mREADY"));
}
#[test]
fn a_program_s_link_inside_a_prompt_carries_its_number() {
let whole = parsed(
b"\x1b]133;A\x07\x1b]8;;file:///work\x1b\\/work $ \x1b]8;;\x1b\\\x1b]133;B\x07",
12,
2,
);
assert_eq!(
prompt(&whole.grid()[Line(0)]),
Some(1),
"a prompt all in a link"
);
assert_eq!(target(&whole, 0, 6).as_deref(), Some("file:///work"));
let first = b"\x1b]133;A\x07\x1b]8;;file:///w\x1b\\/work\r\n\x1b]8;;\x1b\\$ \x1b]133;B\x07\r\no\r\no\r\no";
let term = parsed(first, 8, 3);
assert_eq!(jump(&term, true), Some(2), "its linked first row starts it");
let open = parsed(
b"\x1b]8;;file:///x\x1b\\x\x1b]133;A\x07y\x1b]133;B\x07z",
8,
2,
);
assert_eq!(
prompt(&open.grid()[Line(0)]),
Some(1),
"a prompt inside a link left open"
);
let (y, z) = (
&open.grid()[Line(0)][Column(1)],
&open.grid()[Line(0)][Column(2)],
);
assert!(
z.hyperlink()
.is_some_and(|link| !link.id().starts_with(NUMBERED))
);
assert_eq!(
target(&open, 0, 2).as_deref(),
Some("file:///x"),
"its own again past the prompt"
);
assert!(
y.hyperlink()
.is_some_and(|link| link.id().starts_with(NUMBERED))
);
}
#[test]
fn controls_vte_runs_inside_an_escape_go_on_before_the_link() {
assert_eq!(
passed(&[b"\x1b\n\x07]133;A\x07$"]),
b"\n\x07\x1b]8;id=1;\x7f\x07$"
b"\n\x07\x1b]8;id=\x7f1;\x7f\x07$"
);
}
+9
View File
@@ -338,6 +338,15 @@ mod tests {
assert_eq!(ran.prompt, [0], "the prompt's cell, not the output's");
}
#[test]
fn a_long_escape_of_controls_keeps_the_pty_reading() {
let ran = run(
"printf '\\033'; head -c 3000000 /dev/zero; printf '[0mREADY'",
(40, 4),
);
assert_eq!(ran.text, "READY");
}
#[test]
fn the_foreground_is_the_program_s_own_group() {
let launch = Launch {
+8 -3
View File
@@ -7,7 +7,12 @@ use alacritty_terminal::{
use futures::channel::mpsc::UnboundedSender;
use polling::{Event, PollMode, Poller};
use super::{marks, mouse::MouseMode, prompts::Prompts, pty::PtyEvent};
use super::{
marks,
mouse::MouseMode,
prompts::{self, Prompts},
pty::PtyEvent,
};
/// The longest OSC body kept; longer ones are dropped whole.
pub(super) const MOST: usize = 4096;
@@ -39,8 +44,8 @@ impl<P> Tapped<P> {
impl<P: EventedReadWrite> io::Read for Tapped<P> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
// A rewrite adds 14 bytes at most to a sequence of 6 or more, so a quarter of the room is read.
let take = buf.len().saturating_sub(self.prompts.held() + 64) / 4;
// What a rewrite adds stays inside the buffer, with room for the held sequence's own.
let take = buf.len().saturating_sub(self.prompts.held() + 300) / prompts::GROWTH;
if take == 0 {
return Ok(0);
}