Skip to content

Instantly share code, notes, and snippets.

@TheSethRose
Created November 22, 2025 19:52
Show Gist options
  • Select an option

  • Save TheSethRose/0c150dcb2359bf7968c92cd7ac7625fe to your computer and use it in GitHub Desktop.

Select an option

Save TheSethRose/0c150dcb2359bf7968c92cd7ac7625fe to your computer and use it in GitHub Desktop.
Interactive Homebrew TUI (powered by `rich`) to audit versions, discover beta candidates, and interactively automate clean upgrades.
import subprocess
import json
import re
import sys
import argparse
from rich.console import Console
from rich.table import Table
from rich.text import Text
from rich import box
from rich.prompt import Confirm, Prompt
# --- Configuration ---
MAX_VER_WIDTH = 25 # Prevents massive version strings from breaking layout
console = Console()
def run_command(command, stream=False):
if stream:
return subprocess.call(command, shell=True)
try:
return subprocess.check_output(command, shell=True, text=True).strip()
except subprocess.CalledProcessError:
return ""
def clean_version(v):
if not v: return ""
return re.sub(r'[_\-]\d+$', '', v)
def truncate(text, limit):
text = str(text)
if len(text) > limit:
return text[:limit-1] + "…"
return text
def get_data():
with console.status("[bold green]Scanning Homebrew system..."):
# 1. Get Installed
raw_inst = run_command("brew info --json=v2 --installed")
try: installed_data = json.loads(raw_inst)
except: installed_data = {}
# 2. Get Betas
regex = "/(@|-)(beta|alpha|nightly|insider|preview|dev|next|canary|edge)/"
raw_search = run_command(f"brew search '{regex}'")
all_betas = set([x.strip() for x in raw_search.split('\n') if x.strip() and "==>" not in x])
return installed_data, all_betas
def get_beta_metadata(beta_names):
if not beta_names: return {}
cmd = f"brew info --json=v2 {' '.join(beta_names)}"
raw = run_command(cmd)
try:
data = json.loads(raw)
lookup = {}
for cat in ['formulae', 'casks']:
for item in data.get(cat, []):
if item.get('token'): lookup[item.get('token')] = item
if item.get('name'): lookup[item.get('name')] = item
return lookup
except: return {}
def parse_item(item, item_type):
name = item.get('token') or item.get('name')
if item_type == 'cask':
local = item.get('installed', "N/A")
latest = item.get('version', "Unknown")
else:
inst = item.get('installed', [])
local = inst[0]['version'] if inst else "N/A"
latest = item['versions']['stable']
return name, str(local), str(latest)
def build_rows():
installed_data, all_betas = get_data()
rows = []
betas_to_lookup = []
for cat, type_label in [('casks', 'cask'), ('formulae', 'formula')]:
for item in installed_data.get(cat, []):
name, local, latest = parse_item(item, type_label)
beta_match = None
for b in all_betas:
if b.startswith(name):
rem = b[len(name):]
if re.match(r"^(@|-)(beta|alpha|nightly|insider|preview|dev|next|canary|edge)", rem):
beta_match = b
betas_to_lookup.append(b)
break
c_local = clean_version(local)
c_latest = clean_version(latest)
needs_update = c_local != c_latest and local != "N/A"
priority = 3
if needs_update: priority = 1
elif beta_match: priority = 2
rows.append({
"name": name,
"type": type_label,
"local": local,
"latest": latest,
"beta_name": beta_match if beta_match else "-",
"beta_ver": "-",
"priority": priority,
"needs_update": needs_update
})
beta_details = get_beta_metadata(betas_to_lookup)
for row in rows:
if row['beta_name'] != "-":
b_data = beta_details.get(row['beta_name'])
if b_data:
if 'version' in b_data: row['beta_ver'] = b_data['version']
elif 'versions' in b_data: row['beta_ver'] = b_data['versions'].get('stable', 'Unknown')
rows.sort(key=lambda x: (x['priority'], x['name']))
return rows
def print_table(rows):
table = Table(box=box.SIMPLE, expand=True)
table.add_column("App Name", style="cyan", no_wrap=True)
table.add_column("Type", style="magenta", width=8)
table.add_column("Your Ver", max_width=MAX_VER_WIDTH)
table.add_column("Stable", max_width=MAX_VER_WIDTH)
table.add_column("Avail Beta", style="green")
table.add_column("Beta Ver", max_width=MAX_VER_WIDTH)
for r in rows:
row_style = ""
name_style = "cyan"
if r['needs_update']:
row_style = "bold yellow"
name_style = "bold yellow"
elif r['beta_name'] == "-":
row_style = "dim"
local_ver = Text(truncate(r['local'], MAX_VER_WIDTH))
stable_ver = Text(truncate(r['latest'], MAX_VER_WIDTH))
beta_ver = Text(truncate(r['beta_ver'], MAX_VER_WIDTH))
if r['needs_update']:
local_ver.stylize("red strike")
stable_ver.stylize("green")
table.add_row(
Text(r['name'], style=name_style),
Text(r['type']),
local_ver,
stable_ver,
r['beta_name'] if r['beta_name'] != "-" else "",
beta_ver if r['beta_ver'] != "-" else "",
style=row_style
)
console.print(table)
console.print(f"[yellow]Yellow[/] = Update Available | [green]Green[/] = Beta Alternative Available", style="italic dim")
# --- AUTO PILOT LOGIC ---
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--dry-run", action="store_true", help="Print command but do not run")
args = parser.parse_args()
# 1. Show the View
rows = build_rows()
print_table(rows)
print("\n")
# 2. Analyze
outdated_count = len([r for r in rows if r['needs_update']])
potential_swaps = [r for r in rows if r['beta_name'] != "-"]
# 3. Construct Command Chain
chain = ["brew update"] # Always start with fresh definitions
# -- Step A: Updates --
if outdated_count > 0:
if Confirm.ask(f"[bold yellow]Included upgrades for {outdated_count} outdated apps?[/]"):
chain.append("brew upgrade --greedy")
else:
console.print("[dim]System is up to date.[/]")
# -- Step B: Swaps --
if potential_swaps:
console.print(f"\n[bold green]Found {len(potential_swaps)} possible beta swaps.[/]")
console.print("[dim]Type names to swap (space separated), 'all', or 'no'[/]")
choice = Prompt.ask("Selection", default="no")
swaps_to_do = []
if choice.lower() == 'all':
swaps_to_do = potential_swaps
elif choice.lower() not in ['no', 'none', 'n']:
targets = choice.split()
swaps_to_do = [x for x in potential_swaps if x['name'] in targets or x['beta_name'] in targets]
if swaps_to_do:
remove_list = [x['name'] for x in swaps_to_do]
install_list = [x['beta_name'] for x in swaps_to_do]
# Use --force on uninstall to prevent "Directory not empty" errors
chain.append(f"brew uninstall --force {' '.join(remove_list)}")
chain.append(f"brew install --cask {' '.join(install_list)}")
# -- Step C: Cleanup --
chain.append("brew cleanup -s")
chain.append("brew doctor")
# 4. Execute
full_cmd = " && ".join(chain)
console.print("\n[bold white on blue] Final Command to Run: [/]")
console.print(f"[cyan]{full_cmd}[/]\n")
if args.dry_run:
console.print("[yellow][Dry Run] Exiting.[/]")
else:
if Confirm.ask("Run this now?"):
console.print("\n[green]🚀 Executing...[/]")
run_command(full_cmd, stream=True)
else:
console.print("[red]Aborted.[/]")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment