use std::{ffi::OsStr, path::PathBuf}; use anyhow::{bail, Result}; use clap::{builder::StyledStr, ArgAction, Parser, Subcommand}; use clap_complete::{ArgValueCompleter, CompletionCandidate}; use url::Url; use crate::{ interface::{input::Input, project::ProjectName}, state, task, }; #[derive(Parser, Debug)] #[command(author, version, about, long_about, verbatim_doc_comment)] /// This is the core interface to the system-integrated task management /// /// `tskm` effectively combines multiple applications together: /// - `taskwarrior` projects are raised connected to `firefox` profiles, making it possible to “open” /// a project. /// /// - Every `taskwarrior` project has a determined `neorg` path, so that extra information for a /// `project` can be stored in this `norg` file. /// /// - `tskm` can track inputs for you. These are URLs with optional tags which you can that /// “review” to open tasks based on them. pub struct CliArgs { #[command(subcommand)] pub command: Command, /// Increase message verbosity #[arg(long="verbose", short = 'v', action = ArgAction::Count, default_value_t = 2)] pub verbosity: u8, /// Silence all output #[arg(long, short = 'q')] pub quiet: bool, } #[derive(Subcommand, Debug)] pub enum Command { /// Interact with projects. Projects { #[command(subcommand)] command: ProjectCommand, }, /// Manage the input queue. Inputs { #[command(subcommand)] command: InputCommand, }, /// Access the associated `neorg` workspace for the project/task. Neorg { #[command(subcommand)] command: NeorgCommand, }, /// Interface with the Firefox profile of each project. Open { #[command(subcommand)] command: OpenCommand, }, } #[derive(Subcommand, Debug)] pub enum ProjectCommand { /// Lists all available projects. List, /// Allows you to quickly add projects. Add { /// The name of the new project. #[arg(value_parser = ProjectName::try_from_project)] new_project_name: ProjectName, }, } #[derive(Subcommand, Debug, Clone, Copy)] pub enum NeorgCommand { /// Open the `neorg` project associated with id of the task. Task { /// The working set id of the task #[arg(value_parser = task_from_working_set_id, add = ArgValueCompleter::new(complete_task_id))] id: task::Task, }, } fn task_from_working_set_id(id: &str) -> Result { let id: usize = id.parse()?; let mut state = state::State::new_ro()?; let Some(task) = task::Task::from_working_set(id, &mut state)? else { bail!("Working set id '{id}' is not valid!") }; Ok(task) } #[derive(Subcommand, Debug)] pub enum OpenCommand { /// Open each project's Firefox profile consecutively, that was opened since the last review. /// /// This allows you to remove stale opened tabs and to commit open tabs to the `inputs`. Review, /// Opens Firefox with either the supplied project or the currently active project profile. Project { /// The project to open. #[arg(value_parser = task::Project::from_project_string, add = ArgValueCompleter::new(complete_project))] project: task::Project, /// The URL to open. url: Option, }, /// Open a selected project in it's Firefox profile. /// /// This will use rofi's dmenu mode to select one project from the list of all registered /// projects. Select { /// The URL to open. url: Option, }, /// List all open tabs in the project. ListTabs { /// The project to open. #[arg(value_parser = task::Project::from_project_string, add = ArgValueCompleter::new(complete_project))] project: Option, }, } #[derive(Subcommand, Debug)] pub enum InputCommand { /// Add URLs as inputs to be categorized. Add { inputs: Vec }, /// Remove URLs Remove { #[arg(add = ArgValueCompleter::new(complete_input_url))] inputs: Vec, }, /// Add all URLs in the file as inputs to be categorized. /// /// This expects each line to contain one URL. File { file: PathBuf }, /// Like 'review', but for the inputs that have previously been added. /// It takes a project in which to open the URLs. Review { /// Opens all the URLs in this project. #[arg(value_parser = task::Project::from_project_string, add = ArgValueCompleter::new(complete_project))] project: task::Project, }, /// List all the previously added inputs. List, } fn complete_task_id(current: &OsStr) -> Vec { fn format_task( task: task::Task, current: &str, state: &mut state::State, ) -> Option { let id = { let Ok(base) = task.working_set_id(state) else { return None; }; base.to_string() }; if !id.starts_with(current) { return None; } let description = { let Ok(base) = task.description(state) else { return None; }; StyledStr::from(base) }; Some(CompletionCandidate::new(id).help(Some(description))) } let mut output = vec![]; let Some(current) = current.to_str() else { return output; }; let Ok(mut state) = state::State::new_ro() else { return output; }; let Ok(pending) = state.replica().pending_tasks() else { return output; }; let Ok(current_project) = task::Project::get_current() else { return output; }; if let Some(current_project) = current_project { for t in pending { let task = task::Task::from(&t); if let Ok(project) = task.project(&mut state) { if project == current_project { if let Some(out) = format_task(task, current, &mut state) { output.push(out); } else { continue; } } } } } else { for t in pending { let task = task::Task::from(&t); if let Some(out) = format_task(task, current, &mut state) { output.push(out); } } } output } fn complete_project(current: &OsStr) -> Vec { let mut output = vec![]; let Some(current) = current.to_str() else { return output; }; let Ok(all) = task::Project::all() else { return output; }; for a in all { if a.to_project_display().starts_with(current) { output.push(CompletionCandidate::new(a.to_project_display())); } } output } fn complete_input_url(current: &OsStr) -> Vec { let mut output = vec![]; let Some(current) = current.to_str() else { return output; }; let Ok(all) = Input::all() else { return output; }; for a in all { if a.to_string().starts_with(current) { output.push(CompletionCandidate::new(a.to_string())); } } output }