tau 25 KB

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