4e0ac8402f
Content area now enforces full height so the context help bar stays pinned to the terminal bottom. Hint keys rendered with bold highlight color for scannability. Status messages (created, deleted, etc.) auto-clear after 2 seconds, reverting to the entity count.
278 lines
5.6 KiB
Go
278 lines
5.6 KiB
Go
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{}
|
|
|
|
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) tea.Cmd {
|
|
return tea.Tick(d, func(time.Time) tea.Msg {
|
|
return statusClearMsg{}
|
|
})
|
|
}
|