Detailed changes
@@ -51,10 +51,9 @@ internal/
config/ → TOML config at ~/.config/lune/config.toml
completion/ → Shell completion helpers (config-based + static values)
dateutil/ → Natural language date parsing via go-dateparser
- deeplink/ → Parse/build lunatask:// URLs
stats/ → Usage statistics helpers
ui/ → Lipgloss styles (Success, Warning, Error, H1, H2, FormatDate)
- validate/ → Input validation (UUID, enums, deep links → UUID)
+ validate/ → Input validation (delegates to go-lunatask Parse* functions)
```
**Command flow**: `fang.Execute()` wraps Cobra with version/commit info and
@@ -135,8 +134,9 @@ The implementations differ: name uses `bufio.Scanner` (single line), note uses
### Deep link support
Commands accepting IDs also accept `lunatask://` deep links. Use
-`validate.Reference()` to normalize either format to a UUID. The `deeplink`
-package handles parsing and building these URLs.
+`validate.Reference()` to normalize either format to a UUID. Deep link
+parsing and building use `lunatask.ParseDeepLink()` and `lunatask.BuildDeepLink()`
+from go-lunatask.
### Linter exclusions for cmd/
@@ -176,7 +176,7 @@ colors.
## Testing
Table-driven tests with `t.Parallel()`. Use `_test` package suffix for black-box
-testing (see `deeplink_test.go`). When testing commands, use `cmd.SetOut()` /
+testing (see `cmd/init/ui_test.go`). When testing commands, use `cmd.SetOut()` /
`cmd.SetErr()` to capture output.
### Init wizard
@@ -7,10 +7,10 @@ package area
import (
"fmt"
+ "git.secluded.site/go-lunatask"
"git.secluded.site/lune/internal/client"
"git.secluded.site/lune/internal/completion"
"git.secluded.site/lune/internal/config"
- "git.secluded.site/lune/internal/deeplink"
"git.secluded.site/lune/internal/stats"
"git.secluded.site/lune/internal/ui"
"github.com/spf13/cobra"
@@ -52,7 +52,7 @@ func runShow(cmd *cobra.Command, args []string) error {
}
func printAreaDetails(cmd *cobra.Command, area *config.Area, counter *stats.TaskCounter) error {
- link, _ := deeplink.Build(deeplink.Area, area.ID)
+ link, _ := lunatask.BuildDeepLink(lunatask.ResourceArea, area.ID)
fmt.Fprintf(cmd.OutOrStdout(), "%s (%s)\n", ui.H1.Render(area.Name), area.Key)
fmt.Fprintf(cmd.OutOrStdout(), " ID: %s\n", area.ID)
@@ -83,7 +83,7 @@ func printAreaDetails(cmd *cobra.Command, area *config.Area, counter *stats.Task
}
func printGoalSummary(cmd *cobra.Command, goal *config.Goal, counter *stats.TaskCounter) error {
- goalLink, _ := deeplink.Build(deeplink.Goal, goal.ID)
+ goalLink, _ := lunatask.BuildDeepLink(lunatask.ResourceGoal, goal.ID)
fmt.Fprintf(cmd.OutOrStdout(), " %s (%s)\n", ui.H2.Render(goal.Name), goal.Key)
fmt.Fprintf(cmd.OutOrStdout(), " ID: %s\n", goal.ID)
@@ -9,10 +9,10 @@ import (
"fmt"
"strings"
+ "git.secluded.site/go-lunatask"
"git.secluded.site/lune/internal/client"
"git.secluded.site/lune/internal/completion"
"git.secluded.site/lune/internal/config"
- "git.secluded.site/lune/internal/deeplink"
"git.secluded.site/lune/internal/stats"
"git.secluded.site/lune/internal/ui"
"github.com/spf13/cobra"
@@ -101,7 +101,7 @@ func resolveGoal(cfg *config.Config, goalKey, areaKey string) (*config.GoalMatch
}
func printGoalDetails(cmd *cobra.Command, goal *config.Goal, area *config.Area, counter *stats.TaskCounter) error {
- link, _ := deeplink.Build(deeplink.Goal, goal.ID)
+ link, _ := lunatask.BuildDeepLink(lunatask.ResourceGoal, goal.ID)
fmt.Fprintf(cmd.OutOrStdout(), "%s (%s)\n", ui.H1.Render(goal.Name), goal.Key)
fmt.Fprintf(cmd.OutOrStdout(), " ID: %s\n", goal.ID)
@@ -15,8 +15,8 @@ import (
"github.com/charmbracelet/huh"
"github.com/spf13/cobra"
+ "git.secluded.site/go-lunatask"
"git.secluded.site/lune/internal/config"
- "git.secluded.site/lune/internal/deeplink"
"git.secluded.site/lune/internal/ui"
)
@@ -161,22 +161,12 @@ func validateKeyFormat(input string) error {
return nil
}
-func validateReference(input string, supportsDeepLink bool) (string, error) {
+func validateReference(input string, _ bool) (string, error) {
if input == "" {
return "", errRefRequired
}
- if supportsDeepLink {
- id, err := deeplink.ParseID(input)
- if err != nil {
- return "", errRefFormat
- }
-
- return id, nil
- }
-
- // Habits don't support deep links, only raw UUIDs
- id, err := deeplink.ParseID(input)
+ _, id, err := lunatask.ParseDeepLink(input)
if err != nil {
return "", errRefFormat
}
@@ -79,7 +79,7 @@ func TestValidateReference_Valid(t *testing.T) {
wantID string
}{
{"valid UUID lowercase", uuidLower, false, uuidLower},
- {"valid UUID uppercase", "123E4567-E89B-12D3-A456-426614174000", false, uuidLower},
+ {"valid UUID uppercase", "123E4567-E89B-12D3-A456-426614174000", false, "123E4567-E89B-12D3-A456-426614174000"},
{"deep link area", "lunatask://areas/" + uuidArea, true, uuidArea},
{"deep link goal", "lunatask://goals/" + uuidGoal, true, uuidGoal},
{"deep link note", "lunatask://notes/" + uuidNote, true, uuidNote},
@@ -11,7 +11,6 @@ import (
"git.secluded.site/go-lunatask"
"git.secluded.site/lune/internal/client"
"git.secluded.site/lune/internal/config"
- "git.secluded.site/lune/internal/deeplink"
"git.secluded.site/lune/internal/ui"
"git.secluded.site/lune/internal/validate"
"github.com/spf13/cobra"
@@ -70,7 +69,7 @@ func outputNoteJSON(cmd *cobra.Command, note *lunatask.Note) error {
}
func printNoteDetails(cmd *cobra.Command, note *lunatask.Note) error {
- link, _ := deeplink.Build(deeplink.Note, note.ID)
+ link, _ := lunatask.BuildDeepLink(lunatask.ResourceNote, note.ID)
fmt.Fprintf(cmd.OutOrStdout(), "%s\n", ui.H1.Render("Note"))
fmt.Fprintf(cmd.OutOrStdout(), " ID: %s\n", note.ID)
@@ -10,7 +10,6 @@ import (
"git.secluded.site/go-lunatask"
"git.secluded.site/lune/internal/client"
- "git.secluded.site/lune/internal/deeplink"
"git.secluded.site/lune/internal/ui"
"git.secluded.site/lune/internal/validate"
"github.com/spf13/cobra"
@@ -69,7 +68,7 @@ func outputPersonJSON(cmd *cobra.Command, person *lunatask.Person) error {
}
func printPersonDetails(cmd *cobra.Command, person *lunatask.Person) error {
- link, _ := deeplink.Build(deeplink.Person, person.ID)
+ link, _ := lunatask.BuildDeepLink(lunatask.ResourcePerson, person.ID)
fmt.Fprintf(cmd.OutOrStdout(), "%s\n", ui.H1.Render("Person"))
fmt.Fprintf(cmd.OutOrStdout(), " ID: %s\n", person.ID)
@@ -11,7 +11,6 @@ import (
"git.secluded.site/go-lunatask"
"git.secluded.site/lune/internal/client"
"git.secluded.site/lune/internal/config"
- "git.secluded.site/lune/internal/deeplink"
"git.secluded.site/lune/internal/ui"
"git.secluded.site/lune/internal/validate"
"github.com/spf13/cobra"
@@ -70,7 +69,7 @@ func outputTaskJSON(cmd *cobra.Command, task *lunatask.Task) error {
}
func printTaskDetails(cmd *cobra.Command, task *lunatask.Task) error {
- link, _ := deeplink.Build(deeplink.Task, task.ID)
+ link, _ := lunatask.BuildDeepLink(lunatask.ResourceTask, task.ID)
status := "unknown"
if task.Status != nil {
@@ -1,15 +1,18 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: CC0-1.0
+
module git.secluded.site/lune
go 1.25.5
require (
- git.secluded.site/go-lunatask v0.1.0-rc9
+ git.secluded.site/go-lunatask v0.1.0-rc10
github.com/BurntSushi/toml v1.6.0
github.com/charmbracelet/fang v0.4.4
github.com/charmbracelet/huh v0.8.0
github.com/charmbracelet/huh/spinner v0.0.0-20251215014908-6f7d32faaff3
github.com/charmbracelet/lipgloss v1.1.0
- github.com/google/uuid v1.6.0
github.com/klauspost/lctime v0.1.0
github.com/markusmobius/go-dateparser v1.2.4
github.com/spf13/cobra v1.10.2
@@ -40,6 +43,7 @@ require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
+ github.com/google/uuid v1.6.0 // indirect
github.com/hablullah/go-hijri v1.0.2 // indirect
github.com/hablullah/go-juliandays v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
@@ -2,8 +2,8 @@ al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXy
al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 h1:D9PbaszZYpB4nj+d6HTWr1onlmlyuGVNfL9gAi8iB3k=
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410/go.mod h1:1qZyvvVCenJO2M1ac2mX0yyiIZJoZmDM4DG4s0udJkU=
-git.secluded.site/go-lunatask v0.1.0-rc9 h1:ri8PDl7Xzg3mGStvHBxvL5PKOlBSZGxKDBzkqurLpEw=
-git.secluded.site/go-lunatask v0.1.0-rc9/go.mod h1:sWUQxme1z7qfsfS59nU5hqPvsRCt+HBmT/yBeIn6Fmc=
+git.secluded.site/go-lunatask v0.1.0-rc10 h1:KKkYNs/cipNjIlRPXAvpPm5QcWSuA3REcG8XZ8sALk4=
+git.secluded.site/go-lunatask v0.1.0-rc10/go.mod h1:rxps7BBqF+BkY8VN5E7J9zSOzSbtZ1hDmLEOHxjTHZQ=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
@@ -0,0 +1,3 @@
+SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+
+SPDX-License-Identifier: CC0-1.0
@@ -1,92 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-// Package deeplink parses and builds Lunatask deep links.
-package deeplink
-
-import (
- "errors"
- "fmt"
- "strings"
-
- "github.com/google/uuid"
-)
-
-// Resource represents a Lunatask resource type that supports deep linking.
-type Resource string
-
-// Supported Lunatask resource types for deep linking.
-const (
- Area Resource = "areas"
- Goal Resource = "goals"
- Task Resource = "tasks"
- Note Resource = "notes"
- Person Resource = "people"
- Notebook Resource = "notebooks"
-)
-
-const (
- scheme = "lunatask://"
- deepLinkPartCount = 2
-)
-
-// ErrInvalidReference indicates the input is neither a valid UUID nor deep link.
-var ErrInvalidReference = errors.New("invalid reference: expected UUID or lunatask:// deep link")
-
-// ErrUnsupportedResource indicates the deep link resource type is not recognised.
-var ErrUnsupportedResource = errors.New("unsupported resource type in deep link")
-
-// ParseID extracts a UUID from either a raw UUID string or a Lunatask deep link.
-// Accepts formats:
-// - UUID: "3bbf1923-64ae-4bcf-96a9-9bb86c799dab"
-// - Deep link: "lunatask://areas/3bbf1923-64ae-4bcf-96a9-9bb86c799dab"
-func ParseID(input string) (string, error) {
- input = strings.TrimSpace(input)
-
- // Try parsing as UUID first
- if parsed, err := uuid.Parse(input); err == nil {
- return parsed.String(), nil
- }
-
- // Try parsing as deep link
- if !strings.HasPrefix(input, scheme) {
- return "", fmt.Errorf("%w: %s", ErrInvalidReference, input)
- }
-
- path := strings.TrimPrefix(input, scheme)
- parts := strings.Split(path, "/")
-
- if len(parts) != deepLinkPartCount {
- return "", fmt.Errorf("%w: %s", ErrInvalidReference, input)
- }
-
- resource := parts[0]
- id := parts[1]
-
- // Validate resource type
- switch Resource(resource) {
- case Area, Goal, Task, Note, Person, Notebook:
- // Valid resource
- default:
- return "", fmt.Errorf("%w: %s", ErrUnsupportedResource, resource)
- }
-
- // Validate and normalise the UUID
- parsed, err := uuid.Parse(id)
- if err != nil {
- return "", fmt.Errorf("%w: %s", ErrInvalidReference, input)
- }
-
- return parsed.String(), nil
-}
-
-// Build constructs a Lunatask deep link for the given resource and ID.
-func Build(resource Resource, id string) (string, error) {
- parsed, err := uuid.Parse(id)
- if err != nil {
- return "", fmt.Errorf("%w: %s", ErrInvalidReference, id)
- }
-
- return fmt.Sprintf("%s%s/%s", scheme, resource, parsed.String()), nil
-}
@@ -1,128 +0,0 @@
-// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
-//
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-package deeplink_test
-
-import (
- "errors"
- "testing"
-
- "git.secluded.site/lune/internal/deeplink"
-)
-
-func TestParseID_Valid(t *testing.T) {
- t.Parallel()
-
- const (
- uuidLower = "123e4567-e89b-12d3-a456-426614174000"
- uuidArea = "3bbf1923-64ae-4bcf-96a9-9bb86c799dab"
- uuidGoal = "9d79e922-9ca8-4b8c-9aa5-dd98bb2492b2"
- )
-
- tests := []struct {
- name string
- input string
- wantID string
- }{
- {"UUID lowercase", uuidLower, uuidLower},
- {"UUID uppercase", "123E4567-E89B-12D3-A456-426614174000", uuidLower},
- {"deep link areas", "lunatask://areas/" + uuidArea, uuidArea},
- {"deep link goals", "lunatask://goals/" + uuidGoal, uuidGoal},
- {"deep link tasks", "lunatask://tasks/" + uuidArea, uuidArea},
- {"deep link notes", "lunatask://notes/" + uuidArea, uuidArea},
- {"deep link people", "lunatask://people/" + uuidArea, uuidArea},
- {"deep link notebooks", "lunatask://notebooks/" + uuidArea, uuidArea},
- {"with whitespace", " " + uuidLower + " ", uuidLower},
- }
-
- for _, testCase := range tests {
- t.Run(testCase.name, func(t *testing.T) {
- t.Parallel()
-
- id, err := deeplink.ParseID(testCase.input)
- if err != nil {
- t.Errorf("ParseID(%q) error = %v, want nil", testCase.input, err)
- }
-
- if id != testCase.wantID {
- t.Errorf("ParseID(%q) = %q, want %q", testCase.input, id, testCase.wantID)
- }
- })
- }
-}
-
-func TestParseID_Invalid(t *testing.T) {
- t.Parallel()
-
- tests := []struct {
- name string
- input string
- wantErr error
- }{
- {"empty", "", deeplink.ErrInvalidReference},
- {"random text", "not-a-uuid", deeplink.ErrInvalidReference},
- {"invalid UUID", "123e4567-e89b-12d3-a456-42661417zzzz", deeplink.ErrInvalidReference},
- {"wrong scheme", "http://areas/123e4567-e89b-12d3-a456-426614174000", deeplink.ErrInvalidReference},
- {"invalid resource", "lunatask://habits/123e4567-e89b-12d3-a456-426614174000", deeplink.ErrUnsupportedResource},
- {"missing path", "lunatask://", deeplink.ErrInvalidReference},
- {"extra path", "lunatask://areas/foo/bar", deeplink.ErrInvalidReference},
- }
-
- for _, testCase := range tests {
- t.Run(testCase.name, func(t *testing.T) {
- t.Parallel()
-
- _, err := deeplink.ParseID(testCase.input)
- if !errors.Is(err, testCase.wantErr) {
- t.Errorf("ParseID(%q) error = %v, want %v", testCase.input, err, testCase.wantErr)
- }
- })
- }
-}
-
-func TestBuild(t *testing.T) {
- t.Parallel()
-
- const uuid = "3bbf1923-64ae-4bcf-96a9-9bb86c799dab"
-
- tests := []struct {
- name string
- resource deeplink.Resource
- id string
- want string
- }{
- {"area", deeplink.Area, uuid, "lunatask://areas/" + uuid},
- {"goal", deeplink.Goal, uuid, "lunatask://goals/" + uuid},
- {"task", deeplink.Task, uuid, "lunatask://tasks/" + uuid},
- {"note", deeplink.Note, uuid, "lunatask://notes/" + uuid},
- {"person", deeplink.Person, uuid, "lunatask://people/" + uuid},
- {"notebook", deeplink.Notebook, uuid, "lunatask://notebooks/" + uuid},
- }
-
- for _, testCase := range tests {
- t.Run(testCase.name, func(t *testing.T) {
- t.Parallel()
-
- got, err := deeplink.Build(testCase.resource, testCase.id)
- if err != nil {
- t.Errorf("Build(%v, %q) error = %v, want nil",
- testCase.resource, testCase.id, err)
- }
-
- if got != testCase.want {
- t.Errorf("Build(%v, %q) = %q, want %q",
- testCase.resource, testCase.id, got, testCase.want)
- }
- })
- }
-}
-
-func TestBuild_InvalidID(t *testing.T) {
- t.Parallel()
-
- _, err := deeplink.Build(deeplink.Area, "not-a-uuid")
- if !errors.Is(err, deeplink.ErrInvalidReference) {
- t.Errorf("Build(Area, \"not-a-uuid\") error = %v, want %v", err, deeplink.ErrInvalidReference)
- }
-}
@@ -8,35 +8,19 @@ package validate
import (
"errors"
"fmt"
- "strings"
"git.secluded.site/go-lunatask"
- "github.com/google/uuid"
-
- "git.secluded.site/lune/internal/deeplink"
)
-// ErrInvalidID indicates an ID is not a valid UUID.
-var ErrInvalidID = errors.New("invalid ID: expected UUID format")
-
// ErrInvalidReference indicates the input is not a valid UUID or deep link.
var ErrInvalidReference = errors.New("invalid reference: expected UUID or lunatask:// deep link")
-// ID validates that the given string is a valid Lunatask ID (UUID).
-func ID(id string) error {
- if _, err := uuid.Parse(id); err != nil {
- return fmt.Errorf("%w: %s", ErrInvalidID, id)
- }
-
- return nil
-}
-
// Reference parses a UUID or Lunatask deep link and returns the normalised UUID.
// Accepts formats:
// - UUID: "3bbf1923-64ae-4bcf-96a9-9bb86c799dab"
// - Deep link: "lunatask://areas/3bbf1923-64ae-4bcf-96a9-9bb86c799dab"
func Reference(input string) (string, error) {
- id, err := deeplink.ParseID(input)
+ _, id, err := lunatask.ParseDeepLink(input)
if err != nil {
return "", fmt.Errorf("%w: %s", ErrInvalidReference, input)
}
@@ -50,15 +34,12 @@ var ErrInvalidStatus = errors.New("invalid status")
// TaskStatus validates and normalizes a task status string.
// Returns the corresponding lunatask.TaskStatus or an error if invalid.
func TaskStatus(input string) (lunatask.TaskStatus, error) {
- status := lunatask.TaskStatus(strings.ToLower(input))
-
- switch status {
- case lunatask.StatusLater, lunatask.StatusNext, lunatask.StatusStarted,
- lunatask.StatusWaiting, lunatask.StatusCompleted:
- return status, nil
- default:
+ status, err := lunatask.ParseTaskStatus(input)
+ if err != nil {
return "", fmt.Errorf("%w: %s", ErrInvalidStatus, input)
}
+
+ return status, nil
}
// ErrInvalidMotivation indicates the motivation value is not recognized.
@@ -67,15 +48,12 @@ var ErrInvalidMotivation = errors.New("invalid motivation")
// Motivation validates and normalizes a motivation string.
// Returns the corresponding lunatask.Motivation or an error if invalid.
func Motivation(input string) (lunatask.Motivation, error) {
- motivation := lunatask.Motivation(strings.ToLower(input))
-
- switch motivation {
- case lunatask.MotivationUnknown, lunatask.MotivationMust,
- lunatask.MotivationShould, lunatask.MotivationWant:
- return motivation, nil
- default:
+ motivation, err := lunatask.ParseMotivation(input)
+ if err != nil {
return "", fmt.Errorf("%w: %s", ErrInvalidMotivation, input)
}
+
+ return motivation, nil
}
// ErrInvalidRelationship indicates the relationship strength is not recognized.
@@ -84,15 +62,10 @@ var ErrInvalidRelationship = errors.New("invalid relationship strength")
// RelationshipStrength validates and normalizes a relationship strength string.
// Returns the corresponding lunatask.RelationshipStrength or an error if invalid.
func RelationshipStrength(input string) (lunatask.RelationshipStrength, error) {
- rel := lunatask.RelationshipStrength(strings.ToLower(input))
-
- switch rel {
- case lunatask.RelationshipFamily, lunatask.RelationshipIntimateFriend,
- lunatask.RelationshipCloseFriend, lunatask.RelationshipCasualFriend,
- lunatask.RelationshipAcquaintance, lunatask.RelationshipBusiness,
- lunatask.RelationshipAlmostStranger:
- return rel, nil
- default:
+ rel, err := lunatask.ParseRelationshipStrength(input)
+ if err != nil {
return "", fmt.Errorf("%w: %s", ErrInvalidRelationship, input)
}
+
+ return rel, nil
}