Browse Source

bin/tau: [WIP] add tau-python frontend

Dastan-glitch 2 năm trước cách đây
mục cha
commit
02a2a1587c

+ 76 - 0
bin/tau/tau-python/api.py

@@ -0,0 +1,76 @@
+import asyncio, json, random
+import sys
+
+# import lib.config
+from lib.net import Channel
+
+async def create_channel():
+    # server_name = lib.config.get("server", "localhost")
+    server_name = "localhost"
+    reader, writer = await asyncio.open_connection(server_name, 23341)
+    channel = Channel(reader, writer)
+    return channel
+
+def random_id():
+    return random.randint(0, 2**32)
+
+async def query(method, params):
+    channel = await create_channel()
+    request = {
+        "id": random_id(),
+        "method": method,
+        "params": params,
+        "jsonrpc": "2.0",
+    }
+    await channel.send(request)
+
+    response = await channel.receive()
+    # Closed connect returns None
+    if response is None:
+        print("error: connection with server was closed", file=sys.stderr)
+        sys.exit(-1)
+
+    if "error" in response:
+        error = response["error"]
+        errcode, errmsg = error["code"], error["message"]
+        print(f"error: {errcode} - {errmsg}", file=sys.stderr)
+        sys.exit(-1)
+
+    return response["result"]
+
+async def get_info():
+    return await query("get_info", [])
+########
+async def get_ref_ids():
+    return await query("get_ref_ids", [])
+
+async def get_task_by_ref_id(refid):
+    return await query("get_task_by_ref_id", [refid])
+
+########
+async def add_task(task):
+    return await query("add", [task])
+
+async def fetch_active_tasks():
+    return await query("fetch_active_tasks", [])
+
+async def fetch_deactive_tasks(month):
+    return await query("fetch_deactive_tasks", [month])
+
+async def fetch_task(task_id):
+    return await query("fetch_task", [task_id])
+
+async def fetch_archive_task(task_id, month):
+    return await query("fetch_archive_task", [task_id, month])
+
+async def modify_task(who, id, changes):
+    return await query("modify_task", [who, id, changes])
+
+async def change_task_status(who, id, status):
+    await query("change_task_status", [who, id, status])
+    return True
+
+async def add_task_comment(who, id, comment):
+    await query("add_task_comment", [who, id, comment])
+    return True
+

+ 114 - 0
bin/tau/tau-python/lib/net.py

