191 lines
6.5 KiB
Python
191 lines
6.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Simple curses TUI for selecting a distribution host and release.
|
|
|
|
This TUI reuses helper functions from `jibo_updater.py` to probe hosts and
|
|
list local archives. It prints a JSON object to stdout with the chosen
|
|
selection so other tools or a GUI wrapper can invoke it.
|
|
|
|
Usage:
|
|
python3 jibo_updater_tui.py [--distributors Distributors.json]
|
|
|
|
Output (on success): JSON to stdout, e.g.
|
|
{"host": "https://...","source":"remote","tag":"v3.3.0","tarball_url":"https://..."}
|
|
|
|
Keyboard:
|
|
- Up/Down: navigate
|
|
- Enter: select
|
|
- b: back
|
|
- v: view release notes/name
|
|
- q: quit
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import curses
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import List
|
|
|
|
try:
|
|
from jibo_updater import (
|
|
load_distributors_file,
|
|
measure_host_latency,
|
|
get_releases_from_host,
|
|
list_local_archives,
|
|
_version_tuple,
|
|
check_updater_version,
|
|
__version__ as UPDATER_VERSION,
|
|
DEFAULT_UPDATER_RELEASES_API,
|
|
)
|
|
except Exception as e:
|
|
print(f"Failed to import helpers from jibo_updater: {e}", file=sys.stderr)
|
|
raise
|
|
|
|
|
|
def gather_host_infos(distributors_path: Path):
|
|
d = load_distributors_file(distributors_path)
|
|
hosts = d.get("UpdateHosts") or d.get("OfficialHosts") or []
|
|
hosts = [h for h in hosts if isinstance(h, str)]
|
|
|
|
infos = []
|
|
for h in hosts:
|
|
lat = measure_host_latency(h)
|
|
rels = get_releases_from_host(h)
|
|
infos.append({"host": h, "lat": lat, "rels": rels})
|
|
|
|
local = list_local_archives()
|
|
if local:
|
|
infos.append({"host": "local", "lat": 0.0, "rels": local})
|
|
|
|
infos.sort(key=lambda x: x["lat"] if isinstance(x["lat"], float) else float("inf"))
|
|
return infos
|
|
|
|
|
|
class TUI:
|
|
def __init__(self, stdscr, distributors: Path):
|
|
self.stdscr = stdscr
|
|
self.distributors = distributors
|
|
curses.curs_set(0)
|
|
try:
|
|
latest, is_newer = check_updater_version(DEFAULT_UPDATER_RELEASES_API, UPDATER_VERSION)
|
|
if latest and is_newer:
|
|
self.updater_status = f"Updater update available: {latest} (current {UPDATER_VERSION})"
|
|
elif latest:
|
|
self.updater_status = f"Updater up-to-date ({UPDATER_VERSION})"
|
|
else:
|
|
self.updater_status = "Updater version check failed"
|
|
except Exception:
|
|
self.updater_status = "Updater version check failed"
|
|
|
|
self.hosts = gather_host_infos(distributors)
|
|
|
|
def draw_list(self, items: List[str], title: str, idx: int, offset: int = 0):
|
|
self.stdscr.clear()
|
|
h, w = self.stdscr.getmaxyx()
|
|
header = title[: w - 1]
|
|
status = (" - " + self.updater_status) if hasattr(self, "updater_status") else ""
|
|
if len(header) + len(status) < w - 1:
|
|
header = header + status
|
|
self.stdscr.addstr(0, 0, header[: w - 1])
|
|
for i, line in enumerate(items[offset : offset + h - 3]):
|
|
y = i + 2
|
|
style = curses.A_REVERSE if (i + offset) == idx else curses.A_NORMAL
|
|
try:
|
|
self.stdscr.addstr(y, 0, line[: w - 1], style)
|
|
except curses.error:
|
|
pass
|
|
self.stdscr.addstr(h - 1, 0, "Enter=select v=view b=back q=quit")
|
|
self.stdscr.refresh()
|
|
|
|
def run(self):
|
|
if not self.hosts:
|
|
self.stdscr.addstr(0, 0, "No hosts found in distributors file or no local archives.")
|
|
self.stdscr.addstr(2, 0, "Press any key to exit.")
|
|
self.stdscr.getch()
|
|
return 1
|
|
|
|
idx = 0
|
|
offset = 0
|
|
while True:
|
|
items = [f"{h['host']} ({'local' if h['host']=='local' else f'{h['lat']:.2f}s'}) - {len(h['rels'])} releases" for h in self.hosts]
|
|
self.draw_list(items, "Select host:", idx, offset)
|
|
c = self.stdscr.getch()
|
|
if c in (curses.KEY_DOWN, ord('j')):
|
|
if idx < len(items) - 1:
|
|
idx += 1
|
|
elif c in (curses.KEY_UP, ord('k')):
|
|
if idx > 0:
|
|
idx -= 1
|
|
elif c in (ord('\n'), ord('\r')):
|
|
choice = self.hosts[idx]
|
|
res = self.show_releases(choice)
|
|
if res:
|
|
print(json.dumps(res))
|
|
return 0
|
|
elif c in (ord('q'), 27):
|
|
return 1
|
|
|
|
def show_releases(self, host_info):
|
|
rels = host_info["rels"]
|
|
if not rels:
|
|
return None
|
|
rels.sort(key=lambda r: _version_tuple(r.tag_name), reverse=True)
|
|
idx = 0
|
|
while True:
|
|
items = [f"{r.tag_name}{' [prerelease]' if r.prerelease else ''} - {r.name}" for r in rels]
|
|
self.draw_list(items, f"Host: {host_info['host']}", idx)
|
|
c = self.stdscr.getch()
|
|
if c in (curses.KEY_DOWN, ord('j')):
|
|
if idx < len(items) - 1:
|
|
idx += 1
|
|
elif c in (curses.KEY_UP, ord('k')):
|
|
if idx > 0:
|
|
idx -= 1
|
|
elif c in (ord('b'), 8):
|
|
return None
|
|
elif c == ord('v'):
|
|
self.show_text(rels[idx].name or "(no notes)")
|
|
elif c in (ord('\n'), ord('\r')):
|
|
chosen = rels[idx]
|
|
res = {
|
|
"host": host_info["host"],
|
|
"source": "local" if host_info["host"] == "local" else "remote",
|
|
"tag": chosen.tag_name,
|
|
"tarball_url": chosen.tarball_url,
|
|
}
|
|
return res
|
|
elif c in (ord('q'), 27):
|
|
return None
|
|
|
|
def show_text(self, text: str):
|
|
self.stdscr.clear()
|
|
h, w = self.stdscr.getmaxyx()
|
|
lines = []
|
|
for ln in text.splitlines():
|
|
while ln:
|
|
lines.append(ln[: w - 1])
|
|
ln = ln[w - 1 :]
|
|
for i, ln in enumerate(lines[: h - 2]):
|
|
try:
|
|
self.stdscr.addstr(i, 0, ln)
|
|
except curses.error:
|
|
pass
|
|
self.stdscr.addstr(h - 1, 0, "Press any key to return")
|
|
self.stdscr.refresh()
|
|
self.stdscr.getch()
|
|
|
|
|
|
def main(argv):
|
|
import argparse
|
|
|
|
parser = argparse.ArgumentParser(description="Curses TUI for jibo_updater selection")
|
|
parser.add_argument("--distributors", type=Path, default=Path("Distributors.json"), help="Path to Distributors.json")
|
|
args = parser.parse_args(argv)
|
|
|
|
curses.wrapper(lambda stdscr: TUI(stdscr, args.distributors).run())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv[1:]) or 0)
|