feat(tui): status debounce, scroll indicator, drawer label, card grouping
Status messages now use a sequence counter so rapid actions don't cause premature clearing. Detail pane shows scroll position and supports pgup/pgdown/g/G. Capture drawer border includes inline label. Cards view groups by intent (pinned/grab/read/fill) with gutter labels matching the stream view's date grouping pattern.
This commit is contained in:
+117
-13
@@ -64,9 +64,16 @@ func matchesIntent(e *db.Entity, i intent) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type cardGroup struct {
|
||||
label string
|
||||
start int
|
||||
count int
|
||||
}
|
||||
|
||||
type cardsModel struct {
|
||||
entities []*db.Entity
|
||||
filtered []*db.Entity
|
||||
groups []cardGroup
|
||||
cursor int
|
||||
offset int
|
||||
height int
|
||||
@@ -91,24 +98,69 @@ func (c *cardsModel) setIntent(i intent) {
|
||||
}
|
||||
|
||||
func (c *cardsModel) applyFilter() {
|
||||
c.filtered = nil
|
||||
var pinned, rest []*db.Entity
|
||||
for _, e := range c.entities {
|
||||
if !matchesIntent(e, c.intent) {
|
||||
continue
|
||||
}
|
||||
if e.Pinned {
|
||||
pinned = append(pinned, e)
|
||||
} else {
|
||||
rest = append(rest, e)
|
||||
}
|
||||
}
|
||||
c.filtered = append(pinned, rest...)
|
||||
c.filtered, c.groups = sortAndGroupCards(c.entities, c.intent)
|
||||
if c.cursor >= len(c.filtered) {
|
||||
c.cursor = max(0, len(c.filtered)-1)
|
||||
}
|
||||
}
|
||||
|
||||
func sortAndGroupCards(entities []*db.Entity, intentFilter intent) ([]*db.Entity, []cardGroup) {
|
||||
if intentFilter != intentAll {
|
||||
var pinned, rest []*db.Entity
|
||||
for _, e := range entities {
|
||||
if !matchesIntent(e, intentFilter) {
|
||||
continue
|
||||
}
|
||||
if e.Pinned {
|
||||
pinned = append(pinned, e)
|
||||
} else {
|
||||
rest = append(rest, e)
|
||||
}
|
||||
}
|
||||
return append(pinned, rest...), nil
|
||||
}
|
||||
|
||||
var pinned, grab, read, fill []*db.Entity
|
||||
for _, e := range entities {
|
||||
if e.Pinned {
|
||||
pinned = append(pinned, e)
|
||||
} else {
|
||||
switch {
|
||||
case matchesIntent(e, intentGrab):
|
||||
grab = append(grab, e)
|
||||
case matchesIntent(e, intentRead):
|
||||
read = append(read, e)
|
||||
case matchesIntent(e, intentFill):
|
||||
fill = append(fill, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var filtered []*db.Entity
|
||||
var groups []cardGroup
|
||||
for _, bucket := range []struct {
|
||||
label string
|
||||
entities []*db.Entity
|
||||
}{
|
||||
{"pinned", pinned},
|
||||
{"grab", grab},
|
||||
{"read", read},
|
||||
{"fill", fill},
|
||||
} {
|
||||
if len(bucket.entities) == 0 {
|
||||
continue
|
||||
}
|
||||
groups = append(groups, cardGroup{
|
||||
label: bucket.label,
|
||||
start: len(filtered),
|
||||
count: len(bucket.entities),
|
||||
})
|
||||
filtered = append(filtered, bucket.entities...)
|
||||
}
|
||||
|
||||
return filtered, groups
|
||||
}
|
||||
|
||||
func (c *cardsModel) setSize(width, height int) {
|
||||
c.width = width
|
||||
c.height = height
|
||||
@@ -166,6 +218,9 @@ func (c cardsModel) view(width int) string {
|
||||
if len(c.filtered) == 0 {
|
||||
return statusStyle.Render("no cards")
|
||||
}
|
||||
if len(c.groups) > 0 {
|
||||
return c.groupedView(width)
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
visible := c.visibleCount()
|
||||
@@ -188,6 +243,55 @@ func (c cardsModel) view(width int) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (c cardsModel) groupedView(width int) string {
|
||||
entityWidth := width - 4 - dateGutterWidth
|
||||
|
||||
type displayLine struct {
|
||||
text string
|
||||
entityIdx int
|
||||
}
|
||||
|
||||
var lines []displayLine
|
||||
for _, g := range c.groups {
|
||||
for i := 0; i < g.count; i++ {
|
||||
eIdx := g.start + i
|
||||
var gutter string
|
||||
if i == 0 {
|
||||
gutter = gutterStyle.Render(padRight(g.label, 6) + " │ ")
|
||||
} else {
|
||||
gutter = gutterStyle.Render(" │ ")
|
||||
}
|
||||
line := gutter + renderCard(c.filtered[eIdx], entityWidth)
|
||||
lines = append(lines, displayLine{text: line, entityIdx: eIdx})
|
||||
}
|
||||
}
|
||||
|
||||
visible := c.visibleCount()
|
||||
offset := c.offset
|
||||
if c.cursor < offset {
|
||||
offset = c.cursor
|
||||
}
|
||||
if c.cursor >= offset+visible {
|
||||
offset = c.cursor - visible + 1
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
end := min(offset+visible, len(lines))
|
||||
for i := offset; i < end; i++ {
|
||||
dl := lines[i]
|
||||
if dl.entityIdx == c.cursor {
|
||||
b.WriteString(selectedItemStyle.Render(" " + dl.text))
|
||||
} else {
|
||||
b.WriteString(listItemStyle.Render(dl.text))
|
||||
}
|
||||
if i < end-1 {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (c cardsModel) visibleCount() int {
|
||||
if c.height <= 0 {
|
||||
return 20
|
||||
|
||||
Reference in New Issue
Block a user