T35: review round 1: the tap holds an OSC that may be a mark until it ends and reads it as the session does, so 133;AB makes no prompt; each prompt's link carries the prompt's number, so a blank row within a prompt keeps one start; a program's own link keeps its cells and a prompt's link takes up again after it; controls vte runs in an escape go on before the link
CI / platforms (ubuntu-24.04) (push) Successful in 1m3s
CI / platforms (windows-latest) (push) Successful in 2m2s
CI / macos (push) Successful in 12m13s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-17 00:03:34 -04:00
1 parent 15e83f5b7e
commit 310c219386
5 files changed
+368 -200

No files matched your search

+211 -106
View File
@@ -1,159 +1,264 @@
use alacritty_terminal::{
grid::{Dimensions, Scroll},
grid::{Dimensions, Row, Scroll},
index::Line,
term::{
Term,
cell::{Cell, Hyperlink},
},
term::{Term, cell::Cell},
};
use gpui::Context;
use super::view::TerminalView;
use super::{tap::MOST, view::TerminalView};
/// The target of the OSC 8 link each prompt mark becomes; no program names a link so.
/// The target of the OSC 8 links prompts become, each named by its prompt's number; no program names a link so.
pub(crate) const LINK: &str = "\u{7f}";
/// The most bytes a mark's start may span, ignored controls in it too.
const MOST: usize = 64;
/// What ends a prompt's link.
const CLOSE: &[u8] = b"\x1b]8;;\x07";
const ESC: u8 = 0x1b;
/// Controls vte skips inside an OSC.
fn ignored(byte: u8) -> bool {
matches!(byte, 0x00..=0x06 | 0x08..=0x17 | 0x19 | 0x1c..=0x1f)
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
enum State {
#[default]
Ground,
/// An ESC at this index, which may start a mark.
Escape(usize),
/// Inside `ESC ]` from this index, with this much of `133;` matched.
Body(usize, usize),
/// Past a rewritten mark's letter, its options blanked to the end.
Options,
/// After an ESC, held until the next byte says whether an OSC starts.
Escape,
/// An OSC held whole, since it may be a mark or a link to rewrite.
Held,
/// An OSC passed on as it comes.
Passing,
}
/// Turns each OSC 133 mark into an OSC 8 link as it passes to the parser, so the parser marks the cells each prompt prints.
/// Rewrites what goes to the parser so it stamps each prompt's cells with a link of its own, numbered per prompt.
#[derive(Default)]
pub(crate) struct Prompts {
state: State,
/// A mark's start, held from the parser until its letter shows.
/// The escape or OSC held, from its ESC.
held: Vec<u8>,
}
/// Controls vte skips inside a sequence.
fn ignored(byte: u8) -> bool {
matches!(byte, 0x00..=0x06 | 0x08..=0x17 | 0x19 | 0x1c..=0x1f)
/// The OSC's body as vte reads it, the controls it skips left out.
body: Vec<u8>,
/// The body passed the scanner's bound, so it counts for nothing.
overflow: bool,
/// The prompt open now, from its start mark to its end.
prompt: Option<u32>,
/// A program's own link is open.
user: bool,
/// The last prompt's number.
count: u32,
/// What goes to the parser from the last read.
out: Vec<u8>,
}
impl Prompts {
/// The bytes held from the last read, which go before the next.
/// The bytes held from earlier reads.
pub fn held(&self) -> usize {
self.held.len()
}
/// Rewrites the marks in `buf[..held + read]`, the held bytes put first; returns how many bytes go on.
pub fn pass(&mut self, buf: &mut [u8], read: usize) -> usize {
let from = self.held.len();
buf[..from].copy_from_slice(&self.held);
self.held.clear();
let end = from + read;
for at in from..end {
let byte = buf[at];
self.state = match self.state {
State::Ground if byte == ESC => State::Escape(at),
State::Ground => State::Ground,
State::Escape(start) => match byte {
b']' => State::Body(start, 0),
ESC => State::Escape(at),
// vte runs these and stays in the escape.
byte if byte == 0x07 || ignored(byte) => State::Escape(start),
_ => State::Ground,
},
State::Body(start, seen) => match byte {
ESC => State::Escape(at),
0x07 | 0x18 | 0x1a => State::Ground,
byte if ignored(byte) => State::Body(start, seen),
byte if seen < 4 && byte == b"133;"[seen] => State::Body(start, seen + 1),
byte if seen == 4 => match byte {
b'A' | b'N' => link(&mut buf[start..=at], true),
b'B' | b'I' | b'C' | b'D' => link(&mut buf[start..=at], false),
_ => State::Ground,
},
_ => State::Ground,
},
State::Options => match byte {
ESC => State::Escape(at),
0x07 | 0x18 | 0x1a => State::Ground,
byte if ignored(byte) => State::Options,
_ => {
buf[at] = 0;
State::Options
}
},
};
/// What goes to the parser for `read`: marks become links, and a held start waits for its end.
pub fn pass(&mut self, read: &[u8]) -> &[u8] {
self.out.clear();
for &byte in read {
self.step(byte);
}
match self.state {
State::Escape(start) | State::Body(start, _) if end - start <= MOST => {
self.held.extend_from_slice(&buf[start..end]);
self.state = match self.state {
State::Body(_, seen) => State::Body(0, seen),
_ => State::Escape(0),
};
start
&self.out
}
fn step(&mut self, byte: u8) {
self.state = match (self.state, byte) {
(State::Ground, ESC) => {
self.held.push(ESC);
State::Escape
}
State::Escape(_) | State::Body(..) => {
log::warn!("pty: a mark's start over {MOST} bytes passes as it came");
self.state = State::Ground;
end
(State::Ground, _) => {
self.out.push(byte);
State::Ground
}
State::Ground | State::Options => end,
(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;
State::Held
}
(State::Escape, ESC) => {
self.release();
self.held.push(ESC);
State::Escape
}
// vte runs these and stays in the escape.
(State::Escape, _) if byte == 0x07 || ignored(byte) => {
self.held.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.held.push(byte);
self.release();
State::Ground
}
(State::Held | State::Passing, 0x07 | 0x18 | 0x1a) => {
self.end(Some(byte));
State::Ground
}
(State::Held | State::Passing, ESC) => {
self.end(None);
self.held.push(ESC);
State::Escape
}
(State::Held, _) => {
self.held.push(byte);
self.read(byte);
if self.wanted() {
State::Held
} else {
self.release();
State::Passing
}
}
(State::Passing, _) => {
self.out.push(byte);
self.read(byte);
State::Passing
}
};
}
/// Adds `byte` to the body as the tap's scanner does; skipped controls bound the bytes held too.
fn read(&mut self, byte: u8) {
if self.held.len() > 2 * MOST {
self.overflow = true;
}
if ignored(byte) {
return;
}
if self.body.len() < MOST {
self.body.push(byte);
} else {
self.overflow = true;
}
}
/// Whether the OSC held may be a mark, or a link that ends inside a prompt, within the scanner's bounds.
fn wanted(&self) -> bool {
let fits = |prefix: &[u8]| {
let length = self.body.len().min(prefix.len());
self.body[..length] == prefix[..length]
};
!self.overflow && (fits(b"133;") || (self.prompt.is_some() && fits(b"8;")))
}
fn release(&mut self) {
self.out.append(&mut self.held);
}
/// The OSC ends at `end`, BEL, CAN or SUB, or at an ESC when none.
fn end(&mut self, end: Option<u8>) {
let rewrite = if self.overflow {
None
} else if let Some(opens) = self.body.strip_prefix(b"133;").and_then(mark) {
if opens {
self.count = self.count.wrapping_add(1);
self.prompt = Some(self.count);
(!self.user).then(|| link(self.count))
} else {
let closes = self.prompt.take().is_some() && !self.user;
closes.then(|| CLOSE.to_vec())
}
} else if let Some(opens) = 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)
} else {
None
};
match (rewrite, self.state) {
(Some(bytes), State::Held) => {
self.held.clear();
self.out.extend_from_slice(&bytes);
// CAN and SUB end the OSC and run as well.
self.out.extend(end.filter(|&byte| byte != 0x07));
}
(_, State::Held) => {
self.held.extend(end);
self.release();
}
_ => self.out.extend(end),
}
}
}
/// Makes `ESC ] 133;X` an OSC 8 link of the terminal's own: opened at a prompt's start, else closed.
fn link(span: &mut [u8], open: bool) -> State {
let bytes = [b'8', b';', b';', if open { 0x7f } else { 0 }, 0];
let body = span
.iter()
.position(|&byte| byte == b']')
.expect("a mark holds its ]")
+ 1;
let mut next = bytes.into_iter();
for byte in span[body..].iter_mut().filter(|byte| !ignored(**byte)) {
*byte = next.next().expect("a mark's five bytes");
/// Whether a mark's body opens a prompt or ends one, as `marks::read` reads its letter; none for neither.
fn mark(body: &[u8]) -> Option<bool> {
let (&letter, rest) = body.split_first()?;
if !rest.is_empty() && rest[0] != b';' {
return None;
}
match letter {
b'A' | b'N' => Some(true),
b'B' | b'I' | b'C' | b'D' => Some(false),
_ => None,
}
State::Options
}
/// The prompt link a cell carries.
fn prompt_link(cell: &Cell) -> Option<Hyperlink> {
cell.hyperlink().filter(|link| link.uri() == LINK)
/// 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> {
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()),
)
}
/// The prompt `line` shows.
fn prompt<T>(term: &Term<T>, line: Line) -> Option<Hyperlink> {
term.grid()[line].into_iter().find_map(prompt_link)
/// 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()
}
/// Whether `line` starts a prompt: it shows one the line above does not.
fn starts<T>(term: &Term<T>, line: Line) -> bool {
prompt(term, line).is_some_and(|link| {
line == term.topmost_line() || prompt(term, line - 1).as_ref() != Some(&link)
/// The number of the prompt `row` shows, as its link names 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()
})
}
/// The display offset that puts the prompt before or after the view's top at the top, as Ghostty jumps.
pub fn jump<T>(term: &Term<T>, up: bool) -> Option<usize> {
let top = Line(-(term.grid().display_offset() as i32));
// ponytail: a scan of every row on the way; index prompt rows if long histories lag.
let top = -(term.grid().display_offset() as i32);
let (first, last) = (term.topmost_line().0, term.bottommost_line().0);
let number = |line: i32| prompt(&term.grid()[Line(line)]);
// ponytail: a scan of the rows on the way; index prompt rows if long histories lag.
let found = if up {
let mut lines = (term.topmost_line().0..top.0).rev().map(Line);
lines.find(|&line| starts(term, line))
// The nearest prompt above the view's top, then its first row, blank rows within it passed.
let (line, own) = (first..top)
.rev()
.find_map(|line| number(line).map(|own| (line, own)))?;
let mut start = line;
for above in (first..line).rev() {
match number(above) {
Some(other) if other != own => break,
Some(_) => start = above,
None => {}
}
}
start
} else {
let mut lines = (top.0 + 1..=term.bottommost_line().0).map(Line);
lines.find(|&line| starts(term, line))
}?;
// The prompt at or above the view's top may go on below it; the next one starts.
let here = (first..=top).rev().find_map(number);
(top + 1..=last).find(|&line| number(line).is_some_and(|other| Some(other) != here))?
};
// A prompt on the screen means the bottom, which the view cannot pass.
Some((-found.0).max(0) as usize)
Some((-found).max(0) as usize)
}
impl TerminalView {
+146 -85
View File
@@ -6,37 +6,72 @@ use alacritty_terminal::{
};
use super::*;
use crate::term::marks::{self, Mark};
/// What the parser gets from `chunks`, each one read.
fn passed(chunks: &[&[u8]]) -> Vec<u8> {
let mut prompts = Prompts::default();
let mut out = Vec::new();
for chunk in chunks {
let held = prompts.held();
let mut buf = vec![0; held + chunk.len()];
buf[held..].copy_from_slice(chunk);
let count = prompts.pass(&mut buf, chunk.len());
out.extend_from_slice(&buf[..count]);
out.extend_from_slice(prompts.pass(chunk));
}
out
}
#[test]
fn a_mark_becomes_a_link_of_the_same_length() {
assert_eq!(passed(&[b"\x1b]133;A\x07"]), b"\x1b]8;;\x7f\0\x07");
assert_eq!(passed(&[b"\x1b]133;N\x07"]), b"\x1b]8;;\x7f\0\x07");
assert_eq!(passed(&[b"\x1b]133;B\x1b\\"]), b"\x1b]8;;\0\0\x1b\\");
assert_eq!(passed(&[b"\x1b]133;D;1\x07"]), b"\x1b]8;;\0\0\0\0\x07");
assert_eq!(
passed(&[b"\x1b]133;A;k=i\x07x"]),
b"\x1b]8;;\x7f\0\0\0\0\0\x07x"
/// A terminal `columns` wide and `lines` high after `bytes` passed through the rewrite.
fn parsed(bytes: &[u8], columns: usize, lines: usize) -> Term<VoidListener> {
let mut term = Term::new(
Config::default(),
&TermSize::new(columns, lines),
VoidListener,
);
assert_eq!(passed(&[b"\x1b]1\x0033;C\x07"]), b"\x1b]8\x00;;\0\0\x07");
let mut parser: Processor = Processor::new();
parser.advance(&mut term, &passed(&[bytes]));
term
}
/// The target of the link at a cell.
fn target(term: &Term<VoidListener>, line: i32, column: usize) -> Option<String> {
let cell = &term.grid()[Line(line)][Column(column)];
cell.hyperlink().map(|link| link.uri().to_string())
}
#[test]
fn a_mark_split_across_reads_waits_for_its_letter() {
let whole: &[u8] = b"a\x1b]133;A;k=i\x07$ \x1b]133;B\x1b\\b";
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"
);
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"
);
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"
);
assert_eq!(
passed(&[b"\x1b]133;D;0\x07"]),
b"\x1b]133;D;0\x07",
"an end with no prompt open passes"
);
}
#[test]
fn the_rewrite_reads_a_mark_as_the_session_hears_it() {
let bodies = [
"A", "A;k=i", "N", "B", "I", "C;x", "D;1", "D;abc", "D", "", "AB", "L", "P;k=i", "Q", "a",
];
for body in bodies {
let heard = marks::read(body.as_bytes()).map(|mark| mark == Mark::Prompt);
assert_eq!(mark(body.as_bytes()), heard, "{body}");
}
}
#[test]
fn a_mark_split_across_reads_comes_out_whole() {
let whole: &[u8] =
b"a\x1b]133;A;k=i\x07\x1b]8;;file:///w\x1b\\w\x1b]8;;\x1b\\$ \x1b]133;B\x1b\\b";
let once = passed(&[whole]);
for cut in 0..=whole.len() {
let (first, second) = whole.split_at(cut);
@@ -49,67 +84,76 @@ fn a_mark_split_across_reads_waits_for_its_letter() {
#[test]
fn other_sequences_pass_as_they_came() {
for bytes in [
&b"\x1b]1337;A\x07"[..],
b"\x1b]13;A\x07",
b"\x1b]133;P;k=i\x07",
b"\x1b]133;L\x07",
b"\x1b]133;\x07",
b"\x1b]133\x18;A\x07",
b"\x1b]0;133;A\x07",
b"\x1b[31m\x1bc",
b"text",
"\x1b]133;AB\x07x",
"\x1b]1337;A\x07",
"\x1b]133;P;k=i\x07",
"\x1b]133;\x07",
"\x1b]0;133;A\x07",
"\x1b[31m\x1bc",
"\x1b]8;;file:///x\x07x\x1b]8;;\x07",
"text",
] {
assert_eq!(
passed(&[bytes]),
bytes,
"{}",
String::from_utf8_lossy(bytes)
);
assert_eq!(passed(&[bytes.as_bytes()]), bytes.as_bytes(), "{bytes:?}");
}
}
#[test]
fn a_mark_s_start_waits_no_longer_than_its_cap() {
let mut long = b"\x1b]".to_vec();
long.extend([0u8; MOST]);
assert_eq!(passed(&[&long]), long);
}
/// A terminal `columns` wide and two lines high after `bytes` passed through the rewrite.
fn parsed(bytes: &[u8], columns: usize) -> Term<VoidListener> {
let mut term = Term::new(Config::default(), &TermSize::new(columns, 2), VoidListener);
let mut parser: Processor = Processor::new();
parser.advance(&mut term, &passed(&[bytes]));
term
}
#[test]
fn a_prompt_s_cells_carry_its_link_and_its_input_none() {
let term = parsed(b"x\r\n\x1b]133;A\x07\x1b[31m$\x1b[0m \x1b]133;B\x07ls", 8);
let cell = |line, column| prompt_link(&term.grid()[Line(line)][Column(column)]);
assert!(cell(0, 0).is_none());
assert!(
cell(1, 0).is_some(),
"a prompt keeps its link through SGR 0"
let mut long = b"\x1b]133;A;".to_vec();
long.extend([b'x'; MOST]);
long.push(0x07);
assert_eq!(passed(&[&long]), long, "past the scanner's bound");
let mut skipped = b"\x1b]133;A".to_vec();
skipped.extend([0; 3 * MOST]);
skipped.push(0x07);
assert_eq!(
passed(&[&skipped]),
skipped,
"skipped controls bound the bytes held"
);
assert_eq!(cell(1, 0), cell(1, 1));
assert!(cell(1, 2).is_none(), "input");
assert!(starts(&term, Line(1)));
assert!(!starts(&term, Line(0)));
}
#[test]
fn a_prompt_over_two_rows_starts_at_its_first() {
let term = parsed(b"\x1b]133;A\x07~/dir\r\n> \x1b]133;B\x07", 8);
assert!(starts(&term, Line(0)));
assert!(!starts(&term, Line(1)), "the same prompt goes on");
let next = parsed(b"\x1b]133;A\x07$ \x1b]133;B\x07\r\n\x1b]133;A\x07$ ", 8);
assert!(
starts(&next, Line(1)),
"an empty line's next prompt is its own"
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$"
);
}
#[test]
fn a_prompt_s_cells_carry_its_link_through_sgr_0_and_its_input_none() {
let term = parsed(
b"x\r\n\x1b]133;A\x07\x1b[31m$\x1b[0m \x1b]133;B\x07ls",
8,
2,
);
assert_eq!(prompt(&term.grid()[Line(0)]), None);
assert_eq!(prompt(&term.grid()[Line(1)]), Some(1));
assert_eq!(target(&term, 1, 1).as_deref(), Some(LINK));
assert_eq!(target(&term, 1, 2), None, "input");
}
#[test]
fn a_program_s_own_link_and_the_prompt_s_keep_each_other() {
let term = parsed(
b"\x1b]133;A\x07\x1b]8;;file:///work\x1b\\/work\x1b]8;;\x1b\\ $ \x1b]133;B\x07",
12,
2,
);
assert_eq!(target(&term, 0, 0).as_deref(), Some("file:///work"));
assert_eq!(target(&term, 0, 6).as_deref(), Some(LINK));
assert_eq!(prompt(&term.grid()[Line(0)]), Some(1));
let term = parsed(
b"\x1b]8;;file:///x\x1b\\x\x1b]133;A\x07y\x1b]8;;\x1b\\z\x1b]133;B\x07",
12,
2,
);
assert_eq!(target(&term, 0, 0).as_deref(), Some("file:///x"));
assert_eq!(
target(&term, 0, 1).as_deref(),
Some("file:///x"),
"a prompt inside a link keeps it"
);
assert_eq!(target(&term, 0, 2).as_deref(), Some(LINK));
}
/// Prompts on lines 0, 3 and 6 of a history four lines high, the last on the screen.
fn history() -> Term<VoidListener> {
let mut bytes = Vec::new();
@@ -119,17 +163,14 @@ fn history() -> Term<VoidListener> {
);
}
bytes.extend_from_slice(b"\x1b]133;A\x07$ \x1b]133;B\x07");
let mut term = Term::new(Config::default(), &TermSize::new(8, 4), VoidListener);
let mut parser: Processor = Processor::new();
parser.advance(&mut term, &passed(&[&bytes]));
term
parsed(&bytes, 8, 4)
}
#[test]
fn up_and_down_go_to_the_prompts_beside_the_view_s_top() {
let mut term = history();
assert_eq!(term.grid().history_size(), 6);
// From the bottom: the prompt at the view's top (line 0) is passed, the one before it found.
// From the bottom: the prompt at the view's top is passed, the one before it found.
assert_eq!(jump(&term, true), Some(3));
term.scroll_display(Scroll::Delta(3));
assert_eq!(jump(&term, true), Some(6));
@@ -145,16 +186,36 @@ fn up_and_down_go_to_the_prompts_beside_the_view_s_top() {
}
#[test]
fn a_prompt_over_rows_starts_at_its_first_blank_rows_within_it_too() {
let bytes = b"\x1b]133;A\x07FIRST\r\n\r\nLAST $ \x1b]133;B\x07\r\nout1\r\nout2\r\nout3\r\nout4";
let mut term = parsed(bytes, 8, 3);
assert_eq!(term.grid().history_size(), 4);
assert_eq!(jump(&term, true), Some(4), "FIRST, not LAST");
term.scroll_display(Scroll::Delta(4));
assert_eq!(jump(&term, false), None, "LAST is the same prompt");
let next = parsed(b"\x1b]133;A\x07$ \x1b]133;B\x07\r\n\x1b]133;A\x07$ ", 8, 2);
assert_eq!(
(prompt(&next.grid()[Line(0)]), prompt(&next.grid()[Line(1)])),
(Some(1), Some(2)),
"an empty line's next prompt is its own"
);
}
#[test]
fn a_prompt_keeps_its_start_through_a_resize() {
let bytes = b"\x1b]133;A\x07~/folder $ \x1b]133;B\x07ls\r\nout\r\n\x1b]133;A\x07$ ";
let mut term = Term::new(Config::default(), &TermSize::new(12, 4), VoidListener);
let mut parser: Processor = Processor::new();
parser.advance(&mut term, &passed(&[bytes]));
term.resize(TermSize::new(6, 4));
let lines = term.topmost_line().0..=term.bottommost_line().0;
let starts: Vec<i32> = lines.filter(|&line| starts(&term, Line(line))).collect();
// The first prompt wraps over two rows and starts at its first; the last starts two below.
let first = starts.first().copied().expect("a prompt");
assert_eq!(starts.len(), 2, "{starts:?}");
assert!(!starts.contains(&(first + 1)), "{starts:?}");
let bytes =
b"\x1b]133;A\x07~/folder $ \x1b]133;B\x07ls\r\nout\r\nout\r\nout\r\n\x1b]133;A\x07$ ";
let mut term = parsed(bytes, 12, 3);
term.resize(TermSize::new(6, 3));
let rows = term.topmost_line().0..=term.bottommost_line().0;
let numbers: Vec<Option<u32>> = rows.map(|line| prompt(&term.grid()[Line(line)])).collect();
let first = numbers.iter().position(|number| *number == Some(1));
// The first prompt wraps over two rows; the view jumps to the first of them.
assert_eq!(
numbers.iter().filter(|number| **number == Some(1)).count(),
2
);
let offset = jump(&term, true).expect("a prompt above");
let top = term.topmost_line().0 + first.expect("prompt 1") as i32;
assert_eq!(offset as i32, -top);
}
+9 -7
View File
@@ -10,7 +10,7 @@ use polling::{Event, PollMode, Poller};
use super::{marks, mouse::MouseMode, prompts::Prompts, pty::PtyEvent};
/// The longest OSC body kept; longer ones are dropped whole.
const MOST: usize = 4096;
pub(super) const MOST: usize = 4096;
/// A pty whose reader shows each byte to its scanners before the parser.
pub(crate) struct Tapped<P> {
@@ -39,13 +39,13 @@ impl<P> Tapped<P> {
impl<P: EventedReadWrite> io::Read for Tapped<P> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let held = self.prompts.held();
// A buffer with no room past the held bytes waits for the parser to empty it.
if buf.len() <= held {
// 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;
if take == 0 {
return Ok(0);
}
let count = self.inner.reader().read(&mut buf[held..])?;
let read = &buf[held..held + count];
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 |body| {
if let Some(mark) = body.strip_prefix(b"133;").and_then(marks::read) {
@@ -74,7 +74,9 @@ impl<P: EventedReadWrite> io::Read for Tapped<P> {
log::debug!("pty {pid}: a mouse mode after its session closed");
}
}
Ok(self.prompts.pass(buf, count))
let out = self.prompts.pass(&buf[..count]);
buf[..out.len()].copy_from_slice(out);
Ok(out.len())
}
}