diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d24b4fe..6d264e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ on: - '.github/**' - '.dockerignore' - 'docs/**' - - 'etl/**' + - '../../card_data/**' - '.gitignore' - 'demo**' - 'go.mod' @@ -28,7 +28,7 @@ on: - main env: - VERSION_NUMBER: 'v1.4.0' + VERSION_NUMBER: 'v1.5.0' DOCKERHUB_REGISTRY_NAME: 'digitalghostdev/poke-cli' AWS_REGION: 'us-west-2' diff --git a/.gitignore b/.gitignore index 33bfbea..43b0a1d 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,12 @@ go.work.sum .env # Python -etl/.venv \ No newline at end of file +card_data/.venv +__pycache__/ + +# Terraform +card_data/terraform/access-token +/card_data/terraform/access-token +card_data/terraform/secrets.tfvars +card_data/terraform/terraform.tfstate +/card_data/terraform/.terraform/ diff --git a/.goreleaser.yml b/.goreleaser.yml index cf6446a..de0dbd5 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -14,7 +14,7 @@ builds: - windows - darwin ldflags: - - -s -w -X main.version=v1.4.0 + - -s -w -X main.version=v1.5.0 archives: - formats: [ 'zip' ] @@ -45,7 +45,7 @@ homebrew_casks: owner: digitalghost-dev name: homebrew-tap token: "{{.Env.GITHUB_TOKEN}}" - homepage: "https://github.com/digitalghost-dev/poke-cli" + homepage: "https://docs.poke-cli.com/" description: "A hybrid CLI/TUI tool written in Go for viewing Pokémon data from the terminal!" license: "Apache License 2.0" hooks: @@ -53,4 +53,23 @@ homebrew_casks: install: | if OS.mac? && system_command("/usr/bin/xattr", args: ["-h"]).exit_status == 0 system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/poke-cli"] - end \ No newline at end of file + end + +winget: + - name: poke-cli + publisher: digitalghost-dev + license: MIT + homepage: "https://docs.poke-cli.com/" + short_description: "Pokémon CLI/TUI terminal tool." + repository: + owner: digitalghost-dev + name: winget-pkgs + token: "{{.Env.GITHUB_TOKEN}}" + branch: "poke-cli-{{.Version}}" + pull_request: + enabled: true + base: + owner: microsoft + name: winget-pkgs + branch: master + description: "A hybrid CLI/TUI tool written in Go for viewing Pokémon data from the terminal!" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 58c0abc..b93908e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,12 +8,12 @@ RUN go mod download COPY . . -RUN go build -ldflags "-X main.version=v1.4.0" -o poke-cli . +RUN go build -ldflags "-X main.version=v1.5.0" -o poke-cli . # build 2 FROM --platform=$BUILDPLATFORM alpine:3.22 -# Install only necessary packages and remove them after use +# Installing only necessary packages and remove them after use RUN apk upgrade && \ apk add --no-cache shadow && \ addgroup -S poke_group && adduser -S poke_user -G poke_group && \ @@ -25,10 +25,8 @@ COPY --from=build /app/poke-cli /app/poke-cli ENV TERM=xterm-256color ENV COLOR_OUTPUT=true -# Set correct permissions RUN chown -R poke_user:poke_group /app -# Switch to non-root user USER poke_user ENTRYPOINT ["/app/poke-cli"] \ No newline at end of file diff --git a/README.md b/README.md index 7a8d474..4322495 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ pokemon-logo

Pokémon CLI

