src

Go monorepo.
Log | Files | Refs

commit d45e6b6586be684aaf34c690809eb016df79084d
parent b6b9161af11c1ca8a7b9451b926123aedb1413a0
Author: dwrz <dwrz@dwrz.net>
Date:   Mon,  9 Jan 2023 21:10:07 +0000

Add wisdom

Diffstat:
Acmd/wisdom/wisdom.go | 35+++++++++++++++++++++++++++++++++++
Apkg/wisdom/quote/quote.go | 48++++++++++++++++++++++++++++++++++++++++++++++++
Apkg/wisdom/wisdom.go | 325+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 408 insertions(+), 0 deletions(-)

diff --git a/cmd/wisdom/wisdom.go b/cmd/wisdom/wisdom.go @@ -0,0 +1,35 @@ +package main + +import ( + "flag" + "os" + "path/filepath" + + "code.dwrz.net/src/pkg/log" + "code.dwrz.net/src/pkg/wisdom" +) + +var ( + wrap = flag.Int("w", 72, "word wrap; disabled if negative") +) + +func main() { + var l = log.New(os.Stderr) + + flag.Parse() + + dir, err := os.UserConfigDir() + if err != nil { + l.Error.Fatalf("failed to get user config dir: %v", err) + } + + w, err := wisdom.New(wisdom.Parameters{ + Log: l, + Path: filepath.Join(dir, "wisdom"), + Wrap: *wrap, + }) + + if err := w.Command(flag.CommandLine.Args()); err != nil { + l.Error.Fatal(err) + } +} diff --git a/pkg/wisdom/quote/quote.go b/pkg/wisdom/quote/quote.go @@ -0,0 +1,48 @@ +package quote + +import ( + "strings" + + "code.dwrz.net/src/pkg/text" +) + +type Quote struct { + Author string `json:"author"` + Comment string `json:"comment"` + Source string `json:"source"` + Tags map[string]struct{} `json:"tags"` + Text string `json:"text"` +} + +func (q *Quote) Render(width int) string { + var str strings.Builder + + str.WriteString(text.Wrap(q.Text, width)) + + if q.Author != "" || q.Source != "" { + str.WriteString("\n\n") + } + if q.Author != "" { + str.WriteString(text.Wrap(q.Author, width)) + } + if q.Author != "" && q.Source != "" { + str.WriteString(", ") + } + if q.Source != "" { + str.WriteString(text.Wrap(q.Source, width)) + } + if q.Comment != "" { + str.WriteString("\n\n") + str.WriteString(text.Wrap(q.Comment, width)) + } + if len(q.Tags) > 0 { + str.WriteString("\nTags: ") + var tags string + for tag := range q.Tags { + tags += tag + " " + } + str.WriteString(text.Wrap(tags, width)) + } + + return str.String() +} diff --git a/pkg/wisdom/wisdom.go b/pkg/wisdom/wisdom.go @@ -0,0 +1,325 @@ +package wisdom + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + "text/tabwriter" + "unicode" + + "github.com/google/uuid" + + "code.dwrz.net/src/pkg/log" + "code.dwrz.net/src/pkg/store" + "code.dwrz.net/src/pkg/text" + "code.dwrz.net/src/pkg/wisdom/quote" +) + +const coll = "data" + +type Parameters struct { + Log *log.Logger + Path string + Wrap int +} + +type Wisdom struct { + log *log.Logger + store *store.Store + wrap int +} + +func New(p Parameters) (*Wisdom, error) { + store, err := store.New(store.Parameters{ + Log: p.Log, + Path: p.Path, + }) + if err != nil { + return nil, fmt.Errorf("failed to create store: %v", err) + } + + if err := store.NewCollection(coll); err != nil { + return nil, fmt.Errorf("failed to create collection: %v", err) + } + + return &Wisdom{ + log: p.Log, + store: store, + wrap: p.Wrap, + }, nil +} + +func (w *Wisdom) Command(args []string) error { + // Show a random quote by default. + if len(args) == 0 { + d, err := w.store.Collection(coll).Random() + if err != nil { + return fmt.Errorf("failed to load quote: %v", err) + } + + var q quote.Quote + if err := d.Unmarshal(&q); err != nil { + return fmt.Errorf( + "failed to unmarshal document: %v", err, + ) + } + + fmt.Println(q.Render(w.wrap)) + + return nil + } + + command, rest := args[0], args[1:] + switch command { + case "add": + if err := w.add(); err != nil { + return fmt.Errorf("failed to add quote: %v", err) + } + + case "edit": + if err := w.edit(rest); err != nil { + return fmt.Errorf("failed to edit quote: %v", err) + } + + case "show": + if err := w.show(rest); err != nil { + return fmt.Errorf("failed to show quote: %v", err) + } + + case "list": + if err := w.list(); err != nil { + return fmt.Errorf("failed to list quotes: %v", err) + } + + case "remove": + if err := w.remove(rest); err != nil { + return fmt.Errorf("failed to remove quote: %v", err) + } + + default: + return fmt.Errorf("unrecognized command: %v", command) + } + + return nil +} + +func (w *Wisdom) add() error { + var ( + in = bufio.NewReader(os.Stdin) + str strings.Builder + q = quote.Quote{ + Tags: map[string]struct{}{}, + } + ) + + // Text + fmt.Print("Text (Ctrl-D for EOF):") + if _, err := io.Copy(&str, os.Stdin); err != nil { + if err != io.EOF { + return fmt.Errorf("failed to read: %v", err) + } + } + q.Text = strings.TrimRightFunc(str.String(), unicode.IsSpace) + if q.Text == "" { + return fmt.Errorf("missing text") + } + str.Reset() + + // Author + fmt.Print("Author: ") + line, err := in.ReadString('\n') + if err != nil { + return fmt.Errorf("failed to read: %v", err) + } + q.Author = strings.TrimSpace(line) + + // Source + fmt.Print("Source: ") + line, err = in.ReadString('\n') + if err != nil { + return fmt.Errorf("failed to read: %v", err) + } + q.Source = strings.TrimSpace(line) + + // Tags + fmt.Print("Tags (comma delimited): ") + line, err = in.ReadString('\n') + if err != nil { + return fmt.Errorf("failed to read: %v", err) + } + for _, t := range strings.Split(strings.TrimSpace(line), ",") { + if t == "" { + continue + } + q.Tags[t] = struct{}{} + } + + // Comment + fmt.Print("Comment (Ctrl-D for EOF): ") + if _, err := io.Copy(&str, os.Stdin); err != nil { + if err != io.EOF { + return fmt.Errorf("failed to read: %v", err) + } + } + q.Comment = strings.TrimSpace(str.String()) + str.Reset() + + if _, err := w.store.Collection(coll).Create( + uuid.NewString(), q, + ); err != nil { + return fmt.Errorf("failed to create: %v", err) + } + + return nil +} + +func (w *Wisdom) edit(args []string) error { + if len(args) == 0 { + return fmt.Errorf("missing id") + } + + editor := strings.Split(os.Getenv("EDITOR"), " ") + if len(editor) == 0 { + return fmt.Errorf("missing $EDITOR") + } + + path, err := exec.LookPath(editor[0]) + if err != nil { + return fmt.Errorf( + "failed to find $EDITOR %s: %w", editor, err, + ) + } + + for _, id := range args { + d, err := w.store.Collection(coll).FindId(id) + if err != nil { + return fmt.Errorf("failed to load quote: %v", err) + } + + temp := filepath.Join(os.TempDir(), "wisdom", id) + if err := os.WriteFile(temp, d.Data, 0600); err != nil { + return err + } + + cmd := exec.Cmd{ + Path: path, + Args: append(editor, temp), + Stdin: os.Stdin, + Stdout: os.Stdout, + Stderr: os.Stderr, + } + if err := cmd.Run(); err != nil { + return err + } + + data, err := os.ReadFile(temp) + if err != nil { + return fmt.Errorf( + "failed to read file %s: %w", temp, err, + ) + } + + var q = &quote.Quote{} + if err := json.Unmarshal(data, q); err != nil { + return fmt.Errorf( + "failed to json unmarshal %s: %w", temp, err, + ) + } + + if err := os.Remove(temp); err != nil { + return fmt.Errorf("failed to remove temp file: %v", err) + } + + d, err = w.store.Collection(coll).Create(uuid.NewString(), q) + if err != nil { + return fmt.Errorf( + "failed to create new document: %v", err, + ) + } + + if err := w.store.Collection(coll).Delete(id); err != nil { + return fmt.Errorf( + "failed to delete original document: %v", err, + ) + } + } + + return nil +} + +func (w *Wisdom) list() error { + docs, err := w.store.Collection(coll).All() + if err != nil { + return fmt.Errorf("failed to load quotes: %v", err) + } + + tw := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', 0) + fmt.Fprintln(tw, "Id\tText\tAuthor\tSource\t") + for _, d := range docs { + var q = &quote.Quote{} + + if err := d.Unmarshal(q); err != nil { + w.log.Error.Printf( + "failed to unmarshal document %s: %v", + d.Id, err, + ) + continue + } + + var t = q.Text + if w.wrap > 0 { + t = text.Truncate(q.Text, w.wrap-1) + "…" + } + + fmt.Fprintf( + tw, "%s\t%s\t%s\t%s\t\n", + d.Id, + strings.ReplaceAll(t, "\n", " "), + q.Author, + q.Source, + ) + } + tw.Flush() + + return nil +} + +func (w *Wisdom) remove(args []string) error { + if len(args) == 0 { + return fmt.Errorf("missing quote filename") + } + + for _, name := range args { + if err := w.store.Collection(coll).Delete(name); err != nil { + return fmt.Errorf("failed to delete %s: %v", name, err) + } + } + + return nil +} + +func (w *Wisdom) show(args []string) error { + for _, id := range args { + d, err := w.store.Collection(coll).FindId(id) + if err != nil { + return fmt.Errorf("failed to load quotes: %v", err) + } + + var q = &quote.Quote{} + if err := d.Unmarshal(q); err != nil { + return fmt.Errorf( + "failed to unmarshal document %s: %v", + d.Id, err, + ) + } + + fmt.Println(q.Render(w.wrap)) + } + + return nil +}