Commit 9f8d9005 by PLN (Algolia)

feat(tray): 🌊 Launch Gig becomes GIG UP β€” one reconciler, armed not crippled

Un-parks the rewire that was blocked behind the Ardour double-launch (log 041).
The patch saved with that log no longer applied β€” perf-tray.py moved under it in
90afa239 β€” so this is reimplemented against the current file rather than forced;
`git apply --reject` had left the file half-patched and was reset before starting.

PLN, 2026-09-05: "click launch gig started pulsar, not ardour??", then "is our
launch gig now autodoing?".

The old button opened Pulsar and nothing else. That was deliberate β€” #116 says
the hot path must not be able to start the sound by accident β€” but the name
promised a gig, so it read as broken rather than as careful, and the rig's real
launcher (the Bridge's RIG UP, which converges every unit and then opens the
apps) had not been pressed in 6.7 days. A safety rule that makes the safe path
invisible protects nothing; it just moves the launch to a hand-typed command.

So the tray and the Bridge now drive the SAME reconciler, and #116 is honoured
by ARMING instead of by crippling β€” the idiom already proven two menu entries
below on Restart SuperDirt:

- SuperDirt already up  β†’ the press only converges and focuses. One click.
- SuperDirt down        β†’ the press would start the sound. First click arms and
                          says so in a tray notification; a second click, which
                          means reopening the menu, commits. Arming lapses after
                          10 s.
