diff options
Diffstat (limited to '')
-rw-r--r-- | pkgs/by-name/ts/tskm/src/cli.rs | 266 |
1 files changed, 266 insertions, 0 deletions
diff --git a/pkgs/by-name/ts/tskm/src/cli.rs b/pkgs/by-name/ts/tskm/src/cli.rs new file mode 100644 index 00000000..c1eba387 --- /dev/null +++ b/pkgs/by-name/ts/tskm/src/cli.rs @@ -0,0 +1,266 @@ +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<task::Task> { + 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<Url>, + }, + + /// 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<Url>, + }, + + /// 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<task::Project>, + }, +} + +#[derive(Subcommand, Debug)] +pub enum InputCommand { + /// Add URLs as inputs to be categorized. + Add { inputs: Vec<Input> }, + /// Remove URLs + Remove { + #[arg(add = ArgValueCompleter::new(complete_input_url))] + inputs: Vec<Input>, + }, + + /// 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<CompletionCandidate> { + fn format_task( + task: task::Task, + current: &str, + state: &mut state::State, + ) -> Option<CompletionCandidate> { + 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<CompletionCandidate> { + 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<CompletionCandidate> { + 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 +} |