aboutsummaryrefslogtreecommitdiffstats
path: root/pkgs/by-name/ts/tskm/src/task
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-04-04 11:48:44 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-04-04 11:48:44 +0200
commit135d09bfb305d54cac1ba1fb9861d5b9309a7b3a (patch)
tree459109a40320530993ae560f55a730a72df31416 /pkgs/by-name/ts/tskm/src/task
parentrefactor(modules/legacy/firefox): Move to by-name (diff)
downloadnixos-config-135d09bfb305d54cac1ba1fb9861d5b9309a7b3a.zip
feat(pkgs/neorg): Rewrite in rust
This improves upon neorg by integrating it better into the system context.
Diffstat (limited to 'pkgs/by-name/ts/tskm/src/task')
-rw-r--r--pkgs/by-name/ts/tskm/src/task/mod.rs322
1 files changed, 322 insertions, 0 deletions
diff --git a/pkgs/by-name/ts/tskm/src/task/mod.rs b/pkgs/by-name/ts/tskm/src/task/mod.rs
new file mode 100644
index 00000000..c3a6d614
--- /dev/null
+++ b/pkgs/by-name/ts/tskm/src/task/mod.rs
@@ -0,0 +1,322 @@
+use std::{
+ fmt::Display,
+ fs::{self, read_to_string, File},
+ path::PathBuf,
+ process::Command,
+ str::FromStr,
+ sync::OnceLock,
+};
+
+use anyhow::{bail, Context, Result};
+use log::{debug, info, trace};
+
+use crate::interface::project::ProjectName;
+
+/// The `taskwarrior` id of a task.
+#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Ord, Eq)]
+pub struct Id {
+ id: u64,
+}
+impl Id {
+ /// # Errors
+ /// When `task` execution fails
+ pub fn get_current() -> Result<Option<Self>> {
+ // We have ensured that only one task may be active
+ let self_str = run_task(&["+ACTIVE", "_ids"])?;
+
+ if self_str.is_empty() {
+ Ok(None)
+ } else {
+ Self::from_str(&self_str).map(Some)
+ }
+ }
+
+ /// # Errors
+ /// When `task` execution fails
+ pub fn to_uuid(&self) -> Result<String> {
+ let uuid = run_task(&[self.to_string().as_str(), "uuids"])?;
+
+ Ok(uuid)
+ }
+
+ /// # Panics
+ /// When internal assertions fail.
+ /// # Errors
+ /// When `task` execution fails
+ pub fn annotate(&self, message: &str) -> Result<()> {
+ run_task(&["annotate", self.to_string().as_str(), "--", message])?;
+ Ok(())
+ }
+
+ /// # Panics
+ /// When internal assertions fail.
+ /// # Errors
+ /// When `task` execution fails
+ pub fn start(&self) -> Result<()> {
+ info!("Activating {self}");
+
+ let output = run_task(&["start", self.to_string().as_str()])?;
+ assert!(output.is_empty());
+ Ok(())
+ }
+ /// # Panics
+ /// When internal assertions fail.
+ /// # Errors
+ /// When `task` execution fails
+ pub fn stop(&self) -> Result<()> {
+ info!("Stopping {self}");
+
+ let output = run_task(&["stop", self.to_string().as_str()])?;
+ assert!(output.is_empty());
+ Ok(())
+ }
+
+ /// # Panics
+ /// When internal assertions fail.
+ /// # Errors
+ /// When `task` execution fails
+ pub fn description(&self) -> Result<String> {
+ let output = run_task(&["rc.context=none", "_zshids", self.to_string().as_str()])?;
+ let (id, desc) = output
+ .split_once(':')
+ .expect("The output should always contain one colon");
+ assert_eq!(id.parse::<Id>().expect("This should be a valid id"), *self);
+ Ok(desc.to_owned())
+ }
+
+ /// # Panics
+ /// When internal assertions fail.
+ /// # Errors
+ /// When `task` execution fails
+ pub fn project(&self) -> Result<Project> {
+ let output = run_task(&[
+ "rc.context=none",
+ "_get",
+ format!("{self}.project").as_str(),
+ ])?;
+ let project = Project::from_project_string(output.as_str())
+ .expect("This comes from tw, it should be valid");
+ Ok(project)
+ }
+}
+
+impl Display for Id {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.id.fmt(f)
+ }
+}
+
+impl FromStr for Id {
+ type Err = anyhow::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let id = u64::from_str(s)?;
+ Ok(Self { id })
+ }
+}
+
+/// A registered task Project
+#[derive(Debug, Clone, PartialEq)]
+pub struct Project {
+ /// The project name.
+ /// For example:
+ /// ```no_run
+ /// &["trinitrix", "testing", "infra"]
+ /// ```
+ name: Vec<String>,
+}
+
+static ALL_CACHE: OnceLock<Vec<Project>> = OnceLock::new();
+impl Project {
+ #[must_use]
+ pub fn to_project_display(&self) -> String {
+ self.name.join(".")
+ }
+ #[must_use]
+ pub fn to_context_display(&self) -> String {
+ self.name.join("_")
+ }
+
+ /// # Errors
+ /// - When the string does not encode a previously registered project.
+ /// - When the string does not adhere to the project syntax.
+ pub fn from_project_string(s: &str) -> Result<Self> {
+ Self::from_input(s, ProjectName::from_project)
+ }
+
+ /// # Errors
+ /// - When the string does not encode a previously registered project.
+ /// - When the string does not adhere to the context syntax.
+ pub fn from_context_string(s: &str) -> Result<Self> {
+ Self::from_input(s, ProjectName::from_context)
+ }
+
+ fn from_input<F>(s: &str, f: F) -> Result<Self>
+ where
+ F: Fn(&str) -> ProjectName,
+ {
+ if s.is_empty() {
+ bail!("Your project is empty")
+ }
+
+ let all = Self::all()?;
+ let me = Self::from_project_name_unchecked(&f(s));
+ if all.contains(&me) {
+ Ok(me)
+ } else {
+ bail!(
+ "Your project '{}' is not registered!",
+ me.to_project_display()
+ );
+ }
+ }
+ fn from_project_name_unchecked(pn: &ProjectName) -> Self {
+ Self {
+ name: pn.segments().to_owned(),
+ }
+ }
+
+ /// Return all known valid projects.
+ ///
+ /// # Errors
+ /// When file operations fail.
+ ///
+ /// # Panics
+ /// Only when internal assertions fail.
+ pub fn all<'a>() -> Result<&'a [Project]> {
+ // Inlined from `OnceLock::get_or_try_init`
+ {
+ let this = &ALL_CACHE;
+ let f = || {
+ let file = dirs::config_local_dir()
+ .expect("Should be some")
+ .join("tskm/projects.list");
+ let contents = read_to_string(&file)
+ .with_context(|| format!("Failed to read file: '{}'", file.display()))?;
+
+ Ok::<_, anyhow::Error>(
+ contents
+ .lines()
+ .map(|s| Self::from_project_name_unchecked(&ProjectName::from_project(s)))
+ .collect::<Vec<_>>(),
+ )
+ };
+
+ // Fast path check
+ // NOTE: We need to perform an acquire on the state in this method
+ // in order to correctly synchronize `LazyLock::force`. This is
+ // currently done by calling `self.get()`, which in turn calls
+ // `self.is_initialized()`, which in turn performs the acquire.
+ if let Some(value) = this.get() {
+ return Ok(value);
+ }
+
+ this.set(f()?).expect(
+ "This should always be able to take our value, as we initialize only once.",
+ );
+
+ Ok(this.get().expect("This was initialized"))
+ }
+ }
+
+ fn touch_dir(&self) -> PathBuf {
+ let lock_dir = dirs::data_dir()
+ .expect("Should be found")
+ .join("tskm/review");
+ lock_dir.join(format!("{}.opened", self.to_project_display()))
+ }
+
+ /// Mark this project as having been interacted with.
+ ///
+ /// # Errors
+ /// When IO operations fail.
+ pub fn touch(&self) -> Result<()> {
+ let lock_file = self.touch_dir();
+
+ File::create(&lock_file)
+ .with_context(|| format!("Failed to create lock_file at: {}", lock_file.display()))?;
+
+ Ok(())
+ }
+ /// Returns [`true`] if it was previously [`Self::touch`]ed.
+ #[must_use]
+ pub fn is_touched(&self) -> bool {
+ let lock_file = self.touch_dir();
+ lock_file.exists()
+ }
+ /// Mark this project as having not been interacted with.
+ ///
+ /// # Errors
+ /// When IO operations fail.
+ pub fn untouch(&self) -> Result<()> {
+ let lock_file = self.touch_dir();
+
+ fs::remove_file(&lock_file)
+ .with_context(|| format!("Failed to create lock_file at: {}", lock_file.display()))?;
+
+ Ok(())
+ }
+
+ /// # Errors
+ /// When `task` execution fails.
+ pub fn get_tasks(&self) -> Result<Vec<Id>> {
+ let output = run_task(&[
+ "rc.context=none",
+ format!("project:{}", self.to_project_display()).as_str(),
+ "_ids",
+ ])?;
+
+ if output.is_empty() {
+ Ok(vec![])
+ } else {
+ output
+ .lines()
+ .map(Id::from_str)
+ .collect::<Result<Vec<Id>>>()
+ }
+ }
+
+ /// # Errors
+ /// When `task` execution fails.
+ pub fn activate(&self) -> Result<()> {
+ debug!("Setting project {}", self.to_context_display());
+
+ run_task(&["context", self.to_context_display().as_str()]).map(|_| ())
+ }
+ /// # Errors
+ /// When `task` execution fails.
+ pub fn clear() -> Result<()> {
+ debug!("Clearing active project");
+
+ run_task(&["context", "none"]).map(|_| ())
+ }
+
+ /// # Errors
+ /// When `task` execution fails.
+ pub fn get_current() -> Result<Option<Self>> {
+ let self_str = run_task(&["_get", "rc.context"])?;
+
+ if self_str.is_empty() {
+ Ok(None)
+ } else {
+ Self::from_context_string(&self_str).map(Some)
+ }
+ }
+}
+
+pub(crate) fn run_task(args: &[&str]) -> Result<String> {
+ debug!("Running task command: `task {}`", args.join(" "));
+
+ let output = Command::new("task")
+ .args(args)
+ .output()
+ .with_context(|| format!("Failed to run `task {}`", args.join(" ")))?;
+
+ let stdout = String::from_utf8(output.stdout).context("Failed to read task output as utf8")?;
+ let stderr = String::from_utf8(output.stderr).context("Failed to read task output as utf8")?;
+
+ trace!("Output (stdout): '{}'", stdout.trim());
+ trace!("Output (stderr): '{}'", stderr.trim());
+
+ Ok(stdout.trim().to_owned())
+}