Merge pull request 'feat(tui): add tag autocomplete and query composition' (#43) from feat/tag-autocomplete-query-compose into main
CI / test (push) Successful in 2m17s
CI / test (push) Successful in 2m17s
Reviewed-on: #43
This commit was merged in pull request #43.
This commit is contained in:
+53
-12
@@ -4,19 +4,23 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
Body string
|
||||
Glyph string
|
||||
Title *string
|
||||
Description *string
|
||||
TimeAnchor *string
|
||||
Tags []string
|
||||
FilterTags []string
|
||||
CardSuffix *string
|
||||
Pin bool
|
||||
Query bool
|
||||
Body string
|
||||
Glyph string
|
||||
Title *string
|
||||
Description *string
|
||||
TimeAnchor *string
|
||||
Tags []string
|
||||
FilterTags []string
|
||||
CardSuffix *string
|
||||
Pin bool
|
||||
Query bool
|
||||
QueryDateFrom *string
|
||||
QueryDateTo *string
|
||||
QueryCardType *string
|
||||
}
|
||||
|
||||
var validCardTypes = map[string]string{
|
||||
@@ -66,11 +70,48 @@ func Parse(input string) (*Result, error) {
|
||||
r.Glyph = ""
|
||||
tokens := strings.Fields(remaining)
|
||||
var bodyParts []string
|
||||
now := time.Now()
|
||||
for _, tok := range tokens {
|
||||
if strings.HasPrefix(tok, "#") && len(tok) > 1 && !strings.HasPrefix(tok, "##") {
|
||||
switch {
|
||||
case strings.HasPrefix(tok, "#") && len(tok) > 1 && !strings.HasPrefix(tok, "##"):
|
||||
tag := strings.ToLower(tok[1:])
|
||||
r.FilterTags = append(r.FilterTags, tag)
|
||||
} else {
|
||||
case tok == "@today":
|
||||
d := now.Format("2006-01-02")
|
||||
r.QueryDateFrom = &d
|
||||
r.QueryDateTo = &d
|
||||
case tok == "@yesterday":
|
||||
d := now.AddDate(0, 0, -1).Format("2006-01-02")
|
||||
r.QueryDateFrom = &d
|
||||
r.QueryDateTo = &d
|
||||
case tok == "@week":
|
||||
d := now.AddDate(0, 0, -7).Format("2006-01-02")
|
||||
r.QueryDateFrom = &d
|
||||
case tok == "@month":
|
||||
d := now.AddDate(0, -1, 0).Format("2006-01-02")
|
||||
r.QueryDateFrom = &d
|
||||
case strings.HasPrefix(tok, ">") && strings.HasSuffix(tok, "d"):
|
||||
if n, err := strconv.Atoi(tok[1 : len(tok)-1]); err == nil && n > 0 {
|
||||
d := now.AddDate(0, 0, -n).Format("2006-01-02")
|
||||
r.QueryDateTo = &d
|
||||
} else {
|
||||
bodyParts = append(bodyParts, tok)
|
||||
}
|
||||
case strings.HasPrefix(tok, "<") && strings.HasSuffix(tok, "d"):
|
||||
if n, err := strconv.Atoi(tok[1 : len(tok)-1]); err == nil && n > 0 {
|
||||
d := now.AddDate(0, 0, -n).Format("2006-01-02")
|
||||
r.QueryDateFrom = &d
|
||||
} else {
|
||||
bodyParts = append(bodyParts, tok)
|
||||
}
|
||||
case strings.HasPrefix(tok, "^") && len(tok) > 1:
|
||||
suffix := tok[1:]
|
||||
if ct, ok := validCardTypes[suffix]; ok {
|
||||
r.QueryCardType = &ct
|
||||
} else {
|
||||
bodyParts = append(bodyParts, tok)
|
||||
}
|
||||
default:
|
||||
bodyParts = append(bodyParts, tok)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,6 +158,67 @@ func TestParse(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseQueryComposition(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantBody string
|
||||
wantTags []string
|
||||
wantDateFrom bool
|
||||
wantDateTo bool
|
||||
wantCardType *string
|
||||
}{
|
||||
{"today", "?@today", "", nil, true, true, nil},
|
||||
{"yesterday", "?@yesterday", "", nil, true, true, nil},
|
||||
{"week", "?@week", "", nil, true, false, nil},
|
||||
{"month", "?@month", "", nil, true, false, nil},
|
||||
{"newer than", "?<7d", "", nil, true, false, nil},
|
||||
{"older than", "?>30d", "", nil, false, true, nil},
|
||||
{"card type snippet", "?^snippet", "", nil, false, false, sp("snippet")},
|
||||
{"card type shorthand", "?^c", "", nil, false, false, sp("snippet")},
|
||||
{"card type checklist", "?^checklist", "", nil, false, false, sp("checklist")},
|
||||
{"invalid card type stays as body", "?^bogus", "^bogus", nil, false, false, nil},
|
||||
{"combined text and date", "?deploy @today", "deploy", nil, true, true, nil},
|
||||
{"combined tags and date", "?#ops @week", "", []string{"ops"}, true, false, nil},
|
||||
{"combined all", "?deploy #ops @week ^snippet", "deploy", []string{"ops"}, true, false, sp("snippet")},
|
||||
{"invalid age stays as body", "?>abcd", ">abcd", nil, false, false, nil},
|
||||
{"zero days stays as body", "?>0d", ">0d", nil, false, false, nil},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Parse(tt.input)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !got.Query {
|
||||
t.Fatal("expected Query=true")
|
||||
}
|
||||
if got.Body != tt.wantBody {
|
||||
t.Errorf("body: got %q, want %q", got.Body, tt.wantBody)
|
||||
}
|
||||
if !tagsEq(got.FilterTags, tt.wantTags) {
|
||||
t.Errorf("tags: got %v, want %v", got.FilterTags, tt.wantTags)
|
||||
}
|
||||
if tt.wantDateFrom && got.QueryDateFrom == nil {
|
||||
t.Error("expected QueryDateFrom to be set")
|
||||
}
|
||||
if !tt.wantDateFrom && got.QueryDateFrom != nil {
|
||||
t.Errorf("expected QueryDateFrom nil, got %v", *got.QueryDateFrom)
|
||||
}
|
||||
if tt.wantDateTo && got.QueryDateTo == nil {
|
||||
t.Error("expected QueryDateTo to be set")
|
||||
}
|
||||
if !tt.wantDateTo && got.QueryDateTo != nil {
|
||||
t.Errorf("expected QueryDateTo nil, got %v", *got.QueryDateTo)
|
||||
}
|
||||
if !ptrEq(got.QueryCardType, tt.wantCardType) {
|
||||
t.Errorf("card type: got %v, want %v", strPtr(got.QueryCardType), strPtr(tt.wantCardType))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func ptrEq(a, b *string) bool {
|
||||
if a == nil && b == nil {
|
||||
return true
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/lerko/nib/internal/db"
|
||||
)
|
||||
|
||||
const maxSuggestions = 5
|
||||
|
||||
type autocompleteModel struct {
|
||||
suggestions []string
|
||||
cursor int
|
||||
active bool
|
||||
prefix string
|
||||
tokenStart int
|
||||
tokenEnd int
|
||||
}
|
||||
|
||||
func (a *autocompleteModel) moveUp() {
|
||||
if a.cursor > 0 {
|
||||
a.cursor--
|
||||
}
|
||||
}
|
||||
|
||||
func (a *autocompleteModel) moveDown() {
|
||||
if a.cursor < len(a.suggestions)-1 {
|
||||
a.cursor++
|
||||
}
|
||||
}
|
||||
|
||||
func (a autocompleteModel) selected() string {
|
||||
if len(a.suggestions) == 0 || a.cursor >= len(a.suggestions) {
|
||||
return ""
|
||||
}
|
||||
return a.suggestions[a.cursor]
|
||||
}
|
||||
|
||||
func (a autocompleteModel) visibleCount() int {
|
||||
if len(a.suggestions) > maxSuggestions {
|
||||
return maxSuggestions
|
||||
}
|
||||
return len(a.suggestions)
|
||||
}
|
||||
|
||||
func (a autocompleteModel) view(width int) string {
|
||||
if !a.active || len(a.suggestions) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
n := a.visibleCount()
|
||||
for i := 0; i < n; i++ {
|
||||
tag := "#" + a.suggestions[i]
|
||||
if i == a.cursor {
|
||||
b.WriteString(acSelectedStyle.Render(" › " + tag))
|
||||
} else {
|
||||
b.WriteString(acItemStyle.Render(" " + tag))
|
||||
}
|
||||
if i < n-1 {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
if len(a.suggestions) > maxSuggestions {
|
||||
b.WriteString("\n")
|
||||
b.WriteString(acItemStyle.Render(" …"))
|
||||
}
|
||||
|
||||
box := lipgloss.NewStyle().
|
||||
Width(min(30, width)).
|
||||
Render(b.String())
|
||||
return box
|
||||
}
|
||||
|
||||
func tagTokenAtCursor(val string, cursorPos int) (tokenStart, tokenEnd int, prefix string, ok bool) {
|
||||
if cursorPos > len(val) {
|
||||
cursorPos = len(val)
|
||||
}
|
||||
|
||||
start := cursorPos
|
||||
for start > 0 && val[start-1] != ' ' {
|
||||
start--
|
||||
}
|
||||
|
||||
if start >= len(val) || val[start] != '#' {
|
||||
return 0, 0, "", false
|
||||
}
|
||||
|
||||
end := cursorPos
|
||||
for end < len(val) && val[end] != ' ' {
|
||||
end++
|
||||
}
|
||||
|
||||
prefix = strings.ToLower(val[start+1 : cursorPos])
|
||||
return start, end, prefix, true
|
||||
}
|
||||
|
||||
func filterTagSuggestions(tags []db.TagCount, prefix string) []string {
|
||||
if prefix == "" {
|
||||
return nil
|
||||
}
|
||||
prefix = strings.ToLower(prefix)
|
||||
var result []string
|
||||
for _, tc := range tags {
|
||||
lower := strings.ToLower(tc.Tag)
|
||||
if strings.HasPrefix(lower, prefix) && lower != prefix {
|
||||
result = append(result, tc.Tag)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/lerko/nib/internal/db"
|
||||
)
|
||||
|
||||
func TestTagTokenAtCursor(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
val string
|
||||
cursor int
|
||||
wantStart int
|
||||
wantEnd int
|
||||
wantPfx string
|
||||
wantOk bool
|
||||
}{
|
||||
{"mid tag cursor after a", "hello #par world", 9, 6, 10, "pa", true},
|
||||
{"end of tag", "hello #par world", 10, 6, 10, "par", true},
|
||||
{"end of input", "hello #parenting", 16, 6, 16, "parenting", true},
|
||||
{"start of tag just hash", "hello # world", 7, 6, 7, "", true},
|
||||
{"not in tag", "hello world", 5, 0, 0, "", false},
|
||||
{"tag at start", "#ops stuff", 4, 0, 4, "ops", true},
|
||||
{"cursor at hash", "#ops", 1, 0, 4, "", true},
|
||||
{"multiple tags second", "hello #ops #inf", 15, 11, 15, "inf", true},
|
||||
{"empty string", "", 0, 0, 0, "", false},
|
||||
{"cursor past end", "#ops", 10, 0, 4, "ops", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
start, end, pfx, ok := tagTokenAtCursor(tt.val, tt.cursor)
|
||||
if ok != tt.wantOk {
|
||||
t.Fatalf("ok = %v, want %v", ok, tt.wantOk)
|
||||
}
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if start != tt.wantStart || end != tt.wantEnd {
|
||||
t.Fatalf("range = [%d,%d), want [%d,%d)", start, end, tt.wantStart, tt.wantEnd)
|
||||
}
|
||||
if pfx != tt.wantPfx {
|
||||
t.Fatalf("prefix = %q, want %q", pfx, tt.wantPfx)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterTagSuggestions(t *testing.T) {
|
||||
tags := []db.TagCount{
|
||||
{Tag: "ops", Count: 5},
|
||||
{Tag: "ops-deploy", Count: 3},
|
||||
{Tag: "infra", Count: 2},
|
||||
{Tag: "ops-team", Count: 1},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
prefix string
|
||||
want []string
|
||||
}{
|
||||
{"empty prefix", "", nil},
|
||||
{"exact match excluded", "ops", []string{"ops-deploy", "ops-team"}},
|
||||
{"partial match", "op", []string{"ops", "ops-deploy", "ops-team"}},
|
||||
{"no match", "zzz", nil},
|
||||
{"case insensitive", "OP", []string{"ops", "ops-deploy", "ops-team"}},
|
||||
{"single match", "inf", []string{"infra"}},
|
||||
{"full match excluded", "infra", nil},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := filterTagSuggestions(tags, tt.prefix)
|
||||
if len(got) != len(tt.want) {
|
||||
t.Fatalf("got %v, want %v", got, tt.want)
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != tt.want[i] {
|
||||
t.Fatalf("got %v, want %v", got, tt.want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,18 @@ func renderHelp(width, height int) string {
|
||||
{"Capture Bar", [][2]string{
|
||||
{"enter", "submit (or browse if empty)"},
|
||||
{"?…", "search (type ?query)"},
|
||||
{"#…", "tag (autocomplete with tab)"},
|
||||
{"-", "todo prefix"},
|
||||
{"@", "event prefix"},
|
||||
{"!", "reminder prefix"},
|
||||
}},
|
||||
{"Query Operators", [][2]string{
|
||||
{"?text", "substring search"},
|
||||
{"?#tag1 #tag2", "filter by tags (AND)"},
|
||||
{"?@today @week", "date filter (@yesterday @month)"},
|
||||
{"?<7d >30d", "newer/older than N days"},
|
||||
{"?^snippet", "card type filter"},
|
||||
}},
|
||||
{"Navigation", [][2]string{
|
||||
{"j/k ↑/↓", "move cursor"},
|
||||
{"g/G home/end", "top / bottom"},
|
||||
|
||||
+27
-8
@@ -11,10 +11,13 @@ import (
|
||||
)
|
||||
|
||||
type inputResult struct {
|
||||
entity *db.Entity
|
||||
query bool
|
||||
body string
|
||||
tags []string
|
||||
entity *db.Entity
|
||||
query bool
|
||||
body string
|
||||
tags []string
|
||||
dateFrom *string
|
||||
dateTo *string
|
||||
cardType *db.CardType
|
||||
}
|
||||
|
||||
type inputModel struct {
|
||||
@@ -47,11 +50,18 @@ func (i inputModel) submit() *inputResult {
|
||||
}
|
||||
|
||||
if parsed.Query {
|
||||
return &inputResult{
|
||||
query: true,
|
||||
body: parsed.Body,
|
||||
tags: parsed.FilterTags,
|
||||
r := &inputResult{
|
||||
query: true,
|
||||
body: parsed.Body,
|
||||
tags: parsed.FilterTags,
|
||||
dateFrom: parsed.QueryDateFrom,
|
||||
dateTo: parsed.QueryDateTo,
|
||||
}
|
||||
if parsed.QueryCardType != nil {
|
||||
ct := db.CardType(*parsed.QueryCardType)
|
||||
r.cardType = &ct
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
e := &db.Entity{
|
||||
@@ -120,6 +130,15 @@ func (i inputModel) previewText() string {
|
||||
for _, t := range p.FilterTags {
|
||||
q += " #" + t
|
||||
}
|
||||
if p.QueryDateFrom != nil {
|
||||
q += " from:" + *p.QueryDateFrom
|
||||
}
|
||||
if p.QueryDateTo != nil {
|
||||
q += " to:" + *p.QueryDateTo
|
||||
}
|
||||
if p.QueryCardType != nil {
|
||||
q += " ^" + *p.QueryCardType
|
||||
}
|
||||
return "search: " + q
|
||||
}
|
||||
|
||||
|
||||
+135
-18
@@ -79,26 +79,30 @@ type model struct {
|
||||
width int
|
||||
height int
|
||||
|
||||
list listModel
|
||||
cards cardsModel
|
||||
detail detailModel
|
||||
input inputModel
|
||||
filter filterModel
|
||||
promote promoteModel
|
||||
absorb absorbModel
|
||||
tagRail tagRailModel
|
||||
stumble stumbleModel
|
||||
showHelp bool
|
||||
list listModel
|
||||
cards cardsModel
|
||||
detail detailModel
|
||||
input inputModel
|
||||
filter filterModel
|
||||
promote promoteModel
|
||||
absorb absorbModel
|
||||
tagRail tagRailModel
|
||||
stumble stumbleModel
|
||||
showHelp bool
|
||||
autocomplete autocompleteModel
|
||||
|
||||
focus focusPane
|
||||
splitDetail bool
|
||||
showTagRail bool
|
||||
|
||||
filterTag string
|
||||
confirmID string
|
||||
cardsSort cardsSort
|
||||
searchQuery string
|
||||
searchTags []string
|
||||
filterTag string
|
||||
confirmID string
|
||||
cardsSort cardsSort
|
||||
searchQuery string
|
||||
searchTags []string
|
||||
queryDateFrom *string
|
||||
queryDateTo *string
|
||||
queryCardType *db.CardType
|
||||
|
||||
status string
|
||||
statusSeq int
|
||||
@@ -149,6 +153,15 @@ func (m model) listParams() db.ListParams {
|
||||
if m.filterTag != "" {
|
||||
p.Tag = &m.filterTag
|
||||
}
|
||||
if m.queryDateFrom != nil {
|
||||
p.From = m.queryDateFrom
|
||||
}
|
||||
if m.queryDateTo != nil {
|
||||
p.To = m.queryDateTo
|
||||
}
|
||||
if m.queryCardType != nil {
|
||||
p.CardTypeFilter = m.queryCardType
|
||||
}
|
||||
if m.mode == modeCards {
|
||||
p.CardsOnly = true
|
||||
switch m.cardsSort {
|
||||
@@ -167,7 +180,7 @@ func (m model) listParams() db.ListParams {
|
||||
}
|
||||
|
||||
func (m model) hasSearch() bool {
|
||||
return m.searchQuery != "" || len(m.searchTags) > 0
|
||||
return m.searchQuery != "" || len(m.searchTags) > 0 || m.queryDateFrom != nil || m.queryDateTo != nil || m.queryCardType != nil
|
||||
}
|
||||
|
||||
func (m *model) applySearch() {
|
||||
@@ -363,6 +376,10 @@ func (m model) updateKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
func (m model) updateCapture(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "enter":
|
||||
if m.autocomplete.active {
|
||||
m.acceptAutocomplete()
|
||||
return m, nil
|
||||
}
|
||||
val := m.input.ti.Value()
|
||||
if val == "" {
|
||||
cmd := m.setFocus(focusList)
|
||||
@@ -375,23 +392,92 @@ func (m model) updateCapture(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
if result.query {
|
||||
m.searchQuery = result.body
|
||||
m.searchTags = result.tags
|
||||
m.queryDateFrom = result.dateFrom
|
||||
m.queryDateTo = result.dateTo
|
||||
m.queryCardType = result.cardType
|
||||
m.input.clearText()
|
||||
m.autocomplete.active = false
|
||||
if result.dateFrom != nil || result.dateTo != nil || result.cardType != nil {
|
||||
cmd := m.setFocus(focusList)
|
||||
return m, tea.Batch(cmd, loadEntities(m.store, m.listParams()))
|
||||
}
|
||||
m.applySearch()
|
||||
cmd := m.setFocus(focusList)
|
||||
return m, cmd
|
||||
}
|
||||
if result.entity != nil {
|
||||
m.autocomplete.active = false
|
||||
return m, createEntity(m.store, result.entity)
|
||||
}
|
||||
return m, nil
|
||||
case "esc", "tab":
|
||||
case "tab":
|
||||
if m.autocomplete.active {
|
||||
m.acceptAutocomplete()
|
||||
return m, nil
|
||||
}
|
||||
cmd := m.setFocus(focusList)
|
||||
return m, cmd
|
||||
case "esc":
|
||||
if m.autocomplete.active {
|
||||
m.autocomplete.active = false
|
||||
return m, nil
|
||||
}
|
||||
cmd := m.setFocus(focusList)
|
||||
return m, cmd
|
||||
case "up":
|
||||
if m.autocomplete.active {
|
||||
m.autocomplete.moveUp()
|
||||
return m, nil
|
||||
}
|
||||
case "down":
|
||||
if m.autocomplete.active {
|
||||
m.autocomplete.moveDown()
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
m.input = m.input.updateKey(msg)
|
||||
m.updateAutocompleteSuggestions()
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *model) updateAutocompleteSuggestions() {
|
||||
val := m.input.ti.Value()
|
||||
pos := m.input.ti.Position()
|
||||
start, end, prefix, ok := tagTokenAtCursor(val, pos)
|
||||
if !ok || prefix == "" {
|
||||
m.autocomplete.active = false
|
||||
return
|
||||
}
|
||||
suggestions := filterTagSuggestions(m.tagRail.tags, prefix)
|
||||
if len(suggestions) == 0 {
|
||||
m.autocomplete.active = false
|
||||
return
|
||||
}
|
||||
m.autocomplete.suggestions = suggestions
|
||||
m.autocomplete.prefix = prefix
|
||||
m.autocomplete.tokenStart = start
|
||||
m.autocomplete.tokenEnd = end
|
||||
m.autocomplete.active = true
|
||||
if m.autocomplete.cursor >= len(suggestions) {
|
||||
m.autocomplete.cursor = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) acceptAutocomplete() {
|
||||
if !m.autocomplete.active || len(m.autocomplete.suggestions) == 0 {
|
||||
return
|
||||
}
|
||||
selected := m.autocomplete.selected()
|
||||
if selected == "" {
|
||||
return
|
||||
}
|
||||
val := m.input.ti.Value()
|
||||
newVal := val[:m.autocomplete.tokenStart] + "#" + selected + " " + val[m.autocomplete.tokenEnd:]
|
||||
m.input.ti.SetValue(newVal)
|
||||
m.input.ti.SetCursor(m.autocomplete.tokenStart + 1 + len(selected) + 1)
|
||||
m.autocomplete.active = false
|
||||
}
|
||||
|
||||
func (m model) updateBrowse(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
// Tag rail focus handling
|
||||
if m.focus == focusTagRail && m.state == stateList {
|
||||
@@ -636,9 +722,16 @@ func (m model) updateBrowse(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
if m.state == stateList && m.hasSearch() {
|
||||
hadDBFilters := m.queryDateFrom != nil || m.queryDateTo != nil || m.queryCardType != nil
|
||||
m.searchQuery = ""
|
||||
m.searchTags = nil
|
||||
m.queryDateFrom = nil
|
||||
m.queryDateTo = nil
|
||||
m.queryCardType = nil
|
||||
m.status = ""
|
||||
if hadDBFilters {
|
||||
return m, loadEntities(m.store, m.listParams())
|
||||
}
|
||||
if m.mode == modeCards {
|
||||
m.cards.applyFilter()
|
||||
} else {
|
||||
@@ -953,6 +1046,10 @@ func (m model) View() string {
|
||||
|
||||
content = lipgloss.NewStyle().Width(m.width).Height(m.contentHeight()).Render(content)
|
||||
|
||||
acView := m.autocomplete.view(m.width)
|
||||
if acView != "" {
|
||||
return header + "\n" + content + "\n" + acView + "\n" + captureBar + "\n" + statusLine
|
||||
}
|
||||
return header + "\n" + content + "\n" + captureBar + "\n" + statusLine
|
||||
}
|
||||
|
||||
@@ -994,6 +1091,15 @@ func (m model) headerView() string {
|
||||
for _, t := range m.searchTags {
|
||||
pill += " #" + t
|
||||
}
|
||||
if m.queryDateFrom != nil {
|
||||
pill += " from:" + *m.queryDateFrom
|
||||
}
|
||||
if m.queryDateTo != nil {
|
||||
pill += " to:" + *m.queryDateTo
|
||||
}
|
||||
if m.queryCardType != nil {
|
||||
pill += " ^" + string(*m.queryCardType)
|
||||
}
|
||||
header += " " + searchPillStyle.Render(pill)
|
||||
}
|
||||
|
||||
@@ -1021,7 +1127,18 @@ func (m model) statusLine() string {
|
||||
}
|
||||
|
||||
func (m model) contentHeight() int {
|
||||
return m.height - 4
|
||||
h := m.height - 4
|
||||
if m.autocomplete.active && len(m.autocomplete.suggestions) > 0 {
|
||||
n := m.autocomplete.visibleCount()
|
||||
if len(m.autocomplete.suggestions) > maxSuggestions {
|
||||
n++
|
||||
}
|
||||
h -= n + 1
|
||||
}
|
||||
if h < 1 {
|
||||
h = 1
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func (m *model) recalcSizes() {
|
||||
|
||||
@@ -41,6 +41,8 @@ var (
|
||||
railActiveTagStyle lipgloss.Style
|
||||
railCountStyle lipgloss.Style
|
||||
stumbleAgeStyle lipgloss.Style
|
||||
acSelectedStyle lipgloss.Style
|
||||
acItemStyle lipgloss.Style
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -96,4 +98,6 @@ func applyTheme() {
|
||||
railActiveTagStyle = lipgloss.NewStyle().Foreground(ok).Bold(true)
|
||||
railCountStyle = lipgloss.NewStyle().Foreground(dim)
|
||||
stumbleAgeStyle = lipgloss.NewStyle().Foreground(remind)
|
||||
acSelectedStyle = lipgloss.NewStyle().Foreground(accent).Bold(true)
|
||||
acItemStyle = lipgloss.NewStyle().Foreground(muted)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user