formalconf 2.0.7 → 2.0.9

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.
Files changed (3) hide show
  1. package/README.md +33 -0
  2. package/dist/formalconf.js +1980 -212
  3. package/package.json +1 -1
@@ -6,12 +6,14 @@ import { spawn as nodeSpawn } from "child_process";
6
6
  import { readFile as nodeReadFile, writeFile as nodeWriteFile, mkdir } from "fs/promises";
7
7
  import { dirname } from "path";
8
8
  import { fileURLToPath } from "url";
9
- async function exec(command, cwd) {
9
+ async function exec(command, cwd, env) {
10
+ const mergedEnv = env ? { ...process.env, ...env } : undefined;
10
11
  if (isBun) {
11
12
  const proc = Bun.spawn(command, {
12
13
  stdout: "pipe",
13
14
  stderr: "pipe",
14
- cwd
15
+ cwd,
16
+ env: mergedEnv
15
17
  });
16
18
  const [stdout, stderr] = await Promise.all([
17
19
  new Response(proc.stdout).text(),
@@ -27,7 +29,7 @@ async function exec(command, cwd) {
27
29
  }
28
30
  return new Promise((resolve) => {
29
31
  const [cmd, ...args] = command;
30
- const proc = nodeSpawn(cmd, args, { cwd, shell: false });
32
+ const proc = nodeSpawn(cmd, args, { cwd, shell: false, env: mergedEnv });
31
33
  let stdout = "";
32
34
  let stderr = "";
33
35
  proc.stdout?.on("data", (data) => {
@@ -315,8 +317,17 @@ var scriptPath = getScriptDir(import.meta);
315
317
  var ROOT_DIR = join(scriptPath, "..", "..");
316
318
  var CONFIGS_DIR = join(CONFIG_DIR, "configs");
317
319
  var THEMES_DIR = join(CONFIG_DIR, "themes");
320
+ var HOOKS_DIR = join(CONFIG_DIR, "hooks");
318
321
  var PKG_CONFIG_PATH = join(CONFIG_DIR, "pkg-config.json");
319
322
  var PKG_LOCK_PATH = join(CONFIG_DIR, "pkg-lock.json");
323
+ var THEME_CONFIG_PATH = join(CONFIG_DIR, "theme-config.json");
324
+ var TEMPLATES_DIR = join(CONFIG_DIR, "templates");
325
+ var TEMPLATES_MANIFEST_PATH = join(TEMPLATES_DIR, "templates.json");
326
+ var GENERATED_DIR = join(CONFIG_DIR, "generated");
327
+ var BUNDLED_TEMPLATES_DIR = join(ROOT_DIR, "templates");
328
+ var BUNDLED_MANIFEST_PATH = join(BUNDLED_TEMPLATES_DIR, "templates.json");
329
+ var GTK_DIR = join(CONFIG_DIR, "gtk");
330
+ var COLLOID_DIR = join(GTK_DIR, "colloid-gtk-theme");
320
331
  async function ensureDir2(path) {
321
332
  await ensureDir(path);
322
333
  }
@@ -326,6 +337,8 @@ async function ensureConfigDir() {
326
337
  await ensureDir2(THEMES_DIR);
327
338
  await ensureDir2(THEME_TARGET_DIR);
328
339
  await ensureDir2(BACKGROUNDS_TARGET_DIR);
340
+ await ensureDir2(TEMPLATES_DIR);
341
+ await ensureDir2(GENERATED_DIR);
329
342
  }
330
343
  async function dirHasContents(path) {
331
344
  try {
@@ -446,7 +459,7 @@ function StatusIndicator({
446
459
  // package.json
447
460
  var package_default = {
448
461
  name: "formalconf",
449
- version: "2.0.7",
462
+ version: "2.0.9",
450
463
  description: "Dotfiles management TUI for macOS - config management, package sync, and theme switching",
451
464
  type: "module",
452
465
  main: "./dist/formalconf.js",
@@ -4502,21 +4515,34 @@ function PackageMenu({ onBack }) {
4502
4515
  // src/components/menus/ThemeMenu.tsx
4503
4516
  import { useState as useState10, useEffect as useEffect6, useMemo as useMemo4 } from "react";
4504
4517
  import { Box as Box16, Text as Text15 } from "ink";
4505
- import { existsSync as existsSync7, readdirSync as readdirSync5 } from "fs";
4506
- import { join as join6 } from "path";
4507
4518
 
4508
4519
  // src/components/ThemeCard.tsx
4509
4520
  import { Box as Box15, Text as Text14 } from "ink";
4510
4521
  import { jsxDEV as jsxDEV18 } from "react/jsx-dev-runtime";
4511
- function ThemeCard({ theme, isSelected, width }) {
4522
+ function isLegacyTheme(theme) {
4523
+ return "files" in theme;
4524
+ }
4525
+ function ThemeCard({ theme, isSelected, width, isDeviceTheme }) {
4512
4526
  const borderColor = isSelected ? colors.accent : colors.border;
4513
4527
  const nameColor = isSelected ? colors.primary : colors.text;
4514
4528
  const indicators = [];
4515
- if (theme.hasBackgrounds)
4516
- indicators.push("bg");
4517
- if (theme.isLightMode)
4518
- indicators.push("light");
4529
+ if (isDeviceTheme)
4530
+ indicators.push("device");
4531
+ if (isLegacyTheme(theme)) {
4532
+ if (theme.hasBackgrounds)
4533
+ indicators.push("bg");
4534
+ if (theme.isLightMode)
4535
+ indicators.push("light");
4536
+ } else {
4537
+ if (theme.type === "json")
4538
+ indicators.push("json");
4539
+ if (theme.hasBackgrounds)
4540
+ indicators.push("bg");
4541
+ if (theme.isLightMode)
4542
+ indicators.push("light");
4543
+ }
4519
4544
  const indicatorText = indicators.length > 0 ? ` [${indicators.join(" ")}]` : "";
4545
+ const displayName = isLegacyTheme(theme) ? theme.name : theme.displayName;
4520
4546
  return /* @__PURE__ */ jsxDEV18(Box15, {
4521
4547
  flexDirection: "column",
4522
4548
  width,
@@ -4533,7 +4559,7 @@ function ThemeCard({ theme, isSelected, width }) {
4533
4559
  color: nameColor,
4534
4560
  bold: true,
4535
4561
  wrap: "truncate",
4536
- children: theme.name
4562
+ children: displayName
4537
4563
  }, undefined, false, undefined, this),
4538
4564
  /* @__PURE__ */ jsxDEV18(Text14, {
4539
4565
  color: colors.primaryDim,
@@ -4553,6 +4579,7 @@ function useThemeGrid({
4553
4579
  layoutOverhead = 20,
4554
4580
  minCardWidth = 28,
4555
4581
  onSelect,
4582
+ onSelectAndSave,
4556
4583
  onBack,
4557
4584
  enabled = true
4558
4585
  }) {
@@ -4602,8 +4629,12 @@ function useThemeGrid({
4602
4629
  setSelectedIndex(prevIndex);
4603
4630
  }
4604
4631
  }
4605
- if (key.return && onSelect) {
4606
- onSelect(selectedIndex);
4632
+ if (key.return) {
4633
+ if (key.shift && onSelectAndSave) {
4634
+ onSelectAndSave(selectedIndex);
4635
+ } else if (onSelect) {
4636
+ onSelect(selectedIndex);
4637
+ }
4607
4638
  }
4608
4639
  });
4609
4640
  const visibleStartIndex = scrollOffset * cardsPerRow;
@@ -4623,6 +4654,18 @@ function useThemeGrid({
4623
4654
  };
4624
4655
  }
4625
4656
 
4657
+ // src/cli/set-theme.ts
4658
+ import { parseArgs as parseArgs4 } from "util";
4659
+ import {
4660
+ readdirSync as readdirSync9,
4661
+ existsSync as existsSync12,
4662
+ rmSync,
4663
+ symlinkSync,
4664
+ unlinkSync as unlinkSync2,
4665
+ copyFileSync
4666
+ } from "fs";
4667
+ import { join as join11, basename as basename4 } from "path";
4668
+
4626
4669
  // src/lib/theme-parser.ts
4627
4670
  init_runtime();
4628
4671
  import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
@@ -4698,232 +4741,1942 @@ async function parseTheme(themePath, themeName) {
4698
4741
  };
4699
4742
  }
4700
4743
 
4701
- // src/cli/set-theme.ts
4702
- import { parseArgs as parseArgs4 } from "util";
4703
- import { readdirSync as readdirSync4, existsSync as existsSync6, rmSync, symlinkSync, unlinkSync } from "fs";
4744
+ // src/lib/hooks.ts
4745
+ init_runtime();
4704
4746
  import { join as join5 } from "path";
4705
- var colors5 = {
4706
- red: "\x1B[0;31m",
4707
- green: "\x1B[0;32m",
4708
- blue: "\x1B[0;34m",
4709
- yellow: "\x1B[1;33m",
4710
- cyan: "\x1B[0;36m",
4711
- dim: "\x1B[2m",
4712
- reset: "\x1B[0m"
4713
- };
4714
- async function listThemes() {
4715
- await ensureConfigDir();
4716
- if (!existsSync6(THEMES_DIR)) {
4717
- return [];
4747
+ import { existsSync as existsSync6, readdirSync as readdirSync4, statSync } from "fs";
4748
+ async function runHooks(hookType, env = {}) {
4749
+ const hookDir = join5(HOOKS_DIR, hookType);
4750
+ if (!existsSync6(hookDir)) {
4751
+ return { executed: 0, succeeded: 0, failed: 0, results: [] };
4718
4752
  }
4719
- const entries = readdirSync4(THEMES_DIR, { withFileTypes: true });
4720
- const themes = [];
4753
+ const entries = readdirSync4(hookDir).filter((name) => !name.startsWith(".")).sort();
4754
+ const results = [];
4721
4755
  for (const entry of entries) {
4722
- if (entry.isDirectory()) {
4723
- const themePath = join5(THEMES_DIR, entry.name);
4724
- const theme = await parseTheme(themePath, entry.name);
4725
- themes.push(theme);
4756
+ const scriptPath2 = join5(hookDir, entry);
4757
+ const stat = statSync(scriptPath2);
4758
+ if (stat.isDirectory()) {
4759
+ continue;
4726
4760
  }
4761
+ const result = await exec([scriptPath2], undefined, env);
4762
+ results.push({
4763
+ script: entry,
4764
+ success: result.success,
4765
+ stdout: result.stdout,
4766
+ stderr: result.stderr,
4767
+ exitCode: result.exitCode
4768
+ });
4727
4769
  }
4728
- return themes;
4770
+ const succeeded = results.filter((r) => r.success).length;
4771
+ return {
4772
+ executed: results.length,
4773
+ succeeded,
4774
+ failed: results.length - succeeded,
4775
+ results
4776
+ };
4729
4777
  }
4730
- function clearDirectory(dir) {
4731
- if (existsSync6(dir)) {
4732
- const entries = readdirSync4(dir, { withFileTypes: true });
4733
- for (const entry of entries) {
4734
- const fullPath = join5(dir, entry.name);
4735
- if (entry.isSymbolicLink() || entry.isFile()) {
4736
- unlinkSync(fullPath);
4737
- } else if (entry.isDirectory()) {
4738
- rmSync(fullPath, { recursive: true, force: true });
4739
- }
4740
- }
4778
+
4779
+ // src/lib/theme-config.ts
4780
+ import { hostname } from "os";
4781
+ import { existsSync as existsSync7, readFileSync, writeFileSync } from "fs";
4782
+ var DEFAULT_CONFIG = {
4783
+ version: 1,
4784
+ defaultTheme: null,
4785
+ devices: {}
4786
+ };
4787
+ function getDeviceHostname() {
4788
+ return hostname();
4789
+ }
4790
+ function loadThemeConfig() {
4791
+ if (!existsSync7(THEME_CONFIG_PATH)) {
4792
+ return { ...DEFAULT_CONFIG, devices: {} };
4793
+ }
4794
+ try {
4795
+ const content = readFileSync(THEME_CONFIG_PATH, "utf-8");
4796
+ const parsed = JSON.parse(content);
4797
+ return {
4798
+ version: parsed.version ?? 1,
4799
+ defaultTheme: parsed.defaultTheme ?? null,
4800
+ devices: parsed.devices ?? {}
4801
+ };
4802
+ } catch {
4803
+ return { ...DEFAULT_CONFIG, devices: {} };
4741
4804
  }
4742
4805
  }
4743
- function createSymlink(source, target) {
4744
- if (existsSync6(target)) {
4745
- unlinkSync(target);
4806
+ async function saveThemeConfig(config) {
4807
+ await ensureConfigDir();
4808
+ writeFileSync(THEME_CONFIG_PATH, JSON.stringify(config, null, 2) + `
4809
+ `);
4810
+ }
4811
+ function getDeviceTheme() {
4812
+ const config = loadThemeConfig();
4813
+ const device = getDeviceHostname();
4814
+ const mapping = config.devices[device];
4815
+ if (mapping) {
4816
+ return mapping.theme;
4817
+ }
4818
+ return config.defaultTheme;
4819
+ }
4820
+ async function setDeviceTheme(themeName) {
4821
+ const config = loadThemeConfig();
4822
+ const device = getDeviceHostname();
4823
+ config.devices[device] = {
4824
+ theme: themeName,
4825
+ setAt: new Date().toISOString()
4826
+ };
4827
+ await saveThemeConfig(config);
4828
+ }
4829
+ async function setDefaultTheme(themeName) {
4830
+ const config = loadThemeConfig();
4831
+ config.defaultTheme = themeName;
4832
+ await saveThemeConfig(config);
4833
+ }
4834
+ async function clearDeviceTheme() {
4835
+ const config = loadThemeConfig();
4836
+ const device = getDeviceHostname();
4837
+ delete config.devices[device];
4838
+ await saveThemeConfig(config);
4839
+ }
4840
+ function listDeviceMappings() {
4841
+ const config = loadThemeConfig();
4842
+ const currentDevice = getDeviceHostname();
4843
+ return Object.entries(config.devices).map(([device, mapping]) => ({
4844
+ device,
4845
+ theme: mapping.theme,
4846
+ setAt: mapping.setAt,
4847
+ isCurrent: device === currentDevice
4848
+ }));
4849
+ }
4850
+ function getDefaultTheme() {
4851
+ const config = loadThemeConfig();
4852
+ return config.defaultTheme;
4853
+ }
4854
+
4855
+ // src/lib/theme-v2/loader.ts
4856
+ init_runtime();
4857
+ import { existsSync as existsSync8, readdirSync as readdirSync5 } from "fs";
4858
+ import { join as join6, basename as basename2 } from "path";
4859
+
4860
+ // src/lib/theme-v2/color.ts
4861
+ function isValidHex(hex) {
4862
+ return /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/.test(hex);
4863
+ }
4864
+ function normalizeHex(hex) {
4865
+ if (!hex.startsWith("#")) {
4866
+ hex = `#${hex}`;
4867
+ }
4868
+ if (hex.length === 4) {
4869
+ const r = hex[1];
4870
+ const g = hex[2];
4871
+ const b = hex[3];
4872
+ hex = `#${r}${r}${g}${g}${b}${b}`;
4873
+ }
4874
+ return hex.toUpperCase();
4875
+ }
4876
+ function hexToRgb(hex) {
4877
+ const normalized = normalizeHex(hex);
4878
+ if (!isValidHex(normalized)) {
4879
+ throw new Error(`Invalid hex color: ${hex}`);
4880
+ }
4881
+ const r = parseInt(normalized.slice(1, 3), 16);
4882
+ const g = parseInt(normalized.slice(3, 5), 16);
4883
+ const b = parseInt(normalized.slice(5, 7), 16);
4884
+ return { r, g, b };
4885
+ }
4886
+ function hexToColorVariable(hex) {
4887
+ const normalized = normalizeHex(hex);
4888
+ const { r, g, b } = hexToRgb(normalized);
4889
+ return {
4890
+ hex: normalized,
4891
+ strip: normalized.slice(1),
4892
+ rgb: `rgb(${r},${g},${b})`,
4893
+ rgba: `rgba(${r},${g},${b},1)`,
4894
+ r,
4895
+ g,
4896
+ b,
4897
+ red: r / 255,
4898
+ green: g / 255,
4899
+ blue: b / 255
4900
+ };
4901
+ }
4902
+ function hexToColorVariableOrDefault(hex, fallback) {
4903
+ return hexToColorVariable(hex ?? fallback);
4904
+ }
4905
+
4906
+ // src/lib/theme-v2/validator.ts
4907
+ var REQUIRED_PALETTE_COLORS = [
4908
+ "color0",
4909
+ "color1",
4910
+ "color2",
4911
+ "color3",
4912
+ "color4",
4913
+ "color5",
4914
+ "color6",
4915
+ "color7",
4916
+ "color8",
4917
+ "color9",
4918
+ "color10",
4919
+ "color11",
4920
+ "color12",
4921
+ "color13",
4922
+ "color14",
4923
+ "color15",
4924
+ "background",
4925
+ "foreground",
4926
+ "cursor"
4927
+ ];
4928
+ var OPTIONAL_PALETTE_COLORS = [
4929
+ "selection_background",
4930
+ "selection_foreground",
4931
+ "accent",
4932
+ "border"
4933
+ ];
4934
+ function validateColor(value, path) {
4935
+ if (typeof value !== "string") {
4936
+ return { path, message: "must be a string" };
4746
4937
  }
4747
- symlinkSync(source, target);
4938
+ if (!isValidHex(value)) {
4939
+ return { path, message: `invalid hex color: ${value}` };
4940
+ }
4941
+ return null;
4748
4942
  }
4749
- async function applyTheme(themeName) {
4750
- const themeDir = join5(THEMES_DIR, themeName);
4751
- if (!existsSync6(themeDir)) {
4752
- return { output: `Theme '${themeName}' not found`, success: false };
4943
+ function isValidUrl(value) {
4944
+ try {
4945
+ const url = new URL(value);
4946
+ return ["http:", "https:"].includes(url.protocol);
4947
+ } catch {
4948
+ return false;
4753
4949
  }
4754
- await ensureConfigDir();
4755
- await ensureDir2(THEME_TARGET_DIR);
4756
- const theme = await parseTheme(themeDir, themeName);
4757
- clearDirectory(THEME_TARGET_DIR);
4758
- if (existsSync6(BACKGROUNDS_TARGET_DIR)) {
4759
- rmSync(BACKGROUNDS_TARGET_DIR, { recursive: true, force: true });
4950
+ }
4951
+ function validateUrlArray(arr, path) {
4952
+ const errors = [];
4953
+ if (!Array.isArray(arr)) {
4954
+ errors.push({ path, message: "must be an array" });
4955
+ return errors;
4760
4956
  }
4761
- const entries = readdirSync4(themeDir, { withFileTypes: true });
4762
- for (const entry of entries) {
4763
- const source = join5(themeDir, entry.name);
4764
- if (entry.isFile() && entry.name !== "theme.yaml" && entry.name !== "light.mode") {
4765
- const target = join5(THEME_TARGET_DIR, entry.name);
4766
- createSymlink(source, target);
4767
- }
4957
+ if (arr.length === 0) {
4958
+ errors.push({ path, message: "must have at least one URL" });
4959
+ return errors;
4768
4960
  }
4769
- if (theme.hasBackgrounds) {
4770
- const backgroundsSource = join5(themeDir, "backgrounds");
4771
- createSymlink(backgroundsSource, BACKGROUNDS_TARGET_DIR);
4961
+ for (let i = 0;i < arr.length; i++) {
4962
+ const item = arr[i];
4963
+ if (typeof item !== "string") {
4964
+ errors.push({ path: `${path}[${i}]`, message: "must be a string" });
4965
+ } else if (!isValidUrl(item)) {
4966
+ errors.push({ path: `${path}[${i}]`, message: "must be a valid http/https URL" });
4967
+ }
4772
4968
  }
4773
- let output = `Theme '${theme.name}' applied successfully`;
4774
- if (theme.metadata?.author) {
4775
- output += `
4776
- Author: ${theme.metadata.author}`;
4969
+ return errors;
4970
+ }
4971
+ function validateWallpapers(config, path) {
4972
+ const errors = [];
4973
+ if (typeof config !== "object" || config === null) {
4974
+ errors.push({ path, message: "must be an object" });
4975
+ return errors;
4777
4976
  }
4778
- if (theme.hasBackgrounds) {
4779
- output += `
4780
- Wallpapers available at: ~/.config/formalconf/current/backgrounds/`;
4977
+ const obj = config;
4978
+ if (!("dark" in obj)) {
4979
+ errors.push({ path: `${path}.dark`, message: "is required" });
4980
+ } else {
4981
+ errors.push(...validateUrlArray(obj.dark, `${path}.dark`));
4781
4982
  }
4782
- if (theme.isLightMode) {
4783
- output += `
4784
- Note: This is a light mode theme`;
4983
+ if ("light" in obj) {
4984
+ errors.push(...validateUrlArray(obj.light, `${path}.light`));
4785
4985
  }
4786
- return { output, success: true };
4986
+ return errors;
4787
4987
  }
4788
- async function showThemeInfo(themeName) {
4789
- const themeDir = join5(THEMES_DIR, themeName);
4790
- if (!existsSync6(themeDir)) {
4791
- console.error(`${colors5.red}Error: Theme '${themeName}' not found${colors5.reset}`);
4792
- process.exit(1);
4988
+ function validatePalette(palette, path) {
4989
+ const errors = [];
4990
+ if (typeof palette !== "object" || palette === null) {
4991
+ errors.push({ path, message: "must be an object" });
4992
+ return errors;
4793
4993
  }
4794
- const theme = await parseTheme(themeDir, themeName);
4795
- console.log(`
4796
- ${colors5.cyan}Theme: ${theme.name}${colors5.reset}`);
4797
- if (theme.metadata) {
4798
- if (theme.metadata.author)
4799
- console.log(`Author: ${theme.metadata.author}`);
4800
- if (theme.metadata.description)
4801
- console.log(`Description: ${theme.metadata.description}`);
4802
- if (theme.metadata.version)
4803
- console.log(`Version: ${theme.metadata.version}`);
4804
- if (theme.metadata.source)
4805
- console.log(`Source: ${theme.metadata.source}`);
4994
+ const obj = palette;
4995
+ for (const color of REQUIRED_PALETTE_COLORS) {
4996
+ if (!(color in obj)) {
4997
+ errors.push({ path: `${path}.${color}`, message: "is required" });
4998
+ } else {
4999
+ const error = validateColor(obj[color], `${path}.${color}`);
5000
+ if (error)
5001
+ errors.push(error);
5002
+ }
4806
5003
  }
4807
- console.log(`
4808
- Files (${theme.files.length}):`);
4809
- for (const file of theme.files) {
4810
- console.log(` ${colors5.blue}•${colors5.reset} ${file.name}`);
5004
+ for (const color of OPTIONAL_PALETTE_COLORS) {
5005
+ if (color in obj) {
5006
+ const error = validateColor(obj[color], `${path}.${color}`);
5007
+ if (error)
5008
+ errors.push(error);
5009
+ }
4811
5010
  }
4812
- if (theme.hasBackgrounds) {
4813
- console.log(`
4814
- ${colors5.green}Has wallpapers${colors5.reset}`);
5011
+ return errors;
5012
+ }
5013
+ function validateNeovimConfig(config, path) {
5014
+ const errors = [];
5015
+ if (typeof config !== "object" || config === null) {
5016
+ errors.push({ path, message: "must be an object" });
5017
+ return errors;
4815
5018
  }
4816
- if (theme.hasPreview) {
4817
- console.log(`${colors5.green}Has preview image${colors5.reset}`);
5019
+ const obj = config;
5020
+ if (!("repo" in obj) || typeof obj.repo !== "string") {
5021
+ errors.push({ path: `${path}.repo`, message: "is required and must be a string" });
4818
5022
  }
4819
- if (theme.isLightMode) {
4820
- console.log(`${colors5.yellow}Light mode theme${colors5.reset}`);
5023
+ if (!("colorscheme" in obj) || typeof obj.colorscheme !== "string") {
5024
+ errors.push({
5025
+ path: `${path}.colorscheme`,
5026
+ message: "is required and must be a string"
5027
+ });
5028
+ }
5029
+ if ("light_colorscheme" in obj && typeof obj.light_colorscheme !== "string") {
5030
+ errors.push({
5031
+ path: `${path}.light_colorscheme`,
5032
+ message: "must be a string"
5033
+ });
5034
+ }
5035
+ if ("opts" in obj && (typeof obj.opts !== "object" || obj.opts === null)) {
5036
+ errors.push({ path: `${path}.opts`, message: "must be an object" });
4821
5037
  }
5038
+ return errors;
4822
5039
  }
4823
- async function runSetTheme(themeName) {
4824
- return applyTheme(themeName);
5040
+ function validateGtkConfig(config, path) {
5041
+ const errors = [];
5042
+ if (typeof config !== "object" || config === null) {
5043
+ errors.push({ path, message: "must be an object" });
5044
+ return errors;
5045
+ }
5046
+ const obj = config;
5047
+ if ("variant" in obj && typeof obj.variant !== "string") {
5048
+ errors.push({ path: `${path}.variant`, message: "must be a string" });
5049
+ }
5050
+ if ("tweaks" in obj) {
5051
+ if (!Array.isArray(obj.tweaks)) {
5052
+ errors.push({ path: `${path}.tweaks`, message: "must be an array" });
5053
+ } else {
5054
+ for (let i = 0;i < obj.tweaks.length; i++) {
5055
+ if (typeof obj.tweaks[i] !== "string") {
5056
+ errors.push({
5057
+ path: `${path}.tweaks[${i}]`,
5058
+ message: "must be a string"
5059
+ });
5060
+ }
5061
+ }
5062
+ }
5063
+ }
5064
+ return errors;
4825
5065
  }
4826
- async function main4() {
4827
- const { positionals, values } = parseArgs4({
4828
- args: process.argv.slice(2),
4829
- options: {
4830
- info: { type: "boolean", short: "i" }
4831
- },
4832
- allowPositionals: true
4833
- });
4834
- const [themeName] = positionals;
4835
- if (!themeName) {
4836
- const themes = await listThemes();
4837
- if (themes.length === 0) {
4838
- console.log(`${colors5.yellow}No themes available.${colors5.reset}`);
4839
- console.log(`This system is compatible with omarchy themes.`);
4840
- console.log(`
4841
- Add themes to: ${colors5.cyan}~/.config/formalconf/themes/${colors5.reset}`);
4842
- process.exit(0);
5066
+ function validateThemeJson(data) {
5067
+ const errors = [];
5068
+ if (typeof data !== "object" || data === null) {
5069
+ return {
5070
+ valid: false,
5071
+ errors: [{ path: "", message: "theme must be an object" }]
5072
+ };
5073
+ }
5074
+ const obj = data;
5075
+ if (!("title" in obj) || typeof obj.title !== "string") {
5076
+ errors.push({ path: "title", message: "is required and must be a string" });
5077
+ }
5078
+ const optionalStrings = ["description", "author", "version", "source"];
5079
+ for (const field of optionalStrings) {
5080
+ if (field in obj && typeof obj[field] !== "string") {
5081
+ errors.push({ path: field, message: "must be a string" });
4843
5082
  }
4844
- console.log(`${colors5.cyan}Usage: formalconf theme <theme-name>${colors5.reset}`);
4845
- console.log(` formalconf theme --info <theme-name>
5083
+ }
5084
+ if (!("dark" in obj) && !("light" in obj)) {
5085
+ errors.push({
5086
+ path: "",
5087
+ message: "at least one of 'dark' or 'light' palette is required"
5088
+ });
5089
+ }
5090
+ if ("dark" in obj) {
5091
+ errors.push(...validatePalette(obj.dark, "dark"));
5092
+ }
5093
+ if ("light" in obj) {
5094
+ errors.push(...validatePalette(obj.light, "light"));
5095
+ }
5096
+ if ("neovim" in obj) {
5097
+ errors.push(...validateNeovimConfig(obj.neovim, "neovim"));
5098
+ }
5099
+ if ("gtk" in obj) {
5100
+ errors.push(...validateGtkConfig(obj.gtk, "gtk"));
5101
+ }
5102
+ if ("wallpapers" in obj) {
5103
+ errors.push(...validateWallpapers(obj.wallpapers, "wallpapers"));
5104
+ }
5105
+ return {
5106
+ valid: errors.length === 0,
5107
+ errors
5108
+ };
5109
+ }
5110
+ function formatValidationErrors(errors) {
5111
+ return errors.map((e) => e.path ? `${e.path}: ${e.message}` : e.message).join(`
4846
5112
  `);
4847
- console.log("Available themes:");
4848
- for (const theme of themes) {
4849
- const extras = [];
4850
- if (theme.hasBackgrounds)
4851
- extras.push("wallpapers");
4852
- if (theme.isLightMode)
4853
- extras.push("light");
4854
- const suffix = extras.length ? ` ${colors5.dim}(${extras.join(", ")})${colors5.reset}` : "";
4855
- console.log(` ${colors5.blue}•${colors5.reset} ${theme.name}${suffix}`);
5113
+ }
5114
+
5115
+ // src/lib/theme-v2/loader.ts
5116
+ class ThemeLoadError extends Error {
5117
+ path;
5118
+ constructor(path, message) {
5119
+ super(`Failed to load theme at ${path}: ${message}`);
5120
+ this.path = path;
5121
+ this.name = "ThemeLoadError";
5122
+ }
5123
+ }
5124
+
5125
+ class ThemeValidationError extends Error {
5126
+ path;
5127
+ validationErrors;
5128
+ constructor(path, validationErrors) {
5129
+ super(`Invalid theme at ${path}:
5130
+ ${validationErrors}`);
5131
+ this.path = path;
5132
+ this.validationErrors = validationErrors;
5133
+ this.name = "ThemeValidationError";
5134
+ }
5135
+ }
5136
+ async function loadThemeJson(themePath) {
5137
+ if (!existsSync8(themePath)) {
5138
+ throw new ThemeLoadError(themePath, "file not found");
5139
+ }
5140
+ let content;
5141
+ try {
5142
+ content = await readText(themePath);
5143
+ } catch (err) {
5144
+ throw new ThemeLoadError(themePath, `could not read file: ${err instanceof Error ? err.message : String(err)}`);
5145
+ }
5146
+ let parsed;
5147
+ try {
5148
+ parsed = JSON.parse(content);
5149
+ } catch (err) {
5150
+ throw new ThemeLoadError(themePath, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
5151
+ }
5152
+ const validation = validateThemeJson(parsed);
5153
+ if (!validation.valid) {
5154
+ throw new ThemeValidationError(themePath, formatValidationErrors(validation.errors));
5155
+ }
5156
+ return parsed;
5157
+ }
5158
+ function getAvailableModes(theme) {
5159
+ const modes = [];
5160
+ if (theme.dark)
5161
+ modes.push("dark");
5162
+ if (theme.light)
5163
+ modes.push("light");
5164
+ return modes;
5165
+ }
5166
+ async function listJsonThemes() {
5167
+ if (!existsSync8(THEMES_DIR)) {
5168
+ return [];
5169
+ }
5170
+ const entries = readdirSync5(THEMES_DIR, { withFileTypes: true });
5171
+ const themes = [];
5172
+ for (const entry of entries) {
5173
+ if (entry.isFile() && entry.name.endsWith(".json")) {
5174
+ const path = join6(THEMES_DIR, entry.name);
5175
+ try {
5176
+ const theme = await loadThemeJson(path);
5177
+ themes.push({
5178
+ name: basename2(entry.name, ".json"),
5179
+ path,
5180
+ theme,
5181
+ availableModes: getAvailableModes(theme)
5182
+ });
5183
+ } catch {
5184
+ continue;
5185
+ }
4856
5186
  }
4857
- process.exit(0);
4858
5187
  }
4859
- if (values.info) {
4860
- await showThemeInfo(themeName);
4861
- } else {
4862
- const result = await applyTheme(themeName);
4863
- console.log(result.success ? `${colors5.green}${result.output}${colors5.reset}` : `${colors5.red}${result.output}${colors5.reset}`);
5188
+ return themes;
5189
+ }
5190
+
5191
+ // src/lib/template-engine/engine.ts
5192
+ init_runtime();
5193
+ import { join as join8 } from "path";
5194
+
5195
+ // src/lib/template-engine/modifiers.ts
5196
+ var VALID_MODIFIERS = [
5197
+ "hex",
5198
+ "strip",
5199
+ "rgb",
5200
+ "rgba",
5201
+ "r",
5202
+ "g",
5203
+ "b",
5204
+ "red",
5205
+ "green",
5206
+ "blue"
5207
+ ];
5208
+ function isValidModifier(modifier) {
5209
+ return VALID_MODIFIERS.includes(modifier);
5210
+ }
5211
+ function applyModifier(color, modifier) {
5212
+ if (!modifier) {
5213
+ return color.hex;
5214
+ }
5215
+ switch (modifier) {
5216
+ case "hex":
5217
+ return color.hex;
5218
+ case "strip":
5219
+ return color.strip;
5220
+ case "rgb":
5221
+ return color.rgb;
5222
+ case "rgba":
5223
+ return color.rgba;
5224
+ case "r":
5225
+ return String(color.r);
5226
+ case "g":
5227
+ return String(color.g);
5228
+ case "b":
5229
+ return String(color.b);
5230
+ case "red":
5231
+ return color.red.toFixed(6);
5232
+ case "green":
5233
+ return color.green.toFixed(6);
5234
+ case "blue":
5235
+ return color.blue.toFixed(6);
5236
+ default:
5237
+ return color.hex;
4864
5238
  }
4865
5239
  }
4866
- var isMainModule4 = process.argv[1]?.includes("set-theme");
4867
- if (isMainModule4) {
4868
- main4().catch(console.error);
5240
+ function parseVariableReference(variable) {
5241
+ const parts = variable.split(".");
5242
+ if (parts[0] === "theme" && parts.length === 2) {
5243
+ return { name: variable };
5244
+ }
5245
+ if (parts.length === 2 && isValidModifier(parts[1])) {
5246
+ return { name: parts[0], modifier: parts[1] };
5247
+ }
5248
+ return { name: variable };
4869
5249
  }
4870
5250
 
4871
- // src/components/menus/ThemeMenu.tsx
4872
- import { jsxDEV as jsxDEV19 } from "react/jsx-dev-runtime";
4873
- function ThemeMenu({ onBack }) {
4874
- const [themes, setThemes] = useState10([]);
4875
- const [loading, setLoading] = useState10(true);
4876
- const { state, output, success, isRunning, isResult, execute, reset } = useMenuAction();
4877
- const grid = useThemeGrid({
4878
- itemCount: themes.length,
4879
- onSelect: (index) => applyTheme2(themes[index]),
4880
- onBack,
4881
- enabled: state === "menu" && !loading && themes.length > 0
4882
- });
4883
- useEffect6(() => {
4884
- async function loadThemes() {
4885
- if (!existsSync7(THEMES_DIR)) {
4886
- setThemes([]);
4887
- setLoading(false);
4888
- return;
4889
- }
4890
- const entries = readdirSync5(THEMES_DIR, { withFileTypes: true });
4891
- const loadedThemes = [];
4892
- for (const entry of entries) {
4893
- if (entry.isDirectory()) {
4894
- const themePath = join6(THEMES_DIR, entry.name);
4895
- const theme = await parseTheme(themePath, entry.name);
4896
- loadedThemes.push(theme);
4897
- }
4898
- }
4899
- setThemes(loadedThemes);
4900
- setLoading(false);
5251
+ // src/lib/template-engine/parser.ts
5252
+ var VARIABLE_REGEX = /\{\{([a-zA-Z0-9_.]+)\}\}/g;
5253
+ var COLOR_VARIABLES = [
5254
+ "color0",
5255
+ "color1",
5256
+ "color2",
5257
+ "color3",
5258
+ "color4",
5259
+ "color5",
5260
+ "color6",
5261
+ "color7",
5262
+ "color8",
5263
+ "color9",
5264
+ "color10",
5265
+ "color11",
5266
+ "color12",
5267
+ "color13",
5268
+ "color14",
5269
+ "color15",
5270
+ "background",
5271
+ "foreground",
5272
+ "cursor",
5273
+ "selection_background",
5274
+ "selection_foreground",
5275
+ "accent",
5276
+ "border"
5277
+ ];
5278
+ function isColorVariable(name) {
5279
+ return COLOR_VARIABLES.includes(name);
5280
+ }
5281
+ function getContextValue(context, variableName, modifier) {
5282
+ if (variableName.startsWith("theme.")) {
5283
+ const key = variableName.slice(6);
5284
+ const value = context.theme[key];
5285
+ return value !== undefined ? String(value) : undefined;
5286
+ }
5287
+ if (variableName === "mode") {
5288
+ return context.mode;
5289
+ }
5290
+ if (isColorVariable(variableName)) {
5291
+ const color = context[variableName];
5292
+ return applyModifier(color, modifier);
5293
+ }
5294
+ return;
5295
+ }
5296
+ function renderTemplate(template, context) {
5297
+ return template.replace(VARIABLE_REGEX, (match, variable) => {
5298
+ const { name, modifier } = parseVariableReference(variable);
5299
+ const value = getContextValue(context, name, modifier);
5300
+ if (value === undefined) {
5301
+ return match;
4901
5302
  }
4902
- loadThemes();
4903
- }, []);
4904
- const applyTheme2 = async (theme) => {
4905
- const themeName = theme.path.split("/").pop();
4906
- await execute(() => runSetTheme(themeName));
4907
- };
4908
- const visibleThemes = useMemo4(() => {
4909
- return themes.slice(grid.visibleStartIndex, grid.visibleEndIndex);
4910
- }, [themes, grid.visibleStartIndex, grid.visibleEndIndex]);
4911
- if (loading || isRunning) {
4912
- return /* @__PURE__ */ jsxDEV19(LoadingPanel, {
4913
- title: "Select Theme",
4914
- label: loading ? "Loading themes..." : "Applying theme..."
4915
- }, undefined, false, undefined, this);
5303
+ return value;
5304
+ });
5305
+ }
5306
+ function renderDualModeTemplate(template, contexts) {
5307
+ let result = template.replace(/\{\{dark\.([a-zA-Z0-9_.]+)\}\}/g, (match, variable) => {
5308
+ const { name, modifier } = parseVariableReference(variable);
5309
+ const value = getContextValue(contexts.dark, name, modifier);
5310
+ return value ?? match;
5311
+ });
5312
+ result = result.replace(/\{\{light\.([a-zA-Z0-9_.]+)\}\}/g, (match, variable) => {
5313
+ const { name, modifier } = parseVariableReference(variable);
5314
+ const value = getContextValue(contexts.light, name, modifier);
5315
+ return value ?? match;
5316
+ });
5317
+ result = result.replace(/\{\{theme\.([a-zA-Z0-9_]+)\}\}/g, (match, key) => {
5318
+ const value = contexts.theme[key];
5319
+ return value !== undefined ? String(value) : match;
5320
+ });
5321
+ return result;
5322
+ }
5323
+
5324
+ // src/lib/template-engine/versioning.ts
5325
+ init_runtime();
5326
+ import { existsSync as existsSync9, readdirSync as readdirSync6 } from "fs";
5327
+ import { join as join7 } from "path";
5328
+ var DEFAULT_MANIFEST = {
5329
+ version: 1,
5330
+ templates: {}
5331
+ };
5332
+ async function loadTemplatesManifest() {
5333
+ if (!existsSync9(TEMPLATES_MANIFEST_PATH)) {
5334
+ return { ...DEFAULT_MANIFEST };
4916
5335
  }
4917
- if (isResult) {
4918
- return /* @__PURE__ */ jsxDEV19(CommandOutput, {
4919
- title: "Select Theme",
4920
- output,
4921
- success,
4922
- onDismiss: reset
4923
- }, undefined, false, undefined, this);
5336
+ try {
5337
+ const content = await readText(TEMPLATES_MANIFEST_PATH);
5338
+ return JSON.parse(content);
5339
+ } catch {
5340
+ return { ...DEFAULT_MANIFEST };
4924
5341
  }
4925
- if (themes.length === 0) {
4926
- return /* @__PURE__ */ jsxDEV19(Panel, {
5342
+ }
5343
+ async function saveTemplatesManifest(manifest) {
5344
+ await ensureDir2(TEMPLATES_DIR);
5345
+ await writeFile(TEMPLATES_MANIFEST_PATH, JSON.stringify(manifest, null, 2));
5346
+ }
5347
+ async function loadBundledManifest() {
5348
+ if (!existsSync9(BUNDLED_MANIFEST_PATH)) {
5349
+ return { version: 1, templates: {} };
5350
+ }
5351
+ try {
5352
+ const content = await readText(BUNDLED_MANIFEST_PATH);
5353
+ return JSON.parse(content);
5354
+ } catch {
5355
+ return { version: 1, templates: {} };
5356
+ }
5357
+ }
5358
+ function compareVersions(a, b) {
5359
+ const [aMajor = 0, aMinor = 0, aPatch = 0] = a.split(".").map(Number);
5360
+ const [bMajor = 0, bMinor = 0, bPatch = 0] = b.split(".").map(Number);
5361
+ if (aMajor !== bMajor)
5362
+ return aMajor - bMajor;
5363
+ if (aMinor !== bMinor)
5364
+ return aMinor - bMinor;
5365
+ return aPatch - bPatch;
5366
+ }
5367
+ async function checkTemplateUpdates() {
5368
+ const installed = await loadTemplatesManifest();
5369
+ const bundled = await loadBundledManifest();
5370
+ const updates = [];
5371
+ for (const [name, bundledMeta] of Object.entries(bundled.templates)) {
5372
+ const installedMeta = installed.templates[name];
5373
+ if (!installedMeta) {
5374
+ updates.push({
5375
+ name,
5376
+ installedVersion: "0.0.0",
5377
+ bundledVersion: bundledMeta.version,
5378
+ customOverride: false,
5379
+ updateAvailable: true
5380
+ });
5381
+ } else if (compareVersions(bundledMeta.version, installedMeta.version) > 0) {
5382
+ updates.push({
5383
+ name,
5384
+ installedVersion: installedMeta.version,
5385
+ bundledVersion: bundledMeta.version,
5386
+ customOverride: installedMeta.customOverride,
5387
+ updateAvailable: !installedMeta.customOverride
5388
+ });
5389
+ }
5390
+ }
5391
+ return updates;
5392
+ }
5393
+ async function installTemplate(templateName) {
5394
+ const bundled = await loadBundledManifest();
5395
+ const bundledMeta = bundled.templates[templateName];
5396
+ if (!bundledMeta) {
5397
+ throw new Error(`Template '${templateName}' not found in bundled templates`);
5398
+ }
5399
+ const sourcePath = join7(BUNDLED_TEMPLATES_DIR, templateName);
5400
+ if (!existsSync9(sourcePath)) {
5401
+ throw new Error(`Template file not found: ${sourcePath}`);
5402
+ }
5403
+ await ensureDir2(TEMPLATES_DIR);
5404
+ const content = await readText(sourcePath);
5405
+ const destPath = join7(TEMPLATES_DIR, templateName);
5406
+ await writeFile(destPath, content);
5407
+ const manifest = await loadTemplatesManifest();
5408
+ manifest.templates[templateName] = {
5409
+ version: bundledMeta.version,
5410
+ installedAt: new Date().toISOString(),
5411
+ customOverride: false
5412
+ };
5413
+ await saveTemplatesManifest(manifest);
5414
+ }
5415
+ async function installAllTemplates() {
5416
+ const bundled = await loadBundledManifest();
5417
+ for (const name of Object.keys(bundled.templates)) {
5418
+ await installTemplate(name);
5419
+ }
5420
+ }
5421
+ function getTemplateType(filename) {
5422
+ const dualModeTemplates = ["ghostty.conf.template", "neovim.lua.template", "lynk.css.template"];
5423
+ if (dualModeTemplates.includes(filename)) {
5424
+ return "dual";
5425
+ }
5426
+ if (filename.includes("-dark.") || filename.includes("-light.")) {
5427
+ return "partial";
5428
+ }
5429
+ return "single";
5430
+ }
5431
+ function getPartialMode(filename) {
5432
+ if (filename.includes("-dark."))
5433
+ return "dark";
5434
+ if (filename.includes("-light."))
5435
+ return "light";
5436
+ return;
5437
+ }
5438
+ function getOutputFilename(templateName) {
5439
+ let output = templateName.replace(/\.template$/, "");
5440
+ if (templateName.startsWith("kitty-dark")) {
5441
+ return "dark-theme.auto.conf";
5442
+ }
5443
+ if (templateName.startsWith("kitty-light")) {
5444
+ return "light-theme.auto.conf";
5445
+ }
5446
+ if (templateName.startsWith("waybar-dark")) {
5447
+ return "style-dark.css";
5448
+ }
5449
+ if (templateName.startsWith("waybar-light")) {
5450
+ return "style-light.css";
5451
+ }
5452
+ if (templateName === "ghostty-dark.theme.template") {
5453
+ return "formalconf-dark";
5454
+ }
5455
+ if (templateName === "ghostty-light.theme.template") {
5456
+ return "formalconf-light";
5457
+ }
5458
+ if (templateName.startsWith("btop-dark")) {
5459
+ return "formalconf-dark.theme";
5460
+ }
5461
+ if (templateName.startsWith("btop-light")) {
5462
+ return "formalconf-light.theme";
5463
+ }
5464
+ return output;
5465
+ }
5466
+ async function listInstalledTemplates() {
5467
+ if (!existsSync9(TEMPLATES_DIR)) {
5468
+ return [];
5469
+ }
5470
+ const entries = readdirSync6(TEMPLATES_DIR, { withFileTypes: true });
5471
+ const templates = [];
5472
+ for (const entry of entries) {
5473
+ if (entry.isFile() && entry.name.endsWith(".template")) {
5474
+ templates.push({
5475
+ name: entry.name,
5476
+ path: join7(TEMPLATES_DIR, entry.name),
5477
+ outputName: getOutputFilename(entry.name),
5478
+ type: getTemplateType(entry.name),
5479
+ partialMode: getPartialMode(entry.name)
5480
+ });
5481
+ }
5482
+ }
5483
+ return templates;
5484
+ }
5485
+
5486
+ // src/lib/neovim/generator.ts
5487
+ function toLua(value, indent = 2) {
5488
+ const spaces = " ".repeat(indent);
5489
+ if (value === null || value === undefined) {
5490
+ return "nil";
5491
+ }
5492
+ if (typeof value === "boolean") {
5493
+ return value ? "true" : "false";
5494
+ }
5495
+ if (typeof value === "number") {
5496
+ return String(value);
5497
+ }
5498
+ if (typeof value === "string") {
5499
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
5500
+ return `"${escaped}"`;
5501
+ }
5502
+ if (Array.isArray(value)) {
5503
+ if (value.length === 0) {
5504
+ return "{}";
5505
+ }
5506
+ const items = value.map((v) => `${spaces}${toLua(v, indent + 2)}`);
5507
+ return `{
5508
+ ${items.join(`,
5509
+ `)}
5510
+ ${" ".repeat(indent - 2)}}`;
5511
+ }
5512
+ if (typeof value === "object") {
5513
+ const entries = Object.entries(value);
5514
+ if (entries.length === 0) {
5515
+ return "{}";
5516
+ }
5517
+ const items = entries.map(([k, v]) => {
5518
+ const key = /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(k) ? k : `["${k}"]`;
5519
+ return `${spaces}${key} = ${toLua(v, indent + 2)}`;
5520
+ });
5521
+ return `{
5522
+ ${items.join(`,
5523
+ `)}
5524
+ ${" ".repeat(indent - 2)}}`;
5525
+ }
5526
+ return "nil";
5527
+ }
5528
+ function generateOptsSection(opts) {
5529
+ if (!opts || Object.keys(opts).length === 0) {
5530
+ return "";
5531
+ }
5532
+ return ` opts = ${toLua(opts, 6)},`;
5533
+ }
5534
+ function generateNeovimConfig(theme, mode) {
5535
+ if (!theme.neovim) {
5536
+ return "";
5537
+ }
5538
+ const { repo, colorscheme, light_colorscheme, opts } = theme.neovim;
5539
+ const effectiveColorscheme = mode === "light" && light_colorscheme ? light_colorscheme : colorscheme;
5540
+ const optsSection = generateOptsSection(opts);
5541
+ const hasDualMode = light_colorscheme && light_colorscheme !== colorscheme;
5542
+ const lines = [
5543
+ `-- ${theme.title} (${mode}) - Generated by FormalConf`,
5544
+ `-- Neovim colorscheme configuration for LazyVim`,
5545
+ ``,
5546
+ `return {`,
5547
+ ` {`,
5548
+ ` "${repo}",`,
5549
+ ` name = "${theme.title.toLowerCase().replace(/\s+/g, "-")}",`,
5550
+ ` priority = 1000,`
5551
+ ];
5552
+ if (optsSection) {
5553
+ lines.push(optsSection);
5554
+ }
5555
+ lines.push(` },`, ` {`, ` "LazyVim/LazyVim",`, ` opts = {`, ` colorscheme = "${effectiveColorscheme}",`, ` },`, ` },`);
5556
+ if (hasDualMode) {
5557
+ lines.push(` {`, ` "f-person/auto-dark-mode.nvim",`, ` opts = {`, ` update_background = false,`, ` set_dark_mode = function()`, ` vim.cmd("colorscheme ${colorscheme}")`, ` end,`, ` set_light_mode = function()`, ` vim.cmd("colorscheme ${light_colorscheme}")`, ` end,`, ` },`, ` },`);
5558
+ }
5559
+ lines.push(`}`);
5560
+ return lines.join(`
5561
+ `) + `
5562
+ `;
5563
+ }
5564
+ function hasNeovimConfig(theme) {
5565
+ return !!theme.neovim?.repo && !!theme.neovim?.colorscheme;
5566
+ }
5567
+
5568
+ // src/lib/template-engine/engine.ts
5569
+ function buildThemeMetadata(theme, mode) {
5570
+ return {
5571
+ title: theme.title,
5572
+ author: theme.author ?? "",
5573
+ version: theme.version ?? "",
5574
+ description: theme.description ?? "",
5575
+ source: theme.source ?? "",
5576
+ mode
5577
+ };
5578
+ }
5579
+ function buildTemplateContext(theme, palette, mode) {
5580
+ return {
5581
+ color0: hexToColorVariable(palette.color0),
5582
+ color1: hexToColorVariable(palette.color1),
5583
+ color2: hexToColorVariable(palette.color2),
5584
+ color3: hexToColorVariable(palette.color3),
5585
+ color4: hexToColorVariable(palette.color4),
5586
+ color5: hexToColorVariable(palette.color5),
5587
+ color6: hexToColorVariable(palette.color6),
5588
+ color7: hexToColorVariable(palette.color7),
5589
+ color8: hexToColorVariable(palette.color8),
5590
+ color9: hexToColorVariable(palette.color9),
5591
+ color10: hexToColorVariable(palette.color10),
5592
+ color11: hexToColorVariable(palette.color11),
5593
+ color12: hexToColorVariable(palette.color12),
5594
+ color13: hexToColorVariable(palette.color13),
5595
+ color14: hexToColorVariable(palette.color14),
5596
+ color15: hexToColorVariable(palette.color15),
5597
+ background: hexToColorVariable(palette.background),
5598
+ foreground: hexToColorVariable(palette.foreground),
5599
+ cursor: hexToColorVariable(palette.cursor),
5600
+ selection_background: hexToColorVariableOrDefault(palette.selection_background, palette.color0),
5601
+ selection_foreground: hexToColorVariableOrDefault(palette.selection_foreground, palette.foreground),
5602
+ accent: hexToColorVariableOrDefault(palette.accent, palette.color4),
5603
+ border: hexToColorVariableOrDefault(palette.border, palette.color0),
5604
+ theme: buildThemeMetadata(theme, mode),
5605
+ mode
5606
+ };
5607
+ }
5608
+ function buildDualModeContext(theme) {
5609
+ if (!theme.dark || !theme.light) {
5610
+ return null;
5611
+ }
5612
+ return {
5613
+ dark: buildTemplateContext(theme, theme.dark, "dark"),
5614
+ light: buildTemplateContext(theme, theme.light, "light"),
5615
+ theme: buildThemeMetadata(theme, "dark")
5616
+ };
5617
+ }
5618
+ async function renderTemplateFile(templateFile, theme, mode) {
5619
+ const templateContent = await readText(templateFile.path);
5620
+ let content;
5621
+ if (templateFile.type === "dual") {
5622
+ const dualContext = buildDualModeContext(theme);
5623
+ if (!dualContext) {
5624
+ const palette = theme[mode] ?? theme.dark ?? theme.light;
5625
+ if (!palette) {
5626
+ throw new Error(`Theme '${theme.title}' does not have any palette`);
5627
+ }
5628
+ const context = buildTemplateContext(theme, palette, mode);
5629
+ const fallbackDualContext = {
5630
+ dark: context,
5631
+ light: context,
5632
+ theme: buildThemeMetadata(theme, mode)
5633
+ };
5634
+ content = renderDualModeTemplate(templateContent, fallbackDualContext);
5635
+ } else {
5636
+ content = renderDualModeTemplate(templateContent, dualContext);
5637
+ }
5638
+ } else {
5639
+ const effectiveMode = templateFile.partialMode ?? mode;
5640
+ const palette = theme[effectiveMode];
5641
+ if (!palette) {
5642
+ throw new Error(`Theme '${theme.title}' does not have a ${effectiveMode} palette`);
5643
+ }
5644
+ const context = buildTemplateContext(theme, palette, effectiveMode);
5645
+ content = renderTemplate(templateContent, context);
5646
+ }
5647
+ return {
5648
+ template: templateFile,
5649
+ content,
5650
+ outputPath: join8(GENERATED_DIR, templateFile.outputName)
5651
+ };
5652
+ }
5653
+ async function renderAllTemplates(theme, mode) {
5654
+ const templates = await listInstalledTemplates();
5655
+ const results = [];
5656
+ for (const template of templates) {
5657
+ if (template.type === "partial" && template.partialMode !== mode) {
5658
+ if (theme[template.partialMode]) {
5659
+ const result = await renderTemplateFile(template, theme, mode);
5660
+ results.push(result);
5661
+ }
5662
+ continue;
5663
+ }
5664
+ try {
5665
+ const result = await renderTemplateFile(template, theme, mode);
5666
+ results.push(result);
5667
+ } catch (err) {
5668
+ console.error(`Warning: Could not render ${template.name}: ${err}`);
5669
+ }
5670
+ }
5671
+ return results;
5672
+ }
5673
+ async function writeRenderedTemplates(results) {
5674
+ await ensureDir2(GENERATED_DIR);
5675
+ for (const result of results) {
5676
+ await writeFile(result.outputPath, result.content);
5677
+ }
5678
+ }
5679
+ async function generateNeovimConfigFile(theme, mode) {
5680
+ if (!hasNeovimConfig(theme)) {
5681
+ return null;
5682
+ }
5683
+ const content = generateNeovimConfig(theme, mode);
5684
+ const outputPath = join8(GENERATED_DIR, "neovim.lua");
5685
+ await writeFile(outputPath, content);
5686
+ return {
5687
+ template: {
5688
+ name: "neovim.lua",
5689
+ path: "",
5690
+ outputName: "neovim.lua",
5691
+ type: "single"
5692
+ },
5693
+ content,
5694
+ outputPath
5695
+ };
5696
+ }
5697
+ async function generateThemeConfigs(theme, mode) {
5698
+ const results = await renderAllTemplates(theme, mode);
5699
+ await writeRenderedTemplates(results);
5700
+ const neovimResult = await generateNeovimConfigFile(theme, mode);
5701
+ if (neovimResult) {
5702
+ results.push(neovimResult);
5703
+ }
5704
+ return results;
5705
+ }
5706
+
5707
+ // src/lib/migration/extractor.ts
5708
+ init_runtime();
5709
+ import { existsSync as existsSync10, readdirSync as readdirSync7 } from "fs";
5710
+ import { join as join9 } from "path";
5711
+ function normalizeHex2(hex) {
5712
+ hex = hex.replace(/^(#|0x)/i, "");
5713
+ if (hex.length === 3) {
5714
+ hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
5715
+ }
5716
+ return `#${hex.toLowerCase()}`;
5717
+ }
5718
+ async function extractFromKitty(path) {
5719
+ const content = await readText(path);
5720
+ const colors5 = {};
5721
+ const colorMappings = {
5722
+ foreground: "foreground",
5723
+ background: "background",
5724
+ cursor: "cursor",
5725
+ selection_foreground: "selection_foreground",
5726
+ selection_background: "selection_background",
5727
+ color0: "color0",
5728
+ color1: "color1",
5729
+ color2: "color2",
5730
+ color3: "color3",
5731
+ color4: "color4",
5732
+ color5: "color5",
5733
+ color6: "color6",
5734
+ color7: "color7",
5735
+ color8: "color8",
5736
+ color9: "color9",
5737
+ color10: "color10",
5738
+ color11: "color11",
5739
+ color12: "color12",
5740
+ color13: "color13",
5741
+ color14: "color14",
5742
+ color15: "color15"
5743
+ };
5744
+ for (const line of content.split(`
5745
+ `)) {
5746
+ const trimmed = line.trim();
5747
+ if (!trimmed || trimmed.startsWith("#"))
5748
+ continue;
5749
+ const match = trimmed.match(/^(\w+)\s+(#?[0-9a-fA-F]{3,6})/);
5750
+ if (match) {
5751
+ const [, key, value] = match;
5752
+ if (key in colorMappings) {
5753
+ colors5[colorMappings[key]] = normalizeHex2(value);
5754
+ }
5755
+ }
5756
+ }
5757
+ return { colors: colors5, source: "kitty" };
5758
+ }
5759
+ async function extractFromAlacritty(path) {
5760
+ const content = await readText(path);
5761
+ const colors5 = {};
5762
+ const colorRegex = /(\w+)\s*=\s*["']?(#?[0-9a-fA-F]{3,6})["']?/g;
5763
+ const sectionMappings = {
5764
+ "colors.primary": {
5765
+ background: "background",
5766
+ foreground: "foreground"
5767
+ },
5768
+ "colors.cursor": {
5769
+ cursor: "cursor"
5770
+ },
5771
+ "colors.selection": {
5772
+ background: "selection_background",
5773
+ foreground: "selection_foreground"
5774
+ },
5775
+ "colors.normal": {
5776
+ black: "color0",
5777
+ red: "color1",
5778
+ green: "color2",
5779
+ yellow: "color3",
5780
+ blue: "color4",
5781
+ magenta: "color5",
5782
+ cyan: "color6",
5783
+ white: "color7"
5784
+ },
5785
+ "colors.bright": {
5786
+ black: "color8",
5787
+ red: "color9",
5788
+ green: "color10",
5789
+ yellow: "color11",
5790
+ blue: "color12",
5791
+ magenta: "color13",
5792
+ cyan: "color14",
5793
+ white: "color15"
5794
+ }
5795
+ };
5796
+ let currentSection = "";
5797
+ for (const line of content.split(`
5798
+ `)) {
5799
+ const trimmed = line.trim();
5800
+ const sectionMatch = trimmed.match(/^\[([^\]]+)\]/);
5801
+ if (sectionMatch) {
5802
+ currentSection = sectionMatch[1];
5803
+ continue;
5804
+ }
5805
+ const colorMatch = trimmed.match(/^(\w+)\s*=\s*["']?(#?[0-9a-fA-F]{3,6})["']?/);
5806
+ if (colorMatch && currentSection in sectionMappings) {
5807
+ const [, key, value] = colorMatch;
5808
+ const sectionMap = sectionMappings[currentSection];
5809
+ if (key in sectionMap) {
5810
+ colors5[sectionMap[key]] = normalizeHex2(value);
5811
+ }
5812
+ }
5813
+ }
5814
+ return { colors: colors5, source: "alacritty" };
5815
+ }
5816
+ async function extractFromGhostty(path) {
5817
+ const content = await readText(path);
5818
+ const colors5 = {};
5819
+ const mappings = {
5820
+ foreground: "foreground",
5821
+ background: "background",
5822
+ "cursor-color": "cursor",
5823
+ "selection-foreground": "selection_foreground",
5824
+ "selection-background": "selection_background"
5825
+ };
5826
+ for (const line of content.split(`
5827
+ `)) {
5828
+ const trimmed = line.trim();
5829
+ if (!trimmed || trimmed.startsWith("#"))
5830
+ continue;
5831
+ const paletteMatch = trimmed.match(/^palette\s*=\s*(\d+)=([0-9a-fA-F]{6})/);
5832
+ if (paletteMatch) {
5833
+ const [, index, value] = paletteMatch;
5834
+ const colorKey = `color${index}`;
5835
+ if (parseInt(index) <= 15) {
5836
+ colors5[colorKey] = normalizeHex2(value);
5837
+ }
5838
+ continue;
5839
+ }
5840
+ const colorMatch = trimmed.match(/^([\w-]+)\s*=\s*([0-9a-fA-F]{6})/);
5841
+ if (colorMatch) {
5842
+ const [, key, value] = colorMatch;
5843
+ if (key in mappings) {
5844
+ colors5[mappings[key]] = normalizeHex2(value);
5845
+ }
5846
+ }
5847
+ }
5848
+ return { colors: colors5, source: "ghostty" };
5849
+ }
5850
+ async function extractColors(filePath) {
5851
+ if (!existsSync10(filePath)) {
5852
+ return null;
5853
+ }
5854
+ const filename = filePath.toLowerCase();
5855
+ if (filename.includes("kitty") || filename.endsWith(".conf")) {
5856
+ const content = await readText(filePath);
5857
+ if (content.includes("foreground") && content.includes("color0")) {
5858
+ return extractFromKitty(filePath);
5859
+ }
5860
+ }
5861
+ if (filename.includes("alacritty") || filename.endsWith(".toml")) {
5862
+ return extractFromAlacritty(filePath);
5863
+ }
5864
+ if (filename.includes("ghostty")) {
5865
+ return extractFromGhostty(filePath);
5866
+ }
5867
+ if (filename.endsWith(".conf")) {
5868
+ return extractFromKitty(filePath);
5869
+ }
5870
+ return null;
5871
+ }
5872
+ async function extractFromLegacyTheme(themePath) {
5873
+ if (!existsSync10(themePath)) {
5874
+ return null;
5875
+ }
5876
+ const files = readdirSync7(themePath, { withFileTypes: true });
5877
+ const preferredFiles = [
5878
+ "kitty.conf",
5879
+ "alacritty.toml",
5880
+ "ghostty.conf"
5881
+ ];
5882
+ for (const preferred of preferredFiles) {
5883
+ const match = files.find((f) => f.name.toLowerCase() === preferred.toLowerCase());
5884
+ if (match) {
5885
+ return extractColors(join9(themePath, match.name));
5886
+ }
5887
+ }
5888
+ for (const file of files) {
5889
+ if (file.isFile() && (file.name.endsWith(".conf") || file.name.endsWith(".toml"))) {
5890
+ const result = await extractColors(join9(themePath, file.name));
5891
+ if (result && Object.keys(result.colors).length > 0) {
5892
+ return result;
5893
+ }
5894
+ }
5895
+ }
5896
+ return null;
5897
+ }
5898
+ function validatePalette2(colors5) {
5899
+ const required = [
5900
+ "color0",
5901
+ "color1",
5902
+ "color2",
5903
+ "color3",
5904
+ "color4",
5905
+ "color5",
5906
+ "color6",
5907
+ "color7",
5908
+ "color8",
5909
+ "color9",
5910
+ "color10",
5911
+ "color11",
5912
+ "color12",
5913
+ "color13",
5914
+ "color14",
5915
+ "color15",
5916
+ "background",
5917
+ "foreground",
5918
+ "cursor"
5919
+ ];
5920
+ const missing = [];
5921
+ for (const key of required) {
5922
+ if (!colors5[key]) {
5923
+ missing.push(key);
5924
+ }
5925
+ }
5926
+ return missing;
5927
+ }
5928
+ function fillMissingColors(colors5) {
5929
+ const defaults = {
5930
+ color0: "#000000",
5931
+ color1: "#cc0000",
5932
+ color2: "#4e9a06",
5933
+ color3: "#c4a000",
5934
+ color4: "#3465a4",
5935
+ color5: "#75507b",
5936
+ color6: "#06989a",
5937
+ color7: "#d3d7cf",
5938
+ color8: "#555753",
5939
+ color9: "#ef2929",
5940
+ color10: "#8ae234",
5941
+ color11: "#fce94f",
5942
+ color12: "#739fcf",
5943
+ color13: "#ad7fa8",
5944
+ color14: "#34e2e2",
5945
+ color15: "#eeeeec",
5946
+ background: "#1e1e1e",
5947
+ foreground: "#d4d4d4",
5948
+ cursor: "#ffffff"
5949
+ };
5950
+ return {
5951
+ ...defaults,
5952
+ ...colors5,
5953
+ selection_background: colors5.selection_background ?? colors5.color0 ?? defaults.color0,
5954
+ selection_foreground: colors5.selection_foreground ?? colors5.foreground ?? defaults.foreground,
5955
+ accent: colors5.accent ?? colors5.color4 ?? defaults.color4,
5956
+ border: colors5.border ?? colors5.color0 ?? defaults.color0
5957
+ };
5958
+ }
5959
+ function generateThemeJson(name, colors5, options = {}) {
5960
+ const palette = fillMissingColors(colors5);
5961
+ const theme = {
5962
+ title: name,
5963
+ description: options.description,
5964
+ author: options.author,
5965
+ version: "1.0.0"
5966
+ };
5967
+ if (options.isLight) {
5968
+ theme.light = palette;
5969
+ } else {
5970
+ theme.dark = palette;
5971
+ }
5972
+ return theme;
5973
+ }
5974
+
5975
+ // src/cli/set-theme.ts
5976
+ init_runtime();
5977
+
5978
+ // src/lib/wallpaper.ts
5979
+ import { existsSync as existsSync11, readdirSync as readdirSync8, unlinkSync } from "fs";
5980
+ import { join as join10 } from "path";
5981
+ var DEFAULT_TIMEOUT_MS = 30000;
5982
+ var MAX_FILE_SIZE = 50 * 1024 * 1024;
5983
+ function clearBackgroundsDir() {
5984
+ if (!existsSync11(BACKGROUNDS_TARGET_DIR)) {
5985
+ return;
5986
+ }
5987
+ const entries = readdirSync8(BACKGROUNDS_TARGET_DIR, { withFileTypes: true });
5988
+ for (const entry of entries) {
5989
+ if (entry.isFile() || entry.isSymbolicLink()) {
5990
+ unlinkSync(join10(BACKGROUNDS_TARGET_DIR, entry.name));
5991
+ }
5992
+ }
5993
+ }
5994
+ function getExtension(url, contentType) {
5995
+ const urlPath = new URL(url).pathname;
5996
+ const urlExt = urlPath.split(".").pop()?.toLowerCase();
5997
+ if (urlExt && ["png", "jpg", "jpeg", "webp", "gif", "bmp"].includes(urlExt)) {
5998
+ return urlExt;
5999
+ }
6000
+ const typeMap = {
6001
+ "image/png": "png",
6002
+ "image/jpeg": "jpg",
6003
+ "image/jpg": "jpg",
6004
+ "image/webp": "webp",
6005
+ "image/gif": "gif",
6006
+ "image/bmp": "bmp"
6007
+ };
6008
+ return typeMap[contentType] || "png";
6009
+ }
6010
+ async function downloadWallpaper(url, filename, timeoutMs = DEFAULT_TIMEOUT_MS) {
6011
+ let parsedUrl;
6012
+ try {
6013
+ parsedUrl = new URL(url);
6014
+ if (!["http:", "https:"].includes(parsedUrl.protocol)) {
6015
+ return { success: false, error: "URL must use http or https protocol" };
6016
+ }
6017
+ } catch {
6018
+ return { success: false, error: "Invalid URL" };
6019
+ }
6020
+ await ensureDir2(BACKGROUNDS_TARGET_DIR);
6021
+ const controller = new AbortController;
6022
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
6023
+ try {
6024
+ const response = await fetch(url, { signal: controller.signal });
6025
+ clearTimeout(timeoutId);
6026
+ if (!response.ok) {
6027
+ return {
6028
+ success: false,
6029
+ error: `HTTP error: ${response.status} ${response.statusText}`
6030
+ };
6031
+ }
6032
+ const contentType = response.headers.get("content-type") || "";
6033
+ if (!contentType.startsWith("image/")) {
6034
+ return {
6035
+ success: false,
6036
+ error: `Invalid content type: ${contentType} (expected image/*)`
6037
+ };
6038
+ }
6039
+ const contentLength = response.headers.get("content-length");
6040
+ if (contentLength && parseInt(contentLength, 10) > MAX_FILE_SIZE) {
6041
+ return {
6042
+ success: false,
6043
+ error: `File too large: ${Math.round(parseInt(contentLength, 10) / 1024 / 1024)}MB (max 50MB)`
6044
+ };
6045
+ }
6046
+ const arrayBuffer = await response.arrayBuffer();
6047
+ if (arrayBuffer.byteLength > MAX_FILE_SIZE) {
6048
+ return {
6049
+ success: false,
6050
+ error: `File too large: ${Math.round(arrayBuffer.byteLength / 1024 / 1024)}MB (max 50MB)`
6051
+ };
6052
+ }
6053
+ const ext = getExtension(url, contentType);
6054
+ const outputPath = join10(BACKGROUNDS_TARGET_DIR, `${filename}.${ext}`);
6055
+ await Bun.write(outputPath, arrayBuffer);
6056
+ return { success: true, path: outputPath };
6057
+ } catch (err) {
6058
+ clearTimeout(timeoutId);
6059
+ if (err instanceof Error && err.name === "AbortError") {
6060
+ return { success: false, error: `Download timed out after ${timeoutMs / 1000}s` };
6061
+ }
6062
+ return {
6063
+ success: false,
6064
+ error: err instanceof Error ? err.message : "Unknown error"
6065
+ };
6066
+ }
6067
+ }
6068
+ async function downloadThemeWallpapers(wallpapers, mode) {
6069
+ clearBackgroundsDir();
6070
+ const urls = mode === "light" && wallpapers.light && wallpapers.light.length > 0 ? wallpapers.light : wallpapers.dark;
6071
+ const paths = [];
6072
+ const errors = [];
6073
+ for (let i = 0;i < urls.length; i++) {
6074
+ const filename = urls.length === 1 ? "wallpaper" : `wallpaper-${i + 1}`;
6075
+ const result = await downloadWallpaper(urls[i], filename);
6076
+ if (result.success && result.path) {
6077
+ paths.push(result.path);
6078
+ } else if (result.error) {
6079
+ errors.push(`[${i + 1}] ${result.error}`);
6080
+ }
6081
+ }
6082
+ return { paths, errors };
6083
+ }
6084
+
6085
+ // src/cli/set-theme.ts
6086
+ var colors5 = {
6087
+ red: "\x1B[0;31m",
6088
+ green: "\x1B[0;32m",
6089
+ blue: "\x1B[0;34m",
6090
+ yellow: "\x1B[1;33m",
6091
+ cyan: "\x1B[0;36m",
6092
+ dim: "\x1B[2m",
6093
+ reset: "\x1B[0m"
6094
+ };
6095
+ async function listAllThemes() {
6096
+ await ensureConfigDir();
6097
+ const themes = [];
6098
+ const jsonThemes = await listJsonThemes();
6099
+ for (const item of jsonThemes) {
6100
+ for (const mode of item.availableModes) {
6101
+ themes.push({
6102
+ displayName: `${item.theme.title} (${mode})`,
6103
+ identifier: `${item.name}:${mode}`,
6104
+ type: "json",
6105
+ mode,
6106
+ path: item.path,
6107
+ author: item.theme.author
6108
+ });
6109
+ }
6110
+ }
6111
+ if (existsSync12(THEMES_DIR)) {
6112
+ const entries = readdirSync9(THEMES_DIR, { withFileTypes: true });
6113
+ for (const entry of entries) {
6114
+ if (entry.isDirectory()) {
6115
+ const themePath = join11(THEMES_DIR, entry.name);
6116
+ const theme = await parseTheme(themePath, entry.name);
6117
+ themes.push({
6118
+ displayName: theme.name,
6119
+ identifier: theme.name,
6120
+ type: "legacy",
6121
+ path: themePath,
6122
+ author: theme.metadata?.author,
6123
+ hasBackgrounds: theme.hasBackgrounds,
6124
+ isLightMode: theme.isLightMode
6125
+ });
6126
+ }
6127
+ }
6128
+ }
6129
+ return themes;
6130
+ }
6131
+ function clearDirectory(dir) {
6132
+ if (existsSync12(dir)) {
6133
+ const entries = readdirSync9(dir, { withFileTypes: true });
6134
+ for (const entry of entries) {
6135
+ const fullPath = join11(dir, entry.name);
6136
+ if (entry.isSymbolicLink() || entry.isFile()) {
6137
+ unlinkSync2(fullPath);
6138
+ } else if (entry.isDirectory()) {
6139
+ rmSync(fullPath, { recursive: true, force: true });
6140
+ }
6141
+ }
6142
+ }
6143
+ }
6144
+ function createSymlink(source, target) {
6145
+ if (existsSync12(target)) {
6146
+ unlinkSync2(target);
6147
+ }
6148
+ symlinkSync(source, target);
6149
+ }
6150
+ function parseThemeIdentifier(identifier) {
6151
+ if (identifier.includes(":")) {
6152
+ const [name, mode] = identifier.split(":");
6153
+ if (mode === "dark" || mode === "light") {
6154
+ return { name, mode };
6155
+ }
6156
+ }
6157
+ return { name: identifier };
6158
+ }
6159
+ async function applyJsonTheme(themePath, mode, saveMapping, identifier) {
6160
+ const theme = await loadThemeJson(themePath);
6161
+ await ensureConfigDir();
6162
+ await ensureDir2(THEME_TARGET_DIR);
6163
+ await ensureDir2(GENERATED_DIR);
6164
+ const installedTemplates = await listInstalledTemplates();
6165
+ if (installedTemplates.length === 0) {
6166
+ await installAllTemplates();
6167
+ }
6168
+ clearDirectory(THEME_TARGET_DIR);
6169
+ if (existsSync12(BACKGROUNDS_TARGET_DIR)) {
6170
+ rmSync(BACKGROUNDS_TARGET_DIR, { recursive: true, force: true });
6171
+ }
6172
+ const results = await generateThemeConfigs(theme, mode);
6173
+ const ghosttyThemesDir = join11(HOME_DIR, ".config", "ghostty", "themes");
6174
+ for (const result of results) {
6175
+ const filename = basename4(result.outputPath);
6176
+ if (filename === "formalconf-dark" || filename === "formalconf-light") {
6177
+ await ensureDir2(ghosttyThemesDir);
6178
+ const targetPath2 = join11(ghosttyThemesDir, filename);
6179
+ copyFileSync(result.outputPath, targetPath2);
6180
+ }
6181
+ const targetPath = join11(THEME_TARGET_DIR, filename);
6182
+ copyFileSync(result.outputPath, targetPath);
6183
+ }
6184
+ let wallpaperPaths = [];
6185
+ let wallpaperErrors = [];
6186
+ if (theme.wallpapers) {
6187
+ const wallpaperResult = await downloadThemeWallpapers(theme.wallpapers, mode);
6188
+ wallpaperPaths = wallpaperResult.paths;
6189
+ wallpaperErrors = wallpaperResult.errors;
6190
+ }
6191
+ if (saveMapping) {
6192
+ await setDeviceTheme(identifier);
6193
+ }
6194
+ let output = `Theme '${theme.title} (${mode})' applied successfully`;
6195
+ output += `
6196
+ Generated ${results.length} config files`;
6197
+ if (saveMapping) {
6198
+ output += `
6199
+ Saved as device preference for '${getDeviceHostname()}'`;
6200
+ }
6201
+ if (theme.author) {
6202
+ output += `
6203
+ Author: ${theme.author}`;
6204
+ }
6205
+ if (wallpaperPaths.length > 0) {
6206
+ output += `
6207
+ Wallpapers (${wallpaperPaths.length}):`;
6208
+ for (const path of wallpaperPaths) {
6209
+ output += `
6210
+ ${path}`;
6211
+ }
6212
+ }
6213
+ for (const error of wallpaperErrors) {
6214
+ output += `
6215
+ Warning: ${error}`;
6216
+ }
6217
+ const hookEnv = {
6218
+ FORMALCONF_THEME: identifier,
6219
+ FORMALCONF_THEME_MODE: mode,
6220
+ FORMALCONF_THEME_FILE: themePath
6221
+ };
6222
+ if (wallpaperPaths.length > 0) {
6223
+ hookEnv.FORMALCONF_WALLPAPER_PATHS = wallpaperPaths.join(":");
6224
+ }
6225
+ const hookSummary = await runHooks("theme-change", hookEnv);
6226
+ if (hookSummary.executed > 0) {
6227
+ output += `
6228
+ Hooks: ${hookSummary.succeeded}/${hookSummary.executed} succeeded`;
6229
+ for (const result of hookSummary.results) {
6230
+ if (!result.success) {
6231
+ output += `
6232
+ Warning: ${result.script} failed (exit ${result.exitCode})`;
6233
+ }
6234
+ }
6235
+ }
6236
+ return { output, success: true };
6237
+ }
6238
+ async function applyLegacyTheme(themeName, saveMapping) {
6239
+ const themeDir = join11(THEMES_DIR, themeName);
6240
+ if (!existsSync12(themeDir)) {
6241
+ return { output: `Theme '${themeName}' not found`, success: false };
6242
+ }
6243
+ await ensureConfigDir();
6244
+ await ensureDir2(THEME_TARGET_DIR);
6245
+ const theme = await parseTheme(themeDir, themeName);
6246
+ clearDirectory(THEME_TARGET_DIR);
6247
+ if (existsSync12(BACKGROUNDS_TARGET_DIR)) {
6248
+ rmSync(BACKGROUNDS_TARGET_DIR, { recursive: true, force: true });
6249
+ }
6250
+ const entries = readdirSync9(themeDir, { withFileTypes: true });
6251
+ for (const entry of entries) {
6252
+ const source = join11(themeDir, entry.name);
6253
+ if (entry.isFile() && entry.name !== "theme.yaml" && entry.name !== "light.mode") {
6254
+ const target = join11(THEME_TARGET_DIR, entry.name);
6255
+ createSymlink(source, target);
6256
+ }
6257
+ }
6258
+ if (theme.hasBackgrounds) {
6259
+ const backgroundsSource = join11(themeDir, "backgrounds");
6260
+ createSymlink(backgroundsSource, BACKGROUNDS_TARGET_DIR);
6261
+ }
6262
+ if (saveMapping) {
6263
+ await setDeviceTheme(themeName);
6264
+ }
6265
+ let output = `Theme '${theme.name}' applied successfully`;
6266
+ if (saveMapping) {
6267
+ output += ` (saved as device preference for '${getDeviceHostname()}')`;
6268
+ }
6269
+ if (theme.metadata?.author) {
6270
+ output += `
6271
+ Author: ${theme.metadata.author}`;
6272
+ }
6273
+ if (theme.hasBackgrounds) {
6274
+ output += `
6275
+ Wallpapers available at: ~/.config/formalconf/current/backgrounds/`;
6276
+ }
6277
+ if (theme.isLightMode) {
6278
+ output += `
6279
+ Note: This is a light mode theme`;
6280
+ }
6281
+ const hookSummary = await runHooks("theme-change", {
6282
+ FORMALCONF_THEME: themeName,
6283
+ FORMALCONF_THEME_DIR: themeDir
6284
+ });
6285
+ if (hookSummary.executed > 0) {
6286
+ output += `
6287
+ Hooks: ${hookSummary.succeeded}/${hookSummary.executed} succeeded`;
6288
+ for (const result of hookSummary.results) {
6289
+ if (!result.success) {
6290
+ output += `
6291
+ Warning: ${result.script} failed (exit ${result.exitCode})`;
6292
+ }
6293
+ }
6294
+ }
6295
+ return { output, success: true };
6296
+ }
6297
+ async function applyTheme(themeIdentifier, saveMapping = false) {
6298
+ const { name, mode } = parseThemeIdentifier(themeIdentifier);
6299
+ const jsonPath = join11(THEMES_DIR, `${name}.json`);
6300
+ if (existsSync12(jsonPath) && mode) {
6301
+ return applyJsonTheme(jsonPath, mode, saveMapping, themeIdentifier);
6302
+ }
6303
+ const legacyPath = join11(THEMES_DIR, name);
6304
+ if (existsSync12(legacyPath)) {
6305
+ return applyLegacyTheme(name, saveMapping);
6306
+ }
6307
+ const allThemes = await listAllThemes();
6308
+ const suggestions = allThemes.filter((t) => t.displayName.toLowerCase().includes(name.toLowerCase())).slice(0, 3);
6309
+ let output = `Theme '${themeIdentifier}' not found`;
6310
+ if (suggestions.length > 0) {
6311
+ output += `
6312
+
6313
+ Did you mean:`;
6314
+ for (const s of suggestions) {
6315
+ output += `
6316
+ - ${s.identifier}`;
6317
+ }
6318
+ }
6319
+ return { output, success: false };
6320
+ }
6321
+ async function showThemeInfo(themeIdentifier) {
6322
+ const { name, mode } = parseThemeIdentifier(themeIdentifier);
6323
+ const jsonPath = join11(THEMES_DIR, `${name}.json`);
6324
+ if (existsSync12(jsonPath)) {
6325
+ const theme2 = await loadThemeJson(jsonPath);
6326
+ const modes = getAvailableModes(theme2);
6327
+ console.log(`
6328
+ ${colors5.cyan}Theme: ${theme2.title}${colors5.reset}`);
6329
+ console.log(`Type: JSON template-based theme`);
6330
+ if (theme2.author)
6331
+ console.log(`Author: ${theme2.author}`);
6332
+ if (theme2.description)
6333
+ console.log(`Description: ${theme2.description}`);
6334
+ if (theme2.version)
6335
+ console.log(`Version: ${theme2.version}`);
6336
+ if (theme2.source)
6337
+ console.log(`Source: ${theme2.source}`);
6338
+ console.log(`Available modes: ${modes.join(", ")}`);
6339
+ if (theme2.neovim) {
6340
+ console.log(`
6341
+ ${colors5.green}Neovim integration:${colors5.reset}`);
6342
+ console.log(` Plugin: ${theme2.neovim.repo}`);
6343
+ console.log(` Colorscheme: ${theme2.neovim.colorscheme}`);
6344
+ if (theme2.neovim.light_colorscheme) {
6345
+ console.log(` Light colorscheme: ${theme2.neovim.light_colorscheme}`);
6346
+ }
6347
+ }
6348
+ if (theme2.wallpapers) {
6349
+ console.log(`
6350
+ ${colors5.green}Wallpapers:${colors5.reset}`);
6351
+ console.log(` Dark (${theme2.wallpapers.dark.length}):`);
6352
+ for (const url of theme2.wallpapers.dark) {
6353
+ console.log(` ${url}`);
6354
+ }
6355
+ if (theme2.wallpapers.light && theme2.wallpapers.light.length > 0) {
6356
+ console.log(` Light (${theme2.wallpapers.light.length}):`);
6357
+ for (const url of theme2.wallpapers.light) {
6358
+ console.log(` ${url}`);
6359
+ }
6360
+ }
6361
+ }
6362
+ return;
6363
+ }
6364
+ const themeDir = join11(THEMES_DIR, name);
6365
+ if (!existsSync12(themeDir)) {
6366
+ console.error(`${colors5.red}Error: Theme '${themeIdentifier}' not found${colors5.reset}`);
6367
+ process.exit(1);
6368
+ }
6369
+ const theme = await parseTheme(themeDir, name);
6370
+ console.log(`
6371
+ ${colors5.cyan}Theme: ${theme.name}${colors5.reset}`);
6372
+ console.log(`Type: Legacy directory-based theme`);
6373
+ if (theme.metadata) {
6374
+ if (theme.metadata.author)
6375
+ console.log(`Author: ${theme.metadata.author}`);
6376
+ if (theme.metadata.description)
6377
+ console.log(`Description: ${theme.metadata.description}`);
6378
+ if (theme.metadata.version)
6379
+ console.log(`Version: ${theme.metadata.version}`);
6380
+ if (theme.metadata.source)
6381
+ console.log(`Source: ${theme.metadata.source}`);
6382
+ }
6383
+ console.log(`
6384
+ Files (${theme.files.length}):`);
6385
+ for (const file of theme.files) {
6386
+ console.log(` ${colors5.blue}•${colors5.reset} ${file.name}`);
6387
+ }
6388
+ if (theme.hasBackgrounds) {
6389
+ console.log(`
6390
+ ${colors5.green}Has wallpapers${colors5.reset}`);
6391
+ }
6392
+ if (theme.hasPreview) {
6393
+ console.log(`${colors5.green}Has preview image${colors5.reset}`);
6394
+ }
6395
+ if (theme.isLightMode) {
6396
+ console.log(`${colors5.yellow}Light mode theme${colors5.reset}`);
6397
+ }
6398
+ }
6399
+ async function runSetTheme(themeName, saveMapping = false) {
6400
+ return applyTheme(themeName, saveMapping);
6401
+ }
6402
+ function showDeviceMappings() {
6403
+ const mappings = listDeviceMappings();
6404
+ const defaultTheme = getDefaultTheme();
6405
+ const currentDevice = getDeviceHostname();
6406
+ console.log(`${colors5.cyan}Device Theme Mappings${colors5.reset}`);
6407
+ console.log(`Current device: ${colors5.blue}${currentDevice}${colors5.reset}
6408
+ `);
6409
+ if (defaultTheme) {
6410
+ console.log(`Default theme: ${colors5.green}${defaultTheme}${colors5.reset}
6411
+ `);
6412
+ }
6413
+ if (mappings.length === 0) {
6414
+ console.log(`${colors5.dim}No device-specific themes configured.${colors5.reset}`);
6415
+ return;
6416
+ }
6417
+ console.log("Configured devices:");
6418
+ for (const mapping of mappings) {
6419
+ const marker = mapping.isCurrent ? ` ${colors5.green}(current)${colors5.reset}` : "";
6420
+ const date = new Date(mapping.setAt).toLocaleDateString();
6421
+ console.log(` ${colors5.blue}•${colors5.reset} ${mapping.device}${marker}: ${mapping.theme} ${colors5.dim}(set ${date})${colors5.reset}`);
6422
+ }
6423
+ }
6424
+ async function showThemeList() {
6425
+ const themes = await listAllThemes();
6426
+ const deviceTheme = getDeviceTheme();
6427
+ if (themes.length === 0) {
6428
+ console.log(`${colors5.yellow}No themes available.${colors5.reset}`);
6429
+ console.log(`
6430
+ To add themes:`);
6431
+ console.log(` - JSON themes: ${colors5.cyan}~/.config/formalconf/themes/*.json${colors5.reset}`);
6432
+ console.log(` - Legacy themes: ${colors5.cyan}~/.config/formalconf/themes/<name>/${colors5.reset}`);
6433
+ return;
6434
+ }
6435
+ console.log(`${colors5.cyan}Usage: formalconf theme <theme-id>${colors5.reset}`);
6436
+ console.log(` formalconf theme <theme-id> --save ${colors5.dim}(save as device preference)${colors5.reset}`);
6437
+ console.log(` formalconf theme --apply ${colors5.dim}(apply device's theme)${colors5.reset}`);
6438
+ console.log(` formalconf theme --list-devices ${colors5.dim}(show device mappings)${colors5.reset}`);
6439
+ console.log(` formalconf theme --default <id> ${colors5.dim}(set default theme)${colors5.reset}`);
6440
+ console.log(` formalconf theme --clear-default ${colors5.dim}(remove default theme)${colors5.reset}`);
6441
+ console.log(` formalconf theme --clear ${colors5.dim}(remove device mapping)${colors5.reset}`);
6442
+ console.log(` formalconf theme --info <theme-id> ${colors5.dim}(show theme details)${colors5.reset}
6443
+ `);
6444
+ const jsonThemes = themes.filter((t) => t.type === "json");
6445
+ const legacyThemes = themes.filter((t) => t.type === "legacy");
6446
+ if (jsonThemes.length > 0) {
6447
+ console.log(`${colors5.cyan}Template-based themes:${colors5.reset}`);
6448
+ for (const theme of jsonThemes) {
6449
+ const extras = [];
6450
+ if (theme.identifier === deviceTheme)
6451
+ extras.push("device");
6452
+ const suffix = extras.length ? ` ${colors5.dim}(${extras.join(", ")})${colors5.reset}` : "";
6453
+ console.log(` ${colors5.blue}•${colors5.reset} ${theme.displayName} ${colors5.dim}[${theme.identifier}]${colors5.reset}${suffix}`);
6454
+ }
6455
+ }
6456
+ if (legacyThemes.length > 0) {
6457
+ if (jsonThemes.length > 0)
6458
+ console.log("");
6459
+ console.log(`${colors5.cyan}Legacy themes:${colors5.reset}`);
6460
+ for (const theme of legacyThemes) {
6461
+ const extras = [];
6462
+ if (theme.hasBackgrounds)
6463
+ extras.push("wallpapers");
6464
+ if (theme.isLightMode)
6465
+ extras.push("light");
6466
+ if (theme.identifier === deviceTheme)
6467
+ extras.push("device");
6468
+ const suffix = extras.length ? ` ${colors5.dim}(${extras.join(", ")})${colors5.reset}` : "";
6469
+ console.log(` ${colors5.blue}•${colors5.reset} ${theme.displayName}${suffix}`);
6470
+ }
6471
+ }
6472
+ }
6473
+ async function migrateTheme(themeName) {
6474
+ const legacyPath = join11(THEMES_DIR, themeName);
6475
+ if (!existsSync12(legacyPath)) {
6476
+ console.error(`${colors5.red}Error: Legacy theme '${themeName}' not found${colors5.reset}`);
6477
+ process.exit(1);
6478
+ }
6479
+ console.log(`Extracting colors from '${themeName}'...`);
6480
+ const result = await extractFromLegacyTheme(legacyPath);
6481
+ if (!result) {
6482
+ console.error(`${colors5.red}Error: Could not extract colors from theme${colors5.reset}`);
6483
+ console.error(`No supported config files found (kitty.conf, alacritty.toml, ghostty.conf)`);
6484
+ process.exit(1);
6485
+ }
6486
+ console.log(`Found colors in: ${result.source}`);
6487
+ const missing = validatePalette2(result.colors);
6488
+ if (missing.length > 0) {
6489
+ console.log(`${colors5.yellow}Warning: Missing colors will be filled with defaults:${colors5.reset}`);
6490
+ console.log(` ${missing.join(", ")}`);
6491
+ }
6492
+ const isLight = existsSync12(join11(legacyPath, "light.mode"));
6493
+ const themeJson = generateThemeJson(themeName, result.colors, {
6494
+ description: `Migrated from legacy theme`,
6495
+ isLight
6496
+ });
6497
+ const outputPath = join11(THEMES_DIR, `${themeName}.json`);
6498
+ if (existsSync12(outputPath)) {
6499
+ console.error(`${colors5.red}Error: JSON theme '${themeName}.json' already exists${colors5.reset}`);
6500
+ console.error(`Delete or rename it first, then try again.`);
6501
+ process.exit(1);
6502
+ }
6503
+ await writeFile(outputPath, JSON.stringify(themeJson, null, 2));
6504
+ console.log(`${colors5.green}Theme migrated successfully to '${themeName}.json'${colors5.reset}`);
6505
+ console.log(`
6506
+ Next steps:`);
6507
+ console.log(` 1. Review and edit ${outputPath}`);
6508
+ console.log(` 2. Add a light palette if needed`);
6509
+ console.log(` 3. Add neovim configuration if desired`);
6510
+ console.log(` 4. Test with: bun run theme ${themeName}:${isLight ? "light" : "dark"}`);
6511
+ }
6512
+ async function showTemplateStatus() {
6513
+ const installed = await listInstalledTemplates();
6514
+ const updates = await checkTemplateUpdates();
6515
+ console.log(`${colors5.cyan}Template Status${colors5.reset}
6516
+ `);
6517
+ if (installed.length === 0) {
6518
+ console.log(`${colors5.yellow}No templates installed.${colors5.reset}`);
6519
+ console.log(`Run 'formalconf theme --install-templates' to install bundled templates.`);
6520
+ return;
6521
+ }
6522
+ console.log(`Installed templates (${installed.length}):`);
6523
+ for (const template of installed) {
6524
+ console.log(` ${colors5.blue}•${colors5.reset} ${template.name}`);
6525
+ }
6526
+ if (updates.length > 0) {
6527
+ console.log(`
6528
+ ${colors5.yellow}Updates available:${colors5.reset}`);
6529
+ for (const update of updates) {
6530
+ if (update.updateAvailable) {
6531
+ const locked = update.customOverride ? ` ${colors5.dim}(locked)${colors5.reset}` : "";
6532
+ console.log(` ${colors5.blue}•${colors5.reset} ${update.name}: ${update.installedVersion} -> ${update.bundledVersion}${locked}`);
6533
+ }
6534
+ }
6535
+ }
6536
+ }
6537
+ async function main4() {
6538
+ const { positionals, values } = parseArgs4({
6539
+ args: process.argv.slice(2),
6540
+ options: {
6541
+ info: { type: "boolean", short: "i" },
6542
+ save: { type: "boolean", short: "s" },
6543
+ apply: { type: "boolean", short: "a" },
6544
+ "list-devices": { type: "boolean", short: "l" },
6545
+ default: { type: "string", short: "d" },
6546
+ "clear-default": { type: "boolean" },
6547
+ clear: { type: "boolean", short: "c" },
6548
+ "install-templates": { type: "boolean" },
6549
+ "template-status": { type: "boolean" },
6550
+ migrate: { type: "string", short: "m" }
6551
+ },
6552
+ allowPositionals: true
6553
+ });
6554
+ const [themeName] = positionals;
6555
+ if (values["template-status"]) {
6556
+ await showTemplateStatus();
6557
+ return;
6558
+ }
6559
+ if (values["install-templates"]) {
6560
+ await installAllTemplates();
6561
+ console.log(`${colors5.green}Templates installed successfully.${colors5.reset}`);
6562
+ return;
6563
+ }
6564
+ if (values.migrate) {
6565
+ await migrateTheme(values.migrate);
6566
+ return;
6567
+ }
6568
+ if (values["list-devices"]) {
6569
+ showDeviceMappings();
6570
+ return;
6571
+ }
6572
+ if (values.clear) {
6573
+ const deviceTheme = getDeviceTheme();
6574
+ if (!deviceTheme) {
6575
+ console.log(`${colors5.yellow}No theme configured for this device.${colors5.reset}`);
6576
+ return;
6577
+ }
6578
+ await clearDeviceTheme();
6579
+ console.log(`${colors5.green}Removed theme mapping for '${getDeviceHostname()}'.${colors5.reset}`);
6580
+ return;
6581
+ }
6582
+ if (values["clear-default"]) {
6583
+ await setDefaultTheme(null);
6584
+ console.log(`${colors5.green}Default theme cleared.${colors5.reset}`);
6585
+ return;
6586
+ }
6587
+ if (values.default !== undefined) {
6588
+ const allThemes = await listAllThemes();
6589
+ const exists = allThemes.some((t) => t.identifier === values.default);
6590
+ if (!exists) {
6591
+ console.error(`${colors5.red}Error: Theme '${values.default}' not found${colors5.reset}`);
6592
+ process.exit(1);
6593
+ }
6594
+ await setDefaultTheme(values.default);
6595
+ console.log(`${colors5.green}Default theme set to '${values.default}'.${colors5.reset}`);
6596
+ return;
6597
+ }
6598
+ if (values.apply) {
6599
+ const deviceTheme = getDeviceTheme();
6600
+ if (!deviceTheme) {
6601
+ console.log(`${colors5.yellow}No theme configured for device '${getDeviceHostname()}'.${colors5.reset}`);
6602
+ console.log(`Use 'formalconf theme <name> --save' to set a device preference.`);
6603
+ return;
6604
+ }
6605
+ const result2 = await applyTheme(deviceTheme);
6606
+ console.log(result2.success ? `${colors5.green}${result2.output}${colors5.reset}` : `${colors5.red}${result2.output}${colors5.reset}`);
6607
+ return;
6608
+ }
6609
+ if (!themeName) {
6610
+ const deviceTheme = getDeviceTheme();
6611
+ if (deviceTheme) {
6612
+ const result2 = await applyTheme(deviceTheme);
6613
+ console.log(result2.success ? `${colors5.green}${result2.output}${colors5.reset}` : `${colors5.red}${result2.output}${colors5.reset}`);
6614
+ } else {
6615
+ await showThemeList();
6616
+ }
6617
+ return;
6618
+ }
6619
+ if (values.info) {
6620
+ await showThemeInfo(themeName);
6621
+ return;
6622
+ }
6623
+ const result = await applyTheme(themeName, values.save ?? false);
6624
+ console.log(result.success ? `${colors5.green}${result.output}${colors5.reset}` : `${colors5.red}${result.output}${colors5.reset}`);
6625
+ }
6626
+ var isMainModule4 = process.argv[1]?.includes("set-theme");
6627
+ if (isMainModule4) {
6628
+ main4().catch(console.error);
6629
+ }
6630
+
6631
+ // src/components/menus/ThemeMenu.tsx
6632
+ import { jsxDEV as jsxDEV19 } from "react/jsx-dev-runtime";
6633
+ function ThemeMenu({ onBack }) {
6634
+ const [themes, setThemes] = useState10([]);
6635
+ const [loading, setLoading] = useState10(true);
6636
+ const [deviceTheme, setDeviceThemeName] = useState10(null);
6637
+ const { state, output, success, isRunning, isResult, execute, reset } = useMenuAction();
6638
+ const hostname2 = getDeviceHostname();
6639
+ const grid = useThemeGrid({
6640
+ itemCount: themes.length,
6641
+ onSelect: (index) => applyTheme2(themes[index], false),
6642
+ onSelectAndSave: (index) => applyTheme2(themes[index], true),
6643
+ onBack,
6644
+ enabled: state === "menu" && !loading && themes.length > 0
6645
+ });
6646
+ useEffect6(() => {
6647
+ async function loadThemes() {
6648
+ const loadedThemes = await listAllThemes();
6649
+ setThemes(loadedThemes);
6650
+ setDeviceThemeName(getDeviceTheme());
6651
+ setLoading(false);
6652
+ }
6653
+ loadThemes();
6654
+ }, []);
6655
+ const applyTheme2 = async (theme, saveAsDeviceDefault) => {
6656
+ await execute(() => runSetTheme(theme.identifier, saveAsDeviceDefault));
6657
+ if (saveAsDeviceDefault) {
6658
+ setDeviceThemeName(theme.identifier);
6659
+ }
6660
+ };
6661
+ const visibleThemes = useMemo4(() => {
6662
+ return themes.slice(grid.visibleStartIndex, grid.visibleEndIndex);
6663
+ }, [themes, grid.visibleStartIndex, grid.visibleEndIndex]);
6664
+ if (loading || isRunning) {
6665
+ return /* @__PURE__ */ jsxDEV19(LoadingPanel, {
6666
+ title: "Select Theme",
6667
+ label: loading ? "Loading themes..." : "Applying theme..."
6668
+ }, undefined, false, undefined, this);
6669
+ }
6670
+ if (isResult) {
6671
+ return /* @__PURE__ */ jsxDEV19(CommandOutput, {
6672
+ title: "Select Theme",
6673
+ output,
6674
+ success,
6675
+ onDismiss: reset
6676
+ }, undefined, false, undefined, this);
6677
+ }
6678
+ if (themes.length === 0) {
6679
+ return /* @__PURE__ */ jsxDEV19(Panel, {
4927
6680
  title: "Select Theme",
4928
6681
  children: [
4929
6682
  /* @__PURE__ */ jsxDEV19(Box16, {
@@ -4934,11 +6687,15 @@ function ThemeMenu({ onBack }) {
4934
6687
  children: "No themes available."
4935
6688
  }, undefined, false, undefined, this),
4936
6689
  /* @__PURE__ */ jsxDEV19(Text15, {
4937
- children: "This system is compatible with omarchy themes."
6690
+ children: "Add themes to one of the following locations:"
6691
+ }, undefined, false, undefined, this),
6692
+ /* @__PURE__ */ jsxDEV19(Text15, {
6693
+ dimColor: true,
6694
+ children: " JSON themes: ~/.config/formalconf/themes/*.json"
4938
6695
  }, undefined, false, undefined, this),
4939
6696
  /* @__PURE__ */ jsxDEV19(Text15, {
4940
6697
  dimColor: true,
4941
- children: "Add themes to ~/.config/formalconf/themes/"
6698
+ children: " Legacy themes: ~/.config/formalconf/themes/name/"
4942
6699
  }, undefined, false, undefined, this)
4943
6700
  ]
4944
6701
  }, undefined, true, undefined, this),
@@ -4973,8 +6730,9 @@ function ThemeMenu({ onBack }) {
4973
6730
  children: visibleThemes.map((theme, index) => /* @__PURE__ */ jsxDEV19(ThemeCard, {
4974
6731
  theme,
4975
6732
  isSelected: grid.visibleStartIndex + index === grid.selectedIndex,
4976
- width: grid.cardWidth
4977
- }, theme.path, false, undefined, this))
6733
+ width: grid.cardWidth,
6734
+ isDeviceTheme: theme.identifier === deviceTheme
6735
+ }, theme.identifier, false, undefined, this))
4978
6736
  }, undefined, false, undefined, this),
4979
6737
  grid.showScrollDown && /* @__PURE__ */ jsxDEV19(Text15, {
4980
6738
  dimColor: true,
@@ -4988,11 +6746,21 @@ function ThemeMenu({ onBack }) {
4988
6746
  }, undefined, true, undefined, this),
4989
6747
  /* @__PURE__ */ jsxDEV19(Box16, {
4990
6748
  marginTop: 1,
4991
- children: /* @__PURE__ */ jsxDEV19(Text15, {
4992
- dimColor: true,
4993
- children: "←→↑↓/hjkl navigate Enter select • Esc back"
4994
- }, undefined, false, undefined, this)
4995
- }, undefined, false, undefined, this)
6749
+ flexDirection: "column",
6750
+ children: [
6751
+ /* @__PURE__ */ jsxDEV19(Text15, {
6752
+ dimColor: true,
6753
+ children: "←→↑↓/hjkl navigate Enter apply • Shift+Enter save as device default • Esc back"
6754
+ }, undefined, false, undefined, this),
6755
+ /* @__PURE__ */ jsxDEV19(Text15, {
6756
+ dimColor: true,
6757
+ children: [
6758
+ "Device: ",
6759
+ hostname2
6760
+ ]
6761
+ }, undefined, true, undefined, this)
6762
+ ]
6763
+ }, undefined, true, undefined, this)
4996
6764
  ]
4997
6765
  }, undefined, true, undefined, this);
4998
6766
  }