feat(desktop): add Dock icon preview command

This commit is contained in:
tianma-if committed 2026-08-09 21:11:45 +08:00
1 parent dfe8abd965
commit a500db5289
4 files changed
+160 -1

No files matched your search

+9
View File
@@ -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
+2 -1
View File
@@ -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",
+121
View File
@@ -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) => `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>${PREVIEW_APP_NAME}</string>
<key>CFBundleExecutable</key>
<string>DockIconPreview</string>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
<key>CFBundleIdentifier</key>
<string>${bundleIdentifier}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${PREVIEW_APP_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
<key>LSUIElement</key>
<false/>
<key>NSHighResolutionCapable</key>
<true/>
</dict>
</plist>
`;
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);
}
}
+28
View File
@@ -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(`<string>${PREVIEW_APP_NAME}</string>`);
expect(plist).toContain("<string>AppIcon</string>");
expect(plist).toContain("<string>org.edgeever.logo-preview.test</string>");
expect(plist).toContain("<key>LSUIElement</key>\n <false/>");
});
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();
});
});