formalconf 2.0.1 → 2.0.3

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 (3) hide show
  1. package/LICENSE +21 -0
  2. package/dist/formalconf.js +1444 -865
  3. package/package.json +1 -1
@@ -1,78 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  // src/cli/formalconf.tsx
3
- import { useState as useState5, useEffect as useEffect4, useMemo as useMemo2, useCallback, useRef } from "react";
4
- import { render, Box as Box12, Text as Text11, useApp, useInput as useInput5 } from "ink";
5
- import { Spinner } from "@inkjs/ui";
6
-
7
- // src/components/ui/VimSelect.tsx
8
- import { useState } from "react";
9
- import { Box, Text, useInput } from "ink";
10
-
11
- // src/lib/theme.ts
12
- var colors = {
13
- primary: "#5eead4",
14
- primaryDim: "#2dd4bf",
15
- accent: "#06b6d4",
16
- success: "#22c55e",
17
- error: "#ef4444",
18
- warning: "#f59e0b",
19
- info: "#3b82f6",
20
- text: "white",
21
- textDim: "gray",
22
- border: "#374151",
23
- borderLight: "#4b5563"
24
- };
25
- var borderStyles = {
26
- panel: "round",
27
- header: "round",
28
- footer: "single"
29
- };
30
-
31
- // src/components/ui/VimSelect.tsx
32
- import { jsxDEV } from "react/jsx-dev-runtime";
33
- function VimSelect({ options, onChange, isDisabled = false }) {
34
- const [index, setIndex] = useState(0);
35
- useInput((input, key) => {
36
- if (isDisabled)
37
- return;
38
- if (input === "j" || key.downArrow) {
39
- setIndex((i) => i < options.length - 1 ? i + 1 : i);
40
- }
41
- if (input === "k" || key.upArrow) {
42
- setIndex((i) => i > 0 ? i - 1 : i);
43
- }
44
- if (input === "l" || key.return) {
45
- onChange(options[index].value);
46
- }
47
- });
48
- return /* @__PURE__ */ jsxDEV(Box, {
49
- flexDirection: "column",
50
- children: options.map((opt, i) => /* @__PURE__ */ jsxDEV(Box, {
51
- children: /* @__PURE__ */ jsxDEV(Text, {
52
- color: i === index ? colors.primary : undefined,
53
- children: [
54
- i === index ? "❯" : " ",
55
- " ",
56
- opt.label
57
- ]
58
- }, undefined, true, undefined, this)
59
- }, opt.value, false, undefined, this))
60
- }, undefined, false, undefined, this);
61
- }
62
-
63
- // src/cli/formalconf.tsx
64
- import { readdirSync as readdirSync5, existsSync as existsSync6 } from "fs";
65
- import { join as join5 } from "path";
3
+ import { useState as useState10, useEffect as useEffect6 } from "react";
4
+ import { render, useApp as useApp2, useInput as useInput10 } from "ink";
5
+ import { Spinner as Spinner2 } from "@inkjs/ui";
66
6
 
67
7
  // src/components/layout/Layout.tsx
68
- import { Box as Box6 } from "ink";
8
+ import { Box as Box5 } from "ink";
69
9
 
70
10
  // src/hooks/useTerminalSize.ts
71
11
  import { useStdout } from "ink";
