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::program::{Program, ProgramOptions};
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 [`Program`].
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`](uncurses::screen::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. The synchronous reads keep capability tracking alive on
117/// their own; only the stream needs
118/// [`observe_event`](Self::observe_event) called by hand.
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    program: Program<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 [`Program`] 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 [`Program::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(Program::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 [`Program`], 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(Program::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 [`Program`].
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 [`Program::init`]. Initialize the screen through the
252    /// backend or manually before starting an interactive session.
253    pub fn new(program: Program<I, O>) -> Self {
254        Self {
255            program,
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.program.screen().size();
298            let h = height.min(size.height);
299            self.program.screen_mut().resize((size.width, h));
300        }
301        self.viewport = viewport;
302    }
303
304    /// Borrow the wrapped [`Program`] 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 [`program_mut`](Self::program_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 program(&self) -> &Program<I, O> {
318        &self.program
319    }
320
321    /// Mutably borrow the wrapped [`Program`] 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 program_mut(&mut self) -> &mut Program<I, O> {
342        &mut self.program
343    }
344
345    /// Poll the wrapped screen's input source.
346    ///
347    /// This delegates to [`Program::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(&self, timeout: Option<Duration>) -> io::Result<bool> {
374        self.program.poll_event(timeout)
375    }
376
377    /// Try to read the next queued event without blocking.
378    ///
379    /// This delegates to the wrapped [`Program`], which tracks capabilities as
380    /// the event passes through. Do not also pass the event to
381    /// [`observe_event`](Self::observe_event); it would count twice.
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) -> io::Result<Option<Event>> {
396        self.program.try_read_event()
397    }
398
399    /// Block until the next event is available.
400    ///
401    /// This delegates to the wrapped [`Program`], which tracks capabilities as
402    /// the event passes through. Do not also pass the event to
403    /// [`observe_event`](Self::observe_event); it would count twice.
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.program.read_event()
424    }
425
426    /// Feed an event back through the wrapped [`Program`] for capability
427    /// tracking.
428    ///
429    /// Only the async [`event_stream`](Self::event_stream) needs this: it
430    /// bypasses the backend, so nothing has observed what it yields. The
431    /// synchronous reads ([`read_event`](Self::read_event),
432    /// [`try_read_event`](Self::try_read_event)) already observe on your
433    /// behalf, and observing one of their events again would count it twice.
434    ///
435    /// ## Errors
436    ///
437    /// Returns the errors [`Program::observe_event`] reports.
438    pub fn observe_event(&mut self, event: &Event) -> io::Result<()> {
439        self.program.observe_event(event)
440    }
441
442    /// Build an async [`EventStream`](uncurses::event::EventStream) over the
443    /// wrapped screen's input.
444    ///
445    /// The stream shares the screen's decoder, so it does not race the sync read
446    /// methods on the same file descriptor. Unlike those, it yields events
447    /// without observing them; pair it with
448    /// [`observe_event`](Self::observe_event) in your `select!` loop to keep
449    /// capability tracking alive.
450    ///
451    /// ## Returns
452    ///
453    /// An owned `EventStream` you can hold alongside `&mut self` (it shares the
454    /// source by handle rather than borrowing the backend).
455    #[cfg(feature = "async")]
456    pub fn event_stream(&self) -> uncurses::event::EventStream<I>
457    where
458        I: 'static,
459    {
460        self.program.event_stream()
461    }
462}
463
464impl<I, O> UncursesBackend<I, O>
465where
466    I: Input + Copy,
467    O: Output,
468{
469    /// Begin an interactive session with default [`ProgramOptions`].
470    ///
471    /// This delegates to [`Program::init`]: the program enters raw mode and
472    /// applies the always-on defaults. It sends no capability query, and does
473    /// not enter the alternate screen or hide the cursor by itself; the
474    /// crate-level setup helpers perform those additional steps.
475    ///
476    /// ## Returns
477    ///
478    /// `Ok(())` after raw mode and screen initialization have been staged.
479    ///
480    /// ## Errors
481    ///
482    /// Returns errors from raw-mode setup, autoresizing, or bracketed paste
483    /// setup.
484    ///
485    /// ## Panics
486    ///
487    /// Does not intentionally panic.
488    ///
489    /// ## Usage note
490    ///
491    /// Pair successful manual initialization with [`restore`](Self::restore).
492    pub fn init(&mut self) -> io::Result<()> {
493        self.program.init()
494    }
495
496    /// Begin an interactive session with explicit [`ProgramOptions`].
497    ///
498    /// This delegates to [`Program::init_with`], allowing the caller to choose
499    /// bracketed paste and mouse tracking. It sends no capability queries, and
500    /// does not enter the alternate screen or hide the cursor by itself.
501    ///
502    /// ## Parameters
503    ///
504    /// * `options` - screen defaults to apply during initialization.
505    ///
506    /// ## Returns
507    ///
508    /// `Ok(())` after raw mode and screen initialization have been staged.
509    ///
510    /// ## Errors
511    ///
512    /// Returns errors from raw-mode setup, autoresizing, or always-on mode
513    /// setup.
514    ///
515    /// ## Panics
516    ///
517    /// Does not intentionally panic.
518    ///
519    /// ## Usage note
520    ///
521    /// Pair successful manual initialization with [`restore`](Self::restore).
522    pub fn init_with(&mut self, options: ProgramOptions) -> io::Result<()> {
523        self.program.init_with(options)
524    }
525
526    /// Restore terminal state after a backend-managed session.
527    ///
528    /// This delegates to [`Program::pause`]. It tears down staged modes, resets
529    /// buffer-controlled state such as alternate screen and cursor visibility,
530    /// flushes pending output, and restores the terminal mode while keeping the
531    /// screen available for future use.
532    ///
533    /// ## Returns
534    ///
535    /// `Ok(())` after teardown and terminal-mode restoration complete.
536    ///
537    /// ## Errors
538    ///
539    /// Returns errors from mode teardown, flushing, or terminal restoration.
540    ///
541    /// ## Panics
542    ///
543    /// Does not intentionally panic.
544    ///
545    /// ## Usage note
546    ///
547    /// Treat this as the single teardown entry point for backend-managed setup.
548    pub fn restore(&mut self) -> io::Result<()> {
549        self.program.pause()
550    }
551}
552
553impl<I, O> Write for UncursesBackend<I, O>
554where
555    I: Input,
556    O: Write,
557{
558    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
559        self.program.screen_mut().write(buf)
560    }
561
562    fn flush(&mut self) -> io::Result<()> {
563        Write::flush(self.program.screen_mut())
564    }
565}
566
567impl<I, O> Backend for UncursesBackend<I, O>
568where
569    I: Input + Copy,
570    O: Output,
571{
572    type Error = io::Error;
573
574    /// Stage a frame's cells into the wrapped screen buffer.
575    ///
576    /// The iterator supplies absolute buffer coordinates and concrete cells.
577    /// Each cell is converted to an uncurses cell and written to the screen.
578    /// This method only stages into the buffer; the buffer diff is computed
579    /// and written when the surrounding terminal calls [`Backend::flush`].
580    ///
581    /// ## Parameters
582    ///
583    /// * `content` - visible frame cells as `(x, y, cell)` triples.
584    ///
585    /// ## Errors
586    ///
587    /// This implementation performs no I/O and returns `Ok(())`; renderer and
588    /// output errors surface from [`Backend::flush`].
589    ///
590    /// ## Usage note
591    ///
592    /// Inline viewports translate `y` by the stored inline origin. A detected
593    /// terminal-size change invalidates the screen so this frame repaints in
594    /// full.
595    fn draw<'a, J>(&mut self, content: J) -> io::Result<()>
596    where
597        J: Iterator<Item = (u16, u16, &'a RtCell)>,
598    {
599        // Keep the screen buffer in step with what the widget library draws into.
600        // For an inline viewport the buffer is only the inline height and
601        // the widget library's absolute rows are translated down by the viewport top;
602        // otherwise it tracks the full terminal size.
603        let size = self.program.screen().size();
604        let (full_w, full_h) = self
605            .program
606            .get_window_size()
607            .ok()
608            .map(|s| (s.col, s.row))
609            .filter(|&(c, r)| c != 0 && r != 0)
610            .unwrap_or((size.width, size.height));
611        let (w, h, top) = match self.viewport {
612            Viewport::Inline(height) => {
613                let h = height.min(full_h);
614                let top = self.inline_origin.min(full_h.saturating_sub(h));
615                (full_w, h, top)
616            }
617            _ => (full_w, full_h, 0),
618        };
619        // Repaint in full if the terminal size changed since the last
620        // observation (covers cases where the buffer dimensions stay the
621        // same, e.g. an inline viewport on a vertical-only resize).
622        self.note_size((full_w, full_h));
623        if self.size_dirty.take() {
624            self.program.screen_mut().invalidate();
625        }
626        if (w, h) != (size.width, size.height) {
627            self.program.screen_mut().resize((w, h));
628        }
629        for (x, y, rc) in content {
630            let cell = cell_from_ratatui(rc);
631            self.program
632                .screen_mut()
633                .set_cell((x, y.saturating_sub(top)), &cell);
634        }
635        Ok(())
636    }
637
638    /// Hide the terminal cursor immediately.
639    ///
640    /// Delegates to [`Program::hide_cursor`], which stages cursor visibility on
641    /// the buffer and flushes before returning.
642    ///
643    /// ## Errors
644    ///
645    /// Returns output errors from flushing the visibility change.
646    fn hide_cursor(&mut self) -> io::Result<()> {
647        self.program.hide_cursor()
648    }
649
650    /// Show the terminal cursor immediately.
651    ///
652    /// Delegates to [`Program::show_cursor`], which stages cursor visibility on
653    /// the buffer and flushes before returning.
654    ///
655    /// ## Errors
656    ///
657    /// Returns output errors from flushing the visibility change.
658    fn show_cursor(&mut self) -> io::Result<()> {
659        self.program.show_cursor()
660    }
661
662    /// Query the terminal for its current cursor position.
663    ///
664    /// This sends a cursor-position request, then polls input until a
665    /// [`Event::CursorPosition`] report arrives or the short setup timeout
666    /// expires. Non-report events read while waiting are unread back into the
667    /// screen in their original order so the application can still consume them.
668    /// If no report arrives, the backend returns the origin.
669    ///
670    /// ## Returns
671    ///
672    /// The zero-based cursor position reported by the terminal, or `(0, 0)` on
673    /// timeout. Inline viewports also seed their initial origin from this row.
674    ///
675    /// ## Errors
676    ///
677    /// Returns errors from writing the cursor-position request or polling the
678    /// input source.
679    ///
680    /// ## Usage note
681    ///
682    /// The reply parser also accepts the multi-event ambiguity that occurs when
683    /// the row-1 CPR wire form collides with a modified function key sequence.
684    fn get_cursor_position(&mut self) -> io::Result<RtPosition> {
685        // Query the terminal for its cursor position (CPR): write the
686        // request, then read events until the report arrives or the timeout
687        // elapses. The reply is absolute, zero-based, and matches the widget library's
688        // coordinate space (the same space `set_cursor_position` writes
689        // absolute moves into), so it needs no translation. Fall back to the
690        // origin if the terminal does not answer.
691        self.program.request_cursor_position()?;
692        let deadline = Instant::now() + CURSOR_QUERY_TIMEOUT;
693        // Events read while waiting for the report are not ours to consume;
694        // stash them and put them back (in original order) so the app's loop
695        // still sees them.
696        let mut stash: Vec<Event> = Vec::new();
697        let found = loop {
698            let Some(remaining) = deadline.checked_duration_since(Instant::now()) else {
699                break None;
700            };
701            if !self.program.poll_event(Some(remaining))? {
702                break None;
703            }
704            match self.program.try_read_event()? {
705                Some(ev) => match cursor_position_report(&ev) {
706                    Some(pos) => break Some(pos),
707                    None => stash.push(ev),
708                },
709                None => continue,
710            }
711        };
712        for ev in stash.into_iter().rev() {
713            self.program.unread_event(ev);
714        }
715        let pos = found.unwrap_or(Position::new(0, 0));
716        // the widget library calls this to anchor an inline viewport at the cursor row,
717        // then draws content at absolute rows starting there. Seed the
718        // inline origin so the very first `draw` translates those absolute
719        // rows into the inline buffer; before this the origin defaulted to 0
720        // and a cursor below the top clipped the first frame. The exact top
721        // is still re-derived by `clear_region` on later resizes.
722        if matches!(self.viewport, Viewport::Inline(_)) {
723            self.inline_origin = pos.y;
724            self.last_cursor_row = pos.y;
725        }
726        Ok(RtPosition { x: pos.x, y: pos.y })
727    }
728
729    /// Move the terminal cursor to an absolute position immediately.
730    ///
731    /// The backend writes an absolute CUP escape directly, records the row for
732    /// inline-viewport bookkeeping, updates the renderer's tracked cursor
733    /// position in buffer-relative coordinates, and flushes.
734    ///
735    /// ## Parameters
736    ///
737    /// * `position` - zero-based absolute cursor position requested by the
738    ///   surrounding terminal.
739    ///
740    /// ## Errors
741    ///
742    /// Returns output errors from writing or flushing the cursor movement.
743    ///
744    /// ## Usage note
745    ///
746    /// Direct absolute movement keeps the renderer and inline viewport aligned;
747    /// it intentionally bypasses the renderer's cost-optimized relative moves.
748    fn set_cursor_position<P: Into<RtPosition>>(&mut self, position: P) -> io::Result<()> {
749        let p = position.into();
750        // Remember the requested row: when the widget library clears an inline
751        // viewport it places the cursor at the viewport top first, letting
752        // `clear_region` recover the (possibly shifted) origin on resize.
753        self.last_cursor_row = p.y;
754        // Emit an absolute CUP directly rather than going through the
755        // renderer's cost-optimized (possibly relative) move: ratatui calls
756        // this to place its own cursor, so the move must be unconditional
757        // and absolute.
758        uncurses::ansi::cursor::write_cup(self.program.screen_mut(), p.y, p.x)?;
759        // Keep the renderer's cursor bookkeeping in step with the move we
760        // just made, translated into the (inline) buffer. In relative-cursor
761        // mode merely invalidating would lose the absolute row — the next
762        // frame's vertical moves would drift the viewport — so assert the
763        // exact buffer-relative position instead. For a non-inline viewport
764        // `inline_origin` is 0, so this is the absolute position unchanged.
765        let top = match self.viewport {
766            Viewport::Inline(_) => self.inline_origin,
767            _ => 0,
768        };
769        self.program
770            .screen_mut()
771            .set_tracked_cursor((p.x, p.y.saturating_sub(top)));
772        Write::flush(self.program.screen_mut())
773    }
774
775    /// Clear the entire backend surface immediately.
776    ///
777    /// This delegates to [`Backend::clear_region`] with [`ClearType::All`].
778    ///
779    /// ## Errors
780    ///
781    /// Returns output errors from rendering and flushing the staged blank cells.
782    fn clear(&mut self) -> io::Result<()> {
783        self.clear_region(ClearType::All)
784    }
785
786    /// Clear part of the backend surface immediately.
787    ///
788    /// The implementation blanks only the cells covered by `clear_type` in the
789    /// screen's staging buffer, invalidates tracked cursor state, renders the
790    /// diff, and flushes before returning. For inline viewports,
791    /// [`ClearType::AfterCursor`] is also the resize/viewport-reanchor path: the
792    /// last absolute cursor row becomes the new inline origin and the full
793    /// inline buffer is blanked.
794    ///
795    /// ## Parameters
796    ///
797    /// * `clear_type` - the clear region requested by the surrounding terminal.
798    ///
799    /// ## Errors
800    ///
801    /// Returns output errors from rendering or flushing the clear operation.
802    ///
803    /// ## Usage note
804    ///
805    /// Clearing is immediate by backend contract; unlike [`Backend::draw`], this
806    /// method does not wait for a later flush call to make output visible.
807    fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
808        let size = self.program.screen().size();
809        let w = size.width;
810        let h = size.height;
811        let cursor = self.program.screen().tracked_cursor().unwrap_or_default();
812        if w == 0 || h == 0 {
813            return Ok(());
814        }
815        let region = match clear_type {
816            ClearType::All => Some(uncurses::layout::Rect::new(0, 0, w, h)),
817            ClearType::AfterCursor if matches!(self.viewport, Viewport::Inline(_)) => {
818                // Inline-viewport resize/clear path. ratatui homes the
819                // cursor to the viewport's (recomputed) top row, then erases
820                // to the end of the screen — for our inline buffer that is
821                // the whole thing. Adopt the fresh origin (the app cursor
822                // reported by `get_cursor_position` may sit anywhere in the
823                // viewport, so this is the authoritative top), and blank the
824                // entire staging buffer so the upcoming full repaint starts
825                // clean: the staging buffer preserves overlapping cells
826                // across a grow, which a diff-style painter never overwrites,
827                // and would otherwise duplicate the previous frame's right
828                // edge.
829                self.inline_origin = self.last_cursor_row;
830                Some(uncurses::layout::Rect::new(0, 0, w, h))
831            }
832            ClearType::AfterCursor => {
833                if cursor.y < h {
834                    let tail_x = cursor.x.min(w);
835                    self.program.screen_mut().fill_rect(
836                        uncurses::layout::Rect::new(tail_x, cursor.y, w - tail_x, 1),
837                        &CzCell::BLANK,
838                    );
839                }
840                (cursor.y + 1 < h)
841                    .then(|| uncurses::layout::Rect::new(0, cursor.y + 1, w, h - cursor.y - 1))
842            }
843            ClearType::BeforeCursor => {
844                if cursor.y > 0 {
845                    self.program.screen_mut().fill_rect(
846                        uncurses::layout::Rect::new(0, 0, w, cursor.y),
847                        &CzCell::BLANK,
848                    );
849                }
850                (cursor.y < h).then(|| {
851                    let head_w = (cursor.x.min(w).saturating_add(1)).min(w);
852                    uncurses::layout::Rect::new(0, cursor.y, head_w, 1)
853                })
854            }
855            ClearType::CurrentLine => {
856                (cursor.y < h).then(|| uncurses::layout::Rect::new(0, cursor.y, w, 1))
857            }
858            ClearType::UntilNewLine => (cursor.y < h && cursor.x < w)
859                .then(|| uncurses::layout::Rect::new(cursor.x, cursor.y, w - cursor.x, 1)),
860        };
861        if let Some(region) = region {
862            self.program.screen_mut().fill_rect(region, &CzCell::BLANK);
863        }
864        self.program.screen_mut().invalidate_tracked_cursor();
865        // Push the staged blanks to the wire so the clear takes effect
866        // before this call returns, matching the immediate-clear contract.
867        self.program.screen_mut().render()
868    }
869
870    /// Return the current terminal size in cells.
871    ///
872    /// This queries the live window size through the screen. If that query fails
873    /// or returns zero dimensions, it falls back to the current screen buffer
874    /// size. Size changes are recorded so the next draw can invalidate and
875    /// repaint.
876    ///
877    /// ## Returns
878    ///
879    /// The full terminal width and height in cells.
880    ///
881    /// ## Errors
882    ///
883    /// This implementation falls back on query failure and currently returns
884    /// `Ok` with the best available size.
885    fn size(&self) -> io::Result<RtSize> {
886        // The full terminal size: the widget library needs it to anchor inline
887        // viewports and to detect resizes. Fall back to the screen's
888        // buffer size if the query fails (e.g. output is not a tty).
889        let size = self.program.screen().size();
890        let (width, height) = self
891            .program
892            .get_window_size()
893            .ok()
894            .map(|s| (s.col, s.row))
895            .filter(|&(c, r)| c != 0 && r != 0)
896            .unwrap_or((size.width, size.height));
897        self.note_size((width, height));
898        Ok(RtSize { width, height })
899    }
900
901    /// Return the current terminal size in cells and pixels.
902    ///
903    /// This uses the screen's live window-size query when available. Cell
904    /// dimensions fall back to the buffer size on failure or zero reports; pixel
905    /// dimensions fall back to zero when unavailable. Size changes are recorded
906    /// so the next draw can invalidate and repaint.
907    ///
908    /// ## Returns
909    ///
910    /// A [`WindowSize`] with `columns_rows` populated from the best available
911    /// cell size and `pixels` populated from the query when reported.
912    ///
913    /// ## Errors
914    ///
915    /// This implementation falls back on query failure and currently returns
916    /// `Ok` with the best available size.
917    fn window_size(&mut self) -> io::Result<WindowSize> {
918        // One query reports both cell and pixel dimensions; fall back to
919        // the screen's buffer size for cells if it fails.
920        let size = self.program.screen().size();
921        let ws = self.program.get_window_size().ok();
922        let (width, height) = ws
923            .as_ref()
924            .map(|w| (w.col, w.row))
925            .filter(|&(c, r)| c != 0 && r != 0)
926            .unwrap_or((size.width, size.height));
927        self.note_size((width, height));
928        Ok(WindowSize {
929            columns_rows: RtSize { width, height },
930            pixels: RtSize {
931                width: ws.as_ref().map(|w| w.xpixel).unwrap_or(0),
932                height: ws.as_ref().map(|w| w.ypixel).unwrap_or(0),
933            },
934        })
935    }
936
937    /// Diff the staged buffer and write the frame to the output handle.
938    ///
939    /// [`Backend::draw`] only stages cells into the buffer; this is where the
940    /// renderer computes the minimal diff against the tracked terminal and
941    /// flushes the resulting bytes through the wrapped screen.
942    ///
943    /// ## Errors
944    ///
945    /// Returns renderer or output errors from the wrapped screen.
946    fn flush(&mut self) -> io::Result<()> {
947        self.program.screen_mut().render()
948    }
949
950    /// Append blank lines to the underlying output.
951    ///
952    /// The backend writes `n` newline-terminated blank lines through the screen
953    /// and flushes immediately.
954    ///
955    /// ## Parameters
956    ///
957    /// * `n` - number of lines to append.
958    ///
959    /// ## Errors
960    ///
961    /// Returns output errors from writing or flushing the lines.
962    fn append_lines(&mut self, n: u16) -> io::Result<()> {
963        for _ in 0..n {
964            let _ = writeln!(self.program.screen_mut());
965        }
966        Write::flush(self.program.screen_mut())
967    }
968
969    /// Handle a request to scroll a region upward.
970    ///
971    /// This backend does not use terminal scrolling for region updates; drawing
972    /// and buffer diffing repaint the resulting cells instead. The method is a
973    /// no-op that satisfies the backend trait.
974    ///
975    /// ## Parameters
976    ///
977    /// * `_region` - ignored requested row range.
978    /// * `_amount` - ignored scroll amount.
979    ///
980    /// ## Errors
981    ///
982    /// This implementation is infallible and returns `Ok(())`.
983    fn scroll_region_up(&mut self, _region: std::ops::Range<u16>, _amount: u16) -> io::Result<()> {
984        Ok(())
985    }
986
987    /// Handle a request to scroll a region downward.
988    ///
989    /// This backend does not use terminal scrolling for region updates; drawing
990    /// and buffer diffing repaint the resulting cells instead. The method is a
991    /// no-op that satisfies the backend trait.
992    ///
993    /// ## Parameters
994    ///
995    /// * `_region` - ignored requested row range.
996    /// * `_amount` - ignored scroll amount.
997    ///
998    /// ## Errors
999    ///
1000    /// This implementation is infallible and returns `Ok(())`.
1001    fn scroll_region_down(
1002        &mut self,
1003        _region: std::ops::Range<u16>,
1004        _amount: u16,
1005    ) -> io::Result<()> {
1006        Ok(())
1007    }
1008}
1009
1010#[cfg(test)]
1011mod tests {
1012    use super::cursor_position_report;
1013    use uncurses::event::{Event, Key, KeyCode, KeyModifiers};
1014    use uncurses::layout::Position;
1015
1016    #[test]
1017    fn report_from_plain_cursor_position() {
1018        let ev = Event::CursorPosition(Position::new(4, 9));
1019        assert_eq!(cursor_position_report(&ev), Some(Position::new(4, 9)));
1020    }
1021
1022    #[test]
1023    fn report_unwraps_multi_for_row1_f3_ambiguity() {
1024        // At terminal row 1 the CPR wire form collides with modified-F3, so
1025        // the decoder emits both inside a Multi; the report is still found.
1026        let ev = Event::Multi(vec![
1027            Event::KeyPress(Key::new(KeyCode::F(3), KeyModifiers::empty())),
1028            Event::CursorPosition(Position::new(2, 0)),
1029        ]);
1030        assert_eq!(cursor_position_report(&ev), Some(Position::new(2, 0)));
1031    }
1032
1033    #[test]
1034    fn report_ignores_unrelated_events() {
1035        let ev = Event::KeyPress(Key::new(KeyCode::Char('x'), KeyModifiers::empty()));
1036        assert_eq!(cursor_position_report(&ev), None);
1037        let multi = Event::Multi(vec![Event::FocusIn, Event::FocusOut]);
1038        assert_eq!(cursor_position_report(&multi), None);
1039    }
1040}