use std::io::{self, Write};
use crossterm::event::{
DisableFocusChange, DisableMouseCapture, EnableFocusChange, EnableMouseCapture,
KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
};
use crossterm::terminal;
use crossterm::{cursor, execute};
use super::backend::{Backend, BackendState, draw_iter};
use super::sgr::emit_cup;
use super::{Cell, Rect};
pub struct CrosstermBackend<W: Write> {
writer: W,
state: BackendState,
}
impl<W: Write> CrosstermBackend<W> {
pub fn new(writer: W) -> Self {
Self {
writer,
state: BackendState::default(),
}
}
pub fn writer(&self) -> &W {
&self.writer
}
pub fn writer_mut(&mut self) -> &mut W {
&mut self.writer
}
}
impl<W: Write> Write for CrosstermBackend<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.writer.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.writer.flush()
}
}
impl<W: Write> Backend for CrosstermBackend<W> {
fn size(&self) -> io::Result<Rect> {
let (w, h) = terminal::size()?;
Ok(Rect::new(0, 0, w, h))
}
fn clear(&mut self) -> io::Result<()> {
execute!(
self.writer,
terminal::Clear(terminal::ClearType::All),
cursor::MoveTo(0, 0),
)?;
self.writer.write_all(b"\x1b[0m")?;
self.state = BackendState::default();
Ok(())
}
fn hide_cursor(&mut self) -> io::Result<()> {
execute!(self.writer, cursor::Hide)
}
fn show_cursor(&mut self) -> io::Result<()> {
execute!(self.writer, cursor::Show)
}
fn set_cursor_position(&mut self, x: u16, y: u16) -> io::Result<()> {
emit_cup(&mut self.writer, x, y)?;
self.state.cursor = Some((x, y));
Ok(())
}
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
draw_iter(&mut self.writer, &mut self.state, content)
}
fn reset_style_cache(&mut self) {
self.state = BackendState::default();
}
}
pub fn enter_tui_mode<W: Write>(writer: &mut W) -> io::Result<()> {
terminal::enable_raw_mode()?;
execute!(
writer,
terminal::EnterAlternateScreen,
cursor::Hide,
EnableMouseCapture,
EnableFocusChange,
PushKeyboardEnhancementFlags(
KeyboardEnhancementFlags::REPORT_EVENT_TYPES
| KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES,
),
)?;
Ok(())
}
pub fn leave_tui_mode<W: Write>(writer: &mut W) -> io::Result<()> {
execute!(
writer,
PopKeyboardEnhancementFlags,
DisableFocusChange,
DisableMouseCapture,
cursor::Show,
terminal::LeaveAlternateScreen,
)?;
writer.write_all(b"\x1b[0m")?;
writer.flush()?;
terminal::disable_raw_mode()
}
pub fn enter_mouse_capture<W: Write>(writer: &mut W) -> io::Result<()> {
execute!(writer, EnableMouseCapture)
}
pub fn leave_mouse_capture<W: Write>(writer: &mut W) -> io::Result<()> {
execute!(writer, DisableMouseCapture)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::{Color, Style};
struct Sink(Vec<u8>);
impl Write for Sink {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
fn backend() -> CrosstermBackend<Sink> {
CrosstermBackend::new(Sink(Vec::new()))
}
fn bytes(b: &CrosstermBackend<Sink>) -> &[u8] {
&b.writer.0
}
#[test]
fn set_cursor_position_emits_cup() {
let mut b = backend();
b.set_cursor_position(5, 2).unwrap();
assert_eq!(bytes(&b), b"\x1b[3;6H");
}
#[test]
fn draw_basic_cell() {
use crate::render::Buffer;
let prev = Buffer::empty(Rect::new(0, 0, 10, 2));
let mut next = prev.clone();
next.set_string(2, 0, "hi", Style::new().fg(Color::Rgb(255, 0, 0)));
let mut b = backend();
b.draw(next.diff_iter(&prev)).unwrap();
assert_eq!(bytes(&b), b"\x1b[1;3H\x1b[38;2;255;0;0mhi");
}
#[test]
fn draw_persists_state_across_calls() {
use crate::render::Buffer;
let prev = Buffer::empty(Rect::new(0, 0, 10, 2));
let mut n1 = prev.clone();
n1.set_string(0, 0, "A", Style::new().fg(Color::Rgb(255, 0, 0)));
let mut b = backend();
b.draw(n1.diff_iter(&prev)).unwrap();
let snapshot_len = b.writer.0.len();
let mut n2 = prev.clone();
n2.set_string(1, 0, "B", Style::new().fg(Color::Rgb(255, 0, 0)));
b.draw(n2.diff_iter(&prev)).unwrap();
let delta = &b.writer.0[snapshot_len..];
assert_eq!(delta, b"B");
}
#[test]
fn reset_style_cache_drops_history() {
use crate::render::Buffer;
let prev = Buffer::empty(Rect::new(0, 0, 10, 2));
let mut n1 = prev.clone();
n1.set_string(0, 0, "A", Style::new().fg(Color::Rgb(255, 0, 0)));
let mut b = backend();
b.draw(n1.diff_iter(&prev)).unwrap();
b.writer.0.clear();
b.reset_style_cache();
let mut n2 = prev.clone();
n2.set_string(0, 0, "B", Style::new().fg(Color::Rgb(255, 0, 0)));
b.draw(n2.diff_iter(&prev)).unwrap();
assert_eq!(bytes(&b), b"\x1b[1;1H\x1b[38;2;255;0;0mB");
}
#[test]
fn link_cells_emit_osc8_wrappers() {
use crate::render::Buffer;
let prev = Buffer::empty(Rect::new(0, 0, 10, 1));
let mut next = prev.clone();
next.set_string(0, 0, "hi", Style::new());
next.set_link_range(0, 0, 2, Some("https://example.com"));
let mut b = backend();
b.draw(next.diff_iter(&prev)).unwrap();
let out = bytes(&b);
let expected = b"\x1b[1;1H\x1b]8;;https://example.com\x1b\\hi\x1b]8;;\x1b\\";
assert_eq!(out, expected, "got {:?}", std::str::from_utf8(out));
}
#[test]
fn adjacent_same_link_run_emits_one_open_and_one_close() {
use crate::render::Buffer;
let prev = Buffer::empty(Rect::new(0, 0, 10, 1));
let mut next = prev.clone();
next.set_string(0, 0, "abc", Style::new());
next.set_link_range(0, 0, 3, Some("x"));
let mut b = backend();
b.draw(next.diff_iter(&prev)).unwrap();
let out = String::from_utf8_lossy(bytes(&b)).to_string();
assert_eq!(out.matches("\x1b]8;;x\x1b\\").count(), 1);
assert_eq!(out.matches("\x1b]8;;\x1b\\").count(), 1);
}
#[test]
fn link_transition_closes_old_opens_new() {
use crate::render::Buffer;
let prev = Buffer::empty(Rect::new(0, 0, 10, 1));
let mut next = prev.clone();
next.set_string(0, 0, "ab", Style::new());
next.set_link_range(0, 0, 1, Some("first"));
next.set_link_range(1, 0, 1, Some("second"));
let mut b = backend();
b.draw(next.diff_iter(&prev)).unwrap();
let out = String::from_utf8_lossy(bytes(&b)).to_string();
assert!(out.contains("\x1b]8;;first\x1b\\a\x1b]8;;\x1b\\\x1b]8;;second\x1b\\b"));
assert_eq!(out.matches("\x1b]8;;first\x1b\\").count(), 1);
assert_eq!(out.matches("\x1b]8;;second\x1b\\").count(), 1);
assert_eq!(out.matches("\x1b]8;;\x1b\\").count(), 2); }
#[test]
fn draw_without_any_links_emits_no_osc8() {
use crate::render::Buffer;
let prev = Buffer::empty(Rect::new(0, 0, 10, 1));
let mut next = prev.clone();
next.set_string(0, 0, "plain", Style::new());
let mut b = backend();
b.draw(next.diff_iter(&prev)).unwrap();
let out = String::from_utf8_lossy(bytes(&b)).to_string();
assert!(!out.contains("\x1b]8;;"));
}
#[test]
fn draw_closes_open_link_at_end_of_frame() {
use crate::render::Buffer;
let prev = Buffer::empty(Rect::new(0, 0, 10, 1));
let mut next = prev.clone();
next.set_string(0, 0, "x", Style::new());
next.set_link_range(0, 0, 1, Some("u"));
let mut b = backend();
b.draw(next.diff_iter(&prev)).unwrap();
let out = bytes(&b);
assert!(out.ends_with(b"\x1b]8;;\x1b\\"));
}
}