box drawing fixes

This commit is contained in:
Zacharias-Brohn
2026-02-01 19:20:35 +01:00
parent 73b52ab341
commit 32fa9c891b
4 changed files with 2181 additions and 855 deletions
+648 -113
View File
File diff suppressed because it is too large Load Diff
+42 -7
View File
@@ -294,11 +294,29 @@ impl KeyboardState {
/// Encodes a key event according to the Kitty keyboard protocol.
pub struct KeyEncoder<'a> {
state: &'a KeyboardState,
/// Whether application cursor keys mode (DECCKM) is enabled.
/// When true, arrow keys send SS3 format (ESC O letter).
/// When false, arrow keys send CSI format (ESC [ letter).
application_cursor_keys: bool,
}
impl<'a> KeyEncoder<'a> {
pub fn new(state: &'a KeyboardState) -> Self {
Self { state }
Self {
state,
application_cursor_keys: false,
}
}
/// Creates a new KeyEncoder with application cursor keys mode setting.
pub fn with_cursor_mode(
state: &'a KeyboardState,
application_cursor_keys: bool,
) -> Self {
Self {
state,
application_cursor_keys,
}
}
/// Encodes a functional key press to bytes.
@@ -369,7 +387,9 @@ impl<'a> KeyEncoder<'a> {
if has_event_type {
result.push(b':');
result.extend_from_slice((event_type as u8).to_string().as_bytes());
result.extend_from_slice(
(event_type as u8).to_string().as_bytes(),
);
}
}
@@ -383,7 +403,11 @@ impl<'a> KeyEncoder<'a> {
}
/// Encodes functional keys in legacy mode.
fn encode_legacy_functional(&self, key: FunctionalKey, modifiers: Modifiers) -> Vec<u8> {
fn encode_legacy_functional(
&self,
key: FunctionalKey,
modifiers: Modifiers,
) -> Vec<u8> {
let mod_param = modifiers.encode();
match key {
@@ -453,12 +477,20 @@ impl<'a> KeyEncoder<'a> {
// Other functional keys - encode as CSI u
_ => {
let key_code = key as u32;
self.encode_csi_u(key_code, modifiers, KeyEventType::Press, None)
self.encode_csi_u(
key_code,
modifiers,
KeyEventType::Press,
None,
)
}
}
}
/// Encodes arrow/home/end keys: CSI 1;mod X (with modifiers) or SS3 X (no modifiers).
/// Encodes arrow/home/end keys based on DECCKM mode:
/// - Normal mode (application_cursor_keys=false): CSI letter (ESC [ letter)
/// - Application mode (application_cursor_keys=true): SS3 letter (ESC O letter)
/// With modifiers, always use CSI 1;mod letter format.
fn encode_arrow(&self, letter: u8, mod_param: Option<u8>) -> Vec<u8> {
if let Some(m) = mod_param {
// With modifiers: CSI 1;mod letter
@@ -466,9 +498,12 @@ impl<'a> KeyEncoder<'a> {
result.extend_from_slice(m.to_string().as_bytes());
result.push(letter);
result
} else {
// No modifiers: SS3 letter (application cursor mode)
} else if self.application_cursor_keys {
// Application cursor mode: SS3 letter (ESC O letter)
vec![0x1b, b'O', letter]
} else {
// Normal cursor mode: CSI letter (ESC [ letter)
vec![0x1b, b'[', letter]
}
}
+708 -228
View File
File diff suppressed because it is too large Load Diff
+408 -132
View File
@@ -16,6 +16,9 @@ pub enum TerminalCommand {
/// Triggered by OSC 51;statusline;<content> ST
/// Empty content clears the statusline (restores default).
SetStatusline(Option<String>),
/// Set clipboard content via OSC 52.
/// Triggered by OSC 52;c;<base64-data> ST
SetClipboard(String),
}
/// Direction for pane navigation.
@@ -132,23 +135,23 @@ impl Default for ColorPalette {
let mut colors = [[0u8; 3]; 256];
// Standard ANSI colors (0-7)
colors[0] = [0, 0, 0]; // Black
colors[1] = [204, 0, 0]; // Red
colors[2] = [0, 204, 0]; // Green
colors[3] = [204, 204, 0]; // Yellow
colors[4] = [0, 0, 204]; // Blue
colors[5] = [204, 0, 204]; // Magenta
colors[6] = [0, 204, 204]; // Cyan
colors[0] = [0, 0, 0]; // Black
colors[1] = [204, 0, 0]; // Red
colors[2] = [0, 204, 0]; // Green
colors[3] = [204, 204, 0]; // Yellow
colors[4] = [0, 0, 204]; // Blue
colors[5] = [204, 0, 204]; // Magenta
colors[6] = [0, 204, 204]; // Cyan
colors[7] = [204, 204, 204]; // White
// Bright ANSI colors (8-15)
colors[8] = [102, 102, 102]; // Bright Black (Gray)
colors[9] = [255, 0, 0]; // Bright Red
colors[10] = [0, 255, 0]; // Bright Green
colors[11] = [255, 255, 0]; // Bright Yellow
colors[12] = [0, 0, 255]; // Bright Blue
colors[13] = [255, 0, 255]; // Bright Magenta
colors[14] = [0, 255, 255]; // Bright Cyan
colors[8] = [102, 102, 102]; // Bright Black (Gray)
colors[9] = [255, 0, 0]; // Bright Red
colors[10] = [0, 255, 0]; // Bright Green
colors[11] = [255, 255, 0]; // Bright Yellow
colors[12] = [0, 0, 255]; // Bright Blue
colors[13] = [255, 0, 255]; // Bright Magenta
colors[14] = [0, 255, 255]; // Bright Cyan
colors[15] = [255, 255, 255]; // Bright White
// 216 color cube (16-231)
@@ -156,7 +159,8 @@ impl Default for ColorPalette {
for g in 0..6 {
for b in 0..6 {
let idx = 16 + r * 36 + g * 6 + b;
let to_val = |c: usize| if c == 0 { 0 } else { (55 + c * 40) as u8 };
let to_val =
|c: usize| if c == 0 { 0 } else { (55 + c * 40) as u8 };
colors[idx] = [to_val(r), to_val(g), to_val(b)];
}
}
@@ -196,7 +200,11 @@ impl ColorPalette {
let parse_component = |s: &str| -> Option<u8> {
let val = u16::from_str_radix(s, 16).ok()?;
// Scale to 8-bit if it's a 16-bit value
Some(if s.len() > 2 { (val >> 8) as u8 } else { val as u8 })
Some(if s.len() > 2 {
(val >> 8) as u8
} else {
val as u8
})
};
let r = parse_component(parts[0])?;
let g = parse_component(parts[1])?;
@@ -215,7 +223,9 @@ impl ColorPalette {
let [r, g, b] = self.default_fg;
[r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0]
}
Color::Rgb(r, g, b) => [*r as f32 / 255.0, *g as f32 / 255.0, *b as f32 / 255.0, 1.0],
Color::Rgb(r, g, b) => {
[*r as f32 / 255.0, *g as f32 / 255.0, *b as f32 / 255.0, 1.0]
}
Color::Indexed(idx) => {
let [r, g, b] = self.colors[*idx as usize];
[r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0]
@@ -230,7 +240,9 @@ impl ColorPalette {
let [r, g, b] = self.default_bg;
[r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0]
}
Color::Rgb(r, g, b) => [*r as f32 / 255.0, *g as f32 / 255.0, *b as f32 / 255.0, 1.0],
Color::Rgb(r, g, b) => {
[*r as f32 / 255.0, *g as f32 / 255.0, *b as f32 / 255.0, 1.0]
}
Color::Indexed(idx) => {
let [r, g, b] = self.colors[*idx as usize];
[r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0]
@@ -320,9 +332,13 @@ impl ProcessingStats {
#[cfg(feature = "render_timing")]
pub fn log_if_slow(&self, threshold_ms: u64) {
let total_ms = (self.scroll_up_ns + self.text_handler_ns + self.csi_handler_ns) / 1_000_000;
let total_ms =
(self.scroll_up_ns + self.text_handler_ns + self.csi_handler_ns)
/ 1_000_000;
if total_ms >= threshold_ms {
let vt_only_ns = self.vt_parser_ns.saturating_sub(self.text_handler_ns + self.csi_handler_ns);
let vt_only_ns = self
.vt_parser_ns
.saturating_sub(self.text_handler_ns + self.csi_handler_ns);
log::info!(
"[PARSE_DETAIL] text={:.2}ms ({}chars) csi={:.2}ms ({}x) vt_only={:.2}ms ({}calls) scroll={:.2}ms ({}x)",
self.text_handler_ns as f64 / 1_000_000.0,
@@ -520,6 +536,8 @@ pub struct Terminal {
pub application_cursor_keys: bool,
/// Auto-wrap mode (DECAWM) - wrap at end of line.
auto_wrap: bool,
/// Origin mode (DECOM) - cursor positioning relative to scroll region.
origin_mode: bool,
/// Bracketed paste mode - wrap pasted text with escape sequences.
pub bracketed_paste: bool,
/// Focus event reporting mode.
@@ -545,7 +563,12 @@ impl Terminal {
/// Creates a new terminal with the given dimensions and scrollback limit.
pub fn new(cols: usize, rows: usize, scrollback_limit: usize) -> Self {
log::info!("Terminal::new: cols={}, rows={}, scroll_bottom={}", cols, rows, rows.saturating_sub(1));
log::info!(
"Terminal::new: cols={}, rows={}, scroll_bottom={}",
cols,
rows,
rows.saturating_sub(1)
);
let grid = vec![vec![Cell::default(); cols]; rows];
let line_map: Vec<usize> = (0..rows).collect();
@@ -579,14 +602,15 @@ impl Terminal {
alternate_screen: None,
using_alternate_screen: false,
application_cursor_keys: false,
auto_wrap: true, // Auto-wrap is on by default
auto_wrap: true,
origin_mode: false,
bracketed_paste: false,
focus_reporting: false,
synchronized_output: false,
stats: ProcessingStats::default(),
command_queue: Vec::new(),
image_storage: ImageStorage::new(),
cell_width: 10.0, // Default, will be set by renderer
cell_width: 10.0, // Default, will be set by renderer
cell_height: 20.0, // Default, will be set by renderer
}
}
@@ -736,7 +760,13 @@ impl Terminal {
return;
}
log::info!("Terminal::resize: {}x{} -> {}x{}", self.cols, self.rows, cols, rows);
log::info!(
"Terminal::resize: {}x{} -> {}x{}",
self.cols,
self.rows,
cols,
rows
);
let old_cols = self.cols;
let old_rows = self.rows;
@@ -750,7 +780,8 @@ impl Terminal {
// Use actual row length - may differ from self.cols after scrollback swap
let old_row_len = self.grid[old_grid_row].len();
for col in 0..cols.min(old_row_len) {
new_grid[visual_row][col] = self.grid[old_grid_row][col].clone();
new_grid[visual_row][col] =
self.grid[old_grid_row][col].clone();
}
}
@@ -772,11 +803,16 @@ impl Terminal {
if let Some(ref mut saved) = self.alternate_screen {
let mut new_saved_grid = vec![vec![Cell::default(); cols]; rows];
for visual_row in 0..rows.min(old_rows) {
let old_grid_row = saved.line_map.get(visual_row).copied().unwrap_or(visual_row);
let old_grid_row = saved
.line_map
.get(visual_row)
.copied()
.unwrap_or(visual_row);
if old_grid_row < saved.grid.len() {
for col in 0..cols.min(old_cols) {
if col < saved.grid[old_grid_row].len() {
new_saved_grid[visual_row][col] = saved.grid[old_grid_row][col].clone();
new_saved_grid[visual_row][col] =
saved.grid[old_grid_row][col].clone();
}
}
}
@@ -859,7 +895,9 @@ impl Terminal {
let n = n.min(region_size);
#[cfg(feature = "render_timing")]
{ self.stats.scroll_up_count += n as u32; }
{
self.stats.scroll_up_count += n as u32;
}
for _ in 0..n {
// Save the top line's grid index before rotation
@@ -868,7 +906,10 @@ impl Terminal {
// Save to scrollback only if scrolling from the very top of the screen
// AND not in alternate screen mode (alternate screen never uses scrollback)
// AND scrollback is enabled (capacity > 0)
if self.scroll_top == 0 && !self.using_alternate_screen && self.scrollback.capacity > 0 {
if self.scroll_top == 0
&& !self.using_alternate_screen
&& self.scrollback.capacity > 0
{
// Get a slot in the ring buffer - this is O(1) with just modulo arithmetic
// If buffer is full, this overwrites the oldest line (perfect for our swap)
let cols = self.cols;
@@ -884,7 +925,10 @@ impl Terminal {
}
// Rotate line_map: shift all indices up within scroll region using memmove
self.line_map.copy_within(self.scroll_top + 1..=self.scroll_bottom, self.scroll_top);
self.line_map.copy_within(
self.scroll_top + 1..=self.scroll_bottom,
self.scroll_top,
);
self.line_map[self.scroll_bottom] = recycled_grid_row;
}
@@ -909,7 +953,11 @@ impl Terminal {
let word_end = word_start + 63;
// Calculate bit range within this word
let bit_start = if start > word_start { start - word_start } else { 0 };
let bit_start = if start > word_start {
start - word_start
} else {
0
};
let bit_end = if end < word_end { end - word_start } else { 63 };
// Create mask for bits [bit_start, bit_end]
@@ -936,7 +984,10 @@ impl Terminal {
let recycled_grid_row = self.line_map[self.scroll_bottom];
// Rotate line_map: shift all indices down within scroll region using memmove
self.line_map.copy_within(self.scroll_top..self.scroll_bottom, self.scroll_top + 1);
self.line_map.copy_within(
self.scroll_top..self.scroll_bottom,
self.scroll_top + 1,
);
self.line_map[self.scroll_top] = recycled_grid_row;
// Clear the recycled line (now at visual top of scroll region)
@@ -1089,7 +1140,8 @@ impl Terminal {
// M for press, m for release
// Most modern and recommended format
let suffix = if pressed { b'M' } else { b'm' };
format!("\x1b[<{};{};{}{}", cb, col, row, suffix as char).into_bytes()
format!("\x1b[<{};{};{}{}", cb, col, row, suffix as char)
.into_bytes()
}
MouseEncoding::Urxvt => {
// URXVT encoding: ESC [ Cb ; Cx ; Cy M
@@ -1123,7 +1175,8 @@ impl Terminal {
if i < lines_from_scrollback {
// This row comes from scrollback
// Use ring buffer's get() method with logical index
let scrollback_idx = scrollback_len - self.scroll_offset + i;
let scrollback_idx =
scrollback_len - self.scroll_offset + i;
if let Some(line) = self.scrollback.get(scrollback_idx) {
rows.push(line);
} else {
@@ -1161,8 +1214,10 @@ impl Terminal {
if row_idx < lines_from_scrollback {
// This row comes from scrollback
let scrollback_idx = scrollback_len - self.scroll_offset + row_idx;
self.scrollback.get(scrollback_idx)
let scrollback_idx =
scrollback_len - self.scroll_offset + row_idx;
self.scrollback
.get(scrollback_idx)
.or_else(|| Some(&self.grid[self.line_map[row_idx]]))
} else {
// This row comes from the grid
@@ -1179,7 +1234,9 @@ impl Terminal {
/// Inserts n blank lines at the cursor position, scrolling lines below down.
/// Uses line_map rotation for efficiency.
fn insert_lines(&mut self, n: usize) {
if self.cursor_row < self.scroll_top || self.cursor_row > self.scroll_bottom {
if self.cursor_row < self.scroll_top
|| self.cursor_row > self.scroll_bottom
{
return;
}
let n = n.min(self.scroll_bottom - self.cursor_row + 1);
@@ -1208,7 +1265,9 @@ impl Terminal {
/// Deletes n lines at the cursor position, scrolling lines below up.
/// Uses line_map rotation for efficiency.
fn delete_lines(&mut self, n: usize) {
if self.cursor_row < self.scroll_top || self.cursor_row > self.scroll_bottom {
if self.cursor_row < self.scroll_top
|| self.cursor_row > self.scroll_bottom
{
return;
}
let n = n.min(self.scroll_bottom - self.cursor_row + 1);
@@ -1243,7 +1302,10 @@ impl Terminal {
// Truncate n characters from the end
row.truncate(self.cols - n);
// Insert n blank characters at cursor position (single O(cols) operation)
row.splice(self.cursor_col..self.cursor_col, std::iter::repeat(blank).take(n));
row.splice(
self.cursor_col..self.cursor_col,
std::iter::repeat(blank).take(n),
);
self.mark_line_dirty(self.cursor_row);
}
@@ -1348,7 +1410,11 @@ impl Handler for Terminal {
if self.cursor_row > self.scroll_bottom {
self.scroll_up(1);
self.cursor_row = self.scroll_bottom;
log::trace!("LF: scrolled at row {}, now at scroll_bottom {}", old_row, self.cursor_row);
log::trace!(
"LF: scrolled at row {}, now at scroll_bottom {}",
old_row,
self.cursor_row
);
}
// Update cache after line change
cached_row = self.cursor_row;
@@ -1379,7 +1445,8 @@ impl Handler for Terminal {
// Write character directly - no wide char handling needed for ASCII
// SAFETY: cp is in 0x20..=0x7E which are valid ASCII chars
let c = unsafe { char::from_u32_unchecked(cp) };
self.grid[grid_row][self.cursor_col] = self.make_cell(c, false);
self.grid[grid_row][self.cursor_col] =
self.make_cell(c, false);
self.cursor_col += 1;
}
// Slow path for non-ASCII printable characters (including all Unicode)
@@ -1453,10 +1520,18 @@ impl Handler for Terminal {
if parts.len() >= 3 {
if let Ok(index_str) = std::str::from_utf8(parts[1]) {
if let Ok(index) = index_str.parse::<u8>() {
if let Ok(color_spec) = std::str::from_utf8(parts[2]) {
if let Some(rgb) = ColorPalette::parse_color_spec(color_spec) {
if let Ok(color_spec) =
std::str::from_utf8(parts[2])
{
if let Some(rgb) =
ColorPalette::parse_color_spec(color_spec)
{
self.palette.colors[index as usize] = rgb;
log::debug!("OSC 4: Set color {} to {:?}", index, rgb);
log::debug!(
"OSC 4: Set color {} to {:?}",
index,
rgb
);
}
}
}
@@ -1467,9 +1542,14 @@ impl Handler for Terminal {
10 => {
if parts.len() >= 2 {
if let Ok(color_spec) = std::str::from_utf8(parts[1]) {
if let Some(rgb) = ColorPalette::parse_color_spec(color_spec) {
if let Some(rgb) =
ColorPalette::parse_color_spec(color_spec)
{
self.palette.default_fg = rgb;
log::debug!("OSC 10: Set default foreground to {:?}", rgb);
log::debug!(
"OSC 10: Set default foreground to {:?}",
rgb
);
}
}
}
@@ -1478,9 +1558,14 @@ impl Handler for Terminal {
11 => {
if parts.len() >= 2 {
if let Ok(color_spec) = std::str::from_utf8(parts[1]) {
if let Some(rgb) = ColorPalette::parse_color_spec(color_spec) {
if let Some(rgb) =
ColorPalette::parse_color_spec(color_spec)
{
self.palette.default_bg = rgb;
log::debug!("OSC 11: Set default background to {:?}", rgb);
log::debug!(
"OSC 11: Set default background to {:?}",
rgb
);
}
}
}
@@ -1496,7 +1581,9 @@ impl Handler for Terminal {
match command {
"navigate" => {
if parts.len() >= 3 {
if let Ok(direction_str) = std::str::from_utf8(parts[2]) {
if let Ok(direction_str) =
std::str::from_utf8(parts[2])
{
let direction = match direction_str {
"up" => Some(Direction::Up),
"down" => Some(Direction::Down),
@@ -1505,8 +1592,15 @@ impl Handler for Terminal {
_ => None,
};
if let Some(dir) = direction {
log::debug!("OSC 51: Navigate {:?}", dir);
self.command_queue.push(TerminalCommand::NavigatePane(dir));
log::debug!(
"OSC 51: Navigate {:?}",
dir
);
self.command_queue.push(
TerminalCommand::NavigatePane(
dir,
),
);
}
}
}
@@ -1517,10 +1611,16 @@ impl Handler for Terminal {
// Content may be base64-encoded (prefixed with "b64:") to avoid
// escape sequence interpretation issues in the terminal
let prefix = b"51;statusline;";
let raw_content = if data.len() > prefix.len() && data.starts_with(prefix) {
std::str::from_utf8(&data[prefix.len()..]).ok().map(|s| s.to_string())
let raw_content = if data.len() > prefix.len()
&& data.starts_with(prefix)
{
std::str::from_utf8(&data[prefix.len()..])
.ok()
.map(|s| s.to_string())
} else if parts.len() >= 3 {
std::str::from_utf8(parts[2]).ok().map(|s| s.to_string())
std::str::from_utf8(parts[2])
.ok()
.map(|s| s.to_string())
} else {
None
};
@@ -1538,12 +1638,54 @@ impl Handler for Terminal {
}
});
let statusline = content.filter(|s| !s.is_empty());
log::info!("OSC 51: Set statusline: {:?}", statusline.as_ref().map(|s| format!("{} bytes", s.len())));
self.command_queue.push(TerminalCommand::SetStatusline(statusline));
let statusline =
content.filter(|s| !s.is_empty());
log::info!(
"OSC 51: Set statusline: {:?}",
statusline
.as_ref()
.map(|s| format!("{} bytes", s.len()))
);
self.command_queue.push(
TerminalCommand::SetStatusline(statusline),
);
}
_ => {
log::debug!("OSC 51: Unknown command '{}'", command);
log::debug!(
"OSC 51: Unknown command '{}'",
command
);
}
}
}
}
}
// OSC 52 - Clipboard operations
// Format: OSC 52;Pc;Pd ST
// Pc = clipboard type ('c' for clipboard, 'p' for primary, 's' for selection)
// Pd = base64-encoded data to set, or '?' to query
52 => {
if parts.len() >= 3 {
if let Ok(data_str) = std::str::from_utf8(parts[2]) {
if data_str == "?" {
log::debug!(
"OSC 52: Query clipboard (not implemented)"
);
} else {
use base64::Engine;
if let Ok(decoded) =
base64::engine::general_purpose::STANDARD
.decode(data_str)
{
if let Ok(text) = String::from_utf8(decoded) {
log::debug!(
"OSC 52: Set clipboard ({} bytes)",
text.len()
);
self.command_queue.push(
TerminalCommand::SetClipboard(text),
);
}
}
}
}
@@ -1590,8 +1732,10 @@ impl Handler for Terminal {
}
}
} else {
log::debug!("Unhandled DCS sequence: {:?}",
std::str::from_utf8(data).unwrap_or("<invalid utf8>"));
log::debug!(
"Unhandled DCS sequence: {:?}",
std::str::from_utf8(data).unwrap_or("<invalid utf8>")
);
}
}
@@ -1606,56 +1750,79 @@ impl Handler for Terminal {
let secondary = params.secondary;
match action {
// Cursor Up
'A' => {
let n = params.get(0, 1).max(1) as usize;
let old_row = self.cursor_row;
self.cursor_row = self.cursor_row.saturating_sub(n);
log::trace!("CSI A: cursor up {} from row {} to {}", n, old_row, self.cursor_row);
let min_row =
if self.origin_mode { self.scroll_top } else { 0 };
self.cursor_row =
self.cursor_row.saturating_sub(n).max(min_row);
}
// Cursor Down
'B' => {
let n = params.get(0, 1).max(1) as usize;
let old_row = self.cursor_row;
self.cursor_row = (self.cursor_row + n).min(self.rows - 1);
log::trace!("CSI B: cursor down {} from row {} to {}", n, old_row, self.cursor_row);
let max_row = if self.origin_mode {
self.scroll_bottom
} else {
self.rows - 1
};
self.cursor_row = (self.cursor_row + n).min(max_row);
}
// Cursor Forward
'C' => {
let n = params.get(0, 1).max(1) as usize;
let old_col = self.cursor_col;
self.cursor_col = (self.cursor_col + n).min(self.cols - 1);
log::trace!("CSI C: cursor forward {} from col {} to {}", n, old_col, self.cursor_col);
log::trace!(
"CSI C: cursor forward {} from col {} to {}",
n,
old_col,
self.cursor_col
);
}
// Cursor Back
'D' => {
let n = params.get(0, 1).max(1) as usize;
self.cursor_col = self.cursor_col.saturating_sub(n);
}
// Cursor Next Line (CNL)
'E' => {
let n = params.get(0, 1).max(1) as usize;
let max_row = if self.origin_mode {
self.scroll_bottom
} else {
self.rows - 1
};
self.cursor_col = 0;
self.cursor_row = (self.cursor_row + n).min(self.rows - 1);
self.cursor_row = (self.cursor_row + n).min(max_row);
}
// Cursor Previous Line (CPL)
'F' => {
let n = params.get(0, 1).max(1) as usize;
let min_row =
if self.origin_mode { self.scroll_top } else { 0 };
self.cursor_col = 0;
self.cursor_row = self.cursor_row.saturating_sub(n);
self.cursor_row =
self.cursor_row.saturating_sub(n).max(min_row);
}
// Cursor Horizontal Absolute (CHA)
'G' => {
let col = params.get(0, 1).max(1) as usize;
let old_col = self.cursor_col;
self.cursor_col = (col - 1).min(self.cols - 1);
log::trace!("CSI G: cursor to col {} (was {})", self.cursor_col, old_col);
log::trace!(
"CSI G: cursor to col {} (was {})",
self.cursor_col,
old_col
);
}
// Cursor Position
'H' | 'f' => {
let row = params.get(0, 1).max(1) as usize;
let col = params.get(1, 1).max(1) as usize;
self.cursor_row = (row - 1).min(self.rows - 1);
if self.origin_mode {
let abs_row =
(self.scroll_top + row - 1).min(self.scroll_bottom);
self.cursor_row = abs_row;
} else {
self.cursor_row = (row - 1).min(self.rows - 1);
}
self.cursor_col = (col - 1).min(self.cols - 1);
}
// Erase in Display
@@ -1754,7 +1921,8 @@ impl Handler for Terminal {
let n = (params.get(0, 1).max(1) as usize).min(65535); // Like Kitty's CSI_REP_MAX_REPETITIONS
if self.cursor_col > 0 && n > 0 {
let grid_row = self.line_map[self.cursor_row];
let last_char = self.grid[grid_row][self.cursor_col - 1].character;
let last_char =
self.grid[grid_row][self.cursor_col - 1].character;
let last_cp = last_char as u32;
// Fast path for ASCII: direct grid write, no width lookup
@@ -1799,7 +1967,13 @@ impl Handler for Terminal {
// Vertical Position Absolute (VPA)
'd' => {
let row = params.get(0, 1).max(1) as usize;
self.cursor_row = (row - 1).min(self.rows - 1);
if self.origin_mode {
let abs_row =
(self.scroll_top + row - 1).min(self.scroll_bottom);
self.cursor_row = abs_row;
} else {
self.cursor_row = (row - 1).min(self.rows - 1);
}
}
// SGR (Select Graphic Rendition)
'm' => {
@@ -1815,8 +1989,13 @@ impl Handler for Terminal {
}
6 => {
// Cursor position report
let response = format!("\x1b[{};{}R", self.cursor_row + 1, self.cursor_col + 1);
self.response_queue.extend_from_slice(response.as_bytes());
let response = format!(
"\x1b[{};{}R",
self.cursor_row + 1,
self.cursor_col + 1
);
self.response_queue
.extend_from_slice(response.as_bytes());
}
_ => {}
}
@@ -1844,7 +2023,10 @@ impl Handler for Terminal {
self.scroll_top = (top - 1).min(self.rows - 1);
self.scroll_bottom = (bottom - 1).min(self.rows - 1);
if self.scroll_top > self.scroll_bottom {
std::mem::swap(&mut self.scroll_top, &mut self.scroll_bottom);
std::mem::swap(
&mut self.scroll_top,
&mut self.scroll_bottom,
);
}
// Move cursor to home position
self.cursor_row = 0;
@@ -1856,25 +2038,44 @@ impl Handler for Terminal {
match ps {
14 => {
// Report text area size in pixels: CSI 4 ; height ; width t
let pixel_height = (self.rows as f32 * self.cell_height) as u32;
let pixel_width = (self.cols as f32 * self.cell_width) as u32;
let response = format!("\x1b[4;{};{}t", pixel_height, pixel_width);
self.response_queue.extend_from_slice(response.as_bytes());
log::debug!("XTWINOPS 14: Reported text area size {}x{} pixels", pixel_width, pixel_height);
let pixel_height =
(self.rows as f32 * self.cell_height) as u32;
let pixel_width =
(self.cols as f32 * self.cell_width) as u32;
let response =
format!("\x1b[4;{};{}t", pixel_height, pixel_width);
self.response_queue
.extend_from_slice(response.as_bytes());
log::debug!(
"XTWINOPS 14: Reported text area size {}x{} pixels",
pixel_width,
pixel_height
);
}
16 => {
// Report cell size in pixels: CSI 6 ; height ; width t
let cell_h = self.cell_height as u32;
let cell_w = self.cell_width as u32;
let response = format!("\x1b[6;{};{}t", cell_h, cell_w);
self.response_queue.extend_from_slice(response.as_bytes());
log::debug!("XTWINOPS 16: Reported cell size {}x{} pixels", cell_w, cell_h);
self.response_queue
.extend_from_slice(response.as_bytes());
log::debug!(
"XTWINOPS 16: Reported cell size {}x{} pixels",
cell_w,
cell_h
);
}
18 => {
// Report text area size in characters: CSI 8 ; rows ; cols t
let response = format!("\x1b[8;{};{}t", self.rows, self.cols);
self.response_queue.extend_from_slice(response.as_bytes());
log::debug!("XTWINOPS 18: Reported text area size {}x{} chars", self.cols, self.rows);
let response =
format!("\x1b[8;{};{}t", self.rows, self.cols);
self.response_queue
.extend_from_slice(response.as_bytes());
log::debug!(
"XTWINOPS 18: Reported text area size {}x{} chars",
self.cols,
self.rows
);
}
22 | 23 => {
// Save/restore window title - ignore
@@ -1884,9 +2085,17 @@ impl Handler for Terminal {
}
}
}
// Kitty keyboard protocol
// ANSI Save Cursor (CSI s) - DECSLRM uses CSI ? s which has primary='?'
's' if primary == 0 => {
self.save_cursor();
}
// CSI u: ANSI restore cursor (no params) vs Kitty keyboard protocol (with params)
'u' => {
self.handle_keyboard_protocol_csi(params);
if primary == 0 && params.num_params == 0 {
self.restore_cursor();
} else {
self.handle_keyboard_protocol_csi(params);
}
}
// DEC Private Mode Set (CSI ? Ps h)
'h' if primary == b'?' => {
@@ -1922,19 +2131,29 @@ impl Handler for Terminal {
underline_style: self.current_underline_style,
strikethrough: self.current_strikethrough,
};
log::debug!("ESC 7: Cursor saved at ({}, {})", self.cursor_col, self.cursor_row);
log::debug!(
"ESC 7: Cursor saved at ({}, {})",
self.cursor_col,
self.cursor_row
);
}
fn restore_cursor(&mut self) {
self.cursor_col = self.saved_cursor.col.min(self.cols.saturating_sub(1));
self.cursor_row = self.saved_cursor.row.min(self.rows.saturating_sub(1));
self.cursor_col =
self.saved_cursor.col.min(self.cols.saturating_sub(1));
self.cursor_row =
self.saved_cursor.row.min(self.rows.saturating_sub(1));
self.current_fg = self.saved_cursor.fg;
self.current_bg = self.saved_cursor.bg;
self.current_bold = self.saved_cursor.bold;
self.current_italic = self.saved_cursor.italic;
self.current_underline_style = self.saved_cursor.underline_style;
self.current_strikethrough = self.saved_cursor.strikethrough;
log::debug!("ESC 8: Cursor restored to ({}, {})", self.cursor_col, self.cursor_row);
log::debug!(
"ESC 8: Cursor restored to ({}, {})",
self.cursor_col,
self.cursor_row
);
}
fn reset(&mut self) {
@@ -1954,6 +2173,7 @@ impl Handler for Terminal {
self.mouse_encoding = MouseEncoding::X10;
self.application_cursor_keys = false;
self.auto_wrap = true;
self.origin_mode = false;
self.bracketed_paste = false;
self.focus_reporting = false;
self.synchronized_output = false;
@@ -2014,7 +2234,7 @@ impl Handler for Terminal {
for visual_row in 0..self.rows {
let grid_row = self.line_map[visual_row];
for cell in &mut self.grid[grid_row] {
*cell = Cell {
*cell = Cell {
character: 'E',
fg_color: Color::Default,
bg_color: Color::Default,
@@ -2085,14 +2305,18 @@ impl Terminal {
// If we're overwriting a wide character's continuation cell,
// we need to clear the first cell of that wide character
if self.grid[grid_row][self.cursor_col].wide_continuation && self.cursor_col > 0 {
if self.grid[grid_row][self.cursor_col].wide_continuation
&& self.cursor_col > 0
{
self.grid[grid_row][self.cursor_col - 1] = Cell::default();
}
// If we're overwriting the first cell of a wide character,
// we need to clear its continuation cell
if char_width == 1 && self.cursor_col + 1 < self.cols
&& self.grid[grid_row][self.cursor_col + 1].wide_continuation {
if char_width == 1
&& self.cursor_col + 1 < self.cols
&& self.grid[grid_row][self.cursor_col + 1].wide_continuation
{
self.grid[grid_row][self.cursor_col + 1] = Cell::default();
}
@@ -2106,7 +2330,8 @@ impl Terminal {
// If the next cell is the first cell of another wide character,
// clear its continuation cell
if self.cursor_col + 1 < self.cols
&& self.grid[grid_row][self.cursor_col + 1].wide_continuation {
&& self.grid[grid_row][self.cursor_col + 1].wide_continuation
{
self.grid[grid_row][self.cursor_col + 1] = Cell::default();
}
@@ -2120,7 +2345,10 @@ impl Terminal {
///
/// SAFETY: Caller must ensure i < params.num_params
#[inline(always)]
fn parse_extended_color(params: &CsiParams, i: usize) -> Option<(Color, usize)> {
fn parse_extended_color(
params: &CsiParams,
i: usize,
) -> Option<(Color, usize)> {
let num = params.num_params;
let p = &params.params;
let is_sub = &params.is_sub_param;
@@ -2131,11 +2359,10 @@ impl Terminal {
if mode == 5 && i + 2 < num {
return Some((Color::Indexed(p[i + 2] as u8), 2));
} else if mode == 2 && i + 4 < num {
return Some((Color::Rgb(
p[i + 2] as u8,
p[i + 3] as u8,
p[i + 4] as u8,
), 4));
return Some((
Color::Rgb(p[i + 2] as u8, p[i + 3] as u8, p[i + 4] as u8),
4,
));
}
} else if i + 2 < num {
// Regular format (38;2;r;g;b or 38;5;idx)
@@ -2143,11 +2370,10 @@ impl Terminal {
if mode == 5 {
return Some((Color::Indexed(p[i + 2] as u8), 2));
} else if mode == 2 && i + 4 < num {
return Some((Color::Rgb(
p[i + 2] as u8,
p[i + 3] as u8,
p[i + 4] as u8,
), 4));
return Some((
Color::Rgb(p[i + 2] as u8, p[i + 3] as u8, p[i + 4] as u8),
4,
));
}
}
None
@@ -2195,13 +2421,17 @@ impl Terminal {
22 => self.current_bold = false,
23 => self.current_italic = false,
24 => self.current_underline_style = 0,
27 => std::mem::swap(&mut self.current_fg, &mut self.current_bg),
27 => {
std::mem::swap(&mut self.current_fg, &mut self.current_bg)
}
29 => self.current_strikethrough = false,
// Standard foreground colors (30-37)
30..=37 => self.current_fg = Color::Indexed((code - 30) as u8),
38 => {
// Extended foreground color
if let Some((color, consumed)) = Self::parse_extended_color(params, i) {
if let Some((color, consumed)) =
Self::parse_extended_color(params, i)
{
self.current_fg = color;
i += consumed;
}
@@ -2211,16 +2441,22 @@ impl Terminal {
40..=47 => self.current_bg = Color::Indexed((code - 40) as u8),
48 => {
// Extended background color
if let Some((color, consumed)) = Self::parse_extended_color(params, i) {
if let Some((color, consumed)) =
Self::parse_extended_color(params, i)
{
self.current_bg = color;
i += consumed;
}
}
49 => self.current_bg = Color::Default,
// Bright foreground colors (90-97)
90..=97 => self.current_fg = Color::Indexed((code - 90 + 8) as u8),
90..=97 => {
self.current_fg = Color::Indexed((code - 90 + 8) as u8)
}
// Bright background colors (100-107)
100..=107 => self.current_bg = Color::Indexed((code - 100 + 8) as u8),
100..=107 => {
self.current_bg = Color::Indexed((code - 100 + 8) as u8)
}
_ => {}
}
i += 1;
@@ -2250,7 +2486,11 @@ impl Terminal {
let flags = params.get(0, 0) as u8;
let mode = params.get(1, 1) as u8;
self.keyboard.set_flags(flags, mode);
log::debug!("Keyboard flags set to {:?} (mode {})", self.keyboard.flags(), mode);
log::debug!(
"Keyboard flags set to {:?} (mode {})",
self.keyboard.flags(),
mode
);
}
b'>' => {
let flags = if params.num_params == 0 {
@@ -2259,12 +2499,18 @@ impl Terminal {
Some(params.params[0] as u8)
};
self.keyboard.push(flags);
log::debug!("Keyboard flags pushed: {:?}", self.keyboard.flags());
log::debug!(
"Keyboard flags pushed: {:?}",
self.keyboard.flags()
);
}
b'<' => {
let count = params.get(0, 1) as usize;
self.keyboard.pop(count);
log::debug!("Keyboard flags popped: {:?}", self.keyboard.flags());
log::debug!(
"Keyboard flags popped: {:?}",
self.keyboard.flags()
);
}
_ => {}
}
@@ -2279,6 +2525,16 @@ impl Terminal {
self.application_cursor_keys = true;
log::debug!("DECCKM: Application cursor keys enabled");
}
6 => {
self.origin_mode = true;
self.cursor_row = self.scroll_top;
self.cursor_col = 0;
log::debug!(
"DECOM: Origin mode enabled, cursor at ({}, {})",
self.cursor_col,
self.cursor_row
);
}
7 => {
self.auto_wrap = true;
log::debug!("DECAWM: Auto-wrap enabled");
@@ -2334,7 +2590,10 @@ impl Terminal {
self.synchronized_output = true;
log::trace!("Synchronized output enabled");
}
_ => log::debug!("Unhandled DEC private mode set: {}", params.params[i]),
_ => log::debug!(
"Unhandled DEC private mode set: {}",
params.params[i]
),
}
}
}
@@ -2348,6 +2607,14 @@ impl Terminal {
self.application_cursor_keys = false;
log::debug!("DECCKM: Normal cursor keys enabled");
}
6 => {
self.origin_mode = false;
self.cursor_row = 0;
self.cursor_col = 0;
log::debug!(
"DECOM: Origin mode disabled, cursor at (0, 0)"
);
}
7 => {
self.auto_wrap = false;
log::debug!("DECAWM: Auto-wrap disabled");
@@ -2372,7 +2639,9 @@ impl Terminal {
1002 => {
if self.mouse_tracking == MouseTrackingMode::ButtonEvent {
self.mouse_tracking = MouseTrackingMode::None;
log::debug!("Mouse tracking: Button-event mode disabled");
log::debug!(
"Mouse tracking: Button-event mode disabled"
);
}
}
1003 => {
@@ -2417,7 +2686,10 @@ impl Terminal {
self.synchronized_output = false;
log::trace!("Synchronized output disabled");
}
_ => log::debug!("Unhandled DEC private mode reset: {}", params.params[i]),
_ => log::debug!(
"Unhandled DEC private mode reset: {}",
params.params[i]
),
}
}
}
@@ -2449,13 +2721,14 @@ impl Terminal {
let absolute_row = self.scrollback.len() + self.cursor_row;
// Process the command
let (response, placement_result) = self.image_storage.process_command(
cmd,
self.cursor_col,
absolute_row,
self.cell_width,
self.cell_height,
);
let (response, placement_result) =
self.image_storage.process_command(
cmd,
self.cursor_col,
absolute_row,
self.cell_width,
self.cell_height,
);
// Queue the response to send back to the application
if let Some(resp) = response {
@@ -2468,10 +2741,13 @@ impl Terminal {
// by the number of rows in the image placement rectangle."
// However, if C=1 was specified, don't move the cursor.
if let Some(placement) = placement_result {
if !placement.suppress_cursor_move && !placement.virtual_placement {
if !placement.suppress_cursor_move
&& !placement.virtual_placement
{
// Move cursor down by (rows - 1) since we're already on the first row
// Then set cursor to the column after the image
let new_row = self.cursor_row + placement.rows.saturating_sub(1);
let new_row =
self.cursor_row + placement.rows.saturating_sub(1);
if new_row >= self.rows {
// Need to scroll
let scroll_amount = new_row - self.rows + 1;