Follow-up testing showed that the mailbox limit did not fix the base64 hang: moving the pointer stalled the app, and opening a tab could hang it. Mouse link lookup copied the entire wrapped line before checking modifier eligibility, while renderer link searches had no whole-search work budget. Filter eligible links before traversing scrollback. Add bounded logical line selection and skip automatic regex lookup on lines exceeding 65,536 grid cells. Keep explicit line selection and OSC8 handling unrestricted. Skip oversized lines instead of truncating URLs into different targets. Apply the existing terminal StringMap retry budget to renderer searches and handle search errors without panicking. The regression triggered a retry-limit panic through the old onig search API and now returns normally, allowing subsequent link patterns to run. Document the lookup limits and correct the earlier mailbox fix entry to record that it did not resolve the reported hang on its own. Test Plan: - 1,513 unit/integration tests and one doctest pass across core, renderer, and terminal with Mesa software OpenGL; one existing doctest is ignored. - Release application build, formatting, and all-target Clippy pass. - GTK handles 400 pointer moves, including Ctrl hover, during continuous base64 output, opens/closes a tab, handles Ctrl+C, prints a marker, and exits cleanly. Normal URL hover still works after a hard newline. Reference: docs/rewrite/DEVIANT_FIXES.md (entry 41)
3514 lines
133 KiB
Rust
3514 lines
133 KiB
Rust
//! 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<Box<Hyperlink>>,
|
|
|
|
/// 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<Slots>,
|
|
}
|
|
|
|
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<Pin>,
|
|
|
|
/// 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<SavedCursor>,
|
|
|
|
/// 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<Selection>,
|
|
|
|
/// 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<Screen, Error> {
|
|
// 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<Point>) -> Result<Screen, Error> {
|
|
// 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<Selection> = 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<IncreaseCapacity>,
|
|
) -> Result<NodeId, Error> {
|
|
// 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<Style> = if self.cursor.style_id == style::DEFAULT_ID {
|
|
None
|
|
} else {
|
|
Some(self.cursor.style)
|
|
};
|
|
if old_style.is_some() {
|
|
// Release the style directly from the old page instead of going through
|
|
// manual_style_update, because the cursor position may have already been
|
|
// updated but the pin has not, which would fail integrity checks.
|
|
let id = self.cursor.style_id;
|
|
let _ = self.page_mut(cur.node).styles.release(id);
|
|
self.cursor.style = Style::default();
|
|
self.cursor.style_id = style::DEFAULT_ID;
|
|
}
|
|
|
|
// If we have a hyperlink then we need to release it from the old page.
|
|
if self.cursor.hyperlink.is_some() {
|
|
let id = self.cursor.hyperlink_id;
|
|
let _ = self.page_mut(cur.node).hyperlink_set.release(id);
|
|
}
|
|
|
|
// Update our pin to the new page
|
|
self.set_cursor_pin(new);
|
|
|
|
// On the new page, we need to migrate our style
|
|
if let Some(old_style) = old_style {
|
|
self.cursor.style = old_style;
|
|
if 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 change err={err:?}");
|
|
self.cursor.style = Style::default();
|
|
self.cursor.style_id = 0;
|
|
}
|
|
}
|
|
|
|
// On the new page, we need to migrate our 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
|
|
let id = match &link.id {
|
|
HyperlinkId::Explicit(v) => Some(v.as_slice()),
|
|
HyperlinkId::Implicit(_) => None,
|
|
};
|
|
if let Err(err) = self.start_hyperlink(&link.uri, id) {
|
|
// 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:?}");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Mark the cursor position as dirty.
|
|
pub fn cursor_mark_dirty(&mut self) {
|
|
let pin = self.cursor_pin();
|
|
// A split can temporarily restore the old cursor pin beyond the live
|
|
// row count. Its allocated row still exists, like Zig's cached page_row.
|
|
self.page_mut(pin.node).rows[usize::from(pin.y)].set_dirty(true);
|
|
}
|
|
|
|
/// Reset the cursor row's soft-wrap state and the cursor's pending wrap.
|
|
/// Also handles clearing the spacer head on the cursor row and resetting
|
|
/// the wrap_continuation flag on the next row if necessary.
|
|
///
|
|
/// NOTE(qwerasd): This method is not scrolling region aware, and cannot be
|
|
/// since it's on Screen not Terminal. This needs to be addressed down the
|
|
/// line. Not an extremely urgent issue since it's an edge case of an edge
|
|
/// case, but not ideal.
|
|
pub fn cursor_reset_wrap(&mut self) {
|
|
// Reset the cursor's pending wrap state
|
|
self.cursor.pending_wrap = false;
|
|
|
|
if !self.cursor_row().wrap() {
|
|
return;
|
|
}
|
|
|
|
// This row does not wrap and the next row is not wrapped to
|
|
self.cursor_row_mut().set_wrap(false);
|
|
|
|
let pin = self.cursor_pin();
|
|
if let Some(next_row) = pin.down(&self.pages, 1) {
|
|
self.page_mut(next_row.node)
|
|
.get_row_mut(usize::from(next_row.y))
|
|
.expect("next row")
|
|
.set_wrap_continuation(false);
|
|
}
|
|
|
|
// If the last cell in the row is a spacer head we need to clear it.
|
|
let cols = usize::from(self.page(pin.node).size.cols);
|
|
let cell = self.cursor_row_cells()[cols - 1];
|
|
if cell.wide() == Wide::SpacerHead {
|
|
self.clear_cells(pin.node, pin.y, cols - 1, cols);
|
|
}
|
|
}
|
|
|
|
/// Scroll the viewport of the terminal grid.
|
|
pub fn scroll(&mut self, behavior: Scroll) {
|
|
// No matter what, scrolling marks our image state as dirty since
|
|
// it could move placements. If there are no placements or no images
|
|
// this is still a very cheap operation.
|
|
self.kitty_images.dirty = true;
|
|
|
|
self.pages.scroll(match behavior {
|
|
Scroll::Active => ListScroll::Active,
|
|
Scroll::Top => ListScroll::Top,
|
|
Scroll::Pin(p) => ListScroll::Pin(p),
|
|
Scroll::Row(v) => ListScroll::Row(v),
|
|
Scroll::DeltaRow(v) => ListScroll::DeltaRow(v),
|
|
Scroll::DeltaPrompt(v) => ListScroll::DeltaPrompt(v),
|
|
});
|
|
self.assert_integrity();
|
|
}
|
|
|
|
/// See PageList.scroll_clear. In addition to that, we reset the cursor
|
|
/// to be on top.
|
|
pub fn scroll_clear(&mut self) -> Result<(), Error> {
|
|
self.pages.scroll_clear()?;
|
|
self.cursor_reload();
|
|
|
|
// No matter what, scrolling marks our image state as dirty since
|
|
// it could move placements. If there are no placements or no images
|
|
// this is still a very cheap operation.
|
|
self.kitty_images.dirty = true;
|
|
self.assert_integrity();
|
|
Ok(())
|
|
}
|
|
|
|
/// Returns true if the viewport is scrolled to the bottom of the screen.
|
|
pub fn viewport_is_bottom(&self) -> bool {
|
|
self.pages.viewport == Viewport::Active
|
|
}
|
|
|
|
/// Erase the region specified by tl and br, inclusive. This will physically
|
|
/// erase the rows meaning the memory will be reclaimed (if the underlying
|
|
/// page is empty) and other rows will be shifted up.
|
|
///
|
|
/// Like Zig's `PageList.eraseRows` this requires that `tl` is valid.
|
|
pub fn erase_rows(&mut self, tl: Point, bl: Option<Point>) {
|
|
// Erase the rows
|
|
self.pages.erase_rows(tl, bl).expect("valid erase range");
|
|
|
|
// Just to be safe, reset our cursor since it is possible depending
|
|
// on the points that our active area shifted so our pointers are
|
|
// invalid.
|
|
self.cursor_reload();
|
|
self.assert_integrity();
|
|
}
|
|
|
|
/// Clear the region specified by tl and bl, inclusive. Cleared cells are
|
|
/// colored with the current style background color. This will clear all
|
|
/// cells in the rows.
|
|
///
|
|
/// If protected is true, the protected flag will be respected and only
|
|
/// unprotected cells will be cleared. Otherwise, all cells will be cleared.
|
|
pub fn clear_rows(&mut self, tl: Point, bl: Option<Point>, protected: bool) {
|
|
let chunks: Vec<Chunk> = self
|
|
.pages
|
|
.page_iterator(Direction::RightDown, tl, bl)
|
|
.collect();
|
|
let cols = usize::from(self.pages.cols);
|
|
for chunk in chunks {
|
|
for y in chunk.start..chunk.end {
|
|
let cells_offset = self.page(chunk.node).rows[usize::from(y)].cells();
|
|
|
|
// Clear all cells
|
|
if protected {
|
|
self.clear_unprotected_cells(chunk.node, y, 0, cols);
|
|
// We need to preserve other row attributes since we only
|
|
// cleared unprotected cells.
|
|
self.page_mut(chunk.node).rows[usize::from(y)].set_cells(cells_offset);
|
|
} else {
|
|
self.clear_cells(chunk.node, y, 0, cols);
|
|
let mut row = Row::default();
|
|
row.set_cells(cells_offset);
|
|
self.page_mut(chunk.node).rows[usize::from(y)] = row;
|
|
}
|
|
|
|
self.page_mut(chunk.node).rows[usize::from(y)].set_dirty(true);
|
|
}
|
|
}
|
|
self.assert_integrity();
|
|
}
|
|
|
|
/// Clear the cells `[x0, x1)` of row `y` of the given page with the blank
|
|
/// cell.
|
|
///
|
|
/// This takes care to handle cleaning up graphemes and styles.
|
|
pub fn clear_cells(&mut self, node: NodeId, y: u16, x0: usize, x1: usize) {
|
|
let blank = self.blank_cell();
|
|
let full_row = x1 - x0 == usize::from(self.pages.cols);
|
|
let page = self.page_mut(node);
|
|
let y = usize::from(y);
|
|
let base = page.rows[y].cells();
|
|
assert!(x0 <= x1 && x1 <= usize::from(page.size.cols));
|
|
let row = page.rows[y];
|
|
|
|
// If we have managed memory (styles, graphemes, or hyperlinks)
|
|
// in this row then we go cell by cell and clear them if present.
|
|
if row.grapheme() {
|
|
for i in base + x0..base + x1 {
|
|
if page.cells[i].has_grapheme() {
|
|
page.clear_grapheme(i);
|
|
}
|
|
}
|
|
|
|
// If we have no left/right scroll region we can be sure
|
|
// that we've cleared all the graphemes, so we clear the
|
|
// flag, otherwise we ask the page to update the flag.
|
|
if full_row {
|
|
page.rows[y].set_grapheme(false);
|
|
} else {
|
|
page.update_row_grapheme_flag(y);
|
|
}
|
|
}
|
|
|
|
if row.hyperlink() {
|
|
for i in base + x0..base + x1 {
|
|
if page.cells[i].hyperlink() {
|
|
let _ = page.clear_hyperlink(i);
|
|
}
|
|
}
|
|
|
|
if full_row {
|
|
page.rows[y].set_hyperlink(false);
|
|
} else {
|
|
page.update_row_hyperlink_flag(y);
|
|
}
|
|
}
|
|
|
|
if row.styled() {
|
|
for i in base + x0..base + x1 {
|
|
if page.cells[i].has_styling() {
|
|
let id = page.cells[i].style_id();
|
|
let _ = page.styles.release(id);
|
|
}
|
|
}
|
|
|
|
if full_row {
|
|
page.rows[y].set_styled(false);
|
|
} else {
|
|
page.update_row_styled_flag(y);
|
|
}
|
|
}
|
|
|
|
if row.kitty_virtual_placeholder()
|
|
&& full_row
|
|
&& !page.cells[base + x0..base + x1]
|
|
.iter()
|
|
.any(|c| c.codepoint() == KITTY_PLACEHOLDER)
|
|
{
|
|
page.rows[y].set_kitty_virtual_placeholder(false);
|
|
}
|
|
|
|
page.cells[base + x0..base + x1].fill(blank);
|
|
}
|
|
|
|
/// Clear cells but only if they are not protected.
|
|
pub fn clear_unprotected_cells(&mut self, node: NodeId, y: u16, x0: usize, x1: usize) {
|
|
let base = self.page(node).rows[usize::from(y)].cells();
|
|
let mut i = x0;
|
|
|
|
while i < x1 {
|
|
while self.page(node).cells[base + i].protected() {
|
|
i += 1;
|
|
if i >= x1 {
|
|
self.assert_integrity();
|
|
return;
|
|
}
|
|
}
|
|
let mut j = i + 1;
|
|
while j < x1 && !self.page(node).cells[base + j].protected() {
|
|
j += 1;
|
|
}
|
|
self.clear_cells(node, y, i, j);
|
|
i = j;
|
|
}
|
|
|
|
self.assert_integrity();
|
|
}
|
|
|
|
/// Clean up boundary conditions where a cell will become discontiguous with
|
|
/// a neighboring cell because either one of them will be moved and/or cleared.
|
|
///
|
|
/// For performance reasons this is specialized to operate on the cursor row.
|
|
///
|
|
/// Handles the boundary between the cell at `x` and the cell at `x - 1`.
|
|
///
|
|
/// So, for example, when moving a region of cells [a, b] (inclusive), call this
|
|
/// function with `x = a` and `x = b + 1`. It is okay if `x` is out of bounds by
|
|
/// 1, this will be interpreted correctly.
|
|
///
|
|
/// DOES NOT MODIFY ROW WRAP STATE! See `cursor_reset_wrap` for that.
|
|
///
|
|
/// The following boundary conditions are handled:
|
|
///
|
|
/// - `x - 1` is a wide character and `x` is a spacer tail:
|
|
/// o Both cells will be cleared.
|
|
/// o If `x - 1` is the start of the row and was wrapped from a previous row
|
|
/// then the previous row is checked for a spacer head, which is cleared if
|
|
/// present.
|
|
///
|
|
/// - `x == 0` and is a wide character:
|
|
/// o If the row is a wrap continuation then the previous row will be checked
|
|
/// for a spacer head, which is cleared if present.
|
|
///
|
|
/// - `x == cols` and `x - 1` is a spacer head:
|
|
/// o `x - 1` will be cleared.
|
|
///
|
|
/// NOTE(qwerasd): This method is not scrolling region aware, and cannot be
|
|
/// since it's on Screen not Terminal. This needs to be addressed down the
|
|
/// line. Not an extremely urgent issue since it's an edge case of an edge
|
|
/// case, but not ideal.
|
|
pub fn split_cell_boundary(&mut self, x: size::CellCountInt) {
|
|
let pin = self.cursor_pin();
|
|
let cols = self.page(pin.node).size.cols;
|
|
|
|
// `x` may be up to an INCLUDING `cols`, since that signifies splitting
|
|
// the boundary to the right of the final cell in the row.
|
|
assert!(x <= cols);
|
|
|
|
// [ A B C D E F|]
|
|
// ^ Boundary between final cell and row end.
|
|
if x == cols {
|
|
if !self.cursor_row().wrap() {
|
|
return;
|
|
}
|
|
|
|
// Spacer head at end of wrapped row.
|
|
if self.cursor_row_cells()[usize::from(cols - 1)].wide() == Wide::SpacerHead {
|
|
self.clear_cells(pin.node, pin.y, usize::from(cols - 1), usize::from(cols));
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// [|A B C D E F ]
|
|
// ^ Boundary between first cell and row start.
|
|
//
|
|
// OR
|
|
//
|
|
// [ A|B C D E F ]
|
|
// ^ Boundary between first cell and second cell.
|
|
//
|
|
// First cell may be a wrapped wide cell with a spacer
|
|
// head on the previous row that needs to be cleared.
|
|
if (x == 0 || x == 1) && self.cursor_row().wrap_continuation() {
|
|
// If the first cell in a row is wide the previous row
|
|
// may have a spacer head which needs to be cleared.
|
|
if self.cursor_row_cells()[0].wide() == Wide::Wide
|
|
&& let Some(p_row) = pin.up(&self.pages, 1)
|
|
{
|
|
let p_cols = self.page(p_row.node).size.cols;
|
|
let p_cell = self
|
|
.page(p_row.node)
|
|
.get_cells(usize::from(p_row.y))
|
|
.expect("row")[usize::from(p_cols - 1)];
|
|
if p_cell.wide() == Wide::SpacerHead {
|
|
self.clear_cells(
|
|
p_row.node,
|
|
p_row.y,
|
|
usize::from(p_cols - 1),
|
|
usize::from(p_cols),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// If x is 0 then we're done.
|
|
if x == 0 {
|
|
return;
|
|
}
|
|
|
|
// [ ... X|Y ... ]
|
|
// ^ Boundary between two cells in the middle of the row.
|
|
{
|
|
assert!(x > 0);
|
|
assert!(x < cols);
|
|
|
|
let left = self.cursor_row_cells()[usize::from(x - 1)];
|
|
match left.wide() {
|
|
// There should not be spacer heads in the middle of the row.
|
|
Wide::SpacerHead => unreachable!("spacer head in the middle of a row"),
|
|
|
|
// We don't need to do anything for narrow cells or spacer tails.
|
|
Wide::Narrow | Wide::SpacerTail => {}
|
|
|
|
// A wide char would be split, so must be cleared.
|
|
Wide::Wide => {
|
|
self.clear_cells(pin.node, pin.y, usize::from(x - 1), usize::from(x + 1));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns the blank cell to use when doing terminal operations that
|
|
/// require preserving the bg color.
|
|
#[inline]
|
|
pub fn blank_cell(&self) -> Cell {
|
|
if self.cursor.style_id == style::DEFAULT_ID {
|
|
return Cell::default();
|
|
}
|
|
self.cursor.style.bg_cell().unwrap_or_default()
|
|
}
|
|
}
|
|
|
|
impl Screen {
|
|
/// Resize the screen. The rows or cols can be bigger or smaller.
|
|
///
|
|
/// If this returns an error, the screen is left in a likely garbage state.
|
|
/// It is very hard to undo this operation without blowing up our memory
|
|
/// usage. The only way to recover is to reset the screen. The only way
|
|
/// this really fails is if page allocation is required and fails, which
|
|
/// probably means the system is in trouble anyways. I'd like to improve this
|
|
/// in the future but it is not a priority particularly because this scenario
|
|
/// (resize) is difficult.
|
|
pub fn resize(&mut self, opts: Resize) -> Result<(), Error> {
|
|
// No matter what we mark our image state as dirty
|
|
self.kitty_images.dirty = true;
|
|
|
|
// Release the cursor style while resizing just
|
|
// in case the cursor ends up on a different page.
|
|
let cursor_style = self.cursor.style;
|
|
self.cursor.style = Style::default();
|
|
self.manual_style_update()
|
|
.expect("releasing to the default style cannot fail");
|
|
|
|
// We need to insert a tracked pin for our saved cursor so we can
|
|
// modify its X/Y for reflow.
|
|
let saved_cursor_pin = match self.saved_cursor {
|
|
Some(sc) => match self.pages.pin(Point::Active(Coordinate {
|
|
x: sc.x,
|
|
y: u32::from(sc.y),
|
|
})) {
|
|
Some(pin) => match self.pages.track_pin(pin) {
|
|
Ok(h) => Some(h),
|
|
Err(e) => {
|
|
self.restore_cursor_style(cursor_style);
|
|
self.assert_integrity();
|
|
return Err(e);
|
|
}
|
|
},
|
|
None => None,
|
|
},
|
|
None => None,
|
|
};
|
|
|
|
let result = self.resize_inner(opts, saved_cursor_pin);
|
|
if let Some(p) = saved_cursor_pin {
|
|
self.pages.untrack_pin(p);
|
|
}
|
|
|
|
// Restore the cursor style.
|
|
self.restore_cursor_style(cursor_style);
|
|
self.assert_integrity();
|
|
result
|
|
}
|
|
|
|
fn restore_cursor_style(&mut self, cursor_style: Style) {
|
|
self.cursor.style = cursor_style;
|
|
if 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;
|
|
}
|
|
}
|
|
|
|
fn resize_inner(
|
|
&mut self,
|
|
opts: Resize,
|
|
saved_cursor_pin: Option<TrackedPin>,
|
|
) -> Result<(), Error> {
|
|
// If we have a hyperlink, release it from the old page
|
|
// and then we need to re-add it to the new page. This needs
|
|
// to happen because resize below typically reallocates a
|
|
// new page so the old hyperlink is invalid.
|
|
let hyperlink_ = self.cursor.hyperlink.take();
|
|
if self.cursor.hyperlink_id != 0 {
|
|
// Note we do NOT use end_hyperlink because we want to keep
|
|
// our allocated hyperlink valid.
|
|
let id = self.cursor.hyperlink_id;
|
|
let node = self.cursor_pin().node;
|
|
let _ = self.page_mut(node).hyperlink_set.release(id);
|
|
self.cursor.hyperlink_id = 0;
|
|
}
|
|
|
|
// If our cursor is on a prompt or input line, clear it so the shell can
|
|
// redraw it. This works with OSC 133 semantic prompts.
|
|
//
|
|
// We check cursor.semantic_content rather than page_row.semantic_prompt
|
|
// because some shells (e.g., Nu) mark input areas with OSC 133 B but don't
|
|
// mark continuation lines with k=s. If the input spans multiple lines and
|
|
// continuation lines are unmarked, checking only page_row.semantic_prompt
|
|
// would miss them. By checking semantic_content, we assume that if the
|
|
// cursor is on anything other than command output, we're at a prompt/input
|
|
// line and should clear from there.
|
|
if opts.prompt_redraw != Redraw::False
|
|
&& self.cursor.semantic_content != SemanticContent::Output
|
|
{
|
|
let cols = usize::from(self.pages.cols);
|
|
match opts.prompt_redraw {
|
|
Redraw::False => unreachable!(),
|
|
|
|
// For `Last`, only clear the current line where the cursor is.
|
|
// For `True`, clear all prompt lines starting from the beginning.
|
|
Redraw::Last => {
|
|
let p = self.cursor_pin();
|
|
self.clear_cells(p.node, p.y, 0, cols);
|
|
}
|
|
|
|
Redraw::True => {
|
|
let start = self
|
|
.cursor_pin()
|
|
.prompt_iterator(&self.pages, Direction::LeftUp, None)
|
|
.next();
|
|
if let Some(start) = start {
|
|
// Clear cells from our start down. We replace it with spaces,
|
|
// and do not physically erase the rows (erase_rows) because the
|
|
// shell is going to expect this space to be available.
|
|
let rows: Vec<Pin> = start
|
|
.row_iterator(&self.pages, Direction::RightDown, None)
|
|
.collect();
|
|
for pin in rows {
|
|
self.clear_cells(pin.node, pin.y, 0, cols);
|
|
}
|
|
} else {
|
|
// This should never happen because prompt_iterator should always
|
|
// find a prompt if we already verified our row is some kind of
|
|
// prompt.
|
|
log::warn!("cursor on prompt line but promptIterator found no prompt");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Perform the resize operation.
|
|
self.pages.resize(crate::page_list::Resize {
|
|
rows: Some(opts.rows),
|
|
cols: Some(opts.cols),
|
|
reflow: opts.reflow,
|
|
cursor: Some(crate::page_list::Cursor {
|
|
x: self.cursor.x,
|
|
y: self.cursor.y,
|
|
}),
|
|
})?;
|
|
|
|
// If we have no scrollback and we shrunk our rows, we must explicitly
|
|
// erase our history. This is because PageList always keeps at least
|
|
// a page size of history.
|
|
if self.no_scrollback {
|
|
self.pages
|
|
.erase_rows(Point::History(Coordinate::default()), None)
|
|
.expect("valid history range");
|
|
}
|
|
|
|
// If our cursor was updated, we do a full reload so all our cursor
|
|
// state is correct.
|
|
self.cursor_reload();
|
|
|
|
// If we reflowed a saved cursor, update it.
|
|
if let Some(p) = saved_cursor_pin {
|
|
// This should never fail because a non-null saved_cursor_pin
|
|
// implies a non-null saved_cursor.
|
|
let pin = self.pages.tracked_pin(p);
|
|
let pt = pin.and_then(|pin| self.pages.point_from_pin(Tag::Active, pin));
|
|
if let Some(sc) = self.saved_cursor.as_mut() {
|
|
if let Some(pt) = pt {
|
|
sc.x = pt.coord().x;
|
|
sc.y = pt.coord().y as size::CellCountInt;
|
|
|
|
// If we had pending wrap set and we're no longer at the end of
|
|
// the line, we unset the pending wrap and move the cursor to
|
|
// reflect the correct next position.
|
|
if sc.pending_wrap && sc.x != opts.cols - 1 {
|
|
sc.pending_wrap = false;
|
|
sc.x += 1;
|
|
}
|
|
} else {
|
|
// I think this can happen if the screen is resized to be
|
|
// less rows or less cols and our saved cursor moves outside
|
|
// the active area. In this case, there isn't anything really
|
|
// reasonable we can do so we just move the cursor to the
|
|
// top-left. It may be reasonable to also move the cursor to
|
|
// match the primary cursor. Any behavior is fine since this is
|
|
// totally unspecified.
|
|
sc.x = 0;
|
|
sc.y = 0;
|
|
sc.pending_wrap = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fix up our hyperlink if we had one.
|
|
if let Some(link) = hyperlink_ {
|
|
let id = match &link.id {
|
|
HyperlinkId::Explicit(v) => Some(v.as_slice()),
|
|
HyperlinkId::Implicit(_) => None,
|
|
};
|
|
if let Err(err) = self.start_hyperlink(&link.uri, id) {
|
|
// 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 resize err={err:?}");
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Set a style attribute for the current cursor.
|
|
///
|
|
/// If the style can't be set due to any internal errors (memory-related),
|
|
/// then this will revert back to the existing style and return an error.
|
|
pub fn set_attribute(&mut self, attr: sgr::Attribute<'_>) -> Result<(), Error> {
|
|
// If we fail to set our style for any reason, we should revert
|
|
// back to the old style. If we fail to do that, we revert back to
|
|
// the default style.
|
|
let old_style = self.cursor.style;
|
|
|
|
use sgr::Attribute as A;
|
|
let st = &mut self.cursor.style;
|
|
match attr {
|
|
A::Unset => *st = Style::default(),
|
|
A::Bold => st.flags.bold = true,
|
|
A::ResetBold => {
|
|
// Bold and faint share the same SGR code for this
|
|
st.flags.bold = false;
|
|
st.flags.faint = false;
|
|
}
|
|
A::Italic => st.flags.italic = true,
|
|
A::ResetItalic => st.flags.italic = false,
|
|
A::Faint => st.flags.faint = true,
|
|
A::Underline(v) => st.flags.underline = v,
|
|
A::UnderlineColor(rgb) => st.underline_color = style::Color::Rgb(rgb),
|
|
A::UnderlineColor256(idx) => st.underline_color = style::Color::Palette(idx),
|
|
A::ResetUnderlineColor => st.underline_color = style::Color::None,
|
|
A::Overline => st.flags.overline = true,
|
|
A::ResetOverline => st.flags.overline = false,
|
|
A::Blink => st.flags.blink = true,
|
|
A::ResetBlink => st.flags.blink = false,
|
|
A::Inverse => st.flags.inverse = true,
|
|
A::ResetInverse => st.flags.inverse = false,
|
|
A::Invisible => st.flags.invisible = true,
|
|
A::ResetInvisible => st.flags.invisible = false,
|
|
A::Strikethrough => st.flags.strikethrough = true,
|
|
A::ResetStrikethrough => st.flags.strikethrough = false,
|
|
A::DirectColorFg(rgb) => st.fg_color = style::Color::Rgb(rgb),
|
|
A::DirectColorBg(rgb) => st.bg_color = style::Color::Rgb(rgb),
|
|
A::Fg8(n) => st.fg_color = style::Color::Palette(n.0),
|
|
A::Bg8(n) => st.bg_color = style::Color::Palette(n.0),
|
|
A::ResetFg => st.fg_color = style::Color::None,
|
|
A::ResetBg => st.bg_color = style::Color::None,
|
|
A::Fg8Bright(n) => st.fg_color = style::Color::Palette(n.0),
|
|
A::Bg8Bright(n) => st.bg_color = style::Color::Palette(n.0),
|
|
A::Fg256(idx) => st.fg_color = style::Color::Palette(idx),
|
|
A::Bg256(idx) => st.bg_color = style::Color::Palette(idx),
|
|
A::Unknown(_) => return Ok(()),
|
|
}
|
|
|
|
if let Err(e) = self.manual_style_update() {
|
|
self.cursor.style = old_style;
|
|
if let Err(err) = self.manual_style_update() {
|
|
log::warn!("setAttribute error restoring old style after failure err={err:?}");
|
|
self.cursor.style = Style::default();
|
|
let _ = self.manual_style_update();
|
|
}
|
|
return Err(e);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Call this whenever you manually change the cursor style.
|
|
///
|
|
/// This function can NOT fail if the cursor style is changing to the
|
|
/// default style.
|
|
///
|
|
/// If this returns an error, the style change did not take effect and
|
|
/// the cursor style is reverted back to the default. The only scenario
|
|
/// this returns an error is if there is a physical memory allocation failure
|
|
/// or if there is no possible way to increase style capacity to store
|
|
/// the style.
|
|
///
|
|
/// This function WILL split pages as necessary to accommodate the new style.
|
|
/// So if OutOfSpace is returned, it means that even after splitting the page
|
|
/// there was still no room for the new style.
|
|
pub fn manual_style_update(&mut self) -> Result<(), Error> {
|
|
let result = self.manual_style_update_inner();
|
|
self.assert_integrity();
|
|
result
|
|
}
|
|
|
|
fn manual_style_update_inner(&mut self) -> Result<(), Error> {
|
|
let node = self.cursor_pin().node;
|
|
|
|
// Release our previous style if it was not default.
|
|
if self.cursor.style_id != style::DEFAULT_ID {
|
|
let id = self.cursor.style_id;
|
|
let _ = self.page_mut(node).styles.release(id);
|
|
}
|
|
|
|
// If our new style is the default, just reset to that
|
|
if self.cursor.style.is_default() {
|
|
self.cursor.style_id = style::DEFAULT_ID;
|
|
return Ok(());
|
|
}
|
|
|
|
// Clear the cursor style ID to prevent weird things from happening
|
|
// if the page capacity has to be adjusted which would end up calling
|
|
// manual_style_update again.
|
|
//
|
|
// This also ensures that if anything fails below, we fall back to
|
|
// clearing our style.
|
|
self.cursor.style_id = style::DEFAULT_ID;
|
|
|
|
// After setting the style, we need to update our style map.
|
|
// Note that we COULD lazily do this in print. We should look into
|
|
// if that makes a meaningful difference. Our priority is to keep print
|
|
// fast because setting a ton of styles that do nothing is uncommon
|
|
// and weird.
|
|
let st = self.cursor.style;
|
|
let id = match self.page_mut(node).styles.add(st) {
|
|
Ok(id) => id,
|
|
Err(err) => {
|
|
// Our style map is full or needs to be rehashed, so we need to
|
|
// increase style capacity (or rehash).
|
|
let adjustment = match err {
|
|
crate::ref_counted_set::AddError::OutOfMemory => Some(IncreaseCapacity::Styles),
|
|
crate::ref_counted_set::AddError::NeedsRehash => None,
|
|
other => return Err(other.into()),
|
|
};
|
|
let node = match self.increase_capacity(self.cursor_pin().node, adjustment) {
|
|
Ok(n) => n,
|
|
Err(Error::OutOfSpace) => {
|
|
// Out of space, we need to split the page. Split wherever
|
|
// is using less capacity and hope that works. If it doesn't
|
|
// work, we tried.
|
|
self.split_for_capacity(self.cursor_pin())?;
|
|
self.cursor_pin().node
|
|
}
|
|
Err(e) => return Err(e),
|
|
};
|
|
|
|
match self.page_mut(node).styles.add(st) {
|
|
Ok(id) => id,
|
|
Err(crate::ref_counted_set::AddError::NeedsRehash) => {
|
|
// This should be impossible because we rehash above
|
|
// and rehashing should never result in a duplicate. But
|
|
// we don't want to simply hard crash so log it and
|
|
// clear our style.
|
|
log::error!("style rehash resulted in needs rehash");
|
|
return Ok(());
|
|
}
|
|
Err(crate::ref_counted_set::AddError::OutOfMemory) => {
|
|
// This shouldn't happen because increase_capacity is
|
|
// guaranteed to increase our capacity by at least one and
|
|
// we only need one space, but again, I don't want to crash
|
|
// here so let's log loudly and reset.
|
|
log::error!("style addition failed after capacity increase");
|
|
return Err(Error::OutOfMemory);
|
|
}
|
|
Err(e) => return Err(e.into()),
|
|
}
|
|
}
|
|
};
|
|
|
|
self.cursor.style_id = id;
|
|
Ok(())
|
|
}
|
|
|
|
/// Split at the given pin so that the pinned row moves to the page
|
|
/// with less used capacity after the split.
|
|
///
|
|
/// The primary use case for this is to handle IncreaseCapacityError
|
|
/// OutOfSpace conditions where we need to split the page in order
|
|
/// to make room for more managed memory.
|
|
///
|
|
/// If the caller cares about where the pin moves to, they should
|
|
/// setup a tracked pin before calling this and then check that.
|
|
/// In many calling cases, the input pin is tracked (e.g. the cursor
|
|
/// pin).
|
|
///
|
|
/// If this returns OOM then its a system OOM. If this returns OutOfSpace
|
|
/// then it means the page can't be split further.
|
|
fn split_for_capacity(&mut self, pin: Pin) -> Result<(), Error> {
|
|
// Get our capacities. We include our target row because its
|
|
// capacity will be preserved.
|
|
let (bytes_above, bytes_below) = {
|
|
let page = self.page(pin.node);
|
|
let above =
|
|
Page::layout(page.exact_row_capacity(0, usize::from(pin.y) + 1)?).total_size;
|
|
let below = Page::layout(
|
|
page.exact_row_capacity(usize::from(pin.y), usize::from(page.size.rows))?,
|
|
)
|
|
.total_size;
|
|
(above, below)
|
|
};
|
|
|
|
// We need to track the old cursor pin because if our split
|
|
// moves the cursor pin we need to update our accounting.
|
|
let old_cursor = self.cursor_pin();
|
|
|
|
// If our bytes above are less than bytes below, we move the pin
|
|
// to split down one since splitting includes the pinned row in
|
|
// the new node.
|
|
self.pages.split(if bytes_above < bytes_below {
|
|
pin.down(&self.pages, 1).unwrap_or(pin)
|
|
} else {
|
|
pin
|
|
})?;
|
|
|
|
// Cursor didn't change nodes, we're done.
|
|
if self.cursor_pin().node == old_cursor.node {
|
|
return Ok(());
|
|
}
|
|
|
|
// Cursor changed, we need to restore the old pin then use
|
|
// cursor_change_pin to move to the new pin. The old node is guaranteed
|
|
// to still exist, just not the row.
|
|
let new_cursor = self.cursor_pin();
|
|
self.set_cursor_pin(old_cursor);
|
|
self.cursor_change_pin(new_cursor);
|
|
Ok(())
|
|
}
|
|
|
|
/// Append a grapheme to the given cell (column `x`) within the current
|
|
/// cursor row.
|
|
pub fn append_grapheme(&mut self, x: size::CellCountInt, cp: u32) -> Result<(), Error> {
|
|
let result = {
|
|
let p = self.cursor_pin();
|
|
self.page_mut(p.node)
|
|
.append_grapheme(usize::from(x), usize::from(p.y), cp)
|
|
};
|
|
match result {
|
|
Err(Error::GraphemeAllocOutOfMemory | Error::GraphemeMapOutOfMemory) => {
|
|
// Adjust our capacity. This will update our cursor page pin and
|
|
// force us to reload. The cell is addressed by column so it stays
|
|
// valid.
|
|
let node = self.cursor_pin().node;
|
|
self.increase_capacity(node, Some(IncreaseCapacity::GraphemeBytes))?;
|
|
|
|
let p = self.cursor_pin();
|
|
self.page_mut(p.node)
|
|
.append_grapheme(usize::from(x), usize::from(p.y), cp)
|
|
.inspect_err(|_| {
|
|
// This should never happen because we just increased capacity.
|
|
// Log loudly but still return an error so we don't just
|
|
// crash.
|
|
log::error!("grapheme append failed after capacity increase");
|
|
})
|
|
}
|
|
other => other,
|
|
}
|
|
}
|
|
|
|
/// Start the hyperlink state. Future cells will be marked as hyperlinks with
|
|
/// this state. Note that various terminal operations may clear the hyperlink
|
|
/// state, such as switching screens (alt screen).
|
|
pub fn start_hyperlink(&mut self, uri: &[u8], id: Option<&[u8]>) -> Result<(), Error> {
|
|
// Create our pending entry.
|
|
let implicit = id.is_none();
|
|
let link = Hyperlink {
|
|
uri: uri.to_vec(),
|
|
id: match id {
|
|
Some(id) => HyperlinkId::Explicit(id.to_vec()),
|
|
None => {
|
|
let v = self.cursor.hyperlink_implicit_id;
|
|
self.cursor.hyperlink_implicit_id = v.wrapping_add(1);
|
|
HyperlinkId::Implicit(v)
|
|
}
|
|
},
|
|
};
|
|
|
|
// Loop until we have enough page memory to add the hyperlink
|
|
loop {
|
|
let adjustment = match self.start_hyperlink_once(&link) {
|
|
Ok(()) => return Ok(()),
|
|
// strings table is out of memory, adjust it up
|
|
Err(Error::StringsOutOfMemory) => Some(IncreaseCapacity::StringBytes),
|
|
// hyperlink set is out of memory, adjust it up
|
|
Err(Error::SetOutOfMemory) => Some(IncreaseCapacity::HyperlinkBytes),
|
|
// hyperlink set is too full, rehash it
|
|
Err(Error::SetNeedsRehash) => None,
|
|
Err(e) => {
|
|
if implicit {
|
|
self.cursor.hyperlink_implicit_id =
|
|
self.cursor.hyperlink_implicit_id.wrapping_sub(1);
|
|
}
|
|
return Err(e);
|
|
}
|
|
};
|
|
|
|
let node = self.cursor_pin().node;
|
|
if let Err(e) = self.increase_capacity(node, adjustment) {
|
|
if implicit {
|
|
self.cursor.hyperlink_implicit_id =
|
|
self.cursor.hyperlink_implicit_id.wrapping_sub(1);
|
|
}
|
|
return Err(e);
|
|
}
|
|
|
|
self.assert_integrity();
|
|
}
|
|
}
|
|
|
|
/// This is like start_hyperlink but if we have to adjust page capacities
|
|
/// this returns an error. This is useful so that we unwind all the
|
|
/// previous state and try again.
|
|
fn start_hyperlink_once(&mut self, source: &Hyperlink) -> Result<(), Error> {
|
|
// End any prior hyperlink
|
|
self.end_hyperlink();
|
|
|
|
// Allocate our new Hyperlink entry in non-page memory. This
|
|
// lets us quickly get access to URI, ID.
|
|
let link = Box::new(source.dupe());
|
|
|
|
// Insert the hyperlink into page memory
|
|
let node = self.cursor_pin().node;
|
|
let id: hyperlink::Id = self.page_mut(node).insert_hyperlink((*link).clone())?;
|
|
|
|
// Save it all
|
|
self.cursor.hyperlink = Some(link);
|
|
self.cursor.hyperlink_id = id;
|
|
Ok(())
|
|
}
|
|
|
|
/// End the hyperlink state so that future cells aren't part of the
|
|
/// current hyperlink (if any). This is safe to call multiple times.
|
|
pub fn end_hyperlink(&mut self) {
|
|
// If we have no hyperlink state then do nothing
|
|
if self.cursor.hyperlink_id == 0 {
|
|
assert!(self.cursor.hyperlink.is_none());
|
|
return;
|
|
}
|
|
|
|
// Release the old hyperlink state. If there are cells using the
|
|
// hyperlink this will work because the creation creates a reference
|
|
// and all additional cells create a new reference. This release will
|
|
// just release our initial reference.
|
|
//
|
|
// If the ref count reaches zero the set will not delete the item
|
|
// immediately; it is kept around in case it is used again (this is
|
|
// how RefCountedSet works). This causes some memory fragmentation but
|
|
// is fine because if it is ever pruned the context deleted callback
|
|
// will be called.
|
|
let id = self.cursor.hyperlink_id;
|
|
let node = self.cursor_pin().node;
|
|
let _ = self.page_mut(node).hyperlink_set.release(id);
|
|
self.cursor.hyperlink_id = 0;
|
|
self.cursor.hyperlink = None;
|
|
}
|
|
|
|
/// Set the current hyperlink state on the current cell.
|
|
pub fn cursor_set_hyperlink(&mut self) -> Result<(), Error> {
|
|
assert!(self.cursor.hyperlink_id != 0);
|
|
|
|
loop {
|
|
let p = self.cursor_pin();
|
|
let id = self.cursor.hyperlink_id;
|
|
let page = self.page_mut(p.node);
|
|
match page.set_hyperlink(usize::from(p.x), usize::from(p.y), id) {
|
|
Ok(()) => {
|
|
// Success, increase the refcount for the hyperlink.
|
|
let _ = page.hyperlink_set.r#use(id);
|
|
return Ok(());
|
|
}
|
|
|
|
// hyperlink_map is out of space, realloc the page to be larger
|
|
Err(Error::HyperlinkMapOutOfMemory) => {
|
|
// Probe the URI allocation before rebuilding, just as
|
|
// cursorSetHyperlink does in the reference implementation.
|
|
while let Some(link) = &self.cursor.hyperlink {
|
|
let len = link.uri.len();
|
|
let node = self.cursor_pin().node;
|
|
if self.page_mut(node).can_allocate_string(len) {
|
|
break;
|
|
}
|
|
self.increase_capacity(node, Some(IncreaseCapacity::StringBytes))?;
|
|
}
|
|
let node = self.cursor_pin().node;
|
|
self.increase_capacity(node, Some(IncreaseCapacity::HyperlinkBytes))?;
|
|
|
|
// Retry
|
|
//
|
|
// We check that the cursor hyperlink hasn't been destroyed
|
|
// by the capacity adjustment first though- since despite the
|
|
// terrible code above, that can still apparently happen ._.
|
|
if self.cursor.hyperlink_id > 0 {
|
|
continue;
|
|
}
|
|
return Ok(());
|
|
}
|
|
|
|
Err(e) => return Err(e),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Modify the semantic content type of the cursor. This should
|
|
/// be preferred over setting it manually since it handles all the
|
|
/// proper accounting.
|
|
pub fn cursor_set_semantic_content(&mut self, t: SemanticContentSet) {
|
|
match t {
|
|
SemanticContentSet::Output => {
|
|
self.cursor.semantic_content = SemanticContent::Output;
|
|
self.cursor.semantic_content_clear_eol = false;
|
|
}
|
|
|
|
SemanticContentSet::Input(clear) => {
|
|
self.cursor.semantic_content = SemanticContent::Input;
|
|
self.cursor.semantic_content_clear_eol = match clear {
|
|
InputClear::ClearExplicit => false,
|
|
InputClear::ClearEol => true,
|
|
};
|
|
}
|
|
|
|
SemanticContentSet::Prompt(kind) => {
|
|
self.semantic_prompt.seen = true;
|
|
self.cursor.semantic_content = SemanticContent::Prompt;
|
|
self.cursor.semantic_content_clear_eol = false;
|
|
self.cursor_row_mut().set_semantic_prompt(match kind {
|
|
PromptKind::Initial | PromptKind::Right => RowPrompt::Prompt,
|
|
PromptKind::Continuation | PromptKind::Secondary => {
|
|
RowPrompt::PromptContinuation
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Set the selection to the given selection. If this is a tracked selection
|
|
/// then the screen will take ownership of the selection. If this is untracked
|
|
/// then the screen will convert it to tracked internally. This will automatically
|
|
/// untrack the prior selection (if any).
|
|
///
|
|
/// Set the selection to None to clear any previous selection.
|
|
///
|
|
/// This is always recommended over setting `selection` directly. Beyond
|
|
/// managing memory for you, it also performs safety checks that the selection
|
|
/// is always tracked.
|
|
pub fn select(&mut self, sel: Option<Selection>) -> Result<(), Error> {
|
|
let Some(sel) = sel else {
|
|
self.clear_selection();
|
|
return Ok(());
|
|
};
|
|
|
|
// If this selection is untracked then we track it.
|
|
let tracked_sel = if sel.tracked() {
|
|
sel
|
|
} else {
|
|
sel.track(&mut self.pages)?
|
|
};
|
|
|
|
// Untrack prior selection
|
|
if let Some(old) = self.selection {
|
|
old.deinit(&mut self.pages);
|
|
}
|
|
self.selection = Some(tracked_sel);
|
|
self.dirty.selection = true;
|
|
Ok(())
|
|
}
|
|
|
|
/// Same as select(None) but can't fail.
|
|
pub fn clear_selection(&mut self) {
|
|
if let Some(sel) = self.selection {
|
|
sel.deinit(&mut self.pages);
|
|
self.dirty.selection = true;
|
|
}
|
|
self.selection = None;
|
|
}
|
|
}
|
|
|
|
fn is_ws(whitespace: &[u32], cp: u32) -> bool {
|
|
whitespace.contains(&cp)
|
|
}
|
|
|
|
impl Screen {
|
|
fn pin_cell(&self, p: Pin) -> Cell {
|
|
*p.row_and_cell(&self.pages).expect("valid pin").1
|
|
}
|
|
|
|
fn pin_row(&self, p: Pin) -> Row {
|
|
*p.row_and_cell(&self.pages).expect("valid pin").0
|
|
}
|
|
|
|
fn pin_row_cells(&self, p: Pin) -> &[Cell] {
|
|
p.cells(&self.pages, CellSubset::All).expect("valid pin")
|
|
}
|
|
|
|
fn pin_cols(&self, p: Pin) -> u16 {
|
|
self.page(p.node).size.cols
|
|
}
|
|
|
|
/// Returns the raw text associated with a selection. This will unwrap
|
|
/// soft-wrapped edges.
|
|
///
|
|
/// For more flexibility, use a ScreenFormatter directly.
|
|
pub fn selection_string(&self, opts: SelectionString) -> String {
|
|
self.selection_string_impl(opts, None)
|
|
}
|
|
|
|
/// Same as `selection_string` but also returns a map from every byte of
|
|
/// the string to the pin it came from (Zig: `SelectionString.map`).
|
|
///
|
|
/// Panics if the map cannot be built, like any other Rust allocation
|
|
/// failure; use [`Screen::try_selection_string_map`] to handle it.
|
|
pub fn selection_string_map(&self, opts: SelectionString) -> StringMap {
|
|
self.try_selection_string_map(opts)
|
|
.expect("selection string map allocation failed")
|
|
}
|
|
|
|
/// Fallible [`Screen::selection_string_map`] (Zig `selectionString` with
|
|
/// `opts.map` set, which can return `error.OutOfMemory` when copying the
|
|
/// map). Nothing is leaked on failure: the string and pin list built so
|
|
/// far are dropped (Zig: `errdefer alloc.free(map_string)`).
|
|
pub fn try_selection_string_map(
|
|
&self,
|
|
opts: SelectionString,
|
|
) -> Result<StringMap, SelectionStringError> {
|
|
let mut pins = Vec::new();
|
|
let string = self.selection_string_impl(opts, Some(&mut pins));
|
|
|
|
// Duplicate the string and take the pins for the map (Zig: `dupeZ`
|
|
// then `pins.toOwnedSlice`).
|
|
let mut map_string = String::new();
|
|
map_string
|
|
.try_reserve_exact(string.len())
|
|
.map_err(|_| SelectionStringError::OutOfMemory)?;
|
|
map_string.push_str(&string);
|
|
selection_string_tw::check(SelectionStringPoint::CopyMap)?;
|
|
pins.shrink_to_fit();
|
|
Ok(StringMap {
|
|
string: map_string,
|
|
map: pins,
|
|
})
|
|
}
|
|
|
|
fn selection_string_impl(
|
|
&self,
|
|
opts: SelectionString,
|
|
pin_map: Option<&mut Vec<Pin>>,
|
|
) -> String {
|
|
// Create a formatter and use that to emit our text.
|
|
let mut formatter = ScreenFormatter::new(
|
|
self,
|
|
FormatOptions {
|
|
unwrap: true,
|
|
trim: opts.trim,
|
|
..FormatOptions::plain()
|
|
},
|
|
);
|
|
formatter.content = FormatContent::Selection(Some(opts.sel));
|
|
|
|
// If we have a string map, we need to set that up.
|
|
formatter.pin_map = pin_map;
|
|
|
|
formatter
|
|
.format_string()
|
|
.expect("formatting to memory cannot fail")
|
|
}
|
|
|
|
/// Select the line under the given point. This will select across soft-wrapped
|
|
/// lines and will omit the leading and trailing whitespace. If the point is
|
|
/// over whitespace but the line has non-whitespace characters elsewhere, the
|
|
/// line will be selected.
|
|
pub fn select_line(&self, opts: SelectLine<'_>) -> Option<Selection> {
|
|
self.select_line_bounded(opts, usize::MAX)
|
|
}
|
|
|
|
/// Select a complete logical line only if it spans at most `max_rows`
|
|
/// physical rows. This bounds interactive link lookup on unbroken output
|
|
/// without truncating a URL into a different, clickable target. Explicit
|
|
/// line selection uses [`Self::select_line`] and remains unrestricted.
|
|
pub fn select_line_bounded(&self, opts: SelectLine<'_>, max_rows: usize) -> Option<Selection> {
|
|
let list = &self.pages;
|
|
let mut remaining = max_rows.checked_sub(1)?; // The hovered row.
|
|
|
|
// Get the current point semantic prompt state since that determines
|
|
// boundary conditions too. This makes it so that line selection can
|
|
// only happen within the same prompt state. For example, if you triple
|
|
// click output, but the shell uses spaces to soft-wrap to the prompt
|
|
// then the selection will stop prior to the prompt. See issue #1329.
|
|
let semantic_prompt_state: Option<SemanticContent> = if opts.semantic_prompt_boundary {
|
|
Some(self.pin_cell(opts.pin).semantic_content())
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// The real start of the row is the first row in the soft-wrap.
|
|
let start_pin: Pin = 'start_pin: {
|
|
let mut it = opts.pin.row_iterator(list, Direction::LeftUp, None);
|
|
let mut it_prev: Pin = it.next().expect("self row"); // skip self
|
|
|
|
// First, check the current row for semantic boundaries before the clicked position.
|
|
if let Some(v) = semantic_prompt_state {
|
|
let cells = self.pin_row_cells(it_prev);
|
|
// Scan backwards from clicked position to find where our content starts
|
|
for i in 0..=usize::from(opts.pin.x) {
|
|
let x_rev = usize::from(opts.pin.x) - i;
|
|
if cells[x_rev].semantic_content() != v {
|
|
let mut copy = it_prev;
|
|
copy.x = (x_rev + 1) as u16;
|
|
break 'start_pin copy;
|
|
}
|
|
}
|
|
|
|
// No boundary found before clicked position on current row.
|
|
// If row doesn't wrap from above, start is at column 0.
|
|
// Otherwise, continue checking previous rows.
|
|
}
|
|
|
|
for p in it {
|
|
let row = self.pin_row(p);
|
|
|
|
if !row.wrap() {
|
|
let mut copy = it_prev;
|
|
copy.x = 0;
|
|
break 'start_pin copy;
|
|
}
|
|
|
|
remaining = remaining.checked_sub(1)?;
|
|
|
|
if let Some(v) = semantic_prompt_state {
|
|
// We need to check every cell in this row in reverse
|
|
// order since we're going up and back.
|
|
let cells = self.pin_row_cells(p);
|
|
for x in 0..cells.len() {
|
|
let x_rev = cells.len() - 1 - x;
|
|
let cell = cells[x_rev];
|
|
if cell.semantic_content() != v {
|
|
break 'start_pin it_prev;
|
|
}
|
|
it_prev = p;
|
|
it_prev.x = x_rev as u16;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
it_prev = p;
|
|
}
|
|
|
|
let mut copy = it_prev;
|
|
copy.x = 0;
|
|
copy
|
|
};
|
|
|
|
// The real end of the row is the final row in the soft-wrap.
|
|
let end_pin: Pin = 'end_pin: {
|
|
for p in opts.pin.row_iterator(list, Direction::RightDown, None) {
|
|
if p.node != opts.pin.node || p.y != opts.pin.y {
|
|
remaining = remaining.checked_sub(1)?;
|
|
}
|
|
let row = self.pin_row(p);
|
|
let cols = self.pin_cols(p);
|
|
|
|
if let Some(v) = semantic_prompt_state {
|
|
// We need to check every cell in this row
|
|
let cells = self.pin_row_cells(p);
|
|
|
|
// If this is our pin row we can start from our x because
|
|
// the start_pin logic already found the real start.
|
|
let start_offset = if p.node == opts.pin.node && p.y == opts.pin.y {
|
|
usize::from(opts.pin.x)
|
|
} else {
|
|
0
|
|
};
|
|
|
|
// Handle the zero case specially because if the first
|
|
// col doesn't match then we end at the end of the prior
|
|
// row. But if this is the first row, we can't go back,
|
|
// so we scan forward to find where our content ends.
|
|
if start_offset == 0 && cells[0].semantic_content() != v {
|
|
let mut prev = p.up(list, 1)?;
|
|
prev.x = cols - 1;
|
|
break 'end_pin prev;
|
|
}
|
|
|
|
// For every other case, we end at the prior cell.
|
|
for (x, cell) in cells.iter().enumerate().skip(start_offset) {
|
|
if cell.semantic_content() != v {
|
|
let mut copy = p;
|
|
copy.x = (x - 1) as u16;
|
|
break 'end_pin copy;
|
|
}
|
|
}
|
|
}
|
|
|
|
if !row.wrap() {
|
|
let mut copy = p;
|
|
copy.x = cols - 1;
|
|
break 'end_pin copy;
|
|
}
|
|
}
|
|
|
|
return None;
|
|
};
|
|
|
|
// Go forward from the start to find the first non-whitespace character.
|
|
let start: Pin = 'start: {
|
|
let Some(whitespace) = opts.whitespace else {
|
|
break 'start start_pin;
|
|
};
|
|
for p in start_pin.cell_iterator(list, Direction::RightDown, Some(end_pin)) {
|
|
let cell = self.pin_cell(p);
|
|
if !cell.has_text() {
|
|
continue;
|
|
}
|
|
|
|
// Non-empty means we found it.
|
|
if is_ws(whitespace, cell.codepoint()) {
|
|
continue;
|
|
}
|
|
|
|
break 'start p;
|
|
}
|
|
|
|
return None;
|
|
};
|
|
|
|
// Go backward from the end to find the first non-whitespace character.
|
|
let end: Pin = 'end: {
|
|
let Some(whitespace) = opts.whitespace else {
|
|
break 'end end_pin;
|
|
};
|
|
for p in end_pin.cell_iterator(list, Direction::LeftUp, Some(start_pin)) {
|
|
let cell = self.pin_cell(p);
|
|
if !cell.has_text() {
|
|
continue;
|
|
}
|
|
|
|
// Non-empty means we found it.
|
|
if is_ws(whitespace, cell.codepoint()) {
|
|
continue;
|
|
}
|
|
|
|
break 'end p;
|
|
}
|
|
|
|
return None;
|
|
};
|
|
|
|
Some(Selection::new(start, end, false))
|
|
}
|
|
|
|
/// Return the selection for all contents on the screen. Surrounding
|
|
/// whitespace is omitted. If there is no selection, this returns None.
|
|
pub fn select_all(&self) -> Option<Selection> {
|
|
let whitespace = DEFAULT_SELECT_WHITESPACE;
|
|
let screen = Point::Screen(Coordinate::default());
|
|
|
|
let start = self
|
|
.pages
|
|
.cell_iterator(Direction::RightDown, screen, None)
|
|
.find(|p| {
|
|
let cell = self.pin_cell(*p);
|
|
cell.has_text() && !is_ws(whitespace, cell.codepoint())
|
|
})?;
|
|
|
|
let end = self
|
|
.pages
|
|
.cell_iterator(Direction::LeftUp, screen, None)
|
|
.find(|p| {
|
|
let cell = self.pin_cell(*p);
|
|
cell.has_text() && !is_ws(whitespace, cell.codepoint())
|
|
})?;
|
|
|
|
Some(Selection::new(start, end, false))
|
|
}
|
|
|
|
/// Select the nearest word to start point that is between start_pt and
|
|
/// end_pt (inclusive). Because it selects "nearest" to start point, start
|
|
/// point can be before or after end point.
|
|
///
|
|
/// The boundary_codepoints parameter should be a slice of codepoints that
|
|
/// mark word boundaries, passed through to select_word.
|
|
///
|
|
/// TODO: test this
|
|
pub fn select_word_between(
|
|
&self,
|
|
start: Pin,
|
|
end: Pin,
|
|
boundary_codepoints: &[u32],
|
|
) -> Option<Selection> {
|
|
let list = &self.pages;
|
|
let dir = if start.before(list, end) {
|
|
Direction::RightDown
|
|
} else {
|
|
Direction::LeftUp
|
|
};
|
|
for pin in start.cell_iterator(list, dir, Some(end)) {
|
|
// Boundary conditions
|
|
match dir {
|
|
Direction::RightDown => {
|
|
if end.before(list, pin) {
|
|
return None;
|
|
}
|
|
}
|
|
Direction::LeftUp => {
|
|
if pin.before(list, end) {
|
|
return None;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we found a word, then return it
|
|
if let Some(sel) = self.select_word(pin, boundary_codepoints) {
|
|
return Some(sel);
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Select the word under the given point. A word is any consecutive series
|
|
/// of characters that are exclusively whitespace or exclusively non-whitespace.
|
|
/// A selection can span multiple physical lines if they are soft-wrapped.
|
|
///
|
|
/// This will return None if a selection is impossible. The only scenario
|
|
/// this happens is if the point pt is outside of the written screen space.
|
|
///
|
|
/// The boundary_codepoints parameter should be a slice of codepoints that
|
|
/// mark word boundaries. This is expected to be pre-parsed from the config.
|
|
pub fn select_word(&self, pin: Pin, boundary_codepoints: &[u32]) -> Option<Selection> {
|
|
let list = &self.pages;
|
|
|
|
// If our cell is empty we can't select a word, because we can't select
|
|
// areas where the screen is not yet written.
|
|
let start_cell = self.pin_cell(pin);
|
|
if !start_cell.has_text() {
|
|
return None;
|
|
}
|
|
|
|
// Determine if we are a boundary or not to determine what our boundary is.
|
|
let expect_boundary = is_ws(boundary_codepoints, start_cell.codepoint());
|
|
|
|
// Go forwards to find our end boundary
|
|
let end: Pin = 'end: {
|
|
let mut it = pin.cell_iterator(list, Direction::RightDown, None);
|
|
let mut prev = it.next().expect("start cell"); // Consume one, our start
|
|
for p in it {
|
|
let (row, cell) = {
|
|
let (r, c) = p.row_and_cell(list).expect("valid pin");
|
|
(*r, *c)
|
|
};
|
|
|
|
// If we reached an empty cell its always a boundary
|
|
if !cell.has_text() {
|
|
break 'end prev;
|
|
}
|
|
|
|
// If we do not match our expected set, we hit a boundary
|
|
let this_boundary = is_ws(boundary_codepoints, cell.codepoint());
|
|
if this_boundary != expect_boundary {
|
|
break 'end prev;
|
|
}
|
|
|
|
// If we are going to the next row and it isn't wrapped, we
|
|
// return the previous.
|
|
if p.x == self.pin_cols(p) - 1 && !row.wrap() {
|
|
break 'end p;
|
|
}
|
|
|
|
prev = p;
|
|
}
|
|
|
|
prev
|
|
};
|
|
|
|
// Go backwards to find our start boundary
|
|
let start: Pin = 'start: {
|
|
let mut it = pin.cell_iterator(list, Direction::LeftUp, None);
|
|
let mut prev = it.next().expect("start cell"); // Consume one, our start
|
|
for p in it {
|
|
let (row, cell) = {
|
|
let (r, c) = p.row_and_cell(list).expect("valid pin");
|
|
(*r, *c)
|
|
};
|
|
|
|
// If we are going to the next row and it isn't wrapped, we
|
|
// return the previous.
|
|
if p.x == self.pin_cols(p) - 1 && !row.wrap() {
|
|
break 'start prev;
|
|
}
|
|
|
|
// If we reached an empty cell its always a boundary
|
|
if !cell.has_text() {
|
|
break 'start prev;
|
|
}
|
|
|
|
// If we do not match our expected set, we hit a boundary
|
|
let this_boundary = is_ws(boundary_codepoints, cell.codepoint());
|
|
if this_boundary != expect_boundary {
|
|
break 'start prev;
|
|
}
|
|
|
|
prev = p;
|
|
}
|
|
|
|
prev
|
|
};
|
|
|
|
Some(Selection::new(start, end, false))
|
|
}
|
|
|
|
/// Select the command output under the given point. The limits of the output
|
|
/// are determined by semantic prompt information provided by shell integration.
|
|
/// A selection can span multiple physical lines if they are soft-wrapped.
|
|
///
|
|
/// This will return None if a selection is impossible:
|
|
/// - the point pt is outside of the written screen space.
|
|
/// - the point pt is on a prompt / input line.
|
|
pub fn select_output(&self, pin: Pin) -> Option<Selection> {
|
|
let list = &self.pages;
|
|
|
|
// If our pin right now is not on output, then we return nothing.
|
|
if self.pin_cell(pin).semantic_content() != SemanticContent::Output {
|
|
return None;
|
|
}
|
|
|
|
// Get the post prior prompt from this pin. This is the prompt whose
|
|
// output we'll be capturing.
|
|
let prompt_pin: Pin = 'prompt: {
|
|
// If we have a prompt above this point (including this point),
|
|
// then thats the prompt we want to capture output from.
|
|
if let Some(p) = pin.prompt_iterator(list, Direction::LeftUp, None).next() {
|
|
break 'prompt p;
|
|
}
|
|
|
|
// If we don't have a prompt, then we assume that we're
|
|
// capturing all the output up to the next prompt.
|
|
let next = pin
|
|
.prompt_iterator(list, Direction::RightDown, None)
|
|
.next()?;
|
|
|
|
// We'll capture from the start of the screen to just above
|
|
// the prompt and will trim the trailing whitespace.
|
|
let start_pin = self.pages.get_top_left(Tag::Screen)?;
|
|
let mut end_pin = next.up(list, 1)?;
|
|
end_pin.x = self.pin_cols(end_pin) - 1;
|
|
for p in end_pin.cell_iterator(list, Direction::LeftUp, Some(start_pin)) {
|
|
end_pin = p;
|
|
if self.pin_cell(p).has_text() {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return Some(Selection::new(start_pin, end_pin, false));
|
|
};
|
|
|
|
// Grab our content
|
|
let (hl_start, mut hl_end) = self
|
|
.pages
|
|
.highlight_semantic_content(prompt_pin, SemanticContent::Output)?;
|
|
|
|
// Trim our trailing whitespace
|
|
for p in hl_end.cell_iterator(list, Direction::LeftUp, Some(hl_start)) {
|
|
hl_end = p;
|
|
if self.pin_cell(p).has_text() {
|
|
break;
|
|
}
|
|
}
|
|
|
|
Some(Selection::new(hl_start, hl_end, false))
|
|
}
|
|
|
|
/// Returns an iterator to move through the soft-wrapped lines starting
|
|
/// from pin.
|
|
pub fn line_iterator(&self, start: Pin) -> LineIterator<'_> {
|
|
LineIterator {
|
|
screen: self,
|
|
current: Some(start),
|
|
}
|
|
}
|
|
|
|
/// Determine the inputs necessary to move the cursor to the given
|
|
/// click location within a prompt input area.
|
|
///
|
|
/// If the cursor isn't currently at a prompt input location, this
|
|
/// returns no movement.
|
|
///
|
|
/// This feature depends on well-behaved OSC133 shell integration. Specifically,
|
|
/// this only moves over designated input areas (OSC 133 B). It is assumed
|
|
/// that the shell will only move the cursor to input cells, so prompt cells
|
|
/// and other blank cells are ignored as part of the movement calculation.
|
|
pub fn prompt_click_move(&self, click_pin: Pin) -> PromptClickMove {
|
|
// If we're not at an input cell with our cursor, no movement will
|
|
// ever be possible.
|
|
if self.cursor.semantic_content != SemanticContent::Input
|
|
&& self.cursor_cell().semantic_content() != SemanticContent::Input
|
|
{
|
|
return PromptClickMove::ZERO;
|
|
}
|
|
|
|
match self.semantic_prompt.click {
|
|
// None doesn't support movement and click_events must use a
|
|
// different mechanism (SGR mouse events) that callers must handle.
|
|
SemanticClick::None | SemanticClick::ClickEvents => PromptClickMove::ZERO,
|
|
SemanticClick::Cl(cl) => match cl {
|
|
// All of these currently use dumb line-based navigation.
|
|
// But eventually we'll support more.
|
|
Click::Line
|
|
| Click::Multiple
|
|
| Click::ConservativeVertical
|
|
| Click::SmartVertical => self.prompt_click_line(click_pin),
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Determine the inputs required to move from the cursor to the given
|
|
/// click location. If the cursor isn't currently at a prompt input
|
|
/// location, this will return zero.
|
|
///
|
|
/// This currently only supports moving a single line.
|
|
fn prompt_click_line(&self, click_pin: Pin) -> PromptClickMove {
|
|
let list = &self.pages;
|
|
|
|
// If our click pin is our cursor pin, no movement is needed.
|
|
// Do this early so we can assume later that they are different.
|
|
let cursor_pin = self.cursor_pin();
|
|
if cursor_pin.eql(click_pin) {
|
|
return PromptClickMove::ZERO;
|
|
}
|
|
|
|
// If our cursor is before our click, we're only emitting right inputs.
|
|
if cursor_pin.before(list, click_pin) {
|
|
let mut count: usize = 0;
|
|
|
|
// We go row-by-row because soft-wrapped rows are still a single
|
|
// line to a shell, so we can't just look at our page row.
|
|
'row_it: for row_pin in
|
|
cursor_pin.row_iterator(list, Direction::RightDown, Some(click_pin))
|
|
{
|
|
let row = self.pin_row(row_pin);
|
|
let cells = self.pin_row_cells(row_pin);
|
|
|
|
// Determine if this row is our cursor.
|
|
let is_cursor_row = row_pin.node == cursor_pin.node && row_pin.y == cursor_pin.y;
|
|
|
|
// If this is not the cursor row, verify it's still part of the
|
|
// continuation of our starting prompt.
|
|
if !is_cursor_row && row.semantic_prompt() != RowPrompt::PromptContinuation {
|
|
break;
|
|
}
|
|
|
|
// Determine where our input starts.
|
|
let start_x: usize = 'start_x: {
|
|
// If this is our cursor row then we start after the cursor.
|
|
if is_cursor_row {
|
|
break 'start_x usize::from(cursor_pin.x) + 1;
|
|
}
|
|
|
|
// Otherwise, we start at the first input cell, because
|
|
// we expect the shell to properly translate arrows across
|
|
// lines to the start of the input. Some shells indent
|
|
// where input starts on subsequent lines so we must do
|
|
// this.
|
|
for (x, cell) in cells.iter().enumerate() {
|
|
if cell.semantic_content() == SemanticContent::Input {
|
|
break 'start_x x;
|
|
}
|
|
}
|
|
|
|
// We never found an input cell, so we need to move to the
|
|
// next row.
|
|
cells.len()
|
|
};
|
|
|
|
// Iterate over the input cells and assume arrow keys only
|
|
// jump to input cells.
|
|
for (x, cell) in cells.iter().enumerate().skip(start_x) {
|
|
// Ignore non-input cells, but allow breaks. We assume
|
|
// the shell will translate arrow keys to only input
|
|
// areas.
|
|
if cell.semantic_content() != SemanticContent::Input {
|
|
continue;
|
|
}
|
|
|
|
// Increment our input count
|
|
count += 1;
|
|
|
|
// If this is our target, we're done.
|
|
if row_pin.node == click_pin.node
|
|
&& row_pin.y == click_pin.y
|
|
&& x == usize::from(click_pin.x)
|
|
{
|
|
break 'row_it;
|
|
}
|
|
}
|
|
|
|
// If this row isn't soft-wrapped, we need to break out
|
|
// because line based moving only handles single lines.
|
|
// We're done!
|
|
if !row.wrap() {
|
|
// If we never found our pin, that means we clicked further
|
|
// right/beyond it. If we're already on a non-empty input cell
|
|
// then we add one so we can move to the newest, empty cell
|
|
// at the end, matching typical editor behavior.
|
|
if self.cursor_cell().semantic_content() == SemanticContent::Input {
|
|
count += 1;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return PromptClickMove {
|
|
left: 0,
|
|
right: count,
|
|
};
|
|
}
|
|
|
|
// Otherwise, cursor is after click, so we're emitting left inputs.
|
|
let mut count: usize = 0;
|
|
|
|
// We go row-by-row because soft-wrapped rows are still a single
|
|
// line to a shell, so we can't just look at our page row.
|
|
'row_it: for row_pin in cursor_pin.row_iterator(list, Direction::LeftUp, Some(click_pin)) {
|
|
let row = self.pin_row(row_pin);
|
|
let cells = self.pin_row_cells(row_pin);
|
|
|
|
// Determine the length of the cells we look at in this row.
|
|
let end_len: usize =
|
|
// If this is our cursor row then we end before the cursor.
|
|
if row_pin.node == cursor_pin.node && row_pin.y == cursor_pin.y {
|
|
usize::from(cursor_pin.x)
|
|
} else {
|
|
// Otherwise, we end at the last cell in the row.
|
|
cells.len()
|
|
};
|
|
|
|
// Iterate backwards over the input cells.
|
|
for rev_x in 0..end_len {
|
|
let x = end_len - 1 - rev_x;
|
|
let cell = cells[x];
|
|
|
|
// Ignore non-input cells.
|
|
if cell.semantic_content() != SemanticContent::Input {
|
|
continue;
|
|
}
|
|
|
|
// Increment our input count
|
|
count += 1;
|
|
|
|
// If this is our target, we're done.
|
|
if row_pin.node == click_pin.node
|
|
&& row_pin.y == click_pin.y
|
|
&& x == usize::from(click_pin.x)
|
|
{
|
|
break 'row_it;
|
|
}
|
|
}
|
|
|
|
// If this row is not a wrap continuation, then break out
|
|
if !row.wrap_continuation() {
|
|
break;
|
|
}
|
|
}
|
|
|
|
PromptClickMove {
|
|
left: count,
|
|
right: 0,
|
|
}
|
|
}
|
|
|
|
/// Dump the screen to a string.
|
|
pub fn dump_string(&self, out: &mut String, opts: DumpString) {
|
|
// Create a formatter and use that to emit our text.
|
|
let mut formatter = ScreenFormatter::new(
|
|
self,
|
|
FormatOptions {
|
|
unwrap: opts.unwrap,
|
|
trim: false,
|
|
..FormatOptions::plain()
|
|
},
|
|
);
|
|
|
|
// Set up the selection based on the pins
|
|
let tl = opts.tl;
|
|
let br = opts
|
|
.br
|
|
.unwrap_or_else(|| self.pages.get_bottom_right(Tag::Screen).expect("screen"));
|
|
formatter.content = FormatContent::Selection(Some(Selection::new(
|
|
tl, br, false, // not rectangle
|
|
)));
|
|
|
|
// Emit
|
|
out.push_str(
|
|
&formatter
|
|
.format_string()
|
|
.expect("formatting to memory cannot fail"),
|
|
);
|
|
}
|
|
|
|
/// You should use dump_string, this is a restricted version mostly for
|
|
/// legacy and convenience reasons for unit tests.
|
|
pub fn dump_string_alloc(&self, tl: Point) -> Result<String, Error> {
|
|
let mut out = String::new();
|
|
self.dump_string(
|
|
&mut out,
|
|
DumpString {
|
|
tl: self
|
|
.pages
|
|
.get_top_left(tl.tag())
|
|
.ok_or(Error::InvalidCoordinate)?,
|
|
br: Some(
|
|
self.pages
|
|
.get_bottom_right(tl.tag())
|
|
.ok_or(Error::InvalidCoordinate)?,
|
|
),
|
|
unwrap: false,
|
|
},
|
|
);
|
|
Ok(out)
|
|
}
|
|
|
|
/// You should use dump_string, this is a restricted version mostly for
|
|
/// legacy and convenience reasons for unit tests.
|
|
pub fn dump_string_alloc_unwrapped(&self, tl: Point) -> Result<String, Error> {
|
|
let mut out = String::new();
|
|
self.dump_string(
|
|
&mut out,
|
|
DumpString {
|
|
tl: self
|
|
.pages
|
|
.get_top_left(tl.tag())
|
|
.ok_or(Error::InvalidCoordinate)?,
|
|
br: Some(
|
|
self.pages
|
|
.get_bottom_right(tl.tag())
|
|
.ok_or(Error::InvalidCoordinate)?,
|
|
),
|
|
unwrap: true,
|
|
},
|
|
);
|
|
Ok(out)
|
|
}
|
|
|
|
/// This is basically a really jank version of Terminal.printString. We
|
|
/// have to reimplement it here because we want a way to print to the screen
|
|
/// to test it but don't want all the features of Terminal.
|
|
pub fn test_write_string(&mut self, text: &str) -> Result<(), Error> {
|
|
for c in text.chars() {
|
|
let c = c as u32;
|
|
|
|
// Explicit newline forces a new row
|
|
if c == u32::from('\n') {
|
|
self.cursor_down_or_scroll()?;
|
|
self.cursor_horizontal_absolute(0);
|
|
self.cursor.pending_wrap = false;
|
|
if self.cursor.semantic_content_clear_eol {
|
|
self.cursor_set_semantic_content(SemanticContentSet::Output);
|
|
} else {
|
|
match self.cursor.semantic_content {
|
|
SemanticContent::Output => {}
|
|
SemanticContent::Prompt | SemanticContent::Input => {
|
|
self.cursor_row_mut()
|
|
.set_semantic_prompt(RowPrompt::PromptContinuation);
|
|
}
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
let width: usize = if c <= 0xFF {
|
|
1
|
|
} else {
|
|
usize::from(ghostty_unicode::table().get(c).width)
|
|
};
|
|
if width == 0 {
|
|
let mut x = self.cursor.x - 1;
|
|
match self.cursor_cell_left(1).wide() {
|
|
Wide::Narrow | Wide::Wide => {}
|
|
Wide::SpacerHead => unreachable!(),
|
|
Wide::SpacerTail => x = self.cursor.x - 2,
|
|
}
|
|
|
|
let p = self.cursor_pin();
|
|
self.page_mut(p.node)
|
|
.append_grapheme(usize::from(x), usize::from(p.y), c)?;
|
|
continue;
|
|
}
|
|
|
|
if self.cursor.pending_wrap {
|
|
assert!(self.cursor.x == self.pages.cols - 1);
|
|
self.cursor.pending_wrap = false;
|
|
self.cursor_row_mut().set_wrap(true);
|
|
self.cursor_down_or_scroll()?;
|
|
self.cursor_horizontal_absolute(0);
|
|
self.cursor_row_mut().set_wrap_continuation(true);
|
|
match self.cursor.semantic_content {
|
|
SemanticContent::Output => {}
|
|
SemanticContent::Input | SemanticContent::Prompt => {
|
|
self.cursor_row_mut()
|
|
.set_semantic_prompt(RowPrompt::PromptContinuation);
|
|
}
|
|
}
|
|
}
|
|
|
|
assert!(width == 1 || width == 2);
|
|
match width {
|
|
1 => {
|
|
let mut cell = Cell::default();
|
|
cell.set_content_tag(ContentTag::Codepoint);
|
|
cell.set_codepoint(c);
|
|
cell.set_style_id(self.cursor.style_id);
|
|
cell.set_protected(self.cursor.protected);
|
|
cell.set_semantic_content(self.cursor.semantic_content);
|
|
*self.cursor_cell_mut() = cell;
|
|
|
|
// If we have a ref-counted style, increase.
|
|
if self.cursor.style_id != style::DEFAULT_ID {
|
|
let id = self.cursor.style_id;
|
|
let _ = self.cursor_page_mut().styles.r#use(id);
|
|
self.cursor_row_mut().set_styled(true);
|
|
}
|
|
|
|
// If we have a hyperlink, add it to the cell.
|
|
if self.cursor.hyperlink_id > 0 {
|
|
self.cursor_set_hyperlink()?;
|
|
}
|
|
}
|
|
|
|
2 => {
|
|
// Need a wide spacer head
|
|
if self.cursor.x == self.pages.cols - 1 {
|
|
let mut cell = Cell::default();
|
|
cell.set_content_tag(ContentTag::Codepoint);
|
|
cell.set_codepoint(0);
|
|
cell.set_wide(Wide::SpacerHead);
|
|
cell.set_protected(self.cursor.protected);
|
|
cell.set_semantic_content(self.cursor.semantic_content);
|
|
*self.cursor_cell_mut() = cell;
|
|
|
|
// If we have a hyperlink, add it to the cell.
|
|
if self.cursor.hyperlink_id > 0 {
|
|
self.cursor_set_hyperlink()?;
|
|
}
|
|
|
|
self.cursor_row_mut().set_wrap(true);
|
|
self.cursor_down_or_scroll()?;
|
|
self.cursor_horizontal_absolute(0);
|
|
self.cursor_row_mut().set_wrap_continuation(true);
|
|
}
|
|
|
|
// Write our wide char
|
|
let mut cell = Cell::default();
|
|
cell.set_content_tag(ContentTag::Codepoint);
|
|
cell.set_codepoint(c);
|
|
cell.set_style_id(self.cursor.style_id);
|
|
cell.set_wide(Wide::Wide);
|
|
cell.set_protected(self.cursor.protected);
|
|
cell.set_semantic_content(self.cursor.semantic_content);
|
|
*self.cursor_cell_mut() = cell;
|
|
|
|
// If we have a hyperlink, add it to the cell.
|
|
if self.cursor.hyperlink_id > 0 {
|
|
self.cursor_set_hyperlink()?;
|
|
}
|
|
|
|
// Write our tail
|
|
self.cursor_right(1);
|
|
let mut cell = Cell::default();
|
|
cell.set_content_tag(ContentTag::Codepoint);
|
|
cell.set_codepoint(0);
|
|
cell.set_wide(Wide::SpacerTail);
|
|
cell.set_protected(self.cursor.protected);
|
|
cell.set_semantic_content(self.cursor.semantic_content);
|
|
*self.cursor_cell_mut() = cell;
|
|
|
|
// If we have a hyperlink, add it to the cell.
|
|
if self.cursor.hyperlink_id > 0 {
|
|
self.cursor_set_hyperlink()?;
|
|
}
|
|
|
|
// If we have a ref-counted style, increase twice.
|
|
if self.cursor.style_id != style::DEFAULT_ID {
|
|
let id = self.cursor.style_id;
|
|
let _ = self.cursor_page_mut().styles.r#use(id);
|
|
let _ = self.cursor_page_mut().styles.r#use(id);
|
|
self.cursor_row_mut().set_styled(true);
|
|
}
|
|
}
|
|
|
|
_ => unreachable!(),
|
|
}
|
|
|
|
if self.cursor.x + 1 < self.pages.cols {
|
|
self.cursor_right(1);
|
|
} else {
|
|
self.cursor.pending_wrap = true;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// An iterator over the soft-wrapped lines of a screen.
|
|
pub struct LineIterator<'a> {
|
|
screen: &'a Screen,
|
|
current: Option<Pin>,
|
|
}
|
|
|
|
impl Iterator for LineIterator<'_> {
|
|
type Item = Selection;
|
|
|
|
fn next(&mut self) -> Option<Selection> {
|
|
let current = self.current?;
|
|
let Some(result) = self.screen.select_line(SelectLine {
|
|
pin: current,
|
|
whitespace: None,
|
|
semantic_prompt_boundary: false,
|
|
}) else {
|
|
self.current = None;
|
|
return None;
|
|
};
|
|
|
|
self.current = result.end(&self.screen.pages).down(&self.screen.pages, 1);
|
|
Some(result)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|