aboutsummaryrefslogtreecommitdiffstats
path: root/modules/by-name/ti/timewarrior
diff options
context:
space:
mode:
Diffstat (limited to 'modules/by-name/ti/timewarrior')
-rw-r--r--modules/by-name/ti/timewarrior/module.nix97
-rw-r--r--modules/by-name/ti/timewarrior/nord.theme25
-rw-r--r--modules/by-name/ti/timewarrior/nord.theme.license9
-rwxr-xr-xmodules/by-name/ti/timewarrior/taskwarrior_hooks/on-modify.track-timewarrior.py109
-rwxr-xr-xmodules/by-name/ti/timewarrior/taskwarrior_hooks/on-modify.track-total-active-time.py120
5 files changed, 360 insertions, 0 deletions
diff --git a/modules/by-name/ti/timewarrior/module.nix b/modules/by-name/ti/timewarrior/module.nix
new file mode 100644
index 00000000..90a541d8
--- /dev/null
+++ b/modules/by-name/ti/timewarrior/module.nix
@@ -0,0 +1,97 @@
+# nixos-config - My current NixOS configuration
+#
+# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# This file is part of my nixos-config.
+#
+# You should have received a copy of the License along with this program.
+# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+{
+ config,
+ lib,
+ pkgs,
+ ...
+}: let
+ cfg = config.soispha.programs.timewarrior;
+
+ track_timewarrior = pkgs.stdenv.mkDerivation {
+ name = "track-timewarrior";
+ nativeBuildInputs = [
+ pkgs.makeWrapper
+ ];
+ buildInputs = [
+ pkgs.timewarrior
+ pkgs.taskwarrior3
+ pkgs.python3
+ ];
+ dontUnpack = true;
+ installPhase = ''
+ install -Dm755 ${./taskwarrior_hooks/on-modify.track-timewarrior.py} $out/bin/track-timewarrior
+ wrapProgram $out/bin/track-timewarrior \
+ --set PATH ${lib.makeBinPath [pkgs.taskwarrior3 pkgs.timewarrior]}
+ '';
+
+ meta.mainProgram = "track-timewarrior";
+ };
+ track_total_active_time = pkgs.stdenv.mkDerivation {
+ name = "track-total-active-time";
+ nativeBuildInputs = [
+ pkgs.makeWrapper
+ ];
+ buildInputs = [
+ pkgs.taskwarrior3
+ pkgs.python3
+ ];
+ dontUnpack = true;
+ installPhase = ''
+ install -Dm755 ${./taskwarrior_hooks/on-modify.track-total-active-time.py} $out/bin/track-total-active-time
+ wrapProgram $out/bin/track-total-active-time \
+ --set PATH ${lib.makeBinPath [pkgs.taskwarrior3]}
+ '';
+
+ meta.mainProgram = "track-total-active-time";
+ };
+in {
+ options.soispha.programs.timewarrior = {
+ enable = lib.mkEnableOption "timewarrior";
+ };
+ config = lib.mkIf cfg.enable {
+ soispha.programs.taskwarrior = {
+ hooks = {
+ track-timewarrior = {
+ mode = "on-modify";
+ executable = track_timewarrior;
+ };
+ track-total-active-time = {
+ mode = "on-modify";
+ executable = track_total_active_time;
+ };
+ };
+ };
+
+ home-manager.users.soispha = {
+ home.packages = [
+ pkgs.timewarrior
+ ];
+
+ xdg.configFile."timewarrior/timewarrior.cfg".text = ''
+ # source: https://github.com/arcticicestudio/igloo
+ #+----+
+ #+ UI +
+ #+----+
+ import ${./nord.theme}
+ color = true
+
+ #+---------+
+ #+ Reports +
+ #+---------+
+ define reports:
+ day:
+ lines = 10
+ month = true
+ week = true
+ '';
+ };
+ };
+}
diff --git a/modules/by-name/ti/timewarrior/nord.theme b/modules/by-name/ti/timewarrior/nord.theme
new file mode 100644
index 00000000..da3c387a
--- /dev/null
+++ b/modules/by-name/ti/timewarrior/nord.theme
@@ -0,0 +1,25 @@
+# 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/timewarrior/themes.html
+# timew(1)
+
+define theme:
+ description = "An arctic, north-bluish clean and elegant Timewarrior theme."
+ colors:
+ exclusion = "bold black"
+ today = "cyan"
+ holiday = "bold blue on white"
+ label = "bold white on black"
+ ids = "bold black on cyan"
+ debug = "magenta"
+
+ # Rotating color palette for tags.
+ palette:
+ color01 = "bold black on cyan"
+ color02 = "bold black on bright cyan"
+ color03 = "bold black on blue"
diff --git a/modules/by-name/ti/timewarrior/nord.theme.license b/modules/by-name/ti/timewarrior/nord.theme.license
new file mode 100644
index 00000000..eae6a84c
--- /dev/null
+++ b/modules/by-name/ti/timewarrior/nord.theme.license
@@ -0,0 +1,9 @@
+nixos-config - My current NixOS configuration
+
+Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+SPDX-License-Identifier: GPL-3.0-or-later
+
+This file is part of my nixos-config.
+
+You should have received a copy of the License along with this program.
+If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
diff --git a/modules/by-name/ti/timewarrior/taskwarrior_hooks/on-modify.track-timewarrior.py b/modules/by-name/ti/timewarrior/taskwarrior_hooks/on-modify.track-timewarrior.py
new file mode 100755
index 00000000..3b42b3f2
--- /dev/null
+++ b/modules/by-name/ti/timewarrior/taskwarrior_hooks/on-modify.track-timewarrior.py
@@ -0,0 +1,109 @@
+#!/usr/bin/env python3
+
+# nixos-config - My current NixOS configuration
+#
+# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+# Copyright 2016 - 2021, 2023, Gothenburg Bit Factory
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# This file is part of my nixos-config.
+#
+# You should have received a copy of the License along with this program.
+# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+import json
+import subprocess
+import sys
+
+# Hook should extract all the following for use as Timewarrior tags:
+# UUID
+# Project
+# Tags
+# Description
+# UDAs
+
+try:
+ input_stream = sys.stdin.buffer
+except AttributeError:
+ input_stream = sys.stdin
+
+
+MAX_ACTIVE = 1
+
+
+def extract_tags_from(json_obj) -> [str]:
+ # Extract attributes for use as tags.
+ tags = [json_obj["description"]]
+
+ if "project" in json_obj:
+ tags.append(json_obj["project"])
+
+ if "tags" in json_obj:
+ if type(json_obj["tags"]) is str:
+ # Usage of tasklib (e.g. in taskpirate) converts the tag list into a string
+ # If this is the case, convert it back into a list first
+ # See https://github.com/tbabej/taskpirate/issues/11
+ tags.extend(json_obj["tags"].split(","))
+ else:
+ tags.extend(json_obj["tags"])
+
+ return tags
+
+
+def extract_annotation_from(json_obj):
+ if "annotations" not in json_obj:
+ return "''"
+
+ return json_obj["annotations"][0]["description"]
+
+
+def main(old, new):
+ start_or_stop = ""
+
+ # Started task.
+ 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(
+ f"Only {MAX_ACTIVE} task(s) can be active at a time.",
+ )
+ sys.exit(1)
+ else:
+ start_or_stop = "start"
+
+ # Stopped task.
+ elif ("start" not in new or "end" in new) and "start" in old:
+ start_or_stop = "stop"
+
+ if start_or_stop:
+ tags = extract_tags_from(new)
+
+ subprocess.call(["timew", start_or_stop] + tags + [":yes"])
+
+ # Modifications to task other than start/stop
+ elif "start" in new and "start" in old:
+ old_tags = extract_tags_from(old)
+ new_tags = extract_tags_from(new)
+
+ if old_tags != new_tags:
+ subprocess.call(["timew", "untag", "@1"] + old_tags + [":yes"])
+ subprocess.call(["timew", "tag", "@1"] + new_tags + [":yes"])
+
+ old_annotation = extract_annotation_from(old)
+ new_annotation = extract_annotation_from(new)
+
+ if old_annotation != new_annotation:
+ subprocess.call(["timew", "annotate", "@1", new_annotation])
+
+
+if __name__ == "__main__":
+ old = json.loads(input_stream.readline().decode("utf-8", errors="replace"))
+ new = json.loads(input_stream.readline().decode("utf-8", errors="replace"))
+ print(json.dumps(new))
+ main(old, new)
diff --git a/modules/by-name/ti/timewarrior/taskwarrior_hooks/on-modify.track-total-active-time.py b/modules/by-name/ti/timewarrior/taskwarrior_hooks/on-modify.track-total-active-time.py
new file mode 100755
index 00000000..303a5c57
--- /dev/null
+++ b/modules/by-name/ti/timewarrior/taskwarrior_hooks/on-modify.track-total-active-time.py
@@ -0,0 +1,120 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# nixos-config - My current NixOS configuration
+#
+# Copyright (C) 2016-present Arctic Ice Studio <development@arcticicestudio.com>
+# Copyright (C) 2016-present Sven Greb <development@svengreb.de>
+# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# This file is part of my nixos-config.
+#
+# You should have received a copy of the License along with this program.
+# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+# 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:
+ 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`_
+
+.. _kostajh/taskwarrior-time-tracking-hook:
+ https://github.com/kostajh/taskwarrior-time-tracking-hook
+"""
+
+import datetime
+import json
+import sys
+import subprocess
+from typing import TypeVar
+
+TIME_FORMAT = "%Y%m%dT%H%M%SZ"
+UDA_KEY = "total_active_time"
+
+MAX_ACTIVE = 1
+
+"""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"):
+ value = datetime.fromisoformat(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()