unity-hub-cli 0.9.0 → 0.10.0

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 +435 -377
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -621,55 +621,133 @@ 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 } 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 "~";
680
+ }
681
+ if (homePrefix && normalizedTarget.startsWith(homePrefix)) {
682
+ return `~/${normalizedTarget.slice(homePrefix.length)}`;
658
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 } 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
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", children: [
714
+ /* @__PURE__ */ jsxs2(Box2, { width: 1, flexDirection: "column", alignItems: "center", marginLeft: isSelected ? 1 : 0, children: [
715
+ /* @__PURE__ */ jsx2(Text, { color: isSelected ? "green" : void 0, children: selectionBar }),
716
+ showBranch ? /* @__PURE__ */ jsx2(Text, { color: isSelected ? "green" : void 0, children: selectionBar }) : null,
717
+ showPath ? /* @__PURE__ */ jsx2(Text, { color: isSelected ? "green" : void 0, children: selectionBar }) : null
718
+ ] }),
719
+ /* @__PURE__ */ jsxs2(Box2, { flexGrow: 1, flexDirection: "column", marginLeft: isSelected ? 2 : 1, children: [
720
+ /* @__PURE__ */ jsxs2(Text, { wrap: "truncate", children: [
721
+ /* @__PURE__ */ jsx2(Text, { color: projectColor, bold: true, children: projectName }),
722
+ /* @__PURE__ */ jsxs2(Text, { children: [
723
+ " ",
724
+ versionLabel
725
+ ] }),
726
+ updatedText ? /* @__PURE__ */ jsx2(Text, { children: ` ${updatedText}` }) : null,
727
+ statusLabel && statusColor ? /* @__PURE__ */ jsx2(Text, { color: statusColor, children: ` ${statusLabel}` }) : null
728
+ ] }),
729
+ showBranch ? /* @__PURE__ */ jsx2(Text, { color: "#e3839c", wrap: "truncate", children: branchLine }) : null,
730
+ showPath ? /* @__PURE__ */ jsx2(Text, { color: "#719bd8", wrap: "truncate", children: pathLine }) : null,
731
+ /* @__PURE__ */ jsx2(Text, { children: " " })
732
+ ] }),
733
+ /* @__PURE__ */ jsxs2(Box2, { marginLeft: 1, width: 1, flexDirection: "column", alignItems: "center", children: [
734
+ /* @__PURE__ */ jsx2(Text, { children: scrollbar.title }),
735
+ showBranch ? /* @__PURE__ */ jsx2(Text, { children: scrollbar.branch }) : null,
736
+ showPath ? /* @__PURE__ */ jsx2(Text, { children: scrollbar.path }) : null,
737
+ /* @__PURE__ */ jsx2(Text, { children: scrollbar.spacer })
738
+ ] })
739
+ ] });
740
+ };
741
+
742
+ // src/presentation/components/ProjectList.tsx
743
+ import { jsx as jsx3 } from "react/jsx-runtime";
744
+ var PROJECT_COLOR = "#abd8e7";
745
+ var LOCK_COLOR = "yellow";
746
+ var STATUS_LABELS = {
747
+ idle: "",
748
+ running: "[running]",
749
+ crashed: "[crash]"
750
+ };
673
751
  var extractRootFolder = (repository) => {
674
752
  if (!repository?.root) {
675
753
  return void 0;
@@ -680,13 +758,13 @@ var extractRootFolder = (repository) => {
680
758
  }
681
759
  return segments[segments.length - 1];
682
760
  };
683
- var formatProjectName = (project, repository, useGitRootName) => {
761
+ var formatProjectName = (projectTitle, repository, useGitRootName) => {
684
762
  if (!useGitRootName) {
685
- return project.title;
763
+ return projectTitle;
686
764
  }
687
765
  const rootFolder = extractRootFolder(repository);
688
766
  if (!rootFolder) {
689
- return project.title;
767
+ return projectTitle;
690
768
  }
691
769
  return rootFolder;
692
770
  };
@@ -750,76 +828,269 @@ var formatUpdatedText = (lastModified) => {
750
828
  }
751
829
  return `${UPDATED_LABEL} ${relativeTime}`;
752
830
  };
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]"
831
+ var ProjectList = ({
832
+ visibleProjects,
833
+ startIndex,
834
+ selectedIndex,
835
+ linesPerProject,
836
+ showBranch,
837
+ showPath,
838
+ useGitRootName,
839
+ releasedProjects,
840
+ launchedProjects,
841
+ totalProjects
842
+ }) => {
843
+ const scrollbarChars = useMemo(() => {
844
+ const totalLines = totalProjects * linesPerProject;
845
+ const windowProjects = visibleProjects.length;
846
+ const visibleLines = windowProjects * linesPerProject;
847
+ if (totalLines === 0 || visibleLines === 0) {
848
+ return [];
849
+ }
850
+ if (totalLines <= visibleLines) {
851
+ return Array.from({ length: visibleLines }, () => "\u2588");
852
+ }
853
+ const trackLength = visibleLines;
854
+ const sliderSize = Math.max(1, Math.round(visibleLines / totalLines * trackLength));
855
+ const maxSliderStart = Math.max(0, trackLength - sliderSize);
856
+ const topLine = startIndex * linesPerProject;
857
+ const denominator = Math.max(1, totalLines - visibleLines);
858
+ const sliderStart = Math.min(
859
+ maxSliderStart,
860
+ Math.round(topLine / denominator * maxSliderStart)
861
+ );
862
+ return Array.from({ length: trackLength }, (_, position) => {
863
+ if (position >= sliderStart && position < sliderStart + sliderSize) {
864
+ return "\u2588";
865
+ }
866
+ return "|";
867
+ });
868
+ }, [linesPerProject, startIndex, totalProjects, visibleProjects.length]);
869
+ const rows = useMemo(() => {
870
+ return visibleProjects.map(({ project, repository, launchStatus }, offset) => {
871
+ const rowIndex = startIndex + offset;
872
+ const isSelected = rowIndex === selectedIndex;
873
+ const selectionBar = isSelected ? "\u2503" : " ";
874
+ const projectName = formatProjectName(project.title, repository, useGitRootName);
875
+ const versionLabel = `(${project.version.value})`;
876
+ const updatedText = formatUpdatedText(project.lastModified);
877
+ const pathLine = shortenHomePath(project.path);
878
+ const branchLine = formatBranch(repository?.branch);
879
+ const hasReleasedLock = releasedProjects.has(project.id);
880
+ const isLocallyLaunched = launchedProjects.has(project.id);
881
+ const displayStatus = (() => {
882
+ if (isLocallyLaunched) {
883
+ return "running";
884
+ }
885
+ if (hasReleasedLock) {
886
+ return "idle";
887
+ }
888
+ return launchStatus;
889
+ })();
890
+ const baseScrollbarIndex = offset * linesPerProject;
891
+ const titleScrollbar = scrollbarChars[baseScrollbarIndex] ?? " ";
892
+ const branchScrollbar = showBranch ? scrollbarChars[baseScrollbarIndex + 1] ?? " " : " ";
893
+ const pathScrollbar = showPath ? scrollbarChars[baseScrollbarIndex + 1 + (showBranch ? 1 : 0)] ?? " " : " ";
894
+ const spacerScrollbar = scrollbarChars[baseScrollbarIndex + linesPerProject - 1] ?? " ";
895
+ const statusLabel = STATUS_LABELS[displayStatus];
896
+ const statusColor = displayStatus === "running" ? LOCK_COLOR : displayStatus === "crashed" ? "red" : void 0;
897
+ return /* @__PURE__ */ jsx3(
898
+ ProjectRow,
899
+ {
900
+ isSelected,
901
+ selectionBar,
902
+ projectName,
903
+ projectColor: PROJECT_COLOR,
904
+ versionLabel,
905
+ updatedText,
906
+ statusLabel,
907
+ statusColor,
908
+ branchLine,
909
+ pathLine,
910
+ showBranch,
911
+ showPath,
912
+ scrollbar: {
913
+ title: titleScrollbar,
914
+ branch: branchScrollbar,
915
+ path: pathScrollbar,
916
+ spacer: spacerScrollbar
917
+ }
918
+ },
919
+ project.id
920
+ );
921
+ });
922
+ }, [
923
+ launchedProjects,
924
+ releasedProjects,
925
+ scrollbarChars,
926
+ selectedIndex,
927
+ showBranch,
928
+ showPath,
929
+ startIndex,
930
+ useGitRootName,
931
+ visibleProjects,
932
+ linesPerProject
933
+ ]);
934
+ return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: rows.length === 0 ? null : rows });
765
935
  };
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)}`;
936
+
937
+ // src/presentation/components/SortPanel.tsx
938
+ import { Box as Box4, Text as Text2 } from "ink";
939
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
940
+ var lineForPrimary = (primary) => {
941
+ return `Primary: ${primary === "updated" ? "Updated" : "Name (Git root)"}`;
942
+ };
943
+ var lineForDirection = (prefs) => {
944
+ if (prefs.primary === "updated") {
945
+ return `Direction: ${prefs.direction === "desc" ? "New to Old" : "Old to New"}`;
775
946
  }
776
- return targetPath;
947
+ return `Direction: ${prefs.direction === "asc" ? "A to Z" : "Z to A"}`;
777
948
  };
778
- var buildCdCommand = (targetPath) => {
779
- return `cd ${targetPath}`;
949
+ var lineForFavorites = (favoritesFirst) => {
950
+ return `Favorites first: ${favoritesFirst ? "ON" : "OFF"}`;
780
951
  };
781
- var getCopyTargetPath = (view) => {
782
- const root = view.repository?.root;
783
- return root && root.length > 0 ? root : view.project.path;
952
+ var SortPanel = ({ sortPreferences, focusedIndex, width }) => {
953
+ const primaryLine = lineForPrimary(sortPreferences.primary);
954
+ const directionLine = lineForDirection(sortPreferences);
955
+ const favoritesLine = lineForFavorites(sortPreferences.favoritesFirst);
956
+ const Item = ({ label, selected }) => {
957
+ const prefix = selected ? "> " : " ";
958
+ return /* @__PURE__ */ jsxs3(Text2, { children: [
959
+ selected ? /* @__PURE__ */ jsx4(Text2, { color: "green", children: prefix }) : prefix,
960
+ label
961
+ ] });
962
+ };
963
+ return /* @__PURE__ */ jsxs3(
964
+ Box4,
965
+ {
966
+ flexDirection: "column",
967
+ borderStyle: "round",
968
+ borderColor: "green",
969
+ paddingX: 1,
970
+ width,
971
+ children: [
972
+ /* @__PURE__ */ jsx4(Item, { label: primaryLine, selected: focusedIndex === 0 }),
973
+ /* @__PURE__ */ jsx4(Item, { label: directionLine, selected: focusedIndex === 1 }),
974
+ /* @__PURE__ */ jsx4(Item, { label: favoritesLine, selected: focusedIndex === 2 })
975
+ ]
976
+ }
977
+ );
784
978
  };
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;
979
+
980
+ // src/presentation/hooks/useSortPreferences.ts
981
+ import { useEffect, useState } from "react";
982
+
983
+ // src/infrastructure/config.ts
984
+ import { mkdir, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
985
+ var defaultPreferences = {
986
+ favoritesFirst: true,
987
+ primary: "updated",
988
+ direction: "desc"
989
+ };
990
+ var getConfigDir = () => {
991
+ const home = process.env.HOME ?? "";
992
+ return `${home}/Library/Application Support/UnityHubCli`;
993
+ };
994
+ var getConfigPath = () => `${getConfigDir()}/config.json`;
995
+ var isValidPrimary = (value) => value === "updated" || value === "name";
996
+ var isValidDirection = (value) => value === "asc" || value === "desc";
997
+ var sanitizePreferences = (input) => {
998
+ if (!input || typeof input !== "object") {
999
+ return defaultPreferences;
1000
+ }
1001
+ const record = input;
1002
+ const favoritesFirst = typeof record.favoritesFirst === "boolean" ? record.favoritesFirst : defaultPreferences.favoritesFirst;
1003
+ const primary = isValidPrimary(record.primary) ? record.primary : defaultPreferences.primary;
1004
+ const direction = isValidDirection(record.direction) ? record.direction : defaultPreferences.direction;
1005
+ return { favoritesFirst, primary, direction };
1006
+ };
1007
+ var readSortPreferences = async () => {
1008
+ try {
1009
+ const content = await readFile3(getConfigPath(), "utf8");
1010
+ const json = JSON.parse(content);
1011
+ return sanitizePreferences(json);
1012
+ } catch {
1013
+ return defaultPreferences;
1014
+ }
1015
+ };
1016
+ var writeSortPreferences = async (prefs) => {
1017
+ try {
1018
+ await mkdir(getConfigDir(), { recursive: true });
1019
+ } catch {
797
1020
  }
798
- return false;
1021
+ const sanitized = sanitizePreferences(prefs);
1022
+ const json = JSON.stringify(sanitized, void 0, 2);
1023
+ await writeFile2(getConfigPath(), json, "utf8");
1024
+ };
1025
+ var getDefaultSortPreferences = () => defaultPreferences;
1026
+
1027
+ // src/presentation/hooks/useSortPreferences.ts
1028
+ var useSortPreferences = () => {
1029
+ const [sortPreferences, setSortPreferences] = useState(getDefaultSortPreferences());
1030
+ const [isLoaded, setIsLoaded] = useState(false);
1031
+ useEffect(() => {
1032
+ void (async () => {
1033
+ try {
1034
+ const prefs = await readSortPreferences();
1035
+ setSortPreferences(prefs);
1036
+ } finally {
1037
+ setIsLoaded(true);
1038
+ }
1039
+ })();
1040
+ }, []);
1041
+ useEffect(() => {
1042
+ if (!isLoaded) {
1043
+ return;
1044
+ }
1045
+ void writeSortPreferences(sortPreferences);
1046
+ }, [isLoaded, sortPreferences]);
1047
+ return { sortPreferences, setSortPreferences, isLoaded };
799
1048
  };
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;
1049
+
1050
+ // src/presentation/hooks/useVisibleCount.ts
1051
+ import { useEffect as useEffect2, useState as useState2 } from "react";
1052
+ var useVisibleCount = (stdout, linesPerProject, panelVisible, panelHeight, minimumVisibleProjectCount2) => {
1053
+ const compute = () => {
1054
+ if (!stdout || typeof stdout.columns !== "number" || typeof stdout.rows !== "number") {
1055
+ return minimumVisibleProjectCount2;
1056
+ }
1057
+ const borderRows = 2;
1058
+ const hintRows = 1;
1059
+ const reservedRows = borderRows + hintRows + (panelVisible ? panelHeight : 0);
1060
+ const availableRows = Math.max(0, stdout.rows - reservedRows);
1061
+ const rowsPerProject = Math.max(linesPerProject, 1);
1062
+ const calculatedCount = Math.max(1, Math.floor(availableRows / rowsPerProject));
1063
+ return calculatedCount;
1064
+ };
1065
+ const [visibleCount, setVisibleCount] = useState2(compute);
1066
+ useEffect2(() => {
1067
+ const updateVisible = () => setVisibleCount(compute());
1068
+ updateVisible();
1069
+ stdout?.on("resize", updateVisible);
1070
+ return () => {
1071
+ stdout?.off("resize", updateVisible);
1072
+ };
1073
+ }, [stdout, linesPerProject, panelVisible, panelHeight]);
1074
+ return visibleCount;
805
1075
  };
806
- var stringWidth = (text) => {
807
- let width = 0;
808
- for (const ch of text) {
809
- width += charWidth(ch);
1076
+
1077
+ // src/presentation/App.tsx
1078
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1079
+ var extractRootFolder2 = (repository) => {
1080
+ if (!repository?.root) {
1081
+ return void 0;
810
1082
  }
811
- return width;
1083
+ const segments = repository.root.split("/").filter((segment) => segment.length > 0);
1084
+ if (segments.length === 0) {
1085
+ return void 0;
1086
+ }
1087
+ return segments[segments.length - 1];
812
1088
  };
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;
1089
+ var minimumVisibleProjectCount = 4;
1090
+ var defaultHintMessage = "Select: j/k \xB7 Open: o \xB7 Quit: q \xB7 Refresh: r \xB7 CopyPath: c \xB7 Sort: s \xB7 Close: ctrl + c";
1091
+ var getCopyTargetPath = (view) => {
1092
+ const root = view.repository?.root;
1093
+ return root && root.length > 0 ? root : view.project.path;
823
1094
  };
824
1095
  var App = ({
825
1096
  projects,
@@ -832,30 +1103,23 @@ var App = ({
832
1103
  }) => {
833
1104
  const { exit } = useApp();
834
1105
  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);
1106
+ const [projectViews, setProjectViews] = useState3(projects);
1107
+ const [isSortMenuOpen, setIsSortMenuOpen] = useState3(false);
848
1108
  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(() => {
1109
+ const panelHeight = 6;
1110
+ const visibleCount = useVisibleCount(stdout, linesPerProject, isSortMenuOpen, panelHeight, minimumVisibleProjectCount);
1111
+ const [index, setIndex] = useState3(0);
1112
+ const [hint, setHint] = useState3(defaultHintMessage);
1113
+ const [windowStart, setWindowStart] = useState3(0);
1114
+ const [releasedProjects, setReleasedProjects] = useState3(/* @__PURE__ */ new Set());
1115
+ const [launchedProjects, setLaunchedProjects] = useState3(/* @__PURE__ */ new Set());
1116
+ const [isRefreshing, setIsRefreshing] = useState3(false);
1117
+ const [sortMenuIndex, setSortMenuIndex] = useState3(0);
1118
+ const { sortPreferences, setSortPreferences } = useSortPreferences();
1119
+ const sortedProjects = useMemo2(() => {
856
1120
  const fallbackTime = 0;
857
1121
  const getNameKey = (view) => {
858
- const rootName = extractRootFolder(view.repository);
1122
+ const rootName = extractRootFolder2(view.repository);
859
1123
  const base = rootName || view.project.title;
860
1124
  return base.toLocaleLowerCase();
861
1125
  };
@@ -904,24 +1168,7 @@ var App = ({
904
1168
  return tieBreaker(a).localeCompare(tieBreaker(b));
905
1169
  });
906
1170
  }, [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(() => {
1171
+ useEffect3(() => {
925
1172
  const handleSigint = () => {
926
1173
  exit();
927
1174
  };
@@ -930,26 +1177,6 @@ var App = ({
930
1177
  process.off("SIGINT", handleSigint);
931
1178
  };
932
1179
  }, [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
1180
  const limit = Math.max(1, visibleCount);
954
1181
  const move = useCallback(
955
1182
  (delta) => {
@@ -994,7 +1221,7 @@ var App = ({
994
1221
  },
995
1222
  [index, limit, sortedProjects.length]
996
1223
  );
997
- useEffect(() => {
1224
+ useEffect3(() => {
998
1225
  setWindowStart((prevStart) => {
999
1226
  if (sortedProjects.length <= limit) {
1000
1227
  return prevStart === 0 ? prevStart : 0;
@@ -1137,7 +1364,7 @@ var App = ({
1137
1364
  }, 3e3);
1138
1365
  }
1139
1366
  }, [index, onTerminate, sortedProjects]);
1140
- useEffect(() => {
1367
+ useEffect3(() => {
1141
1368
  setProjectViews(projects);
1142
1369
  setReleasedProjects(/* @__PURE__ */ new Set());
1143
1370
  setLaunchedProjects(/* @__PURE__ */ new Set());
@@ -1198,10 +1425,7 @@ var App = ({
1198
1425
  useInput((input, key) => {
1199
1426
  if (isSortMenuOpen) {
1200
1427
  if (key.escape || input === "\x1B") {
1201
- clearOverlay();
1202
- forceFullRepaint();
1203
1428
  setIsSortMenuOpen(false);
1204
- setRenderEpoch((prev) => prev + 1);
1205
1429
  return;
1206
1430
  }
1207
1431
  if (input === "j") {
@@ -1262,7 +1486,7 @@ var App = ({
1262
1486
  copyProjectPath();
1263
1487
  }
1264
1488
  });
1265
- const { startIndex, visibleProjects } = useMemo(() => {
1489
+ const { startIndex, visibleProjects } = useMemo2(() => {
1266
1490
  if (sortedProjects.length <= limit) {
1267
1491
  return {
1268
1492
  startIndex: 0,
@@ -1277,219 +1501,53 @@ var App = ({
1277
1501
  visibleProjects: sortedProjects.slice(clampedStart, end)
1278
1502
  };
1279
1503
  }, [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";
1504
+ return /* @__PURE__ */ jsx5(
1505
+ LayoutManager,
1506
+ {
1507
+ layoutMode: getLayoutMode(),
1508
+ panelVisible: isSortMenuOpen,
1509
+ list: /* @__PURE__ */ jsx5(
1510
+ Box5,
1511
+ {
1512
+ flexDirection: "column",
1513
+ borderStyle: "round",
1514
+ borderColor: "green",
1515
+ width: typeof stdout?.columns === "number" ? stdout.columns : void 0,
1516
+ children: sortedProjects.length === 0 ? /* @__PURE__ */ jsx5(Text3, { children: "No Unity Hub projects were found." }) : /* @__PURE__ */ jsx5(
1517
+ ProjectList,
1518
+ {
1519
+ visibleProjects,
1520
+ startIndex,
1521
+ selectedIndex: index,
1522
+ linesPerProject,
1523
+ showBranch,
1524
+ showPath,
1525
+ useGitRootName,
1526
+ releasedProjects,
1527
+ launchedProjects,
1528
+ totalProjects: sortedProjects.length
1529
+ }
1530
+ )
1325
1531
  }
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
- }
1532
+ ),
1533
+ panel: /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", width: typeof stdout?.columns === "number" ? stdout.columns : void 0, children: [
1534
+ /* @__PURE__ */ jsx5(Text3, { children: "Sort Settings" }),
1535
+ /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(
1536
+ SortPanel,
1537
+ {
1538
+ sortPreferences,
1539
+ focusedIndex: sortMenuIndex,
1540
+ width: typeof stdout?.columns === "number" ? stdout.columns : void 0
1541
+ }
1542
+ ) })
1543
+ ] }),
1544
+ statusBar: isSortMenuOpen ? /* @__PURE__ */ jsx5(Text3, { wrap: "truncate", children: "Select: j/k, Toggle: Space, Back: Esc" }) : /* @__PURE__ */ jsx5(Text3, { wrap: "truncate", children: hint })
1482
1545
  }
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);
1546
+ );
1489
1547
  };
1490
1548
 
1491
1549
  // src/index.tsx
1492
- import { jsx as jsx2 } from "react/jsx-runtime";
1550
+ import { jsx as jsx6 } from "react/jsx-runtime";
1493
1551
  var bootstrap = async () => {
1494
1552
  const unityHubReader = new UnityHubProjectsReader();
1495
1553
  const gitRepositoryInfoReader = new GitRepositoryInfoReader();
@@ -1525,7 +1583,7 @@ var bootstrap = async () => {
1525
1583
  try {
1526
1584
  const projects = await listProjectsUseCase.execute();
1527
1585
  const { waitUntilExit } = render(
1528
- /* @__PURE__ */ jsx2(
1586
+ /* @__PURE__ */ jsx6(
1529
1587
  App,
1530
1588
  {
1531
1589
  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.0",
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": {