mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 06:00:09 +02:00
FrameDrop sideloads a Linux zip as a Steam Devkit Game. Frametop can't be copied over as an app, so the zip holds a small installer: playing it runs get.sh --yes in a transient user service (Steam reaps the title's process tree on quit) and shows progress in a GTK window. - probe/probe.sh records what a Devkit Game can reach. On SteamOS 0.3.0 it runs on the host with git, podman, systemd and GTK, even with the SLR4 compat tool set; inside SLR4, flatpak-spawn --host reaches the host. - devkit.sh registers a folder the way FrameDrop does (Valve's devkit-utils), for testing without a PC. - build.sh makes a reproducible Frametop.zip and the FrameDrop manifest. Nothing is published. Open: whether FrameDrop keeps the exec bit, and its start command and runtime choice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
126 lines
4.4 KiB
Python
Executable File
126 lines
4.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Progress window for the FrameDrop installer: follows the install service's log and state.
|
|
|
|
Usage: progress.py UNIT LOG
|
|
|
|
Closing the window doesn't stop the install; it keeps running in the user service UNIT.
|
|
"""
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
import gi
|
|
|
|
gi.require_version("Gtk", "4.0")
|
|
gi.require_version("Adw", "1")
|
|
from gi.repository import Adw, GLib, Gtk, Pango # noqa: E402
|
|
|
|
UNIT, LOG = sys.argv[1], sys.argv[2]
|
|
ANSI = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
|
|
# install.sh marks each step with "== N/10 what it is"
|
|
STEP = re.compile(r"^== (\d+)/(\d+) (.*)$")
|
|
DONE_TEXT = ("Frametop is installed. Restart SteamVR once, or reboot the headset, so it loads "
|
|
"Frametop's driver. Then open Launch a program, then Desktop.")
|
|
|
|
|
|
def unit_state():
|
|
out = subprocess.run(
|
|
["systemctl", "--user", "show", "-P", "ActiveState", UNIT],
|
|
capture_output=True, text=True).stdout.strip()
|
|
return out or "inactive"
|
|
|
|
|
|
class Window(Adw.ApplicationWindow):
|
|
def __init__(self, app):
|
|
super().__init__(application=app, title="Frametop")
|
|
self.set_default_size(900, 600)
|
|
self.offset = 0
|
|
self.partial = ""
|
|
|
|
self.status = Gtk.Label(label="Installing Frametop", xalign=0, wrap=True)
|
|
self.status.add_css_class("title-2")
|
|
self.detail = Gtk.Label(label="Starting...", xalign=0, wrap=True)
|
|
self.bar = Gtk.ProgressBar()
|
|
self.bar.pulse()
|
|
|
|
self.text = Gtk.TextView(editable=False, cursor_visible=False, monospace=True,
|
|
wrap_mode=Pango.WrapMode.WORD_CHAR)
|
|
self.text.set_left_margin(8)
|
|
self.text.set_right_margin(8)
|
|
scroll = Gtk.ScrolledWindow(vexpand=True, child=self.text)
|
|
self.scroll = scroll
|
|
|
|
self.note = note = Gtk.Label(
|
|
label="You can close this window: the install keeps going. Play Frametop again to "
|
|
"come back to it.", xalign=0, wrap=True)
|
|
note.add_css_class("dim-label")
|
|
close = Gtk.Button(label="Close", halign=Gtk.Align.END)
|
|
close.connect("clicked", lambda *_: self.close())
|
|
|
|
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12,
|
|
margin_top=18, margin_bottom=18, margin_start=18, margin_end=18)
|
|
for w in (self.status, self.detail, self.bar, scroll, note, close):
|
|
box.append(w)
|
|
view = Adw.ToolbarView(content=box)
|
|
view.add_top_bar(Adw.HeaderBar())
|
|
self.set_content(view)
|
|
|
|
self.finished = False
|
|
GLib.timeout_add(500, self.tick)
|
|
self.tick()
|
|
|
|
def add_lines(self, lines):
|
|
buf = self.text.get_buffer()
|
|
for line in lines:
|
|
m = STEP.match(line)
|
|
if m:
|
|
n, total, what = int(m[1]), int(m[2]), m[3]
|
|
self.bar.set_fraction((n - 1) / total)
|
|
self.detail.set_label(f"Step {n} of {total}: {what}")
|
|
buf.insert(buf.get_end_iter(), line + "\n")
|
|
# Keep the view at the newest line.
|
|
end = buf.create_mark(None, buf.get_end_iter(), False)
|
|
self.text.scroll_mark_onscreen(end)
|
|
buf.delete_mark(end)
|
|
|
|
def tick(self):
|
|
try:
|
|
with open(LOG, "rb") as f:
|
|
f.seek(self.offset)
|
|
data = f.read()
|
|
self.offset += len(data)
|
|
except FileNotFoundError:
|
|
data = b""
|
|
if data:
|
|
text = self.partial + ANSI.sub("", data.decode("utf-8", "replace")).replace("\r", "\n")
|
|
*lines, self.partial = text.split("\n")
|
|
self.add_lines(lines)
|
|
|
|
state = unit_state()
|
|
if state == "activating":
|
|
if self.bar.get_fraction() == 0:
|
|
self.bar.pulse()
|
|
return True
|
|
if self.partial:
|
|
self.add_lines([self.partial])
|
|
self.partial = ""
|
|
self.note.set_visible(False)
|
|
if state == "active":
|
|
self.status.set_label("Done")
|
|
self.detail.set_label(DONE_TEXT)
|
|
self.bar.set_fraction(1)
|
|
else:
|
|
self.status.set_label("The install stopped")
|
|
self.detail.set_label("The log above says why. Play Frametop again to try again.")
|
|
return False
|
|
|
|
|
|
def main():
|
|
app = Adw.Application(application_id="io.github.deejanuz.FrametopInstall")
|
|
app.connect("activate", lambda a: Window(a).present())
|
|
app.run([])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|