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 +28 -0
- markora/cli.py +591 -0
- markora/config.py +82 -0
- markora/document.py +234 -0
- markora/favorites.py +67 -0
- markora/keep.py +513 -0
- markora/quicknote.py +33 -0
- markora/tui.py +1327 -0
- markora-1.0.0.dist-info/METADATA +294 -0
- markora-1.0.0.dist-info/RECORD +14 -0
- markora-1.0.0.dist-info/WHEEL +5 -0
- markora-1.0.0.dist-info/entry_points.txt +5 -0
- markora-1.0.0.dist-info/licenses/LICENSE +21 -0
- markora-1.0.0.dist-info/top_level.txt +1 -0
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()
|