Unnamed repository; edit this file 'description' to name the repository.
tui: Use Crossterm on Windows (#14454)
| -rw-r--r-- | Cargo.lock | 96 | ||||
| -rw-r--r-- | helix-term/Cargo.toml | 5 | ||||
| -rw-r--r-- | helix-term/src/application.rs | 62 | ||||
| -rw-r--r-- | helix-term/tests/test/helpers.rs | 6 | ||||
| -rw-r--r-- | helix-tui/Cargo.toml | 5 | ||||
| -rw-r--r-- | helix-tui/src/backend/crossterm.rs | 450 | ||||
| -rw-r--r-- | helix-tui/src/backend/mod.rs | 9 | ||||
| -rw-r--r-- | helix-view/Cargo.toml | 3 | ||||
| -rw-r--r-- | helix-view/src/graphics.rs | 42 | ||||
| -rw-r--r-- | helix-view/src/input.rs | 107 | ||||
| -rw-r--r-- | helix-view/src/keyboard.rs | 212 |
11 files changed, 972 insertions, 25 deletions
@@ -242,6 +242,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "futures-core", + "mio", + "parking_lot", + "rustix 0.38.44", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -867,7 +893,7 @@ dependencies = [ "itoa", "libc", "memmap2", - "rustix", + "rustix 1.1.2", "smallvec", "thiserror", ] @@ -1493,7 +1519,7 @@ dependencies = [ "regex-automata", "regex-cursor", "ropey", - "rustix", + "rustix 1.1.2", "tempfile", "unicode-segmentation", "which", @@ -1508,6 +1534,7 @@ dependencies = [ "arc-swap", "chrono", "content_inspector", + "crossterm", "dashmap", "fern", "futures-util", @@ -1553,6 +1580,7 @@ version = "25.7.1" dependencies = [ "bitflags", "cassowary", + "crossterm", "helix-core", "helix-view", "log", @@ -1587,6 +1615,7 @@ dependencies = [ "bitflags", "chardetng", "clipboard-win", + "crossterm", "futures-util", "helix-core", "helix-dap", @@ -1601,7 +1630,7 @@ dependencies = [ "log", "once_cell", "parking_lot", - "rustix", + "rustix 1.1.2", "serde", "serde_json", "slotmap", @@ -1975,6 +2004,12 @@ dependencies = [ [[package]] name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" @@ -2053,6 +2088,7 @@ checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ "hermit-abi", "libc", + "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -2339,6 +2375,19 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" @@ -2346,7 +2395,7 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.11.0", "windows-sys 0.61.0", ] @@ -2467,6 +2516,17 @@ dependencies = [ ] [[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] name = "signal-hook-registry" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2587,7 +2647,7 @@ dependencies = [ "fastrand", "getrandom 0.3.1", "once_cell", - "rustix", + "rustix 1.1.2", "windows-sys 0.61.0", ] @@ -2600,7 +2660,7 @@ dependencies = [ "bitflags", "futures-core", "parking_lot", - "rustix", + "rustix 1.1.2", "signal-hook", "windows-sys 0.61.0", ] @@ -2973,11 +3033,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" dependencies = [ "env_home", - "rustix", + "rustix 1.1.2", "winsafe", ] [[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] name = "winapi-util" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2987,6 +3063,12 @@ dependencies = [ ] [[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 4585aaad..f196be0a 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -54,7 +54,7 @@ anyhow = "1" once_cell = "1.21" tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } -tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["termina"] } +tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["termina", "crossterm"] } termina = { workspace = true, features = ["event-stream"] } signal-hook = "0.3" tokio-stream = "0.1" @@ -93,6 +93,9 @@ grep-searcher = "0.1.14" dashmap = "6.0" +[target.'cfg(windows)'.dependencies] +crossterm = { version = "0.28", features = ["event-stream"] } + [target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100 signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } libc = "0.2.175" diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index c402633c..9ee02a53 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -36,6 +36,7 @@ use std::{ sync::Arc, }; +#[cfg_attr(windows, allow(unused_imports))] use anyhow::{Context, Error}; #[cfg(not(windows))] @@ -43,18 +44,27 @@ use {signal_hook::consts::signal, signal_hook_tokio::Signals}; #[cfg(windows)] type Signals = futures_util::stream::Empty<()>; -#[cfg(not(feature = "integration"))] +#[cfg(all(not(windows), not(feature = "integration")))] use tui::backend::TerminaBackend; +#[cfg(all(windows, not(feature = "integration")))] +use tui::backend::CrosstermBackend; + #[cfg(feature = "integration")] use tui::backend::TestBackend; -#[cfg(not(feature = "integration"))] +#[cfg(all(not(windows), not(feature = "integration")))] type TerminalBackend = TerminaBackend; - +#[cfg(all(windows, not(feature = "integration")))] +type TerminalBackend = CrosstermBackend<std::io::Stdout>; #[cfg(feature = "integration")] type TerminalBackend = TestBackend; +#[cfg(not(windows))] +type TerminalEvent = termina::Event; +#[cfg(windows)] +type TerminalEvent = crossterm::event::Event; + type Terminal = tui::terminal::Terminal<TerminalBackend>; pub struct Application { @@ -102,9 +112,11 @@ impl Application { theme_parent_dirs.extend(helix_loader::runtime_dirs().iter().cloned()); let theme_loader = theme::Loader::new(&theme_parent_dirs); - #[cfg(not(feature = "integration"))] + #[cfg(all(not(windows), not(feature = "integration")))] let backend = TerminaBackend::new((&config.editor).into()) .context("failed to create terminal backend")?; + #[cfg(all(windows, not(feature = "integration")))] + let backend = CrosstermBackend::new(std::io::stdout(), (&config.editor).into()); #[cfg(feature = "integration")] let backend = TestBackend::new(120, 150); @@ -286,7 +298,7 @@ impl Application { pub async fn event_loop<S>(&mut self, input_stream: &mut S) where - S: Stream<Item = std::io::Result<termina::Event>> + Unpin, + S: Stream<Item = std::io::Result<TerminalEvent>> + Unpin, { self.render().await; @@ -299,7 +311,7 @@ impl Application { pub async fn event_loop_until_idle<S>(&mut self, input_stream: &mut S) -> bool where - S: Stream<Item = std::io::Result<termina::Event>> + Unpin, + S: Stream<Item = std::io::Result<TerminalEvent>> + Unpin, { loop { if self.editor.should_close() { @@ -659,7 +671,7 @@ impl Application { false } - pub async fn handle_terminal_events(&mut self, event: std::io::Result<termina::Event>) { + pub async fn handle_terminal_events(&mut self, event: std::io::Result<TerminalEvent>) { let mut cx = crate::compositor::Context { editor: &mut self.editor, jobs: &mut self.jobs, @@ -667,6 +679,7 @@ impl Application { }; // Handle key events let should_redraw = match event.unwrap() { + #[cfg(not(windows))] termina::Event::WindowResized(termina::WindowSize { rows, cols, .. }) => { self.terminal .resize(Rect::new(0, 0, cols, rows)) @@ -679,11 +692,31 @@ impl Application { self.compositor .handle_event(&Event::Resize(cols, rows), &mut cx) } + #[cfg(not(windows))] // Ignore keyboard release events. termina::Event::Key(termina::event::KeyEvent { kind: termina::event::KeyEventKind::Release, .. }) => false, + #[cfg(windows)] + TerminalEvent::Resize(width, height) => { + self.terminal + .resize(Rect::new(0, 0, width, height)) + .expect("Unable to resize terminal"); + + let area = self.terminal.size().expect("couldn't get terminal size"); + + self.compositor.resize(area); + + self.compositor + .handle_event(&Event::Resize(width, height), &mut cx) + } + #[cfg(windows)] + // Ignore keyboard release events. + crossterm::event::Event::Key(crossterm::event::KeyEvent { + kind: crossterm::event::KeyEventKind::Release, + .. + }) => false, event => self.compositor.handle_event(&event.into(), &mut cx), }; @@ -1132,15 +1165,20 @@ impl Application { self.terminal.restore() } - #[cfg(not(feature = "integration"))] - pub fn event_stream(&self) -> impl Stream<Item = std::io::Result<termina::Event>> + Unpin { + #[cfg(all(not(feature = "integration"), not(windows)))] + pub fn event_stream(&self) -> impl Stream<Item = std::io::Result<TerminalEvent>> + Unpin { use termina::Terminal as _; let reader = self.terminal.backend().terminal().event_reader(); termina::EventStream::new(reader, |event| !event.is_escape()) } + #[cfg(all(not(feature = "integration"), windows))] + pub fn event_stream(&self) -> impl Stream<Item = std::io::Result<TerminalEvent>> + Unpin { + crossterm::event::EventStream::new() + } + #[cfg(feature = "integration")] - pub fn event_stream(&self) -> impl Stream<Item = std::io::Result<termina::Event>> + Unpin { + pub fn event_stream(&self) -> impl Stream<Item = std::io::Result<TerminalEvent>> + Unpin { use std::{ pin::Pin, task::{Context, Poll}, @@ -1150,7 +1188,7 @@ impl Application { pub struct DummyEventStream; impl Stream for DummyEventStream { - type Item = std::io::Result<termina::Event>; + type Item = std::io::Result<TerminalEvent>; fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { Poll::Pending @@ -1162,7 +1200,7 @@ impl Application { pub async fn run<S>(&mut self, input_stream: &mut S) -> Result<i32, Error> where - S: Stream<Item = std::io::Result<termina::Event>> + Unpin, + S: Stream<Item = std::io::Result<TerminalEvent>> + Unpin, { self.terminal.claim()?; diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs index 60143aaa..567422c3 100644 --- a/helix-term/tests/test/helpers.rs +++ b/helix-term/tests/test/helpers.rs @@ -10,9 +10,13 @@ use helix_core::{diagnostic::Severity, test, Selection, Transaction}; use helix_term::{application::Application, args::Args, config::Config, keymap::merge_keys}; use helix_view::{current_ref, doc, editor::LspConfig, input::parse_macro, Editor}; use tempfile::NamedTempFile; -use termina::event::{Event, KeyEvent}; use tokio_stream::wrappers::UnboundedReceiverStream; +#[cfg(windows)] +use crossterm::event::{Event, KeyEvent}; +#[cfg(not(windows))] +use termina::event::{Event, KeyEvent}; + /// Specify how to set up the input text with line feeds #[derive(Clone, Debug)] pub enum LineFeedHandling { diff --git a/helix-tui/Cargo.toml b/helix-tui/Cargo.toml index c0820dd7..bcf890f5 100644 --- a/helix-tui/Cargo.toml +++ b/helix-tui/Cargo.toml @@ -12,7 +12,7 @@ repository.workspace = true homepage.workspace = true [features] -default = ["termina"] +default = ["termina", "crossterm"] [dependencies] helix-view = { path = "../helix-view", features = ["term"] } @@ -25,3 +25,6 @@ termina = { workspace = true, optional = true } termini = "1.0" once_cell = "1.21" log = "~0.4" + +[target.'cfg(windows)'.dependencies] +crossterm = { version = "0.28", optional = true } diff --git a/helix-tui/src/backend/crossterm.rs b/helix-tui/src/backend/crossterm.rs new file mode 100644 index 00000000..943821b9 --- /dev/null +++ b/helix-tui/src/backend/crossterm.rs @@ -0,0 +1,450 @@ +use crate::{backend::Backend, buffer::Cell, terminal::Config}; +use crossterm::{ + cursor::{Hide, MoveTo, SetCursorStyle, Show}, + event::{ + DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, + EnableFocusChange, EnableMouseCapture, KeyboardEnhancementFlags, + PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, + }, + execute, queue, + style::{ + Attribute as CAttribute, Color as CColor, Colors, Print, SetAttribute, SetBackgroundColor, + SetColors, 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), + } +} +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)] +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 { + match termini::TermInfo::from_env() { + Err(_) => Capabilities { + has_extended_underlines: config.force_enable_extended_underlines, + ..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() + || 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); + CrosstermBackend { + buffer, + capabilities: Capabilities::from_env_or_default(&config), + config, + supports_keyboard_enhancement_protocol: OnceCell::new(), + mouse_capture_enabled: false, + supports_bracketed_paste: true, + } + } + + #[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> +where + W: Write, +{ + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + self.buffer.write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.buffer.flush() + } +} + +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)>, + { + 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) { + 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 || cell.bg != bg { + queue!( + self.buffer, + SetColors(Colors::new(cell.fg.into(), cell.bg.into())) + )?; + fg = cell.fg; + bg = cell.bg; + } + + let mut new_underline_style = cell.underline_style; + if self.capabilities.has_extended_underlines { + if cell.underline_color != underline_color { + let color = CColor::from(cell.underline_color); + queue!(self.buffer, SetUnderlineColor(color))?; + 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 { + let attr = CAttribute::from(new_underline_style); + queue!(self.buffer, SetAttribute(attr))?; + underline_style = new_underline_style; + } + + queue!(self.buffer, Print(&cell.symbol))?; + } + + 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) + } + + 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::Hidden => unreachable!(), + }; + execute!(self.buffer, Show, shape) + } + + fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> { + execute!(self.buffer, MoveTo(x, y)) + } + + fn clear(&mut self) -> io::Result<()> { + execute!(self.buffer, Clear(ClearType::All)) + } + + fn size(&self) -> io::Result<Rect> { + 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 supports_true_color(&self) -> bool { + false + } +} + +#[derive(Debug)] +struct ModifierDiff { + pub from: Modifier, + pub to: Modifier, +} + +impl ModifierDiff { + fn queue<W>(&self, mut w: W) -> io::Result<()> + where + W: io::Write, + { + //use crossterm::Attribute; + let removed = self.from - self.to; + if removed.contains(Modifier::REVERSED) { + queue!(w, SetAttribute(CAttribute::NoReverse))?; + } + if removed.contains(Modifier::BOLD) { + queue!(w, SetAttribute(CAttribute::NormalIntensity))?; + if self.to.contains(Modifier::DIM) { + queue!(w, SetAttribute(CAttribute::Dim))?; + } + } + if removed.contains(Modifier::ITALIC) { + queue!(w, SetAttribute(CAttribute::NoItalic))?; + } + if removed.contains(Modifier::DIM) { + queue!(w, SetAttribute(CAttribute::NormalIntensity))?; + } + if removed.contains(Modifier::CROSSED_OUT) { + 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))?; + } + + let added = self.to - self.from; + if added.contains(Modifier::REVERSED) { + queue!(w, SetAttribute(CAttribute::Reverse))?; + } + if added.contains(Modifier::BOLD) { + queue!(w, SetAttribute(CAttribute::Bold))?; + } + if added.contains(Modifier::ITALIC) { + queue!(w, SetAttribute(CAttribute::Italic))?; + } + if added.contains(Modifier::DIM) { + queue!(w, SetAttribute(CAttribute::Dim))?; + } + if added.contains(Modifier::CROSSED_OUT) { + queue!(w, SetAttribute(CAttribute::CrossedOut))?; + } + if added.contains(Modifier::SLOW_BLINK) { + 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))?; + } + + 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. +/// 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. +/// See [this nvim issue](https://github.com/neovim/neovim/issues/9270) for details +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SetUnderlineColor(pub CColor); + +impl Command for SetUnderlineColor { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + let color = self.0; + + if color == CColor::Reset { + write!(f, "\x1b[59m")?; + return Ok(()); + } + f.write_str("\x1b[58:")?; + + let res = match color { + CColor::Black => f.write_str("5:0"), + CColor::DarkGrey => f.write_str("5:8"), + CColor::Red => f.write_str("5:9"), + CColor::DarkRed => f.write_str("5:1"), + CColor::Green => f.write_str("5:10"), + CColor::DarkGreen => f.write_str("5:2"), + CColor::Yellow => f.write_str("5:11"), + CColor::DarkYellow => f.write_str("5:3"), + CColor::Blue => f.write_str("5:12"), + CColor::DarkBlue => f.write_str("5:4"), + CColor::Magenta => f.write_str("5:13"), + CColor::DarkMagenta => f.write_str("5:5"), + CColor::Cyan => f.write_str("5:14"), + CColor::DarkCyan => f.write_str("5:6"), + CColor::White => f.write_str("5:15"), + CColor::Grey => f.write_str("5:7"), + CColor::Rgb { r, g, b } => write!(f, "2::{}:{}:{}", r, g, b), + CColor::AnsiValue(val) => write!(f, "5:{}", val), + _ => Ok(()), + }; + res?; + write!(f, "m")?; + Ok(()) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> io::Result<()> { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "SetUnderlineColor not supported by winapi.", + )) + } +} diff --git a/helix-tui/src/backend/mod.rs b/helix-tui/src/backend/mod.rs index 3f0ec355..37160d4f 100644 --- a/helix-tui/src/backend/mod.rs +++ b/helix-tui/src/backend/mod.rs @@ -6,11 +6,16 @@ use crate::{buffer::Cell, terminal::Config}; use helix_view::graphics::{CursorKind, Rect}; -#[cfg(feature = "termina")] +#[cfg(all(feature = "termina", not(windows)))] mod termina; -#[cfg(feature = "termina")] +#[cfg(all(feature = "termina", not(windows)))] pub use self::termina::TerminaBackend; +#[cfg(all(feature = "termina", windows))] +mod crossterm; +#[cfg(all(feature = "termina", windows))] +pub use self::crossterm::CrosstermBackend; + mod test; pub use self::test::TestBackend; diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index cc2905b0..24dd0f2a 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -12,7 +12,7 @@ homepage.workspace = true [features] default = [] -term = ["termina"] +term = ["termina", "crossterm"] unicode-lines = [] [dependencies] @@ -56,6 +56,7 @@ kstring = "2.0" [target.'cfg(windows)'.dependencies] clipboard-win = { version = "5.4", features = ["std"] } +crossterm = { version = "0.28", optional = true } [target.'cfg(unix)'.dependencies] libc = "0.2" diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs index 7625e8b5..3a4eee3d 100644 --- a/helix-view/src/graphics.rs +++ b/helix-view/src/graphics.rs @@ -315,6 +315,34 @@ impl From<Color> for termina::style::ColorSpec { } } +#[cfg(all(feature = "term", windows))] +impl From<Color> for crossterm::style::Color { + fn from(color: Color) -> Self { + use crossterm::style::Color as CColor; + + match color { + Color::Reset => CColor::Reset, + Color::Black => CColor::Black, + Color::Red => CColor::DarkRed, + Color::Green => CColor::DarkGreen, + Color::Yellow => CColor::DarkYellow, + Color::Blue => CColor::DarkBlue, + Color::Magenta => CColor::DarkMagenta, + Color::Cyan => CColor::DarkCyan, + Color::Gray => CColor::DarkGrey, + Color::LightRed => CColor::Red, + Color::LightGreen => CColor::Green, + Color::LightBlue => CColor::Blue, + Color::LightYellow => CColor::Yellow, + Color::LightMagenta => CColor::Magenta, + Color::LightCyan => CColor::Cyan, + Color::LightGray => CColor::Grey, + Color::White => CColor::White, + Color::Indexed(i) => CColor::AnsiValue(i), + Color::Rgb(r, g, b) => CColor::Rgb { r, g, b }, + } + } +} #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum UnderlineStyle { Reset, @@ -354,6 +382,20 @@ impl From<UnderlineStyle> for termina::style::Underline { } } +#[cfg(all(feature = "term", windows))] +impl From<UnderlineStyle> for crossterm::style::Attribute { + fn from(style: UnderlineStyle) -> Self { + match style { + UnderlineStyle::Line => crossterm::style::Attribute::Underlined, + UnderlineStyle::Curl => crossterm::style::Attribute::Undercurled, + UnderlineStyle::Dotted => crossterm::style::Attribute::Underdotted, + UnderlineStyle::Dashed => crossterm::style::Attribute::Underdashed, + UnderlineStyle::DoubleLine => crossterm::style::Attribute::DoubleUnderlined, + UnderlineStyle::Reset => crossterm::style::Attribute::NoUnderline, + } + } +} + bitflags! { /// Modifier changes the way a piece of text is displayed. /// diff --git a/helix-view/src/input.rs b/helix-view/src/input.rs index 6b3a9756..539680a6 100644 --- a/helix-view/src/input.rs +++ b/helix-view/src/input.rs @@ -569,6 +569,113 @@ impl From<KeyEvent> for termina::event::KeyEvent { } } +#[cfg(all(feature = "term", windows))] +impl From<crossterm::event::Event> for Event { + fn from(event: crossterm::event::Event) -> Self { + match event { + crossterm::event::Event::Key(key) => Self::Key(key.into()), + crossterm::event::Event::Mouse(mouse) => Self::Mouse(mouse.into()), + crossterm::event::Event::Resize(w, h) => Self::Resize(w, h), + crossterm::event::Event::FocusGained => Self::FocusGained, + crossterm::event::Event::FocusLost => Self::FocusLost, + crossterm::event::Event::Paste(s) => Self::Paste(s), + } + } +} + +#[cfg(all(feature = "term", windows))] +impl From<crossterm::event::MouseEvent> for MouseEvent { + fn from( + crossterm::event::MouseEvent { + kind, + column, + row, + modifiers, + }: crossterm::event::MouseEvent, + ) -> Self { + Self { + kind: kind.into(), + column, + row, + modifiers: modifiers.into(), + } + } +} + +#[cfg(all(feature = "term", windows))] +impl From<crossterm::event::MouseEventKind> for MouseEventKind { + fn from(kind: crossterm::event::MouseEventKind) -> Self { + match kind { + crossterm::event::MouseEventKind::Down(button) => Self::Down(button.into()), + crossterm::event::MouseEventKind::Up(button) => Self::Up(button.into()), + crossterm::event::MouseEventKind::Drag(button) => Self::Drag(button.into()), + crossterm::event::MouseEventKind::Moved => Self::Moved, + crossterm::event::MouseEventKind::ScrollDown => Self::ScrollDown, + crossterm::event::MouseEventKind::ScrollUp => Self::ScrollUp, + crossterm::event::MouseEventKind::ScrollLeft => Self::ScrollLeft, + crossterm::event::MouseEventKind::ScrollRight => Self::ScrollRight, + } + } +} + +#[cfg(all(feature = "term", windows))] +impl From<crossterm::event::MouseButton> for MouseButton { + fn from(button: crossterm::event::MouseButton) -> Self { + match button { + crossterm::event::MouseButton::Left => MouseButton::Left, + crossterm::event::MouseButton::Right => MouseButton::Right, + crossterm::event::MouseButton::Middle => MouseButton::Middle, + } + } +} + +#[cfg(all(feature = "term", windows))] +impl From<crossterm::event::KeyEvent> for KeyEvent { + fn from( + crossterm::event::KeyEvent { + code, modifiers, .. + }: crossterm::event::KeyEvent, + ) -> Self { + if code == crossterm::event::KeyCode::BackTab { + // special case for BackTab -> Shift-Tab + let mut modifiers: KeyModifiers = modifiers.into(); + modifiers.insert(KeyModifiers::SHIFT); + Self { + code: KeyCode::Tab, + modifiers, + } + } else { + Self { + code: code.into(), + modifiers: modifiers.into(), + } + } + } +} + +#[cfg(all(feature = "term", windows))] +impl From<KeyEvent> for crossterm::event::KeyEvent { + fn from(KeyEvent { code, modifiers }: KeyEvent) -> Self { + if code == KeyCode::Tab && modifiers.contains(KeyModifiers::SHIFT) { + // special case for Shift-Tab -> BackTab + let mut modifiers = modifiers; + modifiers.remove(KeyModifiers::SHIFT); + crossterm::event::KeyEvent { + code: crossterm::event::KeyCode::BackTab, + modifiers: modifiers.into(), + kind: crossterm::event::KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + } + } else { + crossterm::event::KeyEvent { + code: code.into(), + modifiers: modifiers.into(), + kind: crossterm::event::KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + } + } + } +} pub fn parse_macro(keys_str: &str) -> anyhow::Result<Vec<KeyEvent>> { use anyhow::Context; let mut keys_res: anyhow::Result<_> = Ok(Vec::new()); diff --git a/helix-view/src/keyboard.rs b/helix-view/src/keyboard.rs index c3831143..53d85889 100644 --- a/helix-view/src/keyboard.rs +++ b/helix-view/src/keyboard.rs @@ -60,6 +60,53 @@ impl From<termina::event::Modifiers> for KeyModifiers { } } +#[cfg(all(feature = "term", windows))] +impl From<KeyModifiers> for crossterm::event::KeyModifiers { + fn from(key_modifiers: KeyModifiers) -> Self { + use crossterm::event::KeyModifiers as CKeyModifiers; + + let mut result = CKeyModifiers::NONE; + + if key_modifiers.contains(KeyModifiers::SHIFT) { + result.insert(CKeyModifiers::SHIFT); + } + if key_modifiers.contains(KeyModifiers::CONTROL) { + result.insert(CKeyModifiers::CONTROL); + } + if key_modifiers.contains(KeyModifiers::ALT) { + result.insert(CKeyModifiers::ALT); + } + if key_modifiers.contains(KeyModifiers::SUPER) { + result.insert(CKeyModifiers::SUPER); + } + + result + } +} + +#[cfg(all(feature = "term", windows))] +impl From<crossterm::event::KeyModifiers> for KeyModifiers { + fn from(val: crossterm::event::KeyModifiers) -> Self { + use crossterm::event::KeyModifiers as CKeyModifiers; + + let mut result = KeyModifiers::NONE; + + if val.contains(CKeyModifiers::SHIFT) { + result.insert(KeyModifiers::SHIFT); + } + if val.contains(CKeyModifiers::CONTROL) { + result.insert(KeyModifiers::CONTROL); + } + if val.contains(CKeyModifiers::ALT) { + result.insert(KeyModifiers::ALT); + } + if val.contains(CKeyModifiers::SUPER) { + result.insert(KeyModifiers::SUPER); + } + + result + } +} /// Represents a media key (as part of [`KeyCode::Media`]). #[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Hash)] pub enum MediaKeyCode { @@ -137,6 +184,51 @@ impl From<termina::event::MediaKeyCode> for MediaKeyCode { } } +#[cfg(all(feature = "term", windows))] +impl From<MediaKeyCode> for crossterm::event::MediaKeyCode { + fn from(media_key_code: MediaKeyCode) -> Self { + use crossterm::event::MediaKeyCode as CMediaKeyCode; + + match media_key_code { + MediaKeyCode::Play => CMediaKeyCode::Play, + MediaKeyCode::Pause => CMediaKeyCode::Pause, + MediaKeyCode::PlayPause => CMediaKeyCode::PlayPause, + MediaKeyCode::Reverse => CMediaKeyCode::Reverse, + MediaKeyCode::Stop => CMediaKeyCode::Stop, + MediaKeyCode::FastForward => CMediaKeyCode::FastForward, + MediaKeyCode::Rewind => CMediaKeyCode::Rewind, + MediaKeyCode::TrackNext => CMediaKeyCode::TrackNext, + MediaKeyCode::TrackPrevious => CMediaKeyCode::TrackPrevious, + MediaKeyCode::Record => CMediaKeyCode::Record, + MediaKeyCode::LowerVolume => CMediaKeyCode::LowerVolume, + MediaKeyCode::RaiseVolume => CMediaKeyCode::RaiseVolume, + MediaKeyCode::MuteVolume => CMediaKeyCode::MuteVolume, + } + } +} + +#[cfg(all(feature = "term", windows))] +impl From<crossterm::event::MediaKeyCode> for MediaKeyCode { + fn from(val: crossterm::event::MediaKeyCode) -> Self { + use crossterm::event::MediaKeyCode as CMediaKeyCode; + + match val { + CMediaKeyCode::Play => MediaKeyCode::Play, + CMediaKeyCode::Pause => MediaKeyCode::Pause, + CMediaKeyCode::PlayPause => MediaKeyCode::PlayPause, + CMediaKeyCode::Reverse => MediaKeyCode::Reverse, + CMediaKeyCode::Stop => MediaKeyCode::Stop, + CMediaKeyCode::FastForward => MediaKeyCode::FastForward, + CMediaKeyCode::Rewind => MediaKeyCode::Rewind, + CMediaKeyCode::TrackNext => MediaKeyCode::TrackNext, + CMediaKeyCode::TrackPrevious => MediaKeyCode::TrackPrevious, + CMediaKeyCode::Record => MediaKeyCode::Record, + CMediaKeyCode::LowerVolume => MediaKeyCode::LowerVolume, + CMediaKeyCode::RaiseVolume => MediaKeyCode::RaiseVolume, + CMediaKeyCode::MuteVolume => MediaKeyCode::MuteVolume, + } + } +} /// Represents a media key (as part of [`KeyCode::Modifier`]). #[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Hash)] pub enum ModifierKeyCode { @@ -218,6 +310,53 @@ impl From<termina::event::ModifierKeyCode> for ModifierKeyCode { } } +#[cfg(all(feature = "term", windows))] +impl From<ModifierKeyCode> for crossterm::event::ModifierKeyCode { + fn from(modifier_key_code: ModifierKeyCode) -> Self { + use crossterm::event::ModifierKeyCode as CModifierKeyCode; + + match modifier_key_code { + ModifierKeyCode::LeftShift => CModifierKeyCode::LeftShift, + ModifierKeyCode::LeftControl => CModifierKeyCode::LeftControl, + ModifierKeyCode::LeftAlt => CModifierKeyCode::LeftAlt, + ModifierKeyCode::LeftSuper => CModifierKeyCode::LeftSuper, + ModifierKeyCode::LeftHyper => CModifierKeyCode::LeftHyper, + ModifierKeyCode::LeftMeta => CModifierKeyCode::LeftMeta, + ModifierKeyCode::RightShift => CModifierKeyCode::RightShift, + ModifierKeyCode::RightControl => CModifierKeyCode::RightControl, + ModifierKeyCode::RightAlt => CModifierKeyCode::RightAlt, + ModifierKeyCode::RightSuper => CModifierKeyCode::RightSuper, + ModifierKeyCode::RightHyper => CModifierKeyCode::RightHyper, + ModifierKeyCode::RightMeta => CModifierKeyCode::RightMeta, + ModifierKeyCode::IsoLevel3Shift => CModifierKeyCode::IsoLevel3Shift, + ModifierKeyCode::IsoLevel5Shift => CModifierKeyCode::IsoLevel5Shift, + } + } +} + +#[cfg(all(feature = "term", windows))] +impl From<crossterm::event::ModifierKeyCode> for ModifierKeyCode { + fn from(val: crossterm::event::ModifierKeyCode) -> Self { + use crossterm::event::ModifierKeyCode as CModifierKeyCode; + + match val { + CModifierKeyCode::LeftShift => ModifierKeyCode::LeftShift, + CModifierKeyCode::LeftControl => ModifierKeyCode::LeftControl, + CModifierKeyCode::LeftAlt => ModifierKeyCode::LeftAlt, + CModifierKeyCode::LeftSuper => ModifierKeyCode::LeftSuper, + CModifierKeyCode::LeftHyper => ModifierKeyCode::LeftHyper, + CModifierKeyCode::LeftMeta => ModifierKeyCode::LeftMeta, + CModifierKeyCode::RightShift => ModifierKeyCode::RightShift, + CModifierKeyCode::RightControl => ModifierKeyCode::RightControl, + CModifierKeyCode::RightAlt => ModifierKeyCode::RightAlt, + CModifierKeyCode::RightSuper => ModifierKeyCode::RightSuper, + CModifierKeyCode::RightHyper => ModifierKeyCode::RightHyper, + CModifierKeyCode::RightMeta => ModifierKeyCode::RightMeta, + CModifierKeyCode::IsoLevel3Shift => ModifierKeyCode::IsoLevel3Shift, + CModifierKeyCode::IsoLevel5Shift => ModifierKeyCode::IsoLevel5Shift, + } + } +} /// Represents a key. #[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Hash)] pub enum KeyCode { @@ -351,3 +490,76 @@ impl From<termina::event::KeyCode> for KeyCode { } } } + +#[cfg(all(feature = "term", windows))] +impl From<KeyCode> for crossterm::event::KeyCode { + fn from(key_code: KeyCode) -> Self { + use crossterm::event::KeyCode as CKeyCode; + + match key_code { + KeyCode::Backspace => CKeyCode::Backspace, + KeyCode::Enter => CKeyCode::Enter, + KeyCode::Left => CKeyCode::Left, + KeyCode::Right => CKeyCode::Right, + KeyCode::Up => CKeyCode::Up, + KeyCode::Down => CKeyCode::Down, + KeyCode::Home => CKeyCode::Home, + KeyCode::End => CKeyCode::End, + KeyCode::PageUp => CKeyCode::PageUp, + KeyCode::PageDown => CKeyCode::PageDown, + KeyCode::Tab => CKeyCode::Tab, + KeyCode::Delete => CKeyCode::Delete, + KeyCode::Insert => CKeyCode::Insert, + KeyCode::F(f_number) => CKeyCode::F(f_number), + KeyCode::Char(character) => CKeyCode::Char(character), + KeyCode::Null => CKeyCode::Null, + KeyCode::Esc => CKeyCode::Esc, + KeyCode::CapsLock => CKeyCode::CapsLock, + KeyCode::ScrollLock => CKeyCode::ScrollLock, + KeyCode::NumLock => CKeyCode::NumLock, + KeyCode::PrintScreen => CKeyCode::PrintScreen, + KeyCode::Pause => CKeyCode::Pause, + KeyCode::Menu => CKeyCode::Menu, + KeyCode::KeypadBegin => CKeyCode::KeypadBegin, + KeyCode::Media(media_key_code) => CKeyCode::Media(media_key_code.into()), + KeyCode::Modifier(modifier_key_code) => CKeyCode::Modifier(modifier_key_code.into()), + } + } +} + +#[cfg(all(feature = "term", windows))] +impl From<crossterm::event::KeyCode> for KeyCode { + fn from(val: crossterm::event::KeyCode) -> Self { + use crossterm::event::KeyCode as CKeyCode; + + match val { + CKeyCode::Backspace => KeyCode::Backspace, + CKeyCode::Enter => KeyCode::Enter, + CKeyCode::Left => KeyCode::Left, + CKeyCode::Right => KeyCode::Right, + CKeyCode::Up => KeyCode::Up, + CKeyCode::Down => KeyCode::Down, + CKeyCode::Home => KeyCode::Home, + CKeyCode::End => KeyCode::End, + CKeyCode::PageUp => KeyCode::PageUp, + CKeyCode::PageDown => KeyCode::PageDown, + CKeyCode::Tab => KeyCode::Tab, + CKeyCode::BackTab => unreachable!("BackTab should have been handled on KeyEvent level"), + CKeyCode::Delete => KeyCode::Delete, + CKeyCode::Insert => KeyCode::Insert, + CKeyCode::F(f_number) => KeyCode::F(f_number), + CKeyCode::Char(character) => KeyCode::Char(character), + CKeyCode::Null => KeyCode::Null, + CKeyCode::Esc => KeyCode::Esc, + CKeyCode::CapsLock => KeyCode::CapsLock, + CKeyCode::ScrollLock => KeyCode::ScrollLock, + CKeyCode::NumLock => KeyCode::NumLock, + CKeyCode::PrintScreen => KeyCode::PrintScreen, + CKeyCode::Pause => KeyCode::Pause, + CKeyCode::Menu => KeyCode::Menu, + CKeyCode::KeypadBegin => KeyCode::KeypadBegin, + CKeyCode::Media(media_key_code) => KeyCode::Media(media_key_code.into()), + CKeyCode::Modifier(modifier_key_code) => KeyCode::Modifier(modifier_key_code.into()), + } + } +} |