diff options
author | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2024-05-23 13:31:11 +0200 |
---|---|---|
committer | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2024-05-23 13:33:40 +0200 |
commit | fd9b0ecef4142a62b45404700ba1cff488f84a73 (patch) | |
tree | ef6c1f74f05a2220a41ccff4b0890c39229f32f7 /modules/home/conf/taskwarrior/hooks | |
parent | refactor(pkgs): Categorize into `by-name` shards (diff) | |
download | nixos-config-fd9b0ecef4142a62b45404700ba1cff488f84a73.tar.gz nixos-config-fd9b0ecef4142a62b45404700ba1cff488f84a73.zip |
refactor(modules/home): Setup as "normal" NixOS module
Diffstat (limited to 'modules/home/conf/taskwarrior/hooks')
6 files changed, 480 insertions, 0 deletions
diff --git a/modules/home/conf/taskwarrior/hooks/default.nix b/modules/home/conf/taskwarrior/hooks/default.nix new file mode 100644 index 00000000..4bac0ca7 --- /dev/null +++ b/modules/home/conf/taskwarrior/hooks/default.nix @@ -0,0 +1,112 @@ +{ + sysLib, + pkgs, + lib, + config, + ... +}: let + mkProject = project: subproject: + if builtins.isString subproject + then { + name = "${project.name}.${subproject}"; + prefix = null; + } + else let + name = builtins.elemAt (builtins.attrNames subproject) 0; + in { + name = "${project.name}.${name}"; + subprojects = builtins.elemAt (builtins.attrValues subproject) 0; + prefix = null; + }; + + mkProjectName = project: + if builtins.hasAttr "subprojects" project + then + lib.lists.flatten ([project.name] + ++ (builtins.map mkProjectName + (builtins.map (mkProject project) project.subprojects))) + else [project.name]; + projects = lib.lists.unique (lib.lists.naturalSort (lib.lists.flatten (builtins.map mkProjectName (import ../projects {})))); + projects_newline = builtins.concatStringsSep "\n" projects; + projects_comma = builtins.concatStringsSep ", " projects; + projects_pipe = builtins.concatStringsSep "|" projects; + + enforce_policies = sysLib.writeShellScript { + name = "bin"; + src = ./scripts/on-add_enforce-policies.sh; + dependencies = with pkgs; [dash jq taskwarrior gnused gnugrep]; + replacementStrings = { + PROJECTS_NEWLINE = projects_newline; + PROJECTS_COMMA = projects_comma; + }; + }; + track_timewarrior = pkgs.stdenv.mkDerivation { + name = "track_timewarrior.taskwarrior-hook"; + nativeBuildInputs = [ + pkgs.makeWrapper + ]; + buildInputs = [ + pkgs.timewarrior + pkgs.taskwarrior + (pkgs.python3.withPackages (pythonPackages: + with pythonPackages; [ + taskw + ])) + ]; + dontUnpack = true; + installPhase = '' + install -Dm755 ${./scripts/on-modify_track-timewarrior.py} $out/bin/bin + wrapProgram $out/bin/bin \ + --prefix PATH : ${lib.makeBinPath [pkgs.taskwarrior pkgs.timewarrior]} + ''; + }; + track_total_active_time = pkgs.stdenv.mkDerivation { + name = "track_total_active_time.taskwarrior-hook"; + nativeBuildInputs = [ + pkgs.makeWrapper + ]; + buildInputs = [ + pkgs.taskwarrior + (pkgs.python3.withPackages (pythonPackages: + with pythonPackages; [ + taskw + ])) + ]; + dontUnpack = true; + installPhase = '' + install -Dm755 ${./scripts/on-modify_track-total-active-time.py} $out/bin/bin + wrapProgram $out/bin/bin \ + --prefix PATH : ${lib.makeBinPath [pkgs.taskwarrior]} + ''; + }; + + mkSyncGitRepo = type: { + name = "${hookPath}/${type}_sync-git-repo"; + value = { + source = "${sysLib.writeShellScript { + name = "bin"; + src = ./scripts + "/${type}_sync-git-repo.sh"; + dependencies = with pkgs; [dash taskwarrior git]; + }}/bin/bin"; + }; + }; + sync_git_repos = + builtins.listToAttrs (builtins.map mkSyncGitRepo ["on-add" "on-modify"]); + hookPath = config.programs.taskwarrior.config.hooks.location; +in { + options.soispha.taskwarrior.projects = lib.mkOption { + type = lib.types.attrs; + }; + config = { + soispha.taskwarrior.projects = { + inherit projects_newline projects_comma projects projects_pipe; + }; + home.file = + { + "${hookPath}/on-add_enforce-policies".source = "${enforce_policies}/bin/bin"; + "${hookPath}/on-modify_track-timewarrior".source = "${track_timewarrior}/bin/bin"; + "${hookPath}/on-modify_track-total-active-time".source = "${track_total_active_time}/bin/bin"; + } + // sync_git_repos; + }; +} diff --git a/modules/home/conf/taskwarrior/hooks/scripts/on-add_enforce-policies.sh b/modules/home/conf/taskwarrior/hooks/scripts/on-add_enforce-policies.sh new file mode 100755 index 00000000..eaf7f30c --- /dev/null +++ b/modules/home/conf/taskwarrior/hooks/scripts/on-add_enforce-policies.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env dash + +# shellcheck source=/dev/null +SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH + +# override shell lib output to stdout +eprint() { + # shellcheck disable=SC2317 + print "$@" +} +eprintln() { + # shellcheck disable=SC2317 + println "$@" +} + +enable_hook_dbg() { + debug_hooks="$(task _get rc.debug.hooks)" + [ "$debug_hooks" ] && [ "$debug_hooks" -ge 1 ] && dbg_enable +} + +enforce_project() { + project="$(jq '.project' "$(ptmp "$1")")" + [ "$project" = "null" ] && die "No project supplied!" + + if grep -q "^$(echo "$project" | sed 's|"\(.*\)"|\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/modules/home/conf/taskwarrior/hooks/scripts/on-add_sync-git-repo.sh b/modules/home/conf/taskwarrior/hooks/scripts/on-add_sync-git-repo.sh new file mode 100755 index 00000000..dadc96b0 --- /dev/null +++ b/modules/home/conf/taskwarrior/hooks/scripts/on-add_sync-git-repo.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env dash + +# shellcheck source=/dev/null +SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH + +# override shell lib output to stdout +eprint() { + # shellcheck disable=SC2317 + print "$@" +} +eprintln() { + # shellcheck disable=SC2317 + println "$@" +} + +enable_hook_dbg() { + debug_hooks="$(task _get rc.debug.hooks)" + [ "$debug_hooks" ] && [ "$debug_hooks" -ge 1 ] && dbg_enable +} + +update_git_repo() { + task_data="$(task _get rc.data.location)" + [ "$task_data" ] || die "Taskwarrior should have a location set" + + cd "$task_data" || die "(BUG?): Your data.location path is not accessable" + + [ -d ./.git/ ] || git init + + git add . + git commit --message="chore: Update" --no-gpg-sign +} + +read -r new_task +# We don't change the task, thus immediately return the json +echo "$new_task" + +enable_hook_dbg +update_git_repo + +exit 0 + +# vim: ft=sh diff --git a/modules/home/conf/taskwarrior/hooks/scripts/on-modify_sync-git-repo.sh b/modules/home/conf/taskwarrior/hooks/scripts/on-modify_sync-git-repo.sh new file mode 100755 index 00000000..25813e46 --- /dev/null +++ b/modules/home/conf/taskwarrior/hooks/scripts/on-modify_sync-git-repo.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env dash + +# shellcheck source=/dev/null +SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH + +# override shell lib output to stdout +eprint() { + # shellcheck disable=SC2317 + print "$@" +} +eprintln() { + # shellcheck disable=SC2317 + println "$@" +} + +enable_hook_dbg() { + debug_hooks="$(task _get rc.debug.hooks)" + [ "$debug_hooks" ] && [ "$debug_hooks" -ge 1 ] && dbg_enable +} + +update_git_repo() { + task_data="$(task _get rc.data.location)" + [ "$task_data" ] || die "Taskwarrior should have a location set" + + cd "$task_data" || die "(BUG?): Your data.location path is not accessable" + + [ -d ./.git/ ] || git init + + git add . + git commit --message="chore: Update" --no-gpg-sign +} + +read -r _old_task +read -r new_task +# We don't change the task, thus immediately return the json +echo "$new_task" + +enable_hook_dbg +update_git_repo + +exit 0 + +# vim: ft=sh diff --git a/modules/home/conf/taskwarrior/hooks/scripts/on-modify_track-timewarrior.py b/modules/home/conf/taskwarrior/hooks/scripts/on-modify_track-timewarrior.py new file mode 100755 index 00000000..b482af6a --- /dev/null +++ b/modules/home/conf/taskwarrior/hooks/scripts/on-modify_track-timewarrior.py @@ -0,0 +1,94 @@ +#!/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 +""" + +import subprocess +import sys +from json import loads, dumps +from os import system +from sys import stdin +from taskw import TaskWarrior + +# Make no changes to the task, simply observe. +old = loads(stdin.readline()) +new = loads(stdin.readline()) +print(dumps(new)) + + +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 + + +# 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 "start" not in old: + # 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) + + system("timew start " + combined.decode() + " :yes") + +# Task has been stopped. +elif "start" not 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/modules/home/conf/taskwarrior/hooks/scripts/on-modify_track-total-active-time.py b/modules/home/conf/taskwarrior/hooks/scripts/on-modify_track-total-active-time.py new file mode 100755 index 00000000..d5b380d0 --- /dev/null +++ b/modules/home/conf/taskwarrior/hooks/scripts/on-modify_track-total-active-time.py @@ -0,0 +1,148 @@ +#!/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() |