mirror of
https://github.com/MichaelScottsman/LiquidAss-Frame.git
synced 2026-10-17 20:01:00 +02:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7775b8d683 | ||
|
|
8b422d1cd6 |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
<p align="center"><img src="assets/icon.png" alt="LiquidAss icon" width="160"></p>
|
||||
|
||||
# LiquidAss: Liquid Glass for the Steam Frame's VR interface
|
||||
# LiquidAss: Liquid Glass Theme and Virtual Music Player for Steam Frame
|
||||
|
||||
LiquidAss re-skins the Steam Frame's VR UI in Apple's glass design language. AI assisted.
|
||||
|
||||
@@ -50,6 +50,8 @@ Press the paintbrush button at the right end of the dashboard bar. Its panel set
|
||||
|
||||
Press the music note button on the dashboard bar, just left of the paintbrush. The assPod appears in the hand you pressed it with.
|
||||
|
||||
You can also open it from **+** › **Launch Program** › **AssPod**, and this works whether the LiquidAss theme is on or off. The installer adds that entry once VLC is installed. With the theme off, Steam's UI stays stock. Only the assPod and its player run, and they stop by themselves about 30 seconds after you put the assPod away with nothing playing.
|
||||
|
||||
**assPod Model** on its main menu switches the body: **Video Gen 1** (the original) or **Nano Gen 2**, in anodized aluminum with white end caps. The nano has the same menus, laid out for its smaller screen with larger text (six rows, like the real one), and it plays no video. **Color** lists the current body's finishes: Black and White for the Video; Silver, Green, Blue, Pink, Black and (PRODUCT)RED for the nano. The model and colours you pick are kept across launches.
|
||||
|
||||
## Nothing persists
|
||||
@@ -62,11 +64,12 @@ Press the music note button on the dashboard bar, just left of the paintbrush. T
|
||||
|
||||
Nothing starts at boot. There are no systemd units, autostart entries or Steam file patches.
|
||||
|
||||
Only two things stay installed, so the toggle is there after a reboot, plus two files of your own:
|
||||
Only the launcher entries and the files they run stay installed, so they are there after a reboot, plus two files of your own:
|
||||
|
||||
| Path | What it is |
|
||||
|---|---|
|
||||
| `~/.local/share/applications/glass-shell.desktop` | The "LiquidAss" entry in + › Launch Program |
|
||||
| `~/.local/share/applications/asspod.desktop` | The "AssPod" entry in + › Launch Program (only when VLC is installed) |
|
||||
| `~/.local/share/glass-shell/` | The toggle script and the theme files it reads |
|
||||
| `~/.config/glass-shell/tune.json` | Your glass settings from the paintbrush button on the bar (colour, intensity, refraction, frost, highlights). Written only when you change them; **Reset to default** in that panel deletes it. An exception to "nothing persists", by request |
|
||||
| `~/.config/glass-shell/asspod.json` | The assPod model and colours you picked. Written only when you change them. The other exception, by request |
|
||||
@@ -77,7 +80,7 @@ The [uninstall](#uninstall) command (or `python glass.py uninstall`) removes all
|
||||
|
||||
```bash
|
||||
python glass.py install # copy this checkout to the headset, set up glassd and add the launcher (for development)
|
||||
python glass.py on | off | toggle | status
|
||||
python glass.py on | off | toggle | status | pod
|
||||
python glass.py dial 0.7 # glass intensity: 0 = clearest, 1 = most opaque (saved with your glass settings)
|
||||
python glass.py uninstall
|
||||
```
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=AssPod
|
||||
GenericName=assPod music player
|
||||
Comment=Take out the assPod, with or without the LiquidAss theme. It plays your music and videos through VLC.
|
||||
Exec=/home/steamos/.local/share/glass-shell/device/lgs pod
|
||||
Icon=/home/steamos/.local/share/glass-shell/device/asspod/icon.png
|
||||
Terminal=false
|
||||
Categories=AudioVideo;Audio;Player;
|
||||
Keywords=asspod;music;player;vlc;liquidass;
|
||||
StartupNotify=false
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 9.9 KiB |
+77
-7
@@ -6,7 +6,7 @@ dashboard, bar, popups, keyboard, toasts) on or off. The theme is injected
|
||||
into the running Steam client over its local devtools socket and is never
|
||||
written anywhere persistent: restarting Steam or rebooting restores stock.
|
||||
|
||||
usage: lgs [toggle|on|off|reload|status|dial 0..1|toast TEXT|flags ...|check]
|
||||
usage: lgs [toggle|on|off|reload|status|pod|dial 0..1|toast TEXT|flags ...|check]
|
||||
[--quiet] [--native|--css] [--flags a,b=v] [--no-rt]
|
||||
|
||||
toggle on if off, off if on (what the "+ > Launch Program" entry runs)
|
||||
@@ -23,6 +23,12 @@ usage: lgs [toggle|on|off|reload|status|dial 0..1|toast TEXT|flags ...|check]
|
||||
reload re-read theme/*.css and the runtime and swap them in without a toast
|
||||
status print what is applied, module states, and any theme tokens that
|
||||
did not resolve
|
||||
pod take out the assPod (what "+ > Launch Program > AssPod" runs). With
|
||||
the theme on, the bar's music note does the same. With it off, only
|
||||
the assPod's runtime modules go into Steam (rt.podOnly) and lgs-shell
|
||||
starts in pod mode (lgs_shell.py --pod: no theme, the assPod's page
|
||||
script only), which ends itself once the assPod is away and nothing
|
||||
plays. lgs on replaces both with the full theme
|
||||
dial V transparency dial, 0 = clearest .. 1 = most opaque (default 0.5);
|
||||
remembered for the next "on", applied now if the theme is on
|
||||
flags print the effective runtime flags and where each comes from
|
||||
@@ -630,20 +636,26 @@ def _carried():
|
||||
|
||||
|
||||
_RT_EXTRA = [] # [(label, source)] extra module sources: selftest fixtures only (RT-2)
|
||||
POD_RT_FILES = ("rt/02-react.js", "rt/34-asspod.js") # `lgs pod` with the theme off: the assPod and its deps
|
||||
|
||||
|
||||
async def rt_load(s, cli_flags=None, version=None):
|
||||
async def rt_load(s, cli_flags=None, version=None, pod=False):
|
||||
"""Boot 00-rt.js, load every module file (one evaluation each, so a syntax
|
||||
error fails only that file), then start(). Returns a short summary."""
|
||||
error fails only that file), then start(). Returns a short summary.
|
||||
pod=True: the core, device/shared and POD_RT_FILES only, booted podOnly
|
||||
(no liveness watch on the CSS core; `lgs pod` with the theme off)."""
|
||||
files = rt_sources()
|
||||
if not files or files[0][0] != "rt/00-rt.js":
|
||||
return {"runtime": "off", "reason": "device/rt/00-rt.js is missing"}
|
||||
if pod:
|
||||
files = [f for f in files if f[0] == "rt/00-rt.js" or f[0].startswith("shared/") or f[0] in POD_RT_FILES]
|
||||
layers = flag_layers(cli_flags)
|
||||
data, data_errors = rt_data()
|
||||
cfg = {"builtin": layers["builtin"], "defaults": layers["defaults"], "session": layers["session"],
|
||||
"cli": layers["cli"], "version": version, "files": [f for f, _ in files], "data": data,
|
||||
"test": _carried()}
|
||||
_TEST_CARRY.update(t=0, state=None)
|
||||
"test": None if pod else _carried(), "podOnly": bool(pod)}
|
||||
if not pod:
|
||||
_TEST_CARRY.update(t=0, state=None)
|
||||
t0 = time.time()
|
||||
boot = (f"(() => {{\nconst __LGS_RT_CONFIG = {json.dumps(cfg)};\n{_read(files[0][1])}\n}})()\n"
|
||||
"//# sourceURL=lgs/rt/00-rt.js")
|
||||
@@ -652,7 +664,7 @@ async def rt_load(s, cli_flags=None, version=None):
|
||||
return {"runtime": "off", "reason": "00-rt.js did not define __LGS_RT"}
|
||||
for label, msg in data_errors:
|
||||
await s.eval(f"window.__LGS_RT._fail({json.dumps(label)}, {json.dumps(msg)})", 10)
|
||||
todo = [(label, path, None) for label, path in files[1:]] + [(label, None, src) for label, src in _RT_EXTRA]
|
||||
todo = [(label, path, None) for label, path in files[1:]] + ([] if pod else [(label, None, src) for label, src in _RT_EXTRA])
|
||||
for label, path, src in todo:
|
||||
if src is None:
|
||||
try:
|
||||
@@ -2274,6 +2286,62 @@ def is_on():
|
||||
return bool(st and st.get("enabled"))
|
||||
|
||||
|
||||
POD_WAIT_S = 30 # `lgs pod`: how long the pod daemon may take to offer asspod.open
|
||||
POD_RT_JS = ("(() => { const r = window.__LGS_RT; if (!r) return 'none';"
|
||||
" return (r.podOnly ? 'pod' : 'full') + (r.has && r.has('asspod') ? '+asspod' : ''); })()")
|
||||
POD_READY_JS = ("(() => { const r = window.__LGS_RT; if (!r || !r.bridge) return false;"
|
||||
" const d = r.bridge.get('daemon'); return !!(d && typeof d.at === 'number'"
|
||||
" && Date.now() - d.at <= (d.ttlMs || 6000) && Array.isArray(d.actions)"
|
||||
" && d.actions.includes('asspod.open') && typeof window.lgsAction === 'function'); })()")
|
||||
POD_OPEN_JS = ("(async () => { const r = window.__LGS_RT;"
|
||||
" if (!r || !r.has('asspod')) return JSON.stringify({ok: false, error: 'no-asspod'});"
|
||||
" return JSON.stringify(await r.action('asspod.open', {}, {src: 'bar', timeoutMs: 45000})); })()")
|
||||
|
||||
|
||||
def pod():
|
||||
"""lgs pod: the assPod out in the hand that launched it, with or without the theme."""
|
||||
import lgs_shell
|
||||
out = {"theme": is_on()}
|
||||
|
||||
async def go(s):
|
||||
o = {}
|
||||
cur = await s.eval(POD_RT_JS, 10)
|
||||
if not out["theme"] and cur != "pod+asspod":
|
||||
# theme off: the assPod's own runtime (a full one left over is replaced; a pod one that lost
|
||||
# its module is reloaded)
|
||||
if cur != "none":
|
||||
o["rtOff"] = await rt_teardown(s, "lgs pod")
|
||||
o["runtime"] = await rt_load(s, pod=True)
|
||||
cur = await s.eval(POD_RT_JS, 10)
|
||||
o["rt"] = cur
|
||||
return o
|
||||
|
||||
out.update(asyncio.run(_session(go)))
|
||||
if not str(out.get("rt", "")).endswith("+asspod"):
|
||||
out.update(ok=False, error="the assPod module is not running in Steam (flag assPod off, or it failed)")
|
||||
return out
|
||||
if not out["theme"]:
|
||||
out["shell"] = lgs_shell.start(native="off", pod=True)
|
||||
|
||||
async def wait_open(s):
|
||||
t0 = time.time()
|
||||
while not await s.eval(POD_READY_JS, 10):
|
||||
if time.time() - t0 > POD_WAIT_S:
|
||||
return {"ok": False, "error": f"no daemon offering asspod.open after {POD_WAIT_S} s"}
|
||||
await asyncio.sleep(0.5)
|
||||
while True:
|
||||
r = _jsonish(await s.eval(POD_OPEN_JS, 50))
|
||||
# a new pod daemon offers asspod.open a moment before its page script is in SteamVR's systemui
|
||||
if not (isinstance(r, dict) and r.get("ok") is False and "not installed" in str(r.get("detail"))) \
|
||||
or time.time() - t0 > POD_WAIT_S:
|
||||
return r
|
||||
await asyncio.sleep(1.0)
|
||||
|
||||
out["open"] = asyncio.run(_session(wait_open))
|
||||
out["ok"] = bool(isinstance(out["open"], dict) and out["open"].get("ok"))
|
||||
return out
|
||||
|
||||
|
||||
LAB_LOCK = "/tmp/lgs/lab.lock"
|
||||
CLI_LOCK_WAIT_S = 90
|
||||
|
||||
@@ -2362,7 +2430,7 @@ def main(argv):
|
||||
return 0 if res["ok"] else 1
|
||||
if cmd == "selftest":
|
||||
return selftest_main(args[1:], opts)
|
||||
held = _cli_lab_lock() if cmd in ("toggle", "on", "off", "reload", "dial") else None
|
||||
held = _cli_lab_lock() if cmd in ("toggle", "on", "off", "reload", "dial", "pod") else None
|
||||
try:
|
||||
if cmd == "toggle":
|
||||
cmd = "off" if is_on() else "on"
|
||||
@@ -2389,6 +2457,8 @@ def main(argv):
|
||||
res["flagsFile"] = "cleared"
|
||||
elif cmd == "status":
|
||||
res = op("status", vr=True)
|
||||
elif cmd == "pod":
|
||||
res = pod()
|
||||
else:
|
||||
print(__doc__)
|
||||
return 2
|
||||
|
||||
+97
-16
@@ -35,6 +35,14 @@ In both modes it also (docs/phase2/contracts/daemon.md):
|
||||
- injects SteamVR page scripts device/vr/<page>.<name>.js and removes them
|
||||
again.
|
||||
|
||||
Pod mode (--pod, what `lgs pod` starts while the theme is off; + > Launch
|
||||
Program > AssPod): no theme anywhere. SteamVR's pages are not themed, only
|
||||
the assPod's page script goes in, Steam runs only the assPod's runtime
|
||||
modules (rt.podOnly, loaded by lgs pod), and a theme that is off is not a
|
||||
reason to go dormant. It exits once the assPod has been away with nothing
|
||||
playing for POD_IDLE_S, at once after a Steam restart, and on lgs on (which
|
||||
starts a full unit in its place) or lgs off.
|
||||
|
||||
Nothing persists: the unit is transient (systemd-run --collect) and writes no
|
||||
bytecode (python3 -B); lgs off stops it at once. A theme off without lgs off
|
||||
(a lab --stock step) makes it dormant: native layer, glassd, reporter and scene
|
||||
@@ -45,7 +53,7 @@ class, binding global and file it made. If it dies without cleaning up,
|
||||
lgs-native clears itself within 3 s and the scene-graph nodes within 12 s
|
||||
(heartbeat watchdogs).
|
||||
|
||||
lgs_shell.py start [--native|--css|--auto] [--glassd PATH|none]
|
||||
lgs_shell.py start [--native|--css|--auto|--pod] [--glassd PATH|none]
|
||||
[--glassd-args "ARGS"] [--feed] [--stay]
|
||||
[--test-report PATH] [--assume-caps CAPS]
|
||||
start the unit; lgs on does this.
|
||||
@@ -152,6 +160,11 @@ SLAB_SIZE_TOL_PX = 2.5 # glassd slab vs element x backdropScale
|
||||
THEME_OFF_POLLS = 3 # Steam theme seen off this many polls (1 s) -> dormant (or exit)
|
||||
THEME_OFF_GRACE_S = 600 # ... dormant this long before exiting (flag shellThemeGraceS; 0 = exit)
|
||||
STAY_MAX_S = 2400 # --stay: dormant at most this long, and native at most this long after start
|
||||
POD_SCRIPTS = ("asspod",) # --pod: the only SteamVR page scripts it installs
|
||||
POD_IDLE_S = 30.0 # --pod: exit once the assPod is away and nothing plays this long
|
||||
POD_FIRST_OPEN_S = 90.0 # --pod: ... or when it was never taken out this long after start
|
||||
POD_RT_TEARDOWN_JS = ("(async () => { const r = window.__LGS_RT; if (!r || !r.podOnly || typeof r.teardown !== 'function')"
|
||||
" return 'absent'; await r.teardown('pod daemon exit'); return 'removed'; })()")
|
||||
# (glass.py's native-session client gives up after 2400 s; a unit older than
|
||||
# that has lost its client, e.g. a usage-limit cut-off)
|
||||
STEAM_GONE_S = 120 # Steam's devtools unreachable this long -> exit (also with --stay)
|
||||
@@ -916,7 +929,10 @@ def clean_quad(q):
|
||||
|
||||
class Shell:
|
||||
def __init__(self, glassd_path, stay=False, glassd_args=None, native=False, native_req=None, native_src=None,
|
||||
test_report=None, assume_caps=None):
|
||||
test_report=None, assume_caps=None, pod=False):
|
||||
self.pod = bool(pod) # pod mode: the assPod alone, no theme (lgs pod)
|
||||
if self.pod:
|
||||
native = False
|
||||
self.glassd_path = glassd_path
|
||||
self.glassd_args = list(glassd_args or [])
|
||||
self.stay = stay # lab aid: go dormant instead of exiting on theme off
|
||||
@@ -1101,6 +1117,8 @@ class Shell:
|
||||
|
||||
# ---------------------------------------------------------------- state
|
||||
def mode(self):
|
||||
if self.pod:
|
||||
return "pod"
|
||||
if self.dormant_since is not None: # theme off without lgs off: waiting for it (contract §1)
|
||||
return "dormant"
|
||||
if not self.native_enabled:
|
||||
@@ -1364,7 +1382,7 @@ class Shell:
|
||||
rep = self.current_report() or {}
|
||||
return {
|
||||
"pid": os.getpid(), "since": time.strftime("%H:%M:%S", time.localtime(self.since)),
|
||||
"updated": time.time(), "mode": self.mode(),
|
||||
"updated": time.time(), "mode": self.mode(), "pod": self.pod,
|
||||
"native": {"requested": self.native_req, "resolved": "on" if self.native_requested else "off",
|
||||
"source": self.native_src, "enabled": self.native_enabled, "reason": self.css_reason},
|
||||
"flags": self.rt_flags, "profile": self.profile, "reduceMotion": self.reduce_motion,
|
||||
@@ -2214,7 +2232,7 @@ class Shell:
|
||||
self.native_force, self.bridge_sent = True, {}
|
||||
self.changed.set()
|
||||
self.check_stay_limit()
|
||||
if self.theme_off_polls >= THEME_OFF_POLLS:
|
||||
if self.theme_off_polls >= THEME_OFF_POLLS and not self.pod: # pod: no theme, by design
|
||||
# A theme off without `lgs off` (which stops us first) is a
|
||||
# test toggling it (lab --stock, P1's selftests) or a Steam
|
||||
# restart: stay dormant for the grace, exit on a restart.
|
||||
@@ -3116,6 +3134,22 @@ class Shell:
|
||||
self.vr_targets = {}
|
||||
await self.vr_conns_close()
|
||||
continue
|
||||
if self.pod: # no page theming: the assPod's page script only
|
||||
self.vr_targets = {(t.get("title") or t.get("url")): t for t in ts}
|
||||
scripts = {n: v for n, v in self.vr_scripts_cached().items() if n in POD_SCRIPTS}
|
||||
await self.vr_conns_close(keep={t.get("id") or t["webSocketDebuggerUrl"] for t in ts})
|
||||
pages = {}
|
||||
for t in ts:
|
||||
name = t.get("title") or t.get("url")
|
||||
try:
|
||||
await self.vr_scripts_for(t, scripts)
|
||||
pages[name] = "pod"
|
||||
except Exception: # noqa: BLE001 - page reloading or closing
|
||||
pages[name] = "error"
|
||||
self.vr_pages = pages
|
||||
for gone in [n for n in self.vrx_status if n not in pages]:
|
||||
del self.vrx_status[gone]
|
||||
continue
|
||||
s = lgs_vr.signature()
|
||||
if s != sig:
|
||||
sig = s
|
||||
@@ -3149,6 +3183,37 @@ class Shell:
|
||||
except Exception as e: # noqa: BLE001 - best effort, like lgs-vr
|
||||
self.note(f"vr: {e!r}")
|
||||
|
||||
# ---------------------------------------------------------------- pod mode
|
||||
async def pod_loop(self):
|
||||
"""--pod: end once the assPod is away and nothing plays (POD_IDLE_S), when it was never taken out
|
||||
(POD_FIRST_OPEN_S), or at once after a Steam restart (its pod runtime went with the old page)."""
|
||||
opened, idle_since = False, None
|
||||
while not self.stopping:
|
||||
await asyncio.sleep(1.0)
|
||||
if self.steam_restarted:
|
||||
log("pod: Steam restarted; exiting")
|
||||
self.request_stop("pod: steam restarted", strip_vr=True)
|
||||
return
|
||||
p = self.registry.plugins.get("asspod") or {}
|
||||
st = p["ctx"].state if p.get("ctx") is not None else {}
|
||||
busy = bool(st.get("open") or st.get("playing") or st.get("starting"))
|
||||
opened = opened or bool(st.get("open"))
|
||||
now = time.time()
|
||||
if busy:
|
||||
idle_since = None
|
||||
continue
|
||||
if not opened:
|
||||
if now - self.since >= POD_FIRST_OPEN_S:
|
||||
log(f"pod: the assPod was not taken out within {POD_FIRST_OPEN_S:.0f} s; exiting")
|
||||
self.request_stop("pod: never opened", strip_vr=True)
|
||||
return
|
||||
continue
|
||||
idle_since = idle_since or now
|
||||
if now - idle_since >= POD_IDLE_S:
|
||||
log(f"pod: the assPod has been away with nothing playing for {POD_IDLE_S:.0f} s; exiting")
|
||||
self.request_stop("pod: idle", strip_vr=True)
|
||||
return
|
||||
|
||||
# ---------------------------------------------------------------- status
|
||||
async def status_loop(self):
|
||||
while not self.stopping:
|
||||
@@ -3244,6 +3309,9 @@ class Shell:
|
||||
# the inert binding functions (both: removeBinding leaves them):
|
||||
# gone, so callers see no daemon and nothing of ours stays (G-REMOVE)
|
||||
rb = await s.eval(f"({DELETE_BINDINGS_JS})({json.dumps([BINDING, ACTION_BINDING])})", 5)
|
||||
if self.pod: # the assPod's own runtime (lgs pod) goes with us; a full one stays
|
||||
rp = await s.eval(POD_RT_TEARDOWN_JS, 30)
|
||||
log(f"teardown: pod runtime {rp}")
|
||||
finally:
|
||||
await s.close()
|
||||
log(f"teardown: lgs-native {r}, reporter {rr}, binding globals deleted [{rb}]")
|
||||
@@ -3271,14 +3339,16 @@ class Shell:
|
||||
for sig in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP):
|
||||
loop.add_signal_handler(sig, lambda s=sig: self.request_stop(signal.Signals(s).name))
|
||||
os.makedirs(SHM, exist_ok=True)
|
||||
log(f"lgs-shell starting (pid {os.getpid()}, "
|
||||
+ (f"native, glassd {self.glassd_path} {' '.join(self.glassd_args)}".rstrip() if self.native_enabled
|
||||
else f"css only: {self.css_reason}") + ")")
|
||||
log(f"lgs-shell starting (pid {os.getpid()}, " + ("pod mode: the assPod alone, no theme" if self.pod else
|
||||
(f"native, glassd {self.glassd_path} {' '.join(self.glassd_args)}".rstrip() if self.native_enabled
|
||||
else f"css only: {self.css_reason}")) + ")")
|
||||
self.refresh_flags()
|
||||
self.tasks = [loop.create_task(c) for c in (
|
||||
self.stall_loop(), self.steam_loop(), self.native_loop(), self.sysui_loop(), self.glassd_loop(),
|
||||
self.glassd_out_loop(),
|
||||
self.vr_theme_loop(), self.status_loop(), self.geom_loop(), self.actions_loop())]
|
||||
if self.pod:
|
||||
self.tasks.append(loop.create_task(self.pod_loop()))
|
||||
pending = set(self.tasks)
|
||||
while self.stop_task is None and pending:
|
||||
done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED)
|
||||
@@ -3412,7 +3482,7 @@ def ensure_glassd(path=GLASSD_BIN):
|
||||
return False
|
||||
|
||||
|
||||
def start(glassd=GLASSD_BIN, stay=False, glassd_args=None, native=None, test_report=None, assume_caps=None):
|
||||
def start(glassd=GLASSD_BIN, stay=False, glassd_args=None, native=None, test_report=None, assume_caps=None, pod=False):
|
||||
"""Start the unit. Also resumes SteamVR page theming that a lab "--theme
|
||||
off" on a vr: page paused.
|
||||
|
||||
@@ -3423,7 +3493,9 @@ def start(glassd=GLASSD_BIN, stay=False, glassd_args=None, native=None, test_rep
|
||||
then defaults.json, else CSS only). A unit that is stopping is waited for
|
||||
and then started fresh. stay=True (lab testing only) keeps it running,
|
||||
dormant, while the Steam theme is off. test_report (lab) replaces the
|
||||
reporter with a report JSON file.
|
||||
reporter with a report JSON file. pod=True (lgs pod, theme off) runs pod
|
||||
mode: a running pod unit is kept, any other unit is replaced by one; a
|
||||
running pod unit is replaced by a full one for any other request.
|
||||
|
||||
Serialized by START_LOCK: two `lgs on` at once (one lock-free) made the
|
||||
loser's systemd-run fail with "already loaded" although a unit ran (review
|
||||
@@ -3436,13 +3508,15 @@ def start(glassd=GLASSD_BIN, stay=False, glassd_args=None, native=None, test_rep
|
||||
except (OSError, TimeoutError):
|
||||
lk = None
|
||||
try:
|
||||
return _start(glassd, stay, glassd_args, native, test_report, assume_caps)
|
||||
return _start(glassd, stay, glassd_args, native, test_report, assume_caps, pod)
|
||||
finally:
|
||||
if lk is not None:
|
||||
lk.__exit__()
|
||||
|
||||
|
||||
def _start(glassd, stay, glassd_args, native, test_report, assume_caps):
|
||||
def _start(glassd, stay, glassd_args, native, test_report, assume_caps, pod=False):
|
||||
if pod:
|
||||
native = "off"
|
||||
req = "auto" if native is None else native_word(native)
|
||||
try:
|
||||
os.remove(VR_PAUSE)
|
||||
@@ -3455,18 +3529,22 @@ def _start(glassd, stay, glassd_args, native, test_report, assume_caps):
|
||||
return ["--glassd", (glassd if nat and glassd else "none")] + (["--native"] if nat else []) + \
|
||||
(["--stay"] if stay else []) + (["--glassd-args", " ".join(glassd_args)] if nat and glassd_args else []) + \
|
||||
(["--test-report", os.path.abspath(test_report)] if test_report else []) + \
|
||||
(["--assume-caps", ",".join(assume_caps)] if assume_caps else [])
|
||||
(["--assume-caps", ",".join(assume_caps)] if assume_caps else []) + \
|
||||
(["--pod"] if pod else [])
|
||||
|
||||
state, pid = settle()
|
||||
if state in ("active", "activating", "reloading"):
|
||||
argv = daemon_argv(pid)
|
||||
cur = "--native" in argv
|
||||
if pod and "--pod" in argv:
|
||||
return "running (pod)"
|
||||
was_pod = "--pod" in argv
|
||||
running = core_args(argv[argv.index("daemon") + 1:]) if "daemon" in argv else None
|
||||
# an explicit request restarts a unit that runs differently
|
||||
st = read_json(STATUS) or {}
|
||||
stay_over = req == "on" and st.get("pid") == pid and \
|
||||
str((st.get("native") or {}).get("reason") or "").startswith("--stay limit")
|
||||
if (req == "auto" or running == daemon_args(req == "on")) and not stay_over:
|
||||
if (req == "auto" or running == daemon_args(req == "on")) and not stay_over and not was_pod and not pod:
|
||||
if cur and st.get("pid") == pid and not (st.get("native") or {}).get("enabled", True):
|
||||
return f"running (native requested; {st.get('mode')})"
|
||||
return "running (native)" if cur else "running (css only)"
|
||||
@@ -3488,6 +3566,8 @@ def _start(glassd, stay, glassd_args, native, test_report, assume_caps):
|
||||
cur = "--native" in daemon_argv(pid)
|
||||
return "running (native, started concurrently)" if cur else "running (css only, started concurrently)"
|
||||
return f"failed: {r.stderr.strip()}"
|
||||
if pod:
|
||||
return "started (pod)"
|
||||
if nat and not (glassd and os.access(glassd, os.X_OK)):
|
||||
return "started (css only: native requested but no glassd binary)"
|
||||
return "started (native)" if nat else "started (css only)"
|
||||
@@ -4438,8 +4518,8 @@ def main(argv):
|
||||
native=bool(native_arg(argv)), native_req=opt_arg(argv, "--req"),
|
||||
native_src=opt_arg(argv, "--src"),
|
||||
test_report=opt_arg(argv, "--test-report"),
|
||||
assume_caps=[c for c in (opt_arg(argv, "--assume-caps") or "").split(",") if c]
|
||||
).run())
|
||||
assume_caps=[c for c in (opt_arg(argv, "--assume-caps") or "").split(",") if c],
|
||||
pod="--pod" in argv).run())
|
||||
except KeyboardInterrupt:
|
||||
return 0
|
||||
if cmd == "start":
|
||||
@@ -4452,7 +4532,8 @@ def main(argv):
|
||||
gargs = ["--no-feed"]
|
||||
print(start(glassd, stay_arg(argv), gargs, None if nat is None or "--auto" in argv else nat,
|
||||
test_report=opt_arg(argv, "--test-report"),
|
||||
assume_caps=[c for c in (opt_arg(argv, "--assume-caps") or "").split(",") if c]))
|
||||
assume_caps=[c for c in (opt_arg(argv, "--assume-caps") or "").split(",") if c],
|
||||
pod="--pod" in argv))
|
||||
elif cmd == "stop":
|
||||
print(stop())
|
||||
elif cmd == "status":
|
||||
|
||||
+6
-1
@@ -30,6 +30,9 @@
|
||||
const now = () => (W.performance ? W.performance.now() : Date.now());
|
||||
|
||||
CONFIG = CONFIG || {};
|
||||
// `lgs pod` with the theme off: only the assPod's modules, kept by the pod daemon (lgs_shell.py --pod),
|
||||
// not by the CSS core, so the liveness watch below never removes it for a missing html.lgs-on.
|
||||
const POD_ONLY = !!CONFIG.podOnly;
|
||||
|
||||
// A previous instance (manual re-evaluation): tear it down first. lgs.py
|
||||
// already awaits teardown before booting, so this is only a safety net.
|
||||
@@ -755,7 +758,7 @@
|
||||
}
|
||||
// true when it started the teardown
|
||||
function checkLiveness() {
|
||||
if (!started || stopping) return false;
|
||||
if (!started || stopping || POD_ONLY) return false;
|
||||
let on = true, what = 'html.lgs-on gone from main';
|
||||
try {
|
||||
if (moHtml) on = moHtml.classList.contains('lgs-on');
|
||||
@@ -1011,6 +1014,7 @@
|
||||
runtime: stopped ? 'removed' : (stopping ? 'stopping' : (started ? 'running' : 'loaded')),
|
||||
version: VERSION,
|
||||
build: CONFIG.version || null,
|
||||
podOnly: POD_ONLY,
|
||||
since: new Date(T0).toISOString(),
|
||||
modules: list,
|
||||
failedLoads: failedLoads.slice(),
|
||||
@@ -1110,6 +1114,7 @@
|
||||
// ------------------------------------------------------------------ public object
|
||||
const rt = {
|
||||
version: VERSION,
|
||||
podOnly: POD_ONLY,
|
||||
W,
|
||||
define,
|
||||
use(name) {
|
||||
|
||||
+39
-17
@@ -14,7 +14,10 @@
|
||||
quick-access buttons pass. Steam's windows also take no laser clicks meanwhile
|
||||
(html.lgs-asspod-out, theme/34-asspod.css). A bridge value older
|
||||
than its ttl ends all of this on its own, so a dead daemon never leaves Steam's input taken.
|
||||
remove(): the patch layer, the catch-all and the class go. */
|
||||
- Pod only (`lgs pod` with the theme off, rt.podOnly; + > Launch Program > AssPod): no button on
|
||||
Steam's own bar, and no theme CSS, so the fade and the click block of html.lgs-asspod-out come
|
||||
from a small style of this module's own in each window. The rest is the same.
|
||||
remove(): the patch layer, the catch-all, the class and the style go. */
|
||||
let ASSPOD = null;
|
||||
__LGS_RT.define({
|
||||
name: 'asspod',
|
||||
@@ -22,12 +25,12 @@ __LGS_RT.define({
|
||||
flag: 'assPod',
|
||||
install(rt) {
|
||||
const R = rt.react;
|
||||
R.ready();
|
||||
const { React, jsx } = R;
|
||||
const c = R.c || {};
|
||||
const Focusable = c.Focusable || R.Focusable;
|
||||
if (!Focusable) throw new Error('asspod: no Focusable');
|
||||
if (!R.patch.targets.dashboardBar) throw new Error('asspod: no dashboardBar target');
|
||||
const podOnly = !!rt.podOnly;
|
||||
if (!podOnly) {
|
||||
R.ready();
|
||||
if (!(R.c && R.c.Focusable) && !R.Focusable) throw new Error('asspod: no Focusable');
|
||||
if (!R.patch.targets.dashboardBar) throw new Error('asspod: no dashboardBar target');
|
||||
}
|
||||
|
||||
// Steam's gamepad buttons (EGamepadButton)
|
||||
const BTN = { OK: 1, CANCEL: 2, START: 14, STEAM_GUIDE: 27, STEAM_QUICK_MENU: 28 };
|
||||
@@ -35,7 +38,9 @@ __LGS_RT.define({
|
||||
// them too, and pressed the centre button (down) or Menu (left).
|
||||
const MAP = { [BTN.OK]: ['ok', 'right'], [BTN.CANCEL]: ['menu', 'right'], [BTN.START]: ['close', 'right'] };
|
||||
const OUT_CLASS = 'lgs-asspod-out';
|
||||
const ICON = jsx('svg', { viewBox: '0 0 24 24', fill: 'currentColor', 'aria-hidden': 'true', children: [
|
||||
const POD_STYLE = `body { transition: opacity 300ms ease; }
|
||||
html.${OUT_CLASS} body { opacity: 0 !important; pointer-events: none !important; }`;
|
||||
const icon = (jsx) => jsx('svg', { viewBox: '0 0 24 24', fill: 'currentColor', 'aria-hidden': 'true', children: [
|
||||
jsx('path', { d: 'M9 3.6 20 1.4v14.1a3.4 3.4 0 1 1-2-3.1V6.3L11 7.7v10.1A3.4 3.4 0 1 1 9 14.7z' }, 'n'),
|
||||
] });
|
||||
|
||||
@@ -86,8 +91,20 @@ __LGS_RT.define({
|
||||
};
|
||||
S.off.push(rt.bridge.on('asspod', () => { if (S.timer) { S.timer(); S.timer = 0; } check(); }));
|
||||
S.off.push(rt.windows.track((e) => {
|
||||
try { e.doc.documentElement.classList.toggle(OUT_CLASS, S.out); } catch (_) { /* window gone */ }
|
||||
return () => { try { e.doc.documentElement.classList.remove(OUT_CLASS); } catch (_) { /* gone */ } };
|
||||
let style = null;
|
||||
try {
|
||||
e.doc.documentElement.classList.toggle(OUT_CLASS, S.out);
|
||||
if (podOnly) {
|
||||
style = e.doc.createElement('style');
|
||||
style.className = 'lgs-asspod-style';
|
||||
style.textContent = POD_STYLE;
|
||||
(e.doc.head || e.doc.documentElement).appendChild(style);
|
||||
}
|
||||
} catch (_) { /* window gone */ }
|
||||
return () => {
|
||||
try { e.doc.documentElement.classList.remove(OUT_CLASS); } catch (_) { /* gone */ }
|
||||
try { if (style) style.remove(); } catch (_) { /* gone */ }
|
||||
};
|
||||
}));
|
||||
|
||||
const daemon = () => {
|
||||
@@ -107,7 +124,18 @@ __LGS_RT.define({
|
||||
});
|
||||
};
|
||||
|
||||
const api = {
|
||||
status: () => ({ out: S.out, opening: S.opening, daemon: daemon(), sent: S.sent, swallowed: S.swallowed, lastErr: S.lastErr,
|
||||
podOnly, bridge: rt.bridge.get('asspod') || null, patch: S.handle ? { count: S.handle.count, live: S.handle.live } : null,
|
||||
shown: (() => { try { return rt.windows.byKind('bar').some((e) => !!e.doc.querySelector('.lgs-asspod')); } catch (_) { return null; } })() }),
|
||||
open,
|
||||
};
|
||||
if (podOnly) { check(); return api; }
|
||||
|
||||
// ---- the button: Steam's small bar disc in a glass circle of its own
|
||||
const { React, jsx } = R;
|
||||
const Focusable = (R.c && R.c.Focusable) || R.Focusable;
|
||||
const ICON = icon(jsx);
|
||||
const barClasses = () => (R.M && R.M.BarClasses) || {};
|
||||
function AssPodButton() {
|
||||
const [, bump] = React.useState(0);
|
||||
@@ -156,13 +184,7 @@ __LGS_RT.define({
|
||||
}, { optional: true });
|
||||
try { R.patch.rerender(S.handle); } catch (_) { /* shows at the bar's next render */ }
|
||||
check();
|
||||
|
||||
return {
|
||||
status: () => ({ out: S.out, opening: S.opening, daemon: daemon(), sent: S.sent, swallowed: S.swallowed, lastErr: S.lastErr,
|
||||
bridge: rt.bridge.get('asspod') || null, patch: S.handle ? { count: S.handle.count, live: S.handle.live } : null,
|
||||
shown: (() => { try { return rt.windows.byKind('bar').some((e) => !!e.doc.querySelector('.lgs-asspod')); } catch (_) { return null; } })() }),
|
||||
open,
|
||||
};
|
||||
return api;
|
||||
},
|
||||
remove() {
|
||||
const S = ASSPOD;
|
||||
|
||||
@@ -17,7 +17,7 @@ does.
|
||||
--glassd-args "...", --feed, --stay) on
|
||||
python glass.py install sync + add "LiquidAss" to + > Launch Program
|
||||
python glass.py uninstall theme off, remove launcher and files
|
||||
python glass.py on|off|toggle|reload|status
|
||||
python glass.py on|off|toggle|reload|status|pod
|
||||
python glass.py toast "text"
|
||||
python glass.py shot SURF NAME [--route R] [--pre JS] [--theme on|off|keep]
|
||||
[--settle S] [--back]
|
||||
@@ -76,6 +76,7 @@ ROOT = Path(__file__).resolve().parent
|
||||
HELPER = Path.home() / ".claude" / "skills" / "steam-frame-ssh" / "scripts" / "frame_ssh.py"
|
||||
REMOTE = "/home/steamos/.local/share/glass-shell"
|
||||
DESKTOP = "/home/steamos/.local/share/applications/glass-shell.desktop"
|
||||
POD_DESKTOP = "/home/steamos/.local/share/applications/asspod.desktop" # + > Launch Program > AssPod (VLC there)
|
||||
SHOTS = ROOT / "shots"
|
||||
# -B and PYTHONDONTWRITEBYTECODE (REQ P8->P10): lab and lgs commands import lgs, lgs_shell, lab_p2cmd; without them
|
||||
# Python writes device/__pycache__ and lab/__pycache__ on the Frame, which would outlive `lgs off` and a reboot
|
||||
@@ -1161,12 +1162,17 @@ def main(argv):
|
||||
f"chmod 755 {REMOTE}/device/lgs {REMOTE}/device/lgs.py && "
|
||||
f"(update-desktop-database ~/.local/share/applications >/dev/null 2>&1 || true) && "
|
||||
f"echo installed {DESKTOP}")
|
||||
# AssPod in + > Launch Program once the assPod can play (install.sh's pod_launcher)
|
||||
sh(c, f"if command -v vlc >/dev/null 2>&1 || flatpak info org.videolan.VLC >/dev/null 2>&1; then "
|
||||
f"install -Dm644 {REMOTE}/device/asspod.desktop {POD_DESKTOP} && echo installed {POD_DESKTOP}; "
|
||||
f"else rm -f {POD_DESKTOP}; echo 'no VLC: AssPod launcher skipped'; fi; "
|
||||
f"(update-desktop-database ~/.local/share/applications >/dev/null 2>&1 || true)")
|
||||
elif cmd == "uninstall":
|
||||
lgs(c, "off", "--quiet")
|
||||
sh(c, f"rm -f {DESKTOP} && rm -rf {REMOTE} /tmp/lgs \"${{XDG_CONFIG_HOME:-$HOME/.config}}/glass-shell\" "
|
||||
sh(c, f"rm -f {DESKTOP} {POD_DESKTOP} && rm -rf {REMOTE} /tmp/lgs \"${{XDG_CONFIG_HOME:-$HOME/.config}}/glass-shell\" "
|
||||
"&& echo removed")
|
||||
elif cmd in ("on", "off", "toggle", "reload", "status", "toast", "dial"):
|
||||
if cmd in ("on", "reload", "toggle"):
|
||||
elif cmd in ("on", "off", "toggle", "reload", "status", "toast", "dial", "pod"):
|
||||
if cmd in ("on", "reload", "toggle", "pod"):
|
||||
sync(c)
|
||||
return lgs(c, cmd, *rest)
|
||||
elif cmd == "shot":
|
||||
|
||||
+18
-2
@@ -11,6 +11,8 @@
|
||||
# add "LiquidAss" to + > Launch Program. Running it again updates.
|
||||
# The assPod (the music note on the bar) plays through VLC: when VLC
|
||||
# is missing, it offers to install it (Flathub, for this user only).
|
||||
# With VLC in place it also adds "AssPod" to + > Launch Program,
|
||||
# which takes out the assPod with or without the theme.
|
||||
# uninstall turn the theme off, remove the launcher entry, the files and
|
||||
# your saved glass settings.
|
||||
#
|
||||
@@ -26,6 +28,7 @@ ASSET=LiquidAss-Frame.tar.gz
|
||||
DEST=$HOME/.local/share/glass-shell
|
||||
APPS=$HOME/.local/share/applications
|
||||
DESKTOP=$APPS/glass-shell.desktop
|
||||
POD_DESKTOP=$APPS/asspod.desktop
|
||||
CONF=${XDG_CONFIG_HOME:-$HOME/.config}/glass-shell
|
||||
|
||||
say() { printf '%s\n' "LiquidAss: $*"; }
|
||||
@@ -117,6 +120,19 @@ install_app() {
|
||||
fi
|
||||
}
|
||||
|
||||
# + > Launch Program > AssPod: added once the assPod can play (VLC is there), removed when it cannot.
|
||||
pod_launcher() {
|
||||
if has_vlc && [ -f "$DEST/device/asspod.desktop" ]; then
|
||||
mkdir -p "$APPS"
|
||||
sed -e "s|/home/steamos/.local/share/glass-shell|$DEST|g" "$DEST/device/asspod.desktop" > "$POD_DESKTOP"
|
||||
chmod 644 "$POD_DESKTOP"
|
||||
say "added AssPod to + > Launch Program (it works with the theme on or off)"
|
||||
else
|
||||
rm -f "$POD_DESKTOP"
|
||||
fi
|
||||
update-desktop-database "$APPS" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# VLC for the assPod: found (a system vlc or the Flathub app), or offered.
|
||||
has_vlc() {
|
||||
command -v vlc >/dev/null 2>&1 && return 0
|
||||
@@ -162,14 +178,14 @@ uninstall_app() {
|
||||
if [ -f "$DEST/device/lgs.py" ] && command -v python3 >/dev/null 2>&1; then
|
||||
py "$DEST/device/lgs.py" off --quiet >/dev/null 2>&1 || true
|
||||
fi
|
||||
rm -f "$DESKTOP"
|
||||
rm -f "$DESKTOP" "$POD_DESKTOP"
|
||||
update-desktop-database "$APPS" >/dev/null 2>&1 || true
|
||||
rm -rf "$DEST" "$DEST.old" /tmp/lgs "$CONF"
|
||||
say "removed"
|
||||
}
|
||||
|
||||
case "${1:-install}" in
|
||||
install) install_app; check_vlc ;;
|
||||
install) install_app; check_vlc; pod_launcher ;;
|
||||
uninstall) uninstall_app ;;
|
||||
-h|--help|help) sed -n '2,20p' "$0" 2>/dev/null | sed 's/^# \{0,1\}//' || true ;;
|
||||
*) die "unknown command '$1' (use install or uninstall)" ;;
|
||||
|
||||
Reference in new issue
Block a user