use std::fmt::Debug;
use std::io::{self, Write};
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use crossterm::cursor::{MoveTo, MoveToNextLine, RestorePosition, SavePosition};
use crossterm::event::{read, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use crossterm::style::{Color, Print, ResetColor, SetBackgroundColor, SetForegroundColor};
use crossterm::terminal::{Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen};
use crossterm::{cursor, event, execute, queue, terminal};
use std::future::Future;
use std::pin::Pin;
use std::env;
pub fn handle_args<T: Clone>(choices: Vec<Choice<T>>, multi: bool) -> Option<Vec<T>> {
let args: Vec<String> = env::args().collect();
if args.len() > 1 {
let mut selected: Vec<T> = Vec::new();
for arg in &args[1..] {
let mut found = false;
for choice in &choices {
if choice.name == *arg {
selected.push(choice.run().unwrap());
found = true;
}
}
if !found {
eprintln!("Invalid argument: {}", arg);
print_usage(&choices);
return None;
}
}
Some(selected)
} else {
if multi {
match crate::selection_multi!("Select one or more options", &choices) {
Some(selected_choices) => {
let results = selected_choices
.iter()
.map(|choice| choice.run().unwrap())
.collect();
Some(results)
}
None => None,
}
} else {
match crate::selection!("Select an option", &choices) {
Some(choice) => Some(vec![choice.run().unwrap()]),
None => None,
}
}
}
}
fn print_usage(choices: &[Choice<impl Clone>]) {
println!("Usage: program_name [OPTIONS]");
println!("Available options:");
for choice in choices {
println!(" {} - {}", choice.name, choice.description);
}
}
struct ScopeGuard<F: FnOnce()> {
f: Option<F>,
}
impl<F: FnOnce()> ScopeGuard<F> {
fn new(f: F) -> Self {
ScopeGuard { f: Some(f) }
}
}
impl<F: FnOnce()> Drop for ScopeGuard<F> {
fn drop(&mut self) {
if let Some(f) = self.f.take() {
f();
}
}
}
fn guard<F: FnOnce()>(f: F) -> ScopeGuard<F> {
ScopeGuard::new(f)
}
pub trait Pickable {
fn get_title(&self) -> String;
fn get_description(&self) -> String;
}
pub type Closure<T> = Arc<dyn Fn() -> Result<T, Box<dyn std::error::Error>> + Send + Sync>;
#[derive(Clone)]
pub struct Choice<T> {
pub name: String,
pub description: String,
action: Closure<T>,
}
impl<T> Pickable for Choice<T> {
fn get_title(&self) -> String {
self.name.clone()
}
fn get_description(&self) -> String {
self.description.clone()
}
}
impl<T> Choice<T> {
pub fn new(
name: impl Into<String>,
description: impl Into<String>,
action: Closure<T>,
) -> Self {
Choice {
name: name.into(),
description: description.into(),
action,
}
}
pub fn run(&self) -> Result<T, Box<dyn std::error::Error>> {
(self.action)()
}
}
pub struct InputBuilder<T> {
question: String,
default: Option<T>,
validation: Option<Arc<dyn Fn(&str) -> Result<T, String> + Send + Sync>>,
}
impl<T: FromStr + ToString> InputBuilder<T>
where
<T as FromStr>::Err: std::fmt::Debug,
{
pub fn new(question: impl Into<String>) -> Self {
InputBuilder {
question: question.into(),
default: None,
validation: None,
}
}
pub fn with_default(mut self, default: T) -> Self {
self.default = Some(default);
self
}
pub fn with_validation<F>(mut self, validation: F) -> Self
where
F: Fn(&str) -> Result<T, String> + Send + Sync + 'static,
{
self.validation = Some(Arc::new(validation));
self
}
pub fn prompt(self) -> std::io::Result<T> {
let mut question = self.question;
if let Some(ref default) = self.default {
question = format!(
"{}{}{} {}",
question,
RESET_COLOR,
gray_dim.to_string(),
default.to_string()
);
}
loop {
let input = text(question.clone())?;
if input.is_empty() && self.default.is_some() {
return Ok(self.default.unwrap());
}
if let Some(ref validation) = self.validation {
match validation(&input) {
Ok(value) => return Ok(value),
Err(err) => {
showln!(red, "{}", err);
continue;
}
}
}
match input.parse() {
Ok(value) => return Ok(value),
Err(_) => {
showln!(red, "Invalid input. Please try again.");
}
}
}
}
}
pub struct NumberInputBuilder<T> {
question: String,
default: Option<T>,
validation: Option<Arc<dyn Fn(T) -> Result<T, String> + Send + Sync>>,
}
impl<T> NumberInputBuilder<T>
where
T: FromStr + ToString + Clone,
<T as FromStr>::Err: std::fmt::Display,
{
pub fn new(question: impl Into<String>) -> Self {
NumberInputBuilder {
question: question.into(),
default: None,
validation: None,
}
}
pub fn with_default(mut self, default: T) -> Self {
self.default = Some(default);
self
}
pub fn with_validation<F>(mut self, validation: F) -> Self
where
F: Fn(T) -> Result<T, String> + Send + Sync + 'static,
{
self.validation = Some(Arc::new(validation));
self
}
pub fn prompt(self) -> std::io::Result<T> {
let mut question = self.question;
if let Some(ref default) = self.default {
question = format!(
"{}{}{} {}",
question,
RESET_COLOR,
gray_dim.to_string(),
default.to_string()
);
}
loop {
let input = text(question.clone())?;
if input.is_empty() && self.default.is_some() {
return Ok(self.default.unwrap());
}
match input.parse::<T>() {
Ok(value) => {
if let Some(ref validation) = self.validation {
match validation(value.clone()) {
Ok(validated_value) => return Ok(validated_value),
Err(err) => {
showln!(red, "{}", err);
continue;
}
}
} else {
return Ok(value);
}
}
Err(e) => {
showln!(red, "Invalid input: {}. Please try again.", e);
}
}
}
}
}
pub struct ConfirmationBuilder {
question: String,
default: Option<bool>,
}
impl ConfirmationBuilder {
pub fn new(question: impl Into<String>) -> Self {
ConfirmationBuilder {
question: question.into(),
default: None,
}
}
pub fn with_default(mut self, default: bool) -> Self {
self.default = Some(default);
self
}
pub fn prompt(self) -> std::io::Result<bool> {
let mut question = self.question;
match self.default {
Some(true) => {
question = format!(
"{}{}{} {}",
question,
RESET_COLOR,
gray_dim.to_string(),
"yes".to_string()
);
}
Some(false) => {
question = format!(
"{}{}{} {}",
question,
RESET_COLOR,
gray_dim.to_string(),
"no".to_string()
);
}
None => {
question = format!(
"{}{}{} {}",
question,
RESET_COLOR,
gray_dim.to_string(),
"yes/no".to_string()
);
}
}
loop {
let input = text(question.clone())?.to_lowercase();
match input.as_str() {
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
"" if self.default.is_some() => return Ok(self.default.unwrap()),
_ => {
showln!(red, "Please answer with 'y' or 'n'.");
}
}
}
}
}
pub fn text_input(question: impl Into<String>) -> InputBuilder<String> {
InputBuilder::new(question)
}
pub fn number_input<T>(question: impl Into<String>) -> NumberInputBuilder<T>
where
T: FromStr + ToString + Clone,
<T as FromStr>::Err: std::fmt::Display,
{
NumberInputBuilder::new(question)
}
pub fn confirmation(question: impl Into<String>) -> ConfirmationBuilder {
ConfirmationBuilder::new(question)
}
pub fn text(question: impl Into<String>) -> std::io::Result<String> {
let question = question.into();
let mut answer = String::new();
println!();
terminal::enable_raw_mode()?;
let _guard = guard(|| {
let _ = terminal::disable_raw_mode();
});
showln!(yellow, "â•─ ", cyan_bold, question, yellow_bold, " ─");
show!(yellow, "╰→ ");
io::stdout().flush()?;
loop {
match event::read()? {
Event::Key(KeyEvent { code, kind, .. }) => match kind {
KeyEventKind::Press => match code {
KeyCode::Enter => {
println!();
break;
}
KeyCode::Backspace => {
if !answer.is_empty() {
answer.pop();
execute!(
io::stdout(),
cursor::MoveLeft(1),
terminal::Clear(ClearType::UntilNewLine),
)?;
}
}
KeyCode::Char(c) => {
answer.push(c);
show!(white, c);
io::stdout().flush()?;
}
KeyCode::Esc => {
std::process::exit(0);
}
_ => {}
},
_ => {}
},
_ => {}
}
}
Ok(answer)
}
#[macro_export]
macro_rules! text {
($question:expr) => {
$crate::text_input($question).prompt().unwrap()
};
($question:expr, $default:expr) => {
$crate::text_input($question).with_default($default).prompt().unwrap()
};
($question:expr, $default:expr, $validation:expr) => {
$crate::text_input($question)
.with_default($default)
.with_validation($validation)
.prompt()
.unwrap()
};
}
#[macro_export]
macro_rules! number {
($question:expr) => {
$crate::number_input($question).prompt().unwrap()
};
($question:expr, $default:expr) => {
$crate::number_input($question).with_default($default).prompt().unwrap()
};
($question:expr, $default:expr, $validation:expr) => {
$crate::number_input($question)
.with_default($default)
.with_validation($validation)
.prompt()
.unwrap()
};
}
#[macro_export]
macro_rules! confirm {
($question:expr) => {
$crate::confirmation($question).prompt().unwrap()
};
($question:expr, $default:expr) => {
$crate::confirmation($question).with_default($default).prompt().unwrap()
};
}
use crate::{yellow, yellow_bold, white, white_bold, cyan_bold, gray_dim, yellowbg, show, showln};
struct MenuState {
filter: String,
selected_index: usize,
scroll_offset: usize,
}
pub fn menu<T: Clone>(message: impl Into<String>, options: &Vec<Choice<T>>) -> Option<Choice<T>> {
let message = message.into();
let mut filter = String::new();
let mut selected_index = 0;
for _ in 0..options.len() + 2 {
println!();
}
let (_, y) = cursor::position().unwrap();
let corrected_y = y - options.len() as u16 - 2;
terminal::enable_raw_mode().unwrap();
let _clean_up = guard( || {
let _ = terminal::disable_raw_mode();
});
let _ = event::read();
loop {
execute!(
io::stdout(),
cursor::MoveTo(0, corrected_y),
terminal::Clear(ClearType::FromCursorDown)
)
.unwrap();
showln!(yellow_bold, "â•─ ", cyan_bold, &message, yellow_bold, " ─");
let filtered_options: Vec<&Choice<T>> = options
.iter()
.filter(|o| {
let lower_filter = filter.to_lowercase();
o.get_title().to_lowercase().contains(&lower_filter)
|| o.get_description().to_lowercase().contains(&lower_filter)
})
.collect();
for (index, option) in filtered_options.iter().enumerate() {
if index == selected_index {
showln!(
yellow_bold,
"│",
yellowbg,
format!(" {} ", option.get_title()),
white,
" ",
yellow_bold,
option.get_description()
);
} else {
showln!(
yellow_bold,
"│",
white,
format!(" {} ", option.get_title()),
white,
" ",
gray_dim,
option.get_description()
);
}
}
show!(yellow_bold, "╰─→ ", white_bold, &filter);
io::stdout().flush().unwrap();
match event::read().unwrap() {
Event::Key(KeyEvent {
code,
kind: KeyEventKind::Press,
..
}) => match code {
KeyCode::Up => {
if selected_index > 0 {
selected_index -= 1;
}
}
KeyCode::Down => {
if selected_index < filtered_options.len().saturating_sub(1) {
selected_index += 1;
}
}
KeyCode::Enter => {
let selected = filtered_options[selected_index].clone();
execute!(
io::stdout(),
cursor::MoveTo(0, corrected_y + 1),
terminal::Clear(ClearType::FromCursorDown)
)
.unwrap();
showln!(yellow_bold, "╰→ ", white_bold, selected.get_title());
return Some(selected);
}
KeyCode::Char(c) => {
filter.push(c);
selected_index = 0;
}
KeyCode::Backspace => {
filter.pop();
selected_index = 0;
}
KeyCode::Esc => {
return None;
}
_ => {}
},
_ => {}
}
}
}
pub fn multi_select_menu<T: Clone>(
message: impl Into<String>,
options: &Vec<Choice<T>>,
) -> Option<Vec<Choice<T>>> {
let message = message.into();
let mut filter = String::new();
let mut selected_indices = Vec::new();
let mut cursor_index = 0;
for _ in 0..options.len() + 4 {
println!();
}
let (_, y) = cursor::position().unwrap();
let corrected_y = y - (options.len() + 4) as u16;
terminal::enable_raw_mode().unwrap();
let _clean_up = guard(|| {
let _ = terminal::disable_raw_mode();
});
let _ = event::read();
loop {
execute!(
io::stdout(),
cursor::MoveTo(0, corrected_y),
terminal::Clear(ClearType::FromCursorDown)
)
.unwrap();
showln!(yellow_bold, "â•─ ", cyan_bold, &message, yellow_bold, " ─");
showln!(
yellow_bold,
"│ ",
gray_dim,
"use ",
yellow_bold,
"↑/↓ ",
gray_dim,
"to navigate,",
yellow_bold,
"space ",
gray_dim,
"to select & ",
yellow_bold,
"enter ",
gray_dim,
"to confirm, "
);
let filtered_options: Vec<&Choice<T>> = options
.iter()
.filter(|o| {
let lower_filter = filter.to_lowercase();
o.get_title().to_lowercase().contains(&lower_filter)
|| o.get_description().to_lowercase().contains(&lower_filter)
})
.collect();
for (index, option) in filtered_options.iter().enumerate() {
let is_selected = selected_indices.contains(&index);
if index == cursor_index {
if is_selected {
showln!(
yellow_bold,
"│",
yellowbg,
" â– ",
yellowbg,
format!("{} ", option.get_title()),
white,
" ",
yellow_bold,
option.get_description()
);
} else {
showln!(
yellow_bold,
"│",
yellowbg,
" â—» ",
yellowbg,
format!("{} ", option.get_title()),
white,
" ",
yellow_bold,
option.get_description()
);
}
} else {
if is_selected {
showln!(
yellow_bold,
"│",
yellow_bold,
" â– ",
yellow_bold,
format!("{} ", option.get_title()),
white,
" ",
gray_dim,
option.get_description()
);
} else {
showln!(
yellow_bold,
"│",
white,
" â—» ",
white,
format!("{} ", option.get_title()),
white,
" ",
gray_dim,
option.get_description()
);
}
}
}
show!(yellow_bold, "╰─→ ", white_bold, &filter);
io::stdout().flush().unwrap();
match event::read().unwrap() {
Event::Key(KeyEvent {
code,
kind: KeyEventKind::Press,
..
}) => match code {
KeyCode::Up => {
if cursor_index > 0 {
cursor_index -= 1;
}
}
KeyCode::Down => {
if cursor_index < filtered_options.len().saturating_sub(1) {
cursor_index += 1;
}
}
KeyCode::Char(' ') => {
if selected_indices.contains(&cursor_index) {
selected_indices.retain(|&x| x != cursor_index);
} else {
selected_indices.push(cursor_index);
}
}
KeyCode::Enter => {
let mut selected_choices = Vec::new();
for &i in &selected_indices {
if let Some(choice) = filtered_options.get(i) {
selected_choices.push((*choice).clone());
}
}
execute!(
io::stdout(),
cursor::MoveTo(0, corrected_y + 2 + options.len() as u16),
terminal::Clear(ClearType::FromCursorDown)
)
.unwrap();
for choice in &selected_choices {
showln!(yellow_bold, "╰→ ", white_bold, choice.get_title());
}
return Some(selected_choices);
}
KeyCode::Char(c) => {
filter.push(c);
cursor_index = 0;
}
KeyCode::Backspace => {
filter.pop();
cursor_index = 0;
}
KeyCode::Esc => {
return None;
}
_ => {}
},
_ => {}
}
}
}
#[macro_export]
macro_rules! choice {
($name:expr, $description:expr, $action:expr) => {
$crate::Choice::new($name, $description, std::sync::Arc::new($action))
};
($name:expr, $action:expr) => {
$crate::Choice::new($name, "", std::sync::Arc::new($action))
};
}
#[macro_export]
macro_rules! selection {
($message:expr, $choices:expr) => {
$crate::menu($message, $choices)
};
($choices:expr) => {
$crate::menu("", $choices)
};
}
#[macro_export]
macro_rules! selection_multi {
($message:expr, $choices:expr) => {
$crate::multi_select_menu($message, $choices)
};
($choices:expr) => {
$crate::multi_select_menu("select multiple options", $choices)
};
}
#[cfg(feature = "async")]
pub mod async_impl;
#[cfg(feature = "async")]
pub use async_impl::*;
use crate::{DateTime, Dot, RESET_COLOR};
use super::Location;
pub mod location {
use crate::COMMON_LOCATIONS;
use super::*;
pub struct LocationInputBuilder {
question: String,
default: Option<Location>,
}
impl LocationInputBuilder {
pub fn new(question: impl Into<String>) -> Self {
LocationInputBuilder {
question: question.into(),
default: None,
}
}
pub fn with_default(mut self, default: Location) -> Self {
self.default = Some(default);
self
}
pub fn prompt(self) -> Result<Location, Box<dyn std::error::Error>> {
let choices = COMMON_LOCATIONS
.iter()
.map(|l| {
choice!(
l.name.clone(),
format!("{:.2} {:.2}", l.latitude(), l.longitude()),
|| Ok(l.clone())
)
})
.collect::<Vec<Choice<Location>>>();
let selection = multi_select_menu(self.question, &choices);
match selection {
Some(selected_choices) => {
if selected_choices.is_empty() {
Err("No location selected".into())
} else {
let res = selected_choices[0].run();
match res {
Ok(location) => Ok(location),
Err(e) => Err(e),
}
}
}
None => Err("No location selected".into()),
}
}
}
#[macro_use]
#[macro_export]
macro_rules! ask_location {
($question:expr) => {
$crate::console::ask::location::LocationInputBuilder::new($question)
.prompt()
.unwrap()
};
($question:expr, $default:expr) => {
$crate::console::ask::location::LocationInputBuilder::new($question)
.with_default($default)
.prompt()
.unwrap()
};
}
}
pub mod datetime {
use crate::{Date, DateLike, DateTime, Dot, Time};
use super::*;
use std::io::{self, Write};
use crossterm::{
event::{self, Event, KeyCode, KeyEvent, KeyEventKind},
terminal::{self, ClearType},
cursor, execute,
};
use crate::{
gray_dim, yellow, yellow_bold, white, white_bold, cyan_bold, RESET_COLOR, show, showln,
};
pub struct DateInputBuilder {
question: String,
default: Option<Date>,
}
impl DateInputBuilder {
pub fn new(question: impl Into<String>) -> Self {
DateInputBuilder {
question: question.into(),
default: None,
}
}
pub fn with_default(mut self, default: Date) -> Self {
self.default = Some(default);
self
}
pub fn prompt(self) -> io::Result<Date> {
let mut date = self.default.unwrap_or_else(|| Date::now());
let mut cursor_position = 0; let (_, initial_y) = cursor::position().unwrap();
terminal::enable_raw_mode()?;
println!();
showln!(yellow, "â•─ ", cyan_bold, &self.question, yellow_bold, " ─");
loop {
execute!(
io::stdout(),
cursor::MoveToColumn(0),
terminal::Clear(ClearType::CurrentLine)
)?;
show!(yellow, "╰→ ");
let date_str = format!("{:04}-{:02}-{:02}", date.year(), date.month(), date.date());
let (start, end) = match cursor_position {
0 => (0, 4), 1 => (5, 7), 2 => (8, 10), _ => unreachable!(),
};
for (i, c) in date_str.chars().enumerate() {
if i >= start && i < end {
show!(yellowbg, c);
} else {
show!(white, c);
}
}
io::stdout().flush()?;
match event::read()? {
Event::Key(KeyEvent { code, kind, .. }) => {
if kind == KeyEventKind::Press {
match code {
KeyCode::Left => {
cursor_position = (cursor_position + 2) % 3;
}
KeyCode::Right => {
cursor_position = (cursor_position + 1) % 3;
}
KeyCode::Up => {
date = match cursor_position {
0 => date.add_years(1),
1 => date.add_months(1),
2 => date.add_days(1),
_ => unreachable!(),
};
}
KeyCode::Down => {
date = match cursor_position {
0 => date.sub_years(1),
1 => date.sub_months(1),
2 => date.sub_days(1),
_ => unreachable!(),
};
}
KeyCode::Enter => {
execute!(
io::stdout(),
cursor::MoveTo(0, initial_y + 2),
terminal::Clear(ClearType::FromCursorDown)
)?;
showln!(
yellow,
"╰→ ",
white_bold,
format!("{:04}-{:02}-{:02}", date.year(), date.month(), date.date())
);
return Ok(date);
}
KeyCode::Esc => {
println!();
terminal::disable_raw_mode()?;
return Err(io::Error::new(
io::ErrorKind::Interrupted,
"User cancelled input",
));
}
_ => {}
}
}
}
_ => {}
}
}
}
}
pub fn date_input(question: impl Into<String>) -> DateInputBuilder {
DateInputBuilder::new(question)
}
#[macro_export]
macro_rules! ask_date {
($question:expr) => {
$crate::console::ask::datetime::DateInputBuilder::new($question)
.prompt()
.unwrap()
};
($question:expr, $default:expr) => {
$crate::console::ask::datetime::DateInputBuilder::new($question)
.with_default($default)
.prompt()
.unwrap()
};
}
pub struct DateTimeInputBuilder {
question: String,
default: Option<DateTime>,
}
impl DateTimeInputBuilder {
pub fn new(question: impl Into<String>) -> Self {
DateTimeInputBuilder {
question: question.into(),
default: None,
}
}
pub fn with_default(mut self, default: DateTime) -> Self {
self.default = Some(default);
self
}
pub fn prompt(self) -> io::Result<DateTime> {
let mut date = self.default.unwrap_or_else(|| DateTime::now());
let mut cursor_position = 0; let (_, initial_y) = cursor::position().unwrap();
terminal::enable_raw_mode()?;
println!();
showln!(yellow, "â•─ ", cyan_bold, &self.question, yellow_bold, " ─");
loop {
execute!(
io::stdout(),
cursor::MoveToColumn(0),
terminal::Clear(ClearType::CurrentLine)
)?;
show!(yellow, "╰→ ");
let date_str = format!(
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
date.year(),
date.month(),
date.day(),
date.hour(),
date.minute(),
date.second()
);
let (start, end) = match cursor_position {
0 => (0, 4), 1 => (5, 7), 2 => (8, 10), 3 => (11, 13), 4 => (14, 16), 5 => (17, 19), _ => unreachable!(),
};
for (i, c) in date_str.chars().enumerate() {
if i >= start && i < end {
show!(yellowbg, c);
} else {
show!(white, c);
}
}
io::stdout().flush()?;
match event::read()? {
Event::Key(KeyEvent { code, kind, .. }) => {
if kind == KeyEventKind::Press {
match code {
KeyCode::Left => {
cursor_position = (cursor_position + 5) % 6;
}
KeyCode::Right => {
cursor_position = (cursor_position + 1) % 6;
}
KeyCode::Up => {
date = match cursor_position {
0 => date.add_years(1),
1 => date.add_months(1),
2 => date.add_days(1),
3 => date.add_hours(1),
4 => date.add_minutes(1),
5 => date.add_seconds(1),
_ => unreachable!(),
};
}
KeyCode::Down => {
date = match cursor_position {
0 => date.sub_years(1),
1 => date.sub_months(1),
2 => date.sub_days(1),
3 => date.sub_hours(1),
4 => date.sub_minutes(1),
5 => date.sub_seconds(1),
_ => unreachable!(),
};
}
KeyCode::Esc => {
println!();
terminal::disable_raw_mode()?;
return Err(io::Error::new(
io::ErrorKind::Interrupted,
"User cancelled input",
));
}
KeyCode::Enter => {
execute!(
io::stdout(),
cursor::MoveTo(0, initial_y + 2),
terminal::Clear(ClearType::FromCursorDown)
)?;
showln!(
yellow,
"╰→ ",
white_bold,
format!(
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
date.year(),
date.month(),
date.day(),
date.hour(),
date.minute(),
date.second()
)
);
return Ok(date);
}
_ => {}
}
}
}
_ => {}
}
}
}
}
pub fn datetime_input(question: impl Into<String>) -> DateTimeInputBuilder {
DateTimeInputBuilder::new(question)
}
#[macro_export]
macro_rules! ask_datetime {
($question:expr) => {
$crate::console::ask::datetime::DateTimeInputBuilder::new($question)
.prompt()
.unwrap()
};
($question:expr, $default:expr) => {
$crate::console::ask::datetime::DateTimeInputBuilder::new($question)
.with_default($default)
.prompt()
.unwrap()
};
}
pub struct DateTimeLocationInputBuilder {
question: String,
default_date: Option<DateTime>,
default_location: Option<Location>,
}
impl DateTimeLocationInputBuilder {
pub fn new(question: impl Into<String>) -> Self {
DateTimeLocationInputBuilder {
question: question.into(),
default_date: None,
default_location: None,
}
}
pub fn with_default_date(mut self, default: DateTime) -> Self {
self.default_date = Some(default);
self
}
pub fn with_default_location(mut self, default: Location) -> Self {
self.default_location = Some(default);
self
}
pub fn prompt(self) -> io::Result<(DateTime, Location)> {
let date = if let Some(default) = self.default_date {
DateTimeInputBuilder::new(self.question.clone())
.with_default(default)
.prompt()?
} else {
DateTimeInputBuilder::new(self.question.clone()).prompt()?
};
let location = if let Some(default) = self.default_location {
location::LocationInputBuilder::new("Select Location")
.with_default(default)
.prompt()
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?
} else {
location::LocationInputBuilder::new("Select Location")
.prompt()
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?
};
Ok((date, location))
}
}
pub fn datetime_location_input(question: impl Into<String>) -> DateTimeLocationInputBuilder {
DateTimeLocationInputBuilder::new(question)
}
#[macro_export]
macro_rules! ask_datetime_location {
($question:expr) => {
$crate::console::ask::datetime::DateTimeLocationInputBuilder::new($question)
.prompt()
.unwrap()
};
($question:expr, $default_date:expr, $default_location:expr) => {
$crate::console::ask::datetime::DateTimeLocationInputBuilder::new($question)
.with_default_date($default_date)
.with_default_location($default_location)
.prompt()
.unwrap()
};
}
}
fn main() {
let choices = vec![
choice!("Option1", "The first option", || Ok("Result1")),
choice!("Option2", "The second option", || Ok("Result2")),
choice!("Option3", "The third option", || Ok("Result3")),
];
if let Some(selected) = handle_args(choices.clone(), false) {
for item in selected {
println!("Selected: {}", item);
}
}
let multiple_choices = vec![
choice!("Apple", "A fruit", || Ok("Apple")),
choice!("Banana", "Another fruit", || Ok("Banana")),
choice!("Cherry", "A red fruit", || Ok("Cherry")),
];
if let Some(selected) = handle_args(multiple_choices.clone(), true) {
for item in selected {
println!("Selected: {}", item);
}
}
let date = datetime::date_input("Enter a date").prompt().unwrap();
println!("You entered date: {:?}", date);
let datetime = datetime::datetime_input("Enter date and time").prompt().unwrap();
println!("You entered datetime: {:?}", datetime);
let (datetime, location) =
datetime::datetime_location_input("Enter datetime and select location")
.prompt()
.unwrap();
println!("You entered datetime: {:?} and location: {:?}", datetime, location);
}