Unnamed repository; edit this file 'description' to name the repository.
tui: Use Crossterm on Windows (#14454)
Michael Davis 5 months ago
parent 0ae37dc · commit c2b582a
-rw-r--r--Cargo.lock96
-rw-r--r--helix-term/Cargo.toml5
-rw-r--r--helix-term/src/application.rs62
-rw-r--r--helix-term/tests/test/helpers.rs6
-rw-r--r--helix-tui/Cargo.toml5
-rw-r--r--helix-tui/src/backend/crossterm.rs450
-rw-r--r--helix-tui/src/backend/mod.rs9
-rw-r--r--helix-view/Cargo.toml3
-rw-r--r--helix-view/src/graphics.rs42
-rw-r--r--helix-view/src/input.rs107
-rw-r--r--helix-view/src/keyboard.rs212
11 files changed, 972 insertions, 25 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 2033e88c..e33bc7d3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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()),
+ }
+ }
+}