aboutsummaryrefslogtreecommitdiffstats
path: root/nvtable
diff options
context:
space:
mode:
Diffstat (limited to 'nvtable')
-rwxr-xr-xnvtable272
1 files changed, 272 insertions, 0 deletions
diff --git a/nvtable b/nvtable
new file mode 100755
index 0000000..1eb008a
--- /dev/null
+++ b/nvtable
@@ -0,0 +1,272 @@
+#!/bin/bash
+# nvtable - compare upstream (nvchecker) vs installed vs shipped-in-repo versions.
+#
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# Three version sources per tracked name:
+# upstream - nvchecker new_ver.json
+# installed - /var/log/packages, then appimage-updater state, then flatpak
+# repo - VERSION= in <repo>/<pkg>/<pkg>.info
+#
+# When installed == upstream the row is caught up, and nvtake records it so
+# old_ver tracks the system. See take_caught_up() for the locking.
+
+set -uo pipefail
+
+CONF="${NVTABLE_CONF:-$HOME/.config/nvchecker/nvchecker.toml}"
+NEWVER="${NVTABLE_NEWVER:-$HOME/.config/nvchecker/new_ver.json}"
+AI_STATE="${XDG_STATE_HOME:-$HOME/.local/state}/appimage-updater/installed.json"
+LOCK="${TMPDIR:-/tmp}/nvtable-$UID.lock"
+
+REPOS=(
+ "sbo:$HOME/Programming/GIT/sbo-slackbuilds"
+ "my:$HOME/Programming/GIT/GITHUB/my-slackbuilds"
+ "sps:$HOME/Programming/GIT/GITHUB/Slackware-Pentesting-Suite"
+)
+
+# Stanza name -> installed package name(s), space separated for one-to-many.
+# Only for names that differ; everything else joins directly.
+declare -A ALIAS=(
+ [kvantum]="kvantum-qt5 kvantum-qt6"
+ [metasploit-framework]="metasploit-framework-bin"
+)
+
+# Stanza name -> SlackBuild directory name, where the repo dir differs from the
+# stanza (a -bin build tracked under its plain upstream name).
+declare -A REPO_ALIAS=(
+ [kitty]="kitty-bin"
+ [claude-code]="claude-code-bin"
+ [kvantum]="kvantum-qt6"
+ [metasploit-framework]="metasploit-framework-bin"
+)
+
+SHOW_ALL=0; NO_TAKE=0; NO_COLOR=0; NO_EMOJI=0
+for a in "$@"; do
+ case "$a" in
+ -a|--all) SHOW_ALL=1 ;;
+ -n|--no-take) NO_TAKE=1 ;;
+ -C|--no-color) NO_COLOR=1 ;;
+ -E|--no-emoji) NO_EMOJI=1 ;;
+ -h|--help)
+ cat <<EOF
+usage: nvtable [-a] [-n] [-C] [-E]
+ -a, --all show every tracked package, not just those needing action
+ -n, --no-take do not run nvtake on caught-up packages
+ -C, --no-color plain output
+ -E, --no-emoji ASCII status markers instead of emoji
+EOF
+ exit 0 ;;
+ *) echo "nvtable: unknown option $a" >&2; exit 2 ;;
+ esac
+done
+
+[[ -t 1 ]] || NO_COLOR=1
+if (( NO_COLOR )); then
+ R=""; B=""; DIM=""; RED=""; GRN=""; YEL=""; CYA=""; WHT=""
+else
+ R=$'\e[0m'; B=$'\e[1m'; DIM=$'\e[2m'; RED=$'\e[1;31m'
+ GRN=$'\e[1;32m'; YEL=$'\e[1;33m'; CYA=$'\e[1;36m'; WHT=$'\e[1;37m'
+fi
+
+# Status markers. Emoji are double-width, so each ASCII fallback is padded to
+# two columns to keep the name column aligned either way.
+if (( NO_EMOJI )); then
+ E_UP="!!" E_REPO="~ " E_OK="ok" E_NA="- "
+else
+ E_UP="🔴" E_REPO="🟡" E_OK="🟢" E_NA="⚪"
+fi
+
+command -v jq >/dev/null || { echo "nvtable: jq required" >&2; exit 1; }
+[[ -r $NEWVER ]] || { echo "nvtable: no $NEWVER (run nvchecker first)" >&2; exit 1; }
+
+# --- installed versions -----------------------------------------------------
+# Package files are PRGNAM-VERSION-ARCH-BUILD, but VERSION may itself contain a
+# dash (solvespace-3.1-659215d), so the PRGNAM/VERSION boundary cannot be found
+# by counting dashes. Drop ARCH-BUILD from the right, then keep the whole
+# remainder; lookup_installed() anchors on the name it is searching for.
+declare -A PKGREST
+while IFS= read -r f; do
+ rest=${f%-*} # drop BUILD
+ rest=${rest%-*} # drop ARCH -> "PRGNAM-VERSION"
+ [[ -n $rest ]] && PKGREST[$rest]=1
+done < <(/bin/ls -1 /var/log/packages 2>/dev/null)
+
+declare -A INST
+for rest in "${!PKGREST[@]}"; do
+ # Default split: last dash. Correct for the common case, fixed up below for
+ # any name that a tracked stanza claims explicitly.
+ INST[${rest%-*}]=${rest##*-}
+done
+
+# Given an exact package name, take everything after it as the version. The
+# match is anchored on "<want>-" and strips exactly that many characters, so a
+# VERSION containing dashes survives and a longer sibling package (kvantum-qt5
+# when asked for kvantum) is never mistaken for a match.
+pkg_version() {
+ local want=$1 rest
+ for rest in "${!PKGREST[@]}"; do
+ if [[ ${rest,,} == "${want,,}-"* ]]; then
+ rest=${rest:${#want}+1}
+ # Reject when what follows is not a version (another name component).
+ [[ $rest == [0-9]* ]] || continue
+ echo "$rest"; return 0
+ fi
+ done
+ return 1
+}
+
+# AppImages tracked by appimage-updater (its state is authoritative for those).
+if [[ -r $AI_STATE ]]; then
+ while IFS=$'\t' read -r k v; do
+ [[ -n $k ]] && INST[$k]=$v
+ done < <(jq -r 'to_entries[]|"\(.key)\t\(.value.version)"' "$AI_STATE" 2>/dev/null)
+fi
+
+# Flatpak apps, keyed on the last dot-component of the app id.
+if command -v flatpak >/dev/null 2>&1; then
+ while IFS=$'\t' read -r app v; do
+ [[ -n $app ]] || continue
+ INST[${app##*.}]=$v
+ done < <(flatpak list --app --columns=application,version 2>/dev/null)
+fi
+
+installed_of() { # echoes "version" or "" ; handles alias + case-insensitive
+ local n=$1 cand v
+ for cand in ${ALIAS[$n]:-$n}; do
+ # Anchored lookup first: correct even when VERSION contains a dash.
+ v=$(pkg_version "$cand") && { echo "$v"; return; }
+ [[ -n ${INST[$cand]:-} ]] && { echo "${INST[$cand]}"; return; }
+ for k in "${!INST[@]}"; do
+ [[ ${k,,} == "${cand,,}" ]] && { echo "${INST[$k]}"; return; }
+ done
+ done
+}
+
+# --- repo .info versions ----------------------------------------------------
+declare -A RVER RWHICH
+for entry in "${REPOS[@]}"; do
+ tag=${entry%%:*}; dir=${entry#*:}
+ [[ -d $dir ]] || continue
+ while IFS= read -r inf; do
+ pkg=$(basename "$inf" .info)
+ v=$(sed -n 's/^VERSION="\(.*\)"$/\1/p' "$inf" | head -1)
+ [[ -n $v ]] || continue
+ if [[ -n ${RVER[$pkg]:-} ]]; then
+ RWHICH[$pkg]="${RWHICH[$pkg]},$tag"
+ else
+ RVER[$pkg]=$v; RWHICH[$pkg]=$tag
+ fi
+ done < <(find "$dir" -mindepth 2 -maxdepth 3 -name '*.info' 2>/dev/null)
+done
+
+# --- compare ----------------------------------------------------------------
+# Versions come from unrelated sources, so normalise before comparing: strip a
+# leading v, fold - and _ to dots, lowercase.
+norm() { local s=${1,,}; s=${s#v}; s=${s//[-_]/.}; echo "$s"; }
+
+# Some packages append build metadata the upstream version never carries:
+# a kernel version (r8125 9.018.00_6.18.41) or a git hash (solvespace
+# 3.1-659215d). Treat installed as equal when it merely extends upstream at a
+# component boundary.
+vers_equal() {
+ local a b; a=$(norm "$1"); b=$(norm "$2")
+ [[ $a == "$b" ]] && return 0
+ [[ -n $a && -n $b && $b == "$a".* ]] && return 0
+ return 1
+}
+
+rows=(); take=(); n_up=0; n_repo=0; n_ok=0; n_na=0
+while IFS=$'\t' read -r name uver; do
+ [[ -n $name ]] || continue
+ iver=$(installed_of "$name")
+ rkey=$name
+ if [[ -z ${RVER[$rkey]:-} ]]; then
+ for cand in ${REPO_ALIAS[$name]:-} ${ALIAS[$name]:-}; do
+ [[ -n ${RVER[$cand]:-} ]] && { rkey=$cand; break; }
+ done
+ fi
+ rver=${RVER[$rkey]:-}; rwhich=${RWHICH[$rkey]:-}
+
+ same_inst=0; [[ -n $iver ]] && vers_equal "$uver" "$iver" && same_inst=1
+ same_repo=1; [[ -n $rver ]] && { vers_equal "$uver" "$rver" || same_repo=0; }
+
+ if (( same_inst )) && (( same_repo )); then
+ (( n_ok++ )); take+=("$name")
+ (( SHOW_ALL )) || continue
+ else
+ (( same_inst )) || { [[ -n $iver ]] && (( n_up++ )); }
+ (( same_repo )) || (( n_repo++ ))
+ # Nothing installed and the .info already matches upstream: there is no
+ # action to take on this host, so keep it out of the default view.
+ if [[ -z $iver ]] && (( same_repo )); then
+ (( n_na++ ))
+ (( SHOW_ALL )) || continue
+ fi
+ fi
+ # Packed with | not tab: read collapses runs of whitespace IFS chars, which
+ # would shift every field left whenever installed or repo is empty.
+ rows+=("$name|$uver|$iver|$rver|$rwhich|$same_inst|$same_repo")
+done < <(jq -r '.data|to_entries[]|"\(.key)\t\(.value.version // .value)"' "$NEWVER")
+
+# --- output -----------------------------------------------------------------
+if ((${#rows[@]})); then
+ # Widen the name column to the longest actual name, so a long stanza does not
+ # shunt every later column out of alignment.
+ w=22
+ for r in "${rows[@]}"; do n=${r%%|*}; (( ${#n} > w )) && w=${#n}; done
+ printf '%s %-*s %-14s %-14s %s%s\n' "$DIM" "$w" "package" "upstream" "installed" "repo" "$R"
+ for r in "${rows[@]}"; do
+ IFS='|' read -r name uver iver rver rwhich si sr <<<"$r"
+ if [[ -z $iver ]]; then ic="$DIM"; ishow="--"
+ elif (( si )); then ic="$GRN"; ishow=$iver
+ else ic="$RED"; ishow=$iver
+ fi
+ # Installed-behind and repo-behind are independent, and a row can be both
+ # (upstream moved, and neither the system nor the .info has caught up), so
+ # the marker shows each dimension rather than collapsing to the worst one.
+ if [[ -z $iver ]]; then mark="$E_NA"
+ elif (( si )); then mark="$E_OK"
+ else mark="$E_UP"
+ fi
+ (( sr )) || mark+="$E_REPO"
+ if [[ -z $rver ]]; then rc="$DIM"; rtxt="--"
+ elif (( sr )); then rc="$GRN"; rtxt="$rwhich $rver"
+ else rc="$YEL"; rtxt="$rwhich $rver"
+ fi
+ # Pad the marker cell to a fixed width: rows carrying both markers must not
+ # push the name column right relative to rows carrying one.
+ if (( NO_EMOJI )); then mcell=$(printf '%-5s' "$mark")
+ else mcell=$mark; (( ${#mark} > 1 )) || mcell="$mark "
+ fi
+ printf ' %s %s%-*s%s %-14s %s%-14s%s %s%s%s\n' \
+ "$mcell" "$WHT" "$w" "$name" "$R" "$uver" "$ic" "$ishow" "$R" "$rc" "$rtxt" "$R"
+ done
+ echo
+fi
+printf ' %s%d%s to upgrade · %s%d%s repo%s to bump · %s%d%s current' \
+ "$RED" "$n_up" "$R" "$YEL" "$n_repo" "$R" "$( ((n_repo==1)) || echo s )" \
+ "$GRN" "$n_ok" "$R"
+(( n_na )) && printf ' · %s%d%s not installed' "$DIM" "$n_na" "$R"
+echo
+
+# --- nvtake -----------------------------------------------------------------
+# Record caught-up packages so old_ver reflects the system. flock serialises
+# concurrent logins: nvtake rewrites old_ver.json without locking of its own.
+take_caught_up() {
+ ((${#take[@]})) || return 0
+ command -v nvtake >/dev/null || return 0
+ exec 9>"$LOCK" || return 0
+ flock -n 9 || return 0 # another shell is taking; skip, next run catches it
+ nvtake -c "$CONF" --ignore-nonexistent "${take[@]}" >/dev/null 2>&1
+ exec 9>&-
+}
+(( NO_TAKE )) || take_caught_up