formalconf 2.0.7 → 2.0.8

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 +246 -50
  2. package/package.json +1 -1
@@ -317,6 +317,7 @@ var CONFIGS_DIR = join(CONFIG_DIR, "configs");
317
317
  var THEMES_DIR = join(CONFIG_DIR, "themes");
318
318
  var PKG_CONFIG_PATH = join(CONFIG_DIR, "pkg-config.json");
319
319
  var PKG_LOCK_PATH = join(CONFIG_DIR, "pkg-lock.json");
320
+ var THEME_CONFIG_PATH = join(CONFIG_DIR, "theme-config.json");
320
321
  async function ensureDir2(path) {
321
322
  await ensureDir(path);
322
323
  }
@@ -446,7 +447,7 @@ function StatusIndicator({
446
447
  // package.json
447
448
  var package_default = {
448
449
  name: "formalconf",
449
- version: "2.0.7",
450
+ version: "2.0.8",
450
451
  description: "Dotfiles management TUI for macOS - config management, package sync, and theme switching",
451
452
  type: "module",
452
453
  main: "./dist/formalconf.js",
@@ -4502,16 +4503,18 @@ function PackageMenu({ onBack }) {
4502
4503
  // src/components/menus/ThemeMenu.tsx
4503
4504
  import { useState as useState10, useEffect as useEffect6, useMemo as useMemo4 } from "react";
4504
4505
  import { Box as Box16, Text as Text15 } from "ink";
4505
- import { existsSync as existsSync7, readdirSync as readdirSync5 } from "fs";
4506
+ import { existsSync as existsSync8, readdirSync as readdirSync5 } from "fs";
4506
4507
  import { join as join6 } from "path";
4507
4508
 
4508
4509
  // src/components/ThemeCard.tsx
4509
4510
  import { Box as Box15, Text as Text14 } from "ink";
4510
4511
  import { jsxDEV as jsxDEV18 } from "react/jsx-dev-runtime";
4511
- function ThemeCard({ theme, isSelected, width }) {
4512
+ function ThemeCard({ theme, isSelected, width, isDeviceTheme }) {
4512
4513
  const borderColor = isSelected ? colors.accent : colors.border;
4513
4514
  const nameColor = isSelected ? colors.primary : colors.text;
4514
4515
  const indicators = [];
4516
+ if (isDeviceTheme)
4517
+ indicators.push("device");
4515
4518
  if (theme.hasBackgrounds)
4516
4519
  indicators.push("bg");
4517
4520
  if (theme.isLightMode)
@@ -4553,6 +4556,7 @@ function useThemeGrid({
4553
4556
  layoutOverhead = 20,
4554
4557
  minCardWidth = 28,
4555
4558
  onSelect,
4559
+ onSelectAndSave,
4556
4560
  onBack,
4557
4561
  enabled = true
4558
4562
  }) {
@@ -4602,8 +4606,12 @@ function useThemeGrid({
4602
4606
  setSelectedIndex(prevIndex);
4603
4607
  }
4604
4608
  }
4605
- if (key.return && onSelect) {
4606
- onSelect(selectedIndex);
4609
+ if (key.return) {
4610
+ if (key.shift && onSelectAndSave) {
4611
+ onSelectAndSave(selectedIndex);
4612
+ } else if (onSelect) {
4613
+ onSelect(selectedIndex);
4614
+ }
4607
4615
  }
4608
4616
  });
4609
4617
  const visibleStartIndex = scrollOffset * cardsPerRow;
@@ -4700,8 +4708,86 @@ async function parseTheme(themePath, themeName) {
4700
4708
 
4701
4709
  // src/cli/set-theme.ts
4702
4710
  import { parseArgs as parseArgs4 } from "util";
4703
- import { readdirSync as readdirSync4, existsSync as existsSync6, rmSync, symlinkSync, unlinkSync } from "fs";
4711
+ import { readdirSync as readdirSync4, existsSync as existsSync7, rmSync, symlinkSync, unlinkSync } from "fs";
4704
4712
  import { join as join5 } from "path";
4713
+
4714
+ // src/lib/theme-config.ts
4715
+ import { hostname } from "os";
4716
+ import { existsSync as existsSync6, readFileSync, writeFileSync } from "fs";
4717
+ var DEFAULT_CONFIG = {
4718
+ version: 1,
4719
+ defaultTheme: null,
4720
+ devices: {}
4721
+ };
4722
+ function getDeviceHostname() {
4723
+ return hostname();
4724
+ }
4725
+ function loadThemeConfig() {
4726
+ if (!existsSync6(THEME_CONFIG_PATH)) {
4727
+ return { ...DEFAULT_CONFIG, devices: {} };
4728
+ }
4729
+ try {
4730
+ const content = readFileSync(THEME_CONFIG_PATH, "utf-8");
4731
+ const parsed = JSON.parse(content);
4732
+ return {
4733
+ version: parsed.version ?? 1,
4734
+ defaultTheme: parsed.defaultTheme ?? null,
4735
+ devices: parsed.devices ?? {}
4736
+ };
4737
+ } catch {
4738
+ return { ...DEFAULT_CONFIG, devices: {} };
4739
+ }
4740
+ }
4741
+ async function saveThemeConfig(config) {
4742
+ await ensureConfigDir();
4743
+ writeFileSync(THEME_CONFIG_PATH, JSON.stringify(config, null, 2) + `
4744
+ `);
4745
+ }
4746
+ function getDeviceTheme() {
4747
+ const config = loadThemeConfig();
4748
+ const device = getDeviceHostname();
4749
+ const mapping = config.devices[device];
4750
+ if (mapping) {
4751
+ return mapping.theme;
4752
+ }
4753
+ return config.defaultTheme;
4754
+ }
4755
+ async function setDeviceTheme(themeName) {
4756
+ const config = loadThemeConfig();
4757
+ const device = getDeviceHostname();
4758
+ config.devices[device] = {
4759
+ theme: themeName,
4760
+ setAt: new Date().toISOString()
4761
+ };
4762
+ await saveThemeConfig(config);
4763
+ }
4764
+ async function setDefaultTheme(themeName) {
4765
+ const config = loadThemeConfig();
4766
+ config.defaultTheme = themeName;
4767
+ await saveThemeConfig(config);
4768
+ }
4769
+ async function clearDeviceTheme() {
4770
+ const config = loadThemeConfig();
4771
+ const device = getDeviceHostname();
4772
+ delete config.devices[device];
4773
+ await saveThemeConfig(config);
4774
+ }
4775
+ function listDeviceMappings() {
4776
+ const config = loadThemeConfig();
4777
+ const currentDevice = getDeviceHostname();
4778
+ return Object.entries(config.devices).map(([device, mapping]) => ({
4779
+ device,
4780
+ theme: mapping.theme,
4781
+ setAt: mapping.setAt,
4782
+ isCurrent: device === currentDevice
4783
+ }));
4784
+ }
4785
+ function getDefaultTheme() {
4786
+ const config = loadThemeConfig();
4787
+ return config.defaultTheme;
4788
+ }
4789
+
4790
+ // src/cli/set-theme.ts
4705
4791
  var colors5 = {
4706
4792
  red: "\x1B[0;31m",
4707
4793
  green: "\x1B[0;32m",
@@ -4713,7 +4799,7 @@ var colors5 = {
4713
4799
  };
4714
4800
  async function listThemes() {
4715
4801
  await ensureConfigDir();
4716
- if (!existsSync6(THEMES_DIR)) {
4802
+ if (!existsSync7(THEMES_DIR)) {
4717
4803
  return [];
4718
4804
  }
4719
4805
  const entries = readdirSync4(THEMES_DIR, { withFileTypes: true });
@@ -4728,7 +4814,7 @@ async function listThemes() {
4728
4814
  return themes;
4729
4815
  }
4730
4816
  function clearDirectory(dir) {
4731
- if (existsSync6(dir)) {
4817
+ if (existsSync7(dir)) {
4732
4818
  const entries = readdirSync4(dir, { withFileTypes: true });
4733
4819
  for (const entry of entries) {
4734
4820
  const fullPath = join5(dir, entry.name);
@@ -4741,21 +4827,21 @@ function clearDirectory(dir) {
4741
4827
  }
4742
4828
  }
4743
4829
  function createSymlink(source, target) {
4744
- if (existsSync6(target)) {
4830
+ if (existsSync7(target)) {
4745
4831
  unlinkSync(target);
4746
4832
  }
4747
4833
  symlinkSync(source, target);
4748
4834
  }
4749
- async function applyTheme(themeName) {
4835
+ async function applyTheme(themeName, saveMapping = false) {
4750
4836
  const themeDir = join5(THEMES_DIR, themeName);
4751
- if (!existsSync6(themeDir)) {
4837
+ if (!existsSync7(themeDir)) {
4752
4838
  return { output: `Theme '${themeName}' not found`, success: false };
4753
4839
  }
4754
4840
  await ensureConfigDir();
4755
4841
  await ensureDir2(THEME_TARGET_DIR);
4756
4842
  const theme = await parseTheme(themeDir, themeName);
4757
4843
  clearDirectory(THEME_TARGET_DIR);
4758
- if (existsSync6(BACKGROUNDS_TARGET_DIR)) {
4844
+ if (existsSync7(BACKGROUNDS_TARGET_DIR)) {
4759
4845
  rmSync(BACKGROUNDS_TARGET_DIR, { recursive: true, force: true });
4760
4846
  }
4761
4847
  const entries = readdirSync4(themeDir, { withFileTypes: true });
@@ -4770,7 +4856,13 @@ async function applyTheme(themeName) {
4770
4856
  const backgroundsSource = join5(themeDir, "backgrounds");
4771
4857
  createSymlink(backgroundsSource, BACKGROUNDS_TARGET_DIR);
4772
4858
  }
4859
+ if (saveMapping) {
4860
+ await setDeviceTheme(themeName);
4861
+ }
4773
4862
  let output = `Theme '${theme.name}' applied successfully`;
4863
+ if (saveMapping) {
4864
+ output += ` (saved as device preference for '${getDeviceHostname()}')`;
4865
+ }
4774
4866
  if (theme.metadata?.author) {
4775
4867
  output += `
4776
4868
  Author: ${theme.metadata.author}`;
@@ -4787,7 +4879,7 @@ Note: This is a light mode theme`;
4787
4879
  }
4788
4880
  async function showThemeInfo(themeName) {
4789
4881
  const themeDir = join5(THEMES_DIR, themeName);
4790
- if (!existsSync6(themeDir)) {
4882
+ if (!existsSync7(themeDir)) {
4791
4883
  console.error(`${colors5.red}Error: Theme '${themeName}' not found${colors5.reset}`);
4792
4884
  process.exit(1);
4793
4885
  }
@@ -4820,48 +4912,134 @@ ${colors5.green}Has wallpapers${colors5.reset}`);
4820
4912
  console.log(`${colors5.yellow}Light mode theme${colors5.reset}`);
4821
4913
  }
4822
4914
  }
4823
- async function runSetTheme(themeName) {
4824
- return applyTheme(themeName);
4915
+ async function runSetTheme(themeName, saveMapping = false) {
4916
+ return applyTheme(themeName, saveMapping);
4917
+ }
4918
+ function showDeviceMappings() {
4919
+ const mappings = listDeviceMappings();
4920
+ const defaultTheme = getDefaultTheme();
4921
+ const currentDevice = getDeviceHostname();
4922
+ console.log(`${colors5.cyan}Device Theme Mappings${colors5.reset}`);
4923
+ console.log(`Current device: ${colors5.blue}${currentDevice}${colors5.reset}
4924
+ `);
4925
+ if (defaultTheme) {
4926
+ console.log(`Default theme: ${colors5.green}${defaultTheme}${colors5.reset}
4927
+ `);
4928
+ }
4929
+ if (mappings.length === 0) {
4930
+ console.log(`${colors5.dim}No device-specific themes configured.${colors5.reset}`);
4931
+ return;
4932
+ }
4933
+ console.log("Configured devices:");
4934
+ for (const mapping of mappings) {
4935
+ const marker = mapping.isCurrent ? ` ${colors5.green}(current)${colors5.reset}` : "";
4936
+ const date = new Date(mapping.setAt).toLocaleDateString();
4937
+ console.log(` ${colors5.blue}•${colors5.reset} ${mapping.device}${marker}: ${mapping.theme} ${colors5.dim}(set ${date})${colors5.reset}`);
4938
+ }
4939
+ }
4940
+ async function showThemeList() {
4941
+ const themes = await listThemes();
4942
+ const deviceTheme = getDeviceTheme();
4943
+ if (themes.length === 0) {
4944
+ console.log(`${colors5.yellow}No themes available.${colors5.reset}`);
4945
+ console.log(`This system is compatible with omarchy themes.`);
4946
+ console.log(`
4947
+ Add themes to: ${colors5.cyan}~/.config/formalconf/themes/${colors5.reset}`);
4948
+ return;
4949
+ }
4950
+ console.log(`${colors5.cyan}Usage: formalconf theme <theme-name>${colors5.reset}`);
4951
+ console.log(` formalconf theme <theme-name> --save ${colors5.dim}(save as device preference)${colors5.reset}`);
4952
+ console.log(` formalconf theme --apply ${colors5.dim}(apply device's theme)${colors5.reset}`);
4953
+ console.log(` formalconf theme --list-devices ${colors5.dim}(show device mappings)${colors5.reset}`);
4954
+ console.log(` formalconf theme --default <name> ${colors5.dim}(set default theme)${colors5.reset}`);
4955
+ console.log(` formalconf theme --clear-default ${colors5.dim}(remove default theme)${colors5.reset}`);
4956
+ console.log(` formalconf theme --clear ${colors5.dim}(remove device mapping)${colors5.reset}`);
4957
+ console.log(` formalconf theme --info <theme-name> ${colors5.dim}(show theme details)${colors5.reset}
4958
+ `);
4959
+ console.log("Available themes:");
4960
+ for (const theme of themes) {
4961
+ const extras = [];
4962
+ if (theme.hasBackgrounds)
4963
+ extras.push("wallpapers");
4964
+ if (theme.isLightMode)
4965
+ extras.push("light");
4966
+ if (theme.name === deviceTheme)
4967
+ extras.push("device");
4968
+ const suffix = extras.length ? ` ${colors5.dim}(${extras.join(", ")})${colors5.reset}` : "";
4969
+ console.log(` ${colors5.blue}•${colors5.reset} ${theme.name}${suffix}`);
4970
+ }
4825
4971
  }
4826
4972
  async function main4() {
4827
4973
  const { positionals, values } = parseArgs4({
4828
4974
  args: process.argv.slice(2),
4829
4975
  options: {
4830
- info: { type: "boolean", short: "i" }
4976
+ info: { type: "boolean", short: "i" },
4977
+ save: { type: "boolean", short: "s" },
4978
+ apply: { type: "boolean", short: "a" },
4979
+ "list-devices": { type: "boolean", short: "l" },
4980
+ default: { type: "string", short: "d" },
4981
+ "clear-default": { type: "boolean" },
4982
+ clear: { type: "boolean", short: "c" }
4831
4983
  },
4832
4984
  allowPositionals: true
4833
4985
  });
4834
4986
  const [themeName] = positionals;
4835
- if (!themeName) {
4836
- const themes = await listThemes();
4837
- if (themes.length === 0) {
4838
- console.log(`${colors5.yellow}No themes available.${colors5.reset}`);
4839
- console.log(`This system is compatible with omarchy themes.`);
4840
- console.log(`
4841
- Add themes to: ${colors5.cyan}~/.config/formalconf/themes/${colors5.reset}`);
4842
- process.exit(0);
4987
+ if (values["list-devices"]) {
4988
+ showDeviceMappings();
4989
+ return;
4990
+ }
4991
+ if (values.clear) {
4992
+ const deviceTheme = getDeviceTheme();
4993
+ if (!deviceTheme) {
4994
+ console.log(`${colors5.yellow}No theme configured for this device.${colors5.reset}`);
4995
+ return;
4843
4996
  }
4844
- console.log(`${colors5.cyan}Usage: formalconf theme <theme-name>${colors5.reset}`);
4845
- console.log(` formalconf theme --info <theme-name>
4846
- `);
4847
- console.log("Available themes:");
4848
- for (const theme of themes) {
4849
- const extras = [];
4850
- if (theme.hasBackgrounds)
4851
- extras.push("wallpapers");
4852
- if (theme.isLightMode)
4853
- extras.push("light");
4854
- const suffix = extras.length ? ` ${colors5.dim}(${extras.join(", ")})${colors5.reset}` : "";
4855
- console.log(` ${colors5.blue}•${colors5.reset} ${theme.name}${suffix}`);
4997
+ await clearDeviceTheme();
4998
+ console.log(`${colors5.green}Removed theme mapping for '${getDeviceHostname()}'.${colors5.reset}`);
4999
+ return;
5000
+ }
5001
+ if (values["clear-default"]) {
5002
+ await setDefaultTheme(null);
5003
+ console.log(`${colors5.green}Default theme cleared.${colors5.reset}`);
5004
+ return;
5005
+ }
5006
+ if (values.default !== undefined) {
5007
+ const themeDir = join5(THEMES_DIR, values.default);
5008
+ if (!existsSync7(themeDir)) {
5009
+ console.error(`${colors5.red}Error: Theme '${values.default}' not found${colors5.reset}`);
5010
+ process.exit(1);
4856
5011
  }
4857
- process.exit(0);
5012
+ await setDefaultTheme(values.default);
5013
+ console.log(`${colors5.green}Default theme set to '${values.default}'.${colors5.reset}`);
5014
+ return;
5015
+ }
5016
+ if (values.apply) {
5017
+ const deviceTheme = getDeviceTheme();
5018
+ if (!deviceTheme) {
5019
+ console.log(`${colors5.yellow}No theme configured for device '${getDeviceHostname()}'.${colors5.reset}`);
5020
+ console.log(`Use 'formalconf theme <name> --save' to set a device preference.`);
5021
+ return;
5022
+ }
5023
+ const result2 = await applyTheme(deviceTheme);
5024
+ console.log(result2.success ? `${colors5.green}${result2.output}${colors5.reset}` : `${colors5.red}${result2.output}${colors5.reset}`);
5025
+ return;
5026
+ }
5027
+ if (!themeName) {
5028
+ const deviceTheme = getDeviceTheme();
5029
+ if (deviceTheme) {
5030
+ const result2 = await applyTheme(deviceTheme);
5031
+ console.log(result2.success ? `${colors5.green}${result2.output}${colors5.reset}` : `${colors5.red}${result2.output}${colors5.reset}`);
5032
+ } else {
5033
+ await showThemeList();
5034
+ }
5035
+ return;
4858
5036
  }
4859
5037
  if (values.info) {
4860
5038
  await showThemeInfo(themeName);
4861
- } else {
4862
- const result = await applyTheme(themeName);
4863
- console.log(result.success ? `${colors5.green}${result.output}${colors5.reset}` : `${colors5.red}${result.output}${colors5.reset}`);
5039
+ return;
4864
5040
  }
5041
+ const result = await applyTheme(themeName, values.save ?? false);
5042
+ console.log(result.success ? `${colors5.green}${result.output}${colors5.reset}` : `${colors5.red}${result.output}${colors5.reset}`);
4865
5043
  }
4866
5044
  var isMainModule4 = process.argv[1]?.includes("set-theme");
4867
5045
  if (isMainModule4) {
@@ -4873,16 +5051,19 @@ import { jsxDEV as jsxDEV19 } from "react/jsx-dev-runtime";
4873
5051
  function ThemeMenu({ onBack }) {
4874
5052
  const [themes, setThemes] = useState10([]);
4875
5053
  const [loading, setLoading] = useState10(true);
5054
+ const [deviceTheme, setDeviceThemeName] = useState10(null);
4876
5055
  const { state, output, success, isRunning, isResult, execute, reset } = useMenuAction();
5056
+ const hostname2 = getDeviceHostname();
4877
5057
  const grid = useThemeGrid({
4878
5058
  itemCount: themes.length,
4879
- onSelect: (index) => applyTheme2(themes[index]),
5059
+ onSelect: (index) => applyTheme2(themes[index], false),
5060
+ onSelectAndSave: (index) => applyTheme2(themes[index], true),
4880
5061
  onBack,
4881
5062
  enabled: state === "menu" && !loading && themes.length > 0
4882
5063
  });
4883
5064
  useEffect6(() => {
4884
5065
  async function loadThemes() {
4885
- if (!existsSync7(THEMES_DIR)) {
5066
+ if (!existsSync8(THEMES_DIR)) {
4886
5067
  setThemes([]);
4887
5068
  setLoading(false);
4888
5069
  return;
@@ -4897,13 +5078,17 @@ function ThemeMenu({ onBack }) {
4897
5078
  }
4898
5079
  }
4899
5080
  setThemes(loadedThemes);
5081
+ setDeviceThemeName(getDeviceTheme());
4900
5082
  setLoading(false);
4901
5083
  }
4902
5084
  loadThemes();
4903
5085
  }, []);
4904
- const applyTheme2 = async (theme) => {
5086
+ const applyTheme2 = async (theme, saveAsDeviceDefault) => {
4905
5087
  const themeName = theme.path.split("/").pop();
4906
- await execute(() => runSetTheme(themeName));
5088
+ await execute(() => runSetTheme(themeName, saveAsDeviceDefault));
5089
+ if (saveAsDeviceDefault) {
5090
+ setDeviceThemeName(themeName);
5091
+ }
4907
5092
  };
4908
5093
  const visibleThemes = useMemo4(() => {
4909
5094
  return themes.slice(grid.visibleStartIndex, grid.visibleEndIndex);
@@ -4973,7 +5158,8 @@ function ThemeMenu({ onBack }) {
4973
5158
  children: visibleThemes.map((theme, index) => /* @__PURE__ */ jsxDEV19(ThemeCard, {
4974
5159
  theme,
4975
5160
  isSelected: grid.visibleStartIndex + index === grid.selectedIndex,
4976
- width: grid.cardWidth
5161
+ width: grid.cardWidth,
5162
+ isDeviceTheme: theme.name === deviceTheme
4977
5163
  }, theme.path, false, undefined, this))
4978
5164
  }, undefined, false, undefined, this),
4979
5165
  grid.showScrollDown && /* @__PURE__ */ jsxDEV19(Text15, {
@@ -4988,11 +5174,21 @@ function ThemeMenu({ onBack }) {
4988
5174
  }, undefined, true, undefined, this),
4989
5175
  /* @__PURE__ */ jsxDEV19(Box16, {
4990
5176
  marginTop: 1,
4991
- children: /* @__PURE__ */ jsxDEV19(Text15, {
4992
- dimColor: true,
4993
- children: "←→↑↓/hjkl navigate Enter select • Esc back"
4994
- }, undefined, false, undefined, this)
4995
- }, undefined, false, undefined, this)
5177
+ flexDirection: "column",
5178
+ children: [
5179
+ /* @__PURE__ */ jsxDEV19(Text15, {
5180
+ dimColor: true,
5181
+ children: "←→↑↓/hjkl navigate Enter apply • Shift+Enter save as device default • Esc back"
5182
+ }, undefined, false, undefined, this),
5183
+ /* @__PURE__ */ jsxDEV19(Text15, {
5184
+ dimColor: true,
5185
+ children: [
5186
+ "Device: ",
5187
+ hostname2
5188
+ ]
5189
+ }, undefined, true, undefined, this)
5190
+ ]
5191
+ }, undefined, true, undefined, this)
4996
5192
  ]
4997
5193
  }, undefined, true, undefined, this);
4998
5194
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "formalconf",
3
- "version": "2.0.7",
3
+ "version": "2.0.8",
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",