Skip to content
Closed
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
6 changes: 6 additions & 0 deletions apps/desktop/electron-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const productName = pkg.productName;
const macIconPath = join(pkg.resources, "build/icons/icon.icns");
const linuxIconPath = join(pkg.resources, "build/icons");
const winIconPath = join(pkg.resources, "build/icons/icon.ico");
const dmgBackgroundPath = join(pkg.resources, "build/installer/background.tiff");

const config: Configuration = {
appId: "com.superset.desktop",
Expand Down Expand Up @@ -86,6 +87,11 @@ const config: Configuration = {
// Rebuild native modules for Electron's Node.js version
npmRebuild: true,

// macOS DMG installer
dmg: {
...(existsSync(dmgBackgroundPath) ? { background: dmgBackgroundPath } : {}),
},

// macOS
mac: {
...(existsSync(macIconPath) ? { icon: macIconPath } : {}),
Expand Down
273 changes: 29 additions & 244 deletions apps/desktop/src/main/lib/dock-icon.ts
Original file line number Diff line number Diff line change
@@ -1,239 +1,57 @@
import { existsSync } from "node:fs";
import { join } from "node:path";
import { app, nativeImage } from "electron";
import { env } from "main/env.main";
import { getWorkspaceName } from "shared/env.shared";
import twColors from "tailwindcss/colors";
import { prerelease } from "semver";

/**
* Deterministic hash of a string, returned as a non-negative integer.
* Returns true if this is a canary (prerelease) build, e.g. "0.0.53-canary".
*/
function hashString(seed: string): number {
let hash = 0;
for (let i = 0; i < seed.length; i++) {
hash = seed.charCodeAt(i) + ((hash << 5) - hash);
hash |= 0;
}
return Math.abs(hash);
}

/**
* Parses an OKLCH CSS string like "oklch(63.7% 0.237 25.331)".
*/
function parseOklch(str: string): { l: number; c: number; h: number } | null {
const match = str.match(/oklch\(([\d.]+)%\s+([\d.]+)\s+([\d.]+)\)/);
if (!match) return null;
return {
l: Number(match[1]) / 100,
c: Number(match[2]),
h: Number(match[3]),
};
}

/**
* Converts OKLCH to sRGB (all values 0-255).
*/
function oklchToRgb(l: number, c: number, h: number): [number, number, number] {
const hRad = (h * Math.PI) / 180;
const a = c * Math.cos(hRad);
const b = c * Math.sin(hRad);

// OKLab → LMS (cube-root space)
const l_ = l + 0.3963377774 * a + 0.2158037573 * b;
const m_ = l - 0.1055613458 * a - 0.0638541728 * b;
const s_ = l - 0.0894841775 * a - 1.291485548 * b;

const lc = l_ * l_ * l_;
const mc = m_ * m_ * m_;
const sc = s_ * s_ * s_;

// LMS → linear sRGB
const rLin = +4.0767416621 * lc - 3.3077115913 * mc + 0.2309699292 * sc;
const gLin = -1.2684380046 * lc + 2.6097574011 * mc - 0.3413193965 * sc;
const bLin = -0.0041960863 * lc + 0.7034186147 * mc + 0.2967775076 * sc;

const toSrgb = (v: number) => {
const clamped = Math.max(0, Math.min(1, v));
return clamped <= 0.0031308
? 12.92 * clamped
: 1.055 * clamped ** (1 / 2.4) - 0.055;
};

return [
Math.round(toSrgb(rLin) * 255),
Math.round(toSrgb(gLin) * 255),
Math.round(toSrgb(bLin) * 255),
];
function isCanaryBuild(): boolean {
const components = prerelease(app.getVersion());
return components !== null && components.length > 0;
}
Comment on lines +10 to 13
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 isCanaryBuild matches all prerelease channels

semver.prerelease() returns a truthy array for any prerelease tag (e.g. alpha, beta, rc, not just canary). If the project ever ships an alpha or rc build using a version string like 0.0.53-beta.1, this function will return true and the canary dock icon will be shown for those builds too.

A tighter check that matches the intent documented in the JSDoc:

Suggested change
function isCanaryBuild(): boolean {
const components = prerelease(app.getVersion());
return components !== null && components.length > 0;
}
function isCanaryBuild(): boolean {
const components = prerelease(app.getVersion());
return components !== null && components.some((c) => String(c).toLowerCase() === "canary");
}


/** All Tailwind 500-level colors as RGB tuples. */
const TAILWIND_500_COLORS: [number, number, number][] = (() => {
const skip = new Set(["inherit", "current", "transparent", "black", "white"]);
const result: [number, number, number][] = [];
for (const [name, val] of Object.entries(twColors)) {
if (skip.has(name) || typeof val !== "object" || !("500" in val)) continue;
const parsed = parseOklch((val as Record<string, string>)["500"] as string);
if (parsed) result.push(oklchToRgb(parsed.l, parsed.c, parsed.h));
}
return result;
})();

/**
* Gets the path to the app icon PNG.
* Returns the icons directory path.
*/
function getIconPath(): string {
function getIconsDir(): string {
if (app.isPackaged) {
return join(
process.resourcesPath,
"app.asar/resources/build/icons/icon.png",
);
return join(process.resourcesPath, "app.asar/resources/build/icons");
}

if (env.NODE_ENV === "development") {
return join(app.getAppPath(), "src/resources/build/icons/icon.png");
return join(app.getAppPath(), "src/resources/build/icons");
}

return join(__dirname, "../resources/build/icons/icon.png");
return join(__dirname, "../resources/build/icons");
}
Comment on lines 15 to 26
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify the directory structure to ensure the fallback path is valid.
# Expected: The dist folder should have a structure where resources/build/icons
# is accessible via ../resources/build/icons from the lib folder.

# Check the typical dist structure for Electron main process files
fd -t f "dock-icon" --extension ts --extension js -x dirname {} \;

# Check if resources structure exists in expected locations
tree -L 4 -d apps/desktop/src/resources 2>/dev/null || echo "src/resources not found"
tree -L 4 -d apps/desktop/dist/resources 2>/dev/null || echo "dist/resources not found (may not be built)"

Repository: superset-sh/superset

Length of output: 336


🏁 Script executed:

#!/bin/bash
# Check build configuration files in apps/desktop
find apps/desktop -maxdepth 2 -type f \( -name "package.json" -o -name "tsconfig*.json" -o -name "vite.config.*" -o -name "webpack.config.*" -o -name "*.config.js" \)

Repository: superset-sh/superset

Length of output: 116


🏁 Script executed:

#!/bin/bash
# Check the package.json build scripts
cat apps/desktop/package.json | head -50

Repository: superset-sh/superset

Length of output: 2336


🏁 Script executed:

#!/bin/bash
# Check tsconfig.json for output directory
cat apps/desktop/tsconfig.json

Repository: superset-sh/superset

Length of output: 471


🏁 Script executed:

#!/bin/bash
# Check electron-vite configuration
find apps/desktop -name "electron.vite.config.*" -o -name "vite.config.*" | head -5

Repository: superset-sh/superset

Length of output: 100


🏁 Script executed:

#!/bin/bash
# Look at electron-vite config if it exists
cat apps/desktop/electron.vite.config.ts 2>/dev/null | head -100

Repository: superset-sh/superset

Length of output: 3434


🏁 Script executed:

#!/bin/bash
# Check if there's a script that copies resources to dist
grep -r "resources" apps/desktop/scripts --include="*.ts" --include="*.js" 2>/dev/null | head -20

Repository: superset-sh/superset

Length of output: 173


🏁 Script executed:

#!/bin/bash
# Check the vite helpers to see what copyResourcesPlugin does
cat apps/desktop/vite/helpers.ts 2>/dev/null || cat apps/desktop/vite/helpers.js 2>/dev/null || find apps/desktop/vite -name "*.ts" -o -name "*.js" | xargs ls -la

Repository: superset-sh/superset

Length of output: 2607


🏁 Script executed:

#!/bin/bash
# Also check what the resources variable is used for
cat apps/desktop/vite/helpers/copyResourcesPlugin.ts 2>/dev/null || find apps/desktop -name "*copyResources*"

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Verify that build/icons is indeed not in the copy list by checking the entire flow
cat apps/desktop/vite/helpers.ts | grep -A 50 "RESOURCES_TO_COPY"

Repository: superset-sh/superset

Length of output: 1943


🏁 Script executed:

#!/bin/bash
# Check if there's any other mechanism that copies build/ or icons
grep -r "build/icons" apps/desktop --include="*.ts" --include="*.js" --exclude-dir=node_modules | head -20

Repository: superset-sh/superset

Length of output: 1012


🏁 Script executed:

#!/bin/bash
# Check the electron-builder config to see if icons are handled separately
find apps/desktop -name "electron-builder*" -type f | head -5

Repository: superset-sh/superset

Length of output: 136


🏁 Script executed:

#!/bin/bash
# Check the electron-builder config to see how resources are handled
cat apps/desktop/electron-builder.ts 2>/dev/null | head -150

Repository: superset-sh/superset

Length of output: 4622


🏁 Script executed:

#!/bin/bash
# Check specifically how pkg.resources is used
cat apps/desktop/electron-builder.ts | grep -B 5 -A 5 "pkg.resources"

Repository: superset-sh/superset

Length of output: 1474


The fallback path will fail in non-packaged, non-development scenarios.

The third condition (line 25) is triggered when the app is not packaged and not in development mode—such as when running the built output in preview mode. In this case, __dirname resolves to dist/main/lib, and the path ../resources/build/icons becomes dist/resources/build/icons.

However, the build configuration only copies specific resources to dist via copyResourcesPlugin (sounds, tray, browser-extension, migrations, host-migrations, templates), not the build/icons directory. The icons directory only exists in src/resources and is copied to the package by electron-builder during the final packaging step. This means the fallback path will fail when trying to access icons.

Consider using app.getAppPath() for the fallback, which should work consistently across packaging modes, or add build/icons to the resources copied in copyResourcesPlugin.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/lib/dock-icon.ts` around lines 15 - 26, getIconsDir
currently falls back to a path built from __dirname which fails in non-packaged,
non-development runs because build/icons isn’t copied into dist; update
getIconsDir to use app.getAppPath() for the fallback (instead of join(__dirname,
"../resources/build/icons")) so the path resolves the same in
preview/dev/packaged modes (reference function getIconsDir, app.isPackaged,
env.NODE_ENV, and __dirname), or alternatively ensure copyResourcesPlugin copies
src/resources/build/icons into dist so the existing __dirname-based fallback
remains valid.


/**
* Signed distance function for a rounded rectangle.
* Negative = inside, positive = outside, zero = on boundary.
* Resolves the dock icon path for the current build type.
* Resolution order (first existing file wins):
* - dev: icon-dev.png → icon.png
* - canary: icon-canary.png → icon.png
* - stable: icon.png
*/
function sdfRoundedRect(
px: number,
py: number,
left: number,
top: number,
right: number,
bottom: number,
radius: number,
): number {
const cx = (left + right) / 2;
const cy = (top + bottom) / 2;
const halfW = (right - left) / 2;
const halfH = (bottom - top) / 2;

const dx = Math.abs(px - cx) - halfW + radius;
const dy = Math.abs(py - cy) - halfH + radius;

return (
Math.sqrt(Math.max(dx, 0) ** 2 + Math.max(dy, 0) ** 2) +
Math.min(Math.max(dx, dy), 0) -
radius
);
}

/**
* Finds the bounding box of non-transparent pixels in a bitmap.
*/
function findContentBounds(
bitmap: Buffer,
width: number,
height: number,
): { top: number; left: number; bottom: number; right: number } {
let top = height;
let left = width;
let bottom = 0;
let right = 0;
function getIconPath(): string {
const dir = getIconsDir();

for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if ((bitmap[(y * width + x) * 4 + 3] ?? 0) > 10) {
if (y < top) top = y;
if (y > bottom) bottom = y;
if (x < left) left = x;
if (x > right) right = x;
}
}
if (env.NODE_ENV === "development") {
const devIcon = join(dir, "icon-dev.png");
if (existsSync(devIcon)) return devIcon;
} else if (isCanaryBuild()) {
const canaryIcon = join(dir, "icon-canary.png");
if (existsSync(canaryIcon)) return canaryIcon;
}

return { top, left, bottom, right };
return join(dir, "icon.png");
}

/**
* Draws a rounded-rectangle border on raw RGBA bitmap data.
* The border is drawn inward from the specified bounds.
*/
function drawBorder({
bitmap,
width,
thickness,
left,
top,
right,
bottom,
cornerRadius,
rgb,
}: {
bitmap: Buffer;
width: number;
thickness: number;
left: number;
top: number;
right: number;
bottom: number;
cornerRadius: number;
rgb: [number, number, number];
}) {
const innerRadius = Math.max(0, cornerRadius - thickness);

for (let y = top; y <= bottom; y++) {
for (let x = left; x <= right; x++) {
const outerDist = sdfRoundedRect(
x,
y,
left,
top,
right,
bottom,
cornerRadius,
);
const innerDist = sdfRoundedRect(
x,
y,
left + thickness,
top + thickness,
right - thickness,
bottom - thickness,
innerRadius,
);

// Anti-aliased edges
const outerAlpha = Math.max(0, Math.min(1, 0.5 - outerDist));
const innerAlpha = Math.max(0, Math.min(1, innerDist + 0.5));
const borderAlpha = outerAlpha * innerAlpha;

if (borderAlpha > 0.001) {
const offset = (y * width + x) * 4;
const r = bitmap[offset] ?? 0;
const g = bitmap[offset + 1] ?? 0;
const b = bitmap[offset + 2] ?? 0;
const a = bitmap[offset + 3] ?? 0;
bitmap[offset] = Math.round(
rgb[0] * borderAlpha + r * (1 - borderAlpha),
);
bitmap[offset + 1] = Math.round(
rgb[1] * borderAlpha + g * (1 - borderAlpha),
);
bitmap[offset + 2] = Math.round(
rgb[2] * borderAlpha + b * (1 - borderAlpha),
);
bitmap[offset + 3] = Math.max(a, Math.round(borderAlpha * 255));
}
}
}
}

/**
* Sets the macOS dock icon with a colored border based on the workspace name.
* No-op on non-macOS platforms or when no workspace name is set.
* Sets the macOS dock icon based on the current build type and macOS version.
* No-op on non-macOS platforms.
*/
export function setWorkspaceDockIcon(): void {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Stale exported function name

The function was previously responsible for setting a workspace-coloured border on the dock icon, but that concept has been fully removed. The new behaviour is simply "set the appropriate dock icon for this build type." The exported name setWorkspaceDockIcon will confuse future callers (and any documentation) — consider renaming it to setDockIcon or setBuildTypeDockIcon.

Suggested change
export function setWorkspaceDockIcon(): void {
export function setDockIcon(): void {

if (process.platform !== "darwin") return;
if (env.NODE_ENV !== "development") return;

const workspaceName = getWorkspaceName();
if (!workspaceName) return;

try {
const iconPath = getIconPath();
Expand All @@ -243,41 +61,8 @@ export function setWorkspaceDockIcon(): void {
return;
}

const size = icon.getSize();
const bitmap = icon.toBitmap();

const hash = hashString(workspaceName);
const rgb =
TAILWIND_500_COLORS[hash % TAILWIND_500_COLORS.length] ??
([59, 130, 246] as [number, number, number]); // blue-500 fallback

// Find the actual icon content area (skip transparent padding)
const bounds = findContentBounds(bitmap, size.width, size.height);
const thickness = Math.round(size.width * 0.038);
const cornerRadius = Math.round(size.width * 0.22);

// Draw border flush with the content edges, overlapping inward
drawBorder({
bitmap,
width: size.width,
thickness,
left: bounds.left,
top: bounds.top,
right: bounds.right,
bottom: bounds.bottom,
cornerRadius,
rgb,
});

const newIcon = nativeImage.createFromBitmap(bitmap, {
width: size.width,
height: size.height,
});

app.dock?.setIcon(newIcon);
console.log(
`[dock-icon] Set workspace dock icon border rgb(${rgb.join(",")}) for "${workspaceName}"`,
);
app.dock?.setIcon(icon);
console.log(`[dock-icon] Set dock icon from: ${iconPath}`);
} catch (error) {
console.error("[dock-icon] Failed to set dock icon:", error);
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/desktop/src/resources/build/icons/icon-canary.icns
Binary file not shown.
Binary file modified apps/desktop/src/resources/build/icons/icon-canary.ico
Binary file not shown.
Binary file modified apps/desktop/src/resources/build/icons/icon-canary.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/desktop/src/resources/build/icons/icon.icns
Binary file not shown.
Binary file modified apps/desktop/src/resources/build/icons/icon.ico
Binary file not shown.
Binary file modified apps/desktop/src/resources/build/icons/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file modified apps/desktop/src/resources/tray/iconTemplate.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/marketing/public/favicon-192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading