From 16f8e3ad527787db8b7ae707f0c56f195bd2b04b Mon Sep 17 00:00:00 2001 From: Lutetium-Vanadium Date: Thu, 29 Apr 2021 13:39:49 +0530 Subject: [PATCH] Extract crossterm into a backend --- .rustfmt.toml | 1 + .vim/coc-settings.json | 6 + Cargo.toml | 30 +-- src/lib.rs | 83 +++++---- src/question/checkbox.rs | 137 ++++++++------ src/question/confirm.rs | 72 +++++--- src/question/editor.rs | 79 ++++---- src/question/expand.rs | 163 +++++++++-------- src/question/input.rs | 108 ++++++----- src/question/list.rs | 78 ++++---- src/question/mod.rs | 81 +++++---- src/question/number.rs | 144 ++++++++------- src/question/password.rs | 98 ++++++---- src/question/plugin.rs | 26 ++- src/question/rawlist.rs | 159 ++++++++-------- ui/Cargo.toml | 29 ++- ui/src/async_input.rs | 254 +++++--------------------- ui/src/backend/crossterm.rs | 219 ++++++++++++++++++++++ ui/src/backend/curses.rs | 286 +++++++++++++++++++++++++++++ ui/src/backend/mod.rs | 155 ++++++++++++++++ ui/src/backend/style.rs | 351 ++++++++++++++++++++++++++++++++++++ ui/src/backend/termion.rs | 264 +++++++++++++++++++++++++++ ui/src/char_input.rs | 32 ++-- {src => ui/src}/error.rs | 40 ++-- ui/src/events/crossterm.rs | 49 +++++ ui/src/events/mod.rs | 112 ++++++++++++ ui/src/events/unix.rs | 134 ++++++++++++++ ui/src/events/win.rs | 77 ++++++++ ui/src/lib.rs | 45 ++--- ui/src/list.rs | 92 +++++----- ui/src/string_input.rs | 83 +++++---- ui/src/sync_input.rs | 296 +++++++++++++++--------------- ui/src/widget.rs | 26 ++- 33 files changed, 2768 insertions(+), 1041 deletions(-) create mode 100644 .rustfmt.toml create mode 100644 .vim/coc-settings.json create mode 100644 ui/src/backend/crossterm.rs create mode 100644 ui/src/backend/curses.rs create mode 100644 ui/src/backend/mod.rs create mode 100644 ui/src/backend/style.rs create mode 100644 ui/src/backend/termion.rs rename {src => ui/src}/error.rs (50%) create mode 100644 ui/src/events/crossterm.rs create mode 100644 ui/src/events/mod.rs create mode 100644 ui/src/events/unix.rs create mode 100644 ui/src/events/win.rs diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..3c67608 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1 @@ +max_width = 85 diff --git a/.vim/coc-settings.json b/.vim/coc-settings.json new file mode 100644 index 0000000..2f18b70 --- /dev/null +++ b/.vim/coc-settings.json @@ -0,0 +1,6 @@ +{ + "rust-analyzer.diagnostics.disabled": [ + "incorrect-ident-case", + "inactive-code" + ] +} diff --git a/Cargo.toml b/Cargo.toml index 8e7f9bd..01302b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,22 +7,30 @@ edition = "2018" [workspace] members = [ ".", - "ui", + "ui" ] [dependencies] -ahash = { version = "0.7", optional = true } -async-trait = { version = "0.1", optional = true } -crossterm = "0.19" tempfile = "3" +atty = "0.2" ui = { path = "./ui" } -smol = { version = "1.2", optional = true } -tokio = { version = "1.5", optional = true, features = ["fs", "process", "io-util"] } -async-std = { version = "1.9", optional = true, features = ["unstable"] } +ahash = { version = "0.7", optional = true } +async-trait = { version = "0.1", optional = true } +futures = { version = "0.3", optional = true } + +# The following dependencies are renamed in the form `-dep` to allow +# a feature name with the same name to be present. This is necessary since +# these features are required to enable other dependencies +smol-dep = { package = "smol", version = "1.2", optional = true } +tokio-dep = { package = "tokio", version = "1.5", optional = true, features = ["fs", "process", "io-util"] } +async-std-dep = { package = "async-std", version = "1.9", optional = true, features = ["unstable"] } [features] -default = ["ahash"] -async_tokio = ["tokio", "async-trait", "ui/async"] -async_smol = ["smol", "async-trait", "ui/async"] -async_std = ["async-std", "async-trait", "ui/async"] +default = ["crossterm", "ahash"] +crossterm = ["ui/crossterm"] +# termion = ["ui/termion"] +# curses = ["ui/curses"] +tokio = ["tokio-dep", "async-trait", "ui/tokio"] +smol = ["smol-dep", "async-trait", "ui/smol"] +async-std = ["async-std-dep", "async-trait", "ui/async-std"] diff --git a/src/lib.rs b/src/lib.rs index 09431e9..dbd15d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,14 @@ mod answer; -mod error; mod question; -use crossterm::tty::IsTty; +use ui::{backend, error, events}; pub use answer::{Answer, Answers, ExpandItem, ListItem}; -pub use question::{Choice::Choice, Choice::Separator, Plugin, Question}; +pub use question::{Choice::Choice, Choice::Separator, Question}; +pub use ui::error::{ErrorKind, Result}; pub mod plugin { - pub use crate::Plugin; + pub use crate::question::Plugin; pub use ui; } @@ -37,12 +37,15 @@ where self } - fn prompt_impl( + fn prompt_impl( &mut self, - stdout: &mut W, + stdout: &mut B, + events: &mut events::Events, ) -> error::Result> { while let Some(question) = self.questions.next() { - if let Some((name, answer)) = question.ask(&self.answers, stdout)? { + if let Some((name, answer)) = + question.ask(&self.answers, stdout, events)? + { return Ok(Some(self.answers.insert(name, answer))); } } @@ -51,35 +54,39 @@ where } pub fn prompt(&mut self) -> error::Result> { - let stdout = std::io::stdout(); - if !stdout.is_tty() { - return Err(error::InquirerError::NotATty); + if atty::isnt(atty::Stream::Stdout) { + return Err(error::ErrorKind::NotATty); } - let mut stdout = stdout.lock(); - self.prompt_impl(&mut stdout) + let stdout = std::io::stdout(); + let mut stdout = backend::get_backend(stdout.lock())?; + + self.prompt_impl(&mut stdout, &mut events::Events::new()) } pub fn prompt_all(mut self) -> error::Result { self.answers.reserve(self.questions.size_hint().0); - let stdout = std::io::stdout(); - if !stdout.is_tty() { - return Err(error::InquirerError::NotATty); + if atty::isnt(atty::Stream::Stdout) { + return Err(error::ErrorKind::NotATty); } - let mut stdout = stdout.lock(); + let stdout = std::io::stdout(); + let mut stdout = backend::get_backend(stdout.lock())?; - while self.prompt_impl(&mut stdout)?.is_some() {} + let mut events = events::Events::new(); + + while self.prompt_impl(&mut stdout, &mut events)?.is_some() {} Ok(self.answers) } cfg_async! { - async fn prompt_impl_async( + async fn prompt_impl_async( &mut self, - stdout: &mut W, + stdout: &mut B, + events: &mut events::AsyncEvents, ) -> error::Result> { while let Some(question) = self.questions.next() { - if let Some((name, answer)) = question.ask_async(&self.answers, stdout).await? { + if let Some((name, answer)) = question.ask_async(&self.answers, stdout, events).await? { return Ok(Some(self.answers.insert(name, answer))); } } @@ -88,24 +95,27 @@ where } pub async fn prompt_async(&mut self) -> error::Result> { - let stdout = std::io::stdout(); - if !stdout.is_tty() { - return Err(error::InquirerError::NotATty); + if atty::isnt(atty::Stream::Stdout) { + return Err(error::ErrorKind::NotATty); } - let mut stdout = stdout.lock(); - self.prompt_impl_async(&mut stdout).await + let stdout = std::io::stdout(); + let mut stdout = backend::get_backend(stdout.lock())?; + + self.prompt_impl_async(&mut stdout, &mut events::AsyncEvents::new().await?).await } pub async fn prompt_all_async(mut self) -> error::Result { self.answers.reserve(self.questions.size_hint().0); - let stdout = std::io::stdout(); - if !stdout.is_tty() { - return Err(error::InquirerError::NotATty); + if atty::isnt(atty::Stream::Stdout) { + return Err(error::ErrorKind::NotATty); } - let mut stdout = stdout.lock(); + let stdout = std::io::stdout(); + let mut stdout = backend::get_backend(stdout.lock())?; - while self.prompt_impl_async(&mut stdout).await?.is_some() {} + let mut events = events::AsyncEvents::new().await?; + + while self.prompt_impl_async(&mut stdout, &mut events).await?.is_some() {} Ok(self.answers) } @@ -123,12 +133,21 @@ where PromptModule::new(questions).prompt_all() } +cfg_async! { +pub async fn prompt_async<'m, 'w, 'f, 'v, 't, Q>(questions: Q) -> error::Result +where + Q: IntoIterator>, +{ + PromptModule::new(questions).prompt_all_async().await +} +} + /// Sets the exit handler to call when `CTRL+C` or EOF is received /// /// By default, it exits the program, however it can be overridden to not exit. If it doesn't exit, /// [`Input::run`] will return an `Err` pub fn set_exit_handler(handler: fn()) { - ui::set_exit_handler(handler); + plugin::ui::set_exit_handler(handler); } #[doc(hidden)] @@ -136,7 +155,7 @@ pub fn set_exit_handler(handler: fn()) { macro_rules! cfg_async { ($($item:item)*) => { $( - #[cfg(any(feature = "async_tokio", feature = "async_std", feature = "async_smol"))] + #[cfg(any(feature = "tokio", feature = "async-std", feature = "smol"))] $item )* }; diff --git a/src/question/checkbox.rs b/src/question/checkbox.rs index 180660b..8215b4a 100644 --- a/src/question/checkbox.rs +++ b/src/question/checkbox.rs @@ -1,14 +1,14 @@ use std::io; -use crossterm::{ - event, execute, queue, - style::{Color, Print, ResetColor, SetForegroundColor}, +use ui::{ + backend::{Backend, Color}, + error, + events::{KeyCode, KeyEvent}, + widgets, Prompt, Validation, Widget, }; -use ui::{widgets, Prompt, Validation, Widget}; - -use crate::{error, Answer, Answers, ListItem}; use super::{Choice, Filter, Options, Transformer, Validate}; +use crate::{Answer, Answers, ListItem}; #[derive(Debug, Default)] pub struct Checkbox<'f, 'v, 't> { @@ -25,7 +25,10 @@ struct CheckboxPrompt<'f, 'v, 't, 'a> { answers: &'a Answers, } -fn create_list_items(selected: Vec, choices: super::ChoiceList) -> Vec { +fn create_list_items( + selected: Vec, + choices: super::ChoiceList, +) -> Vec { selected .into_iter() .enumerate() @@ -115,24 +118,28 @@ impl ui::AsyncPrompt for CheckboxPrompt<'_, '_, '_, '_> { } impl Widget for CheckboxPrompt<'_, '_, '_, '_> { - fn render(&mut self, max_width: usize, w: &mut W) -> crossterm::Result<()> { - self.picker.render(max_width, w) + fn render( + &mut self, + max_width: usize, + b: &mut B, + ) -> error::Result<()> { + self.picker.render(max_width, b) } fn height(&self) -> usize { self.picker.height() } - fn handle_key(&mut self, key: event::KeyEvent) -> bool { + fn handle_key(&mut self, key: KeyEvent) -> bool { match key.code { - event::KeyCode::Char(' ') => { + KeyCode::Char(' ') => { let index = self.picker.get_at(); self.picker.list.selected[index] = !self.picker.list.selected[index]; } - event::KeyCode::Char('i') => { + KeyCode::Char('i') => { self.picker.list.selected.iter_mut().for_each(|s| *s = !*s); } - event::KeyCode::Char('a') => { + KeyCode::Char('a') => { let select_state = self.picker.list.selected.iter().any(|s| !s); self.picker .list @@ -152,38 +159,40 @@ impl Widget for CheckboxPrompt<'_, '_, '_, '_> { } impl widgets::List for Checkbox<'_, '_, '_> { - fn render_item( + fn render_item( &mut self, index: usize, hovered: bool, max_width: usize, - w: &mut W, - ) -> crossterm::Result<()> { + b: &mut B, + ) -> error::Result<()> { if hovered { - queue!(w, SetForegroundColor(Color::DarkCyan), Print("❯ "))?; + b.set_fg(Color::Cyan)?; + b.write_all("❯ ".as_bytes())?; } else { - w.write_all(b" ")?; + b.write_all(b" ")?; } if self.is_selectable(index) { if self.selected[index] { - queue!(w, SetForegroundColor(Color::Green), Print("◉ "),)?; + b.set_fg(Color::LightGreen)?; + b.write_all("◉ ".as_bytes())?; if hovered { - queue!(w, SetForegroundColor(Color::DarkCyan))?; + b.set_fg(Color::Cyan)?; } else { - queue!(w, ResetColor)?; + b.set_fg(Color::Reset)?; } } else { - w.write_all("◯ ".as_bytes())?; + b.write_all("◯ ".as_bytes())?; } } else { - queue!(w, SetForegroundColor(Color::DarkGrey))?; + b.set_fg(Color::DarkGrey)?; } - self.choices[index].as_str().render(max_width - 4, w)?; + self.choices[index].as_str().render(max_width - 4, b)?; - queue!(w, ResetColor) + b.set_fg(Color::Reset) } fn is_selectable(&self, index: usize) -> bool { @@ -204,30 +213,35 @@ impl widgets::List for Checkbox<'_, '_, '_> { } impl Checkbox<'_, '_, '_> { - pub(crate) fn ask( + pub(crate) fn ask( mut self, message: String, answers: &Answers, - w: &mut W, + b: &mut B, + events: &mut ui::events::Events, ) -> error::Result { let transformer = self.transformer.take(); - let ans = ui::Input::new(CheckboxPrompt { - message, - picker: widgets::ListPicker::new(self), - answers, - }) + let ans = ui::Input::new( + CheckboxPrompt { + message, + picker: widgets::ListPicker::new(self), + answers, + }, + b, + ) .hide_cursor() - .run(w)?; + .run(events)?; match transformer { - Transformer::Sync(transformer) => transformer(&ans, answers, w)?, + Transformer::Sync(transformer) => transformer(&ans, answers, b)?, _ => { - queue!(w, SetForegroundColor(Color::DarkCyan))?; - print_comma_separated(ans.iter().map(|item| item.name.as_str()), w)?; + b.set_fg(Color::Cyan)?; + print_comma_separated(ans.iter().map(|item| item.name.as_str()), b)?; + b.set_fg(Color::Reset)?; - w.write_all(b"\n")?; - execute!(w, ResetColor)?; + b.write_all(b"\n")?; + b.flush()?; } } @@ -235,32 +249,37 @@ impl Checkbox<'_, '_, '_> { } crate::cfg_async! { - pub(crate) async fn ask_async( + pub(crate) async fn ask_async( mut self, message: String, answers: &Answers, - w: &mut W, + b: &mut B, + events: &mut ui::events::AsyncEvents, ) -> error::Result { let transformer = self.transformer.take(); - let ans = ui::AsyncInput::new(CheckboxPrompt { - message, - picker: widgets::ListPicker::new(self), - answers, - }) + let ans = ui::Input::new( + CheckboxPrompt { + message, + picker: widgets::ListPicker::new(self), + answers, + }, + b, + ) .hide_cursor() - .run(w) + .run_async(events) .await?; match transformer { - Transformer::Async(transformer) => transformer(&ans, answers, w).await?, - Transformer::Sync(transformer) => transformer(&ans, answers, w)?, + Transformer::Async(transformer) => transformer(&ans, answers, b).await?, + Transformer::Sync(transformer) => transformer(&ans, answers, b)?, _ => { - queue!(w, SetForegroundColor(Color::DarkCyan))?; - print_comma_separated(ans.iter().map(|item| item.name.as_str()), w)?; + b.set_fg(Color::Cyan)?; + print_comma_separated(ans.iter().map(|item| item.name.as_str()), b)?; + b.set_fg(Color::Reset)?; - w.write_all(b"\n")?; - execute!(w, ResetColor)?; + b.write_all(b"\n")?; + b.flush()?; } } @@ -294,7 +313,11 @@ impl<'m, 'w, 'f, 'v, 't> CheckboxBuilder<'m, 'w, 'f, 'v, 't> { self.choice_with_default(choice, false) } - pub fn choice_with_default>(mut self, choice: I, default: bool) -> Self { + pub fn choice_with_default>( + mut self, + choice: I, + default: bool, + ) -> Self { self.checkbox .choices .choices @@ -427,16 +450,16 @@ impl super::Question<'static, 'static, 'static, 'static, 'static> { } } -fn print_comma_separated<'a, W: io::Write>( +fn print_comma_separated<'a, B: Backend>( iter: impl Iterator, - w: &mut W, + b: &mut B, ) -> io::Result<()> { let mut iter = iter.peekable(); while let Some(item) = iter.next() { - w.write_all(item.as_bytes())?; + b.write_all(item.as_bytes())?; if iter.peek().is_some() { - w.write_all(b", ")?; + b.write_all(b", ")?; } } diff --git a/src/question/confirm.rs b/src/question/confirm.rs index ffe0191..1e99a99 100644 --- a/src/question/confirm.rs +++ b/src/question/confirm.rs @@ -1,9 +1,12 @@ -use crossterm::style::Colorize; -use ui::{widgets, Prompt, Validation, Widget}; - -use crate::{error, Answer, Answers}; +use ui::{ + backend::{Backend, Stylize}, + error, + events::KeyEvent, + widgets, Prompt, Validation, Widget, +}; use super::{Options, TransformerByVal as Transformer}; +use crate::{Answer, Answers}; #[derive(Debug, Default)] pub struct Confirm<'t> { @@ -18,15 +21,19 @@ struct ConfirmPrompt<'t> { } impl Widget for ConfirmPrompt<'_> { - fn render(&mut self, max_width: usize, w: &mut W) -> crossterm::Result<()> { - self.input.render(max_width, w) + fn render( + &mut self, + max_width: usize, + b: &mut B, + ) -> error::Result<()> { + self.input.render(max_width, b) } fn height(&self) -> usize { self.input.height() } - fn handle_key(&mut self, key: crossterm::event::KeyEvent) -> bool { + fn handle_key(&mut self, key: KeyEvent) -> bool { self.input.handle_key(key) } @@ -96,27 +103,32 @@ impl ui::AsyncPrompt for ConfirmPrompt<'_> { } impl Confirm<'_> { - pub(crate) fn ask( + pub(crate) fn ask( mut self, message: String, answers: &Answers, - w: &mut W, + b: &mut B, + events: &mut ui::events::Events, ) -> error::Result { let transformer = self.transformer.take(); - let ans = ui::Input::new(ConfirmPrompt { - confirm: self, - message, - input: widgets::CharInput::new(only_yn), - }) - .run(w)?; + let ans = ui::Input::new( + ConfirmPrompt { + confirm: self, + message, + input: widgets::CharInput::new(only_yn), + }, + b, + ) + .run(events)?; match transformer { - Transformer::Sync(transformer) => transformer(ans, answers, w)?, + Transformer::Sync(transformer) => transformer(ans, answers, b)?, _ => { let ans = if ans { "Yes" } else { "No" }; - - writeln!(w, "{}", ans.dark_cyan())?; + b.write_styled(ans.cyan())?; + b.write_all(b"\n")?; + b.flush()?; } } @@ -124,29 +136,31 @@ impl Confirm<'_> { } crate::cfg_async! { - pub(crate) async fn ask_async( + pub(crate) async fn ask_async( mut self, message: String, answers: &Answers, - w: &mut W, + b: &mut B, + events: &mut ui::events::AsyncEvents, ) -> error::Result { let transformer = self.transformer.take(); - let ans = ui::AsyncInput::new(ConfirmPrompt { + let ans = ui::Input::new(ConfirmPrompt { confirm: self, message, input: widgets::CharInput::new(only_yn), - }) - .run(w) + }, b) + .run_async(events) .await?; match transformer { - Transformer::Async(transformer) => transformer(ans, answers, w).await?, - Transformer::Sync(transformer) => transformer(ans, answers, w)?, + Transformer::Async(transformer) => transformer(ans, answers, b).await?, + Transformer::Sync(transformer) => transformer(ans, answers, b)?, _ => { let ans = if ans { "Yes" } else { "No" }; - - writeln!(w, "{}", ans.dark_cyan())?; + b.write_styled(ans.cyan())?; + b.write_all(b"\n")?; + b.flush()?; } } @@ -197,7 +211,9 @@ crate::impl_transformer_builder!(by val ConfirmBuilder<'m, 'w, t> bool; (this, t }); impl super::Question<'static, 'static, 'static, 'static, 'static> { - pub fn confirm>(name: N) -> ConfirmBuilder<'static, 'static, 'static> { + pub fn confirm>( + name: N, + ) -> ConfirmBuilder<'static, 'static, 'static> { ConfirmBuilder { opts: Options::new(name.into()), confirm: Default::default(), diff --git a/src/question/editor.rs b/src/question/editor.rs index 54c306b..de45956 100644 --- a/src/question/editor.rs +++ b/src/question/editor.rs @@ -6,32 +6,34 @@ use std::{ process::Command, }; -use crossterm::style::Colorize; use tempfile::TempPath; -use ui::{Validation, Widget}; -#[cfg(feature = "async_std")] -use async_std::{ +#[cfg(feature = "async-std")] +use async_std_dep::{ fs::File as AsyncFile, io::prelude::{ReadExt, SeekExt, WriteExt}, process::Command as AsyncCommand, }; -#[cfg(feature = "async_smol")] -use smol::{ +#[cfg(feature = "smol")] +use smol_dep::{ fs::File as AsyncFile, io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}, process::Command as AsyncCommand, }; -#[cfg(feature = "async_tokio")] -use tokio::{ +#[cfg(feature = "tokio")] +use tokio_dep::{ fs::File as AsyncFile, io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}, process::Command as AsyncCommand, }; -use crate::{error, Answer, Answers}; +use ui::{ + backend::{Backend, Stylize}, + error, Validation, Widget, +}; use super::{Filter, Options, Transformer, Validate}; +use crate::{Answer, Answers}; #[derive(Debug)] pub struct Editor<'f, 'v, 't> { @@ -78,7 +80,7 @@ struct EditorPrompt<'f, 'v, 't, 'a> { } impl Widget for EditorPrompt<'_, '_, '_, '_> { - fn render(&mut self, _: usize, _: &mut W) -> crossterm::Result<()> { + fn render(&mut self, _: usize, _: &mut B) -> error::Result<()> { Ok(()) } @@ -146,7 +148,7 @@ struct EditorPromptAsync<'f, 'v, 't, 'a> { } impl Widget for EditorPromptAsync<'_, '_, '_, '_> { - fn render(&mut self, _: usize, _: &mut W) -> crossterm::Result<()> { + fn render(&mut self, _: usize, _: &mut B) -> error::Result<()> { Ok(()) } @@ -218,11 +220,12 @@ impl ui::AsyncPrompt for EditorPromptAsync<'_, '_, '_, '_> { } impl Editor<'_, '_, '_> { - pub(crate) fn ask( + pub(crate) fn ask( mut self, message: String, answers: &Answers, - w: &mut W, + b: &mut B, + events: &mut ui::events::Events, ) -> error::Result { let mut builder = tempfile::Builder::new(); @@ -242,30 +245,38 @@ impl Editor<'_, '_, '_> { let (file, path) = file.into_parts(); - let ans = ui::Input::new(EditorPrompt { - message, - editor: self, - file, - path, - ans: String::new(), - answers, - }) - .run(w)?; + let ans = ui::Input::new( + EditorPrompt { + message, + editor: self, + file, + path, + ans: String::new(), + answers, + }, + b, + ) + .run(events)?; match transformer { - Transformer::Sync(transformer) => transformer(&ans, answers, w)?, - _ => writeln!(w, "{}", "Received".dark_grey())?, + Transformer::Sync(transformer) => transformer(&ans, answers, b)?, + _ => { + b.write_styled("Received".dark_grey())?; + b.write_all(b"\n")?; + b.flush()?; + } } Ok(Answer::String(ans)) } crate::cfg_async! { - pub(crate) async fn ask_async( + pub(crate) async fn ask_async( mut self, message: String, answers: &Answers, - w: &mut W, + b: &mut B, + events: &mut ui::events::AsyncEvents, ) -> error::Result { let mut builder = tempfile::Builder::new(); @@ -284,21 +295,25 @@ impl Editor<'_, '_, '_> { let transformer = self.transformer.take(); - let ans = ui::AsyncInput::new(EditorPromptAsync { + let ans = ui::Input::new(EditorPromptAsync { message, editor: self, file, path, ans: String::new(), answers, - }) - .run(w) + }, b) + .run_async(events) .await?; match transformer { - Transformer::Async(transformer) => transformer(&ans, answers, w).await?, - Transformer::Sync(transformer) => transformer(&ans, answers, w)?, - Transformer::None => writeln!(w, "{}", "Received".dark_grey())?, + Transformer::Async(transformer) => transformer(&ans, answers, b).await?, + Transformer::Sync(transformer) => transformer(&ans, answers, b)?, + Transformer::None => { + b.write_styled("Received".dark_grey())?; + b.write_all(b"\n")?; + b.flush()?; + }, } Ok(Answer::String(ans)) diff --git a/src/question/expand.rs b/src/question/expand.rs index d0da8aa..853eb29 100644 --- a/src/question/expand.rs +++ b/src/question/expand.rs @@ -3,16 +3,15 @@ use ahash::AHashSet as HashSet; #[cfg(not(feature = "ahash"))] use std::collections::HashSet; -use crossterm::{ - cursor, queue, - style::{Color, Colorize, ResetColor, SetForegroundColor}, - terminal, +use ui::{ + backend::{Backend, Color, MoveDirection, Stylize}, + error, + events::KeyEvent, + widgets, Prompt, Validation, Widget, }; -use ui::{widgets, Prompt, Validation, Widget}; - -use crate::{error, Answer, Answers, ExpandItem}; use super::{Choice, Options, Transformer}; +use crate::{Answer, Answers, ExpandItem}; #[derive(Debug)] pub struct Expand<'t> { @@ -112,14 +111,18 @@ impl Option + Send + Sync> ui::AsyncPrompt for ExpandPrompt const ANSWER_PROMPT: &[u8] = b" Answer: "; impl Option> ui::Widget for ExpandPrompt<'_, F> { - fn render(&mut self, max_width: usize, w: &mut W) -> crossterm::Result<()> { + fn render( + &mut self, + max_width: usize, + b: &mut B, + ) -> error::Result<()> { if self.expanded { - let max_width = terminal::size()?.0 as usize - ANSWER_PROMPT.len(); - self.list.render(max_width, w)?; - w.write_all(ANSWER_PROMPT)?; - self.input.render(max_width, w) + let max_width = b.size()?.width as usize - ANSWER_PROMPT.len(); + self.list.render(max_width, b)?; + b.write_all(ANSWER_PROMPT)?; + self.input.render(max_width, b) } else { - self.input.render(max_width, w)?; + self.input.render(max_width, b)?; if let Some(key) = self.input.value() { let name = &self @@ -136,9 +139,10 @@ impl Option> ui::Widget for ExpandPrompt<'_, F> { .map(|item| &*item.name) .unwrap_or("Help, list all options"); - queue!(w, cursor::MoveToNextLine(1))?; + b.move_cursor(MoveDirection::NextLine(1))?; + b.write_styled(">>".cyan())?; - write!(w, "{} {}", ">>".dark_cyan(), name)?; + write!(b, " {}", name)?; } Ok(()) @@ -155,7 +159,7 @@ impl Option> ui::Widget for ExpandPrompt<'_, F> { } } - fn handle_key(&mut self, key: crossterm::event::KeyEvent) -> bool { + fn handle_key(&mut self, key: KeyEvent) -> bool { if self.input.handle_key(key) { self.list.list.selected = self.input.value(); true @@ -184,24 +188,24 @@ thread_local! { } impl widgets::List for Expand<'_> { - fn render_item( + fn render_item( &mut self, index: usize, _: bool, max_width: usize, - w: &mut W, - ) -> crossterm::Result<()> { + b: &mut B, + ) -> error::Result<()> { if index == self.choices.len() { - return HELP_CHOICE.with(|h| self.render_choice(h, max_width, w)); + return HELP_CHOICE.with(|h| self.render_choice(h, max_width, b)); } match &self.choices[index] { - Choice::Choice(item) => self.render_choice(item, max_width, w), + Choice::Choice(item) => self.render_choice(item, max_width, b), Choice::Separator(s) => { - queue!(w, SetForegroundColor(Color::DarkGrey))?; - w.write_all(b" ")?; - super::get_sep_str(s).render(max_width - 3, w)?; - queue!(w, ResetColor) + b.set_fg(Color::DarkGrey)?; + b.write_all(b" ")?; + super::get_sep_str(s).render(max_width - 3, b)?; + b.set_fg(Color::Reset) } } } @@ -224,23 +228,23 @@ impl widgets::List for Expand<'_> { } impl Expand<'_> { - fn render_choice( + fn render_choice( &self, item: &ExpandItem, max_width: usize, - w: &mut W, - ) -> crossterm::Result<()> { + b: &mut B, + ) -> error::Result<()> { let hovered = self.selected.map(|c| c == item.key).unwrap_or(false); if hovered { - queue!(w, SetForegroundColor(Color::DarkCyan))?; + b.set_fg(Color::Cyan)?; } - write!(w, " {}) ", item.key)?; - item.name.as_str().render(max_width - 5, w)?; + write!(b, " {}) ", item.key)?; + item.name.as_str().render(max_width - 5, b)?; if hovered { - queue!(w, ResetColor)?; + b.set_fg(Color::Reset)?; } Ok(()) @@ -275,11 +279,50 @@ impl Expand<'_> { (choices, hint) } - pub(crate) fn ask( + pub(crate) fn ask( mut self, message: String, answers: &Answers, - w: &mut W, + b: &mut B, + events: &mut ui::events::Events, + ) -> error::Result { + let (choices, hint) = self.get_choices_and_hint(); + let transformer = self.transformer.take(); + + let ans = ui::Input::new( + ExpandPrompt { + message, + input: widgets::CharInput::new(|c| { + let c = c.to_ascii_lowercase(); + choices.contains(c).then(|| c) + }), + list: widgets::ListPicker::new(self), + hint, + expanded: false, + }, + b, + ) + .run(events)?; + + match transformer { + Transformer::Sync(transformer) => transformer(&ans, answers, b)?, + _ => { + b.write_styled(ans.name.as_str().cyan())?; + b.write_all(b"\n")?; + b.flush()?; + } + } + + Ok(Answer::ExpandItem(ans)) + } + + crate::cfg_async! { + pub(crate) async fn ask_async( + mut self, + message: String, + answers: &Answers, + b: &mut B, + events: &mut ui::events::AsyncEvents, ) -> error::Result { let (choices, hint) = self.get_choices_and_hint(); let transformer = self.transformer.take(); @@ -293,44 +336,18 @@ impl Expand<'_> { list: widgets::ListPicker::new(self), hint, expanded: false, - }) - .run(w)?; - - match transformer { - Transformer::Sync(transformer) => transformer(&ans, answers, w)?, - _ => writeln!(w, "{}", ans.name.as_str().dark_cyan())?, - } - - Ok(Answer::ExpandItem(ans)) - } - - crate::cfg_async! { - pub(crate) async fn ask_async( - mut self, - message: String, - answers: &Answers, - w: &mut W, - ) -> error::Result { - let (choices, hint) = self.get_choices_and_hint(); - let transformer = self.transformer.take(); - - let ans = ui::AsyncInput::new(ExpandPrompt { - message, - input: widgets::CharInput::new(|c| { - let c = c.to_ascii_lowercase(); - choices.contains(c).then(|| c) - }), - list: widgets::ListPicker::new(self), - hint, - expanded: false, - }) - .run(w) + }, b) + .run_async(events) .await?; match transformer { - Transformer::Async(transformer) => transformer(&ans, answers, w).await?, - Transformer::Sync(transformer) => transformer(&ans, answers, w)?, - _ => writeln!(w, "{}", ans.name.as_str().dark_cyan())?, + Transformer::Async(transformer) => transformer(&ans, answers, b).await?, + Transformer::Sync(transformer) => transformer(&ans, answers, b)?, + _ => { + b.write_styled(ans.name.as_str().cyan())?; + b.write_all(b"\n")?; + b.flush()?; + } } Ok(Answer::ExpandItem(ans)) @@ -425,7 +442,9 @@ impl<'m, 'w, 't> ExpandBuilder<'m, 'w, 't> { } } -impl<'m, 'w, 't> From> for super::Question<'m, 'w, 'static, 'static, 't> { +impl<'m, 'w, 't> From> + for super::Question<'m, 'w, 'static, 'static, 't> +{ fn from(builder: ExpandBuilder<'m, 'w, 't>) -> Self { builder.build() } @@ -453,7 +472,9 @@ crate::impl_transformer_builder!(ExpandBuilder<'m, 'w, t> ExpandItem; (this, tra }); impl super::Question<'static, 'static, 'static, 'static, 'static> { - pub fn expand>(name: N) -> ExpandBuilder<'static, 'static, 'static> { + pub fn expand>( + name: N, + ) -> ExpandBuilder<'static, 'static, 'static> { ExpandBuilder { opts: Options::new(name.into()), expand: Default::default(), diff --git a/src/question/input.rs b/src/question/input.rs index 8bf2b7a..ee0672a 100644 --- a/src/question/input.rs +++ b/src/question/input.rs @@ -1,9 +1,12 @@ -use crossterm::style::Colorize; -use ui::{widgets, Prompt, Validation, Widget}; - -use crate::{error, Answer, Answers}; +use ui::{ + backend::{Backend, Stylize}, + error, + events::KeyEvent, + widgets, Prompt, Validation, Widget, +}; use super::{Filter, Options, Transformer, Validate}; +use crate::{Answer, Answers}; #[derive(Debug, Default)] pub struct Input<'f, 'v, 't> { @@ -21,15 +24,19 @@ struct InputPrompt<'f, 'v, 't, 'a> { } impl Widget for InputPrompt<'_, '_, '_, '_> { - fn render(&mut self, max_width: usize, w: &mut W) -> crossterm::Result<()> { - self.input.render(max_width, w) + fn render( + &mut self, + max_width: usize, + b: &mut B, + ) -> error::Result<()> { + self.input.render(max_width, b) } fn height(&self) -> usize { self.input.height() } - fn handle_key(&mut self, key: crossterm::event::KeyEvent) -> bool { + fn handle_key(&mut self, key: KeyEvent) -> bool { self.input.handle_key(key) } @@ -131,11 +138,50 @@ impl ui::AsyncPrompt for InputPrompt<'_, '_, '_, '_> { } impl Input<'_, '_, '_> { - pub(crate) fn ask( + pub(crate) fn ask( mut self, message: String, answers: &Answers, - w: &mut W, + b: &mut B, + events: &mut ui::events::Events, + ) -> error::Result { + if let Some(ref mut default) = self.default { + default.insert(0, '('); + default.push(')'); + } + + let transformer = self.transformer.take(); + + let ans = ui::Input::new( + InputPrompt { + message, + input_opts: self, + input: widgets::StringInput::default(), + answers, + }, + b, + ) + .run(events)?; + + match transformer { + Transformer::Sync(transformer) => transformer(&ans, answers, b)?, + _ => { + b.write_styled(ans.as_str().cyan())?; + b.write_all(b"\n")?; + b.flush()?; + } + } + + Ok(Answer::String(ans)) + } + + crate::cfg_async! { + pub(crate) async fn ask_async( + mut self, + message: String, + answers: &Answers, + b: &mut B, + events: &mut ui::events::AsyncEvents, ) -> error::Result { if let Some(ref mut default) = self.default { default.insert(0, '('); @@ -149,44 +195,18 @@ impl Input<'_, '_, '_> { input_opts: self, input: widgets::StringInput::default(), answers, - }) - .run(w)?; - - match transformer { - Transformer::Sync(transformer) => transformer(&ans, answers, w)?, - _ => writeln!(w, "{}", ans.as_str().dark_cyan())?, - } - - Ok(Answer::String(ans)) - } - - crate::cfg_async! { - pub(crate) async fn ask_async( - mut self, - message: String, - answers: &Answers, - w: &mut W, - ) -> error::Result { - if let Some(ref mut default) = self.default { - default.insert(0, '('); - default.push(')'); - } - - let transformer = self.transformer.take(); - - let ans = ui::AsyncInput::new(InputPrompt { - message, - input_opts: self, - input: widgets::StringInput::default(), - answers, - }) - .run(w) + }, b) + .run_async(events) .await?; match transformer { - Transformer::Async(transformer) => transformer(&ans, answers, w).await?, - Transformer::Sync(transformer) => transformer(&ans, answers, w)?, - _ => writeln!(w, "{}", ans.as_str().dark_cyan())?, + Transformer::Async(transformer) => transformer(&ans, answers, b).await?, + Transformer::Sync(transformer) => transformer(&ans, answers, b)?, + _ => { + b.write_styled(ans.as_str().cyan())?; + b.write_all(b"\n")?; + b.flush()?; + } } Ok(Answer::String(ans)) diff --git a/src/question/list.rs b/src/question/list.rs index 5960bbc..3b37f5c 100644 --- a/src/question/list.rs +++ b/src/question/list.rs @@ -1,15 +1,12 @@ -use crossterm::{ - queue, - style::{Color, Colorize, Print, ResetColor, SetForegroundColor}, -}; -use ui::{widgets, Prompt, Widget}; - -use crate::{ - answer::{Answer, ListItem}, - error, Answers, +use ui::{ + backend::{Backend, Color, Stylize}, + error, + events::KeyEvent, + widgets, Prompt, Widget, }; use super::{Options, Transformer}; +use crate::{Answer, Answers, ListItem}; #[derive(Debug, Default)] pub struct List<'t> { @@ -77,15 +74,15 @@ impl ui::AsyncPrompt for ListPrompt<'_> { } impl Widget for ListPrompt<'_> { - fn render(&mut self, max_width: usize, w: &mut W) -> crossterm::Result<()> { - self.picker.render(max_width, w) + fn render(&mut self, _: usize, b: &mut B) -> error::Result<()> { + self.picker.render(b.size()?.width as usize, b) } fn height(&self) -> usize { self.picker.height() } - fn handle_key(&mut self, key: crossterm::event::KeyEvent) -> bool { + fn handle_key(&mut self, key: KeyEvent) -> bool { self.picker.handle_key(key) } @@ -95,26 +92,27 @@ impl Widget for ListPrompt<'_> { } impl widgets::List for List<'_> { - fn render_item( + fn render_item( &mut self, index: usize, hovered: bool, max_width: usize, - w: &mut W, - ) -> crossterm::Result<()> { + b: &mut B, + ) -> error::Result<()> { if hovered { - queue!(w, SetForegroundColor(Color::DarkCyan), Print("❯ "))?; + b.set_fg(Color::Cyan)?; + b.write_all("❯ ".as_bytes())?; } else { - w.write_all(b" ")?; + b.write_all(b" ")?; if !self.is_selectable(index) { - queue!(w, SetForegroundColor(Color::DarkGrey))?; + b.set_fg(Color::DarkGrey)?; } } - self.choices[index].as_str().render(max_width - 2, w)?; + self.choices[index].as_str().render(max_width - 2, b)?; - queue!(w, ResetColor) + b.set_fg(Color::Reset) } fn is_selectable(&self, index: usize) -> bool { @@ -135,50 +133,60 @@ impl widgets::List for List<'_> { } impl List<'_> { - pub(crate) fn ask( + pub(crate) fn ask( mut self, message: String, answers: &Answers, - w: &mut W, + b: &mut B, + events: &mut ui::events::Events, ) -> error::Result { let transformer = self.transformer.take(); let mut picker = widgets::ListPicker::new(self); if let Some(default) = picker.list.choices.default() { picker.set_at(default); } - let ans = ui::Input::new(ListPrompt { picker, message }) + let ans = ui::Input::new(ListPrompt { picker, message }, b) .hide_cursor() - .run(w)?; + .run(events)?; match transformer { - Transformer::Sync(transformer) => transformer(&ans, answers, w)?, - _ => writeln!(w, "{}", ans.name.as_str().dark_cyan())?, + Transformer::Sync(transformer) => transformer(&ans, answers, b)?, + _ => { + b.write_styled(ans.name.as_str().cyan())?; + b.write_all(b"\n")?; + b.flush()?; + } } Ok(Answer::ListItem(ans)) } crate::cfg_async! { - pub(crate) async fn ask_async( + pub(crate) async fn ask_async( mut self, message: String, answers: &Answers, - w: &mut W, + b: &mut B, + events: &mut ui::events::AsyncEvents, ) -> error::Result { let transformer = self.transformer.take(); let mut picker = widgets::ListPicker::new(self); if let Some(default) = picker.list.choices.default() { picker.set_at(default); } - let ans = ui::AsyncInput::new(ListPrompt { picker, message }) + let ans = ui::Input::new(ListPrompt { picker, message }, b) .hide_cursor() - .run(w) + .run_async(events) .await?; match transformer { - Transformer::Async(transformer) => transformer(&ans, answers, w).await?, - Transformer::Sync(transformer) => transformer(&ans, answers, w)?, - _ => writeln!(w, "{}", ans.name.as_str().dark_cyan())?, + Transformer::Async(transformer) => transformer(&ans, answers, b).await?, + Transformer::Sync(transformer) => transformer(&ans, answers, b)?, + _ => { + b.write_styled(ans.name.as_str().cyan())?; + b.write_all(b"\n")?; + b.flush()?; + } } Ok(Answer::ListItem(ans)) @@ -248,7 +256,9 @@ impl<'m, 'w, 't> ListBuilder<'m, 'w, 't> { } } -impl<'m, 'w, 't> From> for super::Question<'m, 'w, 'static, 'static, 't> { +impl<'m, 'w, 't> From> + for super::Question<'m, 'w, 'static, 'static, 't> +{ fn from(builder: ListBuilder<'m, 'w, 't>) -> Self { builder.build() } diff --git a/src/question/mod.rs b/src/question/mod.rs index 85699b6..f3ff1d6 100644 --- a/src/question/mod.rs +++ b/src/question/mod.rs @@ -12,13 +12,14 @@ mod password; mod plugin; mod rawlist; -use crate::{error, Answer, Answers}; +use crate::{Answer, Answers}; pub use choice::Choice; use choice::{get_sep_str, ChoiceList}; use options::Options; pub use plugin::Plugin; +use ui::{backend::Backend, error}; -use std::{fmt, future::Future, io::prelude::*, pin::Pin}; +use std::{fmt, future::Future, pin::Pin}; #[derive(Debug)] pub struct Question<'m, 'w, 'f, 'v, 't> { @@ -27,13 +28,16 @@ pub struct Question<'m, 'w, 'f, 'v, 't> { } impl<'m, 'w, 'f, 'v, 't> Question<'m, 'w, 'f, 'v, 't> { - fn new(opts: Options<'m, 'w>, kind: QuestionKind<'f, 'v, 't>) -> Self { + pub(crate) fn new( + opts: Options<'m, 'w>, + kind: QuestionKind<'f, 'v, 't>, + ) -> Self { Self { opts, kind } } } #[derive(Debug)] -enum QuestionKind<'f, 'v, 't> { +pub(crate) enum QuestionKind<'f, 'v, 't> { Input(input::Input<'f, 'v, 't>), Int(number::Int<'f, 'v, 't>), Float(number::Float<'f, 'v, 't>), @@ -49,10 +53,11 @@ enum QuestionKind<'f, 'v, 't> { } impl Question<'_, '_, '_, '_, '_> { - pub(crate) fn ask( + pub(crate) fn ask( mut self, answers: &Answers, - w: &mut W, + b: &mut B, + events: &mut ui::events::Events, ) -> error::Result> { if (!self.opts.ask_if_answered && answers.contains_key(&self.opts.name)) || !self.opts.when.get(answers) @@ -68,27 +73,28 @@ impl Question<'_, '_, '_, '_, '_> { .unwrap_or_else(|| name.clone() + ":"); let res = match self.kind { - QuestionKind::Input(i) => i.ask(message, answers, w)?, - QuestionKind::Int(i) => i.ask(message, answers, w)?, - QuestionKind::Float(f) => f.ask(message, answers, w)?, - QuestionKind::Confirm(c) => c.ask(message, answers, w)?, - QuestionKind::List(l) => l.ask(message, answers, w)?, - QuestionKind::Rawlist(r) => r.ask(message, answers, w)?, - QuestionKind::Expand(e) => e.ask(message, answers, w)?, - QuestionKind::Checkbox(c) => c.ask(message, answers, w)?, - QuestionKind::Password(p) => p.ask(message, answers, w)?, - QuestionKind::Editor(e) => e.ask(message, answers, w)?, - QuestionKind::Plugin(ref mut o) => o.ask(message, answers, w)?, + QuestionKind::Input(i) => i.ask(message, answers, b, events)?, + QuestionKind::Int(i) => i.ask(message, answers, b, events)?, + QuestionKind::Float(f) => f.ask(message, answers, b, events)?, + QuestionKind::Confirm(c) => c.ask(message, answers, b, events)?, + QuestionKind::List(l) => l.ask(message, answers, b, events)?, + QuestionKind::Rawlist(r) => r.ask(message, answers, b, events)?, + QuestionKind::Expand(e) => e.ask(message, answers, b, events)?, + QuestionKind::Checkbox(c) => c.ask(message, answers, b, events)?, + QuestionKind::Password(p) => p.ask(message, answers, b, events)?, + QuestionKind::Editor(e) => e.ask(message, answers, b, events)?, + QuestionKind::Plugin(ref mut o) => o.ask(message, answers, b, events)?, }; Ok(Some((name, res))) } crate::cfg_async! { - pub(crate) async fn ask_async( + pub(crate) async fn ask_async( mut self, answers: &Answers, - w: &mut W, + b: &mut B, + events: &mut ui::events::AsyncEvents, ) -> error::Result> { if (!self.opts.ask_if_answered && answers.contains_key(&self.opts.name)) || !self.opts.when.get(answers) @@ -104,17 +110,17 @@ impl Question<'_, '_, '_, '_, '_> { .unwrap_or_else(|| name.clone() + ":"); let res = match self.kind { - QuestionKind::Input(i) => i.ask_async(message, answers, w).await?, - QuestionKind::Int(i) => i.ask_async(message, answers, w).await?, - QuestionKind::Float(f) => f.ask_async(message, answers, w).await?, - QuestionKind::Confirm(c) => c.ask_async(message, answers, w).await?, - QuestionKind::List(l) => l.ask_async(message, answers, w).await?, - QuestionKind::Rawlist(r) => r.ask_async(message, answers, w).await?, - QuestionKind::Expand(e) => e.ask_async(message, answers, w).await?, - QuestionKind::Checkbox(c) => c.ask_async(message, answers, w).await?, - QuestionKind::Password(p) => p.ask_async(message, answers, w).await?, - QuestionKind::Editor(e) => e.ask_async(message, answers, w).await?, - QuestionKind::Plugin(ref mut o) => o.ask_async(message, answers, w).await?, + QuestionKind::Input(i) => i.ask_async(message, answers, b, events).await?, + QuestionKind::Int(i) => i.ask_async(message, answers, b, events).await?, + QuestionKind::Float(f) => f.ask_async(message, answers, b, events).await?, + QuestionKind::Confirm(c) => c.ask_async(message, answers, b, events).await?, + QuestionKind::List(l) => l.ask_async(message, answers, b, events).await?, + QuestionKind::Rawlist(r) => r.ask_async(message, answers, b, events).await?, + QuestionKind::Expand(e) => e.ask_async(message, answers, b, events).await?, + QuestionKind::Checkbox(c) => c.ask_async(message, answers, b, events).await?, + QuestionKind::Password(p) => p.ask_async(message, answers, b, events).await?, + QuestionKind::Editor(e) => e.ask_async(message, answers, b, events).await?, + QuestionKind::Plugin(ref mut o) => o.ask_async(message, answers, b, events).await?, }; Ok(Some((name, res))) @@ -122,7 +128,8 @@ impl Question<'_, '_, '_, '_, '_> { } } -type BoxFuture<'a, T> = Pin + Send + Sync + 'a>>; +pub(crate) type BoxFuture<'a, T> = + Pin + Send + Sync + 'a>>; macro_rules! handler { ($name:ident, $fn_trait:ident ( $($type:ty),* ) -> $return:ty) => { @@ -200,11 +207,11 @@ handler!(ValidateByVal, Fn(T, &Answers) -> Result<(), String>); // SAFETY: The type signature only contains &T handler!( Transformer, unsafe ?Sized - FnOnce(&T, &Answers, &mut dyn Write) -> error::Result<()> + FnOnce(&T, &Answers, &mut dyn Backend) -> error::Result<()> ); handler!( TransformerByVal, - FnOnce(T, &Answers, &mut dyn Write) -> error::Result<()> + FnOnce(T, &Answers, &mut dyn Backend) -> error::Result<()> ); #[doc(hidden)] @@ -292,7 +299,7 @@ macro_rules! impl_transformer_builder { impl<$($pre_lifetime),*, 't, $($post_lifetime),*> $ty<$($pre_lifetime),*, 't, $($post_lifetime),*> { pub fn transformer<'a, F>(self, transformer: F) -> $ty<$($pre_lifetime),*, 'a, $($post_lifetime),*> where - F: FnOnce(&$t, &crate::Answers, &mut dyn std::io::Write) -> crate::error::Result<()> + Send + Sync + 'a, + F: FnOnce(&$t, &crate::Answers, &mut dyn Backend) -> ui::error::Result<()> + Send + Sync + 'a, { let $self = self; let $transformer = crate::question::Transformer::Sync(Box::new(transformer)); @@ -301,7 +308,7 @@ macro_rules! impl_transformer_builder { pub fn transformer_async<'a, F>(self, transformer: F) -> $ty<$($pre_lifetime),*, 'a, $($post_lifetime),*> where - F: FnOnce(&$t, &crate::Answers, &mut dyn std::io::Write) -> std::pin::Pin> + Send + Sync + 'a>> + Send + Sync + 'a, + F: FnOnce(&$t, &crate::Answers, &mut dyn Backend) -> std::pin::Pin> + Send + Sync + 'a>> + Send + Sync + 'a, { let $self = self; let $transformer = crate::question::Transformer::Async(Box::new(transformer)); @@ -315,7 +322,7 @@ macro_rules! impl_transformer_builder { impl<$($pre_lifetime),*, 't, $($post_lifetime),*> $ty<$($pre_lifetime),*, 't, $($post_lifetime),*> { pub fn transformer<'a, F>(self, transformer: F) -> $ty<$($pre_lifetime),*, 'a, $($post_lifetime),*> where - F: FnOnce($t, &crate::Answers, &mut dyn std::io::Write) -> crate::error::Result<()> + Send + Sync + 'a, + F: FnOnce($t, &crate::Answers, &mut dyn Backend) -> ui::error::Result<()> + Send + Sync + 'a, { let $self = self; let $transformer = crate::question::TransformerByVal::Sync(Box::new(transformer)); @@ -324,7 +331,7 @@ macro_rules! impl_transformer_builder { pub fn transformer_async<'a, F>(self, transformer: F) -> $ty<$($pre_lifetime),*, 'a, $($post_lifetime),*> where - F: FnOnce($t, &crate::Answers, &mut dyn std::io::Write) -> std::pin::Pin> + Send + Sync + 'a>> + Send + Sync + 'a, + F: FnOnce($t, &crate::Answers, &mut dyn Backend) -> std::pin::Pin> + Send + Sync + 'a>> + Send + Sync + 'a, { let $self = self; let $transformer = crate::question::TransformerByVal::Async(Box::new(transformer)); diff --git a/src/question/number.rs b/src/question/number.rs index 8ca9e87..e2d8171 100644 --- a/src/question/number.rs +++ b/src/question/number.rs @@ -1,11 +1,16 @@ use std::marker::PhantomData; -use crossterm::style::{Color, ResetColor, SetForegroundColor}; -use ui::{widgets, Prompt, Validation, Widget}; +use ui::{ + backend::{Backend, Color}, + error, + events::KeyEvent, + widgets, Prompt, Validation, Widget, +}; -use crate::{error, Answer, Answers}; - -use super::{Filter, Options, TransformerByVal as Transformer, ValidateByVal as Validate}; +use super::{ + Filter, Options, TransformerByVal as Transformer, ValidateByVal as Validate, +}; +use crate::{Answer, Answers}; #[derive(Debug, Default)] pub struct Float<'f, 'v, 't> { @@ -31,7 +36,7 @@ trait Number<'f, 'v> { fn filter_map_char(c: char) -> Option; fn parse(s: &str) -> Result; fn default(&self) -> Option; - fn write(inner: Self::Inner, w: &mut W) -> error::Result<()>; + fn write(inner: Self::Inner, b: &mut B) -> error::Result<()>; fn finish(inner: Self::Inner) -> Answer; } @@ -62,15 +67,12 @@ impl<'f, 'v> Number<'f, 'v> for Int<'f, 'v, '_> { self.filter } - fn write(i: Self::Inner, w: &mut W) -> error::Result<()> { - writeln!( - w, - "{}{}{}", - SetForegroundColor(Color::DarkCyan), - i, - ResetColor, - ) - .map_err(Into::into) + fn write(i: Self::Inner, b: &mut B) -> error::Result<()> { + b.set_fg(Color::Cyan)?; + write!(b, "{}", i)?; + b.set_fg(Color::Reset)?; + b.write_all(b"\n")?; + b.flush().map_err(Into::into) } fn finish(i: Self::Inner) -> Answer { @@ -82,7 +84,8 @@ impl<'f, 'v> Number<'f, 'v> for Float<'f, 'v, '_> { type Inner = f64; fn filter_map_char(c: char) -> Option { - if c.is_digit(10) || c == '.' || c == 'e' || c == 'E' || c == '+' || c == '-' { + if c.is_digit(10) || c == '.' || c == 'e' || c == 'E' || c == '+' || c == '-' + { Some(c) } else { None @@ -105,14 +108,16 @@ impl<'f, 'v> Number<'f, 'v> for Float<'f, 'v, '_> { self.filter } - fn write(f: Self::Inner, w: &mut W) -> error::Result<()> { - write!(w, "{}", SetForegroundColor(Color::DarkCyan))?; + fn write(f: Self::Inner, b: &mut B) -> error::Result<()> { + b.set_fg(Color::Cyan)?; if f.log10().abs() > 19.0 { - write!(w, "{:e}", f)?; + write!(b, "{:e}", f)?; } else { - write!(w, "{}", f)?; + write!(b, "{}", f)?; } - writeln!(w, "{}", ResetColor).map_err(Into::into) + b.set_fg(Color::Reset)?; + b.write_all(b"\n")?; + b.flush().map_err(Into::into) } fn finish(f: Self::Inner) -> Answer { @@ -130,15 +135,19 @@ struct NumberPrompt<'f, 'v, 'a, N> { } impl<'f, 'v, N: Number<'f, 'v>> Widget for NumberPrompt<'f, 'v, '_, N> { - fn render(&mut self, max_width: usize, w: &mut W) -> crossterm::Result<()> { - self.input.render(max_width, w) + fn render( + &mut self, + max_width: usize, + b: &mut B, + ) -> error::Result<()> { + self.input.render(max_width, b) } fn height(&self) -> usize { self.input.height() } - fn handle_key(&mut self, key: crossterm::event::KeyEvent) -> bool { + fn handle_key(&mut self, key: KeyEvent) -> bool { self.input.handle_key(key) } @@ -163,9 +172,10 @@ impl<'f, 'v, N: Number<'f, 'v>> Prompt for NumberPrompt<'f, 'v, '_, N> { if self.input.value().is_empty() && self.has_default() { return Ok(Validation::Finish); } + let n = N::parse(self.input.value())?; if let Validate::Sync(validate) = self.number.validate() { - validate(N::parse(self.input.value())?, self.answers)?; + validate(n, self.answers)?; } Ok(Validation::Finish) @@ -211,11 +221,14 @@ impl<'f, 'v, N: Number<'f, 'v> + Send + Sync> ui::AsyncPrompt for NumberPrompt<' return Some(Ok(Validation::Finish)); } + let n = match N::parse(self.input.value()) { + Ok(n) => n, + Err(e) => return Some(Err(e)), + }; + + match self.number.validate() { - Validate::Sync(validate) => match N::parse(self.input.value()) { - Ok(n) => Some(validate(n, self.answers).map(|_| Validation::Finish)), - Err(e) => Some(Err(e)), - }, + Validate::Sync(validate) => Some(validate(n, self.answers).map(|_| Validation::Finish)), _ => None, } } @@ -233,11 +246,43 @@ impl<'f, 'v, N: Number<'f, 'v> + Send + Sync> ui::AsyncPrompt for NumberPrompt<' macro_rules! impl_ask { ($t:ty) => { impl $t { - pub(crate) fn ask( + pub(crate) fn ask( mut self, message: String, answers: &Answers, - w: &mut W, + b: &mut B, + events: &mut ui::events::Events, + ) -> error::Result { + let transformer = self.transformer.take(); + + let ans = ui::Input::new( + NumberPrompt { + hint: self.default.map(|default| format!("({})", default)), + input: widgets::StringInput::new(Self::filter_map_char), + number: self, + message, + answers, + _marker: PhantomData, + }, + b, + ) + .run(events)?; + + match transformer { + Transformer::Sync(transformer) => transformer(ans, answers, b)?, + _ => Self::write(ans, b)?, + } + + Ok(Self::finish(ans)) + } + + crate::cfg_async! { + pub(crate) async fn ask_async( + mut self, + message: String, + answers: &Answers, + b: &mut B, + events: &mut ui::events::AsyncEvents, ) -> error::Result { let transformer = self.transformer.take(); @@ -248,41 +293,14 @@ macro_rules! impl_ask { message, answers, _marker: PhantomData, - }) - .run(w)?; - - match transformer { - Transformer::Sync(transformer) => transformer(ans, answers, w)?, - _ => Self::write(ans, w)?, - } - - Ok(Self::finish(ans)) - } - - crate::cfg_async! { - pub(crate) async fn ask_async( - mut self, - message: String, - answers: &Answers, - w: &mut W, - ) -> error::Result { - let transformer = self.transformer.take(); - - let ans = ui::AsyncInput::new(NumberPrompt { - hint: self.default.map(|default| format!("({})", default)), - input: widgets::StringInput::new(Self::filter_map_char), - number: self, - message, - answers, - _marker: PhantomData, - }) - .run(w) + }, b) + .run_async(events) .await?; match transformer { - Transformer::Async(transformer) => transformer(ans, answers, w).await?, - Transformer::Sync(transformer) => transformer(ans, answers, w)?, - _ => Self::write(ans, w)?, + Transformer::Async(transformer) => transformer(ans, answers, b).await?, + Transformer::Sync(transformer) => transformer(ans, answers, b)?, + _ => Self::write(ans, b)?, } Ok(Self::finish(ans)) diff --git a/src/question/password.rs b/src/question/password.rs index 15a9113..9d4a51e 100644 --- a/src/question/password.rs +++ b/src/question/password.rs @@ -1,9 +1,12 @@ -use crossterm::style::Colorize; -use ui::{widgets, Validation, Widget}; - -use crate::{error, Answer, Answers}; +use ui::{ + backend::{Backend, Stylize}, + error, + events::KeyEvent, + widgets, Validation, Widget, +}; use super::{Filter, Options, Transformer, Validate}; +use crate::{Answer, Answers}; #[derive(Debug, Default)] pub struct Password<'f, 'v, 't> { @@ -92,15 +95,19 @@ impl ui::AsyncPrompt for PasswordPrompt<'_, '_, '_, '_> { } impl Widget for PasswordPrompt<'_, '_, '_, '_> { - fn render(&mut self, max_width: usize, w: &mut W) -> crossterm::Result<()> { - self.input.render(max_width, w) + fn render( + &mut self, + max_width: usize, + b: &mut B, + ) -> error::Result<()> { + self.input.render(max_width, b) } fn height(&self) -> usize { self.input.height() } - fn handle_key(&mut self, key: crossterm::event::KeyEvent) -> bool { + fn handle_key(&mut self, key: KeyEvent) -> bool { self.input.handle_key(key) } @@ -110,11 +117,45 @@ impl Widget for PasswordPrompt<'_, '_, '_, '_> { } impl Password<'_, '_, '_> { - pub(crate) fn ask( + pub(crate) fn ask( mut self, message: String, answers: &Answers, - w: &mut W, + b: &mut B, + events: &mut ui::events::Events, + ) -> error::Result { + let transformer = self.transformer.take(); + + let ans = ui::Input::new( + PasswordPrompt { + message, + input: widgets::StringInput::default().password(self.mask), + password: self, + answers, + }, + b, + ) + .run(events)?; + + match transformer { + Transformer::Sync(transformer) => transformer(&ans, answers, b)?, + _ => { + b.write_styled("[hidden]".dark_grey())?; + b.write_all(b"\n")?; + b.flush()?; + } + } + + Ok(Answer::String(ans)) + } + + crate::cfg_async! { + pub(crate) async fn ask_async( + mut self, + message: String, + answers: &Answers, + b: &mut B, + events: &mut ui::events::AsyncEvents, ) -> error::Result { let transformer = self.transformer.take(); @@ -123,39 +164,18 @@ impl Password<'_, '_, '_> { input: widgets::StringInput::default().password(self.mask), password: self, answers, - }) - .run(w)?; - - match transformer { - Transformer::Sync(transformer) => transformer(&ans, answers, w)?, - _ => writeln!(w, "{}", "[hidden]".dark_grey())?, - } - - Ok(Answer::String(ans)) - } - - crate::cfg_async! { - pub(crate) async fn ask_async( - mut self, - message: String, - answers: &Answers, - w: &mut W, - ) -> error::Result { - let transformer = self.transformer.take(); - - let ans = ui::AsyncInput::new(PasswordPrompt { - message, - input: widgets::StringInput::default().password(self.mask), - password: self, - answers, - }) - .run(w) + }, b) + .run_async(events) .await?; match transformer { - Transformer::Async(transformer) => transformer(&ans, answers, w).await?, - Transformer::Sync(transformer) => transformer(&ans, answers, w)?, - _ => writeln!(w, "{}", "[hidden]".dark_grey())?, + Transformer::Async(transformer) => transformer(&ans, answers, b).await?, + Transformer::Sync(transformer) => transformer(&ans, answers, b)?, + _ => { + b.write_styled("[hidden]".dark_grey())?; + b.write_all(b"\n")?; + b.flush()?; + } } Ok(Answer::String(ans)) diff --git a/src/question/plugin.rs b/src/question/plugin.rs index dc14002..452cd74 100644 --- a/src/question/plugin.rs +++ b/src/question/plugin.rs @@ -1,21 +1,26 @@ -use crate::{error, Answer, Answers}; +use ui::{backend::Backend, error, events}; -use super::{Options, Question}; +use super::{Options, Question, QuestionKind}; +use crate::{Answer, Answers}; pub trait Plugin: std::fmt::Debug { fn ask( &mut self, message: String, answers: &Answers, - stdout: &mut dyn std::io::Write, + stdout: &mut dyn Backend, + events: &mut events::Events, ) -> error::Result; + crate::cfg_async! { fn ask_async<'future>( &mut self, message: String, answers: &Answers, - stdout: &mut dyn std::io::Write, - ) -> super::BoxFuture<'future, error::Result>; + stdout: &mut dyn Backend, + events: &mut events::AsyncEvents, + ) -> crate::question::BoxFuture<'future, error::Result>; + } } pub struct PluginBuilder<'m, 'w, 'p> { @@ -30,7 +35,10 @@ impl<'p, P: Plugin + 'p> From

