markora 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
markora/__init__.py ADDED
@@ -0,0 +1,28 @@
1
+ """
2
+ Markora: Sovereign Markor-style Markdown Notebook, QuickNotes & Todo TUI
3
+ """
4
+
5
+ __version__ = "1.0.0"
6
+ __app_name__ = "markora"
7
+ __author__ = "zyekhabdul"
8
+
9
+ from .document import Task, TodoDocument
10
+ from .quicknote import load_quicknote, append_quicknote
11
+ from .favorites import FavoritesManager
12
+ from .keep import KeepSyncManager
13
+ from .tui import TodoTUI, start_tui
14
+ from .cli import main
15
+
16
+ __all__ = [
17
+ "Task",
18
+ "TodoDocument",
19
+ "load_quicknote",
20
+ "append_quicknote",
21
+ "FavoritesManager",
22
+ "KeepSyncManager",
23
+ "TodoTUI",
24
+ "start_tui",
25
+ "main",
26
+ "__version__",
27
+ "__app_name__",
28
+ ]
markora/cli.py ADDED
@@ -0,0 +1,591 @@
1
+ """
2
+ CLI Command Handlers & Argparse Entry Point for Markora
3
+ """
4
+
5
+ import sys
6
+ import os
7
+ import re
8
+ import glob
9
+ import argparse
10
+ import datetime
11
+ import subprocess
12
+
13
+ from . import __version__, __app_name__
14
+ from .config import (
15
+ DOCS_DIR,
16
+ NOTES_DIR,
17
+ DAILY_DIR,
18
+ QUICKNOTE_FILE,
19
+ Colors,
20
+ make_clickable,
21
+ get_editor,
22
+ ensure_directories,
23
+ )
24
+ from .document import TodoDocument
25
+ from .quicknote import load_quicknote, append_quicknote
26
+ from .favorites import FavoritesManager
27
+ from .keep import KeepSyncManager
28
+ from .tui import start_tui, launch_gui_window
29
+
30
+ BANNER = rf"""{Colors.BOLD}{Colors.CYAN}
31
+ __ __ _
32
+ | \/ | | |
33
+ | \ / | __ _ _ __| | _____ _ __ __ _
34
+ | |\/| |/ _` | '__| |/ / _ \| '__/ _` |
35
+ | | | | (_| | | | < (_) | | | (_| |
36
+ |_| |_|\__,_|_| |_|\_\___/|_| \__,_|
37
+ {Colors.RESET}{Colors.DIM} Sovereign Markor-style Markdown Notebook & Todo TUI v{__version__}{Colors.RESET}
38
+ """
39
+
40
+
41
+ def cmd_quicknote(args):
42
+ ensure_directories()
43
+ if args.text:
44
+ entry = append_quicknote(" ".join(args.text))
45
+ print(f"{Colors.GREEN}[ OK ] Appended to QuickNote:{Colors.RESET} {entry}")
46
+ elif args.edit:
47
+ subprocess.run([get_editor(), QUICKNOTE_FILE])
48
+ else:
49
+ lines = load_quicknote()
50
+ clickable_path = make_clickable(QUICKNOTE_FILE, QUICKNOTE_FILE)
51
+ print(f"\n{Colors.BOLD}{Colors.CYAN}══ QUICKNOTE / BRAIN DUMP ══{Colors.RESET} {Colors.DIM}({clickable_path}){Colors.RESET}\n")
52
+ for line in lines:
53
+ if line.startswith("#"):
54
+ print(f"{Colors.BOLD}{Colors.WHITE}{line}{Colors.RESET}")
55
+ elif line.startswith("- **"):
56
+ print(f" {Colors.YELLOW}•{Colors.RESET} {line[2:]}")
57
+ else:
58
+ print(f" {line}")
59
+ print(f"\n{Colors.DIM}Hint: 'markora qn <text>' to append, 'markora qn -e' to edit in editor.{Colors.RESET}\n")
60
+
61
+
62
+ def cmd_list(args, doc):
63
+ ensure_directories()
64
+ tasks = doc.tasks
65
+ if getattr(args, "pending", False):
66
+ tasks = [t for t in tasks if not t.done]
67
+ elif getattr(args, "done", False):
68
+ tasks = [t for t in tasks if t.done]
69
+
70
+ if getattr(args, "tag", None):
71
+ tasks = [t for t in tasks if any(args.tag.lower() in tag.lower() for tag in t.tags)]
72
+
73
+ if getattr(args, "section", None):
74
+ tasks = [t for t in tasks if args.section.lower() in t.section.lower()]
75
+
76
+ pending_count = sum(1 for t in doc.tasks if not t.done)
77
+ done_count = sum(1 for t in doc.tasks if t.done)
78
+
79
+ clickable_path = make_clickable(doc.filepath, doc.filepath)
80
+ print(f"\n{Colors.BOLD}{Colors.CYAN}══ MASTER TODOLIST ══{Colors.RESET} {Colors.DIM}({clickable_path}){Colors.RESET}")
81
+ print(f"{Colors.DIM}Active: {Colors.YELLOW}{pending_count} pending{Colors.RESET}{Colors.DIM}, {Colors.GREEN}{done_count} completed{Colors.RESET}{Colors.DIM}, Total: {len(doc.tasks)}{Colors.RESET}\n")
82
+
83
+ if not tasks:
84
+ print(f" {Colors.DIM}(No tasks matching filter){Colors.RESET}\n")
85
+ return
86
+
87
+ current_section = None
88
+ for t in tasks:
89
+ if t.section != current_section:
90
+ current_section = t.section
91
+ print(f"{Colors.BOLD}{Colors.WHITE}▶ {current_section}{Colors.RESET}")
92
+
93
+ status_box = f"{Colors.GREEN}[✔]{Colors.RESET}" if t.done else f"{Colors.YELLOW}[ ]{Colors.RESET}"
94
+ id_badge = f"{Colors.BOLD}{Colors.CYAN}[{t.id:2d}]{Colors.RESET}"
95
+
96
+ prio_badge = ""
97
+ if t.priority in ["P1", "(A)"]:
98
+ prio_badge = f" {Colors.BOLD}{Colors.RED}[{t.priority}]{Colors.RESET}"
99
+ elif t.priority in ["P2", "(B)"]:
100
+ prio_badge = f" {Colors.BOLD}{Colors.YELLOW}[{t.priority}]{Colors.RESET}"
101
+ elif t.priority:
102
+ prio_badge = f" {Colors.BOLD}{Colors.CYAN}[{t.priority}]{Colors.RESET}"
103
+
104
+ text_display = f"{Colors.DIM}{t.text}{Colors.RESET}" if t.done else f"{t.text}"
105
+ print(f" {id_badge} {status_box}{prio_badge} {text_display}")
106
+
107
+ if not getattr(args, "compact", False):
108
+ for sub in t.sublines:
109
+ print(f" {Colors.DIM}└─ {sub}{Colors.RESET}")
110
+ print()
111
+
112
+
113
+ def cmd_add(args, doc):
114
+ ensure_directories()
115
+ title = " ".join(args.title)
116
+ if not title:
117
+ print(f"{Colors.RED}[ ERROR ] Title cannot be empty.{Colors.RESET}")
118
+ sys.exit(1)
119
+ ok, msg = doc.add_task(
120
+ title=title,
121
+ priority=getattr(args, "prio", "") or getattr(args, "priority", "") or "",
122
+ section=getattr(args, "section", None),
123
+ scope=getattr(args, "scope", "") or "",
124
+ deliverables=getattr(args, "deliverables", "") or ""
125
+ )
126
+ if ok:
127
+ print(f"{Colors.GREEN}[ OK ] {msg}{Colors.RESET}")
128
+ else:
129
+ print(f"{Colors.RED}[ ERROR ] {msg}{Colors.RESET}")
130
+
131
+
132
+ def cmd_done(args, doc):
133
+ for tid in args.ids:
134
+ ok, msg = doc.toggle_task(tid, set_done=True)
135
+ color = Colors.GREEN if ok else Colors.RED
136
+ print(f"{color}[ {'OK' if ok else 'ERROR'} ] {msg}{Colors.RESET}")
137
+
138
+
139
+ def cmd_undone(args, doc):
140
+ for tid in args.ids:
141
+ ok, msg = doc.toggle_task(tid, set_done=False)
142
+ color = Colors.GREEN if ok else Colors.RED
143
+ print(f"{color}[ {'OK' if ok else 'ERROR'} ] {msg}{Colors.RESET}")
144
+
145
+
146
+ def cmd_toggle(args, doc):
147
+ for tid in args.ids:
148
+ ok, msg = doc.toggle_task(tid)
149
+ color = Colors.GREEN if ok else Colors.RED
150
+ print(f"{color}[ {'OK' if ok else 'ERROR'} ] {msg}{Colors.RESET}")
151
+
152
+
153
+ def cmd_rm(args, doc):
154
+ for tid in args.ids:
155
+ ok, msg = doc.delete_task(tid)
156
+ color = Colors.GREEN if ok else Colors.RED
157
+ print(f"{color}[ {'OK' if ok else 'ERROR'} ] {msg}{Colors.RESET}")
158
+
159
+
160
+ def cmd_edit(args, doc):
161
+ ensure_directories()
162
+ editor = get_editor()
163
+ target = doc.filepath
164
+ if getattr(args, "file", None):
165
+ target = args.file
166
+ subprocess.run([editor, target])
167
+
168
+
169
+ def cmd_note(args):
170
+ ensure_directories()
171
+ editor = get_editor()
172
+ now = datetime.datetime.now()
173
+
174
+ if args.title:
175
+ slug = re.sub(r"[^a-zA-Z0-9_\-]+", "-", " ".join(args.title)).strip("-").lower()
176
+ if getattr(args, "daily", False):
177
+ filename = f"{now.strftime('%Y-%m-%d')}-{slug}.md"
178
+ filepath = os.path.join(DAILY_DIR, filename)
179
+ else:
180
+ filename = f"{now.strftime('%Y-%m-%d')}-{slug}.md"
181
+ filepath = os.path.join(NOTES_DIR, filename)
182
+ if not os.path.exists(filepath):
183
+ with open(filepath, "w", encoding="utf-8") as f:
184
+ f.write(f"# {' '.join(args.title)}\n\n- **Date**: {now.strftime('%Y-%m-%d %H:%M')}\n- **Tags**: #note\n\n---\n\n")
185
+ else:
186
+ filename = f"{now.strftime('%Y-%m-%d')}.md"
187
+ filepath = os.path.join(DAILY_DIR, filename)
188
+ if not os.path.exists(filepath):
189
+ with open(filepath, "w", encoding="utf-8") as f:
190
+ f.write(f"# Daily Note: {now.strftime('%A, %d %B %Y')}\n\n- **Created**: {now.strftime('%H:%M:%S')}\n\n## Tasks\n- [ ] \n\n## Log / Notes\n\n")
191
+
192
+ subprocess.run([editor, filepath])
193
+
194
+
195
+ def cmd_notes_list(args):
196
+ ensure_directories()
197
+ print(f"\n{Colors.BOLD}{Colors.CYAN}══ MARKDOWN NOTES ══{Colors.RESET} {Colors.DIM}({DOCS_DIR}){Colors.RESET}\n")
198
+
199
+ patterns = [
200
+ os.path.join(DOCS_DIR, "*.md"),
201
+ os.path.join(NOTES_DIR, "*.md"),
202
+ os.path.join(DAILY_DIR, "*.md"),
203
+ ]
204
+ all_files = []
205
+ for p in patterns:
206
+ all_files.extend(glob.glob(p))
207
+
208
+ all_files = sorted(list(set(all_files)), key=lambda x: os.path.getmtime(x) if os.path.exists(x) else 0, reverse=True)
209
+ if not all_files:
210
+ print(f" {Colors.DIM}(No notes found){Colors.RESET}\n")
211
+ return
212
+
213
+ for idx, fp in enumerate(all_files, 1):
214
+ rel = os.path.relpath(fp, DOCS_DIR)
215
+ mtime = datetime.datetime.fromtimestamp(os.path.getmtime(fp)).strftime("%Y-%m-%d %H:%M")
216
+ size = os.path.getsize(fp)
217
+
218
+ try:
219
+ with open(fp, "r", encoding="utf-8", errors="ignore") as f:
220
+ content = f.read()
221
+ p_tasks = len(re.findall(r"-\s*\[\s*\]", content))
222
+ task_stat = f" {Colors.YELLOW}[{p_tasks} todo]{Colors.RESET}" if p_tasks else ""
223
+ except Exception:
224
+ task_stat = ""
225
+
226
+ clickable_rel = make_clickable(rel, fp)
227
+ print(f" {Colors.CYAN}[{idx:2d}]{Colors.RESET} {Colors.BOLD}{clickable_rel:35s}{Colors.RESET} {Colors.DIM}{mtime} ({size/1024:.1f} KB){Colors.RESET}{task_stat}")
228
+ print()
229
+
230
+
231
+ def cmd_find(args):
232
+ ensure_directories()
233
+ query = " ".join(args.query).lower()
234
+ if not query:
235
+ print(f"{Colors.RED}[ ERROR ] Query cannot be empty.{Colors.RESET}")
236
+ return
237
+
238
+ print(f"\n{Colors.BOLD}{Colors.CYAN}══ SEARCH RESULTS: '{query}' ══{Colors.RESET}\n")
239
+ patterns = [
240
+ os.path.join(DOCS_DIR, "*.md"),
241
+ os.path.join(NOTES_DIR, "*.md"),
242
+ os.path.join(DAILY_DIR, "*.md"),
243
+ ]
244
+ all_files = []
245
+ for p in patterns:
246
+ all_files.extend(glob.glob(p))
247
+
248
+ match_count = 0
249
+ for fp in sorted(list(set(all_files))):
250
+ try:
251
+ with open(fp, "r", encoding="utf-8", errors="ignore") as f:
252
+ lines = f.read().splitlines()
253
+ file_matches = []
254
+ for i, line in enumerate(lines, 1):
255
+ if query in line.lower():
256
+ file_matches.append((i, line))
257
+ if file_matches:
258
+ rel = os.path.relpath(fp, DOCS_DIR)
259
+ clickable_rel = make_clickable(rel, fp)
260
+ print(f"{Colors.BOLD}{Colors.WHITE}📄 {clickable_rel}{Colors.RESET}")
261
+ for ln, line in file_matches:
262
+ match_count += 1
263
+ highlighted = re.sub(f"({re.escape(query)})", f"{Colors.BOLD}{Colors.YELLOW}\\1{Colors.RESET}", line, flags=re.IGNORECASE)
264
+ print(f" {Colors.CYAN}{ln:4d}:{Colors.RESET} {highlighted}")
265
+ print()
266
+ except Exception:
267
+ continue
268
+
269
+ if match_count == 0:
270
+ print(f" {Colors.DIM}(No matches found){Colors.RESET}\n")
271
+ else:
272
+ print(f"{Colors.DIM}Total matches: {match_count}{Colors.RESET}\n")
273
+
274
+
275
+ def cmd_rename(args, doc):
276
+ ensure_directories()
277
+ target = args.target
278
+ new_name = args.new_name.strip()
279
+ if not target or not new_name:
280
+ print(f"{Colors.RED}[ ERROR ] Target and new name required.{Colors.RESET}")
281
+ return
282
+
283
+ # 1. Check if target is Task ID
284
+ try:
285
+ task_id = int(target)
286
+ ok, msg = doc.edit_task_text(task_id, new_name)
287
+ if ok:
288
+ print(f"{Colors.GREEN}[ SUCCESS ] {msg}: '{new_name}'{Colors.RESET}")
289
+ else:
290
+ print(f"{Colors.RED}[ ERROR ] {msg}{Colors.RESET}")
291
+ return
292
+ except ValueError:
293
+ pass
294
+
295
+ # 2. Target is file or folder path
296
+ full_path = os.path.join(DOCS_DIR, target) if not os.path.isabs(target) else target
297
+ if not os.path.exists(full_path):
298
+ print(f"{Colors.RED}[ ERROR ] Path does not exist: {full_path}{Colors.RESET}")
299
+ return
300
+
301
+ parent_dir = os.path.dirname(full_path)
302
+ if os.path.isfile(full_path) and not new_name.endswith((".md", ".txt")):
303
+ new_name = new_name + ".md"
304
+ new_path = os.path.join(parent_dir, new_name)
305
+
306
+ if os.path.exists(new_path) and new_path != full_path:
307
+ print(f"{Colors.RED}[ ERROR ] Target already exists: {new_name}{Colors.RESET}")
308
+ return
309
+
310
+ try:
311
+ os.rename(full_path, new_path)
312
+ fm = FavoritesManager()
313
+ old_rel = os.path.relpath(full_path, DOCS_DIR)
314
+ new_rel = os.path.relpath(new_path, DOCS_DIR)
315
+ if os.path.isfile(new_path):
316
+ if fm.is_favorite(old_rel):
317
+ fm.favorites.remove(old_rel)
318
+ fm.favorites.add(new_rel)
319
+ fm.save()
320
+ elif os.path.isdir(new_path):
321
+ updated = set()
322
+ for fav in fm.favorites:
323
+ if fav == old_rel:
324
+ updated.add(new_rel)
325
+ elif fav.startswith(old_rel + "/"):
326
+ updated.add(fav.replace(old_rel + "/", new_rel + "/", 1))
327
+ else:
328
+ updated.add(fav)
329
+ fm.favorites = updated
330
+ fm.save()
331
+ print(f"{Colors.GREEN}[ SUCCESS ] Renamed '{os.path.basename(full_path)}' -> '{new_name}'{Colors.RESET}")
332
+ except Exception as e:
333
+ print(f"{Colors.RED}[ ERROR ] Rename failed: {e}{Colors.RESET}")
334
+
335
+
336
+ def cmd_fav(args):
337
+ ensure_directories()
338
+ fm = FavoritesManager()
339
+ action = getattr(args, "action", None) or "list"
340
+
341
+ if action in ["list", "ls"]:
342
+ favs = fm.get_favorites()
343
+ print(f"\n{Colors.BOLD}{Colors.YELLOW}══ ⭐ FAVORITE & PINNED NOTES ({len(favs)}) ══{Colors.RESET}\n")
344
+ if not favs:
345
+ print(f" {Colors.DIM}(No favorite notes yet. In TUI press 'f' on any note to star it){Colors.RESET}\n")
346
+ return
347
+ for idx, (rel, full) in enumerate(favs, 1):
348
+ mtime = datetime.datetime.fromtimestamp(os.path.getmtime(full)).strftime("%Y-%m-%d %H:%M")
349
+ size = os.path.getsize(full)
350
+ clickable = make_clickable(rel, full)
351
+ print(f" {Colors.YELLOW}[★ {idx:2d}]{Colors.RESET} {Colors.BOLD}{clickable:40s}{Colors.RESET} {Colors.DIM}{mtime} ({size/1024:.1f} KB){Colors.RESET}")
352
+ print()
353
+
354
+ elif action == "add":
355
+ target = args.file
356
+ if not target:
357
+ print(f"{Colors.RED}[ ERROR ] File path required.{Colors.RESET}")
358
+ return
359
+ full = os.path.join(DOCS_DIR, target) if not os.path.isabs(target) else target
360
+ if not os.path.exists(full):
361
+ print(f"{Colors.RED}[ ERROR ] File does not exist: {full}{Colors.RESET}")
362
+ return
363
+ fm.add(full)
364
+ print(f"{Colors.GREEN}[ SUCCESS ] Added '{os.path.basename(full)}' to Favorites [⭐].{Colors.RESET}")
365
+
366
+ elif action in ["rm", "remove"]:
367
+ target = args.file
368
+ if not target:
369
+ print(f"{Colors.RED}[ ERROR ] File path required.{Colors.RESET}")
370
+ return
371
+ rel = os.path.relpath(target, DOCS_DIR) if os.path.isabs(target) else target
372
+ if rel in fm.favorites:
373
+ fm.remove(rel)
374
+ print(f"{Colors.GREEN}[ SUCCESS ] Removed '{rel}' from Favorites.{Colors.RESET}")
375
+ else:
376
+ print(f"{Colors.YELLOW}[ NOTE ] '{rel}' was not in Favorites.{Colors.RESET}")
377
+
378
+ elif action == "toggle":
379
+ target = args.file
380
+ if not target:
381
+ print(f"{Colors.RED}[ ERROR ] File path required.{Colors.RESET}")
382
+ return
383
+ full = os.path.join(DOCS_DIR, target) if not os.path.isabs(target) else target
384
+ if not os.path.exists(full):
385
+ print(f"{Colors.RED}[ ERROR ] File does not exist: {full}{Colors.RESET}")
386
+ return
387
+ ok, msg = fm.toggle(full)
388
+ print(f"{Colors.GREEN}[ SUCCESS ] {msg}{Colors.RESET}")
389
+
390
+
391
+ def cmd_keep(args):
392
+ ensure_directories()
393
+ km = KeepSyncManager()
394
+ action = getattr(args, "action", None) or "status"
395
+
396
+ if action == "auth":
397
+ ok, msg = km.auth(email=getattr(args, "email", None), token=getattr(args, "token", None), manual=getattr(args, "manual", False))
398
+ if ok:
399
+ print(f"\n{Colors.GREEN}[ SUCCESS ] {msg}{Colors.RESET}\n")
400
+ else:
401
+ print(f"\n{Colors.RED}[ ERROR ] {msg}{Colors.RESET}\n")
402
+
403
+ elif action == "status":
404
+ print(f"\n{Colors.BOLD}{Colors.CYAN}══ GOOGLE KEEP SYNC STATUS ══{Colors.RESET}\n")
405
+ if km.is_authenticated():
406
+ email = km.config.get("email")
407
+ last_sync = km.config.get("last_sync", "Never")
408
+ print(f" Status : {Colors.GREEN}[ AUTHENTICATED ]{Colors.RESET}")
409
+ print(f" Account : {Colors.BOLD}{email}{Colors.RESET}")
410
+ print(f" Last Sync : {Colors.DIM}{last_sync}{Colors.RESET}")
411
+ print(f" Config File : {Colors.DIM}{km.config_file}{Colors.RESET}")
412
+ print(f"\n{Colors.DIM}Commands: markora keep pull | markora keep push | markora keep sync | markora keep logout{Colors.RESET}\n")
413
+ else:
414
+ print(f" Status : {Colors.YELLOW}[ NOT AUTHENTICATED ]{Colors.RESET}")
415
+ print(f"\n Run {Colors.BOLD}markora keep auth{Colors.RESET} to connect your Google Keep account.\n")
416
+
417
+ elif action == "pull":
418
+ if not km.is_authenticated():
419
+ print(f"{Colors.RED}[ ERROR ] Not authenticated. Run 'markora keep auth' first.{Colors.RESET}")
420
+ return
421
+ print(f"{Colors.DIM}Pulling notes from Google Keep...{Colors.RESET}")
422
+ try:
423
+ count = km.pull()
424
+ print(f"{Colors.GREEN}[ SUCCESS ] Pulled/updated {count} notes from Google Keep.{Colors.RESET}")
425
+ except Exception as e:
426
+ print(f"{Colors.RED}[ ERROR ] Pull failed: {e}{Colors.RESET}")
427
+
428
+ elif action == "push":
429
+ if not km.is_authenticated():
430
+ print(f"{Colors.RED}[ ERROR ] Not authenticated. Run 'markora keep auth' first.{Colors.RESET}")
431
+ return
432
+ print(f"{Colors.DIM}Pushing notes to Google Keep...{Colors.RESET}")
433
+ try:
434
+ count = km.push()
435
+ print(f"{Colors.GREEN}[ SUCCESS ] Pushed/updated {count} notes to Google Keep.{Colors.RESET}")
436
+ except Exception as e:
437
+ print(f"{Colors.RED}[ ERROR ] Push failed: {e}{Colors.RESET}")
438
+
439
+ elif action == "sync":
440
+ if not km.is_authenticated():
441
+ print(f"{Colors.RED}[ ERROR ] Not authenticated. Run 'markora keep auth' first.{Colors.RESET}")
442
+ return
443
+ print(f"{Colors.DIM}Synchronizing with Google Keep...{Colors.RESET}")
444
+ try:
445
+ pulled, pushed = km.sync()
446
+ print(f"{Colors.GREEN}[ SUCCESS ] Sync completed: {pulled} pulled, {pushed} pushed.{Colors.RESET}")
447
+ except Exception as e:
448
+ print(f"{Colors.RED}[ ERROR ] Sync failed: {e}{Colors.RESET}")
449
+
450
+ elif action == "logout":
451
+ ok, msg = km.logout()
452
+ print(f"{Colors.GREEN}[ SUCCESS ] {msg}{Colors.RESET}")
453
+
454
+
455
+ def build_parser():
456
+ parser = argparse.ArgumentParser(
457
+ prog="markora",
458
+ description=f"Markora v{__version__}: Sovereign Markor-style Markdown Notebook, QuickNotes & Todo TUI",
459
+ formatter_class=argparse.RawDescriptionHelpFormatter,
460
+ epilog="Examples:\n"
461
+ " markora ui Launch interactive 4-tab Curses TUI\n"
462
+ " markora add 'Finish report' -p p1\n"
463
+ " markora qn 'Quick thought'\n"
464
+ " markora list -p\n"
465
+ " markora done 1 2\n"
466
+ )
467
+ parser.add_argument("-v", "--version", action="version", version=f"%(prog)s v{__version__}")
468
+ subparsers = parser.add_subparsers(dest="command", help="Subcommand to run")
469
+
470
+ # quicknote / qn
471
+ p_qn = subparsers.add_parser("quicknote", aliases=["qn", "scratch"], help="Append or view QuickNotes (quicknote.md)")
472
+ p_qn.add_argument("text", nargs="*", help="Text to append to QuickNote")
473
+ p_qn.add_argument("-t", "--timestamp", action="store_true", help="Prefix with current timestamp")
474
+ p_qn.add_argument("-e", "--edit", action="store_true", help="Open QuickNote in $EDITOR")
475
+
476
+ # list / ls
477
+ p_list = subparsers.add_parser("list", aliases=["ls", "todo"], help="List todos with status, priority, tags")
478
+ p_list.add_argument("-p", "--pending", action="store_true", help="Show only pending tasks")
479
+ p_list.add_argument("-d", "--done", action="store_true", help="Show only completed tasks")
480
+ p_list.add_argument("-t", "--tag", help="Filter by tag (e.g., #urgent)")
481
+ p_list.add_argument("-s", "--section", help="Filter by section name")
482
+ p_list.add_argument("-c", "--compact", action="store_true", help="Compact single-line output")
483
+
484
+ # add / a
485
+ p_add = subparsers.add_parser("add", aliases=["a"], help="Add a new task")
486
+ p_add.add_argument("title", nargs="+", help="Task title/description")
487
+ p_add.add_argument("-p", "--prio", choices=["p1", "p2", "p3", "P1", "P2", "P3"], help="Priority flag")
488
+ p_add.add_argument("-s", "--section", default="Inbox", help="Target section (default: Inbox)")
489
+
490
+ # done / do
491
+ p_done = subparsers.add_parser("done", aliases=["do", "check"], help="Mark task(s) as completed")
492
+ p_done.add_argument("ids", nargs="+", type=int, help="Task ID(s) to complete")
493
+
494
+ # undone / undo
495
+ p_undone = subparsers.add_parser("undone", aliases=["undo", "uncheck"], help="Mark task(s) as uncompleted/pending")
496
+ p_undone.add_argument("ids", nargs="+", type=int, help="Task ID(s) to mark undone")
497
+
498
+ # toggle / x
499
+ p_toggle = subparsers.add_parser("toggle", aliases=["x"], help="Toggle task(s) completion status")
500
+ p_toggle.add_argument("ids", nargs="+", type=int, help="Task ID(s) to toggle")
501
+
502
+ # rm / del
503
+ p_rm = subparsers.add_parser("rm", aliases=["del", "remove"], help="Delete task(s)")
504
+ p_rm.add_argument("ids", nargs="+", type=int, help="Task ID(s) to delete")
505
+
506
+ # edit / e
507
+ subparsers.add_parser("edit", aliases=["e"], help="Open todo.md in $EDITOR")
508
+
509
+ # note / n
510
+ p_note = subparsers.add_parser("note", aliases=["n"], help="Create or edit a markdown note")
511
+ p_note.add_argument("title", nargs="+", help="Note title")
512
+ p_note.add_argument("-d", "--daily", action="store_true", help="Save in daily-notes/ with YYYY-MM-DD prefix")
513
+
514
+ # notes / nl
515
+ subparsers.add_parser("notes", aliases=["nl"], help="List markdown notes in notebook")
516
+
517
+ # find / search
518
+ p_find = subparsers.add_parser("find", aliases=["search", "grep"], help="Search across all notes & todos")
519
+ p_find.add_argument("query", nargs="+", help="Search query")
520
+
521
+ # rename / mv
522
+ p_rename = subparsers.add_parser("rename", aliases=["mv"], help="Rename a task, note, or folder")
523
+ p_rename.add_argument("target", help="Task ID (e.g. 1) or file/folder path")
524
+ p_rename.add_argument("new_name", help="New name for the task, note, or folder")
525
+
526
+ # ui / tui / dui / app
527
+ subparsers.add_parser("ui", aliases=["tui", "dui", "app"], help="Launch Markora interactive 4-tab Curses TUI")
528
+
529
+ # gui (App launcher background launch)
530
+ subparsers.add_parser("gui", help="Launch in desktop terminal window")
531
+
532
+ # keep / gk / gkeep
533
+ p_keep = subparsers.add_parser("keep", aliases=["gk", "gkeep"], help="Google Keep sync & integration")
534
+ p_keep.add_argument("action", nargs="?", choices=["auth", "status", "pull", "push", "sync", "logout"], default="status", help="Keep action (auth|status|pull|push|sync|logout)")
535
+ p_keep.add_argument("--email", help="Google account email")
536
+ p_keep.add_argument("--token", help="OAuth token or Master Token")
537
+ p_keep.add_argument("--manual", action="store_true", help="Manual token input (disable automated browser popup)")
538
+
539
+ # fav / favorite / pin
540
+ p_fav = subparsers.add_parser("fav", aliases=["favorite", "pin", "star"], help="Manage favorite and pinned notes")
541
+ p_fav.add_argument("action", nargs="?", choices=["list", "ls", "add", "rm", "remove", "toggle"], default="list", help="Favorite action (list|add|rm|toggle)")
542
+ p_fav.add_argument("file", nargs="?", help="Relative or absolute note path")
543
+
544
+ return parser
545
+
546
+
547
+ def main(argv=None):
548
+ parser = build_parser()
549
+ args = parser.parse_args(argv)
550
+ doc = TodoDocument()
551
+
552
+ if args.command in ["quicknote", "qn", "scratch"]:
553
+ cmd_quicknote(args)
554
+ elif args.command in ["list", "ls", "todo", None]:
555
+ if args.command is None:
556
+ dummy_args = argparse.Namespace(pending=False, done=False, tag=None, section=None, compact=False)
557
+ cmd_list(dummy_args, doc)
558
+ else:
559
+ cmd_list(args, doc)
560
+ elif args.command in ["add", "a"]:
561
+ cmd_add(args, doc)
562
+ elif args.command in ["done", "do", "check"]:
563
+ cmd_done(args, doc)
564
+ elif args.command in ["undone", "undo", "uncheck"]:
565
+ cmd_undone(args, doc)
566
+ elif args.command in ["toggle", "x"]:
567
+ cmd_toggle(args, doc)
568
+ elif args.command in ["rm", "del", "remove"]:
569
+ cmd_rm(args, doc)
570
+ elif args.command in ["edit", "e"]:
571
+ cmd_edit(args, doc)
572
+ elif args.command in ["note", "n"]:
573
+ cmd_note(args)
574
+ elif args.command in ["notes", "nl"]:
575
+ cmd_notes_list(args)
576
+ elif args.command in ["find", "search", "grep"]:
577
+ cmd_find(args)
578
+ elif args.command in ["rename", "mv"]:
579
+ cmd_rename(args, doc)
580
+ elif args.command in ["fav", "favorite", "pin", "star"]:
581
+ cmd_fav(args)
582
+ elif args.command in ["keep", "gk", "gkeep"]:
583
+ cmd_keep(args)
584
+ elif args.command in ["ui", "tui", "dui", "app"]:
585
+ start_tui(doc)
586
+ elif args.command == "gui":
587
+ launch_gui_window()
588
+
589
+
590
+ if __name__ == "__main__":
591
+ main()