summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorConrad Ludgate <conradludgate@gmail.com>2023-02-10 17:25:43 +0000
committerGitHub <noreply@github.com>2023-02-10 17:25:43 +0000
commitedda1b741a4a0816eb6e62eafd69fc9896603cf5 (patch)
treecc5cb45caecc4fbe6b34e08f2347fdfdf897d0b5
parenta22ff76be57e74b8189e83e878431d25f34446ec (diff)
crossterm support (#331)
* crossterm v2 * patch crossterm * fix-version * no more tui dependency * lints
-rw-r--r--Cargo.lock104
-rw-r--r--Cargo.toml8
-rw-r--r--atuin-client/src/import/zsh_histdb.rs2
-rw-r--r--src/command/client/search.rs1
-rw-r--r--src/command/client/search/event.rs70
-rw-r--r--src/command/client/search/history_list.rs4
-rw-r--r--src/command/client/search/interactive.rs191
-rw-r--r--src/main.rs1
-rw-r--r--src/tui/LICENSE21
-rw-r--r--src/tui/README.md5
-rw-r--r--src/tui/backend/crossterm.rs221
-rw-r--r--src/tui/backend/mod.rs20
-rw-r--r--src/tui/buffer.rs732
-rw-r--r--src/tui/layout.rs537
-rw-r--r--src/tui/mod.rs20
-rw-r--r--src/tui/style.rs278
-rw-r--r--src/tui/symbols.rs233
-rw-r--r--src/tui/terminal.rs321
-rw-r--r--src/tui/text.rs428
-rw-r--r--src/tui/widgets/block.rs562
-rw-r--r--src/tui/widgets/mod.rs159
-rw-r--r--src/tui/widgets/paragraph.rs194
-rw-r--r--src/tui/widgets/reflow.rs537
23 files changed, 4468 insertions, 181 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 2b3fd3d8..f6f768a3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -77,11 +77,14 @@ dependencies = [
"atuin-common",
"atuin-server",
"base64 0.20.0",
+ "bitflags",
+ "cassowary",
"chrono",
"clap",
"clap_complete",
"cli-table",
"crossbeam-channel",
+ "crossterm",
"directories",
"env_logger",
"eyre",
@@ -95,11 +98,10 @@ dependencies = [
"semver",
"serde",
"serde_json",
- "termion",
"tiny-bip39",
"tokio",
"tracing-subscriber",
- "tui",
+ "unicode-segmentation",
"unicode-width",
"whoami",
]
@@ -489,6 +491,32 @@ dependencies = [
]
[[package]]
+name = "crossterm"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77f67c7faacd4db07a939f55d66a983a5355358a1f17d32cc9a8d01d1266b9ce"
+dependencies = [
+ "bitflags",
+ "crossterm_winapi",
+ "filedescriptor",
+ "libc",
+ "mio",
+ "parking_lot 0.12.1",
+ "signal-hook",
+ "signal-hook-mio",
+ "winapi",
+]
+
+[[package]]
+name = "crossterm_winapi"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -628,6 +656,17 @@ dependencies = [
]
[[package]]
+name = "filedescriptor"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e"
+dependencies = [
+ "libc",
+ "thiserror",
+ "winapi",
+]
+
+[[package]]
name = "flume"
version = "0.10.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1295,12 +1334,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
-name = "numtoa"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
-
-[[package]]
name = "once_cell"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1536,15 +1569,6 @@ dependencies = [
]
[[package]]
-name = "redox_termios"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f"
-dependencies = [
- "redox_syscall",
-]
-
-[[package]]
name = "redox_users"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1898,6 +1922,27 @@ dependencies = [
]
[[package]]
+name = "signal-hook"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-mio"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
+dependencies = [
+ "libc",
+ "mio",
+ "signal-hook",
+]
+
+[[package]]
name = "signal-hook-registry"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2153,18 +2198,6 @@ dependencies = [
]
[[package]]
-name = "termion"
-version = "1.5.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e"
-dependencies = [
- "libc",
- "numtoa",
- "redox_syscall",
- "redox_termios",
-]
-
-[[package]]
name = "thiserror"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2418,19 +2451,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
[[package]]
-name = "tui"
-version = "0.19.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1"
-dependencies = [
- "bitflags",
- "cassowary",
- "termion",
- "unicode-segmentation",
- "unicode-width",
-]
-
-[[package]]
name = "typenum"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 88290e8f..10b0fdfd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -56,8 +56,7 @@ directories = "4"
indicatif = "0.17.1"
serde = { version = "1.0.145", features = ["derive"] }
serde_json = "1.0.86"
-tui = { version = "0.19", default-features = false, features = ["termion"] }
-termion = "1.5"
+crossterm = { version = "0.26", features = ["use-dev-tty"] }
unicode-width = "0.1"
itertools = "0.10.5"
tokio = { version = "1", features = ["full"] }
@@ -75,6 +74,11 @@ semver = "1.0.14"
runtime-format = "0.1.2"
tiny-bip39 = "1"
+# from tui
+bitflags = "1.3"
+cassowary = "0.3"
+unicode-segmentation = "1.2"
+
[dependencies.tracing-subscriber]
version = "0.3"
default-features = false
diff --git a/atuin-client/src/import/zsh_histdb.rs b/atuin-client/src/import/zsh_histdb.rs
index 16de2a7f..b9bce34d 100644
--- a/atuin-client/src/import/zsh_histdb.rs
+++ b/atuin-client/src/import/zsh_histdb.rs
@@ -221,7 +221,7 @@ mod test {
println!("h: {:#?}", histdb.histdb);
println!("counter: {:?}", histdb.histdb.len());
for i in histdb.histdb {
- println!("{:?}", i);
+ println!("{i:?}");
}
}
}
diff --git a/src/command/client/search.rs b/src/command/client/search.rs
index 53471ec1..9321f117 100644
--- a/src/command/client/search.rs
+++ b/src/command/client/search.rs
@@ -13,7 +13,6 @@ use super::history::ListMode;
mod cursor;
mod duration;
-mod event;
mod history_list;
mod interactive;
pub use duration::{format_duration, format_duration_into};
diff --git a/src/command/client/search/event.rs b/src/command/client/search/event.rs
deleted file mode 100644
index 0e791c96..00000000
--- a/src/command/client/search/event.rs
+++ /dev/null
@@ -1,70 +0,0 @@
-use std::{thread, time::Duration};
-
-use crossbeam_channel::unbounded;
-use termion::{event::Event as TermEvent, event::Key, input::TermRead};
-
-pub enum Event<I> {
- Input(I),
- Tick,
-}
-
-/// A small event handler that wrap termion input and tick events. Each event
-/// type is handled in its own thread and returned to a common `Receiver`
-pub struct Events {
- rx: crossbeam_channel::Receiver<Event<TermEvent>>,
-}
-
-#[derive(Debug, Clone, Copy)]
-pub struct Config {
- pub exit_key: Key,
- pub tick_rate: Duration,
-}
-
-impl Default for Config {
- fn default() -> Config {
- Config {
- exit_key: Key::Char('q'),
- tick_rate: Duration::from_millis(250),
- }
- }
-}
-
-impl Events {
- pub fn new() -> Events {
- Events::with_config(Config::default())
- }
-
- pub fn with_config(config: Config) -> Events {
- let (tx, rx) = unbounded();
-
- {
- let tx = tx.clone();
- thread::spawn(move || {
- let tty = termion::get_tty().expect("Could not find tty");
- for event in tty.events().flatten() {
- if let Err(err) = tx.send(Event::Input(event)) {
- eprintln!("{err}");
- return;
- }
- }
- })
- };
-
- thread::spawn(move || loop {
- if tx.send(Event::Tick).is_err() {
- break;
- }
- thread::sleep(config.tick_rate);
- });
-
- Events { rx }
- }
-
- pub fn next(&self) -> Result<Event<TermEvent>, crossbeam_channel::RecvError> {
- self.rx.recv()
- }
-
- pub fn try_next(&self) -> Result<Event<TermEvent>, crossbeam_channel::TryRecvError> {
- self.rx.try_recv()
- }
-}
diff --git a/src/command/client/search/history_list.rs b/src/command/client/search/history_list.rs
index d74221d8..f4725b02 100644
--- a/src/command/client/search/history_list.rs
+++ b/src/command/client/search/history_list.rs
@@ -1,12 +1,12 @@
use std::time::Duration;
-use atuin_client::history::History;
-use tui::{
+use crate::tui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style},
widgets::{Block, StatefulWidget, Widget},
};
+use atuin_client::history::History;
use super::format_duration;
diff --git a/src/command/client/search/interactive.rs b/src/command/client/search/interactive.rs
index e0ceb091..c8ceab58 100644
--- a/src/command/client/search/interactive.rs
+++ b/src/command/client/search/interactive.rs
@@ -1,19 +1,22 @@
-use std::io::stdout;
-
-use eyre::Result;
-use semver::Version;
-use termion::{
- event::Event as TermEvent, event::Key, event::MouseButton, event::MouseEvent,
- input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen,
+use std::{
+ io::{stdout, Write},
+ time::Duration,
};
-use tui::{
- backend::{Backend, TermionBackend},
+
+use crate::tui::{
+ backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::{Span, Spans, Text},
widgets::{Block, BorderType, Borders, Paragraph},
Frame, Terminal,
};
+use crossterm::{
+ event::{self, Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent},
+ execute, terminal,
+};
+use eyre::Result;
+use semver::Version;
use unicode_width::UnicodeWidthStr;
use atuin_client::{
@@ -26,7 +29,6 @@ use atuin_client::{
use super::{
cursor::Cursor,
- event::{Event, Events},
history_list::{HistoryList, ListState, PREFIX_LENGTH},
};
use crate::VERSION;
@@ -62,42 +64,69 @@ impl State {
Ok(results)
}
- fn handle_input(
+ fn handle_input(&mut self, settings: &Settings, input: &Event, len: usize) -> Option<usize> {
+ match input {
+ Event::Key(k) => self.handle_key_input(settings, k, len),
+ Event::Mouse(m) => self.handle_mouse_input(*m, len),
+ _ => None,
+ }
+ }
+
+ fn handle_mouse_input(&mut self, input: MouseEvent, len: usize) -> Option<usize> {
+ match input.kind {
+ event::MouseEventKind::ScrollDown => {
+ let i = self.results_state.selected().saturating_sub(1);
+ self.results_state.select(i);
+ }
+ event::MouseEventKind::ScrollUp => {
+ let i = self.results_state.selected() + 1;
+ self.results_state.select(i.min(len - 1));
+ }
+ _ => {}
+ }
+ None
+ }
+
+ fn handle_key_input(
&mut self,
settings: &Settings,
- input: &TermEvent,
+ input: &KeyEvent,
len: usize,
) -> Option<usize> {
- match input {
- TermEvent::Key(Key::Char('\t')) => {}
- TermEvent::Key(Key::Ctrl('c' | 'd' | 'g')) => return Some(RETURN_ORIGINAL),
- TermEvent::Key(Key::Esc) => {
+ let ctrl = input.modifiers.contains(KeyModifiers::CONTROL);
+ let alt = input.modifiers.contains(KeyModifiers::ALT);
+ match input.code {
+ KeyCode::Char('c' | 'd' | 'g') if ctrl => return Some(RETURN_ORIGINAL),
+ KeyCode::Esc => {
return Some(match settings.exit_mode {
ExitMode::ReturnOriginal => RETURN_ORIGINAL,
ExitMode::ReturnQuery => RETURN_QUERY,
})
}
- TermEvent::Key(Key::Char('\n')) => {
+ KeyCode::Enter => {
return Some(self.results_state.selected());
}
- TermEvent::Key(Key::Alt(c @ '1'..='9')) => {
+ KeyCode::Char(c @ '1'..='9') if alt => {
let c = c.to_digit(10)? as usize;
return Some(self.results_state.selected() + c);
}
- TermEvent::Key(Key::Left | Key::Ctrl('h')) => {
+ KeyCode::Left => {
self.input.left();
}
- TermEvent::Key(Key::Right | Key::Ctrl('l')) => self.input.right(),
- TermEvent::Key(Key::Ctrl('a') | Key::Home) => self.input.start(),
- TermEvent::Key(Key::Ctrl('e') | Key::End) => self.input.end(),
- TermEvent::Key(Key::Char(c)) => self.input.insert(*c),
- TermEvent::Key(Key::Backspace) => {
+ KeyCode::Char('h') if ctrl => {
+ self.input.left();
+ }
+ KeyCode::Right => self.input.right(),
+ KeyCode::Char('l') if ctrl => self.input.right(),
+ KeyCode::Char('a') if ctrl => self.input.start(),
+ KeyCode::Char('e') if ctrl => self.input.end(),
+ KeyCode::Backspace => {
self.input.back();
}
- TermEvent::Key(Key::Delete) => {
+ KeyCode::Delete => {
self.input.remove();
}
- TermEvent::Key(Key::Ctrl('w')) => {
+ KeyCode::Char('w') if ctrl => {
// remove the first batch of whitespace
while matches!(self.input.back(), Some(c) if c.is_whitespace()) {}
while self.input.left() {
@@ -108,8 +137,8 @@ impl State {
self.input.remove();
}
}
- TermEvent::Key(Key::Ctrl('u')) => self.input.clear(),
- TermEvent::Key(Key::Ctrl('r')) => {
+ KeyCode::Char('u') if ctrl => self.input.clear(),
+ KeyCode::Char('r') if ctrl => {
pub static FILTER_MODES: [FilterMode; 4] = [
FilterMode::Global,
FilterMode::Host,
@@ -120,19 +149,24 @@ impl State {
let i = (i + 1) % FILTER_MODES.len();
self.filter_mode = FILTER_MODES[i];
}
- TermEvent::Key(Key::Down | Key::Ctrl('n' | 'j'))
- | TermEvent::Mouse(MouseEvent::Press(MouseButton::WheelDown, _, _)) => {
- if self.results_state.selected() == 0 && input.eq(&TermEvent::Key(Key::Down)) {
- return Some(RETURN_ORIGINAL);
- }
+ KeyCode::Down if self.results_state.selected() == 0 => return Some(RETURN_ORIGINAL),
+ KeyCode::Down => {
+ let i = self.results_state.selected().saturating_sub(1);
+ self.results_state.select(i);
+ }
+ KeyCode::Char('n' | 'j') if ctrl => {
let i = self.results_state.selected().saturating_sub(1);
self.results_state.select(i);
}
- TermEvent::Key(Key::Up | Key::Ctrl('p' | 'k'))
- | TermEvent::Mouse(MouseEvent::Press(MouseButton::WheelUp, _, _)) => {
+ KeyCode::Up => {
+ let i = self.results_state.selected() + 1;
+ self.results_state.select(i.min(len - 1));
+ }
+ KeyCode::Char('p' | 'k') if ctrl => {
let i = self.results_state.selected() + 1;
self.results_state.select(i.min(len - 1));
}
+ KeyCode::Char(c) => self.input.insert(c),
_ => {}
};
@@ -303,6 +337,45 @@ impl State {
}
}
+struct Stdout {
+ stdout: std::io::Stdout,
+}
+
+impl Stdout {
+ pub fn new() -> std::io::Result<Self> {
+ terminal::enable_raw_mode()?;
+ let mut stdout = stdout();
+ execute!(
+ stdout,
+ terminal::EnterAlternateScreen,
+ event::EnableMouseCapture
+ )?;
+ Ok(Self { stdout })
+ }
+}
+
+impl Drop for Stdout {
+ fn drop(&mut self) {
+ execute!(
+ self.stdout,
+ terminal::LeaveAlternateScreen,
+ event::DisableMouseCapture
+ )
+ .unwrap();
+ terminal::disable_raw_mode().unwrap();
+ }
+}
+
+impl Write for Stdout {
+ fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
+ self.stdout.write(buf)
+ }
+
+ fn flush(&mut self) -> std::io::Result<()> {
+ self.stdout.flush()
+ }
+}
+
// this is a big blob of horrible! clean it up!
// for now, it works. But it'd be great if it were more easily readable, and
// modular. I'd like to add some more stats and stuff at some point
@@ -312,15 +385,10 @@ pub async fn history(
settings: &Settings,
db: &mut impl Database,
) -> Result<String> {
- let stdout = stdout().into_raw_mode()?;
- let stdout = MouseTerminal::from(stdout);
- let stdout = AlternateScreen::from(stdout);
- let backend = TermionBackend::new(stdout);
+ let stdout = Stdout::new()?;
+ let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
- // Setup event handlers
- let events = Events::new();
-
let mut input = Cursor::from(query.join(" "));
// Put the cursor at the end of the query by default
input.end();
@@ -343,27 +411,6 @@ pub async fn history(
let mut results = app.query_results(settings.search_mode, db).await?;
let index = 'render: loop {
- let initial_input = app.input.as_str().to_owned();
- let initial_filter_mode = app.filter_mode;
-
- // Handle input
- if let Event::Input(input) = events.next()? {
- if let Some(i) = app.handle_input(settings, &input, results.len()) {
- break 'render i;
- }
- }
-
- // After we receive input process the whole event channel before query/render.
- while let Ok(Event::Input(input)) = events.try_next() {
- if let Some(i) = app.handle_input(settings, &input, results.len()) {
- break 'render i;
- }
- }
-
- if initial_input != app.input.as_str() || initial_filter_mode != app.filter_mode {
- results = app.query_results(settings.search_mode, db).await?;
- }
-
let compact = match settings.style {
atuin_client::settings::Style::Auto => {
terminal.size().map(|size| size.height < 14).unwrap_or(true)
@@ -376,6 +423,24 @@ pub async fn history(
} else {
terminal.draw(|f| app.draw(f, &results))?;
}
+
+ let initial_input = app.input.as_str().to_owned();
+ let initial_filter_mode = app.filter_mode;
+
+ if event::poll(Duration::from_millis(250))? {
+ loop {
+ if let Some(i) = app.handle_input(settings, &event::read()?, results.len()) {
+ break 'render i;
+ }
+ if !event::poll(Duration::ZERO)? {
+ break;
+ }
+ }
+ }
+
+ if initial_input != app.input.as_str() || initial_filter_mode != app.filter_mode {
+ results = app.query_results(settings.search_mode, db).await?;
+ }
};
if index < results.len() {
diff --git a/src/main.rs b/src/main.rs
index 2f81f4fc..3004e0b1 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -6,6 +6,7 @@ use eyre::Result;
use command::AtuinCmd;
mod command;
+mod tui;
const VERSION: &str = env!("CARGO_PKG_VERSION");
diff --git a/src/tui/LICENSE b/src/tui/LICENSE
new file mode 100644
index 00000000..7a0657cb
--- /dev/null
+++ b/src/tui/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Florian Dehau
+
+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.
diff --git a/src/tui/README.md b/src/tui/README.md
new file mode 100644
index 00000000..506bdf8f
--- /dev/null
+++ b/src/tui/README.md
@@ -0,0 +1,5 @@
+# tui-rs
+
+A fork of https://crates.io/crates/tui/0.19.0 since it is now unmaintained.
+
+Some parts have been removed or modified for simplicity, but it is currently mostly equivalent.
diff --git a/src/tui/backend/crossterm.rs b/src/tui/backend/crossterm.rs
new file mode 100644
index 00000000..2cbfd6e0
--- /dev/null
+++ b/src/tui/backend/crossterm.rs
@@ -0,0 +1,221 @@
+use crate::tui::{
+ backend::Backend,
+ buffer::Cell,
+ layout::Rect,
+ style::{Color, Modifier},
+};
+use crossterm::{
+ cursor::{Hide, MoveTo, Show},
+ execute, queue,
+ style::{
+ Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
+ SetForegroundColor,
+ },
+ terminal::{self, Clear, ClearType},
+};
+use std::io::{self, Write};
+
+pub struct CrosstermBackend<W: Write> {
+ buffer: W,
+}
+
+impl<W> CrosstermBackend<W>
+where
+ W: Write,
+{
+ pub fn new(buffer: W) -> CrosstermBackend<W> {
+ CrosstermBackend { buffer }
+ }
+}
+
+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 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 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) {
+ map_error(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 {
+ let color = CColor::from(cell.fg);
+ map_error(queue!(self.buffer, SetForegroundColor(color)))?;
+ fg = cell.fg;
+ }
+ if cell.bg != bg {
+ let color = CColor::from(cell.bg);
+ map_error(queue!(self.buffer, SetBackgroundColor(color)))?;
+ bg = cell.bg;
+ }
+
+ map_error(queue!(self.buffer, Print(&cell.symbol)))?;
+ }
+
+ map_error(queue!(
+ self.buffer,
+ SetForegroundColor(CColor::Reset),
+ SetBackgroundColor(CColor::Reset),
+ SetAttribute(CAttribute::Reset)
+ ))
+ }
+
+ fn hide_cursor(&mut self) -> io::Result<()> {
+ map_error(execute!(self.buffer, Hide))
+ }
+
+ fn show_cursor(&mut self) -> io::Result<()> {
+ map_error(execute!(self.buffer, Show))
+ }
+
+ 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<()> {
+ map_error(execute!(self.buffer, MoveTo(x, y)))
+ }
+
+ fn clear(&mut self) -> io::Result<()> {
+ map_error(execute!(self.buffer, Clear(ClearType::All)))
+ }
+
+ fn size(&self) -> io::Result<Rect> {
+ let (width, height) =
+ terminal::size().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
+
+ Ok(Rect::new(0, 0, width, height))
+ }
+
+ fn flush(&mut self) -> io::Result<()> {
+ self.buffer.flush()
+ }
+}
+
+fn map_error(error: crossterm::Result<()>) -> io::Result<()> {
+ error.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
+}
+
+impl From<Color> for CColor {
+ fn from(color: Color) -> Self {
+ 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::Grey,
+ Color::DarkGray => 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::White => CColor::White,
+ Color::Indexed(i) => CColor::AnsiValue(i),
+ Color::Rgb(r, g, b) => CColor::Rgb { r, g, b },
+ }
+ }
+}
+
+#[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) {
+ map_error(queue!(w, SetAttribute(CAttribute::NoReverse)))?;
+ }
+ if removed.contains(Modifier::BOLD) {
+ map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
+ if self.to.contains(Modifier::DIM) {
+ map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
+ }
+ }