feat(tui): add bubbletea terminal UI #30

Merged
lerko merged 12 commits from feat/tui into main 2026-05-20 01:16:57 +00:00
7 changed files with 413 additions and 95 deletions
Showing only changes of commit a141b2fd4f - Show all commits
+5
View File
@@ -50,6 +50,11 @@ func renderHelp(width, height int) string {
{"enter", "copy resolved"}, {"enter", "copy resolved"},
{"esc", "cancel"}, {"esc", "cancel"},
}}, }},
{"Split View", [][2]string{
{"l", "focus detail pane"},
{"h", "focus list pane"},
{"esc", "close detail / back"},
}},
{"Global", [][2]string{ {"Global", [][2]string{
{"?", "toggle help"}, {"?", "toggle help"},
{"q / ctrl+c", "quit"}, {"q / ctrl+c", "quit"},
+102 -3
View File
@@ -1,6 +1,9 @@
package tui package tui
import ( import (
"fmt"
"strings"
"github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
@@ -16,8 +19,9 @@ type inputResult struct {
} }
type inputModel struct { type inputModel struct {
ti textinput.Model ti textinput.Model
active bool active bool
preview *parse.Result
} }
func newInputModel() inputModel { func newInputModel() inputModel {
@@ -37,6 +41,7 @@ func (i *inputModel) reset() {
i.active = false i.active = false
i.ti.SetValue("") i.ti.SetValue("")
i.ti.Blur() i.ti.Blur()
i.preview = nil
} }
func (i inputModel) submit() *inputResult { func (i inputModel) submit() *inputResult {
@@ -83,9 +88,103 @@ func (i inputModel) submit() *inputResult {
func (i inputModel) updateKey(msg tea.KeyMsg) inputModel { func (i inputModel) updateKey(msg tea.KeyMsg) inputModel {
i.ti, _ = i.ti.Update(msg) i.ti, _ = i.ti.Update(msg)
val := i.ti.Value()
if val != "" {
parsed, err := parse.Parse(val)
if err == nil {
i.preview = parsed
} else {
i.preview = nil
}
} else {
i.preview = nil
}
return i return i
} }
func (i inputModel) view(width int) string { func (i inputModel) view(width int) string {
return i.ti.View() var b strings.Builder
b.WriteString(drawerBorderStyle.Render(strings.Repeat("─", width)))
b.WriteString("\n")
b.WriteString(i.ti.View())
b.WriteString("\n")
b.WriteString(drawerHintsStyle.Render("enter:submit esc:cancel ?:search -:todo @:event !:reminder"))
b.WriteString("\n")
b.WriteString(i.renderPreview(width))
return b.String()
}
func (i inputModel) renderPreview(width int) string {
if i.preview == nil {
return drawerPreviewStyle.Render("")
}
p := i.preview
if p.Query {
q := "?"
if p.Body != "" {
q += p.Body
}
for _, t := range p.FilterTags {
q += " #" + t
}
return drawerPreviewStyle.Render("search: " + q)
}
glyph := glyphForParsed(p.Glyph)
body := p.Body
if p.Title != nil {
body = *p.Title
}
var parts []string
parts = append(parts, glyph, body)
for _, t := range p.Tags {
parts = append(parts, tagStyle.Render("#"+t))
}
if p.Pin {
parts = append(parts, pinnedStyle.Render("•"))
}
if p.CardSuffix != nil {
parts = append(parts, affordanceStyle.Render(*p.CardSuffix))
}
line := strings.Join(parts, " ")
maxW := width - 4
if maxW > 0 && len(stripAnsi(line)) > maxW {
line = truncate(line, maxW)
}
return drawerPreviewStyle.Render(line)
}
func glyphForParsed(glyph string) string {
switch glyph {
case "todo":
return "○"
case "event":
return "◇"
case "reminder":
return "△"
default:
return "—"
}
}
func drawerLines() int {
return 3
}
// formatPreviewEntity builds a preview string showing how the entity will appear
func formatPreviewEntity(p *parse.Result) string {
if p == nil {
return ""
}
glyph := glyphForParsed(p.Glyph)
body := p.Body
if p.Title != nil {
body = *p.Title
}
return fmt.Sprintf("%s %s", glyph, body)
} }
+56 -52
View File
@@ -3,59 +3,63 @@ package tui
import "github.com/charmbracelet/bubbles/key" import "github.com/charmbracelet/bubbles/key"
type keyMap struct { type keyMap struct {
Up key.Binding Up key.Binding
Down key.Binding Down key.Binding
Enter key.Binding Enter key.Binding
Back key.Binding Back key.Binding
Add key.Binding Add key.Binding
Delete key.Binding Delete key.Binding
Quit key.Binding Quit key.Binding
Help key.Binding Help key.Binding
PageUp key.Binding PageUp key.Binding
PageDn key.Binding PageDn key.Binding
Top key.Binding Top key.Binding
Bottom key.Binding Bottom key.Binding
Todo key.Binding Todo key.Binding
Pin key.Binding Pin key.Binding
Filter key.Binding Filter key.Binding
Promote key.Binding Promote key.Binding
Demote key.Binding Demote key.Binding
Copy key.Binding Copy key.Binding
Edit key.Binding Edit key.Binding
Stream key.Binding Stream key.Binding
Cards key.Binding Cards key.Binding
Sort key.Binding Sort key.Binding
Intent key.Binding Intent key.Binding
Absorb key.Binding Absorb key.Binding
Run key.Binding Run key.Binding
Fill key.Binding Fill key.Binding
FocusLeft key.Binding
FocusRight key.Binding
} }
var keys = keyMap{ var keys = keyMap{
Up: key.NewBinding(key.WithKeys("up", "k"), key.WithHelp("↑/k", "up")), Up: key.NewBinding(key.WithKeys("up", "k"), key.WithHelp("↑/k", "up")),
Down: key.NewBinding(key.WithKeys("down", "j"), key.WithHelp("↓/j", "down")), Down: key.NewBinding(key.WithKeys("down", "j"), key.WithHelp("↓/j", "down")),
Enter: key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "view")), Enter: key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "view")),
Back: key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "back")), Back: key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "back")),
Add: key.NewBinding(key.WithKeys("a"), key.WithHelp("a", "add")), Add: key.NewBinding(key.WithKeys("a"), key.WithHelp("a", "add")),
Delete: key.NewBinding(key.WithKeys("d"), key.WithHelp("d", "delete")), Delete: key.NewBinding(key.WithKeys("d"), key.WithHelp("d", "delete")),
Quit: key.NewBinding(key.WithKeys("q", "ctrl+c"), key.WithHelp("q", "quit")), Quit: key.NewBinding(key.WithKeys("q", "ctrl+c"), key.WithHelp("q", "quit")),
Help: key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "help")), Help: key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "help")),
PageUp: key.NewBinding(key.WithKeys("pgup", "ctrl+u"), key.WithHelp("pgup", "page up")), PageUp: key.NewBinding(key.WithKeys("pgup", "ctrl+u"), key.WithHelp("pgup", "page up")),
PageDn: key.NewBinding(key.WithKeys("pgdown", "ctrl+d"), key.WithHelp("pgdn", "page down")), PageDn: key.NewBinding(key.WithKeys("pgdown", "ctrl+d"), key.WithHelp("pgdn", "page down")),
Top: key.NewBinding(key.WithKeys("home", "g"), key.WithHelp("home", "top")), Top: key.NewBinding(key.WithKeys("home", "g"), key.WithHelp("home", "top")),
Bottom: key.NewBinding(key.WithKeys("end", "G"), key.WithHelp("end", "bottom")), Bottom: key.NewBinding(key.WithKeys("end", "G"), key.WithHelp("end", "bottom")),
Todo: key.NewBinding(key.WithKeys("x"), key.WithHelp("x", "toggle todo")), Todo: key.NewBinding(key.WithKeys("x"), key.WithHelp("x", "toggle todo")),
Pin: key.NewBinding(key.WithKeys("!"), key.WithHelp("!", "toggle pin")), Pin: key.NewBinding(key.WithKeys("!"), key.WithHelp("!", "toggle pin")),
Filter: key.NewBinding(key.WithKeys("#"), key.WithHelp("#", "filter tag")), Filter: key.NewBinding(key.WithKeys("#"), key.WithHelp("#", "filter tag")),
Promote: key.NewBinding(key.WithKeys("p"), key.WithHelp("p", "promote")), Promote: key.NewBinding(key.WithKeys("p"), key.WithHelp("p", "promote")),
Demote: key.NewBinding(key.WithKeys("D"), key.WithHelp("D", "demote")), Demote: key.NewBinding(key.WithKeys("D"), key.WithHelp("D", "demote")),
Copy: key.NewBinding(key.WithKeys("c"), key.WithHelp("c", "copy")), Copy: key.NewBinding(key.WithKeys("c"), key.WithHelp("c", "copy")),
Edit: key.NewBinding(key.WithKeys("e"), key.WithHelp("e", "edit")), Edit: key.NewBinding(key.WithKeys("e"), key.WithHelp("e", "edit")),
Stream: key.NewBinding(key.WithKeys("1"), key.WithHelp("1", "stream")), Stream: key.NewBinding(key.WithKeys("1"), key.WithHelp("1", "stream")),
Cards: key.NewBinding(key.WithKeys("2"), key.WithHelp("2", "cards")), Cards: key.NewBinding(key.WithKeys("2"), key.WithHelp("2", "cards")),
Sort: key.NewBinding(key.WithKeys("s"), key.WithHelp("s", "sort")), Sort: key.NewBinding(key.WithKeys("s"), key.WithHelp("s", "sort")),
Intent: key.NewBinding(key.WithKeys("tab"), key.WithHelp("tab", "intent")), Intent: key.NewBinding(key.WithKeys("tab"), key.WithHelp("tab", "intent")),
Absorb: key.NewBinding(key.WithKeys("m"), key.WithHelp("m", "absorb")), Absorb: key.NewBinding(key.WithKeys("m"), key.WithHelp("m", "absorb")),
Run: key.NewBinding(key.WithKeys("r"), key.WithHelp("r", "run checklist")), Run: key.NewBinding(key.WithKeys("r"), key.WithHelp("r", "run checklist")),
Fill: key.NewBinding(key.WithKeys("f"), key.WithHelp("f", "fill template")), Fill: key.NewBinding(key.WithKeys("f"), key.WithHelp("f", "fill template")),
FocusLeft: key.NewBinding(key.WithKeys("h"), key.WithHelp("h", "focus list")),
FocusRight: key.NewBinding(key.WithKeys("l"), key.WithHelp("l", "focus detail")),
} }
+22 -30
View File
@@ -93,6 +93,8 @@ func (l listModel) update(msg tea.KeyMsg) listModel {
return l return l
} }
const dateGutterWidth = 9
func (l listModel) view(width int) string { func (l listModel) view(width int) string {
ents := l.displayEntities() ents := l.displayEntities()
if len(ents) == 0 { if len(ents) == 0 {
@@ -100,22 +102,24 @@ func (l listModel) view(width int) string {
} }
groups := groupByDate(ents) groups := groupByDate(ents)
entityWidth := width - 4 - dateGutterWidth
type displayLine struct { type displayLine struct {
text string text string
entityIdx int entityIdx int
isHeader bool
} }
var lines []displayLine var lines []displayLine
entityIdx := 0 entityIdx := 0
for _, g := range groups { for _, g := range groups {
lines = append(lines, displayLine{ for i, e := range g.entities {
text: dateHeaderStyle.Render("── " + g.label + " ──"), var gutter string
isHeader: true, if i == 0 {
}) gutter = gutterStyle.Render(padRight(g.label, 6) + " │ ")
for _, e := range g.entities { } else {
line := renderEntity(e, width-4) gutter = gutterStyle.Render(" │ ")
}
line := gutter + renderEntity(e, entityWidth)
lines = append(lines, displayLine{ lines = append(lines, displayLine{
text: line, text: line,
entityIdx: entityIdx, entityIdx: entityIdx,
@@ -124,21 +128,17 @@ func (l listModel) view(width int) string {
} }
} }
cursorLine := l.cursorDisplayLine(groups)
visible := l.visibleCount() visible := l.visibleCount()
offset := 0 offset := 0
if cursorLine >= visible { if l.cursor >= visible {
offset = cursorLine - visible + 1 offset = l.cursor - visible + 1
} }
var b strings.Builder var b strings.Builder
end := min(offset+visible, len(lines)) end := min(offset+visible, len(lines))
for i := offset; i < end; i++ { for i := offset; i < end; i++ {
dl := lines[i] dl := lines[i]
if dl.isHeader { if dl.entityIdx == l.cursor {
b.WriteString(dl.text)
} else if dl.entityIdx == l.cursor {
b.WriteString(selectedItemStyle.Render(" " + dl.text)) b.WriteString(selectedItemStyle.Render(" " + dl.text))
} else { } else {
b.WriteString(listItemStyle.Render(dl.text)) b.WriteString(listItemStyle.Render(dl.text))
@@ -151,22 +151,6 @@ func (l listModel) view(width int) string {
return b.String() return b.String()
} }
func (l listModel) cursorDisplayLine(groups []dateGroup) int {
line := 0
entityIdx := 0
for _, g := range groups {
line++
for range g.entities {
if entityIdx == l.cursor {
return line
}
line++
entityIdx++
}
}
return 0
}
func (l listModel) visibleCount() int { func (l listModel) visibleCount() int {
if l.height <= 0 { if l.height <= 0 {
return 20 return 20
@@ -203,6 +187,14 @@ func formatDateLabel(t time.Time) string {
return strings.ToLower(t.Format("Jan 2")) return strings.ToLower(t.Format("Jan 2"))
} }
func padRight(s string, n int) string {
r := []rune(s)
if len(r) >= n {
return string(r[:n])
}
return s + strings.Repeat(" ", n-len(r))
}
func renderEntity(e *db.Entity, maxWidth int) string { func renderEntity(e *db.Entity, maxWidth int) string {
glyphStr := display.DisplayGlyph(e.Glyph, e.CardType) glyphStr := display.DisplayGlyph(e.Glyph, e.CardType)
style := glyphStyle style := glyphStyle
+204 -9
View File
@@ -2,8 +2,10 @@ package tui
import ( import (
"fmt" "fmt"
"strings"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/lerko/nib/internal/db" "github.com/lerko/nib/internal/db"
) )
@@ -57,6 +59,13 @@ func (s cardsSort) next() cardsSort {
} }
} }
type focusPane int
const (
focusList focusPane = iota
focusDetail
)
type model struct { type model struct {
store *db.Store store *db.Store
state viewState state viewState
@@ -73,6 +82,9 @@ type model struct {
absorb absorbModel absorb absorbModel
showHelp bool showHelp bool
focus focusPane
splitDetail bool
filterTag string filterTag string
confirmID string confirmID string
cardsSort cardsSort cardsSort cardsSort
@@ -155,10 +167,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
m.width = msg.Width m.width = msg.Width
m.height = msg.Height m.height = msg.Height
m.list.setSize(m.width, m.contentHeight()) if !m.isSplit() && m.splitDetail {
m.cards.setSize(m.width, m.contentHeight()) m.state = stateDetail
m.detail.setSize(m.width, m.contentHeight()) m.splitDetail = false
m.filter.setHeight(m.contentHeight()) m.focus = focusList
}
m.recalcSizes()
return m, nil return m, nil
case entitiesLoadedMsg: case entitiesLoadedMsg:
@@ -176,6 +190,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case entityCreatedMsg: case entityCreatedMsg:
m.state = stateList m.state = stateList
m.input.reset() m.input.reset()
m.recalcSizes()
m.status = "created" m.status = "created"
return m, loadEntities(m.store, m.listParams()) return m, loadEntities(m.store, m.listParams())
@@ -279,6 +294,91 @@ func (m model) updateKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m, nil return m, nil
} }
if m.splitDetail && m.state == stateList {
switch msg.String() {
case "l":
if m.focus == focusList {
m.focus = focusDetail
return m, nil
}
case "h":
if m.focus == focusDetail {
m.focus = focusList
return m, nil
}
case "esc":
if m.focus == focusDetail {
m.focus = focusList
return m, nil
}
m.splitDetail = false
m.recalcSizes()
return m, nil
}
if m.focus == focusDetail {
switch msg.String() {
case "j", "k", "up", "down", "pgup", "pgdown", "ctrl+u", "ctrl+d":
var cmd tea.Cmd
m.detail, cmd = m.detail.update(msg)
return m, cmd
case "c":
if m.detail.entity != nil {
return m, copyToClipboard(m.store, m.detail.entity)
}
return m, nil
case "e":
if m.detail.entity != nil && m.detail.mode == detailPreview {
return m, editInEditor(m.store, m.detail.entity)
}
return m, nil
case "p":
if m.detail.entity != nil && m.detail.entity.CardType == nil {
m.promote = newPromoteModel(m.detail.entity.ID, m.detail.entity.Body)
m.state = statePromote
m.splitDetail = false
m.recalcSizes()
return m, nil
}
return m, nil
case "D":
if m.detail.entity != nil && m.detail.entity.CardType != nil {
return m, demoteEntity(m.store, m.detail.entity.ID)
}
return m, nil
case "!":
if m.detail.entity != nil {
return m, pinEntity(m.store, m.detail.entity)
}
return m, nil
case "r":
if m.detail.entity != nil && m.detail.mode == detailPreview {
if m.detail.entity.CardType != nil && *m.detail.entity.CardType == db.CardChecklist {
m.detail.run = newRunModel(m.detail.entity.ID, m.detail.entity.CardData)
m.detail.mode = detailRun
m.splitDetail = false
m.state = stateDetail
m.recalcSizes()
return m, nil
}
}
return m, nil
case "f":
if m.detail.entity != nil && m.detail.mode == detailPreview {
if m.detail.entity.CardType != nil && *m.detail.entity.CardType == db.CardTemplate {
m.detail.fill = newFillModel(m.detail.entity.ID, m.detail.entity.Body)
m.detail.mode = detailFill
m.splitDetail = false
m.state = stateDetail
m.recalcSizes()
return m, m.detail.fill.ti.Focus()
}
}
return m, nil
}
}
}
switch msg.String() { switch msg.String() {
case "ctrl+c": case "ctrl+c":
return m, tea.Quit return m, tea.Quit
@@ -333,6 +433,7 @@ func (m model) updateKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
if m.state == stateList { if m.state == stateList {
m.state = stateInput m.state = stateInput
m.input.focus() m.input.focus()
m.recalcSizes()
return m, m.input.ti.Focus() return m, m.input.ti.Focus()
} }
@@ -344,10 +445,29 @@ func (m model) updateKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
cmd = persistSteps(m.store, m.detail.run.entityID, m.detail.run.stepsJSON()) cmd = persistSteps(m.store, m.detail.run.entityID, m.detail.run.stepsJSON())
} }
m.detail.mode = detailPreview m.detail.mode = detailPreview
if m.isSplit() {
m.state = stateList
m.splitDetail = true
m.focus = focusList
m.recalcSizes()
}
return m, cmd return m, cmd
} }
if m.detail.mode == detailFill { if m.detail.mode == detailFill {
m.detail.mode = detailPreview m.detail.mode = detailPreview
if m.isSplit() {
m.state = stateList
m.splitDetail = true
m.focus = focusList
m.recalcSizes()
}
return m, nil
}
if m.isSplit() {
m.state = stateList
m.splitDetail = true
m.focus = focusList
m.recalcSizes()
return m, nil return m, nil
} }
m.state = stateList m.state = stateList
@@ -482,7 +602,13 @@ func (m model) updateKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
if m.state == stateList { if m.state == stateList {
if e := m.selectedEntity(); e != nil { if e := m.selectedEntity(); e != nil {
m.detail.setEntity(e) m.detail.setEntity(e)
m.state = stateDetail if m.isSplit() {
m.splitDetail = true
m.focus = focusDetail
m.recalcSizes()
} else {
m.state = stateDetail
}
} }
} }
return m, nil return m, nil
@@ -495,6 +621,11 @@ func (m model) updateKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
} else { } else {
m.list = m.list.update(msg) m.list = m.list.update(msg)
} }
if m.splitDetail {
if e := m.selectedEntity(); e != nil {
m.detail.setEntity(e)
}
}
case stateDetail: case stateDetail:
var cmd tea.Cmd var cmd tea.Cmd
m.detail, cmd = m.detail.update(msg) m.detail, cmd = m.detail.update(msg)
@@ -508,6 +639,7 @@ func (m model) updateInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
case "esc": case "esc":
m.state = stateList m.state = stateList
m.input.reset() m.input.reset()
m.recalcSizes()
return m, nil return m, nil
case "enter": case "enter":
result := m.input.submit() result := m.input.submit()
@@ -519,6 +651,7 @@ func (m model) updateInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.searchTags = result.tags m.searchTags = result.tags
m.state = stateList m.state = stateList
m.input.reset() m.input.reset()
m.recalcSizes()
m.applySearch() m.applySearch()
return m, nil return m, nil
} }
@@ -601,10 +734,16 @@ func (m model) View() string {
var content string var content string
switch m.state { switch m.state {
case stateList, stateInput, stateConfirm: case stateList, stateInput, stateConfirm:
if m.mode == modeCards { listContent := m.listContent()
content = m.cards.view(m.width) if m.splitDetail {
lw, rw := m.splitWidths()
ch := m.contentHeight()
left := lipgloss.NewStyle().Width(lw).Height(ch).Render(listContent)
sep := m.renderSeparator()
right := lipgloss.NewStyle().Width(rw).Height(ch).Render(m.detail.view(rw))
content = lipgloss.JoinHorizontal(lipgloss.Top, left, sep, right)
} else { } else {
content = m.list.view(m.width) content = listContent
} }
case stateDetail: case stateDetail:
content = m.detail.view(m.width) content = m.detail.view(m.width)
@@ -622,6 +761,21 @@ func (m model) View() string {
return header + "\n" + content + "\n" + footer return header + "\n" + content + "\n" + footer
} }
func (m model) listContent() string {
if m.mode == modeCards {
lw := m.width
if m.splitDetail {
lw, _ = m.splitWidths()
}
return m.cards.view(lw)
}
lw := m.width
if m.splitDetail {
lw, _ = m.splitWidths()
}
return m.list.view(lw)
}
func (m model) headerView() string { func (m model) headerView() string {
header := titleStyle.Render("nib") header := titleStyle.Render("nib")
@@ -678,7 +832,48 @@ func (m model) footerView() string {
} }
func (m model) contentHeight() int { func (m model) contentHeight() int {
return m.height - 3 return m.height - 3 - m.drawerHeight()
}
func (m model) drawerHeight() int {
if m.state == stateInput {
return drawerLines()
}
return 0
}
func (m *model) recalcSizes() {
ch := m.contentHeight()
if m.isSplit() && m.splitDetail {
lw, rw := m.splitWidths()
m.list.setSize(lw, ch)
m.cards.setSize(lw, ch)
m.detail.setSize(rw, ch)
} else {
m.list.setSize(m.width, ch)
m.cards.setSize(m.width, ch)
m.detail.setSize(m.width, ch)
}
m.filter.setHeight(ch)
}
func (m model) isSplit() bool {
return m.width >= 100
}
func (m model) splitWidths() (int, int) {
left := m.width * 40 / 100
right := m.width - left - 1
return left, right
}
func (m model) renderSeparator() string {
ch := m.contentHeight()
lines := make([]string, ch)
for i := range lines {
lines[i] = "│"
}
return separatorStyle.Render(strings.Join(lines, "\n"))
} }
func (m model) selectedEntity() *db.Entity { func (m model) selectedEntity() *db.Entity {
+7 -1
View File
@@ -47,7 +47,7 @@ func contextHints(m model) string {
return "p:promote D:demote c:copy e:edit r:run f:fill !:pin esc:back" return "p:promote D:demote c:copy e:edit r:run f:fill !:pin esc:back"
} }
case stateInput: case stateInput:
return "enter:submit esc:cancel" return ""
case stateTagFilter: case stateTagFilter:
return "j/k:nav enter:select esc:cancel" return "j/k:nav enter:select esc:cancel"
case stateConfirm: case stateConfirm:
@@ -57,6 +57,12 @@ func contextHints(m model) string {
case stateAbsorb: case stateAbsorb:
return "j/k:nav enter:absorb esc:cancel" return "j/k:nav enter:absorb esc:cancel"
default: default:
if m.splitDetail {
if m.focus == focusDetail {
return "h:list c:copy e:edit p:promote D:demote !:pin esc:back"
}
return "l:detail a:add d:del #:filter esc:close ?:help q:quit"
}
if m.mode == modeCards { if m.mode == modeCards {
return "1:stream 2:cards s:sort tab:intent a:add ?:help q:quit" return "1:stream 2:cards s:sort tab:intent a:add ?:help q:quit"
} }
+17
View File
@@ -105,4 +105,21 @@ var (
searchPillStyle = lipgloss.NewStyle(). searchPillStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#E06C75", Dark: "#E06C75"}). Foreground(lipgloss.AdaptiveColor{Light: "#E06C75", Dark: "#E06C75"}).
Bold(true) Bold(true)
gutterStyle = lipgloss.NewStyle().
Foreground(dim)
drawerBorderStyle = lipgloss.NewStyle().
Foreground(dim)
drawerHintsStyle = lipgloss.NewStyle().
Foreground(dim).
PaddingLeft(2)
drawerPreviewStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#555555", Dark: "#AAAAAA"}).
PaddingLeft(2)
separatorStyle = lipgloss.NewStyle().
Foreground(dim)
) )