formalconf 2.0.8 → 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.
@@ -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,9 +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");
320
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");
321
331
  async function ensureDir2(path) {
322
332
  await ensureDir(path);
323
333
  }
@@ -327,6 +337,8 @@ async function ensureConfigDir() {
327
337
  await ensureDir2(THEMES_DIR);
328
338
  await ensureDir2(THEME_TARGET_DIR);
329
339
  await ensureDir2(BACKGROUNDS_TARGET_DIR);
340
+ await ensureDir2(TEMPLATES_DIR);
341
+ await ensureDir2(GENERATED_DIR);
330
342
  }
331
343
  async function dirHasContents(path) {
332
344
  try {
@@ -447,7 +459,7 @@ function StatusIndicator({
447
459
  // package.json
448
460
  var package_default = {
449
461
  name: "formalconf",
450
- version: "2.0.8",
462
+ version: "2.0.9",
451
463
  description: "Dotfiles management TUI for macOS - config management, package sync, and theme switching",
452
464
  type: "module",
453
465
  main: "./dist/formalconf.js",
@@ -4503,23 +4515,34 @@ function PackageMenu({ onBack }) {
4503
4515
  // src/components/menus/ThemeMenu.tsx
4504
4516
  import { useState as useState10, useEffect as useEffect6, useMemo as useMemo4 } from "react";
4505
4517
  import { Box as Box16, Text as Text15 } from "ink";
4506
- import { existsSync as existsSync8, readdirSync as readdirSync5 } from "fs";
4507
- import { join as join6 } from "path";
4508
4518
 
4509
4519
  // src/components/ThemeCard.tsx
4510
4520
  import { Box as Box15, Text as Text14 } from "ink";
4511
4521
  import { jsxDEV as jsxDEV18 } from "react/jsx-dev-runtime";
4522
+ function isLegacyTheme(theme) {
4523
+ return "files" in theme;
4524
+ }
4512
4525
  function ThemeCard({ theme, isSelected, width, isDeviceTheme }) {
4513
4526
  const borderColor = isSelected ? colors.accent : colors.border;
4514
4527
  const nameColor = isSelected ? colors.primary : colors.text;
4515
4528
  const indicators = [];
4516
4529
  if (isDeviceTheme)
4517
4530
  indicators.push("device");
4518
- if (theme.hasBackgrounds)
4519
- indicators.push("bg");
4520
- if (theme.isLightMode)
4521
- indicators.push("light");
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
+ }
4522
4544
  const indicatorText = indicators.length > 0 ? ` [${indicators.join(" ")}]` : "";
4545
+ const displayName = isLegacyTheme(theme) ? theme.name : theme.displayName;
4523
4546
  return /* @__PURE__ */ jsxDEV18(Box15, {
4524
4547
  flexDirection: "column",
4525
4548
  width,
@@ -4536,7 +4559,7 @@ function ThemeCard({ theme, isSelected, width, isDeviceTheme }) {
4536
4559
  color: nameColor,
4537
4560
  bold: true,
4538
4561
  wrap: "truncate",
4539
- children: theme.name
4562
+ children: displayName
4540
4563
  }, undefined, false, undefined, this),
4541
4564
  /* @__PURE__ */ jsxDEV18(Text14, {
4542
4565
  color: colors.primaryDim,
@@ -4631,6 +4654,18 @@ function useThemeGrid({
4631
4654
  };
4632
4655
  }
4633
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
+
4634
4669
  // src/lib/theme-parser.ts
4635
4670
  init_runtime();
4636
4671
  import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
@@ -4706,14 +4741,44 @@ async function parseTheme(themePath, themeName) {
4706
4741
  };
4707
4742
  }
4708
4743
 
4709
- // src/cli/set-theme.ts
4710
- import { parseArgs as parseArgs4 } from "util";
4711
- import { readdirSync as readdirSync4, existsSync as existsSync7, rmSync, symlinkSync, unlinkSync } from "fs";
4744
+ // src/lib/hooks.ts
4745
+ init_runtime();
4712
4746
  import { join as join5 } from "path";
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: [] };
4752
+ }
4753
+ const entries = readdirSync4(hookDir).filter((name) => !name.startsWith(".")).sort();
4754
+ const results = [];
4755
+ for (const entry of entries) {
4756
+ const scriptPath2 = join5(hookDir, entry);
4757
+ const stat = statSync(scriptPath2);
4758
+ if (stat.isDirectory()) {
4759
+ continue;
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
+ });
4769
+ }
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
+ };
4777
+ }
4713
4778
 
4714
4779
  // src/lib/theme-config.ts
4715
4780
  import { hostname } from "os";
4716
- import { existsSync as existsSync6, readFileSync, writeFileSync } from "fs";
4781
+ import { existsSync as existsSync7, readFileSync, writeFileSync } from "fs";
4717
4782
  var DEFAULT_CONFIG = {
4718
4783
  version: 1,
4719
4784
  defaultTheme: null,
@@ -4723,7 +4788,7 @@ function getDeviceHostname() {
4723
4788
  return hostname();
4724
4789
  }
4725
4790
  function loadThemeConfig() {
4726
- if (!existsSync6(THEME_CONFIG_PATH)) {
4791
+ if (!existsSync7(THEME_CONFIG_PATH)) {
4727
4792
  return { ...DEFAULT_CONFIG, devices: {} };
4728
4793
  }
4729
4794
  try {
@@ -4787,6 +4852,1236 @@ function getDefaultTheme() {
4787
4852
  return config.defaultTheme;
4788
4853
  }
4789
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" };
4937
+ }
4938
+ if (!isValidHex(value)) {
4939
+ return { path, message: `invalid hex color: ${value}` };
4940
+ }
4941
+ return null;
4942
+ }
4943
+ function isValidUrl(value) {
4944
+ try {
4945
+ const url = new URL(value);
4946
+ return ["http:", "https:"].includes(url.protocol);
4947
+ } catch {
4948
+ return false;
4949
+ }
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;
4956
+ }
4957
+ if (arr.length === 0) {
4958
+ errors.push({ path, message: "must have at least one URL" });
4959
+ return errors;
4960
+ }
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
+ }
4968
+ }
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;
4976
+ }
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`));
4982
+ }
4983
+ if ("light" in obj) {
4984
+ errors.push(...validateUrlArray(obj.light, `${path}.light`));
4985
+ }
4986
+ return errors;
4987
+ }
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;
4993
+ }
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
+ }
5003
+ }
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
+ }
5010
+ }
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;
5018
+ }
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" });
5022
+ }
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" });
5037
+ }
5038
+ return errors;
5039
+ }
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;
5065
+ }
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" });
5082
+ }
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(`
5112
+ `);
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
+ }
5186
+ }
5187
+ }
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;
5238
+ }
5239
+ }
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 };
5249
+ }
5250
+
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;
5302
+ }
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 };
5335
+ }
5336
+ try {
5337
+ const content = await readText(TEMPLATES_MANIFEST_PATH);
5338
+ return JSON.parse(content);
5339
+ } catch {
5340
+ return { ...DEFAULT_MANIFEST };
5341
+ }
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
+
4790
6085
  // src/cli/set-theme.ts
4791
6086
  var colors5 = {
4792
6087
  red: "\x1B[0;31m",
@@ -4797,29 +6092,49 @@ var colors5 = {
4797
6092
  dim: "\x1B[2m",
4798
6093
  reset: "\x1B[0m"
4799
6094
  };
4800
- async function listThemes() {
6095
+ async function listAllThemes() {
4801
6096
  await ensureConfigDir();
4802
- if (!existsSync7(THEMES_DIR)) {
4803
- return [];
4804
- }
4805
- const entries = readdirSync4(THEMES_DIR, { withFileTypes: true });
4806
6097
  const themes = [];
4807
- for (const entry of entries) {
4808
- if (entry.isDirectory()) {
4809
- const themePath = join5(THEMES_DIR, entry.name);
4810
- const theme = await parseTheme(themePath, entry.name);
4811
- themes.push(theme);
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
+ }
4812
6127
  }
4813
6128
  }
4814
6129
  return themes;
4815
6130
  }
4816
6131
  function clearDirectory(dir) {
4817
- if (existsSync7(dir)) {
4818
- const entries = readdirSync4(dir, { withFileTypes: true });
6132
+ if (existsSync12(dir)) {
6133
+ const entries = readdirSync9(dir, { withFileTypes: true });
4819
6134
  for (const entry of entries) {
4820
- const fullPath = join5(dir, entry.name);
6135
+ const fullPath = join11(dir, entry.name);
4821
6136
  if (entry.isSymbolicLink() || entry.isFile()) {
4822
- unlinkSync(fullPath);
6137
+ unlinkSync2(fullPath);
4823
6138
  } else if (entry.isDirectory()) {
4824
6139
  rmSync(fullPath, { recursive: true, force: true });
4825
6140
  }
@@ -4827,33 +6142,121 @@ function clearDirectory(dir) {
4827
6142
  }
4828
6143
  }
4829
6144
  function createSymlink(source, target) {
4830
- if (existsSync7(target)) {
4831
- unlinkSync(target);
6145
+ if (existsSync12(target)) {
6146
+ unlinkSync2(target);
4832
6147
  }
4833
6148
  symlinkSync(source, target);
4834
6149
  }
4835
- async function applyTheme(themeName, saveMapping = false) {
4836
- const themeDir = join5(THEMES_DIR, themeName);
4837
- if (!existsSync7(themeDir)) {
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)) {
4838
6241
  return { output: `Theme '${themeName}' not found`, success: false };
4839
6242
  }
4840
6243
  await ensureConfigDir();
4841
6244
  await ensureDir2(THEME_TARGET_DIR);
4842
6245
  const theme = await parseTheme(themeDir, themeName);
4843
6246
  clearDirectory(THEME_TARGET_DIR);
4844
- if (existsSync7(BACKGROUNDS_TARGET_DIR)) {
6247
+ if (existsSync12(BACKGROUNDS_TARGET_DIR)) {
4845
6248
  rmSync(BACKGROUNDS_TARGET_DIR, { recursive: true, force: true });
4846
6249
  }
4847
- const entries = readdirSync4(themeDir, { withFileTypes: true });
6250
+ const entries = readdirSync9(themeDir, { withFileTypes: true });
4848
6251
  for (const entry of entries) {
4849
- const source = join5(themeDir, entry.name);
6252
+ const source = join11(themeDir, entry.name);
4850
6253
  if (entry.isFile() && entry.name !== "theme.yaml" && entry.name !== "light.mode") {
4851
- const target = join5(THEME_TARGET_DIR, entry.name);
6254
+ const target = join11(THEME_TARGET_DIR, entry.name);
4852
6255
  createSymlink(source, target);
4853
6256
  }
4854
6257
  }
4855
6258
  if (theme.hasBackgrounds) {
4856
- const backgroundsSource = join5(themeDir, "backgrounds");
6259
+ const backgroundsSource = join11(themeDir, "backgrounds");
4857
6260
  createSymlink(backgroundsSource, BACKGROUNDS_TARGET_DIR);
4858
6261
  }
4859
6262
  if (saveMapping) {
@@ -4875,17 +6278,98 @@ Wallpapers available at: ~/.config/formalconf/current/backgrounds/`;
4875
6278
  output += `
4876
6279
  Note: This is a light mode theme`;
4877
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
+ }
4878
6295
  return { output, success: true };
4879
6296
  }
