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