120 Commits

Author SHA1 Message Date
bel
ad9c32c987 testdata 2024-05-06 21:18:20 -06:00
bel
3ee1dc069e stub rust? 2024-05-06 21:00:31 -06:00
Bel LaPointe
9b482d45b4 bucket inboxes by target for excessive automoving 2023-11-14 18:36:00 -07:00
Bel LaPointe
ba81940b0b k 2023-11-09 08:19:27 -07:00
Bel LaPointe
5d74b458d5 again 2023-11-09 08:19:14 -07:00
Bel LaPointe
f3ac2c63fe pkg from gogs 2023-11-09 08:18:00 -07:00
Bel LaPointe
a3ca9b1917 more manual testing is all k 2023-11-09 07:51:42 -07:00
Bel LaPointe
ca84bcbee3 inboxes can only merge to Todo because tempfiles only have Todo 2023-11-09 07:50:53 -07:00
Bel LaPointe
89a8f61fa3 cache 2023-11-07 07:20:14 -07:00
Bel LaPointe
d3f34709b3 consistent 2023-11-07 06:11:30 -07:00
Bel LaPointe
ea904ea729 if no original file, then edit merges with nil and calls itself an inbox 2023-11-07 06:10:36 -07:00
bel
ead31e077b edit doesnt excess delete but keeps to many files like inboxes 2023-11-06 22:19:39 -07:00
bel
36c5228a6c oop 2023-11-06 22:07:38 -07:00
bel
72fee73a00 dont yield dirs 2023-11-06 21:39:57 -07:00
bel
550bace88d manual test an inbox 2023-11-06 21:29:33 -07:00
bel
0329b9690c untested refacotor of edit.go 2023-11-06 21:25:30 -07:00
bel
fe8595cc68 comment 2023-11-06 20:58:03 -07:00
bel
3549010ab4 wipper on edit.go 2023-11-06 20:49:08 -07:00
Bel LaPointe
caa9f9d6fd progressing 2023-11-06 13:29:56 -07:00
Bel LaPointe
8b63254cf0 progressing 2023-11-06 13:29:21 -07:00
Bel LaPointe
97f2cb197f wip 2023-11-06 13:23:28 -07:00
Bel LaPointe
b2a51e65b0 time for edit.go 2023-11-06 13:15:01 -07:00
Bel LaPointe
f75dcb3f7a test touched a local file oop 2023-11-06 13:14:20 -07:00
Bel LaPointe
d4466598b4 drop test file 2023-11-06 13:13:42 -07:00
Bel LaPointe
3ba3b78afc use default for effectively default 2023-11-06 13:12:09 -07:00
Bel LaPointe
f2e3e6f505 rm extra mem 2023-11-06 13:11:40 -07:00
Bel LaPointe
1ce120a647 shuffle code 2023-11-06 13:11:22 -07:00
Bel LaPointe
bfdeebb7a2 use pttodo.Todos.Like...() 2023-11-06 13:06:11 -07:00
Bel LaPointe
39345e5e2a use pttodo.NewRootFromFiles in cli 2023-11-06 12:58:46 -07:00
Bel LaPointe
bfcec9d1c5 add pttodo.NewRootFromFiles 2023-11-06 12:58:06 -07:00
Bel LaPointe
3e1f58c7b9 test pttodo.NewRootFromFile accepts both []Todo and Root 2023-11-06 12:54:46 -07:00
Bel LaPointe
92e8e14c08 bubble root.yaml to first 2023-11-06 12:44:31 -07:00
Bel LaPointe
84aa7cb319 test config resolving targets 2023-11-06 12:38:11 -07:00
Bel LaPointe
d36f9e74b3 bake listing files not cached in config 2023-11-06 12:25:52 -07:00
Bel LaPointe
8dacd8da5d ptr 2023-11-06 12:17:46 -07:00
Bel LaPointe
23ef7f13ee gitignore bin 2023-11-06 12:15:29 -07:00
Bel LaPointe
02dab4b726 move pttodo-cli up from /cmd/ 2023-11-06 12:15:12 -07:00
Bel LaPointe
cdd1be46a8 drop unused 2023-11-06 12:14:37 -07:00
Bel LaPointe
760c822323 add pttodo.NewRootFromFile, pttodo.NewTodosFromFile 2023-11-06 12:14:27 -07:00
Bel LaPointe
4e69646e88 drop dumb verifies 2023-11-06 12:14:16 -07:00
Bel LaPointe
9aee322c4e drop old 2023-11-06 12:11:44 -07:00
Bel LaPointe
887875f6d8 add creates a .todo.* file 2023-11-06 12:03:03 -07:00
Bel LaPointe
d2f986d8b6 split config.go 2023-11-06 10:37:34 -07:00
Bel LaPointe
58aa05c155 split dump.go from main.go in cli 2023-11-06 10:35:01 -07:00
Bel LaPointe
97aff0f0b8 split cli.go into add.go, edit.go too 2023-11-06 10:33:32 -07:00
Bel LaPointe
eb20706d12 drop some args 2023-11-06 10:28:36 -07:00
bel
a800227c6f oop 2023-10-15 10:40:13 -06:00
bel
2c7563c1ab default file path if not empty 2023-10-15 10:39:58 -06:00
bel
a8a135bb2f fix nil ptr on no previous file 2023-10-15 10:39:01 -06:00
Bel LaPointe
c1c625afc0 skip hidden files 2023-08-16 06:54:21 -06:00
Bel LaPointe
0b790e3468 nil ptr 2023-07-16 19:47:11 -06:00
Bel LaPointe
7175d777cb on write conflict during vim, save as filename.uuid..fileExtension 2023-07-02 10:22:04 -06:00
Bel LaPointe
debe28dbbc support -add-tags x,y,z 2023-04-24 15:02:13 -06:00
Bel LaPointe
c1426566a0 fix 2023-04-10 10:45:16 -06:00
Bel LaPointe
6f79f7da5e gr 2023-04-10 10:44:41 -06:00
Bel LaPointe
284d7c06bd partial go mod 2023-04-10 10:42:11 -06:00
Bel LaPointe
be702b1d74 gitignore 2022-09-10 11:25:23 -06:00
Bel LaPointe
de62d99340 no prior file exists ok 2022-09-10 11:24:41 -06:00
Bel LaPointe
bca9259caa on scheduled task done, in addition to requeue, add to done 2022-09-10 11:13:18 -06:00
Bel LaPointe
dccf5c4028 quick add via cli -add, -add-schedule 2022-04-07 12:48:17 -06:00
Bel LaPointe
55e174e3b1 root moves fixed future schedules to shceduled on automove 2022-03-24 14:43:45 -06:00
Bel LaPointe
d7dab75f48 revert to clean todo 2022-03-24 09:26:44 -06:00
Bel LaPointe
3426deae4d revert automove for crons 2022-03-24 09:25:34 -06:00
Bel LaPointe
7fe4686c05 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
eb57593665 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
814ae3ab23 add todo.ID() 2022-03-23 09:42:21 -06:00
Bel LaPointe
939793bd3f ready for find deleted 2022-03-23 08:06:57 -06:00
Bel LaPointe
1d26cf125f if item in todo is scheduled for later, then move it 2022-03-23 08:04:35 -06:00
Bel LaPointe
e20ce478d5 new edit ready for shuffles 2022-03-23 07:49:54 -06:00
Bel LaPointe
0bd6347a93 getconfig in pttodo-cli 2022-03-23 07:27:49 -06:00
Bel LaPointe
20770ff5e6 update tests for mst 2022-03-23 07:13:55 -06:00
bel
65178b8bdf wip 2022-02-20 17:11:16 -07:00
Bel LaPointe
4066d4aeb5 change default file path from stdin 2022-01-10 09:44:30 -05:00
Bel LaPointe
7f611e67bc update todo 2022-01-09 10:27:30 -05:00
Bel LaPointe
71c03f3ef5 accept THH on ezdate 2022-01-09 10:24:40 -05:00
Bel LaPointe
4b8d82c2a0 dont log noops 2022-01-09 10:22:23 -05:00
Bel LaPointe
850ff92d98 do not save if no effective changes for edit, dump 2022-01-09 10:18:08 -05:00
Bel LaPointe
c2d1381607 add root.equals and todo.equals 2022-01-09 10:17:44 -05:00
Bel LaPointe
33ca7c60e1 root.yaml prio, otherwise sort -f results 2022-01-08 23:44:30 -05:00
Bel LaPointe
cde6ea6cb6 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
6d3f423845 parseinlocation for non utc crud 2022-01-06 21:28:44 -05:00
Bel LaPointe
fe1bd22987 instead of trashing editing file in cli, write err msg and re-edit 2022-01-04 12:37:35 -05:00
Bel LaPointe
6f9589b100 err msg 2022-01-04 12:33:39 -05:00
Bel LaPointe
a0c0cb7053 fix checking hastags and nothas tags 2022-01-04 08:56:19 -05:00
Bel LaPointe
4a22b964db add anti tag search 2022-01-04 07:01:35 -05:00
Bel LaPointe
2caf2ae352 create script to scrape list of lists and scrape aech into a yaml 2022-01-03 16:36:59 -05:00
Bel LaPointe
455a7d52d5 update todo with no more ts in todo section 2022-01-02 20:45:17 -05:00
Bel LaPointe
1a9221f7c7 update todo 2022-01-02 20:44:23 -05:00
Bel LaPointe
6178e6ff93 root does not marshal TSs on todos, always on schedules, dones. Test. 2022-01-02 20:44:04 -05:00
Bel LaPointe
fdb24fcc60 only write TS on a todo if writeTS is set 2022-01-02 20:37:40 -05:00
Bel LaPointe
8002b5e75c new todo 2022-01-02 20:32:16 -05:00
Bel LaPointe
3dd8cd1e03 update todo 2022-01-02 20:31:09 -05:00
Bel LaPointe
c6ab36806d update todo.yaml 2022-01-02 20:17:32 -05:00
Bel LaPointe
bf997c1814 add cli -g to merge in a file 2022-01-02 20:07:52 -05:00
Bel LaPointe
69eb868db6 add root MergeIn 2022-01-01 17:43:20 -05:00
Bel LaPointe
0c80162394 todo 2022-01-01 17:33:59 -05:00
Bel LaPointe
c634fdd4d4 consts for what to print, default to just the todos 2022-01-01 17:30:40 -05:00
Bel LaPointe
92d76443bc ezdate for yyyy-mm-dd for schedule 2022-01-01 17:21:38 -05:00
Bel LaPointe
a51d5e6960 rm test code 2022-01-01 17:14:03 -05:00
Bel LaPointe
7fc594d5c2 ts shouldnt yield zero ever, yield now if so 2022-01-01 17:13:50 -05:00
Bel LaPointe
2d8cfa6397 fix getting ts for completed tasks 2022-01-01 17:04:21 -05:00
Bel LaPointe
f01dc04277 todo 2021-12-31 23:27:21 -05:00
Bel LaPointe
249ee84688 support tag, simple case insensitve search when recursing 2021-12-31 23:20:27 -05:00
Bel LaPointe
4111d1f490 tag saerch on todo 2021-12-31 23:14:31 -05:00
Bel LaPointe
5b6f62983b when writing output file, dont recurse 2021-12-31 23:03:08 -05:00
Bel LaPointe
1056f5d29e update todo 2021-12-31 23:00:16 -05:00
Bel LaPointe
7886723fe3 times are now unix dates over ints 2021-12-31 22:59:13 -05:00
Bel LaPointe
ff77af9ed4 remove unused stub 2021-12-31 22:49:51 -05:00
Bel LaPointe
3bd1c6462d fix syntax highlight by using tempfile.yaml over tempfile 2021-12-31 22:46:49 -05:00
Bel LaPointe
3bcabde553 dont try to find vimrc 2021-12-31 22:45:29 -05:00
Bel LaPointe
604cd610a1 use $EDITOR, default to vim, use $HOME/.vimrc if exists 2021-12-31 22:44:33 -05:00
Bel LaPointe
9d35347b0c updated todo because i oofed 2021-12-31 22:37:47 -05:00
Bel LaPointe
1a8c687260 update install for gomo 2021-12-31 22:32:47 -05:00
Bel LaPointe
3246900db0 if optional positional arg is todo/scheduled/done, then resolve first level 2021-12-31 22:32:07 -05:00
Bel LaPointe
79cc171af5 up install script for location 2021-12-31 22:28:12 -05:00
Bel LaPointe
a75d898487 set ts to currenttime if should display 2021-12-31 22:27:48 -05:00
Bel LaPointe
f63f152b0e cli has dry mode and install script 2021-12-31 22:27:33 -05:00
Bel LaPointe
76a0231511 pttodo-cli default file via env 2021-12-31 22:07:16 -05:00
Bel LaPointe
7f37feea77 pttodo-cli works even if file does not initially exist 2021-12-31 22:05:47 -05:00
Bel LaPointe
b84f8b59c9 rename gomod, root project 2021-12-31 21:58:02 -05:00
35 changed files with 893 additions and 681 deletions

