Preserve native terminal behavior through trusted prompts
This commit is contained in:
parent
7e68cfdc1f
commit
f989f3bc01
3 changed files with 997 additions and 151 deletions
|
|
@ -245,8 +245,11 @@ impl SessionTerminalEngine {
|
|||
if authoritative != TerminalRoute::Sandbox || terminal.releasing {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
if let Some(restoration) = terminal.retained.take_restoration() {
|
||||
terminal.pending_render.extend(restoration);
|
||||
if terminal.retained.take_restoration() {
|
||||
return Ok(vec![SessionTerminalMessage::Output(TerminalOutput {
|
||||
session_id,
|
||||
bytes: Vec::new(),
|
||||
})]);
|
||||
}
|
||||
if let Some(message) = take_rendered_output(terminal, session_id) {
|
||||
return Ok(vec![message]);
|
||||
|
|
@ -454,9 +457,10 @@ impl SessionTerminalEngine {
|
|||
&mut authorize_guest,
|
||||
)?;
|
||||
let mut rendered = terminal.pending_render.drain(..).collect::<Vec<_>>();
|
||||
let restore_presentation = connected && presentation_route != TerminalRoute::Sandbox;
|
||||
if connected && presentation_route != TerminalRoute::Sandbox {
|
||||
terminal.retained.obscure_client();
|
||||
rendered.extend(terminal.retained.take_restoration().unwrap_or_default());
|
||||
terminal.retained.take_restoration();
|
||||
}
|
||||
for message in output {
|
||||
if let SessionTerminalMessage::Output(output) = message {
|
||||
|
|
@ -465,15 +469,19 @@ impl SessionTerminalEngine {
|
|||
}
|
||||
}
|
||||
rendered.truncate(MAX_DRAIN_BYTES);
|
||||
let mut output = rendered
|
||||
.chunks(MAX_OUTPUT_BYTES)
|
||||
.map(|bytes| {
|
||||
SessionTerminalMessage::Output(TerminalOutput {
|
||||
session_id,
|
||||
bytes: bytes.to_vec(),
|
||||
})
|
||||
let mut output = Vec::new();
|
||||
if restore_presentation {
|
||||
output.push(SessionTerminalMessage::Output(TerminalOutput {
|
||||
session_id,
|
||||
bytes: Vec::new(),
|
||||
}));
|
||||
}
|
||||
output.extend(rendered.chunks(MAX_OUTPUT_BYTES).map(|bytes| {
|
||||
SessionTerminalMessage::Output(TerminalOutput {
|
||||
session_id,
|
||||
bytes: bytes.to_vec(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
}));
|
||||
if guest_absent {
|
||||
let clean = clean_terminal(generation_id, session_id)?;
|
||||
self.recorder.clean(clean)?;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
const SYNCHRONIZED_OUTPUT_START: &[u8] = b"\x1b[?2026h";
|
||||
const SYNCHRONIZED_OUTPUT_END: &[u8] = b"\x1b[?2026l";
|
||||
const MAX_CONTROL_SEQUENCE_BYTES: usize = 4096;
|
||||
|
||||
#[derive(Default)]
|
||||
struct TerminalCallbacks {
|
||||
|
|
@ -21,6 +20,12 @@ impl vt100::Callbacks for TerminalCallbacks {
|
|||
.copied()
|
||||
.unwrap_or(0);
|
||||
if intermediate_one.is_none()
|
||||
&& intermediate_two.is_none()
|
||||
&& character == 'n'
|
||||
&& first == 5
|
||||
{
|
||||
self.replies.extend_from_slice(b"\x1b[0n");
|
||||
} else if intermediate_one.is_none()
|
||||
&& intermediate_two.is_none()
|
||||
&& character == 'n'
|
||||
&& first == 6
|
||||
|
|
@ -38,9 +43,30 @@ impl vt100::Callbacks for TerminalCallbacks {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
enum ScanState {
|
||||
#[default]
|
||||
Ground,
|
||||
Escape,
|
||||
EscapeSequence,
|
||||
DiscardEscapeSequence,
|
||||
Csi,
|
||||
DiscardCsi,
|
||||
Osc {
|
||||
escape: bool,
|
||||
},
|
||||
ControlString {
|
||||
escape: bool,
|
||||
},
|
||||
Utf8 {
|
||||
remaining: u8,
|
||||
},
|
||||
}
|
||||
|
||||
pub(super) struct RetainedTerminal {
|
||||
parser: vt100::Parser<TerminalCallbacks>,
|
||||
client_initialized: bool,
|
||||
scan: ScanState,
|
||||
sequence: Vec<u8>,
|
||||
restore_pending: bool,
|
||||
}
|
||||
|
||||
|
|
@ -53,52 +79,324 @@ impl RetainedTerminal {
|
|||
0,
|
||||
TerminalCallbacks::default(),
|
||||
),
|
||||
client_initialized: false,
|
||||
scan: ScanState::Ground,
|
||||
sequence: Vec::new(),
|
||||
restore_pending: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn process(&mut self, bytes: &[u8]) -> (Vec<u8>, Vec<u8>) {
|
||||
let previous = self.parser.screen().clone();
|
||||
self.parser.process(bytes);
|
||||
let rendered = if self.client_initialized {
|
||||
self.parser.screen().state_diff(&previous)
|
||||
} else {
|
||||
self.client_initialized = true;
|
||||
self.redraw()
|
||||
};
|
||||
let mut forwarded = Vec::with_capacity(bytes.len());
|
||||
for &byte in bytes {
|
||||
self.process_byte(byte, &mut forwarded);
|
||||
}
|
||||
let replies = std::mem::take(&mut self.parser.callbacks_mut().replies);
|
||||
(rendered, replies)
|
||||
(forwarded, replies)
|
||||
}
|
||||
|
||||
pub(super) fn resize(&mut self, rows: u16, columns: u16) {
|
||||
self.parser.screen_mut().set_size(rows, columns);
|
||||
if self.client_initialized {
|
||||
self.restore_pending = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn obscure_client(&mut self) {
|
||||
self.restore_pending = true;
|
||||
}
|
||||
|
||||
pub(super) fn take_restoration(&mut self) -> Option<Vec<u8>> {
|
||||
if !self.restore_pending {
|
||||
return None;
|
||||
}
|
||||
self.restore_pending = false;
|
||||
self.client_initialized = true;
|
||||
Some(self.redraw())
|
||||
pub(super) fn take_restoration(&mut self) -> bool {
|
||||
std::mem::take(&mut self.restore_pending)
|
||||
}
|
||||
|
||||
fn redraw(&self) -> Vec<u8> {
|
||||
let mut rendered = Vec::new();
|
||||
rendered.extend_from_slice(SYNCHRONIZED_OUTPUT_START);
|
||||
rendered.extend_from_slice(b"\x1b[2J\x1b[H");
|
||||
rendered.extend(self.parser.screen().state_formatted());
|
||||
rendered.extend_from_slice(SYNCHRONIZED_OUTPUT_END);
|
||||
rendered
|
||||
fn process_byte(&mut self, byte: u8, forwarded: &mut Vec<u8>) {
|
||||
match self.scan {
|
||||
ScanState::Ground => self.process_ground(byte, forwarded),
|
||||
ScanState::Escape => self.process_escape(byte, forwarded),
|
||||
ScanState::EscapeSequence => self.process_escape_sequence(byte, forwarded),
|
||||
ScanState::DiscardEscapeSequence => {
|
||||
if byte == 0x1b {
|
||||
self.sequence.clear();
|
||||
self.sequence.push(byte);
|
||||
self.scan = ScanState::Escape;
|
||||
} else if (0x30..=0x7e).contains(&byte) {
|
||||
self.scan = ScanState::Ground;
|
||||
}
|
||||
}
|
||||
ScanState::Csi => self.process_csi(byte, forwarded),
|
||||
ScanState::DiscardCsi => {
|
||||
if (0x40..=0x7e).contains(&byte) {
|
||||
self.scan = ScanState::Ground;
|
||||
}
|
||||
}
|
||||
ScanState::Osc { escape } => {
|
||||
if matches!(byte, 0x07 | 0x9c) || (escape && byte == b'\\') {
|
||||
self.scan = ScanState::Ground;
|
||||
} else {
|
||||
self.scan = ScanState::Osc {
|
||||
escape: byte == 0x1b,
|
||||
};
|
||||
}
|
||||
}
|
||||
ScanState::ControlString { escape } => {
|
||||
if byte == 0x9c || (escape && byte == b'\\') {
|
||||
self.scan = ScanState::Ground;
|
||||
} else {
|
||||
self.scan = ScanState::ControlString {
|
||||
escape: byte == 0x1b,
|
||||
};
|
||||
}
|
||||
}
|
||||
ScanState::Utf8 { remaining } => self.process_utf8(byte, remaining, forwarded),
|
||||
}
|
||||
}
|
||||
|
||||
fn process_ground(&mut self, byte: u8, forwarded: &mut Vec<u8>) {
|
||||
match byte {
|
||||
0x1b => {
|
||||
self.sequence.clear();
|
||||
self.sequence.push(byte);
|
||||
self.scan = ScanState::Escape;
|
||||
}
|
||||
0x90 | 0x98 | 0x9e | 0x9f => {
|
||||
self.scan = ScanState::ControlString { escape: false };
|
||||
}
|
||||
0x9b => self.scan = ScanState::DiscardCsi,
|
||||
0x9d => self.scan = ScanState::Osc { escape: false },
|
||||
0xc2..=0xdf => self.begin_utf8(byte, 1),
|
||||
0xe0..=0xef => self.begin_utf8(byte, 2),
|
||||
0xf0..=0xf4 => self.begin_utf8(byte, 3),
|
||||
0x80..=0xbf | 0xf5..=0xff => {}
|
||||
_ if !byte.is_ascii_control() || matches!(byte, 0x08..=0x0d) => {
|
||||
self.accept(std::slice::from_ref(&byte), forwarded);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn begin_utf8(&mut self, byte: u8, remaining: u8) {
|
||||
self.sequence.clear();
|
||||
self.sequence.push(byte);
|
||||
self.scan = ScanState::Utf8 { remaining };
|
||||
}
|
||||
|
||||
fn process_utf8(&mut self, byte: u8, remaining: u8, forwarded: &mut Vec<u8>) {
|
||||
if (0x80..=0xbf).contains(&byte) {
|
||||
self.sequence.push(byte);
|
||||
if remaining == 1 {
|
||||
let sequence = std::mem::take(&mut self.sequence);
|
||||
if std::str::from_utf8(&sequence).is_ok() {
|
||||
self.accept(&sequence, forwarded);
|
||||
}
|
||||
self.scan = ScanState::Ground;
|
||||
} else {
|
||||
self.scan = ScanState::Utf8 {
|
||||
remaining: remaining - 1,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
self.sequence.clear();
|
||||
self.scan = ScanState::Ground;
|
||||
self.process_ground(byte, forwarded);
|
||||
}
|
||||
}
|
||||
|
||||
fn process_escape(&mut self, byte: u8, forwarded: &mut Vec<u8>) {
|
||||
match byte {
|
||||
b'[' => {
|
||||
self.sequence.push(byte);
|
||||
self.scan = ScanState::Csi;
|
||||
}
|
||||
b']' => {
|
||||
self.sequence.clear();
|
||||
self.scan = ScanState::Osc { escape: false };
|
||||
}
|
||||
b'P' | b'X' | b'^' | b'_' => {
|
||||
self.sequence.clear();
|
||||
self.scan = ScanState::ControlString { escape: false };
|
||||
}
|
||||
0x20..=0x2f => {
|
||||
self.sequence.push(byte);
|
||||
self.scan = ScanState::EscapeSequence;
|
||||
}
|
||||
0x30..=0x7e => {
|
||||
self.sequence.push(byte);
|
||||
let sequence = std::mem::take(&mut self.sequence);
|
||||
if qualified_escape(&sequence) {
|
||||
self.accept(&sequence, forwarded);
|
||||
}
|
||||
self.scan = ScanState::Ground;
|
||||
}
|
||||
0x1b => {
|
||||
self.sequence.clear();
|
||||
self.sequence.push(byte);
|
||||
}
|
||||
_ => {
|
||||
self.sequence.clear();
|
||||
self.scan = ScanState::Ground;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_escape_sequence(&mut self, byte: u8, forwarded: &mut Vec<u8>) {
|
||||
if (0x20..=0x2f).contains(&byte) {
|
||||
self.sequence.push(byte);
|
||||
if self.sequence.len() > MAX_CONTROL_SEQUENCE_BYTES {
|
||||
self.sequence.clear();
|
||||
self.scan = ScanState::DiscardEscapeSequence;
|
||||
}
|
||||
} else if (0x30..=0x7e).contains(&byte) {
|
||||
self.sequence.push(byte);
|
||||
let sequence = std::mem::take(&mut self.sequence);
|
||||
if qualified_escape(&sequence) {
|
||||
self.accept(&sequence, forwarded);
|
||||
}
|
||||
self.scan = ScanState::Ground;
|
||||
} else if byte == 0x1b {
|
||||
self.sequence.clear();
|
||||
self.sequence.push(byte);
|
||||
self.scan = ScanState::Escape;
|
||||
} else {
|
||||
self.sequence.clear();
|
||||
self.scan = ScanState::Ground;
|
||||
}
|
||||
}
|
||||
|
||||
fn process_csi(&mut self, byte: u8, forwarded: &mut Vec<u8>) {
|
||||
if byte == 0x1b {
|
||||
self.sequence.clear();
|
||||
self.sequence.push(byte);
|
||||
self.scan = ScanState::Escape;
|
||||
return;
|
||||
}
|
||||
self.sequence.push(byte);
|
||||
if self.sequence.len() > MAX_CONTROL_SEQUENCE_BYTES {
|
||||
self.sequence.clear();
|
||||
self.scan = ScanState::DiscardCsi;
|
||||
} else if (0x40..=0x7e).contains(&byte) {
|
||||
let sequence = std::mem::take(&mut self.sequence);
|
||||
if tracked_query(&sequence) {
|
||||
self.parser.process(&sequence);
|
||||
} else if qualified_csi(&sequence) {
|
||||
self.accept(&sequence, forwarded);
|
||||
}
|
||||
self.scan = ScanState::Ground;
|
||||
} else if !(0x20..=0x3f).contains(&byte) {
|
||||
self.sequence.clear();
|
||||
self.scan = ScanState::Ground;
|
||||
}
|
||||
}
|
||||
|
||||
fn accept(&mut self, bytes: &[u8], forwarded: &mut Vec<u8>) {
|
||||
self.parser.process(bytes);
|
||||
forwarded.extend_from_slice(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
fn qualified_escape(sequence: &[u8]) -> bool {
|
||||
matches!(
|
||||
sequence,
|
||||
b"\x1b7"
|
||||
| b"\x1b8"
|
||||
| b"\x1bD"
|
||||
| b"\x1bE"
|
||||
| b"\x1bH"
|
||||
| b"\x1bM"
|
||||
| b"\x1b="
|
||||
| b"\x1b>"
|
||||
| b"\x1bc"
|
||||
) || (sequence.len() == 3
|
||||
&& sequence[0] == 0x1b
|
||||
&& matches!(sequence[1], b'(' | b')' | b'*' | b'+' | b'-' | b'.' | b'/')
|
||||
&& (0x30..=0x7e).contains(&sequence[2]))
|
||||
|| sequence == b"\x1b#8"
|
||||
}
|
||||
|
||||
fn tracked_query(sequence: &[u8]) -> bool {
|
||||
matches!(sequence, b"\x1b[5n" | b"\x1b[6n" | b"\x1b[c" | b"\x1b[0c")
|
||||
}
|
||||
|
||||
fn qualified_csi(sequence: &[u8]) -> bool {
|
||||
if sequence.len() < 3 || !sequence.starts_with(b"\x1b[") {
|
||||
return false;
|
||||
}
|
||||
let final_byte = sequence[sequence.len() - 1];
|
||||
let body = &sequence[2..sequence.len() - 1];
|
||||
if matches!(final_byte, b'c' | b'n' | b't' | b'i') {
|
||||
return false;
|
||||
}
|
||||
if matches!(final_byte, b'h' | b'l') {
|
||||
return qualified_mode(body);
|
||||
}
|
||||
if final_byte == b'q' {
|
||||
return body.last() == Some(&b' ')
|
||||
&& body[..body.len() - 1]
|
||||
.iter()
|
||||
.all(|byte| byte.is_ascii_digit() || *byte == b';');
|
||||
}
|
||||
if !matches!(
|
||||
final_byte,
|
||||
b'@' | b'A'
|
||||
| b'B'
|
||||
| b'C'
|
||||
| b'D'
|
||||
| b'E'
|
||||
| b'F'
|
||||
| b'G'
|
||||
| b'H'
|
||||
| b'I'
|
||||
| b'J'
|
||||
| b'K'
|
||||
| b'L'
|
||||
| b'M'
|
||||
| b'P'
|
||||
| b'S'
|
||||
| b'T'
|
||||
| b'X'
|
||||
| b'Z'
|
||||
| b'`'
|
||||
| b'a'
|
||||
| b'b'
|
||||
| b'd'
|
||||
| b'e'
|
||||
| b'f'
|
||||
| b'g'
|
||||
| b'm'
|
||||
| b'r'
|
||||
| b's'
|
||||
| b'u'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
body.iter()
|
||||
.all(|byte| byte.is_ascii_digit() || matches!(byte, b';' | b':'))
|
||||
}
|
||||
|
||||
fn qualified_mode(body: &[u8]) -> bool {
|
||||
if let Some(parameters) = body.strip_prefix(b"?") {
|
||||
return parameters.split(|byte| *byte == b';').all(|parameter| {
|
||||
matches!(
|
||||
parameter,
|
||||
b"1" | b"7"
|
||||
| b"12"
|
||||
| b"25"
|
||||
| b"47"
|
||||
| b"1000"
|
||||
| b"1002"
|
||||
| b"1003"
|
||||
| b"1004"
|
||||
| b"1005"
|
||||
| b"1006"
|
||||
| b"1015"
|
||||
| b"1047"
|
||||
| b"1048"
|
||||
| b"1049"
|
||||
| b"2004"
|
||||
| b"2026"
|
||||
)
|
||||
});
|
||||
}
|
||||
!body.is_empty()
|
||||
&& body
|
||||
.iter()
|
||||
.all(|byte| byte.is_ascii_digit() || *byte == b';')
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -106,72 +404,73 @@ mod tests {
|
|||
use super::RetainedTerminal;
|
||||
|
||||
#[test]
|
||||
fn parser_and_rendering_are_fragmentation_independent() {
|
||||
let bytes = b"\x1b[2J\x1b[Hplain\xe7\x95\x8c\x1b[2;3H\x1b[1;38;2;12;34;56mstyled\x1b[0m";
|
||||
let expected = retained_state(&[bytes]);
|
||||
fn qualified_stream_and_replies_are_fragmentation_independent() {
|
||||
let bytes = b"plain\x1b[38;2;12;34;56mstyled\x1b[0m\x1b]52;c;ZXZpbA==\x07\x1b[7;11H\x1b[5n\x1b[6n\x1b[c";
|
||||
let expected = qualified(&[bytes]);
|
||||
for split in 0..=bytes.len() {
|
||||
assert_eq!(
|
||||
retained_state(&[&bytes[..split], &bytes[split..]]),
|
||||
expected
|
||||
);
|
||||
assert_eq!(qualified(&[&bytes[..split], &bytes[split..]]), expected);
|
||||
}
|
||||
assert_eq!(
|
||||
expected,
|
||||
(
|
||||
b"plain\x1b[38;2;12;34;56mstyled\x1b[0m\x1b[7;11H".to_vec(),
|
||||
b"\x1b[0n\x1b[7;11R\x1b[?1;2c".to_vec(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_affecting_and_unqualified_controls_are_removed() {
|
||||
let (forwarded, replies) = qualified(&[
|
||||
b"before\x1b]0;title\x07\x1bPpayload\x1b\\\x1b[8;80;120t\x1b[?7727h\x9d0;c1-title\x9c\x9b2Jafter",
|
||||
]);
|
||||
assert_eq!(forwarded, b"beforeafter");
|
||||
assert!(replies.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_escape_sequence_is_discarded_without_delaying_later_text() {
|
||||
let mut bytes = b"before\x1b".to_vec();
|
||||
bytes.extend(std::iter::repeat_n(b' ', super::MAX_CONTROL_SEQUENCE_BYTES));
|
||||
bytes.extend_from_slice(b"0after");
|
||||
assert_eq!(qualified(&[&bytes]).0, b"beforeafter");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utf8_text_remains_fragmentation_independent_without_raw_c1_controls() {
|
||||
let bytes = "plain界🙂".as_bytes();
|
||||
let expected = qualified(&[bytes]);
|
||||
assert_eq!(expected.0, bytes);
|
||||
for split in 0..=bytes.len() {
|
||||
assert_eq!(qualified(&[&bytes[..split], &bytes[split..]]), expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restoration_reproduces_normal_and_alternate_screen_state() {
|
||||
for bytes in [
|
||||
b"normal\x1b[3;4H\x1b[?25l".as_slice(),
|
||||
b"normal\x1b[?1049h\x1b[2J\x1b[Halternate\x1b[4;5H".as_slice(),
|
||||
] {
|
||||
let mut terminal = RetainedTerminal::new(8, 32);
|
||||
let (initial, _) = terminal.process(bytes);
|
||||
terminal.obscure_client();
|
||||
let restored = terminal.take_restoration().unwrap();
|
||||
assert_eq!(rendered_state(&initial), rendered_state(&restored));
|
||||
}
|
||||
fn qualified_terminal_modes_and_alternate_screen_are_forwarded() {
|
||||
let bytes = b"\x1b[?1;7;25;1002;1004;1006;1049;2004;2026h\x1b[?2026;2004;1049;1006;1004;1002;25;7;1l";
|
||||
assert_eq!(qualified(&[bytes]).0, bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_replies_derive_from_retained_cursor() {
|
||||
fn restoration_is_one_route_boundary_without_rendered_bytes() {
|
||||
let mut terminal = RetainedTerminal::new(8, 32);
|
||||
terminal.process(b"content");
|
||||
assert!(!terminal.take_restoration());
|
||||
terminal.obscure_client();
|
||||
assert!(terminal.take_restoration());
|
||||
assert!(!terminal.take_restoration());
|
||||
}
|
||||
|
||||
fn qualified(chunks: &[&[u8]]) -> (Vec<u8>, Vec<u8>) {
|
||||
let mut terminal = RetainedTerminal::new(24, 80);
|
||||
let (_, replies) = terminal.process(b"\x1b[7;11H\x1b[6n\x1b[c");
|
||||
assert_eq!(replies, b"\x1b[7;11R\x1b[?1;2c");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_restores_at_the_new_size() {
|
||||
let mut terminal = RetainedTerminal::new(8, 32);
|
||||
terminal.process(b"content\x1b[6;7H");
|
||||
terminal.resize(6, 24);
|
||||
let restored = terminal.take_restoration().unwrap();
|
||||
let mut parser = vt100::Parser::new(6, 24, 0);
|
||||
parser.process(&restored);
|
||||
assert_eq!(parser.screen().size(), (6, 24));
|
||||
assert!(parser.screen().contents().contains("content"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_before_guest_output_restores_a_blank_sandbox_screen() {
|
||||
let mut terminal = RetainedTerminal::new(8, 32);
|
||||
terminal.obscure_client();
|
||||
let restored = terminal.take_restoration().unwrap();
|
||||
let mut parser = vt100::Parser::new(8, 32, 0);
|
||||
parser.process(&restored);
|
||||
assert!(parser.screen().contents().trim().is_empty());
|
||||
}
|
||||
|
||||
fn retained_state(chunks: &[&[u8]]) -> Vec<u8> {
|
||||
let mut terminal = RetainedTerminal::new(8, 32);
|
||||
let mut forwarded = Vec::new();
|
||||
let mut replies = Vec::new();
|
||||
for chunk in chunks {
|
||||
terminal.process(chunk);
|
||||
let (chunk, response) = terminal.process(chunk);
|
||||
forwarded.extend(chunk);
|
||||
replies.extend(response);
|
||||
}
|
||||
terminal.obscure_client();
|
||||
terminal.take_restoration().unwrap()
|
||||
}
|
||||
|
||||
fn rendered_state(bytes: &[u8]) -> Vec<u8> {
|
||||
let mut parser = vt100::Parser::new(8, 32, 0);
|
||||
parser.process(bytes);
|
||||
parser.screen().state_formatted()
|
||||
(forwarded, replies)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,28 +42,28 @@ fn merged_output_is_ordered_and_prompt_route_isolated_by_session() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_and_host_process_presentations_restore_the_retained_screen() {
|
||||
fn prompt_and_host_process_emit_one_client_restoration_boundary() {
|
||||
let session = id(6);
|
||||
let (fixture, factory, mut engine) = setup(&[session]);
|
||||
engine.acquire(&acquisition(session)).unwrap();
|
||||
factory.state(0).lock().unwrap().output.push_back(
|
||||
b"\x1b[2J\x1b[Hnormal\x1b[?1049h\x1b[2J\x1b[H\x1b[1;38;2;12;34;56malternate\x1b[0m\x1b[4;5H\x1b[?25l".to_vec(),
|
||||
);
|
||||
let guest_output = b"\x1b[2J\x1b[Hnormal\x1b[?1049h\x1b[2J\x1b[H\x1b[1;38;2;12;34;56malternate\x1b[0m\x1b[4;5H\x1b[?25l";
|
||||
factory
|
||||
.state(0)
|
||||
.lock()
|
||||
.unwrap()
|
||||
.output
|
||||
.push_back(guest_output.to_vec());
|
||||
let initial = output_bytes(engine.poll(session, GenerationId::new(7)).unwrap());
|
||||
let mut client = vt100::Parser::new(24, 80, 0);
|
||||
client.process(&initial);
|
||||
let expected = client.screen().state_formatted();
|
||||
assert_eq!(initial, guest_output);
|
||||
|
||||
fixture.set_session_phase(session, SessionPhase::Prompting);
|
||||
engine
|
||||
.set_route(session, GenerationId::new(7), TerminalRoute::Prompt)
|
||||
.unwrap();
|
||||
client.process(b"\x1b[2J\x1b[HPROMPT\x1b[24;80H");
|
||||
fixture.set_session_phase(session, SessionPhase::HostExecuting);
|
||||
engine
|
||||
.set_route(session, GenerationId::new(7), TerminalRoute::HostProcess)
|
||||
.unwrap();
|
||||
client.process(b"\x1b[2J\x1b[HHOST PROCESS\x1b[?25h");
|
||||
assert!(
|
||||
engine
|
||||
.poll(session, GenerationId::new(7))
|
||||
|
|
@ -75,10 +75,26 @@ fn prompt_and_host_process_presentations_restore_the_retained_screen() {
|
|||
engine
|
||||
.set_route(session, GenerationId::new(7), TerminalRoute::Sandbox)
|
||||
.unwrap();
|
||||
let restored = output_bytes(engine.poll(session, GenerationId::new(7)).unwrap());
|
||||
client.process(&restored);
|
||||
assert_eq!(client.screen().state_formatted(), expected);
|
||||
let restored = engine.poll(session, GenerationId::new(7)).unwrap();
|
||||
let [SessionTerminalMessage::Output(restoration)] = restored.as_slice() else {
|
||||
panic!("expected one presentation restoration boundary");
|
||||
};
|
||||
assert!(restoration.bytes.is_empty());
|
||||
assert!(factory.state(0).lock().unwrap().input.is_empty());
|
||||
|
||||
factory
|
||||
.state(0)
|
||||
.lock()
|
||||
.unwrap()
|
||||
.output
|
||||
.push_back(b"\x1b[6n".to_vec());
|
||||
assert!(
|
||||
engine
|
||||
.poll(session, GenerationId::new(7))
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
assert_eq!(factory.state(0).lock().unwrap().input, b"\x1b[4;5R");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in a new issue