tui: Use Crossterm on Windows

This commit is contained in:
Michael Davis
2025-09-07 14:05:47 -04:00
parent d0218f7e78
commit 8e5455b28f
11 changed files with 972 additions and 25 deletions

96
Cargo.lock generated
View File

@@ -248,6 +248,32 @@ version = "0.8.20"
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"
@@ -874,7 +900,7 @@ dependencies = [
"itoa",
"libc",
"memmap2",
"rustix",
"rustix 1.0.8",
"smallvec",
"thiserror",
]
@@ -1500,7 +1526,7 @@ dependencies = [
"regex-automata",
"regex-cursor",
"ropey",
"rustix",
"rustix 1.0.8",
"tempfile",
"unicode-segmentation",
"which",
@@ -1515,6 +1541,7 @@ dependencies = [
"arc-swap",
"chrono",
"content_inspector",
"crossterm",
"dashmap",
"fern",
"futures-util",
@@ -1560,6 +1587,7 @@ version = "25.7.1"
dependencies = [
"bitflags",
"cassowary",
"crossterm",
"helix-core",
"helix-view",
"log",
@@ -1594,6 +1622,7 @@ dependencies = [
"bitflags",
"chardetng",
"clipboard-win",
"crossterm",
"futures-util",
"helix-core",
"helix-dap",
@@ -1608,7 +1637,7 @@ dependencies = [
"log",
"once_cell",
"parking_lot",
"rustix",
"rustix 1.0.8",
"serde",
"serde_json",
"slotmap",
@@ -1980,6 +2009,12 @@ dependencies = [
"zlib-rs",
]
[[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.9.2"
@@ -2060,6 +2095,7 @@ checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
dependencies = [
"hermit-abi",
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.52.0",
]
@@ -2344,6 +2380,19 @@ version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
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.0.8"
@@ -2353,7 +2402,7 @@ dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"linux-raw-sys 0.9.2",
"windows-sys 0.60.2",
]
@@ -2462,6 +2511,17 @@ dependencies = [
"signal-hook-registry",
]
[[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"
@@ -2583,7 +2643,7 @@ dependencies = [
"fastrand",
"getrandom 0.3.1",
"once_cell",
"rustix",
"rustix 1.0.8",
"windows-sys 0.60.2",
]
@@ -2596,7 +2656,7 @@ dependencies = [
"bitflags",
"futures-core",
"parking_lot",
"rustix",
"rustix 1.0.8",
"signal-hook",
"windows-sys 0.60.2",
]
@@ -2969,10 +3029,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d"
dependencies = [
"env_home",
"rustix",
"rustix 1.0.8",
"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"
@@ -2982,6 +3058,12 @@ dependencies = [
"windows-sys 0.59.0",
]
[[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"

View File

@@ -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"

View File

@@ -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()?;

View File

@@ -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 {

View File

@@ -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 }

View File

@@ -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.",
))
}
}

View File

@@ -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;

View File

@@ -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"

View File

@@ -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.
///

View File

@@ -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());

View File

@@ -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()),
}
}
}