pttodo/cmd/edit.go

253 lines
5.2 KiB
Go

package main
import (
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path"
"sort"
"strings"
"syscall"
"time"
"gogs.inhome.blapointe.com/bel/pttodo/pttodo"
"gopkg.in/yaml.v2"
)
func edit(config *config) error {
if !config.edit {
return nil
}
return _edit(func() []string { return config.Targets() })
}
func _edit(filepaths func() []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{}
originals := map[string]string{}
for _, editedFile := range editedFiles {
edits[path.Base(editedFile)] = editedFile
originalFile := func() string {
last := ""
for _, f := range filepaths() {
if path.Base(f) == path.Base(editedFile) {
return f
}
last = f
}
if last != "" {
return path.Join(path.Dir(last), path.Base(editedFile))
}
panic("how?")
}()
edited, err := pttodo.NewTodosFromFile(editedFile)
if err != nil {
return err
}
original, err := pttodo.NewRootFromFile(originalFile)
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 {
originals[path.Base(editedFile)] = originalFile
f, err := os.Create(editedFile)
if err != nil {
return err
}
defer f.Close()
return yaml.NewEncoder(f).Encode(original)
}(); err != nil {
return err
}
}
for _, f := range originals {
if edited, ok := edits[path.Base(f)]; ok {
if err := os.Rename(edited, f); err != nil {
return err
}
} else if err := os.Remove(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 := []string{}
for _, target := range filepaths {
p := path.Join(d, path.Base(target))
if strings.Contains(path.Base(target), ".todo.") {
inboxes = append(inboxes, 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 _, inbox := range inboxes {
target := path.Join(path.Dir(inbox), strings.Split(path.Base(inbox), ".todo.")[0])
p := path.Join(d, path.Base(target))
root, err := pttodo.NewRootFromFile(p)
if err != nil {
return err
}
subroot, err := pttodo.NewRootFromFile(inbox)
if err != nil {
return err
}
root.MergeIn(subroot)
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
}