Files
rea/src/application/MacHopperPaths.ts
T

103 lines
3.5 KiB
TypeScript

import { readdirSync, type Dirent } from "node:fs";
import { readdir } from "node:fs/promises";
import { posix } from "node:path";
export const MAC_HOPPER_APPLICATION_DIRECTORY = "/Applications";
export const MAC_HOPPER_APPLICATION_NAME = "Hopper Disassembler.app";
export const MAC_HOPPER_USER_APPLICATIONS_DIRECTORY = "Applications";
export const MAC_HOPPER_SYSTEM_LAUNCHER = posix.join(
MAC_HOPPER_APPLICATION_DIRECTORY,
MAC_HOPPER_APPLICATION_NAME,
"Contents/MacOS/hopper",
);
export const macHopperUserApplicationPath = (homeDirectory: string): string =>
posix.join(
homeDirectory,
MAC_HOPPER_USER_APPLICATIONS_DIRECTORY,
MAC_HOPPER_APPLICATION_NAME,
);
export const macHopperApplicationRoots = (
homeDirectory: string,
): readonly string[] => [
MAC_HOPPER_APPLICATION_DIRECTORY,
posix.join(homeDirectory, MAC_HOPPER_USER_APPLICATIONS_DIRECTORY),
];
export const macHopperCanonicalLauncherCandidates = (
homeDirectory: string,
): readonly string[] => [
MAC_HOPPER_SYSTEM_LAUNCHER,
posix.join(
macHopperUserApplicationPath(homeDirectory),
"Contents/MacOS/hopper",
),
];
/** Build deterministic candidates from the discovered app bundle names. */
export const macHopperRenamedLauncherCandidates = (
homeDirectory: string,
bundleNamesByRoot: ReadonlyMap<string, readonly string[]>,
): readonly string[] =>
macHopperApplicationRoots(homeDirectory).flatMap((root) =>
[...new Set(bundleNamesByRoot.get(root) ?? [])]
.filter(
(name) =>
/^Hopper.*\.app$/iu.test(name) &&
name.toLocaleLowerCase("en-US") !==
MAC_HOPPER_APPLICATION_NAME.toLocaleLowerCase("en-US"),
)
.sort()
.map((name) => posix.join(root, name, "Contents/MacOS/hopper")),
);
// A linked application bundle retains its selected spelling. Runtime and
// doctor validate the executable beneath each candidate, rejecting dangling
// links and links to ordinary files without interpreting the bundle again.
const possibleBundleNames = (entries: readonly Dirent[]): readonly string[] =>
entries
.filter((entry) => entry.isDirectory() || entry.isSymbolicLink())
.map(({ name }) => name);
const localBundleNames = (root: string): readonly string[] => {
try {
return possibleBundleNames(readdirSync(root, { withFileTypes: true }));
} catch (cause: unknown) {
// Missing or unreadable optional app directories contain no candidates.
void cause;
return [];
}
};
/** Yield renamed bundles from the local filesystem, reading each root only if needed. */
export function* localMacHopperRenamedLauncherCandidates(
homeDirectory: string,
): Generator<string> {
for (const root of macHopperApplicationRoots(homeDirectory))
yield* macHopperRenamedLauncherCandidates(
homeDirectory,
new Map([[root, localBundleNames(root)]]),
);
}
/** Read the same renamed bundles without blocking the event loop, for doctor. */
export const readMacHopperRenamedLauncherCandidates = async (
homeDirectory: string,
): Promise<readonly string[]> => {
const bundleNamesByRoot = new Map<string, readonly string[]>();
for (const root of macHopperApplicationRoots(homeDirectory)) {
try {
bundleNamesByRoot.set(
root,
possibleBundleNames(await readdir(root, { withFileTypes: true })),
);
} catch (cause: unknown) {
// Missing or unreadable optional app directories contain no candidates.
void cause;
}
}
return macHopperRenamedLauncherCandidates(homeDirectory, bundleNamesByRoot);
};