td-task 0.1.0__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.1.0/.github/workflows/publish.yml +42 -0
- td_task-0.1.0/.gitignore +7 -0
- td_task-0.1.0/.python-version +1 -0
- td_task-0.1.0/PKG-INFO +8 -0
- td_task-0.1.0/README.md +45 -0
- td_task-0.1.0/pyproject.toml +23 -0
- td_task-0.1.0/src/td/__init__.py +0 -0
- td_task-0.1.0/src/td/__main__.py +160 -0
- td_task-0.1.0/src/td/db.py +452 -0
- td_task-0.1.0/src/td/terminal.py +148 -0
- td_task-0.1.0/src/td/tui.py +666 -0
- td_task-0.1.0/uv.lock +179 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
deploy:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- name: Checkout code
|
|
13
|
+
uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- name: Install uv
|
|
16
|
+
uses: astral-sh/setup-uv@v1
|
|
17
|
+
with:
|
|
18
|
+
enable-cache: true
|
|
19
|
+
|
|
20
|
+
- name: Install Doppler CLI
|
|
21
|
+
run: curl -Ls https://doppler.com/install.sh | sh
|
|
22
|
+
|
|
23
|
+
- name: Set up Python
|
|
24
|
+
uses: actions/setup-python@v5
|
|
25
|
+
with:
|
|
26
|
+
python-version: '3.12'
|
|
27
|
+
|
|
28
|
+
- name: Install build dependencies
|
|
29
|
+
run: uv pip install build twine
|
|
30
|
+
|
|
31
|
+
- name: Build package
|
|
32
|
+
# Using doppler run to ensure PYPI_API is available during the build process
|
|
33
|
+
run: doppler run -- python -m build
|
|
34
|
+
env:
|
|
35
|
+
DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }}
|
|
36
|
+
|
|
37
|
+
- name: Publish to PyPI
|
|
38
|
+
# Using doppler run to inject the PYPI_API secret into the twine environment
|
|
39
|
+
run: doppler run -- twine upload dist/*
|
|
40
|
+
env:
|
|
41
|
+
DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }}
|
|
42
|
+
TWINE_USERNAME: __token__
|
td_task-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14
|
td_task-0.1.0/PKG-INFO
ADDED
td_task-0.1.0/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# td
|
|
2
|
+
|
|
3
|
+
A minimal TUI todo CLI. No setup required — just run it.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install .
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or run directly with `uv`:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uv run td
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
td # Open the task manager
|
|
21
|
+
td archive # View archived tasks
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Keybindings
|
|
25
|
+
|
|
26
|
+
| Key | Action |
|
|
27
|
+
|-----|--------|
|
|
28
|
+
| ↑/k | Move up |
|
|
29
|
+
| ↓/j | Move down |
|
|
30
|
+
| Enter | Edit hovered item |
|
|
31
|
+
| n | Add new task (max 10) |
|
|
32
|
+
| d | Delete hovered task |
|
|
33
|
+
| Space | Toggle done |
|
|
34
|
+
| a | Archive all done tasks |
|
|
35
|
+
| q | Quit |
|
|
36
|
+
|
|
37
|
+
In edit mode, type to modify text, Enter to confirm, Esc to cancel.
|
|
38
|
+
|
|
39
|
+
## Storage
|
|
40
|
+
|
|
41
|
+
Tasks are stored in `~/.td.db` (portable SQLite file). Delete it to start fresh.
|
|
42
|
+
|
|
43
|
+
## Show some love
|
|
44
|
+
|
|
45
|
+
Ethereum: `0x88a0e1b80B92F0cFaa89a936b827Ce291cFb0028`
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "td-task"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Minimal TUI todo app"
|
|
5
|
+
requires-python = ">=3.14"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"rich>=15.0.0",
|
|
8
|
+
"watchdog>=6.0.0",
|
|
9
|
+
"cryptography>=42.0.0",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[project.scripts]
|
|
13
|
+
td = "td.__main__:main"
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["hatchling"]
|
|
17
|
+
build-backend = "hatchling.build"
|
|
18
|
+
|
|
19
|
+
[tool.hatch.build.targets.wheel]
|
|
20
|
+
packages = ["src/td"]
|
|
21
|
+
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
dev = []
|
|
File without changes
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from .tui import run_main, run_archive, run_settings
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main() -> None:
|
|
10
|
+
if "--dev" in sys.argv:
|
|
11
|
+
_run_dev()
|
|
12
|
+
elif len(sys.argv) > 1 and sys.argv[1] == "archive":
|
|
13
|
+
run_archive()
|
|
14
|
+
elif len(sys.argv) > 1 and sys.argv[1] == "update":
|
|
15
|
+
_run_update()
|
|
16
|
+
elif len(sys.argv) > 1 and sys.argv[1] == "add":
|
|
17
|
+
_run_add()
|
|
18
|
+
elif len(sys.argv) > 1 and sys.argv[1] == "list":
|
|
19
|
+
_run_list()
|
|
20
|
+
else:
|
|
21
|
+
run_main()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _cli_ensure_unlocked() -> None:
|
|
25
|
+
from . import db
|
|
26
|
+
if db.is_encryption_enabled():
|
|
27
|
+
import getpass
|
|
28
|
+
attempts = 0
|
|
29
|
+
while attempts < 3:
|
|
30
|
+
prompt_text = "Database is encrypted. Enter password: " if attempts == 0 else f"Incorrect password. Try again: "
|
|
31
|
+
try:
|
|
32
|
+
password = getpass.getpass(prompt_text)
|
|
33
|
+
except (KeyboardInterrupt, EOFError):
|
|
34
|
+
print("\nCancelled.")
|
|
35
|
+
sys.exit(0)
|
|
36
|
+
if db.set_encryption_key_from_password(password):
|
|
37
|
+
return
|
|
38
|
+
attempts += 1
|
|
39
|
+
print("✗ Too many incorrect attempts. Exiting.")
|
|
40
|
+
sys.exit(1)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _run_add() -> None:
|
|
44
|
+
if len(sys.argv) < 3 or not sys.argv[2].strip():
|
|
45
|
+
print("Usage: td add <task_text>")
|
|
46
|
+
sys.exit(1)
|
|
47
|
+
|
|
48
|
+
task_text = sys.argv[2].strip()
|
|
49
|
+
_cli_ensure_unlocked()
|
|
50
|
+
|
|
51
|
+
from . import db
|
|
52
|
+
result = db.add_task(task_text)
|
|
53
|
+
if result is None:
|
|
54
|
+
print("✗ Failed to add task (maximum active tasks reached).")
|
|
55
|
+
sys.exit(1)
|
|
56
|
+
print(f"✓ Task added successfully (ID: {result['id']})")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _run_list() -> None:
|
|
60
|
+
_cli_ensure_unlocked()
|
|
61
|
+
from . import db
|
|
62
|
+
from rich.console import Console
|
|
63
|
+
from rich.text import Text
|
|
64
|
+
|
|
65
|
+
tasks = db.get_active_tasks()
|
|
66
|
+
console = Console()
|
|
67
|
+
|
|
68
|
+
open_count = sum(1 for t in tasks if t["status"] == "active")
|
|
69
|
+
completed_count = db.get_completed_count()
|
|
70
|
+
header = Text("td • ", style="bold")
|
|
71
|
+
header.append(Text(f"{open_count} open", style="dim"))
|
|
72
|
+
header.append(Text(" / ", style="dim"))
|
|
73
|
+
header.append(Text(f"{completed_count} completed", style="dim"))
|
|
74
|
+
console.print(header)
|
|
75
|
+
|
|
76
|
+
console.print(Text("─" * 40, style="dim"))
|
|
77
|
+
|
|
78
|
+
if not tasks:
|
|
79
|
+
console.print(Text(" No tasks found.", style="dim"))
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
for i, task in enumerate(tasks, 1):
|
|
83
|
+
is_done = task["status"] == "done"
|
|
84
|
+
marker = "✓" if is_done else "○"
|
|
85
|
+
|
|
86
|
+
text = task["text"]
|
|
87
|
+
if is_done:
|
|
88
|
+
line_text = Text(text, style="strike dim")
|
|
89
|
+
marker_text = Text(marker, style="green bold")
|
|
90
|
+
else:
|
|
91
|
+
line_text = Text(text)
|
|
92
|
+
marker_text = Text(marker, style="yellow")
|
|
93
|
+
|
|
94
|
+
line = Text(" ")
|
|
95
|
+
line.append(marker_text)
|
|
96
|
+
line.append(" ")
|
|
97
|
+
line.append(line_text)
|
|
98
|
+
console.print(line)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _run_update() -> None:
|
|
102
|
+
"""Update td to the latest version from PyPI."""
|
|
103
|
+
import subprocess
|
|
104
|
+
print("Updating td...")
|
|
105
|
+
result = subprocess.run(
|
|
106
|
+
["uv", "tool", "upgrade", "td-task"],
|
|
107
|
+
capture_output=True, text=True, timeout=60,
|
|
108
|
+
)
|
|
109
|
+
if result.returncode == 0:
|
|
110
|
+
print("✓ td updated successfully")
|
|
111
|
+
else:
|
|
112
|
+
print(f"✗ update failed: {result.stderr.strip()}")
|
|
113
|
+
sys.exit(1)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _run_dev() -> None:
|
|
117
|
+
"""Watch src/td/ for changes and restart the TUI automatically."""
|
|
118
|
+
from watchdog.observers import Observer
|
|
119
|
+
from watchdog.events import FileSystemEventHandler
|
|
120
|
+
|
|
121
|
+
import subprocess
|
|
122
|
+
import time
|
|
123
|
+
|
|
124
|
+
src_dir = os.path.dirname(__file__)
|
|
125
|
+
|
|
126
|
+
class RestartHandler(FileSystemEventHandler):
|
|
127
|
+
def __init__(self):
|
|
128
|
+
self.changed = False
|
|
129
|
+
|
|
130
|
+
def on_modified(self, event):
|
|
131
|
+
if event.src_path.endswith(".py"):
|
|
132
|
+
self.changed = True
|
|
133
|
+
|
|
134
|
+
def on_created(self, event):
|
|
135
|
+
if event.src_path.endswith(".py"):
|
|
136
|
+
self.changed = True
|
|
137
|
+
|
|
138
|
+
observer = Observer()
|
|
139
|
+
handler = RestartHandler()
|
|
140
|
+
observer.schedule(handler, src_dir, recursive=True)
|
|
141
|
+
observer.start()
|
|
142
|
+
|
|
143
|
+
print("td --dev: watching for changes... (Ctrl+C to stop)")
|
|
144
|
+
subprocess.run(["uv", "run", "td"])
|
|
145
|
+
try:
|
|
146
|
+
while True:
|
|
147
|
+
time.sleep(0.5)
|
|
148
|
+
if handler.changed:
|
|
149
|
+
handler.changed = False
|
|
150
|
+
print("\n⟳ Change detected, restarting...\n")
|
|
151
|
+
subprocess.run(["uv", "run", "td"])
|
|
152
|
+
except KeyboardInterrupt:
|
|
153
|
+
pass
|
|
154
|
+
finally:
|
|
155
|
+
observer.stop()
|
|
156
|
+
observer.join()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
if __name__ == "__main__":
|
|
160
|
+
main()
|