diff options
author | Conrad Ludgate <conradludgate@gmail.com> | 2023-02-10 17:25:43 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-02-10 17:25:43 +0000 |
commit | edda1b741a4a0816eb6e62eafd69fc9896603cf5 (patch) | |
tree | cc5cb45caecc4fbe6b34e08f2347fdfdf897d0b5 | |
parent | a22ff76be57e74b8189e83e878431d25f34446ec (diff) |
crossterm support (#331)
* crossterm v2
* patch crossterm
* fix-version
* no more tui dependency
* lints
-rw-r--r-- | Cargo.lock | 104 | ||||
-rw-r--r-- | Cargo.toml | 8 | ||||
-rw-r--r-- | atuin-client/src/import/zsh_histdb.rs | 2 | ||||
-rw-r--r-- | src/command/client/search.rs | 1 | ||||
-rw-r--r-- | src/command/client/search/event.rs | 70 | ||||
-rw-r--r-- | src/command/client/search/history_list.rs | 4 | ||||
-rw-r--r-- | src/command/client/search/interactive.rs | 191 | ||||
-rw-r--r-- | src/main.rs | 1 | ||||
-rw-r--r-- | src/tui/LICENSE | 21 | ||||
-rw-r--r-- | src/tui/README.md | 5 | ||||
-rw-r--r-- | src/tui/backend/crossterm.rs | 221 | ||||
-rw-r--r-- | src/tui/backend/mod.rs | 20 | ||||
-rw-r--r-- | src/tui/buffer.rs | 732 | ||||
-rw-r--r-- | src/tui/layout.rs | 537 | ||||
-rw-r--r-- | src/tui/mod.rs | 20 | ||||
-rw-r--r-- | src/tui/style.rs | 278 | ||||
-rw-r--r-- | src/tui/symbols.rs | 233 | ||||
-rw-r--r-- | src/tui/terminal.rs | 321 | ||||
-rw-r--r-- | src/tui/text.rs | 428 | ||||
-rw-r--r-- | src/tui/widgets/block.rs | 562 | ||||
-rw-r--r-- | src/tui/widgets/mod.rs | 159 | ||||
-rw-r--r-- | src/tui/widgets/paragraph.rs | 194 | ||||
-rw-r--r-- | src/tui/widgets/reflow.rs | 537 |
23 files changed, 4468 insertions, 181 deletions
@@ -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" @@ -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)))?; + } + } |