218 lines
8.3 KiB
Python
Executable File
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())
|