Skip to content

Commit d17657e

Browse files
authored
feat(dates): natural-language due dates (v0.2.0) (#1)
* feat(dateparse): natural-language date parser (today, tomorrow, weekdays) Signed-off-by: Sanjay Santhanam <[email protected]> * feat(dateparse): relative offsets (in 3d, 2w, 1m) Signed-off-by: Sanjay Santhanam <[email protected]> * feat(dateparse): month names (jul 4, july 4 2027) Signed-off-by: Sanjay Santhanam <[email protected]> * feat(dateparse): aliases (eow, eom, next week, next month) Signed-off-by: Sanjay Santhanam <[email protected]> * feat(cmd): wire dateparse into add/edit Signed-off-by: Sanjay Santhanam <[email protected]> * feat(tui): wire dateparse into inline due field Signed-off-by: Sanjay Santhanam <[email protected]> * docs: date shortcuts in README Signed-off-by: Sanjay Santhanam <[email protected]> * chore: bump version to 0.2.0 Signed-off-by: Sanjay Santhanam <[email protected]> --------- Signed-off-by: Sanjay Santhanam <[email protected]>
1 parent 1d43997 commit d17657e

9 files changed

Lines changed: 820 additions & 6 deletions

File tree

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ brew install Sanjays2402/tap/tsk # coming soon
3838

3939
```
4040
tsk init # create .tsk.md in cwd
41-
tsk add "Buy milk" -p high -d 2026-04-25 -t errand -t home
41+
tsk add "Buy milk" -p high -d tomorrow -t errand -t home
4242
tsk ls # undone by default
4343
tsk ls --all --tag errand
4444
tsk done 1
@@ -48,6 +48,18 @@ tsk export --json > tasks.json
4848
tsk # launch the TUI
4949
```
5050

51+
### Dates
52+
53+
`-d/--due` (and the TUI `D` key) accept natural language as well as `YYYY-MM-DD`:
54+
55+
- `today`, `tomorrow`, `tmrw`, `yesterday`
56+
- Weekdays: `mon`..`sun` / `monday`..`sunday` — next occurrence
57+
- Relative: `3d`, `2w`, `1m`, `in 3d`, `in 2 weeks`
58+
- Months: `jul 4`, `july 4 2027`, `4 jul`, `dec`
59+
- Aliases: `next week`, `next month`, `next mon`, `eow`, `eom`
60+
61+
All dates resolve in `America/Los_Angeles`. Unknown inputs exit with code 2 and a hint.
62+
5163
### TUI keys
5264

5365
| Key | Action |
@@ -57,6 +69,7 @@ tsk # launch the TUI
5769
| `a` | Add task (inline form) |
5870
| `e` | Edit title |
5971
| `t` | Edit tags |
72+
| `D` | Set due date (natural lang) |
6073
| `p` | Cycle priority |
6174
| `d` | Delete (`y` to confirm) |
6275
| `/` | Fuzzy filter |

cmd/tsk/main.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
package main
33

44
import (
5+
"errors"
56
"fmt"
67
"os"
78

@@ -10,7 +11,7 @@ import (
1011
)
1112

1213
var (
13-
version = "dev"
14+
version = "0.2.0"
1415
commit = "none"
1516
date = "unknown"
1617
)
@@ -20,6 +21,10 @@ func main() {
2021
commands.SetTUI(tui.Run)
2122
if err := commands.NewRoot().Execute(); err != nil {
2223
fmt.Fprintln(os.Stderr, "error:", err)
24+
var ec commands.ExitCoder
25+
if errors.As(err, &ec) {
26+
os.Exit(ec.ExitCode())
27+
}
2328
os.Exit(1)
2429
}
2530
}

internal/commands/add.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"strings"
66
"time"
77

8+
"github.com/Sanjays2402/tsk/internal/dateparse"
89
"github.com/Sanjays2402/tsk/internal/model"
910
"github.com/spf13/cobra"
1011
)
@@ -37,9 +38,10 @@ func newAddCmd() *cobra.Command {
3738
Created: time.Now(),
3839
}
3940
if dueStr != "" {
40-
t, err := time.ParseInLocation(model.DateLayout, dueStr, time.Local)
41+
loc := PacificLoc()
42+
t, err := dateparse.Parse(dueStr, time.Now().In(loc), loc)
4143
if err != nil {
42-
return fmt.Errorf("invalid --due (want YYYY-MM-DD): %w", err)
44+
return usageErrorf("%s", err.Error())
4345
}
4446
task.Due = &t
4547
}
@@ -56,7 +58,7 @@ func newAddCmd() *cobra.Command {
5658
},
5759
}
5860
cmd.Flags().StringVarP(&priorityStr, "priority", "p", "medium", "priority (low|medium|high|urgent)")
59-
cmd.Flags().StringVarP(&dueStr, "due", "d", "", "due date (YYYY-MM-DD)")
61+
cmd.Flags().StringVarP(&dueStr, "due", "d", "", "due date (YYYY-MM-DD, or tomorrow/fri/in 3d/jul 4/eow/...)")
6062
cmd.Flags().StringArrayVarP(&tags, "tag", "t", nil, "tag (repeatable)")
6163
cmd.Flags().StringVarP(&notes, "notes", "n", "", "freeform notes")
6264
return cmd

internal/commands/helpers.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,54 @@ import (
44
"fmt"
55
"io"
66
"os"
7+
"sync"
8+
"time"
79

810
"github.com/Sanjays2402/tsk/internal/store"
911
"github.com/spf13/cobra"
1012
)
1113

14+
// ExitCoder is an error that carries a preferred process exit code. Main uses
15+
// it to exit with 2 on user-input errors (e.g. bad --due), distinguishing them
16+
// from unexpected failures that exit 1.
17+
type ExitCoder interface {
18+
error
19+
ExitCode() int
20+
}
21+
22+
type exitErr struct {
23+
msg string
24+
code int
25+
}
26+
27+
func (e *exitErr) Error() string { return e.msg }
28+
29+
func (e *exitErr) ExitCode() int { return e.code }
30+
31+
// usageErrorf returns an error that should exit with code 2.
32+
func usageErrorf(format string, args ...any) error {
33+
return &exitErr{msg: fmt.Sprintf(format, args...), code: 2}
34+
}
35+
36+
var (
37+
locOnce sync.Once
38+
locVal *time.Location
39+
)
40+
41+
// PacificLoc returns the cached America/Los_Angeles location, falling back to
42+
// time.Local if the zoneinfo database is unavailable (rare but possible on
43+
// stripped containers).
44+
func PacificLoc() *time.Location {
45+
locOnce.Do(func() {
46+
if l, err := time.LoadLocation("America/Los_Angeles"); err == nil {
47+
locVal = l
48+
return
49+
}
50+
locVal = time.Local
51+
})
52+
return locVal
53+
}
54+
1255
// pf is a tolerant Fprintf: errors writing to user-facing output are ignored.
1356
func pf(w io.Writer, format string, args ...any) {
1457
_, _ = fmt.Fprintf(w, format, args...)

0 commit comments

Comments
 (0)