tui-piechart 0.3.3

A customizable pie chart widget for Ratatui TUI applications
Documentation
#!/usr/bin/env nu
# Automated version bump script for tui-piechart
# Usage: nu scripts/bump_version.nu [--yes] <new_version>
# Example: nu scripts/bump_version.nu 0.3.2
#          nu scripts/bump_version.nu --yes 0.3.2   # skip confirmation
#
# Updates:
#   • [package] version in Cargo.toml
#   • Version badge in README.md
#   • Cargo.lock (cargo update -p tui-piechart)
#   • CHANGELOG.md (via git-cliff)
# Then commits and tags locally.  Use `just push-release-all` to push.

# ── Helpers ───────────────────────────────────────────────────────────────────

# Read the current [package] version from a Cargo.toml string.
export def read_package_version [content: string] {
    $content
    | lines
    | reduce --fold { in_pkg: false, version: "" } { |line, acc|
        let new_in_pkg = if ($line =~ '^\[package\]') {
            true
        } else if ($acc.in_pkg and ($line =~ '^\[')) {
            false
        } else {
            $acc.in_pkg
        }

        let new_version = if ($acc.in_pkg and ($line =~ '^version\s*=\s*"[^"]*"')) {
            $line
            | parse --regex 'version\s*=\s*"(?P<v>[^"]+)"'
            | get v
            | first
        } else {
            $acc.version
        }

        { in_pkg: $new_in_pkg, version: $new_version }
    }
    | get version
}

# Replace the version line inside [package] only.
export def update_package_version [lines: list<string>, new_version: string] {
    let result = $lines | reduce --fold { in_pkg: false, lines: [] } { |line, acc|
        let new_in_pkg = if ($line =~ '^\[package\]') {
            true
        } else if ($acc.in_pkg and ($line =~ '^\[')) {
            false
        } else {
            $acc.in_pkg
        }

        let new_line = if ($acc.in_pkg and ($line =~ '^version\s*=\s*"[^"]*"')) {
            $'version = "($new_version)"'
        } else {
            $line
        }

        { in_pkg: $new_in_pkg, lines: ($acc.lines | append $new_line) }
    }
    $result.lines
}

# ── Main ──────────────────────────────────────────────────────────────────────

