formalconf 2.0.9 → 2.0.11

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.
Files changed (2) hide show
  1. package/dist/formalconf.js +398 -64
  2. package/package.json +1 -1
@@ -199,6 +199,14 @@ async function writeFile(path, content) {
199
199
  }
200
200
  await nodeWriteFile(path, content, "utf-8");
201
201
  }
202
+ async function writeBuffer(path, data) {
203
+ if (isBun) {
204
+ await Bun.write(path, data);
205
+ return;
206
+ }
207
+ const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
208
+ await nodeWriteFile(path, buffer);
209
+ }
202
210
  async function ensureDir(path) {
203
211
  if (isBun) {
204
212
  await Bun.$`mkdir -p ${path}`.quiet();
@@ -459,7 +467,7 @@ function StatusIndicator({
459
467
  // package.json
460
468
  var package_default = {
461
469
  name: "formalconf",
462
- version: "2.0.9",
470
+ version: "2.0.11",
463
471
  description: "Dotfiles management TUI for macOS - config management, package sync, and theme switching",
464
472
  type: "module",
465
473
  main: "./dist/formalconf.js",
@@ -4658,13 +4666,13 @@ function useThemeGrid({
4658
4666
  import { parseArgs as parseArgs4 } from "util";
4659
4667
  import {
4660
4668
  readdirSync as readdirSync9,
4661
- existsSync as existsSync12,
4669
+ existsSync as existsSync13,
4662
4670
  rmSync,
4663
4671
  symlinkSync,
4664
4672
  unlinkSync as unlinkSync2,
4665
4673
  copyFileSync
4666
4674
  } from "fs";
4667
- import { join as join11, basename as basename4 } from "path";
4675
+ import { join as join12, basename as basename4 } from "path";
4668
4676
 
4669
4677
  // src/lib/theme-parser.ts
4670
4678
  init_runtime();
@@ -4776,9 +4784,318 @@ async function runHooks(hookType, env = {}) {
4776
4784
  };
4777
4785
  }
4778
4786
 
4787
+ // src/lib/gtk/colloid.ts
4788
+ import { existsSync as existsSync7 } from "fs";
4789
+ import { join as join6 } from "path";
4790
+ init_runtime();
4791
+
4792
+ // src/lib/gtk/palette.ts
4793
+ function hexToRgb(hex) {
4794
+ const clean = hex.replace("#", "");
4795
+ return {
4796
+ r: parseInt(clean.substring(0, 2), 16),
4797
+ g: parseInt(clean.substring(2, 4), 16),
4798
+ b: parseInt(clean.substring(4, 6), 16)
4799
+ };
4800
+ }
4801
+ function rgbToHex(r, g, b) {
4802
+ const toHex = (n) => Math.round(Math.max(0, Math.min(255, n))).toString(16).padStart(2, "0");
4803
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
4804
+ }
4805
+ function interpolateColor(color1, color2, factor) {
4806
+ const c1 = hexToRgb(color1);
4807
+ const c2 = hexToRgb(color2);
4808
+ return rgbToHex(c1.r + (c2.r - c1.r) * factor, c1.g + (c2.g - c1.g) * factor, c1.b + (c2.b - c1.b) * factor);
4809
+ }
4810
+ function interpolateGreyscale(bg, fg) {
4811
+ const steps = 19;
4812
+ const colors5 = [];
4813
+ for (let i = 0;i < steps; i++) {
4814
+ const factor = i / (steps - 1);
4815
+ colors5.push(interpolateColor(bg, fg, factor));
4816
+ }
4817
+ return colors5;
4818
+ }
4819
+ function lighten(hex, percent) {
4820
+ const { r, g, b } = hexToRgb(hex);
4821
+ const factor = percent / 100;
4822
+ return rgbToHex(r + (255 - r) * factor, g + (255 - g) * factor, b + (255 - b) * factor);
4823
+ }
4824
+ function darken(hex, percent) {
4825
+ const { r, g, b } = hexToRgb(hex);
4826
+ const factor = 1 - percent / 100;
4827
+ return rgbToHex(r * factor, g * factor, b * factor);
4828
+ }
4829
+ function createColloidPalette(palette, mode) {
4830
+ const isDark = mode === "dark";
4831
+ const grey = isDark ? interpolateGreyscale(palette.background, palette.foreground) : interpolateGreyscale(palette.foreground, palette.background);
4832
+ const orderedGrey = isDark ? grey : [...grey].reverse();
4833
+ const accentBase = palette.accent || palette.color4;
4834
+ return {
4835
+ grey: orderedGrey,
4836
+ black: palette.color0,
4837
+ white: palette.color7,
4838
+ red: {
4839
+ dark: darken(palette.color1, 10),
4840
+ light: lighten(palette.color1, 10)
4841
+ },
4842
+ green: {
4843
+ dark: darken(palette.color2, 10),
4844
+ light: lighten(palette.color2, 10)
4845
+ },
4846
+ yellow: {
4847
+ dark: darken(palette.color3, 10),
4848
+ light: lighten(palette.color3, 10)
4849
+ },
4850
+ blue: {
4851
+ dark: darken(palette.color4, 10),
4852
+ light: lighten(palette.color4, 10)
4853
+ },
4854
+ purple: {
4855
+ dark: darken(palette.color5, 10),
4856
+ light: lighten(palette.color5, 10)
4857
+ },
4858
+ teal: {
4859
+ dark: darken(palette.color6, 10),
4860
+ light: lighten(palette.color6, 10)
4861
+ },
4862
+ accent: {
4863
+ dark: darken(accentBase, 10),
4864
+ light: lighten(accentBase, 10)
4865
+ }
4866
+ };
4867
+ }
4868
+ function generateColloidScss(palette, mode) {
4869
+ const colloid = createColloidPalette(palette, mode);
4870
+ const greyNames = [
4871
+ "050",
4872
+ "100",
4873
+ "150",
4874
+ "200",
4875
+ "250",
4876
+ "300",
4877
+ "350",
4878
+ "400",
4879
+ "450",
4880
+ "500",
4881
+ "550",
4882
+ "600",
4883
+ "650",
4884
+ "700",
4885
+ "750",
4886
+ "800",
4887
+ "850",
4888
+ "900",
4889
+ "950"
4890
+ ];
4891
+ let scss = `// FormalConf Generated Color Palette
4892
+ // Auto-generated - do not edit manually
4893
+
4894
+ // Greyscale (interpolated from theme background/foreground)
4895
+ `;
4896
+ for (let i = 0;i < greyNames.length; i++) {
4897
+ scss += `$grey-${greyNames[i]}: ${colloid.grey[i]};
4898
+ `;
4899
+ }
4900
+ scss += `
4901
+ // Base colors
4902
+ $black: ${colloid.black};
4903
+ $white: ${colloid.white};
4904
+
4905
+ // Semantic colors - dark variants
4906
+ $red-dark: ${colloid.red.dark};
4907
+ $green-dark: ${colloid.green.dark};
4908
+ $yellow-dark: ${colloid.yellow.dark};
4909
+ $blue-dark: ${colloid.blue.dark};
4910
+ $purple-dark: ${colloid.purple.dark};
4911
+ $teal-dark: ${colloid.teal.dark};
4912
+
4913
+ // Semantic colors - light variants
4914
+ $red-light: ${colloid.red.light};
4915
+ $green-light: ${colloid.green.light};
4916
+ $yellow-light: ${colloid.yellow.light};
4917
+ $blue-light: ${colloid.blue.light};
4918
+ $purple-light: ${colloid.purple.light};
4919
+ $teal-light: ${colloid.teal.light};
4920
+
4921
+ // Accent colors (primary buttons, links, highlights)
4922
+ $default-dark: ${colloid.accent.dark};
4923
+ $default-light: ${colloid.accent.light};
4924
+ $links: ${palette.accent || palette.color4};
4925
+
4926
+ // Backgrounds
4927
+ $background: ${palette.background};
4928
+ $foreground: ${palette.foreground};
4929
+ `;
4930
+ return scss;
4931
+ }
4932
+
4933
+ // src/lib/gtk/colloid.ts
4934
+ var COLLOID_REPO_URL = "https://github.com/vinceliuice/Colloid-gtk-theme.git";
4935
+ async function checkGtkDependencies() {
4936
+ const [git, sassc] = await Promise.all([
4937
+ commandExists("git"),
4938
+ commandExists("sassc")
4939
+ ]);
4940
+ const missing = [];
4941
+ if (!git)
4942
+ missing.push("git");
4943
+ if (!sassc)
4944
+ missing.push("sassc");
4945
+ return { git, sassc, missing };
4946
+ }
4947
+ async function getDependencyInstructions(missing) {
4948
+ if (missing.length === 0)
4949
+ return "";
4950
+ const distro = await getLinuxDistro();
4951
+ const instructions = [];
4952
+ for (const dep of missing) {
4953
+ let instruction = `Missing: ${dep}
4954
+ `;
4955
+ switch (distro) {
4956
+ case "arch":
4957
+ instruction += ` Install: sudo pacman -S ${dep}`;
4958
+ break;
4959
+ case "debian":
4960
+ case "ubuntu":
4961
+ instruction += ` Install: sudo apt install ${dep}`;
4962
+ break;
4963
+ case "fedora":
4964
+ instruction += ` Install: sudo dnf install ${dep}`;
4965
+ break;
4966
+ case "opensuse":
4967
+ instruction += ` Install: sudo zypper install ${dep}`;
4968
+ break;
4969
+ default:
4970
+ instruction += ` Install via your package manager`;
4971
+ }
4972
+ instructions.push(instruction);
4973
+ }
4974
+ return instructions.join(`
4975
+ `);
4976
+ }
4977
+ async function cloneColloidRepo() {
4978
+ await ensureDir2(GTK_DIR);
4979
+ const result = await exec([
4980
+ "git",
4981
+ "clone",
4982
+ "--depth",
4983
+ "1",
4984
+ COLLOID_REPO_URL,
4985
+ COLLOID_DIR
4986
+ ]);
4987
+ return result.success;
4988
+ }
4989
+ async function updateColloidRepo() {
4990
+ const result = await exec(["git", "pull", "--rebase"], COLLOID_DIR);
4991
+ return result.success;
4992
+ }
4993
+ async function ensureColloidRepo() {
4994
+ if (existsSync7(join6(COLLOID_DIR, ".git"))) {
4995
+ return updateColloidRepo();
4996
+ }
4997
+ return cloneColloidRepo();
4998
+ }
4999
+ async function writeCustomPalette(palette, mode) {
5000
+ const scss = generateColloidScss(palette, mode);
5001
+ const palettePath = join6(COLLOID_DIR, "src", "sass", "_color-palette-formalconf.scss");
5002
+ await writeFile(palettePath, scss);
5003
+ }
5004
+ async function patchTweaksFile(themeName) {
5005
+ const tweaksContent = `// FormalConf theme: ${themeName}
5006
+ // Auto-patched to use custom color palette
5007
+
5008
+ @import 'color-palette-formalconf';
5009
+
5010
+ $tweaks: true;
5011
+ $colorscheme: true;
5012
+ `;
5013
+ const tweaksPath = join6(COLLOID_DIR, "src", "sass", "_tweaks-temp.scss");
5014
+ await writeFile(tweaksPath, tweaksContent);
5015
+ }
5016
+ async function runColloidInstall(options) {
5017
+ const args = [
5018
+ "./install.sh",
5019
+ "-n",
5020
+ `formalconf-${options.themeName}`,
5021
+ "-c",
5022
+ options.mode === "dark" ? "dark" : "light"
5023
+ ];
5024
+ if (options.installLibadwaita !== false) {
5025
+ args.push("-l");
5026
+ }
5027
+ const tweaks = options.tweaks && options.tweaks.length > 0 ? options.tweaks : ["normal"];
5028
+ args.push("--tweaks", ...tweaks);
5029
+ const result = await exec(args, COLLOID_DIR);
5030
+ return result.exitCode;
5031
+ }
5032
+ function getGtkThemeName(themeName, mode) {
5033
+ const modeCapitalized = mode === "dark" ? "Dark" : "Light";
5034
+ return `Colloid-formalconf-${themeName}-${modeCapitalized}`;
5035
+ }
5036
+ async function applyGtkTheme(theme, mode) {
5037
+ if (getOS() !== "linux") {
5038
+ return {
5039
+ success: true,
5040
+ themeName: "",
5041
+ skipped: true,
5042
+ skipReason: "GTK theming only available on Linux"
5043
+ };
5044
+ }
5045
+ const palette = mode === "dark" ? theme.dark : theme.light;
5046
+ if (!palette) {
5047
+ return {
5048
+ success: false,
5049
+ themeName: "",
5050
+ error: `Theme does not have a ${mode} palette`
5051
+ };
5052
+ }
5053
+ const themeName = theme.title.toLowerCase().replace(/\s+/g, "-");
5054
+ const deps = await checkGtkDependencies();
5055
+ if (deps.missing.length > 0) {
5056
+ const instructions = await getDependencyInstructions(deps.missing);
5057
+ return {
5058
+ success: false,
5059
+ themeName: "",
5060
+ error: `Missing dependencies:
5061
+ ${instructions}`
5062
+ };
5063
+ }
5064
+ const repoReady = await ensureColloidRepo();
5065
+ if (!repoReady) {
5066
+ return {
5067
+ success: false,
5068
+ themeName: "",
5069
+ error: "Failed to clone/update Colloid repository"
5070
+ };
5071
+ }
5072
+ await writeCustomPalette(palette, mode);
5073
+ await patchTweaksFile(themeName);
5074
+ const gtkConfig = theme.gtk || {};
5075
+ const options = {
5076
+ themeName,
5077
+ mode,
5078
+ variant: gtkConfig.variant,
5079
+ tweaks: gtkConfig.tweaks,
5080
+ installLibadwaita: true
5081
+ };
5082
+ const exitCode = await runColloidInstall(options);
5083
+ if (exitCode !== 0) {
5084
+ return {
5085
+ success: false,
5086
+ themeName: "",
5087
+ error: `Colloid install.sh failed with exit code ${exitCode}`
5088
+ };
5089
+ }
5090
+ const installedThemeName = getGtkThemeName(themeName, mode);
5091
+ return {
5092
+ success: true,
5093
+ themeName: installedThemeName
5094
+ };
5095
+ }
4779
5096
  // src/lib/theme-config.ts
4780
5097
  import { hostname } from "os";
4781
- import { existsSync as existsSync7, readFileSync, writeFileSync } from "fs";
5098
+ import { existsSync as existsSync8, readFileSync, writeFileSync } from "fs";
4782
5099
  var DEFAULT_CONFIG = {
4783
5100
  version: 1,
4784
5101
  defaultTheme: null,
@@ -4788,7 +5105,7 @@ function getDeviceHostname() {
4788
5105
  return hostname();
4789
5106
  }
4790
5107
  function loadThemeConfig() {
4791
- if (!existsSync7(THEME_CONFIG_PATH)) {
5108
+ if (!existsSync8(THEME_CONFIG_PATH)) {
4792
5109
  return { ...DEFAULT_CONFIG, devices: {} };
4793
5110
  }
4794
5111
  try {
@@ -4854,8 +5171,8 @@ function getDefaultTheme() {
4854
5171
 
4855
5172
  // src/lib/theme-v2/loader.ts
4856
5173
  init_runtime();
4857
- import { existsSync as existsSync8, readdirSync as readdirSync5 } from "fs";
4858
- import { join as join6, basename as basename2 } from "path";
5174
+ import { existsSync as existsSync9, readdirSync as readdirSync5 } from "fs";
5175
+ import { join as join7, basename as basename2 } from "path";
4859
5176
 
4860
5177
  // src/lib/theme-v2/color.ts
4861
5178
  function isValidHex(hex) {
@@ -4873,7 +5190,7 @@ function normalizeHex(hex) {
4873
5190
  }
4874
5191
  return hex.toUpperCase();
4875
5192
  }
4876
- function hexToRgb(hex) {
5193
+ function hexToRgb2(hex) {
4877
5194
  const normalized = normalizeHex(hex);
4878
5195
  if (!isValidHex(normalized)) {
4879
5196
  throw new Error(`Invalid hex color: ${hex}`);
@@ -4885,7 +5202,7 @@ function hexToRgb(hex) {
4885
5202
  }
4886
5203
  function hexToColorVariable(hex) {
4887
5204
  const normalized = normalizeHex(hex);
4888
- const { r, g, b } = hexToRgb(normalized);
5205
+ const { r, g, b } = hexToRgb2(normalized);
4889
5206
  return {
4890
5207
  hex: normalized,
4891
5208
  strip: normalized.slice(1),
@@ -5134,7 +5451,7 @@ ${validationErrors}`);
5134
5451
  }
5135
5452
  }
5136
5453
  async function loadThemeJson(themePath) {
5137
- if (!existsSync8(themePath)) {
5454
+ if (!existsSync9(themePath)) {
5138
5455
  throw new ThemeLoadError(themePath, "file not found");
5139
5456
  }
5140
5457
  let content;
@@ -5164,14 +5481,14 @@ function getAvailableModes(theme) {
5164
5481
  return modes;
5165
5482
  }
5166
5483
  async function listJsonThemes() {
5167
- if (!existsSync8(THEMES_DIR)) {
5484
+ if (!existsSync9(THEMES_DIR)) {
5168
5485
  return [];
5169
5486
  }
5170
5487
  const entries = readdirSync5(THEMES_DIR, { withFileTypes: true });
5171
5488
  const themes = [];
5172
5489
  for (const entry of entries) {
5173
5490
  if (entry.isFile() && entry.name.endsWith(".json")) {
5174
- const path = join6(THEMES_DIR, entry.name);
5491
+ const path = join7(THEMES_DIR, entry.name);
5175
5492
  try {
5176
5493
  const theme = await loadThemeJson(path);
5177
5494
  themes.push({
@@ -5190,7 +5507,7 @@ async function listJsonThemes() {
5190
5507
 
5191
5508
  // src/lib/template-engine/engine.ts
5192
5509
  init_runtime();
5193
- import { join as join8 } from "path";
5510
+ import { join as join9 } from "path";
5194
5511
 
5195
5512
  // src/lib/template-engine/modifiers.ts
5196
5513
  var VALID_MODIFIERS = [
@@ -5323,14 +5640,14 @@ function renderDualModeTemplate(template, contexts) {
5323
5640
 
5324
5641
  // src/lib/template-engine/versioning.ts
5325
5642
  init_runtime();
5326
- import { existsSync as existsSync9, readdirSync as readdirSync6 } from "fs";
5327
- import { join as join7 } from "path";
5643
+ import { existsSync as existsSync10, readdirSync as readdirSync6 } from "fs";
5644
+ import { join as join8 } from "path";
5328
5645
  var DEFAULT_MANIFEST = {
5329
5646
  version: 1,
5330
5647
  templates: {}
5331
5648
  };
5332
5649
  async function loadTemplatesManifest() {
5333
- if (!existsSync9(TEMPLATES_MANIFEST_PATH)) {
5650
+ if (!existsSync10(TEMPLATES_MANIFEST_PATH)) {
5334
5651
  return { ...DEFAULT_MANIFEST };
5335
5652
  }
5336
5653
  try {
@@ -5345,7 +5662,7 @@ async function saveTemplatesManifest(manifest) {
5345
5662
  await writeFile(TEMPLATES_MANIFEST_PATH, JSON.stringify(manifest, null, 2));
5346
5663
  }
5347
5664
  async function loadBundledManifest() {
5348
- if (!existsSync9(BUNDLED_MANIFEST_PATH)) {
5665
+ if (!existsSync10(BUNDLED_MANIFEST_PATH)) {
5349
5666
  return { version: 1, templates: {} };
5350
5667
  }
5351
5668
  try {
@@ -5396,13 +5713,13 @@ async function installTemplate(templateName) {
5396
5713
  if (!bundledMeta) {
5397
5714
  throw new Error(`Template '${templateName}' not found in bundled templates`);
5398
5715
  }
5399
- const sourcePath = join7(BUNDLED_TEMPLATES_DIR, templateName);
5400
- if (!existsSync9(sourcePath)) {
5716
+ const sourcePath = join8(BUNDLED_TEMPLATES_DIR, templateName);
5717
+ if (!existsSync10(sourcePath)) {
5401
5718
  throw new Error(`Template file not found: ${sourcePath}`);
5402
5719
  }
5403
5720
  await ensureDir2(TEMPLATES_DIR);
5404
5721
  const content = await readText(sourcePath);
5405
- const destPath = join7(TEMPLATES_DIR, templateName);
5722
+ const destPath = join8(TEMPLATES_DIR, templateName);
5406
5723
  await writeFile(destPath, content);
5407
5724
  const manifest = await loadTemplatesManifest();
5408
5725
  manifest.templates[templateName] = {
@@ -5464,7 +5781,7 @@ function getOutputFilename(templateName) {
5464
5781
  return output;
5465
5782
  }
5466
5783
  async function listInstalledTemplates() {
5467
- if (!existsSync9(TEMPLATES_DIR)) {
5784
+ if (!existsSync10(TEMPLATES_DIR)) {
5468
5785
  return [];
5469
5786
  }
5470
5787
  const entries = readdirSync6(TEMPLATES_DIR, { withFileTypes: true });
@@ -5473,7 +5790,7 @@ async function listInstalledTemplates() {
5473
5790
  if (entry.isFile() && entry.name.endsWith(".template")) {
5474
5791
  templates.push({
5475
5792
  name: entry.name,
5476
- path: join7(TEMPLATES_DIR, entry.name),
5793
+ path: join8(TEMPLATES_DIR, entry.name),
5477
5794
  outputName: getOutputFilename(entry.name),
5478
5795
  type: getTemplateType(entry.name),
5479
5796
  partialMode: getPartialMode(entry.name)
@@ -5647,7 +5964,7 @@ async function renderTemplateFile(templateFile, theme, mode) {
5647
5964
  return {
5648
5965
  template: templateFile,
5649
5966
  content,
5650
- outputPath: join8(GENERATED_DIR, templateFile.outputName)
5967
+ outputPath: join9(GENERATED_DIR, templateFile.outputName)
5651
5968
  };
5652
5969
  }
5653
5970
  async function renderAllTemplates(theme, mode) {
@@ -5681,7 +5998,7 @@ async function generateNeovimConfigFile(theme, mode) {
5681
5998
  return null;
5682
5999
  }
5683
6000
  const content = generateNeovimConfig(theme, mode);
5684
- const outputPath = join8(GENERATED_DIR, "neovim.lua");
6001
+ const outputPath = join9(GENERATED_DIR, "neovim.lua");
5685
6002
  await writeFile(outputPath, content);
5686
6003
  return {
5687
6004
  template: {
@@ -5706,8 +6023,8 @@ async function generateThemeConfigs(theme, mode) {
5706
6023
 
5707
6024
  // src/lib/migration/extractor.ts
5708
6025
  init_runtime();
5709
- import { existsSync as existsSync10, readdirSync as readdirSync7 } from "fs";
5710
- import { join as join9 } from "path";
6026
+ import { existsSync as existsSync11, readdirSync as readdirSync7 } from "fs";
6027
+ import { join as join10 } from "path";
5711
6028
  function normalizeHex2(hex) {
5712
6029
  hex = hex.replace(/^(#|0x)/i, "");
5713
6030
  if (hex.length === 3) {
@@ -5848,7 +6165,7 @@ async function extractFromGhostty(path) {
5848
6165
  return { colors: colors5, source: "ghostty" };
5849
6166
  }
5850
6167
  async function extractColors(filePath) {
5851
- if (!existsSync10(filePath)) {
6168
+ if (!existsSync11(filePath)) {
5852
6169
  return null;
5853
6170
  }
5854
6171
  const filename = filePath.toLowerCase();
@@ -5870,7 +6187,7 @@ async function extractColors(filePath) {
5870
6187
  return null;
5871
6188
  }
5872
6189
  async function extractFromLegacyTheme(themePath) {
5873
- if (!existsSync10(themePath)) {
6190
+ if (!existsSync11(themePath)) {
5874
6191
  return null;
5875
6192
  }
5876
6193
  const files = readdirSync7(themePath, { withFileTypes: true });
@@ -5882,12 +6199,12 @@ async function extractFromLegacyTheme(themePath) {
5882
6199
  for (const preferred of preferredFiles) {
5883
6200
  const match = files.find((f) => f.name.toLowerCase() === preferred.toLowerCase());
5884
6201
  if (match) {
5885
- return extractColors(join9(themePath, match.name));
6202
+ return extractColors(join10(themePath, match.name));
5886
6203
  }
5887
6204
  }
5888
6205
  for (const file of files) {
5889
6206
  if (file.isFile() && (file.name.endsWith(".conf") || file.name.endsWith(".toml"))) {
5890
- const result = await extractColors(join9(themePath, file.name));
6207
+ const result = await extractColors(join10(themePath, file.name));
5891
6208
  if (result && Object.keys(result.colors).length > 0) {
5892
6209
  return result;
5893
6210
  }
@@ -5976,18 +6293,19 @@ function generateThemeJson(name, colors5, options = {}) {
5976
6293
  init_runtime();
5977
6294
 
5978
6295
  // src/lib/wallpaper.ts
5979
- import { existsSync as existsSync11, readdirSync as readdirSync8, unlinkSync } from "fs";
5980
- import { join as join10 } from "path";
6296
+ import { existsSync as existsSync12, readdirSync as readdirSync8, unlinkSync } from "fs";
6297
+ import { join as join11 } from "path";
6298
+ init_runtime();
5981
6299
  var DEFAULT_TIMEOUT_MS = 30000;
5982
6300
  var MAX_FILE_SIZE = 50 * 1024 * 1024;
5983
6301
  function clearBackgroundsDir() {
5984
- if (!existsSync11(BACKGROUNDS_TARGET_DIR)) {
6302
+ if (!existsSync12(BACKGROUNDS_TARGET_DIR)) {
5985
6303
  return;
5986
6304
  }
5987
6305
  const entries = readdirSync8(BACKGROUNDS_TARGET_DIR, { withFileTypes: true });
5988
6306
  for (const entry of entries) {
5989
6307
  if (entry.isFile() || entry.isSymbolicLink()) {
5990
- unlinkSync(join10(BACKGROUNDS_TARGET_DIR, entry.name));
6308
+ unlinkSync(join11(BACKGROUNDS_TARGET_DIR, entry.name));
5991
6309
  }
5992
6310
  }
5993
6311
  }
@@ -6051,8 +6369,8 @@ async function downloadWallpaper(url, filename, timeoutMs = DEFAULT_TIMEOUT_MS)
6051
6369
  };
6052
6370
  }
6053
6371
  const ext = getExtension(url, contentType);
6054
- const outputPath = join10(BACKGROUNDS_TARGET_DIR, `${filename}.${ext}`);
6055
- await Bun.write(outputPath, arrayBuffer);
6372
+ const outputPath = join11(BACKGROUNDS_TARGET_DIR, `${filename}.${ext}`);
6373
+ await writeBuffer(outputPath, arrayBuffer);
6056
6374
  return { success: true, path: outputPath };
6057
6375
  } catch (err) {
6058
6376
  clearTimeout(timeoutId);
@@ -6108,11 +6426,11 @@ async function listAllThemes() {
6108
6426
  });
6109
6427
  }
6110
6428
  }
6111
- if (existsSync12(THEMES_DIR)) {
6429
+ if (existsSync13(THEMES_DIR)) {
6112
6430
  const entries = readdirSync9(THEMES_DIR, { withFileTypes: true });
6113
6431
  for (const entry of entries) {
6114
6432
  if (entry.isDirectory()) {
6115
- const themePath = join11(THEMES_DIR, entry.name);
6433
+ const themePath = join12(THEMES_DIR, entry.name);
6116
6434
  const theme = await parseTheme(themePath, entry.name);
6117
6435
  themes.push({
6118
6436
  displayName: theme.name,
@@ -6129,10 +6447,10 @@ async function listAllThemes() {
6129
6447
  return themes;
6130
6448
  }
6131
6449
  function clearDirectory(dir) {
6132
- if (existsSync12(dir)) {
6450
+ if (existsSync13(dir)) {
6133
6451
  const entries = readdirSync9(dir, { withFileTypes: true });
6134
6452
  for (const entry of entries) {
6135
- const fullPath = join11(dir, entry.name);
6453
+ const fullPath = join12(dir, entry.name);
6136
6454
  if (entry.isSymbolicLink() || entry.isFile()) {
6137
6455
  unlinkSync2(fullPath);
6138
6456
  } else if (entry.isDirectory()) {
@@ -6142,7 +6460,7 @@ function clearDirectory(dir) {
6142
6460
  }
6143
6461
  }
6144
6462
  function createSymlink(source, target) {
6145
- if (existsSync12(target)) {
6463
+ if (existsSync13(target)) {
6146
6464
  unlinkSync2(target);
6147
6465
  }
6148
6466
  symlinkSync(source, target);
@@ -6166,19 +6484,19 @@ async function applyJsonTheme(themePath, mode, saveMapping, identifier) {
6166
6484
  await installAllTemplates();
6167
6485
  }
6168
6486
  clearDirectory(THEME_TARGET_DIR);
6169
- if (existsSync12(BACKGROUNDS_TARGET_DIR)) {
6487
+ if (existsSync13(BACKGROUNDS_TARGET_DIR)) {
6170
6488
  rmSync(BACKGROUNDS_TARGET_DIR, { recursive: true, force: true });
6171
6489
  }
6172
6490
  const results = await generateThemeConfigs(theme, mode);
6173
- const ghosttyThemesDir = join11(HOME_DIR, ".config", "ghostty", "themes");
6491
+ const ghosttyThemesDir = join12(HOME_DIR, ".config", "ghostty", "themes");
6174
6492
  for (const result of results) {
6175
6493
  const filename = basename4(result.outputPath);
6176
6494
  if (filename === "formalconf-dark" || filename === "formalconf-light") {
6177
6495
  await ensureDir2(ghosttyThemesDir);
6178
- const targetPath2 = join11(ghosttyThemesDir, filename);
6496
+ const targetPath2 = join12(ghosttyThemesDir, filename);
6179
6497
  copyFileSync(result.outputPath, targetPath2);
6180
6498
  }
6181
- const targetPath = join11(THEME_TARGET_DIR, filename);
6499
+ const targetPath = join12(THEME_TARGET_DIR, filename);
6182
6500
  copyFileSync(result.outputPath, targetPath);
6183
6501
  }
6184
6502
  let wallpaperPaths = [];
@@ -6188,6 +6506,10 @@ async function applyJsonTheme(themePath, mode, saveMapping, identifier) {
6188
6506
  wallpaperPaths = wallpaperResult.paths;
6189
6507
  wallpaperErrors = wallpaperResult.errors;
6190
6508
  }
6509
+ let gtkResult = null;
6510
+ if (getOS() === "linux") {
6511
+ gtkResult = await applyGtkTheme(theme, mode);
6512
+ }
6191
6513
  if (saveMapping) {
6192
6514
  await setDeviceTheme(identifier);
6193
6515
  }
@@ -6214,6 +6536,15 @@ Wallpapers (${wallpaperPaths.length}):`;
6214
6536
  output += `
6215
6537
  Warning: ${error}`;
6216
6538
  }
6539
+ if (gtkResult && !gtkResult.skipped) {
6540
+ if (gtkResult.success) {
6541
+ output += `
6542
+ GTK theme: ${gtkResult.themeName}`;
6543
+ } else {
6544
+ output += `
6545
+ Warning: GTK theme failed - ${gtkResult.error}`;
6546
+ }
6547
+ }
6217
6548
  const hookEnv = {
6218
6549
  FORMALCONF_THEME: identifier,
6219
6550
  FORMALCONF_THEME_MODE: mode,
@@ -6222,6 +6553,9 @@ Wallpapers (${wallpaperPaths.length}):`;
6222
6553
  if (wallpaperPaths.length > 0) {
6223
6554
  hookEnv.FORMALCONF_WALLPAPER_PATHS = wallpaperPaths.join(":");
6224
6555
  }
6556
+ if (gtkResult?.success && gtkResult.themeName) {
6557
+ hookEnv.FORMALCONF_GTK_THEME = gtkResult.themeName;
6558
+ }
6225
6559
  const hookSummary = await runHooks("theme-change", hookEnv);
6226
6560
  if (hookSummary.executed > 0) {
6227
6561
  output += `
@@ -6236,27 +6570,27 @@ Hooks: ${hookSummary.succeeded}/${hookSummary.executed} succeeded`;
6236
6570
  return { output, success: true };
6237
6571
  }
6238
6572
  async function applyLegacyTheme(themeName, saveMapping) {
6239
- const themeDir = join11(THEMES_DIR, themeName);
6240
- if (!existsSync12(themeDir)) {
6573
+ const themeDir = join12(THEMES_DIR, themeName);
6574
+ if (!existsSync13(themeDir)) {
6241
6575
  return { output: `Theme '${themeName}' not found`, success: false };
6242
6576
  }
6243
6577
  await ensureConfigDir();
6244
6578
  await ensureDir2(THEME_TARGET_DIR);
6245
6579
  const theme = await parseTheme(themeDir, themeName);
6246
6580
  clearDirectory(THEME_TARGET_DIR);
6247
- if (existsSync12(BACKGROUNDS_TARGET_DIR)) {
6581
+ if (existsSync13(BACKGROUNDS_TARGET_DIR)) {
6248
6582
  rmSync(BACKGROUNDS_TARGET_DIR, { recursive: true, force: true });
6249
6583
  }
6250
6584
  const entries = readdirSync9(themeDir, { withFileTypes: true });
6251
6585
  for (const entry of entries) {
6252
- const source = join11(themeDir, entry.name);
6586
+ const source = join12(themeDir, entry.name);
6253
6587
  if (entry.isFile() && entry.name !== "theme.yaml" && entry.name !== "light.mode") {
6254
- const target = join11(THEME_TARGET_DIR, entry.name);
6588
+ const target = join12(THEME_TARGET_DIR, entry.name);
6255
6589
  createSymlink(source, target);
6256
6590
  }
6257
6591
  }
6258
6592
  if (theme.hasBackgrounds) {
6259
- const backgroundsSource = join11(themeDir, "backgrounds");
6593
+ const backgroundsSource = join12(themeDir, "backgrounds");
6260
6594
  createSymlink(backgroundsSource, BACKGROUNDS_TARGET_DIR);
6261
6595
  }
6262
6596
  if (saveMapping) {
@@ -6296,12 +6630,12 @@ Hooks: ${hookSummary.succeeded}/${hookSummary.executed} succeeded`;
6296
6630
  }
6297
6631
  async function applyTheme(themeIdentifier, saveMapping = false) {
6298
6632
  const { name, mode } = parseThemeIdentifier(themeIdentifier);
6299
- const jsonPath = join11(THEMES_DIR, `${name}.json`);
6300
- if (existsSync12(jsonPath) && mode) {
6633
+ const jsonPath = join12(THEMES_DIR, `${name}.json`);
6634
+ if (existsSync13(jsonPath) && mode) {
6301
6635
  return applyJsonTheme(jsonPath, mode, saveMapping, themeIdentifier);
6302
6636
  }
6303
- const legacyPath = join11(THEMES_DIR, name);
6304
- if (existsSync12(legacyPath)) {
6637
+ const legacyPath = join12(THEMES_DIR, name);
6638
+ if (existsSync13(legacyPath)) {
6305
6639
  return applyLegacyTheme(name, saveMapping);
6306
6640
  }
6307
6641
  const allThemes = await listAllThemes();
@@ -6320,8 +6654,8 @@ Did you mean:`;
6320
6654
  }
6321
6655
  async function showThemeInfo(themeIdentifier) {
6322
6656
  const { name, mode } = parseThemeIdentifier(themeIdentifier);
6323
- const jsonPath = join11(THEMES_DIR, `${name}.json`);
6324
- if (existsSync12(jsonPath)) {
6657
+ const jsonPath = join12(THEMES_DIR, `${name}.json`);
6658
+ if (existsSync13(jsonPath)) {
6325
6659
  const theme2 = await loadThemeJson(jsonPath);
6326
6660
  const modes = getAvailableModes(theme2);
6327
6661
  console.log(`
@@ -6361,8 +6695,8 @@ ${colors5.green}Wallpapers:${colors5.reset}`);
6361
6695
  }
6362
6696
  return;
6363
6697
  }
6364
- const themeDir = join11(THEMES_DIR, name);
6365
- if (!existsSync12(themeDir)) {
6698
+ const themeDir = join12(THEMES_DIR, name);
6699
+ if (!existsSync13(themeDir)) {
6366
6700
  console.error(`${colors5.red}Error: Theme '${themeIdentifier}' not found${colors5.reset}`);
6367
6701
  process.exit(1);
6368
6702
  }
@@ -6471,8 +6805,8 @@ To add themes:`);
6471
6805
  }
6472
6806
  }
6473
6807
  async function migrateTheme(themeName) {
6474
- const legacyPath = join11(THEMES_DIR, themeName);
6475
- if (!existsSync12(legacyPath)) {
6808
+ const legacyPath = join12(THEMES_DIR, themeName);
6809
+ if (!existsSync13(legacyPath)) {
6476
6810
  console.error(`${colors5.red}Error: Legacy theme '${themeName}' not found${colors5.reset}`);
6477
6811
  process.exit(1);
6478
6812
  }
@@ -6489,13 +6823,13 @@ async function migrateTheme(themeName) {
6489
6823
  console.log(`${colors5.yellow}Warning: Missing colors will be filled with defaults:${colors5.reset}`);
6490
6824
  console.log(` ${missing.join(", ")}`);
6491
6825
  }
6492
- const isLight = existsSync12(join11(legacyPath, "light.mode"));
6826
+ const isLight = existsSync13(join12(legacyPath, "light.mode"));
6493
6827
  const themeJson = generateThemeJson(themeName, result.colors, {
6494
6828
  description: `Migrated from legacy theme`,
6495
6829
  isLight
6496
6830
  });
6497
- const outputPath = join11(THEMES_DIR, `${themeName}.json`);
6498
- if (existsSync12(outputPath)) {
6831
+ const outputPath = join12(THEMES_DIR, `${themeName}.json`);
6832
+ if (existsSync13(outputPath)) {
6499
6833
  console.error(`${colors5.red}Error: JSON theme '${themeName}.json' already exists${colors5.reset}`);
6500
6834
  console.error(`Delete or rename it first, then try again.`);
6501
6835
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "formalconf",
3
- "version": "2.0.9",
3
+ "version": "2.0.11",
4
4
  "description": "Dotfiles management TUI for macOS - config management, package sync, and theme switching",
5
5
  "type": "module",
6
6
  "main": "./dist/formalconf.js",