2
.gitignore vendored
View File

@@ -4,3 +4,5 @@ cmd/cmd
cmd/cli
cmd/pttodo/pttodo
cmd/pttodo-cli/pttodo-cli
cmd/pttodo-cli
pttodoer/target

36
cmd/add.go Normal file
View File

@@ -0,0 +1,36 @@
package main
import (
"fmt"
"io/ioutil"
"os"
"github.com/google/uuid"
"gitea.inhome.blapointe.com/gogs/pttodo/pttodo"
"gopkg.in/yaml.v2"
)
func add(config *config) error {
if config.add == "" {
return nil
}
v := pttodo.Todo{
Todo: config.add,
Schedule: pttodo.Schedule(config.addSchedule),
Tags: config.addTags,
}
return _add(config.Targets()[0], v)
}
func _add(filepath string, todo pttodo.Todo) error {
target := fmt.Sprintf("%s.todo.%s", filepath, uuid.New().String())
c, err := yaml.Marshal([]pttodo.Todo{todo})
if err != nil {
return err
} else if err := ioutil.WriteFile(target, c, os.ModePerm); err != nil {
return err
}
return nil
}

98
cmd/config.go Normal file
View File

@@ -0,0 +1,98 @@
package main
import (
"flag"
"fmt"
"os"
"path"
"path/filepath"
"sort"
"strings"
)
type config struct {
target string
root string
tags []string
search string
edit bool
add string
addSchedule string
addTags string
}
func getConfig() config {
defaultFilepath := os.Getenv("PTTODO_FILE")
if defaultFilepath == "" {
defaultFilepath = "./todo.yaml"
}
var config config
flag.StringVar(&config.target, "f", defaultFilepath, "($PTTODO_FILE) path to yaml file or dir (starting with root then alphabetical for dir)")
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, -x to invert")
flag.StringVar(&config.search, "search", "", "fts case insensitive")
flag.BoolVar(&config.edit, "e", false, "edit file")
flag.StringVar(&config.add, "add", "", "todo to add")
flag.StringVar(&config.addSchedule, "add-schedule", "", "todo to add schedule")
flag.StringVar(&config.addTags, "add-tags", "", "todo to add csv tags")
flag.Parse()
config.tags = strings.Split(tagss, ",")
config.Targets()
return config
}
func (config config) Targets() []string {
result, err := config.targets()
if err != nil {
panic(err)
}
sort.Strings(result)
for i := range result {
if path.Base(result[i]) == "root.yaml" {
newresult := append([]string{result[i]}, result[:i]...)
newresult = append(newresult, result[i+1:]...)
result = newresult
break
}
}
return result
}
func (config config) targets() ([]string, error) {
patterns := []string{config.target, fmt.Sprintf("%s.*", config.target)}
isDir := false
if stat, err := os.Stat(config.target); err == nil {
isDir = stat.IsDir()
}
if isDir {
patterns = []string{fmt.Sprintf("%s/*", config.target)}
}
result := make([]string, 0, 1)
for _, pattern := range patterns {
results, err := filepath.Glob(pattern)
if err != nil {
return nil, err
}
for i := range results {
if stat, err := os.Stat(results[i]); err != nil {
return nil, err
} else if !stat.IsDir() {
result = append(result, results[i])
}
}
}
if len(result) == 0 {
if isDir {
return []string{path.Join(config.target, "root.yaml")}, nil
}
return []string{config.target}, nil
}
return result, nil
}

