From 6fdb31d538269136415ce71cbf58de6017f95463 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 19 Oct 2026 19:12:05 +0200 Subject: Add the PyQt6 GUI: Dashboard, Jobs, VM control, notifications All VM access goes through remote.py (ssh with a shared ControlMaster, local virsh). The main window polls jobs, backs off while ssh is down, checks the libvirt domain and offers Start or Resume, and sends freedesktop notifications when a job ends or the VM comes up. The Jobs tab streams a job's log with job-follow and resumes at the last offset after a drop. Config is TOML, validated against built-in defaults. Co-Authored-By: Claude Opus 5.5 --- sb_builder/fmt.py | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 sb_builder/fmt.py (limited to 'sb_builder/fmt.py') diff --git a/sb_builder/fmt.py b/sb_builder/fmt.py new file mode 100644 index 0000000..d43da2e --- /dev/null +++ b/sb_builder/fmt.py @@ -0,0 +1,61 @@ +# sb-builder - formatting helpers +# Copyright (C) 2026 Danilo M. +# +# 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. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +"""Text formatting shared by the tabs.""" + +import time + +NA = "n/a" +ICON = {"starting": "…", "running": "▶", "ok": "✔", "failed": "✘", + "killed": "■", "lost": "?"} + + +def fmt_bytes(n): + if n is None: + return NA + for unit in ("B", "KiB", "MiB", "GiB"): + if n < 1024: + return f"{n:.0f} {unit}" if unit == "B" else f"{n:.1f} {unit}" + n /= 1024 + return f"{n:.1f} TiB" + + +def fmt_duration(secs): + if secs is None: + return NA + d, r = divmod(int(secs), 86400) + h, r = divmod(r, 3600) + m, s = divmod(r, 60) + return f"{d}d {h}h" if d else f"{h}h {m}m" if h else f"{m}m {s}s" + + +def fmt_age(ts): + return NA if ts is None else fmt_duration(time.time() - ts) + " ago" + + +def fmt_time(ts): + return time.strftime("%m-%d %H:%M", time.localtime(ts)) + + +def job_title(j): + return " ".join([j["kind"], *j["args"]]) + + +def describe(j): + """One-line job summary: icon, kind and args, start time, duration.""" + dur = None if "start" not in j else j.get("end", time.time()) - j["start"] + return (f"{ICON.get(j['status'], '?')} {job_title(j)} " + f"{fmt_time(j.get('start', j['created']))} {fmt_duration(dur)}") -- cgit v1.2.3