unity-hub-cli 0.1.0 → 0.2.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 (3) hide show
  1. package/README.md +4 -2
  2. package/dist/index.js +174 -38
  3. package/package.json +5 -1
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # UnityHub CLI
2
2
 
3
- A CLI tool that reads Unity Hub's `projects-v1.json`, displays projects in an Ink-based TUI, allows navigation with arrow keys/`j`/`k`, and launches Unity Editor by pressing Enter.
3
+ [日本語版はこちら / Japanese version](README_ja.md)
4
+
5
+ A CLI tool that displays the same content as Unity Hub in an Ink-based TUI, allows navigation with arrow keys/`j`/`k`, and launches Unity Editor by pressing `o`.
4
6
 
5
7
  ## Requirements
6
8
 
@@ -83,7 +85,7 @@ npm audit signatures
83
85
  ## Controls
84
86
 
85
87
  - Arrow keys / `j` / `k`: Navigate selection
86
- - `o`: Launch selected project in Unity
88
+ - `o`: Launch selected project in Unity
87
89
  - Ctrl + C (twice): Exit
88
90
 
89
91
  The display includes Git branch (if present), Unity version, project path, and last modified time (`lastModified`).
package/dist/index.js CHANGED
@@ -443,6 +443,7 @@ var UnityLockChecker = class {
443
443
  };
444
444
 
445
445
  // src/presentation/App.tsx
446
+ import clipboard from "clipboardy";
446
447
  import { Box, Text, useApp, useInput, useStdout } from "ink";
447
448
  import { useCallback, useEffect, useMemo, useState } from "react";
448
449
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -458,13 +459,13 @@ var extractRootFolder = (repository) => {
458
459
  };
459
460
  var formatProjectName = (project, repository, useGitRootName) => {
460
461
  if (!useGitRootName) {
461
- return `${project.title} (${project.version.value})`;
462
+ return project.title;
462
463
  }
463
464
  const rootFolder = extractRootFolder(repository);
464
465
  if (!rootFolder) {
465
- return `${project.title} (${project.version.value})`;
466
+ return project.title;
466
467
  }
467
- return `${rootFolder} (${project.version.value})`;
468
+ return rootFolder;
468
469
  };
469
470
  var formatBranch = (branch) => {
470
471
  if (!branch) {
@@ -475,10 +476,61 @@ var formatBranch = (branch) => {
475
476
  }
476
477
  return `detached@${branch.sha}`;
477
478
  };
479
+ var formatRelativeTime = (lastModified) => {
480
+ if (!lastModified) {
481
+ return void 0;
482
+ }
483
+ const now = /* @__PURE__ */ new Date();
484
+ const diffMillis = now.getTime() - lastModified.getTime();
485
+ if (Number.isNaN(diffMillis)) {
486
+ return void 0;
487
+ }
488
+ const safeMillis = Math.max(0, diffMillis);
489
+ const secondsPerMinute = 60;
490
+ const secondsPerHour = secondsPerMinute * 60;
491
+ const secondsPerDay = secondsPerHour * 24;
492
+ const secondsPerMonth = secondsPerDay * 30;
493
+ const secondsPerYear = secondsPerDay * 365;
494
+ const totalSeconds = Math.floor(safeMillis / 1e3);
495
+ if (totalSeconds < 45) {
496
+ return "a few seconds ago";
497
+ }
498
+ if (totalSeconds < 45 * secondsPerMinute) {
499
+ const minutes = Math.max(1, Math.floor(totalSeconds / secondsPerMinute));
500
+ const suffix2 = minutes === 1 ? "minute" : "minutes";
501
+ return `${minutes} ${suffix2} ago`;
502
+ }
503
+ if (totalSeconds < 30 * secondsPerHour) {
504
+ const hours = Math.max(1, Math.floor(totalSeconds / secondsPerHour));
505
+ const suffix2 = hours === 1 ? "hour" : "hours";
506
+ return `${hours} ${suffix2} ago`;
507
+ }
508
+ if (totalSeconds < secondsPerMonth) {
509
+ const days = Math.max(1, Math.floor(totalSeconds / secondsPerDay));
510
+ const suffix2 = days === 1 ? "day" : "days";
511
+ return `${days} ${suffix2} ago`;
512
+ }
513
+ if (totalSeconds < secondsPerYear) {
514
+ const months = Math.max(1, Math.floor(totalSeconds / secondsPerMonth));
515
+ const suffix2 = months === 1 ? "month" : "months";
516
+ return `${months} ${suffix2} ago`;
517
+ }
518
+ const years = Math.max(1, Math.floor(totalSeconds / secondsPerYear));
519
+ const suffix = years === 1 ? "year" : "years";
520
+ return `${years} ${suffix} ago`;
521
+ };
522
+ var UPDATED_LABEL = "Last:";
523
+ var formatUpdatedText = (lastModified) => {
524
+ const relativeTime = formatRelativeTime(lastModified);
525
+ if (!relativeTime) {
526
+ return void 0;
527
+ }
528
+ return `${UPDATED_LABEL} ${relativeTime}`;
529
+ };
478
530
  var homeDirectory = process.env.HOME ?? "";
479
531
  var homePrefix = homeDirectory ? `${homeDirectory}/` : "";
480
532
  var minimumVisibleProjectCount = 4;
481
- var defaultHintMessage = "Move with arrows or j/k \xB7 Launch with o \xB7 Exit with Ctrl+C twice";
533
+ var defaultHintMessage = "Move with arrows or j/k \xB7 Launch & exit with o \xB7 Copy cd path with c \xB7 Exit with Ctrl+C twice";
482
534
  var PROJECT_COLOR = "#abd8e7";
483
535
  var BRANCH_COLOR = "#e3839c";
484
536
  var PATH_COLOR = "#719bd8";
@@ -494,6 +546,10 @@ var shortenHomePath = (targetPath) => {
494
546
  }
495
547
  return targetPath;
496
548
  };