87
cmd/config_test.go Normal file
View File

@@ -0,0 +1,87 @@
package main
import (
"os"
"path"
"testing"
)
func TestConfigTargets(t *testing.T) {
touch := func(t *testing.T, p string) {
if err := os.WriteFile(p, nil, os.ModePerm); err != nil {
t.Fatal(err)
}
}
cases := map[string]struct {
setup func(*testing.T, string)
given string
want []string
}{
"does not exist": {
given: "x",
want: []string{"x"},
},
"one file": {
setup: func(t *testing.T, d string) {
touch(t, path.Join(d, "x"))
},
given: "x",
want: []string{"x"},
},
"two files": {
setup: func(t *testing.T, d string) {
touch(t, path.Join(d, "a"))
touch(t, path.Join(d, "root.yaml"))
touch(t, path.Join(d, "z"))
},
given: "",
want: []string{"root.yaml", "a", "z"},
},
"empty dir": {
setup: func(t *testing.T, d string) {
os.Mkdir(path.Join(d, "x"), os.ModePerm)
},
given: "x",
want: []string{"root.yaml"},
},
"dir": {
setup: func(t *testing.T, d string) {
os.Mkdir(path.Join(d, "x"), os.ModePerm)
touch(t, path.Join(d, "x", "a"))
touch(t, path.Join(d, "x", "a.todo.xyz"))
touch(t, path.Join(d, "x", "b"))
},
given: "x",
want: []string{"a", "a.todo.xyz", "b"},
},
}
for name, d := range cases {
c := d
t.Run(name, func(t *testing.T) {
d := t.TempDir()
if c.setup != nil {
c.setup(t, d)
}
config := config{target: path.Join(d, c.given)}
got := config.Targets()
for i := range got {
got[i] = path.Base(got[i])
}
for i := range c.want {
c.want[i] = path.Base(c.want[i])
}
t.Logf("want\n\t%+v, got \n\t%+v", c.want, got)
if len(got) != len(c.want) {
t.Error(c.want, got)
}
for i := range got {
if got[i] != c.want[i] {
t.Errorf("[%d] wanted %s, got %s", i, c.want[i], got[i])
}
}
})
}
}

