Compare commits

...

3 Commits

Author SHA1 Message Date
Michael Davis
4dab0b5406 Remove Backend::get_cursor
It is unused and cannot be used on some terminal hosts like `conhost`
that do not respond to VT queries. This doesn't have any affect on
behavior - I'm removing it so that we don't rely on it in the future.
2025-07-13 12:16:39 -04:00
Michael Davis
609a661207 Replace Crossterm with Termina
This change switches out the terminal manipulation library to one I've
been working on: Termina. It's somewhat similar to Crossterm API-wise
but is a bit lower-level. It's also influenced a lot by TermWiz - the
terminal manipulation library in WezTerm which we've considered
switching to a few times.

Termina is more verbose than Crossterm as it has a lower level interface
that exposes escape sequences and pushes handling to the application.
API-wise the important piece is that the equivalents of Crossterm's
`poll_internal` / `read_internal` are exposed. This is used for reading
the cursor position in both Crossterm and Termina, for example, but also
now can be used to detect features like the Kitty keyboard protocol and
synchronized output sequences simultaneously.
2025-07-13 12:16:39 -04:00
Michael Davis
f1b3e4bb0d tui: Refactor Config type handling in backends
The `Config` can be passed when creating the backend (for example
`CrosstermBackend::new`) and is already updated in the
`Backend::reconfigure` callback. Recreating the tui `Config` during
`claim` and `restore` is unnecessary and causes a clone of the editor's
Config which is a fairly large type. This change drops the `Config`
parameter from those callbacks and updates the callers. Instead it is
passed to `CrosstermBackend` which then owns it.

I've also moved the override from the `editor.undercurl` key onto the
tui `Config` type - I believe it was just an oversight that this was not
done originally. And I've updated the `From<EditorConfig> for Config`
to take a reference to the editor's `Config` to avoid the unnecessary
clone during `CrosstermBackend::new` and `Backend::reconfigure`.
2025-07-13 12:10:47 -04:00
22 changed files with 927 additions and 713 deletions

231
Cargo.lock generated
View File

