pub mod attr;
pub mod grid;
pub mod pos;
pub mod search;
pub mod square;
pub mod style;
pub mod vi_mode;
use crate::ansi::graphics::GraphicCell;
use crate::ansi::graphics::Graphics;
use crate::ansi::graphics::KittyPlacement;
use crate::ansi::graphics::TextureRef;
use crate::ansi::graphics::UpdateQueues;
use crate::ansi::mode::NamedMode;
use crate::ansi::mode::NamedPrivateMode;
use crate::ansi::mode::PrivateMode;
use crate::ansi::sixel;
use crate::ansi::{
mode::Mode as AnsiMode, ClearMode, CursorShape, KeyboardModes,
KeyboardModesApplyBehavior, LineClearMode, TabulationClearMode,
};
use crate::clipboard::ClipboardType;
use crate::config::colors::{self, ColorRgb};
use crate::crosswords::colors::term::TermColors;
use crate::crosswords::grid::{Dimensions, Grid, Scroll};
use crate::crosswords::square::{CellFlags, Wide};
use crate::event::WindowId;
use crate::event::{EventListener, RioEvent, TerminalDamage};
use crate::performer::handler::Handler;
use crate::selection::{Selection, SelectionRange, SelectionType};
use crate::simd_utf8;
use attr::*;
use base64::{engine::general_purpose, Engine as _};
use bitflags::bitflags;
use copa::Params;
use grid::row::Row;
use pos::{
Boundary, CharsetIndex, Column, Cursor, CursorState, Direction, Line, Pos, Side,
};
use square::{Hyperlink, LineLength, Square};
use std::collections::BTreeSet;
use std::mem;
use std::ops::{Index, IndexMut, Range};
use std::option::Option;
use std::ptr;
use std::sync::Arc;
use sugarloaf::{GraphicData, MAX_GRAPHIC_DIMENSIONS};
use tracing::{debug, info, trace, warn};
use unicode_width::UnicodeWidthChar;
use vi_mode::{ViModeCursor, ViMotion};
pub type NamedColor = colors::NamedColor;
pub const MIN_COLUMNS: usize = 2;
pub const MIN_LINES: usize = 1;
bitflags! {
#[derive(Debug, Copy, Clone)]
pub struct Mode: u32 {
const NONE = 0;
const SHOW_CURSOR = 1;
const APP_CURSOR = 1 << 1;
const APP_KEYPAD = 1 << 2;
const MOUSE_REPORT_CLICK = 1 << 3;
const BRACKETED_PASTE = 1 << 4;
const SGR_MOUSE = 1 << 5;
const MOUSE_MOTION = 1 << 6;
const LINE_WRAP = 1 << 7;
const LINE_FEED_NEW_LINE = 1 << 8;
const ORIGIN = 1 << 9;
const INSERT = 1 << 10;
const FOCUS_IN_OUT = 1 << 11;
const ALT_SCREEN = 1 << 12;
const MOUSE_DRAG = 1 << 13;
const UTF8_MOUSE = 1 << 14;
const ALTERNATE_SCROLL = 1 << 15;
const VI = 1 << 16;
const URGENCY_HINTS = 1 << 17;
const DISAMBIGUATE_ESC_CODES = 1 << 18;
const REPORT_EVENT_TYPES = 1 << 19;
const REPORT_ALTERNATE_KEYS = 1 << 20;
const REPORT_ALL_KEYS_AS_ESC = 1 << 21;
const REPORT_ASSOCIATED_TEXT = 1 << 22;
const MOUSE_MODE = Self::MOUSE_REPORT_CLICK.bits() | Self::MOUSE_MOTION.bits() | Self::MOUSE_DRAG.bits();
const KITTY_KEYBOARD_PROTOCOL = Self::DISAMBIGUATE_ESC_CODES.bits()
| Self::REPORT_EVENT_TYPES.bits()
| Self::REPORT_ALTERNATE_KEYS.bits()
| Self::REPORT_ALL_KEYS_AS_ESC.bits()
| Self::REPORT_ASSOCIATED_TEXT.bits();
const ANY = u32::MAX;
const SIXEL_DISPLAY = 1 << 28;
const SIXEL_PRIV_PALETTE = 1 << 29;
const SIXEL_CURSOR_TO_THE_RIGHT = 1 << 31;
}
}
#[repr(u8)]
#[derive(Debug, Clone, Copy)]
enum ModeState {
NotSupported = 0,
Set = 1,
Reset = 2,
}
impl From<bool> for ModeState {
fn from(value: bool) -> Self {
if value {
Self::Set
} else {
Self::Reset
}
}
}
impl Default for Mode {
fn default() -> Mode {
Mode::SHOW_CURSOR
| Mode::LINE_WRAP
| Mode::ALTERNATE_SCROLL
| Mode::URGENCY_HINTS
| Mode::SIXEL_PRIV_PALETTE
}
}
impl From<KeyboardModes> for Mode {
fn from(value: KeyboardModes) -> Self {
let mut mode = Self::empty();
mode.set(
Mode::DISAMBIGUATE_ESC_CODES,
value.contains(KeyboardModes::DISAMBIGUATE_ESC_CODES),
);
mode.set(
Mode::REPORT_EVENT_TYPES,
value.contains(KeyboardModes::REPORT_EVENT_TYPES),
);
mode.set(
Mode::REPORT_ALTERNATE_KEYS,
value.contains(KeyboardModes::REPORT_ALTERNATE_KEYS),
);
mode.set(
Mode::REPORT_ALL_KEYS_AS_ESC,
value.contains(KeyboardModes::REPORT_ALL_KEYS_AS_ESC),
);
mode.set(
Mode::REPORT_ASSOCIATED_TEXT,
value.contains(KeyboardModes::REPORT_ASSOCIATED_TEXT),
);
mode
}
}
#[derive(Debug)]
pub enum TermDamage<'a> {
Full,
Partial(TermDamageIterator<'a>),
}
#[derive(Clone, Debug)]
pub struct TermDamageIterator<'a> {
line_damage: std::slice::Iter<'a, LineDamage>,
display_offset: usize,
}
impl<'a> TermDamageIterator<'a> {
pub fn new(line_damage: &'a [LineDamage], display_offset: usize) -> Self {
let num_lines = line_damage.len();
let line_damage = &line_damage[..num_lines.saturating_sub(display_offset)];
Self {
display_offset,
line_damage: line_damage.iter(),
}
}
}
impl Iterator for TermDamageIterator<'_> {
type Item = LineDamage;
fn next(&mut self) -> Option<Self::Item> {
self.line_damage.find_map(|line| {
line.is_damaged()
.then_some(LineDamage::new(line.line + self.display_offset, true))
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct LineDamage {
pub line: usize,
pub damaged: bool,
}
impl LineDamage {
#[inline]
pub fn new(line: usize, damaged: bool) -> Self {
Self { line, damaged }
}
#[inline]
pub fn undamaged(line: usize) -> Self {
Self {
line,
damaged: false,
}
}
#[inline]
pub fn reset(&mut self) {
self.damaged = false;
}
#[inline]
pub fn is_damaged(&self) -> bool {
self.damaged
}
#[inline]
pub fn mark_damaged(&mut self) {
self.damaged = true;
}
}
#[derive(Debug, Clone)]
struct TermDamageState {
full: bool,
lines: Vec<LineDamage>,
last_cursor: Pos,
last_vi_cursor_point: Option<Pos>,
last_selection: Option<SelectionRange>,
}
impl TermDamageState {
fn new(num_lines: usize) -> Self {
let lines = (0..num_lines).map(LineDamage::undamaged).collect();
Self {
full: true,
lines,
last_cursor: Default::default(),
last_vi_cursor_point: Default::default(),
last_selection: Default::default(),
}
}
#[inline]
fn resize(&mut self, num_lines: usize) {
self.last_cursor = Default::default();
self.last_vi_cursor_point = None;
self.last_selection = None;
self.full = true;
self.lines.clear();
self.lines.reserve(num_lines);
for line in 0..num_lines {
self.lines.push(LineDamage::undamaged(line));
}
}
#[inline]
fn damage_line(&mut self, line: usize) {
self.lines[line].mark_damaged();
}
fn damage_selection(&mut self, selection: SelectionRange, display_offset: usize) {
let display_offset = display_offset as i32;
let last_visible_line = self.lines.len() as i32 - 1;
if selection.end.row.0 + display_offset < 0
|| selection.start.row.0.abs() < display_offset - last_visible_line
{
return;
};
let start = std::cmp::max(selection.start.row.0 + display_offset, 0);
let end = (selection.end.row.0 + display_offset).clamp(0, last_visible_line);
for line in start as usize..=end as usize {
self.damage_line(line);
}
}
fn reset(&mut self) {
self.full = false;
self.lines.iter_mut().for_each(|line| line.reset());
}
}
#[derive(Debug, Clone)]
struct TabStops {
tabs: Vec<bool>,
}
const INITIAL_TABSTOPS: usize = 8;
impl TabStops {
#[inline]
fn new(columns: usize) -> TabStops {
TabStops {
tabs: (0..columns).map(|i| i % INITIAL_TABSTOPS == 0).collect(),
}
}
#[inline]
fn clear_all(&mut self) {
unsafe {
ptr::write_bytes(self.tabs.as_mut_ptr(), 0, self.tabs.len());
}
}
#[inline]
fn resize(&mut self, columns: usize) {
let mut index = self.tabs.len();
self.tabs.resize_with(columns, || {
let is_tabstop = index.is_multiple_of(INITIAL_TABSTOPS);
index += 1;
is_tabstop
});
}
}
impl Index<Column> for TabStops {
type Output = bool;
fn index(&self, index: Column) -> &bool {
&self.tabs[index.0]
}
}
impl IndexMut<Column> for TabStops {
fn index_mut(&mut self, index: Column) -> &mut bool {
self.tabs.index_mut(index.0)
}
}
fn version_number(mut version: &str) -> usize {
if let Some(separator) = version.rfind('-') {
version = &version[..separator];
}
let mut version_number = 0;
let semver_versions = version.split('.');
for (i, semver_version) in semver_versions.rev().enumerate() {
let semver_number = semver_version.parse::<usize>().unwrap_or(0);
version_number += usize::pow(100, i as u32) * semver_number;
}
version_number
}
fn vs_is_valid_base(base: char, vs: char) -> bool {
use rio_grapheme_width::emoji::Presentation;
let mut buf = [0u8; 8];
let n1 = base.encode_utf8(&mut buf).len();
let n2 = vs.encode_utf8(&mut buf[n1..]).len();
let s = unsafe { core::str::from_utf8_unchecked(&buf[..n1 + n2]) };
Presentation::for_grapheme(s).1.is_some()
}
const TITLE_STACK_MAX_DEPTH: usize = 4096;
const KEYBOARD_MODE_STACK_MAX_DEPTH: usize = 8;
#[derive(Debug)]
pub struct Crosswords<U>
where
U: EventListener,
{
active_charset: CharsetIndex,
mode: Mode,
pub vi_mode_cursor: ViModeCursor,
semantic_escape_chars: String,
pub grid: Grid<Square>,
inactive_grid: Grid<Square>,
scroll_region: Range<Line>,
tabs: TabStops,
event_proxy: U,
pub selection: Option<Selection>,
pub colors: TermColors,
pub title: String,
damage: TermDamageState,
pub graphics: Graphics,
pub cursor_shape: CursorShape,
pub default_cursor_shape: CursorShape,
pub blinking_cursor: bool,
pub window_id: WindowId,
pub route_id: usize,
title_stack: Vec<String>,
pub current_directory: Option<std::path::PathBuf>,
pub damage_event_in_flight: bool,
keyboard_mode_stack: [u8; KEYBOARD_MODE_STACK_MAX_DEPTH],
keyboard_mode_idx: usize,
inactive_keyboard_mode_stack: [u8; KEYBOARD_MODE_STACK_MAX_DEPTH],
inactive_keyboard_mode_idx: usize,
}
impl<U: EventListener> Crosswords<U> {
pub fn new<D: Dimensions>(
dimensions: D,
cursor_shape: CursorShape,
event_proxy: U,
window_id: WindowId,
route_id: usize,
scrollback_history_limit: usize,
) -> Crosswords<U> {
let cols = dimensions.columns();
let rows = dimensions.screen_lines();
let grid = Grid::new(rows, cols, scrollback_history_limit);
let alt = Grid::new(rows, cols, 0);
let scroll_region = Line(0)..Line(rows as i32);
let semantic_escape_chars = String::from(",│`|:\"' ()[]{}<>\t\0");
let term_colors = TermColors::default();
let _url_regex: &str = "(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file:|git://|ssh:|ftp://)\
[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`\\\\]+";
Crosswords {
vi_mode_cursor: ViModeCursor::new(grid.cursor.pos),
semantic_escape_chars,
selection: None,
grid,
inactive_grid: alt,
active_charset: CharsetIndex::default(),
scroll_region,
event_proxy,
colors: term_colors,
title: String::from(""),
tabs: TabStops::new(cols),
mode: Mode::SHOW_CURSOR
| Mode::LINE_WRAP
| Mode::ALTERNATE_SCROLL
| Mode::URGENCY_HINTS,
damage: TermDamageState::new(rows),
graphics: Graphics::new(&dimensions),
default_cursor_shape: cursor_shape,
cursor_shape,
blinking_cursor: false,
window_id,
route_id,
title_stack: Default::default(),
current_directory: None,
damage_event_in_flight: false,
keyboard_mode_stack: Default::default(),
keyboard_mode_idx: 0,
inactive_keyboard_mode_stack: Default::default(),
inactive_keyboard_mode_idx: 0,
}
}
pub fn mark_fully_damaged(&mut self) {
let was_damaged = self.damage.full;
self.damage.full = true;
if !was_damaged {
self.event_proxy
.send_event(RioEvent::RenderRoute(self.route_id), self.window_id);
}
}
#[inline]
pub fn is_fully_damaged(&self) -> bool {
self.damage.full
}
pub fn update_selection_damage(
&mut self,
new_selection: Option<SelectionRange>,
display_offset: usize,
) {
if let Some(old_selection) = self.damage.last_selection {
self.damage.damage_selection(old_selection, display_offset);
}
if let Some(new_selection) = new_selection {
self.damage.damage_selection(new_selection, display_offset);
}
self.damage.last_selection = new_selection;
}
#[must_use]
pub fn damage(&mut self) -> TermDamage<'_> {
if self.mode.contains(Mode::INSERT) {
self.mark_fully_damaged();
}
let previous_cursor =
mem::replace(&mut self.damage.last_cursor, self.grid.cursor.pos);
if self.damage.full {
return TermDamage::Full;
}
if self.damage.last_cursor != previous_cursor {
let previous_line = previous_cursor.row.0 as usize;
self.damage.damage_line(previous_line);
}
let display_offset = self.grid.display_offset();
TermDamage::Partial(TermDamageIterator::new(&self.damage.lines, display_offset))
}
pub fn peek_damage_event(&self) -> Option<TerminalDamage> {
let display_offset = self.grid.display_offset();
if self.damage.full {
Some(TerminalDamage::Full)
} else {
let damaged_lines: BTreeSet<LineDamage> = self
.damage
.lines
.iter()
.filter(|line| line.is_damaged())
.map(|line| LineDamage::new(line.line + display_offset, true))
.collect();
if damaged_lines.is_empty() {
if self.damage.last_cursor != self.grid.cursor.pos {
Some(TerminalDamage::CursorOnly)
} else {
None }
} else {
Some(TerminalDamage::Partial(damaged_lines))
}
}
}
#[inline]
pub fn reset_damage(&mut self) {
self.damage.reset();
}
#[inline]
pub fn display_offset(&self) -> usize {
self.grid.display_offset()
}
#[inline]
pub fn clear_saved_history(&mut self) {
self.clear_screen(ClearMode::Saved);
}
#[inline]
pub fn scroll_display(&mut self, scroll: Scroll) {
let old_display_offset = self.grid.display_offset();
self.grid.scroll_display(scroll);
self.event_proxy
.send_event(RioEvent::MouseCursorDirty, self.window_id);
let viewport_start = -(self.grid.display_offset() as i32);
let viewport_end = viewport_start + self.grid.bottommost_line().0;
let vi_cursor_line = &mut self.vi_mode_cursor.pos.row.0;
*vi_cursor_line =
std::cmp::min(viewport_end, std::cmp::max(viewport_start, *vi_cursor_line));
self.vi_mode_recompute_selection();
if old_display_offset != self.grid.display_offset() {
self.mark_fully_damaged();
if !self.graphics.kitty_placements.is_empty() {
self.graphics.kitty_graphics_dirty = true;
}
}
}
#[inline]
pub fn bottommost_line(&self) -> Line {
self.grid.bottommost_line()
}
#[inline]
pub fn colors(&self) -> &TermColors {
&self.colors
}
#[inline]
pub fn graphics_take_queues(&mut self) -> Option<UpdateQueues> {
self.graphics.take_queues()
}
#[inline]
pub fn send_graphics_updates(&mut self) {
if self.graphics.has_pending_updates() {
if let Some(queues) = self.graphics.take_queues() {
self.event_proxy.send_event(
RioEvent::UpdateGraphics {
route_id: self.route_id,
queues,
},
self.window_id,
);
}
}
}
#[inline]
pub fn exit(&mut self)
where
U: EventListener,
{
self.event_proxy
.send_event(RioEvent::CloseTerminal(self.route_id), self.window_id);
}
pub fn resize<S: Dimensions>(&mut self, size: S) {
let old_cols = self.grid.columns();
let old_lines = self.grid.screen_lines();
let num_cols = size.columns();
let num_lines = size.screen_lines();
if old_cols == num_cols && old_lines == num_lines {
info!("Crosswords::resize dimensions unchanged");
return;
}
let history_size = self.history_size();
let mut delta = num_lines as i32 - old_lines as i32;
let min_delta =
std::cmp::min(0, num_lines as i32 - self.grid.cursor.pos.row.0 - 1);
delta = std::cmp::min(std::cmp::max(delta, min_delta), history_size as i32);
self.vi_mode_cursor.pos.row += delta;
let pre_resize_cursor_abs =
history_size as i64 + self.grid.cursor.pos.row.0 as i64;
let is_alt = self.mode.contains(Mode::ALT_SCREEN);
self.grid.resize(!is_alt, num_lines, num_cols);
self.inactive_grid.resize(is_alt, num_lines, num_cols);
if old_cols != num_cols {
self.selection = None;
self.tabs.resize(num_cols);
} else if let Some(selection) = self.selection.take() {
let max_lines = std::cmp::max(num_lines, old_lines) as i32;
let range = Line(0)..Line(max_lines);
self.selection = selection.rotate(&self.grid, &range, -delta);
}
let vi_pos = self.vi_mode_cursor;
let viewport_top = Line(-(self.grid.display_offset() as i32));
let viewport_bottom = viewport_top + self.bottommost_line();
self.vi_mode_cursor.pos.row =
std::cmp::max(std::cmp::min(vi_pos.pos.row, viewport_bottom), viewport_top);
self.vi_mode_cursor.pos.col =
std::cmp::min(vi_pos.pos.col, self.grid.last_column());
self.scroll_region = Line(0)..Line(self.grid.screen_lines() as i32);
self.damage.resize(num_lines);
self.graphics.resize(&size);
let post_resize_cursor_abs =
self.history_size() as i64 + self.grid.cursor.pos.row.0 as i64;
let dest_row_shift = post_resize_cursor_abs - pre_resize_cursor_abs;
let cell_w = self.graphics.cell_width as usize;
let cell_h = self.graphics.cell_height as usize;
let mut overlay_changed = false;
if cell_w > 0 && cell_h > 0 {
for p in self.graphics.kitty_placements.values_mut() {
if p.columns > 0 {
p.pixel_width = (p.columns as usize * cell_w) as u32;
}
if p.rows > 0 {
p.pixel_height = (p.rows as usize * cell_h) as u32;
}
if dest_row_shift != 0 {
p.dest_row += dest_row_shift;
}
}
for p in self
.graphics
.kitty_inactive_screen
.kitty_placements
.values_mut()
{
if p.columns > 0 {
p.pixel_width = (p.columns as usize * cell_w) as u32;
}
if p.rows > 0 {
p.pixel_height = (p.rows as usize * cell_h) as u32;
}
if dest_row_shift != 0 {
p.dest_row += dest_row_shift;
}
}
overlay_changed = !self.graphics.kitty_placements.is_empty()
|| !self
.graphics
.kitty_inactive_screen
.kitty_placements
.is_empty();
}
if overlay_changed {
self.graphics.kitty_graphics_dirty = true;
}
}
#[inline]
pub fn toggle_vi_mode(&mut self)
where
U: EventListener,
{
self.mode ^= Mode::VI;
if self.mode.contains(Mode::VI) {
let display_offset = self.grid.display_offset() as i32;
if self.grid.cursor.pos.row > self.grid.bottommost_line() - display_offset {
let pos = Pos::new(Line(-display_offset), Column(0));
self.vi_mode_cursor.pos = pos;
} else {
self.vi_mode_cursor.pos = self.grid.cursor.pos;
}
}
self.event_proxy
.send_event(RioEvent::CursorBlinkingChange, self.window_id);
}
#[inline]
fn vi_mode_recompute_selection(&mut self) {
if !self.mode.contains(Mode::VI) {
return;
}
if let Some(selection) = self.selection.as_mut().filter(|s| !s.is_empty()) {
selection.update(self.vi_mode_cursor.pos, Side::Left);
selection.include_all();
}
}
#[inline]
pub fn vi_motion(&mut self, motion: ViMotion)
where
U: EventListener,
{
if !self.mode.contains(Mode::VI) {
return;
}
self.vi_mode_cursor = self.vi_mode_cursor.motion(self, motion);
self.vi_mode_recompute_selection();
}
#[inline]
pub fn vi_goto_pos(&mut self, pos: Pos)
where
U: EventListener,
{
self.scroll_to_pos(pos);
self.vi_mode_cursor.pos = pos;
self.vi_mode_recompute_selection();
}
#[inline]
pub fn scroll_to_pos(&mut self, pos: Pos)
where
U: EventListener,
{
let display_offset = self.grid.display_offset() as i32;
let screen_lines = self.grid.screen_lines() as i32;
if pos.row < -display_offset {
let lines = pos.row + display_offset;
self.scroll_display(Scroll::Delta(-lines.0));
} else if pos.row >= (screen_lines - display_offset) {
let lines = pos.row + display_offset - screen_lines + 1i32;
self.scroll_display(Scroll::Delta(-lines.0));
}
}
pub fn expand_wide(&self, mut pos: Pos, direction: Direction) -> Pos {
use crate::crosswords::square::Wide;
let wide = self.grid[pos.row][pos.col].wide();
match direction {
Direction::Right if matches!(wide, Wide::LeadingSpacer) => {
pos.col = Column(1);
pos.row += 1;
}
Direction::Right if matches!(wide, Wide::Wide) => {
pos.col = std::cmp::min(pos.col + 1, self.grid.last_column());
}
Direction::Left if matches!(wide, Wide::Wide | Wide::Spacer) => {
if matches!(wide, Wide::Spacer) {
pos.col -= 1;
}
let prev = pos.sub(&self.grid, Boundary::Grid, 1);
if matches!(self.grid[prev].wide(), Wide::LeadingSpacer) {
pos = prev;
}
}
_ => (),
}
pos
}
#[inline]
pub fn semantic_escape_chars(&self) -> &str {
&self.semantic_escape_chars
}
#[inline]
pub fn wrapline(&mut self) {
if !self.mode.contains(Mode::LINE_WRAP) {
return;
}
self.grid.cursor_cell().set_wrapline(true);
if self.grid.cursor.pos.row + 1 >= self.scroll_region.end {
self.linefeed();
} else {
self.damage_cursor();
self.grid.cursor.pos.row += 1;
}
self.grid.cursor.pos.col = Column(0);
self.grid.cursor.should_wrap = false;
self.damage_cursor();
}
pub fn history_size(&self) -> usize {
self.grid
.total_lines()
.saturating_sub(self.grid.screen_lines())
}
#[inline]
pub fn damage_cursor_line(&mut self) {
let cursor_line = self.grid.cursor.pos.row.0 as usize;
self.damage_line(cursor_line);
}
#[inline]
pub fn damage_line(&mut self, line: usize) {
self.damage.damage_line(line);
}
#[inline]
pub fn damage_cursor(&mut self) {
self.damage_cursor_line();
}
#[inline]
pub fn damage_cursor_blink(&mut self) {
let cursor_state = self.cursor();
if cursor_state.is_visible() {
self.damage_cursor_line();
}
}
#[inline]
pub fn needs_render(&self) -> bool {
if self.is_fully_damaged() {
return true;
}
if self.damage.lines.iter().any(|line| line.is_damaged()) {
return true;
}
false
}
#[inline]
fn scroll_down_relative(&mut self, origin: Line, mut lines: usize) {
debug!(
"Scrolling down relative: origin={}, lines={}",
origin, lines
);
lines = std::cmp::min(
lines,
(self.scroll_region.end - self.scroll_region.start).0 as usize,
);
lines = std::cmp::min(lines, (self.scroll_region.end - origin).0 as usize);
let region = origin..self.scroll_region.end;
self.selection = self
.selection
.take()
.and_then(|s| s.rotate(&self.grid, ®ion, -(lines as i32)));
let line = &mut self.vi_mode_cursor.pos.row;
if region.start <= *line && region.end > *line {
*line = std::cmp::min(*line + lines, region.end - 1);
}
self.grid.scroll_down(®ion, lines);
self.mark_fully_damaged();
if !self.graphics.kitty_placements.is_empty() {
self.graphics.kitty_graphics_dirty = true;
}
}
#[inline]
pub fn scroll_up_relative(&mut self, origin: Line, mut lines: usize) {
debug!("Scrolling up: origin={origin}, lines={lines}");
lines = std::cmp::min(
lines,
(self.scroll_region.end - self.scroll_region.start).0 as usize,
);
let region = origin..self.scroll_region.end;
self.selection = self
.selection
.take()
.and_then(|s| s.rotate(&self.grid, ®ion, lines as i32));
self.grid.scroll_up(®ion, lines);
let viewport_top = Line(-(self.grid.display_offset() as i32));
let top = if region.start == 0 {
viewport_top
} else {
region.start
};
let line = &mut self.vi_mode_cursor.pos.row;
if (top <= *line) && region.end > *line {
*line = std::cmp::max(*line - lines, top);
}
for line in region.start.0..region.end.0 {
self.damage.damage_line(line as usize);
}
if !self.graphics.kitty_placements.is_empty() {
self.graphics.kitty_graphics_dirty = true;
}
}
#[inline(always)]
pub fn write_at_cursor(&mut self, c: char) {
let c = self.grid.cursor.charsets[self.active_charset].map(c);
let style_id = self.grid.cursor.template.style_id();
let template_extras_id = self.grid.cursor.template.extras_id();
let template_flags = self.grid.cursor.template.cell_flags();
let cursor_square = self.grid.cursor_square();
if matches!(
cursor_square.wide(),
crate::crosswords::square::Wide::Wide
| crate::crosswords::square::Wide::Spacer
) {
let wide =
matches!(cursor_square.wide(), crate::crosswords::square::Wide::Wide);
let point = self.grid.cursor.pos;
if wide && point.col < self.grid.last_column() {
self.grid[point.row][point.col + 1]
.set_wide(crate::crosswords::square::Wide::Narrow);
} else if point.col > 0 {
self.grid[point.row][point.col - 1].clear();
}
if point.col <= 1 && point.row != self.grid.topmost_line() {
let column = self.grid.last_column();
let prev = &mut self.grid[point.row - 1i32][column];
if matches!(prev.wide(), crate::crosswords::square::Wide::LeadingSpacer) {
prev.set_wide(crate::crosswords::square::Wide::Narrow);
}
}
}
let cursor_square = self.grid.cursor_cell();
let mut cell = crate::crosswords::square::Square::default();
cell.set_c(c);
cell.set_style_id(style_id);
cell.set_extras_id(template_extras_id);
cell.set_cell_flags(template_flags);
*cursor_square = cell;
}
#[inline(never)]
fn apply_emoji_vs16(&mut self) {
let columns = self.grid.columns();
let row = self.grid.cursor.pos.row;
let cursor_col = self.grid.cursor.pos.col.0;
let should_wrap = self.grid.cursor.should_wrap;
let base_col = if should_wrap {
cursor_col
} else if cursor_col == 0 {
return;
} else {
cursor_col - 1
};
let base_cell = &self.grid[row][Column(base_col)];
if !matches!(base_cell.wide(), Wide::Narrow) {
return;
}
let base_char = base_cell.c();
if !vs_is_valid_base(base_char, '\u{FE0F}') {
return;
}
let spacer_col = base_col + 1;
if spacer_col >= columns {
if !self.mode.contains(Mode::LINE_WRAP) {
return;
}
let base_snapshot = self.grid[row][Column(base_col)];
self.grid.cursor.pos.col = Column(base_col);
self.grid.cursor.should_wrap = false;
self.write_at_cursor(' ');
self.grid.cursor_cell().set_wide(Wide::LeadingSpacer);
self.wrapline();
let new_row = self.grid.cursor.pos.row;
let mut moved = base_snapshot;
moved.set_wide(Wide::Wide);
self.grid[new_row][Column(0)] = moved;
self.grid.cursor.pos.col = Column(1);
self.write_at_cursor(' ');
self.grid.cursor_cell().set_wide(Wide::Spacer);
if 2 < columns {
self.grid.cursor.pos.col = Column(2);
} else {
self.grid.cursor.should_wrap = true;
}
self.damage.damage_line(row.0 as usize);
self.damage.damage_line(new_row.0 as usize);
return;
}
self.grid[row][Column(base_col)].set_wide(Wide::Wide);
self.grid.cursor.pos.col = Column(spacer_col);
self.grid.cursor.should_wrap = false;
self.write_at_cursor(' ');
self.grid.cursor_cell().set_wide(Wide::Spacer);
if spacer_col + 1 < columns {
self.grid.cursor.pos.col = Column(spacer_col + 1);
} else {
self.grid.cursor.should_wrap = true;
}
self.damage.damage_line(row.0 as usize);
}
#[inline(never)]
fn apply_emoji_vs15(&mut self) {
let row = self.grid.cursor.pos.row;
let cursor_col = self.grid.cursor.pos.col.0;
let should_wrap = self.grid.cursor.should_wrap;
let (base_col, spacer_col) = if should_wrap {
if cursor_col == 0 {
return;
}
(cursor_col - 1, cursor_col)
} else {
if cursor_col < 2 {
return;
}
(cursor_col - 2, cursor_col - 1)
};
let base_cell = &self.grid[row][Column(base_col)];
if !matches!(base_cell.wide(), Wide::Wide) {
return;
}
let base_char = base_cell.c();
if !vs_is_valid_base(base_char, '\u{FE0E}') {
return;
}
self.grid[row][Column(base_col)].set_wide(Wide::Narrow);
self.grid[row][Column(spacer_col)] = Square::default();
self.grid.cursor.pos.col = Column(spacer_col);
self.grid.cursor.should_wrap = false;
self.damage.damage_line(row.0 as usize);
}
#[inline]
pub fn cell_hyperlink(&self, line: Line, col: Column) -> Option<Hyperlink> {
let cell = &self.grid[line][col];
if !cell.has_hyperlink() {
return None;
}
let extras_id = cell.extras_id()?;
self.grid
.extras_table
.get(extras_id)
.and_then(|e| e.hyperlink.clone())
}
#[inline]
pub fn cell_hyperlink_id(&self, line: Line, col: Column) -> Option<u16> {
let cell = &self.grid[line][col];
if !cell.has_hyperlink() {
return None;
}
cell.extras_id()
}
#[inline]
pub fn cell_graphic(&self, line: Line, col: Column) -> Option<&GraphicCell> {
let cell = &self.grid[line][col];
if !cell.has_graphics() {
return None;
}
let extras_id = cell.extras_id()?;
self.grid
.extras_table
.get(extras_id)
.and_then(|e| e.graphic.as_ref())
.and_then(|g| g.first())
}
#[inline]
pub fn visible_rows(&self) -> Vec<Row<Square>> {
let mut start = self.scroll_region.start.0;
let mut end = self.scroll_region.end.0;
let mut visible_rows = Vec::with_capacity(self.grid.screen_lines());
let scroll = self.display_offset() as i32;
if scroll != 0 {
start -= scroll;
end -= scroll;
}
for row in start..end {
visible_rows.push(self.grid[Line(row)].clone());
}
visible_rows
}
pub fn visible_rows_with_damage(
&self,
damage: &TerminalDamage,
) -> (Vec<Row<Square>>, Vec<usize>) {
let mut start = self.scroll_region.start.0;
let mut end = self.scroll_region.end.0;
let mut visible_rows = Vec::with_capacity(self.grid.screen_lines());
let mut damaged_lines = Vec::new();
let scroll = self.display_offset() as i32;
if scroll != 0 {
start -= scroll;
end -= scroll;
}
match damage {
TerminalDamage::Full => {
for (i, row) in (start..end).enumerate() {
visible_rows.push(self.grid[Line(row)].clone());
damaged_lines.push(i);
}
}
TerminalDamage::Partial(lines) => {
let damaged_set: std::collections::HashSet<usize> =
lines.iter().filter(|d| d.damaged).map(|d| d.line).collect();
for (i, row) in (start..end).enumerate() {
visible_rows.push(self.grid[Line(row)].clone());
if damaged_set.contains(&i) {
damaged_lines.push(i);
}
}
}
TerminalDamage::CursorOnly => {
let cursor_line = self.grid.cursor.pos.row.0 as usize;
for (i, row) in (start..end).enumerate() {
visible_rows.push(self.grid[Line(row)].clone());
if i == cursor_line {
damaged_lines.push(i);
}
}
}
TerminalDamage::Noop => {
}
}
(visible_rows, damaged_lines)
}
#[inline]
pub fn columns(&self) -> usize {
self.grid.columns()
}
#[inline]
pub fn screen_lines(&self) -> usize {
self.grid.screen_lines()
}
fn deccolm(&mut self)
where
U: EventListener,
{
self.set_scrolling_region(1, None);
self.grid.reset_region(..);
self.mark_fully_damaged();
}
pub fn mode(&self) -> Mode {
self.mode
}
#[inline]
pub fn cursor(&self) -> CursorState {
let mut content = self.cursor_shape;
let vi_mode = self.mode.contains(Mode::VI);
let scroll = self.display_offset() as i32;
let mut pos = if vi_mode {
let mut vi_cursor_pos = self.vi_mode_cursor.pos;
if scroll > 0 {
vi_cursor_pos.row += scroll;
}
vi_cursor_pos
} else {
if scroll != 0 {
content = CursorShape::Hidden;
}
self.grid.cursor.pos
};
if matches!(self.grid[pos].wide(), Wide::Spacer) {
pos.col -= 1;
}
if !vi_mode && !self.mode.contains(Mode::SHOW_CURSOR) {
content = CursorShape::Hidden;
}
CursorState { pos, content }
}
pub fn swap_alt(&mut self) {
if !self.mode.contains(Mode::ALT_SCREEN) {
self.inactive_grid.cursor = self.grid.cursor.clone();
self.grid.saved_cursor = self.grid.cursor.clone();
self.inactive_grid.reset_region(..);
}
mem::swap(
&mut self.keyboard_mode_stack,
&mut self.inactive_keyboard_mode_stack,
);
mem::swap(
&mut self.keyboard_mode_idx,
&mut self.inactive_keyboard_mode_idx,
);
mem::swap(&mut self.grid, &mut self.inactive_grid);
self.mode ^= Mode::ALT_SCREEN;
self.selection = None;
self.graphics.swap_kitty_screen_state();
self.mark_fully_damaged();
}
#[inline]
pub fn mark_line_damaged(&mut self, line: Line) {
let line_idx = line.0 as usize;
self.damage.damage_line(line_idx);
}
pub fn selection_to_string(&self) -> Option<String> {
let selection_range = self.selection.as_ref().and_then(|s| s.to_range(self))?;
let SelectionRange { start, end, .. } = selection_range;
let mut res = String::new();
match self.selection.as_ref() {
Some(Selection {
ty: SelectionType::Block,
..
}) => {
for line in (start.row.0..end.row.0).map(Line::from) {
res += self
.line_to_string(line, start.col..end.col, start.col.0 != 0)
.trim_end();
res += "\n";
}
res += self
.line_to_string(end.row, start.col..end.col, true)
.trim_end();
}
Some(Selection {
ty: SelectionType::Lines,
..
}) => {
res = self.bounds_to_string(start, end) + "\n";
}
_ => {
res = self.bounds_to_string(start, end);
}
}
Some(res)
}
pub fn bounds_to_string(&self, start: Pos, end: Pos) -> String {
let mut res = String::new();
for line in (start.row.0..=end.row.0).map(Line::from) {
let start_col = if line == start.row {
start.col
} else {
Column(0)
};
let end_col = if line == end.row {
end.col
} else {
self.grid.last_column()
};
res += &self.line_to_string(line, start_col..end_col, line == end.row);
}
res.strip_suffix('\n').map(str::to_owned).unwrap_or(res)
}
fn line_to_string(
&self,
line: Line,
mut cols: Range<Column>,
include_wrapped_wide: bool,
) -> String {
let mut text = String::new();
let grid_line = &self.grid[line];
let line_length = std::cmp::min(grid_line.line_length(), cols.end + 1);
if matches!(grid_line[cols.start].wide(), Wide::Spacer) {
cols.start -= 1;
}
let mut tab_mode = false;
for column in (cols.start.0..line_length.0).map(Column::from) {
let cell = &grid_line[column];
if tab_mode {
if self.tabs[column] || cell.c() != '\0' {
tab_mode = false;
} else {
continue;
}
}
if cell.c() == '\t' {
tab_mode = true;
}
if !matches!(cell.wide(), Wide::Spacer | Wide::LeadingSpacer) {
text.push(cell.c());
if let Some(extras_id) = cell.extras_id() {
if let Some(extras) = self.grid.extras_table.get(extras_id) {
for c in &extras.zerowidth {
text.push(*c);
}
}
}
}
}
if cols.end >= self.grid.columns() - 1
&& (line_length.0 == 0 || !self.grid[line][line_length - 1].wrapline())
{
text.push('\n');
}
if line_length == self.grid.columns()
&& line_length.0 >= 2
&& matches!(grid_line[line_length - 1].wide(), Wide::LeadingSpacer)
&& include_wrapped_wide
{
text.push(self.grid[line - 1i32][Column(0)].c());
}
text
}
#[inline]
fn set_keyboard_mode(&mut self, mode: u8, apply: KeyboardModesApplyBehavior) {
let active_mode = self.keyboard_mode_stack[self.keyboard_mode_idx];
let new_mode = match apply {
KeyboardModesApplyBehavior::Replace => mode,
KeyboardModesApplyBehavior::Union => active_mode | mode,
KeyboardModesApplyBehavior::Difference => active_mode & !mode,
};
info!("Setting keyboard mode to {new_mode:?}");
self.keyboard_mode_stack[self.keyboard_mode_idx] = new_mode;
self.mode &= !Mode::KITTY_KEYBOARD_PROTOCOL;
self.mode |= Mode::from(KeyboardModes::from_bits_truncate(new_mode));
}
pub fn row_search_left(&self, mut point: Pos) -> Pos {
while point.row > self.grid.topmost_line()
&& self.grid[point.row - 1i32][self.grid.last_column()].wrapline()
{
point.row -= 1;
}
point.col = Column(0);
point
}
pub fn row_search_right(&self, mut point: Pos) -> Pos {
while point.row + 1 < self.grid.screen_lines()
&& self.grid[point.row][self.grid.last_column()].wrapline()
{
point.row += 1;
}
point.col = self.grid.last_column();
point
}
}
impl<U: EventListener> Crosswords<U> {
#[inline]
fn clear_cell_graphic(extras_table: &mut grid::ExtrasTable, cell: &mut Square) {
if let Some(eid) = cell.extras_id() {
if let Some(extras) = extras_table.get_mut(eid) {
extras.graphic = None;
if extras.is_empty() {
extras_table.free(eid);
cell.set_extras_id(None);
}
}
}
cell.remove_cell_flag(CellFlags::GRAPHICS);
}
fn delete_all_graphics(&mut self) {
for line_idx in 0..self.grid.screen_lines() {
let line = Line(line_idx as i32);
for col_idx in 0..self.grid.columns() {
let cell = &mut self.grid.raw[line][Column(col_idx)];
if cell.has_graphics() {
Self::clear_cell_graphic(&mut self.grid.extras_table, cell);
}
}
self.mark_line_damaged(line);
}
}
fn delete_graphic_at_position(&mut self, col: Column, row: Line) {
if row.0 >= 0
&& (row.0 as usize) < self.grid.screen_lines()
&& col.0 < self.grid.columns()
{
let cell = &mut self.grid.raw[row][col];
if cell.has_graphics() {
Self::clear_cell_graphic(&mut self.grid.extras_table, cell);
self.mark_line_damaged(row);
}
}
}
fn delete_graphics_in_column(&mut self, col: Column) {
if col.0 < self.grid.columns() {
for line_idx in 0..self.grid.screen_lines() {
let line = Line(line_idx as i32);
let cell = &mut self.grid.raw[line][col];
if cell.has_graphics() {
Self::clear_cell_graphic(&mut self.grid.extras_table, cell);
self.mark_line_damaged(line);
}
}
}
}
fn delete_graphics_in_row(&mut self, row: Line) {
if row.0 >= 0 && (row.0 as usize) < self.grid.screen_lines() {
for col_idx in 0..self.grid.columns() {
let cell = &mut self.grid.raw[row][Column(col_idx)];
if cell.has_graphics() {
Self::clear_cell_graphic(&mut self.grid.extras_table, cell);
}
}
self.mark_line_damaged(row);
}
}
fn collect_used_graphic_ids(&mut self) -> std::collections::HashSet<u64> {
self.graphics.collect_active_graphic_ids()
}
fn cleanup_unused_kitty_images(&mut self) {
let used_ids = self.collect_used_graphic_ids();
let used_kitty_ids: std::collections::HashSet<u32> =
used_ids.iter().map(|&id| id as u32).collect();
self.graphics
.delete_kitty_images(|id, _| !used_kitty_ids.contains(id));
}
}
impl<U: EventListener> Handler for Crosswords<U> {
#[inline]
fn set_mode(&mut self, mode: AnsiMode) {
let mode = match mode {
AnsiMode::Named(mode) => mode,
AnsiMode::Unknown(mode) => {
debug!("Ignoring unknown mode {} in set_mode", mode);
return;
}
};
trace!("Setting public mode: {:?}", mode);
match mode {
NamedMode::Insert => self.mode.insert(Mode::INSERT),
NamedMode::LineFeedNewLine => self.mode.insert(Mode::LINE_FEED_NEW_LINE),
}
}
#[inline]
fn unset_mode(&mut self, mode: AnsiMode) {
let mode = match mode {
AnsiMode::Named(mode) => mode,
AnsiMode::Unknown(mode) => {
debug!("Ignoring unknown mode {} in unset_mode", mode);
return;
}
};
trace!("Setting public mode: {:?}", mode);
match mode {
NamedMode::Insert => {
self.mode.remove(Mode::INSERT);
self.mark_fully_damaged();
}
NamedMode::LineFeedNewLine => self.mode.remove(Mode::LINE_FEED_NEW_LINE),
}
}
#[inline]
fn report_mode(&mut self, mode: AnsiMode) {
trace!("Reporting mode {mode:?}");
let state = match mode {
AnsiMode::Named(mode) => match mode {
NamedMode::Insert => self.mode.contains(Mode::INSERT).into(),
NamedMode::LineFeedNewLine => {
self.mode.contains(Mode::LINE_FEED_NEW_LINE).into()
}
},
AnsiMode::Unknown(_) => ModeState::NotSupported,
};
self.event_proxy.send_event(
RioEvent::PtyWrite(format!("\x1b[{};{}$y", mode.raw(), state as u8,)),
self.window_id,
);
}
#[inline]
fn set_private_mode(&mut self, mode: PrivateMode) {
let mode = match mode {
PrivateMode::Named(mode) => mode,
PrivateMode::Unknown(80) => {
self.mode.insert(Mode::SIXEL_DISPLAY);
return;
}
PrivateMode::Unknown(1070) => {
self.mode.insert(Mode::SIXEL_PRIV_PALETTE);
return;
}
PrivateMode::Unknown(8452) => {
self.mode.insert(Mode::SIXEL_CURSOR_TO_THE_RIGHT);
return;
}
PrivateMode::Unknown(mode) => {
debug!("Ignoring unknown mode {} in set_private_mode", mode);
return;
}
};
trace!("Setting private mode: {:?}", mode);
match mode {
NamedPrivateMode::UrgencyHints => self.mode.insert(Mode::URGENCY_HINTS),
NamedPrivateMode::SwapScreenAndSetRestoreCursor => {
if !self.mode.contains(Mode::ALT_SCREEN) {
self.swap_alt();
}
}
NamedPrivateMode::ShowCursor => self.mode.insert(Mode::SHOW_CURSOR),
NamedPrivateMode::CursorKeys => self.mode.insert(Mode::APP_CURSOR),
NamedPrivateMode::ReportMouseClicks => {
self.mode.remove(Mode::MOUSE_MODE);
self.mode.insert(Mode::MOUSE_REPORT_CLICK);
self.event_proxy
.send_event(RioEvent::MouseCursorDirty, self.window_id);
}
NamedPrivateMode::ReportCellMouseMotion => {
self.mode.remove(Mode::MOUSE_MODE);
self.mode.insert(Mode::MOUSE_DRAG);
self.event_proxy
.send_event(RioEvent::MouseCursorDirty, self.window_id);
}
NamedPrivateMode::ReportAllMouseMotion => {
self.mode.remove(Mode::MOUSE_MODE);
self.mode.insert(Mode::MOUSE_MOTION);
self.event_proxy
.send_event(RioEvent::MouseCursorDirty, self.window_id);
}
NamedPrivateMode::ReportFocusInOut => self.mode.insert(Mode::FOCUS_IN_OUT),
NamedPrivateMode::BracketedPaste => self.mode.insert(Mode::BRACKETED_PASTE),
NamedPrivateMode::SgrMouse => {
self.mode.remove(Mode::UTF8_MOUSE);
self.mode.insert(Mode::SGR_MOUSE);
}
NamedPrivateMode::Utf8Mouse => {
self.mode.remove(Mode::SGR_MOUSE);
self.mode.insert(Mode::UTF8_MOUSE);
}
NamedPrivateMode::AlternateScroll => self.mode.insert(Mode::ALTERNATE_SCROLL),
NamedPrivateMode::LineWrap => self.mode.insert(Mode::LINE_WRAP),
NamedPrivateMode::Origin => self.mode.insert(Mode::ORIGIN),
NamedPrivateMode::ColumnMode => self.deccolm(),
NamedPrivateMode::BlinkingCursor => {
self.blinking_cursor = true;
self.event_proxy
.send_event(RioEvent::CursorBlinkingChange, self.window_id);
}
NamedPrivateMode::SyncUpdate => (),
}
}
#[inline]
fn unset_private_mode(&mut self, mode: PrivateMode) {
let mode = match mode {
PrivateMode::Named(mode) => mode,
PrivateMode::Unknown(80) => {
self.mode.remove(Mode::SIXEL_DISPLAY);
return;
}
PrivateMode::Unknown(1070) => {
self.graphics.sixel_shared_palette = None;
self.mode.remove(Mode::SIXEL_PRIV_PALETTE);
return;
}
PrivateMode::Unknown(8452) => {
self.mode.remove(Mode::SIXEL_CURSOR_TO_THE_RIGHT);
return;
}
PrivateMode::Unknown(mode) => {
debug!("Ignoring unknown mode {} in unset_private_mode", mode);
return;
}
};
trace!("Unsetting private mode: {:?}", mode);
match mode {
NamedPrivateMode::UrgencyHints => self.mode.remove(Mode::URGENCY_HINTS),
NamedPrivateMode::SwapScreenAndSetRestoreCursor => {
if self.mode.contains(Mode::ALT_SCREEN) {
self.swap_alt();
}
}
NamedPrivateMode::ShowCursor => self.mode.remove(Mode::SHOW_CURSOR),
NamedPrivateMode::CursorKeys => self.mode.remove(Mode::APP_CURSOR),
NamedPrivateMode::ReportMouseClicks => {
self.mode.remove(Mode::MOUSE_REPORT_CLICK);
self.event_proxy
.send_event(RioEvent::MouseCursorDirty, self.window_id);
}
NamedPrivateMode::ReportCellMouseMotion => {
self.mode.remove(Mode::MOUSE_DRAG);
self.event_proxy
.send_event(RioEvent::MouseCursorDirty, self.window_id);
}
NamedPrivateMode::ReportAllMouseMotion => {
self.mode.remove(Mode::MOUSE_MOTION);
self.event_proxy
.send_event(RioEvent::MouseCursorDirty, self.window_id);
}
NamedPrivateMode::ReportFocusInOut => self.mode.remove(Mode::FOCUS_IN_OUT),
NamedPrivateMode::BracketedPaste => self.mode.remove(Mode::BRACKETED_PASTE),
NamedPrivateMode::SgrMouse => self.mode.remove(Mode::SGR_MOUSE),
NamedPrivateMode::Utf8Mouse => self.mode.remove(Mode::UTF8_MOUSE),
NamedPrivateMode::AlternateScroll => self.mode.remove(Mode::ALTERNATE_SCROLL),
NamedPrivateMode::LineWrap => self.mode.remove(Mode::LINE_WRAP),
NamedPrivateMode::Origin => self.mode.remove(Mode::ORIGIN),
NamedPrivateMode::ColumnMode => self.deccolm(),
NamedPrivateMode::BlinkingCursor => {
self.blinking_cursor = false;
self.event_proxy
.send_event(RioEvent::CursorBlinkingChange, self.window_id);
}
NamedPrivateMode::SyncUpdate => (),
}
}
#[inline]
fn report_private_mode(&mut self, mode: PrivateMode) {
info!("Reporting private mode {mode:?}");
let state = match mode {
PrivateMode::Named(mode) => match mode {
NamedPrivateMode::CursorKeys => {
self.mode.contains(Mode::APP_CURSOR).into()
}
NamedPrivateMode::Origin => self.mode.contains(Mode::ORIGIN).into(),
NamedPrivateMode::LineWrap => self.mode.contains(Mode::LINE_WRAP).into(),
NamedPrivateMode::BlinkingCursor => self.blinking_cursor.into(),
NamedPrivateMode::ShowCursor => {
self.mode.contains(Mode::SHOW_CURSOR).into()
}
NamedPrivateMode::ReportMouseClicks => {
self.mode.contains(Mode::MOUSE_REPORT_CLICK).into()
}
NamedPrivateMode::ReportCellMouseMotion => {
self.mode.contains(Mode::MOUSE_DRAG).into()
}
NamedPrivateMode::ReportAllMouseMotion => {
self.mode.contains(Mode::MOUSE_MOTION).into()
}
NamedPrivateMode::ReportFocusInOut => {
self.mode.contains(Mode::FOCUS_IN_OUT).into()
}
NamedPrivateMode::Utf8Mouse => {
self.mode.contains(Mode::UTF8_MOUSE).into()
}
NamedPrivateMode::SgrMouse => self.mode.contains(Mode::SGR_MOUSE).into(),
NamedPrivateMode::AlternateScroll => {
self.mode.contains(Mode::ALTERNATE_SCROLL).into()
}
NamedPrivateMode::UrgencyHints => {
self.mode.contains(Mode::URGENCY_HINTS).into()
}
NamedPrivateMode::SwapScreenAndSetRestoreCursor => {
self.mode.contains(Mode::ALT_SCREEN).into()
}
NamedPrivateMode::BracketedPaste => {
self.mode.contains(Mode::BRACKETED_PASTE).into()
}
NamedPrivateMode::SyncUpdate => ModeState::Reset,
NamedPrivateMode::ColumnMode => ModeState::NotSupported,
},
PrivateMode::Unknown(_) => ModeState::NotSupported,
};
self.event_proxy.send_event(
RioEvent::PtyWrite(format!("\x1b[?{};{}$y", mode.raw(), state as u8,)),
self.window_id,
);
}
#[inline]
fn dynamic_color_sequence(&mut self, prefix: String, index: usize, terminator: &str) {
debug!(
"Requested write of escape sequence for color code {}: color[{}]",
prefix, index
);
let terminator = terminator.to_owned();
self.event_proxy.send_event(
RioEvent::ColorRequest(
index,
Arc::new(move |color| {
format!(
"\x1b]{};rgb:{1:02x}{1:02x}/{2:02x}{2:02x}/{3:02x}{3:02x}{4}",
prefix, color.r, color.g, color.b, terminator
)
}),
),
self.window_id,
);
}
#[inline]
fn goto(&mut self, line: Line, col: Column) {
trace!("Going to: line={}, col={}", line, col);
let (y_offset, max_y) = if self.mode.contains(Mode::ORIGIN) {
(self.scroll_region.start, self.scroll_region.end - 1)
} else {
(Line(0), self.grid.bottommost_line())
};
self.damage_cursor();
self.grid.cursor.pos.row =
std::cmp::max(std::cmp::min(line + y_offset, max_y), Line(0));
self.grid.cursor.pos.col = std::cmp::min(col, self.grid.last_column());
self.damage_cursor();
self.grid.cursor.should_wrap = false;
}
#[inline]
fn set_active_charset(&mut self, index: CharsetIndex) {
self.active_charset = index;
}
#[inline]
fn move_forward(&mut self, cols: Column) {
let last_column =
std::cmp::min(self.grid.cursor.pos.col + cols, self.grid.last_column());
let cursor_line = self.grid.cursor.pos.row.0 as usize;
self.damage.damage_line(cursor_line);
self.grid.cursor.pos.col = last_column;
self.grid.cursor.should_wrap = false;
}
#[inline]
fn move_backward(&mut self, cols: Column) {
let column = self.grid.cursor.pos.col.saturating_sub(cols.0);
let cursor_line = self.grid.cursor.pos.row.0 as usize;
self.damage.damage_line(cursor_line);
self.grid.cursor.pos.col = Column(column);
self.grid.cursor.should_wrap = false;
}
#[inline]
fn move_backward_tabs(&mut self, count: u16) {
trace!("Moving backward {} tabs", count);
for _ in 0..count {
let mut col = self.grid.cursor.pos.col;
if col == 0 {
break;
}
for i in (0..(col.0)).rev() {
if self.tabs[Column(i)] {
col = Column(i);
break;
}
}
self.grid.cursor.pos.col = col;
}
let line = self.grid.cursor.pos.row.0 as usize;
self.damage.damage_line(line);
}
#[inline]
fn goto_line(&mut self, line: Line) {
self.goto(line, self.grid.cursor.pos.col)
}
#[inline]
fn goto_col(&mut self, col: Column) {
self.goto(self.grid.cursor.pos.row, col)
}
#[inline]
fn decaln(&mut self) {
for line in (0..self.grid.screen_lines()).map(Line::from) {
for column in 0..self.grid.columns() {
let cell = &mut self.grid[line][Column(column)];
*cell = Square::default();
cell.set_c('E');
}
}
self.mark_fully_damaged();
}
#[inline]
fn move_up(&mut self, rows: usize) {
self.goto(self.grid.cursor.pos.row - rows, self.grid.cursor.pos.col)
}
#[inline]
fn move_down(&mut self, rows: usize) {
self.goto(self.grid.cursor.pos.row + rows, self.grid.cursor.pos.col)
}
#[inline]
fn move_down_and_cr(&mut self, rows: usize) {
self.goto(self.grid.cursor.pos.row + rows, Column(0))
}
#[inline]
fn move_up_and_cr(&mut self, lines: usize) {
self.goto(self.grid.cursor.pos.row - lines, Column(0))
}
#[inline]
fn scroll_up(&mut self, lines: usize) {
let origin = self.scroll_region.start;
self.scroll_up_relative(origin, lines);
}
#[inline]
fn delete_lines(&mut self, lines: usize) {
let origin = self.grid.cursor.pos.row;
let lines = std::cmp::min(self.grid.screen_lines() - origin.0 as usize, lines);
if lines > 0 && self.scroll_region.contains(&origin) {
self.scroll_up_relative(origin, lines);
}
}
#[inline]
fn push_title(&mut self) {
trace!("Pushing '{:?}' onto title stack", self.title);
if self.title_stack.len() >= TITLE_STACK_MAX_DEPTH {
let removed = self.title_stack.remove(0);
trace!(
"Removing '{:?}' from bottom of title stack that exceeds its maximum depth",
removed
);
}
self.title_stack.push(self.title.clone());
}
#[inline]
fn pop_title(&mut self) {
trace!("Attempting to pop title from stack...");
if let Some(popped) = self.title_stack.pop() {
trace!("Title '{:?}' popped from stack", popped);
self.set_title(Some(popped));
}
}
#[inline]
fn erase_chars(&mut self, count: Column) {
let start = self.grid.cursor.pos.col;
let end = std::cmp::min(start + count, Column(self.grid.columns()));
let bg = self.grid.style_of(&self.grid.cursor.template).bg;
let blank = self.grid.blank_with_bg(bg);
let line = self.grid.cursor.pos.row;
self.damage.damage_line(line.0 as usize);
let row = &mut self.grid[line];
for cell in &mut row[start..end] {
*cell = blank;
}
if !self.graphics.kitty_placements.is_empty() {
self.graphics.kitty_graphics_dirty = true;
}
}
#[inline]
fn delete_chars(&mut self, count: usize) {
let columns = self.grid.columns();
let bg = self.grid.style_of(&self.grid.cursor.template).bg;
let blank = self.grid.blank_with_bg(bg);
let count = std::cmp::min(count, columns);
let start = self.grid.cursor.pos.col.0;
let end = std::cmp::min(start + count, columns - 1);
let num_cells = columns - end;
let line = self.grid.cursor.pos.row;
self.damage.damage_line(line.0 as usize);
let row = &mut self.grid[line][..];
for offset in 0..num_cells {
row.swap(start + offset, end + offset);
}
let end = columns - count;
for cell in &mut row[end..] {
*cell = blank;
}
}
#[inline]
fn scroll_down(&mut self, lines: usize) {
let origin = self.scroll_region.start;
self.scroll_down_relative(origin, lines);
}
#[inline]
fn insert_blank_lines(&mut self, lines: usize) {
let origin = self.grid.cursor.pos.row;
if self.scroll_region.contains(&origin) {
self.scroll_down_relative(origin, lines);
}
}
#[inline]
fn insert_blank(&mut self, count: usize) {
let bg = self.grid.style_of(&self.grid.cursor.template).bg;
let blank = self.grid.blank_with_bg(bg);
let count =
std::cmp::min(count, self.grid.columns() - self.grid.cursor.pos.col.0);
let source = self.grid.cursor.pos.col;
let destination = self.grid.cursor.pos.col.0 + count;
let num_cells = self.grid.columns() - destination;
let line = self.grid.cursor.pos.row;
self.damage.damage_line(line.0 as usize);
let row = &mut self.grid[line][..];
for offset in (0..num_cells).rev() {
row.swap(destination + offset, source.0 + offset);
}
for cell in &mut row[source.0..destination] {
*cell = blank;
}
}
#[inline]
fn reverse_index(&mut self) {
if self.grid.cursor.pos.row == self.scroll_region.start {
self.scroll_down(1);
} else {
self.damage_cursor();
self.grid.cursor.pos.row =
std::cmp::max(self.grid.cursor.pos.row - 1, Line(0));
self.damage_cursor();
}
}
#[inline]
fn reset_state(&mut self) {
if self.mode.contains(Mode::ALT_SCREEN) {
std::mem::swap(&mut self.grid, &mut self.inactive_grid);
}
self.active_charset = Default::default();
self.cursor_shape = self.default_cursor_shape;
self.grid.reset();
self.inactive_grid.reset();
self.scroll_region = Line(0)..Line(self.grid.screen_lines() as i32);
self.tabs = TabStops::new(self.grid.columns());
self.title_stack = Vec::new();
self.keyboard_mode_stack = [0; KEYBOARD_MODE_STACK_MAX_DEPTH];
self.inactive_keyboard_mode_stack = [0; KEYBOARD_MODE_STACK_MAX_DEPTH];
self.keyboard_mode_idx = 0;
self.inactive_keyboard_mode_idx = 0;
self.title = String::from("");
self.selection = None;
self.vi_mode_cursor = Default::default();
self.keyboard_mode_stack = Default::default();
self.inactive_keyboard_mode_stack = Default::default();
self.graphics.clear_all_kitty_state();
self.mode &= Mode::VI;
self.mode.insert(Mode::default());
self.event_proxy
.send_event(RioEvent::CursorBlinkingChange, self.window_id);
self.mark_fully_damaged();
}
#[inline]
fn terminal_attribute(&mut self, attr: Attr) {
trace!("Setting attribute: {:?}", attr);
use crate::crosswords::style::StyleFlags;
match attr {
Attr::Foreground(color) => self.grid.update_template_style(|s| s.fg = color),
Attr::Background(color) => self.grid.update_template_style(|s| s.bg = color),
Attr::UnderlineColor(color) => self
.grid
.update_template_style(|s| s.underline_color = color),
Attr::Reset => self
.grid
.set_template_style(crate::crosswords::style::Style::default()),
Attr::Reverse => self
.grid
.update_template_style(|s| s.flags.insert(StyleFlags::INVERSE)),
Attr::CancelReverse => self
.grid
.update_template_style(|s| s.flags.remove(StyleFlags::INVERSE)),
Attr::Bold => self
.grid
.update_template_style(|s| s.flags.insert(StyleFlags::BOLD)),
Attr::CancelBold => self
.grid
.update_template_style(|s| s.flags.remove(StyleFlags::BOLD)),
Attr::Dim => self
.grid
.update_template_style(|s| s.flags.insert(StyleFlags::DIM)),
Attr::CancelBoldDim => self.grid.update_template_style(|s| {
s.flags.remove(StyleFlags::BOLD | StyleFlags::DIM)
}),
Attr::Italic => self
.grid
.update_template_style(|s| s.flags.insert(StyleFlags::ITALIC)),
Attr::CancelItalic => self
.grid
.update_template_style(|s| s.flags.remove(StyleFlags::ITALIC)),
Attr::Underline => self.grid.update_template_style(|s| {
s.flags.remove(StyleFlags::ALL_UNDERLINES);
s.flags.insert(StyleFlags::UNDERLINE);
}),
Attr::DoubleUnderline => self.grid.update_template_style(|s| {
s.flags.remove(StyleFlags::ALL_UNDERLINES);
s.flags.insert(StyleFlags::DOUBLE_UNDERLINE);
}),
Attr::Undercurl => self.grid.update_template_style(|s| {
s.flags.remove(StyleFlags::ALL_UNDERLINES);
s.flags.insert(StyleFlags::UNDERCURL);
}),
Attr::DottedUnderline => self.grid.update_template_style(|s| {
s.flags.remove(StyleFlags::ALL_UNDERLINES);
s.flags.insert(StyleFlags::DOTTED_UNDERLINE);
}),
Attr::DashedUnderline => self.grid.update_template_style(|s| {
s.flags.remove(StyleFlags::ALL_UNDERLINES);
s.flags.insert(StyleFlags::DASHED_UNDERLINE);
}),
Attr::BlinkSlow | Attr::BlinkFast | Attr::CancelBlink => {
info!("Term got unhandled attr: {:?}", attr);
}
Attr::CancelUnderline => self
.grid
.update_template_style(|s| s.flags.remove(StyleFlags::ALL_UNDERLINES)),
Attr::Hidden => self
.grid
.update_template_style(|s| s.flags.insert(StyleFlags::HIDDEN)),
Attr::CancelHidden => self
.grid
.update_template_style(|s| s.flags.remove(StyleFlags::HIDDEN)),
Attr::Strike => self
.grid
.update_template_style(|s| s.flags.insert(StyleFlags::STRIKEOUT)),
Attr::CancelStrike => self
.grid
.update_template_style(|s| s.flags.remove(StyleFlags::STRIKEOUT)),
}
}
fn set_title(&mut self, title: Option<String>) {
self.title = title.unwrap_or_default();
}
fn set_progress_report(&mut self, report: crate::event::ProgressReport) {
self.event_proxy
.send_event(RioEvent::ProgressReport(report), self.window_id);
}
fn set_current_directory(&mut self, path: std::path::PathBuf) {
trace!("Setting working directory {:?}", path);
self.current_directory = Some(path);
}
#[inline]
fn set_cursor_style(&mut self, style: Option<CursorShape>, blinking: bool) {
if let Some(cursor_shape) = style {
self.cursor_shape = cursor_shape;
} else {
self.cursor_shape = self.default_cursor_shape;
}
self.blinking_cursor = blinking;
self.event_proxy
.send_event(RioEvent::CursorBlinkingChange, self.window_id);
}
#[inline]
fn set_cursor_shape(&mut self, shape: CursorShape) {
self.cursor_shape = shape;
}
#[inline]
fn set_keypad_application_mode(&mut self) {
trace!("Setting keypad application mode");
self.mode.insert(Mode::APP_KEYPAD);
}
#[inline]
fn unset_keypad_application_mode(&mut self) {
trace!("Unsetting keypad application mode");
self.mode.remove(Mode::APP_KEYPAD);
}
#[inline]
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) = general_purpose::STANDARD.decode(base64) {
if let Ok(text) = simd_utf8::from_utf8_to_string(&bytes) {
self.event_proxy.send_event(
RioEvent::ClipboardStore(clipboard_type, text),
self.window_id,
);
}
}
}
#[inline]
fn configure_charset(
&mut self,
index: pos::CharsetIndex,
charset: pos::StandardCharset,
) {
trace!("Configuring charset {:?} as {:?}", index, charset);
self.grid.cursor.charsets[index] = charset;
}
#[inline(never)]
fn input(&mut self, c: char) {
let width = match c.width() {
Some(width) => width,
None => return,
};
if width == 0 {
match c {
'\u{FE0F}' => self.apply_emoji_vs16(),
'\u{FE0E}' => self.apply_emoji_vs15(),
_ => {}
}
let mut column = self.grid.cursor.pos.col;
if !self.grid.cursor.should_wrap {
column.0 = column.saturating_sub(1);
}
let row = self.grid.cursor.pos.row;
if matches!(self.grid[row][column].wide(), Wide::Spacer) {
column.0 = column.saturating_sub(1);
}
let cell = &mut self.grid[row][column];
let existing_id = cell.extras_id();
if let Some(id) = existing_id {
if let Some(extras) = self.grid.extras_table.get_mut(id) {
extras.zerowidth.push(c);
}
} else {
let mut extras = crate::crosswords::square::Extras::default();
extras.zerowidth.push(c);
let id = self.grid.extras_table.alloc(extras);
let cell = &mut self.grid[row][column];
cell.set_extras_id(Some(id));
cell.insert_cell_flag(CellFlags::GRAPHEME);
}
return;
}
if self.grid.cursor.should_wrap {
self.wrapline();
}
let columns = self.grid.columns();
if self.mode.contains(Mode::INSERT) && self.grid.cursor.pos.col + width < columns
{
let line = self.grid.cursor.pos.row;
let col = self.grid.cursor.pos.col;
let row = &mut self.grid[line][..];
for col in (col.0..(columns - width)).rev() {
row.swap(col + width, col);
}
}
if width == 1 {
self.write_at_cursor(c);
} else {
if self.grid.cursor.pos.col + 1 >= columns {
if self.mode.contains(Mode::LINE_WRAP) {
self.write_at_cursor(' ');
self.grid.cursor_cell().set_wide(Wide::LeadingSpacer);
self.wrapline();
} else {
self.grid.cursor.should_wrap = true;
return;
}
}
self.write_at_cursor(c);
self.grid.cursor_cell().set_wide(Wide::Wide);
self.grid.cursor.pos.col += 1;
self.write_at_cursor(' ');
self.grid.cursor_cell().set_wide(Wide::Spacer);
}
let cursor_line = self.grid.cursor.pos.row.0 as usize;
self.damage.damage_line(cursor_line);
if self.grid.cursor.pos.col + 1 < columns {
self.grid.cursor.pos.col += 1;
} else {
self.grid.cursor.should_wrap = true;
}
}
#[inline]
fn identify_terminal(&mut self, intermediate: Option<char>) {
match intermediate {
None => {
trace!("Reporting primary device attributes");
let text = String::from("\x1b[?62;4;6;22c");
self.event_proxy
.send_event(RioEvent::PtyWrite(text), self.window_id);
}
Some('>') => {
trace!("Reporting secondary device attributes");
let version = version_number(env!("CARGO_PKG_VERSION"));
let text = format!("\x1b[>0;{version};1c");
self.event_proxy
.send_event(RioEvent::PtyWrite(text), self.window_id);
}
_ => debug!("Unsupported device attributes intermediate"),
}
}
#[inline]
fn report_version(&mut self) {
trace!("Reporting terminal version (XTVERSION)");
let version = env!("CARGO_PKG_VERSION");
let text = format!("\x1bP>|Rio {version}\x1b\\");
self.event_proxy
.send_event(RioEvent::PtyWrite(text), self.window_id);
}
#[inline]
fn report_keyboard_mode(&mut self) {
let current_mode = self.keyboard_mode_stack[self.keyboard_mode_idx];
let text = format!("\x1b[?{current_mode}u");
self.event_proxy
.send_event(RioEvent::PtyWrite(text), self.window_id);
}
#[inline]
fn push_keyboard_mode(&mut self, mode: KeyboardModes) {
self.keyboard_mode_idx = self.keyboard_mode_idx.wrapping_add(1);
if self.keyboard_mode_idx >= KEYBOARD_MODE_STACK_MAX_DEPTH {
self.keyboard_mode_idx %= KEYBOARD_MODE_STACK_MAX_DEPTH;
}
self.keyboard_mode_stack[self.keyboard_mode_idx] = mode.bits();
self.mode &= !Mode::KITTY_KEYBOARD_PROTOCOL;
self.mode |= Mode::from(mode);
}
#[inline]
fn pop_keyboard_modes(&mut self, to_pop: u16) {
if usize::from(to_pop) >= KEYBOARD_MODE_STACK_MAX_DEPTH {
self.keyboard_mode_stack.fill(KeyboardModes::NO_MODE.bits());
self.keyboard_mode_idx = 0;
self.mode &= !Mode::KITTY_KEYBOARD_PROTOCOL;
return;
}
for _ in 0..to_pop {
self.keyboard_mode_stack[self.keyboard_mode_idx] =
KeyboardModes::NO_MODE.bits();
self.keyboard_mode_idx = self.keyboard_mode_idx.wrapping_sub(1);
if self.keyboard_mode_idx >= KEYBOARD_MODE_STACK_MAX_DEPTH {
self.keyboard_mode_idx %= KEYBOARD_MODE_STACK_MAX_DEPTH;
}
}
let current_mode = self.keyboard_mode_stack[self.keyboard_mode_idx];
self.mode &= !Mode::KITTY_KEYBOARD_PROTOCOL;
self.mode |= Mode::from(KeyboardModes::from_bits_truncate(current_mode));
}
#[inline]
fn set_keyboard_mode(
&mut self,
mode: KeyboardModes,
apply: KeyboardModesApplyBehavior,
) {
self.set_keyboard_mode(mode.bits(), apply);
}
#[inline]
fn device_status(&mut self, arg: usize) {
trace!("Reporting device status: {}", arg);
match arg {
5 => {
let text = String::from("\x1b[0n");
self.event_proxy
.send_event(RioEvent::PtyWrite(text), self.window_id);
}
6 => {
let pos = self.grid.cursor.pos;
let text = format!("\x1b[{};{}R", pos.row + 1, pos.col + 1);
self.event_proxy
.send_event(RioEvent::PtyWrite(text), self.window_id);
}
_ => debug!("unknown device status query: {}", arg),
};
}
#[inline]
fn newline(&mut self) {
self.linefeed();
if self.mode.contains(Mode::LINE_FEED_NEW_LINE) {
self.carriage_return();
}
}
#[inline]
fn backspace(&mut self) {
if self.grid.cursor.pos.col > Column(0) {
let line = self.grid.cursor.pos.row.0 as usize;
self.grid.cursor.pos.col -= 1;
self.grid.cursor.should_wrap = false;
self.damage.damage_line(line);
}
}
#[inline]
fn clear_screen(&mut self, mode: ClearMode) {
let bg = self.grid.style_of(&self.grid.cursor.template).bg;
let blank = self.grid.blank_with_bg(bg);
let screen_lines = self.grid.screen_lines();
match mode {
ClearMode::Above => {
let cursor = self.grid.cursor.pos;
if cursor.row > 1 {
self.grid.reset_region(..cursor.row);
}
let end = std::cmp::min(cursor.col + 1, Column(self.grid.columns()));
for cell in &mut self.grid[cursor.row][..end] {
*cell = blank;
}
let range = Line(0)..=cursor.row;
self.selection =
self.selection.take().filter(|s| !s.intersects_range(range));
}
ClearMode::Below => {
let cursor = self.grid.cursor.pos;
for cell in &mut self.grid[cursor.row][cursor.col..] {
*cell = blank;
}
if (cursor.row.0 as usize) < screen_lines - 1 {
self.grid.reset_region((cursor.row + 1)..);
}
let range = cursor.row..Line(screen_lines as i32);
self.selection =
self.selection.take().filter(|s| !s.intersects_range(range));
}
ClearMode::All => {
if self.mode.contains(Mode::ALT_SCREEN) {
self.grid.reset_region(..);
} else {
let old_offset = self.grid.display_offset();
self.grid.clear_viewport();
let lines = self.grid.display_offset().saturating_sub(old_offset);
self.vi_mode_cursor.pos.row = (self.vi_mode_cursor.pos.row - lines)
.grid_clamp(&self.grid, Boundary::Grid);
}
self.selection = None;
}
ClearMode::Saved if self.history_size() > 0 => {
self.grid.clear_history();
self.vi_mode_cursor.pos.row = self
.vi_mode_cursor
.pos
.row
.grid_clamp(&self.grid, Boundary::Cursor);
self.selection = self
.selection
.take()
.filter(|s| !s.intersects_range(..Line(0)));
}
ClearMode::Saved => (),
}
match mode {
ClearMode::Above => {
let cursor_row = self.grid.cursor.pos.row.0 as usize;
for line in 0..=cursor_row {
self.damage.damage_line(line);
}
}
ClearMode::Below => {
let cursor_row = self.grid.cursor.pos.row.0 as usize;
for line in cursor_row..screen_lines {
self.damage.damage_line(line);
}
}
ClearMode::All | ClearMode::Saved => {
self.mark_fully_damaged();
}
}
if !self.graphics.kitty_placements.is_empty() {
self.graphics.kitty_graphics_dirty = true;
}
}
#[inline]
fn clear_tabs(&mut self, mode: TabulationClearMode) {
match mode {
TabulationClearMode::Current => {
self.tabs[self.grid.cursor.pos.col] = false;
}
TabulationClearMode::All => {
self.tabs.clear_all();
}
}
}
#[inline]
fn linefeed(&mut self) {
let next = self.grid.cursor.pos.row + 1;
if next == self.scroll_region.end {
self.scroll_up_relative(self.scroll_region.start, 1);
} else if next < self.grid.screen_lines() {
self.damage_cursor();
self.grid.cursor.pos.row += 1;
self.damage_cursor();
}
}
#[inline]
fn set_horizontal_tabstop(&mut self) {
self.tabs[self.grid.cursor.pos.col] = true;
}
#[inline]
fn set_hyperlink(&mut self, hyperlink: Option<Hyperlink>) {
match hyperlink {
Some(hl) => {
let id =
self.grid
.extras_table
.alloc(crate::crosswords::square::Extras {
hyperlink: Some(hl),
..Default::default()
});
self.grid.cursor.template.set_extras_id(Some(id));
self.grid
.cursor
.template
.insert_cell_flag(crate::crosswords::square::CellFlags::HYPERLINK);
}
None => {
self.grid.cursor.template.set_extras_id(None);
self.grid
.cursor
.template
.remove_cell_flag(crate::crosswords::square::CellFlags::HYPERLINK);
}
}
}
#[inline]
fn set_color(&mut self, index: usize, color: ColorRgb) {
let color_arr = color.to_arr();
if index != NamedColor::Cursor as usize && self.colors[index] != Some(color_arr) {
self.mark_fully_damaged();
}
self.colors[index] = Some(color_arr);
self.event_proxy.send_event(
RioEvent::ColorChange(self.route_id, index, Some(color)),
self.window_id,
);
}
#[inline]
fn reset_color(&mut self, index: usize) {
if index != NamedColor::Cursor as usize && self.colors[index].is_some() {
self.mark_fully_damaged();
}
self.colors[index] = None;
self.event_proxy.send_event(
RioEvent::ColorChange(self.route_id, index, None),
self.window_id,
);
}
#[inline]
fn bell(&mut self) {
self.event_proxy.send_event(RioEvent::Bell, self.window_id);
}
#[inline]
fn desktop_notification(&mut self, title: String, body: String) {
self.event_proxy.send_event(
RioEvent::DesktopNotification { title, body },
self.window_id,
);
}
#[inline]
fn substitute(&mut self) {
warn!("[unimplemented] Substitute");
}
#[inline]
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.event_proxy.send_event(
RioEvent::ClipboardLoad(
clipboard_type,
Arc::new(move |text| {
let base64 = general_purpose::STANDARD.encode(text);
format!("\x1b]52;{};{}{}", clipboard as char, base64, terminator)
}),
),
self.window_id,
);
}
#[inline]
fn put_tab(&mut self, mut count: u16) {
if self.grid.cursor.should_wrap {
self.wrapline();
return;
}
while self.grid.cursor.pos.col < self.grid.columns() && count != 0 {
count -= 1;
let c = self.grid.cursor.charsets[self.active_charset].map('\t');
let cell = self.grid.cursor_square();
if cell.c() == '\0' {
cell.set_c(c);
}
loop {
if (self.grid.cursor.pos.col + 1) == self.grid.columns() {
break;
}
self.grid.cursor.pos.col += 1;
if self.tabs[self.grid.cursor.pos.col] {
break;
}
}
}
}
#[inline]
fn carriage_return(&mut self) {
trace!("Carriage return");
let new_col = 0;
let row = self.grid.cursor.pos.row.0 as usize;
self.damage.damage_line(row);
self.grid.cursor.pos.col = Column(new_col);
self.grid.cursor.should_wrap = false;
}
#[inline]
fn move_forward_tabs(&mut self, count: u16) {
trace!("Moving forward {} tabs", count);
let num_cols = self.columns();
for _ in 0..count {
let mut col = self.grid.cursor.pos.col;
if col == num_cols - 1 {
break;
}
for i in col.0 + 1..num_cols {
col = Column(i);
if self.tabs[col] {
break;
}
}
self.grid.cursor.pos.col = col;
}
let line = self.grid.cursor.pos.row.0 as usize;
self.damage.damage_line(line);
}
#[inline]
fn save_cursor_position(&mut self) {
self.grid.saved_cursor = self.grid.cursor.clone();
}
#[inline]
fn restore_cursor_position(&mut self) {
trace!("Restoring cursor position");
self.damage_cursor();
self.grid.cursor = self.grid.saved_cursor.clone();
self.damage_cursor();
}
#[inline]
fn clear_line(&mut self, mode: LineClearMode) {
let bg = self.grid.style_of(&self.grid.cursor.template).bg;
let blank = self.grid.blank_with_bg(bg);
let point = self.grid.cursor.pos;
let should_wrap = self.grid.cursor.should_wrap;
let (left, right) = match mode {
LineClearMode::Right if should_wrap => return,
LineClearMode::Right => (point.col, Column(self.grid.columns())),
LineClearMode::Left => (Column(0), point.col + 1),
LineClearMode::All => (Column(0), Column(self.grid.columns())),
};
self.damage.damage_line(point.row.0 as usize);
let row = &mut self.grid[point.row];
for cell in &mut row[left..right] {
*cell = blank;
}
let range = self.grid.cursor.pos.row..=self.grid.cursor.pos.row;
self.selection = self.selection.take().filter(|s| !s.intersects_range(range));
if !self.graphics.kitty_placements.is_empty() {
self.graphics.kitty_graphics_dirty = true;
}
}
#[inline]
fn set_scrolling_region(&mut self, top: usize, bottom: Option<usize>) {
let bottom = bottom.unwrap_or_else(|| self.grid.screen_lines());
if top >= bottom {
warn!("Invalid scrolling region: ({};{})", top, bottom);
return;
}
let start = Line(top as i32 - 1);
let end = Line(bottom as i32);
debug!("Setting scrolling region: ({};{})", start, end);
let screen_lines = Line(self.grid.screen_lines() as i32);
self.scroll_region.start = std::cmp::min(start, screen_lines);
self.scroll_region.end = std::cmp::min(end, screen_lines);
self.goto(Line(0), Column(0));
}
#[inline]
fn text_area_size_pixels(&mut self) {
debug!("text_area_size_pixels");
self.event_proxy.send_event(
RioEvent::TextAreaSizeRequest(Arc::new(move |window_size| {
let height = window_size.height;
let width = window_size.width;
format!("\x1b[4;{height};{width}t")
})),
self.window_id,
);
}
#[inline]
fn cells_size_pixels(&mut self) {
let text = format!(
"\x1b[6;{};{}t",
self.graphics.cell_height, self.graphics.cell_width
);
debug!("cells_size_pixels {:?}", text);
self.event_proxy
.send_event(RioEvent::PtyWrite(text), self.window_id);
}
#[inline]
fn text_area_size_chars(&mut self) {
let text = format!(
"\x1b[8;{};{}t",
self.grid.screen_lines(),
self.grid.columns()
);
debug!("text_area_size_chars {:?}", text);
self.event_proxy
.send_event(RioEvent::PtyWrite(text), self.window_id);
}
#[inline]
fn graphics_attribute(&mut self, pi: u16, pa: u16) {
fn generate_response(pi: u16, ps: u16, pv: &[usize]) -> String {
use std::fmt::Write;
let mut text = format!("\x1b[?{pi};{ps}");
for item in pv {
let _ = write!(&mut text, ";{item}");
}
text.push('S');
text
}
let (ps, pv) = match pi {
1 => {
match pa {
1 => (0, &[sixel::MAX_COLOR_REGISTERS][..]), 2 => (3, &[][..]), 3 => (3, &[][..]), 4 => (0, &[sixel::MAX_COLOR_REGISTERS][..]),
_ => (2, &[][..]), }
}
2 => {
match pa {
1 => {
self.event_proxy.send_event(
RioEvent::TextAreaSizeRequest(Arc::new(move |window_size| {
let width = window_size.width;
let height = window_size.height;
let graphic_dimensions = [
std::cmp::min(
width as usize,
MAX_GRAPHIC_DIMENSIONS[0],
),
std::cmp::min(
height as usize,
MAX_GRAPHIC_DIMENSIONS[1],
),
];
let (ps, pv) = (0, &graphic_dimensions[..]);
generate_response(pi, ps, pv)
})),
self.window_id,
);
return;
}
2 => (3, &[][..]), 3 => (3, &[][..]), 4 => (0, &MAX_GRAPHIC_DIMENSIONS[..]),
_ => (2, &[][..]), }
}
3 => {
(1, &[][..]) }
_ => {
(1, &[][..]) }
};
self.event_proxy.send_event(
RioEvent::PtyWrite(generate_response(pi, ps, pv)),
self.window_id,
);
}
#[inline]
fn sixel_graphic_start(&mut self, params: &Params) {
let palette = self.graphics.sixel_shared_palette.take();
self.graphics.sixel_parser = Some(Box::new(sixel::Parser::new(params, palette)));
}
#[inline]
fn is_sixel_graphic_active(&self) -> bool {
self.graphics.sixel_parser.is_some()
}
#[inline]
fn sixel_graphic_put(&mut self, byte: u8) -> Result<(), sixel::Error> {
if let Some(parser) = &mut self.graphics.sixel_parser {
parser.put(byte)
} else {
self.sixel_graphic_reset();
Err(sixel::Error::NonExistentParser)
}
}
#[inline]
fn sixel_graphic_reset(&mut self) {
self.graphics.sixel_parser = None;
}
#[inline]
fn sixel_graphic_finish(&mut self) {
let parser = self.graphics.sixel_parser.take();
if let Some(parser) = parser {
match parser.finish() {
Ok((graphic, palette)) => {
self.insert_graphic(graphic, Some(palette), None)
}
Err(err) => warn!("Failed to parse Sixel data: {}", err),
}
} else {
warn!("Failed to sixel_graphic_finish");
}
}
#[inline]
fn insert_graphic(
&mut self,
graphic: GraphicData,
palette: Option<Vec<ColorRgb>>,
cursor_movement: Option<u8>,
) {
debug!(
"insert_graphic called: id={}, {}x{}, format={:?}, cursor_movement={:?}",
graphic.id.get(),
graphic.width,
graphic.height,
graphic.color_type,
cursor_movement
);
let cell_width = self.graphics.cell_width as usize;
let cell_height = self.graphics.cell_height as usize;
if let Some(palette) = palette {
if !self.mode.contains(Mode::SIXEL_PRIV_PALETTE) {
self.graphics.sixel_shared_palette = Some(palette);
}
}
let view_width = cell_width * self.grid.columns();
let view_height = cell_height * self.grid.screen_lines();
let (display_w, display_h) = graphic.compute_display_dimensions(
cell_width,
cell_height,
view_width,
view_height,
);
if display_w > MAX_GRAPHIC_DIMENSIONS[0] || display_h > MAX_GRAPHIC_DIMENSIONS[1]
{
debug!(
"insert_graphic: display dimensions too large {}x{}, max is {:?}",
display_w, display_h, MAX_GRAPHIC_DIMENSIONS
);
return;
}
let width = display_w as u16;
let height = display_h as u16;
if width == 0 || height == 0 {
debug!("insert_graphic: zero width or height, aborting");
return;
}
let mut graphic = graphic;
graphic.display_width = Some(display_w);
graphic.display_height = Some(display_h);
let graphic_bytes = graphic.pixels.len();
debug!(
"insert_graphic: image needs {} bytes, current total: {}/{}",
graphic_bytes, self.graphics.total_bytes, self.graphics.total_limit
);
if self.graphics.total_bytes + graphic_bytes > self.graphics.total_limit {
let used_ids = self.collect_used_graphic_ids();
debug!(
"insert_graphic: {} images currently in use in grid, need eviction",
used_ids.len()
);
if !self.graphics.evict_images(graphic_bytes, &used_ids) {
warn!(
"Failed to evict enough images for {} bytes, image may not display",
graphic_bytes
);
}
}
let graphic_id = self.graphics.next_id();
debug!("insert_graphic: assigned new id {}", graphic_id.0);
self.graphics.track_graphic(graphic_id, graphic_bytes);
let scrolling = !self.mode.contains(Mode::SIXEL_DISPLAY);
let leftmost = if scrolling {
self.grid.cursor.pos.col.0
} else {
0
};
let texture = Arc::new(TextureRef {
id: graphic_id,
width,
height,
cell_height,
texture_operations: Arc::downgrade(&self.graphics.texture_operations),
});
self.graphics
.register_placed_texture(graphic_id, Arc::downgrade(&texture));
for (top, offset_y) in (0..).zip((0..height).step_by(cell_height)) {
let line = if scrolling {
self.grid.cursor.pos.row
} else {
if top >= self.grid.screen_lines() as i32 {
break;
}
Line(top)
};
let row_len = self.grid[line].len();
for (left, offset_x) in (leftmost..).zip((0..width).step_by(cell_width)) {
if left >= row_len {
break;
}
let graphic_cell = GraphicCell {
texture: texture.clone(),
offset_x,
offset_y,
};
let cell_ref = &mut self.grid.raw[line][Column(left)];
if cell_ref.is_bg_only() {
cell_ref.clear();
}
if let Some(eid) = cell_ref.extras_id() {
if let Some(extras) = self.grid.extras_table.get_mut(eid) {
extras.graphic = Some(smallvec::smallvec![graphic_cell]);
}
} else {
let eid = self.grid.extras_table.alloc(square::Extras {
graphic: Some(smallvec::smallvec![graphic_cell]),
..Default::default()
});
cell_ref.set_extras_id(Some(eid));
}
cell_ref.insert_cell_flag(CellFlags::GRAPHICS);
}
self.mark_line_damaged(line);
if scrolling && offset_y < height.saturating_sub(cell_height as u16) {
self.linefeed();
}
}
match cursor_movement {
None => {
if self.mode.contains(Mode::SIXEL_CURSOR_TO_THE_RIGHT) {
let graphic_columns = graphic.width.div_ceil(cell_width);
self.move_forward(Column(graphic_columns));
} else if scrolling {
self.carriage_return();
}
}
Some(0) => {
if self.mode.contains(Mode::SIXEL_CURSOR_TO_THE_RIGHT) {
let graphic_columns = graphic.width.div_ceil(cell_width);
self.move_forward(Column(graphic_columns));
} else if scrolling {
self.carriage_return();
}
}
Some(1) => {
}
Some(_) => {
if scrolling && !self.mode.contains(Mode::SIXEL_CURSOR_TO_THE_RIGHT) {
self.carriage_return();
}
}
}
debug!(
"insert_graphic: adding to pending queue, graphic_id={}, final size={}x{}",
graphic_id.0, width, height
);
self.graphics.pending.push(GraphicData {
id: graphic_id,
..graphic
});
debug!("insert_graphic: sending graphics updates");
self.send_graphics_updates();
}
#[inline]
fn store_graphic(&mut self, graphic: GraphicData) {
let image_id = graphic.id.get() as u32;
debug!(
"Storing kitty graphic: id={}, {}x{}",
image_id, graphic.width, graphic.height
);
self.graphics.store_kitty_image(image_id, None, graphic);
}
fn kitty_transmit_and_display(
&mut self,
graphic_data: GraphicData,
placement: crate::ansi::kitty_graphics_protocol::PlacementRequest,
) {
let image_id = graphic_data.id.get() as u32;
debug!(
"Kitty transmit+display: id={}, {}x{}",
image_id, graphic_data.width, graphic_data.height
);
self.graphics
.store_kitty_image(image_id, None, graphic_data);
self.place_kitty_overlay(image_id, &placement);
}
#[inline]
fn place_graphic(
&mut self,
placement: crate::ansi::kitty_graphics_protocol::PlacementRequest,
) {
debug!(
"Kitty graphics placement: image_id={}, x={}, y={}, columns={}, rows={}, unicode_placeholder={}",
placement.image_id,
placement.x,
placement.y,
placement.columns,
placement.rows,
placement.unicode_placeholder
);
if placement.unicode_placeholder > 0 {
self.place_virtual_graphic(placement);
return;
}
let image_id = placement.image_id;
if self.graphics.get_kitty_image(image_id).is_some() {
self.place_kitty_overlay(image_id, &placement);
} else {
warn!(
"Attempted to place non-existent kitty graphic: id={}",
placement.image_id
);
}
}
#[inline]
fn delete_graphics(
&mut self,
delete: crate::ansi::kitty_graphics_protocol::DeleteRequest,
) {
debug!(
"Kitty graphics delete: action={}, image_id={}, x={}, y={}, z_index={}",
delete.action as char, delete.image_id, delete.x, delete.y, delete.z_index
);
let mut overlay_changed = false;
match delete.action {
b'a' | b'A' => {
self.delete_all_graphics();
self.graphics.kitty_placements.clear();
overlay_changed = true;
if delete.delete_data {
self.graphics.kitty_images.clear();
self.graphics.kitty_image_numbers.clear();
}
}
b'i' | b'I' => {
let image_id_to_match = delete.image_id;
let before = self.graphics.kitty_placements.len();
if delete.placement_id != 0 {
self.graphics
.kitty_placements
.remove(&(image_id_to_match, delete.placement_id));
} else {
self.graphics
.kitty_placements
.retain(|k, _| k.0 != image_id_to_match);
}
overlay_changed = self.graphics.kitty_placements.len() != before;
if delete.delete_data {
self.graphics
.delete_kitty_images(|id, _| *id == image_id_to_match);
}
}
b'c' | b'C' => {
let cursor_pos = self.grid.cursor.pos;
self.delete_graphic_at_position(cursor_pos.col, cursor_pos.row);
let col = cursor_pos.col.0;
let abs_row = self.history_size() as i64 + cursor_pos.row.0 as i64;
let before = self.graphics.kitty_placements.len();
self.graphics.kitty_placements.retain(|_, p| {
!(p.dest_col <= col
&& col < p.dest_col + p.columns as usize
&& p.dest_row <= abs_row
&& abs_row < p.dest_row + p.rows as i64)
});
overlay_changed = self.graphics.kitty_placements.len() != before;
if delete.delete_data {
self.cleanup_unused_kitty_images();
}
}
b'p' | b'P' => {
if delete.x > 0 && delete.y > 0 {
let col = Column((delete.x - 1) as usize);
let row = Line((delete.y - 1) as i32);
self.delete_graphic_at_position(col, row);
let abs_row = self.history_size() as i64 + row.0 as i64;
let c = col.0;
let before = self.graphics.kitty_placements.len();
self.graphics.kitty_placements.retain(|_, p| {
!(p.dest_col <= c
&& c < p.dest_col + p.columns as usize
&& p.dest_row <= abs_row
&& abs_row < p.dest_row + p.rows as i64)
});
overlay_changed = self.graphics.kitty_placements.len() != before;
}
if delete.delete_data {
self.cleanup_unused_kitty_images();
}
}
b'x' | b'X' => {
if delete.x > 0 {
let col = Column((delete.x - 1) as usize);
self.delete_graphics_in_column(col);
let c = col.0;
let before = self.graphics.kitty_placements.len();
self.graphics.kitty_placements.retain(|_, p| {
!(p.dest_col <= c && c < p.dest_col + p.columns as usize)
});
overlay_changed = self.graphics.kitty_placements.len() != before;
}
if delete.delete_data {
self.cleanup_unused_kitty_images();
}
}
b'y' | b'Y' => {
if delete.y > 0 {
let row = Line((delete.y - 1) as i32);
self.delete_graphics_in_row(row);
let abs_row = self.history_size() as i64 + row.0 as i64;
let before = self.graphics.kitty_placements.len();
self.graphics.kitty_placements.retain(|_, p| {
!(p.dest_row <= abs_row && abs_row < p.dest_row + p.rows as i64)
});
overlay_changed = self.graphics.kitty_placements.len() != before;
}
if delete.delete_data {
self.cleanup_unused_kitty_images();
}
}
b'z' | b'Z' => {
let z = delete.z_index;
let before = self.graphics.kitty_placements.len();
self.graphics.kitty_placements.retain(|_, p| p.z_index != z);
overlay_changed = self.graphics.kitty_placements.len() != before;
if delete.delete_data {
self.cleanup_unused_kitty_images();
}
}
b'n' | b'N' => {
let lookup_number = if delete.image_number > 0 {
delete.image_number
} else {
delete.image_id
};
if let Some(&image_id) =
self.graphics.kitty_image_numbers.get(&lookup_number)
{
let before = self.graphics.kitty_placements.len();
if delete.placement_id != 0 {
self.graphics
.kitty_placements
.remove(&(image_id, delete.placement_id));
} else {
self.graphics
.kitty_placements
.retain(|k, _| k.0 != image_id);
}
overlay_changed = self.graphics.kitty_placements.len() != before;
if delete.delete_data {
self.graphics.delete_kitty_images(|id, _| *id == image_id);
}
}
}
b'q' | b'Q' => {
if delete.x > 0 && delete.y > 0 {
let col = Column((delete.x - 1) as usize);
let row = Line((delete.y - 1) as i32);
let z = delete.z_index;
let abs_row = self.history_size() as i64 + row.0 as i64;
let c = col.0;
let before = self.graphics.kitty_placements.len();
self.graphics.kitty_placements.retain(|_, p| {
!(p.z_index == z
&& p.dest_col <= c
&& c < p.dest_col + p.columns as usize
&& p.dest_row <= abs_row
&& abs_row < p.dest_row + p.rows as i64)
});
overlay_changed = self.graphics.kitty_placements.len() != before;
}
if delete.delete_data {
self.cleanup_unused_kitty_images();
}
}
b'r' | b'R' => {
let range_start = delete.x;
let range_end = delete.y;
if range_start > 0 && range_end >= range_start {
let before = self.graphics.kitty_placements.len();
self.graphics
.kitty_placements
.retain(|k, _| k.0 < range_start || k.0 > range_end);
overlay_changed = self.graphics.kitty_placements.len() != before;
if delete.delete_data {
self.graphics.delete_kitty_images(|id, _| {
*id >= range_start && *id <= range_end
});
}
}
}
_ => {
debug!(
"Kitty graphics delete mode '{}' not implemented",
delete.action as char
);
}
}
if overlay_changed {
self.graphics.kitty_graphics_dirty = true;
}
self.send_graphics_updates();
}
#[inline]
fn kitty_graphics_response(&mut self, response: String) {
self.event_proxy
.send_event(RioEvent::PtyWrite(response), self.window_id);
}
#[inline]
fn xtgettcap_response(&mut self, response: String) {
self.event_proxy
.send_event(RioEvent::PtyWrite(response), self.window_id);
}
#[inline]
fn kitty_chunking_state_mut(
&mut self,
) -> Option<&mut crate::ansi::kitty_graphics_protocol::KittyGraphicsState> {
Some(&mut self.graphics.kitty_chunking_state)
}
}
pub struct CrosswordsSize {
pub columns: usize,
pub screen_lines: usize,
pub width: u32,
pub height: u32,
pub square_width: u32,
pub square_height: u32,
}
impl CrosswordsSize {
pub fn new(columns: usize, screen_lines: usize) -> Self {
Self {
columns,
screen_lines,
width: 0,
height: 0,
square_width: 0,
square_height: 0,
}
}
pub fn new_with_dimensions(
columns: usize,
screen_lines: usize,
width: u32,
height: u32,
square_width: u32,
square_height: u32,
) -> Self {
Self {
columns,
screen_lines,
width,
height,
square_width,
square_height,
}
}
}
impl Dimensions for CrosswordsSize {
fn total_lines(&self) -> usize {
self.screen_lines()
}
fn screen_lines(&self) -> usize {
self.screen_lines
}
fn columns(&self) -> usize {
self.columns
}
fn square_width(&self) -> f32 {
0.
}
fn square_height(&self) -> f32 {
0.
}
}
impl<T: EventListener> Dimensions for Crosswords<T> {
#[inline]
fn columns(&self) -> usize {
self.grid.columns()
}
#[inline]
fn screen_lines(&self) -> usize {
self.grid.screen_lines()
}
#[inline]
fn total_lines(&self) -> usize {
self.grid.total_lines()
}
fn square_width(&self) -> f32 {
self.graphics.cell_width
}
fn square_height(&self) -> f32 {
self.graphics.cell_height
}
}
impl<U: EventListener> Crosswords<U> {
fn place_kitty_overlay(
&mut self,
image_id: u32,
placement: &crate::ansi::kitty_graphics_protocol::PlacementRequest,
) {
let stored = match self.graphics.get_kitty_image(image_id) {
Some(s) => s,
None => {
warn!("place_kitty_overlay: image {} not found", image_id);
return;
}
};
let mut graphic_data = stored.data.clone();
if placement.columns > 0 || placement.rows > 0 {
let both_specified = placement.columns > 0 && placement.rows > 0;
graphic_data.resize = Some(sugarloaf::ResizeCommand {
width: if placement.columns > 0 {
sugarloaf::ResizeParameter::Cells(placement.columns)
} else {
sugarloaf::ResizeParameter::Auto
},
height: if placement.rows > 0 {
sugarloaf::ResizeParameter::Cells(placement.rows)
} else {
sugarloaf::ResizeParameter::Auto
},
preserve_aspect_ratio: !both_specified,
});
}
let cell_width = self.graphics.cell_width as usize;
let cell_height = self.graphics.cell_height as usize;
if cell_width == 0 || cell_height == 0 {
return;
}
let view_width = cell_width * self.grid.columns();
let view_height = cell_height * self.grid.screen_lines();
let (display_w, display_h) = graphic_data.compute_display_dimensions(
cell_width,
cell_height,
view_width,
view_height,
);
if display_w == 0 || display_h == 0 {
return;
}
if display_w > MAX_GRAPHIC_DIMENSIONS[0] || display_h > MAX_GRAPHIC_DIMENSIONS[1]
{
return;
}
graphic_data.display_width = Some(display_w);
graphic_data.display_height = Some(display_h);
let transmit_time = self
.graphics
.get_kitty_image(image_id)
.map(|s| s.transmission_time)
.unwrap_or_else(std::time::Instant::now);
graphic_data.transmit_time = transmit_time;
let dest_col = if placement.x > 0 {
placement.x as usize
} else {
self.grid.cursor.pos.col.0
};
let cursor_row = if placement.y > 0 {
placement.y as i32
} else {
self.grid.cursor.pos.row.0
};
let dest_row = self.history_size() as i64 + cursor_row as i64;
let columns = if placement.columns > 0 {
placement.columns
} else {
display_w.div_ceil(cell_width) as u32
};
let rows = if placement.rows > 0 {
placement.rows
} else {
display_h.div_ceil(cell_height) as u32
};
let placement_id = if placement.placement_id == 0 {
self.graphics.allocate_internal_placement_id()
} else {
placement.placement_id
};
let kitty_placement = KittyPlacement {
image_id,
placement_id,
source_x: placement.x,
source_y: placement.y,
source_width: placement.width,
source_height: placement.height,
dest_col,
dest_row,
columns,
rows,
pixel_width: display_w as u32,
pixel_height: display_h as u32,
cell_x_offset: 0,
cell_y_offset: 0,
z_index: placement.z_index,
transmit_time,
};
let needs_upload = match self
.graphics
.kitty_placements
.get(&(image_id, placement_id))
{
Some(existing) => existing.transmit_time != transmit_time,
None => true,
};
self.graphics
.kitty_placements
.insert((image_id, placement_id), kitty_placement);
self.graphics.kitty_graphics_dirty = true;
if needs_upload {
self.graphics.pending_images.push((image_id, graphic_data));
self.send_graphics_updates();
}
match placement.cursor_movement {
0 => {
let rows_to_advance = rows.saturating_sub(1) as usize;
for _ in 0..rows_to_advance {
self.linefeed();
}
self.carriage_return();
}
1 => {
}
_ => {
let rows_to_advance = rows.saturating_sub(1) as usize;
for _ in 0..rows_to_advance {
self.linefeed();
}
self.carriage_return();
}
}
}
fn place_virtual_graphic(
&mut self,
placement: crate::ansi::kitty_graphics_protocol::PlacementRequest,
) {
use crate::ansi::graphics::VirtualPlacement;
use crate::ansi::kitty_virtual;
debug!(
"Virtual placement: image_id={}, placement_id={}, columns={}, rows={}",
placement.image_id, placement.placement_id, placement.columns, placement.rows
);
let vp = VirtualPlacement {
image_id: placement.image_id,
placement_id: placement.placement_id,
columns: placement.columns,
rows: placement.rows,
x: placement.x,
y: placement.y,
};
self.graphics
.kitty_virtual_placements
.insert((placement.image_id, placement.placement_id), vp);
let columns = if placement.columns > 0 {
placement.columns as usize
} else {
10
};
let rows = if placement.rows > 0 {
placement.rows as usize
} else {
10
};
let image_id_low = placement.image_id & 0x00FFFFFF; let image_id_high = if placement.image_id > 0x00FFFFFF {
Some(((placement.image_id >> 24) & 0xFF) as u8)
} else {
None
};
let fg_color = kitty_virtual::id_to_rgb(image_id_low);
let underline_color = if placement.placement_id > 0 {
Some(kitty_virtual::id_to_rgb(placement.placement_id))
} else {
None
};
let start_col = self.grid.cursor.pos.col;
let _start_row = self.grid.cursor.pos.row;
let saved_template = self.grid.cursor.template;
if placement.x > 0 || placement.y > 0 {
self.grid.cursor.pos.col = Column(placement.x as usize);
self.grid.cursor.pos.row = Line(placement.y as i32);
}
for row_idx in 0..rows {
self.grid.cursor.pos.col = if placement.x > 0 {
Column(placement.x as usize)
} else {
start_col
};
for col_idx in 0..columns {
let fg = crate::config::colors::AnsiColor::Spec(fg_color);
let ul = underline_color.map(crate::config::colors::AnsiColor::Spec);
self.grid.update_template_style(|s| {
s.fg = fg;
s.underline_color = ul;
});
let placeholder_str = kitty_virtual::encode_placeholder(
row_idx as u32,
col_idx as u32,
image_id_high,
);
for ch in placeholder_str.chars() {
self.write_at_cursor(ch);
}
}
if row_idx < rows - 1 {
self.linefeed();
}
}
self.grid.cursor.template = saved_template;
debug!(
"Wrote {}x{} placeholder cells for virtual placement (image_id={:#X})",
columns, rows, placement.image_id
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crosswords::pos::{Column, Line, Pos, Side};
use crate::crosswords::CrosswordsSize;
use crate::event::VoidListener;
#[test]
fn scroll_up() {
let size = CrosswordsSize::new(1, 10);
let window_id = crate::event::WindowId::from(0);
let mut cw = Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
);
for i in 0..10 {
cw.grid[Line(i)][Column(0)].set_c(i as u8 as char);
}
cw.grid.scroll_up(&(Line(0)..Line(10)), 2);
assert_eq!(cw.grid[Line(0)][Column(0)].c(), '\u{2}');
assert_eq!(cw.grid[Line(0)].occ, 1);
assert_eq!(cw.grid[Line(1)][Column(0)].c(), '\u{3}');
assert_eq!(cw.grid[Line(1)].occ, 1);
assert_eq!(cw.grid[Line(2)][Column(0)].c(), '\u{4}');
assert_eq!(cw.grid[Line(2)].occ, 1);
assert_eq!(cw.grid[Line(3)][Column(0)].c(), '\u{5}');
assert_eq!(cw.grid[Line(3)].occ, 1);
assert_eq!(cw.grid[Line(4)][Column(0)].c(), '\u{6}');
assert_eq!(cw.grid[Line(4)].occ, 1);
assert_eq!(cw.grid[Line(5)][Column(0)].c(), '\u{7}');
assert_eq!(cw.grid[Line(5)].occ, 1);
assert_eq!(cw.grid[Line(6)][Column(0)].c(), '\u{8}');
assert_eq!(cw.grid[Line(6)].occ, 1);
assert_eq!(cw.grid[Line(7)][Column(0)].c(), '\u{9}');
assert_eq!(cw.grid[Line(7)].occ, 1);
assert_eq!(cw.grid[Line(8)][Column(0)].c(), '\0'); assert_eq!(cw.grid[Line(8)].occ, 0);
assert_eq!(cw.grid[Line(9)][Column(0)].c(), '\0'); assert_eq!(cw.grid[Line(9)].occ, 0);
}
#[test]
fn test_linefeed() {
let size = CrosswordsSize::new(1, 1);
let window_id = crate::event::WindowId::from(0);
let mut cw = Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
);
assert_eq!(cw.grid.total_lines(), 1);
cw.linefeed();
assert_eq!(cw.grid.total_lines(), 2);
}
#[test]
fn test_linefeed_moving_cursor() {
let size = CrosswordsSize::new(1, 3);
let window_id = crate::event::WindowId::from(0);
let mut cw = Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
);
let cursor = cw.cursor();
assert_eq!(cursor.pos.col, 0);
assert_eq!(cursor.pos.row, 0);
cw.linefeed();
let cursor = cw.cursor();
assert_eq!(cursor.pos.col, 0);
assert_eq!(cursor.pos.row, 1);
for _ in 0..20 {
cw.linefeed();
}
let cursor = cw.cursor();
assert_eq!(cursor.pos.col, 0);
assert_eq!(cursor.pos.row, 2);
assert_eq!(cw.grid.total_lines(), 22);
}
#[test]
fn test_input() {
let size = CrosswordsSize::new(5, 10);
let window_id = crate::event::WindowId::from(0);
let mut cw = Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
);
for i in 0..4 {
cw.grid[Line(0)][Column(i)].set_c(i as u8 as char);
}
cw.grid[Line(1)][Column(3)].set_c('b');
assert_eq!(cw.grid[Line(0)][Column(0)].c(), '\u{0}');
assert_eq!(cw.grid[Line(0)][Column(1)].c(), '\u{1}');
assert_eq!(cw.grid[Line(0)][Column(2)].c(), '\u{2}');
assert_eq!(cw.grid[Line(0)][Column(3)].c(), '\u{3}');
assert_eq!(cw.grid[Line(0)][Column(4)].c(), '\0');
assert_eq!(cw.grid[Line(1)][Column(2)].c(), '\0');
assert_eq!(cw.grid[Line(1)][Column(3)].c(), 'b');
assert_eq!(cw.grid[Line(0)][Column(4)].c(), '\0');
}
#[test]
fn osc8_hyperlink_basic() {
use crate::performer::handler::{Processor, StdSyncHandler};
let size = CrosswordsSize::new(40, 5);
let window_id = crate::event::WindowId::from(0);
let mut cw = Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
);
let mut processor = Processor::<StdSyncHandler>::new();
let bytes = b"go \x1b]8;;https://example.com\x07click\x1b]8;;\x07.";
processor.advance(&mut cw, bytes);
for col in 0..3 {
assert!(
cw.cell_hyperlink(Line(0), Column(col)).is_none(),
"col {} should have no hyperlink",
col,
);
assert!(cw.cell_hyperlink_id(Line(0), Column(col)).is_none());
}
let id = cw
.cell_hyperlink_id(Line(0), Column(3))
.expect("expected hyperlink id at col 3");
for col in 3..8 {
assert_eq!(
cw.cell_hyperlink_id(Line(0), Column(col)),
Some(id),
"col {} should share the link's extras_id",
col,
);
let hl = cw.cell_hyperlink(Line(0), Column(col)).expect("hyperlink");
assert_eq!(hl.uri(), "https://example.com");
}
assert!(cw.cell_hyperlink(Line(0), Column(8)).is_none());
assert!(cw.cell_hyperlink_id(Line(0), Column(8)).is_none());
}
#[test]
fn osc8_hyperlink_two_distinct_links_use_distinct_ids() {
use crate::performer::handler::{Processor, StdSyncHandler};
let size = CrosswordsSize::new(40, 5);
let window_id = crate::event::WindowId::from(0);
let mut cw = Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
);
let mut processor = Processor::<StdSyncHandler>::new();
let bytes = b"\x1b]8;;https://a.example\x07A\x1b]8;;\x07\
\x1b]8;;https://b.example\x07B\x1b]8;;\x07";
processor.advance(&mut cw, bytes);
let id_a = cw.cell_hyperlink_id(Line(0), Column(0));
let id_b = cw.cell_hyperlink_id(Line(0), Column(1));
assert!(id_a.is_some());
assert!(id_b.is_some());
assert_ne!(
id_a, id_b,
"two distinct hyperlinks must allocate distinct extras_ids",
);
let hl_a = cw.cell_hyperlink(Line(0), Column(0)).unwrap();
let hl_b = cw.cell_hyperlink(Line(0), Column(1)).unwrap();
assert_eq!(hl_a.uri(), "https://a.example");
assert_eq!(hl_b.uri(), "https://b.example");
}
#[test]
fn osc8_hyperlink_reset_clears_template() {
use crate::performer::handler::{Processor, StdSyncHandler};
let size = CrosswordsSize::new(40, 5);
let window_id = crate::event::WindowId::from(0);
let mut cw = Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
);
let mut processor = Processor::<StdSyncHandler>::new();
processor.advance(&mut cw, b"\x1b]8;;https://example.com\x07X\x1b]8;;\x07Y");
assert!(cw.cell_hyperlink(Line(0), Column(0)).is_some());
assert!(
cw.cell_hyperlink(Line(0), Column(1)).is_none(),
"cells after the OSC 8 reset must not inherit the previous link",
);
}
#[test]
fn osc8_hyperlink_spans_linefeed() {
use crate::performer::handler::{Processor, StdSyncHandler};
let size = CrosswordsSize::new(40, 5);
let window_id = crate::event::WindowId::from(0);
let mut cw = Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
);
let mut processor = Processor::<StdSyncHandler>::new();
processor.advance(
&mut cw,
b"\x1b]8;;https://example.com\x07AB\r\nCD\x1b]8;;\x07",
);
let id_a = cw.cell_hyperlink_id(Line(0), Column(0)).unwrap();
let id_b = cw.cell_hyperlink_id(Line(0), Column(1)).unwrap();
let id_c = cw.cell_hyperlink_id(Line(1), Column(0)).unwrap();
let id_d = cw.cell_hyperlink_id(Line(1), Column(1)).unwrap();
assert_eq!(id_a, id_b);
assert_eq!(id_b, id_c);
assert_eq!(id_c, id_d);
let hl = cw.cell_hyperlink(Line(1), Column(1)).unwrap();
assert_eq!(hl.uri(), "https://example.com");
}
#[test]
fn test_damage_tracking_after_control_c() {
let size = CrosswordsSize::new(80, 24);
let window_id = crate::event::WindowId::from(0);
let mut cw = Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
);
let test_text = "fzf> search term";
for ch in test_text.chars() {
cw.input(ch);
}
assert!(
cw.peek_damage_event().is_some(),
"Input should cause damage"
);
cw.reset_damage();
cw.damage.last_cursor = cw.grid.cursor.pos;
assert!(
cw.peek_damage_event().is_none(),
"Should have no damage after reset with cursor sync"
);
cw.carriage_return();
cw.clear_line(LineClearMode::Right);
let damage = cw.peek_damage_event();
assert!(
damage.is_some(),
"Clear line operation should register damage"
);
match damage {
Some(TerminalDamage::Partial(_)) | Some(TerminalDamage::Full) => {
}
Some(TerminalDamage::CursorOnly) | Some(TerminalDamage::Noop) => {
panic!(
"Clear line should register line damage, not just cursor movement"
);
}
None => {
panic!("Clear line should register damage");
}
}
let cursor_line = cw.grid.cursor.pos.row;
for col in 0..test_text.len() {
assert_eq!(
cw.grid[cursor_line][Column(col)].c(),
'\0',
"Line should be cleared after Control+C"
);
}
}
#[test]
fn test_damage_tracking_cursor_movement() {
let size = CrosswordsSize::new(80, 24);
let window_id = crate::event::WindowId::from(0);
let mut cw = Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
);
cw.input('A');
cw.linefeed();
cw.input('B');
cw.linefeed();
cw.input('C');
cw.reset_damage();
let old_line = cw.grid.cursor.pos.row;
cw.move_up(1);
let new_line = cw.grid.cursor.pos.row;
let damage = cw.peek_damage_event();
assert!(damage.is_some(), "Cursor movement should register damage");
assert_ne!(old_line, new_line, "Cursor should have moved");
}
#[test]
fn test_damage_tracking_clear_operations() {
let size = CrosswordsSize::new(80, 24);
let window_id = crate::event::WindowId::from(0);
let mut cw = Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
);
for line in 0..5 {
for col in 0..10 {
cw.grid[Line(line)][Column(col)].set_c('X');
}
}
cw.reset_damage();
cw.grid.cursor.pos = Pos::new(Line(2), Column(5));
cw.clear_line(LineClearMode::Right);
let damage = cw.peek_damage_event();
assert!(damage.is_some(), "Clear line should register damage");
for col in 5..10 {
assert_eq!(
cw.grid[Line(2)][Column(col)].c(),
'\0',
"Characters from cursor to end should be cleared"
);
}
for col in 0..5 {
assert_eq!(
cw.grid[Line(2)][Column(col)].c(),
'X',
"Characters before cursor should remain"
);
}
}
#[test]
fn test_damage_tracking_prompt_redraw() {
let size = CrosswordsSize::new(80, 24);
let window_id = crate::event::WindowId::from(0);
let mut cw = Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
);
let prompt = "$ ";
for ch in prompt.chars() {
cw.input(ch);
}
let command = "ls -la";
for ch in command.chars() {
cw.input(ch);
}
cw.reset_damage();
cw.carriage_return();
cw.clear_line(LineClearMode::Right);
assert!(cw.peek_damage_event().is_some(), "Line clear should damage");
for ch in prompt.chars() {
cw.input(ch);
}
assert_eq!(cw.grid[cw.grid.cursor.pos.row][Column(0)].c(), '$');
assert_eq!(cw.grid[cw.grid.cursor.pos.row][Column(1)].c(), ' ');
for col in 2..8 {
assert_eq!(
cw.grid[cw.grid.cursor.pos.row][Column(col)].c(),
'\0',
"Old command should be cleared"
);
}
}
#[test]
fn simple_selection_works() {
let size = CrosswordsSize::new(5, 5);
let window_id = crate::event::WindowId::from(0);
let mut term = Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
);
let grid = &mut term.grid;
for i in 0..4 {
if i == 1 {
continue;
}
grid[Line(i)][Column(0)].set_c('"');
for j in 1..4 {
grid[Line(i)][Column(j)].set_c('a');
}
grid[Line(i)][Column(4)].set_c('"');
}
grid[Line(2)][Column(0)].set_c(' ');
grid[Line(2)][Column(4)].set_c(' ');
grid[Line(2)][Column(4)].set_wrapline(true);
grid[Line(3)][Column(0)].set_c(' ');
term.selection = Some(Selection::new(
SelectionType::Simple,
Pos {
row: Line(0),
col: Column(0),
},
Side::Left,
));
if let Some(s) = term.selection.as_mut() {
s.update(
Pos {
row: Line(2),
col: Column(4),
},
Side::Right,
);
}
assert_eq!(
term.selection_to_string(),
Some(String::from("\"aaa\"\n\n aaa "))
);
term.selection = Some(Selection::new(
SelectionType::Simple,
Pos {
row: Line(2),
col: Column(0),
},
Side::Left,
));
if let Some(s) = term.selection.as_mut() {
s.update(
Pos {
row: Line(3),
col: Column(4),
},
Side::Right,
);
}
assert_eq!(
term.selection_to_string(),
Some(String::from(" aaa aaa\""))
);
}
#[test]
fn line_selection_works() {
let size = CrosswordsSize::new(5, 1);
let window_id = crate::event::WindowId::from(0);
let mut term = Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
);
let mut grid: Grid<Square> = Grid::new(1, 5, 0);
for i in 0..5 {
grid[Line(0)][Column(i)].set_c('a');
}
grid[Line(0)][Column(0)].set_c('"');
grid[Line(0)][Column(3)].set_c('"');
mem::swap(&mut term.grid, &mut grid);
term.selection = Some(Selection::new(
SelectionType::Lines,
Pos {
row: Line(0),
col: Column(3),
},
Side::Left,
));
assert_eq!(term.selection_to_string(), Some(String::from("\"aa\"a\n")));
}
#[test]
fn block_selection_works() {
let size = CrosswordsSize::new(5, 5);
let window_id = crate::event::WindowId::from(0);
let mut term = Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
);
let grid = &mut term.grid;
for i in 1..4 {
grid[Line(i)][Column(0)].set_c('"');
for j in 1..4 {
grid[Line(i)][Column(j)].set_c('a');
}
grid[Line(i)][Column(4)].set_c('"');
}
grid[Line(2)][Column(2)].set_c(' ');
grid[Line(2)][Column(4)].set_wrapline(true);
grid[Line(3)][Column(4)].set_c(' ');
term.selection = Some(Selection::new(
SelectionType::Block,
Pos {
row: Line(0),
col: Column(3),
},
Side::Left,
));
if let Some(s) = term.selection.as_mut() {
s.update(
Pos {
row: Line(3),
col: Column(3),
},
Side::Right,
);
}
assert_eq!(term.selection_to_string(), Some(String::from("\na\na\na")));
if let Some(s) = term.selection.as_mut() {
s.update(
Pos {
row: Line(3),
col: Column(0),
},
Side::Left,
);
}
assert_eq!(
term.selection_to_string(),
Some(String::from("\n\"aa\n\"a\n\"aa"))
);
if let Some(s) = term.selection.as_mut() {
s.update(
Pos {
row: Line(3),
col: Column(4),
},
Side::Right,
);
}
assert_eq!(
term.selection_to_string(),
Some(String::from("\na\"\na\"\na"))
);
}
#[test]
fn parse_cargo_version() {
assert_eq!(version_number("0.0.1-nightly"), 1);
assert_eq!(version_number("0.1.2-nightly"), 1_02);
assert_eq!(version_number("1.2.3-nightly"), 1_02_03);
assert_eq!(version_number("999.99.99"), 9_99_99_99);
}
#[test]
fn test_cursor_damage_after_clear() {
use crate::ansi::CursorShape;
use crate::crosswords::CrosswordsSize;
use crate::event::{VoidListener, WindowId};
use crate::performer::handler::Handler;
let size = CrosswordsSize::new(10, 10);
let window_id = WindowId::from(0);
let mut term = Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
);
term.goto(Line(1), Column(5));
for c in "hello".chars() {
term.input(c);
}
{
let _initial_damage = term.damage();
}
term.reset_damage();
term.clear_screen(ClearMode::All);
term.goto(Line(0), Column(0));
assert_eq!(term.grid.cursor.pos.row, Line(0));
assert_eq!(term.grid.cursor.pos.col, Column(0));
{
let _clear_damage = term.damage();
}
term.reset_damage();
term.input('a');
let has_damage_first = {
let damage_after_first_a = term.damage();
match damage_after_first_a {
TermDamage::Partial(iter) => {
let damaged_lines: Vec<_> = iter.collect();
!damaged_lines.is_empty()
&& damaged_lines.iter().any(|line| line.line == 0)
}
TermDamage::Full => true,
}
};
assert!(has_damage_first, "First 'a' should cause line damage");
term.reset_damage();
term.input('a');
let has_damage_second = {
let damage_after_second_a = term.damage();
match damage_after_second_a {
TermDamage::Partial(iter) => {
let damaged_lines: Vec<_> = iter.collect();
!damaged_lines.is_empty()
&& damaged_lines.iter().any(|line| line.line == 0)
}
TermDamage::Full => true,
}
};
assert!(has_damage_second, "Second 'a' should cause line damage");
term.reset_damage();
assert_eq!(term.grid.cursor.pos.row, Line(0));
assert_eq!(term.grid.cursor.pos.col, Column(2)); }
#[test]
fn test_line_damage_approach() {
use crate::ansi::CursorShape;
use crate::crosswords::CrosswordsSize;
use crate::event::{VoidListener, WindowId};
let size = CrosswordsSize::new(10, 10);
let window_id = WindowId::from(0);
let mut term = Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
);
term.reset_damage();
term.goto(Line(2), Column(3));
term.damage_cursor_line();
let damage_result = {
let damage = term.damage();
match damage {
TermDamage::Partial(iter) => {
let damaged_lines: Vec<_> = iter.collect();
damaged_lines
.iter()
.find(|line| line.line == 2)
.map(|line| line.damaged)
}
TermDamage::Full => Some(true), }
};
assert_eq!(damage_result, Some(true), "Should damage line 2");
term.reset_damage();
term.damage_line(5);
let damage_result_2 = {
let damage = term.damage();
match damage {
TermDamage::Partial(iter) => {
let damaged_lines: Vec<_> = iter.collect();
damaged_lines
.iter()
.find(|line| line.line == 5)
.map(|line| line.damaged)
}
TermDamage::Full => Some(true),
}
};
assert_eq!(damage_result_2, Some(true), "Should damage line 5");
}
#[test]
fn test_keyboard_mode_push_pop() {
let size = CrosswordsSize::new(10, 10);
let window_id = WindowId::from(0);
let mut term = Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
);
assert_eq!(
term.keyboard_mode_stack[term.keyboard_mode_idx],
KeyboardModes::NO_MODE.bits()
);
assert_eq!(term.keyboard_mode_idx, 0);
Handler::push_keyboard_mode(&mut term, KeyboardModes::DISAMBIGUATE_ESC_CODES);
assert_eq!(term.keyboard_mode_idx, 1);
assert_eq!(
term.keyboard_mode_stack[1],
KeyboardModes::DISAMBIGUATE_ESC_CODES.bits()
);
Handler::push_keyboard_mode(&mut term, KeyboardModes::REPORT_EVENT_TYPES);
assert_eq!(term.keyboard_mode_idx, 2);
assert_eq!(
term.keyboard_mode_stack[2],
KeyboardModes::REPORT_EVENT_TYPES.bits()
);
Handler::pop_keyboard_modes(&mut term, 1);
assert_eq!(term.keyboard_mode_idx, 1);
assert_eq!(term.keyboard_mode_stack[2], KeyboardModes::NO_MODE.bits());
Handler::pop_keyboard_modes(&mut term, 1);
assert_eq!(term.keyboard_mode_idx, 0);
assert_eq!(term.keyboard_mode_stack[1], KeyboardModes::NO_MODE.bits()); }
#[test]
fn test_keyboard_mode_stack_wraparound() {
let size = CrosswordsSize::new(10, 10);
let window_id = WindowId::from(0);
let mut term = Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
);
for i in 0..KEYBOARD_MODE_STACK_MAX_DEPTH {
Handler::push_keyboard_mode(&mut term, KeyboardModes::DISAMBIGUATE_ESC_CODES);
assert_eq!(
term.keyboard_mode_idx,
(i + 1) % KEYBOARD_MODE_STACK_MAX_DEPTH
);
}
Handler::push_keyboard_mode(&mut term, KeyboardModes::REPORT_EVENT_TYPES);
assert_eq!(term.keyboard_mode_idx, 1); assert_eq!(
term.keyboard_mode_stack[1],
KeyboardModes::REPORT_EVENT_TYPES.bits()
);
}
#[test]
fn test_keyboard_mode_pop_excessive() {
let size = CrosswordsSize::new(10, 10);
let window_id = WindowId::from(0);
let mut term = Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
);
Handler::push_keyboard_mode(&mut term, KeyboardModes::DISAMBIGUATE_ESC_CODES);
Handler::push_keyboard_mode(&mut term, KeyboardModes::REPORT_EVENT_TYPES);
Handler::push_keyboard_mode(&mut term, KeyboardModes::REPORT_ALTERNATE_KEYS);
Handler::pop_keyboard_modes(&mut term, KEYBOARD_MODE_STACK_MAX_DEPTH as u16);
assert_eq!(term.keyboard_mode_idx, 0);
for i in 0..KEYBOARD_MODE_STACK_MAX_DEPTH {
assert_eq!(term.keyboard_mode_stack[i], KeyboardModes::NO_MODE.bits());
}
}
#[test]
fn test_keyboard_mode_set_replace() {
let size = CrosswordsSize::new(10, 10);
let window_id = WindowId::from(0);
let mut term = Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
);
Handler::set_keyboard_mode(
&mut term,
KeyboardModes::DISAMBIGUATE_ESC_CODES,
KeyboardModesApplyBehavior::Replace,
);
assert_eq!(
term.keyboard_mode_stack[term.keyboard_mode_idx],
KeyboardModes::DISAMBIGUATE_ESC_CODES.bits()
);
Handler::set_keyboard_mode(
&mut term,
KeyboardModes::REPORT_EVENT_TYPES,
KeyboardModesApplyBehavior::Replace,
);
assert_eq!(
term.keyboard_mode_stack[term.keyboard_mode_idx],
KeyboardModes::REPORT_EVENT_TYPES.bits()
);
}
#[test]
fn test_keyboard_mode_set_union() {
let size = CrosswordsSize::new(10, 10);
let window_id = WindowId::from(0);
let mut term = Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
);
Handler::set_keyboard_mode(
&mut term,
KeyboardModes::DISAMBIGUATE_ESC_CODES,
KeyboardModesApplyBehavior::Replace,
);
Handler::set_keyboard_mode(
&mut term,
KeyboardModes::REPORT_EVENT_TYPES,
KeyboardModesApplyBehavior::Union,
);
let expected = KeyboardModes::DISAMBIGUATE_ESC_CODES.bits()
| KeyboardModes::REPORT_EVENT_TYPES.bits();
assert_eq!(term.keyboard_mode_stack[term.keyboard_mode_idx], expected);
}
#[test]
fn test_keyboard_mode_set_difference() {
let size = CrosswordsSize::new(10, 10);
let window_id = WindowId::from(0);
let mut term = Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
);
let combined_mode =
KeyboardModes::DISAMBIGUATE_ESC_CODES | KeyboardModes::REPORT_EVENT_TYPES;
Handler::set_keyboard_mode(
&mut term,
combined_mode,
KeyboardModesApplyBehavior::Replace,
);
Handler::set_keyboard_mode(
&mut term,
KeyboardModes::REPORT_EVENT_TYPES,
KeyboardModesApplyBehavior::Difference,
);
assert_eq!(
term.keyboard_mode_stack[term.keyboard_mode_idx],
KeyboardModes::DISAMBIGUATE_ESC_CODES.bits()
);
}
#[test]
fn test_keyboard_mode_report() {
let size = CrosswordsSize::new(10, 10);
let window_id = WindowId::from(0);
let listener = VoidListener {};
let mut term =
Crosswords::new(size, CursorShape::Block, listener, window_id, 0, 10_000);
Handler::push_keyboard_mode(&mut term, KeyboardModes::DISAMBIGUATE_ESC_CODES);
let current_mode = term.keyboard_mode_stack[term.keyboard_mode_idx];
assert_eq!(current_mode, KeyboardModes::DISAMBIGUATE_ESC_CODES.bits());
}
#[test]
fn test_keyboard_mode_reset() {
let size = CrosswordsSize::new(10, 10);
let window_id = WindowId::from(0);
let mut term = Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
);
Handler::push_keyboard_mode(&mut term, KeyboardModes::DISAMBIGUATE_ESC_CODES);
Handler::push_keyboard_mode(&mut term, KeyboardModes::REPORT_EVENT_TYPES);
Handler::push_keyboard_mode(&mut term, KeyboardModes::REPORT_ALTERNATE_KEYS);
term.reset_state();
assert_eq!(term.keyboard_mode_idx, 0);
assert_eq!(term.inactive_keyboard_mode_idx, 0);
for i in 0..KEYBOARD_MODE_STACK_MAX_DEPTH {
assert_eq!(term.keyboard_mode_stack[i], 0);
assert_eq!(term.inactive_keyboard_mode_stack[i], 0);
}
}
#[test]
fn test_keyboard_mode_stack_underflow_protection() {
let size = CrosswordsSize::new(10, 10);
let window_id = WindowId::from(0);
let mut term = Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
);
assert_eq!(term.keyboard_mode_idx, 0);
Handler::pop_keyboard_modes(&mut term, 1);
let expected_idx = (0_usize.wrapping_sub(1)) % KEYBOARD_MODE_STACK_MAX_DEPTH;
assert_eq!(term.keyboard_mode_idx, expected_idx);
assert_eq!(term.keyboard_mode_stack[0], KeyboardModes::NO_MODE.bits()); }
#[test]
fn test_xtversion_report() {
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Clone)]
struct TestListener {
events: Rc<RefCell<Vec<RioEvent>>>,
}
impl EventListener for TestListener {
fn event(&self) -> (Option<RioEvent>, bool) {
(None, false)
}
fn send_event(&self, event: RioEvent, _id: WindowId) {
self.events.borrow_mut().push(event);
}
}
let size = CrosswordsSize::new(10, 10);
let window_id = WindowId::from(0);
let events = Rc::new(RefCell::new(Vec::new()));
let listener = TestListener {
events: events.clone(),
};
let mut term =
Crosswords::new(size, CursorShape::Block, listener, window_id, 0, 10_000);
Handler::report_version(&mut term);
let captured_events = events.borrow();
assert_eq!(captured_events.len(), 1, "Should have sent one event");
match &captured_events[0] {
RioEvent::PtyWrite(text) => {
assert!(
text.starts_with("\x1bP>|Rio "),
"Should start with DCS>|Rio"
);
assert!(text.ends_with("\x1b\\"), "Should end with ST");
let version = env!("CARGO_PKG_VERSION");
let expected = format!("\x1bP>|Rio {}\x1b\\", version);
assert_eq!(
text, &expected,
"XTVERSION response should match expected format"
);
}
other => panic!("Expected PtyWrite event, got {:?}", other),
}
}
#[test]
fn test_keyboard_mode_syncs_with_mode() {
let size = CrosswordsSize::new(10, 10);
let window_id = WindowId::from(0);
let mut term = Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
);
assert!(!term.mode().contains(Mode::DISAMBIGUATE_ESC_CODES));
assert!(!term.mode().contains(Mode::REPORT_ALL_KEYS_AS_ESC));
Handler::push_keyboard_mode(&mut term, KeyboardModes::DISAMBIGUATE_ESC_CODES);
assert!(
term.mode().contains(Mode::DISAMBIGUATE_ESC_CODES),
"mode() should contain DISAMBIGUATE_ESC_CODES after push"
);
assert!(!term.mode().contains(Mode::REPORT_ALL_KEYS_AS_ESC));
Handler::push_keyboard_mode(&mut term, KeyboardModes::REPORT_ALL_KEYS_AS_ESC);
assert!(
term.mode().contains(Mode::REPORT_ALL_KEYS_AS_ESC),
"mode() should contain REPORT_ALL_KEYS_AS_ESC after push"
);
assert!(!term.mode().contains(Mode::DISAMBIGUATE_ESC_CODES),
"mode() should not contain DISAMBIGUATE_ESC_CODES after pushing different mode"
);
Handler::pop_keyboard_modes(&mut term, 1);
assert!(
term.mode().contains(Mode::DISAMBIGUATE_ESC_CODES),
"mode() should contain DISAMBIGUATE_ESC_CODES after pop"
);
assert!(
!term.mode().contains(Mode::REPORT_ALL_KEYS_AS_ESC),
"mode() should not contain REPORT_ALL_KEYS_AS_ESC after pop"
);
Handler::set_keyboard_mode(
&mut term,
KeyboardModes::REPORT_EVENT_TYPES,
KeyboardModesApplyBehavior::Union,
);
assert!(
term.mode().contains(Mode::DISAMBIGUATE_ESC_CODES),
"mode() should still contain DISAMBIGUATE_ESC_CODES after union"
);
assert!(
term.mode().contains(Mode::REPORT_EVENT_TYPES),
"mode() should contain REPORT_EVENT_TYPES after union"
);
Handler::set_keyboard_mode(
&mut term,
KeyboardModes::REPORT_ALTERNATE_KEYS,
KeyboardModesApplyBehavior::Replace,
);
assert!(
term.mode().contains(Mode::REPORT_ALTERNATE_KEYS),
"mode() should contain REPORT_ALTERNATE_KEYS after replace"
);
assert!(
!term.mode().contains(Mode::DISAMBIGUATE_ESC_CODES),
"mode() should not contain DISAMBIGUATE_ESC_CODES after replace"
);
assert!(
!term.mode().contains(Mode::REPORT_EVENT_TYPES),
"mode() should not contain REPORT_EVENT_TYPES after replace"
);
}
#[test]
fn sixel_stores_graphic_in_extras_table() {
let size = CrosswordsSize::new(20, 10);
let window_id = crate::event::WindowId::from(0);
let mut cw = Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
);
cw.graphics.cell_width = 10.0;
cw.graphics.cell_height = 10.0;
let graphic = GraphicData {
id: sugarloaf::GraphicId::new(0), width: 20,
height: 20,
pixels: vec![0u8; 20 * 20 * 4],
color_type: sugarloaf::ColorType::Rgba,
is_opaque: true,
display_width: None,
display_height: None,
resize: None,
transmit_time: std::time::Instant::now(),
};
cw.insert_graphic(graphic, None, None);
for row in 0..2 {
for col in 0..2 {
let cell = &cw.grid[Line(row)][Column(col)];
assert!(
cell.has_graphics(),
"cell ({row},{col}) should have GRAPHICS flag"
);
let eid = cell.extras_id().expect("cell should have extras_id");
let extras = cw
.grid
.extras_table
.get(eid)
.expect("extras slot should exist");
let gc = extras
.graphic
.as_ref()
.expect("extras should have graphic")
.first()
.expect("graphic SmallVec should have one entry");
assert_eq!(gc.offset_x, (col * 10) as u16);
assert_eq!(gc.offset_y, (row * 10) as u16);
}
}
assert!(!cw.grid[Line(0)][Column(2)].has_graphics());
}
#[test]
fn cell_graphic_accessor() {
let size = CrosswordsSize::new(20, 10);
let window_id = crate::event::WindowId::from(0);
let mut cw = Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
);
cw.graphics.cell_width = 10.0;
cw.graphics.cell_height = 10.0;
let graphic = GraphicData {
id: sugarloaf::GraphicId::new(0),
width: 10,
height: 10,
pixels: vec![0u8; 10 * 10 * 4],
color_type: sugarloaf::ColorType::Rgba,
is_opaque: true,
display_width: None,
display_height: None,
resize: None,
transmit_time: std::time::Instant::now(),
};
cw.insert_graphic(graphic, None, None);
let gc = cw
.cell_graphic(Line(0), Column(0))
.expect("cell_graphic should return a GraphicCell");
assert_eq!(gc.offset_x, 0);
assert_eq!(gc.offset_y, 0);
assert!(gc.texture.id.get() > 0);
}
#[test]
fn delete_all_graphics_frees_extras() {
let size = CrosswordsSize::new(20, 10);
let window_id = crate::event::WindowId::from(0);
let mut cw = Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
);
cw.graphics.cell_width = 10.0;
cw.graphics.cell_height = 10.0;
let graphic = GraphicData {
id: sugarloaf::GraphicId::new(0),
width: 20,
height: 20,
pixels: vec![0u8; 20 * 20 * 4],
color_type: sugarloaf::ColorType::Rgba,
is_opaque: true,
display_width: None,
display_height: None,
resize: None,
transmit_time: std::time::Instant::now(),
};
cw.insert_graphic(graphic, None, None);
assert!(cw.grid[Line(0)][Column(0)].has_graphics());
cw.delete_all_graphics();
for row in 0..2 {
for col in 0..2 {
let cell = &cw.grid[Line(row)][Column(col)];
assert!(
!cell.has_graphics(),
"cell ({row},{col}) should no longer have GRAPHICS"
);
}
}
assert!(cw.cell_graphic(Line(0), Column(0)).is_none());
}
fn new_term(cols: usize, rows: usize) -> Crosswords<VoidListener> {
let size = CrosswordsSize::new(cols, rows);
let window_id = crate::event::WindowId::from(0);
Crosswords::new(
size,
CursorShape::Block,
VoidListener {},
window_id,
0,
10_000,
)
}
#[test]
fn vs16_widens_text_presentation_emoji() {
use crate::performer::handler::Handler;
let mut cw = new_term(10, 3);
cw.input('\u{1F39F}');
cw.input('\u{FE0F}');
let row = Line(0);
assert_eq!(cw.grid[row][Column(0)].c(), '\u{1F39F}');
assert_eq!(cw.grid[row][Column(0)].wide(), Wide::Wide);
assert_eq!(cw.grid[row][Column(1)].wide(), Wide::Spacer);
assert_eq!(cw.grid.cursor.pos.col, Column(2));
assert!(!cw.grid.cursor.should_wrap);
let extras_id = cw.grid[row][Column(0)].extras_id();
assert!(extras_id.is_some());
}
#[test]
fn vs16_on_non_emoji_base_leaves_cell_narrow() {
use crate::performer::handler::Handler;
let mut cw = new_term(10, 3);
cw.input('a');
cw.input('\u{FE0F}');
let row = Line(0);
assert_eq!(cw.grid[row][Column(0)].c(), 'a');
assert_eq!(cw.grid[row][Column(0)].wide(), Wide::Narrow);
assert_eq!(cw.grid[row][Column(1)].wide(), Wide::Narrow);
assert_eq!(cw.grid.cursor.pos.col, Column(1));
}
#[test]
fn vs16_on_already_wide_emoji_is_noop() {
use crate::performer::handler::Handler;
let mut cw = new_term(10, 3);
cw.input('\u{1F91D}');
cw.input('\u{FE0F}');
let row = Line(0);
assert_eq!(cw.grid[row][Column(0)].wide(), Wide::Wide);
assert_eq!(cw.grid[row][Column(1)].wide(), Wide::Spacer);
assert_eq!(cw.grid.cursor.pos.col, Column(2));
}
#[test]
fn vs15_narrows_default_emoji() {
use crate::performer::handler::Handler;
let mut cw = new_term(10, 3);
cw.input('\u{1F44D}');
cw.input('\u{FE0E}');
let row = Line(0);
assert_eq!(cw.grid[row][Column(0)].c(), '\u{1F44D}');
assert_eq!(cw.grid[row][Column(0)].wide(), Wide::Narrow);
assert_eq!(cw.grid[row][Column(1)].wide(), Wide::Narrow);
assert_eq!(cw.grid.cursor.pos.col, Column(1));
assert!(!cw.grid.cursor.should_wrap);
}
#[test]
fn vs15_on_non_listed_emoji_is_noop() {
use crate::performer::handler::Handler;
let mut cw = new_term(10, 3);
cw.input('\u{1F91D}');
cw.input('\u{FE0E}');
let row = Line(0);
assert_eq!(cw.grid[row][Column(0)].wide(), Wide::Wide);
assert_eq!(cw.grid[row][Column(1)].wide(), Wide::Spacer);
assert_eq!(cw.grid.cursor.pos.col, Column(2));
}
#[test]
fn vs16_at_column_zero_noop() {
use crate::performer::handler::Handler;
let mut cw = new_term(10, 3);
cw.input('\u{FE0F}');
let row = Line(0);
assert_eq!(cw.grid[row][Column(0)].wide(), Wide::Narrow);
assert_eq!(cw.grid.cursor.pos.col, Column(0));
}
#[test]
fn vs16_at_last_column_wraps_base_to_next_row() {
use crate::performer::handler::Handler;
let mut cw = new_term(3, 3);
cw.input('a');
cw.input('a');
cw.input('\u{1F39F}');
assert!(cw.grid.cursor.should_wrap);
cw.input('\u{FE0F}');
assert_eq!(cw.grid[Line(0)][Column(0)].c(), 'a');
assert_eq!(cw.grid[Line(0)][Column(1)].c(), 'a');
assert_eq!(cw.grid[Line(0)][Column(2)].wide(), Wide::LeadingSpacer);
assert_eq!(cw.grid[Line(1)][Column(0)].c(), '\u{1F39F}');
assert_eq!(cw.grid[Line(1)][Column(0)].wide(), Wide::Wide);
assert_eq!(cw.grid[Line(1)][Column(1)].wide(), Wide::Spacer);
assert_eq!(cw.grid.cursor.pos.row, Line(1));
assert_eq!(cw.grid.cursor.pos.col, Column(2));
assert!(!cw.grid.cursor.should_wrap);
}
#[test]
fn vs16_at_last_column_preserves_base_extras() {
use crate::performer::handler::Handler;
let mut cw = new_term(3, 3);
cw.input('a');
cw.input('a');
cw.input('\u{1F39F}');
cw.input('\u{200D}');
let original_extras = cw.grid[Line(0)][Column(2)].extras_id();
assert!(original_extras.is_some());
cw.input('\u{FE0F}');
let moved_extras = cw.grid[Line(1)][Column(0)].extras_id();
assert_eq!(moved_extras, original_extras);
assert_eq!(cw.grid[Line(1)][Column(0)].wide(), Wide::Wide);
assert_eq!(cw.grid[Line(0)][Column(2)].wide(), Wide::LeadingSpacer);
}
#[test]
fn vs16_then_vs15_round_trip_narrows() {
use crate::performer::handler::Handler;
let mut cw = new_term(10, 3);
cw.input('\u{1F39F}');
cw.input('\u{FE0F}');
cw.input('\u{FE0E}');
let row = Line(0);
assert_eq!(cw.grid[row][Column(0)].wide(), Wide::Narrow);
assert_eq!(cw.grid[row][Column(1)].wide(), Wide::Narrow);
assert_eq!(cw.grid.cursor.pos.col, Column(1));
}
#[test]
fn vs16_then_following_char_does_not_overlap() {
use crate::performer::handler::Handler;
let mut cw = new_term(10, 3);
cw.input('"');
cw.input('\u{1F39F}');
cw.input('\u{FE0F}');
cw.input('"');
let row = Line(0);
assert_eq!(cw.grid[row][Column(0)].c(), '"');
assert_eq!(cw.grid[row][Column(1)].c(), '\u{1F39F}');
assert_eq!(cw.grid[row][Column(1)].wide(), Wide::Wide);
assert_eq!(cw.grid[row][Column(2)].wide(), Wide::Spacer);
assert_eq!(cw.grid[row][Column(3)].c(), '"');
assert_eq!(cw.grid.cursor.pos.col, Column(4));
}
}