44
cmd/dump.go Normal file
View File

@@ -0,0 +1,44 @@
package main
import (
"io"
"os"
"gitea.inhome.blapointe.com/gogs/pttodo/pttodo"
"gopkg.in/yaml.v2"
)
func dump(config config) error {
return _dump(os.Stdout, config.Targets(), config.tags, config.search, config.root)
}
func _dump(writer io.Writer, filepaths []string, tags []string, search, rootDisplay string) error {
root, err := pttodo.NewRootFromFiles(filepaths...)
if err != nil {
return err
}
for _, x := range []*[]pttodo.Todo{
&root.Todo,
&root.Scheduled,
&root.Done,
} {
y := pttodo.Todos(*x)
y = y.LikeTags(tags)
y = y.LikeSearch(search)
*x = y
}
var v interface{} = root
switch rootDisplay {
case DUMP_TODO:
v = root.Todo
case DUMP_SCHEDULED:
v = root.Scheduled
case DUMP_DONE:
v = root.Done
default:
}
return yaml.NewEncoder(writer).Encode(v)
}

262
cmd/edit.go Normal file
View File

@@ -0,0 +1,262 @@
package main
import (
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path"
"sort"
"strings"
"syscall"
"time"
"gitea.inhome.blapointe.com/gogs/pttodo/pttodo"
"github.com/google/uuid"
"gopkg.in/yaml.v2"
)
func edit(config *config) error {
if !config.edit {
return nil
}
return _edit(config.Targets())
}
func _edit(filepaths []string) error {
editableDir, err := inEditableDirAsTodos(filepaths)
if err != nil {
return err
}
if err := modifyEditableDir(editableDir, func() error {
files, err := listDir(editableDir)
if err != nil {
return err
}
for _, p := range files {
if _, err := pttodo.NewTodosFromFile(p); err != nil {
return err
}
}
return nil
}); err != nil {
return err
}
editedFiles, err := listDir(editableDir)
if err != nil {
return err
}
edits := map[string]string{}
for _, editedFile := range editedFiles {
edits[path.Base(editedFile)] = editedFile
edited, err := pttodo.NewTodosFromFile(editedFile)
if err != nil {
return err
}
original, err := pttodo.NewRootFromFile(func() string {
for _, f := range filepaths {
if path.Base(f) == path.Base(editedFile) {
return f
}
}
return "/dev/null"
}())
if err != nil {
return err
}
editedIDs := map[string]int{}
for i := range edited {
editedIDs[edited[i].ID()] = 1
}
for i := range original.Todo {
if _, ok := editedIDs[original.Todo[i].ID()]; ok {
continue
}
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])
}
original.Done = append(original.Done, original.Todo[i])
}
original.Todo = edited
original.AutoMove()
if err := func() error {
f, err := os.Create(editedFile)
if err != nil {
return err
}
defer f.Close()
return yaml.NewEncoder(f).Encode(original)
}(); err != nil {
return err
}
}
dir := ""
for _, f := range filepaths {
if edited, ok := edits[path.Base(f)]; ok {
if err := os.Rename(edited, f); err != nil {
return err
}
delete(edits, path.Base(f))
} else if err := os.Remove(f); err != nil {
return err
}
dir = path.Dir(f)
}
for base, editedFile := range edits {
f := path.Join(dir, base)
if _, err := os.Stat(f); err == nil {
f = fmt.Sprintf("%s.todo.%s", f, uuid.New().String())
}
if err := os.Rename(editedFile, f); err != nil {
return err
}
}
return nil
}
func inEditableDirAsTodos(filepaths []string) (string, error) {
tempDir, err := ioutil.TempDir(os.TempDir(), "edit-pttodo-*")
if err != nil {
return "", err
}
return tempDir, copyTodoToDir(tempDir, filepaths)
}
func copyTodoToDir(d string, filepaths []string) error {
inboxes := map[string][]string{}
for _, target := range filepaths {
p := path.Join(d, path.Base(target))
if strings.Contains(path.Base(target), ".todo.") {
p := path.Join(d, strings.Split(path.Base(p), ".todo")[0])
inboxes[p] = append(inboxes[p], target)
continue
}
if root, err := pttodo.NewRootFromFile(target); err != nil {
return err
} else if b, err := yaml.Marshal(root.Todo); err != nil {
return err
} else if err := os.WriteFile(p, b, os.ModePerm); err != nil {
return err
}
}
for p, inboxes := range inboxes {
inboxRoot := pttodo.Root{}
for _, inbox := range inboxes {
subInboxRoot, err := pttodo.NewRootFromFile(inbox)
if err != nil {
return err
}
inboxRoot.MergeIn(pttodo.Root{Todo: subInboxRoot.Todo})
inboxRoot.MergeIn(pttodo.Root{Todo: subInboxRoot.Scheduled})
}
root, err := pttodo.NewRootFromFile(p)
if err != nil {
return err
}
root.MergeIn(pttodo.Root{Todo: inboxRoot.Todo})
root.MergeIn(pttodo.Root{Todo: inboxRoot.Scheduled})
if b, err := yaml.Marshal(root.Todo); err != nil {
return err
} else if err := os.WriteFile(p, b, os.ModePerm); err != nil {
return err
}
}
return nil
}
func modifyEditableDir(d string, check func() error) error {
for {
if err := vimd(d); err != nil {
return err
}
err := check()
if err == nil {
return nil
}
log.Printf("%s, press <Enter> to resume editing", err)
b := make([]byte, 1)
if _, err := os.Stdin.Read(b); err != nil {
return err
}
}
}
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 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
}
if strings.HasPrefix(path.Base(entries[i].Name()), ".") {
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
}