549
+ var buildCdCommand = (targetPath) => {
550
+ const escapedPath = targetPath.replaceAll('"', '\\"');
551
+ return `cd "${escapedPath}"`;
552
+ };
497
553
  var App = ({
498
554
  projects,
499
555
  onLaunch,
@@ -505,8 +561,9 @@ var App = ({
505
561
  const { stdout } = useStdout();
506
562
  const [visibleCount, setVisibleCount] = useState(minimumVisibleProjectCount);
507
563
  const [index, setIndex] = useState(0);
508
- const [hint, setHint] = useState("Move with j/k \xB7 Launch with o \xB7 Exit with Ctrl+C twice");
564
+ const [hint, setHint] = useState(defaultHintMessage);
509
565
  const [pendingExit, setPendingExit] = useState(false);
566
+ const [windowStart, setWindowStart] = useState(0);
510
567
  const linesPerProject = (showBranch ? 1 : 0) + (showPath ? 1 : 0) + 2;
511
568
  const sortedProjects = useMemo(() => {
512
569
  const fallbackTime = 0;
@@ -561,7 +618,10 @@ var App = ({
561
618
  setVisibleCount(minimumVisibleProjectCount);
562
619
  return;
563
620
  }
564
- const reservedRows = 6;
621
+ const borderRows = 2;
622
+ const hintRows = 1;
623
+ const hintMarginRows = 1;
624
+ const reservedRows = borderRows + hintRows + hintMarginRows;
565
625
  const availableRows = stdout.rows - reservedRows;
566
626
  const rowsPerProject = Math.max(linesPerProject, 1);
567
627
  const calculatedCount = Math.max(
@@ -576,6 +636,7 @@ var App = ({
576
636
  stdout?.off("resize", updateVisibleCount);
577
637
  };
578
638
  }, [linesPerProject, stdout]);
639
+ const limit = Math.max(minimumVisibleProjectCount, visibleCount);
579
640
  const move = useCallback(
580
641
  (delta) => {
581
642
  setIndex((prev) => {
@@ -591,20 +652,102 @@ var App = ({
591
652
  }
592
653
  return next;
593
654
  });
655
+ setWindowStart((prevStart) => {
656
+ if (sortedProjects.length <= limit) {
657
+ return 0;
658
+ }
659
+ const maxStart = Math.max(0, sortedProjects.length - limit);
660
+ let nextStart = prevStart;
661
+ if (delta > 0) {
662
+ const nextIndex = Math.min(sortedProjects.length - 1, index + delta);
663
+ if (nextIndex >= prevStart + limit) {
664
+ nextStart = nextIndex - limit + 1;
665
+ }
666
+ } else if (delta < 0) {
667
+ const nextIndex = Math.max(0, index + delta);
668
+ if (nextIndex < prevStart) {
669
+ nextStart = nextIndex;
670
+ }
671
+ }
672
+ if (nextStart < 0) {
673
+ nextStart = 0;
674
+ }
675
+ if (nextStart > maxStart) {
676
+ nextStart = maxStart;
677
+ }
678
+ return nextStart;
679
+ });
594
680
  },
595
- [sortedProjects.length]
681
+ [index, limit, sortedProjects.length]
596
682
  );
597
- const launchSelected = useCallback(async () => {
598
- const project = sortedProjects[index]?.project;
599
- if (!project) {
683
+ useEffect(() => {
684
+ setWindowStart((prevStart) => {
685
+ if (sortedProjects.length <= limit) {
686
+ return prevStart === 0 ? prevStart : 0;
687
+ }
688
+ const maxStart = Math.max(0, sortedProjects.length - limit);
689
+ let nextStart = prevStart;
690
+ if (index < prevStart) {
691
+ nextStart = index;
692
+ } else if (index >= prevStart + limit) {
693
+ nextStart = index - limit + 1;
694
+ }
695
+ if (nextStart < 0) {
696
+ nextStart = 0;
697
+ }
698
+ if (nextStart > maxStart) {
699
+ nextStart = maxStart;
700
+ }
701
+ return nextStart;
702
+ });
703
+ }, [index, limit, sortedProjects.length]);
704
+ const copyProjectPath = useCallback(() => {
705
+ const projectPath = sortedProjects[index]?.project.path;
706
+ if (!projectPath) {
707
+ setHint("No project to copy");
708
+ setTimeout(() => {
709
+ setHint(defaultHintMessage);
710
+ }, 2e3);
600
711
  return;
601
712
  }
602
713
  try {
603
- await onLaunch(project);
604
- setHint(`Launched Unity: ${project.title}`);
714
+ const command = buildCdCommand(projectPath);
715
+ clipboard.writeSync(command);
716
+ const displayPath = shortenHomePath(projectPath);
717
+ setHint(`Copied command: cd "${displayPath}"`);
718
+ } catch (error) {
719
+ const message = error instanceof Error ? error.message : String(error);
720
+ setHint(`Failed to copy: ${message}`);
721
+ }
722
+ setTimeout(() => {
723
+ setHint(defaultHintMessage);
724
+ }, 2e3);
725
+ }, [index, sortedProjects]);
726
+ const launchSelectedAndExit = useCallback(async () => {
727
+ const projectView = sortedProjects[index];
728
+ if (!projectView) {
729
+ setHint("No project to launch");
605
730
  setTimeout(() => {
606
731
  setHint(defaultHintMessage);
607
732
  }, 2e3);
733
+ return;
734
+ }
735
+ const { project } = projectView;
736
+ try {
737
+ const command = buildCdCommand(project.path);
738
+ clipboard.writeSync(command);
739
+ } catch (error) {
740
+ const message = error instanceof Error ? error.message : String(error);
741
+ setHint(`Failed to copy: ${message}`);
742
+ setTimeout(() => {
743
+ setHint(defaultHintMessage);
744
+ }, 3e3);
745
+ return;
746
+ }
747
+ try {
748
+ await onLaunch(project);
749
+ stdout?.write("\x1B[2J\x1B[H");
750
+ exit();
608
751
  } catch (error) {
609
752
  if (error instanceof LaunchCancelledError) {
610
753
  setHint("Launch cancelled");
@@ -619,7 +762,7 @@ var App = ({
619
762
  setHint(defaultHintMessage);
620
763
  }, 3e3);
621
764
  }
622
- }, [index, onLaunch, sortedProjects]);
765
+ }, [exit, index, onLaunch, sortedProjects, stdout]);
623
766
  useInput((input, key) => {
624
767
  if (input === "j" || key.downArrow) {
625
768
  move(1);
@@ -628,34 +771,27 @@ var App = ({
628
771
  move(-1);
629
772
  }
630
773
  if (input === "o") {
631
- void launchSelected();
774
+ void launchSelectedAndExit();
775
+ }
776
+ if (input === "c") {
777
+ copyProjectPath();
632
778
  }
633
779
  });
634
780
  const { startIndex, visibleProjects } = useMemo(() => {
635
- const limit = Math.max(minimumVisibleProjectCount, visibleCount);
636
781
  if (sortedProjects.length <= limit) {
637
782
  return {
638
783
  startIndex: 0,
639
- endIndex: sortedProjects.length,
640
784
  visibleProjects: sortedProjects
641
785
  };
642
786
  }
643
- const halfWindow = Math.floor(limit / 2);
644
- let start = index - halfWindow;
645
- let end = index + halfWindow + limit % 2;
646
- if (start < 0) {
647
- start = 0;
648
- end = limit;
649
- }
650
- if (end > sortedProjects.length) {
651
- end = sortedProjects.length;
652
- start = Math.max(0, end - limit);
653
- }
787
+ const maxStart = Math.max(0, sortedProjects.length - limit);
788
+ const clampedStart = Math.min(Math.max(0, windowStart), maxStart);
789
+ const end = Math.min(clampedStart + limit, sortedProjects.length);
654
790
  return {
655
- startIndex: start,
656
- visibleProjects: sortedProjects.slice(start, end)
791
+ startIndex: clampedStart,
792
+ visibleProjects: sortedProjects.slice(clampedStart, end)
657
793
  };
658
- }, [index, sortedProjects, visibleCount]);
794
+ }, [limit, sortedProjects, windowStart]);
659
795
  const scrollbarChars = useMemo(() => {
660
796
  const totalProjects = projects.length;
661
797
  const totalLines = totalProjects * linesPerProject;
@@ -688,7 +824,9 @@ var App = ({
688
824
  const rowIndex = startIndex + offset;
689
825
  const isSelected = rowIndex === index;
690
826
  const arrow = isSelected ? ">" : " ";
691
- const titleLine = formatProjectName(project, repository, useGitRootName);
827
+ const projectName = formatProjectName(project, repository, useGitRootName);
828
+ const versionLabel = `(${project.version.value})`;
829
+ const updatedText = formatUpdatedText(project.lastModified);
692
830
  const pathLine = shortenHomePath(project.path);
693
831
  const branchLine = formatBranch(repository?.branch);
694
832
  const baseScrollbarIndex = offset * linesPerProject;
@@ -696,21 +834,19 @@ var App = ({
696
834
  const branchScrollbar = showBranch ? scrollbarChars[baseScrollbarIndex + 1] ?? " " : " ";
697
835
  const pathScrollbar = showPath ? scrollbarChars[baseScrollbarIndex + 1 + (showBranch ? 1 : 0)] ?? " " : " ";
698
836
  const spacerScrollbar = scrollbarChars[baseScrollbarIndex + linesPerProject - 1] ?? " ";
699
- const versionStart = titleLine.indexOf("(");
700
- const versionText = versionStart >= 0 ? titleLine.slice(versionStart) : "";
701
- const nameText = versionStart >= 0 ? titleLine.slice(0, versionStart).trimEnd() : titleLine;
702
837
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
703
838
  /* @__PURE__ */ jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [
704
839
  /* @__PURE__ */ jsxs(Text, { children: [
705
840
  /* @__PURE__ */ jsxs(Text, { color: isSelected ? "green" : PROJECT_COLOR, bold: true, children: [
706
841
  arrow,
707
842
  " ",
708
- nameText
843
+ projectName
709
844
  ] }),
710
- versionText ? /* @__PURE__ */ jsxs(Text, { color: isSelected ? "green" : void 0, children: [
845
+ /* @__PURE__ */ jsxs(Text, { color: isSelected ? "green" : void 0, children: [
711
846
  " ",
712
- versionText
713
- ] }) : null
847
+ versionLabel
848
+ ] }),
849
+ updatedText ? /* @__PURE__ */ jsx(Text, { color: isSelected ? "green" : void 0, children: ` ${updatedText}` }) : null
714
850
  ] }),
715
851
  showBranch ? /* @__PURE__ */ jsxs(Text, { color: isSelected ? "green" : BRANCH_COLOR, children: [
716
852
  " ",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unity-hub-cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.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": {
@@ -36,9 +36,13 @@
36
36
  "dist"
37
37
  ],
38
38
  "dependencies": {
39
+ "clipboardy": "^4.0.0",
39
40
  "ink": "^4.4.1",
40
41
  "react": "^18.3.1"
41
42
  },
43
+ "engines": {
44
+ "node": ">=18"
45
+ },
42
46
  "devDependencies": {
43
47
  "@types/node": "^20.19.20",
44
48
  "@types/react": "^18.3.26",