diff options
author | Soispha <soispha@vhack.eu> | 2023-10-01 17:08:33 +0200 |
---|---|---|
committer | Soispha <soispha@vhack.eu> | 2023-10-01 18:23:53 +0200 |
commit | 3a9a44daac682fcf57ee312788ffc14867c412f9 (patch) | |
tree | a7da1d6f0a6b9623d7b37c3877cf2a41f902cb6e | |
parent | feat(hm/pkgs/scrs/neorg): Add support for opening current task context (diff) | |
download | nixos-config-3a9a44daac682fcf57ee312788ffc14867c412f9.tar.gz nixos-config-3a9a44daac682fcf57ee312788ffc14867c412f9.zip |
feat(hm/conf/taskwarrior): Add hook scripts
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() |