aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-04-14 14:33:24 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-04-14 14:33:24 +0200
commit8f4f24b31abcf62e6688614b7986534d41de0b00 (patch)
tree3abbb3c78f206a8c8d12fd304cf3ffb5f94db4a6
parentpkgs/default.nix: No longer required `sysLib` as input (diff)
downloadnixos-config-8f4f24b31abcf62e6688614b7986534d41de0b00.zip
pkgs/tskm: Add completions for dynamic values
This brings `tskm` again on the same level `neorg` was with regard to completions.
-rw-r--r--pkgs/by-name/ts/tskm/.envrc3
-rw-r--r--pkgs/by-name/ts/tskm/Cargo.lock34
-rw-r--r--pkgs/by-name/ts/tskm/Cargo.toml13
-rw-r--r--pkgs/by-name/ts/tskm/build.rs52
-rw-r--r--pkgs/by-name/ts/tskm/package.nix10
-rw-r--r--pkgs/by-name/ts/tskm/src/cli.rs133
-rw-r--r--pkgs/by-name/ts/tskm/src/main.rs42
-rw-r--r--pkgs/by-name/ts/tskm/src/task/mod.rs10
8 files changed, 178 insertions, 119 deletions
diff --git a/pkgs/by-name/ts/tskm/.envrc b/pkgs/by-name/ts/tskm/.envrc
index d21a17fc..a83561cc 100644
--- a/pkgs/by-name/ts/tskm/.envrc
+++ b/pkgs/by-name/ts/tskm/.envrc
@@ -1,8 +1,5 @@
#!/usr/bin/env sh
-SHELL_COMPLETION_DIR="$(pwd)/target/shell"
-export SHELL_COMPLETION_DIR
-
export TSKM_PROJECT_FILE=/home/soispha/repos/nix/config/modules/common/projects.json
use flake
diff --git a/pkgs/by-name/ts/tskm/Cargo.lock b/pkgs/by-name/ts/tskm/Cargo.lock
index e4beb42a..58c4e505 100644
--- a/pkgs/by-name/ts/tskm/Cargo.lock
+++ b/pkgs/by-name/ts/tskm/Cargo.lock
@@ -174,6 +174,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06f5378ea264ad4f82bbc826628b5aad714a75abf6ece087e923010eb937fb6"
dependencies = [
"clap",
+ "clap_lex",
+ "is_executable",
+ "shlex",
]
[[package]]
@@ -506,6 +509,15 @@ dependencies = [
]
[[package]]
+name = "is_executable"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4a1b5bad6f9072935961dfbf1cced2f3d129963d091b6f69f007fe04e758ae2"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1047,6 +1059,22 @@ dependencies = [
]
[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1056,6 +1084,12 @@ dependencies = [
]
[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
name = "windows-core"
version = "0.61.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/pkgs/by-name/ts/tskm/Cargo.toml b/pkgs/by-name/ts/tskm/Cargo.toml
index 39feea0c..38c9aab2 100644
--- a/pkgs/by-name/ts/tskm/Cargo.toml
+++ b/pkgs/by-name/ts/tskm/Cargo.toml
@@ -8,6 +8,7 @@ edition = "2021"
[dependencies]
anyhow = "1.0.97"
clap = { version = "4.5.35", features = ["derive"] }
+clap_complete = { version = "4.5.47", features = ["unstable-dynamic"] }
dirs = "6.0.0"
log = "0.4.27"
lz4_flex = "0.11.3"
@@ -77,15 +78,3 @@ perf = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
missing_panics_doc = "allow"
missing_errors_doc = "allow"
-
-[build-dependencies]
-anyhow = "1.0.97"
-clap = { version = "4.5.35", features = ["derive"] }
-clap_complete = "4.5.47"
-dirs = "6.0.0"
-log = "0.4.27"
-serde = { version = "1.0.219", features = ["derive"] }
-serde_json = "1.0.140"
-taskchampion = { version = "2.0.3", default-features = false }
-url = "2.5.4"
-walkdir = "2.5.0"
diff --git a/pkgs/by-name/ts/tskm/build.rs b/pkgs/by-name/ts/tskm/build.rs
deleted file mode 100644
index e3b60bb9..00000000
--- a/pkgs/by-name/ts/tskm/build.rs
+++ /dev/null
@@ -1,52 +0,0 @@
-use anyhow::{Context, Result};
-use clap::{CommandFactory, ValueEnum};
-use clap_complete::generate_to;
-use clap_complete::Shell;
-
-use std::env;
-use std::fs;
-use std::path::PathBuf;
-
-use crate::cli::CliArgs;
-
-pub mod task {
- include!("src/task/mod.rs");
-}
-pub mod state {
- include!("src/state.rs");
-}
-
-pub mod interface {
- pub mod input {
- include!("src/interface/input/mod.rs");
- }
- pub mod project {
- include!("src/interface/project/mod.rs");
- }
-}
-
-pub mod cli {
- include!("src/cli.rs");
-}
-
-fn main() -> Result<()> {
- let outdir = match env::var_os("SHELL_COMPLETION_DIR") {
- None => return Ok(()),
- Some(outdir) => outdir,
- };
-
- if !PathBuf::from(&outdir).exists() {
- fs::create_dir_all(&outdir)?;
- }
-
- let mut cmd = CliArgs::command();
-
- for &shell in Shell::value_variants() {
- let path = generate_to(shell, &mut cmd, "tskm", &outdir).with_context(|| {
- format!("Failed to output shell completion for {shell} to {outdir:?}")
- })?;
- println!("cargo:warning=completion file for {shell} is generated at: {path:?}");
- }
-
- Ok(())
-}
diff --git a/pkgs/by-name/ts/tskm/package.nix b/pkgs/by-name/ts/tskm/package.nix
index 3d320772..71ef7ed6 100644
--- a/pkgs/by-name/ts/tskm/package.nix
+++ b/pkgs/by-name/ts/tskm/package.nix
@@ -19,10 +19,6 @@ rustPlatform.buildRustPackage (finalAttrs: {
lockFile = ./Cargo.lock;
};
- env = {
- SHELL_COMPLETION_DIR = "./shell";
- };
-
buildInputs = [
taskwarrior3
git
@@ -38,9 +34,9 @@ rustPlatform.buildRustPackage (finalAttrs: {
postInstall = ''
installShellCompletion --cmd tskm \
- --bash ./shell/tskm.bash \
- --fish ./shell/tskm.fish \
- --zsh ./shell/_tskm
+ --bash <(COMPLETE=bash $out/bin/tskm) \
+ --fish <(COMPLETE=fish $out/bin/tskm) \
+ --zsh <(COMPLETE=zsh $out/bin/tskm)
# NOTE: We cannot clear the path, because we need access to the $EDITOR. <2025-04-04>
wrapProgram $out/bin/tskm \
diff --git a/pkgs/by-name/ts/tskm/src/cli.rs b/pkgs/by-name/ts/tskm/src/cli.rs
index 1c72b3c2..c1eba387 100644
--- a/pkgs/by-name/ts/tskm/src/cli.rs
+++ b/pkgs/by-name/ts/tskm/src/cli.rs
@@ -1,13 +1,13 @@
-use std::path::PathBuf;
+use std::{ffi::OsStr, path::PathBuf};
use anyhow::{bail, Result};
-use clap::{ArgAction, Parser, Subcommand};
+use clap::{builder::StyledStr, ArgAction, Parser, Subcommand};
+use clap_complete::{ArgValueCompleter, CompletionCandidate};
use url::Url;
use crate::{
interface::{input::Input, project::ProjectName},
- state::State,
- task,
+ state, task,
};
#[derive(Parser, Debug)]
@@ -17,8 +17,10 @@ use crate::{
/// `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 {
@@ -79,14 +81,14 @@ 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)]
+ #[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::new_ro()?;
+ 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!")
@@ -104,7 +106,7 @@ pub enum OpenCommand {
/// 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)]
+ #[arg(value_parser = task::Project::from_project_string, add = ArgValueCompleter::new(complete_project))]
project: task::Project,
/// The URL to open.
@@ -123,7 +125,7 @@ pub enum OpenCommand {
/// List all open tabs in the project.
ListTabs {
/// The project to open.
- #[arg(value_parser = task::Project::from_project_string)]
+ #[arg(value_parser = task::Project::from_project_string, add = ArgValueCompleter::new(complete_project))]
project: Option<task::Project>,
},
}
@@ -133,7 +135,10 @@ 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.
///
@@ -144,10 +149,118 @@ pub enum InputCommand {
/// 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,
}
+
+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
+}
diff --git a/pkgs/by-name/ts/tskm/src/main.rs b/pkgs/by-name/ts/tskm/src/main.rs
index f4416c6d..77f2dcca 100644
--- a/pkgs/by-name/ts/tskm/src/main.rs
+++ b/pkgs/by-name/ts/tskm/src/main.rs
@@ -1,8 +1,11 @@
use anyhow::Result;
-use clap::Parser;
-use state::State;
+use clap::{CommandFactory, Parser};
-use crate::interface::{input, neorg, open, project};
+use crate::{
+ cli::{CliArgs, Command},
+ interface::{input, neorg, open, project},
+ state::State,
+};
pub mod cli;
pub mod interface;
@@ -10,38 +13,9 @@ pub mod rofi;
pub mod state;
pub mod task;
-use crate::cli::{CliArgs, Command};
-
fn main() -> Result<(), anyhow::Error> {
- // TODO: Support these completions for the respective types <2025-04-04>
- //
- // ID_GENERATION_FUNCTION
- // ```sh
- // context="$(task _get rc.context)"
- // if [ "$context" ]; then
- // filter="project:$context"
- // else
- // filter="0-10000"
- // fi
- // tasks="$(task "$filter" _ids)"
- //
- // if [ "$tasks" ]; then
- // echo "$tasks" | xargs task _zshids | awk -F: -v q="'" '{gsub(/'\''/, q "\\" q q ); print $1 ":" q $2 q}'
- // fi
- // ```
- //
- // ARGUMENTS:
- // ID | *([0-9]) := [[%ID_GENERATION_FUNCTION]]
- // The function displays all possible IDs of the eligible tasks.
- //
- // WS := %ALL_WORKSPACES
- // All possible workspaces.
- //
- // P := %ALL_PROJECTS_PIPE
- // The possible project.
- //
- // F := [[fd . --max-depth 3]]
- // A URL-Input file to use as source.
+ clap_complete::CompleteEnv::with_factory(CliArgs::command).complete();
+
let args = CliArgs::parse();
stderrlog::new()
diff --git a/pkgs/by-name/ts/tskm/src/task/mod.rs b/pkgs/by-name/ts/tskm/src/task/mod.rs
index 03a12faa..989f273a 100644
--- a/pkgs/by-name/ts/tskm/src/task/mod.rs
+++ b/pkgs/by-name/ts/tskm/src/task/mod.rs
@@ -66,6 +66,14 @@ impl Task {
pub fn uuid(&self) -> &taskchampion::Uuid {
&self.uuid
}
+ #[must_use]
+ pub fn working_set_id(&self, state: &mut State) -> Result<usize> {
+ Ok(state
+ .replica()
+ .working_set()?
+ .by_uuid(self.uuid)
+ .expect("The task should be in the working set"))
+ }
fn as_task(&self, state: &mut State) -> Result<taskchampion::Task> {
Ok(state
@@ -121,7 +129,7 @@ impl Task {
.expect("Every task should have a project")
.to_owned()
};
- let project = Project::from_project_string(output.as_str())
+ let project = Project::from_project_string(output.as_str().trim())
.expect("This comes from tw, it should be valid");
Ok(project)
}