Files
galaxy/app/src/terminal/model/grid/ansi_handler.rs
T

1947 lines
73 KiB
Rust

// path attribute needed due to current non-fs-based nesting of ansi_handler
// under grid_handler.
#[path = "ansi_handler/tab_stops.rs"]
mod tab_stops;
use std::cmp::min;
use std::collections::HashMap;
use std::io;
use std::ops::Range;
use std::sync::Arc;
use base64::Engine as _;
use bounded_vec_deque::BoundedVecDeque;
use galaxy_core::channel::ChannelState;
use galaxy_core::features::FeatureFlag;
use galaxy_terminal::model::ansi::CharsetIndex;
use galaxy_terminal::model::grid::cell;
use galaxy_terminal::model::{KeyboardModes, KeyboardModesApplyBehavior};
use galaxyui::image_cache::{resize_dimensions, FitType};
use pathfinder_geometry::vector::Vector2F;
use rand::Rng;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use crate::server::telemetry::ImageProtocol;
use crate::terminal::event::Event;
use crate::terminal::event_listener::ChannelEventListener;
use crate::terminal::model::ansi::{
self, Attr, Color, CursorStyle, Handler as _, NamedColor, PrecmdValue, PreexecValue,
};
use crate::terminal::model::cell::{Cell, Flags};
use crate::terminal::model::char_or_str::CharOrStr;
use crate::terminal::model::grid::indexing::IndexRegion as _;
use crate::terminal::model::grid::{grapheme_cursor, Dimensions as _};
use crate::terminal::model::image_map::{ImagePlacementData, ImageType, StoredImageMetadata};
use crate::terminal::model::index::{Point, VisibleRow};
use crate::terminal::model::iterm_image::{ITermImage, ITermImageDimensionUnit};
use crate::terminal::model::kitty::{
CursorMovementPolicy, KittyAction, KittyError, KittyResponse, StorageError,
};
use crate::terminal::model::selection::ScrollDelta;
use crate::terminal::model::ObfuscateSecrets;
use crate::terminal::{ClipboardType, SizeInfo};
use super::{AbsolutePoint, GridHandler, PerformResetGridChecks, TermMode};
use tab_stops::TabStops;
const MAX_IMAGE_CELL_HEIGHT: u32 = 255;
/// State needed for the grid-level implementation of [`ansi::Handler`].
#[derive(Clone)]
pub(super) struct State {
/// Information about cell dimensions.
pub cell_width: usize,
pub cell_height: usize,
/// Mode flags.
pub mode: TermMode,
/// Tabstops.
pub tabs: TabStops,
/// Current style of the cursor.
pub cursor_style: CursorStyle,
/// Index into `charsets`, pointing to what ASCII is currently being mapped to.
pub active_charset: CharsetIndex,
/// Scroll region.
///
/// Range going from top to bottom of the terminal.
pub scroll_region: Range<VisibleRow>,
/// Proxy for sending events to the event loop.
pub event_proxy: ChannelEventListener,
/// Whether the grid is for the alt screen.
pub is_alt_screen: bool,
/// Whether or not to obfuscate secrets on copy, respecting the Safe Mode setting.
pub obfuscate_secrets: ObfuscateSecrets,
/// Whether this Grid is in a shell context which supports handling the emoji presentation selector
/// correctly. Notably, Zsh does NOT support this well in bracketed paste mode (which we use for all Warp
/// commands), which can lead to cursor misalignment issues.
pub supports_emoji_presentation_selector: bool,
/// State related to the Reset Grid logic for ConPTY.
reset_grid_checks: ResetGridChecks,
/// Range of cells that were dirtied during the current run of byte parsing. See
/// [`Self::finish_byte_processing`] for where this is reset.
pub dirty_cells_range: Range<Point>,
// Dimension of pane.
pub pane_size: Vector2F,
/// The currently active keyboard mode.
pub keyboard_mode: KeyboardModes,
/// Kitty keyboard enhancement protocol mode stack.
/// Used purely for push/pop save/restore semantics.
/// `push` appends the new mode; `pop` truncates and restores
/// the previous entry as the active mode.
pub keyboard_mode_stack: BoundedVecDeque<KeyboardModes>,
}
impl State {
pub fn new(
size_info: &SizeInfo,
event_proxy: ChannelEventListener,
is_alt_screen: bool,
obfuscate_secrets: ObfuscateSecrets,
perform_reset_grid_checks: PerformResetGridChecks,
) -> Self {
let scroll_region = VisibleRow(0)..VisibleRow(size_info.rows());
let tabs = TabStops::new(size_info.columns());
let reset_grid_checks = match perform_reset_grid_checks {
PerformResetGridChecks::Yes => ResetGridChecks::Enabled {
received_osc: false,
},
PerformResetGridChecks::No => ResetGridChecks::Disabled,
};
Self {
cell_width: size_info.cell_width_px.as_f32() as usize,
cell_height: size_info.cell_height_px.as_f32() as usize,
mode: Default::default(),
tabs,
cursor_style: Default::default(),
active_charset: Default::default(),
scroll_region,
event_proxy,
is_alt_screen,
obfuscate_secrets,
// Assume that the Grid supports emoji presentation selector, until set otherwise.
supports_emoji_presentation_selector: true,
reset_grid_checks,
dirty_cells_range: Default::default(),
pane_size: size_info.pane_size_px(),
keyboard_mode: KeyboardModes::NO_MODE,
keyboard_mode_stack: BoundedVecDeque::new(super::KEYBOARD_MODE_STACK_MAX_DEPTH),
}
}
}
#[derive(Default, Clone, Copy, PartialEq, Debug)]
enum ResetGridChecks {
/// Checks are enabled for the grid.
Enabled { received_osc: bool },
/// Checks are disabled for the grid.
#[default]
Disabled,
}
impl ansi::Handler for GridHandler {
fn set_title(&mut self, _: Option<String>) {
log::error!("Handler method GridHandler::set_title should never be called. This should be handled by TerminalModel.");
}
fn set_cursor_style(&mut self, style: Option<ansi::CursorStyle>) {
self.ansi_handler_state.cursor_style = style.unwrap_or_default();
// Notify UI about blinking changes.
self.ansi_handler_state
.event_proxy
.send_terminal_event(Event::CursorBlinkingChange(
self.ansi_handler_state.cursor_style.blinking,
));
}
fn set_cursor_shape(&mut self, shape: ansi::CursorShape) {
self.ansi_handler_state.cursor_style.shape = shape;
}
fn input(&mut self, c: char) {
// We disable Reset Grid checks in unit tests, as they are not designed to test
// PTY integration. `#[cfg(test)]` only applies to unit tests, not integration tests.
#[cfg(all(windows, not(test)))]
if let ResetGridChecks::Enabled { received_osc } = self.ansi_handler_state.reset_grid_checks
{
debug_assert!(
received_osc,
"Grid received input but did not receive Reset Grid OSC"
);
}
// Number of cells the char will occupy.
let Some(width) = c.width() else {
return;
};
let num_cols = self.columns();
// Handle zero-width characters.
if width == 0 {
// Get previous column.
let mut col = self.grid.cursor().point.col;
if !self.grid.cursor().input_needs_wrap {
col = col.saturating_sub(1);
}
// Put zerowidth characters over first fullwidth character cell.
let row = self.grid.cursor_point().row;
if self.grid[row][col].flags.contains(Flags::WIDE_CHAR_SPACER) {
col = col.saturating_sub(1);
}
self.grid[row][col].push_zerowidth(c, /* log_long_grapheme_warnings */ true);
let cell_content_width = match self.grid[row][col].raw_content() {
CharOrStr::Str(s) => s.width(),
// Note that we should never reach here since we are pushing a zerowidth character,
// which should always make the cell content a string. However, we cover these cases
// exhaustively as a safeguard (to avoid panics).
CharOrStr::Char(c) => match c.width() {
Some(width) => width,
None => {
return;
}
},
};
// Bash and Fish support emoji variation selectors, but Zsh does not in bracketed paste
// mode. Specifically, this references sequences such as \0x2601\0xFE0F (☁️),
// which are commonly used in prompts e.g. GCloud prompt chip in Starship.
if cell_content_width == 2
&& self.ansi_handler_state.supports_emoji_presentation_selector
{
// Current cursor cell contains a wide character (double-width).
self.grid[row][col].flags.insert(Flags::WIDE_CHAR);
// Insert spacer at the next cell.
self.write_at_cursor(cell::DEFAULT_CHAR)
.flags
.insert(Flags::WIDE_CHAR_SPACER);
// Update cursor appropriately before early-return.
self.advance_cursor_by_one_cell();
}
return;
}
// Move cursor to next line.
if self.grid.cursor().input_needs_wrap {
self.wrapline();
}
// If in insert mode, first shift cells to the right.
if self.ansi_handler_state.mode.contains(TermMode::INSERT)
&& self.grid.cursor().point.col + width < num_cols
{
let cursor_point = self.grid.cursor_point();
let col = self.grid.cursor().point.col;
let bg = self.grid.cursor().template.bg;
// Reset any wide char pair at the insertion point before the
// shift moves cells away (write_at_cursor's own start boundary
// check runs too late — after the shift).
self.reset_wide_char_at_start_boundary(cursor_point.row, col, bg);
// Reset any wide char pair straddling the push-off boundary.
// Cells at [num_cols - width, num_cols) are discarded by the
// shift.
let push_off = num_cols - width;
self.reset_wide_char_at_end_boundary(cursor_point.row, push_off, bg);
let row = &mut self.grid[cursor_point.row][..];
for col in (col..(num_cols - width)).rev() {
row.swap(col + width, col);
}
}
if width == 1 {
self.write_at_cursor(c);
} else {
if self.grid.cursor().point.col + 1 >= num_cols {
if self.ansi_handler_state.mode.contains(TermMode::LINE_WRAP) {
// Insert placeholder before wide char if glyph does not fit in this row.
self.write_at_cursor(cell::DEFAULT_CHAR)
.flags
.insert(Flags::LEADING_WIDE_CHAR_SPACER);
self.wrapline();
} else {
// Prevent out of bounds crash when linewrapping is disabled.
self.move_cursor_forward(|cursor| {
cursor.input_needs_wrap = true;
});
return;
}
}
// Write full width glyph to current cursor cell.
self.write_at_cursor(c).flags.insert(Flags::WIDE_CHAR);
// Write spacer to cell following the wide glyph.
self.move_cursor_forward(|cursor| {
cursor.point.col += 1;
});
self.write_at_cursor(cell::DEFAULT_CHAR)
.flags
.insert(Flags::WIDE_CHAR_SPACER);
}
self.advance_cursor_by_one_cell();
}
fn goto(&mut self, row: VisibleRow, column: usize) {
log::trace!("Going to: line={row}, col={column}");
let (y_offset, max_y) = if self.ansi_handler_state.mode.contains(TermMode::ORIGIN) {
(
self.ansi_handler_state.scroll_region.start,
self.ansi_handler_state.scroll_region.end - 1,
)
} else {
(VisibleRow(0), VisibleRow(self.visible_rows() - 1))
};
let columns = self.columns();
self.update_cursor(|cursor| {
cursor.point.row = min(row + y_offset, max_y);
cursor.point.col = min(column, columns.saturating_sub(1));
cursor.input_needs_wrap = false;
});
}
fn goto_line(&mut self, row: VisibleRow) {
self.goto(row, self.grid.cursor().point.col);
}
fn goto_col(&mut self, column: usize) {
log::trace!("Going to column: {column}");
self.goto(self.grid.cursor().point.row, column)
}
fn insert_blank(&mut self, count: usize) {
let cursor = &self.grid.cursor();
let bg = cursor.template.bg;
// Ensure inserting within terminal bounds
let count = min(count, self.columns() - cursor.point.col);
let source = cursor.point.col;
let destination = cursor.point.col + count;
let num_cells = self.columns() - destination;
let cursor_point = self.grid.cursor_point();
// Reset any wide character pair that straddles the insertion boundary.
self.reset_wide_char_at_start_boundary(cursor_point.row, source, bg);
// Reset any wide char pair that straddles the push-off boundary.
// Cells at [cols - count, cols) are discarded when shifted right; if
// the first discarded cell is a WIDE_CHAR_SPACER, its partner in the
// kept zone would become orphaned after the shift.
let cols = self.columns();
let push_off = cols - count;
if push_off < cols {
self.reset_wide_char_at_end_boundary(cursor_point.row, push_off, bg);
}
let row = &mut self.grid[cursor_point.row][..];
for offset in (0..num_cells).rev() {
row.swap(destination + offset, source + offset);
}
// Cells were just moved out toward the end of the line;
// fill in between source and dest with blanks.
for cell in &mut row[source..destination] {
*cell = bg.into();
}
}
fn move_up(&mut self, lines: usize) {
log::trace!("Moving up: {lines}");
let move_to = self.grid.cursor().point.row.saturating_sub(lines);
self.goto(move_to, self.grid.cursor().point.col)
}
fn move_down(&mut self, lines: usize) {
log::trace!("Moving down: {lines}");
let move_to = self.grid.cursor().point.row + lines;
self.goto(move_to, self.grid.cursor().point.col)
}
fn identify_terminal<W: std::io::Write>(&mut self, writer: &mut W, intermediate: Option<char>) {
match intermediate {
None => {
log::trace!("Reporting primary device attributes");
let _ = writer.write_all(b"\x1b[?62c");
}
Some('>') => {
log::trace!("Reporting secondary device attributes");
// The version here is hardcoded, but there is a reason for that! :)
// Following the documentation from: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
// and Send Device Attributes (Secondary DA), the value should be of format: ` CSI > Pp ; Pv ; Pc c`.
// Pp denotes the terminal type, where 0 stands for `VT100` which we emulate. The
// `Pv` is a firmware version, and `Pc` indicates ROM cartridge.
//
// It turns out that this values are necessary for vim to recognize the terminal's
// mouse capabilities (SGR_MOUSE). Ie. if you want to use your mouse in vim and `:set mouse=a`
// to work like a charm, this value needs to pass set of checks.
// Apparently, in actual Vim's source code[1] there's a hardcoded list of versions
// for different recognized terminals/use cases. Otherwise, the code checks that
// the version `Pv` is higher than the `xterm` version when the SGR_MOUSE support
// was introduced[2] - version 277.
//
// Since we didn't want to claim xterm functionalities that we haven't yet implemnted in
// Warp, rather than passing the higher `Pv` value, we decided to use one of the
// hardcoded ones. `0;95;0` is set what iTerm2 sends.
// [1] https://github.com/vim/vim/blob/20c370d9f2ee89cb854054edf71f5004f6efff77/src/term.c#L4630
// [2] https://invisible-island.net/xterm/xterm.log.html#xterm_277
//
// Further reading for even more context:
// * http://vimdoc.sourceforge.net/htmldoc/options.html#'ttymouse'
// * https://github.com/alacritty/alacritty/issues/803
// * https://github.com/vim/vim/issues/2309
let version = "0;95;0";
let _ = writer.write_all(format!("\x1b[>{version}c").as_bytes());
}
_ => log::debug!("Unsupported device attributes intermediate"),
}
}
fn report_xtversion<W: std::io::Write>(&mut self, writer: &mut W) {
log::trace!("Reporting xtversion");
let version = ChannelState::app_version().unwrap_or("");
let _ = writer.write_all(format!("\x1bP>|Warp({version})\x1b\\").as_bytes());
}
fn device_status<W: std::io::Write>(&mut self, writer: &mut W, arg: usize) {
log::trace!("Reporting device status: {arg}");
match arg {
5 => {
let _ = writer.write_all(b"\x1b[0n");
}
6 => {
let pos = self.grid.cursor().point;
let response = format!("\x1b[{};{}R", pos.row + 1, pos.col + 1);
let _ = writer.write_all(response.as_bytes());
}
_ => log::debug!("unknown device status query: {arg}"),
};
}
fn move_forward(&mut self, columns: usize) {
log::trace!("Moving forward: {columns}");
let num_cols = self.columns();
self.move_cursor_forward(|cursor| {
cursor.point.col = min(cursor.point.col + columns, num_cols.saturating_sub(1));
cursor.input_needs_wrap = false;
});
}
fn move_backward(&mut self, columns: usize) {
log::trace!("Moving backward: {columns}");
self.update_cursor(|cursor| {
cursor.point.col = cursor.point.col.saturating_sub(columns);
cursor.input_needs_wrap = false;
});
}
fn move_down_and_cr(&mut self, lines: usize) {
log::trace!("Moving down and cr: {lines}");
let move_to = self.grid.cursor().point.row + lines;
self.goto(move_to, 0)
}
fn move_up_and_cr(&mut self, lines: usize) {
log::trace!("Moving up and cr: {lines}");
self.goto(self.grid.cursor().point.row.saturating_sub(lines), 0)
}
/// Insert tab at cursor position.
fn put_tab(&mut self, mut count: u16) {
// A tab after the last column is the same as a linebreak.
if self.grid.cursor().input_needs_wrap {
self.wrapline();
return;
}
while self.grid.cursor().point.col < self.columns() && count != 0 {
count -= 1;
let c = self.grid.cursor().charsets[self.ansi_handler_state.active_charset].map('\t');
let cell = self.grid.cursor_cell();
// Overwrite empty cells or ones containing whitespace with the
// current charset's tab character.
if cell.c == cell::DEFAULT_CHAR || cell.c == ' ' {
cell.c = c;
}
loop {
if (self.grid.cursor().point.col + 1) == self.columns() {
break;
}
self.move_cursor_forward(|cursor| {
cursor.point.col += 1;
});
if self.ansi_handler_state.tabs[self.grid.cursor().point.col] {
break;
}
}
}
}
fn backspace(&mut self) {
log::trace!("Backspace");
if self.grid.cursor().point.col > 0 {
self.update_cursor(|cursor| {
cursor.point.col -= 1;
cursor.input_needs_wrap = false;
});
}
}
fn carriage_return(&mut self) {
log::trace!("Carriage return");
self.update_cursor(|cursor| {
cursor.point.col = 0;
cursor.input_needs_wrap = false;
});
}
fn linefeed(&mut self) -> ScrollDelta {
log::trace!("Linefeed");
let next = self.grid.cursor().point.row + 1;
if next == self.ansi_handler_state.scroll_region.end {
return self.scroll_up(1);
}
if next.0 < self.visible_rows() {
self.move_cursor_forward(|cursor| {
cursor.point.row += 1;
});
}
ScrollDelta::zero()
}
/// Ring the terminal bell.
fn bell(&mut self) {
log::trace!("Bell");
self.ansi_handler_state
.event_proxy
.send_terminal_event(Event::Bell);
}
fn substitute(&mut self) {}
/// Run LF/NL.
///
/// LF/NL mode has some interesting history. According to ECMA-48 4th
/// edition, in LINE FEED mode,
///
/// > The execution of the formatter functions LINE FEED (LF), FORM FEED
/// > (FF), LINE TABULATION (VT) cause only movement of the active position in
/// > the direction of the line progression.
///
/// In NEW LINE mode,
///
/// > The execution of the formatter functions LINE FEED (LF), FORM FEED
/// > (FF), LINE TABULATION (VT) cause movement to the line home position on
/// > the following line, the following form, etc. In the case of LF this is
/// > referred to as the New Line (NL) option.
///
/// Additionally, ECMA-48 4th edition says that this option is deprecated.
/// ECMA-48 5th edition only mentions this option (without explanation)
/// saying that it's been removed.
///
/// As an emulator, we need to support it since applications may still rely
/// on it.
fn newline(&mut self) {
self.linefeed();
if self
.ansi_handler_state
.mode
.contains(TermMode::LINE_FEED_NEW_LINE)
{
self.carriage_return();
}
}
fn set_horizontal_tabstop(&mut self) {
log::trace!("Setting horizontal tabstop");
self.ansi_handler_state.tabs[self.grid.cursor().point.col] = true;
}
fn scroll_up(&mut self, lines: usize) -> ScrollDelta {
let origin = self.ansi_handler_state.scroll_region.start;
self.scroll_up_relative(origin, lines)
}
fn scroll_down(&mut self, lines: usize) -> ScrollDelta {
let origin = self.ansi_handler_state.scroll_region.start;
self.scroll_down_relative(origin, lines)
}
fn insert_blank_lines(&mut self, lines: usize) -> ScrollDelta {
log::trace!("Inserting blank {lines} lines");
let origin = self.grid.cursor().point.row;
if self.ansi_handler_state.scroll_region.contains(&origin) {
self.scroll_down_relative(origin, lines)
} else {
ScrollDelta::zero()
}
}
fn delete_lines(&mut self, lines: usize) -> ScrollDelta {
let origin = self.grid.cursor().point.row;
let lines = min(self.visible_rows() - origin.0, lines);
log::trace!("Deleting {lines} lines");
if lines > 0
&& self
.ansi_handler_state
.scroll_region
.contains(&self.grid.cursor().point.row)
{
self.scroll_up_relative(origin, lines)
} else {
ScrollDelta::zero()
}
}
fn erase_chars(&mut self, count: usize) {
if count == 0 {
return;
}
let cursor = &self.grid.cursor();
log::trace!("Erasing chars: count={}, col={}", count, cursor.point.col);
let start = cursor.point.col;
let end = min(start + count, self.columns());
// Cleared cells have current background color set.
let bg = self.grid.cursor().template.bg;
let cursor_point = self.grid.cursor_point();
// Reset any wide character pair that straddles the erase boundary.
self.reset_wide_char_at_start_boundary(cursor_point.row, start, bg);
if end < self.columns() {
self.reset_wide_char_at_end_boundary(cursor_point.row, end, bg);
}
let row = &mut self.grid[cursor_point.row];
for cell in &mut row[start..end] {
*cell = bg.into();
}
}
fn delete_chars(&mut self, count: usize) {
if count == 0 {
return;
}
let cols = self.columns();
let cursor = &self.grid.cursor();
let bg = cursor.template.bg;
// Ensure deleting within terminal bounds.
let count = min(count, cols);
let start = cursor.point.col;
let end = min(start + count, cols - 1);
let num_cells = cols - end;
let cursor_point = self.grid.cursor_point();
// Reset any wide character pair that straddles the delete boundary.
self.reset_wide_char_at_start_boundary(cursor_point.row, start, bg);
if end < cols {
self.reset_wide_char_at_end_boundary(cursor_point.row, end, bg);
}
let row = &mut self.grid[cursor_point.row][..];
for offset in 0..num_cells {
row.swap(start + offset, end + offset);
}
// Clear last `count` cells in the row. If deleting 1 char, need to delete
// 1 cell.
let end = cols - count;
for cell in &mut row[end..] {
*cell = bg.into();
}
}
fn move_backward_tabs(&mut self, count: u16) {
log::trace!("Moving backward {count} tabs");
for _ in 0..count {
let mut col = self.grid.cursor().point.col;
for i in (0..(col)).rev() {
if self.ansi_handler_state.tabs[i] {
col = i;
break;
}
}
self.update_cursor(|cursor| {
cursor.point.col = col;
});
}
}
fn move_forward_tabs(&mut self, count: u16) {
log::trace!("[unimplemented] Moving forward {count} tabs")
}
fn save_cursor_position(&mut self) {
log::trace!("Saving cursor position");
self.grid.saved_cursor = self.grid.cursor().clone();
}
fn restore_cursor_position(&mut self) {
log::trace!("Restoring cursor position");
let saved_cursor = self.grid.saved_cursor.clone();
self.update_cursor(|cursor| {
*cursor = saved_cursor;
});
}
fn clear_line(&mut self, mode: ansi::LineClearMode) {
log::trace!("Clearing line: {mode:?}");
let cursor = &self.grid.cursor();
let bg = cursor.template.bg;
let point = self.grid.cursor_point();
// Reset any wide character pair that straddles the clear boundary.
match mode {
ansi::LineClearMode::Right => {
self.reset_wide_char_at_start_boundary(point.row, point.col, bg);
}
ansi::LineClearMode::Left => {
let num_cols = self.columns();
if point.col + 1 < num_cols {
self.reset_wide_char_at_end_boundary(point.row, point.col + 1, bg);
}
}
ansi::LineClearMode::All => {}
}
let row = &mut self.grid[point.row];
let mut start_point = point;
let mut end_point = point;
match mode {
ansi::LineClearMode::Right => {
for cell in &mut row[point.col..] {
*cell = bg.into();
}
end_point.col = usize::MAX;
}
ansi::LineClearMode::Left => {
for cell in &mut row[..=point.col] {
*cell = bg.into();
}
start_point.col = usize::MIN;
}
ansi::LineClearMode::All => {
for cell in &mut row[..] {
*cell = bg.into();
}
end_point.col = usize::MAX;
end_point.col = usize::MAX;
}
}
self.images.evict_image_ids_between_points_with_type(
AbsolutePoint::from_point(start_point, self),
AbsolutePoint::from_point(end_point, self),
vec![ImageType::ITerm],
);
// TODO(alokedesai): Need to handle selection here.
}
fn clear_screen(&mut self, mode: ansi::ClearMode) {
log::trace!("Clearing screen: {mode:?}");
let bg = self.grid.cursor().template.bg;
let num_lines = self.visible_rows();
match mode {
ansi::ClearMode::Above => {
// Clearing above the cursor is guaranteed to clear everything
// in scrollback.
self.flat_storage.clear();
let cursor = self.grid.cursor().point;
// If clearing more than one line.
if cursor.row > VisibleRow(1) {
// Fully clear all lines before the current line.
self.grid
.region_mut(..cursor.row)
.each(|cell| *cell = bg.into());
}
// Clear up to the current column in the current line.
let end = min(cursor.col + 1, self.columns());
let cursor_point = self.grid.cursor_point();
// Reset any wide char pair that straddles the boundary just
// past the cleared region on the cursor row.
if end < self.columns() {
self.reset_wide_char_at_end_boundary(cursor_point.row, end, bg);
}
for cell in &mut self.grid[cursor_point.row][..end] {
*cell = bg.into();
}
}
ansi::ClearMode::Below => {
let cursor = self.grid.cursor().point;
let cursor_point = self.grid.cursor_point();
// Reset any wide char pair that straddles the start of the
// cleared region on the cursor row.
self.reset_wide_char_at_start_boundary(cursor_point.row, cursor.col, bg);
for cell in &mut self.grid[cursor_point.row][cursor.col..] {
*cell = bg.into();
}
if cursor.row.0 < num_lines - 1 {
self.grid
.region_mut((cursor.row + 1)..)
.each(|cell| *cell = bg.into());
}
}
ansi::ClearMode::All => {
if self.ansi_handler_state.is_alt_screen {
self.grid.region_mut(..).each(|cell| *cell = bg.into());
} else {
self.clear_viewport();
}
}
ansi::ClearMode::Saved if self.history_size() > 0 => {
self.flat_storage.clear();
self.grid.clear_history();
}
// We have no history to clear.
ansi::ClearMode::Saved => (),
ansi::ClearMode::ResetAndClear | ansi::ClearMode::ActiveBlock => {
self.flat_storage.clear();
self.grid.clear_and_reset_saving_cursor_line();
// Clear out state that will no longer be valid now that the
// grid has been cleared.
self.clear_secrets();
self.clear_displayed_rows_and_filter_matches();
// The row with the cursor still exists, though, so mark it as
// dirty and re-compute state accordingly.
let cursor_row = self.cursor_point().row;
self.ansi_handler_state.dirty_cells_range =
Point::new(cursor_row, 0)..Point::new(cursor_row + 1, 0);
self.on_finish_byte_processing(&ansi::ProcessorInput::new(&[]));
}
}
}
fn clear_tabs(&mut self, mode: ansi::TabulationClearMode) {
log::trace!("Clearing tabs: {mode:?}");
match mode {
ansi::TabulationClearMode::Current => {
self.ansi_handler_state.tabs[self.grid.cursor().point.col] = false;
}
ansi::TabulationClearMode::All => {
self.ansi_handler_state.tabs.clear_all();
}
}
}
/// Reset all important fields in the term struct.
#[inline]
fn reset_state(&mut self) {
self.grid.reset();
self.flat_storage.clear();
self.clear_secrets();
self.ansi_handler_state.active_charset = Default::default();
self.ansi_handler_state.cursor_style = CursorStyle::default();
self.ansi_handler_state.scroll_region = VisibleRow(0)..VisibleRow(self.visible_rows());
self.ansi_handler_state.tabs = TabStops::new(self.columns());
let blinking = self.ansi_handler_state.cursor_style.blinking;
self.ansi_handler_state
.event_proxy
.send_terminal_event(Event::CursorBlinkingChange(blinking));
}
fn reverse_index(&mut self) -> ScrollDelta {
log::trace!("Reversing index");
// If cursor is at the top.
if self.grid.cursor().point.row == self.ansi_handler_state.scroll_region.start {
self.scroll_down(1)
} else {
self.update_cursor(|cursor| {
cursor.point.row = cursor.point.row.saturating_sub(1);
});
ScrollDelta::zero()
}
}
/// Set a terminal attribute.
#[inline]
fn terminal_attribute(&mut self, attr: ansi::Attr) {
log::trace!("Setting attribute: {attr:?}");
let template = &mut self.grid.cursor.template;
match attr {
Attr::Foreground(color) => template.fg = color,
Attr::Background(color) => template.bg = color,
Attr::Reset => {
template.fg = Color::Named(NamedColor::Foreground);
template.bg = Color::Named(NamedColor::Background);
template.flags = Flags::empty();
}
Attr::Reverse => template.flags.insert(Flags::INVERSE),
Attr::CancelReverse => template.flags.remove(Flags::INVERSE),
Attr::Bold => template.flags.insert(Flags::BOLD),
Attr::CancelBold => template.flags.remove(Flags::BOLD),
Attr::Dim => template.flags.insert(Flags::DIM),
Attr::CancelBoldDim => template.flags.remove(Flags::BOLD | Flags::DIM),
Attr::Italic => template.flags.insert(Flags::ITALIC),
Attr::CancelItalic => template.flags.remove(Flags::ITALIC),
Attr::Underline => {
template.flags.remove(Flags::DOUBLE_UNDERLINE);
template.flags.insert(Flags::UNDERLINE);
}
Attr::DoubleUnderline => {
template.flags.remove(Flags::UNDERLINE);
template.flags.insert(Flags::DOUBLE_UNDERLINE);
}
Attr::CancelUnderline => {
template
.flags
.remove(Flags::UNDERLINE | Flags::DOUBLE_UNDERLINE);
}
Attr::Hidden => template.flags.insert(Flags::HIDDEN),
Attr::CancelHidden => template.flags.remove(Flags::HIDDEN),
Attr::Strike => template.flags.insert(Flags::STRIKEOUT),
Attr::CancelStrike => template.flags.remove(Flags::STRIKEOUT),
_ => {
log::debug!("Term got unhandled attr: {attr:?}");
}
}
}
fn set_mode(&mut self, mode: ansi::Mode) {
log::trace!("Setting mode: {mode:?}");
match mode {
ansi::Mode::UrgencyHints => {
self.ansi_handler_state.mode.insert(TermMode::URGENCY_HINTS)
}
ansi::Mode::SwapScreen { .. } => unreachable!("Handled in model layer"),
ansi::Mode::ShowCursor => self.ansi_handler_state.mode.insert(TermMode::SHOW_CURSOR),
ansi::Mode::CursorKeys => self.ansi_handler_state.mode.insert(TermMode::APP_CURSOR),
// Mouse protocols are mutually exclusive.
ansi::Mode::ReportMouseClicks => {
self.ansi_handler_state.mode.remove(TermMode::MOUSE_MODE);
self.ansi_handler_state
.mode
.insert(TermMode::MOUSE_REPORT_CLICK);
self.ansi_handler_state
.event_proxy
.send_terminal_event(Event::MouseCursorDirty);
}
ansi::Mode::ReportCellMouseMotion => {
self.ansi_handler_state.mode.remove(TermMode::MOUSE_MODE);
self.ansi_handler_state.mode.insert(TermMode::MOUSE_DRAG);
self.ansi_handler_state
.event_proxy
.send_terminal_event(Event::MouseCursorDirty);
}
ansi::Mode::ReportAllMouseMotion => {
self.ansi_handler_state.mode.remove(TermMode::MOUSE_MODE);
self.ansi_handler_state.mode.insert(TermMode::MOUSE_MOTION);
self.ansi_handler_state
.event_proxy
.send_terminal_event(Event::MouseCursorDirty);
}
ansi::Mode::ReportFocusInOut => {
self.ansi_handler_state.mode.insert(TermMode::FOCUS_IN_OUT)
}
ansi::Mode::BracketedPaste => self
.ansi_handler_state
.mode
.insert(TermMode::BRACKETED_PASTE),
// Mouse encodings are mutually exclusive.
ansi::Mode::SgrMouse => {
self.ansi_handler_state.mode.remove(TermMode::UTF8_MOUSE);
self.ansi_handler_state.mode.insert(TermMode::SGR_MOUSE);
}
ansi::Mode::Utf8Mouse => {
self.ansi_handler_state.mode.remove(TermMode::SGR_MOUSE);
self.ansi_handler_state.mode.insert(TermMode::UTF8_MOUSE);
}
ansi::Mode::AlternateScroll => self
.ansi_handler_state
.mode
.insert(TermMode::ALTERNATE_SCROLL),
ansi::Mode::LineWrap => self.ansi_handler_state.mode.insert(TermMode::LINE_WRAP),
ansi::Mode::LineFeedNewLine => self
.ansi_handler_state
.mode
.insert(TermMode::LINE_FEED_NEW_LINE),
ansi::Mode::Origin => self.ansi_handler_state.mode.insert(TermMode::ORIGIN),
ansi::Mode::DECCOLM => self.deccolm(),
ansi::Mode::Insert => self.ansi_handler_state.mode.insert(TermMode::INSERT),
ansi::Mode::BlinkingCursor => {
self.ansi_handler_state.cursor_style.blinking = true;
self.ansi_handler_state
.event_proxy
.send_terminal_event(Event::CursorBlinkingChange(true));
}
ansi::Mode::SyncOutput => {}
}
}
fn unset_mode(&mut self, mode: ansi::Mode) {
log::trace!("Unsetting mode: {mode:?}");
match mode {
ansi::Mode::UrgencyHints => {
self.ansi_handler_state.mode.remove(TermMode::URGENCY_HINTS)
}
ansi::Mode::SwapScreen { .. } => unreachable!("Handled in model layer"),
ansi::Mode::ShowCursor => self.ansi_handler_state.mode.remove(TermMode::SHOW_CURSOR),
ansi::Mode::CursorKeys => self.ansi_handler_state.mode.remove(TermMode::APP_CURSOR),
ansi::Mode::ReportMouseClicks => {
self.ansi_handler_state
.mode
.remove(TermMode::MOUSE_REPORT_CLICK);
self.ansi_handler_state
.event_proxy
.send_terminal_event(Event::MouseCursorDirty);
}
ansi::Mode::ReportCellMouseMotion => {
self.ansi_handler_state.mode.remove(TermMode::MOUSE_DRAG);
self.ansi_handler_state
.event_proxy
.send_terminal_event(Event::MouseCursorDirty);
}
ansi::Mode::ReportAllMouseMotion => {
self.ansi_handler_state.mode.remove(TermMode::MOUSE_MOTION);
self.ansi_handler_state
.event_proxy
.send_terminal_event(Event::MouseCursorDirty);
}
ansi::Mode::ReportFocusInOut => {
self.ansi_handler_state.mode.remove(TermMode::FOCUS_IN_OUT)
}
ansi::Mode::BracketedPaste => self
.ansi_handler_state
.mode
.remove(TermMode::BRACKETED_PASTE),
ansi::Mode::SgrMouse => self.ansi_handler_state.mode.remove(TermMode::SGR_MOUSE),
ansi::Mode::Utf8Mouse => self.ansi_handler_state.mode.remove(TermMode::UTF8_MOUSE),
ansi::Mode::AlternateScroll => self
.ansi_handler_state
.mode
.remove(TermMode::ALTERNATE_SCROLL),
ansi::Mode::LineWrap => self.ansi_handler_state.mode.remove(TermMode::LINE_WRAP),
ansi::Mode::LineFeedNewLine => self
.ansi_handler_state
.mode
.remove(TermMode::LINE_FEED_NEW_LINE),
ansi::Mode::Origin => self.ansi_handler_state.mode.remove(TermMode::ORIGIN),
ansi::Mode::DECCOLM => self.deccolm(),
ansi::Mode::Insert => self.ansi_handler_state.mode.remove(TermMode::INSERT),
ansi::Mode::BlinkingCursor => {
self.ansi_handler_state.cursor_style.blinking = false;
self.ansi_handler_state
.event_proxy
.send_terminal_event(Event::CursorBlinkingChange(false));
}
_ => {}
}
}
fn set_scrolling_region(&mut self, top: usize, bottom: Option<usize>) {
// Fallback to the last line as default.
let bottom = bottom.unwrap_or_else(|| self.visible_rows());
if top >= bottom {
log::debug!("Invalid scrolling region: ({top};{bottom})");
return;
}
// Bottom should be included in the range, but range end is not
// usually included. One option would be to use an inclusive
// range, but instead we just let the open range end be 1
// higher.
let start = VisibleRow(top - 1);
let end = VisibleRow(bottom);
log::trace!("Setting scrolling region: ({start};{end})");
self.ansi_handler_state.scroll_region.start = min(start, VisibleRow(self.visible_rows()));
self.ansi_handler_state.scroll_region.end = min(end, VisibleRow(self.visible_rows()));
self.goto(VisibleRow(0), 0);
}
fn set_keypad_application_mode(&mut self) {
self.ansi_handler_state.mode.insert(TermMode::APP_KEYPAD);
}
fn unset_keypad_application_mode(&mut self) {
self.ansi_handler_state.mode.remove(TermMode::APP_KEYPAD);
}
fn set_active_charset(&mut self, index: ansi::CharsetIndex) {
self.ansi_handler_state.active_charset = index;
}
fn configure_charset(&mut self, index: ansi::CharsetIndex, charset: ansi::StandardCharset) {
self.grid.cursor.charsets[index] = charset;
}
fn set_color(&mut self, _: usize, _: galaxyui::color::ColorU) {
log::error!("Handler method GridHandler::set_color should never be called. This should be handled by TerminalModel.");
}
fn dynamic_color_sequence<W: std::io::Write>(&mut self, _: &mut W, _: u8, _: usize, _: &str) {
log::error!("Handler method GridHandler::dynamic_color_sequence should never be called. This should be handled by TerminalModel.");
}
fn reset_color(&mut self, _: usize) {
log::error!("Handler method GridHandler::reset_color should never be called. This should be handled by TerminalModel.");
}
fn clipboard_store(&mut self, clipboard: u8, base64: &[u8]) {
let clipboard_type = match clipboard {
b'c' => ClipboardType::Clipboard,
b'p' | b's' => ClipboardType::Selection,
_ => return,
};
if let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(base64) {
if let Ok(text) = String::from_utf8(bytes) {
self.ansi_handler_state
.event_proxy
.send_terminal_event(Event::ClipboardStore(clipboard_type, text));
}
}
}
fn clipboard_load(&mut self, clipboard: u8, terminator: &str) {
let clipboard_type = match clipboard {
b'c' => ClipboardType::Clipboard,
b'p' | b's' => ClipboardType::Selection,
_ => return,
};
let terminator = terminator.to_owned();
self.ansi_handler_state
.event_proxy
.send_terminal_event(Event::ClipboardLoad(
clipboard_type,
Arc::new(move |text| {
let base64 = base64::engine::general_purpose::STANDARD.encode(text);
format!("\x1b]52;{};{}{}", clipboard as char, base64, terminator)
}),
));
}
fn decaln(&mut self) {
log::trace!("Decalnning");
self.grid.region_mut(..).each(|cell| {
*cell = Cell::default();
cell.c = 'E';
});
}
fn push_title(&mut self) {
log::error!("Handler method GridHandler::push_title should never be called. This should be handled by TerminalModel.");
}
fn pop_title(&mut self) {
log::error!("Handler method GridHandler::pop_title should never be called. This should be handled by TerminalModel.");
}
fn text_area_size_pixels<W: std::io::Write>(&mut self, writer: &mut W) {
let width = self.ansi_handler_state.cell_width * self.columns();
let height = self.ansi_handler_state.cell_height * self.visible_rows();
let _ = write!(writer, "\x1b[4;{height};{width}t");
}
fn text_area_size_chars<W: std::io::Write>(&mut self, writer: &mut W) {
let _ = write!(writer, "\x1b[8;{};{}t", self.visible_rows(), self.columns());
}
fn precmd(&mut self, _: PrecmdValue) {
unreachable!("Precmd hook is handled at block layer")
}
fn preexec(&mut self, _: PreexecValue) {
unreachable!("Precmd hook is handled at block layer")
}
fn on_finish_byte_processing(&mut self, _: &ansi::ProcessorInput<'_>) {
// Make sure the max cursor and dirty cell range are up-to-date.
self.grid.update_max_cursor();
self.update_dirty_cells_range();
self.maybe_scan_dirty_cells_for_secrets();
if self.finished && self.num_lines_truncated() > 0 {
// Occassionally upon finishing the grid there are truncated rows that we have
// not yet accounted for.
self.refilter_lines();
} else {
self.maybe_filter_dirty_lines();
}
self.reset_dirty_cells_range_to_cursor_point();
// Update bottommost visible content row for content_len() trimming.
if self.track_content_length {
self.bottommost_visible_content_row = self.bottommost_visible_content_row_backward();
}
if self.ansi_handler_state.is_alt_screen {
debug_assert_eq!(
self.flat_storage.total_rows(),
0,
"the alt screen grid should never put any rows in flat storage"
);
}
}
fn on_reset_grid(&mut self) {
match &mut self.ansi_handler_state.reset_grid_checks {
ResetGridChecks::Enabled { received_osc } => {
debug_assert!(
!*received_osc,
"Grid has already received a Reset Grid OSC."
);
*received_osc = true;
}
ResetGridChecks::Disabled => (),
}
}
fn handle_completed_iterm_image(&mut self, image: ITermImage) {
if !FeatureFlag::ITermImages.is_enabled() {
return;
}
if image.metadata.image_size.x() == 0.0 || image.metadata.image_size.y() == 0.0 {
return;
}
if let Some((width, _)) = image.metadata.desired_width {
if width == 0 {
return;
}
}
if let Some((height, _)) = image.metadata.desired_height {
if height == 0 {
return;
}
}
let mut desired_width_px = image.metadata.image_size.x();
let mut desired_height_px = image.metadata.image_size.y();
// We may have received a desired dimension in the form of pixels, percent of pane, or # of cells.
// We need to convert this into pixels given the current state of the terminal and its sizing.
if let Some((width, width_units)) = image.metadata.desired_width {
desired_width_px = match width_units {
ITermImageDimensionUnit::Cell => {
((self.ansi_handler_state.cell_width as u32) * width) as f32
}
ITermImageDimensionUnit::Percent => {
(self.columns() as f32)
* (self.ansi_handler_state.cell_width as f32)
* (width as f32)
* 0.01
}
ITermImageDimensionUnit::Pixel => width as f32,
}
}
if let Some((height, height_units)) = image.metadata.desired_height {
desired_height_px = match height_units {
ITermImageDimensionUnit::Cell => {
((self.ansi_handler_state.cell_height as u32) * height) as f32
}
ITermImageDimensionUnit::Percent => {
self.ansi_handler_state.pane_size.y() * (height as f32) * 0.01
}
ITermImageDimensionUnit::Pixel => height as f32,
}
}
// The largest iTerm will render an image is the width of the terminal when it receives the image.
// This logic ensures that when an image's size is calculated based on the current state of the terminal,
// it will not be rendered partially off the right of the screen. At max, it will occupy the columns from the
// cursor to the right edge.
let max_width = (self.columns() as u32 - self.cursor_point().col as u32)
* (self.ansi_handler_state.cell_width as u32);
let desired_width_px = min(desired_width_px as u32, max_width);
let max_height = MAX_IMAGE_CELL_HEIGHT * self.ansi_handler_state.cell_height as u32;
let desired_height_px = min(desired_height_px as u32, max_height);
let (width_px, height_px) = resize_dimensions(
image.metadata.image_size.x() as u32,
image.metadata.image_size.y() as u32,
desired_width_px,
desired_height_px,
if image.metadata.preserve_aspect_ratio {
FitType::Contain
} else {
FitType::Stretch
},
);
let image_size = Vector2F::new(width_px as f32, height_px as f32);
// Convert the visual dimension in pixels to cells. We want to round up if this doesn't perfectly fit within an amount of cells.
let height_cells =
(height_px as f32 / (self.ansi_handler_state.cell_height as f32)).ceil() as usize;
// Convert the user requested dimension in pixels to cells. This is needed to scroll the cursor by the space the user requested.
// If preserve_aspect_ratio is true, this may be larger than the visual dimension of the image.
// We want to round up if this doesn't perfectly fit within an amount of cells.
let (scroll_up_px, scroll_right_px) =
if image.metadata.desired_height.is_some() && image.metadata.desired_width.is_some() {
(desired_height_px as f32, desired_width_px as f32)
} else {
(height_px as f32, width_px as f32)
};
let scroll_up_cells =
(scroll_up_px / (self.ansi_handler_state.cell_height as f32)).ceil() as usize;
let scroll_right_cells =
(scroll_right_px / (self.ansi_handler_state.cell_width as f32)).ceil() as usize;
let image_id = image.metadata.id;
let placement_id = rand::thread_rng().gen();
self.ansi_handler_state
.event_proxy
.send_terminal_event(Event::ImageReceived {
image_id,
image_data: image.data,
image_protocol: ImageProtocol::ITerm,
});
self.images.add_image_placement_data(
image_id,
placement_id,
ImagePlacementData {
z_index: 0,
height_cells,
image_size,
},
);
self.images.place(
image_id,
placement_id,
AbsolutePoint::from_point(self.cursor_point(), self),
ImageType::ITerm,
self.num_lines_truncated(),
);
// Create the whitespace to fit the image on.
for _ in 0..scroll_up_cells - 1 {
self.newline();
}
let num_cols = self.columns();
// Move the cursor to the same row but after the image.
self.move_cursor_forward(|cursor| {
if cursor.point.col + scroll_right_cells < num_cols {
cursor.point.col += scroll_right_cells;
} else {
cursor.input_needs_wrap = true;
}
});
}
fn handle_completed_kitty_action(
&mut self,
action: KittyAction,
metadata: &mut HashMap<u32, StoredImageMetadata>,
) -> Option<KittyResponse> {
Some(self.handle_completed_kitty_action_internal(action, metadata))
}
fn set_keyboard_enhancement_flags(
&mut self,
mode: KeyboardModes,
apply: KeyboardModesApplyBehavior,
) {
if !FeatureFlag::KittyKeyboardProtocol.is_enabled() {
return;
}
self.set_keyboard_mode(mode, apply);
}
fn push_keyboard_enhancement_flags(&mut self, mode: KeyboardModes) {
if !FeatureFlag::KittyKeyboardProtocol.is_enabled() {
return;
}
self.push_keyboard_mode(mode);
}
fn pop_keyboard_enhancement_flags(&mut self, count: u16) {
if !FeatureFlag::KittyKeyboardProtocol.is_enabled() {
return;
}
self.pop_keyboard_modes(count);
}
fn query_keyboard_enhancement_flags<W: io::Write>(&mut self, writer: &mut W) {
if !FeatureFlag::KittyKeyboardProtocol.is_enabled() {
return;
}
// Respond with CSI ? flags u
let mode = self.ansi_handler_state.keyboard_mode;
let response = format!("\x1b[?{}u", mode.bits());
let _ = writer.write_all(response.as_bytes());
}
}
/// Helper functions for the [`ansi::Handler`] implementation.
impl GridHandler {
/// Advances the cursor by one cell, handling wrapping appropriately.
fn advance_cursor_by_one_cell(&mut self) {
let num_cols = self.columns();
self.move_cursor_forward(|cursor| {
if cursor.point.col + 1 < num_cols {
cursor.point.col += 1;
} else {
cursor.input_needs_wrap = true;
}
});
}
/// Insert a linebreak at the current cursor position.
#[inline]
pub(crate) fn wrapline(&mut self) {
if !self.ansi_handler_state.mode.contains(TermMode::LINE_WRAP) {
return;
}
log::trace!("Wrapping input");
self.grid.cursor_cell().flags.insert(Flags::WRAPLINE);
if (self.grid.cursor().point.row + 1) >= self.ansi_handler_state.scroll_region.end {
self.linefeed();
} else {
self.move_cursor_forward(|cursor| {
cursor.point.row += 1;
});
}
// CORRECTNESS: Even though we're technically moving the cursor
// backwards, the cursor is guaranteed to be ahead of where it
// was at the start of `wrapline()`, which is the important thing
// from a correctness perspective.
self.move_cursor_forward_unchecked(|cursor| {
cursor.point.col = 0;
cursor.input_needs_wrap = false;
});
}
/// Resets both halves of a wide character pair when `col` is at the
/// **start** of an operation's range (i.e., `col` itself is about to be
/// overwritten). Handles both WIDE_CHAR_SPACER (resets the preceding
/// WIDE_CHAR) and WIDE_CHAR (resets the following spacer).
fn reset_wide_char_at_start_boundary(&mut self, row: usize, col: usize, bg: Color) {
let num_cols = self.columns();
let grid_row = &mut self.grid[row][..];
if grid_row[col].flags.contains(Flags::WIDE_CHAR_SPACER) && col > 0 {
// Reset the spacer and its WIDE_CHAR partner.
grid_row[col] = bg.into();
grid_row[col - 1] = bg.into();
}
if grid_row[col].flags.contains(Flags::WIDE_CHAR) && col + 1 < num_cols {
// Reset the WIDE_CHAR and its spacer partner.
grid_row[col] = bg.into();
grid_row[col + 1] = bg.into();
}
}
/// Resets a wide character pair when `col` is at the **end** of an
/// operation's range (i.e., `col` is the first cell just past the range).
/// Only handles WIDE_CHAR_SPACER, because a WIDE_CHAR at `col` means the
/// entire pair is outside the range and should be left intact.
fn reset_wide_char_at_end_boundary(&mut self, row: usize, col: usize, bg: Color) {
let grid_row = &mut self.grid[row][..];
if grid_row[col].flags.contains(Flags::WIDE_CHAR_SPACER) && col > 0 {
// The spacer's WIDE_CHAR partner is inside the operation's range
// and is being cleared, so reset the spacer too.
grid_row[col] = bg.into();
grid_row[col - 1] = bg.into();
}
}
/// Write `c` to the cell at the cursor position.
#[inline(always)]
fn write_at_cursor(&mut self, c: char) -> &mut Cell {
self.images.evict_images_at_point_with_type(
AbsolutePoint::from_point(self.cursor_point(), self),
&[ImageType::ITerm],
);
// If the cursor cell is part of a wide character pair, reset the
// other half so we don't leave an orphaned flag. The check is
// inlined here (rather than delegating to
// reset_wide_char_at_start_boundary) to avoid an opaque function
// call inside this #[inline(always)] hot path — that call would
// force the compiler to reload all self state from memory
// afterward.
let cursor_point = self.grid.cursor_point();
let col = cursor_point.col;
let row = cursor_point.row;
let cell_flags = self.grid[row][col].flags;
if cell_flags.intersects(Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER) {
let bg = self.grid.cursor().template.bg;
let num_cols = self.columns();
// Clear LEADING_WIDE_CHAR_SPACER on the previous row when
// overwriting a wrapped wide char at cols 0-1. A fresh write
// during the normal wrapping flow has an empty cell, so the
// outer intersects guard prevents us from touching a spacer
// that was just placed.
if col <= 1 && row > 0 {
let is_wrapped = (col == 0 && cell_flags.contains(Flags::WIDE_CHAR))
|| (col == 1 && cell_flags.contains(Flags::WIDE_CHAR_SPACER));
if is_wrapped {
self.grid[row - 1][num_cols - 1]
.flags
.remove(Flags::LEADING_WIDE_CHAR_SPACER);
}
}
// Reset the other half of the wide char pair.
if cell_flags.contains(Flags::WIDE_CHAR_SPACER) && col > 0 {
self.grid[row][col] = bg.into();
self.grid[row][col - 1] = bg.into();
} else if cell_flags.contains(Flags::WIDE_CHAR) && col + 1 < num_cols {
self.grid[row][col] = bg.into();
self.grid[row][col + 1] = bg.into();
}
}
let c = self.grid.cursor().charsets[self.ansi_handler_state.active_charset].map(c);
let fg = self.grid.cursor().template.fg;
let bg = self.grid.cursor().template.bg;
let flags = self.grid.cursor().template.flags;
let cursor_cell = self.grid.cursor_cell();
cursor_cell.drop_extra();
cursor_cell.c = c;
cursor_cell.fg = fg;
cursor_cell.bg = bg;
cursor_cell.flags = flags;
cursor_cell
}
/// Scroll screen down.
///
/// Text moves down; clear at bottom
/// Expects origin to be in scroll range.
#[inline]
fn scroll_down_relative(&mut self, origin_row: VisibleRow, mut lines: usize) -> ScrollDelta {
log::trace!("Scrolling down relative: origin={origin_row}, lines={lines}");
lines = min(
lines,
self.ansi_handler_state.scroll_region.end - self.ansi_handler_state.scroll_region.start,
);
lines = min(
lines,
self.ansi_handler_state.scroll_region.end - origin_row,
);
let region = origin_row..self.ansi_handler_state.scroll_region.end;
// Scroll between origin and bottom
self.grid.scroll_down(&region, lines);
ScrollDelta::Down { lines }
}
/// Scroll screen up
///
/// Text moves up; clear at top
/// Expects origin to be in scroll range.
#[inline]
fn scroll_up_relative(&mut self, origin_row: VisibleRow, mut lines: usize) -> ScrollDelta {
log::trace!("Scrolling up relative: origin={origin_row}, lines={lines}");
lines = min(
lines,
self.ansi_handler_state.scroll_region.end - self.ansi_handler_state.scroll_region.start,
);
let region = origin_row..self.ansi_handler_state.scroll_region.end;
self.scroll_region_up(region, lines);
ScrollDelta::Up { lines }
}
fn scroll_region_up(&mut self, region: Range<VisibleRow>, lines: usize) {
// Move lines into scrollback. We don't do this for the alt screen,
// as it has no scrollback.
if !self.ansi_handler_state.is_alt_screen {
let range = region.start..std::cmp::min(region.end, region.start + lines);
for row_idx in range.start.0..range.end.0 {
self.flat_storage
.push_rows([&self.grid[VisibleRow(row_idx)]]);
}
}
// Scroll from origin to bottom less number of lines.
self.grid.scroll_up(&region, lines);
// The cursor point implicitly grows when a line is added into scrollback,
// so we need to make sure the dirty cells range accounts for this.
self.update_dirty_cells_range();
}
fn deccolm(&mut self) {
// Setting 132 column font makes no sense, but run the other side effects.
// Clear scrolling region.
ansi::Handler::set_scrolling_region(self, 1, None);
// Clear grid.
let bg = self.grid.cursor().template.bg;
self.grid.region_mut(..).each(|cell| *cell = bg.into());
}
fn clear_viewport(&mut self) {
// Determine how many lines to scroll up by.
let end = Point {
row: 0,
col: self.columns(),
};
let mut cursor = self.grapheme_cursor_from(end, grapheme_cursor::Wrap::All);
cursor.move_backward();
while let Some(cursor_item) = cursor.current_item() {
if !cursor_item.cell().is_empty() || cursor_item.point().row >= self.visible_rows() {
break;
}
cursor.move_backward();
}
let row = cursor.last_valid_position().row;
debug_assert!(row <= self.visible_rows());
let positions = self.visible_rows() - row;
let region = VisibleRow(0)..VisibleRow(self.visible_rows());
self.scroll_region_up(region, positions);
// Reset rotated lines.
let template = self.grid.cursor().template.clone();
for i in positions..self.visible_rows() {
self.grid[i].reset(&template);
}
}
pub(in crate::terminal::model) fn disable_reset_grid_checks(&mut self) {
self.ansi_handler_state.reset_grid_checks = ResetGridChecks::Disabled;
}
/// Marks the grid as having NOT received the Reset Grid OSC.
/// This is useful for grids that expect to receive multiple OSCs.
pub(in crate::terminal::model) fn reset_received_osc(&mut self) {
if let ResetGridChecks::Enabled { received_osc } =
&mut self.ansi_handler_state.reset_grid_checks
{
*received_osc = false;
}
}
fn handle_completed_kitty_action_internal(
&mut self,
action: KittyAction,
metadata: &mut HashMap<u32, StoredImageMetadata>,
) -> Result<(), KittyError> {
if !FeatureFlag::KittyImages.is_enabled() {
return Err(KittyError::KittyFeatureDisabled);
}
match action {
KittyAction::StoreOnly(action) => {
let metadata = match metadata.get(&action.image_id) {
Some(StoredImageMetadata::Kitty(metadata)) => metadata,
Some(_) | None => {
return Err(StorageError::UnknownId {
id: action.image_id,
}
.into())
}
};
if metadata.image_size.x() == 0.0 || metadata.image_size.y() == 0.0 {
return Ok(());
}
self.ansi_handler_state
.event_proxy
.send_terminal_event(Event::ImageReceived {
image_id: action.image_id,
image_data: action.image.data,
image_protocol: ImageProtocol::Kitty,
});
}
KittyAction::StoreAndDisplay(action) => {
let metadata = match metadata.get(&action.image_id) {
Some(StoredImageMetadata::Kitty(metadata)) => metadata,
Some(_) | None => {
return Err(StorageError::UnknownId {
id: action.image_id,
}
.into())
}
};
if metadata.image_size.x() == 0.0 || metadata.image_size.y() == 0.0 {
return Ok(());
}
if let Some(0) = action.placement_data.cols {
return Ok(());
}
if let Some(0) = action.placement_data.rows {
return Ok(());
}
let max_width =
(self.columns() - self.cursor_point().col) * self.ansi_handler_state.cell_width;
let max_height =
MAX_IMAGE_CELL_HEIGHT as usize * self.ansi_handler_state.cell_height;
let desired_dimensions = action.placement_data.get_desired_dimensions(
metadata.image_size,
self.ansi_handler_state.cell_height,
self.ansi_handler_state.cell_width,
max_width,
max_height,
);
let (width_px, height_px) = resize_dimensions(
metadata.image_size.x() as u32,
metadata.image_size.y() as u32,
desired_dimensions.x() as u32,
desired_dimensions.y() as u32,
FitType::Stretch,
);
let image_size = Vector2F::new(width_px as f32, height_px as f32);
// Convert the dimension in pixels to cells. We want to round up if this doesn't perfectly fit within an amount of cells.
let height_cells = (height_px as f32 / (self.ansi_handler_state.cell_height as f32))
.ceil() as usize;
let width_cells =
(width_px as f32 / (self.ansi_handler_state.cell_width as f32)).ceil() as usize;
self.ansi_handler_state
.event_proxy
.send_terminal_event(Event::ImageReceived {
image_id: action.image_id,
image_data: action.image.data,
image_protocol: ImageProtocol::Kitty,
});
self.images.add_image_placement_data(
action.image_id,
action.placement_id,
ImagePlacementData {
z_index: action.placement_data.z_index,
height_cells,
image_size,
},
);
self.images.place(
action.image_id,
action.placement_id,
AbsolutePoint::from_point(self.cursor_point(), self),
ImageType::Kitty,
self.num_lines_truncated(),
);
if !matches!(
action.placement_data.cursor_movement_policy,
CursorMovementPolicy::MoveCursor
) {
return Ok(());
}
// Create the whitespace to fit the image on.
for _ in 0..height_cells - 1 {
self.newline();
}
let num_cols = self.columns();
// Move the cursor to the same row but after the image.
self.move_cursor_forward(|cursor| {
if cursor.point.col + width_cells < num_cols {
cursor.point.col += width_cells;
} else {
cursor.input_needs_wrap = true;
}
});
}
KittyAction::DisplayStoredImage(action) => {
let metadata = match metadata.get(&action.image_id) {
Some(StoredImageMetadata::Kitty(metadata)) => metadata,
Some(_) | None => {
return Err(StorageError::UnknownId {
id: action.image_id,
}
.into())
}
};
if let Some(0) = action.placement_data.cols {
return Ok(());
}
if let Some(0) = action.placement_data.rows {
return Ok(());
}
let max_width =
(self.columns() - self.cursor_point().col) * self.ansi_handler_state.cell_width;
let max_height =
MAX_IMAGE_CELL_HEIGHT as usize * self.ansi_handler_state.cell_height;
let desired_dimensions = action.placement_data.get_desired_dimensions(
metadata.image_size,
self.ansi_handler_state.cell_height,
self.ansi_handler_state.cell_width,
max_width,
max_height,
);
let (width_px, height_px) = resize_dimensions(
metadata.image_size.x() as u32,
metadata.image_size.y() as u32,
desired_dimensions.x() as u32,
desired_dimensions.y() as u32,
FitType::Stretch,
);
let image_size = Vector2F::new(width_px as f32, height_px as f32);
// Convert the dimension in pixels to cells. We want to round up if this doesn't perfectly fit within an amount of cells.
let height_cells = (height_px as f32 / (self.ansi_handler_state.cell_height as f32))
.ceil() as usize;
let width_cells =
(width_px as f32 / (self.ansi_handler_state.cell_width as f32)).ceil() as usize;
self.images.add_image_placement_data(
action.image_id,
action.placement_id,
ImagePlacementData {
z_index: action.placement_data.z_index,
height_cells,
image_size,
},
);
self.images.place(
action.image_id,
action.placement_id,
AbsolutePoint::from_point(self.cursor_point(), self),
ImageType::Kitty,
self.num_lines_truncated(),
);
if !matches!(
action.placement_data.cursor_movement_policy,
CursorMovementPolicy::MoveCursor
) {
return Ok(());
}
// Create the whitespace to fit the image on.
for _ in 0..height_cells - 1 {
self.newline();
}
let num_cols = self.columns();
// Move the cursor to the same row but after the image.
self.move_cursor_forward(|cursor| {
if cursor.point.col + width_cells < num_cols {
cursor.point.col += width_cells;
} else {
cursor.input_needs_wrap = true;
}
});
}
KittyAction::QuerySupport(_) => {}
KittyAction::Delete { .. } => {}
}
Ok(())
}
}