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.
- package/README.md +33 -0
- package/dist/formalconf.js +1668 -87
- package/package.json +1 -1
package/dist/formalconf.js
CHANGED
|
@@ -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.
|
|
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
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
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:
|
|
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/
|
|
4710
|
-
|
|
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
|
|
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 (!
|
|
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
|
|
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
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
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 (
|
|
4818
|
-
const entries =
|
|
6141
|
+
if (existsSync12(dir)) {
|
|
6142
|
+
const entries = readdirSync9(dir, { withFileTypes: true });
|
|
4819
6143
|
for (const entry of entries) {
|
|
4820
|
-
const fullPath =
|
|
6144
|
+
const fullPath = join11(dir, entry.name);
|
|
4821
6145
|
if (entry.isSymbolicLink() || entry.isFile()) {
|
|
4822
|
-
|
|
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 (
|
|
4831
|
-
|
|
6154
|
+
if (existsSync12(target)) {
|
|
6155
|
+
unlinkSync2(target);
|
|
4832
6156
|
}
|
|
4833
6157
|
symlinkSync(source, target);
|
|
4834
6158
|
}
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
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 (
|
|
6256
|
+
if (existsSync12(BACKGROUNDS_TARGET_DIR)) {
|
|
4845
6257
|
rmSync(BACKGROUNDS_TARGET_DIR, { recursive: true, force: true });
|
|
4846
6258
|
}
|
|
4847
|
-
const entries =
|
|
6259
|
+
const entries = readdirSync9(themeDir, { withFileTypes: true });
|
|
4848
6260
|
for (const entry of entries) {
|
|
4849
|
-
const source =
|
|
6261
|
+
const source = join11(themeDir, entry.name);
|
|
4850
6262
|
if (entry.isFile() && entry.name !== "theme.yaml" && entry.name !== "light.mode") {
|
|
4851
|
-
const target =
|
|
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 =
|
|
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
|
|
4881
|
-
const
|
|
4882
|
-
|
|
4883
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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-
|
|
4951
|
-
console.log(` formalconf theme <theme-
|
|
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 <
|
|
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-
|
|
6451
|
+
console.log(` formalconf theme --info <theme-id> ${colors5.dim}(show theme details)${colors5.reset}
|
|
4958
6452
|
`);
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
extras.
|
|
4968
|
-
|
|
4969
|
-
|
|
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
|
|
5008
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5088
|
-
await execute(() => runSetTheme(themeName, saveAsDeviceDefault));
|
|
6665
|
+
await execute(() => runSetTheme(theme.identifier, saveAsDeviceDefault));
|
|
5089
6666
|
if (saveAsDeviceDefault) {
|
|
5090
|
-
setDeviceThemeName(
|
|
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: "
|
|
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: "
|
|
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.
|
|
5163
|
-
}, theme.
|
|
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,
|