about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorSoispha <soispha@vhack.eu>2023-10-01 17:08:33 +0200
committerSoispha <soispha@vhack.eu>2023-10-01 18:23:53 +0200
commit3a9a44daac682fcf57ee312788ffc14867c412f9 (patch)
treea7da1d6f0a6b9623d7b37c3877cf2a41f902cb6e
parentfeat(hm/pkgs/scrs/neorg): Add support for opening current task context (diff)
downloadnixos-config-3a9a44daac682fcf57ee312788ffc14867c412f9.tar.gz
nixos-config-3a9a44daac682fcf57ee312788ffc14867c412f9.zip
feat(hm/conf/taskwarrior): Add hook scripts
-rw-r--r--hm/soispha/conf/taskwarrior/default.nix22
-rw-r--r--hm/soispha/conf/taskwarrior/hooks/default.nix46
-rwxr-xr-xhm/soispha/conf/taskwarrior/hooks/scripts/on-add_enforce-policies.sh41
-rwxr-xr-xhm/soispha/conf/taskwarrior/hooks/scripts/on-modify_track-timewarrior.py68
-rwxr-xr-xhm/soispha/conf/taskwarrior/hooks/scripts/on-modify_track-total-active-time.py135
5 files changed, 306 insertions, 6 deletions
diff --git a/hm/soispha/conf/taskwarrior/default.nix b/hm/soispha/conf/taskwarrior/default.nix
index 78bcacd0..3d647482 100644
--- a/hm/soispha/conf/taskwarrior/default.nix
+++ b/hm/soispha/conf/taskwarrior/default.nix
@@ -1,4 +1,8 @@
 {...}: {
+  imports = [
+    ./hooks
+  ];
+
   programs.taskwarrior = let
     mkContext = project: {
       inherit (project) name;
@@ -13,6 +17,7 @@
     context = builtins.listToAttrs (builtins.map mkContext (import ./projects {}));
   in {
     enable = true;
+    colorTheme = ./nord.theme;
     config = {
       news.version = "2.6.0";
       complete.all.tags = true;
@@ -23,17 +28,22 @@
       regex = true;
       weekstart = "Monday";
       uda = {
-        neorg = {
-          type = "string";
-          label = "Neorg";
+        total_active_time = {
+          type = "duration";
+          label = "Total active time";
         };
       };
+      alias = {
+        a = "add";
+        bd = "burndown.daily";
+        cx = "context";
+        e = "edit";
+        mod = "modify";
+        n = "execute neorg --task";
+      };
       color = true;
 
       inherit context;
     };
-    extraConfig = ''
-      include ${./nord.theme}
-    '';
   };
 }
diff --git a/hm/soispha/conf/taskwarrior/hooks/default.nix b/hm/soispha/conf/taskwarrior/hooks/default.nix
new file mode 100644
index 00000000..9bd66c38
--- /dev/null
+++ b/hm/soispha/conf/taskwarrior/hooks/default.nix
@@ -0,0 +1,46 @@
+{
+  sysLib,
+  pkgs,
+  ...
+}: let
+  enforce_policies = sysLib.writeShellScriptWithLibrary {
+    name = "enforce_policies.taskwarrior-hook";
+    src = ./scripts/on-add_enforce-policies.sh;
+    dependencies = with pkgs; [dash jq taskwarrior gnused gnugrep];
+    replacementStrings = let
+      projects = builtins.map (project: project.name) (import ../projects {});
+      projects_newline = builtins.concatStringsSep "\n" projects;
+      projects_comma = builtins.concatStringsSep ", " projects;
+    in {
+      PROJECTS_NEWLINE = projects_newline;
+      PROJECTS_COMMA = projects_comma;
+    };
+  };
+  track_timewarrior = pkgs.stdenv.mkDerivation {
+    name = "track_timewarrior.taskwarrior-hook";
+    propagatedBuildInputs = [
+      pkgs.timewarrior
+    ];
+    dontUnpack = true;
+    installPhase = "install -Dm755 ${./scripts/on-modify_track-timewarrior.py} $out/bin/bin";
+  };
+  track_total_active_time = pkgs.stdenv.mkDerivation {
+    name = "track_total_active_time.taskwarrior-hook";
+    propagatedBuildInputs = [
+      (pkgs.python3.withPackages (pythonPackages:
+        with pythonPackages; [
+          taskw
+        ]))
+    ];
+    dontUnpack = true;
+    installPhase = "install -Dm755 ${./scripts/on-modify_track-total-active-time.py} $out/bin/bin";
+  };
+in {
+  xdg = {
+    dataFile = {
+      "task/hooks/on-add_enforce-policies".source = "${enforce_policies}/bin/enforce_policies.taskwarrior-hook";
+      "task/hooks/on-modify_track-timewarrior".source = "${track_timewarrior}/bin/bin";
+      "task/hooks/on-modify_track-total-active-time".source = "${track_total_active_time}/bin/bin";
+    };
+  };
+}
diff --git a/hm/soispha/conf/taskwarrior/hooks/scripts/on-add_enforce-policies.sh b/hm/soispha/conf/taskwarrior/hooks/scripts/on-add_enforce-policies.sh
new file mode 100755
index 00000000..59333a49
--- /dev/null
+++ b/hm/soispha/conf/taskwarrior/hooks/scripts/on-add_enforce-policies.sh
@@ -0,0 +1,41 @@
+#!/usr/bin/env dash
+
+# shellcheck source=/dev/null
+SHELL_LIBRARY_VERSION="1.4.2" . %SHELL_LIBRARY_PATH
+
+# override shell lib output to stdout
+eprint() {
+    # shellcheck disable=SC2317
+    print "$@";
+};
+eprintln() {
+    # shellcheck disable=SC2317
+    println "$@";
+};
+
+enable_hook_dbg() {
+    [ "$(task _get rc.debug.hooks)" -ge 1 ] && dbg_enable
+}
+
+enforce_project() {
+    project="$(jq '.project' "$(ptmp "$1")")";
+    [ "$project" = "null" ] && die "No project supplied!"
+
+    if grep "^$(echo "$project" | sed 's|"\([a-z_-]*\).*"|\1|')\$" "$(ptmp "%PROJECTS_NEWLINE")"; then
+        dbg "project('$project') is a valid part of %PROJECTS_COMMA";
+    else
+        die "The project '$(echo "$project" | sed 's|"||g')' is not registered with the nix config, registered projects: %PROJECTS_COMMA";
+    fi
+}
+
+
+read -r new_task;
+# We don't change the task, thus immediately return the json
+echo "$new_task";
+
+enable_hook_dbg;
+enforce_project "$new_task";
+
+exit 0;
+
+# vim: ft=sh
diff --git a/hm/soispha/conf/taskwarrior/hooks/scripts/on-modify_track-timewarrior.py b/hm/soispha/conf/taskwarrior/hooks/scripts/on-modify_track-timewarrior.py
new file mode 100755
index 00000000..63065754
--- /dev/null
+++ b/hm/soispha/conf/taskwarrior/hooks/scripts/on-modify_track-timewarrior.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2016-present Arctic Ice Studio <development@arcticicestudio.com>
+# Copyright (C) 2016-present Sven Greb <development@svengreb.de>
+
+# Project:    igloo
+# Repository: https://github.com/arcticicestudio/igloo
+# License:    MIT
+# References:
+#   https://taskwarrior.org/docs
+#   https://taskwarrior.org/docs/timewarrior
+#   timew(1)
+#   task(1)
+
+"""A Taskwarrior hook to track the time of a active task with Taskwarrior.
+
+This hook will extract all of the following for use as Timewarrior tags:
+
+* UUID
+* Project
+* Tags
+* Description
+* UDAs
+
+Note:
+    This hook requires Python 3 and is only compatible with Taskwarrior version greater or equal to 2.4!
+
+This hook is a fork from the `official on-modify.timewarrior hook`_.
+
+.. _`official on-modify.timewarrior hook`:
+   https://github.com/GothenburgBitFactory/timewarrior/blob/dev/ext/on-modify.timewarrior
+"""
+
+from sys import stdin
+from os import system
+from json import loads, dumps
+
+# Make no changes to the task, simply observe.
+old = loads(stdin.readline())
+new = loads(stdin.readline())
+print(dumps(new))
+
+# Extract attributes for use as tags.
+tags = [new["description"]]
+
+if "project" in new:
+    project = new["project"]
+    tags.append(project)
+    if "." in project:
+        tags.extend([tag for tag in project.split(".")])
+
+if "tags" in new:
+    tags.extend(new["tags"])
+
+combined = " ".join(["'%s'" % tag for tag in tags]).encode("utf-8").strip()
+
+# Task has been started.
+if "start" in new and not "start" in old:
+    system("timew start " + combined.decode() + " :yes")
+
+# Task has been stopped.
+elif not "start" in new and "start" in old:
+    system("timew stop " + combined.decode() + " :yes")
+
+# Any task that is active, with a non-pending status should not be tracked.
+elif "start" in new and new["status"] != "pending":
+    system("timew stop " + combined.decode() + " :yes")
diff --git a/hm/soispha/conf/taskwarrior/hooks/scripts/on-modify_track-total-active-time.py b/hm/soispha/conf/taskwarrior/hooks/scripts/on-modify_track-total-active-time.py
new file mode 100755
index 00000000..88234cbf
--- /dev/null
+++ b/hm/soispha/conf/taskwarrior/hooks/scripts/on-modify_track-total-active-time.py
@@ -0,0 +1,135 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2016-present Arctic Ice Studio <development@arcticicestudio.com>
+# Copyright (C) 2016-present Sven Greb <development@svengreb.de>
+
+# Project:    igloo
+# Repository: https://github.com/arcticicestudio/igloo
+# License:    MIT
+# References:
+#   https://taskwarrior.org/docs
+#   task(1)
+
+"""A Taskwarrior hook to track the total active time of a task.
+
+The tracked time is stored in a UDA task duration attribute named ``totalactivetime`` of type ``duration`` holding the total number of seconds the task was
+active. The tracked time can then be included in any report by adding the ``totalactivetime`` column.
+
+By default, this plugin allows to have one task active at a time. This can be changed by setting ``max_active_tasks`` in ``.taskrc`` to a value greater than
+``1``.
+
+Note:
+    This hook requires Python 3 and the `taskw`_ package to be installed which provides the python bindings for Taskwarrior!
+    Also note that this hook is only compatible with Taskwarrior version greater or equal to 2.4!
+
+This hook is a fork from `kostajh/taskwarrior-time-tracking-hook`_
+
+.. _taskw:
+   https://pypi.python.org/pypi/taskw
+.. _kostajh/taskwarrior-time-tracking-hook:
+   https://github.com/kostajh/taskwarrior-time-tracking-hook
+"""
+
+import datetime
+import json
+import re
+import sys
+import subprocess
+from taskw import TaskWarrior
+from typing import TypeVar
+
+TIME_FORMAT = "%Y%m%dT%H%M%SZ"
+UDA_KEY = "total_active_time"
+
+w = TaskWarrior(config_filename=sys.argv[4].replace("rc:", ""))
+config = w.load_config(config_filename=sys.argv[4].replace("rc:", ""))
+if "max_active_tasks" in config:
+    MAX_ACTIVE = int(config["max_active_tasks"])
+else:
+    MAX_ACTIVE = 1
+
+"""Compiled regular expression for the duration as ISO-8601 formatted string."""
+ISO8601DURATION = re.compile("P((\d*)Y)?((\d*)M)?((\d*)D)?T((\d*)H)?((\d*)M)?((\d*)S)?")
+
+"""The duration type either as integer (in seconds), as ISO-8601 formatted string ("PT1H10M31S") or the seconds suffixed with "seconds"."""
+DurationType = TypeVar("DurationType", str, int)
+
+
+def duration_str_to_time_delta(duration_str: DurationType) -> datetime.timedelta:
+    """Converts duration string into a timedelta object.
+
+    :param duration_str: The duration
+    :return: The duration as timedelta object
+    """
+    if duration_str.startswith("P"):
+        match = ISO8601DURATION.match(duration_str)
+        if match:
+            year = match.group(2)
+            month = match.group(4)
+            day = match.group(6)
+            hour = match.group(8)
+            minute = match.group(10)
+            second = match.group(12)
+            value = 0
+            if second:
+                value += int(second)
+            if minute:
+                value += int(minute) * 60
+            if hour:
+                value += int(hour) * 3600
+            if day:
+                value += int(day) * 3600 * 24
+            if month:
+                # Assume a month is 30 days for now.
+                value += int(month) * 3600 * 24 * 30
+            if year:
+                # Assume a year is 365 days for now.
+                value += int(year) * 3600 * 24 * 365
+        else:
+            value = int(duration_str)
+    elif duration_str.endswith("seconds"):
+        value = int(duration_str.rstrip("seconds"))
+    else:
+        value = int(duration_str)
+    return datetime.timedelta(seconds=value)
+
+
+def main():
+    original = json.loads(sys.stdin.readline())
+    modified = json.loads(sys.stdin.readline())
+
+    # An active task has just been started.
+    if "start" in modified and "start" not in original:
+        # Prevent this task from starting if "task +ACTIVE count" is greater than "MAX_ACTIVE".
+        p = subprocess.Popen(["task", "+ACTIVE", "status:pending", "count", "rc.verbose:off"], stdout=subprocess.PIPE)
+        out, err = p.communicate()
+        count = int(out.rstrip())
+        if count >= MAX_ACTIVE:
+            print("Only %d task(s) can be active at a time. "
+                  "See 'max_active_tasks' in .taskrc." % MAX_ACTIVE)
+            sys.exit(1)
+
+    # An active task has just been stopped.
+    if "start" in original and "start" not in modified:
+        # Calculate the elapsed time.
+        start = datetime.datetime.strptime(original["start"], TIME_FORMAT)
+        end = datetime.datetime.utcnow()
+
+        if UDA_KEY not in modified:
+            modified[UDA_KEY] = 0
+
+        this_duration = (end - start)
+        total_duration = (this_duration + duration_str_to_time_delta(str(modified[UDA_KEY])))
+        print("Total Time Tracked: %s (%s in this instance)" % (total_duration, this_duration))
+        modified[UDA_KEY] = str(int(total_duration.days * (60 * 60 * 24) + total_duration.seconds)) + "seconds"
+
+    return json.dumps(modified, separators=(",", ":"))
+
+
+def cmdline():
+    sys.stdout.write(main())
+
+
+if __name__ == "__main__":
+    cmdline()