@@ -0,0 +1,114 @@
+import json, os, sys
+from Crypto.Cipher import AES
+
+# import lib.config
+
+# should be 32 bytes hex
+# https://pycryptodome.readthedocs.io/en/latest/src/cipher/aes.html
+# KEY = bytes.fromhex(lib.config.get(
+#     "shared_secret",
+#     "87b9b70e722d20c046c8dba8d0add1f16307fec33debffec9d001fd20dbca3ee"
+# ))
+
+class Channel:
+
+    def __init__(self, reader, writer):
+        self.reader = reader
+        self.writer = writer
+
+    async def readline(self):
+        if not (line := await self.reader.readline()):
+            self.writer.close()
+            return None
+        # Strip the newline
+        return line[:-1].decode()
+
+    async def receive(self):
+        if (plaintext := await self.readline()) is None:
+            return None
+        # if (ciphertext := await self.readline()) is None:
+        #     return None
+        # if (tag := await self.readline()) is None:
+        #     return None
+
+        # nonce = bytes.fromhex(nonce)
+        # ciphertext = bytes.fromhex(ciphertext)
+        # tag = bytes.fromhex(tag)
+
+        #print(f"{nonce.hex()}")
+        #print(f"{ciphertext.hex()}")
+        #print(f"{tag.hex()}")
+        #print()
+
+        # Decrypt
+        # cipher = AES.new(KEY, AES.MODE_EAX, nonce=nonce)
+        # plaintext = cipher.decrypt(ciphertext)
+        # try:
+        #     cipher.verify(tag)
+        # except ValueError:
+        #     print("error: key incorrect or message corrupted", file=sys.stderr)
+        #     return None
+
+        message = plaintext
+        response = json.loads(message)
+        return response
+
+    async def send(self, obj):
+        message = json.dumps(obj)
+        data = message.encode()
+
+        # Encrypt
+        # cipher = AES.new(KEY, AES.MODE_EAX)
+        # nonce = cipher.nonce
+        # ciphertext, tag = cipher.encrypt_and_digest(data)
+
+        #print(f"{nonce.hex()}")
+        #print(f"{ciphertext.hex()}")
+        #print(f"{tag.hex()}")
+        #print()
+
+        # Encode as hex strings since the bytes might contain new lines
+        # nonce = nonce.hex().encode()
+        # ciphertext = ciphertext.hex().encode()
+        # tag = tag.hex().encode()
+
+        # self.writer.write(nonce + b"\n")
+        # self.writer.write(ciphertext + b"\n")
+        # self.writer.write(tag + b"\n")
+        self.writer.write(data + b"\n")
+        await self.writer.drain()
+
+async def _test_client():
+    await asyncio.sleep(1)
+    reader, writer = await asyncio.open_connection("127.0.0.1", 7643)
+    channel = Channel(reader, writer)
+    request = {
+        "foo": "bar"
+    }
+    await channel.send(request)
+    response = await channel.receive()
+    print(f"Client: {response}")
+
+async def _test_server(reader, writer):
+    channel = Channel(reader, writer)
+    request = await channel.receive()
+    print(f"Server: {request}")
+    response = {
+        "abc": "xyz"
+    }
+    await channel.send(response)
+
+async def _test_channel():
+    server = await asyncio.start_server(_test_server, "127.0.0.1", 7643)
+    task1 = asyncio.create_task(_test_client())
+    async with server:
+        task2 = asyncio.create_task(server.serve_forever())
+        await asyncio.sleep(3)
+    await task1
+    task2.cancel()
+
+if __name__ == "__main__":
+    import asyncio
+    # run send and recv testes
+    asyncio.run(_test_channel())
+

+ 43 - 0
bin/tau/tau-python/lib/util.py

@@ -0,0 +1,43 @@
+import random, time
+from datetime import datetime
+
+def random_blob_idx():
+    return "%030x" % random.randrange(16**30)
+
+def datetime_to_unix(dt):
+    return int(time.mktime(dt.timetuple()))
+def now():
+    return datetime_to_unix(datetime.now())
+
+# returns MMYY format
+def current_month():
+    today = datetime.today()
+    return today.strftime("%m%y")
+
+def unix_to_datetime(timestamp):
+    return datetime.utcfromtimestamp(int(timestamp))
+
+task_template = {
+    "workspace": str,
+    "title": str,
+    "tags": list,
+    "desc": str,
+    "owner": str,
+    "assign": list,
+    "project": list,
+    "due": int,
+    "rank": float,
+    "created_at": int,
+    "state": str,
+    "events": list,
+    "comments": list,
+}
+
+def _enforce_task_format(task):
+    for attr, val in task.items():
+        val_type = task_template[attr]
+        if val is None:
+            assert val_type == list or attr not in ["created"]
+            continue
+        assert isinstance(val, val_type)
+

+ 608 - 0
bin/tau/tau-python/main.py

