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}