Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-tui/src/backend/crossterm.rs')
| -rw-r--r-- | helix-tui/src/backend/crossterm.rs | 261 |
1 files changed, 59 insertions, 202 deletions
diff --git a/helix-tui/src/backend/crossterm.rs b/helix-tui/src/backend/crossterm.rs index 3b53c21f..c00e1f40 100644 --- a/helix-tui/src/backend/crossterm.rs +++ b/helix-tui/src/backend/crossterm.rs @@ -1,142 +1,69 @@ -use crate::{backend::Backend, buffer::Cell, terminal::Config}; +use crate::{backend::Backend, buffer::Cell}; use crossterm::{ - cursor::{Hide, MoveTo, SetCursorStyle, Show}, - event::{ - DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, - EnableFocusChange, EnableMouseCapture, KeyboardEnhancementFlags, - PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, - }, + cursor::{CursorShape, Hide, MoveTo, SetCursorShape, Show}, execute, queue, style::{ - Attribute as CAttribute, Color as CColor, Colors, Print, SetAttribute, SetBackgroundColor, - SetColors, SetForegroundColor, + Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor, + SetForegroundColor, }, terminal::{self, Clear, ClearType}, Command, }; use helix_view::graphics::{Color, CursorKind, Modifier, Rect, UnderlineStyle}; -use once_cell::sync::OnceCell; use std::{ fmt, io::{self, Write}, }; -use termini::TermInfo; fn term_program() -> Option<String> { - // Some terminals don't set $TERM_PROGRAM - match std::env::var("TERM_PROGRAM") { - Err(_) => std::env::var("TERM").ok(), - Ok(term_program) => Some(term_program), - } + std::env::var("TERM_PROGRAM").ok() } fn vte_version() -> Option<usize> { std::env::var("VTE_VERSION").ok()?.parse().ok() } -fn reset_cursor_approach(terminfo: TermInfo) -> String { - let mut reset_str = "\x1B[0 q".to_string(); - - if let Some(termini::Value::Utf8String(se_str)) = terminfo.extended_cap("Se") { - reset_str.push_str(se_str); - }; - - reset_str.push_str( - terminfo - .utf8_string_cap(termini::StringCapability::CursorNormal) - .unwrap_or(""), - ); - - reset_str -} /// Describes terminal capabilities like extended underline, truecolor, etc. -#[derive(Clone, Debug)] +#[derive(Copy, Clone, Debug, Default)] struct Capabilities { /// Support for undercurled, underdashed, etc. has_extended_underlines: bool, - /// Support for resetting the cursor style back to normal. - reset_cursor_command: String, -} - -impl Default for Capabilities { - fn default() -> Self { - Self { - has_extended_underlines: false, - reset_cursor_command: "\x1B[0 q".to_string(), - } - } } 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, or just undercurl - /// if config.undercurl is set. - pub fn from_env_or_default(config: &Config) -> Self { + /// a default value where no capability is supported. + pub fn from_env_or_default() -> Self { match termini::TermInfo::from_env() { - Err(_) => Capabilities { - has_extended_underlines: config.force_enable_extended_underlines, - ..Capabilities::default() - }, + 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 - // WezTerm supports underlines but a lot of distros don't properly install its terminfo - has_extended_underlines: config.force_enable_extended_underlines - || t.extended_cap("Smulx").is_some() + // WezTerm supports underlines but a lot of distros don't properly install it's terminfo + has_extended_underlines: t.extended_cap("Smulx").is_some() || t.extended_cap("Su").is_some() || vte_version() >= Some(5102) || matches!(term_program().as_deref(), Some("WezTerm")), - reset_cursor_command: reset_cursor_approach(t), }, } } } -/// Terminal backend supporting a wide variety of terminals pub struct CrosstermBackend<W: Write> { buffer: W, - config: Config, capabilities: Capabilities, - supports_keyboard_enhancement_protocol: OnceCell<bool>, - mouse_capture_enabled: bool, - supports_bracketed_paste: bool, } impl<W> CrosstermBackend<W> where W: Write, { - pub fn new(buffer: W, config: Config) -> CrosstermBackend<W> { - // helix is not usable without colors, but crossterm will disable - // them by default if NO_COLOR is set in the environment. Override - // this behaviour. - crossterm::style::force_color_output(true); + pub fn new(buffer: W) -> CrosstermBackend<W> { CrosstermBackend { buffer, - capabilities: Capabilities::from_env_or_default(&config), - config, - supports_keyboard_enhancement_protocol: OnceCell::new(), - mouse_capture_enabled: false, - supports_bracketed_paste: true, + capabilities: Capabilities::from_env_or_default(), } } - - #[inline] - fn supports_keyboard_enhancement_protocol(&self) -> bool { - *self.supports_keyboard_enhancement_protocol - .get_or_init(|| { - use std::time::Instant; - - let now = Instant::now(); - let supported = matches!(terminal::supports_keyboard_enhancement(), Ok(true)); - log::debug!( - "The keyboard enhancement protocol is {}supported in this terminal (checked in {:?})", - if supported { "" } else { "not " }, - Instant::now().duration_since(now) - ); - supported - }) - } } impl<W> Write for CrosstermBackend<W> @@ -156,73 +83,6 @@ impl<W> Backend for CrosstermBackend<W> where W: Write, { - fn claim(&mut self) -> io::Result<()> { - terminal::enable_raw_mode()?; - execute!( - self.buffer, - terminal::EnterAlternateScreen, - EnableFocusChange - )?; - match execute!(self.buffer, EnableBracketedPaste,) { - Err(err) if err.kind() == io::ErrorKind::Unsupported => { - log::warn!("Bracketed paste is not supported on this terminal."); - self.supports_bracketed_paste = false; - } - Err(err) => return Err(err), - Ok(_) => (), - }; - execute!(self.buffer, terminal::Clear(terminal::ClearType::All))?; - if self.config.enable_mouse_capture { - execute!(self.buffer, EnableMouseCapture)?; - self.mouse_capture_enabled = true; - } - if self.supports_keyboard_enhancement_protocol() { - execute!( - self.buffer, - PushKeyboardEnhancementFlags( - KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES - | KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS - ) - )?; - } - Ok(()) - } - - fn reconfigure(&mut self, config: Config) -> io::Result<()> { - if self.mouse_capture_enabled != config.enable_mouse_capture { - if config.enable_mouse_capture { - execute!(self.buffer, EnableMouseCapture)?; - } else { - execute!(self.buffer, DisableMouseCapture)?; - } - self.mouse_capture_enabled = config.enable_mouse_capture; - } - self.config = config; - - Ok(()) - } - - fn restore(&mut self) -> io::Result<()> { - // reset cursor shape - self.buffer - .write_all(self.capabilities.reset_cursor_command.as_bytes())?; - if self.config.enable_mouse_capture { - execute!(self.buffer, DisableMouseCapture)?; - } - if self.supports_keyboard_enhancement_protocol() { - execute!(self.buffer, PopKeyboardEnhancementFlags)?; - } - if self.supports_bracketed_paste { - execute!(self.buffer, DisableBracketedPaste,)?; - } - execute!( - self.buffer, - DisableFocusChange, - terminal::LeaveAlternateScreen - )?; - terminal::disable_raw_mode() - } - fn draw<'a, I>(&mut self, content: I) -> io::Result<()> where I: Iterator<Item = (u16, u16, &'a Cell)>, @@ -236,7 +96,7 @@ where 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) { - queue!(self.buffer, MoveTo(x, y))?; + map_error(queue!(self.buffer, MoveTo(x, y)))?; } last_pos = Some((x, y)); if cell.modifier != modifier { @@ -247,12 +107,14 @@ where diff.queue(&mut self.buffer)?; modifier = cell.modifier; } - if cell.fg != fg || cell.bg != bg { - queue!( - self.buffer, - SetColors(Colors::new(cell.fg.into(), cell.bg.into())) - )?; + 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; } @@ -260,7 +122,7 @@ where if self.capabilities.has_extended_underlines { if cell.underline_color != underline_color { let color = CColor::from(cell.underline_color); - queue!(self.buffer, SetUnderlineColor(color))?; + map_error(queue!(self.buffer, SetUnderlineColor(color)))?; underline_color = cell.underline_color; } } else { @@ -272,42 +134,47 @@ where if new_underline_style != underline_style { let attr = CAttribute::from(new_underline_style); - queue!(self.buffer, SetAttribute(attr))?; + map_error(queue!(self.buffer, SetAttribute(attr)))?; underline_style = new_underline_style; } - queue!(self.buffer, Print(&cell.symbol))?; + map_error(queue!(self.buffer, Print(&cell.symbol)))?; } - queue!( + map_error(queue!( self.buffer, SetUnderlineColor(CColor::Reset), SetForegroundColor(CColor::Reset), SetBackgroundColor(CColor::Reset), SetAttribute(CAttribute::Reset) - ) + )) } fn hide_cursor(&mut self) -> io::Result<()> { - execute!(self.buffer, Hide) + map_error(execute!(self.buffer, Hide)) } fn show_cursor(&mut self, kind: CursorKind) -> io::Result<()> { let shape = match kind { - CursorKind::Block => SetCursorStyle::SteadyBlock, - CursorKind::Bar => SetCursorStyle::SteadyBar, - CursorKind::Underline => SetCursorStyle::SteadyUnderScore, + CursorKind::Block => CursorShape::Block, + CursorKind::Bar => CursorShape::Line, + CursorKind::Underline => CursorShape::UnderScore, CursorKind::Hidden => unreachable!(), }; - execute!(self.buffer, Show, shape) + 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<()> { - execute!(self.buffer, MoveTo(x, y)) + map_error(execute!(self.buffer, MoveTo(x, y))) } fn clear(&mut self) -> io::Result<()> { - execute!(self.buffer, Clear(ClearType::All)) + map_error(execute!(self.buffer, Clear(ClearType::All))) } fn size(&self) -> io::Result<Rect> { @@ -320,14 +187,10 @@ where fn flush(&mut self) -> io::Result<()> { self.buffer.flush() } +} - fn supports_true_color(&self) -> bool { - false - } - - fn get_theme_mode(&self) -> Option<helix_view::theme::Mode> { - None - } +fn map_error(error: crossterm::Result<()>) -> io::Result<()> { + error.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string())) } #[derive(Debug)] @@ -344,63 +207,57 @@ impl ModifierDiff { //use crossterm::Attribute; let removed = self.from - self.to; if removed.contains(Modifier::REVERSED) { - queue!(w, SetAttribute(CAttribute::NoReverse))?; + map_error(queue!(w, SetAttribute(CAttribute::NoReverse)))?; } if removed.contains(Modifier::BOLD) { - queue!(w, SetAttribute(CAttribute::NormalIntensity))?; + map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?; if self.to.contains(Modifier::DIM) { - queue!(w, SetAttribute(CAttribute::Dim))?; + map_error(queue!(w, SetAttribute(CAttribute::Dim)))?; } } if removed.contains(Modifier::ITALIC) { - queue!(w, SetAttribute(CAttribute::NoItalic))?; + map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?; } if removed.contains(Modifier::DIM) { - queue!(w, SetAttribute(CAttribute::NormalIntensity))?; + map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?; } if removed.contains(Modifier::CROSSED_OUT) { - queue!(w, SetAttribute(CAttribute::NotCrossedOut))?; + map_error(queue!(w, SetAttribute(CAttribute::NotCrossedOut)))?; } if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) { - queue!(w, SetAttribute(CAttribute::NoBlink))?; - } - if removed.contains(Modifier::HIDDEN) { - queue!(w, SetAttribute(CAttribute::NoHidden))?; + map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?; } let added = self.to - self.from; if added.contains(Modifier::REVERSED) { - queue!(w, SetAttribute(CAttribute::Reverse))?; + map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?; } if added.contains(Modifier::BOLD) { - queue!(w, SetAttribute(CAttribute::Bold))?; + map_error(queue!(w, SetAttribute(CAttribute::Bold)))?; } if added.contains(Modifier::ITALIC) { - queue!(w, SetAttribute(CAttribute::Italic))?; + map_error(queue!(w, SetAttribute(CAttribute::Italic)))?; } if added.contains(Modifier::DIM) { - queue!(w, SetAttribute(CAttribute::Dim))?; + map_error(queue!(w, SetAttribute(CAttribute::Dim)))?; } if added.contains(Modifier::CROSSED_OUT) { - queue!(w, SetAttribute(CAttribute::CrossedOut))?; + map_error(queue!(w, SetAttribute(CAttribute::CrossedOut)))?; } if added.contains(Modifier::SLOW_BLINK) { - queue!(w, SetAttribute(CAttribute::SlowBlink))?; + map_error(queue!(w, SetAttribute(CAttribute::SlowBlink)))?; } if added.contains(Modifier::RAPID_BLINK) { - queue!(w, SetAttribute(CAttribute::RapidBlink))?; - } - if added.contains(Modifier::HIDDEN) { - queue!(w, SetAttribute(CAttribute::Hidden))?; + map_error(queue!(w, SetAttribute(CAttribute::RapidBlink)))?; } Ok(()) } } -/// Crossterm uses semicolon as a separator for colors -/// this is actually not spec compliant (although commonly supported) -/// However the correct approach is to use colons as a separator. +/// Crossterm uses semicolon as a seperator for colors +/// this is actually not spec compliant (altough commonly supported) +/// However the correct approach is to use colons as a seperator. /// This usually doesn't make a difference for emulators that do support colored underlines. /// However terminals that do not support colored underlines will ignore underlines colors with colons /// while escape sequences with semicolons are always processed which leads to weird visual artifacts. @@ -445,7 +302,7 @@ impl Command for SetUnderlineColor { } #[cfg(windows)] - fn execute_winapi(&self) -> io::Result<()> { + fn execute_winapi(&self) -> crossterm::Result<()> { Err(std::io::Error::new( std::io::ErrorKind::Other, "SetUnderlineColor not supported by winapi.", |