13
cmd/go.mod Normal file
View File

@@ -0,0 +1,13 @@
module pttodo-cli
go 1.17
require (
gitea.inhome.blapointe.com/gogs/pttodo v0.0.0-20231109151914-5d74b458d542
github.com/google/uuid v1.3.0
gopkg.in/yaml.v2 v2.4.0
)
require github.com/robfig/cron/v3 v3.0.1 // indirect
replace gogs.inhome.blapointe.com/gogs/pttodo => ./..

View File

@@ -1,3 +1,7 @@
gitea.inhome.blapointe.com/gogs/pttodo v0.0.0-20231109151914-5d74b458d542 h1:eOWrA2hEQoyu413vbdXbEXHLXEX2TVBXjWawlWndFhg=
gitea.inhome.blapointe.com/gogs/pttodo v0.0.0-20231109151914-5d74b458d542/go.mod h1:9CFZf/SSod0Z/8WvbmOI4gEzy6xGpBQAq8coVNncNvk=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

102
cmd/main.go Normal file
View File

@@ -0,0 +1,102 @@
package main
import (
"bytes"
"io"
"io/ioutil"
"os"
"gitea.inhome.blapointe.com/gogs/pttodo/pttodo"
"gopkg.in/yaml.v2"
)
const (
DUMP_ALL = "all"
DUMP_TODO = "todo"
DUMP_SCHEDULED = "scheduled"
DUMP_DONE = "done"
)
func main() {
if err := _main(); err != nil {
panic(err)
}
}
func _main() error {
config := getConfig()
if err := add(&config); err != nil {
return err
}
if err := edit(&config); err != nil {
return err
}
return dump(config)
}
func merge(filepath string, mergeTargetFilePath string) error {
baseReader, err := filePathReader(filepath)
if err != nil {
return err
}
baseB, err := ioutil.ReadAll(baseReader)
if err != nil {
return err
}
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)
tmppath, err := marshalRootToTempFile(base)
if err != nil {
return err
}
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 filePathReader(path string) (io.Reader, error) {
if path == "-" {
return os.Stdin, nil
}
b, err := ioutil.ReadFile(path)
if os.IsNotExist(err) {
return bytes.NewReader([]byte("{}")), nil
}
if err != nil {
return nil, err
}
return bytes.NewReader(b), nil
}

