aboutsummaryrefslogtreecommitdiffstats
path: root/pkgs/by-name/ts/tskm/src
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-04-06 18:36:27 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-04-06 18:38:03 +0200
commita9db63802db2293ac4ee280394568b09f6feaa87 (patch)
tree32b00aa17fda1bf11bf87fdefa71b77d2bc44348 /pkgs/by-name/ts/tskm/src
parentfix(modules/taskwarrior/mkHook): Use correct `grep` silencing argument (diff)
downloadnixos-config-a9db63802db2293ac4ee280394568b09f6feaa87.zip
feat(pkgs/tskm/task): Use taskchampion instead of run_task
Diffstat (limited to 'pkgs/by-name/ts/tskm/src')
-rw-r--r--pkgs/by-name/ts/tskm/src/cli.rs20
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/neorg/handle.rs10
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/open/handle.rs27
-rw-r--r--pkgs/by-name/ts/tskm/src/main.rs8
-rw-r--r--pkgs/by-name/ts/tskm/src/state.rs45
-rw-r--r--pkgs/by-name/ts/tskm/src/task/mod.rs176
6 files changed, 185 insertions, 101 deletions
diff --git a/pkgs/by-name/ts/tskm/src/cli.rs b/pkgs/by-name/ts/tskm/src/cli.rs
index 99c8693e..bc79866a 100644
--- a/pkgs/by-name/ts/tskm/src/cli.rs
+++ b/pkgs/by-name/ts/tskm/src/cli.rs
@@ -1,9 +1,11 @@
use std::path::PathBuf;
-use clap::{Parser, Subcommand};
+use anyhow::{bail, Result};
+use clap::{ArgAction, Parser, Subcommand};
use crate::{
interface::{input::Input, project::ProjectName},
+ state::State,
task,
};
@@ -66,7 +68,21 @@ 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_parser = task_from_working_set_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 Some(task) = task::Task::from_working_set(id, &mut state)? else {
+ bail!("Working set id '{id}' is not valid!")
+ };
+ Ok(task)
}
#[derive(Subcommand, Debug)]
diff --git a/pkgs/by-name/ts/tskm/src/interface/neorg/handle.rs b/pkgs/by-name/ts/tskm/src/interface/neorg/handle.rs
index a9a46ee7..45e1f916 100644
--- a/pkgs/by-name/ts/tskm/src/interface/neorg/handle.rs
+++ b/pkgs/by-name/ts/tskm/src/interface/neorg/handle.rs
@@ -7,12 +7,12 @@ use std::{
use anyhow::{bail, Result};
-use crate::cli::NeorgCommand;
+use crate::{cli::NeorgCommand, state::State};
-pub fn handle(command: NeorgCommand) -> Result<()> {
+pub fn handle(command: NeorgCommand, state: &mut State) -> Result<()> {
match command {
NeorgCommand::Task { id } => {
- let project = id.project()?;
+ let project = id.project(state)?;
let path = dirs::data_local_dir()
.expect("This should exists")
.join("notes")
@@ -36,7 +36,7 @@ pub fn handle(command: NeorgCommand) -> Result<()> {
.args([
path.to_str().expect("Should be a utf-8 str"),
"-c",
- format!("/% {}", id.to_uuid()?).as_str(),
+ format!("/% {}", id.uuid()).as_str(),
])
.status()?;
if !status.success() {
@@ -71,7 +71,7 @@ pub fn handle(command: NeorgCommand) -> Result<()> {
}
{
- id.annotate("[neorg data]")?;
+ id.mark_neorg_data(state)?;
}
}
}
diff --git a/pkgs/by-name/ts/tskm/src/interface/open/handle.rs b/pkgs/by-name/ts/tskm/src/interface/open/handle.rs
index dc0d165d..0b565abd 100644
--- a/pkgs/by-name/ts/tskm/src/interface/open/handle.rs
+++ b/pkgs/by-name/ts/tskm/src/interface/open/handle.rs
@@ -3,15 +3,15 @@ use std::process;
use anyhow::{bail, Context, Result};
use log::{error, info};
-use crate::{cli::OpenCommand, rofi, task};
+use crate::{cli::OpenCommand, rofi, state::State, task};
-pub fn handle(command: OpenCommand) -> Result<()> {
+pub fn handle(command: OpenCommand, state: &mut State) -> Result<()> {
match command {
OpenCommand::Review => {
for project in task::Project::all().context("Failed to get all project files")? {
if project.is_touched() {
info!("Reviewing project: '{}'", project.to_project_display());
- open_in_browser(project).with_context(|| {
+ open_in_browser(project, state).with_context(|| {
format!(
"Failed to open project ('{}') in Firefox",
project.to_project_display()
@@ -38,7 +38,7 @@ pub fn handle(command: OpenCommand) -> Result<()> {
};
project.touch().context("Failed to touch project")?;
- open_in_browser(&project).with_context(|| {
+ open_in_browser(&project, state).with_context(|| {
format!("Failed to open project: {}", project.to_project_display())
})?;
}
@@ -60,7 +60,7 @@ pub fn handle(command: OpenCommand) -> Result<()> {
.touch()
.context("Failed to touch project")?;
- open_in_browser(&selected_project).context("Failed to open project")?;
+ open_in_browser(&selected_project, state).context("Failed to open project")?;
}
OpenCommand::ListTabs { project } => {
let project = if let Some(p) = project {
@@ -109,12 +109,11 @@ pub fn handle(command: OpenCommand) -> Result<()> {
Ok(())
}
-fn open_in_browser(selected_project: &task::Project) -> Result<()> {
+fn open_in_browser(selected_project: &task::Project, state: &mut State) -> Result<()> {
let old_project: Option<task::Project> =
task::Project::get_current().context("Failed to get currently active project")?;
- // We have ensured that only one task may be active
- let old_task: Option<task::Id> =
- task::Id::get_current().context("Failed to get currently active task")?;
+ let old_task: Option<task::Task> =
+ task::Task::get_current(state).context("Failed to get currently active task")?;
selected_project.activate().with_context(|| {
format!(
@@ -124,7 +123,7 @@ fn open_in_browser(selected_project: &task::Project) -> Result<()> {
})?;
let tracking_task = {
- let all_tasks = selected_project.get_tasks().with_context(|| {
+ let all_tasks = selected_project.get_tasks(state).with_context(|| {
format!(
"Failed to get assoctiated tasks for project: '{}'",
selected_project.to_project_display()
@@ -132,7 +131,7 @@ fn open_in_browser(selected_project: &task::Project) -> Result<()> {
})?;
let tracking_task = all_tasks.into_iter().find(|t| {
- let maybe_desc = t.description();
+ let maybe_desc = t.description(state);
if let Ok(desc) = maybe_desc {
desc == "tracking"
} else {
@@ -149,7 +148,7 @@ fn open_in_browser(selected_project: &task::Project) -> Result<()> {
"Starting task {} -> tracking",
selected_project.to_project_display()
);
- task.start()
+ task.start(state)
.with_context(|| format!("Failed to start task {task}"))?;
}
tracking_task
@@ -169,11 +168,11 @@ fn open_in_browser(selected_project: &task::Project) -> Result<()> {
}
if let Some(task) = tracking_task {
- task.stop()
+ task.stop(state)
.with_context(|| format!("Failed to stop task {task}"))?;
}
if let Some(task) = old_task {
- task.start()
+ task.start(state)
.with_context(|| format!("Failed to start task {task}"))?;
}
diff --git a/pkgs/by-name/ts/tskm/src/main.rs b/pkgs/by-name/ts/tskm/src/main.rs
index 7fc9c0d4..6e506895 100644
--- a/pkgs/by-name/ts/tskm/src/main.rs
+++ b/pkgs/by-name/ts/tskm/src/main.rs
@@ -3,12 +3,14 @@
use anyhow::Result;
use clap::Parser;
+use state::State;
use crate::interface::{input, neorg, open, project};
pub mod cli;
pub mod interface;
pub mod rofi;
+pub mod state;
pub mod task;
use crate::cli::{CliArgs, Command};
@@ -55,10 +57,12 @@ fn main() -> Result<(), anyhow::Error> {
.init()
.expect("Let's just hope that this does not panic");
+ let mut state = State::new_rw()?;
+
match args.command {
Command::Inputs { command } => input::handle(command)?,
- Command::Neorg { command } => neorg::handle(command)?,
- Command::Open { command } => open::handle(command)?,
+ Command::Neorg { command } => neorg::handle(command, &mut state)?,
+ Command::Open { command } => open::handle(command, &mut state)?,
Command::Projects { command } => project::handle(command)?,
}
diff --git a/pkgs/by-name/ts/tskm/src/state.rs b/pkgs/by-name/ts/tskm/src/state.rs
new file mode 100644
index 00000000..175a7f03
--- /dev/null
+++ b/pkgs/by-name/ts/tskm/src/state.rs
@@ -0,0 +1,45 @@
+use std::path::PathBuf;
+
+use anyhow::Result;
+use taskchampion::{storage::AccessMode, Replica, StorageConfig};
+
+pub struct State {
+ replica: Replica,
+}
+
+impl std::fmt::Debug for State {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "State")
+ }
+}
+
+impl State {
+ fn taskdb_dir() -> PathBuf {
+ dirs::data_local_dir().expect("Should exist").join("task")
+ }
+
+ fn new(taskdb_dir: PathBuf, access_mode: AccessMode) -> Result<Self> {
+ let storage = StorageConfig::OnDisk {
+ taskdb_dir,
+ create_if_missing: false,
+ access_mode,
+ }
+ .into_storage()?;
+
+ let replica = Replica::new(storage);
+
+ Ok(Self { replica })
+ }
+
+ pub fn new_ro() -> Result<Self> {
+ Self::new(Self::taskdb_dir(), AccessMode::ReadOnly)
+ }
+ pub fn new_rw() -> Result<Self> {
+ Self::new(Self::taskdb_dir(), AccessMode::ReadWrite)
+ }
+
+ #[must_use]
+ pub fn replica(&mut self) -> &mut Replica {
+ &mut self.replica
+ }
+}
diff --git a/pkgs/by-name/ts/tskm/src/task/mod.rs b/pkgs/by-name/ts/tskm/src/task/mod.rs
index c3a6d614..03a12faa 100644
--- a/pkgs/by-name/ts/tskm/src/task/mod.rs
+++ b/pkgs/by-name/ts/tskm/src/task/mod.rs
@@ -9,109 +9,136 @@ use std::{
use anyhow::{bail, Context, Result};
use log::{debug, info, trace};
+use taskchampion::Tag;
-use crate::interface::project::ProjectName;
+use crate::{interface::project::ProjectName, state::State};
/// The `taskwarrior` id of a task.
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Ord, Eq)]
-pub struct Id {
- id: u64,
+pub struct Task {
+ uuid: taskchampion::Uuid,
}
-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)
+impl From<&taskchampion::Task> for Task {
+ fn from(value: &taskchampion::Task) -> Self {
+ Self {
+ uuid: value.get_uuid(),
+ }
+ }
+}
+impl From<&taskchampion::TaskData> for Task {
+ fn from(value: &taskchampion::TaskData) -> Self {
+ Self {
+ uuid: value.get_uuid(),
+ }
+ }
+}
+
+impl Task {
+ pub fn from_working_set(id: usize, state: &mut State) -> Result<Option<Self>> {
+ Ok(state
+ .replica()
+ .working_set()?
+ .by_index(id)
+ .map(|uuid| Self { uuid }))
+ }
+
+ pub fn get_current(state: &mut State) -> Result<Option<Self>> {
+ let tasks = state
+ .replica()
+ .pending_tasks()?
+ .into_iter()
+ .filter(taskchampion::Task::is_active)
+ .collect::<Vec<_>>();
+
+ assert!(
+ tasks.len() <= 1,
+ "We have ensured that only one task may be active, via a hook"
+ );
+ if let Some(active) = tasks.first() {
+ Ok(Some(Self::from(active)))
} else {
- Self::from_str(&self_str).map(Some)
+ Ok(None)
}
}
- /// # Errors
- /// When `task` execution fails
- pub fn to_uuid(&self) -> Result<String> {
- let uuid = run_task(&[self.to_string().as_str(), "uuids"])?;
+ #[must_use]
+ pub fn uuid(&self) -> &taskchampion::Uuid {
+ &self.uuid
+ }
- Ok(uuid)
+ fn as_task(&self, state: &mut State) -> Result<taskchampion::Task> {
+ Ok(state
+ .replica()
+ .get_task(self.uuid)?
+ .expect("We have the task from this replica, it should still be in it"))
}
- /// # 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])?;
+ /// Adds a tag to the task, to show the user that it has additional neorg data.
+ pub fn mark_neorg_data(&self, state: &mut State) -> Result<()> {
+ let mut ops = vec![];
+ self.as_task(state)?
+ .add_tag(&Tag::from_str("neorg_data").expect("Is valid"), &mut ops)?;
+ state.replica().commit_operations(ops)?;
Ok(())
}
- /// # Panics
- /// When internal assertions fail.
- /// # Errors
- /// When `task` execution fails
- pub fn start(&self) -> Result<()> {
+ /// Try to start this task.
+ /// It will stop previously active tasks.
+ pub fn start(&self, state: &mut State) -> Result<()> {
info!("Activating {self}");
- let output = run_task(&["start", self.to_string().as_str()])?;
- assert!(output.is_empty());
+ if let Some(active) = Self::get_current(state)? {
+ active.stop(state)?;
+ }
+
+ let mut ops = vec![];
+ self.as_task(state)?.start(&mut ops)?;
+ state.replica().commit_operations(ops)?;
Ok(())
}
- /// # Panics
- /// When internal assertions fail.
- /// # Errors
- /// When `task` execution fails
- pub fn stop(&self) -> Result<()> {
+
+ /// Stops this task.
+ pub fn stop(&self, state: &mut State) -> Result<()> {
info!("Stopping {self}");
- let output = run_task(&["stop", self.to_string().as_str()])?;
- assert!(output.is_empty());
+ let mut ops = vec![];
+ self.as_task(state)?.stop(&mut ops)?;
+ state.replica().commit_operations(ops)?;
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())
+ pub fn description(&self, state: &mut State) -> Result<String> {
+ Ok(self.as_task(state)?.get_description().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(),
- ])?;
+ pub fn project(&self, state: &mut State) -> Result<Project> {
+ let output = {
+ let task = self.as_task(state)?;
+ let task_data = task.into_task_data();
+ task_data
+ .get("project")
+ .expect("Every task should have a project")
+ .to_owned()
+ };
let project = Project::from_project_string(output.as_str())
.expect("This comes from tw, it should be valid");
Ok(project)
}
}
-impl Display for Id {
+impl Display for Task {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- self.id.fmt(f)
+ self.uuid.fmt(f)
}
}
-impl FromStr for Id {
+impl FromStr for Task {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
- let id = u64::from_str(s)?;
- Ok(Self { id })
+ let uuid = taskchampion::Uuid::from_str(s)?;
+ Ok(Self { uuid })
}
}
@@ -259,21 +286,14 @@ impl Project {
/// # 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>>>()
- }
+ pub fn get_tasks(&self, state: &mut State) -> Result<Vec<Task>> {
+ Ok(state
+ .replica()
+ .pending_task_data()?
+ .into_iter()
+ .filter(|t| t.get("project").expect("Is set") == self.to_project_display())
+ .map(|t| Task::from(&t))
+ .collect())
}
/// # Errors