feat(tui): collapsible tag rail with ambient tag awareness
Persistent left panel showing tags with counts. Provides ambient awareness of tag landscape without requiring a modal. - New tagRailModel in tagrail.go: tag list with cursor, scroll, counts - Rail visible at >=100 cols width, 18% width (min 16 chars) - ctrl+b toggles rail visibility - focusTagRail added to focus cycle: capture → tags → list → detail - j/k navigates, enter filters/unfilters by tag - Active filter tag highlighted bold in rail - Tags refresh after entity create/delete/absorb - Rail auto-hides on narrow terminals, # modal still works as fallback - Width allocation accounts for rail in split and non-split layouts
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/lerko/nib/internal/db"
|
||||
)
|
||||
|
||||
type tagRailModel struct {
|
||||
tags []db.TagCount
|
||||
cursor int
|
||||
offset int
|
||||
height int
|
||||
width int
|
||||
activeTag string
|
||||
}
|
||||
|
||||
func newTagRailModel() tagRailModel {
|
||||
return tagRailModel{}
|
||||
}
|
||||
|
||||
func (r *tagRailModel) setTags(tags []db.TagCount) {
|
||||
r.tags = tags
|
||||
if r.cursor >= len(tags) {
|
||||
r.cursor = max(0, len(tags)-1)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *tagRailModel) setSize(width, height int) {
|
||||
r.width = width
|
||||
r.height = height
|
||||
}
|
||||
|
||||
func (r tagRailModel) selectedTag() string {
|
||||
if len(r.tags) == 0 || r.cursor >= len(r.tags) {
|
||||
return ""
|
||||
}
|
||||
return r.tags[r.cursor].Tag
|
||||
}
|
||||
|
||||
func (r tagRailModel) update(key string) tagRailModel {
|
||||
switch key {
|
||||
case "up", "k":
|
||||
if r.cursor > 0 {
|
||||
r.cursor--
|
||||
if r.cursor < r.offset {
|
||||
r.offset = r.cursor
|
||||
}
|
||||
}
|
||||
case "down", "j":
|
||||
if r.cursor < len(r.tags)-1 {
|
||||
r.cursor++
|
||||
visible := r.visibleCount()
|
||||
if r.cursor >= r.offset+visible {
|
||||
r.offset = r.cursor - visible + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (r tagRailModel) visibleCount() int {
|
||||
v := r.height - 2
|
||||
if v <= 0 {
|
||||
return 10
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (r tagRailModel) view(focused bool) string {
|
||||
w := r.width
|
||||
if w <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
|
||||
headerStyle := railHeaderStyle
|
||||
if focused {
|
||||
headerStyle = headerStyle.Foreground(highlight)
|
||||
}
|
||||
b.WriteString(headerStyle.Render("tags"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(separatorStyle.Render(strings.Repeat("─", w)))
|
||||
b.WriteString("\n")
|
||||
|
||||
if len(r.tags) == 0 {
|
||||
b.WriteString(hintDescStyle.Render(" no tags"))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
visible := r.visibleCount()
|
||||
end := min(r.offset+visible, len(r.tags))
|
||||
|
||||
countW := 0
|
||||
for _, tc := range r.tags {
|
||||
cw := len(fmt.Sprintf("%d", tc.Count))
|
||||
if cw > countW {
|
||||
countW = cw
|
||||
}
|
||||
}
|
||||
|
||||
nameW := w - countW - 3
|
||||
if nameW < 4 {
|
||||
nameW = 4
|
||||
}
|
||||
|
||||
for i := r.offset; i < end; i++ {
|
||||
tc := r.tags[i]
|
||||
name := "#" + tc.Tag
|
||||
if len(name) > nameW {
|
||||
name = name[:nameW-1] + "…"
|
||||
}
|
||||
|
||||
count := fmt.Sprintf("%*d", countW, tc.Count)
|
||||
gap := w - len(name) - len(count) - 1
|
||||
if gap < 1 {
|
||||
gap = 1
|
||||
}
|
||||
|
||||
var line string
|
||||
if i == r.cursor && focused {
|
||||
line = selectedItemStyle.Render(" " + name + strings.Repeat(" ", gap) + railCountStyle.Render(count))
|
||||
} else if tc.Tag == r.activeTag {
|
||||
line = " " + railActiveTagStyle.Render(name) + strings.Repeat(" ", gap) + railCountStyle.Render(count)
|
||||
} else {
|
||||
line = " " + railTagStyle.Render(name) + strings.Repeat(" ", gap) + railCountStyle.Render(count)
|
||||
}
|
||||
|
||||
b.WriteString(line)
|
||||
if i < end-1 {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
Reference in New Issue
Block a user