Extract crossterm into a backend

This commit is contained in:
Lutetium-Vanadium 2021-04-29 13:39:49 +05:30
parent ad569fd25f
commit 16f8e3ad52
33 changed files with 2768 additions and 1041 deletions

1
.rustfmt.toml Normal file
View File

@ -0,0 +1 @@
max_width = 85

6
.vim/coc-settings.json Normal file
View File

@ -0,0 +1,6 @@
{
"rust-analyzer.diagnostics.disabled": [
"incorrect-ident-case",
"inactive-code"
]
}

View File

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

View File

@ -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<W: std::io::Write>(
fn prompt_impl<B: backend::Backend>(
&mut self,
stdout: &mut W,
stdout: &mut B,
events: &mut events::Events,
) -> error::Result<Option<&mut Answer>> {
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<Option<&mut Answer>> {
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<Answers> {
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<W: std::io::Write>(
async fn prompt_impl_async<B: backend::Backend>(
&mut self,
stdout: &mut W,
stdout: &mut B,
events: &mut events::AsyncEvents,
) -> error::Result<Option<&mut Answer>> {
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<Option<&mut Answer>> {
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<Answers> {
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<Answers>
where
Q: IntoIterator<Item = Question<'m, 'w, 'f, 'v, 't>>,
{
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
)*
};

View File

@ -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<bool>, choices: super::ChoiceList<String>) -> Vec<ListItem> {
fn create_list_items(
selected: Vec<bool>,
choices: super::ChoiceList<String>,
) -> Vec<ListItem> {
selected
.into_iter()
.enumerate()
@ -115,24 +118,28 @@ impl ui::AsyncPrompt for CheckboxPrompt<'_, '_, '_, '_> {
}
impl Widget for CheckboxPrompt<'_, '_, '_, '_> {
fn render<W: io::Write>(&mut self, max_width: usize, w: &mut W) -> crossterm::Result<()> {
self.picker.render(max_width, w)
fn render<B: Backend>(
&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<W: io::Write>(
fn render_item<B: Backend>(
&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<W: io::Write>(
pub(crate) fn ask<B: Backend>(
mut self,
message: String,
answers: &Answers,
w: &mut W,
b: &mut B,
events: &mut ui::events::Events,
) -> error::Result<Answer> {
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<W: io::Write>(
pub(crate) async fn ask_async<B: Backend>(
mut self,
message: String,
answers: &Answers,
w: &mut W,
b: &mut B,
events: &mut ui::events::AsyncEvents,
) -> error::Result<Answer> {
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<I: Into<String>>(mut self, choice: I, default: bool) -> Self {
pub fn choice_with_default<I: Into<String>>(
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<Item = &'a str>,
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", ")?;
}
}

View File

@ -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<W: std::io::Write>(&mut self, max_width: usize, w: &mut W) -> crossterm::Result<()> {
self.input.render(max_width, w)
fn render<B: Backend>(
&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<W: std::io::Write>(
pub(crate) fn ask<B: Backend>(
mut self,
message: String,
answers: &Answers,
w: &mut W,
b: &mut B,
events: &mut ui::events::Events,
) -> error::Result<Answer> {
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<W: std::io::Write>(
pub(crate) async fn ask_async<B: Backend>(
mut self,
message: String,
answers: &Answers,
w: &mut W,
b: &mut B,
events: &mut ui::events::AsyncEvents,
) -> error::Result<Answer> {
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<N: Into<String>>(name: N) -> ConfirmBuilder<'static, 'static, 'static> {
pub fn confirm<N: Into<String>>(
name: N,
) -> ConfirmBuilder<'static, 'static, 'static> {
ConfirmBuilder {
opts: Options::new(name.into()),
confirm: Default::default(),

View File

@ -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<W: Write>(&mut self, _: usize, _: &mut W) -> crossterm::Result<()> {
fn render<B: Backend>(&mut self, _: usize, _: &mut B) -> error::Result<()> {
Ok(())
}
@ -146,7 +148,7 @@ struct EditorPromptAsync<'f, 'v, 't, 'a> {
}
impl Widget for EditorPromptAsync<'_, '_, '_, '_> {
fn render<W: Write>(&mut self, _: usize, _: &mut W) -> crossterm::Result<()> {
fn render<B: Backend>(&mut self, _: usize, _: &mut B) -> error::Result<()> {
Ok(())
}
@ -218,11 +220,12 @@ impl ui::AsyncPrompt for EditorPromptAsync<'_, '_, '_, '_> {
}
impl Editor<'_, '_, '_> {
pub(crate) fn ask<W: Write>(
pub(crate) fn ask<B: Backend>(
mut self,
message: String,
answers: &Answers,
w: &mut W,
b: &mut B,
events: &mut ui::events::Events,
) -> error::Result<Answer> {
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<W: Write>(
pub(crate) async fn ask_async<B: Backend>(
mut self,
message: String,
answers: &Answers,
w: &mut W,
b: &mut B,
events: &mut ui::events::AsyncEvents,
) -> error::Result<Answer> {
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))

View File

@ -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<F: Fn(char) -> Option<char> + Send + Sync> ui::AsyncPrompt for ExpandPrompt
const ANSWER_PROMPT: &[u8] = b" Answer: ";
impl<F: Fn(char) -> Option<char>> ui::Widget for ExpandPrompt<'_, F> {
fn render<W: std::io::Write>(&mut self, max_width: usize, w: &mut W) -> crossterm::Result<()> {
fn render<B: Backend>(
&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<F: Fn(char) -> Option<char>> 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<F: Fn(char) -> Option<char>> 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<W: std::io::Write>(
fn render_item<B: Backend>(
&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<W: std::io::Write>(
fn render_choice<B: Backend>(
&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<W: std::io::Write>(
pub(crate) fn ask<B: Backend>(
mut self,
message: String,
answers: &Answers,
w: &mut W,
b: &mut B,
events: &mut ui::events::Events,
) -> error::Result<Answer> {
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<B: Backend>(
mut self,
message: String,
answers: &Answers,
b: &mut B,
events: &mut ui::events::AsyncEvents,
) -> error::Result<Answer> {
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<W: std::io::Write>(
mut self,
message: String,
answers: &Answers,
w: &mut W,
) -> error::Result<Answer> {
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<ExpandBuilder<'m, 'w, 't>> for super::Question<'m, 'w, 'static, 'static, 't> {
impl<'m, 'w, 't> From<ExpandBuilder<'m, 'w, 't>>
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<N: Into<String>>(name: N) -> ExpandBuilder<'static, 'static, 'static> {
pub fn expand<N: Into<String>>(
name: N,
) -> ExpandBuilder<'static, 'static, 'static> {
ExpandBuilder {
opts: Options::new(name.into()),
expand: Default::default(),

View File

@ -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<W: std::io::Write>(&mut self, max_width: usize, w: &mut W) -> crossterm::Result<()> {
self.input.render(max_width, w)
fn render<B: Backend>(
&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<W: std::io::Write>(
pub(crate) fn ask<B: Backend>(
mut self,
message: String,
answers: &Answers,
w: &mut W,
b: &mut B,
events: &mut ui::events::Events,
) -> error::Result<Answer> {
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<B: Backend>(
mut self,
message: String,
answers: &Answers,
b: &mut B,
events: &mut ui::events::AsyncEvents,
) -> error::Result<Answer> {
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<W: std::io::Write>(
mut self,
message: String,
answers: &Answers,
w: &mut W,
) -> error::Result<Answer> {
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))

View File

@ -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<W: std::io::Write>(&mut self, max_width: usize, w: &mut W) -> crossterm::Result<()> {
self.picker.render(max_width, w)
fn render<B: Backend>(&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<W: std::io::Write>(
fn render_item<B: Backend>(
&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<W: std::io::Write>(
pub(crate) fn ask<B: Backend>(
mut self,
message: String,
answers: &Answers,
w: &mut W,
b: &mut B,
events: &mut ui::events::Events,
) -> error::Result<Answer> {
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<W: std::io::Write>(
pub(crate) async fn ask_async<B: Backend>(
mut self,
message: String,
answers: &Answers,
w: &mut W,
b: &mut B,
events: &mut ui::events::AsyncEvents,
) -> error::Result<Answer> {
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<ListBuilder<'m, 'w, 't>> for super::Question<'m, 'w, 'static, 'static, 't> {
impl<'m, 'w, 't> From<ListBuilder<'m, 'w, 't>>
for super::Question<'m, 'w, 'static, 'static, 't>
{
fn from(builder: ListBuilder<'m, 'w, 't>) -> Self {
builder.build()
}

View File

@ -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<W: Write>(
pub(crate) fn ask<B: Backend>(
mut self,
answers: &Answers,
w: &mut W,
b: &mut B,
events: &mut ui::events::Events,
) -> error::Result<Option<(String, Answer)>> {
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<W: Write>(
pub(crate) async fn ask_async<B: Backend>(
mut self,
answers: &Answers,
w: &mut W,
b: &mut B,
events: &mut ui::events::AsyncEvents,
) -> error::Result<Option<(String, Answer)>> {
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<Box<dyn Future<Output = T> + Send + Sync + 'a>>;
pub(crate) type BoxFuture<'a, T> =
Pin<Box<dyn Future<Output = T> + 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<Box<dyn std::future::Future<Output = crate::error::Result<()>> + Send + Sync + 'a>> + Send + Sync + 'a,
F: FnOnce(&$t, &crate::Answers, &mut dyn Backend) -> std::pin::Pin<Box<dyn std::future::Future<Output = ui::error::Result<()>> + 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<Box<dyn std::future::Future<Output = crate::error::Result<()>> + Send + Sync + 'a>> + Send + Sync + 'a,
F: FnOnce($t, &crate::Answers, &mut dyn Backend) -> std::pin::Pin<Box<dyn std::future::Future<Output = ui::error::Result<()>> + Send + Sync + 'a>> + Send + Sync + 'a,
{
let $self = self;
let $transformer = crate::question::TransformerByVal::Async(Box::new(transformer));

View File

@ -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<char>;
fn parse(s: &str) -> Result<Self::Inner, String>;
fn default(&self) -> Option<Self::Inner>;
fn write<W: std::io::Write>(inner: Self::Inner, w: &mut W) -> error::Result<()>;
fn write<B: Backend>(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<W: std::io::Write>(i: Self::Inner, w: &mut W) -> error::Result<()> {
writeln!(
w,
"{}{}{}",
SetForegroundColor(Color::DarkCyan),
i,
ResetColor,
)
.map_err(Into::into)
fn write<B: Backend>(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<char> {
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<W: std::io::Write>(f: Self::Inner, w: &mut W) -> error::Result<()> {
write!(w, "{}", SetForegroundColor(Color::DarkCyan))?;
fn write<B: Backend>(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<W: std::io::Write>(&mut self, max_width: usize, w: &mut W) -> crossterm::Result<()> {
self.input.render(max_width, w)
fn render<B: Backend>(
&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<W: std::io::Write>(
pub(crate) fn ask<B: Backend>(
mut self,
message: String,
answers: &Answers,
w: &mut W,
b: &mut B,
events: &mut ui::events::Events,
) -> error::Result<Answer> {
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<B: Backend>(
mut self,
message: String,
answers: &Answers,
b: &mut B,
events: &mut ui::events::AsyncEvents,
) -> error::Result<Answer> {
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<W: std::io::Write>(
mut self,
message: String,
answers: &Answers,
w: &mut W,
) -> error::Result<Answer> {
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))

View File

@ -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<W: std::io::Write>(&mut self, max_width: usize, w: &mut W) -> crossterm::Result<()> {
self.input.render(max_width, w)
fn render<B: Backend>(
&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<W: std::io::Write>(
pub(crate) fn ask<B: Backend>(
mut self,
message: String,
answers: &Answers,
w: &mut W,
b: &mut B,
events: &mut ui::events::Events,
) -> error::Result<Answer> {
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<B: Backend>(
mut self,
message: String,
answers: &Answers,
b: &mut B,
events: &mut ui::events::AsyncEvents,
) -> error::Result<Answer> {
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<W: std::io::Write>(
mut self,
message: String,
answers: &Answers,
w: &mut W,
) -> error::Result<Answer> {
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))

View File

@ -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<Answer>;
crate::cfg_async! {
fn ask_async<'future>(
&mut self,
message: String,
answers: &Answers,
stdout: &mut dyn std::io::Write,
) -> super::BoxFuture<'future, error::Result<Answer>>;
stdout: &mut dyn Backend,
events: &mut events::AsyncEvents,
) -> crate::question::BoxFuture<'future, error::Result<Answer>>;
}
}
pub struct PluginBuilder<'m, 'w, 'p> {
@ -30,7 +35,10 @@ impl<'p, P: Plugin + 'p> From<P> for Box<dyn Plugin + 'p> {
}
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<String>,
P: Into<Box<dyn Plugin + 'a>>,
@ -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<PluginBuilder<'m, 'w, 'q>> for Question<'m, 'w, 'q, 'static, 'static> {
impl<'m, 'w, 'q> From<PluginBuilder<'m, 'w, 'q>>
for Question<'m, 'w, 'q, 'static, 'static>
{
fn from(builder: PluginBuilder<'m, 'w, 'q>) -> Self {
builder.build()
}

View File

@ -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<W: std::io::Write>(&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<B: Backend>(&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::<usize>() {
if let Ok(n) = self.input.value().parse::<usize>() {
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<W: std::io::Write>(
fn render_item<B: Backend>(
&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<W: std::io::Write>(
pub(crate) fn ask<B: Backend>(
mut self,
message: String,
answers: &Answers,
w: &mut W,
b: &mut B,
events: &mut ui::events::Events,
) -> error::Result<Answer> {
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<B: Backend>(
mut self,
message: String,
answers: &Answers,
b: &mut B,
events: &mut ui::events::AsyncEvents,
) -> error::Result<Answer> {
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<W: std::io::Write>(
mut self,
message: String,
answers: &Answers,
w: &mut W,
) -> error::Result<Answer> {
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<N: Into<String>>(name: N) -> RawlistBuilder<'static, 'static, 'static> {
pub fn rawlist<N: Into<String>>(
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,
}
}
}

View File

@ -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-name>-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"] }

View File

@ -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<P> {
prompt: P,
terminal_h: u16,
terminal_w: u16,
base_row: u16,
base_col: u16,
hide_cursor: bool,
}
impl<P: AsyncPrompt + Send> AsyncInput<P> {
fn adjust_scrollback<W: io::Write>(
&self,
height: usize,
stdout: &mut W,
) -> crossterm::Result<u16> {
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<W: io::Write>(&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<W: io::Write>(&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<W: io::Write>(&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<P: AsyncPrompt + Send, B: Backend + Unpin> Input<P, B> {
#[inline]
async fn finish<W: io::Write>(
self,
async fn finish_async(
mut self,
pressed_enter: bool,
prompt_len: u16,
stdout: &mut W,
) -> crossterm::Result<P::Output> {
self.clear(prompt_len, stdout)?;
if self.hide_cursor {
queue!(stdout, cursor::Show)?;
}
stdout.flush()?;
) -> error::Result<P::Output> {
self.clear(prompt_len)?;
self.reset_terminal()?;
if pressed_enter {
Ok(self.prompt.finish_async().await)
@ -121,140 +54,49 @@ impl<P: AsyncPrompt + Send> AsyncInput<P> {
/// 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<W: io::Write>(mut self, stdout: &mut W) -> crossterm::Result<P::Output> {
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<P::Output> {
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<P> AsyncInput<P> {
/// 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
}
}

219
ui/src/backend/crossterm.rs Normal file
View File

@ -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<W> {
buffer: W,
}
impl<W> CrosstermBackend<W> {
pub fn new(buffer: W) -> error::Result<CrosstermBackend<W>> {
Ok(CrosstermBackend { buffer })
}
}
impl<W: Write> Write for CrosstermBackend<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.buffer.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.buffer.flush()
}
}
impl<W: Write> Backend for CrosstermBackend<W> {
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<Size> {
terminal::size().map(Into::into).map_err(Into::into)
}
}
impl From<Color> for CColor {
fn from(color: Color) -> Self {
match color {
Color::Reset => CColor::Reset,
Color::Black => CColor::Black,
Color::Red => CColor::DarkRed,
Color::Green => CColor::DarkGreen,
Color::Yellow => CColor::DarkYellow,
Color::Blue => CColor::DarkBlue,
Color::Magenta => CColor::DarkMagenta,
Color::Cyan => CColor::DarkCyan,
Color::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<ClearType> 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<W: Write>(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<W: Write>(
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(())
}

286
ui/src/backend/curses.rs Normal file
View File

@ -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<CursesBackend> {
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<Item = (u16, u16, &'a Cell)>,
{
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<Rect, io::Error> {
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<Color> for Option<easycurses::Color> {
fn from(color: Color) -> Option<easycurses::Color> {
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);
}
}

155
ui/src/backend/mod.rs Normal file
View File

@ -0,0 +1,155 @@
use crate::error;
pub fn get_backend<W: std::io::Write>(buf: W) -> error::Result<impl Backend> {
#[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<Size>;
}
fn default_move_cursor<B: Backend + ?Sized>(
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<Size> {
(**self).size()
}
}

351
ui/src/backend/style.rs Normal file
View File

@ -0,0 +1,351 @@
use crate::error;
pub struct Styled<'a> {
fg: Option<Color>,
bg: Option<Color>,
attributes: Attributes,
content: &'a str,
}
impl Styled<'_> {
pub(super) fn write<B: super::Backend + ?Sized>(
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<Styled<'a>>> 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
}
}

264
ui/src/backend/termion.rs Normal file
View File

@ -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<W>
where
W: Write,
{
stdout: W,
}
impl<W> TermionBackend<W>
where
W: Write,
{
pub fn new(stdout: W) -> TermionBackend<W> {
TermionBackend { stdout }
}
}
impl<W> Write for TermionBackend<W>
where
W: Write,
{
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.stdout.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.stdout.flush()
}
}
impl<W> Backend for TermionBackend<W>
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<Item = (u16, u16, &'a Cell)>,
{
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<Rect> {
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(())
}
}

View File

@ -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<F = crate::widgets::FilterMapChar> {
pub struct CharInput<F = super::widgets::FilterMapChar> {
value: Option<char>,
filter_map_char: F,
}
@ -40,14 +40,14 @@ where
}
}
impl<F> Widget for CharInput<F>
impl<F> super::Widget for CharInput<F>
where
F: Fn(char) -> Option<char>,
{
/// 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<W: Write>(&mut self, max_width: usize, w: &mut W) -> crossterm::Result<()> {
fn render<B: Backend>(
&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)
}
}

View File

@ -1,10 +1,10 @@
use std::{fmt, io};
pub type Result<T> = std::result::Result<T, InquirerError>;
pub type Result<T> = std::result::Result<T, ErrorKind>;
#[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),

View File

@ -0,0 +1,49 @@
use crate::error;
use crossterm::event;
pub fn next_event() -> error::Result<super::KeyEvent> {
loop {
if let event::Event::Key(k) = event::read()? {
return Ok(k.into());
}
}
}
impl From<event::KeyEvent> 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 }
}
}

112
ui/src/events/mod.rs Normal file
View File

@ -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<KeyCode> 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<KeyEvent>;
fn next(&mut self) -> Option<Self::Item> {
#[cfg(feature = "crossterm")]
Some(self::crossterm::next_event())
}
}

134
ui/src/events/unix.rs Normal file
View File

@ -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<AsyncEvents> {
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<KeyEvent>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
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<AKeyModifiers> 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<AKeyCode> 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,
}
}
}

77
ui/src/events/win.rs Normal file
View File

@ -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<error::Result<events::KeyEvent>>;
pub struct AsyncEvents {
events: Receiver,
waker: Arc<AtomicWaker>,
}
impl AsyncEvents {
pub async fn new() -> error::Result<Self> {
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<events::KeyEvent>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
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!(),
}
}
}

View File

@ -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<char>;
@ -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<Self> {
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
)*
};
}

View File

@ -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<W: Write>(
fn render_item<B: Backend>(
&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<L: List> ListPicker<L> {
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<L: List> ListPicker<L> {
}
/// Renders the lines in a given iterator
fn render_in<W: Write>(
fn render_in<B: Backend>(
&mut self,
iter: impl Iterator<Item = usize>,
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<L: List> Widget for ListPicker<L> {
impl<L: List> super::Widget for ListPicker<L> {
/// 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<L: List> Widget for ListPicker<L> {
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<L: List> Widget for ListPicker<L> {
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<L: List> Widget for ListPicker<L> {
true
}
fn render<W: Write>(&mut self, _: usize, w: &mut W) -> crossterm::Result<()> {
queue!(w, cursor::MoveToNextLine(1))?;
fn render<B: Backend>(
&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(())

View File

@ -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<F = crate::widgets::FilterMapChar> {
pub struct StringInput<F = super::widgets::FilterMapChar> {
value: String,
mask: Option<char>,
hide_output: bool,
@ -98,10 +101,13 @@ impl<F> StringInput<F> {
}
/// Get the word bound iterator for a given range
fn word_iter(&self, r: Range<usize>) -> impl DoubleEndedIterator<Item = (usize, &str)> {
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<usize>,
) -> impl DoubleEndedIterator<Item = (usize, &str)> {
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<F> StringInput<F> {
}
}
impl<F> Widget for StringInput<F>
impl<F> super::Widget for StringInput<F>
where
F: Fn(char) -> Option<char>,
{
/// 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<W: Write>(&mut self, max_width: usize, w: &mut W) -> crossterm::Result<()> {
fn render<B: Backend>(
&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)
}
}

View File

@ -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<P> {
prompt: P,
terminal_h: u16,
terminal_w: u16,
base_row: u16,
base_col: u16,
hide_cursor: bool,
pub struct Input<P, B: Backend> {
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<P: Prompt> Input<P> {
fn adjust_scrollback<W: io::Write>(
&self,
height: usize,
stdout: &mut W,
) -> crossterm::Result<u16> {
let th = self.terminal_h as usize;
impl<P: Prompt, B: Backend> Input<P, B> {
pub(super) fn init(&mut self) -> error::Result<u16> {
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<u16> {
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<W: io::Write>(&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<W: io::Write>(&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<W: io::Write>(&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<W: io::Write>(
self,
fn finish(
mut self,
pressed_enter: bool,
prompt_len: u16,
stdout: &mut W,
) -> crossterm::Result<P::Output> {
self.clear(prompt_len, stdout)?;
if self.hide_cursor {
queue!(stdout, cursor::Show)?;
}
stdout.flush()?;
) -> error::Result<P::Output> {
self.clear(prompt_len)?;
self.reset_terminal()?;
if pressed_enter {
Ok(self.prompt.finish())
} else {
@ -133,122 +183,64 @@ impl<P: Prompt> Input<P> {
/// 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<W: io::Write>(mut self, stdout: &mut W) -> crossterm::Result<P::Output> {
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<P::Output> {
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<P> Input<P> {
impl<P, B: Backend> Input<P, B> {
#[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<P, &mut B> {
Input {
prompt,
backend,
base_row: 0,
base_col: 0,
terminal_h: 0,
terminal_w: 0,
hide_cursor: false,
size: Size::default(),
}
}

View File

@ -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<W: Write>(&mut self, max_width: usize, stdout: &mut W) -> crossterm::Result<()>;
fn render<B: Backend>(
&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<T: AsRef<str>> Widget for T {
fn render<W: Write>(&mut self, max_width: usize, w: &mut W) -> crossterm::Result<()> {
fn render<B: Backend>(
&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)
}
}