a96c1a52f4
Port all web CSS token themes to TUI via shared vocabulary (accent, dim, muted, ok, todo, event, remind, danger). Styles rebuild from active theme on switch. Press T to cycle, persists to ~/.nib/theme. Glamour markdown renderer respects light/dark per theme.
165 lines
3.5 KiB
Go
165 lines
3.5 KiB
Go
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/charmbracelet/glamour"
|
|
|
|
"github.com/lerko/nib/internal/db"
|
|
"github.com/lerko/nib/internal/display"
|
|
)
|
|
|
|
const staleThresholdDays = 30
|
|
|
|
type stumbleModel struct {
|
|
entries []*db.Entity
|
|
cursor int
|
|
width int
|
|
height int
|
|
done bool
|
|
}
|
|
|
|
func newStumbleModel() stumbleModel {
|
|
return stumbleModel{}
|
|
}
|
|
|
|
func (s *stumbleModel) setEntries(entries []*db.Entity) {
|
|
s.entries = entries
|
|
s.cursor = 0
|
|
s.done = len(entries) == 0
|
|
}
|
|
|
|
func (s *stumbleModel) setSize(width, height int) {
|
|
s.width = width
|
|
s.height = height
|
|
}
|
|
|
|
func (s stumbleModel) current() *db.Entity {
|
|
if s.done || len(s.entries) == 0 || s.cursor >= len(s.entries) {
|
|
return nil
|
|
}
|
|
return s.entries[s.cursor]
|
|
}
|
|
|
|
func (s *stumbleModel) advance() {
|
|
s.cursor++
|
|
if s.cursor >= len(s.entries) {
|
|
s.done = true
|
|
}
|
|
}
|
|
|
|
func (s *stumbleModel) removeCurrent() {
|
|
if s.cursor < len(s.entries) {
|
|
s.entries = append(s.entries[:s.cursor], s.entries[s.cursor+1:]...)
|
|
if s.cursor >= len(s.entries) {
|
|
s.done = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s stumbleModel) total() int {
|
|
return len(s.entries)
|
|
}
|
|
|
|
func (s stumbleModel) view() string {
|
|
if s.done {
|
|
return s.doneView()
|
|
}
|
|
|
|
e := s.current()
|
|
if e == nil {
|
|
return s.doneView()
|
|
}
|
|
|
|
w := s.width
|
|
var b strings.Builder
|
|
|
|
progress := fmt.Sprintf("stumble [%d/%d]", s.cursor+1, len(s.entries))
|
|
b.WriteString(detailHeaderStyle.Render(progress))
|
|
b.WriteString("\n")
|
|
b.WriteString(separatorStyle.Render(strings.Repeat("─", w)))
|
|
b.WriteString("\n\n")
|
|
|
|
glyph := display.DisplayGlyph(e.Glyph, e.CardType)
|
|
title := e.Body
|
|
if e.Title != nil {
|
|
title = *e.Title
|
|
}
|
|
if len(title) > w-6 {
|
|
title = title[:w-9] + "…"
|
|
}
|
|
b.WriteString(" " + glyphStyle.Render(glyph) + " " + title)
|
|
b.WriteString("\n")
|
|
|
|
var meta []string
|
|
meta = append(meta, string(e.Glyph))
|
|
if e.CardType != nil {
|
|
meta = append(meta, string(*e.CardType))
|
|
}
|
|
for _, t := range e.Tags {
|
|
meta = append(meta, tagStyle.Render("#"+t))
|
|
}
|
|
meta = append(meta, "captured "+e.CreatedAt.Format("Jan 2"))
|
|
b.WriteString(" " + idStyle.Render(strings.Join(meta, " · ")))
|
|
b.WriteString("\n\n")
|
|
|
|
bodyWidth := w - 6
|
|
if bodyWidth < 20 {
|
|
bodyWidth = 20
|
|
}
|
|
r, _ := glamour.NewTermRenderer(
|
|
glamour.WithStylePath(glamourStyle()),
|
|
glamour.WithWordWrap(bodyWidth),
|
|
)
|
|
rendered, err := r.Render(e.Body)
|
|
if err != nil {
|
|
rendered = e.Body
|
|
}
|
|
rendered = strings.TrimRight(rendered, "\n")
|
|
b.WriteString(" " + rendered)
|
|
b.WriteString("\n\n")
|
|
|
|
age := daysAgo(e.ModifiedAt)
|
|
ageText := fmt.Sprintf("last touched %d days ago", age)
|
|
b.WriteString(" " + stumbleAgeStyle.Render(ageText))
|
|
b.WriteString("\n")
|
|
b.WriteString(separatorStyle.Render(strings.Repeat("─", w)))
|
|
|
|
lines := strings.Split(b.String(), "\n")
|
|
if len(lines) > s.height {
|
|
lines = lines[:s.height]
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|
|
|
|
func (s stumbleModel) doneView() string {
|
|
var b strings.Builder
|
|
b.WriteString("\n\n")
|
|
b.WriteString(detailHeaderStyle.Render(" all caught up"))
|
|
b.WriteString("\n\n")
|
|
reviewed := s.total()
|
|
if reviewed > 0 {
|
|
b.WriteString(idStyle.Render(fmt.Sprintf(" %d entries reviewed", reviewed)))
|
|
} else {
|
|
b.WriteString(idStyle.Render(" no stale entries found"))
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func daysAgo(t time.Time) int {
|
|
return int(math.Floor(time.Since(t).Hours() / 24))
|
|
}
|
|
|
|
func staleParams() db.ListParams {
|
|
threshold := time.Now().UTC().AddDate(0, 0, -staleThresholdDays)
|
|
return db.ListParams{
|
|
ModifiedBefore: &threshold,
|
|
Sort: "modified_at",
|
|
Order: "asc",
|
|
Limit: 50,
|
|
}
|
|
}
|