move pttodo-cli up from /cmd/

master
Bel LaPointe 2023-11-06 12:15:12 -07:00
parent cdd1be46a8
commit 02dab4b726
14 changed files with 581 additions and 0 deletions

40
cmd/add.go Normal file
View File

@ -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
}

47
cmd/config.go Normal file
View File

@ -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
}

106
cmd/dump.go Normal file
View File

@ -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
}

161
cmd/edit.go Normal file
View File

@ -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 <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
}
}
}
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
}

15
cmd/go.mod Normal file
View File

@ -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 => ./..

8
cmd/go.sum Normal file
View File

@ -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=

20
cmd/install.sh Normal file
View File

@ -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"

129
cmd/main.go Normal file
View File

@ -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
}

29
cmd/main_test.go Normal file
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/testdata/1.yaml vendored Normal file
View File

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

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

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

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

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

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

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

10
cmd/testdata/test.yaml vendored Executable file
View File

@ -0,0 +1,10 @@
todo:
- hi20890
- hi
- b
- todo: loop
schedule: 10s
- todo: past
schedule: "2000-01-02"
scheduled: []
done: []