td-task 0.2.4__tar.gz → 0.2.6__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.
Files changed (30) hide show
  1. td_task-0.2.6/.github/workflows/pages.yml +50 -0
  2. {td_task-0.2.4 → td_task-0.2.6}/PKG-INFO +43 -3
  3. {td_task-0.2.4 → td_task-0.2.6}/README.md +41 -1
  4. td_task-0.2.6/index.html +78 -0
  5. {td_task-0.2.4 → td_task-0.2.6}/pyproject.toml +1 -1
  6. {td_task-0.2.4 → td_task-0.2.6}/src/td/CHANGELOG.md +13 -0
  7. {td_task-0.2.4 → td_task-0.2.6}/src/td/__main__.py +100 -0
  8. td_task-0.2.6/src/td/backup.py +304 -0
  9. td_task-0.2.6/src/td/config.py +55 -0
  10. {td_task-0.2.4 → td_task-0.2.6}/src/td/terminal.py +72 -20
  11. {td_task-0.2.4 → td_task-0.2.6}/src/td/tui.py +391 -70
  12. td_task-0.2.6/tests/test_backup.py +129 -0
  13. {td_task-0.2.4 → td_task-0.2.6}/tests/test_cli_notes.py +7 -0
  14. td_task-0.2.6/tests/test_resize.py +333 -0
  15. {td_task-0.2.4 → td_task-0.2.6}/tests/test_tui_notes.py +89 -0
  16. {td_task-0.2.4 → td_task-0.2.6}/uv.lock +1 -1
  17. td_task-0.2.6/website/README.md +29 -0
  18. td_task-0.2.6/website/favicon.svg +4 -0
  19. td_task-0.2.6/website/main.js +21 -0
  20. td_task-0.2.6/website/style.css +57 -0
  21. {td_task-0.2.4 → td_task-0.2.6}/.env.enc +0 -0
  22. {td_task-0.2.4 → td_task-0.2.6}/.github/workflows/publish.yml +0 -0
  23. {td_task-0.2.4 → td_task-0.2.6}/.gitignore +0 -0
  24. {td_task-0.2.4 → td_task-0.2.6}/.python-version +0 -0
  25. {td_task-0.2.4 → td_task-0.2.6}/assets/demo.gif +0 -0
  26. {td_task-0.2.4 → td_task-0.2.6}/demo.tape +0 -0
  27. {td_task-0.2.4 → td_task-0.2.6}/src/td/__init__.py +0 -0
  28. {td_task-0.2.4 → td_task-0.2.6}/src/td/db.py +0 -0
  29. {td_task-0.2.4 → td_task-0.2.6}/tests/test_notes.py +0 -0
  30. {td_task-0.2.4 → td_task-0.2.6}/tests/test_update.py +0 -0
