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