diff options
author | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2025-04-06 18:36:27 +0200 |
---|---|---|
committer | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2025-04-06 18:38:03 +0200 |
commit | a9db63802db2293ac4ee280394568b09f6feaa87 (patch) | |
tree | 32b00aa17fda1bf11bf87fdefa71b77d2bc44348 /pkgs/by-name/ts/tskm/src | |
parent | fix(modules/taskwarrior/mkHook): Use correct `grep` silencing argument (diff) | |
download | nixos-config-a9db63802db2293ac4ee280394568b09f6feaa87.zip |
feat(pkgs/tskm/task): Use taskchampion instead of run_task
Diffstat (limited to '')
-rw-r--r-- | pkgs/by-name/ts/tskm/src/cli.rs | 20 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/src/interface/neorg/handle.rs | 10 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/src/interface/open/handle.rs | 27 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/src/main.rs | 8 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/src/state.rs | 45 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/src/task/mod.rs | 176 |
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 |