DEV Community

julio
julio

Posted on

How to organize your daily task with Task Warrior

Have some months that I am trying to organize my daily tasks, and the first thing that I tried was to create a time board, where I put what I wanted to do on my day.

First I started with 50 minutes of computer science where I studied things like Data Structures, Algorithms, etc.

Then after that I reserve 50 minutes to learn about Hacking, things like how can I secure systems, the basics things, like network, tools, etc.

For improve my english and speaking skills I read a book for 30 minutes, in high voice.

Another important things is to make exercises for improve my body and mind, then some push-ups, squat, etc.

I decided too to make some networking in Linkedin, but I need to confess that I don't do it any time yet, but I will.

But, you know the problem with it? the time that I reserved for each thing, although is good to have a time limit, sometimes I was very focused studding about computer science and then the alarm trigger and I lost all the focus, or sometimes I am not focused and then the alarm trigger and I see that i lost the 50 minutes thinking about another thing or doing another thing.

Then I decided to stop using timers and start finishing tasks and subtasks, basically instead of read 30 minutes every day, I will read 5 pages per day, instead of 50 minutes of computer science I will choose one or two topics and study.

But where I will organize those tasks? Currently my full setup is in the terminal, my code editor (nvim), my git (lazygit), my code agent (opencode), docker (lazydocker).

And I discovered a tool called Task Warrior, that is a simple task organizer but I have a problem, creating tasks and subtasks in the Task Warrior is king boring, then why not create an automation for it?

Basically we have two scripts, one in Bash and the other in Python.

The script in Bash will do the following:

  1. Read a MD file
  2. Get check-boxes and convert in Tasks
  3. Case the checkbox is a sub checkbox then it is a child or subtask.
  4. In the end I need to know which task was added then I put a logic for insert the UUID from the Task into the markdown line of the task
#!/usr/bin/env bash

set -euo pipefail

if ! command -v awk >/dev/null; then
  echo "awk is required"
  exit 1
fi

if [ $# -lt 1 ]; then
  echo "Usage: $0 <markdown-file> <project-name>"
  exit 1
fi

FILE="$1"

if [ ! -f "$FILE" ]; then
  echo "File not found: $FILE"
  exit 1
fi

PROJECT="${2:-}"

echo "=== Taskwarrior Markdown Importer ==="

if [ -z "$PROJECT" ]; then
  while true; do
    read -rp "Project name: " INPUT_PROJECT

    if [ ! -z "$INPUT_PROJECT" ]; then
      PROJECT="$INPUT_PROJECT"
      break
    fi
  done
fi

echo "Project: $PROJECT, File: $FILE"

get_task() {
  awk -v line="$1" '
    BEGIN {
      gsub(/\r/, "", line)
      gsub(/\t/, "    ", line)

      match(line, /^ */)
      indent = int(RLENGTH / 4)

      trimmed = substr(line, RLENGTH + 1)

      checked = "false"

      if (match(trimmed, /^[-*+][[:space:]]*\[([xX ])\][[:space:]]*/)) {
        if (substr(trimmed, RSTART+2, 3) ~ /[xX]/) {
          checked = "true"
        }

        trimmed = substr(trimmed, RLENGTH + 1)

        while (match(trimmed, /\[\[[^]]+\]\]/)) {
          link = substr(trimmed, RSTART + 2, RLENGTH - 4)  # content inside [[ ]]
          n = split(link, tmp, /\|/)
          replacement = tmp[n]  # last part (after | if exists)

          trimmed = substr(trimmed, 1, RSTART - 1) replacement substr(trimmed, RSTART + RLENGTH)
        }

        gsub(/  +/, " ", trimmed)

        n = split(trimmed, parts, /;;/)

        description = parts[1]
        priority = (n >= 2 && parts[2] != "" ? parts[2] : "L")
        due = (n >= 3 && parts[3] != "" ? parts[3] : "today")

        printf "%d\x1f%s\x1f%s\x1f%s\x1f%s\n",
        indent, checked, description, priority, due
      }
    }'
}

