Unnamed repository; edit this file 'description' to name the repository.
-rw-r--r--Cargo.lock232
-rw-r--r--Cargo.toml1
-rw-r--r--helix-term/Cargo.toml7
-rw-r--r--helix-term/src/application.rs94
-rw-r--r--helix-term/src/health.rs38
-rw-r--r--helix-term/src/main.rs4
-rw-r--r--helix-term/src/ui/editor.rs6
-rw-r--r--helix-term/tests/test/helpers.rs2
-rw-r--r--helix-tui/Cargo.toml4
-rw-r--r--helix-tui/src/backend/crossterm.rs465
-rw-r--r--helix-tui/src/backend/mod.rs11
-rw-r--r--helix-tui/src/backend/termina.rs645
-rw-r--r--helix-tui/src/backend/test.rs8
-rw-r--r--helix-tui/src/lib.rs130
-rw-r--r--helix-view/Cargo.toml4
-rw-r--r--helix-view/src/base64.rs163
-rw-r--r--helix-view/src/clipboard.rs52
-rw-r--r--helix-view/src/graphics.rs56
-rw-r--r--helix-view/src/input.rs83
-rw-r--r--helix-view/src/keyboard.rs48
-rw-r--r--helix-view/src/lib.rs1
21 files changed, 921 insertions, 1133 deletions
diff --git a/Cargo.lock b/Cargo.lock
index e6cfab9b..51472863 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -248,34 +248,6 @@ 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",
- "filedescriptor",
- "futures-core",
- "libc",
- "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"
@@ -415,17 +387,6 @@ dependencies = [
]
[[package]]
-name = "filedescriptor"
-version = "0.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e"
-dependencies = [
- "libc",
- "thiserror 1.0.69",
- "winapi",
-]
-
-[[package]]
name = "filetime"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -591,7 +552,7 @@ dependencies = [
"gix-worktree",
"once_cell",
"smallvec",
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -604,7 +565,7 @@ dependencies = [
"gix-date",
"gix-utils",
"itoa",
- "thiserror 2.0.16",
+ "thiserror",
"winnow",
]
@@ -621,7 +582,7 @@ dependencies = [
"gix-trace",
"kstring",
"smallvec",
- "thiserror 2.0.16",
+ "thiserror",
"unicode-bom",
]
@@ -631,7 +592,7 @@ version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1db9765c69502650da68f0804e3dc2b5f8ccc6a2d104ca6c85bc40700d37540"
dependencies = [
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -640,7 +601,7 @@ version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b1f1d8764958699dc764e3f727cef280ff4d1bd92c107bbf8acd85b30c1bd6f"
dependencies = [
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -666,7 +627,7 @@ dependencies = [
"gix-chunk",
"gix-hash",
"memmap2",
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -685,7 +646,7 @@ dependencies = [
"memchr",
"once_cell",
"smallvec",
- "thiserror 2.0.16",
+ "thiserror",
"unicode-bom",
"winnow",
]
@@ -700,7 +661,7 @@ dependencies = [
"bstr",
"gix-path",
"libc",
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -713,7 +674,7 @@ dependencies = [
"itoa",
"jiff",
"smallvec",
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -737,7 +698,7 @@ dependencies = [
"gix-traverse",
"gix-worktree",
"imara-diff 0.1.8",
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -757,7 +718,7 @@ dependencies = [
"gix-trace",
"gix-utils",
"gix-worktree",
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -773,7 +734,7 @@ dependencies = [
"gix-path",
"gix-ref",
"gix-sec",
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -790,7 +751,7 @@ dependencies = [
"libc",
"once_cell",
"prodash",
- "thiserror 2.0.16",
+ "thiserror",
"walkdir",
]
@@ -812,7 +773,7 @@ dependencies = [
"gix-trace",
"gix-utils",
"smallvec",
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -826,7 +787,7 @@ dependencies = [
"gix-features",
"gix-path",
"gix-utils",
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -850,7 +811,7 @@ dependencies = [
"faster-hex",
"gix-features",
"sha1-checked",
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -900,9 +861,9 @@ dependencies = [
"itoa",
"libc",
"memmap2",
- "rustix 1.0.8",
+ "rustix",
"smallvec",
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -913,7 +874,7 @@ checksum = "b9fa71da90365668a621e184eb5b979904471af1b3b09b943a84bc50e8ad42ed"
dependencies = [
"gix-tempfile",
"gix-utils",
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -933,7 +894,7 @@ dependencies = [
"gix-validate",
"itoa",
"smallvec",
- "thiserror 2.0.16",
+ "thiserror",
"winnow",
]
@@ -955,7 +916,7 @@ dependencies = [
"gix-quote",
"parking_lot",
"tempfile",
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -973,7 +934,7 @@ dependencies = [
"gix-path",
"memmap2",
"smallvec",
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -985,7 +946,7 @@ dependencies = [
"bstr",
"faster-hex",
"gix-trace",
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -997,7 +958,7 @@ dependencies = [
"bstr",
"faster-hex",
"gix-trace",
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -1011,7 +972,7 @@ dependencies = [
"gix-validate",
"home",
"once_cell",
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -1026,7 +987,7 @@ dependencies = [
"gix-config-value",
"gix-glob",
"gix-path",
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -1044,7 +1005,7 @@ dependencies = [
"gix-transport",
"gix-utils",
"maybe-async",
- "thiserror 2.0.16",
+ "thiserror",
"winnow",
]
@@ -1056,7 +1017,7 @@ checksum = "4a375a75b4d663e8bafe3bf4940a18a23755644c13582fa326e99f8f987d83fd"
dependencies = [
"bstr",
"gix-utils",
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -1076,7 +1037,7 @@ dependencies = [
"gix-utils",
"gix-validate",
"memmap2",
- "thiserror 2.0.16",
+ "thiserror",
"winnow",
]
@@ -1091,7 +1052,7 @@ dependencies = [
"gix-revision",
"gix-validate",
"smallvec",
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -1106,7 +1067,7 @@ dependencies = [
"gix-hash",
"gix-object",
"gix-revwalk",
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -1121,7 +1082,7 @@ dependencies = [
"gix-hashtable",
"gix-object",
"smallvec",
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -1145,7 +1106,7 @@ dependencies = [
"bstr",
"gix-hash",
"gix-lock",
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -1168,7 +1129,7 @@ dependencies = [
"gix-pathspec",
"gix-worktree",
"portable-atomic",
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -1183,7 +1144,7 @@ dependencies = [
"gix-pathspec",
"gix-refspec",
"gix-url",
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -1219,7 +1180,7 @@ dependencies = [
"gix-quote",
"gix-sec",
"gix-url",
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -1236,7 +1197,7 @@ dependencies = [
"gix-object",
"gix-revwalk",
"smallvec",
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -1249,7 +1210,7 @@ dependencies = [
"gix-features",
"gix-path",
"percent-encoding",
- "thiserror 2.0.16",
+ "thiserror",
"url",
]
@@ -1271,7 +1232,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77b9e00cacde5b51388d28ed746c493b18a6add1f19b5e01d686b3b9ece66d4d"
dependencies = [
"bstr",
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -1431,7 +1392,7 @@ dependencies = [
"serde",
"serde_json",
"slotmap",
- "thiserror 2.0.16",
+ "thiserror",
"tokio",
"tokio-stream",
]
@@ -1485,7 +1446,7 @@ dependencies = [
"serde",
"serde_json",
"slotmap",
- "thiserror 2.0.16",
+ "thiserror",
"tokio",
"tokio-stream",
]
@@ -1515,7 +1476,7 @@ dependencies = [
"regex-automata",
"regex-cursor",
"ropey",
- "rustix 1.0.8",
+ "rustix",
"tempfile",
"unicode-segmentation",
"which",
@@ -1530,7 +1491,6 @@ dependencies = [
"arc-swap",
"chrono",
"content_inspector",
- "crossterm",
"dashmap",
"fern",
"futures-util",
@@ -1561,8 +1521,9 @@ dependencies = [
"signal-hook-tokio",
"smallvec",
"tempfile",
+ "termina",
"termini",
- "thiserror 2.0.16",
+ "thiserror",
"tokio",
"tokio-stream",
"toml",
@@ -1575,11 +1536,11 @@ version = "25.7.1"
dependencies = [
"bitflags",
"cassowary",
- "crossterm",
"helix-core",
"helix-view",
"log",
"once_cell",
+ "termina",
"termini",
"unicode-segmentation",
]
@@ -1609,7 +1570,6 @@ dependencies = [
"bitflags",
"chardetng",
"clipboard-win",
- "crossterm",
"futures-util",
"helix-core",
"helix-dap",
@@ -1624,12 +1584,13 @@ dependencies = [
"log",
"once_cell",
"parking_lot",
- "rustix 1.0.8",
+ "rustix",
"serde",
"serde_json",
"slotmap",
"tempfile",
- "thiserror 2.0.16",
+ "termina",
+ "thiserror",
"tokio",
"tokio-stream",
"toml",
@@ -1997,12 +1958,6 @@ dependencies = [
[[package]]
name = "linux-raw-sys"
-version = "0.4.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
-
-[[package]]
-name = "linux-raw-sys"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9"
@@ -2081,7 +2036,6 @@ checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
dependencies = [
"hermit-abi",
"libc",
- "log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.52.0",
]
@@ -2368,19 +2322,6 @@ 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.14",
- "windows-sys 0.59.0",
-]
-
-[[package]]
-name = "rustix"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
@@ -2388,7 +2329,7 @@ dependencies = [
"bitflags",
"errno",
"libc",
- "linux-raw-sys 0.9.2",
+ "linux-raw-sys",
"windows-sys 0.60.2",
]
@@ -2498,17 +2439,6 @@ 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"
@@ -2629,7 +2559,21 @@ dependencies = [
"fastrand",
"getrandom 0.3.1",
"once_cell",
- "rustix 1.0.8",
+ "rustix",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "termina"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f58aa978d5fc10a8f839e51150a41308f3bb05a6c0bf3fcc59ca079312f83d4"
+dependencies = [
+ "bitflags",
+ "futures-core",
+ "parking_lot",
+ "rustix",
+ "signal-hook",
"windows-sys 0.60.2",
]
@@ -2655,31 +2599,11 @@ dependencies = [
[[package]]
name = "thiserror"
-version = "1.0.69"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
-dependencies = [
- "thiserror-impl 1.0.69",
-]
-
-[[package]]
-name = "thiserror"
version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
dependencies = [
- "thiserror-impl 2.0.16",
-]
-
-[[package]]
-name = "thiserror-impl"
-version = "1.0.69"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "thiserror-impl",
]
[[package]]
@@ -2835,7 +2759,7 @@ dependencies = [
"libloading",
"regex-cursor",
"ropey",
- "thiserror 2.0.16",
+ "thiserror",
]
[[package]]
@@ -3021,27 +2945,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d"
dependencies = [
"env_home",
- "rustix 1.0.8",
+ "rustix",
"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"
@@ -3051,12 +2959,6 @@ 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/Cargo.toml b/Cargo.toml
index a17ed400..3b1ff307 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -51,6 +51,7 @@ futures-executor = "0.3"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
tokio-stream = "0.1.17"
toml = "0.9"
+termina = "0.1.0"
[workspace.package]
version = "25.7.1"
diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml
index bb63bb22..4585aaad 100644
--- a/helix-term/Cargo.toml
+++ b/helix-term/Cargo.toml
@@ -54,8 +54,8 @@ 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 = ["crossterm"] }
-crossterm = { version = "0.28", features = ["event-stream"] }
+tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["termina"] }
+termina = { workspace = true, features = ["event-stream"] }
signal-hook = "0.3"
tokio-stream = "0.1"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
@@ -97,9 +97,6 @@ dashmap = "6.0"
signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
libc = "0.2.175"
-[target.'cfg(target_os = "macos")'.dependencies]
-crossterm = { version = "0.28", features = ["event-stream", "use-dev-tty", "libc"] }
-
[build-dependencies]
helix-loader = { path = "../helix-loader" }
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index de661f30..8487e245 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -30,28 +30,27 @@ use crate::{
};
use log::{debug, error, info, warn};
-#[cfg(not(feature = "integration"))]
-use std::io::stdout;
-use std::{io::stdin, path::Path, sync::Arc};
+use std::{
+ io::{stdin, IsTerminal},
+ path::Path,
+ sync::Arc,
+};
-#[cfg(not(windows))]
-use anyhow::Context;
-use anyhow::Error;
+use anyhow::{Context, Error};
-use crossterm::{event::Event as CrosstermEvent, tty::IsTty};
#[cfg(not(windows))]
use {signal_hook::consts::signal, signal_hook_tokio::Signals};
#[cfg(windows)]
type Signals = futures_util::stream::Empty<()>;
#[cfg(not(feature = "integration"))]
-use tui::backend::CrosstermBackend;
+use tui::backend::TerminaBackend;
#[cfg(feature = "integration")]
use tui::backend::TestBackend;
#[cfg(not(feature = "integration"))]
-type TerminalBackend = CrosstermBackend<std::io::Stdout>;
+type TerminalBackend = TerminaBackend;
#[cfg(feature = "integration")]
type TerminalBackend = TestBackend;
@@ -104,7 +103,8 @@ impl Application {
let theme_loader = theme::Loader::new(&theme_parent_dirs);
#[cfg(not(feature = "integration"))]
- let backend = CrosstermBackend::new(stdout(), (&config.editor).into());
+ let backend = TerminaBackend::new((&config.editor).into())
+ .context("failed to create terminal backend")?;
#[cfg(feature = "integration")]
let backend = TestBackend::new(120, 150);
@@ -123,7 +123,11 @@ impl Application {
})),
handlers,
);
- Self::load_configured_theme(&mut editor, &config.load());
+ Self::load_configured_theme(
+ &mut editor,
+ &config.load(),
+ terminal.backend().supports_true_color(),
+ );
let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| {
&config.keys
@@ -214,7 +218,7 @@ impl Application {
} else {
editor.new_file(Action::VerticalSplit);
}
- } else if stdin().is_tty() || cfg!(feature = "integration") {
+ } else if stdin().is_terminal() || cfg!(feature = "integration") {
editor.new_file(Action::VerticalSplit);
} else {
editor
@@ -282,7 +286,7 @@ impl Application {
pub async fn event_loop<S>(&mut self, input_stream: &mut S)
where
- S: Stream<Item = std::io::Result<crossterm::event::Event>> + Unpin,
+ S: Stream<Item = std::io::Result<termina::Event>> + Unpin,
{
self.render().await;
@@ -295,7 +299,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<crossterm::event::Event>> + Unpin,
+ S: Stream<Item = std::io::Result<termina::Event>> + Unpin,
{
loop {
if self.editor.should_close() {
@@ -396,7 +400,11 @@ impl Application {
// the sake of locals highlighting.
let lang_loader = helix_core::config::user_lang_loader()?;
self.editor.syn_loader.store(Arc::new(lang_loader));
- Self::load_configured_theme(&mut self.editor, &default_config);
+ Self::load_configured_theme(
+ &mut self.editor,
+ &default_config,
+ self.terminal.backend().supports_true_color(),
+ );
// Re-parse any open documents with the new language config.
let lang_loader = self.editor.syn_loader.load();
@@ -429,8 +437,8 @@ impl Application {
}
/// Load the theme set in configuration
- fn load_configured_theme(editor: &mut Editor, config: &Config) {
- let true_color = config.editor.true_color || crate::true_color();
+ fn load_configured_theme(editor: &mut Editor, config: &Config, terminal_true_color: bool) {
+ let true_color = terminal_true_color || config.editor.true_color || crate::true_color();
let theme = config
.theme
.as_ref()
@@ -634,7 +642,7 @@ impl Application {
false
}
- pub async fn handle_terminal_events(&mut self, event: std::io::Result<CrosstermEvent>) {
+ pub async fn handle_terminal_events(&mut self, event: std::io::Result<termina::Event>) {
let mut cx = crate::compositor::Context {
editor: &mut self.editor,
jobs: &mut self.jobs,
@@ -642,9 +650,9 @@ impl Application {
};
// Handle key events
let should_redraw = match event.unwrap() {
- CrosstermEvent::Resize(width, height) => {
+ termina::Event::WindowResized(termina::WindowSize { rows, cols, .. }) => {
self.terminal
- .resize(Rect::new(0, 0, width, height))
+ .resize(Rect::new(0, 0, cols, rows))
.expect("Unable to resize terminal");
let area = self.terminal.size().expect("couldn't get terminal size");
@@ -652,11 +660,11 @@ impl Application {
self.compositor.resize(area);
self.compositor
- .handle_event(&Event::Resize(width, height), &mut cx)
+ .handle_event(&Event::Resize(cols, rows), &mut cx)
}
// Ignore keyboard release events.
- CrosstermEvent::Key(crossterm::event::KeyEvent {
- kind: crossterm::event::KeyEventKind::Release,
+ termina::Event::Key(termina::event::KeyEvent {
+ kind: termina::event::KeyEventKind::Release,
..
}) => false,
event => self.compositor.handle_event(&event.into(), &mut cx),
@@ -1107,22 +1115,40 @@ impl Application {
self.terminal.restore()
}
+ #[cfg(not(feature = "integration"))]
+ pub fn event_stream(&self) -> impl Stream<Item = std::io::Result<termina::Event>> + Unpin {
+ use termina::Terminal as _;
+ let reader = self.terminal.backend().terminal().event_reader();
+ termina::EventStream::new(reader, |event| !event.is_escape())
+ }
+
+ #[cfg(feature = "integration")]
+ pub fn event_stream(&self) -> impl Stream<Item = std::io::Result<termina::Event>> + Unpin {
+ use std::{
+ pin::Pin,
+ task::{Context, Poll},
+ };
+
+ /// A dummy stream that never polls as ready.
+ pub struct DummyEventStream;
+
+ impl Stream for DummyEventStream {
+ type Item = std::io::Result<termina::Event>;
+
+ fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
+ Poll::Pending
+ }
+ }
+
+ DummyEventStream
+ }
+
pub async fn run<S>(&mut self, input_stream: &mut S) -> Result<i32, Error>
where
- S: Stream<Item = std::io::Result<crossterm::event::Event>> + Unpin,
+ S: Stream<Item = std::io::Result<termina::Event>> + Unpin,
{
self.terminal.claim()?;
- // Exit the alternate screen and disable raw mode before panicking
- let hook = std::panic::take_hook();
- std::panic::set_hook(Box::new(move |info| {
- // We can't handle errors properly inside this closure. And it's
- // probably not a good idea to `unwrap()` inside a panic handler.
- // So we just ignore the `Result`.
- let _ = TerminalBackend::force_restore();
- hook(info);
- }));
-
self.event_loop(input_stream).await;
let close_errs = self.close().await;
diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs
index 78b51939..dbf25e60 100644
--- a/helix-term/src/health.rs
+++ b/helix-term/src/health.rs
@@ -1,11 +1,14 @@
use crate::config::{Config, ConfigLoadError};
-use crossterm::{
- style::{Color, StyledContent, Stylize},
- tty::IsTty,
-};
use helix_core::config::{default_lang_config, user_lang_config};
use helix_loader::grammar::load_runtime_file;
-use std::{collections::HashSet, io::Write};
+use std::{
+ collections::HashSet,
+ io::{IsTerminal, Write},
+};
+use termina::{
+ style::{ColorSpec, StyleExt as _, Stylized},
+ Terminal as _,
+};
#[derive(Copy, Clone)]
pub enum TsFeature {
@@ -183,21 +186,24 @@ fn languages(selection: Option<HashSet<String>>) -> std::io::Result<()> {
headings.push(feat.short_title())
}
- let terminal_cols = crossterm::terminal::size().map(|(c, _)| c).unwrap_or(80);
+ let terminal_cols = termina::PlatformTerminal::new()
+ .and_then(|terminal| terminal.get_dimensions())
+ .map(|size| size.cols)
+ .unwrap_or(80);
let column_width = terminal_cols as usize / headings.len();
- let is_terminal = std::io::stdout().is_tty();
+ let is_terminal = std::io::stdout().is_terminal();
- let fit = |s: &str| -> StyledContent<String> {
+ let fit = |s: &str| -> Stylized<'static> {
format!(
"{:column_width$}",
s.get(..column_width - 2)
.map(|s| format!("{}…", s))
.unwrap_or_else(|| s.to_string())
)
- .stylize()
+ .stylized()
};
- let color = |s: StyledContent<String>, c: Color| if is_terminal { s.with(c) } else { s };
- let bold = |s: StyledContent<String>| if is_terminal { s.bold() } else { s };
+ let color = |s: Stylized<'static>, c: ColorSpec| if is_terminal { s.foreground(c) } else { s };
+ let bold = |s: Stylized<'static>| if is_terminal { s.bold() } else { s };
for heading in headings {
write!(stdout, "{}", bold(fit(heading)))?;
@@ -210,10 +216,10 @@ fn languages(selection: Option<HashSet<String>>) -> std::io::Result<()> {
let check_binary_with_name = |cmd: Option<(&str, &str)>| match cmd {
Some((name, cmd)) => match helix_stdx::env::which(cmd) {
- Ok(_) => color(fit(&format!("✓ {}", name)), Color::Green),
- Err(_) => color(fit(&format!("✘ {}", name)), Color::Red),
+ Ok(_) => color(fit(&format!("✓ {}", name)), ColorSpec::BRIGHT_GREEN),
+ Err(_) => color(fit(&format!("✘ {}", name)), ColorSpec::BRIGHT_RED),
},
- None => color(fit("None"), Color::Yellow),
+ None => color(fit("None"), ColorSpec::BRIGHT_YELLOW),
};
let check_binary = |cmd: Option<&str>| check_binary_with_name(cmd.map(|cmd| (cmd, cmd)));
@@ -247,8 +253,8 @@ fn languages(selection: Option<HashSet<String>>) -> std::io::Result<()> {
for ts_feat in TsFeature::all() {
match load_runtime_file(&lang.language_id, ts_feat.runtime_filename()).is_ok() {
- true => write!(stdout, "{}", color(fit("✓"), Color::Green))?,
- false => write!(stdout, "{}", color(fit("✘"), Color::Red))?,
+ true => write!(stdout, "{}", color(fit("✓"), ColorSpec::BRIGHT_GREEN))?,
+ false => write!(stdout, "{}", color(fit("✘"), ColorSpec::BRIGHT_RED))?,
}
}
diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs
index ccbba2e9..c1404d4f 100644
--- a/helix-term/src/main.rs
+++ b/helix-term/src/main.rs
@@ -1,5 +1,4 @@
use anyhow::{Context, Error, Result};
-use crossterm::event::EventStream;
use helix_loader::VERSION_AND_GIT_HASH;
use helix_term::application::Application;
use helix_term::args::Args;
@@ -151,8 +150,9 @@ FLAGS:
// TODO: use the thread local executor to spawn the application task separately from the work pool
let mut app = Application::new(args, config, lang_loader).context("unable to start Helix")?;
+ let mut events = app.event_stream();
- let exit_code = app.run(&mut EventStream::new()).await?;
+ let exit_code = app.run(&mut events).await?;
Ok(exit_code)
}
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index d268edfa..b25af107 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -538,7 +538,7 @@ impl EditorView {
};
spans.push((selection_scope, range.anchor..selection_end));
// add block cursors
- // skip primary cursor if terminal is unfocused - crossterm cursor is used in that case
+ // skip primary cursor if terminal is unfocused - terminal cursor is used in that case
if !selection_is_primary || (cursor_is_block && is_terminal_focused) {
spans.push((cursor_scope, cursor_start..range.head));
}
@@ -546,7 +546,7 @@ impl EditorView {
// Reverse case.
let cursor_end = next_grapheme_boundary(text, range.head);
// add block cursors
- // skip primary cursor if terminal is unfocused - crossterm cursor is used in that case
+ // skip primary cursor if terminal is unfocused - terminal cursor is used in that case
if !selection_is_primary || (cursor_is_block && is_terminal_focused) {
spans.push((cursor_scope, range.head..cursor_end));
}
@@ -1631,7 +1631,7 @@ impl Component for EditorView {
if self.terminal_focused {
(pos, CursorKind::Hidden)
} else {
- // use crossterm cursor when terminal loses focus
+ // use terminal cursor when terminal loses focus
(pos, CursorKind::Underline)
}
}
diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs
index ef910852..60143aaa 100644
--- a/helix-term/tests/test/helpers.rs
+++ b/helix-term/tests/test/helpers.rs
@@ -6,11 +6,11 @@ use std::{
};
use anyhow::bail;
-use crossterm::event::{Event, KeyEvent};
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;
/// Specify how to set up the input text with line feeds
diff --git a/helix-tui/Cargo.toml b/helix-tui/Cargo.toml
index 2b5767a5..c0820dd7 100644
--- a/helix-tui/Cargo.toml
+++ b/helix-tui/Cargo.toml
@@ -12,7 +12,7 @@ repository.workspace = true
homepage.workspace = true
[features]
-default = ["crossterm"]
+default = ["termina"]
[dependencies]
helix-view = { path = "../helix-view", features = ["term"] }
@@ -21,7 +21,7 @@ helix-core = { path = "../helix-core" }
bitflags.workspace = true
cassowary = "0.3"
unicode-segmentation.workspace = true
-crossterm = { version = "0.28", optional = true }
+termina = { workspace = true, optional = true }
termini = "1.0"
once_cell = "1.21"
log = "~0.4"
diff --git a/helix-tui/src/backend/crossterm.rs b/helix-tui/src/backend/crossterm.rs
deleted file mode 100644
index d04d00af..00000000
--- a/helix-tui/src/backend/crossterm.rs
+++ /dev/null
@@ -1,465 +0,0 @@
-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 force_restore() -> io::Result<()> {
- let mut stdout = io::stdout();
-
- // reset cursor shape
- write!(stdout, "\x1B[0 q")?;
- // Ignore errors on disabling, this might trigger on windows if we call
- // disable without calling enable previously
- let _ = execute!(stdout, DisableMouseCapture);
- let _ = execute!(stdout, PopKeyboardEnhancementFlags);
- let _ = execute!(stdout, DisableBracketedPaste);
- execute!(stdout, 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 get_cursor(&mut self) -> io::Result<(u16, u16)> {
- crossterm::cursor::position()
- .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
- }
-
- fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
- execute!(self.buffer, MoveTo(x, y))
- }
-
- 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()
- }
-}
-
-#[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 1423bc0a..33596a66 100644
--- a/helix-tui/src/backend/mod.rs
+++ b/helix-tui/src/backend/mod.rs
@@ -6,10 +6,10 @@ use crate::{buffer::Cell, terminal::Config};
use helix_view::graphics::{CursorKind, Rect};
-#[cfg(feature = "crossterm")]
-mod crossterm;
-#[cfg(feature = "crossterm")]
-pub use self::crossterm::CrosstermBackend;
+#[cfg(feature = "termina")]
+mod termina;
+#[cfg(feature = "termina")]
+pub use self::termina::TerminaBackend;
mod test;
pub use self::test::TestBackend;
@@ -22,8 +22,6 @@ pub trait Backend {
fn reconfigure(&mut self, config: Config) -> Result<(), io::Error>;
/// Restores the terminal to a normal state, undoes `claim`
fn restore(&mut self) -> Result<(), io::Error>;
- /// Forcibly resets the terminal, ignoring errors and configuration
- fn force_restore() -> Result<(), io::Error>;
/// Draws styled text to the terminal
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
where
@@ -42,4 +40,5 @@ pub trait Backend {
fn size(&self) -> Result<Rect, io::Error>;
/// Flushes the terminal buffer
fn flush(&mut self) -> Result<(), io::Error>;
+ fn supports_true_color(&self) -> bool;
}
diff --git a/helix-tui/src/backend/termina.rs b/helix-tui/src/backend/termina.rs
new file mode 100644
index 00000000..c818d901
--- /dev/null
+++ b/helix-tui/src/backend/termina.rs
@@ -0,0 +1,645 @@
+use std::io::{self, Write as _};
+
+use helix_view::{
+ graphics::{CursorKind, Rect, UnderlineStyle},
+ theme::{Color, Modifier},
+};
+use termina::{
+ escape::{
+ csi::{self, Csi, SgrAttributes, SgrModifiers},
+ dcs::{self, Dcs},
+ },
+ style::{CursorStyle, RgbColor},
+ Event, OneBased, PlatformTerminal, Terminal as _, WindowSize,
+};
+use termini::TermInfo;
+
+use crate::{buffer::Cell, terminal::Config};
+
+use super::Backend;
+
+// These macros are helpers to set/unset modes like bracketed paste or enter/exit the alternate
+// screen.
+macro_rules! decset {
+ ($mode:ident) => {
+ Csi::Mode(csi::Mode::SetDecPrivateMode(csi::DecPrivateMode::Code(
+ csi::DecPrivateModeCode::$mode,
+ )))
+ };
+}
+macro_rules! decreset {
+ ($mode:ident) => {
+ Csi::Mode(csi::Mode::ResetDecPrivateMode(csi::DecPrivateMode::Code(
+ csi::DecPrivateModeCode::$mode,
+ )))
+ };
+}
+
+fn term_program() -> Option<String> {
+ // Some terminals don't set $TERM_PROGRAM
+ match std::env::var("TERM_PROGRAM") {
+ Err(_) => std::env::var("TERM").ok(),
+ Ok(term_program) => Some(term_program),
+ }
+}
+fn vte_version() -> Option<usize> {
+ std::env::var("VTE_VERSION").ok()?.parse().ok()
+}
+fn reset_cursor_approach(terminfo: TermInfo) -> String {
+ let mut reset_str = Csi::Cursor(csi::Cursor::CursorStyle(CursorStyle::Default)).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
+}
+
+#[derive(Debug, Default, Clone, Copy)]
+struct Capabilities {
+ kitty_keyboard: KittyKeyboardSupport,
+ synchronized_output: bool,
+ true_color: bool,
+ extended_underlines: bool,
+}
+
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
+enum KittyKeyboardSupport {
+ /// The terminal doesn't support the protocol.
+ #[default]
+ None,
+ /// The terminal supports the protocol but we haven't checked yet whether it has full or
+ /// partial support for the flags we require.
+ Some,
+ /// The terminal only supports some of the flags we require.
+ Partial,
+ /// The terminal supports all flags require.
+ Full,
+}
+
+#[derive(Debug)]
+pub struct TerminaBackend {
+ terminal: PlatformTerminal,
+ config: Config,
+ capabilities: Capabilities,
+ reset_cursor_command: String,
+ is_synchronized_output_set: bool,
+}
+
+impl TerminaBackend {
+ pub fn new(config: Config) -> io::Result<Self> {
+ let mut terminal = PlatformTerminal::new()?;
+ let (capabilities, reset_cursor_command) =
+ Self::detect_capabilities(&mut terminal, &config)?;
+
+ // In the case of a panic, reset the terminal eagerly. If we didn't do this and instead
+ // relied on `Drop`, the backtrace would be lost because it is printed before we would
+ // clear and exit the alternate screen.
+ let hook_reset_cursor_command = reset_cursor_command.clone();
+ terminal.set_panic_hook(move |term| {
+ let _ = write!(
+ term,
+ "{}{}{}{}{}{}{}{}{}{}{}",
+ Csi::Keyboard(csi::Keyboard::PopFlags(1)),
+ decreset!(MouseTracking),
+ decreset!(ButtonEventMouse),
+ decreset!(AnyEventMouse),
+ decreset!(RXVTMouse),
+ decreset!(SGRMouse),
+ &hook_reset_cursor_command,
+ decreset!(BracketedPaste),
+ decreset!(FocusTracking),
+ Csi::Edit(csi::Edit::EraseInDisplay(csi::EraseInDisplay::EraseDisplay)),
+ decreset!(ClearAndEnableAlternateScreen),
+ );
+ });
+
+ Ok(Self {
+ terminal,
+ config,
+ capabilities,
+ reset_cursor_command,
+ is_synchronized_output_set: false,
+ })
+ }
+
+ pub fn terminal(&self) -> &PlatformTerminal {
+ &self.terminal
+ }
+
+ fn detect_capabilities(
+ terminal: &mut PlatformTerminal,
+ config: &Config,
+ ) -> io::Result<(Capabilities, String)> {
+ use std::time::{Duration, Instant};
+
+ // Colibri "midnight"
+ const TEST_COLOR: RgbColor = RgbColor::new(59, 34, 76);
+
+ terminal.enter_raw_mode()?;
+
+ let mut capabilities = Capabilities::default();
+ let start = Instant::now();
+
+ // Many terminal extensions can be detected by querying the terminal for the state of the
+ // extension and then sending a request for the primary device attributes (which is
+ // consistently supported by all terminals). If we receive the status of the feature (for
+ // example the current Kitty keyboard flags) then we know that the feature is supported.
+ // If we only receive the device attributes then we know it is not.
+ write!(
+ terminal,
+ "{}{}{}{}{}{}{}",
+ // Kitty keyboard
+ Csi::Keyboard(csi::Keyboard::QueryFlags),
+ // Synchronized output
+ Csi::Mode(csi::Mode::QueryDecPrivateMode(csi::DecPrivateMode::Code(
+ csi::DecPrivateModeCode::SynchronizedOutput
+ ))),
+ // True color and while we're at it, extended underlines:
+ // <https://github.com/termstandard/colors?tab=readme-ov-file#querying-the-terminal>
+ Csi::Sgr(csi::Sgr::Background(TEST_COLOR.into())),
+ Csi::Sgr(csi::Sgr::UnderlineColor(TEST_COLOR.into())),
+ Dcs::Request(dcs::DcsRequest::GraphicRendition),
+ Csi::Sgr(csi::Sgr::Reset),
+ // Finally request the primary device attributes
+ Csi::Device(csi::Device::RequestPrimaryDeviceAttributes),
+ )?;
+ terminal.flush()?;
+
+ let device_attributes = |event: &Event| {
+ matches!(
+ event,
+ Event::Csi(Csi::Device(csi::Device::DeviceAttributes(_)))
+ )
+ };
+ // TODO: tune this poll constant? Does it need to be longer when on an SSH connection?
+ let poll_duration = Duration::from_millis(100);
+ if terminal.poll(device_attributes, Some(poll_duration))? {
+ while terminal.poll(Event::is_escape, Some(Duration::ZERO))? {
+ match terminal.read(Event::is_escape)? {
+ Event::Csi(Csi::Keyboard(csi::Keyboard::ReportFlags(_))) => {
+ capabilities.kitty_keyboard = KittyKeyboardSupport::Some;
+ }
+ Event::Csi(Csi::Mode(csi::Mode::ReportDecPrivateMode {
+ mode: csi::DecPrivateMode::Code(csi::DecPrivateModeCode::SynchronizedOutput),
+ setting: csi::DecModeSetting::Set | csi::DecModeSetting::Reset,
+ })) => {
+ capabilities.synchronized_output = true;
+ }
+ Event::Dcs(dcs::Dcs::Response {
+ value: dcs::DcsResponse::GraphicRendition(sgrs),
+ ..
+ }) => {
+ capabilities.true_color =
+ sgrs.contains(&csi::Sgr::Background(TEST_COLOR.into()));
+ capabilities.extended_underlines =
+ sgrs.contains(&csi::Sgr::UnderlineColor(TEST_COLOR.into()));
+ }
+ _ => (),
+ }
+ }
+
+ let end = Instant::now();
+ log::debug!(
+ "Detected terminal capabilities in {:?}: {capabilities:?}",
+ end.duration_since(start)
+ );
+ } else {
+ log::debug!("Failed to detect terminal capabilities within {poll_duration:?}. Using default capabilities only");
+ }
+
+ capabilities.extended_underlines |= config.force_enable_extended_underlines;
+
+ let reset_cursor_approach = if let Ok(t) = termini::TermInfo::from_env() {
+ capabilities.extended_underlines |= t.extended_cap("Smulx").is_some()
+ || t.extended_cap("Su").is_some()
+ || vte_version() >= Some(5102)
+ // HACK: once WezTerm can support DECRQSS/DECRPSS for SGR we can remove this line.
+ // <https://github.com/wezterm/wezterm/pull/6856>
+ || matches!(term_program().as_deref(), Some("WezTerm"));
+
+ reset_cursor_approach(t)
+ } else {
+ Csi::Cursor(csi::Cursor::CursorStyle(CursorStyle::Default)).to_string()
+ };
+
+ terminal.enter_cooked_mode()?;
+
+ Ok((capabilities, reset_cursor_approach))
+ }
+
+ fn enable_mouse_capture(&mut self) -> io::Result<()> {
+ if self.config.enable_mouse_capture {
+ write!(
+ self.terminal,
+ "{}{}{}{}{}",
+ decset!(MouseTracking),
+ decset!(ButtonEventMouse),
+ decset!(AnyEventMouse),
+ decset!(RXVTMouse),
+ decset!(SGRMouse),
+ )?;
+ }
+ Ok(())
+ }
+
+ fn disable_mouse_capture(&mut self) -> io::Result<()> {
+ if self.config.enable_mouse_capture {
+ write!(
+ self.terminal,
+ "{}{}{}{}{}",
+ decreset!(MouseTracking),
+ decreset!(ButtonEventMouse),
+ decreset!(AnyEventMouse),
+ decreset!(RXVTMouse),
+ decreset!(SGRMouse),
+ )?;
+ }
+ Ok(())
+ }
+
+ fn enable_extensions(&mut self) -> io::Result<()> {
+ const KEYBOARD_FLAGS: csi::KittyKeyboardFlags =
+ csi::KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES
+ .union(csi::KittyKeyboardFlags::REPORT_ALTERNATE_KEYS);
+
+ match self.capabilities.kitty_keyboard {
+ KittyKeyboardSupport::None | KittyKeyboardSupport::Partial => (),
+ KittyKeyboardSupport::Full => {
+ write!(
+ self.terminal,
+ "{}",
+ Csi::Keyboard(csi::Keyboard::PushFlags(KEYBOARD_FLAGS))
+ )?;
+ }
+ KittyKeyboardSupport::Some => {
+ write!(
+ self.terminal,
+ "{}{}",
+ // Enable the flags we need.
+ Csi::Keyboard(csi::Keyboard::PushFlags(KEYBOARD_FLAGS)),
+ // Then request the current flags. We need to check if the terminal enabled
+ // all of the flags we require.
+ Csi::Keyboard(csi::Keyboard::QueryFlags),
+ )?;
+ self.terminal.flush()?;
+
+ let event = self.terminal.read(|event| {
+ matches!(
+ event,
+ Event::Csi(Csi::Keyboard(csi::Keyboard::ReportFlags(_)))
+ )
+ })?;
+ let Event::Csi(Csi::Keyboard(csi::Keyboard::ReportFlags(flags))) = event else {
+ unreachable!();
+ };
+ if flags != KEYBOARD_FLAGS {
+ log::info!("Turning off enhanced keyboard support because the terminal enabled different flags. Requested {KEYBOARD_FLAGS:?} but got {flags:?}");
+ write!(
+ self.terminal,
+ "{}",
+ Csi::Keyboard(csi::Keyboard::PopFlags(1))
+ )?;
+ self.terminal.flush()?;
+ self.capabilities.kitty_keyboard = KittyKeyboardSupport::Partial;
+ } else {
+ log::debug!(
+ "The terminal fully supports the requested keyboard enhancement flags"
+ );
+ self.capabilities.kitty_keyboard = KittyKeyboardSupport::Full;
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ fn disable_extensions(&mut self) -> io::Result<()> {
+ if self.capabilities.kitty_keyboard == KittyKeyboardSupport::Full {
+ write!(
+ self.terminal,
+ "{}",
+ Csi::Keyboard(csi::Keyboard::PopFlags(1))
+ )?;
+ }
+
+ Ok(())
+ }
+
+ // See <https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036>.
+ // Synchronized output sequences tell the terminal when we are "starting to render" and
+ // stopping, enabling to make better choices about when it draws a frame. This avoids all
+ // kinds of ugly visual artifacts like tearing and flashing (i.e. the background color
+ // after clearing the terminal).
+
+ fn start_synchronized_render(&mut self) -> io::Result<()> {
+ if self.capabilities.synchronized_output && !self.is_synchronized_output_set {
+ write!(self.terminal, "{}", decset!(SynchronizedOutput))?;
+ self.is_synchronized_output_set = true;
+ }
+ Ok(())
+ }
+
+ fn end_sychronized_render(&mut self) -> io::Result<()> {
+ if self.is_synchronized_output_set {
+ write!(self.terminal, "{}", decreset!(SynchronizedOutput))?;
+ self.is_synchronized_output_set = false;
+ }
+ Ok(())
+ }
+}
+
+impl Backend for TerminaBackend {
+ fn claim(&mut self) -> io::Result<()> {
+ self.terminal.enter_raw_mode()?;
+
+ write!(
+ self.terminal,
+ "{}{}{}{}",
+ // Enter an alternate screen.
+ decset!(ClearAndEnableAlternateScreen),
+ decset!(BracketedPaste),
+ decset!(FocusTracking),
+ // Clear the buffer. `ClearAndEnableAlternateScreen` **should** do this but some
+ // things like mosh are buggy. See <https://github.com/helix-editor/helix/pull/1944>.
+ Csi::Edit(csi::Edit::EraseInDisplay(csi::EraseInDisplay::EraseDisplay)),
+ )?;
+ self.enable_mouse_capture()?;
+ self.enable_extensions()?;
+
+ Ok(())
+ }
+
+ fn reconfigure(&mut self, mut config: Config) -> io::Result<()> {
+ std::mem::swap(&mut self.config, &mut config);
+ if self.config.enable_mouse_capture != config.enable_mouse_capture {
+ if self.config.enable_mouse_capture {
+ self.enable_mouse_capture()?;
+ } else {
+ self.disable_mouse_capture()?;
+ }
+ }
+ self.capabilities.extended_underlines |= self.config.force_enable_extended_underlines;
+ Ok(())
+ }
+
+ fn restore(&mut self) -> io::Result<()> {
+ self.disable_extensions()?;
+ self.disable_mouse_capture()?;
+ write!(
+ self.terminal,
+ "{}{}{}{}",
+ &self.reset_cursor_command,
+ decreset!(BracketedPaste),
+ decreset!(FocusTracking),
+ decreset!(ClearAndEnableAlternateScreen),
+ )?;
+ self.terminal.flush()?;
+ self.terminal.enter_cooked_mode()?;
+ Ok(())
+ }
+
+ fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
+ where
+ I: Iterator<Item = (u16, u16, &'a Cell)>,
+ {
+ self.start_synchronized_render()?;
+
+ let mut fg = Color::Reset;
+ let mut bg = Color::Reset;
+ let mut underline_color = Color::Reset;
+ let mut underline_style = UnderlineStyle::Reset;
+ let mut modifier = Modifier::empty();
+ let mut last_pos: Option<(u16, u16)> = None;
+ for (x, y, cell) in content {
+ // Move the cursor if the previous location was not (x - 1, y)
+ if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
+ write!(
+ self.terminal,
+ "{}",
+ Csi::Cursor(csi::Cursor::Position {
+ col: OneBased::from_zero_based(x),
+ line: OneBased::from_zero_based(y),
+ })
+ )?;
+ }
+ last_pos = Some((x, y));
+
+ let mut attributes = SgrAttributes::default();
+ if cell.fg != fg {
+ attributes.foreground = Some(cell.fg.into());
+ fg = cell.fg;
+ }
+ if cell.bg != bg {
+ attributes.background = Some(cell.bg.into());
+ bg = cell.bg;
+ }
+ if cell.modifier != modifier {
+ attributes.modifiers = diff_modifiers(modifier, cell.modifier);
+ modifier = cell.modifier;
+ }
+
+ // Set underline style and color separately from SgrAttributes. Some terminals seem
+ // to not like underline colors and styles being intermixed with other SGRs.
+ let mut new_underline_style = cell.underline_style;
+ if self.capabilities.extended_underlines {
+ if cell.underline_color != underline_color {
+ write!(
+ self.terminal,
+ "{}",
+ Csi::Sgr(csi::Sgr::UnderlineColor(cell.underline_color.into()))
+ )?;
+ underline_color = cell.underline_color;
+ }
+ } else {
+ match new_underline_style {
+ UnderlineStyle::Reset | UnderlineStyle::Line => (),
+ _ => new_underline_style = UnderlineStyle::Line,
+ }
+ }
+ if new_underline_style != underline_style {
+ write!(
+ self.terminal,
+ "{}",
+ Csi::Sgr(csi::Sgr::Underline(new_underline_style.into()))
+ )?;
+ underline_style = new_underline_style;
+ }
+
+ // `attributes` will be empty if nothing changed between two cells. Empty
+ // `SgrAttributes` behave the same as a `Sgr::Reset` rather than a 'no-op' though so
+ // we should avoid writing them if they're empty.
+ if !attributes.is_empty() {
+ write!(
+ self.terminal,
+ "{}",
+ Csi::Sgr(csi::Sgr::Attributes(attributes))
+ )?;
+ }
+
+ write!(self.terminal, "{}", &cell.symbol)?;
+ }
+
+ write!(self.terminal, "{}", Csi::Sgr(csi::Sgr::Reset))?;
+
+ self.end_sychronized_render()?;
+
+ Ok(())
+ }
+
+ fn hide_cursor(&mut self) -> io::Result<()> {
+ write!(self.terminal, "{}", decreset!(ShowCursor))?;
+ self.flush()
+ }
+
+ fn show_cursor(&mut self, kind: CursorKind) -> io::Result<()> {
+ let style = match kind {
+ CursorKind::Block => CursorStyle::SteadyBlock,
+ CursorKind::Bar => CursorStyle::SteadyBar,
+ CursorKind::Underline => CursorStyle::SteadyUnderline,
+ CursorKind::Hidden => unreachable!(),
+ };
+ write!(
+ self.terminal,
+ "{}{}",
+ decset!(ShowCursor),
+ Csi::Cursor(csi::Cursor::CursorStyle(style)),
+ )?;
+ self.flush()
+ }
+
+ fn get_cursor(&mut self) -> Result<(u16, u16), io::Error> {
+ write!(
+ self.terminal,
+ "{}",
+ csi::Csi::Cursor(csi::Cursor::RequestActivePositionReport),
+ )?;
+ self.terminal.flush()?;
+ let event = self.terminal.read(|event| {
+ matches!(
+ event,
+ Event::Csi(Csi::Cursor(csi::Cursor::ActivePositionReport { .. }))
+ )
+ })?;
+ let Event::Csi(Csi::Cursor(csi::Cursor::ActivePositionReport { line, col })) = event else {
+ unreachable!();
+ };
+ Ok((line.get_zero_based(), col.get_zero_based()))
+ }
+
+ fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
+ let col = OneBased::from_zero_based(x);
+ let line = OneBased::from_zero_based(y);
+ write!(
+ self.terminal,
+ "{}",
+ Csi::Cursor(csi::Cursor::Position { line, col })
+ )?;
+ self.flush()
+ }
+
+ fn clear(&mut self) -> io::Result<()> {
+ self.start_synchronized_render()?;
+ write!(
+ self.terminal,
+ "{}",
+ Csi::Edit(csi::Edit::EraseInDisplay(csi::EraseInDisplay::EraseDisplay))
+ )?;
+ self.flush()
+ }
+
+ fn size(&self) -> io::Result<Rect> {
+ let WindowSize { rows, cols, .. } = self.terminal.get_dimensions()?;
+ Ok(Rect::new(0, 0, cols, rows))
+ }
+
+ fn flush(&mut self) -> io::Result<()> {
+ self.terminal.flush()
+ }
+
+ fn supports_true_color(&self) -> bool {
+ self.capabilities.true_color
+ }
+}
+
+impl Drop for TerminaBackend {
+ fn drop(&mut self) {
+ // Avoid resetting the terminal while panicking because we set a panic hook above in
+ // `Self::new`.
+ if !std::thread::panicking() {
+ let _ = self.disable_extensions();
+ let _ = self.disable_mouse_capture();
+ let _ = write!(
+ self.terminal,
+ "{}{}{}{}",
+ &self.reset_cursor_command,
+ decreset!(BracketedPaste),
+ decreset!(FocusTracking),
+ decreset!(ClearAndEnableAlternateScreen),
+ );
+ // NOTE: Drop for Platform terminal resets the mode and flushes the buffer when not
+ // panicking.
+ }
+ }
+}
+
+fn diff_modifiers(from: Modifier, to: Modifier) -> SgrModifiers {
+ let mut modifiers = SgrModifiers::default();
+
+ let removed = from - to;
+ if removed.contains(Modifier::REVERSED) {
+ modifiers |= SgrModifiers::NO_REVERSE;
+ }
+ if removed.contains(Modifier::BOLD) && !to.contains(Modifier::DIM) {
+ modifiers |= SgrModifiers::INTENSITY_NORMAL;
+ }
+ if removed.contains(Modifier::DIM) {
+ modifiers |= SgrModifiers::INTENSITY_NORMAL;
+ }
+ if removed.contains(Modifier::ITALIC) {
+ modifiers |= SgrModifiers::NO_ITALIC;
+ }
+ if removed.contains(Modifier::CROSSED_OUT) {
+ modifiers |= SgrModifiers::NO_STRIKE_THROUGH;
+ }
+ if removed.contains(Modifier::HIDDEN) {
+ modifiers |= SgrModifiers::NO_INVISIBLE;
+ }
+ if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
+ modifiers |= SgrModifiers::BLINK_NONE;
+ }
+
+ let added = to - from;
+ if added.contains(Modifier::REVERSED) {
+ modifiers |= SgrModifiers::REVERSE;
+ }
+ if added.contains(Modifier::BOLD) {
+ modifiers |= SgrModifiers::INTENSITY_BOLD;
+ }
+ if added.contains(Modifier::DIM) {
+ modifiers |= SgrModifiers::INTENSITY_DIM;
+ }
+ if added.contains(Modifier::ITALIC) {
+ modifiers |= SgrModifiers::ITALIC;
+ }
+ if added.contains(Modifier::CROSSED_OUT) {
+ modifiers |= SgrModifiers::STRIKE_THROUGH;
+ }
+ if added.contains(Modifier::HIDDEN) {
+ modifiers |= SgrModifiers::INVISIBLE;
+ }
+ if added.contains(Modifier::SLOW_BLINK) {
+ modifiers |= SgrModifiers::BLINK_SLOW;
+ }
+ if added.contains(Modifier::RAPID_BLINK) {
+ modifiers |= SgrModifiers::BLINK_RAPID;
+ }
+
+ modifiers
+}
diff --git a/helix-tui/src/backend/test.rs b/helix-tui/src/backend/test.rs
index 8cd3a2fd..47049cd8 100644
--- a/helix-tui/src/backend/test.rs
+++ b/helix-tui/src/backend/test.rs
@@ -119,10 +119,6 @@ impl Backend for TestBackend {
Ok(())
}
- fn force_restore() -> Result<(), io::Error> {
- Ok(())
- }
-
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
@@ -164,4 +160,8 @@ impl Backend for TestBackend {
fn flush(&mut self) -> Result<(), io::Error> {
Ok(())
}
+
+ fn supports_true_color(&self) -> bool {
+ false
+ }
}
diff --git a/helix-tui/src/lib.rs b/helix-tui/src/lib.rs
index 59327d7c..91e7d7bd 100644
--- a/helix-tui/src/lib.rs
+++ b/helix-tui/src/lib.rs
@@ -1,133 +1,3 @@
-//! [tui](https://github.com/fdehau/tui-rs) is a library used to build rich
-//! terminal users interfaces and dashboards.
-//!
-//! ![](https://raw.githubusercontent.com/fdehau/tui-rs/master/assets/demo.gif)
-//!
-//! # Get started
-//!
-//! ## Adding `tui` as a dependency
-//!
-//! ```toml
-//! [dependencies]
-//! tui = "0.15"
-//! crossterm = "0.19"
-//! ```
-//!
-//! The same logic applies for all other available backends.
-//!
-//! ## Creating a `Terminal`
-//!
-//! Every application using `tui` should start by instantiating a `Terminal`. It is a light
-//! abstraction over available backends that provides basic functionalities such as clearing the
-//! screen, hiding the cursor, etc.
-//!
-//! ```rust,no_run
-//! use std::io;
-//! use helix_tui::Terminal;
-//! use helix_tui::backend::CrosstermBackend;
-//! use helix_view::editor::Config;
-//!
-//! fn main() -> Result<(), io::Error> {
-//! let stdout = io::stdout();
-//! let config = Config::default();
-//! let backend = CrosstermBackend::new(stdout, &config);
-//! let mut terminal = Terminal::new(backend)?;
-//! Ok(())
-//! }
-//! ```
-//!
-//! You may also refer to the examples to find out how to create a `Terminal` for each available
-//! backend.
-//!
-//! ## Building a User Interface (UI)
-//!
-//! Every component of your interface will be implementing the `Widget` trait. The library comes
-//! with a predefined set of widgets that should meet most of your use cases. You are also free to
-//! implement your own.
-//!
-//! Each widget follows a builder pattern API providing a default configuration along with methods
-//! to customize them. The widget is then rendered using the `Frame::render_widget` which take
-//! your widget instance an area to draw to.
-//!
-//! The following example renders a block of the size of the terminal:
-//!
-//! ```rust,no_run
-//! use std::io;
-//! use crossterm::terminal;
-//! use helix_tui::Terminal;
-//! use helix_tui::backend::CrosstermBackend;
-//! use helix_tui::widgets::{Widget, Block, Borders};
-//! use helix_tui::layout::{Layout, Constraint, Direction};
-//! use helix_view::editor::Config;
-//!
-//! fn main() -> Result<(), io::Error> {
-//! terminal::enable_raw_mode().unwrap();
-//! let stdout = io::stdout();
-//! let config = Config::default();
-//! let backend = CrosstermBackend::new(stdout, &config);
-//! let mut terminal = Terminal::new(backend)?;
-//! // terminal.draw(|f| {
-//! // let size = f.size();
-//! // let block = Block::default()
-//! // .title("Block")
-//! // .borders(Borders::ALL);
-//! // f.render_widget(block, size);
-//! // })?;
-//! Ok(())
-//! }
-//! ```
-//!
-//! ## Layout
-//!
-//! The library comes with a basic yet useful layout management object called `Layout`. As you may
-//! see below and in the examples, the library makes heavy use of the builder pattern to provide
-//! full customization. And `Layout` is no exception:
-//!
-//! ```rust,no_run
-//! use std::io;
-//! use crossterm::terminal;
-//! use helix_tui::Terminal;
-//! use helix_tui::backend::CrosstermBackend;
-//! use helix_tui::widgets::{Widget, Block, Borders};
-//! use helix_tui::layout::{Layout, Constraint, Direction};
-//! use helix_view::editor::Config;
-//!
-//! fn main() -> Result<(), io::Error> {
-//! terminal::enable_raw_mode().unwrap();
-//! let stdout = io::stdout();
-//! let config = Config::default();
-//! let backend = CrosstermBackend::new(stdout, &config);
-//! let mut terminal = Terminal::new(backend)?;
-//! // terminal.draw(|f| {
-//! // let chunks = Layout::default()
-//! // .direction(Direction::Vertical)
-//! // .margin(1)
-//! // .constraints(
-//! // [
-//! // Constraint::Percentage(10),
-//! // Constraint::Percentage(80),
-//! // Constraint::Percentage(10)
-//! // ].as_ref()
-//! // )
-//! // .split(f.size());
-//! // let block = Block::default()
-//! // .title("Block")
-//! // .borders(Borders::ALL);
-//! // f.render_widget(block, chunks[0]);
-//! // let block = Block::default()
-//! // .title("Block 2")
-//! // .borders(Borders::ALL);
-//! // f.render_widget(block, chunks[1]);
-//! // })?;
-//! Ok(())
-//! }
-//! ```
-//!
-//! This let you describe responsive terminal UI by nesting layouts. You should note that by
-//! default the computed layout tries to fill the available space completely. So if for any reason
-//! you might need a blank space somewhere, try to pass an additional constraint and don't use the
-//! corresponding area.
-
pub mod backend;
pub mod buffer;
pub mod layout;
diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml
index 3154788d..ab8c9fe5 100644
--- a/helix-view/Cargo.toml
+++ b/helix-view/Cargo.toml
@@ -12,7 +12,7 @@ homepage.workspace = true
[features]
default = []
-term = ["crossterm"]
+term = ["termina"]
unicode-lines = []
[dependencies]
@@ -26,7 +26,7 @@ helix-vcs = { path = "../helix-vcs" }
bitflags.workspace = true
anyhow = "1"
-crossterm = { version = "0.28", optional = true }
+termina = { workspace = true, optional = true }
tempfile.workspace = true
diff --git a/helix-view/src/base64.rs b/helix-view/src/base64.rs
deleted file mode 100644
index 13ee919d..00000000
--- a/helix-view/src/base64.rs
+++ /dev/null
@@ -1,163 +0,0 @@
-// A minimal base64 implementation to keep from pulling in a crate for just that. It's based on
-// https://github.com/marshallpierce/rust-base64 but without all the customization options.
-// The biggest portion comes from
-// https://github.com/marshallpierce/rust-base64/blob/a675443d327e175f735a37f574de803d6a332591/src/engine/naive.rs#L42
-// Thanks, rust-base64!
-
-// The MIT License (MIT)
-
-// Copyright (c) 2015 Alice Maz
-
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-// THE SOFTWARE.
-
-use std::ops::{BitAnd, BitOr, Shl, Shr};
-
-const PAD_BYTE: u8 = b'=';
-const ENCODE_TABLE: &[u8] =
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".as_bytes();
-const LOW_SIX_BITS: u32 = 0x3F;
-
-pub fn encode(input: &[u8]) -> String {
- let rem = input.len() % 3;
- let complete_chunks = input.len() / 3;
- let remainder_chunk = usize::from(rem != 0);
- let encoded_size = (complete_chunks + remainder_chunk) * 4;
-
- let mut output = vec![0; encoded_size];
-
- // complete chunks first
- let complete_chunk_len = input.len() - rem;
-
- let mut input_index = 0_usize;
- let mut output_index = 0_usize;
- while input_index < complete_chunk_len {
- let chunk = &input[input_index..input_index + 3];
-
- // populate low 24 bits from 3 bytes
- let chunk_int: u32 =
- (chunk[0] as u32).shl(16) | (chunk[1] as u32).shl(8) | (chunk[2] as u32);
- // encode 4x 6-bit output bytes
- output[output_index] = ENCODE_TABLE[chunk_int.shr(18) as usize];
- output[output_index + 1] = ENCODE_TABLE[chunk_int.shr(12_u8).bitand(LOW_SIX_BITS) as usize];
- output[output_index + 2] = ENCODE_TABLE[chunk_int.shr(6_u8).bitand(LOW_SIX_BITS) as usize];
- output[output_index + 3] = ENCODE_TABLE[chunk_int.bitand(LOW_SIX_BITS) as usize];
-
- input_index += 3;
- output_index += 4;
- }
-
- // then leftovers
- if rem == 2 {
- let chunk = &input[input_index..input_index + 2];
-
- // high six bits of chunk[0]
- output[output_index] = ENCODE_TABLE[chunk[0].shr(2) as usize];
- // bottom 2 bits of [0], high 4 bits of [1]
- output[output_index + 1] = ENCODE_TABLE
- [(chunk[0].shl(4_u8).bitor(chunk[1].shr(4_u8)) as u32).bitand(LOW_SIX_BITS) as usize];
- // bottom 4 bits of [1], with the 2 bottom bits as zero
- output[output_index + 2] =
- ENCODE_TABLE[(chunk[1].shl(2_u8) as u32).bitand(LOW_SIX_BITS) as usize];
- output[output_index + 3] = PAD_BYTE;
- } else if rem == 1 {
- let byte = input[input_index];
- output[output_index] = ENCODE_TABLE[byte.shr(2) as usize];
- output[output_index + 1] =
- ENCODE_TABLE[(byte.shl(4_u8) as u32).bitand(LOW_SIX_BITS) as usize];
- output[output_index + 2] = PAD_BYTE;
- output[output_index + 3] = PAD_BYTE;
- }
- String::from_utf8(output).expect("Invalid UTF8")
-}
-
-#[cfg(test)]
-mod tests {
- fn compare_encode(expected: &str, target: &[u8]) {
- assert_eq!(expected, super::encode(target));
- }
-
- #[test]
- fn encode_rfc4648_0() {
- compare_encode("", b"");
- }
-
- #[test]
- fn encode_rfc4648_1() {
- compare_encode("Zg==", b"f");
- }
-
- #[test]
- fn encode_rfc4648_2() {
- compare_encode("Zm8=", b"fo");
- }
-
- #[test]
- fn encode_rfc4648_3() {
- compare_encode("Zm9v", b"foo");
- }
-
- #[test]
- fn encode_rfc4648_4() {
- compare_encode("Zm9vYg==", b"foob");
- }
-
- #[test]
- fn encode_rfc4648_5() {
- compare_encode("Zm9vYmE=", b"fooba");
- }
-
- #[test]
- fn encode_rfc4648_6() {
- compare_encode("Zm9vYmFy", b"foobar");
- }
-
- #[test]
- fn encode_all_ascii() {
- let mut ascii = Vec::<u8>::with_capacity(128);
-
- for i in 0..128 {
- ascii.push(i);
- }
-
- compare_encode(
- "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7P\
- D0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn8\
- =",
- &ascii,
- );
- }
-
- #[test]
- fn encode_all_bytes() {
- let mut bytes = Vec::<u8>::with_capacity(256);
-
- for i in 0..255 {
- bytes.push(i);
- }
- bytes.push(255); //bug with "overflowing" ranges?
-
- compare_encode(
- "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7P\
- D0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn\
- +AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6\
- /wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w==",
- &bytes,
- );
- }
-}
diff --git a/helix-view/src/clipboard.rs b/helix-view/src/clipboard.rs
index 1cf63348..c9e2a8e0 100644
--- a/helix-view/src/clipboard.rs
+++ b/helix-view/src/clipboard.rs
@@ -292,10 +292,17 @@ mod external {
},
#[cfg(feature = "term")]
Self::Termcode => {
- crossterm::queue!(
- std::io::stdout(),
- osc52::SetClipboardCommand::new(content, clipboard_type)
- )?;
+ use std::io::Write;
+ use termina::escape::osc::{self, Osc};
+ let selection = match clipboard_type {
+ ClipboardType::Clipboard => osc::Selection::CLIPBOARD,
+ ClipboardType::Selection => osc::Selection::PRIMARY,
+ };
+ // NOTE: it would be ideal to have the terminal execute this but it _should_
+ // work to send this over stdout instead.
+ let mut stdout = std::io::stdout().lock();
+ write!(stdout, "{}", Osc::SetSelection(selection, content))?;
+ stdout.flush()?;
Ok(())
}
Self::Custom(command_provider) => match clipboard_type {
@@ -400,43 +407,6 @@ mod external {
paste => "termux-clipboard-set";
}
- #[cfg(feature = "term")]
- mod osc52 {
- use {super::ClipboardType, crate::base64};
-
- pub struct SetClipboardCommand {
- encoded_content: String,
- clipboard_type: ClipboardType,
- }
-
- impl SetClipboardCommand {
- pub fn new(content: &str, clipboard_type: ClipboardType) -> Self {
- Self {
- encoded_content: base64::encode(content.as_bytes()),
- clipboard_type,
- }
- }
- }
-
- impl crossterm::Command for SetClipboardCommand {
- fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
- let kind = match &self.clipboard_type {
- ClipboardType::Clipboard => "c",
- ClipboardType::Selection => "p",
- };
- // Send an OSC 52 set command: https://terminalguide.namepad.de/seq/osc-52/
- write!(f, "\x1b]52;{};{}\x1b\\", kind, &self.encoded_content)
- }
- #[cfg(windows)]
- fn execute_winapi(&self) -> std::result::Result<(), std::io::Error> {
- Err(std::io::Error::new(
- std::io::ErrorKind::Other,
- "OSC clipboard codes not supported by winapi.",
- ))
- }
- }
- }
-
fn execute_command(
cmd: &Command,
input: Option<&str>,
diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs
index 3cd3c862..b41265d2 100644
--- a/helix-view/src/graphics.rs
+++ b/helix-view/src/graphics.rs
@@ -289,30 +289,28 @@ impl Color {
}
#[cfg(feature = "term")]
-impl From<Color> for crossterm::style::Color {
+impl From<Color> for termina::style::ColorSpec {
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 },
+ Color::Reset => Self::Reset,
+ Color::Black => Self::BLACK,
+ Color::Red => Self::RED,
+ Color::Green => Self::GREEN,
+ Color::Yellow => Self::YELLOW,
+ Color::Blue => Self::BLUE,
+ Color::Magenta => Self::MAGENTA,
+ Color::Cyan => Self::CYAN,
+ Color::Gray => Self::BRIGHT_BLACK,
+ Color::White => Self::WHITE,
+ Color::LightRed => Self::BRIGHT_RED,
+ Color::LightGreen => Self::BRIGHT_GREEN,
+ Color::LightBlue => Self::BRIGHT_BLUE,
+ Color::LightYellow => Self::BRIGHT_YELLOW,
+ Color::LightMagenta => Self::BRIGHT_MAGENTA,
+ Color::LightCyan => Self::BRIGHT_CYAN,
+ Color::LightGray => Self::BRIGHT_WHITE,
+ Color::Indexed(i) => Self::PaletteIndex(i),
+ Color::Rgb(r, g, b) => termina::style::RgbColor::new(r, g, b).into(),
}
}
}
@@ -343,15 +341,15 @@ impl FromStr for UnderlineStyle {
}
#[cfg(feature = "term")]
-impl From<UnderlineStyle> for crossterm::style::Attribute {
+impl From<UnderlineStyle> for termina::style::Underline {
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,
+ UnderlineStyle::Reset => Self::None,
+ UnderlineStyle::Line => Self::Single,
+ UnderlineStyle::Curl => Self::Curly,
+ UnderlineStyle::Dotted => Self::Dotted,
+ UnderlineStyle::Dashed => Self::Dashed,
+ UnderlineStyle::DoubleLine => Self::Double,
}
}
}
diff --git a/helix-view/src/input.rs b/helix-view/src/input.rs
index d359db70..6b3a9756 100644
--- a/helix-view/src/input.rs
+++ b/helix-view/src/input.rs
@@ -1,4 +1,4 @@
-//! Input event handling, currently backed by crossterm.
+//! Input event handling, currently backed by termina.
use anyhow::{anyhow, Error};
use helix_core::unicode::{segmentation::UnicodeSegmentation, width::UnicodeWidthStr};
use serde::de::{self, Deserialize, Deserializer};
@@ -65,7 +65,7 @@ pub enum MouseButton {
pub struct KeyEvent {
pub code: KeyCode,
pub modifiers: KeyModifiers,
- // TODO: crossterm now supports kind & state if terminal supports kitty's extended protocol
+ // TODO: termina now supports kind & state if terminal supports kitty's extended protocol
}
impl KeyEvent {
@@ -459,28 +459,31 @@ impl<'de> Deserialize<'de> for KeyEvent {
}
#[cfg(feature = "term")]
-impl From<crossterm::event::Event> for Event {
- fn from(event: crossterm::event::Event) -> Self {
+impl From<termina::event::Event> for Event {
+ fn from(event: termina::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),
+ termina::event::Event::Key(key) => Self::Key(key.into()),
+ termina::event::Event::Mouse(mouse) => Self::Mouse(mouse.into()),
+ termina::event::Event::WindowResized(termina::WindowSize { rows, cols, .. }) => {
+ Self::Resize(cols, rows)
+ }
+ termina::event::Event::FocusIn => Self::FocusGained,
+ termina::event::Event::FocusOut => Self::FocusLost,
+ termina::event::Event::Paste(s) => Self::Paste(s),
+ _ => unreachable!(),
}
}
}
#[cfg(feature = "term")]
-impl From<crossterm::event::MouseEvent> for MouseEvent {
+impl From<termina::event::MouseEvent> for MouseEvent {
fn from(
- crossterm::event::MouseEvent {
+ termina::event::MouseEvent {
kind,
column,
row,
modifiers,
- }: crossterm::event::MouseEvent,
+ }: termina::event::MouseEvent,
) -> Self {
Self {
kind: kind.into(),
@@ -492,40 +495,40 @@ impl From<crossterm::event::MouseEvent> for MouseEvent {
}
#[cfg(feature = "term")]
-impl From<crossterm::event::MouseEventKind> for MouseEventKind {
- fn from(kind: crossterm::event::MouseEventKind) -> Self {
+impl From<termina::event::MouseEventKind> for MouseEventKind {
+ fn from(kind: termina::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,
+ termina::event::MouseEventKind::Down(button) => Self::Down(button.into()),
+ termina::event::MouseEventKind::Up(button) => Self::Up(button.into()),
+ termina::event::MouseEventKind::Drag(button) => Self::Drag(button.into()),
+ termina::event::MouseEventKind::Moved => Self::Moved,
+ termina::event::MouseEventKind::ScrollDown => Self::ScrollDown,
+ termina::event::MouseEventKind::ScrollUp => Self::ScrollUp,
+ termina::event::MouseEventKind::ScrollLeft => Self::ScrollLeft,
+ termina::event::MouseEventKind::ScrollRight => Self::ScrollRight,
}
}
}
#[cfg(feature = "term")]
-impl From<crossterm::event::MouseButton> for MouseButton {
- fn from(button: crossterm::event::MouseButton) -> Self {
+impl From<termina::event::MouseButton> for MouseButton {
+ fn from(button: termina::event::MouseButton) -> Self {
match button {
- crossterm::event::MouseButton::Left => MouseButton::Left,
- crossterm::event::MouseButton::Right => MouseButton::Right,
- crossterm::event::MouseButton::Middle => MouseButton::Middle,
+ termina::event::MouseButton::Left => MouseButton::Left,
+ termina::event::MouseButton::Right => MouseButton::Right,
+ termina::event::MouseButton::Middle => MouseButton::Middle,
}
}
}
#[cfg(feature = "term")]
-impl From<crossterm::event::KeyEvent> for KeyEvent {
+impl From<termina::event::KeyEvent> for KeyEvent {
fn from(
- crossterm::event::KeyEvent {
+ termina::event::KeyEvent {
code, modifiers, ..
- }: crossterm::event::KeyEvent,
+ }: termina::event::KeyEvent,
) -> Self {
- if code == crossterm::event::KeyCode::BackTab {
+ if code == termina::event::KeyCode::BackTab {
// special case for BackTab -> Shift-Tab
let mut modifiers: KeyModifiers = modifiers.into();
modifiers.insert(KeyModifiers::SHIFT);
@@ -543,24 +546,24 @@ impl From<crossterm::event::KeyEvent> for KeyEvent {
}
#[cfg(feature = "term")]
-impl From<KeyEvent> for crossterm::event::KeyEvent {
+impl From<KeyEvent> for termina::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,
+ termina::event::KeyEvent {
+ code: termina::event::KeyCode::BackTab,
modifiers: modifiers.into(),
- kind: crossterm::event::KeyEventKind::Press,
- state: crossterm::event::KeyEventState::NONE,
+ kind: termina::event::KeyEventKind::Press,
+ state: termina::event::KeyEventState::NONE,
}
} else {
- crossterm::event::KeyEvent {
+ termina::event::KeyEvent {
code: code.into(),
modifiers: modifiers.into(),
- kind: crossterm::event::KeyEventKind::Press,
- state: crossterm::event::KeyEventState::NONE,
+ kind: termina::event::KeyEventKind::Press,
+ state: termina::event::KeyEventState::NONE,
}
}
}
diff --git a/helix-view/src/keyboard.rs b/helix-view/src/keyboard.rs
index d816a52e..c3831143 100644
--- a/helix-view/src/keyboard.rs
+++ b/helix-view/src/keyboard.rs
@@ -13,9 +13,9 @@ bitflags! {
}
#[cfg(feature = "term")]
-impl From<KeyModifiers> for crossterm::event::KeyModifiers {
+impl From<KeyModifiers> for termina::event::Modifiers {
fn from(key_modifiers: KeyModifiers) -> Self {
- use crossterm::event::KeyModifiers as CKeyModifiers;
+ use termina::event::Modifiers as CKeyModifiers;
let mut result = CKeyModifiers::NONE;
@@ -37,9 +37,9 @@ impl From<KeyModifiers> for crossterm::event::KeyModifiers {
}
#[cfg(feature = "term")]
-impl From<crossterm::event::KeyModifiers> for KeyModifiers {
- fn from(val: crossterm::event::KeyModifiers) -> Self {
- use crossterm::event::KeyModifiers as CKeyModifiers;
+impl From<termina::event::Modifiers> for KeyModifiers {
+ fn from(val: termina::event::Modifiers) -> Self {
+ use termina::event::Modifiers as CKeyModifiers;
let mut result = KeyModifiers::NONE;
@@ -92,9 +92,9 @@ pub enum MediaKeyCode {
}
#[cfg(feature = "term")]
-impl From<MediaKeyCode> for crossterm::event::MediaKeyCode {
+impl From<MediaKeyCode> for termina::event::MediaKeyCode {
fn from(media_key_code: MediaKeyCode) -> Self {
- use crossterm::event::MediaKeyCode as CMediaKeyCode;
+ use termina::event::MediaKeyCode as CMediaKeyCode;
match media_key_code {
MediaKeyCode::Play => CMediaKeyCode::Play,
@@ -115,9 +115,9 @@ impl From<MediaKeyCode> for crossterm::event::MediaKeyCode {
}
#[cfg(feature = "term")]
-impl From<crossterm::event::MediaKeyCode> for MediaKeyCode {
- fn from(val: crossterm::event::MediaKeyCode) -> Self {
- use crossterm::event::MediaKeyCode as CMediaKeyCode;
+impl From<termina::event::MediaKeyCode> for MediaKeyCode {
+ fn from(val: termina::event::MediaKeyCode) -> Self {
+ use termina::event::MediaKeyCode as CMediaKeyCode;
match val {
CMediaKeyCode::Play => MediaKeyCode::Play,
@@ -171,9 +171,9 @@ pub enum ModifierKeyCode {
}
#[cfg(feature = "term")]
-impl From<ModifierKeyCode> for crossterm::event::ModifierKeyCode {
+impl From<ModifierKeyCode> for termina::event::ModifierKeyCode {
fn from(modifier_key_code: ModifierKeyCode) -> Self {
- use crossterm::event::ModifierKeyCode as CModifierKeyCode;
+ use termina::event::ModifierKeyCode as CModifierKeyCode;
match modifier_key_code {
ModifierKeyCode::LeftShift => CModifierKeyCode::LeftShift,
@@ -195,9 +195,9 @@ impl From<ModifierKeyCode> for crossterm::event::ModifierKeyCode {
}
#[cfg(feature = "term")]
-impl From<crossterm::event::ModifierKeyCode> for ModifierKeyCode {
- fn from(val: crossterm::event::ModifierKeyCode) -> Self {
- use crossterm::event::ModifierKeyCode as CModifierKeyCode;
+impl From<termina::event::ModifierKeyCode> for ModifierKeyCode {
+ fn from(val: termina::event::ModifierKeyCode) -> Self {
+ use termina::event::ModifierKeyCode as CModifierKeyCode;
match val {
CModifierKeyCode::LeftShift => ModifierKeyCode::LeftShift,
@@ -280,9 +280,9 @@ pub enum KeyCode {
}
#[cfg(feature = "term")]
-impl From<KeyCode> for crossterm::event::KeyCode {
+impl From<KeyCode> for termina::event::KeyCode {
fn from(key_code: KeyCode) -> Self {
- use crossterm::event::KeyCode as CKeyCode;
+ use termina::event::KeyCode as CKeyCode;
match key_code {
KeyCode::Backspace => CKeyCode::Backspace,
@@ -298,10 +298,10 @@ impl From<KeyCode> for crossterm::event::KeyCode {
KeyCode::Tab => CKeyCode::Tab,
KeyCode::Delete => CKeyCode::Delete,
KeyCode::Insert => CKeyCode::Insert,
- KeyCode::F(f_number) => CKeyCode::F(f_number),
+ KeyCode::F(f_number) => CKeyCode::Function(f_number),
KeyCode::Char(character) => CKeyCode::Char(character),
KeyCode::Null => CKeyCode::Null,
- KeyCode::Esc => CKeyCode::Esc,
+ KeyCode::Esc => CKeyCode::Escape,
KeyCode::CapsLock => CKeyCode::CapsLock,
KeyCode::ScrollLock => CKeyCode::ScrollLock,
KeyCode::NumLock => CKeyCode::NumLock,
@@ -316,9 +316,9 @@ impl From<KeyCode> for crossterm::event::KeyCode {
}
#[cfg(feature = "term")]
-impl From<crossterm::event::KeyCode> for KeyCode {
- fn from(val: crossterm::event::KeyCode) -> Self {
- use crossterm::event::KeyCode as CKeyCode;
+impl From<termina::event::KeyCode> for KeyCode {
+ fn from(val: termina::event::KeyCode) -> Self {
+ use termina::event::KeyCode as CKeyCode;
match val {
CKeyCode::Backspace => KeyCode::Backspace,
@@ -335,10 +335,10 @@ impl From<crossterm::event::KeyCode> for KeyCode {
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::Function(f_number) => KeyCode::F(f_number),
CKeyCode::Char(character) => KeyCode::Char(character),
CKeyCode::Null => KeyCode::Null,
- CKeyCode::Esc => KeyCode::Esc,
+ CKeyCode::Escape => KeyCode::Esc,
CKeyCode::CapsLock => KeyCode::CapsLock,
CKeyCode::ScrollLock => KeyCode::ScrollLock,
CKeyCode::NumLock => KeyCode::NumLock,
diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs
index e30a2338..a7e9f461 100644
--- a/helix-view/src/lib.rs
+++ b/helix-view/src/lib.rs
@@ -2,7 +2,6 @@
pub mod macros;
pub mod annotations;
-pub mod base64;
pub mod clipboard;
pub mod document;
pub mod editor;