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]
|
[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)]
|
// FIXME: remove this
|
||||||
mod tests {
|
#![allow(dead_code)]
|
||||||
#[test]
|
|
||||||
fn it_works() {
|
mod answer;
|
||||||
assert_eq!(2 + 2, 4);
|
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