Skip to main content

justerm_core/
cursor.rs

1//! The cursor and its drawing pen.
2
3use crate::cell::{Cell, CellFlags};
4use crate::color::Color;
5
6/// The current SGR state — the appearance copied into each printed cell.
7///
8/// Modelling it as a "template cell" mirrors Alacritty: a later slice can make
9/// erase (ED/EL) fill cleared cells with `bg` instead of `Default` and that
10/// *is* Background Color Erase (BCE), no structural change. See `term.rs`.
11#[derive(Clone, Copy, Debug, Default)]
12pub struct Pen {
13    pub fg: Color,
14    pub bg: Color,
15    pub flags: CellFlags,
16    /// The underline colour (SGR 58, #520): what an underline / strikethrough draws
17    /// in, independent of `fg`. `Default` means "follow the fg". It is *not* packed
18    /// into the printed `Cell` (the 12-byte cell is full); the print path stamps a
19    /// non-default value into the row's ucolor map. See `term.rs::write_glyph`.
20    pub underline_color: Color,
21}
22
23impl Pen {
24    /// Reset to default appearance (SGR 0).
25    pub fn reset(&mut self) {
26        *self = Pen::default();
27    }
28
29    /// Build a cell carrying this pen's appearance and the given glyph.
30    pub fn cell(&self, c: char) -> Cell {
31        Cell::from_parts(c, self.fg, self.bg, self.flags)
32    }
33}
34
35/// The cursor's drawn shape (DECSCUSR / the renderer's caret glyph). The engine
36/// reports it on the frame; the renderer draws it. Default `Block` (#81).
37///
38/// **Deliberately exhaustive (#843) — and the reason is the wire, not the spec.**
39///
40/// An earlier draft of that sweep said "DECSCUSR's shape space, closed". **That is
41/// false**, and the counter-example is in this repository: `justerm-renderer` has
42/// carried a fourth shape, `HollowBlock`, since before the sweep
43/// (`justerm-renderer/src/cursor.rs:60`, wire id `3`), and no core frame can ask
44/// for it. The space is not closed by the spec; it has already been grown once,
45/// one crate over.
46///
47/// What actually decides it is that **this enum is mapped onto wire values by a
48/// `match` outside this crate** — `justerm-wasm-decode/src/lib.rs:198` turns each
49/// member into an int for the frame header. Marking it non-exhaustive would force
50/// a `_` arm there, converting a future compile error into a silently wrong wire
51/// value. That is the same construct that reddened `cargo test --workspace` for
52/// [`crate::MarkerKind`], where the rule is stated in full.
53#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
54pub enum CursorShape {
55    #[default]
56    Block,
57    Underline,
58    Bar,
59}
60
61/// The input position, its pending-wrap state, and the current pen.
62#[derive(Clone, Copy, Debug)]
63pub struct Cursor {
64    pub row: usize,
65    pub col: usize,
66    /// Deferred last-column wrap (xterm's "wrapnext"). Set when a print fills the
67    /// last column: the cursor stays put and the actual line wrap happens on the
68    /// *next* print. Eager wrapping here is the classic off-by-one that shifts
69    /// lines (see `docs/architecture.md` "Hidden VT state").
70    pub pending_wrap: bool,
71    pub pen: Pen,
72    /// Whether the cursor is shown (DEC ?25). The engine only reports it.
73    pub visible: bool,
74    /// The caret shape (DECSCUSR, #89) — reported on the frame, drawn by the
75    /// renderer.
76    pub shape: CursorShape,
77    /// Whether the caret blinks (att610 ?12, #81). The engine reports the *mode*;
78    /// the actual animation is the renderer's.
79    pub blink: bool,
80}
81
82impl Cursor {
83    /// The cursor's `(row, col)` position.
84    pub(crate) fn point(&self) -> (usize, usize) {
85        (self.row, self.col)
86    }
87
88    /// Set the position, clamped to a `rows` x `cols` screen.
89    pub(crate) fn set_point(&mut self, point: (usize, usize), rows: usize, cols: usize) {
90        self.row = point.0.min(rows - 1);
91        self.col = point.1.min(cols - 1);
92    }
93}
94
95impl Default for Cursor {
96    fn default() -> Self {
97        // The cursor starts visible; a manual impl is needed because `bool`'s
98        // derived default is `false`.
99        Cursor {
100            row: 0,
101            col: 0,
102            pending_wrap: false,
103            pen: Pen::default(),
104            visible: true,
105            shape: CursorShape::Block,
106            blink: false,
107        }
108    }
109}