kitowall 3.1.0 → 3.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/dist/cli.js CHANGED
@@ -55,6 +55,7 @@ const staticUrl_1 = require("./adapters/staticUrl");
55
55
  const logs_1 = require("./core/logs");
56
56
  const node_fs_1 = require("node:fs");
57
57
  const node_path_1 = require("node:path");
58
+ const exec_1 = require("./utils/exec");
58
59
  const workshop_1 = require("./core/workshop");
59
60
  const live_1 = require("./core/live");
60
61
  function getCliVersion() {
@@ -159,17 +160,11 @@ Commands:
159
160
  live remove <id> [--delete-files]
160
161
  live thumb regen [--id <id>] [--all]
161
162
  live open [--id <id>] Print folder path (root or item folder)
163
+ live service-autostart <install|enable|disable|start|stop|restart|status>
162
164
  live config show
163
- live config wallpaper-show --id <id>
164
- live config wallpaper --id <id> [--keep-services true|false] [--mute-audio true|false] [--profile performance|balanced|quality]
165
- [--display-fps <n|off>] [--seamless-loop true|false] [--loop-crossfade true|false]
166
- [--loop-crossfade-seconds <n>] [--optimize true|false]
167
- [--proxy-width <n>] [--proxy-fps <n>] [--proxy-crf <n>]
168
165
  live config runner [--bin-name <name>]
169
- live config apply-defaults [--keep-services true|false] [--mute-audio true|false] [--display-fps <n|off>]
170
- [--proxy-width-hd <n>] [--proxy-width-4k <n>] [--proxy-fps <n>] [--proxy-crf-hd <n>] [--proxy-crf-4k <n>]
171
- [--loop-crossfade-seconds <n>] [--profile performance|balanced|quality]
172
- [--seamless-loop true|false] [--loop-crossfade true|false] [--optimize true|false]
166
+ live config apply-defaults [--video-fps <n>] [--video-speed <n>] [--hwaccel auto|nvdec|vaapi|none]
167
+ [--quality low|medium|high|ultra] [--pause-on-steam-game true|false] [--steam-poll-ms <n>]
173
168
  live doctor [--fix]
174
169
  check [--namespace <ns>] [--json] Quick system check (no changes)
175
170
 
@@ -266,6 +261,21 @@ function formatError(err) {
266
261
  }
267
262
  return { message, hint, code };
268
263
  }
