first question impl
There are a lot of things to do, but everything renders well
This commit is contained in:
parent
c1aa566d97
commit
818e2b54ee
|
@ -11,3 +11,7 @@ members = [
|
|||
]
|
||||
|
||||
[dependencies]
|
||||
crossterm = "0.19.0"
|
||||
fxhash = "0.2.1"
|
||||
tempfile = "3"
|
||||
ui = { path = "./ui" }
|
||||
|
|
24
src/answer.rs
Normal file
24
src/answer.rs
Normal file
|
@ -0,0 +1,24 @@
|
|||
use fxhash::FxHashSet as HashSet;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Answer {
|
||||
String(String),
|
||||
ListItem(ListItem),
|
||||
ExpandItem(ExpandItem),
|
||||
Int(i64),
|
||||
Float(f64),
|
||||
Bool(bool),
|
||||
ListItems(HashSet<ListItem>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ListItem {
|
||||
pub index: usize,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ExpandItem {
|
||||
pub key: char,
|
||||
pub name: String,
|
||||
}
|
59
src/error.rs
Normal file
59
src/error.rs
Normal file
|
@ -0,0 +1,59 @@
|
|||
use std::{fmt, io};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, InquirerError>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum InquirerError {
|
||||
IoError(io::Error),
|
||||
FmtError(fmt::Error),
|
||||
Utf8Error(std::string::FromUtf8Error),
|
||||
ParseIntError(std::num::ParseIntError),
|
||||
}
|
||||
|
||||
impl std::error::Error for InquirerError {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: better display impl
|
||||
impl fmt::Display for InquirerError {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
InquirerError::IoError(_) => write!(fmt, "IO-error occurred"),
|
||||
_ => write!(fmt, "Some error has occurred"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_from {
|
||||
($from:path, $e:ident => $body:expr) => {
|
||||
impl From<$from> for InquirerError {
|
||||
fn from(e: $from) -> Self {
|
||||
let $e = e;
|
||||
$body
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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!(crossterm::ErrorKind, e =>
|
||||
match e {
|
||||
crossterm::ErrorKind::IoError(e) => Self::from(e),
|
||||
crossterm::ErrorKind::FmtError(e) => Self::from(e),
|
||||
crossterm::ErrorKind::Utf8Error(e) => Self::from(e),
|
||||
crossterm::ErrorKind::ParseIntError(e) => Self::from(e),
|
||||
crossterm::ErrorKind::ResizingTerminalFailure(_)
|
||||
| crossterm::ErrorKind::SettingTerminalTitleFailure => unreachable!(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
);
|
39
src/lib.rs
39
src/lib.rs
|
@ -1,7 +1,36 @@
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
// FIXME: remove this
|
||||
#![allow(dead_code)]
|
||||
|
||||
mod answer;
|
||||
mod error;
|
||||
pub mod question;
|
||||
|
||||
pub use answer::{Answer, ExpandItem, ListItem};
|
||||
pub use question::{Choice, Choice::Separator, Question};
|
||||
|
||||
pub struct Inquisition {
|
||||
questions: Vec<Question>,
|
||||
}
|
||||
|
||||
impl Inquisition {
|
||||
pub fn new(questions: Vec<Question>) -> Self {
|
||||
Inquisition { questions }
|
||||
}
|
||||
|
||||
pub fn add_question(&mut self, question: Question) {
|
||||
self.questions.push(question)
|
||||
}
|
||||
|
||||
pub fn prompt(self) -> PromptModule {
|
||||
PromptModule {
|
||||
answers: Vec::with_capacity(self.questions.len()),
|
||||
questions: self.questions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: ask questions
|
||||
pub struct PromptModule {
|
||||
questions: Vec<Question>,
|
||||
answers: Vec<Answer>,
|
||||
}
|
||||
|
|
184
src/main.rs
Normal file
184
src/main.rs
Normal file
|
@ -0,0 +1,184 @@
|
|||
// TODO: delete
|
||||
// this is a temporary file, for testing out the prompts
|
||||
use inquisition::{Choice::Separator, ExpandItem, Question};
|
||||
use std::{env, io};
|
||||
|
||||
fn main() {
|
||||
let (a, b) = match env::args().nth(1).as_deref() {
|
||||
Some("b") => (
|
||||
Question::confirm("a".into(), "Hello there 1".into(), true),
|
||||
Question::confirm("b".into(), "Hello there 2".into(), false),
|
||||
),
|
||||
Some("s") => (
|
||||
Question::input("a".into(), "Hello there 1".into(), "No".into()),
|
||||
Question::input("b".into(), "Hello there 2".into(), "Yes".into()),
|
||||
),
|
||||
Some("p") => (
|
||||
Question::password("a".into(), "password 1".into()).with_mask('*'),
|
||||
Question::password("b".into(), "password 2".into()),
|
||||
),
|
||||
Some("i") => (
|
||||
Question::int("a".into(), "int 1".into(), 0),
|
||||
Question::int("b".into(), "int 2".into(), 3),
|
||||
),
|
||||
Some("f") => (
|
||||
Question::float("a".into(), "float 1".into(), 0.123),
|
||||
Question::float("b".into(), "float 2".into(), 3.12),
|
||||
),
|
||||
Some("e") => (
|
||||
Question::editor("a".into(), "editor 1".into()),
|
||||
Question::editor("b".into(), "editor 2".into()),
|
||||
),
|
||||
|
||||
Some("l") => (
|
||||
Question::list(
|
||||
"a".into(),
|
||||
"list 1".into(),
|
||||
vec![
|
||||
Separator(Some("=== TITLE BOI ===".into())),
|
||||
"hello worldssssss 1".into(),
|
||||
"hello worldssssss 2".into(),
|
||||
"hello worldssssss 3".into(),
|
||||
"hello worldssssss 4".into(),
|
||||
"hello worldssssss 5".into(),
|
||||
],
|
||||
0,
|
||||
),
|
||||
Question::list(
|
||||
"b".into(),
|
||||
"list 2".into(),
|
||||
vec![
|
||||
"0".into(),
|
||||
Separator(None),
|
||||
"1".into(),
|
||||
"2".into(),
|
||||
"3".into(),
|
||||
Separator(Some("== Hello separator".into())),
|
||||
],
|
||||
0,
|
||||
),
|
||||
),
|
||||
|
||||
Some("c") => (
|
||||
Question::checkbox(
|
||||
"a".into(),
|
||||
"checkbox 1".into(),
|
||||
vec![
|
||||
Separator(Some("=== TITLE BOI ===".into())),
|
||||
"hello worldssssss 1".into(),
|
||||
"hello worldssssss 2".into(),
|
||||
"hello worldssssss 3".into(),
|
||||
"hello worldssssss 4".into(),
|
||||
"hello worldssssss 5".into(),
|
||||
],
|
||||
),
|
||||
Question::checkbox(
|
||||
"b".into(),
|
||||
"checkbox 2".into(),
|
||||
vec![
|
||||
"0".into(),
|
||||
Separator(None),
|
||||
"1".into(),
|
||||
"2".into(),
|
||||
"3".into(),
|
||||
Separator(Some("== Hello separator".into())),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Some("r") => (
|
||||
Question::raw_list(
|
||||
"a".into(),
|
||||
"list 1".into(),
|
||||
vec![
|
||||
Separator(Some("=== TITLE BOI ===".into())),
|
||||
"hello worldssssss 1".into(),
|
||||
"hello worldssssss 2".into(),
|
||||
"hello worldssssss 3".into(),
|
||||
"hello worldssssss 4".into(),
|
||||
"hello worldssssss 5".into(),
|
||||
],
|
||||
0,
|
||||
),
|
||||
Question::raw_list(
|
||||
"b".into(),
|
||||
"list 2".into(),
|
||||
vec![
|
||||
"0".into(),
|
||||
Separator(None),
|
||||
"1".into(),
|
||||
"2".into(),
|
||||
"3".into(),
|
||||
Separator(Some("== Hello separator".into())),
|
||||
],
|
||||
0,
|
||||
),
|
||||
),
|
||||
|
||||
Some("x") => (
|
||||
Question::expand(
|
||||
"a".into(),
|
||||
"expand 1".into(),
|
||||
vec![
|
||||
ExpandItem {
|
||||
key: 'y',
|
||||
name: "Overwrite".into(),
|
||||
}
|
||||
.into(),
|
||||
ExpandItem {
|
||||
key: 'a',
|
||||
name: "Overwrite this one and all next".into(),
|
||||
}
|
||||
.into(),
|
||||
ExpandItem {
|
||||
key: 'd',
|
||||
name: "Show diff".into(),
|
||||
}
|
||||
.into(),
|
||||
Separator(None),
|
||||
ExpandItem {
|
||||
key: 'x',
|
||||
name: "Abort".into(),
|
||||
}
|
||||
.into(),
|
||||
],
|
||||
None,
|
||||
),
|
||||
Question::expand(
|
||||
"b".into(),
|
||||
"expand 2".into(),
|
||||
vec![
|
||||
ExpandItem {
|
||||
key: 'a',
|
||||
name: "Name for a".into(),
|
||||
}
|
||||
.into(),
|
||||
Separator(None),
|
||||
ExpandItem {
|
||||
key: 'b',
|
||||
name: "Name for b".into(),
|
||||
}
|
||||
.into(),
|
||||
ExpandItem {
|
||||
key: 'c',
|
||||
name: "Name for c".into(),
|
||||
}
|
||||
.into(),
|
||||
Separator(None),
|
||||
ExpandItem {
|
||||
key: 'd',
|
||||
name: "Name for d".into(),
|
||||
}
|
||||
.into(),
|
||||
Separator(Some("== Hello separator".into())),
|
||||
],
|
||||
Some('b'),
|
||||
),
|
||||
),
|
||||
_ => panic!("no arg"),
|
||||
};
|
||||
|
||||
let mut stdout = io::stdout();
|
||||
println!("{:?}", a.ask(&mut stdout));
|
||||
println!("{:?}", b.ask(&mut stdout));
|
||||
}
|
205
src/question/checkbox.rs
Normal file
205
src/question/checkbox.rs
Normal file
|
@ -0,0 +1,205 @@
|
|||
use std::io;
|
||||
|
||||
use crossterm::{
|
||||
event, execute, queue,
|
||||
style::{Color, Print, ResetColor, SetForegroundColor},
|
||||
};
|
||||
use ui::{widgets, Widget};
|
||||
|
||||
use crate::{answer::ListItem, error, Answer};
|
||||
|
||||
use super::Options;
|
||||
|
||||
pub struct Checkbox {
|
||||
// FIXME: What is type here?
|
||||
choices: super::ChoiceList<String>,
|
||||
selected: Vec<bool>,
|
||||
}
|
||||
|
||||
struct CheckboxPrompt {
|
||||
picker: widgets::ListPicker<Checkbox>,
|
||||
opts: Options,
|
||||
}
|
||||
|
||||
impl ui::Prompt for CheckboxPrompt {
|
||||
type ValidateErr = &'static str;
|
||||
type Output = Checkbox;
|
||||
|
||||
fn prompt(&self) -> &str {
|
||||
&self.opts.message
|
||||
}
|
||||
|
||||
fn hint(&self) -> Option<&str> {
|
||||
Some("(Press <space> to select, <a> to toggle all, <i> to invert selection)")
|
||||
}
|
||||
|
||||
fn finish(self) -> Self::Output {
|
||||
self.picker.finish()
|
||||
}
|
||||
|
||||
fn finish_default(self) -> Self::Output {
|
||||
unreachable!()
|
||||
}
|
||||
fn has_default(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
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 height(&self) -> usize {
|
||||
self.picker.height()
|
||||
}
|
||||
|
||||
fn handle_key(&mut self, key: event::KeyEvent) -> bool {
|
||||
match key.code {
|
||||
event::KeyCode::Char(' ') => {
|
||||
let index = self.picker.get_at();
|
||||
self.picker.list.selected[index] = !self.picker.list.selected[index];
|
||||
}
|
||||
event::KeyCode::Char('i') => {
|
||||
self.picker.list.selected.iter_mut().for_each(|s| *s = !*s);
|
||||
}
|
||||
event::KeyCode::Char('a') => {
|
||||
let select_state = self.picker.list.selected.iter().any(|s| !s);
|
||||
self.picker
|
||||
.list
|
||||
.selected
|
||||
.iter_mut()
|
||||
.for_each(|s| *s = select_state);
|
||||
}
|
||||
_ => return self.picker.handle_key(key),
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn cursor_pos(&self, prompt_len: u16) -> (u16, u16) {
|
||||
self.picker.cursor_pos(prompt_len)
|
||||
}
|
||||
}
|
||||
|
||||
impl widgets::List for Checkbox {
|
||||
fn render_item<W: io::Write>(
|
||||
&mut self,
|
||||
index: usize,
|
||||
hovered: bool,
|
||||
max_width: usize,
|
||||
w: &mut W,
|
||||
) -> crossterm::Result<()> {
|
||||
if hovered {
|
||||
queue!(w, SetForegroundColor(Color::DarkCyan), Print("❯ "))?;
|
||||
} else {
|
||||
w.write_all(b" ")?;
|
||||
}
|
||||
|
||||
if self.is_selectable(index) {
|
||||
if self.selected[index] {
|
||||
queue!(w, SetForegroundColor(Color::Green), Print("◉ "),)?;
|
||||
|
||||
if hovered {
|
||||
queue!(w, SetForegroundColor(Color::DarkCyan))?;
|
||||
} else {
|
||||
queue!(w, ResetColor)?;
|
||||
}
|
||||
} else {
|
||||
w.write_all("◯ ".as_bytes())?;
|
||||
}
|
||||
} else {
|
||||
queue!(w, SetForegroundColor(Color::DarkGrey))?;
|
||||
}
|
||||
|
||||
self.choices[index].as_str().render(max_width - 4, w)?;
|
||||
|
||||
queue!(w, ResetColor)
|
||||
}
|
||||
|
||||
fn is_selectable(&self, index: usize) -> bool {
|
||||
!self.choices[index].is_separator()
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.choices.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl Checkbox {
|
||||
pub fn ask<W: io::Write>(self, opts: super::Options, w: &mut W) -> error::Result<Answer> {
|
||||
// We cannot simply process the Vec<bool> to a HashSet<ListItem> since we want to print the
|
||||
// selected ones in order
|
||||
let checkbox = ui::Input::new(CheckboxPrompt {
|
||||
picker: widgets::ListPicker::new(self),
|
||||
opts,
|
||||
})
|
||||
.hide_cursor()
|
||||
.run(w)?;
|
||||
|
||||
queue!(w, SetForegroundColor(Color::DarkCyan))?;
|
||||
print_comma_separated(
|
||||
checkbox
|
||||
.selected
|
||||
.iter()
|
||||
.zip(checkbox.choices.choices.iter())
|
||||
.filter_map(|item| match item {
|
||||
(true, super::Choice::Choice(name)) => Some(name.as_str()),
|
||||
_ => None,
|
||||
}),
|
||||
w,
|
||||
)?;
|
||||
|
||||
w.write_all(b"\n")?;
|
||||
execute!(w, ResetColor)?;
|
||||
|
||||
let ans = checkbox
|
||||
.selected
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.zip(checkbox.choices.choices.into_iter())
|
||||
.filter_map(|((index, is_selected), name)| match (is_selected, name) {
|
||||
(true, super::Choice::Choice(name)) => Some(ListItem { index, name }),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Answer::ListItems(ans))
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Question {
|
||||
pub fn checkbox(name: String, message: String, choices: Vec<super::Choice<String>>) -> Self {
|
||||
Self::new(
|
||||
name,
|
||||
message,
|
||||
super::QuestionKind::Checkbox(Checkbox {
|
||||
selected: vec![false; choices.len()],
|
||||
choices: super::ChoiceList {
|
||||
choices,
|
||||
default: 0,
|
||||
should_loop: true,
|
||||
// FIXME: this should be something sensible. page size is currently not used so
|
||||
// its fine for now
|
||||
page_size: 0,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn print_comma_separated<'a, W: io::Write>(
|
||||
iter: impl Iterator<Item = &'a str>,
|
||||
w: &mut W,
|
||||
) -> io::Result<()> {
|
||||
let mut iter = iter.peekable();
|
||||
|
||||
while let Some(item) = iter.next() {
|
||||
w.write_all(item.as_bytes())?;
|
||||
if iter.peek().is_some() {
|
||||
w.write_all(b", ")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
86
src/question/choice_list.rs
Normal file
86
src/question/choice_list.rs
Normal file
|
@ -0,0 +1,86 @@
|
|||
use std::ops::{Index, IndexMut};
|
||||
|
||||
pub(crate) struct ChoiceList<T> {
|
||||
pub(crate) choices: Vec<Choice<T>>,
|
||||
pub(crate) default: usize,
|
||||
pub(crate) should_loop: bool,
|
||||
pub(crate) page_size: usize,
|
||||
}
|
||||
|
||||
impl<T> ChoiceList<T> {
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.choices.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Index<usize> for ChoiceList<T> {
|
||||
type Output = Choice<T>;
|
||||
|
||||
fn index(&self, index: usize) -> &Self::Output {
|
||||
&self.choices[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IndexMut<usize> for ChoiceList<T> {
|
||||
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
|
||||
&mut self.choices[index]
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Choice<T> {
|
||||
Choice(T),
|
||||
Separator(Option<String>),
|
||||
}
|
||||
|
||||
impl<T> Choice<T> {
|
||||
pub(crate) fn is_separator(&self) -> bool {
|
||||
matches!(self, Choice::Separator(_))
|
||||
}
|
||||
|
||||
pub(crate) fn as_ref(&self) -> Choice<&T> {
|
||||
match self {
|
||||
Choice::Choice(t) => Choice::Choice(t),
|
||||
Choice::Separator(s) => Choice::Separator(s.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn unwrap_choice(self) -> T {
|
||||
if let Choice::Choice(c) = self {
|
||||
c
|
||||
} else {
|
||||
panic!("Called unwrap_choice on separator")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_sep_str(separator: &Option<String>) -> &str {
|
||||
separator
|
||||
.as_ref()
|
||||
.map(String::as_str)
|
||||
.unwrap_or("──────────────")
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> Choice<T> {
|
||||
pub(crate) fn as_str(&self) -> &str {
|
||||
match self {
|
||||
Choice::Choice(t) => t.as_ref(),
|
||||
Choice::Separator(s) => get_sep_str(s),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn as_bytes(&self) -> &[u8] {
|
||||
self.as_str().as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Choice<T> {
|
||||
fn from(t: T) -> Self {
|
||||
Choice::Choice(t)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'_ str> for Choice<String> {
|
||||
fn from(s: &str) -> Self {
|
||||
Choice::Choice(s.into())
|
||||
}
|
||||
}
|
101
src/question/confirm.rs
Normal file
101
src/question/confirm.rs
Normal file
|
@ -0,0 +1,101 @@
|
|||
use crossterm::style::Colorize;
|
||||
use ui::{widgets, Widget};
|
||||
|
||||
use crate::{error, Answer};
|
||||
|
||||
use super::Options;
|
||||
|
||||
pub struct Confirm {
|
||||
pub(crate) default: bool,
|
||||
}
|
||||
|
||||
struct ConfirmPrompt {
|
||||
confirm: Confirm,
|
||||
opts: Options,
|
||||
input: widgets::CharInput,
|
||||
}
|
||||
|
||||
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 height(&self) -> usize {
|
||||
self.input.height()
|
||||
}
|
||||
|
||||
fn handle_key(&mut self, key: crossterm::event::KeyEvent) -> bool {
|
||||
self.input.handle_key(key)
|
||||
}
|
||||
|
||||
fn cursor_pos(&self, prompt_len: u16) -> (u16, u16) {
|
||||
self.input.cursor_pos(prompt_len)
|
||||
}
|
||||
}
|
||||
|
||||
fn only_yn(c: char) -> Option<char> {
|
||||
match c {
|
||||
'y' | 'Y' | 'n' | 'N' => Some(c),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
impl ui::Prompt for ConfirmPrompt {
|
||||
type ValidateErr = &'static str;
|
||||
type Output = bool;
|
||||
|
||||
fn prompt(&self) -> &str {
|
||||
&self.opts.message
|
||||
}
|
||||
|
||||
fn hint(&self) -> Option<&str> {
|
||||
if self.confirm.default {
|
||||
Some("(y/n) (default y)")
|
||||
} else {
|
||||
Some("(y/n) (default n)")
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(self) -> Self::Output {
|
||||
match self.input.finish() {
|
||||
Some('y') | Some('Y') => true,
|
||||
Some('n') | Some('N') => true,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_default(self) -> Self::Output {
|
||||
self.confirm.default
|
||||
}
|
||||
}
|
||||
|
||||
impl Confirm {
|
||||
pub(crate) fn ask<W: std::io::Write>(
|
||||
self,
|
||||
opts: super::Options,
|
||||
w: &mut W,
|
||||
) -> error::Result<Answer> {
|
||||
let ans = ui::Input::new(ConfirmPrompt {
|
||||
confirm: self,
|
||||
opts,
|
||||
input: widgets::CharInput::new(only_yn),
|
||||
})
|
||||
.run(w)?;
|
||||
|
||||
let s = if ans { "Yes" } else { "No" };
|
||||
|
||||
writeln!(w, "{}", s.dark_cyan())?;
|
||||
|
||||
Ok(Answer::Bool(ans))
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Question {
|
||||
pub fn confirm(name: String, message: String, default: bool) -> Self {
|
||||
Self::new(
|
||||
name,
|
||||
message,
|
||||
super::QuestionKind::Confirm(Confirm { default }),
|
||||
)
|
||||
}
|
||||
}
|
97
src/question/editor.rs
Normal file
97
src/question/editor.rs
Normal file
|
@ -0,0 +1,97 @@
|
|||
use std::{
|
||||
env,
|
||||
ffi::OsString,
|
||||
io::{self, Read},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use crossterm::style::Colorize;
|
||||
use ui::Widget;
|
||||
|
||||
use crate::{error, Answer};
|
||||
|
||||
use super::{Options, Question, QuestionKind};
|
||||
|
||||
pub struct Editor {
|
||||
// FIXME: What is correct type here?
|
||||
default: String,
|
||||
// TODO: What is this??
|
||||
postfix: (),
|
||||
}
|
||||
|
||||
fn get_editor() -> OsString {
|
||||
env::var_os("VISUAL")
|
||||
.or_else(|| env::var_os("EDITOR"))
|
||||
.unwrap_or_else(|| {
|
||||
if cfg!(windows) {
|
||||
"notepad".into()
|
||||
} else {
|
||||
"vim".into()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
struct EditorPrompt {
|
||||
opts: Options,
|
||||
}
|
||||
|
||||
impl Widget for EditorPrompt {
|
||||
fn render<W: io::Write>(&mut self, _: usize, _: &mut W) -> crossterm::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn height(&self) -> usize {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
impl ui::Prompt for EditorPrompt {
|
||||
type ValidateErr = &'static str;
|
||||
type Output = ();
|
||||
|
||||
fn prompt(&self) -> &str {
|
||||
&self.opts.message
|
||||
}
|
||||
|
||||
fn hint(&self) -> Option<&str> {
|
||||
Some("Press <enter> to launch your preferred editor.")
|
||||
}
|
||||
|
||||
fn finish(self) -> Self::Output {}
|
||||
|
||||
fn finish_default(self) -> Self::Output {}
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
pub fn ask<W: io::Write>(self, opts: Options, w: &mut W) -> error::Result<Answer> {
|
||||
ui::Input::new(EditorPrompt { opts }).run(w)?;
|
||||
|
||||
let mut file = tempfile::NamedTempFile::new()?;
|
||||
|
||||
// FIXME: handle error
|
||||
assert!(Command::new(get_editor())
|
||||
.arg(file.path())
|
||||
.status()?
|
||||
.success());
|
||||
|
||||
let mut ans = String::new();
|
||||
file.read_to_string(&mut ans)?;
|
||||
|
||||
writeln!(w, "{}", "Received".dark_grey())?;
|
||||
|
||||
Ok(Answer::String(ans))
|
||||
}
|
||||
}
|
||||
|
||||
impl Question {
|
||||
pub fn editor(name: String, message: String) -> Self {
|
||||
Self::new(
|
||||
name,
|
||||
message,
|
||||
QuestionKind::Editor(Editor {
|
||||
default: String::new(),
|
||||
postfix: (),
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
272
src/question/expand.rs
Normal file
272
src/question/expand.rs
Normal file
|
@ -0,0 +1,272 @@
|
|||
use crossterm::{
|
||||
cursor, queue,
|
||||
style::{Color, Colorize, ResetColor, SetForegroundColor},
|
||||
terminal,
|
||||
};
|
||||
use ui::{widgets, Validation, Widget};
|
||||
|
||||
use crate::{answer::ExpandItem, error, Answer};
|
||||
|
||||
use super::{Choice, Options};
|
||||
|
||||
pub struct Expand {
|
||||
choices: super::ChoiceList<ExpandItem>,
|
||||
selected: Option<char>,
|
||||
default: Option<char>,
|
||||
}
|
||||
|
||||
struct ExpandPrompt<'a, F> {
|
||||
list: widgets::ListPicker<Expand>,
|
||||
input: widgets::CharInput<F>,
|
||||
opts: Options,
|
||||
hint: &'a str,
|
||||
expanded: bool,
|
||||
}
|
||||
|
||||
impl<F> ExpandPrompt<'_, F> {
|
||||
fn finish_with(self, c: char) -> ExpandItem {
|
||||
self.list
|
||||
.finish()
|
||||
.choices
|
||||
.choices
|
||||
.into_iter()
|
||||
.filter_map(|choice| match choice {
|
||||
Choice::Choice(choice) => Some(choice),
|
||||
_ => None,
|
||||
})
|
||||
.find(|item| item.key == c)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: Fn(char) -> Option<char>> ui::Prompt for ExpandPrompt<'_, F> {
|
||||
type ValidateErr = &'static str;
|
||||
type Output = ExpandItem;
|
||||
|
||||
fn prompt(&self) -> &str {
|
||||
&self.opts.message
|
||||
}
|
||||
|
||||
fn hint(&self) -> Option<&str> {
|
||||
Some(self.hint)
|
||||
}
|
||||
|
||||
fn validate(&mut self) -> Result<Validation, Self::ValidateErr> {
|
||||
match self.input.value() {
|
||||
Some('h') => {
|
||||
self.expanded = true;
|
||||
self.input.set_value(None);
|
||||
self.list.list.selected = None;
|
||||
Ok(Validation::Continue)
|
||||
}
|
||||
None if self.list.list.default.is_none() => Err("Please enter a command"),
|
||||
_ => Ok(Validation::Finish),
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(self) -> Self::Output {
|
||||
let c = self.input.value().unwrap();
|
||||
self.finish_with(c)
|
||||
}
|
||||
|
||||
fn has_default(&self) -> bool {
|
||||
self.list.list.default.is_some()
|
||||
}
|
||||
|
||||
fn finish_default(self) -> Self::Output {
|
||||
let c = self.list.list.default.unwrap();
|
||||
self.finish_with(c)
|
||||
}
|
||||
}
|
||||
|
||||
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<()> {
|
||||
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)
|
||||
} else {
|
||||
self.input.render(max_width, w)?;
|
||||
|
||||
if let Some(key) = self.input.value() {
|
||||
let name = &self
|
||||
.list
|
||||
.list
|
||||
.choices
|
||||
.choices
|
||||
.iter()
|
||||
.filter_map(|choice| match choice {
|
||||
Choice::Choice(choice) => Some(choice),
|
||||
_ => None,
|
||||
})
|
||||
.find(|item| item.key == key)
|
||||
.map(|item| &*item.name)
|
||||
.unwrap_or("Help, list all options");
|
||||
|
||||
queue!(w, cursor::MoveToNextLine(1))?;
|
||||
|
||||
write!(w, "{} {}", ">>".dark_cyan(), name)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn height(&self) -> usize {
|
||||
if self.expanded {
|
||||
self.list.height() + 1
|
||||
} else if self.input.value().is_some() {
|
||||
self.input.height() + 1
|
||||
} else {
|
||||
self.input.height()
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_key(&mut self, key: crossterm::event::KeyEvent) -> bool {
|
||||
if self.input.handle_key(key) {
|
||||
self.list.list.selected = self.input.value();
|
||||
true
|
||||
} else if self.expanded {
|
||||
self.list.handle_key(key)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn cursor_pos(&self, prompt_len: u16) -> (u16, u16) {
|
||||
if self.expanded {
|
||||
let w = self.input.cursor_pos(ANSWER_PROMPT.len() as u16).0;
|
||||
(w, self.height() as u16)
|
||||
} else {
|
||||
self.input.cursor_pos(prompt_len)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Expand {
|
||||
fn render_choice<W: std::io::Write>(
|
||||
&self,
|
||||
item: &ExpandItem,
|
||||
max_width: usize,
|
||||
w: &mut W,
|
||||
) -> crossterm::Result<()> {
|
||||
let hovered = self.selected.map(|c| c == item.key).unwrap_or(false);
|
||||
|
||||
if hovered {
|
||||
queue!(w, SetForegroundColor(Color::DarkCyan))?;
|
||||
}
|
||||
|
||||
write!(w, " {}) ", item.key)?;
|
||||
item.name.as_str().render(max_width - 5, w)?;
|
||||
|
||||
if hovered {
|
||||
queue!(w, ResetColor)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static HELP_CHOICE: ExpandItem = ExpandItem {
|
||||
key: 'h',
|
||||
name: "Help, list all options".into(),
|
||||
};
|
||||
}
|
||||
|
||||
impl widgets::List for Expand {
|
||||
fn render_item<W: std::io::Write>(
|
||||
&mut self,
|
||||
index: usize,
|
||||
_: bool,
|
||||
max_width: usize,
|
||||
w: &mut W,
|
||||
) -> crossterm::Result<()> {
|
||||
if index == self.choices.len() {
|
||||
return HELP_CHOICE.with(|h| self.render_choice(h, max_width, w));
|
||||
}
|
||||
|
||||
match &self.choices[index] {
|
||||
Choice::Choice(item) => self.render_choice(item, max_width, w),
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_selectable(&self, _: usize) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.choices.len() + 1
|
||||
}
|
||||
}
|
||||
|
||||
impl Expand {
|
||||
pub fn ask<W: std::io::Write>(self, opts: Options, w: &mut W) -> error::Result<Answer> {
|
||||
let hint: String = {
|
||||
let mut s = String::with_capacity(3 + self.choices.len());
|
||||
s.push('(');
|
||||
s.extend(
|
||||
self.choices
|
||||
.choices
|
||||
.iter()
|
||||
.filter_map(|choice| match choice {
|
||||
Choice::Choice(choice) => Some(choice.key),
|
||||
_ => None,
|
||||
}),
|
||||
);
|
||||
s += "h)";
|
||||
s
|
||||
};
|
||||
|
||||
let ans = ui::Input::new(ExpandPrompt {
|
||||
input: widgets::CharInput::new(|c| {
|
||||
let c = c.to_ascii_lowercase();
|
||||
hint[1..(hint.len() - 1)].contains(c).then(|| c)
|
||||
}),
|
||||
list: widgets::ListPicker::new(self),
|
||||
opts,
|
||||
hint: &hint,
|
||||
expanded: false,
|
||||
})
|
||||
.run(w)?;
|
||||
|
||||
writeln!(w, "{}", ans.name.as_str().dark_cyan())?;
|
||||
|
||||
Ok(Answer::ExpandItem(ans))
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Question {
|
||||
pub fn expand(
|
||||
name: String,
|
||||
message: String,
|
||||
choices: Vec<Choice<ExpandItem>>,
|
||||
default: Option<char>,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
name,
|
||||
message,
|
||||
super::QuestionKind::Expand(Expand {
|
||||
choices: super::ChoiceList {
|
||||
choices,
|
||||
default: 0,
|
||||
should_loop: true,
|
||||
// FIXME: this should be something sensible. page size is currently not used so
|
||||
// its fine for now
|
||||
page_size: 0,
|
||||
},
|
||||
selected: None,
|
||||
default,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
78
src/question/input.rs
Normal file
78
src/question/input.rs
Normal file
|
@ -0,0 +1,78 @@
|
|||
use crossterm::style::Colorize;
|
||||
use ui::{widgets, Widget};
|
||||
|
||||
use crate::{error, Answer};
|
||||
|
||||
use super::Options;
|
||||
|
||||
pub struct Input {
|
||||
// FIXME: reference instead?
|
||||
pub(crate) default: String,
|
||||
}
|
||||
|
||||
struct InputPrompt {
|
||||
input_opts: Input,
|
||||
opts: Options,
|
||||
input: widgets::StringInput,
|
||||
hint: String,
|
||||
}
|
||||
|
||||
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 height(&self) -> usize {
|
||||
self.input.height()
|
||||
}
|
||||
|
||||
fn handle_key(&mut self, key: crossterm::event::KeyEvent) -> bool {
|
||||
self.input.handle_key(key)
|
||||
}
|
||||
|
||||
fn cursor_pos(&self, prompt_len: u16) -> (u16, u16) {
|
||||
self.input.cursor_pos(prompt_len)
|
||||
}
|
||||
}
|
||||
|
||||
impl ui::Prompt for InputPrompt {
|
||||
type ValidateErr = &'static str;
|
||||
type Output = String;
|
||||
|
||||
fn prompt(&self) -> &str {
|
||||
&self.opts.message
|
||||
}
|
||||
|
||||
fn hint(&self) -> Option<&str> {
|
||||
Some(&self.hint)
|
||||
}
|
||||
|
||||
fn finish(self) -> Self::Output {
|
||||
self.input.finish().unwrap_or(self.input_opts.default)
|
||||
}
|
||||
fn finish_default(self) -> <Self as ui::Prompt>::Output {
|
||||
self.input_opts.default
|
||||
}
|
||||
}
|
||||
|
||||
impl Input {
|
||||
pub fn ask<W: std::io::Write>(self, opts: Options, w: &mut W) -> error::Result<Answer> {
|
||||
let ans = ui::Input::new(InputPrompt {
|
||||
opts,
|
||||
hint: format!("({})", self.default),
|
||||
input_opts: self,
|
||||
input: widgets::StringInput::default(),
|
||||
})
|
||||
.run(w)?;
|
||||
|
||||
writeln!(w, "{}", ans.as_str().dark_cyan())?;
|
||||
|
||||
Ok(Answer::String(ans))
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Question {
|
||||
pub fn input(name: String, message: String, default: String) -> Self {
|
||||
Self::new(name, message, super::QuestionKind::Input(Input { default }))
|
||||
}
|
||||
}
|
149
src/question/list.rs
Normal file
149
src/question/list.rs
Normal file
|
@ -0,0 +1,149 @@
|
|||
use crossterm::{
|
||||
queue,
|
||||
style::{Color, Colorize, Print, ResetColor, SetForegroundColor},
|
||||
};
|
||||
use ui::{widgets, Widget};
|
||||
|
||||
use crate::{
|
||||
answer::{Answer, ListItem},
|
||||
error,
|
||||
};
|
||||
|
||||
use super::Options;
|
||||
|
||||
pub struct List {
|
||||
// FIXME: Whats the correct type?
|
||||
choices: super::ChoiceList<String>,
|
||||
}
|
||||
|
||||
struct ListPrompt {
|
||||
picker: widgets::ListPicker<List>,
|
||||
opts: Options,
|
||||
}
|
||||
|
||||
impl ListPrompt {
|
||||
fn finish_index(self, index: usize) -> ListItem {
|
||||
ListItem {
|
||||
index,
|
||||
name: self
|
||||
.picker
|
||||
.finish()
|
||||
.choices
|
||||
.choices
|
||||
.swap_remove(index)
|
||||
.unwrap_choice(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ui::Prompt for ListPrompt {
|
||||
type ValidateErr = &'static str;
|
||||
type Output = ListItem;
|
||||
|
||||
fn prompt(&self) -> &str {
|
||||
&self.opts.message
|
||||
}
|
||||
|
||||
fn hint(&self) -> Option<&str> {
|
||||
Some("(Use arrow keys)")
|
||||
}
|
||||
|
||||
fn finish(self) -> Self::Output {
|
||||
let index = self.picker.get_at();
|
||||
self.finish_index(index)
|
||||
}
|
||||
|
||||
fn finish_default(self) -> Self::Output {
|
||||
let index = self.picker.list.choices.default;
|
||||
self.finish_index(index)
|
||||
}
|
||||
}
|
||||
|
||||
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 height(&self) -> usize {
|
||||
self.picker.height()
|
||||
}
|
||||
|
||||
fn handle_key(&mut self, key: crossterm::event::KeyEvent) -> bool {
|
||||
self.picker.handle_key(key)
|
||||
}
|
||||
|
||||
fn cursor_pos(&self, prompt_len: u16) -> (u16, u16) {
|
||||
self.picker.cursor_pos(prompt_len)
|
||||
}
|
||||
}
|
||||
|
||||
impl widgets::List for List {
|
||||
fn render_item<W: std::io::Write>(
|
||||
&mut self,
|
||||
index: usize,
|
||||
hovered: bool,
|
||||
max_width: usize,
|
||||
w: &mut W,
|
||||
) -> crossterm::Result<()> {
|
||||
if hovered {
|
||||
queue!(w, SetForegroundColor(Color::DarkCyan), Print("❯ "))?;
|
||||
} else {
|
||||
w.write_all(b" ")?;
|
||||
|
||||
if !self.is_selectable(index) {
|
||||
queue!(w, SetForegroundColor(Color::DarkGrey))?;
|
||||
}
|
||||
}
|
||||
|
||||
self.choices[index].as_str().render(max_width - 2, w)?;
|
||||
|
||||
queue!(w, ResetColor)
|
||||
}
|
||||
|
||||
fn is_selectable(&self, index: usize) -> bool {
|
||||
!self.choices[index].is_separator()
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.choices.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl List {
|
||||
pub fn ask<W: std::io::Write>(self, opts: Options, w: &mut W) -> error::Result<Answer> {
|
||||
let ans = ui::Input::new(ListPrompt {
|
||||
picker: widgets::ListPicker::new(self),
|
||||
opts,
|
||||
})
|
||||
.hide_cursor()
|
||||
.run(w)?;
|
||||
|
||||
writeln!(w, "{}", ans.name.as_str().dark_cyan())?;
|
||||
|
||||
Ok(Answer::ListItem(ans))
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Question {
|
||||
pub fn list(
|
||||
name: String,
|
||||
message: String,
|
||||
choices: Vec<super::Choice<String>>,
|
||||
default: usize,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
name,
|
||||
message,
|
||||
super::QuestionKind::List(List {
|
||||
choices: super::ChoiceList {
|
||||
choices,
|
||||
default,
|
||||
should_loop: true,
|
||||
// FIXME: this should be something sensible. page size is currently not used so
|
||||
// its fine for now
|
||||
page_size: 0,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
73
src/question/mod.rs
Normal file
73
src/question/mod.rs
Normal file
|
@ -0,0 +1,73 @@
|
|||
mod checkbox;
|
||||
mod choice_list;
|
||||
mod confirm;
|
||||
mod editor;
|
||||
mod expand;
|
||||
mod input;
|
||||
mod list;
|
||||
mod number;
|
||||
mod password;
|
||||
mod raw_list;
|
||||
|
||||
use crate::{error, Answer};
|
||||
pub use choice_list::Choice;
|
||||
use choice_list::{get_sep_str, ChoiceList};
|
||||
|
||||
use std::io::prelude::*;
|
||||
|
||||
pub struct Options {
|
||||
// FIXME: reference instead?
|
||||
name: String,
|
||||
// FIXME: reference instead? Dynamic messages?
|
||||
message: String,
|
||||
// FIXME: Wrong type
|
||||
when: bool,
|
||||
}
|
||||
|
||||
pub struct Question {
|
||||
kind: QuestionKind,
|
||||
opts: Options,
|
||||
}
|
||||
|
||||
impl Question {
|
||||
fn new(name: String, message: String, kind: QuestionKind) -> Self {
|
||||
Self {
|
||||
opts: Options {
|
||||
name,
|
||||
message,
|
||||
when: true,
|
||||
},
|
||||
kind,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum QuestionKind {
|
||||
Input(input::Input),
|
||||
Int(number::Int),
|
||||
Float(number::Float),
|
||||
Confirm(confirm::Confirm),
|
||||
List(list::List),
|
||||
Rawlist(raw_list::Rawlist),
|
||||
Expand(expand::Expand),
|
||||
Checkbox(checkbox::Checkbox),
|
||||
Password(password::Password),
|
||||
Editor(editor::Editor),
|
||||
}
|
||||
|
||||
impl Question {
|
||||
pub fn ask<W: Write>(self, w: &mut W) -> error::Result<Answer> {
|
||||
match self.kind {
|
||||
QuestionKind::Input(i) => i.ask(self.opts, w),
|
||||
QuestionKind::Int(i) => i.ask(self.opts, w),
|
||||
QuestionKind::Float(f) => f.ask(self.opts, w),
|
||||
QuestionKind::Confirm(c) => c.ask(self.opts, w),
|
||||
QuestionKind::List(l) => l.ask(self.opts, w),
|
||||
QuestionKind::Rawlist(r) => r.ask(self.opts, w),
|
||||
QuestionKind::Expand(e) => e.ask(self.opts, w),
|
||||
QuestionKind::Checkbox(c) => c.ask(self.opts, w),
|
||||
QuestionKind::Password(p) => p.ask(self.opts, w),
|
||||
QuestionKind::Editor(e) => e.ask(self.opts, w),
|
||||
}
|
||||
}
|
||||
}
|
170
src/question/number.rs
Normal file
170
src/question/number.rs
Normal file
|
@ -0,0 +1,170 @@
|
|||
use crossterm::style::{Color, ResetColor, SetForegroundColor};
|
||||
use ui::{widgets, Validation, Widget};
|
||||
|
||||
use crate::{error, Answer};
|
||||
|
||||
use super::Options;
|
||||
|
||||
pub struct Float {
|
||||
default: f64,
|
||||
}
|
||||
|
||||
pub struct Int {
|
||||
default: i64,
|
||||
}
|
||||
|
||||
trait Number {
|
||||
type Inner;
|
||||
|
||||
fn filter_map_char(c: char) -> Option<char>;
|
||||
fn parse(s: &str) -> Result<Self::Inner, String>;
|
||||
fn default(&self) -> Self::Inner;
|
||||
fn finish<W: std::io::Write>(inner: Self::Inner, w: &mut W) -> error::Result<Answer>;
|
||||
}
|
||||
|
||||
impl Number for Int {
|
||||
type Inner = i64;
|
||||
|
||||
fn filter_map_char(c: char) -> Option<char> {
|
||||
if c.is_digit(10) || c == '-' || c == '+' {
|
||||
Some(c)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Result<i64, String> {
|
||||
s.parse::<i64>().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn default(&self) -> Self::Inner {
|
||||
self.default
|
||||
}
|
||||
|
||||
fn finish<W: std::io::Write>(i: Self::Inner, w: &mut W) -> error::Result<Answer> {
|
||||
writeln!(
|
||||
w,
|
||||
"{}{}{}",
|
||||
SetForegroundColor(Color::DarkCyan),
|
||||
i,
|
||||
ResetColor,
|
||||
)?;
|
||||
|
||||
Ok(Answer::Int(i))
|
||||
}
|
||||
}
|
||||
impl Number for Float {
|
||||
type Inner = f64;
|
||||
|
||||
fn filter_map_char(c: char) -> Option<char> {
|
||||
if c.is_digit(10) || c == '.' || c == 'e' || c == 'E' || c == '+' || c == '-' {
|
||||
Some(c)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Result<f64, String> {
|
||||
s.parse::<f64>().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn default(&self) -> Self::Inner {
|
||||
self.default
|
||||
}
|
||||
|
||||
fn finish<W: std::io::Write>(f: Self::Inner, w: &mut W) -> error::Result<Answer> {
|
||||
write!(w, "{}", SetForegroundColor(Color::DarkCyan))?;
|
||||
if f > 1e20 {
|
||||
write!(w, "{:e}", f)?;
|
||||
} else {
|
||||
write!(w, "{}", f)?;
|
||||
}
|
||||
writeln!(w, "{}", ResetColor)?;
|
||||
|
||||
Ok(Answer::Float(f))
|
||||
}
|
||||
}
|
||||
|
||||
struct NumberPrompt<N> {
|
||||
number: N,
|
||||
opts: Options,
|
||||
input: widgets::StringInput,
|
||||
hint: String,
|
||||
}
|
||||
|
||||
impl<N: Number> Widget for NumberPrompt<N> {
|
||||
fn render<W: std::io::Write>(&mut self, max_width: usize, w: &mut W) -> crossterm::Result<()> {
|
||||
self.input.render(max_width, w)
|
||||
}
|
||||
|
||||
fn height(&self) -> usize {
|
||||
self.input.height()
|
||||
}
|
||||
|
||||
fn handle_key(&mut self, key: crossterm::event::KeyEvent) -> bool {
|
||||
self.input.handle_key(key)
|
||||
}
|
||||
|
||||
fn cursor_pos(&self, prompt_len: u16) -> (u16, u16) {
|
||||
self.input.cursor_pos(prompt_len)
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: Number> ui::Prompt for NumberPrompt<N> {
|
||||
type ValidateErr = String;
|
||||
type Output = N::Inner;
|
||||
|
||||
fn prompt(&self) -> &str {
|
||||
&self.opts.message
|
||||
}
|
||||
|
||||
fn hint(&self) -> Option<&str> {
|
||||
Some(&self.hint)
|
||||
}
|
||||
|
||||
fn validate(&mut self) -> Result<Validation, Self::ValidateErr> {
|
||||
N::parse(self.input.value()).map(|_| Validation::Finish)
|
||||
}
|
||||
|
||||
fn finish(self) -> Self::Output {
|
||||
N::parse(self.input.value()).unwrap_or_else(|_| self.number.default())
|
||||
}
|
||||
fn finish_default(self) -> Self::Output {
|
||||
self.number.default()
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_ask {
|
||||
($t:ty) => {
|
||||
impl $t {
|
||||
pub(crate) fn ask<W: std::io::Write>(
|
||||
self,
|
||||
opts: super::Options,
|
||||
w: &mut W,
|
||||
) -> error::Result<Answer> {
|
||||
let ans = ui::Input::new(NumberPrompt {
|
||||
hint: format!("({})", self.default),
|
||||
input: widgets::StringInput::new(Self::filter_map_char),
|
||||
number: self,
|
||||
opts,
|
||||
})
|
||||
.run(w)?;
|
||||
|
||||
Self::finish(ans, w)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_ask!(Int);
|
||||
impl_ask!(Float);
|
||||
|
||||
impl super::Question {
|
||||
pub fn int(name: String, message: String, default: i64) -> Self {
|
||||
Self::new(name, message, super::QuestionKind::Int(Int { default }))
|
||||
}
|
||||
|
||||
pub fn float(name: String, message: String, default: f64) -> Self {
|
||||
Self::new(name, message, super::QuestionKind::Float(Float { default }))
|
||||
}
|
||||
}
|
98
src/question/password.rs
Normal file
98
src/question/password.rs
Normal file
|
@ -0,0 +1,98 @@
|
|||
use crossterm::style::Colorize;
|
||||
use ui::{widgets, Widget};
|
||||
|
||||
use crate::{error, Answer};
|
||||
|
||||
use super::Options;
|
||||
|
||||
pub struct Password {
|
||||
pub(crate) mask: Option<char>,
|
||||
}
|
||||
|
||||
struct PasswordPrompt {
|
||||
password: Password,
|
||||
input: widgets::StringInput,
|
||||
opts: Options,
|
||||
}
|
||||
|
||||
impl ui::Prompt for PasswordPrompt {
|
||||
type ValidateErr = &'static str;
|
||||
type Output = String;
|
||||
|
||||
fn prompt(&self) -> &str {
|
||||
&self.opts.message
|
||||
}
|
||||
|
||||
fn hint(&self) -> Option<&str> {
|
||||
if self.password.mask.is_none() {
|
||||
Some("[input is hidden]")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(self) -> Self::Output {
|
||||
self.input.finish().unwrap_or_else(String::new)
|
||||
}
|
||||
|
||||
fn has_default(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn finish_default(self) -> Self::Output {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
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 height(&self) -> usize {
|
||||
self.input.height()
|
||||
}
|
||||
|
||||
fn handle_key(&mut self, key: crossterm::event::KeyEvent) -> bool {
|
||||
self.input.handle_key(key)
|
||||
}
|
||||
|
||||
fn cursor_pos(&self, prompt_len: u16) -> (u16, u16) {
|
||||
self.input.cursor_pos(prompt_len)
|
||||
}
|
||||
}
|
||||
|
||||
impl Password {
|
||||
pub fn ask<W: std::io::Write>(self, opts: Options, w: &mut W) -> error::Result<Answer> {
|
||||
let ans = ui::Input::new(PasswordPrompt {
|
||||
input: widgets::StringInput::new(widgets::no_filter as _).password(self.mask),
|
||||
password: self,
|
||||
opts,
|
||||
})
|
||||
.run(w)?;
|
||||
|
||||
writeln!(w, "{}", "[hidden]".dark_grey())?;
|
||||
|
||||
Ok(Answer::String(ans))
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Question {
|
||||
pub fn password(name: String, message: String) -> Self {
|
||||
Self::new(
|
||||
name,
|
||||
message,
|
||||
super::QuestionKind::Password(Password { mask: None }),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn with_mask(mut self, mask: char) -> Self {
|
||||
if let super::QuestionKind::Password(ref mut p) = self.kind {
|
||||
p.mask = Some(mask);
|
||||
} else {
|
||||
unreachable!("with_mask should only be called when a question is password")
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
}
|
211
src/question/raw_list.rs
Normal file
211
src/question/raw_list.rs
Normal file
|
@ -0,0 +1,211 @@
|
|||
use crossterm::{
|
||||
event, queue,
|
||||
style::{Color, Colorize, ResetColor, SetForegroundColor},
|
||||
terminal,
|
||||
};
|
||||
use ui::{widgets, Validation, Widget};
|
||||
use widgets::List;
|
||||
|
||||
use crate::{
|
||||
answer::{Answer, ListItem},
|
||||
error,
|
||||
};
|
||||
|
||||
use super::{Choice, Options};
|
||||
|
||||
pub struct Rawlist {
|
||||
// FIXME: Whats the correct type?
|
||||
choices: super::ChoiceList<(usize, String)>,
|
||||
}
|
||||
|
||||
struct RawlistPrompt {
|
||||
list: widgets::ListPicker<Rawlist>,
|
||||
input: widgets::StringInput,
|
||||
opts: Options,
|
||||
}
|
||||
|
||||
impl RawlistPrompt {
|
||||
fn finish_index(self, index: usize) -> ListItem {
|
||||
ListItem {
|
||||
index,
|
||||
name: self
|
||||
.list
|
||||
.finish()
|
||||
.choices
|
||||
.choices
|
||||
.swap_remove(index)
|
||||
.unwrap_choice()
|
||||
.1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ui::Prompt for RawlistPrompt {
|
||||
type ValidateErr = &'static str;
|
||||
type Output = ListItem;
|
||||
|
||||
fn prompt(&self) -> &str {
|
||||
&self.opts.message
|
||||
}
|
||||
|
||||
fn hint(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn validate(&mut self) -> Result<Validation, Self::ValidateErr> {
|
||||
if self.list.get_at() >= self.list.list.len() {
|
||||
Err("Please enter a valid index")
|
||||
} else {
|
||||
Ok(Validation::Finish)
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(self) -> Self::Output {
|
||||
let index = self.list.get_at();
|
||||
self.finish_index(index)
|
||||
}
|
||||
|
||||
fn finish_default(self) -> Self::Output {
|
||||
let index = self.list.list.choices.default;
|
||||
self.finish_index(index)
|
||||
}
|
||||
}
|
||||
|
||||
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 height(&self) -> usize {
|
||||
self.list.height() + 1
|
||||
}
|
||||
|
||||
fn handle_key(&mut self, key: event::KeyEvent) -> bool {
|
||||
if self.input.handle_key(key) {
|
||||
if let Ok(mut 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));
|
||||
|
||||
if let Some(pos) = pos {
|
||||
self.list.set_at(pos + n);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.list.set_at(self.list.list.len() + 1);
|
||||
true
|
||||
} else if self.list.handle_key(key) {
|
||||
self.input.set_value(self.list.get_at().to_string());
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn cursor_pos(&self, _: u16) -> (u16, u16) {
|
||||
let w = self.input.cursor_pos(ANSWER_PROMPT.len() as u16).0;
|
||||
(w, self.height() as u16)
|
||||
}
|
||||
}
|
||||
|
||||
impl widgets::List for Rawlist {
|
||||
fn render_item<W: std::io::Write>(
|
||||
&mut self,
|
||||
index: usize,
|
||||
hovered: bool,
|
||||
max_width: usize,
|
||||
w: &mut W,
|
||||
) -> crossterm::Result<()> {
|
||||
match &self.choices[index] {
|
||||
Choice::Choice((index, name)) => {
|
||||
if hovered {
|
||||
queue!(w, SetForegroundColor(Color::DarkCyan))?;
|
||||
}
|
||||
|
||||
write!(w, " {}) ", index + 1)?;
|
||||
name.as_str()
|
||||
.render(max_width - (*index as f64).log10() as usize + 5, w)?;
|
||||
|
||||
if hovered {
|
||||
queue!(w, ResetColor)?;
|
||||
}
|
||||
}
|
||||
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)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_selectable(&self, index: usize) -> bool {
|
||||
!self.choices[index].is_separator()
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.choices.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl Rawlist {
|
||||
pub fn ask<W: std::io::Write>(self, opts: Options, w: &mut W) -> error::Result<Answer> {
|
||||
let ans = ui::Input::new(RawlistPrompt {
|
||||
input: widgets::StringInput::new(|c| c.is_digit(10).then(|| c)),
|
||||
list: widgets::ListPicker::new(self),
|
||||
opts,
|
||||
})
|
||||
.run(w)?;
|
||||
|
||||
writeln!(w, "{}", ans.name.as_str().dark_cyan())?;
|
||||
|
||||
Ok(Answer::ListItem(ans))
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Question {
|
||||
pub fn raw_list(
|
||||
name: String,
|
||||
message: String,
|
||||
choices: Vec<Choice<String>>,
|
||||
default: usize,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
name,
|
||||
message,
|
||||
super::QuestionKind::Rawlist(Rawlist {
|
||||
choices: super::ChoiceList {
|
||||
choices: choices
|
||||
.into_iter()
|
||||
.scan(0, |index, choice| match choice {
|
||||
Choice::Choice(s) => {
|
||||
let res = Choice::Choice((*index, s));
|
||||
*index += 1;
|
||||
Some(res)
|
||||
}
|
||||
Choice::Separator(s) => Some(Choice::Separator(s)),
|
||||
})
|
||||
.collect(),
|
||||
default,
|
||||
should_loop: true,
|
||||
// FIXME: this should be something sensible. page size is currently not used so
|
||||
// its fine for now
|
||||
page_size: 0,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user