use crate::{
db::tags::{Tag, Tags},
libs::{messages::Message, pick, prompt::ensure_interactive, view::View},
msg_error, msg_info, msg_print, msg_success,
};
use anyhow::Result;
use clap::{Args, Subcommand};
use dialoguer::{Confirm, Input, Select, theme::ColorfulTheme};
#[derive(Debug, Args)]
pub struct TagArgs {
#[command(subcommand)]
command: Option<TagCommand>,
}
#[derive(Debug, Subcommand)]
enum TagCommand {
Add {
name: String,
#[arg(short, long)]
color: Option<String>,
},
List,
Show {
tag: String,
},
Edit {
tag: Option<String>,
},
Remove {
tag: Option<String>,
#[arg(long, short = 'y')]
yes: bool,
},
}
pub async fn cmd(args: TagArgs) -> Result<()> {
match args.command {
Some(TagCommand::Add { name, color }) => handle_create(name, color),
Some(TagCommand::List) => handle_list(),
Some(TagCommand::Show { tag }) => handle_show_tasks(tag).await,
Some(TagCommand::Edit { tag }) => handle_edit(resolve_tag(tag, "Edit which tag?")?),
Some(TagCommand::Remove { tag, yes }) => handle_delete(resolve_tag(tag, "Remove which tag?")?, yes),
None => {
ensure_interactive("no subcommand given; run `kasl tag list` or see `kasl tag --help`")?;
handle_interactive()
}
}
}
fn resolve_tag(tag: Option<String>, prompt: &str) -> Result<String> {
match tag {
Some(tag) => Ok(tag),
None => pick::tag(&Tags::new()?.get_all()?, prompt),
}
}
fn handle_create(name: String, color: Option<String>) -> Result<()> {
let mut tags_db = Tags::new()?;
if tags_db.get_by_name(&name)?.is_some() {
msg_error!(Message::TagAlreadyExists(name));
return Ok(());
}
let tag = Tag::new(name.clone(), color);
tags_db.create(&tag)?;
msg_success!(Message::TagCreated(name));
Ok(())
}
fn handle_list() -> Result<()> {
let mut tags_db = Tags::new()?;
let tags = tags_db.get_all()?;
if tags.is_empty() {
msg_info!(Message::NoTagsFound);
return Ok(());
}
msg_print!(Message::TagListHeader, true);
View::tags(&tags)?;
Ok(())
}
fn handle_edit(tag_identifier: String) -> Result<()> {
ensure_interactive("`kasl tag edit` is interactive and needs a terminal")?;
let mut tags_db = Tags::new()?;
let tag = if let Ok(id) = tag_identifier.parse::<i32>() {
tags_db.get_by_id(id)?
} else {
tags_db.get_by_name(&tag_identifier)?
};
let tag = match tag {
Some(t) => t,
None => {
msg_error!(Message::TagNotFound(tag_identifier));
return Ok(());
}
};
msg_print!(Message::EditingTag(tag.name.clone()), true);
let new_name = Input::with_theme(&ColorfulTheme::default())
.with_prompt(Message::PromptTagName.to_string())
.default(tag.name.clone())
.interact_text()?;
let new_color = Input::with_theme(&ColorfulTheme::default())
.with_prompt(Message::PromptTagColor.to_string())
.default(tag.color.unwrap_or_default())
.allow_empty(true)
.interact_text()?;
let color = if new_color.is_empty() { None } else { Some(new_color) };
tags_db.update(&Tag {
id: tag.id,
name: new_name.clone(),
color,
created_at: None,
})?;
msg_success!(Message::TagUpdated(new_name));
Ok(())
}
fn handle_delete(tag_identifier: String, assume_yes: bool) -> Result<()> {
let mut tags_db = Tags::new()?;
let tag = if let Ok(id) = tag_identifier.parse::<i32>() {
tags_db.get_by_id(id)?
} else {
tags_db.get_by_name(&tag_identifier)?
};
let tag = match tag {
Some(t) => t,
None => {
msg_error!(Message::TagNotFound(tag_identifier));
return Ok(());
}
};
let task_count = tags_db.get_tasks_by_tag(tag.id.unwrap())?.len();
if !assume_yes {
ensure_interactive(&format!("refusing to remove tag '{}' without --yes outside an interactive terminal", tag.name))?;
let prompt = if task_count > 0 {
Message::ConfirmDeleteTagWithTasks(tag.name.clone(), task_count)
} else {
Message::ConfirmDeleteTag(tag.name.clone())
};
let confirmed = Confirm::with_theme(&ColorfulTheme::default())
.with_prompt(prompt.to_string())
.default(false)
.interact()?;
if !confirmed {
msg_info!(Message::OperationCancelled);
return Ok(());
}
}
tags_db.delete(tag.id.unwrap())?;
msg_success!(Message::TagDeleted(tag.name));
Ok(())
}
async fn handle_show_tasks(tag_name: String) -> Result<()> {
let mut tags_db = Tags::new()?;
let tag = match tags_db.get_by_name(&tag_name)? {
Some(t) => t,
None => {
msg_error!(Message::TagNotFound(tag_name));
return Ok(());
}
};
let task_ids = tags_db.get_tasks_by_tag(tag.id.unwrap())?;
if task_ids.is_empty() {
msg_info!(Message::NoTasksWithTag(tag_name));
return Ok(());
}
use crate::db::tasks::Tasks;
let tasks = Tasks::new()?.fetch(crate::libs::task::TaskFilter::ByIds(task_ids))?;
msg_print!(Message::TasksWithTag(tag_name), true);
View::tasks(&tasks)?;
Ok(())
}
fn handle_interactive() -> Result<()> {
let options = vec!["Add tag", "List tags", "Edit tag", "Remove tag"];
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt(Message::SelectTagAction.to_string())
.items(&options)
.interact()?;
match selection {
0 => {
let name = Input::with_theme(&ColorfulTheme::default())
.with_prompt(Message::PromptTagName.to_string())
.interact_text()?;
let color: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt(Message::PromptTagColor.to_string())
.allow_empty(true)
.interact_text()?;
handle_create(name, if color.is_empty() { None } else { Some(color) })
}
1 => handle_list(),
2 => {
let tags = Tags::new()?.get_all()?;
if tags.is_empty() {
msg_info!(Message::NoTagsFound);
return Ok(());
}
handle_edit(pick::tag(&tags, &Message::SelectTagToEdit.to_string())?)
}
3 => {
let tags = Tags::new()?.get_all()?;
if tags.is_empty() {
msg_info!(Message::NoTagsFound);
return Ok(());
}
handle_delete(pick::tag(&tags, &Message::SelectTagToDelete.to_string())?, false)
}
_ => Ok(()),
}
}