package tui import ( "os" "os/exec" "strings" "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 entityAbsorbedMsg struct { targetID string } type absorbSourcesLoadedMsg struct { targetID string entities []*db.Entity } type stepsPersistedMsg struct{} type templateCopiedMsg struct{} type tagsLoadedMsg struct { tags []db.TagCount } type statusClearMsg struct{ seq int } 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 { editorEnv := os.Getenv("EDITOR") if editorEnv == "" { editorEnv = os.Getenv("VISUAL") } if editorEnv == "" { editorEnv = "vi" } parts := strings.Fields(editorEnv) editor, editorArgs := parts[0], parts[1:] 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, append(editorArgs, 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} }) } func loadAbsorbSources(store *db.Store, targetID string) tea.Cmd { return func() tea.Msg { entities, err := store.List(db.DefaultListParams()) if err != nil { return errMsg{err} } return absorbSourcesLoadedMsg{targetID, entities} } } func absorbEntity(store *db.Store, targetID, sourceID string) tea.Cmd { return func() tea.Msg { if err := store.Absorb(targetID, sourceID); err != nil { return errMsg{err} } return entityAbsorbedMsg{targetID} } } func persistSteps(store *db.Store, entityID string, stepsJSON string) tea.Cmd { return func() tea.Msg { update := db.EntityUpdate{CardData: &stepsJSON} if err := store.Update(entityID, &update); err != nil { return errMsg{err} } return stepsPersistedMsg{} } } func copyResolved(store *db.Store, entityID string, resolved string) tea.Cmd { return func() tea.Msg { if err := clipboard.WriteAll(resolved); err != nil { return errMsg{err} } if err := store.IncrementUse(entityID); err != nil { return errMsg{err} } return templateCopiedMsg{} } } func clearStatusAfter(d time.Duration, seq int) tea.Cmd { return tea.Tick(d, func(time.Time) tea.Msg { return statusClearMsg{seq: seq} }) }