unity-hub-cli 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +439 -378
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -621,55 +621,136 @@ var UnityTempDirectoryCleaner = class {
621
621
 
622
622
  // src/presentation/App.tsx
623
623
  import clipboard from "clipboardy";
624
- import { Box, Text, useApp, useInput, useStdout } from "ink";
625
- import { useCallback, useEffect, useMemo, useState } from "react";
624
+ import { Box as Box5, Text as Text3, useApp, useInput, useStdout as useStdout2 } from "ink";
625
+ import { useCallback, useEffect as useEffect3, useMemo as useMemo2, useState as useState3 } from "react";
626
626
 
627
- // src/infrastructure/config.ts
628
- import { mkdir, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
629
- var defaultPreferences = {
630
- favoritesFirst: true,
631
- primary: "updated",
632
- direction: "desc"
633
- };
634
- var getConfigDir = () => {
635
- const home = process.env.HOME ?? "";
636
- return `${home}/Library/Application Support/UnityHubCli`;
627
+ // src/presentation/components/LayoutManager.tsx
628
+ import { Box } from "ink";
629
+ import { jsx, jsxs } from "react/jsx-runtime";
630
+ var getLayoutMode = () => {
631
+ const value = process.env.UNITYHUBCLI_LAYOUT;
632
+ if (value === "right") return "rightPanel";
633
+ if (value === "screen") return "screenSwitch";
634
+ return "screenSwitch";
637
635
  };
638
- var getConfigPath = () => `${getConfigDir()}/config.json`;
639
- var isValidPrimary = (value) => value === "updated" || value === "name";
640
- var isValidDirection = (value) => value === "asc" || value === "desc";
641
- var sanitizePreferences = (input) => {
642
- if (!input || typeof input !== "object") {
643
- return defaultPreferences;
636
+ var LayoutManager = ({
637
+ layoutMode,
638
+ panelVisible,
639
+ list,
640
+ panel,
641
+ statusBar
642
+ }) => {
643
+ if (layoutMode === "rightPanel") {
644
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
645
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
646
+ /* @__PURE__ */ jsx(Box, { flexGrow: 1, children: list }),
647
+ panelVisible ? /* @__PURE__ */ jsx(Box, { marginLeft: 1, children: panel }) : null
648
+ ] }),
649
+ /* @__PURE__ */ jsx(Box, { children: statusBar })
650
+ ] });
644
651
  }
645
- const record = input;
646
- const favoritesFirst = typeof record.favoritesFirst === "boolean" ? record.favoritesFirst : defaultPreferences.favoritesFirst;
647
- const primary = isValidPrimary(record.primary) ? record.primary : defaultPreferences.primary;
648
- const direction = isValidDirection(record.direction) ? record.direction : defaultPreferences.direction;
649
- return { favoritesFirst, primary, direction };
652
+ if (layoutMode === "screenSwitch") {
653
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
654
+ /* @__PURE__ */ jsx(Box, { children: panelVisible ? panel : list }),
655
+ /* @__PURE__ */ jsx(Box, { children: statusBar })
656
+ ] });
657
+ }
658
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
659
+ /* @__PURE__ */ jsx(Box, { children: list }),
660
+ panelVisible ? /* @__PURE__ */ jsx(Box, { marginTop: 1, children: panel }) : null,
661
+ /* @__PURE__ */ jsx(Box, { children: statusBar })
662
+ ] });
650
663
  };
