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