mirror of
https://github.com/wez/wezterm.git
synced 2024-12-23 21:32:13 +03:00
impl IRM insert mode and improve esctest conformance
I've had mixed results with esctest; the IRM and cursor save/restore tests fail for me in terminal.app, iterm2 and xterm, and fail in the same way on wezterm, so I'm not sure if I'm not running those tests correctly. However, they did encourage the discovery of some other real issues in the wezterm emulation.
This commit is contained in:
parent
6be7c74967
commit
6cbb3ba432
@ -3,6 +3,10 @@ set -x
|
||||
export RUST_BACKTRACE=1
|
||||
cargo run -- start --front-end null -- python -B ./ci/esctest/esctest/esctest.py \
|
||||
--expected-terminal=xterm \
|
||||
--xterm-checksum=334 \
|
||||
--v=3 \
|
||||
--timeout=0.1 \
|
||||
--no-print-logs \
|
||||
--logfile=esctest.log
|
||||
|
||||
#--stop-on-failure \
|
||||
|
@ -40,7 +40,7 @@ fn channel<T: Send>(proxy: EventsLoopProxy) -> (GuiSender<T>, Receiver<T>) {
|
||||
// Set an upper bound on the number of items in the queue, so that
|
||||
// we don't swamp the gui loop; this puts back pressure on the
|
||||
// producer side so that we have a chance for eg: processing CTRL-C
|
||||
let (tx, rx) = mpsc::sync_channel(4);
|
||||
let (tx, rx) = mpsc::sync_channel(12);
|
||||
(GuiSender { tx, proxy }, rx)
|
||||
}
|
||||
|
||||
|
@ -98,9 +98,14 @@ impl Screen {
|
||||
}
|
||||
|
||||
pub fn insert_cell(&mut self, x: usize, y: VisibleRowIndex) {
|
||||
let phys_cols = self.physical_cols;
|
||||
|
||||
let line_idx = self.phys_row(y);
|
||||
let line = self.line_mut(line_idx);
|
||||
line.insert_cell(x, Cell::default());
|
||||
if line.cells().len() > phys_cols {
|
||||
line.resize(phys_cols);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn erase_cell(&mut self, x: usize, y: VisibleRowIndex) {
|
||||
|
@ -8,7 +8,7 @@ use std::fmt::Write;
|
||||
use std::sync::Arc;
|
||||
use termwiz::escape::csi::{
|
||||
Cursor, DecPrivateMode, DecPrivateModeCode, Device, Edit, EraseInDisplay, EraseInLine, Mode,
|
||||
Sgr, Window,
|
||||
Sgr, TerminalMode, TerminalModeCode, Window,
|
||||
};
|
||||
use termwiz::escape::osc::{ITermFileData, ITermProprietary};
|
||||
use termwiz::escape::{Action, ControlCode, Esc, EscCode, OperatingSystemCommand, CSI};
|
||||
@ -56,6 +56,13 @@ impl TabStop {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
struct SavedCursor {
|
||||
position: CursorPosition,
|
||||
wrap_next: bool,
|
||||
insert: bool,
|
||||
}
|
||||
|
||||
struct ScreenOrAlt {
|
||||
/// The primary screen + scrollback
|
||||
screen: Screen,
|
||||
@ -63,6 +70,8 @@ struct ScreenOrAlt {
|
||||
alt_screen: Screen,
|
||||
/// Tells us which screen is active
|
||||
alt_screen_is_active: bool,
|
||||
saved_cursor: Option<SavedCursor>,
|
||||
alt_saved_cursor: Option<SavedCursor>,
|
||||
}
|
||||
|
||||
impl Deref for ScreenOrAlt {
|
||||
@ -96,6 +105,8 @@ impl ScreenOrAlt {
|
||||
screen,
|
||||
alt_screen,
|
||||
alt_screen_is_active: false,
|
||||
saved_cursor: None,
|
||||
alt_saved_cursor: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,6 +126,14 @@ impl ScreenOrAlt {
|
||||
pub fn is_alt_screen_active(&self) -> bool {
|
||||
self.alt_screen_is_active
|
||||
}
|
||||
|
||||
pub fn saved_cursor(&mut self) -> &mut Option<SavedCursor> {
|
||||
if self.alt_screen_is_active {
|
||||
&mut self.alt_saved_cursor
|
||||
} else {
|
||||
&mut self.saved_cursor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TerminalState {
|
||||
@ -125,12 +144,14 @@ pub struct TerminalState {
|
||||
/// The current cursor position, relative to the top left
|
||||
/// of the screen. 0-based index.
|
||||
cursor: CursorPosition,
|
||||
saved_cursor: CursorPosition,
|
||||
|
||||
/// if true, implicitly move to the next line on the next
|
||||
/// printed character
|
||||
wrap_next: bool,
|
||||
|
||||
/// If true, writing a character inserts a new cell
|
||||
insert: bool,
|
||||
|
||||
/// The scroll region
|
||||
scroll_region: Range<VisibleRowIndex>,
|
||||
|
||||
@ -218,9 +239,9 @@ impl TerminalState {
|
||||
screen,
|
||||
pen: CellAttributes::default(),
|
||||
cursor: CursorPosition::default(),
|
||||
saved_cursor: CursorPosition::default(),
|
||||
scroll_region: 0..physical_rows as VisibleRowIndex,
|
||||
wrap_next: false,
|
||||
insert: false,
|
||||
application_cursor_keys: false,
|
||||
application_keypad: false,
|
||||
bracketed_paste: false,
|
||||
@ -1309,6 +1330,15 @@ impl TerminalState {
|
||||
DecPrivateModeCode::StartBlinkingCursor,
|
||||
)) => {}
|
||||
|
||||
Mode::SetMode(TerminalMode::Code(TerminalModeCode::Insert)) => {
|
||||
eprintln!("Enable insert");
|
||||
self.insert = true;
|
||||
}
|
||||
Mode::ResetMode(TerminalMode::Code(TerminalModeCode::Insert)) => {
|
||||
eprintln!("Disable insert");
|
||||
self.insert = false;
|
||||
}
|
||||
|
||||
Mode::SetDecPrivateMode(DecPrivateMode::Code(DecPrivateModeCode::BracketedPaste)) => {
|
||||
self.bracketed_paste = true;
|
||||
}
|
||||
@ -1316,6 +1346,23 @@ impl TerminalState {
|
||||
self.bracketed_paste = false;
|
||||
}
|
||||
|
||||
Mode::SetDecPrivateMode(DecPrivateMode::Code(
|
||||
DecPrivateModeCode::EnableAlternateScreen,
|
||||
)) => {
|
||||
if !self.screen.is_alt_screen_active() {
|
||||
self.screen.activate_alt_screen();
|
||||
self.set_scroll_viewport(0);
|
||||
}
|
||||
}
|
||||
Mode::ResetDecPrivateMode(DecPrivateMode::Code(
|
||||
DecPrivateModeCode::EnableAlternateScreen,
|
||||
)) => {
|
||||
if self.screen.is_alt_screen_active() {
|
||||
self.screen.activate_primary_screen();
|
||||
self.set_scroll_viewport(0);
|
||||
}
|
||||
}
|
||||
|
||||
Mode::SetDecPrivateMode(DecPrivateMode::Code(
|
||||
DecPrivateModeCode::ApplicationCursorKeys,
|
||||
)) => {
|
||||
@ -1396,9 +1443,42 @@ impl TerminalState {
|
||||
| Mode::RestoreDecPrivateMode(DecPrivateMode::Unspecified(n)) => {
|
||||
eprintln!("unhandled DecPrivateMode {}", n);
|
||||
}
|
||||
|
||||
Mode::SetMode(TerminalMode::Unspecified(n))
|
||||
| Mode::ResetMode(TerminalMode::Unspecified(n)) => {
|
||||
eprintln!("unhandled TerminalMode {}", n);
|
||||
}
|
||||
|
||||
Mode::SetMode(m) | Mode::ResetMode(m) => {
|
||||
eprintln!("unhandled TerminalMode {:?}", m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn checksum_rectangle(&mut self, left: u32, top: u32, right: u32, bottom: u32) -> u16 {
|
||||
let screen = self.screen_mut();
|
||||
let mut checksum = 0;
|
||||
debug!(
|
||||
"checksum left={} top={} right={} bottom={}",
|
||||
left, top, right, bottom
|
||||
);
|
||||
for y in top..=bottom {
|
||||
let line_idx = screen.phys_row(y as VisibleRowIndex);
|
||||
let line = screen.line_mut(line_idx);
|
||||
for (col, cell) in line.cells().iter().enumerate().skip(left as usize) {
|
||||
if col > right as usize {
|
||||
break;
|
||||
}
|
||||
|
||||
let ch = cell.str().chars().nth(0).unwrap() as u32;
|
||||
debug!("y={} col={} ch={:x} cell={:?}", y, col, ch, cell);
|
||||
|
||||
checksum += (ch as u8) as u16;
|
||||
}
|
||||
}
|
||||
checksum
|
||||
}
|
||||
|
||||
fn perform_csi_window(&mut self, window: Window, host: &mut TerminalHost) {
|
||||
match window {
|
||||
Window::ReportTextAreaSizeCells => {
|
||||
@ -1409,8 +1489,19 @@ impl TerminalState {
|
||||
let response = Window::ResizeWindowCells { width, height };
|
||||
write!(host.writer(), "{}", CSI::Window(response)).ok();
|
||||
}
|
||||
Window::Iconify | Window::DeIconify => {},
|
||||
Window::PopIconAndWindowTitle => {},
|
||||
Window::ChecksumRectangularArea {
|
||||
request_id,
|
||||
top,
|
||||
left,
|
||||
bottom,
|
||||
right,
|
||||
..
|
||||
} => {
|
||||
let checksum = self.checksum_rectangle(left, top, right, bottom);
|
||||
write!(host.writer(), "\x1bP{}!~{:04x}\x1b\\", request_id, checksum).ok();
|
||||
}
|
||||
Window::Iconify | Window::DeIconify => {}
|
||||
Window::PopIconAndWindowTitle => {}
|
||||
_ => eprintln!("unhandled Window CSI {:?}", window),
|
||||
}
|
||||
}
|
||||
@ -1635,12 +1726,34 @@ impl TerminalState {
|
||||
}
|
||||
|
||||
fn save_cursor(&mut self) {
|
||||
self.saved_cursor = self.cursor;
|
||||
let saved = SavedCursor {
|
||||
position: self.cursor,
|
||||
insert: self.insert,
|
||||
wrap_next: self.wrap_next,
|
||||
};
|
||||
debug!(
|
||||
"saving cursor {:?} is_alt={}",
|
||||
saved,
|
||||
self.screen.is_alt_screen_active()
|
||||
);
|
||||
*self.screen.saved_cursor() = Some(saved);
|
||||
}
|
||||
fn restore_cursor(&mut self) {
|
||||
let x = self.saved_cursor.x;
|
||||
let y = self.saved_cursor.y;
|
||||
let saved = self.screen.saved_cursor().unwrap_or_else(|| SavedCursor {
|
||||
position: CursorPosition::default(),
|
||||
insert: false,
|
||||
wrap_next: false,
|
||||
});
|
||||
debug!(
|
||||
"restore cursor {:?} is_alt={}",
|
||||
saved,
|
||||
self.screen.is_alt_screen_active()
|
||||
);
|
||||
let x = saved.position.x;
|
||||
let y = saved.position.y;
|
||||
self.set_cursor_pos(&Position::Absolute(x as i64), &Position::Absolute(y));
|
||||
self.wrap_next = saved.wrap_next;
|
||||
self.insert = saved.insert;
|
||||
}
|
||||
|
||||
fn perform_csi_sgr(&mut self, sgr: Sgr) {
|
||||
@ -1726,8 +1839,10 @@ impl<'a> Performer<'a> {
|
||||
None => return,
|
||||
};
|
||||
|
||||
let mut x_offset = 0;
|
||||
|
||||
for g in unicode_segmentation::UnicodeSegmentation::graphemes(p.as_str(), true) {
|
||||
if self.wrap_next {
|
||||
if !self.insert && self.wrap_next {
|
||||
self.new_line(true);
|
||||
}
|
||||
|
||||
@ -1737,29 +1852,38 @@ impl<'a> Performer<'a> {
|
||||
|
||||
let pen = self.pen.clone();
|
||||
|
||||
// Assign the cell and extract its printable width
|
||||
let print_width = {
|
||||
let cell = self
|
||||
.screen_mut()
|
||||
.set_cell(x, y, &Cell::new_grapheme(g, pen.clone()));
|
||||
// the max(1) here is to ensure that we advance to the next cell
|
||||
// position for zero-width graphemes. We want to make sure that
|
||||
// they occupy a cell so that we can re-emit them when we output them.
|
||||
// If we didn't do this, then we'd effectively filter them out from
|
||||
// the model, which seems like a lossy design choice.
|
||||
cell.width().max(1)
|
||||
};
|
||||
let cell = Cell::new_grapheme(g, pen.clone());
|
||||
// the max(1) here is to ensure that we advance to the next cell
|
||||
// position for zero-width graphemes. We want to make sure that
|
||||
// they occupy a cell so that we can re-emit them when we output them.
|
||||
// If we didn't do this, then we'd effectively filter them out from
|
||||
// the model, which seems like a lossy design choice.
|
||||
let print_width = cell.width().max(1);
|
||||
|
||||
if self.insert {
|
||||
let screen = self.screen_mut();
|
||||
for _ in x..x + print_width as usize {
|
||||
screen.insert_cell(x + x_offset, y);
|
||||
}
|
||||
}
|
||||
|
||||
// Assign the cell
|
||||
self.screen_mut().set_cell(x + x_offset, y, &cell);
|
||||
|
||||
self.clear_selection_if_intersects(
|
||||
x..x + print_width,
|
||||
y as ScrollbackOrVisibleRowIndex,
|
||||
);
|
||||
|
||||
if x + print_width < width {
|
||||
self.cursor.x += print_width;
|
||||
self.wrap_next = false;
|
||||
if self.insert {
|
||||
x_offset += print_width;
|
||||
} else {
|
||||
self.wrap_next = true;
|
||||
if x + print_width < width {
|
||||
self.cursor.x += print_width;
|
||||
self.wrap_next = false;
|
||||
} else {
|
||||
self.wrap_next = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,18 +28,27 @@ fn test_rep() {
|
||||
assert_visible_contents(&term, &["hhha", " ", " "]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_irm() {
|
||||
let mut term = TestTerm::new(3, 8, 0);
|
||||
term.print("foo");
|
||||
term.cup(0, 0);
|
||||
term.print("\x1b[4hBAR");
|
||||
assert_visible_contents(&term, &["BARfoo ", " ", " "]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ich() {
|
||||
let mut term = TestTerm::new(3, 4, 0);
|
||||
term.print("hey!wat?");
|
||||
term.cup(1, 0);
|
||||
term.print("\x1b[2@");
|
||||
assert_visible_contents(&term, &["h ey!", "wat?", " "]);
|
||||
assert_visible_contents(&term, &["h e", "wat?", " "]);
|
||||
// check how we handle overflowing the width
|
||||
term.print("\x1b[12@");
|
||||
assert_visible_contents(&term, &["h ey!", "wat?", " "]);
|
||||
assert_visible_contents(&term, &["h ", "wat?", " "]);
|
||||
term.print("\x1b[-12@");
|
||||
assert_visible_contents(&term, &["h ey!", "wat?", " "]);
|
||||
assert_visible_contents(&term, &["h ", "wat?", " "]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -266,6 +266,15 @@ pub enum Window {
|
||||
PopIconAndWindowTitle,
|
||||
PopIconTitle,
|
||||
PopWindowTitle,
|
||||
/// DECRQCRA; used by esctest
|
||||
ChecksumRectangularArea {
|
||||
request_id: i64,
|
||||
page_number: i64,
|
||||
top: u32,
|
||||
left: u32,
|
||||
bottom: u32,
|
||||
right: u32,
|
||||
},
|
||||
}
|
||||
|
||||
fn numstr_or_empty(x: &Option<i64>) -> String {
|
||||
@ -320,6 +329,23 @@ impl Display for Window {
|
||||
Window::PopIconAndWindowTitle => write!(f, "23;0t"),
|
||||
Window::PopIconTitle => write!(f, "23;1t"),
|
||||
Window::PopWindowTitle => write!(f, "23;2t"),
|
||||
Window::ChecksumRectangularArea {
|
||||
request_id,
|
||||
page_number,
|
||||
top,
|
||||
left,
|
||||
bottom,
|
||||
right,
|
||||
} => write!(
|
||||
f,
|
||||
"{};{};{};{};{};{}*y",
|
||||
request_id,
|
||||
page_number,
|
||||
top + 1,
|
||||
left + 1,
|
||||
bottom + 1,
|
||||
right + 1
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -388,6 +414,8 @@ pub enum Mode {
|
||||
ResetDecPrivateMode(DecPrivateMode),
|
||||
SaveDecPrivateMode(DecPrivateMode),
|
||||
RestoreDecPrivateMode(DecPrivateMode),
|
||||
SetMode(TerminalMode),
|
||||
ResetMode(TerminalMode),
|
||||
}
|
||||
|
||||
impl Display for Mode {
|
||||
@ -401,11 +429,22 @@ impl Display for Mode {
|
||||
write!(f, "?{}{}", value, $flag)
|
||||
}};
|
||||
}
|
||||
macro_rules! emit_mode {
|
||||
($flag:expr, $mode:expr) => {{
|
||||
let value = match $mode {
|
||||
TerminalMode::Code(mode) => mode.to_u16().ok_or_else(|| FmtError)?,
|
||||
TerminalMode::Unspecified(mode) => *mode,
|
||||
};
|
||||
write!(f, "?{}{}", value, $flag)
|
||||
}};
|
||||
}
|
||||
match self {
|
||||
Mode::SetDecPrivateMode(mode) => emit!("h", mode),
|
||||
Mode::ResetDecPrivateMode(mode) => emit!("l", mode),
|
||||
Mode::SaveDecPrivateMode(mode) => emit!("s", mode),
|
||||
Mode::RestoreDecPrivateMode(mode) => emit!("r", mode),
|
||||
Mode::SetMode(mode) => emit_mode!("h", mode),
|
||||
Mode::ResetMode(mode) => emit_mode!("l", mode),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -435,9 +474,24 @@ pub enum DecPrivateModeCode {
|
||||
/// will be encoded.
|
||||
SGRMouse = 1006,
|
||||
ClearAndEnableAlternateScreen = 1049,
|
||||
EnableAlternateScreen = 47,
|
||||
BracketedPaste = 2004,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum TerminalMode {
|
||||
Code(TerminalModeCode),
|
||||
Unspecified(u16),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
||||
pub enum TerminalModeCode {
|
||||
KeyboardAction = 2,
|
||||
Insert = 4,
|
||||
SendReceive = 12,
|
||||
AutomaticNewline = 20,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Cursor {
|
||||
/// CBT Moves cursor to the Ps tabs backward. The default value of Ps is 1.
|
||||
@ -1153,8 +1207,14 @@ impl<'a> CSIParser<'a> {
|
||||
('e', &[]) => parse!(Cursor, LinePositionForward, params),
|
||||
('f', &[]) => parse!(Cursor, CharacterAndLinePosition, line, col, params),
|
||||
('g', &[]) => parse!(Cursor, TabulationClear, params),
|
||||
('h', &[]) => self
|
||||
.terminal_mode(params)
|
||||
.map(|mode| CSI::Mode(Mode::SetMode(mode))),
|
||||
('j', &[]) => parse!(Cursor, CharacterPositionBackward, params),
|
||||
('k', &[]) => parse!(Cursor, LinePositionBackward, params),
|
||||
('l', &[]) => self
|
||||
.terminal_mode(params)
|
||||
.map(|mode| CSI::Mode(Mode::ResetMode(mode))),
|
||||
|
||||
('m', &[]) => self.sgr(params).map(CSI::Sgr),
|
||||
('n', &[]) => self.dsr(params),
|
||||
@ -1163,6 +1223,25 @@ impl<'a> CSIParser<'a> {
|
||||
('s', &[]) => noparams!(Cursor, SaveCursor, params),
|
||||
('t', &[]) => self.window(params).map(CSI::Window),
|
||||
('u', &[]) => noparams!(Cursor, RestoreCursor, params),
|
||||
('y', &[b'*']) => {
|
||||
fn p(params: &[i64], idx: usize) -> Result<i64, ()> {
|
||||
params.get(idx).cloned().ok_or(())
|
||||
}
|
||||
let request_id = p(params, 0)?;
|
||||
let page_number = p(params, 1)?;
|
||||
let top = to_1b_u32(p(params, 2)?)?.saturating_sub(1);
|
||||
let left = to_1b_u32(p(params, 3)?)?.saturating_sub(1);
|
||||
let bottom = to_1b_u32(p(params, 4)?)?.saturating_sub(1);
|
||||
let right = to_1b_u32(p(params, 5)?)?.saturating_sub(1);
|
||||
Ok(CSI::Window(Window::ChecksumRectangularArea {
|
||||
request_id,
|
||||
page_number,
|
||||
top,
|
||||
left,
|
||||
bottom,
|
||||
right,
|
||||
}))
|
||||
}
|
||||
|
||||
('p', &[b'!']) => Ok(CSI::Device(Box::new(Device::SoftReset))),
|
||||
|
||||
@ -1379,6 +1458,13 @@ impl<'a> CSIParser<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn terminal_mode(&mut self, params: &'a [i64]) -> Result<TerminalMode, ()> {
|
||||
match num::FromPrimitive::from_i64(params[0]) {
|
||||
None => Ok(TerminalMode::Unspecified(params[0].to_u16().ok_or(())?)),
|
||||
Some(mode) => Ok(self.advance_by(1, params, TerminalMode::Code(mode))),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_sgr_color(&mut self, params: &'a [i64]) -> Result<ColorSpec, ()> {
|
||||
if params.len() >= 5 && params[1] == 2 {
|
||||
let red = to_u8(params[2])?;
|
||||
|
Loading…
Reference in New Issue
Block a user