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.
This commit is contained in:
2026-05-14 11:17:27 -04:00
parent 51cbf86d77
commit a6fda5d1ee
6 changed files with 312 additions and 2 deletions
+70
View File
@@ -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
}
+131
View File
@@ -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())
}
+52
View File
@@ -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)
}
+13
View File
@@ -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)
}
+36
View File
@@ -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
}
+10 -2
View File
@@ -1,7 +1,15 @@
package main package main
import "fmt" import (
"fmt"
"os"
"github.com/lerko/nib/cmd"
)
func main() { func main() {
fmt.Println("nib") if err := cmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
} }