kitowall 2.2.0 → 2.3.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
@@ -132,6 +132,8 @@ Commands:
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
134
  we app-status Detect if Wallpaper Engine (AppID 431960) is installed
135
+ we active Show current livewallpaper authority/lock state
136
+ we stop [--monitor <name> | --all] Stop livewallpaper instances and restore previous services
135
137
  we coexist enter|exit|status Temporarily stop/restore wallpaper rotation services
136
138
  check [--namespace <ns>] [--json] Quick system check (no changes)
137
139
 
@@ -418,6 +420,20 @@ async function main() {
418
420
  console.log(JSON.stringify(out, null, 2));
419
421
  return;
420
422
  }
423
+ if (action === 'active') {
424
+ const out = (0, workshop_1.workshopActiveStatus)();
425
+ console.log(JSON.stringify(out, null, 2));
426
+ return;
427
+ }
428
+ if (action === 'stop') {
429
+ const monitor = cleanOpt(getOptionValue(args, '--monitor'));
430
+ const out = await (0, workshop_1.workshopStop)({
431
+ monitor,
432
+ all: args.includes('--all') || !monitor
433
+ });
434
+ console.log(JSON.stringify(out, null, 2));
435
+ return;
436
+ }
421
437
  if (action === 'coexist') {
422
438
  const sub = cleanOpt(args[2] ?? null);
423
439
  if (sub === 'enter') {
@@ -437,7 +453,7 @@ async function main() {
437
453
  }
438
454
  throw new Error('Usage: we coexist <enter|exit|status>');
439
455
  }
440
- throw new Error('Usage: we <config|search|details|download|job|jobs|library|scan-steam|sync-steam|app-status|run-job|coexist> ...');
456
+ throw new Error('Usage: we <config|search|details|download|job|jobs|library|scan-steam|sync-steam|app-status|active|stop|run-job|coexist> ...');
441
457
  }
442
458
  // Regular commands (need config/state)
443
459
  const config = (0, config_1.loadConfig)();
@@ -12,9 +12,12 @@ exports.workshopSyncSteamDownloads = workshopSyncSteamDownloads;
12
12
  exports.workshopSearch = workshopSearch;
13
13
  exports.workshopDetails = workshopDetails;
14
14
  exports.workshopQueueDownload = workshopQueueDownload;
15
+ exports.workshopActiveStatus = workshopActiveStatus;
16
+ exports.workshopSetActive = workshopSetActive;
15
17
  exports.workshopCoexistenceEnter = workshopCoexistenceEnter;
16
18
  exports.workshopCoexistenceExit = workshopCoexistenceExit;
17
19
  exports.workshopCoexistenceStatus = workshopCoexistenceStatus;
20
+ exports.workshopStop = workshopStop;
18
21
  exports.workshopRunJob = workshopRunJob;
19
22
  exports.workshopGetJob = workshopGetJob;
20
23
  exports.workshopListJobs = workshopListJobs;
@@ -64,6 +67,7 @@ function ensureWePaths(paths) {
64
67
  (0, fs_1.ensureDir)(paths.runtime);
65
68
  (0, fs_1.ensureDir)(node_path_1.default.join(paths.cache, 'search'));
66
69
  (0, fs_1.ensureDir)(node_path_1.default.join(paths.steamcmd, 'logs'));
70
+ (0, fs_1.ensureDir)(node_path_1.default.join(paths.root, 'coexistence', 'snapshots'));
67
71
  }
68
72
  function getWeConfigPath() {
69
73
  return node_path_1.default.join(node_os_1.default.homedir(), '.config', 'kitowall', 'we.json');
@@ -351,6 +355,40 @@ function writeMetaFile(meta) {
351
355
  cached_at: now()
352
356
  });
353
357
  }
