12 Commits

Author SHA1 Message Date
Bel LaPointe
bf10c00708 add cli -g to merge in a file 2022-01-02 20:07:52 -05:00
Bel LaPointe
cd5ce0f0df add root MergeIn 2022-01-01 17:43:20 -05:00
Bel LaPointe
eb41f065a5 todo 2022-01-01 17:33:59 -05:00
Bel LaPointe
96bfb96ee3 consts for what to print, default to just the todos 2022-01-01 17:30:40 -05:00
Bel LaPointe
770f2719d2 ezdate for yyyy-mm-dd for schedule 2022-01-01 17:21:38 -05:00
Bel LaPointe
3554bf8ba4 rm test code 2022-01-01 17:14:03 -05:00
Bel LaPointe
b4aa4ad310 ts shouldnt yield zero ever, yield now if so 2022-01-01 17:13:50 -05:00
Bel LaPointe
0dddf26265 fix getting ts for completed tasks 2022-01-01 17:04:21 -05:00
Bel LaPointe
5afdeed3b5 todo 2021-12-31 23:27:21 -05:00
Bel LaPointe
af0f094a65 support tag, simple case insensitve search when recursing 2021-12-31 23:20:27 -05:00
Bel LaPointe
031db8788b tag saerch on todo 2021-12-31 23:14:31 -05:00
Bel LaPointe
b8efdbfa52 when writing output file, dont recurse 2021-12-31 23:03:08 -05:00
8 changed files with 315 additions and 28 deletions

View File

