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}