From a6fda5d1ee098e59b1c850311b34c1004f27c0dd Mon Sep 17 00:00:00 2001 From: Tyler Koenig Date: Thu, 14 May 2026 11:17:27 -0400 Subject: [PATCH] feat(cli): add nib add + nib ls commands Default command delegation: `nib "..."` routes to `nib add`. Capture bar parses grammar, creates entities. `nib ls` lists with date grouping, tag filter, 48h default window. Display glyphs for all entity types. --- cmd/add.go | 70 ++++++++++++++++++++ cmd/ls.go | 131 ++++++++++++++++++++++++++++++++++++++ cmd/root.go | 52 +++++++++++++++ cmd/store.go | 13 ++++ internal/display/glyph.go | 36 +++++++++++ main.go | 12 +++- 6 files changed, 312 insertions(+), 2 deletions(-) create mode 100644 cmd/add.go create mode 100644 cmd/ls.go create mode 100644 cmd/root.go create mode 100644 cmd/store.go create mode 100644 internal/display/glyph.go diff --git a/cmd/add.go b/cmd/add.go new file mode 100644 index 0000000..9e7cb8a --- /dev/null +++ b/cmd/add.go @@ -0,0 +1,70 @@ +package cmd + +import ( + "fmt" + "strings" + + "github.com/lerko/nib/internal/db" + "github.com/lerko/nib/internal/display" + "github.com/lerko/nib/internal/parse" + "github.com/spf13/cobra" +) + +var addCmd = &cobra.Command{ + Use: "add [input]", + Short: "capture a new entity", + Args: cobra.MinimumNArgs(1), + RunE: runAdd, +} + +func runAdd(_ *cobra.Command, args []string) error { + input := strings.Join(args, " ") + + parsed, err := parse.Parse(input) + if err != nil { + return fmt.Errorf("parse: %w", err) + } + + store, err := openStore() + if err != nil { + return err + } + defer store.Close() + + e := &db.Entity{ + Body: parsed.Body, + Glyph: db.Glyph(parsed.Glyph), + Tags: parsed.Tags, + } + if parsed.TimeAnchor != nil { + e.TimeAnchor = parsed.TimeAnchor + } + if parsed.CardSuffix != nil { + ct := db.CardType(*parsed.CardSuffix) + e.CardType = &ct + } + + if err := store.Create(e); err != nil { + return err + } + + glyph := display.DisplayGlyph(e.Glyph, e.CardType) + shortID := display.FormatID(e.ID) + + var parts []string + parts = append(parts, glyph) + parts = append(parts, " "+e.Body) + if e.TimeAnchor != nil { + parts = append(parts, " @"+*e.TimeAnchor) + } + for _, tag := range e.Tags { + parts = append(parts, " #"+tag) + } + parts = append(parts, " ["+shortID+"]") + if e.CardType != nil { + parts = append(parts, fmt.Sprintf(" (%s)", *e.CardType)) + } + + fmt.Println(strings.Join(parts, "")) + return nil +} diff --git a/cmd/ls.go b/cmd/ls.go new file mode 100644 index 0000000..2f7de44 --- /dev/null +++ b/cmd/ls.go @@ -0,0 +1,131 @@ +package cmd + +import ( + "fmt" + "strings" + "time" + + "github.com/lerko/nib/internal/db" + "github.com/lerko/nib/internal/display" + "github.com/spf13/cobra" +) + +var ( + lsTag string + lsDate string + lsAll bool +) + +var lsCmd = &cobra.Command{ + Use: "ls", + Short: "list entities in stream order", + RunE: runLs, +} + +func init() { + lsCmd.Flags().StringVar(&lsTag, "tag", "", "filter by tag") + lsCmd.Flags().StringVar(&lsDate, "date", "", "filter by date (YYYY-MM-DD)") + lsCmd.Flags().BoolVar(&lsAll, "all", false, "include deleted entities") +} + +func runLs(_ *cobra.Command, _ []string) error { + store, err := openStore() + if err != nil { + return err + } + defer store.Close() + + p := db.DefaultListParams() + p.IncludeDeleted = lsAll + + if lsTag != "" { + p.Tag = &lsTag + } + if lsDate != "" { + p.Date = &lsDate + } else { + since := time.Now().UTC().Add(-48 * time.Hour) + p.Since = &since + } + + entities, err := store.List(p) + if err != nil { + return err + } + + if len(entities) == 0 { + return nil + } + + groups := groupByDate(entities) + for _, g := range groups { + fmt.Printf("── %s ──\n", g.label) + for _, e := range g.entities { + printEntity(e) + } + fmt.Println() + } + + return nil +} + +type dateGroup struct { + label string + entities []*db.Entity +} + +func groupByDate(entities []*db.Entity) []dateGroup { + var groups []dateGroup + var current *dateGroup + + for _, e := range entities { + label := formatDateLabel(e.CreatedAt) + if current == nil || current.label != label { + if current != nil { + groups = append(groups, *current) + } + current = &dateGroup{label: label} + } + current.entities = append(current.entities, e) + } + if current != nil { + groups = append(groups, *current) + } + return groups +} + +func formatDateLabel(t time.Time) string { + return strings.ToLower(t.Format("Jan 2")) +} + +func printEntity(e *db.Entity) { + glyph := display.DisplayGlyph(e.Glyph, e.CardType) + shortID := display.FormatID(e.ID) + + var line strings.Builder + fmt.Fprintf(&line, "%s %-40s", glyph, e.Body) + + if e.TimeAnchor != nil { + fmt.Fprintf(&line, " @%-5s", *e.TimeAnchor) + } else { + line.WriteString(" ") + } + + var tagStr string + for _, tag := range e.Tags { + tagStr += " #" + tag + } + if tagStr != "" { + fmt.Fprintf(&line, " %-16s", strings.TrimSpace(tagStr)) + } else { + line.WriteString(" ") + } + + fmt.Fprintf(&line, " %s", shortID) + + if e.UseCount > 0 { + fmt.Fprintf(&line, " (%d×)", e.UseCount) + } + + fmt.Println(line.String()) +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..95f1471 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,52 @@ +package cmd + +import ( + "os" + "strings" + + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "nib", + Short: "capture and crystallize notes, todos, and events", + Args: cobra.ArbitraryArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return cmd.Help() + } + return runAdd(cmd, args) + }, + SilenceUsage: true, +} + +func Execute() error { + if len(os.Args) > 1 { + first := os.Args[1] + isFlag := strings.HasPrefix(first, "-") && !strings.Contains(first, " ") + if first != "help" && first != "completion" && + !isFlag && !isSubcommand(first) { + rootCmd.SetArgs(append([]string{"add", "--"}, os.Args[1:]...)) + } + } + return rootCmd.Execute() +} + +func isSubcommand(name string) bool { + for _, c := range rootCmd.Commands() { + if c.Name() == name { + return true + } + for _, alias := range c.Aliases { + if alias == name { + return true + } + } + } + return false +} + +func init() { + rootCmd.AddCommand(addCmd) + rootCmd.AddCommand(lsCmd) +} diff --git a/cmd/store.go b/cmd/store.go new file mode 100644 index 0000000..4959e98 --- /dev/null +++ b/cmd/store.go @@ -0,0 +1,13 @@ +package cmd + +import ( + "github.com/lerko/nib/internal/db" +) + +func openStore() (*db.Store, error) { + path, err := db.DefaultPath() + if err != nil { + return nil, err + } + return db.Open(path) +} diff --git a/internal/display/glyph.go b/internal/display/glyph.go new file mode 100644 index 0000000..cdd371f --- /dev/null +++ b/internal/display/glyph.go @@ -0,0 +1,36 @@ +package display + +import "github.com/lerko/nib/internal/db" + +var glyphMap = map[db.Glyph]string{ + db.GlyphNote: "◦", + db.GlyphTodo: "▸", + db.GlyphEvent: "◇", +} + +var cardGlyphMap = map[db.CardType]string{ + db.CardSnippet: "◆", + db.CardTemplate: "◈", + db.CardChecklist: "☐", + db.CardDecision: "⚖", + db.CardLink: "🔗", +} + +func DisplayGlyph(glyph db.Glyph, cardType *db.CardType) string { + if cardType != nil { + if g, ok := cardGlyphMap[*cardType]; ok { + return g + } + } + if g, ok := glyphMap[glyph]; ok { + return g + } + return "◦" +} + +func FormatID(id string) string { + if len(id) > 6 { + return id[:6] + } + return id +} diff --git a/main.go b/main.go index ae90840..fada07d 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,15 @@ package main -import "fmt" +import ( + "fmt" + "os" + + "github.com/lerko/nib/cmd" +) func main() { - fmt.Println("nib") + if err := cmd.Execute(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } }