kitowall 3.5.38 → 6.2.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/README.md CHANGED
@@ -35,12 +35,7 @@ node dist/cli.js check --json
35
35
  ```bash
36
36
  cd ui
37
37
  npm install
38
- npm run tauri:dev
39
- ```
40
-
41
- If your system needs it on Wayland:
42
- ```bash
43
- WEBKIT_DISABLE_DMABUF_RENDERER=1 npm run tauri:dev
38
+ npm run electron:dev
44
39
  ```
45
40
 
46
41
  ## AppImage (Recommended)
@@ -81,7 +76,7 @@ npm run release:check
81
76
  # Build distributable CLI tarball
82
77
  npm run package:cli
83
78
 
84
- # Build desktop app package (Tauri)
79
+ # Build desktop app package (Electron)
85
80
  npm run package:ui
86
81
 
87
82
  # Full pipeline
package/dist/cli.js CHANGED
@@ -135,9 +135,9 @@ Commands:
135
135
  we sync-steam Sync local Steam Workshop 431960 items into Kitsune downloads
136
136
  we app-status Detect if Wallpaper Engine is installed in Steam
137
137
  we active Show current livewallpaper authority/lock state
138
- we apply <id> --monitor <name> [--backend auto|mpvpaper]
138
+ we apply <id> --monitor <name>
139
139
  Apply video live wallpaper on one monitor
140
- we apply --map DP-1:<id1>,HDMI-A-1:<id2> [--backend auto|mpvpaper]
140
+ we apply --map DP-1:<id1>,HDMI-A-1:<id2>
141
141
  Apply wallpapers in batch by monitor map
142
142
  we stop [--monitor <name> | --all] Stop livewallpaper instances and restore previous services
143
143
  we coexist enter|exit|status Temporarily stop/restore wallpaper rotation services
@@ -482,7 +482,7 @@ async function main() {
482
482
  const id = cleanOpt(args[2] ?? null);
483
483
  const monitor = cleanOpt(getOptionValue(args, '--monitor'));
484
484
  if (!id || !monitor) {
485
- throw new Error('Usage: we apply <id> --monitor <name> [--backend auto|mpvpaper] OR we apply --map DP-1:<id1>,HDMI-A-1:<id2>');
485
+ throw new Error('Usage: we apply <id> --monitor <name> OR we apply --map DP-1:<id1>,HDMI-A-1:<id2>');
486
486
  }
487
487
  const out = await (0, workshop_1.workshopApply)({ id, monitor, backend });
488
488
  console.log(JSON.stringify(out, null, 2));
package/dist/core/live.js CHANGED
@@ -955,6 +955,31 @@ function integratedRendercoreStart(bin) {
955
955
  code: 0
956
956
  };
957
957
  }
958
+ function integratedRendercoreFailureHint() {
959
+ const logPath = integratedRendercoreLogPath();
960
+ try {
961
+ const raw = node_fs_1.default.readFileSync(logPath, 'utf8').trim();
962
+ if (!raw)
963
+ return '';
964
+ const lines = raw.split('\n');
965
+ return lines.slice(-20).join('\n');
966
+ }
967
+ catch {
968
+ return '';
969
+ }
970
+ }
971
+ async function waitForIntegratedRendercoreReady(bin, timeoutMs = 4000) {
972
+ const until = Date.now() + timeoutMs;
973
+ while (Date.now() < until) {
974
+ const status = integratedRendercoreStatus(bin);
975
+ if (status.code === 0)
976
+ return;
977
+ await new Promise((resolve) => setTimeout(resolve, 150));
978
+ }
979
+ const hint = integratedRendercoreFailureHint();
980
+ throw new Error(`Integrated rendercore failed to stay alive after start (${bin}).` +
981
+ (hint ? ` Recent log:\n${hint}` : ''));
982
+ }
958
983
  function integratedRendercoreStop(bin) {
959
984
  const pid = readIntegratedRendercorePid();
960
985
  if (!pid) {
@@ -1160,6 +1185,7 @@ function ensureRendercoreServiceFiles(bin, defaults) {
1160
1185
  '',
1161
1186
  '[Service]',
1162
1187
  'Type=simple',
1188
+ `WorkingDirectory=${home}`,
1163
1189
  `Environment=PATH=${pathEnv}`,
1164
1190
  `EnvironmentFile=-${envPath}`,
1165
1191
  `ExecStart=${bin}`,
@@ -1194,6 +1220,44 @@ async function runSystemctlUser(args, acceptNonZero = false) {
1194
1220
  throw err;
1195
1221
  }
1196
1222
  }
1223
+ async function importRendercoreSessionEnv() {
1224
+ const keys = [
1225
+ 'WAYLAND_DISPLAY',
1226
+ 'DISPLAY',
1227
+ 'XDG_RUNTIME_DIR',
1228
+ 'HYPRLAND_INSTANCE_SIGNATURE',
1229
+ 'DBUS_SESSION_BUS_ADDRESS'
1230
+ ].filter((key) => clean(process.env[key]).length > 0);
1231
+ if (keys.length === 0)
1232
+ return;
1233
+ try {
1234
+ await runSystemctlUser(['import-environment', ...keys], true);
1235
+ }
1236
+ catch {
1237
+ // best effort
1238
+ }
1239
+ }
1240
+ async function waitForRendercoreServiceActive(timeoutMs = 5000) {
1241
+ const until = Date.now() + timeoutMs;
1242
+ let lastState = '';
1243
+ while (Date.now() < until) {
1244
+ const out = await runSystemctlUser(['show', 'kitsune-rendercore.service', '--property', 'ActiveState,SubState,Result', '--value'], true);
1245
+ const parts = out.stdout
1246
+ .split('\n')
1247
+ .map((line) => line.trim())
1248
+ .filter(Boolean);
1249
+ const [activeState = '', subState = '', result = ''] = parts;
1250
+ lastState = `ActiveState=${activeState} SubState=${subState} Result=${result}`;
1251
+ if (activeState === 'active')
1252
+ return;
1253
+ if (activeState === 'failed')
1254
+ break;
1255
+ await new Promise((resolve) => setTimeout(resolve, 200));
1256
+ }
1257
+ const status = await runSystemctlUser(['status', '--no-pager', 'kitsune-rendercore.service'], true);
1258
+ const detail = [lastState, status.stderr.trim(), status.stdout.trim()].filter(Boolean).join('\n');
1259
+ throw new Error(`kitsune-rendercore.service failed to become active.\n${detail}`);
1260
+ }
1197
1261
  async function resolvePost(provider, pageUrl) {
1198
1262
  const html = await fetchHtml(pageUrl, pageUrl);
1199
1263
  const parsed = liveParsePostFromHtml(provider, pageUrl, html);
@@ -1799,41 +1863,54 @@ async function liveApply(opts) {
1799
1863
  '--monitor', monitor,
1800
1864
  '--video', item.file_path
1801
1865
  ];
1802
- await (0, exec_1.run)(bin, setVideoArgs, { timeoutMs: 120000 });
1803
- // Live mode owns wallpaper state: stop static rotation services while live is active.
1804
- await (0, workshop_1.workshopCoexistenceEnter)();
1805
- // Keep live authority persistent across session restarts.
1806
- if (integratedRendercoreMode()) {
1807
- integratedRendercoreStart(bin);
1866
+ let coexistEntered = false;
1867
+ try {
1868
+ await (0, exec_1.run)(bin, setVideoArgs, { timeoutMs: 120000 });
1869
+ // Keep live authority persistent across session restarts.
1870
+ if (integratedRendercoreMode()) {
1871
+ integratedRendercoreStart(bin);
1872
+ await waitForIntegratedRendercoreReady(bin);
1873
+ }
1874
+ else {
1875
+ const deps = await ensureRendercoreHostRuntimeDeps();
1876
+ if (deps.missing.length > 0) {
1877
+ throw new Error(`Missing host dependencies: ${deps.missing.join(', ')}. ` +
1878
+ 'Install on host (Arch): sudo pacman -S --needed ffmpeg hyprland');
1879
+ }
1880
+ const binPath = await resolveHostExecutablePath(bin);
1881
+ ensureRendercoreServiceFiles(binPath, index.apply_defaults);
1882
+ await importRendercoreSessionEnv();
1883
+ await runSystemctlUser(['daemon-reload']);
1884
+ await runSystemctlUser(['enable', 'kitsune-rendercore.service']);
1885
+ await runSystemctlUser(['start', 'kitsune-rendercore.service']);
1886
+ await waitForRendercoreServiceActive();
1887
+ }
1888
+ // Switch wallpaper authority only after rendercore is confirmed active.
1889
+ await (0, workshop_1.workshopCoexistenceEnter)();
1890
+ coexistEntered = true;
1891
+ withLiveLock(() => {
1892
+ const current = readIndex();
1893
+ const updatedItems = current.items.map(v => (v.id === item.id ? { ...v, last_applied_at: nowUnix() } : v));
1894
+ const perMonitor = { ...current.per_monitor };
1895
+ const currentMon = perMonitor[monitor] || {
1896
+ auto_apply: false,
1897
+ preferred_quality: 'auto',
1898
+ last_applied_id: null
1899
+ };
1900
+ perMonitor[monitor] = {
1901
+ ...currentMon,
1902
+ last_applied_id: item.id
1903
+ };
1904
+ writeIndexAtomic({ ...current, items: updatedItems, per_monitor: perMonitor });
1905
+ return true;
1906
+ });
1808
1907
  }
1809
- else {
1810
- const deps = await ensureRendercoreHostRuntimeDeps();
1811
- if (deps.missing.length > 0) {
1812
- throw new Error(`Missing host dependencies: ${deps.missing.join(', ')}. ` +
1813
- 'Install on host (Arch): sudo pacman -S --needed ffmpeg hyprland');
1908
+ catch (error) {
1909
+ if (coexistEntered) {
1910
+ await (0, workshop_1.workshopCoexistenceExit)().catch(() => undefined);
1814
1911
  }
1815
- const binPath = await resolveHostExecutablePath(bin);
1816
- ensureRendercoreServiceFiles(binPath, index.apply_defaults);
1817
- await runSystemctlUser(['daemon-reload']);
1818
- await runSystemctlUser(['enable', 'kitsune-rendercore.service']);
1819
- await runSystemctlUser(['start', 'kitsune-rendercore.service']);
1912
+ throw error;
1820
1913
  }
1821
- withLiveLock(() => {
1822
- const current = readIndex();
1823
- const updatedItems = current.items.map(v => (v.id === item.id ? { ...v, last_applied_at: nowUnix() } : v));
1824
- const perMonitor = { ...current.per_monitor };
1825
- const currentMon = perMonitor[monitor] || {
1826
- auto_apply: false,
1827
- preferred_quality: 'auto',
1828
- last_applied_id: null
1829
- };
1830
- perMonitor[monitor] = {
1831
- ...currentMon,
1832
- last_applied_id: item.id
1833
- };
1834
- writeIndexAtomic({ ...current, items: updatedItems, per_monitor: perMonitor });
1835
- return true;
1836
- });
1837
1914
  return {
1838
1915
  ok: true,
1839
1916
  id: item.id,
@@ -129,15 +129,11 @@ function getSteamWebApiKey() {
129
129
  function getCoexistServices() {
130
130
  const cfg = readWeConfig();
131
131
  const defaults = [
132
- 'swww-daemon.service',
133
132
  'swww-daemon@kitowall.service',
134
133
  'kitowall-login-apply.service',
135
134
  'kitowall-watch.service',
136
135
  'kitowall-next.service',
137
- 'kitowall-next.timer',
138
- 'hyprwall-watch.service',
139
- 'hyprwall-next.service',
140
- 'hyprwall-next.timer'
136
+ 'kitowall-next.timer'
141
137
  ];
142
138
  const configured = Array.isArray(cfg.coexistServices) ? cfg.coexistServices.map(v => String(v).trim()).filter(Boolean) : [];
143
139
  return configured.length > 0 ? configured : defaults;
@@ -964,7 +960,6 @@ async function stopKnownLiveProcesses() {
964
960
  // Live V2 can run wallpapers without registering pids in workshop active-state.
965
961
  // Stop common runtime processes as best-effort fallback.
966
962
  const patterns = [
967
- 'mpvpaper',
968
963
  'kitsune-livewallpaper',
969
964
  'kitsune-rendercore'
970
965
  ];
@@ -1049,7 +1044,7 @@ async function workshopCoexistenceEnter() {
1049
1044
  const snapshotId = String(now());
1050
1045
  const snap = { id: snapshotId, ts: now(), active, enabled };
1051
1046
  (0, fs_1.writeJson)(node_path_1.default.join(snapshotDir(paths), `${snapshotId}.json`), snap);
1052
- (0, fs_1.writeJson)(snapshotFile(paths), { id: snapshotId, ts: snap.ts });
1047
+ (0, fs_1.writeJson)(snapshotFile(paths), snap);
1053
1048
  return { ok: true, stopped: active, snapshot: active, snapshot_id: snapshotId };
1054
1049
  }
1055
1050
  async function workshopCoexistenceExit() {
@@ -1105,9 +1100,23 @@ async function workshopCoexistenceStatus() {
1105
1100
  const paths = getWePaths();
1106
1101
  ensureWePaths(paths);
1107
1102
  const snapPath = snapshotFile(paths);
1108
- const snapshot = node_fs_1.default.existsSync(snapPath)
1109
- ? (JSON.parse(node_fs_1.default.readFileSync(snapPath, 'utf8')).active ?? [])
1110
- : [];
1103
+ let snapshot = [];
1104
+ if (node_fs_1.default.existsSync(snapPath)) {
1105
+ try {
1106
+ const raw = JSON.parse(node_fs_1.default.readFileSync(snapPath, 'utf8'));
1107
+ snapshot = Array.isArray(raw.active) ? raw.active.map(v => String(v)) : [];
1108
+ if (snapshot.length === 0 && raw.id) {
1109
+ const historical = node_path_1.default.join(snapshotDir(paths), `${raw.id}.json`);
1110
+ if (node_fs_1.default.existsSync(historical)) {
1111
+ const snap = JSON.parse(node_fs_1.default.readFileSync(historical, 'utf8'));
1112
+ snapshot = Array.isArray(snap.active) ? snap.active.map(v => String(v)) : [];
1113
+ }
1114
+ }
1115
+ }
1116
+ catch {
1117
+ snapshot = [];
1118
+ }
1119
+ }
1111
1120
  const units = getCoexistServices();
1112
1121
  const current = {};
1113
1122
  for (const unit of units) {
@@ -1277,29 +1286,6 @@ function workshopLibrary() {
1277
1286
  items.sort((a, b) => a.id.localeCompare(b.id));
1278
1287
  return { root: paths.downloads, items };
1279
1288
  }
1280
- function spawnMpvpaper(monitor, entry) {
1281
- return new Promise((resolve, reject) => {
1282
- const child = (0, node_child_process_1.spawn)('mpvpaper', ['-o', 'no-audio --loop-file=inf', monitor, entry], {
1283
- detached: true,
1284
- stdio: 'ignore',
1285
- env: process.env
1286
- });
1287
- let settled = false;
1288
- const done = (fn) => {
1289
- if (settled)
1290
- return;
1291
- settled = true;
1292
- fn();
1293
- };
1294
- child.once('error', (err) => done(() => reject(err)));
1295
- setTimeout(() => {
1296
- done(() => {
1297
- child.unref();
1298
- resolve(child.pid ?? 0);
1299
- });
1300
- }, 180);
1301
- });
1302
- }
1303
1289
  async function workshopApply(input) {
1304
1290
  const id = clean(input.id);
1305
1291
  const monitor = clean(input.monitor);
@@ -1319,46 +1305,16 @@ async function workshopApply(input) {
1319
1305
  const type = project.type !== 'unknown'
1320
1306
  ? project.type
1321
1307
  : detectTypeFromEntry(inferredEntry ?? node_path_1.default.join(dir, 'scene.json'));
1322
- if (type !== 'video') {
1323
- throw new Error(`Unsupported wallpaper type for apply: ${type}. Supported: video (mpvpaper).`);
1324
- }
1325
- let state = readActiveState();
1326
- if (!state) {
1327
- const coexist = await workshopCoexistenceEnter();
1328
- state = {
1329
- mode: 'livewallpaper',
1330
- started_at: now(),
1331
- snapshot_id: coexist.snapshot_id,
1332
- instances: {}
1333
- };
1334
- }
1335
- const current = state.instances[monitor];
1336
- if (current?.pid) {
1337
- killPid(current.pid);
1308
+ if (requestedBackend !== 'auto') {
1309
+ throw new Error(`Invalid backend for video wallpaper: ${requestedBackend}. Wallpaper Engine apply no longer supports custom backends.`);
1338
1310
  }
1339
- let backend;
1340
- let pid = 0;
1341
- if (!(requestedBackend === 'auto' || requestedBackend === 'mpvpaper')) {
1342
- throw new Error(`Invalid backend for video wallpaper: ${requestedBackend}`);
1311
+ if (type !== 'video') {
1312
+ throw new Error(`Unsupported wallpaper type for apply: ${type}. Only video items can be migrated to the LiveWallpapers library.`);
1343
1313
  }
1344
1314
  if (!inferredEntry || !node_fs_1.default.existsSync(inferredEntry)) {
1345
1315
  throw new Error(`Video entry not found for wallpaper: ${id}`);
1346
1316
  }
1347
- backend = 'mpvpaper';
1348
- pid = await spawnMpvpaper(monitor, inferredEntry).catch((err) => {
1349
- throw new Error(`Failed to launch mpvpaper: ${err instanceof Error ? err.message : String(err)}`);
1350
- });
1351
- if (!pid) {
1352
- throw new Error(`${backend} started without pid`);
1353
- }
1354
- state.instances[monitor] = {
1355
- id,
1356
- pid,
1357
- backend,
1358
- type
1359
- };
1360
- writeActiveState(state);
1361
- return { ok: true, applied: true, monitor, id, backend, pid, state };
1317
+ throw new Error('Wallpaper Engine direct apply was removed. Import the video into LiveWallpapers and apply it with rendercore instead.');
1362
1318
  }
1363
1319
  async function workshopApplyMap(input) {
1364
1320
  const raw = clean(input.map);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kitowall",
3
- "version": "3.5.38",
3
+ "version": "6.2.0",
4
4
  "description": "CLI/daemon for Hyprland wallpapers using swww with pack-based rotation.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -25,8 +25,8 @@
25
25
  "lint": "node dist/cli.js --help",
26
26
  "release:check": "npm run build && npm run test:e2e",
27
27
  "package:cli": "npm pack",
28
- "package:ui": "npm --prefix ui run tauri:build",
29
- "package:appimage": "npm --prefix ui run tauri:build",
28
+ "package:ui": "npm --prefix ui run electron:build",
29
+ "package:appimage": "npm --prefix ui run electron:build",
30
30
  "package:all": "npm run release:check && npm run package:cli && npm run package:ui",
31
31
  "test:smoke": "bash ./tests/smoke.e2e.sh",
32
32
  "test:regression": "bash ./tests/regression.e2e.sh",