Merge pull request 'feat(tui): collapsible tag rail with ambient tag awareness' (#35) from feat/tag-rail into main
Reviewed-on: #35
This commit was merged in pull request #35.
This commit is contained in:
@@ -58,6 +58,10 @@ type tagsLoadedMsg struct {
|
|||||||
tags []db.TagCount
|
tags []db.TagCount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type railTagsLoadedMsg struct {
|
||||||
|
tags []db.TagCount
|
||||||
|
}
|
||||||
|
|
||||||
type statusClearMsg struct{ seq int }
|
type statusClearMsg struct{ seq int }
|
||||||
|
|
||||||
type editorFinishedMsg struct {
|
type editorFinishedMsg struct {
|
||||||
@@ -181,6 +185,16 @@ func loadTags(store *db.Store) tea.Cmd {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadRailTags(store *db.Store) tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
tags, err := store.ListTags(false)
|
||||||
|
if err != nil {
|
||||||
|
return errMsg{err}
|
||||||
|
}
|
||||||
|
return railTagsLoadedMsg{tags}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func editInEditor(store *db.Store, e *db.Entity) tea.Cmd {
|
func editInEditor(store *db.Store, e *db.Entity) tea.Cmd {
|
||||||
editorEnv := os.Getenv("EDITOR")
|
editorEnv := os.Getenv("EDITOR")
|
||||||
if editorEnv == "" {
|
if editorEnv == "" {
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ func renderHelp(width, height int) string {
|
|||||||
binds [][2]string
|
binds [][2]string
|
||||||
}{
|
}{
|
||||||
{"Focus", [][2]string{
|
{"Focus", [][2]string{
|
||||||
{"tab", "cycle focus: capture → list → detail"},
|
{"tab", "toggle capture ↔ list"},
|
||||||
{"esc", "back / clear filter / to capture"},
|
{"esc", "back / clear filter / to capture"},
|
||||||
{"a", "focus capture bar"},
|
{"a", "focus capture bar"},
|
||||||
|
{"h", "focus tag rail (from list)"},
|
||||||
|
{"l", "focus detail (split view)"},
|
||||||
|
{"ctrl+b", "toggle tag rail"},
|
||||||
}},
|
}},
|
||||||
{"Capture Bar", [][2]string{
|
{"Capture Bar", [][2]string{
|
||||||
{"enter", "submit (or browse if empty)"},
|
{"enter", "submit (or browse if empty)"},
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ type keyMap struct {
|
|||||||
FocusLeft key.Binding
|
FocusLeft key.Binding
|
||||||
FocusRight key.Binding
|
FocusRight key.Binding
|
||||||
Tab key.Binding
|
Tab key.Binding
|
||||||
|
ToggleRail key.Binding
|
||||||
}
|
}
|
||||||
|
|
||||||
var keys = keyMap{
|
var keys = keyMap{
|
||||||
@@ -64,4 +65,5 @@ var keys = keyMap{
|
|||||||
FocusLeft: key.NewBinding(key.WithKeys("h"), key.WithHelp("h", "focus list")),
|
FocusLeft: key.NewBinding(key.WithKeys("h"), key.WithHelp("h", "focus list")),
|
||||||
FocusRight: key.NewBinding(key.WithKeys("l"), key.WithHelp("l", "focus detail")),
|
FocusRight: key.NewBinding(key.WithKeys("l"), key.WithHelp("l", "focus detail")),
|
||||||
Tab: key.NewBinding(key.WithKeys("tab"), key.WithHelp("tab", "focus cycle")),
|
Tab: key.NewBinding(key.WithKeys("tab"), key.WithHelp("tab", "focus cycle")),
|
||||||
|
ToggleRail: key.NewBinding(key.WithKeys("ctrl+b"), key.WithHelp("ctrl+b", "toggle tag rail")),
|
||||||
}
|
}
|
||||||
|
|||||||
+119
-22
@@ -65,6 +65,7 @@ type focusPane int
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
focusCapture focusPane = iota
|
focusCapture focusPane = iota
|
||||||
|
focusTagRail
|
||||||
focusList
|
focusList
|
||||||
focusDetail
|
focusDetail
|
||||||
)
|
)
|
||||||
@@ -83,10 +84,12 @@ type model struct {
|
|||||||
filter filterModel
|
filter filterModel
|
||||||
promote promoteModel
|
promote promoteModel
|
||||||
absorb absorbModel
|
absorb absorbModel
|
||||||
|
tagRail tagRailModel
|
||||||
showHelp bool
|
showHelp bool
|
||||||
|
|
||||||
focus focusPane
|
focus focusPane
|
||||||
splitDetail bool
|
splitDetail bool
|
||||||
|
showTagRail bool
|
||||||
|
|
||||||
filterTag string
|
filterTag string
|
||||||
confirmID string
|
confirmID string
|
||||||
@@ -107,11 +110,13 @@ func newModel(store *db.Store) model {
|
|||||||
state: stateList,
|
state: stateList,
|
||||||
mode: modeStream,
|
mode: modeStream,
|
||||||
focus: focusCapture,
|
focus: focusCapture,
|
||||||
|
showTagRail: true,
|
||||||
list: newListModel(),
|
list: newListModel(),
|
||||||
cards: newCardsModel(),
|
cards: newCardsModel(),
|
||||||
detail: newDetailModel(),
|
detail: newDetailModel(),
|
||||||
input: inp,
|
input: inp,
|
||||||
filter: newFilterModel(),
|
filter: newFilterModel(),
|
||||||
|
tagRail: newTagRailModel(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +136,7 @@ func (m *model) setStatus(msg string) tea.Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m model) Init() tea.Cmd {
|
func (m model) Init() tea.Cmd {
|
||||||
return tea.Batch(loadEntities(m.store, m.listParams()), m.input.ti.Focus())
|
return tea.Batch(loadEntities(m.store, m.listParams()), loadRailTags(m.store), m.input.ti.Focus())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m model) listParams() db.ListParams {
|
func (m model) listParams() db.ListParams {
|
||||||
@@ -188,6 +193,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.splitDetail = false
|
m.splitDetail = false
|
||||||
m.focus = focusList
|
m.focus = focusList
|
||||||
}
|
}
|
||||||
|
if !m.railVisible() && m.focus == focusTagRail {
|
||||||
|
m.focus = focusList
|
||||||
|
}
|
||||||
m.recalcSizes()
|
m.recalcSizes()
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
@@ -203,13 +211,18 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.err = nil
|
m.err = nil
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
|
case railTagsLoadedMsg:
|
||||||
|
m.tagRail.setTags(msg.tags)
|
||||||
|
m.tagRail.activeTag = m.filterTag
|
||||||
|
return m, nil
|
||||||
|
|
||||||
case entityCreatedMsg:
|
case entityCreatedMsg:
|
||||||
m.input.clearText()
|
m.input.clearText()
|
||||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), m.setStatus("created"))
|
return m, tea.Batch(loadEntities(m.store, m.listParams()), loadRailTags(m.store), m.setStatus("created"))
|
||||||
|
|
||||||
case entityDeletedMsg:
|
case entityDeletedMsg:
|
||||||
m.state = stateList
|
m.state = stateList
|
||||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), m.setStatus("deleted"))
|
return m, tea.Batch(loadEntities(m.store, m.listParams()), loadRailTags(m.store), m.setStatus("deleted"))
|
||||||
|
|
||||||
case entityUpdatedMsg:
|
case entityUpdatedMsg:
|
||||||
if m.state == stateDetail {
|
if m.state == stateDetail {
|
||||||
@@ -229,7 +242,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
|
|
||||||
case entityAbsorbedMsg:
|
case entityAbsorbedMsg:
|
||||||
m.state = stateList
|
m.state = stateList
|
||||||
return m, tea.Batch(loadEntities(m.store, m.listParams()), m.setStatus("absorbed"))
|
return m, tea.Batch(loadEntities(m.store, m.listParams()), loadRailTags(m.store), m.setStatus("absorbed"))
|
||||||
|
|
||||||
case absorbSourcesLoadedMsg:
|
case absorbSourcesLoadedMsg:
|
||||||
m.absorb = newAbsorbModel(msg.targetID)
|
m.absorb = newAbsorbModel(msg.targetID)
|
||||||
@@ -248,6 +261,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
|
|
||||||
case tagsLoadedMsg:
|
case tagsLoadedMsg:
|
||||||
m.filter.setTags(msg.tags)
|
m.filter.setTags(msg.tags)
|
||||||
|
m.tagRail.setTags(msg.tags)
|
||||||
m.state = stateTagFilter
|
m.state = stateTagFilter
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
@@ -352,13 +366,50 @@ func (m model) updateCapture(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m model) updateBrowse(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
func (m model) updateBrowse(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
|
// Tag rail focus handling
|
||||||
|
if m.focus == focusTagRail && m.state == stateList {
|
||||||
|
switch msg.String() {
|
||||||
|
case "j", "k", "up", "down":
|
||||||
|
m.tagRail = m.tagRail.update(msg.String())
|
||||||
|
return m, nil
|
||||||
|
case "enter":
|
||||||
|
tag := m.tagRail.selectedTag()
|
||||||
|
if tag != "" {
|
||||||
|
if tag == m.filterTag {
|
||||||
|
m.filterTag = ""
|
||||||
|
m.tagRail.activeTag = ""
|
||||||
|
} else {
|
||||||
|
m.filterTag = tag
|
||||||
|
m.tagRail.activeTag = tag
|
||||||
|
}
|
||||||
|
m.focus = focusList
|
||||||
|
return m, loadEntities(m.store, m.listParams())
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
case "l", "tab", "esc":
|
||||||
|
m.focus = focusList
|
||||||
|
return m, nil
|
||||||
|
case "ctrl+b":
|
||||||
|
m.showTagRail = false
|
||||||
|
m.focus = focusList
|
||||||
|
m.recalcSizes()
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ctrl+b toggle from any browse focus
|
||||||
|
if msg.String() == "ctrl+b" && m.state == stateList {
|
||||||
|
m.showTagRail = !m.showTagRail
|
||||||
|
if !m.railVisible() && m.focus == focusTagRail {
|
||||||
|
m.focus = focusList
|
||||||
|
}
|
||||||
|
m.recalcSizes()
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
if m.splitDetail && m.state == stateList {
|
if m.splitDetail && m.state == stateList {
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "tab":
|
case "tab":
|
||||||
if m.focus == focusList {
|
|
||||||
m.focus = focusDetail
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
cmd := m.setFocus(focusCapture)
|
cmd := m.setFocus(focusCapture)
|
||||||
return m, cmd
|
return m, cmd
|
||||||
case "l":
|
case "l":
|
||||||
@@ -371,6 +422,10 @@ func (m model) updateBrowse(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
m.focus = focusList
|
m.focus = focusList
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
if m.focus == focusList && m.railVisible() {
|
||||||
|
m.focus = focusTagRail
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
case "esc":
|
case "esc":
|
||||||
if m.focus == focusDetail {
|
if m.focus == focusDetail {
|
||||||
m.focus = focusList
|
m.focus = focusList
|
||||||
@@ -491,6 +546,13 @@ func (m model) updateBrowse(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
cmd := m.setFocus(focusCapture)
|
cmd := m.setFocus(focusCapture)
|
||||||
return m, cmd
|
return m, cmd
|
||||||
|
|
||||||
|
case "h":
|
||||||
|
if m.state == stateList && m.railVisible() && m.focus == focusList {
|
||||||
|
m.focus = focusTagRail
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
|
||||||
case "a":
|
case "a":
|
||||||
if m.state == stateList {
|
if m.state == stateList {
|
||||||
cmd := m.setFocus(focusCapture)
|
cmd := m.setFocus(focusCapture)
|
||||||
@@ -775,6 +837,13 @@ func (m model) View() string {
|
|||||||
} else {
|
} else {
|
||||||
content = listContent
|
content = listContent
|
||||||
}
|
}
|
||||||
|
if m.railVisible() {
|
||||||
|
rw := m.railWidth()
|
||||||
|
ch := m.contentHeight()
|
||||||
|
rail := lipgloss.NewStyle().Width(rw).Height(ch).Render(m.tagRail.view(m.focus == focusTagRail))
|
||||||
|
sep := m.renderSeparator()
|
||||||
|
content = lipgloss.JoinHorizontal(lipgloss.Top, rail, sep, content)
|
||||||
|
}
|
||||||
case stateDetail:
|
case stateDetail:
|
||||||
content = m.detail.view(m.width)
|
content = m.detail.view(m.width)
|
||||||
case stateTagFilter:
|
case stateTagFilter:
|
||||||
@@ -794,18 +863,23 @@ func (m model) View() string {
|
|||||||
return header + "\n" + content + "\n" + captureBar + "\n" + statusLine
|
return header + "\n" + content + "\n" + captureBar + "\n" + statusLine
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m model) listWidth() int {
|
||||||
|
if m.splitDetail {
|
||||||
|
lw, _ := m.splitWidths()
|
||||||
|
return lw
|
||||||
|
}
|
||||||
|
w := m.width - m.railWidth()
|
||||||
|
if m.railVisible() {
|
||||||
|
w--
|
||||||
|
}
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
func (m model) listContent() string {
|
func (m model) listContent() string {
|
||||||
|
lw := m.listWidth()
|
||||||
if m.mode == modeCards {
|
if m.mode == modeCards {
|
||||||
lw := m.width
|
|
||||||
if m.splitDetail {
|
|
||||||
lw, _ = m.splitWidths()
|
|
||||||
}
|
|
||||||
return m.cards.view(lw)
|
return m.cards.view(lw)
|
||||||
}
|
}
|
||||||
lw := m.width
|
|
||||||
if m.splitDetail {
|
|
||||||
lw, _ = m.splitWidths()
|
|
||||||
}
|
|
||||||
return m.list.view(lw)
|
return m.list.view(lw)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -859,26 +933,49 @@ func (m model) contentHeight() int {
|
|||||||
|
|
||||||
func (m *model) recalcSizes() {
|
func (m *model) recalcSizes() {
|
||||||
ch := m.contentHeight()
|
ch := m.contentHeight()
|
||||||
|
lw := m.listWidth()
|
||||||
if m.isSplit() && m.splitDetail {
|
if m.isSplit() && m.splitDetail {
|
||||||
lw, rw := m.splitWidths()
|
_, rw := m.splitWidths()
|
||||||
m.list.setSize(lw, ch)
|
m.list.setSize(lw, ch)
|
||||||
m.cards.setSize(lw, ch)
|
m.cards.setSize(lw, ch)
|
||||||
m.detail.setSize(rw, ch)
|
m.detail.setSize(rw, ch)
|
||||||
} else {
|
} else {
|
||||||
m.list.setSize(m.width, ch)
|
m.list.setSize(lw, ch)
|
||||||
m.cards.setSize(m.width, ch)
|
m.cards.setSize(lw, ch)
|
||||||
m.detail.setSize(m.width, ch)
|
m.detail.setSize(lw, ch)
|
||||||
}
|
}
|
||||||
m.filter.setHeight(ch)
|
m.filter.setHeight(ch)
|
||||||
|
if m.railVisible() {
|
||||||
|
m.tagRail.setSize(m.railWidth(), ch)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m model) isSplit() bool {
|
func (m model) isSplit() bool {
|
||||||
return m.width >= 100
|
return m.width >= 100
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m model) railVisible() bool {
|
||||||
|
return m.showTagRail && m.width >= 100
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) railWidth() int {
|
||||||
|
if !m.railVisible() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
w := m.width * 18 / 100
|
||||||
|
if w < 16 {
|
||||||
|
w = 16
|
||||||
|
}
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
func (m model) splitWidths() (int, int) {
|
func (m model) splitWidths() (int, int) {
|
||||||
left := m.width * 40 / 100
|
avail := m.width - m.railWidth()
|
||||||
right := m.width - left - 1
|
if m.railVisible() {
|
||||||
|
avail--
|
||||||
|
}
|
||||||
|
left := avail * 40 / 100
|
||||||
|
right := avail - left - 1
|
||||||
return left, right
|
return left, right
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ func contextHints(m model) []hint {
|
|||||||
switch m.focus {
|
switch m.focus {
|
||||||
case focusCapture:
|
case focusCapture:
|
||||||
return []hint{{"enter", "submit"}, {"esc", "browse"}, {"?…", "search"}, {"-", "todo"}, {"@", "event"}}
|
return []hint{{"enter", "submit"}, {"esc", "browse"}, {"?…", "search"}, {"-", "todo"}, {"@", "event"}}
|
||||||
|
case focusTagRail:
|
||||||
|
return []hint{{"j/k", "nav"}, {"enter", "filter"}, {"l", "list"}, {"ctrl+b", "hide"}}
|
||||||
case focusDetail:
|
case focusDetail:
|
||||||
if m.splitDetail {
|
if m.splitDetail {
|
||||||
return []hint{{"h", "list"}, {"c", "copy"}, {"e", "edit"}, {"p", "promote"}, {"!", "pin"}, {"tab", "capture"}}
|
return []hint{{"h", "list"}, {"c", "copy"}, {"e", "edit"}, {"p", "promote"}, {"!", "pin"}, {"tab", "capture"}}
|
||||||
|
|||||||
@@ -129,4 +129,18 @@ var (
|
|||||||
|
|
||||||
hintDescStyle = lipgloss.NewStyle().
|
hintDescStyle = lipgloss.NewStyle().
|
||||||
Foreground(dim)
|
Foreground(dim)
|
||||||
|
|
||||||
|
railHeaderStyle = lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(dim)
|
||||||
|
|
||||||
|
railTagStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"})
|
||||||
|
|
||||||
|
railActiveTagStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"}).
|
||||||
|
Bold(true)
|
||||||
|
|
||||||
|
railCountStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(dim)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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