4 Commits
Author SHA1 Message Date
MichaelScottsman c498d29b3d Merge pull request #3 from MichaelScottsman/asspod-plus-shortcut
AssPod in + > Launch Program, with or without the theme
2026-10-10 14:47:07 -04:00
MichaelScottsman 7775b8d683 README: title names the music player too 2026-10-10 14:38:06 -04:00
MichaelScottsman 8b422d1cd6 AssPod in + > Launch Program: the installer adds it once VLC is there, and it opens the assPod with the theme on or off
lgs pod: with the theme on, the bar's asspod.open; with it off, only the assPod's runtime modules
in Steam (rt.podOnly, no liveness watch on the CSS core) and lgs-shell in pod mode (no theme, the
assPod's page script only), which exits once the assPod is away with nothing playing. lgs on
replaces a pod unit with a full one.
2026-10-10 14:37:53 -04:00
MichaelScottsman d19fd83a27 Merge pull request #2 from MichaelScottsman/morepods
assPod: Nano Gen 2 body
2026-10-10 14:10:03 -04:00
9 changed files with 264 additions and 50 deletions

No files matched your search

+6 -3
View File
@@ -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
```
+11
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+10 -4
View File
@@ -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
View File
@@ -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)" ;;