- No confirmation dialog, ever: a modal stealing focus mid-set is its own
  hazard (#136).

"Would this start the sound?" is answered from SuperDirt's unit state, not by
asking the Bridge β€” the cheap local truth, and it keeps an HTTP round trip out
of the front of a button press. The label carries all three states so the
dangerous one announces itself before it is pressed.

The POST runs in a worker thread and is never waited on. A cold converge takes
~100 s, and the reason PLN launched Ardour by hand mid-evening β€” believing RIG UP
had failed β€” was a face that showed a spinner and no window. The Bridge answers
immediately and runs the job in the background; the Bridge page opens right away.
Nothing in the worker touches Qt.

Verified end-to-end, with Ardour already running and SuperDirt up:
  POST /api/rig {"launch_apps":true} β†’ {"ok":true,"status":"started"}
  journal:  pulsar: SPAWNED pid 2779892 (it was down)
            ardour: running β†’ focus
            midimon: running β†’ focus
  ArdourGUI process count: 1 before, 1 after.

That "ardour: running β†’ focus" is the line this whole thread existed to produce:
the last time this path ran it said "launched" while Ardour was up, and the two
instances killed each other over the session lock.

Tray after restart: active, NRestarts=0. Incidentally closes the suspected
perf-tray memory leak β€” systemd's own accounting reports a 59.6M memory peak
over 2h15m wall clock, so the "2.6 GB RSS" in the hand-off was a misread of
virtual size, not a leak. Nothing to fix.
parent 9381e227
......@@ -31,9 +31,12 @@ Design notes
* PyQt5 because it ships in the Arch repos (python-pyqt5) β€” no extra deps.
"""
import json
import subprocess
import sys
import threading
import time
import urllib.request
from pathlib import Path
from PyQt5.QtWidgets import (
......@@ -58,6 +61,7 @@ BRIDGE_URL = "http://127.0.0.1:8773/"
# menu can do, so it gets its own confirm step (see PerfTray.on_sc_clicked).
SC_UNIT = "parvagues-sc"
SC_CONFIRM_MS = 10_000 # how long an armed restart stays armed
GIG_CONFIRM_MS = 10_000 # same, for a GIG UP that would start the sound
SC_READY_S = 30 # measured 2026-08-14: active→responder armed in 27 s
......@@ -311,12 +315,26 @@ class PerfTray:
# not focus a window. It can, through KWin's scripting bus; see
# launchers.focus_window. So this is a top-level entry AND a real focus.
#
# Deliberately editor-only: it does not start SuperCollider, boot Tidal
# or arm Ardour. #116 is the standing rule that the hot path must not be
# able to start the sound by accident β€” one wrong click before a set is
# a stuck scsynth, not a convenience.
self.gig_act = QAction("🌊 Launch Gig", self.menu)
self.gig_act.triggered.connect(lambda: self._launch("pulsar"))
# PLN, 2026-09-05: "click launch gig started pulsar, not ardour??", then
# "is our launch gig now autodoing?".
#
# It opened Pulsar and nothing else, and that was deliberate β€” #116 says
# the hot path must not be able to start the sound by accident. But the
# name promised a gig, so the button read as broken rather than as
# careful, and the rig's real launcher (the Bridge's RIG UP, which
# converges every unit and then opens the apps) had not been pressed in
# 6.7 days. A safety rule that makes the safe path invisible protects
# nothing; it just moves the launch to a hand-typed command.
#
# So this now drives the SAME reconciler as the Bridge, and #116 is
# honoured by ARMING rather than by crippling β€” the idiom already proven
# two entries below on Restart SuperDirt. With SuperDirt already up the
# press only converges and focuses, so it is one click. With it DOWN the
# press would start the sound, so the first click arms and says so and a
# second (which means reopening the menu) commits. No confirmation
# dialog: a modal stealing focus mid-set is its own hazard (#136).
self.gig_act = QAction("🌊 GIG UP", self.menu)
self.gig_act.triggered.connect(self.on_gig_clicked)
self.menu.addAction(self.gig_act)
# PLN, 2026-08-14: "add a restart SC command in parvagues tray gui menu".
......@@ -341,6 +359,7 @@ class PerfTray:
self.sc_act.triggered.connect(self.on_sc_clicked)
self.menu.addAction(self.sc_act)
self._sc_armed = False
self._gig_armed = False
self._sc_restart_at = None # monotonic time of the last restart we fired
self.menu.aboutToShow.connect(self.refresh_sc)
......@@ -478,13 +497,17 @@ class PerfTray:
act.setEnabled(item["available"])
self.rig_menu.setTitle(f"Rig β–Έ {up}/{total} up")
if getattr(self, "gig_act", None) is not None:
pulsar = next((i for i in snap["apps"] if i["key"] == "pulsar"), None)
# Say which of the two things the click will do. "Focus" vs "Open" is
# the difference between a no-op-looking click and a confident one.
self.gig_act.setText("🌊 Launch Gig β€” focus ParVagues"
if pulsar and pulsar["running"]
else "🌊 Launch Gig β€” open ParVagues")
self.gig_act.setEnabled(bool(pulsar and pulsar["available"]))
# Three states, because the click means three different things and the
# dangerous one has to announce itself BEFORE it is pressed.
if self._gig_armed:
self.gig_act.setText("⚠ GIG UP β€” click again to START THE RIG")
else:
try:
sc_up = bool(sc_state()[0])
except Exception:
sc_up = False
self.gig_act.setText("🌊 GIG UP β€” converge & focus" if sc_up
else "🌊 GIG UP β€” boot the rig")
# ------------------------------------------------------------ SuperDirt
#
......@@ -533,6 +556,63 @@ class PerfTray:
self._sc_armed = False
self.refresh_sc()
# ------------------------------------------------------------ GIG UP
def on_gig_clicked(self):
# "Would this press start the sound?" is answered by SuperDirt's unit
# state, not by asking the Bridge: it is the cheap local truth, it is the
# same question #116 is about, and it keeps an HTTP round trip out of the
# front of a button press.
try:
active, _ = sc_state()
except Exception:
active = False
if not active and not self._gig_armed:
self._gig_armed = True
QTimer.singleShot(GIG_CONFIRM_MS, self._disarm_gig)
self.tray.showMessage(
"perf-tray: GIG UP armed",
"SuperDirt is DOWN. This will boot the rig and START THE SOUND.\n"
"Open the menu and click again within 10 s to confirm.",
QSystemTrayIcon.Warning, GIG_CONFIRM_MS,
)
return
self._disarm_gig()
self._rig_up()
def _disarm_gig(self):
self._gig_armed = False
def _rig_up(self):
"""Ask the Bridge to converge the rig, then open it.
POSTed from a worker thread and never waited on: a cold converge takes
~100 s, and the reason PLN launched Ardour by hand mid-evening β€” believing
RIG UP had failed β€” was a face that showed a spinner and no window. The
Bridge answers the POST immediately and runs the job in the background,
so the only thing that must not block is this thread. Nothing in the
worker touches Qt, which is not thread-safe.
"""
self.tray.showMessage(
"perf-tray: GIG UP",
"Converging the rig β€” the Bridge is opening.",
QSystemTrayIcon.Information, 4000,
)
def post():
body = json.dumps({"launch_apps": True}).encode()
req = urllib.request.Request(
BRIDGE_URL + "api/rig", data=body, method="POST",
headers={"Content-Type": "application/json"})
try:
urllib.request.urlopen(req, timeout=10).read()
except Exception as e:
print(f"perf-tray: GIG UP post failed: {e}", file=sys.stderr,
flush=True)
threading.Thread(target=post, daemon=True).start()
self._open_url(BRIDGE_URL)
def _sc_restart(self, was_up):
# QProcess, not subprocess.run: a restart takes a couple of seconds to
# return and the tray must not freeze while it does.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment