32 Commits

Author SHA1 Message Date
Bel LaPointe
e913c9cd3c revert to clean todo 2022-03-24 09:26:44 -06:00
Bel LaPointe
02e4c95d31 revert automove for crons 2022-03-24 09:25:34 -06:00
Bel LaPointe
bbdd38fba7 always write ts, when a todo with schedule deleted, conditionally go back to schedule for loopers 2022-03-24 09:18:19 -06:00
Bel LaPointe
4bdee5d9a0 cli on delete todo, write in Done identified by entire struct so dupes wouldnt but thats ok 2022-03-23 09:46:16 -06:00
Bel LaPointe
7c84d7650c add todo.ID() 2022-03-23 09:42:21 -06:00
Bel LaPointe
29646808f9 ready for find deleted 2022-03-23 08:06:57 -06:00
Bel LaPointe
8276308d52 if item in todo is scheduled for later, then move it 2022-03-23 08:04:35 -06:00
Bel LaPointe
8c050d1d89 new edit ready for shuffles 2022-03-23 07:49:54 -06:00
Bel LaPointe
e8a83176aa getconfig in pttodo-cli 2022-03-23 07:27:49 -06:00
Bel LaPointe
7d98e6f0fc update tests for mst 2022-03-23 07:13:55 -06:00
bel
82a615919c wip 2022-02-20 17:11:16 -07:00
Bel LaPointe
50913b8913 change default file path from stdin 2022-01-10 09:44:30 -05:00
Bel LaPointe
42d532ee61 update todo 2022-01-09 10:27:30 -05:00
Bel LaPointe
24316fe690 accept THH on ezdate 2022-01-09 10:24:40 -05:00
Bel LaPointe
8058304219 dont log noops 2022-01-09 10:22:23 -05:00
Bel LaPointe
7f24b3f337 do not save if no effective changes for edit, dump 2022-01-09 10:18:08 -05:00
Bel LaPointe
fc12e0550d add root.equals and todo.equals 2022-01-09 10:17:44 -05:00
Bel LaPointe
05f5244cd1 root.yaml prio, otherwise sort -f results 2022-01-08 23:44:30 -05:00
Bel LaPointe
c559c8eba6 if cli -f is a dir, then edit each file+clean each file+keep each as individual file 2022-01-08 23:42:05 -05:00
Bel LaPointe
dd7ac8d786 parseinlocation for non utc crud 2022-01-06 21:28:44 -05:00
Bel LaPointe
ccdd2615c4 instead of trashing editing file in cli, write err msg and re-edit 2022-01-04 12:37:35 -05:00
Bel LaPointe
0241f2d76c err msg 2022-01-04 12:33:39 -05:00
Bel LaPointe
fa812a16ee fix checking hastags and nothas tags 2022-01-04 08:56:19 -05:00
Bel LaPointe
cf3b233a11 add anti tag search 2022-01-04 07:01:35 -05:00
Bel LaPointe
64868bdc0b create script to scrape list of lists and scrape aech into a yaml 2022-01-03 16:36:59 -05:00
Bel LaPointe
1b1724bea7 update todo with no more ts in todo section 2022-01-02 20:45:17 -05:00
Bel LaPointe
f4968d1d1b update todo 2022-01-02 20:44:23 -05:00
Bel LaPointe
6203ced79b root does not marshal TSs on todos, always on schedules, dones. Test. 2022-01-02 20:44:04 -05:00
Bel LaPointe
4d171d10d0 only write TS on a todo if writeTS is set 2022-01-02 20:37:40 -05:00
Bel LaPointe
8ad2ca9345 new todo 2022-01-02 20:32:16 -05:00
Bel LaPointe
bc0bb05fb6 update todo 2022-01-02 20:31:09 -05:00
Bel LaPointe
22dc04341c update todo.yaml 2022-01-02 20:17:32 -05:00
19 changed files with 696 additions and 138 deletions

View File

