diff --git a/apps/desktop/README.md b/apps/desktop/README.md index cb426169..fda41f5f 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -36,6 +36,15 @@ This writes `apps/desktop/assets/icon.icns` (full ICNS) and `icon.png` (1024 master). Packaging validation rejects incomplete ICNS types. Runtime also calls `app.dock.setIcon` as a Launch Services / Dock cache fallback. +Preview the committed icon at its real Dock size without starting EdgeEver: + +```sh +bun run preview:desktop:icon +``` + +The temporary preview stays in the Dock until you quit it from the icon's +context menu. + Build an unsigned installer for the current platform with: ```sh diff --git a/package.json b/package.json index a68e8e70..b892b671 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "prepare:brand:icons": "bun scripts/trace-brand-icon.mjs && bun scripts/prepare-brand-icons.mjs", "prepare:mobile:icons": "bun scripts/prepare-mobile-icons.mjs", "prepare:desktop:icons": "bun scripts/prepare-desktop-icons.mjs", + "preview:desktop:icon": "bun scripts/preview-macos-dock-icon.mjs", "build:android:fast": "bash scripts/build-android-local.sh fast", "build:android:fast:local": "bun --env-file=${EDGE_EVER_ANDROID_ENV_FILE:-$HOME/.config/edgeever/android/signing.env} run build:android:fast", "build:android:apk": "bash scripts/build-android-local.sh apk", @@ -43,7 +44,7 @@ "build:desktop": "EDGE_EVER_DESKTOP_BUILD=1 bun run prepare:desktop:icons && bun run build:web && bun run build:desktop:sidecar && bun run --cwd apps/desktop dist", "benchmark:desktop:sidecar": "bun scripts/benchmark-desktop-sidecar.mjs", "verify:web-performance": "bun scripts/verify-web-performance.mjs", - "test:release-planning": "bun test scripts/plan-native-release.test.mjs scripts/check-native-release-assets.test.mjs scripts/create-mac-update-metadata.test.mjs scripts/release.test.mjs scripts/validate-store-delivery.test.mjs scripts/store-delivery.test.mjs scripts/verify-android-apk-signature.test.mjs scripts/desktop-icns.test.mjs apps/web/src/lib/version-check.test.mjs apps/mobile/src/lib/mobile-release.test.ts", + "test:release-planning": "bun test scripts/plan-native-release.test.mjs scripts/check-native-release-assets.test.mjs scripts/create-mac-update-metadata.test.mjs scripts/release.test.mjs scripts/validate-store-delivery.test.mjs scripts/store-delivery.test.mjs scripts/verify-android-apk-signature.test.mjs scripts/desktop-icns.test.mjs scripts/preview-macos-dock-icon.test.mjs apps/web/src/lib/version-check.test.mjs apps/mobile/src/lib/mobile-release.test.ts", "release": "bun scripts/release.mjs", "publish:stores": "bun scripts/store-delivery.mjs", "test:desktop:sidecar": "bun scripts/test-desktop-sidecar.mjs", diff --git a/scripts/preview-macos-dock-icon.mjs b/scripts/preview-macos-dock-icon.mjs new file mode 100644 index 00000000..45a8e585 --- /dev/null +++ b/scripts/preview-macos-dock-icon.mjs @@ -0,0 +1,121 @@ +import { copyFile, mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { basename, dirname, join, resolve } from "node:path"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { assertMacIcnsComplete } from "./desktop-icns.mjs"; + +const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const defaultIconPath = join(projectRoot, "apps/desktop/assets/icon.icns"); + +export const PREVIEW_APP_NAME = "EdgeEver Logo Preview"; + +export const createPreviewInfoPlist = (bundleIdentifier) => ` + + + + CFBundleDisplayName + ${PREVIEW_APP_NAME} + CFBundleExecutable + DockIconPreview + CFBundleIconFile + AppIcon + CFBundleIdentifier + ${bundleIdentifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PREVIEW_APP_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSMinimumSystemVersion + 12.0 + LSUIElement + + NSHighResolutionCapable + + + +`; + +export const previewSwiftSource = `import AppKit + +final class DockIconPreviewDelegate: NSObject, NSApplicationDelegate { + func applicationDidFinishLaunching(_ notification: Notification) { + NSApp.setActivationPolicy(.regular) + if let iconPath = Bundle.main.path(forResource: "AppIcon", ofType: "icns"), + let icon = NSImage(contentsOfFile: iconPath) { + NSApp.applicationIconImage = icon + } + NSApp.activate(ignoringOtherApps: false) + } + + func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + false + } +} + +let application = NSApplication.shared +let delegate = DockIconPreviewDelegate() +application.delegate = delegate +application.run() +`; + +export const assertPreviewPlatform = (platform) => { + if (platform !== "darwin") { + throw new Error("Dock icon preview is only available on macOS."); + } +}; + +const run = (command, args) => { + const result = spawnSync(command, args, { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + if (result.status !== 0) { + throw new Error(`${command} ${args.join(" ")} failed:\n${result.stderr || result.stdout || result.error}`); + } +}; + +export const launchDockIconPreview = async ({ iconPath = defaultIconPath } = {}) => { + assertPreviewPlatform(process.platform); + const resolvedIconPath = resolve(iconPath); + assertMacIcnsComplete(await readFile(resolvedIconPath), resolvedIconPath); + + const previewRoot = await mkdtemp(join(tmpdir(), "edgeever-dock-icon-preview-")); + const appPath = join(previewRoot, `${PREVIEW_APP_NAME}.app`); + const contentsPath = join(appPath, "Contents"); + const macosPath = join(contentsPath, "MacOS"); + const resourcesPath = join(contentsPath, "Resources"); + const sourcePath = join(previewRoot, "DockIconPreview.swift"); + const executablePath = join(macosPath, "DockIconPreview"); + const bundleIdentifier = `org.edgeever.logo-preview.v${Date.now()}p${process.pid}`; + + await mkdir(macosPath, { recursive: true }); + await mkdir(resourcesPath, { recursive: true }); + await writeFile(sourcePath, previewSwiftSource); + await writeFile(join(contentsPath, "Info.plist"), createPreviewInfoPlist(bundleIdentifier)); + await copyFile(resolvedIconPath, join(resourcesPath, "AppIcon.icns")); + + run("xcrun", ["swiftc", sourcePath, "-o", executablePath, "-framework", "AppKit"]); + run("codesign", ["--force", "--deep", "--sign", "-", appPath]); + run("open", ["-n", appPath]); + + return { appPath, iconPath: resolvedIconPath }; +}; + +if (import.meta.main) { + try { + const iconPath = process.argv[2] ? resolve(process.cwd(), process.argv[2]) : defaultIconPath; + const result = await launchDockIconPreview({ iconPath }); + console.log(`[preview-macos-dock-icon] showing ${basename(result.iconPath)} in the Dock as “${PREVIEW_APP_NAME}”`); + console.log("[preview-macos-dock-icon] right-click the Dock icon and choose Quit when finished"); + } catch (error) { + console.error(`[preview-macos-dock-icon] ${error instanceof Error ? error.message : String(error)}`); + process.exit(1); + } +} diff --git a/scripts/preview-macos-dock-icon.test.mjs b/scripts/preview-macos-dock-icon.test.mjs new file mode 100644 index 00000000..fe2cd1fd --- /dev/null +++ b/scripts/preview-macos-dock-icon.test.mjs @@ -0,0 +1,28 @@ +import { describe, expect, test } from "bun:test"; +import { + PREVIEW_APP_NAME, + assertPreviewPlatform, + createPreviewInfoPlist, + previewSwiftSource, +} from "./preview-macos-dock-icon.mjs"; + +describe("macOS Dock icon preview", () => { + test("creates a regular app bundle that declares the preview icon", () => { + const plist = createPreviewInfoPlist("org.edgeever.logo-preview.test"); + expect(plist).toContain(`${PREVIEW_APP_NAME}`); + expect(plist).toContain("AppIcon"); + expect(plist).toContain("org.edgeever.logo-preview.test"); + expect(plist).toContain("LSUIElement\n "); + }); + + test("keeps the Cocoa preview alive and sets its Dock icon", () => { + expect(previewSwiftSource).toContain("NSApp.setActivationPolicy(.regular)"); + expect(previewSwiftSource).toContain("NSApp.applicationIconImage = icon"); + expect(previewSwiftSource).toContain("application.run()"); + }); + + test("rejects unsupported platforms", () => { + expect(() => assertPreviewPlatform("linux")).toThrow(/only available on macOS/i); + expect(() => assertPreviewPlatform("darwin")).not.toThrow(); + }); +});