diff --git a/cmd/add.go b/cmd/add.go new file mode 100644 index 0000000..04745b1 --- /dev/null +++ b/cmd/add.go @@ -0,0 +1,40 @@ +package main + +import ( + "io/ioutil" + "os" + + "github.com/google/uuid" + "gogs.inhome.blapointe.com/bel/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, + } + newTarget, err := _add(config.targets, v) + if err != nil { + return err + } + config.targets = append(config.targets, newTarget) + return nil +} + +func _add(filepaths []string, todo pttodo.Todo) (string, error) { + target := filepaths[0] + ".todo." + 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 target, nil +} diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 0000000..b9d629a --- /dev/null +++ b/cmd/config.go @@ -0,0 +1,47 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" +) + +type config struct { + targets []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 + 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.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 = []string{target} + if stat, err := os.Stat(target); err == nil && stat.IsDir() { + config.targets, _ = listDir(target) + } + + return config +} diff --git a/cmd/dump.go b/cmd/dump.go new file mode 100644 index 0000000..f1e4f76 --- /dev/null +++ b/cmd/dump.go @@ -0,0 +1,106 @@ +package main + +import ( + "fmt" + "io" + "io/ioutil" + "log" + "os" + "strings" + + "gogs.inhome.blapointe.com/bel/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 { + 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 !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 +} diff --git a/cmd/edit.go b/cmd/edit.go new file mode 100644 index 0000000..71f30d3 --- /dev/null +++ b/cmd/edit.go @@ -0,0 +1,161 @@ +package main + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "path" + "syscall" + "time" + + "github.com/google/uuid" + "gogs.inhome.blapointe.com/bel/pttodo/pttodo" + "gopkg.in/yaml.v2" +) + +func edit(config config) error { + if !config.edit { + return nil + } + return _edit(config.targets) +} + +func _edit(filepaths []string) error { + tempDir, err := ioutil.TempDir(os.TempDir(), "edit-pttodo-*") + if err != nil { + return err + } + originals := map[string]pttodo.Root{} + lastModified := map[string]time.Time{} + 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 info, _ := os.Stat(target); info != nil { + lastModified[target] = info.ModTime() + } else { + lastModified[target] = time.Time{} + } + } + 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 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 + } + } + } + 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 + } + outputPath := target + if info, _ := os.Stat(target); info != nil && info.ModTime() != lastModified[target] { + outputPath = fmt.Sprintf("%s.%s.%s", outputPath, uuid.New().String(), path.Ext(outputPath)) + } + if err := ioutil.WriteFile(outputPath, c, os.ModePerm); err != nil { + return err + } + } + 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 +} diff --git a/cmd/go.mod b/cmd/go.mod new file mode 100644 index 0000000..bb93bf9 --- /dev/null +++ b/cmd/go.mod @@ -0,0 +1,15 @@ +module pttodo-cli + +go 1.17 + +require ( + gogs.inhome.blapointe.com/bel/pttodo v0.4.0 + gopkg.in/yaml.v2 v2.4.0 +) + +require ( + github.com/google/uuid v1.3.0 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect +) + +replace gogs.inhome.blapointe.com/bel/pttodo => ./.. diff --git a/cmd/go.sum b/cmd/go.sum new file mode 100644 index 0000000..200a035 --- /dev/null +++ b/cmd/go.sum @@ -0,0 +1,8 @@ +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= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/cmd/install.sh b/cmd/install.sh new file mode 100644 index 0000000..bdb4a97 --- /dev/null +++ b/cmd/install.sh @@ -0,0 +1,20 @@ +#! /bin/bash + +cd "$(dirname "$BASH_SOURCE")" + +binary_name="$(head -n 1 go.mod | awk '{print $NF}' | sed 's/.*\///')" +git_commit="$(( + git rev-list -1 HEAD + if git diff | grep . > /dev/null; then + echo "-dirty" + fi +) 2> /dev/null | tr -d '\n')" + +GOFLAGS="" \ +GO111MODULE="" \ +CGO_ENABLED=0 \ +go build \ + -o $GOPATH/bin/$binary_name \ + -a \ + -installsuffix cgo \ + -ldflags "-s -w -X main.GitCommit=$git_commit" diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..aeb25c6 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,129 @@ +package main + +import ( + "bytes" + "io" + "io/ioutil" + "os" + "path" + "sort" + "strings" + + "gogs.inhome.blapointe.com/bel/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 +} + +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 +} diff --git a/cmd/main_test.go b/cmd/main_test.go new file mode 100644 index 0000000..d6a96c5 --- /dev/null +++ b/cmd/main_test.go @@ -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) + } +} diff --git a/cmd/testdata/1.yaml b/cmd/testdata/1.yaml new file mode 100644 index 0000000..ca66683 --- /dev/null +++ b/cmd/testdata/1.yaml @@ -0,0 +1,4 @@ +todo: +- "1" +scheduled: [] +done: [] diff --git a/cmd/testdata/2.yaml b/cmd/testdata/2.yaml new file mode 100644 index 0000000..2c974d6 --- /dev/null +++ b/cmd/testdata/2.yaml @@ -0,0 +1,4 @@ +todo: +- "2" +scheduled: [] +done: [] diff --git a/cmd/testdata/3.yaml b/cmd/testdata/3.yaml new file mode 100644 index 0000000..10ac22b --- /dev/null +++ b/cmd/testdata/3.yaml @@ -0,0 +1,4 @@ +todo: +- "3" +scheduled: [] +done: [] diff --git a/cmd/testdata/root.yaml b/cmd/testdata/root.yaml new file mode 100644 index 0000000..fc779e6 --- /dev/null +++ b/cmd/testdata/root.yaml @@ -0,0 +1,4 @@ +todo: +- root +scheduled: [] +done: [] diff --git a/cmd/testdata/test.yaml b/cmd/testdata/test.yaml new file mode 100755 index 0000000..657fa00 --- /dev/null +++ b/cmd/testdata/test.yaml @@ -0,0 +1,10 @@ +todo: +- hi20890 +- hi +- b +- todo: loop + schedule: 10s +- todo: past + schedule: "2000-01-02" +scheduled: [] +done: []