tui-piechart 1.2.0

A customizable pie chart widget for Ratatui TUI applications
Documentation
#!/usr/bin/env nu
# ──────────────────────────────────────────────────────────────────────────────
# TokenBurn — did the dependency update REALLY upgrade something?
# ──────────────────────────────────────────────────────────────────────────────
# The nightly job runs `cargo update` and then decides whether to bump, build and
# publish a new patch version. A lockfile that merely *changed* is not enough: this
# compares the package versions before and after and releases only when
#
#   * at least one package moved to a HIGHER version, and
#   * nothing moved to a LOWER one (a downgrade means a yanked release or a
#     resolver surprise; it is never published, a human should look).
#
# Added / removed packages and checksum-only changes are reported but never trigger a
# release on their own.
#
# Inputs are two files, either `cargo tree --prefix none --format '{p}'` output
# (lines like `serde v1.0.228`) or Cargo.lock files.
#
# Usage:
#   nu scripts/ci/lock_changes.nu before.txt after.txt [--output "$GITHUB_OUTPUT"]
#
# Writes `changed=true|false` (plus `upgraded=N` and `downgraded=N`) to --output, and
# prints a human summary either way.
# ──────────────────────────────────────────────────────────────────────────────

# Split a semantic version into numeric parts and the pre-release tag.
def split_version [v: string]: nothing -> record {
    let core = ($v | str trim --left --char "v" | split row "+" | first)
    let parts = ($core | split row "-")
    let nums = ($parts | first | split row "." | each {|p|
        try { $p | into int } catch { 0 }
    })
    { nums: $nums, pre: ($parts | skip 1 | str join "-") }
}

# Compare two versions: -1 when a < b, 0 when equal, 1 when a > b. Build metadata
# (`+x`) is ignored and a pre-release (`1.0.0-rc.1`) sorts before its release.
export def cmp_version [a: string, b: string]: nothing -> int {
    let x = (split_version $a)
    let y = (split_version $b)
    let len = ([($x.nums | length) ($y.nums | length)] | math max)
    for i in 0..<$len {
        let p = ($x.nums | get -o $i | default 0)
        let q = ($y.nums | get -o $i | default 0)
        if $p < $q { return (-1) }
        if $p > $q { return 1 }
    }
    if $x.pre == $y.pre { return 0 }
    if ($x.pre | is-empty) { return 1 }
    if ($y.pre | is-empty) { return (-1) }
    if $x.pre < $y.pre { -1 } else { 1 }
}

# `name vX.Y.Z` lines → [{name, version}], deduplicated. Annotations such as `(*)` and
# `(proc-macro)` are ignored.
export def parse_tree [text: string]: nothing -> list {
    $text
    | lines
    | each {|l| $l | str trim }
    | where {|l| $l != "" }
    | each {|l|
        let w = ($l | split row " " | where {|x| $x != "" })
        { name: ($w | get 0), version: ($w | get -o 1 | default "" | str trim --left --char "v") }
    }
    | where {|p| $p.version != "" }
    | uniq
}

# Read either kind of input file.
export def read_packages [path: string]: nothing -> list {
    if ($path | str ends-with ".lock") {
        open $path --raw | from toml | get package | select name version | uniq
    } else {
        parse_tree (open $path --raw)
    }
}

# Sort versions ascending.
def sorted_versions [versions: list]: nothing -> list {
    $versions | sort-by --custom {|a, b| (cmp_version $a $b) < 0 }
}

# What changed between two package lists.
export def classify [before: list, after: list]: nothing -> record {
    let names = ($before | append $after | get name | uniq | sort)
    mut upgraded = []
    mut downgraded = []
    mut added = []
    mut removed = []
    for name in $names {
        let b = ($before | where name == $name | get version | uniq)
        let a = ($after | where name == $name | get version | uniq)
        # Only versions that appeared / disappeared matter, so a crate that exists
        # twice (objc2 0.5 and 0.6) is compared version by version.
        let gone = (sorted_versions ($b | where {|v| $v not-in $a }))
        let came = (sorted_versions ($a | where {|v| $v not-in $b }))
        let pairs = ([($gone | length) ($came | length)] | math min)
        for i in 0..<$pairs {
            let from = ($gone | get $i)
            let to = ($came | get $i)
            let row = { name: $name, from: $from, to: $to }
            if (cmp_version $to $from) > 0 {
                $upgraded = ($upgraded | append $row)
            } else {
                $downgraded = ($downgraded | append $row)
            }
        }
        for v in ($came | skip $pairs) { $added = ($added | append { name: $name, version: $v }) }
        for v in ($gone | skip $pairs) { $removed = ($removed | append { name: $name, version: $v }) }
    }
    { upgraded: $upgraded, downgraded: $downgraded, added: $added, removed: $removed }
}

# The decision: release only for real upgrades and never alongside a downgrade.
export def should_release [changes: record]: nothing -> bool {
    ($changes.upgraded | length) > 0 and ($changes.downgraded | is-empty)
}

def main [
    before: string,        # packages before `cargo update`
    after: string,         # packages after
    --output: string = "", # file to append `changed=…` to (e.g. $GITHUB_OUTPUT)
] {
    let changes = (classify (read_packages $before) (read_packages $after))
    let release = (should_release $changes)

    for u in $changes.upgraded { print $"  ↑ ($u.name) ($u.from) → ($u.to)" }
    for d in $changes.downgraded { print $"  ↓ DOWNGRADE ($d.name) ($d.from) → ($d.to)" }
    for a in $changes.added { print $"  + ($a.name) ($a.version)" }
    for r in $changes.removed { print $"  - ($r.name) ($r.version)" }

    if $release {
        print $"✅ ($changes.upgraded | length) package\(s\) upgraded, no downgrades: releasing."
    } else if not ($changes.downgraded | is-empty) {
        print $"⚠️  ($changes.downgraded | length) package\(s\) would be DOWNGRADED: not releasing. Look at it by hand."
    } else {
        print "No package moved to a higher version: not releasing."
    }

    if $output != "" {
        let lines = [
            $"changed=($release)"
            $"upgraded=($changes.upgraded | length)"
            $"downgraded=($changes.downgraded | length)"
        ] | str join "\n"
        $"($lines)\n" | save --append $output
    }
}