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
33 changes: 33 additions & 0 deletions apps/desktop/resources/dmg/dmg-background-latest.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
64 changes: 64 additions & 0 deletions apps/desktop/resources/dmg/dmg-background-nightly.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed apps/desktop/resources/icon.icns
Binary file not shown.
Binary file removed apps/desktop/resources/icon.ico
Binary file not shown.
Binary file removed apps/desktop/resources/icon.png
Binary file not shown.
33 changes: 17 additions & 16 deletions apps/desktop/scripts/electron-launcher.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ export const APP_BUNDLE_ID = isDevelopment
? `com.t3tools.t3code.dev.${devBundleIdSuffix || "local"}`
: "com.t3tools.t3code";
const APP_PROTOCOL_SCHEMES = isDevelopment ? ["t3code-dev"] : ["t3code"];
const LAUNCHER_VERSION = 14;
const defaultIconPath = NodePath.join(desktopDir, "resources", "icon.icns");
const LAUNCHER_VERSION = 15;
const developmentMacIconPngPath = NodePath.join(
repoRoot,
"assets",
"dev",
"blueprint-macos-1024.png",
);
const productionMacIconPngPath = NodePath.join(repoRoot, "assets", "prod", "black-macos-1024.png");
// oxlint-disable-next-line t3code/no-global-process-runtime -- Standalone launcher script has no Effect runtime.
const hostPlatform = NodeOS.platform();

Expand Down Expand Up @@ -165,15 +165,22 @@ function registerMacLauncherBundle(appBundlePath) {
}
}

function ensureDevelopmentIconIcns(runtimeDir) {
const generatedIconPath = NodePath.join(runtimeDir, "icon-dev.icns");
export function resolveMacLauncherIconPaths(runtimeDir, development = isDevelopment) {
return {
sourceIconPath: development ? developmentMacIconPngPath : productionMacIconPngPath,
generatedIconPath: NodePath.join(runtimeDir, development ? "icon-dev.icns" : "icon-prod.icns"),
};
}

function ensureMacIconIcns(runtimeDir) {
const { sourceIconPath, generatedIconPath } = resolveMacLauncherIconPaths(runtimeDir);
NodeFS.mkdirSync(runtimeDir, { recursive: true });

if (!NodeFS.existsSync(developmentMacIconPngPath)) {
return defaultIconPath;
if (!NodeFS.existsSync(sourceIconPath)) {
throw new Error(`Desktop macOS icon source is missing at ${sourceIconPath}`);
}

const sourceMtimeMs = NodeFS.statSync(developmentMacIconPngPath).mtimeMs;
const sourceMtimeMs = NodeFS.statSync(sourceIconPath).mtimeMs;
if (
NodeFS.existsSync(generatedIconPath) &&
NodeFS.statSync(generatedIconPath).mtimeMs >= sourceMtimeMs
Expand All @@ -191,7 +198,7 @@ function ensureDevelopmentIconIcns(runtimeDir) {
"-z",
String(size),
String(size),
developmentMacIconPngPath,
sourceIconPath,
"--out",
NodePath.join(iconsetDir, `icon_${size}x${size}.png`),
]);
Expand All @@ -201,20 +208,14 @@ function ensureDevelopmentIconIcns(runtimeDir) {
"-z",
String(retinaSize),
String(retinaSize),
developmentMacIconPngPath,
sourceIconPath,
"--out",
NodePath.join(iconsetDir, `icon_${size}x${size}@2x.png`),
]);
}

runChecked("iconutil", ["-c", "icns", iconsetDir, "-o", generatedIconPath]);
return generatedIconPath;
} catch (error) {
console.warn(
"[desktop-launcher] Failed to generate dev macOS icon, falling back to default icon.",
error,
);
return defaultIconPath;
} finally {
NodeFS.rmSync(iconsetRoot, { recursive: true, force: true });
}
Expand Down Expand Up @@ -297,7 +298,7 @@ function buildMacLauncher(electronBinaryPath) {
const launcherBinaryPath = isDevelopment
? developmentPaths.launcherBinaryPath
: runtimeElectronBinaryPath;
const iconPath = isDevelopment ? ensureDevelopmentIconIcns(runtimeDir) : defaultIconPath;
const iconPath = ensureMacIconIcns(runtimeDir);
const metadataPath = NodePath.join(runtimeDir, "metadata.json");

NodeFS.mkdirSync(runtimeDir, { recursive: true });
Expand Down
11 changes: 11 additions & 0 deletions apps/desktop/scripts/electron-launcher.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { assert, describe, it } from "vite-plus/test";
import {
makeDevelopmentLauncherScript,
resolveElectronBinaryPath,
resolveMacLauncherIconPaths,
resolveMacLauncherPaths,
} from "./electron-launcher.mjs";

Expand Down Expand Up @@ -78,4 +79,14 @@ describe("electron development launcher", () => {
);
assert.notInclude(script, "node_modules/electron");
});

it("derives launcher icons from canonical development and production assets", () => {
const development = resolveMacLauncherIconPaths("/runtime", true);
const production = resolveMacLauncherIconPaths("/runtime", false);

assert.match(development.sourceIconPath, /assets\/dev\/blueprint-macos-1024\.png$/);
assert.equal(development.generatedIconPath, "/runtime/icon-dev.icns");
assert.match(production.sourceIconPath, /assets\/prod\/black-macos-1024\.png$/);
assert.equal(production.generatedIconPath, "/runtime/icon-prod.icns");
});
});
40 changes: 40 additions & 0 deletions apps/desktop/src/app/DesktopAssets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { assert, describe, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as PlatformError from "effect/PlatformError";

import * as DesktopAssets from "./DesktopAssets.ts";
Expand All @@ -22,6 +23,45 @@ const environmentLayer = DesktopEnvironment.layer({
}).pipe(Layer.provide(Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({}))));

describe("DesktopAssets", () => {
it.effect("uses canonical source-tree icons for unpackaged development", () =>
Effect.gen(function* () {
const developmentEnvironmentLayer = DesktopEnvironment.layer({
dirname: "/repo/apps/desktop/dist-electron",
homeDirectory: "/Users/alice",
platform: "linux",
processArch: "x64",
appVersion: "1.2.3",
appPath: "/repo",
isPackaged: false,
resourcesPath: "/repo/apps/desktop/resources",
runningUnderArm64Translation: false,
}).pipe(
Layer.provide(
Layer.mergeAll(
NodeServices.layer,
DesktopConfig.layerTest({ VITE_DEV_SERVER_URL: "http://localhost:5733" }),
),
),
);
const fileSystemLayer = FileSystem.layerNoop({
exists: (path) => Effect.succeed(String(path).includes("/assets/dev/")),
});
const assets = yield* DesktopAssets.DesktopAssets.pipe(
Effect.provide(
DesktopAssets.layer.pipe(
Layer.provide(Layer.merge(fileSystemLayer, developmentEnvironmentLayer)),
),
),
);

const icons = yield* assets.iconPaths;

assert.match(Option.getOrThrow(icons.ico), /assets\/dev\/blueprint-windows\.ico$/);
assert.match(Option.getOrThrow(icons.png), /assets\/dev\/blueprint-universal-1024\.png$/);
assert.isTrue(Option.isNone(icons.icns));
}),
);

it.effect("preserves the failed asset candidate and filesystem cause", () =>
Effect.gen(function* () {
const fileName = "custom.bin";
Expand Down
43 changes: 36 additions & 7 deletions apps/desktop/src/app/DesktopAssets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,35 @@ const resolveResourcePath = Effect.fn("desktop.assets.resolveResourcePath")(func
return Option.none<string>();
});

const sourceTreeIconFileNames = {
dev: {
ico: "blueprint-windows.ico",
macPng: "blueprint-macos-1024.png",
universalPng: "blueprint-universal-1024.png",
},
prod: {
ico: "t3-black-windows.ico",
macPng: "black-macos-1024.png",
universalPng: "black-universal-1024.png",
},
} as const;

function resolveSourceTreeIconPath(
environment: DesktopEnvironment.DesktopEnvironment["Service"],
ext: keyof DesktopIconPaths,
): string | undefined {
if (environment.isPackaged || ext === "icns") return undefined;
const brand = environment.isDevelopment ? "dev" : "prod";
const fileNames = sourceTreeIconFileNames[brand];
const fileName =
ext === "ico"
? fileNames.ico
: environment.platform === "darwin"
? fileNames.macPng
: fileNames.universalPng;
return environment.path.join(environment.rootDir, "assets", brand, fileName);
}

const resolveIconPath = Effect.fn("desktop.assets.resolveIconPath")(function* (
ext: keyof DesktopIconPaths,
): Effect.fn.Return<
Expand All @@ -70,20 +99,20 @@ const resolveIconPath = Effect.fn("desktop.assets.resolveIconPath")(function* (
> {
const fileSystem = yield* FileSystem.FileSystem;
const environment = yield* DesktopEnvironment.DesktopEnvironment;
if (environment.isDevelopment && environment.platform === "darwin" && ext === "png") {
const developmentDockIconPath = environment.developmentDockIconPath;
const developmentDockIconExists = yield* fileSystem.exists(developmentDockIconPath).pipe(
const sourceTreeIconPath = resolveSourceTreeIconPath(environment, ext);
if (sourceTreeIconPath !== undefined) {
const sourceTreeIconExists = yield* fileSystem.exists(sourceTreeIconPath).pipe(
Effect.mapError(
(cause) =>
new DesktopAssetProbeError({
fileName: "icon.png",
candidatePath: developmentDockIconPath,
fileName: `icon.${ext}`,
candidatePath: sourceTreeIconPath,
cause,
}),
),
);
if (developmentDockIconExists) {
return Option.some(developmentDockIconPath);
if (sourceTreeIconExists) {
return Option.some(sourceTreeIconPath);
}
}

Expand Down
2 changes: 0 additions & 2 deletions apps/desktop/src/app/DesktopEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ export class DesktopEnvironment extends Context.Service<
readonly runtimeInfo: DesktopRuntimeInfo;
readonly resolvePickFolderDefaultPath: (rawOptions: unknown) => Option.Option<string>;
readonly resolveResourcePathCandidates: (fileName: string) => readonly string[];
readonly developmentDockIconPath: string;
}
>()("@t3tools/desktop/app/DesktopEnvironment") {}

Expand Down Expand Up @@ -270,7 +269,6 @@ const make = Effect.fn("desktop.environment.make")(function* (
path.join(resourcesPath, "resources", fileName),
path.join(resourcesPath, fileName),
],
developmentDockIconPath: path.join(rootDir, "assets", "dev", "blueprint-macos-1024.png"),
});
});

Expand Down
5 changes: 5 additions & 0 deletions docs/internals/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ authenticated.

- Default build is unsigned/not notarized for local sharing.
- The DMG build uses `assets/prod/black-macos-1024.png` as the production app icon source.
- The DMG chrome follows the release channel: neutral for Latest and the Nightly sky artwork for
Nightly. Blueprint artwork remains exclusive to Dev builds. Packaging rasterizes the selected
SVG into standard and Retina PNGs inside the disposable staging directory.
- The Finder window is 540×412 while its background is 540×380; the extra 32px accounts for the
title bar included in Finder's window bounds.
- Desktop production windows load the bundled UI from the `t3code://app/` root URL (not a
`127.0.0.1` document URL, and not an explicit `index.html` path).
- Desktop packaging includes `apps/server/dist` (the `t3` backend) and starts it on loopback with an
Expand Down
Loading
Loading