@@ -259,34 +259,6 @@ 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",
"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"
@@ -426,17 +398,6 @@ dependencies = [
"log",
]
[[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"
@@ -603,7 +564,7 @@ dependencies = [
"gix-worktree",
"once_cell",
"smallvec",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -616,7 +577,7 @@ dependencies = [
"gix-date",
"gix-utils",
"itoa",
"thiserror 2.0.12",
"thiserror",
"winnow",
]
@@ -633,7 +594,7 @@ dependencies = [
"gix-trace",
"kstring",
"smallvec",
"thiserror 2.0.12",
"thiserror",
"unicode-bom",
]
@@ -643,7 +604,7 @@ version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1db9765c69502650da68f0804e3dc2b5f8ccc6a2d104ca6c85bc40700d37540"
dependencies = [
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -652,7 +613,7 @@ version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b1f1d8764958699dc764e3f727cef280ff4d1bd92c107bbf8acd85b30c1bd6f"
dependencies = [
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -678,7 +639,7 @@ dependencies = [
"gix-chunk",
"gix-hash",
"memmap2",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -697,7 +658,7 @@ dependencies = [
"memchr",
"once_cell",
"smallvec",
"thiserror 2.0.12",
"thiserror",
"unicode-bom",
"winnow",
]
@@ -712,7 +673,7 @@ dependencies = [
"bstr",
"gix-path",
"libc",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -725,7 +686,7 @@ dependencies = [
"itoa",
"jiff",
"smallvec",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -749,7 +710,7 @@ dependencies = [
"gix-traverse",
"gix-worktree",
"imara-diff 0.1.8",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -769,7 +730,7 @@ dependencies = [
"gix-trace",
"gix-utils",
"gix-worktree",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -785,7 +746,7 @@ dependencies = [
"gix-path",
"gix-ref",
"gix-sec",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -802,7 +763,7 @@ dependencies = [
"libc",
"once_cell",
"prodash",
"thiserror 2.0.12",
"thiserror",
"walkdir",
]
@@ -824,7 +785,7 @@ dependencies = [
"gix-trace",
"gix-utils",
"smallvec",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -838,7 +799,7 @@ dependencies = [
"gix-features",
"gix-path",
"gix-utils",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -862,7 +823,7 @@ dependencies = [
"faster-hex",
"gix-features",
"sha1-checked",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -912,9 +873,9 @@ dependencies = [
"itoa",
"libc",
"memmap2",
"rustix 1.0.7",
"rustix",
"smallvec",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -925,7 +886,7 @@ checksum = "570f8b034659f256366dc90f1a24924902f20acccd6a15be96d44d1269e7a796"
dependencies = [
"gix-tempfile",
"gix-utils",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -945,7 +906,7 @@ dependencies = [
"gix-validate",
"itoa",
"smallvec",
"thiserror 2.0.12",
"thiserror",
"winnow",
]
@@ -967,7 +928,7 @@ dependencies = [
"gix-quote",
"parking_lot",
"tempfile",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -985,7 +946,7 @@ dependencies = [
"gix-path",
"memmap2",
"smallvec",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -997,7 +958,7 @@ dependencies = [
"bstr",
"faster-hex",
"gix-trace",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -1009,7 +970,7 @@ dependencies = [
"bstr",
"faster-hex",
"gix-trace",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -1023,7 +984,7 @@ dependencies = [
"gix-validate",
"home",
"once_cell",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -1038,7 +999,7 @@ dependencies = [
"gix-config-value",
"gix-glob",
"gix-path",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -1056,7 +1017,7 @@ dependencies = [
"gix-transport",
"gix-utils",
"maybe-async",
"thiserror 2.0.12",
"thiserror",
"winnow",
]
@@ -1068,7 +1029,7 @@ checksum = "4a375a75b4d663e8bafe3bf4940a18a23755644c13582fa326e99f8f987d83fd"
dependencies = [
"bstr",
"gix-utils",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -1088,7 +1049,7 @@ dependencies = [
"gix-utils",
"gix-validate",
"memmap2",
"thiserror 2.0.12",
"thiserror",
"winnow",
]
@@ -1103,7 +1064,7 @@ dependencies = [
"gix-revision",
"gix-validate",
"smallvec",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -1118,7 +1079,7 @@ dependencies = [
"gix-hash",
"gix-object",
"gix-revwalk",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -1133,7 +1094,7 @@ dependencies = [
"gix-hashtable",
"gix-object",
"smallvec",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -1157,7 +1118,7 @@ dependencies = [
"bstr",
"gix-hash",
"gix-lock",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -1180,7 +1141,7 @@ dependencies = [
"gix-pathspec",
"gix-worktree",
"portable-atomic",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -1195,7 +1156,7 @@ dependencies = [
"gix-pathspec",
"gix-refspec",
"gix-url",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -1231,7 +1192,7 @@ dependencies = [
"gix-quote",
"gix-sec",
"gix-url",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -1248,7 +1209,7 @@ dependencies = [
"gix-object",
"gix-revwalk",
"smallvec",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -1261,7 +1222,7 @@ dependencies = [
"gix-features",
"gix-path",
"percent-encoding",
"thiserror 2.0.12",
"thiserror",
"url",
]
@@ -1283,7 +1244,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77b9e00cacde5b51388d28ed746c493b18a6add1f19b5e01d686b3b9ece66d4d"
dependencies = [
"bstr",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -1447,7 +1408,7 @@ dependencies = [
"serde",
"serde_json",
"slotmap",
"thiserror 2.0.12",
"thiserror",
"tokio",
"tokio-stream",
]
@@ -1501,7 +1462,7 @@ dependencies = [
"serde",
"serde_json",
"slotmap",
"thiserror 2.0.12",
"thiserror",
"tokio",
"tokio-stream",
]
@@ -1531,7 +1492,7 @@ dependencies = [
"regex-automata",
"regex-cursor",
"ropey",
"rustix 1.0.7",
"rustix",
"tempfile",
"unicode-segmentation",
"which",
@@ -1546,7 +1507,6 @@ dependencies = [
"arc-swap",
"chrono",
"content_inspector",
"crossterm",
"fern",
"futures-util",
"grep-regex",
@@ -1576,8 +1536,9 @@ dependencies = [
"signal-hook-tokio",
"smallvec",
"tempfile",
"termina",
"termini",
"thiserror 2.0.12",
"thiserror",
"tokio",
"tokio-stream",
"toml",
@@ -1590,11 +1551,11 @@ version = "25.1.1"
dependencies = [
"bitflags",
"cassowary",
"crossterm",
"helix-core",
"helix-view",
"log",
"once_cell",
"termina",
"termini",
"unicode-segmentation",
]
@@ -1624,7 +1585,6 @@ dependencies = [
"bitflags",
"chardetng",
"clipboard-win",
"crossterm",
"futures-util",
"helix-core",
"helix-dap",
@@ -1638,12 +1598,13 @@ dependencies = [
"log",
"once_cell",
"parking_lot",
"rustix 1.0.7",
"rustix",
"serde",
"serde_json",
"slotmap",
"tempfile",
"thiserror 2.0.12",
"termina",
"thiserror",
"tokio",
"tokio-stream",
"toml",
@@ -2009,12 +1970,6 @@ dependencies = [
"zlib-rs",
]
[[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"
@@ -2095,7 +2050,6 @@ checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
dependencies = [
"hermit-abi",
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.52.0",
]
@@ -2381,19 +2335,6 @@ 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.14",
"windows-sys 0.59.0",
]
[[package]]
name = "rustix"
version = "1.0.7"
@@ -2403,7 +2344,7 @@ dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys 0.9.2",
"linux-raw-sys",
"windows-sys 0.59.0",
]
@@ -2512,17 +2453,6 @@ 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"
@@ -2644,7 +2574,20 @@ dependencies = [
"fastrand",
"getrandom 0.3.1",
"once_cell",
"rustix 1.0.7",
"rustix",
"windows-sys 0.59.0",
]
[[package]]
name = "termina"
version = "0.1.0-beta.1"
source = "git+https://github.com/helix-editor/termina?rev=3f861342e0de0b8f0cead5248b41083d8b3506da#3f861342e0de0b8f0cead5248b41083d8b3506da"
dependencies = [
"bitflags",
"futures-core",
"parking_lot",
"rustix",
"signal-hook",
"windows-sys 0.59.0",
]
@@ -2668,33 +2611,13 @@ dependencies = [
"unicode-width 0.2.0",
]
[[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.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl 2.0.12",
]
[[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]]
@@ -2852,7 +2775,7 @@ dependencies = [
"libloading",
"regex-cursor",
"ropey",
"thiserror 2.0.12",
"thiserror",
]
[[package]]
@@ -3038,26 +2961,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d"
dependencies = [
"env_home",
"rustix 1.0.7",
"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"
@@ -3067,12 +2974,6 @@ 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

@@ -50,6 +50,10 @@ parking_lot = "0.12"
futures-executor = "0.3"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
tokio-stream = "0.1.17"
# TODO: publish v0.1.0.
# termina = "0.1.0-beta.1"
termina = { git = "https://github.com/helix-editor/termina", rev = "3f861342e0de0b8f0cead5248b41083d8b3506da" }
# termina.path = "../termina"
[workspace.package]
version = "25.1.1"

View File

@@ -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 }
@@ -95,9 +95,6 @@ grep-searcher = "0.1.14"
signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
libc = "0.2.174"
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { version = "0.28", features = ["event-stream", "use-dev-tty", "libc"] }
[build-dependencies]
helix-loader = { path = "../helix-loader" }

View File

@@ -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);
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() {
@@ -367,7 +371,7 @@ impl Application {
ConfigEvent::Update(editor_config) => {
let mut app_config = (*self.config.load().clone()).clone();
app_config.editor = *editor_config;
if let Err(err) = self.terminal.reconfigure(app_config.editor.clone().into()) {
if let Err(err) = self.terminal.reconfigure((&app_config.editor).into()) {
self.editor.set_error(err.to_string());
};
self.config.store(Arc::new(app_config));
@@ -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();
@@ -412,8 +420,7 @@ impl Application {
document.replace_diagnostics(diagnostics, &[], None);
}
self.terminal
.reconfigure(default_config.editor.clone().into())?;
self.terminal.reconfigure((&default_config.editor).into())?;
// Store new config
self.config.store(Arc::new(default_config));
Ok(())
@@ -430,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()
@@ -503,7 +510,7 @@ impl Application {
// https://github.com/neovim/neovim/issues/12322
// https://github.com/neovim/neovim/pull/13084
for retries in 1..=10 {
match self.claim_term().await {
match self.terminal.claim() {
Ok(()) => break,
Err(err) if retries == 10 => panic!("Failed to claim terminal: {}", err),
Err(_) => continue,
@@ -635,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,
@@ -643,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");
@@ -653,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),
@@ -1099,36 +1106,48 @@ impl Application {
lsp::ShowDocumentResult { success: true }
}
async fn claim_term(&mut self) -> std::io::Result<()> {
let terminal_config = self.config.load().editor.clone().into();
self.terminal.claim(terminal_config)
}
fn restore_term(&mut self) -> std::io::Result<()> {
let terminal_config = self.config.load().editor.clone().into();
use helix_view::graphics::CursorKind;
self.terminal
.backend_mut()
.show_cursor(CursorKind::Block)
.ok();
self.terminal.restore(terminal_config)
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.claim_term().await?;
// 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.terminal.claim()?;
self.event_loop(input_stream).await;

View File

@@ -1,11 +1,11 @@
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::io::Write;
use std::io::{IsTerminal, Write};
use termina::{
style::{ColorSpec, StyleExt as _, Stylized},
Terminal as _,
};
#[derive(Copy, Clone)]
pub enum TsFeature {
@@ -160,21 +160,24 @@ pub fn languages_all() -> 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)))?;
@@ -187,10 +190,10 @@ pub fn languages_all() -> 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)));
@@ -217,8 +220,8 @@ pub fn languages_all() -> 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))?,
}
}

View File

@@ -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;
@@ -149,8 +148,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)
}

View File

@@ -505,7 +505,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));
}
@@ -513,7 +513,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));
}
@@ -1597,7 +1597,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)
}
}

View File

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

View File

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

View File

@@ -14,10 +14,7 @@ use crossterm::{
terminal::{self, Clear, ClearType},
Command,
};
use helix_view::{
editor::Config as EditorConfig,
graphics::{Color, CursorKind, Modifier, Rect, UnderlineStyle},
};
use helix_view::graphics::{Color, CursorKind, Modifier, Rect, UnderlineStyle};
use once_cell::sync::OnceCell;
use std::{
fmt,
@@ -74,17 +71,17 @@ impl Capabilities {
/// 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: &EditorConfig) -> Self {
pub fn from_env_or_default(config: &Config) -> Self {
match termini::TermInfo::from_env() {
Err(_) => Capabilities {
has_extended_underlines: config.undercurl,
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.undercurl
has_extended_underlines: config.force_enable_extended_underlines
|| t.extended_cap("Smulx").is_some()
|| t.extended_cap("Su").is_some()
|| vte_version() >= Some(5102)
@@ -98,6 +95,7 @@ impl Capabilities {
/// 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,
@@ -108,14 +106,15 @@ impl<W> CrosstermBackend<W>
where
W: Write,
{
pub fn new(buffer: W, config: &EditorConfig) -> CrosstermBackend<W> {
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),
capabilities: Capabilities::from_env_or_default(&config),
config,
supports_keyboard_enhancement_protocol: OnceCell::new(),
mouse_capture_enabled: false,
supports_bracketed_paste: true,
@@ -157,7 +156,7 @@ impl<W> Backend for CrosstermBackend<W>
where
W: Write,
{
fn claim(&mut self, config: Config) -> io::Result<()> {
fn claim(&mut self) -> io::Result<()> {
terminal::enable_raw_mode()?;
execute!(
self.buffer,
@@ -173,7 +172,7 @@ where
Ok(_) => (),
};
execute!(self.buffer, terminal::Clear(terminal::ClearType::All))?;
if config.enable_mouse_capture {
if self.config.enable_mouse_capture {
execute!(self.buffer, EnableMouseCapture)?;
self.mouse_capture_enabled = true;
}
@@ -198,15 +197,16 @@ where
}
self.mouse_capture_enabled = config.enable_mouse_capture;
}
self.config = config;
Ok(())
}
fn restore(&mut self, config: Config) -> io::Result<()> {
fn restore(&mut self) -> io::Result<()> {
// reset cursor shape
self.buffer
.write_all(self.capabilities.reset_cursor_command.as_bytes())?;
if config.enable_mouse_capture {
if self.config.enable_mouse_capture {
execute!(self.buffer, DisableMouseCapture)?;
}
if self.supports_keyboard_enhancement_protocol() {

View File

@@ -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;
@@ -17,13 +17,11 @@ pub use self::test::TestBackend;
/// Representation of a terminal backend.
pub trait Backend {
/// Claims the terminal for TUI use.
fn claim(&mut self, config: Config) -> Result<(), io::Error>;
fn claim(&mut self) -> Result<(), io::Error>;
/// Update terminal configuration.
fn reconfigure(&mut self, config: Config) -> Result<(), io::Error>;
/// Restores the terminal to a normal state, undoes `claim`
fn restore(&mut self, config: Config) -> Result<(), io::Error>;
/// Forcibly resets the terminal, ignoring errors and configuration
fn force_restore() -> Result<(), io::Error>;
fn restore(&mut self) -> Result<(), io::Error>;
/// Draws styled text to the terminal
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
where
@@ -32,8 +30,6 @@ pub trait Backend {
fn hide_cursor(&mut self) -> Result<(), io::Error>;
/// Sets the cursor to the given shape
fn show_cursor(&mut self, kind: CursorKind) -> Result<(), io::Error>;
/// Gets the current position of the cursor
fn get_cursor(&mut self) -> Result<(u16, u16), io::Error>;
/// Sets the cursor to the given position
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error>;
/// Clears the terminal
@@ -42,4 +38,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;
}

View File

@@ -0,0 +1,622 @@
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()?;
// TODO: tune this poll constant?
let device_attributes = |event: &Event| {
matches!(
event,
Event::Csi(Csi::Device(csi::Device::DeviceAttributes(_)))
)
};
if terminal.poll(device_attributes, Some(Duration::from_millis(20)))? {
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 20ms. 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 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) || 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
}

View File

@@ -107,7 +107,7 @@ impl TestBackend {
}
impl Backend for TestBackend {
fn claim(&mut self, _config: Config) -> Result<(), io::Error> {
fn claim(&mut self) -> Result<(), io::Error> {
Ok(())
}
@@ -115,11 +115,7 @@ impl Backend for TestBackend {
Ok(())
}
fn restore(&mut self, _config: Config) -> Result<(), io::Error> {
Ok(())
}
fn force_restore() -> Result<(), io::Error> {
fn restore(&mut self) -> Result<(), io::Error> {
Ok(())
}
@@ -143,10 +139,6 @@ impl Backend for TestBackend {
Ok(())
}
fn get_cursor(&mut self) -> Result<(u16, u16), io::Error> {
Ok(self.pos)
}
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error> {
self.pos = (x, y);
Ok(())
@@ -164,4 +156,8 @@ impl Backend for TestBackend {
fn flush(&mut self) -> Result<(), io::Error> {
Ok(())
}
fn supports_true_color(&self) -> bool {
false
}
}

View File

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

View File

@@ -24,12 +24,14 @@ pub struct Viewport {
#[derive(Debug)]
pub struct Config {
pub enable_mouse_capture: bool,
pub force_enable_extended_underlines: bool,
}
impl From<EditorConfig> for Config {
fn from(config: EditorConfig) -> Self {
impl From<&EditorConfig> for Config {
fn from(config: &EditorConfig) -> Self {
Self {
enable_mouse_capture: config.mouse,
force_enable_extended_underlines: config.undercurl,
}
}
}
@@ -102,16 +104,16 @@ where
})
}
pub fn claim(&mut self, config: Config) -> io::Result<()> {
self.backend.claim(config)
pub fn claim(&mut self) -> io::Result<()> {
self.backend.claim()
}
pub fn reconfigure(&mut self, config: Config) -> io::Result<()> {
self.backend.reconfigure(config)
}
pub fn restore(&mut self, config: Config) -> io::Result<()> {
self.backend.restore(config)
pub fn restore(&mut self) -> io::Result<()> {
self.backend.restore()
}
// /// Get a Frame object which provides a consistent view into the terminal state for rendering.
@@ -218,10 +220,6 @@ where
Ok(())
}
pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
self.backend.get_cursor()
}
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.backend.set_cursor(x, y)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,6 @@
pub mod macros;
pub mod annotations;
pub mod base64;
pub mod clipboard;
pub mod document;
pub mod editor;