358
+ function normalizeWallpaperType(raw) {
359
+ const t = clean(raw)?.toLowerCase();
360
+ if (!t)
361
+ return 'unknown';
362
+ if (t === 'video')
363
+ return 'video';
364
+ if (t === 'scene')
365
+ return 'scene';
366
+ if (t === 'web')
367
+ return 'web';
368
+ if (t === 'application')
369
+ return 'application';
370
+ return 'unknown';
371
+ }
372
+ function detectAudioReactiveFromText(text) {
373
+ return /(audio|spectrum|visualizer|now[\s_-]?playing|media[\s_-]?integration|fft|bass|reactive)/i.test(text);
374
+ }
375
+ function readProjectInfo(dir) {
376
+ const projectPath = node_path_1.default.join(dir, 'project.json');
377
+ if (!node_fs_1.default.existsSync(projectPath)) {
378
+ return { type: 'unknown', audioReactive: false };
379
+ }
380
+ try {
381
+ const raw = node_fs_1.default.readFileSync(projectPath, 'utf8');
382
+ const json = JSON.parse(raw);
383
+ const type = normalizeWallpaperType(json.type);
384
+ const title = clean(json.title) ?? '';
385
+ const audioReactive = detectAudioReactiveFromText(`${title}\n${raw}`);
386
+ return { type, audioReactive };
387
+ }
388
+ catch {
389
+ return { type: 'unknown', audioReactive: false };
390
+ }
391
+ }
354
392
  function readLibraryFoldersVdf(vdfPath) {
355
393
  if (!node_fs_1.default.existsSync(vdfPath))
356
394
  return [];
@@ -543,6 +581,7 @@ function workshopSyncSteamDownloads() {
543
581
  imported += 1;
544
582
  }
545
583
  const metadataFile = node_path_1.default.join(paths.metadata, `${id}.json`);
584
+ const info = readProjectInfo(sourceDir);
546
585
  if (!node_fs_1.default.existsSync(metadataFile)) {
547
586
  const previewCandidate = findPreviewCandidate(sourceDir);
548
587
  let thumbLocal;
@@ -561,9 +600,26 @@ function workshopSyncSteamDownloads() {
561
600
  tags: [],
562
601
  preview_thumb_local: thumbLocal,
563
602
  author_name: 'Steam Workshop',
564
- time_updated: Math.floor(Date.now() / 1000)
603
+ time_updated: Math.floor(Date.now() / 1000),
604
+ wallpaper_type: info.type,
605
+ audio_reactive: info.audioReactive
565
606
  });
566
607
  }
608
+ else {
609
+ try {
610
+ const current = JSON.parse(node_fs_1.default.readFileSync(metadataFile, 'utf8'));
611
+ if (!current.wallpaper_type || current.wallpaper_type === 'unknown' || current.audio_reactive === undefined) {
612
+ writeMetaFile({
613
+ ...current,
614
+ wallpaper_type: current.wallpaper_type ?? info.type,
615
+ audio_reactive: current.audio_reactive ?? info.audioReactive
616
+ });
617
+ }
618
+ }
619
+ catch {
620
+ // ignore malformed metadata
621
+ }
622
+ }
567
623
  }
568
624
  }
