use crate::{backend::Backend, buffer::Cell}; use crossterm::{ cursor::{CursorShape, Hide, MoveTo, SetCursorShape, Show}, execute, queue, style::{ Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor, SetForegroundColor, SetUnderlineColor, }, terminal::{self, Clear, ClearType}, }; use helix_view::graphics::{Color, CursorKind, Modifier, Rect, UnderlineStyle}; use std::io::{self, Write}; fn vte_version() -> Option { std::env::var("VTE_VERSION").ok()?.parse().ok() } /// Describes terminal capabilities like extended underline, truecolor, etc. #[derive(Copy, Clone, Debug, Default)] struct Capabilities { /// Support for undercurled, underdashed, etc. has_extended_underlines: bool, } impl Capabilities { /// Detect capabilities from the terminfo database located based /// on the $TERM environment variable. If detection fails, returns /// a default value where no capability is supported. pub fn from_env_or_default() -> Self { match cxterminfo::terminfo::TermInfo::from_env() { Err(_) => Capabilities::default(), Ok(t) => Capabilities { // Smulx, VTE: https://unix.stackexchange.com/a/696253/246284 // Su (used by kitty): https://sw.kovidgoyal.net/kitty/underlines has_extended_underlines: t.get_ext_string("Smulx").is_some() || *t.get_ext_bool("Su").unwrap_or(&false) || vte_version() >= Some(5102), }, } } } pub struct CrosstermBackend { buffer: W, capabilities: Capabilities, } impl CrosstermBackend where W: Write, { pub fn new(buffer: W) -> CrosstermBackend { CrosstermBackend { buffer, capabilities: Capabilities::from_env_or_default(), } } } impl Write for CrosstermBackend where W: Write, { fn write(&mut self, buf: &[u8]) -> io::Result { self.buffer.write(buf) } fn flush(&mut self) -> io::Result<()> { self.buffer.flush() } } impl Backend for CrosstermBackend where W: Write, { fn draw<'a, I>(&mut self, content: I) -> io::Result<()> where I: Iterator, { let mut fg = Color::Reset; let mut bg = Color::Reset; let mut underline_color = Color::Reset; let mut underline_style = UnderlineStyle::Reset; let mut modifier = Modifier::empty(); let mut last_pos: Option<(u16, u16)> = None; for (x, y, cell) in content { // Move the cursor if the previous location was not (x - 1, y) if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) { map_error(queue!(self.buffer, MoveTo(x, y)))?; } last_pos = Some((x, y)); if cell.modifier != modifier { let diff = ModifierDiff { from: modifier, to: cell.modifier, }; diff.queue(&mut self.buffer)?; modifier = cell.modifier; } if cell.fg != fg { let color = CColor::from(cell.fg); map_error(queue!(self.buffer, SetForegroundColor(color)))?; fg = cell.fg; } if cell.bg != bg { let color = CColor::from(cell.bg); map_error(queue!(self.buffer, SetBackgroundColor(color)))?; bg = cell.bg; } if cell.underline_color != underline_color { let color = CColor::from(cell.underline_color); map_error(queue!(self.buffer, SetUnderlineColor(color)))?; underline_color = cell.underline_color; } let mut new_underline_style = cell.underline_style; if !self.capabilities.has_extended_underlines { match new_underline_style { UnderlineStyle::Reset => (), _ => new_underline_style = UnderlineStyle::Line, } } if new_underline_style != underline_style { let attr = CAttribute::from(cell.underline_style); map_error(queue!(self.buffer, SetAttribute(attr)))?; underline_style = new_underline_style; } map_error(queue!(self.buffer, Print(&cell.symbol)))?; } map_error(queue!( self.buffer, SetUnderlineColor(CColor::Reset), SetForegroundColor(CColor::Reset), SetBackgroundColor(CColor::Reset), SetAttribute(CAttribute::Reset) )) } fn hide_cursor(&mut self) -> io::Result<()> { map_error(execute!(self.buffer, Hide)) } fn show_cursor(&mut self, kind: CursorKind) -> io::Result<()> { let shape = match kind { CursorKind::Block => CursorShape::Block, CursorKind::Bar => CursorShape::Line, CursorKind::Underline => CursorShape::UnderScore, CursorKind::Hidden => unreachable!(), }; map_error(execute!(self.buffer, Show, SetCursorShape(shape))) } fn get_cursor(&mut self) -> io::Result<(u16, u16)> { crossterm::cursor::position() .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string())) } fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> { map_error(execute!(self.buffer, MoveTo(x, y))) } fn clear(&mut self) -> io::Result<()> { map_error(execute!(self.buffer, Clear(ClearType::All))) } fn size(&self) -> io::Result { let (width, height) = terminal::size().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; Ok(Rect::new(0, 0, width, height)) } fn flush(&mut self) -> io::Result<()> { self.buffer.flush() } } fn map_error(error: crossterm::Result<()>) -> io::Result<()> { error.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string())) } #[derive(Debug)] struct ModifierDiff { pub from: Modifier, pub to: Modifier, } impl ModifierDiff { fn queue(&self, mut w: W) -> io::Result<()> where W: io::Write, { //use crossterm::Attribute; let removed = self.from - self.to; if removed.contains(Modifier::REVERSED) { map_error(queue!(w, SetAttribute(CAttribute::NoReverse)))?; } if removed.contains(Modifier::BOLD) { map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?; if self.to.contains(Modifier::DIM) { map_error(queue!(w, SetAttribute(CAttribute::Dim)))?; } } if removed.contains(Modifier::ITALIC) { map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?; } if removed.contains(Modifier::DIM) { map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?; } if removed.contains(Modifier::CROSSED_OUT) { map_error(queue!(w, SetAttribute(CAttribute::NotCrossedOut)))?; } if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) { map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?; } let added = self.to - self.from; if added.contains(Modifier::REVERSED) { map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?; } if added.contains(Modifier::BOLD) { map_error(queue!(w, SetAttribute(CAttribute::Bold)))?; } if added.contains(Modifier::ITALIC) { map_error(queue!(w, SetAttribute(CAttribute::Italic)))?; } if added.contains(Modifier::DIM) { map_error(queue!(w, SetAttribute(CAttribute::Dim)))?; } if added.contains(Modifier::CROSSED_OUT) { map_error(queue!(w, SetAttribute(CAttribute::CrossedOut)))?; } if added.contains(Modifier::SLOW_BLINK) { map_error(queue!(w, SetAttribute(CAttribute::SlowBlink)))?; } if added.contains(Modifier::RAPID_BLINK) { map_error(queue!(w, SetAttribute(CAttribute::RapidBlink)))?; } Ok(()) } }