View File

@@ -1,568 +0,0 @@
package main
import (
"bytes"
"flag"
"fmt"
"io"
"io/ioutil"
"local/pt-todo-server/pttodo"
"log"
"os"
"os/exec"
"path"
"sort"
"strings"
"syscall"
"time"
"gopkg.in/yaml.v2"
)
const (
DUMP_ALL = "all"
DUMP_TODO = "todo"
DUMP_SCHEDULED = "scheduled"
DUMP_DONE = "done"
)
type config struct {
targets []string
mergeme string
root string
tags []string
search string
edit bool
dry bool
add string
addSchedule string
}
func main() {
if err := _main(); err != nil {
panic(err)
}
}
func _main() error {
config := getConfig()
if config.mergeme != "" {
if err := merge(config.dry, config.targets[0], config.mergeme); err != nil {
return err
}
}
if config.add != "" {
if err := add(config.dry, config.targets, pttodo.Todo{
Todo: config.add,
Schedule: pttodo.Schedule(config.addSchedule),
}); 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 = "./todo.yaml"
}
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.StringVar(&config.add, "add", "", "todo to add")
flag.StringVar(&config.addSchedule, "add-schedule", "", "todo to add schedule")
flag.Parse()
config.tags = strings.Split(tagss, ",")
config.targets = []string{target}
if stat, err := os.Stat(target); err == nil && stat.IsDir() {
config.targets, _ = listDir(target)
}
return config
}
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 {
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 add(dry bool, filepaths []string, todo pttodo.Todo) error {
target := filepaths[0]
var original pttodo.Root
r, err := filePathReader(target)
if err != nil {
return err
}
if err := yaml.NewDecoder(r).Decode(&original); err != nil {
return err
}
original.Todo = append([]pttodo.Todo{todo}, original.Todo...)
original.AutoMove()
c, err := yaml.Marshal(original)
if err != nil {
return err
} else if dry {
log.Printf("wouldve written...\n%s", c)
return nil
} else 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
}
originals := map[string]pttodo.Root{}
for _, target := range filepaths {
var original pttodo.Root
if r, err := filePathReader(target); err != nil {
return err
} else if err := yaml.NewDecoder(r).Decode(&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 r, err := filePathReader(path.Join(tempDir, path.Base(target))); err != nil {
return err
} else if err := yaml.NewDecoder(r).Decode(&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 {
r, err := filePathReader(path.Join(tempDir, path.Base(target)))
if err != nil {
return err
}
var newTodos []pttodo.Todo
if err := yaml.NewDecoder(r).Decode(&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])
}
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
}
if _, err := os.Stat(filepath); err == nil {
g, err := os.Open(filepath)
if err != nil {
return err
}
if _, err := io.Copy(f, g); err != nil {
return err
}
g.Close()
}
f.Close()
return nil
}
cp := func() error {
for _, filepath := range filepaths {
if err := cpOne(filepath); err != nil {
return err
}
}
return nil
}
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)
}
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 := filePathReader(tempFile); err != nil {
return err
} else if err := yaml.NewDecoder(a).Decode(&rootTemp); err != nil {
return err
} else if b, err := filePathReader(filepath); err != nil {
return err
} else if err := yaml.NewDecoder(b).Decode(&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 {
return err
}
}
if !dry {
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 {
return err
}
baseB, err := ioutil.ReadAll(baseReader)
if err != nil {
return err
}
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, filepaths []string, tags []string, search, rootDisplay string) error {
var root pttodo.Root
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
switch rootDisplay {
case DUMP_ALL:
case DUMP_TODO:
v = root.Todo
case DUMP_SCHEDULED:
v = root.Scheduled
case DUMP_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 {
skip := false
for _, tag := range tags {
positiveTag := strings.TrimLeft(tag, "-")
hasTag := strings.Contains(todo.Tags, positiveTag)
wantToHaveTag := !strings.HasPrefix(tag, "-")
skip = skip || !(hasTag == wantToHaveTag)
}
if !skip {
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)
if err != nil {
return err
}
fmt.Fprintf(writer, "%s\n", b2)
return nil
}
func filePathReader(path string) (io.Reader, error) {
if path == "-" {
return os.Stdin, nil
}
b, err := ioutil.ReadFile(path)
if os.IsNotExist(err) {
return bytes.NewReader([]byte("{}")), nil
}
if err != nil {
return nil, err
}
return bytes.NewReader(b), 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

@@ -1,12 +0,0 @@
module pttodo-cli
go 1.17
require (
gopkg.in/yaml.v2 v2.4.0
local/pt-todo-server v0.0.0-00010101000000-000000000000
)
require github.com/robfig/cron/v3 v3.0.1 // indirect
replace local/pt-todo-server => ../../

View File

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

0
cmd/pttodo-cli/testdata/1.yaml → cmd/testdata/1.yaml vendored Normal file → Executable file
View File

0
cmd/pttodo-cli/testdata/2.yaml → cmd/testdata/2.yaml vendored Normal file → Executable file
View File

0
cmd/pttodo-cli/testdata/3.yaml → cmd/testdata/3.yaml vendored Normal file → Executable file
View File

17
cmd/testdata/root.yaml vendored Executable file
View File

@@ -0,0 +1,17 @@
todo:
- root
- stub
scheduled:
- todo: abc
schedule: "2024-01-01"
ts: Thu Nov 9 07:49:59 MST 2023
- todo: "1"
schedule: "2024-01-02"
ts: Thu Nov 9 07:51:13 MST 2023
- todo: "1"
schedule: "2024-01-02"
ts: Thu Nov 9 07:51:28 MST 2023
- todo: "1"
schedule: "2024-01-02"
ts: Thu Nov 9 07:51:36 MST 2023
done: []

View File

@@ -1,21 +0,0 @@
#! /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,55 +0,0 @@
#! /bin/bash
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)"
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() {
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
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[@]}" "$@"
}
if [ "$0" == "$BASH_SOURCE" ]; then
main "$@"
fi

10
go.mod
View File

@@ -1,4 +1,4 @@
module pttodo
module gitea.inhome.blapointe.com/gogs/pttodo
go 1.17
@@ -6,11 +6,3 @@ 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,17 +1,5 @@
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

@@ -1,11 +1,58 @@
package pttodo
import (
"bytes"
"os"
yaml "gopkg.in/yaml.v2"
)
type Root struct {
Todo []Todo
Scheduled []Todo
Done []Todo
}
func NewRootFromFiles(p ...string) (Root, error) {
var result Root
for _, p := range p {
subroot, err := NewRootFromFile(p)
if err != nil {
return Root{}, err
}
result.MergeIn(subroot)
}
result.AutoMove()
return result, nil
}
func NewRootFromFile(p string) (Root, error) {
if b, err := os.ReadFile(p); err == nil && len(bytes.TrimSpace(b)) == 0 {
return Root{}, nil
}
f, err := os.Open(p)
if os.IsNotExist(err) {
return Root{}, nil
}
if err != nil {
return Root{}, err
}
defer f.Close()
var result Root
if err := yaml.NewDecoder(f).Decode(&result); err != nil {
todos, err2 := NewTodosFromFile(p)
if err2 != nil {
return Root{}, err
}
result.Todo = todos
}
result.AutoMove()
return result, nil
}
func (root Root) Equals(other Root) bool {
for i, slice := range [][2][]Todo{
[2][]Todo{root.Todo, other.Todo},

View File

@@ -4,6 +4,8 @@ import (
"bytes"
"encoding/json"
"fmt"
"os"
"path"
"strconv"
"strings"
"testing"
@@ -180,3 +182,78 @@ done:
t.Fatalf("want\n\t%q, got\n\t%q", want, string(got))
}
}
func TestRootFromFile(t *testing.T) {
cases := map[string]struct {
given string
want Root
}{
"empty": {},
"happy": {
given: `{"todo":["a", "b"],"scheduled":["c"], "done":[{"todo": "d"}]}`,
want: Root{
Todo: []Todo{
Todo{Todo: "a"},
Todo{Todo: "b"},
},
Scheduled: []Todo{
Todo{Todo: "c"},
},
Done: []Todo{
Todo{Todo: "d"},
},
},
},
"todos": {
given: `["a", {"todo": "b"}]`,
want: Root{
Todo: []Todo{
{Todo: "a"},
{Todo: "b"},
},
},
},
}
for name, d := range cases {
c := d
t.Run(name, func(t *testing.T) {
d := t.TempDir()
p := path.Join(d, "input.yaml")
if err := os.WriteFile(p, []byte(c.given), os.ModePerm); err != nil {
t.Fatal(err)
}
got, err := NewRootFromFile(p)
if err != nil {
t.Fatal(err)
}
if fmt.Sprintf("%+v", got) != fmt.Sprintf("%+v", c.want) {
t.Errorf("want\n\t%+v, got\n\t%+v", c.want, got)
}
})
}
}
func TestRootFromFiles(t *testing.T) {
d := t.TempDir()
ps := []string{
path.Join(d, "a"),
path.Join(d, "b"),
}
os.WriteFile(ps[0], []byte(`["a"]`), os.ModePerm)
os.WriteFile(ps[1], []byte(`["b"]`), os.ModePerm)
got, err := NewRootFromFiles(ps...)
if err != nil {
t.Fatal(err)
}
want := Root{
Todo: []Todo{
{Todo: "a"},
{Todo: "b"},
},
}
if fmt.Sprintf("%+v", got) != fmt.Sprintf("%+v", want) {
t.Errorf("want\n\t%+v, got \n\t%+v", want, got)
}
}

View File

@@ -4,7 +4,10 @@ import (
"encoding/base64"
"fmt"
"hash/crc32"
"os"
"time"
yaml "gopkg.in/yaml.v2"
)
type Todo struct {
@@ -17,6 +20,24 @@ type Todo struct {
writeTS bool
}
func NewTodosFromFile(p string) ([]Todo, error) {
f, err := os.Open(p)
if os.IsNotExist(err) {
return nil, nil
}
if err != nil {
return nil, err
}
defer f.Close()
var result []Todo
if err := yaml.NewDecoder(f).Decode(&result); err != nil {
return nil, err
}
return result, nil
}
func (todo Todo) ID() string {
hash := crc32.NewIEEE()
fmt.Fprintf(hash, "%d:%s", 0, todo.Todo)

36
pttodo/todos.go Normal file
View File

@@ -0,0 +1,36 @@
package pttodo
import "strings"
type Todos []Todo
func (todos Todos) LikeSearch(search string) Todos {
return todos.Like(func(todo Todo) bool {
return strings.Contains(
strings.ToLower(todo.Todo),
strings.ToLower(search),
)
})
}
func (todos Todos) LikeTags(tags []string) Todos {
return todos.Like(func(todo Todo) bool {
matches := true
for _, tag := range tags {
str := strings.TrimLeft(tag, "-")
want := !strings.HasPrefix(tag, "-")
matches = matches && strings.Contains(todo.Tags, str) == want
}
return matches
})
}
func (todos Todos) Like(like func(Todo) bool) Todos {
result := make(Todos, 0)
for i := range todos {
if like(todos[i]) {
result = append(result, todos[i])
}
}
return result
}

17
pttodo/todos_test.go Normal file
View File

@@ -0,0 +1,17 @@
package pttodo
import "testing"
func TestTodosLikeTags(t *testing.T) {
todos := Todos{
{Todo: "a", Tags: "x"},
{Todo: "b", Tags: "x,y"},
}
result := todos.LikeTags([]string{"x", "-y"})
if len(result) != 1 {
t.Error(result)
} else if result[0].Todo != "a" {
t.Error(result[0].Todo)
}
}

7
pttodoer/Cargo.lock generated Normal file
View File

@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "pttodoer"
version = "0.1.0"

8
pttodoer/Cargo.toml Normal file
View File

@@ -0,0 +1,8 @@
[package]
name = "pttodoer"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

3
pttodoer/src/main.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}

4
pttodoer/src/testdata/.mvp.yaml.done vendored Normal file
View File

@@ -0,0 +1,4 @@
- todo: a b and c
schedule: * * * * *
ts: 123
- d e f

View File

@@ -0,0 +1 @@
plaintext

View File

@@ -0,0 +1,4 @@
todo: todo here
schedule: * * * * *
details: |
hello world

2
pttodoer/src/testdata/mvp.yaml vendored Normal file
View File

@@ -0,0 +1,2 @@
- x
- y and z