| ... | @@ -3,6 +3,10 @@ import { spawn } from "node:child_process"; | ... | @@ -3,6 +3,10 @@ import { spawn } from "node:child_process"; |
| 3 | import { EDITORS, type EditorId } from "@t3tools/contracts"; | 3 | import { EDITORS, type EditorId } from "@t3tools/contracts"; |
| 4 | import { ServiceMap, Schema, Effect, Layer } from "effect"; | 4 | import { ServiceMap, Schema, Effect, Layer } from "effect"; |
| 5 | | 5 | |
| | 6 | // ============================== |
| | 7 | // Definitions |
| | 8 | // ============================== |
| | 9 | |
| 6 | export class OpenError extends Schema.TaggedErrorClass<OpenError>()("OpenError", { | 10 | export class OpenError extends Schema.TaggedErrorClass<OpenError>()("OpenError", { |
| 7 | message: Schema.String, | 11 | message: Schema.String, |
| 8 | cause: Schema.optional(Schema.Defect), | 12 | cause: Schema.optional(Schema.Defect), |
| ... | @@ -25,13 +29,17 @@ export interface OpenShape { | ... | @@ -25,13 +29,17 @@ export interface OpenShape { |
| 25 | | 29 | |
| 26 | export class Open extends ServiceMap.Service<Open, OpenShape>()("server/Open") {} | 30 | export class Open extends ServiceMap.Service<Open, OpenShape>()("server/Open") {} |
| 27 | | 31 | |
| 28 | export function resolveEditorLaunch( | 32 | // ============================== |
| | 33 | // Implementations |
| | 34 | // ============================== |
| | 35 | |
| | 36 | export const resolveEditorLaunch = Effect.fnUntraced(function* ( |
| 29 | input: OpenInEditorInput, | 37 | input: OpenInEditorInput, |
| 30 | platform: NodeJS.Platform = process.platform, | 38 | platform: NodeJS.Platform = process.platform, |
| 31 | ): EditorLaunch { | 39 | ): Effect.fn.Return<EditorLaunch, OpenError> { |
| 32 | const editorDef = EDITORS.find((editor) => editor.id === input.editor); | 40 | const editorDef = EDITORS.find((editor) => editor.id === input.editor); |
| 33 | if (!editorDef) { | 41 | if (!editorDef) { |
| 34 | throw new OpenError({ message: `Unknown editor: ${input.editor}` }); | 42 | return yield* new OpenError({ message: `Unknown editor: ${input.editor}` }); |
| 35 | } | 43 | } |
| 36 | | 44 | |
| 37 | if (editorDef.command) { | 45 | if (editorDef.command) { |
| ... | @@ -39,7 +47,7 @@ export function resolveEditorLaunch( | ... | @@ -39,7 +47,7 @@ export function resolveEditorLaunch( |
| 39 | } | 47 | } |
| 40 | | 48 | |
| 41 | if (editorDef.id !== "file-manager") { | 49 | if (editorDef.id !== "file-manager") { |
| 42 | throw new OpenError({ message: `Unsupported editor: ${input.editor}` }); | 50 | return yield* new OpenError({ message: `Unsupported editor: ${input.editor}` }); |
| 43 | } | 51 | } |
| 44 | | 52 | |
| 45 | switch (platform) { | 53 | switch (platform) { |
| ... | @@ -50,24 +58,25 @@ export function resolveEditorLaunch( | ... | @@ -50,24 +58,25 @@ export function resolveEditorLaunch( |
| 50 | default: | 58 | default: |
| 51 | return { command: "xdg-open", args: [input.cwd] }; | 59 | return { command: "xdg-open", args: [input.cwd] }; |
| 52 | } | 60 | } |
| 53 | } | 61 | }); |
| 54 | | 62 | |
| 55 | export function launchDetached(command: string, args: ReadonlyArray<string>): Promise<void> { | 63 | export const launchDetached = (launch: EditorLaunch) => |
| 56 | return new Promise((resolve, reject) => { | 64 | Effect.callback<void, OpenError>((resume) => { |
| 57 | const child = spawn(command, [...args], { | 65 | const child = spawn(launch.command, [...launch.args], { |
| 58 | detached: true, | 66 | detached: true, |
| 59 | stdio: "ignore", | 67 | stdio: "ignore", |
| 60 | }); | 68 | }); |
| 61 | | 69 | |
| 62 | const handleSpawn = () => { | 70 | const handleSpawn = () => { |
| 63 | child.unref(); | 71 | child.unref(); |
| 64 | resolve(); | 72 | resume(Effect.void); |
| 65 | }; | 73 | }; |
| 66 | | 74 | |
| 67 | child.once("spawn", handleSpawn); | 75 | child.once("spawn", handleSpawn); |
| 68 | child.once("error", (error) => reject(error)); | 76 | child.once("error", (cause) => |
| | 77 | resume(Effect.fail(new OpenError({ message: "failed to spawn detached process", cause }))), |
| | 78 | ); |
| 69 | }); | 79 | }); |
| 70 | } | | |
| 71 | | 80 | |
| 72 | const make = Effect.gen(function* () { | 81 | const make = Effect.gen(function* () { |
| 73 | const open = yield* Effect.promise(() => import("open")); | 82 | const open = yield* Effect.promise(() => import("open")); |
| ... | @@ -78,17 +87,7 @@ const make = Effect.gen(function* () { | ... | @@ -78,17 +87,7 @@ const make = Effect.gen(function* () { |
| 78 | try: () => open.default(target), | 87 | try: () => open.default(target), |
| 79 | catch: (cause) => new OpenError({ message: "Browser auto-open failed", cause }), | 88 | catch: (cause) => new OpenError({ message: "Browser auto-open failed", cause }), |
| 80 | }), | 89 | }), |
| 81 | openInEditor: (input) => | 90 | openInEditor: (input) => Effect.flatMap(resolveEditorLaunch(input), launchDetached), |
| 82 | Effect.tryPromise({ | | |
| 83 | try: () => { | | |
| 84 | const launch = resolveEditorLaunch(input); | | |
| 85 | return launchDetached(launch.command, launch.args); | | |
| 86 | }, | | |
| 87 | catch: (cause) => | | |
| 88 | cause instanceof OpenError | | |
| 89 | ? cause | | |
| 90 | : new OpenError({ message: "Failed to open editor", cause }), | | |
| 91 | }), | | |
| 92 | } satisfies OpenShape; | 91 | } satisfies OpenShape; |
| 93 | }); | 92 | }); |
| 94 | | 93 | |