dida-cli 0.2.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.
- dida/__init__.py +3 -0
- dida/cli.py +302 -0
- dida/commands/__init__.py +1 -0
- dida/config.py +40 -0
- dida/const.py +13 -0
- dida/db/__init__.py +10 -0
- dida/db/connection.py +37 -0
- dida/db/schema.py +59 -0
- dida_cli-0.2.0.dist-info/METADATA +119 -0
- dida_cli-0.2.0.dist-info/RECORD +13 -0
- dida_cli-0.2.0.dist-info/WHEEL +4 -0
- dida_cli-0.2.0.dist-info/entry_points.txt +2 -0
- dida_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
dida/__init__.py
ADDED
dida/cli.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""CLI 主入口
|
|
2
|
+
|
|
3
|
+
使用 Typer 构建命令行接口,使用 dong-core 的 json_output 装饰器。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from dong import json_output, ValidationError, NotFoundError
|
|
9
|
+
|
|
10
|
+
from . import __version__
|
|
11
|
+
from .db.connection import init_db, get_connection
|
|
12
|
+
from .const import DB_PATH, PRIORITIES
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(
|
|
15
|
+
name="dida",
|
|
16
|
+
help=f"事咚咚 - 个人待办管理 CLI (v{__version__})",
|
|
17
|
+
no_args_is_help=True,
|
|
18
|
+
add_completion=False,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@app.callback()
|
|
23
|
+
def main_callback(
|
|
24
|
+
version: bool = typer.Option(False, "--version", "-v", help="显示版本"),
|
|
25
|
+
) -> None:
|
|
26
|
+
"""主回调"""
|
|
27
|
+
if version:
|
|
28
|
+
typer.echo(f"dida {__version__}")
|
|
29
|
+
raise typer.Exit()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@app.command()
|
|
33
|
+
@json_output
|
|
34
|
+
def init():
|
|
35
|
+
"""初始化数据库"""
|
|
36
|
+
init_db()
|
|
37
|
+
return {
|
|
38
|
+
"message": "数据库初始化成功",
|
|
39
|
+
"db_path": str(DB_PATH)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.command()
|
|
44
|
+
@json_output
|
|
45
|
+
def add(
|
|
46
|
+
content: str = typer.Argument(..., help="待办内容"),
|
|
47
|
+
due: str = typer.Option(None, "--due", "-d", help="截止时间 (YYYY-MM-DD HH:MM)"),
|
|
48
|
+
priority: str = typer.Option("medium", "--priority", "-p",
|
|
49
|
+
help=f"优先级: {','.join(PRIORITIES)}"),
|
|
50
|
+
note: str = typer.Option(None, "--note", "-n", help="备注"),
|
|
51
|
+
):
|
|
52
|
+
"""创建待办"""
|
|
53
|
+
if not content or not content.strip():
|
|
54
|
+
raise ValidationError("content", "待办内容不能为空")
|
|
55
|
+
|
|
56
|
+
if priority not in PRIORITIES:
|
|
57
|
+
raise ValidationError("priority", f"无效的优先级: {priority}")
|
|
58
|
+
|
|
59
|
+
with get_connection() as conn:
|
|
60
|
+
cursor = conn.cursor()
|
|
61
|
+
now = datetime.now().isoformat()
|
|
62
|
+
cursor.execute(
|
|
63
|
+
"""
|
|
64
|
+
INSERT INTO todos (content, due_date, priority, note, created_at, updated_at)
|
|
65
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
66
|
+
""",
|
|
67
|
+
(content.strip(), due, priority, note, now, now)
|
|
68
|
+
)
|
|
69
|
+
todo_id = cursor.lastrowid
|
|
70
|
+
|
|
71
|
+
return {"id": todo_id, "content": content, "priority": priority}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@app.command()
|
|
75
|
+
@json_output
|
|
76
|
+
def ls(
|
|
77
|
+
limit: int = typer.Option(20, "--limit", "-l", help="显示数量"),
|
|
78
|
+
completed: bool = typer.Option(None, "--completed", help="按完成状态筛选"),
|
|
79
|
+
priority: str = typer.Option(None, "--priority", "-p", help="按优先级筛选"),
|
|
80
|
+
):
|
|
81
|
+
"""列出待办"""
|
|
82
|
+
with get_connection() as conn:
|
|
83
|
+
cursor = conn.cursor()
|
|
84
|
+
|
|
85
|
+
conditions = []
|
|
86
|
+
params = []
|
|
87
|
+
|
|
88
|
+
if completed is not None:
|
|
89
|
+
conditions.append("completed = ?")
|
|
90
|
+
params.append(1 if completed else 0)
|
|
91
|
+
|
|
92
|
+
if priority:
|
|
93
|
+
conditions.append("priority = ?")
|
|
94
|
+
params.append(priority)
|
|
95
|
+
|
|
96
|
+
where_clause = " AND ".join(conditions) if conditions else "1=1"
|
|
97
|
+
params.append(limit)
|
|
98
|
+
|
|
99
|
+
cursor.execute(
|
|
100
|
+
f"""
|
|
101
|
+
SELECT id, content, completed, priority, due_date, created_at, note
|
|
102
|
+
FROM todos
|
|
103
|
+
WHERE {where_clause}
|
|
104
|
+
ORDER BY completed ASC, created_at DESC
|
|
105
|
+
LIMIT ?
|
|
106
|
+
""",
|
|
107
|
+
params
|
|
108
|
+
)
|
|
109
|
+
rows = cursor.fetchall()
|
|
110
|
+
|
|
111
|
+
todos = [dict(row) for row in rows]
|
|
112
|
+
return {"todos": todos, "total": len(todos)}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@app.command()
|
|
116
|
+
@json_output
|
|
117
|
+
def get(
|
|
118
|
+
todo_id: int = typer.Argument(..., help="待办 ID"),
|
|
119
|
+
):
|
|
120
|
+
"""获取待办详情"""
|
|
121
|
+
with get_connection() as conn:
|
|
122
|
+
cursor = conn.cursor()
|
|
123
|
+
cursor.execute(
|
|
124
|
+
"SELECT * FROM todos WHERE id = ?",
|
|
125
|
+
(todo_id,)
|
|
126
|
+
)
|
|
127
|
+
row = cursor.fetchone()
|
|
128
|
+
|
|
129
|
+
if not row:
|
|
130
|
+
raise NotFoundError("Todo", todo_id, message=f"未找到 ID 为 {todo_id} 的待办")
|
|
131
|
+
|
|
132
|
+
return dict(row)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@app.command()
|
|
136
|
+
@json_output
|
|
137
|
+
def done(
|
|
138
|
+
todo_id: int = typer.Argument(..., help="待办 ID"),
|
|
139
|
+
):
|
|
140
|
+
"""标记完成"""
|
|
141
|
+
with get_connection() as conn:
|
|
142
|
+
cursor = conn.cursor()
|
|
143
|
+
cursor.execute("SELECT id FROM todos WHERE id = ?", (todo_id,))
|
|
144
|
+
if not cursor.fetchone():
|
|
145
|
+
raise NotFoundError("Todo", todo_id)
|
|
146
|
+
|
|
147
|
+
now = datetime.now().isoformat()
|
|
148
|
+
cursor.execute(
|
|
149
|
+
"UPDATE todos SET completed = 1, updated_at = ? WHERE id = ?",
|
|
150
|
+
(now, todo_id)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return {"id": todo_id, "completed": True}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@app.command()
|
|
157
|
+
@json_output
|
|
158
|
+
def undo(
|
|
159
|
+
todo_id: int = typer.Argument(..., help="待办 ID"),
|
|
160
|
+
):
|
|
161
|
+
"""取消完成"""
|
|
162
|
+
with get_connection() as conn:
|
|
163
|
+
cursor = conn.cursor()
|
|
164
|
+
cursor.execute("SELECT id FROM todos WHERE id = ?", (todo_id,))
|
|
165
|
+
if not cursor.fetchone():
|
|
166
|
+
raise NotFoundError("Todo", todo_id)
|
|
167
|
+
|
|
168
|
+
now = datetime.now().isoformat()
|
|
169
|
+
cursor.execute(
|
|
170
|
+
"UPDATE todos SET completed = 0, updated_at = ? WHERE id = ?",
|
|
171
|
+
(now, todo_id)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return {"id": todo_id, "completed": False}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@app.command()
|
|
178
|
+
@json_output
|
|
179
|
+
def update(
|
|
180
|
+
todo_id: int = typer.Argument(..., help="待办 ID"),
|
|
181
|
+
content: str = typer.Option(None, "--content", "-c", help="更新内容"),
|
|
182
|
+
due: str = typer.Option(None, "--due", "-d", help="更新截止时间"),
|
|
183
|
+
priority: str = typer.Option(None, "--priority", "-p", help="更新优先级"),
|
|
184
|
+
note: str = typer.Option(None, "--note", "-n", help="更新备注"),
|
|
185
|
+
):
|
|
186
|
+
"""更新待办"""
|
|
187
|
+
with get_connection() as conn:
|
|
188
|
+
cursor = conn.cursor()
|
|
189
|
+
cursor.execute("SELECT id FROM todos WHERE id = ?", (todo_id,))
|
|
190
|
+
if not cursor.fetchone():
|
|
191
|
+
raise NotFoundError("Todo", todo_id)
|
|
192
|
+
|
|
193
|
+
updates = []
|
|
194
|
+
params = []
|
|
195
|
+
|
|
196
|
+
if content is not None:
|
|
197
|
+
updates.append("content = ?")
|
|
198
|
+
params.append(content.strip())
|
|
199
|
+
if due is not None:
|
|
200
|
+
updates.append("due_date = ?")
|
|
201
|
+
params.append(due)
|
|
202
|
+
if priority is not None:
|
|
203
|
+
if priority not in PRIORITIES:
|
|
204
|
+
raise ValidationError("priority", f"无效的优先级: {priority}")
|
|
205
|
+
updates.append("priority = ?")
|
|
206
|
+
params.append(priority)
|
|
207
|
+
if note is not None:
|
|
208
|
+
updates.append("note = ?")
|
|
209
|
+
params.append(note)
|
|
210
|
+
|
|
211
|
+
if updates:
|
|
212
|
+
updates.append("updated_at = ?")
|
|
213
|
+
params.append(datetime.now().isoformat())
|
|
214
|
+
params.append(todo_id)
|
|
215
|
+
|
|
216
|
+
query = f"UPDATE todos SET {', '.join(updates)} WHERE id = ?"
|
|
217
|
+
cursor.execute(query, params)
|
|
218
|
+
|
|
219
|
+
return {"id": todo_id, "updated": True}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@app.command()
|
|
223
|
+
@json_output
|
|
224
|
+
def delete(
|
|
225
|
+
todo_id: int = typer.Argument(..., help="待办 ID"),
|
|
226
|
+
force: bool = typer.Option(False, "--force", "-f", help="强制删除"),
|
|
227
|
+
):
|
|
228
|
+
"""删除待办"""
|
|
229
|
+
with get_connection() as conn:
|
|
230
|
+
cursor = conn.cursor()
|
|
231
|
+
cursor.execute("SELECT id, content FROM todos WHERE id = ?", (todo_id,))
|
|
232
|
+
row = cursor.fetchone()
|
|
233
|
+
|
|
234
|
+
if not row:
|
|
235
|
+
raise NotFoundError("Todo", todo_id)
|
|
236
|
+
|
|
237
|
+
if not force:
|
|
238
|
+
confirm = typer.confirm(f"确定要删除待办吗?\n{row['content']}")
|
|
239
|
+
if not confirm:
|
|
240
|
+
return {"cancelled": True, "message": "已取消删除"}
|
|
241
|
+
|
|
242
|
+
cursor.execute("DELETE FROM todos WHERE id = ?", (todo_id,))
|
|
243
|
+
|
|
244
|
+
return {"deleted": True, "id": todo_id}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@app.command()
|
|
248
|
+
@json_output
|
|
249
|
+
def search(
|
|
250
|
+
keyword: str = typer.Argument(..., help="搜索关键词"),
|
|
251
|
+
limit: int = typer.Option(20, "--limit", "-l", help="返回数量"),
|
|
252
|
+
):
|
|
253
|
+
"""搜索待办"""
|
|
254
|
+
with get_connection() as conn:
|
|
255
|
+
cursor = conn.cursor()
|
|
256
|
+
cursor.execute(
|
|
257
|
+
"""
|
|
258
|
+
SELECT * FROM todos
|
|
259
|
+
WHERE content LIKE ? OR note LIKE ?
|
|
260
|
+
ORDER BY created_at DESC
|
|
261
|
+
LIMIT ?
|
|
262
|
+
""",
|
|
263
|
+
(f"%{keyword}%", f"%{keyword}%", limit)
|
|
264
|
+
)
|
|
265
|
+
rows = cursor.fetchall()
|
|
266
|
+
|
|
267
|
+
todos = [dict(row) for row in rows]
|
|
268
|
+
return {"todos": todos, "total": len(todos), "keyword": keyword}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@app.command()
|
|
272
|
+
@json_output
|
|
273
|
+
def stats():
|
|
274
|
+
"""统计信息"""
|
|
275
|
+
with get_connection() as conn:
|
|
276
|
+
cursor = conn.cursor()
|
|
277
|
+
|
|
278
|
+
# 总数
|
|
279
|
+
cursor.execute("SELECT COUNT(*) FROM todos")
|
|
280
|
+
total = cursor.fetchone()[0]
|
|
281
|
+
|
|
282
|
+
# 已完成
|
|
283
|
+
cursor.execute("SELECT COUNT(*) FROM todos WHERE completed = 1")
|
|
284
|
+
completed = cursor.fetchone()[0]
|
|
285
|
+
|
|
286
|
+
# 未完成
|
|
287
|
+
pending = total - completed
|
|
288
|
+
|
|
289
|
+
# 按优先级统计
|
|
290
|
+
cursor.execute("SELECT priority, COUNT(*) FROM todos GROUP BY priority")
|
|
291
|
+
by_priority = dict(cursor.fetchall())
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
"total": total,
|
|
295
|
+
"completed": completed,
|
|
296
|
+
"pending": pending,
|
|
297
|
+
"by_priority": by_priority
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
if __name__ == "__main__":
|
|
302
|
+
app()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""命令模块"""
|
dida/config.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""配置管理模块
|
|
2
|
+
|
|
3
|
+
继承 dong.config.Config,管理 dida-cli 的用户配置。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dong.config import Config
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DidaConfig(Config):
|
|
10
|
+
"""待咚咚配置类"""
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def get_name(cls) -> str:
|
|
14
|
+
return "dida"
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def get_defaults(cls) -> dict:
|
|
18
|
+
return {
|
|
19
|
+
"default_status": "pending",
|
|
20
|
+
"default_priority": 0,
|
|
21
|
+
"default_limit": 20,
|
|
22
|
+
"statuses": ["pending", "in_progress", "completed", "cancelled"],
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# 便捷函数
|
|
27
|
+
def get_config() -> dict:
|
|
28
|
+
return DidaConfig.load()
|
|
29
|
+
|
|
30
|
+
def get_default_status() -> str:
|
|
31
|
+
return DidaConfig.get("default_status", "pending")
|
|
32
|
+
|
|
33
|
+
def get_default_priority() -> int:
|
|
34
|
+
return DidaConfig.get("default_priority", 0)
|
|
35
|
+
|
|
36
|
+
def get_default_limit() -> int:
|
|
37
|
+
return DidaConfig.get("default_limit", 20)
|
|
38
|
+
|
|
39
|
+
def get_statuses() -> list:
|
|
40
|
+
return DidaConfig.get("statuses", ["pending", "in_progress", "completed", "cancelled"])
|
dida/const.py
ADDED
dida/db/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""数据库层"""
|
|
2
|
+
|
|
3
|
+
from .connection import DidaDatabase, get_connection, close_connection, get_cursor, get_db_path
|
|
4
|
+
from .schema import DidaSchemaManager, SCHEMA_VERSION, get_schema_version, set_schema_version, is_initialized, init_database
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"DidaDatabase", "DidaSchemaManager",
|
|
8
|
+
"get_connection", "close_connection", "get_cursor", "get_db_path",
|
|
9
|
+
"SCHEMA_VERSION", "get_schema_version", "set_schema_version", "is_initialized", "init_database",
|
|
10
|
+
]
|
dida/db/connection.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""数据库连接管理模块
|
|
2
|
+
|
|
3
|
+
继承 dong.db.Database,提供 dida-cli 专用数据库访问。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sqlite3
|
|
7
|
+
from typing import Iterator
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
|
|
10
|
+
from dong.db import Database as DongDatabase
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DidaDatabase(DongDatabase):
|
|
14
|
+
"""待咚咚数据库类 - 继承自 dong.db.Database
|
|
15
|
+
|
|
16
|
+
数据库路径: ~/.dida/dida.db
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def get_name(cls) -> str:
|
|
21
|
+
return "dida"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# 兼容性函数
|
|
25
|
+
def get_connection(db_path=None):
|
|
26
|
+
return DidaDatabase.get_connection()
|
|
27
|
+
|
|
28
|
+
def close_connection():
|
|
29
|
+
DidaDatabase.close_connection()
|
|
30
|
+
|
|
31
|
+
@contextmanager
|
|
32
|
+
def get_cursor() -> Iterator[sqlite3.Cursor]:
|
|
33
|
+
with DidaDatabase.get_cursor() as cur:
|
|
34
|
+
yield cur
|
|
35
|
+
|
|
36
|
+
def get_db_path():
|
|
37
|
+
return DidaDatabase.get_db_path()
|
dida/db/schema.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""数据库 Schema 定义和版本管理
|
|
2
|
+
|
|
3
|
+
继承 dong.db.SchemaManager,管理 dida-cli 的数据库 schema。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dong.db import SchemaManager
|
|
7
|
+
from .connection import DidaDatabase
|
|
8
|
+
|
|
9
|
+
SCHEMA_VERSION = "1.0.0"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DidaSchemaManager(SchemaManager):
|
|
13
|
+
"""待咚咚 Schema 管理器"""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
super().__init__(
|
|
17
|
+
db_class=DidaDatabase,
|
|
18
|
+
current_version=SCHEMA_VERSION
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def init_schema(self) -> None:
|
|
22
|
+
self._create_todos_table()
|
|
23
|
+
self._create_indexes()
|
|
24
|
+
|
|
25
|
+
def _create_todos_table(self) -> None:
|
|
26
|
+
with DidaDatabase.get_cursor() as cur:
|
|
27
|
+
cur.execute("""
|
|
28
|
+
CREATE TABLE IF NOT EXISTS todos (
|
|
29
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
30
|
+
title TEXT NOT NULL,
|
|
31
|
+
description TEXT,
|
|
32
|
+
status TEXT DEFAULT 'pending',
|
|
33
|
+
priority INTEGER DEFAULT 0,
|
|
34
|
+
due_date TEXT,
|
|
35
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
36
|
+
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
37
|
+
)
|
|
38
|
+
""")
|
|
39
|
+
|
|
40
|
+
def _create_indexes(self) -> None:
|
|
41
|
+
with DidaDatabase.get_cursor() as cur:
|
|
42
|
+
cur.execute("CREATE INDEX IF NOT EXISTS idx_todos_status ON todos(status)")
|
|
43
|
+
cur.execute("CREATE INDEX IF NOT EXISTS idx_todos_due_date ON todos(due_date)")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# 兼容性函数
|
|
47
|
+
def get_schema_version() -> str | None:
|
|
48
|
+
return DidaSchemaManager().get_stored_version()
|
|
49
|
+
|
|
50
|
+
def set_schema_version(version: str) -> None:
|
|
51
|
+
DidaDatabase.set_meta(DidaSchemaManager.VERSION_KEY, version)
|
|
52
|
+
|
|
53
|
+
def is_initialized() -> bool:
|
|
54
|
+
return DidaSchemaManager().is_initialized()
|
|
55
|
+
|
|
56
|
+
def init_database() -> None:
|
|
57
|
+
schema = DidaSchemaManager()
|
|
58
|
+
if not schema.is_initialized():
|
|
59
|
+
schema.initialize()
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dida-cli
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: 事咚咚 - 个人待办管理的 CLI 工具
|
|
5
|
+
Author: gudong
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: cli,task-management,todo
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: dong-core>=0.3.0
|
|
10
|
+
Requires-Dist: rich>=13.0.0
|
|
11
|
+
Requires-Dist: typer>=0.12.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# 事咚咚 (Dida)
|
|
18
|
+
|
|
19
|
+
> 管理个人待办事项的 CLI 工具 —— 为事咚咚智能体提供底层能力
|
|
20
|
+
|
|
21
|
+
## 产品定位
|
|
22
|
+
|
|
23
|
+
> **在你最自然的地方,用最自然的方式,管理你要做的事**
|
|
24
|
+
|
|
25
|
+
### 我们解决什么问题
|
|
26
|
+
|
|
27
|
+
| 痛点 | 描述 |
|
|
28
|
+
|------|------|
|
|
29
|
+
| 记不住 | 说过的话、答应的事转头就忘 |
|
|
30
|
+
| 懒得记 | 专门的 todo app 太重,打开步骤多 |
|
|
31
|
+
| 易遗漏 | 任务到期了才想起,或者根本没想起 |
|
|
32
|
+
| 难追踪 | 想知道"今天要做什么"要到处翻 |
|
|
33
|
+
| 不连贯 | 聊天中说要做的事,得手动复制到 todo app |
|
|
34
|
+
|
|
35
|
+
### 核心价值
|
|
36
|
+
|
|
37
|
+
- **零摩擦**:记录一个任务 ≤ 5 秒
|
|
38
|
+
- **不遗漏**:到期主动提醒
|
|
39
|
+
- **心有数**:随时随地问,立即知道要做什么
|
|
40
|
+
- **可信赖**:它记住的,就是你要做的
|
|
41
|
+
|
|
42
|
+
### 我们不做什么
|
|
43
|
+
|
|
44
|
+
| 不做 | 原因 |
|
|
45
|
+
|------|------|
|
|
46
|
+
| 复杂项目管理 | 超出个人日常范畴,交给专业工具 |
|
|
47
|
+
| 多人协作 | 专注个人事务 |
|
|
48
|
+
| 复杂依赖关系 | 保持简单,"今天要做什么"就够了 |
|
|
49
|
+
| 精密时间块规划 | 番茄钟、日历视图交给专业 app |
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 安装
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install dida-cli
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 快速开始
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# 初始化
|
|
63
|
+
dida init
|
|
64
|
+
|
|
65
|
+
# 记录待办
|
|
66
|
+
dida add "给妈妈打电话"
|
|
67
|
+
dida add "明天下午三点开会" --due "2026-03-16 15:00"
|
|
68
|
+
dida add "周五前把报告写完" --due "2026-03-14" --priority high
|
|
69
|
+
|
|
70
|
+
# 列出待办
|
|
71
|
+
dida ls
|
|
72
|
+
|
|
73
|
+
# 标记完成
|
|
74
|
+
dida done 1
|
|
75
|
+
|
|
76
|
+
# 删除
|
|
77
|
+
dida delete 1
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## 命令
|
|
81
|
+
|
|
82
|
+
| 命令 | 说明 |
|
|
83
|
+
|------|------|
|
|
84
|
+
| `dida init` | 初始化数据库 |
|
|
85
|
+
| `dida add` | 创建待办 |
|
|
86
|
+
| `dida ls` | 列出待办 |
|
|
87
|
+
| `dida get` | 获取详情 |
|
|
88
|
+
| `dida done` | 标记完成 |
|
|
89
|
+
| `dida undo` | 取消完成 |
|
|
90
|
+
| `dida update` | 更新待办 |
|
|
91
|
+
| `dida delete` | 删除待办 |
|
|
92
|
+
| `dida search` | 搜索待办 |
|
|
93
|
+
| `dida stats` | 统计信息 |
|
|
94
|
+
|
|
95
|
+
## 数据存储
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
~/.dida/dida.db
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## 开发
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# 克隆
|
|
105
|
+
git clone https://github.com/gudong/dida-cli.git
|
|
106
|
+
cd dida-cli
|
|
107
|
+
|
|
108
|
+
# 安装依赖
|
|
109
|
+
python -m venv venv
|
|
110
|
+
source venv/bin/activate
|
|
111
|
+
pip install -e ".[dev]"
|
|
112
|
+
|
|
113
|
+
# 运行测试
|
|
114
|
+
pytest
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
dida/__init__.py,sha256=IJV0Ukhg2nunGc_2ToFOZUSxxM9lrgvygDaFEmQN6Rw,64
|
|
2
|
+
dida/cli.py,sha256=GAx2Qb4hYJ7SVTFU-Mkcv3js1WEebTuPxfzx9wXo1cQ,8597
|
|
3
|
+
dida/config.py,sha256=C9aRJBfaZoRO1KsZZkOAWoR0ps3AgiRdJ91_iVxMFM8,963
|
|
4
|
+
dida/const.py,sha256=22pPyLyG1QgnP8vA4uAyoUNh63ukahEp85W73OcpNPI,220
|
|
5
|
+
dida/commands/__init__.py,sha256=26xy0UOcWLZEjqdzebAmJ9V8_PnUTe8ipqe-RqT_Kf0,19
|
|
6
|
+
dida/db/__init__.py,sha256=GpgmZD_aQueeecbN9YmnyRMPIC7frNtvJMJGAyDouO8,469
|
|
7
|
+
dida/db/connection.py,sha256=Zkw-YAgCFVTIiaCRLFUFK_qnXD_SFqk8HKBxdeySwmo,779
|
|
8
|
+
dida/db/schema.py,sha256=oguNjBiMxY0x-yCyy0db2KIPzvomYzP19mhYDh6FSpM,1834
|
|
9
|
+
dida_cli-0.2.0.dist-info/METADATA,sha256=BIje8gRssquhEh__ZXuhEnMlpp_7Z8pA9cQdPjwVru0,2537
|
|
10
|
+
dida_cli-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
11
|
+
dida_cli-0.2.0.dist-info/entry_points.txt,sha256=MQu3O92QcrQ5nbk6War7EdhFG9t51kSbttpWXX4UOJo,38
|
|
12
|
+
dida_cli-0.2.0.dist-info/licenses/LICENSE,sha256=yx0ZLJJzA6p8qblMHjC8z10DetflV_asazG5AEu_zZE,1063
|
|
13
|
+
dida_cli-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 gudong
|
|
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
|
|
20
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
21
|
+
IN THE SOFTWARE.
|