4880
- async function showThemeInfo(themeName) {
4881
- const themeDir = join5(THEMES_DIR, themeName);
4882
- if (!existsSync7(themeDir)) {
4883
- console.error(`${colors5.red}Error: Theme '${themeName}' not found${colors5.reset}`);
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}`);
4884
6367
  process.exit(1);
4885
6368
  }
4886
- const theme = await parseTheme(themeDir, themeName);
6369
+ const theme = await parseTheme(themeDir, name);
4887
6370
  console.log(`
4888
6371
  ${colors5.cyan}Theme: ${theme.name}${colors5.reset}`);
6372
+ console.log(`Type: Legacy directory-based theme`);
4889
6373
  if (theme.metadata) {
4890
6374
  if (theme.metadata.author)
4891
6375
  console.log(`Author: ${theme.metadata.author}`);
@@ -4938,35 +6422,116 @@ function showDeviceMappings() {
4938
6422
  }
4939
6423
  }
4940
6424
  async function showThemeList() {
4941
- const themes = await listThemes();
6425
+ const themes = await listAllThemes();
4942
6426
  const deviceTheme = getDeviceTheme();
4943
6427
  if (themes.length === 0) {
4944
6428
  console.log(`${colors5.yellow}No themes available.${colors5.reset}`);
4945
- console.log(`This system is compatible with omarchy themes.`);
4946
6429
  console.log(`
4947
- Add themes to: ${colors5.cyan}~/.config/formalconf/themes/${colors5.reset}`);
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}`);
4948
6433
  return;
