Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 0 additions & 30 deletions apps/desktop/src/shell/DesktopOpenWith.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
} from "@t3tools/contracts";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Schema from "effect/Schema";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
Expand Down Expand Up @@ -144,35 +143,6 @@ describe("DesktopOpenWith launch resolution", () => {
}).pipe(Effect.provide(NodeServices.layer)),
);

it.effect("resolves CFBundleExecutable and reports missing bundle executables", () =>
Effect.gen(function* () {
// oxlint-disable-next-line t3code/no-global-process-runtime -- Platform-gated native fixture.
if (process.platform !== "darwin") return;
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const base = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-open-with-test-" });
const applicationPath = path.join(base, "Fixture.app");
const contentsPath = path.join(applicationPath, "Contents");
const executableDirectory = path.join(contentsPath, "MacOS");
yield* fileSystem.makeDirectory(executableDirectory, { recursive: true });
yield* fileSystem.writeFileString(
path.join(contentsPath, "Info.plist"),
`<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0"><dict><key>CFBundleExecutable</key><string>Fixture</string></dict></plist>`,
);
const executablePath = path.join(executableDirectory, "Fixture");
yield* fileSystem.writeFileString(executablePath, "#!/bin/sh\n");

assert.equal(
yield* DesktopOpenWith.resolveMacBundleExecutable(applicationPath),
executablePath,
);
yield* fileSystem.remove(executablePath);
const error = yield* Effect.flip(DesktopOpenWith.resolveMacBundleExecutable(applicationPath));
assert.equal(error.reason, "missing-executable");
}).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
);

it.effect("reports available and missing command presentations", () =>
Effect.gen(function* () {
const openWith = yield* DesktopOpenWith.DesktopOpenWith;
Expand Down
68 changes: 4 additions & 64 deletions apps/desktop/src/shell/DesktopOpenWith.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// @effect-diagnostics nodeBuiltinImport:off - macOS bundle paths use the host path grammar.
import {
OpenWithBundleResolutionError,
OpenWithEnvironmentError,
OpenWithInvalidTargetError,
OpenWithMissingEntryError,
Expand Down Expand Up @@ -70,65 +69,6 @@ const entryIsAvailable = Effect.fn("desktop.openWith.entryIsAvailable")(function
return Option.isSome(stat) && stat.value.type === "Directory";
});

export const resolveMacBundleExecutable = Effect.fn("desktop.openWith.resolveMacBundleExecutable")(
function* (applicationPath: string) {
if (!isMacApplicationPath(applicationPath)) {
return yield* new OpenWithBundleResolutionError({
applicationPath,
reason: "invalid-application-path",
});
}
const infoPlistPath = NodePath.join(applicationPath, "Contents", "Info.plist");
const plistStat = yield* statPath(infoPlistPath);
if (Option.isNone(plistStat) || plistStat.value.type !== "File") {
return yield* new OpenWithBundleResolutionError({
applicationPath,
reason: "missing-info-plist",
});
}

const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const executableName = yield* spawner
.string(
ChildProcess.make(
"/usr/bin/plutil",
["-extract", "CFBundleExecutable", "raw", "-o", "-", infoPlistPath],
{ stdin: "ignore", stdout: "pipe", stderr: "pipe" },
),
)
.pipe(
Effect.map((output) => output.trim()),
Effect.mapError(
(cause) =>
new OpenWithBundleResolutionError({
applicationPath,
reason: "malformed-info-plist",
cause,
}),
),
);
if (
executableName.length === 0 ||
executableName.includes("/") ||
executableName.includes("\\")
) {
return yield* new OpenWithBundleResolutionError({
applicationPath,
reason: "malformed-info-plist",
});
}
const executablePath = NodePath.join(applicationPath, "Contents", "MacOS", executableName);
const executableStat = yield* statPath(executablePath);
if (Option.isNone(executableStat) || executableStat.value.type !== "File") {
return yield* new OpenWithBundleResolutionError({
applicationPath,
reason: "missing-executable",
});
}
return executablePath;
},
);

const expandDirectoryArguments = (args: readonly string[], directory: string): string[] =>
args.map((argument) => argument.replaceAll("{directory}", directory));

Expand Down Expand Up @@ -169,8 +109,8 @@ export const resolveOpenWithLaunch = Effect.fn("desktop.openWith.resolveLaunch")
if (entry.directoryMode === "working-directory") {
if (entry.invocation.type === "mac-application") {
return {
command: yield* resolveMacBundleExecutable(entry.invocation.applicationPath),
args: [...entry.arguments],
command: "/usr/bin/open",
args: ["-a", entry.invocation.applicationPath, "--args", ...entry.arguments],
cwd: directory,
};
}
Expand All @@ -187,8 +127,8 @@ export const resolveOpenWithLaunch = Effect.fn("desktop.openWith.resolveLaunch")
const args = expandDirectoryArguments(entry.arguments, directory);
if (entry.invocation.type === "mac-application") {
return {
command: yield* resolveMacBundleExecutable(entry.invocation.applicationPath),
args,
command: "/usr/bin/open",
args: ["-a", entry.invocation.applicationPath, "--args", ...args],
cwd: null,
};
}
Expand Down
22 changes: 0 additions & 22 deletions packages/contracts/src/openWith.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,27 +132,6 @@ export class OpenWithUnavailableApplicationError extends Schema.TaggedErrorClass
}
}

export const OpenWithBundleResolutionReason = Schema.Literals([
"invalid-application-path",
"missing-info-plist",
"malformed-info-plist",
"missing-executable",
]);
export type OpenWithBundleResolutionReason = typeof OpenWithBundleResolutionReason.Type;

export class OpenWithBundleResolutionError extends Schema.TaggedErrorClass<OpenWithBundleResolutionError>()(
"OpenWithBundleResolutionError",
{
applicationPath: Schema.String,
reason: OpenWithBundleResolutionReason,
cause: Schema.optional(Schema.Defect()),
},
) {
override get message(): string {
return `Unable to resolve the executable in macOS application '${this.applicationPath}' (${this.reason}).`;
}
}

export class OpenWithSpawnError extends Schema.TaggedErrorClass<OpenWithSpawnError>()(
"OpenWithSpawnError",
{
Expand All @@ -173,7 +152,6 @@ export const OpenWithLaunchError = Schema.Union([
OpenWithMissingEntryError,
OpenWithInvalidTargetError,
OpenWithUnavailableApplicationError,
OpenWithBundleResolutionError,
OpenWithSpawnError,
]);
export type OpenWithLaunchError = typeof OpenWithLaunchError.Type;
Loading