fix: hide the painting cursor and synchronize terminal frames
This commit is contained in:
+101
-3
@@ -15,7 +15,10 @@ use crossterm::{
|
||||
KeyModifiers,
|
||||
},
|
||||
execute,
|
||||
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
|
||||
terminal::{
|
||||
BeginSynchronizedUpdate, EndSynchronizedUpdate, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
disable_raw_mode, enable_raw_mode,
|
||||
},
|
||||
};
|
||||
use ratatui::{
|
||||
Frame, Terminal,
|
||||
@@ -56,6 +59,7 @@ fn restore_terminal() {
|
||||
let _ = disable_raw_mode();
|
||||
let _ = execute!(
|
||||
io::stdout(),
|
||||
EndSynchronizedUpdate,
|
||||
DisableBracketedPaste,
|
||||
LeaveAlternateScreen,
|
||||
Show
|
||||
@@ -134,6 +138,19 @@ impl BufferViewports {
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_frame<W: io::Write>(
|
||||
terminal: &mut Terminal<CrosstermBackend<W>>,
|
||||
render: impl FnOnce(&mut Frame),
|
||||
) -> io::Result<()> {
|
||||
crossterm::queue!(terminal.backend_mut(), BeginSynchronizedUpdate)?;
|
||||
// Terminals without synchronized updates still hide the painting cursor.
|
||||
let result = terminal
|
||||
.hide_cursor()
|
||||
.and_then(|()| terminal.draw(render).map(|_| ()));
|
||||
let finish = execute!(terminal.backend_mut(), EndSynchronizedUpdate);
|
||||
result.and(finish)
|
||||
}
|
||||
|
||||
/// Renders session state and translates terminal input; the session owns plugin
|
||||
/// execution and retirement, so the adapter needs no lifecycle callback.
|
||||
pub fn run(session: &mut EditorSession) -> io::Result<()> {
|
||||
@@ -145,7 +162,9 @@ pub fn run(session: &mut EditorSession) -> io::Result<()> {
|
||||
while !session.editor().should_quit() {
|
||||
redraw |= session.poll();
|
||||
if redraw {
|
||||
terminal.draw(|frame| viewports.draw(frame, session.editor()))?;
|
||||
draw_frame(&mut terminal, |frame| {
|
||||
viewports.draw(frame, session.editor())
|
||||
})?;
|
||||
redraw = false;
|
||||
}
|
||||
if !event::poll(Duration::from_millis(25))? {
|
||||
@@ -4104,6 +4123,74 @@ end
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scrolling_never_paints_with_an_exposed_cursor_even_without_synchronized_updates() {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor
|
||||
.apply(vec![crate::editor::Effect::Replace(
|
||||
(0..40).map(|row| format!("row {row}\n")).collect(),
|
||||
)])
|
||||
.unwrap();
|
||||
let mut output = Vec::new();
|
||||
let mut terminal = Terminal::with_options(
|
||||
CrosstermBackend::new(&mut output),
|
||||
ratatui::TerminalOptions {
|
||||
viewport: ratatui::Viewport::Fixed(Rect::new(0, 0, 20, 5)),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let mut viewports = BufferViewports::default();
|
||||
for key in std::iter::repeat_n('j', 20).chain(std::iter::repeat_n('k', 20)) {
|
||||
editor.handle(Input::Char(key));
|
||||
draw_frame(&mut terminal, |frame| viewports.draw(frame, &editor)).unwrap();
|
||||
}
|
||||
drop(terminal);
|
||||
let output = std::str::from_utf8(&output).unwrap();
|
||||
for supports_sync in [false, true] {
|
||||
let (mut visible, mut synchronized) = (true, false);
|
||||
let mut screen = [[b' '; 20]; 5];
|
||||
let (mut x, mut y) = (0, 0);
|
||||
for escape in output.split('\x1b').skip(1) {
|
||||
let rest = escape.strip_prefix('[').unwrap();
|
||||
let end = rest.find(|ch| ('@'..='~').contains(&ch)).unwrap();
|
||||
let (command, text) = rest.split_at(end + 1);
|
||||
match command {
|
||||
"?25h" => visible = true,
|
||||
"?25l" => visible = false,
|
||||
"?2026h" => synchronized = supports_sync,
|
||||
"?2026l" => synchronized = false,
|
||||
_ if command.ends_with('H') => {
|
||||
let (row, column) = command[..command.len() - 1].split_once(';').unwrap();
|
||||
y = row.parse::<usize>().unwrap() - 1;
|
||||
x = column.parse::<usize>().unwrap() - 1;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
for byte in text.bytes().filter(|&byte| byte >= b' ' && byte != 127) {
|
||||
assert!(
|
||||
if supports_sync {
|
||||
synchronized
|
||||
} else {
|
||||
!visible
|
||||
},
|
||||
"partial frame exposed during redraw (sync={supports_sync}): {text:?}",
|
||||
);
|
||||
if y < screen.len() && x < screen[y].len() {
|
||||
screen[y][x] = byte;
|
||||
}
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
assert!(visible);
|
||||
assert!(!synchronized);
|
||||
for (row, expected) in [(0, "row 0"), (1, "row 1"), (2, "row 2")] {
|
||||
let line = std::str::from_utf8(&screen[row]).unwrap().trim_end();
|
||||
assert!(line.ends_with(expected), "{line:?}");
|
||||
}
|
||||
assert_eq!(screen[y][x], b'r', "caret was not restored to the source");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn terminal_session_scopes_the_callers_panic_hook() {
|
||||
@@ -4125,7 +4212,15 @@ end
|
||||
match mode.as_str() {
|
||||
"normal" => Ok(()),
|
||||
"error" => Err(io::Error::from(io::ErrorKind::BrokenPipe)),
|
||||
"panic" => panic::panic_any(73_u64),
|
||||
"panic" => {
|
||||
let mut terminal = Terminal::with_options(
|
||||
CrosstermBackend::new(io::stdout()),
|
||||
ratatui::TerminalOptions {
|
||||
viewport: ratatui::Viewport::Fixed(Rect::new(0, 0, 20, 5)),
|
||||
},
|
||||
)?;
|
||||
draw_frame(&mut terminal, |_| panic::panic_any(73_u64))
|
||||
}
|
||||
"keys" => {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor.handle(Input::Ctrl('p'));
|
||||
@@ -4244,6 +4339,9 @@ end
|
||||
"{mode}: {before:?}"
|
||||
);
|
||||
if mode == "panic" {
|
||||
let begin = before.find("\x1b[?2026h").unwrap();
|
||||
let end = before.find("\x1b[?2026l").unwrap();
|
||||
assert!(begin < end && end < before.find("CALLER_HOOK").unwrap());
|
||||
assert!(
|
||||
before.find("\x1b[?1049l").unwrap() < before.find("CALLER_HOOK").unwrap(),
|
||||
"{before:?}",
|
||||
|
||||
Reference in New Issue
Block a user