Files
Rene KievitsandClaude Opus 5 d3e4cb6410 Restrict the brightness sink to one app, picked by name not typed
Confirmed the brightness command applies globally, not per-LED
(serverinfo showed exactly one adjustment object, "id": "default",
covering the whole string), so no LED-count configuration is needed
for this at all -- that question resolved itself once the mechanism
was actually inspected instead of assumed.

For "only react while Spotify is running": only one app can be in the
foreground on webOS at a time, so a "capture the current app" button
in this app's own UI can never work -- pressing it means this app is
foreground, not Spotify. The only workable UI is picking a target from
every *installed* app by name, regardless of what's currently running.

That needed a new native capability this service never had: calling
OUT to another Luna service, not just being called. Two additions:

  foreground_app.c   subscribes once, at startup, to
                      com.webos.applicationManager/getForegroundAppInfo
                      and keeps a thread-safe cache the audio thread can
                      read without a blocking Luna call
  service.c           new listApps method, bridging to
                      com.webos.applicationManager/listApps so the
                      frontend never has to call another service
                      directly -- same rule as everywhere else here

Until the subscription has delivered at least one reply, a restricted
sink treats the target app as inactive, not active -- reacting to
audio when the user explicitly restricted it to one app would be the
wrong failure mode. Verified end to end on the host: engine_smoke.c
opens the sink with a restriction set, confirms it reports itself
correctly inactive against the stub Luna bus (which always "fails" to
call out, exactly like a real host with no bus).

Needed real, linkable stub bodies for LSCall/LSCallOneReply/
LSCallCancel/LSMessageGetPayload/LSErrorInit/LSErrorFree
(test/stubs/luna-service2/lunaservice_stub.c) since foreground_app.c
is the first source file here that's actually linked into a host test
binary rather than only syntax-checked -- service.c/main.c's existing
stub declarations were never called, only compiled against. Confirmed
those really are the correct symbol names by cross-compiling clean
against the real webOS SDK's actual libluna-service2, not just the
stub.

Bumped to 1.0.5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 15:46:09 +02:00

136 lines
4.2 KiB
JavaScript

// Luna bus access.
//
// Talks to PalmServiceBridge directly rather than pulling in webOSTV.js: it is
// the same object webOSTV.js wraps, it is injected into every webOS app, and
// it means there is no third-party file to keep in sync.
//
// Opened in a desktop browser the bridge is absent, so everything falls back
// to a small mock. That is what makes the UI developable without a TV.
(function (global) {
'use strict';
var SERVICE = 'luna://org.webosbrew.audiocap.service/';
var HBCHANNEL = 'luna://org.webosbrew.hbchannel.service/';
var haveBridge = typeof global.PalmServiceBridge !== 'undefined';
// A call in flight. `cancel()` tears down a subscription.
function Request(bridge) {
this.bridge = bridge;
this.cancelled = false;
}
Request.prototype.cancel = function () {
this.cancelled = true;
if (this.bridge && this.bridge.cancel) {
this.bridge.cancel();
}
};
// Low-level call. `onReply` fires once per reply, so subscriptions keep
// calling it until cancelled.
function call(uri, params, onReply, onError) {
if (!haveBridge) {
return global.LunaMock.call(uri, params, onReply, onError);
}
var bridge = new global.PalmServiceBridge();
var request = new Request(bridge);
bridge.onservicecallback = function (raw) {
if (request.cancelled) {
return;
}
var reply;
try {
reply = JSON.parse(raw);
} catch (e) {
if (onError) {
onError('Malformed reply from ' + uri);
}
return;
}
// Luna reports both its own failures and ours through returnValue.
if (reply.returnValue === false) {
if (onError) {
onError(reply.errorText || reply.errorMessage || 'Call to ' + uri + ' failed');
}
return;
}
if (onReply) {
onReply(reply);
}
};
try {
bridge.call(uri, JSON.stringify(params || {}));
} catch (e) {
if (onError) {
onError(String(e));
}
}
return request;
}
var Luna = {
available: haveBridge,
// --- our service -------------------------------------------------------
start: function (patch, ok, fail) {
return call(SERVICE + 'start', patch || {}, ok, fail);
},
stop: function (ok, fail) {
return call(SERVICE + 'stop', {}, ok, fail);
},
subscribeStatus: function (ok, fail) {
return call(SERVICE + 'getStatus', { subscribe: true }, ok, fail);
},
getConfig: function (ok, fail) {
return call(SERVICE + 'getConfig', {}, ok, fail);
},
setConfig: function (patch, ok, fail) {
return call(SERVICE + 'setConfig', { settings: patch }, ok, fail);
},
resetConfig: function (ok, fail) {
return call(SERVICE + 'resetConfig', {}, ok, fail);
},
listBackends: function (ok, fail) {
return call(SERVICE + 'listBackends', {}, ok, fail);
},
listSinks: function (ok, fail) {
return call(SERVICE + 'listSinks', {}, ok, fail);
},
listApps: function (ok, fail) {
return call(SERVICE + 'listApps', {}, ok, fail);
},
getDiagnostics: function (ok, fail) {
return call(SERVICE + 'getDiagnostics', {}, ok, fail);
},
// Ends the service process. Any later call starts a fresh one, which is
// how the service picks up new bus permissions after being elevated.
quit: function (ok, fail) {
return call(SERVICE + 'quit', {}, ok, fail);
},
// `clear` empties the ring buffer after reading it, so the reply is the
// last thing anyone sees of those lines.
getLogs: function (clear, ok, fail) {
return call(SERVICE + 'getLogs', { clear: !!clear }, ok, fail);
},
// --- Homebrew Channel --------------------------------------------------
// Used for the boot symlink. Needs the Homebrew Channel installed, which
// it will be on any TV that can run this app.
exec: function (command, ok, fail) {
return call(HBCHANNEL + 'exec', { command: command }, ok, fail);
},
// --- system ------------------------------------------------------------
getNetworkStatus: function (ok, fail) {
return call('luna://com.palm.connectionmanager/getStatus', {}, ok, fail);
},
};
global.Luna = Luna;
})(window);