kitowall 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +39 -2
- package/dist/core/workshop.js +429 -5
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -131,7 +131,13 @@ Commands:
|
|
|
131
131
|
we library List downloaded workshop items
|
|
132
132
|
we scan-steam Detect Steam workshop folders and list downloaded ids
|
|
133
133
|
we sync-steam Sync local Steam Workshop 431960 items into Kitsune downloads
|
|
134
|
-
we app-status Detect if Wallpaper Engine
|
|
134
|
+
we app-status Detect if Wallpaper Engine and scene engine are installed
|
|
135
|
+
we active Show current livewallpaper authority/lock state
|
|
136
|
+
we apply <id> --monitor <name> [--backend auto|mpvpaper|linux-wallpaperengine]
|
|
137
|
+
Apply live wallpaper on one monitor (auto resolves by wallpaper type)
|
|
138
|
+
we apply --map DP-1:<id1>,HDMI-A-1:<id2> [--backend auto|mpvpaper|linux-wallpaperengine]
|
|
139
|
+
Apply wallpapers in batch by monitor map
|
|
140
|
+
we stop [--monitor <name> | --all] Stop livewallpaper instances and restore previous services
|
|
135
141
|
we coexist enter|exit|status Temporarily stop/restore wallpaper rotation services
|
|
136
142
|
check [--namespace <ns>] [--json] Quick system check (no changes)
|
|
137
143
|
|
|
@@ -418,6 +424,37 @@ async function main() {
|
|
|
418
424
|
console.log(JSON.stringify(out, null, 2));
|
|
419
425
|
return;
|
|
420
426
|
}
|
|
427
|
+
if (action === 'active') {
|
|
428
|
+
const out = (0, workshop_1.workshopActiveStatus)();
|
|
429
|
+
console.log(JSON.stringify(out, null, 2));
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (action === 'apply') {
|
|
433
|
+
const mapValue = cleanOpt(getOptionValue(args, '--map'));
|
|
434
|
+
const backend = cleanOpt(getOptionValue(args, '--backend')) ?? 'auto';
|
|
435
|
+
if (mapValue) {
|
|
436
|
+
const out = await (0, workshop_1.workshopApplyMap)({ map: mapValue, backend });
|
|
437
|
+
console.log(JSON.stringify(out, null, 2));
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const id = cleanOpt(args[2] ?? null);
|
|
441
|
+
const monitor = cleanOpt(getOptionValue(args, '--monitor'));
|
|
442
|
+
if (!id || !monitor) {
|
|
443
|
+
throw new Error('Usage: we apply <id> --monitor <name> [--backend auto|mpvpaper|linux-wallpaperengine] OR we apply --map DP-1:<id1>,HDMI-A-1:<id2>');
|
|
444
|
+
}
|
|
445
|
+
const out = await (0, workshop_1.workshopApply)({ id, monitor, backend });
|
|
446
|
+
console.log(JSON.stringify(out, null, 2));
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (action === 'stop') {
|
|
450
|
+
const monitor = cleanOpt(getOptionValue(args, '--monitor'));
|
|
451
|
+
const out = await (0, workshop_1.workshopStop)({
|
|
452
|
+
monitor,
|
|
453
|
+
all: args.includes('--all') || !monitor
|
|
454
|
+
});
|
|
455
|
+
console.log(JSON.stringify(out, null, 2));
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
421
458
|
if (action === 'coexist') {
|
|
422
459
|
const sub = cleanOpt(args[2] ?? null);
|
|
423
460
|
if (sub === 'enter') {
|
|
@@ -437,7 +474,7 @@ async function main() {
|
|
|
437
474
|
}
|
|
438
475
|
throw new Error('Usage: we coexist <enter|exit|status>');
|
|
439
476
|
}
|
|
440
|
-
throw new Error('Usage: we <config|search|details|download|job|jobs|library|scan-steam|sync-steam|app-status|run-job|coexist> ...');
|
|
477
|
+
throw new Error('Usage: we <config|search|details|download|job|jobs|library|scan-steam|sync-steam|app-status|active|apply|stop|run-job|coexist> ...');
|
|
441
478
|
}
|
|
442
479
|
// Regular commands (need config/state)
|
|
443
480
|
const config = (0, config_1.loadConfig)();
|
package/dist/core/workshop.js
CHANGED
|
@@ -7,18 +7,24 @@ exports.setWorkshopApiKey = setWorkshopApiKey;
|
|
|
7
7
|
exports.workshopGetSteamRoots = workshopGetSteamRoots;
|
|
8
8
|
exports.workshopSetSteamRoots = workshopSetSteamRoots;
|
|
9
9
|
exports.workshopWallpaperEngineStatus = workshopWallpaperEngineStatus;
|
|
10
|
+
exports.workshopSceneEngineStatus = workshopSceneEngineStatus;
|
|
10
11
|
exports.workshopScanSteamDownloads = workshopScanSteamDownloads;
|
|
11
12
|
exports.workshopSyncSteamDownloads = workshopSyncSteamDownloads;
|
|
12
13
|
exports.workshopSearch = workshopSearch;
|
|
13
14
|
exports.workshopDetails = workshopDetails;
|
|
14
15
|
exports.workshopQueueDownload = workshopQueueDownload;
|
|
16
|
+
exports.workshopActiveStatus = workshopActiveStatus;
|
|
17
|
+
exports.workshopSetActive = workshopSetActive;
|
|
15
18
|
exports.workshopCoexistenceEnter = workshopCoexistenceEnter;
|
|
16
19
|
exports.workshopCoexistenceExit = workshopCoexistenceExit;
|
|
17
20
|
exports.workshopCoexistenceStatus = workshopCoexistenceStatus;
|
|
21
|
+
exports.workshopStop = workshopStop;
|
|
18
22
|
exports.workshopRunJob = workshopRunJob;
|
|
19
23
|
exports.workshopGetJob = workshopGetJob;
|
|
20
24
|
exports.workshopListJobs = workshopListJobs;
|
|
21
25
|
exports.workshopLibrary = workshopLibrary;
|
|
26
|
+
exports.workshopApply = workshopApply;
|
|
27
|
+
exports.workshopApplyMap = workshopApplyMap;
|
|
22
28
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
23
29
|
const node_os_1 = __importDefault(require("node:os"));
|
|
24
30
|
const node_path_1 = __importDefault(require("node:path"));
|
|
@@ -40,6 +46,32 @@ function clean(input) {
|
|
|
40
46
|
const v = input.trim();
|
|
41
47
|
return v.length > 0 ? v : undefined;
|
|
42
48
|
}
|
|
49
|
+
function isExecutable(filePath) {
|
|
50
|
+
try {
|
|
51
|
+
node_fs_1.default.accessSync(filePath, node_fs_1.default.constants.X_OK);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function resolveOnPath(binary) {
|
|
59
|
+
const bin = clean(binary);
|
|
60
|
+
if (!bin)
|
|
61
|
+
return undefined;
|
|
62
|
+
if (bin.includes('/')) {
|
|
63
|
+
const resolved = node_path_1.default.resolve(bin);
|
|
64
|
+
return node_fs_1.default.existsSync(resolved) && isExecutable(resolved) ? resolved : undefined;
|
|
65
|
+
}
|
|
66
|
+
const pathEnv = clean(process.env.PATH) ?? '';
|
|
67
|
+
const parts = pathEnv.split(':').filter(Boolean);
|
|
68
|
+
for (const p of parts) {
|
|
69
|
+
const candidate = node_path_1.default.join(p, bin);
|
|
70
|
+
if (node_fs_1.default.existsSync(candidate) && isExecutable(candidate))
|
|
71
|
+
return candidate;
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
43
75
|
function getWePaths() {
|
|
44
76
|
const root = node_path_1.default.join(node_os_1.default.homedir(), '.local', 'share', 'kitsune', 'we');
|
|
45
77
|
return {
|
|
@@ -64,6 +96,7 @@ function ensureWePaths(paths) {
|
|
|
64
96
|
(0, fs_1.ensureDir)(paths.runtime);
|
|
65
97
|
(0, fs_1.ensureDir)(node_path_1.default.join(paths.cache, 'search'));
|
|
66
98
|
(0, fs_1.ensureDir)(node_path_1.default.join(paths.steamcmd, 'logs'));
|
|
99
|
+
(0, fs_1.ensureDir)(node_path_1.default.join(paths.root, 'coexistence', 'snapshots'));
|
|
67
100
|
}
|
|
68
101
|
function getWeConfigPath() {
|
|
69
102
|
return node_path_1.default.join(node_os_1.default.homedir(), '.config', 'kitowall', 'we.json');
|
|
@@ -351,6 +384,67 @@ function writeMetaFile(meta) {
|
|
|
351
384
|
cached_at: now()
|
|
352
385
|
});
|
|
353
386
|
}
|
|
387
|
+
function normalizeWallpaperType(raw) {
|
|
388
|
+
const t = clean(raw)?.toLowerCase();
|
|
389
|
+
if (!t)
|
|
390
|
+
return 'unknown';
|
|
391
|
+
if (t === 'video')
|
|
392
|
+
return 'video';
|
|
393
|
+
if (t === 'scene')
|
|
394
|
+
return 'scene';
|
|
395
|
+
if (t === 'web')
|
|
396
|
+
return 'web';
|
|
397
|
+
if (t === 'application')
|
|
398
|
+
return 'application';
|
|
399
|
+
return 'unknown';
|
|
400
|
+
}
|
|
401
|
+
function detectAudioReactiveFromText(text) {
|
|
402
|
+
return /(audio|spectrum|visualizer|now[\s_-]?playing|media[\s_-]?integration|fft|bass|reactive)/i.test(text);
|
|
403
|
+
}
|
|
404
|
+
function readProjectInfo(dir) {
|
|
405
|
+
const projectPath = node_path_1.default.join(dir, 'project.json');
|
|
406
|
+
if (!node_fs_1.default.existsSync(projectPath)) {
|
|
407
|
+
return { type: 'unknown', audioReactive: false, entry: undefined };
|
|
408
|
+
}
|
|
409
|
+
try {
|
|
410
|
+
const raw = node_fs_1.default.readFileSync(projectPath, 'utf8');
|
|
411
|
+
const json = JSON.parse(raw);
|
|
412
|
+
const type = normalizeWallpaperType(json.type);
|
|
413
|
+
const title = clean(json.title) ?? '';
|
|
414
|
+
const fileEntry = clean(json.file);
|
|
415
|
+
const entry = fileEntry ? node_path_1.default.join(dir, fileEntry) : undefined;
|
|
416
|
+
const audioReactive = detectAudioReactiveFromText(`${title}\n${raw}`);
|
|
417
|
+
return { type, audioReactive, entry };
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
return { type: 'unknown', audioReactive: false, entry: undefined };
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
function inferVideoEntry(dir) {
|
|
424
|
+
if (!node_fs_1.default.existsSync(dir))
|
|
425
|
+
return undefined;
|
|
426
|
+
const files = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
427
|
+
const preferred = ['.mp4', '.webm', '.gif', '.mkv', '.avi', '.mov'];
|
|
428
|
+
for (const ext of preferred) {
|
|
429
|
+
const match = files.find((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(ext));
|
|
430
|
+
if (match)
|
|
431
|
+
return node_path_1.default.join(dir, match.name);
|
|
432
|
+
}
|
|
433
|
+
return undefined;
|
|
434
|
+
}
|
|
435
|
+
function detectTypeFromEntry(entry) {
|
|
436
|
+
const e = clean(entry)?.toLowerCase();
|
|
437
|
+
if (!e)
|
|
438
|
+
return 'unknown';
|
|
439
|
+
if (e.endsWith('.mp4') || e.endsWith('.webm') || e.endsWith('.gif') || e.endsWith('.mkv') || e.endsWith('.avi') || e.endsWith('.mov')) {
|
|
440
|
+
return 'video';
|
|
441
|
+
}
|
|
442
|
+
if (e.endsWith('index.html') || e.endsWith('.html'))
|
|
443
|
+
return 'web';
|
|
444
|
+
if (e.endsWith('scene.json') || e.endsWith('gifscene.json'))
|
|
445
|
+
return 'scene';
|
|
446
|
+
return 'unknown';
|
|
447
|
+
}
|
|
354
448
|
function readLibraryFoldersVdf(vdfPath) {
|
|
355
449
|
if (!node_fs_1.default.existsSync(vdfPath))
|
|
356
450
|
return [];
|
|
@@ -465,13 +559,37 @@ function workshopWallpaperEngineStatus() {
|
|
|
465
559
|
if (node_fs_1.default.existsSync(manifest))
|
|
466
560
|
manifests.push(manifest);
|
|
467
561
|
}
|
|
562
|
+
const engine = workshopSceneEngineStatus();
|
|
468
563
|
return {
|
|
469
564
|
ok: true,
|
|
470
565
|
installed: manifests.length > 0,
|
|
471
566
|
manifests,
|
|
472
|
-
steamapps
|
|
567
|
+
steamapps,
|
|
568
|
+
engine
|
|
473
569
|
};
|
|
474
570
|
}
|
|
571
|
+
function workshopSceneEngineStatus() {
|
|
572
|
+
const configured = clean(process.env.KITOWALL_SCENE_ENGINE_CMD);
|
|
573
|
+
const candidates = configured
|
|
574
|
+
? [configured]
|
|
575
|
+
: ['linux-wallpaperengine', 'linux-wallpaperengine-cli', 'wallpaper-engine'];
|
|
576
|
+
for (const cmd of candidates) {
|
|
577
|
+
const resolved = resolveOnPath(cmd);
|
|
578
|
+
if (!resolved)
|
|
579
|
+
continue;
|
|
580
|
+
const verOut = (0, node_child_process_1.spawnSync)(resolved, ['--version'], { encoding: 'utf8', timeout: 2500 });
|
|
581
|
+
const stdout = clean(verOut.stdout) ?? '';
|
|
582
|
+
const stderr = clean(verOut.stderr) ?? '';
|
|
583
|
+
const version = (stdout.split('\n')[0] || stderr.split('\n')[0] || '').trim();
|
|
584
|
+
return {
|
|
585
|
+
installed: true,
|
|
586
|
+
path: resolved,
|
|
587
|
+
version: version || undefined,
|
|
588
|
+
command: cmd
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
return { installed: false, command: configured ?? 'linux-wallpaperengine' };
|
|
592
|
+
}
|
|
475
593
|
function findPreviewCandidate(dir) {
|
|
476
594
|
if (!node_fs_1.default.existsSync(dir))
|
|
477
595
|
return undefined;
|
|
@@ -543,6 +661,7 @@ function workshopSyncSteamDownloads() {
|
|
|
543
661
|
imported += 1;
|
|
544
662
|
}
|
|
545
663
|
const metadataFile = node_path_1.default.join(paths.metadata, `${id}.json`);
|
|
664
|
+
const info = readProjectInfo(sourceDir);
|
|
546
665
|
if (!node_fs_1.default.existsSync(metadataFile)) {
|
|
547
666
|
const previewCandidate = findPreviewCandidate(sourceDir);
|
|
548
667
|
let thumbLocal;
|
|
@@ -561,9 +680,28 @@ function workshopSyncSteamDownloads() {
|
|
|
561
680
|
tags: [],
|
|
562
681
|
preview_thumb_local: thumbLocal,
|
|
563
682
|
author_name: 'Steam Workshop',
|
|
564
|
-
time_updated: Math.floor(Date.now() / 1000)
|
|
683
|
+
time_updated: Math.floor(Date.now() / 1000),
|
|
684
|
+
wallpaper_type: info.type,
|
|
685
|
+
audio_reactive: info.audioReactive,
|
|
686
|
+
entry: info.entry
|
|
565
687
|
});
|
|
566
688
|
}
|
|
689
|
+
else {
|
|
690
|
+
try {
|
|
691
|
+
const current = JSON.parse(node_fs_1.default.readFileSync(metadataFile, 'utf8'));
|
|
692
|
+
if (!current.wallpaper_type || current.wallpaper_type === 'unknown' || current.audio_reactive === undefined || !current.entry) {
|
|
693
|
+
writeMetaFile({
|
|
694
|
+
...current,
|
|
695
|
+
wallpaper_type: current.wallpaper_type ?? info.type,
|
|
696
|
+
audio_reactive: current.audio_reactive ?? info.audioReactive,
|
|
697
|
+
entry: current.entry ?? info.entry
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
catch {
|
|
702
|
+
// ignore malformed metadata
|
|
703
|
+
}
|
|
704
|
+
}
|
|
567
705
|
}
|
|
568
706
|
}
|
|
569
707
|
return {
|
|
@@ -813,6 +951,78 @@ async function copyDownloadedContent(job) {
|
|
|
813
951
|
function snapshotFile(paths) {
|
|
814
952
|
return node_path_1.default.join(paths.runtime, 'coexistence.snapshot.json');
|
|
815
953
|
}
|
|
954
|
+
function snapshotDir(paths) {
|
|
955
|
+
return node_path_1.default.join(paths.root, 'coexistence', 'snapshots');
|
|
956
|
+
}
|
|
957
|
+
function activeLockFile(paths) {
|
|
958
|
+
return node_path_1.default.join(paths.runtime, 'active.lock');
|
|
959
|
+
}
|
|
960
|
+
function activeStateFile(paths) {
|
|
961
|
+
return node_path_1.default.join(paths.runtime, 'active.json');
|
|
962
|
+
}
|
|
963
|
+
function readActiveState() {
|
|
964
|
+
const paths = getWePaths();
|
|
965
|
+
ensureWePaths(paths);
|
|
966
|
+
const p = activeStateFile(paths);
|
|
967
|
+
if (!node_fs_1.default.existsSync(p))
|
|
968
|
+
return null;
|
|
969
|
+
try {
|
|
970
|
+
return JSON.parse(node_fs_1.default.readFileSync(p, 'utf8'));
|
|
971
|
+
}
|
|
972
|
+
catch {
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
function writeActiveState(state) {
|
|
977
|
+
const paths = getWePaths();
|
|
978
|
+
ensureWePaths(paths);
|
|
979
|
+
(0, fs_1.writeJson)(activeStateFile(paths), state);
|
|
980
|
+
node_fs_1.default.writeFileSync(activeLockFile(paths), `${state.mode}:${state.started_at}\n`, 'utf8');
|
|
981
|
+
}
|
|
982
|
+
function clearActiveState() {
|
|
983
|
+
const paths = getWePaths();
|
|
984
|
+
ensureWePaths(paths);
|
|
985
|
+
try {
|
|
986
|
+
node_fs_1.default.unlinkSync(activeStateFile(paths));
|
|
987
|
+
}
|
|
988
|
+
catch {
|
|
989
|
+
// ignore
|
|
990
|
+
}
|
|
991
|
+
try {
|
|
992
|
+
node_fs_1.default.unlinkSync(activeLockFile(paths));
|
|
993
|
+
}
|
|
994
|
+
catch {
|
|
995
|
+
// ignore
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
function killPid(pid) {
|
|
999
|
+
if (!pid || !Number.isFinite(pid) || pid <= 1)
|
|
1000
|
+
return false;
|
|
1001
|
+
try {
|
|
1002
|
+
process.kill(pid, 'SIGTERM');
|
|
1003
|
+
return true;
|
|
1004
|
+
}
|
|
1005
|
+
catch {
|
|
1006
|
+
return false;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
function workshopActiveStatus() {
|
|
1010
|
+
const paths = getWePaths();
|
|
1011
|
+
ensureWePaths(paths);
|
|
1012
|
+
const state = readActiveState();
|
|
1013
|
+
const lock = node_fs_1.default.existsSync(activeLockFile(paths));
|
|
1014
|
+
return {
|
|
1015
|
+
ok: true,
|
|
1016
|
+
active: lock && !!state,
|
|
1017
|
+
lock_path: activeLockFile(paths),
|
|
1018
|
+
state_path: activeStateFile(paths),
|
|
1019
|
+
state: state ?? undefined
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
function workshopSetActive(state) {
|
|
1023
|
+
writeActiveState(state);
|
|
1024
|
+
return { ok: true, active: true, state };
|
|
1025
|
+
}
|
|
816
1026
|
async function isUnitActive(unit) {
|
|
817
1027
|
try {
|
|
818
1028
|
const out = await (0, exec_1.run)('systemctl', ['--user', 'show', unit, '--property', 'ActiveState', '--value']);
|
|
@@ -839,8 +1049,11 @@ async function workshopCoexistenceEnter() {
|
|
|
839
1049
|
// best effort
|
|
840
1050
|
}
|
|
841
1051
|
}
|
|
842
|
-
|
|
843
|
-
|
|
1052
|
+
const snapshotId = String(now());
|
|
1053
|
+
const snap = { id: snapshotId, ts: now(), active };
|
|
1054
|
+
(0, fs_1.writeJson)(node_path_1.default.join(snapshotDir(paths), `${snapshotId}.json`), snap);
|
|
1055
|
+
(0, fs_1.writeJson)(snapshotFile(paths), { id: snapshotId, ts: snap.ts });
|
|
1056
|
+
return { ok: true, stopped: active, snapshot: active, snapshot_id: snapshotId };
|
|
844
1057
|
}
|
|
845
1058
|
async function workshopCoexistenceExit() {
|
|
846
1059
|
const paths = getWePaths();
|
|
@@ -849,7 +1062,19 @@ async function workshopCoexistenceExit() {
|
|
|
849
1062
|
if (!node_fs_1.default.existsSync(snapPath))
|
|
850
1063
|
return { ok: true, restored: [] };
|
|
851
1064
|
const raw = JSON.parse(node_fs_1.default.readFileSync(snapPath, 'utf8'));
|
|
852
|
-
|
|
1065
|
+
let active = Array.isArray(raw.active) ? raw.active.map(v => String(v)) : [];
|
|
1066
|
+
if ((!active || active.length === 0) && raw.id) {
|
|
1067
|
+
const historical = node_path_1.default.join(snapshotDir(paths), `${raw.id}.json`);
|
|
1068
|
+
if (node_fs_1.default.existsSync(historical)) {
|
|
1069
|
+
try {
|
|
1070
|
+
const snap = JSON.parse(node_fs_1.default.readFileSync(historical, 'utf8'));
|
|
1071
|
+
active = Array.isArray(snap.active) ? snap.active.map(v => String(v)) : [];
|
|
1072
|
+
}
|
|
1073
|
+
catch {
|
|
1074
|
+
active = [];
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
853
1078
|
const restored = [];
|
|
854
1079
|
for (const unit of active) {
|
|
855
1080
|
try {
|
|
@@ -882,6 +1107,46 @@ async function workshopCoexistenceStatus() {
|
|
|
882
1107
|
}
|
|
883
1108
|
return { ok: true, snapshot, current };
|
|
884
1109
|
}
|
|
1110
|
+
async function workshopStop(options) {
|
|
1111
|
+
let state = readActiveState();
|
|
1112
|
+
const hadActive = !!state;
|
|
1113
|
+
const stopped = [];
|
|
1114
|
+
let shouldRestore = false;
|
|
1115
|
+
if (state?.instances) {
|
|
1116
|
+
if (options?.monitor) {
|
|
1117
|
+
const inst = state.instances[options.monitor];
|
|
1118
|
+
if (killPid(inst?.pid))
|
|
1119
|
+
stopped.push(options.monitor);
|
|
1120
|
+
delete state.instances[options.monitor];
|
|
1121
|
+
if (Object.keys(state.instances).length > 0 && !options?.all) {
|
|
1122
|
+
writeActiveState(state);
|
|
1123
|
+
}
|
|
1124
|
+
else {
|
|
1125
|
+
clearActiveState();
|
|
1126
|
+
shouldRestore = true;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
else {
|
|
1130
|
+
for (const [monitor, inst] of Object.entries(state.instances)) {
|
|
1131
|
+
if (killPid(inst?.pid))
|
|
1132
|
+
stopped.push(monitor);
|
|
1133
|
+
}
|
|
1134
|
+
clearActiveState();
|
|
1135
|
+
shouldRestore = true;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
else if (options?.all) {
|
|
1139
|
+
clearActiveState();
|
|
1140
|
+
shouldRestore = true;
|
|
1141
|
+
}
|
|
1142
|
+
const coexist = shouldRestore ? await workshopCoexistenceExit() : { ok: true, restored: [] };
|
|
1143
|
+
return {
|
|
1144
|
+
ok: true,
|
|
1145
|
+
stopped_instances: stopped,
|
|
1146
|
+
restored_units: coexist.restored,
|
|
1147
|
+
had_active: hadActive
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
885
1150
|
async function workshopRunJob(jobId) {
|
|
886
1151
|
const job = readJob(jobId);
|
|
887
1152
|
if (!job)
|
|
@@ -980,6 +1245,16 @@ function workshopLibrary() {
|
|
|
980
1245
|
meta = undefined;
|
|
981
1246
|
}
|
|
982
1247
|
}
|
|
1248
|
+
const info = readProjectInfo(p);
|
|
1249
|
+
if (meta) {
|
|
1250
|
+
meta.wallpaper_type = meta.wallpaper_type ?? info.type;
|
|
1251
|
+
if (meta.audio_reactive === undefined) {
|
|
1252
|
+
meta.audio_reactive = info.audioReactive;
|
|
1253
|
+
}
|
|
1254
|
+
if (!meta.entry) {
|
|
1255
|
+
meta.entry = info.entry;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
983
1258
|
items.push({
|
|
984
1259
|
id,
|
|
985
1260
|
path: p,
|
|
@@ -990,3 +1265,152 @@ function workshopLibrary() {
|
|
|
990
1265
|
items.sort((a, b) => a.id.localeCompare(b.id));
|
|
991
1266
|
return { root: paths.downloads, items };
|
|
992
1267
|
}
|
|
1268
|
+
function spawnMpvpaper(monitor, entry) {
|
|
1269
|
+
return new Promise((resolve, reject) => {
|
|
1270
|
+
const child = (0, node_child_process_1.spawn)('mpvpaper', ['-o', 'no-audio --loop-file=inf', monitor, entry], {
|
|
1271
|
+
detached: true,
|
|
1272
|
+
stdio: 'ignore',
|
|
1273
|
+
env: process.env
|
|
1274
|
+
});
|
|
1275
|
+
let settled = false;
|
|
1276
|
+
const done = (fn) => {
|
|
1277
|
+
if (settled)
|
|
1278
|
+
return;
|
|
1279
|
+
settled = true;
|
|
1280
|
+
fn();
|
|
1281
|
+
};
|
|
1282
|
+
child.once('error', (err) => done(() => reject(err)));
|
|
1283
|
+
setTimeout(() => {
|
|
1284
|
+
done(() => {
|
|
1285
|
+
child.unref();
|
|
1286
|
+
resolve(child.pid ?? 0);
|
|
1287
|
+
});
|
|
1288
|
+
}, 180);
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
function spawnSceneEngine(monitor, wallpaperDir) {
|
|
1292
|
+
const engine = workshopSceneEngineStatus();
|
|
1293
|
+
if (!engine.installed || !engine.path) {
|
|
1294
|
+
throw new Error('Scene engine not installed. Install linux-wallpaperengine first.');
|
|
1295
|
+
}
|
|
1296
|
+
const enginePath = engine.path;
|
|
1297
|
+
return new Promise((resolve, reject) => {
|
|
1298
|
+
const child = (0, node_child_process_1.spawn)(enginePath, ['--screen-root', monitor, '--bg', wallpaperDir], {
|
|
1299
|
+
detached: true,
|
|
1300
|
+
stdio: 'ignore',
|
|
1301
|
+
env: process.env
|
|
1302
|
+
});
|
|
1303
|
+
let settled = false;
|
|
1304
|
+
const done = (fn) => {
|
|
1305
|
+
if (settled)
|
|
1306
|
+
return;
|
|
1307
|
+
settled = true;
|
|
1308
|
+
fn();
|
|
1309
|
+
};
|
|
1310
|
+
child.once('error', (err) => done(() => reject(err)));
|
|
1311
|
+
setTimeout(() => {
|
|
1312
|
+
done(() => {
|
|
1313
|
+
child.unref();
|
|
1314
|
+
resolve(child.pid ?? 0);
|
|
1315
|
+
});
|
|
1316
|
+
}, 220);
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
async function workshopApply(input) {
|
|
1320
|
+
const id = clean(input.id);
|
|
1321
|
+
const monitor = clean(input.monitor);
|
|
1322
|
+
const requestedBackend = clean(input.backend) ?? 'auto';
|
|
1323
|
+
if (!id)
|
|
1324
|
+
throw new Error('id is required');
|
|
1325
|
+
if (!monitor)
|
|
1326
|
+
throw new Error('monitor is required');
|
|
1327
|
+
const paths = getWePaths();
|
|
1328
|
+
ensureWePaths(paths);
|
|
1329
|
+
const dir = node_path_1.default.join(paths.downloads, id);
|
|
1330
|
+
if (!node_fs_1.default.existsSync(dir)) {
|
|
1331
|
+
throw new Error(`Wallpaper not found in downloads: ${id}`);
|
|
1332
|
+
}
|
|
1333
|
+
const project = readProjectInfo(dir);
|
|
1334
|
+
const inferredEntry = project.entry && node_fs_1.default.existsSync(project.entry) ? project.entry : inferVideoEntry(dir);
|
|
1335
|
+
const type = project.type !== 'unknown'
|
|
1336
|
+
? project.type
|
|
1337
|
+
: detectTypeFromEntry(inferredEntry ?? node_path_1.default.join(dir, 'scene.json'));
|
|
1338
|
+
if (type === 'web' || type === 'application' || type === 'unknown') {
|
|
1339
|
+
throw new Error(`Unsupported wallpaper type for apply: ${type}. Supported: video, scene.`);
|
|
1340
|
+
}
|
|
1341
|
+
let state = readActiveState();
|
|
1342
|
+
if (!state) {
|
|
1343
|
+
const coexist = await workshopCoexistenceEnter();
|
|
1344
|
+
state = {
|
|
1345
|
+
mode: 'livewallpaper',
|
|
1346
|
+
started_at: now(),
|
|
1347
|
+
snapshot_id: coexist.snapshot_id,
|
|
1348
|
+
instances: {}
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
const current = state.instances[monitor];
|
|
1352
|
+
if (current?.pid) {
|
|
1353
|
+
killPid(current.pid);
|
|
1354
|
+
}
|
|
1355
|
+
let backend;
|
|
1356
|
+
let pid = 0;
|
|
1357
|
+
if (type === 'video') {
|
|
1358
|
+
if (!(requestedBackend === 'auto' || requestedBackend === 'mpvpaper')) {
|
|
1359
|
+
throw new Error(`Invalid backend for video wallpaper: ${requestedBackend}`);
|
|
1360
|
+
}
|
|
1361
|
+
if (!inferredEntry || !node_fs_1.default.existsSync(inferredEntry)) {
|
|
1362
|
+
throw new Error(`Video entry not found for wallpaper: ${id}`);
|
|
1363
|
+
}
|
|
1364
|
+
backend = 'mpvpaper';
|
|
1365
|
+
pid = await spawnMpvpaper(monitor, inferredEntry).catch((err) => {
|
|
1366
|
+
throw new Error(`Failed to launch mpvpaper: ${err instanceof Error ? err.message : String(err)}`);
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
else if (type === 'scene') {
|
|
1370
|
+
if (!(requestedBackend === 'auto' || requestedBackend === 'linux-wallpaperengine')) {
|
|
1371
|
+
throw new Error(`Invalid backend for scene wallpaper: ${requestedBackend}`);
|
|
1372
|
+
}
|
|
1373
|
+
backend = 'linux-wallpaperengine';
|
|
1374
|
+
pid = await spawnSceneEngine(monitor, dir).catch((err) => {
|
|
1375
|
+
throw new Error(`Failed to launch scene engine: ${err instanceof Error ? err.message : String(err)}`);
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
else {
|
|
1379
|
+
throw new Error(`Unsupported wallpaper type for apply: ${type}`);
|
|
1380
|
+
}
|
|
1381
|
+
if (!pid) {
|
|
1382
|
+
throw new Error(`${backend} started without pid`);
|
|
1383
|
+
}
|
|
1384
|
+
state.instances[monitor] = {
|
|
1385
|
+
id,
|
|
1386
|
+
pid,
|
|
1387
|
+
backend,
|
|
1388
|
+
type
|
|
1389
|
+
};
|
|
1390
|
+
writeActiveState(state);
|
|
1391
|
+
return { ok: true, applied: true, monitor, id, backend, pid, state };
|
|
1392
|
+
}
|
|
1393
|
+
async function workshopApplyMap(input) {
|
|
1394
|
+
const raw = clean(input.map);
|
|
1395
|
+
if (!raw)
|
|
1396
|
+
throw new Error('map is required');
|
|
1397
|
+
const entries = raw.split(',').map(v => v.trim()).filter(Boolean);
|
|
1398
|
+
if (entries.length === 0)
|
|
1399
|
+
throw new Error('map has no entries');
|
|
1400
|
+
const applied = [];
|
|
1401
|
+
for (const pair of entries) {
|
|
1402
|
+
const idx = pair.indexOf(':');
|
|
1403
|
+
if (idx <= 0 || idx === pair.length - 1) {
|
|
1404
|
+
throw new Error(`Invalid map entry: ${pair}. Expected <monitor>:<id>`);
|
|
1405
|
+
}
|
|
1406
|
+
const monitor = pair.slice(0, idx).trim();
|
|
1407
|
+
const id = pair.slice(idx + 1).trim();
|
|
1408
|
+
if (!monitor || !id) {
|
|
1409
|
+
throw new Error(`Invalid map entry: ${pair}. Expected <monitor>:<id>`);
|
|
1410
|
+
}
|
|
1411
|
+
const out = await workshopApply({ id, monitor, backend: input.backend });
|
|
1412
|
+
applied.push({ monitor: out.monitor, id: out.id, backend: out.backend, pid: out.pid });
|
|
1413
|
+
}
|
|
1414
|
+
const state = readActiveState() ?? undefined;
|
|
1415
|
+
return { ok: true, applied, state };
|
|
1416
|
+
}
|