264
+ async function switchToStaticWallpaperMode() {
265
+ const liveBin = cleanOpt(process.env.KITOWALL_LIVE_RUNNER_BIN ?? null) ?? 'kitsune-rendercore';
266
+ try {
267
+ await (0, exec_1.run)(liveBin, ['service', 'disable'], { timeoutMs: 20000 });
268
+ }
269
+ catch {
270
+ // best effort
271
+ }
272
+ try {
273
+ await (0, workshop_1.workshopCoexistenceExit)();
274
+ }
275
+ catch {
276
+ // best effort
277
+ }
278
+ }
269
279
  async function main() {
270
280
  const args = process.argv.slice(2);
271
281
  outputJsonOnError = args.includes('--json');
@@ -511,7 +521,7 @@ async function main() {
511
521
  if (cmd === 'live') {
512
522
  const action = cleanOpt(args[1] ?? null);
513
523
  if (!action)
514
- throw new Error('Usage: live <init|list|browse|search|resolve|preview|preview-clear|fetch|apply|auto-apply|favorite|remove|thumb|open|config|doctor> ...');
524
+ throw new Error('Usage: live <init|list|browse|search|resolve|preview|preview-clear|fetch|apply|auto-apply|favorite|remove|thumb|open|service-autostart|config|doctor> ...');
515
525
  if (action === 'init') {
516
526
  console.log(JSON.stringify((0, live_1.liveInit)(), null, 2));
517
527
  return;
@@ -652,6 +662,21 @@ async function main() {
652
662
  console.log(JSON.stringify(out, null, 2));
653
663
  return;
654
664
  }
665
+ if (action === 'service-autostart') {
666
+ const sub = cleanOpt(args[2] ?? null);
667
+ if (sub !== 'install' &&
668
+ sub !== 'enable' &&
669
+ sub !== 'disable' &&
670
+ sub !== 'status' &&
671
+ sub !== 'start' &&
672
+ sub !== 'stop' &&
673
+ sub !== 'restart') {
674
+ throw new Error('Usage: live service-autostart <install|enable|disable|start|stop|restart|status>');
675
+ }
676
+ const out = await (0, live_1.liveServiceAutostart)(sub);
677
+ console.log(JSON.stringify(out, null, 2));
678
+ return;
679
+ }
655
680
  if (action === 'doctor') {
656
681
  const out = await (0, live_1.liveDoctor)({ fix: args.includes('--fix') });
657
682
  console.log(JSON.stringify(out, null, 2));
@@ -663,39 +688,6 @@ async function main() {
663
688
  console.log(JSON.stringify((0, live_1.liveGetConfig)(), null, 2));
664
689
  return;
665
690
  }
666
- if (sub === 'wallpaper-show') {
667
- const id = cleanOpt(getOptionValue(args, '--id'));
668
- if (!id)
669
- throw new Error('Usage: live config wallpaper-show --id <id>');
670
- console.log(JSON.stringify((0, live_1.liveGetWallpaperConfig)(id), null, 2));
671
- return;
672
- }
673
- if (sub === 'wallpaper') {
674
- const id = cleanOpt(getOptionValue(args, '--id'));
675
- if (!id) {
676
- throw new Error('Usage: live config wallpaper --id <id> [--keep-services true|false] [--mute-audio true|false] [--profile performance|balanced|quality] [--display-fps <n|off>] [--seamless-loop true|false] [--loop-crossfade true|false] [--loop-crossfade-seconds <n>] [--optimize true|false] [--proxy-width <n>] [--proxy-fps <n>] [--proxy-crf <n>]');
677
- }
678
- const profile = cleanOpt(getOptionValue(args, '--profile'));
679
- const displayFpsRaw = cleanOpt(getOptionValue(args, '--display-fps'));
680
- const display_fps = !displayFpsRaw
681
- ? undefined
682
- : (displayFpsRaw.toLowerCase() === 'off' ? null : Number(displayFpsRaw));
683
- const out = (0, live_1.liveSetWallpaperConfig)(id, {
684
- keep_services: parseBool(getOptionValue(args, '--keep-services')),
685
- mute_audio: parseBool(getOptionValue(args, '--mute-audio')),
686
- profile: (profile === 'performance' || profile === 'balanced' || profile === 'quality') ? profile : undefined,
687
- display_fps: (display_fps === undefined || display_fps === null || Number.isFinite(display_fps)) ? display_fps : undefined,
688
- seamless_loop: parseBool(getOptionValue(args, '--seamless-loop')),
689
- loop_crossfade: parseBool(getOptionValue(args, '--loop-crossfade')),
690
- loop_crossfade_seconds: cleanOpt(getOptionValue(args, '--loop-crossfade-seconds')) ? Number(cleanOpt(getOptionValue(args, '--loop-crossfade-seconds'))) : undefined,
691
- optimize: parseBool(getOptionValue(args, '--optimize')),
692
- proxy_width: cleanOpt(getOptionValue(args, '--proxy-width')) ? Number(cleanOpt(getOptionValue(args, '--proxy-width'))) : undefined,
693
- proxy_fps: cleanOpt(getOptionValue(args, '--proxy-fps')) ? Number(cleanOpt(getOptionValue(args, '--proxy-fps'))) : undefined,
694
- proxy_crf: cleanOpt(getOptionValue(args, '--proxy-crf')) ? Number(cleanOpt(getOptionValue(args, '--proxy-crf'))) : undefined
695
- });
696
- console.log(JSON.stringify(out, null, 2));
697
- return;
698
- }
699
691
  if (sub === 'runner') {
700
692
  const out = (0, live_1.liveSetRunner)({
701
693
  bin_name: cleanOpt(getOptionValue(args, '--bin-name')) ?? undefined
@@ -704,24 +696,13 @@ async function main() {
704
696
  return;
705
697
  }
706
698
  if (sub === 'apply-defaults') {
707
- const displayFpsRaw = cleanOpt(getOptionValue(args, '--display-fps'));
708
- const display_fps = !displayFpsRaw
709
- ? undefined
710
- : (displayFpsRaw.toLowerCase() === 'off' ? null : Number(displayFpsRaw));
711
699
  const out = (0, live_1.liveSetApplyDefaults)({
712
- keep_services: parseBool(getOptionValue(args, '--keep-services')),
713
- mute_audio: parseBool(getOptionValue(args, '--mute-audio')),
714
- profile: cleanOpt(getOptionValue(args, '--profile')) ?? undefined,
715
- display_fps: (display_fps === undefined || display_fps === null || Number.isFinite(display_fps)) ? display_fps : undefined,
716
- seamless_loop: parseBool(getOptionValue(args, '--seamless-loop')),
717
- loop_crossfade: parseBool(getOptionValue(args, '--loop-crossfade')),
718
- loop_crossfade_seconds: cleanOpt(getOptionValue(args, '--loop-crossfade-seconds')) ? Number(cleanOpt(getOptionValue(args, '--loop-crossfade-seconds'))) : undefined,
719
- optimize: parseBool(getOptionValue(args, '--optimize')),
720
- proxy_width_hd: cleanOpt(getOptionValue(args, '--proxy-width-hd')) ? Number(cleanOpt(getOptionValue(args, '--proxy-width-hd'))) : undefined,
721
- proxy_width_4k: cleanOpt(getOptionValue(args, '--proxy-width-4k')) ? Number(cleanOpt(getOptionValue(args, '--proxy-width-4k'))) : undefined,
722
- proxy_fps: cleanOpt(getOptionValue(args, '--proxy-fps')) ? Number(cleanOpt(getOptionValue(args, '--proxy-fps'))) : undefined,
723
- proxy_crf_hd: cleanOpt(getOptionValue(args, '--proxy-crf-hd')) ? Number(cleanOpt(getOptionValue(args, '--proxy-crf-hd'))) : undefined,
724
- proxy_crf_4k: cleanOpt(getOptionValue(args, '--proxy-crf-4k')) ? Number(cleanOpt(getOptionValue(args, '--proxy-crf-4k'))) : undefined
700
+ video_fps: cleanOpt(getOptionValue(args, '--video-fps')) ? Number(cleanOpt(getOptionValue(args, '--video-fps'))) : undefined,
701
+ video_speed: cleanOpt(getOptionValue(args, '--video-speed')) ? Number(cleanOpt(getOptionValue(args, '--video-speed'))) : undefined,
702
+ hwaccel: cleanOpt(getOptionValue(args, '--hwaccel')) ?? undefined,
703
+ quality: cleanOpt(getOptionValue(args, '--quality')) ?? undefined,
704
+ pause_on_steam_game: parseBool(getOptionValue(args, '--pause-on-steam-game')),
705
+ steam_poll_ms: cleanOpt(getOptionValue(args, '--steam-poll-ms')) ? Number(cleanOpt(getOptionValue(args, '--steam-poll-ms'))) : undefined
725
706
  });
726
707
  console.log(JSON.stringify(out, null, 2));
727
708
  return;
@@ -732,7 +713,7 @@ async function main() {
732
713
  console.log(JSON.stringify((0, live_1.liveViewData)(), null, 2));
733
714
  return;
734
715
  }
735
- throw new Error('Usage: live <init|list|browse|search|resolve|preview|preview-clear|fetch|apply|auto-apply|favorite|remove|thumb|open|config|doctor> ...');
716
+ throw new Error('Usage: live <init|list|browse|search|resolve|preview|preview-clear|fetch|apply|auto-apply|favorite|remove|thumb|open|service-autostart|config|doctor> ...');
736
717
  }
737
718
  // Regular commands (need config/state)
738
719
  const config = (0, config_1.loadConfig)();
@@ -1514,6 +1495,7 @@ async function main() {
1514
1495
  state.mode = value;
1515
1496
  state.last_updated = Date.now();
1516
1497
  (0, state_1.saveState)(state);
1498
+ await switchToStaticWallpaperMode();
1517
1499
  console.log(JSON.stringify({ ok: true, mode: state.mode }, null, 2));
1518
1500
  return;
1519
1501
  }
@@ -1531,6 +1513,7 @@ async function main() {
1531
1513
  }, null, 2));
1532
1514
  return;
1533
1515
  }
1516
+ await switchToStaticWallpaperMode();
1534
1517
  const result = await controller.applyNext(pack, namespace);
1535
1518
  console.log(JSON.stringify({
1536
1519
  pack: result.pack,
package/dist/core/live.js CHANGED
@@ -22,6 +22,7 @@ exports.liveThumbRegen = liveThumbRegen;
22
22
  exports.liveAutoApplySet = liveAutoApplySet;
23
23
  exports.liveAutoApplyUnset = liveAutoApplyUnset;
24
24
  exports.liveDoctor = liveDoctor;
25
+ exports.liveServiceAutostart = liveServiceAutostart;
25
26
  exports.liveSetRunner = liveSetRunner;
26
27
  exports.liveGetWallpaperConfig = liveGetWallpaperConfig;
27
28
  exports.liveSetWallpaperConfig = liveSetWallpaperConfig;
@@ -37,6 +38,7 @@ const promises_1 = require("node:stream/promises");
37
38
  const exec_1 = require("../utils/exec");
38
39
  const net_1 = require("../utils/net");
39
40
  const fs_1 = require("../utils/fs");
41
+ const workshop_1 = require("./workshop");
40
42
  const LIVE_LOCK_STALE_MS = 10 * 60 * 1000;
41
43
  const LIVE_DOWNLOAD_MIN_BYTES = 3 * 1024 * 1024;
42
44
  const LIVE_PREVIEW_MIN_BYTES = 64 * 1024;
@@ -103,7 +105,7 @@ function defaultRunnerConfig() {
103
105
  return {
104
106
  mode: 'bin',
105
107
  cargo_project_dir: process.env.KITOWALL_LIVE_RUNNER_PROJECT?.trim() || '',
106
- bin_name: process.env.KITOWALL_LIVE_RUNNER_BIN?.trim() || 'kitsune-livewallpaper'
108
+ bin_name: process.env.KITOWALL_LIVE_RUNNER_BIN?.trim() || 'kitsune-rendercore'
107
109
  };
108
110
  }
109
111
  function defaultIndex() {
@@ -112,19 +114,12 @@ function defaultIndex() {
112
114
  items: [],
113
115
  per_monitor: {},
114
116
  apply_defaults: {
115
- keep_services: false,
116
- mute_audio: false,
117
- profile: 'quality',
118
- display_fps: null,
119
- seamless_loop: true,
120
- loop_crossfade: true,
121
- loop_crossfade_seconds: 0.35,
122
- optimize: true,
123
- proxy_width_hd: 1920,
124
- proxy_width_4k: 3840,
125
- proxy_fps: 60,
126
- proxy_crf_hd: 18,
127
- proxy_crf_4k: 16
117
+ video_fps: 30,
118
+ video_speed: 1.0,
119
+ hwaccel: 'auto',
120
+ quality: 'high',
121
+ pause_on_steam_game: true,
122
+ steam_poll_ms: 1000
128
123
  },
129
124
  runner: defaultRunnerConfig()
130
125
  };
@@ -136,10 +131,7 @@ function ensureIndexShape(index) {
136
131
  version: 1,
137
132
  items: items.filter(v => v && typeof v === 'object'),
138
133
  per_monitor: (index.per_monitor && typeof index.per_monitor === 'object') ? index.per_monitor : {},
139
- apply_defaults: {
140
- ...base.apply_defaults,
141
- ...(index.apply_defaults ?? {})
142
- },
134
+ apply_defaults: normalizeApplyDefaults(index.apply_defaults, base.apply_defaults),
143
135
  runner: {
144
136
  ...base.runner,
145
137
  ...(index.runner ?? {}),
@@ -209,7 +201,21 @@ function readIndex() {
209
201
  }
210
202
  try {
211
203
  const raw = node_fs_1.default.readFileSync(idxPath, 'utf8');
212
- return ensureIndexShape(JSON.parse(raw));
204
+ const parsed = ensureIndexShape(JSON.parse(raw));
205
+ // Auto-migrate legacy runner to RenderCore.
206
+ if (clean(parsed.runner.bin_name) === 'kitsune-livewallpaper') {
207
+ const migrated = {
208
+ ...parsed,
209
+ runner: {
210
+ ...parsed.runner,
211
+ mode: 'bin',
212
+ bin_name: 'kitsune-rendercore'
213
+ }
214
+ };
215
+ writeIndexAtomic(migrated);
216
+ return migrated;
217
+ }
218
+ return parsed;
213
219
  }
214
220
  catch {
215
221
  return defaultIndex();
@@ -824,48 +830,92 @@ function clampFloat(value, fallback, min, max = Number.MAX_SAFE_INTEGER) {
824
830
  return fallback;
825
831
  return Math.min(max, Math.max(min, n));
826
832
  }
827
- function buildVideoConfigFromDefaults(defaults, variant) {
833
+ function mapProfileToQuality(profile) {
834
+ const p = clean(profile).toLowerCase();
835
+ if (p === 'performance')
836
+ return 'low';
837
+ if (p === 'balanced')
838
+ return 'medium';
839
+ if (p === 'quality')
840
+ return 'high';
841
+ if (p === 'low' || p === 'medium' || p === 'high' || p === 'ultra')
842
+ return p;
843
+ return 'high';
844
+ }
845
+ function normalizeApplyDefaults(raw, fallback) {
846
+ const src = (raw && typeof raw === 'object') ? raw : {};
847
+ const hwaccelRaw = clean(src.hwaccel).toLowerCase();
848
+ const qualityRaw = clean(src.quality).toLowerCase();
849
+ const profileRaw = src.profile;
850
+ const hwaccel = (hwaccelRaw === 'auto' || hwaccelRaw === 'nvdec' || hwaccelRaw === 'vaapi' || hwaccelRaw === 'none') ? hwaccelRaw : fallback.hwaccel;
851
+ const qualityFromProfile = mapProfileToQuality(profileRaw);
852
+ const quality = (qualityRaw === 'low' || qualityRaw === 'medium' || qualityRaw === 'high' || qualityRaw === 'ultra') ? qualityRaw : (profileRaw === undefined ? fallback.quality : qualityFromProfile);
828
853
  return {
829
- keep_services: !!defaults.keep_services,
830
- mute_audio: !!defaults.mute_audio,
831
- profile: defaults.profile,
832
- display_fps: defaults.display_fps === null ? null : clampInt(defaults.display_fps, 0, 1),
833
- seamless_loop: !!defaults.seamless_loop,
834
- loop_crossfade: !!defaults.loop_crossfade,
835
- loop_crossfade_seconds: clampFloat(defaults.loop_crossfade_seconds, 0.35, 0),
836
- optimize: !!defaults.optimize,
837
- proxy_width: variant === '4k'
838
- ? clampInt(defaults.proxy_width_4k, 3840, 320)
839
- : clampInt(defaults.proxy_width_hd, 1920, 320),
840
- proxy_fps: clampInt(defaults.proxy_fps, 60, 1, 240),
841
- proxy_crf: variant === '4k'
842
- ? clampInt(defaults.proxy_crf_4k, 16, 1, 51)
843
- : clampInt(defaults.proxy_crf_hd, 18, 1, 51)
854
+ video_fps: clampInt(Number(src.video_fps ?? (src.display_fps === null ? fallback.video_fps : src.display_fps)), fallback.video_fps, 1, 240),
855
+ video_speed: clampFloat(Number(src.video_speed), fallback.video_speed, 0.1, 4.0),
856
+ hwaccel,
857
+ quality,
858
+ pause_on_steam_game: src.pause_on_steam_game === undefined
859
+ ? fallback.pause_on_steam_game
860
+ : parseBoolish(String(src.pause_on_steam_game), fallback.pause_on_steam_game),
861
+ steam_poll_ms: clampInt(Number(src.steam_poll_ms), fallback.steam_poll_ms, 200, 120000)
844
862
  };
845
863
  }
846
- function normalizeVideoConfig(config, fallback) {
847
- const displayFpsInput = config?.display_fps;
848
- const displayFps = displayFpsInput === null
849
- ? null
850
- : (displayFpsInput === undefined
851
- ? fallback.display_fps
852
- : clampInt(displayFpsInput, fallback.display_fps ?? 30, 1, 240));
853
- const profile = config?.profile === 'performance' || config?.profile === 'balanced' || config?.profile === 'quality'
854
- ? config.profile
855
- : fallback.profile;
856
- return {
857
- keep_services: config?.keep_services ?? fallback.keep_services,
858
- mute_audio: config?.mute_audio ?? fallback.mute_audio,
859
- profile,
860
- display_fps: displayFps,
861
- seamless_loop: config?.seamless_loop ?? fallback.seamless_loop,
862
- loop_crossfade: config?.loop_crossfade ?? fallback.loop_crossfade,
863
- loop_crossfade_seconds: clampFloat(config?.loop_crossfade_seconds ?? fallback.loop_crossfade_seconds, fallback.loop_crossfade_seconds, 0),
864
- optimize: config?.optimize ?? fallback.optimize,
865
- proxy_width: clampInt(config?.proxy_width ?? fallback.proxy_width, fallback.proxy_width, 320),
866
- proxy_fps: clampInt(config?.proxy_fps ?? fallback.proxy_fps, fallback.proxy_fps, 1, 240),
867
- proxy_crf: clampInt(config?.proxy_crf ?? fallback.proxy_crf, fallback.proxy_crf, 1, 51)
868
- };
864
+ function rendercoreEnvPath() {
865
+ return node_path_1.default.join(node_os_1.default.homedir(), '.config', 'kitsune-rendercore', 'env');
866
+ }
867
+ function syncRendercoreEnv(defaults) {
868
+ const p = rendercoreEnvPath();
869
+ (0, fs_1.ensureDir)(node_path_1.default.dirname(p));
870
+ const existing = {};
871
+ if (node_fs_1.default.existsSync(p)) {
872
+ const raw = node_fs_1.default.readFileSync(p, 'utf8');
873
+ for (const line of raw.split('\n')) {
874
+ const t = line.trim();
875
+ if (!t || t.startsWith('#'))
876
+ continue;
877
+ const eq = t.indexOf('=');
878
+ if (eq <= 0)
879
+ continue;
880
+ existing[t.slice(0, eq).trim()] = t.slice(eq + 1).trim();
881
+ }
882
+ }
883
+ existing.KRC_VIDEO_FPS = String(defaults.video_fps);
884
+ existing.KRC_VIDEO_SPEED = String(defaults.video_speed);
885
+ existing.KRC_HWACCEL = defaults.hwaccel;
886
+ existing.KRC_QUALITY = defaults.quality;
887
+ existing.KRC_PAUSE_ON_STEAM_GAME = defaults.pause_on_steam_game ? 'true' : 'false';
888
+ existing.KRC_STEAM_POLL_MS = String(defaults.steam_poll_ms);
889
+ const ordered = [
890
+ 'KRC_VIDEO_DEFAULT',
891
+ 'KRC_VIDEO_MAP_FILE',
892
+ 'KRC_VIDEO_MAP',
893
+ 'KRC_VIDEO_FPS',
894
+ 'KRC_VIDEO_SPEED',
895
+ 'KRC_HWACCEL',
896
+ 'KRC_QUALITY',
897
+ 'KRC_PAUSE_ON_STEAM_GAME',
898
+ 'KRC_STEAM_POLL_MS'
899
+ ];
900
+ const keys = Array.from(new Set([...ordered, ...Object.keys(existing)])).sort((a, b) => {
901
+ const ia = ordered.indexOf(a);
902
+ const ib = ordered.indexOf(b);
903
+ if (ia >= 0 && ib >= 0)
904
+ return ia - ib;
905
+ if (ia >= 0)
906
+ return -1;
907
+ if (ib >= 0)
908
+ return 1;
909
+ return a.localeCompare(b);
910
+ });
911
+ const lines = [];
912
+ for (const key of keys) {
913
+ const value = existing[key];
914
+ if (value === undefined)
915
+ continue;
916
+ lines.push(`${key}=${value}`);
917
+ }
918
+ node_fs_1.default.writeFileSync(p, `${lines.join('\n')}\n`, 'utf8');
869
919
  }
870
920
  async function resolvePost(provider, pageUrl) {
871
921
  const html = await fetchHtml(pageUrl, pageUrl);
@@ -1039,26 +1089,6 @@ function parseId(id) {
1039
1089
  throw new Error(`Invalid variant in id: ${id}`);
1040
1090
  return { provider, slug, variant };
1041
1091
  }
1042
- async function buildApplyCommand(index, item, monitor) {
1043
- const defs = index.apply_defaults;
1044
- const proxyWidth = item.variant === '4k' ? 3840 : 1920;
1045
- const proxyCrf = item.variant === '4k' ? defs.proxy_crf_4k : defs.proxy_crf_hd;
1046
- const bool = (v) => (v ? 'true' : 'false');
1047
- const args = [
1048
- 'video-play',
1049
- item.file_path,
1050
- '--monitor', monitor,
1051
- '--profile', defs.profile,
1052
- '--seamless-loop', bool(defs.seamless_loop),
1053
- '--loop-crossfade', bool(defs.loop_crossfade),
1054
- '--loop-crossfade-seconds', String(defs.loop_crossfade_seconds),
1055
- '--optimize', bool(defs.optimize),
1056
- '--proxy-width', String(proxyWidth),
1057
- '--proxy-fps', String(defs.proxy_fps),
1058
- '--proxy-crf', String(proxyCrf)
1059
- ];
1060
- return { cmd: index.runner.bin_name, args };
1061
- }
1062
1092
  async function liveResolve(pageUrl) {
1063
1093
  const provider = providerFromUrl(pageUrl);
1064
1094
  const post = await resolvePost(provider, pageUrl);
@@ -1428,7 +1458,6 @@ async function liveFetch(opts) {
1428
1458
  }
1429
1459
  const id = itemId(provider, slug, selected.variant);
1430
1460
  const thumbPath = await generateThumb(filePath, id);
1431
- const defaults = readIndex().apply_defaults;
1432
1461
  const item = {
1433
1462
  id,
1434
1463
  provider,
@@ -1442,25 +1471,19 @@ async function liveFetch(opts) {
1442
1471
  size_bytes: sizeBytes,
1443
1472
  favorite: false,
1444
1473
  added_at: nowUnix(),
1445
- last_applied_at: 0,
1446
- video_config: buildVideoConfigFromDefaults(defaults, selected.variant)
1474
+ last_applied_at: 0
1447
1475
  };
1448
1476
  withLiveLock(() => {
1449
1477
  const current = readIndex();
1450
1478
  const prev = current.items.find(v => v.id === item.id);
1451
- const fallbackCfg = buildVideoConfigFromDefaults(current.apply_defaults, item.variant);
1452
1479
  const next = prev
1453
1480
  ? {
1454
1481
  ...item,
1455
1482
  favorite: prev.favorite,
1456
1483
  added_at: prev.added_at || item.added_at,
1457
- last_applied_at: prev.last_applied_at || 0,
1458
- video_config: normalizeVideoConfig(prev.video_config, fallbackCfg)
1484
+ last_applied_at: prev.last_applied_at || 0
1459
1485
  }
1460
- : {
1461
- ...item,
1462
- video_config: normalizeVideoConfig(item.video_config, fallbackCfg)
1463
- };
1486
+ : item;
1464
1487
  const updated = upsertItem(current, next);
1465
1488
  writeIndexAtomic(updated);
1466
1489
  return true;
@@ -1493,49 +1516,30 @@ async function liveApply(opts) {
1493
1516
  if (!item || !node_fs_1.default.existsSync(item.file_path)) {
1494
1517
  throw new Error(`Live file not found for ${idRaw}. Re-download it with live fetch.`);
1495
1518
  }
1496
- const fallbackCfg = buildVideoConfigFromDefaults(index.apply_defaults, item.variant);
1497
- const cfg = normalizeVideoConfig(item.video_config, fallbackCfg);
1498
1519
  const bin = index.runner.bin_name;
1499
1520
  const setVideoArgs = [
1500
- 'config',
1501
1521
  'set-video',
1502
1522
  '--monitor', monitor,
1503
- '--video', item.file_path,
1504
- '--keep-services', cfg.keep_services ? 'true' : 'false',
1505
- '--profile', cfg.profile,
1506
- '--loop-crossfade-seconds', String(cfg.loop_crossfade_seconds),
1507
- '--proxy-width', String(cfg.proxy_width),
1508
- '--proxy-fps', String(cfg.proxy_fps),
1509
- '--proxy-crf', String(cfg.proxy_crf)
1523
+ '--video', item.file_path
1510
1524
  ];
1511
- if (cfg.mute_audio)
1512
- setVideoArgs.push('--mute-audio');
1513
- if (cfg.display_fps !== null)
1514
- setVideoArgs.push('--display-fps', String(cfg.display_fps));
1515
- if (cfg.seamless_loop)
1516
- setVideoArgs.push('--seamless-loop');
1517
- if (cfg.loop_crossfade)
1518
- setVideoArgs.push('--loop-crossfade');
1519
- if (cfg.optimize)
1520
- setVideoArgs.push('--optimize');
1521
1525
  await (0, exec_1.run)(bin, setVideoArgs, { timeoutMs: 120000 });
1522
- // Keep livewallpaper authority persistent across session restarts when user applies from library.
1526
+ // Live mode owns wallpaper state: stop static rotation services while live is active.
1523
1527
  try {
1524
- await (0, exec_1.run)(bin, ['service-autostart', 'install', '--overwrite'], { timeoutMs: 15000 });
1528
+ await (0, workshop_1.workshopCoexistenceEnter)();
1525
1529
  }
1526
1530
  catch { }
1531
+ // Keep live authority persistent across session restarts.
1527
1532
  try {
1528
- await (0, exec_1.run)(bin, ['service-autostart', 'enable'], { timeoutMs: 15000 });
1533
+ await (0, exec_1.run)(bin, ['service', 'install'], { timeoutMs: 20000 });
1529
1534
  }
1530
1535
  catch { }
1531
1536
  try {
1532
- await (0, exec_1.run)(bin, ['stop-services'], { timeoutMs: 15000 });
1537
+ await (0, exec_1.run)(bin, ['service', 'enable'], { timeoutMs: 20000 });
1533
1538
  }
1534
1539
  catch { }
1535
- await (0, exec_1.run)(bin, ['start-config'], { timeoutMs: 120000 });
1536
1540
  withLiveLock(() => {
1537
1541
  const current = readIndex();
1538
- const updatedItems = current.items.map(v => (v.id === item.id ? { ...v, last_applied_at: nowUnix(), video_config: cfg } : v));
1542
+ const updatedItems = current.items.map(v => (v.id === item.id ? { ...v, last_applied_at: nowUnix() } : v));
1539
1543
  const perMonitor = { ...current.per_monitor };
1540
1544
  const currentMon = perMonitor[monitor] || {
1541
1545
  auto_apply: false,
@@ -1555,7 +1559,7 @@ async function liveApply(opts) {
1555
1559
  monitor,
1556
1560
  runner: index.runner.mode,
1557
1561
  command: bin,
1558
- args: ['start-config']
1562
+ args: setVideoArgs
1559
1563
  };
1560
1564
  }
1561
1565
  function liveFavorite(id, on) {
@@ -1685,8 +1689,8 @@ async function liveDoctor(opts) {
1685
1689
  }
1686
1690
  if (opts?.fix && deps.runner_bin) {
1687
1691
  try {
1688
- await (0, exec_1.run)(index.runner.bin_name, ['install-dependencies'], { timeoutMs: 180000 });
1689
- fix.push(`Executed: ${index.runner.bin_name} install-dependencies`);
1692
+ await (0, exec_1.run)(index.runner.bin_name, ['install-deps'], { timeoutMs: 180000 });
1693
+ fix.push(`Executed: ${index.runner.bin_name} install-deps`);
1690
1694
  try {
1691
1695
  await (0, exec_1.run)('ffmpeg', ['-version'], { timeoutMs: 4000 });
1692
1696
  deps.ffmpeg = true;
@@ -1703,11 +1707,11 @@ async function liveDoctor(opts) {
1703
1707
  }
1704
1708
  }
1705
1709
  catch (e) {
1706
- fix.push(`Failed to execute '${index.runner.bin_name} install-dependencies': ${String(e)}`);
1710
+ fix.push(`Failed to execute '${index.runner.bin_name} install-deps': ${String(e)}`);
1707
1711
  }
1708
1712
  }
1709
1713
  else if (!deps.ffmpeg || !deps.hyprctl) {
1710
- fix.push(`Run '${index.runner.bin_name} install-dependencies' to install missing runtime dependencies`);
1714
+ fix.push(`Run '${index.runner.bin_name} install-deps' to install missing runtime dependencies`);
1711
1715
  }
1712
1716
  return {
1713
1717
  ok: Object.values(deps).every(Boolean) || (deps.hyprctl === false && Object.keys(deps).filter(k => k !== 'hyprctl').every(k => deps[k])),
@@ -1717,6 +1721,32 @@ async function liveDoctor(opts) {
1717
1721
  fix
1718
1722
  };
1719
1723
  }
1724
+ async function liveServiceAutostart(action) {
1725
+ const index = readIndex();
1726
+ let bin = index.runner.bin_name;
1727
+ let out;
1728
+ try {
1729
+ out = await (0, exec_1.run)(bin, ['service', action], { timeoutMs: 30000 });
1730
+ }
1731
+ catch (err) {
1732
+ // Legacy compatibility: if user still points to kitsune-livewallpaper,
1733
+ // map to its older subcommand to avoid hard failure.
1734
+ if (clean(bin) === 'kitsune-livewallpaper') {
1735
+ out = await (0, exec_1.run)(bin, ['service-autostart', action], { timeoutMs: 30000 });
1736
+ }
1737
+ else {
1738
+ throw err;
1739
+ }
1740
+ }
1741
+ return {
1742
+ ok: true,
1743
+ action,
1744
+ runner: bin,
1745
+ stdout: out.stdout.trim(),
1746
+ stderr: out.stderr.trim(),
1747
+ code: out.code
1748
+ };
1749
+ }
1720
1750
  function liveSetRunner(opts) {
1721
1751
  return withLiveLock(() => {
1722
1752
  const current = readIndex();
@@ -1730,36 +1760,12 @@ function liveSetRunner(opts) {
1730
1760
  });
1731
1761
  }
1732
1762
  function liveGetWallpaperConfig(id) {
1733
- const itemIdValue = clean(id);
1734
- if (!itemIdValue)
1735
- throw new Error('id is required');
1736
- const index = readIndex();
1737
- const item = index.items.find(v => v.id === itemIdValue);
1738
- if (!item)
1739
- throw new Error(`Live item not found: ${itemIdValue}`);
1740
- const fallback = buildVideoConfigFromDefaults(index.apply_defaults, item.variant);
1741
- return {
1742
- ok: true,
1743
- id: item.id,
1744
- video_config: normalizeVideoConfig(item.video_config, fallback)
1745
- };
1763
+ throw new Error('Per-wallpaper config is no longer supported. Use: live config apply-defaults');
1746
1764
  }
1747
1765
  function liveSetWallpaperConfig(id, opts) {
1748
- const itemIdValue = clean(id);
1749
- if (!itemIdValue)
1750
- throw new Error('id is required');
1751
- return withLiveLock(() => {
1752
- const current = readIndex();
1753
- const item = current.items.find(v => v.id === itemIdValue);
1754
- if (!item)
1755
- throw new Error(`Live item not found: ${itemIdValue}`);
1756
- const fallback = buildVideoConfigFromDefaults(current.apply_defaults, item.variant);
1757
- const previous = normalizeVideoConfig(item.video_config, fallback);
1758
- const nextCfg = normalizeVideoConfig({ ...previous, ...opts }, fallback);
1759
- const items = current.items.map(v => (v.id === itemIdValue ? { ...v, video_config: nextCfg } : v));
1760
- writeIndexAtomic({ ...current, items });
1761
- return { ok: true, id: itemIdValue, video_config: nextCfg };
1762
- });
1766
+ void id;
1767
+ void opts;
1768
+ throw new Error('Per-wallpaper config is no longer supported. Use: live config apply-defaults');
1763
1769
  }
1764
1770
  function liveGetConfig() {
1765
1771
  const index = readIndex();
@@ -1768,34 +1774,12 @@ function liveGetConfig() {
1768
1774
  function liveSetApplyDefaults(opts) {
1769
1775
  return withLiveLock(() => {
1770
1776
  const current = readIndex();
1771
- const next = { ...current.apply_defaults };
1772
- if (opts.keep_services !== undefined)
1773
- next.keep_services = opts.keep_services;
1774
- if (opts.mute_audio !== undefined)
1775
- next.mute_audio = opts.mute_audio;
1776
- if (opts.profile !== undefined)
1777
- next.profile = opts.profile;
1778
- if (opts.display_fps !== undefined)
1779
- next.display_fps = opts.display_fps;
1780
- if (opts.seamless_loop !== undefined)
1781
- next.seamless_loop = opts.seamless_loop;
1782
- if (opts.loop_crossfade !== undefined)
1783
- next.loop_crossfade = opts.loop_crossfade;
1784
- if (opts.loop_crossfade_seconds !== undefined)
1785
- next.loop_crossfade_seconds = opts.loop_crossfade_seconds;
1786
- if (opts.optimize !== undefined)
1787
- next.optimize = opts.optimize;
1788
- if (opts.proxy_width_hd !== undefined)
1789
- next.proxy_width_hd = opts.proxy_width_hd;
1790
- if (opts.proxy_width_4k !== undefined)
1791
- next.proxy_width_4k = opts.proxy_width_4k;
1792
- if (opts.proxy_fps !== undefined)
1793
- next.proxy_fps = opts.proxy_fps;
1794
- if (opts.proxy_crf_hd !== undefined)
1795
- next.proxy_crf_hd = opts.proxy_crf_hd;
1796
- if (opts.proxy_crf_4k !== undefined)
1797
- next.proxy_crf_4k = opts.proxy_crf_4k;
1777
+ const next = normalizeApplyDefaults({
1778
+ ...current.apply_defaults,
1779
+ ...opts
1780
+ }, current.apply_defaults);
1798
1781
  writeIndexAtomic({ ...current, apply_defaults: next });
1782
+ syncRendercoreEnv(next);
1799
1783
  return { ok: true, apply_defaults: next };
1800
1784
  });
1801
1785
  }
@@ -956,6 +956,29 @@ function killPid(pid) {
956
956
  return false;
957
957
  }
958
958
  }
959
+ async function stopKnownLiveProcesses() {
960
+ // Live V2 can run wallpapers without registering pids in workshop active-state.
961
+ // Stop common runtime processes as best-effort fallback.
962
+ const patterns = [
963
+ 'mpvpaper',
964
+ 'kitsune-livewallpaper',
965
+ 'kitsune-rendercore'
966
+ ];
967
+ for (const pattern of patterns) {
968
+ try {
969
+ await (0, exec_1.run)('pkill', ['-f', pattern]);
970
+ }
971
+ catch {
972
+ // best effort
973
+ }
974
+ }
975
+ try {
976
+ await (0, exec_1.run)('systemctl', ['--user', 'stop', 'kitsune-rendercore.service']);
977
+ }
978
+ catch {
979
+ // best effort
980
+ }
981
+ }
959
982
  function workshopActiveStatus() {
960
983
  const paths = getWePaths();
961
984
  ensureWePaths(paths);
@@ -1086,9 +1109,13 @@ async function workshopStop(options) {
1086
1109
  }
1087
1110
  }
1088
1111
  else if (options?.all) {
1112
+ await stopKnownLiveProcesses();
1089
1113
  clearActiveState();
1090
1114
  shouldRestore = true;
1091
1115
  }
1116
+ if (options?.all) {
1117
+ await stopKnownLiveProcesses();
1118
+ }
1092
1119
  const coexist = shouldRestore ? await workshopCoexistenceExit() : { ok: true, restored: [] };
1093
1120
  return {
1094
1121
  ok: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kitowall",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "CLI/daemon for Hyprland wallpapers using swww with pack-based rotation.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "type": "commonjs",