263 lines
5.5 KiB
Go
263 lines
5.5 KiB
Go
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 := 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 := 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
|
|
}
|