feat(tui): add status bar, help overlay, tag filter, and entity actions
Status bar with entity count and context-sensitive key hints. Help overlay via ? key. Tag filter via # with cursor-navigable tag list. Todo toggle (x), pin (!), promote (p), demote (D), copy (c), edit (e) via $EDITOR. Delete confirmation with 3s timeout. Date-grouped list with completed todo and pinned indicators. Esc clears active tag filter. Adds CompletedAt/ClearCompleted to EntityUpdate for todo toggling.
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
"github.com/lerko/nib/internal/db"
|
||||
@@ -18,6 +23,29 @@ type entityDeletedMsg struct {
|
||||
id string
|
||||
}
|
||||
|
||||
type entityUpdatedMsg struct {
|
||||
entity *db.Entity
|
||||
action string
|
||||
}
|
||||
|
||||
type entityPromotedMsg struct {
|
||||
id string
|
||||
}
|
||||
|
||||
type entityDemotedMsg struct {
|
||||
id string
|
||||
}
|
||||
|
||||
type entityCopiedMsg struct{}
|
||||
|
||||
type tagsLoadedMsg struct {
|
||||
tags []db.TagCount
|
||||
}
|
||||
|
||||
type editorFinishedMsg struct {
|
||||
err error
|
||||
}
|
||||
|
||||
type errMsg struct {
|
||||
err error
|
||||
}
|
||||
@@ -49,3 +77,130 @@ func deleteEntity(store *db.Store, id string) tea.Cmd {
|
||||
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) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if err := store.Promote(id, db.CardSnippet, nil); err != nil {
|
||||
return errMsg{err}
|
||||
}
|
||||
return entityPromotedMsg{id}
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user