unity-hub-cli 0.8.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 +537 -191
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -621,9 +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
+
627
+ // src/presentation/components/LayoutManager.tsx
628
+ import { Box } from "ink";
626
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";
635
+ };
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
+ ] });
651
+ }
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
+ ] });
663
+ };
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)}`;
683
+ }
684
+ return targetPath;
685
+ };
686
+ var buildCdCommand = (targetPath) => {
687
+ if (process.platform === "win32") {
688
+ const escapedForWindows = targetPath.replace(/"/g, '""');
689
+ return `cd "${escapedForWindows}"`;
690
+ }
691
+ const escapedForPosix = targetPath.replace(/'/g, "'\\''");
692
+ return `cd '${escapedForPosix}'`;
693
+ };
694
+
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
+ };
627
751
  var extractRootFolder = (repository) => {
628
752
  if (!repository?.root) {
629
753
  return void 0;
@@ -634,13 +758,13 @@ var extractRootFolder = (repository) => {
634
758
  }
635
759
  return segments[segments.length - 1];
636
760
  };
637
- var formatProjectName = (project, repository, useGitRootName) => {
761
+ var formatProjectName = (projectTitle, repository, useGitRootName) => {
638
762
  if (!useGitRootName) {
639
- return project.title;
763
+ return projectTitle;
640
764
  }
641
765
  const rootFolder = extractRootFolder(repository);
642
766
  if (!rootFolder) {
643
- return project.title;
767
+ return projectTitle;
644
768
  }
645
769
  return rootFolder;
646
770
  };
@@ -704,34 +828,269 @@ var formatUpdatedText = (lastModified) => {
704
828
  }
705
829
  return `${UPDATED_LABEL} ${relativeTime}`;
706
830
  };
707
- var homeDirectory = process.env.HOME ?? "";
708
- var homePrefix = homeDirectory ? `${homeDirectory}/` : "";
709
- var minimumVisibleProjectCount = 4;
710
- var defaultHintMessage = "Move with arrows or j/k \xB7 Launch with o \xB7 Quit Unity with q \xB7 Refresh with r \xB7 Copy cd path with c";
711
- var PROJECT_COLOR = "#abd8e7";
712
- var BRANCH_COLOR = "#e3839c";
713
- var PATH_COLOR = "#719bd8";
714
- var LOCK_COLOR = "yellow";
715
- var STATUS_LABELS = {
716
- idle: "",
717
- running: "[running]",
718
- 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 });
719
935
  };
720
- var shortenHomePath = (targetPath) => {
721
- if (!homeDirectory) {
722
- return targetPath;
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"}`;
723
946
  }
724
- if (targetPath === homeDirectory) {
725
- return "~";
947
+ return `Direction: ${prefs.direction === "asc" ? "A to Z" : "Z to A"}`;
948
+ };
949
+ var lineForFavorites = (favoritesFirst) => {
950
+ return `Favorites first: ${favoritesFirst ? "ON" : "OFF"}`;
951
+ };
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
+ );
978
+ };
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;
726
1000
  }
727
- if (homePrefix && targetPath.startsWith(homePrefix)) {
728
- return `~/${targetPath.slice(homePrefix.length)}`;
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;
729
1014
  }
730
- return targetPath;
731
1015
  };
732
- var buildCdCommand = (targetPath) => {
733
- const escapedPath = targetPath.replaceAll('"', '\\"');
734
- return `cd "${escapedPath}"`;
1016
+ var writeSortPreferences = async (prefs) => {
1017
+ try {
1018
+ await mkdir(getConfigDir(), { recursive: true });
1019
+ } catch {
1020
+ }
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 };
1048
+ };
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;
1075
+ };
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;
1082
+ }
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];
1088
+ };
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;
735
1094
  };