4949
6434
  }
4950
- console.log(`${colors5.cyan}Usage: formalconf theme <theme-name>${colors5.reset}`);
4951
- console.log(` formalconf theme <theme-name> --save ${colors5.dim}(save as device preference)${colors5.reset}`);
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}`);
4952
6437
  console.log(` formalconf theme --apply ${colors5.dim}(apply device's theme)${colors5.reset}`);
4953
6438
  console.log(` formalconf theme --list-devices ${colors5.dim}(show device mappings)${colors5.reset}`);
4954
- console.log(` formalconf theme --default <name> ${colors5.dim}(set default theme)${colors5.reset}`);
6439
+ console.log(` formalconf theme --default <id> ${colors5.dim}(set default theme)${colors5.reset}`);
4955
6440
  console.log(` formalconf theme --clear-default ${colors5.dim}(remove default theme)${colors5.reset}`);
4956
6441
  console.log(` formalconf theme --clear ${colors5.dim}(remove device mapping)${colors5.reset}`);
4957
- console.log(` formalconf theme --info <theme-name> ${colors5.dim}(show theme details)${colors5.reset}
6442
+ console.log(` formalconf theme --info <theme-id> ${colors5.dim}(show theme details)${colors5.reset}
4958
6443
  `);
4959
- console.log("Available themes:");
4960
- for (const theme of themes) {
4961
- const extras = [];
4962
- if (theme.hasBackgrounds)
4963
- extras.push("wallpapers");
4964
- if (theme.isLightMode)
4965
- extras.push("light");
4966
- if (theme.name === deviceTheme)
4967
- extras.push("device");
4968
- const suffix = extras.length ? ` ${colors5.dim}(${extras.join(", ")})${colors5.reset}` : "";
4969
- console.log(` ${colors5.blue}•${colors5.reset} ${theme.name}${suffix}`);
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
+ }
4970
6535
  }
4971
6536
  }
4972
6537
  async function main4() {
@@ -4979,11 +6544,27 @@ async function main4() {
4979
6544
  "list-devices": { type: "boolean", short: "l" },
4980
6545
  default: { type: "string", short: "d" },
4981
6546
  "clear-default": { type: "boolean" },
4982
- clear: { type: "boolean", short: "c" }
6547
+ clear: { type: "boolean", short: "c" },
6548
+ "install-templates": { type: "boolean" },
6549
+ "template-status": { type: "boolean" },
6550
+ migrate: { type: "string", short: "m" }
4983
6551
  },
4984
6552
  allowPositionals: true
4985
6553
  });
4986
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
+ }
4987
6568
  if (values["list-devices"]) {
4988
6569
  showDeviceMappings();
4989
6570
  return;
@@ -5004,8 +6585,9 @@ async function main4() {
5004
6585
  return;
5005
6586
  }
5006
6587
  if (values.default !== undefined) {
5007
- const themeDir = join5(THEMES_DIR, values.default);
5008
- if (!existsSync7(themeDir)) {
6588
+ const allThemes = await listAllThemes();
6589
+ const exists = allThemes.some((t) => t.identifier === values.default);
6590
+ if (!exists) {
5009
6591
  console.error(`${colors5.red}Error: Theme '${values.default}' not found${colors5.reset}`);
5010
6592
  process.exit(1);
5011
6593
  }
@@ -5063,20 +6645,7 @@ function ThemeMenu({ onBack }) {
5063
6645
  });
5064
6646
  useEffect6(() => {
5065
6647
  async function loadThemes() {
5066
- if (!existsSync8(THEMES_DIR)) {
5067
- setThemes([]);
5068
- setLoading(false);
5069
- return;
5070
- }
5071
- const entries = readdirSync5(THEMES_DIR, { withFileTypes: true });
5072
- const loadedThemes = [];
5073
- for (const entry of entries) {
5074
- if (entry.isDirectory()) {
5075
- const themePath = join6(THEMES_DIR, entry.name);
5076
- const theme = await parseTheme(themePath, entry.name);
5077
- loadedThemes.push(theme);
5078
- }
5079
- }
6648
+ const loadedThemes = await listAllThemes();
5080
6649
  setThemes(loadedThemes);
5081
6650
  setDeviceThemeName(getDeviceTheme());
5082
6651
  setLoading(false);
@@ -5084,10 +6653,9 @@ function ThemeMenu({ onBack }) {
5084
6653
  loadThemes();
5085
6654
  }, []);
5086
6655
  const applyTheme2 = async (theme, saveAsDeviceDefault) => {
5087
- const themeName = theme.path.split("/").pop();
5088
- await execute(() => runSetTheme(themeName, saveAsDeviceDefault));
6656
+ await execute(() => runSetTheme(theme.identifier, saveAsDeviceDefault));
5089
6657
  if (saveAsDeviceDefault) {
5090
- setDeviceThemeName(themeName);
6658
+ setDeviceThemeName(theme.identifier);
5091
6659
  }
5092
6660
  };
5093
6661
  const visibleThemes = useMemo4(() => {
@@ -5119,11 +6687,15 @@ function ThemeMenu({ onBack }) {
5119
6687
  children: "No themes available."
5120
6688
  }, undefined, false, undefined, this),
5121
6689
  /* @__PURE__ */ jsxDEV19(Text15, {
5122
- 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"
5123
6695
  }, undefined, false, undefined, this),
5124
6696
  /* @__PURE__ */ jsxDEV19(Text15, {
5125
6697
  dimColor: true,
5126
- children: "Add themes to ~/.config/formalconf/themes/"
6698
+ children: " Legacy themes: ~/.config/formalconf/themes/name/"
5127
6699
  }, undefined, false, undefined, this)
5128
6700
  ]
5129
6701
  }, undefined, true, undefined, this),
@@ -5159,8 +6731,8 @@ function ThemeMenu({ onBack }) {
5159
6731
  theme,
5160
6732
  isSelected: grid.visibleStartIndex + index === grid.selectedIndex,
5161
6733
  width: grid.cardWidth,
5162
- isDeviceTheme: theme.name === deviceTheme
5163
- }, theme.path, false, undefined, this))
6734
+ isDeviceTheme: theme.identifier === deviceTheme
6735
+ }, theme.identifier, false, undefined, this))
5164
6736
  }, undefined, false, undefined, this),
5165
6737
  grid.showScrollDown && /* @__PURE__ */ jsxDEV19(Text15, {
5166
6738
  dimColor: true,