@@ -11,8 +11,10 @@ import (
"os"
"os/exec"
"path"
"sort"
"strings"
"syscall"
"time"
"gopkg.in/yaml.v2"
)
@@ -24,6 +26,16 @@ const (
DUMP_DONE = "done"
)
type config struct {
targets []string
mergeme string
root string
tags []string
search string
edit bool
dry bool
}
func main() {
if err := _main(); err != nil {
panic(err)
@@ -31,33 +43,45 @@ func main() {
}
func _main() error {
config := getConfig()
if config.mergeme != "" {
if err := merge(config.dry, config.targets[0], config.mergeme); err != nil {
return err
}
}
if config.edit {
if err := edit(config.dry, config.targets); err != nil {
return err
}
}
return dump(config.dry, os.Stdout, config.targets, config.tags, config.search, config.root)
}
func getConfig() config {
defaultFilepath, ok := os.LookupEnv("PTTODO_FILE")
if !ok {
defaultFilepath = "-"
defaultFilepath = "./todo.yaml"
}
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")
dry := flag.Bool("dry", false, "dry run")
var config config
var target string
flag.StringVar(&target, "f", defaultFilepath, "($PTTODO_FILE) path to yaml file or dir (starting with root then alphabetical for dir)")
flag.StringVar(&config.mergeme, "g", "", "path to yaml file to merge into -f (modifies f)")
flag.StringVar(&config.root, "root", DUMP_TODO, "path to pretty print ("+fmt.Sprint([]string{DUMP_ALL, DUMP_TODO, DUMP_SCHEDULED, DUMP_DONE})+")")
var tagss string
flag.StringVar(&tagss, "tags", "", "csv of all tags to find, -tag to invert")
flag.StringVar(&config.search, "search", "", "fts case insensitive")
flag.BoolVar(&config.edit, "e", false, "edit file")
flag.BoolVar(&config.dry, "dry", false, "dry run")
flag.Parse()
if *filepathToMergeIn != "" {
if err := merge(*dry, *filepath, *filepathToMergeIn); err != nil {
return err
}
config.tags = strings.Split(tagss, ",")
config.targets = []string{target}
if stat, err := os.Stat(target); err == nil && stat.IsDir() {
config.targets, _ = listDir(target)
}
if *e {
if err := edit(*dry, *filepath); err != nil {
return err
}
}
var tagslist []string
if *tags != "" {
tagslist = strings.Split(*tags, ",")
}
return dump(*dry, os.Stdout, *filepath, tagslist, *search, *root)
return config
}
func verifyRoot(root pttodo.Root) error {
@@ -79,13 +103,109 @@ func verifyRoot(root pttodo.Root) error {
}
func verifyFile(path string) error {
return dump(true, io.Discard, path, nil, "", DUMP_ALL)
if err := dump(true, io.Discard, []string{path}, nil, "", DUMP_ALL); err != nil {
return fmt.Errorf("failed verifying file %s: %w", path, err)
}
return nil
}
func edit(dry bool, filepath string) error {
var tempFile string
cp := func() error {
f, err := ioutil.TempFile(os.TempDir(), path.Base(filepath))
func edit(dry bool, filepaths []string) error {
tempDir, err := ioutil.TempDir(os.TempDir(), "edit-pttodo-*")
if err != nil {
return err
}
originals := map[string]pttodo.Root{}
for _, target := range filepaths {
var original pttodo.Root
if b, err := ioutil.ReadFile(target); err != nil && !os.IsNotExist(err) {
return err
} else if err := yaml.Unmarshal(b, &original); err != nil {
return err
} else if c, err := yaml.Marshal(original.Todo); err != nil {
return err
} else if err := ioutil.WriteFile(path.Join(tempDir, path.Base(target)), c, os.ModePerm); err != nil {
return err
}
originals[target] = original
}
if err := vimd(tempDir); err != nil {
return err
}
for _, target := range filepaths {
for {
err := func() error {
var todos []pttodo.Todo
if b, err := ioutil.ReadFile(path.Join(tempDir, path.Base(target))); err != nil {
return err
} else if err := yaml.Unmarshal(b, &todos); err != nil {
return err
}
return nil
}()
if err == nil {
break
}
log.Printf("%s, press <Enter> to resume editing", err)
b := make([]byte, 1)
if _, err := os.Stdin.Read(b); err != nil {
return err
}
if err := vimd(tempDir); err != nil {
return err
}
}
}
if dry {
return nil
}
for _, target := range filepaths {
b, err := ioutil.ReadFile(path.Join(tempDir, path.Base(target)))
if err != nil {
return err
}
var newTodos []pttodo.Todo
if err := yaml.Unmarshal(b, &newTodos); err != nil {
return err
}
original := originals[target]
newTodoIDs := map[string]struct{}{}
for i := range newTodos {
newTodoIDs[newTodos[i].ID()] = struct{}{}
}
for i := range original.Todo {
if _, ok := newTodoIDs[original.Todo[i].ID()]; !ok {
original.Todo[i].TS = pttodo.TS(time.Now().Unix())
if string(original.Todo[i].Schedule) != "" && !original.Todo[i].Triggered() {
original.Scheduled = append(original.Scheduled, original.Todo[i])
} else {
original.Done = append(original.Done, original.Todo[i])
}
}
}
original.Todo = newTodos
original.AutoMove()
c, err := yaml.Marshal(original)
if err != nil {
return err
}
if err := ioutil.WriteFile(target, c, os.ModePerm); err != nil {
return err
}
}
return nil
}
func _edit(dry bool, filepaths []string) error {
tempDir, err := ioutil.TempDir(os.TempDir(), "edit-pttodo")
if err != nil {
return err
}
cpOne := func(filepath string) error {
f, err := os.Create(path.Join(tempDir, path.Base(filepath)))
if err != nil {
return err
}
@@ -100,58 +220,74 @@ func edit(dry bool, filepath string) error {
g.Close()
}
f.Close()
tempFile = f.Name() + ".yaml"
return os.Rename(f.Name(), tempFile)
return nil
}
vi := func() error {
bin := "vim"
if editor := os.Getenv("EDITOR"); editor != "" {
bin = editor
}
editorbin, err := exec.LookPath(bin)
if err != nil {
editorbin, err = exec.LookPath("vi")
}
if err != nil {
return err
}
args := []string{editorbin, tempFile}
cpid, err := syscall.ForkExec(
editorbin,
args,
&syscall.ProcAttr{
Dir: "",
Env: os.Environ(),
Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()},
Sys: nil,
},
)
if err != nil {
return err
}
proc, err := os.FindProcess(cpid)
if err != nil {
return err
}
state, err := proc.Wait()
if err != nil {
return err
}
if exitCode := state.ExitCode(); exitCode != 0 {
return fmt.Errorf("bad exit code on vim: %d, state: %+v", exitCode, state)
cp := func() error {
for _, filepath := range filepaths {
if err := cpOne(filepath); err != nil {
return err
}
}
return nil
}
verify := func() error {
vi := func() error {
return vimd(tempDir)
}
verifyOne := func(tempFile string) error {
for {
err := verifyFile(tempFile)
if err == nil {
break
}
log.Printf("%v, press <Enter> to resume editing", err)
b := make([]byte, 1)
if _, err := os.Stdin.Read(b); err != nil {
break
}
if err := vi(); err != nil {
return err
}
}
return verifyFile(tempFile)
}
save := func() error {
verify := func() error {
for _, filepath := range filepaths {
tempFile := path.Join(tempDir, path.Base(filepath))
if err := verifyOne(tempFile); err != nil {
return err
}
}
return nil
}
saveOne := func(filepath string) error {
tempFile := path.Join(tempDir, path.Base(filepath))
var rootTemp, rootOld pttodo.Root
if a, err := ioutil.ReadFile(tempFile); err != nil {
return err
} else if err := yaml.Unmarshal(a, &rootTemp); err != nil {
return err
} else if b, err := ioutil.ReadFile(filepath); err != nil {
return err
} else if err := yaml.Unmarshal(b, &rootOld); err != nil {
return err
} else if rootTemp.Equals(rootOld) {
//log.Printf("no changes to %s", filepath)
return nil
}
if dry {
log.Printf("would've saved %s as %s", tempFile, filepath)
return nil
}
return os.Rename(tempFile, filepath)
}
save := func() error {
for _, filepath := range filepaths {
if err := saveOne(filepath); err != nil {
return err
}
}
return nil
}
for _, foo := range []func() error{cp, vi, verify, save} {
if err := foo(); err != nil {
@@ -159,12 +295,54 @@ func edit(dry bool, filepath string) error {
}
}
if !dry {
os.Remove(tempFile)
os.RemoveAll(tempDir)
}
return nil
}
func vimd(d string) error {
bin := "vim"
editorbin, err := exec.LookPath(bin)
if err != nil {
editorbin, err = exec.LookPath("vi")
}
if err != nil {
return err
}
args := []string{editorbin, "-p"}
files, err := listDir(d)
if err != nil {
return err
}
args = append(args, files...)
cpid, err := syscall.ForkExec(
editorbin,
args,
&syscall.ProcAttr{
Dir: d,
Env: os.Environ(),
Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()},
Sys: nil,
},
)
if err != nil {
return err
}
proc, err := os.FindProcess(cpid)
if err != nil {
return err
}
state, err := proc.Wait()
if err != nil {
return err
}
if exitCode := state.ExitCode(); exitCode != 0 {
return fmt.Errorf("bad exit code on vim: %d, state: %+v", exitCode, state)
}
return nil
}
func merge(dry bool, filepath string, mergeTargetFilePath string) error {
baseReader, err := filePathReader(filepath)
if err != nil {
@@ -224,21 +402,48 @@ func marshalRootToTempFile(root pttodo.Root) (string, error) {
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)
if err != nil {
return err
}
func dump(dry bool, writer io.Writer, filepaths []string, tags []string, search, rootDisplay string) error {
var root pttodo.Root
if err := yaml.Unmarshal(b, &root); err != nil {
return err
for _, filepath := range filepaths {
reader, err := filePathReader(filepath)
if err != nil {
return err
}
b, err := ioutil.ReadAll(reader)
if err != nil {
return err
}
var root2, root2post pttodo.Root
if err := yaml.Unmarshal(b, &root2); err != nil {
return err
}
if err := yaml.Unmarshal(b, &root2post); err != nil {
return err
}
root2.MoveScheduledToTodo()
if !dry {
if !root2.Equals(root2post) {
log.Printf("refreshing %s", filepath)
b3, err := yaml.Marshal(root2)
if err != nil {
return err
}
if err := os.WriteFile(filepath, b3, os.ModePerm); err != nil {
return err
}
} else {
//log.Printf("not refreshing %s", filepath)
}
}
root.MergeIn(root2)
}
root.MoveScheduledToTodo()
var v interface{} = root
@@ -255,11 +460,14 @@ func dump(dry bool, writer io.Writer, filepath string, tags []string, search, ro
if len(tags) > 0 {
result := make([]pttodo.Todo, 0, len(todos))
for _, todo := range todos {
want := len(todo.Tags) > 0
skip := false
for _, tag := range tags {
want = want && strings.Contains(todo.Tags, tag)
positiveTag := strings.TrimLeft(tag, "-")
hasTag := strings.Contains(todo.Tags, positiveTag)
wantToHaveTag := !strings.HasPrefix(tag, "-")
skip = skip || !(hasTag == wantToHaveTag)
}
if want {
if !skip {
result = append(result, todo)
}
}
@@ -276,21 +484,13 @@ func dump(dry bool, writer io.Writer, filepath string, tags []string, search, ro
}
v = todos
}
b2, err := yaml.Marshal(v)
if err != nil {
return err
}
fmt.Fprintf(writer, "%s\n", b2)
if dry {
return nil
}
b3, err := yaml.Marshal(root)
if err != nil {
return err
}
return os.WriteFile(filepath, b3, os.ModePerm)
return nil
}
func filePathReader(path string) (io.Reader, error) {
@@ -306,3 +506,24 @@ func filePathReader(path string) (io.Reader, error) {
}
return reader, nil
}
func listDir(dname string) ([]string, error) {
entries, err := os.ReadDir(dname)
if err != nil {
return nil, err
}
paths := make([]string, 0, len(entries))
for i := range entries {
if entries[i].IsDir() {
continue
}
paths = append(paths, path.Join(dname, entries[i].Name()))
}
sort.Slice(paths, func(i, j int) bool {
if path.Base(paths[i]) == "root.yaml" {
return true
}
return paths[i] < paths[j]
})
return paths, nil
}

View File

@@ -0,0 +1,29 @@
package main
import (
"fmt"
"io/ioutil"
"os"
"path"
"strconv"
"testing"
)
func TestListDir(t *testing.T) {
d := t.TempDir()
want := []string{}
for i := 0; i < 3; i++ {
p := path.Join(d, strconv.Itoa(i))
ioutil.WriteFile(p, []byte{}, os.ModePerm)
want = append(want, p)
}
os.Mkdir(path.Join(d, "d"), os.ModePerm)
got, err := listDir(d)
if err != nil {
t.Fatal(err)
}
if fmt.Sprint(want) != fmt.Sprint(got) {
t.Fatal(want, got)
}
}

4
cmd/pttodo-cli/testdata/1.yaml vendored Normal file
View File

@@ -0,0 +1,4 @@
todo:
- "1"
scheduled: []
done: []

4
cmd/pttodo-cli/testdata/2.yaml vendored Normal file
View File

@@ -0,0 +1,4 @@
todo:
- "2"
scheduled: []
done: []

4
cmd/pttodo-cli/testdata/3.yaml vendored Normal file
View File

@@ -0,0 +1,4 @@
todo:
- "3"
scheduled: []
done: []

4
cmd/pttodo-cli/testdata/root.yaml vendored Normal file
View File

@@ -0,0 +1,4 @@
todo:
- root
scheduled: []
done: []

45
cmd/pttodo-cli/testdata/test.yaml vendored Executable file
View File

@@ -0,0 +1,45 @@
todo:
- todo: b
ts: Thu Mar 24 09:03:24 MDT 2022
scheduled:
- todo: due
schedule: "2022-05-01"
ts: Thu Mar 24 09:16:21 MDT 2022
- todo: loop
schedule: 10s
ts: Thu Mar 24 09:17:11 MDT 2022
done:
- todo: abc
schedule: "2022-05-01"
ts: Wed Mar 23 08:03:45 MDT 2022
- todo: def
schedule: "2022-05-01"
ts: Wed Mar 23 08:04:05 MDT 2022
- todo: other
ts: Wed Mar 23 09:44:57 MDT 2022
- todo: b
ts: Wed Mar 23 09:45:13 MDT 2022
- todo: a
ts: Wed Mar 23 09:45:15 MDT 2022
- todo: a
ts: Wed Mar 23 09:45:24 MDT 2022
- todo: a
ts: Wed Mar 23 09:45:31 MDT 2022
- todo: looper
schedule: 15s
ts: Thu Mar 24 09:01:41 MDT 2022
- todo: looper
schedule: 15s
ts: Thu Mar 24 09:04:51 MDT 2022
- todo: loop
schedule: 10s
ts: Thu Mar 24 09:06:59 MDT 2022
- todo: due
schedule: "2022-02-01"
ts: Thu Mar 24 09:15:42 MDT 2022
- todo: due
schedule: "2022-02-01"
ts: Thu Mar 24 09:16:53 MDT 2022
- todo: due
schedule: "2022-03-01"
ts: Thu Mar 24 09:17:07 MDT 2022

View File

@@ -0,0 +1,21 @@
#! /bin/bash
main() {
cd "$(dirname "$BASH_SOURCE")"
source ./from_todo_server_to_pttodo.sh
type from_todo_server_to_pttodo_main
list_lists | jq .list[] | jq -c . | while read -r line; do
local name="$(echo "$line" | jq -r .name | sed 's/^Today$/todo/g' | tr '[:upper:]' '[:lower:]' | sed 's/ /-/g')"
local id="$(echo "$line" | jq -r .id)"
echo $name, $id
TODO_SERVER_LIST="$id" from_todo_server_to_pttodo_main > ./$name.yaml
done
}
list_lists() {
todo_server_curl "$TODO_SERVER_URL/ajax.php?loadLists=&rnd=0.9900282499544026"
}
if [ "$0" == "$BASH_SOURCE" ]; then
main "$@"
fi

View File

@@ -1,10 +1,14 @@
#! /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"}"
export TODO_SERVER_URL="${TODO_SERVER_URL:-"https://todo-server.remote.blapointe.com"}"
export TODO_SERVER_HEADERS="${TODO_SERVER_HEADERS:-"Cookie: BOAuthZ=$TODO_SERVER_BOAUTHZ"}"
export TODO_SERVER_LIST="${TODO_SERVER_LIST:-"2548023766"}"
main() {
from_todo_server_to_pttodo_main "$@"
}
from_todo_server_to_pttodo_main() {
set -e
set -o pipefail
local tasks_in_todo="$(fetch_tasks_in_list | format_tasks_in_list)"
@@ -28,6 +32,10 @@ format_tasks_in_list() {
}
fetch_tasks_in_list() {
todo_server_curl "$TODO_SERVER_URL/ajax.php?loadTasks=&list=$TODO_SERVER_LIST&compl=${COMPL:-0}&looping=${LOOPING:-0}"
}
todo_server_curl() {
local csv_headers="$TODO_SERVER_HEADERS"
local headers=()
while [ "$csv_headers" != "" ]; do
@@ -39,7 +47,7 @@ fetch_tasks_in_list() {
csv_headers=""
fi
done
curl -sS "${headers[@]}" "$TODO_SERVER_URL/ajax.php?loadTasks=&list=$TODO_SERVER_LIST&compl=${COMPL:-0}&looping=${LOOPING:-0}"
curl -sS "${headers[@]}" "$@"
}
if [ "$0" == "$BASH_SOURCE" ]; then

8
go.mod
View File

@@ -6,3 +6,11 @@ require (
github.com/robfig/cron/v3 v3.0.1
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/bytbox/go-pop3 v0.0.0-20120201222208-3046caf0763e // indirect
github.com/emersion/go-imap v1.2.0 // indirect
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
golang.org/x/text v0.3.7 // indirect
)

12
go.sum
View File

@@ -1,5 +1,17 @@
github.com/bytbox/go-pop3 v0.0.0-20120201222208-3046caf0763e h1:mQTN05gz0rDZSABqKMzAPMb5ATWcvvdMljRzEh0LjBo=
github.com/bytbox/go-pop3 v0.0.0-20120201222208-3046caf0763e/go.mod h1:alXX+s7a4cKaIprgjeEboqi4Tm7XR/HXEwUTxUV/ywU=
github.com/emersion/go-imap v1.2.0 h1:lyUQ3+EVM21/qbWE/4Ya5UG9r5+usDxlg4yfp3TgHFA=
github.com/emersion/go-imap v1.2.0/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=

View File

@@ -6,6 +6,37 @@ type Root struct {
Done []Todo
}
func (root Root) Equals(other Root) bool {
for i, slice := range [][2][]Todo{
[2][]Todo{root.Todo, other.Todo},
[2][]Todo{root.Scheduled, other.Scheduled},
[2][]Todo{root.Done, other.Done},
} {
_ = i
if !equalTodoSlices(slice[0], slice[1]) {
return false
}
}
return true
}
func (root *Root) AutoMove() {
root.MoveScheduledToTodo()
//root.MoveTodoToScheduled()
}
func (root *Root) MoveTodoToScheduled() {
for i := len(root.Todo) - 1; i >= 0; i-- {
if root.Todo[i].Schedule != "" && !root.Todo[i].Triggered() {
root.Scheduled = append(root.Scheduled, root.Todo[i])
for j := i; j < len(root.Todo)-1; j++ {
root.Todo[j] = root.Todo[j+1]
}
root.Todo = root.Todo[:len(root.Todo)-1]
}
}
}
func (root *Root) MoveScheduledToTodo() {
for i := len(root.Scheduled) - 1; i >= 0; i-- {
if root.Scheduled[i].Triggered() {
@@ -36,3 +67,17 @@ func (root *Root) MergeIn(root2 Root) {
}
}
}
func (root Root) MarshalYAML() (interface{}, error) {
for i := range root.Todo {
root.Todo[i].writeTS = false
}
for i := range root.Scheduled {
root.Scheduled[i].writeTS = true
}
for i := range root.Done {
root.Done[i].writeTS = true
}
type Alt Root
return (Alt)(root), nil
}

View File

@@ -1,9 +1,11 @@
package pttodo
import (
"bytes"
"encoding/json"
"fmt"
"strconv"
"strings"
"testing"
"time"
@@ -23,6 +25,8 @@ func TestJSONRoot(t *testing.T) {
t.Fatal(err)
} else if fmt.Sprint(root) != fmt.Sprint(root2) {
t.Fatal(root, root2)
} else if !root.Equals(root2) {
t.Fatal(root2)
}
}
@@ -94,6 +98,8 @@ func TestRootMoveScheduledToTodo(t *testing.T) {
got.MoveScheduledToTodo()
if fmt.Sprintf("%+v", got) != fmt.Sprintf("%+v", want) {
t.Fatalf("want \n\t%+v, got \n\t%+v", want, got)
} else if !got.Equals(want) {
t.Fatalf("want \n\t%+v, got \n\t%+v", want, got)
}
})
}
@@ -143,5 +149,34 @@ todo:
root0.MergeIn(root1)
if fmt.Sprintf("%+v", root0) != fmt.Sprintf("%+v", rootWant) {
t.Fatalf("want \n\t%+v, got \n\t%+v", rootWant, root0)
} else if !root0.Equals(rootWant) {
t.Fatalf("want \n\t%+v, got \n\t%+v", rootWant, root0)
}
}
func TestRootMarshalYAMLWriteTS(t *testing.T) {
root := Root{
Todo: []Todo{Todo{Todo: "todo", TS: 1, writeTS: true}},
Scheduled: []Todo{Todo{Todo: "sched", TS: 2, writeTS: false, Schedule: "2099-01-01"}},
Done: []Todo{Todo{Todo: "done", TS: 3, writeTS: false}},
}
got, err := yaml.Marshal(root)
if err != nil {
t.Fatal(err)
}
got = bytes.TrimSpace(got)
want := strings.TrimSpace(`
todo:
- todo
scheduled:
- todo: sched
schedule: "2099-01-01"
ts: Wed Dec 31 17:00:02 MST 1969
done:
- todo: done
ts: Wed Dec 31 17:00:03 MST 1969
`)
if string(got) != want {
t.Fatalf("want\n\t%q, got\n\t%q", want, string(got))
}
}

View File

@@ -94,8 +94,11 @@ func (due scheduleDue) next(time.Time) (time.Time, error) {
// 2022-01-01
type scheduleEZDate string
var scheduleEZDatePattern = regexp.MustCompile(`^[0-9]{4}-[0-9]{2}-[0-9]{2}$`)
var scheduleEZDatePattern = regexp.MustCompile(`^[0-9]{4}-[0-9]{2}-[0-9]{2}(T[0-9]{2})?$`)
func (ezdate scheduleEZDate) next(time.Time) (time.Time, error) {
return time.Parse("2006-01-02", string(ezdate))
if len(ezdate) == len("20xx-xx-xxTxx") {
return time.ParseInLocation("2006-01-02T15", string(ezdate), time.Local)
}
return time.ParseInLocation("2006-01-02", string(ezdate), time.Local)
}

View File

@@ -51,11 +51,17 @@ func TestJSONSchedule(t *testing.T) {
func TestSchedulerFactory(t *testing.T) {
start := time.Date(2000, 1, 1, 1, 1, 1, 1, time.UTC)
someDay := time.Date(2001, 2, 3, 0, 0, 0, 0, time.UTC)
someHour := time.Date(2001, 2, 3, 15, 0, 0, 0, time.UTC)
cases := map[string]struct {
input string
want interface{}
next time.Time
}{
"ezdate with hour": {
input: `2000-01-03T15`,
want: scheduleEZDate(`2000-01-03T15`),
next: someHour,
},
"long dur": {
input: `2h1m`,
want: scheduleDuration("2h1m"),

View File

@@ -1,17 +1,32 @@
package pttodo
import (
"encoding/base64"
"fmt"
"hash/crc32"
"time"
)
type Todo struct {
Todo string
TS TS
Details string `yaml:",omitempty"`
Schedule Schedule `yaml:",omitempty"`
Tags string `yaml:",omitempty"`
Subtasks []Todo `yaml:",omitempty"`
TS TS `yaml:",omitempty"`
writeTS bool
}
func (todo Todo) ID() string {
hash := crc32.NewIEEE()
fmt.Fprintf(hash, "%d:%s", 0, todo.Todo)
fmt.Fprintf(hash, "%d:%s", 1, todo.Details)
fmt.Fprintf(hash, "%d:%s", 2, todo.Schedule)
fmt.Fprintf(hash, "%d:%s", 3, todo.Tags)
for i := range todo.Subtasks {
fmt.Fprintf(hash, "%d:%s", 4, todo.Subtasks[i].ID())
}
return base64.StdEncoding.EncodeToString(hash.Sum(nil))
}
func (todo Todo) Triggered() bool {
@@ -21,6 +36,11 @@ func (todo Todo) Triggered() bool {
}
func (todo Todo) MarshalYAML() (interface{}, error) {
if !todo.writeTS {
todo.TS = 0
} else {
todo.TS = TS(todo.TS.time().Unix())
}
if fmt.Sprint(todo) == fmt.Sprint(Todo{Todo: todo.Todo}) {
return todo.Todo, nil
}
@@ -37,3 +57,37 @@ func (todo *Todo) UnmarshalYAML(unmarshal func(interface{}) error) error {
alt := (*Alt)(todo)
return unmarshal(alt)
}
func (todo Todo) Equals(other Todo) bool {
if !equalTodoSlices(todo.Subtasks, other.Subtasks) {
return false
}
if todo.TS != other.TS {
return false
}
if todo.Tags != other.Tags {
return false
}
if todo.Schedule != other.Schedule {
return false
}
if todo.Details != other.Details {
return false
}
if todo.Todo != other.Todo {
return false
}
return true
}
func equalTodoSlices(a, b []Todo) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if !a[i].Equals(b[i]) {
return false
}
}
return true
}

View File

@@ -125,3 +125,33 @@ func TestMarshalTodo(t *testing.T) {
}
})
}
func TestTodoID(t *testing.T) {
cases := map[string]Todo{
"empty": Todo{},
"todo": Todo{Todo: "abc"},
"details": Todo{Details: "abc"},
"todo,details": Todo{Todo: "abc", Details: "abc"},
}
got := map[string]bool{}
for name, todod := range cases {
todo := todod
t.Run(name, func(t *testing.T) {
if _, ok := got[todo.ID()]; ok {
t.Error("dupe", todo.ID())
}
got[todo.ID()] = true
t.Logf("%s: %+v", todo.ID(), todo)
todo2 := todo
todo2.TS = 1
if todo.ID() != todo2.ID() {
t.Error("ts changed")
}
todo2.writeTS = true
if todo.ID() != todo2.ID() {
t.Error("writets changed")
}
})
}
}

View File

@@ -44,7 +44,7 @@ func (ts *TS) UnmarshalYAML(unmarshaller func(interface{}) error) error {
}
var s string
if err := unmarshaller(&s); err == nil {
t, err := time.Parse(time.UnixDate, s)
t, err := time.ParseInLocation(time.UnixDate, s, time.Local)
*ts = TS(t.Unix())
return err
}

View File

@@ -1,18 +1,12 @@
todo:
- merge multi todo files for 'inbox' style with some dedupe
- when merging, check for complete/incomplete and cross reference todo vs scheduled
vs done
- 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
modify only file on remote
ts: Fri Dec 31 22:33:12 EST 2021
details: |
- 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
tags: stuffToTry,secondTag
scheduled: []
done:
- xactions emails extend ledger,todo-server with pttodo - schedule merge inbox style
- xactions emails extend ledger,todo-server with pttodo
- todo: ez edit on many platforms, even offline and mobile
ts: Fri Dec 31 22:33:12 EST 2021
details: |
mobile view + complete method
collab editing of file prob resolves mobile and other stuff...
@@ -20,38 +14,69 @@ todo:
web server access to ops
is a web ui for mobile best solution?
let git be smart-ish and keep latest? would provide versioning OOTB without touching raw
scheduled: []
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
- 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
here is my really here is my really here is my really here is my really here is
my really here is my really long string
- vim doesnt source vimrc, so stuff like tab width and tab characters, also syntax
highlight :(
- crontab -e style editing to ensure good syntax
- todo: when to run scheduled modifier? like, syncthing could have conflicts if I
modify only file on remote
details: |
- 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
tags: stuffToTry,secondTag
- 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
- merge multi todo files for 'inbox' style with some dedupe (probably best to just
do this via calling pttodo-cli from script or somethin) (probably at an off hour
so no collisions from live editing)
- how to defer a scheduled task?
- todo: add tag anti-search, like -tag=-notMe
ts: Tue Jan 4 06:56:03 EST 2022
- todo: if in todo, then omit ts field
ts: Sun Jan 2 20:44:27 EST 2022
- todo: merge full files to import from todo-server
ts: Sun Jan 2 20:44:27 EST 2022
- todo: more schedule formats, like just 2022-01-15, for deferring
ts: Sun Jan 2 20:44:27 EST 2022
- todo: from todo-server to pttodo format
ts: Sun Jan 2 20:44:27 EST 2022
- todo: add -tag to search via cli
ts: Sun Jan 2 20:44:27 EST 2022
- todo: ts to human readable
ts: Sun Jan 2 20:44:27 EST 2022
- todo: 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 here is my really here is my really here is my really here is my really
here is my really here is my really long string
ts: Sun Jan 2 20:44:27 EST 2022
- todo: vim doesnt source vimrc, so stuff like tab width and tab characters, also
syntax highlight :(
ts: Sun Jan 2 20:44:27 EST 2022
- todo: crontab -e style editing to ensure good syntax
ts: Sun Jan 2 20:44:27 EST 2022
- todo: YAML based todo
ts: Fri Dec 31 22:33:12 EST 2021
details: because goddamnit a year of this shit isn't significant on disk or in RAM
for vim
ts: Fri Dec 31 22:33:12 EST 2021
- todo: yaml based todo for plaintext
ts: Fri Dec 31 22:33:12 EST 2021
details: a year isnt even a mb
- ez edit, start on many platforms
- defer
- schedule
- looping
- details
- let UI be UI for whatever platform
- tags
- todo: sub tasks
ts: Fri Dec 31 22:33:12 EST 2021
- todo: ez edit, start on many platforms
ts: Sun Jan 2 20:44:27 EST 2022
- todo: defer
ts: Sun Jan 2 20:44:27 EST 2022
- todo: schedule
ts: Sun Jan 2 20:44:27 EST 2022
- todo: looping
ts: Sun Jan 2 20:44:27 EST 2022
- todo: details
ts: Sun Jan 2 20:44:27 EST 2022
- todo: let UI be UI for whatever platform
ts: Sun Jan 2 20:44:27 EST 2022
- todo: tags
ts: Sun Jan 2 20:44:27 EST 2022
- todo: sub tasks
subtasks:
- a
ts: Fri Dec 31 22:33:12 EST 2021
- todo: crap losing on a bad edit hurts
ts: Fri Dec 31 22:37:58 EST 2021
details: |
?
ts: Fri Dec 31 22:37:58 EST 2021