package main import ( "bytes" "flag" "fmt" "io" "io/ioutil" "os" "path" "sort" "strings" "gogs.inhome.blapointe.com/bel/pttodo/pttodo" "gopkg.in/yaml.v2" ) const ( DUMP_ALL = "all" DUMP_TODO = "todo" DUMP_SCHEDULED = "scheduled" DUMP_DONE = "done" ) type config struct { targets []string root string tags []string search string edit bool add string addSchedule string addTags string } func main() { if err := _main(); err != nil { panic(err) } } func _main() error { config := getConfig() if err := add(config); err != nil { return err } if err := edit(config); err != nil { return err } return dump(config) } func getConfig() config { defaultFilepath := os.Getenv("PTTODO_FILE") if defaultFilepath == "" { defaultFilepath = "./todo.yaml" } var config config var target string flag.StringVar(&target, "f", defaultFilepath, "($PTTODO_FILE) path to yaml file or dir (starting with root then alphabetical for dir)") flag.StringVar(&config.root, "root", DUMP_TODO, "path to pretty print ("+fmt.Sprint([]string{DUMP_ALL, DUMP_TODO, DUMP_SCHEDULED, DUMP_DONE})+")") var tagss string flag.StringVar(&tagss, "tags", "", "csv of all tags to find, -x to invert") flag.StringVar(&config.search, "search", "", "fts case insensitive") flag.BoolVar(&config.edit, "e", false, "edit file") flag.StringVar(&config.add, "add", "", "todo to add") flag.StringVar(&config.addSchedule, "add-schedule", "", "todo to add schedule") flag.StringVar(&config.addTags, "add-tags", "", "todo to add csv tags") flag.Parse() config.tags = strings.Split(tagss, ",") config.targets = []string{target} if stat, err := os.Stat(target); err == nil && stat.IsDir() { config.targets, _ = listDir(target) } return config } func verifyRoot(root pttodo.Root) error { f, err := ioutil.TempFile(os.TempDir(), "tmp") if err != nil { return err } f.Close() tempFile := f.Name() b, err := yaml.Marshal(root) if err != nil { return err } if err := ioutil.WriteFile(tempFile, b, os.ModePerm); err != nil { return err } defer os.Remove(tempFile) return verifyFile(tempFile) } func verifyFile(path string) error { if err := _dump(io.Discard, []string{path}, nil, "", DUMP_ALL); err != nil { return fmt.Errorf("failed verifying file %s: %w", path, err) } return nil } func merge(filepath string, mergeTargetFilePath string) error { baseReader, err := filePathReader(filepath) if err != nil { return err } baseB, err := ioutil.ReadAll(baseReader) if err != nil { return err } mergingReader, err := filePathReader(mergeTargetFilePath) if err != nil { return err } mergingB, err := ioutil.ReadAll(mergingReader) if err != nil { return err } var base, merging pttodo.Root if err := yaml.Unmarshal(baseB, &base); err != nil { return err } if err := yaml.Unmarshal(mergingB, &merging); err != nil { return err } base.MergeIn(merging) if err := verifyRoot(base); err != nil { return err } tmppath, err := marshalRootToTempFile(base) if err != nil { return err } return os.Rename(tmppath, filepath) } func marshalRootToTempFile(root pttodo.Root) (string, error) { f, err := ioutil.TempFile(os.TempDir(), "tmp") if err != nil { return "", err } f.Close() os.Remove(f.Name()) b, err := yaml.Marshal(root) if err != nil { return "", err } filepath := f.Name() + ".yaml" err = ioutil.WriteFile(filepath, b, os.ModePerm) return filepath, err } func filePathReader(path string) (io.Reader, error) { if path == "-" { return os.Stdin, nil } b, err := ioutil.ReadFile(path) if os.IsNotExist(err) { return bytes.NewReader([]byte("{}")), nil } if err != nil { return nil, err } return bytes.NewReader(b), 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 }