use std::ops::Not;
use crate::{
api::{
rest::{Gateway, Project, Section, Task},
tree::Tree,
},
config::Config,
interactive, labels,
tasks::{
close, edit, filter,
state::{State, TaskMenu},
},
};
use color_eyre::{Result, eyre::WrapErr};
use owo_colors::OwoColorize;
use strum::{Display, FromRepr, VariantNames};
use super::create;
#[derive(clap::Parser, Debug)]
pub struct Params {
#[clap(flatten)]
filter: filter::Filter,
#[arg(short = 'n', long = "nointeractive")]
nointeractive: bool,
#[clap(flatten)]
project: interactive::Selection<Project>,
#[clap(flatten)]
section: interactive::Selection<Section>,
#[clap(flatten)]
label: labels::LabelSelect,
#[arg(short = 'e', long = "expand")]
expand: bool,
#[arg(short = 'i', long = "interactive")]
continuous: bool,
}
pub async fn list(params: Params, gw: &Gateway, cfg: &Config) -> Result<()> {
if params.continuous && !params.nointeractive {
return list_interactive(params, gw, cfg).await;
}
match list_action(¶ms, gw, cfg).await {
Ok(_) => Ok(()),
Err(e) => Err(e),
}
}
async fn list_action(params: &Params, gw: &Gateway, cfg: &Config) -> Result<()> {
let state = if params.expand {
State::fetch_full_tree(Some(¶ms.filter.select(cfg)), gw, cfg).await
} else {
State::fetch_tree(Some(¶ms.filter.select(cfg)), gw, cfg).await
}?;
let state = filter_list(state, params).await?;
if params.nointeractive {
list_tasks(&state.tasks, &state);
} else {
match state.select_task()? {
Some(task) => select_task_option(task, &state, gw).await?,
None => {
println!("No selection was made");
}
}
}
Ok(())
}
async fn list_interactive(params: Params, gw: &Gateway, cfg: &Config) -> Result<()> {
let mut params = params;
loop {
match list_interactive_action(&mut params, gw, cfg).await {
Ok(ListAction::Cancel) => return Ok(()),
Ok(_) => {}
Err(e) => return Err(e),
}
}
}
pub enum ListAction {
Action,
Cancel,
}
async fn list_interactive_action(
params: &mut Params,
gw: &Gateway,
cfg: &Config,
) -> Result<ListAction> {
let filter = params.filter.select(cfg);
let state = if params.expand {
State::fetch_full_tree(Some(&filter), gw, cfg).await
} else {
State::fetch_tree(Some(&filter), gw, cfg).await
}?;
let state = filter_list(state, params).await?;
match state.select_or_menu()? {
TaskMenu::Menu => {
match interactive::select(
"Select Action:",
&[
"Create Task...",
&format!(
"Set Filter{}...",
if filter.is_empty().not() {
format!(" ({})", filter.yellow())
} else {
Default::default()
}
),
"| Show All Tasks",
"| Inbox",
"| Upcoming",
"| Default Filter",
],
)? {
Some(0) => create::create(create::Params {}, gw, cfg).await?,
Some(1) => {
let filter = filter.is_empty().not().then_some(filter);
params.filter.set_filter(Some(
&interactive::input_optional("Filter", filter)?.unwrap_or_default(),
));
}
Some(2) => params.filter.set_filter(Some("all")),
Some(3) => params.filter.set_filter(Some("#inbox")),
Some(4) => params.filter.set_filter(Some(&cfg.default_filter)),
Some(5) => params.filter.set_filter(Some("(today | overdue)")),
Some(_) => unreachable!(),
None => {}
};
Ok(ListAction::Action)
}
TaskMenu::Select(task) => {
select_task_option(task, &state, gw).await?;
Ok(ListAction::Action)
}
TaskMenu::None => {
println!("No selection was made");
Ok(ListAction::Cancel)
}
}
}
async fn filter_list<'a>(state: State<'a>, params: &'_ Params) -> Result<State<'a>> {
let projects = state
.projects
.values()
.map(ToOwned::to_owned)
.collect::<Vec<_>>();
let sections = state
.sections
.values()
.map(ToOwned::to_owned)
.collect::<Vec<_>>();
let labels = state
.labels
.values()
.map(ToOwned::to_owned)
.collect::<Vec<_>>();
let project = params.project.optional(&projects)?;
let section = params.section.optional(§ions)?;
let labels = params
.label
.labels(&labels, labels::Selection::AllowEmpty)?;
let mut state = state;
if let Some(p) = project {
state = state.filter(|tree| tree.project_id == *p.id);
}
if let Some(s) = section {
state = state.filter(|tree| tree.section_id.as_ref() == Some(&s.id));
}
if !labels.is_empty() {
state = state.filter(|tree| {
labels
.iter()
.map(|l| l.id.clone())
.any(|l| tree.labels.contains(&l))
});
}
Ok(state)
}
fn list_tasks<'a>(tasks: &'a [Tree<Task>], state: &'a State) {
let mut tasks = tasks.to_vec();
tasks.sort();
for task in tasks.iter() {
println!("{}", state.table_task(task));
list_tasks(&task.subitems, state);
}
}
#[derive(Display, FromRepr, VariantNames)]
enum TaskOptions {
#[strum(serialize = "Close Task")]
Close,
#[strum(serialize = "Complete Forever")]
Complete,
Edit,
Quit,
}
async fn select_task_option<'a>(
task: &'a Tree<Task>,
state: &'a State<'_>,
gw: &'_ Gateway,
) -> Result<()> {
println!("{}", state.full_task(task));
let result = match make_selection(TaskOptions::VARIANTS)? {
Some(index) => TaskOptions::from_repr(index).unwrap(),
None => {
println!("No selection made");
return Ok(());
}
};
match result {
TaskOptions::Close => {
close::close(
close::Params {
task: task.id.clone().into(),
complete: false,
},
gw,
state.config,
)
.await?
}
TaskOptions::Complete => {
close::close(
close::Params {
task: task.id.clone().into(),
complete: true,
},
gw,
state.config,
)
.await?
}
TaskOptions::Edit => edit_task(task, gw, state.config).await?,
TaskOptions::Quit => {}
};
Ok(())
}
#[derive(Display, FromRepr, VariantNames)]
enum EditOptions {
Name,
Description,
Due,
Priority,
Quit,
}
async fn edit_task(task: &Tree<Task>, gw: &Gateway, cfg: &Config) -> Result<()> {
let result = match make_selection(EditOptions::VARIANTS)? {
Some(index) => EditOptions::from_repr(index).unwrap(),
None => {
println!("No selection made");
return Ok(());
}
};
match result {
EditOptions::Quit => {}
EditOptions::Priority => {
let selection = dialoguer::Select::new()
.with_prompt("Set priority")
.items(&["1 - Urgent", "2 - Very High", "3 - High", "4 - Normal"])
.default((4 - task.priority as u8) as usize)
.interact()
.wrap_err("Bad user input")?
+ 1;
let mut params = edit::Params::new(task.id.clone());
params.priority = Some(selection.try_into()?);
edit::edit(params, gw, cfg).await?;
}
_ => {
let text = dialoguer::Input::new()
.with_prompt("New value")
.interact_text()
.wrap_err("Bad user input")?;
let mut params = edit::Params::new(task.id.clone());
match result {
EditOptions::Name => {
params.name = Some(text);
}
EditOptions::Description => {
params.desc = Some(text);
}
EditOptions::Due => {
params.due = Some(text);
}
EditOptions::Priority => unreachable!(),
EditOptions::Quit => unreachable!(),
};
edit::edit(params, gw, cfg).await?;
}
};
Ok(())
}
fn make_selection<T: ToString + std::fmt::Display>(variants: &[T]) -> Result<Option<usize>> {
dialoguer::FuzzySelect::with_theme(&dialoguer::theme::ColorfulTheme::default())
.items(variants)
.default(0)
.interact_opt()
.wrap_err("Unable to make a selection")
}