insert_uuid_on_file() {
  local file="$1"
  local lineno="$2"
  local uuid="$3"

  local short="${uuid:0:8}"

  if sed -n "${lineno}p" "$file" | grep -q "task:"; then
    return
  fi

  sed -i "${lineno}s|\$| <!-- task:${short} -->|" "$file"
}

IGNORED=()

PCHECKED="false"
PID=""
CID=""
UUID=""

tmp=$(mktemp)
cp "$FILE" "$tmp"

i=0

while IFS= read -r line; do
  ((i += 1))

  task=$(get_task "$line")

  IFS=$'\x1f' read -r indent checked description priority due <<<"$task"

  if [[ -z "${description// /}" ]]; then
    continue
  fi

  child="false"

  if [[ "$indent" -gt 0 ]]; then
    child="true"
  else
    PCHECKED="$checked"
  fi

  if [[ $checked = "true" || $child = "true" && $PCHECKED = "true" || "$line" =~ task: ]]; then
    IGNORED+=("$description|$priority|$due")

    continue
  fi

  ID=$(task add "$description" project:"$PROJECT" priority:"$priority" due:"$due" mdfile:"$FILE" |
    grep -oP 'Created task \K[0-9]+')

  if [[ "$child" = "false" ]]; then
    PID="$ID"
  else
    CID="$ID"

    task "$CID" modify +P"$PID" >/dev/null

    task "$PID" modify depends:"$CID" >/dev/null
  fi

  UUID=$(task _get "$ID".uuid)

  insert_uuid_on_file "$FILE" "$i" "$UUID"

  # echo "$indent|$checked|$description|$priority|$due"
done <"$tmp"

rm "$tmp"

echo "=== IGNORED ==="
printf "%s\n" "${IGNORED[@]}"
echo "=== === === ==="

Enter fullscreen mode Exit fullscreen mode

You need awk installed for use it, and I this only work in Linux and MacOS.

And you can run with ./taskmd.sh <path/to/file.md> <project_name (optional)>

In my case I created a function in my fish shell for execute this script, then I don't need to put the path to the script, maybe it can be a alias in the terminal too.

And the Python is not really necessary in this logic, cause it only do a simple logic of when I finish some task in the Task Warrior this script will be executed as a hook, and It will check the checkbox in the Markdown file.

#!/usr/bin/env python3

import json
import re
import sys
from pathlib import Path

def update_line(line: str, uuid: str, status: str):
    if f"task:{uuid}" not in line:
        return line, False

    # line = re.sub(r"\s*<!--\s*task:" + re.escape(uuid) + r"\s*-->", "", line)

    if status in ("completed", "deleted"):
        line = re.sub(r"\[\s\]", "[x]", line)
    else:
        line = re.sub(r"\[[xX]\]", "[ ]", line)
    return line, True

def main():
    _ = json.loads(sys.stdin.readline())
    new_task = json.loads(sys.stdin.readline())

    status = new_task.get("status", "")

    if status not in ("completed", "deleted"):
        print(json.dumps(new_task))
        return

    uuid = new_task.get("uuid", "")
    if not uuid:
        print(json.dumps(new_task))
        return

    uuid_short = uuid[:8]

    mdfile = new_task.get("mdfile")
    if not mdfile:
        print(json.dumps(new_task))
        return

    path = Path(mdfile)
    if not path.exists():
        print(json.dumps(new_task))
        return

    lines = path.read_text().splitlines()
    changed = False

    for i, line in enumerate(lines):
        new_line, matched = update_line(line, uuid_short, new_task.get("status"))
        if matched:
            lines[i] = new_line
            changed = True
            break

    if changed:
        path.write_text("\n".join(lines) + "\n")

    print(json.dumps(new_task))

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

This python script need to stay in ~/.task/hooks with name like on-modify.<anyname>.pyon Linux case, you need to verify in Windows where this paste need to be.

And you need to add those lines in the .taskrc file, that on Linux, will stay in the ~ directory.

uda.mdfile.type=string
uda.mdfile.label=File
Enter fullscreen mode Exit fullscreen mode

In that way I can convert markdown files, that is very simple to create in tasks on my Task Warrior.

The task warrior you can download here and I recommend to use the Task Warrior TUI for have a better visualization in the terminal.

Is that, have a nice day, and see you soon :)

Top comments (0)