use clap::{Parser, Subcommand};
use crate::{
color,
config::Config,
errors::Error,
filters,
input::{self, DateTimeInput},
labels,
lists::Flag,
projects, sections,
tasks::{self, TaskAttribute, priority::Priority},
todoist,
};
#[derive(Subcommand, Debug, Clone)]
pub enum TaskCommands {
#[clap(alias = "q")]
QuickAdd(QuickAdd),
#[clap(alias = "c")]
Create(Create),
#[clap(alias = "e")]
Edit(Edit),
#[clap(alias = "n")]
Next(Next),
#[clap(alias = "o")]
Complete(Complete),
#[clap(alias = "m")]
Comment(Comment),
}
#[derive(Parser, Debug, Clone)]
pub struct QuickAdd {
#[arg(short, long, num_args(1..))]
content: Option<Vec<String>>,
}
#[derive(Parser, Debug, Clone)]
pub struct Create {
#[arg(short, long)]
project: Option<String>,
#[arg(short = 'u', long)]
due: Option<String>,
#[arg(short, long, default_value_t = String::new())]
description: String,
#[arg(short, long)]
content: Option<String>,
#[arg(short, long, default_value_t = false)]
no_section: bool,
#[arg(short = 'r', long)]
priority: Option<u8>,
#[arg(short, long)]
label: Vec<String>,
}
#[derive(Parser, Debug, Clone)]
pub struct Edit {
#[arg(short, long)]
project: Option<String>,
#[arg(short, long)]
filter: Option<String>,
}
#[derive(Parser, Debug, Clone)]
pub struct Next {
#[arg(short, long)]
project: Option<String>,
#[arg(short, long)]
filter: Option<String>,
}
#[derive(Parser, Debug, Clone)]
pub struct Complete {}
#[derive(Parser, Debug, Clone)]
pub struct Comment {
#[arg(short, long)]
content: Option<String>,
}
pub async fn quick_add(config: Config, args: &QuickAdd) -> Result<String, Error> {
let QuickAdd { content } = args;
let maybe_string = content.as_ref().map(|c| c.join(" "));
let content = super::fetch_string(maybe_string.as_deref(), &config, input::CONTENT)?;
let (content, reminder) = if let Some(index) = content.find('!') {
let (before, after) = content.split_at(index);
(
before.trim().to_string(),
Some(after[1..].trim().to_string()),
)
} else {
(content, None)
};
todoist::quick_create_task(&config, &content, reminder).await?;
Ok(color::green_string("✓"))
}
fn is_no_sections(args: &Create, config: &Config) -> bool {
args.no_section || config.no_sections.unwrap_or_default()
}
pub async fn create(config: Config, args: &Create) -> Result<String, Error> {
if no_flags_used(args) {
let options = tasks::create_task_attributes();
let selections = input::multi_select(input::ATTRIBUTES, options, config.mock_select)?;
let content = super::fetch_string(None, &config, input::CONTENT)?;
let description = if selections.contains(&TaskAttribute::Description) {
super::fetch_string(None, &config, input::DESCRIPTION)?
} else {
String::new()
};
let priority = if selections.contains(&TaskAttribute::Priority) {
super::fetch_priority(&None, &config)?
} else {
Priority::None
};
let due = if selections.contains(&TaskAttribute::Due) {
let datetime_input = input::datetime(
config.mock_select,
config.mock_string.clone(),
config.natural_language_only,
false,
false,
)?;
match datetime_input {
DateTimeInput::Skip => unreachable!(),
DateTimeInput::Complete => unreachable!(),
DateTimeInput::None => None,
DateTimeInput::Text(datetime) => Some(datetime),
}
} else {
None
};
let labels = if selections.contains(&TaskAttribute::Labels) {
let all_labels = labels::get_labels(&config, false).await?;
input::multi_select(input::LABELS, all_labels, config.mock_select)?
} else {
Vec::new()
}
.into_iter()
.map(|l| l.name.to_owned())
.collect::<Vec<String>>();
let project = match super::fetch_project(args.project.as_deref(), &config).await? {
Flag::Project(project) => project,
_ => unreachable!(),
};
let section = if is_no_sections(args, &config) {
None
} else {
sections::select_section(&config, &project).await?
};
todoist::create_task(
&config,
&content,
&project,
section,
priority,
&description,
due.as_deref(),
&labels,
)
.await?;
} else {
let Create {
project,
due,
description,
content,
priority,
label: labels,
no_section: _no_section,
} = args;
let project = match super::fetch_project(project.as_deref(), &config).await? {
Flag::Project(project) => project,
_ => unreachable!(),
};
let section = if is_no_sections(args, &config) {
None
} else {
sections::select_section(&config, &project).await?
};
let content = super::fetch_string(content.as_deref(), &config, input::CONTENT)?;
let priority = super::fetch_priority(priority, &config)?;
todoist::create_task(
&config,
&content,
&project,
section,
priority,
description,
due.as_deref(),
labels,
)
.await?;
}
Ok(color::green_string("✓"))
}
fn no_flags_used(args: &Create) -> bool {
let Create {
project,
due,
description,
content,
no_section: _no_section,
priority,
label,
} = args;
project.is_none()
&& due.is_none()
&& description.is_empty()
&& content.is_none()
&& priority.is_none()
&& label.is_empty()
}
pub async fn edit(config: Config, args: &Edit) -> Result<String, Error> {
let Edit { project, filter } = args;
match super::fetch_project_or_filter(project.as_deref(), filter.as_deref(), &config).await? {
Flag::Project(project) => projects::edit_task(&config, &project).await,
Flag::Filter(filter) => filters::edit_task(&config, filter).await,
}
}
pub async fn next(config: Config, args: &Next) -> Result<String, Error> {
let Next { project, filter } = args;
match super::fetch_project_or_filter(project.as_deref(), filter.as_deref(), &config).await? {
Flag::Project(project) => projects::next_task(config, &project).await,
Flag::Filter(filter) => filters::next_task(&config, &filter).await,
}
}
pub async fn complete(config: Config, _args: &Complete) -> Result<String, Error> {
match config.next_task() {
Some(task) => {
todoist::complete_task(&config, &task, true).await?;
Ok(color::green_string("Task completed successfully"))
}
None => Err(Error::new(
"task_complete",
"There is nothing to complete. A task must first be marked as 'next'.",
)),
}
}
pub async fn comment(config: Config, args: &Comment) -> Result<String, Error> {
let Comment { content } = args;
match config.next_task() {
Some(task) => {
let content = super::fetch_string(content.as_deref(), &config, input::CONTENT)?;
todoist::create_comment(&config, &task, content, true).await?;
Ok(color::green_string("Comment created successfully"))
}
None => Err(Error::new(
"task_comment",
"There is nothing to comment on. A task must first be marked as 'next'.",
)),
}
}