@@ -0,0 +1,608 @@
+#!/usr/bin/python3
+import asyncio, json, os, sys, tempfile
+from datetime import datetime
+from tabulate import tabulate
+from colorama import Fore, Back, Style
+
+import api, lib.util
+
+# USERNAME = lib.config.get("username", "Anonymous")
+USERNAME = "Anonymous"
+
+async def add_task(task_args):
+    task = {
+        "title": None,
+        "tags": [],
+        "desc": None,
+        "assign": [],
+        "project": [],
+        "due": None,
+        "rank": None,
+        "created_at": lib.util.now(),
+        "state": "open"
+    }
+    # Everything that isn't an attribute is part of the title
+    # Open text editor if desc isn't set to write desc text
+    title_words = []
+    for arg in task_args:
+        if arg[0] == "+":
+            tag = arg[1:]
+            if tag in task["tags"]:
+                print(f"error: duplicate tag +{tag} in task", file=sys.stderr)
+                sys.exit(-1)
+            task["tags"].append(tag)
+        elif arg[0] == "@":
+            assign = arg[1:]
+            if assign in task["assign"]:
+                print(f"error: duplicate assign @{assign} in task", file=sys.stderr)
+                sys.exit(-1)
+            task["assign"].append(assign)
+        elif ":" in arg:
+            attr, val = arg.split(":", 1)
+            set_task_attr(task, attr, val)
+        else:
+            title_words.append(arg)
+
+    title = " ".join(title_words)
+    if len(title) == 0:
+        print("Error: Title is required")
+        exit(-1)
+    task["title"] = title
+    if task["desc"] is None:
+        task["desc"] = prompt_description_text(task)
+    
+    if task["desc"].strip() == '':
+        print("Abort adding the task due to empty description.")
+        exit(-1)
+
+    id = await api.add_task(task)
+    print(f"Created task {id}.")
+
+def prompt_text(comment_lines):
+    temp = tempfile.NamedTemporaryFile()
+    temp.write(b"\n")
+    for line in comment_lines:
+        temp.write(line.encode() + b"\n")
+    temp.flush()
+    editor = os.environ.get('EDITOR') if os.environ.get('EDITOR') else 'nano'    
+    os.system(f"{editor} {temp.name}")
+    desc = open(temp.name, "r").read()
+    # Remove comments and empty lines from desc
+    cleaned = []
+    for line in desc.split("\n"):
+        if line == "# ------------------------ >8 ------------------------":
+            break
+        if line.startswith("#"):
+            continue
+        cleaned.append(line)
+
+    return "\n".join(cleaned)
+
+def prompt_description_text(task):
+    return prompt_text([
+        "# Write task description above this line.",
+        "# These lines will be removed.",
+        "# An empty description aborts adding the task",
+        "\n# ------------------------ >8 ------------------------",
+        "# Do not modify or remove the line above.",
+        "# Everything below it will be ignored.",
+        f"\n{tabulate_task(task)}"
+    ])
+
+def prompt_comment_text():
+    return prompt_text([
+        "# Write comments above this line",
+        "# These lines will be removed"
+    ])
+
+def set_task_attr(task, attr, val):
+    # templ = lib.util.task_template
+    assert attr in ["desc", "rank", "due", "project"]
+    print(attr)
+    # assert templ[attr] != list
+
+    if val.lower() == "none":
+        task[attr] = None
+    else:
+        val = convert_attr_val(attr, val)
+        task[attr] = val
+
+    lib.util._enforce_task_format(task)
+
+def convert_attr_val(attr, val):
+    templ = lib.util.task_template
+
+    if attr in ["desc", "title"]:
+        assert templ[attr] == str
+        return val
+    elif attr == "rank":
+        try:
+            return float(val)
+        except ValueError:
+            print(f"error: rank value {val} isn't convertable to float",
+                  file=sys.stderr)
+            sys.exit(-1)
+    elif attr == "due":
+        # Other date formats not yet supported... ez to add
+        assert len(val) == 4
+        date = datetime.now().date()
+        year = int(date.strftime("%Y"))%100
+        try:
+            dt = datetime.strptime(f"18:00 {val}{year}", "%H:%M %d%m%y")
+        except ValueError:
+            print(f"error: unknown date format {val}")
+            sys.exit(-1)
+        due = lib.util.datetime_to_unix(dt)
+        return due
+    elif attr == "project":
+        try:
+            return [val]
+        except ValueError:
+            print(f"error: project value {val} isn't convertable to list",
+                  file=sys.stderr)
+            sys.exit(-1)
+    else:
+        print(f"error: unhandled attr '{attr}' = {val}")
+        sys.exit(-1)
+
+async def show_active_tasks():
+    refids = await api.get_ref_ids()
+    tasks = []
+    for refid in refids:
+        tasks.append(await api.get_task_by_ref_id(refid))
+    list_tasks(tasks, [])
+
+async def show_deactive_tasks(month):
+    tasks = await api.fetch_deactive_tasks(month)
+    list_tasks(tasks, [])
+
+def list_tasks(tasks, filters):
+    headers = ["ID", "Title", "Status", "Project",
+               "Tags", "assign", "Rank", "Due"]
+    table_rows = []
+    for id, task in enumerate(tasks):
+        if task is None:
+            continue
+        if is_filtered(task, filters):
+            continue
+        title = task["title"]
+        status = task["state"]
+        # project = task["project"] if task["project"] is not None else ""
+        tags = " ".join(f"+{tag}" for tag in task["tags"])
+        assign = " ".join(f"@{assign}" for assign in task["assign"])
+        project = " ".join(f"{project}" for project in task["project"])
+        if task["due"] is None:
+            due = ""
+        else:
+            dt = lib.util.unix_to_datetime(task["due"])
+            due = dt.strftime("%H:%M %d/%m/%y")
+
+        rank = task["rank"] if task["rank"] is not None else ""
+
+        if status == "start":
+            id =        Fore.GREEN + str(id)         + Style.RESET_ALL
+            title =     Fore.GREEN + str(title)      + Style.RESET_ALL
+            status =    Fore.GREEN + str(status)     + Style.RESET_ALL
+            project =   Fore.GREEN + str(project)    + Style.RESET_ALL
+            tags =      Fore.GREEN + str(tags)       + Style.RESET_ALL
+            assign =    Fore.GREEN + str(assign)     + Style.RESET_ALL
+            rank =      Fore.GREEN + str(rank)       + Style.RESET_ALL
+            due =       Fore.GREEN + str(due)        + Style.RESET_ALL
+        elif status == "pause":
+            id =        Fore.YELLOW + str(id)        + Style.RESET_ALL
+            title =     Fore.YELLOW + str(title)     + Style.RESET_ALL
+            status =    Fore.YELLOW + str(status)    + Style.RESET_ALL
+            project =   Fore.YELLOW + str(project)   + Style.RESET_ALL
+            tags =      Fore.YELLOW + str(tags)      + Style.RESET_ALL
+            assign =    Fore.YELLOW + str(assign)    + Style.RESET_ALL
+            rank =      Fore.YELLOW + str(rank)      + Style.RESET_ALL
+            due =       Fore.YELLOW + str(due)       + Style.RESET_ALL
+        else:
+            #id =        Style.DIM  + str(id)        + Style.RESET_ALL
+            #title =     Style.DIM  + str(title)     + Style.RESET_ALL
+            #status =    Style.DIM  + str(status)    + Style.RESET_ALL
+            project =    Style.DIM  + str(project)   + Style.RESET_ALL
+            tags =       Style.DIM  + str(tags)      + Style.RESET_ALL
+            #assign =    Style.DIM  + str(assign)    + Style.RESET_ALL
+            rank =       Style.DIM  + str(rank)      + Style.RESET_ALL
+            due =        Style.DIM  + str(due)       + Style.RESET_ALL
+
+        rank_value = task["rank"] if task["rank"] is not None else 0
+        row = [
+            id,
+            title,
+            status,
+            project,
+            tags,
+            assign,
+            rank,
+            due,
+        ]
+        table_rows.append((rank_value, row))
+
+    table = [row for (_, row) in
+             sorted(table_rows, key=lambda item: item[0], reverse=True)]
+    print(tabulate(table, headers=headers))
+
+async def show_task(refid):
+    task = await api.get_task_by_ref_id(refid)
+    task_table(task)
+    return 0
+
+async def show_archive_task(id, month):
+    task = await api.fetch_archive_task(id, month)
+    task_table(task)
+    return 0
+
+def tabulate_task(task):
+    tags = " ".join(f"+{tag}" for tag in task["tags"])
+    assign = " ".join(f"@{assign}" for assign in task["assign"])
+    rank = task["rank"] if task["rank"] is not None else ""
+    if task["due"] is None:
+        due = ""
+    else:
+        dt = lib.util.unix_to_datetime(task["due"])
+        due = dt.strftime("%H:%M %d/%m/%y")
+
+    assert task["created_at"] is not None
+    dt = lib.util.unix_to_datetime(task["created_at"])
+    created_at = dt.strftime("%H:%M %d/%m/%y")
+
+    table = [
+        ["Title:", task["title"]],
+        ["Description:", task["desc"]],
+        ["Status:", task["state"]],
+        ["Project:", task["project"]],
+        ["Tags:", tags],
+        ["assign:", assign],
+        ["Rank:", rank],
+        ["Due:", due],
+        ["Created:", created_at],
+    ]
+    return tabulate(table, headers=["Attribute", "Value"])
+
+def task_table(task):
+    print(tabulate_task(task))
+
+    table = []
+    for event in task["events"]:
+        cmd, when, args = event[0], event[1], event[2:]
+        when = lib.util.unix_to_datetime(when)
+        when = when.strftime("%H:%M %d/%m/%y")
+        if cmd == "set":
+            who, attr, val = args
+            if attr == "due" and val is not None:
+                val = lib.util.unix_to_datetime(val)
+                val = val.strftime("%H:%M %d/%m/%y")
+            table.append([
+                Style.DIM + f"{who} changed {attr} to {val}" + Style.RESET_ALL,
+                "",
+                Style.DIM + when + Style.RESET_ALL
+            ])
+        elif cmd == "append":
+            who, attr, val = args
+            if attr == "tags":
+                val = f"+{val}"
+            elif attr == "assign":
+                val = f"@{val}"
+            table.append([
+                Style.DIM + f"{who} added {val} to {attr}" + Style.RESET_ALL,
+                "",
+                Style.DIM + when + Style.RESET_ALL
+            ])
+        elif cmd == "remove":
+            who, attr, val = args
+            if attr == "tags":
+                val = f"+{val}"
+            elif attr == "assign":
+                val = f"@{val}"
+            table.append([
+                Style.DIM + f"{who} removed {val} from {attr}" + Style.RESET_ALL,
+                "",
+                Style.DIM + when + Style.RESET_ALL
+            ])
+        elif cmd == "state":
+            who, status = args
+            if status == "pause":
+                status_verb = "paused"
+            elif status in ["start", "cancel"]:
+                status_verb = f"{status}ed"
+            elif status == "stop":
+                status_verb = f"stopped"
+            else:
+                print(f"internal error: unhandled task state {status}",
+                      file=sys.stderr)
+                sys.exit(-2)
+
+            table.append([
+                f"{who} {status_verb} task",
+                "",
+                Style.DIM + when + Style.RESET_ALL
+            ])
+    print(tabulate(table))
+
+    table = []
+    for event in task['events']:
+        cmd, when, args = event[0], event[1], event[2:]
+        when = lib.util.unix_to_datetime(when)
+        when = when.strftime("%H:%M %d/%m/%y")
+        if cmd == "comment":
+            who, comment = args
+            table.append([
+                f"{who}>",
+                wrap_comment(comment, 58),
+                Style.DIM + when + Style.RESET_ALL
+            ])
+    if len(table) > 0:
+        print("Comments:")
+    print(tabulate(table))
+
+def wrap_comment(comment, width):
+    lines = []
+    line_start = 0
+    for i, char in enumerate(comment):
+        if char == ' ' and (i - line_start >= width):
+            lines.append(comment[line_start:i + 1])
+            line_start = i + 1
+
+    if line_start < len(comment):
+        lines.append(comment[line_start:])
+    return '\n'.join(lines)
+
+async def modify_task(id, args):
+    changes = []
+    for arg in args:
+        if arg[0] == "+":
+            tag = arg[1:]
+            changes.append(("append", "tags", tag))
+        # This must go before the next elif block
+        elif arg.startswith("-@"):
+            assign = arg[2:]
+            changes.append(("remove", "assign", assign))
+        elif arg[0] == "-":
+            tag = arg[1:]
+            changes.append(("remove", "tags", tag))
+        elif arg[0] == "@":
+            assign = arg[1:]
+            changes.append(("append", "assign", assign))
+        elif ":" in arg:
+            attr, val = arg.split(":", 1)
+            if val.lower() == "none":
+                if attr not in ["project", "rank", "due"]:
+                    print(f"error: invalid you cannot set {attr} to none",
+                          file=sys.stderr)
+                    return -1
+                val = None
+            else:
+                val = convert_attr_val(attr, val)
+            changes.append(("set", attr, val))
+        else:
+            print(f"warning: unknown arg '{arg}'. Skipping...", file=sys.stderr)
+    await api.modify_task(USERNAME, id, changes)
+    return 0
+
+async def change_task_status(id, status):
+    task = await api.fetch_task(id)
+    assert task is not None
+    title = task["title"]
+
+    if not await api.change_task_status(USERNAME, id, status):
+        return -1
+
+    if status == "start":
+        print(f"Started task {id} '{title}'")
+    elif status == "pause":
+        print(f"Paused task {id} '{title}'")
+    elif status == "stop":
+        print(f"Completed task {id} '{title}'")
+    elif status == "cancel":
+        print(f"Cancelled task {id} '{title}'")
+
+    return 0
+
+async def comment(id, args):
+    if not args:
+        comment = prompt_comment_text()
+    else:
+        comment = " ".join(args)
+
+    if not await api.add_task_comment(USERNAME, id, comment):
+        return -1
+
+    task = await api.fetch_task(id)
+    assert task is not None
+    title = task["title"]
+    print(f"Commented on task {id} '{title}'")
+    return 0
+
+def is_filtered(task, filters):
+    for fltr in filters:
+        if fltr.startswith("+"):
+            tag = fltr[1:]
+            if tag not in task["tags"]:
+                return True
+        elif fltr.startswith("@"):
+            assign = fltr[1:]
+            if assign not in task["assign"]:
+                return True
+        elif ":" in fltr:
+            attr, val = fltr.split(":", 1)
+            if val.lower() == "none":
+                if attr not in ["project", "rank", "due"]:
+                    print(f"error: invalid you cannot set {attr} to none",
+                            file=sys.stderr)
+                    sys.exit(-1)
+                if task[attr] is not None:
+                    return True
+            elif attr == "state" :
+                if val not in ["open", "start", "pause"]:
+                    print(f"error: invalid, filter by {attr} can only be [\"open\", \"start\", \"pause\"]",
+                            file=sys.stderr)
+                    sys.exit(-1)
+                if task["state"] != val:
+                    return True
+            elif attr == "project":
+                if task["project"] is None:
+                    return True
+                if not task["project"].startswith(val):
+                    return True
+            else:
+                val = convert_attr_val(attr, val)
+                if task[attr] != val:
+                    return True
+        else:
+            print(f"error: unknown arg '{fltr}'", file=sys.stderr)
+            sys.exit(-1)
+
+    return False
+
+def find_free_id(task_ids):
+    for i in range(1, len(task_ids)):
+        if i not in task_ids:
+            return i
+    1
+
+def map_ids(task_ids, ref_ids):
+    return dict(zip(task_ids, ref_ids))
+
+async def main():
+
+    refids = await api.get_ref_ids()
+    free_ids = []
+    tasks = []
+    for refid in refids:
+        tasks.append(await api.get_task_by_ref_id(refid))
+        numba = find_free_id(free_ids)
+        print(numba)
+        free_ids.append(numba)
+
+    print(refids)
+
+    print(len(tasks))
+
+    # for i in range(1, len(tasks)):
+    #     numba = find_free_id(free_ids)
+    #     print(numba)
+    #     free_ids.append(numba)
+    
+    print(free_ids)
+
+    mapped_ids = map_ids(free_ids, refids)
+
+    print(mapped_ids)
+    
+    json.dump( mapped_ids, open( "IDsMap.json", 'w' ) )
+
+    try:
+        data = json.load( open( "IDsMap.json" ) )
+    except OSError:
+        print("Error: Mapped IDs file does not exist")
+        
+
+    if len(sys.argv) == 1:
+        await show_active_tasks()
+        return 0
+    
+    
+
+
+    if sys.argv[1] in ["-h", "--help", "help"]:
+        print('''USAGE:
+    tau [OPTIONS] [SUBCOMMAND]
+
+OPTIONS:
+    -h, --help                   Print help information
+
+SUBCOMMANDS:
+    add        Add a new task.
+    archive    Show completed tasks.
+    comment    Write comment for task by id.
+    modify     Modify an existing task by id.
+    pause      Pause task(s).
+    start      Start task(s).
+    stop       Stop task(s).
+    help       Show this help text.
+
+Example:
+    tau add task one due:0312 rank:1.022 project:zk +lol @sk desc:desc +abc +def
+    tau add task two  rank:1.044 project:cr +mol @up desc:desc2
+    tau add task three due:0512 project:zy +trol @kk desc:desc3 +who
+    tau 1 modify @upgr due:1112 rank:none
+    tau 1 modify -mol -xx
+    tau 2 start
+    tau 1 comment "this is an awesome comment"
+    tau 2 pause
+    tau archive         # current month's completed tasks
+    tau archive 1122    # completed tasks in Nov. 2022
+    tau 0 archive 1122  # show info of task completed in Nov. 2022
+''')
+        return 0
+    elif sys.argv[1] == "add":
+        task_args = sys.argv[2:]
+        await add_task(task_args)
+        return 0
+    elif sys.argv[1] == "archive":
+        if len(sys.argv) > 2:
+            if len(sys.argv[2]) == 4:
+                month = sys.argv[2]
+            else:
+                print("error: month must be of format MMYY")
+                return -1
+        else:
+            month = lib.util.current_month()
+
+        await show_deactive_tasks(month)
+        return 0
+    elif sys.argv[1] == "show":
+        if len(sys.argv) > 2:
+            filters = sys.argv[2:]
+            list_tasks(tasks, filters)
+        else:
+            await show_active_tasks()
+        return 0
+
+    try:
+        id = int(sys.argv[1])
+        refid = data[str(id)]
+    except ValueError:
+        print("error: invalid ID", file=sys.stderr)
+        return -1
+
+    args = sys.argv[2:]
+
+    if not args:
+        return await show_task(refid)
+
+    subcmd, args = args[0], args[1:]
+
+    if subcmd == "modify":
+        if (errc := await modify_task(id, args)) < 0:
+            return errc
+        return await show_task(id)
+    elif subcmd in ["start", "pause", "stop", "cancel"]:
+        status = subcmd
+        if (errc := await change_task_status(id, status)) < 0:
+            return errc
+    elif subcmd == "comment":
+        if (errc := await comment(id, args)) < 0:
+            return errc
+    elif subcmd == "archive":
+        if len(args) == 1:
+            if len(args[0]) == 4:
+                month = args[0]
+            else:
+                print("Error: month must be of format MMYY")
+                return -1
+        else:
+            month = lib.util.current_month()
+
+        if (errc := await show_archive_task(id, month)) < 0:
+            return errc
+    else:
+        print(f"error: unknown subcommand '{subcmd}'")
+        return -1
+
+    return 0
+
+asyncio.run(main())
+