unity-hub-cli 0.7.0 → 0.9.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 +382 -59
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -72,9 +72,10 @@ var LaunchProjectUseCase = class {
72
72
  }
73
73
  };
74
74
  var TerminateProjectUseCase = class {
75
- constructor(unityProcessReader, unityProcessTerminator) {
75
+ constructor(unityProcessReader, unityProcessTerminator, unityTempDirectoryCleaner) {
76
76
  this.unityProcessReader = unityProcessReader;
77
77
  this.unityProcessTerminator = unityProcessTerminator;
78
+ this.unityTempDirectoryCleaner = unityTempDirectoryCleaner;
78
79
  }
79
80
  async execute(project) {
80
81
  const unityProcess = await this.unityProcessReader.findByProjectPath(project.path);
@@ -84,13 +85,21 @@ var TerminateProjectUseCase = class {
84
85
  message: "No Unity process is running for this project."
85
86
  };
86
87
  }
87
- const terminated = await this.unityProcessTerminator.terminate(unityProcess);
88
- if (!terminated) {
88
+ const termination = await this.unityProcessTerminator.terminate(unityProcess);
89
+ if (!termination.terminated) {
89
90
  return {
90
91
  terminated: false,
91
92
  message: "Failed to terminate the Unity process."
92
93
  };
93
94
  }
95
+ if (termination.stage === "sigterm" || termination.stage === "sigkill") {
96
+ try {
97
+ await this.unityTempDirectoryCleaner.clean(project.path);
98
+ } catch (error) {
99
+ const message = error instanceof Error ? error.message : String(error);
100
+ console.error(`Failed to clean Temp directory after termination: ${message}`);
101
+ }
102
+ }
94
103
  return {
95
104
  terminated: true
96
105
  };
@@ -436,6 +445,8 @@ var PROCESS_LIST_ARGS = ["-axo", "pid=,command=", "-ww"];
436
445
  var PROCESS_LIST_COMMAND = "ps";
437
446
  var TERMINATE_TIMEOUT_MILLIS = 5e3;
438
447
  var TERMINATE_POLL_INTERVAL_MILLIS = 200;
448
+ var GRACEFUL_QUIT_TIMEOUT_MILLIS = 3e3;
449
+ var GRACEFUL_QUIT_POLL_INTERVAL_MILLIS = 200;
439
450
  var delay = async (duration) => {
440
451
  await new Promise((resolveDelay) => {
441
452
  setTimeout(() => {
@@ -535,11 +546,33 @@ var MacUnityProcessReader = class {
535
546
  };
536
547
  var MacUnityProcessTerminator = class {
537
548
  async terminate(unityProcess) {
549
+ let attemptedGraceful = false;
550
+ if (process.platform === "darwin") {
551
+ attemptedGraceful = true;
552
+ try {
553
+ const script = [
554
+ 'tell application "System Events"',
555
+ ` set frontmost of (first process whose unix id is ${unityProcess.pid}) to true`,
556
+ ' keystroke "q" using {command down}',
557
+ "end tell"
558
+ ].join("\n");
559
+ await execFileAsync2("osascript", ["-e", script]);
560
+ const deadlineGraceful = Date.now() + GRACEFUL_QUIT_TIMEOUT_MILLIS;
561
+ while (Date.now() < deadlineGraceful) {
562
+ await delay(GRACEFUL_QUIT_POLL_INTERVAL_MILLIS);
563
+ const alive = ensureProcessAlive(unityProcess.pid);
564
+ if (!alive) {
565
+ return { terminated: true, stage: "graceful" };
566
+ }
567
+ }
568
+ } catch {
569
+ }
570
+ }
538
571
  try {
539
572
  process.kill(unityProcess.pid, "SIGTERM");
540
573
  } catch (error) {
541
574
  if (isProcessMissingError(error)) {
542
- return false;
575
+ return { terminated: true, stage: attemptedGraceful ? "graceful" : "sigterm" };
543
576
  }
544
577
  throw new Error(
545
578
  `Failed to terminate the Unity process (PID: ${unityProcess.pid}): ${error instanceof Error ? error.message : String(error)}`
@@ -550,21 +583,22 @@ var MacUnityProcessTerminator = class {
550
583
  await delay(TERMINATE_POLL_INTERVAL_MILLIS);
551
584
  const alive = ensureProcessAlive(unityProcess.pid);
552
585
  if (!alive) {
553
- return true;
586
+ return { terminated: true, stage: "sigterm" };
554
587
  }
555
588
  }
556
589
  try {
557
590
  process.kill(unityProcess.pid, "SIGKILL");
558
591
  } catch (error) {
559
592
  if (isProcessMissingError(error)) {
560
- return true;
593
+ return { terminated: true, stage: "sigkill" };
561
594
  }
562
595
  throw new Error(
563
596
  `Failed to forcefully terminate the Unity process (PID: ${unityProcess.pid}): ${error instanceof Error ? error.message : String(error)}`
564
597
  );
565
598
  }
566
599
  await delay(TERMINATE_POLL_INTERVAL_MILLIS);
567
- return !ensureProcessAlive(unityProcess.pid);
600
+ const aliveAfterKill = ensureProcessAlive(unityProcess.pid);
601
+ return aliveAfterKill ? { terminated: false } : { terminated: true, stage: "sigkill" };
568
602
  }
569
603
  };
570
604
 
@@ -589,6 +623,52 @@ var UnityTempDirectoryCleaner = class {
589
623
  import clipboard from "clipboardy";
590
624
  import { Box, Text, useApp, useInput, useStdout } from "ink";
591
625
  import { useCallback, useEffect, useMemo, useState } from "react";
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`;
637
+ };
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;
644
+ }
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 };
650
+ };
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;
658
+ }
659
+ };
660
+ var writeSortPreferences = async (prefs) => {
661
+ try {
662
+ await mkdir(getConfigDir(), { recursive: true });
663
+ } catch {
664
+ }
665
+ const sanitized = sanitizePreferences(prefs);
666
+ const json = JSON.stringify(sanitized, void 0, 2);
667
+ await writeFile2(getConfigPath(), json, "utf8");
668
+ };
669
+ var getDefaultSortPreferences = () => defaultPreferences;
670
+
671
+ // src/presentation/App.tsx
592
672
  import { jsx, jsxs } from "react/jsx-runtime";
593
673
  var extractRootFolder = (repository) => {
594
674
  if (!repository?.root) {
@@ -673,7 +753,7 @@ var formatUpdatedText = (lastModified) => {
673
753
  var homeDirectory = process.env.HOME ?? "";
674
754
  var homePrefix = homeDirectory ? `${homeDirectory}/` : "";
675
755
  var minimumVisibleProjectCount = 4;
676
- 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";
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";
677
757
  var PROJECT_COLOR = "#abd8e7";
678
758
  var BRANCH_COLOR = "#e3839c";
679
759
  var PATH_COLOR = "#719bd8";
@@ -696,8 +776,50 @@ var shortenHomePath = (targetPath) => {
696
776
  return targetPath;
697
777
  };
698
778
  var buildCdCommand = (targetPath) => {
699
- const escapedPath = targetPath.replaceAll('"', '\\"');
700
- return `cd "${escapedPath}"`;
779
+ return `cd ${targetPath}`;
780
+ };
781
+ var getCopyTargetPath = (view) => {
782
+ const root = view.repository?.root;
783
+ return root && root.length > 0 ? root : view.project.path;
784
+ };
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;
797
+ }
798
+ return false;
799
+ };
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;
805
+ };
806
+ var stringWidth = (text) => {
807
+ let width = 0;
808
+ for (const ch of text) {
809
+ width += charWidth(ch);
810
+ }
811
+ return width;
812
+ };
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;
701
823
  };
702
824
  var App = ({
703
825
  projects,
@@ -718,36 +840,87 @@ var App = ({
718
840
  const [releasedProjects, setReleasedProjects] = useState(/* @__PURE__ */ new Set());
719
841
  const [launchedProjects, setLaunchedProjects] = useState(/* @__PURE__ */ new Set());
720
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);
721
848
  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]);
722
855
  const sortedProjects = useMemo(() => {
723
856
  const fallbackTime = 0;
724
- const toSortKey = (view) => {
725
- if (useGitRootName) {
726
- const rootName = extractRootFolder(view.repository);
727
- if (rootName) {
728
- return rootName.toLocaleLowerCase();
729
- }
857
+ const getNameKey = (view) => {
858
+ const rootName = extractRootFolder(view.repository);
859
+ const base = rootName || view.project.title;
860
+ return base.toLocaleLowerCase();
861
+ };
862
+ const tieBreaker = (view) => view.project.path.toLocaleLowerCase();
863
+ const compareByUpdated = (a, b, direction) => {
864
+ const timeA = a.project.lastModified?.getTime() ?? fallbackTime;
865
+ const timeB = b.project.lastModified?.getTime() ?? fallbackTime;
866
+ if (timeA === timeB) {
867
+ return 0;
868
+ }
869
+ return direction === "desc" ? timeB - timeA : timeA - timeB;
870
+ };
871
+ const compareByName = (a, b, direction) => {
872
+ const keyA = getNameKey(a);
873
+ const keyB = getNameKey(b);
874
+ if (keyA === keyB) {
875
+ return 0;
730
876
  }
731
- return view.project.title.toLocaleLowerCase();
877
+ return direction === "desc" ? keyB.localeCompare(keyA) : keyA.localeCompare(keyB);
732
878
  };
733
- const toTieBreaker = (view) => view.project.path.toLocaleLowerCase();
734
879
  return [...projectViews].sort((a, b) => {
735
- if (a.project.favorite !== b.project.favorite) {
880
+ if (sortPreferences.favoritesFirst && a.project.favorite !== b.project.favorite) {
736
881
  return a.project.favorite ? -1 : 1;
737
882
  }
738
- const timeA = a.project.lastModified?.getTime() ?? fallbackTime;
739
- const timeB = b.project.lastModified?.getTime() ?? fallbackTime;
740
- if (timeA !== timeB) {
741
- return timeB - timeA;
883
+ const primary = sortPreferences.primary;
884
+ const direction = sortPreferences.direction;
885
+ if (primary === "updated") {
886
+ const updatedOrder2 = compareByUpdated(a, b, direction);
887
+ if (updatedOrder2 !== 0) {
888
+ return updatedOrder2;
889
+ }
890
+ const nameOrder2 = compareByName(a, b, "asc");
891
+ if (nameOrder2 !== 0) {
892
+ return nameOrder2;
893
+ }
894
+ return tieBreaker(a).localeCompare(tieBreaker(b));
742
895
  }
743
- const keyA = toSortKey(a);
744
- const keyB = toSortKey(b);
745
- if (keyA === keyB) {
746
- return toTieBreaker(a).localeCompare(toTieBreaker(b));
896
+ const nameOrder = compareByName(a, b, direction);
897
+ if (nameOrder !== 0) {
898
+ return nameOrder;
899
+ }
900
+ const updatedOrder = compareByUpdated(a, b, "desc");
901
+ if (updatedOrder !== 0) {
902
+ return updatedOrder;
747
903
  }
748
- return keyA.localeCompare(keyB);
904
+ return tieBreaker(a).localeCompare(tieBreaker(b));
749
905
  });
750
- }, [projectViews, useGitRootName]);
906
+ }, [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]);
751
924
  useEffect(() => {
752
925
  const handleSigint = () => {
753
926
  exit();
@@ -766,12 +939,9 @@ var App = ({
766
939
  const borderRows = 2;
767
940
  const hintRows = 1;
768
941
  const reservedRows = borderRows + hintRows;
769
- const availableRows = stdout.rows - reservedRows;
942
+ const availableRows = Math.max(0, stdout.rows - reservedRows);
770
943
  const rowsPerProject = Math.max(linesPerProject, 1);
771
- const calculatedCount = Math.max(
772
- minimumVisibleProjectCount,
773
- Math.floor(availableRows / rowsPerProject)
774
- );
944
+ const calculatedCount = Math.max(1, Math.floor(availableRows / rowsPerProject));
775
945
  setVisibleCount(calculatedCount);
776
946
  };
777
947
  updateVisibleCount();
@@ -780,7 +950,7 @@ var App = ({
780
950
  stdout?.off("resize", updateVisibleCount);
781
951
  };
782
952
  }, [linesPerProject, stdout]);
783
- const limit = Math.max(minimumVisibleProjectCount, visibleCount);
953
+ const limit = Math.max(1, visibleCount);
784
954
  const move = useCallback(
785
955
  (delta) => {
786
956
  setIndex((prev) => {
@@ -846,7 +1016,8 @@ var App = ({
846
1016
  });
847
1017
  }, [index, limit, sortedProjects.length]);
848
1018
  const copyProjectPath = useCallback(() => {
849
- const projectPath = sortedProjects[index]?.project.path;
1019
+ const projectView = sortedProjects[index];
1020
+ const projectPath = projectView ? getCopyTargetPath(projectView) : void 0;
850
1021
  if (!projectPath) {
851
1022
  setHint("No project to copy");
852
1023
  setTimeout(() => {
@@ -858,7 +1029,7 @@ var App = ({
858
1029
  const command = buildCdCommand(projectPath);
859
1030
  clipboard.writeSync(command);
860
1031
  const displayPath = shortenHomePath(projectPath);
861
- setHint(`Copied command: cd "${displayPath}"`);
1032
+ setHint(`Copied command: cd ${displayPath}`);
862
1033
  } catch (error) {
863
1034
  const message = error instanceof Error ? error.message : String(error);
864
1035
  setHint(`Failed to copy: ${message}`);
@@ -878,7 +1049,8 @@ var App = ({
878
1049
  }
879
1050
  const { project } = projectView;
880
1051
  try {
881
- const command = buildCdCommand(project.path);
1052
+ const cdTarget = getCopyTargetPath(projectView);
1053
+ const command = buildCdCommand(cdTarget);
882
1054
  clipboard.writeSync(command);
883
1055
  } catch (error) {
884
1056
  const message = error instanceof Error ? error.message : String(error);
@@ -1024,6 +1196,50 @@ var App = ({
1024
1196
  }
1025
1197
  }, [isRefreshing, onRefresh, sortedProjects]);
1026
1198
  useInput((input, key) => {
1199
+ if (isSortMenuOpen) {
1200
+ if (key.escape || input === "\x1B") {
1201
+ clearOverlay();
1202
+ forceFullRepaint();
1203
+ setIsSortMenuOpen(false);
1204
+ setRenderEpoch((prev) => prev + 1);
1205
+ return;
1206
+ }
1207
+ if (input === "j") {
1208
+ setSortMenuIndex((prev) => {
1209
+ const last = 2;
1210
+ const next = prev + 1;
1211
+ return next > last ? 0 : next;
1212
+ });
1213
+ return;
1214
+ }
1215
+ if (input === "k") {
1216
+ setSortMenuIndex((prev) => {
1217
+ const last = 2;
1218
+ const next = prev - 1;
1219
+ return next < 0 ? last : next;
1220
+ });
1221
+ return;
1222
+ }
1223
+ const toggleCurrent = () => {
1224
+ if (sortMenuIndex === 0) {
1225
+ setSortPreferences((prev) => ({ ...prev, primary: prev.primary === "updated" ? "name" : "updated" }));
1226
+ return;
1227
+ }
1228
+ if (sortMenuIndex === 1) {
1229
+ setSortPreferences((prev) => ({ ...prev, direction: prev.direction === "asc" ? "desc" : "asc" }));
1230
+ return;
1231
+ }
1232
+ setSortPreferences((prev) => ({ ...prev, favoritesFirst: !prev.favoritesFirst }));
1233
+ };
1234
+ if (input === " ") {
1235
+ toggleCurrent();
1236
+ }
1237
+ return;
1238
+ }
1239
+ if (input === "S" || input === "s") {
1240
+ setIsSortMenuOpen(true);
1241
+ return;
1242
+ }
1027
1243
  if (input === "j" || key.downArrow) {
1028
1244
  move(1);
1029
1245
  }
@@ -1092,7 +1308,7 @@ var App = ({
1092
1308
  return visibleProjects.map(({ project, repository, launchStatus }, offset) => {
1093
1309
  const rowIndex = startIndex + offset;
1094
1310
  const isSelected = rowIndex === index;
1095
- const arrow = isSelected ? ">" : " ";
1311
+ const selectionBar = isSelected ? "\u2503" : " ";
1096
1312
  const projectName = formatProjectName(project, repository, useGitRootName);
1097
1313
  const versionLabel = `(${project.version.value})`;
1098
1314
  const updatedText = formatUpdatedText(project.lastModified);
@@ -1117,28 +1333,23 @@ var App = ({
1117
1333
  const statusLabel = STATUS_LABELS[displayStatus];
1118
1334
  const statusColor = displayStatus === "running" ? LOCK_COLOR : displayStatus === "crashed" ? "red" : void 0;
1119
1335
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
1120
- /* @__PURE__ */ jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [
1121
- /* @__PURE__ */ jsxs(Text, { children: [
1122
- /* @__PURE__ */ jsxs(Text, { color: isSelected ? "green" : PROJECT_COLOR, bold: true, children: [
1123
- arrow,
1124
- " ",
1125
- projectName
1126
- ] }),
1127
- /* @__PURE__ */ jsxs(Text, { color: isSelected ? "green" : void 0, 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: [
1128
1345
  " ",
1129
1346
  versionLabel
1130
1347
  ] }),
1131
- updatedText ? /* @__PURE__ */ jsx(Text, { color: isSelected ? "green" : void 0, children: ` ${updatedText}` }) : null,
1348
+ updatedText ? /* @__PURE__ */ jsx(Text, { children: ` ${updatedText}` }) : null,
1132
1349
  statusLabel && statusColor ? /* @__PURE__ */ jsx(Text, { color: statusColor, children: ` ${statusLabel}` }) : null
1133
1350
  ] }),
1134
- showBranch ? /* @__PURE__ */ jsxs(Text, { color: isSelected ? "green" : BRANCH_COLOR, children: [
1135
- " ",
1136
- branchLine
1137
- ] }) : null,
1138
- showPath ? /* @__PURE__ */ jsxs(Text, { color: isSelected ? "green" : PATH_COLOR, children: [
1139
- " ",
1140
- pathLine
1141
- ] }) : null,
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,
1142
1353
  /* @__PURE__ */ jsx(Text, { children: " " })
1143
1354
  ] }),
1144
1355
  /* @__PURE__ */ jsxs(Box, { marginLeft: 1, width: 1, flexDirection: "column", alignItems: "center", children: [
@@ -1160,10 +1371,121 @@ var App = ({
1160
1371
  useGitRootName,
1161
1372
  visibleProjects
1162
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
+ }
1482
+ }
1483
+ stdout.write("\x1B8");
1484
+ }, [linesPerProject, rows.length, visibleProjects.length, stdout]);
1163
1485
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1164
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 }),
1165
- /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { children: hint }) })
1166
- ] });
1487
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { wrap: "truncate", children: hint }) })
1488
+ ] }, renderEpoch);
1167
1489
  };
1168
1490
 
1169
1491
  // src/index.tsx
@@ -1194,7 +1516,8 @@ var bootstrap = async () => {
1194
1516
  );
1195
1517
  const terminateProjectUseCase = new TerminateProjectUseCase(
1196
1518
  unityProcessReader,
1197
- unityProcessTerminator
1519
+ unityProcessTerminator,
1520
+ unityTempDirectoryCleaner
1198
1521
  );
1199
1522
  const useGitRootName = !process2.argv.includes("--no-git-root-name");
1200
1523
  const showBranch = !process2.argv.includes("--hide-branch");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unity-hub-cli",
3
- "version": "0.7.0",
3
+ "version": "0.9.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": {