Skip to main content

uncurses/screen/
mod.rs

1//! [`Screen`] — the cell-diff renderer you draw into.
2//!
3//! `Screen<W>` owns a desired cell grid, a diff renderer, and a writer. You
4//! paint cells into it and call [`render`](Screen::render); it works out the
5//! minimal escape sequence that turns what the terminal is showing into what
6//! you asked for, and writes it.
7//!
8//! That is the whole job. `Screen` reads no input, tracks no capabilities,
9//! and manages no session — [`Program`](crate::program::Program) does all of
10//! that and owns a `Screen` to render with. A `Screen` on its own is enough
11//! for output-only programs, tests, and offscreen rendering, since `W` is any
12//! [`Write`]: a terminal handle, a `Vec<u8>`, a file.
13//!
14//! ```
15//! use uncurses::screen::Screen;
16//! use uncurses::style::Style;
17//! use uncurses::text::TextSurface;
18//!
19//! # fn main() -> std::io::Result<()> {
20//! let mut screen = Screen::new(Vec::new(), (20, 3));
21//! screen.set_str((0, 0), "hello", Style::default());
22//! screen.render()?;
23//! assert!(!screen.writer().is_empty());
24//! # Ok(())
25//! # }
26//! ```
27//!
28//! # Render properties
29//!
30//! A `Screen` holds only the state that changes how a frame is drawn:
31//!
32//! * [`fullscreen`](Screen::fullscreen) — whether the managed area is the
33//!   whole viewport (the alternate screen buffer, addressed with absolute
34//!   moves) or a band in the normal buffer (the default, addressed
35//!   relatively so scrollback above and the shell prompt below survive).
36//! * Cursor visibility and the declarative resting
37//!   [position](Screen::set_cursor_position).
38//! * [Synchronized output](Screen::set_synchronized_output), grapheme-cluster
39//!   [width mode](Screen::set_grapheme_clusters), the
40//!   [color profile](Screen::set_color_profile), and the renderer
41//!   [optimizations](Screen::set_optimizations).
42//!
43//! Every one of these is a plain setter: it cannot fail and it writes nothing.
44//! A `Screen` never *persists* a terminal mode: the only modes it emits are
45//! the synchronized-output and cursor-visibility markers it wraps a single
46//! frame in, and it closes both before [`render`](Screen::render) returns.
47//! Leaving a mode on is
48//! [`Program`](crate::program::Program)'s job, and it pushes the render
49//! consequence down here with the matching setter — so
50//! [`Program::enter_alt_screen`](crate::program::Program::enter_alt_screen)
51//! emits DECSET 1049 and calls [`set_fullscreen`](Screen::set_fullscreen), and
52//! [`Program::hide_cursor`](crate::program::Program::hide_cursor) emits DECTCEM
53//! and calls [`set_cursor_visible`](Screen::set_cursor_visible). Drive a
54//! `Screen` yourself and you own both halves.
55//!
56//! ```text
57//!  Inline (default): the surface lives in the normal buffer, only as
58//!  many rows as you draw; scrollback and the shell prompt stay intact.
59//!
60//!    $ earlier shell output
61//!    $ ... scrollback ...
62//!    ┌─────────────────────────┐
63//!    │ managed surface         │  <- only the rows you draw, full width
64//!    └─────────────────────────┘
65//!    $ shell prompt resumes
66//!
67//!  Fullscreen: the whole viewport is the surface, addressed with
68//!  absolute moves, and restored on exit.
69//!
70//!    ┌─────────────────────────────┐
71//!    │                             │
72//!    │  the whole terminal         │
73//!    │  viewport is the surface    │
74//!    │                             │
75//!    └─────────────────────────────┘
76//! ```
77
78#[cfg(test)]
79mod tests;
80
81/// Cell-diff capability flags controlling which optimized escape
82/// sequences the screen's renderer may emit. Re-exported from the
83/// renderer so applications can configure rendering with
84/// [`Screen::set_optimizations`] without depending on renderer internals.
85pub use crate::renderer::Optimizations;
86
87use std::io::{self, Write};
88
89use crate::ansi::mode;
90use crate::buffer::{Bounded, Surface, SurfaceMut};
91use crate::cell::Cell;
92use crate::layout::{Position, Rect, Size};
93use crate::renderer::{RenderBuffer, Renderer};
94use crate::text::{TextSurface, WidthMode};
95
96/// A cell-diff renderer over a writer. See the [module documentation](self).
97///
98/// `Screen` is [`Send`] and [`Sync`] whenever its writer is, so it can be
99/// moved onto another thread or held across an `.await` point.
100pub struct Screen<W: Write> {
101    /// Where rendered bytes go.
102    writer: W,
103    /// Caller-facing desired cell grid. Touched spans record where the
104    /// application wrote since the last sync; the renderer filters them
105    /// again against its staging buffer before diffing the terminal.
106    front_buf: RenderBuffer,
107    /// The diff renderer holding the tracked on-screen buffer, cursor model,
108    /// color profile, and optimizations.
109    renderer: Renderer,
110    /// Scratch byte buffer that drawing and property methods stage escape
111    /// bytes into before [`io::Write::flush`] drains them to the writer.
112    out_buf: Vec<u8>,
113    /// Managed area width in cells.
114    width: u16,
115    /// Managed area height in cells.
116    height: u16,
117    /// East-Asian Ambiguous width policy used when measuring strings: when
118    /// `true`, code points whose East-Asian-Width property is `Ambiguous`
119    /// are measured as 2 cells instead of 1. See [`crate::text::char_width`].
120    eaw_wide: bool,
121    /// Whether the managed area is the whole viewport or an inline band.
122    fullscreen: bool,
123    /// Cursor visibility (DECTCEM). Render-coupled: a frame hides a *visible*
124    /// cursor around the cell diff, and bracketing a cursor the caller
125    /// deliberately hid would turn it back on.
126    cursor_visible: bool,
127    /// Synchronized updates: when `true`, each non-empty frame is wrapped in
128    /// synchronized-output begin/end sequences.
129    sync_updates: bool,
130    /// Unicode core / grapheme cluster mode (DEC 2027). When `true`, width is
131    /// calculated per grapheme cluster (UTS-29 + emoji rules); when `false`,
132    /// per code point (wcwidth-style).
133    grapheme_clusters: bool,
134    /// Declarative resting position for the cursor, applied at the end of
135    /// every [`render`](Screen::render) via
136    /// [`set_cursor_position`](Screen::set_cursor_position). Sticky: it
137    /// persists across frames and is re-applied each render (a no-op when the
138    /// cursor is already there) until changed or cleared. `None` means no
139    /// declarative resting position, so the cursor is left wherever the cell
140    /// diff ended.
141    desired_cursor: Option<Position>,
142    /// The resting position the last frame walked the cursor to, when the
143    /// walk ended on a column only the terminal can name.
144    ///
145    /// Paired with [`Renderer::cursor_placed_on_row`] this says "the cursor
146    /// is already resting here", which the tracked position alone cannot,
147    /// because the walk deliberately forgets the column. An immediate move
148    /// clears this record: another walk can leave the same row known while
149    /// it places the cursor at a different logical position.
150    cursor_rested_at: Option<Position>,
151}
152
153impl<W: Write> Screen<W> {
154    // --- Construction ---------------------------------------------------
155
156    /// Build a screen rendering into `writer`, with a managed area of `size`.
157    ///
158    /// Nothing is written and no terminal state is touched. The color profile
159    /// defaults to [`Profile::Ansi`](crate::color::Profile::Ansi) and the
160    /// optimizations to [`Optimizations::default`]; set them with
161    /// [`set_color_profile`](Self::set_color_profile) and
162    /// [`set_optimizations`](Self::set_optimizations), or let
163    /// [`Program`](crate::program::Program) detect both from the environment.
164    pub fn new(writer: W, size: impl Into<Size>) -> Self {
165        let mut renderer = Renderer::new();
166        // Defaults match inline: the surface is anchored wherever the cursor
167        // sits, so moves stay relative.
168        renderer.set_fullscreen(false);
169        renderer.set_relative_cursor(true);
170        let mut screen = Self {
171            writer,
172            front_buf: RenderBuffer::new(0, 0),
173            renderer,
174            out_buf: Vec::with_capacity(4096),
175            width: 0,
176            height: 0,
177            eaw_wide: false,
178            fullscreen: false,
179            cursor_visible: true,
180            sync_updates: false,
181            grapheme_clusters: false,
182            desired_cursor: None,
183            cursor_rested_at: None,
184        };
185        let size = size.into();
186        if size.width != 0 || size.height != 0 {
187            screen.resize(size);
188        }
189        screen
190    }
191
192    /// Borrow the writer frames are rendered into.
193    pub fn writer(&self) -> &W {
194        &self.writer
195    }
196
197    /// Borrow the writer mutably.
198    ///
199    /// Bytes written straight to the writer bypass the staging buffer, so they
200    /// can land *before* escapes already staged by drawing or property
201    /// methods. Prefer writing through the screen itself (it implements
202    /// [`Write`]), which keeps everything in order.
203    pub fn writer_mut(&mut self) -> &mut W {
204        &mut self.writer
205    }
206
207    /// Consume the screen and return its writer, discarding anything still
208    /// staged and unflushed.
209    pub fn into_writer(self) -> W {
210        self.writer
211    }
212
213    // --- Drawing -------------------------------------------------------
214
215    /// Write `cell` at `pos` in the desired frame.
216    /// A continuation is placed by the wide cell that owns it, so one passed
217    /// here is ignored and the column keeps the value its owner gave it.
218    pub fn set_cell(&mut self, pos: impl Into<Position>, cell: &Cell) {
219        self.front_buf.set_cell(pos.into(), cell);
220    }
221
222    /// Borrow the cell at `pos` mutably, marking its columns touched.
223    pub fn cell_mut(&mut self, pos: impl Into<Position>) -> Option<&mut Cell> {
224        self.front_buf.cell_mut(pos.into())
225    }
226
227    /// The managed area size in cells.
228    pub fn size(&self) -> Size {
229        Size::new(self.width, self.height)
230    }
231
232    /// Diff the staged frame against the tracked terminal, stage the
233    /// minimal escape bytes, and flush them to the writer.
234    ///
235    /// When a declarative cursor rest position has been staged with
236    /// [`set_cursor_position`](Self::set_cursor_position), the cursor is moved
237    /// there at the end of the frame, inside the same hide/synchronized-output
238    /// bracket as the cell diff, so it lands atomically and without flicker.
239    pub fn render(&mut self) -> io::Result<()> {
240        let changed = self.renderer.sync_front(&mut self.front_buf);
241        if changed || self.cursor_move_pending() {
242            self.write_frame();
243        }
244        self.flush()
245    }
246
247    /// Force a full redraw on the next [`render`](Self::render).
248    pub fn invalidate(&mut self) {
249        self.renderer.request_clear();
250    }
251
252    /// Resize the managed area. In fullscreen pass the terminal viewport
253    /// size; inline, the terminal width and the application surface height.
254    ///
255    /// Resizing discards the tracked terminal contents, so the next
256    /// [`render`](Self::render) is a full repaint. That holds even when the
257    /// size is unchanged: a resize report can follow a font or window change
258    /// that keeps the cell grid but moves where those cells land, and the
259    /// tracked contents are then wrong in a way no diff can see. An explicit
260    /// resize says the caller wants the area re-established, so it is.
261    ///
262    /// [`Program::autoresize`](crate::program::Program::autoresize) runs on
263    /// every resize report, including ones that change nothing, so it skips
264    /// the call when the size already matches rather than repainting on each.
265    pub fn resize(&mut self, size: impl Into<Size>) {
266        let size = size.into();
267        self.width = size.width;
268        self.height = size.height;
269        self.front_buf.resize(size.width, size.height);
270        self.renderer.request_clear();
271    }
272
273    /// Insert `content` into the scrollback above the managed area and flush
274    /// it to the writer. Inline this pushes the lines into the terminal's
275    /// scrollback; in fullscreen they go into the alternate screen's hidden
276    /// scrollback. The managed area is preserved in place, so no redraw is
277    /// needed and a following [`render`](Self::render) sees no change. An
278    /// empty string is a no-op.
279    ///
280    /// # Errors
281    ///
282    /// Returns any error from flushing the inserted lines to the writer.
283    pub fn insert_above(&mut self, content: &str) -> io::Result<()> {
284        if content.is_empty() {
285            return Ok(());
286        }
287
288        let width = self.width;
289        let height = self.height;
290        let y = self.renderer.cursor_position().y;
291
292        self.out_buf.write_all(b"\r").unwrap();
293        let down = height.saturating_sub(y).saturating_sub(1);
294        if down > 0 {
295            crate::ansi::cursor::write_cud(&mut self.out_buf, down).unwrap();
296        }
297
298        let lines: Vec<&str> = content.split('\n').collect();
299        let mut offset: u16 = lines.len() as u16;
300        let width_mode = self.width_mode();
301        for line in &lines {
302            let lw =
303                crate::ansi::text::string_width(line.as_bytes(), width_mode, self.eaw_wide) as u16;
304            if let Some(n) = lw.checked_div(width) {
305                offset = offset.saturating_add(n);
306            }
307        }
308
309        for _ in 0..offset {
310            self.out_buf.write_all(b"\n").unwrap();
311        }
312
313        let up = offset.saturating_add(height).saturating_sub(1);
314        if up > 0 {
315            crate::ansi::cursor::write_cuu(&mut self.out_buf, up).unwrap();
316        }
317        crate::ansi::screen::write_insert_lines(&mut self.out_buf, offset).unwrap();
318        for line in &lines {
319            self.out_buf.write_all(line.as_bytes()).unwrap();
320            self.out_buf
321                .write_all(crate::ansi::screen::ERASE_LINE_RIGHT)
322                .unwrap();
323            self.out_buf.write_all(b"\r\n").unwrap();
324        }
325
326        self.renderer.set_cursor_position(Position { y: 0, x: 0 });
327        self.flush()
328    }
329
330    // --- Cursor position ------------------------------------------------
331
332    /// Immediately move the terminal cursor to a buffer-relative position and
333    /// flush.
334    ///
335    /// The target is normalized against the managed area first: a column at or
336    /// past the width wraps into the following row, and the row is clamped to
337    /// the last row. The move is a no-op once the cursor already sits at that
338    /// normalized position, so asking for a column one past the last is a
339    /// request to wrap and is honored as one, even when the renderer is
340    /// already tracking the cursor there with its wrap pending.
341    ///
342    /// Past a cluster whose terminal width is uncertain, the move re-emits
343    /// cells from the last rendered row until it reaches the logical target.
344    /// It leaves staged edits for `render`. A target inside that cluster
345    /// rests on its primary cell. The tracked column becomes unknown because
346    /// the terminal decides how far each cluster advances.
347    ///
348    /// After a resize, screen switch, or invalidation, the move uses control
349    /// sequences until `render` establishes the displayed cells again.
350    ///
351    /// This is imperative: the move is emitted and flushed now, independent of
352    /// [`render`](Self::render). It does **not** affect the declarative resting
353    /// position staged with [`set_cursor_position`](Self::set_cursor_position);
354    /// a subsequent `render` will snap the cursor back to that sticky position
355    /// if one is set. To change where frames leave the cursor, use
356    /// `set_cursor_position` instead.
357    pub fn move_cursor_to(&mut self, pos: impl Into<Position>) -> io::Result<()> {
358        self.stage_move_cursor_to(pos.into());
359        self.flush()
360    }
361
362    /// Immediately move the terminal cursor relative to the
363    /// [tracked cursor](Self::tracked_cursor) and flush.
364    ///
365    /// Convenience over [`move_cursor_to`](Self::move_cursor_to): the target
366    /// is the tracked cursor offset by `(dx, dy)`, saturating at the buffer
367    /// origin and then normalized the same way, so a column past the width
368    /// wraps into the following row and the row is clamped to the surface. An
369    /// unknown tracked cursor is treated as the origin.
370    pub fn move_cursor_by(&mut self, dx: i16, dy: i16) -> io::Result<()> {
371        let cur = self.tracked_cursor().unwrap_or(Position::ORIGIN);
372        let x = cur.x.saturating_add_signed(dx);
373        let y = cur.y.saturating_add_signed(dy);
374        self.move_cursor_to((x, y))
375    }
376
377    /// Stage a declarative resting position for the cursor, applied at the end
378    /// of every [`render`](Self::render).
379    ///
380    /// This is the cursor analogue of [`set_cell`](Self::set_cell): it stages
381    /// intent rather than emitting now. `render` leaves the terminal cursor at
382    /// the buffer-relative `pos` after each frame's cell diff. Call
383    /// [`clear_cursor_position`](Self::clear_cursor_position) to stop steering
384    /// it and leave the cursor wherever the diff ended.
385    ///
386    /// The position is **sticky** — it persists across frames and is re-applied
387    /// on every `render` (cheaply, as a no-op when the cursor is already there)
388    /// until you change or clear it. An app whose cursor follows content
389    /// (e.g. a text field) should call this each time that content moves.
390    ///
391    /// Cursor visibility is orthogonal: this never shows or hides the cursor.
392    /// Use [`set_cursor_visible`](Self::set_cursor_visible) for that (or
393    /// [`Program::show_cursor`](crate::program::Program::show_cursor) /
394    /// [`hide_cursor`](crate::program::Program::hide_cursor), which also emit
395    /// DECTCEM). A position outside the managed area is clamped to its edges.
396    ///
397    /// The argument is anything that converts into a [`Position`], so a bare
398    /// `(x, y)` works:
399    ///
400    /// ```
401    /// # fn main() -> std::io::Result<()> {
402    /// let mut screen = uncurses::screen::Screen::new(Vec::new(), (20, 3));
403    /// screen.set_cursor_position((4, 0)); // stage
404    /// screen.clear_cursor_position();     // stop steering it
405    /// # Ok(())
406    /// # }
407    /// ```
408    pub fn set_cursor_position(&mut self, pos: impl Into<Position>) {
409        self.desired_cursor = Some(pos.into());
410    }
411
412    /// Clear the staged cursor [resting position](Self::set_cursor_position),
413    /// leaving the cursor wherever each frame's cell diff ends.
414    pub fn clear_cursor_position(&mut self) {
415        self.desired_cursor = None;
416    }
417
418    /// The renderer's tracked cursor: the buffer-relative cell where the
419    /// renderer believes the terminal cursor currently sits, or `None` when
420    /// that position is unknown (initially, after a screen reset, or after
421    /// [`invalidate_tracked_cursor`](Self::invalidate_tracked_cursor)). This
422    /// is bookkeeping, not a live cursor-position query.
423    ///
424    /// A cursor move past a cluster the
425    /// terminal may measure differently also reads as unknown. The cursor is
426    /// where it was asked to go, and the terminal is the only one that can
427    /// say which column that is. See
428    /// [`set_grapheme_clusters`](Self::set_grapheme_clusters) for what makes
429    /// a cluster uncertain and how to settle it.
430    pub fn tracked_cursor(&self) -> Option<Position> {
431        self.renderer
432            .cursor_known()
433            .then(|| self.renderer.cursor_position())
434    }
435
436    /// Mark the tracked cursor position unknown, so the next staged move
437    /// always emits rather than short-circuiting on a matching tracked
438    /// position. Use after moving the terminal cursor by a means the
439    /// renderer cannot see (e.g. a raw escape written directly).
440    pub fn invalidate_tracked_cursor(&mut self) {
441        self.renderer.invalidate_cursor();
442    }
443
444    /// Set the tracked cursor to buffer-relative `pos`, with both axes
445    /// known, *without* emitting any move. This only updates the renderer's
446    /// belief; the caller must have already placed the terminal cursor there
447    /// (e.g. with a raw escape the renderer cannot see). For an actual cursor
448    /// move use [`move_cursor_to`](Self::move_cursor_to).
449    pub fn set_tracked_cursor(&mut self, pos: impl Into<Position>) {
450        self.renderer.set_cursor_position(pos.into());
451    }
452
453    /// Clamp a buffer-relative position to the managed area's edges.
454    fn clamp_to_surface(&self, pos: Position) -> Position {
455        Position {
456            x: pos.x.min(self.width.saturating_sub(1)),
457            y: pos.y.min(self.height.saturating_sub(1)),
458        }
459    }
460
461    /// Whether a declarative cursor rest position is staged and the renderer's
462    /// tracked cursor isn't already there, so [`render`](Self::render) must
463    /// emit a move even when no cells changed.
464    fn cursor_move_pending(&self) -> bool {
465        match self.desired_cursor {
466            Some(pos) => {
467                let pos = self.clamp_to_surface(pos);
468                // A walk past an uncertain cluster ends on a column only the
469                // terminal can name, so the tracked position reads as "not
470                // there" even though the cursor is exactly where it was asked
471                // to go. Take the recorded rest as the answer while the
472                // cursor still sits where that walk left it.
473                if self.cursor_rested_at == Some(pos) && self.renderer.cursor_placed_on_row(pos.y) {
474                    return false;
475                }
476                !self.renderer.cursor_known() || self.renderer.cursor_position() != pos
477            }
478            None => false,
479        }
480    }
481
482    // --- Render properties ----------------------------------------------
483
484    /// Set whether the managed area is the whole viewport.
485    ///
486    /// `true` means the managed area covers the whole terminal and is
487    /// addressed with absolute moves — what you want on the alternate screen
488    /// buffer. `false` (the default) makes it a band in the normal buffer, as
489    /// tall as you draw and addressed with relative moves, leaving the
490    /// scrollback above and the shell prompt below intact.
491    ///
492    /// This only sets state — it emits nothing. Switching screen buffers is a
493    /// terminal mode (DECSET/DECRST 1049) and belongs to whoever owns the
494    /// terminal: [`Program::enter_alt_screen`] and
495    /// [`Program::exit_alt_screen`] emit it and set this for you. Driving a
496    /// bare `Screen`, emit the mode yourself and keep this in step, or the
497    /// renderer will address the wrong buffer.
498    ///
499    /// An actual change discards the tracked contents, so the next
500    /// [`render`](Self::render) is a full repaint.
501    ///
502    /// [`Program::enter_alt_screen`]: crate::program::Program::enter_alt_screen
503    /// [`Program::exit_alt_screen`]: crate::program::Program::exit_alt_screen
504    pub fn set_fullscreen(&mut self, fullscreen: bool) {
505        if self.fullscreen == fullscreen {
506            return;
507        }
508        self.fullscreen = fullscreen;
509        self.renderer.set_fullscreen(fullscreen);
510        self.renderer.set_relative_cursor(!fullscreen);
511        if fullscreen {
512            self.renderer.save_cursor();
513        } else {
514            self.renderer.restore_cursor();
515        }
516        // Either direction swaps which screen buffer the managed area lives
517        // on, and the other buffer holds something this renderer never wrote.
518        // Diffing against the record from the buffer being left would skip
519        // every cell the two happen to agree on, so discard it and repaint.
520        self.renderer.request_clear();
521    }
522
523    /// Whether the managed area is the whole viewport rather than a band in
524    /// the normal buffer. See [`set_fullscreen`](Self::set_fullscreen).
525    pub fn fullscreen(&self) -> bool {
526        self.fullscreen
527    }
528
529    /// Record whether the terminal cursor is visible.
530    ///
531    /// This only sets state — it emits nothing. DECTCEM is a terminal mode
532    /// and belongs to whoever owns the terminal:
533    /// [`Program::show_cursor`](crate::program::Program::show_cursor) and
534    /// [`Program::hide_cursor`](crate::program::Program::hide_cursor) emit it
535    /// and set this for you.
536    ///
537    /// The renderer needs to know only so it can bracket a frame correctly: a
538    /// *visible* cursor is hidden around the cell diff so it does not dance
539    /// across cells as the renderer repositions it, and shown again after. If
540    /// this said `true` while the cursor was actually hidden, that closing
541    /// show would turn it back on.
542    pub fn set_cursor_visible(&mut self, visible: bool) {
543        self.cursor_visible = visible;
544    }
545
546    /// Whether the terminal cursor is recorded as visible. See
547    /// [`set_cursor_visible`](Self::set_cursor_visible).
548    pub fn cursor_visible(&self) -> bool {
549        self.cursor_visible
550    }
551
552    /// Set whether text is measured per extended grapheme cluster (UTS-29
553    /// plus emoji presentation rules) rather than per code point
554    /// (wcwidth-style). Affects [`set_str`](crate::text::TextSurface::set_str)
555    /// and [`insert_above`](Self::insert_above).
556    ///
557    /// This only sets state — it emits nothing. Unicode core (DECSET 2027) is
558    /// a terminal mode and belongs to whoever owns the terminal:
559    /// [`Program::enable_grapheme_clusters`] and
560    /// [`Program::disable_grapheme_clusters`] emit it and set this for you.
561    /// Measuring differently from the terminal misplaces every cell after the
562    /// first cluster on a line, so the two must agree.
563    ///
564    /// Under the per-code-point model the renderer covers the one difference
565    /// it can predict: a cluster of several code points, which a terminal
566    /// draws as a single glyph of its own choosing. That glyph may be
567    /// narrower than the parts add up to, as with a ligated family emoji, or
568    /// wider, as with a heart that an emoji selector promotes to two columns.
569    /// Either way the renderer stops diffing that row at the cluster and
570    /// repaints the rest of it in one run, which asks nothing about where the
571    /// terminal put the glyph. That keeps the row honest on such a terminal
572    /// while measurement is per code point. Measuring whole clusters asserts
573    /// that the terminal counts them the same way, so every row takes the
574    /// ordinary path, and it is on you to have put the terminal in that mode.
575    ///
576    /// Changing the mode discards the tracked terminal contents, so the next
577    /// [`render`](Self::render) is a full repaint: what is already on screen
578    /// was measured the other way. Setting the current value is a no-op.
579    ///
580    /// The repaint clears the screen, but buffered cells keep the width they
581    /// were measured with, so re-write any text whose measurement changes.
582    /// Applications that redraw their content each frame get that for free.
583    ///
584    /// [`Program::enable_grapheme_clusters`]: crate::program::Program::enable_grapheme_clusters
585    /// [`Program::disable_grapheme_clusters`]: crate::program::Program::disable_grapheme_clusters
586    pub fn set_grapheme_clusters(&mut self, enabled: bool) {
587        if self.grapheme_clusters == enabled {
588            return;
589        }
590        self.grapheme_clusters = enabled;
591        self.renderer.set_width_mode(self.width_mode());
592        // Whatever is on screen was measured under the old model, so the
593        // tracked terminal contents no longer describe it. Diffing against
594        // that record would leave the two disagreeing about which column
595        // holds what, so discard it and repaint.
596        self.invalidate();
597    }
598
599    /// Whether text is measured per extended grapheme cluster. See
600    /// [`set_grapheme_clusters`](Self::set_grapheme_clusters).
601    pub fn grapheme_clusters(&self) -> bool {
602        self.grapheme_clusters
603    }
604
605    /// Enable or disable synchronized-output frame wrapping.
606    ///
607    /// When enabled, each non-empty [`render`](Self::render) is wrapped in
608    /// begin/end synchronized-output sequences (DEC mode 2026) so terminals
609    /// that support it present the frame atomically, with no mid-frame
610    /// repaint. Terminals that don't support 2026 ignore the markers.
611    ///
612    /// This is your switch to flip: uncurses does not second-guess it against
613    /// detected capabilities. [`Program`](crate::program::Program) enables it
614    /// automatically when the terminal reports 2026 support, which happens once
615    /// the caller has asked and read the reply, and you can override that here
616    /// at any time.
617    ///
618    /// Enabling it also changes how the cursor is handled per frame. With sync
619    /// off, a visible cursor is hidden around the cell diff so it doesn't dance
620    /// across cells as the renderer repositions it. With sync on, the frame is
621    /// presented in one step, so that hide/show pair is dropped: it is
622    /// redundant, and toggling the cursor every frame resets its blink phase,
623    /// which reads as flicker.
624    ///
625    /// This only sets state; the markers are emitted on the next `render`.
626    /// Scroll detection is gated on this, so enabling it is what lets
627    /// [`set_scroll_optimize`](Self::set_scroll_optimize) take effect; that
628    /// setting still has to be on, and the screen still has to be
629    /// [fullscreen](Self::set_fullscreen). A terminal that advertises DEC
630    /// 2026 but does not honour it therefore gets both the markers and the
631    /// scroll plans that rely on them, and a scroll's corrective repaint may
632    /// be visible. Disable scroll optimization on such a terminal.
633    ///
634    pub fn set_synchronized_output(&mut self, enabled: bool) {
635        self.sync_updates = enabled;
636        // Scroll detection is gated on this: a scroll the renderer emits can
637        // move cells that should have stayed put, and the repaint that fixes
638        // them is only invisible inside a synchronized frame.
639        self.renderer.set_sync_output(enabled);
640    }
641
642    /// Whether [synchronized output](Self::set_synchronized_output) frame
643    /// wrapping is enabled.
644    pub fn synchronized_output(&self) -> bool {
645        self.sync_updates
646    }
647
648    /// Enable or disable the renderer's scroll-detection pass.
649    ///
650    /// On by default, and best left on: when a run of rows has simply moved,
651    /// telling the terminal to move them costs a handful of bytes instead of
652    /// a repaint.
653    ///
654    /// Detection additionally requires [synchronized
655    /// output](Self::set_synchronized_output); see below. Turning this off
656    /// gives up scrolling entirely, including on frames where it would have
657    /// been safe.
658    ///
659    /// A fixed column is why that requirement exists. The scrolls uncurses
660    /// emits are always full width: rows move with `SU`, `IL`/`DL` or a bare
661    /// line feed,
662    /// and the renderer does not set the left/right margins
663    /// ([DECLRMM](crate::ansi::mode::Mode::LEFT_RIGHT_MARGIN) and
664    /// [DECSLRM](crate::ansi::screen::write_set_left_right_margins)) that
665    /// would confine them to a column range on a terminal supporting those.
666    /// So a detected scroll moves that region too, and the renderer paints it
667    /// back within the same frame. The end state is correct either way —
668    /// which is why a test that compares the finished screen sees nothing
669    /// wrong — but what the user sees is that region jumping and being put
670    /// back, on every frame, for as long as they keep scrolling.
671    ///
672    /// Because that intermediate state is only hidden when the frame is
673    /// presented in one step, detection runs **only** under
674    /// [synchronized output](Self::set_synchronized_output), which wraps the
675    /// frame in DEC 2026. Without it no scroll is emitted at all, whatever
676    /// this setting says, and rows are redrawn directly instead.
677    ///
678    /// Synchronized output is off by default, and a
679    /// [`Program`](crate::program::Program) turns it on only once the
680    /// terminal has reported 2026 support, which takes an explicit
681    /// [`query_capabilities`](crate::program::Program::query_capabilities).
682    /// An application that never asks never gets scroll optimization,
683    /// whatever its terminal supports.
684    ///
685    /// Detection is skipped outside [fullscreen](Self::set_fullscreen)
686    /// regardless of this setting.
687    ///
688    /// This only sets state; it takes effect on the next
689    /// [`render`](Self::render).
690    pub fn set_scroll_optimize(&mut self, enabled: bool) {
691        self.renderer.set_scroll_optimize(enabled);
692    }
693
694    /// Set the color profile used when emitting styled cells.
695    pub fn set_color_profile(&mut self, profile: crate::color::Profile) {
696        self.renderer.set_color_profile(profile);
697    }
698
699    /// Return the color profile used when emitting styled cells.
700    ///
701    /// This is the profile the renderer downsamples colors to, set by
702    /// [`set_color_profile`](Self::set_color_profile) or detected from the
703    /// environment by [`Program`](crate::program::Program). Pass it to
704    /// [`Encode::encode_with`](crate::text::Encode::encode_with) to serialize
705    /// a surface the same way this screen renders it.
706    pub fn color_profile(&self) -> crate::color::Profile {
707        self.renderer.color_profile()
708    }
709
710    /// Set the renderer optimization flags.
711    ///
712    /// [`TABS`](Optimizations::TABS) and [`BS`](Optimizations::BS) take
713    /// effect immediately but do not persist across a raw-mode entry:
714    /// [`Program::init`](crate::program::Program::init) and
715    /// [`Program::resume`](crate::program::Program::resume) enable both,
716    /// since raw mode is what makes them safe. Every other flag —
717    /// including [`ONLCR`](Optimizations::ONLCR), which is opt-in and
718    /// never granted — is left exactly as set here.
719    pub fn set_optimizations(&mut self, optimizations: Optimizations) {
720        self.renderer.set_optimizations(optimizations);
721    }
722
723    /// Return the renderer optimization flags currently in effect.
724    pub fn optimizations(&self) -> Optimizations {
725        self.renderer.optimizations()
726    }
727
728    // --- Render staging internals ---------------------------------------
729
730    /// Stage a single rendered frame into [`out_buf`](Self::out_buf):
731    /// synchronized-output begin, the renderer's cell diff, the optional
732    /// declarative cursor move, synchronized-output end. Assumes the front
733    /// buffer was synced.
734    ///
735    /// A visible cursor is hidden around the diff so it doesn't dance across
736    /// cells as the renderer repositions it, *unless* synchronized output is
737    /// enabled. A synchronized frame is presented in one step, so the cursor
738    /// never visibly moves mid-frame; the hide/show pair is then skipped, both
739    /// because it is redundant and because toggling DECTCEM every frame resets
740    /// the cursor's blink phase, which reads as flicker. Whether to trust
741    /// synchronized output is the caller's choice via
742    /// [`set_synchronized_output`](Self::set_synchronized_output), not gated on
743    /// detected capabilities.
744    fn write_frame(&mut self) {
745        let bracket_cursor = self.cursor_visible && !self.sync_updates;
746
747        if self.sync_updates {
748            mode::Mode::SYNCHRONIZED_OUTPUT
749                .set(&mut self.out_buf)
750                .unwrap();
751        }
752        if bracket_cursor {
753            mode::Mode::CURSOR_VISIBLE.reset(&mut self.out_buf).unwrap();
754        }
755
756        self.renderer.render_back(&mut self.out_buf).unwrap();
757
758        // Apply the declarative resting position (if any) inside the same
759        // bracket as the cell diff, so the cursor lands atomically.
760        // Sticky: re-applied every frame; move_to no-ops when already there.
761        if let Some(pos) = self.desired_cursor {
762            let pos = self.clamp_to_surface(pos);
763            self.renderer
764                .move_to_resting(&mut self.out_buf, &self.front_buf, pos.y, pos.x)
765                .unwrap();
766            self.cursor_rested_at = Some(pos);
767        }
768
769        if bracket_cursor {
770            mode::Mode::CURSOR_VISIBLE.set(&mut self.out_buf).unwrap();
771        }
772        if self.sync_updates {
773            mode::Mode::SYNCHRONIZED_OUTPUT
774                .reset(&mut self.out_buf)
775                .unwrap();
776        }
777    }
778
779    /// Stage a cursor move without flushing. See
780    /// [`move_cursor_to`](Self::move_cursor_to).
781    pub(crate) fn stage_move_cursor_to(&mut self, target: Position) {
782        self.cursor_rested_at = None;
783        let size = self.size();
784        self.renderer
785            .move_to_between_frames(&mut self.out_buf, size, target.y, target.x)
786            .unwrap();
787    }
788
789    // --- Session handoff (driven by Program) -----------------------------
790
791    /// Stage a move to the bottom row of the *last rendered* surface.
792    ///
793    /// Used before a shell handoff so the cursor lands below the managed
794    /// area. Deliberately uses the last-render height rather than the live
795    /// height, so a terminal that grew between the last render and the
796    /// handoff does not push the cursor below where the user started.
797    pub(crate) fn park_cursor(&mut self) -> io::Result<()> {
798        let (last_width, last_height) = self.renderer.last_size();
799        if last_height > 0 {
800            let last = Size::new(last_width, last_height);
801            self.renderer
802                .move_to_between_frames(&mut self.out_buf, last, last_height - 1, 0)?;
803        }
804        Ok(())
805    }
806
807    /// Save the renderer's inline cursor anchor before the terminal switches
808    /// to the alternate screen, so leaving it can restore the anchor.
809    pub(crate) fn save_cursor(&mut self) {
810        self.renderer.save_cursor();
811    }
812
813    /// Restore the inline cursor anchor saved by
814    /// [`save_cursor`](Self::save_cursor).
815    pub(crate) fn restore_cursor(&mut self) {
816        self.renderer.restore_cursor();
817    }
818
819    /// Forget where the cursor is.
820    ///
821    /// The terminal is being handed back to the shell. Once it returns (e.g.
822    /// after a suspend/resume, possibly with a resize that reflowed the
823    /// surface), the tracked position is void; forget it so the next frame
824    /// re-anchors at the current physical position instead of stepping up
825    /// from a stale row and overwriting content above the surface.
826    pub(crate) fn invalidate_cursor(&mut self) {
827        self.renderer.invalidate_cursor();
828    }
829}
830
831impl<W: Write> Write for Screen<W> {
832    /// Append raw bytes to the staging buffer, ordered with any staged mode
833    /// or frame bytes. They reach the writer on the next [`flush`](Self::flush).
834    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
835        self.out_buf.extend_from_slice(buf);
836        Ok(buf.len())
837    }
838
839    /// Drain the staging buffer to the writer and flush it.
840    fn flush(&mut self) -> io::Result<()> {
841        if !self.out_buf.is_empty() {
842            #[cfg(debug_assertions)]
843            crate::trace::tee_output(&self.out_buf);
844            self.writer.write_all(&self.out_buf)?;
845            self.out_buf.clear();
846        }
847        self.writer.flush()
848    }
849}
850
851impl<W: Write> Bounded for Screen<W> {
852    fn bounds(&self) -> Rect {
853        self.front_buf.bounds()
854    }
855}
856
857impl<W: Write> Surface for Screen<W> {
858    fn cell(&self, pos: Position) -> Option<&Cell> {
859        self.front_buf.cell(pos)
860    }
861}
862
863impl<W: Write> SurfaceMut for Screen<W> {
864    fn set_cell(&mut self, pos: Position, cell: &Cell) {
865        self.front_buf.set_cell(pos, cell);
866    }
867
868    fn cell_mut(&mut self, pos: Position) -> Option<&mut Cell> {
869        self.front_buf.cell_mut(pos)
870    }
871
872    fn insert_lines(&mut self, y: u16, n: u16, bounds_bottom: u16, fill: &Cell) {
873        self.front_buf.insert_lines(y, n, bounds_bottom, fill);
874    }
875
876    fn delete_lines(&mut self, y: u16, n: u16, bounds_bottom: u16, fill: &Cell) {
877        self.front_buf.delete_lines(y, n, bounds_bottom, fill);
878    }
879
880    fn insert_cells(&mut self, pos: Position, n: u16, bounds_right: u16, fill: &Cell) {
881        self.front_buf.insert_cells(pos, n, bounds_right, fill);
882    }
883
884    fn delete_cells(&mut self, pos: Position, n: u16, bounds_right: u16, fill: &Cell) {
885        self.front_buf.delete_cells(pos, n, bounds_right, fill);
886    }
887}
888
889impl<W: Write> TextSurface for Screen<W> {
890    fn width_mode(&self) -> WidthMode {
891        if self.grapheme_clusters {
892            WidthMode::Grapheme
893        } else {
894            WidthMode::Wc
895        }
896    }
897
898    fn eaw_wide(&self) -> bool {
899        self.eaw_wide
900    }
901}