From 840ed375405d71f30bef1e6b9ccb9046fa944e3b Mon Sep 17 00:00:00 2001 From: Matt Nish-Lapidus Date: Fri, 28 Mar 2025 11:52:22 -0400 Subject: [PATCH] tasks --- flake.lock | 36 ++++---- hosts/media-server/configuration.nix | 6 ++ modules/home/niri.nix | 10 +- modules/home/taskwarrior.nix | 20 +++- modules/home/taskwarrior/hooks/on-add-expire | 19 ++++ .../hooks/on-modify-complete-recur | 92 +++++++++++++++++++ .../hooks/on-modify.relative-recur | 83 +++++++++++++++++ 7 files changed, 242 insertions(+), 24 deletions(-) create mode 100755 modules/home/taskwarrior/hooks/on-add-expire create mode 100755 modules/home/taskwarrior/hooks/on-modify-complete-recur create mode 100755 modules/home/taskwarrior/hooks/on-modify.relative-recur diff --git a/flake.lock b/flake.lock index 0f36bf1..45ce26b 100644 --- a/flake.lock +++ b/flake.lock @@ -70,11 +70,11 @@ "nixpkgs-stable": "nixpkgs-stable" }, "locked": { - "lastModified": 1743127925, - "narHash": "sha256-O+6FDPgubFcCw6FIEIz8I65sOuqhv/d+2XEYkovzeLQ=", + "lastModified": 1743153396, + "narHash": "sha256-Wtgr/u0kYqMHvKjXRu9b7mPo32YDw/7IOIJ2eV8KFuQ=", "owner": "nix-community", "repo": "emacs-overlay", - "rev": "16b1c42e9567cb7fc145674e169d1d724a221a8d", + "rev": "40a9308b06ee2061c5871038024ffcfd992a9ce8", "type": "github" }, "original": { @@ -357,11 +357,11 @@ ] }, "locked": { - "lastModified": 1743097780, - "narHash": "sha256-5tUbaMBKYbfTe/4aXACxmiXG22TgwPBNcfZ8Kg3rt+g=", + "lastModified": 1743136572, + "narHash": "sha256-uwaVrKgi6g1TUq56247j6QvvFtYHloCkjCrEpGBvV54=", "owner": "nix-community", "repo": "home-manager", - "rev": "b14a70c40f4fd0b73d095ab04a7c6e31fbc18e52", + "rev": "1efd2503172016a6742c87b47b43ca2c8145607d", "type": "github" }, "original": { @@ -411,11 +411,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1742397293, - "narHash": "sha256-WVREToubLhIlogCoNJzo+HdatLKkEushwStDU1uyRnc=", + "lastModified": 1743171426, + "narHash": "sha256-uChAGmceKS9F9jqs1xb58BLTVZLF+sFU00MWDEVfYLg=", "owner": "hyprwm", "repo": "hypridle", - "rev": "71e875e49e583c7b8b1364b55dfe494375c4e3ea", + "rev": "84f9f2e127dba49c9bffe293f46bed400e0034a0", "type": "github" }, "original": { @@ -744,11 +744,11 @@ "xwayland-satellite-unstable": "xwayland-satellite-unstable" }, "locked": { - "lastModified": 1742954683, - "narHash": "sha256-ZJBJzpWQcZYuxiX7YbLCaiZl1aOt8xQphXt0ZO0st+8=", + "lastModified": 1743151665, + "narHash": "sha256-Aiy00UNzuu74OHV/bMXtCLLJJA4+e6fvstSaTIPfFjk=", "owner": "sodiboo", "repo": "niri-flake", - "rev": "41db28938147dfa2a2d528f24b59b6962b96f0c5", + "rev": "af777439cd6a35b27831a57bd99d8fe44d22fbd9", "type": "github" }, "original": { @@ -907,11 +907,11 @@ }, "nixos-hardware": { "locked": { - "lastModified": 1742806253, - "narHash": "sha256-zvQ4GsCJT6MTOzPKLmlFyM+lxo0JGQ0cSFaZSACmWfY=", + "lastModified": 1743167577, + "narHash": "sha256-I09SrXIO0UdyBFfh0fxDq5WnCDg8XKmZ1HQbaXzMA1k=", "owner": "NixOS", "repo": "nixos-hardware", - "rev": "ecaa2d911e77c265c2a5bac8b583c40b0f151726", + "rev": "0ed819e708af17bfc4bbc63ee080ef308a24aa42", "type": "github" }, "original": { @@ -1583,11 +1583,11 @@ "rust-overlay": "rust-overlay_3" }, "locked": { - "lastModified": 1743091329, - "narHash": "sha256-knXwz4OKNSG+41dltSOrPLQZlIucPmUfHuyyKeHkwx0=", + "lastModified": 1743164093, + "narHash": "sha256-w1BMtZ2C+xtmYs5pVVoiM1Xl+o9zo6UAvlGqDBTzBvg=", "owner": "sxyazi", "repo": "yazi", - "rev": "a25bbe2e9da43ffe6ae6ba0fed50663ceb9bc64f", + "rev": "0ada74efbe11de17b9cc3588f91eb1f465b518f1", "type": "github" }, "original": { diff --git a/hosts/media-server/configuration.nix b/hosts/media-server/configuration.nix index 90eab2f..88039d2 100644 --- a/hosts/media-server/configuration.nix +++ b/hosts/media-server/configuration.nix @@ -233,6 +233,12 @@ openFirewall = true; }; + services.taskchampion-sync-server = { + enable = true; + openFirewall = true; + snapshot.days = 1; + }; + # services.sabnzbd.configFile = ./sabnzbd.ini; # services.transmission = { diff --git a/modules/home/niri.nix b/modules/home/niri.nix index 6f58c2f..bfa8800 100644 --- a/modules/home/niri.nix +++ b/modules/home/niri.nix @@ -102,7 +102,7 @@ in cursor = { theme = "Bibata-Modern-Classic"; # size = 32; - hide-after-inactive-ms = 30000; + hide-after-inactive-ms = 10000; }; animations.slowdown = 1.0; @@ -239,10 +239,10 @@ in } ]; - # switch-events = with config.lib.niri.actions; { - # lid-close.action = spawn "shikanectl switch desk-clam"; - # lid-open.action = spawn "niri msg output eDP-1 on && shikanectl reload"; - # }; + switch-events = with config.lib.niri.actions; { + lid-close.action = spawn "niri msg output eDP-1 off"; + lid-open.action = spawn "niri msg output eDP-1 on"; + }; binds = with config.lib.niri.actions; diff --git a/modules/home/taskwarrior.nix b/modules/home/taskwarrior.nix index f965b1b..bf35759 100644 --- a/modules/home/taskwarrior.nix +++ b/modules/home/taskwarrior.nix @@ -1,4 +1,4 @@ -{ nix-config, pkgs, ... }: +{ pkgs, ... }: { @@ -13,5 +13,23 @@ programs.taskwarrior = { enable = true; package = pkgs.taskwarrior3; + config = { + regex = false; + uda.relativeRecurDue.type = "duration"; + uda.relativeRecurDue.label = "Rel. Rec. Due"; + uda.relativeRecurWait.type = "duration"; + uda.relativeRecurWait.label = "Rel. Rec. Wait"; + uda.expires.type = "string"; + uda.expires.label = "Expires"; + uda.completeRecurDue.type = "string"; + uda.completeRecurDue.label = "Com. Rec. Due"; + uda.completeRecurWait.type = "string"; + uda.completeRecurWait.label = "Com. Rec. Wait"; + }; + }; + + home.file.".local/share/task/hooks" = { + source = ./taskwarrior/hooks; + recursive = true; }; } diff --git a/modules/home/taskwarrior/hooks/on-add-expire b/modules/home/taskwarrior/hooks/on-add-expire new file mode 100755 index 0000000..a19f9af --- /dev/null +++ b/modules/home/taskwarrior/hooks/on-add-expire @@ -0,0 +1,19 @@ +#!/bin/bash + +# Read the new task JSON object from standard input +read new_task + +# Extract the status and the 'expires' attribute from the new task +status=$(echo "$new_task" | jq -r '.status') +due=$(echo "$new_task" | jq -r '.due // empty') +expires=$(echo "$new_task" | jq -r '.expires // empty') + +# Check if the status is not 'recurring' and the 'expires' attribute is set +if [[ "$status" != "recurring" && -n "$expires" ]]; then + # Update the 'until' attribute with the value of 'expires' + new_expire_date=$(task calc "$due + $expires") + new_task=$(echo "$new_task" | jq -r -c --arg expires "$new_expire_date" '. + {until: $expires}') +fi + +# Output the new task +echo "$new_task" diff --git a/modules/home/taskwarrior/hooks/on-modify-complete-recur b/modules/home/taskwarrior/hooks/on-modify-complete-recur new file mode 100755 index 0000000..9b4af40 --- /dev/null +++ b/modules/home/taskwarrior/hooks/on-modify-complete-recur @@ -0,0 +1,92 @@ +#!/usr/bin/env python + +""" +Module is a copy/paste of https://github.com/mlaradji/task-relative-recur.git +Difference is, we'll execute completeRecurDue and completeRecurWait as strings. + +Usage example: + task add 'Do the dishes' completeRecurWait:"tomorrow +17hours" completeRecurDue:"tomorrow +1day" + +""" + +import json +import sys +import subprocess +import uuid +import os +import tempfile +import time + +TIME_FORMAT = "%Y%m%dT%H%M%SZ" +UDA_DUE = "completeRecurDue" +UDA_WAIT = "completeRecurWait" + +env = os.environ.copy() + +# Hand back duration format parsing to task warrior +def calc(statement): + calc = subprocess.Popen( + ["task", "rc.verbose=nothing", "rc.date.iso=yes", "calc", statement], + stdout=subprocess.PIPE, + env=env, + ) + out, err = calc.communicate() + # Workaround for TW-1254 (https://bug.tasktools.org/browse/TW-1254) + return out.decode("utf-8") + + +# Parse the modified task +original = json.loads(sys.stdin.readline()) +modified = sys.stdin.readline() + +# Return the unmodified modified task, so it is actually changed +print(modified) +modified = json.loads(modified) + +# Has a task with UDA been marked as completed? +if ( + (UDA_DUE in original or UDA_WAIT in original) + and original["status"] != "completed" + and modified["status"] == "completed" +): + del original["modified"] + if "start" in original: + del original["start"] + if UDA_DUE in original: + original["due"] = calc(original[UDA_DUE]) + if UDA_WAIT in original: + original["wait"] = calc(original[UDA_WAIT]) + original["status"] = "waiting" + else: + original["status"] = "pending" + print("Created follow-up task") + original["entry"] = modified["end"] + original["uuid"] = str(uuid.uuid4()) + + # Wait for taskwarrior to finish, so we can safely `task import` the new + # task. + sys.stdout.flush() + task_pid = os.getppid() + if 0 < os.fork(): + sys.exit(0) + else: + # Taskwarrior also waits for stdout to close + try: + os.close(sys.stdout.fileno()) + except OSError: + pass # Thrown because of closing stdout. Don't worry, that's fine. + + # Wait for taskwarrior to finish + # while os.path.exists("/proc/%s" % str(task_pid)): + time.sleep(1) + + # Import the follow-up task + with tempfile.NamedTemporaryFile(mode="wt") as new_task: + new_task.write(json.dumps(original)) + new_task.flush() + add = subprocess.Popen( + ["task", "rc.verbose=nothing", "import", new_task.name], + env=env, + ) + add.communicate() + diff --git a/modules/home/taskwarrior/hooks/on-modify.relative-recur b/modules/home/taskwarrior/hooks/on-modify.relative-recur new file mode 100755 index 0000000..76ce961 --- /dev/null +++ b/modules/home/taskwarrior/hooks/on-modify.relative-recur @@ -0,0 +1,83 @@ +#!/usr/bin/env python + +import json +import sys +import subprocess +import uuid +import os +import tempfile +import time + +TIME_FORMAT = "%Y%m%dT%H%M%SZ" +UDA_DUE = "relativeRecurDue" +UDA_WAIT = "relativeRecurWait" + +env = os.environ.copy() +env["TZ"] = "UTC0" + + +# Hand back duration format parsing to task warrior +def calc(statement): + calc = subprocess.Popen( + ["task", "rc.verbose=nothing", "rc.date.iso=yes", "calc", statement], + stdout=subprocess.PIPE, + env=env, + ) + out, err = calc.communicate() + # Workaround for TW-1254 (https://bug.tasktools.org/browse/TW-1254) + return out.decode("utf-8").rstrip().replace("-", "").replace(":", "") + "Z" + + +# Parse the modified task +original = json.loads(sys.stdin.readline()) +modified = sys.stdin.readline() +# Return the unmodified modified task, so it is actually changed +print(modified) +modified = json.loads(modified) + +# Has a task with UDA been marked as completed? +if ( + (UDA_DUE in original or UDA_WAIT in original) + and original["status"] != "completed" + and modified["status"] == "completed" +): + del original["modified"] + if "start" in original: + del original["start"] + if UDA_DUE in original: + original["due"] = calc(modified["end"] + "+" + original[UDA_DUE]) + if UDA_WAIT in original: + original["wait"] = calc(modified["end"] + "+" + original[UDA_WAIT]) + original["status"] = "waiting" + else: + original["status"] = "pending" + print("Created follow-up task") + original["entry"] = modified["end"] + original["uuid"] = str(uuid.uuid4()) + + # Wait for taskwarrior to finish, so we can safely `task import` the new + # task. + sys.stdout.flush() + task_pid = os.getppid() + if 0 < os.fork(): + sys.exit(0) + else: + # Taskwarrior also waits for stdout to close + try: + os.close(sys.stdout.fileno()) + except OSError: + pass # Thrown because of closing stdout. Don't worry, that's fine. + + # Wait for taskwarrior to finish + while os.path.exists("/proc/%s" % str(task_pid)): + time.sleep(0.25) + + # Import the follow-up task + with tempfile.NamedTemporaryFile(mode="wt") as new_task: + new_task.write(json.dumps(original)) + new_task.flush() + add = subprocess.Popen( + ["task", "rc.verbose=nothing", "import", new_task.name], + env=env, + ) + add.communicate()