736
1095
  var App = ({
737
1096
  projects,
@@ -744,45 +1103,72 @@ var App = ({
744
1103
  }) => {
745
1104
  const { exit } = useApp();
746
1105
  const { stdout } = useStdout();
747
- const [projectViews, setProjectViews] = useState(projects);
748
- const [visibleCount, setVisibleCount] = useState(minimumVisibleProjectCount);
749
- const [index, setIndex] = useState(0);
750
- const [hint, setHint] = useState(defaultHintMessage);
751
- const [windowStart, setWindowStart] = useState(0);
752
- const [releasedProjects, setReleasedProjects] = useState(/* @__PURE__ */ new Set());
753
- const [launchedProjects, setLaunchedProjects] = useState(/* @__PURE__ */ new Set());
754
- const [isRefreshing, setIsRefreshing] = useState(false);
1106
+ const [projectViews, setProjectViews] = useState3(projects);
1107
+ const [isSortMenuOpen, setIsSortMenuOpen] = useState3(false);
755
1108
  const linesPerProject = (showBranch ? 1 : 0) + (showPath ? 1 : 0) + 2;
756
- 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(() => {
757
1120
  const fallbackTime = 0;
758
- const toSortKey = (view) => {
759
- if (useGitRootName) {
760
- const rootName = extractRootFolder(view.repository);
761
- if (rootName) {
762
- return rootName.toLocaleLowerCase();
763
- }
1121
+ const getNameKey = (view) => {
1122
+ const rootName = extractRootFolder2(view.repository);
1123
+ const base = rootName || view.project.title;
1124
+ return base.toLocaleLowerCase();
1125
+ };
1126
+ const tieBreaker = (view) => view.project.path.toLocaleLowerCase();
1127
+ const compareByUpdated = (a, b, direction) => {
1128
+ const timeA = a.project.lastModified?.getTime() ?? fallbackTime;
1129
+ const timeB = b.project.lastModified?.getTime() ?? fallbackTime;
1130
+ if (timeA === timeB) {
1131
+ return 0;
764
1132
  }
765
- return view.project.title.toLocaleLowerCase();
1133
+ return direction === "desc" ? timeB - timeA : timeA - timeB;
1134
+ };
1135
+ const compareByName = (a, b, direction) => {
1136
+ const keyA = getNameKey(a);
1137
+ const keyB = getNameKey(b);
1138
+ if (keyA === keyB) {
1139
+ return 0;
1140
+ }
1141
+ return direction === "desc" ? keyB.localeCompare(keyA) : keyA.localeCompare(keyB);
766
1142
  };
767
- const toTieBreaker = (view) => view.project.path.toLocaleLowerCase();
768
1143
  return [...projectViews].sort((a, b) => {
769
- if (a.project.favorite !== b.project.favorite) {
1144
+ if (sortPreferences.favoritesFirst && a.project.favorite !== b.project.favorite) {
770
1145
  return a.project.favorite ? -1 : 1;
771
1146
  }
772
- const timeA = a.project.lastModified?.getTime() ?? fallbackTime;
773
- const timeB = b.project.lastModified?.getTime() ?? fallbackTime;
774
- if (timeA !== timeB) {
775
- return timeB - timeA;
1147
+ const primary = sortPreferences.primary;
1148
+ const direction = sortPreferences.direction;
1149
+ if (primary === "updated") {
1150
+ const updatedOrder2 = compareByUpdated(a, b, direction);
1151
+ if (updatedOrder2 !== 0) {
1152
+ return updatedOrder2;
1153
+ }
1154
+ const nameOrder2 = compareByName(a, b, "asc");
1155
+ if (nameOrder2 !== 0) {
1156
+ return nameOrder2;
1157
+ }
1158
+ return tieBreaker(a).localeCompare(tieBreaker(b));
776
1159
  }
777
- const keyA = toSortKey(a);
778
- const keyB = toSortKey(b);
779
- if (keyA === keyB) {
780
- return toTieBreaker(a).localeCompare(toTieBreaker(b));
1160
+ const nameOrder = compareByName(a, b, direction);
1161
+ if (nameOrder !== 0) {
1162
+ return nameOrder;
781
1163
  }
782
- return keyA.localeCompare(keyB);
1164
+ const updatedOrder = compareByUpdated(a, b, "desc");
1165
+ if (updatedOrder !== 0) {
1166
+ return updatedOrder;
1167
+ }
1168
+ return tieBreaker(a).localeCompare(tieBreaker(b));
783
1169
  });
784
- }, [projectViews, useGitRootName]);
785
- useEffect(() => {
1170
+ }, [projectViews, sortPreferences]);
1171
+ useEffect3(() => {
786
1172
  const handleSigint = () => {
787
1173
  exit();
788
1174
  };
@@ -791,30 +1177,7 @@ var App = ({
791
1177
  process.off("SIGINT", handleSigint);
792
1178
  };
793
1179
  }, [exit]);
794
- useEffect(() => {
795
- const updateVisibleCount = () => {
796
- if (!stdout || typeof stdout.columns !== "number" || typeof stdout.rows !== "number") {
797
- setVisibleCount(minimumVisibleProjectCount);
798
- return;
799
- }
800
- const borderRows = 2;
801
- const hintRows = 1;
802
- const reservedRows = borderRows + hintRows;
803
- const availableRows = stdout.rows - reservedRows;
804
- const rowsPerProject = Math.max(linesPerProject, 1);
805
- const calculatedCount = Math.max(
806
- minimumVisibleProjectCount,
807
- Math.floor(availableRows / rowsPerProject)
808
- );
809
- setVisibleCount(calculatedCount);
810
- };
811
- updateVisibleCount();
812
- stdout?.on("resize", updateVisibleCount);
813
- return () => {
814
- stdout?.off("resize", updateVisibleCount);
815
- };
816
- }, [linesPerProject, stdout]);
817
- const limit = Math.max(minimumVisibleProjectCount, visibleCount);
1180
+ const limit = Math.max(1, visibleCount);
818
1181
  const move = useCallback(
819
1182
  (delta) => {
820
1183
  setIndex((prev) => {
@@ -858,7 +1221,7 @@ var App = ({
858
1221
  },
859
1222
  [index, limit, sortedProjects.length]
860
1223
  );
861
- useEffect(() => {
1224
+ useEffect3(() => {
862
1225
  setWindowStart((prevStart) => {
863
1226
  if (sortedProjects.length <= limit) {
864
1227
  return prevStart === 0 ? prevStart : 0;
@@ -880,7 +1243,8 @@ var App = ({
880
1243
  });
881
1244
  }, [index, limit, sortedProjects.length]);
882
1245
  const copyProjectPath = useCallback(() => {
883
- const projectPath = sortedProjects[index]?.project.path;
1246
+ const projectView = sortedProjects[index];
1247
+ const projectPath = projectView ? getCopyTargetPath(projectView) : void 0;
884
1248
  if (!projectPath) {
885
1249
  setHint("No project to copy");
886
1250
  setTimeout(() => {
@@ -892,7 +1256,7 @@ var App = ({
892
1256
  const command = buildCdCommand(projectPath);
893
1257
  clipboard.writeSync(command);
894
1258
  const displayPath = shortenHomePath(projectPath);
895
- setHint(`Copied command: cd "${displayPath}"`);
1259
+ setHint(`Copied command: cd ${displayPath}`);
896
1260
  } catch (error) {
897
1261
  const message = error instanceof Error ? error.message : String(error);
898
1262
  setHint(`Failed to copy: ${message}`);
@@ -912,7 +1276,8 @@ var App = ({
912
1276
  }
913
1277
  const { project } = projectView;
914
1278
  try {
915
- const command = buildCdCommand(project.path);
1279
+ const cdTarget = getCopyTargetPath(projectView);
1280
+ const command = buildCdCommand(cdTarget);
916
1281
  clipboard.writeSync(command);
917
1282
  } catch (error) {
918
1283
  const message = error instanceof Error ? error.message : String(error);
@@ -999,7 +1364,7 @@ var App = ({
999
1364
  }, 3e3);
1000
1365
  }
1001
1366
  }, [index, onTerminate, sortedProjects]);
1002
- useEffect(() => {
1367
+ useEffect3(() => {
1003
1368
  setProjectViews(projects);
1004
1369
  setReleasedProjects(/* @__PURE__ */ new Set());
1005
1370
  setLaunchedProjects(/* @__PURE__ */ new Set());
@@ -1058,6 +1423,47 @@ var App = ({
1058
1423
  }
1059
1424
  }, [isRefreshing, onRefresh, sortedProjects]);
1060
1425
  useInput((input, key) => {
1426
+ if (isSortMenuOpen) {
1427
+ if (key.escape || input === "\x1B") {
1428
+ setIsSortMenuOpen(false);
1429
+ return;
1430
+ }
1431
+ if (input === "j") {
1432
+ setSortMenuIndex((prev) => {
1433
+ const last = 2;
1434
+ const next = prev + 1;
1435
+ return next > last ? 0 : next;
1436
+ });
1437
+ return;
1438
+ }
1439
+ if (input === "k") {
1440
+ setSortMenuIndex((prev) => {
1441
+ const last = 2;
1442
+ const next = prev - 1;
1443
+ return next < 0 ? last : next;
1444
+ });
1445
+ return;
1446
+ }
1447
+ const toggleCurrent = () => {
1448
+ if (sortMenuIndex === 0) {
1449
+ setSortPreferences((prev) => ({ ...prev, primary: prev.primary === "updated" ? "name" : "updated" }));
1450
+ return;
1451
+ }
1452
+ if (sortMenuIndex === 1) {
1453
+ setSortPreferences((prev) => ({ ...prev, direction: prev.direction === "asc" ? "desc" : "asc" }));
1454
+ return;
1455
+ }
1456
+ setSortPreferences((prev) => ({ ...prev, favoritesFirst: !prev.favoritesFirst }));
1457
+ };
1458
+ if (input === " ") {
1459
+ toggleCurrent();
1460
+ }
1461
+ return;
1462
+ }
1463
+ if (input === "S" || input === "s") {
1464
+ setIsSortMenuOpen(true);
1465
+ return;
1466
+ }
1061
1467
  if (input === "j" || key.downArrow) {
1062
1468
  move(1);
1063
1469
  }
@@ -1080,7 +1486,7 @@ var App = ({
1080
1486
  copyProjectPath();
1081
1487
  }
1082
1488
  });
1083
- const { startIndex, visibleProjects } = useMemo(() => {
1489
+ const { startIndex, visibleProjects } = useMemo2(() => {
1084
1490
  if (sortedProjects.length <= limit) {
1085
1491
  return {
1086
1492
  startIndex: 0,
@@ -1095,113 +1501,53 @@ var App = ({
1095
1501
  visibleProjects: sortedProjects.slice(clampedStart, end)
1096
1502
  };
1097
1503
  }, [limit, sortedProjects, windowStart]);
1098
- const scrollbarChars = useMemo(() => {
1099
- const totalProjects = projectViews.length;
1100
- const totalLines = totalProjects * linesPerProject;
1101
- const windowProjects = visibleProjects.length;
1102
- const visibleLines = windowProjects * linesPerProject;
1103
- if (totalLines === 0 || visibleLines === 0) {
1104
- return [];
1105
- }
1106
- if (totalLines <= visibleLines) {
1107
- return Array.from({ length: visibleLines }, () => "\u2588");
1108
- }
1109
- const trackLength = visibleLines;
1110
- const sliderSize = Math.max(1, Math.round(visibleLines / totalLines * trackLength));
1111
- const maxSliderStart = Math.max(0, trackLength - sliderSize);
1112
- const topLine = startIndex * linesPerProject;
1113
- const denominator = Math.max(1, totalLines - visibleLines);
1114
- const sliderStart = Math.min(
1115
- maxSliderStart,
1116
- Math.round(topLine / denominator * maxSliderStart)
1117
- );
1118
- return Array.from({ length: trackLength }, (_, position) => {
1119
- if (position >= sliderStart && position < sliderStart + sliderSize) {
1120
- return "\u2588";
1121
- }
1122
- return "|";
1123
- });
1124
- }, [linesPerProject, projectViews.length, startIndex, visibleProjects]);
1125
- const rows = useMemo(() => {
1126
- return visibleProjects.map(({ project, repository, launchStatus }, offset) => {
1127
- const rowIndex = startIndex + offset;
1128
- const isSelected = rowIndex === index;
1129
- const arrow = isSelected ? ">" : " ";
1130
- const projectName = formatProjectName(project, repository, useGitRootName);
1131
- const versionLabel = `(${project.version.value})`;
1132
- const updatedText = formatUpdatedText(project.lastModified);
1133
- const pathLine = shortenHomePath(project.path);
1134
- const branchLine = formatBranch(repository?.branch);
1135
- const hasReleasedLock = releasedProjects.has(project.id);
1136
- const isLocallyLaunched = launchedProjects.has(project.id);
1137
- const displayStatus = (() => {
1138
- if (isLocallyLaunched) {
1139
- return "running";
1140
- }
1141
- if (hasReleasedLock) {
1142
- 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
+ )
1143
1531
  }
1144
- return launchStatus;
1145
- })();
1146
- const baseScrollbarIndex = offset * linesPerProject;
1147
- const titleScrollbar = scrollbarChars[baseScrollbarIndex] ?? " ";
1148
- const branchScrollbar = showBranch ? scrollbarChars[baseScrollbarIndex + 1] ?? " " : " ";
1149
- const pathScrollbar = showPath ? scrollbarChars[baseScrollbarIndex + 1 + (showBranch ? 1 : 0)] ?? " " : " ";
1150
- const spacerScrollbar = scrollbarChars[baseScrollbarIndex + linesPerProject - 1] ?? " ";
1151
- const statusLabel = STATUS_LABELS[displayStatus];
1152
- const statusColor = displayStatus === "running" ? LOCK_COLOR : displayStatus === "crashed" ? "red" : void 0;
1153
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
1154
- /* @__PURE__ */ jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [
1155
- /* @__PURE__ */ jsxs(Text, { children: [
1156
- /* @__PURE__ */ jsxs(Text, { color: isSelected ? "green" : PROJECT_COLOR, bold: true, children: [
1157
- arrow,
1158
- " ",
1159
- projectName
1160
- ] }),
1161
- /* @__PURE__ */ jsxs(Text, { color: isSelected ? "green" : void 0, children: [
1162
- " ",
1163
- versionLabel
1164
- ] }),
1165
- updatedText ? /* @__PURE__ */ jsx(Text, { color: isSelected ? "green" : void 0, children: ` ${updatedText}` }) : null,
1166
- statusLabel && statusColor ? /* @__PURE__ */ jsx(Text, { color: statusColor, children: ` ${statusLabel}` }) : null
1167
- ] }),
1168
- showBranch ? /* @__PURE__ */ jsxs(Text, { color: isSelected ? "green" : BRANCH_COLOR, children: [
1169
- " ",
1170
- branchLine
1171
- ] }) : null,
1172
- showPath ? /* @__PURE__ */ jsxs(Text, { color: isSelected ? "green" : PATH_COLOR, children: [
1173
- " ",
1174
- pathLine
1175
- ] }) : null,
1176
- /* @__PURE__ */ jsx(Text, { children: " " })
1177
- ] }),
1178
- /* @__PURE__ */ jsxs(Box, { marginLeft: 1, width: 1, flexDirection: "column", alignItems: "center", children: [
1179
- /* @__PURE__ */ jsx(Text, { children: titleScrollbar }),
1180
- showBranch ? /* @__PURE__ */ jsx(Text, { children: branchScrollbar }) : null,
1181
- showPath ? /* @__PURE__ */ jsx(Text, { children: pathScrollbar }) : null,
1182
- /* @__PURE__ */ jsx(Text, { children: spacerScrollbar })
1183
- ] })
1184
- ] }, project.id);
1185
- });
1186
- }, [
1187
- index,
1188
- launchedProjects,
1189
- releasedProjects,
1190
- scrollbarChars,
1191
- showBranch,
1192
- showPath,
1193
- startIndex,
1194
- useGitRootName,
1195
- visibleProjects
1196
- ]);
1197
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1198
- /* @__PURE__ */ jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", children: rows.length === 0 ? /* @__PURE__ */ jsx(Text, { children: "No Unity Hub projects were found." }) : rows }),
1199
- /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { children: hint }) })
1200
- ] });
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 })
1545
+ }
1546
+ );
1201
1547
  };
1202
1548
 
1203
1549
  // src/index.tsx
1204
- import { jsx as jsx2 } from "react/jsx-runtime";
1550
+ import { jsx as jsx6 } from "react/jsx-runtime";
1205
1551
  var bootstrap = async () => {
1206
1552
  const unityHubReader = new UnityHubProjectsReader();
1207
1553
  const gitRepositoryInfoReader = new GitRepositoryInfoReader();
@@ -1237,7 +1583,7 @@ var bootstrap = async () => {
1237
1583
  try {
1238
1584
  const projects = await listProjectsUseCase.execute();
1239
1585
  const { waitUntilExit } = render(
1240
- /* @__PURE__ */ jsx2(
1586
+ /* @__PURE__ */ jsx6(
1241
1587
  App,
1242
1588
  {
1243
1589
  projects,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unity-hub-cli",
3
- "version": "0.8.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": {