use crossterm::{
cursor::{Hide, MoveTo, Show},
event::{
DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
EnableFocusChange, EnableMouseCapture,
},
execute, queue,
style::{
Attribute, Color as CrosstermColor, ResetColor, SetAttribute, SetBackgroundColor,
SetForegroundColor,
},
terminal::{
self, disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen,
LeaveAlternateScreen,
},
};
use std::io::{self, Write};
use super::traits::{Backend, BackendCapabilities};
use crate::render::cell::Modifier;
use crate::style::Color;
use crate::Result;
pub struct CrosstermBackend<W: Write> {
writer: W,
raw_mode: bool,
mouse_enabled: bool,
bracketed_paste_enabled: bool,
focus_events_enabled: bool,
}
impl<W: Write> CrosstermBackend<W> {
pub fn new(writer: W) -> Self {
Self {
writer,
raw_mode: false,
mouse_enabled: false,
bracketed_paste_enabled: false,
focus_events_enabled: false,
}
}
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 init(&mut self) -> Result<()> {
self.init_with_mouse(true)
}
fn init_with_mouse(&mut self, enable_mouse: bool) -> Result<()> {
enable_raw_mode()?;
self.raw_mode = true;
if enable_mouse {
execute!(
self.writer,
EnterAlternateScreen,
EnableMouseCapture,
EnableBracketedPaste,
EnableFocusChange,
Hide,
Clear(ClearType::All)
)?;
self.mouse_enabled = true;
} else {
execute!(
self.writer,
EnterAlternateScreen,
EnableBracketedPaste,
EnableFocusChange,
Hide,
Clear(ClearType::All)
)?;
}
self.bracketed_paste_enabled = true;
self.focus_events_enabled = true;
Ok(())
}
fn restore(&mut self) -> Result<()> {
if self.raw_mode {
if self.mouse_enabled {
execute!(
self.writer,
DisableMouseCapture,
DisableBracketedPaste,
DisableFocusChange,
ResetColor,
Show,
LeaveAlternateScreen
)?;
} else {
execute!(
self.writer,
DisableBracketedPaste,
DisableFocusChange,
ResetColor,
Show,
LeaveAlternateScreen
)?;
}
disable_raw_mode()?;
self.raw_mode = false;
self.mouse_enabled = false;
self.bracketed_paste_enabled = false;
self.focus_events_enabled = false;
}
Ok(())
}
fn size(&self) -> Result<(u16, u16)> {
Ok(terminal::size()?)
}
fn clear(&mut self) -> Result<()> {
execute!(self.writer, Clear(ClearType::All))?;
Ok(())
}
fn hide_cursor(&mut self) -> Result<()> {
execute!(self.writer, Hide)?;
Ok(())
}
fn show_cursor(&mut self) -> Result<()> {
execute!(self.writer, Show)?;
Ok(())
}
fn set_cursor(&mut self, x: u16, y: u16) -> Result<()> {
queue!(self.writer, MoveTo(x, y))?;
Ok(())
}
fn set_fg(&mut self, color: Color) -> Result<()> {
queue!(self.writer, SetForegroundColor(to_crossterm_color(color)))?;
Ok(())
}
fn set_bg(&mut self, color: Color) -> Result<()> {
queue!(self.writer, SetBackgroundColor(to_crossterm_color(color)))?;
Ok(())
}
fn reset_fg(&mut self) -> Result<()> {
queue!(self.writer, SetForegroundColor(CrosstermColor::Reset))?;
Ok(())
}
fn reset_bg(&mut self) -> Result<()> {
queue!(self.writer, SetBackgroundColor(CrosstermColor::Reset))?;
Ok(())
}
fn set_modifier(&mut self, modifier: Modifier) -> Result<()> {
if modifier.contains(Modifier::BOLD) {
queue!(self.writer, SetAttribute(Attribute::Bold))?;
}
if modifier.contains(Modifier::ITALIC) {
queue!(self.writer, SetAttribute(Attribute::Italic))?;
}
if modifier.contains(Modifier::UNDERLINE) {
queue!(self.writer, SetAttribute(Attribute::Underlined))?;
}
if modifier.contains(Modifier::DIM) {
queue!(self.writer, SetAttribute(Attribute::Dim))?;
}
if modifier.contains(Modifier::CROSSED_OUT) {
queue!(self.writer, SetAttribute(Attribute::CrossedOut))?;
}
Ok(())
}
fn reset_style(&mut self) -> Result<()> {
queue!(self.writer, SetAttribute(Attribute::Reset))?;
Ok(())
}
fn enable_mouse(&mut self) -> Result<()> {
if !self.mouse_enabled {
execute!(self.writer, EnableMouseCapture)?;
self.mouse_enabled = true;
}
Ok(())
}
fn disable_mouse(&mut self) -> Result<()> {
if self.mouse_enabled {
execute!(self.writer, DisableMouseCapture)?;
self.mouse_enabled = false;
}
Ok(())
}
fn capabilities(&self) -> BackendCapabilities {
BackendCapabilities {
true_color: true,
hyperlinks: true,
mouse: true,
bracketed_paste: true,
focus_events: true,
}
}
fn name(&self) -> &'static str {
"crossterm"
}
}
impl<W: Write> Drop for CrosstermBackend<W> {
fn drop(&mut self) {
let _ = self.restore();
}
}
fn to_crossterm_color(color: Color) -> CrosstermColor {
CrosstermColor::Rgb {
r: color.r,
g: color.g,
b: color.b,
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockWriter {
buffer: Vec<u8>,
}
impl MockWriter {
fn new() -> Self {
Self { buffer: Vec::new() }
}
}
impl Write for MockWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.buffer.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[test]
fn test_backend_name() {
let backend = CrosstermBackend::new(MockWriter::new());
assert_eq!(backend.name(), "crossterm");
}
#[test]
fn test_capabilities() {
let backend = CrosstermBackend::new(MockWriter::new());
let caps = backend.capabilities();
assert!(caps.true_color);
assert!(caps.hyperlinks);
assert!(caps.mouse);
}
#[test]
fn test_to_crossterm_color() {
let color = Color::rgb(255, 128, 64);
let ct_color = to_crossterm_color(color);
match ct_color {
CrosstermColor::Rgb { r, g, b } => {
assert_eq!(r, 255);
assert_eq!(g, 128);
assert_eq!(b, 64);
}
_ => panic!("Expected RGB color"),
}
}
}