Extract crossterm into a backend
This commit is contained in:
parent
ad569fd25f
commit
16f8e3ad52
1
.rustfmt.toml
Normal file
1
.rustfmt.toml
Normal file
|
@ -0,0 +1 @@
|
|||
max_width = 85
|
6
.vim/coc-settings.json
Normal file
6
.vim/coc-settings.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"rust-analyzer.diagnostics.disabled": [
|
||||
"incorrect-ident-case",
|
||||
"inactive-code"
|
||||
]
|
||||
}
|
30
Cargo.toml
30
Cargo.toml
|
@ -7,22 +7,30 @@ edition = "2018"
|
|||
[workspace]
|
||||
members = [
|
||||
".",
|
||||
"ui",
|
||||
"ui"
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
ahash = { version = "0.7", optional = true }
|
||||
async-trait = { version = "0.1", optional = true }
|
||||
crossterm = "0.19"
|
||||
tempfile = "3"
|
||||
atty = "0.2"
|
||||
ui = { path = "./ui" }
|
||||
|
||||
smol = { version = "1.2", optional = true }
|
||||
tokio = { version = "1.5", optional = true, features = ["fs", "process", "io-util"] }
|
||||
async-std = { version = "1.9", optional = true, features = ["unstable"] }
|
||||
ahash = { version = "0.7", optional = true }
|
||||
async-trait = { version = "0.1", optional = true }
|
||||
futures = { version = "0.3", optional = true }
|
||||
|
||||
# The following dependencies are renamed in the form `<dep-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"]
|
||||
|
|
83
src/lib.rs
83
src/lib.rs
|
@ -1,14 +1,14 @@
|
|||
mod answer;
|
||||
mod error;
|
||||
mod question;
|
||||
|
||||
use crossterm::tty::IsTty;
|
||||
use ui::{backend, error, events};
|
||||
|
||||
pub use answer::{Answer, Answers, ExpandItem, ListItem};
|
||||
pub use question::{Choice::Choice, Choice::Separator, Plugin, Question};
|
||||
pub use question::{Choice::Choice, Choice::Separator, Question};
|
||||
pub use ui::error::{ErrorKind, Result};
|
||||
|
||||
pub mod plugin {
|
||||
pub use crate::Plugin;
|
||||
pub use crate::question::Plugin;
|
||||
pub use ui;
|
||||
}
|
||||
|
||||
|
@ -37,12 +37,15 @@ where
|
|||
self
|
||||
}
|
||||
|
||||
fn prompt_impl<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
|
||||
)*
|
||||
};
|
||||
|
|
|
@ -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", ")?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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
219
ui/src/backend/crossterm.rs
Normal 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
286
ui/src/backend/curses.rs
Normal 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
155
ui/src/backend/mod.rs
Normal 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
351
ui/src/backend/style.rs
Normal 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
264
ui/src/backend/termion.rs
Normal 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(())
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
49
ui/src/events/crossterm.rs
Normal file
49
ui/src/events/crossterm.rs
Normal 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
112
ui/src/events/mod.rs
Normal 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
134
ui/src/events/unix.rs
Normal 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
77
ui/src/events/win.rs
Normal 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!(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user