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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,9 @@ Filters:

`-p`/`-a`/`-t` combine with any view. The date filters (`--on`, `--from`,
`--to`) apply to date-filterable views — `today`, `upcoming`, `anytime`,
`someday`, `deadlines`, and project listings — and `--on` can't be combined
with `--from`/`--to`. `--include-completed` applies to the `today` view only.
`deadlines`, and project listings (`someday` items have no start date, so
they can't be date-filtered) — and `--on` can't be combined with
`--from`/`--to`. `--include-completed` applies to the `today` view only.

Examples:

Expand Down
2 changes: 1 addition & 1 deletion internal/db/areas.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func (d *DB) ListAreas() ([]model.Area, error) {
}
defer rows.Close()

var areas []model.Area
areas := []model.Area{}
for rows.Next() {
var a model.Area
var visible int
Expand Down
46 changes: 46 additions & 0 deletions internal/db/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,49 @@ func TestOpenBadPath(t *testing.T) {
t.Fatal("expected error for bad path")
}
}

// Empty result sets must be non-nil slices: a nil slice JSON-encodes as
// `null`, which breaks documented `--json | jq '.[]'` pipelines.
func TestEmptyResultsAreNonNil(t *testing.T) {
d := newTestDB(t)

tasks, err := d.ListTasks("today", TaskFilter{})
if err != nil {
t.Fatalf("ListTasks: %v", err)
}
if tasks == nil || len(tasks) != 0 {
t.Errorf("ListTasks: want non-nil empty slice, got %#v", tasks)
}

found, err := d.SearchTasks("no-such-task")
if err != nil {
t.Fatalf("SearchTasks: %v", err)
}
if found == nil || len(found) != 0 {
t.Errorf("SearchTasks: want non-nil empty slice, got %#v", found)
}

projects, err := d.ListProjects("", false)
if err != nil {
t.Fatalf("ListProjects: %v", err)
}
if projects == nil || len(projects) != 0 {
t.Errorf("ListProjects: want non-nil empty slice, got %#v", projects)
}

areas, err := d.ListAreas()
if err != nil {
t.Fatalf("ListAreas: %v", err)
}
if areas == nil || len(areas) != 0 {
t.Errorf("ListAreas: want non-nil empty slice, got %#v", areas)
}

tags, err := d.ListTags()
if err != nil {
t.Fatalf("ListTags: %v", err)
}
if tags == nil || len(tags) != 0 {
t.Errorf("ListTags: want non-nil empty slice, got %#v", tags)
}
}
2 changes: 1 addition & 1 deletion internal/db/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func (d *DB) ListProjects(areaFilter string, includeCompleted bool) ([]model.Pro
}
defer rows.Close()

var projects []model.Project
projects := []model.Project{}
for rows.Next() {
var p model.Project
var tagsStr string
Expand Down
2 changes: 1 addition & 1 deletion internal/db/tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func (d *DB) ListTags() ([]model.Tag, error) {
}
defer rows.Close()

var tags []model.Tag
tags := []model.Tag{}
for rows.Next() {
var t model.Tag
if err := rows.Scan(&t.UUID, &t.Title, &t.Shortcut, &t.ParentUUID); err != nil {
Expand Down
6 changes: 3 additions & 3 deletions internal/db/tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ type TaskFilter struct {

// dateFilterableViews lists the views where --on/--from/--to make sense.
// Excluded: inbox tasks have no startDate; trash is trashed-only; logbook
// items have a stopDate but no meaningful startDate filter.
// items have a stopDate but no meaningful startDate filter; someday requires
// startDate IS NULL, so a startDate range could never match anything.
var dateFilterableViews = map[string]bool{
"today": true,
"upcoming": true,
"anytime": true,
"someday": true,
"deadlines": true,
"project": true,
}
Expand Down Expand Up @@ -286,7 +286,7 @@ func (d *DB) collectTasks(query string, args ...any) ([]model.Task, error) {
}
defer rows.Close()

var tasks []model.Task
tasks := []model.Task{}
for rows.Next() {
t, err := scanTask(rows)
if err != nil {
Expand Down
6 changes: 4 additions & 2 deletions internal/db/tasks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,8 +316,10 @@ func TestListTasksDeadlinesDateFilters(t *testing.T) {
}

func TestDateFilterableView(t *testing.T) {
allowed := []string{"today", "upcoming", "anytime", "someday", "deadlines", "project"}
denied := []string{"inbox", "trash", "logbook", "bogus"}
allowed := []string{"today", "upcoming", "anytime", "deadlines", "project"}
// someday is denied because its view predicate requires startDate IS NULL —
// a startDate range filter could never match anything.
denied := []string{"inbox", "trash", "logbook", "someday", "bogus"}
for _, v := range allowed {
if !DateFilterableView(v) {
t.Errorf("%q: expected filterable", v)
Expand Down
11 changes: 11 additions & 0 deletions internal/output/output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,17 @@ func TestPrintEmptyTasks(t *testing.T) {
}
}

// Empty lists must encode as [], not null — jq '.[]' fails on null.
func TestPrintEmptyTasksJSON(t *testing.T) {
var buf bytes.Buffer
if err := Print(&buf, []model.Task{}, true); err != nil {
t.Fatalf("Print: %v", err)
}
if got := strings.TrimSpace(buf.String()); got != "[]" {
t.Errorf("expected [], got %q", got)
}
}

func TestPrintProjectsPlain(t *testing.T) {
projects := []model.Project{
{UUID: "p1", Title: "Empty project", TaskCount: 0},
Expand Down
3 changes: 2 additions & 1 deletion internal/skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ things list [view] [--project P] [--area A] [--tag T] [--on D | --from D --to D]
# shortcut: `things today`, `things inbox`, etc.
# --on / --from / --to take YYYY-MM-DD (or RFC3339). They filter startDate
# on most views and `deadline` on the `deadlines` view. Not supported on
# inbox/trash/logbook. --on is mutually exclusive with --from/--to.
# inbox/trash/logbook/someday (someday items have no start date).
# --on is mutually exclusive with --from/--to.
# today shows only open tasks; --include-completed also lists completed/
# cancelled items Things hasn't logged out of Today yet (today only).

Expand Down
Loading