doingapp 0.6.0__tar.gz → 0.7.1__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.
- {doingapp-0.6.0 → doingapp-0.7.1}/PKG-INFO +17 -3
- {doingapp-0.6.0 → doingapp-0.7.1}/README.md +16 -2
- {doingapp-0.6.0 → doingapp-0.7.1}/doing_cli/api.py +60 -18
- {doingapp-0.6.0 → doingapp-0.7.1}/doing_cli/main.py +97 -11
- {doingapp-0.6.0 → doingapp-0.7.1}/doingapp.egg-info/PKG-INFO +17 -3
- {doingapp-0.6.0 → doingapp-0.7.1}/pyproject.toml +1 -1
- {doingapp-0.6.0 → doingapp-0.7.1}/LICENSE +0 -0
- {doingapp-0.6.0 → doingapp-0.7.1}/doing_cli/__init__.py +0 -0
- {doingapp-0.6.0 → doingapp-0.7.1}/doing_cli/auth.py +0 -0
- {doingapp-0.6.0 → doingapp-0.7.1}/doingapp.egg-info/SOURCES.txt +0 -0
- {doingapp-0.6.0 → doingapp-0.7.1}/doingapp.egg-info/dependency_links.txt +0 -0
- {doingapp-0.6.0 → doingapp-0.7.1}/doingapp.egg-info/entry_points.txt +0 -0
- {doingapp-0.6.0 → doingapp-0.7.1}/doingapp.egg-info/requires.txt +0 -0
- {doingapp-0.6.0 → doingapp-0.7.1}/doingapp.egg-info/top_level.txt +0 -0
- {doingapp-0.6.0 → doingapp-0.7.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: doingapp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: A fast CLI task tracker — track what you're doing from the terminal.
|
|
5
5
|
Author-email: Parker Grimes <parker@altellus.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -73,13 +73,14 @@ doing apikey --key doing_xxxxx
|
|
|
73
73
|
| Command | Description |
|
|
74
74
|
|---------|-------------|
|
|
75
75
|
| `doing` | Show active tasks (same as `doing ls`) |
|
|
76
|
-
| `doing add <title>` | Add a task (`-n` notes, `-c` context ID) |
|
|
76
|
+
| `doing add <title>` | Add a task (`-n` notes, `-c` context ID, `-d "tomorrow 3pm"` due, `-r daily` repeat) |
|
|
77
77
|
| `doing get <id>` | Get a single task by ID |
|
|
78
78
|
| `doing done <id>` | Mark a task as done |
|
|
79
79
|
| `doing pause <id>` | Pause a task |
|
|
80
80
|
| `doing resume <id>` | Resume a paused task |
|
|
81
81
|
| `doing reopen <id>` | Reopen a completed task |
|
|
82
|
-
| `doing edit <id>` | Edit a task (`-t` title, `-n` notes, `-c` context, `--no-context`) |
|
|
82
|
+
| `doing edit <id>` | Edit a task (`-t` title, `-n` notes, `-c` context, `--no-context`, `-d` due, `--no-due`, `-r` repeat, `--no-repeat`) |
|
|
83
|
+
| `doing snooze <id> [10m\|1h\|3h\|1d\|morning]` | Push a due task back (default 10m) |
|
|
83
84
|
| `doing rm <id>` | Delete a task |
|
|
84
85
|
| `doing ls` | List active tasks (`-c` context ID) |
|
|
85
86
|
| `doing log` | Show tasks done today (`-d YYYY-MM-DD`, `-c` context ID) |
|
|
@@ -93,6 +94,19 @@ doing apikey --key doing_xxxxx
|
|
|
93
94
|
| `doing logout` | Log out |
|
|
94
95
|
| `doing apikey` | Save an API key (`-k` key) |
|
|
95
96
|
|
|
97
|
+
## Due dates and reminders
|
|
98
|
+
|
|
99
|
+
Give a task a due date and the Doing iOS app nags you every few minutes once it's overdue, until you mark it done or snooze it.
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
doing add "Call mom" -d "tomorrow 3pm"
|
|
103
|
+
doing add "Water plants" -d friday -r 3d # repeats 3 days after each completion
|
|
104
|
+
doing add "Standup notes" -d "9am" -r weekdays
|
|
105
|
+
doing snooze 12 1h
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`-d` accepts natural language: `in 20m`, `tonight`, `fri 9am`, `oct 30`. Dates without a time use your morning time (9:00 by default). `-r` accepts `daily`, `weekdays`, days like `mon,thu`, or `Nd` (N days after done).
|
|
109
|
+
|
|
96
110
|
## JSON output
|
|
97
111
|
|
|
98
112
|
Every command supports `--json` (or `-j`) for machine-readable output. This is useful for scripting and AI coding agents like Claude Code or Codex CLI.
|
|
@@ -48,13 +48,14 @@ doing apikey --key doing_xxxxx
|
|
|
48
48
|
| Command | Description |
|
|
49
49
|
|---------|-------------|
|
|
50
50
|
| `doing` | Show active tasks (same as `doing ls`) |
|
|
51
|
-
| `doing add <title>` | Add a task (`-n` notes, `-c` context ID) |
|
|
51
|
+
| `doing add <title>` | Add a task (`-n` notes, `-c` context ID, `-d "tomorrow 3pm"` due, `-r daily` repeat) |
|
|
52
52
|
| `doing get <id>` | Get a single task by ID |
|
|
53
53
|
| `doing done <id>` | Mark a task as done |
|
|
54
54
|
| `doing pause <id>` | Pause a task |
|
|
55
55
|
| `doing resume <id>` | Resume a paused task |
|
|
56
56
|
| `doing reopen <id>` | Reopen a completed task |
|
|
57
|
-
| `doing edit <id>` | Edit a task (`-t` title, `-n` notes, `-c` context, `--no-context`) |
|
|
57
|
+
| `doing edit <id>` | Edit a task (`-t` title, `-n` notes, `-c` context, `--no-context`, `-d` due, `--no-due`, `-r` repeat, `--no-repeat`) |
|
|
58
|
+
| `doing snooze <id> [10m\|1h\|3h\|1d\|morning]` | Push a due task back (default 10m) |
|
|
58
59
|
| `doing rm <id>` | Delete a task |
|
|
59
60
|
| `doing ls` | List active tasks (`-c` context ID) |
|
|
60
61
|
| `doing log` | Show tasks done today (`-d YYYY-MM-DD`, `-c` context ID) |
|
|
@@ -68,6 +69,19 @@ doing apikey --key doing_xxxxx
|
|
|
68
69
|
| `doing logout` | Log out |
|
|
69
70
|
| `doing apikey` | Save an API key (`-k` key) |
|
|
70
71
|
|
|
72
|
+
## Due dates and reminders
|
|
73
|
+
|
|
74
|
+
Give a task a due date and the Doing iOS app nags you every few minutes once it's overdue, until you mark it done or snooze it.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
doing add "Call mom" -d "tomorrow 3pm"
|
|
78
|
+
doing add "Water plants" -d friday -r 3d # repeats 3 days after each completion
|
|
79
|
+
doing add "Standup notes" -d "9am" -r weekdays
|
|
80
|
+
doing snooze 12 1h
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`-d` accepts natural language: `in 20m`, `tonight`, `fri 9am`, `oct 30`. Dates without a time use your morning time (9:00 by default). `-r` accepts `daily`, `weekdays`, days like `mon,thu`, or `Nd` (N days after done).
|
|
84
|
+
|
|
71
85
|
## JSON output
|
|
72
86
|
|
|
73
87
|
Every command supports `--json` (or `-j`) for machine-readable output. This is useful for scripting and AI coding agents like Claude Code or Codex CLI.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
-
import
|
|
2
|
+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|
3
3
|
|
|
4
4
|
import httpx
|
|
5
5
|
|
|
@@ -8,16 +8,35 @@ from doing_cli.auth import load_token
|
|
|
8
8
|
BASE_URL = os.environ.get("DOING_API_URL", "https://doingapp.co")
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def
|
|
11
|
+
def _is_iana_tz(name: str) -> bool:
|
|
12
12
|
try:
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
ZoneInfo(name)
|
|
14
|
+
except (ZoneInfoNotFoundError, ValueError):
|
|
15
|
+
return False
|
|
16
|
+
return True
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _local_tz() -> str | None:
|
|
20
|
+
"""IANA name of the local timezone, or None if it can't be determined.
|
|
21
|
+
|
|
22
|
+
TZ wins over /etc/localtime, as it does for the OS itself; containers
|
|
23
|
+
commonly set TZ while /etc/localtime still points at UTC. Abbreviations
|
|
24
|
+
like "MST" are never sent: the server would treat them as fixed offsets
|
|
25
|
+
with no DST. When this returns None the server uses the account timezone.
|
|
26
|
+
"""
|
|
27
|
+
candidates = []
|
|
28
|
+
if tz := os.environ.get("TZ", "").lstrip(":"):
|
|
29
|
+
candidates.append(tz)
|
|
30
|
+
try:
|
|
31
|
+
candidates.append(os.path.realpath("/etc/localtime").split("zoneinfo/")[1])
|
|
15
32
|
except (IndexError, OSError):
|
|
16
33
|
pass
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
34
|
+
return next((name for name in candidates if _is_iana_tz(name)), None)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _tz_params() -> dict:
|
|
38
|
+
tz = _local_tz()
|
|
39
|
+
return {"tz": tz} if tz else {}
|
|
21
40
|
|
|
22
41
|
|
|
23
42
|
def _client() -> httpx.Client:
|
|
@@ -49,14 +68,19 @@ def get_me() -> dict:
|
|
|
49
68
|
return r.json()
|
|
50
69
|
|
|
51
70
|
|
|
52
|
-
def create_task(title: str, notes: str | None = None, context_id: int | None = None
|
|
71
|
+
def create_task(title: str, notes: str | None = None, context_id: int | None = None,
|
|
72
|
+
due: str | None = None, recurrence: str | None = None) -> dict:
|
|
53
73
|
payload: dict = {"title": title}
|
|
54
74
|
if notes is not None:
|
|
55
75
|
payload["notes"] = notes
|
|
56
76
|
if context_id is not None:
|
|
57
77
|
payload["context_id"] = context_id
|
|
78
|
+
if due is not None:
|
|
79
|
+
payload["due"] = due
|
|
80
|
+
if recurrence is not None:
|
|
81
|
+
payload["recurrence"] = recurrence
|
|
58
82
|
with _client() as c:
|
|
59
|
-
r = c.post("/tasks", json=payload)
|
|
83
|
+
r = c.post("/tasks", json=payload, params=_tz_params())
|
|
60
84
|
r.raise_for_status()
|
|
61
85
|
return r.json()
|
|
62
86
|
|
|
@@ -70,7 +94,7 @@ def get_task(task_id: int) -> dict:
|
|
|
70
94
|
|
|
71
95
|
def list_tasks(status: str | None = None, date: str | None = None,
|
|
72
96
|
context_id: int | None = None) -> list[dict]:
|
|
73
|
-
params: dict =
|
|
97
|
+
params: dict = _tz_params()
|
|
74
98
|
if status is not None:
|
|
75
99
|
params["status"] = status
|
|
76
100
|
if date is not None:
|
|
@@ -85,7 +109,7 @@ def list_tasks(status: str | None = None, date: str | None = None,
|
|
|
85
109
|
|
|
86
110
|
def mark_done(task_id: int) -> dict:
|
|
87
111
|
with _client() as c:
|
|
88
|
-
r = c.post(f"/tasks/{task_id}/done")
|
|
112
|
+
r = c.post(f"/tasks/{task_id}/done", params=_tz_params())
|
|
89
113
|
r.raise_for_status()
|
|
90
114
|
return r.json()
|
|
91
115
|
|
|
@@ -112,7 +136,9 @@ def resume_task(task_id: int) -> dict:
|
|
|
112
136
|
|
|
113
137
|
|
|
114
138
|
def update_task(task_id: int, title: str | None = None, notes: str | None = None,
|
|
115
|
-
context_id: int | None = None, clear_context: bool = False
|
|
139
|
+
context_id: int | None = None, clear_context: bool = False,
|
|
140
|
+
due: str | None = None, recurrence: str | None = None,
|
|
141
|
+
clear_due: bool = False, clear_recurrence: bool = False) -> dict:
|
|
116
142
|
payload: dict = {}
|
|
117
143
|
if title is not None:
|
|
118
144
|
payload["title"] = title
|
|
@@ -122,8 +148,24 @@ def update_task(task_id: int, title: str | None = None, notes: str | None = None
|
|
|
122
148
|
payload["clear_context"] = True
|
|
123
149
|
elif context_id is not None:
|
|
124
150
|
payload["context_id"] = context_id
|
|
151
|
+
if clear_due:
|
|
152
|
+
payload["clear_due"] = True
|
|
153
|
+
elif due is not None:
|
|
154
|
+
payload["due"] = due
|
|
155
|
+
if clear_recurrence:
|
|
156
|
+
payload["clear_recurrence"] = True
|
|
157
|
+
elif recurrence is not None:
|
|
158
|
+
payload["recurrence"] = recurrence
|
|
159
|
+
with _client() as c:
|
|
160
|
+
r = c.patch(f"/tasks/{task_id}", json=payload, params=_tz_params())
|
|
161
|
+
r.raise_for_status()
|
|
162
|
+
return r.json()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def snooze_task(task_id: int, minutes: int | None = None, preset: str | None = None) -> dict:
|
|
166
|
+
payload: dict = {"minutes": minutes} if minutes is not None else {"preset": preset}
|
|
125
167
|
with _client() as c:
|
|
126
|
-
r = c.
|
|
168
|
+
r = c.post(f"/tasks/{task_id}/snooze", json=payload, params=_tz_params())
|
|
127
169
|
r.raise_for_status()
|
|
128
170
|
return r.json()
|
|
129
171
|
|
|
@@ -135,7 +177,7 @@ def delete_task(task_id: int) -> None:
|
|
|
135
177
|
|
|
136
178
|
|
|
137
179
|
def get_stats(context_id: int | None = None) -> dict:
|
|
138
|
-
params: dict =
|
|
180
|
+
params: dict = _tz_params()
|
|
139
181
|
if context_id is not None:
|
|
140
182
|
params["context_id"] = context_id
|
|
141
183
|
with _client() as c:
|
|
@@ -146,7 +188,7 @@ def get_stats(context_id: int | None = None) -> dict:
|
|
|
146
188
|
|
|
147
189
|
def get_done_history(limit: int = 7, before: str | None = None,
|
|
148
190
|
context_id: int | None = None) -> dict:
|
|
149
|
-
params: dict = {
|
|
191
|
+
params: dict = {**_tz_params(), "limit": limit}
|
|
150
192
|
if before is not None:
|
|
151
193
|
params["before"] = before
|
|
152
194
|
if context_id is not None:
|
|
@@ -158,7 +200,7 @@ def get_done_history(limit: int = 7, before: str | None = None,
|
|
|
158
200
|
|
|
159
201
|
|
|
160
202
|
def get_today(context_id: int | None = None) -> dict:
|
|
161
|
-
params: dict =
|
|
203
|
+
params: dict = _tz_params()
|
|
162
204
|
if context_id is not None:
|
|
163
205
|
params["context_id"] = context_id
|
|
164
206
|
with _client() as c:
|
|
@@ -202,7 +244,7 @@ def update_context(context_id: int, name: str, icon: str | None = None) -> dict:
|
|
|
202
244
|
|
|
203
245
|
|
|
204
246
|
def snooze_context(context_id: int) -> dict:
|
|
205
|
-
params: dict =
|
|
247
|
+
params: dict = _tz_params()
|
|
206
248
|
with _client() as c:
|
|
207
249
|
r = c.post(f"/contexts/{context_id}/snooze", params=params)
|
|
208
250
|
r.raise_for_status()
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import re
|
|
2
3
|
import sys
|
|
4
|
+
from datetime import datetime, timezone
|
|
3
5
|
|
|
4
6
|
import httpx
|
|
5
7
|
import typer
|
|
@@ -99,6 +101,51 @@ def _validate_name(name: str) -> str:
|
|
|
99
101
|
return cleaned
|
|
100
102
|
|
|
101
103
|
|
|
104
|
+
WEEKDAYS = ("mon", "tue", "wed", "thu", "fri", "sat", "sun")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _normalize_repeat(value: str) -> str:
|
|
108
|
+
"""Accept friendly repeat specs: daily, weekdays, mon,thu, 3d, or the raw API form."""
|
|
109
|
+
v = value.strip().lower().replace(" ", "")
|
|
110
|
+
if v in ("daily", "weekdays") or v.startswith(("weekly:", "after:")):
|
|
111
|
+
return v
|
|
112
|
+
if all(day in WEEKDAYS for day in v.split(",")):
|
|
113
|
+
return f"weekly:{v}"
|
|
114
|
+
if m := re.fullmatch(r"(\d+)d(ays?)?", v):
|
|
115
|
+
return f"after:{m.group(1)}"
|
|
116
|
+
_validation_error(f"Invalid --repeat '{value}'. Use daily, weekdays, mon,thu, or 3d (days after done).")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _describe_repeat(rule: str) -> str:
|
|
120
|
+
if rule.startswith("weekly:"):
|
|
121
|
+
return "every " + ", ".join(d.title() for d in rule.split(":", 1)[1].split(","))
|
|
122
|
+
if rule.startswith("after:"):
|
|
123
|
+
n = rule.split(":", 1)[1]
|
|
124
|
+
return f"{n} day{'s' if n != '1' else ''} after done"
|
|
125
|
+
return rule
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _format_due(task: dict) -> str:
|
|
129
|
+
"""Rich-formatted due label, e.g. '[red]overdue · Mon 3:00 PM[/red]'."""
|
|
130
|
+
if not task.get("due_at"):
|
|
131
|
+
return ""
|
|
132
|
+
due = datetime.fromisoformat(task["due_at"])
|
|
133
|
+
if due.tzinfo is None:
|
|
134
|
+
due = due.replace(tzinfo=timezone.utc)
|
|
135
|
+
due = due.astimezone()
|
|
136
|
+
now = datetime.now().astimezone()
|
|
137
|
+
if task.get("due_all_day"):
|
|
138
|
+
when = due.strftime("%a %b %-d")
|
|
139
|
+
elif due.date() == now.date():
|
|
140
|
+
when = due.strftime("%-I:%M %p")
|
|
141
|
+
else:
|
|
142
|
+
when = due.strftime("%a %b %-d %-I:%M %p")
|
|
143
|
+
repeat = f" ↻ {_describe_repeat(task['recurrence'])}" if task.get("recurrence") else ""
|
|
144
|
+
if task.get("status") == "active" and due <= now:
|
|
145
|
+
return f" [bold red]⏰ overdue · {when}{repeat}[/bold red]"
|
|
146
|
+
return f" [cyan]⏰ {when}{repeat}[/cyan]"
|
|
147
|
+
|
|
148
|
+
|
|
102
149
|
def _print_notes(task: dict) -> None:
|
|
103
150
|
if task.get("notes"):
|
|
104
151
|
console.print(f" [dim]{task['notes']}[/dim]")
|
|
@@ -106,13 +153,13 @@ def _print_notes(task: dict) -> None:
|
|
|
106
153
|
|
|
107
154
|
def _print_active_task(task: dict) -> None:
|
|
108
155
|
ctx = f" [dim][{task['context_name']}][/dim]" if task.get("context_name") else ""
|
|
109
|
-
console.print(f" [green bold]●[/green bold] [dim]#{task['id']}[/dim] {task['title']}{ctx}")
|
|
156
|
+
console.print(f" [green bold]●[/green bold] [dim]#{task['id']}[/dim] {task['title']}{ctx}{_format_due(task)}")
|
|
110
157
|
_print_notes(task)
|
|
111
158
|
|
|
112
159
|
|
|
113
160
|
def _print_paused_task(task: dict) -> None:
|
|
114
161
|
ctx = f" [dim][{task['context_name']}][/dim]" if task.get("context_name") else ""
|
|
115
|
-
console.print(f" [yellow]⏸[/yellow] [dim]#{task['id']}[/dim] {task['title']}{ctx}")
|
|
162
|
+
console.print(f" [yellow]⏸[/yellow] [dim]#{task['id']}[/dim] {task['title']}{ctx}{_format_due(task)}")
|
|
116
163
|
_print_notes(task)
|
|
117
164
|
|
|
118
165
|
|
|
@@ -220,16 +267,19 @@ def add(
|
|
|
220
267
|
title: str = typer.Argument(..., help="What are you working on?"),
|
|
221
268
|
notes: str = typer.Option(None, "--notes", "-n", help="Optional notes"),
|
|
222
269
|
context: int = typer.Option(None, "--context", "-c", help="Context ID"),
|
|
270
|
+
due: str = typer.Option(None, "--due", "-d", help="When it's due, e.g. 'tomorrow 3pm', 'in 20m', 'friday'"),
|
|
271
|
+
repeat: str = typer.Option(None, "--repeat", "-r", help="daily, weekdays, mon,thu, or 3d (3 days after done)"),
|
|
223
272
|
):
|
|
224
273
|
"""Add something to your plate."""
|
|
225
274
|
title = _validate_title(title)
|
|
275
|
+
recurrence = _normalize_repeat(repeat) if repeat else None
|
|
226
276
|
try:
|
|
227
|
-
task = api.create_task(title, notes, context)
|
|
277
|
+
task = api.create_task(title, notes, context, due=due, recurrence=recurrence)
|
|
228
278
|
except Exception as exc:
|
|
229
279
|
_handle_api_error(exc)
|
|
230
280
|
if _json_mode:
|
|
231
281
|
_output_json(task)
|
|
232
|
-
console.print(f"[green bold]●[/green bold] Added: [bold]{task['title']}[/bold] [dim](#{task['id']})[/dim]")
|
|
282
|
+
console.print(f"[green bold]●[/green bold] Added: [bold]{task['title']}[/bold] [dim](#{task['id']})[/dim]{_format_due(task)}")
|
|
233
283
|
|
|
234
284
|
|
|
235
285
|
@app.command()
|
|
@@ -246,7 +296,7 @@ def get(
|
|
|
246
296
|
status_icon = {"active": "[green bold]●[/green bold]", "paused": "[yellow]⏸[/yellow]", "done": "[green]✓[/green]"}
|
|
247
297
|
icon = status_icon.get(task["status"], "?")
|
|
248
298
|
ctx = f" [dim][{task['context_name']}][/dim]" if task.get("context_name") else ""
|
|
249
|
-
console.print(f" {icon} [dim]#{task['id']}[/dim] {task['title']}{ctx}")
|
|
299
|
+
console.print(f" {icon} [dim]#{task['id']}[/dim] {task['title']}{ctx}{_format_due(task)}")
|
|
250
300
|
if task.get("notes"):
|
|
251
301
|
console.print(f" [dim]{task['notes']}[/dim]")
|
|
252
302
|
console.print(f" [dim]Status: {task['status']} Created: {task['created_at']}[/dim]")
|
|
@@ -311,7 +361,8 @@ def done(
|
|
|
311
361
|
continue
|
|
312
362
|
results.append(task)
|
|
313
363
|
if not _json_mode:
|
|
314
|
-
|
|
364
|
+
next_due = f" [dim]next:[/dim]{_format_due(task)}" if task.get("recurrence") else ""
|
|
365
|
+
console.print(f"[green]✓[/green] Done: [bold]{task['title']}[/bold]{next_due}")
|
|
315
366
|
if _json_mode:
|
|
316
367
|
_output_json(results[0] if len(task_ids) == 1 and results else results)
|
|
317
368
|
if errors:
|
|
@@ -367,25 +418,60 @@ def edit(
|
|
|
367
418
|
notes: str = typer.Option(None, "--notes", "-n", help="New notes"),
|
|
368
419
|
context: int = typer.Option(None, "--context", "-c", help="Move to context ID"),
|
|
369
420
|
no_context: bool = typer.Option(False, "--no-context", help="Remove context"),
|
|
421
|
+
due: str = typer.Option(None, "--due", "-d", help="When it's due, e.g. 'tomorrow 3pm'"),
|
|
422
|
+
no_due: bool = typer.Option(False, "--no-due", help="Remove due date (and repeat)"),
|
|
423
|
+
repeat: str = typer.Option(None, "--repeat", "-r", help="daily, weekdays, mon,thu, or 3d"),
|
|
424
|
+
no_repeat: bool = typer.Option(False, "--no-repeat", help="Stop repeating"),
|
|
370
425
|
):
|
|
371
|
-
"""Edit a task's title, notes, or
|
|
426
|
+
"""Edit a task's title, notes, context, due date, or repeat."""
|
|
372
427
|
if no_context and context is not None:
|
|
373
428
|
_validation_error("--no-context and --context cannot be used together")
|
|
429
|
+
if no_due and due is not None:
|
|
430
|
+
_validation_error("--no-due and --due cannot be used together")
|
|
431
|
+
if no_repeat and repeat is not None:
|
|
432
|
+
_validation_error("--no-repeat and --repeat cannot be used together")
|
|
374
433
|
|
|
375
|
-
if title is None and notes is None and context is None and not no_context
|
|
376
|
-
|
|
434
|
+
if (title is None and notes is None and context is None and not no_context
|
|
435
|
+
and due is None and not no_due and repeat is None and not no_repeat):
|
|
436
|
+
_validation_error("No changes specified. Use --title, --notes, --context, --due, or --repeat.")
|
|
377
437
|
|
|
378
438
|
if title is not None:
|
|
379
439
|
title = _validate_title(title)
|
|
440
|
+
recurrence = _normalize_repeat(repeat) if repeat else None
|
|
380
441
|
|
|
381
442
|
try:
|
|
382
443
|
task = api.update_task(task_id, title=title, notes=notes,
|
|
383
|
-
context_id=context, clear_context=no_context
|
|
444
|
+
context_id=context, clear_context=no_context,
|
|
445
|
+
due=due, clear_due=no_due,
|
|
446
|
+
recurrence=recurrence, clear_recurrence=no_repeat)
|
|
447
|
+
except Exception as exc:
|
|
448
|
+
_handle_api_error(exc)
|
|
449
|
+
if _json_mode:
|
|
450
|
+
_output_json(task)
|
|
451
|
+
console.print(f"[green bold]Updated:[/green bold] {task['title']} [dim](#{task['id']})[/dim]{_format_due(task)}")
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
@app.command()
|
|
455
|
+
def snooze(
|
|
456
|
+
task_id: int = typer.Argument(..., help="ID of the task to snooze"),
|
|
457
|
+
duration: str = typer.Argument("10m", help="10m, 1h, 3h, 1d, or 'morning' (tomorrow morning)"),
|
|
458
|
+
):
|
|
459
|
+
"""Push a task's due time back (stops the nagging until then)."""
|
|
460
|
+
d = duration.strip().lower()
|
|
461
|
+
minutes = preset = None
|
|
462
|
+
if d in ("morning", "tomorrow"):
|
|
463
|
+
preset = "tomorrow_morning"
|
|
464
|
+
elif m := re.fullmatch(r"(\d+)\s*(m|min|h|hr|d)", d):
|
|
465
|
+
minutes = int(m.group(1)) * {"m": 1, "min": 1, "h": 60, "hr": 60, "d": 1440}[m.group(2)]
|
|
466
|
+
else:
|
|
467
|
+
_validation_error(f"Invalid duration '{duration}'. Use 10m, 1h, 3h, 1d, or morning.")
|
|
468
|
+
try:
|
|
469
|
+
task = api.snooze_task(task_id, minutes=minutes, preset=preset)
|
|
384
470
|
except Exception as exc:
|
|
385
471
|
_handle_api_error(exc)
|
|
386
472
|
if _json_mode:
|
|
387
473
|
_output_json(task)
|
|
388
|
-
console.print(f"[
|
|
474
|
+
console.print(f"[cyan]💤 Snoozed:[/cyan] [bold]{task['title']}[/bold]{_format_due(task)}")
|
|
389
475
|
|
|
390
476
|
|
|
391
477
|
@app.command()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: doingapp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: A fast CLI task tracker — track what you're doing from the terminal.
|
|
5
5
|
Author-email: Parker Grimes <parker@altellus.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -73,13 +73,14 @@ doing apikey --key doing_xxxxx
|
|
|
73
73
|
| Command | Description |
|
|
74
74
|
|---------|-------------|
|
|
75
75
|
| `doing` | Show active tasks (same as `doing ls`) |
|
|
76
|
-
| `doing add <title>` | Add a task (`-n` notes, `-c` context ID) |
|
|
76
|
+
| `doing add <title>` | Add a task (`-n` notes, `-c` context ID, `-d "tomorrow 3pm"` due, `-r daily` repeat) |
|
|
77
77
|
| `doing get <id>` | Get a single task by ID |
|
|
78
78
|
| `doing done <id>` | Mark a task as done |
|
|
79
79
|
| `doing pause <id>` | Pause a task |
|
|
80
80
|
| `doing resume <id>` | Resume a paused task |
|
|
81
81
|
| `doing reopen <id>` | Reopen a completed task |
|
|
82
|
-
| `doing edit <id>` | Edit a task (`-t` title, `-n` notes, `-c` context, `--no-context`) |
|
|
82
|
+
| `doing edit <id>` | Edit a task (`-t` title, `-n` notes, `-c` context, `--no-context`, `-d` due, `--no-due`, `-r` repeat, `--no-repeat`) |
|
|
83
|
+
| `doing snooze <id> [10m\|1h\|3h\|1d\|morning]` | Push a due task back (default 10m) |
|
|
83
84
|
| `doing rm <id>` | Delete a task |
|
|
84
85
|
| `doing ls` | List active tasks (`-c` context ID) |
|
|
85
86
|
| `doing log` | Show tasks done today (`-d YYYY-MM-DD`, `-c` context ID) |
|
|
@@ -93,6 +94,19 @@ doing apikey --key doing_xxxxx
|
|
|
93
94
|
| `doing logout` | Log out |
|
|
94
95
|
| `doing apikey` | Save an API key (`-k` key) |
|
|
95
96
|
|
|
97
|
+
## Due dates and reminders
|
|
98
|
+
|
|
99
|
+
Give a task a due date and the Doing iOS app nags you every few minutes once it's overdue, until you mark it done or snooze it.
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
doing add "Call mom" -d "tomorrow 3pm"
|
|
103
|
+
doing add "Water plants" -d friday -r 3d # repeats 3 days after each completion
|
|
104
|
+
doing add "Standup notes" -d "9am" -r weekdays
|
|
105
|
+
doing snooze 12 1h
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`-d` accepts natural language: `in 20m`, `tonight`, `fri 9am`, `oct 30`. Dates without a time use your morning time (9:00 by default). `-r` accepts `daily`, `weekdays`, days like `mon,thu`, or `Nd` (N days after done).
|
|
109
|
+
|
|
96
110
|
## JSON output
|
|
97
111
|
|
|
98
112
|
Every command supports `--json` (or `-j`) for machine-readable output. This is useful for scripting and AI coding agents like Claude Code or Codex CLI.
|
|
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
|