formalconf 2.0.8 → 2.0.10

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