1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";
4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
6import * as Schema from "effect/Schema";
7
8import * as DesktopEnvironment from "./DesktopEnvironment.ts";
9
10export interface DesktopIconPaths {
11 readonly ico: Option.Option<string>;
12 readonly icns: Option.Option<string>;
13 readonly png: Option.Option<string>;
14}
15
16export class DesktopAssetProbeError extends Schema.TaggedError<DesktopAssetProbeError>()(
17 "DesktopAssetProbeError",
18 {
19 fileName: Schema.String,
20 candidatePath: Schema.String,
21 cause: Schema.Defect(),
22 },
23) {
24 override get message(): string {
25 return `Failed to probe desktop asset "${this.fileName}" at ${this.candidatePath}.`;
26 }
27}
28
29export class DesktopAssets extends Context.Service<
30 DesktopAssets,
31 {
32 readonly iconPaths: Effect.Effect<DesktopIconPaths>;
33 readonly resolveResourcePath: (
34 fileName: string,
35 ) => Effect.Effect<Option.Option<string>, DesktopAssetProbeError>;
36 }
37>()("@t3tools/desktop/app/DesktopAssets") {}
38
39const resolveResourcePath = Effect.fn("desktop.assets.resolveResourcePath")(function* (
40 fileName: string,
41): Effect.fn.Return<
42 Option.Option<string>,
43 DesktopAssetProbeError,
44 FileSystem.FileSystem | DesktopEnvironment.DesktopEnvironment
45> {
46 const fileSystem = yield* FileSystem.FileSystem;
47 const environment = yield* DesktopEnvironment.DesktopEnvironment;
48 const candidates = environment.resolveResourcePathCandidates(fileName);
49 for (const candidate of candidates) {
50 const exists = yield* fileSystem
51 .exists(candidate)
52 .pipe(
53 Effect.mapError(
54 (cause) => new DesktopAssetProbeError({ fileName, candidatePath: candidate, cause }),
55 ),
56 );
57 if (exists) {
58 return Option.some(candidate);
59 }
60 }
61 return Option.none<string>();
62});
63
64function resolveSourceTreeIconPath(
65 environment: DesktopEnvironment.DesktopEnvironment["Service"],
66 ext: keyof DesktopIconPaths,
67): string | undefined {
68 if (environment.isPackaged || ext === "icns") return undefined;
69 const fileName =
70 ext === "ico"
71 ? "icon.ico"
72 : environment.platform === "darwin"
73 ? "macos-1024.png"
74 : "icon-1024.png";
75 return environment.path.join(environment.rootDir, "assets", "knife", fileName);
76}
77
78const resolveIconPath = Effect.fn("desktop.assets.resolveIconPath")(function* (
79 ext: keyof DesktopIconPaths,
80): Effect.fn.Return<
81 Option.Option<string>,
82 DesktopAssetProbeError,
83 FileSystem.FileSystem | DesktopEnvironment.DesktopEnvironment
84> {
85 const fileSystem = yield* FileSystem.FileSystem;
86 const environment = yield* DesktopEnvironment.DesktopEnvironment;
87 const sourceTreeIconPath = resolveSourceTreeIconPath(environment, ext);
88 if (sourceTreeIconPath !== undefined) {
89 const sourceTreeIconExists = yield* fileSystem.exists(sourceTreeIconPath).pipe(
90 Effect.mapError(
91 (cause) =>
92 new DesktopAssetProbeError({
93 fileName: `icon.${ext}`,
94 candidatePath: sourceTreeIconPath,
95 cause,
96 }),
97 ),
98 );
99 if (sourceTreeIconExists) {
100 return Option.some(sourceTreeIconPath);
101 }
102 }
103
104 return yield* resolveResourcePath(`icon.${ext}`);
105});
106
107/** @public Service construction is part of the canonical Effect module API. */
108export const make = Effect.gen(function* () {
109 const context = yield* Effect.context<
110 FileSystem.FileSystem | DesktopEnvironment.DesktopEnvironment
111 >();
112 const [ico, icns, png] = yield* Effect.all(
113 [resolveIconPath("ico"), resolveIconPath("icns"), resolveIconPath("png")] as const,
114 { concurrency: "unbounded" },
115 );
116 const iconPaths = { ico, icns, png } satisfies DesktopIconPaths;
117
118 return DesktopAssets.of({
119 iconPaths: Effect.succeed(iconPaths),
120 resolveResourcePath: Effect.fn("desktop.assets.resolveResourcePath.scoped")(
121 function* (fileName) {
122 return yield* resolveResourcePath(fileName).pipe(Effect.provide(context));
123 },
124 ),
125 });
126});
127
128export const layer = Layer.effect(DesktopAssets, make);