diff --git a/cmd/things/add.go b/cmd/things/add.go index b06637d..8ed389a 100644 --- a/cmd/things/add.go +++ b/cmd/things/add.go @@ -86,7 +86,7 @@ func preAdd(d *Deps, kind string, p addParams, flags TagFlags) error { func resolveAddTarget(d *Deps, list, heading string) (db.Target, createdDest) { if list == "" { if heading != "" { - fmt.Fprintf(d.errOut(), "warning: --heading %q needs --list or --project; Things will ignore it and put the to-do in the Inbox\n", heading) + fmt.Fprintf(d.errOut(), "warning: %s\n", headingNeedsList("--heading", heading, "--list or --project", "put the to-do in the Inbox")) } return db.Target{}, createdDest{checked: true} } @@ -102,7 +102,7 @@ func resolveAddTarget(d *Deps, list, heading string) (db.Target, createdDest) { fmt.Fprintf(d.errOut(), "warning: %s\n", noTarget("project or area", list, false, "it will put the to-do in the Inbox")) return target, createdDest{checked: true} case heading != "" && !headingFound: - fmt.Fprintf(d.errOut(), "warning: %s\n", noHeading(list, heading, "add the to-do there without a heading")) + fmt.Fprintf(d.errOut(), "warning: %s\n", noHeading(listName(list, target), heading, "add the to-do there without a heading")) } noteTarget(d, list, "lists", target) return target, addDest(target) diff --git a/cmd/things/edit.go b/cmd/things/edit.go index d6cb31c..396bc3f 100644 --- a/cmd/things/edit.go +++ b/cmd/things/edit.go @@ -205,7 +205,7 @@ func (c *EditCmd) checkMove(d *Deps, database *db.DB, task *model.Task) bool { return !inList || task.HeadingUUID != target.Heading case target.UUID != "": if heading != "" { - fmt.Fprintf(d.errOut(), "warning: %s\n", noHeading(list, heading, "move the to-do there without a heading")) + fmt.Fprintf(d.errOut(), "warning: %s\n", noHeading(listName(list, target), heading, "move the to-do there without a heading")) } return task.HeadingUUID != "" || !inList case c.Heading == nil: @@ -219,7 +219,7 @@ func (c *EditCmd) checkMove(d *Deps, database *db.DB, task *model.Task) bool { // The unknown list's warning has said the to-do stays put. return false case task.ProjectUUID == "": - fmt.Fprintf(d.errOut(), "warning: --heading %q needs --list: the to-do is not in a project, so Things will leave it where it is\n", heading) + fmt.Fprintf(d.errOut(), "warning: %s\n", headingNeedsList("--heading", heading, "--list or --list-id, as the to-do is not in a project", "leave the to-do where it is")) return false } t, f, err := database.AddTarget(task.ProjectUUID, heading) diff --git a/cmd/things/edit_test.go b/cmd/things/edit_test.go index ba0c132..0fc1d61 100644 --- a/cmd/things/edit_test.go +++ b/cmd/things/edit_test.go @@ -635,17 +635,17 @@ func TestEditWarnsOnUnresolvedMove(t *testing.T) { {"unknownHeadingIDWithList", []string{"edit", "one-1", "--list", "Tools", "--heading-id", "nope"}, `no heading with id "nope"; it will ignore --heading-id`, false}, {"unknownHeading", []string{"edit", "one-1", "--list", "Tools", "--heading", "Later"}, `"Tools" has no heading "Later"; Things will move the to-do there without a heading`, false}, {"paddedHeading", []string{"edit", "one-1", "--list", "Tools", "--heading", " Setup "}, `"Tools" has no heading " Setup "`, false}, - {"unknownHeadingInListID", []string{"edit", "one-1", "--list-id", "proj-1", "--heading", "Later"}, `"proj-1" has no heading "Later"`, false}, + {"unknownHeadingInListID", []string{"edit", "one-1", "--list-id", "proj-1", "--heading", "Later"}, `"Tools" has no heading "Later"`, false}, {"knownHeadingInListID", []string{"edit", "one-1", "--list-id", "proj-1", "--heading", "Setup"}, "", false}, - {"unknownHeadingInListUUID", []string{"edit", "one-1", "--list", "proj-1", "--heading", "Later"}, `"proj-1" has no heading "Later"`, false}, - {"headingInAreaUUID", []string{"edit", "one-1", "--list", "area-1", "--heading", "Setup"}, `"area-1" has no heading "Setup"`, false}, + {"unknownHeadingInListUUID", []string{"edit", "one-1", "--list", "proj-1", "--heading", "Later"}, `"Tools" has no heading "Later"`, false}, + {"headingInAreaUUID", []string{"edit", "one-1", "--list", "area-1", "--heading", "Setup"}, `"Personal" has no heading "Setup"`, false}, {"headingOnlyKnown", []string{"edit", "tool-1", "--heading", "SETUP"}, "", false}, // Of two headings that differ only in case, Things files the to-do // under the one with the lower uuid, whichever case was sent. {"caseTwinHeading", []string{"edit", "twin-todo", "--heading", "SETUP"}, "", false}, {"caseTwinListAndHeading", []string{"edit", "twin-todo", "--list", "Tools", "--heading", "SETUP"}, "", false}, {"headingOnlyUnknown", []string{"edit", "tool-1", "--heading", "Later"}, `"Tools" has no heading "Later"; Things will leave the to-do where it is`, true}, - {"headingOnlyNoProject", []string{"edit", "one-1", "--heading", "Setup"}, `--heading "Setup" needs --list: the to-do is not in a project, so Things will leave it where it is`, true}, + {"headingOnlyNoProject", []string{"edit", "one-1", "--heading", "Setup"}, `--heading "Setup" needs --list or --list-id, as the to-do is not in a project; Things will ignore it and leave the to-do where it is`, true}, {"headingID", []string{"edit", "one-1", "--list", "Tools", "--heading-id", "head-1"}, "", false}, // Measured in Things 3: an empty heading alone clears the heading, // and an empty list unfiles the to-do into Anytime. Both are sent diff --git a/cmd/things/importdest.go b/cmd/things/importdest.go index d61aa84..fbd253e 100644 --- a/cmd/things/importdest.go +++ b/cmd/things/importdest.go @@ -192,7 +192,7 @@ func (r *destResolver) taskDest(c importCreate) createdDest { return createdDest{checked: true} case list == "": if heading != "" { - r.warn(c, "heading %q needs a list; Things will ignore it and put the to-do in the Inbox", heading) + r.warn(c, "%s", headingNeedsList("heading", heading, "list or list-id", "put the to-do in the Inbox")) } return createdDest{checked: true} case !byID && to.listInPayload: @@ -225,7 +225,7 @@ func (r *destResolver) taskDest(c importCreate) createdDest { // lowest-uuid twin of one the list already has. dest.anyHeading = true case heading != "" && !res.headingFound: - r.warn(c, "%s", noHeading(list, heading, "add the to-do there without a heading")) + r.warn(c, "%s", noHeading(listName(list, res.target), heading, "add the to-do there without a heading")) } return dest } diff --git a/cmd/things/project.go b/cmd/things/project.go index ff69dee..5d3cf9b 100644 --- a/cmd/things/project.go +++ b/cmd/things/project.go @@ -87,7 +87,7 @@ func projectArea(d *Deps, area, fallback string) (target db.Target, read bool) { case err != nil: return db.Target{}, false case t.UUID == "": - fmt.Fprintf(d.errOut(), "warning: %s\n", noArea(area, false, fallback)) + fmt.Fprintf(d.errOut(), "warning: %s\n", noTarget("area", area, false, fallback)) } return t, true } @@ -147,7 +147,7 @@ func (c *ProjectEditCmd) checkOwn(d *Deps, database *db.DB, task *model.Task) bo return moves } -// checkAreaID warns when Things has no area with the uuid --area-id gives, +// checkAreaID warns when Things finds no area with the uuid --area-id gives, // and reports whether the project may move. Checked in Things 3: an unknown // area-id leaves the project where it is. func (c *ProjectEditCmd) checkAreaID(d *Deps, database *db.DB, task *model.Task) bool { @@ -166,6 +166,6 @@ func (c *ProjectEditCmd) checkAreaID(d *Deps, database *db.DB, task *model.Task) case t.ByUUID: return true } - fmt.Fprintf(d.errOut(), "warning: %s\n", noArea(*c.AreaID, true, "the project will stay where it is")) + fmt.Fprintf(d.errOut(), "warning: %s\n", noTarget("area", *c.AreaID, true, "the project will stay where it is")) return false } diff --git a/cmd/things/targetmsg.go b/cmd/things/targetmsg.go index 5482a21..7300c47 100644 --- a/cmd/things/targetmsg.go +++ b/cmd/things/targetmsg.go @@ -1,16 +1,22 @@ package main -import "fmt" +import ( + "fmt" + + "github.com/ryanlewis/things-cli/internal/db" +) // The warnings add, edit, project and import give when Things cannot find // the list, area or heading an item was sent to. Agents read this wording, -// so each command builds it here rather than by hand. (The "--heading needs -// --list" warnings differ per command and stay with add and edit.) Where the -// rules differ between commands, the caller decides; these only say it. +// so each command builds it here rather than by hand. Where the rules differ +// between commands, the caller decides; these only say it. // noTarget is the warning for ref, sent by uuid when byID and by title // otherwise, when Things finds no noun ("project or area", or "area") for -// it. next says what Things does instead. +// it. next says what Things does instead. It says "finds no" rather than +// "has no": the check is of how Things matches the name, and an item can +// exist that Things will still not match, such as one by its uuid sent as a +// title. func noTarget(noun, ref string, byID bool, next string) string { if byID { return fmt.Sprintf("Things finds no %s with id %q; %s", noun, ref, next) @@ -18,13 +24,21 @@ func noTarget(noun, ref string, byID bool, next string) string { return fmt.Sprintf("Things finds no %s called %q; %s", noun, ref, next) } -// noArea is project's warning for an area Things cannot find, as noTarget. -// It says "has no area" where import's says "finds no area". -func noArea(ref string, byID bool, next string) string { - if byID { - return fmt.Sprintf("Things has no area with id %q; %s", ref, next) +// headingNeedsList is the warning for a heading sent with no list to file it +// in. name is the flag or field ("--heading", "heading"), needs what would +// give it a list, and next what Things does with the to-do instead. +func headingNeedsList(name, heading, needs, next string) string { + return fmt.Sprintf("%s %q needs %s; Things will ignore it and %s", name, heading, needs, next) +} + +// listName is how a warning names the list ref led to: by its title when +// ref was the list's uuid, since that is the name the user knows it by, and +// as given otherwise. +func listName(ref string, t db.Target) string { + if t.ByUUID && t.Title != "" { + return t.Title } - return fmt.Sprintf("Things has no area called %q; %s", ref, next) + return ref } // noHeading is the warning for a heading title list does not have. next is diff --git a/cmd/things/targetmsg_test.go b/cmd/things/targetmsg_test.go index 82a28c1..be5209d 100644 --- a/cmd/things/targetmsg_test.go +++ b/cmd/things/targetmsg_test.go @@ -19,6 +19,8 @@ func TestTargetWarningsExact(t *testing.T) { `Things finds no project or area called "Nowhere"; it will put the to-do in the Inbox`}, {"addUnknownHeading", []string{"add", "Buy oat milk", "--list", "Tools", "--heading", "Later"}, `"Tools" has no heading "Later"; Things will add the to-do there without a heading`}, + {"addUnknownHeadingInListUUID", []string{"add", "Buy oat milk", "--list", "proj-1", "--heading", "Later"}, + `"Tools" has no heading "Later"; Things will add the to-do there without a heading`}, {"addHeadingWithoutList", []string{"add", "Buy oat milk", "--heading", "Setup"}, `--heading "Setup" needs --list or --project; Things will ignore it and put the to-do in the Inbox`}, {"editUnknownList", []string{"edit", "one-1", "--list", "Nowhere"}, @@ -34,21 +36,21 @@ func TestTargetWarningsExact(t *testing.T) { {"editUnknownHeading", []string{"edit", "one-1", "--list", "Tools", "--heading", "Later"}, `"Tools" has no heading "Later"; Things will move the to-do there without a heading`}, {"editUnknownHeadingInListID", []string{"edit", "one-1", "--list-id", "proj-1", "--heading", "Later"}, - `"proj-1" has no heading "Later"; Things will move the to-do there without a heading`}, + `"Tools" has no heading "Later"; Things will move the to-do there without a heading`}, {"editUnknownOwnHeading", []string{"edit", "tool-1", "--heading", "Later"}, `"Tools" has no heading "Later"; Things will leave the to-do where it is`}, {"editHeadingNoProject", []string{"edit", "one-1", "--heading", "Setup"}, - `--heading "Setup" needs --list: the to-do is not in a project, so Things will leave it where it is`}, + `--heading "Setup" needs --list or --list-id, as the to-do is not in a project; Things will ignore it and leave the to-do where it is`}, {"editUnknownHeadingID", []string{"edit", "tool-1", "--heading-id", "nope"}, `Things has no heading with id "nope"; the to-do will stay where it is`}, {"editUnknownHeadingIDWithList", []string{"edit", "one-1", "--list", "Tools", "--heading-id", "nope"}, `Things has no heading with id "nope"; it will ignore --heading-id`}, {"projectAddUnknownArea", []string{"project", "add", "Launch", "--area", "Nowhere"}, - `Things has no area called "Nowhere"; it will create the project with no area`}, + `Things finds no area called "Nowhere"; it will create the project with no area`}, {"projectEditUnknownArea", []string{"project", "edit", "garden-1", "--area", "Nowhere"}, - `Things has no area called "Nowhere"; the project will stay where it is`}, + `Things finds no area called "Nowhere"; the project will stay where it is`}, {"projectEditUnknownAreaID", []string{"project", "edit", "garden-1", "--area-id", "nope"}, - `Things has no area with id "nope"; the project will stay where it is`}, + `Things finds no area with id "nope"; the project will stay where it is`}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -86,6 +88,8 @@ func TestImportTargetWarningsExact(t *testing.T) { {"type":"to-do","attributes":{"title":"c","list":"proj-1"}}, {"type":"to-do","attributes":{"title":"d","list":"Tools","heading":"Setup"}}, {"type":"to-do","attributes":{"title":"e","list":"Tools","heading-id":"nope"}}, + {"type":"to-do","attributes":{"title":"f","heading":"Setup"}}, + {"type":"to-do","attributes":{"title":"g","list-id":"proj-1","heading":"Later"}}, {"type":"project","attributes":{"title":"Shed","area":"Nowhere"}}, {"type":"project","attributes":{"title":"Barn","area-id":"nope"}} ]` @@ -99,8 +103,10 @@ func TestImportTargetWarningsExact(t *testing.T) { `[2]: list "proj-1" is an id, and Things matches list by title only; it will put the to-do in the Inbox (use list-id)`, `[3]: "Tools" has no heading "Setup"; Things will add the to-do there without a heading`, `[4]: Things has no heading with id "nope"; it will ignore heading-id and heading`, - `[5]: Things finds no area called "Nowhere"; it will create the project in no area`, - `[6]: Things finds no area with id "nope"; it will create the project in no area`, + `[5]: heading "Setup" needs list or list-id; Things will ignore it and put the to-do in the Inbox`, + `[6]: "Tools" has no heading "Later"; Things will add the to-do there without a heading`, + `[7]: Things finds no area called "Nowhere"; it will create the project in no area`, + `[8]: Things finds no area with id "nope"; it will create the project in no area`, } { if !strings.Contains(stderr, "warning: "+want+"\n") { t.Errorf("stderr = %q, want the line %q", stderr, "warning: "+want) diff --git a/docs/content/commands.md b/docs/content/commands.md index 8df5f0c..212b572 100644 --- a/docs/content/commands.md +++ b/docs/content/commands.md @@ -566,7 +566,7 @@ nothing matches it creates the project with no area. `project add` warns on stderr when that will happen, then sends the project anyway: ``` -warning: Things has no area called "Nowhere"; it will create the project with no area +warning: Things finds no area called "Nowhere"; it will create the project with no area ``` Each line of `--todos`, like each `--checklist` item, may be at most 4000 @@ -649,7 +649,7 @@ warning: Things has no heading with id "abc123"; the to-do will stay where it is warning: Things has no heading with id "abc123"; it will ignore --heading-id warning: "Tools" has no heading "Later"; Things will move the to-do there without a heading warning: "Tools" has no heading "Later"; Things will leave the to-do where it is -warning: --heading "Later" needs --list: the to-do is not in a project, so Things will leave it where it is +warning: --heading "Later" needs --list or --list-id, as the to-do is not in a project; Things will ignore it and leave the to-do where it is ``` When the move files the to-do somewhere new, `edit` notes the same things @@ -798,8 +798,8 @@ one Things will use, as `project add` does, unless the project is already there. A move to the area the project is already in counts as no change. ``` -warning: Things has no area called "Nowhere"; the project will stay where it is -warning: Things has no area with id "abc123"; the project will stay where it is +warning: Things finds no area called "Nowhere"; the project will stay where it is +warning: Things finds no area with id "abc123"; the project will stay where it is ``` ## Tags must already exist