for Box { } impl Question<'static, 'static, 'static, 'static, 'static> { - pub fn plugin<'a, N, P>(name: N, plugin: P) -> PluginBuilder<'static, 'static, 'a> + pub fn plugin<'a, N, P>( + name: N, + plugin: P, + ) -> PluginBuilder<'static, 'static, 'a> where N: Into, P: Into>, @@ -51,11 +59,13 @@ crate::impl_options_builder!(PluginBuilder<'q>; (this, opts) => { impl<'m, 'w, 'q> PluginBuilder<'m, 'w, 'q> { pub fn build(self) -> Question<'m, 'w, 'q, 'static, 'static> { - Question::new(self.opts, super::QuestionKind::Plugin(self.plugin)) + Question::new(self.opts, QuestionKind::Plugin(self.plugin)) } } -impl<'m, 'w, 'q> From> for Question<'m, 'w, 'q, 'static, 'static> { +impl<'m, 'w, 'q> From> + for Question<'m, 'w, 'q, 'static, 'static> +{ fn from(builder: PluginBuilder<'m, 'w, 'q>) -> Self { builder.build() } diff --git a/src/question/rawlist.rs b/src/question/rawlist.rs index 35110de..e8568f6 100644 --- a/src/question/rawlist.rs +++ b/src/question/rawlist.rs @@ -1,17 +1,13 @@ -use crossterm::{ - event, queue, - style::{Color, Colorize, ResetColor, SetForegroundColor}, - terminal, -}; -use ui::{widgets, Prompt, Validation, Widget}; -use widgets::List; - -use crate::{ - answer::{Answer, ListItem}, - error, Answers, +use ui::{ + backend::{Backend, Color, Stylize}, + error, + events::KeyEvent, + widgets::{self, List}, + Prompt, Validation, Widget, }; use super::{Choice, Options, Transformer}; +use crate::{Answer, Answers, ListItem}; #[derive(Debug, Default)] pub struct Rawlist<'t> { @@ -92,30 +88,27 @@ impl ui::AsyncPrompt for RawlistPrompt<'_> { const ANSWER_PROMPT: &[u8] = b" Answer: "; impl Widget for RawlistPrompt<'_> { - fn render(&mut self, _: usize, w: &mut W) -> crossterm::Result<()> { - let max_width = terminal::size()?.0 as usize; - self.list.render(max_width, w)?; - w.write_all(ANSWER_PROMPT)?; - self.input.render(max_width - ANSWER_PROMPT.len(), w) + fn render(&mut self, _: usize, b: &mut B) -> error::Result<()> { + let max_width = b.size()?.width as usize; + self.list.render(max_width, b)?; + b.write_all(ANSWER_PROMPT)?; + self.input.render(max_width - ANSWER_PROMPT.len(), b) } fn height(&self) -> usize { self.list.height() + 1 } - fn handle_key(&mut self, key: event::KeyEvent) -> bool { + fn handle_key(&mut self, key: KeyEvent) -> bool { if self.input.handle_key(key) { - if let Ok(mut n) = self.input.value().parse::() { + if let Ok(n) = self.input.value().parse::() { if n < self.list.list.len() && n > 0 { - // Choices are 1 indexed for the user - n -= 1; - - let pos = self.list.list.choices.choices[n..] - .iter() - .position(|choice| matches!(choice, Choice::Choice((i, _)) if *i == n)); + let pos = self.list.list.choices.choices[(n-1)..].iter().position( + |choice| matches!(choice, Choice::Choice((i, _)) if *i == n), + ); if let Some(pos) = pos { - self.list.set_at(pos + n); + self.list.set_at(pos + n - 1); return true; } } @@ -125,7 +118,7 @@ impl Widget for RawlistPrompt<'_> { true } else if self.list.handle_key(key) { let at = self.list.get_at(); - let index = self.list.list.choices[at].as_ref().unwrap_choice().0 + 1; + let index = self.list.list.choices[at].as_ref().unwrap_choice().0; self.input.set_value(index.to_string()); true } else { @@ -140,32 +133,32 @@ impl Widget for RawlistPrompt<'_> { } impl widgets::List for Rawlist<'_> { - fn render_item( + fn render_item( &mut self, index: usize, hovered: bool, max_width: usize, - w: &mut W, - ) -> crossterm::Result<()> { + b: &mut B, + ) -> error::Result<()> { match &self.choices[index] { Choice::Choice((index, name)) => { if hovered { - queue!(w, SetForegroundColor(Color::DarkCyan))?; + b.set_fg(Color::Cyan)?; } - write!(w, " {}) ", index + 1)?; + write!(b, " {}) ", index)?; name.as_str() - .render(max_width - (*index as f64).log10() as usize + 5, w)?; + .render(max_width - (*index as f64).log10() as usize + 5, b)?; if hovered { - queue!(w, ResetColor)?; + b.set_fg(Color::Reset)?; } } Choice::Separator(s) => { - queue!(w, SetForegroundColor(Color::DarkGrey))?; - w.write_all(b" ")?; - super::get_sep_str(s).render(max_width - 3, w)?; - queue!(w, ResetColor)?; + b.set_fg(Color::DarkGrey)?; + b.write_all(b" ")?; + super::get_sep_str(s).render(max_width - 3, b)?; + b.set_fg(Color::Reset)?; } } @@ -190,11 +183,49 @@ impl widgets::List for Rawlist<'_> { } impl Rawlist<'_> { - pub(crate) fn ask( + pub(crate) fn ask( mut self, message: String, answers: &Answers, - w: &mut W, + b: &mut B, + events: &mut ui::events::Events, + ) -> error::Result { + let transformer = self.transformer.take(); + + let mut list = widgets::ListPicker::new(self); + if let Some(default) = list.list.choices.default() { + list.set_at(default); + } + + let ans = ui::Input::new( + RawlistPrompt { + input: widgets::StringInput::new(|c| c.is_digit(10).then(|| c)), + list, + message, + }, + b, + ) + .run(events)?; + + match transformer { + Transformer::Sync(transformer) => transformer(&ans, answers, b)?, + _ => { + b.write_styled(ans.name.as_str().cyan())?; + b.write_all(b"\n")?; + b.flush()?; + } + } + + Ok(Answer::ListItem(ans)) + } + + crate::cfg_async! { + pub(crate) async fn ask_async( + mut self, + message: String, + answers: &Answers, + b: &mut B, + events: &mut ui::events::AsyncEvents, ) -> error::Result { let transformer = self.transformer.take(); @@ -207,43 +238,18 @@ impl Rawlist<'_> { input: widgets::StringInput::new(|c| c.is_digit(10).then(|| c)), list, message, - }) - .run(w)?; - - match transformer { - Transformer::Sync(transformer) => transformer(&ans, answers, w)?, - _ => writeln!(w, "{}", ans.name.as_str().dark_cyan())?, - } - - Ok(Answer::ListItem(ans)) - } - - crate::cfg_async! { - pub(crate) async fn ask_async( - mut self, - message: String, - answers: &Answers, - w: &mut W, - ) -> error::Result { - let transformer = self.transformer.take(); - - let mut list = widgets::ListPicker::new(self); - if let Some(default) = list.list.choices.default() { - list.set_at(default); - } - - let ans = ui::AsyncInput::new(RawlistPrompt { - input: widgets::StringInput::new(|c| c.is_digit(10).then(|| c)), - list, - message, - }) - .run(w) + }, b) + .run_async(events) .await?; match transformer { - Transformer::Async(transformer) => transformer(&ans, answers, w).await?, - Transformer::Sync(transformer) => transformer(&ans, answers, w)?, - _ => writeln!(w, "{}", ans.name.as_str().dark_cyan())?, + Transformer::Async(transformer) => transformer(&ans, answers, b).await?, + Transformer::Sync(transformer) => transformer(&ans, answers, b)?, + _ => { + b.write_styled(ans.name.as_str().cyan())?; + b.write_all(b"\n")?; + b.flush()? + } } Ok(Answer::ListItem(ans)) @@ -348,11 +354,14 @@ crate::impl_transformer_builder!(RawlistBuilder<'m, 'w, t> ListItem; (this, tran }); impl super::Question<'static, 'static, 'static, 'static, 'static> { - pub fn rawlist>(name: N) -> RawlistBuilder<'static, 'static, 'static> { + pub fn rawlist>( + name: N, + ) -> RawlistBuilder<'static, 'static, 'static> { RawlistBuilder { opts: Options::new(name.into()), list: Default::default(), - choice_count: 0, + // It is one indexed for the user + choice_count: 1, } } } diff --git a/ui/Cargo.toml b/ui/Cargo.toml index f941150..bd614a0 100644 --- a/ui/Cargo.toml +++ b/ui/Cargo.toml @@ -1,19 +1,36 @@ [package] name = "ui" -version = "0.1.0" +version = "0.0.1" authors = ["Lutetium Vanadium"] edition = "2018" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] -crossterm = "0.19" +bitflags = "1.2" lazy_static = "1.4" unicode-segmentation = "1.7" async-trait = { version = "0.1", optional = true } -futures = { version = "0.3", optional = true, default-features = false } +futures = { version = "0.3", optional = true } + +crossterm = { version = "0.19", optional = true } +# TODO: add all of these +# termion = { version = "1.5", optional = true } +# easycurses = { version = "0.12", optional = true } +# pancurses = { version = "0.16", optional = true, features = ["win32a"] } + +# The following dependencies are renamed in the form `-dep` to allow +# a feature name with the same name to be present. This is necessary since +# these features are required to enable other dependencies +smol-dep = { package = "smol", version = "1.2", optional = true } +tokio-dep = { package = "tokio", version = "1.5", optional = true, features = ["fs", "rt"] } +async-std-dep = { package = "async-std", version = "1.9", optional = true, features = ["unstable"] } [features] default = [] -async = ["async-trait", "futures", "crossterm/event-stream"] +# curses = ["easycurses", "pancurses"] +tokio = ["tokio-dep", "async-trait", "futures", "anes"] +smol = ["smol-dep", "async-trait", "futures", "anes"] +async-std = ["async-std-dep", "async-trait", "futures", "anes"] + +[target.'cfg(unix)'.dependencies] +anes = { version = "0.1", optional = true, features = ["parser"] } diff --git a/ui/src/async_input.rs b/ui/src/async_input.rs index 0219b16..2fea6d5 100644 --- a/ui/src/async_input.rs +++ b/ui/src/async_input.rs @@ -1,14 +1,14 @@ -use std::{convert::TryFrom, io}; +use std::io; use async_trait::async_trait; -use crossterm::{ - cursor, event, execute, queue, - style::{Colorize, Print, PrintStyledContent, Styler}, - terminal, -}; use futures::StreamExt; -use crate::{Prompt, RawMode, Validation}; +use super::{Input, Prompt, Validation}; +use crate::{ + backend::Backend, + error, + events::{AsyncEvents, KeyCode, KeyModifiers}, +}; /// This trait should be implemented by all 'root' widgets. /// @@ -30,87 +30,20 @@ pub trait AsyncPrompt: Prompt { Ok(Validation::Finish) } - /// The value to return from [`AsyncInput::run`]. This will only be called once validation returns + /// The value to return from [`Input::run_async`]. This will only be called once validation returns /// [`Validation::Finish`]; async fn finish_async(self) -> Self::Output; } -/// The ui runner. It renders and processes events with the help of a type that implements [`AsyncPrompt`] -/// -/// See [`run`](AsyncInput::run) for more information -pub struct AsyncInput

{ - prompt: P, - terminal_h: u16, - terminal_w: u16, - base_row: u16, - base_col: u16, - hide_cursor: bool, -} - -impl AsyncInput

{ - fn adjust_scrollback( - &self, - height: usize, - stdout: &mut W, - ) -> crossterm::Result { - let th = self.terminal_h as usize; - - let mut base_row = self.base_row; - - if self.base_row as usize >= th - height { - let dist = (self.base_row as usize + height - th + 1) as u16; - base_row -= dist; - queue!(stdout, terminal::ScrollUp(dist), cursor::MoveUp(dist))?; - } - - Ok(base_row) - } - - fn set_cursor_pos(&self, stdout: &mut W) -> crossterm::Result<()> { - let (dcw, dch) = self.prompt.cursor_pos(self.base_col); - execute!(stdout, cursor::MoveTo(dcw, self.base_row + dch)) - } - - fn render(&mut self, stdout: &mut W) -> crossterm::Result<()> { - let height = self.prompt.height(); - self.base_row = self.adjust_scrollback(height, stdout)?; - self.clear(self.base_col, stdout)?; - queue!(stdout, cursor::MoveTo(self.base_col, self.base_row))?; - - self.prompt - .render((self.terminal_w - self.base_col) as usize, stdout)?; - - self.set_cursor_pos(stdout) - } - - fn clear(&self, prompt_len: u16, stdout: &mut W) -> crossterm::Result<()> { - if self.base_row + 1 < self.terminal_h { - queue!( - stdout, - cursor::MoveTo(0, self.base_row + 1), - terminal::Clear(terminal::ClearType::FromCursorDown), - )?; - } - - queue!( - stdout, - cursor::MoveTo(prompt_len, self.base_row), - terminal::Clear(terminal::ClearType::UntilNewLine), - ) - } - +impl Input { #[inline] - async fn finish( - self, + async fn finish_async( + mut self, pressed_enter: bool, prompt_len: u16, - stdout: &mut W, - ) -> crossterm::Result { - self.clear(prompt_len, stdout)?; - if self.hide_cursor { - queue!(stdout, cursor::Show)?; - } - stdout.flush()?; + ) -> error::Result { + self.clear(prompt_len)?; + self.reset_terminal()?; if pressed_enter { Ok(self.prompt.finish_async().await) @@ -121,140 +54,49 @@ impl AsyncInput

{ /// Run the ui on the given writer. It will return when the user presses `Enter` or `Escape` /// based on the [`AsyncPrompt`] implementation. - pub async fn run(mut self, stdout: &mut W) -> crossterm::Result { - let (tw, th) = terminal::size()?; - self.terminal_h = th; - self.terminal_w = tw; - - let prompt = self.prompt.prompt(); - let prompt_len = u16::try_from(prompt.chars().count() + 3).expect("really big prompt"); - - let raw_mode = RawMode::enable()?; - if self.hide_cursor { - queue!(stdout, cursor::Hide)?; - }; - - let height = self.prompt.height(); - self.base_row = cursor::position()?.1; - self.base_row = self.adjust_scrollback(height, stdout)?; - - queue!( - stdout, - PrintStyledContent("? ".green()), - PrintStyledContent(prompt.bold()), - Print(' '), - )?; - - let hint_len = match self.prompt.hint() { - Some(hint) => { - queue!(stdout, PrintStyledContent(hint.dark_grey()), Print(' '))?; - u16::try_from(hint.chars().count() + 1).expect("really big prompt") - } - None => 0, - }; - - self.base_col = prompt_len + hint_len; - - self.render(stdout)?; - - let mut events = event::EventStream::new(); + pub async fn run_async(mut self, events: &mut AsyncEvents) -> error::Result { + let prompt_len = self.init()?; loop { - match events.next().await.unwrap()? { - event::Event::Resize(tw, th) => { - self.terminal_w = tw; - self.terminal_h = th; + let e = events.next().await.unwrap()?; + let key_handled = match e.code { + KeyCode::Char('c') if e.modifiers.contains(KeyModifiers::CONTROL) => { + self.exit()?; + return Err(io::Error::new(io::ErrorKind::Other, "CTRL+C").into()); + } + KeyCode::Null => { + self.exit()?; + return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "EOF").into()); + } + KeyCode::Esc if self.prompt.has_default() => { + return self.finish_async(false, prompt_len).await; } - event::Event::Key(e) => { - let key_handled = match e.code { - event::KeyCode::Char('c') - if e.modifiers.contains(event::KeyModifiers::CONTROL) => - { - queue!( - stdout, - cursor::MoveTo(0, self.base_row + self.prompt.height() as u16) - )?; - drop(raw_mode); - if self.hide_cursor { - queue!(stdout, cursor::Show)?; - } - crate::exit(); - - return Err(io::Error::new(io::ErrorKind::Other, "CTRL+C").into()); - } - event::KeyCode::Null => { - queue!( - stdout, - cursor::MoveTo(0, self.base_row + self.prompt.height() as u16) - )?; - drop(raw_mode); - if self.hide_cursor { - queue!(stdout, cursor::Show)?; - } - crate::exit(); - return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "EOF").into()); - } - event::KeyCode::Esc if self.prompt.has_default() => { - return self.finish(false, prompt_len, stdout).await; - } - - event::KeyCode::Enter => { - let result = match self.prompt.try_validate_sync() { - Some(res) => res, - None => self.prompt.validate_async().await, - }; - - match result { - Ok(Validation::Finish) => { - return self.finish(true, prompt_len, stdout).await; - } - Ok(Validation::Continue) => true, - Err(e) => { - let height = self.prompt.height() + 1; - self.base_row = self.adjust_scrollback(height, stdout)?; - - queue!( - stdout, - cursor::MoveTo(0, self.base_row + height as u16) - )?; - write!(stdout, "{} {}", ">>".dark_red(), e)?; - - self.set_cursor_pos(stdout)?; - - continue; - } - } - } - _ => self.prompt.handle_key(e), + KeyCode::Enter => { + let result = match self.prompt.try_validate_sync() { + Some(res) => res, + None => self.prompt.validate_async().await, }; - if key_handled { - self.render(stdout)?; + match result { + Ok(Validation::Finish) => { + return self.finish_async(true, prompt_len).await; + } + Ok(Validation::Continue) => true, + Err(e) => { + self.print_error(e)?; + continue; + } } } - _ => {} + _ => self.prompt.handle_key(e), + }; + + if key_handled { + self.size = self.backend.size()?; + + self.render()?; } } } } - -impl

AsyncInput

{ - /// Creates a new AsyncInput - pub fn new(prompt: P) -> Self { - Self { - prompt, - base_row: 0, - base_col: 0, - terminal_h: 0, - terminal_w: 0, - hide_cursor: false, - } - } - - /// Hides the cursor while running the input - pub fn hide_cursor(mut self) -> Self { - self.hide_cursor = true; - self - } -} diff --git a/ui/src/backend/crossterm.rs b/ui/src/backend/crossterm.rs new file mode 100644 index 0000000..0eb1208 --- /dev/null +++ b/ui/src/backend/crossterm.rs @@ -0,0 +1,219 @@ +use std::io::{self, Write}; + +use crossterm::{ + cursor, queue, + style::{ + Attribute as CAttribute, Color as CColor, SetAttribute, SetBackgroundColor, + SetForegroundColor, + }, + terminal, +}; + +use super::{Attributes, Backend, ClearType, Color, MoveDirection, Size}; +use crate::error; + +pub struct CrosstermBackend { + buffer: W, +} + +impl CrosstermBackend { + pub fn new(buffer: W) -> error::Result> { + Ok(CrosstermBackend { buffer }) + } +} + +impl Write for CrosstermBackend { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.buffer.write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.buffer.flush() + } +} + +impl Backend for CrosstermBackend { + fn enable_raw_mode(&mut self) -> error::Result<()> { + terminal::enable_raw_mode().map_err(Into::into) + } + + fn disable_raw_mode(&mut self) -> error::Result<()> { + terminal::disable_raw_mode().map_err(Into::into) + } + + fn hide_cursor(&mut self) -> error::Result<()> { + queue!(self.buffer, cursor::Hide).map_err(Into::into) + } + + fn show_cursor(&mut self) -> error::Result<()> { + queue!(self.buffer, cursor::Show).map_err(Into::into) + } + + fn get_cursor(&mut self) -> error::Result<(u16, u16)> { + cursor::position().map_err(Into::into) + } + + fn set_cursor(&mut self, x: u16, y: u16) -> error::Result<()> { + queue!(self.buffer, cursor::MoveTo(x, y)).map_err(Into::into) + } + + fn move_cursor(&mut self, direction: MoveDirection) -> error::Result<()> { + match direction { + MoveDirection::Up(n) => queue!(self.buffer, cursor::MoveUp(n)), + MoveDirection::Down(n) => queue!(self.buffer, cursor::MoveDown(n)), + MoveDirection::Left(n) => queue!(self.buffer, cursor::MoveLeft(n)), + MoveDirection::Right(n) => queue!(self.buffer, cursor::MoveRight(n)), + MoveDirection::NextLine(n) => { + queue!(self.buffer, cursor::MoveToNextLine(n)) + } + MoveDirection::Column(n) => queue!(self.buffer, cursor::MoveToColumn(n)), + MoveDirection::PrevLine(n) => { + queue!(self.buffer, cursor::MoveToPreviousLine(n)) + } + } + .map_err(Into::into) + } + + fn scroll(&mut self, dist: i32) -> error::Result<()> { + if dist >= 0 { + queue!(self.buffer, terminal::ScrollDown(dist as u16))?; + } else { + queue!(self.buffer, terminal::ScrollUp(-dist as u16))?; + } + Ok(()) + } + + fn set_attributes(&mut self, attributes: Attributes) -> error::Result<()> { + set_attributes(attributes, &mut self.buffer) + } + + fn remove_attributes(&mut self, attributes: Attributes) -> error::Result<()> { + remove_attributes(attributes, &mut self.buffer) + } + + fn set_fg(&mut self, color: Color) -> error::Result<()> { + queue!(self.buffer, SetForegroundColor(color.into())).map_err(Into::into) + } + + fn set_bg(&mut self, color: Color) -> error::Result<()> { + queue!(self.buffer, SetBackgroundColor(color.into())).map_err(Into::into) + } + + fn clear(&mut self, clear_type: ClearType) -> error::Result<()> { + queue!(self.buffer, terminal::Clear(clear_type.into())).map_err(Into::into) + } + + fn size(&self) -> error::Result { + terminal::size().map(Into::into).map_err(Into::into) + } +} + +impl From 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::Grey => CColor::Grey, + Color::DarkGrey => 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::Ansi(i) => CColor::AnsiValue(i), + Color::Rgb(r, g, b) => CColor::Rgb { r, g, b }, + } + } +} + +impl From for terminal::ClearType { + fn from(clear: ClearType) -> Self { + match clear { + ClearType::All => terminal::ClearType::All, + ClearType::FromCursorDown => terminal::ClearType::FromCursorDown, + ClearType::FromCursorUp => terminal::ClearType::FromCursorUp, + ClearType::CurrentLine => terminal::ClearType::CurrentLine, + ClearType::UntilNewLine => terminal::ClearType::UntilNewLine, + } + } +} + +fn set_attributes(attributes: Attributes, mut w: W) -> error::Result<()> { + if attributes.contains(Attributes::RESET) { + return queue!(w, SetAttribute(CAttribute::Reset)).map_err(Into::into); + } + + if attributes.contains(Attributes::REVERSED) { + queue!(w, SetAttribute(CAttribute::Reverse))?; + } + if attributes.contains(Attributes::BOLD) { + queue!(w, SetAttribute(CAttribute::Bold))?; + } + if attributes.contains(Attributes::ITALIC) { + queue!(w, SetAttribute(CAttribute::Italic))?; + } + if attributes.contains(Attributes::UNDERLINED) { + queue!(w, SetAttribute(CAttribute::Underlined))?; + } + if attributes.contains(Attributes::DIM) { + queue!(w, SetAttribute(CAttribute::Dim))?; + } + if attributes.contains(Attributes::CROSSED_OUT) { + queue!(w, SetAttribute(CAttribute::CrossedOut))?; + } + if attributes.contains(Attributes::SLOW_BLINK) { + queue!(w, SetAttribute(CAttribute::SlowBlink))?; + } + if attributes.contains(Attributes::RAPID_BLINK) { + queue!(w, SetAttribute(CAttribute::RapidBlink))?; + } + if attributes.contains(Attributes::HIDDEN) { + queue!(w, SetAttribute(CAttribute::Hidden))?; + } + + Ok(()) +} + +fn remove_attributes( + attributes: Attributes, + mut w: W, +) -> error::Result<()> { + if attributes.contains(Attributes::RESET) { + return queue!(w, SetAttribute(CAttribute::Reset)).map_err(Into::into); + } + + if attributes.contains(Attributes::REVERSED) { + queue!(w, SetAttribute(CAttribute::NoReverse))?; + } + if attributes.contains(Attributes::BOLD) { + queue!(w, SetAttribute(CAttribute::NoBold))?; + } + if attributes.contains(Attributes::ITALIC) { + queue!(w, SetAttribute(CAttribute::NoItalic))?; + } + if attributes.contains(Attributes::UNDERLINED) { + queue!(w, SetAttribute(CAttribute::NoUnderline))?; + } + if attributes.contains(Attributes::DIM) { + queue!(w, SetAttribute(CAttribute::NormalIntensity))?; + } + if attributes.contains(Attributes::CROSSED_OUT) { + queue!(w, SetAttribute(CAttribute::NotCrossedOut))?; + } + if attributes.contains(Attributes::SLOW_BLINK | Attributes::RAPID_BLINK) { + queue!(w, SetAttribute(CAttribute::NoBlink))?; + } + if attributes.contains(Attributes::HIDDEN) { + queue!(w, SetAttribute(CAttribute::NoHidden))?; + } + + Ok(()) +} diff --git a/ui/src/backend/curses.rs b/ui/src/backend/curses.rs new file mode 100644 index 0000000..ff89920 --- /dev/null +++ b/ui/src/backend/curses.rs @@ -0,0 +1,286 @@ +use std::io; + +use crate::backend::Backend; +use crate::buffer::Cell; +use crate::layout::Rect; +use crate::style::{Color, Modifier}; +use crate::symbols::{bar, block}; +#[cfg(unix)] +use crate::symbols::{line, DOT}; +#[cfg(unix)] +use pancurses::{chtype, ToChtype}; +use unicode_segmentation::UnicodeSegmentation; + +pub struct CursesBackend { + curses: easycurses::EasyCurses, +} + +impl CursesBackend { + pub fn new() -> Option { + let curses = easycurses::EasyCurses::initialize_system()?; + Some(CursesBackend { curses }) + } + + pub fn with_curses(curses: easycurses::EasyCurses) -> CursesBackend { + CursesBackend { curses } + } + + pub fn get_curses(&self) -> &easycurses::EasyCurses { + &self.curses + } + + pub fn get_curses_mut(&mut self) -> &mut easycurses::EasyCurses { + &mut self.curses + } +} + +impl Backend for CursesBackend { + fn draw<'a, I>(&mut self, content: I) -> io::Result<()> + where + I: Iterator, + { + let mut last_col = 0; + let mut last_row = 0; + let mut fg = Color::Reset; + let mut bg = Color::Reset; + let mut modifier = Modifier::empty(); + let mut curses_style = CursesStyle { + fg: easycurses::Color::White, + bg: easycurses::Color::Black, + }; + let mut update_color = false; + for (col, row, cell) in content { + if row != last_row || col != last_col + 1 { + self.curses.move_rc(i32::from(row), i32::from(col)); + } + last_col = col; + last_row = row; + if cell.modifier != modifier { + apply_modifier_diff(&mut self.curses.win, modifier, cell.modifier); + modifier = cell.modifier; + }; + if cell.fg != fg { + update_color = true; + if let Some(ccolor) = cell.fg.into() { + fg = cell.fg; + curses_style.fg = ccolor; + } else { + fg = Color::White; + curses_style.fg = easycurses::Color::White; + } + }; + if cell.bg != bg { + update_color = true; + if let Some(ccolor) = cell.bg.into() { + bg = cell.bg; + curses_style.bg = ccolor; + } else { + bg = Color::Black; + curses_style.bg = easycurses::Color::Black; + } + }; + if update_color { + self.curses.set_color_pair(easycurses::ColorPair::new( + curses_style.fg, + curses_style.bg, + )); + }; + update_color = false; + draw(&mut self.curses, cell.symbol.as_str()); + } + self.curses.win.attrset(pancurses::Attribute::Normal); + self.curses.set_color_pair(easycurses::ColorPair::new( + easycurses::Color::White, + easycurses::Color::Black, + )); + Ok(()) + } + fn hide_cursor(&mut self) -> io::Result<()> { + self.curses + .set_cursor_visibility(easycurses::CursorVisibility::Invisible); + Ok(()) + } + fn show_cursor(&mut self) -> io::Result<()> { + self.curses + .set_cursor_visibility(easycurses::CursorVisibility::Visible); + Ok(()) + } + fn get_cursor(&mut self) -> io::Result<(u16, u16)> { + let (y, x) = self.curses.get_cursor_rc(); + Ok((x as u16, y as u16)) + } + fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> { + self.curses.move_rc(i32::from(y), i32::from(x)); + Ok(()) + } + fn clear(&mut self) -> io::Result<()> { + self.curses.clear(); + // self.curses.refresh(); + Ok(()) + } + fn size(&self) -> Result { + let (nrows, ncols) = self.curses.get_row_col_count(); + Ok(Rect::new(0, 0, ncols as u16, nrows as u16)) + } + fn flush(&mut self) -> io::Result<()> { + self.curses.refresh(); + Ok(()) + } +} + +struct CursesStyle { + fg: easycurses::Color, + bg: easycurses::Color, +} + +#[cfg(unix)] +/// Deals with lack of unicode support for ncurses on unix +fn draw(curses: &mut easycurses::EasyCurses, symbol: &str) { + for grapheme in symbol.graphemes(true) { + let ch = match grapheme { + line::TOP_RIGHT => pancurses::ACS_URCORNER(), + line::VERTICAL => pancurses::ACS_VLINE(), + line::HORIZONTAL => pancurses::ACS_HLINE(), + line::TOP_LEFT => pancurses::ACS_ULCORNER(), + line::BOTTOM_RIGHT => pancurses::ACS_LRCORNER(), + line::BOTTOM_LEFT => pancurses::ACS_LLCORNER(), + line::VERTICAL_LEFT => pancurses::ACS_RTEE(), + line::VERTICAL_RIGHT => pancurses::ACS_LTEE(), + line::HORIZONTAL_DOWN => pancurses::ACS_TTEE(), + line::HORIZONTAL_UP => pancurses::ACS_BTEE(), + block::FULL => pancurses::ACS_BLOCK(), + block::SEVEN_EIGHTHS => pancurses::ACS_BLOCK(), + block::THREE_QUARTERS => pancurses::ACS_BLOCK(), + block::FIVE_EIGHTHS => pancurses::ACS_BLOCK(), + block::HALF => pancurses::ACS_BLOCK(), + block::THREE_EIGHTHS => ' ' as chtype, + block::ONE_QUARTER => ' ' as chtype, + block::ONE_EIGHTH => ' ' as chtype, + bar::SEVEN_EIGHTHS => pancurses::ACS_BLOCK(), + bar::THREE_QUARTERS => pancurses::ACS_BLOCK(), + bar::FIVE_EIGHTHS => pancurses::ACS_BLOCK(), + bar::HALF => pancurses::ACS_BLOCK(), + bar::THREE_EIGHTHS => pancurses::ACS_S9(), + bar::ONE_QUARTER => pancurses::ACS_S9(), + bar::ONE_EIGHTH => pancurses::ACS_S9(), + DOT => pancurses::ACS_BULLET(), + unicode_char => { + if unicode_char.is_ascii() { + let mut chars = unicode_char.chars(); + if let Some(ch) = chars.next() { + ch.to_chtype() + } else { + pancurses::ACS_BLOCK() + } + } else { + pancurses::ACS_BLOCK() + } + } + }; + curses.win.addch(ch); + } +} + +#[cfg(windows)] +fn draw(curses: &mut easycurses::EasyCurses, symbol: &str) { + for grapheme in symbol.graphemes(true) { + let ch = match grapheme { + block::SEVEN_EIGHTHS => block::FULL, + block::THREE_QUARTERS => block::FULL, + block::FIVE_EIGHTHS => block::HALF, + block::THREE_EIGHTHS => block::HALF, + block::ONE_QUARTER => block::HALF, + block::ONE_EIGHTH => " ", + bar::SEVEN_EIGHTHS => bar::FULL, + bar::THREE_QUARTERS => bar::FULL, + bar::FIVE_EIGHTHS => bar::HALF, + bar::THREE_EIGHTHS => bar::HALF, + bar::ONE_QUARTER => bar::HALF, + bar::ONE_EIGHTH => " ", + ch => ch, + }; + // curses.win.addch(ch); + curses.print(ch); + } +} + +impl From for Option { + fn from(color: Color) -> Option { + match color { + Color::Reset => None, + Color::Black => Some(easycurses::Color::Black), + Color::Red | Color::LightRed => Some(easycurses::Color::Red), + Color::Green | Color::LightGreen => Some(easycurses::Color::Green), + Color::Yellow | Color::LightYellow => Some(easycurses::Color::Yellow), + Color::Magenta | Color::LightMagenta => Some(easycurses::Color::Magenta), + Color::Cyan | Color::LightCyan => Some(easycurses::Color::Cyan), + Color::White | Color::Gray | Color::DarkGray => { + Some(easycurses::Color::White) + } + Color::Blue | Color::LightBlue => Some(easycurses::Color::Blue), + Color::Indexed(_) => None, + Color::Rgb(_, _, _) => None, + } + } +} + +fn apply_modifier_diff(win: &mut pancurses::Window, from: Modifier, to: Modifier) { + remove_modifier(win, from - to); + add_modifier(win, to - from); +} + +fn remove_modifier(win: &mut pancurses::Window, remove: Modifier) { + if remove.contains(Modifier::BOLD) { + win.attroff(pancurses::Attribute::Bold); + } + if remove.contains(Modifier::DIM) { + win.attroff(pancurses::Attribute::Dim); + } + if remove.contains(Modifier::ITALIC) { + win.attroff(pancurses::Attribute::Italic); + } + if remove.contains(Modifier::UNDERLINED) { + win.attroff(pancurses::Attribute::Underline); + } + if remove.contains(Modifier::SLOW_BLINK) + || remove.contains(Modifier::RAPID_BLINK) + { + win.attroff(pancurses::Attribute::Blink); + } + if remove.contains(Modifier::REVERSED) { + win.attroff(pancurses::Attribute::Reverse); + } + if remove.contains(Modifier::HIDDEN) { + win.attroff(pancurses::Attribute::Invisible); + } + if remove.contains(Modifier::CROSSED_OUT) { + win.attroff(pancurses::Attribute::Strikeout); + } +} + +fn add_modifier(win: &mut pancurses::Window, add: Modifier) { + if add.contains(Modifier::BOLD) { + win.attron(pancurses::Attribute::Bold); + } + if add.contains(Modifier::DIM) { + win.attron(pancurses::Attribute::Dim); + } + if add.contains(Modifier::ITALIC) { + win.attron(pancurses::Attribute::Italic); + } + if add.contains(Modifier::UNDERLINED) { + win.attron(pancurses::Attribute::Underline); + } + if add.contains(Modifier::SLOW_BLINK) || add.contains(Modifier::RAPID_BLINK) { + win.attron(pancurses::Attribute::Blink); + } + if add.contains(Modifier::REVERSED) { + win.attron(pancurses::Attribute::Reverse); + } + if add.contains(Modifier::HIDDEN) { + win.attron(pancurses::Attribute::Invisible); + } + if add.contains(Modifier::CROSSED_OUT) { + win.attron(pancurses::Attribute::Strikeout); + } +} diff --git a/ui/src/backend/mod.rs b/ui/src/backend/mod.rs new file mode 100644 index 0000000..43f3b4c --- /dev/null +++ b/ui/src/backend/mod.rs @@ -0,0 +1,155 @@ +use crate::error; + +pub fn get_backend(buf: W) -> error::Result { + #[cfg(feature = "crossterm")] + CrosstermBackend::new(buf) +} + +mod style; +// #[cfg(feature = "termion")] +// mod termion; +// #[cfg(feature = "termion")] +// pub use self::termion::TermionBackend; + +#[cfg(feature = "crossterm")] +mod crossterm; +#[cfg(feature = "crossterm")] +pub use self::crossterm::CrosstermBackend; + +// #[cfg(feature = "curses")] +// mod curses; +// #[cfg(feature = "curses")] +// pub use self::curses::CursesBackend; + +pub use style::*; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, Default)] +pub struct Size { + pub width: u16, + pub height: u16, +} + +impl From<(u16, u16)> for Size { + fn from((width, height): (u16, u16)) -> Self { + Size { width, height } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +pub enum ClearType { + All, + /// All cells from the cursor position downwards. + FromCursorDown, + /// All cells from the cursor position upwards. + FromCursorUp, + /// All cells at the cursor row. + CurrentLine, + /// All cells from the cursor position until the new line. + UntilNewLine, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +pub enum MoveDirection { + Up(u16), + Down(u16), + Left(u16), + Right(u16), + NextLine(u16), + Column(u16), + PrevLine(u16), +} + +pub trait Backend: std::io::Write { + fn enable_raw_mode(&mut self) -> error::Result<()>; + fn disable_raw_mode(&mut self) -> error::Result<()>; + fn hide_cursor(&mut self) -> error::Result<()>; + fn show_cursor(&mut self) -> error::Result<()>; + fn get_cursor(&mut self) -> error::Result<(u16, u16)>; + fn set_cursor(&mut self, x: u16, y: u16) -> error::Result<()>; + fn move_cursor(&mut self, direction: MoveDirection) -> error::Result<()> { + default_move_cursor(self, direction) + } + fn scroll(&mut self, dist: i32) -> error::Result<()>; + fn set_attributes(&mut self, attributes: Attributes) -> error::Result<()>; + fn remove_attributes(&mut self, attributes: Attributes) -> error::Result<()>; + fn set_fg(&mut self, color: Color) -> error::Result<()>; + fn set_bg(&mut self, color: Color) -> error::Result<()>; + fn write_styled(&mut self, styled: Styled<'_>) -> error::Result<()> { + styled.write(self) + } + fn clear(&mut self, clear_type: ClearType) -> error::Result<()>; + fn size(&self) -> error::Result; +} + +fn default_move_cursor( + backend: &mut B, + direction: MoveDirection, +) -> error::Result<()> { + let (mut x, mut y) = backend.get_cursor()?; + + match direction { + MoveDirection::Up(dy) => y = y.saturating_sub(dy), + MoveDirection::Down(dy) => y = y.saturating_add(dy), + MoveDirection::Left(dx) => x = x.saturating_sub(dx), + MoveDirection::Right(dx) => x = x.saturating_add(dx), + MoveDirection::NextLine(dy) => { + x = 0; + y = y.saturating_add(dy); + } + MoveDirection::Column(new_x) => x = new_x, + MoveDirection::PrevLine(dy) => { + x = 0; + y = y.saturating_sub(dy); + } + } + + backend.set_cursor(x, y) +} + +impl<'a, B: Backend> Backend for &'a mut B { + fn enable_raw_mode(&mut self) -> error::Result<()> { + (**self).enable_raw_mode() + } + fn disable_raw_mode(&mut self) -> error::Result<()> { + (**self).disable_raw_mode() + } + fn hide_cursor(&mut self) -> error::Result<()> { + (**self).hide_cursor() + } + fn show_cursor(&mut self) -> error::Result<()> { + (**self).show_cursor() + } + fn get_cursor(&mut self) -> error::Result<(u16, u16)> { + (**self).get_cursor() + } + fn set_cursor(&mut self, x: u16, y: u16) -> error::Result<()> { + (**self).set_cursor(x, y) + } + fn move_cursor(&mut self, direction: MoveDirection) -> error::Result<()> { + (**self).move_cursor(direction) + } + fn scroll(&mut self, dist: i32) -> error::Result<()> { + (**self).scroll(dist) + } + fn set_attributes(&mut self, attributes: Attributes) -> error::Result<()> { + (**self).set_attributes(attributes) + } + fn remove_attributes(&mut self, attributes: Attributes) -> error::Result<()> { + (**self).remove_attributes(attributes) + } + fn set_fg(&mut self, color: Color) -> error::Result<()> { + (**self).set_fg(color) + } + fn set_bg(&mut self, color: Color) -> error::Result<()> { + (**self).set_bg(color) + } + fn write_styled(&mut self, styled: Styled<'_>) -> error::Result<()> { + (**self).write_styled(styled) + } + fn clear(&mut self, clear_type: ClearType) -> error::Result<()> { + (**self).clear(clear_type) + } + fn size(&self) -> error::Result { + (**self).size() + } +} diff --git a/ui/src/backend/style.rs b/ui/src/backend/style.rs new file mode 100644 index 0000000..f238946 --- /dev/null +++ b/ui/src/backend/style.rs @@ -0,0 +1,351 @@ +use crate::error; + +pub struct Styled<'a> { + fg: Option, + bg: Option, + attributes: Attributes, + content: &'a str, +} + +impl Styled<'_> { + pub(super) fn write( + self, + backend: &mut B, + ) -> error::Result<()> { + if let Some(fg) = self.fg { + backend.set_fg(fg)?; + } + if let Some(bg) = self.bg { + backend.set_bg(bg)?; + } + backend.set_attributes(self.attributes)?; + + write!(backend, "{}", self.content)?; + + if self.fg.is_some() { + backend.set_fg(Color::Reset)?; + } + if self.bg.is_some() { + backend.set_bg(Color::Reset)?; + } + backend.set_attributes(Attributes::RESET) + } +} + +impl<'a> From<&'a str> for Styled<'a> { + fn from(content: &'a str) -> Self { + Self { + fg: None, + bg: None, + attributes: Attributes::empty(), + content, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Color { + Reset, + Black, + Red, + Green, + Yellow, + Blue, + Magenta, + Cyan, + Grey, + DarkGrey, + LightRed, + LightGreen, + LightYellow, + LightBlue, + LightMagenta, + LightCyan, + White, + Rgb(u8, u8, u8), + Ansi(u8), +} + +bitflags::bitflags! { + /// Attributes change the way a piece of text is displayed. + pub struct Attributes: u16 { + const RESET = 0b0000_0000_0001; + const BOLD = 0b0000_0000_0010; + const DIM = 0b0000_0000_0100; + const ITALIC = 0b0000_0000_1000; + const UNDERLINED = 0b0000_0001_0000; + const SLOW_BLINK = 0b0000_0010_0000; + const RAPID_BLINK = 0b0000_0100_0000; + const REVERSED = 0b0000_1000_0000; + const HIDDEN = 0b0001_0000_0000; + const CROSSED_OUT = 0b0010_0000_0000; + } +} + +/// Provides a set of methods to set the colors and attributes. +/// +/// Every method with the `on_` prefix sets the background color. Other color methods set the +/// foreground color. Method names correspond to the [`Attributes`] names. +/// +/// Method names correspond to the [`Color`](enum.Color.html) enum variants. +pub trait Stylize<'a> { + fn black(self) -> Styled<'a>; + fn dark_grey(self) -> Styled<'a>; + fn light_red(self) -> Styled<'a>; + fn red(self) -> Styled<'a>; + fn light_green(self) -> Styled<'a>; + fn green(self) -> Styled<'a>; + fn light_yellow(self) -> Styled<'a>; + fn yellow(self) -> Styled<'a>; + fn light_blue(self) -> Styled<'a>; + fn blue(self) -> Styled<'a>; + fn light_magenta(self) -> Styled<'a>; + fn magenta(self) -> Styled<'a>; + fn light_cyan(self) -> Styled<'a>; + fn cyan(self) -> Styled<'a>; + fn white(self) -> Styled<'a>; + fn grey(self) -> Styled<'a>; + + fn on_black(self) -> Styled<'a>; + fn on_dark_grey(self) -> Styled<'a>; + fn on_light_red(self) -> Styled<'a>; + fn on_red(self) -> Styled<'a>; + fn on_light_green(self) -> Styled<'a>; + fn on_green(self) -> Styled<'a>; + fn on_light_yellow(self) -> Styled<'a>; + fn on_yellow(self) -> Styled<'a>; + fn on_light_blue(self) -> Styled<'a>; + fn on_blue(self) -> Styled<'a>; + fn on_light_magenta(self) -> Styled<'a>; + fn on_magenta(self) -> Styled<'a>; + fn on_light_cyan(self) -> Styled<'a>; + fn on_cyan(self) -> Styled<'a>; + fn on_white(self) -> Styled<'a>; + fn on_grey(self) -> Styled<'a>; + + fn reset(self) -> Styled<'a>; + fn bold(self) -> Styled<'a>; + fn underlined(self) -> Styled<'a>; + fn reverse(self) -> Styled<'a>; + fn dim(self) -> Styled<'a>; + fn italic(self) -> Styled<'a>; + fn slow_blink(self) -> Styled<'a>; + fn rapid_blink(self) -> Styled<'a>; + fn hidden(self) -> Styled<'a>; + fn crossed_out(self) -> Styled<'a>; +} + +impl<'a, I: Into>> Stylize<'a> for I { + fn black(self) -> Styled<'a> { + let mut styled = self.into(); + styled.fg = Some(Color::Black); + styled + } + fn dark_grey(self) -> Styled<'a> { + let mut styled = self.into(); + styled.fg = Some(Color::DarkGrey); + styled + } + fn light_red(self) -> Styled<'a> { + let mut styled = self.into(); + styled.fg = Some(Color::LightRed); + styled + } + fn red(self) -> Styled<'a> { + let mut styled = self.into(); + styled.fg = Some(Color::Red); + styled + } + fn light_green(self) -> Styled<'a> { + let mut styled = self.into(); + styled.fg = Some(Color::LightGreen); + styled + } + fn green(self) -> Styled<'a> { + let mut styled = self.into(); + styled.fg = Some(Color::Green); + styled + } + fn light_yellow(self) -> Styled<'a> { + let mut styled = self.into(); + styled.fg = Some(Color::LightYellow); + styled + } + fn yellow(self) -> Styled<'a> { + let mut styled = self.into(); + styled.fg = Some(Color::Yellow); + styled + } + fn light_blue(self) -> Styled<'a> { + let mut styled = self.into(); + styled.fg = Some(Color::LightBlue); + styled + } + fn blue(self) -> Styled<'a> { + let mut styled = self.into(); + styled.fg = Some(Color::Blue); + styled + } + fn light_magenta(self) -> Styled<'a> { + let mut styled = self.into(); + styled.fg = Some(Color::LightMagenta); + styled + } + fn magenta(self) -> Styled<'a> { + let mut styled = self.into(); + styled.fg = Some(Color::Magenta); + styled + } + fn light_cyan(self) -> Styled<'a> { + let mut styled = self.into(); + styled.fg = Some(Color::LightCyan); + styled + } + fn cyan(self) -> Styled<'a> { + let mut styled = self.into(); + styled.fg = Some(Color::Cyan); + styled + } + fn white(self) -> Styled<'a> { + let mut styled = self.into(); + styled.fg = Some(Color::White); + styled + } + fn grey(self) -> Styled<'a> { + let mut styled = self.into(); + styled.fg = Some(Color::Grey); + styled + } + + fn on_black(self) -> Styled<'a> { + let mut styled = self.into(); + styled.bg = Some(Color::Black); + styled + } + fn on_dark_grey(self) -> Styled<'a> { + let mut styled = self.into(); + styled.bg = Some(Color::DarkGrey); + styled + } + fn on_light_red(self) -> Styled<'a> { + let mut styled = self.into(); + styled.bg = Some(Color::LightRed); + styled + } + fn on_red(self) -> Styled<'a> { + let mut styled = self.into(); + styled.bg = Some(Color::Red); + styled + } + fn on_light_green(self) -> Styled<'a> { + let mut styled = self.into(); + styled.bg = Some(Color::LightGreen); + styled + } + fn on_green(self) -> Styled<'a> { + let mut styled = self.into(); + styled.bg = Some(Color::Green); + styled + } + fn on_light_yellow(self) -> Styled<'a> { + let mut styled = self.into(); + styled.bg = Some(Color::LightYellow); + styled + } + fn on_yellow(self) -> Styled<'a> { + let mut styled = self.into(); + styled.bg = Some(Color::Yellow); + styled + } + fn on_light_blue(self) -> Styled<'a> { + let mut styled = self.into(); + styled.bg = Some(Color::LightBlue); + styled + } + fn on_blue(self) -> Styled<'a> { + let mut styled = self.into(); + styled.bg = Some(Color::Blue); + styled + } + fn on_light_magenta(self) -> Styled<'a> { + let mut styled = self.into(); + styled.bg = Some(Color::LightMagenta); + styled + } + fn on_magenta(self) -> Styled<'a> { + let mut styled = self.into(); + styled.bg = Some(Color::Magenta); + styled + } + fn on_light_cyan(self) -> Styled<'a> { + let mut styled = self.into(); + styled.bg = Some(Color::LightCyan); + styled + } + fn on_cyan(self) -> Styled<'a> { + let mut styled = self.into(); + styled.bg = Some(Color::Cyan); + styled + } + fn on_white(self) -> Styled<'a> { + let mut styled = self.into(); + styled.bg = Some(Color::White); + styled + } + fn on_grey(self) -> Styled<'a> { + let mut styled = self.into(); + styled.bg = Some(Color::Grey); + styled + } + + fn reset(self) -> Styled<'a> { + let mut styled = self.into(); + styled.attributes |= Attributes::RESET; + styled + } + fn bold(self) -> Styled<'a> { + let mut styled = self.into(); + styled.attributes |= Attributes::BOLD; + styled + } + fn underlined(self) -> Styled<'a> { + let mut styled = self.into(); + styled.attributes |= Attributes::UNDERLINED; + styled + } + fn reverse(self) -> Styled<'a> { + let mut styled = self.into(); + styled.attributes |= Attributes::REVERSED; + styled + } + fn dim(self) -> Styled<'a> { + let mut styled = self.into(); + styled.attributes |= Attributes::DIM; + styled + } + fn italic(self) -> Styled<'a> { + let mut styled = self.into(); + styled.attributes |= Attributes::ITALIC; + styled + } + fn slow_blink(self) -> Styled<'a> { + let mut styled = self.into(); + styled.attributes |= Attributes::SLOW_BLINK; + styled + } + fn rapid_blink(self) -> Styled<'a> { + let mut styled = self.into(); + styled.attributes |= Attributes::RAPID_BLINK; + styled + } + fn hidden(self) -> Styled<'a> { + let mut styled = self.into(); + styled.attributes |= Attributes::HIDDEN; + styled + } + fn crossed_out(self) -> Styled<'a> { + let mut styled = self.into(); + styled.attributes |= Attributes::CROSSED_OUT; + styled + } +} diff --git a/ui/src/backend/termion.rs b/ui/src/backend/termion.rs new file mode 100644 index 0000000..d09e7f2 --- /dev/null +++ b/ui/src/backend/termion.rs @@ -0,0 +1,264 @@ +use super::Backend; +use crate::{ + buffer::Cell, + layout::Rect, + style::{Color, Modifier}, +}; +use std::{ + fmt, + io::{self, Write}, +}; + +pub struct TermionBackend +where + W: Write, +{ + stdout: W, +} + +impl TermionBackend +where + W: Write, +{ + pub fn new(stdout: W) -> TermionBackend { + TermionBackend { stdout } + } +} + +impl Write for TermionBackend +where + W: Write, +{ + fn write(&mut self, buf: &[u8]) -> io::Result { + self.stdout.write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.stdout.flush() + } +} + +impl Backend for TermionBackend +where + W: Write, +{ + /// Clears the entire screen and move the cursor to the top left of the screen + fn clear(&mut self) -> io::Result<()> { + write!(self.stdout, "{}", termion::clear::All)?; + write!(self.stdout, "{}", termion::cursor::Goto(1, 1))?; + self.stdout.flush() + } + + /// Hides cursor + fn hide_cursor(&mut self) -> io::Result<()> { + write!(self.stdout, "{}", termion::cursor::Hide)?; + self.stdout.flush() + } + + /// Shows cursor + fn show_cursor(&mut self) -> io::Result<()> { + write!(self.stdout, "{}", termion::cursor::Show)?; + self.stdout.flush() + } + + /// Gets cursor position (0-based index) + fn get_cursor(&mut self) -> io::Result<(u16, u16)> { + termion::cursor::DetectCursorPos::cursor_pos(&mut self.stdout) + .map(|(x, y)| (x - 1, y - 1)) + } + + /// Sets cursor position (0-based index) + fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> { + write!(self.stdout, "{}", termion::cursor::Goto(x + 1, y + 1))?; + self.stdout.flush() + } + + fn draw<'a, I>(&mut self, content: I) -> io::Result<()> + where + I: Iterator, + { + use std::fmt::Write; + + let mut string = String::with_capacity(content.size_hint().0 * 3); + 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) { + write!(string, "{}", termion::cursor::Goto(x + 1, y + 1)).unwrap(); + } + last_pos = Some((x, y)); + if cell.modifier != modifier { + write!( + string, + "{}", + ModifierDiff { + from: modifier, + to: cell.modifier + } + ) + .unwrap(); + modifier = cell.modifier; + } + if cell.fg != fg { + write!(string, "{}", Fg(cell.fg)).unwrap(); + fg = cell.fg; + } + if cell.bg != bg { + write!(string, "{}", Bg(cell.bg)).unwrap(); + bg = cell.bg; + } + string.push_str(&cell.symbol); + } + write!( + self.stdout, + "{}{}{}{}", + string, + Fg(Color::Reset), + Bg(Color::Reset), + termion::style::Reset, + ) + } + + /// Return the size of the terminal + fn size(&self) -> io::Result { + let terminal = termion::terminal_size()?; + Ok(Rect::new(0, 0, terminal.0, terminal.1)) + } + + fn flush(&mut self) -> io::Result<()> { + self.stdout.flush() + } +} + +struct Fg(Color); + +struct Bg(Color); + +struct ModifierDiff { + from: Modifier, + to: Modifier, +} + +impl fmt::Display for Fg { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use termion::color::Color as TermionColor; + match self.0 { + Color::Reset => termion::color::Reset.write_fg(f), + Color::Black => termion::color::Black.write_fg(f), + Color::Red => termion::color::Red.write_fg(f), + Color::Green => termion::color::Green.write_fg(f), + Color::Yellow => termion::color::Yellow.write_fg(f), + Color::Blue => termion::color::Blue.write_fg(f), + Color::Magenta => termion::color::Magenta.write_fg(f), + Color::Cyan => termion::color::Cyan.write_fg(f), + Color::Gray => termion::color::White.write_fg(f), + Color::DarkGray => termion::color::LightBlack.write_fg(f), + Color::LightRed => termion::color::LightRed.write_fg(f), + Color::LightGreen => termion::color::LightGreen.write_fg(f), + Color::LightBlue => termion::color::LightBlue.write_fg(f), + Color::LightYellow => termion::color::LightYellow.write_fg(f), + Color::LightMagenta => termion::color::LightMagenta.write_fg(f), + Color::LightCyan => termion::color::LightCyan.write_fg(f), + Color::White => termion::color::LightWhite.write_fg(f), + Color::Indexed(i) => termion::color::AnsiValue(i).write_fg(f), + Color::Rgb(r, g, b) => termion::color::Rgb(r, g, b).write_fg(f), + } + } +} +impl fmt::Display for Bg { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use termion::color::Color as TermionColor; + match self.0 { + Color::Reset => termion::color::Reset.write_bg(f), + Color::Black => termion::color::Black.write_bg(f), + Color::Red => termion::color::Red.write_bg(f), + Color::Green => termion::color::Green.write_bg(f), + Color::Yellow => termion::color::Yellow.write_bg(f), + Color::Blue => termion::color::Blue.write_bg(f), + Color::Magenta => termion::color::Magenta.write_bg(f), + Color::Cyan => termion::color::Cyan.write_bg(f), + Color::Gray => termion::color::White.write_bg(f), + Color::DarkGray => termion::color::LightBlack.write_bg(f), + Color::LightRed => termion::color::LightRed.write_bg(f), + Color::LightGreen => termion::color::LightGreen.write_bg(f), + Color::LightBlue => termion::color::LightBlue.write_bg(f), + Color::LightYellow => termion::color::LightYellow.write_bg(f), + Color::LightMagenta => termion::color::LightMagenta.write_bg(f), + Color::LightCyan => termion::color::LightCyan.write_bg(f), + Color::White => termion::color::LightWhite.write_bg(f), + Color::Indexed(i) => termion::color::AnsiValue(i).write_bg(f), + Color::Rgb(r, g, b) => termion::color::Rgb(r, g, b).write_bg(f), + } + } +} + +impl fmt::Display for ModifierDiff { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let remove = self.from - self.to; + if remove.contains(Modifier::REVERSED) { + write!(f, "{}", termion::style::NoInvert)?; + } + if remove.contains(Modifier::BOLD) { + // XXX: the termion NoBold flag actually enables double-underline on ECMA-48 compliant + // terminals, and NoFaint additionally disables bold... so we use this trick to get + // the right semantics. + write!(f, "{}", termion::style::NoFaint)?; + + if self.to.contains(Modifier::DIM) { + write!(f, "{}", termion::style::Faint)?; + } + } + if remove.contains(Modifier::ITALIC) { + write!(f, "{}", termion::style::NoItalic)?; + } + if remove.contains(Modifier::UNDERLINED) { + write!(f, "{}", termion::style::NoUnderline)?; + } + if remove.contains(Modifier::DIM) { + write!(f, "{}", termion::style::NoFaint)?; + + // XXX: the NoFaint flag additionally disables bold as well, so we need to re-enable it + // here if we want it. + if self.to.contains(Modifier::BOLD) { + write!(f, "{}", termion::style::Bold)?; + } + } + if remove.contains(Modifier::CROSSED_OUT) { + write!(f, "{}", termion::style::NoCrossedOut)?; + } + if remove.contains(Modifier::SLOW_BLINK) + || remove.contains(Modifier::RAPID_BLINK) + { + write!(f, "{}", termion::style::NoBlink)?; + } + + let add = self.to - self.from; + if add.contains(Modifier::REVERSED) { + write!(f, "{}", termion::style::Invert)?; + } + if add.contains(Modifier::BOLD) { + write!(f, "{}", termion::style::Bold)?; + } + if add.contains(Modifier::ITALIC) { + write!(f, "{}", termion::style::Italic)?; + } + if add.contains(Modifier::UNDERLINED) { + write!(f, "{}", termion::style::Underline)?; + } + if add.contains(Modifier::DIM) { + write!(f, "{}", termion::style::Faint)?; + } + if add.contains(Modifier::CROSSED_OUT) { + write!(f, "{}", termion::style::CrossedOut)?; + } + if add.contains(Modifier::SLOW_BLINK) || add.contains(Modifier::RAPID_BLINK) + { + write!(f, "{}", termion::style::Blink)?; + } + + Ok(()) + } +} diff --git a/ui/src/char_input.rs b/ui/src/char_input.rs index ab36b56..4c266a5 100644 --- a/ui/src/char_input.rs +++ b/ui/src/char_input.rs @@ -1,12 +1,12 @@ -use std::{fmt, io::Write}; - -use crossterm::event; - -use crate::widget::Widget; +use crate::{ + backend::Backend, + error, + events::{KeyCode, KeyEvent}, +}; /// A widget that inputs a single character. If multiple characters are inputted to it, it will have /// the last character -pub struct CharInput { +pub struct CharInput { value: Option, filter_map_char: F, } @@ -40,14 +40,14 @@ where } } -impl Widget for CharInput +impl super::Widget for CharInput where F: Fn(char) -> Option, { /// Handles character, backspace and delete events. - fn handle_key(&mut self, key: event::KeyEvent) -> bool { + fn handle_key(&mut self, key: KeyEvent) -> bool { match key.code { - event::KeyCode::Char(c) => { + KeyCode::Char(c) => { if let Some(c) = (self.filter_map_char)(c) { self.value = Some(c); @@ -57,7 +57,7 @@ where false } - event::KeyCode::Backspace | event::KeyCode::Delete if self.value.is_some() => { + KeyCode::Backspace | KeyCode::Delete if self.value.is_some() => { self.value = None; true } @@ -66,13 +66,17 @@ where } } - fn render(&mut self, max_width: usize, w: &mut W) -> crossterm::Result<()> { + fn render( + &mut self, + max_width: usize, + backend: &mut B, + ) -> error::Result<()> { if let Some(value) = self.value { if max_width == 0 { - return Err(fmt::Error.into()); + return Err(std::fmt::Error.into()); } - write!(w, "{}", value)?; + write!(backend, "{}", value)?; } Ok(()) } @@ -88,6 +92,6 @@ where impl Default for CharInput { fn default() -> Self { - Self::new(crate::widgets::no_filter) + Self::new(super::widgets::no_filter) } } diff --git a/src/error.rs b/ui/src/error.rs similarity index 50% rename from src/error.rs rename to ui/src/error.rs index a6ace9c..4334ab0 100644 --- a/src/error.rs +++ b/ui/src/error.rs @@ -1,10 +1,10 @@ use std::{fmt, io}; -pub type Result = std::result::Result; +pub type Result = std::result::Result; #[derive(Debug)] #[non_exhaustive] -pub enum InquirerError { +pub enum ErrorKind { IoError(io::Error), FmtError(fmt::Error), Utf8Error(std::string::FromUtf8Error), @@ -12,33 +12,33 @@ pub enum InquirerError { NotATty, } -impl std::error::Error for InquirerError { +impl std::error::Error for ErrorKind { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { - InquirerError::IoError(e) => Some(e), - InquirerError::FmtError(e) => Some(e), - InquirerError::Utf8Error(e) => Some(e), - InquirerError::ParseIntError(e) => Some(e), - InquirerError::NotATty => None, + ErrorKind::IoError(e) => Some(e), + ErrorKind::FmtError(e) => Some(e), + ErrorKind::Utf8Error(e) => Some(e), + ErrorKind::ParseIntError(e) => Some(e), + ErrorKind::NotATty => None, } } } -impl fmt::Display for InquirerError { +impl fmt::Display for ErrorKind { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - InquirerError::IoError(e) => write!(fmt, "IoError: {}", e), - InquirerError::FmtError(e) => write!(fmt, "FmtError: {}", e), - InquirerError::Utf8Error(e) => write!(fmt, "Utf8Error: {}", e), - InquirerError::ParseIntError(e) => write!(fmt, "ParseIntError: {}", e), - InquirerError::NotATty => write!(fmt, "Not a tty"), + ErrorKind::IoError(e) => write!(fmt, "IoError: {}", e), + ErrorKind::FmtError(e) => write!(fmt, "FmtError: {}", e), + ErrorKind::Utf8Error(e) => write!(fmt, "Utf8Error: {}", e), + ErrorKind::ParseIntError(e) => write!(fmt, "ParseIntError: {}", e), + ErrorKind::NotATty => write!(fmt, "Not a tty"), } } } macro_rules! impl_from { ($from:path, $e:ident => $body:expr) => { - impl From<$from> for InquirerError { + impl From<$from> for ErrorKind { fn from(e: $from) -> Self { let $e = e; $body @@ -47,10 +47,12 @@ macro_rules! impl_from { }; } -impl_from!(io::Error, e => InquirerError::IoError(e)); -impl_from!(fmt::Error, e => InquirerError::FmtError(e)); -impl_from!(std::string::FromUtf8Error, e => InquirerError::Utf8Error(e)); -impl_from!(std::num::ParseIntError, e => InquirerError::ParseIntError(e)); +impl_from!(io::Error, e => ErrorKind::IoError(e)); +impl_from!(fmt::Error, e => ErrorKind::FmtError(e)); +impl_from!(std::string::FromUtf8Error, e => ErrorKind::Utf8Error(e)); +impl_from!(std::num::ParseIntError, e => ErrorKind::ParseIntError(e)); + +#[cfg(feature = "crossterm")] impl_from!(crossterm::ErrorKind, e => match e { crossterm::ErrorKind::IoError(e) => Self::from(e), diff --git a/ui/src/events/crossterm.rs b/ui/src/events/crossterm.rs new file mode 100644 index 0000000..dfd0bb1 --- /dev/null +++ b/ui/src/events/crossterm.rs @@ -0,0 +1,49 @@ +use crate::error; +use crossterm::event; + +pub fn next_event() -> error::Result { + loop { + if let event::Event::Key(k) = event::read()? { + return Ok(k.into()); + } + } +} + +impl From for super::KeyEvent { + fn from(event: event::KeyEvent) -> Self { + let code = match event.code { + event::KeyCode::Backspace => super::KeyCode::Backspace, + event::KeyCode::Enter => super::KeyCode::Enter, + event::KeyCode::Left => super::KeyCode::Left, + event::KeyCode::Right => super::KeyCode::Right, + event::KeyCode::Up => super::KeyCode::Up, + event::KeyCode::Down => super::KeyCode::Down, + event::KeyCode::Home => super::KeyCode::Home, + event::KeyCode::End => super::KeyCode::End, + event::KeyCode::PageUp => super::KeyCode::PageUp, + event::KeyCode::PageDown => super::KeyCode::PageDown, + event::KeyCode::Tab => super::KeyCode::Tab, + event::KeyCode::BackTab => super::KeyCode::BackTab, + event::KeyCode::Delete => super::KeyCode::Delete, + event::KeyCode::Insert => super::KeyCode::Insert, + event::KeyCode::F(f) => super::KeyCode::F(f), + event::KeyCode::Char(c) => super::KeyCode::Char(c), + event::KeyCode::Null => super::KeyCode::Null, + event::KeyCode::Esc => super::KeyCode::Esc, + }; + + let mut modifiers = super::KeyModifiers::empty(); + + if event.modifiers.contains(event::KeyModifiers::SHIFT) { + modifiers |= super::KeyModifiers::SHIFT; + } + if event.modifiers.contains(event::KeyModifiers::CONTROL) { + modifiers |= super::KeyModifiers::CONTROL; + } + if event.modifiers.contains(event::KeyModifiers::ALT) { + modifiers |= super::KeyModifiers::ALT; + } + + super::KeyEvent { code, modifiers } + } +} diff --git a/ui/src/events/mod.rs b/ui/src/events/mod.rs new file mode 100644 index 0000000..c2d6565 --- /dev/null +++ b/ui/src/events/mod.rs @@ -0,0 +1,112 @@ +use crate::error; + +crate::cfg_async! { +#[cfg(unix)] +mod unix; +#[cfg(unix)] +pub use unix::AsyncEvents; + +#[cfg(windows)] +mod win; +#[cfg(windows)] +pub use win::AsyncEvents; +} + +#[cfg(feature = "crossterm")] +mod crossterm; + +bitflags::bitflags! { + /// Represents key modifiers (shift, control, alt). + pub struct KeyModifiers: u8 { + const SHIFT = 0b0000_0001; + const CONTROL = 0b0000_0010; + const ALT = 0b0000_0100; + } +} + +/// Represents a key event. +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +pub struct KeyEvent { + /// The key itself. + pub code: KeyCode, + /// Additional key modifiers. + pub modifiers: KeyModifiers, +} + +impl KeyEvent { + pub fn new(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent { + KeyEvent { code, modifiers } + } +} + +impl From for KeyEvent { + fn from(code: KeyCode) -> Self { + KeyEvent { + code, + modifiers: KeyModifiers::empty(), + } + } +} + +/// Represents a key. +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +pub enum KeyCode { + /// Backspace key. + Backspace, + /// Enter key. + Enter, + /// Left arrow key. + Left, + /// Right arrow key. + Right, + /// Up arrow key. + Up, + /// Down arrow key. + Down, + /// Home key. + Home, + /// End key. + End, + /// Page up key. + PageUp, + /// Page dow key. + PageDown, + /// Tab key. + Tab, + /// Shift + Tab key. + BackTab, + /// Delete key. + Delete, + /// Insert key. + Insert, + /// F key. + /// + /// `KeyEvent::F(1)` represents F1 key, etc. + F(u8), + /// A character. + /// + /// `KeyEvent::Char('c')` represents `c` character, etc. + Char(char), + /// Null. + Null, + /// Escape key. + Esc, +} + +#[derive(Default)] +pub struct Events {} + +impl Events { + pub fn new() -> Self { + Self {} + } +} + +impl Iterator for Events { + type Item = error::Result; + + fn next(&mut self) -> Option { + #[cfg(feature = "crossterm")] + Some(self::crossterm::next_event()) + } +} diff --git a/ui/src/events/unix.rs b/ui/src/events/unix.rs new file mode 100644 index 0000000..12d1e39 --- /dev/null +++ b/ui/src/events/unix.rs @@ -0,0 +1,134 @@ +#[cfg(feature = "async-std")] +use async_std_dep::{fs::File, io::Read}; +#[cfg(feature = "smol")] +use smol_dep::{fs::File, io::AsyncRead}; +#[cfg(feature = "tokio")] +use tokio_dep::{fs::File, io::AsyncRead}; + +use std::{ + pin::Pin, + task::{Context, Poll}, +}; + +use anes::parser::{KeyCode as AKeyCode, KeyModifiers as AKeyModifiers, Parser, Sequence}; +use futures::Stream; + +use super::{KeyCode, KeyEvent, KeyModifiers}; +use crate::error; + +pub struct AsyncEvents { + tty: File, + parser: Parser, +} + +impl AsyncEvents { + pub async fn new() -> error::Result { + let tty = File::open("/dev/tty").await?; + Ok(Self { + tty, + parser: Parser::default(), + }) + } + + // #[cfg_attr(feature = "tokio", allow(clippy::unnecessary_wraps))] + #[cfg(feature = "tokio")] + fn try_get_events(&mut self, cx: &mut Context<'_>) -> std::io::Result<()> { + #[cfg(nightly)] + let mut buf = std::mem::MaybeUninit::uninit_array::<1024>(); + #[cfg(nightly)] + let mut buf = tokio_dep::io::ReadBuf::uninit(&mut buf); + + #[cfg(not(nightly))] + let mut buf = [0u8; 1024]; + #[cfg(not(nightly))] + let mut buf = tokio_dep::io::ReadBuf::new(&mut buf); + + let tty = Pin::new(&mut self.tty); + + if tty.poll_read(cx, &mut buf[..]).is_ready() { + self.parser.advance(buf.filled(), buf.remaining() == 0); + } + + Ok(()) + } + + #[cfg(not(feature = "tokio"))] + fn try_get_events(&mut self, cx: &mut Context<'_>) -> std::io::Result<()> { + let mut buf = [0u8; 1024]; + + let tty = Pin::new(&mut self.tty); + + if let Poll::Ready(read) = tty.poll_read(cx, &mut buf) { + let read = read?; + self.parser.advance(&buf[..read], read == buf.len()); + } + + Ok(()) + } +} + +impl Stream for AsyncEvents { + type Item = error::Result; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + if let Err(e) = self.try_get_events(cx) { + return Poll::Ready(Some(Err(e.into()))); + } + + loop { + match self.parser.next() { + Some(Sequence::Key(code, modifiers)) => { + break Poll::Ready(Some(Ok(KeyEvent { + code: code.into(), + modifiers: modifiers.into(), + }))) + } + Some(_) => continue, + None => break Poll::Pending, + } + } + } +} + +impl From for KeyModifiers { + fn from(amodifiers: AKeyModifiers) -> Self { + let mut modifiers = KeyModifiers::empty(); + + if amodifiers.contains(AKeyModifiers::SHIFT) { + modifiers |= KeyModifiers::SHIFT; + } + if amodifiers.contains(AKeyModifiers::CONTROL) { + modifiers |= KeyModifiers::CONTROL; + } + if amodifiers.contains(AKeyModifiers::ALT) { + modifiers |= KeyModifiers::ALT; + } + + modifiers + } +} + +impl From for KeyCode { + fn from(code: AKeyCode) -> Self { + match code { + AKeyCode::Backspace => KeyCode::Backspace, + AKeyCode::Enter => KeyCode::Enter, + AKeyCode::Left => KeyCode::Left, + AKeyCode::Right => KeyCode::Right, + AKeyCode::Up => KeyCode::Up, + AKeyCode::Down => KeyCode::Down, + AKeyCode::Home => KeyCode::Home, + AKeyCode::End => KeyCode::End, + AKeyCode::PageUp => KeyCode::PageUp, + AKeyCode::PageDown => KeyCode::PageDown, + AKeyCode::Tab => KeyCode::Tab, + AKeyCode::BackTab => KeyCode::BackTab, + AKeyCode::Delete => KeyCode::Delete, + AKeyCode::Insert => KeyCode::Insert, + AKeyCode::F(f) => KeyCode::F(f), + AKeyCode::Char(c) => KeyCode::Char(c), + AKeyCode::Null => KeyCode::Null, + AKeyCode::Esc => KeyCode::Esc, + } + } +} diff --git a/ui/src/events/win.rs b/ui/src/events/win.rs new file mode 100644 index 0000000..a38fbb5 --- /dev/null +++ b/ui/src/events/win.rs @@ -0,0 +1,77 @@ +#[cfg(feature = "async-std")] +use async_std_dep::task::spawn_blocking; +#[cfg(feature = "smol")] +use smol_dep::unblock as spawn_blocking; +#[cfg(feature = "tokio")] +use tokio_dep::task::spawn_blocking; + +use std::{ + pin::Pin, + sync::{mpsc, Arc}, + task::{Context, Poll}, +}; + +use futures::{task::AtomicWaker, Stream}; + +use crate::{events, error}; + +type Receiver = mpsc::Receiver>; + +pub struct AsyncEvents { + events: Receiver, + waker: Arc, +} + +impl AsyncEvents { + pub async fn new() -> error::Result { + let res = spawn_blocking(|| { + let (tx, rx) = mpsc::sync_channel(16); + let waker = Arc::new(AtomicWaker::new()); + let events = AsyncEvents { + events: rx, + waker: Arc::clone(&waker), + }; + + std::thread::spawn(move || { + let events = super::Events::new(); + + for event in events { + if tx.send(event).is_err() { + break; + } + + waker.wake(); + } + }); + + events + }).await; + + #[cfg(feature = "tokio")] + return res.map_err(|_| { + std::io::Error::new( + std::io::ErrorKind::Other, + "failed to spawn event thread", + ) + .into() + }); + + #[cfg(not(feature = "tokio"))] + Ok(res) + } +} + +impl Stream for AsyncEvents { + type Item = error::Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.events.try_recv() { + Ok(e) => Poll::Ready(Some(e)), + Err(mpsc::TryRecvError::Empty) => { + self.waker.register(cx.waker()); + Poll::Pending + } + Err(mpsc::TryRecvError::Disconnected) => unreachable!(), + } + } +} diff --git a/ui/src/lib.rs b/ui/src/lib.rs index ee546de..0bdf3b7 100644 --- a/ui/src/lib.rs +++ b/ui/src/lib.rs @@ -1,19 +1,14 @@ -#![deny(missing_docs, rust_2018_idioms)] //! A widget based cli ui rendering library use std::sync::Mutex; -use crossterm::terminal; - -#[cfg(feature = "async")] -pub use async_input::{AsyncInput, AsyncPrompt}; pub use sync_input::{Input, Prompt}; pub use widget::Widget; /// In build widgets pub mod widgets { - pub use crate::char_input::CharInput; - pub use crate::list::{List, ListPicker}; - pub use crate::string_input::StringInput; + pub use super::char_input::CharInput; + pub use super::list::{List, ListPicker}; + pub use super::string_input::StringInput; /// The default type for filter_map_char in [`StringInput`] and [`CharInput`] pub type FilterMapChar = fn(char) -> Option; @@ -24,9 +19,15 @@ pub mod widgets { } } -#[cfg(feature = "async")] +cfg_async! { +pub use async_input::AsyncPrompt; mod async_input; +} + +pub mod backend; mod char_input; +pub mod error; +pub mod events; mod list; mod string_input; mod sync_input; @@ -65,21 +66,13 @@ fn exit() { } } -/// Simple helper to make sure if the code panics in between, raw mode is disabled -struct RawMode { - _private: (), -} - -impl RawMode { - /// Enable raw mode for the terminal - pub fn enable() -> crossterm::Result { - terminal::enable_raw_mode()?; - Ok(Self { _private: () }) - } -} - -impl Drop for RawMode { - fn drop(&mut self) { - let _ = terminal::disable_raw_mode(); - } +#[doc(hidden)] +#[macro_export] +macro_rules! cfg_async { + ($($item:item)*) => { + $( + #[cfg(any(feature = "tokio", feature = "async-std", feature = "smol"))] + $item + )* + }; } diff --git a/ui/src/list.rs b/ui/src/list.rs index b676478..19dc6d6 100644 --- a/ui/src/list.rs +++ b/ui/src/list.rs @@ -1,23 +1,19 @@ -use std::io::Write; - -use crossterm::{ - cursor, event, queue, - style::{Colorize, PrintStyledContent}, - terminal, +use crate::{ + backend::{Backend, MoveDirection, Stylize}, + error, + events::{KeyCode, KeyEvent}, }; -use crate::widget::Widget; - /// A trait to represent a renderable list pub trait List { /// Render a single element at some index in **only** one line - fn render_item( + fn render_item( &mut self, index: usize, hovered: bool, max_width: usize, - w: &mut W, - ) -> crossterm::Result<()>; + backend: &mut B, + ) -> error::Result<()>; /// Whether the element at a particular index is selectable. Those that are not selectable are /// skipped over when the navigation keys are used @@ -144,16 +140,22 @@ impl ListPicker { fn adjust_page_start(&mut self, moved: Direction) { // Check whether at is within second and second last element of the page - if self.at <= self.page_start || self.at >= self.page_start + self.list.page_size() - 2 { + if self.at <= self.page_start + || self.at >= self.page_start + self.list.page_size() - 2 + { self.page_start = match moved { // At end of the list, but shouldn't loop, so the last element should be at the end // of the page - Direction::Down if !self.list.should_loop() && self.at == self.list.len() - 1 => { + Direction::Down + if !self.list.should_loop() + && self.at == self.list.len() - 1 => + { self.list.len() - self.list.page_size() + 1 } // Make sure cursor is at second last element of the page Direction::Down => { - (self.list.len() + self.at + 3 - self.list.page_size()) % self.list.len() + (self.list.len() + self.at + 3 - self.list.page_size()) + % self.list.len() } // At start of the list, but shouldn't loop, so the first element should be at the // start of the page @@ -165,40 +167,39 @@ impl ListPicker { } /// Renders the lines in a given iterator - fn render_in( + fn render_in( &mut self, iter: impl Iterator, - w: &mut W, - ) -> crossterm::Result<()> { - let max_width = terminal::size()?.0 as usize; - + max_width: usize, + b: &mut B, + ) -> error::Result<()> { for i in iter { - self.list.render_item(i, i == self.at, max_width, w)?; - queue!(w, cursor::MoveToNextLine(1))?; + self.list.render_item(i, i == self.at, max_width, b)?; + b.move_cursor(MoveDirection::NextLine(1))?; } Ok(()) } } -impl Widget for ListPicker { +impl super::Widget for ListPicker { /// It handles the following keys: /// - Up and 'k' to move up to the previous selectable element /// - Down and 'j' to move up to the next selectable element /// - Home, PageUp and 'g' to go to the first selectable element /// - End, PageDown and 'G' to go to the last selectable element - fn handle_key(&mut self, key: event::KeyEvent) -> bool { + fn handle_key(&mut self, key: KeyEvent) -> bool { let moved = match key.code { - event::KeyCode::Up | event::KeyCode::Char('k') => { + KeyCode::Up | KeyCode::Char('k') => { self.at = self.prev_selectable(); Direction::Up } - event::KeyCode::Down | event::KeyCode::Char('j') => { + KeyCode::Down | KeyCode::Char('j') => { self.at = self.next_selectable(); Direction::Down } - event::KeyCode::PageUp + KeyCode::PageUp if !self.is_paginating() // No pagination, PageUp is same as Home // No looping, and first item is shown in this page || (!self.list.should_loop() && self.at + 2 < self.list.page_size()) => @@ -206,13 +207,14 @@ impl Widget for ListPicker { self.at = self.first_selectable; Direction::Up } - event::KeyCode::PageUp => { - self.at = (self.list.len() + self.at + 2 - self.list.page_size()) % self.list.len(); + KeyCode::PageUp => { + self.at = (self.list.len() + self.at + 2 - self.list.page_size()) + % self.list.len(); self.at = self.next_selectable(); Direction::Up } - event::KeyCode::PageDown + KeyCode::PageDown if !self.is_paginating() // No pagination, PageDown same as End || (!self.list.should_loop() // No looping and last item is shown in this page && self.at + self.list.page_size() - 2 >= self.list.len()) => @@ -220,17 +222,17 @@ impl Widget for ListPicker { self.at = self.last_selectable; Direction::Down } - event::KeyCode::PageDown => { + KeyCode::PageDown => { self.at = (self.at + self.list.page_size() - 2) % self.list.len(); self.at = self.prev_selectable(); Direction::Down } - event::KeyCode::Home | event::KeyCode::Char('g') if self.at != 0 => { + KeyCode::Home | KeyCode::Char('g') if self.at != 0 => { self.at = self.first_selectable; Direction::Up } - event::KeyCode::End | event::KeyCode::Char('G') if self.at != self.list.len() - 1 => { + KeyCode::End | KeyCode::Char('G') if self.at != self.list.len() - 1 => { self.at = self.last_selectable; Direction::Down } @@ -245,33 +247,35 @@ impl Widget for ListPicker { true } - fn render(&mut self, _: usize, w: &mut W) -> crossterm::Result<()> { - queue!(w, cursor::MoveToNextLine(1))?; + fn render( + &mut self, + max_width: usize, + b: &mut B, + ) -> error::Result<()> { + b.move_cursor(MoveDirection::NextLine(1))?; if self.is_paginating() { - let end_iter = - self.page_start..(self.page_start + self.list.page_size() - 1).min(self.list.len()); + let end_iter = self.page_start + ..(self.page_start + self.list.page_size() - 1).min(self.list.len()); if self.list.should_loop() { // Since we should loop, we need to chain the start of the list as well let end_iter_len = end_iter.size_hint().0; self.render_in( end_iter.chain(0..(self.list.page_size() - end_iter_len - 1)), - w, + max_width, + b, )?; } else { - self.render_in(end_iter, w)?; + self.render_in(end_iter, max_width, b)?; } } else { - self.render_in(0..self.list.len(), w)?; + self.render_in(0..self.list.len(), max_width, b)?; }; if self.is_paginating() { - queue!( - w, - PrintStyledContent("(Move up and down to reveal more choices)".dark_grey()), - cursor::MoveToNextLine(1) - )?; + b.write_styled("(Move up and down to reveal more choices)".dark_grey())?; + b.move_cursor(MoveDirection::NextLine(1))?; } Ok(()) diff --git a/ui/src/string_input.rs b/ui/src/string_input.rs index 946b206..f1b9b30 100644 --- a/ui/src/string_input.rs +++ b/ui/src/string_input.rs @@ -4,13 +4,16 @@ use std::{ ops::Range, }; -use crossterm::event; use unicode_segmentation::UnicodeSegmentation; -use crate::widget::Widget; +use crate::{ + backend::Backend, + error, + events::{KeyCode, KeyEvent, KeyModifiers}, +}; /// A widget that inputs a line of text -pub struct StringInput { +pub struct StringInput { value: String, mask: Option, hide_output: bool, @@ -98,10 +101,13 @@ impl StringInput { } /// Get the word bound iterator for a given range - fn word_iter(&self, r: Range) -> impl DoubleEndedIterator { - self.value[r] - .split_word_bound_indices() - .filter(|(_, s)| !s.chars().next().map(char::is_whitespace).unwrap_or(true)) + fn word_iter( + &self, + r: Range, + ) -> impl DoubleEndedIterator { + self.value[r].split_word_bound_indices().filter(|(_, s)| { + !s.chars().next().map(char::is_whitespace).unwrap_or(true) + }) } /// Returns the byte index of the start of the first word to the left (< byte_i) @@ -121,45 +127,47 @@ impl StringInput { } } -impl Widget for StringInput +impl super::Widget for StringInput where F: Fn(char) -> Option, { /// Handles characters, backspace, delete, left arrow, right arrow, home and end. - fn handle_key(&mut self, key: event::KeyEvent) -> bool { + fn handle_key(&mut self, key: KeyEvent) -> bool { match key.code { - event::KeyCode::Left + KeyCode::Left if self.at != 0 && key .modifiers - .intersects(event::KeyModifiers::CONTROL | event::KeyModifiers::ALT) => + .intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) => { - self.at = self.get_char_i(self.find_word_left(self.get_byte_i(self.at))); + self.at = + self.get_char_i(self.find_word_left(self.get_byte_i(self.at))); } - event::KeyCode::Left if self.at != 0 => { + KeyCode::Left if self.at != 0 => { self.at -= 1; } - event::KeyCode::Right + KeyCode::Right if self.at != self.value_len && key .modifiers - .intersects(event::KeyModifiers::CONTROL | event::KeyModifiers::ALT) => + .intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) => { - self.at = self.get_char_i(self.find_word_right(self.get_byte_i(self.at))); + self.at = + self.get_char_i(self.find_word_right(self.get_byte_i(self.at))); } - event::KeyCode::Right if self.at != self.value_len => { + KeyCode::Right if self.at != self.value_len => { self.at += 1; } - event::KeyCode::Home if self.at != 0 => { + KeyCode::Home if self.at != 0 => { self.at = 0; } - event::KeyCode::End if self.at != self.value_len => { + KeyCode::End if self.at != self.value_len => { self.at = self.value_len; } - event::KeyCode::Char(c) if !key.modifiers.contains(event::KeyModifiers::CONTROL) => { + KeyCode::Char(c) if !key.modifiers.contains(KeyModifiers::CONTROL) => { if let Some(c) = (self.filter_map_char)(c) { if self.at == self.value_len { self.value.push(c); @@ -175,8 +183,8 @@ where } } - event::KeyCode::Backspace if self.at == 0 => {} - event::KeyCode::Backspace if key.modifiers.contains(event::KeyModifiers::ALT) => { + KeyCode::Backspace if self.at == 0 => {} + KeyCode::Backspace if key.modifiers.contains(KeyModifiers::ALT) => { let was_at = self.at; let byte_i = self.get_byte_i(self.at); let prev_word = self.find_word_left(byte_i); @@ -184,30 +192,30 @@ where self.value_len -= was_at - self.at; self.value.replace_range(prev_word..byte_i, ""); } - event::KeyCode::Backspace if self.at == self.value_len => { + KeyCode::Backspace if self.at == self.value_len => { self.at -= 1; self.value_len -= 1; self.value.pop(); } - event::KeyCode::Backspace => { + KeyCode::Backspace => { self.at -= 1; let byte_i = self.get_byte_i(self.at); self.value_len -= 1; self.value.remove(byte_i); } - event::KeyCode::Delete if self.at == self.value_len => {} - event::KeyCode::Delete if key.modifiers.contains(event::KeyModifiers::ALT) => { + KeyCode::Delete if self.at == self.value_len => {} + KeyCode::Delete if key.modifiers.contains(KeyModifiers::ALT) => { let byte_i = self.get_byte_i(self.at); let next_word = self.find_word_right(byte_i); self.value_len -= self.get_char_i(next_word) - self.at; self.value.replace_range(byte_i..next_word, ""); } - event::KeyCode::Delete if self.at == self.value_len - 1 => { + KeyCode::Delete if self.at == self.value_len - 1 => { self.value_len -= 1; self.value.pop(); } - event::KeyCode::Delete => { + KeyCode::Delete => { let byte_i = self.get_byte_i(self.at); self.value_len -= 1; self.value.remove(byte_i); @@ -219,7 +227,11 @@ where true } - fn render(&mut self, max_width: usize, w: &mut W) -> crossterm::Result<()> { + fn render( + &mut self, + max_width: usize, + backend: &mut B, + ) -> error::Result<()> { if self.hide_output { return Ok(()); } @@ -229,11 +241,16 @@ where } if self.value_len > max_width { - unimplemented!("Big strings"); + unimplemented!( + "Big strings {} {} {}", + self.value_len, + self.value().chars().count(), + max_width + ); } else if let Some(mask) = self.mask { - print_mask(self.value_len, mask, w)?; + print_mask(self.value_len, mask, backend)?; } else { - w.write_all(self.value.as_bytes())?; + backend.write_all(self.value.as_bytes())?; } Ok(()) @@ -254,7 +271,7 @@ where impl Default for StringInput { fn default() -> Self { - Self::new(crate::widgets::no_filter) + Self::new(super::widgets::no_filter) } } diff --git a/ui/src/sync_input.rs b/ui/src/sync_input.rs index 20ea4e0..76f6782 100644 --- a/ui/src/sync_input.rs +++ b/ui/src/sync_input.rs @@ -1,13 +1,12 @@ use std::{convert::TryFrom, io}; -use crossterm::{ - cursor, event, execute, queue, - style::{Colorize, Print, PrintStyledContent, Styler}, - terminal, +use super::{Validation, Widget}; +use crate::{ + backend::{Backend, ClearType, MoveDirection, Size, Stylize}, + error, + events::{Events, KeyCode, KeyModifiers}, }; -use crate::{RawMode, Validation, Widget}; - /// This trait should be implemented by all 'root' widgets. /// /// It provides the functionality specifically required only by the main controlling widget. For the @@ -48,82 +47,133 @@ pub trait Prompt: Widget { } } +// TODO: disable_raw_mode is not called when a panic occurs + /// The ui runner. It renders and processes events with the help of a type that implements [`Prompt`] /// /// See [`run`](Input::run) for more information -pub struct Input

{ - prompt: P, - terminal_h: u16, - terminal_w: u16, - base_row: u16, - base_col: u16, - hide_cursor: bool, +pub struct Input { + pub(super) prompt: P, + pub(super) backend: B, + pub(super) base_row: u16, + pub(super) base_col: u16, + pub(super) hide_cursor: bool, + pub(super) size: Size, } -impl Input

{ - fn adjust_scrollback( - &self, - height: usize, - stdout: &mut W, - ) -> crossterm::Result { - let th = self.terminal_h as usize; +impl Input { + pub(super) fn init(&mut self) -> error::Result { + let prompt = self.prompt.prompt(); + let prompt_len = + u16::try_from(prompt.chars().count() + 3).expect("really big prompt"); + + self.size = self.backend.size()?; + self.backend.enable_raw_mode()?; + if self.hide_cursor { + self.backend.hide_cursor()?; + }; + + self.backend.write_styled("? ".light_green())?; + self.backend.write_styled(prompt.bold())?; + self.backend.write_all(b" ")?; + + let hint_len = match self.prompt.hint() { + Some(hint) => { + self.backend.write_styled(hint.dark_grey())?; + self.backend.write_all(b" ")?; + u16::try_from(hint.chars().count() + 1).expect("really big prompt") + } + None => 0, + }; + + self.base_row = self.backend.get_cursor()?.1; + self.base_row = self.adjust_scrollback(self.prompt.height())?; + self.base_col = prompt_len + hint_len; + + self.render()?; + + Ok(prompt_len) + } + + pub(super) fn adjust_scrollback(&mut self, height: usize) -> error::Result { + let th = self.size.height as usize; let mut base_row = self.base_row; if self.base_row as usize >= th - height { let dist = (self.base_row as usize + height - th + 1) as u16; base_row -= dist; - queue!(stdout, terminal::ScrollUp(dist), cursor::MoveUp(dist))?; + self.backend.scroll(-(dist as i32))?; + self.backend.move_cursor(MoveDirection::Up(dist))?; } Ok(base_row) } - fn set_cursor_pos(&self, stdout: &mut W) -> crossterm::Result<()> { + pub(super) fn set_cursor_pos(&mut self) -> error::Result<()> { let (dcw, dch) = self.prompt.cursor_pos(self.base_col); - execute!(stdout, cursor::MoveTo(dcw, self.base_row + dch)) + self.backend.set_cursor(dcw, self.base_row + dch)?; + self.backend.flush().map_err(Into::into) } - fn render(&mut self, stdout: &mut W) -> crossterm::Result<()> { + pub(super) fn render(&mut self) -> error::Result<()> { let height = self.prompt.height(); - self.base_row = self.adjust_scrollback(height, stdout)?; - self.clear(self.base_col, stdout)?; - queue!(stdout, cursor::MoveTo(self.base_col, self.base_row))?; + self.base_row = self.adjust_scrollback(height)?; + self.clear(self.base_col)?; + self.backend.set_cursor(self.base_col, self.base_row)?; - self.prompt - .render((self.terminal_w - self.base_col) as usize, stdout)?; + self.prompt.render( + (self.size.width - self.base_col) as usize, + &mut self.backend, + )?; - self.set_cursor_pos(stdout) + self.set_cursor_pos() } - fn clear(&self, prompt_len: u16, stdout: &mut W) -> crossterm::Result<()> { - if self.base_row + 1 < self.terminal_h { - queue!( - stdout, - cursor::MoveTo(0, self.base_row + 1), - terminal::Clear(terminal::ClearType::FromCursorDown), - )?; + pub(super) fn clear(&mut self, prompt_len: u16) -> error::Result<()> { + if self.base_row + 1 < self.size.height { + self.backend.set_cursor(0, self.base_row + 1)?; + self.backend.clear(ClearType::FromCursorDown)?; } - queue!( - stdout, - cursor::MoveTo(prompt_len, self.base_row), - terminal::Clear(terminal::ClearType::UntilNewLine), - ) + self.backend.set_cursor(prompt_len, self.base_row)?; + self.backend.clear(ClearType::UntilNewLine) + } + + pub(super) fn print_error(&mut self, e: P::ValidateErr) -> error::Result<()> { + self.size = self.backend.size()?; + let height = self.prompt.height() + 1; + self.base_row = self.adjust_scrollback(height)?; + self.backend.set_cursor(0, self.base_row + height as u16)?; + self.backend.write_styled(">>".red())?; + write!(self.backend, " {}", e)?; + self.set_cursor_pos() + } + + pub(super) fn reset_terminal(&mut self) -> error::Result<()> { + if self.hide_cursor { + self.backend.show_cursor()?; + } + self.backend.disable_raw_mode() + } + + pub(super) fn exit(&mut self) -> error::Result<()> { + self.backend + .set_cursor(0, self.base_row + self.prompt.height() as u16)?; + self.reset_terminal()?; + super::exit(); + Ok(()) } #[inline] - fn finish( - self, + fn finish( + mut self, pressed_enter: bool, prompt_len: u16, - stdout: &mut W, - ) -> crossterm::Result { - self.clear(prompt_len, stdout)?; - if self.hide_cursor { - queue!(stdout, cursor::Show)?; - } - stdout.flush()?; + ) -> error::Result { + self.clear(prompt_len)?; + self.reset_terminal()?; + if pressed_enter { Ok(self.prompt.finish()) } else { @@ -133,122 +183,64 @@ impl Input

{ /// Run the ui on the given writer. It will return when the user presses `Enter` or `Escape` /// based on the [`Prompt`] implementation. - pub fn run(mut self, stdout: &mut W) -> crossterm::Result { - let (tw, th) = terminal::size()?; - self.terminal_h = th; - self.terminal_w = tw; - - let prompt = self.prompt.prompt(); - let prompt_len = u16::try_from(prompt.chars().count() + 3).expect("really big prompt"); - - let raw_mode = RawMode::enable()?; - if self.hide_cursor { - queue!(stdout, cursor::Hide)?; - }; - - let height = self.prompt.height(); - self.base_row = cursor::position()?.1; - self.base_row = self.adjust_scrollback(height, stdout)?; - - queue!( - stdout, - PrintStyledContent("? ".green()), - PrintStyledContent(prompt.bold()), - Print(' '), - )?; - - let hint_len = match self.prompt.hint() { - Some(hint) => { - queue!(stdout, PrintStyledContent(hint.dark_grey()), Print(' '))?; - u16::try_from(hint.chars().count() + 1).expect("really big prompt") - } - None => 0, - }; - - self.base_col = prompt_len + hint_len; - - self.render(stdout)?; + pub fn run(mut self, events: &mut Events) -> error::Result { + let prompt_len = self.init()?; loop { - match event::read()? { - event::Event::Resize(tw, th) => { - self.terminal_w = tw; - self.terminal_h = th; + let e = events.next().unwrap()?; + + let key_handled = match e.code { + KeyCode::Char('c') + if e.modifiers.contains(KeyModifiers::CONTROL) => + { + self.exit()?; + return Err( + io::Error::new(io::ErrorKind::Other, "CTRL+C").into() + ); + } + KeyCode::Null => { + self.exit()?; + return Err( + io::Error::new(io::ErrorKind::UnexpectedEof, "EOF").into() + ); + } + KeyCode::Esc if self.prompt.has_default() => { + return self.finish(false, prompt_len); } - event::Event::Key(e) => { - let key_handled = match e.code { - event::KeyCode::Char('c') - if e.modifiers.contains(event::KeyModifiers::CONTROL) => - { - queue!( - stdout, - cursor::MoveTo(0, self.base_row + self.prompt.height() as u16) - )?; - drop(raw_mode); - if self.hide_cursor { - queue!(stdout, cursor::Show)?; - } - crate::exit(); - - return Err(io::Error::new(io::ErrorKind::Other, "CTRL+C").into()); - } - event::KeyCode::Null => { - queue!( - stdout, - cursor::MoveTo(0, self.base_row + self.prompt.height() as u16) - )?; - drop(raw_mode); - if self.hide_cursor { - queue!(stdout, cursor::Show)?; - } - crate::exit(); - return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "EOF").into()); - } - event::KeyCode::Esc if self.prompt.has_default() => { - return self.finish(false, prompt_len, stdout); - } - - event::KeyCode::Enter => match self.prompt.validate() { - Ok(Validation::Finish) => { - return self.finish(true, prompt_len, stdout); - } - Ok(Validation::Continue) => true, - Err(e) => { - let height = self.prompt.height() + 1; - self.base_row = self.adjust_scrollback(height, stdout)?; - - queue!(stdout, cursor::MoveTo(0, self.base_row + height as u16))?; - write!(stdout, "{} {}", ">>".dark_red(), e)?; - - self.set_cursor_pos(stdout)?; - - continue; - } - }, - _ => self.prompt.handle_key(e), - }; - - if key_handled { - self.render(stdout)?; + KeyCode::Enter => match self.prompt.validate() { + Ok(Validation::Finish) => { + return self.finish(true, prompt_len); } - } - _ => {} + Ok(Validation::Continue) => true, + Err(e) => { + self.print_error(e)?; + + continue; + } + }, + _ => self.prompt.handle_key(e), + }; + + if key_handled { + self.size = self.backend.size()?; + self.render()?; } } } } -impl

Input

{ +impl Input { + #[allow(clippy::new_ret_no_self)] /// Creates a new Input - pub fn new(prompt: P) -> Self { - Self { + pub fn new(prompt: P, backend: &mut B) -> Input { + Input { prompt, + backend, base_row: 0, base_col: 0, - terminal_h: 0, - terminal_w: 0, hide_cursor: false, + size: Size::default(), } } diff --git a/ui/src/widget.rs b/ui/src/widget.rs index 50b56fe..9b29d69 100644 --- a/ui/src/widget.rs +++ b/ui/src/widget.rs @@ -1,18 +1,20 @@ -use std::{fmt, io::Write}; - -use crossterm::event; +use crate::{backend::Backend, error, events::KeyEvent}; /// A trait to represent renderable objects. pub trait Widget { /// Handle a key input. It should return whether key was handled. #[allow(unused_variables)] - fn handle_key(&mut self, key: event::KeyEvent) -> bool { + fn handle_key(&mut self, key: KeyEvent) -> bool { false } /// Render to stdout. `max_width` is the number of characters that can be printed in the current /// line. - fn render(&mut self, max_width: usize, stdout: &mut W) -> crossterm::Result<()>; + fn render( + &mut self, + max_width: usize, + backend: &mut B, + ) -> error::Result<()>; /// The number of rows of the terminal the widget will take when rendered fn height(&self) -> usize; @@ -25,19 +27,23 @@ pub trait Widget { } impl> Widget for T { - fn render(&mut self, max_width: usize, w: &mut W) -> crossterm::Result<()> { + fn render( + &mut self, + max_width: usize, + backend: &mut B, + ) -> error::Result<()> { let s = self.as_ref(); if max_width <= 3 { - return Err(fmt::Error.into()); + return Err(std::fmt::Error.into()); } if s.chars().count() > max_width { let byte_i = s.char_indices().nth(max_width - 3).unwrap().0; - w.write_all(s[..byte_i].as_bytes())?; - w.write_all(b"...").map_err(Into::into) + backend.write_all(s[..byte_i].as_bytes())?; + backend.write_all(b"...").map_err(Into::into) } else { - w.write_all(s.as_bytes()).map_err(Into::into) + backend.write_all(s.as_bytes()).map_err(Into::into) } }