1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-24 13:52:55 +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:
Wez Furlong 2019-03-17 23:10:04 -07:00
parent 6be7c74967
commit 6cbb3ba432
6 changed files with 257 additions and 29 deletions

View File

@ -3,6 +3,10 @@ set -x
export RUST_BACKTRACE=1 export RUST_BACKTRACE=1
cargo run -- start --front-end null -- python -B ./ci/esctest/esctest/esctest.py \ cargo run -- start --front-end null -- python -B ./ci/esctest/esctest/esctest.py \
--expected-terminal=xterm \ --expected-terminal=xterm \
--xterm-checksum=334 \
--v=3 \ --v=3 \
--timeout=0.1 \ --timeout=0.1 \
--no-print-logs \
--logfile=esctest.log --logfile=esctest.log
#--stop-on-failure \

View File

@ -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 // 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 // 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 // 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) (GuiSender { tx, proxy }, rx)
} }

View File

@ -98,9 +98,14 @@ impl Screen {
} }
pub fn insert_cell(&mut self, x: usize, y: VisibleRowIndex) { 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_idx = self.phys_row(y);
let line = self.line_mut(line_idx); let line = self.line_mut(line_idx);
line.insert_cell(x, Cell::default()); 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) { pub fn erase_cell(&mut self, x: usize, y: VisibleRowIndex) {

View File

@ -8,7 +8,7 @@ use std::fmt::Write;
use std::sync::Arc; use std::sync::Arc;
use termwiz::escape::csi::{ use termwiz::escape::csi::{
Cursor, DecPrivateMode, DecPrivateModeCode, Device, Edit, EraseInDisplay, EraseInLine, Mode, Cursor, DecPrivateMode, DecPrivateModeCode, Device, Edit, EraseInDisplay, EraseInLine, Mode,
Sgr, Window, Sgr, TerminalMode, TerminalModeCode, Window,
}; };
use termwiz::escape::osc::{ITermFileData, ITermProprietary}; use termwiz::escape::osc::{ITermFileData, ITermProprietary};
use termwiz::escape::{Action, ControlCode, Esc, EscCode, OperatingSystemCommand, CSI}; 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 { struct ScreenOrAlt {
/// The primary screen + scrollback /// The primary screen + scrollback
screen: Screen, screen: Screen,
@ -63,6 +70,8 @@ struct ScreenOrAlt {
alt_screen: Screen, alt_screen: Screen,
/// Tells us which screen is active /// Tells us which screen is active
alt_screen_is_active: bool, alt_screen_is_active: bool,
saved_cursor: Option<SavedCursor>,
alt_saved_cursor: Option<SavedCursor>,
} }
impl Deref for ScreenOrAlt { impl Deref for ScreenOrAlt {
@ -96,6 +105,8 @@ impl ScreenOrAlt {
screen, screen,
alt_screen, alt_screen,
alt_screen_is_active: false, 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 { pub fn is_alt_screen_active(&self) -> bool {
self.alt_screen_is_active 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 { pub struct TerminalState {
@ -125,12 +144,14 @@ pub struct TerminalState {
/// The current cursor position, relative to the top left /// The current cursor position, relative to the top left
/// of the screen. 0-based index. /// of the screen. 0-based index.
cursor: CursorPosition, cursor: CursorPosition,
saved_cursor: CursorPosition,
/// if true, implicitly move to the next line on the next /// if true, implicitly move to the next line on the next
/// printed character /// printed character
wrap_next: bool, wrap_next: bool,
/// If true, writing a character inserts a new cell
insert: bool,
/// The scroll region /// The scroll region
scroll_region: Range<VisibleRowIndex>, scroll_region: Range<VisibleRowIndex>,
@ -218,9 +239,9 @@ impl TerminalState {
screen, screen,
pen: CellAttributes::default(), pen: CellAttributes::default(),
cursor: CursorPosition::default(), cursor: CursorPosition::default(),
saved_cursor: CursorPosition::default(),
scroll_region: 0..physical_rows as VisibleRowIndex, scroll_region: 0..physical_rows as VisibleRowIndex,
wrap_next: false, wrap_next: false,
insert: false,
application_cursor_keys: false, application_cursor_keys: false,
application_keypad: false, application_keypad: false,
bracketed_paste: false, bracketed_paste: false,
@ -1309,6 +1330,15 @@ impl TerminalState {
DecPrivateModeCode::StartBlinkingCursor, 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)) => { Mode::SetDecPrivateMode(DecPrivateMode::Code(DecPrivateModeCode::BracketedPaste)) => {
self.bracketed_paste = true; self.bracketed_paste = true;
} }
@ -1316,6 +1346,23 @@ impl TerminalState {
self.bracketed_paste = false; 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( Mode::SetDecPrivateMode(DecPrivateMode::Code(
DecPrivateModeCode::ApplicationCursorKeys, DecPrivateModeCode::ApplicationCursorKeys,
)) => { )) => {
@ -1396,9 +1443,42 @@ impl TerminalState {
| Mode::RestoreDecPrivateMode(DecPrivateMode::Unspecified(n)) => { | Mode::RestoreDecPrivateMode(DecPrivateMode::Unspecified(n)) => {
eprintln!("unhandled DecPrivateMode {}", 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) { fn perform_csi_window(&mut self, window: Window, host: &mut TerminalHost) {
match window { match window {
Window::ReportTextAreaSizeCells => { Window::ReportTextAreaSizeCells => {
@ -1409,8 +1489,19 @@ impl TerminalState {
let response = Window::ResizeWindowCells { width, height }; let response = Window::ResizeWindowCells { width, height };
write!(host.writer(), "{}", CSI::Window(response)).ok(); write!(host.writer(), "{}", CSI::Window(response)).ok();
} }
Window::Iconify | Window::DeIconify => {}, Window::ChecksumRectangularArea {
Window::PopIconAndWindowTitle => {}, 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), _ => eprintln!("unhandled Window CSI {:?}", window),
} }
} }
@ -1635,12 +1726,34 @@ impl TerminalState {
} }
fn save_cursor(&mut self) { 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) { fn restore_cursor(&mut self) {
let x = self.saved_cursor.x; let saved = self.screen.saved_cursor().unwrap_or_else(|| SavedCursor {
let y = self.saved_cursor.y; 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.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) { fn perform_csi_sgr(&mut self, sgr: Sgr) {
@ -1726,8 +1839,10 @@ impl<'a> Performer<'a> {
None => return, None => return,
}; };
let mut x_offset = 0;
for g in unicode_segmentation::UnicodeSegmentation::graphemes(p.as_str(), true) { 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); self.new_line(true);
} }
@ -1737,29 +1852,38 @@ impl<'a> Performer<'a> {
let pen = self.pen.clone(); let pen = self.pen.clone();
// Assign the cell and extract its printable width let cell = Cell::new_grapheme(g, pen.clone());
let print_width = { // the max(1) here is to ensure that we advance to the next cell
let cell = self // position for zero-width graphemes. We want to make sure that
.screen_mut() // they occupy a cell so that we can re-emit them when we output them.
.set_cell(x, y, &Cell::new_grapheme(g, pen.clone())); // If we didn't do this, then we'd effectively filter them out from
// the max(1) here is to ensure that we advance to the next cell // the model, which seems like a lossy design choice.
// position for zero-width graphemes. We want to make sure that let print_width = cell.width().max(1);
// 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 if self.insert {
// the model, which seems like a lossy design choice. let screen = self.screen_mut();
cell.width().max(1) 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( self.clear_selection_if_intersects(
x..x + print_width, x..x + print_width,
y as ScrollbackOrVisibleRowIndex, y as ScrollbackOrVisibleRowIndex,
); );
if x + print_width < width { if self.insert {
self.cursor.x += print_width; x_offset += print_width;
self.wrap_next = false;
} else { } else {
self.wrap_next = true; if x + print_width < width {
self.cursor.x += print_width;
self.wrap_next = false;
} else {
self.wrap_next = true;
}
} }
} }
} }

View File

@ -28,18 +28,27 @@ fn test_rep() {
assert_visible_contents(&term, &["hhha", " ", " "]); 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] #[test]
fn test_ich() { fn test_ich() {
let mut term = TestTerm::new(3, 4, 0); let mut term = TestTerm::new(3, 4, 0);
term.print("hey!wat?"); term.print("hey!wat?");
term.cup(1, 0); term.cup(1, 0);
term.print("\x1b[2@"); 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 // check how we handle overflowing the width
term.print("\x1b[12@"); term.print("\x1b[12@");
assert_visible_contents(&term, &["h ey!", "wat?", " "]); assert_visible_contents(&term, &["h ", "wat?", " "]);
term.print("\x1b[-12@"); term.print("\x1b[-12@");
assert_visible_contents(&term, &["h ey!", "wat?", " "]); assert_visible_contents(&term, &["h ", "wat?", " "]);
} }
#[test] #[test]

View File

@ -266,6 +266,15 @@ pub enum Window {
PopIconAndWindowTitle, PopIconAndWindowTitle,
PopIconTitle, PopIconTitle,
PopWindowTitle, 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 { fn numstr_or_empty(x: &Option<i64>) -> String {
@ -320,6 +329,23 @@ impl Display for Window {
Window::PopIconAndWindowTitle => write!(f, "23;0t"), Window::PopIconAndWindowTitle => write!(f, "23;0t"),
Window::PopIconTitle => write!(f, "23;1t"), Window::PopIconTitle => write!(f, "23;1t"),
Window::PopWindowTitle => write!(f, "23;2t"), 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), ResetDecPrivateMode(DecPrivateMode),
SaveDecPrivateMode(DecPrivateMode), SaveDecPrivateMode(DecPrivateMode),
RestoreDecPrivateMode(DecPrivateMode), RestoreDecPrivateMode(DecPrivateMode),
SetMode(TerminalMode),
ResetMode(TerminalMode),
} }
impl Display for Mode { impl Display for Mode {
@ -401,11 +429,22 @@ impl Display for Mode {
write!(f, "?{}{}", value, $flag) 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 { match self {
Mode::SetDecPrivateMode(mode) => emit!("h", mode), Mode::SetDecPrivateMode(mode) => emit!("h", mode),
Mode::ResetDecPrivateMode(mode) => emit!("l", mode), Mode::ResetDecPrivateMode(mode) => emit!("l", mode),
Mode::SaveDecPrivateMode(mode) => emit!("s", mode), Mode::SaveDecPrivateMode(mode) => emit!("s", mode),
Mode::RestoreDecPrivateMode(mode) => emit!("r", 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. /// will be encoded.
SGRMouse = 1006, SGRMouse = 1006,
ClearAndEnableAlternateScreen = 1049, ClearAndEnableAlternateScreen = 1049,
EnableAlternateScreen = 47,
BracketedPaste = 2004, 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)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum Cursor { pub enum Cursor {
/// CBT Moves cursor to the Ps tabs backward. The default value of Ps is 1. /// 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), ('e', &[]) => parse!(Cursor, LinePositionForward, params),
('f', &[]) => parse!(Cursor, CharacterAndLinePosition, line, col, params), ('f', &[]) => parse!(Cursor, CharacterAndLinePosition, line, col, params),
('g', &[]) => parse!(Cursor, TabulationClear, params), ('g', &[]) => parse!(Cursor, TabulationClear, params),
('h', &[]) => self
.terminal_mode(params)
.map(|mode| CSI::Mode(Mode::SetMode(mode))),
('j', &[]) => parse!(Cursor, CharacterPositionBackward, params), ('j', &[]) => parse!(Cursor, CharacterPositionBackward, params),
('k', &[]) => parse!(Cursor, LinePositionBackward, 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), ('m', &[]) => self.sgr(params).map(CSI::Sgr),
('n', &[]) => self.dsr(params), ('n', &[]) => self.dsr(params),
@ -1163,6 +1223,25 @@ impl<'a> CSIParser<'a> {
('s', &[]) => noparams!(Cursor, SaveCursor, params), ('s', &[]) => noparams!(Cursor, SaveCursor, params),
('t', &[]) => self.window(params).map(CSI::Window), ('t', &[]) => self.window(params).map(CSI::Window),
('u', &[]) => noparams!(Cursor, RestoreCursor, params), ('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))), ('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, ()> { fn parse_sgr_color(&mut self, params: &'a [i64]) -> Result<ColorSpec, ()> {
if params.len() >= 5 && params[1] == 2 { if params.len() >= 5 && params[1] == 2 {
let red = to_u8(params[2])?; let red = to_u8(params[2])?;