@@ -11,11 +11,19 @@ import (
"os" "os"
"os/exec" "os/exec"
"path" "path"
"strings"
"syscall" "syscall"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
const (
DUMP_ALL = "all"
DUMP_TODO = "todo"
DUMP_SCHEDULED = "scheduled"
DUMP_DONE = "done"
)
func main() { func main() {
if err := _main(); err != nil { if err := _main(); err != nil {
panic(err) panic(err)
@@ -28,15 +36,50 @@ func _main() error {
defaultFilepath = "-" defaultFilepath = "-"
} }
filepath := flag.String("f", defaultFilepath, "($PTTODO_FILE) path to yaml file") filepath := flag.String("f", defaultFilepath, "($PTTODO_FILE) path to yaml file")
filepathToMergeIn := flag.String("g", "", "path to yaml file to merge into -f")
root := flag.String("root", DUMP_TODO, "path to pretty print ("+fmt.Sprint([]string{DUMP_ALL, DUMP_TODO, DUMP_SCHEDULED, DUMP_DONE})+")")
tags := flag.String("tags", "", "csv of all tags to find")
search := flag.String("search", "", "fts case insensitive")
e := flag.Bool("e", false, "edit file") e := flag.Bool("e", false, "edit file")
dry := flag.Bool("dry", false, "dry run") dry := flag.Bool("dry", false, "dry run")
flag.Parse() flag.Parse()
if *filepathToMergeIn != "" {
if err := merge(*dry, *filepath, *filepathToMergeIn); err != nil {
return err
}
}
if *e { if *e {
if err := edit(*dry, *filepath); err != nil { if err := edit(*dry, *filepath); err != nil {
return err return err
} }
} }
return dump(*dry, os.Stdout, *filepath) var tagslist []string
if *tags != "" {
tagslist = strings.Split(*tags, ",")
}
return dump(*dry, os.Stdout, *filepath, tagslist, *search, *root)
}
func verifyRoot(root pttodo.Root) error {
f, err := ioutil.TempFile(os.TempDir(), "tmp")
if err != nil {
return err
}
f.Close()
tempFile := f.Name()
b, err := yaml.Marshal(root)
if err != nil {
return err
}
if err := ioutil.WriteFile(tempFile, b, os.ModePerm); err != nil {
return err
}
defer os.Remove(tempFile)
return verifyFile(tempFile)
}
func verifyFile(path string) error {
return dump(true, io.Discard, path, nil, "", DUMP_ALL)
} }
func edit(dry bool, filepath string) error { func edit(dry bool, filepath string) error {
@@ -100,10 +143,7 @@ func edit(dry bool, filepath string) error {
return nil return nil
} }
verify := func() error { verify := func() error {
if err := dump(true, io.Discard, tempFile); err != nil { return verifyFile(tempFile)
return fmt.Errorf("failed to verify %s: %v", tempFile, err)
}
return nil
} }
save := func() error { save := func() error {
if dry { if dry {
@@ -125,16 +165,69 @@ func edit(dry bool, filepath string) error {
return nil return nil
} }
func dump(dry bool, writer io.Writer, filepath string) error { func merge(dry bool, filepath string, mergeTargetFilePath string) error {
var reader io.Reader baseReader, err := filePathReader(filepath)
if filepath == "-" { if err != nil {
reader = os.Stdin return err
} else { }
b, err := ioutil.ReadFile(filepath) baseB, err := ioutil.ReadAll(baseReader)
if err != nil { if err != nil {
return err return err
} }
reader = bytes.NewReader(b)
mergingReader, err := filePathReader(mergeTargetFilePath)
if err != nil {
return err
}
mergingB, err := ioutil.ReadAll(mergingReader)
if err != nil {
return err
}
var base, merging pttodo.Root
if err := yaml.Unmarshal(baseB, &base); err != nil {
return err
}
if err := yaml.Unmarshal(mergingB, &merging); err != nil {
return err
}
base.MergeIn(merging)
if err := verifyRoot(base); err != nil {
return err
}
tmppath, err := marshalRootToTempFile(base)
if err != nil {
return err
}
if dry {
log.Printf("would've moved %s to %s when adding %s", tmppath, filepath, mergeTargetFilePath)
return nil
}
return os.Rename(tmppath, filepath)
}
func marshalRootToTempFile(root pttodo.Root) (string, error) {
f, err := ioutil.TempFile(os.TempDir(), "tmp")
if err != nil {
return "", err
}
f.Close()
os.Remove(f.Name())
b, err := yaml.Marshal(root)
if err != nil {
return "", err
}
filepath := f.Name() + ".yaml"
err = ioutil.WriteFile(filepath, b, os.ModePerm)
return filepath, err
}
func dump(dry bool, writer io.Writer, filepath string, tags []string, search, rootDisplay string) error {
reader, err := filePathReader(filepath)
if err != nil {
return err
} }
b, err := ioutil.ReadAll(reader) b, err := ioutil.ReadAll(reader)
@@ -149,15 +242,40 @@ func dump(dry bool, writer io.Writer, filepath string) error {
root.MoveScheduledToTodo() root.MoveScheduledToTodo()
var v interface{} = root var v interface{} = root
switch flag.Arg(0) { switch rootDisplay {
case "": case DUMP_ALL:
case "todo": case DUMP_TODO:
v = root.Todo v = root.Todo
case "scheduled": case DUMP_SCHEDULED:
v = root.Scheduled v = root.Scheduled
case "done": case DUMP_DONE:
v = root.Done v = root.Done
} }
if todos, ok := v.([]pttodo.Todo); ok {
if len(tags) > 0 {
result := make([]pttodo.Todo, 0, len(todos))
for _, todo := range todos {
want := len(todo.Tags) > 0
for _, tag := range tags {
want = want && strings.Contains(todo.Tags, tag)
}
if want {
result = append(result, todo)
}
}
todos = result
}
if len(search) > 0 {
result := make([]pttodo.Todo, 0, len(todos))
for _, todo := range todos {
if strings.Contains(strings.ToLower(fmt.Sprint(todo)), strings.ToLower(search)) {
result = append(result, todo)
}
}
todos = result
}
v = todos
}
b2, err := yaml.Marshal(v) b2, err := yaml.Marshal(v)
if err != nil { if err != nil {
return err return err
@@ -168,5 +286,23 @@ func dump(dry bool, writer io.Writer, filepath string) error {
return nil return nil
} }
return os.WriteFile(filepath, b2, os.ModePerm) b3, err := yaml.Marshal(root)
if err != nil {
return err
}
return os.WriteFile(filepath, b3, os.ModePerm)
}
func filePathReader(path string) (io.Reader, error) {
var reader io.Reader
if path == "-" {
reader = os.Stdin
} else {
b, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
reader = bytes.NewReader(b)
}
return reader, nil
} }

View File

@@ -0,0 +1,47 @@
#! /bin/bash
TODO_SERVER_URL="${TODO_SERVER_URL:-"https://todo-server.remote.blapointe.com"}"
TODO_SERVER_HEADERS="${TODO_SERVER_HEADERS:-"Cookie: BOAuthZ=$TODO_SERVER_BOAUTHZ"}"
TODO_SERVER_LIST="${TODO_SERVER_LIST:-"2548023766"}"
main() {
set -e
set -o pipefail
local tasks_in_todo="$(fetch_tasks_in_list | format_tasks_in_list)"
local schedule_tasks_in_flight="$(COMPL=1 LOOPING=1 fetch_tasks_in_list | format_tasks_in_list)"
echo "{\"todo\": $tasks_in_todo, \"scheduled\": $schedule_tasks_in_flight, \"done\": []}" | yq -P eval -
}
format_tasks_in_list() {
jq -c .list[] | while read -r line; do
echo "$line" \
| jq '{
todo: .title,
details:.note,
ts: (if .compl == 1 then (.dateCompleted | strptime("%d %b %Y %I:%M %p") | mktime) else .dateEditedInt end),
subtasks: [],
tags: .tags,
schedule: (if (.cron != "") then (.cron) else (.loop) end)
}' \
| jq -c .
done | jq -sc | yq -P eval - | grep -v -E ' (""|\[]|0s)$' | yq -j eval - | jq -c .
}
fetch_tasks_in_list() {
local csv_headers="$TODO_SERVER_HEADERS"
local headers=()
while [ "$csv_headers" != "" ]; do
header="${csv_headers%%,*}"
headers+=("-H" "${header%%:*}: ${header#*:}")
if echo "$csv_headers" | grep -q ,; then
csv_headers="${csv_headers#*,}"
else
csv_headers=""
fi
done
curl -sS "${headers[@]}" "$TODO_SERVER_URL/ajax.php?loadTasks=&list=$TODO_SERVER_LIST&compl=${COMPL:-0}&looping=${LOOPING:-0}"
}
if [ "$0" == "$BASH_SOURCE" ]; then
main "$@"
fi

View File

@@ -18,3 +18,21 @@ func (root *Root) MoveScheduledToTodo() {
} }
} }
} }
func (root *Root) MergeIn(root2 Root) {
for _, listPair := range [][2]*[]Todo{
[2]*[]Todo{&root.Todo, &root2.Todo},
[2]*[]Todo{&root.Scheduled, &root2.Scheduled},
[2]*[]Todo{&root.Done, &root2.Done},
} {
for _, candidate := range *listPair[1] {
found := false
for i := range *listPair[0] {
found = found || ((*listPair[0])[i].Todo == candidate.Todo)
}
if !found {
*listPair[0] = append(*listPair[0], candidate)
}
}
}
}

View File

@@ -6,6 +6,8 @@ import (
"strconv" "strconv"
"testing" "testing"
"time" "time"
yaml "gopkg.in/yaml.v2"
) )
func TestJSONRoot(t *testing.T) { func TestJSONRoot(t *testing.T) {
@@ -96,3 +98,50 @@ func TestRootMoveScheduledToTodo(t *testing.T) {
}) })
} }
} }
func TestMergeRoots(t *testing.T) {
root0yaml := `
todo:
- a
- b
- todo: c
- todo: d
tags: a
- exclusive to 0
`
root1yaml := `
todo:
- a
- b
- todo: c
- todo: d
tags: b
- exclusive to 1
`
rootWantyaml := `
todo:
- a
- b
- todo: c
- todo: d
tags: a
- exclusive to 0
- exclusive to 1
`
var root0, root1, rootWant Root
if err := yaml.Unmarshal([]byte(root0yaml), &root0); err != nil {
t.Fatal(err)
}
if err := yaml.Unmarshal([]byte(root1yaml), &root1); err != nil {
t.Fatal(err)
}
if err := yaml.Unmarshal([]byte(rootWantyaml), &rootWant); err != nil {
t.Fatal(err)
}
root0.MergeIn(root1)
if fmt.Sprintf("%+v", root0) != fmt.Sprintf("%+v", rootWant) {
t.Fatalf("want \n\t%+v, got \n\t%+v", rootWant, root0)
}
}

