Rewrite TUI as full-screen Bubble Tea two-pane interactive browser
Replace the line-based stdin pagination loop with a charmbracelet/bubbletea
application providing a responsive two-pane layout: a filterable deal list
on the left and a scrollable detail viewport on the right.
Key capabilities:
- Async startup: spinner + skeleton while store/deals are fetched, then
transition to the interactive view. Fatal load errors propagate cleanly.
- Grouped sections: deals are bucketed by category (BOGO first, then by
count descending) with numbered section headers. Jump with [/] for
prev/next and 1-9 for direct section access.
- Inline filter cycling: s (sort: relevance/savings/ending), g (bogo
toggle), c (category cycle), a (department cycle), l (limit cycle),
r (reset to CLI-start defaults). Filters rebuild the list while
preserving cursor position via stable IDs.
- Fuzzy search: / activates bubbletea's built-in list filter. Section
jumps are disabled while a fuzzy filter is active.
- Detail pane: full deal metadata with word-wrapped text, scrollable
via j/k, u/d (half-page), b/f (full page). Tab switches focus.
- Terminal size awareness: minimum 92x24, graceful too-small message.
- JSON mode: tui --json fetches and filters without launching the
interactive UI, consistent with other commands.
New files:
cmd/tui_model.go — Bubble Tea model, view, update, grouping logic
cmd/tui_model_test.go — Tests for sort canonicalization, group ordering,
and category choice building
Dependencies added: charmbracelet/bubbles, charmbracelet/bubbletea.
Transitive deps upgraded: lipgloss, x/ansi, x/cellbuf, x/term, go-colorful,
go-runewidth, sys.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
195
cmd/tui.go
195
cmd/tui.go
@@ -1,13 +1,12 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/tayloree/publix-deals/internal/api"
|
"github.com/tayloree/publix-deals/internal/api"
|
||||||
"github.com/tayloree/publix-deals/internal/display"
|
"github.com/tayloree/publix-deals/internal/display"
|
||||||
@@ -15,11 +14,9 @@ import (
|
|||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
const tuiPageSize = 10
|
|
||||||
|
|
||||||
var tuiCmd = &cobra.Command{
|
var tuiCmd = &cobra.Command{
|
||||||
Use: "tui",
|
Use: "tui",
|
||||||
Short: "Browse deals interactively in the terminal",
|
Short: "Browse deals in a full-screen interactive terminal UI",
|
||||||
Example: ` pubcli tui --zip 33101
|
Example: ` pubcli tui --zip 33101
|
||||||
pubcli tui --store 1425 --category produce --sort ending`,
|
pubcli tui --store 1425 --category produce --sort ending`,
|
||||||
RunE: runTUI,
|
RunE: runTUI,
|
||||||
@@ -34,50 +31,111 @@ func runTUI(cmd *cobra.Command, _ []string) error {
|
|||||||
if err := validateSortMode(); err != nil {
|
if err := validateSortMode(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !flagJSON && !isInteractiveSession(cmd.InOrStdin(), cmd.OutOrStdout()) {
|
|
||||||
return invalidArgsError(
|
|
||||||
"`pubcli tui` requires an interactive terminal",
|
|
||||||
"Use `pubcli --zip 33101 --json` in pipelines.",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
client := api.NewClient()
|
initialOpts := filter.Options{
|
||||||
storeNumber, err := resolveStore(cmd, client)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := client.FetchSavings(cmd.Context(), storeNumber)
|
|
||||||
if err != nil {
|
|
||||||
return upstreamError("fetching deals", err)
|
|
||||||
}
|
|
||||||
if len(resp.Savings) == 0 {
|
|
||||||
return notFoundError(
|
|
||||||
fmt.Sprintf("no deals found for store #%s", storeNumber),
|
|
||||||
"Try another store with --store.",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
items := filter.Apply(resp.Savings, filter.Options{
|
|
||||||
BOGO: flagBogo,
|
BOGO: flagBogo,
|
||||||
Category: flagCategory,
|
Category: flagCategory,
|
||||||
Department: flagDepartment,
|
Department: flagDepartment,
|
||||||
Query: flagQuery,
|
Query: flagQuery,
|
||||||
Sort: flagSort,
|
Sort: flagSort,
|
||||||
Limit: flagLimit,
|
Limit: flagLimit,
|
||||||
})
|
|
||||||
if len(items) == 0 {
|
|
||||||
return notFoundError(
|
|
||||||
"no deals match your filters",
|
|
||||||
"Relax filters like --category/--department/--query.",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if flagJSON {
|
if flagJSON {
|
||||||
|
_, _, rawItems, err := loadTUIData(cmd.Context(), flagStore, flagZip)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
items := filter.Apply(rawItems, initialOpts)
|
||||||
|
if len(items) == 0 {
|
||||||
|
return notFoundError(
|
||||||
|
"no deals match your filters",
|
||||||
|
"Relax filters like --category/--department/--query.",
|
||||||
|
)
|
||||||
|
}
|
||||||
return display.PrintDealsJSON(cmd.OutOrStdout(), items)
|
return display.PrintDealsJSON(cmd.OutOrStdout(), items)
|
||||||
}
|
}
|
||||||
|
|
||||||
return runTUILoop(cmd.OutOrStdout(), cmd.InOrStdin(), storeNumber, items)
|
if !isInteractiveSession(cmd.InOrStdin(), cmd.OutOrStdout()) {
|
||||||
|
return invalidArgsError(
|
||||||
|
"`pubcli tui` requires an interactive terminal",
|
||||||
|
"Use `pubcli --zip 33101 --json` in pipelines.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
model := newLoadingDealsTUIModel(tuiLoadConfig{
|
||||||
|
ctx: cmd.Context(),
|
||||||
|
storeNumber: flagStore,
|
||||||
|
zipCode: flagZip,
|
||||||
|
initialOpts: initialOpts,
|
||||||
|
})
|
||||||
|
|
||||||
|
program := tea.NewProgram(
|
||||||
|
model,
|
||||||
|
tea.WithAltScreen(),
|
||||||
|
tea.WithInput(cmd.InOrStdin()),
|
||||||
|
tea.WithOutput(cmd.OutOrStdout()),
|
||||||
|
)
|
||||||
|
|
||||||
|
finalModel, err := program.Run()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("running tui: %w", err)
|
||||||
|
}
|
||||||
|
if finalState, ok := finalModel.(dealsTUIModel); ok && finalState.fatalErr != nil {
|
||||||
|
return finalState.fatalErr
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveStoreForTUI(ctx context.Context, client *api.Client, storeNumber, zipCode string) (resolvedStoreNumber, storeLabel string, err error) {
|
||||||
|
if storeNumber != "" {
|
||||||
|
return storeNumber, "#" + storeNumber, nil
|
||||||
|
}
|
||||||
|
if zipCode == "" {
|
||||||
|
return "", "", invalidArgsError(
|
||||||
|
"please provide --store NUMBER or --zip ZIPCODE",
|
||||||
|
"pubcli tui --zip 33101",
|
||||||
|
"pubcli tui --store 1425",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
stores, err := client.FetchStores(ctx, zipCode, 1)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", upstreamError("finding stores", err)
|
||||||
|
}
|
||||||
|
if len(stores) == 0 {
|
||||||
|
return "", "", notFoundError(
|
||||||
|
fmt.Sprintf("no Publix stores found near %s", zipCode),
|
||||||
|
"Try a nearby ZIP code.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
store := stores[0]
|
||||||
|
resolvedStoreNumber = api.StoreNumber(store.Key)
|
||||||
|
storeLabel = fmt.Sprintf("#%s — %s (%s, %s)", resolvedStoreNumber, store.Name, store.City, store.State)
|
||||||
|
return resolvedStoreNumber, storeLabel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadTUIData(ctx context.Context, storeNumber, zipCode string) (resolvedStoreNumber, storeLabel string, items []api.SavingItem, err error) {
|
||||||
|
client := api.NewClient()
|
||||||
|
|
||||||
|
resolvedStoreNumber, storeLabel, err = resolveStoreForTUI(ctx, client, storeNumber, zipCode)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.FetchSavings(ctx, resolvedStoreNumber)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", nil, upstreamError("fetching deals", err)
|
||||||
|
}
|
||||||
|
if len(resp.Savings) == 0 {
|
||||||
|
return "", "", nil, notFoundError(
|
||||||
|
fmt.Sprintf("no deals found for store #%s", resolvedStoreNumber),
|
||||||
|
"Try another store with --store.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedStoreNumber, storeLabel, resp.Savings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isInteractiveSession(stdin io.Reader, stdout io.Writer) bool {
|
func isInteractiveSession(stdin io.Reader, stdout io.Writer) bool {
|
||||||
@@ -90,66 +148,3 @@ func isInteractiveSession(stdin io.Reader, stdout io.Writer) bool {
|
|||||||
}
|
}
|
||||||
return isTTY(stdout)
|
return isTTY(stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runTUILoop(out io.Writer, in io.Reader, storeNumber string, items []api.SavingItem) error {
|
|
||||||
reader := bufio.NewReader(in)
|
|
||||||
page := 0
|
|
||||||
totalPages := (len(items)-1)/tuiPageSize + 1
|
|
||||||
|
|
||||||
for {
|
|
||||||
renderTUIPage(out, storeNumber, items, page, totalPages)
|
|
||||||
|
|
||||||
line, err := reader.ReadString('\n')
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
cmd := strings.TrimSpace(strings.ToLower(line))
|
|
||||||
|
|
||||||
switch cmd {
|
|
||||||
case "q", "quit", "exit":
|
|
||||||
return nil
|
|
||||||
case "n", "next":
|
|
||||||
if page < totalPages-1 {
|
|
||||||
page++
|
|
||||||
}
|
|
||||||
case "p", "prev", "previous":
|
|
||||||
if page > 0 {
|
|
||||||
page--
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
idx, convErr := strconv.Atoi(cmd)
|
|
||||||
if convErr != nil || idx < 1 || idx > len(items) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
renderDealDetail(out, items[idx-1], idx, len(items))
|
|
||||||
if _, err := reader.ReadString('\n'); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderTUIPage(out io.Writer, storeNumber string, items []api.SavingItem, page, totalPages int) {
|
|
||||||
fmt.Fprint(out, "\033[H\033[2J")
|
|
||||||
fmt.Fprintf(out, "pubcli tui | store #%s | %d deals | page %d/%d\n\n", storeNumber, len(items), page+1, totalPages)
|
|
||||||
|
|
||||||
start := page * tuiPageSize
|
|
||||||
end := minInt(start+tuiPageSize, len(items))
|
|
||||||
for i := start; i < end; i++ {
|
|
||||||
item := items[i]
|
|
||||||
title := topDealTitle(item)
|
|
||||||
savings := filter.CleanText(filter.Deref(item.Savings))
|
|
||||||
if savings == "" {
|
|
||||||
savings = "-"
|
|
||||||
}
|
|
||||||
fmt.Fprintf(out, "%2d. %s [%s]\n", i+1, title, savings)
|
|
||||||
}
|
|
||||||
fmt.Fprintf(out, "\ncommands: number=details | n=next | p=prev | q=quit\n> ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderDealDetail(out io.Writer, item api.SavingItem, index, total int) {
|
|
||||||
fmt.Fprint(out, "\033[H\033[2J")
|
|
||||||
fmt.Fprintf(out, "deal %d/%d\n\n", index, total)
|
|
||||||
display.PrintDeals(out, []api.SavingItem{item})
|
|
||||||
fmt.Fprint(out, "press Enter to return\n")
|
|
||||||
}
|
|
||||||
|
|||||||
1128
cmd/tui_model.go
Normal file
1128
cmd/tui_model.go
Normal file
File diff suppressed because it is too large
Load Diff
62
cmd/tui_model_test.go
Normal file
62
cmd/tui_model_test.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/tayloree/publix-deals/internal/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func strPtr(value string) *string { return &value }
|
||||||
|
|
||||||
|
func TestCanonicalSortMode(t *testing.T) {
|
||||||
|
assert.Equal(t, "savings", canonicalSortMode("savings"))
|
||||||
|
assert.Equal(t, "ending", canonicalSortMode("end"))
|
||||||
|
assert.Equal(t, "ending", canonicalSortMode("expiry"))
|
||||||
|
assert.Equal(t, "ending", canonicalSortMode("expiration"))
|
||||||
|
assert.Equal(t, "", canonicalSortMode("relevance"))
|
||||||
|
assert.Equal(t, "", canonicalSortMode("unknown"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildGroupedListItems_BogoFirstAndNumberedHeaders(t *testing.T) {
|
||||||
|
deals := []api.SavingItem{
|
||||||
|
{ID: "1", Title: strPtr("Bananas"), Categories: []string{"produce"}},
|
||||||
|
{ID: "2", Title: strPtr("Chicken"), Categories: []string{"meat", "bogo"}},
|
||||||
|
{ID: "3", Title: strPtr("Apples"), Categories: []string{"produce"}},
|
||||||
|
{ID: "4", Title: strPtr("Ground Beef"), Categories: []string{"meat"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
items, starts := buildGroupedListItems(deals)
|
||||||
|
|
||||||
|
assert.NotEmpty(t, items)
|
||||||
|
assert.Equal(t, []int{0, 2, 5}, starts)
|
||||||
|
|
||||||
|
header, ok := items[0].(tuiGroupItem)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "BOGO", header.name)
|
||||||
|
assert.Equal(t, 1, header.ordinal)
|
||||||
|
|
||||||
|
header2, ok := items[2].(tuiGroupItem)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "Produce", header2.name)
|
||||||
|
assert.Equal(t, 2, header2.count)
|
||||||
|
|
||||||
|
header3, ok := items[5].(tuiGroupItem)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "Meat", header3.name)
|
||||||
|
assert.Equal(t, 1, header3.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildCategoryChoices_AlwaysIncludesCurrent(t *testing.T) {
|
||||||
|
deals := []api.SavingItem{
|
||||||
|
{Categories: []string{"produce"}},
|
||||||
|
{Categories: []string{"meat"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
choices := buildCategoryChoices(deals, "seafood")
|
||||||
|
|
||||||
|
assert.Contains(t, choices, "")
|
||||||
|
assert.Contains(t, choices, "produce")
|
||||||
|
assert.Contains(t, choices, "meat")
|
||||||
|
assert.Contains(t, choices, "seafood")
|
||||||
|
}
|
||||||
28
go.mod
28
go.mod
@@ -3,28 +3,40 @@ module github.com/tayloree/publix-deals
|
|||||||
go 1.24.4
|
go 1.24.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/charmbracelet/bubbles v1.0.0
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
|
github.com/spf13/pflag v1.0.9
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
golang.org/x/term v0.30.0
|
golang.org/x/term v0.30.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
|
github.com/clipperhouse/displaywidth v0.9.0 // indirect
|
||||||
|
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/muesli/termenv v0.16.0 // indirect
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/spf13/pflag v1.0.9 // indirect
|
github.com/sahilm/fuzzy v0.1.1 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
golang.org/x/sys v0.31.0 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
|
golang.org/x/text v0.3.8 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
64
go.sum
64
go.sum
@@ -1,34 +1,61 @@
|
|||||||
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
|
||||||
|
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
|
||||||
|
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||||
|
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
||||||
|
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||||
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
||||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||||
|
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
|
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||||
|
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
|
||||||
|
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
|
||||||
|
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||||
|
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
|
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||||
|
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||||
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
|
||||||
|
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||||
@@ -38,13 +65,16 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD
|
|||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||||
|
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||||
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
Reference in New Issue
Block a user