td-task 0.2.3__tar.gz → 0.2.4__tar.gz
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.
- {td_task-0.2.3 → td_task-0.2.4}/PKG-INFO +2 -1
- {td_task-0.2.3 → td_task-0.2.4}/README.md +1 -0
- {td_task-0.2.3 → td_task-0.2.4}/pyproject.toml +1 -1
- {td_task-0.2.3 → td_task-0.2.4}/src/td/CHANGELOG.md +3 -0
- {td_task-0.2.3 → td_task-0.2.4}/src/td/tui.py +139 -18
- {td_task-0.2.3 → td_task-0.2.4}/tests/test_tui_notes.py +109 -1
- {td_task-0.2.3 → td_task-0.2.4}/uv.lock +1 -1
- {td_task-0.2.3 → td_task-0.2.4}/.env.enc +0 -0
- {td_task-0.2.3 → td_task-0.2.4}/.github/workflows/publish.yml +0 -0
- {td_task-0.2.3 → td_task-0.2.4}/.gitignore +0 -0
- {td_task-0.2.3 → td_task-0.2.4}/.python-version +0 -0
- {td_task-0.2.3 → td_task-0.2.4}/assets/demo.gif +0 -0
- {td_task-0.2.3 → td_task-0.2.4}/demo.tape +0 -0
- {td_task-0.2.3 → td_task-0.2.4}/src/td/__init__.py +0 -0
- {td_task-0.2.3 → td_task-0.2.4}/src/td/__main__.py +0 -0
- {td_task-0.2.3 → td_task-0.2.4}/src/td/db.py +0 -0
- {td_task-0.2.3 → td_task-0.2.4}/src/td/terminal.py +0 -0
- {td_task-0.2.3 → td_task-0.2.4}/tests/test_cli_notes.py +0 -0
- {td_task-0.2.3 → td_task-0.2.4}/tests/test_notes.py +0 -0
- {td_task-0.2.3 → td_task-0.2.4}/tests/test_update.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: td-task
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Fast TUI and CLI manager for tasks and Markdown notes
|
|
5
5
|
Requires-Python: >=3.14
|
|
6
6
|
Requires-Dist: cryptography>=42.0.0
|
|
@@ -123,6 +123,7 @@ td notes delete 12 --json
|
|
|
123
123
|
| `s` | Star task (pin to top, bold yellow) |
|
|
124
124
|
| `c` | Archive all completed tasks |
|
|
125
125
|
| `y` | Copy task list to clipboard |
|
|
126
|
+
| `f` | Filter notes by title in current list; submit empty query to clear |
|
|
126
127
|
| `↑` / `k` `↓` / `j` | Navigate tasks |
|
|
127
128
|
| `PageUp` / `PageDown` | Move one viewport |
|
|
128
129
|
| `Ctrl+↑` / `Ctrl+↓` | Reorder task position |
|
|
@@ -112,6 +112,7 @@ td notes delete 12 --json
|
|
|
112
112
|
| `s` | Star task (pin to top, bold yellow) |
|
|
113
113
|
| `c` | Archive all completed tasks |
|
|
114
114
|
| `y` | Copy task list to clipboard |
|
|
115
|
+
| `f` | Filter notes by title in current list; submit empty query to clear |
|
|
115
116
|
| `↑` / `k` `↓` / `j` | Navigate tasks |
|
|
116
117
|
| `PageUp` / `PageDown` | Move one viewport |
|
|
117
118
|
| `Ctrl+↑` / `Ctrl+↓` | Reorder task position |
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.4] - 2026-08-05
|
|
4
|
+
- Add live, case-insensitive note-title filtering with `f` in the current list.
|
|
5
|
+
|
|
3
6
|
## [0.2.3] - 2026-08-05
|
|
4
7
|
- Scroll large task lists inside a fixed header and footer viewport.
|
|
5
8
|
- Buffer complete TUI frames to prevent flicker and partial header rendering.
|
|
@@ -155,7 +155,7 @@ def _ensure_list_unlocked(list_name: str) -> bool:
|
|
|
155
155
|
|
|
156
156
|
|
|
157
157
|
def _normal_hint_text(lock_list: bool = False) -> str:
|
|
158
|
-
parts = ["a:task", "A:note", "e/Enter:edit", "E:rename note", "d:delete", "Space:done", "s:star", "c:clear"]
|
|
158
|
+
parts = ["f:filter", "a:task", "A:note", "e/Enter:edit", "E:rename note", "d:delete", "Space:done", "s:star", "c:clear"]
|
|
159
159
|
if not lock_list:
|
|
160
160
|
parts.append("l:view lists")
|
|
161
161
|
parts.append("q:quit")
|
|
@@ -185,6 +185,7 @@ def _render_help_screen(lock_list: bool = False) -> None:
|
|
|
185
185
|
console.print(" s Toggle star/priority (pin to top)")
|
|
186
186
|
console.print(" c Archive all completed tasks")
|
|
187
187
|
console.print(" y Copy active tasks in list to clipboard")
|
|
188
|
+
console.print(" f Filter notes by title in current list")
|
|
188
189
|
console.print()
|
|
189
190
|
|
|
190
191
|
console.print(Text("Note Editor:", style="bold yellow"))
|
|
@@ -237,7 +238,7 @@ def _render_help_screen(lock_list: bool = False) -> None:
|
|
|
237
238
|
|
|
238
239
|
def _task_visible_rows(mode: str, status_msg: str, term_height: int) -> int:
|
|
239
240
|
"""Return task rows that fit above the footer and transient messages."""
|
|
240
|
-
reserved_rows = 2 if mode in ("new_note", "confirm") or status_msg else 0
|
|
241
|
+
reserved_rows = 2 if mode in ("new_note", "note_filter", "confirm") or status_msg else 0
|
|
241
242
|
return max(1, term_height - 6 - reserved_rows)
|
|
242
243
|
|
|
243
244
|
|
|
@@ -308,6 +309,19 @@ def _task_page_hover(
|
|
|
308
309
|
return 0
|
|
309
310
|
|
|
310
311
|
|
|
312
|
+
def _filter_notes(tasks: list[dict], query: str) -> list[dict]:
|
|
313
|
+
"""Return notes whose titles contain the case-insensitive query."""
|
|
314
|
+
normalized_query = query.strip().casefold()
|
|
315
|
+
if not normalized_query:
|
|
316
|
+
return tasks
|
|
317
|
+
return [
|
|
318
|
+
task
|
|
319
|
+
for task in tasks
|
|
320
|
+
if task.get("is_note")
|
|
321
|
+
and normalized_query in task.get("text", "").casefold()
|
|
322
|
+
]
|
|
323
|
+
|
|
324
|
+
|
|
311
325
|
@_buffered_frame
|
|
312
326
|
def _render_main(
|
|
313
327
|
tasks: list[dict],
|
|
@@ -323,6 +337,7 @@ def _render_main(
|
|
|
323
337
|
lists_scroll: int = 0,
|
|
324
338
|
tasks_scroll: int = 0,
|
|
325
339
|
fuzzy_scroll: int = 0,
|
|
340
|
+
note_filter: str = "",
|
|
326
341
|
) -> None:
|
|
327
342
|
divider_width = min(len(_normal_hint_text(lock_list)), console.width or 80)
|
|
328
343
|
|
|
@@ -331,7 +346,7 @@ def _render_main(
|
|
|
331
346
|
header = Text("td • ", style="bold")
|
|
332
347
|
header.append(Text("lists menu", style="bold cyan"))
|
|
333
348
|
header.append(Text(f" • {len(lists)} lists", style="dim"))
|
|
334
|
-
console.print(header, end="\033[K\n")
|
|
349
|
+
console.print(header, end="\033[K\n", overflow="crop", no_wrap=True)
|
|
335
350
|
else:
|
|
336
351
|
open_count = sum(1 for t in tasks if t["status"] == "active")
|
|
337
352
|
completed_count = db.get_completed_count(list_name)
|
|
@@ -341,7 +356,10 @@ def _render_main(
|
|
|
341
356
|
header.append(Text(f"{open_count} open", style="dim"))
|
|
342
357
|
header.append(Text(" / ", style="dim"))
|
|
343
358
|
header.append(Text(f"{completed_count} completed", style="dim"))
|
|
344
|
-
|
|
359
|
+
if note_filter:
|
|
360
|
+
header.append(Text(" • ", style="dim"))
|
|
361
|
+
header.append(Text(f'filter: "{note_filter}"', style="bold yellow"))
|
|
362
|
+
console.print(header, end="\033[K\n", overflow="crop", no_wrap=True)
|
|
345
363
|
|
|
346
364
|
# Determine hints
|
|
347
365
|
if mode == "edit":
|
|
@@ -357,6 +375,8 @@ def _render_main(
|
|
|
357
375
|
hint_parts = ["Enter:confirm", "Esc:cancel"]
|
|
358
376
|
elif mode == "fuzzy_list":
|
|
359
377
|
hint_parts = ["Esc:cancel", "Enter:go to list", "↑/↓:navigate", "PgUp/PgDn:page"]
|
|
378
|
+
elif mode == "note_filter":
|
|
379
|
+
hint_parts = ["Esc:cancel", "Enter:apply", "type:filter note titles", "empty:clear"]
|
|
360
380
|
else:
|
|
361
381
|
# mode == "normal"
|
|
362
382
|
if view == "lists_menu":
|
|
@@ -365,7 +385,7 @@ def _render_main(
|
|
|
365
385
|
"Enter:open", "q:quit",
|
|
366
386
|
]
|
|
367
387
|
else:
|
|
368
|
-
hint_parts = ["PgUp/PgDn:page", "a:task", "A:note", "e/Enter:edit", "E:rename note", "d:delete", "Space:done", "s:star", "c:clear"]
|
|
388
|
+
hint_parts = ["PgUp/PgDn:page", "f:filter", "a:task", "A:note", "e/Enter:edit", "E:rename note", "d:delete", "Space:done", "s:star", "c:clear"]
|
|
369
389
|
if not lock_list:
|
|
370
390
|
hint_parts.append("l:view lists")
|
|
371
391
|
hint_parts.append("q:quit")
|
|
@@ -582,7 +602,12 @@ def _render_main(
|
|
|
582
602
|
prev_starred = is_starred
|
|
583
603
|
|
|
584
604
|
if not tasks:
|
|
585
|
-
|
|
605
|
+
empty_message = (
|
|
606
|
+
" No matching notes."
|
|
607
|
+
if note_filter
|
|
608
|
+
else " No items. Press a for a task or A for a note."
|
|
609
|
+
)
|
|
610
|
+
lines.append(Text(empty_message, style="dim"))
|
|
586
611
|
|
|
587
612
|
visible_rows = _task_visible_rows(mode, status_msg, console.height or 24)
|
|
588
613
|
visible_lines = lines[tasks_scroll:tasks_scroll + visible_rows]
|
|
@@ -591,7 +616,16 @@ def _render_main(
|
|
|
591
616
|
for _ in range(visible_rows - len(visible_lines)):
|
|
592
617
|
console.print(end="\033[K\n")
|
|
593
618
|
|
|
594
|
-
if mode == "
|
|
619
|
+
if mode == "note_filter":
|
|
620
|
+
console.print(end="\033[K\n")
|
|
621
|
+
input_line = Text(" Filter notes > ", style="yellow bold")
|
|
622
|
+
input_line.append(Text(edit_text[:edit_cursor], style="yellow bold"))
|
|
623
|
+
char_under = edit_text[edit_cursor] if edit_cursor < len(edit_text) else " "
|
|
624
|
+
input_line.append(Text(char_under, style="reverse yellow bold"))
|
|
625
|
+
if edit_cursor < len(edit_text):
|
|
626
|
+
input_line.append(Text(edit_text[edit_cursor + 1:], style="yellow bold"))
|
|
627
|
+
console.print(input_line, end="\033[K\n", overflow="crop", no_wrap=True)
|
|
628
|
+
elif mode == "new_note":
|
|
595
629
|
console.print(end="\033[K\n")
|
|
596
630
|
console.print(Text(" Note name", style="bold yellow"), end="\033[K\n")
|
|
597
631
|
input_line = Text(" > ", style="yellow bold")
|
|
@@ -1205,6 +1239,7 @@ def _run_main_loop(list_name: str = "main", lock_list: bool = False) -> None:
|
|
|
1205
1239
|
lists_scroll = 0
|
|
1206
1240
|
tasks_scroll = 0
|
|
1207
1241
|
fuzzy_scroll = 0
|
|
1242
|
+
note_filter = ""
|
|
1208
1243
|
|
|
1209
1244
|
current_list = list_name
|
|
1210
1245
|
if not lock_list and active_lists and current_list not in active_lists:
|
|
@@ -1226,7 +1261,9 @@ def _run_main_loop(list_name: str = "main", lock_list: bool = False) -> None:
|
|
|
1226
1261
|
hover = 0
|
|
1227
1262
|
continue
|
|
1228
1263
|
|
|
1229
|
-
|
|
1264
|
+
all_tasks = db.get_active_tasks(current_list) if view == "tasks" else []
|
|
1265
|
+
displayed_filter = edit_text if mode == "note_filter" else note_filter
|
|
1266
|
+
tasks = _filter_notes(all_tasks, displayed_filter)
|
|
1230
1267
|
|
|
1231
1268
|
# Determine and clamp hovers
|
|
1232
1269
|
if mode == "fuzzy_list":
|
|
@@ -1318,6 +1355,7 @@ def _run_main_loop(list_name: str = "main", lock_list: bool = False) -> None:
|
|
|
1318
1355
|
lists_scroll,
|
|
1319
1356
|
tasks_scroll,
|
|
1320
1357
|
fuzzy_scroll,
|
|
1358
|
+
displayed_filter,
|
|
1321
1359
|
)
|
|
1322
1360
|
status_msg = ""
|
|
1323
1361
|
|
|
@@ -1360,12 +1398,16 @@ def _run_main_loop(list_name: str = "main", lock_list: bool = False) -> None:
|
|
|
1360
1398
|
edit_cursor = min(len(edit_text), edit_cursor + 1)
|
|
1361
1399
|
elif key == term.KEY_ENTER:
|
|
1362
1400
|
if matched:
|
|
1363
|
-
|
|
1401
|
+
selected_list = matched[hover]
|
|
1402
|
+
if selected_list != current_list:
|
|
1403
|
+
note_filter = ""
|
|
1404
|
+
current_list = selected_list
|
|
1364
1405
|
else:
|
|
1365
1406
|
cleaned_name = edit_text.strip()
|
|
1366
1407
|
if cleaned_name:
|
|
1367
1408
|
db.create_list(cleaned_name)
|
|
1368
1409
|
current_list = cleaned_name
|
|
1410
|
+
note_filter = ""
|
|
1369
1411
|
mode = "normal"
|
|
1370
1412
|
view = "tasks"
|
|
1371
1413
|
edit_text = ""
|
|
@@ -1378,6 +1420,70 @@ def _run_main_loop(list_name: str = "main", lock_list: bool = False) -> None:
|
|
|
1378
1420
|
edit_text = edit_text[:edit_cursor] + key + edit_text[edit_cursor:]
|
|
1379
1421
|
edit_cursor += 1
|
|
1380
1422
|
|
|
1423
|
+
elif mode == "note_filter":
|
|
1424
|
+
if key == term.KEY_ESC:
|
|
1425
|
+
mode = "normal"
|
|
1426
|
+
edit_text = ""
|
|
1427
|
+
edit_cursor = 0
|
|
1428
|
+
hover = 0
|
|
1429
|
+
tasks_scroll = 0
|
|
1430
|
+
elif key == term.KEY_ENTER:
|
|
1431
|
+
note_filter = edit_text.strip()
|
|
1432
|
+
mode = "normal"
|
|
1433
|
+
edit_text = ""
|
|
1434
|
+
edit_cursor = 0
|
|
1435
|
+
hover = 0
|
|
1436
|
+
tasks_scroll = 0
|
|
1437
|
+
elif key == term.KEY_ARROW_UP:
|
|
1438
|
+
hover = max(0, hover - 1)
|
|
1439
|
+
elif key == term.KEY_ARROW_DOWN:
|
|
1440
|
+
hover = min(max(0, len(tasks) - 1), hover + 1)
|
|
1441
|
+
elif key == term.KEY_PAGE_UP:
|
|
1442
|
+
hover = _task_page_hover(
|
|
1443
|
+
tasks,
|
|
1444
|
+
hover,
|
|
1445
|
+
min(len(_normal_hint_text(lock_list)), console.width or 80),
|
|
1446
|
+
_task_visible_rows(mode, status_msg, console.height or 24),
|
|
1447
|
+
-1,
|
|
1448
|
+
)
|
|
1449
|
+
elif key == term.KEY_PAGE_DOWN:
|
|
1450
|
+
hover = _task_page_hover(
|
|
1451
|
+
tasks,
|
|
1452
|
+
hover,
|
|
1453
|
+
min(len(_normal_hint_text(lock_list)), console.width or 80),
|
|
1454
|
+
_task_visible_rows(mode, status_msg, console.height or 24),
|
|
1455
|
+
1,
|
|
1456
|
+
)
|
|
1457
|
+
elif key in (term.KEY_CMD_BACKSPACE, term.KEY_CTRL_BACKSPACE):
|
|
1458
|
+
edit_text = ""
|
|
1459
|
+
edit_cursor = 0
|
|
1460
|
+
hover = 0
|
|
1461
|
+
tasks_scroll = 0
|
|
1462
|
+
elif key == term.KEY_BACKSPACE:
|
|
1463
|
+
if edit_cursor > 0:
|
|
1464
|
+
edit_text = edit_text[:edit_cursor - 1] + edit_text[edit_cursor:]
|
|
1465
|
+
edit_cursor -= 1
|
|
1466
|
+
hover = 0
|
|
1467
|
+
tasks_scroll = 0
|
|
1468
|
+
elif key == term.KEY_DELETE:
|
|
1469
|
+
if edit_cursor < len(edit_text):
|
|
1470
|
+
edit_text = edit_text[:edit_cursor] + edit_text[edit_cursor + 1:]
|
|
1471
|
+
hover = 0
|
|
1472
|
+
tasks_scroll = 0
|
|
1473
|
+
elif key == term.KEY_ARROW_LEFT:
|
|
1474
|
+
edit_cursor = max(0, edit_cursor - 1)
|
|
1475
|
+
elif key == term.KEY_ARROW_RIGHT:
|
|
1476
|
+
edit_cursor = min(len(edit_text), edit_cursor + 1)
|
|
1477
|
+
elif key == term.KEY_HOME:
|
|
1478
|
+
edit_cursor = 0
|
|
1479
|
+
elif key == term.KEY_END:
|
|
1480
|
+
edit_cursor = len(edit_text)
|
|
1481
|
+
elif len(key) == 1 and ord(key) >= 32:
|
|
1482
|
+
edit_text = edit_text[:edit_cursor] + key + edit_text[edit_cursor:]
|
|
1483
|
+
edit_cursor += 1
|
|
1484
|
+
hover = 0
|
|
1485
|
+
tasks_scroll = 0
|
|
1486
|
+
|
|
1381
1487
|
elif mode == "normal":
|
|
1382
1488
|
if view == "lists_menu":
|
|
1383
1489
|
if key in ("q", term.KEY_ESC):
|
|
@@ -1408,7 +1514,10 @@ def _run_main_loop(list_name: str = "main", lock_list: bool = False) -> None:
|
|
|
1408
1514
|
elif key == term.KEY_ENTER:
|
|
1409
1515
|
lists = db.get_all_lists()
|
|
1410
1516
|
if lists:
|
|
1411
|
-
|
|
1517
|
+
selected_list = lists[hover]
|
|
1518
|
+
if selected_list != current_list:
|
|
1519
|
+
note_filter = ""
|
|
1520
|
+
current_list = selected_list
|
|
1412
1521
|
view = "tasks"
|
|
1413
1522
|
hover = 0
|
|
1414
1523
|
tasks_scroll = 0
|
|
@@ -1485,6 +1594,12 @@ def _run_main_loop(list_name: str = "main", lock_list: bool = False) -> None:
|
|
|
1485
1594
|
hover = 0
|
|
1486
1595
|
fuzzy_scroll = 0
|
|
1487
1596
|
continue
|
|
1597
|
+
elif key == "f":
|
|
1598
|
+
mode = "note_filter"
|
|
1599
|
+
edit_text = note_filter
|
|
1600
|
+
edit_cursor = len(edit_text)
|
|
1601
|
+
hover = 0
|
|
1602
|
+
tasks_scroll = 0
|
|
1488
1603
|
elif key in (term.KEY_ARROW_UP, "k"):
|
|
1489
1604
|
if hover > 0:
|
|
1490
1605
|
hover -= 1
|
|
@@ -1534,6 +1649,8 @@ def _run_main_loop(list_name: str = "main", lock_list: bool = False) -> None:
|
|
|
1534
1649
|
if current_list in lists:
|
|
1535
1650
|
curr_idx = lists.index(current_list)
|
|
1536
1651
|
next_idx = max(0, curr_idx - 1)
|
|
1652
|
+
if next_idx != curr_idx:
|
|
1653
|
+
note_filter = ""
|
|
1537
1654
|
current_list = lists[next_idx]
|
|
1538
1655
|
hover = 0
|
|
1539
1656
|
tasks_scroll = 0
|
|
@@ -1542,6 +1659,8 @@ def _run_main_loop(list_name: str = "main", lock_list: bool = False) -> None:
|
|
|
1542
1659
|
if current_list in lists:
|
|
1543
1660
|
curr_idx = lists.index(current_list)
|
|
1544
1661
|
next_idx = min(len(lists) - 1, curr_idx + 1)
|
|
1662
|
+
if next_idx != curr_idx:
|
|
1663
|
+
note_filter = ""
|
|
1545
1664
|
current_list = lists[next_idx]
|
|
1546
1665
|
hover = 0
|
|
1547
1666
|
tasks_scroll = 0
|
|
@@ -1555,15 +1674,15 @@ def _run_main_loop(list_name: str = "main", lock_list: bool = False) -> None:
|
|
|
1555
1674
|
hover += 1
|
|
1556
1675
|
elif key == term.KEY_ALT_ARROW_UP:
|
|
1557
1676
|
limit = db.get_max_tasks(current_list)
|
|
1558
|
-
if tasks and hover > 0 and len(
|
|
1677
|
+
if tasks and hover > 0 and len(all_tasks) < limit:
|
|
1559
1678
|
db.duplicate_task(tasks[hover]["id"], -1)
|
|
1560
1679
|
tasks = db.get_active_tasks(current_list)
|
|
1561
1680
|
hover -= 1
|
|
1562
|
-
elif tasks and len(
|
|
1681
|
+
elif tasks and len(all_tasks) >= limit:
|
|
1563
1682
|
status_msg = f"max tasks reached ({limit})"
|
|
1564
1683
|
elif key == term.KEY_ALT_ARROW_DOWN:
|
|
1565
1684
|
limit = db.get_max_tasks(current_list)
|
|
1566
|
-
if tasks and len(
|
|
1685
|
+
if tasks and len(all_tasks) < limit:
|
|
1567
1686
|
db.duplicate_task(tasks[hover]["id"], 1)
|
|
1568
1687
|
tasks = db.get_active_tasks(current_list)
|
|
1569
1688
|
hover += 1
|
|
@@ -1587,7 +1706,7 @@ def _run_main_loop(list_name: str = "main", lock_list: bool = False) -> None:
|
|
|
1587
1706
|
edit_cursor = len(edit_text)
|
|
1588
1707
|
elif key == "a":
|
|
1589
1708
|
limit = db.get_max_tasks(current_list)
|
|
1590
|
-
if len(
|
|
1709
|
+
if len(all_tasks) < limit:
|
|
1591
1710
|
new_task = db.add_task("", current_list)
|
|
1592
1711
|
if new_task:
|
|
1593
1712
|
tasks = db.get_active_tasks(current_list)
|
|
@@ -1600,7 +1719,7 @@ def _run_main_loop(list_name: str = "main", lock_list: bool = False) -> None:
|
|
|
1600
1719
|
status_msg = f"max tasks reached ({limit})"
|
|
1601
1720
|
elif key == "A":
|
|
1602
1721
|
limit = db.get_max_tasks(current_list)
|
|
1603
|
-
if len(
|
|
1722
|
+
if len(all_tasks) < limit:
|
|
1604
1723
|
edit_text = _default_note_title()
|
|
1605
1724
|
edit_cursor = len(edit_text)
|
|
1606
1725
|
mode = "new_note"
|
|
@@ -1625,10 +1744,9 @@ def _run_main_loop(list_name: str = "main", lock_list: bool = False) -> None:
|
|
|
1625
1744
|
db.toggle_starred(tasks[hover]["id"])
|
|
1626
1745
|
elif key == "y":
|
|
1627
1746
|
# Yank copy
|
|
1628
|
-
|
|
1629
|
-
if active_tasks:
|
|
1747
|
+
if tasks:
|
|
1630
1748
|
lines = []
|
|
1631
|
-
for t in
|
|
1749
|
+
for t in tasks:
|
|
1632
1750
|
marker = "★" if t.get("starred", 0) == 1 else "○"
|
|
1633
1751
|
lines.append(f"{marker} {t['text']}")
|
|
1634
1752
|
content = "\n".join(lines)
|
|
@@ -1660,6 +1778,7 @@ def _run_main_loop(list_name: str = "main", lock_list: bool = False) -> None:
|
|
|
1660
1778
|
lists = db.get_all_lists()
|
|
1661
1779
|
if current_list == confirm_list_name:
|
|
1662
1780
|
current_list = lists[0] if lists else "main"
|
|
1781
|
+
note_filter = ""
|
|
1663
1782
|
hover = 0
|
|
1664
1783
|
view = "lists_menu"
|
|
1665
1784
|
elif confirm_action == "archive_list":
|
|
@@ -1667,6 +1786,7 @@ def _run_main_loop(list_name: str = "main", lock_list: bool = False) -> None:
|
|
|
1667
1786
|
lists = db.get_all_lists()
|
|
1668
1787
|
if current_list == confirm_list_name:
|
|
1669
1788
|
current_list = lists[0] if lists else "main"
|
|
1789
|
+
note_filter = ""
|
|
1670
1790
|
hover = min(hover, max(0, len(lists) - 1))
|
|
1671
1791
|
view = "lists_menu"
|
|
1672
1792
|
mode = "normal"
|
|
@@ -1775,6 +1895,7 @@ def _run_main_loop(list_name: str = "main", lock_list: bool = False) -> None:
|
|
|
1775
1895
|
if cleaned_name:
|
|
1776
1896
|
db.create_list(cleaned_name)
|
|
1777
1897
|
current_list = cleaned_name
|
|
1898
|
+
note_filter = ""
|
|
1778
1899
|
hover = 0
|
|
1779
1900
|
view = "tasks"
|
|
1780
1901
|
tasks_scroll = 0
|
|
@@ -160,6 +160,88 @@ class NoteTuiTests(unittest.TestCase):
|
|
|
160
160
|
self.assertEqual(ends, [2, 4])
|
|
161
161
|
self.assertEqual(total, 4)
|
|
162
162
|
|
|
163
|
+
def test_note_filter_matches_note_titles_only(self) -> None:
|
|
164
|
+
tasks = [
|
|
165
|
+
{"text": "Alpha task", "is_note": False},
|
|
166
|
+
{"text": "Alpha Journal", "is_note": True},
|
|
167
|
+
{"text": "Beta Journal", "is_note": True},
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
self.assertEqual(tui._filter_notes(tasks, "alpha"), [tasks[1]])
|
|
171
|
+
self.assertEqual(tui._filter_notes(tasks, " JOURNAL "), tasks[1:])
|
|
172
|
+
self.assertEqual(tui._filter_notes(tasks, ""), tasks)
|
|
173
|
+
|
|
174
|
+
def test_f_filters_notes_in_current_list(self) -> None:
|
|
175
|
+
db.create_list("main")
|
|
176
|
+
db.add_task("Beta task", "main")
|
|
177
|
+
db.add_note("Alpha Journal", "main")
|
|
178
|
+
db.add_note("Beta Journal", "main")
|
|
179
|
+
frames: list[tuple[str, list[str], str]] = []
|
|
180
|
+
keys = iter(["f", "B", "E", "T", "A", terminal.KEY_ENTER, "q"])
|
|
181
|
+
|
|
182
|
+
with (
|
|
183
|
+
patch.object(terminal, "read_key", side_effect=lambda: next(keys)),
|
|
184
|
+
patch.object(
|
|
185
|
+
tui,
|
|
186
|
+
"_render_main",
|
|
187
|
+
side_effect=lambda *args: frames.append(
|
|
188
|
+
(args[2], [task["text"] for task in args[0]], args[-1])
|
|
189
|
+
),
|
|
190
|
+
),
|
|
191
|
+
):
|
|
192
|
+
tui._run_main_loop("main", lock_list=True)
|
|
193
|
+
|
|
194
|
+
self.assertEqual(frames[-1], ("normal", ["Beta Journal"], "BETA"))
|
|
195
|
+
|
|
196
|
+
def test_empty_note_filter_clears_active_filter(self) -> None:
|
|
197
|
+
db.create_list("main")
|
|
198
|
+
db.add_task("Task", "main")
|
|
199
|
+
db.add_note("Alpha Journal", "main")
|
|
200
|
+
frames: list[tuple[list[str], str]] = []
|
|
201
|
+
keys = iter([
|
|
202
|
+
"f", "a", "l", "p", "h", "a", terminal.KEY_ENTER,
|
|
203
|
+
"f", terminal.KEY_CMD_BACKSPACE, terminal.KEY_ENTER, "q",
|
|
204
|
+
])
|
|
205
|
+
|
|
206
|
+
with (
|
|
207
|
+
patch.object(terminal, "read_key", side_effect=lambda: next(keys)),
|
|
208
|
+
patch.object(
|
|
209
|
+
tui,
|
|
210
|
+
"_render_main",
|
|
211
|
+
side_effect=lambda *args: frames.append(
|
|
212
|
+
([task["text"] for task in args[0]], args[-1])
|
|
213
|
+
),
|
|
214
|
+
),
|
|
215
|
+
):
|
|
216
|
+
tui._run_main_loop("main", lock_list=True)
|
|
217
|
+
|
|
218
|
+
self.assertEqual(frames[-1], (["Task", "Alpha Journal"], ""))
|
|
219
|
+
|
|
220
|
+
def test_escape_cancels_note_filter_edits(self) -> None:
|
|
221
|
+
db.create_list("main")
|
|
222
|
+
db.add_note("Alpha Journal", "main")
|
|
223
|
+
db.add_note("Beta Journal", "main")
|
|
224
|
+
frames: list[tuple[list[str], str]] = []
|
|
225
|
+
keys = iter([
|
|
226
|
+
"f", "a", "l", "p", "h", "a", terminal.KEY_ENTER,
|
|
227
|
+
"f", terminal.KEY_CMD_BACKSPACE, "b", "e", "t", "a",
|
|
228
|
+
terminal.KEY_ESC, "q",
|
|
229
|
+
])
|
|
230
|
+
|
|
231
|
+
with (
|
|
232
|
+
patch.object(terminal, "read_key", side_effect=lambda: next(keys)),
|
|
233
|
+
patch.object(
|
|
234
|
+
tui,
|
|
235
|
+
"_render_main",
|
|
236
|
+
side_effect=lambda *args: frames.append(
|
|
237
|
+
([task["text"] for task in args[0]], args[-1])
|
|
238
|
+
),
|
|
239
|
+
),
|
|
240
|
+
):
|
|
241
|
+
tui._run_main_loop("main", lock_list=True)
|
|
242
|
+
|
|
243
|
+
self.assertEqual(frames[-1], (["Alpha Journal"], "alpha"))
|
|
244
|
+
|
|
163
245
|
def test_main_loop_scrolls_large_task_lists(self) -> None:
|
|
164
246
|
db.create_list("main")
|
|
165
247
|
db.set_max_tasks(40, "main")
|
|
@@ -176,7 +258,7 @@ class NoteTuiTests(unittest.TestCase):
|
|
|
176
258
|
patch.object(
|
|
177
259
|
tui,
|
|
178
260
|
"_render_main",
|
|
179
|
-
side_effect=lambda *args: scroll_positions.append(args[-
|
|
261
|
+
side_effect=lambda *args: scroll_positions.append(args[-3]),
|
|
180
262
|
),
|
|
181
263
|
):
|
|
182
264
|
tui._run_main_loop("main", lock_list=True)
|
|
@@ -252,6 +334,32 @@ class NoteTuiTests(unittest.TestCase):
|
|
|
252
334
|
self.assertFalse(frame.endswith("\n\033[J"))
|
|
253
335
|
self.assertEqual(output.write_count, 1, [repr(value) for value in output.writes])
|
|
254
336
|
|
|
337
|
+
output.seek(0)
|
|
338
|
+
output.truncate(0)
|
|
339
|
+
output.write_count = 0
|
|
340
|
+
output.writes.clear()
|
|
341
|
+
|
|
342
|
+
with (
|
|
343
|
+
patch.object(tui, "console", test_console),
|
|
344
|
+
patch.object(terminal, "reset_cursor"),
|
|
345
|
+
patch.object(tui.sys.stdout, "write"),
|
|
346
|
+
patch.object(tui.sys.stdout, "flush"),
|
|
347
|
+
):
|
|
348
|
+
tui._render_main(
|
|
349
|
+
tasks[:1],
|
|
350
|
+
hover=0,
|
|
351
|
+
mode="note_filter",
|
|
352
|
+
edit_text="Task" * 40,
|
|
353
|
+
edit_cursor=160,
|
|
354
|
+
list_name="main",
|
|
355
|
+
note_filter="Task" * 40,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
filter_frame = output.getvalue()
|
|
359
|
+
self.assertEqual(filter_frame.count("\n"), 9)
|
|
360
|
+
self.assertTrue(filter_frame.startswith("\033[Htd • main"))
|
|
361
|
+
self.assertEqual(output.write_count, 1, [repr(value) for value in output.writes])
|
|
362
|
+
|
|
255
363
|
def test_alt_arrows_move_editor_lines(self) -> None:
|
|
256
364
|
note = db.add_note("Movement")
|
|
257
365
|
assert note is not None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|