Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-tui/src/backend/termina.rs')
| -rw-r--r-- | helix-tui/src/backend/termina.rs | 649 |
1 files changed, 0 insertions, 649 deletions
diff --git a/helix-tui/src/backend/termina.rs b/helix-tui/src/backend/termina.rs deleted file mode 100644 index ad1c7c68..00000000 --- a/helix-tui/src/backend/termina.rs +++ /dev/null @@ -1,649 +0,0 @@ -use std::io::{self, Write as _}; - -use helix_view::{ - editor::KittyKeyboardProtocolConfig, - graphics::{CursorKind, Rect, UnderlineStyle}, - theme::{self, Color, Modifier}, -}; -use termina::{ - escape::{ - csi::{self, Csi, SgrAttributes, SgrModifiers}, - dcs::{self, Dcs}, - }, - style::{CursorStyle, RgbColor}, - Event, OneBased, PlatformTerminal, Terminal as _, WindowSize, -}; - -use crate::{buffer::Cell, terminal::Config}; - -use super::Backend; - -// These macros are helpers to set/unset modes like bracketed paste or enter/exit the alternate -// screen. -macro_rules! decset { - ($mode:ident) => { - Csi::Mode(csi::Mode::SetDecPrivateMode(csi::DecPrivateMode::Code( - csi::DecPrivateModeCode::$mode, - ))) - }; -} -macro_rules! decreset { - ($mode:ident) => { - Csi::Mode(csi::Mode::ResetDecPrivateMode(csi::DecPrivateMode::Code( - csi::DecPrivateModeCode::$mode, - ))) - }; -} - -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), - } -} -fn vte_version() -> Option<usize> { - std::env::var("VTE_VERSION").ok()?.parse().ok() -} - -#[derive(Debug, Default, Clone, Copy)] -struct Capabilities { - kitty_keyboard: KittyKeyboardSupport, - synchronized_output: bool, - true_color: bool, - extended_underlines: bool, - theme_mode: Option<theme::Mode>, -} - -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -enum KittyKeyboardSupport { - /// The terminal doesn't support the protocol. - #[default] - None, - /// The terminal supports the protocol but we haven't checked yet whether it has full or - /// partial support for the flags we require. - Some, - /// The terminal only supports some of the flags we require. - Partial, - /// The terminal supports all flags require. - Full, -} - -#[derive(Debug)] -pub struct TerminaBackend { - terminal: PlatformTerminal, - config: Config, - capabilities: Capabilities, - reset_cursor_command: String, - is_synchronized_output_set: bool, -} - -impl TerminaBackend { - pub fn new(config: Config) -> io::Result<Self> { - let mut terminal = PlatformTerminal::new()?; - let (capabilities, reset_cursor_command) = - Self::detect_capabilities(&mut terminal, &config)?; - - // In the case of a panic, reset the terminal eagerly. If we didn't do this and instead - // relied on `Drop`, the backtrace would be lost because it is printed before we would - // clear and exit the alternate screen. - let hook_reset_cursor_command = reset_cursor_command.clone(); - terminal.set_panic_hook(move |term| { - let _ = write!( - term, - "{}{}{}{}{}{}{}{}{}{}{}", - Csi::Keyboard(csi::Keyboard::PopFlags(1)), - decreset!(MouseTracking), - decreset!(ButtonEventMouse), - decreset!(AnyEventMouse), - decreset!(RXVTMouse), - decreset!(SGRMouse), - &hook_reset_cursor_command, - decreset!(BracketedPaste), - decreset!(FocusTracking), - Csi::Edit(csi::Edit::EraseInDisplay(csi::EraseInDisplay::EraseDisplay)), - decreset!(ClearAndEnableAlternateScreen), - ); - }); - - Ok(Self { - terminal, - config, - capabilities, - reset_cursor_command, - is_synchronized_output_set: false, - }) - } - - pub fn terminal(&self) -> &PlatformTerminal { - &self.terminal - } - - fn detect_capabilities( - terminal: &mut PlatformTerminal, - config: &Config, - ) -> io::Result<(Capabilities, String)> { - use std::time::{Duration, Instant}; - - // Colibri "midnight" - const TEST_COLOR: RgbColor = RgbColor::new(59, 34, 76); - - terminal.enter_raw_mode()?; - - let mut capabilities = Capabilities::default(); - let start = Instant::now(); - - capabilities.kitty_keyboard = match config.kitty_keyboard_protocol { - KittyKeyboardProtocolConfig::Disabled => KittyKeyboardSupport::None, - KittyKeyboardProtocolConfig::Enabled => KittyKeyboardSupport::Full, - KittyKeyboardProtocolConfig::Auto => { - write!(terminal, "{}", Csi::Keyboard(csi::Keyboard::QueryFlags))?; - KittyKeyboardSupport::None - } - }; - - // Many terminal extensions can be detected by querying the terminal for the state of the - // extension and then sending a request for the primary device attributes (which is - // consistently supported by all terminals). If we receive the status of the feature (for - // example the current Kitty keyboard flags) then we know that the feature is supported. - // If we only receive the device attributes then we know it is not. - write!( - terminal, - "{}{}{}{}{}{}{}", - // Synchronized output - Csi::Mode(csi::Mode::QueryDecPrivateMode(csi::DecPrivateMode::Code( - csi::DecPrivateModeCode::SynchronizedOutput - ))), - // Mode 2031 theme updates. Query the current theme. - Csi::Mode(csi::Mode::QueryTheme), - // True color and while we're at it, extended underlines: - // <https://github.com/termstandard/colors?tab=readme-ov-file#querying-the-terminal> - Csi::Sgr(csi::Sgr::Background(TEST_COLOR.into())), - Csi::Sgr(csi::Sgr::UnderlineColor(TEST_COLOR.into())), - Dcs::Request(dcs::DcsRequest::GraphicRendition), - Csi::Sgr(csi::Sgr::Reset), - // Finally request the primary device attributes - Csi::Device(csi::Device::RequestPrimaryDeviceAttributes), - )?; - terminal.flush()?; - - let device_attributes = |event: &Event| { - matches!( - event, - Event::Csi(Csi::Device(csi::Device::DeviceAttributes(_))) - ) - }; - // TODO: tune this poll constant? Does it need to be longer when on an SSH connection? - let poll_duration = Duration::from_millis(100); - if terminal.poll(device_attributes, Some(poll_duration))? { - while terminal.poll(Event::is_escape, Some(Duration::ZERO))? { - match terminal.read(Event::is_escape)? { - Event::Csi(Csi::Keyboard(csi::Keyboard::ReportFlags(_))) => { - capabilities.kitty_keyboard = KittyKeyboardSupport::Some; - } - Event::Csi(Csi::Mode(csi::Mode::ReportDecPrivateMode { - mode: csi::DecPrivateMode::Code(csi::DecPrivateModeCode::SynchronizedOutput), - setting: csi::DecModeSetting::Set | csi::DecModeSetting::Reset, - })) => { - capabilities.synchronized_output = true; - } - Event::Csi(Csi::Mode(csi::Mode::ReportTheme(mode))) => { - capabilities.theme_mode = Some(mode.into()); - } - Event::Dcs(dcs::Dcs::Response { - value: dcs::DcsResponse::GraphicRendition(sgrs), - .. - }) => { - capabilities.true_color = - sgrs.contains(&csi::Sgr::Background(TEST_COLOR.into())); - capabilities.extended_underlines = - sgrs.contains(&csi::Sgr::UnderlineColor(TEST_COLOR.into())); - } - _ => (), - } - } - - let end = Instant::now(); - log::debug!( - "Detected terminal capabilities in {:?}: {capabilities:?}", - end.duration_since(start) - ); - } else { - log::debug!("Failed to detect terminal capabilities within {poll_duration:?}. Using default capabilities only"); - } - - capabilities.extended_underlines |= config.force_enable_extended_underlines; - - let mut reset_cursor_command = - Csi::Cursor(csi::Cursor::CursorStyle(CursorStyle::Default)).to_string(); - if let Ok(t) = termini::TermInfo::from_env() { - capabilities.extended_underlines |= t.extended_cap("Smulx").is_some() - || t.extended_cap("Su").is_some() - || vte_version() >= Some(5102) - // HACK: once WezTerm can support DECRQSS/DECRPSS for SGR we can remove this line. - // <https://github.com/wezterm/wezterm/pull/6856> - || matches!(term_program().as_deref(), Some("WezTerm")); - - if let Some(termini::Value::Utf8String(se_str)) = t.extended_cap("Se") { - reset_cursor_command.push_str(se_str); - }; - reset_cursor_command.push_str( - t.utf8_string_cap(termini::StringCapability::CursorNormal) - .unwrap_or(""), - ); - log::debug!( - "Cursor reset escape sequence detected from terminfo: {reset_cursor_command:?}" - ); - } else { - log::debug!("terminfo could not be read, using default cursor reset escape sequence: {reset_cursor_command:?}"); - } - - terminal.enter_cooked_mode()?; - - Ok((capabilities, reset_cursor_command)) - } - - fn enable_mouse_capture(&mut self) -> io::Result<()> { - if self.config.enable_mouse_capture { - write!( - self.terminal, - "{}{}{}{}{}", - decset!(MouseTracking), - decset!(ButtonEventMouse), - decset!(AnyEventMouse), - decset!(RXVTMouse), - decset!(SGRMouse), - )?; - } - Ok(()) - } - - fn disable_mouse_capture(&mut self) -> io::Result<()> { - if self.config.enable_mouse_capture { - write!( - self.terminal, - "{}{}{}{}{}", - decreset!(MouseTracking), - decreset!(ButtonEventMouse), - decreset!(AnyEventMouse), - decreset!(RXVTMouse), - decreset!(SGRMouse), - )?; - } - Ok(()) - } - - fn enable_extensions(&mut self) -> io::Result<()> { - const KEYBOARD_FLAGS: csi::KittyKeyboardFlags = - csi::KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES - .union(csi::KittyKeyboardFlags::REPORT_ALTERNATE_KEYS); - - match self.capabilities.kitty_keyboard { - KittyKeyboardSupport::None | KittyKeyboardSupport::Partial => (), - KittyKeyboardSupport::Full => { - write!( - self.terminal, - "{}", - Csi::Keyboard(csi::Keyboard::PushFlags(KEYBOARD_FLAGS)) - )?; - } - KittyKeyboardSupport::Some => { - write!( - self.terminal, - "{}{}", - // Enable the flags we need. - Csi::Keyboard(csi::Keyboard::PushFlags(KEYBOARD_FLAGS)), - // Then request the current flags. We need to check if the terminal enabled - // all of the flags we require. - Csi::Keyboard(csi::Keyboard::QueryFlags), - )?; - self.terminal.flush()?; - - let event = self.terminal.read(|event| { - matches!( - event, - Event::Csi(Csi::Keyboard(csi::Keyboard::ReportFlags(_))) - ) - })?; - let Event::Csi(Csi::Keyboard(csi::Keyboard::ReportFlags(flags))) = event else { - unreachable!(); - }; - if flags != KEYBOARD_FLAGS { - log::info!("Turning off enhanced keyboard support because the terminal enabled different flags. Requested {KEYBOARD_FLAGS:?} but got {flags:?}"); - write!( - self.terminal, - "{}", - Csi::Keyboard(csi::Keyboard::PopFlags(1)) - )?; - self.terminal.flush()?; - self.capabilities.kitty_keyboard = KittyKeyboardSupport::Partial; - } else { - log::debug!( - "The terminal fully supports the requested keyboard enhancement flags" - ); - self.capabilities.kitty_keyboard = KittyKeyboardSupport::Full; - } - } - } - - if self.capabilities.theme_mode.is_some() { - // Enable mode 2031 theme mode notifications: - write!(self.terminal, "{}", decset!(Theme))?; - } - - Ok(()) - } - - fn disable_extensions(&mut self) -> io::Result<()> { - if self.capabilities.kitty_keyboard == KittyKeyboardSupport::Full { - write!( - self.terminal, - "{}", - Csi::Keyboard(csi::Keyboard::PopFlags(1)) - )?; - } - - if self.capabilities.theme_mode.is_some() { - // Mode 2031 theme notifications. - write!(self.terminal, "{}", decreset!(Theme))?; - } - - Ok(()) - } - - // See <https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036>. - // Synchronized output sequences tell the terminal when we are "starting to render" and - // stopping, enabling to make better choices about when it draws a frame. This avoids all - // kinds of ugly visual artifacts like tearing and flashing (i.e. the background color - // after clearing the terminal). - - fn start_synchronized_render(&mut self) -> io::Result<()> { - if self.capabilities.synchronized_output && !self.is_synchronized_output_set { - write!(self.terminal, "{}", decset!(SynchronizedOutput))?; - self.is_synchronized_output_set = true; - } - Ok(()) - } - - fn end_sychronized_render(&mut self) -> io::Result<()> { - if self.is_synchronized_output_set { - write!(self.terminal, "{}", decreset!(SynchronizedOutput))?; - self.is_synchronized_output_set = false; - } - Ok(()) - } -} - -impl Backend for TerminaBackend { - fn claim(&mut self) -> io::Result<()> { - self.terminal.enter_raw_mode()?; - - write!( - self.terminal, - "{}{}{}{}", - // Enter an alternate screen. - decset!(ClearAndEnableAlternateScreen), - decset!(BracketedPaste), - decset!(FocusTracking), - // Clear the buffer. `ClearAndEnableAlternateScreen` **should** do this but some - // things like mosh are buggy. See <https://github.com/helix-editor/helix/pull/1944>. - Csi::Edit(csi::Edit::EraseInDisplay(csi::EraseInDisplay::EraseDisplay)), - )?; - self.enable_mouse_capture()?; - self.enable_extensions()?; - - Ok(()) - } - - fn reconfigure(&mut self, mut config: Config) -> io::Result<()> { - std::mem::swap(&mut self.config, &mut config); - if self.config.enable_mouse_capture != config.enable_mouse_capture { - if self.config.enable_mouse_capture { - self.enable_mouse_capture()?; - } else { - self.disable_mouse_capture()?; - } - } - self.capabilities.extended_underlines |= self.config.force_enable_extended_underlines; - Ok(()) - } - - fn restore(&mut self) -> io::Result<()> { - self.disable_extensions()?; - self.disable_mouse_capture()?; - write!( - self.terminal, - "{}{}{}{}", - &self.reset_cursor_command, - decreset!(BracketedPaste), - decreset!(FocusTracking), - decreset!(ClearAndEnableAlternateScreen), - )?; - self.terminal.flush()?; - self.terminal.enter_cooked_mode()?; - Ok(()) - } - - fn draw<'a, I>(&mut self, content: I) -> io::Result<()> - where - I: Iterator<Item = (u16, u16, &'a Cell)>, - { - self.start_synchronized_render()?; - - 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) { - write!( - self.terminal, - "{}", - Csi::Cursor(csi::Cursor::Position { - col: OneBased::from_zero_based(x), - line: OneBased::from_zero_based(y), - }) - )?; - } - last_pos = Some((x, y)); - - let mut attributes = SgrAttributes::default(); - if cell.fg != fg { - attributes.foreground = Some(cell.fg.into()); - fg = cell.fg; - } - if cell.bg != bg { - attributes.background = Some(cell.bg.into()); - bg = cell.bg; - } - if cell.modifier != modifier { - attributes.modifiers = diff_modifiers(modifier, cell.modifier); - modifier = cell.modifier; - } - - // Set underline style and color separately from SgrAttributes. Some terminals seem - // to not like underline colors and styles being intermixed with other SGRs. - let mut new_underline_style = cell.underline_style; - if self.capabilities.extended_underlines { - if cell.underline_color != underline_color { - write!( - self.terminal, - "{}", - Csi::Sgr(csi::Sgr::UnderlineColor(cell.underline_color.into())) - )?; - underline_color = cell.underline_color; - } - } else { - match new_underline_style { - UnderlineStyle::Reset | UnderlineStyle::Line => (), - _ => new_underline_style = UnderlineStyle::Line, - } - } - if new_underline_style != underline_style { - write!( - self.terminal, - "{}", - Csi::Sgr(csi::Sgr::Underline(new_underline_style.into())) - )?; - underline_style = new_underline_style; - } - - // `attributes` will be empty if nothing changed between two cells. Empty - // `SgrAttributes` behave the same as a `Sgr::Reset` rather than a 'no-op' though so - // we should avoid writing them if they're empty. - if !attributes.is_empty() { - write!( - self.terminal, - "{}", - Csi::Sgr(csi::Sgr::Attributes(attributes)) - )?; - } - - write!(self.terminal, "{}", &cell.symbol)?; - } - - write!(self.terminal, "{}", Csi::Sgr(csi::Sgr::Reset))?; - - self.end_sychronized_render()?; - - Ok(()) - } - - fn hide_cursor(&mut self) -> io::Result<()> { - write!(self.terminal, "{}", decreset!(ShowCursor))?; - self.flush() - } - - fn show_cursor(&mut self, kind: CursorKind) -> io::Result<()> { - let style = match kind { - CursorKind::Block => CursorStyle::SteadyBlock, - CursorKind::Bar => CursorStyle::SteadyBar, - CursorKind::Underline => CursorStyle::SteadyUnderline, - CursorKind::Hidden => unreachable!(), - }; - write!( - self.terminal, - "{}{}", - decset!(ShowCursor), - Csi::Cursor(csi::Cursor::CursorStyle(style)), - )?; - self.flush() - } - - fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> { - let col = OneBased::from_zero_based(x); - let line = OneBased::from_zero_based(y); - write!( - self.terminal, - "{}", - Csi::Cursor(csi::Cursor::Position { line, col }) - )?; - self.flush() - } - - fn clear(&mut self) -> io::Result<()> { - self.start_synchronized_render()?; - write!( - self.terminal, - "{}", - Csi::Edit(csi::Edit::EraseInDisplay(csi::EraseInDisplay::EraseDisplay)) - )?; - self.flush() - } - - fn size(&self) -> io::Result<Rect> { - let WindowSize { rows, cols, .. } = self.terminal.get_dimensions()?; - Ok(Rect::new(0, 0, cols, rows)) - } - - fn flush(&mut self) -> io::Result<()> { - self.terminal.flush() - } - - fn supports_true_color(&self) -> bool { - self.capabilities.true_color - } - - fn get_theme_mode(&self) -> Option<theme::Mode> { - self.capabilities.theme_mode - } -} - -impl Drop for TerminaBackend { - fn drop(&mut self) { - // Avoid resetting the terminal while panicking because we set a panic hook above in - // `Self::new`. - if !std::thread::panicking() { - let _ = self.disable_extensions(); - let _ = self.disable_mouse_capture(); - let _ = write!( - self.terminal, - "{}{}{}{}", - &self.reset_cursor_command, - decreset!(BracketedPaste), - decreset!(FocusTracking), - decreset!(ClearAndEnableAlternateScreen), - ); - // NOTE: Drop for Platform terminal resets the mode and flushes the buffer when not - // panicking. - } - } -} - -fn diff_modifiers(from: Modifier, to: Modifier) -> SgrModifiers { - let mut modifiers = SgrModifiers::default(); - - let removed = from - to; - if removed.contains(Modifier::REVERSED) { - modifiers |= SgrModifiers::NO_REVERSE; - } - if removed.contains(Modifier::BOLD) && !to.contains(Modifier::DIM) { - modifiers |= SgrModifiers::INTENSITY_NORMAL; - } - if removed.contains(Modifier::DIM) { - modifiers |= SgrModifiers::INTENSITY_NORMAL; - } - if removed.contains(Modifier::ITALIC) { - modifiers |= SgrModifiers::NO_ITALIC; - } - if removed.contains(Modifier::CROSSED_OUT) { - modifiers |= SgrModifiers::NO_STRIKE_THROUGH; - } - if removed.contains(Modifier::HIDDEN) { - modifiers |= SgrModifiers::NO_INVISIBLE; - } - if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) { - modifiers |= SgrModifiers::BLINK_NONE; - } - - let added = to - from; - if added.contains(Modifier::REVERSED) { - modifiers |= SgrModifiers::REVERSE; - } - if added.contains(Modifier::BOLD) { - modifiers |= SgrModifiers::INTENSITY_BOLD; - } - if added.contains(Modifier::DIM) { - modifiers |= SgrModifiers::INTENSITY_DIM; - } - if added.contains(Modifier::ITALIC) { - modifiers |= SgrModifiers::ITALIC; - } - if added.contains(Modifier::CROSSED_OUT) { - modifiers |= SgrModifiers::STRIKE_THROUGH; - } - if added.contains(Modifier::HIDDEN) { - modifiers |= SgrModifiers::INVISIBLE; - } - if added.contains(Modifier::SLOW_BLINK) { - modifiers |= SgrModifiers::BLINK_SLOW; - } - if added.contains(Modifier::RAPID_BLINK) { - modifiers |= SgrModifiers::BLINK_RAPID; - } - - modifiers -} |