1
1
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:
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
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 \

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
// 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)
}

View File

@ -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) {

View File

@ -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;
}
}
}
}

View File

@ -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]

View File

@ -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])?;