72
- import { useState as useState2, useEffect } from "react";
12
+ import { useState, useEffect } from "react";
73
13
  function useTerminalSize() {
74
14
  const { stdout } = useStdout();
75
- const [size, setSize] = useState2({
15
+ const [size, setSize] = useState({
76
16
  columns: stdout.columns || 80,
77
17
  rows: stdout.rows || 24
78
18
  });
@@ -92,15 +32,16 @@ function useTerminalSize() {
92
32
  }
93
33
 
94
34
  // src/components/Header.tsx
95
- import { Box as Box3, Text as Text3 } from "ink";
35
+ import { Box as Box2, Text as Text2 } from "ink";
96
36
 
97
37
  // src/hooks/useSystemStatus.ts
98
- import { useState as useState3, useEffect as useEffect2 } from "react";
38
+ import { useState as useState2, useEffect as useEffect2 } from "react";
99
39
  import { existsSync, readlinkSync, readdirSync, lstatSync } from "fs";
100
40
 
101
41
  // src/lib/paths.ts
102
42
  import { homedir } from "os";
103
43
  import { join } from "path";
44
+ import { readdir } from "fs/promises";
104
45
 
105
46
  // src/lib/runtime.ts
106
47
  import { spawn as nodeSpawn } from "child_process";
@@ -354,11 +295,24 @@ async function ensureConfigDir() {
354
295
  await ensureDir2(THEME_TARGET_DIR);
355
296
  await ensureDir2(BACKGROUNDS_TARGET_DIR);
356
297
  }
298
+ async function dirHasContents(path) {
299
+ try {
300
+ const entries = await readdir(path);
301
+ return entries.length > 0;
302
+ } catch {
303
+ return false;
304
+ }
305
+ }
306
+ async function isFirstRun() {
307
+ const configsExist = await dirHasContents(CONFIGS_DIR);
308
+ const themesExist = await dirHasContents(THEMES_DIR);
309
+ return !configsExist && !themesExist;
310
+ }
357
311
 
358
312
  // src/hooks/useSystemStatus.ts
359
313
  import { basename, dirname as dirname2, join as join2 } from "path";
360
314
  function useSystemStatus() {
361
- const [status, setStatus] = useState3({
315
+ const [status, setStatus] = useState2({
362
316
  currentTheme: null,
363
317
  configsLinked: false,
364
318
  loading: true
@@ -395,8 +349,30 @@ function useSystemStatus() {
395
349
  }
396
350
 
397
351
  // src/components/ui/StatusIndicator.tsx
398
- import { Box as Box2, Text as Text2 } from "ink";
399
- import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
352
+ import { Box, Text } from "ink";
353
+
354
+ // src/lib/theme.ts
355
+ var colors = {
356
+ primary: "#5eead4",
357
+ primaryDim: "#2dd4bf",
358
+ accent: "#06b6d4",
359
+ success: "#22c55e",
360
+ error: "#ef4444",
361
+ warning: "#f59e0b",
362
+ info: "#3b82f6",
363
+ text: "white",
364
+ textDim: "gray",
365
+ border: "#374151",
366
+ borderLight: "#4b5563"
367
+ };
368
+ var borderStyles = {
369
+ panel: "round",
370
+ header: "round",
371
+ footer: "single"
372
+ };
373
+
374
+ // src/components/ui/StatusIndicator.tsx
375
+ import { jsxDEV } from "react/jsx-dev-runtime";
400
376
  function StatusIndicator({
401
377
  label,
402
378
  value,
@@ -414,17 +390,17 @@ function StatusIndicator({
414
390
  error: "●",
415
391
  neutral: "○"
416
392
  };
417
- return /* @__PURE__ */ jsxDEV2(Box2, {
393
+ return /* @__PURE__ */ jsxDEV(Box, {
418
394
  gap: 1,
419
395
  children: [
420
- /* @__PURE__ */ jsxDEV2(Text2, {
396
+ /* @__PURE__ */ jsxDEV(Text, {
421
397
  dimColor: true,
422
398
  children: [
423
399
  label,
424
400
  ":"
425
401
  ]
426
402
  }, undefined, true, undefined, this),
427
- /* @__PURE__ */ jsxDEV2(Text2, {
403
+ /* @__PURE__ */ jsxDEV(Text, {
428
404
  color: statusColors[status],
429
405
  children: [
430
406
  icon[status],
@@ -438,7 +414,7 @@ function StatusIndicator({
438
414
  // package.json
439
415
  var package_default = {
440
416
  name: "formalconf",
441
- version: "2.0.1",
417
+ version: "2.0.3",
442
418
  description: "Dotfiles management TUI for macOS - config management, package sync, and theme switching",
443
419
  type: "module",
444
420
  main: "./dist/formalconf.js",
@@ -491,11 +467,11 @@ var package_default = {
491
467
  };
492
468
 
493
469
  // src/components/Header.tsx
494
- import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
470
+ import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
495
471
  function Header() {
496
472
  const { columns } = useTerminalSize();
497
473
  const { currentTheme, configsLinked, loading } = useSystemStatus();
498
- return /* @__PURE__ */ jsxDEV3(Box3, {
474
+ return /* @__PURE__ */ jsxDEV2(Box2, {
499
475
  flexDirection: "column",
500
476
  width: columns - 2,
501
477
  borderStyle: borderStyles.header,
@@ -503,24 +479,24 @@ function Header() {
503
479
  paddingX: 2,
504
480
  marginBottom: 1,
505
481
  children: [
506
- /* @__PURE__ */ jsxDEV3(Box3, {
482
+ /* @__PURE__ */ jsxDEV2(Box2, {
507
483
  justifyContent: "space-between",
508
484
  width: "100%",
509
485
  children: [
510
- /* @__PURE__ */ jsxDEV3(Box3, {
486
+ /* @__PURE__ */ jsxDEV2(Box2, {
511
487
  children: [
512
- /* @__PURE__ */ jsxDEV3(Text3, {
488
+ /* @__PURE__ */ jsxDEV2(Text2, {
513
489
  bold: true,
514
490
  color: colors.primary,
515
491
  children: "FormalConf"
516
492
  }, undefined, false, undefined, this),
517
- /* @__PURE__ */ jsxDEV3(Text3, {
493
+ /* @__PURE__ */ jsxDEV2(Text2, {
518
494
  dimColor: true,
519
495
  children: " - Dotfiles Manager"
520
496
  }, undefined, false, undefined, this)
521
497
  ]
522
498
  }, undefined, true, undefined, this),
523
- /* @__PURE__ */ jsxDEV3(Text3, {
499
+ /* @__PURE__ */ jsxDEV2(Text2, {
524
500
  dimColor: true,
525
501
  children: [
526
502
  "v",
@@ -529,16 +505,16 @@ function Header() {
529
505
  }, undefined, true, undefined, this)
530
506
  ]
531
507
  }, undefined, true, undefined, this),
532
- !loading && /* @__PURE__ */ jsxDEV3(Box3, {
508
+ !loading && /* @__PURE__ */ jsxDEV2(Box2, {
533
509
  marginTop: 1,
534
510
  gap: 4,
535
511
  children: [
536
- /* @__PURE__ */ jsxDEV3(StatusIndicator, {
512
+ /* @__PURE__ */ jsxDEV2(StatusIndicator, {
537
513
  label: "Theme",
538
514
  value: currentTheme,
539
515
  status: currentTheme ? "success" : "neutral"
540
516
  }, undefined, false, undefined, this),
541
- /* @__PURE__ */ jsxDEV3(StatusIndicator, {
517
+ /* @__PURE__ */ jsxDEV2(StatusIndicator, {
542
518
  label: "Configs",
543
519
  value: configsLinked ? "Linked" : "Not linked",
544
520
  status: configsLinked ? "success" : "warning"
@@ -550,8 +526,8 @@ function Header() {
550
526
  }
551
527
 
552
528
  // src/components/layout/Footer.tsx
553
- import { Box as Box4, Text as Text4 } from "ink";
554
- import { jsxDEV as jsxDEV4 } from "react/jsx-dev-runtime";
529
+ import { Box as Box3, Text as Text3 } from "ink";
530
+ import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
555
531
  var defaultShortcuts = [
556
532
  { key: "↑↓/jk", label: "Navigate" },
557
533
  { key: "Enter/l", label: "Select" },
@@ -560,7 +536,7 @@ var defaultShortcuts = [
560
536
  ];
561
537
  function Footer({ shortcuts = defaultShortcuts }) {
562
538
  const { columns } = useTerminalSize();
563
- return /* @__PURE__ */ jsxDEV4(Box4, {
539
+ return /* @__PURE__ */ jsxDEV3(Box3, {
564
540
  width: columns - 2,
565
541
  borderStyle: borderStyles.footer,
566
542
  borderColor: colors.border,
@@ -568,15 +544,15 @@ function Footer({ shortcuts = defaultShortcuts }) {
568
544
  marginTop: 1,
569
545
  justifyContent: "center",
570
546
  gap: 2,
571
- children: shortcuts.map((shortcut, index) => /* @__PURE__ */ jsxDEV4(Box4, {
547
+ children: shortcuts.map((shortcut, index) => /* @__PURE__ */ jsxDEV3(Box3, {
572
548
  gap: 1,
573
549
  children: [
574
- /* @__PURE__ */ jsxDEV4(Text4, {
550
+ /* @__PURE__ */ jsxDEV3(Text3, {
575
551
  color: colors.primary,
576
552
  bold: true,
577
553
  children: shortcut.key
578
554
  }, undefined, false, undefined, this),
579
- /* @__PURE__ */ jsxDEV4(Text4, {
555
+ /* @__PURE__ */ jsxDEV3(Text3, {
580
556
  dimColor: true,
581
557
  children: shortcut.label
582
558
  }, undefined, false, undefined, this)
@@ -586,14 +562,14 @@ function Footer({ shortcuts = defaultShortcuts }) {
586
562
  }
587
563
 
588
564
  // src/components/layout/Breadcrumb.tsx
589
- import React2 from "react";
590
- import { Box as Box5, Text as Text5 } from "ink";
591
- import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
565
+ import React from "react";
566
+ import { Box as Box4, Text as Text4 } from "ink";
567
+ import { jsxDEV as jsxDEV4 } from "react/jsx-dev-runtime";
592
568
  function Breadcrumb({ path }) {
593
- return /* @__PURE__ */ jsxDEV5(Box5, {
594
- children: path.map((segment, index) => /* @__PURE__ */ jsxDEV5(React2.Fragment, {
569
+ return /* @__PURE__ */ jsxDEV4(Box4, {
570
+ children: path.map((segment, index) => /* @__PURE__ */ jsxDEV4(React.Fragment, {
595
571
  children: [
596
- index > 0 && /* @__PURE__ */ jsxDEV5(Text5, {
572
+ index > 0 && /* @__PURE__ */ jsxDEV4(Text4, {
597
573
  color: colors.textDim,
598
574
  children: [
599
575
  " ",
@@ -601,7 +577,7 @@ function Breadcrumb({ path }) {
601
577
  " "
602
578
  ]
603
579
  }, undefined, true, undefined, this),
604
- /* @__PURE__ */ jsxDEV5(Text5, {
580
+ /* @__PURE__ */ jsxDEV4(Text4, {
605
581
  color: index === path.length - 1 ? colors.primary : colors.textDim,
606
582
  bold: index === path.length - 1,
607
583
  children: segment
@@ -612,39 +588,39 @@ function Breadcrumb({ path }) {
612
588
  }
613
589
 
614
590
  // src/components/layout/Layout.tsx
615
- import { jsxDEV as jsxDEV6 } from "react/jsx-dev-runtime";
591
+ import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
616
592
  function Layout({
617
593
  children,
618
594
  breadcrumb = ["Main"],
619
595
  showFooter = true
620
596
  }) {
621
597
  const { columns } = useTerminalSize();
622
- return /* @__PURE__ */ jsxDEV6(Box6, {
598
+ return /* @__PURE__ */ jsxDEV5(Box5, {
623
599
  flexDirection: "column",
624
600
  width: columns,
625
601
  padding: 1,
626
602
  children: [
627
- /* @__PURE__ */ jsxDEV6(Header, {}, undefined, false, undefined, this),
628
- breadcrumb.length > 1 && /* @__PURE__ */ jsxDEV6(Box6, {
603
+ /* @__PURE__ */ jsxDEV5(Header, {}, undefined, false, undefined, this),
604
+ breadcrumb.length > 1 && /* @__PURE__ */ jsxDEV5(Box5, {
629
605
  marginBottom: 1,
630
606
  marginLeft: 1,
631
- children: /* @__PURE__ */ jsxDEV6(Breadcrumb, {
607
+ children: /* @__PURE__ */ jsxDEV5(Breadcrumb, {
632
608
  path: breadcrumb
633
609
  }, undefined, false, undefined, this)
634
610
  }, undefined, false, undefined, this),
635
- /* @__PURE__ */ jsxDEV6(Box6, {
611
+ /* @__PURE__ */ jsxDEV5(Box5, {
636
612
  flexDirection: "column",
637
613
  flexGrow: 1,
638
614
  children
639
615
  }, undefined, false, undefined, this),
640
- showFooter && /* @__PURE__ */ jsxDEV6(Footer, {}, undefined, false, undefined, this)
616
+ showFooter && /* @__PURE__ */ jsxDEV5(Footer, {}, undefined, false, undefined, this)
641
617
  ]
642
618
  }, undefined, true, undefined, this);
643
619
  }
644
620
 
645
621
  // src/components/layout/Panel.tsx
646
- import { Box as Box7, Text as Text6 } from "ink";
647
- import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
622
+ import { Box as Box6, Text as Text5 } from "ink";
623
+ import { jsxDEV as jsxDEV6 } from "react/jsx-dev-runtime";
648
624
  function Panel({
649
625
  title,
650
626
  children,
@@ -652,7 +628,7 @@ function Panel({
652
628
  flexGrow,
653
629
  borderColor = colors.border
654
630
  }) {
655
- return /* @__PURE__ */ jsxDEV7(Box7, {
631
+ return /* @__PURE__ */ jsxDEV6(Box6, {
656
632
  flexDirection: "column",
657
633
  width,
658
634
  flexGrow,
@@ -660,9 +636,9 @@ function Panel({
660
636
  borderColor,
661
637
  paddingX: 1,
662
638
  children: [
663
- title && /* @__PURE__ */ jsxDEV7(Box7, {
639
+ title && /* @__PURE__ */ jsxDEV6(Box6, {
664
640
  marginBottom: 1,
665
- children: /* @__PURE__ */ jsxDEV7(Text6, {
641
+ children: /* @__PURE__ */ jsxDEV6(Text5, {
666
642
  bold: true,
667
643
  color: colors.primary,
668
644
  children: title
@@ -673,281 +649,696 @@ function Panel({
673
649
  }, undefined, true, undefined, this);
674
650
  }
675
651
 
676
- // src/components/CommandOutput.tsx
677
- import { Box as Box8, Text as Text7, useInput as useInput2 } from "ink";
678
- import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
679
- function CommandOutput({
680
- title,
681
- output,
682
- success = true,
683
- onDismiss
684
- }) {
685
- useInput2(() => {
686
- onDismiss();
687
- });
688
- return /* @__PURE__ */ jsxDEV8(Panel, {
689
- title,
690
- borderColor: success ? colors.success : colors.error,
691
- children: [
692
- output && /* @__PURE__ */ jsxDEV8(Box8, {
693
- flexDirection: "column",
694
- marginBottom: 1,
695
- children: /* @__PURE__ */ jsxDEV8(Text7, {
696
- children: output
697
- }, undefined, false, undefined, this)
698
- }, undefined, false, undefined, this),
699
- /* @__PURE__ */ jsxDEV8(Text7, {
700
- color: success ? colors.success : colors.error,
701
- children: success ? "Done" : "Failed"
702
- }, undefined, false, undefined, this),
703
- /* @__PURE__ */ jsxDEV8(Text7, {
704
- dimColor: true,
705
- children: "Press any key to continue..."
706
- }, undefined, false, undefined, this)
707
- ]
708
- }, undefined, true, undefined, this);
709
- }
710
-
711
- // src/components/ThemeCard.tsx
712
- import { Box as Box9, Text as Text8 } from "ink";
713
- import { jsxDEV as jsxDEV9 } from "react/jsx-dev-runtime";
714
- function ThemeCard({ theme, isSelected, width }) {
715
- const borderColor = isSelected ? colors.accent : colors.border;
716
- const nameColor = isSelected ? colors.primary : colors.text;
717
- const indicators = [];
718
- if (theme.hasBackgrounds)
719
- indicators.push("bg");
720
- if (theme.isLightMode)
721
- indicators.push("light");
722
- const indicatorText = indicators.length > 0 ? ` [${indicators.join(" ")}]` : "";
723
- return /* @__PURE__ */ jsxDEV9(Box9, {
724
- flexDirection: "column",
725
- width,
726
- borderStyle: borderStyles.panel,
727
- borderColor,
728
- paddingX: 1,
729
- children: /* @__PURE__ */ jsxDEV9(Box9, {
652
+ // src/components/PrerequisiteError.tsx
653
+ import { Box as Box7, Text as Text6, useInput } from "ink";
654
+ import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
655
+ function PrerequisiteError({ missing, onExit }) {
656
+ useInput(() => onExit());
657
+ return /* @__PURE__ */ jsxDEV7(Layout, {
658
+ breadcrumb: ["Error"],
659
+ children: /* @__PURE__ */ jsxDEV7(Panel, {
660
+ title: "Missing Prerequisites",
661
+ borderColor: colors.error,
730
662
  children: [
731
- /* @__PURE__ */ jsxDEV9(Text8, {
732
- color: isSelected ? colors.accent : colors.primaryDim,
733
- children: isSelected ? "● " : " "
663
+ /* @__PURE__ */ jsxDEV7(Text6, {
664
+ color: colors.error,
665
+ children: "Required tools are not installed:"
734
666
  }, undefined, false, undefined, this),
735
- /* @__PURE__ */ jsxDEV9(Text8, {
736
- color: nameColor,
737
- bold: true,
738
- wrap: "truncate",
739
- children: theme.name
667
+ /* @__PURE__ */ jsxDEV7(Box7, {
668
+ flexDirection: "column",
669
+ marginTop: 1,
670
+ children: missing.map((dep) => /* @__PURE__ */ jsxDEV7(Box7, {
671
+ children: [
672
+ /* @__PURE__ */ jsxDEV7(Text6, {
673
+ color: colors.warning,
674
+ children: [
675
+ "• ",
676
+ dep.name
677
+ ]
678
+ }, undefined, true, undefined, this),
679
+ /* @__PURE__ */ jsxDEV7(Text6, {
680
+ dimColor: true,
681
+ children: [
682
+ " — Install: ",
683
+ dep.install
684
+ ]
685
+ }, undefined, true, undefined, this)
686
+ ]
687
+ }, dep.name, true, undefined, this))
740
688
  }, undefined, false, undefined, this),
741
- /* @__PURE__ */ jsxDEV9(Text8, {
742
- color: colors.primaryDim,
743
- children: indicatorText
689
+ /* @__PURE__ */ jsxDEV7(Box7, {
690
+ marginTop: 1,
691
+ children: /* @__PURE__ */ jsxDEV7(Text6, {
692
+ dimColor: true,
693
+ children: "Press any key to exit..."
694
+ }, undefined, false, undefined, this)
744
695
  }, undefined, false, undefined, this)
745
696
  ]
746
697
  }, undefined, true, undefined, this)
747
698
  }, undefined, false, undefined, this);
748
699
  }
749
700
 
750
- // src/components/ScrollableLog.tsx
751
- import { useState as useState4, useEffect as useEffect3, useMemo } from "react";
752
- import { Box as Box10, Text as Text9, useInput as useInput3 } from "ink";
753
- import { jsxDEV as jsxDEV10 } from "react/jsx-dev-runtime";
754
- function ScrollableLog({
755
- lines,
756
- maxHeight,
757
- autoScroll = true,
758
- showScrollHint = true
759
- }) {
760
- const { rows } = useTerminalSize();
761
- const visibleLines = maxHeight || Math.max(5, rows - 12);
762
- const [scrollOffset, setScrollOffset] = useState4(0);
763
- const [isAutoScrolling, setIsAutoScrolling] = useState4(autoScroll);
764
- const totalLines = lines.length;
765
- const maxOffset = Math.max(0, totalLines - visibleLines);
766
- useEffect3(() => {
767
- if (isAutoScrolling) {
768
- setScrollOffset(maxOffset);
769
- }
770
- }, [totalLines, maxOffset, isAutoScrolling]);
771
- useInput3((input, key) => {
772
- if (key.downArrow || input === "j") {
773
- setIsAutoScrolling(false);
774
- setScrollOffset((prev) => Math.min(prev + 1, maxOffset));
775
- }
776
- if (key.upArrow || input === "k") {
777
- setIsAutoScrolling(false);
778
- setScrollOffset((prev) => Math.max(prev - 1, 0));
701
+ // src/components/Onboarding.tsx
702
+ import { useState as useState4 } from "react";
703
+ import { Box as Box9, Text as Text8, useInput as useInput3 } from "ink";
704
+
705
+ // src/components/ui/VimSelect.tsx
706
+ import { useState as useState3 } from "react";
707
+ import { Box as Box8, Text as Text7, useInput as useInput2 } from "ink";
708
+ import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
709
+ function VimSelect({ options, onChange, isDisabled = false }) {
710
+ const [index, setIndex] = useState3(0);
711
+ useInput2((input, key) => {
712
+ if (isDisabled)
713
+ return;
714
+ if (input === "j" || key.downArrow) {
715
+ setIndex((i) => i < options.length - 1 ? i + 1 : i);
779
716
  }
780
- if (input === "G") {
781
- setIsAutoScrolling(true);
782
- setScrollOffset(maxOffset);
717
+ if (input === "k" || key.upArrow) {
718
+ setIndex((i) => i > 0 ? i - 1 : i);
783
719
  }
784
- if (input === "g") {
785
- setIsAutoScrolling(false);
786
- setScrollOffset(0);
720
+ if (input === "l" || key.return) {
721
+ onChange(options[index].value);
787
722
  }
788
723
  });
789
- const visibleContent = useMemo(() => {
790
- return lines.slice(scrollOffset, scrollOffset + visibleLines);
791
- }, [lines, scrollOffset, visibleLines]);
792
- const showScrollUp = scrollOffset > 0;
793
- const showScrollDown = scrollOffset < maxOffset;
794
- return /* @__PURE__ */ jsxDEV10(Box10, {
724
+ return /* @__PURE__ */ jsxDEV8(Box8, {
795
725
  flexDirection: "column",
796
- children: [
797
- showScrollHint && showScrollUp && /* @__PURE__ */ jsxDEV10(Text9, {
798
- dimColor: true,
726
+ children: options.map((opt, i) => /* @__PURE__ */ jsxDEV8(Box8, {
727
+ children: /* @__PURE__ */ jsxDEV8(Text7, {
728
+ color: i === index ? colors.primary : undefined,
799
729
  children: [
800
- " ",
801
- scrollOffset,
802
- " more line",
803
- scrollOffset !== 1 ? "s" : ""
730
+ i === index ? "❯" : " ",
731
+ " ",
732
+ opt.label
804
733
  ]
805
- }, undefined, true, undefined, this),
806
- /* @__PURE__ */ jsxDEV10(Box10, {
807
- flexDirection: "column",
808
- height: visibleLines,
809
- overflow: "hidden",
810
- children: visibleContent.map((line, i) => /* @__PURE__ */ jsxDEV10(Text9, {
811
- children: line
812
- }, scrollOffset + i, false, undefined, this))
813
- }, undefined, false, undefined, this),
814
- showScrollHint && showScrollDown && /* @__PURE__ */ jsxDEV10(Text9, {
815
- dimColor: true,
734
+ }, undefined, true, undefined, this)
735
+ }, opt.value, false, undefined, this))
736
+ }, undefined, false, undefined, this);
737
+ }
738
+
739
+ // src/lib/templates.ts
740
+ import { join as join3 } from "path";
741
+ import { existsSync as existsSync2 } from "fs";
742
+ var EXAMPLE_CONFIG_README = `# Example Stow Config Package
743
+
744
+ This is an example dotfiles package for use with GNU Stow.
745
+
746
+ ## Structure
747
+ Files are organized to mirror your home directory:
748
+ - \`.config/example-app/config.toml\` -> \`~/.config/example-app/config.toml\`
749
+ - \`.example-app-rc\` -> \`~/.example-app-rc\`
750
+
751
+ ## Usage
752
+ 1. Place your dotfiles in this directory structure
753
+ 2. Run \`formalconf\` and use Config Manager -> Stow
754
+ 3. Symlinks will be created from your home directory
755
+
756
+ ## Creating Your Own
757
+ 1. Copy this directory and rename it (e.g., \`git\`, \`zsh\`, \`nvim\`)
758
+ 2. Add your dotfiles mirroring your home directory structure
759
+ 3. Stow the package to create symlinks
760
+ `;
761
+ var EXAMPLE_CONFIG_TOML = `# Example configuration file
762
+ # This will be symlinked to ~/.config/example-app/config.toml
763
+
764
+ [settings]
765
+ theme = "default"
766
+ auto_save = true
767
+
768
+ [keybindings]
769
+ quit = "q"
770
+ save = "ctrl+s"
771
+ `;
772
+ var EXAMPLE_RC = `# Example rc file
773
+ # This will be symlinked to ~/.example-app-rc
774
+ export EXAMPLE_VAR="hello"
775
+ `;
776
+ var EXAMPLE_THEME_YAML = `name: Example Theme
777
+ author: Your Name
778
+ description: A template theme for FormalConf
779
+ version: 1.0.0
780
+
781
+ colors:
782
+ primary: "#5eead4"
783
+ secondary: "#2dd4bf"
784
+ background: "#1a1a2e"
785
+ foreground: "#e4e4e7"
786
+ accent: "#06b6d4"
787
+ `;
788
+ var EXAMPLE_NEOVIM_LUA = `-- Neovim colorscheme configuration
789
+ -- This file is symlinked when the theme is applied
790
+ return {
791
+ {
792
+ "your-colorscheme/nvim",
793
+ name = "example-theme",
794
+ priority = 1000,
795
+ },
796
+ {
797
+ "LazyVim/LazyVim",
798
+ opts = {
799
+ colorscheme = "example-theme",
800
+ },
801
+ },
802
+ }
803
+ `;
804
+ var EXAMPLE_GHOSTTY_CONF = `# Ghostty terminal theme
805
+ # Add your terminal colors here
806
+ theme = example-theme
807
+ `;
808
+ var THEME_README = `# Example Theme
809
+
810
+ This is a template theme for FormalConf.
811
+
812
+ ## Structure
813
+ - \`theme.yaml\` - Theme metadata and color definitions
814
+ - \`neovim.lua\` - Neovim colorscheme config
815
+ - \`ghostty.conf\` - Ghostty terminal theme
816
+ - \`backgrounds/\` - Wallpaper images (optional)
817
+
818
+ ## Creating Your Own Theme
819
+ 1. Copy this directory and rename it
820
+ 2. Update \`theme.yaml\` with your theme info
821
+ 3. Add config files for your applications
822
+ 4. Files are symlinked to ~/.config/formalconf/current/theme/
823
+ `;
824
+ var BACKGROUNDS_README = `# Backgrounds
825
+
826
+ Place wallpaper images here:
827
+ - Supported formats: PNG, JPG
828
+ - These will be available at ~/.config/formalconf/current/backgrounds/
829
+ `;
830
+ var CONFIGS_README = `# Configs Directory
831
+
832
+ This directory contains your stow packages - collections of dotfiles
833
+ that are symlinked to your home directory.
834
+
835
+ ## Creating a Config Package
836
+
837
+ 1. Create a new directory: \`mkdir my-app\`
838
+ 2. Add files mirroring your home directory structure
839
+ 3. Use FormalConf to stow the package
840
+
841
+ ## Example Structure
842
+ \`\`\`
843
+ my-app/
844
+ .config/
845
+ my-app/
846
+ config.toml -> ~/.config/my-app/config.toml
847
+ .my-app-rc -> ~/.my-app-rc
848
+ \`\`\`
849
+
850
+ ## Commands
851
+ - Stow: Creates symlinks from home directory to these files
852
+ - Unstow: Removes the symlinks
853
+ - Status: Shows which packages are stowed
854
+ `;
855
+ var THEMES_README = `# Themes Directory
856
+
857
+ Themes contain application-specific config files that define colors and styling.
858
+
859
+ ## Theme Structure
860
+ \`\`\`
861
+ my-theme/
862
+ theme.yaml # Theme metadata (required)
863
+ neovim.lua # Neovim colorscheme
864
+ ghostty.conf # Terminal theme
865
+ backgrounds/ # Wallpaper images
866
+ \`\`\`
867
+
868
+ ## Applying Themes
869
+ Select a theme in FormalConf to symlink its files to:
870
+ \`~/.config/formalconf/current/theme/\`
871
+
872
+ Your applications should source files from this location.
873
+ `;
874
+ async function installExampleConfig() {
875
+ const dest = join3(CONFIGS_DIR, "example-config");
876
+ if (existsSync2(dest))
877
+ return;
878
+ await ensureDir2(dest);
879
+ await ensureDir2(join3(dest, ".config", "example-app"));
880
+ await writeFile(join3(dest, "README.md"), EXAMPLE_CONFIG_README);
881
+ await writeFile(join3(dest, ".config", "example-app", "config.toml"), EXAMPLE_CONFIG_TOML);
882
+ await writeFile(join3(dest, ".example-app-rc"), EXAMPLE_RC);
883
+ }
884
+ async function installExampleTheme() {
885
+ const dest = join3(THEMES_DIR, "example-theme");
886
+ if (existsSync2(dest))
887
+ return;
888
+ await ensureDir2(dest);
889
+ await ensureDir2(join3(dest, "backgrounds"));
890
+ await writeFile(join3(dest, "theme.yaml"), EXAMPLE_THEME_YAML);
891
+ await writeFile(join3(dest, "neovim.lua"), EXAMPLE_NEOVIM_LUA);
892
+ await writeFile(join3(dest, "ghostty.conf"), EXAMPLE_GHOSTTY_CONF);
893
+ await writeFile(join3(dest, "backgrounds", "README.md"), BACKGROUNDS_README);
894
+ await writeFile(join3(dest, "README.md"), THEME_README);
895
+ }
896
+ async function installReadmes() {
897
+ const configsReadme = join3(CONFIGS_DIR, "README.md");
898
+ const themesReadme = join3(THEMES_DIR, "README.md");
899
+ if (!existsSync2(configsReadme)) {
900
+ await writeFile(configsReadme, CONFIGS_README);
901
+ }
902
+ if (!existsSync2(themesReadme)) {
903
+ await writeFile(themesReadme, THEMES_README);
904
+ }
905
+ }
906
+ var DEFAULT_PKG_CONFIG = {
907
+ config: {
908
+ purge: false,
909
+ purgeInteractive: true,
910
+ autoUpdate: true
911
+ },
912
+ taps: [],
913
+ packages: [],
914
+ casks: [],
915
+ mas: {}
916
+ };
917
+ async function installPkgConfig() {
918
+ if (existsSync2(PKG_CONFIG_PATH))
919
+ return;
920
+ await writeFile(PKG_CONFIG_PATH, JSON.stringify(DEFAULT_PKG_CONFIG, null, 2));
921
+ }
922
+
923
+ // src/components/Onboarding.tsx
924
+ import { jsxDEV as jsxDEV9 } from "react/jsx-dev-runtime";
925
+ function Onboarding({ onComplete }) {
926
+ const [step, setStep] = useState4("welcome");
927
+ const [createdItems, setCreatedItems] = useState4([]);
928
+ const addCreatedItem = (item) => {
929
+ setCreatedItems((prev) => [...prev, item]);
930
+ };
931
+ switch (step) {
932
+ case "welcome":
933
+ return /* @__PURE__ */ jsxDEV9(WelcomeStep, {
934
+ onNext: () => setStep("configs")
935
+ }, undefined, false, undefined, this);
936
+ case "configs":
937
+ return /* @__PURE__ */ jsxDEV9(ConfigsStep, {
938
+ onNext: () => setStep("themes"),
939
+ onCreate: async () => {
940
+ await installExampleConfig();
941
+ addCreatedItem("Example config package");
942
+ }
943
+ }, undefined, false, undefined, this);
944
+ case "themes":
945
+ return /* @__PURE__ */ jsxDEV9(ThemesStep, {
946
+ onNext: () => setStep("packages"),
947
+ onCreate: async () => {
948
+ await installExampleTheme();
949
+ addCreatedItem("Example theme");
950
+ }
951
+ }, undefined, false, undefined, this);
952
+ case "packages":
953
+ return /* @__PURE__ */ jsxDEV9(PackagesStep, {
954
+ onNext: async () => {
955
+ await installPkgConfig();
956
+ setStep("complete");
957
+ }
958
+ }, undefined, false, undefined, this);
959
+ case "complete":
960
+ return /* @__PURE__ */ jsxDEV9(CompleteStep, {
961
+ createdItems,
962
+ onComplete: async () => {
963
+ await installReadmes();
964
+ onComplete();
965
+ }
966
+ }, undefined, false, undefined, this);
967
+ }
968
+ }
969
+ function WelcomeStep({ onNext }) {
970
+ useInput3((_, key) => {
971
+ if (key.return)
972
+ onNext();
973
+ });
974
+ return /* @__PURE__ */ jsxDEV9(Layout, {
975
+ breadcrumb: ["Setup"],
976
+ showFooter: false,
977
+ children: /* @__PURE__ */ jsxDEV9(Panel, {
978
+ title: "Welcome to FormalConf",
979
+ children: /* @__PURE__ */ jsxDEV9(Box9, {
980
+ flexDirection: "column",
981
+ gap: 1,
816
982
  children: [
817
- " ↓ ",
818
- maxOffset - scrollOffset,
819
- " more line",
820
- maxOffset - scrollOffset !== 1 ? "s" : ""
983
+ /* @__PURE__ */ jsxDEV9(Text8, {
984
+ children: "FormalConf helps you manage your dotfiles and system configuration."
985
+ }, undefined, false, undefined, this),
986
+ /* @__PURE__ */ jsxDEV9(Text8, {
987
+ dimColor: true,
988
+ children: "This setup will walk you through the basics and optionally create example files to get you started."
989
+ }, undefined, false, undefined, this),
990
+ /* @__PURE__ */ jsxDEV9(Box9, {
991
+ marginTop: 1,
992
+ children: /* @__PURE__ */ jsxDEV9(Text8, {
993
+ color: colors.primary,
994
+ children: "Press Enter to continue..."
995
+ }, undefined, false, undefined, this)
996
+ }, undefined, false, undefined, this)
821
997
  ]
822
- }, undefined, true, undefined, this),
823
- showScrollHint && totalLines > visibleLines && /* @__PURE__ */ jsxDEV10(Text9, {
824
- dimColor: true,
998
+ }, undefined, true, undefined, this)
999
+ }, undefined, false, undefined, this)
1000
+ }, undefined, false, undefined, this);
1001
+ }
1002
+ function ConfigsStep({
1003
+ onNext,
1004
+ onCreate
1005
+ }) {
1006
+ const [isCreating, setIsCreating] = useState4(false);
1007
+ const handleSelect = async (value) => {
1008
+ if (value === "create") {
1009
+ setIsCreating(true);
1010
+ await onCreate();
1011
+ }
1012
+ onNext();
1013
+ };
1014
+ return /* @__PURE__ */ jsxDEV9(Layout, {
1015
+ breadcrumb: ["Setup", "Config Packages"],
1016
+ showFooter: false,
1017
+ children: /* @__PURE__ */ jsxDEV9(Panel, {
1018
+ title: "Config Packages",
1019
+ children: /* @__PURE__ */ jsxDEV9(Box9, {
1020
+ flexDirection: "column",
1021
+ gap: 1,
825
1022
  children: [
826
- "j/k scroll g top • G bottom ",
827
- isAutoScrolling ? "(auto-scroll)" : ""
1023
+ /* @__PURE__ */ jsxDEV9(Text8, {
1024
+ children: "Config packages are directories containing your dotfiles."
1025
+ }, undefined, false, undefined, this),
1026
+ /* @__PURE__ */ jsxDEV9(Text8, {
1027
+ dimColor: true,
1028
+ children: "FormalConf uses GNU Stow to create symlinks from your home directory."
1029
+ }, undefined, false, undefined, this),
1030
+ /* @__PURE__ */ jsxDEV9(Box9, {
1031
+ marginTop: 1,
1032
+ flexDirection: "column",
1033
+ children: [
1034
+ /* @__PURE__ */ jsxDEV9(Text8, {
1035
+ dimColor: true,
1036
+ children: "Example structure:"
1037
+ }, undefined, false, undefined, this),
1038
+ /* @__PURE__ */ jsxDEV9(Text8, {
1039
+ children: " my-config/"
1040
+ }, undefined, false, undefined, this),
1041
+ /* @__PURE__ */ jsxDEV9(Text8, {
1042
+ children: " .config/"
1043
+ }, undefined, false, undefined, this),
1044
+ /* @__PURE__ */ jsxDEV9(Text8, {
1045
+ children: " app/config.toml"
1046
+ }, undefined, false, undefined, this)
1047
+ ]
1048
+ }, undefined, true, undefined, this),
1049
+ /* @__PURE__ */ jsxDEV9(Box9, {
1050
+ marginTop: 1,
1051
+ children: /* @__PURE__ */ jsxDEV9(VimSelect, {
1052
+ options: [
1053
+ { label: "Create example config package", value: "create" },
1054
+ { label: "Skip", value: "skip" }
1055
+ ],
1056
+ onChange: handleSelect,
1057
+ isDisabled: isCreating
1058
+ }, undefined, false, undefined, this)
1059
+ }, undefined, false, undefined, this)
828
1060
  ]
829
1061
  }, undefined, true, undefined, this)
830
- ]
831
- }, undefined, true, undefined, this);
1062
+ }, undefined, false, undefined, this)
1063
+ }, undefined, false, undefined, this);
832
1064
  }
833
-
834
- // src/components/PromptInput.tsx
835
- import { Box as Box11, Text as Text10, useInput as useInput4 } from "ink";
836
- import { jsxDEV as jsxDEV11 } from "react/jsx-dev-runtime";
837
- function PromptInput({
838
- question,
839
- options = ["y", "n"],
840
- onAnswer
1065
+ function ThemesStep({
1066
+ onNext,
1067
+ onCreate
841
1068
  }) {
842
- useInput4((input) => {
843
- const lower = input.toLowerCase();
844
- if (options.includes(lower)) {
845
- onAnswer(lower);
1069
+ const [isCreating, setIsCreating] = useState4(false);
1070
+ const handleSelect = async (value) => {
1071
+ if (value === "create") {
1072
+ setIsCreating(true);
1073
+ await onCreate();
1074
+ }
1075
+ onNext();
1076
+ };
1077
+ return /* @__PURE__ */ jsxDEV9(Layout, {
1078
+ breadcrumb: ["Setup", "Themes"],
1079
+ showFooter: false,
1080
+ children: /* @__PURE__ */ jsxDEV9(Panel, {
1081
+ title: "Themes",
1082
+ children: /* @__PURE__ */ jsxDEV9(Box9, {
1083
+ flexDirection: "column",
1084
+ gap: 1,
1085
+ children: [
1086
+ /* @__PURE__ */ jsxDEV9(Text8, {
1087
+ children: "Themes contain application configs for colors and styling."
1088
+ }, undefined, false, undefined, this),
1089
+ /* @__PURE__ */ jsxDEV9(Text8, {
1090
+ dimColor: true,
1091
+ children: "When applied, theme files are symlinked to a central location your apps can source from."
1092
+ }, undefined, false, undefined, this),
1093
+ /* @__PURE__ */ jsxDEV9(Box9, {
1094
+ marginTop: 1,
1095
+ flexDirection: "column",
1096
+ children: [
1097
+ /* @__PURE__ */ jsxDEV9(Text8, {
1098
+ dimColor: true,
1099
+ children: "Theme structure:"
1100
+ }, undefined, false, undefined, this),
1101
+ /* @__PURE__ */ jsxDEV9(Text8, {
1102
+ children: " my-theme/"
1103
+ }, undefined, false, undefined, this),
1104
+ /* @__PURE__ */ jsxDEV9(Text8, {
1105
+ children: " theme.yaml"
1106
+ }, undefined, false, undefined, this),
1107
+ /* @__PURE__ */ jsxDEV9(Text8, {
1108
+ children: " neovim.lua"
1109
+ }, undefined, false, undefined, this),
1110
+ /* @__PURE__ */ jsxDEV9(Text8, {
1111
+ children: " backgrounds/"
1112
+ }, undefined, false, undefined, this)
1113
+ ]
1114
+ }, undefined, true, undefined, this),
1115
+ /* @__PURE__ */ jsxDEV9(Box9, {
1116
+ marginTop: 1,
1117
+ children: /* @__PURE__ */ jsxDEV9(VimSelect, {
1118
+ options: [
1119
+ { label: "Create example theme", value: "create" },
1120
+ { label: "Skip", value: "skip" }
1121
+ ],
1122
+ onChange: handleSelect,
1123
+ isDisabled: isCreating
1124
+ }, undefined, false, undefined, this)
1125
+ }, undefined, false, undefined, this)
1126
+ ]
1127
+ }, undefined, true, undefined, this)
1128
+ }, undefined, false, undefined, this)
1129
+ }, undefined, false, undefined, this);
1130
+ }
1131
+ function PackagesStep({ onNext }) {
1132
+ const [isCreating, setIsCreating] = useState4(false);
1133
+ useInput3(async (_, key) => {
1134
+ if (key.return && !isCreating) {
1135
+ setIsCreating(true);
1136
+ await onNext();
846
1137
  }
847
1138
  });
848
- return /* @__PURE__ */ jsxDEV11(Box11, {
849
- marginTop: 1,
850
- borderStyle: "single",
851
- borderColor: colors.accent,
852
- paddingX: 1,
853
- children: /* @__PURE__ */ jsxDEV11(Text10, {
854
- children: [
855
- question,
856
- " ",
857
- /* @__PURE__ */ jsxDEV11(Text10, {
858
- color: colors.accent,
859
- children: [
860
- "[",
861
- options.join("/"),
862
- "]"
863
- ]
864
- }, undefined, true, undefined, this),
865
- /* @__PURE__ */ jsxDEV11(Text10, {
866
- dimColor: true,
867
- children: ": "
868
- }, undefined, false, undefined, this)
869
- ]
870
- }, undefined, true, undefined, this)
1139
+ return /* @__PURE__ */ jsxDEV9(Layout, {
1140
+ breadcrumb: ["Setup", "Package Sync"],
1141
+ showFooter: false,
1142
+ children: /* @__PURE__ */ jsxDEV9(Panel, {
1143
+ title: "Package Sync",
1144
+ children: /* @__PURE__ */ jsxDEV9(Box9, {
1145
+ flexDirection: "column",
1146
+ gap: 1,
1147
+ children: [
1148
+ /* @__PURE__ */ jsxDEV9(Text8, {
1149
+ children: "FormalConf can sync your Homebrew packages from a config file."
1150
+ }, undefined, false, undefined, this),
1151
+ /* @__PURE__ */ jsxDEV9(Text8, {
1152
+ dimColor: true,
1153
+ children: "Edit ~/.config/formalconf/pkg-config.json to define your packages, then run Package Sync from the main menu."
1154
+ }, undefined, false, undefined, this),
1155
+ /* @__PURE__ */ jsxDEV9(Box9, {
1156
+ marginTop: 1,
1157
+ children: /* @__PURE__ */ jsxDEV9(Text8, {
1158
+ color: colors.primary,
1159
+ children: "Press Enter to continue..."
1160
+ }, undefined, false, undefined, this)
1161
+ }, undefined, false, undefined, this)
1162
+ ]
1163
+ }, undefined, true, undefined, this)
1164
+ }, undefined, false, undefined, this)
1165
+ }, undefined, false, undefined, this);
1166
+ }
1167
+ function CompleteStep({
1168
+ createdItems,
1169
+ onComplete
1170
+ }) {
1171
+ const [isFinishing, setIsFinishing] = useState4(false);
1172
+ useInput3(async (_, key) => {
1173
+ if (key.return && !isFinishing) {
1174
+ setIsFinishing(true);
1175
+ await onComplete();
1176
+ }
1177
+ });
1178
+ return /* @__PURE__ */ jsxDEV9(Layout, {
1179
+ breadcrumb: ["Setup", "Complete"],
1180
+ showFooter: false,
1181
+ children: /* @__PURE__ */ jsxDEV9(Panel, {
1182
+ title: "Setup Complete",
1183
+ children: /* @__PURE__ */ jsxDEV9(Box9, {
1184
+ flexDirection: "column",
1185
+ gap: 1,
1186
+ children: [
1187
+ /* @__PURE__ */ jsxDEV9(Text8, {
1188
+ color: colors.success,
1189
+ children: "You're all set!"
1190
+ }, undefined, false, undefined, this),
1191
+ createdItems.length > 0 && /* @__PURE__ */ jsxDEV9(Box9, {
1192
+ flexDirection: "column",
1193
+ marginTop: 1,
1194
+ children: [
1195
+ /* @__PURE__ */ jsxDEV9(Text8, {
1196
+ dimColor: true,
1197
+ children: "Created:"
1198
+ }, undefined, false, undefined, this),
1199
+ createdItems.map((item, i) => /* @__PURE__ */ jsxDEV9(Text8, {
1200
+ children: [
1201
+ " - ",
1202
+ item
1203
+ ]
1204
+ }, i, true, undefined, this))
1205
+ ]
1206
+ }, undefined, true, undefined, this),
1207
+ /* @__PURE__ */ jsxDEV9(Box9, {
1208
+ marginTop: 1,
1209
+ children: /* @__PURE__ */ jsxDEV9(Text8, {
1210
+ dimColor: true,
1211
+ children: "README files have been added to help you get started."
1212
+ }, undefined, false, undefined, this)
1213
+ }, undefined, false, undefined, this),
1214
+ /* @__PURE__ */ jsxDEV9(Box9, {
1215
+ marginTop: 1,
1216
+ children: /* @__PURE__ */ jsxDEV9(Text8, {
1217
+ color: colors.primary,
1218
+ children: "Press Enter to start..."
1219
+ }, undefined, false, undefined, this)
1220
+ }, undefined, false, undefined, this)
1221
+ ]
1222
+ }, undefined, true, undefined, this)
1223
+ }, undefined, false, undefined, this)
871
1224
  }, undefined, false, undefined, this);
872
1225
  }
873
1226
 
874
- // src/lib/theme-parser.ts
875
- import { existsSync as existsSync2, readdirSync as readdirSync2 } from "fs";
876
- import { join as join3 } from "path";
877
- function parseYaml(content) {
878
- const result = {};
879
- const lines = content.split(`
880
- `);
881
- let currentSection = null;
882
- let currentKey = "";
883
- for (const line of lines) {
884
- const trimmed = line.trim();
885
- if (!trimmed || trimmed.startsWith("#"))
886
- continue;
887
- const indentLevel = line.search(/\S/);
888
- const match = trimmed.match(/^([\w-]+):\s*(.*)$/);
889
- if (match) {
890
- const [, key, value] = match;
891
- if (indentLevel === 0) {
892
- if (value) {
893
- result[key] = value.replace(/^["']|["']$/g, "");
894
- } else {
895
- currentKey = key;
896
- currentSection = {};
897
- result[key] = currentSection;
1227
+ // src/components/menus/MainMenu.tsx
1228
+ import { useApp } from "ink";
1229
+ import { jsxDEV as jsxDEV10 } from "react/jsx-dev-runtime";
1230
+ function MainMenu({ onSelect }) {
1231
+ const { exit } = useApp();
1232
+ return /* @__PURE__ */ jsxDEV10(Panel, {
1233
+ title: "Main Menu",
1234
+ children: /* @__PURE__ */ jsxDEV10(VimSelect, {
1235
+ options: [
1236
+ { label: "Config Manager", value: "config" },
1237
+ { label: "Package Sync", value: "packages" },
1238
+ { label: "Set Theme", value: "themes" },
1239
+ { label: "Exit", value: "exit" }
1240
+ ],
1241
+ onChange: (value) => {
1242
+ if (value === "exit") {
1243
+ exit();
1244
+ return;
898
1245
  }
899
- } else if (currentSection) {
900
- currentSection[key] = value.replace(/^["']|["']$/g, "");
1246
+ onSelect(value);
901
1247
  }
902
- }
903
- }
904
- return result;
1248
+ }, undefined, false, undefined, this)
1249
+ }, undefined, false, undefined, this);
905
1250
  }
906
- async function parseThemeMetadata(themePath) {
907
- const yamlPath = join3(themePath, "theme.yaml");
908
- if (!existsSync2(yamlPath)) {
909
- return;
910
- }
911
- try {
912
- const content = await readText(yamlPath);
913
- const parsed = parseYaml(content);
914
- return {
915
- name: parsed.name || "",
916
- author: parsed.author,
917
- description: parsed.description,
918
- version: parsed.version,
919
- source: parsed.source,
920
- colors: parsed.colors
921
- };
922
- } catch {
923
- return;
924
- }
1251
+
1252
+ // src/components/CommandOutput.tsx
1253
+ import { Box as Box10, Text as Text9, useInput as useInput4 } from "ink";
1254
+ import { jsxDEV as jsxDEV11 } from "react/jsx-dev-runtime";
1255
+ function CommandOutput({
1256
+ title,
1257
+ output,
1258
+ success = true,
1259
+ onDismiss
1260
+ }) {
1261
+ useInput4(() => {
1262
+ onDismiss();
1263
+ });
1264
+ return /* @__PURE__ */ jsxDEV11(Panel, {
1265
+ title,
1266
+ borderColor: success ? colors.success : colors.error,
1267
+ children: [
1268
+ output && /* @__PURE__ */ jsxDEV11(Box10, {
1269
+ flexDirection: "column",
1270
+ marginBottom: 1,
1271
+ children: /* @__PURE__ */ jsxDEV11(Text9, {
1272
+ children: output
1273
+ }, undefined, false, undefined, this)
1274
+ }, undefined, false, undefined, this),
1275
+ /* @__PURE__ */ jsxDEV11(Text9, {
1276
+ color: success ? colors.success : colors.error,
1277
+ children: success ? "Done" : "Failed"
1278
+ }, undefined, false, undefined, this),
1279
+ /* @__PURE__ */ jsxDEV11(Text9, {
1280
+ dimColor: true,
1281
+ children: "Press any key to continue..."
1282
+ }, undefined, false, undefined, this)
1283
+ ]
1284
+ }, undefined, true, undefined, this);
925
1285
  }
926
- function parseThemeFiles(themePath) {
927
- const entries = readdirSync2(themePath, { withFileTypes: true });
928
- return entries.filter((e) => e.isFile() && !e.name.startsWith(".") && e.name !== "theme.yaml" && e.name !== "light.mode").map((e) => ({
929
- name: e.name,
930
- path: join3(themePath, e.name),
931
- application: e.name.replace(/\.(conf|theme|lua|toml|css|json|ini)$/, "")
932
- }));
1286
+
1287
+ // src/components/LoadingPanel.tsx
1288
+ import { Spinner } from "@inkjs/ui";
1289
+ import { jsxDEV as jsxDEV12 } from "react/jsx-dev-runtime";
1290
+ function LoadingPanel({ title, label = "Processing..." }) {
1291
+ return /* @__PURE__ */ jsxDEV12(Panel, {
1292
+ title,
1293
+ children: /* @__PURE__ */ jsxDEV12(Spinner, {
1294
+ label
1295
+ }, undefined, false, undefined, this)
1296
+ }, undefined, false, undefined, this);
933
1297
  }
934
- async function parseTheme(themePath, themeName) {
935
- const files = parseThemeFiles(themePath);
936
- const metadata = await parseThemeMetadata(themePath);
1298
+
1299
+ // src/hooks/useMenuAction.ts
1300
+ import { useState as useState5, useCallback } from "react";
1301
+ function useMenuAction() {
1302
+ const [state, setState] = useState5("menu");
1303
+ const [output, setOutput] = useState5("");
1304
+ const [success, setSuccess] = useState5(true);
1305
+ const execute = useCallback(async (action) => {
1306
+ setState("running");
1307
+ const result = await action();
1308
+ setOutput(result.output);
1309
+ setSuccess(result.success);
1310
+ setState("result");
1311
+ }, []);
1312
+ const reset = useCallback(() => {
1313
+ setState("menu");
1314
+ }, []);
937
1315
  return {
938
- name: metadata?.name || themeName,
939
- path: themePath,
940
- files,
941
- metadata,
942
- hasBackgrounds: existsSync2(join3(themePath, "backgrounds")),
943
- hasPreview: existsSync2(join3(themePath, "preview.png")),
944
- isLightMode: existsSync2(join3(themePath, "light.mode"))
1316
+ state,
1317
+ output,
1318
+ success,
1319
+ isRunning: state === "running",
1320
+ isResult: state === "result",
1321
+ execute,
1322
+ reset
945
1323
  };
946
1324
  }
947
1325
 
1326
+ // src/hooks/useBackNavigation.ts
1327
+ import { useInput as useInput5 } from "ink";
1328
+ function useBackNavigation({
1329
+ enabled = true,
1330
+ onBack
1331
+ }) {
1332
+ useInput5((input, key) => {
1333
+ if (enabled && (key.escape || key.leftArrow || input === "h")) {
1334
+ onBack();
1335
+ }
1336
+ });
1337
+ }
1338
+
948
1339
  // src/cli/config-manager.ts
949
1340
  import { parseArgs } from "util";
950
- import { readdirSync as readdirSync3, existsSync as existsSync3, lstatSync as lstatSync2, readlinkSync as readlinkSync2 } from "fs";
1341
+ import { readdirSync as readdirSync2, existsSync as existsSync3, lstatSync as lstatSync2, readlinkSync as readlinkSync2 } from "fs";
951
1342
  var colors2 = {
952
1343
  red: "\x1B[0;31m",
953
1344
  green: "\x1B[0;32m",
@@ -963,7 +1354,7 @@ async function checkStow() {
963
1354
  }
964
1355
  }
965
1356
  function listPackages() {
966
- const entries = readdirSync3(CONFIGS_DIR, { withFileTypes: true });
1357
+ const entries = readdirSync2(CONFIGS_DIR, { withFileTypes: true });
967
1358
  return entries.filter((e) => e.isDirectory()).map((e) => ({
968
1359
  name: e.name,
969
1360
  path: `${CONFIGS_DIR}/${e.name}`,
@@ -974,7 +1365,7 @@ function checkPackageStowed(packageName) {
974
1365
  const packageDir = `${CONFIGS_DIR}/${packageName}`;
975
1366
  if (!existsSync3(packageDir))
976
1367
  return false;
977
- const entries = readdirSync3(packageDir, { withFileTypes: true });
1368
+ const entries = readdirSync2(packageDir, { withFileTypes: true });
978
1369
  for (const entry of entries) {
979
1370
  const targetPath = `${HOME_DIR}/${entry.name}`;
980
1371
  if (!existsSync3(targetPath))
@@ -1237,9 +1628,177 @@ ${colors2.cyan}Available packages:${colors2.reset}`);
1237
1628
  break;
1238
1629
  }
1239
1630
  }
1240
- var isMainModule = process.argv[1]?.includes("config-manager");
1241
- if (isMainModule) {
1242
- main().catch(console.error);
1631
+ var isMainModule = process.argv[1]?.includes("config-manager");
1632
+ if (isMainModule) {
1633
+ main().catch(console.error);
1634
+ }
1635
+
1636
+ // src/components/menus/ConfigMenu.tsx
1637
+ import { jsxDEV as jsxDEV13 } from "react/jsx-dev-runtime";
1638
+ function ConfigMenu({ onBack }) {
1639
+ const { state, output, success, isRunning, isResult, execute, reset } = useMenuAction();
1640
+ useBackNavigation({ enabled: state === "menu", onBack });
1641
+ const handleAction = async (action) => {
1642
+ if (action === "back") {
1643
+ onBack();
1644
+ return;
1645
+ }
1646
+ await execute(() => runConfigManager([action]));
1647
+ };
1648
+ if (isRunning) {
1649
+ return /* @__PURE__ */ jsxDEV13(LoadingPanel, {
1650
+ title: "Config Manager"
1651
+ }, undefined, false, undefined, this);
1652
+ }
1653
+ if (isResult) {
1654
+ return /* @__PURE__ */ jsxDEV13(CommandOutput, {
1655
+ title: "Config Manager",
1656
+ output,
1657
+ success,
1658
+ onDismiss: reset
1659
+ }, undefined, false, undefined, this);
1660
+ }
1661
+ return /* @__PURE__ */ jsxDEV13(Panel, {
1662
+ title: "Config Manager",
1663
+ children: /* @__PURE__ */ jsxDEV13(VimSelect, {
1664
+ options: [
1665
+ { label: "Stow all packages", value: "stow-all" },
1666
+ { label: "Unstow all packages", value: "unstow-all" },
1667
+ { label: "Check status", value: "status" },
1668
+ { label: "List packages", value: "list" },
1669
+ { label: "Back", value: "back" }
1670
+ ],
1671
+ onChange: handleAction
1672
+ }, undefined, false, undefined, this)
1673
+ }, undefined, false, undefined, this);
1674
+ }
1675
+
1676
+ // src/components/menus/PackageMenu.tsx
1677
+ import { useState as useState7, useCallback as useCallback2, useMemo as useMemo2, useRef } from "react";
1678
+ import { Box as Box13, Text as Text12, useInput as useInput8 } from "ink";
1679
+
1680
+ // src/components/ScrollableLog.tsx
1681
+ import { useState as useState6, useEffect as useEffect3, useMemo } from "react";
1682
+ import { Box as Box11, Text as Text10, useInput as useInput6 } from "ink";
1683
+ import { jsxDEV as jsxDEV14 } from "react/jsx-dev-runtime";
1684
+ function ScrollableLog({
1685
+ lines,
1686
+ maxHeight,
1687
+ autoScroll = true,
1688
+ showScrollHint = true
1689
+ }) {
1690
+ const { rows } = useTerminalSize();
1691
+ const visibleLines = maxHeight || Math.max(5, rows - 12);
1692
+ const [scrollOffset, setScrollOffset] = useState6(0);
1693
+ const [isAutoScrolling, setIsAutoScrolling] = useState6(autoScroll);
1694
+ const totalLines = lines.length;
1695
+ const maxOffset = Math.max(0, totalLines - visibleLines);
1696
+ useEffect3(() => {
1697
+ if (isAutoScrolling) {
1698
+ setScrollOffset(maxOffset);
1699
+ }
1700
+ }, [totalLines, maxOffset, isAutoScrolling]);
1701
+ useInput6((input, key) => {
1702
+ if (key.downArrow || input === "j") {
1703
+ setIsAutoScrolling(false);
1704
+ setScrollOffset((prev) => Math.min(prev + 1, maxOffset));
1705
+ }
1706
+ if (key.upArrow || input === "k") {
1707
+ setIsAutoScrolling(false);
1708
+ setScrollOffset((prev) => Math.max(prev - 1, 0));
1709
+ }
1710
+ if (input === "G") {
1711
+ setIsAutoScrolling(true);
1712
+ setScrollOffset(maxOffset);
1713
+ }
1714
+ if (input === "g") {
1715
+ setIsAutoScrolling(false);
1716
+ setScrollOffset(0);
1717
+ }
1718
+ });
1719
+ const visibleContent = useMemo(() => {
1720
+ return lines.slice(scrollOffset, scrollOffset + visibleLines);
1721
+ }, [lines, scrollOffset, visibleLines]);
1722
+ const showScrollUp = scrollOffset > 0;
1723
+ const showScrollDown = scrollOffset < maxOffset;
1724
+ return /* @__PURE__ */ jsxDEV14(Box11, {
1725
+ flexDirection: "column",
1726
+ children: [
1727
+ showScrollHint && showScrollUp && /* @__PURE__ */ jsxDEV14(Text10, {
1728
+ dimColor: true,
1729
+ children: [
1730
+ " ↑ ",
1731
+ scrollOffset,
1732
+ " more line",
1733
+ scrollOffset !== 1 ? "s" : ""
1734
+ ]
1735
+ }, undefined, true, undefined, this),
1736
+ /* @__PURE__ */ jsxDEV14(Box11, {
1737
+ flexDirection: "column",
1738
+ height: visibleLines,
1739
+ overflow: "hidden",
1740
+ children: visibleContent.map((line, i) => /* @__PURE__ */ jsxDEV14(Text10, {
1741
+ children: line
1742
+ }, scrollOffset + i, false, undefined, this))
1743
+ }, undefined, false, undefined, this),
1744
+ showScrollHint && showScrollDown && /* @__PURE__ */ jsxDEV14(Text10, {
1745
+ dimColor: true,
1746
+ children: [
1747
+ " ↓ ",
1748
+ maxOffset - scrollOffset,
1749
+ " more line",
1750
+ maxOffset - scrollOffset !== 1 ? "s" : ""
1751
+ ]
1752
+ }, undefined, true, undefined, this),
1753
+ showScrollHint && totalLines > visibleLines && /* @__PURE__ */ jsxDEV14(Text10, {
1754
+ dimColor: true,
1755
+ children: [
1756
+ "j/k scroll • g top • G bottom ",
1757
+ isAutoScrolling ? "(auto-scroll)" : ""
1758
+ ]
1759
+ }, undefined, true, undefined, this)
1760
+ ]
1761
+ }, undefined, true, undefined, this);
1762
+ }
1763
+
1764
+ // src/components/PromptInput.tsx
1765
+ import { Box as Box12, Text as Text11, useInput as useInput7 } from "ink";
1766
+ import { jsxDEV as jsxDEV15 } from "react/jsx-dev-runtime";
1767
+ function PromptInput({
1768
+ question,
1769
+ options = ["y", "n"],
1770
+ onAnswer
1771
+ }) {
1772
+ useInput7((input) => {
1773
+ const lower = input.toLowerCase();
1774
+ if (options.includes(lower)) {
1775
+ onAnswer(lower);
1776
+ }
1777
+ });
1778
+ return /* @__PURE__ */ jsxDEV15(Box12, {
1779
+ marginTop: 1,
1780
+ borderStyle: "single",
1781
+ borderColor: colors.accent,
1782
+ paddingX: 1,
1783
+ children: /* @__PURE__ */ jsxDEV15(Text11, {
1784
+ children: [
1785
+ question,
1786
+ " ",
1787
+ /* @__PURE__ */ jsxDEV15(Text11, {
1788
+ color: colors.accent,
1789
+ children: [
1790
+ "[",
1791
+ options.join("/"),
1792
+ "]"
1793
+ ]
1794
+ }, undefined, true, undefined, this),
1795
+ /* @__PURE__ */ jsxDEV15(Text11, {
1796
+ dimColor: true,
1797
+ children: ": "
1798
+ }, undefined, false, undefined, this)
1799
+ ]
1800
+ }, undefined, true, undefined, this)
1801
+ }, undefined, false, undefined, this);
1243
1802
  }
1244
1803
 
1245
1804
  // src/cli/pkg-sync.ts
@@ -2025,287 +2584,46 @@ async function main3() {
2025
2584
  case "update": {
2026
2585
  console.log(`${colors4.cyan}Updating lockfile...${colors4.reset}`);
2027
2586
  const lock = await updateLockfile();
2028
- const total = Object.keys(lock.formulas).length + Object.keys(lock.casks).length;
2029
- console.log(`${colors4.green}Lockfile updated with ${total} packages.${colors4.reset}`);
2030
- break;
2031
- }
2032
- case "status":
2033
- await showStatus2();
2034
- break;
2035
- case "reset": {
2036
- console.log(`${colors4.cyan}Regenerating lockfile...${colors4.reset}`);
2037
- const lock = await generateLockfile();
2038
- await savePkgLock(lock);
2039
- const total = Object.keys(lock.formulas).length + Object.keys(lock.casks).length;
2040
- console.log(`${colors4.green}Lockfile regenerated with ${total} packages.${colors4.reset}`);
2041
- break;
2042
- }
2043
- case "show":
2044
- await showLockfile();
2045
- break;
2046
- default:
2047
- console.error(`${colors4.red}Unknown command: ${command}${colors4.reset}`);
2048
- printUsage3();
2049
- process.exit(1);
2050
- }
2051
- }
2052
- var isMainModule3 = process.argv[1]?.includes("pkg-lock");
2053
- if (isMainModule3) {
2054
- main3().catch(console.error);
2055
- }
2056
-
2057
- // src/cli/set-theme.ts
2058
- import { parseArgs as parseArgs4 } from "util";
2059
- import { readdirSync as readdirSync4, existsSync as existsSync5, rmSync, symlinkSync, unlinkSync } from "fs";
2060
- import { join as join4 } from "path";
2061
- var colors5 = {
2062
- red: "\x1B[0;31m",
2063
- green: "\x1B[0;32m",
2064
- blue: "\x1B[0;34m",
2065
- yellow: "\x1B[1;33m",
2066
- cyan: "\x1B[0;36m",
2067
- dim: "\x1B[2m",
2068
- reset: "\x1B[0m"
2069
- };
2070
- async function listThemes() {
2071
- await ensureConfigDir();
2072
- if (!existsSync5(THEMES_DIR)) {
2073
- return [];
2074
- }
2075
- const entries = readdirSync4(THEMES_DIR, { withFileTypes: true });
2076
- const themes = [];
2077
- for (const entry of entries) {
2078
- if (entry.isDirectory()) {
2079
- const themePath = join4(THEMES_DIR, entry.name);
2080
- const theme = await parseTheme(themePath, entry.name);
2081
- themes.push(theme);
2082
- }
2083
- }
2084
- return themes;
2085
- }
2086
- function clearDirectory(dir) {
2087
- if (existsSync5(dir)) {
2088
- const entries = readdirSync4(dir, { withFileTypes: true });
2089
- for (const entry of entries) {
2090
- const fullPath = join4(dir, entry.name);
2091
- if (entry.isSymbolicLink() || entry.isFile()) {
2092
- unlinkSync(fullPath);
2093
- } else if (entry.isDirectory()) {
2094
- rmSync(fullPath, { recursive: true, force: true });
2095
- }
2096
- }
2097
- }
2098
- }
2099
- function createSymlink(source, target) {
2100
- if (existsSync5(target)) {
2101
- unlinkSync(target);
2102
- }
2103
- symlinkSync(source, target);
2104
- }
2105
- async function applyTheme(themeName) {
2106
- const themeDir = join4(THEMES_DIR, themeName);
2107
- if (!existsSync5(themeDir)) {
2108
- return { output: `Theme '${themeName}' not found`, success: false };
2109
- }
2110
- await ensureConfigDir();
2111
- await ensureDir2(THEME_TARGET_DIR);
2112
- const theme = await parseTheme(themeDir, themeName);
2113
- clearDirectory(THEME_TARGET_DIR);
2114
- if (existsSync5(BACKGROUNDS_TARGET_DIR)) {
2115
- rmSync(BACKGROUNDS_TARGET_DIR, { recursive: true, force: true });
2116
- }
2117
- const entries = readdirSync4(themeDir, { withFileTypes: true });
2118
- for (const entry of entries) {
2119
- const source = join4(themeDir, entry.name);
2120
- if (entry.isFile() && entry.name !== "theme.yaml" && entry.name !== "light.mode") {
2121
- const target = join4(THEME_TARGET_DIR, entry.name);
2122
- createSymlink(source, target);
2123
- }
2124
- }
2125
- if (theme.hasBackgrounds) {
2126
- const backgroundsSource = join4(themeDir, "backgrounds");
2127
- createSymlink(backgroundsSource, BACKGROUNDS_TARGET_DIR);
2128
- }
2129
- let output = `Theme '${theme.name}' applied successfully`;
2130
- if (theme.metadata?.author) {
2131
- output += `
2132
- Author: ${theme.metadata.author}`;
2133
- }
2134
- if (theme.hasBackgrounds) {
2135
- output += `
2136
- Wallpapers available at: ~/.config/formalconf/current/backgrounds/`;
2137
- }
2138
- if (theme.isLightMode) {
2139
- output += `
2140
- Note: This is a light mode theme`;
2141
- }
2142
- return { output, success: true };
2143
- }
2144
- async function showThemeInfo(themeName) {
2145
- const themeDir = join4(THEMES_DIR, themeName);
2146
- if (!existsSync5(themeDir)) {
2147
- console.error(`${colors5.red}Error: Theme '${themeName}' not found${colors5.reset}`);
2148
- process.exit(1);
2149
- }
2150
- const theme = await parseTheme(themeDir, themeName);
2151
- console.log(`
2152
- ${colors5.cyan}Theme: ${theme.name}${colors5.reset}`);
2153
- if (theme.metadata) {
2154
- if (theme.metadata.author)
2155
- console.log(`Author: ${theme.metadata.author}`);
2156
- if (theme.metadata.description)
2157
- console.log(`Description: ${theme.metadata.description}`);
2158
- if (theme.metadata.version)
2159
- console.log(`Version: ${theme.metadata.version}`);
2160
- if (theme.metadata.source)
2161
- console.log(`Source: ${theme.metadata.source}`);
2162
- }
2163
- console.log(`
2164
- Files (${theme.files.length}):`);
2165
- for (const file of theme.files) {
2166
- console.log(` ${colors5.blue}•${colors5.reset} ${file.name}`);
2167
- }
2168
- if (theme.hasBackgrounds) {
2169
- console.log(`
2170
- ${colors5.green}Has wallpapers${colors5.reset}`);
2171
- }
2172
- if (theme.hasPreview) {
2173
- console.log(`${colors5.green}Has preview image${colors5.reset}`);
2174
- }
2175
- if (theme.isLightMode) {
2176
- console.log(`${colors5.yellow}Light mode theme${colors5.reset}`);
2177
- }
2178
- }
2179
- async function runSetTheme(themeName) {
2180
- return applyTheme(themeName);
2181
- }
2182
- async function main4() {
2183
- const { positionals, values } = parseArgs4({
2184
- args: process.argv.slice(2),
2185
- options: {
2186
- info: { type: "boolean", short: "i" }
2187
- },
2188
- allowPositionals: true
2189
- });
2190
- const [themeName] = positionals;
2191
- if (!themeName) {
2192
- const themes = await listThemes();
2193
- if (themes.length === 0) {
2194
- console.log(`${colors5.yellow}No themes available.${colors5.reset}`);
2195
- console.log(`This system is compatible with omarchy themes.`);
2196
- console.log(`
2197
- Add themes to: ${colors5.cyan}~/.config/formalconf/themes/${colors5.reset}`);
2198
- process.exit(0);
2199
- }
2200
- console.log(`${colors5.cyan}Usage: formalconf theme <theme-name>${colors5.reset}`);
2201
- console.log(` formalconf theme --info <theme-name>
2202
- `);
2203
- console.log("Available themes:");
2204
- for (const theme of themes) {
2205
- const extras = [];
2206
- if (theme.hasBackgrounds)
2207
- extras.push("wallpapers");
2208
- if (theme.isLightMode)
2209
- extras.push("light");
2210
- const suffix = extras.length ? ` ${colors5.dim}(${extras.join(", ")})${colors5.reset}` : "";
2211
- console.log(` ${colors5.blue}•${colors5.reset} ${theme.name}${suffix}`);
2212
- }
2213
- process.exit(0);
2214
- }
2215
- if (values.info) {
2216
- await showThemeInfo(themeName);
2217
- } else {
2218
- const result = await applyTheme(themeName);
2219
- console.log(result.success ? `${colors5.green}${result.output}${colors5.reset}` : `${colors5.red}${result.output}${colors5.reset}`);
2220
- }
2221
- }
2222
- var isMainModule4 = process.argv[1]?.includes("set-theme");
2223
- if (isMainModule4) {
2224
- main4().catch(console.error);
2225
- }
2226
-
2227
- // src/cli/formalconf.tsx
2228
- import { jsxDEV as jsxDEV12 } from "react/jsx-dev-runtime";
2229
- function MainMenu({ onSelect }) {
2230
- const { exit } = useApp();
2231
- return /* @__PURE__ */ jsxDEV12(Panel, {
2232
- title: "Main Menu",
2233
- children: /* @__PURE__ */ jsxDEV12(VimSelect, {
2234
- options: [
2235
- { label: "Config Manager", value: "config" },
2236
- { label: "Package Sync", value: "packages" },
2237
- { label: "Set Theme", value: "themes" },
2238
- { label: "Exit", value: "exit" }
2239
- ],
2240
- onChange: (value) => {
2241
- if (value === "exit") {
2242
- exit();
2243
- return;
2244
- }
2245
- onSelect(value);
2246
- }
2247
- }, undefined, false, undefined, this)
2248
- }, undefined, false, undefined, this);
2249
- }
2250
- function ConfigMenu({ onBack }) {
2251
- const [state, setState] = useState5("menu");
2252
- const [output, setOutput] = useState5("");
2253
- const [success, setSuccess] = useState5(true);
2254
- useInput5((input, key) => {
2255
- if (state === "menu" && (key.escape || key.leftArrow || input === "h")) {
2256
- onBack();
2587
+ const total = Object.keys(lock.formulas).length + Object.keys(lock.casks).length;
2588
+ console.log(`${colors4.green}Lockfile updated with ${total} packages.${colors4.reset}`);
2589
+ break;
2257
2590
  }
2258
- });
2259
- const handleAction = async (action) => {
2260
- if (action === "back") {
2261
- onBack();
2262
- return;
2591
+ case "status":
2592
+ await showStatus2();
2593
+ break;
2594
+ case "reset": {
2595
+ console.log(`${colors4.cyan}Regenerating lockfile...${colors4.reset}`);
2596
+ const lock = await generateLockfile();
2597
+ await savePkgLock(lock);
2598
+ const total = Object.keys(lock.formulas).length + Object.keys(lock.casks).length;
2599
+ console.log(`${colors4.green}Lockfile regenerated with ${total} packages.${colors4.reset}`);
2600
+ break;
2263
2601
  }
2264
- setState("running");
2265
- const result = await runConfigManager([action]);
2266
- setOutput(result.output);
2267
- setSuccess(result.success);
2268
- setState("result");
2269
- };
2270
- if (state === "running") {
2271
- return /* @__PURE__ */ jsxDEV12(Panel, {
2272
- title: "Config Manager",
2273
- children: /* @__PURE__ */ jsxDEV12(Spinner, {
2274
- label: "Processing..."
2275
- }, undefined, false, undefined, this)
2276
- }, undefined, false, undefined, this);
2277
- }
2278
- if (state === "result") {
2279
- return /* @__PURE__ */ jsxDEV12(CommandOutput, {
2280
- title: "Config Manager",
2281
- output,
2282
- success,
2283
- onDismiss: () => setState("menu")
2284
- }, undefined, false, undefined, this);
2602
+ case "show":
2603
+ await showLockfile();
2604
+ break;
2605
+ default:
2606
+ console.error(`${colors4.red}Unknown command: ${command}${colors4.reset}`);
2607
+ printUsage3();
2608
+ process.exit(1);
2285
2609
  }
2286
- return /* @__PURE__ */ jsxDEV12(Panel, {
2287
- title: "Config Manager",
2288
- children: /* @__PURE__ */ jsxDEV12(VimSelect, {
2289
- options: [
2290
- { label: "Stow all packages", value: "stow-all" },
2291
- { label: "Unstow all packages", value: "unstow-all" },
2292
- { label: "Check status", value: "status" },
2293
- { label: "List packages", value: "list" },
2294
- { label: "Back", value: "back" }
2295
- ],
2296
- onChange: handleAction
2297
- }, undefined, false, undefined, this)
2298
- }, undefined, false, undefined, this);
2299
2610
  }
2611
+ var isMainModule3 = process.argv[1]?.includes("pkg-lock");
2612
+ if (isMainModule3) {
2613
+ main3().catch(console.error);
2614
+ }
2615
+
2616
+ // src/components/menus/PackageMenu.tsx
2617
+ import { jsxDEV as jsxDEV16 } from "react/jsx-dev-runtime";
2300
2618
  function PackageMenu({ onBack }) {
2301
- const [state, setState] = useState5("menu");
2302
- const [lines, setLines] = useState5([]);
2303
- const [output, setOutput] = useState5("");
2304
- const [isStreamingOp, setIsStreamingOp] = useState5(true);
2305
- const [pendingPrompt, setPendingPrompt] = useState5(null);
2306
- const [success, setSuccess] = useState5(true);
2619
+ const [state, setState] = useState7("menu");
2620
+ const [lines, setLines] = useState7([]);
2621
+ const [output, setOutput] = useState7("");
2622
+ const [isStreamingOp, setIsStreamingOp] = useState7(true);
2623
+ const [pendingPrompt, setPendingPrompt] = useState7(null);
2624
+ const [success, setSuccess] = useState7(true);
2307
2625
  const isRunningRef = useRef(false);
2308
- useInput5((input, key) => {
2626
+ useInput8((input, key) => {
2309
2627
  if (state === "menu" && (key.escape || key.leftArrow || input === "h")) {
2310
2628
  onBack();
2311
2629
  }
@@ -2324,7 +2642,7 @@ function PackageMenu({ onBack }) {
2324
2642
  });
2325
2643
  }
2326
2644
  }), []);
2327
- const handlePromptAnswer = useCallback((answer) => {
2645
+ const handlePromptAnswer = useCallback2((answer) => {
2328
2646
  if (pendingPrompt) {
2329
2647
  setLines((prev) => [...prev, `> ${answer}`]);
2330
2648
  pendingPrompt.resolve(answer);
@@ -2382,20 +2700,17 @@ function PackageMenu({ onBack }) {
2382
2700
  };
2383
2701
  if (state === "running") {
2384
2702
  if (!isStreamingOp) {
2385
- return /* @__PURE__ */ jsxDEV12(Panel, {
2386
- title: "Package Sync",
2387
- children: /* @__PURE__ */ jsxDEV12(Spinner, {
2388
- label: "Processing..."
2389
- }, undefined, false, undefined, this)
2703
+ return /* @__PURE__ */ jsxDEV16(LoadingPanel, {
2704
+ title: "Package Sync"
2390
2705
  }, undefined, false, undefined, this);
2391
2706
  }
2392
- return /* @__PURE__ */ jsxDEV12(Panel, {
2707
+ return /* @__PURE__ */ jsxDEV16(Panel, {
2393
2708
  title: "Package Sync",
2394
2709
  children: [
2395
- /* @__PURE__ */ jsxDEV12(ScrollableLog, {
2710
+ /* @__PURE__ */ jsxDEV16(ScrollableLog, {
2396
2711
  lines
2397
2712
  }, undefined, false, undefined, this),
2398
- pendingPrompt && /* @__PURE__ */ jsxDEV12(PromptInput, {
2713
+ pendingPrompt && /* @__PURE__ */ jsxDEV16(PromptInput, {
2399
2714
  question: pendingPrompt.question,
2400
2715
  options: pendingPrompt.options,
2401
2716
  onAnswer: handlePromptAnswer
@@ -2405,126 +2720,435 @@ function PackageMenu({ onBack }) {
2405
2720
  }
2406
2721
  if (state === "result") {
2407
2722
  if (!isStreamingOp) {
2408
- return /* @__PURE__ */ jsxDEV12(CommandOutput, {
2723
+ return /* @__PURE__ */ jsxDEV16(CommandOutput, {
2409
2724
  title: "Package Sync",
2410
2725
  output,
2411
2726
  success,
2412
2727
  onDismiss: () => setState("menu")
2413
2728
  }, undefined, false, undefined, this);
2414
2729
  }
2415
- return /* @__PURE__ */ jsxDEV12(Panel, {
2416
- title: "Package Sync",
2417
- borderColor: success ? colors.success : colors.error,
2418
- children: [
2419
- /* @__PURE__ */ jsxDEV12(ScrollableLog, {
2420
- lines,
2421
- autoScroll: false
2422
- }, undefined, false, undefined, this),
2423
- /* @__PURE__ */ jsxDEV12(Box12, {
2424
- marginTop: 1,
2425
- children: /* @__PURE__ */ jsxDEV12(Text11, {
2426
- color: success ? colors.success : colors.error,
2427
- children: success ? "Done" : "Failed"
2428
- }, undefined, false, undefined, this)
2429
- }, undefined, false, undefined, this),
2430
- /* @__PURE__ */ jsxDEV12(Text11, {
2431
- dimColor: true,
2432
- children: "Press any key to continue..."
2433
- }, undefined, false, undefined, this)
2434
- ]
2435
- }, undefined, true, undefined, this);
2730
+ return /* @__PURE__ */ jsxDEV16(Panel, {
2731
+ title: "Package Sync",
2732
+ borderColor: success ? colors.success : colors.error,
2733
+ children: [
2734
+ /* @__PURE__ */ jsxDEV16(ScrollableLog, {
2735
+ lines,
2736
+ autoScroll: false
2737
+ }, undefined, false, undefined, this),
2738
+ /* @__PURE__ */ jsxDEV16(Box13, {
2739
+ marginTop: 1,
2740
+ children: /* @__PURE__ */ jsxDEV16(Text12, {
2741
+ color: success ? colors.success : colors.error,
2742
+ children: success ? "Done" : "Failed"
2743
+ }, undefined, false, undefined, this)
2744
+ }, undefined, false, undefined, this),
2745
+ /* @__PURE__ */ jsxDEV16(Text12, {
2746
+ dimColor: true,
2747
+ children: "Press any key to continue..."
2748
+ }, undefined, false, undefined, this)
2749
+ ]
2750
+ }, undefined, true, undefined, this);
2751
+ }
2752
+ return /* @__PURE__ */ jsxDEV16(Panel, {
2753
+ title: "Package Sync",
2754
+ children: /* @__PURE__ */ jsxDEV16(VimSelect, {
2755
+ options: [
2756
+ { label: "Sync packages", value: "sync" },
2757
+ { label: "Sync with purge", value: "sync-purge" },
2758
+ { label: "Upgrade all (with verification)", value: "upgrade" },
2759
+ { label: "Upgrade interactive", value: "upgrade-interactive" },
2760
+ { label: "Update lockfile", value: "lock-update" },
2761
+ { label: "Lockfile status", value: "lock-status" },
2762
+ { label: "Back", value: "back" }
2763
+ ],
2764
+ onChange: handleAction
2765
+ }, undefined, false, undefined, this)
2766
+ }, undefined, false, undefined, this);
2767
+ }
2768
+
2769
+ // src/components/menus/ThemeMenu.tsx
2770
+ import { useState as useState9, useEffect as useEffect5, useMemo as useMemo4 } from "react";
2771
+ import { Box as Box15, Text as Text14 } from "ink";
2772
+ import { existsSync as existsSync7, readdirSync as readdirSync5 } from "fs";
2773
+ import { join as join6 } from "path";
2774
+
2775
+ // src/components/ThemeCard.tsx
2776
+ import { Box as Box14, Text as Text13 } from "ink";
2777
+ import { jsxDEV as jsxDEV17 } from "react/jsx-dev-runtime";
2778
+ function ThemeCard({ theme, isSelected, width }) {
2779
+ const borderColor = isSelected ? colors.accent : colors.border;
2780
+ const nameColor = isSelected ? colors.primary : colors.text;
2781
+ const indicators = [];
2782
+ if (theme.hasBackgrounds)
2783
+ indicators.push("bg");
2784
+ if (theme.isLightMode)
2785
+ indicators.push("light");
2786
+ const indicatorText = indicators.length > 0 ? ` [${indicators.join(" ")}]` : "";
2787
+ return /* @__PURE__ */ jsxDEV17(Box14, {
2788
+ flexDirection: "column",
2789
+ width,
2790
+ borderStyle: borderStyles.panel,
2791
+ borderColor,
2792
+ paddingX: 1,
2793
+ children: /* @__PURE__ */ jsxDEV17(Box14, {
2794
+ children: [
2795
+ /* @__PURE__ */ jsxDEV17(Text13, {
2796
+ color: isSelected ? colors.accent : colors.primaryDim,
2797
+ children: isSelected ? "● " : " "
2798
+ }, undefined, false, undefined, this),
2799
+ /* @__PURE__ */ jsxDEV17(Text13, {
2800
+ color: nameColor,
2801
+ bold: true,
2802
+ wrap: "truncate",
2803
+ children: theme.name
2804
+ }, undefined, false, undefined, this),
2805
+ /* @__PURE__ */ jsxDEV17(Text13, {
2806
+ color: colors.primaryDim,
2807
+ children: indicatorText
2808
+ }, undefined, false, undefined, this)
2809
+ ]
2810
+ }, undefined, true, undefined, this)
2811
+ }, undefined, false, undefined, this);
2812
+ }
2813
+
2814
+ // src/hooks/useThemeGrid.ts
2815
+ import { useState as useState8, useEffect as useEffect4 } from "react";
2816
+ import { useInput as useInput9 } from "ink";
2817
+ function useThemeGrid({
2818
+ itemCount,
2819
+ cardHeight = 3,
2820
+ layoutOverhead = 20,
2821
+ minCardWidth = 28,
2822
+ onSelect,
2823
+ onBack,
2824
+ enabled = true
2825
+ }) {
2826
+ const { columns, rows } = useTerminalSize();
2827
+ const [selectedIndex, setSelectedIndex] = useState8(0);
2828
+ const [scrollOffset, setScrollOffset] = useState8(0);
2829
+ const availableWidth = columns - 6;
2830
+ const cardsPerRow = Math.max(1, Math.floor(availableWidth / minCardWidth));
2831
+ const cardWidth = Math.floor(availableWidth / cardsPerRow);
2832
+ const availableHeight = rows - layoutOverhead;
2833
+ const visibleRows = Math.max(1, Math.floor(availableHeight / cardHeight));
2834
+ const selectedRow = Math.floor(selectedIndex / cardsPerRow);
2835
+ const totalRows = Math.ceil(itemCount / cardsPerRow);
2836
+ useEffect4(() => {
2837
+ if (selectedRow < scrollOffset) {
2838
+ setScrollOffset(selectedRow);
2839
+ } else if (selectedRow >= scrollOffset + visibleRows) {
2840
+ setScrollOffset(selectedRow - visibleRows + 1);
2841
+ }
2842
+ }, [selectedRow, scrollOffset, visibleRows]);
2843
+ useInput9((input, key) => {
2844
+ if (!enabled)
2845
+ return;
2846
+ if (key.escape && onBack) {
2847
+ onBack();
2848
+ return;
2849
+ }
2850
+ if (key.rightArrow || input === "l") {
2851
+ if (selectedIndex < itemCount - 1) {
2852
+ setSelectedIndex((i) => i + 1);
2853
+ }
2854
+ }
2855
+ if (key.leftArrow || input === "h") {
2856
+ if (selectedIndex > 0) {
2857
+ setSelectedIndex((i) => i - 1);
2858
+ }
2859
+ }
2860
+ if (key.downArrow || input === "j") {
2861
+ const nextIndex = selectedIndex + cardsPerRow;
2862
+ if (nextIndex < itemCount) {
2863
+ setSelectedIndex(nextIndex);
2864
+ }
2865
+ }
2866
+ if (key.upArrow || input === "k") {
2867
+ const prevIndex = selectedIndex - cardsPerRow;
2868
+ if (prevIndex >= 0) {
2869
+ setSelectedIndex(prevIndex);
2870
+ }
2871
+ }
2872
+ if (key.return && onSelect) {
2873
+ onSelect(selectedIndex);
2874
+ }
2875
+ });
2876
+ const visibleStartIndex = scrollOffset * cardsPerRow;
2877
+ const visibleEndIndex = (scrollOffset + visibleRows) * cardsPerRow;
2878
+ return {
2879
+ cardsPerRow,
2880
+ cardWidth,
2881
+ visibleRows,
2882
+ scrollOffset,
2883
+ selectedIndex,
2884
+ visibleStartIndex,
2885
+ visibleEndIndex,
2886
+ showScrollUp: scrollOffset > 0,
2887
+ showScrollDown: scrollOffset + visibleRows < totalRows,
2888
+ gridHeight: visibleRows * cardHeight,
2889
+ totalRows
2890
+ };
2891
+ }
2892
+
2893
+ // src/lib/theme-parser.ts
2894
+ import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
2895
+ import { join as join4 } from "path";
2896
+ function parseYaml(content) {
2897
+ const result = {};
2898
+ const lines = content.split(`
2899
+ `);
2900
+ let currentSection = null;
2901
+ let currentKey = "";
2902
+ for (const line of lines) {
2903
+ const trimmed = line.trim();
2904
+ if (!trimmed || trimmed.startsWith("#"))
2905
+ continue;
2906
+ const indentLevel = line.search(/\S/);
2907
+ const match = trimmed.match(/^([\w-]+):\s*(.*)$/);
2908
+ if (match) {
2909
+ const [, key, value] = match;
2910
+ if (indentLevel === 0) {
2911
+ if (value) {
2912
+ result[key] = value.replace(/^["']|["']$/g, "");
2913
+ } else {
2914
+ currentKey = key;
2915
+ currentSection = {};
2916
+ result[key] = currentSection;
2917
+ }
2918
+ } else if (currentSection) {
2919
+ currentSection[key] = value.replace(/^["']|["']$/g, "");
2920
+ }
2921
+ }
2922
+ }
2923
+ return result;
2924
+ }
2925
+ async function parseThemeMetadata(themePath) {
2926
+ const yamlPath = join4(themePath, "theme.yaml");
2927
+ if (!existsSync5(yamlPath)) {
2928
+ return;
2929
+ }
2930
+ try {
2931
+ const content = await readText(yamlPath);
2932
+ const parsed = parseYaml(content);
2933
+ return {
2934
+ name: parsed.name || "",
2935
+ author: parsed.author,
2936
+ description: parsed.description,
2937
+ version: parsed.version,
2938
+ source: parsed.source,
2939
+ colors: parsed.colors
2940
+ };
2941
+ } catch {
2942
+ return;
2943
+ }
2944
+ }
2945
+ function parseThemeFiles(themePath) {
2946
+ const entries = readdirSync3(themePath, { withFileTypes: true });
2947
+ return entries.filter((e) => e.isFile() && !e.name.startsWith(".") && e.name !== "theme.yaml" && e.name !== "light.mode").map((e) => ({
2948
+ name: e.name,
2949
+ path: join4(themePath, e.name),
2950
+ application: e.name.replace(/\.(conf|theme|lua|toml|css|json|ini)$/, "")
2951
+ }));
2952
+ }
2953
+ async function parseTheme(themePath, themeName) {
2954
+ const files = parseThemeFiles(themePath);
2955
+ const metadata = await parseThemeMetadata(themePath);
2956
+ return {
2957
+ name: metadata?.name || themeName,
2958
+ path: themePath,
2959
+ files,
2960
+ metadata,
2961
+ hasBackgrounds: existsSync5(join4(themePath, "backgrounds")),
2962
+ hasPreview: existsSync5(join4(themePath, "preview.png")),
2963
+ isLightMode: existsSync5(join4(themePath, "light.mode"))
2964
+ };
2965
+ }
2966
+
2967
+ // src/cli/set-theme.ts
2968
+ import { parseArgs as parseArgs4 } from "util";
2969
+ import { readdirSync as readdirSync4, existsSync as existsSync6, rmSync, symlinkSync, unlinkSync } from "fs";
2970
+ import { join as join5 } from "path";
2971
+ var colors5 = {
2972
+ red: "\x1B[0;31m",
2973
+ green: "\x1B[0;32m",
2974
+ blue: "\x1B[0;34m",
2975
+ yellow: "\x1B[1;33m",
2976
+ cyan: "\x1B[0;36m",
2977
+ dim: "\x1B[2m",
2978
+ reset: "\x1B[0m"
2979
+ };
2980
+ async function listThemes() {
2981
+ await ensureConfigDir();
2982
+ if (!existsSync6(THEMES_DIR)) {
2983
+ return [];
2984
+ }
2985
+ const entries = readdirSync4(THEMES_DIR, { withFileTypes: true });
2986
+ const themes = [];
2987
+ for (const entry of entries) {
2988
+ if (entry.isDirectory()) {
2989
+ const themePath = join5(THEMES_DIR, entry.name);
2990
+ const theme = await parseTheme(themePath, entry.name);
2991
+ themes.push(theme);
2992
+ }
2436
2993
  }
2437
- return /* @__PURE__ */ jsxDEV12(Panel, {
2438
- title: "Package Sync",
2439
- children: /* @__PURE__ */ jsxDEV12(VimSelect, {
2440
- options: [
2441
- { label: "Sync packages", value: "sync" },
2442
- { label: "Sync with purge", value: "sync-purge" },
2443
- { label: "Upgrade all (with verification)", value: "upgrade" },
2444
- { label: "Upgrade interactive", value: "upgrade-interactive" },
2445
- { label: "Update lockfile", value: "lock-update" },
2446
- { label: "Lockfile status", value: "lock-status" },
2447
- { label: "Back", value: "back" }
2448
- ],
2449
- onChange: handleAction
2450
- }, undefined, false, undefined, this)
2451
- }, undefined, false, undefined, this);
2994
+ return themes;
2452
2995
  }
2453
- function ThemeMenu({ onBack }) {
2454
- const [themes, setThemes] = useState5([]);
2455
- const [loading, setLoading] = useState5(true);
2456
- const [selectedIndex, setSelectedIndex] = useState5(0);
2457
- const [state, setState] = useState5("menu");
2458
- const [output, setOutput] = useState5("");
2459
- const [success, setSuccess] = useState5(true);
2460
- const { columns, rows } = useTerminalSize();
2461
- const CARD_HEIGHT = 3;
2462
- const LAYOUT_OVERHEAD = 20;
2463
- const cardWidth = useMemo2(() => {
2464
- const availableWidth = columns - 6;
2465
- const cardsPerRow2 = Math.max(1, Math.floor(availableWidth / 28));
2466
- return Math.floor(availableWidth / cardsPerRow2);
2467
- }, [columns]);
2468
- const cardsPerRow = useMemo2(() => {
2469
- const availableWidth = columns - 6;
2470
- return Math.max(1, Math.floor(availableWidth / 28));
2471
- }, [columns]);
2472
- const visibleRows = useMemo2(() => {
2473
- const availableHeight = rows - LAYOUT_OVERHEAD;
2474
- return Math.max(1, Math.floor(availableHeight / CARD_HEIGHT));
2475
- }, [rows]);
2476
- const selectedRow = Math.floor(selectedIndex / cardsPerRow);
2477
- const totalRows = Math.ceil(themes.length / cardsPerRow);
2478
- const [scrollOffset, setScrollOffset] = useState5(0);
2479
- useEffect4(() => {
2480
- if (selectedRow < scrollOffset) {
2481
- setScrollOffset(selectedRow);
2482
- } else if (selectedRow >= scrollOffset + visibleRows) {
2483
- setScrollOffset(selectedRow - visibleRows + 1);
2484
- }
2485
- }, [selectedRow, scrollOffset, visibleRows]);
2486
- const visibleThemes = useMemo2(() => {
2487
- const startIdx = scrollOffset * cardsPerRow;
2488
- const endIdx = (scrollOffset + visibleRows) * cardsPerRow;
2489
- return themes.slice(startIdx, endIdx);
2490
- }, [themes, scrollOffset, visibleRows, cardsPerRow]);
2491
- const visibleStartIndex = scrollOffset * cardsPerRow;
2492
- useInput5((input, key) => {
2493
- if (state !== "menu" || loading)
2494
- return;
2495
- if (key.escape) {
2496
- onBack();
2497
- return;
2498
- }
2499
- if (key.rightArrow || input === "l") {
2500
- if (selectedIndex < themes.length - 1) {
2501
- setSelectedIndex((i) => i + 1);
2502
- }
2503
- }
2504
- if (key.leftArrow || input === "h") {
2505
- if (selectedIndex > 0) {
2506
- setSelectedIndex((i) => i - 1);
2996
+ function clearDirectory(dir) {
2997
+ if (existsSync6(dir)) {
2998
+ const entries = readdirSync4(dir, { withFileTypes: true });
2999
+ for (const entry of entries) {
3000
+ const fullPath = join5(dir, entry.name);
3001
+ if (entry.isSymbolicLink() || entry.isFile()) {
3002
+ unlinkSync(fullPath);
3003
+ } else if (entry.isDirectory()) {
3004
+ rmSync(fullPath, { recursive: true, force: true });
2507
3005
  }
2508
3006
  }
2509
- if (key.downArrow || input === "j") {
2510
- const nextIndex = selectedIndex + cardsPerRow;
2511
- if (nextIndex < themes.length) {
2512
- setSelectedIndex(nextIndex);
2513
- }
3007
+ }
3008
+ }
3009
+ function createSymlink(source, target) {
3010
+ if (existsSync6(target)) {
3011
+ unlinkSync(target);
3012
+ }
3013
+ symlinkSync(source, target);
3014
+ }
3015
+ async function applyTheme(themeName) {
3016
+ const themeDir = join5(THEMES_DIR, themeName);
3017
+ if (!existsSync6(themeDir)) {
3018
+ return { output: `Theme '${themeName}' not found`, success: false };
3019
+ }
3020
+ await ensureConfigDir();
3021
+ await ensureDir2(THEME_TARGET_DIR);
3022
+ const theme = await parseTheme(themeDir, themeName);
3023
+ clearDirectory(THEME_TARGET_DIR);
3024
+ if (existsSync6(BACKGROUNDS_TARGET_DIR)) {
3025
+ rmSync(BACKGROUNDS_TARGET_DIR, { recursive: true, force: true });
3026
+ }
3027
+ const entries = readdirSync4(themeDir, { withFileTypes: true });
3028
+ for (const entry of entries) {
3029
+ const source = join5(themeDir, entry.name);
3030
+ if (entry.isFile() && entry.name !== "theme.yaml" && entry.name !== "light.mode") {
3031
+ const target = join5(THEME_TARGET_DIR, entry.name);
3032
+ createSymlink(source, target);
2514
3033
  }
2515
- if (key.upArrow || input === "k") {
2516
- const prevIndex = selectedIndex - cardsPerRow;
2517
- if (prevIndex >= 0) {
2518
- setSelectedIndex(prevIndex);
2519
- }
3034
+ }
3035
+ if (theme.hasBackgrounds) {
3036
+ const backgroundsSource = join5(themeDir, "backgrounds");
3037
+ createSymlink(backgroundsSource, BACKGROUNDS_TARGET_DIR);
3038
+ }
3039
+ let output = `Theme '${theme.name}' applied successfully`;
3040
+ if (theme.metadata?.author) {
3041
+ output += `
3042
+ Author: ${theme.metadata.author}`;
3043
+ }
3044
+ if (theme.hasBackgrounds) {
3045
+ output += `
3046
+ Wallpapers available at: ~/.config/formalconf/current/backgrounds/`;
3047
+ }
3048
+ if (theme.isLightMode) {
3049
+ output += `
3050
+ Note: This is a light mode theme`;
3051
+ }
3052
+ return { output, success: true };
3053
+ }
3054
+ async function showThemeInfo(themeName) {
3055
+ const themeDir = join5(THEMES_DIR, themeName);
3056
+ if (!existsSync6(themeDir)) {
3057
+ console.error(`${colors5.red}Error: Theme '${themeName}' not found${colors5.reset}`);
3058
+ process.exit(1);
3059
+ }
3060
+ const theme = await parseTheme(themeDir, themeName);
3061
+ console.log(`
3062
+ ${colors5.cyan}Theme: ${theme.name}${colors5.reset}`);
3063
+ if (theme.metadata) {
3064
+ if (theme.metadata.author)
3065
+ console.log(`Author: ${theme.metadata.author}`);
3066
+ if (theme.metadata.description)
3067
+ console.log(`Description: ${theme.metadata.description}`);
3068
+ if (theme.metadata.version)
3069
+ console.log(`Version: ${theme.metadata.version}`);
3070
+ if (theme.metadata.source)
3071
+ console.log(`Source: ${theme.metadata.source}`);
3072
+ }
3073
+ console.log(`
3074
+ Files (${theme.files.length}):`);
3075
+ for (const file of theme.files) {
3076
+ console.log(` ${colors5.blue}•${colors5.reset} ${file.name}`);
3077
+ }
3078
+ if (theme.hasBackgrounds) {
3079
+ console.log(`
3080
+ ${colors5.green}Has wallpapers${colors5.reset}`);
3081
+ }
3082
+ if (theme.hasPreview) {
3083
+ console.log(`${colors5.green}Has preview image${colors5.reset}`);
3084
+ }
3085
+ if (theme.isLightMode) {
3086
+ console.log(`${colors5.yellow}Light mode theme${colors5.reset}`);
3087
+ }
3088
+ }
3089
+ async function runSetTheme(themeName) {
3090
+ return applyTheme(themeName);
3091
+ }
3092
+ async function main4() {
3093
+ const { positionals, values } = parseArgs4({
3094
+ args: process.argv.slice(2),
3095
+ options: {
3096
+ info: { type: "boolean", short: "i" }
3097
+ },
3098
+ allowPositionals: true
3099
+ });
3100
+ const [themeName] = positionals;
3101
+ if (!themeName) {
3102
+ const themes = await listThemes();
3103
+ if (themes.length === 0) {
3104
+ console.log(`${colors5.yellow}No themes available.${colors5.reset}`);
3105
+ console.log(`This system is compatible with omarchy themes.`);
3106
+ console.log(`
3107
+ Add themes to: ${colors5.cyan}~/.config/formalconf/themes/${colors5.reset}`);
3108
+ process.exit(0);
2520
3109
  }
2521
- if (key.return) {
2522
- applyTheme2(themes[selectedIndex]);
3110
+ console.log(`${colors5.cyan}Usage: formalconf theme <theme-name>${colors5.reset}`);
3111
+ console.log(` formalconf theme --info <theme-name>
3112
+ `);
3113
+ console.log("Available themes:");
3114
+ for (const theme of themes) {
3115
+ const extras = [];
3116
+ if (theme.hasBackgrounds)
3117
+ extras.push("wallpapers");
3118
+ if (theme.isLightMode)
3119
+ extras.push("light");
3120
+ const suffix = extras.length ? ` ${colors5.dim}(${extras.join(", ")})${colors5.reset}` : "";
3121
+ console.log(` ${colors5.blue}•${colors5.reset} ${theme.name}${suffix}`);
2523
3122
  }
3123
+ process.exit(0);
3124
+ }
3125
+ if (values.info) {
3126
+ await showThemeInfo(themeName);
3127
+ } else {
3128
+ const result = await applyTheme(themeName);
3129
+ console.log(result.success ? `${colors5.green}${result.output}${colors5.reset}` : `${colors5.red}${result.output}${colors5.reset}`);
3130
+ }
3131
+ }
3132
+ var isMainModule4 = process.argv[1]?.includes("set-theme");
3133
+ if (isMainModule4) {
3134
+ main4().catch(console.error);
3135
+ }
3136
+
3137
+ // src/components/menus/ThemeMenu.tsx
3138
+ import { jsxDEV as jsxDEV18 } from "react/jsx-dev-runtime";
3139
+ function ThemeMenu({ onBack }) {
3140
+ const [themes, setThemes] = useState9([]);
3141
+ const [loading, setLoading] = useState9(true);
3142
+ const { state, output, success, isRunning, isResult, execute, reset } = useMenuAction();
3143
+ const grid = useThemeGrid({
3144
+ itemCount: themes.length,
3145
+ onSelect: (index) => applyTheme2(themes[index]),
3146
+ onBack,
3147
+ enabled: state === "menu" && !loading && themes.length > 0
2524
3148
  });
2525
- useEffect4(() => {
3149
+ useEffect5(() => {
2526
3150
  async function loadThemes() {
2527
- if (!existsSync6(THEMES_DIR)) {
3151
+ if (!existsSync7(THEMES_DIR)) {
2528
3152
  setThemes([]);
2529
3153
  setLoading(false);
2530
3154
  return;
@@ -2533,7 +3157,7 @@ function ThemeMenu({ onBack }) {
2533
3157
  const loadedThemes = [];
2534
3158
  for (const entry of entries) {
2535
3159
  if (entry.isDirectory()) {
2536
- const themePath = join5(THEMES_DIR, entry.name);
3160
+ const themePath = join6(THEMES_DIR, entry.name);
2537
3161
  const theme = await parseTheme(themePath, entry.name);
2538
3162
  loadedThemes.push(theme);
2539
3163
  }
@@ -2544,52 +3168,49 @@ function ThemeMenu({ onBack }) {
2544
3168
  loadThemes();
2545
3169
  }, []);
2546
3170
  const applyTheme2 = async (theme) => {
2547
- setState("running");
2548
3171
  const themeName = theme.path.split("/").pop();
2549
- const result = await runSetTheme(themeName);
2550
- setOutput(result.output);
2551
- setSuccess(result.success);
2552
- setState("result");
3172
+ await execute(() => runSetTheme(themeName));
2553
3173
  };
2554
- if (loading || state === "running") {
2555
- return /* @__PURE__ */ jsxDEV12(Panel, {
3174
+ const visibleThemes = useMemo4(() => {
3175
+ return themes.slice(grid.visibleStartIndex, grid.visibleEndIndex);
3176
+ }, [themes, grid.visibleStartIndex, grid.visibleEndIndex]);
3177
+ if (loading || isRunning) {
3178
+ return /* @__PURE__ */ jsxDEV18(LoadingPanel, {
2556
3179
  title: "Select Theme",
2557
- children: /* @__PURE__ */ jsxDEV12(Spinner, {
2558
- label: loading ? "Loading themes..." : "Applying theme..."
2559
- }, undefined, false, undefined, this)
3180
+ label: loading ? "Loading themes..." : "Applying theme..."
2560
3181
  }, undefined, false, undefined, this);
2561
3182
  }
2562
- if (state === "result") {
2563
- return /* @__PURE__ */ jsxDEV12(CommandOutput, {
3183
+ if (isResult) {
3184
+ return /* @__PURE__ */ jsxDEV18(CommandOutput, {
2564
3185
  title: "Select Theme",
2565
3186
  output,
2566
3187
  success,
2567
- onDismiss: () => setState("menu")
3188
+ onDismiss: reset
2568
3189
  }, undefined, false, undefined, this);
2569
3190
  }
2570
3191
  if (themes.length === 0) {
2571
- return /* @__PURE__ */ jsxDEV12(Panel, {
3192
+ return /* @__PURE__ */ jsxDEV18(Panel, {
2572
3193
  title: "Select Theme",
2573
3194
  children: [
2574
- /* @__PURE__ */ jsxDEV12(Box12, {
3195
+ /* @__PURE__ */ jsxDEV18(Box15, {
2575
3196
  flexDirection: "column",
2576
3197
  children: [
2577
- /* @__PURE__ */ jsxDEV12(Text11, {
3198
+ /* @__PURE__ */ jsxDEV18(Text14, {
2578
3199
  color: colors.warning,
2579
3200
  children: "No themes available."
2580
3201
  }, undefined, false, undefined, this),
2581
- /* @__PURE__ */ jsxDEV12(Text11, {
3202
+ /* @__PURE__ */ jsxDEV18(Text14, {
2582
3203
  children: "This system is compatible with omarchy themes."
2583
3204
  }, undefined, false, undefined, this),
2584
- /* @__PURE__ */ jsxDEV12(Text11, {
3205
+ /* @__PURE__ */ jsxDEV18(Text14, {
2585
3206
  dimColor: true,
2586
3207
  children: "Add themes to ~/.config/formalconf/themes/"
2587
3208
  }, undefined, false, undefined, this)
2588
3209
  ]
2589
3210
  }, undefined, true, undefined, this),
2590
- /* @__PURE__ */ jsxDEV12(Box12, {
3211
+ /* @__PURE__ */ jsxDEV18(Box15, {
2591
3212
  marginTop: 1,
2592
- children: /* @__PURE__ */ jsxDEV12(VimSelect, {
3213
+ children: /* @__PURE__ */ jsxDEV18(VimSelect, {
2593
3214
  options: [{ label: "Back", value: "back" }],
2594
3215
  onChange: () => onBack()
2595
3216
  }, undefined, false, undefined, this)
@@ -2597,44 +3218,43 @@ function ThemeMenu({ onBack }) {
2597
3218
  ]
2598
3219
  }, undefined, true, undefined, this);
2599
3220
  }
2600
- const showScrollUp = scrollOffset > 0;
2601
- const showScrollDown = scrollOffset + visibleRows < totalRows;
2602
- const gridHeight = visibleRows * CARD_HEIGHT;
2603
- return /* @__PURE__ */ jsxDEV12(Panel, {
3221
+ return /* @__PURE__ */ jsxDEV18(Panel, {
2604
3222
  title: "Select Theme",
2605
3223
  children: [
2606
- showScrollUp && /* @__PURE__ */ jsxDEV12(Text11, {
3224
+ grid.showScrollUp && /* @__PURE__ */ jsxDEV18(Text14, {
2607
3225
  dimColor: true,
2608
3226
  children: [
2609
- " ",
2610
- scrollOffset,
3227
+ " ",
3228
+ "↑ ",
3229
+ grid.scrollOffset,
2611
3230
  " more row",
2612
- scrollOffset > 1 ? "s" : ""
3231
+ grid.scrollOffset > 1 ? "s" : ""
2613
3232
  ]
2614
3233
  }, undefined, true, undefined, this),
2615
- /* @__PURE__ */ jsxDEV12(Box12, {
3234
+ /* @__PURE__ */ jsxDEV18(Box15, {
2616
3235
  flexDirection: "row",
2617
3236
  flexWrap: "wrap",
2618
- height: gridHeight,
3237
+ height: grid.gridHeight,
2619
3238
  overflow: "hidden",
2620
- children: visibleThemes.map((theme, index) => /* @__PURE__ */ jsxDEV12(ThemeCard, {
3239
+ children: visibleThemes.map((theme, index) => /* @__PURE__ */ jsxDEV18(ThemeCard, {
2621
3240
  theme,
2622
- isSelected: visibleStartIndex + index === selectedIndex,
2623
- width: cardWidth
3241
+ isSelected: grid.visibleStartIndex + index === grid.selectedIndex,
3242
+ width: grid.cardWidth
2624
3243
  }, theme.path, false, undefined, this))
2625
3244
  }, undefined, false, undefined, this),
2626
- showScrollDown && /* @__PURE__ */ jsxDEV12(Text11, {
3245
+ grid.showScrollDown && /* @__PURE__ */ jsxDEV18(Text14, {
2627
3246
  dimColor: true,
2628
3247
  children: [
2629
- " ",
2630
- totalRows - scrollOffset - visibleRows,
3248
+ " ",
3249
+ "↓ ",
3250
+ grid.totalRows - grid.scrollOffset - grid.visibleRows,
2631
3251
  " more row",
2632
- totalRows - scrollOffset - visibleRows > 1 ? "s" : ""
3252
+ grid.totalRows - grid.scrollOffset - grid.visibleRows > 1 ? "s" : ""
2633
3253
  ]
2634
3254
  }, undefined, true, undefined, this),
2635
- /* @__PURE__ */ jsxDEV12(Box12, {
3255
+ /* @__PURE__ */ jsxDEV18(Box15, {
2636
3256
  marginTop: 1,
2637
- children: /* @__PURE__ */ jsxDEV12(Text11, {
3257
+ children: /* @__PURE__ */ jsxDEV18(Text14, {
2638
3258
  dimColor: true,
2639
3259
  children: "←→↑↓/hjkl navigate • Enter select • Esc back"
2640
3260
  }, undefined, false, undefined, this)
@@ -2642,122 +3262,81 @@ function ThemeMenu({ onBack }) {
2642
3262
  ]
2643
3263
  }, undefined, true, undefined, this);
2644
3264
  }
2645
- function PrerequisiteError({
2646
- missing,
2647
- onExit
2648
- }) {
2649
- useInput5(() => onExit());
2650
- return /* @__PURE__ */ jsxDEV12(Layout, {
2651
- breadcrumb: ["Error"],
2652
- children: /* @__PURE__ */ jsxDEV12(Panel, {
2653
- title: "Missing Prerequisites",
2654
- borderColor: colors.error,
2655
- children: [
2656
- /* @__PURE__ */ jsxDEV12(Text11, {
2657
- color: colors.error,
2658
- children: "Required tools are not installed:"
2659
- }, undefined, false, undefined, this),
2660
- /* @__PURE__ */ jsxDEV12(Box12, {
2661
- flexDirection: "column",
2662
- marginTop: 1,
2663
- children: missing.map((dep) => /* @__PURE__ */ jsxDEV12(Box12, {
2664
- children: [
2665
- /* @__PURE__ */ jsxDEV12(Text11, {
2666
- color: colors.warning,
2667
- children: [
2668
- "• ",
2669
- dep.name
2670
- ]
2671
- }, undefined, true, undefined, this),
2672
- /* @__PURE__ */ jsxDEV12(Text11, {
2673
- dimColor: true,
2674
- children: [
2675
- " — Install: ",
2676
- dep.install
2677
- ]
2678
- }, undefined, true, undefined, this)
2679
- ]
2680
- }, dep.name, true, undefined, this))
2681
- }, undefined, false, undefined, this),
2682
- /* @__PURE__ */ jsxDEV12(Box12, {
2683
- marginTop: 1,
2684
- children: /* @__PURE__ */ jsxDEV12(Text11, {
2685
- dimColor: true,
2686
- children: "Press any key to exit..."
2687
- }, undefined, false, undefined, this)
2688
- }, undefined, false, undefined, this)
2689
- ]
2690
- }, undefined, true, undefined, this)
2691
- }, undefined, false, undefined, this);
2692
- }
3265
+
3266
+ // src/cli/formalconf.tsx
3267
+ import { jsxDEV as jsxDEV19 } from "react/jsx-dev-runtime";
3268
+ var BREADCRUMBS = {
3269
+ main: ["Main"],
3270
+ config: ["Main", "Config Manager"],
3271
+ packages: ["Main", "Package Sync"],
3272
+ themes: ["Main", "Themes"]
3273
+ };
2693
3274
  function App() {
2694
- const [appState, setAppState] = useState5("loading");
2695
- const [missingDeps, setMissingDeps] = useState5([]);
2696
- const [screen, setScreen] = useState5("main");
2697
- const { exit } = useApp();
2698
- useInput5((input) => {
2699
- if (input === "q") {
3275
+ const [appState, setAppState] = useState10("loading");
3276
+ const [missingDeps, setMissingDeps] = useState10([]);
3277
+ const [screen, setScreen] = useState10("main");
3278
+ const { exit } = useApp2();
3279
+ useInput10((input) => {
3280
+ if (input === "q")
2700
3281
  exit();
2701
- }
2702
3282
  });
2703
- useEffect4(() => {
3283
+ useEffect6(() => {
2704
3284
  async function init() {
2705
- ensureConfigDir();
3285
+ await ensureConfigDir();
2706
3286
  const result = await checkPrerequisites();
2707
3287
  if (!result.ok) {
2708
3288
  setMissingDeps(result.missing);
2709
3289
  setAppState("error");
2710
- } else {
2711
- setAppState("ready");
3290
+ return;
3291
+ }
3292
+ const firstRun = await isFirstRun();
3293
+ if (firstRun) {
3294
+ setAppState("onboarding");
3295
+ return;
2712
3296
  }
3297
+ setAppState("ready");
2713
3298
  }
2714
3299
  init();
2715
3300
  }, []);
2716
- const getBreadcrumb = () => {
2717
- switch (screen) {
2718
- case "config":
2719
- return ["Main", "Config Manager"];
2720
- case "packages":
2721
- return ["Main", "Package Sync"];
2722
- case "themes":
2723
- return ["Main", "Themes"];
2724
- default:
2725
- return ["Main"];
2726
- }
2727
- };
2728
3301
  if (appState === "loading") {
2729
- return /* @__PURE__ */ jsxDEV12(Layout, {
3302
+ return /* @__PURE__ */ jsxDEV19(Layout, {
2730
3303
  breadcrumb: ["Loading"],
2731
- children: /* @__PURE__ */ jsxDEV12(Panel, {
3304
+ children: /* @__PURE__ */ jsxDEV19(Panel, {
2732
3305
  title: "FormalConf",
2733
- children: /* @__PURE__ */ jsxDEV12(Spinner, {
3306
+ children: /* @__PURE__ */ jsxDEV19(Spinner2, {
2734
3307
  label: "Checking prerequisites..."
2735
3308
  }, undefined, false, undefined, this)
2736
3309
  }, undefined, false, undefined, this)
2737
3310
  }, undefined, false, undefined, this);
2738
3311
  }
2739
3312
  if (appState === "error") {
2740
- return /* @__PURE__ */ jsxDEV12(PrerequisiteError, {
3313
+ return /* @__PURE__ */ jsxDEV19(PrerequisiteError, {
2741
3314
  missing: missingDeps,
2742
3315
  onExit: exit
2743
3316
  }, undefined, false, undefined, this);
2744
3317
  }
2745
- return /* @__PURE__ */ jsxDEV12(Layout, {
2746
- breadcrumb: getBreadcrumb(),
3318
+ if (appState === "onboarding") {
3319
+ return /* @__PURE__ */ jsxDEV19(Onboarding, {
3320
+ onComplete: () => setAppState("ready")
3321
+ }, undefined, false, undefined, this);
3322
+ }
3323
+ const goBack = () => setScreen("main");
3324
+ return /* @__PURE__ */ jsxDEV19(Layout, {
3325
+ breadcrumb: BREADCRUMBS[screen],
2747
3326
  children: [
2748
- screen === "main" && /* @__PURE__ */ jsxDEV12(MainMenu, {
3327
+ screen === "main" && /* @__PURE__ */ jsxDEV19(MainMenu, {
2749
3328
  onSelect: setScreen
2750
3329
  }, undefined, false, undefined, this),
2751
- screen === "config" && /* @__PURE__ */ jsxDEV12(ConfigMenu, {
2752
- onBack: () => setScreen("main")
3330
+ screen === "config" && /* @__PURE__ */ jsxDEV19(ConfigMenu, {
3331
+ onBack: goBack
2753
3332
  }, undefined, false, undefined, this),
2754
- screen === "packages" && /* @__PURE__ */ jsxDEV12(PackageMenu, {
2755
- onBack: () => setScreen("main")
3333
+ screen === "packages" && /* @__PURE__ */ jsxDEV19(PackageMenu, {
3334
+ onBack: goBack
2756
3335
  }, undefined, false, undefined, this),
2757
- screen === "themes" && /* @__PURE__ */ jsxDEV12(ThemeMenu, {
2758
- onBack: () => setScreen("main")
3336
+ screen === "themes" && /* @__PURE__ */ jsxDEV19(ThemeMenu, {
3337
+ onBack: goBack
2759
3338
  }, undefined, false, undefined, this)
2760
3339
  ]
2761
3340
  }, undefined, true, undefined, this);
2762
3341
  }
2763
- render(/* @__PURE__ */ jsxDEV12(App, {}, undefined, false, undefined, this));
3342
+ render(/* @__PURE__ */ jsxDEV19(App, {}, undefined, false, undefined, this));