version-label - docker-image-size + docker-image-size ci-status-badge
@@ -24,7 +24,7 @@ View future plans in the [Roadmap](#roadmap) section. --- ## Demo -![demo](https://poke-cli-s3-bucket.s3.us-west-2.amazonaws.com/demo-v1.3.3.gif) +![demo](https://poke-cli-s3-bucket.s3.us-west-2.amazonaws.com/demo-v1.5.0.gif) --- ## Installation @@ -32,6 +32,7 @@ View future plans in the [Roadmap](#roadmap) section. * [Binary](#binary) * [Docker Image](#docker-image) * [Homebrew](#homebrew) +* [Winget](#winget) * [Source](#source) ### Binary @@ -77,11 +78,11 @@ View future plans in the [Roadmap](#roadmap) section. 3. Choose how to interact with the container: * Run a single command and exit: ```bash - docker run --rm -it digitalghostdev/poke-cli:v1.4.0 [subcommand] flag] + docker run --rm -it digitalghostdev/poke-cli:v1.5.0 [subcommand] flag] ``` * Enter the container and use its shell: ```bash - docker run --rm -it --name poke-cli --entrypoint /bin/sh digitalghostdev/poke-cli:v1.4.0 -c "cd /app && exec sh" + docker run --rm -it --name poke-cli --entrypoint /bin/sh digitalghostdev/poke-cli:v1.5.0 -c "cd /app && exec sh" # placed into the /app directory, run the program with './poke-cli' # example: ./poke-cli ability swift-swim ``` @@ -91,7 +92,18 @@ View future plans in the [Roadmap](#roadmap) section. ```bash brew install --cask digitalghost-dev/tap/poke-cli ```` -2. Verify install! +2. Verify install: + ```bash + poke-cli -v + ``` + +### Winget +1. Install the package: + ```powershell + winget install poke-cli + ``` + +2. Verify install: ```bash poke-cli -v ``` @@ -108,35 +120,36 @@ View future plans in the [Roadmap](#roadmap) section. ## Usage By running `poke-cli [-h | --help]`, it'll display information on how to use the tool. ``` -╭──────────────────────────────────────────────────────────╮ -│Welcome! This tool displays data related to Pokémon! │ -│ │ -│ USAGE: │ -│ poke-cli [flag] │ -│ poke-cli [flag] │ -│ poke-cli [flag] │ -│ │ -│ FLAGS: │ -│ -h, --help Shows the help menu │ -│ -l, --latest Prints the latest version available │ -│ -v, --version Prints the current version │ -│ │ -│ COMMANDS: │ -│ ability Get details about an ability │ -│ item Get details about an item │ -│ move Get details about a move │ -│ natures Get details about all natures │ -│ pokemon Get details about a Pokémon │ -│ search Search for a resource │ -│ types Get details about a typing │ -│ │ -│ hint: when calling a resource with a space, use a hyphen │ -│ example: poke-cli ability strong-jaw │ -│ example: poke-cli pokemon flutter-mane │ -│ │ -│ ↓ ctrl/cmd + click for docs/guides │ -│ docs.poke-cli.com │ -╰──────────────────────────────────────────────────────────╯ +╭───────────────────────────────────────────────────────────────╮ +│Welcome! This tool displays data related to Pokémon! │ +│ │ +│ USAGE: │ +│ poke-cli [flag] │ +│ poke-cli [flag] │ +│ poke-cli [flag] │ +│ │ +│ FLAGS: │ +│ -h, --help Shows the help menu │ +│ -l, --latest Prints the latest version available │ +│ -v, --version Prints the current version │ +│ │ +│ COMMANDS: │ +│ ability Get details about an ability │ +│ item Get details about an item │ +│ move Get details about a move │ +│ natures Get details about all natures │ +│ pokemon Get details about a Pokémon │ +│ search Search for a resource │ +│ speed Calculate the speed of a Pokémon in battle │ +│ types Get details about a typing │ +│ │ +│ hint: when calling a resource with a space, use a hyphen │ +│ example: poke-cli ability strong-jaw │ +│ example: poke-cli pokemon flutter-mane │ +│ │ +│ ↓ ctrl/cmd + click for docs/guides │ +│ docs.poke-cli.com │ +╰───────────────────────────────────────────────────────────────╯ ``` --- @@ -163,7 +176,7 @@ Below is a list of the planned/completed commands and flags: - [ ] `item` - [x] `move` - [x] `pokemon` -- [ ] `speed`: compare speed stats between two Pokémon. +- [x] `speed`: compare speed stats between two Pokémon. - [x] `types`: get data about a specific typing. --- diff --git a/cli.go b/cli.go index fc7b1a9..8c1d3cd 100644 --- a/cli.go +++ b/cli.go @@ -9,6 +9,7 @@ import ( "github.com/digitalghost-dev/poke-cli/cmd/natures" "github.com/digitalghost-dev/poke-cli/cmd/pokemon" "github.com/digitalghost-dev/poke-cli/cmd/search" + "github.com/digitalghost-dev/poke-cli/cmd/speed" "github.com/digitalghost-dev/poke-cli/cmd/types" "github.com/digitalghost-dev/poke-cli/cmd/utils" "github.com/digitalghost-dev/poke-cli/flags" @@ -72,6 +73,7 @@ func runCLI(args []string) int { fmt.Sprintf("\n\t%-15s %s", "natures", "Get details about all natures"), fmt.Sprintf("\n\t%-15s %s", "pokemon", "Get details about a Pokémon"), fmt.Sprintf("\n\t%-15s %s", "search", "Search for a resource"), + fmt.Sprintf("\n\t%-15s %s", "speed", "Calculate the speed of a Pokémon in battle"), fmt.Sprintf("\n\t%-15s %s", "types", "Get details about a typing"), "\n\n", styling.StyleItalic.Render("hint: when calling a resource with a space, use a hyphen"), "\n", styling.StyleItalic.Render("example: poke-cli ability strong-jaw"), @@ -105,6 +107,7 @@ func runCLI(args []string) int { "move": utils.HandleCommandOutput(move.MoveCommand), "natures": utils.HandleCommandOutput(natures.NaturesCommand), "pokemon": utils.HandleCommandOutput(pokemon.PokemonCommand), + "speed": utils.HandleCommandOutput(speed.SpeedCommand), "types": utils.HandleCommandOutput(types.TypesCommand), "search": func() int { search.SearchCommand() @@ -141,6 +144,7 @@ func runCLI(args []string) int { fmt.Sprintf("\n\t%-15s %s", "natures", "Get details about all natures"), fmt.Sprintf("\n\t%-15s %s", "pokemon", "Get details about a Pokémon"), fmt.Sprintf("\n\t%-15s %s", "search", "Search for a resource"), + fmt.Sprintf("\n\t%-15s %s", "speed", "Calculate the speed of a Pokémon in battle"), fmt.Sprintf("\n\t%-15s %s", "types", "Get details about a typing"), fmt.Sprintf("\n\nAlso run %s for more info!", styling.StyleBold.Render("poke-cli -h")), ) diff --git a/cli_test.go b/cli_test.go index 2a330a9..2e9709c 100644 --- a/cli_test.go +++ b/cli_test.go @@ -101,6 +101,12 @@ func TestRunCLI(t *testing.T) { expectedOutput: utils.LoadGolden(t, "cli_help.golden"), expectedCode: 0, }, + { + name: "Non-valid command", + args: []string{"movesets"}, + expectedOutput: utils.LoadGolden(t, "cli_incorrect_command.golden"), + expectedCode: 1, + }, } for _, tt := range tests { diff --git a/cmd/speed/speed.go b/cmd/speed/speed.go new file mode 100644 index 0000000..0617d51 --- /dev/null +++ b/cmd/speed/speed.go @@ -0,0 +1,331 @@ +package speed + +import ( + "errors" + "flag" + "fmt" + "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" + "github.com/digitalghost-dev/poke-cli/styling" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "log" + "math" + "os" + "strconv" + "strings" +) + +// DefaultSpeedStat is the default implementation of SpeedStatFunc +var DefaultSpeedStat SpeedStatFunc = func(name string) (string, error) { + pokemonStruct, _, err := connections.PokemonApiCall("pokemon", name, connections.APIURL) + if err != nil { + return "", fmt.Errorf("API call failed: %w", err) + } + + for _, stat := range pokemonStruct.Stats { + if stat.Stat.Name == "speed" { + return strconv.Itoa(stat.BaseStat), nil + } + } + + return "", errors.New("speed stat not found") +} + +var ( + pokemon PokemonDetails + output strings.Builder + abilityMultipliers = map[string]float64{ + "None": 1.0, + "Swift Swim": 2.0, + "Chlorophyll": 2.0, + "Sand Rush": 2.0, + "Slush Rush": 2.0, + "Unburden": 2.0, + "Quick Feet": 1.5, + "Surge Surfer": 2.0, + } + modifierMultipliers = map[string]float64{ + "Choice Scarf": 1.5, + "Tailwind": 2.0, + } + stageMultipliers = map[int]float64{ + -6: 0.25, + -5: 2.0 / 7.0, + -4: 1.0 / 3.0, + -3: 0.4, + -2: 0.5, + -1: 2.0 / 3.0, + 0: 1.0, + +1: 1.5, + +2: 2.0, + +3: 2.5, + +4: 3.0, + +5: 3.5, + +6: 4.0, + } + natureMultipliers = map[string]float64{ + "+10%": 1.1, + "0%": 1.0, + "-10%": 0.9, + } +) + +type PokemonDetails struct { + Name string + SpeedStage string + Nature string + Level string + Modifier []string + Ability string + SpeedEV string + SpeedIV string +} + +// SpeedStatFunc is a function type for getting a Pokémon's base speed stat +type SpeedStatFunc func(name string) (string, error) + +func SpeedCommand() (string, error) { + // Reset the output string builder + output.Reset() + + flag.Usage = func() { + helpMessage := styling.HelpBorder.Render( + "Calculate the speed of a Pokémon.\n\n", + styling.StyleBold.Render("USAGE:"), + fmt.Sprintf("\n\t%s %s %s", "poke-cli", styling.StyleBold.Render("speed"), "[flag]"), + "\n\n", + styling.StyleBold.Render("FLAGS:"), + fmt.Sprintf("\n\t%-30s %s", "-h, --help", "Prints out the help menu"), + ) + output.WriteString(helpMessage) + } + + flag.Parse() + + // Handle help flag + if len(os.Args) == 3 && (os.Args[2] == "-h" || os.Args[2] == "--help") { + flag.Usage() + return output.String(), nil + } + + // Validate arguments + if err := utils.ValidateSpeedArgs(os.Args); err != nil { + output.WriteString(err.Error()) + return output.String(), err + } + + form := form() + + err := form.Run() + if err != nil { + return "", err + } + + result, err := formula() + if err != nil { + return "", err + } + + return result, nil +} + +func form() *huh.Form { + form := huh.NewForm( + huh.NewGroup(huh.NewNote(). + Title("Speed Calculator"). + Description("This command will calculate the speed stat\nof a Pokémon during an in-game battle."). + Next(true). + NextLabel("Next"), + ), + huh.NewGroup( + huh.NewInput(). + Value(&pokemon.Name). + Title("Enter the first Pokémon's name:"). + Placeholder("incineroar"). + Validate(func(s string) error { + if strings.TrimSpace(s) == "" { + return errors.New("input cannot be blank") + } + _, _, err := connections.PokemonApiCall("pokemon", s, connections.APIURL) + if err != nil { + return errors.New("not a valid Pokémon") + } + return nil + }), + huh.NewInput(). + Value(&pokemon.Level). + Title("And its level:"). + Placeholder("50"). + Validate(func(s string) error { + num, err := strconv.Atoi(s) + if err != nil { + return errors.New("please enter a valid number") + } + if num < 1 || num > 100 { + return errors.New("level must be between 1 and 100") + } + return nil + }), + huh.NewInput(). + Value(&pokemon.SpeedEV). + Title("EV amount:"). + Description("Enter the Pokémon's EV level for the speed stat"). + Placeholder("252"). + Validate(func(s string) error { + num, err := strconv.Atoi(s) + if err != nil { + return errors.New("please enter a valid number") + } + if num < 0 || num > 252 { + return errors.New("level must be between 0 and 252") + } + return nil + }), + huh.NewInput(). + Value(&pokemon.SpeedIV). + Title("IV amount"). + Description("Enter the Pokémon's IV level for the speed stat"). + Placeholder("31"). + Validate(func(s string) error { + num, err := strconv.Atoi(s) + if err != nil { + return errors.New("please enter a valid number") + } + if num < 0 || num > 31 { + return errors.New("level must be between 0 and 31") + } + return nil + }), + huh.NewMultiSelect[string](). + Title("Modifiers"). + Description("Select any amount of options"). + Options( + huh.NewOption("Choice Scarf", "Choice Scarf"), + huh.NewOption("Tailwind", "Tailwind"), + ).Value(&pokemon.Modifier), + ), + // Page 3 + huh.NewGroup( + huh.NewSelect[string](). + Title("Ability"). + Description("Select an ability in play"). + Value(&pokemon.Ability). + Options( + huh.NewOptions("None", "Swift Swim", "Chlorophyll", "Sand Rush", "Slush Rush", "Unburden", "Quick Feet", "Surge Surfer")..., + ), + huh.NewSelect[string](). + Options( + huh.NewOptions("+10%", "0%", "-10%")..., + ). + Title("Nature"). + Description("Nature benefit/detriment 1"). + Value(&pokemon.Nature). + Validate(func(value string) error { + if value == "" { + return errors.New("please select a nature") + } + return nil + }), + huh.NewInput(). + Value(&pokemon.SpeedStage). + Title("Speed Stage"). + Placeholder("ex: +6 or 0 or -3"). + Validate(func(s string) error { + num, err := strconv.Atoi(s) + if err != nil { + return errors.New("please enter a whole number between -6 and +6") + } + if num < -6 || num > 6 { + return errors.New("level must be between -6 and +6") + } + return nil + }), + ), + ).WithTheme(styling.FormTheme()) + + return form +} + +func formula() (string, error) { + modifierMultiplier := 1.0 // start with no change + for _, mod := range pokemon.Modifier { + if val, ok := modifierMultipliers[mod]; ok { + modifierMultiplier *= val + } + } + + abilityMultiplier := abilityMultipliers[pokemon.Ability] + + speedStageInt, err := strconv.Atoi(pokemon.SpeedStage) + if err != nil { + log.Fatalf("Invalid SpeedStage: %v", err) + } + stageMultiplier := stageMultipliers[speedStageInt] + + // Get the Pokémon's base speed using the DefaultSpeedStat function + speedStr, err := DefaultSpeedStat(pokemon.Name) + if err != nil { + return "", err + } + + baseSpeed, err := strconv.Atoi(speedStr) + if err != nil { + return "", fmt.Errorf("failed to convert speed to int: %w", err) + } + + intIV, err := strconv.Atoi(pokemon.SpeedIV) + if err != nil { + return "", fmt.Errorf("failed to convert speed to int: %w", err) + } + intEV, err := strconv.Atoi(pokemon.SpeedEV) + if err != nil { + return "", fmt.Errorf("failed to convert speed to int: %w", err) + } + + intLevel, err := strconv.Atoi(pokemon.Level) + if err != nil { + return "", fmt.Errorf("failed to convert speed to int: %w", err) + } + + natureMultiplier := natureMultipliers[pokemon.Nature] + + chosenPokemon := cases.Title(language.English).String(pokemon.Name) + + // Calculate final speed using the formula: + // (((2 x base + IV + (EV / 4)) x level / 100 + 5) * nature * modifier * ability * stage + finalSpeed := float64(((2*baseSpeed+intIV+(intEV/4))*intLevel/100)+5) * natureMultiplier * abilityMultiplier * stageMultiplier * modifierMultiplier + + // Round down the final speed + finalSpeedFloor := math.Floor(finalSpeed) + finalSpeedStr := fmt.Sprintf("%.0f", finalSpeedFloor) + + header := fmt.Sprintf("%s at level %s with selected options has a current speed of %s.", + styling.YellowAdaptive(chosenPokemon), + styling.YellowAdaptive(pokemon.Level), + styling.YellowAdaptive(finalSpeedStr), + ) + body := fmt.Sprintf("EVs: %s\nIVs: %s\nModifiers: %s\nNature: %s\nAbility: %s\nSpeed Stage: %s\nBase Speed: %s", + styling.YellowAdaptive(pokemon.SpeedEV), + styling.YellowAdaptive(pokemon.SpeedIV), + styling.YellowAdaptive(xstrings.EnglishJoin(pokemon.Modifier, true)), + styling.YellowAdaptive(pokemon.Nature), + styling.YellowAdaptive(pokemon.Ability), + styling.YellowAdaptive(pokemon.SpeedStage), + styling.YellowAdaptive(speedStr), + ) + + docStyle := lipgloss.NewStyle(). + Padding(1, 2). + BorderStyle(lipgloss.ThickBorder()). + BorderForeground(lipgloss.AdaptiveColor{Light: "#444", Dark: "#EEE"}). + Width(32) + + fullDoc := lipgloss.JoinVertical(lipgloss.Top, header, "---", body) + output.WriteString(docStyle.Render(fullDoc)) + + return output.String(), nil +} diff --git a/cmd/speed/speed_test.go b/cmd/speed/speed_test.go new file mode 100644 index 0000000..16a604d --- /dev/null +++ b/cmd/speed/speed_test.go @@ -0,0 +1,234 @@ +package speed + +import ( + "github.com/digitalghost-dev/poke-cli/cmd/utils" + "github.com/digitalghost-dev/poke-cli/styling" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "os" + "testing" +) + +func TestSpeedCommand(t *testing.T) { + tests := []struct { + name string + args []string + expectedOutput string + wantError bool + }{ + { + name: "Speed help flag", + args: []string{"speed", "--help"}, + expectedOutput: utils.LoadGolden(t, "speed_help.golden"), + }, + { + name: "Speed help flag", + args: []string{"speed", "-h"}, + expectedOutput: utils.LoadGolden(t, "speed_help.golden"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + originalArgs := os.Args + os.Args = append([]string{"poke-cli"}, tt.args...) + defer func() { os.Args = originalArgs }() + + output, _ := SpeedCommand() + cleanOutput := styling.StripANSI(output) + + assert.Equal(t, tt.expectedOutput, cleanOutput, "Output should match expected") + }) + } +} + +func TestFormula(t *testing.T) { + // Save the original DefaultSpeedStat function and restore it after the test + originalSpeedStat := DefaultSpeedStat + defer func() { DefaultSpeedStat = originalSpeedStat }() + + // Create a mock SpeedStatFunc that always returns 90 (Pikachu's base speed) + DefaultSpeedStat = func(name string) (string, error) { + return "90", nil + } + + tests := []struct { + name string + pokemonDetails PokemonDetails + expectedSpeed string + wantError bool + }{ + { + name: "Basic calculation with default values", + pokemonDetails: PokemonDetails{ + Name: "pikachu", + SpeedStage: "0", + Nature: "0%", + Level: "50", + Modifier: []string{}, + Ability: "None", + SpeedEV: "0", + SpeedIV: "0", + }, + expectedSpeed: "95", + wantError: false, + }, + { + name: "With positive nature", + pokemonDetails: PokemonDetails{ + Name: "pikachu", + SpeedStage: "0", + Nature: "+10%", + Level: "50", + Modifier: []string{}, + Ability: "None", + SpeedEV: "0", + SpeedIV: "0", + }, + expectedSpeed: "104", + wantError: false, + }, + { + name: "With negative nature", + pokemonDetails: PokemonDetails{ + Name: "pikachu", + SpeedStage: "0", + Nature: "-10%", + Level: "50", + Modifier: []string{}, + Ability: "None", + SpeedEV: "0", + SpeedIV: "0", + }, + expectedSpeed: "85", + wantError: false, + }, + { + name: "With ability multiplier", + pokemonDetails: PokemonDetails{ + Name: "pikachu", + SpeedStage: "0", + Nature: "0%", + Level: "50", + Modifier: []string{}, + Ability: "Swift Swim", + SpeedEV: "0", + SpeedIV: "0", + }, + expectedSpeed: "190", + wantError: false, + }, + { + name: "With positive speed stage", + pokemonDetails: PokemonDetails{ + Name: "pikachu", + SpeedStage: "2", + Nature: "0%", + Level: "50", + Modifier: []string{}, + Ability: "None", + SpeedEV: "0", + SpeedIV: "0", + }, + expectedSpeed: "190", + wantError: false, + }, + { + name: "With negative speed stage", + pokemonDetails: PokemonDetails{ + Name: "pikachu", + SpeedStage: "-2", + Nature: "0%", + Level: "50", + Modifier: []string{}, + Ability: "None", + SpeedEV: "0", + SpeedIV: "0", + }, + expectedSpeed: "47", + wantError: false, + }, + { + name: "With modifier", + pokemonDetails: PokemonDetails{ + Name: "pikachu", + SpeedStage: "0", + Nature: "0%", + Level: "50", + Modifier: []string{"Choice Scarf"}, + Ability: "None", + SpeedEV: "0", + SpeedIV: "0", + }, + expectedSpeed: "142", + wantError: false, + }, + { + name: "With multiple modifiers", + pokemonDetails: PokemonDetails{ + Name: "pikachu", + SpeedStage: "0", + Nature: "0%", + Level: "50", + Modifier: []string{"Choice Scarf", "Tailwind"}, + Ability: "None", + SpeedEV: "0", + SpeedIV: "0", + }, + expectedSpeed: "285", + wantError: false, + }, + { + name: "With EVs and IVs", + pokemonDetails: PokemonDetails{ + Name: "pikachu", + SpeedStage: "0", + Nature: "0%", + Level: "50", + Modifier: []string{}, + Ability: "None", + SpeedEV: "252", + SpeedIV: "31", + }, + expectedSpeed: "142", + wantError: false, + }, + { + name: "Complex scenario", + pokemonDetails: PokemonDetails{ + Name: "pikachu", + SpeedStage: "1", + Nature: "+10%", + Level: "100", + Modifier: []string{"Choice Scarf"}, + Ability: "Quick Feet", + SpeedEV: "252", + SpeedIV: "31", + }, + expectedSpeed: "1035", + wantError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pokemon = tt.pokemonDetails + + result, err := formula() + + if tt.wantError { + assert.Error(t, err) + } else { + require.NoError(t, err) + + cleanOutput := styling.StripANSI(result) + + t.Logf("Expected speed: %s", tt.expectedSpeed) + + // Check if the output contains the expected speed + assert.Contains(t, cleanOutput, "current speed of "+tt.expectedSpeed, + "Expected speed "+tt.expectedSpeed+" not found in output: "+cleanOutput) + } + }) + } +} diff --git a/cmd/types/types_test.go b/cmd/types/types_test.go index ccef786..64d6be5 100644 --- a/cmd/types/types_test.go +++ b/cmd/types/types_test.go @@ -45,6 +45,7 @@ func TestTypesCommand(t *testing.T) { }) } } + func TestModelInit(t *testing.T) { m := model{} cmd := m.Init() diff --git a/cmd/utils/validateargs.go b/cmd/utils/validateargs.go index dce7cfe..791d763 100644 --- a/cmd/utils/validateargs.go +++ b/cmd/utils/validateargs.go @@ -163,6 +163,19 @@ func ValidateSearchArgs(args []string) error { return nil } +// ValidateSpeedArgs validates the command line arguments +func ValidateSpeedArgs(args []string) error { + if err := checkLength(args, 3); err != nil { + return err + } + + if err := checkNoOtherOptions(args, 3, ""); err != nil { + return err + } + + return nil +} + // ValidateTypesArgs validates the command line arguments func ValidateTypesArgs(args []string) error { if err := checkLength(args, 3); err != nil { diff --git a/demo.gif b/demo.gif index adeeec4..c2b828f 100644 Binary files a/demo.gif and b/demo.gif differ diff --git a/demo.tape b/demo.tape index a040354..094d66c 100644 --- a/demo.tape +++ b/demo.tape @@ -61,18 +61,18 @@ Require echo Set Shell "bash" Set FontSize 32 -Set Width 2100 +Set Width 2300 Set Height 1300 -Set TypingSpeed 100ms +Set TypingSpeed 95ms Set Theme "tokyonight-storm" Type "poke-cli pokemon charizard --abilities --stats --types" -Sleep 3s +Sleep 2s Enter -Sleep 6s +Sleep 4s Type clear @@ -80,11 +80,11 @@ Enter Type "poke-cli pokemon tyranitar --image=sm" -Sleep 3s +Sleep 2s Enter -Sleep 6s +Sleep 4s Type clear @@ -92,7 +92,7 @@ Enter Type "poke-cli types" -Sleep 3s +Sleep 2s Enter @@ -112,17 +112,17 @@ Type "poke-cli natures" Enter -Sleep 6s +Sleep 4s Type clear Enter -Type "poke-cli ability stench --pokemon" +Type "poke-cli ability solar-power --pokemon" Enter -Sleep 6s +Sleep 4s Type clear @@ -140,6 +140,18 @@ Type clear Enter +Type "poke-cli item choice-band" + +Sleep 2s + +Enter + +Sleep 3s + +Type clear + +Enter + Type "poke-cli search" Sleep 2s @@ -148,7 +160,7 @@ Enter Sleep 2s -Down@1s 2 +Down@500ms 2 Sleep 2s @@ -162,6 +174,79 @@ Sleep 2s Enter -Sleep 4s +Sleep 3s + +Escape + +Sleep 2s + +Type clear + +Enter + +Type "poke-cli speed" + +Sleep 2s + +Enter + +Sleep 5s + +Enter + +Sleep 1.5s + +Type "zapdos" + +Enter + +Sleep 1.5s + +Type "50" + +Enter + +Sleep 1.5s + +Type "252" + +Enter + +Sleep 1.5s + +Type "31" + +Enter + +Sleep 1.5s + +Down@500ms 1 + +Type "x" + +Sleep 2s + +Enter + +Down@500ms 3 + +Up@750ms 3 + +Enter + +Sleep 1.5s + +Down@500ms 1 + +Enter + +Sleep 1.5s + +Type "1" + +Sleep 2s + +Enter + +Sleep 6s -Type esc \ No newline at end of file diff --git a/docs/assets/speed.gif b/docs/assets/speed.gif new file mode 100644 index 0000000..892bd10 Binary files /dev/null and b/docs/assets/speed.gif differ diff --git a/docs/commands.md b/docs/commands.md index 2610a02..4ab9af0 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -130,6 +130,19 @@ Output: --- +## `speed` +* Calculate the speed of a Pokémon in battle. + +Example: +```console +$ poke-cli speed +``` +Output: + +![speed_command](assets/speed.gif) + +--- + ## `types` * Retrieve details about a specific type and a damage relation table. diff --git a/go.mod b/go.mod index 124b266..6b963cc 100644 --- a/go.mod +++ b/go.mod @@ -3,40 +3,44 @@ module github.com/digitalghost-dev/poke-cli go 1.24.4 require ( - github.com/charmbracelet/bubbles v0.20.0 - github.com/charmbracelet/bubbletea v1.3.5 + github.com/charmbracelet/bubbles v0.21.0 + github.com/charmbracelet/bubbletea v1.3.6 + github.com/charmbracelet/huh v0.7.0 github.com/charmbracelet/lipgloss v1.1.0 + github.com/charmbracelet/x/exp/strings v0.0.0-20250708181618-a60a724ba6c3 github.com/charmbracelet/x/exp/teatest v0.0.0-20250702191427-5bdfc8f2e4ff github.com/charmbracelet/x/term v0.2.1 github.com/disintegration/imaging v1.6.2 github.com/stretchr/testify v1.10.0 - golang.org/x/text v0.26.0 + golang.org/x/text v0.27.0 ) 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/catppuccin/go v0.3.0 // indirect github.com/charmbracelet/colorprofile v0.3.1 // indirect github.com/charmbracelet/x/ansi v0.9.3 // indirect github.com/charmbracelet/x/cellbuf v0.0.13 // indirect github.com/charmbracelet/x/exp/golden v0.0.0-20250702191427-5bdfc8f2e4ff // 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 github.com/lucasb-eyer/go-colorful v1.2.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.16 // 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 github.com/muesli/termenv v0.16.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/image v0.28.0 // indirect - golang.org/x/sync v0.15.0 // indirect - golang.org/x/sys v0.33.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.34.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e6a6aa2..765af5d 100644 --- a/go.sum +++ b/go.sum @@ -1,31 +1,51 @@ +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/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= -github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= -github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc= -github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54= +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.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= +github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= +github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU= +github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc= github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40= github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0= +github.com/charmbracelet/huh v0.7.0 h1:W8S1uyGETgj9Tuda3/JdVkc3x7DBLZYPZc4c+/rnRdc= +github.com/charmbracelet/huh v0.7.0/go.mod h1:UGC3DZHlgOKHvHC07a5vHag41zzhpPFj34U92sOmyuk= 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.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0= github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= +github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ= +github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= +github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= github.com/charmbracelet/x/exp/golden v0.0.0-20250702191427-5bdfc8f2e4ff h1:mEllIwjDs9aKqnzckYmNZqxoULwp4afFLVgH9x9QAGA= github.com/charmbracelet/x/exp/golden v0.0.0-20250702191427-5bdfc8f2e4ff/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= +github.com/charmbracelet/x/exp/strings v0.0.0-20250708181618-a60a724ba6c3 h1:QTf5vyE6CO+zZiF83Je/r3zWol31h3sKRJ7Vt2PwJVg= +github.com/charmbracelet/x/exp/strings v0.0.0-20250708181618-a60a724ba6c3/go.mod h1:Rgw3/F+xlcUc5XygUtimVSxAqCOsqyvJjqF5UHRvc5k= github.com/charmbracelet/x/exp/teatest v0.0.0-20250702191427-5bdfc8f2e4ff h1:DKxPeQDSnQPDxD27Bq8nUDwiSecy2Yf+nT3U/TlPXs8= github.com/charmbracelet/x/exp/teatest v0.0.0-20250702191427-5bdfc8f2e4ff/go.mod h1:RXbDhep1qKL/SEz2IuOhOUrsNHDKGqRmGks1nZStKyU= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +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/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI= +github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4= +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= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -36,6 +56,8 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +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= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -56,15 +78,15 @@ golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQz golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE= golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 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.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/styling/styling.go b/styling/styling.go index cdb1a68..8499d35 100644 --- a/styling/styling.go +++ b/styling/styling.go @@ -2,15 +2,19 @@ package styling import ( "fmt" + "github.com/charmbracelet/huh" "github.com/charmbracelet/lipgloss" "image/color" "regexp" ) var ( - Green = lipgloss.NewStyle().Foreground(lipgloss.Color("#38B000")) - Red = lipgloss.NewStyle().Foreground(lipgloss.Color("#D00000")) - Gray = lipgloss.Color("#777777") + Green = lipgloss.NewStyle().Foreground(lipgloss.Color("#38B000")) + Red = lipgloss.NewStyle().Foreground(lipgloss.Color("#D00000")) + Gray = lipgloss.Color("#777777") + YellowAdaptive = func(s string) string { + return lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#E1AD01", Dark: "#FFDE00"}).Render(s) + } ColoredBullet = lipgloss.NewStyle(). SetString("•"). Foreground(lipgloss.Color("#FFCC00")) @@ -69,7 +73,7 @@ func StripANSI(input string) string { return ansiRegex.ReplaceAllString(input, "") } -// To avoid unnecessary dependencies, I adapted the MakeColor function from +// Color To avoid unnecessary dependencies, I adapted the MakeColor function from // "github.com/lucasb-eyer/go-colorful" and implemented it using only the // standard library. Since I only needed this function, importing the entire // library was unnecessary. @@ -77,7 +81,7 @@ type Color struct { R, G, B float64 } -// Implement the Go color.Color interface. +// RGBA Implement the Go color.Color interface. func (col Color) RGBA() (uint32, uint32, uint32, uint32) { return uint32(col.R*65535.0 + 0.5), uint32(col.G*65535.0 + 0.5), uint32(col.B*65535.0 + 0.5), 0xFFFF } @@ -102,3 +106,49 @@ 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("#FFDE00") + 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/cli_help.golden b/testdata/cli_help.golden index 73ec9eb..3076d2f 100644 --- a/testdata/cli_help.golden +++ b/testdata/cli_help.golden @@ -1,29 +1,30 @@ -╭──────────────────────────────────────────────────────────╮ -│Welcome! This tool displays data related to Pokémon! │ -│ │ -│ USAGE: │ -│ poke-cli [flag] │ -│ poke-cli [flag] │ -│ poke-cli [flag] │ -│ │ -│ FLAGS: │ -│ -h, --help Shows the help menu │ -│ -l, --latest Prints the latest version available │ -│ -v, --version Prints the current version │ -│ │ -│ COMMANDS: │ -│ ability Get details about an ability │ -│ item Get details about an item │ -│ move Get details about a move │ -│ natures Get details about all natures │ -│ pokemon Get details about a Pokémon │ -│ search Search for a resource │ -│ types Get details about a typing │ -│ │ -│ hint: when calling a resource with a space, use a hyphen │ -│ example: poke-cli ability strong-jaw │ -│ example: poke-cli pokemon flutter-mane │ -│ │ -│ ↓ ctrl/cmd + click for docs/guides │ -│ ]8;;https://docs.poke-cli.com\docs.poke-cli.com]8;;\ │ -╰──────────────────────────────────────────────────────────╯ +╭───────────────────────────────────────────────────────────────╮ +│Welcome! This tool displays data related to Pokémon! │ +│ │ +│ USAGE: │ +│ poke-cli [flag] │ +│ poke-cli [flag] │ +│ poke-cli [flag] │ +│ │ +│ FLAGS: │ +│ -h, --help Shows the help menu │ +│ -l, --latest Prints the latest version available │ +│ -v, --version Prints the current version │ +│ │ +│ COMMANDS: │ +│ ability Get details about an ability │ +│ item Get details about an item │ +│ move Get details about a move │ +│ natures Get details about all natures │ +│ pokemon Get details about a Pokémon │ +│ search Search for a resource │ +│ speed Calculate the speed of a Pokémon in battle │ +│ types Get details about a typing │ +│ │ +│ hint: when calling a resource with a space, use a hyphen │ +│ example: poke-cli ability strong-jaw │ +│ example: poke-cli pokemon flutter-mane │ +│ │ +│ ↓ ctrl/cmd + click for docs/guides │ +│ ]8;;https://docs.poke-cli.com\docs.poke-cli.com]8;;\ │ +╰───────────────────────────────────────────────────────────────╯ diff --git a/testdata/cli_incorrect_command.golden b/testdata/cli_incorrect_command.golden index 8603c98..f87d825 100644 --- a/testdata/cli_incorrect_command.golden +++ b/testdata/cli_incorrect_command.golden @@ -1,14 +1,16 @@ -╭──────────────────────────────────────────────────╮ -│Error! │ -│ 'movesets' is not a valid command. │ -│ │ -│Commands: │ -│ ability Get details about an ability │ -│ move Get details about a move │ -│ natures Get details about all natures │ -│ pokemon Get details about a Pokémon │ -│ search Search for a resource │ -│ types Get details about a typing │ -│ │ -│Also run poke-cli -h for more info! │ -╰──────────────────────────────────────────────────╯ \ No newline at end of file +╭───────────────────────────────────────────────────────────────╮ +│Error! │ +│ 'movesets' is not a valid command. │ +│ │ +│Commands: │ +│ ability Get details about an ability │ +│ item Get details about an item │ +│ move Get details about a move │ +│ natures Get details about all natures │ +│ pokemon Get details about a Pokémon │ +│ search Search for a resource │ +│ speed Calculate the speed of a Pokémon in battle │ +│ types Get details about a typing │ +│ │ +│Also run poke-cli -h for more info! │ +╰───────────────────────────────────────────────────────────────╯ diff --git a/testdata/speed_help.golden b/testdata/speed_help.golden new file mode 100644 index 0000000..995d9f3 --- /dev/null +++ b/testdata/speed_help.golden @@ -0,0 +1,9 @@ +╭───────────────────────────────────────────────────────────╮ +│Calculate the speed of a Pokémon. │ +│ │ +│ USAGE: │ +│ poke-cli speed [flag] │ +│ │ +│ FLAGS: │ +│ -h, --help Prints out the help menu│ +╰───────────────────────────────────────────────────────────╯ \ No newline at end of file diff --git a/testdata/speed_invalid_option.golden b/testdata/speed_invalid_option.golden new file mode 100644 index 0000000..557a5b5 --- /dev/null +++ b/testdata/speed_invalid_option.golden @@ -0,0 +1,5 @@ +╭────────────────────────────────────╮ +│Error! │ +│The only available options after the│ +│ command are '-h' or '--help'│ +╰────────────────────────────────────╯ \ No newline at end of file