kitowall 7.9.0 → 8.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/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
- throw new Error('Usage: host-setup <check dependency <id>|check service <id>|list|versions|install <id>> [--namespace <ns>] [--json]');
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);
@@ -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
  }
@@ -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, swww_1.applySwww)(outputImages, this.config.transition, namespace);
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) {
@@ -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)('swww', ['query', '--namespace', namespace]);
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('swww'),
115
- swwwDaemon: await which('swww-daemon'),
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('Missing dependency: swww');
126
+ hints.push(`Missing dependency: ${backendInfo.bin}`);
121
127
  if (!deps.swwwDaemon)
122
- hints.push('Missing dependency: swww-daemon');
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(`swww-daemon@${namespace}.service`),
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(`swww query failed for namespace "${namespace}"`);
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)} swww`);
192
- line(` ${badge(r.deps.swwwDaemon)} swww-daemon`);
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('swww namespace:');
210
- line(` ${badge(r.swww.namespaceQueryOk)} swww query --namespace ${namespace}`);
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 runHostShell(`command -v ${cmd} >/dev/null 2>&1`);
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 = ['swww', 'swww-daemon', 'hyprctl'];
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('swww') || missing.includes('swww-daemon'))
42
- pkgSet.add('swww');
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
- const pkgArgs = packages.join(' ');
49
- await runHostShell(`if command -v sudo >/dev/null 2>&1; then ` +
50
- `(sudo -n pacman -S --needed --noconfirm ${pkgArgs} || sudo pacman -S --needed ${pkgArgs}); ` +
51
- `else pacman -S --needed ${pkgArgs}; fi`).catch(() => { });
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 swww hyprland`);
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) swww-daemon template
119
+ // 1) wallpaper daemon template
114
120
  const swwwDaemonTemplate = `
115
121
  [Unit]
116
- Description=swww wallpaper daemon (namespace %i)
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 swww-daemon --no-cache --namespace %i`)}
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, 'swww-daemon@.service'), swwwDaemonTemplate, 'utf8');
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=swww-daemon@${ns}.service
139
- Requires=swww-daemon@${ns}.service
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 swww-daemon@${ns}.service
154
- Requires=swww-daemon@${ns}.service
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 swww-daemon@${ns}.service
175
- Requires=swww-daemon@${ns}.service
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', `swww-daemon@${ns}.service`]);
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.
@@ -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)
@@ -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: 'swww', bin: 'swww', label: 'swww', installer: 'swww', system: true },
23
- { id: 'swww-daemon', bin: 'swww-daemon', label: 'swww-daemon', installer: 'swww-daemon', system: true },
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);
@@ -257,7 +265,7 @@ async function maybeSystemctlShow(unit, props) {
257
265
  }
258
266
  }
259
267
  async function checkSetupDependency(id) {
260
- const def = exports.HOST_DEPENDENCY_DEFS.find(item => item.id === id);
268
+ const def = exports.HOST_DEPENDENCY_DEFS.find(item => item.id === normalizeSetupDependencyId(id));
261
269
  if (!def)
262
270
  throw new Error(`Unknown dependency id: ${id}`);
263
271
  let binPath = '';
@@ -396,21 +404,95 @@ async function runBootstrapSystemItems(ids) {
396
404
  return { ok: true, code: out.code, logs: `${out.stdout}${out.stderr}` };
397
405
  }
398
406
  async function installSetupItem(id, namespace = 'kitowall') {
399
- const dep = exports.HOST_DEPENDENCY_DEFS.find(item => item.id === id);
407
+ const normalizedId = normalizeSetupDependencyId(id);
408
+ const dep = exports.HOST_DEPENDENCY_DEFS.find(item => item.id === normalizedId);
400
409
  if (dep?.system)
401
- return await runBootstrapSystemItems([id]);
402
- if (id === 'kitowall')
410
+ return await runBootstrapSystemItems([normalizedId]);
411
+ if (normalizedId === 'kitowall')
403
412
  return await runBootstrapHostMode('kitowall-only');
404
- if (id === 'kitsune' || id === 'kitsune-rendercore' || id === 'kitsune-rendercore.service') {
413
+ if (normalizedId === 'kitsune' || normalizedId === 'kitsune-rendercore' || normalizedId === 'kitsune-rendercore.service') {
405
414
  return await runBootstrapHostMode('kitsune-only');
406
415
  }
407
- if (id === 'kitowall-config') {
416
+ if (normalizedId === 'kitowall-config') {
408
417
  await (0, init_1.initKitowall)({ namespace, apply: true, force: true });
409
418
  return { ok: true, code: 0, logs: JSON.stringify({ ok: true, init: true, namespace }, null, 2) };
410
419
  }
411
- if (id === 'kitowall-next.timer') {
420
+ if (normalizedId === 'kitowall-next.timer') {
412
421
  await (0, systemd_1.installSystemd)({ every: '600s' });
413
422
  return { ok: true, code: 0, logs: JSON.stringify({ ok: true, installed: true, every: '600s' }, null, 2) };
414
423
  }
415
424
  throw new Error(`Unsupported setup item: ${id}`);
416
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
+ }
@@ -130,7 +130,7 @@ function getSteamWebApiKey() {
130
130
  function getCoexistServices() {
131
131
  const cfg = readWeConfig();
132
132
  const defaults = [
133
- 'swww-daemon@kitowall.service',
133
+ 'awww-daemon@kitowall.service',
134
134
  'kitowall-login-apply.service',
135
135
  'kitowall-watch.service',
136
136
  'kitowall-next.service',
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applyAwww = applyAwww;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const exec_1 = require("../utils/exec");
6
+ function startAwwwDaemon(namespace) {
7
+ const args = ['--layer', 'background'];
8
+ if (namespace)
9
+ args.push('--namespace', namespace);
10
+ const child = (0, node_child_process_1.spawn)('awww-daemon', args, {
11
+ stdio: 'ignore',
12
+ detached: true
13
+ });
14
+ child.on('error', () => { });
15
+ child.unref();
16
+ }
17
+ async function ensureAwwwRunning(namespace) {
18
+ const q = ['query', '--json'];
19
+ if (namespace)
20
+ q.push('--namespace', namespace);
21
+ try {
22
+ await (0, exec_1.run)('awww', q);
23
+ return;
24
+ }
25
+ catch { }
26
+ startAwwwDaemon(namespace);
27
+ for (let i = 0; i < 10; i++) {
28
+ try {
29
+ await (0, exec_1.run)('awww', q);
30
+ return;
31
+ }
32
+ catch { }
33
+ await new Promise(r => setTimeout(r, 150));
34
+ }
35
+ throw new Error('No se pudo iniciar awww-daemon.');
36
+ }
37
+ async function applyAwww(images, transition, namespace = 'kitowall') {
38
+ await ensureAwwwRunning(namespace);
39
+ for (const item of images) {
40
+ const args = [
41
+ 'img',
42
+ '--namespace', namespace,
43
+ '--outputs', item.output,
44
+ item.path,
45
+ '--transition-type', transition.type,
46
+ '--transition-fps', String(transition.fps),
47
+ '--transition-duration', String(transition.duration)
48
+ ];
49
+ if (transition.angle !== undefined)
50
+ args.push('--transition-angle', String(transition.angle));
51
+ if (transition.pos)
52
+ args.push('--transition-pos', transition.pos);
53
+ await (0, exec_1.run)('awww', args);
54
+ }
55
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kitowall",
3
- "version": "7.9.0",
4
- "description": "CLI/daemon for Hyprland wallpapers using swww with pack-based rotation.",
3
+ "version": "8.1.0",
4
+ "description": "CLI/daemon for Hyprland wallpapers using awww with pack-based rotation.",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/KitotsuMolina/Kitowall"
@@ -70,7 +70,7 @@ install_arch_deps() {
70
70
  local repo_pkgs=(
71
71
  nodejs npm
72
72
  hyprland
73
- swww
73
+ awww
74
74
  cava
75
75
  pkgconf
76
76
  gtk4
@@ -116,7 +116,7 @@ install_system_deps() {
116
116
  install_ubuntu_deps
117
117
  return
118
118
  fi
119
- echo "[bootstrap] unsupported distro package manager. Install manually: nodejs npm rust cargo pkg-config gtk4 gtk4-layer-shell swww hyprland cava" >&2
119
+ echo "[bootstrap] unsupported distro package manager. Install manually: nodejs npm rust cargo pkg-config gtk4 gtk4-layer-shell awww hyprland cava" >&2
120
120
  exit 1
121
121
  }
122
122
 
@@ -447,7 +447,7 @@ verify_bins() {
447
447
  elif [[ "$KITOWALL_BOOTSTRAP_MODE" == "kitowall-only" ]]; then
448
448
  required_bins=(kitowall)
449
449
  else
450
- required_bins=(kitowall kitsune kitsune-rendercore swww swww-daemon cava)
450
+ required_bins=(kitowall kitsune kitsune-rendercore awww awww-daemon cava)
451
451
  fi
452
452
  local missing=()
453
453
  for b in "${required_bins[@]}"; do
@@ -45,7 +45,7 @@ install_arch_deps() {
45
45
  local repo_pkgs=(
46
46
  nodejs npm
47
47
  hyprland
48
- swww
48
+ awww
49
49
  cava
50
50
  pkgconf
51
51
  gtk4
@@ -76,7 +76,7 @@ arch_packages_for_ids() {
76
76
  nodejs) pkgs+=(nodejs) ;;
77
77
  npm) pkgs+=(npm) ;;
78
78
  hyprctl) pkgs+=(hyprland) ;;
79
- swww|swww-daemon) pkgs+=(swww) ;;
79
+ awww|awww-daemon|swww|swww-daemon) pkgs+=(awww) ;;
80
80
  cava) pkgs+=(cava) ;;
81
81
  pkgconf) pkgs+=(pkgconf) ;;
82
82
  gtk4) pkgs+=(gtk4) ;;
@@ -180,7 +180,7 @@ main() {
180
180
  exit 0
181
181
  fi
182
182
 
183
- echo "[bootstrap-system] unsupported distro package manager. Install manually: nodejs npm rust cargo pkg-config gtk4 gtk4-layer-shell swww hyprland cava" >&2
183
+ echo "[bootstrap-system] unsupported distro package manager. Install manually: nodejs npm rust cargo pkg-config gtk4 gtk4-layer-shell awww hyprland cava" >&2
184
184
  exit 1
185
185
  }
186
186