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 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 (AppID 431960) is installed
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)();
@@ -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
- (0, fs_1.writeJson)(snapshotFile(paths), { ts: now(), active });
843
- return { ok: true, stopped: active, snapshot: active };
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
- const active = Array.isArray(raw.active) ? raw.active.map(v => String(v)) : [];
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kitowall",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "description": "CLI/daemon for Hyprland wallpapers using swww with pack-based rotation.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "type": "commonjs",