kitowall 7.7.0 → 8.0.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 +13 -1
- package/dist/core/config.js +6 -0
- package/dist/core/configValidator.js +6 -0
- package/dist/core/controller.js +2 -2
- package/dist/core/doctor.js +22 -15
- package/dist/core/init.js +31 -25
- package/dist/core/outputs.js +38 -0
- package/dist/core/setup.js +102 -11
- package/dist/core/wallpaperBackend.js +58 -0
- package/dist/core/workshop.js +1 -1
- package/dist/managers/awww.js +55 -0
- package/dist/managers/types.js +2 -0
- package/package.json +4 -2
- package/scripts/bootstrap-host.sh +552 -0
- package/scripts/bootstrap-system.sh +187 -0
package/dist/cli.js
CHANGED
|
@@ -199,6 +199,8 @@ Host Setup:
|
|
|
199
199
|
Show local/latest versions for kitowall, kitsune, and rendercore
|
|
200
200
|
host-setup install <id> [--namespace <ns>]
|
|
201
201
|
Install, reinstall, or repair one host item
|
|
202
|
+
host-setup purge [--namespace <ns>] --yes
|
|
203
|
+
Remove kitowall, kitsune, rendercore, configs, services, state, and profiles
|
|
202
204
|
Examples:
|
|
203
205
|
kitowall init --namespace kitowall --apply
|
|
204
206
|
kitowall install-systemd --every 5m
|
|
@@ -398,7 +400,17 @@ async function main() {
|
|
|
398
400
|
process.exitCode = result.ok ? 0 : (result.code || 1);
|
|
399
401
|
return;
|
|
400
402
|
}
|
|
401
|
-
|
|
403
|
+
if (action === 'purge') {
|
|
404
|
+
if (!args.includes('--yes')) {
|
|
405
|
+
throw new Error('Usage: host-setup purge [--namespace <ns>] --yes');
|
|
406
|
+
}
|
|
407
|
+
const result = await (0, setup_1.purgeSetup)(namespace);
|
|
408
|
+
if (result.logs.trim())
|
|
409
|
+
process.stdout.write(result.logs);
|
|
410
|
+
process.exitCode = result.ok ? 0 : (result.code || 1);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
throw new Error('Usage: host-setup <check dependency <id>|check service <id>|list|versions|install <id>|purge --yes> [--namespace <ns>] [--json]');
|
|
402
414
|
}
|
|
403
415
|
if (cmd === 'we') {
|
|
404
416
|
const action = cleanOpt(args[1] ?? null);
|
package/dist/core/config.js
CHANGED
|
@@ -79,6 +79,7 @@ function defaultConfig() {
|
|
|
79
79
|
return {
|
|
80
80
|
schemaVersion: exports.CONFIG_SCHEMA_VERSION,
|
|
81
81
|
mode: 'manual',
|
|
82
|
+
wallpaper_backend: 'auto',
|
|
82
83
|
rotation_interval_seconds: 1800,
|
|
83
84
|
transition: { type: 'center', fps: 60, duration: 0.7 },
|
|
84
85
|
selection: {
|
|
@@ -124,6 +125,11 @@ function loadConfig() {
|
|
|
124
125
|
}
|
|
125
126
|
if (!config.transition)
|
|
126
127
|
config.transition = fallback.transition;
|
|
128
|
+
if (config.wallpaper_backend !== 'auto' &&
|
|
129
|
+
config.wallpaper_backend !== 'swww' &&
|
|
130
|
+
config.wallpaper_backend !== 'awww') {
|
|
131
|
+
config.wallpaper_backend = fallback.wallpaper_backend;
|
|
132
|
+
}
|
|
127
133
|
if (!config.packs)
|
|
128
134
|
config.packs = {};
|
|
129
135
|
if (!config.selection)
|
|
@@ -107,6 +107,12 @@ function validateConfig(config) {
|
|
|
107
107
|
if (config.mode !== 'manual' && config.mode !== 'rotate') {
|
|
108
108
|
errors.push('mode must be manual|rotate');
|
|
109
109
|
}
|
|
110
|
+
if (config.wallpaper_backend !== undefined &&
|
|
111
|
+
config.wallpaper_backend !== 'auto' &&
|
|
112
|
+
config.wallpaper_backend !== 'swww' &&
|
|
113
|
+
config.wallpaper_backend !== 'awww') {
|
|
114
|
+
errors.push('wallpaper_backend must be auto|swww|awww');
|
|
115
|
+
}
|
|
110
116
|
if (!positiveNumber(config.rotation_interval_seconds)) {
|
|
111
117
|
errors.push('rotation_interval_seconds must be > 0');
|
|
112
118
|
}
|
package/dist/core/controller.js
CHANGED
|
@@ -10,7 +10,6 @@ const state_1 = require("./state");
|
|
|
10
10
|
const history_1 = require("./history");
|
|
11
11
|
const outputs_1 = require("./outputs");
|
|
12
12
|
const localFolder_1 = require("../adapters/localFolder");
|
|
13
|
-
const swww_1 = require("../managers/swww");
|
|
14
13
|
const selector_1 = require("../adapters/selector");
|
|
15
14
|
const cache_1 = require("./cache");
|
|
16
15
|
const fs_1 = __importDefault(require("fs"));
|
|
@@ -21,6 +20,7 @@ const reddit_1 = require("../adapters/reddit");
|
|
|
21
20
|
const wallhaven_1 = require("../adapters/wallhaven");
|
|
22
21
|
const unsplash_1 = require("../adapters/unsplash");
|
|
23
22
|
const staticUrl_1 = require("../adapters/staticUrl");
|
|
23
|
+
const wallpaperBackend_1 = require("./wallpaperBackend");
|
|
24
24
|
class Controller {
|
|
25
25
|
constructor(config, state) {
|
|
26
26
|
this.config = config;
|
|
@@ -356,7 +356,7 @@ class Controller {
|
|
|
356
356
|
await hydrate(item.path);
|
|
357
357
|
}
|
|
358
358
|
}
|
|
359
|
-
await (0,
|
|
359
|
+
await (0, wallpaperBackend_1.applyWallpaperBackend)(this.config, outputImages, namespace);
|
|
360
360
|
const now = Date.now();
|
|
361
361
|
// Commit State B (recents + last_set + anti-growth)
|
|
362
362
|
for (const item of outputImages) {
|
package/dist/core/doctor.js
CHANGED
|
@@ -4,6 +4,8 @@ exports.getHealth = getHealth;
|
|
|
4
4
|
exports.printDoctor = printDoctor;
|
|
5
5
|
// src/core/doctor.ts
|
|
6
6
|
const exec_1 = require("../utils/exec");
|
|
7
|
+
const config_1 = require("./config");
|
|
8
|
+
const wallpaperBackend_1 = require("./wallpaperBackend");
|
|
7
9
|
const ENABLED_STATES = new Set(['enabled', 'enabled-runtime', 'static', 'generated']);
|
|
8
10
|
const ACTIVE_STATES = new Set(['active', 'activating']);
|
|
9
11
|
function toText(out) {
|
|
@@ -27,7 +29,7 @@ async function which(cmd) {
|
|
|
27
29
|
}
|
|
28
30
|
async function systemctlExists(unit) {
|
|
29
31
|
try {
|
|
30
|
-
await (0, exec_1.run)('systemctl', ['--user', 'cat', unit]);
|
|
32
|
+
await (0, exec_1.run)('systemctl', ['--user', 'cat', unit], { timeoutMs: 1500 });
|
|
31
33
|
return true;
|
|
32
34
|
}
|
|
33
35
|
catch {
|
|
@@ -36,7 +38,7 @@ async function systemctlExists(unit) {
|
|
|
36
38
|
}
|
|
37
39
|
async function systemctlIsEnabled(unit) {
|
|
38
40
|
try {
|
|
39
|
-
const out = await (0, exec_1.run)('systemctl', ['--user', 'is-enabled', unit]);
|
|
41
|
+
const out = await (0, exec_1.run)('systemctl', ['--user', 'is-enabled', unit], { timeoutMs: 1500 });
|
|
40
42
|
const raw = toText(out).trim();
|
|
41
43
|
return { enabled: ENABLED_STATES.has(raw), raw };
|
|
42
44
|
}
|
|
@@ -48,7 +50,7 @@ async function systemctlIsEnabled(unit) {
|
|
|
48
50
|
}
|
|
49
51
|
async function systemctlIsActive(unit) {
|
|
50
52
|
try {
|
|
51
|
-
const out = await (0, exec_1.run)('systemctl', ['--user', 'is-active', unit]);
|
|
53
|
+
const out = await (0, exec_1.run)('systemctl', ['--user', 'is-active', unit], { timeoutMs: 1500 });
|
|
52
54
|
const raw = toText(out).trim();
|
|
53
55
|
return { active: ACTIVE_STATES.has(raw), raw };
|
|
54
56
|
}
|
|
@@ -100,8 +102,10 @@ async function unitStatus(unit) {
|
|
|
100
102
|
return st;
|
|
101
103
|
}
|
|
102
104
|
async function swwwNamespaceQuery(namespace) {
|
|
105
|
+
const backend = await (0, wallpaperBackend_1.resolveWallpaperBackend)((0, config_1.loadConfig)());
|
|
106
|
+
const info = (0, wallpaperBackend_1.wallpaperBackendInfo)(backend);
|
|
103
107
|
try {
|
|
104
|
-
await (0, exec_1.run)(
|
|
108
|
+
await (0, exec_1.run)(info.bin, [...info.queryArgs, '--namespace', namespace]);
|
|
105
109
|
return { ok: true };
|
|
106
110
|
}
|
|
107
111
|
catch (e) {
|
|
@@ -110,22 +114,24 @@ async function swwwNamespaceQuery(namespace) {
|
|
|
110
114
|
}
|
|
111
115
|
async function getHealth(namespace) {
|
|
112
116
|
const hints = [];
|
|
117
|
+
const backend = await (0, wallpaperBackend_1.resolveWallpaperBackend)((0, config_1.loadConfig)());
|
|
118
|
+
const backendInfo = (0, wallpaperBackend_1.wallpaperBackendInfo)(backend);
|
|
113
119
|
const deps = {
|
|
114
|
-
swww: await which(
|
|
115
|
-
swwwDaemon: await which(
|
|
120
|
+
swww: await which(backendInfo.bin),
|
|
121
|
+
swwwDaemon: await which(backendInfo.daemonBin),
|
|
116
122
|
hyprctl: await which('hyprctl'),
|
|
117
123
|
systemctlUser: await which('systemctl'),
|
|
118
124
|
};
|
|
119
125
|
if (!deps.swww)
|
|
120
|
-
hints.push(
|
|
126
|
+
hints.push(`Missing dependency: ${backendInfo.bin}`);
|
|
121
127
|
if (!deps.swwwDaemon)
|
|
122
|
-
hints.push(
|
|
128
|
+
hints.push(`Missing dependency: ${backendInfo.daemonBin}`);
|
|
123
129
|
if (!deps.hyprctl)
|
|
124
130
|
hints.push('Missing dependency: hyprctl');
|
|
125
131
|
if (!deps.systemctlUser)
|
|
126
132
|
hints.push('Missing dependency: systemctl');
|
|
127
133
|
const units = {
|
|
128
|
-
swwwDaemonNs: await unitStatus(
|
|
134
|
+
swwwDaemonNs: await unitStatus(`${backendInfo.daemonUnitBase}@${namespace}.service`),
|
|
129
135
|
watch: await unitStatus('kitowall-watch.service'),
|
|
130
136
|
nextService: await unitStatus('kitowall-next.service'),
|
|
131
137
|
nextTimer: await unitStatus('kitowall-next.timer'),
|
|
@@ -151,7 +157,7 @@ async function getHealth(namespace) {
|
|
|
151
157
|
// solo nos importa que exista
|
|
152
158
|
const swww = await swwwNamespaceQuery(namespace);
|
|
153
159
|
if (!swww.ok)
|
|
154
|
-
hints.push(
|
|
160
|
+
hints.push(`${backendInfo.bin} query failed for namespace "${namespace}"`);
|
|
155
161
|
const depsOk = deps.swww && deps.swwwDaemon && deps.hyprctl && deps.systemctlUser;
|
|
156
162
|
let code;
|
|
157
163
|
if (!deps.swww || !deps.swwwDaemon || !deps.hyprctl || !deps.systemctlUser) {
|
|
@@ -179,17 +185,18 @@ async function getHealth(namespace) {
|
|
|
179
185
|
units.nextTimer.enabled === true &&
|
|
180
186
|
units.nextTimer.active === true &&
|
|
181
187
|
swww.ok;
|
|
182
|
-
return { ok, code, namespace, deps, units, swww: { namespaceQueryOk: swww.ok, error: swww.error }, hints };
|
|
188
|
+
return { ok, code, namespace, backend, deps, units, swww: { namespaceQueryOk: swww.ok, error: swww.error }, hints };
|
|
183
189
|
}
|
|
184
190
|
async function printDoctor(namespace) {
|
|
185
191
|
const r = await getHealth(namespace);
|
|
186
192
|
const line = (s) => console.log(s);
|
|
187
193
|
const badge = (b) => (b ? '✅' : '❌');
|
|
188
194
|
line(`kitowall doctor (namespace="${namespace}")`);
|
|
195
|
+
line(`wallpaper backend: ${r.backend}`);
|
|
189
196
|
line('');
|
|
190
197
|
line('Dependencies:');
|
|
191
|
-
line(` ${badge(r.deps.swww)}
|
|
192
|
-
line(` ${badge(r.deps.swwwDaemon)}
|
|
198
|
+
line(` ${badge(r.deps.swww)} ${r.backend}`);
|
|
199
|
+
line(` ${badge(r.deps.swwwDaemon)} ${r.backend}-daemon`);
|
|
193
200
|
line(` ${badge(r.deps.hyprctl)} hyprctl`);
|
|
194
201
|
line(` ${badge(r.deps.systemctlUser)} systemctl (--user)`);
|
|
195
202
|
line('');
|
|
@@ -206,8 +213,8 @@ async function printDoctor(namespace) {
|
|
|
206
213
|
line(` ${badge(isOk)} ${u.name} (${ex}, ${act}/${sub}, ${en}, ${ac})`);
|
|
207
214
|
}
|
|
208
215
|
line('');
|
|
209
|
-
line(
|
|
210
|
-
line(` ${badge(r.swww.namespaceQueryOk)}
|
|
216
|
+
line(`${r.backend} namespace:`);
|
|
217
|
+
line(` ${badge(r.swww.namespaceQueryOk)} ${r.backend} query --namespace ${namespace}`);
|
|
211
218
|
if (!r.swww.namespaceQueryOk && r.swww.error)
|
|
212
219
|
line(` error: ${r.swww.error}`);
|
|
213
220
|
line('');
|
package/dist/core/init.js
CHANGED
|
@@ -9,6 +9,7 @@ const exec_1 = require("../utils/exec");
|
|
|
9
9
|
const config_1 = require("./config");
|
|
10
10
|
const state_1 = require("./state");
|
|
11
11
|
const controller_1 = require("./controller");
|
|
12
|
+
const wallpaperBackend_1 = require("./wallpaperBackend");
|
|
12
13
|
function ensureDir(p) {
|
|
13
14
|
(0, node_fs_1.mkdirSync)(p, { recursive: true });
|
|
14
15
|
}
|
|
@@ -16,20 +17,17 @@ function esc(a) {
|
|
|
16
17
|
// ExecStart=... necesita escapado simple. JSON.stringify funciona bien para espacios/comillas.
|
|
17
18
|
return JSON.stringify(a);
|
|
18
19
|
}
|
|
19
|
-
async function runHostShell(cmd) {
|
|
20
|
-
return (0, exec_1.run)('sh', ['-lc', cmd]);
|
|
21
|
-
}
|
|
22
20
|
async function hostCmdExists(cmd) {
|
|
23
21
|
try {
|
|
24
|
-
await
|
|
22
|
+
await (0, exec_1.run)('which', [cmd], { timeoutMs: 1500 });
|
|
25
23
|
return true;
|
|
26
24
|
}
|
|
27
25
|
catch {
|
|
28
26
|
return false;
|
|
29
27
|
}
|
|
30
28
|
}
|
|
31
|
-
async function ensureHostDeps() {
|
|
32
|
-
const required = [
|
|
29
|
+
async function ensureHostDeps(wallpaperBackend) {
|
|
30
|
+
const required = [wallpaperBackend.bin, wallpaperBackend.daemonBin, 'hyprctl'];
|
|
33
31
|
const missing = [];
|
|
34
32
|
for (const dep of required) {
|
|
35
33
|
if (!(await hostCmdExists(dep)))
|
|
@@ -38,17 +36,22 @@ async function ensureHostDeps() {
|
|
|
38
36
|
if (missing.length === 0)
|
|
39
37
|
return;
|
|
40
38
|
const pkgSet = new Set();
|
|
41
|
-
if (missing.includes(
|
|
42
|
-
pkgSet.add(
|
|
39
|
+
if (missing.includes(wallpaperBackend.bin) || missing.includes(wallpaperBackend.daemonBin)) {
|
|
40
|
+
pkgSet.add(wallpaperBackend.packageName);
|
|
41
|
+
}
|
|
43
42
|
if (missing.includes('hyprctl'))
|
|
44
43
|
pkgSet.add('hyprland');
|
|
45
44
|
const packages = Array.from(pkgSet);
|
|
46
45
|
// Best effort auto-install on Arch host; if it fails, we keep a clear actionable error.
|
|
47
46
|
if (packages.length > 0 && await hostCmdExists('pacman')) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
if (await hostCmdExists('sudo')) {
|
|
48
|
+
await (0, exec_1.run)('sudo', ['-n', 'pacman', '-S', '--needed', '--noconfirm', ...packages]).catch(async () => {
|
|
49
|
+
await (0, exec_1.run)('sudo', ['pacman', '-S', '--needed', ...packages]).catch(() => { });
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
await (0, exec_1.run)('pacman', ['-S', '--needed', ...packages]).catch(() => { });
|
|
54
|
+
}
|
|
52
55
|
}
|
|
53
56
|
const stillMissing = [];
|
|
54
57
|
for (const dep of required) {
|
|
@@ -57,7 +60,7 @@ async function ensureHostDeps() {
|
|
|
57
60
|
}
|
|
58
61
|
if (stillMissing.length > 0) {
|
|
59
62
|
throw new Error(`Missing host dependencies: ${stillMissing.join(', ')}. ` +
|
|
60
|
-
`Install on host (Arch): sudo pacman -S --needed
|
|
63
|
+
`Install on host (Arch): sudo pacman -S --needed ${wallpaperBackend.packageName} hyprland`);
|
|
61
64
|
}
|
|
62
65
|
}
|
|
63
66
|
async function disableIfExists(unit) {
|
|
@@ -68,6 +71,8 @@ async function detectAndHandleConflicts(force) {
|
|
|
68
71
|
// Conflictos directos con swww (estos sí o sí)
|
|
69
72
|
await disableIfExists('swww.service');
|
|
70
73
|
await disableIfExists('swww-daemon.service');
|
|
74
|
+
await disableIfExists('awww.service');
|
|
75
|
+
await disableIfExists('awww-daemon.service');
|
|
71
76
|
// Otros gestores de wallpaper (solo si force)
|
|
72
77
|
if (!force)
|
|
73
78
|
return;
|
|
@@ -85,10 +90,11 @@ async function detectAndHandleConflicts(force) {
|
|
|
85
90
|
async function initKitowall(opts) {
|
|
86
91
|
const config = (0, config_1.loadConfig)(); // crea/migra config si hace falta
|
|
87
92
|
const state = (0, state_1.loadState)(); // crea/migra state si hace falta
|
|
93
|
+
const wallpaperBackend = (0, wallpaperBackend_1.wallpaperBackendInfo)(await (0, wallpaperBackend_1.resolveWallpaperBackend)(config));
|
|
88
94
|
const ns = (opts.namespace && opts.namespace.trim()) ? opts.namespace.trim() : 'kitowall';
|
|
89
95
|
const force = !!opts.force;
|
|
90
96
|
// Validaciones mínimas
|
|
91
|
-
await ensureHostDeps();
|
|
97
|
+
await ensureHostDeps(wallpaperBackend);
|
|
92
98
|
// Apagar servicios que pisan el wallpaper
|
|
93
99
|
await detectAndHandleConflicts(force);
|
|
94
100
|
const userDir = (0, node_path_1.join)((0, node_os_1.homedir)(), '.config', 'systemd', 'user');
|
|
@@ -110,10 +116,10 @@ async function initKitowall(opts) {
|
|
|
110
116
|
const waylandBootstrap = 'WAYLAND_DISPLAY="${WAYLAND_DISPLAY:-$(ls \\"$XDG_RUNTIME_DIR\\"/wayland-* 2>/dev/null | xargs -r -n1 basename | sort | tail -n1)}"; ' +
|
|
111
117
|
'if [ -z "$WAYLAND_DISPLAY" ]; then WAYLAND_DISPLAY=wayland-1; fi; ' +
|
|
112
118
|
'export WAYLAND_DISPLAY;';
|
|
113
|
-
// 1)
|
|
119
|
+
// 1) wallpaper daemon template
|
|
114
120
|
const swwwDaemonTemplate = `
|
|
115
121
|
[Unit]
|
|
116
|
-
Description
|
|
122
|
+
Description=${wallpaperBackend.daemonBin} wallpaper daemon (namespace %i)
|
|
117
123
|
After=graphical-session.target
|
|
118
124
|
Wants=graphical-session.target
|
|
119
125
|
|
|
@@ -121,22 +127,22 @@ Wants=graphical-session.target
|
|
|
121
127
|
Type=simple
|
|
122
128
|
Environment=PATH=${pathEnv}
|
|
123
129
|
Environment=XDG_RUNTIME_DIR=${xdgRuntimeDir}
|
|
124
|
-
ExecStart=/bin/sh -lc ${esc(`${waylandBootstrap} exec
|
|
130
|
+
ExecStart=/bin/sh -lc ${esc(`${waylandBootstrap} exec ${wallpaperBackend.daemonBin} --no-cache --namespace %i${wallpaperBackend.name === 'awww' ? ' --layer background' : ''}`)}
|
|
125
131
|
Restart=on-failure
|
|
126
132
|
RestartSec=1
|
|
127
133
|
|
|
128
134
|
[Install]
|
|
129
135
|
WantedBy=default.target
|
|
130
136
|
`.trimStart();
|
|
131
|
-
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(userDir,
|
|
137
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(userDir, `${wallpaperBackend.daemonUnitBase}@.service`), swwwDaemonTemplate, 'utf8');
|
|
132
138
|
// 2) kitowall-next.service (oneshot)
|
|
133
139
|
// OJO: aunque CLI ignore --namespace en algunos comandos, aquí lo dejamos por compatibilidad.
|
|
134
140
|
const nextExec = `/bin/sh -lc ${esc(`${waylandBootstrap} exec ${cliInvoke} next --namespace ${JSON.stringify(ns)}`)}`;
|
|
135
141
|
const kitowallNextService = `
|
|
136
142
|
[Unit]
|
|
137
143
|
Description=Kitowall apply next wallpapers
|
|
138
|
-
After
|
|
139
|
-
Requires
|
|
144
|
+
After=${wallpaperBackend.daemonUnitBase}@${ns}.service
|
|
145
|
+
Requires=${wallpaperBackend.daemonUnitBase}@${ns}.service
|
|
140
146
|
|
|
141
147
|
[Service]
|
|
142
148
|
Type=oneshot
|
|
@@ -150,8 +156,8 @@ ExecStart=${nextExec}
|
|
|
150
156
|
const kitowallWatchService = `
|
|
151
157
|
[Unit]
|
|
152
158
|
Description=Kitowall watcher (monitor hotplug)
|
|
153
|
-
After=graphical-session.target
|
|
154
|
-
Requires
|
|
159
|
+
After=graphical-session.target ${wallpaperBackend.daemonUnitBase}@${ns}.service
|
|
160
|
+
Requires=${wallpaperBackend.daemonUnitBase}@${ns}.service
|
|
155
161
|
Wants=graphical-session.target
|
|
156
162
|
|
|
157
163
|
[Service]
|
|
@@ -171,8 +177,8 @@ WantedBy=default.target
|
|
|
171
177
|
const kitowallLoginApplyService = `
|
|
172
178
|
[Unit]
|
|
173
179
|
Description=Kitowall apply wallpapers on session start
|
|
174
|
-
After=graphical-session.target
|
|
175
|
-
Requires
|
|
180
|
+
After=graphical-session.target ${wallpaperBackend.daemonUnitBase}@${ns}.service
|
|
181
|
+
Requires=${wallpaperBackend.daemonUnitBase}@${ns}.service
|
|
176
182
|
Wants=graphical-session.target
|
|
177
183
|
|
|
178
184
|
[Service]
|
|
@@ -187,7 +193,7 @@ WantedBy=default.target
|
|
|
187
193
|
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(userDir, 'kitowall-login-apply.service'), kitowallLoginApplyService, 'utf8');
|
|
188
194
|
// Activación
|
|
189
195
|
await (0, exec_1.run)('systemctl', ['--user', 'daemon-reload']);
|
|
190
|
-
await (0, exec_1.run)('systemctl', ['--user', 'enable', '--now',
|
|
196
|
+
await (0, exec_1.run)('systemctl', ['--user', 'enable', '--now', `${wallpaperBackend.daemonUnitBase}@${ns}.service`]);
|
|
191
197
|
await (0, exec_1.run)('systemctl', ['--user', 'enable', '--now', 'kitowall-watch.service']);
|
|
192
198
|
// login-apply is oneshot and can fail on first run if library is still empty.
|
|
193
199
|
// Enable it for next graphical session, but do not make init fail here.
|
package/dist/core/outputs.js
CHANGED
|
@@ -46,6 +46,36 @@ async function outputsFromSwwwQuery() {
|
|
|
46
46
|
}
|
|
47
47
|
return outputs;
|
|
48
48
|
}
|
|
49
|
+
function outputNamesFromUnknown(value) {
|
|
50
|
+
if (Array.isArray(value)) {
|
|
51
|
+
return value.flatMap(item => {
|
|
52
|
+
if (typeof item === 'string' && item.trim())
|
|
53
|
+
return [{ name: item.trim() }];
|
|
54
|
+
if (item && typeof item === 'object') {
|
|
55
|
+
const candidate = item;
|
|
56
|
+
const name = [candidate.name, candidate.output, candidate.output_name]
|
|
57
|
+
.find(entry => typeof entry === 'string' && String(entry).trim().length > 0);
|
|
58
|
+
if (typeof name === 'string')
|
|
59
|
+
return [{ name: name.trim() }];
|
|
60
|
+
}
|
|
61
|
+
return [];
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (value && typeof value === 'object') {
|
|
65
|
+
const obj = value;
|
|
66
|
+
if (Array.isArray(obj.outputs))
|
|
67
|
+
return outputNamesFromUnknown(obj.outputs);
|
|
68
|
+
return Object.keys(obj)
|
|
69
|
+
.filter(key => key.trim().length > 0)
|
|
70
|
+
.map(name => ({ name }));
|
|
71
|
+
}
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
async function outputsFromAwwwQuery() {
|
|
75
|
+
const result = await (0, exec_1.run)('awww', ['query', '--json']);
|
|
76
|
+
const parsed = JSON.parse(result.stdout);
|
|
77
|
+
return outputNamesFromUnknown(parsed);
|
|
78
|
+
}
|
|
49
79
|
async function detectOutputs() {
|
|
50
80
|
try {
|
|
51
81
|
const outputs = await outputsFromHyprctl();
|
|
@@ -63,6 +93,14 @@ async function detectOutputs() {
|
|
|
63
93
|
catch {
|
|
64
94
|
// ignore and fallback
|
|
65
95
|
}
|
|
96
|
+
try {
|
|
97
|
+
const outputs = await outputsFromAwwwQuery();
|
|
98
|
+
if (outputs.length > 0)
|
|
99
|
+
return outputs;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// ignore
|
|
103
|
+
}
|
|
66
104
|
try {
|
|
67
105
|
const outputs = await outputsFromSwwwQuery();
|
|
68
106
|
if (outputs.length > 0)
|
package/dist/core/setup.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.checkSetupService = checkSetupService;
|
|
|
7
7
|
exports.listSetupStatus = listSetupStatus;
|
|
8
8
|
exports.listSetupVersions = listSetupVersions;
|
|
9
9
|
exports.installSetupItem = installSetupItem;
|
|
10
|
+
exports.purgeSetup = purgeSetup;
|
|
10
11
|
const node_fs_1 = require("node:fs");
|
|
11
12
|
const node_os_1 = require("node:os");
|
|
12
13
|
const node_path_1 = require("node:path");
|
|
@@ -19,8 +20,8 @@ exports.HOST_DEPENDENCY_DEFS = [
|
|
|
19
20
|
{ id: 'kitowall', bin: 'kitowall', label: 'Kitowall CLI', installer: 'kitowall-only' },
|
|
20
21
|
{ id: 'kitsune', bin: 'kitsune', label: 'Kitsune', installer: 'kitsune-only' },
|
|
21
22
|
{ id: 'kitsune-rendercore', bin: 'kitsune-rendercore', label: 'Kitsune RenderCore', installer: 'kitsune-only' },
|
|
22
|
-
{ id: '
|
|
23
|
-
{ id: '
|
|
23
|
+
{ id: 'awww', bin: 'awww', label: 'awww', installer: 'awww', system: true },
|
|
24
|
+
{ id: 'awww-daemon', bin: 'awww-daemon', label: 'awww-daemon', installer: 'awww-daemon', system: true },
|
|
24
25
|
{ id: 'hyprctl', bin: 'hyprctl', label: 'hyprctl', installer: 'hyprctl', system: true },
|
|
25
26
|
{ id: 'cava', bin: 'cava', label: 'cava', installer: 'cava', system: true }
|
|
26
27
|
];
|
|
@@ -32,6 +33,13 @@ exports.HOST_SERVICE_DEFS = [
|
|
|
32
33
|
function homeDir() {
|
|
33
34
|
return process.env.HOME || (0, node_os_1.homedir)();
|
|
34
35
|
}
|
|
36
|
+
function normalizeSetupDependencyId(id) {
|
|
37
|
+
if (id === 'swww')
|
|
38
|
+
return 'awww';
|
|
39
|
+
if (id === 'swww-daemon')
|
|
40
|
+
return 'awww-daemon';
|
|
41
|
+
return id;
|
|
42
|
+
}
|
|
35
43
|
async function fileExists(targetPath) {
|
|
36
44
|
try {
|
|
37
45
|
await node_fs_1.promises.access(targetPath);
|
|
@@ -140,6 +148,11 @@ function bootstrapHostPath() {
|
|
|
140
148
|
function bootstrapSystemPath() {
|
|
141
149
|
return (0, node_path_1.join)(ROOT_DIR, 'scripts', 'bootstrap-system.sh');
|
|
142
150
|
}
|
|
151
|
+
async function assertBootstrapScriptExists(targetPath, label) {
|
|
152
|
+
if (await fileExists(targetPath))
|
|
153
|
+
return;
|
|
154
|
+
throw new Error(`${label} not found at ${targetPath}. Reinstala el CLI con: npm i -g --prefix ~/.local kitowall@latest`);
|
|
155
|
+
}
|
|
143
156
|
async function bootstrapVersionsPath() {
|
|
144
157
|
return (0, node_path_1.join)(homeDir(), '.local', 'share', 'kitowall', 'bootstrap-versions.json');
|
|
145
158
|
}
|
|
@@ -252,7 +265,7 @@ async function maybeSystemctlShow(unit, props) {
|
|
|
252
265
|
}
|
|
253
266
|
}
|
|
254
267
|
async function checkSetupDependency(id) {
|
|
255
|
-
const def = exports.HOST_DEPENDENCY_DEFS.find(item => item.id === id);
|
|
268
|
+
const def = exports.HOST_DEPENDENCY_DEFS.find(item => item.id === normalizeSetupDependencyId(id));
|
|
256
269
|
if (!def)
|
|
257
270
|
throw new Error(`Unknown dependency id: ${id}`);
|
|
258
271
|
let binPath = '';
|
|
@@ -365,7 +378,9 @@ async function listSetupVersions() {
|
|
|
365
378
|
return { ok: true, items };
|
|
366
379
|
}
|
|
367
380
|
async function runBootstrapHostMode(mode) {
|
|
368
|
-
const
|
|
381
|
+
const scriptPath = bootstrapHostPath();
|
|
382
|
+
await assertBootstrapScriptExists(scriptPath, 'bootstrap-host.sh');
|
|
383
|
+
const out = await (0, exec_1.run)('bash', [scriptPath], {
|
|
369
384
|
env: {
|
|
370
385
|
...process.env,
|
|
371
386
|
HOME: homeDir(),
|
|
@@ -377,7 +392,9 @@ async function runBootstrapHostMode(mode) {
|
|
|
377
392
|
return { ok: true, code: out.code, logs: `${out.stdout}${out.stderr}` };
|
|
378
393
|
}
|
|
379
394
|
async function runBootstrapSystemItems(ids) {
|
|
380
|
-
const
|
|
395
|
+
const scriptPath = bootstrapSystemPath();
|
|
396
|
+
await assertBootstrapScriptExists(scriptPath, 'bootstrap-system.sh');
|
|
397
|
+
const out = await (0, exec_1.run)('pkexec', ['bash', scriptPath, ...ids], {
|
|
381
398
|
env: {
|
|
382
399
|
...process.env,
|
|
383
400
|
HOME: homeDir(),
|
|
@@ -387,21 +404,95 @@ async function runBootstrapSystemItems(ids) {
|
|
|
387
404
|
return { ok: true, code: out.code, logs: `${out.stdout}${out.stderr}` };
|
|
388
405
|
}
|
|
389
406
|
async function installSetupItem(id, namespace = 'kitowall') {
|
|
390
|
-
const
|
|
407
|
+
const normalizedId = normalizeSetupDependencyId(id);
|
|
408
|
+
const dep = exports.HOST_DEPENDENCY_DEFS.find(item => item.id === normalizedId);
|
|
391
409
|
if (dep?.system)
|
|
392
|
-
return await runBootstrapSystemItems([
|
|
393
|
-
if (
|
|
410
|
+
return await runBootstrapSystemItems([normalizedId]);
|
|
411
|
+
if (normalizedId === 'kitowall')
|
|
394
412
|
return await runBootstrapHostMode('kitowall-only');
|
|
395
|
-
if (
|
|
413
|
+
if (normalizedId === 'kitsune' || normalizedId === 'kitsune-rendercore' || normalizedId === 'kitsune-rendercore.service') {
|
|
396
414
|
return await runBootstrapHostMode('kitsune-only');
|
|
397
415
|
}
|
|
398
|
-
if (
|
|
416
|
+
if (normalizedId === 'kitowall-config') {
|
|
399
417
|
await (0, init_1.initKitowall)({ namespace, apply: true, force: true });
|
|
400
418
|
return { ok: true, code: 0, logs: JSON.stringify({ ok: true, init: true, namespace }, null, 2) };
|
|
401
419
|
}
|
|
402
|
-
if (
|
|
420
|
+
if (normalizedId === 'kitowall-next.timer') {
|
|
403
421
|
await (0, systemd_1.installSystemd)({ every: '600s' });
|
|
404
422
|
return { ok: true, code: 0, logs: JSON.stringify({ ok: true, installed: true, every: '600s' }, null, 2) };
|
|
405
423
|
}
|
|
406
424
|
throw new Error(`Unsupported setup item: ${id}`);
|
|
407
425
|
}
|
|
426
|
+
async function rmIfExists(targetPath, logs) {
|
|
427
|
+
if (!(await fileExists(targetPath)))
|
|
428
|
+
return;
|
|
429
|
+
await node_fs_1.promises.rm(targetPath, { recursive: true, force: true });
|
|
430
|
+
logs.push(`[removed] ${targetPath}`);
|
|
431
|
+
}
|
|
432
|
+
async function runSystemctlUserBestEffort(args, logs) {
|
|
433
|
+
try {
|
|
434
|
+
await (0, exec_1.run)('systemctl', ['--user', ...args], {
|
|
435
|
+
env: {
|
|
436
|
+
...process.env,
|
|
437
|
+
HOME: homeDir(),
|
|
438
|
+
PATH: hostPathEntries().join(':')
|
|
439
|
+
},
|
|
440
|
+
timeoutMs: 4000
|
|
441
|
+
});
|
|
442
|
+
logs.push(`[systemctl] ${args.join(' ')}`);
|
|
443
|
+
}
|
|
444
|
+
catch (err) {
|
|
445
|
+
logs.push(`[systemctl-warn] ${args.join(' ')} :: ${err instanceof Error ? err.message : String(err)}`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
async function purgeSetup(namespace = 'kitowall') {
|
|
449
|
+
const home = homeDir();
|
|
450
|
+
const logs = [];
|
|
451
|
+
const removeTargets = [
|
|
452
|
+
(0, node_path_1.join)(home, '.config', 'kitowall'),
|
|
453
|
+
(0, node_path_1.join)(home, '.local', 'share', 'kitowall'),
|
|
454
|
+
(0, node_path_1.join)(home, '.local', 'state', 'kitowall'),
|
|
455
|
+
(0, node_path_1.join)(home, '.config', 'kitsune'),
|
|
456
|
+
(0, node_path_1.join)(home, '.local', 'share', 'kitsune'),
|
|
457
|
+
(0, node_path_1.join)(home, '.local', 'state', 'kitsune'),
|
|
458
|
+
(0, node_path_1.join)(home, '.config', 'kitsune-rendercore'),
|
|
459
|
+
(0, node_path_1.join)(home, '.local', 'lib', 'node_modules', 'kitowall'),
|
|
460
|
+
(0, node_path_1.join)(home, '.npm-global', 'lib', 'node_modules', 'kitowall')
|
|
461
|
+
];
|
|
462
|
+
const removeFiles = [
|
|
463
|
+
(0, node_path_1.join)(home, '.local', 'bin', 'kitowall'),
|
|
464
|
+
(0, node_path_1.join)(home, '.local', 'bin', 'kitsune'),
|
|
465
|
+
(0, node_path_1.join)(home, '.local', 'bin', 'kitsune-overlay'),
|
|
466
|
+
(0, node_path_1.join)(home, '.local', 'bin', 'kitsune-color-resolve'),
|
|
467
|
+
(0, node_path_1.join)(home, '.local', 'bin', 'kitsune-rendercore'),
|
|
468
|
+
(0, node_path_1.join)(home, '.cargo', 'bin', 'kitsune'),
|
|
469
|
+
(0, node_path_1.join)(home, '.cargo', 'bin', 'kitsune-overlay'),
|
|
470
|
+
(0, node_path_1.join)(home, '.cargo', 'bin', 'kitsune-color-resolve'),
|
|
471
|
+
(0, node_path_1.join)(home, '.cargo', 'bin', 'kitsune-rendercore'),
|
|
472
|
+
(0, node_path_1.join)(home, '.config', 'systemd', 'user', 'kitowall-next.service'),
|
|
473
|
+
(0, node_path_1.join)(home, '.config', 'systemd', 'user', 'kitowall-next.timer'),
|
|
474
|
+
(0, node_path_1.join)(home, '.config', 'systemd', 'user', 'kitowall-watch.service'),
|
|
475
|
+
(0, node_path_1.join)(home, '.config', 'systemd', 'user', 'kitowall-login-apply.service'),
|
|
476
|
+
(0, node_path_1.join)(home, '.config', 'systemd', 'user', 'kitsune-rendercore.service'),
|
|
477
|
+
(0, node_path_1.join)(home, '.config', 'systemd', 'user', 'awww-daemon@.service'),
|
|
478
|
+
(0, node_path_1.join)(home, '.config', 'systemd', 'user', 'swww-daemon@.service')
|
|
479
|
+
];
|
|
480
|
+
logs.push(`[purge] namespace=${namespace}`);
|
|
481
|
+
await runSystemctlUserBestEffort(['disable', '--now', `awww-daemon@${namespace}.service`], logs);
|
|
482
|
+
await runSystemctlUserBestEffort(['disable', '--now', `swww-daemon@${namespace}.service`], logs);
|
|
483
|
+
await runSystemctlUserBestEffort(['disable', '--now', 'kitowall-next.timer'], logs);
|
|
484
|
+
await runSystemctlUserBestEffort(['stop', 'kitowall-next.service'], logs);
|
|
485
|
+
await runSystemctlUserBestEffort(['disable', '--now', 'kitowall-watch.service'], logs);
|
|
486
|
+
await runSystemctlUserBestEffort(['disable', '--now', 'kitowall-login-apply.service'], logs);
|
|
487
|
+
await runSystemctlUserBestEffort(['disable', '--now', 'kitsune-rendercore.service'], logs);
|
|
488
|
+
await runSystemctlUserBestEffort(['daemon-reload'], logs);
|
|
489
|
+
await runSystemctlUserBestEffort(['reset-failed', `awww-daemon@${namespace}.service`, `swww-daemon@${namespace}.service`, 'kitowall-next.service', 'kitowall-next.timer', 'kitowall-watch.service', 'kitowall-login-apply.service', 'kitsune-rendercore.service'], logs);
|
|
490
|
+
for (const targetPath of removeFiles) {
|
|
491
|
+
await rmIfExists(targetPath, logs);
|
|
492
|
+
}
|
|
493
|
+
for (const targetPath of removeTargets) {
|
|
494
|
+
await rmIfExists(targetPath, logs);
|
|
495
|
+
}
|
|
496
|
+
logs.push('[ok] purge complete');
|
|
497
|
+
return { ok: true, code: 0, logs: `${logs.join('\n')}\n` };
|
|
498
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.wallpaperBackendInfo = wallpaperBackendInfo;
|
|
4
|
+
exports.hostCmdExists = hostCmdExists;
|
|
5
|
+
exports.resolveWallpaperBackend = resolveWallpaperBackend;
|
|
6
|
+
exports.applyWallpaperBackend = applyWallpaperBackend;
|
|
7
|
+
const exec_1 = require("../utils/exec");
|
|
8
|
+
const awww_1 = require("../managers/awww");
|
|
9
|
+
const swww_1 = require("../managers/swww");
|
|
10
|
+
function wallpaperBackendInfo(name) {
|
|
11
|
+
if (name === 'awww') {
|
|
12
|
+
return {
|
|
13
|
+
name,
|
|
14
|
+
bin: 'awww',
|
|
15
|
+
daemonBin: 'awww-daemon',
|
|
16
|
+
daemonUnitBase: 'awww-daemon',
|
|
17
|
+
packageName: 'awww',
|
|
18
|
+
queryArgs: ['query', '--json']
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
name,
|
|
23
|
+
bin: 'swww',
|
|
24
|
+
daemonBin: 'swww-daemon',
|
|
25
|
+
daemonUnitBase: 'swww-daemon',
|
|
26
|
+
packageName: 'swww',
|
|
27
|
+
queryArgs: ['query']
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
async function hostCmdExists(cmd) {
|
|
31
|
+
try {
|
|
32
|
+
await (0, exec_1.run)('which', [cmd], { timeoutMs: 1500 });
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function resolveWallpaperBackend(config) {
|
|
40
|
+
const preferred = config?.wallpaper_backend ?? 'auto';
|
|
41
|
+
if (preferred === 'awww' || preferred === 'swww')
|
|
42
|
+
return preferred;
|
|
43
|
+
if (await hostCmdExists('awww'))
|
|
44
|
+
return 'awww';
|
|
45
|
+
if (await hostCmdExists('swww'))
|
|
46
|
+
return 'swww';
|
|
47
|
+
return 'awww';
|
|
48
|
+
}
|
|
49
|
+
async function applyWallpaperBackend(config, images, namespace = 'kitowall') {
|
|
50
|
+
const backend = await resolveWallpaperBackend(config);
|
|
51
|
+
if (backend === 'awww') {
|
|
52
|
+
await (0, awww_1.applyAwww)(images, config.transition, namespace);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
await (0, swww_1.applySwww)(images, config.transition, namespace);
|
|
56
|
+
}
|
|
57
|
+
return backend;
|
|
58
|
+
}
|