uncurses_ratatui/backend.rs
1use std::io::{self, Write};
2use std::time::{Duration, Instant};
3
4use ratatui::Viewport;
5use ratatui::backend::{Backend, ClearType, WindowSize};
6use ratatui::buffer::Cell as RtCell;
7use ratatui::layout::{Position as RtPosition, Size as RtSize};
8use uncurses::buffer::SurfaceMut;
9use uncurses::cell::Cell as CzCell;
10use uncurses::event::{Event, Input};
11use uncurses::layout::Position;
12use uncurses::screen::{Screen, ScreenOptions};
13use uncurses::terminal::{Stdin, Stdout, TtyInput, TtyOutput};
14
15use crate::convert::cell_from_ratatui;
16
17/// Platform bound required for an output handle usable by the backend.
18///
19/// The handle must be writable, cheaply copyable, and expose the platform OS
20/// handle used by terminal mode and window-size operations. Process stdio and
21/// controlling-terminal output handles satisfy this bound.
22///
23/// This trait is sealed only by its bounds: any type that implements the listed
24/// platform traits implements `Output` automatically. It is the output
25/// counterpart to the `Input` bound on the backend's input handle.
26#[cfg(unix)]
27pub trait Output: Write + Copy + std::os::fd::AsFd {}
28#[cfg(unix)]
29impl<T: Write + Copy + std::os::fd::AsFd> Output for T {}
30/// Platform bound required for an output handle usable by the backend.
31///
32/// The handle must be writable, cheaply copyable, and expose the platform OS
33/// handle used by terminal mode and window-size operations. Process stdio and
34/// controlling-terminal output handles satisfy this bound.
35///
36/// This trait is sealed only by its bounds: any type that implements the listed
37/// platform traits implements `Output` automatically. It is the output
38/// counterpart to the `Input` bound on the backend's input handle.
39#[cfg(windows)]
40pub trait Output: Write + Copy + std::os::windows::io::AsHandle {}
41#[cfg(windows)]
42impl<T: Write + Copy + std::os::windows::io::AsHandle> Output for T {}
43
44/// How long [`Backend::get_cursor_position`] waits for a cursor-position
45/// report before falling back to the origin. The widget library calls it at
46/// most once per inline-viewport setup, so a small budget keeps setup
47/// responsive on terminals that never answer.
48const CURSOR_QUERY_TIMEOUT: Duration = Duration::from_millis(100);
49
50/// Extract a cursor-position report from a reply event. The report is the
51/// [`Event::CursorPosition`] variant, but at terminal row 1 the wire form
52/// collides with a modified-F3 key and is decoded as an [`Event::Multi`]
53/// carrying both; unwrap that case too.
54fn cursor_position_report(ev: &Event) -> Option<Position> {
55 match ev {
56 Event::CursorPosition(pos) => Some(*pos),
57 Event::Multi(events) => events.iter().find_map(|e| match e {
58 Event::CursorPosition(pos) => Some(*pos),
59 _ => None,
60 }),
61 _ => None,
62 }
63}
64
65/// Backend implementation that drives rendering, input, and lifecycle through
66/// one [`Screen`].
67///
68/// ## What it wraps
69///
70/// The wrapped screen owns the terminal handle, the cell buffer, and the event
71/// source. Keeping those pieces behind one backend means frame rendering,
72/// cursor movement, clearing, size tracking, raw-mode setup, and input reads all
73/// observe the same terminal state.
74///
75/// ## Rendering
76///
77/// [`Backend::draw`] converts each concrete buffer cell to an uncurses cell and
78/// writes it into the screen's buffer, staging the frame without any I/O.
79/// [`Backend::flush`] then calls [`Screen::render`], which diffs the buffer,
80/// stages the minimal escape bytes, and flushes them through the screen.
81///
82/// ```text
83/// ┌─────────────────────┐
84/// │ Frame buffer │
85/// └─────────┬───────────┘
86/// │ buffer cells
87/// ▼
88/// ┌─────────────────────┐
89/// │ UncursesBackend │
90/// │ draw + conversion │
91/// └─────────┬───────────┘
92/// │ Screen::set_cell
93/// ▼
94/// ┌─────────────────────┐
95/// │ Screen (diff render) │
96/// │ diff against output │
97/// └─────────┬───────────┘
98/// │ flush → Screen::render
99/// ▼
100/// terminal
101/// ```
102///
103/// ## Viewports
104///
105/// The default viewport is [`Viewport::Fullscreen`]. The init helpers call
106/// [`set_viewport`](Self::set_viewport) with the viewport stored in
107/// terminal options. Inline viewports keep an absolute origin in
108/// `inline_origin`; drawing subtracts that origin so the screen buffer contains
109/// only the inline region.
110///
111/// ## Events
112///
113/// Use [`poll_event`](Self::poll_event),
114/// [`try_read_event`](Self::try_read_event), and [`read_event`](Self::read_event)
115/// for synchronous loops, or [`event_stream`](Self::event_stream) with the
116/// `async` feature. Every read is pure, exactly like [`Screen`]'s: call
117/// [`observe_event`](Self::observe_event) on each event to keep capability
118/// tracking alive, or skip it and reads still work.
119///
120/// ## Setup
121///
122/// Construction is inert: it does not enter raw mode, enter the alternate
123/// screen, hide the cursor, or choose a non-default viewport. Call
124/// [`init`](Self::init) or [`init_with`](Self::init_with) for manual setup, or
125/// use the crate-level setup helpers for process stdio. Call
126/// [`restore`](Self::restore) when the session ends.
127pub struct UncursesBackend<I: Input, O: Write> {
128 screen: Screen<I, O>,
129 /// The widget-library viewport, set via [`set_viewport`](Self::set_viewport)
130 /// (by `init_with_options`). Determines the screen buffer height
131 /// (inline height vs full terminal height) and whether `draw` /
132 /// `set_cursor_position` translate absolute rows into the inline
133 /// region.
134 viewport: Viewport,
135 /// Top row of an inline viewport. Seeded at initial setup from
136 /// [`get_cursor_position`](Backend::get_cursor_position), then kept
137 /// exact across resizes by [`clear_region`](Backend::clear_region),
138 /// which observes the cursor the widget library parks at the recomputed
139 /// viewport top before clearing. Absolute rows are translated down by this when
140 /// rendering an inline viewport.
141 inline_origin: u16,
142 /// Last full terminal size observed by [`size`](Backend::size) /
143 /// [`window_size`](Backend::window_size). When it changes the screen
144 /// is marked stale (`size_dirty`) so the next [`draw`](Backend::draw)
145 /// repaints in full. Tracked behind `Cell` because `size` takes
146 /// `&self`.
147 last_size: std::cell::Cell<(u16, u16)>,
148 /// Set when `last_size` changes; consumed by `draw` to invalidate the
149 /// screen.
150 size_dirty: std::cell::Cell<bool>,
151 /// Absolute row last requested by
152 /// [`set_cursor_position`](Backend::set_cursor_position). When the widget
153 /// library recomputes an inline viewport (initial setup and every resize) it
154 /// positions the cursor at the viewport's top row before clearing it,
155 /// so [`clear_region`](Backend::clear_region) reads this back as the
156 /// fresh `inline_origin` — the only place the *true* viewport top is
157 /// observable (the cursor reported by `get_cursor_position` sits at the
158 /// app's cursor, which need not be the viewport top).
159 last_cursor_row: u16,
160}
161
162impl UncursesBackend<Stdin, Stdout> {
163 /// Build a backend over process standard input and output.
164 ///
165 /// This constructs a [`Screen`] with `stdin` and `stdout`, then wraps it in
166 /// [`UncursesBackend::new`]. It does not enter raw mode, hide the cursor,
167 /// enter the alternate screen, or apply screen options.
168 ///
169 /// ## Returns
170 ///
171 /// A backend ready for manual setup or for construction of a widget-library
172 /// terminal.
173 ///
174 /// ## Errors
175 ///
176 /// Returns errors from [`Screen::stdio`], including failures to inspect the
177 /// terminal size or initialize the input event source.
178 ///
179 /// ## Panics
180 ///
181 /// Does not intentionally panic.
182 ///
183 /// ## Usage note
184 ///
185 /// Prefer crate-level setup helpers when process stdio and conventional
186 /// setup are sufficient.
187 pub fn stdio() -> io::Result<Self> {
188 Ok(Self::new(Screen::stdio()?))
189 }
190}
191
192impl UncursesBackend<TtyInput, TtyOutput> {
193 /// Build a backend over the controlling terminal instead of process stdio.
194 ///
195 /// This opens the platform controlling terminal (`/dev/tty` on Unix,
196 /// console handles on Windows), constructs a [`Screen`], and wraps it in
197 /// [`UncursesBackend::new`]. It is useful when standard input or output is
198 /// redirected but the application still needs an interactive terminal.
199 ///
200 /// ## Returns
201 ///
202 /// A backend ready for manual setup or for construction of a widget-library
203 /// terminal.
204 ///
205 /// ## Errors
206 ///
207 /// Returns errors from opening or initializing the controlling terminal,
208 /// sizing the buffer, or creating the input event source.
209 ///
210 /// ## Panics
211 ///
212 /// Does not intentionally panic.
213 ///
214 /// ## Usage note
215 ///
216 /// Like [`stdio`](UncursesBackend::stdio), this constructor is inert; call
217 /// [`init`](UncursesBackend::init) or
218 /// [`init_with`](UncursesBackend::init_with) before interactive use.
219 pub fn open() -> io::Result<Self> {
220 Ok(Self::new(Screen::open()?))
221 }
222}
223
224impl<I, O> UncursesBackend<I, O>
225where
226 I: Input,
227 O: Write,
228{
229 /// Build a backend over an existing [`Screen`].
230 ///
231 /// Use this when the screen has been constructed by the caller, or when the
232 /// terminal handles are not process stdio or the controlling terminal. The
233 /// backend starts with [`Viewport::Fullscreen`], an inline origin of `0`,
234 /// no remembered terminal size, and no dirty-size flag.
235 ///
236 /// ## Parameters
237 ///
238 /// * `screen` - the screen facade that will own rendering, input, and
239 /// terminal lifecycle for this backend.
240 ///
241 /// ## Returns
242 ///
243 /// A backend wrapping `screen`.
244 ///
245 /// ## Panics
246 ///
247 /// Does not panic.
248 ///
249 /// ## Usage note
250 ///
251 /// This does not call [`Screen::init`]. Initialize the screen through the
252 /// backend or manually before starting an interactive session.
253 pub fn new(screen: Screen<I, O>) -> Self {
254 Self {
255 screen,
256 viewport: Viewport::Fullscreen,
257 inline_origin: 0,
258 last_size: std::cell::Cell::new((0, 0)),
259 size_dirty: std::cell::Cell::new(false),
260 last_cursor_row: 0,
261 }
262 }
263
264 /// Record an observed full terminal size; if it differs from the last,
265 /// flag the screen stale so the next [`draw`](Backend::draw) repaints
266 /// in full. Takes `&self` so `size`/`window_size` can call it.
267 fn note_size(&self, size: (u16, u16)) {
268 if self.last_size.get() != size {
269 self.last_size.set(size);
270 self.size_dirty.set(true);
271 }
272 }
273
274 /// Record the viewport used by the surrounding terminal.
275 ///
276 /// The setup helpers call this with the viewport from terminal options. The
277 /// backend uses it to size the screen buffer and, for inline viewports,
278 /// translate absolute frame rows into the inline buffer region. The default before this method is called is
279 /// [`Viewport::Fullscreen`].
280 ///
281 /// ## Parameters
282 ///
283 /// * `viewport` - the viewport selected for the terminal.
284 ///
285 /// ## Panics
286 ///
287 /// Does not panic.
288 ///
289 /// ## Usage note
290 ///
291 /// For [`Viewport::Inline`], the screen buffer is resized immediately to
292 /// the requested height clamped to the current terminal height. Fullscreen
293 /// and fixed viewports are stored without resizing here; drawing keeps the
294 /// screen in step with the current full size.
295 pub fn set_viewport(&mut self, viewport: Viewport) {
296 if let Viewport::Inline(height) = viewport {
297 let size = self.screen.size();
298 let h = height.min(size.height);
299 self.screen.resize((size.width, h));
300 }
301 self.viewport = viewport;
302 }
303
304 /// Borrow the wrapped [`Screen`] facade.
305 ///
306 /// Use this for read-only access to screen state such as cached capability
307 /// or size information. Rendering and input operations that mutate the
308 /// screen require [`screen_mut`](Self::screen_mut).
309 ///
310 /// ## Returns
311 ///
312 /// A shared reference to the screen owned by this backend.
313 ///
314 /// ## Panics
315 ///
316 /// Does not panic.
317 pub fn screen(&self) -> &Screen<I, O> {
318 &self.screen
319 }
320
321 /// Mutably borrow the wrapped [`Screen`] facade.
322 ///
323 /// Use this for screen operations not surfaced by the backend: setting
324 /// screen modes, using the alternate screen directly, configuring renderer
325 /// options, or manual rendering. For the async event stream, prefer the
326 /// backend's own [`event_stream`](Self::event_stream) paired with
327 /// [`observe_event`](Self::observe_event).
328 ///
329 /// ## Returns
330 ///
331 /// A mutable reference to the screen owned by this backend.
332 ///
333 /// ## Panics
334 ///
335 /// Does not panic.
336 ///
337 /// ## Usage note
338 ///
339 /// Avoid mixing manual buffer writes with normal backend drawing unless the
340 /// ordering is deliberate; both paths affect the same buffer.
341 pub fn screen_mut(&mut self) -> &mut Screen<I, O> {
342 &mut self.screen
343 }
344
345 /// Poll the wrapped screen's input source.
346 ///
347 /// This delegates to [`Screen::poll_event`], which drives the underlying
348 /// event source for at most `timeout`. It does not remove an event from the
349 /// queue; call [`try_read_event`](Self::try_read_event) or
350 /// [`read_event`](Self::read_event) after it reports availability.
351 ///
352 /// ## Parameters
353 ///
354 /// * `timeout` - `Some(duration)` to wait up to that duration, or `None` to
355 /// use the event source's blocking poll behavior.
356 ///
357 /// ## Returns
358 ///
359 /// `Ok(true)` if an event is available, `Ok(false)` if the poll timed out.
360 ///
361 /// ## Errors
362 ///
363 /// Returns I/O errors from the input source.
364 ///
365 /// ## Panics
366 ///
367 /// Panics if the screen's internal event-source lock is poisoned.
368 ///
369 /// ## Usage note
370 ///
371 /// Polling through the backend keeps capability detection and application
372 /// input on the same event source.
373 pub fn poll_event(&mut self, timeout: Option<Duration>) -> io::Result<bool> {
374 self.screen.poll_event(timeout)
375 }
376
377 /// Try to read the next queued event without blocking.
378 ///
379 /// This delegates to the wrapped [`Screen`]'s pure read. Feed the returned
380 /// event to [`observe_event`](Self::observe_event) if you want capability
381 /// tracking; skipping it still reads fine.
382 ///
383 /// ## Returns
384 ///
385 /// `Some(event)` when an event was already queued; `None` when reading would
386 /// require blocking or additional I/O.
387 ///
388 /// ## Panics
389 ///
390 /// Panics if the screen's internal event-source lock is poisoned.
391 ///
392 /// ## Usage note
393 ///
394 /// Pair this with [`poll_event`](Self::poll_event) for timeout-based loops.
395 pub fn try_read_event(&mut self) -> Option<Event> {
396 self.screen.try_read_event()
397 }
398
399 /// Block until the next event is available.
400 ///
401 /// This delegates to the wrapped [`Screen`]'s pure read. Feed the returned
402 /// event to [`observe_event`](Self::observe_event) if you want capability
403 /// tracking; skipping it still reads fine.
404 ///
405 /// ## Returns
406 ///
407 /// The next decoded terminal [`Event`].
408 ///
409 /// ## Errors
410 ///
411 /// Returns I/O errors from the input source.
412 ///
413 /// ## Panics
414 ///
415 /// Panics if the screen's internal event-source lock is poisoned.
416 ///
417 /// ## Usage note
418 ///
419 /// Use this for simple blocking event loops. Use
420 /// [`event_stream`](Self::event_stream) instead when the `async` feature is
421 /// enabled and the application is already asynchronous.
422 pub fn read_event(&mut self) -> io::Result<Event> {
423 self.screen.read_event()
424 }
425
426 /// Feed an event back through the wrapped [`Screen`] for capability
427 /// tracking.
428 ///
429 /// Reads on this backend are pure, exactly like [`Screen`]'s. Call this once
430 /// per event, on both the sync ([`read_event`](Self::read_event),
431 /// [`try_read_event`](Self::try_read_event)) and async
432 /// ([`event_stream`](Self::event_stream)) paths, to keep capability
433 /// detection, resize handling, and discovery-driven defaults alive. Skip it
434 /// and reads still work, you just forgo those upgrades.
435 ///
436 /// ## Errors
437 ///
438 /// Returns I/O errors from applying discovery-driven screen defaults
439 /// triggered by capability replies.
440 pub fn observe_event(&mut self, event: &Event) -> io::Result<()> {
441 self.screen.observe_event(event)
442 }
443
444 /// Build an async [`EventStream`](uncurses::event::EventStream) over the
445 /// wrapped screen's input.
446 ///
447 /// The stream shares the screen's decoder, so it does not race the sync read
448 /// methods on the same file descriptor. Like every read on this backend it
449 /// yields events without observing them; pair it with
450 /// [`observe_event`](Self::observe_event) in your `select!` loop to keep
451 /// capability tracking alive.
452 ///
453 /// ## Returns
454 ///
455 /// An owned `EventStream` you can hold alongside `&mut self` (it shares the
456 /// source by handle rather than borrowing the backend).
457 #[cfg(feature = "async")]
458 pub fn event_stream(&self) -> uncurses::event::EventStream<I>
459 where
460 I: 'static,
461 {
462 self.screen.event_stream()
463 }
464}
465
466impl<I, O> UncursesBackend<I, O>
467where
468 I: Input + Copy,
469 O: Output,
470{
471 /// Begin an interactive session with default [`ScreenOptions`].
472 ///
473 /// This delegates to [`Screen::init`]: the screen enters raw mode, applies
474 /// always-on defaults, and stages its terminal capability queries. It does
475 /// not enter the alternate screen or hide the cursor by itself; the
476 /// crate-level setup helpers perform those additional steps.
477 ///
478 /// ## Returns
479 ///
480 /// `Ok(())` after raw mode and screen initialization have been staged.
481 ///
482 /// ## Errors
483 ///
484 /// Returns errors from raw-mode setup, autoresizing, bracketed paste setup,
485 /// or staging capability queries.
486 ///
487 /// ## Panics
488 ///
489 /// Does not intentionally panic.
490 ///
491 /// ## Usage note
492 ///
493 /// Pair successful manual initialization with [`restore`](Self::restore).
494 pub fn init(&mut self) -> io::Result<()> {
495 self.screen.init()
496 }
497
498 /// Begin an interactive session with explicit [`ScreenOptions`].
499 ///
500 /// This delegates to [`Screen::init_with`], allowing the caller to choose
501 /// bracketed paste, keyboard enhancements, mouse tracking, in-band resize
502 /// preference, and pixel-size behavior before capability queries are staged.
503 /// It does not enter the alternate screen or hide the cursor by itself.
504 ///
505 /// ## Parameters
506 ///
507 /// * `options` - screen defaults to apply during initialization.
508 ///
509 /// ## Returns
510 ///
511 /// `Ok(())` after raw mode and screen initialization have been staged.
512 ///
513 /// ## Errors
514 ///
515 /// Returns errors from raw-mode setup, autoresizing, always-on mode setup,
516 /// or staging capability queries.
517 ///
518 /// ## Panics
519 ///
520 /// Does not intentionally panic.
521 ///
522 /// ## Usage note
523 ///
524 /// Pair successful manual initialization with [`restore`](Self::restore).
525 pub fn init_with(&mut self, options: ScreenOptions) -> io::Result<()> {
526 self.screen.init_with(options)
527 }
528
529 /// Restore terminal state after a backend-managed session.
530 ///
531 /// This delegates to [`Screen::pause`]. It tears down staged modes, resets
532 /// buffer-controlled state such as alternate screen and cursor visibility,
533 /// flushes pending output, and restores the terminal mode while keeping the
534 /// screen available for future use.
535 ///
536 /// ## Returns
537 ///
538 /// `Ok(())` after teardown and terminal-mode restoration complete.
539 ///
540 /// ## Errors
541 ///
542 /// Returns errors from mode teardown, flushing, or terminal restoration.
543 ///
544 /// ## Panics
545 ///
546 /// Does not intentionally panic.
547 ///
548 /// ## Usage note
549 ///
550 /// Treat this as the single teardown entry point for backend-managed setup.
551 pub fn restore(&mut self) -> io::Result<()> {
552 self.screen.pause()
553 }
554}
555
556impl<I, O> Write for UncursesBackend<I, O>
557where
558 I: Input,
559 O: Write,
560{
561 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
562 self.screen.write(buf)
563 }
564
565 fn flush(&mut self) -> io::Result<()> {
566 Write::flush(&mut self.screen)
567 }
568}
569
570impl<I, O> Backend for UncursesBackend<I, O>
571where
572 I: Input + Copy,
573 O: Output,
574{
575 type Error = io::Error;
576
577 /// Stage a frame's cells into the wrapped screen buffer.
578 ///
579 /// The iterator supplies absolute buffer coordinates and concrete cells.
580 /// Each cell is converted to an uncurses cell and written to the screen.
581 /// This method only stages into the buffer; the buffer diff is computed
582 /// and written when the surrounding terminal calls [`Backend::flush`].
583 ///
584 /// ## Parameters
585 ///
586 /// * `content` - visible frame cells as `(x, y, cell)` triples.
587 ///
588 /// ## Errors
589 ///
590 /// This implementation performs no I/O and returns `Ok(())`; renderer and
591 /// output errors surface from [`Backend::flush`].
592 ///
593 /// ## Usage note
594 ///
595 /// Inline viewports translate `y` by the stored inline origin. A detected
596 /// terminal-size change invalidates the screen so this frame repaints in
597 /// full.
598 fn draw<'a, J>(&mut self, content: J) -> io::Result<()>
599 where
600 J: Iterator<Item = (u16, u16, &'a RtCell)>,
601 {
602 // Keep the screen buffer in step with what the widget library draws into.
603 // For an inline viewport the buffer is only the inline height and
604 // the widget library's absolute rows are translated down by the viewport top;
605 // otherwise it tracks the full terminal size.
606 let size = self.screen.size();
607 let (full_w, full_h) = self
608 .screen
609 .get_window_size()
610 .ok()
611 .map(|s| (s.col, s.row))
612 .filter(|&(c, r)| c != 0 && r != 0)
613 .unwrap_or((size.width, size.height));
614 let (w, h, top) = match self.viewport {
615 Viewport::Inline(height) => {
616 let h = height.min(full_h);
617 let top = self.inline_origin.min(full_h.saturating_sub(h));
618 (full_w, h, top)
619 }
620 _ => (full_w, full_h, 0),
621 };
622 // Repaint in full if the terminal size changed since the last
623 // observation (covers cases where the buffer dimensions stay the
624 // same, e.g. an inline viewport on a vertical-only resize).
625 self.note_size((full_w, full_h));
626 if self.size_dirty.take() {
627 self.screen.invalidate();
628 }
629 if (w, h) != (size.width, size.height) {
630 self.screen.invalidate();
631 self.screen.resize((w, h));
632 }
633 for (x, y, rc) in content {
634 let cell = cell_from_ratatui(rc);
635 self.screen.set_cell((x, y.saturating_sub(top)), &cell);
636 }
637 Ok(())
638 }
639
640 /// Hide the terminal cursor immediately.
641 ///
642 /// Delegates to [`Screen::hide_cursor`], which stages cursor visibility on
643 /// the buffer and flushes before returning.
644 ///
645 /// ## Errors
646 ///
647 /// Returns output errors from flushing the visibility change.
648 fn hide_cursor(&mut self) -> io::Result<()> {
649 self.screen.hide_cursor()
650 }
651
652 /// Show the terminal cursor immediately.
653 ///
654 /// Delegates to [`Screen::show_cursor`], which stages cursor visibility on
655 /// the buffer and flushes before returning.
656 ///
657 /// ## Errors
658 ///
659 /// Returns output errors from flushing the visibility change.
660 fn show_cursor(&mut self) -> io::Result<()> {
661 self.screen.show_cursor()
662 }
663
664 /// Query the terminal for its current cursor position.
665 ///
666 /// This sends a cursor-position request, then polls input until a
667 /// [`Event::CursorPosition`] report arrives or the short setup timeout
668 /// expires. Non-report events read while waiting are unread back into the
669 /// screen in their original order so the application can still consume them.
670 /// If no report arrives, the backend returns the origin.
671 ///
672 /// ## Returns
673 ///
674 /// The zero-based cursor position reported by the terminal, or `(0, 0)` on
675 /// timeout. Inline viewports also seed their initial origin from this row.
676 ///
677 /// ## Errors
678 ///
679 /// Returns errors from writing the cursor-position request or polling the
680 /// input source.
681 ///
682 /// ## Usage note
683 ///
684 /// The reply parser also accepts the multi-event ambiguity that occurs when
685 /// the row-1 CPR wire form collides with a modified function key sequence.
686 fn get_cursor_position(&mut self) -> io::Result<RtPosition> {
687 // Query the terminal for its cursor position (CPR): write the
688 // request, then read events until the report arrives or the timeout
689 // elapses. The reply is absolute, zero-based, and matches the widget library's
690 // coordinate space (the same space `set_cursor_position` writes
691 // absolute moves into), so it needs no translation. Fall back to the
692 // origin if the terminal does not answer.
693 self.screen.request_cursor_position()?;
694 let deadline = Instant::now() + CURSOR_QUERY_TIMEOUT;
695 // Events read while waiting for the report are not ours to consume;
696 // stash them and put them back (in original order) so the app's loop
697 // still sees them.
698 let mut stash: Vec<Event> = Vec::new();
699 let found = loop {
700 let Some(remaining) = deadline.checked_duration_since(Instant::now()) else {
701 break None;
702 };
703 if !self.screen.poll_event(Some(remaining))? {
704 break None;
705 }
706 match self.screen.try_read_event() {
707 Some(ev) => match cursor_position_report(&ev) {
708 Some(pos) => break Some(pos),
709 None => stash.push(ev),
710 },
711 None => continue,
712 }
713 };
714 for ev in stash.into_iter().rev() {
715 self.screen.unread_event(ev);
716 }
717 let pos = found.unwrap_or(Position::new(0, 0));
718 // the widget library calls this to anchor an inline viewport at the cursor row,
719 // then draws content at absolute rows starting there. Seed the
720 // inline origin so the very first `draw` translates those absolute
721 // rows into the inline buffer; before this the origin defaulted to 0
722 // and a cursor below the top clipped the first frame. The exact top
723 // is still re-derived by `clear_region` on later resizes.
724 if matches!(self.viewport, Viewport::Inline(_)) {
725 self.inline_origin = pos.y;
726 self.last_cursor_row = pos.y;
727 }
728 Ok(RtPosition { x: pos.x, y: pos.y })
729 }
730
731 /// Move the terminal cursor to an absolute position immediately.
732 ///
733 /// The backend writes an absolute CUP escape directly, records the row for
734 /// inline-viewport bookkeeping, updates the renderer's tracked cursor
735 /// position in buffer-relative coordinates, and flushes.
736 ///
737 /// ## Parameters
738 ///
739 /// * `position` - zero-based absolute cursor position requested by the
740 /// surrounding terminal.
741 ///
742 /// ## Errors
743 ///
744 /// Returns output errors from writing or flushing the cursor movement.
745 ///
746 /// ## Usage note
747 ///
748 /// Direct absolute movement keeps the renderer and inline viewport aligned;
749 /// it intentionally bypasses the renderer's cost-optimized relative moves.
750 fn set_cursor_position<P: Into<RtPosition>>(&mut self, position: P) -> io::Result<()> {
751 let p = position.into();
752 // Remember the requested row: when the widget library clears an inline
753 // viewport it places the cursor at the viewport top first, letting
754 // `clear_region` recover the (possibly shifted) origin on resize.
755 self.last_cursor_row = p.y;
756 // Emit an absolute CUP directly rather than going through the
757 // renderer's cost-optimized (possibly relative) move: ratatui calls
758 // this to place its own cursor, so the move must be unconditional
759 // and absolute.
760 uncurses::ansi::cursor::write_cup(&mut self.screen, p.y, p.x)?;
761 // Keep the renderer's cursor bookkeeping in step with the move we
762 // just made, translated into the (inline) buffer. In relative-cursor
763 // mode merely invalidating would lose the absolute row — the next
764 // frame's vertical moves would drift the viewport — so assert the
765 // exact buffer-relative position instead. For a non-inline viewport
766 // `inline_origin` is 0, so this is the absolute position unchanged.
767 let top = match self.viewport {
768 Viewport::Inline(_) => self.inline_origin,
769 _ => 0,
770 };
771 self.screen
772 .set_tracked_cursor((p.x, p.y.saturating_sub(top)));
773 Write::flush(&mut self.screen)
774 }
775
776 /// Clear the entire backend surface immediately.
777 ///
778 /// This delegates to [`Backend::clear_region`] with [`ClearType::All`].
779 ///
780 /// ## Errors
781 ///
782 /// Returns output errors from rendering and flushing the staged blank cells.
783 fn clear(&mut self) -> io::Result<()> {
784 self.clear_region(ClearType::All)
785 }
786
787 /// Clear part of the backend surface immediately.
788 ///
789 /// The implementation blanks only the cells covered by `clear_type` in the
790 /// screen's staging buffer, invalidates tracked cursor state, renders the
791 /// diff, and flushes before returning. For inline viewports,
792 /// [`ClearType::AfterCursor`] is also the resize/viewport-reanchor path: the
793 /// last absolute cursor row becomes the new inline origin and the full
794 /// inline buffer is blanked.
795 ///
796 /// ## Parameters
797 ///
798 /// * `clear_type` - the clear region requested by the surrounding terminal.
799 ///
800 /// ## Errors
801 ///
802 /// Returns output errors from rendering or flushing the clear operation.
803 ///
804 /// ## Usage note
805 ///
806 /// Clearing is immediate by backend contract; unlike [`Backend::draw`], this
807 /// method does not wait for a later flush call to make output visible.
808 fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
809 let size = self.screen.size();
810 let w = size.width;
811 let h = size.height;
812 let cursor = self.screen.tracked_cursor().unwrap_or_default();
813 if w == 0 || h == 0 {
814 return Ok(());
815 }
816 let region = match clear_type {
817 ClearType::All => Some(uncurses::layout::Rect::new(0, 0, w, h)),
818 ClearType::AfterCursor if matches!(self.viewport, Viewport::Inline(_)) => {
819 // Inline-viewport resize/clear path. ratatui homes the
820 // cursor to the viewport's (recomputed) top row, then erases
821 // to the end of the screen — for our inline buffer that is
822 // the whole thing. Adopt the fresh origin (the app cursor
823 // reported by `get_cursor_position` may sit anywhere in the
824 // viewport, so this is the authoritative top), and blank the
825 // entire staging buffer so the upcoming full repaint starts
826 // clean: the staging buffer preserves overlapping cells
827 // across a grow, which a diff-style painter never overwrites,
828 // and would otherwise duplicate the previous frame's right
829 // edge.
830 self.inline_origin = self.last_cursor_row;
831 Some(uncurses::layout::Rect::new(0, 0, w, h))
832 }
833 ClearType::AfterCursor => {
834 if cursor.y < h {
835 let tail_x = cursor.x.min(w);
836 self.screen.fill_rect(
837 uncurses::layout::Rect::new(tail_x, cursor.y, w - tail_x, 1),
838 &CzCell::BLANK,
839 );
840 }
841 (cursor.y + 1 < h)
842 .then(|| uncurses::layout::Rect::new(0, cursor.y + 1, w, h - cursor.y - 1))
843 }
844 ClearType::BeforeCursor => {
845 if cursor.y > 0 {
846 self.screen.fill_rect(
847 uncurses::layout::Rect::new(0, 0, w, cursor.y),
848 &CzCell::BLANK,
849 );
850 }
851 (cursor.y < h).then(|| {
852 let head_w = (cursor.x.min(w).saturating_add(1)).min(w);
853 uncurses::layout::Rect::new(0, cursor.y, head_w, 1)
854 })
855 }
856 ClearType::CurrentLine => {
857 (cursor.y < h).then(|| uncurses::layout::Rect::new(0, cursor.y, w, 1))
858 }
859 ClearType::UntilNewLine => (cursor.y < h && cursor.x < w)
860 .then(|| uncurses::layout::Rect::new(cursor.x, cursor.y, w - cursor.x, 1)),
861 };
862 if let Some(region) = region {
863 self.screen.fill_rect(region, &CzCell::BLANK);
864 }
865 self.screen.invalidate_tracked_cursor();
866 // Push the staged blanks to the wire so the clear takes effect
867 // before this call returns, matching the immediate-clear contract.
868 self.screen.render()
869 }
870
871 /// Return the current terminal size in cells.
872 ///
873 /// This queries the live window size through the screen. If that query fails
874 /// or returns zero dimensions, it falls back to the current screen buffer
875 /// size. Size changes are recorded so the next draw can invalidate and
876 /// repaint.
877 ///
878 /// ## Returns
879 ///
880 /// The full terminal width and height in cells.
881 ///
882 /// ## Errors
883 ///
884 /// This implementation falls back on query failure and currently returns
885 /// `Ok` with the best available size.
886 fn size(&self) -> io::Result<RtSize> {
887 // The full terminal size: the widget library needs it to anchor inline
888 // viewports and to detect resizes. Fall back to the screen's
889 // buffer size if the query fails (e.g. output is not a tty).
890 let size = self.screen.size();
891 let (width, height) = self
892 .screen
893 .get_window_size()
894 .ok()
895 .map(|s| (s.col, s.row))
896 .filter(|&(c, r)| c != 0 && r != 0)
897 .unwrap_or((size.width, size.height));
898 self.note_size((width, height));
899 Ok(RtSize { width, height })
900 }
901
902 /// Return the current terminal size in cells and pixels.
903 ///
904 /// This uses the screen's live window-size query when available. Cell
905 /// dimensions fall back to the buffer size on failure or zero reports; pixel
906 /// dimensions fall back to zero when unavailable. Size changes are recorded
907 /// so the next draw can invalidate and repaint.
908 ///
909 /// ## Returns
910 ///
911 /// A [`WindowSize`] with `columns_rows` populated from the best available
912 /// cell size and `pixels` populated from the query when reported.
913 ///
914 /// ## Errors
915 ///
916 /// This implementation falls back on query failure and currently returns
917 /// `Ok` with the best available size.
918 fn window_size(&mut self) -> io::Result<WindowSize> {
919 // One query reports both cell and pixel dimensions; fall back to
920 // the screen's buffer size for cells if it fails.
921 let size = self.screen.size();
922 let ws = self.screen.get_window_size().ok();
923 let (width, height) = ws
924 .as_ref()
925 .map(|w| (w.col, w.row))
926 .filter(|&(c, r)| c != 0 && r != 0)
927 .unwrap_or((size.width, size.height));
928 self.note_size((width, height));
929 Ok(WindowSize {
930 columns_rows: RtSize { width, height },
931 pixels: RtSize {
932 width: ws.as_ref().map(|w| w.xpixel).unwrap_or(0),
933 height: ws.as_ref().map(|w| w.ypixel).unwrap_or(0),
934 },
935 })
936 }
937
938 /// Diff the staged buffer and write the frame to the output handle.
939 ///
940 /// [`Backend::draw`] only stages cells into the buffer; this is where the
941 /// renderer computes the minimal diff against the tracked terminal and
942 /// flushes the resulting bytes through the wrapped screen.
943 ///
944 /// ## Errors
945 ///
946 /// Returns renderer or output errors from the wrapped screen.
947 fn flush(&mut self) -> io::Result<()> {
948 self.screen.render()
949 }
950
951 /// Append blank lines to the underlying output.
952 ///
953 /// The backend writes `n` newline-terminated blank lines through the screen
954 /// and flushes immediately.
955 ///
956 /// ## Parameters
957 ///
958 /// * `n` - number of lines to append.
959 ///
960 /// ## Errors
961 ///
962 /// Returns output errors from writing or flushing the lines.
963 fn append_lines(&mut self, n: u16) -> io::Result<()> {
964 for _ in 0..n {
965 let _ = writeln!(self.screen);
966 }
967 Write::flush(&mut self.screen)
968 }
969
970 /// Handle a request to scroll a region upward.
971 ///
972 /// This backend does not use terminal scrolling for region updates; drawing
973 /// and buffer diffing repaint the resulting cells instead. The method is a
974 /// no-op that satisfies the backend trait.
975 ///
976 /// ## Parameters
977 ///
978 /// * `_region` - ignored requested row range.
979 /// * `_amount` - ignored scroll amount.
980 ///
981 /// ## Errors
982 ///
983 /// This implementation is infallible and returns `Ok(())`.
984 fn scroll_region_up(&mut self, _region: std::ops::Range<u16>, _amount: u16) -> io::Result<()> {
985 Ok(())
986 }
987
988 /// Handle a request to scroll a region downward.
989 ///
990 /// This backend does not use terminal scrolling for region updates; drawing
991 /// and buffer diffing repaint the resulting cells instead. The method is a
992 /// no-op that satisfies the backend trait.
993 ///
994 /// ## Parameters
995 ///
996 /// * `_region` - ignored requested row range.
997 /// * `_amount` - ignored scroll amount.
998 ///
999 /// ## Errors
1000 ///
1001 /// This implementation is infallible and returns `Ok(())`.
1002 fn scroll_region_down(
1003 &mut self,
1004 _region: std::ops::Range<u16>,
1005 _amount: u16,
1006 ) -> io::Result<()> {
1007 Ok(())
1008 }
1009}
1010
1011#[cfg(test)]
1012mod tests {
1013 use super::cursor_position_report;
1014 use uncurses::event::{Event, Key, KeyCode, KeyModifiers};
1015 use uncurses::layout::Position;
1016
1017 #[test]
1018 fn report_from_plain_cursor_position() {
1019 let ev = Event::CursorPosition(Position::new(4, 9));
1020 assert_eq!(cursor_position_report(&ev), Some(Position::new(4, 9)));
1021 }
1022
1023 #[test]
1024 fn report_unwraps_multi_for_row1_f3_ambiguity() {
1025 // At terminal row 1 the CPR wire form collides with modified-F3, so
1026 // the decoder emits both inside a Multi; the report is still found.
1027 let ev = Event::Multi(vec![
1028 Event::KeyPress(Key::new(KeyCode::F(3), KeyModifiers::empty())),
1029 Event::CursorPosition(Position::new(2, 0)),
1030 ]);
1031 assert_eq!(cursor_position_report(&ev), Some(Position::new(2, 0)));
1032 }
1033
1034 #[test]
1035 fn report_ignores_unrelated_events() {
1036 let ev = Event::KeyPress(Key::new(KeyCode::Char('x'), KeyModifiers::empty()));
1037 assert_eq!(cursor_position_report(&ev), None);
1038 let multi = Event::Multi(vec![Event::FocusIn, Event::FocusOut]);
1039 assert_eq!(cursor_position_report(&multi), None);
1040 }
1041}