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}; 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::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, self.capabilities)?; 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 != underline { let color = CColor::from(cell.underline); map_error(queue!(self.buffer, SetUnderlineColor(color)))?; underline = cell.underline; } map_error(queue!(self.buffer, Print(&cell.symbol)))?; } map_error(queue!( self.buffer, 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, caps: Capabilities) -> 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.intersects(Modifier::ANY_UNDERLINE) { map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?; } 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 queue_styled_underline = |styled_underline, w: &mut W| -> io::Result<()> { let underline = match caps.has_extended_underlines { true => styled_underline, false => CAttribute::Underlined, }; map_error(queue!(w, SetAttribute(underline))) }; 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::UNDERLINED) { map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?; } if added.contains(Modifier::UNDERCURLED) { queue_styled_underline(CAttribute::Undercurled, &mut w)?; } if added.contains(Modifier::UNDERDOTTED) { queue_styled_underline(CAttribute::Underdotted, &mut w)?; } if added.contains(Modifier::UNDERDASHED) { queue_styled_underline(CAttribute::Underdashed, &mut w)?; } if added.contains(Modifier::DOUBLE_UNDERLINED) { queue_styled_underline(CAttribute::DoubleUnderlined, &mut w)?; } 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(()) } }