about summary refs log tree commit diff stats
path: root/modules/home.legacy/conf/taskwarrior
diff options
context:
space:
mode:
Diffstat (limited to 'modules/home.legacy/conf/taskwarrior')
-rw-r--r--modules/home.legacy/conf/taskwarrior/default.nix125
-rw-r--r--modules/home.legacy/conf/taskwarrior/firefox/default.nix32
-rw-r--r--modules/home.legacy/conf/taskwarrior/hooks/default.nix114
-rwxr-xr-xmodules/home.legacy/conf/taskwarrior/hooks/scripts/on-add_enforce-policies.sh41
-rwxr-xr-xmodules/home.legacy/conf/taskwarrior/hooks/scripts/on-add_sync-git-repo.sh42
-rwxr-xr-xmodules/home.legacy/conf/taskwarrior/hooks/scripts/on-modify_sync-git-repo.sh43
-rwxr-xr-xmodules/home.legacy/conf/taskwarrior/hooks/scripts/on-modify_track-timewarrior.py94
-rwxr-xr-xmodules/home.legacy/conf/taskwarrior/hooks/scripts/on-modify_track-total-active-time.py148
-rw-r--r--modules/home.legacy/conf/taskwarrior/nord.theme100
-rw-r--r--modules/home.legacy/conf/taskwarrior/projects/default.nix119
10 files changed, 858 insertions, 0 deletions
diff --git a/modules/home.legacy/conf/taskwarrior/default.nix b/modules/home.legacy/conf/taskwarrior/default.nix
new file mode 100644
index 00000000..d7aec156
--- /dev/null
+++ b/modules/home.legacy/conf/taskwarrior/default.nix
@@ -0,0 +1,125 @@
+{
+  nixosConfig,
+  lib,
+  config,
+  ...
+}: {
+  imports = [
+    ./hooks
+  ];
+
+  services.taskwarrior-sync = {
+    enable = true;
+  };
+
+  programs.taskwarrior = let
+    projects = import ./projects {};
+
+    mkContext = project:
+      if builtins.hasAttr "subprojects" project
+      then
+        lib.lists.flatten (
+          (builtins.map mkContext (builtins.map (mkProject project) project.subprojects))
+          ++ (mkContext (builtins.removeAttrs project ["subprojects"]))
+        )
+      else [
+        {
+          inherit (project) name;
+          value = let
+            name =
+              if builtins.hasAttr "pname" project
+              then project.pname
+              else project.name;
+          in {
+            read = "project:${name}";
+            write = "project:${name}";
+            rc = {
+              neorg_path =
+                if builtins.hasAttr "neorg_path" project
+                then project.neorg_path
+                else "${project.prefix}/${project.name}/index.norg";
+            };
+          };
+        }
+      ];
+    mkProject = project: subproject: let
+      pname =
+        if builtins.hasAttr "pname" project
+        then project.pname
+        else project.name;
+    in
+      if builtins.isString subproject
+      then {
+        name = "${project.name}_${subproject}";
+        pname = "${pname}.${subproject}";
+        neorg_path =
+          if builtins.hasAttr "neorg_path_prefix" project
+          then "${project.neorg_path_prefix}/${subproject}/index.norg"
+          else "${project.prefix}/${project.name}/${subproject}/index.norg";
+      }
+      else if builtins.isAttrs subproject
+      then let
+        name = builtins.elemAt (builtins.attrNames subproject) 0;
+      in {
+        name = "${project.name}_${name}";
+        pname = "${pname}.${name}";
+        prefix = "${project.prefix}/${project.name}";
+        neorg_path_prefix = "${project.prefix}/${project.name}/${name}";
+        subprojects = builtins.elemAt (builtins.attrValues subproject) 0;
+      }
+      else builtins.throw "Subproject not a string or a attrs: ${subproject}";
+
+    context =
+      builtins.listToAttrs (lib.lists.flatten (builtins.map mkContext projects));
+  in {
+    enable = true;
+    colorTheme = ./nord.theme;
+    extraConfig = ''
+      # This include just contains my taskd user credentials
+      include ${nixosConfig.age.secrets.taskserverCredentials.path}
+    '';
+    config = {
+      news.version = "2.6.0";
+      complete.all.tags = true;
+      list.all = {
+        projects = true;
+        tags = true;
+      };
+      regex = true;
+      weekstart = "Monday";
+      uda = {
+        total_active_time = {
+          type = "duration";
+          label = "Total active time";
+        };
+      };
+      alias = {
+        mod = "modify";
+        n = "execute neorg --task";
+        fstart = "execute neorg fstart";
+      };
+      color = true;
+
+      hooks.location = "${config.xdg.configHome}/task/hooks";
+
+      urgency.uda.priority = {
+        H.coefficient = 6.0;
+        M.coefficient = 0;
+        L.coefficient = -1.8;
+      };
+
+      inherit context;
+
+      taskd = {
+        server = "taskserver.vhack.eu:53589";
+        trust = "strict";
+        ca =
+          nixosConfig.age.secrets.taskserverCA.path;
+        key =
+          nixosConfig.age.secrets.taskserverPrivate.path;
+        certificate =
+          nixosConfig.age.secrets.taskserverPublic.path;
+      };
+    };
+  };
+}
diff --git a/modules/home.legacy/conf/taskwarrior/firefox/default.nix b/modules/home.legacy/conf/taskwarrior/firefox/default.nix
new file mode 100644
index 00000000..fb5daaa8
--- /dev/null
+++ b/modules/home.legacy/conf/taskwarrior/firefox/default.nix
@@ -0,0 +1,32 @@
+{
+  config,
+  lib,
+  # options
+  prefConfig,
+  profile_size,
+  search,
+  userChrome,
+  ...
+}: let
+  inherit (config.soispha.taskwarrior.projects) projects;
+
+  mkFirefoxProfile = {
+    name,
+    id,
+  }: {
+    inherit name;
+    value = {
+      isDefault = false;
+      extraConfig = prefConfig;
+      inherit id name search userChrome;
+    };
+  };
+  projects_id =
+    lib.imap0 (id: project: {
+      name = project;
+      id = id + profile_size;
+    })
+    projects;
+  firefoxProfiles = builtins.listToAttrs (builtins.map mkFirefoxProfile projects_id);
+in
+  firefoxProfiles
diff --git a/modules/home.legacy/conf/taskwarrior/hooks/default.nix b/modules/home.legacy/conf/taskwarrior/hooks/default.nix
new file mode 100644
index 00000000..ef97e1b5
--- /dev/null
+++ b/modules/home.legacy/conf/taskwarrior/hooks/default.nix
@@ -0,0 +1,114 @@
+{
+  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
+      # TODO: Use a `taskw` package, that actually supports newer python variants <2024-07-13>
+      (pkgs.python311.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
+      # TODO: Use a `taskw` package, that actually supports newer python variants <2024-07-13>
+      (pkgs.python311.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.legacy/conf/taskwarrior/hooks/scripts/on-add_enforce-policies.sh b/modules/home.legacy/conf/taskwarrior/hooks/scripts/on-add_enforce-policies.sh
new file mode 100755
index 00000000..eaf7f30c
--- /dev/null
+++ b/modules/home.legacy/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.legacy/conf/taskwarrior/hooks/scripts/on-add_sync-git-repo.sh b/modules/home.legacy/conf/taskwarrior/hooks/scripts/on-add_sync-git-repo.sh
new file mode 100755
index 00000000..dadc96b0
--- /dev/null
+++ b/modules/home.legacy/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.legacy/conf/taskwarrior/hooks/scripts/on-modify_sync-git-repo.sh b/modules/home.legacy/conf/taskwarrior/hooks/scripts/on-modify_sync-git-repo.sh
new file mode 100755
index 00000000..25813e46
--- /dev/null
+++ b/modules/home.legacy/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.legacy/conf/taskwarrior/hooks/scripts/on-modify_track-timewarrior.py b/modules/home.legacy/conf/taskwarrior/hooks/scripts/on-modify_track-timewarrior.py
new file mode 100755
index 00000000..b482af6a
--- /dev/null
+++ b/modules/home.legacy/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.legacy/conf/taskwarrior/hooks/scripts/on-modify_track-total-active-time.py b/modules/home.legacy/conf/taskwarrior/hooks/scripts/on-modify_track-total-active-time.py
new file mode 100755
index 00000000..d5b380d0
--- /dev/null
+++ b/modules/home.legacy/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()
diff --git a/modules/home.legacy/conf/taskwarrior/nord.theme b/modules/home.legacy/conf/taskwarrior/nord.theme
new file mode 100644
index 00000000..2897418f
--- /dev/null
+++ b/modules/home.legacy/conf/taskwarrior/nord.theme
@@ -0,0 +1,100 @@
+# 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/themes.html
+#   task-color(5)
+#   taskrc(5)
+
+rule.precedence.color=deleted,completed,active,keyword.,tag.,project.,overdue,scheduled,due.today,due,blocked,blocking,recurring,tagged,uda.
+
+#+---------+
+#+ General +
+#+---------+
+color.label=
+color.label.sort=
+color.alternate=
+color.header=bold blue
+color.footnote=cyan
+color.warning=bold black on yellow
+color.error=bold black on red
+color.debug=magenta
+
+#+-------------+
+#+ Task States +
+#+-------------+
+color.completed=green
+color.deleted=red
+color.active=bold black on cyan
+color.recurring=
+color.scheduled=white on black
+color.until=white on bright black
+color.blocked=yellow on black
+color.blocking=bold yellow on black
+
+#+----------+
+#+ Projects +
+#+----------+
+color.project.none=
+
+#+----------+
+#+ Priority +
+#+----------+
+color.uda.priority.H=bold cyan
+color.uda.priority.M=bold blue
+color.uda.priority.L=color245
+
+#+------+
+#+ Tags +
+#+------+
+color.tag.next=
+color.tag.none=
+color.tagged=
+
+#+-----+
+#+ Due +
+#+-----+
+color.due=blue
+color.due.today=cyan on black
+color.overdue=bold red
+
+#+---------+
+#+ Reports +
+#+---------+
+color.burndown.done=bold black on cyan
+color.burndown.pending=black on bright cyan
+color.burndown.started=black on blue
+
+color.history.add=bold black on blue
+color.history.delete=bright white on bold black
+color.history.done=bold black on cyan
+
+color.summary.background=bright white on black
+color.summary.bar=black on cyan
+
+#+----------+
+#+ Calendar +
+#+----------+
+color.calendar.due=bold black on blue
+color.calendar.due.today=bold black on cyan
+color.calendar.holiday=bold blue on white
+color.calendar.overdue=bold black on red
+color.calendar.today=bold black on cyan
+color.calendar.weekend=bright white on bright black
+color.calendar.weeknumber=bold black
+
+#+-----------------+
+#+ Synchronization +
+#+-----------------+
+color.sync.added=green
+color.sync.changed=yellow
+color.sync.rejected=red
+
+#+------+
+#+ Undo +
+#+------+
+color.undo.after=green
+color.undo.before=red
diff --git a/modules/home.legacy/conf/taskwarrior/projects/default.nix b/modules/home.legacy/conf/taskwarrior/projects/default.nix
new file mode 100644
index 00000000..9b555fef
--- /dev/null
+++ b/modules/home.legacy/conf/taskwarrior/projects/default.nix
@@ -0,0 +1,119 @@
+{}: [
+  {
+    name = "me";
+    prefix = "";
+    subprojects = ["health" "sweden" "bank"];
+  }
+  {
+    name = "timesinks";
+    prefix = "";
+    subprojects = ["youtube" "games" "netflix" "music"];
+  }
+  {
+    name = "input";
+    prefix = "research";
+    subprojects = ["read-things" "dotfiles"];
+  }
+  {
+    name = "aoc";
+    prefix = "programming/advent_of_code";
+  }
+  {
+    name = "camera";
+    prefix = "programming/zig";
+    subprojects = [];
+  }
+  {
+    name = "trinitrix";
+    prefix = "programming/rust";
+    subprojects = ["testing" "documentation"];
+  }
+  {
+    name = "serverphone";
+    prefix = "programming/rust";
+  }
+  {
+    name = "latex";
+    prefix = "programming/latex";
+  }
+  {
+    name = "presentation";
+    prefix = "research";
+  }
+  {
+    name = "possible-projects";
+    prefix = "research";
+  }
+  {
+    name = "school";
+    prefix = "research";
+    subprojects = [
+      "biologie"
+      "chemie"
+      "deutsch"
+      "english"
+      "geographie"
+      "geschichte"
+      "infomatik"
+      "klausuren"
+      "latein"
+      "mathematik"
+      "musik"
+      "philosophie"
+      "physik"
+      "sozialkunde"
+      "sport"
+      {extern = ["bwinf" "dsa"];}
+      {chemie = ["facharbeit"];} # TODO: Remove once the ff tabs are cleared <2024-05-10>
+    ];
+  }
+  {
+    name = "hardware";
+    prefix = "research";
+  }
+  {
+    name = "buy";
+    prefix = "buy";
+    subprojects = ["books" "pc"];
+  }
+  {
+    name = "system";
+    prefix = "config";
+    subprojects = [
+      "youtube"
+      "backup"
+      "bar"
+      "email"
+      "firefox"
+      "gpg"
+      "keyboard"
+      "laptop"
+      "nvim"
+      "rss"
+      "shell"
+      "task"
+      "wm"
+    ];
+  }
+  {
+    name = "server";
+    prefix = "config";
+    subprojects = [
+      "b-peetz"
+      "email"
+      "blog"
+      "nix-sync"
+      "sudo-less"
+      "ci"
+    ];
+  }
+  {
+    name = "3d-printer";
+    prefix = "hardware";
+  }
+  {
+    name = "smartphone";
+    prefix = "hardware";
+    subprojects = ["airplay" "airdrop"];
+  }
+]