//! A single terminal screen: a page list plus the cursor, saved cursor, //! selection, charset state, kitty keyboard flags and semantic prompt state. //! //! Port of `ZIG/src/terminal/Screen.zig`. //! //! # Differences from the Zig API //! //! * The cursor's `page_pin` is a [`TrackedPin`] handle into `screen.pages`; //! the cached `page_row` / `page_cell` pointers are replaced by on-demand //! accessors ([`Screen::cursor_row`], [`Screen::cursor_cell`], ...). //! * Errors use [`page::Error`] (which covers Zig's `Allocator.Error`, //! `IncreaseCapacityError`, `SplitError` and the page insertion errors). //! * Selection helpers take the screen's [`PageList`] explicitly (see //! `selection.rs`). #![allow( clippy::manual_assert_eq, clippy::single_match_else, clippy::doc_overindented_list_items, clippy::items_after_statements )] use ghostty_vt::{ ansi::ProtectedMode, charsets::{Charset, Slots}, kitty::key::FlagStack, osc::semantic_prompt::{Click, PromptKind, Redraw}, sgr, }; /// The Unicode codepoint used by the Kitty graphics protocol for virtual /// placements (`kitty.graphics.unicode.placeholder`). use crate::kitty::graphics_unicode::PLACEHOLDER as KITTY_PLACEHOLDER; pub use crate::page::Error; use crate::{ cursor::Style as CursorStyle, formatter::{Content as FormatContent, Options as FormatOptions, ScreenFormatter}, hyperlink::{self, Hyperlink, HyperlinkId}, kitty::graphics_storage::ImageStorage, page::{Cell, ContentTag, Page, Row, SemanticContent, SemanticPrompt as RowPrompt, Wide}, page_list::{ CellSubset, Chunk, Direction, IncreaseCapacity, NodeId, PageList, Pin, Scroll as ListScroll, TrackedPin, Viewport, }, point::{Coordinate, Point, Tag}, selection::{Bounds, Order, Selection}, size, style::{self, Style}, }; /// See Terminal.Dirty. This behaves the same way. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct Dirty { /// Set when the selection is set or unset, regardless of if the /// selection is changed or not. pub selection: bool, /// When an OSC8 hyperlink is hovered, we set the full screen as dirty /// because links can span multiple lines. pub hyperlink_hover: bool, } /// How click handling in a prompt is handled. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SemanticClick { None, ClickEvents, Cl(Click), } /// Semantic prompt (OSC133) state. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SemanticPrompt { /// This is flipped to true when any sort of semantic content is /// seen. In particular, this is set to true only when a `prompt` type /// is ever set on our cursor. /// /// This is used to optimize away semantic content operations if we know /// we've never seen them. pub seen: bool, /// This is set on any `cl` or `click_events` option set on the /// most recent OSC 133 commands to specify how click handling in a /// prompt is handling. pub click: SemanticClick, } impl SemanticPrompt { pub const DISABLED: SemanticPrompt = SemanticPrompt { seen: false, click: SemanticClick::None, }; } /// Legacy dirty flag value retained for source compatibility. /// Screens now own the full [`ImageStorage`]. #[derive(Debug, Clone, Default)] pub struct KittyImages { /// Dirty flag for the renderer. pub dirty: bool, } /// The cursor position and style. #[derive(Debug, Clone)] pub struct Cursor { /// The x position within the active area. pub x: size::CellCountInt, /// The y position within the active area. pub y: size::CellCountInt, /// The visual style of the cursor. This defaults to block because /// it has to default to something, but users of this struct are /// encouraged to set their own default. pub cursor_style: CursorStyle, /// The "last column flag (LCF)" as its called. If this is set then the /// next character print will force a soft-wrap. pub pending_wrap: bool, /// The protected mode state of the cursor. If this is true then /// all new characters printed will have the protected state set. pub protected: bool, /// The currently active style. This is the concrete style value /// that should be kept up to date. The style ID to use for cell writing /// is below. pub style: Style, /// The currently active style ID. The style is page-specific so when /// we change pages we need to ensure that we update that page with /// our style when used. pub style_id: style::Id, /// The hyperlink ID that is currently active for the cursor. A value /// of zero means no hyperlink is active. (Implements OSC8, saying that /// so code search can find it.). pub hyperlink_id: hyperlink::Id, /// This is the implicit ID to use for hyperlinks that don't specify /// an ID. We do an overflowing add to this so repeats can technically /// happen with carefully crafted inputs but for real workloads its /// highly unlikely -- and the fix is for the TUI program to use explicit /// IDs. pub hyperlink_implicit_id: size::OffsetInt, /// Heap-allocated hyperlink state so that we can recreate it when /// the cursor page pin changes. We can't get it from the old screen /// state because the page may be cleared. pub hyperlink: Option>, /// The current semantic content type for the cursor that will be /// applied to any newly written cells. pub semantic_content: SemanticContent, pub semantic_content_clear_eol: bool, /// The tracked pin into the page list where the cursor is currently /// located. This makes it faster to move the cursor. Read it with /// [`Screen::cursor_pin`]. pub page_pin: TrackedPin, } impl Cursor { fn new(page_pin: TrackedPin) -> Self { Self { x: 0, y: 0, cursor_style: CursorStyle::Block, pending_wrap: false, protected: false, style: Style::default(), style_id: style::DEFAULT_ID, hyperlink_id: 0, hyperlink_implicit_id: 0, hyperlink: None, semantic_content: SemanticContent::Output, semantic_content_clear_eol: false, page_pin, } } } /// The set of 4 charsets (G0-G3). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct CharsetArray { pub g0: Charset, pub g1: Charset, pub g2: Charset, pub g3: Charset, } impl Default for CharsetArray { fn default() -> Self { Self { g0: Charset::Utf8, g1: Charset::Utf8, g2: Charset::Utf8, g3: Charset::Utf8, } } } impl CharsetArray { #[inline] pub fn get(&self, slot: Slots) -> Charset { match slot { Slots::G0 => self.g0, Slots::G1 => self.g1, Slots::G2 => self.g2, Slots::G3 => self.g3, } } #[inline] pub fn set(&mut self, slot: Slots, charset: Charset) { match slot { Slots::G0 => self.g0 = charset, Slots::G1 => self.g1 = charset, Slots::G2 => self.g2 = charset, Slots::G3 => self.g3 = charset, } } } /// State required for all charset operations. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct CharsetState { /// The list of graphical charsets by slot pub charsets: CharsetArray, /// GL is the slot to use when using a 7-bit printable char (up to 127) /// GR used for 8-bit printable chars. pub gl: Slots, pub gr: Slots, /// Single shift where a slot is used for exactly one char. pub single_shift: Option, } impl Default for CharsetState { fn default() -> Self { Self { charsets: CharsetArray::default(), gl: Slots::G0, gr: Slots::G2, single_shift: None, } } } /// Saved cursor state. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SavedCursor { pub x: size::CellCountInt, pub y: size::CellCountInt, pub style: Style, pub protected: bool, pub pending_wrap: bool, pub origin: bool, pub charset: CharsetState, } #[derive(Debug, Clone, Copy)] pub struct Options { pub cols: size::CellCountInt, pub rows: size::CellCountInt, /// The maximum size of scrollback in bytes. Zero means no scrollback. /// Any other value will be clamped to support a minimum of the active /// area. pub max_scrollback: usize, /// The total storage limit for Kitty images in bytes for this /// screen. Kitty image storage is per-screen. pub kitty_image_storage_limit: usize, } impl Options { /// A simple, default terminal. If you rely on specific dimensions or /// scrollback (or lack of) then do not use this directly. This is just /// for callers that need some defaults. pub const DEFAULT: Options = Options { cols: 80, rows: 24, max_scrollback: 0, kitty_image_storage_limit: 320 * 1000 * 1000, // 320MB }; pub fn new(cols: size::CellCountInt, rows: size::CellCountInt, max_scrollback: usize) -> Self { Self { cols, rows, max_scrollback, ..Self::DEFAULT } } } impl Default for Options { fn default() -> Self { Self::DEFAULT } } /// Options for scrolling the viewport of the terminal grid. The reason /// we have this in addition to PageList.Scroll is because we have additional /// scroll behaviors that are not part of the PageList.Scroll enum. #[derive(Debug, Clone, Copy)] pub enum Scroll { /// For all of these, see PageList.Scroll. Active, Top, Pin(Pin), Row(usize), DeltaRow(isize), DeltaPrompt(isize), } #[derive(Debug, Clone, Copy)] pub struct Resize { /// The new size to resize to pub cols: size::CellCountInt, pub rows: size::CellCountInt, /// Whether to reflow soft-wrapped text. /// /// This will reflow soft-wrapped text. If the screen size is getting /// smaller and the maximum scrollback size is exceeded, data will be /// lost from the top of the scrollback. pub reflow: bool, /// Set this to enable prompt redraw on resize. This signals /// that the running program can redraw the prompt if the cursor is /// currently at a prompt. This detects OSC133 prompts lines and clears /// them. If set to `Last`, only the most recent prompt line is cleared. pub prompt_redraw: Redraw, } impl Resize { pub fn new(cols: size::CellCountInt, rows: size::CellCountInt) -> Self { Self { cols, rows, reflow: true, prompt_redraw: Redraw::False, } } } /// The semantic content setting for `cursor_set_semantic_content`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum InputClear { ClearExplicit, ClearEol, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SemanticContentSet { Prompt(PromptKind), Output, Input(InputClear), } pub use crate::string_map::StringMap; /// Failure of [`Screen::try_selection_string_map`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] pub enum SelectionStringError { #[error("out of memory")] OutOfMemory, } /// Failure points of `selection_string` (Zig: `selectionString_tw`). #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum SelectionStringPoint { CopyMap, } crate::tripwire_module!( selection_string_tw, SelectionStringPoint, SelectionStringError ); #[derive(Debug, Clone, Copy)] pub struct SelectionString { /// The selection to convert to a string. pub sel: Selection, /// If true, trim whitespace around the selection. pub trim: bool, } impl SelectionString { pub fn new(sel: Selection) -> Self { Self { sel, trim: true } } } /// The default whitespace set for line/all selection: NUL, space and tab. pub const DEFAULT_SELECT_WHITESPACE: &[u32] = &[0, ' ' as u32, '\t' as u32]; #[derive(Debug, Clone, Copy)] pub struct SelectLine<'a> { /// The pin of some part of the line to select. pub pin: Pin, /// These are the codepoints to consider whitespace to trim /// from the ends of the selection. pub whitespace: Option<&'a [u32]>, /// If true, line selection will consider semantic prompt /// state changing a boundary. State changing is ANY state /// change. pub semantic_prompt_boundary: bool, } impl SelectLine<'_> { pub fn new(pin: Pin) -> SelectLine<'static> { SelectLine { pin, whitespace: Some(DEFAULT_SELECT_WHITESPACE), semantic_prompt_boundary: true, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct PromptClickMove { pub left: usize, pub right: usize, } impl PromptClickMove { pub const ZERO: PromptClickMove = PromptClickMove { left: 0, right: 0 }; } /// Options for [`Screen::dump_string`]. #[derive(Debug, Clone, Copy)] pub struct DumpString { /// The start and end points of the dump, both inclusive. The x will /// be ignored and the full row will always be dumped. pub tl: Pin, pub br: Option, /// If true, this will unwrap soft-wrapped lines. If false, this will /// dump the screen as it is visually seen in a rendered window. pub unwrap: bool, } impl DumpString { pub fn new(tl: Pin) -> Self { Self { tl, br: None, unwrap: true, } } } /// A screen is a single terminal grid with its own cursor and scrollback. #[derive(Debug)] pub struct Screen { /// The list of pages in the screen. pub pages: PageList, /// Special-case where we want no scrollback whatsoever. We have to flag /// this because max_size 0 in PageList gets rounded up to two pages so /// we can always have an active screen. pub no_scrollback: bool, /// The current cursor position pub cursor: Cursor, /// The saved cursor pub saved_cursor: Option, /// The selection for this screen (if any). This MUST be a tracked /// selection otherwise the selection will become invalid. Instead of /// accessing this directly to set it, use [`Screen::select`] which will /// automatically setup tracking. pub selection: Option, /// The charset state pub charset: CharsetState, /// The current or most recent protected mode. Once a protection mode is /// set, this will never become "off" again until the screen is reset. /// The current state of whether protection attributes should be set is /// set on the Cell pen; this is only used to determine the most recent /// protection mode since some sequences such as ECH depend on this. pub protected_mode: ProtectedMode, /// The kitty keyboard settings. pub kitty_keyboard: FlagStack, /// Kitty graphics protocol state. pub kitty_images: ImageStorage, /// Semantic prompt (OSC133) state. pub semantic_prompt: SemanticPrompt, /// Dirty flags for the renderer. pub dirty: Dirty, } impl Drop for Screen { fn drop(&mut self) { self.kitty_images.deinit(&mut self.pages); } } impl Screen { /// Initialize a new screen. /// /// max_scrollback is the amount of scrollback to keep in bytes. This /// will be rounded UP to the nearest page size because our minimum /// allocation size is that anyways. /// /// If max scrollback is 0, then no scrollback is kept at all. pub fn new(opts: Options) -> Result { // Initialize our backing pages. let mut pages = PageList::init(opts.cols, opts.rows, Some(opts.max_scrollback))?; // Create our tracked pin for the cursor. let first = pages.first.ok_or(Error::InvalidId)?; let page_pin = pages.track_pin(Pin { node: first, y: 0, x: 0, garbage: false, })?; let mut result = Screen { pages, no_scrollback: opts.max_scrollback == 0, cursor: Cursor::new(page_pin), saved_cursor: None, selection: None, charset: CharsetState::default(), protected_mode: ProtectedMode::Off, kitty_keyboard: FlagStack::default(), kitty_images: ImageStorage::default(), semantic_prompt: SemanticPrompt::DISABLED, dirty: Dirty::default(), }; result .kitty_images .set_limit(&mut result.pages, opts.kitty_image_storage_limit) .expect("empty image storage accepts any limit"); Ok(result) } /// The page for a node (panics if the node is stale). pub fn page(&self, node: NodeId) -> &Page { &self.pages.node(node).expect("live page node").data } /// The mutable page for a node (panics if the node is stale). pub fn page_mut(&mut self, node: NodeId) -> &mut Page { &mut self.pages.node_mut(node).expect("live page node").data } /// The current location of the cursor in the page list. pub fn cursor_pin(&self) -> Pin { self.pages .tracked_pin(self.cursor.page_pin) .expect("cursor pin is always tracked") } fn set_cursor_pin(&mut self, p: Pin) { self.pages .set_tracked_pin(self.cursor.page_pin, p) .expect("cursor pin must be valid"); } /// The page the cursor is currently on. pub fn cursor_page(&self) -> &Page { self.page(self.cursor_pin().node) } /// The page the cursor is currently on. pub fn cursor_page_mut(&mut self) -> &mut Page { let node = self.cursor_pin().node; self.page_mut(node) } /// The row the cursor is on (Zig: `cursor.page_row`). pub fn cursor_row(&self) -> Row { let p = self.cursor_pin(); *self .page(p.node) .get_row(usize::from(p.y)) .expect("cursor row") } pub fn cursor_row_mut(&mut self) -> &mut Row { let p = self.cursor_pin(); self.page_mut(p.node) .get_row_mut(usize::from(p.y)) .expect("cursor row") } /// The cell under the cursor (Zig: `cursor.page_cell`). pub fn cursor_cell(&self) -> Cell { let p = self.cursor_pin(); *self .page(p.node) .get_row_and_cell(usize::from(p.x), usize::from(p.y)) .expect("cursor cell") .1 } pub fn cursor_cell_mut(&mut self) -> &mut Cell { let p = self.cursor_pin(); let page = self.page_mut(p.node); let i = page .cell_index(usize::from(p.x), usize::from(p.y)) .expect("cursor cell"); &mut page.cells[i] } /// The cells of the cursor's row. pub fn cursor_row_cells(&self) -> &[Cell] { let p = self.cursor_pin(); self.page(p.node) .get_cells(usize::from(p.y)) .expect("cursor row") } /// Assert that the screen is in a consistent state. This doesn't check /// all pages in the page list because that is SO SLOW even just for /// tests. This only asserts the screen specific data so callers should /// ensure they're also calling page integrity checks if necessary. pub fn assert_integrity(&self) { if cfg!(debug_assertions) { assert!(self.cursor.x < self.pages.cols); assert!(self.cursor.y < self.pages.rows); // Our cursor x/y should always match the pin. If this doesn't // match then it indicates that the tracked pin moved and we // didn't account for it by either calling cursor_reload or // manually adjusting. let pt = self .pages .point_from_pin(Tag::Active, self.cursor_pin()) .expect("cursor pin must be in the active area"); assert_eq!(self.cursor.x, pt.coord().x); assert_eq!(u32::from(self.cursor.y), pt.coord().y); } } /// Reset the screen according to the logic of a DEC RIS sequence. /// /// - Clears the screen and attempts to reclaim memory. /// - Moves the cursor to the top-left. /// - Clears any cursor state: style, hyperlink, etc. /// - Resets the charset /// - Clears the selection /// - Deletes all Kitty graphics /// - Resets Kitty Keyboard settings /// - Disables protection mode pub fn reset(&mut self) -> Result<(), Error> { // Reset our pages self.pages.reset()?; // The above reset preserves tracked pins so we can still use // our cursor pin, which should be at the top-left already. let first = self.pages.first.ok_or(Error::InvalidId)?; let pin = self.cursor_pin(); assert!(pin.node == first); assert!(pin.x == 0); assert!(pin.y == 0); self.set_cursor_pin(Pin { garbage: false, ..pin }); self.cursor = Cursor::new(self.cursor.page_pin); // Reset kitty graphics storage self.kitty_images.deinit(&mut self.pages); self.kitty_images = ImageStorage { dirty: true, ..Default::default() }; // Reset our basic state self.saved_cursor = None; self.charset = CharsetState::default(); self.kitty_keyboard = FlagStack::default(); self.protected_mode = ProtectedMode::Off; self.semantic_prompt = SemanticPrompt::DISABLED; self.clear_selection(); Ok(()) } /// Clone the screen. /// /// This will copy: /// /// - Screen dimensions /// - Screen data (cell state, etc.) for the region /// /// Anything not mentioned above is NOT copied. Some of this is for /// very good reason: /// /// - Kitty images have a LOT of data. This is not efficient to copy. /// Use a lock and access the image data. The dirty bit is there for /// a reason. /// - Cursor location can be expensive to calculate with respect to the /// specified region. It is faster to grab the cursor from the old /// screen and then move it to the new screen. /// - Current hyperlink cursor state has heap allocations. Since clone /// is only for read-only operations, it is better to not have any /// hyperlink state. Note that already-written hyperlinks are cloned. /// /// If not mentioned above, then there isn't a specific reason right now /// to not copy some data other than we probably didn't need it and it /// isn't necessary for screen coherency. /// /// Other notes: /// /// - The viewport will always be set to the active area of the new /// screen. This is the bottom "rows" rows. /// - If the clone region is smaller than a viewport area, blanks will /// be filled in at the bottom. pub fn clone_screen(&self, top: Point, bot: Option) -> Result { // Create a tracked pin remapper for our selection and cursor. let (mut pages, pin_remap) = self.pages.clone_range(top, bot)?; // Find our cursor. If the cursor isn't in the cloned area, we move it // to the top-left arbitrarily because a screen must have SOME cursor. let mut cursor_xy = None; if let Some(&handle) = pin_remap.get(&self.cursor.page_pin) && let Some(p) = pages.tracked_pin(handle) && let Some(pt) = pages.point_from_pin(Tag::Active, p) { cursor_xy = Some((handle, pt.coord().x, pt.coord().y as u16)); } let (page_pin, cx, cy) = match cursor_xy { Some(v) => v, None => { let first = pages.first.ok_or(Error::InvalidId)?; let h = pages.track_pin(Pin { node: first, y: 0, x: 0, garbage: false, })?; (h, 0, 0) } }; let mut cursor = Cursor::new(page_pin); cursor.x = cx; cursor.y = cy; // Preserve our selection if we have one. let sel: Option = if let Some(sel) = self.selection { assert!(sel.tracked()); let Bounds::Tracked { start, end } = sel.bounds else { unreachable!() }; let (tl, br) = match sel.order(&self.pages) { Order::Forward | Order::MirroredForward => (start, end), Order::Reverse | Order::MirroredReverse => (end, start), }; let tl_pin = self.pages.tracked_pin(tl).expect("selection pin"); let br_pin = self.pages.tracked_pin(br).expect("selection pin"); let start_handle = match pin_remap.get(&tl).copied() { Some(h) => Some(h), None => 'start: { // No start means it is outside the cloned area. // If we have no end pin then either // (1) our whole selection is outside the cloned area or // (2) our cloned area is within the selection if !pin_remap.contains_key(&br) { // We check if the selection bottom right pin is above // the cloned area or if the top left pin is below the // cloned area, in either of these cases it means that // the selection is fully out of bounds, so we have no // selection in the cloned area and break out now. let Some(clone_top) = self.pages.pin(top) else { break 'start None; }; let clone_top_y = self .pages .point_from_pin(Tag::Screen, clone_top) .expect("pin in screen") .coord() .y; if self .pages .point_from_pin(Tag::Screen, br_pin) .expect("pin in screen") .coord() .y < clone_top_y { break 'start None; } if self .pages .point_from_pin(Tag::Screen, tl_pin) .expect("pin in screen") .coord() .y > clone_top_y { break 'start None; } } // We move the top pin back in bounds to the top row. let first = pages.first.ok_or(Error::InvalidId)?; Some(pages.track_pin(Pin { node: first, y: 0, x: if sel.rectangle { tl_pin.x } else { 0 }, garbage: false, })?) } }; match start_handle { None => None, Some(start_handle) => { // If we got to this point it means that the selection is // not fully out of bounds, so we move the bottom right pin // back in bounds if it isn't already. let end_handle = match pin_remap.get(&br).copied() { Some(h) => h, None => { let last = pages.last.ok_or(Error::InvalidId)?; let rows = pages.node(last).expect("pin in screen").data.size.rows; pages.track_pin(Pin { node: last, x: if sel.rectangle { br_pin.x } else { pages.cols - 1 }, y: rows - 1, garbage: false, })? } }; Some(Selection { bounds: Bounds::Tracked { start: start_handle, end: end_handle, }, rectangle: sel.rectangle, }) } } } else { None }; let result = Screen { pages, no_scrollback: self.no_scrollback, cursor, saved_cursor: None, selection: sel, charset: CharsetState::default(), protected_mode: ProtectedMode::Off, kitty_keyboard: FlagStack::default(), kitty_images: ImageStorage::default(), semantic_prompt: SemanticPrompt::DISABLED, dirty: self.dirty, }; result.assert_integrity(); Ok(result) } /// Increase the capacity of a page, keeping our cursor state (style and /// hyperlink reference counts) consistent when it is the cursor page. pub fn increase_capacity( &mut self, node: NodeId, adjustment: Option, ) -> Result { // If the page being modified isn't our cursor page then // this is a quick operation because we have no additional // accounting. We have to do this check here BEFORE calling // increase_capacity because increase_capacity will update all // our tracked pins (including our cursor). if node != self.cursor_pin().node { return self.pages.increase_capacity(node, adjustment); } // We're modifying the cursor page. When we increase the // capacity below it will be short the ref count on our // current style and hyperlink, so we need to init those. let new_node = self.pages.increase_capacity(node, adjustment)?; // Re-add the style, if the page somehow doesn't have enough // memory to add it, we emit a warning and gracefully degrade // to the default style for the cursor. if self.cursor.style_id != style::DEFAULT_ID { let st = self.cursor.style; self.cursor.style_id = match self.page_mut(new_node).styles.add(st) { Ok(id) => id, Err(err) => { // TODO: Should we increase the capacity further in this case? log::warn!( "(Screen.increaseCapacity) Failed to add cursor style back to page, err={err:?}" ); // Reset the cursor style. self.cursor.style = Style::default(); style::DEFAULT_ID } }; } // Re-add the hyperlink, if the page somehow doesn't have enough // memory to add it, we emit a warning and gracefully degrade to // no hyperlink. if let Some(link) = self.cursor.hyperlink.take() { // So we don't attempt to free any memory in the replaced page. self.cursor.hyperlink_id = 0; // Re-add if let Err(err) = self.start_hyperlink_once(&link) { // TODO: Should we increase the capacity further in this case? log::warn!( "(Screen.increaseCapacity) Failed to add cursor hyperlink back to page, err={err:?}" ); } } // Reload the cursor information because the pin changed. // So our page row/cell and so on are all off. self.cursor_reload(); Ok(new_node) } /// The cell `n` columns right of the cursor. pub fn cursor_cell_right(&self, n: size::CellCountInt) -> Cell { assert!(self.cursor.x + n < self.pages.cols); self.cursor_row_cells()[usize::from(self.cursor.x + n)] } pub fn cursor_cell_right_mut(&mut self, n: size::CellCountInt) -> &mut Cell { assert!(self.cursor.x + n < self.pages.cols); let p = self.cursor_pin(); let page = self.page_mut(p.node); let i = page .cell_index(usize::from(p.x + n), usize::from(p.y)) .expect("cursor cell"); &mut page.cells[i] } /// The cell `n` columns left of the cursor. pub fn cursor_cell_left(&self, n: size::CellCountInt) -> Cell { assert!(self.cursor.x >= n); self.cursor_row_cells()[usize::from(self.cursor.x - n)] } pub fn cursor_cell_left_mut(&mut self, n: size::CellCountInt) -> &mut Cell { assert!(self.cursor.x >= n); let p = self.cursor_pin(); let page = self.page_mut(p.node); let i = page .cell_index(usize::from(p.x - n), usize::from(p.y)) .expect("cursor cell"); &mut page.cells[i] } /// The last cell of the row above the cursor row. pub fn cursor_cell_end_of_prev(&self) -> Cell { assert!(self.cursor.y > 0); let mut page_pin = self .cursor_pin() .up(&self.pages, 1) .expect("row above cursor"); page_pin.x = self.pages.cols - 1; *page_pin .row_and_cell(&self.pages) .expect("end of prev cell") .1 } /// Move the cursor right. This is a specialized function that is very fast /// if the caller can guarantee we have space to move right (no wrapping). pub fn cursor_right(&mut self, n: size::CellCountInt) { assert!(self.cursor.x + n < self.pages.cols); let mut p = self.cursor_pin(); p.x += n; self.set_cursor_pin(p); self.cursor.x += n; self.assert_integrity(); } /// Move the cursor left. pub fn cursor_left(&mut self, n: size::CellCountInt) { assert!(self.cursor.x >= n); let mut p = self.cursor_pin(); p.x -= n; self.set_cursor_pin(p); self.cursor.x -= n; self.assert_integrity(); } /// Move the cursor up. /// /// Precondition: The cursor is not at the top of the screen. pub fn cursor_up(&mut self, n: size::CellCountInt) { assert!(self.cursor.y >= n); self.cursor.y -= n; // Must be set before cursor_change_pin let new = self .cursor_pin() .up(&self.pages, usize::from(n)) .expect("row above cursor"); self.cursor_change_pin(new); self.assert_integrity(); } /// The row `n` rows above the cursor row. pub fn cursor_row_up(&self, n: size::CellCountInt) -> Row { assert!(self.cursor.y >= n); let page_pin = self .cursor_pin() .up(&self.pages, usize::from(n)) .expect("row above cursor"); *page_pin.row_and_cell(&self.pages).expect("row").0 } /// Move the cursor down. /// /// Precondition: The cursor is not at the bottom of the screen. pub fn cursor_down(&mut self, n: size::CellCountInt) { assert!(u32::from(self.cursor.y) + u32::from(n) < u32::from(self.pages.rows)); self.cursor.y += n; // Must be set before cursor_change_pin // We move the offset into our page list to the next row and then // get the pointers to the row/cell and set all the cursor state up. let new = self .cursor_pin() .down(&self.pages, usize::from(n)) .expect("row below cursor"); self.cursor_change_pin(new); self.assert_integrity(); } /// Move the cursor to some absolute horizontal position. pub fn cursor_horizontal_absolute(&mut self, x: size::CellCountInt) { assert!(x < self.pages.cols); let mut p = self.cursor_pin(); p.x = x; self.set_cursor_pin(p); self.cursor.x = x; self.assert_integrity(); } /// Move the cursor to some absolute position. pub fn cursor_absolute(&mut self, x: size::CellCountInt, y: size::CellCountInt) { assert!(x < self.pages.cols); assert!(y < self.pages.rows); let cur = self.cursor_pin(); let mut page_pin = match y.cmp(&self.cursor.y) { std::cmp::Ordering::Less => cur .up(&self.pages, usize::from(self.cursor.y - y)) .expect("row above cursor"), std::cmp::Ordering::Greater => cur .down(&self.pages, usize::from(y - self.cursor.y)) .expect("row below cursor"), std::cmp::Ordering::Equal => cur, }; page_pin.x = x; self.cursor.x = x; // Must be set before cursor_change_pin self.cursor.y = y; self.cursor_change_pin(page_pin); self.assert_integrity(); } /// Reloads the cursor information into the screen. This is expensive /// so it should only be done in cases where the pointers are invalidated /// in such a way that its difficult to recover otherwise. pub fn cursor_reload(&mut self) { // Our tracked pin is ALWAYS accurate, so we derive the active // point from the pin. If this returns None it means our pin // points outside the active area. In that case, we update the // pin to be the top-left. let pt = match self.pages.point_from_pin(Tag::Active, self.cursor_pin()) { Some(pt) => pt, None => { let pin = self .pages .pin(Point::Active(Coordinate::default())) .expect("active area exists"); self.set_cursor_pin(pin); self.pages .point_from_pin(Tag::Active, pin) .expect("active area") } }; self.cursor.x = pt.coord().x; self.cursor.y = pt.coord().y as size::CellCountInt; // If we have a style, we need to ensure it is in the page because this // method may also be called after a page change. if self.cursor.style_id != style::DEFAULT_ID && let Err(err) = self.manual_style_update() { // This failure should not happen because manual_style_update // handles page splitting, overflow, and more. This should only // happen if we're out of RAM. In this case, we'll just degrade // gracefully back to the default style. log::error!("failed to update style on cursor reload err={err:?}"); self.cursor.style = Style::default(); self.cursor.style_id = 0; } self.assert_integrity(); } /// Scroll the active area and keep the cursor at the bottom of the screen. /// This is a very specialized function but it keeps it fast. pub fn cursor_down_scroll(&mut self) -> Result<(), Error> { assert!(self.cursor.y == self.pages.rows - 1); // Scrolling dirties the images because it updates their placements pins. self.kitty_images.dirty = true; // If we have no scrollback, then we shift all our rows instead. if self.no_scrollback { // If we have a single-row screen, we have no rows to shift // so our cursor is in the correct place we just have to clear // the cells. if self.pages.rows == 1 { let p = self.cursor_pin(); let cols = usize::from(self.pages.cols); self.clear_cells(p.node, p.y, 0, cols); self.cursor_mark_dirty(); } else { // The call to `erase_row` will move the tracked cursor pin up // by one row, but we don't actually want that, so we keep the // old pin and put it back after calling `erase_row`. let old_pin = self.cursor_pin(); // erase_row will shift everything below it up. self.pages.erase_row(Point::Active(Coordinate::default()))?; // Note we don't need to mark anything dirty in this branch // because erase_row will mark all the rotated rows as dirty // in the entire page. // We don't use `cursor_change_pin` here because we aren't // actually changing the pin, we're keeping it the same. self.set_cursor_pin(old_pin); } } else { let old_pin = self.cursor_pin(); // Grow our pages by one row. The PageList will handle if we need to // allocate, prune scrollback, whatever. self.pages.grow()?; let new_pin = { // If our pin page change it means that the page that the pin // was on was pruned. In this case, grow() moves the pin to // the top-left of the new page. This effectively moves it by // one already, we just need to fix up the x value. let cur = self.cursor_pin(); let page_pin = if old_pin.node == cur.node { cur.down(&self.pages, 1).expect("row below cursor") } else { let mut pin = cur; pin.x = self.cursor.x; pin }; // These assertions help catch some pagelist math errors. Our // x/y should be unchanged after the grow. if cfg!(debug_assertions) { let active = self .pages .point_from_pin(Tag::Active, page_pin) .expect("active") .coord(); assert_eq!(active.x, self.cursor.x); assert_eq!(active.y, u32::from(self.cursor.y)); } page_pin }; self.cursor_change_pin(new_pin); // Our new row is always dirty self.cursor_mark_dirty(); // Clear the new row so it gets our bg color. We only do this // if we have a bg color at all. if self.cursor.style.bg_color != style::Color::None { let p = self.cursor_pin(); let cols = usize::from(self.pages.cols); self.clear_cells(p.node, p.y, 0, cols); } } if self.cursor.style_id != style::DEFAULT_ID { // The newly created line needs to be styled according to // the bg color if it is set. self.fill_cursor_row_bg(); } self.assert_integrity(); Ok(()) } /// If the cursor style has a background, fill the whole cursor row with /// the blank background cell. fn fill_cursor_row_bg(&mut self) { if let Some(blank_cell) = self.cursor.style.bg_cell() { self.cursor_page_row_cells_mut().fill(blank_cell); } } fn cursor_page_row_cells_mut(&mut self) -> &mut [Cell] { let p = self.cursor_pin(); self.page_mut(p.node) .get_cells_mut(usize::from(p.y)) .expect("cursor row") } /// This scrolls the active area at and above the cursor. /// The lines below the cursor are not scrolled. pub fn cursor_scroll_above(&mut self) -> Result<(), Error> { // We unconditionally mark the cursor row as dirty here because // the cursor always changes page rows inside this function, and // when that happens it can mean the text in the old row needs to // be re-shaped because the cursor splits runs to break ligatures. self.cursor_mark_dirty(); // If the cursor is on the bottom of the screen, its faster to use // our specialized function for that case. if self.cursor.y == self.pages.rows - 1 { return self.cursor_down_scroll(); } // Logic below assumes we always have at least one row that isn't moving assert!(self.cursor.y < self.pages.rows - 1); // Explanation: // We don't actually move everything that's at or above the cursor row, // since this would require us to shift up our ENTIRE scrollback, which // would be ridiculously expensive. Instead, we insert a new row at the // end of the pagelist (`grow()`), and move everything BELOW the cursor // DOWN by one row. This has the same practical result but it's a whole // lot cheaper in 99% of cases. let old_pin = self.cursor_pin(); if self.pages.grow()?.is_some() { self.cursor_scroll_above_rotate()?; } else { // In this case, it means grow() didn't allocate a new page. if Some(self.cursor_pin().node) == self.pages.last { // If we're on the last page we can do a very fast path because // all the rows we need to move around are within a single page. // Note: we don't need to call cursor_change_pin here because // the pin page is the same so there is no accounting to do // for styles or any of that. assert!(old_pin.node == self.cursor_pin().node); let down = self .cursor_pin() .down(&self.pages, 1) .expect("row below cursor"); self.set_cursor_pin(down); let page = self.page_mut(down.node); // Rotate the rows so that the newly created empty row is at the // beginning. e.g. [ 0 1 2 3 ] in to [ 3 0 1 2 ]. let n = usize::from(page.size.rows); page.rows[usize::from(down.y)..n].rotate_right(1); // Mark the whole page as dirty. // // Technically we only need to mark from the cursor row to the // end but this is a hot function, so we want to minimize work. page.dirty = true; } else { // We didn't grow pages but our cursor isn't on the last page. // In this case we need to do more work because we need to copy // elements between pages. self.cursor_scroll_above_rotate()?; } } if self.cursor.style_id != style::DEFAULT_ID { // The newly created line needs to be styled according to // the bg color if it is set. self.fill_cursor_row_bg(); } self.assert_integrity(); Ok(()) } fn cursor_scroll_above_rotate(&mut self) -> Result<(), Error> { let down = self .cursor_pin() .down(&self.pages, 1) .expect("row below cursor"); self.cursor_change_pin(down); let cursor_pin = self.cursor_pin(); // Go through each of the pages following our pin, shift all rows // down by one, and copy the last row of the previous page. let mut current = self.pages.last.expect("last page"); while current != cursor_pin.node { let prev = self .pages .node(current) .and_then(|n| n.prev) .expect("prev page"); let prev_rows = self.page(prev).size.rows; // Rotate the pages down: [ 0 1 2 3 ] => [ 3 0 1 2 ] let cur_page = self.page_mut(current); let n = usize::from(cur_page.size.rows); cur_page.rows[..n].rotate_right(1); // Copy the last row of the previous page to the top of current. self.pages.clone_row_between( Pin { node: prev, y: prev_rows - 1, x: 0, garbage: false, }, Pin { node: current, y: 0, x: 0, garbage: false, }, )?; // Mark dirty on the page, since we are dirtying all rows with this. self.page_mut(current).dirty = true; current = prev; } // Our current is our cursor page, we need to rotate down from // our cursor and clear our row. assert!(current == cursor_pin.node); let cols = usize::from(self.pages.cols); let cur_page = self.page_mut(current); let n = usize::from(cur_page.size.rows); cur_page.rows[usize::from(cursor_pin.y)..n].rotate_right(1); self.clear_cells(current, cursor_pin.y, 0, cols); // Mark the whole page as dirty. // // Technically we only need to mark from the cursor row to the // end but this is a hot function, so we want to minimize work. self.page_mut(current).dirty = true; Ok(()) } /// Move the cursor down if we're not at the bottom of the screen. Otherwise /// scroll. Currently only used for testing. pub fn cursor_down_or_scroll(&mut self) -> Result<(), Error> { if self.cursor.y + 1 < self.pages.rows { self.cursor_down(1); Ok(()) } else { self.cursor_down_scroll() } } /// Copy another cursor. The cursor can be on any screen but the x/y /// must be within our screen bounds. /// /// `hyperlink`: Copy the hyperlink from the other cursor. If not set, /// this will clear our current hyperlink. pub fn cursor_copy(&mut self, other: &Cursor, hyperlink: bool) -> Result<(), Error> { assert!(other.x < self.pages.cols); assert!(other.y < self.pages.rows); // End any currently active hyperlink on our cursor. self.end_hyperlink(); let old = self.cursor.clone(); let mut new = other.clone(); // Keep our old style ID so it can be properly cleaned up below. new.style_id = old.style_id; // Hyperlinks will be managed separately below. new.hyperlink_id = 0; new.hyperlink = None; // Keep our old page pin and X/Y because: // 1. The old style will need to be cleaned up from the page it's from. // 2. The new position navigated to by `cursor_absolute` needs to be in our // own screen. new.page_pin = old.page_pin; new.x = old.x; new.y = old.y; self.cursor = new; // Call manual style update in order to clean up our old style, if we have // one, and also to load the style from the other cursor, if it had one. if let Err(e) = self.manual_style_update() { self.cursor = old; return Err(e); } // Move to the correct location to match the other cursor. self.cursor_absolute(other.x, other.y); // If the other cursor had a hyperlink, add it to ours. if hyperlink && other.hyperlink_id != 0 && let Some(other_link) = &other.hyperlink { // And it to our cursor. let id = match &other_link.id { HyperlinkId::Explicit(id) => Some(id.clone()), HyperlinkId::Implicit(_) => None, }; if let Err(err) = self.start_hyperlink(&other_link.uri, id.as_deref()) { // This shouldn't happen because start_hyperlink should handle // resizing. This only happens if we're truly out of RAM. Degrade // to forgetting the hyperlink. log::error!("failed to update hyperlink on cursor change err={err:?}"); } } Ok(()) } /// Always use this to write to the cursor pin. /// /// This specifically handles the case when the new pin is on a different /// page than the old AND we have a style or hyperlink set. In that case, /// we must release our old one and insert the new one, since styles are /// stored per-page. /// /// Note that this can change the cursor pin AGAIN if the process of /// setting up our cursor forces a capacity adjustment of the underlying /// cursor page, so any references to the page pin should be re-read /// from `cursor_pin()` after calling this. fn cursor_change_pin(&mut self, new: Pin) { let cur = self.cursor_pin(); // Moving the cursor affects text run splitting (ligatures) so // we must mark the old and new page dirty. We do this as long // as the pins are not equal if !cur.eql(new) { self.cursor_mark_dirty(); new.mark_dirty(&mut self.pages); } // If our pin is on the same page, then we can just update the pin. // We don't need to migrate any state. if cur.node == new.node { self.set_cursor_pin(new); return; } // If we have an old style then we need to release it from the old page. let old_style: Option