diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 86e8c1b..cc1ca57 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -31,7 +31,7 @@ on:
- main
env:
- VERSION_NUMBER: 'v1.8.6'
+ VERSION_NUMBER: 'v1.8.7'
DOCKERHUB_REGISTRY_NAME: 'digitalghostdev/poke-cli'
AWS_REGION: 'us-west-2'
diff --git a/.goreleaser.yml b/.goreleaser.yml
index c1b0403..e914dd7 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -14,7 +14,7 @@ builds:
- windows
- darwin
ldflags:
- - -s -w -X main.version=v1.8.6
+ - -s -w -X main.version=v1.8.7
archives:
- formats: [ 'zip' ]
diff --git a/Dockerfile b/Dockerfile
index 2d12483..efc542d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -8,7 +8,7 @@ RUN go mod download
COPY . .
-RUN go build -ldflags "-X main.version=v1.8.6" -o poke-cli .
+RUN go build -ldflags "-X main.version=v1.8.7" -o poke-cli .
# build 2
FROM --platform=$BUILDPLATFORM alpine:3.23
diff --git a/README.md b/README.md
index 0c6ff71..7254d01 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
-
+
@@ -96,11 +96,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.8.6 [subcommand] [flag]
+ docker run --rm -it digitalghostdev/poke-cli:v1.8.7 [subcommand] [flag]
```
* Enter the container and use its shell:
```bash
- docker run --rm -it --name poke-cli --entrypoint /bin/sh digitalghostdev/poke-cli:v1.8.6 -c "cd /app && exec sh"
+ docker run --rm -it --name poke-cli --entrypoint /bin/sh digitalghostdev/poke-cli:v1.8.7 -c "cd /app && exec sh"
# placed into the /app directory, run the program with './poke-cli'
# example: ./poke-cli ability swift-swim
```
diff --git a/card_data/pipelines/definitions.py b/card_data/pipelines/definitions.py
index d8a6cb1..e1c9c47 100644
--- a/card_data/pipelines/definitions.py
+++ b/card_data/pipelines/definitions.py
@@ -5,7 +5,11 @@
import dagster as dg
from .defs.extract.tcgcsv.extract_pricing import build_dataframe
+from .defs.extract.tcgdex.extract_sets import extract_sets_data
+from .defs.extract.tcgdex.extract_series import extract_series_data
from .defs.load.tcgcsv.load_pricing import load_pricing_data, data_quality_checks_on_pricing
+from .defs.load.tcgdex.load_sets import load_sets_data, data_quality_check_on_sets
+from .defs.load.tcgdex.load_series import load_series_data, data_quality_check_on_series
from .sensors import discord_success_sensor, discord_failure_sensor
@@ -13,9 +17,14 @@
def defs() -> dg.Definitions:
# load_from_defs_folder discovers dbt assets from transform_data.py
folder_defs: dg.Definitions = load_from_defs_folder(project_root=Path(__file__).parent.parent)
- return dg.Definitions.merge(folder_defs, defs_pricing)
+ return dg.Definitions.merge(folder_defs, defs_discord_sensors, defs_pricing, defs_sets, defs_series)
-# Define the pricing pipeline job that materializes the assets and downstream dbt model
+
+defs_discord_sensors: dg.Definitions = dg.Definitions(
+ sensors=[discord_success_sensor, discord_failure_sensor],
+)
+
+# Pricing pipeline
pricing_pipeline_job = dg.define_asset_job(
name="pricing_pipeline_job",
selection=dg.AssetSelection.assets(build_dataframe).downstream(include_self=True),
@@ -32,5 +41,26 @@ def defs() -> dg.Definitions:
assets=[build_dataframe, load_pricing_data, data_quality_checks_on_pricing],
jobs=[pricing_pipeline_job],
schedules=[price_schedule],
- sensors=[discord_success_sensor, discord_failure_sensor]
+)
+
+# Series pipeline
+series_pipeline_job = dg.define_asset_job(
+ name="series_pipeline_job",
+ selection=dg.AssetSelection.assets(extract_series_data).downstream(include_self=True),
+)
+
+defs_series: dg.Definitions = dg.Definitions(
+ assets=[extract_series_data, load_series_data, data_quality_check_on_series],
+ jobs=[series_pipeline_job],
+)
+
+# Sets pipeline
+sets_pipeline_job = dg.define_asset_job(
+ name="sets_pipeline_job",
+ selection=dg.AssetSelection.assets(extract_sets_data).downstream(include_self=True),
+)
+
+defs_sets: dg.Definitions = dg.Definitions(
+ assets=[extract_sets_data, load_sets_data, data_quality_check_on_sets],
+ jobs=[sets_pipeline_job],
)
\ No newline at end of file
diff --git a/card_data/pipelines/poke_cli_dbt/dbt_project.yml b/card_data/pipelines/poke_cli_dbt/dbt_project.yml
index 46f6ddf..ee0e8a3 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.8.6'
+version: '1.8.7'
profile: 'poke_cli_dbt'
diff --git a/card_data/pipelines/sensors.py b/card_data/pipelines/sensors.py
index 8707e30..00bdd81 100644
--- a/card_data/pipelines/sensors.py
+++ b/card_data/pipelines/sensors.py
@@ -18,6 +18,8 @@ def discord_success_sensor(context: RunStatusSensorContext):
timeout=10,
)
context.log.info(f"n8n response: {response.status_code}")
+ except requests.RequestException as e:
+ context.log.error(f"Requests or network error: {e}")
except Exception as e:
context.log.error(f"Failed to send notification: {e}")
@@ -36,5 +38,7 @@ def discord_failure_sensor(context: RunStatusSensorContext):
timeout=10,
)
context.log.info(f"n8n response: {response.status_code}")
+ except requests.RequestException as e:
+ context.log.error(f"Requests or network error: {e}")
except Exception as e:
context.log.error(f"Failed to send notification: {e}")
\ No newline at end of file
diff --git a/cmd/card/cardlist.go b/cmd/card/cardlist.go
index 6627f1b..b8e46b4 100644
--- a/cmd/card/cardlist.go
+++ b/cmd/card/cardlist.go
@@ -5,9 +5,11 @@ import (
"fmt"
"io"
"net/http"
+ "strings"
"time"
"github.com/charmbracelet/bubbles/table"
+ "github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/digitalghost-dev/poke-cli/styling"
@@ -19,13 +21,42 @@ type CardsModel struct {
ImageMap map[string]string
PriceMap map[string]string
RegulationMarkMap map[string]string
+ AllRows []table.Row
Quitting bool
+ Search textinput.Model
SelectedOption string
SeriesName string
Table table.Model
+ TableStyles table.Styles
ViewImage bool
}
+const (
+ activeTableSelectedBg lipgloss.Color = "#FFCC00"
+ inactiveTableSelectedBg lipgloss.Color = "#808080"
+)
+
+func cardTableStyles(selectedBg lipgloss.Color) table.Styles {
+ s := table.DefaultStyles()
+ s.Header = s.Header.
+ BorderStyle(lipgloss.NormalBorder()).
+ BorderForeground(lipgloss.Color("#FFCC00")).
+ BorderBottom(true)
+ s.Selected = s.Selected.
+ Foreground(lipgloss.Color("#000")).
+ Background(selectedBg)
+ return s
+}
+
+func (m *CardsModel) syncTableStylesForFocus() {
+ if m.Search.Focused() {
+ m.TableStyles = cardTableStyles(inactiveTableSelectedBg)
+ } else {
+ m.TableStyles = cardTableStyles(activeTableSelectedBg)
+ }
+ m.Table.SetStyles(m.TableStyles)
+}
+
func (m CardsModel) Init() tea.Cmd {
return nil
}
@@ -36,16 +67,46 @@ func (m CardsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
- case "esc", "ctrl+c":
+ case "ctrl+c":
m.Quitting = true
return m, tea.Quit
- case " ":
- m.ViewImage = true
+ case "esc":
+ // If in the search bar, exit search mode instead of quitting.
+ if m.Search.Focused() {
+ m.Search.Blur()
+ m.Table.Focus()
+ m.syncTableStylesForFocus()
+ return m, nil
+ }
+ m.Quitting = true
return m, tea.Quit
+ case "?":
+ if !m.Search.Focused() {
+ m.ViewImage = true
+ return m, tea.Quit
+ }
+ case "tab":
+ if m.Search.Focused() {
+ m.Search.Blur()
+ m.Table.Focus()
+ } else {
+ m.Table.Blur()
+ m.Search.Focus()
+ }
+ m.syncTableStylesForFocus()
+ return m, nil
}
}
- m.Table, bubbleCmd = m.Table.Update(msg)
+ if m.Search.Focused() {
+ prev := m.Search.Value()
+ m.Search, bubbleCmd = m.Search.Update(msg)
+ if m.Search.Value() != prev {
+ m.applyFilter()
+ }
+ } else {
+ m.Table, bubbleCmd = m.Table.Update(msg)
+ }
// Keep the selected option in sync on every update
if row := m.Table.SelectedRow(); len(row) > 0 {
@@ -58,6 +119,28 @@ func (m CardsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, bubbleCmd
}
+func (m *CardsModel) applyFilter() {
+ q := strings.TrimSpace(strings.ToLower(m.Search.Value()))
+ if q == "" {
+ m.Table.SetRows(m.AllRows)
+ m.Table.SetCursor(0)
+ return
+ }
+
+ filtered := make([]table.Row, 0, len(m.AllRows))
+ for _, r := range m.AllRows {
+ if len(r) == 0 {
+ continue
+ }
+ if strings.Contains(strings.ToLower(r[0]), q) {
+ filtered = append(filtered, r)
+ }
+ }
+
+ m.Table.SetRows(filtered)
+ m.Table.SetCursor(0)
+}
+
func (m CardsModel) View() string {
if m.Quitting {
return "\n Quitting card search...\n\n"
@@ -75,7 +158,8 @@ func (m CardsModel) View() string {
selectedCard = cardName + "\n---\n" + price + "\n---\n" + illustrator + "\n---\n" + regulationMark
}
- leftPanel := styling.TypesTableBorder.Render(m.Table.View())
+ leftContent := lipgloss.JoinVertical(lipgloss.Left, m.Search.View(), m.Table.View())
+ leftPanel := styling.TypesTableBorder.Render(leftContent)
rightPanel := lipgloss.NewStyle().
Width(40).
@@ -87,9 +171,10 @@ func (m CardsModel) View() string {
screen := lipgloss.JoinHorizontal(lipgloss.Top, leftPanel, rightPanel)
- return fmt.Sprintf("Highlight a card!\n%s\n%s",
+ return fmt.Sprintf(
+ "Highlight a card!\n%s\n%s",
screen,
- styling.KeyMenu.Render("β (move up)\nβ (move down)\nspace (view image)\nctrl+c | esc (quit)"))
+ styling.KeyMenu.Render("β (move up)\nβ (move down)\n? (view image)\ntab (toggle search)\nctrl+c | esc (quit)"))
}
type cardData struct {
@@ -117,10 +202,13 @@ func CardsList(setID string) (CardsModel, error) {
// Extract card names and build table rows + price map
rows := make([]table.Row, len(allCards))
+ allRows := rows
+
priceMap := make(map[string]string)
imageMap := make(map[string]string)
illustratorMap := make(map[string]string)
regulationMarkMap := make(map[string]string)
+
for i, card := range allCards {
rows[i] = []string{card.NumberPlusName}
if card.MarketPrice != 0 {
@@ -144,29 +232,32 @@ func CardsList(setID string) (CardsModel, error) {
imageMap[card.NumberPlusName] = card.ImageURL
}
+ ti := textinput.New()
+ ti.Placeholder = "type name..."
+ ti.Prompt = "π "
+ ti.CharLimit = 24
+ ti.Width = 30
+ ti.Blur()
+
t := table.New(
table.WithColumns([]table.Column{{Title: "Card Name", Width: 35}}),
table.WithRows(rows),
table.WithFocused(true),
- table.WithHeight(28),
+ table.WithHeight(27),
)
- s := table.DefaultStyles()
- s.Header = s.Header.
- BorderStyle(lipgloss.NormalBorder()).
- BorderForeground(lipgloss.Color("#FFCC00")).
- BorderBottom(true)
- s.Selected = s.Selected.
- Foreground(lipgloss.Color("#000")).
- Background(lipgloss.Color("#FFCC00"))
- t.SetStyles(s)
+ styles := cardTableStyles(activeTableSelectedBg)
+ t.SetStyles(styles)
return CardsModel{
- IllustratorMap: illustratorMap,
- ImageMap: imageMap,
- PriceMap: priceMap,
- RegulationMarkMap: regulationMarkMap,
- Table: t,
+ AllRows: allRows,
+ IllustratorMap: illustratorMap,
+ ImageMap: imageMap,
+ PriceMap: priceMap,
+ RegulationMarkMap: regulationMarkMap,
+ Search: ti,
+ Table: t,
+ TableStyles: styles,
}, nil
}
diff --git a/cmd/card/cardlist_test.go b/cmd/card/cardlist_test.go
index e3c51e9..a016a01 100644
--- a/cmd/card/cardlist_test.go
+++ b/cmd/card/cardlist_test.go
@@ -8,6 +8,7 @@ import (
"testing"
"github.com/charmbracelet/bubbles/table"
+ "github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
)
@@ -91,7 +92,58 @@ func TestCardsModel_Update_CtrlC(t *testing.T) {
}
}
-func TestCardsModel_Update_SpaceBar(t *testing.T) {
+func TestCardsModel_Update_TabTogglesSearchFocusAndTableSelectedBackground(t *testing.T) {
+ rows := []table.Row{{"001/198 - Bulbasaur"}}
+ columns := []table.Column{{Title: "Card Name", Width: 35}}
+
+ tbl := table.New(
+ table.WithColumns(columns),
+ table.WithRows(rows),
+ table.WithFocused(true),
+ )
+
+ search := textinput.New()
+ search.Blur()
+
+ initialStyles := cardTableStyles(activeTableSelectedBg)
+ tbl.SetStyles(initialStyles)
+
+ model := CardsModel{
+ Search: search,
+ Table: tbl,
+ TableStyles: initialStyles,
+ }
+
+ // Tab into the search bar.
+ newModel, _ := model.Update(tea.KeyMsg{Type: tea.KeyTab})
+ m1 := newModel.(CardsModel)
+ if !m1.Search.Focused() {
+ t.Fatal("expected search to be focused after tab")
+ }
+
+ bg1 := m1.TableStyles.Selected.GetBackground()
+ r1, g1, b1, a1 := bg1.RGBA()
+ re, ge, be, ae := inactiveTableSelectedBg.RGBA()
+ if r1 != re || g1 != ge || b1 != be || a1 != ae {
+ t.Fatalf("expected selected background to be gray when searching; got RGBA(%d,%d,%d,%d)", r1, g1, b1, a1)
+ }
+
+ // Tab back to the table.
+ newModel2, _ := m1.Update(tea.KeyMsg{Type: tea.KeyTab})
+ m2 := newModel2.(CardsModel)
+ if m2.Search.Focused() {
+ t.Fatal("expected search to be blurred after tabbing back")
+ }
+
+ bg2 := m2.TableStyles.Selected.GetBackground()
+ r2, g2, b2, a2 := bg2.RGBA()
+ re2, ge2, be2, ae2 := activeTableSelectedBg.RGBA()
+ if r2 != re2 || g2 != ge2 || b2 != be2 || a2 != ae2 {
+ t.Fatalf("expected selected background to be yellow when table is focused; got RGBA(%d,%d,%d,%d)", r2, g2, b2, a2)
+ }
+}
+
+func TestCardsModel_Update_ViewImageKey_QuestionMark(t *testing.T) {
rows := []table.Row{
{"001/198 - Bulbasaur"},
}
@@ -109,17 +161,51 @@ func TestCardsModel_Update_SpaceBar(t *testing.T) {
ViewImage: false,
}
- msg := tea.KeyMsg{Type: tea.KeySpace}
+ msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'?'}}
newModel, cmd := model.Update(msg)
resultModel := newModel.(CardsModel)
if !resultModel.ViewImage {
- t.Error("ViewImage should be set to true when SPACE is pressed")
+ t.Error("ViewImage should be set to true when '?' is pressed")
}
if cmd == nil {
- t.Error("Update with SPACE should return tea.Quit command")
+ t.Error("Update with '?' should return tea.Quit command")
+ }
+}
+
+func TestCardsModel_Update_ViewImageKey_DoesNotOverrideSearch(t *testing.T) {
+ rows := []table.Row{{"001/198 - Bulbasaur"}}
+ columns := []table.Column{{Title: "Card Name", Width: 35}}
+
+ tbl := table.New(
+ table.WithColumns(columns),
+ table.WithRows(rows),
+ table.WithFocused(true),
+ )
+
+ search := textinput.New()
+ search.Focus()
+
+ model := CardsModel{
+ Search: search,
+ Table: tbl,
+ ViewImage: false,
+ }
+
+ msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'?'}}
+ newModel, _ := model.Update(msg)
+ resultModel := newModel.(CardsModel)
+
+ if resultModel.ViewImage {
+ t.Fatal("expected ViewImage to remain false when typing '?' in the search field")
+ }
+ if resultModel.Quitting {
+ t.Fatal("expected Quitting to remain false when typing in the search field")
+ }
+ if got := resultModel.Search.Value(); got != "?" {
+ t.Fatalf("expected search input to receive '?'; got %q", got)
}
}
diff --git a/cmd/move/move.go b/cmd/move/move.go
index 37792fb..0ddd236 100644
--- a/cmd/move/move.go
+++ b/cmd/move/move.go
@@ -96,18 +96,35 @@ func moveInfoContainer(output *strings.Builder, moveStruct structs.MoveJSONStruc
}
func moveEffectContainer(output *strings.Builder, moveStruct structs.MoveJSONStruct) {
+ var sv string
+ var swsh string
+
docStyle := lipgloss.NewStyle().
Padding(1, 2).
BorderStyle(lipgloss.ThickBorder()).
BorderForeground(lipgloss.Color(styling.GetTypeColor(moveStruct.Type.Name))).
Width(32)
- var flavorTextEntry string
for _, entry := range moveStruct.FlavorTextEntries {
- if entry.Language.Name == "en" && entry.VersionGroup.Name == "scarlet-violet" {
- flavorTextEntry = entry.FlavorText
+ if entry.Language.Name != "en" {
+ continue
+ }
+
+ if entry.VersionGroup.Name == "scarlet-violet" {
+ sv = entry.FlavorText
break
}
+
+ if entry.VersionGroup.Name == "sword-shield" {
+ swsh = entry.FlavorText
+ }
+ }
+
+ flavorTextEntry := "API missing data."
+ if sv != "" {
+ flavorTextEntry = sv
+ } else if swsh != "" {
+ flavorTextEntry = swsh
}
effectBold := styling.StyleBold.Render("Effect:")
diff --git a/nfpm.yaml b/nfpm.yaml
index 65a9d59..25201c9 100644
--- a/nfpm.yaml
+++ b/nfpm.yaml
@@ -1,7 +1,7 @@
name: "poke-cli"
arch: "arm64"
platform: "linux"
-version: "v1.8.6"
+version: "v1.8.7"
section: "default"
version_schema: semver
maintainer: "Christian S"
diff --git a/testdata/main_latest_flag.golden b/testdata/main_latest_flag.golden
index 78b7cbb..cd92649 100644
--- a/testdata/main_latest_flag.golden
+++ b/testdata/main_latest_flag.golden
@@ -2,6 +2,6 @@
β β
β Latest available release β
β on GitHub: β
-β β’ v1.8.5 β
+β β’ v1.8.6 β
β β
ββββββββββββββββββββββββββββββββ
diff --git a/testdata/move_2.golden b/testdata/move_2.golden
new file mode 100644
index 0000000..b878200
--- /dev/null
+++ b/testdata/move_2.golden
@@ -0,0 +1,23 @@
+ββββββββββββββββββββββββββββββββββ
+β β
+β Thousand Arrows β
+β β
+β Type |Ground β
+β Power |90 β
+β PP |10 β
+β Accuracy |100 β
+β Category |Physical β
+β Effect Chance |100% β
+β Priority |0 β
+β β
+ββββββββββββββββββββββββββββββββββ
+ββββββββββββββββββββββββββββββββββ
+β β
+β Effect: β
+β This move also hits opposing β
+β PokΓ©mon that are in β
+β the air. Those PokΓ©mon are β
+β knocked down to β
+β the ground. β
+β β
+ββββββββββββββββββββββββββββββββββ