1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
|
package store
import (
"encoding/json"
"fmt"
"sort"
"strings"
"time"
)
// TockStore implements Store by shelling out to the tock CLI.
type TockStore struct{ R Runner }
// New returns a TockStore over the given Runner.
func New(r Runner) *TockStore { return &TockStore{R: r} }
type rawEntry struct {
Description string `json:"description"`
Project string `json:"project"`
StartTime string `json:"start_time"`
EndTime string `json:"end_time"`
Tags []string `json:"tags"`
Notes string `json:"notes"`
Duration string `json:"duration"`
}
func parseTime(s string) (time.Time, error) {
if strings.TrimSpace(s) == "" {
return time.Time{}, nil
}
return time.Parse(time.RFC3339Nano, s)
}
func parseDur(s string) time.Duration {
var h, m, sec int
fmt.Sscanf(strings.TrimSpace(s), "%d:%d:%d", &h, &m, &sec)
return time.Duration(h)*time.Hour + time.Duration(m)*time.Minute + time.Duration(sec)*time.Second
}
// Day returns a day's entries sorted by start, each stamped with its tock Key.
func (t *TockStore) Day(date string) ([]Entry, error) {
out, _ := t.R.Run("tock", "report", "--date", date, "--json")
if len(strings.TrimSpace(string(out))) == 0 {
return nil, nil // tock prints nothing on an empty day
}
var raws []rawEntry
if err := json.Unmarshal(out, &raws); err != nil {
return nil, fmt.Errorf("parse tock report: %w", err)
}
entries := make([]Entry, 0, len(raws))
for _, r := range raws {
st, err := parseTime(r.StartTime)
if err != nil {
return nil, fmt.Errorf("parse start_time %q: %w", r.StartTime, err)
}
et, err := parseTime(r.EndTime)
if err != nil {
return nil, fmt.Errorf("parse end_time %q: %w", r.EndTime, err)
}
entries = append(entries, Entry{
Start: st, End: et, Project: r.Project, Description: r.Description,
Tags: r.Tags, Notes: r.Notes, Duration: parseDur(r.Duration),
})
}
sort.SliceStable(entries, func(i, j int) bool {
return entries[i].Start.Before(entries[j].Start)
})
for i := range entries {
entries[i].Key = fmt.Sprintf("%s-%02d", date, i+1)
}
return entries, nil
}
func fmtStamp(t time.Time) string {
return t.Format("2006-01-02 15:04")
}
// Add creates a tock entry.
func (t *TockStore) Add(e Entry) error {
args := []string{"add", "-p", e.Project, "-d", e.Description,
"-s", fmtStamp(e.Start), "-e", fmtStamp(e.End)}
for _, tag := range e.Tags {
if tag = strings.TrimSpace(tag); tag != "" {
args = append(args, "--tag", tag)
}
}
if strings.TrimSpace(e.Notes) != "" {
args = append(args, "--note", e.Notes)
}
_, err := t.R.Run("tock", args...)
return err
}
// Remove deletes the entry with the given date-index Key.
func (t *TockStore) Remove(key string) error {
_, err := t.R.Run("tock", "remove", key, "-y")
return err
}
// SafeReplace removes then re-adds an entry. If the replacement Add fails it
// restores the original, so an edit can never silently delete an entry.
func (t *TockStore) SafeReplace(key string, updated, original Entry) error {
if err := t.Remove(key); err != nil {
return fmt.Errorf("remove %s: %w", key, err)
}
if err := t.Add(updated); err != nil {
if rerr := t.Add(original); rerr != nil {
return fmt.Errorf("add failed (%v) AND restore failed (%v); entry %s may be lost", err, rerr, key)
}
return fmt.Errorf("edit rejected, original restored: %w", err)
}
return nil
}
|