Files
nib-v1/internal/tui/commands.go
T
lerko ce335cabd6 feat(tui): add cards view, mode switching, promote picker, and card detail
Stream/cards toggle with 1/2 keys. Cards view with intent filtering
(tab cycles grab/read/fill/all), sort cycling (s key), pinned-first
ordering, and affordance badges. Promote picker (p key) with card type
selection and auto-detection from body content. Detail view renders
card_data per type: checklist steps, template slots, decision fields,
link URLs.

Extracts generateCardData to internal/carddata for reuse across cmd
and tui packages.
2026-05-17 21:14:14 -04:00

210 lines
4.1 KiB
Go

package tui
import (
"os"
"os/exec"
"time"
"github.com/atotto/clipboard"
tea "github.com/charmbracelet/bubbletea"
"github.com/lerko/nib/internal/carddata"
"github.com/lerko/nib/internal/db"
)
type entitiesLoadedMsg struct {
entities []*db.Entity
}
type entityCreatedMsg struct {
entity *db.Entity
}
type entityDeletedMsg struct {
id string
}
type entityUpdatedMsg struct {
entity *db.Entity
action string
}
type entityPromotedMsg struct {
id string
cardType db.CardType
}
type entityDemotedMsg struct {
id string
}
type entityCopiedMsg struct{}
type tagsLoadedMsg struct {
tags []db.TagCount
}
type editorFinishedMsg struct {
err error
}
type errMsg struct {
err error
}
func loadEntities(store *db.Store, params db.ListParams) tea.Cmd {
return func() tea.Msg {
entities, err := store.List(params)
if err != nil {
return errMsg{err}
}
return entitiesLoadedMsg{entities}
}
}
func createEntity(store *db.Store, e *db.Entity) tea.Cmd {
return func() tea.Msg {
if err := store.Create(e); err != nil {
return errMsg{err}
}
return entityCreatedMsg{e}
}
}
func deleteEntity(store *db.Store, id string) tea.Cmd {
return func() tea.Msg {
if _, err := store.SoftDelete(id); err != nil {
return errMsg{err}
}
return entityDeletedMsg{id}
}
}
func toggleTodo(store *db.Store, e *db.Entity) tea.Cmd {
return func() tea.Msg {
var update db.EntityUpdate
if e.CompletedAt == nil {
now := time.Now().UTC()
update = db.EntityUpdate{CompletedAt: &now}
} else {
update = db.EntityUpdate{ClearCompleted: true}
}
if err := store.Update(e.ID, &update); err != nil {
return errMsg{err}
}
updated, err := store.Get(e.ID)
if err != nil {
return errMsg{err}
}
action := "completed"
if e.CompletedAt != nil {
action = "reopened"
}
return entityUpdatedMsg{updated, action}
}
}
func pinEntity(store *db.Store, e *db.Entity) tea.Cmd {
return func() tea.Msg {
newPinned := !e.Pinned
update := db.EntityUpdate{Pinned: &newPinned}
if err := store.Update(e.ID, &update); err != nil {
return errMsg{err}
}
updated, err := store.Get(e.ID)
if err != nil {
return errMsg{err}
}
action := "pinned"
if !newPinned {
action = "unpinned"
}
return entityUpdatedMsg{updated, action}
}
}
func promoteEntity(store *db.Store, id string, ct db.CardType, body string) tea.Cmd {
return func() tea.Msg {
cd := carddata.GenerateCardData(ct, body)
if err := store.Promote(id, ct, cd); err != nil {
return errMsg{err}
}
return entityPromotedMsg{id, ct}
}
}
func demoteEntity(store *db.Store, id string) tea.Cmd {
return func() tea.Msg {
if err := store.Demote(id); err != nil {
return errMsg{err}
}
return entityDemotedMsg{id}
}
}
func copyToClipboard(store *db.Store, e *db.Entity) tea.Cmd {
return func() tea.Msg {
if err := clipboard.WriteAll(e.Body); err != nil {
return errMsg{err}
}
if err := store.IncrementUse(e.ID); err != nil {
return errMsg{err}
}
return entityCopiedMsg{}
}
}
func loadTags(store *db.Store) tea.Cmd {
return func() tea.Msg {
tags, err := store.ListTags(false)
if err != nil {
return errMsg{err}
}
return tagsLoadedMsg{tags}
}
}
func editInEditor(store *db.Store, e *db.Entity) tea.Cmd {
editor := os.Getenv("EDITOR")
if editor == "" {
editor = "vi"
}
f, err := os.CreateTemp("", "nib-edit-*.md")
if err != nil {
return func() tea.Msg { return errMsg{err} }
}
if _, err := f.WriteString(e.Body); err != nil {
f.Close()
os.Remove(f.Name())
return func() tea.Msg { return errMsg{err} }
}
f.Close()
c := exec.Command(editor, f.Name())
return tea.ExecProcess(c, func(err error) tea.Msg {
defer os.Remove(f.Name())
if err != nil {
return editorFinishedMsg{err}
}
content, readErr := os.ReadFile(f.Name())
if readErr != nil {
return editorFinishedMsg{readErr}
}
newBody := string(content)
if newBody == e.Body {
return editorFinishedMsg{nil}
}
update := db.EntityUpdate{Body: &newBody}
if updateErr := store.Update(e.ID, &update); updateErr != nil {
return editorFinishedMsg{updateErr}
}
return editorFinishedMsg{nil}
})
}