use std::io::{self, Write};
use crossterm::execute;
use crossterm::terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate};
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::Rect;
use ratatui_core::terminal::Terminal;
use ratatui_crossterm::CrosstermBackend;
use crate::widget::PointerShape;
const POINTER_SHAPES_VAR: &str = "QUVYTA_POINTER_SHAPES";
pub(crate) struct Screen<W: Write> {
terminal: Terminal<CrosstermBackend<W>>,
shown: Option<Buffer>,
pointer_shapes: bool,
shape: PointerShape,
}
impl<W: Write> Screen<W> {
pub(crate) fn new(terminal: Terminal<CrosstermBackend<W>>) -> Self {
Self { terminal, shown: None, pointer_shapes: false, shape: PointerShape::Default }
}
pub(crate) fn pointer_shapes(mut self, on: bool) -> Self {
self.pointer_shapes = on;
self
}
pub(crate) fn present(&mut self, render: impl FnOnce(&mut Buffer) -> PointerShape) -> io::Result<bool> {
self.terminal.autoresize()?;
let mut frame = self.terminal.get_frame();
let shape = render(frame.buffer_mut());
let reshape = Some(shape).filter(|shape| self.pointer_shapes && *shape != self.shape);
let painted = self.terminal.current_buffer_mut();
if self.shown.as_ref() == Some(&*painted) {
let Some(shape) = reshape else {
return Ok(false);
};
self.write_shape(shape)?;
self.terminal.backend_mut().flush()?;
return Ok(true);
}
let painted = painted.clone();
execute!(self.terminal.backend_mut(), BeginSynchronizedUpdate)?;
self.terminal.apply_buffer_with_cursor(None)?;
if let Some(shape) = reshape {
self.write_shape(shape)?;
}
execute!(self.terminal.backend_mut(), EndSynchronizedUpdate)?;
self.shown = Some(painted);
Ok(true)
}
pub(crate) fn reset_pointer_shape(&mut self) -> io::Result<()> {
if self.pointer_shapes && self.shape != PointerShape::Default {
self.write_shape(PointerShape::Default)?;
self.terminal.backend_mut().flush()?;
}
Ok(())
}
fn write_shape(&mut self, shape: PointerShape) -> io::Result<()> {
write!(self.terminal.backend_mut(), "\x1b]22;{}\x1b\\", shape.name())?;
self.shape = shape;
Ok(())
}
pub(crate) fn size(&self) -> io::Result<Rect> {
Ok(self.terminal.size()?.into())
}
pub(crate) fn redraw_all(&mut self, area: Rect) -> io::Result<()> {
self.terminal.resize(area)?;
self.shown = None;
Ok(())
}
pub(crate) fn into_terminal(self) -> Terminal<CrosstermBackend<W>> {
self.terminal
}
}
pub(crate) fn pointer_shapes_supported(env: impl Fn(&str) -> Option<String>) -> bool {
let lower = |name: &str| env(name).map(|value| value.trim().to_lowercase());
match lower(POINTER_SHAPES_VAR).as_deref() {
Some("on") => return true,
Some("off") => return false,
_ => {}
}
if env("TMUX").is_some() || env("STY").is_some() {
return false;
}
let term = lower("TERM").unwrap_or_default();
let program = lower("TERM_PROGRAM").unwrap_or_default();
term == "xterm-kitty" || term == "foot" || term == "foot-extra" || program == "wezterm" || program == "kitty"
}
#[cfg(test)]
mod tests {
use ratatui_core::terminal::{TerminalOptions, Viewport};
use super::*;
#[derive(Clone, Default)]
struct Wire(std::rc::Rc<std::cell::RefCell<Vec<u8>>>);
impl Write for Wire {
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
self.0.borrow_mut().extend_from_slice(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
fn screen() -> (Screen<Wire>, Wire) {
let wire = Wire::default();
let options = TerminalOptions { viewport: Viewport::Fixed(Rect::new(0, 0, 12, 2)) };
let terminal = Terminal::with_options(CrosstermBackend::new(wire.clone()), options).expect("a terminal");
(Screen::new(terminal), wire)
}
fn paint(text: &'static str) -> impl FnOnce(&mut Buffer) -> PointerShape {
pointing(text, PointerShape::Default)
}
fn pointing(text: &'static str, shape: PointerShape) -> impl FnOnce(&mut Buffer) -> PointerShape {
move |buffer: &mut Buffer| {
buffer.reset();
buffer.set_string(0, 0, text, ratatui_core::style::Style::default());
shape
}
}
fn bytes(screen: &mut Screen<Wire>, wire: &Wire, render: impl FnOnce(&mut Buffer) -> PointerShape) -> usize {
written(screen, wire, render).len()
}
fn written(screen: &mut Screen<Wire>, wire: &Wire, render: impl FnOnce(&mut Buffer) -> PointerShape) -> String {
wire.0.borrow_mut().clear();
screen.present(render).expect("a frame");
String::from_utf8_lossy(&wire.0.borrow()).into_owned()
}
const OSC_22: &str = "\x1b]22;";
#[test]
fn the_pointer_shape_is_written_with_the_frame_only_when_it_changes() {
let (screen, wire) = screen();
let mut screen = screen.pointer_shapes(true);
let first = written(&mut screen, &wire, paint("notes"));
assert!(!first.contains(OSC_22), "the terminal starts with its usual pointer: {first:?}");
let arrow = written(&mut screen, &wire, pointing("notes", PointerShape::EwResize));
assert_eq!(arrow, "\x1b]22;ew-resize\x1b\\", "no cell changed, so the shape is all that is written");
assert_eq!(
bytes(&mut screen, &wire, pointing("notes", PointerShape::EwResize)),
0,
"the same shape again is not a byte"
);
let cells = written(&mut screen, &wire, pointing("notes!", PointerShape::EwResize));
assert!(!cells.is_empty() && !cells.contains(OSC_22), "a changed cell with the same shape: {cells:?}");
let both = written(&mut screen, &wire, pointing("notes?", PointerShape::NwseResize));
let (begin, end) = (both.find("\x1b[?2026h"), both.find("\x1b[?2026l"));
let at = both.find("\x1b]22;nwse-resize\x1b\\");
assert!(begin < at && at < end, "the shape goes inside the frame's synchronized update: {both:?}");
assert_eq!(
written(&mut screen, &wire, paint("notes?")),
"\x1b]22;default\x1b\\",
"and back to the usual pointer"
);
assert_eq!(bytes(&mut screen, &wire, paint("notes?")), 0);
}
#[test]
fn a_terminal_that_does_not_know_pointer_shapes_is_sent_none() {
let (mut screen, wire) = screen();
bytes(&mut screen, &wire, paint("notes"));
assert_eq!(bytes(&mut screen, &wire, pointing("notes", PointerShape::EwResize)), 0);
let cells = written(&mut screen, &wire, pointing("notes!", PointerShape::NsResize));
assert!(!cells.is_empty() && !cells.contains(OSC_22), "{cells:?}");
screen.reset_pointer_shape().expect("nothing to write");
assert_eq!(wire.0.borrow().len(), cells.len(), "nor is anything written to reset it");
}
#[test]
fn resetting_the_pointer_writes_the_usual_shape_once() {
let (screen, wire) = screen();
let mut screen = screen.pointer_shapes(true);
bytes(&mut screen, &wire, pointing("notes", PointerShape::NeswResize));
wire.0.borrow_mut().clear();
screen.reset_pointer_shape().expect("a reset");
assert_eq!(String::from_utf8_lossy(&wire.0.borrow()), "\x1b]22;default\x1b\\");
let once = wire.0.borrow().len();
screen.reset_pointer_shape().expect("a reset");
assert_eq!(wire.0.borrow().len(), once, "already the usual pointer: nothing more");
}
#[test]
fn pointer_shapes_are_sent_only_to_terminals_known_to_understand_them() {
let with = |vars: &[(&str, &str)]| {
let vars: Vec<(String, String)> = vars.iter().map(|(k, v)| ((*k).to_owned(), (*v).to_owned())).collect();
pointer_shapes_supported(move |name| vars.iter().find(|(k, _)| k == name).map(|(_, v)| v.clone()))
};
assert!(with(&[("TERM", "xterm-kitty")]), "kitty");
assert!(with(&[("TERM", "foot")]) && with(&[("TERM", "foot-extra")]), "foot");
assert!(with(&[("TERM", "xterm-256color"), ("TERM_PROGRAM", "WezTerm")]), "WezTerm");
assert!(!with(&[("TERM", "xterm-256color")]), "an unknown terminal gets nothing");
assert!(!with(&[]), "nor does one that says nothing");
assert!(
!with(&[("TERM", "tmux-256color"), ("TMUX", "/tmp/tmux-1000/default,1,0")]),
"tmux does not pass it on"
);
assert!(!with(&[("TERM", "xterm-kitty"), ("TMUX", "/tmp/tmux-1000/default,1,0")]));
assert!(with(&[("TERM", "xterm-256color"), (POINTER_SHAPES_VAR, "on")]), "the variable turns it on");
assert!(!with(&[("TERM", "xterm-kitty"), (POINTER_SHAPES_VAR, "off")]), "and off");
assert!(
with(&[("TERM", "xterm-kitty"), (POINTER_SHAPES_VAR, "auto")]),
"anything else leaves it to the terminal"
);
}
#[test]
fn a_frame_equal_to_the_one_shown_writes_nothing() {
let (mut screen, wire) = screen();
assert!(bytes(&mut screen, &wire, paint("12:04")) > 0, "the first frame is written");
assert_eq!(bytes(&mut screen, &wire, paint("12:04")), 0, "the same frame again is not a byte");
assert!(bytes(&mut screen, &wire, paint("12:05")) > 0, "a changed cell is written");
assert_eq!(bytes(&mut screen, &wire, paint("12:05")), 0);
}
#[test]
fn after_the_screen_was_lent_the_same_frame_is_written_whole() {
let (mut screen, wire) = screen();
bytes(&mut screen, &wire, paint("notes"));
screen.redraw_all(Rect::new(0, 0, 12, 2)).expect("a resize");
assert!(bytes(&mut screen, &wire, paint("notes")) > 0, "nothing on screen can be trusted after a handoff");
}
}