tui-checkbox 0.5.3

A customizable checkbox widget for Ratatui TUI applications
Documentation
#!/usr/bin/env nu
# ──────────────────────────────────────────────────────────────────────────────
# tui-checkbox — Compute next patch version (downgrade-safe)
# ──────────────────────────────────────────────────────────────────────────────
# Usage:
#   nu scripts/next_patch_version.nu <current_version>            # simple bump
#   nu scripts/next_patch_version.nu <current_version> --safe     # guarded bump
#
# The `--safe` mode is used by the nightly dependency-upgrade workflow. It
# guards against ever producing a version that would:
#   • be lower than or equal to the highest existing `vX.Y.Z` git tag, and
#   • collide with a version already published on crates.io.
#
# This matters because the nightly job always bumps from whatever is in
# Cargo.toml on `main`. If a manual release/tag ever gets ahead of that (or a
# previous nightly run partially failed after tagging but before crates.io
# publish), a naive "+1 patch" could produce a tag/version that is behind —
# effectively a downgrade — or a duplicate that fails to publish.
# ──────────────────────────────────────────────────────────────────────────────

use ci/publish.nu [is-published]

# Parse a MAJOR.MINOR.PATCH (optionally with a `-prerelease` suffix, which is
# dropped) semver string into its numeric components.
export def parse-semver [version: string]: nothing -> record<major: int, minor: int, patch: int> {
    let pattern = '^(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-.*)?$'
    let parsed = ($version | parse --regex $pattern)

    if ($parsed | is-empty) {
        error make { msg: $"'($version)' is not a valid semantic version" }
    }

    let row = ($parsed | first)
    {
        major: ($row.major | into int),
        minor: ($row.minor | into int),
        patch: ($row.patch | into int),
    }
}

# Given a MAJOR.MINOR.PATCH version string, return the next patch version
# (pre-release suffixes are dropped).
export def next-patch [version: string]: nothing -> string {
    let v = (parse-semver $version)
    $"($v.major).($v.minor).($v.patch + 1)"
}

# Compare two semver strings. Returns -1, 0, or 1 (like a normal comparator).
export def compare-semver [a: string, b: string]: nothing -> int {
    let va = (parse-semver $a)
    let vb = (parse-semver $b)

    if $va.major != $vb.major {
        if $va.major < $vb.major { -1 } else { 1 }
    } else if $va.minor != $vb.minor {
        if $va.minor < $vb.minor { -1 } else { 1 }
    } else if $va.patch != $vb.patch {
        if $va.patch < $vb.patch { -1 } else { 1 }
    } else {
        0
    }
}

# Return the highest `vX.Y.Z` version among the given git tags (as bare
# "X.Y.Z" strings, without the "v" prefix). Tags that don't match the
# expected shape are ignored. Returns null if there are no matching tags.
export def highest-tag-version [tags: list<string>]: nothing -> any {
    let versions = (
        $tags
        | where { |t| $t | str starts-with "v" }
        | each { |t| $t | str substring 1.. }
        | where { |v| (try { parse-semver $v; true } catch { false }) }
    )

    if ($versions | is-empty) {
        null
    } else {
        $versions | reduce { |it, acc| if (compare-semver $it $acc) > 0 { $it } else { $acc } }
    }
}

# Compute a downgrade-safe next version:
#   1. Start from the higher of `current_version` and the highest existing
#      `vX.Y.Z` git tag (so a stale Cargo.toml on `main` never regresses a
#      version that was already tagged/released elsewhere).
#   2. Bump the patch component.
#   3. If that version is already published on crates.io, keep bumping the
#      patch until an unpublished version is found.
export def safe-next-version [
    current_version: string,
    crate_name: string,
    tags: list<string>,
]: nothing -> string {
    let highest_tag = (highest-tag-version $tags)

    let base = if $highest_tag == null {
        $current_version
    } else if (compare-semver $highest_tag $current_version) > 0 {
        $highest_tag
    } else {
        $current_version
    }

    mut candidate = (next-patch $base)
    while (is-published $crate_name $candidate) {
        $candidate = (next-patch $candidate)
    }
    $candidate
}

def main [
    current_version: string,  # Current version from Cargo.toml
    --safe,                   # Guard against downgrades / crates.io collisions
    --crate-name: string = "", # Crate name (defaults to Cargo.toml package.name)
] {
    if $safe {
        let name = if $crate_name == "" {
            (open Cargo.toml | get package.name)
        } else {
            $crate_name
        }
        let tags = (try { git tag --list "v*" | lines } catch { [] })
        print (safe-next-version $current_version $name $tags)
    } else {
        print (next-patch $current_version)
    }
}