diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e58186..924f972 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ on: - main env: - VERSION_NUMBER: 'v1.9.2' + VERSION_NUMBER: 'v1.9.3' DOCKERHUB_REGISTRY_NAME: 'digitalghostdev/poke-cli' AWS_REGION: 'us-west-2' diff --git a/.gitignore b/.gitignore index 2bf9bec..5d79543 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ web/.coverage # Python card_data/.venv __pycache__/ +.ruff_cache/ # Terraform .terraformrc diff --git a/.goreleaser.yml b/.goreleaser.yml index f5e6bac..475906b 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -14,7 +14,7 @@ builds: - windows - darwin ldflags: - - -s -w -X main.version=v1.9.2 + - -s -w -X main.version=v1.9.3 archives: - formats: [ 'zip' ] diff --git a/Dockerfile b/Dockerfile index 5480709..7f43ac5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ RUN go mod download COPY . . -RUN go build -ldflags "-X main.version=v1.9.2" -o poke-cli . +RUN go build -ldflags "-X main.version=v1.9.3" -o poke-cli . # build 2 FROM --platform=$BUILDPLATFORM alpine:3.23 diff --git a/README.md b/README.md index 50944d4..25f767e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ pokemon-logo

version-label - docker-image-size + docker-image-size ci-status-badge
@@ -99,11 +99,11 @@ Cloudsmith is a fully cloud-based service that lets you easily create, store, an 3. Choose how to interact with the container: * Run a single command and exit: ```bash - docker run --rm -it digitalghostdev/poke-cli:v1.9.2 [subcommand] [flag] + docker run --rm -it digitalghostdev/poke-cli:v1.9.3 [subcommand] [flag] ``` * Enter the container and use its shell: ```bash - docker run --rm -it --name poke-cli --entrypoint /bin/sh digitalghostdev/poke-cli:v1.9.2 -c "cd /app && exec sh" + docker run --rm -it --name poke-cli --entrypoint /bin/sh digitalghostdev/poke-cli:v1.9.3 -c "cd /app && exec sh" # placed into the /app directory, run the program with './poke-cli' # example: ./poke-cli ability swift-swim ``` @@ -112,13 +112,13 @@ Cloudsmith is a fully cloud-based service that lets you easily create, store, an > The `card` command renders TCG card images using your terminal's graphics protocol. When running inside Docker, pass your terminal's environment variables so image rendering works correctly: > ```bash > # Kitty -> docker run --rm -it -e TERM -e KITTY_WINDOW_ID digitalghostdev/poke-cli:v1.9.2 card +> docker run --rm -it -e TERM -e KITTY_WINDOW_ID digitalghostdev/poke-cli:v1.9.3 card > > # WezTerm, iTerm2, Ghostty, Konsole, Rio, Tabby -> docker run --rm -it -e TERM -e TERM_PROGRAM digitalghostdev/poke-cli:v1.9.2 card +> docker run --rm -it -e TERM -e TERM_PROGRAM digitalghostdev/poke-cli:v1.9.3 card > > # Windows Terminal (Sixel) -> docker run --rm -it -e WT_SESSION digitalghostdev/poke-cli:v1.9.2 card +> docker run --rm -it -e WT_SESSION digitalghostdev/poke-cli:v1.9.3 card > ``` > If your terminal is not listed above, image rendering is not supported inside Docker. diff --git a/card_data/pipelines/poke_cli_dbt/dbt_project.yml b/card_data/pipelines/poke_cli_dbt/dbt_project.yml index d684c53..b87090d 100644 --- a/card_data/pipelines/poke_cli_dbt/dbt_project.yml +++ b/card_data/pipelines/poke_cli_dbt/dbt_project.yml @@ -1,5 +1,5 @@ name: 'poke_cli_dbt' -version: '1.9.2' +version: '1.9.3' profile: 'poke_cli_dbt' diff --git a/cli.go b/cli.go index d454fa6..19e7b66 100644 --- a/cli.go +++ b/cli.go @@ -159,14 +159,11 @@ func runCLI(args []string) int { case exists: return cmdFunc() default: - errMessage := styling.ErrorBorder.Render( - styling.ErrorColor.Render("✖ Error!"), - fmt.Sprintf("\n\t%-15s", fmt.Sprintf("'%s' is not a valid command.\n", cmdArg)), - styling.StyleBold.Render("\nCommands:"), - renderCommandList(), - fmt.Sprintf("\n\nAlso run %s for more info!", styling.StyleBold.Render("poke-cli -h")), - ) - output.WriteString(errMessage) + msg := fmt.Sprintf("\t%-15s", fmt.Sprintf("'%s' is not a valid command.\n", cmdArg)) + + styling.StyleBold.Render("\nCommands:") + + renderCommandList() + + fmt.Sprintf("\n\nAlso run %s for more info!", styling.StyleBold.Render("poke-cli -h")) + output.WriteString(utils.FormatError(msg)) fmt.Println(output.String()) diff --git a/cli_test.go b/cli_test.go index 80134c5..91400b3 100644 --- a/cli_test.go +++ b/cli_test.go @@ -132,14 +132,14 @@ func TestRunCLI_VariousCommands(t *testing.T) { args []string expected int }{ - //{"Invalid command", []string{"foobar"}, 1}, + {"Invalid command", []string{"foobar"}, 1}, {"Latest flag long", []string{"--latest"}, 0}, {"Latest flag short", []string{"-l"}, 0}, {"Version flag long", []string{"--version"}, 0}, {"Version flag short", []string{"-v"}, 0}, {"Search command with invalid args", []string{"search", "pokemon", "extra-arg"}, 1}, - //{"Missing Pokémon name", []string{"pokemon"}, 1}, - //{"Another invalid command", []string{"invalid"}, 1}, + {"Missing Pokémon name", []string{"pokemon"}, 1}, + {"Another invalid command", []string{"invalid"}, 1}, } for _, tt := range tests { diff --git a/cmd/ability/ability.go b/cmd/ability/ability.go index df65bb2..e3a9c0e 100644 --- a/cmd/ability/ability.go +++ b/cmd/ability/ability.go @@ -1,6 +1,7 @@ package ability import ( + "errors" "flag" "fmt" "os" @@ -48,9 +49,10 @@ func AbilityCommand() (string, error) { abilityName := strings.ToLower(args[2]) if err := af.FlagSet.Parse(args[3:]); err != nil { + if errors.Is(err, flag.ErrHelp) { + return output.String(), nil + } fmt.Fprintf(&output, "error parsing flags: %v\n", err) - af.FlagSet.Usage() - return output.String(), err } diff --git a/cmd/ability/ability_test.go b/cmd/ability/ability_test.go index 1ce0496..dedfaa5 100644 --- a/cmd/ability/ability_test.go +++ b/cmd/ability/ability_test.go @@ -52,6 +52,12 @@ func TestAbilityCommand(t *testing.T) { args: []string{"ability", "poison-point"}, expectedOutput: utils.LoadGolden(t, "ability_poison_point.golden"), }, + { + name: "Ability invalid flag", + args: []string{"ability", "clear-body", "--bogus"}, + expectedOutput: utils.LoadGolden(t, "ability_invalid_flag.golden"), + wantError: true, + }, } for _, tt := range tests { diff --git a/cmd/berry/berry.go b/cmd/berry/berry.go index 557591e..5b84a68 100644 --- a/cmd/berry/berry.go +++ b/cmd/berry/berry.go @@ -7,9 +7,9 @@ import ( "os" "strings" - "github.com/charmbracelet/bubbles/table" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + "charm.land/bubbles/v2/table" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "github.com/digitalghost-dev/poke-cli/cmd/utils" "github.com/digitalghost-dev/poke-cli/connections" "github.com/digitalghost-dev/poke-cli/styling" @@ -65,7 +65,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var bubbleCmd tea.Cmd switch msg := msg.(type) { - case tea.KeyMsg: + case tea.KeyPressMsg: switch msg.String() { case "esc", "ctrl+c": m.quitting = true @@ -87,9 +87,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // View renders the current UI -func (m model) View() string { +func (m model) View() tea.View { if m.quitting { - return "\n Goodbye! \n" + return tea.NewView("\n Goodbye! \n") } selectedBerry := "" @@ -100,7 +100,7 @@ func (m model) View() string { leftPanel := styling.TypesTableBorder.Render(m.table.View()) rightPanel := lipgloss.NewStyle(). - Width(50). + Width(52). Height(29). Border(lipgloss.RoundedBorder()). BorderForeground(styling.YellowColor). @@ -109,18 +109,18 @@ func (m model) View() string { screen := lipgloss.JoinHorizontal(lipgloss.Top, leftPanel, rightPanel) - return fmt.Sprintf("Highlight a berry!\n%s\n%s", + return tea.NewView(fmt.Sprintf("Highlight a berry!\n%s\n%s", screen, - styling.KeyMenu.Render("↑ (move up) • ↓ (move down)\nctrl+c | esc (quit)")) + styling.KeyMenu.Render("↑ (move up) • ↓ (move down)\nctrl+c | esc (quit)"))) } func tableGeneration() error { namesList, err := connections.QueryBerryData(` - SELECT + SELECT UPPER(SUBSTR(name, 1, 1)) || SUBSTR(name, 2) - FROM - berries - ORDER BY + FROM + berries + ORDER BY name`) if err != nil { log.Fatalf("Failed to get berry names: %v", err) @@ -136,6 +136,7 @@ func tableGeneration() error { table.WithRows(rows), table.WithFocused(true), table.WithHeight(28), + table.WithWidth(16), ) s := table.DefaultStyles() diff --git a/cmd/berry/berry_test.go b/cmd/berry/berry_test.go index d813416..105e014 100644 --- a/cmd/berry/berry_test.go +++ b/cmd/berry/berry_test.go @@ -6,10 +6,10 @@ import ( "testing" "time" - "github.com/charmbracelet/bubbles/table" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/charmbracelet/x/exp/teatest" + "charm.land/bubbles/v2/table" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" + "github.com/charmbracelet/x/exp/teatest/v2" "github.com/digitalghost-dev/poke-cli/styling" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -74,6 +74,7 @@ func TestModelUpdate(t *testing.T) { table.WithRows(rows), table.WithFocused(true), table.WithHeight(5), + table.WithWidth(16), ) m := model{ @@ -124,15 +125,15 @@ func TestModelView(t *testing.T) { } view := m.View() - if view == "" { + if view.Content == "" { t.Errorf("View() should not return empty string for normal state") } // Test quitting state m.quitting = true view = m.View() - if !strings.Contains(view, "Goodbye") { - t.Errorf("View() should contain 'Goodbye' when quitting, got %q", view) + if !strings.Contains(view.Content, "Goodbye") { + t.Errorf("View() should contain 'Goodbye' when quitting, got %q", view.Content) } } @@ -145,6 +146,7 @@ func TestModelViewWithSelectedBerry(t *testing.T) { table.WithRows(rows), table.WithFocused(true), table.WithHeight(5), + table.WithWidth(16), ) m := model{ @@ -161,8 +163,8 @@ func TestModelViewWithSelectedBerry(t *testing.T) { } for _, element := range expectedElements { - if !strings.Contains(view, element) { - t.Errorf("View() should contain %q, got %q", element, view) + if !strings.Contains(view.Content, element) { + t.Errorf("View() should contain %q, got %q", element, view.Content) } } } @@ -182,6 +184,7 @@ func createTestModel() model { table.WithRows(rows), table.WithFocused(true), table.WithHeight(5), + table.WithWidth(16), ) s := table.DefaultStyles() @@ -202,14 +205,14 @@ func TestTableNavigation(t *testing.T) { testModel := teatest.NewTestModel(t, m, teatest.WithInitialTermSize(100, 50)) // Navigate down twice - testModel.Send(tea.KeyMsg{Type: tea.KeyDown}) - testModel.Send(tea.KeyMsg{Type: tea.KeyDown}) + testModel.Send(tea.KeyPressMsg{Code: tea.KeyDown}) + testModel.Send(tea.KeyPressMsg{Code: tea.KeyDown}) // Navigate back up once - testModel.Send(tea.KeyMsg{Type: tea.KeyUp}) + testModel.Send(tea.KeyPressMsg{Code: tea.KeyUp}) // Quit the program - testModel.Send(tea.KeyMsg{Type: tea.KeyCtrlC}) + testModel.Send(tea.KeyPressMsg{Code: 'c', Mod: tea.ModCtrl}) testModel.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond)) final := testModel.FinalModel(t).(model) @@ -229,7 +232,7 @@ func TestTableQuitWithEscape(t *testing.T) { testModel := teatest.NewTestModel(t, m, teatest.WithInitialTermSize(100, 50)) // Quit with escape - testModel.Send(tea.KeyMsg{Type: tea.KeyEsc}) + testModel.Send(tea.KeyPressMsg{Code: tea.KeyEscape}) testModel.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond)) final := testModel.FinalModel(t).(model) @@ -244,7 +247,7 @@ func TestTableInitialSelection(t *testing.T) { testModel := teatest.NewTestModel(t, m, teatest.WithInitialTermSize(100, 50)) // Don't navigate, just quit immediately - testModel.Send(tea.KeyMsg{Type: tea.KeyCtrlC}) + testModel.Send(tea.KeyPressMsg{Code: 'c', Mod: tea.ModCtrl}) testModel.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond)) final := testModel.FinalModel(t).(model) diff --git a/cmd/berry/berryinfo.go b/cmd/berry/berryinfo.go index 3b284df..4aef543 100644 --- a/cmd/berry/berryinfo.go +++ b/cmd/berry/berryinfo.go @@ -5,7 +5,7 @@ import ( "net/http" "strings" - "github.com/charmbracelet/lipgloss" + "charm.land/lipgloss/v2" "github.com/digitalghost-dev/poke-cli/connections" "github.com/digitalghost-dev/poke-cli/styling" "github.com/disintegration/imaging" @@ -95,7 +95,7 @@ func BerryImage(berryName string) string { c2, _ := styling.MakeColor(img.At(x, heightCounter+1)) color2 := lipgloss.Color(c2.Hex()) - styleKey := string(color1) + "_" + string(color2) + styleKey := c1.Hex() + "_" + c2.Hex() style, exists := styleCache[styleKey] if !exists { style = lipgloss.NewStyle().Foreground(color1).Background(color2) diff --git a/cmd/card/card.go b/cmd/card/card.go index 6881ed1..41e53b7 100644 --- a/cmd/card/card.go +++ b/cmd/card/card.go @@ -6,7 +6,7 @@ import ( "os" "strings" - tea "github.com/charmbracelet/bubbletea" + tea "charm.land/bubbletea/v2" "github.com/digitalghost-dev/poke-cli/cmd/utils" ) @@ -37,7 +37,7 @@ func CardCommand() (string, error) { } // Program 1: Series selection - finalModel, err := tea.NewProgram(SeriesList(), tea.WithAltScreen()).Run() + finalModel, err := tea.NewProgram(SeriesList()).Run() if err != nil { return "", fmt.Errorf("error running series selection program: %w", err) } @@ -54,7 +54,7 @@ func CardCommand() (string, error) { return "", fmt.Errorf("error loading sets: %w", err) } - finalSetsModel, err := tea.NewProgram(setsMdl, tea.WithAltScreen()).Run() + finalSetsModel, err := tea.NewProgram(setsMdl).Run() if err != nil { return "", fmt.Errorf("error running sets selection program: %w", err) } @@ -76,7 +76,7 @@ func CardCommand() (string, error) { } for { - finalCardsModel, err := tea.NewProgram(cardsMdl, tea.WithAltScreen()).Run() + finalCardsModel, err := tea.NewProgram(cardsMdl).Run() if err != nil { return "", fmt.Errorf("error running cards program: %w", err) } @@ -89,7 +89,7 @@ func CardCommand() (string, error) { if cardsResult.ViewImage { // Launch image viewer imageURL := cardsResult.ImageMap[cardsResult.SelectedOption] - _, err := tea.NewProgram(ImageRenderer(cardsResult.SelectedOption, imageURL), tea.WithAltScreen()).Run() + _, err := tea.NewProgram(ImageRenderer(cardsResult.SelectedOption, imageURL)).Run() if err != nil { fmt.Fprintf(os.Stderr, "Warning: image viewer error: %v\n", err) } diff --git a/cmd/card/cardlist.go b/cmd/card/cardlist.go index 65c457a..58a7aef 100644 --- a/cmd/card/cardlist.go +++ b/cmd/card/cardlist.go @@ -3,13 +3,14 @@ package card import ( "encoding/json" "fmt" + "image/color" "strings" - "github.com/charmbracelet/bubbles/spinner" - "github.com/charmbracelet/bubbles/table" - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + "charm.land/bubbles/v2/spinner" + "charm.land/bubbles/v2/table" + "charm.land/bubbles/v2/textinput" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "github.com/digitalghost-dev/poke-cli/connections" "github.com/digitalghost-dev/poke-cli/styling" ) @@ -47,11 +48,11 @@ type cardDataMsg struct { } var ( - activeTableSelectedBg = styling.YellowColor - inactiveTableSelectedBg = lipgloss.Color("#808080") + activeTableSelectedBg color.Color = styling.YellowColor + inactiveTableSelectedBg color.Color = lipgloss.Color("#808080") ) -func cardTableStyles(selectedBg lipgloss.Color) table.Styles { +func cardTableStyles(selectedBg color.Color) table.Styles { s := table.DefaultStyles() s.Header = s.Header. BorderStyle(lipgloss.NormalBorder()). @@ -137,7 +138,7 @@ func (m cardsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var bubbleCmd tea.Cmd switch msg := msg.(type) { - case tea.KeyMsg: + case tea.KeyPressMsg: switch msg.String() { case "ctrl+c": m.Quitting = true @@ -185,14 +186,15 @@ func (m cardsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { ti.Placeholder = "type name..." ti.Prompt = "🔎 " ti.CharLimit = 24 - ti.Width = 30 + ti.SetWidth(30) ti.Blur() t := table.New( table.WithColumns([]table.Column{{Title: "Card Name", Width: 35}}), table.WithRows(msg.allRows), table.WithFocused(true), - table.WithHeight(27), + table.WithHeight(25), + table.WithWidth(35), ) styles := cardTableStyles(activeTableSelectedBg) @@ -261,52 +263,55 @@ func applyFilter(m *cardsModel) { m.Table.SetCursor(0) } -func (m cardsModel) View() string { +func (m cardsModel) View() tea.View { + var content string if m.Quitting { - return "\n Quitting card search...\n\n" - } - if m.Error != nil { - return styling.ApiErrorStyle.Render( + content = "\n Quitting card search...\n\n" + } else if m.Error != nil { + content = styling.ApiErrorStyle.Render( "Error loading cards from Supabase:\n" + m.Error.Error() + "\n\n" + "Press ctrl+c or esc to exit.", ) - } - if m.Loading { - return lipgloss.NewStyle().Padding(2).Render( + } else if m.Loading { + content = lipgloss.NewStyle().Padding(2).Render( m.Spinner.View() + " Loading cards...", ) - } - - selectedCard := "" - if row := m.Table.SelectedRow(); len(row) > 0 { - cardName := row[0] - price := m.PriceMap[cardName] - if price == "" { - price = "Price: Not available" + } else { + selectedCard := "" + if row := m.Table.SelectedRow(); len(row) > 0 { + cardName := row[0] + price := m.PriceMap[cardName] + if price == "" { + price = "Price: Not available" + } + illustrator := m.IllustratorMap[cardName] + regulationMark := m.RegulationMarkMap[cardName] + selectedCard = cardName + "\n---\n" + price + "\n---\n" + illustrator + "\n---\n" + regulationMark } - illustrator := m.IllustratorMap[cardName] - regulationMark := m.RegulationMarkMap[cardName] - selectedCard = cardName + "\n---\n" + price + "\n---\n" + illustrator + "\n---\n" + regulationMark - } - leftContent := lipgloss.JoinVertical(lipgloss.Left, m.Search.View(), m.Table.View()) - leftPanel := styling.TypesTableBorder.Render(leftContent) + leftContent := lipgloss.JoinVertical(lipgloss.Left, m.Search.View(), m.Table.View()) + leftPanel := styling.TypesTableBorder.Render(leftContent) - rightPanel := lipgloss.NewStyle(). - Width(40). - Height(29). - Border(lipgloss.RoundedBorder()). - BorderForeground(styling.YellowColor). - Padding(1). - Render(selectedCard) + rightPanel := lipgloss.NewStyle(). + Width(42). + Height(29). + Border(lipgloss.RoundedBorder()). + BorderForeground(styling.YellowColor). + Padding(1). + Render(selectedCard) - screen := lipgloss.JoinHorizontal(lipgloss.Top, leftPanel, rightPanel) + screen := lipgloss.JoinHorizontal(lipgloss.Top, leftPanel, rightPanel) + + content = fmt.Sprintf( + "Highlight a card!\n\nNote: Prices are for normal variations of cards.\n%s\n%s", + screen, + styling.KeyMenu.Render("↑ (move up)\n↓ (move down)\n? (view image)\ntab (toggle search)\nctrl+c | esc (quit)")) + } - return fmt.Sprintf( - "Highlight a card!\n\nNote: Prices are for normal variations of cards.\n%s\n%s", - screen, - styling.KeyMenu.Render("↑ (move up)\n↓ (move down)\n? (view image)\ntab (toggle search)\nctrl+c | esc (quit)")) + v := tea.NewView(content) + v.AltScreen = true + return v } type cardData struct { diff --git a/cmd/card/cardlist_test.go b/cmd/card/cardlist_test.go index 9a982ac..e3a5552 100644 --- a/cmd/card/cardlist_test.go +++ b/cmd/card/cardlist_test.go @@ -5,9 +5,9 @@ import ( "strings" "testing" - "github.com/charmbracelet/bubbles/table" - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" + "charm.land/bubbles/v2/table" + "charm.land/bubbles/v2/textinput" + tea "charm.land/bubbletea/v2" ) func TestCardsModel_Init(t *testing.T) { @@ -31,6 +31,7 @@ func TestCardsModel_Update_EscKey(t *testing.T) { table.WithColumns(columns), table.WithRows(rows), table.WithFocused(true), + table.WithWidth(35), ) model := cardsModel{ @@ -38,7 +39,7 @@ func TestCardsModel_Update_EscKey(t *testing.T) { Quitting: false, } - msg := tea.KeyMsg{Type: tea.KeyEsc} + msg := tea.KeyPressMsg{Code: tea.KeyEscape} newModel, cmd := model.Update(msg) resultModel := newModel.(cardsModel) @@ -63,6 +64,7 @@ func TestCardsModel_Update_CtrlC(t *testing.T) { table.WithColumns(columns), table.WithRows(rows), table.WithFocused(true), + table.WithWidth(35), ) model := cardsModel{ @@ -70,7 +72,7 @@ func TestCardsModel_Update_CtrlC(t *testing.T) { Quitting: false, } - msg := tea.KeyMsg{Type: tea.KeyCtrlC} + msg := tea.KeyPressMsg{Code: 'c', Mod: tea.ModCtrl} newModel, cmd := model.Update(msg) resultModel := newModel.(cardsModel) @@ -92,6 +94,7 @@ func TestCardsModel_Update_TabTogglesSearchFocusAndTableSelectedBackground(t *te table.WithColumns(columns), table.WithRows(rows), table.WithFocused(true), + table.WithWidth(35), ) search := textinput.New() @@ -107,7 +110,7 @@ func TestCardsModel_Update_TabTogglesSearchFocusAndTableSelectedBackground(t *te } // Tab into the search bar. - newModel, _ := model.Update(tea.KeyMsg{Type: tea.KeyTab}) + newModel, _ := model.Update(tea.KeyPressMsg{Code: tea.KeyTab}) m1 := newModel.(cardsModel) if !m1.Search.Focused() { t.Fatal("expected search to be focused after tab") @@ -121,7 +124,7 @@ func TestCardsModel_Update_TabTogglesSearchFocusAndTableSelectedBackground(t *te } // Tab back to the table. - newModel2, _ := m1.Update(tea.KeyMsg{Type: tea.KeyTab}) + newModel2, _ := m1.Update(tea.KeyPressMsg{Code: tea.KeyTab}) m2 := newModel2.(cardsModel) if m2.Search.Focused() { t.Fatal("expected search to be blurred after tabbing back") @@ -146,6 +149,7 @@ func TestCardsModel_Update_ViewImageKey_QuestionMark(t *testing.T) { table.WithColumns(columns), table.WithRows(rows), table.WithFocused(true), + table.WithWidth(35), ) model := cardsModel{ @@ -153,7 +157,7 @@ func TestCardsModel_Update_ViewImageKey_QuestionMark(t *testing.T) { ViewImage: false, } - msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'?'}} + msg := tea.KeyPressMsg{Code: '?', Text: "?"} newModel, cmd := model.Update(msg) resultModel := newModel.(cardsModel) @@ -175,6 +179,7 @@ func TestCardsModel_Update_ViewImageKey_DoesNotOverrideSearch(t *testing.T) { table.WithColumns(columns), table.WithRows(rows), table.WithFocused(true), + table.WithWidth(35), ) search := textinput.New() @@ -186,7 +191,7 @@ func TestCardsModel_Update_ViewImageKey_DoesNotOverrideSearch(t *testing.T) { ViewImage: false, } - msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'?'}} + msg := tea.KeyPressMsg{Code: '?', Text: "?"} newModel, _ := model.Update(msg) resultModel := newModel.(cardsModel) @@ -208,8 +213,8 @@ func TestCardsModel_View_Quitting(t *testing.T) { result := model.View() - if !strings.Contains(result, "Quitting card search") { - t.Errorf("View() when quitting should contain 'Quitting card search', got: %s", result) + if !strings.Contains(result.Content, "Quitting card search") { + t.Errorf("View() when quitting should contain 'Quitting card search', got: %s", result.Content) } } @@ -224,6 +229,7 @@ func TestCardsModel_View_PriceDisplay(t *testing.T) { table.WithColumns(columns), table.WithRows(rows), table.WithFocused(true), + table.WithWidth(35), ) priceMap := map[string]string{ @@ -239,7 +245,7 @@ func TestCardsModel_View_PriceDisplay(t *testing.T) { result := model.View() // The view should include the card name - if !strings.Contains(result, "001/198 - Bulbasaur") { + if !strings.Contains(result.Content, "001/198 - Bulbasaur") { t.Error("View() should display selected card name") } } @@ -255,6 +261,7 @@ func TestCardsModel_View_MissingPrice(t *testing.T) { table.WithColumns(columns), table.WithRows(rows), table.WithFocused(true), + table.WithWidth(35), ) // Empty price map - simulates missing price data @@ -269,7 +276,7 @@ func TestCardsModel_View_MissingPrice(t *testing.T) { result := model.View() // Should show "Price: Not available" when price is missing - if !strings.Contains(result, "Price: Not available") { + if !strings.Contains(result.Content, "Price: Not available") { t.Error("View() should display 'Price: Not available' for cards without pricing") } } @@ -290,8 +297,8 @@ func TestCardsList_ReturnsLoadingModel(t *testing.T) { // View should show loading spinner view := model.View() - if !strings.Contains(view, "Loading cards") { - t.Errorf("expected view to show loading state, got: %s", view) + if !strings.Contains(view.Content, "Loading cards") { + t.Errorf("expected view to show loading state, got: %s", view.Content) } } diff --git a/cmd/card/imageviewer.go b/cmd/card/imageviewer.go index 88ba82f..1de2277 100644 --- a/cmd/card/imageviewer.go +++ b/cmd/card/imageviewer.go @@ -1,9 +1,9 @@ package card import ( - "github.com/charmbracelet/bubbles/spinner" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + "charm.land/bubbles/v2/spinner" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "github.com/digitalghost-dev/poke-cli/styling" ) @@ -63,7 +63,7 @@ func (m imageModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.Spinner, cmd = m.Spinner.Update(msg) return m, cmd - case tea.KeyMsg: + case tea.KeyPressMsg: switch msg.String() { case "ctrl+c", "esc": return m, tea.Quit @@ -72,22 +72,26 @@ func (m imageModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } -func (m imageModel) View() string { +func (m imageModel) View() tea.View { + var content string if m.Loading { - return lipgloss.NewStyle().Padding(2).Render( + content = lipgloss.NewStyle().Padding(2).Render( m.Spinner.View() + "Loading image for \n" + m.CardName, ) - } - if m.Error != nil { + } else if m.Error != nil { // Styling the error message with padding for better readability - return lipgloss.NewStyle(). + content = lipgloss.NewStyle(). Padding(2). BorderStyle(lipgloss.RoundedBorder()). BorderForeground(styling.YellowColor). Render(styling.Red.Render(m.Error.Error())) + } else { + content = m.ImageData } - return m.ImageData + v := tea.NewView(content) + v.AltScreen = true + return v } func ImageRenderer(cardName string, imageURL string) imageModel { diff --git a/cmd/card/imageviewer_test.go b/cmd/card/imageviewer_test.go index e288c1b..d1dcc45 100644 --- a/cmd/card/imageviewer_test.go +++ b/cmd/card/imageviewer_test.go @@ -4,8 +4,8 @@ import ( "strings" "testing" - spinnerpkg "github.com/charmbracelet/bubbles/spinner" - tea "github.com/charmbracelet/bubbletea" + spinnerpkg "charm.land/bubbles/v2/spinner" + tea "charm.land/bubbletea/v2" ) func TestImageModel_Init(t *testing.T) { @@ -24,7 +24,7 @@ func TestImageModel_Update_EscKey(t *testing.T) { } // Test ESC key - msg := tea.KeyMsg{Type: tea.KeyEsc} + msg := tea.KeyPressMsg{Code: tea.KeyEscape} newModel, cmd := model.Update(msg) // Should return quit command @@ -44,7 +44,7 @@ func TestImageModel_Update_CtrlC(t *testing.T) { ImageURL: "test-sixel-data", } - msg := tea.KeyMsg{Type: tea.KeyCtrlC} + msg := tea.KeyPressMsg{Code: 'c', Mod: tea.ModCtrl} _, cmd := model.Update(msg) if cmd == nil { @@ -58,7 +58,7 @@ func TestImageModel_Update_DifferentKey(t *testing.T) { ImageURL: "test-sixel-data", } - msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'a'}} + msg := tea.KeyPressMsg{Code: 'a', Text: "a"} _, cmd := model.Update(msg) if cmd != nil { @@ -72,11 +72,11 @@ func TestImageModel_View_Loading(t *testing.T) { result := model.View() // When loading, should show spinner and card name - if result == "" { + if result.Content == "" { t.Error("View() should not be empty when loading") } // Can't check exact spinner output as it's dynamic, but should contain card name - if !strings.Contains(result, "001/198 - Pineco") { + if !strings.Contains(result.Content, "001/198 - Pineco") { t.Error("View() should contain card name when loading") } } @@ -92,8 +92,8 @@ func TestImageModel_View_Loaded(t *testing.T) { result := model.View() - if result != expectedData { - t.Errorf("View() = %v, want %v", result, expectedData) + if result.Content != expectedData { + t.Errorf("View() = %v, want %v", result.Content, expectedData) } } @@ -107,8 +107,8 @@ func TestImageModel_View_Empty(t *testing.T) { result := model.View() - if result != "" { - t.Errorf("View() with empty ImageData should return empty string, got %v", result) + if result.Content != "" { + t.Errorf("View() with empty ImageData should return empty string, got %v", result.Content) } } diff --git a/cmd/card/serieslist.go b/cmd/card/serieslist.go index 9d7132e..139cf80 100644 --- a/cmd/card/serieslist.go +++ b/cmd/card/serieslist.go @@ -1,8 +1,8 @@ package card import ( - "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" + "charm.land/bubbles/v2/list" + tea "charm.land/bubbletea/v2" "github.com/digitalghost-dev/poke-cli/styling" ) @@ -26,7 +26,7 @@ func (m seriesModel) Init() tea.Cmd { func (m seriesModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { - case tea.KeyMsg: + case tea.KeyPressMsg: switch msg.String() { case "ctrl+c", "esc": m.Quitting = true @@ -50,15 +50,19 @@ func (m seriesModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } -func (m seriesModel) View() string { +func (m seriesModel) View() tea.View { + var content string if m.Quitting { - return "\n Quitting card search...\n\n" - } - if m.Choice != "" { - return styling.QuitTextStyle.Render("Series selected:", m.Choice) + content = "\n Quitting card search...\n\n" + } else if m.Choice != "" { + content = styling.QuitTextStyle.Render("Series selected:", m.Choice) + } else { + content = "\n" + m.List.View() } - return "\n" + m.List.View() + v := tea.NewView(content) + v.AltScreen = true + return v } func SeriesList() seriesModel { diff --git a/cmd/card/serieslist_test.go b/cmd/card/serieslist_test.go index ff295af..edbc97d 100644 --- a/cmd/card/serieslist_test.go +++ b/cmd/card/serieslist_test.go @@ -4,9 +4,9 @@ import ( "testing" "time" - "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/x/exp/teatest" + "charm.land/bubbles/v2/list" + tea "charm.land/bubbletea/v2" + "github.com/charmbracelet/x/exp/teatest/v2" "github.com/digitalghost-dev/poke-cli/styling" ) @@ -37,7 +37,7 @@ func TestSeriesModelQuit(t *testing.T) { testModel := teatest.NewTestModel(t, model, teatest.WithInitialTermSize(80, 24)) // Test ctrl+c quit - testModel.Send(tea.KeyMsg{Type: tea.KeyCtrlC}) + testModel.Send(tea.KeyPressMsg{Code: 'c', Mod: tea.ModCtrl}) testModel.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond)) final := testModel.FinalModel(t).(seriesModel) @@ -59,7 +59,7 @@ func TestSeriesModelEscQuit(t *testing.T) { testModel := teatest.NewTestModel(t, model, teatest.WithInitialTermSize(80, 24)) // Test esc quit - testModel.Send(tea.KeyMsg{Type: tea.KeyEsc}) + testModel.Send(tea.KeyPressMsg{Code: tea.KeyEscape}) testModel.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond)) final := testModel.FinalModel(t).(seriesModel) @@ -81,8 +81,8 @@ func TestSeriesModelSelection(t *testing.T) { testModel := teatest.NewTestModel(t, model, teatest.WithInitialTermSize(80, 24)) // Navigate and select - testModel.Send(tea.KeyMsg{Type: tea.KeyDown}) // Move to second item - testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) // Select it + testModel.Send(tea.KeyPressMsg{Code: tea.KeyDown}) // Move to second item + testModel.Send(tea.KeyPressMsg{Code: tea.KeyEnter}) // Select it testModel.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond)) final := testModel.FinalModel(t).(seriesModel) @@ -124,22 +124,22 @@ func TestSeriesModelView(t *testing.T) { // Test normal view model := seriesModel{List: l} view := model.View() - if view == "" { + if view.Content == "" { t.Errorf("Expected non-empty view, got empty string") } // Test quitting view model.Quitting = true view = model.View() - if view != "\n Quitting card search...\n\n" { - t.Errorf("Expected quitting message, got '%s'", view) + if view.Content != "\n Quitting card search...\n\n" { + t.Errorf("Expected quitting message, got '%s'", view.Content) } // Test choice made view model.Quitting = false model.Choice = "Scarlet & Violet" view = model.View() - if view == "" { + if view.Content == "" { t.Errorf("Expected non-empty view for choice, got empty string") } } diff --git a/cmd/card/setslist.go b/cmd/card/setslist.go index 4c23080..6a2d60f 100644 --- a/cmd/card/setslist.go +++ b/cmd/card/setslist.go @@ -3,10 +3,10 @@ package card import ( "encoding/json" - "github.com/charmbracelet/bubbles/list" - "github.com/charmbracelet/bubbles/spinner" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + "charm.land/bubbles/v2/list" + "charm.land/bubbles/v2/spinner" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "github.com/digitalghost-dev/poke-cli/connections" "github.com/digitalghost-dev/poke-cli/styling" ) @@ -73,7 +73,7 @@ func (m setsModel) Init() tea.Cmd { func (m setsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { - case tea.KeyMsg: + case tea.KeyPressMsg: switch msg.String() { case "ctrl+c", "esc": m.Quitting = true @@ -135,27 +135,29 @@ func (m setsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } -func (m setsModel) View() string { +func (m setsModel) View() tea.View { + var content string if m.Error != nil { - return styling.ApiErrorStyle.Render( + content = styling.ApiErrorStyle.Render( "Error loading sets from Supabase:\n" + m.Error.Error() + "\n\n" + "Press ctrl+c or esc to exit.", ) - } - if m.Choice != "" { - return styling.QuitTextStyle.Render("Set selected:", m.Choice) - } - if m.Loading { - return lipgloss.NewStyle().Padding(2).Render( + } else if m.Choice != "" { + content = styling.QuitTextStyle.Render("Set selected:", m.Choice) + } else if m.Loading { + content = lipgloss.NewStyle().Padding(2).Render( m.Spinner.View() + "Loading sets...", ) - } - if m.Quitting { - return "\n Quitting card search...\n\n" + } else if m.Quitting { + content = "\n Quitting card search...\n\n" + } else { + content = "\n" + m.List.View() } - return "\n" + m.List.View() + v := tea.NewView(content) + v.AltScreen = true + return v } type setData struct { diff --git a/cmd/card/setslist_test.go b/cmd/card/setslist_test.go index f823b0b..daa51e8 100644 --- a/cmd/card/setslist_test.go +++ b/cmd/card/setslist_test.go @@ -5,8 +5,8 @@ import ( "strings" "testing" - "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" + "charm.land/bubbles/v2/list" + tea "charm.land/bubbletea/v2" "github.com/digitalghost-dev/poke-cli/styling" ) @@ -32,7 +32,7 @@ func TestSetsModel_Update_EscKey(t *testing.T) { Quitting: false, } - msg := tea.KeyMsg{Type: tea.KeyEsc} + msg := tea.KeyPressMsg{Code: tea.KeyEscape} newModel, cmd := model.Update(msg) resultModel, ok := newModel.(setsModel) @@ -61,7 +61,7 @@ func TestSetsModel_Update_CtrlC(t *testing.T) { Quitting: false, } - msg := tea.KeyMsg{Type: tea.KeyCtrlC} + msg := tea.KeyPressMsg{Code: 'c', Mod: tea.ModCtrl} newModel, cmd := model.Update(msg) resultModel, ok := newModel.(setsModel) @@ -119,8 +119,8 @@ func TestSetsModel_View_Quitting(t *testing.T) { result := model.View() - if !strings.Contains(result, "Quitting card search") { - t.Errorf("View() when quitting should contain 'Quitting card search', got: %s", result) + if !strings.Contains(result.Content, "Quitting card search") { + t.Errorf("View() when quitting should contain 'Quitting card search', got: %s", result.Content) } } @@ -137,8 +137,8 @@ func TestSetsModel_View_ChoiceMade(t *testing.T) { result := model.View() - if !strings.Contains(result, "Set selected: Scarlet & Violet") { - t.Errorf("View() with choice should contain 'Set selected: Scarlet & Violet', got: %s", result) + if !strings.Contains(result.Content, "Set selected: Scarlet & Violet") { + t.Errorf("View() with choice should contain 'Set selected: Scarlet & Violet', got: %s", result.Content) } } @@ -156,7 +156,7 @@ func TestSetsModel_View_Normal(t *testing.T) { result := model.View() - if result == "" { + if result.Content == "" { t.Error("View() should not return empty string in normal state") } } @@ -178,7 +178,7 @@ func TestSetsModel_Update_EnterKey(t *testing.T) { SetsIDMap: setsIDMap, } - msg := tea.KeyMsg{Type: tea.KeyEnter} + msg := tea.KeyPressMsg{Code: tea.KeyEnter} _, cmd := model.Update(msg) if cmd == nil { @@ -202,7 +202,7 @@ func TestSetsList_Success(t *testing.T) { } // View should show loading spinner - if model.View() == "" { + if model.View().Content == "" { t.Error("model view should render loading state") } } diff --git a/cmd/item/item.go b/cmd/item/item.go index e2cca5c..e2c0a2c 100644 --- a/cmd/item/item.go +++ b/cmd/item/item.go @@ -6,7 +6,7 @@ import ( "os" "strings" - "github.com/charmbracelet/lipgloss" + "charm.land/lipgloss/v2" "github.com/digitalghost-dev/poke-cli/cmd/utils" "github.com/digitalghost-dev/poke-cli/connections" "github.com/digitalghost-dev/poke-cli/structs" @@ -61,11 +61,13 @@ func itemInfoContainer(output *strings.Builder, itemStruct structs.ItemJSONStruc itemCost := fmt.Sprintf("Cost: %d", itemStruct.Cost) itemCategory := "Category: " + styling.CapitalizeResourceName(itemStruct.Category.Name) + isDark := lipgloss.HasDarkBackground(os.Stdin, os.Stdout) + ld := lipgloss.LightDark(isDark) docStyle := lipgloss.NewStyle(). Padding(1, 2). BorderStyle(lipgloss.ThickBorder()). - BorderForeground(lipgloss.AdaptiveColor{Light: "#444", Dark: "#EEE"}). - Width(32) + BorderForeground(ld(lipgloss.Color("#444"), lipgloss.Color("#EEE"))). + Width(34) var flavorTextEntry string var missingData string diff --git a/cmd/move/move.go b/cmd/move/move.go index 7078812..1a3ae4c 100644 --- a/cmd/move/move.go +++ b/cmd/move/move.go @@ -7,7 +7,7 @@ import ( "strconv" "strings" - "github.com/charmbracelet/lipgloss" + "charm.land/lipgloss/v2" "github.com/digitalghost-dev/poke-cli/cmd/utils" "github.com/digitalghost-dev/poke-cli/connections" "github.com/digitalghost-dev/poke-cli/structs" @@ -66,7 +66,7 @@ func moveInfoContainer(output *strings.Builder, moveStruct structs.MoveJSONStruc Padding(1, 2). BorderStyle(lipgloss.ThickBorder()). BorderForeground(lipgloss.Color(styling.GetTypeColor(moveStruct.Type.Name))). - Width(32) + Width(34) headerStyle := lipgloss.NewStyle(). Bold(true). @@ -103,7 +103,7 @@ func moveEffectContainer(output *strings.Builder, moveStruct structs.MoveJSONStr Padding(1, 2). BorderStyle(lipgloss.ThickBorder()). BorderForeground(lipgloss.Color(styling.GetTypeColor(moveStruct.Type.Name))). - Width(32) + Width(34) for _, entry := range moveStruct.FlavorTextEntries { if entry.Language.Name != "en" { diff --git a/cmd/natures/natures.go b/cmd/natures/natures.go index aca4b33..f5242a9 100644 --- a/cmd/natures/natures.go +++ b/cmd/natures/natures.go @@ -5,8 +5,8 @@ import ( "os" "strings" - "github.com/charmbracelet/lipgloss" - "github.com/charmbracelet/lipgloss/table" + "charm.land/lipgloss/v2" + "charm.land/lipgloss/v2/table" "github.com/digitalghost-dev/poke-cli/cmd/utils" "github.com/digitalghost-dev/poke-cli/styling" ) diff --git a/cmd/pokemon/pokemon.go b/cmd/pokemon/pokemon.go index db6a974..51087d2 100644 --- a/cmd/pokemon/pokemon.go +++ b/cmd/pokemon/pokemon.go @@ -2,6 +2,7 @@ package pokemon import ( "bytes" + "errors" "flag" "fmt" "io" @@ -10,7 +11,7 @@ import ( "sort" "strings" - "github.com/charmbracelet/lipgloss" + "charm.land/lipgloss/v2" "github.com/digitalghost-dev/poke-cli/cmd/utils" "github.com/digitalghost-dev/poke-cli/connections" "github.com/digitalghost-dev/poke-cli/flags" @@ -61,8 +62,10 @@ func PokemonCommand() (string, error) { pokemonName := strings.ToLower(args[2]) if err := pf.FlagSet.Parse(args[3:]); err != nil { - fmt.Printf("error parsing flags: %v\n", err) - pf.FlagSet.Usage() + if errors.Is(err, flag.ErrHelp) { + return output.String(), nil + } + fmt.Fprintf(&output, "error parsing flags: %v\n", err) return output.String(), err } diff --git a/cmd/pokemon/pokemon_test.go b/cmd/pokemon/pokemon_test.go index 4b6740b..a2bc417 100644 --- a/cmd/pokemon/pokemon_test.go +++ b/cmd/pokemon/pokemon_test.go @@ -92,6 +92,12 @@ func TestPokemonCommand(t *testing.T) { args: []string{"pokemon", "slowking-galar"}, expectedOutput: utils.LoadGolden(t, "pokemon_regional_form_2.golden"), }, + { + name: "Pokemon invalid flag", + args: []string{"pokemon", "pikachu", "--bogus"}, + expectedOutput: utils.LoadGolden(t, "pokemon_invalid_flag.golden"), + expectedError: true, + }, } for _, tt := range tests { diff --git a/cmd/search/model_input.go b/cmd/search/model_input.go index a73f0f4..2a3b67a 100644 --- a/cmd/search/model_input.go +++ b/cmd/search/model_input.go @@ -4,9 +4,10 @@ import ( "fmt" "strings" - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + "charm.land/bubbles/v2/textinput" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" + "github.com/digitalghost-dev/poke-cli/cmd/utils" "github.com/digitalghost-dev/poke-cli/styling" ) @@ -14,7 +15,7 @@ import ( func UpdateInput(msg tea.Msg, m model) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { - case tea.KeyMsg: + case tea.KeyPressMsg: if m.ShowResults { // If results are shown, pressing 'b' resets to search view if msg.String() == "b" { @@ -25,15 +26,14 @@ func UpdateInput(msg tea.Msg, m model) (tea.Model, tea.Cmd) { return m, textinput.Blink } } else { - switch msg.Type { + switch msg.Code { case tea.KeyEnter: searchTerm := m.TextInput.Value() _, endpoint := RenderInput(m) // checking for blank queries if strings.TrimSpace(searchTerm) == "" { - errMessage := styling.ErrorBorder.Render(styling.ErrorColor.Render("✖ Error!"), "\nNo blank queries") - m.WarningMessage = errMessage + m.WarningMessage = utils.FormatError("No blank queries") return m, nil } diff --git a/cmd/search/model_input_test.go b/cmd/search/model_input_test.go index 011e36f..f6df872 100644 --- a/cmd/search/model_input_test.go +++ b/cmd/search/model_input_test.go @@ -3,8 +3,8 @@ package search import ( "testing" - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" + "charm.land/bubbles/v2/textinput" + tea "charm.land/bubbletea/v2" ) func TestUpdateInput(t *testing.T) { @@ -16,7 +16,7 @@ func TestUpdateInput(t *testing.T) { TextInput: ti, } - msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'b'}} + msg := tea.KeyPressMsg{Code: 'b', Text: "b"} mUpdated, _ := UpdateInput(msg, m) updated := mUpdated.(model) diff --git a/cmd/search/model_selection.go b/cmd/search/model_selection.go index 0561194..ec53b63 100644 --- a/cmd/search/model_selection.go +++ b/cmd/search/model_selection.go @@ -3,15 +3,15 @@ package search import ( "fmt" - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" + "charm.land/bubbles/v2/textinput" + tea "charm.land/bubbletea/v2" "github.com/digitalghost-dev/poke-cli/styling" ) // UpdateSelection handles navigation in the selection menu. func UpdateSelection(msg tea.Msg, m model) (tea.Model, tea.Cmd) { switch msg := msg.(type) { - case tea.KeyMsg: + case tea.KeyPressMsg: switch msg.String() { case "down": m.Choice++ diff --git a/cmd/search/model_selection_test.go b/cmd/search/model_selection_test.go index 4751bdf..255b0dc 100644 --- a/cmd/search/model_selection_test.go +++ b/cmd/search/model_selection_test.go @@ -4,19 +4,19 @@ import ( "testing" "time" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/x/exp/teatest" + tea "charm.land/bubbletea/v2" + "github.com/charmbracelet/x/exp/teatest/v2" ) func TestSelection(t *testing.T) { m := initialModel() testModel := teatest.NewTestModel(t, m, teatest.WithInitialTermSize(500, 600)) - testModel.Send(tea.KeyMsg{Type: tea.KeyDown}) - testModel.Send(tea.KeyMsg{Type: tea.KeyUp}) - testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) + testModel.Send(tea.KeyPressMsg{Code: tea.KeyDown}) + testModel.Send(tea.KeyPressMsg{Code: tea.KeyUp}) + testModel.Send(tea.KeyPressMsg{Code: tea.KeyEnter}) - testModel.Send(tea.KeyMsg{Type: tea.KeyCtrlC}) + testModel.Send(tea.KeyPressMsg{Code: 'c', Mod: tea.ModCtrl}) testModel.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond)) final := testModel.FinalModel(t).(model) @@ -40,17 +40,17 @@ func TestChoiceClamping(t *testing.T) { testModel := teatest.NewTestModel(t, m) // Move down twice, this should attempt to exceed max Choice - testModel.Send(tea.KeyMsg{Type: tea.KeyDown}) // 0 → 1 - testModel.Send(tea.KeyMsg{Type: tea.KeyDown}) // 1 → 2, but should clamp to 1 + testModel.Send(tea.KeyPressMsg{Code: tea.KeyDown}) // 0 → 1 + testModel.Send(tea.KeyPressMsg{Code: tea.KeyDown}) // 1 → 2, but should clamp to 1 // Move up three times, this should attempt to go below 0 - testModel.Send(tea.KeyMsg{Type: tea.KeyUp}) // 1 → 0 - testModel.Send(tea.KeyMsg{Type: tea.KeyUp}) // 0 → -1, clamp to 0 - testModel.Send(tea.KeyMsg{Type: tea.KeyUp}) // stays at 0 + testModel.Send(tea.KeyPressMsg{Code: tea.KeyUp}) // 1 → 0 + testModel.Send(tea.KeyPressMsg{Code: tea.KeyUp}) // 0 → -1, clamp to 0 + testModel.Send(tea.KeyPressMsg{Code: tea.KeyUp}) // stays at 0 // Simulate enter and quit to finish - testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) - testModel.Send(tea.KeyMsg{Type: tea.KeyCtrlC}) + testModel.Send(tea.KeyPressMsg{Code: tea.KeyEnter}) + testModel.Send(tea.KeyPressMsg{Code: 'c', Mod: tea.ModCtrl}) testModel.WaitFinished(t) final := testModel.FinalModel(t).(model) diff --git a/cmd/search/search.go b/cmd/search/search.go index 66199f6..0de158f 100644 --- a/cmd/search/search.go +++ b/cmd/search/search.go @@ -5,8 +5,8 @@ import ( "os" "strings" - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" + "charm.land/bubbles/v2/textinput" + tea "charm.land/bubbletea/v2" "github.com/digitalghost-dev/poke-cli/cmd/utils" ) @@ -58,7 +58,7 @@ func initialModel() model { ti := textinput.New() ti.Placeholder = "type name..." ti.CharLimit = 20 - ti.Width = 20 + ti.SetWidth(20) return model{ TextInput: ti, @@ -73,7 +73,7 @@ func (m model) Init() tea.Cmd { // Update handles keypresses and updates the state. func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { - case tea.KeyMsg: + case tea.KeyPressMsg: switch msg.String() { case "ctrl+c", "esc": m.Quitting = true @@ -88,17 +88,17 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // View renders the correct UI screen. -func (m model) View() string { +func (m model) View() tea.View { if m.Quitting { - return "\n Quitting search...\n\n" + return tea.NewView("\n Quitting search...\n\n") } if m.ShowResults { resultsView, _ := RenderInput(m) // Fetch results view - return resultsView + return tea.NewView(resultsView) } if !m.Chosen { - return RenderSelection(m) + return tea.NewView(RenderSelection(m)) } inputView, _ := RenderInput(m) - return inputView + return tea.NewView(inputView) } diff --git a/cmd/search/search_test.go b/cmd/search/search_test.go index e0e8db5..bd1406e 100644 --- a/cmd/search/search_test.go +++ b/cmd/search/search_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - tea "github.com/charmbracelet/bubbletea" + tea "charm.land/bubbletea/v2" "github.com/digitalghost-dev/poke-cli/styling" "github.com/stretchr/testify/require" @@ -66,7 +66,7 @@ func TestModelQuit(t *testing.T) { m := model{} // Simulate pressing Esc - msg := tea.KeyMsg{Type: tea.KeyEsc} + msg := tea.KeyPressMsg{Code: tea.KeyEscape} newModel, cmd := m.Update(msg) assert.True(t, newModel.(model).Quitting, "Model should be set to quitting") @@ -92,7 +92,7 @@ func TestSearchCommandValidationError(t *testing.T) { func TestModelViewQuitting(t *testing.T) { m := model{Quitting: true} view := m.View() - assert.Contains(t, view, "Quitting search", "View should show quitting message") + assert.Contains(t, view.Content, "Quitting search", "View should show quitting message") } func TestModelViewShowResults(t *testing.T) { @@ -102,12 +102,12 @@ func TestModelViewShowResults(t *testing.T) { } view := m.View() // View calls RenderInput when ShowResults is true - assert.NotEmpty(t, view, "View should render results") + assert.NotEmpty(t, view.Content, "View should render results") } func TestModelViewNotChosen(t *testing.T) { m := model{Chosen: false} view := m.View() // View calls RenderSelection when not chosen - assert.Contains(t, view, "Search for a resource", "View should show selection prompt") + assert.Contains(t, view.Content, "Search for a resource", "View should show selection prompt") } diff --git a/cmd/speed/speed.go b/cmd/speed/speed.go index 598ffb2..1f360bd 100644 --- a/cmd/speed/speed.go +++ b/cmd/speed/speed.go @@ -10,8 +10,8 @@ import ( "strconv" "strings" + "charm.land/lipgloss/v2" "github.com/charmbracelet/huh" - "github.com/charmbracelet/lipgloss" xstrings "github.com/charmbracelet/x/exp/strings" "github.com/digitalghost-dev/poke-cli/cmd/utils" "github.com/digitalghost-dev/poke-cli/connections" @@ -316,11 +316,13 @@ func formula() (string, error) { styling.Yellow.Render(speedStr), ) + isDark := lipgloss.HasDarkBackground(os.Stdin, os.Stdout) + ld := lipgloss.LightDark(isDark) docStyle := lipgloss.NewStyle(). Padding(1, 2). BorderStyle(lipgloss.ThickBorder()). - BorderForeground(lipgloss.AdaptiveColor{Light: "#444", Dark: "#EEE"}). - Width(32) + BorderForeground(ld(lipgloss.Color("#444"), lipgloss.Color("#EEE"))). + Width(34) fullDoc := lipgloss.JoinVertical(lipgloss.Top, header, "---", body) output.WriteString(docStyle.Render(fullDoc)) diff --git a/cmd/tcg/dashboard.go b/cmd/tcg/dashboard.go index 4f9d195..390473d 100644 --- a/cmd/tcg/dashboard.go +++ b/cmd/tcg/dashboard.go @@ -2,19 +2,22 @@ package tcg import ( "fmt" + "image/color" + "os" "strings" - "github.com/charmbracelet/bubbles/table" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + "charm.land/bubbles/v2/table" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "github.com/digitalghost-dev/poke-cli/styling" ) type styles struct { - doc lipgloss.Style - inactiveTab lipgloss.Style - activeTab lipgloss.Style - window lipgloss.Style + doc lipgloss.Style + inactiveTab lipgloss.Style + activeTab lipgloss.Style + window lipgloss.Style + highlightColor color.Color } func tabBorderWithBottom(left, middle, right string) lipgloss.Border { @@ -28,7 +31,9 @@ func tabBorderWithBottom(left, middle, right string) lipgloss.Border { func newStyles() *styles { inactiveTabBorder := tabBorderWithBottom("┴", "─", "┴") activeTabBorder := tabBorderWithBottom("┘", " ", "└") - highlightColor := lipgloss.AdaptiveColor{Light: "#874BFD", Dark: "#7D56F4"} + isDark := lipgloss.HasDarkBackground(os.Stdin, os.Stdout) + ld := lipgloss.LightDark(isDark) + highlightColor := ld(lipgloss.Color("#874BFD"), lipgloss.Color("#7D56F4")) s := new(styles) s.doc = lipgloss.NewStyle(). @@ -44,6 +49,7 @@ func newStyles() *styles { Padding(2, 0). Border(lipgloss.NormalBorder()). UnsetBorderTop() + s.highlightColor = highlightColor return s } @@ -73,7 +79,7 @@ func overviewView(m model, contentWidth int) string { if len(m.standings) == 0 { return " Loading..." } - return overviewContent(m.flag, m.tournament, m.tournamentType, m.tournamentDate, m.winner, m.winningDeck, m.totalPlayers, contentWidth) + return overviewContent(m.flag, m.tournament, m.tournamentType, m.tournamentDate, m.winner, m.winningDeck, m.totalPlayers, contentWidth, m.styles.highlightColor) } func countriesView(s []countryStats, width int) string { @@ -86,7 +92,7 @@ func (m model) Init() tea.Cmd { func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { - case tea.KeyMsg: + case tea.KeyPressMsg: switch keypress := msg.String(); keypress { case "ctrl+c", "esc": return m, tea.Quit @@ -151,9 +157,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } -func (m model) View() string { +func (m model) View() tea.View { if m.styles == nil { - return "" + return tea.NewView("") } doc := strings.Builder{} @@ -192,8 +198,8 @@ func (m model) View() string { // Fill the gap between the tab row and the window's right edge so the top // border line stretches the full width of the window. - highlightColor := lipgloss.AdaptiveColor{Light: "#874BFD", Dark: "#7D56F4"} - fillWidth := (windowWidth + 2) - lipgloss.Width(row) + highlightColor := m.styles.highlightColor + fillWidth := windowWidth - lipgloss.Width(row) if fillWidth > 0 { fill := lipgloss.NewStyle().Foreground(highlightColor). Render(strings.Repeat("─", fillWidth-1) + "┐") @@ -243,5 +249,7 @@ func (m model) View() string { doc.WriteString("\n") doc.WriteString(styling.KeyMenu.Render("← → (switch tab) • b (back) • ctrl+c | esc (quit)")) - return s.doc.Render(doc.String()) + v := tea.NewView(s.doc.Render(doc.String())) + v.AltScreen = true + return v } diff --git a/cmd/tcg/dashboard_test.go b/cmd/tcg/dashboard_test.go index 24a36ab..c16995c 100644 --- a/cmd/tcg/dashboard_test.go +++ b/cmd/tcg/dashboard_test.go @@ -6,8 +6,8 @@ import ( "testing" "time" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/x/exp/teatest" + tea "charm.land/bubbletea/v2" + "github.com/charmbracelet/x/exp/teatest/v2" ) func newTestModel() model { @@ -45,16 +45,16 @@ func TestDashboardModel_Init_ReturnsCmd(t *testing.T) { func TestDashboardModel_Update_Quit(t *testing.T) { tests := []struct { name string - key tea.KeyType + msg tea.KeyPressMsg }{ - {"ctrl+c", tea.KeyCtrlC}, - {"esc", tea.KeyEsc}, + {"ctrl+c", tea.KeyPressMsg{Code: 'c', Mod: tea.ModCtrl}}, + {"esc", tea.KeyPressMsg{Code: tea.KeyEscape}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := newTestModel() tm := teatest.NewTestModel(t, m, teatest.WithInitialTermSize(120, 40)) - tm.Send(tea.KeyMsg{Type: tt.key}) + tm.Send(tt.msg) tm.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond)) }) } @@ -63,7 +63,7 @@ func TestDashboardModel_Update_Quit(t *testing.T) { func TestDashboardModel_Update_Back(t *testing.T) { m := newTestModel() tm := teatest.NewTestModel(t, m, teatest.WithInitialTermSize(120, 40)) - tm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'b'}}) + tm.Send(tea.KeyPressMsg{Code: 'b', Text: "b"}) tm.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond)) final := tm.FinalModel(t).(model) if !final.goBack { @@ -74,12 +74,12 @@ func TestDashboardModel_Update_Back(t *testing.T) { func TestDashboardModel_Update_TabNavigation(t *testing.T) { m := newTestModel() // right moves forward - newM, _ := m.Update(tea.KeyMsg{Type: tea.KeyRight}) + newM, _ := m.Update(tea.KeyPressMsg{Code: tea.KeyRight}) if newM.(model).activeTab != 1 { t.Errorf("expected activeTab=1 after right, got %d", newM.(model).activeTab) } // left moves back - newM2, _ := newM.(model).Update(tea.KeyMsg{Type: tea.KeyLeft}) + newM2, _ := newM.(model).Update(tea.KeyPressMsg{Code: tea.KeyLeft}) if newM2.(model).activeTab != 0 { t.Errorf("expected activeTab=0 after left, got %d", newM2.(model).activeTab) } @@ -88,13 +88,13 @@ func TestDashboardModel_Update_TabNavigation(t *testing.T) { func TestDashboardModel_Update_TabNavigation_Clamps(t *testing.T) { m := newTestModel() // can't go below 0 - newM, _ := m.Update(tea.KeyMsg{Type: tea.KeyLeft}) + newM, _ := m.Update(tea.KeyPressMsg{Code: tea.KeyLeft}) if newM.(model).activeTab != 0 { t.Errorf("expected activeTab to clamp at 0, got %d", newM.(model).activeTab) } // can't exceed last tab m.activeTab = 3 - newM2, _ := m.Update(tea.KeyMsg{Type: tea.KeyRight}) + newM2, _ := m.Update(tea.KeyPressMsg{Code: tea.KeyRight}) if newM2.(model).activeTab != 3 { t.Errorf("expected activeTab to clamp at 3, got %d", newM2.(model).activeTab) } @@ -164,7 +164,7 @@ func TestDashboardModel_Update_WindowSize(t *testing.T) { func TestDashboardModel_View_NilStyles(t *testing.T) { m := model{} - if m.View() != "" { + if m.View().Content != "" { t.Error("expected empty string when styles is nil") } } @@ -173,7 +173,7 @@ func TestDashboardModel_View_ContainsTabs(t *testing.T) { m := newTestModel() view := m.View() for _, tab := range []string{"Overview", "Standings", "Decks", "Countries"} { - if !strings.Contains(view, tab) { + if !strings.Contains(view.Content, tab) { t.Errorf("expected view to contain tab %q", tab) } } @@ -182,7 +182,7 @@ func TestDashboardModel_View_ContainsTabs(t *testing.T) { func TestDashboardModel_View_LoadingState(t *testing.T) { m := newTestModel() view := m.View() - if !strings.Contains(view, "Loading") { + if !strings.Contains(view.Content, "Loading") { t.Error("expected loading message before data arrives") } } @@ -191,8 +191,8 @@ func TestDashboardModel_View_FetchError(t *testing.T) { m := newTestModel() m.err = errors.New("network error") view := m.View() - if !strings.Contains(view, "fetch error") { - t.Errorf("expected fetch error in view, got: %s", view) + if !strings.Contains(view.Content, "fetch error") { + t.Errorf("expected fetch error in view, got: %s", view.Content) } } @@ -201,7 +201,7 @@ func TestDashboardModel_View_AllTabs(t *testing.T) { for tab := 0; tab <= 3; tab++ { m.activeTab = tab view := m.View() - if view == "" { + if view.Content == "" { t.Errorf("expected non-empty view for tab %d", tab) } } diff --git a/cmd/tcg/data.go b/cmd/tcg/data.go index 73dc6d3..ebeeb3f 100644 --- a/cmd/tcg/data.go +++ b/cmd/tcg/data.go @@ -5,7 +5,7 @@ import ( "net/url" "strings" - tea "github.com/charmbracelet/bubbletea" + tea "charm.land/bubbletea/v2" ) type standingRows struct { diff --git a/cmd/tcg/tab_overview.go b/cmd/tcg/tab_overview.go index 725cac3..236d65d 100644 --- a/cmd/tcg/tab_overview.go +++ b/cmd/tcg/tab_overview.go @@ -2,10 +2,11 @@ package tcg import ( "fmt" + "image/color" "strconv" "strings" - "github.com/charmbracelet/lipgloss" + "charm.land/lipgloss/v2" ) func formatInt(n int) string { @@ -20,8 +21,7 @@ func formatInt(n int) string { return result.String() } -func overviewContent(flag, tournament, tournamentType, tournamentDate, winner, winningDeck string, totalPlayers, contentWidth int) string { - highlightColor := lipgloss.AdaptiveColor{Light: "#874BFD", Dark: "#7D56F4"} +func overviewContent(flag, tournament, tournamentType, tournamentDate, winner, winningDeck string, totalPlayers, contentWidth int, highlightColor color.Color) string { header := fmt.Sprintf("%s %s · %s · %s", flag, tournament, tournamentType, tournamentDate) statBox := lipgloss.NewStyle(). diff --git a/cmd/tcg/tab_overview_test.go b/cmd/tcg/tab_overview_test.go index 1bc5604..5b42142 100644 --- a/cmd/tcg/tab_overview_test.go +++ b/cmd/tcg/tab_overview_test.go @@ -3,6 +3,8 @@ package tcg import ( "strings" "testing" + + "charm.land/lipgloss/v2" ) func TestOverviewContent(t *testing.T) { @@ -53,7 +55,7 @@ func TestOverviewContent(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := overviewContent(tt.flag, tt.tournament, tt.tType, tt.tDate, tt.winner, tt.winningDeck, tt.totalPlayers, tt.contentWidth) + result := overviewContent(tt.flag, tt.tournament, tt.tType, tt.tDate, tt.winner, tt.winningDeck, tt.totalPlayers, tt.contentWidth, lipgloss.Color("#7D56F4")) if result == "" { t.Fatal("expected non-empty output") } diff --git a/cmd/tcg/tab_standings.go b/cmd/tcg/tab_standings.go index 0143039..31f8e51 100644 --- a/cmd/tcg/tab_standings.go +++ b/cmd/tcg/tab_standings.go @@ -3,8 +3,8 @@ package tcg import ( "strconv" - "github.com/charmbracelet/bubbles/table" - "github.com/charmbracelet/lipgloss" + "charm.land/bubbles/v2/table" + "charm.land/lipgloss/v2" "github.com/digitalghost-dev/poke-cli/styling" ) @@ -39,6 +39,7 @@ func standingsTable(rows []standingRows, width, height int) table.Model { } tableHeight := max(height-14, 5) + tableWidth := fixedWidth + deckWidth + separators s := table.DefaultStyles() s.Header = s.Header. @@ -55,6 +56,7 @@ func standingsTable(rows []standingRows, width, height int) table.Model { table.WithRows(tableRows), table.WithFocused(true), table.WithHeight(tableHeight), + table.WithWidth(tableWidth), ) t.SetStyles(s) diff --git a/cmd/tcg/tcg.go b/cmd/tcg/tcg.go index 4793632..1a96df3 100644 --- a/cmd/tcg/tcg.go +++ b/cmd/tcg/tcg.go @@ -1,12 +1,13 @@ package tcg import ( + "errors" "flag" "fmt" "os" "strings" - tea "github.com/charmbracelet/bubbletea" + tea "charm.land/bubbletea/v2" "github.com/digitalghost-dev/poke-cli/cmd/utils" "github.com/digitalghost-dev/poke-cli/connections" "github.com/digitalghost-dev/poke-cli/flags" @@ -33,8 +34,6 @@ func TcgCommand() (string, error) { return output.String(), nil } - flag.Parse() - if err := utils.ValidateArgs(os.Args, utils.Validator{MaxArgs: 3, CmdName: "tcg", RequireName: false, HasFlags: true}); err != nil { output.WriteString(err.Error()) return output.String(), err @@ -42,6 +41,9 @@ func TcgCommand() (string, error) { tf := flags.SetupTcgFlagSet() if err := tf.FlagSet.Parse(os.Args[2:]); err != nil { + if errors.Is(err, flag.ErrHelp) { + return output.String(), nil + } fmt.Fprintf(&output, "error parsing flags: %v\n", err) return output.String(), err } @@ -58,7 +60,7 @@ func TcgCommand() (string, error) { conn := connections.CallTCGData runTournaments := func(m tournamentsModel) (tournamentsModel, error) { - final, err := tea.NewProgram(m, tea.WithAltScreen()).Run() + final, err := tea.NewProgram(m).Run() if err != nil { return tournamentsModel{}, err } @@ -70,7 +72,7 @@ func TcgCommand() (string, error) { } runDashboard := func(m model) (model, error) { - final, err := tea.NewProgram(m, tea.WithAltScreen()).Run() + final, err := tea.NewProgram(m).Run() if err != nil { return model{}, err } diff --git a/cmd/tcg/tcg_test.go b/cmd/tcg/tcg_test.go index f949c50..20b47e0 100644 --- a/cmd/tcg/tcg_test.go +++ b/cmd/tcg/tcg_test.go @@ -107,6 +107,12 @@ func TestTcgCommand(t *testing.T) { golden: "tcg_too_many_args.golden", wantErr: true, }, + { + name: "invalid flag", + args: []string{"poke-cli", "tcg", "--bogus"}, + golden: "tcg_invalid_flag.golden", + wantErr: true, + }, } for _, tt := range tests { diff --git a/cmd/tcg/tournamentslist.go b/cmd/tcg/tournamentslist.go index a465011..53db148 100644 --- a/cmd/tcg/tournamentslist.go +++ b/cmd/tcg/tournamentslist.go @@ -3,9 +3,9 @@ package tcg import ( "encoding/json" - "github.com/charmbracelet/bubbles/list" - "github.com/charmbracelet/bubbles/spinner" - tea "github.com/charmbracelet/bubbletea" + "charm.land/bubbles/v2/list" + "charm.land/bubbles/v2/spinner" + tea "charm.land/bubbletea/v2" "github.com/digitalghost-dev/poke-cli/styling" ) @@ -68,7 +68,7 @@ func (m tournamentsModel) Init() tea.Cmd { func (m tournamentsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { - case tea.KeyMsg: + case tea.KeyPressMsg: switch msg.String() { case "ctrl+c", "esc": m.quitting = true @@ -131,23 +131,25 @@ func (m tournamentsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } -func (m tournamentsModel) View() string { +func (m tournamentsModel) View() tea.View { + var content string if m.quitting { - return "\n Quitting...\n\n" - } - if m.error != nil { - return styling.ApiErrorStyle.Render( + content = "\n Quitting...\n\n" + } else if m.error != nil { + content = styling.ApiErrorStyle.Render( "Error loading tournaments from Supabase:\n" + m.error.Error() + "\n\n" + "Press ctrl+c or esc to exit.", ) - } - if m.loading { - return "\n " + m.spinner.View() + " Loading tournaments...\n\n" - } - if m.selected != nil { - return styling.QuitTextStyle.Render("Tournament selected:", m.selected.Location+" · "+m.selected.TextDate) + } else if m.loading { + content = "\n " + m.spinner.View() + " Loading tournaments...\n\n" + } else if m.selected != nil { + content = styling.QuitTextStyle.Render("Tournament selected:", m.selected.Location+" · "+m.selected.TextDate) + } else { + content = "\n" + m.list.View() } - return "\n" + m.list.View() + v := tea.NewView(content) + v.AltScreen = true + return v } diff --git a/cmd/tcg/tournamentslist_test.go b/cmd/tcg/tournamentslist_test.go index 32b8ab4..4326904 100644 --- a/cmd/tcg/tournamentslist_test.go +++ b/cmd/tcg/tournamentslist_test.go @@ -6,9 +6,9 @@ import ( "testing" "time" - "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/x/exp/teatest" + "charm.land/bubbles/v2/list" + tea "charm.land/bubbletea/v2" + "github.com/charmbracelet/x/exp/teatest/v2" "github.com/digitalghost-dev/poke-cli/styling" ) @@ -111,17 +111,17 @@ func TestFetchTournaments_Success(t *testing.T) { func TestTournamentsModel_Update_CtrlC(t *testing.T) { tests := []struct { name string - key tea.KeyType + msg tea.KeyPressMsg }{ - {name: "ctrl+c", key: tea.KeyCtrlC}, - {name: "esc", key: tea.KeyEsc}, + {name: "ctrl+c", msg: tea.KeyPressMsg{Code: 'c', Mod: tea.ModCtrl}}, + {name: "esc", msg: tea.KeyPressMsg{Code: tea.KeyEscape}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := loadedModel() tm := teatest.NewTestModel(t, m, teatest.WithInitialTermSize(80, 24)) - tm.Send(tea.KeyMsg{Type: tt.key}) + tm.Send(tt.msg) tm.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond)) final := tm.FinalModel(t).(tournamentsModel) if !final.quitting { @@ -134,7 +134,7 @@ func TestTournamentsModel_Update_CtrlC(t *testing.T) { func TestTournamentsModel_Update_Enter_SetsSelected(t *testing.T) { m := loadedModel() tm := teatest.NewTestModel(t, m, teatest.WithInitialTermSize(80, 24)) - tm.Send(tea.KeyMsg{Type: tea.KeyEnter}) + tm.Send(tea.KeyPressMsg{Code: tea.KeyEnter}) tm.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond)) final := tm.FinalModel(t).(tournamentsModel) if final.selected == nil { @@ -207,8 +207,8 @@ func TestTournamentsModel_Update_WindowResize_WhenLoading(t *testing.T) { func TestTournamentsModel_View_Loading(t *testing.T) { m := tournamentsList(noopConn) view := m.View() - if !strings.Contains(view, "Loading tournaments") { - t.Errorf("expected loading message, got %q", view) + if !strings.Contains(view.Content, "Loading tournaments") { + t.Errorf("expected loading message, got %q", view.Content) } } @@ -217,8 +217,8 @@ func TestTournamentsModel_View_Error(t *testing.T) { m.loading = false m.error = errors.New("something went wrong") view := m.View() - if !strings.Contains(view, "something went wrong") { - t.Errorf("expected error message in view, got %q", view) + if !strings.Contains(view.Content, "something went wrong") { + t.Errorf("expected error message in view, got %q", view.Content) } } @@ -226,8 +226,8 @@ func TestTournamentsModel_View_Quitting(t *testing.T) { m := tournamentsList(noopConn) m.quitting = true view := m.View() - if !strings.Contains(view, "Quitting") { - t.Errorf("expected quitting message, got %q", view) + if !strings.Contains(view.Content, "Quitting") { + t.Errorf("expected quitting message, got %q", view.Content) } } @@ -236,15 +236,15 @@ func TestTournamentsModel_View_Selected(t *testing.T) { td := m.tournaments[0] m.selected = &td view := m.View() - if !strings.Contains(view, "London") { - t.Errorf("expected selected tournament in view, got %q", view) + if !strings.Contains(view.Content, "London") { + t.Errorf("expected selected tournament in view, got %q", view.Content) } } func TestTournamentsModel_View_Normal(t *testing.T) { m := loadedModel() view := m.View() - if view == "" { + if view.Content == "" { t.Error("expected non-empty view for loaded model") } } diff --git a/cmd/types/damage_table.go b/cmd/types/damage_table.go index f44e39f..572b3da 100644 --- a/cmd/types/damage_table.go +++ b/cmd/types/damage_table.go @@ -5,7 +5,7 @@ import ( "os" "strings" - "github.com/charmbracelet/lipgloss" + "charm.land/lipgloss/v2" "github.com/charmbracelet/x/term" "github.com/digitalghost-dev/poke-cli/connections" "github.com/digitalghost-dev/poke-cli/styling" @@ -17,7 +17,9 @@ import ( func DamageTable(typesName string, endpoint string) { // Setting up variables to style the list var columnWidth = 11 - var subtle = lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#383838"} + isDark := lipgloss.HasDarkBackground(os.Stdin, os.Stdout) + ld := lipgloss.LightDark(isDark) + subtle := ld(lipgloss.Color("#383838"), lipgloss.Color("#D9DCCF")) var list = lipgloss.NewStyle().Border(lipgloss.NormalBorder(), false, true, false, false).BorderForeground(subtle).MarginRight(2).Height(8) var listHeader = lipgloss.NewStyle().BorderStyle(lipgloss.NormalBorder()).BorderBottom(true).BorderForeground(subtle).MarginRight(2).Render var listItem = lipgloss.NewStyle().Render diff --git a/cmd/types/damage_table_test.go b/cmd/types/damage_table_test.go index f0fab2d..e14a7db 100644 --- a/cmd/types/damage_table_test.go +++ b/cmd/types/damage_table_test.go @@ -5,6 +5,8 @@ import ( "os" "strings" "testing" + + "github.com/digitalghost-dev/poke-cli/styling" ) func TestDamageTable(t *testing.T) { @@ -30,7 +32,7 @@ func TestDamageTable(t *testing.T) { if err != nil { t.Fatalf("Failed to read from pipe: %v", err) } - output := buf.String() + output := styling.StripANSI(buf.String()) // Step 7: Assert the output contains expected strings if !strings.Contains(output, "You selected the Fire type.") { diff --git a/cmd/types/types.go b/cmd/types/types.go index d461a2b..e69b093 100644 --- a/cmd/types/types.go +++ b/cmd/types/types.go @@ -6,9 +6,9 @@ import ( "os" "strings" - "github.com/charmbracelet/bubbles/table" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + "charm.land/bubbles/v2/table" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "github.com/digitalghost-dev/poke-cli/cmd/utils" "github.com/digitalghost-dev/poke-cli/styling" ) @@ -64,7 +64,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var bubbleCmd tea.Cmd switch msg := msg.(type) { - case tea.KeyMsg: + case tea.KeyPressMsg: switch msg.String() { case "esc", "ctrl+c": m.quitting = true @@ -82,20 +82,20 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // View renders the current UI -func (m model) View() string { +func (m model) View() tea.View { if m.quitting { - return "\n Goodbye! \n" + return tea.NewView("\n Goodbye! \n") } // Don't render anything if a selection has been made if m.selectedOption != "" { - return "" + return tea.NewView("") } // Render the type selection table with instructions - return fmt.Sprintf("Select a type!\n%s\n%s", + return tea.NewView(fmt.Sprintf("Select a type!\n%s\n%s", styling.TypesTableBorder.Render(m.table.View()), - styling.KeyMenu.Render("↑ (move up) • ↓ (move down)\nenter (select) • ctrl+c | esc (quit)")) + styling.KeyMenu.Render("↑ (move up) • ↓ (move down)\nenter (select) • ctrl+c | esc (quit)"))) } func createTypeSelectionTable() model { @@ -113,6 +113,7 @@ func createTypeSelectionTable() model { table.WithRows(rows), table.WithFocused(true), table.WithHeight(10), + table.WithWidth(16), ) s := table.DefaultStyles() diff --git a/cmd/types/types_test.go b/cmd/types/types_test.go index 52894ba..1af37df 100644 --- a/cmd/types/types_test.go +++ b/cmd/types/types_test.go @@ -5,10 +5,10 @@ import ( "testing" "time" - "github.com/charmbracelet/bubbles/table" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/charmbracelet/x/exp/teatest" + "charm.land/bubbles/v2/table" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" + "github.com/charmbracelet/x/exp/teatest/v2" "github.com/digitalghost-dev/poke-cli/cmd/utils" "github.com/digitalghost-dev/poke-cli/styling" "github.com/stretchr/testify/assert" @@ -68,6 +68,7 @@ func createTestModel() model { table.WithRows(rows), table.WithFocused(true), table.WithHeight(7), + table.WithWidth(16), ) // Set table styles @@ -90,7 +91,7 @@ func TestUpdate(t *testing.T) { testModel := teatest.NewTestModel(t, m) // Send escape key - testModel.Send(tea.KeyMsg{Type: tea.KeyEsc}) + testModel.Send(tea.KeyPressMsg{Code: tea.KeyEscape}) testModel.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond)) final := testModel.FinalModel(t).(model) @@ -102,7 +103,7 @@ func TestUpdate(t *testing.T) { testModel := teatest.NewTestModel(t, m) // Send ctrl+c key - testModel.Send(tea.KeyMsg{Type: tea.KeyCtrlC}) + testModel.Send(tea.KeyPressMsg{Code: 'c', Mod: tea.ModCtrl}) testModel.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond)) final := testModel.FinalModel(t).(model) @@ -114,7 +115,7 @@ func TestUpdate(t *testing.T) { testModel := teatest.NewTestModel(t, m) // Send enter key - testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) + testModel.Send(tea.KeyPressMsg{Code: tea.KeyEnter}) testModel.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond)) final := testModel.FinalModel(t).(model) @@ -126,10 +127,10 @@ func TestUpdate(t *testing.T) { testModel := teatest.NewTestModel(t, m) // Send down arrow key to select the second row - testModel.Send(tea.KeyMsg{Type: tea.KeyDown}) + testModel.Send(tea.KeyPressMsg{Code: tea.KeyDown}) // Then send enter to select it - testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) + testModel.Send(tea.KeyPressMsg{Code: tea.KeyEnter}) testModel.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond)) final := testModel.FinalModel(t).(model) @@ -143,7 +144,7 @@ func TestView(t *testing.T) { m.quitting = true view := m.View() - assert.Equal(t, "\n Goodbye! \n", view, "View should return goodbye message when quitting") + assert.Equal(t, "\n Goodbye! \n", view.Content, "View should return goodbye message when quitting") }) t.Run("View should return empty string when selectedOption is set", func(t *testing.T) { @@ -155,9 +156,9 @@ func TestView(t *testing.T) { m := createTestModel() view := m.View() - assert.Contains(t, view, "Select a type!", "View should contain the title") - assert.Contains(t, view, "Type", "View should contain the table header") - assert.Contains(t, view, "move up", "View should contain the key menu") + assert.Contains(t, view.Content, "Select a type!", "View should contain the title") + assert.Contains(t, view.Content, "Type", "View should contain the table header") + assert.Contains(t, view.Content, "move up", "View should contain the key menu") }) } @@ -166,10 +167,10 @@ func TestTypeSelection(t *testing.T) { testModel := teatest.NewTestModel(t, m, teatest.WithInitialTermSize(300, 500)) - testModel.Send(tea.KeyMsg{Type: tea.KeyDown}) - testModel.Send(tea.KeyMsg{Type: tea.KeyDown}) - testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) - testModel.Send(tea.KeyMsg{Type: tea.KeyCtrlC}) + testModel.Send(tea.KeyPressMsg{Code: tea.KeyDown}) + testModel.Send(tea.KeyPressMsg{Code: tea.KeyDown}) + testModel.Send(tea.KeyPressMsg{Code: tea.KeyEnter}) + testModel.Send(tea.KeyPressMsg{Code: 'c', Mod: tea.ModCtrl}) testModel.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond)) final := testModel.FinalModel(t).(model) diff --git a/cmd/utils/errors.go b/cmd/utils/errors.go new file mode 100644 index 0000000..3f439be --- /dev/null +++ b/cmd/utils/errors.go @@ -0,0 +1,10 @@ +package utils + +import "github.com/digitalghost-dev/poke-cli/styling" + +func FormatError(message string) string { + return styling.ErrorBorder.Render( + styling.ErrorColor.Render("✖ Error!"), + "\n"+message, + ) +} diff --git a/cmd/utils/validateargs.go b/cmd/utils/validateargs.go index ee0e455..b2dc035 100644 --- a/cmd/utils/validateargs.go +++ b/cmd/utils/validateargs.go @@ -3,8 +3,6 @@ package utils import ( "fmt" "strings" - - "github.com/digitalghost-dev/poke-cli/styling" ) type Validator struct { @@ -17,10 +15,7 @@ type Validator struct { // checkLength checks if the number of arguments is lower than the max value. Helper Function. func checkLength(args []string, max int) error { if len(args) > max { - errMessage := styling.ErrorBorder.Render( - styling.ErrorColor.Render("✖ Error!") + "\nToo many arguments", - ) - return fmt.Errorf("%s", errMessage) + return fmt.Errorf("%s", FormatError("Too many arguments")) } return nil } @@ -28,9 +23,7 @@ func checkLength(args []string, max int) error { // checkNoOtherOptions checks if there are exactly 3 arguments and the third argument is neither '-h' nor '--help' func checkNoOtherOptions(args []string, max int, commandName string) error { if len(args) == max && args[2] != "-h" && args[2] != "--help" { - errMsg := styling.ErrorColor.Render("✖ Error!") + - "\nThe only available options after the\n" + "<" + commandName + "> command are '-h' or '--help'" - return fmt.Errorf("%s", styling.ErrorBorder.Render(errMsg)) + return fmt.Errorf("%s", FormatError(fmt.Sprintf("The only available options after the\n<%s> command are '-h' or '--help'", commandName))) } return nil } @@ -40,13 +33,10 @@ func ValidateArgs(args []string, v Validator) error { return err } if v.RequireName && len(args) == 2 { - errMessage := styling.ErrorBorder.Render( - styling.ErrorColor.Render("✖ Error!"), - fmt.Sprintf("\nPlease declare a(n) %s's name after the <%s> command", v.CmdName, v.CmdName), - fmt.Sprintf("\nRun 'poke-cli %s -h' for more details", v.CmdName), - "\nerror: insufficient arguments", - ) - return fmt.Errorf("%s", errMessage) + return fmt.Errorf("%s", FormatError(fmt.Sprintf( + "Please declare a(n) %s's name after the <%s> command\nRun 'poke-cli %s -h' for more details\nerror: insufficient arguments", + v.CmdName, v.CmdName, v.CmdName, + ))) } if !v.HasFlags && !v.RequireName { if err := checkNoOtherOptions(args, v.MaxArgs, v.CmdName); err != nil { @@ -60,13 +50,9 @@ func ValidateArgs(args []string, v Validator) error { func ValidatePokemonArgs(args []string) error { // Check if the number of arguments is less than 3 if len(args) < 3 { - errMessage := styling.ErrorBorder.Render( - styling.ErrorColor.Render("✖ Error!"), - "\nPlease declare a Pokémon's name after the command", - "\nRun 'poke-cli pokemon -h' for more details", - "\nerror: insufficient arguments", - ) - return fmt.Errorf("%s", errMessage) + return fmt.Errorf("%s", FormatError( + "Please declare a Pokémon's name after the command\nRun 'poke-cli pokemon -h' for more details\nerror: insufficient arguments", + )) } if err := checkLength(args, 8); err != nil { @@ -74,11 +60,7 @@ func ValidatePokemonArgs(args []string) error { } printImageFlagError := func() error { - msg := styling.ErrorBorder.Render( - styling.ErrorColor.Render("✖ Error!") + - "\nThe image flag (-i or --image) requires a non-empty value.\nValid sizes are: lg, md, sm.", - ) - return fmt.Errorf("%s", msg) + return fmt.Errorf("%s", FormatError("The image flag (-i or --image) requires a non-empty value.\nValid sizes are: lg, md, sm.")) } for _, arg := range args { @@ -97,26 +79,12 @@ func ValidatePokemonArgs(args []string) error { for _, arg := range args[3:] { // Check for an empty flag after Pokémon's name if arg == "-" || arg == "--" { - errorTitle := styling.ErrorColor.Render("✖ Error!") - errorString := fmt.Sprintf( - "\nEmpty flag '%s'.\nPlease specify valid flag(s).", - arg, - ) - finalErrorMessage := errorTitle + errorString - renderedError := styling.ErrorBorder.Render(finalErrorMessage) - return fmt.Errorf("%s", renderedError) + return fmt.Errorf("%s", FormatError(fmt.Sprintf("Empty flag '%s'.\nPlease specify valid flag(s).", arg))) } // Check if the argument after Pokémon's name is an attempted flag if arg[0] != '-' { - errorTitle := styling.ErrorColor.Render("✖ Error!") - errorString := fmt.Sprintf( - "\nInvalid argument '%s'.\nOnly flags are allowed after declaring a Pokémon's name", - arg, - ) - finalErrorMessage := errorTitle + errorString - renderedError := styling.ErrorBorder.Render(finalErrorMessage) - return fmt.Errorf("%s", renderedError) + return fmt.Errorf("%s", FormatError(fmt.Sprintf("Invalid argument '%s'.\nOnly flags are allowed after declaring a Pokémon's name", arg))) } } } diff --git a/connections/connection.go b/connections/connection.go index 9a3242d..160ff0f 100644 --- a/connections/connection.go +++ b/connections/connection.go @@ -10,8 +10,8 @@ import ( "net/url" "time" + "github.com/digitalghost-dev/poke-cli/cmd/utils" "github.com/digitalghost-dev/poke-cli/structs" - "github.com/digitalghost-dev/poke-cli/styling" ) const APIURL = "https://pokeapi.co/api/v2/" @@ -30,11 +30,7 @@ func FetchEndpoint[T EndpointResource](endpoint, resourceName, baseURL, resource err := ApiCallSetup(fullURL, &result, false) if err != nil { - errMessage := styling.ErrorBorder.Render( - styling.ErrorColor.Render("✖ Error!"), - fmt.Sprintf("\n%s not found.\n• Perhaps a typo?\n• Missing a hyphen instead of a space?", resourceType), - ) - return zero, "", fmt.Errorf("%s", errMessage) + return zero, "", fmt.Errorf("%s", utils.FormatError(resourceType+" not found.\n• Perhaps a typo?\n• Missing a hyphen instead of a space?")) } return result, result.GetResourceName(), nil diff --git a/flags/abilityflagset.go b/flags/abilityflagset.go index 0326c6a..8adfa15 100644 --- a/flags/abilityflagset.go +++ b/flags/abilityflagset.go @@ -20,7 +20,7 @@ type AbilityFlags struct { func SetupAbilityFlagSet() *AbilityFlags { af := &AbilityFlags{} - af.FlagSet = flag.NewFlagSet("abilityFlags", flag.ExitOnError) + af.FlagSet = flag.NewFlagSet("abilityFlags", flag.ContinueOnError) af.Pokemon = af.FlagSet.Bool("pokemon", false, "List all Pokémon with chosen ability") af.ShortPokemon = af.FlagSet.Bool("p", false, "List all Pokémon with chosen ability") diff --git a/flags/pokemonflagset.go b/flags/pokemonflagset.go index 5c920a2..5c6b567 100644 --- a/flags/pokemonflagset.go +++ b/flags/pokemonflagset.go @@ -9,13 +9,15 @@ import ( "io" "log" "net/http" + "os" "sort" "strconv" "strings" "sync" - "github.com/charmbracelet/lipgloss" - "github.com/charmbracelet/lipgloss/table" + "charm.land/lipgloss/v2" + "charm.land/lipgloss/v2/table" + cmdutils "github.com/digitalghost-dev/poke-cli/cmd/utils" "github.com/digitalghost-dev/poke-cli/connections" "github.com/digitalghost-dev/poke-cli/structs" "github.com/digitalghost-dev/poke-cli/styling" @@ -57,7 +59,7 @@ func header(header string) string { func SetupPokemonFlagSet() *PokemonFlags { pf := &PokemonFlags{} - pf.FlagSet = flag.NewFlagSet("pokeFlags", flag.ExitOnError) + pf.FlagSet = flag.NewFlagSet("pokeFlags", flag.ContinueOnError) pf.Abilities = pf.FlagSet.Bool("abilities", false, "Print the Pokémon's abilities") pf.ShortAbilities = pf.FlagSet.Bool("a", false, "Print the Pokémon's abilities") @@ -347,7 +349,7 @@ func ImageFlag(w io.Writer, endpoint string, pokemonName string, size string) er c2, _ := styling.MakeColor(img.At(x, heightCounter+1)) color2 := lipgloss.Color(c2.Hex()) - styleKey := string(color1) + "_" + string(color2) + styleKey := c1.Hex() + "_" + c2.Hex() style, exists := styleCache[styleKey] if !exists { style = lipgloss.NewStyle().Foreground(color1).Background(color2) @@ -386,8 +388,7 @@ func ImageFlag(w io.Writer, endpoint string, pokemonName string, size string) er // Validate size dimensions, exists := sizeMap[strings.ToLower(size)] if !exists { - errMessage := styling.ErrorBorder.Render(styling.ErrorColor.Render("✖ Error!"), "\nInvalid image size.\nValid sizes are: lg, md, sm") - return fmt.Errorf("%s", errMessage) + return fmt.Errorf("%s", cmdutils.FormatError("Invalid image size.\nValid sizes are: lg, md, sm")) } imgStr := ToString(dimensions[0], dimensions[1], img) @@ -510,7 +511,9 @@ func MovesFlag(w io.Writer, endpoint string, pokemonName string) error { } // Build and print table - color := lipgloss.AdaptiveColor{Light: "#4B4B4B", Dark: "#D3D3D3"} + isDark := lipgloss.HasDarkBackground(os.Stdin, os.Stdout) + ld := lipgloss.LightDark(isDark) + color := ld(lipgloss.Color("#4B4B4B"), lipgloss.Color("#D3D3D3")) t := table.New(). Border(lipgloss.NormalBorder()). diff --git a/flags/tcg_flagset.go b/flags/tcg_flagset.go index ed8c6cf..93cba58 100644 --- a/flags/tcg_flagset.go +++ b/flags/tcg_flagset.go @@ -18,7 +18,7 @@ type TcgFlags struct { func SetupTcgFlagSet() *TcgFlags { tf := &TcgFlags{} - tf.FlagSet = flag.NewFlagSet("tcgFlags", flag.ExitOnError) + tf.FlagSet = flag.NewFlagSet("tcgFlags", flag.ContinueOnError) tf.Web = tf.FlagSet.Bool("web", false, "Opens a Streamlit dashboard of stats in the browser") tf.ShortWeb = tf.FlagSet.Bool("w", false, "Opens a Streamlit dashboard of stats in the browser") diff --git a/flags/version.go b/flags/version.go index aa21351..91c6eaa 100644 --- a/flags/version.go +++ b/flags/version.go @@ -8,9 +8,10 @@ import ( "io" "net/http" "net/url" + "os" "strings" - "github.com/charmbracelet/lipgloss" + "charm.land/lipgloss/v2" "github.com/digitalghost-dev/poke-cli/styling" ) @@ -75,11 +76,13 @@ func latestRelease(output *strings.Builder) error { releaseString := "Latest available release on GitHub:" releaseTag := styling.ColoredBullet.Render("") + release.TagName + isDark := lipgloss.HasDarkBackground(os.Stdin, os.Stdout) + ld := lipgloss.LightDark(isDark) docStyle := lipgloss.NewStyle(). Padding(1, 2). BorderStyle(lipgloss.ThickBorder()). - BorderForeground(lipgloss.AdaptiveColor{Light: "#444", Dark: "#EEE"}). - Width(30) + BorderForeground(ld(lipgloss.Color("#444"), lipgloss.Color("#EEE"))). + Width(32) fullDoc := lipgloss.JoinVertical(lipgloss.Top, releaseString, releaseTag) diff --git a/go.mod b/go.mod index b74cb0d..8d0334e 100644 --- a/go.mod +++ b/go.mod @@ -3,19 +3,21 @@ module github.com/digitalghost-dev/poke-cli go 1.25.8 require ( - github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 - github.com/charmbracelet/bubbletea v1.3.10 + charm.land/bubbles/v2 v2.1.0 + charm.land/bubbletea/v2 v2.0.2 + charm.land/lipgloss/v2 v2.0.2 github.com/charmbracelet/huh v0.8.0 github.com/charmbracelet/lipgloss v1.1.0 - github.com/charmbracelet/x/ansi v0.11.0 + github.com/charmbracelet/x/ansi v0.11.6 github.com/charmbracelet/x/exp/strings v0.0.0-20251110210702-903592506081 - github.com/charmbracelet/x/exp/teatest v0.0.0-20251110210702-903592506081 + github.com/charmbracelet/x/exp/teatest/v2 v2.0.0-20260413165052-6921c759c913 github.com/charmbracelet/x/term v0.2.2 github.com/disintegration/imaging v1.6.2 github.com/dolmen-go/kittyimg v0.0.0-20250610224728-874967bd8ea4 github.com/schollz/closestmatch v2.1.0+incompatible github.com/stretchr/testify v1.11.1 golang.org/x/image v0.33.0 + golang.org/x/term v0.42.0 golang.org/x/text v0.31.0 modernc.org/sqlite v1.39.1 ) @@ -23,15 +25,19 @@ require ( require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/aymanbagabas/go-udiff v0.3.1 // indirect - github.com/bits-and-blooms/bitset v1.24.3 // indirect + github.com/aymanbagabas/go-udiff v0.4.1 // indirect + github.com/bits-and-blooms/bitset v1.24.4 // indirect github.com/catppuccin/go v0.3.0 // indirect - github.com/charmbracelet/colorprofile v0.3.3 // indirect + github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect + github.com/charmbracelet/bubbletea v1.3.10 // indirect + github.com/charmbracelet/colorprofile v0.4.2 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect github.com/charmbracelet/x/cellbuf v0.0.14 // indirect github.com/charmbracelet/x/exp/golden v0.0.0-20251110210702-903592506081 // indirect - github.com/clipperhouse/displaywidth v0.5.0 // indirect - github.com/clipperhouse/stringish v0.1.1 // indirect - github.com/clipperhouse/uax29/v2 v2.3.0 // indirect + github.com/charmbracelet/x/termios v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect @@ -39,7 +45,7 @@ require ( github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.21 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect @@ -51,7 +57,8 @@ require ( github.com/sahilm/fuzzy v0.1.1 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect - golang.org/x/sys v0.38.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.43.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/libc v1.66.10 // indirect modernc.org/mathutil v1.7.1 // indirect diff --git a/go.sum b/go.sum index 2ee4fad..c4d181a 100644 --- a/go.sum +++ b/go.sum @@ -1,27 +1,35 @@ +charm.land/bubbles/v2 v2.1.0 h1:YSnNh5cPYlYjPxRrzs5VEn3vwhtEn3jVGRBT3M7/I0g= +charm.land/bubbles/v2 v2.1.0/go.mod h1:l97h4hym2hvWBVfmJDtrEHHCtkIKeTEb3TTJ4ZOB3wY= +charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0= +charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ= +charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs= +charm.land/lipgloss/v2 v2.0.2/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= 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/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= -github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= -github.com/bits-and-blooms/bitset v1.24.3 h1:Bte86SlO3lwPQqww+7BE9ZuUCKIjfqnG5jtEyqA9y9Y= -github.com/bits-and-blooms/bitset v1.24.3/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= +github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= +github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE= +github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws= github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw= 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.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI= -github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4= +github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= +github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY= github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= 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/x/ansi v0.11.0 h1:uuIVK7GIplwX6UBIz8S2TF8nkr7xRlygSsBRjSJqIvA= -github.com/charmbracelet/x/ansi v0.11.0/go.mod h1:uQt8bOrq/xgXjlGcFMc8U2WYbnxyjrKhnvTQluvfCaE= +github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA= +github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98= +github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= +github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4= github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA= github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= @@ -32,20 +40,20 @@ github.com/charmbracelet/x/exp/golden v0.0.0-20251110210702-903592506081 h1:0pHM github.com/charmbracelet/x/exp/golden v0.0.0-20251110210702-903592506081/go.mod h1:V8n/g3qVKNxr2FR37Y+otCsMySvZr601T0C7coEP0bw= github.com/charmbracelet/x/exp/strings v0.0.0-20251110210702-903592506081 h1:pTHy/fb1lG8MTw0FizbBQV9HHXEO2+MtPXkcE0S44nM= github.com/charmbracelet/x/exp/strings v0.0.0-20251110210702-903592506081/go.mod h1:/ehtMPNh9K4odGFkqYJKpIYyePhdp1hLBRvyY4bWkH8= -github.com/charmbracelet/x/exp/teatest v0.0.0-20251110210702-903592506081 h1:4V7nggB2MvTMnI03immNNETBuRZHZE9N/awjP77IooY= -github.com/charmbracelet/x/exp/teatest v0.0.0-20251110210702-903592506081/go.mod h1:aPVjFrBwbJgj5Qz1F0IXsnbcOVJcMKgu1ySUfTAxh7k= +github.com/charmbracelet/x/exp/teatest/v2 v2.0.0-20260413165052-6921c759c913 h1:UMEoVjbcvT7AhKY++IA3sJJwwhLrHjLfP6acSW0N6f0= +github.com/charmbracelet/x/exp/teatest/v2 v2.0.0-20260413165052-6921c759c913/go.mod h1:aRoQwQWmN9LBG2xi3sVByMFt2fdkPCagd0GAJ1qwOfw= 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/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= +github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI= github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4= -github.com/clipperhouse/displaywidth v0.5.0 h1:AIG5vQaSL2EKqzt0M9JMnvNxOCRTKUc4vUnLWGgP89I= -github.com/clipperhouse/displaywidth v0.5.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= -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.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= -github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -70,8 +78,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= 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/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= +github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= @@ -103,12 +111,14 @@ golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ= golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc= golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= diff --git a/nfpm.yaml b/nfpm.yaml index 16135db..c3b5afa 100644 --- a/nfpm.yaml +++ b/nfpm.yaml @@ -1,7 +1,7 @@ name: "poke-cli" arch: "arm64" platform: "linux" -version: "v1.9.2" +version: "v1.9.3" section: "default" version_schema: semver maintainer: "Christian S" diff --git a/styling/huh_theme.go b/styling/huh_theme.go new file mode 100644 index 0000000..98fa6a4 --- /dev/null +++ b/styling/huh_theme.go @@ -0,0 +1,52 @@ +package styling + +import ( + "github.com/charmbracelet/huh" + lipgloss "github.com/charmbracelet/lipgloss" +) + +func FormTheme() *huh.Theme { + var ( + yellow = lipgloss.Color(LightYellow) + blue = lipgloss.Color("#3B4CCA") + red = lipgloss.Color("#D00000") + black = lipgloss.Color("#000000") + normalFg = lipgloss.AdaptiveColor{Light: "235", Dark: "252"} + ) + t := huh.ThemeBase() + + t.Focused.Base = t.Focused.Base.BorderForeground(lipgloss.Color("238")) + t.Focused.Card = t.Focused.Base + t.Focused.Title = t.Focused.Title.Foreground(blue).Bold(true) + t.Focused.NoteTitle = t.Focused.NoteTitle.Foreground(blue).Bold(true).MarginBottom(1) + t.Focused.Directory = t.Focused.Directory.Foreground(blue) + t.Focused.Description = t.Focused.Description.Foreground(lipgloss.AdaptiveColor{Light: "", Dark: "243"}) + t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(red) + t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(red) + t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(red) + t.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(yellow) + t.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(yellow) + t.Focused.Option = t.Focused.Option.Foreground(normalFg) + t.Focused.MultiSelectSelector = t.Focused.MultiSelectSelector.Foreground(red) + t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(red) + t.Focused.SelectedPrefix = lipgloss.NewStyle().Foreground(red).SetString("✓ ") + t.Focused.UnselectedPrefix = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "", Dark: "243"}).SetString("• ") + t.Focused.UnselectedOption = t.Focused.UnselectedOption.Foreground(normalFg) + t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(black).Background(yellow) + t.Focused.Next = t.Focused.FocusedButton + + t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(yellow) + t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(lipgloss.AdaptiveColor{Light: "248", Dark: "238"}) + t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(red) + + t.Blurred = t.Focused + t.Blurred.Base = t.Focused.Base.BorderStyle(lipgloss.HiddenBorder()) + t.Blurred.Card = t.Blurred.Base + t.Blurred.NextIndicator = lipgloss.NewStyle() + t.Blurred.PrevIndicator = lipgloss.NewStyle() + + t.Group.Title = t.Focused.Title + t.Group.Description = t.Focused.Description + + return t +} diff --git a/styling/list.go b/styling/list.go index edd730b..a77b0e4 100644 --- a/styling/list.go +++ b/styling/list.go @@ -3,22 +3,32 @@ package styling import ( "fmt" "io" + "os" "strings" - "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + "charm.land/bubbles/v2/list" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" ) var ( TitleStyle = lipgloss.NewStyle().MarginLeft(2) ItemStyle = lipgloss.NewStyle().PaddingLeft(4) - SelectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(YellowAdaptive) - PaginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4) - HelpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1) + SelectedItemStyle lipgloss.Style + PaginationStyle lipgloss.Style + HelpStyle lipgloss.Style QuitTextStyle = lipgloss.NewStyle().Margin(1, 0, 2, 4) ) +func init() { + isDark := lipgloss.HasDarkBackground(os.Stdin, os.Stdout) + ld := lipgloss.LightDark(isDark) + defaults := list.DefaultStyles(isDark) + PaginationStyle = defaults.PaginationStyle.PaddingLeft(4) + HelpStyle = defaults.HelpStyle.PaddingLeft(4).PaddingBottom(1) + SelectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(ld(lipgloss.Color(DarkYellow), lipgloss.Color(LightYellow))) +} + type Item string func (i Item) FilterValue() string { return "" } diff --git a/styling/list_test.go b/styling/list_test.go index d43ce4d..0766cac 100644 --- a/styling/list_test.go +++ b/styling/list_test.go @@ -4,8 +4,8 @@ import ( "bytes" "testing" - "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" + "charm.land/bubbles/v2/list" + tea "charm.land/bubbletea/v2" ) func TestItemFilterValue(t *testing.T) { @@ -37,7 +37,7 @@ func TestItemDelegateSpacing(t *testing.T) { func TestItemDelegateUpdate(t *testing.T) { delegate := ItemDelegate{} - cmd := delegate.Update(tea.KeyMsg{}, &list.Model{}) + cmd := delegate.Update(tea.KeyPressMsg{}, &list.Model{}) if cmd != nil { t.Error("Expected Update to return nil, got non-nil value") diff --git a/styling/styling.go b/styling/styling.go index d4cea25..882ee73 100644 --- a/styling/styling.go +++ b/styling/styling.go @@ -3,11 +3,12 @@ package styling import ( "fmt" "image/color" + "os" "regexp" "strings" - "github.com/charmbracelet/huh" - "github.com/charmbracelet/lipgloss" + "charm.land/lipgloss/v2" + "golang.org/x/term" "golang.org/x/text/cases" "golang.org/x/text/language" ) @@ -22,31 +23,27 @@ const ( var ( YellowColor = lipgloss.Color(PrimaryYellow) - YellowAdaptive = lipgloss.AdaptiveColor{Light: DarkYellow, Dark: LightYellow} - YellowAdaptive2 = lipgloss.AdaptiveColor{Light: DarkYellow, Dark: PrimaryYellow} + YellowAdaptive color.Color + YellowAdaptive2 color.Color ) var ( Green = lipgloss.NewStyle().Foreground(lipgloss.Color("#38B000")) Red = lipgloss.NewStyle().Foreground(lipgloss.Color("#D00000")) Gray = lipgloss.Color("#777777") - Yellow = lipgloss.NewStyle().Foreground(YellowAdaptive) - ColoredBullet = lipgloss.NewStyle(). - SetString("•"). - Foreground(YellowColor) - CheckboxStyle = lipgloss.NewStyle().Foreground(YellowColor) + Yellow lipgloss.Style + ColoredBullet lipgloss.Style + CheckboxStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(PrimaryYellow)) KeyMenu = lipgloss.NewStyle().Foreground(lipgloss.Color("#777777")) - DocsLink = lipgloss.NewStyle(). - Foreground(YellowAdaptive2). - Render("\x1b]8;;https://docs.poke-cli.com\x1b\\docs.poke-cli.com\x1b]8;;\x1b\\") + DocsLink string StyleBold = lipgloss.NewStyle().Bold(true) StyleItalic = lipgloss.NewStyle().Italic(true) StyleUnderline = lipgloss.NewStyle().Underline(true) HelpBorder = lipgloss.NewStyle(). BorderStyle(lipgloss.RoundedBorder()). - BorderForeground(YellowColor) + BorderForeground(lipgloss.Color(PrimaryYellow)) ErrorColor = lipgloss.NewStyle().Foreground(lipgloss.Color("#F2055C")) ErrorBorder = lipgloss.NewStyle(). BorderStyle(lipgloss.RoundedBorder()). @@ -61,7 +58,7 @@ var ( BorderForeground(lipgloss.Color("#FF8C00")) TypesTableBorder = lipgloss.NewStyle(). BorderStyle(lipgloss.NormalBorder()). - BorderForeground(YellowColor) + BorderForeground(lipgloss.Color(PrimaryYellow)) ColorMap = map[string]string{ "normal": "#B7B7A9", "fire": "#FF4422", @@ -84,6 +81,23 @@ var ( } ) +func init() { + isDark := true + if term.IsTerminal(int(os.Stdin.Fd())) && term.IsTerminal(int(os.Stdout.Fd())) { + isDark = lipgloss.HasDarkBackground(os.Stdin, os.Stdout) + } + ld := lipgloss.LightDark(isDark) + YellowAdaptive = ld(lipgloss.Color(DarkYellow), lipgloss.Color(LightYellow)) + YellowAdaptive2 = ld(lipgloss.Color(DarkYellow), lipgloss.Color(PrimaryYellow)) + Yellow = lipgloss.NewStyle().Foreground(YellowAdaptive) + ColoredBullet = lipgloss.NewStyle(). + SetString("•"). + Foreground(lipgloss.Color(PrimaryYellow)) + DocsLink = lipgloss.NewStyle(). + Foreground(YellowAdaptive2). + Render("\x1b]8;;https://docs.poke-cli.com\x1b\\docs.poke-cli.com\x1b]8;;\x1b\\") +} + // GetTypeColor Helper function to get color for a given type name from colorMap func GetTypeColor(typeName string) string { typeColor := ColorMap[typeName] @@ -157,49 +171,3 @@ func (col Color) Hex() string { return fmt.Sprintf("#%02x%02x%02x", uint8(col.R*255.0+0.5), uint8(col.G*255.0+0.5), uint8(col.B*255.0+0.5)) } - -func FormTheme() *huh.Theme { - var ( - yellow = lipgloss.Color(LightYellow) - blue = lipgloss.Color("#3B4CCA") - red = lipgloss.Color("#D00000") - black = lipgloss.Color("#000000") - normalFg = lipgloss.AdaptiveColor{Light: "235", Dark: "252"} - ) - t := huh.ThemeBase() - - t.Focused.Base = t.Focused.Base.BorderForeground(lipgloss.Color("238")) - t.Focused.Card = t.Focused.Base - t.Focused.Title = t.Focused.Title.Foreground(blue).Bold(true) - t.Focused.NoteTitle = t.Focused.NoteTitle.Foreground(blue).Bold(true).MarginBottom(1) - t.Focused.Directory = t.Focused.Directory.Foreground(blue) - t.Focused.Description = t.Focused.Description.Foreground(lipgloss.AdaptiveColor{Light: "", Dark: "243"}) - t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(red) - t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(red) - t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(red) - t.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(yellow) - t.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(yellow) - t.Focused.Option = t.Focused.Option.Foreground(normalFg) - t.Focused.MultiSelectSelector = t.Focused.MultiSelectSelector.Foreground(red) - t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(red) - t.Focused.SelectedPrefix = lipgloss.NewStyle().Foreground(red).SetString("✓ ") - t.Focused.UnselectedPrefix = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "", Dark: "243"}).SetString("• ") - t.Focused.UnselectedOption = t.Focused.UnselectedOption.Foreground(normalFg) - t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(black).Background(yellow) - t.Focused.Next = t.Focused.FocusedButton - - t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(yellow) - t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(lipgloss.AdaptiveColor{Light: "248", Dark: "238"}) - t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(red) - - t.Blurred = t.Focused - t.Blurred.Base = t.Focused.Base.BorderStyle(lipgloss.HiddenBorder()) - t.Blurred.Card = t.Blurred.Base - t.Blurred.NextIndicator = lipgloss.NewStyle() - t.Blurred.PrevIndicator = lipgloss.NewStyle() - - t.Group.Title = t.Focused.Title - t.Group.Description = t.Focused.Description - - return t -} diff --git a/testdata/ability_invalid_flag.golden b/testdata/ability_invalid_flag.golden new file mode 100644 index 0000000..a7c394a --- /dev/null +++ b/testdata/ability_invalid_flag.golden @@ -0,0 +1 @@ +error parsing flags: flag provided but not defined: -bogus diff --git a/testdata/main_latest_flag.golden b/testdata/main_latest_flag.golden index d29afa4..5d9163c 100644 --- a/testdata/main_latest_flag.golden +++ b/testdata/main_latest_flag.golden @@ -2,6 +2,6 @@ ┃ ┃ ┃ Latest available release ┃ ┃ on GitHub: ┃ -┃ • v1.9.1 ┃ +┃ • v1.9.2 ┃ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ diff --git a/testdata/pokemon_invalid_flag.golden b/testdata/pokemon_invalid_flag.golden new file mode 100644 index 0000000..a7c394a --- /dev/null +++ b/testdata/pokemon_invalid_flag.golden @@ -0,0 +1 @@ +error parsing flags: flag provided but not defined: -bogus diff --git a/testdata/tcg_invalid_flag.golden b/testdata/tcg_invalid_flag.golden new file mode 100644 index 0000000..a7c394a --- /dev/null +++ b/testdata/tcg_invalid_flag.golden @@ -0,0 +1 @@ +error parsing flags: flag provided but not defined: -bogus diff --git a/web/pyproject.toml b/web/pyproject.toml index 7718174..8f1d0a3 100644 --- a/web/pyproject.toml +++ b/web/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "web" -version = "1.9.2" +version = "1.9.3" description = "Streamlit dashboard for browsing and visualizing Pokémon TCG tournament standings and results." readme = "README.md" requires-python = ">=3.12"