pttodo/cmd/edit.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
}