View File

@@ -37,6 +37,8 @@ func schedulerFactory(s string) scheduler {
} else if scheduleDuePattern.MatchString(s) { } else if scheduleDuePattern.MatchString(s) {
n, _ := strconv.Atoi(s) n, _ := strconv.Atoi(s)
return scheduleDue(n) return scheduleDue(n)
} else if scheduleEZDatePattern.MatchString(s) {
return scheduleEZDate(s)
} }
return scheduleCron(s) return scheduleCron(s)
} }
@@ -88,3 +90,12 @@ var scheduleDuePattern = regexp.MustCompile(`^[0-9]+$`)
func (due scheduleDue) next(time.Time) (time.Time, error) { func (due scheduleDue) next(time.Time) (time.Time, error) {
return TS(due).time(), nil return TS(due).time(), nil
} }
// 2022-01-01
type scheduleEZDate string
var scheduleEZDatePattern = regexp.MustCompile(`^[0-9]{4}-[0-9]{2}-[0-9]{2}$`)
func (ezdate scheduleEZDate) next(time.Time) (time.Time, error) {
return time.Parse("2006-01-02", string(ezdate))
}

View File

@@ -50,6 +50,7 @@ func TestJSONSchedule(t *testing.T) {
func TestSchedulerFactory(t *testing.T) { func TestSchedulerFactory(t *testing.T) {
start := time.Date(2000, 1, 1, 1, 1, 1, 1, time.UTC) start := time.Date(2000, 1, 1, 1, 1, 1, 1, time.UTC)
someDay := time.Date(2001, 2, 3, 0, 0, 0, 0, time.UTC)
cases := map[string]struct { cases := map[string]struct {
input string input string
want interface{} want interface{}
@@ -70,11 +71,6 @@ func TestSchedulerFactory(t *testing.T) {
want: scheduleDue(1), want: scheduleDue(1),
next: time.Unix(1, 0), next: time.Unix(1, 0),
}, },
"zero ts": {
input: `0`,
want: scheduleDue(0),
next: time.Unix(0, 0),
},
"never": { "never": {
input: ``, input: ``,
want: scheduleNever{}, want: scheduleNever{},
@@ -90,6 +86,11 @@ func TestSchedulerFactory(t *testing.T) {
want: scheduleCron(`5 * * * *`), want: scheduleCron(`5 * * * *`),
next: start.Add(time.Duration(-1*start.Nanosecond() + -1*start.Minute() + -1*start.Second())).Add(5 * time.Minute), next: start.Add(time.Duration(-1*start.Nanosecond() + -1*start.Minute() + -1*start.Second())).Add(5 * time.Minute),
}, },
"ezdate": {
input: `2000-01-03`,
want: scheduleEZDate(`2000-01-03`),
next: someDay,
},
} }
for name, d := range cases { for name, d := range cases {
@@ -108,4 +109,18 @@ func TestSchedulerFactory(t *testing.T) {
} }
}) })
} }
t.Run("zero ts", func(t *testing.T) {
got := schedulerFactory("0")
if fmt.Sprintf("%T", scheduleDue(0)) != fmt.Sprintf("%T", got) {
t.Fatalf("want type %T, got %T", scheduleDue(0), got)
}
if fmt.Sprint(scheduleDue(0)) != fmt.Sprint(got) {
t.Fatalf("want %+v, got %+v", scheduleDue(0), got)
}
next, _ := got.next(start)
if now := time.Now(); next.Sub(now) > time.Second {
t.Fatalf("want next %+v, got %+v", now, next)
}
})
} }

