Skip to main content

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}