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