View File

@@ -11,6 +11,9 @@ import (
type TS int64 type TS int64
func (ts TS) time() time.Time { func (ts TS) time() time.Time {
if ts == 0 {
ts = TS(time.Now().Unix())
}
return time.Unix(int64(ts), 0) return time.Unix(int64(ts), 0)
} }
@@ -26,8 +29,7 @@ func (ts TS) MarshalYAML() (interface{}, error) {
if ts == 0 { if ts == 0 {
ts = TS(time.Now().Unix()) ts = TS(time.Now().Unix())
} }
t := time.Unix(int64(ts), 0) return ts.time().Format(time.UnixDate), nil
return t.Format(time.UnixDate), nil
} }
func (ts *TS) UnmarshalJSON(b []byte) error { func (ts *TS) UnmarshalJSON(b []byte) error {

View File

@@ -1,10 +1,16 @@
todo: todo:
- merge multi todo files for 'inbox' style with some dedupe
- click on links when dumped via cli
- what do about todo-now vs todo vs watch vs later... could do many files and manage
outside binary, but search would suck. Good would be vendor lockin, and that's UI problem
- add tag anti-search, like -tag=-notMe
- todo: when to run scheduled modifier? like, syncthing could have conflicts if I - todo: when to run scheduled modifier? like, syncthing could have conflicts if I
modify only file on remote modify only file on remote
ts: Fri Dec 31 22:33:12 EST 2021 ts: Fri Dec 31 22:33:12 EST 2021
details: | details: |
- if it's a web ui hook or somethin, then it'd only have file conflict if I modify without waiting - if it's a web ui hook or somethin, then it'd only have file conflict if I modify without waiting
- but thats a step back from current todo solution - but thats a step back from current todo solution
tags: stuffToTry,secondTag
- todo: ez edit on many platforms, even offline and mobile - todo: ez edit on many platforms, even offline and mobile
ts: Fri Dec 31 22:33:12 EST 2021 ts: Fri Dec 31 22:33:12 EST 2021
details: | details: |
@@ -16,6 +22,9 @@ todo:
let git be smart-ish and keep latest? would provide versioning OOTB without touching raw let git be smart-ish and keep latest? would provide versioning OOTB without touching raw
scheduled: [] scheduled: []
done: done:
- more schedule formats, like just 2022-01-15, for deferring
- from todo-server to pttodo format
- add -tag to search via cli
- ts to human readable - ts to human readable
- here is my really here is my really here is my really here is my really here is - here is my really here is my really here is my really here is my really here is
my really here is my really here is my really here is my really here is my really my really here is my really here is my really here is my really here is my really