@@ -0,0 +1,50 @@
1
+ name: Publish homepage
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ push:
6
+ branches: [main]
7
+ paths:
8
+ - index.html
9
+ - website/**
10
+ - assets/demo.gif
11
+ - .github/workflows/pages.yml
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ concurrency:
17
+ group: github-pages
18
+ cancel-in-progress: false
19
+
20
+ jobs:
21
+ deploy:
22
+ runs-on: ubuntu-latest
23
+ permissions:
24
+ pages: write
25
+ id-token: write
26
+ environment:
27
+ name: github-pages
28
+ url: ${{ steps.deployment.outputs.page_url }}
29
+ steps:
30
+ - name: Checkout
31
+ uses: actions/checkout@v6
32
+
33
+ - name: Configure Pages
34
+ uses: actions/configure-pages@v5
35
+
36
+ - name: Stage public files
37
+ run: |
38
+ mkdir -p _site/assets
39
+ cp index.html _site/
40
+ cp -R website _site/website
41
+ cp assets/demo.gif _site/assets/
42
+
43
+ - name: Upload site
44
+ uses: actions/upload-pages-artifact@v4
45
+ with:
46
+ path: _site
47
+
48
+ - name: Deploy
49
+ id: deployment
50
+ uses: actions/deploy-pages@v4
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: td-task
3
- Version: 0.2.4
3
+ Version: 0.2.6
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
@@ -66,6 +66,8 @@ td [command] [--list <name>]
66
66
  | `td archive` | Open the archive screen |
67
67
  | `td export [file]` | Export database to JSON (stdout if no file) |
68
68
  | `td import <file>` | Merge a JSON backup into the database |
69
+ | `td backup github [repo]` | Encrypt and upload a backup to a private GitHub repo |
70
+ | `td restore github [repo]` | Download and restore an encrypted GitHub backup |
69
71
  | `td update` | Upgrade to the latest version |
70
72
  | `td --version` | Print the installed version |
71
73
  | `td --help` | Print the help menu |
@@ -110,6 +112,9 @@ td notes delete 12 --json
110
112
 
111
113
  ## TUI Keybindings
112
114
 
115
+ The TUI redraws automatically when you resize the terminal, keeping the current
116
+ selection and unsaved edits. Help supports arrow-key and PageUp/PageDown scrolling.
117
+
113
118
  ### Tasks
114
119
 
115
120
  | Key | Action |
@@ -121,6 +126,7 @@ td notes delete 12 --json
121
126
  | `d` | Delete selected task |
122
127
  | `Space` | Toggle done / active |
123
128
  | `s` | Star task (pin to top, bold yellow) |
129
+ | `S` | Cycle task sort: A-Z / Z-A / original order |
124
130
  | `c` | Archive all completed tasks |
125
131
  | `y` | Copy task list to clipboard |
126
132
  | `f` | Filter notes by title in current list; submit empty query to clear |
@@ -203,7 +209,8 @@ marker. Press `Enter` or `Backspace` on an empty continuation to leave list mode
203
209
  | `q` / `Esc` | Return |
204
210
 
205
211
  Settings you can change: max tasks per list (3–50), max starred tasks, whole-database
206
- encryption, current-list encryption, and backup export / import.
212
+ encryption, current-list encryption, backup export / import, GitHub backup repo,
213
+ GitHub backup now, auto-sync, and GitHub backup encryption.
207
214
 
208
215
  ## Agent-friendly
209
216
 
@@ -288,9 +295,42 @@ td export > backup.json
288
295
 
289
296
  # restore on another machine
290
297
  td import backup.json
298
+
299
+ # encrypt and upload to a private GitHub repo (creates td-backup if missing)
300
+ td backup github --save-password
301
+ td backup github lucaspon/my-td-backup --save-password
302
+
303
+ # optional: store plain JSON instead of an encrypted backup
304
+ td backup github --no-encrypt
305
+
306
+ # restore and merge from GitHub
307
+ td restore github
291
308
  ```
292
309
 
293
310
  You can encrypt the whole database or only the current list from settings (`/`).
294
311
  Each encrypted list uses its own password-derived key. Tasks and note bodies stay
295
312
  encrypted in JSON backups. Whole-database and per-list encryption cannot run
296
313
  together. If you forget a password, there is no recovery.
314
+
315
+ By default, GitHub backups add a separate password-based encryption layer around
316
+ the full JSON export. In encrypted mode, `td` sends only the encrypted envelope
317
+ to GitHub. The `gh` CLI must be
318
+ installed and authenticated. `td backup github` creates the target repository as
319
+ private when it does not exist, and refuses to write to a public repository.
320
+
321
+ Auto-sync uses `~/.config/td/config.toml`:
322
+
323
+ ```toml
324
+ [github_backup]
325
+ auto_sync = true
326
+ repo = "owner/td-backup"
327
+ encrypt = true
328
+ ```
329
+
330
+ Auto-sync runs after TUI sessions and CLI writes. Encrypted auto-sync needs a
331
+ password saved in macOS Keychain with `td backup github --save-password`.
332
+ Set `encrypt = false` to
333
+ store plain JSON. The repository remains private, but GitHub can then read the
334
+ backup. Encrypted mode remains the default. `td` stores a digest of the last
335
+ successful export and skips the GitHub update when the data has not changed.
336
+ Manual backups update the same digest.
@@ -55,6 +55,8 @@ td [command] [--list <name>]
55
55
  | `td archive` | Open the archive screen |
56
56
  | `td export [file]` | Export database to JSON (stdout if no file) |
57
57
  | `td import <file>` | Merge a JSON backup into the database |
58
+ | `td backup github [repo]` | Encrypt and upload a backup to a private GitHub repo |
59
+ | `td restore github [repo]` | Download and restore an encrypted GitHub backup |
58
60
  | `td update` | Upgrade to the latest version |
59
61
  | `td --version` | Print the installed version |
60
62
  | `td --help` | Print the help menu |
@@ -99,6 +101,9 @@ td notes delete 12 --json
99
101
 
100
102
  ## TUI Keybindings
101
103
 
104
+ The TUI redraws automatically when you resize the terminal, keeping the current
105
+ selection and unsaved edits. Help supports arrow-key and PageUp/PageDown scrolling.
106
+
102
107
  ### Tasks
103
108
 
104
109
  | Key | Action |
@@ -110,6 +115,7 @@ td notes delete 12 --json
110
115
  | `d` | Delete selected task |
111
116
  | `Space` | Toggle done / active |
112
117
  | `s` | Star task (pin to top, bold yellow) |
118
+ | `S` | Cycle task sort: A-Z / Z-A / original order |
113
119
  | `c` | Archive all completed tasks |
114
120
  | `y` | Copy task list to clipboard |
115
121
  | `f` | Filter notes by title in current list; submit empty query to clear |
@@ -192,7 +198,8 @@ marker. Press `Enter` or `Backspace` on an empty continuation to leave list mode
192
198
  | `q` / `Esc` | Return |
193
199
 
194
200
  Settings you can change: max tasks per list (3–50), max starred tasks, whole-database
195
- encryption, current-list encryption, and backup export / import.
201
+ encryption, current-list encryption, backup export / import, GitHub backup repo,
202
+ GitHub backup now, auto-sync, and GitHub backup encryption.
196
203
 
197
204
  ## Agent-friendly
198
205
 
@@ -277,9 +284,42 @@ td export > backup.json
277
284
 
278
285
  # restore on another machine
279
286
  td import backup.json
287
+
288
+ # encrypt and upload to a private GitHub repo (creates td-backup if missing)
289
+ td backup github --save-password
290
+ td backup github lucaspon/my-td-backup --save-password
291
+
292
+ # optional: store plain JSON instead of an encrypted backup
293
+ td backup github --no-encrypt
294
+
295
+ # restore and merge from GitHub
296
+ td restore github
280
297
  ```
281
298
 
282
299
  You can encrypt the whole database or only the current list from settings (`/`).
283
300
  Each encrypted list uses its own password-derived key. Tasks and note bodies stay
284
301
  encrypted in JSON backups. Whole-database and per-list encryption cannot run
285
302
  together. If you forget a password, there is no recovery.
303
+
304
+ By default, GitHub backups add a separate password-based encryption layer around
305
+ the full JSON export. In encrypted mode, `td` sends only the encrypted envelope
306
+ to GitHub. The `gh` CLI must be
307
+ installed and authenticated. `td backup github` creates the target repository as
308
+ private when it does not exist, and refuses to write to a public repository.
309
+
310
+ Auto-sync uses `~/.config/td/config.toml`:
311
+
312
+ ```toml
313
+ [github_backup]
314
+ auto_sync = true
315
+ repo = "owner/td-backup"
316
+ encrypt = true
317
+ ```
318
+
319
+ Auto-sync runs after TUI sessions and CLI writes. Encrypted auto-sync needs a
320
+ password saved in macOS Keychain with `td backup github --save-password`.
321
+ Set `encrypt = false` to
322
+ store plain JSON. The repository remains private, but GitHub can then read the
323
+ backup. Encrypted mode remains the default. `td` stores a digest of the last
324
+ successful export and skips the GitHub update when the data has not changed.
325
+ Manual backups update the same digest.
@@ -0,0 +1,78 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="theme-color" content="#141414">
7
+ <meta name="description" content="td: tasks and Markdown notes in your terminal.">
8
+ <meta property="og:type" content="website">
9
+ <meta property="og:title" content="td">
10
+ <meta property="og:description" content="Tasks and Markdown notes in your terminal.">
11
+ <title>td — terminal tasks and notes</title>
12
+ <link rel="icon" href="website/favicon.svg" type="image/svg+xml">
13
+ <link rel="stylesheet" href="website/style.css">
14
+ <script src="website/main.js" defer></script>
15
+ </head>
16
+ <body>
17
+ <a class="skip-link" href="#main">Skip to content</a>
18
+ <div class="page">
19
+ <header>
20
+ <p>td — tasks and Markdown notes in your terminal.</p>
21
+ </header>
22
+
23
+ <main id="main">
24
+ <ul class="features" aria-label="Features">
25
+ <li>Multiple lists and keyboard navigation.</li>
26
+ <li>Markdown notes with a built-in editor.</li>
27
+ <li>Optional database and list encryption.</li>
28
+ <li>JSON export and a scriptable CLI.</li>
29
+ </ul>
30
+ <figure>
31
+ <a href="assets/demo.gif" aria-label="Open the demo GIF at full size">
32
+ <img src="assets/demo.gif" width="680" height="360" alt="Demo of td: adding tasks, starring priorities, and switching lists with the keyboard." fetchpriority="high">
33
+ </a>
34
+ </figure>
35
+
36
+ <section id="install" aria-label="Installation and usage">
37
+ <dl class="commands">
38
+ <div>
39
+ <dt>install (uv)</dt>
40
+ <dd>
41
+ <code>uv tool install td-task</code>
42
+ <button type="button" data-copy="uv tool install td-task" aria-label="Copy uv install command" hidden>[copy]</button>
43
+ </dd>
44
+ </div>
45
+ <div>
46
+ <dt>or pip</dt>
47
+ <dd>
48
+ <code>pip install td-task</code>
49
+ <button type="button" data-copy="pip install td-task" aria-label="Copy pip install command" hidden>[copy]</button>
50
+ </dd>
51
+ </div>
52
+ <div>
53
+ <dt>run</dt>
54
+ <dd>
55
+ <code>td</code>
56
+ <button type="button" data-copy="td" aria-label="Copy td launch command" hidden>[copy]</button>
57
+ </dd>
58
+ </div>
59
+ <div>
60
+ <dt>or uvx</dt>
61
+ <dd>
62
+ <code>uvx td-task</code>
63
+ <button type="button" data-copy="uvx td-task" aria-label="Copy run without installing command" hidden>[copy]</button>
64
+ </dd>
65
+ </div>
66
+ </dl>
67
+ <p class="requirement">Python 3.14+. <a href="https://docs.astral.sh/uv/getting-started/installation/">uv docs</a>.</p>
68
+ <p class="copy-status" role="status" aria-live="polite"></p>
69
+ </section>
70
+ </main>
71
+
72
+ <footer>
73
+ <a href="https://github.com/lucaspon/td">github.com/lucaspon/td</a>
74
+ <a href="https://twitter.com/lucasponn">@lucasponn</a>
75
+ </footer>
76
+ </div>
77
+ </body>
78
+ </html>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "td-task"
3
- version = "0.2.4"
3
+ version = "0.2.6"
4
4
  description = "Fast TUI and CLI manager for tasks and Markdown notes"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.14"
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.6] - 2026-11-04
4
+ - Back up and restore data through private GitHub repositories using the gh CLI.
5
+ - Encrypt backups by default, with optional plain JSON storage.
6
+ - Add configurable auto-sync that skips unchanged data and supports macOS Keychain passwords.
7
+ - Add Sync settings, backup progress screens, and a link to open the GitHub repository.
8
+ - Support backups to new and existing empty repositories.
9
+
10
+ ## [0.2.5] - 2026-11-04
11
+ - Redraw automatically when terminal dimensions change, without waiting for a keypress.
12
+ - Preserve selection, unsaved edits, note cursor, and confirmations while resizing.
13
+ - Fit frames to smaller windows and reflow note content when the terminal grows.
14
+ - Scroll help with arrow keys and PageUp/PageDown.
15
+
3
16
  ## [0.2.4] - 2026-08-05
4
17
  - Add live, case-insensitive note-title filtering with `f` in the current list.
5
18
 
@@ -72,6 +72,10 @@ def main() -> None:
72
72
  _run_export(args)
73
73
  elif len(args) > 1 and args[1] in ("import", "-import", "--import"):
74
74
  _run_import(args)
75
+ elif len(args) > 1 and args[1] in ("backup", "-backup", "--backup"):
76
+ _run_backup(args)
77
+ elif len(args) > 1 and args[1] in ("restore", "-restore", "--restore"):
78
+ _run_restore(args)
75
79
  else:
76
80
  from . import db
77
81
  lists = db.get_all_lists()
@@ -123,6 +127,8 @@ def _run_help() -> None:
123
127
  commands_table.add_row(" archive", "Open TUI directly in the completed archive screen")
124
128
  commands_table.add_row(" export [file]", "Export database to JSON file or print to stdout")
125
129
  commands_table.add_row(" import <file>", "Import and merge database records from a JSON file")
130
+ commands_table.add_row(" backup github [repo]", "Encrypt and upload a backup to a private GitHub repo")
131
+ commands_table.add_row(" restore github [repo]", "Download and restore an encrypted GitHub backup")
126
132
  commands_table.add_row(" update", "Upgrade the `td` package to the latest version")
127
133
 
128
134
  console.print(commands_table)
@@ -136,6 +142,8 @@ def _run_help() -> None:
136
142
 
137
143
  options_table.add_row(" -l, --list <name>", "Specify/create list context (TUI list-lock, CLI scope)")
138
144
  options_table.add_row(" --dev", "Watch source code directory and restart interactive TUI on changes")
145
+ options_table.add_row(" --no-encrypt", "Store a GitHub backup as plain JSON (private repo required)")
146
+ options_table.add_row(" --save-password", "Save encrypted backup password in macOS Keychain for auto-sync")
139
147
  options_table.add_row(" -h, --help", "Print this detailed, LLM-friendly help menu and exit")
140
148
 
141
149
  console.print(options_table)
@@ -256,6 +264,7 @@ def _run_notes(list_name: str, has_list: bool, args: list[str]) -> None:
256
264
  print(json.dumps(note, indent=2))
257
265
  else:
258
266
  print(f"✓ Note added to '{list_name}' (note ID: {note['id']}, task ID: {note['task_id']})")
267
+ _auto_sync()
259
268
  return
260
269
 
261
270
  if action == "list":
@@ -315,6 +324,7 @@ def _run_notes(list_name: str, has_list: bool, args: list[str]) -> None:
315
324
  print(json.dumps(updated, indent=2))
316
325
  else:
317
326
  print(f"✓ Note {note_id} updated.")
327
+ _auto_sync()
318
328
  return
319
329
 
320
330
  deleted = db.delete_note(note_id)
@@ -322,6 +332,7 @@ def _run_notes(list_name: str, has_list: bool, args: list[str]) -> None:
322
332
  print(json.dumps({"id": note_id, "deleted": deleted}))
323
333
  else:
324
334
  print(f"✓ Note {note_id} deleted.")
335
+ _auto_sync()
325
336
 
326
337
 
327
338
  def _run_add(list_name: str, args: list[str]) -> None:
@@ -338,6 +349,7 @@ def _run_add(list_name: str, args: list[str]) -> None:
338
349
  print("✗ Failed to add task (maximum active tasks reached).")
339
350
  sys.exit(1)
340
351
  print(f"✓ Task added successfully to list '{list_name}' (ID: {result['id']})")
352
+ _auto_sync()
341
353
 
342
354
 
343
355
  def _run_list(list_name: str) -> None:
@@ -571,10 +583,98 @@ def _run_import(args: list[str]) -> None:
571
583
  json_str = f.read()
572
584
  db.import_from_json(json_str)
573
585
  print(f"✓ Database successfully imported and merged from {filepath}")
586
+ _auto_sync()
574
587
  except Exception as e:
575
588
  print(f"✗ Import failed: {e}")
576
589
  sys.exit(1)
577
590
 
578
591
 
592
+ def _github_backup_repo(args: list[str]) -> str:
593
+ repo = _option_value(args, "--repo")
594
+ if repo is None and len(args) > 3 and not args[3].startswith("-"):
595
+ repo = args[3]
596
+ return (repo or "td-backup").strip()
597
+
598
+
599
+ def _backup_password(*, confirm: bool) -> str:
600
+ import getpass
601
+
602
+ try:
603
+ password = getpass.getpass("Backup password: ")
604
+ if confirm:
605
+ repeated = getpass.getpass("Confirm backup password: ")
606
+ if password != repeated:
607
+ raise ValueError("Backup passwords do not match")
608
+ if not password:
609
+ raise ValueError("Backup password cannot be empty")
610
+ return password
611
+ except (KeyboardInterrupt, EOFError):
612
+ print("\nCancelled.")
613
+ sys.exit(0)
614
+
615
+
616
+ def _auto_sync() -> None:
617
+ from . import backup, config, db
618
+
619
+ try:
620
+ repo = backup.sync_if_configured(db.export_to_json())
621
+ if repo:
622
+ print(
623
+ f"✓ auto-synced backup to {repo}/{backup.BACKUP_FILENAME}",
624
+ file=sys.stderr,
625
+ )
626
+ except Exception as error:
627
+ print(f"✗ auto-sync failed: {error}", file=sys.stderr)
628
+
629
+
630
+ def _run_backup(args: list[str]) -> None:
631
+ if len(args) < 3 or args[2] != "github":
632
+ print("Usage: td backup github [repo|--repo <owner/name>] [--no-encrypt] [--save-password]")
633
+ sys.exit(1)
634
+
635
+ from . import backup, config, db
636
+
637
+ _cli_ensure_unlocked()
638
+ try:
639
+ repo_name = _github_backup_repo(args)
640
+ should_encrypt = "--no-encrypt" not in args
641
+ if not should_encrypt and "--save-password" in args:
642
+ raise ValueError("--save-password requires encrypted backup mode")
643
+ password = _backup_password(confirm=True) if should_encrypt else None
644
+ data = db.export_to_json()
645
+ payload = backup.encrypt_backup(data, password) if password else data
646
+ repo = backup.upload_backup(repo_name, payload)
647
+ config.set_github_backup(
648
+ repo=repo,
649
+ last_sync_digest=backup.backup_digest(data, should_encrypt),
650
+ )
651
+ if password and "--save-password" in args:
652
+ backup.store_password(repo, password)
653
+ mode = "encrypted" if password else "unencrypted"
654
+ print(f"✓ {mode} backup uploaded to {repo}/{backup.BACKUP_FILENAME}")
655
+ except Exception as error:
656
+ print(f"✗ GitHub backup failed: {error}")
657
+ sys.exit(1)
658
+
659
+
660
+ def _run_restore(args: list[str]) -> None:
661
+ if len(args) < 3 or args[2] != "github":
662
+ print("Usage: td restore github [repo|--repo <owner/name>]")
663
+ sys.exit(1)
664
+
665
+ from . import backup, db
666
+
667
+ _cli_ensure_unlocked()
668
+ try:
669
+ payload = backup.download_backup(_github_backup_repo(args))
670
+ password = _backup_password(confirm=False) if backup.is_encrypted_backup(payload) else None
671
+ db.import_from_json(backup.decode_backup(payload, password))
672
+ mode = "encrypted" if password else "unencrypted"
673
+ print(f"✓ {mode} GitHub backup restored and merged")
674
+ except Exception as error:
675
+ print(f"✗ GitHub restore failed: {error}")
676
+ sys.exit(1)
677
+
678
+
579
679
  if __name__ == "__main__":
580
680
  main()