| author | |
| committer | |
| log | ad117235b544e23545fe39143812db2ddd41af1f |
| tree | 72ec6e2078215f2104c63667f488e359aa7ced55 |
| parent | 20a70420a85aecab0bde1a58921197f423e401cc |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Rodrigo Brechard <rodrigo@clubtidy.fr>
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>13 files changed, 384 insertions(+), 25 deletions(-)
apps/desktop/resources/dmg/dmg-background-latest.svg created+33| ... | @@ -0,0 +1,33 @@ | ||
| 1 | <svg width="1080" height="760" viewBox="0 0 540 380" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
| 2 | <defs> | ||
| 3 | <linearGradient id="latest-surface" x1="0" y1="0" x2="540" y2="380" gradientUnits="userSpaceOnUse"> | ||
| 4 | <stop stop-color="#FAFAF9"/> | ||
| 5 | <stop offset="1" stop-color="#F5F5F4"/> | ||
| 6 | </linearGradient> | ||
| 7 | <radialGradient id="latest-light" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(430 0) rotate(152) scale(320 150)"> | ||
| 8 | <stop stop-color="#FFFFFF"/> | ||
| 9 | <stop offset="1" stop-color="#FFFFFF" stop-opacity="0"/> | ||
| 10 | </radialGradient> | ||
| 11 | </defs> | ||
| 12 | |||
| 13 | <rect width="540" height="380" fill="url(#latest-surface)"/> | ||
| 14 | <rect width="540" height="112" fill="#EFEFED"/> | ||
| 15 | <rect width="540" height="112" fill="url(#latest-light)"/> | ||
| 16 | <path d="M0 111.5H540" stroke="#18181B" stroke-opacity="0.08"/> | ||
| 17 | <path d="M32 28H64" stroke="#18181B" stroke-width="2"/> | ||
| 18 | |||
| 19 | <g font-family="-apple-system, BlinkMacSystemFont, sans-serif" fill="#18181B"> | ||
| 20 | <text x="32" y="50" font-size="12" font-weight="600" letter-spacing="1.4">T3 CODE</text> | ||
| 21 | <text x="32" y="82" font-size="22" font-weight="600" letter-spacing="-0.45">Desktop</text> | ||
| 22 | <text x="508" y="50" text-anchor="end" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="8.5" font-weight="600" letter-spacing="1.2" fill="#71717A">LATEST RELEASE</text> | ||
| 23 | </g> | ||
| 24 | |||
| 25 | <path d="M196 224C242 239 307 201 362 220" stroke="#71717A" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="1 7" opacity="0.42"/> | ||
| 26 | <circle cx="362" cy="220" r="2" fill="#71717A" opacity="0.55"/> | ||
| 27 | <path d="M176 204V227L182 221L187.5 232L192 229.8L186.5 219H194L176 204Z" fill="#71717A" stroke="#F7F7F5" stroke-width="1.5" stroke-linejoin="round"/> | ||
| 28 | |||
| 29 | <g font-family="-apple-system, BlinkMacSystemFont, sans-serif" text-anchor="middle"> | ||
| 30 | <text x="270" y="322" font-size="13" font-weight="600" letter-spacing="-0.1" fill="#18181B">Drag T3 Code to Applications</text> | ||
| 31 | <text x="270" y="345" font-size="9.5" fill="#71717A">Open it from Applications when the copy finishes.</text> | ||
| 32 | </g> | ||
| 33 | </svg> | ||
apps/desktop/resources/dmg/dmg-background-nightly.svg created+64| ... | @@ -0,0 +1,64 @@ | ||
| 1 | <svg width="1080" height="760" viewBox="0 0 540 380" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
| 2 | <defs> | ||
| 3 | <linearGradient id="night-sky" x1="28" y1="0" x2="510" y2="112" gradientUnits="userSpaceOnUse"> | ||
| 4 | <stop stop-color="#07152F"/> | ||
| 5 | <stop offset="0.52" stop-color="#151443"/> | ||
| 6 | <stop offset="1" stop-color="#32155B"/> | ||
| 7 | </linearGradient> | ||
| 8 | <radialGradient id="night-glow" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(420 18) rotate(157) scale(260 126)"> | ||
| 9 | <stop stop-color="#5165D8" stop-opacity="0.42"/> | ||
| 10 | <stop offset="0.55" stop-color="#283075" stop-opacity="0.15"/> | ||
| 11 | <stop offset="1" stop-color="#111635" stop-opacity="0"/> | ||
| 12 | </radialGradient> | ||
| 13 | <linearGradient id="night-cloud" x1="18" y1="62" x2="260" y2="112" gradientUnits="userSpaceOnUse"> | ||
| 14 | <stop stop-color="#4EA4FF" stop-opacity="0.5"/> | ||
| 15 | <stop offset="0.52" stop-color="#696FEA" stop-opacity="0.62"/> | ||
| 16 | <stop offset="1" stop-color="#A85BEA" stop-opacity="0.5"/> | ||
| 17 | </linearGradient> | ||
| 18 | <filter id="night-soft" x="-24" y="35" width="588" height="101" filterUnits="userSpaceOnUse"> | ||
| 19 | <feGaussianBlur stdDeviation="4"/> | ||
| 20 | </filter> | ||
| 21 | </defs> | ||
| 22 | |||
| 23 | <rect width="540" height="380" fill="#F7F7F5"/> | ||
| 24 | <rect width="540" height="112" fill="url(#night-sky)"/> | ||
| 25 | <rect width="540" height="112" fill="url(#night-glow)"/> | ||
| 26 | |||
| 27 | <g fill="#E4EAFF"> | ||
| 28 | <circle cx="20" cy="18" r="0.8" opacity="0.82"/> | ||
| 29 | <circle cx="76" cy="27" r="0.55" opacity="0.55"/> | ||
| 30 | <circle cx="133" cy="13" r="0.65" opacity="0.7"/> | ||
| 31 | <circle cx="198" cy="28" r="0.5" opacity="0.5"/> | ||
| 32 | <circle cx="258" cy="15" r="0.75" opacity="0.8"/> | ||
| 33 | <circle cx="326" cy="29" r="0.5" opacity="0.55"/> | ||
| 34 | <circle cx="384" cy="12" r="0.65" opacity="0.72"/> | ||
| 35 | <circle cx="455" cy="26" r="0.55" opacity="0.58"/> | ||
| 36 | <circle cx="517" cy="15" r="0.75" opacity="0.78"/> | ||
| 37 | </g> | ||
| 38 | <g stroke="#EEF2FF" stroke-linecap="round" stroke-width="0.65" opacity="0.68"> | ||
| 39 | <path d="M104 30V34M102 32H106"/> | ||
| 40 | <path d="M448 24V28M446 26H450"/> | ||
| 41 | </g> | ||
| 42 | |||
| 43 | <g filter="url(#night-soft)"> | ||
| 44 | <path d="M-12 112V101C-12 87 0 76 14 76C18 63 30 54 44 54C58 54 70 62 74 75C79 70 86 67 94 67C110 67 123 79 124 95C132 96 138 101 141 112H-12Z" fill="url(#night-cloud)"/> | ||
| 45 | <path d="M374 112C375 98 387 87 401 87C405 72 418 62 434 62C450 62 463 72 467 87C478 87 488 94 492 104C506 103 518 106 526 112H374Z" fill="url(#night-cloud)" opacity="0.82"/> | ||
| 46 | </g> | ||
| 47 | <path d="M0 111.5H540" stroke="#050A19" stroke-opacity="0.3"/> | ||
| 48 | <path d="M32 28H64" stroke="#FFFFFF" stroke-width="2"/> | ||
| 49 | |||
| 50 | <g font-family="-apple-system, BlinkMacSystemFont, sans-serif" fill="#FFFFFF"> | ||
| 51 | <text x="32" y="50" font-size="12" font-weight="600" letter-spacing="1.4">T3 CODE</text> | ||
| 52 | <text x="32" y="82" font-size="22" font-weight="600" letter-spacing="-0.45">Desktop</text> | ||
| 53 | <text x="508" y="50" text-anchor="end" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="8.5" font-weight="600" letter-spacing="1.2" opacity="0.84">NIGHTLY BUILD</text> | ||
| 54 | </g> | ||
| 55 | |||
| 56 | <path d="M196 224C242 239 307 201 362 220" stroke="#71717A" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="1 7" opacity="0.42"/> | ||
| 57 | <circle cx="362" cy="220" r="2" fill="#71717A" opacity="0.55"/> | ||
| 58 | <path d="M176 204V227L182 221L187.5 232L192 229.8L186.5 219H194L176 204Z" fill="#71717A" stroke="#F7F7F5" stroke-width="1.5" stroke-linejoin="round"/> | ||
| 59 | |||
| 60 | <g font-family="-apple-system, BlinkMacSystemFont, sans-serif" text-anchor="middle"> | ||
| 61 | <text x="270" y="322" font-size="13" font-weight="600" letter-spacing="-0.1" fill="#18181B">Drag T3 Code to Applications</text> | ||
| 62 | <text x="270" y="345" font-size="9.5" fill="#71717A">Open it from Applications when the copy finishes.</text> | ||
| 63 | </g> | ||
| 64 | </svg> | ||
apps/desktop/resources/icon.icns deleted| Binary files a/apps/desktop/resources/icon.icns and /dev/null differ | |||
apps/desktop/resources/icon.ico deleted| Binary files a/apps/desktop/resources/icon.ico and /dev/null differ | |||
apps/desktop/resources/icon.png deleted| Binary files a/apps/desktop/resources/icon.png and /dev/null differ | |||
apps/desktop/scripts/electron-launcher.mjs+17-16| ... | @@ -20,14 +20,14 @@ export const APP_BUNDLE_ID = isDevelopment | ... | @@ -20,14 +20,14 @@ export const APP_BUNDLE_ID = isDevelopment |
| 20 | ? `com.t3tools.t3code.dev.${devBundleIdSuffix || "local"}` | 20 | ? `com.t3tools.t3code.dev.${devBundleIdSuffix || "local"}` |
| 21 | : "com.t3tools.t3code"; | 21 | : "com.t3tools.t3code"; |
| 22 | const APP_PROTOCOL_SCHEMES = isDevelopment ? ["t3code-dev"] : ["t3code"]; | 22 | const APP_PROTOCOL_SCHEMES = isDevelopment ? ["t3code-dev"] : ["t3code"]; |
| 23 | const LAUNCHER_VERSION = 14; | 23 | const LAUNCHER_VERSION = 15; |
| 24 | const defaultIconPath = NodePath.join(desktopDir, "resources", "icon.icns"); | ||
| 25 | const developmentMacIconPngPath = NodePath.join( | 24 | const developmentMacIconPngPath = NodePath.join( |
| 26 | repoRoot, | 25 | repoRoot, |
| 27 | "assets", | 26 | "assets", |
| 28 | "dev", | 27 | "dev", |
| 29 | "blueprint-macos-1024.png", | 28 | "blueprint-macos-1024.png", |
| 30 | ); | 29 | ); |
| 30 | const productionMacIconPngPath = NodePath.join(repoRoot, "assets", "prod", "black-macos-1024.png"); | ||
| 31 | // oxlint-disable-next-line t3code/no-global-process-runtime -- Standalone launcher script has no Effect runtime. | 31 | // oxlint-disable-next-line t3code/no-global-process-runtime -- Standalone launcher script has no Effect runtime. |
| 32 | const hostPlatform = NodeOS.platform(); | 32 | const hostPlatform = NodeOS.platform(); |
| 33 | 33 | ||
| ... | @@ -165,15 +165,22 @@ function registerMacLauncherBundle(appBundlePath) { | ... | @@ -165,15 +165,22 @@ function registerMacLauncherBundle(appBundlePath) { |
| 165 | } | 165 | } |
| 166 | } | 166 | } |
| 167 | 167 | ||
| 168 | function ensureDevelopmentIconIcns(runtimeDir) { | 168 | export function resolveMacLauncherIconPaths(runtimeDir, development = isDevelopment) { |
| 169 | const generatedIconPath = NodePath.join(runtimeDir, "icon-dev.icns"); | 169 | return { |
| 170 | sourceIconPath: development ? developmentMacIconPngPath : productionMacIconPngPath, | ||
| 171 | generatedIconPath: NodePath.join(runtimeDir, development ? "icon-dev.icns" : "icon-prod.icns"), | ||
| 172 | }; | ||
| 173 | } | ||
| 174 | |||
| 175 | function ensureMacIconIcns(runtimeDir) { | ||
| 176 | const { sourceIconPath, generatedIconPath } = resolveMacLauncherIconPaths(runtimeDir); | ||
| 170 | NodeFS.mkdirSync(runtimeDir, { recursive: true }); | 177 | NodeFS.mkdirSync(runtimeDir, { recursive: true }); |
| 171 | 178 | ||
| 172 | if (!NodeFS.existsSync(developmentMacIconPngPath)) { | 179 | if (!NodeFS.existsSync(sourceIconPath)) { |
| 173 | return defaultIconPath; | 180 | throw new Error(`Desktop macOS icon source is missing at ${sourceIconPath}`); |
| 174 | } | 181 | } |
| 175 | 182 | ||
| 176 | const sourceMtimeMs = NodeFS.statSync(developmentMacIconPngPath).mtimeMs; | 183 | const sourceMtimeMs = NodeFS.statSync(sourceIconPath).mtimeMs; |
| 177 | if ( | 184 | if ( |
| 178 | NodeFS.existsSync(generatedIconPath) && | 185 | NodeFS.existsSync(generatedIconPath) && |
| 179 | NodeFS.statSync(generatedIconPath).mtimeMs >= sourceMtimeMs | 186 | NodeFS.statSync(generatedIconPath).mtimeMs >= sourceMtimeMs |
| ... | @@ -191,7 +198,7 @@ function ensureDevelopmentIconIcns(runtimeDir) { | ... | @@ -191,7 +198,7 @@ function ensureDevelopmentIconIcns(runtimeDir) { |
| 191 | "-z", | 198 | "-z", |
| 192 | String(size), | 199 | String(size), |
| 193 | String(size), | 200 | String(size), |
| 194 | developmentMacIconPngPath, | 201 | sourceIconPath, |
| 195 | "--out", | 202 | "--out", |
| 196 | NodePath.join(iconsetDir, `icon_${size}x${size}.png`), | 203 | NodePath.join(iconsetDir, `icon_${size}x${size}.png`), |
| 197 | ]); | 204 | ]); |
| ... | @@ -201,7 +208,7 @@ function ensureDevelopmentIconIcns(runtimeDir) { | ... | @@ -201,7 +208,7 @@ function ensureDevelopmentIconIcns(runtimeDir) { |
| 201 | "-z", | 208 | "-z", |
| 202 | String(retinaSize), | 209 | String(retinaSize), |
| 203 | String(retinaSize), | 210 | String(retinaSize), |
| 204 | developmentMacIconPngPath, | 211 | sourceIconPath, |
| 205 | "--out", | 212 | "--out", |
| 206 | NodePath.join(iconsetDir, `icon_${size}x${size}@2x.png`), | 213 | NodePath.join(iconsetDir, `icon_${size}x${size}@2x.png`), |
| 207 | ]); | 214 | ]); |
| ... | @@ -209,12 +216,6 @@ function ensureDevelopmentIconIcns(runtimeDir) { | ... | @@ -209,12 +216,6 @@ function ensureDevelopmentIconIcns(runtimeDir) { |
| 209 | 216 | ||
| 210 | runChecked("iconutil", ["-c", "icns", iconsetDir, "-o", generatedIconPath]); | 217 | runChecked("iconutil", ["-c", "icns", iconsetDir, "-o", generatedIconPath]); |
| 211 | return generatedIconPath; | 218 | return generatedIconPath; |
| 212 | } catch (error) { | ||
| 213 | console.warn( | ||
| 214 | "[desktop-launcher] Failed to generate dev macOS icon, falling back to default icon.", | ||
| 215 | error, | ||
| 216 | ); | ||
| 217 | return defaultIconPath; | ||
| 218 | } finally { | 219 | } finally { |
| 219 | NodeFS.rmSync(iconsetRoot, { recursive: true, force: true }); | 220 | NodeFS.rmSync(iconsetRoot, { recursive: true, force: true }); |
| 220 | } | 221 | } |
| ... | @@ -297,7 +298,7 @@ function buildMacLauncher(electronBinaryPath) { | ... | @@ -297,7 +298,7 @@ function buildMacLauncher(electronBinaryPath) { |
| 297 | const launcherBinaryPath = isDevelopment | 298 | const launcherBinaryPath = isDevelopment |
| 298 | ? developmentPaths.launcherBinaryPath | 299 | ? developmentPaths.launcherBinaryPath |
| 299 | : runtimeElectronBinaryPath; | 300 | : runtimeElectronBinaryPath; |
| 300 | const iconPath = isDevelopment ? ensureDevelopmentIconIcns(runtimeDir) : defaultIconPath; | 301 | const iconPath = ensureMacIconIcns(runtimeDir); |
| 301 | const metadataPath = NodePath.join(runtimeDir, "metadata.json"); | 302 | const metadataPath = NodePath.join(runtimeDir, "metadata.json"); |
| 302 | 303 | ||
| 303 | NodeFS.mkdirSync(runtimeDir, { recursive: true }); | 304 | NodeFS.mkdirSync(runtimeDir, { recursive: true }); |
apps/desktop/scripts/electron-launcher.test.mjs+11| ... | @@ -3,6 +3,7 @@ import { assert, describe, it } from "vite-plus/test"; | ... | @@ -3,6 +3,7 @@ import { assert, describe, it } from "vite-plus/test"; |
| 3 | import { | 3 | import { |
| 4 | makeDevelopmentLauncherScript, | 4 | makeDevelopmentLauncherScript, |
| 5 | resolveElectronBinaryPath, | 5 | resolveElectronBinaryPath, |
| 6 | resolveMacLauncherIconPaths, | ||
| 6 | resolveMacLauncherPaths, | 7 | resolveMacLauncherPaths, |
| 7 | } from "./electron-launcher.mjs"; | 8 | } from "./electron-launcher.mjs"; |
| 8 | 9 | ||
| ... | @@ -78,4 +79,14 @@ describe("electron development launcher", () => { | ... | @@ -78,4 +79,14 @@ describe("electron development launcher", () => { |
| 78 | ); | 79 | ); |
| 79 | assert.notInclude(script, "node_modules/electron"); | 80 | assert.notInclude(script, "node_modules/electron"); |
| 80 | }); | 81 | }); |
| 82 | |||
| 83 | it("derives launcher icons from canonical development and production assets", () => { | ||
| 84 | const development = resolveMacLauncherIconPaths("/runtime", true); | ||
| 85 | const production = resolveMacLauncherIconPaths("/runtime", false); | ||
| 86 | |||
| 87 | assert.match(development.sourceIconPath, /assets\/dev\/blueprint-macos-1024\.png$/); | ||
| 88 | assert.equal(development.generatedIconPath, "/runtime/icon-dev.icns"); | ||
| 89 | assert.match(production.sourceIconPath, /assets\/prod\/black-macos-1024\.png$/); | ||
| 90 | assert.equal(production.generatedIconPath, "/runtime/icon-prod.icns"); | ||
| 91 | }); | ||
| 81 | }); | 92 | }); |
apps/desktop/src/app/DesktopAssets.test.ts+40| ... | @@ -3,6 +3,7 @@ import { assert, describe, it } from "@effect/vitest"; | ... | @@ -3,6 +3,7 @@ import { assert, describe, it } from "@effect/vitest"; |
| 3 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as FileSystem from "effect/FileSystem"; | 4 | import * as FileSystem from "effect/FileSystem"; |
| 5 | import * as Layer from "effect/Layer"; | 5 | import * as Layer from "effect/Layer"; |
| 6 | import * as Option from "effect/Option"; | ||
| 6 | import * as PlatformError from "effect/PlatformError"; | 7 | import * as PlatformError from "effect/PlatformError"; |
| 7 | 8 | ||
| 8 | import * as DesktopAssets from "./DesktopAssets.ts"; | 9 | import * as DesktopAssets from "./DesktopAssets.ts"; |
| ... | @@ -22,6 +23,45 @@ const environmentLayer = DesktopEnvironment.layer({ | ... | @@ -22,6 +23,45 @@ const environmentLayer = DesktopEnvironment.layer({ |
| 22 | }).pipe(Layer.provide(Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({})))); | 23 | }).pipe(Layer.provide(Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({})))); |
| 23 | 24 | ||
| 24 | describe("DesktopAssets", () => { | 25 | describe("DesktopAssets", () => { |
| 26 | it.effect("uses canonical source-tree icons for unpackaged development", () => | ||
| 27 | Effect.gen(function* () { | ||
| 28 | const developmentEnvironmentLayer = DesktopEnvironment.layer({ | ||
| 29 | dirname: "/repo/apps/desktop/dist-electron", | ||
| 30 | homeDirectory: "/Users/alice", | ||
| 31 | platform: "linux", | ||
| 32 | processArch: "x64", | ||
| 33 | appVersion: "1.2.3", | ||
| 34 | appPath: "/repo", | ||
| 35 | isPackaged: false, | ||
| 36 | resourcesPath: "/repo/apps/desktop/resources", | ||
| 37 | runningUnderArm64Translation: false, | ||
| 38 | }).pipe( | ||
| 39 | Layer.provide( | ||
| 40 | Layer.mergeAll( | ||
| 41 | NodeServices.layer, | ||
| 42 | DesktopConfig.layerTest({ VITE_DEV_SERVER_URL: "http://localhost:5733" }), | ||
| 43 | ), | ||
| 44 | ), | ||
| 45 | ); | ||
| 46 | const fileSystemLayer = FileSystem.layerNoop({ | ||
| 47 | exists: (path) => Effect.succeed(String(path).includes("/assets/dev/")), | ||
| 48 | }); | ||
| 49 | const assets = yield* DesktopAssets.DesktopAssets.pipe( | ||
| 50 | Effect.provide( | ||
| 51 | DesktopAssets.layer.pipe( | ||
| 52 | Layer.provide(Layer.merge(fileSystemLayer, developmentEnvironmentLayer)), | ||
| 53 | ), | ||
| 54 | ), | ||
| 55 | ); | ||
| 56 | |||
| 57 | const icons = yield* assets.iconPaths; | ||
| 58 | |||
| 59 | assert.match(Option.getOrThrow(icons.ico), /assets\/dev\/blueprint-windows\.ico$/); | ||
| 60 | assert.match(Option.getOrThrow(icons.png), /assets\/dev\/blueprint-universal-1024\.png$/); | ||
| 61 | assert.isTrue(Option.isNone(icons.icns)); | ||
| 62 | }), | ||
| 63 | ); | ||
| 64 | |||
| 25 | it.effect("preserves the failed asset candidate and filesystem cause", () => | 65 | it.effect("preserves the failed asset candidate and filesystem cause", () => |
| 26 | Effect.gen(function* () { | 66 | Effect.gen(function* () { |
| 27 | const fileName = "custom.bin"; | 67 | const fileName = "custom.bin"; |
apps/desktop/src/app/DesktopAssets.ts+36-7| ... | @@ -61,6 +61,35 @@ const resolveResourcePath = Effect.fn("desktop.assets.resolveResourcePath")(func | ... | @@ -61,6 +61,35 @@ const resolveResourcePath = Effect.fn("desktop.assets.resolveResourcePath")(func |
| 61 | return Option.none<string>(); | 61 | return Option.none<string>(); |
| 62 | }); | 62 | }); |
| 63 | 63 | ||
| 64 | const sourceTreeIconFileNames = { | ||
| 65 | dev: { | ||
| 66 | ico: "blueprint-windows.ico", | ||
| 67 | macPng: "blueprint-macos-1024.png", | ||
| 68 | universalPng: "blueprint-universal-1024.png", | ||
| 69 | }, | ||
| 70 | prod: { | ||
| 71 | ico: "t3-black-windows.ico", | ||
| 72 | macPng: "black-macos-1024.png", | ||
| 73 | universalPng: "black-universal-1024.png", | ||
| 74 | }, | ||
| 75 | } as const; | ||
| 76 | |||
| 77 | function resolveSourceTreeIconPath( | ||
| 78 | environment: DesktopEnvironment.DesktopEnvironment["Service"], | ||
| 79 | ext: keyof DesktopIconPaths, | ||
| 80 | ): string | undefined { | ||
| 81 | if (environment.isPackaged || ext === "icns") return undefined; | ||
| 82 | const brand = environment.isDevelopment ? "dev" : "prod"; | ||
| 83 | const fileNames = sourceTreeIconFileNames[brand]; | ||
| 84 | const fileName = | ||
| 85 | ext === "ico" | ||
| 86 | ? fileNames.ico | ||
| 87 | : environment.platform === "darwin" | ||
| 88 | ? fileNames.macPng | ||
| 89 | : fileNames.universalPng; | ||
| 90 | return environment.path.join(environment.rootDir, "assets", brand, fileName); | ||
| 91 | } | ||
| 92 | |||
| 64 | const resolveIconPath = Effect.fn("desktop.assets.resolveIconPath")(function* ( | 93 | const resolveIconPath = Effect.fn("desktop.assets.resolveIconPath")(function* ( |
| 65 | ext: keyof DesktopIconPaths, | 94 | ext: keyof DesktopIconPaths, |
| 66 | ): Effect.fn.Return< | 95 | ): Effect.fn.Return< |
| ... | @@ -70,20 +99,20 @@ const resolveIconPath = Effect.fn("desktop.assets.resolveIconPath")(function* ( | ... | @@ -70,20 +99,20 @@ const resolveIconPath = Effect.fn("desktop.assets.resolveIconPath")(function* ( |
| 70 | > { | 99 | > { |
| 71 | const fileSystem = yield* FileSystem.FileSystem; | 100 | const fileSystem = yield* FileSystem.FileSystem; |
| 72 | const environment = yield* DesktopEnvironment.DesktopEnvironment; | 101 | const environment = yield* DesktopEnvironment.DesktopEnvironment; |
| 73 | if (environment.isDevelopment && environment.platform === "darwin" && ext === "png") { | 102 | const sourceTreeIconPath = resolveSourceTreeIconPath(environment, ext); |
| 74 | const developmentDockIconPath = environment.developmentDockIconPath; | 103 | if (sourceTreeIconPath !== undefined) { |
| 75 | const developmentDockIconExists = yield* fileSystem.exists(developmentDockIconPath).pipe( | 104 | const sourceTreeIconExists = yield* fileSystem.exists(sourceTreeIconPath).pipe( |
| 76 | Effect.mapError( | 105 | Effect.mapError( |
| 77 | (cause) => | 106 | (cause) => |
| 78 | new DesktopAssetProbeError({ | 107 | new DesktopAssetProbeError({ |
| 79 | fileName: "icon.png", | 108 | fileName: `icon.${ext}`, |
| 80 | candidatePath: developmentDockIconPath, | 109 | candidatePath: sourceTreeIconPath, |
| 81 | cause, | 110 | cause, |
| 82 | }), | 111 | }), |
| 83 | ), | 112 | ), |
| 84 | ); | 113 | ); |
| 85 | if (developmentDockIconExists) { | 114 | if (sourceTreeIconExists) { |
| 86 | return Option.some(developmentDockIconPath); | 115 | return Option.some(sourceTreeIconPath); |
| 87 | } | 116 | } |
| 88 | } | 117 | } |
| 89 | 118 |
apps/desktop/src/app/DesktopEnvironment.ts-2| ... | @@ -82,7 +82,6 @@ export class DesktopEnvironment extends Context.Service< | ... | @@ -82,7 +82,6 @@ export class DesktopEnvironment extends Context.Service< |
| 82 | readonly runtimeInfo: DesktopRuntimeInfo; | 82 | readonly runtimeInfo: DesktopRuntimeInfo; |
| 83 | readonly resolvePickFolderDefaultPath: (rawOptions: unknown) => Option.Option<string>; | 83 | readonly resolvePickFolderDefaultPath: (rawOptions: unknown) => Option.Option<string>; |
| 84 | readonly resolveResourcePathCandidates: (fileName: string) => readonly string[]; | 84 | readonly resolveResourcePathCandidates: (fileName: string) => readonly string[]; |
| 85 | readonly developmentDockIconPath: string; | ||
| 86 | } | 85 | } |
| 87 | >()("@t3tools/desktop/app/DesktopEnvironment") {} | 86 | >()("@t3tools/desktop/app/DesktopEnvironment") {} |
| 88 | 87 | ||
| ... | @@ -270,7 +269,6 @@ const make = Effect.fn("desktop.environment.make")(function* ( | ... | @@ -270,7 +269,6 @@ const make = Effect.fn("desktop.environment.make")(function* ( |
| 270 | path.join(resourcesPath, "resources", fileName), | 269 | path.join(resourcesPath, "resources", fileName), |
| 271 | path.join(resourcesPath, fileName), | 270 | path.join(resourcesPath, fileName), |
| 272 | ], | 271 | ], |
| 273 | developmentDockIconPath: path.join(rootDir, "assets", "dev", "blueprint-macos-1024.png"), | ||
| 274 | }); | 272 | }); |
| 275 | }); | 273 | }); |
| 276 | 274 |
docs/internals/scripts.md+5| ... | @@ -78,6 +78,11 @@ authenticated. | ... | @@ -78,6 +78,11 @@ authenticated. |
| 78 | 78 | ||
| 79 | - Default build is unsigned/not notarized for local sharing. | 79 | - Default build is unsigned/not notarized for local sharing. |
| 80 | - The DMG build uses `assets/prod/black-macos-1024.png` as the production app icon source. | 80 | - The DMG build uses `assets/prod/black-macos-1024.png` as the production app icon source. |
| 81 | - The DMG chrome follows the release channel: neutral for Latest and the Nightly sky artwork for | ||
| 82 | Nightly. Blueprint artwork remains exclusive to Dev builds. Packaging rasterizes the selected | ||
| 83 | SVG into standard and Retina PNGs inside the disposable staging directory. | ||
| 84 | - The Finder window is 540×412 while its background is 540×380; the extra 32px accounts for the | ||
| 85 | title bar included in Finder's window bounds. | ||
| 81 | - Desktop production windows load the bundled UI from the `t3code://app/` root URL (not a | 86 | - Desktop production windows load the bundled UI from the `t3code://app/` root URL (not a |
| 82 | `127.0.0.1` document URL, and not an explicit `index.html` path). | 87 | `127.0.0.1` document URL, and not an explicit `index.html` path). |
| 83 | - Desktop packaging includes `apps/server/dist` (the `t3` backend) and starts it on loopback with an | 88 | - Desktop packaging includes `apps/server/dist` (the `t3` backend) and starts it on loopback with an |
scripts/build-desktop-artifact.test.ts+103| ... | @@ -13,6 +13,7 @@ import { ChildProcessSpawner } from "effect/unstable/process"; | ... | @@ -13,6 +13,7 @@ import { ChildProcessSpawner } from "effect/unstable/process"; |
| 13 | import { | 13 | import { |
| 14 | BundleNotSelfContainedError, | 14 | BundleNotSelfContainedError, |
| 15 | BuildCommandFailedError, | 15 | BuildCommandFailedError, |
| 16 | DesktopDmgBackgroundSourceMissingError, | ||
| 16 | createStageWorkspaceConfig, | 17 | createStageWorkspaceConfig, |
| 17 | createStagePatchedDependencies, | 18 | createStagePatchedDependencies, |
| 18 | createBuildConfig, | 19 | createBuildConfig, |
| ... | @@ -45,6 +46,7 @@ import { | ... | @@ -45,6 +46,7 @@ import { |
| 45 | resolveMockUpdateServerUrl, | 46 | resolveMockUpdateServerUrl, |
| 46 | resolvePackageManagerUserAgent, | 47 | resolvePackageManagerUserAgent, |
| 47 | stageLinuxIconSize, | 48 | stageLinuxIconSize, |
| 49 | stageDesktopDmgBackground, | ||
| 48 | STAGE_INSTALL_ARGS, | 50 | STAGE_INSTALL_ARGS, |
| 49 | ancestorNodeModulesPaths, | 51 | ancestorNodeModulesPaths, |
| 50 | copyDirectoryPreservingSymlinks, | 52 | copyDirectoryPreservingSymlinks, |
| ... | @@ -463,6 +465,17 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { | ... | @@ -463,6 +465,17 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { |
| 463 | "**/node_modules/.bin", | 465 | "**/node_modules/.bin", |
| 464 | "**/node_modules/.bin/**", | 466 | "**/node_modules/.bin/**", |
| 465 | ]); | 467 | ]); |
| 468 | assert.deepStrictEqual(mac.dmg, { | ||
| 469 | title: "T3 Code (Alpha) 1.2.3 Installer", | ||
| 470 | background: "dmg/dmg-background-latest.png", | ||
| 471 | window: { width: 540, height: 412 }, | ||
| 472 | contents: [ | ||
| 473 | { x: 130, y: 220, type: "file" }, | ||
| 474 | { x: 410, y: 220, type: "link", path: "/Applications" }, | ||
| 475 | ], | ||
| 476 | iconSize: 80, | ||
| 477 | iconTextSize: 12, | ||
| 478 | }); | ||
| 466 | // Linux must register the renderer schemes so the generated .desktop | 479 | // Linux must register the renderer schemes so the generated .desktop |
| 467 | // entry advertises MimeType=x-scheme-handler/t3code; for OAuth deep links. | 480 | // entry advertises MimeType=x-scheme-handler/t3code; for OAuth deep links. |
| 468 | assert.deepStrictEqual((linux.linux as Record<string, unknown>).protocols, [ | 481 | assert.deepStrictEqual((linux.linux as Record<string, unknown>).protocols, [ |
| ... | @@ -784,6 +797,77 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { | ... | @@ -784,6 +797,77 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { |
| 784 | }); | 797 | }); |
| 785 | }); | 798 | }); |
| 786 | 799 | ||
| 800 | it.effect("rasterizes staged DMG backgrounds at standard and Retina sizes", () => | ||
| 801 | Effect.scoped( | ||
| 802 | Effect.gen(function* () { | ||
| 803 | const fs = yield* FileSystem.FileSystem; | ||
| 804 | const path = yield* Path.Path; | ||
| 805 | const stageResourcesDir = yield* fs.makeTempDirectoryScoped({ | ||
| 806 | prefix: "t3code-dmg-background-", | ||
| 807 | }); | ||
| 808 | const dmgDir = path.join(stageResourcesDir, "dmg"); | ||
| 809 | yield* fs.makeDirectory(dmgDir, { recursive: true }); | ||
| 810 | const sourcePath = path.join(dmgDir, "dmg-background-nightly.svg"); | ||
| 811 | yield* fs.writeFileString(sourcePath, '<svg xmlns="http://www.w3.org/2000/svg"/>'); | ||
| 812 | const commands: Array<{ readonly command: string; readonly args: ReadonlyArray<string> }> = | ||
| 813 | []; | ||
| 814 | |||
| 815 | yield* stageDesktopDmgBackground(stageResourcesDir, "nightly", false).pipe( | ||
| 816 | Effect.provide(iconResizeSpawnerLayer(commands, [0, 0])), | ||
| 817 | ); | ||
| 818 | |||
| 819 | assert.deepStrictEqual( | ||
| 820 | commands.map((command) => [command.command, ...command.args]), | ||
| 821 | [ | ||
| 822 | [ | ||
| 823 | "sips", | ||
| 824 | "-s", | ||
| 825 | "format", | ||
| 826 | "png", | ||
| 827 | "-z", | ||
| 828 | "380", | ||
| 829 | "540", | ||
| 830 | sourcePath, | ||
| 831 | "--out", | ||
| 832 | path.join(dmgDir, "dmg-background-nightly.png"), | ||
| 833 | ], | ||
| 834 | [ | ||
| 835 | "sips", | ||
| 836 | "-s", | ||
| 837 | "format", | ||
| 838 | "png", | ||
| 839 | "-z", | ||
| 840 | "760", | ||
| 841 | "1080", | ||
| 842 | sourcePath, | ||
| 843 | "--out", | ||
| 844 | path.join(dmgDir, "dmg-background-nightly@2x.png"), | ||
| 845 | ], | ||
| 846 | ], | ||
| 847 | ); | ||
| 848 | }), | ||
| 849 | ), | ||
| 850 | ); | ||
| 851 | |||
| 852 | it.effect("fails clearly when the selected DMG background source is missing", () => | ||
| 853 | Effect.scoped( | ||
| 854 | Effect.gen(function* () { | ||
| 855 | const fs = yield* FileSystem.FileSystem; | ||
| 856 | const stageResourcesDir = yield* fs.makeTempDirectoryScoped({ | ||
| 857 | prefix: "t3code-dmg-background-missing-", | ||
| 858 | }); | ||
| 859 | |||
| 860 | const error = yield* stageDesktopDmgBackground(stageResourcesDir, "latest", false).pipe( | ||
| 861 | Effect.flip, | ||
| 862 | ); | ||
| 863 | |||
| 864 | assert.instanceOf(error, DesktopDmgBackgroundSourceMissingError); | ||
| 865 | assert.equal(error.channel, "latest"); | ||
| 866 | assert.include(error.sourcePath, "dmg-background-latest.svg"); | ||
| 867 | }), | ||
| 868 | ), | ||
| 869 | ); | ||
| 870 | |||
| 787 | it("derives macOS passkey signing configuration from the Clerk publishable key", () => { | 871 | it("derives macOS passkey signing configuration from the Clerk publishable key", () => { |
| 788 | const configuration = resolveMacPasskeySigningConfiguration({ | 872 | const configuration = resolveMacPasskeySigningConfiguration({ |
| 789 | T3CODE_APPLE_TEAM_ID: "abc1234567", | 873 | T3CODE_APPLE_TEAM_ID: "abc1234567", |
| ... | @@ -916,6 +1000,25 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { | ... | @@ -916,6 +1000,25 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { |
| 916 | }).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })))), | 1000 | }).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })))), |
| 917 | ); | 1001 | ); |
| 918 | 1002 | ||
| 1003 | it.effect("uses the nightly DMG background for nightly macOS builds", () => | ||
| 1004 | Effect.gen(function* () { | ||
| 1005 | const config = yield* createBuildConfig( | ||
| 1006 | "mac", | ||
| 1007 | "dmg", | ||
| 1008 | "1.2.3-nightly.20260815.1", | ||
| 1009 | false, | ||
| 1010 | false, | ||
| 1011 | undefined, | ||
| 1012 | undefined, | ||
| 1013 | ); | ||
| 1014 | |||
| 1015 | assert.equal( | ||
| 1016 | (config.dmg as Record<string, unknown>).background, | ||
| 1017 | "dmg/dmg-background-nightly.png", | ||
| 1018 | ); | ||
| 1019 | }).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })))), | ||
| 1020 | ); | ||
| 1021 | |||
| 919 | it.effect("keeps executable resource editing enabled for unsigned Windows builds", () => | 1022 | it.effect("keeps executable resource editing enabled for unsigned Windows builds", () => |
| 920 | Effect.gen(function* () { | 1023 | Effect.gen(function* () { |
| 921 | const config = yield* createBuildConfig( | 1024 | const config = yield* createBuildConfig( |
scripts/build-desktop-artifact.ts+75| ... | @@ -318,6 +318,18 @@ export class DesktopIconSourceMissingError extends Schema.TaggedErrorClass<Deskt | ... | @@ -318,6 +318,18 @@ export class DesktopIconSourceMissingError extends Schema.TaggedErrorClass<Deskt |
| 318 | } | 318 | } |
| 319 | } | 319 | } |
| 320 | 320 | ||
| 321 | export class DesktopDmgBackgroundSourceMissingError extends Schema.TaggedErrorClass<DesktopDmgBackgroundSourceMissingError>()( | ||
| 322 | "DesktopDmgBackgroundSourceMissingError", | ||
| 323 | { | ||
| 324 | channel: Schema.Literals(["latest", "nightly"]), | ||
| 325 | sourcePath: Schema.String, | ||
| 326 | }, | ||
| 327 | ) { | ||
| 328 | override get message(): string { | ||
| 329 | return `Desktop ${this.channel} DMG background source is missing at ${this.sourcePath}`; | ||
| 330 | } | ||
| 331 | } | ||
| 332 | |||
| 321 | export class BundledClientAssetsMissingError extends Schema.TaggedErrorClass<BundledClientAssetsMissingError>()( | 333 | export class BundledClientAssetsMissingError extends Schema.TaggedErrorClass<BundledClientAssetsMissingError>()( |
| 322 | "BundledClientAssetsMissingError", | 334 | "BundledClientAssetsMissingError", |
| 323 | { | 335 | { |
| ... | @@ -1770,6 +1782,39 @@ function stageMacIcons(stageResourcesDir: string, sourcePng: string, verbose: bo | ... | @@ -1770,6 +1782,39 @@ function stageMacIcons(stageResourcesDir: string, sourcePng: string, verbose: bo |
| 1770 | }); | 1782 | }); |
| 1771 | } | 1783 | } |
| 1772 | 1784 | ||
| 1785 | export const stageDesktopDmgBackground = Effect.fn("stageDesktopDmgBackground")(function* ( | ||
| 1786 | stageResourcesDir: string, | ||
| 1787 | channel: "latest" | "nightly", | ||
| 1788 | verbose: boolean, | ||
| 1789 | ) { | ||
| 1790 | const fs = yield* FileSystem.FileSystem; | ||
| 1791 | const path = yield* Path.Path; | ||
| 1792 | const sourcePath = path.join(stageResourcesDir, "dmg", `dmg-background-${channel}.svg`); | ||
| 1793 | if (!(yield* fs.exists(sourcePath))) { | ||
| 1794 | return yield* new DesktopDmgBackgroundSourceMissingError({ channel, sourcePath }); | ||
| 1795 | } | ||
| 1796 | |||
| 1797 | for (const output of [ | ||
| 1798 | { suffix: "", width: 540, height: 380 }, | ||
| 1799 | { suffix: "@2x", width: 1080, height: 760 }, | ||
| 1800 | ] as const) { | ||
| 1801 | const targetPath = path.join( | ||
| 1802 | stageResourcesDir, | ||
| 1803 | "dmg", | ||
| 1804 | `dmg-background-${channel}${output.suffix}.png`, | ||
| 1805 | ); | ||
| 1806 | yield* runCommand( | ||
| 1807 | ChildProcess.make( | ||
| 1808 | {}, | ||
| 1809 | )`sips -s format png -z ${output.height} ${output.width} ${sourcePath} --out ${targetPath}`, | ||
| 1810 | { | ||
| 1811 | label: `sips ${channel} DMG background${output.suffix || "@1x"}`, | ||
| 1812 | verbose, | ||
| 1813 | }, | ||
| 1814 | ); | ||
| 1815 | } | ||
| 1816 | }); | ||
| 1817 | |||
| 1773 | function stageLinuxIcons(stageResourcesDir: string, sourcePng: string, verbose: boolean) { | 1818 | function stageLinuxIcons(stageResourcesDir: string, sourcePng: string, verbose: boolean) { |
| 1774 | return Effect.gen(function* () { | 1819 | return Effect.gen(function* () { |
| 1775 | const fs = yield* FileSystem.FileSystem; | 1820 | const fs = yield* FileSystem.FileSystem; |
| ... | @@ -2039,6 +2084,29 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* ( | ... | @@ -2039,6 +2084,29 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* ( |
| 2039 | }; | 2084 | }; |
| 2040 | } | 2085 | } |
| 2041 | 2086 | ||
| 2087 | if (platform === "mac" && target === "dmg") { | ||
| 2088 | buildConfig.dmg = { | ||
| 2089 | // Give the themed installer its own Finder volume name. Finder caches | ||
| 2090 | // DMG window backgrounds by volume name, so reusing a generic name can | ||
| 2091 | // make a newly built background look unchanged during testing. | ||
| 2092 | title: `${resolveDesktopProductName(version)} ${version} Installer`, | ||
| 2093 | background: `dmg/dmg-background-${updateChannel}.png`, | ||
| 2094 | window: { | ||
| 2095 | width: 540, | ||
| 2096 | // Finder counts its 32px title bar in the window bounds. The themed | ||
| 2097 | // background itself is 380px tall, so add the chrome height here to | ||
| 2098 | // keep the full canvas visible. | ||
| 2099 | height: 412, | ||
| 2100 | }, | ||
| 2101 | contents: [ | ||
| 2102 | { x: 130, y: 220, type: "file" }, | ||
| 2103 | { x: 410, y: 220, type: "link", path: "/Applications" }, | ||
| 2104 | ], | ||
| 2105 | iconSize: 80, | ||
| 2106 | iconTextSize: 12, | ||
| 2107 | }; | ||
| 2108 | } | ||
| 2109 | |||
| 2042 | if (platform === "linux") { | 2110 | if (platform === "linux") { |
| 2043 | buildConfig.linux = { | 2111 | buildConfig.linux = { |
| 2044 | target: [target], | 2112 | target: [target], |
| ... | @@ -2738,6 +2806,13 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( | ... | @@ -2738,6 +2806,13 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( |
| 2738 | yield* Effect.log("[desktop-artifact] Staging release app..."); | 2806 | yield* Effect.log("[desktop-artifact] Staging release app..."); |
| 2739 | yield* fs.copy(distDirs.desktopDist, path.join(stageAppDir, "apps/desktop/dist-electron")); | 2807 | yield* fs.copy(distDirs.desktopDist, path.join(stageAppDir, "apps/desktop/dist-electron")); |
| 2740 | yield* fs.copy(distDirs.desktopResources, stageResourcesDir); | 2808 | yield* fs.copy(distDirs.desktopResources, stageResourcesDir); |
| 2809 | if (options.platform === "mac" && options.target === "dmg") { | ||
| 2810 | yield* stageDesktopDmgBackground( | ||
| 2811 | stageResourcesDir, | ||
| 2812 | resolveDesktopUpdateChannel(appVersion), | ||
| 2813 | options.verbose, | ||
| 2814 | ); | ||
| 2815 | } | ||
| 2741 | // On Windows the server tree ships in the server.asar sidecar instead of | 2816 | // On Windows the server tree ships in the server.asar sidecar instead of |
| 2742 | // app.asar (see stageWindowsServerSidecar), so the app stage omits it. | 2817 | // app.asar (see stageWindowsServerSidecar), so the app stage omits it. |
| 2743 | if (options.platform !== "win") { | 2818 | if (options.platform !== "win") { |