#!/usr/bin/env python3 import pathlib import subprocess import sys import gi gi.require_version("Gtk", "4.0") gi.require_version("Gdk", "4.0") from gi.repository import Gdk, GLib, Gtk # noqa: E402 CSS = """ window#blacksite-splash { background: #111418; color: #edf0f2; border: 1px solid #4b535c; } .top-rule { background: #d7a83c; min-height: 4px; } .eyebrow { color: #d7a83c; font-family: monospace; font-size: 11px; font-weight: 700; } .brand { color: #f4f5f6; font-family: sans-serif; font-size: 48px; font-weight: 800; } .edition { color: #9da6ae; font-family: monospace; font-size: 12px; } .monogram { color: #313840; font-family: monospace; font-size: 88px; font-weight: 800; } .build-chip { background: #242a30; color: #c7cdd2; border: 1px solid #3e464e; border-radius: 2px; font-family: monospace; font-size: 10px; padding: 5px 9px; } .footer { background: #1a1f24; border-top: 1px solid #353d45; } .phase { color: #f0f2f4; font-size: 14px; font-weight: 700; } .detail { color: #9da6ae; font-family: monospace; font-size: 11px; } .crate-count { color: #c7cdd2; font-family: monospace; font-size: 12px; font-weight: 700; min-width: 74px; } .percentage { color: #d7a83c; font-family: monospace; font-size: 12px; font-weight: 700; } progressbar trough { background: #30373e; border: 0; border-radius: 0; min-height: 5px; } progressbar progress { background: #d7a83c; border: 0; border-radius: 0; min-height: 5px; } progressbar.error progress { background: #d55b55; } .error-text { color: #ef8b84; } button { background: #293038; color: #eef0f2; border: 1px solid #4a535c; border-radius: 3px; padding: 6px 14px; font-weight: 600; } button:hover { background: #343d45; } button.suggested-action { background: #b98b2f; color: #101317; border-color: #d7a83c; } """ class BlacksiteSplash(Gtk.Application): def __init__(self, status_path: pathlib.Path, log_path: pathlib.Path) -> None: super().__init__(application_id="com.fallingmetal.BlacksiteSplash") self.status_path = status_path self.log_path = log_path self.last_status = None def do_activate(self) -> None: provider = Gtk.CssProvider() provider.load_from_data(CSS.encode("utf-8")) Gtk.StyleContext.add_provider_for_display( Gdk.Display.get_default(), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ) self.window = Gtk.ApplicationWindow(application=self) self.window.set_name("blacksite-splash") self.window.set_title("Blacksite Editor") self.window.set_decorated(False) self.window.set_resizable(False) self.window.set_default_size(760, 410) root = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.window.set_child(root) top_rule = Gtk.Box() top_rule.add_css_class("top-rule") root.append(top_rule) content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) content.set_margin_top(34) content.set_margin_start(44) content.set_margin_end(44) content.set_vexpand(True) root.append(content) header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) content.append(header) brand_stack = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=7) brand_stack.set_hexpand(True) header.append(brand_stack) eyebrow = Gtk.Label(label="FALLING METAL INTERACTIVE") eyebrow.set_halign(Gtk.Align.START) eyebrow.add_css_class("eyebrow") brand_stack.append(eyebrow) brand = Gtk.Label() brand.set_markup("BLACKSITE") brand.set_halign(Gtk.Align.START) brand.add_css_class("brand") brand_stack.append(brand) edition = Gtk.Label(label="PROJECT EDITOR / DEVELOPMENT BUILD") edition.set_halign(Gtk.Align.START) edition.add_css_class("edition") brand_stack.append(edition) monogram = Gtk.Label(label="B/S") monogram.set_halign(Gtk.Align.END) monogram.add_css_class("monogram") header.append(monogram) spacer = Gtk.Box() spacer.set_vexpand(True) content.append(spacer) chip = Gtk.Label(label="BEVY ENGINE / EDITOR RUNTIME") chip.set_halign(Gtk.Align.START) chip.add_css_class("build-chip") content.append(chip) footer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=9) footer.set_margin_top(25) footer.set_margin_bottom(26) footer.set_margin_start(44) footer.set_margin_end(44) footer.add_css_class("footer") root.append(footer) status_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) status_row.set_margin_top(20) footer.append(status_row) labels = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=3) labels.set_hexpand(True) status_row.append(labels) self.phase = Gtk.Label(label="Preparing workspace") self.phase.set_halign(Gtk.Align.START) self.phase.add_css_class("phase") labels.append(self.phase) self.detail = Gtk.Label(label="Reading project configuration") self.detail.set_halign(Gtk.Align.START) self.detail.set_ellipsize(3) self.detail.add_css_class("detail") labels.append(self.detail) self.crate_count = Gtk.Label(label="") self.crate_count.set_valign(Gtk.Align.CENTER) self.crate_count.add_css_class("crate-count") self.crate_count.set_visible(False) status_row.append(self.crate_count) self.percentage = Gtk.Label(label="05%") self.percentage.set_valign(Gtk.Align.CENTER) self.percentage.add_css_class("percentage") status_row.append(self.percentage) self.actions = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) self.actions.set_visible(False) status_row.append(self.actions) open_log = Gtk.Button(label="Open Log") open_log.connect("clicked", self.open_log) self.actions.append(open_log) close = Gtk.Button(label="Close") close.add_css_class("suggested-action") close.connect("clicked", lambda _button: self.quit()) self.actions.append(close) self.progress = Gtk.ProgressBar() self.progress.set_fraction(0.05) footer.append(self.progress) GLib.timeout_add(100, self.poll_status) self.window.present() def open_log(self, _button: Gtk.Button) -> None: if not self.log_path.exists(): return try: subprocess.Popen( ["xdg-open", str(self.log_path)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) except OSError: pass def poll_status(self) -> bool: try: raw_status = self.status_path.read_text(encoding="utf-8") except OSError: return GLib.SOURCE_CONTINUE if raw_status == self.last_status: return GLib.SOURCE_CONTINUE self.last_status = raw_status lines = raw_status.splitlines() if len(lines) < 4: return GLib.SOURCE_CONTINUE phase, detail, progress_text, state = lines[:4] crate_count = lines[4] if len(lines) >= 5 else "" try: progress = max(0, min(100, int(progress_text))) except ValueError: progress = 0 if state == "close": self.quit() return GLib.SOURCE_REMOVE self.phase.set_label(phase) self.detail.set_label(detail) self.progress.set_fraction(progress / 100.0) self.percentage.set_label(f"{progress:02d}%") self.crate_count.set_label(f"({crate_count})" if crate_count else "") self.crate_count.set_visible(bool(crate_count)) if state == "error": self.phase.add_css_class("error-text") self.progress.add_css_class("error") self.percentage.set_visible(False) self.actions.set_visible(True) return GLib.SOURCE_CONTINUE def main() -> int: if len(sys.argv) != 3: print("usage: splash.py STATUS_FILE LOG_FILE", file=sys.stderr) return 2 app = BlacksiteSplash(pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2])) return app.run([sys.argv[0]]) if __name__ == "__main__": raise SystemExit(main())