def main [
    new_version: string,  # New version in X.Y.Z format
    --yes (-y),           # Skip confirmation prompt (non-interactive)
] {
    let red    = (ansi red)
    let green  = (ansi green)
    let yellow = (ansi yellow)
    let cyan   = (ansi cyan)
    let reset  = (ansi reset)

    # ── Validate version format ───────────────────────────────────────────────
    if not ($new_version =~ '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$') {
        error make { msg: $"($red)Error: Invalid version format($reset)
Version must be in format: X.Y.Z or X.Y.Z-suffix \(e.g., 0.3.2 or 1.0.0-beta.1\)" }
    }

    print $"($cyan)════════════════════════════════════════($reset)"
    print $"($cyan)  tui-piechart Version Bump($reset)"
    print $"($cyan)════════════════════════════════════════($reset)"
    print ""

    # ── Read current version from [package] ───────────────────────────────────
    let cargo_content = (open Cargo.toml --raw)
    let cargo_lines   = ($cargo_content | lines)

    let current_version = (read_package_version $cargo_content)

    if ($current_version | is-empty) {
        error make { msg: $"($red)Error: Could not read [package] version from Cargo.toml($reset)" }
    }

    print $"Current version: ($yellow)($current_version)($reset)"
    print $"New version:     ($green)($new_version)($reset)"
    print ""

    # ── Guard: already at requested version ──────────────────────────────────
    if $current_version == $new_version {
        error make { msg: $"($red)Error: Cargo.toml is already at version ($new_version).($reset)
($yellow)  Bump to the next version, or delete the tag if you need to re-release:($reset)
      git tag -d v($new_version) && git push origin :refs/tags/v($new_version)" }
    }

    # ── Guard: tag already exists locally ────────────────────────────────────
    let tag_name = $"v($new_version)"
    let existing_tags = (git tag | lines)
    if ($existing_tags | any { |t| $t == $tag_name }) {
        error make { msg: $"($red)Error: Tag ($tag_name) already exists locally.($reset)
($yellow)  Delete it first if you really want to recreate it:($reset)
      git tag -d ($tag_name)" }
    }

    # ── Confirmation ─────────────────────────────────────────────────────────
    if $yes {
        print $"($cyan)Running non-interactively \(--yes passed\).($reset)"
    } else {
        let reply = (input "Continue with version bump? (y/n) ")
        if not ($reply =~ '^[Yy]') {
            print $"($yellow)Aborted($reset)"
            return
        }
    }

    print ""

    # ── Step 1: Update [package] version ──────────────────────────────────────
    print $"($cyan)Step 1/6: Updating [package] version in Cargo.toml...($reset)"

    let final_lines = (update_package_version $cargo_lines $new_version)
    $final_lines | str join "\n" | save --force Cargo.toml

    # Verify version took effect
    let verify_version = (read_package_version (open Cargo.toml --raw))
    if $verify_version != $new_version {
        error make { msg: $"($red)Failed to update [package] version \(got ($verify_version)\).($reset)
($yellow)  Check the version line format in Cargo.toml and update manually.($reset)" }
    }
    print $"($green)✓ Cargo.toml updated \(($current_version) → ($new_version)\)($reset)"

    # ── Step 2: Update README.md badges + installation snippet ───────────────
    print ""
    print $"($cyan)Step 2/6: Updating README.md...($reset)"

    if ("README.md" | path exists) {
        let readme = (open README.md --raw)

        # Update version badge
        let readme = if ($readme =~ 'version-[0-9]+\.[0-9]+\.[0-9]+-blue') {
            let updated = (
                $readme
                | str replace --all --regex 'version-[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+)?-blue' $"version-($new_version)-blue"
            )
            print $"($green)✓ README.md badges updated($reset)"
            $updated
        } else {
            print $"($yellow)⚠ No version badge found in README.md — skipping($reset)"
            $readme
        }

        # Update installation snippet: tui-piechart = "X.Y.Z"
        let readme = if ($readme =~ 'tui-piechart\s*=\s*"[0-9]+\.[0-9]+\.[0-9]+"') {
            let updated = (
                $readme
                | str replace --all --regex 'tui-piechart\s*=\s*"[0-9]+\.[0-9]+\.[0-9]+"' $'tui-piechart = "($new_version)"'
            )
            print $"($green)✓ README.md installation snippet updated($reset)"
            $updated
        } else {
            print $"($yellow)⚠ No tui-piechart version pin found in README.md — skipping($reset)"
            $readme
        }

        # Update ratatui version from Cargo.toml
        let ratatui_ver = (
            $cargo_content
            | lines
            | where { |line| $line =~ 'ratatui\s*=' }
            | first
            | parse --regex 'ratatui\s*=\s*\{?\s*version\s*=\s*"(?P<v>[^"]+)"'
            | get v
            | first
        )
        let readme = if (not ($ratatui_ver | is-empty)) and ($readme =~ 'ratatui\s*=\s*"[0-9]+\.[0-9]+"') {
            let updated = (
                $readme
                | str replace --all --regex 'ratatui\s*=\s*"[0-9]+\.[0-9]+"' $'ratatui = "($ratatui_ver)"'
            )
            print $"($green)✓ README.md ratatui version updated to ($ratatui_ver)($reset)"
            $updated
        } else {
            $readme
        }

        $readme | save --force README.md
    } else {
        print $"($yellow)⚠ README.md not found — skipping($reset)"
    }

    # ── Step 3: Update Cargo.lock ─────────────────────────────────────────────
    print ""
    print $"($cyan)Step 3/6: Updating Cargo.lock...($reset)"
    run-external "cargo" "update" "-p" "tui-piechart"
    print $"($green)✓ Cargo.lock updated($reset)"

    # ── Step 4: cargo fmt ─────────────────────────────────────────────────────
    print ""
    print $"($cyan)Step 4/6: Running cargo fmt...($reset)"
    run-external "cargo" "fmt"
    print $"($green)✓ Code formatted($reset)"

    # ── Step 5: cargo clippy ──────────────────────────────────────────────────
    print ""
    print $"($cyan)Step 5/6: Running cargo clippy...($reset)"
    let clippy = (do {
        run-external "cargo" "clippy" "--" "-D" "warnings"
    } | complete)
    if $clippy.exit_code != 0 {
        error make { msg: $"($red)✗ Clippy found issues. Please fix them before continuing.($reset)" }
    }
    print $"($green)✓ Clippy passed($reset)"

    # ── Step 6: cargo test + CHANGELOG + commit + tag ─────────────────────────
    print ""
    print $"($cyan)Step 6/6: Running tests, generating changelog, committing...($reset)"

    let tests = (do {
        run-external "cargo" "test" "--locked" "--all-features" "--all-targets"
    } | complete)
    if $tests.exit_code != 0 {
        error make { msg: $"($red)✗ Tests failed. Please fix them before continuing.($reset)" }
    }
    print $"($green)✓ All tests passed($reset)"

    if (which git-cliff | length) > 0 {
        run-external "git-cliff" "--tag" $tag_name "-o" "CHANGELOG.md"
        print $"($green)✓ CHANGELOG.md generated($reset)"
    } else {
        print $"($yellow)⚠ git-cliff not found — skipping changelog generation($reset)"
        print $"($yellow)  Install it with: cargo install git-cliff($reset)"
    }

    # Stage changed files
    run-external "git" "add" "Cargo.toml" "Cargo.lock" "README.md" "CHANGELOG.md"
    let commit_msg = $"chore: bump version to ($new_version)

- Update version in Cargo.toml to ($new_version)
- Update version badge in README.md
- Regenerate Cargo.lock
- Generate updated CHANGELOG.md"
    run-external "git" "commit" "-m" $commit_msg

    let tag_msg = $"Release ($tag_name)

Includes all changes documented in CHANGELOG.md for version ($new_version)."
    run-external "git" "tag" "-a" $tag_name "-m" $tag_msg
    print $"($green)✓ Tag ($tag_name) created($reset)"

    # ── Summary ───────────────────────────────────────────────────────────────
    print ""
    print $"($cyan)════════════════════════════════════════($reset)"
    print $"($green)✓ Version bump complete! 🚀($reset)"
    print $"($cyan)════════════════════════════════════════($reset)"
    print ""
    print $"($yellow)Next steps:($reset)"
    print  "  1. Review the changes:"
    print $"     ($cyan)git show($reset)"
    print  ""
    print  "  2. Push to GitHub (triggers the release workflow):"
    print $"     ($cyan)git push --follow-tags origin main($reset)"
    print  ""
    print  "  3. Push to Gitea as well:"
    print $"     ($cyan)git push --follow-tags gitea main($reset)"
    print  ""
    print  "  4. Or use the just shortcuts:"
    print $"     ($cyan)just push-release-all($reset)   # push branch + tags to all remotes"
    print ""
}