package main import ( "fmt" "io/ioutil" "log" "os" "os/exec" "path" "sort" "strings" "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(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 } for _, target := range editedFiles { r, err := filePathReader(path.Join(editableDir, 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 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 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 }