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