aboutsummaryrefslogtreecommitdiffstats
path: root/pkgs/by-name/ts/tskm/src/cli.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--pkgs/by-name/ts/tskm/src/cli.rs375
1 files changed, 375 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..3dc1181d
--- /dev/null
+++ b/pkgs/by-name/ts/tskm/src/cli.rs
@@ -0,0 +1,375 @@
+// nixos-config - My current NixOS configuration
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of my nixos-config.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::{ffi::OsStr, path::PathBuf, thread};
+
+use anyhow::{bail, Result};
+use clap::{builder::StyledStr, ArgAction, Parser, Subcommand, ValueEnum};
+use clap_complete::{ArgValueCompleter, CompletionCandidate};
+use tokio::runtime::Runtime;
+
+use crate::{
+ interface::{
+ input::{Input, Tag},
+ open::UrlLike,
+ project::ProjectName,
+ },
+ state, task,
+};
+
+macro_rules! as_sync {
+ (
+ wrap $old_name_1:ident($($arg_name_1:ident : $arg_type_1:ty),*) -> $output_1:ty => $new_name_1:ident;
+ $(
+ wrap $old_name:ident($($arg_name:ident : $arg_type:ty),*) -> $output:ty => $new_name:ident;
+ )+
+ ) => {
+ as_sync!(
+ wrap $old_name_1($($arg_name_1 : $arg_type_1),*) -> $output_1 => $new_name_1;
+ );
+ as_sync!(
+ $(
+ wrap $old_name($($arg_name : $arg_type),*) -> $output => $new_name;
+ )+
+ );
+ };
+ (
+ wrap $old_name:ident($($arg_name:ident : $arg_type:ty),*) -> $output:ty => $new_name:ident $(;)?
+ ) => {
+ fn $new_name($($arg_name: $arg_type),*) -> $output {
+ $(
+ let $arg_name = $arg_name.to_owned();
+ ),*
+
+ let handle: std::thread::JoinHandle<$output> = thread::spawn(move || {
+ let rt = Runtime::new().expect("No runtime issue");
+
+ let output = rt.block_on($old_name($($arg_name.as_ref()),*));
+
+ output
+ });
+
+ handle.join().expect("The thread should be joinable")
+ }
+ };
+}
+
+#[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 connected to `qutebrowser` 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 Qutebrowser 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_name = "ID", value_parser = task_from_working_set_id_sync, add = ArgValueCompleter::new(complete_task_id_sync))]
+ task: task::Task,
+ },
+}
+
+as_sync!(
+ wrap task_from_working_set_id(id: &str) -> Result<task::Task> => task_from_working_set_id_sync;
+ wrap complete_task_id(current: &OsStr) -> Vec<CompletionCandidate> => complete_task_id_sync;
+);
+
+async fn task_from_working_set_id(id: &str) -> Result<task::Task> {
+ let id: usize = id.parse()?;
+ let mut state = state::State::new_ro().await?;
+
+ let Some(task) = task::Task::from_working_set(id, &mut state).await? else {
+ bail!("Working set id '{id}' is not valid!")
+ };
+ Ok(task)
+}
+
+#[derive(Subcommand, Debug)]
+pub enum OpenCommand {
+ /// Open each project's Qutebrowser 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 {
+ /// Review all projects, if they contain tabs
+ #[arg(short, long, default_value_t)]
+ non_empty: bool,
+ },
+
+ /// Opens Qutebrowser 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 URLs to open.
+ urls: Option<Vec<UrlLike>>,
+ },
+
+ /// Open a selected project in it's Qutebrowser profile.
+ ///
+ /// This will use rofi's dmenu mode to select one project from the list of all registered
+ /// projects.
+ Select {
+ /// The URLs to open.
+ urls: Option<Vec<UrlLike>>,
+ },
+
+ /// List all open tabs in the project.
+ ListTabs {
+ /// The projects to open.
+ #[arg(value_parser = task::Project::from_project_string, add = ArgValueCompleter::new(complete_project))]
+ projects: Option<Vec<task::Project>>,
+
+ /// Only show the tabs, that are in this mode
+ #[arg(short, long, conflicts_with = "projects")]
+ mode: Option<ListMode>,
+ },
+}
+
+#[derive(Clone, Copy, ValueEnum, Debug)]
+pub enum ListMode {
+ // The tab contains no tabs.
+ Empty,
+
+ // The tab contains tabs.
+ NonEmpty,
+}
+
+#[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 {
+ /// The file to read from.
+ file: PathBuf,
+
+ /// Additional tags to apply to every read URL in the file.
+ #[arg(add = ArgValueCompleter::new(complete_tag))]
+ tags: Vec<Tag>,
+ },
+
+ /// 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 {
+ /// Only list the inputs that have all the specified tags
+ #[arg(add = ArgValueCompleter::new(complete_tag))]
+ tags: Vec<Tag>,
+ },
+
+ /// Show all the available tags.
+ Tags {},
+}
+
+async fn complete_task_id(current: &OsStr) -> Vec<CompletionCandidate> {
+ async fn format_task(
+ task: task::Task,
+ current: &str,
+ state: &mut state::State,
+ ) -> Option<CompletionCandidate> {
+ let id = {
+ let Ok(base) = task.working_set_id(state).await else {
+ return None;
+ };
+ base.to_string()
+ };
+
+ if !id.starts_with(current) {
+ return None;
+ }
+
+ let description = {
+ let Ok(base) = task.description(state).await 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().await else {
+ return output;
+ };
+
+ let Ok(pending) = state.replica().pending_tasks().await 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).await {
+ if project == current_project {
+ if let Some(out) = format_task(task, current, &mut state).await {
+ output.push(out);
+ }
+ }
+ }
+ }
+ } else {
+ for t in pending {
+ let task = task::Task::from(&t);
+ if let Some(out) = format_task(task, current, &mut state).await {
+ 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
+}
+fn complete_tag(current: &OsStr) -> Vec<CompletionCandidate> {
+ let mut output = vec![];
+
+ let Some(current) = current.to_str() else {
+ return output;
+ };
+
+ if !current.starts_with('+') {
+ output.push(CompletionCandidate::new(format!("+{current}")));
+ }
+
+ output
+}
+
+#[cfg(test)]
+mod test {
+ use clap::CommandFactory;
+
+ use super::CliArgs;
+ #[test]
+ fn verify_cli() {
+ CliArgs::command().debug_assert();
+ }
+}