569
625
  return {
@@ -813,6 +869,78 @@ async function copyDownloadedContent(job) {
813
869
  function snapshotFile(paths) {
814
870
  return node_path_1.default.join(paths.runtime, 'coexistence.snapshot.json');
815
871
  }
872
+ function snapshotDir(paths) {
873
+ return node_path_1.default.join(paths.root, 'coexistence', 'snapshots');
874
+ }
875
+ function activeLockFile(paths) {
876
+ return node_path_1.default.join(paths.runtime, 'active.lock');
877
+ }
878
+ function activeStateFile(paths) {
879
+ return node_path_1.default.join(paths.runtime, 'active.json');
880
+ }
881
+ function readActiveState() {
882
+ const paths = getWePaths();
883
+ ensureWePaths(paths);
884
+ const p = activeStateFile(paths);
885
+ if (!node_fs_1.default.existsSync(p))
886
+ return null;
887
+ try {
888
+ return JSON.parse(node_fs_1.default.readFileSync(p, 'utf8'));
889
+ }
890
+ catch {
891
+ return null;
892
+ }
893
+ }
894
+ function writeActiveState(state) {
895
+ const paths = getWePaths();
896
+ ensureWePaths(paths);
897
+ (0, fs_1.writeJson)(activeStateFile(paths), state);
898
+ node_fs_1.default.writeFileSync(activeLockFile(paths), `${state.mode}:${state.started_at}\n`, 'utf8');
899
+ }
900
+ function clearActiveState() {
901
+ const paths = getWePaths();
902
+ ensureWePaths(paths);
903
+ try {
904
+ node_fs_1.default.unlinkSync(activeStateFile(paths));
905
+ }
906
+ catch {
907
+ // ignore
908
+ }
909
+ try {
910
+ node_fs_1.default.unlinkSync(activeLockFile(paths));
911
+ }
912
+ catch {
913
+ // ignore
914
+ }
915
+ }
916
+ function killPid(pid) {
917
+ if (!pid || !Number.isFinite(pid) || pid <= 1)
918
+ return false;
919
+ try {
920
+ process.kill(pid, 'SIGTERM');
921
+ return true;
922
+ }
923
+ catch {
924
+ return false;
925
+ }
926
+ }
927
+ function workshopActiveStatus() {
928
+ const paths = getWePaths();
929
+ ensureWePaths(paths);
930
+ const state = readActiveState();
931
+ const lock = node_fs_1.default.existsSync(activeLockFile(paths));
932
+ return {
933
+ ok: true,
934
+ active: lock && !!state,
935
+ lock_path: activeLockFile(paths),
936
+ state_path: activeStateFile(paths),
937
+ state: state ?? undefined
938
+ };
939
+ }
940
+ function workshopSetActive(state) {
941
+ writeActiveState(state);
942
+ return { ok: true, active: true, state };
943
+ }
816
944
  async function isUnitActive(unit) {
817
945
  try {
818
946
  const out = await (0, exec_1.run)('systemctl', ['--user', 'show', unit, '--property', 'ActiveState', '--value']);
@@ -839,8 +967,11 @@ async function workshopCoexistenceEnter() {
839
967
  // best effort
840
968
  }
841
969
  }
842
- (0, fs_1.writeJson)(snapshotFile(paths), { ts: now(), active });
843
- return { ok: true, stopped: active, snapshot: active };
970
+ const snapshotId = String(now());
971
+ const snap = { id: snapshotId, ts: now(), active };
972
+ (0, fs_1.writeJson)(node_path_1.default.join(snapshotDir(paths), `${snapshotId}.json`), snap);
973
+ (0, fs_1.writeJson)(snapshotFile(paths), { id: snapshotId, ts: snap.ts });
974
+ return { ok: true, stopped: active, snapshot: active, snapshot_id: snapshotId };
844
975
  }
845
976
  async function workshopCoexistenceExit() {
846
977
  const paths = getWePaths();
@@ -849,7 +980,19 @@ async function workshopCoexistenceExit() {
849
980
  if (!node_fs_1.default.existsSync(snapPath))
850
981
  return { ok: true, restored: [] };
851
982
  const raw = JSON.parse(node_fs_1.default.readFileSync(snapPath, 'utf8'));
852
- const active = Array.isArray(raw.active) ? raw.active.map(v => String(v)) : [];
983
+ let active = Array.isArray(raw.active) ? raw.active.map(v => String(v)) : [];
984
+ if ((!active || active.length === 0) && raw.id) {
985
+ const historical = node_path_1.default.join(snapshotDir(paths), `${raw.id}.json`);
986
+ if (node_fs_1.default.existsSync(historical)) {
987
+ try {
988
+ const snap = JSON.parse(node_fs_1.default.readFileSync(historical, 'utf8'));
989
+ active = Array.isArray(snap.active) ? snap.active.map(v => String(v)) : [];
990
+ }
991
+ catch {
992
+ active = [];
993
+ }
994
+ }
995
+ }
853
996
  const restored = [];
854
997
  for (const unit of active) {
855
998
  try {
@@ -882,6 +1025,42 @@ async function workshopCoexistenceStatus() {
882
1025
  }
883
1026
  return { ok: true, snapshot, current };
884
1027
  }
1028
+ async function workshopStop(options) {
1029
+ const state = readActiveState();
1030
+ const hadActive = !!state;
1031
+ const stopped = [];
1032
+ if (state?.instances) {
1033
+ if (options?.monitor) {
1034
+ const inst = state.instances[options.monitor];
1035
+ if (killPid(inst?.pid))
1036
+ stopped.push(options.monitor);
1037
+ delete state.instances[options.monitor];
1038
+ if (Object.keys(state.instances).length > 0 && !options?.all) {
1039
+ writeActiveState(state);
1040
+ }
1041
+ else {
1042
+ clearActiveState();
1043
+ }
1044
+ }
1045
+ else {
1046
+ for (const [monitor, inst] of Object.entries(state.instances)) {
1047
+ if (killPid(inst?.pid))
1048
+ stopped.push(monitor);
1049
+ }
1050
+ clearActiveState();
1051
+ }
1052
+ }
1053
+ else if (options?.all) {
1054
+ clearActiveState();
1055
+ }
1056
+ const coexist = await workshopCoexistenceExit();
1057
+ return {
1058
+ ok: true,
1059
+ stopped_instances: stopped,
1060
+ restored_units: coexist.restored,
1061
+ had_active: hadActive
1062
+ };
1063
+ }
885
1064
  async function workshopRunJob(jobId) {
886
1065
  const job = readJob(jobId);
887
1066
  if (!job)
@@ -980,6 +1159,13 @@ function workshopLibrary() {
980
1159
  meta = undefined;
981
1160
  }
982
1161
  }
1162
+ const info = readProjectInfo(p);
1163
+ if (meta) {
1164
+ meta.wallpaper_type = meta.wallpaper_type ?? info.type;
1165
+ if (meta.audio_reactive === undefined) {
1166
+ meta.audio_reactive = info.audioReactive;
1167
+ }
1168
+ }
983
1169
  items.push({
984
1170
  id,
985
1171
  path: p,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kitowall",
3
- "version": "2.2.0",
3
+ "version": "2.3.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",