651
- var readSortPreferences = async () => {
652
- try {
653
- const content = await readFile3(getConfigPath(), "utf8");
654
- const json = JSON.parse(content);
655
- return sanitizePreferences(json);
656
- } catch {
657
- return defaultPreferences;
664
+
665
+ // src/presentation/components/ProjectList.tsx
666
+ import { Box as Box3 } from "ink";
667
+ import { useMemo } from "react";
668
+
669
+ // src/presentation/utils/path.ts
670
+ var homeDirectory = process.env.HOME ?? process.env.USERPROFILE ?? "";
671
+ var normalizedHomeDirectory = homeDirectory.replace(/\\/g, "/");
672
+ var homePrefix = normalizedHomeDirectory ? `${normalizedHomeDirectory}/` : "";
673
+ var shortenHomePath = (targetPath) => {
674
+ if (!normalizedHomeDirectory) {
675
+ return targetPath;
676
+ }
677
+ const normalizedTarget = targetPath.replace(/\\/g, "/");
678
+ if (normalizedTarget === normalizedHomeDirectory) {
679
+ return "~";
658
680
  }
681
+ if (homePrefix && normalizedTarget.startsWith(homePrefix)) {
682
+ return `~/${normalizedTarget.slice(homePrefix.length)}`;
683
+ }
684
+ return targetPath;
659
685
  };
660
- var writeSortPreferences = async (prefs) => {
661
- try {
662
- await mkdir(getConfigDir(), { recursive: true });
663
- } catch {
686
+ var buildCdCommand = (targetPath) => {
687
+ if (process.platform === "win32") {
688
+ const escapedForWindows = targetPath.replace(/"/g, '""');
689
+ return `cd "${escapedForWindows}"`;
664
690
  }
665
- const sanitized = sanitizePreferences(prefs);
666
- const json = JSON.stringify(sanitized, void 0, 2);
667
- await writeFile2(getConfigPath(), json, "utf8");
691
+ const escapedForPosix = targetPath.replace(/'/g, "'\\''");
692
+ return `cd '${escapedForPosix}'`;
668
693
  };
669
- var getDefaultSortPreferences = () => defaultPreferences;
670
694
 
671
- // src/presentation/App.tsx
672
- import { jsx, jsxs } from "react/jsx-runtime";
695
+ // src/presentation/components/ProjectRow.tsx
696
+ import { Box as Box2, Text, useStdout } from "ink";
697
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
698
+ var ProjectRow = ({
699
+ isSelected,
700
+ selectionBar,
701
+ projectName,
702
+ projectColor,
703
+ versionLabel,
704
+ updatedText,
705
+ statusLabel,
706
+ statusColor,
707
+ branchLine,
708
+ pathLine,
709
+ showBranch,
710
+ showPath,
711
+ scrollbar
712
+ }) => {
713
+ const { stdout } = useStdout();
714
+ const computedCenterWidth = typeof stdout?.columns === "number" ? Math.max(0, stdout.columns - 6) : void 0;
715
+ const centerWidth = typeof computedCenterWidth === "number" ? Math.max(0, computedCenterWidth - (isSelected ? 1 : 0)) : void 0;
716
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", children: [
717
+ /* @__PURE__ */ jsxs2(Box2, { width: 1, flexDirection: "column", alignItems: "center", marginLeft: 0, children: [
718
+ /* @__PURE__ */ jsx2(Text, { color: isSelected ? "green" : void 0, children: selectionBar }),
719
+ showBranch ? /* @__PURE__ */ jsx2(Text, { color: isSelected ? "green" : void 0, children: selectionBar }) : null,
720
+ showPath ? /* @__PURE__ */ jsx2(Text, { color: isSelected ? "green" : void 0, children: selectionBar }) : null
721
+ ] }),
722
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginLeft: isSelected ? 2 : 1, width: centerWidth, children: [
723
+ /* @__PURE__ */ jsxs2(Text, { wrap: "truncate", children: [
724
+ /* @__PURE__ */ jsx2(Text, { color: projectColor, bold: true, children: projectName }),
725
+ /* @__PURE__ */ jsxs2(Text, { children: [
726
+ " ",
727
+ versionLabel
728
+ ] }),
729
+ updatedText ? /* @__PURE__ */ jsx2(Text, { children: ` ${updatedText}` }) : null,
730
+ statusLabel && statusColor ? /* @__PURE__ */ jsx2(Text, { color: statusColor, children: ` ${statusLabel}` }) : null
731
+ ] }),
732
+ showBranch ? /* @__PURE__ */ jsx2(Text, { color: "#e3839c", wrap: "truncate", children: branchLine }) : null,
733
+ showPath ? /* @__PURE__ */ jsx2(Text, { color: "#719bd8", wrap: "truncate", children: pathLine }) : null,
734
+ /* @__PURE__ */ jsx2(Text, { children: " " })
735
+ ] }),
736
+ /* @__PURE__ */ jsxs2(Box2, { marginLeft: 1, width: 1, flexDirection: "column", alignItems: "center", children: [
737
+ /* @__PURE__ */ jsx2(Text, { children: scrollbar.title }),
738
+ showBranch ? /* @__PURE__ */ jsx2(Text, { children: scrollbar.branch }) : null,
739
+ showPath ? /* @__PURE__ */ jsx2(Text, { children: scrollbar.path }) : null,
740
+ /* @__PURE__ */ jsx2(Text, { children: scrollbar.spacer })
741
+ ] })
742
+ ] });
743
+ };
744
+
745
+ // src/presentation/components/ProjectList.tsx
746
+ import { jsx as jsx3 } from "react/jsx-runtime";
747
+ var PROJECT_COLOR = "#abd8e7";
748
+ var LOCK_COLOR = "yellow";
749
+ var STATUS_LABELS = {
750
+ idle: "",
751
+ running: "[running]",
752
+ crashed: "[crash]"
753
+ };
673
754
  var extractRootFolder = (repository) => {
674
755
  if (!repository?.root) {
675
756
  return void 0;
@@ -680,13 +761,13 @@ var extractRootFolder = (repository) => {
680
761
  }
681
762
  return segments[segments.length - 1];
682
763
  };
683
- var formatProjectName = (project, repository, useGitRootName) => {
764
+ var formatProjectName = (projectTitle, repository, useGitRootName) => {
684
765
  if (!useGitRootName) {
685
- return project.title;
766
+ return projectTitle;
686
767
  }
687
768
  const rootFolder = extractRootFolder(repository);
688
769
  if (!rootFolder) {
689
- return project.title;
770
+ return projectTitle;
690
771
  }
691
772
  return rootFolder;
692
773
  };
@@ -750,76 +831,269 @@ var formatUpdatedText = (lastModified) => {
750
831
  }
751
832
  return `${UPDATED_LABEL} ${relativeTime}`;
752
833
  };
753
- var homeDirectory = process.env.HOME ?? "";
754
- var homePrefix = homeDirectory ? `${homeDirectory}/` : "";
755
- var minimumVisibleProjectCount = 4;
756
- var defaultHintMessage = "Select: j/k \xB7 Open: o \xB7 Quit: q \xB7 Refresh: r \xB7 CopyPath: c \xB7 Sort: s \xB7 Close: ctrl + c";
757
- var PROJECT_COLOR = "#abd8e7";
758
- var BRANCH_COLOR = "#e3839c";
759
- var PATH_COLOR = "#719bd8";
760
- var LOCK_COLOR = "yellow";
761
- var STATUS_LABELS = {
762
- idle: "",
763
- running: "[running]",
764
- crashed: "[crash]"
834
+ var ProjectList = ({
835
+ visibleProjects,
836
+ startIndex,
837
+ selectedIndex,
838
+ linesPerProject,
839
+ showBranch,
840
+ showPath,
841
+ useGitRootName,
842
+ releasedProjects,
843
+ launchedProjects,
844
+ totalProjects
845
+ }) => {
846
+ const scrollbarChars = useMemo(() => {
847
+ const totalLines = totalProjects * linesPerProject;
848
+ const windowProjects = visibleProjects.length;
849
+ const visibleLines = windowProjects * linesPerProject;
850
+ if (totalLines === 0 || visibleLines === 0) {
851
+ return [];
852
+ }
853
+ if (totalLines <= visibleLines) {
854
+ return Array.from({ length: visibleLines }, () => "\u2588");
855
+ }
856
+ const trackLength = visibleLines;
857
+ const sliderSize = Math.max(1, Math.round(visibleLines / totalLines * trackLength));
858
+ const maxSliderStart = Math.max(0, trackLength - sliderSize);
859
+ const topLine = startIndex * linesPerProject;
860
+ const denominator = Math.max(1, totalLines - visibleLines);
861
+ const sliderStart = Math.min(
862
+ maxSliderStart,
863
+ Math.round(topLine / denominator * maxSliderStart)
864
+ );
865
+ return Array.from({ length: trackLength }, (_, position) => {
866
+ if (position >= sliderStart && position < sliderStart + sliderSize) {
867
+ return "\u2588";
868
+ }
869
+ return "|";
870
+ });
871
+ }, [linesPerProject, startIndex, totalProjects, visibleProjects.length]);
872
+ const rows = useMemo(() => {
873
+ return visibleProjects.map(({ project, repository, launchStatus }, offset) => {
874
+ const rowIndex = startIndex + offset;
875
+ const isSelected = rowIndex === selectedIndex;
876
+ const selectionBar = isSelected ? "\u2503" : " ";
877
+ const projectName = formatProjectName(project.title, repository, useGitRootName);
878
+ const versionLabel = `(${project.version.value})`;
879
+ const updatedText = formatUpdatedText(project.lastModified);
880
+ const pathLine = shortenHomePath(project.path);
881
+ const branchLine = formatBranch(repository?.branch);
882
+ const hasReleasedLock = releasedProjects.has(project.id);
883
+ const isLocallyLaunched = launchedProjects.has(project.id);
884
+ const displayStatus = (() => {
885
+ if (isLocallyLaunched) {
886
+ return "running";
887
+ }
888
+ if (hasReleasedLock) {
889
+ return "idle";
890
+ }
891
+ return launchStatus;
892
+ })();
893
+ const baseScrollbarIndex = offset * linesPerProject;
894
+ const titleScrollbar = scrollbarChars[baseScrollbarIndex] ?? " ";
895
+ const branchScrollbar = showBranch ? scrollbarChars[baseScrollbarIndex + 1] ?? " " : " ";
896
+ const pathScrollbar = showPath ? scrollbarChars[baseScrollbarIndex + 1 + (showBranch ? 1 : 0)] ?? " " : " ";
897
+ const spacerScrollbar = scrollbarChars[baseScrollbarIndex + linesPerProject - 1] ?? " ";
898
+ const statusLabel = STATUS_LABELS[displayStatus];
899
+ const statusColor = displayStatus === "running" ? LOCK_COLOR : displayStatus === "crashed" ? "red" : void 0;
900
+ return /* @__PURE__ */ jsx3(
901
+ ProjectRow,
902
+ {
903
+ isSelected,
904
+ selectionBar,
905
+ projectName,
906
+ projectColor: PROJECT_COLOR,
907
+ versionLabel,
908
+ updatedText,
909
+ statusLabel,
910
+ statusColor,
911
+ branchLine,
912
+ pathLine,
913
+ showBranch,
914
+ showPath,
915
+ scrollbar: {
916
+ title: titleScrollbar,
917
+ branch: branchScrollbar,
918
+ path: pathScrollbar,
919
+ spacer: spacerScrollbar
920
+ }
921
+ },
922
+ project.id
923
+ );
924
+ });
925
+ }, [
926
+ launchedProjects,
927
+ releasedProjects,
928
+ scrollbarChars,
929
+ selectedIndex,
930
+ showBranch,
931
+ showPath,
932
+ startIndex,
933
+ useGitRootName,
934
+ visibleProjects,
935
+ linesPerProject
936
+ ]);
937
+ return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: rows.length === 0 ? null : rows });
765
938
  };
766
- var shortenHomePath = (targetPath) => {
767
- if (!homeDirectory) {
768
- return targetPath;
769
- }
770
- if (targetPath === homeDirectory) {
771
- return "~";
772
- }
773
- if (homePrefix && targetPath.startsWith(homePrefix)) {
774
- return `~/${targetPath.slice(homePrefix.length)}`;
939
+
940
+ // src/presentation/components/SortPanel.tsx
941
+ import { Box as Box4, Text as Text2 } from "ink";
942
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
943
+ var lineForPrimary = (primary) => {
944
+ return `Primary: ${primary === "updated" ? "Updated" : "Name (Git root)"}`;
945
+ };
946
+ var lineForDirection = (prefs) => {
947
+ if (prefs.primary === "updated") {
948
+ return `Direction: ${prefs.direction === "desc" ? "New to Old" : "Old to New"}`;
775
949
  }
776
- return targetPath;
950
+ return `Direction: ${prefs.direction === "asc" ? "A to Z" : "Z to A"}`;
777
951
  };
778
- var buildCdCommand = (targetPath) => {
779
- return `cd ${targetPath}`;
952
+ var lineForFavorites = (favoritesFirst) => {
953
+ return `Favorites first: ${favoritesFirst ? "ON" : "OFF"}`;
780
954
  };
781
- var getCopyTargetPath = (view) => {
782
- const root = view.repository?.root;
783
- return root && root.length > 0 ? root : view.project.path;
955
+ var SortPanel = ({ sortPreferences, focusedIndex, width }) => {
956
+ const primaryLine = lineForPrimary(sortPreferences.primary);
957
+ const directionLine = lineForDirection(sortPreferences);
958
+ const favoritesLine = lineForFavorites(sortPreferences.favoritesFirst);
959
+ const Item = ({ label, selected }) => {
960
+ const prefix = selected ? "> " : " ";
961
+ return /* @__PURE__ */ jsxs3(Text2, { children: [
962
+ selected ? /* @__PURE__ */ jsx4(Text2, { color: "green", children: prefix }) : prefix,
963
+ label
964
+ ] });
965
+ };
966
+ return /* @__PURE__ */ jsxs3(
967
+ Box4,
968
+ {
969
+ flexDirection: "column",
970
+ borderStyle: "round",
971
+ borderColor: "green",
972
+ paddingX: 1,
973
+ width,
974
+ children: [
975
+ /* @__PURE__ */ jsx4(Item, { label: primaryLine, selected: focusedIndex === 0 }),
976
+ /* @__PURE__ */ jsx4(Item, { label: directionLine, selected: focusedIndex === 1 }),
977
+ /* @__PURE__ */ jsx4(Item, { label: favoritesLine, selected: focusedIndex === 2 })
978
+ ]
979
+ }
980
+ );
784
981
  };
785
- var isControl = (code) => code >= 0 && code < 32 || code >= 127 && code < 160;
786
- var isFullwidth = (code) => {
787
- if (code >= 4352 && (code <= 4447 || // Hangul Jamo
788
- code === 9001 || code === 9002 || code >= 11904 && code <= 42191 && code !== 12351 || // CJK ... Yi
789
- code >= 44032 && code <= 55203 || // Hangul Syllables
790
- code >= 63744 && code <= 64255 || // CJK Compatibility Ideographs
791
- code >= 65040 && code <= 65049 || // Vertical forms
792
- code >= 65072 && code <= 65135 || // CJK Compatibility Forms
793
- code >= 65280 && code <= 65376 || // Fullwidth forms
794
- code >= 65504 && code <= 65510 || code >= 127744 && code <= 128591 || // Emojis
795
- code >= 129280 && code <= 129535 || code >= 131072 && code <= 262141)) {
796
- return true;
982
+
983
+ // src/presentation/hooks/useSortPreferences.ts
984
+ import { useEffect, useState } from "react";
985
+
986
+ // src/infrastructure/config.ts
987
+ import { mkdir, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
988
+ var defaultPreferences = {
989
+ favoritesFirst: true,
990
+ primary: "updated",
991
+ direction: "desc"
992
+ };
993
+ var getConfigDir = () => {
994
+ const home = process.env.HOME ?? "";
995
+ return `${home}/Library/Application Support/UnityHubCli`;
996
+ };
997
+ var getConfigPath = () => `${getConfigDir()}/config.json`;
998
+ var isValidPrimary = (value) => value === "updated" || value === "name";
999
+ var isValidDirection = (value) => value === "asc" || value === "desc";
1000
+ var sanitizePreferences = (input) => {
1001
+ if (!input || typeof input !== "object") {
1002
+ return defaultPreferences;
1003
+ }
1004
+ const record = input;
1005
+ const favoritesFirst = typeof record.favoritesFirst === "boolean" ? record.favoritesFirst : defaultPreferences.favoritesFirst;
1006
+ const primary = isValidPrimary(record.primary) ? record.primary : defaultPreferences.primary;
1007
+ const direction = isValidDirection(record.direction) ? record.direction : defaultPreferences.direction;
1008
+ return { favoritesFirst, primary, direction };
1009
+ };
1010
+ var readSortPreferences = async () => {
1011
+ try {
1012
+ const content = await readFile3(getConfigPath(), "utf8");
1013
+ const json = JSON.parse(content);
1014
+ return sanitizePreferences(json);
1015
+ } catch {
1016
+ return defaultPreferences;
1017
+ }
1018
+ };
1019
+ var writeSortPreferences = async (prefs) => {
1020
+ try {
1021
+ await mkdir(getConfigDir(), { recursive: true });
1022
+ } catch {
797
1023
  }
798
- return false;
1024
+ const sanitized = sanitizePreferences(prefs);
1025
+ const json = JSON.stringify(sanitized, void 0, 2);
1026
+ await writeFile2(getConfigPath(), json, "utf8");
1027
+ };
1028
+ var getDefaultSortPreferences = () => defaultPreferences;
1029
+
1030
+ // src/presentation/hooks/useSortPreferences.ts
1031
+ var useSortPreferences = () => {
1032
+ const [sortPreferences, setSortPreferences] = useState(getDefaultSortPreferences());
1033
+ const [isLoaded, setIsLoaded] = useState(false);
1034
+ useEffect(() => {
1035
+ void (async () => {
1036
+ try {
1037
+ const prefs = await readSortPreferences();
1038
+ setSortPreferences(prefs);
1039
+ } finally {
1040
+ setIsLoaded(true);
1041
+ }
1042
+ })();
1043
+ }, []);
1044
+ useEffect(() => {
1045
+ if (!isLoaded) {
1046
+ return;
1047
+ }
1048
+ void writeSortPreferences(sortPreferences);
1049
+ }, [isLoaded, sortPreferences]);
1050
+ return { sortPreferences, setSortPreferences, isLoaded };
799
1051
  };
800
- var charWidth = (char) => {
801
- const code = char.codePointAt(0);
802
- if (code === void 0) return 0;
803
- if (isControl(code)) return 0;
804
- return isFullwidth(code) ? 2 : 1;
1052
+
1053
+ // src/presentation/hooks/useVisibleCount.ts
1054
+ import { useEffect as useEffect2, useState as useState2 } from "react";
1055
+ var useVisibleCount = (stdout, linesPerProject, panelVisible, panelHeight, minimumVisibleProjectCount2) => {
1056
+ const compute = () => {
1057
+ if (!stdout || typeof stdout.columns !== "number" || typeof stdout.rows !== "number") {
1058
+ return minimumVisibleProjectCount2;
1059
+ }
1060
+ const borderRows = 2;
1061
+ const hintRows = 1;
1062
+ const reservedRows = borderRows + hintRows + (panelVisible ? panelHeight : 0);
1063
+ const availableRows = Math.max(0, stdout.rows - reservedRows);
1064
+ const rowsPerProject = Math.max(linesPerProject, 1);
1065
+ const calculatedCount = Math.max(1, Math.floor(availableRows / rowsPerProject));
1066
+ return calculatedCount;
1067
+ };
1068
+ const [visibleCount, setVisibleCount] = useState2(compute);
1069
+ useEffect2(() => {
1070
+ const updateVisible = () => setVisibleCount(compute());
1071
+ updateVisible();
1072
+ stdout?.on("resize", updateVisible);
1073
+ return () => {
1074
+ stdout?.off("resize", updateVisible);
1075
+ };
1076
+ }, [stdout, linesPerProject, panelVisible, panelHeight]);
1077
+ return visibleCount;
805
1078
  };
806
- var stringWidth = (text) => {
807
- let width = 0;
808
- for (const ch of text) {
809
- width += charWidth(ch);
1079
+
1080
+ // src/presentation/App.tsx
1081
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1082
+ var extractRootFolder2 = (repository) => {
1083
+ if (!repository?.root) {
1084
+ return void 0;
810
1085
  }
811
- return width;
1086
+ const segments = repository.root.split("/").filter((segment) => segment.length > 0);
1087
+ if (segments.length === 0) {
1088
+ return void 0;
1089
+ }
1090
+ return segments[segments.length - 1];
812
1091
  };
813
- var truncateToWidth = (text, maxWidth) => {
814
- let width = 0;
815
- let result = "";
816
- for (const ch of text) {
817
- const w = charWidth(ch);
818
- if (width + w > maxWidth) break;
819
- result += ch;
820
- width += w;
821
- }
822
- return result;
1092
+ var minimumVisibleProjectCount = 4;
1093
+ var defaultHintMessage = "Select: j/k \xB7 Open: o \xB7 Quit: q \xB7 Refresh: r \xB7 CopyPath: c \xB7 Sort: s \xB7 Close: ctrl + c";
1094
+ var getCopyTargetPath = (view) => {
1095
+ const root = view.repository?.root;
1096
+ return root && root.length > 0 ? root : view.project.path;
823
1097
  };
824
1098
  var App = ({
825
1099
  projects,
@@ -831,31 +1105,24 @@ var App = ({
831
1105
  showPath = true
832
1106
  }) => {
833
1107
  const { exit } = useApp();
834
- const { stdout } = useStdout();
835
- const [projectViews, setProjectViews] = useState(projects);
836
- const [visibleCount, setVisibleCount] = useState(minimumVisibleProjectCount);
837
- const [index, setIndex] = useState(0);
838
- const [hint, setHint] = useState(defaultHintMessage);
839
- const [windowStart, setWindowStart] = useState(0);
840
- const [releasedProjects, setReleasedProjects] = useState(/* @__PURE__ */ new Set());
841
- const [launchedProjects, setLaunchedProjects] = useState(/* @__PURE__ */ new Set());
842
- const [isRefreshing, setIsRefreshing] = useState(false);
843
- const [isSortMenuOpen, setIsSortMenuOpen] = useState(false);
844
- const [sortMenuIndex, setSortMenuIndex] = useState(0);
845
- const [sortPreferences, setSortPreferences] = useState(getDefaultSortPreferences());
846
- const [isSortPreferencesLoaded, setIsSortPreferencesLoaded] = useState(false);
847
- const [renderEpoch, setRenderEpoch] = useState(0);
1108
+ const { stdout } = useStdout2();
1109
+ const [projectViews, setProjectViews] = useState3(projects);
1110
+ const [isSortMenuOpen, setIsSortMenuOpen] = useState3(false);
848
1111
  const linesPerProject = (showBranch ? 1 : 0) + (showPath ? 1 : 0) + 2;
849
- const forceFullRepaint = useCallback(() => {
850
- if (!stdout) {
851
- return;
852
- }
853
- stdout.write("\x1B[2J\x1B[3J\x1B[H");
854
- }, [stdout]);
855
- const sortedProjects = useMemo(() => {
1112
+ const panelHeight = 6;
1113
+ const visibleCount = useVisibleCount(stdout, linesPerProject, isSortMenuOpen, panelHeight, minimumVisibleProjectCount);
1114
+ const [index, setIndex] = useState3(0);
1115
+ const [hint, setHint] = useState3(defaultHintMessage);
1116
+ const [windowStart, setWindowStart] = useState3(0);
1117
+ const [releasedProjects, setReleasedProjects] = useState3(/* @__PURE__ */ new Set());
1118
+ const [launchedProjects, setLaunchedProjects] = useState3(/* @__PURE__ */ new Set());
1119
+ const [isRefreshing, setIsRefreshing] = useState3(false);
1120
+ const [sortMenuIndex, setSortMenuIndex] = useState3(0);
1121
+ const { sortPreferences, setSortPreferences } = useSortPreferences();
1122
+ const sortedProjects = useMemo2(() => {
856
1123
  const fallbackTime = 0;
857
1124
  const getNameKey = (view) => {
858
- const rootName = extractRootFolder(view.repository);
1125
+ const rootName = extractRootFolder2(view.repository);
859
1126
  const base = rootName || view.project.title;
860
1127
  return base.toLocaleLowerCase();
861
1128
  };
@@ -904,24 +1171,7 @@ var App = ({
904
1171
  return tieBreaker(a).localeCompare(tieBreaker(b));
905
1172
  });
906
1173
  }, [projectViews, sortPreferences]);
907
- useEffect(() => {
908
- void (async () => {
909
- try {
910
- const prefs = await readSortPreferences();
911
- setSortPreferences(prefs);
912
- } catch {
913
- } finally {
914
- setIsSortPreferencesLoaded(true);
915
- }
916
- })();
917
- }, []);
918
- useEffect(() => {
919
- if (!isSortPreferencesLoaded) {
920
- return;
921
- }
922
- void writeSortPreferences(sortPreferences);
923
- }, [sortPreferences, isSortPreferencesLoaded]);
924
- useEffect(() => {
1174
+ useEffect3(() => {
925
1175
  const handleSigint = () => {
926
1176
  exit();
927
1177
  };
@@ -930,26 +1180,6 @@ var App = ({
930
1180
  process.off("SIGINT", handleSigint);
931
1181
  };
932
1182
  }, [exit]);
933
- useEffect(() => {
934
- const updateVisibleCount = () => {
935
- if (!stdout || typeof stdout.columns !== "number" || typeof stdout.rows !== "number") {
936
- setVisibleCount(minimumVisibleProjectCount);
937
- return;
938
- }
939
- const borderRows = 2;
940
- const hintRows = 1;
941
- const reservedRows = borderRows + hintRows;
942
- const availableRows = Math.max(0, stdout.rows - reservedRows);
943
- const rowsPerProject = Math.max(linesPerProject, 1);
944
- const calculatedCount = Math.max(1, Math.floor(availableRows / rowsPerProject));
945
- setVisibleCount(calculatedCount);
946
- };
947
- updateVisibleCount();
948
- stdout?.on("resize", updateVisibleCount);
949
- return () => {
950
- stdout?.off("resize", updateVisibleCount);
951
- };
952
- }, [linesPerProject, stdout]);
953
1183
  const limit = Math.max(1, visibleCount);
954
1184
  const move = useCallback(
955
1185
  (delta) => {
@@ -994,7 +1224,7 @@ var App = ({
994
1224
  },
995
1225
  [index, limit, sortedProjects.length]
996
1226
  );
997
- useEffect(() => {
1227
+ useEffect3(() => {
998
1228
  setWindowStart((prevStart) => {
999
1229
  if (sortedProjects.length <= limit) {
1000
1230
  return prevStart === 0 ? prevStart : 0;
@@ -1137,7 +1367,7 @@ var App = ({
1137
1367
  }, 3e3);
1138
1368
  }
1139
1369
  }, [index, onTerminate, sortedProjects]);
1140
- useEffect(() => {
1370
+ useEffect3(() => {
1141
1371
  setProjectViews(projects);
1142
1372
  setReleasedProjects(/* @__PURE__ */ new Set());
1143
1373
  setLaunchedProjects(/* @__PURE__ */ new Set());
@@ -1198,10 +1428,7 @@ var App = ({
1198
1428
  useInput((input, key) => {
1199
1429
  if (isSortMenuOpen) {
1200
1430
  if (key.escape || input === "\x1B") {
1201
- clearOverlay();
1202
- forceFullRepaint();
1203
1431
  setIsSortMenuOpen(false);
1204
- setRenderEpoch((prev) => prev + 1);
1205
1432
  return;
1206
1433
  }
1207
1434
  if (input === "j") {
@@ -1262,7 +1489,7 @@ var App = ({
1262
1489
  copyProjectPath();
1263
1490
  }
1264
1491
  });
1265
- const { startIndex, visibleProjects } = useMemo(() => {
1492
+ const { startIndex, visibleProjects } = useMemo2(() => {
1266
1493
  if (sortedProjects.length <= limit) {
1267
1494
  return {
1268
1495
  startIndex: 0,
@@ -1277,219 +1504,53 @@ var App = ({
1277
1504
  visibleProjects: sortedProjects.slice(clampedStart, end)
1278
1505
  };
1279
1506
  }, [limit, sortedProjects, windowStart]);
1280
- const scrollbarChars = useMemo(() => {
1281
- const totalProjects = projectViews.length;
1282
- const totalLines = totalProjects * linesPerProject;
1283
- const windowProjects = visibleProjects.length;
1284
- const visibleLines = windowProjects * linesPerProject;
1285
- if (totalLines === 0 || visibleLines === 0) {
1286
- return [];
1287
- }
1288
- if (totalLines <= visibleLines) {
1289
- return Array.from({ length: visibleLines }, () => "\u2588");
1290
- }
1291
- const trackLength = visibleLines;
1292
- const sliderSize = Math.max(1, Math.round(visibleLines / totalLines * trackLength));
1293
- const maxSliderStart = Math.max(0, trackLength - sliderSize);
1294
- const topLine = startIndex * linesPerProject;
1295
- const denominator = Math.max(1, totalLines - visibleLines);
1296
- const sliderStart = Math.min(
1297
- maxSliderStart,
1298
- Math.round(topLine / denominator * maxSliderStart)
1299
- );
1300
- return Array.from({ length: trackLength }, (_, position) => {
1301
- if (position >= sliderStart && position < sliderStart + sliderSize) {
1302
- return "\u2588";
1303
- }
1304
- return "|";
1305
- });
1306
- }, [linesPerProject, projectViews.length, startIndex, visibleProjects]);
1307
- const rows = useMemo(() => {
1308
- return visibleProjects.map(({ project, repository, launchStatus }, offset) => {
1309
- const rowIndex = startIndex + offset;
1310
- const isSelected = rowIndex === index;
1311
- const selectionBar = isSelected ? "\u2503" : " ";
1312
- const projectName = formatProjectName(project, repository, useGitRootName);
1313
- const versionLabel = `(${project.version.value})`;
1314
- const updatedText = formatUpdatedText(project.lastModified);
1315
- const pathLine = shortenHomePath(project.path);
1316
- const branchLine = formatBranch(repository?.branch);
1317
- const hasReleasedLock = releasedProjects.has(project.id);
1318
- const isLocallyLaunched = launchedProjects.has(project.id);
1319
- const displayStatus = (() => {
1320
- if (isLocallyLaunched) {
1321
- return "running";
1322
- }
1323
- if (hasReleasedLock) {
1324
- return "idle";
1507
+ return /* @__PURE__ */ jsx5(
1508
+ LayoutManager,
1509
+ {
1510
+ layoutMode: getLayoutMode(),
1511
+ panelVisible: isSortMenuOpen,
1512
+ list: /* @__PURE__ */ jsx5(
1513
+ Box5,
1514
+ {
1515
+ flexDirection: "column",
1516
+ borderStyle: "round",
1517
+ borderColor: "green",
1518
+ width: typeof stdout?.columns === "number" ? stdout.columns : void 0,
1519
+ children: sortedProjects.length === 0 ? /* @__PURE__ */ jsx5(Text3, { children: "No Unity Hub projects were found." }) : /* @__PURE__ */ jsx5(
1520
+ ProjectList,
1521
+ {
1522
+ visibleProjects,
1523
+ startIndex,
1524
+ selectedIndex: index,
1525
+ linesPerProject,
1526
+ showBranch,
1527
+ showPath,
1528
+ useGitRootName,
1529
+ releasedProjects,
1530
+ launchedProjects,
1531
+ totalProjects: sortedProjects.length
1532
+ }
1533
+ )
1325
1534
  }
1326
- return launchStatus;
1327
- })();
1328
- const baseScrollbarIndex = offset * linesPerProject;
1329
- const titleScrollbar = scrollbarChars[baseScrollbarIndex] ?? " ";
1330
- const branchScrollbar = showBranch ? scrollbarChars[baseScrollbarIndex + 1] ?? " " : " ";
1331
- const pathScrollbar = showPath ? scrollbarChars[baseScrollbarIndex + 1 + (showBranch ? 1 : 0)] ?? " " : " ";
1332
- const spacerScrollbar = scrollbarChars[baseScrollbarIndex + linesPerProject - 1] ?? " ";
1333
- const statusLabel = STATUS_LABELS[displayStatus];
1334
- const statusColor = displayStatus === "running" ? LOCK_COLOR : displayStatus === "crashed" ? "red" : void 0;
1335
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
1336
- /* @__PURE__ */ jsxs(Box, { width: 1, flexDirection: "column", alignItems: "center", marginLeft: isSelected ? 1 : 0, children: [
1337
- /* @__PURE__ */ jsx(Text, { color: isSelected ? "green" : void 0, children: selectionBar }),
1338
- showBranch ? /* @__PURE__ */ jsx(Text, { color: isSelected ? "green" : void 0, children: selectionBar }) : null,
1339
- showPath ? /* @__PURE__ */ jsx(Text, { color: isSelected ? "green" : void 0, children: selectionBar }) : null
1340
- ] }),
1341
- /* @__PURE__ */ jsxs(Box, { flexGrow: 1, flexDirection: "column", marginLeft: isSelected ? 2 : 1, children: [
1342
- /* @__PURE__ */ jsxs(Text, { wrap: "truncate", children: [
1343
- /* @__PURE__ */ jsx(Text, { color: PROJECT_COLOR, bold: true, children: projectName }),
1344
- /* @__PURE__ */ jsxs(Text, { children: [
1345
- " ",
1346
- versionLabel
1347
- ] }),
1348
- updatedText ? /* @__PURE__ */ jsx(Text, { children: ` ${updatedText}` }) : null,
1349
- statusLabel && statusColor ? /* @__PURE__ */ jsx(Text, { color: statusColor, children: ` ${statusLabel}` }) : null
1350
- ] }),
1351
- showBranch ? /* @__PURE__ */ jsx(Text, { color: BRANCH_COLOR, wrap: "truncate", children: branchLine }) : null,
1352
- showPath ? /* @__PURE__ */ jsx(Text, { color: PATH_COLOR, wrap: "truncate", children: pathLine }) : null,
1353
- /* @__PURE__ */ jsx(Text, { children: " " })
1354
- ] }),
1355
- /* @__PURE__ */ jsxs(Box, { marginLeft: 1, width: 1, flexDirection: "column", alignItems: "center", children: [
1356
- /* @__PURE__ */ jsx(Text, { children: titleScrollbar }),
1357
- showBranch ? /* @__PURE__ */ jsx(Text, { children: branchScrollbar }) : null,
1358
- showPath ? /* @__PURE__ */ jsx(Text, { children: pathScrollbar }) : null,
1359
- /* @__PURE__ */ jsx(Text, { children: spacerScrollbar })
1360
- ] })
1361
- ] }, project.id);
1362
- });
1363
- }, [
1364
- index,
1365
- launchedProjects,
1366
- releasedProjects,
1367
- scrollbarChars,
1368
- showBranch,
1369
- showPath,
1370
- startIndex,
1371
- useGitRootName,
1372
- visibleProjects
1373
- ]);
1374
- useEffect(() => {
1375
- if (!isSortMenuOpen || !stdout) {
1376
- return;
1377
- }
1378
- const columns = typeof stdout.columns === "number" ? stdout.columns : 80;
1379
- const contentRows = rows.length === 0 ? 1 : visibleProjects.length * linesPerProject;
1380
- const contentWidth = Math.max(10, columns - 2);
1381
- const modalInnerWidth = Math.min(60, Math.max(28, contentWidth - 6));
1382
- const modalWidth = Math.min(contentWidth, modalInnerWidth);
1383
- const leftPadding = Math.max(0, Math.floor((contentWidth - modalWidth) / 2));
1384
- const title = "Sort Menu (Select: j/k, Toggle: Space, Close: Esc)";
1385
- const linePrimary = `Primary: ${sortPreferences.primary === "updated" ? "Updated" : "Name (Git root)"}`;
1386
- const lineDirection = `Direction: ${sortPreferences.primary === "updated" ? sortPreferences.direction === "desc" ? "New to Old" : "Old to New" : sortPreferences.direction === "asc" ? "A to Z" : "Z to A"}`;
1387
- const lineFav = `Favorites first: ${sortPreferences.favoritesFirst ? "ON" : "OFF"}`;
1388
- const innerWidth = modalWidth - 2;
1389
- const BORDER_ON = "\x1B[32m";
1390
- const BORDER_OFF = "\x1B[39m";
1391
- const fixedPrefixWidth = 2;
1392
- const buildContentLine = (label, selected) => {
1393
- const arrow = selected ? "> " : " ";
1394
- const maxLabelWidth = Math.max(0, innerWidth - fixedPrefixWidth);
1395
- const rawLabel = label;
1396
- const limitedLabel = stringWidth(rawLabel) > maxLabelWidth ? `${truncateToWidth(rawLabel, Math.max(0, maxLabelWidth - 3))}...` : rawLabel;
1397
- const visibleBase = `${arrow}${limitedLabel}`;
1398
- const colored = selected ? `\x1B[32m${visibleBase}\x1B[39m` : visibleBase;
1399
- const pad = Math.max(0, innerWidth - stringWidth(visibleBase));
1400
- return `${BORDER_ON}\u2502${BORDER_OFF}${colored}${" ".repeat(pad)}${BORDER_ON}\u2502${BORDER_OFF}`;
1401
- };
1402
- const contentLines = [
1403
- (() => {
1404
- const prefix = " ";
1405
- const maxTitleWidth = Math.max(0, innerWidth - fixedPrefixWidth);
1406
- const visibleTitle = stringWidth(title) > maxTitleWidth ? `${truncateToWidth(title, Math.max(0, maxTitleWidth - 3))}...` : title;
1407
- const content = `${prefix}${visibleTitle}`;
1408
- const pad = Math.max(0, innerWidth - stringWidth(content));
1409
- return `${BORDER_ON}\u2502${BORDER_OFF}${content}${" ".repeat(pad)}${BORDER_ON}\u2502${BORDER_OFF}`;
1410
- })(),
1411
- `${BORDER_ON}\u2502${BORDER_OFF}${" ".repeat(innerWidth)}${BORDER_ON}\u2502${BORDER_OFF}`,
1412
- buildContentLine(linePrimary, sortMenuIndex === 0),
1413
- buildContentLine(lineDirection, sortMenuIndex === 1),
1414
- buildContentLine(lineFav, sortMenuIndex === 2)
1415
- ];
1416
- const topBorder = `${BORDER_ON}\u250C${"\u2500".repeat(modalWidth - 2)}\u2510${BORDER_OFF}`;
1417
- const bottomBorder = `${BORDER_ON}\u2514${"\u2500".repeat(modalWidth - 2)}\u2518${BORDER_OFF}`;
1418
- const overlayLines = [topBorder, ...contentLines, bottomBorder];
1419
- const overlayHeight = overlayLines.length;
1420
- const overlayTopWithinContent = Math.max(0, Math.floor((contentRows - overlayHeight) / 2));
1421
- const overlayTopRelativeToComponent = 1 + overlayTopWithinContent;
1422
- const bottomIndex = contentRows + 2;
1423
- const moveUp = Math.max(0, bottomIndex - overlayTopRelativeToComponent);
1424
- const moveRight = 1 + leftPadding;
1425
- stdout.write("\x1B7");
1426
- if (moveUp > 0) {
1427
- stdout.write(`\x1B[${moveUp}A`);
1428
- }
1429
- stdout.write("\r");
1430
- for (let i = 0; i < overlayLines.length; i++) {
1431
- stdout.write("\r");
1432
- if (moveRight > 0) {
1433
- stdout.write(`\x1B[${moveRight}C`);
1434
- }
1435
- stdout.write(" ".repeat(Math.max(0, modalWidth)));
1436
- stdout.write(`\x1B[${Math.max(0, modalWidth)}D`);
1437
- stdout.write(overlayLines[i]);
1438
- if (i < overlayLines.length - 1) {
1439
- stdout.write("\r\n");
1440
- }
1441
- }
1442
- stdout.write("\x1B8");
1443
- }, [
1444
- isSortMenuOpen,
1445
- linesPerProject,
1446
- rows,
1447
- sortPreferences,
1448
- sortMenuIndex,
1449
- stdout,
1450
- visibleProjects.length
1451
- ]);
1452
- const clearOverlay = useCallback(() => {
1453
- if (!stdout) {
1454
- return;
1455
- }
1456
- const columns = typeof stdout.columns === "number" ? stdout.columns : 80;
1457
- const contentRows = rows.length === 0 ? 1 : visibleProjects.length * linesPerProject;
1458
- const contentWidth = Math.max(10, columns - 2);
1459
- const modalInnerWidth = Math.min(60, Math.max(28, contentWidth - 6));
1460
- const modalWidth = Math.min(contentWidth, modalInnerWidth);
1461
- const leftPadding = Math.max(0, Math.floor((contentWidth - modalWidth) / 2));
1462
- const overlayHeight = 6;
1463
- const overlayTopWithinContent = Math.max(0, Math.floor((contentRows - overlayHeight) / 2));
1464
- const overlayTopRelativeToComponent = 1 + overlayTopWithinContent;
1465
- const bottomIndex = contentRows + 2;
1466
- const moveUp = Math.max(0, bottomIndex - overlayTopRelativeToComponent);
1467
- const moveRight = 1 + leftPadding;
1468
- stdout.write("\x1B7");
1469
- if (moveUp > 0) {
1470
- stdout.write(`\x1B[${moveUp}A`);
1471
- }
1472
- stdout.write("\r");
1473
- for (let i = 0; i < overlayHeight; i++) {
1474
- stdout.write("\r");
1475
- if (moveRight > 0) {
1476
- stdout.write(`\x1B[${moveRight}C`);
1477
- }
1478
- stdout.write(" ".repeat(Math.max(0, modalWidth)));
1479
- if (i < overlayHeight - 1) {
1480
- stdout.write("\r\n");
1481
- }
1535
+ ),
1536
+ panel: /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", width: typeof stdout?.columns === "number" ? stdout.columns : void 0, children: [
1537
+ /* @__PURE__ */ jsx5(Text3, { children: "Sort Settings" }),
1538
+ /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(
1539
+ SortPanel,
1540
+ {
1541
+ sortPreferences,
1542
+ focusedIndex: sortMenuIndex,
1543
+ width: typeof stdout?.columns === "number" ? stdout.columns : void 0
1544
+ }
1545
+ ) })
1546
+ ] }),
1547
+ statusBar: isSortMenuOpen ? /* @__PURE__ */ jsx5(Text3, { wrap: "truncate", children: "Select: j/k, Toggle: Space, Back: Esc" }) : /* @__PURE__ */ jsx5(Text3, { wrap: "truncate", children: hint })
1482
1548
  }
1483
- stdout.write("\x1B8");
1484
- }, [linesPerProject, rows.length, visibleProjects.length, stdout]);
1485
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1486
- /* @__PURE__ */ jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", children: rows.length === 0 ? /* @__PURE__ */ jsx(Text, { children: "No Unity Hub projects were found." }) : rows }),
1487
- /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { wrap: "truncate", children: hint }) })
1488
- ] }, renderEpoch);
1549
+ );
1489
1550
  };
1490
1551
 
1491
1552
  // src/index.tsx
1492
- import { jsx as jsx2 } from "react/jsx-runtime";
1553
+ import { jsx as jsx6 } from "react/jsx-runtime";
1493
1554
  var bootstrap = async () => {
1494
1555
  const unityHubReader = new UnityHubProjectsReader();
1495
1556
  const gitRepositoryInfoReader = new GitRepositoryInfoReader();
@@ -1525,7 +1586,7 @@ var bootstrap = async () => {
1525
1586
  try {
1526
1587
  const projects = await listProjectsUseCase.execute();
1527
1588
  const { waitUntilExit } = render(
1528
- /* @__PURE__ */ jsx2(
1589
+ /* @__PURE__ */ jsx6(
1529
1590
  App,
1530
1591
  {
1531
1592
  projects,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unity-hub-cli",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "A CLI tool that reads Unity Hub's projects and launches Unity Editor with an interactive TUI",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {