kitowall 3.5.37 → 6.1.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 +2 -7
- package/dist/cli.js +3 -3
- package/dist/core/live.js +109 -32
- package/dist/core/workshop.js +24 -68
- package/package.json +3 -3
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
|
|
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 (
|
|
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>
|
|
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>
|
|
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>
|
|
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
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1866
|
+
let coexistEntered = false;
|
|
1867
|
+
try {
|
|
1868
|
+
await (0, exec_1.run)(bin, setVideoArgs, { timeoutMs: 120000 });
|
|
1869
|
+
// Live mode owns wallpaper state: stop only Kitowall static wallpaper services.
|
|
1870
|
+
await (0, workshop_1.workshopCoexistenceEnter)();
|
|
1871
|
+
coexistEntered = true;
|
|
1872
|
+
// Keep live authority persistent across session restarts.
|
|
1873
|
+
if (integratedRendercoreMode()) {
|
|
1874
|
+
integratedRendercoreStart(bin);
|
|
1875
|
+
await waitForIntegratedRendercoreReady(bin);
|
|
1876
|
+
}
|
|
1877
|
+
else {
|
|
1878
|
+
const deps = await ensureRendercoreHostRuntimeDeps();
|
|
1879
|
+
if (deps.missing.length > 0) {
|
|
1880
|
+
throw new Error(`Missing host dependencies: ${deps.missing.join(', ')}. ` +
|
|
1881
|
+
'Install on host (Arch): sudo pacman -S --needed ffmpeg hyprland');
|
|
1882
|
+
}
|
|
1883
|
+
const binPath = await resolveHostExecutablePath(bin);
|
|
1884
|
+
ensureRendercoreServiceFiles(binPath, index.apply_defaults);
|
|
1885
|
+
await importRendercoreSessionEnv();
|
|
1886
|
+
await runSystemctlUser(['daemon-reload']);
|
|
1887
|
+
await runSystemctlUser(['enable', 'kitsune-rendercore.service']);
|
|
1888
|
+
await runSystemctlUser(['start', 'kitsune-rendercore.service']);
|
|
1889
|
+
await waitForRendercoreServiceActive();
|
|
1890
|
+
}
|
|
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
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
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
|
-
|
|
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,
|
package/dist/core/workshop.js
CHANGED
|
@@ -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),
|
|
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
|
-
|
|
1109
|
-
|
|
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 (
|
|
1323
|
-
throw new Error(`
|
|
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
|
-
|
|
1340
|
-
|
|
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
|
-
|
|
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
|
+
"version": "6.1.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
|
|
29
|
-
"package:appimage": "npm --prefix ui run
|
|
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",
|