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
This commit is contained in:
5 files changed
+368
-200
No files matched your search
+211
-106
@@ -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
@@ -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
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user