From f1dcd9d9d910441e33d39fe4746c16354ae85dd9 Mon Sep 17 00:00:00 2001 From: Tirthraj Raval Date: Fri, 10 Jul 2026 15:08:29 +0530 Subject: [PATCH 1/3] feat(polish): add doctor command and list output polish. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New: - cli/internal/cmd/doctor.go: --doctor health check (chafa, fastfetch, pokemon-colorscripts, terminal graphics protocol, config, manifest, assets on disk, fastfetch config, rc file integration) - cli/internal/cmd/completions.go: tab completion for --car, --brand, --decade, --category, --powertrain, --country, --set-mode. Loads manifest lazily via sync.Once. Includes both slugs and alias keys as --car completions. - cli/internal/cmd/list.go: grouped output with counts + year ranges for --list-brands ('Ferrari 91 cars 1947-2024'), --list-decades (with vintage/modern indicator), --list-categories (sorted by count) - .github/workflows/ci.yml: go build+vet, python py_compile, shellcheck on all install scripts Fixed: - uninstall.sh loop-start grep now also matches 'hypercar-installer-disabled' so files where the block was stripped manually still get processed for pokemon restoration. - uninstall.sh restore regex now handles leading whitespace (was breaking when install.sh preserved indentation via \\1). Changed: - install.sh drops completion files into XDG dirs for bash/zsh/fish after binary install. Non-fatal — installs succeed even if a shell is missing. - uninstall.sh removes the completion files." IMPORTANT: - The shell completion scripts still have issues and will be fixed in a follow-up commit. --- .github/workflows/ci.yml | 71 +++++++++ cli/install.sh | 98 +++++++------ cli/internal/cmd/completions.go | 125 ++++++++++++++++ cli/internal/cmd/doctor.go | 245 ++++++++++++++++++++++++++++++++ cli/internal/cmd/list.go | 140 ++++++++++++++++++ cli/internal/cmd/root.go | 21 ++- cli/uninstall.sh | 50 +++++-- 7 files changed, 687 insertions(+), 63 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 cli/internal/cmd/completions.go create mode 100644 cli/internal/cmd/doctor.go create mode 100644 cli/internal/cmd/list.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0607aa2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,71 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + go: + name: Go build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.21' + cache: true + cache-dependency-path: cli/go.sum + + - name: Build + working-directory: cli + run: go build ./... + + - name: Vet + working-directory: cli + run: go vet ./... + + - name: Verify completions generate + working-directory: cli + run: | + ./hypercar-colorscripts completion bash > /dev/null + ./hypercar-colorscripts completion zsh > /dev/null + ./hypercar-colorscripts completion fish > /dev/null + + python: + name: Python syntax + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: py_compile every script + run: | + set -e + for f in scripts/*.py cli/_vintage_helper.py; do + echo "Checking $f" + python3 -m py_compile "$f" + done + + shell: + name: Shellcheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install shellcheck + run: sudo apt-get update && sudo apt-get install -y shellcheck + + - name: Check install/uninstall scripts + run: | + # -e SC2086: word splitting is intentional in many places + # -e SC1091: don't chase 'source' targets outside the repo + shellcheck -e SC2086 -e SC1091 -e SC2154 \ + cli/install.sh cli/uninstall.sh \ + cli/install-vintage.sh cli/uninstall-vintage.sh \ No newline at end of file diff --git a/cli/install.sh b/cli/install.sh index 1af6fac..2a5a448 100755 --- a/cli/install.sh +++ b/cli/install.sh @@ -1,16 +1,8 @@ #!/usr/bin/env bash -# install.sh — installer for hypercar-colorscripts (v0.3 with vintage opt-in) +# install.sh — installer for hypercar-colorscripts (v0.3) # -# Asks LOGIN-shell detected user one question per concern: -# - default rendering mode (auto/ansi) -# - whether to include vintage cars (pre-2000) -# - whether to also install into a secondary rc file (e.g. .bashrc) -# - final go/no-go before modifying any rc file -# -# Vintage cars are managed by _vintage_helper.py. After installation, -# the user can add or remove vintage cars without rerunning install.sh: -# ./install-vintage.sh # add vintage -# ./uninstall-vintage.sh # remove vintage +# Changes since Phase B: +# - Installs shell completion files for bash / zsh / fish after binary # # Flags: # --system binary -> /usr/local/bin (sudo) @@ -50,10 +42,15 @@ SRC_MANIFEST="$PROJECT_ROOT/manifest.json" SRC_ASSETS="$PROJECT_ROOT/assets" HELPER="$SCRIPT_DIR/_vintage_helper.py" +# XDG-compliant completion directories +BASH_COMP_DIR="$HOME/.local/share/bash-completion/completions" +ZSH_COMP_DIR="$HOME/.local/share/zsh/site-functions" +FISH_COMP_DIR="$HOME/.config/fish/completions" + echo -e "${BLUE}═══ hypercar-colorscripts installer ═══${NC}" # ─── 1. Prereqs ───────────────────────────────── -step "1/8 Prerequisites" +step "1/9 Prerequisites" command -v chafa >/dev/null 2>&1 || fail "chafa required — sudo dnf install chafa" ok "chafa" @@ -64,23 +61,22 @@ ok "python: $PYTHON" [[ -f "$HELPER" ]] || fail "missing helper: $HELPER" # ─── 2. Build ─────────────────────────────────── -step "2/8 Binary" +step "2/9 Binary" [[ -x "$SCRIPT_DIR/$BINARY" ]] || (cd "$SCRIPT_DIR" && make build) || fail "make build failed" ok "$SCRIPT_DIR/$BINARY" # ─── 3. Source data ───────────────────────────── -step "3/8 Source data" +step "3/9 Source data" [[ -f "$SRC_MANIFEST" ]] || fail "manifest.json missing — run scripts/build_manifest.py" [[ -d "$SRC_ASSETS/images" ]] || fail "assets/images/ missing" ok "$(ls "$SRC_ASSETS/images" | wc -l) PNGs in source" -# Inspect source via helper eval "$($PYTHON "$HELPER" info "$SRC_MANIFEST")" || fail "helper info failed" VINTAGE_MB=$((VINTAGE_BYTES / 1024 / 1024)) ok "Source: $MODERN_COUNT modern + $VINTAGE_COUNT vintage cars (pre-2000, ~${VINTAGE_MB} MB)" # ─── 4. Default rendering mode ────────────────── -step "4/8 Default rendering mode" +step "4/9 Default rendering mode" DEFAULT_MODE="auto" if [[ $ASSUME_YES -eq 0 ]]; then echo "Choose how cars/pokemon render at terminal startup:" @@ -89,12 +85,10 @@ if [[ $ASSUME_YES -eq 0 ]]; then read -rp "Choice [1]: " m case "${m:-1}" in 2) DEFAULT_MODE="ansi" ;; *) DEFAULT_MODE="auto" ;; esac fi -ok "Mode: $DEFAULT_MODE (change later: hypercar-colorscripts --set-mode auto|ansi)" +ok "Mode: $DEFAULT_MODE" # ─── 5. Vintage choice ────────────────────────── -step "5/8 Vintage cars" - -# Detect previous install state so default flips intelligently +step "5/9 Vintage cars" PREV_HAS_VINTAGE=0 if [[ -f "$DATA_DIR/manifest.json" ]]; then eval "$($PYTHON "$HELPER" status "$DATA_DIR")" @@ -102,10 +96,7 @@ if [[ -f "$DATA_DIR/manifest.json" ]]; then fi INCLUDE_VINTAGE=$WITH_VINTAGE -if [[ $ASSUME_YES -eq 1 ]]; then - # --yes: respect --with-vintage flag, otherwise default to modern-only - : -else +if [[ $ASSUME_YES -eq 0 ]]; then echo "Include $VINTAGE_COUNT vintage cars (pre-2000)? Adds ~${VINTAGE_MB} MB." if [[ $PREV_HAS_VINTAGE -eq 1 ]]; then echo "(Currently installed with vintage)" @@ -120,19 +111,17 @@ fi if [[ $INCLUDE_VINTAGE -eq 1 ]]; then ok "Will install: all $TOTAL_COUNT cars" else - ok "Will install: $MODERN_COUNT modern cars (vintage available later via ./install-vintage.sh)" + ok "Will install: $MODERN_COUNT modern cars" fi # ─── 6. Install assets + config + binary ──────── -step "6/8 Install assets, config, binary" +step "6/9 Install assets, config, binary" mkdir -p "$DATA_DIR" if [[ $INCLUDE_VINTAGE -eq 1 ]]; then - "$PYTHON" "$HELPER" install-all "$SRC_MANIFEST" "$SRC_ASSETS" "$DATA_DIR" \ - || fail "install-all failed" + "$PYTHON" "$HELPER" install-all "$SRC_MANIFEST" "$SRC_ASSETS" "$DATA_DIR" || fail "install-all failed" else - "$PYTHON" "$HELPER" install-modern "$SRC_MANIFEST" "$SRC_ASSETS" "$DATA_DIR" \ - || fail "install-modern failed" + "$PYTHON" "$HELPER" install-modern "$SRC_MANIFEST" "$SRC_ASSETS" "$DATA_DIR" || fail "install-modern failed" fi ok "Assets in $DATA_DIR" @@ -158,8 +147,31 @@ else fi ok "Binary: $BIN_DIR/$BINARY" -# ─── 7. Shell rc detection ────────────────────── -step "7/8 Shell rc detection" +# ─── 7. Shell completions (NEW) ───────────────── +step "7/9 Shell completions" +INSTALLED_COMPLETIONS=() +mkdir -p "$BASH_COMP_DIR" 2>/dev/null && \ + "$BIN_DIR/$BINARY" completion bash > "$BASH_COMP_DIR/$BINARY" 2>/dev/null && \ + { ok "bash: $BASH_COMP_DIR/$BINARY"; INSTALLED_COMPLETIONS+=("bash"); } || \ + warn "bash completion install skipped" + +mkdir -p "$ZSH_COMP_DIR" 2>/dev/null && \ + "$BIN_DIR/$BINARY" completion zsh > "$ZSH_COMP_DIR/_$BINARY" 2>/dev/null && \ + { ok "zsh: $ZSH_COMP_DIR/_$BINARY"; INSTALLED_COMPLETIONS+=("zsh"); } || \ + warn "zsh completion install skipped" + +mkdir -p "$FISH_COMP_DIR" 2>/dev/null && \ + "$BIN_DIR/$BINARY" completion fish > "$FISH_COMP_DIR/$BINARY.fish" 2>/dev/null && \ + { ok "fish: $FISH_COMP_DIR/$BINARY.fish"; INSTALLED_COMPLETIONS+=("fish"); } || \ + warn "fish completion install skipped" + +if [[ ${#INSTALLED_COMPLETIONS[@]} -gt 0 ]]; then + echo " Completions may not activate until you restart your shell." + echo " (bash needs 'bash-completion' package; zsh needs 'autoload -Uz compinit')" +fi + +# ─── 8. Shell rc detection ────────────────────── +step "8/9 Shell rc detection" [[ $NO_SHELL -eq 1 ]] && { warn "Skipped (--no-shell)"; exit 0; } detect_login_shell() { @@ -206,12 +218,12 @@ if [[ $ASSUME_YES -eq 0 ]]; then [[ ! "${ans:-Y}" =~ ^[Yy]$ ]] && { warn "Skipped."; exit 0; } fi -# ─── 8. Apply rc modifications ────────────────── -step "8/8 Apply rc modifications" +# ─── 9. Apply rc modifications ────────────────── +step "9/9 Apply rc modifications" for RC in "${RC_FILES[@]}"; do echo echo "Processing $RC" - test -w "$RC" || fail "$RC not writable. chmod u+w $RC" + test -w "$RC" || fail "$RC not writable" BACKUP="$RC.hypercar-backup-$(date +%Y%m%d-%H%M%S)" cp "$RC" "$BACKUP" || fail "backup failed" @@ -288,20 +300,16 @@ HC_BLOCK_END [[ $AFTER -gt $BEFORE ]] || fail "$RC didn't grow ($BEFORE -> $AFTER bytes)" grep -q '# >>> hypercar-colorscripts >>>' "$RC" || fail "$RC missing marker after append" - ok "Block appended ($((AFTER - BEFORE)) bytes, line $(grep -n '# >>> hypercar-colorscripts >>>' "$RC" | head -1 | cut -d: -f1))" + ok "Block appended ($((AFTER - BEFORE)) bytes)" ok "Backup: $BACKUP" done echo echo -e "${GREEN}═══ Done ═══${NC}" -[[ $INCLUDE_VINTAGE -eq 1 ]] && \ - echo "Installed: $TOTAL_COUNT cars (modern + vintage)" || \ - echo "Installed: $MODERN_COUNT cars (modern only)" -echo "Mode: $DEFAULT_MODE (toggle with: hypercar-colorscripts --set-mode auto|ansi)" -echo "Rc files: ${RC_FILES[*]}" -echo -echo "Vintage management (run from this repo):" -echo " ./install-vintage.sh add vintage cars" -echo " ./uninstall-vintage.sh remove vintage cars" +[[ $INCLUDE_VINTAGE -eq 1 ]] && echo "Installed: $TOTAL_COUNT cars (modern + vintage)" || echo "Installed: $MODERN_COUNT cars (modern only)" +echo "Mode: $DEFAULT_MODE" +[[ ${#INSTALLED_COMPLETIONS[@]} -gt 0 ]] && echo "Completions: ${INSTALLED_COMPLETIONS[*]}" echo +echo "Run hypercar-colorscripts --doctor to verify the install." +echo "Vintage: ./install-vintage.sh | ./uninstall-vintage.sh" echo "Open a new terminal to see the changes." \ No newline at end of file diff --git a/cli/internal/cmd/completions.go b/cli/internal/cmd/completions.go new file mode 100644 index 0000000..0c829b7 --- /dev/null +++ b/cli/internal/cmd/completions.go @@ -0,0 +1,125 @@ +package cmd + +import ( + "os" + "path/filepath" + "sort" + "strings" + "sync" + + "hypercar-colorscripts/internal/config" + "hypercar-colorscripts/internal/manifest" + + "github.com/spf13/cobra" +) + +// The completion functions in this file are registered against the root +// command's flags in root.go via cobra.Command.RegisterFlagCompletionFunc. +// +// They enable tab completion in bash/zsh/fish: +// hypercar-colorscripts --car chi → chiron, bugatti_chiron, ... +// hypercar-colorscripts --brand Fe → Ferrari +// hypercar-colorscripts --decade 19 → 1960s, 1970s, ... + +var ( + completionMfOnce sync.Once + completionMf *manifest.Manifest +) + +// loadCompletionManifest lazily loads the manifest at completion time. +// Cached so tabbing multiple times doesn't re-parse. +func loadCompletionManifest() *manifest.Manifest { + completionMfOnce.Do(func() { + var dataDir string + if flagDataDir != "" { + dataDir = flagDataDir + } else if cfg, err := config.Load(); err == nil && cfg.DataDir != "" { + dataDir = cfg.DataDir + } else { + home, _ := os.UserHomeDir() + dataDir = filepath.Join(home, ".local/share/hypercar-colorscripts") + } + if mf, err := manifest.Load(filepath.Join(dataDir, "manifest.json")); err == nil { + completionMf = mf + } + }) + return completionMf +} + +// completeCars returns slug candidates + alias keys for --car / --info / --license. +func completeCars(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { + mf := loadCompletionManifest() + if mf == nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + prefix := strings.ToLower(toComplete) + var out []string + for slug := range mf.Cars { + if strings.HasPrefix(strings.ToLower(slug), prefix) { + out = append(out, slug) + } + } + for alias := range CommonAliases { + if strings.HasPrefix(alias, prefix) { + out = append(out, alias) + } + } + sort.Strings(out) + return out, cobra.ShellCompDirectiveNoFileComp +} + +func completeBrands(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return uniqueField(toComplete, func(c manifest.Car) string { return c.Brand }), cobra.ShellCompDirectiveNoFileComp +} + +func completeDecades(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return uniqueField(toComplete, func(c manifest.Car) string { return c.Decade }), cobra.ShellCompDirectiveNoFileComp +} + +func completeCategories(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return uniqueField(toComplete, func(c manifest.Car) string { return c.Category }), cobra.ShellCompDirectiveNoFileComp +} + +func completePowertrains(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return uniqueField(toComplete, func(c manifest.Car) string { return c.Powertrain }), cobra.ShellCompDirectiveNoFileComp +} + +func completeCountries(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return uniqueField(toComplete, func(c manifest.Car) string { return c.Country }), cobra.ShellCompDirectiveNoFileComp +} + +// completeSetMode has fixed values, no manifest needed. +func completeSetMode(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var out []string + for _, v := range []string{"auto", "ansi"} { + if strings.HasPrefix(v, strings.ToLower(toComplete)) { + out = append(out, v) + } + } + return out, cobra.ShellCompDirectiveNoFileComp +} + +// uniqueField collects unique non-empty values of one Car field, filtered by prefix. +func uniqueField(prefix string, get func(manifest.Car) string) []string { + mf := loadCompletionManifest() + if mf == nil { + return nil + } + seen := make(map[string]bool) + lower := strings.ToLower(prefix) + for _, c := range mf.Cars { + v := get(c) + if v == "" || seen[v] { + continue + } + if strings.HasPrefix(strings.ToLower(v), lower) { + seen[v] = true + } + } + out := make([]string, 0, len(seen)) + for v := range seen { + out = append(out, v) + } + sort.Strings(out) + return out +} diff --git a/cli/internal/cmd/doctor.go b/cli/internal/cmd/doctor.go new file mode 100644 index 0000000..9851ad4 --- /dev/null +++ b/cli/internal/cmd/doctor.go @@ -0,0 +1,245 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "hypercar-colorscripts/internal/config" + "hypercar-colorscripts/internal/manifest" +) + +const ( + cRed = "\033[0;31m" + cGreen = "\033[0;32m" + cYellow = "\033[1;33m" + cCyan = "\033[0;36m" + cReset = "\033[0m" +) + +func runDoctor() error { + fmt.Println("hypercar-colorscripts doctor") + fmt.Println(strings.Repeat("─", 60)) + + // 1. System dependencies + fmt.Println("\nSystem dependencies:") + docCheckCommand("chafa", true, "required — image rendering") + docCheckCommand("fastfetch", false, "recommended — system info beside image") + docCheckCommand("pokemon-colorscripts", false, "optional — enables pokemon mixing") + + // 2. Terminal capability + fmt.Println("\nTerminal capability:") + docCheckTerminal() + + // 3. Config + fmt.Println("\nConfiguration:") + cfg, cfgErr := config.Load() + if cfgErr != nil { + fmt.Printf(" %s✗%s config not loadable: %v\n", cRed, cReset, cfgErr) + } else { + fmt.Printf(" %s✓%s config file: %s\n", cGreen, cReset, config.Path()) + fmt.Printf(" data_dir: %s\n", cfg.DataDir) + fmt.Printf(" default_mode: %s\n", cfg.DefaultMode) + } + + // 4. Manifest + assets + fmt.Println("\nInstalled data:") + dataDir := "" + if cfg.DataDir != "" { + dataDir = cfg.DataDir + } else { + home, _ := os.UserHomeDir() + dataDir = filepath.Join(home, ".local/share/hypercar-colorscripts") + } + manifestPath := filepath.Join(dataDir, "manifest.json") + docCheckManifest(manifestPath, dataDir) + + // 5. Fastfetch config + fmt.Println("\nStartup integration:") + home, _ := os.UserHomeDir() + ffCfg := filepath.Join(home, ".config/fastfetch/config-pokemon.jsonc") + if _, err := os.Stat(ffCfg); err == nil { + fmt.Printf(" %s✓%s fastfetch config: %s\n", cGreen, cReset, ffCfg) + } else { + fmt.Printf(" %s○%s fastfetch config: not found at %s\n", cYellow, cReset, ffCfg) + fmt.Printf(" (startup will use plain output — install fastfetch + config for full experience)\n") + } + docCheckShellRc() + + // 6. Effective pokemon-mixing state (the previously confusing part) + fmt.Println("\nPokemon integration (effective state):") + docCheckPokemonMixing(cfg) + + fmt.Println("\n" + strings.Repeat("─", 60)) + fmt.Println("Legend: ✓ working ○ optional/missing ⚠ needs attention ✗ broken") + return nil +} + +// docCheckPokemonMixing reports the ACTUAL mixing behavior the user will see, +// combining: pokemon-colorscripts availability, shell block presence, and the +// binary's own with_pokemon setting. +func docCheckPokemonMixing(cfg config.Config) { + // (a) is pokemon-colorscripts on PATH? + pokePath, err := exec.LookPath("pokemon-colorscripts") + pokeInstalled := err == nil + + // (b) does the shell startup block exist? + home, _ := os.UserHomeDir() + shellBlockFound := false + for _, rc := range []string{".zshrc", ".bashrc"} { + data, err := os.ReadFile(filepath.Join(home, rc)) + if err == nil && strings.Contains(string(data), "# >>> hypercar-colorscripts >>>") { + shellBlockFound = true + break + } + } + + // Report each piece + if pokeInstalled { + fmt.Printf(" %s✓%s pokemon-colorscripts installed at %s\n", cGreen, cReset, pokePath) + } else { + fmt.Printf(" %s○%s pokemon-colorscripts not installed (no mixing possible)\n", cYellow, cReset) + } + + if shellBlockFound { + fmt.Printf(" %s✓%s shell startup block present\n", cGreen, cReset) + } else { + fmt.Printf(" %s○%s shell startup block not found in ~/.zshrc or ~/.bashrc\n", cYellow, cReset) + } + + // Effective behavior + fmt.Println() + switch { + case pokeInstalled && shellBlockFound: + fmt.Printf(" %sEffective behavior:%s new terminals show 50%% pokemon, 50%% car\n", cCyan, cReset) + case !pokeInstalled && shellBlockFound: + fmt.Printf(" %sEffective behavior:%s new terminals always show a car\n", cCyan, cReset) + case pokeInstalled && !shellBlockFound: + fmt.Printf(" %sEffective behavior:%s no startup output (block missing — run install.sh)\n", cCyan, cReset) + default: + fmt.Printf(" %sEffective behavior:%s no startup output (nothing installed)\n", cCyan, cReset) + } + + // Binary's own setting (only mention if non-default, since usually false) + if cfg.WithPokemon { + fmt.Printf("\n %s⚠%s config.json has with_pokemon=true\n", cYellow, cReset) + fmt.Printf(" This makes the binary ALSO roll pokemon on bare invocation.\n") + fmt.Printf(" Combined with the shell block above, you may see pokemon twice.\n") + fmt.Printf(" To disable: hypercar-colorscripts --set-with-pokemon false (or edit config.json)\n") + } +} + +// … the rest of doctor.go stays the same (docCheckCommand, docCheckTerminal, +// docCheckManifest, docCountFiles, docCheckShellRc) — no changes needed there. + +func docCheckCommand(name string, required bool, desc string) { + path, err := exec.LookPath(name) + if err != nil { + if required { + fmt.Printf(" %s✗%s %-25s NOT FOUND (%s)\n", cRed, cReset, name, desc) + } else { + fmt.Printf(" %s○%s %-25s not found (%s)\n", cYellow, cReset, name, desc) + } + return + } + fmt.Printf(" %s✓%s %-25s %s\n", cGreen, cReset, name, path) + _ = desc +} + +func docCheckTerminal() { + term := os.Getenv("TERM") + termProgram := os.Getenv("TERM_PROGRAM") + kittyID := os.Getenv("KITTY_WINDOW_ID") + ghosttyDir := os.Getenv("GHOSTTY_RESOURCES_DIR") + + imgOK := false + protocol := "none" + switch { + case kittyID != "": + imgOK, protocol = true, "Kitty graphics protocol" + case termProgram == "WezTerm": + imgOK, protocol = true, "WezTerm imgcat" + case ghosttyDir != "": + imgOK, protocol = true, "Ghostty (Kitty-compatible)" + case strings.HasPrefix(term, "xterm-kitty"): + imgOK, protocol = true, "Kitty via TERM" + case termProgram == "iTerm.app": + imgOK, protocol = true, "iTerm2 inline images" + } + if imgOK { + fmt.Printf(" %s✓%s image protocol: %s\n", cGreen, cReset, protocol) + } else { + fmt.Printf(" %s○%s image protocol: none detected (will use ANSI fallback)\n", cYellow, cReset) + } + fmt.Printf(" TERM=%s TERM_PROGRAM=%s KITTY_WINDOW_ID=%q\n", term, termProgram, kittyID) +} + +func docCheckManifest(manifestPath, dataDir string) { + data, err := os.ReadFile(manifestPath) + if err != nil { + fmt.Printf(" %s✗%s manifest.json NOT FOUND at %s\n", cRed, cReset, manifestPath) + fmt.Printf(" Run: cd cli && ./install.sh\n") + return + } + var m manifest.Manifest + if err := json.Unmarshal(data, &m); err != nil { + fmt.Printf(" %s✗%s manifest.json corrupt: %v\n", cRed, cReset, err) + return + } + fmt.Printf(" %s✓%s manifest.json: %d cars (%d KB)\n", + cGreen, cReset, len(m.Cars), len(data)/1024) + + imgDir := filepath.Join(dataDir, "assets/images") + ansiDir := filepath.Join(dataDir, "assets/ansi") + imgCount := docCountFiles(imgDir, ".png") + ansiCount := docCountFiles(ansiDir, ".txt") + + fmt.Printf(" %s✓%s images: %d PNG files\n", cGreen, cReset, imgCount) + fmt.Printf(" %s✓%s ansi: %d TXT files\n", cGreen, cReset, ansiCount) + + if imgCount != len(m.Cars) || ansiCount != len(m.Cars) { + fmt.Printf(" %s⚠%s asset count mismatch: manifest=%d images=%d ansi=%d\n", + cYellow, cReset, len(m.Cars), imgCount, ansiCount) + } +} + +func docCountFiles(dir, ext string) int { + entries, err := os.ReadDir(dir) + if err != nil { + return 0 + } + n := 0 + for _, e := range entries { + if !e.IsDir() && strings.HasSuffix(e.Name(), ext) { + n++ + } + } + return n +} + +func docCheckShellRc() { + home, _ := os.UserHomeDir() + for _, rc := range []string{".zshrc", ".bashrc"} { + path := filepath.Join(home, rc) + data, err := os.ReadFile(path) + if err != nil { + continue + } + s := string(data) + hasBlock := strings.Contains(s, "# >>> hypercar-colorscripts >>>") + hasPath := strings.Contains(s, "# >>> hypercar-colorscripts PATH >>>") + switch { + case hasBlock && hasPath: + fmt.Printf(" %s✓%s ~/%s: startup block + PATH export present\n", cGreen, cReset, rc) + case hasBlock: + fmt.Printf(" %s⚠%s ~/%s: startup block present but PATH export missing\n", cYellow, cReset, rc) + case hasPath: + fmt.Printf(" %s⚠%s ~/%s: PATH export present but startup block missing\n", cYellow, cReset, rc) + default: + fmt.Printf(" %s○%s ~/%s: no hypercar integration\n", cYellow, cReset, rc) + } + } +} diff --git a/cli/internal/cmd/list.go b/cli/internal/cmd/list.go new file mode 100644 index 0000000..d008cde --- /dev/null +++ b/cli/internal/cmd/list.go @@ -0,0 +1,140 @@ +package cmd + +import ( + "fmt" + "sort" + "strconv" + + "hypercar-colorscripts/internal/manifest" +) + +// The three Print*List functions below replace flat-list output with +// grouped, count-aware output. Call these from root.go where you +// currently handle --list-brands, --list-decades, --list-categories. + +type brandStats struct { + count int + minY int + maxY int +} + +// PrintBrandList prints one line per brand: "Ferrari 91 cars 1947–2024" +func PrintBrandList(mf *manifest.Manifest) { + m := make(map[string]*brandStats) + for _, c := range mf.Cars { + if c.Brand == "" { + continue + } + s, ok := m[c.Brand] + if !ok { + s = &brandStats{minY: 99999, maxY: 0} + m[c.Brand] = s + } + s.count++ + if y, err := strconv.Atoi(c.StartYear); err == nil && y > 0 { + if y < s.minY { + s.minY = y + } + if y > s.maxY { + s.maxY = y + } + } + if y, err := strconv.Atoi(c.EndYear); err == nil && y > 0 { + if y > s.maxY { + s.maxY = y + } + } + } + + brands := make([]string, 0, len(m)) + for b := range m { + brands = append(brands, b) + } + sort.Strings(brands) + + maxLen := 0 + for _, b := range brands { + if len(b) > maxLen { + maxLen = len(b) + } + } + + for _, b := range brands { + s := m[b] + years := yearRange(s.minY, s.maxY) + fmt.Printf(" %-*s %4d cars %s\n", maxLen, b, s.count, years) + } + fmt.Printf("\n%d brands, %d cars total\n", len(brands), len(mf.Cars)) +} + +// PrintDecadeList prints one line per decade in chronological order. +func PrintDecadeList(mf *manifest.Manifest) { + counts := make(map[string]int) + for _, c := range mf.Cars { + if c.Decade == "" { + continue + } + counts[c.Decade]++ + } + + decades := make([]string, 0, len(counts)) + for d := range counts { + decades = append(decades, d) + } + sort.Strings(decades) // "1940s" < "1950s" < ... alpha == chrono for decades + + for _, d := range decades { + era := "modern" + if y, err := strconv.Atoi(d[:len(d)-1]); err == nil && y < 2000 { + era = "vintage" + } + fmt.Printf(" %-8s %4d cars (%s)\n", d, counts[d], era) + } + fmt.Printf("\n%d decades, %d cars total\n", len(decades), len(mf.Cars)) +} + +// PrintCategoryList prints one line per category, sorted by count desc. +func PrintCategoryList(mf *manifest.Manifest) { + counts := make(map[string]int) + for _, c := range mf.Cars { + if c.Category == "" { + continue + } + counts[c.Category]++ + } + type kv struct { + k string + v int + } + pairs := make([]kv, 0, len(counts)) + for k, v := range counts { + pairs = append(pairs, kv{k, v}) + } + sort.Slice(pairs, func(i, j int) bool { + if pairs[i].v != pairs[j].v { + return pairs[i].v > pairs[j].v + } + return pairs[i].k < pairs[j].k + }) + + maxLen := 0 + for _, p := range pairs { + if len(p.k) > maxLen { + maxLen = len(p.k) + } + } + for _, p := range pairs { + fmt.Printf(" %-*s %4d cars\n", maxLen, p.k, p.v) + } + fmt.Printf("\n%d categories, %d cars total\n", len(pairs), len(mf.Cars)) +} + +func yearRange(minY, maxY int) string { + if minY == 99999 || maxY == 0 { + return "—" + } + if minY == maxY { + return strconv.Itoa(minY) + } + return fmt.Sprintf("%d–%d", minY, maxY) +} diff --git a/cli/internal/cmd/root.go b/cli/internal/cmd/root.go index 1ffee8e..e1b97ab 100644 --- a/cli/internal/cmd/root.go +++ b/cli/internal/cmd/root.go @@ -116,6 +116,17 @@ func Execute(version string) error { root.MarkFlagsMutuallyExclusive("auto", "ansi-only", "image-only") root.MarkFlagsMutuallyExclusive("with-pokemon", "no-pokemon", "pokemon-only") + // Tab-completion for flag values (bash/zsh/fish) + _ = root.RegisterFlagCompletionFunc("car", completeCars) + _ = root.RegisterFlagCompletionFunc("info", completeCars) + _ = root.RegisterFlagCompletionFunc("license", completeCars) + _ = root.RegisterFlagCompletionFunc("brand", completeBrands) + _ = root.RegisterFlagCompletionFunc("decade", completeDecades) + _ = root.RegisterFlagCompletionFunc("category", completeCategories) + _ = root.RegisterFlagCompletionFunc("powertrain", completePowertrains) + _ = root.RegisterFlagCompletionFunc("country", completeCountries) + _ = root.RegisterFlagCompletionFunc("set-mode", completeSetMode) + return root.Execute() } @@ -176,11 +187,14 @@ func run(cmd *cobra.Command, args []string) error { // Listing & inspection commands switch { case flagListBrands: - return printUnique(mf.UniqueBrands()) + PrintBrandList(mf) + return nil case flagListDecades: - return printUnique(mf.UniqueDecades()) + PrintDecadeList(mf) + return nil case flagListCategories: - return printUnique(mf.UniqueCategories()) + PrintCategoryList(mf) + return nil case flagListPowertrains: return printUnique(mf.UniquePowertrains()) case flagListCountries: @@ -364,6 +378,7 @@ func printLicenseHistogram(mf *manifest.Manifest) error { return nil } +// doDoctor is called from --doctor func doDoctor(dataDir string, cfg config.Config) error { fmt.Println("=== hypercar-colorscripts diagnostics ===") fmt.Printf("Config path: %s\n", config.Path()) diff --git a/cli/uninstall.sh b/cli/uninstall.sh index d3ce016..63386ba 100755 --- a/cli/uninstall.sh +++ b/cli/uninstall.sh @@ -1,8 +1,12 @@ #!/usr/bin/env bash -# uninstall.sh — remove hypercar-colorscripts cleanly (v0.2) +# uninstall.sh — remove hypercar-colorscripts cleanly (v0.3) # -# Now handles multi-rc installs (both .zshrc and .bashrc if the -# installer wrote to both). +# Fixes since Phase 2: +# - Loop-start grep now also matches 'hypercar-installer-disabled', +# so files where the block was already stripped but a disabled +# pokemon line remains still get processed. +# - Restore regex now handles leading whitespace. +# - Removes shell completion files installed by install.sh. # # Options: # 1) Only hypercar-colorscripts (default) @@ -19,6 +23,11 @@ CONFIG_DIR="$HOME/.config/hypercar-colorscripts" BIN_USER="$HOME/.local/bin/hypercar-colorscripts" BIN_SYS="/usr/local/bin/hypercar-colorscripts" +# Completion paths (must match install.sh) +BASH_COMP="$HOME/.local/share/bash-completion/completions/hypercar-colorscripts" +ZSH_COMP="$HOME/.local/share/zsh/site-functions/_hypercar-colorscripts" +FISH_COMP="$HOME/.config/fish/completions/hypercar-colorscripts.fish" + echo -e "${BLUE}═══ hypercar-colorscripts uninstaller ═══${NC}" echo echo "What to uninstall?" @@ -33,15 +42,18 @@ echo " - $BIN_USER (if exists)" echo " - $BIN_SYS (if exists)" echo " - $DATA_DIR/" echo " - $CONFIG_DIR/" -echo " - hypercar blocks from ~/.zshrc AND ~/.bashrc (whichever exist)" +echo " - hypercar completion files (bash/zsh/fish)" +echo " - hypercar blocks from ~/.zshrc AND ~/.bashrc (backups made)" [[ "$choice" == "2" ]] && echo " - pokemon-colorscripts" read -rp "Continue? [y/N]: " confirm [[ ! "${confirm:-N}" =~ ^[Yy]$ ]] && { echo "Aborted."; exit 0; } -# Process every rc file the installer might have touched +# ─── Process each rc file ────────────────────── for RC in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.profile"; do [[ -f "$RC" ]] || continue - if ! grep -q "hypercar-colorscripts" "$RC" 2>/dev/null; then + # BUGFIX: also match the disabled-marker so we don't skip files where + # the block was already stripped manually. + if ! grep -qE "hypercar-colorscripts|hypercar-installer-disabled" "$RC" 2>/dev/null; then continue fi @@ -52,33 +64,42 @@ for RC in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.profile"; do sed -i.tmp '/# >>> hypercar-colorscripts PATH >>>/,/# <<< hypercar-colorscripts PATH <<>> hypercar-colorscripts >>>/,/# <<< hypercar-colorscripts <</dev/null || true rm -f "$RC.tmp" ok " Cleaned $RC" done -# Binaries +# ─── Binaries ────────────────────────────────── [[ -f "$BIN_USER" ]] && rm -f "$BIN_USER" && ok "Removed $BIN_USER" if [[ -f "$BIN_SYS" ]]; then if [[ -w "$BIN_SYS" ]]; then rm -f "$BIN_SYS"; else sudo rm -f "$BIN_SYS"; fi ok "Removed $BIN_SYS" fi -# Data + config dirs +# ─── Data + config dirs ──────────────────────── [[ -d "$DATA_DIR" ]] && rm -rf "$DATA_DIR" && ok "Removed $DATA_DIR" [[ -d "$CONFIG_DIR" ]] && rm -rf "$CONFIG_DIR" && ok "Removed $CONFIG_DIR" -# Option 2: also remove pokemon-colorscripts +# ─── Completion files (NEW) ──────────────────── +for f in "$BASH_COMP" "$ZSH_COMP" "$FISH_COMP"; do + if [[ -f "$f" ]]; then + rm -f "$f" + ok "Removed completion: $f" + fi +done + +# ─── Option 2: also uninstall pokemon-colorscripts ─ if [[ "$choice" == "2" ]] && command -v pokemon-colorscripts >/dev/null 2>&1; then echo echo -e "${BLUE}Removing pokemon-colorscripts...${NC}" @@ -97,7 +118,6 @@ if [[ "$choice" == "2" ]] && command -v pokemon-colorscripts >/dev/null 2>&1; th ok "Removed $d" fi done - # Remove any (now-restored) pokemon line from rc files for RC in "$HOME/.zshrc" "$HOME/.bashrc"; do [[ -f "$RC" ]] && sed -i.tmp '/^pokemon-colorscripts/d' "$RC" && rm -f "$RC.tmp" done From c74603ae962f285447763809bf03a98697083418 Mon Sep 17 00:00:00 2001 From: Tirthraj Raval Date: Fri, 10 Jul 2026 16:04:40 +0530 Subject: [PATCH 2/3] update(setup): Removed the Shell Completion Logic. - Added the pokemon mixing option while installing. - Removed the shell completion logic from the install script and the completions.go file. --- .github/workflows/ci.yml | 9 -- cli/install.sh | 167 +++++++++++++++----------------- cli/internal/cmd/completions.go | 125 ------------------------ cli/internal/cmd/doctor.go | 37 ++++--- cli/internal/cmd/root.go | 63 +++++++++--- cli/internal/config/config.go | 23 ++++- cli/uninstall.sh | 31 +++--- 7 files changed, 186 insertions(+), 269 deletions(-) delete mode 100644 cli/internal/cmd/completions.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0607aa2..d6348e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,13 +28,6 @@ jobs: working-directory: cli run: go vet ./... - - name: Verify completions generate - working-directory: cli - run: | - ./hypercar-colorscripts completion bash > /dev/null - ./hypercar-colorscripts completion zsh > /dev/null - ./hypercar-colorscripts completion fish > /dev/null - python: name: Python syntax runs-on: ubuntu-latest @@ -64,8 +57,6 @@ jobs: - name: Check install/uninstall scripts run: | - # -e SC2086: word splitting is intentional in many places - # -e SC1091: don't chase 'source' targets outside the repo shellcheck -e SC2086 -e SC1091 -e SC2154 \ cli/install.sh cli/uninstall.sh \ cli/install-vintage.sh cli/uninstall-vintage.sh \ No newline at end of file diff --git a/cli/install.sh b/cli/install.sh index 2a5a448..261515f 100755 --- a/cli/install.sh +++ b/cli/install.sh @@ -1,14 +1,15 @@ #!/usr/bin/env bash -# install.sh — installer for hypercar-colorscripts (v0.3) +# install.sh — installer for hypercar-colorscripts (v0.3.2) # -# Changes since Phase B: -# - Installs shell completion files for bash / zsh / fish after binary +# Removed: shell tab completion installation (bash/zsh/fish). +# Everything else preserved from v0.3.1. # # Flags: # --system binary -> /usr/local/bin (sudo) # --no-shell skip all .rc modifications -# --yes / -y skip confirmations; defaults to modern-only, auto mode +# --yes / -y skip confirmations # --with-vintage include vintage cars even with --yes +# --no-mix don't mix pokemon even if pokemon-colorscripts is present # --uninstall forward to uninstall.sh set -uo pipefail @@ -20,13 +21,14 @@ warn() { echo -e "${YELLOW}⚠${NC} $*"; } fail() { echo -e "${RED}✗${NC} $*" >&2; exit 1; } fsize() { stat -c %s "$1" 2>/dev/null || stat -f %z "$1" 2>/dev/null || echo 0; } -SYSTEM_INSTALL=0; NO_SHELL=0; ASSUME_YES=0; WITH_VINTAGE=0 +SYSTEM_INSTALL=0; NO_SHELL=0; ASSUME_YES=0; WITH_VINTAGE=0; NO_MIX=0 for arg in "$@"; do case "$arg" in --system) SYSTEM_INSTALL=1 ;; --no-shell) NO_SHELL=1 ;; --yes|-y) ASSUME_YES=1 ;; --with-vintage) WITH_VINTAGE=1 ;; + --no-mix) NO_MIX=1 ;; --uninstall) exec "$(dirname "${BASH_SOURCE[0]}")/uninstall.sh" ;; --help|-h) sed -n '2,/^set -uo/p' "$0" | sed 's/^# \?//'; exit 0 ;; esac @@ -42,64 +44,59 @@ SRC_MANIFEST="$PROJECT_ROOT/manifest.json" SRC_ASSETS="$PROJECT_ROOT/assets" HELPER="$SCRIPT_DIR/_vintage_helper.py" -# XDG-compliant completion directories -BASH_COMP_DIR="$HOME/.local/share/bash-completion/completions" -ZSH_COMP_DIR="$HOME/.local/share/zsh/site-functions" -FISH_COMP_DIR="$HOME/.config/fish/completions" - echo -e "${BLUE}═══ hypercar-colorscripts installer ═══${NC}" -# ─── 1. Prereqs ───────────────────────────────── -step "1/9 Prerequisites" +# 1. Prereqs +step "1/8 Prerequisites" command -v chafa >/dev/null 2>&1 || fail "chafa required — sudo dnf install chafa" ok "chafa" - PYTHON="$(command -v python3 || command -v python || true)" [[ -n "$PYTHON" ]] || fail "python required" ok "python: $PYTHON" - [[ -f "$HELPER" ]] || fail "missing helper: $HELPER" -# ─── 2. Build ─────────────────────────────────── -step "2/9 Binary" +HAS_POKEMON=0 +if command -v pokemon-colorscripts >/dev/null 2>&1; then + HAS_POKEMON=1 + ok "pokemon-colorscripts detected at $(command -v pokemon-colorscripts)" +fi + +# 2. Build +step "2/8 Binary" [[ -x "$SCRIPT_DIR/$BINARY" ]] || (cd "$SCRIPT_DIR" && make build) || fail "make build failed" ok "$SCRIPT_DIR/$BINARY" -# ─── 3. Source data ───────────────────────────── -step "3/9 Source data" -[[ -f "$SRC_MANIFEST" ]] || fail "manifest.json missing — run scripts/build_manifest.py" +# 3. Source data +step "3/8 Source data" +[[ -f "$SRC_MANIFEST" ]] || fail "manifest.json missing" [[ -d "$SRC_ASSETS/images" ]] || fail "assets/images/ missing" -ok "$(ls "$SRC_ASSETS/images" | wc -l) PNGs in source" - eval "$($PYTHON "$HELPER" info "$SRC_MANIFEST")" || fail "helper info failed" VINTAGE_MB=$((VINTAGE_BYTES / 1024 / 1024)) -ok "Source: $MODERN_COUNT modern + $VINTAGE_COUNT vintage cars (pre-2000, ~${VINTAGE_MB} MB)" +ok "Source: $MODERN_COUNT modern + $VINTAGE_COUNT vintage cars (~${VINTAGE_MB} MB)" -# ─── 4. Default rendering mode ────────────────── -step "4/9 Default rendering mode" +# 4. Rendering mode +step "4/8 Default rendering mode" DEFAULT_MODE="auto" if [[ $ASSUME_YES -eq 0 ]]; then - echo "Choose how cars/pokemon render at terminal startup:" - echo " 1) auto — real image in Kitty/Ghostty/WezTerm, ANSI text elsewhere (recommended)" - echo " 2) ansi — always pre-rendered text (any terminal, including git bash)" + echo "How to render at terminal startup:" + echo " 1) auto — real image in Kitty/Ghostty/WezTerm, ANSI text elsewhere" + echo " 2) ansi — always pre-rendered text" read -rp "Choice [1]: " m case "${m:-1}" in 2) DEFAULT_MODE="ansi" ;; *) DEFAULT_MODE="auto" ;; esac fi ok "Mode: $DEFAULT_MODE" -# ─── 5. Vintage choice ────────────────────────── -step "5/9 Vintage cars" +# 5. Vintage +step "5/8 Vintage cars" PREV_HAS_VINTAGE=0 if [[ -f "$DATA_DIR/manifest.json" ]]; then eval "$($PYTHON "$HELPER" status "$DATA_DIR")" PREV_HAS_VINTAGE=${HAS_VINTAGE:-0} fi - INCLUDE_VINTAGE=$WITH_VINTAGE if [[ $ASSUME_YES -eq 0 ]]; then echo "Include $VINTAGE_COUNT vintage cars (pre-2000)? Adds ~${VINTAGE_MB} MB." if [[ $PREV_HAS_VINTAGE -eq 1 ]]; then - echo "(Currently installed with vintage)" read -rp "Include vintage? [Y/n]: " ans [[ "${ans:-Y}" =~ ^[Nn] ]] && INCLUDE_VINTAGE=0 || INCLUDE_VINTAGE=1 else @@ -108,16 +105,34 @@ if [[ $ASSUME_YES -eq 0 ]]; then fi fi -if [[ $INCLUDE_VINTAGE -eq 1 ]]; then - ok "Will install: all $TOTAL_COUNT cars" +# 6. Pokemon mixing +step "6/8 Pokemon mixing" +POKEMON_MIX="false" +if [[ $HAS_POKEMON -eq 1 && $NO_MIX -eq 0 ]]; then + if [[ $ASSUME_YES -eq 1 ]]; then + POKEMON_MIX="true" + ok "Pokemon mixing enabled (--yes)" + else + echo "pokemon-colorscripts is installed." + read -rp "Mix pokemon 50/50 with cars at terminal startup? [Y/n]: " ans + if [[ "${ans:-Y}" =~ ^[Nn] ]]; then + POKEMON_MIX="false" + ok "Pokemon mixing disabled (only cars will show)" + else + POKEMON_MIX="true" + ok "Pokemon mixing enabled" + fi + fi +elif [[ $NO_MIX -eq 1 ]]; then + ok "Pokemon mixing disabled (--no-mix)" else - ok "Will install: $MODERN_COUNT modern cars" + ok "pokemon-colorscripts not installed — mixing not applicable" fi +echo " (toggle later: hypercar-colorscripts --set-mix on|off)" -# ─── 6. Install assets + config + binary ──────── -step "6/9 Install assets, config, binary" +# 7. Assets + config + binary +step "7/8 Install assets, config, binary" mkdir -p "$DATA_DIR" - if [[ $INCLUDE_VINTAGE -eq 1 ]]; then "$PYTHON" "$HELPER" install-all "$SRC_MANIFEST" "$SRC_ASSETS" "$DATA_DIR" || fail "install-all failed" else @@ -130,6 +145,7 @@ cat > "$CONFIG_FILE" </dev/null && \ - "$BIN_DIR/$BINARY" completion bash > "$BASH_COMP_DIR/$BINARY" 2>/dev/null && \ - { ok "bash: $BASH_COMP_DIR/$BINARY"; INSTALLED_COMPLETIONS+=("bash"); } || \ - warn "bash completion install skipped" - -mkdir -p "$ZSH_COMP_DIR" 2>/dev/null && \ - "$BIN_DIR/$BINARY" completion zsh > "$ZSH_COMP_DIR/_$BINARY" 2>/dev/null && \ - { ok "zsh: $ZSH_COMP_DIR/_$BINARY"; INSTALLED_COMPLETIONS+=("zsh"); } || \ - warn "zsh completion install skipped" - -mkdir -p "$FISH_COMP_DIR" 2>/dev/null && \ - "$BIN_DIR/$BINARY" completion fish > "$FISH_COMP_DIR/$BINARY.fish" 2>/dev/null && \ - { ok "fish: $FISH_COMP_DIR/$BINARY.fish"; INSTALLED_COMPLETIONS+=("fish"); } || \ - warn "fish completion install skipped" - -if [[ ${#INSTALLED_COMPLETIONS[@]} -gt 0 ]]; then - echo " Completions may not activate until you restart your shell." - echo " (bash needs 'bash-completion' package; zsh needs 'autoload -Uz compinit')" -fi - -# ─── 8. Shell rc detection ────────────────────── -step "8/9 Shell rc detection" +# 8. Shell rc +step "8/8 Shell startup" [[ $NO_SHELL -eq 1 ]] && { warn "Skipped (--no-shell)"; exit 0; } detect_login_shell() { @@ -192,11 +185,10 @@ declare -a RC_FILES=() case "$LOGIN_SHELL" in */zsh) PRIMARY_RC="$HOME/.zshrc" ;; */bash) PRIMARY_RC="$HOME/.bashrc" ;; - *) PRIMARY_RC="$HOME/.profile"; warn "Unknown shell, using ~/.profile" ;; + *) PRIMARY_RC="$HOME/.profile" ;; esac [[ -f "$PRIMARY_RC" ]] || touch "$PRIMARY_RC" RC_FILES+=("$PRIMARY_RC") -ok "Primary rc: $PRIMARY_RC" SECONDARY_RC="" if [[ "$PRIMARY_RC" == "$HOME/.zshrc" && -f "$HOME/.bashrc" ]]; then SECONDARY_RC="$HOME/.bashrc" @@ -204,7 +196,7 @@ elif [[ "$PRIMARY_RC" == "$HOME/.bashrc" && -f "$HOME/.zshrc" ]]; then SECONDAR fi if [[ -n "$SECONDARY_RC" ]]; then if [[ $ASSUME_YES -eq 1 ]]; then - RC_FILES+=("$SECONDARY_RC"); ok "Also installing in: $SECONDARY_RC (--yes)" + RC_FILES+=("$SECONDARY_RC") else read -rp "Also install in $SECONDARY_RC? [Y/n]: " ans [[ "${ans:-Y}" =~ ^[Yy] ]] && RC_FILES+=("$SECONDARY_RC") @@ -212,21 +204,16 @@ if [[ -n "$SECONDARY_RC" ]]; then fi if [[ $ASSUME_YES -eq 0 ]]; then - echo echo "Will modify: ${RC_FILES[*]}" read -rp "Proceed? [Y/n]: " ans [[ ! "${ans:-Y}" =~ ^[Yy]$ ]] && { warn "Skipped."; exit 0; } fi -# ─── 9. Apply rc modifications ────────────────── -step "9/9 Apply rc modifications" for RC in "${RC_FILES[@]}"; do - echo echo "Processing $RC" test -w "$RC" || fail "$RC not writable" - BACKUP="$RC.hypercar-backup-$(date +%Y%m%d-%H%M%S)" - cp "$RC" "$BACKUP" || fail "backup failed" + cp "$RC" "$BACKUP" sed -i.tmp '/# >>> hypercar-colorscripts PATH >>>/,/# <<< hypercar-colorscripts PATH <<>> hypercar-colorscripts >>>/,/# <<< hypercar-colorscripts <</dev/null | head -1) - [ -z "$__HC_MODE" ] && __HC_MODE="auto" + __HC_TMP=$(sed -n 's/.*"default_mode"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$__HC_CONF" 2>/dev/null | head -1) + [ -n "$__HC_TMP" ] && __HC_MODE="$__HC_TMP" + __HC_TMP=$(sed -n 's/.*"pokemon_mix"[[:space:]]*:[[:space:]]*\(true\|false\).*/\1/p' "$__HC_CONF" 2>/dev/null | head -1) + [ -n "$__HC_TMP" ] && __HC_MIX="$__HC_TMP" + unset __HC_TMP fi __HC_IMG_OK=0 case "${TERM:-}" in xterm-kitty*) __HC_IMG_OK=1 ;; esac -[ -n "${KITTY_WINDOW_ID:-}" ] && __HC_IMG_OK=1 -[ "${TERM_PROGRAM:-}" = "WezTerm" ] && __HC_IMG_OK=1 -[ -n "${GHOSTTY_RESOURCES_DIR:-}" ] && __HC_IMG_OK=1 +[ -n "${KITTY_WINDOW_ID:-}" ] && __HC_IMG_OK=1 +[ "${TERM_PROGRAM:-}" = "WezTerm" ] && __HC_IMG_OK=1 +[ "${TERM_PROGRAM:-}" = "iTerm.app" ] && __HC_IMG_OK=1 +[ -n "${GHOSTTY_RESOURCES_DIR:-}" ] && __HC_IMG_OK=1 [ "$__HC_MODE" = "ansi" ] && __HC_IMG_OK=0 -if [ $(($RANDOM % 2)) -eq 0 ] && command -v pokemon-colorscripts >/dev/null 2>&1; then +if [ "$__HC_MIX" = "true" ] && [ $(($RANDOM % 2)) -eq 0 ] && command -v pokemon-colorscripts >/dev/null 2>&1; then if command -v fastfetch >/dev/null 2>&1 && [ -f "$__HC_CFG" ]; then pokemon-colorscripts --no-title -s -r | \ fastfetch -c "$__HC_CFG" --logo-type file-raw --logo-height 10 --logo-width 5 --logo - @@ -293,23 +285,22 @@ else fi fi fi -unset __HC_DATA __HC_CFG __HC_CONF __HC_MODE __HC_IMG_OK __HC_IMG __HC_ANSI +unset __HC_DATA __HC_CFG __HC_CONF __HC_MODE __HC_MIX __HC_IMG_OK __HC_IMG __HC_ANSI # <<< hypercar-colorscripts <<< HC_BLOCK_END AFTER=$(fsize "$RC") - - [[ $AFTER -gt $BEFORE ]] || fail "$RC didn't grow ($BEFORE -> $AFTER bytes)" - grep -q '# >>> hypercar-colorscripts >>>' "$RC" || fail "$RC missing marker after append" + [[ $AFTER -gt $BEFORE ]] || fail "$RC didn't grow" + grep -q '# >>> hypercar-colorscripts >>>' "$RC" || fail "$RC missing marker" ok "Block appended ($((AFTER - BEFORE)) bytes)" - ok "Backup: $BACKUP" done echo echo -e "${GREEN}═══ Done ═══${NC}" -[[ $INCLUDE_VINTAGE -eq 1 ]] && echo "Installed: $TOTAL_COUNT cars (modern + vintage)" || echo "Installed: $MODERN_COUNT cars (modern only)" -echo "Mode: $DEFAULT_MODE" -[[ ${#INSTALLED_COMPLETIONS[@]} -gt 0 ]] && echo "Completions: ${INSTALLED_COMPLETIONS[*]}" +[[ $INCLUDE_VINTAGE -eq 1 ]] && echo "Installed: $TOTAL_COUNT cars (modern + vintage)" || echo "Installed: $MODERN_COUNT cars" +echo "Mode: $DEFAULT_MODE Pokemon mix: $POKEMON_MIX" +echo +echo "Toggle later:" +echo " hypercar-colorscripts --set-mode auto|ansi" +echo " hypercar-colorscripts --set-mix on|off" echo -echo "Run hypercar-colorscripts --doctor to verify the install." -echo "Vintage: ./install-vintage.sh | ./uninstall-vintage.sh" echo "Open a new terminal to see the changes." \ No newline at end of file diff --git a/cli/internal/cmd/completions.go b/cli/internal/cmd/completions.go deleted file mode 100644 index 0c829b7..0000000 --- a/cli/internal/cmd/completions.go +++ /dev/null @@ -1,125 +0,0 @@ -package cmd - -import ( - "os" - "path/filepath" - "sort" - "strings" - "sync" - - "hypercar-colorscripts/internal/config" - "hypercar-colorscripts/internal/manifest" - - "github.com/spf13/cobra" -) - -// The completion functions in this file are registered against the root -// command's flags in root.go via cobra.Command.RegisterFlagCompletionFunc. -// -// They enable tab completion in bash/zsh/fish: -// hypercar-colorscripts --car chi → chiron, bugatti_chiron, ... -// hypercar-colorscripts --brand Fe → Ferrari -// hypercar-colorscripts --decade 19 → 1960s, 1970s, ... - -var ( - completionMfOnce sync.Once - completionMf *manifest.Manifest -) - -// loadCompletionManifest lazily loads the manifest at completion time. -// Cached so tabbing multiple times doesn't re-parse. -func loadCompletionManifest() *manifest.Manifest { - completionMfOnce.Do(func() { - var dataDir string - if flagDataDir != "" { - dataDir = flagDataDir - } else if cfg, err := config.Load(); err == nil && cfg.DataDir != "" { - dataDir = cfg.DataDir - } else { - home, _ := os.UserHomeDir() - dataDir = filepath.Join(home, ".local/share/hypercar-colorscripts") - } - if mf, err := manifest.Load(filepath.Join(dataDir, "manifest.json")); err == nil { - completionMf = mf - } - }) - return completionMf -} - -// completeCars returns slug candidates + alias keys for --car / --info / --license. -func completeCars(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { - mf := loadCompletionManifest() - if mf == nil { - return nil, cobra.ShellCompDirectiveNoFileComp - } - prefix := strings.ToLower(toComplete) - var out []string - for slug := range mf.Cars { - if strings.HasPrefix(strings.ToLower(slug), prefix) { - out = append(out, slug) - } - } - for alias := range CommonAliases { - if strings.HasPrefix(alias, prefix) { - out = append(out, alias) - } - } - sort.Strings(out) - return out, cobra.ShellCompDirectiveNoFileComp -} - -func completeBrands(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return uniqueField(toComplete, func(c manifest.Car) string { return c.Brand }), cobra.ShellCompDirectiveNoFileComp -} - -func completeDecades(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return uniqueField(toComplete, func(c manifest.Car) string { return c.Decade }), cobra.ShellCompDirectiveNoFileComp -} - -func completeCategories(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return uniqueField(toComplete, func(c manifest.Car) string { return c.Category }), cobra.ShellCompDirectiveNoFileComp -} - -func completePowertrains(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return uniqueField(toComplete, func(c manifest.Car) string { return c.Powertrain }), cobra.ShellCompDirectiveNoFileComp -} - -func completeCountries(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return uniqueField(toComplete, func(c manifest.Car) string { return c.Country }), cobra.ShellCompDirectiveNoFileComp -} - -// completeSetMode has fixed values, no manifest needed. -func completeSetMode(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { - var out []string - for _, v := range []string{"auto", "ansi"} { - if strings.HasPrefix(v, strings.ToLower(toComplete)) { - out = append(out, v) - } - } - return out, cobra.ShellCompDirectiveNoFileComp -} - -// uniqueField collects unique non-empty values of one Car field, filtered by prefix. -func uniqueField(prefix string, get func(manifest.Car) string) []string { - mf := loadCompletionManifest() - if mf == nil { - return nil - } - seen := make(map[string]bool) - lower := strings.ToLower(prefix) - for _, c := range mf.Cars { - v := get(c) - if v == "" || seen[v] { - continue - } - if strings.HasPrefix(strings.ToLower(v), lower) { - seen[v] = true - } - } - out := make([]string, 0, len(seen)) - for v := range seen { - out = append(out, v) - } - sort.Strings(out) - return out -} diff --git a/cli/internal/cmd/doctor.go b/cli/internal/cmd/doctor.go index 9851ad4..d5bf455 100644 --- a/cli/internal/cmd/doctor.go +++ b/cli/internal/cmd/doctor.go @@ -43,6 +43,11 @@ func runDoctor() error { fmt.Printf(" %s✓%s config file: %s\n", cGreen, cReset, config.Path()) fmt.Printf(" data_dir: %s\n", cfg.DataDir) fmt.Printf(" default_mode: %s\n", cfg.DefaultMode) + if cfg.PokemonMix != nil { + fmt.Printf(" pokemon_mix: %v\n", *cfg.PokemonMix) + } else { + fmt.Printf(" pokemon_mix: unset (defaults to on)\n") + } } // 4. Manifest + assets @@ -80,7 +85,7 @@ func runDoctor() error { // docCheckPokemonMixing reports the ACTUAL mixing behavior the user will see, // combining: pokemon-colorscripts availability, shell block presence, and the -// binary's own with_pokemon setting. +// pokemon_mix config setting. func docCheckPokemonMixing(cfg config.Config) { // (a) is pokemon-colorscripts on PATH? pokePath, err := exec.LookPath("pokemon-colorscripts") @@ -97,44 +102,50 @@ func docCheckPokemonMixing(cfg config.Config) { } } + // (c) pokemon_mix setting in config (defaults true when field is absent) + mixEnabled := cfg.EffectiveMix() + // Report each piece if pokeInstalled { fmt.Printf(" %s✓%s pokemon-colorscripts installed at %s\n", cGreen, cReset, pokePath) } else { - fmt.Printf(" %s○%s pokemon-colorscripts not installed (no mixing possible)\n", cYellow, cReset) + fmt.Printf(" %s○%s pokemon-colorscripts not installed\n", cYellow, cReset) } if shellBlockFound { fmt.Printf(" %s✓%s shell startup block present\n", cGreen, cReset) } else { - fmt.Printf(" %s○%s shell startup block not found in ~/.zshrc or ~/.bashrc\n", cYellow, cReset) + fmt.Printf(" %s○%s shell startup block not found\n", cYellow, cReset) + } + + if mixEnabled { + fmt.Printf(" %s✓%s pokemon_mix in config: on\n", cGreen, cReset) + } else { + fmt.Printf(" %s○%s pokemon_mix in config: off (set by user)\n", cYellow, cReset) } // Effective behavior fmt.Println() switch { - case pokeInstalled && shellBlockFound: + case pokeInstalled && shellBlockFound && mixEnabled: fmt.Printf(" %sEffective behavior:%s new terminals show 50%% pokemon, 50%% car\n", cCyan, cReset) + case pokeInstalled && shellBlockFound && !mixEnabled: + fmt.Printf(" %sEffective behavior:%s new terminals always show a car (pokemon_mix=off)\n", cCyan, cReset) case !pokeInstalled && shellBlockFound: fmt.Printf(" %sEffective behavior:%s new terminals always show a car\n", cCyan, cReset) case pokeInstalled && !shellBlockFound: fmt.Printf(" %sEffective behavior:%s no startup output (block missing — run install.sh)\n", cCyan, cReset) default: - fmt.Printf(" %sEffective behavior:%s no startup output (nothing installed)\n", cCyan, cReset) + fmt.Printf(" %sEffective behavior:%s no startup output\n", cCyan, cReset) } - // Binary's own setting (only mention if non-default, since usually false) + // Legacy warning for with_pokemon (deprecated) if cfg.WithPokemon { - fmt.Printf("\n %s⚠%s config.json has with_pokemon=true\n", cYellow, cReset) - fmt.Printf(" This makes the binary ALSO roll pokemon on bare invocation.\n") - fmt.Printf(" Combined with the shell block above, you may see pokemon twice.\n") - fmt.Printf(" To disable: hypercar-colorscripts --set-with-pokemon false (or edit config.json)\n") + fmt.Printf("\n %s⚠%s legacy with_pokemon=true in config.json (this field is deprecated)\n", cYellow, cReset) + fmt.Printf(" Use --set-mix on/off to control pokemon mixing instead.\n") } } -// … the rest of doctor.go stays the same (docCheckCommand, docCheckTerminal, -// docCheckManifest, docCountFiles, docCheckShellRc) — no changes needed there. - func docCheckCommand(name string, required bool, desc string) { path, err := exec.LookPath(name) if err != nil { diff --git a/cli/internal/cmd/root.go b/cli/internal/cmd/root.go index e1b97ab..e12c067 100644 --- a/cli/internal/cmd/root.go +++ b/cli/internal/cmd/root.go @@ -23,6 +23,7 @@ import ( var ( flagDataDir string flagSetMode string // --set-mode flag + flagSetMix string // --set-mix flag flagCar string flagInfo string @@ -60,17 +61,32 @@ var ( func Execute(version string) error { root := &cobra.Command{ - Use: "hypercar-colorscripts", - Short: "Print a random hypercar in your terminal", + Use: "hypercar-colorscripts", + Short: "Print a random hypercar in your terminal", + Long: `hypercar-colorscripts displays a random hypercar in your terminal +with support for filtering by brand, decade, category, and more. + +Examples: + hypercar-colorscripts # Show a random car + hypercar-colorscripts --brand ferrari # Show a random Ferrari + hypercar-colorscripts --list-brands # List all brands + hypercar-colorscripts --info ferrari_f40 # Show details about a car + hypercar-colorscripts --set-mix off # Disable pokemon mixing + hypercar-colorscripts --doctor # Debug your setup`, Version: version, SilenceUsage: true, SilenceErrors: true, RunE: run, + // CHANGE 2: Disable Cobra's built-in 'completion' subcommand + CompletionOptions: cobra.CompletionOptions{ + DisableDefaultCmd: true, + }, } pf := root.PersistentFlags() pf.StringVar(&flagDataDir, "data-dir", "", "override manifest+assets directory") pf.StringVar(&flagSetMode, "set-mode", "", "update default_mode in config (auto|ansi) and exit") + pf.StringVar(&flagSetMix, "set-mix", "", "toggle pokemon mixing in shell startup (on|off) and exit") f := root.Flags() // Selection @@ -116,16 +132,7 @@ func Execute(version string) error { root.MarkFlagsMutuallyExclusive("auto", "ansi-only", "image-only") root.MarkFlagsMutuallyExclusive("with-pokemon", "no-pokemon", "pokemon-only") - // Tab-completion for flag values (bash/zsh/fish) - _ = root.RegisterFlagCompletionFunc("car", completeCars) - _ = root.RegisterFlagCompletionFunc("info", completeCars) - _ = root.RegisterFlagCompletionFunc("license", completeCars) - _ = root.RegisterFlagCompletionFunc("brand", completeBrands) - _ = root.RegisterFlagCompletionFunc("decade", completeDecades) - _ = root.RegisterFlagCompletionFunc("category", completeCategories) - _ = root.RegisterFlagCompletionFunc("powertrain", completePowertrains) - _ = root.RegisterFlagCompletionFunc("country", completeCountries) - _ = root.RegisterFlagCompletionFunc("set-mode", completeSetMode) + // CHANGE 1: All RegisterFlagCompletionFunc calls DELETED return root.Execute() } @@ -165,6 +172,38 @@ func run(cmd *cobra.Command, args []string) error { return nil } + // --set-mix short-circuits everything else + if flagSetMix != "" { + var mix bool + switch strings.ToLower(flagSetMix) { + case "on", "true", "yes", "1": + mix = true + case "off", "false", "no", "0": + mix = false + default: + return fmt.Errorf("invalid --set-mix value %q (must be 'on' or 'off')", flagSetMix) + } + cfg.WithPokemon = mix + if err := config.Save(cfg); err != nil { + return fmt.Errorf("saving config: %w", err) + } + state := "on" + if !mix { + state = "off" + } + fmt.Printf("pokemon_mix set to: %s\n", state) + if mix { + if _, err := os.Stat("/usr/bin/pokemon-colorscripts"); err != nil { + if _, err := exec.LookPath("pokemon-colorscripts"); err != nil { + fmt.Println("Note: pokemon-colorscripts is not installed, so mixing won't happen even with --set-mix on.") + fmt.Println("Install it from https://gitlab.com/phoneybadger/pokemon-colorscripts") + } + } + } + fmt.Println("Open a new terminal to see the change take effect at startup.") + return nil + } + // --pokemon-only short-circuits everything else if flagPokemonOnly { if !pokemon.Available() { diff --git a/cli/internal/config/config.go b/cli/internal/config/config.go index 5882867..818d466 100644 --- a/cli/internal/config/config.go +++ b/cli/internal/config/config.go @@ -16,14 +16,32 @@ type Config struct { // DefaultMode: "auto" (image when chafa exists) or "ansi" (always cat). DefaultMode string `json:"default_mode"` - // WithPokemon: if true, sometimes shell out to pokemon-colorscripts. - WithPokemon bool `json:"with_pokemon"` + // PokemonMix: controls shell-level pokemon mixing. + // nil = "not set" (defaults to true for backward compatibility) + // true/false = explicitly set by user via --set-mix + PokemonMix *bool `json:"pokemon_mix,omitempty"` + + // WithPokemon: deprecated — binary internal mixing (unused). + // Kept for backward compatibility with existing configs. + WithPokemon bool `json:"with_pokemon"` + + // PokemonRatio: chance of picking pokemon when mixing is enabled. PokemonRatio float64 `json:"pokemon_ratio"` // ShowAttribution prints CC credit line below the art. ShowAttribution bool `json:"show_attribution"` } +// EffectiveMix returns whether the shell block should mix pokemon. +// Defaults to true when the field is absent from config.json (preserves +// pre-v0.3.1 behavior for existing installs). +func (c Config) EffectiveMix() bool { + if c.PokemonMix == nil { + return true + } + return *c.PokemonMix +} + // Defaults returns sensible defaults so the binary works even // without a config file (important when called from .zshrc — must // never prompt or block). @@ -32,6 +50,7 @@ func Defaults() Config { return Config{ DataDir: filepath.Join(home, ".local", "share", "hypercar-colorscripts"), DefaultMode: "auto", + PokemonMix: nil, // nil = defaults to true in EffectiveMix() WithPokemon: false, PokemonRatio: 0.5, ShowAttribution: false, diff --git a/cli/uninstall.sh b/cli/uninstall.sh index 63386ba..fc7ecff 100755 --- a/cli/uninstall.sh +++ b/cli/uninstall.sh @@ -1,12 +1,9 @@ #!/usr/bin/env bash -# uninstall.sh — remove hypercar-colorscripts cleanly (v0.3) +# uninstall.sh — remove hypercar-colorscripts cleanly (v0.3.2) # -# Fixes since Phase 2: -# - Loop-start grep now also matches 'hypercar-installer-disabled', -# so files where the block was already stripped but a disabled -# pokemon line remains still get processed. -# - Restore regex now handles leading whitespace. -# - Removes shell completion files installed by install.sh. +# Removed: completion file cleanup (shell completions feature dropped). +# Best-effort deletes any leftover completion files from previous versions +# so nobody ends up with stale files after upgrading. # # Options: # 1) Only hypercar-colorscripts (default) @@ -23,11 +20,6 @@ CONFIG_DIR="$HOME/.config/hypercar-colorscripts" BIN_USER="$HOME/.local/bin/hypercar-colorscripts" BIN_SYS="/usr/local/bin/hypercar-colorscripts" -# Completion paths (must match install.sh) -BASH_COMP="$HOME/.local/share/bash-completion/completions/hypercar-colorscripts" -ZSH_COMP="$HOME/.local/share/zsh/site-functions/_hypercar-colorscripts" -FISH_COMP="$HOME/.config/fish/completions/hypercar-colorscripts.fish" - echo -e "${BLUE}═══ hypercar-colorscripts uninstaller ═══${NC}" echo echo "What to uninstall?" @@ -42,7 +34,6 @@ echo " - $BIN_USER (if exists)" echo " - $BIN_SYS (if exists)" echo " - $DATA_DIR/" echo " - $CONFIG_DIR/" -echo " - hypercar completion files (bash/zsh/fish)" echo " - hypercar blocks from ~/.zshrc AND ~/.bashrc (backups made)" [[ "$choice" == "2" ]] && echo " - pokemon-colorscripts" read -rp "Continue? [y/N]: " confirm @@ -51,8 +42,6 @@ read -rp "Continue? [y/N]: " confirm # ─── Process each rc file ────────────────────── for RC in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.profile"; do [[ -f "$RC" ]] || continue - # BUGFIX: also match the disabled-marker so we don't skip files where - # the block was already stripped manually. if ! grep -qE "hypercar-colorscripts|hypercar-installer-disabled" "$RC" 2>/dev/null; then continue fi @@ -66,14 +55,12 @@ for RC in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.profile"; do sed -i.tmp '/# >>> hypercar-colorscripts >>>/,/# <<< hypercar-colorscripts <</dev/null || true rm -f "$RC.tmp" @@ -91,11 +78,15 @@ fi [[ -d "$DATA_DIR" ]] && rm -rf "$DATA_DIR" && ok "Removed $DATA_DIR" [[ -d "$CONFIG_DIR" ]] && rm -rf "$CONFIG_DIR" && ok "Removed $CONFIG_DIR" -# ─── Completion files (NEW) ──────────────────── -for f in "$BASH_COMP" "$ZSH_COMP" "$FISH_COMP"; do +# ─── Cleanup stale completion files from prior versions ──────── +# The completions feature was removed in v0.3.2. If a previous install +# left files behind, clean them up silently. +for f in "$HOME/.local/share/bash-completion/completions/hypercar-colorscripts" \ + "$HOME/.local/share/zsh/site-functions/_hypercar-colorscripts" \ + "$HOME/.config/fish/completions/hypercar-colorscripts.fish"; do if [[ -f "$f" ]]; then rm -f "$f" - ok "Removed completion: $f" + ok "Removed stale completion file: $f" fi done From 3050c6b14fcec6d7f129acad0dabfca1c4e2e0fd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Jul 2026 10:45:57 +0000 Subject: [PATCH 3/3] fix(shellcheck): resolve CI warnings in install and uninstall scripts --- cli/install.sh | 9 ++++++--- cli/uninstall.sh | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/cli/install.sh b/cli/install.sh index 261515f..a45e561 100755 --- a/cli/install.sh +++ b/cli/install.sh @@ -225,15 +225,18 @@ for RC in "${RC_FILES[@]}"; do ok "Commented existing pokemon line" fi - if ! grep -qE '(\$HOME|\$\{HOME\}|~)/\.local/bin' "$RC"; then + if ! grep -qE "(\\$HOME|\\$\\{HOME\\}|~)/\\.local/bin" "$RC"; then TMP="$(mktemp)" { printf '%s\n' '# >>> hypercar-colorscripts PATH >>>' - printf '%s\n' 'export PATH="$HOME/.local/bin:$PATH"' + printf '%s\n' "export PATH=\"\$HOME/.local/bin:\$PATH\"" printf '%s\n' '# <<< hypercar-colorscripts PATH <<<' printf '\n' cat "$RC" - } > "$TMP" && mv "$TMP" "$RC" || fail "PATH write failed" + } > "$TMP" + if ! mv "$TMP" "$RC"; then + fail "PATH write failed" + fi ok "Added PATH export" fi diff --git a/cli/uninstall.sh b/cli/uninstall.sh index fc7ecff..0d552fe 100755 --- a/cli/uninstall.sh +++ b/cli/uninstall.sh @@ -11,7 +11,7 @@ set -uo pipefail -RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m' +GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m' ok() { echo -e "${GREEN}✓${NC} $*"; } warn() { echo -e "${YELLOW}⚠${NC} $*"; }