Unnamed repository; edit this file 'description' to name the repository.
| -rw-r--r-- | Cargo.lock | 232 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | helix-term/Cargo.toml | 7 | ||||
| -rw-r--r-- | helix-term/src/application.rs | 94 | ||||
| -rw-r--r-- | helix-term/src/health.rs | 38 | ||||
| -rw-r--r-- | helix-term/src/main.rs | 4 | ||||
| -rw-r--r-- | helix-term/src/ui/editor.rs | 6 | ||||
| -rw-r--r-- | helix-term/tests/test/helpers.rs | 2 | ||||
| -rw-r--r-- | helix-tui/Cargo.toml | 4 | ||||
| -rw-r--r-- | helix-tui/src/backend/crossterm.rs | 465 | ||||
| -rw-r--r-- | helix-tui/src/backend/mod.rs | 11 | ||||
| -rw-r--r-- | helix-tui/src/backend/termina.rs | 645 | ||||
| -rw-r--r-- | helix-tui/src/backend/test.rs | 8 | ||||
| -rw-r--r-- | helix-tui/src/lib.rs | 130 | ||||
| -rw-r--r-- | helix-view/Cargo.toml | 4 | ||||
| -rw-r--r-- | helix-view/src/base64.rs | 163 | ||||
| -rw-r--r-- | helix-view/src/clipboard.rs | 52 | ||||
| -rw-r--r-- | helix-view/src/graphics.rs | 56 | ||||
| -rw-r--r-- | helix-view/src/input.rs | 83 | ||||
| -rw-r--r-- | helix-view/src/keyboard.rs | 48 | ||||
| -rw-r--r-- | helix-view/src/lib.rs | 1 |
21 files changed, 921 insertions, 1133 deletions
@@ -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" @@ -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. -//! -//!  -//! -//! # 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; |