tau 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839
  1. #!/usr/bin/env python3
  2. import asyncio, os, sys, tempfile
  3. from datetime import datetime
  4. import time
  5. from tabulate import tabulate
  6. from colorama import Fore, Style
  7. import api, lib.util
  8. known_attrs = ["desc", "rank", "due", "project", "bounty"]
  9. async def add_task(task_args, server_name, port):
  10. task = {
  11. "title": None,
  12. "tags": [],
  13. "desc": None,
  14. "assign": [],
  15. "project": [],
  16. "due": None,
  17. "rank": None,
  18. "bounty": None,
  19. "created_at": lib.util.now(),
  20. "state": "open"
  21. }
  22. # Everything that isn't an attribute is part of the title
  23. # Open text editor if desc isn't set to write desc text
  24. title_words = []
  25. for arg in task_args:
  26. if arg[0] == "+":
  27. tag = arg
  28. if tag in task["tags"]:
  29. print(f"error: duplicate tag {tag} in task", file=sys.stderr)
  30. sys.exit(-1)
  31. task["tags"].append(tag)
  32. elif arg[0] == "@":
  33. assign = arg
  34. if assign in task["assign"]:
  35. print(f"error: duplicate assign {assign} in task", file=sys.stderr)
  36. sys.exit(-1)
  37. task["assign"].append(assign)
  38. elif ":" in arg and arg.split(":")[0] in known_attrs:
  39. attr, val = arg.split(":", 1)
  40. set_task_attr(task, attr, val)
  41. else:
  42. title_words.append(arg)
  43. title = " ".join(title_words)
  44. if len(title) == 0:
  45. print("Error: Title is required")
  46. exit(-1)
  47. task["title"] = title
  48. if task["desc"] is None:
  49. task["desc"] = prompt_description_text(task)
  50. if task["desc"].strip() == '':
  51. print("Abort adding the task due to empty description.")
  52. exit(-1)
  53. if task["rank"] is not None:
  54. task["rank"] = round(task["rank"], 4)
  55. if task["bounty"] is not None:
  56. task["bounty"] = round(task["bounty"], 4)
  57. try:
  58. if task["ref_id"].strip() == '':
  59. task.pop('ref_id')
  60. if task["workspace"].strip() == '':
  61. task.pop('workspace')
  62. except KeyError:
  63. pass
  64. ref = await api.add_task(task, server_name, port)
  65. if ref:
  66. return ref, title
  67. else:
  68. print("You don't have write access")
  69. exit(-1)
  70. def prompt_text(comment_lines):
  71. temp = tempfile.NamedTemporaryFile()
  72. temp.write(b"\n")
  73. for line in comment_lines:
  74. temp.write(line.encode() + b"\n")
  75. temp.flush()
  76. editor = os.environ.get('EDITOR') if os.environ.get('EDITOR') else 'nano'
  77. os.system(f"{editor} {temp.name}")
  78. desc = open(temp.name, "r").read()
  79. # Remove comments and empty lines from desc
  80. cleaned = []
  81. for line in desc.split("\n"):
  82. if line == "# ------------------------ >8 ------------------------":
  83. break
  84. if line.startswith("#"):
  85. continue
  86. cleaned.append(line)
  87. return "\n".join(cleaned)
  88. def prompt_description_text(task):
  89. return prompt_text([
  90. "# Write task description above this line.",
  91. "# These lines will be removed.",
  92. "# An empty description aborts adding the task",
  93. "\n# ------------------------ >8 ------------------------",
  94. "# Do not modify or remove the line above.",
  95. "# Everything below it will be ignored.",
  96. f"\n{tabulate_task(task, True)}"
  97. ])
  98. def prompt_comment_text():
  99. return prompt_text([
  100. "# Write comments above this line",
  101. "# These lines will be removed"
  102. ])
  103. def prompt_description_edit(text):
  104. return prompt_text([
  105. f"{text}"
  106. "# Edit the task description above this line",
  107. "# These lines will be removed"
  108. ])
  109. def set_task_attr(task, attr, val):
  110. if attr not in known_attrs:
  111. print(f"Error: invalid attribute: {attr} {val}")
  112. print("Task is not added")
  113. exit(-1)
  114. if val.lower() == "none":
  115. task[attr] = None
  116. else:
  117. val = convert_attr_val(attr, val)
  118. task[attr] = val
  119. lib.util._enforce_task_format(task)
  120. def convert_attr_val(attr, val):
  121. templ = lib.util.task_template
  122. if attr in ["desc", "title"]:
  123. assert templ[attr] == str
  124. return val
  125. elif attr == "rank":
  126. try:
  127. return float(val)
  128. except ValueError:
  129. print(f"error: rank value {val} isn't convertable to float",
  130. file=sys.stderr)
  131. sys.exit(-1)
  132. elif attr == "bounty":
  133. try:
  134. return float(val)
  135. except ValueError:
  136. print(f"error: bounty value {val} isn't convertable to float",
  137. file=sys.stderr)
  138. sys.exit(-1)
  139. elif attr == "due":
  140. # Other date formats not yet supported... ez to add
  141. if len(val) != 4:
  142. print(f"Error: due date must be of length 4 in mmyy format")
  143. sys.exit(-1)
  144. date = datetime.now().date()
  145. year = int(date.strftime("%Y"))%100
  146. try:
  147. dt = datetime.strptime(f"18:00 {val}{year}", "%H:%M %d%m%y")
  148. if dt.date() < date:
  149. dt = datetime.strptime(f"18:00 {val}{year+1}", "%H:%M %d%m%y")
  150. except ValueError:
  151. print(f"error: unknown date format {val}")
  152. sys.exit(-1)
  153. due = lib.util.datetime_to_unix(dt)
  154. return due
  155. elif attr == "project":
  156. try:
  157. return [val]
  158. except ValueError:
  159. print(f"error: project value {val} isn't convertable to list",
  160. file=sys.stderr)
  161. sys.exit(-1)
  162. else:
  163. print(f"error: unhandled attr '{attr}' = {val}")
  164. sys.exit(-1)
  165. async def show_active_tasks(workspace, server_name, port):
  166. refids = await api.get_ref_ids(server_name, port)
  167. tasks = []
  168. for refid in refids:
  169. tasks.append(await api.fetch_task(refid, server_name, port))
  170. list_tasks(tasks, workspace, [])
  171. async def show_deactive_tasks(month_ts, workspace, server_name, port):
  172. tasks = await api.fetch_deactive_tasks(month_ts, server_name, port)
  173. list_tasks(tasks, workspace, [])
  174. async def show_log(server_name, port, timeframe):
  175. # fetch all tasks
  176. refids = await api.get_ref_ids(server_name, port)
  177. tasks = await api.fetch_deactive_tasks(None, server_name, port)
  178. for refid in refids:
  179. tasks.append(await api.fetch_task(refid, server_name, port))
  180. # list tasks events within a timeframe
  181. if timeframe == "day":
  182. timestamp = 86400
  183. elif timeframe == "week" or timeframe == None:
  184. timestamp = 604800
  185. elif timeframe == "month":
  186. timestamp = 2628002
  187. else:
  188. print(f"Error: invalid timeframe {timeframe}")
  189. print("valid timeframes are: 'day', 'week' and 'month'")
  190. print("try: tau log week")
  191. exit(-1)
  192. res_events = []
  193. now = lib.util.now()
  194. for task in tasks:
  195. events = task["events"]
  196. for event in events:
  197. # if timestamp is in ms convert it to s
  198. event_ts = int(event["timestamp"])
  199. if event_ts > 10e10:
  200. event_ts //= 1000
  201. date = lib.util.unix_to_datetime(event_ts)
  202. if (now - event_ts) < timestamp:
  203. if event["action"] == "state":
  204. action = "stopped" if event["content"] == "stop" else f"{event["content"]}ed"
  205. res = f"{date}: {event["author"]} {action} task: ({task["ref_id"][:6]}) '{task["title"]}'"
  206. res_events.append(res)
  207. if event["action"] == "comment":
  208. res = f"{date}: {event["author"]} commented on task: ({task["ref_id"][:6]}) '{task["title"]}'"
  209. res_events.append(res)
  210. if event["action"] in ["assign", "tags"] :
  211. res = f"{date}: {event["author"]} added {event["content"][1:]} to {event["action"]} in task: ({task["ref_id"][:6]}) '{task["title"]}'"
  212. res_events.append(res)
  213. for i in res_events:
  214. print(i)
  215. def list_tasks(tasks, workspace, filters):
  216. print(f"Workspace: {workspace}")
  217. headers = ["ID", "Title", "Status", "Project",
  218. "Tags", "assign", "Rank", "Due",
  219. "Bounty", "RefID"]
  220. table_rows = []
  221. for id, task in enumerate(tasks, 1):
  222. if task is None:
  223. continue
  224. if is_filtered(task, filters):
  225. continue
  226. ref_id = task["ref_id"][:6]
  227. title = task["title"]
  228. status = task["state"]
  229. # project = task["project"] if task["project"] is not None else ""
  230. tags = " ".join(f"+{tag}" for tag in task["tags"])
  231. assign = " ".join(f"@{assign}" for assign in task["assign"])
  232. project = " ".join(f"{project}" for project in task["project"])
  233. if task["due"] is None:
  234. due = ""
  235. else:
  236. dt = lib.util.unix_to_datetime(task["due"])
  237. due = dt.strftime("%H:%M %d/%m/%y")
  238. rank = round(task["rank"], 4) if task["rank"] is not None else ""
  239. bounty = "$" + str(round(task["bounty"], 4)) if task.get("bounty") is not None else ""
  240. if status == "start":
  241. id = Fore.GREEN + str(id) + Style.RESET_ALL
  242. title = Fore.GREEN + str(title) + Style.RESET_ALL
  243. status = Fore.GREEN + str(status) + Style.RESET_ALL
  244. project = Fore.GREEN + str(project) + Style.RESET_ALL
  245. tags = Fore.GREEN + str(tags) + Style.RESET_ALL
  246. assign = Fore.GREEN + str(assign) + Style.RESET_ALL
  247. rank = Fore.GREEN + str(rank) + Style.RESET_ALL
  248. due = Fore.GREEN + str(due) + Style.RESET_ALL
  249. bounty = Fore.GREEN + str(bounty) + Style.RESET_ALL
  250. ref_id = Fore.GREEN + str(ref_id) + Style.RESET_ALL
  251. elif status == "pause":
  252. id = Fore.YELLOW + str(id) + Style.RESET_ALL
  253. title = Fore.YELLOW + str(title) + Style.RESET_ALL
  254. status = Fore.YELLOW + str(status) + Style.RESET_ALL
  255. project = Fore.YELLOW + str(project) + Style.RESET_ALL
  256. tags = Fore.YELLOW + str(tags) + Style.RESET_ALL
  257. assign = Fore.YELLOW + str(assign) + Style.RESET_ALL
  258. rank = Fore.YELLOW + str(rank) + Style.RESET_ALL
  259. due = Fore.YELLOW + str(due) + Style.RESET_ALL
  260. bounty = Fore.YELLOW + str(bounty) + Style.RESET_ALL
  261. ref_id = Fore.YELLOW + str(ref_id) + Style.RESET_ALL
  262. elif status == "stop":
  263. id = Fore.RED + str(id) + Style.RESET_ALL
  264. title = Fore.RED + str(title) + Style.RESET_ALL
  265. status = Fore.RED + str(status) + Style.RESET_ALL
  266. project = Fore.RED + str(project) + Style.RESET_ALL
  267. tags = Fore.RED + str(tags) + Style.RESET_ALL
  268. assign = Fore.RED + str(assign) + Style.RESET_ALL
  269. rank = Fore.RED + str(rank) + Style.RESET_ALL
  270. due = Fore.RED + str(due) + Style.RESET_ALL
  271. bounty = Fore.RED + str(bounty) + Style.RESET_ALL
  272. ref_id = Fore.RED + str(ref_id) + Style.RESET_ALL
  273. else:
  274. #id = Style.DIM + str(id) + Style.RESET_ALL
  275. #title = Style.DIM + str(title) + Style.RESET_ALL
  276. #status = Style.DIM + str(status) + Style.RESET_ALL
  277. project = Style.DIM + str(project) + Style.RESET_ALL
  278. tags = Style.DIM + str(tags) + Style.RESET_ALL
  279. #assign = Style.DIM + str(assign) + Style.RESET_ALL
  280. rank = Style.DIM + str(rank) + Style.RESET_ALL
  281. due = Style.DIM + str(due) + Style.RESET_ALL
  282. bounty = Style.DIM + str(bounty) + Style.RESET_ALL
  283. #ref_id = Style.DIM + str(ref_id) + Style.RESET_ALL
  284. rank_value = task["rank"] if task["rank"] is not None else 0
  285. row = [
  286. id,
  287. title,
  288. status,
  289. project,
  290. tags,
  291. assign,
  292. rank,
  293. due,
  294. bounty,
  295. ref_id
  296. ]
  297. table_rows.append((rank_value, row))
  298. table = [row for (_, row) in
  299. sorted(table_rows, key=lambda item: item[0], reverse=True)]
  300. print(tabulate(table, headers=headers))
  301. async def show_task(refid, server_name, port):
  302. task = await api.fetch_task(refid, server_name, port)
  303. task_table(task)
  304. return 0
  305. async def show_archive_task(ref_id, month_ts, server_name, port):
  306. task = await api.fetch_archive_task(ref_id, month_ts, server_name, port)
  307. task_table(task)
  308. return 0
  309. def tabulate_task(task, prompt):
  310. tags = " ".join(f"+{tag}" for tag in task["tags"])
  311. assign = " ".join(f"{assign}" for assign in task["assign"])
  312. project = " ".join(f"{project}" for project in task["project"])
  313. rank = round(task["rank"], 4) if task["rank"] is not None else ""
  314. bounty = round(task["bounty"], 4) if task["bounty"] is not None else ""
  315. if task["due"] is None:
  316. due = ""
  317. else:
  318. dt = lib.util.unix_to_datetime(task["due"])
  319. due = dt.strftime("%H:%M %d/%m/%y")
  320. assert task["created_at"] is not None
  321. dt = lib.util.unix_to_datetime(task["created_at"])
  322. created_at = dt.strftime("%H:%M %d/%m/%y")
  323. if prompt:
  324. task["ref_id"] = ''
  325. task["workspace"] = ''
  326. table = [
  327. ["RefID:", task["ref_id"]],
  328. ["Title:", task["title"]],
  329. ["Workspace:", task["workspace"]],
  330. ["Description:", task["desc"]],
  331. ["Status:", task["state"]],
  332. ["Project:", project],
  333. ["Tags:", tags],
  334. ["Assign:", assign],
  335. ["Rank:", rank],
  336. ["Due:", due],
  337. ["Bounty:", bounty],
  338. ["Created:", created_at],
  339. ]
  340. return tabulate(table, headers=["Attribute", "Value"])
  341. def task_table(task):
  342. print(tabulate_task(task, False))
  343. table = []
  344. for event in task["events"]:
  345. act, who, when, args = event["action"], event["author"], event["timestamp"], event["content"]
  346. when = lib.util.unix_to_datetime(when)
  347. when = when.strftime("%H:%M %d/%m/%y")
  348. if act == "due" and when is not None:
  349. due_date = lib.util.unix_to_datetime(args)
  350. due_date = due_date.strftime("%H:%M %d/%m/%y")
  351. table.append([
  352. Style.DIM + f"{who} changed {act} to {due_date}" + Style.RESET_ALL,
  353. "",
  354. Style.DIM + when + Style.RESET_ALL
  355. ])
  356. elif act == "tags" or act == "assign":
  357. val = f"{args}"
  358. event = f"{who} added {val} to {act}"
  359. if val[0] == "-":
  360. event = f"{who} removed {val[1:]} from {act}"
  361. table.append([
  362. Style.DIM + event + Style.RESET_ALL,
  363. "",
  364. Style.DIM + when + Style.RESET_ALL
  365. ])
  366. elif act == "state":
  367. status = args
  368. if status == "pause":
  369. status_verb = "paused"
  370. elif status in ["start", "open"]:
  371. status_verb = f"{status}ed"
  372. elif status == "stop":
  373. status_verb = f"stopped"
  374. else:
  375. print(f"internal error: unhandled task state {status}",
  376. file=sys.stderr)
  377. sys.exit(-2)
  378. table.append([
  379. f"{who} {status_verb} task",
  380. "",
  381. Style.DIM + when + Style.RESET_ALL
  382. ])
  383. elif act == "comment":
  384. continue
  385. else:
  386. table.append([
  387. Style.DIM + f"{who} changed {act} to {args}" + Style.RESET_ALL,
  388. "",
  389. Style.DIM + when + Style.RESET_ALL
  390. ])
  391. print(tabulate(table))
  392. table = []
  393. for event in task['events']:
  394. act, who, when, args = event["action"], event["author"], event["timestamp"], event["content"]
  395. when = lib.util.unix_to_datetime(when)
  396. when = when.strftime("%H:%M %d/%m/%y")
  397. if act == "comment":
  398. comment = args
  399. table.append([
  400. f"{who}>",
  401. wrap_comment(comment, 58),
  402. Style.DIM + when + Style.RESET_ALL
  403. ])
  404. if len(table) > 0:
  405. print("Comments:")
  406. print(tabulate(table))
  407. def wrap_comment(comment, width):
  408. lines = []
  409. line_start = 0
  410. for i, char in enumerate(comment):
  411. if char == ' ' and (i - line_start >= width):
  412. lines.append(comment[line_start:i + 1])
  413. line_start = i + 1
  414. if line_start < len(comment):
  415. lines.append(comment[line_start:])
  416. return '\n'.join(lines)
  417. async def modify_task(refid, args, server_name, port):
  418. task = await api.fetch_task(refid, server_name, port)
  419. current_assigns = task["assign"]
  420. current_tags = task["tags"]
  421. changes = {}
  422. changes["assign"] = []
  423. changes["tags"] = []
  424. for arg in args:
  425. # This must go before the next elif block
  426. if arg.startswith("@") or (arg.startswith("-@") and arg[2:] in current_assigns):
  427. changes["assign"].append(arg)
  428. elif arg.startswith("+") or (arg.startswith("-") and arg[1:] in current_tags):
  429. changes["tags"].append(arg)
  430. elif arg.lower() in ["desc", "description"]:
  431. desc = task["desc"]
  432. new_desc = prompt_description_edit(desc).lstrip()
  433. if desc == new_desc:
  434. print("Abort due to unchanged description")
  435. exit(-1)
  436. changes["desc"] = new_desc
  437. elif ":" in arg:
  438. attr, val = arg.split(":", 1)
  439. if val.lower() == "none":
  440. if attr not in ["project", "rank", "due", "bounty"]:
  441. print(f"error: invalid you cannot set {attr} to none",
  442. file=sys.stderr)
  443. return -1
  444. val = None
  445. else:
  446. val = convert_attr_val(attr, val)
  447. changes[str(attr)] = val
  448. else:
  449. print(f"warning: unknown arg '{arg}'. Skipping...", file=sys.stderr)
  450. if not await api.modify_task(refid, changes, server_name, port):
  451. print("You don't have write access")
  452. exit(-1)
  453. return 0
  454. async def change_task_status(refid, status, server_name, port):
  455. task = await api.fetch_task(refid, server_name, port)
  456. assert task is not None
  457. title = task["title"]
  458. if not await api.change_task_status(refid, status, server_name, port):
  459. return -1
  460. if status == "start":
  461. print(f"Started task '{title}'")
  462. elif status == "pause":
  463. print(f"Paused task '{title}'")
  464. elif status == "stop":
  465. print(f"Completed task '{title}'")
  466. elif status == "open":
  467. print(f"Opened task '{title}'")
  468. return 0
  469. async def comment(refid, args, server_name, port):
  470. if not args:
  471. comment = prompt_comment_text()
  472. else:
  473. comment = " ".join(args)
  474. if comment.strip() == '':
  475. print("Abort adding comment due to empty content.")
  476. exit(-1)
  477. if not await api.add_task_comment(refid, comment, server_name, port):
  478. print("You don't have write access")
  479. exit(-1)
  480. # Two json rpcs back to back cause Unexpected EOF error
  481. time.sleep(0.1)
  482. task = await api.fetch_task(refid, server_name, port)
  483. assert task is not None
  484. title = task["title"]
  485. print(f"Commented on task '{title}'")
  486. return 0
  487. def is_filtered(task, filters):
  488. for fltr in filters:
  489. if fltr.startswith("+"):
  490. tag = fltr[1:]
  491. if tag not in task["tags"]:
  492. return True
  493. elif fltr.startswith("@"):
  494. assign = fltr[1:]
  495. if assign not in task["assign"]:
  496. return True
  497. elif ":" in fltr:
  498. attr, val = fltr.split(":", 1)
  499. if val.lower() == "none":
  500. if attr not in ["project", "rank", "due", "bounty"]:
  501. print(f"error: invalid you cannot set {attr} to none",
  502. file=sys.stderr)
  503. sys.exit(-1)
  504. if task[attr] is not None:
  505. return True
  506. elif attr == "state" :
  507. if val not in ["open", "start", "pause"]:
  508. print(f"error: invalid, filter by {attr} can only be [\"open\", \"start\", \"pause\"]",
  509. file=sys.stderr)
  510. sys.exit(-1)
  511. if task["state"] != val:
  512. return True
  513. else:
  514. val = convert_attr_val(attr, val)
  515. if task[attr] != val:
  516. return True
  517. else:
  518. print(f"error: unknown arg '{fltr}'", file=sys.stderr)
  519. sys.exit(-1)
  520. return False
  521. def find_free_id(task_ids):
  522. for i in range(1, 1000):
  523. if i not in task_ids:
  524. return i
  525. 1
  526. def map_ids(task_ids, ref_ids):
  527. return dict(zip(task_ids, ref_ids))
  528. async def main():
  529. val = str('127.0.0.1:9805')
  530. allowed_states = ["start", "pause", "stop", "open"]
  531. for i in range(1, len(sys.argv)):
  532. if sys.argv[i] == "-e":
  533. val = sys.argv[i+1]
  534. del sys.argv[i]
  535. del sys.argv[i]
  536. break
  537. server_name, port = val.split(':')
  538. refids = await api.get_ref_ids(server_name, port)
  539. free_ids = []
  540. tasks = []
  541. for refid in refids:
  542. tasks.append(await api.fetch_task(refid, server_name, port))
  543. free_ids.append(find_free_id(free_ids))
  544. data = map_ids(free_ids, refids)
  545. workspace = await api.get_workspace(server_name, port)
  546. if len(sys.argv) == 1:
  547. await show_active_tasks(workspace, server_name, port)
  548. return 0
  549. if any(x in ["-h", "--help", "help"] for x in sys.argv):
  550. print('''USAGE:
  551. tau [OPTIONS] [SUBCOMMAND]
  552. OPTIONS:
  553. -h, --help Print help information
  554. -e RPC endpoint [default: 127.0.0.1:23330]
  555. SUBCOMMANDS:
  556. add Add a new task.
  557. archive Show completed tasks.
  558. comment Write comment for task by id.
  559. modify Modify an existing task by id.
  560. pause Pause task(s).
  561. start Start task(s).
  562. stop Stop task(s).
  563. switch Switch between configured workspaces.
  564. show List filtered tasks.
  565. export Save current workspace tasks to a path.
  566. import Load current workspace tasks from a path.
  567. help Show this help text.
  568. Examples:
  569. tau add task one due:0312 rank:1.022 project:zk +lol @sk desc:desc +abc +def
  570. tau add task two rank:1.044 project:cr +mol @up desc:desc2
  571. tau add task three due:0512 project:zy +trol @kk desc:desc3 +who
  572. tau 1 modify @upgr due:1112 rank:none
  573. tau 1 modify -@up
  574. tau 1 modify -mol -xx
  575. tau 1,2 modify +dev @erto
  576. tau 1-3 start
  577. tau 1 comment "this is an awesome comment"
  578. tau 2 pause
  579. tau show @erto state:start # list started tasks that are assigned to 'erto'
  580. tau show +dev project:zk # list tasks with 'dev' tag project 'zk'
  581. tau switch darkfi # switch to configured 'darkfi' workspace
  582. tau archive # current month's completed tasks
  583. tau archive 1122 # completed tasks of Nov. 2022
  584. tau archive 1122 1 # show info of task completed in Nov. 2022
  585. ''')
  586. return 0
  587. elif sys.argv[1] == "log":
  588. if len(sys.argv) == 3:
  589. timeframe = sys.argv[2]
  590. else:
  591. timeframe = None
  592. await show_log(server_name, port, timeframe)
  593. return 0
  594. elif sys.argv[1] == "add":
  595. task_args = sys.argv[2:]
  596. ref, title = await add_task(task_args, server_name, port)
  597. if title:
  598. print(f"Created task ({find_free_id(free_ids)}) ({ref[:7]}) '{title}'.")
  599. return 0
  600. elif sys.argv[1] == "archive":
  601. if len(sys.argv) == 4:
  602. if len(sys.argv[2]) == 4:
  603. month = sys.argv[2]
  604. month_ts = lib.util.month_to_unix(month)
  605. else:
  606. print("error: usage format is: tau archive [MONTH] [ID]")
  607. return -1
  608. archive_refids = await api.get_archive_ref_ids(month_ts, server_name, port)
  609. afree_ids = []
  610. atasks = []
  611. for arefid in archive_refids:
  612. atasks.append(await api.fetch_archive_task(arefid, month_ts, server_name, port))
  613. afree_ids.append(find_free_id(afree_ids))
  614. adata = map_ids(afree_ids, archive_refids)
  615. if len(sys.argv[3]) < 4:
  616. try:
  617. tid = int(sys.argv[3])
  618. arefid = adata[tid]
  619. except (ValueError, KeyError):
  620. print("error: invalid ID", file=sys.stderr)
  621. return -1
  622. else:
  623. print("error: invalid ID", file=sys.stderr)
  624. return -1
  625. if (errc := await show_archive_task(arefid, month_ts, server_name, port)) < 0:
  626. return errc
  627. elif len(sys.argv) == 3:
  628. if sys.argv[2] == "all":
  629. await show_deactive_tasks(None, workspace, server_name, port)
  630. elif len(sys.argv[2]) == 4:
  631. month = sys.argv[2]
  632. month_ts = lib.util.month_to_unix(month)
  633. await show_deactive_tasks(month_ts, workspace, server_name, port)
  634. elif len(sys.argv[2]) < 4:
  635. month_ts = lib.util.month_to_unix()
  636. archive_refids = await api.get_archive_ref_ids(month_ts, server_name, port)
  637. afree_ids = []
  638. atasks = []
  639. for arefid in archive_refids:
  640. atasks.append(await api.fetch_archive_task(arefid, month_ts, server_name, port))
  641. afree_ids.append(find_free_id(afree_ids))
  642. adata = map_ids(afree_ids, archive_refids)
  643. try:
  644. tid = int(sys.argv[2])
  645. arefid = adata[tid]
  646. except (ValueError, KeyError):
  647. print("error: invalid ID", file=sys.stderr)
  648. return -1
  649. if (errc := await show_archive_task(arefid, month_ts, server_name, port)) < 0:
  650. return errc
  651. else:
  652. print("error: month must be of format MMYY")
  653. return -1
  654. else:
  655. month_ts = lib.util.month_to_unix()
  656. await show_deactive_tasks(month_ts, workspace, server_name, port)
  657. return 0
  658. elif sys.argv[1] == "show":
  659. if len(sys.argv) > 2:
  660. filters = sys.argv[2:]
  661. list_tasks(tasks, workspace, filters)
  662. else:
  663. await show_active_tasks(workspace, server_name, port)
  664. return 0
  665. elif sys.argv[1] == "switch":
  666. if not len(sys.argv) == 3:
  667. print("Error: you must provide workspace name")
  668. return 0
  669. if not await api.switch_workspace(sys.argv[2], server_name, port):
  670. print(f"Error: Workspace \"{sys.argv[2]}\" is not configured.")
  671. else:
  672. print(f"You are now on \"{sys.argv[2]}\" workspace.")
  673. return 0
  674. elif sys.argv[1] == "export":
  675. if len(sys.argv) == 2:
  676. path = "~/.local/share/darkfi"
  677. else:
  678. path = sys.argv[2]
  679. if await api.export_to(path, server_name, port):
  680. print(f"Exported tasks successfuly to {path}")
  681. return 0
  682. elif sys.argv[1] == "import":
  683. if len(sys.argv) == 2:
  684. path = "~/.local/share/darkfi"
  685. else:
  686. path = sys.argv[2]
  687. if await api.import_from(path, server_name, port):
  688. print(f"Imported tasks successfuly from {path}")
  689. return 0
  690. try:
  691. id = sys.argv[1]
  692. subcommands = ["modify", "comment"]
  693. if any(id in ls for ls in [allowed_states, subcommands]):
  694. user_input = input("This command has no filter, and will modify all tasks. Are you sure? [y/N] ")
  695. if user_input.lower() in ['y', 'yes']:
  696. refid = list(refids)
  697. args = sys.argv[1:]
  698. else:
  699. print("Command prevented from running.")
  700. exit(-1)
  701. elif any(id == rfid[:len(id)] for rfid in refids if len(id) > 2):
  702. refid = []
  703. for rid in refids:
  704. if id == rid[:len(id)]:
  705. refid.append(rid)
  706. args = sys.argv[2:]
  707. else:
  708. lines = id.split(',')
  709. numbers = []
  710. for line in lines:
  711. if line == '':
  712. continue
  713. elif '-' in line:
  714. t = line.split('-')
  715. numbers += range(int(t[0]), int(t[1]) + 1)
  716. else:
  717. numbers.append(int(line))
  718. refid = []
  719. for i in numbers:
  720. refid.append(data[i])
  721. args = sys.argv[2:]
  722. except (ValueError, KeyError):
  723. print("error: invalid ID", file=sys.stderr)
  724. return -1
  725. except EOFError:
  726. print('\nOperation is cancelled')
  727. return -1
  728. if not args:
  729. for rid in refid:
  730. await show_task(rid, server_name, port)
  731. return 0
  732. subcmd, args = args[0], args[1:]
  733. if subcmd == "modify":
  734. if not args:
  735. print("Error: modify subcommand must have at least one argument.")
  736. exit(-1)
  737. for rid in refid:
  738. if (errc := await modify_task(rid, args, server_name, port)) < 0:
  739. return errc
  740. time.sleep(0.1)
  741. await show_task(rid, server_name, port)
  742. elif subcmd in allowed_states:
  743. status = subcmd
  744. for rid in refid:
  745. if (errc := await change_task_status(rid, status, server_name, port)) < 0:
  746. return errc
  747. time.sleep(0.1)
  748. elif subcmd == "comment":
  749. for rid in refid:
  750. if (errc := await comment(rid, args, server_name, port)) < 0:
  751. return errc
  752. else:
  753. print(f"error: unknown subcommand '{subcmd}'")
  754. return -1
  755. return 0
  756. asyncio.run(main())