doingapp 0.6.0__tar.gz → 0.7.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: doingapp
3
- Version: 0.6.0
3
+ Version: 0.7.0
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.
@@ -49,14 +49,19 @@ def get_me() -> dict:
49
49
  return r.json()
50
50
 
51
51
 
52
- def create_task(title: str, notes: str | None = None, context_id: int | None = None) -> dict:
52
+ def create_task(title: str, notes: str | None = None, context_id: int | None = None,
53
+ due: str | None = None, recurrence: str | None = None) -> dict:
53
54
  payload: dict = {"title": title}
54
55
  if notes is not None:
55
56
  payload["notes"] = notes
56
57
  if context_id is not None:
57
58
  payload["context_id"] = context_id
59
+ if due is not None:
60
+ payload["due"] = due
61
+ if recurrence is not None:
62
+ payload["recurrence"] = recurrence
58
63
  with _client() as c:
59
- r = c.post("/tasks", json=payload)
64
+ r = c.post("/tasks", json=payload, params={"tz": _local_tz()})
60
65
  r.raise_for_status()
61
66
  return r.json()
62
67
 
@@ -85,7 +90,7 @@ def list_tasks(status: str | None = None, date: str | None = None,
85
90
 
86
91
  def mark_done(task_id: int) -> dict:
87
92
  with _client() as c:
88
- r = c.post(f"/tasks/{task_id}/done")
93
+ r = c.post(f"/tasks/{task_id}/done", params={"tz": _local_tz()})
89
94
  r.raise_for_status()
90
95
  return r.json()
91
96
 
@@ -112,7 +117,9 @@ def resume_task(task_id: int) -> dict:
112
117
 
113
118
 
114
119
  def update_task(task_id: int, title: str | None = None, notes: str | None = None,
115
- context_id: int | None = None, clear_context: bool = False) -> dict:
120
+ context_id: int | None = None, clear_context: bool = False,
121
+ due: str | None = None, recurrence: str | None = None,
122
+ clear_due: bool = False, clear_recurrence: bool = False) -> dict:
116
123
  payload: dict = {}
117
124
  if title is not None:
118
125
  payload["title"] = title
@@ -122,8 +129,24 @@ def update_task(task_id: int, title: str | None = None, notes: str | None = None
122
129
  payload["clear_context"] = True
123
130
  elif context_id is not None:
124
131
  payload["context_id"] = context_id
132
+ if clear_due:
133
+ payload["clear_due"] = True
134
+ elif due is not None:
135
+ payload["due"] = due
136
+ if clear_recurrence:
137
+ payload["clear_recurrence"] = True
138
+ elif recurrence is not None:
139
+ payload["recurrence"] = recurrence
125
140
  with _client() as c:
126
- r = c.patch(f"/tasks/{task_id}", json=payload)
141
+ r = c.patch(f"/tasks/{task_id}", json=payload, params={"tz": _local_tz()})
142
+ r.raise_for_status()
143
+ return r.json()
144
+
145
+
146
+ def snooze_task(task_id: int, minutes: int | None = None, preset: str | None = None) -> dict:
147
+ payload: dict = {"minutes": minutes} if minutes is not None else {"preset": preset}
148
+ with _client() as c:
149
+ r = c.post(f"/tasks/{task_id}/snooze", json=payload, params={"tz": _local_tz()})
127
150
  r.raise_for_status()
128
151
  return r.json()
129
152
 
@@ -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
- console.print(f"[green]✓[/green] Done: [bold]{task['title']}[/bold]")
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 context."""
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
- _validation_error("No changes specified. Use --title, --notes, --context, or --no-context.")
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"[green bold]Updated:[/green bold] {task['title']} [dim](#{task['id']})[/dim]")
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.6.0
3
+ Version: 0.7.0
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.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "doingapp"
7
- version = "0.6.0"
7
+ version = "0.7.0"
8
8
  description = "A fast CLI task tracker — track what you're doing from the terminal."
9
9
  readme = "README.md"
10
10
  license = "MIT"
File without changes
File without changes
File without changes
File without changes