Blacksite/scripts/editor/splash.py

218 lines
8.3 KiB
Python
Executable File

#!/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("<span letter_spacing='4300'>BLACKSITE</span>")
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())