chen-todo 0.1.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.
- chen_todo-0.1.0.dist-info/METADATA +76 -0
- chen_todo-0.1.0.dist-info/RECORD +9 -0
- chen_todo-0.1.0.dist-info/WHEEL +4 -0
- chen_todo-0.1.0.dist-info/entry_points.txt +2 -0
- chen_todo-0.1.0.dist-info/licenses/LICENSE +21 -0
- todo_cli/__init__.py +3 -0
- todo_cli/cli.py +124 -0
- todo_cli/core.py +83 -0
- todo_cli/storage.py +41 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chen-todo
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 一个简单友好的命令行待办清单工具
|
|
5
|
+
Author: chen
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: cli,productivity,task,todo,待办
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Utilities
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# todo-cli
|
|
17
|
+
|
|
18
|
+
一个简单友好的命令行待办清单工具。用一条命令管理你的待办事项,数据保存在本地,随时可用。
|
|
19
|
+
|
|
20
|
+
## ✨ 功能
|
|
21
|
+
|
|
22
|
+
- ➕ 添加任务
|
|
23
|
+
- 📋 查看任务(编号连续,删除后自动重排)
|
|
24
|
+
- ✅ 标记完成
|
|
25
|
+
- 🗑️ 删除任务
|
|
26
|
+
- 🧹 一键清除所有已完成
|
|
27
|
+
- 数据存在本地 JSON,中文友好,无需联网
|
|
28
|
+
|
|
29
|
+
## 📦 安装
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install todo-cli
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 🚀 使用
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
todo add 学会发布到PyPI
|
|
39
|
+
todo add 买 一盒 牛奶
|
|
40
|
+
todo list
|
|
41
|
+
todo done 1
|
|
42
|
+
todo remove 2
|
|
43
|
+
todo clear
|
|
44
|
+
todo --version
|
|
45
|
+
todo --help
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`todo list` 的输出长这样:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
[✓] 1 买牛奶
|
|
52
|
+
[ ] 2 学会发布到PyPI
|
|
53
|
+
|
|
54
|
+
共 2 个任务,1 个已完成
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## 💾 数据存在哪?
|
|
58
|
+
|
|
59
|
+
所有任务保存在你用户主目录下的 `.todo/tasks.json`:
|
|
60
|
+
|
|
61
|
+
- Windows:`C:\Users\你的用户名\.todo\tasks.json`
|
|
62
|
+
- macOS / Linux:`~/.todo/tasks.json`
|
|
63
|
+
|
|
64
|
+
## 🛠️ 参与开发
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
git clone <你的仓库地址>
|
|
68
|
+
cd todo-cli
|
|
69
|
+
uv sync
|
|
70
|
+
uv run pytest
|
|
71
|
+
uv run todo list
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## 📄 License
|
|
75
|
+
|
|
76
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
todo_cli/__init__.py,sha256=Y6Yh7JoMJ8EVqM_dcfJlXe6UjHH81M28zy0AXOwruMU,97
|
|
2
|
+
todo_cli/cli.py,sha256=yBh-F-UfJq9Tjr_lFIl8UlvLtY5K7xyJ9GaLs5xq1o4,3747
|
|
3
|
+
todo_cli/core.py,sha256=HrPpR9R3YlaQzB8diAAGXZ4BUi-rREXRQ0ZBhR9SffM,2694
|
|
4
|
+
todo_cli/storage.py,sha256=llgSKon65SuFYE2UtHerUWj3iLOM4leRaNg8fxWxiS8,1663
|
|
5
|
+
chen_todo-0.1.0.dist-info/METADATA,sha256=syDN-1TT6z3WtgpyA6HlbjdOocHyrEEWBv7K1-jowLc,1495
|
|
6
|
+
chen_todo-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
7
|
+
chen_todo-0.1.0.dist-info/entry_points.txt,sha256=tbLFspxat4zitLAWOPfWh7DJ_QgTNeKxfwkPKFSPb8s,43
|
|
8
|
+
chen_todo-0.1.0.dist-info/licenses/LICENSE,sha256=LGV1Ciet_f6yEbN2QVwQIgwpYVGlz7XsNGGh0Uc_A9Q,1069
|
|
9
|
+
chen_todo-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 你的名字
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
todo_cli/__init__.py
ADDED
todo_cli/cli.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""命令行界面:把「存取」(storage) 和「逻辑」(core) 拼起来,
|
|
2
|
+
并用 argparse 解析 add / list / done / remove / clear 等子命令。
|
|
3
|
+
|
|
4
|
+
pyproject.toml 里配的 todo 命令,最终执行的就是这里的 main()。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from todo_cli import __version__, core, storage
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# ---- storage 和 core 之间的「翻译」:dict ↔ Task ----
|
|
14
|
+
|
|
15
|
+
def _load():
|
|
16
|
+
return [core.Task.from_dict(d) for d in storage.load_tasks()]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _save(tasks):
|
|
20
|
+
storage.save_tasks([task.to_dict() for task in tasks])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ---- 每个子命令对应一个处理函数 ----
|
|
24
|
+
|
|
25
|
+
def cmd_add(args):
|
|
26
|
+
title = " ".join(args.title)
|
|
27
|
+
tasks = _load()
|
|
28
|
+
task = core.add_task(tasks, title)
|
|
29
|
+
_save(tasks)
|
|
30
|
+
print(f"已添加 #{len(tasks)}:{task.title}") # 新任务在末尾,编号 = 当前总数
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def cmd_list(args):
|
|
34
|
+
tasks = _load()
|
|
35
|
+
if not tasks:
|
|
36
|
+
print('还没有任务。用 todo add "买牛奶" 添加一个吧!')
|
|
37
|
+
return
|
|
38
|
+
width = len(str(len(tasks))) # 最大编号有几位数,用来对齐
|
|
39
|
+
done_count = 0
|
|
40
|
+
for number, task in enumerate(tasks, start=1): # 编号 = 位置,永远连续
|
|
41
|
+
box = "✓" if task.done else " "
|
|
42
|
+
if task.done:
|
|
43
|
+
done_count += 1
|
|
44
|
+
print(f"[{box}] {number:>{width}} {task.title}")
|
|
45
|
+
print()
|
|
46
|
+
print(f"共 {len(tasks)} 个任务,{done_count} 个已完成")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def cmd_done(args):
|
|
50
|
+
tasks = _load()
|
|
51
|
+
try:
|
|
52
|
+
task = core.mark_done(tasks, args.number)
|
|
53
|
+
except core.TaskNotFoundError as error:
|
|
54
|
+
print(error, file=sys.stderr)
|
|
55
|
+
sys.exit(1)
|
|
56
|
+
_save(tasks)
|
|
57
|
+
print(f"已完成 #{args.number}:{task.title}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def cmd_remove(args):
|
|
61
|
+
tasks = _load()
|
|
62
|
+
try:
|
|
63
|
+
task = core.remove_task(tasks, args.number)
|
|
64
|
+
except core.TaskNotFoundError as error:
|
|
65
|
+
print(error, file=sys.stderr)
|
|
66
|
+
sys.exit(1)
|
|
67
|
+
_save(tasks)
|
|
68
|
+
print(f"已删除:{task.title}") # 删除后编号会重排,这里就不提编号了
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def cmd_clear(args):
|
|
72
|
+
tasks = _load()
|
|
73
|
+
count = core.clear_done(tasks)
|
|
74
|
+
_save(tasks)
|
|
75
|
+
print(f"已清除 {count} 个已完成的任务")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ---- 组装命令行解析器 ----
|
|
79
|
+
|
|
80
|
+
def main():
|
|
81
|
+
parser = argparse.ArgumentParser(
|
|
82
|
+
prog="todo",
|
|
83
|
+
description="一个简单友好的命令行待办清单工具。",
|
|
84
|
+
)
|
|
85
|
+
parser.add_argument(
|
|
86
|
+
"--version", action="version", version=f"%(prog)s {__version__}"
|
|
87
|
+
)
|
|
88
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
89
|
+
|
|
90
|
+
# todo add 买牛奶
|
|
91
|
+
p_add = subparsers.add_parser("add", help="添加一个新任务")
|
|
92
|
+
p_add.add_argument("title", nargs="+", help="任务内容(可以不加引号)")
|
|
93
|
+
p_add.set_defaults(func=cmd_add)
|
|
94
|
+
|
|
95
|
+
# todo list
|
|
96
|
+
p_list = subparsers.add_parser("list", help="列出所有任务")
|
|
97
|
+
p_list.set_defaults(func=cmd_list)
|
|
98
|
+
|
|
99
|
+
# todo done 1
|
|
100
|
+
p_done = subparsers.add_parser("done", help="把某个任务标记为已完成")
|
|
101
|
+
p_done.add_argument("number", type=int, help="任务编号(就是 list 里显示的数字)")
|
|
102
|
+
p_done.set_defaults(func=cmd_done)
|
|
103
|
+
|
|
104
|
+
# todo remove 1
|
|
105
|
+
p_remove = subparsers.add_parser("remove", help="删除某个任务")
|
|
106
|
+
p_remove.add_argument("number", type=int, help="任务编号(就是 list 里显示的数字)")
|
|
107
|
+
p_remove.set_defaults(func=cmd_remove)
|
|
108
|
+
|
|
109
|
+
# todo clear
|
|
110
|
+
p_clear = subparsers.add_parser("clear", help="清除所有已完成的任务")
|
|
111
|
+
p_clear.set_defaults(func=cmd_clear)
|
|
112
|
+
|
|
113
|
+
args = parser.parse_args()
|
|
114
|
+
|
|
115
|
+
# 用户只敲了 todo、没给子命令 → 打印帮助
|
|
116
|
+
if not hasattr(args, "func"):
|
|
117
|
+
parser.print_help()
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
args.func(args)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if __name__ == "__main__":
|
|
124
|
+
main()
|
todo_cli/core.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""核心逻辑层:定义「任务」的结构,以及增、删、改、查的纯逻辑。
|
|
2
|
+
|
|
3
|
+
这一层只跟「内存里的任务列表」打交道,不碰文件、也不管命令行。
|
|
4
|
+
|
|
5
|
+
关于「编号」:我们不给任务存固定的 id,而是用它在清单里的「位置」当编号
|
|
6
|
+
(第 1 个就是 1、第 2 个就是 2……)。这样一删任务,后面的自动往前补位,
|
|
7
|
+
编号永远是连续的 1、2、3——和 ufw 等工具的行为一致。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TaskNotFoundError(Exception):
|
|
15
|
+
"""给的编号在当前清单里不存在(越界)时抛出。"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, number):
|
|
18
|
+
self.number = number
|
|
19
|
+
super().__init__(f"找不到编号为 {number} 的任务")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Task:
|
|
24
|
+
"""一个待办任务。注意:没有 id 字段,编号靠「位置」决定。"""
|
|
25
|
+
|
|
26
|
+
title: str
|
|
27
|
+
done: bool = False
|
|
28
|
+
created_at: str = ""
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def from_dict(cls, data):
|
|
32
|
+
# 用 .get() 给默认值:老数据即使还带着多余的 "id" 字段也照样能读,
|
|
33
|
+
# 缺 created_at 也不会崩。
|
|
34
|
+
return cls(
|
|
35
|
+
title=data["title"],
|
|
36
|
+
done=data.get("done", False),
|
|
37
|
+
created_at=data.get("created_at", ""),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def to_dict(self):
|
|
41
|
+
return {
|
|
42
|
+
"title": self.title,
|
|
43
|
+
"done": self.done,
|
|
44
|
+
"created_at": self.created_at,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _task_at(tasks, number):
|
|
49
|
+
"""按「从 1 开始的编号」取任务;越界就抛 TaskNotFoundError。"""
|
|
50
|
+
if number < 1 or number > len(tasks):
|
|
51
|
+
raise TaskNotFoundError(number)
|
|
52
|
+
return tasks[number - 1]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def add_task(tasks, title):
|
|
56
|
+
"""新建一个任务,追加到清单末尾,并返回它。"""
|
|
57
|
+
task = Task(
|
|
58
|
+
title=title,
|
|
59
|
+
done=False,
|
|
60
|
+
created_at=datetime.now().isoformat(timespec="seconds"),
|
|
61
|
+
)
|
|
62
|
+
tasks.append(task)
|
|
63
|
+
return task
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def mark_done(tasks, number):
|
|
67
|
+
"""把第 number 个任务标记为已完成,返回它;编号非法就抛异常。"""
|
|
68
|
+
task = _task_at(tasks, number)
|
|
69
|
+
task.done = True
|
|
70
|
+
return task
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def remove_task(tasks, number):
|
|
74
|
+
"""删除第 number 个任务,返回被删掉的那个;编号非法就抛异常。"""
|
|
75
|
+
_task_at(tasks, number) # 先校验编号(越界会抛异常)
|
|
76
|
+
return tasks.pop(number - 1) # 按位置删除,后面的自动补位
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def clear_done(tasks):
|
|
80
|
+
"""删除所有已完成的任务,返回删掉的数量。"""
|
|
81
|
+
before = len(tasks)
|
|
82
|
+
tasks[:] = [task for task in tasks if not task.done] # 原地只保留未完成的
|
|
83
|
+
return before - len(tasks)
|
todo_cli/storage.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""数据存储层:负责把任务列表读写到本地 JSON 文件。
|
|
2
|
+
|
|
3
|
+
这一层只关心「数据怎么存到硬盘、怎么读回来」,
|
|
4
|
+
完全不关心任务的业务逻辑(增删改查是 core.py 的事)。
|
|
5
|
+
把「存储」和「逻辑」分开的好处:将来想换存储方式
|
|
6
|
+
(比如换成数据库),只改这一个文件就行,别处都不用动。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
# 数据文件放在「用户主目录」下的 .todo 文件夹里。
|
|
13
|
+
# Path.home() 在你的电脑上是 C:\Users\chen,
|
|
14
|
+
# 所以数据文件就是 C:\Users\chen\.todo\tasks.json
|
|
15
|
+
DATA_DIR = Path.home() / ".todo"
|
|
16
|
+
DATA_FILE = DATA_DIR / "tasks.json"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_tasks(path=DATA_FILE):
|
|
20
|
+
"""读出所有任务,返回一个列表(里面每一项是一个 dict)。
|
|
21
|
+
|
|
22
|
+
- 文件还不存在(第一次用) → 返回空列表 []
|
|
23
|
+
- 文件是空的、或内容坏了 → 也返回空列表,保证程序不会崩
|
|
24
|
+
"""
|
|
25
|
+
if not path.exists():
|
|
26
|
+
return []
|
|
27
|
+
try:
|
|
28
|
+
text = path.read_text(encoding="utf-8")
|
|
29
|
+
return json.loads(text)
|
|
30
|
+
except (json.JSONDecodeError, ValueError):
|
|
31
|
+
return []
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def save_tasks(tasks, path=DATA_FILE):
|
|
35
|
+
"""把任务列表写回文件。"""
|
|
36
|
+
# 确保 .todo 文件夹存在,不存在就自动创建(parents=True 连父目录一起建)
|
|
37
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
# ensure_ascii=False → 中文原样保存,不会变成 买 这种乱码
|
|
39
|
+
# indent=2 → 存成带缩进的漂亮格式,方便打开来看
|
|
40
|
+
text = json.dumps(tasks, ensure_ascii=False, indent=2)
|
|
41
|
+
path.write_text(text, encoding="utf-8")
|