diff --git a/cmd/things/run_test.go b/cmd/things/run_test.go index cebca0f6..935febfd 100644 --- a/cmd/things/run_test.go +++ b/cmd/things/run_test.go @@ -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 @@ -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) { diff --git a/docs/content/agents.md b/docs/content/agents.md index 822722be..eafec695 100644 --- a/docs/content/agents.md +++ b/docs/content/agents.md @@ -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 @@ -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`. @@ -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. diff --git a/docs/content/commands.md b/docs/content/commands.md index 2e02114e..d408de75 100644 --- a/docs/content/commands.md +++ b/docs/content/commands.md @@ -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`, diff --git a/internal/model/model.go b/internal/model/model.go index 1ca90a9b..f4dcfb39 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -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", } @@ -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) { @@ -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 } diff --git a/internal/model/model_test.go b/internal/model/model_test.go index 89ae2f38..32d6e229 100644 --- a/internal/model/model_test.go +++ b/internal/model/model_test.go @@ -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 @@ -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 @@ -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) @@ -323,7 +323,7 @@ func TestTaskTypeString(t *testing.T) { taskType TaskType want string }{ - {TypeTask, "todo"}, + {TypeTask, "task"}, {TypeProject, "project"}, {TypeHeading, "heading"}, {TaskType(99), "unknown"}, diff --git a/internal/skill/SKILL.md b/internal/skill/SKILL.md index 3ea13849..6cf7ff88 100644 --- a/internal/skill/SKILL.md +++ b/internal/skill/SKILL.md @@ -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.