diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 876491e6..36211ec2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -31,7 +31,7 @@ on:
- main
env:
- VERSION_NUMBER: 'v1.9.0'
+ VERSION_NUMBER: 'v1.9.1'
DOCKERHUB_REGISTRY_NAME: 'digitalghostdev/poke-cli'
AWS_REGION: 'us-west-2'
@@ -96,7 +96,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
- go-version: '1.24.6'
+ go-version: '1.25.8'
- name: Build Go Binary
env:
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index aa292cd3..5061b8c0 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -16,7 +16,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
- go-version: 1.24
+ go-version: 1.25
- name: Install dependencies
run: |
diff --git a/.github/workflows/go_lint.yml b/.github/workflows/go_lint.yml
index 6c06f7f7..c9314a04 100644
--- a/.github/workflows/go_lint.yml
+++ b/.github/workflows/go_lint.yml
@@ -20,10 +20,10 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
- go-version: 1.24
+ go-version: 1.25
- name: Lint
uses: golangci/golangci-lint-action@v7
with:
- version: v2.0.1
+ version: v2.11.4
skip-cache: true
diff --git a/.github/workflows/go_test.yml b/.github/workflows/go_test.yml
index efb93b38..8b72de58 100644
--- a/.github/workflows/go_test.yml
+++ b/.github/workflows/go_test.yml
@@ -16,7 +16,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
- go-version: 1.24
+ go-version: 1.25
- name: Install dependencies
run: |
diff --git a/.github/workflows/codspeed.yml b/.github/workflows/python_testing.yml
similarity index 52%
rename from .github/workflows/codspeed.yml
rename to .github/workflows/python_testing.yml
index 7c08c7d2..a188d6ae 100644
--- a/.github/workflows/codspeed.yml
+++ b/.github/workflows/python_testing.yml
@@ -1,21 +1,24 @@
-name: Codspeed Benchmarks
+name: Python Tests
+
on:
push:
branches:
- main
paths:
- 'card_data/**'
+ - 'web/**'
pull_request:
- types: [ opened, reopened, synchronize ]
+ types: [opened, reopened, synchronize]
paths:
- 'card_data/**'
+ - 'web/**'
permissions:
contents: read
id-token: write
jobs:
- benchmarks:
+ card-data-benchmarks:
runs-on: ubuntu-22.04
defaults:
run:
@@ -41,4 +44,28 @@ jobs:
with:
working-directory: card_data
mode: simulation
- run: uv run pytest pipelines/tests/ -v --codspeed
\ No newline at end of file
+ run: uv run pytest pipelines/tests/ -v --codspeed
+
+ streamlit-testing:
+ runs-on: ubuntu-22.04
+ defaults:
+ run:
+ working-directory: web
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Setup Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: '3.12'
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v7
+
+ - name: Install dependencies
+ run: uv sync --dev
+
+ - name: Run tests
+ run: uv run pytest app_test.py -v
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index a3b2810d..d3f3c46d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -18,7 +18,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
- go-version: 1.24
+ go-version: 1.25
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
diff --git a/.goreleaser.yml b/.goreleaser.yml
index e2d9e31c..fdaea092 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -14,7 +14,7 @@ builds:
- windows
- darwin
ldflags:
- - -s -w -X main.version=v1.9.0
+ - -s -w -X main.version=v1.9.1
archives:
- formats: [ 'zip' ]
diff --git a/Dockerfile b/Dockerfile
index 40c60e99..f3af9367 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
# build 1
-FROM golang:1.24.12-alpine3.23 AS build
+FROM golang:1.25.8-alpine3.23 AS build
WORKDIR /app
@@ -8,7 +8,7 @@ RUN go mod download
COPY . .
-RUN go build -ldflags "-X main.version=v1.9.0" -o poke-cli .
+RUN go build -ldflags "-X main.version=v1.9.1" -o poke-cli .
# build 2
FROM --platform=$BUILDPLATFORM alpine:3.23
diff --git a/README.md b/README.md
index 885625f0..2d0fc9be 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
-
+
@@ -99,15 +99,29 @@ 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.0
[subcommand] [flag]
+ docker run --rm -it digitalghostdev/poke-cli:v1.9.1 [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.0 -c "cd /app && exec sh"
+ docker run --rm -it --name poke-cli --entrypoint /bin/sh digitalghostdev/poke-cli:v1.9.1 -c "cd /app && exec sh"
# placed into the /app directory, run the program with './poke-cli'
# example: ./poke-cli ability swift-swim
```
+> [!NOTE]
+> 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.1 card
+>
+> # WezTerm, iTerm2, Ghostty, Konsole, Rio, Tabby
+> docker run --rm -it -e TERM -e TERM_PROGRAM digitalghostdev/poke-cli:v1.9.1 card
+>
+> # Windows Terminal (Sixel)
+> docker run --rm -it -e WT_SESSION digitalghostdev/poke-cli:v1.9.1 card
+> ```
+> If your terminal is not listed above, image rendering is not supported inside Docker.
+
### Binary
1. Head to the [releases](https://github.com/digitalghost-dev/poke-cli/releases) page of the project.
diff --git a/card_data/pipelines/defs/extract/tcgcsv/extract_pricing.py b/card_data/pipelines/defs/extract/tcgcsv/extract_pricing.py
index ba43e59f..5c9babe3 100644
--- a/card_data/pipelines/defs/extract/tcgcsv/extract_pricing.py
+++ b/card_data/pipelines/defs/extract/tcgcsv/extract_pricing.py
@@ -11,6 +11,7 @@
SET_PRODUCT_MATCHING = {
# Mega Evolution
+ "me03": "24587",
"me02.5": "24541",
"me02": "24448",
"me01": "24380",
diff --git a/card_data/pipelines/defs/extract/tcgdex/extract_cards.py b/card_data/pipelines/defs/extract/tcgdex/extract_cards.py
index 37f2f8b7..225b963f 100644
--- a/card_data/pipelines/defs/extract/tcgdex/extract_cards.py
+++ b/card_data/pipelines/defs/extract/tcgdex/extract_cards.py
@@ -10,7 +10,7 @@
@dg.asset(kinds={"API"}, name="extract_card_url_from_set_data")
def extract_card_url_from_set() -> list:
- urls = ["https://api.tcgdex.net/v2/en/sets/me02.5"]
+ urls = ["https://api.tcgdex.net/v2/en/sets/me03"]
all_card_urls = []
diff --git a/card_data/pipelines/soda/checks_sets.yml b/card_data/pipelines/soda/checks_sets.yml
index 42307555..54eb2b5d 100644
--- a/card_data/pipelines/soda/checks_sets.yml
+++ b/card_data/pipelines/soda/checks_sets.yml
@@ -1,6 +1,6 @@
checks for sets:
# Row count validation
- - row_count > 41
+ - row_count > 50
# Schema validation checks
- schema:
diff --git a/card_data/pipelines/tests/extract_series_test.py b/card_data/pipelines/tests/extract_series_test.py
index 1ce99587..0dbc2302 100644
--- a/card_data/pipelines/tests/extract_series_test.py
+++ b/card_data/pipelines/tests/extract_series_test.py
@@ -8,6 +8,7 @@
import responses
from pipelines.defs.extract.tcgdex.extract_series import extract_series_data
+
@pytest.fixture
def mock_api_response():
"""Sample API response matching tcgdex format"""
@@ -19,11 +20,10 @@ def mock_api_response():
{"id": "sm", "name": "Sun & Moon", "logo": None},
]
-@pytest.mark.benchmark
+
@responses.activate
-def test_extract_series_data_success(mock_api_response):
+def test_extract_series_data_success(benchmark, mock_api_response):
"""Test successful extraction and filtering"""
- # Mock the API call
responses.add(
responses.GET,
"https://api.tcgdex.net/v2/en/series",
@@ -31,11 +31,10 @@ def test_extract_series_data_success(mock_api_response):
status=200
)
- result = extract_series_data()
+ result = benchmark(extract_series_data)
- # Assertions
- assert isinstance(result, pl.DataFrame) # nosec
- assert len(result) == 4 # nosec
- assert set(result["id"].to_list()) == {"swsh", "sv", "me", "sm"} # nosec
- assert "name" in result.columns # nosec
- assert "logo" in result.columns # nosec
\ No newline at end of file
+ assert isinstance(result, pl.DataFrame) # nosec
+ assert len(result) == 4 # nosec
+ assert set(result["id"].to_list()) == {"swsh", "sv", "me", "sm"} # nosec
+ assert "name" in result.columns # nosec
+ assert "logo" in result.columns # nosec
diff --git a/card_data/pipelines/tests/extract_sets_test.py b/card_data/pipelines/tests/extract_sets_test.py
index 521fdca1..e086ae47 100644
--- a/card_data/pipelines/tests/extract_sets_test.py
+++ b/card_data/pipelines/tests/extract_sets_test.py
@@ -82,11 +82,9 @@ def mock_api_response():
}
-@pytest.mark.benchmark
@responses.activate
-def test_extract_sets_data_success(mock_api_response):
+def test_extract_sets_data_success(benchmark, mock_api_response):
"""Test successful extraction of sets from multiple series"""
- # Mock all API calls
for url, response_data in mock_api_response.items():
responses.add(
responses.GET,
@@ -95,9 +93,8 @@ def test_extract_sets_data_success(mock_api_response):
status=200,
)
- result = extract_sets_data()
+ result = benchmark(extract_sets_data)
- # Assertions
assert isinstance(result, pl.DataFrame) # nosec
assert len(result) == 6 # nosec (2 + 2 + 1 + 1 sets)
assert set(result.columns) == { # nosec
@@ -113,11 +110,9 @@ def test_extract_sets_data_success(mock_api_response):
assert set(result["set_id"].to_list()) == {"me01", "me02", "sv01", "sv02", "swsh1", "sm1"} # nosec
-@pytest.mark.benchmark
@responses.activate
-def test_extract_sets_data_empty_sets(mock_api_response):
+def test_extract_sets_data_empty_sets(benchmark, mock_api_response):
"""Test extraction when a series has no sets"""
- # Modify one response to have empty sets
mock_api_response["https://api.tcgdex.net/v2/en/series/me"]["sets"] = []
for url, response_data in mock_api_response.items():
@@ -128,16 +123,15 @@ def test_extract_sets_data_empty_sets(mock_api_response):
status=200,
)
- result = extract_sets_data()
+ result = benchmark(extract_sets_data)
assert isinstance(result, pl.DataFrame) # nosec
assert len(result) == 4 # nosec (0 + 2 + 1 + 1 sets)
assert "me" not in result["series_id"].to_list() # nosec
-@pytest.mark.benchmark
@responses.activate
-def test_extract_sets_data_null_card_counts():
+def test_extract_sets_data_null_card_counts(benchmark):
"""Test extraction with null card counts"""
mock_responses = {
"https://api.tcgdex.net/v2/en/series/me": {
@@ -178,7 +172,7 @@ def test_extract_sets_data_null_card_counts():
status=200,
)
- result = extract_sets_data()
+ result = benchmark(extract_sets_data)
assert isinstance(result, pl.DataFrame) # nosec
assert len(result) == 1 # nosec
diff --git a/card_data/pipelines/tests/json_retriever_test.py b/card_data/pipelines/tests/json_retriever_test.py
index 316da8fc..f2697bdc 100644
--- a/card_data/pipelines/tests/json_retriever_test.py
+++ b/card_data/pipelines/tests/json_retriever_test.py
@@ -9,9 +9,8 @@
from pipelines.utils.json_retriever import fetch_json
-@pytest.mark.benchmark
@responses.activate
-def test_fetch_json_success():
+def test_fetch_json_success(benchmark):
"""Test successful JSON retrieval."""
responses.add(
responses.GET,
@@ -20,16 +19,15 @@ def test_fetch_json_success():
status=200,
)
- result = fetch_json("https://api.example.com/data")
+ result = benchmark(fetch_json, "https://api.example.com/data")
assert isinstance(result, dict) # nosec
assert result["id"] == 1 # nosec
assert result["name"] == "Pikachu" # nosec
-@pytest.mark.benchmark
@responses.activate
-def test_fetch_json_with_nested_data():
+def test_fetch_json_with_nested_data(benchmark):
"""Test retrieval of nested JSON structures."""
payload = {
"results": [
@@ -45,16 +43,15 @@ def test_fetch_json_with_nested_data():
status=200,
)
- result = fetch_json("https://api.example.com/products")
+ result = benchmark(fetch_json, "https://api.example.com/products")
assert result["totalItems"] == 2 # nosec
assert len(result["results"]) == 2 # nosec
assert result["results"][0]["productId"] == 100 # nosec
-@pytest.mark.benchmark
@responses.activate
-def test_fetch_json_http_404():
+def test_fetch_json_http_404(benchmark):
"""Test that a 404 response raises HTTPError."""
responses.add(
responses.GET,
@@ -63,13 +60,15 @@ def test_fetch_json_http_404():
status=404,
)
- with pytest.raises(requests.exceptions.HTTPError):
- fetch_json("https://api.example.com/missing")
+ def run():
+ with pytest.raises(requests.exceptions.HTTPError):
+ fetch_json("https://api.example.com/missing")
+
+ benchmark(run)
-@pytest.mark.benchmark
@responses.activate
-def test_fetch_json_http_500():
+def test_fetch_json_http_500(benchmark):
"""Test that a 500 response raises HTTPError."""
responses.add(
responses.GET,
@@ -78,13 +77,15 @@ def test_fetch_json_http_500():
status=500,
)
- with pytest.raises(requests.exceptions.HTTPError):
- fetch_json("https://api.example.com/error")
+ def run():
+ with pytest.raises(requests.exceptions.HTTPError):
+ fetch_json("https://api.example.com/error")
+
+ benchmark(run)
-@pytest.mark.benchmark
@responses.activate
-def test_fetch_json_connection_error():
+def test_fetch_json_connection_error(benchmark):
"""Test that a connection error raises ConnectionError."""
responses.add(
responses.GET,
@@ -92,13 +93,15 @@ def test_fetch_json_connection_error():
body=requests.exceptions.ConnectionError("Connection refused"),
)
- with pytest.raises(requests.exceptions.ConnectionError):
- fetch_json("https://api.example.com/down")
+ def run():
+ with pytest.raises(requests.exceptions.ConnectionError):
+ fetch_json("https://api.example.com/down")
+
+ benchmark(run)
-@pytest.mark.benchmark
@responses.activate
-def test_fetch_json_timeout():
+def test_fetch_json_timeout(benchmark):
"""Test that a timeout raises an appropriate exception."""
responses.add(
responses.GET,
@@ -106,13 +109,15 @@ def test_fetch_json_timeout():
body=requests.exceptions.ReadTimeout("Read timed out"),
)
- with pytest.raises(requests.exceptions.ReadTimeout):
- fetch_json("https://api.example.com/slow")
+ def run():
+ with pytest.raises(requests.exceptions.ReadTimeout):
+ fetch_json("https://api.example.com/slow")
+
+ benchmark(run)
-@pytest.mark.benchmark
@responses.activate
-def test_fetch_json_empty_object():
+def test_fetch_json_empty_object(benchmark):
"""Test retrieval of an empty JSON object."""
responses.add(
responses.GET,
@@ -121,14 +126,13 @@ def test_fetch_json_empty_object():
status=200,
)
- result = fetch_json("https://api.example.com/empty")
+ result = benchmark(fetch_json, "https://api.example.com/empty")
assert result == {} # nosec
-@pytest.mark.benchmark
@responses.activate
-def test_fetch_json_invalid_json():
+def test_fetch_json_invalid_json(benchmark):
"""Test that an invalid JSON body raises a ValueError (JSONDecodeError)."""
responses.add(
responses.GET,
@@ -138,5 +142,8 @@ def test_fetch_json_invalid_json():
content_type="application/json",
)
- with pytest.raises(requests.exceptions.JSONDecodeError):
- fetch_json("https://api.example.com/bad")
+ def run():
+ with pytest.raises(requests.exceptions.JSONDecodeError):
+ fetch_json("https://api.example.com/bad")
+
+ benchmark(run)
diff --git a/card_data/pipelines/tests/secret_retriever_test.py b/card_data/pipelines/tests/secret_retriever_test.py
index 4ba9f92d..b9664af5 100644
--- a/card_data/pipelines/tests/secret_retriever_test.py
+++ b/card_data/pipelines/tests/secret_retriever_test.py
@@ -8,7 +8,7 @@
from unittest.mock import patch, MagicMock
from pipelines.utils.secret_retriever import fetch_secret, fetch_n8n_webhook_secret
-@pytest.mark.benchmark
+
@patch("pipelines.utils.secret_retriever.SecretCache")
@patch("pipelines.utils.secret_retriever.botocore.session.get_session")
def test_fetch_secret_success(mock_get_session, mock_secret_cache_cls):
@@ -25,7 +25,6 @@ def test_fetch_secret_success(mock_get_session, mock_secret_cache_cls):
mock_cache_instance.get_secret_string.assert_called_once_with("supabase")
-@pytest.mark.benchmark
@patch("pipelines.utils.secret_retriever.SecretCache")
@patch("pipelines.utils.secret_retriever.botocore.session.get_session")
def test_fetch_secret_missing_key(mock_get_session, mock_secret_cache_cls):
@@ -40,7 +39,6 @@ def test_fetch_secret_missing_key(mock_get_session, mock_secret_cache_cls):
fetch_secret()
-@pytest.mark.benchmark
@patch("pipelines.utils.secret_retriever.SecretCache")
@patch("pipelines.utils.secret_retriever.botocore.session.get_session")
def test_fetch_secret_invalid_json(mock_get_session, mock_secret_cache_cls):
@@ -53,7 +51,6 @@ def test_fetch_secret_invalid_json(mock_get_session, mock_secret_cache_cls):
fetch_secret()
-@pytest.mark.benchmark
@patch("pipelines.utils.secret_retriever.SecretCache")
@patch("pipelines.utils.secret_retriever.botocore.session.get_session")
def test_fetch_secret_empty_json_object(mock_get_session, mock_secret_cache_cls):
@@ -66,7 +63,6 @@ def test_fetch_secret_empty_json_object(mock_get_session, mock_secret_cache_cls)
fetch_secret()
-@pytest.mark.benchmark
@patch("pipelines.utils.secret_retriever.SecretCache")
@patch("pipelines.utils.secret_retriever.botocore.session.get_session")
def test_fetch_secret_cache_raises(mock_get_session, mock_secret_cache_cls):
@@ -86,7 +82,6 @@ def test_fetch_secret_cache_raises(mock_get_session, mock_secret_cache_cls):
# ---------------------------------------------------------------------------
-@pytest.mark.benchmark
@patch("pipelines.utils.secret_retriever.SecretCache")
@patch("pipelines.utils.secret_retriever.botocore.session.get_session")
def test_fetch_n8n_webhook_secret_success(mock_get_session, mock_secret_cache_cls):
@@ -103,7 +98,6 @@ def test_fetch_n8n_webhook_secret_success(mock_get_session, mock_secret_cache_cl
mock_cache_instance.get_secret_string.assert_called_once_with("n8n_webhook")
-@pytest.mark.benchmark
@patch("pipelines.utils.secret_retriever.SecretCache")
@patch("pipelines.utils.secret_retriever.botocore.session.get_session")
def test_fetch_n8n_webhook_secret_missing_key(mock_get_session, mock_secret_cache_cls):
@@ -118,7 +112,6 @@ def test_fetch_n8n_webhook_secret_missing_key(mock_get_session, mock_secret_cach
fetch_n8n_webhook_secret()
-@pytest.mark.benchmark
@patch("pipelines.utils.secret_retriever.SecretCache")
@patch("pipelines.utils.secret_retriever.botocore.session.get_session")
def test_fetch_n8n_webhook_secret_invalid_json(
@@ -133,7 +126,6 @@ def test_fetch_n8n_webhook_secret_invalid_json(
fetch_n8n_webhook_secret()
-@pytest.mark.benchmark
@patch("pipelines.utils.secret_retriever.SecretCache")
@patch("pipelines.utils.secret_retriever.botocore.session.get_session")
def test_fetch_n8n_webhook_secret_empty_json_object(
@@ -148,7 +140,6 @@ def test_fetch_n8n_webhook_secret_empty_json_object(
fetch_n8n_webhook_secret()
-@pytest.mark.benchmark
@patch("pipelines.utils.secret_retriever.SecretCache")
@patch("pipelines.utils.secret_retriever.botocore.session.get_session")
def test_fetch_n8n_webhook_secret_cache_raises(
diff --git a/cmd/card/card.go b/cmd/card/card.go
index 6b9ef350..6881ed12 100644
--- a/cmd/card/card.go
+++ b/cmd/card/card.go
@@ -36,34 +36,32 @@ func CardCommand() (string, error) {
return output.String(), err
}
- seriesModel := SeriesList()
// Program 1: Series selection
- finalModel, err := tea.NewProgram(seriesModel, tea.WithAltScreen()).Run()
+ finalModel, err := tea.NewProgram(SeriesList(), tea.WithAltScreen()).Run()
if err != nil {
return "", fmt.Errorf("error running series selection program: %w", err)
}
- result, ok := finalModel.(SeriesModel)
+ result, ok := finalModel.(seriesModel)
if !ok {
- return "", fmt.Errorf("unexpected model type from series selection: got %T, want SeriesModel", finalModel)
+ return "", fmt.Errorf("unexpected model type from series selection: got %T, want seriesModel", finalModel)
}
if result.SeriesID != "" {
// Program 2: Sets selection
- setsModel, err := SetsList(result.SeriesID)
-
+ setsMdl, err := SetsList(result.SeriesID)
if err != nil {
return "", fmt.Errorf("error loading sets: %w", err)
}
- finalSetsModel, err := tea.NewProgram(setsModel, tea.WithAltScreen()).Run()
+ finalSetsModel, err := tea.NewProgram(setsMdl, tea.WithAltScreen()).Run()
if err != nil {
return "", fmt.Errorf("error running sets selection program: %w", err)
}
- setsResult, ok := finalSetsModel.(SetsModel)
+ setsResult, ok := finalSetsModel.(setsModel)
if !ok {
- return "", fmt.Errorf("unexpected model type from sets selection: got %T, want SetsModel", finalSetsModel)
+ return "", fmt.Errorf("unexpected model type from sets selection: got %T, want setsModel", finalSetsModel)
}
if setsResult.Quitting {
@@ -72,34 +70,33 @@ func CardCommand() (string, error) {
// Program 3: Cards display
if setsResult.SetID != "" {
- cardsModel, err := CardsList(setsResult.SetID)
+ cardsMdl, err := CardsList(setsResult.SetID)
if err != nil {
return "", fmt.Errorf("error loading cards: %w", err)
}
for {
- finalCardsModel, err := tea.NewProgram(cardsModel, tea.WithAltScreen()).Run()
+ finalCardsModel, err := tea.NewProgram(cardsMdl, tea.WithAltScreen()).Run()
if err != nil {
return "", fmt.Errorf("error running cards program: %w", err)
}
- cardsResult, ok := finalCardsModel.(CardsModel)
+ cardsResult, ok := finalCardsModel.(cardsModel)
if !ok {
- return "", fmt.Errorf("unexpected model type from cards display: got %T, want CardsModel", finalCardsModel)
+ return "", fmt.Errorf("unexpected model type from cards display: got %T, want cardsModel", finalCardsModel)
}
if cardsResult.ViewImage {
// Launch image viewer
imageURL := cardsResult.ImageMap[cardsResult.SelectedOption]
- imageModel := ImageRenderer(cardsResult.SelectedOption, imageURL)
- _, err := tea.NewProgram(imageModel, tea.WithAltScreen()).Run()
+ _, err := tea.NewProgram(ImageRenderer(cardsResult.SelectedOption, imageURL), tea.WithAltScreen()).Run()
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: image viewer error: %v\n", err)
}
// Re-launch cards with same state
cardsResult.ViewImage = false
- cardsModel = cardsResult
+ cardsMdl = cardsResult
} else {
break
}
diff --git a/cmd/card/cardlist.go b/cmd/card/cardlist.go
index cead01d7..65c457a3 100644
--- a/cmd/card/cardlist.go
+++ b/cmd/card/cardlist.go
@@ -16,7 +16,7 @@ import (
var getCardData = connections.CallTCGData
-type CardsModel struct {
+type cardsModel struct {
AllRows []table.Row
Choice string
Error error
@@ -63,7 +63,7 @@ func cardTableStyles(selectedBg lipgloss.Color) table.Styles {
return s
}
-func syncTableStylesForFocus(m *CardsModel) {
+func syncTableStylesForFocus(m *cardsModel) {
if m.Search.Focused() {
m.TableStyles = cardTableStyles(inactiveTableSelectedBg)
} else {
@@ -126,14 +126,14 @@ func fetchCardsCmd(setID string) tea.Cmd {
}
}
-func (m CardsModel) Init() tea.Cmd {
+func (m cardsModel) Init() tea.Cmd {
return tea.Batch(
m.Spinner.Tick,
fetchCardsCmd(m.SetID),
)
}
-func (m CardsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (m cardsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var bubbleCmd tea.Cmd
switch msg := msg.(type) {
@@ -239,7 +239,7 @@ func (m CardsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, bubbleCmd
}
-func applyFilter(m *CardsModel) {
+func applyFilter(m *cardsModel) {
q := strings.TrimSpace(strings.ToLower(m.Search.Value()))
if q == "" {
m.Table.SetRows(m.AllRows)
@@ -261,7 +261,7 @@ func applyFilter(m *CardsModel) {
m.Table.SetCursor(0)
}
-func (m CardsModel) View() string {
+func (m cardsModel) View() string {
if m.Quitting {
return "\n Quitting card search...\n\n"
}
@@ -319,12 +319,12 @@ type cardData struct {
}
// CardsList returns a minimal model - data fetching happens via Init()
-func CardsList(setID string) (CardsModel, error) {
+func CardsList(setID string) (cardsModel, error) {
s := spinner.New()
s.Spinner = spinner.Dot
s.Style = styling.Yellow
- return CardsModel{
+ return cardsModel{
SetID: setID,
Loading: true,
Spinner: s,
diff --git a/cmd/card/cardlist_test.go b/cmd/card/cardlist_test.go
index 8563fbd5..9a982acd 100644
--- a/cmd/card/cardlist_test.go
+++ b/cmd/card/cardlist_test.go
@@ -33,7 +33,7 @@ func TestCardsModel_Update_EscKey(t *testing.T) {
table.WithFocused(true),
)
- model := CardsModel{
+ model := cardsModel{
Table: tbl,
Quitting: false,
}
@@ -41,7 +41,7 @@ func TestCardsModel_Update_EscKey(t *testing.T) {
msg := tea.KeyMsg{Type: tea.KeyEsc}
newModel, cmd := model.Update(msg)
- resultModel := newModel.(CardsModel)
+ resultModel := newModel.(cardsModel)
if !resultModel.Quitting {
t.Error("Quitting should be set to true when ESC is pressed")
@@ -65,7 +65,7 @@ func TestCardsModel_Update_CtrlC(t *testing.T) {
table.WithFocused(true),
)
- model := CardsModel{
+ model := cardsModel{
Table: tbl,
Quitting: false,
}
@@ -73,7 +73,7 @@ func TestCardsModel_Update_CtrlC(t *testing.T) {
msg := tea.KeyMsg{Type: tea.KeyCtrlC}
newModel, cmd := model.Update(msg)
- resultModel := newModel.(CardsModel)
+ resultModel := newModel.(cardsModel)
if !resultModel.Quitting {
t.Error("Quitting should be set to true when Ctrl+C is pressed")
@@ -100,7 +100,7 @@ func TestCardsModel_Update_TabTogglesSearchFocusAndTableSelectedBackground(t *te
initialStyles := cardTableStyles(activeTableSelectedBg)
tbl.SetStyles(initialStyles)
- model := CardsModel{
+ model := cardsModel{
Search: search,
Table: tbl,
TableStyles: initialStyles,
@@ -108,7 +108,7 @@ func TestCardsModel_Update_TabTogglesSearchFocusAndTableSelectedBackground(t *te
// Tab into the search bar.
newModel, _ := model.Update(tea.KeyMsg{Type: tea.KeyTab})
- m1 := newModel.(CardsModel)
+ m1 := newModel.(cardsModel)
if !m1.Search.Focused() {
t.Fatal("expected search to be focused after tab")
}
@@ -122,7 +122,7 @@ func TestCardsModel_Update_TabTogglesSearchFocusAndTableSelectedBackground(t *te
// Tab back to the table.
newModel2, _ := m1.Update(tea.KeyMsg{Type: tea.KeyTab})
- m2 := newModel2.(CardsModel)
+ m2 := newModel2.(cardsModel)
if m2.Search.Focused() {
t.Fatal("expected search to be blurred after tabbing back")
}
@@ -148,7 +148,7 @@ func TestCardsModel_Update_ViewImageKey_QuestionMark(t *testing.T) {
table.WithFocused(true),
)
- model := CardsModel{
+ model := cardsModel{
Table: tbl,
ViewImage: false,
}
@@ -156,7 +156,7 @@ func TestCardsModel_Update_ViewImageKey_QuestionMark(t *testing.T) {
msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'?'}}
newModel, cmd := model.Update(msg)
- resultModel := newModel.(CardsModel)
+ resultModel := newModel.(cardsModel)
if !resultModel.ViewImage {
t.Error("ViewImage should be set to true when '?' is pressed")
@@ -180,7 +180,7 @@ func TestCardsModel_Update_ViewImageKey_DoesNotOverrideSearch(t *testing.T) {
search := textinput.New()
search.Focus()
- model := CardsModel{
+ model := cardsModel{
Search: search,
Table: tbl,
ViewImage: false,
@@ -188,7 +188,7 @@ func TestCardsModel_Update_ViewImageKey_DoesNotOverrideSearch(t *testing.T) {
msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'?'}}
newModel, _ := model.Update(msg)
- resultModel := newModel.(CardsModel)
+ resultModel := newModel.(cardsModel)
if resultModel.ViewImage {
t.Fatal("expected ViewImage to remain false when typing '?' in the search field")
@@ -202,7 +202,7 @@ func TestCardsModel_Update_ViewImageKey_DoesNotOverrideSearch(t *testing.T) {
}
func TestCardsModel_View_Quitting(t *testing.T) {
- model := CardsModel{
+ model := cardsModel{
Quitting: true,
}
@@ -230,7 +230,7 @@ func TestCardsModel_View_PriceDisplay(t *testing.T) {
"001/198 - Bulbasaur": "Price: $1.50",
}
- model := CardsModel{
+ model := cardsModel{
Table: tbl,
PriceMap: priceMap,
Quitting: false,
@@ -260,7 +260,7 @@ func TestCardsModel_View_MissingPrice(t *testing.T) {
// Empty price map - simulates missing price data
priceMap := map[string]string{}
- model := CardsModel{
+ model := cardsModel{
Table: tbl,
PriceMap: priceMap,
Quitting: false,
@@ -320,7 +320,7 @@ func TestCardDataMsg_PopulatesModel(t *testing.T) {
}
newModel, _ := model.Update(msg)
- resultModel := newModel.(CardsModel)
+ resultModel := newModel.(cardsModel)
if resultModel.Loading {
t.Error("Loading should be false after receiving data")
@@ -360,7 +360,7 @@ func TestCardDataMsg_Error_StoresError(t *testing.T) {
}
newModel, cmd := model.Update(msg)
- resultModel := newModel.(CardsModel)
+ resultModel := newModel.(cardsModel)
if resultModel.Error == nil {
t.Error("Error should be set when error received")
@@ -392,7 +392,7 @@ func TestCardDataMsg_EmptyResult(t *testing.T) {
}
newModel, _ := model.Update(msg)
- resultModel := newModel.(CardsModel)
+ resultModel := newModel.(cardsModel)
if resultModel.Loading {
t.Error("Loading should be false after receiving data")
diff --git a/cmd/card/imageviewer.go b/cmd/card/imageviewer.go
index 68e96ae5..88ba82f1 100644
--- a/cmd/card/imageviewer.go
+++ b/cmd/card/imageviewer.go
@@ -7,7 +7,7 @@ import (
"github.com/digitalghost-dev/poke-cli/styling"
)
-type ImageModel struct {
+type imageModel struct {
CardName string
ImageURL string
Error error
@@ -37,14 +37,14 @@ func fetchImageCmd(imageURL string) tea.Cmd {
}
}
-func (m ImageModel) Init() tea.Cmd {
+func (m imageModel) Init() tea.Cmd {
return tea.Batch(
m.Spinner.Tick,
fetchImageCmd(m.ImageURL),
)
}
-func (m ImageModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (m imageModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case imageReadyMsg:
m.Loading = false
@@ -72,7 +72,7 @@ func (m ImageModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
-func (m ImageModel) View() string {
+func (m imageModel) View() string {
if m.Loading {
return lipgloss.NewStyle().Padding(2).Render(
m.Spinner.View() + "Loading image for \n" + m.CardName,
@@ -90,12 +90,12 @@ func (m ImageModel) View() string {
return m.ImageData
}
-func ImageRenderer(cardName string, imageURL string) ImageModel {
+func ImageRenderer(cardName string, imageURL string) imageModel {
s := spinner.New()
s.Spinner = spinner.Dot
s.Style = styling.Yellow
- return ImageModel{
+ return imageModel{
CardName: cardName,
ImageURL: imageURL,
Loading: true,
diff --git a/cmd/card/imageviewer_test.go b/cmd/card/imageviewer_test.go
index 381014d9..e288c1b5 100644
--- a/cmd/card/imageviewer_test.go
+++ b/cmd/card/imageviewer_test.go
@@ -18,7 +18,7 @@ func TestImageModel_Init(t *testing.T) {
}
func TestImageModel_Update_EscKey(t *testing.T) {
- model := ImageModel{
+ model := imageModel{
CardName: "001/198 - Pineco",
ImageURL: "test-sixel-data",
}
@@ -33,13 +33,13 @@ func TestImageModel_Update_EscKey(t *testing.T) {
}
// Model should be returned (even if quitting)
- if _, ok := newModel.(ImageModel); !ok {
+ if _, ok := newModel.(imageModel); !ok {
t.Error("Update should return ImageModel")
}
}
func TestImageModel_Update_CtrlC(t *testing.T) {
- model := ImageModel{
+ model := imageModel{
CardName: "001/198 - Pineco",
ImageURL: "test-sixel-data",
}
@@ -53,7 +53,7 @@ func TestImageModel_Update_CtrlC(t *testing.T) {
}
func TestImageModel_Update_DifferentKey(t *testing.T) {
- model := ImageModel{
+ model := imageModel{
CardName: "001/198 - Pineco",
ImageURL: "test-sixel-data",
}
@@ -83,7 +83,7 @@ func TestImageModel_View_Loading(t *testing.T) {
func TestImageModel_View_Loaded(t *testing.T) {
expectedData := "test-sixel-data-123"
- model := ImageModel{
+ model := imageModel{
CardName: "001/198 - Pineco",
ImageURL: "http://example.com/image.png",
Loading: false,
@@ -98,7 +98,7 @@ func TestImageModel_View_Loaded(t *testing.T) {
}
func TestImageModel_View_Empty(t *testing.T) {
- model := ImageModel{
+ model := imageModel{
CardName: "001/198 - Pineco",
ImageURL: "",
Loading: false,
@@ -143,7 +143,7 @@ func TestImageModel_Update_ImageReady(t *testing.T) {
t.Error("Update with imageReadyMsg should return nil command")
}
- updatedModel := newModel.(ImageModel)
+ updatedModel := newModel.(imageModel)
if updatedModel.Loading {
t.Error(`Update with imageReadyMsg should set Loading to false`)
}
@@ -173,7 +173,7 @@ func TestImageModel_Update_SpinnerTick(t *testing.T) {
}
// Model should still be ImageModel
- if _, ok := newModel.(ImageModel); !ok {
+ if _, ok := newModel.(imageModel); !ok {
t.Error("Update should return ImageModel")
}
}
diff --git a/cmd/card/serieslist.go b/cmd/card/serieslist.go
index ffe56fa5..9d7132e3 100644
--- a/cmd/card/serieslist.go
+++ b/cmd/card/serieslist.go
@@ -13,18 +13,18 @@ var seriesIDMap = map[string]string{
"Sun & Moon": "sm",
}
-type SeriesModel struct {
+type seriesModel struct {
List list.Model
Choice string
SeriesID string
Quitting bool
}
-func (m SeriesModel) Init() tea.Cmd {
+func (m seriesModel) Init() tea.Cmd {
return nil
}
-func (m SeriesModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (m seriesModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
@@ -50,7 +50,7 @@ func (m SeriesModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, cmd
}
-func (m SeriesModel) View() string {
+func (m seriesModel) View() string {
if m.Quitting {
return "\n Quitting card search...\n\n"
}
@@ -61,7 +61,7 @@ func (m SeriesModel) View() string {
return "\n" + m.List.View()
}
-func SeriesList() SeriesModel {
+func SeriesList() seriesModel {
items := []list.Item{
styling.Item("Mega Evolution"),
styling.Item("Scarlet & Violet"),
@@ -80,5 +80,5 @@ func SeriesList() SeriesModel {
l.Styles.PaginationStyle = styling.PaginationStyle
l.Styles.HelpStyle = styling.HelpStyle
- return SeriesModel{List: l}
+ return seriesModel{List: l}
}
diff --git a/cmd/card/serieslist_test.go b/cmd/card/serieslist_test.go
index 4c1b71d2..ff295afe 100644
--- a/cmd/card/serieslist_test.go
+++ b/cmd/card/serieslist_test.go
@@ -17,7 +17,7 @@ func TestSeriesModelInit(t *testing.T) {
styling.Item("Sword & Shield"),
}
l := list.New(items, styling.ItemDelegate{}, 20, 12)
- model := SeriesModel{List: l}
+ model := seriesModel{List: l}
cmd := model.Init()
if cmd != nil {
@@ -32,7 +32,7 @@ func TestSeriesModelQuit(t *testing.T) {
styling.Item("Sword & Shield"),
}
l := list.New(items, styling.ItemDelegate{}, 20, 12)
- model := SeriesModel{List: l}
+ model := seriesModel{List: l}
testModel := teatest.NewTestModel(t, model, teatest.WithInitialTermSize(80, 24))
@@ -40,7 +40,7 @@ func TestSeriesModelQuit(t *testing.T) {
testModel.Send(tea.KeyMsg{Type: tea.KeyCtrlC})
testModel.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond))
- final := testModel.FinalModel(t).(SeriesModel)
+ final := testModel.FinalModel(t).(seriesModel)
if !final.Quitting {
t.Errorf("Expected model to be quitting after ctrl+c")
@@ -54,7 +54,7 @@ func TestSeriesModelEscQuit(t *testing.T) {
styling.Item("Sword & Shield"),
}
l := list.New(items, styling.ItemDelegate{}, 20, 12)
- model := SeriesModel{List: l}
+ model := seriesModel{List: l}
testModel := teatest.NewTestModel(t, model, teatest.WithInitialTermSize(80, 24))
@@ -62,7 +62,7 @@ func TestSeriesModelEscQuit(t *testing.T) {
testModel.Send(tea.KeyMsg{Type: tea.KeyEsc})
testModel.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond))
- final := testModel.FinalModel(t).(SeriesModel)
+ final := testModel.FinalModel(t).(seriesModel)
if !final.Quitting {
t.Errorf("Expected model to be quitting after esc")
@@ -76,7 +76,7 @@ func TestSeriesModelSelection(t *testing.T) {
styling.Item("Sword & Shield"),
}
l := list.New(items, styling.ItemDelegate{}, 20, 12)
- model := SeriesModel{List: l}
+ model := seriesModel{List: l}
testModel := teatest.NewTestModel(t, model, teatest.WithInitialTermSize(80, 24))
@@ -85,7 +85,7 @@ func TestSeriesModelSelection(t *testing.T) {
testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) // Select it
testModel.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond))
- final := testModel.FinalModel(t).(SeriesModel)
+ final := testModel.FinalModel(t).(seriesModel)
if final.Choice == "" {
t.Errorf("Expected a choice to be made, got empty string")
@@ -102,11 +102,11 @@ func TestSeriesModelWindowResize(t *testing.T) {
styling.Item("Sword & Shield"),
}
l := list.New(items, styling.ItemDelegate{}, 20, 12)
- model := SeriesModel{List: l}
+ model := seriesModel{List: l}
// Send window resize message
updatedModel, _ := model.Update(tea.WindowSizeMsg{Width: 100, Height: 30})
- finalModel := updatedModel.(SeriesModel)
+ finalModel := updatedModel.(seriesModel)
if finalModel.List.Width() != 100 {
t.Errorf("Expected list width to be 100 after resize, got %d", finalModel.List.Width())
@@ -122,7 +122,7 @@ func TestSeriesModelView(t *testing.T) {
l := list.New(items, styling.ItemDelegate{}, 20, 12)
// Test normal view
- model := SeriesModel{List: l}
+ model := seriesModel{List: l}
view := model.View()
if view == "" {
t.Errorf("Expected non-empty view, got empty string")
diff --git a/cmd/card/setslist.go b/cmd/card/setslist.go
index c530449d..4c230800 100644
--- a/cmd/card/setslist.go
+++ b/cmd/card/setslist.go
@@ -13,7 +13,7 @@ import (
var getSetsData = connections.CallTCGData
-type SetsModel struct {
+type setsModel struct {
Choice string
Error error
Loading bool
@@ -64,14 +64,14 @@ func fetchSetsCmd(seriesID string) tea.Cmd {
}
}
-func (m SetsModel) Init() tea.Cmd {
+func (m setsModel) Init() tea.Cmd {
return tea.Batch(
m.Spinner.Tick,
fetchSetsCmd(m.SeriesName),
)
}
-func (m SetsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (m setsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
@@ -135,7 +135,7 @@ func (m SetsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
-func (m SetsModel) View() string {
+func (m setsModel) View() string {
if m.Error != nil {
return styling.ApiErrorStyle.Render(
"Error loading sets from Supabase:\n" +
@@ -169,12 +169,12 @@ type setData struct {
}
// SetsList returns a minimal model - data fetching happens via Init()
-func SetsList(seriesID string) (SetsModel, error) {
+func SetsList(seriesID string) (setsModel, error) {
s := spinner.New()
s.Spinner = spinner.Dot
s.Style = styling.Yellow
- return SetsModel{
+ return setsModel{
SeriesName: seriesID,
Loading: true,
Spinner: s,
diff --git a/cmd/card/setslist_test.go b/cmd/card/setslist_test.go
index 14c20c67..f823b0b8 100644
--- a/cmd/card/setslist_test.go
+++ b/cmd/card/setslist_test.go
@@ -26,7 +26,7 @@ func TestSetsModel_Update_EscKey(t *testing.T) {
}
l := list.New(items, styling.ItemDelegate{}, 20, 20)
- model := SetsModel{
+ model := setsModel{
List: l,
SeriesName: "sv",
Quitting: false,
@@ -35,7 +35,7 @@ func TestSetsModel_Update_EscKey(t *testing.T) {
msg := tea.KeyMsg{Type: tea.KeyEsc}
newModel, cmd := model.Update(msg)
- resultModel, ok := newModel.(SetsModel)
+ resultModel, ok := newModel.(setsModel)
if !ok {
t.Fatalf("expected SetsModel, got %T", newModel)
}
@@ -55,7 +55,7 @@ func TestSetsModel_Update_CtrlC(t *testing.T) {
}
l := list.New(items, styling.ItemDelegate{}, 20, 20)
- model := SetsModel{
+ model := setsModel{
List: l,
SeriesName: "sv",
Quitting: false,
@@ -64,7 +64,7 @@ func TestSetsModel_Update_CtrlC(t *testing.T) {
msg := tea.KeyMsg{Type: tea.KeyCtrlC}
newModel, cmd := model.Update(msg)
- resultModel, ok := newModel.(SetsModel)
+ resultModel, ok := newModel.(setsModel)
if !ok {
t.Fatalf("expected SetsModel, got %T", newModel)
}
@@ -84,7 +84,7 @@ func TestSetsModel_Update_WindowSizeMsg(t *testing.T) {
}
l := list.New(items, styling.ItemDelegate{}, 20, 20)
- model := SetsModel{
+ model := setsModel{
List: l,
SeriesName: "sv",
}
@@ -92,7 +92,7 @@ func TestSetsModel_Update_WindowSizeMsg(t *testing.T) {
msg := tea.WindowSizeMsg{Width: 100, Height: 50}
newModel, cmd := model.Update(msg)
- resultModel, ok := newModel.(SetsModel)
+ resultModel, ok := newModel.(setsModel)
if !ok {
t.Fatalf("expected SetsModel, got %T", newModel)
}
@@ -112,7 +112,7 @@ func TestSetsModel_View_Quitting(t *testing.T) {
}
l := list.New(items, styling.ItemDelegate{}, 20, 20)
- model := SetsModel{
+ model := setsModel{
List: l,
Quitting: true,
}
@@ -130,7 +130,7 @@ func TestSetsModel_View_ChoiceMade(t *testing.T) {
}
l := list.New(items, styling.ItemDelegate{}, 20, 20)
- model := SetsModel{
+ model := setsModel{
List: l,
Choice: "Scarlet & Violet",
}
@@ -148,7 +148,7 @@ func TestSetsModel_View_Normal(t *testing.T) {
}
l := list.New(items, styling.ItemDelegate{}, 20, 20)
- model := SetsModel{
+ model := setsModel{
List: l,
Quitting: false,
Choice: "",
@@ -173,7 +173,7 @@ func TestSetsModel_Update_EnterKey(t *testing.T) {
"Paldea Evolved": "sv02",
}
- model := SetsModel{
+ model := setsModel{
List: l,
SetsIDMap: setsIDMap,
}
@@ -225,7 +225,7 @@ func TestSetsDataMsg_PopulatesModel(t *testing.T) {
}
newModel, _ := model.Update(msg)
- resultModel := newModel.(SetsModel)
+ resultModel := newModel.(setsModel)
if resultModel.Loading {
t.Error("Loading should be false after receiving data")
@@ -248,7 +248,7 @@ func TestSetsDataMsg_Error_StoresError(t *testing.T) {
}
newModel, cmd := model.Update(msg)
- resultModel := newModel.(SetsModel)
+ resultModel := newModel.(setsModel)
if resultModel.Error == nil {
t.Error("Error should be set when error received")
@@ -278,7 +278,7 @@ func TestSetsDataMsg_EmptyResult(t *testing.T) {
}
newModel, _ := model.Update(msg)
- resultModel := newModel.(SetsModel)
+ resultModel := newModel.(setsModel)
if resultModel.Loading {
t.Error("Loading should be false after receiving data")
diff --git a/cmd/search/model_input.go b/cmd/search/model_input.go
index c25ed6e8..a73f0f4d 100644
--- a/cmd/search/model_input.go
+++ b/cmd/search/model_input.go
@@ -11,7 +11,7 @@ import (
)
// UpdateInput handles text input updates.
-func UpdateInput(msg tea.Msg, m Model) (tea.Model, tea.Cmd) {
+func UpdateInput(msg tea.Msg, m model) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
@@ -63,7 +63,7 @@ func UpdateInput(msg tea.Msg, m Model) (tea.Model, tea.Cmd) {
}
// RenderInput renders the input view.
-func RenderInput(m Model) (string, string) {
+func RenderInput(m model) (string, string) {
var msg string
var endpoint string
diff --git a/cmd/search/model_input_test.go b/cmd/search/model_input_test.go
index 3d71a251..011e36f3 100644
--- a/cmd/search/model_input_test.go
+++ b/cmd/search/model_input_test.go
@@ -11,7 +11,7 @@ func TestUpdateInput(t *testing.T) {
ti := textinput.New()
ti.SetValue("mewtwo")
- m := Model{
+ m := model{
ShowResults: true,
TextInput: ti,
}
@@ -19,7 +19,7 @@ func TestUpdateInput(t *testing.T) {
msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'b'}}
mUpdated, _ := UpdateInput(msg, m)
- updated := mUpdated.(Model)
+ updated := mUpdated.(model)
if updated.ShowResults {
t.Errorf("expected ShowResults to be false after pressing 'b'")
diff --git a/cmd/search/model_selection.go b/cmd/search/model_selection.go
index 02f5d573..05611949 100644
--- a/cmd/search/model_selection.go
+++ b/cmd/search/model_selection.go
@@ -9,7 +9,7 @@ import (
)
// UpdateSelection handles navigation in the selection menu.
-func UpdateSelection(msg tea.Msg, m Model) (tea.Model, tea.Cmd) {
+func UpdateSelection(msg tea.Msg, m model) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
@@ -33,7 +33,7 @@ func UpdateSelection(msg tea.Msg, m Model) (tea.Model, tea.Cmd) {
}
// RenderSelection renders the selection menu.
-func RenderSelection(m Model) string {
+func RenderSelection(m model) string {
c := m.Choice
greeting := styling.StyleItalic.Render("Search for a resource and return a matching selection table")
choices := fmt.Sprintf(
diff --git a/cmd/search/model_selection_test.go b/cmd/search/model_selection_test.go
index 05786d88..4751bdfb 100644
--- a/cmd/search/model_selection_test.go
+++ b/cmd/search/model_selection_test.go
@@ -9,8 +9,8 @@ import (
)
func TestSelection(t *testing.T) {
- model := initialModel()
- testModel := teatest.NewTestModel(t, model, teatest.WithInitialTermSize(500, 600))
+ 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})
@@ -19,7 +19,7 @@ func TestSelection(t *testing.T) {
testModel.Send(tea.KeyMsg{Type: tea.KeyCtrlC})
testModel.WaitFinished(t, teatest.WithFinalTimeout(300*time.Millisecond))
- final := testModel.FinalModel(t).(Model)
+ final := testModel.FinalModel(t).(model)
if !final.Chosen {
t.Errorf("Expected model to be in Chosen state after pressing enter")
@@ -36,8 +36,8 @@ func TestSelection(t *testing.T) {
}
func TestChoiceClamping(t *testing.T) {
- model := initialModel()
- testModel := teatest.NewTestModel(t, model)
+ m := initialModel()
+ testModel := teatest.NewTestModel(t, m)
// Move down twice, this should attempt to exceed max Choice
testModel.Send(tea.KeyMsg{Type: tea.KeyDown}) // 0 → 1
@@ -53,7 +53,7 @@ func TestChoiceClamping(t *testing.T) {
testModel.Send(tea.KeyMsg{Type: tea.KeyCtrlC})
testModel.WaitFinished(t)
- final := testModel.FinalModel(t).(Model)
+ final := testModel.FinalModel(t).(model)
if final.Choice != 0 && final.Choice != 1 {
t.Errorf("Choice should be clamped between 0 and 1, got %d", final.Choice)
diff --git a/cmd/search/search.go b/cmd/search/search.go
index 6951a7f2..66199f65 100644
--- a/cmd/search/search.go
+++ b/cmd/search/search.go
@@ -43,8 +43,8 @@ func SearchCommand() (string, error) {
return output.String(), nil
}
-// Model structure
-type Model struct {
+// model structure
+type model struct {
Choice int
Chosen bool
Quitting bool
@@ -54,24 +54,24 @@ type Model struct {
WarningMessage string
}
-func initialModel() Model {
+func initialModel() model {
ti := textinput.New()
ti.Placeholder = "type name..."
ti.CharLimit = 20
ti.Width = 20
- return Model{
+ return model{
TextInput: ti,
}
}
// Init initializes the program.
-func (m Model) Init() tea.Cmd {
+func (m model) Init() tea.Cmd {
return nil
}
// Update handles keypresses and updates the state.
-func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
@@ -88,7 +88,7 @@ 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() string {
if m.Quitting {
return "\n Quitting search...\n\n"
}
diff --git a/cmd/search/search_test.go b/cmd/search/search_test.go
index 4da0682e..e0e8db52 100644
--- a/cmd/search/search_test.go
+++ b/cmd/search/search_test.go
@@ -57,19 +57,19 @@ func TestSearchCommand(t *testing.T) {
}
func TestModelInit(t *testing.T) {
- m := Model{}
+ m := model{}
cmd := m.Init()
assert.Nil(t, cmd, "Init() should return nil")
}
func TestModelQuit(t *testing.T) {
- m := Model{}
+ m := model{}
// Simulate pressing Esc
msg := tea.KeyMsg{Type: tea.KeyEsc}
newModel, cmd := m.Update(msg)
- assert.True(t, newModel.(Model).Quitting, "Model should be set to quitting")
+ assert.True(t, newModel.(model).Quitting, "Model should be set to quitting")
if cmd != nil {
assert.Equal(t, cmd(), tea.Quit(), "Update() should return tea.Quit command")
@@ -90,13 +90,13 @@ func TestSearchCommandValidationError(t *testing.T) {
}
func TestModelViewQuitting(t *testing.T) {
- m := Model{Quitting: true}
+ m := model{Quitting: true}
view := m.View()
assert.Contains(t, view, "Quitting search", "View should show quitting message")
}
func TestModelViewShowResults(t *testing.T) {
- m := Model{
+ m := model{
ShowResults: true,
SearchResults: "Test Results",
}
@@ -106,7 +106,7 @@ func TestModelViewShowResults(t *testing.T) {
}
func TestModelViewNotChosen(t *testing.T) {
- m := Model{Chosen: false}
+ 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")
diff --git a/docs/Dockerfile b/docs/Dockerfile
index f62b43a4..70ae6d5d 100644
--- a/docs/Dockerfile
+++ b/docs/Dockerfile
@@ -17,19 +17,13 @@ USER docsuser
RUN mkdocs build
-# --- Serve with lightweight HTTP server ---
-FROM python:3.12-slim
+# --- Serve with nginx ---
+FROM nginx:alpine
-# Create non-root user for runtime
-RUN groupadd -r -g 10001 docsuser && \
- useradd -r -u 10001 -g docsuser -m -s /sbin/nologin docsuser
-
-WORKDIR /site
+COPY --from=builder /build/site /usr/share/nginx/html
-COPY --from=builder --chown=docsuser:docsuser /build/site /site
-
-USER docsuser
+COPY docs/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 8080
-CMD ["python3", "-m", "http.server", "8080"]
\ No newline at end of file
+CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/docs/Infrastructure_Guide/aws.md b/docs/Infrastructure_Guide/aws.md
index bf4b8b0a..794f061d 100644
--- a/docs/Infrastructure_Guide/aws.md
+++ b/docs/Infrastructure_Guide/aws.md
@@ -22,6 +22,7 @@ local deployment and moves it into the cloud.
* [VPC](#vpc)
* [RDS](#rds)
* [EC2](#ec2)
+* [S3](#s3)
* [Elastic IPs](#elastic-ips)
* [EventBridge](#eventbridge)
* [Secrets Manager](#secrets-manager)
@@ -195,9 +196,78 @@ AWS EC2 (Elastic Compute Cloud) is a cloud service that provides resizable virtu
---
+## S3
+AWS S3 is a cloud storage service that lets you store and retrieve any number of files (called "objects") from anywhere on the internet.
+
+This project uses s3 to store demo GIFs of the CLI and assets for the web app.
+
+1. Visit the [S3 console](https://console.aws.amazon.com/s3).
+2. Click on **Create Bucket**.
+3. Select **general purpose** for the bucket type, then **global namespace** for the bucket namespace.
+4. Give the bucket a name.
+5. Under the **Object Ownership** section, choose **ACLs disabled (recommended)**.
+6. Choose to block all public access. (CloudFront will be set up as a CDN)
+7. Disable bucket versioning.
+8. Optionally, add tags for organizational purposes.
+9. Leave **Default Encryption** section with default options.
+10. Click **Create Bucket**.
+
+### CloudFront
+AWS CloudFront is a content delivery network (CDN) that sits in front of your S3 files, serving content from servers closest to the user for faster load times, while also providing DDoS protection.
+
+This project uses CloudFront to serve the static assets of the web app.
+
+1. Visit the [CloudFront console](https://console.aws.amazon.com/cloudfront/home).
+2. Click on create distribution
+
+* Step 1 // Choose a Plan
+ 1. Choose the free plan, click next
+* Step 2 // Get Started
+ 1. Provide a name for the distribution
+ 2. Project doesn’t have domains on Route 53
+ 3. Add tags for organization, click next
+* Step 3 // Specify Origin
+ 1. Origin type is Amazon S3
+ 2. Click **Browse S3** and choose the correct bucket
+ 3. Keep recommended settings, click next
+* Step 4 // Enable Security
+ 1. Enable **Use monitor mode**
+* Step 5 // Review and Create
+ 1. Review the configuration and then click **Create Distribution** when ready
+* Step 6 // Update S3 Bucket Policy
+ 1. Visit the S3 [homepage](https://us-east-1.console.aws.amazon.com/s3/home) and select the bucket
+ 2. Under the **Permissions** tab, in the **Block public access** section, ensure that block _all_ public access is on.
+ 3. In the **Bucket Policy** section, click on **Edit**, paste in the following policy (fill missing values):
+ ```json
+ {
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Sid": "AllowCloudFrontServicePrincipal",
+ "Effect": "Allow",
+ "Principal": {
+ "Service": "cloudfront.amazonaws.com"
+ },
+ "Action": "s3:GetObject",
+ "Resource": "arn:aws:s3:::/*",
+ "Condition": {
+ "ArnLike": {
+ "AWS:SourceArn": "arn:aws:cloudfront:::distribution/"
+ }
+ }
+ }
+ ]
+ }
+ ```
+ 4. Save the policy changes.
+
+---
+
## Elastic IPs
An Elastic IP is a static public IPv4 address in AWS that can be assigned to an EC2 instance.
+A static IP address is used to ensure the virtual machine can reliably connect to the PostgreSQL database on every startup.
+
1. Visit the [EC2 console](https://console.aws.amazon.com/ec2).
2. On the left, under **Network & Security**, click on **Elastic IPs**.
3. In the upper-right, click on **Allocate Elastic IP Address**.
diff --git a/docs/assets/tcg.gif b/docs/assets/tcg.gif
new file mode 100644
index 00000000..5def3412
Binary files /dev/null and b/docs/assets/tcg.gif differ
diff --git a/docs/commands.md b/docs/commands.md
index d52e3bf8..f2478e84 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -4,7 +4,6 @@
**Available Flags**
-* `--help | -h`
* `--latest | -l`
* `--version | -v`
@@ -16,13 +15,12 @@ the generation in which it first appeared, and a list of Pokémon that possess i
**Available Flags**
-* `--help | -h`
* `--pokemon | -p`
Example:
```console
-$ poke-cli ability solar-power
-$ poke-cli ability solar-power --pokemon # list Pokémon that posses the ability
+poke-cli ability solar-power
+poke-cli ability solar-power --pokemon # list Pokémon that posses the ability
```
Output:
@@ -36,7 +34,7 @@ Output:
Example:
```console
-$ poke-cli item poke-ball
+poke-cli item poke-ball
```
Output:
@@ -51,7 +49,7 @@ and the move's effect.
Example:
```console
-$ poke-cli move dazzling-gleam
+poke-cli move dazzling-gleam
```
Output:
@@ -65,7 +63,7 @@ Output:
Example:
```console
-$ poke-cli natures
+poke-cli natures
```
Output:
@@ -79,7 +77,6 @@ Output:
**Available Flags**
-* `-h | --help`
* `-a | --abilities`
* `-d | --defense`
* `-i=xx | --image=xx`
@@ -94,7 +91,7 @@ Output:
Example:
```console
-$ poke-cli pokemon rockruff --abilities --moves
+poke-cli pokemon rockruff --abilities --moves
```
Output:
@@ -103,7 +100,7 @@ Output:
Example:
```console
-$ poke-cli pokemon gastrodon --defense
+poke-cli pokemon gastrodon --defense
```
Output:
@@ -113,7 +110,7 @@ Output:
Example:
```console
# choose between three sizes: 'sm', 'md', 'lg'
-$ poke-cli pokemon tyranitar --image=sm
+poke-cli pokemon tyranitar --image=sm
```
Output:
@@ -122,7 +119,7 @@ Output:
Example:
```console
-$ poke-cli pokemon cacturne --stats
+poke-cli pokemon cacturne --stats
```
Output:
@@ -136,7 +133,7 @@ Output:
Example:
```console
-$ poke-cli search
+poke-cli search
```
Output:
@@ -150,7 +147,7 @@ Output:
Example:
```console
-$ poke-cli speed
+poke-cli speed
```
Output:
@@ -158,12 +155,30 @@ Output:
---
+## `tcg`
+* Retrieve details about all competitive TCG tournaments for the current season.
+
+**Available Flags**
+
+* `--web | -w` - Open the tournament's website in the default browser.
+
+Example:
+```console
+poke-cli tcg
+```
+
+Output:
+
+
+
+---
+
## `types`
* Retrieve details about a specific type and a damage relation table.
Example:
```console
-$ poke-cli types
+poke-cli types
```
Output:
diff --git a/docs/index.md b/docs/index.md
index e56ea268..dfa629a0 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -13,5 +13,12 @@ including CI/CD pipelines with GitHub Actions and data workflows using Dagster a
View the [guides](Infrastructure_Guide/index.md) section for more details.
## Demo
-Here is a quick demo of the CLI/TUI tool in action.
-
\ No newline at end of file
+Here are some demos of the CLI/TUI tool in action.
+
+### Video Game Data
+
+
+
+### Trading Card Game Data
+
+
\ No newline at end of file
diff --git a/docs/nginx.conf b/docs/nginx.conf
new file mode 100644
index 00000000..df13beb0
--- /dev/null
+++ b/docs/nginx.conf
@@ -0,0 +1,15 @@
+server {
+ listen 8080;
+ root /usr/share/nginx/html;
+ index index.html;
+
+ add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self';" always;
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header X-Frame-Options "SAMEORIGIN" always;
+ add_header X-XSS-Protection "1; mode=block" always;
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
+
+ location / {
+ try_files $uri $uri/ $uri.html =404;
+ }
+}
diff --git a/go.mod b/go.mod
index 956b178c..b74cb0d1 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
module github.com/digitalghost-dev/poke-cli
-go 1.24.12
+go 1.25.8
require (
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7
diff --git a/nfpm.yaml b/nfpm.yaml
index 8291cec7..aa968e2a 100644
--- a/nfpm.yaml
+++ b/nfpm.yaml
@@ -1,7 +1,7 @@
name: "poke-cli"
arch: "arm64"
platform: "linux"
-version: "v1.9.0"
+version: "v1.9.1"
section: "default"
version_schema: semver
maintainer: "Christian S"
diff --git a/testdata/main_latest_flag.golden b/testdata/main_latest_flag.golden
index 0d62e917..4a9faff1 100644
--- a/testdata/main_latest_flag.golden
+++ b/testdata/main_latest_flag.golden
@@ -2,6 +2,6 @@
┃ ┃
┃ Latest available release ┃
┃ on GitHub: ┃
-┃ • v1.8.11 ┃
+┃ • v1.9.0 ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