Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions cmd/things/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -826,7 +826,7 @@ func TestRunListSomedayAndLogbookIncludeProjects(t *testing.T) {
// either the indented or the compact encoding.
var numericTypeField = regexp.MustCompile(`"type":\s*-?\d`)

// `type` renders as a string on every JSON surface that carries it — `todo`
// `type` renders as a string on every JSON surface that carries it — `task`
// or `project`, never the raw Things code (issue #208). The assertions are
// against the raw JSON rather than an unmarshalled model.Task on purpose:
// TaskType.UnmarshalJSON still accepts the legacy integer, so decoding would
Expand All @@ -852,11 +852,11 @@ func TestRunJSONRendersTypeAsString(t *testing.T) {
args []string
want []string
}{
{"list", []string{"--json", "list", "today"}, []string{`"type": "project"`, `"type": "todo"`}},
{"show todo", []string{"--json", "show", "todo-milk"}, []string{`"type": "todo"`}},
{"list", []string{"--json", "list", "today"}, []string{`"type": "project"`, `"type": "task"`}},
{"show todo", []string{"--json", "show", "todo-milk"}, []string{`"type": "task"`}},
{"show project", []string{"--json", "show", "proj-audit"}, []string{`"type": "project"`}},
{"search", []string{"--json", "search", "Runbook"}, []string{`"type": "project"`}},
{"repeating", []string{"--json", "list", "repeating"}, []string{`"type": "todo"`, `"type": "project"`}},
{"repeating", []string{"--json", "list", "repeating"}, []string{`"type": "task"`, `"type": "project"`}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
Expand Down
8 changes: 4 additions & 4 deletions docs/content/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ Every command accepts `-j` / `--json`, and it changes more than the format:
- **Status is a string enum**, `"open"`, `"completed"` or `"cancelled"`,
not the raw Things integer. `"repeating": true` marks a repeating item
and is omitted otherwise.
- **Type is a string enum too**, `"todo"` or `"project"`, not the raw
- **Type is a string enum too**, `"task"` or `"project"`, not the raw
Things integer. It rides on task rows only — `things projects` rows
carry no `type` — and headings are never returned, so the third Things
type never reaches the output. In v0.7.0 and earlier this field was the
Expand Down Expand Up @@ -220,7 +220,7 @@ same reason applied to a different column, so a sweep of what is due no
longer misses a project deadline. A bare filter with no view named —
`things -p X`, `things -a Work`, `things -t urgent` — is the exception: it
lists the open to-dos of that project, area or tag, so name a view when the
project rows matter. Each row carries `"type"` — `"todo"` or `"project"` — so
project rows matter. Each row carries `"type"` — `"task"` or `"project"` — so
a script that acts on a listing should say which kind it means. It matters: `edit` refuses a project with `not a task`, and `complete`
on a project closes every to-do inside it, so it asks first and refuses
outright under `--json` without `--yes`.
Expand All @@ -236,8 +236,8 @@ things complete "$uuid"
things deadlines -j | jq '.[] | select(.deadline < "2026-10-01") | {title, deadline}'

# Reschedule a whole area. Not transactional: partial failures stick.
# select(.type=="todo") keeps scheduled projects out of `things edit`.
things upcoming --area Work -j | jq -r '.[] | select(.type=="todo") | .uuid' |
# select(.type=="task") keeps scheduled projects out of `things edit`.
things upcoming --area Work -j | jq -r '.[] | select(.type=="task") | .uuid' |
while read -r uuid; do things edit "$uuid" --when monday; done

# Bulk create or update in one call via the Things JSON URL scheme.
Expand Down
2 changes: 1 addition & 1 deletion docs/content/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ accept `-j` / `--json` for structured output. Run `things --help` or

In JSON, `status` and `type` are string enums rather than the raw Things
integers. `status` is `"open"`, `"completed"` or `"cancelled"`, and appears
on to-dos, projects and checklist items. `type` is `"todo"` or `"project"`,
on to-dos, projects and checklist items. `type` is `"task"` or `"project"`,
and appears on task rows only — `projects`, `areas` and `tags` rows carry no
`type`. Headings are never returned by any command, so the third Things type
never reaches the output. In v0.7.0 and earlier `type` was the integer `0`,
Expand Down
10 changes: 5 additions & 5 deletions internal/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,19 @@ const (
// human-readable string so scripts and agents never have to decode the magic
// ints — the same treatment Status gets.
//
// Only "todo" and "project" ever reach output: every list view pins the type
// Only "task" and "project" ever reach output: every list view pins the type
// in SQL and every lookup applies the notHeading filter, so a heading row is
// never returned (see internal/db/tasks.go). "heading" is defined because the
// codec has to be total over the three codes the database uses.
type TaskType int

// typeNames is the single source of truth for the name<->code mapping used by
// String, MarshalJSON, and UnmarshalJSON. TypeTask renders as "todo", which is
// String, MarshalJSON, and UnmarshalJSON. TypeTask renders as "task", which is
// deliberately not the "to-do" that Things' own JSON URL scheme uses for the
// same concept in a `things import` payload — that payload is Things'
// vocabulary, not the CLI's, and the two are not interchangeable.
var typeNames = map[TaskType]string{
TypeTask: "todo",
TypeTask: "task",
TypeProject: "project",
TypeHeading: "heading",
}
Expand All @@ -54,7 +54,7 @@ func (t TaskType) String() string {
}

// MarshalJSON renders a recognized type as its string name
// ("todo"/"project"/"heading"). An unrecognized raw Things code is preserved
// ("task"/"project"/"heading"). An unrecognized raw Things code is preserved
// as its integer so the value round-trips losslessly rather than collapsing to
// a lossy "unknown" string.
func (t TaskType) MarshalJSON() ([]byte, error) {
Expand All @@ -70,7 +70,7 @@ func (t TaskType) MarshalJSON() ([]byte, error) {
func (t *TaskType) UnmarshalJSON(data []byte) error {
// Per the json.Unmarshaler convention, a JSON null is a no-op: leave the
// existing value untouched rather than silently coercing it to
// TaskType(0) ("todo").
// TaskType(0) ("task").
if string(data) == "null" {
return nil
}
Expand Down
8 changes: 4 additions & 4 deletions internal/model/model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ func TestTaskTypeMarshalJSON(t *testing.T) {
taskType TaskType
want string
}{
{TypeTask, `"todo"`},
{TypeTask, `"task"`},
{TypeProject, `"project"`},
{TypeHeading, `"heading"`},
{TaskType(99), `99`}, // unrecognized code preserved as its raw int
Expand All @@ -262,7 +262,7 @@ func TestTaskTypeUnmarshalJSON(t *testing.T) {
in string
want TaskType
}{
{`"todo"`, TypeTask},
{`"task"`, TypeTask},
{`"project"`, TypeProject},
{`"heading"`, TypeHeading},
{`0`, TypeTask}, // legacy integer input
Expand All @@ -280,7 +280,7 @@ func TestTaskTypeUnmarshalJSON(t *testing.T) {
}
}
// A JSON null is a no-op: it must leave the existing value untouched rather
// than silently coercing it to TaskType(0) ("todo").
// than silently coercing it to TaskType(0) ("task").
pre := TypeProject
if err := json.Unmarshal([]byte(`null`), &pre); err != nil {
t.Fatalf("Unmarshal(null): %v", err)
Expand Down Expand Up @@ -323,7 +323,7 @@ func TestTaskTypeString(t *testing.T) {
taskType TaskType
want string
}{
{TypeTask, "todo"},
{TypeTask, "task"},
{TypeProject, "project"},
{TypeHeading, "heading"},
{TaskType(99), "unknown"},
Expand Down
4 changes: 2 additions & 2 deletions internal/skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ A title can match several items. Under `--json` an ambiguous reference is an err
Most commands accept `--json` / `-j`. Prefer it when parsing. It also guarantees the command never blocks on a prompt.

- `status` is a string enum — `"open"`, `"cancelled"`, `"completed"` — on tasks, projects and checklist items, not the raw Things integer. Filter with `jq 'select(.status=="open")'`.
- `type` is a string enum the same way — `"todo"` or `"project"` — not the raw Things integer. It is on task rows only: `things projects` rows and checklist items carry no `type`. Headings are never returned by any command, so `"heading"` never appears. Filter with `jq 'select(.type=="project")'`. **This changed:** in v0.7.0 and earlier `type` was the integer `0`, `1` or `2`, so a filter matching on `.type==1` needs updating. Do not copy this value into an `import` payload — that format is Things' own and spells a to-do `"to-do"`, and neither the CLI nor Things will tell you the item was dropped.
- `type` is a string enum the same way — `"task"` or `"project"` — not the raw Things integer. It is on task rows only: `things projects` rows and checklist items carry no `type`. Headings are never returned by any command, so `"heading"` never appears. Filter with `jq 'select(.type=="project")'`. **This changed:** in v0.7.0 and earlier `type` was the integer `0`, `1` or `2`, so a filter matching on `.type==1` needs updating. Do not copy this value into an `import` payload — that format is Things' own and spells a to-do `"to-do"`, and neither the CLI nor Things will tell you the item was dropped.
- `"repeating": true` marks an item Things treats as repeating; the field is omitted otherwise. A project appearing as a row in a task listing carries `"type": "project"`.
- `things projects` reports `start`, `startBucket`, `startDate` and `deadline` under the same names and encodings a to-do uses, so a scheduled project reads the same way without a per-project `show`. `startDate` and `deadline` are omitted when unset.
- The `today`, `upcoming`, `anytime`, `someday` and `logbook` views list projects alongside to-dos, as the app does — scheduled in the first three, deferred in `someday`, completed in `logbook`. Split them on `"type"` — `jq 'select(.type=="project")'` for the projects, `select(.type=="todo")` for the to-dos. Plain output tags a project row `(project)`.
- The `today`, `upcoming`, `anytime`, `someday` and `logbook` views list projects alongside to-dos, as the app does — scheduled in the first three, deferred in `someday`, completed in `logbook`. Split them on `"type"` — `jq 'select(.type=="project")'` for the projects, `select(.type=="task")` for the to-dos. Plain output tags a project row `(project)`.
- `things projects` also reports `taskCount` and `openCount`. `taskCount` is every untrashed to-do in the project; `openCount` is the ones still open. The difference is the ones that are no longer open, which means completed or cancelled. To-dos filed under a project heading count towards both; the heading rows themselves never do, and neither do trashed to-dos or checklist items. Both numbers are Things' own bookkeeping, read straight from the database rather than recounted by the CLI.
- Human output is styled and column-aligned; colour auto-disables when piping or under `NO_COLOR`. `--color=always|never` overrides. JSON is unaffected.

Expand Down
Loading