feat(tui): add bubbletea terminal UI with entity list, detail, and capture
Adds `nib tui` command and `make tui` target. Scrollable entity list with j/k navigation, enter for detail view, `a` to capture new entries using the existing parse grammar, and `d` to delete.
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
"github.com/lerko/nib/internal/db"
|
||||
"github.com/lerko/nib/internal/display"
|
||||
)
|
||||
|
||||
type detailModel struct {
|
||||
entity *db.Entity
|
||||
scroll int
|
||||
height int
|
||||
width int
|
||||
}
|
||||
|
||||
func newDetailModel() detailModel {
|
||||
return detailModel{}
|
||||
}
|
||||
|
||||
func (d *detailModel) setEntity(e *db.Entity) {
|
||||
d.entity = e
|
||||
d.scroll = 0
|
||||
}
|
||||
|
||||
func (d *detailModel) setSize(width, height int) {
|
||||
d.width = width
|
||||
d.height = height
|
||||
}
|
||||
|
||||
func (d detailModel) update(msg tea.KeyMsg) detailModel {
|
||||
switch msg.String() {
|
||||
case "up", "k":
|
||||
if d.scroll > 0 {
|
||||
d.scroll--
|
||||
}
|
||||
case "down", "j":
|
||||
d.scroll++
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func (d detailModel) view(width int) string {
|
||||
if d.entity == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
e := d.entity
|
||||
var b strings.Builder
|
||||
|
||||
glyph := display.DisplayGlyph(e.Glyph, e.CardType)
|
||||
header := fmt.Sprintf("%s %s", glyph, display.FormatID(e.ID))
|
||||
b.WriteString(detailHeaderStyle.Render(header))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if e.Title != nil {
|
||||
b.WriteString(detailBodyStyle.Render("title: " + *e.Title))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString(detailBodyStyle.Render(e.Body))
|
||||
b.WriteString("\n")
|
||||
|
||||
if len(e.Tags) > 0 {
|
||||
tagParts := make([]string, len(e.Tags))
|
||||
for i, t := range e.Tags {
|
||||
tagParts[i] = tagStyle.Render("#" + t)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
b.WriteString(detailBodyStyle.Render(strings.Join(tagParts, " ")))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
meta := fmt.Sprintf("created %s", e.CreatedAt.Format(time.DateTime))
|
||||
if e.ModifiedAt != e.CreatedAt {
|
||||
meta += fmt.Sprintf("\nmodified %s", e.ModifiedAt.Format(time.DateTime))
|
||||
}
|
||||
if e.TimeAnchor != nil {
|
||||
meta += fmt.Sprintf("\nanchored @%s", *e.TimeAnchor)
|
||||
}
|
||||
if e.Pinned {
|
||||
meta += "\npinned"
|
||||
}
|
||||
if e.CardType != nil {
|
||||
meta += fmt.Sprintf("\ncard %s", *e.CardType)
|
||||
}
|
||||
if e.CompletedAt != nil {
|
||||
meta += fmt.Sprintf("\ndone %s", e.CompletedAt.Format(time.DateTime))
|
||||
}
|
||||
b.WriteString(idStyle.Render(meta))
|
||||
|
||||
lines := strings.Split(b.String(), "\n")
|
||||
if d.scroll > 0 && d.scroll < len(lines) {
|
||||
lines = lines[d.scroll:]
|
||||
}
|
||||
if d.height > 0 && len(lines) > d.height {
|
||||
lines = lines[:d.height]
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
Reference in New Issue
Block a user