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}