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.rs295
1 files changed, 274 insertions, 21 deletions
diff --git a/pkgs/by-name/ts/tskm/src/cli.rs b/pkgs/by-name/ts/tskm/src/cli.rs
index 99c8693e..3dc1181d 100644
--- a/pkgs/by-name/ts/tskm/src/cli.rs
+++ b/pkgs/by-name/ts/tskm/src/cli.rs
@@ -1,26 +1,90 @@
-use std::path::PathBuf;
+// 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 clap::{Parser, Subcommand};
+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, project::ProjectName},
- task,
+ 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 raised connected to `firefox` profiles, making it possible to “open”
+/// - `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)]
@@ -43,7 +107,7 @@ pub enum Command {
command: NeorgCommand,
},
- /// Interface with the Firefox profile of each project.
+ /// Interface with the Qutebrowser profile of each project.
Open {
#[command(subcommand)]
command: OpenCommand,
@@ -66,57 +130,246 @@ pub enum ProjectCommand {
#[derive(Subcommand, Debug, Clone, Copy)]
pub enum NeorgCommand {
/// Open the `neorg` project associated with id of the task.
- Task { id: task::Id },
+ 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 Firefox profile consecutively, that was opened since the last review.
+ /// 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 {
+ /// Review all projects, if they contain tabs
+ #[arg(short, long, default_value_t)]
+ non_empty: bool,
+ },
- /// Opens Firefox with either the supplied project or the currently active project profile.
+ /// 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)]
- project: Option<task::Project>,
+ #[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 Firefox profile.
+ /// 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,
+ Select {
+ /// The URLs to open.
+ urls: Option<Vec<UrlLike>>,
+ },
/// List all open tabs in the project.
ListTabs {
- /// The project to open.
- #[arg(value_parser = task::Project::from_project_string)]
- project: Option<task::Project>,
+ /// 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 { inputs: Vec<Input> },
+ 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 },
+ 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)]
+ #[arg(value_parser = task::Project::from_project_string, add = ArgValueCompleter::new(complete_project))]
project: task::Project,
},
/// List all the previously added inputs.
- List,
+ 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();
+ }
}