| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581 |
- #!/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, 1):
- 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 = round(task["rank"], 4) 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 = round(task["rank"], 4) 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, 1000):
- 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))
- free_ids.append(find_free_id(free_ids))
- data = map_ids(free_ids, refids)
- 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[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())
|