mirror of
https://github.com/openai/codex.git
synced 2026-10-22 17:58:46 +00:00
Repair cursor-style rendering on older JediTerm terminals (#41673)
## Why Older JediTerm versions can print the space intermediate in `DECSCUSR`, overwriting the glyph beneath a cursor-style command. ## What changed - Apply cursor-style commands at a repairable, terminal-owned glyph and redraw that glyph with its original style and hyperlink before restoring the requested cursor position. - Omit the style command when the viewport has no safe repair anchor. - Handle skipped cells, wide glyphs, and single-column viewports without corrupting content or causing scrolling. ## Testing Add cursor rendering tests for styled and cursor-only frames, skipped and wide hyperlinked glyphs, single-column viewports, and viewports without owned cells. GitOrigin-RevId: c8bbb74c53737b5980c533859af39da26f0e06cc
This commit is contained in:
committed by
copyberry
parent
cefa060695
commit
da23e131e6
@@ -48,6 +48,8 @@ use ratatui::style::Color;
|
||||
use ratatui::style::Modifier;
|
||||
use ratatui::widgets::WidgetRef;
|
||||
|
||||
mod cursor;
|
||||
|
||||
fn osc8_hyperlink_parts(symbol: &str) -> Option<(&str, &str)> {
|
||||
let content = symbol.strip_prefix("\x1b]8;;")?;
|
||||
let destination_end = content.find('\x07')?;
|
||||
@@ -428,7 +430,7 @@ where
|
||||
match cursor_position {
|
||||
None => self.hide_cursor()?,
|
||||
Some(position) => {
|
||||
self.set_cursor_style(cursor_style)?;
|
||||
self.set_cursor_style_with_repair(cursor_style)?;
|
||||
self.set_cursor_position(position)?;
|
||||
self.show_cursor()?;
|
||||
}
|
||||
@@ -835,7 +837,9 @@ impl ModifierDiff {
|
||||
}
|
||||
}
|
||||
|
||||
// Keep nested #[path] modules discoverable by cargo-shear as well as rustc.
|
||||
#[cfg(test)]
|
||||
#[path = "custom_terminal/tests"]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::num::NonZeroU16;
|
||||
@@ -852,6 +856,9 @@ mod tests {
|
||||
use ratatui::widgets::Widget;
|
||||
use ratatui::widgets::Wrap;
|
||||
|
||||
#[path = "cursor_tests.rs"]
|
||||
mod cursor;
|
||||
|
||||
struct CaptureBackend {
|
||||
output: Vec<u8>,
|
||||
size: Size,
|
||||
@@ -1232,31 +1239,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_draw_applies_requested_cursor_style() {
|
||||
let mut output = Vec::new();
|
||||
let mut terminal =
|
||||
Terminal::with_options(CaptureBackend::new(/*width*/ 2, /*height*/ 1))
|
||||
.expect("terminal");
|
||||
terminal.set_viewport_area(Rect::new(0, 0, 2, 1));
|
||||
|
||||
terminal
|
||||
.try_draw(|frame| {
|
||||
frame.set_cursor_style(SetCursorStyle::SteadyBar);
|
||||
frame.set_cursor_position((0, 0));
|
||||
io::Result::Ok(())
|
||||
})
|
||||
.expect("draw");
|
||||
|
||||
queue!(output, SetCursorStyle::SteadyBar).expect("queue style");
|
||||
let expected = String::from_utf8(output).expect("utf8");
|
||||
let actual = terminal.backend().output();
|
||||
assert!(
|
||||
actual.contains(&expected),
|
||||
"expected terminal output to contain cursor style {expected:?}, got {actual:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_draw_moves_cursor_before_showing_it() {
|
||||
let cursor_position = Position { x: 1, y: 0 };
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
//! Repair the glyph beneath cursor-style commands on older JediTerm terminals.
|
||||
//!
|
||||
//! This helper issues cursor-style commands only at an owned, non-skipped glyph leader
|
||||
//! with positive width that fits within its row, then redraws it with the same style and
|
||||
//! hyperlink. Without a safe anchor, it omits the command. The caller must restore the
|
||||
//! requested cursor position afterward.
|
||||
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
|
||||
use crossterm::cursor::SetCursorStyle;
|
||||
use ratatui::backend::Backend;
|
||||
use ratatui::buffer::CellDiffOption;
|
||||
use ratatui::buffer::CellWidth;
|
||||
use ratatui::layout::Position;
|
||||
|
||||
use super::DrawCommand;
|
||||
use super::Terminal;
|
||||
use super::draw;
|
||||
|
||||
impl<B> Terminal<B>
|
||||
where
|
||||
B: Backend<Error = io::Error> + Write,
|
||||
{
|
||||
pub(super) fn set_cursor_style_with_repair(
|
||||
&mut self,
|
||||
cursor_style: SetCursorStyle,
|
||||
) -> io::Result<()> {
|
||||
// JediTerm before 3.56 prints DECSCUSR's space intermediate at the cursor.
|
||||
// Apply the style over an owned glyph, then repair it even on unchanged frames.
|
||||
// https://github.com/JetBrains/jediterm/commit/0c4524f2978bddae65a46c35f264bf89e2ed58fd
|
||||
let buffer = &self.buffers[self.current];
|
||||
let anchor = (0..buffer.area.height).find_map(|row| {
|
||||
let row_start = usize::from(row) * usize::from(buffer.area.width);
|
||||
let mut column = 0;
|
||||
while column < usize::from(buffer.area.width) {
|
||||
let cell = &buffer.content[row_start + column];
|
||||
let width = usize::from(cell.cell_width());
|
||||
let is_skip = cell.diff_option == CellDiffOption::Skip;
|
||||
if !is_skip && width > 0 && column + width <= usize::from(buffer.area.width) {
|
||||
let (x, y) = buffer.pos_of(row_start + column);
|
||||
return Some((Position { x, y }, cell.clone()));
|
||||
}
|
||||
column += width.max(1);
|
||||
}
|
||||
None
|
||||
});
|
||||
// Empty and externally owned viewports have no cell we can safely repair.
|
||||
if let Some((anchor, cell)) = anchor {
|
||||
self.set_cursor_position(anchor)?;
|
||||
self.set_cursor_style(cursor_style)?;
|
||||
let Position { x, y } = anchor;
|
||||
draw(
|
||||
&mut self.backend,
|
||||
std::iter::once(DrawCommand::Put { x, y, cell }),
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
use super::*;
|
||||
use crate::terminal_palette::rgb_color;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn terminal_draw_repairs_styled_anchor_on_cursor_only_frames() {
|
||||
let mut terminal =
|
||||
Terminal::with_options(CaptureBackend::new(/*width*/ 12, /*height*/ 2)).expect("terminal");
|
||||
let area = Rect::new(
|
||||
/*x*/ 0, /*y*/ 1, /*width*/ 12, /*height*/ 1,
|
||||
);
|
||||
terminal.set_viewport_area(area);
|
||||
let mut frames = Vec::new();
|
||||
let mut parser =
|
||||
vt100::Parser::new(/*rows*/ 2, /*cols*/ 12, /*scrollback_len*/ 0);
|
||||
|
||||
for (x, style) in [
|
||||
(1, SetCursorStyle::DefaultUserShape),
|
||||
(4, SetCursorStyle::DefaultUserShape),
|
||||
(3, SetCursorStyle::SteadyBar),
|
||||
(1, SetCursorStyle::SteadyBlock),
|
||||
] {
|
||||
terminal.backend_mut().output.clear();
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
Paragraph::new("ab cd ef")
|
||||
.style(Style::default().bg(rgb_color((80, 80, 80))).bold())
|
||||
.render(area, frame.buffer_mut());
|
||||
frame.set_cursor_style(style);
|
||||
frame.set_cursor_position((x, 1));
|
||||
})
|
||||
.expect("draw");
|
||||
|
||||
parser.process(&terminal.backend().output);
|
||||
assert_eq!(parser.screen().contents(), "\nab cd ef ");
|
||||
assert_eq!(parser.screen().cursor_position(), (1, x));
|
||||
for column in 0..area.width {
|
||||
let cell = parser.screen().cell(1, column).expect("viewport cell");
|
||||
assert_eq!(cell.bgcolor(), vt100::Color::Rgb(80, 80, 80));
|
||||
assert!(
|
||||
cell.bold(),
|
||||
"lost anchor or trailing-cell modifier at {column}"
|
||||
);
|
||||
}
|
||||
frames.push(terminal.backend().output().escape_debug().to_string());
|
||||
}
|
||||
assert_snapshot!("cursor_style_styled_frames", frames.join("\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_draw_repairs_owned_wide_hyperlink_after_skipped_glyphs() {
|
||||
let mut terminal =
|
||||
Terminal::with_options(CaptureBackend::new(/*width*/ 8, /*height*/ 1)).expect("terminal");
|
||||
let area = Rect::new(
|
||||
/*x*/ 0, /*y*/ 0, /*width*/ 8, /*height*/ 1,
|
||||
);
|
||||
terminal.set_viewport_area(area);
|
||||
let mut frames = Vec::new();
|
||||
for _ in 0..2 {
|
||||
terminal.backend_mut().output.clear();
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
let buffer = frame.buffer_mut();
|
||||
buffer.set_string(0, 0, "中x界", Style::default().bg(Color::Blue));
|
||||
buffer[(0, 0)].diff_option = CellDiffOption::Skip;
|
||||
buffer[(2, 0)].diff_option = CellDiffOption::Skip;
|
||||
buffer[(3, 0)].set_symbol("\x1b]8;;https://example.com\x07界\x1b]8;;\x07");
|
||||
buffer[(3, 0)].diff_option =
|
||||
CellDiffOption::ForcedWidth(NonZeroU16::new(/*n*/ 2).expect("wide glyph"));
|
||||
frame.set_cursor_position((5, 0));
|
||||
})
|
||||
.expect("draw");
|
||||
frames.push(terminal.backend().output().escape_debug().to_string());
|
||||
}
|
||||
assert_snapshot!("cursor_style_owned_wide_frames", frames.join("\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_draw_repairs_single_column_without_scrolling() {
|
||||
let mut terminal =
|
||||
Terminal::with_options(CaptureBackend::new(/*width*/ 1, /*height*/ 1)).expect("terminal");
|
||||
let area = Rect::new(
|
||||
/*x*/ 0, /*y*/ 0, /*width*/ 1, /*height*/ 1,
|
||||
);
|
||||
terminal.set_viewport_area(area);
|
||||
let mut frames = Vec::new();
|
||||
let mut parser =
|
||||
vt100::Parser::new(/*rows*/ 1, /*cols*/ 1, /*scrollback_len*/ 1);
|
||||
for _ in 0..3 {
|
||||
terminal.backend_mut().output.clear();
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
Paragraph::new("x")
|
||||
.style(Style::default().bg(Color::Blue))
|
||||
.render(area, frame.buffer_mut());
|
||||
frame.set_cursor_position((0, 0));
|
||||
})
|
||||
.expect("draw");
|
||||
parser.process(&terminal.backend().output);
|
||||
assert_eq!(parser.screen().contents(), "x");
|
||||
assert_eq!(parser.screen().cursor_position(), (0, 0));
|
||||
frames.push(terminal.backend().output().escape_debug().to_string());
|
||||
}
|
||||
parser.screen_mut().set_scrollback(/*rows*/ 1);
|
||||
assert_eq!(parser.screen().scrollback(), 0);
|
||||
assert_snapshot!("cursor_style_single_column_frames", frames.join("\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_draw_omits_cursor_style_without_an_owned_glyph() {
|
||||
let mut terminal =
|
||||
Terminal::with_options(CaptureBackend::new(/*width*/ 2, /*height*/ 1)).expect("terminal");
|
||||
for width in [0, 2] {
|
||||
terminal.set_viewport_area(Rect::new(
|
||||
/*x*/ 0, /*y*/ 0, width, /*height*/ 1,
|
||||
));
|
||||
for buffer in &mut terminal.buffers {
|
||||
for cell in &mut buffer.content {
|
||||
cell.diff_option = CellDiffOption::Skip;
|
||||
}
|
||||
}
|
||||
terminal.backend_mut().output.clear();
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
for cell in &mut frame.buffer_mut().content {
|
||||
cell.diff_option = CellDiffOption::Skip;
|
||||
}
|
||||
frame.set_cursor_style(SetCursorStyle::SteadyBar);
|
||||
frame.set_cursor_position((1, 0));
|
||||
})
|
||||
.expect("draw");
|
||||
assert_eq!(
|
||||
terminal.backend().output(),
|
||||
"\x1b[39m\x1b[49m\x1b[0m\x1b[1;2H\x1b[?25h"
|
||||
);
|
||||
}
|
||||
terminal.set_viewport_area(Rect::default());
|
||||
terminal.backend_mut().output.clear();
|
||||
terminal.draw(|_| {}).expect("hide cursor");
|
||||
assert_eq!(
|
||||
terminal.backend().output(),
|
||||
"\x1b[39m\x1b[49m\x1b[0m\x1b[?25l"
|
||||
);
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: tui/src/custom_terminal/tests/cursor_tests.rs
|
||||
expression: "frames.join(\"\\n\")"
|
||||
---
|
||||
\u{1b}[1;4H\u{1b}[39;48;5;4m\u{1b}]8;;https://example.com\u{7}界\u{1b}]8;;\u{7}\u{1b}[39m\u{1b}[49m\u{1b}[0m\u{1b}[1;4H\u{1b}[0 q\u{1b}[1;4H\u{1b}[39;48;5;4m\u{1b}]8;;https://example.com\u{7}界\u{1b}]8;;\u{7}\u{1b}[39m\u{1b}[49m\u{1b}[0m\u{1b}[1;6H\u{1b}[?25h
|
||||
\u{1b}[39m\u{1b}[49m\u{1b}[0m\u{1b}[1;4H\u{1b}[0 q\u{1b}[1;4H\u{1b}[39;48;5;4m\u{1b}]8;;https://example.com\u{7}界\u{1b}]8;;\u{7}\u{1b}[39m\u{1b}[49m\u{1b}[0m\u{1b}[1;6H\u{1b}[?25h
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
---
|
||||
source: tui/src/custom_terminal/tests/cursor_tests.rs
|
||||
expression: "frames.join(\"\\n\")"
|
||||
---
|
||||
\u{1b}[1;1H\u{1b}[39;48;5;4mx\u{1b}[39m\u{1b}[49m\u{1b}[0m\u{1b}[1;1H\u{1b}[0 q\u{1b}[1;1H\u{1b}[39;48;5;4mx\u{1b}[39m\u{1b}[49m\u{1b}[0m\u{1b}[1;1H\u{1b}[?25h
|
||||
\u{1b}[39m\u{1b}[49m\u{1b}[0m\u{1b}[1;1H\u{1b}[0 q\u{1b}[1;1H\u{1b}[39;48;5;4mx\u{1b}[39m\u{1b}[49m\u{1b}[0m\u{1b}[1;1H\u{1b}[?25h
|
||||
\u{1b}[39m\u{1b}[49m\u{1b}[0m\u{1b}[1;1H\u{1b}[0 q\u{1b}[1;1H\u{1b}[39;48;5;4mx\u{1b}[39m\u{1b}[49m\u{1b}[0m\u{1b}[1;1H\u{1b}[?25h
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
---
|
||||
source: tui/src/custom_terminal/tests/cursor_tests.rs
|
||||
expression: "frames.join(\"\\n\")"
|
||||
---
|
||||
\u{1b}[2;1H\u{1b}[1m\u{1b}[39;48;2;80;80;80mab cd ef \u{1b}[39m\u{1b}[49m\u{1b}[0m\u{1b}[2;1H\u{1b}[0 q\u{1b}[2;1H\u{1b}[1m\u{1b}[39;48;2;80;80;80ma\u{1b}[39m\u{1b}[49m\u{1b}[0m\u{1b}[2;2H\u{1b}[?25h
|
||||
\u{1b}[39m\u{1b}[49m\u{1b}[0m\u{1b}[2;1H\u{1b}[0 q\u{1b}[2;1H\u{1b}[1m\u{1b}[39;48;2;80;80;80ma\u{1b}[39m\u{1b}[49m\u{1b}[0m\u{1b}[2;5H\u{1b}[?25h
|
||||
\u{1b}[39m\u{1b}[49m\u{1b}[0m\u{1b}[2;1H\u{1b}[6 q\u{1b}[2;1H\u{1b}[1m\u{1b}[39;48;2;80;80;80ma\u{1b}[39m\u{1b}[49m\u{1b}[0m\u{1b}[2;4H\u{1b}[?25h
|
||||
\u{1b}[39m\u{1b}[49m\u{1b}[0m\u{1b}[2;1H\u{1b}[2 q\u{1b}[2;1H\u{1b}[1m\u{1b}[39;48;2;80;80;80ma\u{1b}[39m\u{1b}[49m\u{1b}[0m\u{1b}[2;2H\u{1b}[?25h
|
||||
Reference in New Issue
Block a user