Some Linux Laptop/Desktop detection and goodbye penguin

This commit is contained in:
Ryuichi Leo Takashige
2026-03-10 17:03:56 +00:00
parent f376c24895
commit 7199e556ef
4 changed files with 351 additions and 182 deletions
+111 -86
View File
@@ -42,11 +42,10 @@
const NVIDIA_LOGO_PATH =
"M0.81 0.429V0.299c0.013 -0.001 0.026 -0.002 0.038 -0.002 0.355 -0.011 0.588 0.306 0.588 0.306S1.186 0.952 0.916 0.952c-0.036 0 -0.071 -0.006 -0.105 -0.017V0.542c0.138 0.017 0.166 0.078 0.249 0.216l0.185 -0.155s-0.135 -0.177 -0.362 -0.177c-0.024 -0.001 -0.048 0.001 -0.072 0.003m0 -0.429v0.194l0.038 -0.002c0.494 -0.017 0.816 0.405 0.816 0.405s-0.37 0.45 -0.754 0.45c-0.034 0 -0.066 -0.003 -0.099 -0.009v0.12c0.027 0.003 0.055 0.006 0.082 0.006 0.358 0 0.618 -0.183 0.869 -0.399 0.042 0.034 0.212 0.114 0.247 0.15 -0.238 0.2 -0.794 0.361 -1.11 0.361 -0.03 0 -0.059 -0.002 -0.088 -0.005v0.169h1.362V0zm0 0.935v0.102c-0.331 -0.059 -0.423 -0.404 -0.423 -0.404s0.159 -0.176 0.423 -0.205v0.112h-0.001C0.671 0.524 0.562 0.654 0.562 0.654s0.062 0.218 0.248 0.282m-0.588 -0.316s0.196 -0.29 0.589 -0.32V0.194C0.376 0.229 0 0.597 0 0.597s0.213 0.616 0.81 0.672v-0.112c-0.438 -0.054 -0.588 -0.538 -0.588 -0.538";
// Tux penguin paths (viewBox 0 0 100 100)
const TUX_BODY_PATH =
"M50 8c-8 0-14 6-14 13 0 4 2 8 5 10-8 4-16 14-16 28v12c0 4 2 7 5 9l-6 4c-2 1-3 3-3 5v3c0 2 2 4 4 4h10l4-4h22l4 4h10c2 0 4-2 4-4v-3c0-2-1-4-3-5l-6-4c3-2 5-5 5-9V59c0-14-8-24-16-28 3-2 5-6 5-10 0-7-6-13-14-13z";
const TUX_BELLY_PATH =
"M38 52c0-8 5-15 12-15s12 7 12 15v14c0 4-5 7-12 7s-12-3-12-7V52z";
// Tux penguin logo path (viewBox 0 0 100 100)
const TUX_LOGO_PATH =
"M50 5C42 5 36 11 36 18c0 4 2 8 5 10C33 32 25 42 25 56v12c0 4 2 7 5 9l-6 4c-2 1-3 3-3 5v3c0 2 2 4 4 4h10l4-4h22l4 4h10c2 0 4-2 4-4v-3c0-2-1-4-3-5l-6-4c3-2 5-5 5-9V56c0-14-8-24-16-28 3-2 5-6 5-10 0-7-6-13-14-13z";
const TUX_LOGO_VIEWBOX = "0 0 100 100";
const wireColor = "rgba(179,179,179,0.8)";
const strokeWidth = 1.5;
@@ -55,7 +54,8 @@
const isSpark = $derived(
modelLower.includes("dgx") || modelLower.includes("gx10"),
);
const isLinux = $derived(!isSpark && modelLower.includes("linux"));
const isLinux = $derived(!isSpark && modelLower.startsWith("linux"));
const isLinuxLaptop = $derived(isLinux && modelLower.includes("laptop"));
// ── DGX Spark dimensions ──
const dgxW = $derived(size * 1.55);
@@ -75,13 +75,11 @@
const dgxClipId = $derived(`di-dgx-${uid}`);
const dgxTextureId = $derived(`di-dgx-tex-${uid}`);
// ── Linux Tux dimensions ──
const tuxSize = $derived(Math.min(size * 1.0, size * 1.1));
const tuxScale = $derived(tuxSize / 100);
const tuxTx = $derived(cx - (100 * tuxScale) / 2);
const tuxTy = $derived(cy - (100 * tuxScale) / 2);
const tuxEyeR = $derived(2.5 * tuxScale);
const tuxBellyClipId = $derived(`di-tux-belly-${uid}`);
// ── Linux Desktop dimensions (reuses Mac Studio proportions) ──
const linuxDesktopClipId = $derived(`di-linux-desktop-${uid}`);
// ── Linux Laptop dimensions (reuses MacBook proportions) ──
const linuxScreenClipId = $derived(`di-linux-screen-${uid}`);
// ── Mac Studio dimensions (same ratios as TopologyGraph) ──
const studioW = $derived(size * 1.25);
@@ -285,98 +283,125 @@
font-weight="700">NVIDIA</text
>
</g>
{:else if isLinux}
<!-- Linux Tux Penguin -->
{:else if isLinuxLaptop}
<!-- Linux Laptop — MacBook shape with Tux logo -->
<defs>
<clipPath id={tuxBellyClipId}>
<path
d={TUX_BELLY_PATH}
transform="translate({tuxTx}, {tuxTy}) scale({tuxScale})"
<clipPath id={linuxScreenClipId}>
<rect
x={mbScreenX + mbBezel}
y={mbY + mbBezel}
width={mbScreenW - mbBezel * 2}
height={mbScreenH - mbBezel * 2}
rx="2"
/>
</clipPath>
</defs>
<!-- Body -->
<path
d={TUX_BODY_PATH}
transform="translate({tuxTx}, {tuxTy}) scale({tuxScale})"
<rect
x={mbScreenX}
y={mbY}
width={mbScreenW}
height={mbScreenH}
rx="3"
fill="#1a1a1a"
stroke={wireColor}
stroke-width={strokeWidth / tuxScale}
stroke-width={strokeWidth}
/>
<!-- White belly -->
<path
d={TUX_BELLY_PATH}
transform="translate({tuxTx}, {tuxTy}) scale({tuxScale})"
fill="rgba(220,220,220,0.85)"
<rect
x={mbScreenX + mbBezel}
y={mbY + mbBezel}
width={mbScreenW - mbBezel * 2}
height={mbScreenH - mbBezel * 2}
rx="2"
fill="#0a0a12"
/>
<!-- Memory fill on belly -->
{#if ramPercent > 0}
{@const bellyTop = tuxTy + 37 * tuxScale}
{@const bellyH = 36 * tuxScale}
{@const memH = (ramPercent / 100) * bellyH}
<rect
x={tuxTx + 38 * tuxScale}
y={bellyTop + bellyH - memH}
width={24 * tuxScale}
height={memH}
fill="rgba(255,215,0,0.75)"
clip-path="url(#{tuxBellyClipId})"
x={mbScreenX + mbBezel}
y={mbY + mbBezel + (mbMemTotalH - mbMemH)}
width={mbScreenW - mbBezel * 2}
height={mbMemH}
fill="rgba(255,215,0,0.85)"
clip-path="url(#{linuxScreenClipId})"
/>
{/if}
<!-- Eyes -->
<circle
cx={tuxTx + 44 * tuxScale}
cy={tuxTy + 16 * tuxScale}
r={tuxEyeR}
fill="white"
/>
<circle
cx={tuxTx + 56 * tuxScale}
cy={tuxTy + 16 * tuxScale}
r={tuxEyeR}
fill="white"
/>
<!-- Pupils -->
<circle
cx={tuxTx + 44 * tuxScale}
cy={tuxTy + 16 * tuxScale}
r={tuxEyeR * 0.5}
fill="#1a1a1a"
/>
<circle
cx={tuxTx + 56 * tuxScale}
cy={tuxTy + 16 * tuxScale}
r={tuxEyeR * 0.5}
fill="#1a1a1a"
/>
<!-- Tux logo on screen -->
{@const tuxH = mbScreenH * 0.35}
{@const tuxW = tuxH}
{@const tuxX = cx - tuxW / 2}
{@const tuxY = mbY + mbScreenH / 2 - tuxH / 2}
<svg x={tuxX} y={tuxY} width={tuxW} height={tuxH} viewBox={TUX_LOGO_VIEWBOX}>
<path d={TUX_LOGO_PATH} fill="#FFFFFF" opacity="0.9" />
</svg>
<!-- Beak -->
<path
d="M{tuxTx + 46 * tuxScale} {tuxTy + 22 * tuxScale} L{tuxTx +
50 * tuxScale} {tuxTy + 27 * tuxScale} L{tuxTx + 54 * tuxScale} {tuxTy +
22 * tuxScale} Z"
fill="#E8A317"
d="M {mbBaseTopX} {mbBaseY} L {mbBaseTopX +
mbBaseTopW} {mbBaseY} L {mbBaseBottomX + mbBaseBottomW} {mbBaseY +
mbBaseH} L {mbBaseBottomX} {mbBaseY + mbBaseH} Z"
fill="#2c2c2c"
stroke={wireColor}
stroke-width="1"
/>
<rect
x={mbKbX}
y={mbKbY}
width={mbKbW}
height={mbKbH}
fill="rgba(0,0,0,0.2)"
rx="2"
/>
<rect
x={mbTpX}
y={mbTpY}
width={mbTpW}
height={mbTpH}
fill="rgba(255,255,255,0.08)"
rx="2"
/>
{:else if isLinux}
<!-- Linux Desktop — Mac Studio shape with Tux logo -->
<defs>
<clipPath id={linuxDesktopClipId}>
<rect
x={studioX}
y={studioY + studioTopH}
width={studioW}
height={studioH - studioTopH}
rx={studioCorner - 1}
/>
</clipPath>
</defs>
<!-- Feet -->
<ellipse
cx={tuxTx + 42 * tuxScale}
cy={tuxTy + 94 * tuxScale}
rx={6 * tuxScale}
ry={2.5 * tuxScale}
fill="#E8A317"
/>
<ellipse
cx={tuxTx + 58 * tuxScale}
cy={tuxTy + 94 * tuxScale}
rx={6 * tuxScale}
ry={2.5 * tuxScale}
fill="#E8A317"
<rect
x={studioX}
y={studioY}
width={studioW}
height={studioH}
rx={studioCorner}
fill="#1a1a1a"
stroke={wireColor}
stroke-width={strokeWidth}
/>
{#if ramPercent > 0}
<rect
x={studioX}
y={studioY + studioTopH + (studioMemTotalH - studioMemH)}
width={studioW}
height={studioMemH}
fill="rgba(255,215,0,0.75)"
clip-path="url(#{linuxDesktopClipId})"
/>
{/if}
<!-- Tux logo centered -->
{@const dtH = (studioH - studioTopH) * 0.55}
{@const dtW = dtH}
{@const dtX = cx - dtW / 2}
{@const dtY = studioY + studioTopH + (studioH - studioTopH) / 2 - dtH / 2}
<svg x={dtX} y={dtY} width={dtW} height={dtH} viewBox={TUX_LOGO_VIEWBOX}>
<path d={TUX_LOGO_PATH} fill="rgba(255,255,255,0.6)" />
</svg>
{:else if modelLower === "mac studio" || modelLower === "mac mini"}
<!-- Mac Studio / Mac Mini -->
<defs>
+44 -8
View File
@@ -988,8 +988,20 @@
stroke-width="1.5"
/>
<!-- Side accents -->
<rect x="0" y="0" width={dgxW * 0.02} height={dgxH} fill="#8a7a56" />
<rect x={dgxW - dgxW * 0.02} y="0" width={dgxW * 0.02} height={dgxH} fill="#8a7a56" />
<rect
x="0"
y="0"
width={dgxW * 0.02}
height={dgxH}
fill="#8a7a56"
/>
<rect
x={dgxW - dgxW * 0.02}
y="0"
width={dgxW * 0.02}
height={dgxH}
fill="#8a7a56"
/>
<!-- Left handle -->
<rect
x={dgxW * 0.04}
@@ -1025,7 +1037,8 @@
x="2"
y={dgxH - dgxH * (node.newPercent / 100)}
width={dgxW - 4}
height={dgxH * ((node.newPercent - node.currentPercent) / 100)}
height={dgxH *
((node.newPercent - node.currentPercent) / 100)}
fill="#FFD700"
filter="url(#memGlow-{filterId})"
class="animate-pulse-slow"
@@ -1054,16 +1067,39 @@
<!-- Eyes -->
<circle cx={44 * sc} cy={16 * sc} r={2.5 * sc} fill="white" />
<circle cx={56 * sc} cy={16 * sc} r={2.5 * sc} fill="white" />
<circle cx={44 * sc} cy={16 * sc} r={1.2 * sc} fill="#1a1a1a" />
<circle cx={56 * sc} cy={16 * sc} r={1.2 * sc} fill="#1a1a1a" />
<circle
cx={44 * sc}
cy={16 * sc}
r={1.2 * sc}
fill="#1a1a1a"
/>
<circle
cx={56 * sc}
cy={16 * sc}
r={1.2 * sc}
fill="#1a1a1a"
/>
<!-- Beak -->
<path
d="M{46 * sc} {22 * sc} L{50 * sc} {27 * sc} L{54 * sc} {22 * sc} Z"
d="M{46 * sc} {22 * sc} L{50 * sc} {27 * sc} L{54 *
sc} {22 * sc} Z"
fill="#E8A317"
/>
<!-- Feet -->
<ellipse cx={42 * sc} cy={94 * sc} rx={6 * sc} ry={2.5 * sc} fill="#E8A317" />
<ellipse cx={58 * sc} cy={94 * sc} rx={6 * sc} ry={2.5 * sc} fill="#E8A317" />
<ellipse
cx={42 * sc}
cy={94 * sc}
rx={6 * sc}
ry={2.5 * sc}
fill="#E8A317"
/>
<ellipse
cx={58 * sc}
cy={94 * sc}
rx={6 * sc}
ry={2.5 * sc}
fill="#E8A317"
/>
</g>
{:else}
<!-- Unknown device - hexagon -->
+182 -87
View File
@@ -121,12 +121,11 @@
const NVIDIA_LOGO_PATH =
"M0.81 0.429V0.299c0.013 -0.001 0.026 -0.002 0.038 -0.002 0.355 -0.011 0.588 0.306 0.588 0.306S1.186 0.952 0.916 0.952c-0.036 0 -0.071 -0.006 -0.105 -0.017V0.542c0.138 0.017 0.166 0.078 0.249 0.216l0.185 -0.155s-0.135 -0.177 -0.362 -0.177c-0.024 -0.001 -0.048 0.001 -0.072 0.003m0 -0.429v0.194l0.038 -0.002c0.494 -0.017 0.816 0.405 0.816 0.405s-0.37 0.45 -0.754 0.45c-0.034 0 -0.066 -0.003 -0.099 -0.009v0.12c0.027 0.003 0.055 0.006 0.082 0.006 0.358 0 0.618 -0.183 0.869 -0.399 0.042 0.034 0.212 0.114 0.247 0.15 -0.238 0.2 -0.794 0.361 -1.11 0.361 -0.03 0 -0.059 -0.002 -0.088 -0.005v0.169h1.362V0zm0 0.935v0.102c-0.331 -0.059 -0.423 -0.404 -0.423 -0.404s0.159 -0.176 0.423 -0.205v0.112h-0.001C0.671 0.524 0.562 0.654 0.562 0.654s0.062 0.218 0.248 0.282m-0.588 -0.316s0.196 -0.29 0.589 -0.32V0.194C0.376 0.229 0 0.597 0 0.597s0.213 0.616 0.81 0.672v-0.112c-0.438 -0.054 -0.588 -0.538 -0.588 -0.538";
// Tux penguin SVG path — simplified silhouette for topology view
// viewBox: 0 0 100 100
const TUX_BODY_PATH =
"M50 8c-8 0-14 6-14 13 0 4 2 8 5 10-8 4-16 14-16 28v12c0 4 2 7 5 9l-6 4c-2 1-3 3-3 5v3c0 2 2 4 4 4h10l4-4h22l4 4h10c2 0 4-2 4-4v-3c0-2-1-4-3-5l-6-4c3-2 5-5 5-9V59c0-14-8-24-16-28 3-2 5-6 5-10 0-7-6-13-14-13z";
const TUX_BELLY_PATH =
"M38 52c0-8 5-15 12-15s12 7 12 15v14c0 4-5 7-12 7s-12-3-12-7V52z";
// Tux penguin logo — simplified silhouette suitable as a logo overlay
// viewBox: 0 0 100 120
const TUX_LOGO_PATH =
"M50 5C42 5 36 11 36 18c0 4 2 8 5 10C33 32 25 42 25 56v12c0 4 2 7 5 9l-6 4c-2 1-3 3-3 5v3c0 2 2 4 4 4h10l4-4h22l4 4h10c2 0 4-2 4-4v-3c0-2-1-4-3-5l-6-4c3-2 5-5 5-9V56c0-14-8-24-16-28 3-2 5-6 5-10 0-7-6-13-14-13z";
const TUX_LOGO_VIEWBOX = "0 0 100 100";
function formatBytes(bytes: number, decimals = 1): string {
if (!bytes || bytes === 0) return "0B";
@@ -567,10 +566,11 @@
const modelLower = modelId.toLowerCase();
const identity = identitiesData[nodeInfo.id];
const nameLower = (friendlyName || "").toLowerCase();
const isSpark =
modelLower.includes("dgx") || modelLower.includes("gx10");
const isSpark = modelLower.includes("dgx") || modelLower.includes("gx10");
const isLinux =
!isSpark && (modelLower === "linux" || identity?.osVersion === "Linux");
!isSpark &&
(modelLower.startsWith("linux") || identity?.osVersion === "Linux");
const isLinuxLaptop = isLinux && modelLower.includes("laptop");
// Check node states for styling
const isHighlighted = highlightedNodes.has(nodeInfo.id);
@@ -826,107 +826,202 @@
.attr("font-family", "monospace")
.attr("font-weight", "700")
.text("NVIDIA");
} else if (isLinux) {
// Linux — Tux penguin
iconBaseWidth = nodeRadius * 1.0;
iconBaseHeight = nodeRadius * 1.1;
const tuxSize = Math.min(iconBaseWidth, iconBaseHeight);
const scale = tuxSize / 100;
const tx = nodeInfo.x - (100 * scale) / 2;
const ty = nodeInfo.y - (100 * scale) / 2;
} else if (isLinuxLaptop) {
// Linux Laptop — same shape as MacBook but with Tux logo
iconBaseWidth = nodeRadius * 1.6;
iconBaseHeight = nodeRadius * 1.15;
const x = nodeInfo.x - iconBaseWidth / 2;
const y = nodeInfo.y - iconBaseHeight / 2;
// Body (dark outline)
const screenHeight = iconBaseHeight * 0.7;
const baseHeight = iconBaseHeight * 0.3;
const screenWidth = iconBaseWidth * 0.85;
const screenX = nodeInfo.x - screenWidth / 2;
const screenBezel = 3;
const linuxScreenClipId = `linux-screen-${nodeInfo.id.replace(/[^a-zA-Z0-9]/g, "-")}`;
defs
.append("clipPath")
.attr("id", linuxScreenClipId)
.append("rect")
.attr("x", screenX + screenBezel)
.attr("y", y + screenBezel)
.attr("width", screenWidth - screenBezel * 2)
.attr("height", screenHeight - screenBezel * 2)
.attr("rx", 2);
// Screen outer frame
nodeG
.append("path")
.append("rect")
.attr("class", "node-outline")
.attr("d", TUX_BODY_PATH)
.attr("transform", `translate(${tx}, ${ty}) scale(${scale})`)
.attr("x", screenX)
.attr("y", y)
.attr("width", screenWidth)
.attr("height", screenHeight)
.attr("rx", 3)
.attr("fill", "#1a1a1a")
.attr("stroke", wireColor)
.attr("stroke-width", strokeWidth / scale);
.attr("stroke-width", strokeWidth);
// White belly
// Screen inner
nodeG
.append("path")
.attr("d", TUX_BELLY_PATH)
.attr("transform", `translate(${tx}, ${ty}) scale(${scale})`)
.attr("fill", "rgba(220,220,220,0.85)");
.append("rect")
.attr("x", screenX + screenBezel)
.attr("y", y + screenBezel)
.attr("width", screenWidth - screenBezel * 2)
.attr("height", screenHeight - screenBezel * 2)
.attr("rx", 2)
.attr("fill", "#0a0a12");
// Memory fill on belly (from bottom up)
// Memory fill on screen
if (ramUsagePercent > 0) {
const bellyClipId = `tux-belly-${nodeInfo.id.replace(/[^a-zA-Z0-9]/g, "-")}`;
defs
.append("clipPath")
.attr("id", bellyClipId)
.append("path")
.attr("d", TUX_BELLY_PATH)
.attr("transform", `translate(${tx}, ${ty}) scale(${scale})`);
const bellyTop = ty + 37 * scale;
const bellyHeight = 36 * scale;
const memHeight = (ramUsagePercent / 100) * bellyHeight;
const memFillTotalHeight = screenHeight - screenBezel * 2;
const memFillActualHeight =
(ramUsagePercent / 100) * memFillTotalHeight;
nodeG
.append("rect")
.attr("x", tx + 38 * scale)
.attr("y", bellyTop + bellyHeight - memHeight)
.attr("width", 24 * scale)
.attr("height", memHeight)
.attr("fill", "rgba(255,215,0,0.75)")
.attr("clip-path", `url(#${bellyClipId})`);
.attr("x", screenX + screenBezel)
.attr(
"y",
y + screenBezel + (memFillTotalHeight - memFillActualHeight),
)
.attr("width", screenWidth - screenBezel * 2)
.attr("height", memFillActualHeight)
.attr("fill", "rgba(255,215,0,0.85)")
.attr("clip-path", `url(#${linuxScreenClipId})`);
}
// Eyes
const eyeRadius = 2.5 * scale;
// Tux logo on screen
const tuxLogoH = screenHeight * 0.35;
const tuxLogoW = tuxLogoH;
const tuxLogoX = nodeInfo.x - tuxLogoW / 2;
const tuxLogoY = y + screenHeight / 2 - tuxLogoH / 2;
nodeG
.append("circle")
.attr("cx", tx + 44 * scale)
.attr("cy", ty + 16 * scale)
.attr("r", eyeRadius)
.attr("fill", "white");
nodeG
.append("circle")
.attr("cx", tx + 56 * scale)
.attr("cy", ty + 16 * scale)
.attr("r", eyeRadius)
.attr("fill", "white");
// Pupils
nodeG
.append("circle")
.attr("cx", tx + 44 * scale)
.attr("cy", ty + 16 * scale)
.attr("r", eyeRadius * 0.5)
.attr("fill", "#1a1a1a");
nodeG
.append("circle")
.attr("cx", tx + 56 * scale)
.attr("cy", ty + 16 * scale)
.attr("r", eyeRadius * 0.5)
.attr("fill", "#1a1a1a");
.append("svg")
.attr("x", tuxLogoX)
.attr("y", tuxLogoY)
.attr("width", tuxLogoW)
.attr("height", tuxLogoH)
.attr("viewBox", TUX_LOGO_VIEWBOX)
.append("path")
.attr("d", TUX_LOGO_PATH)
.attr("fill", "#FFFFFF")
.attr("opacity", 0.9);
// Keyboard base (trapezoidal)
const baseY = y + screenHeight;
const baseTopWidth = screenWidth;
const baseBottomWidth = iconBaseWidth;
const baseTopX = nodeInfo.x - baseTopWidth / 2;
const baseBottomX = nodeInfo.x - baseBottomWidth / 2;
// Beak
nodeG
.append("path")
.attr(
"d",
`M${tx + 46 * scale} ${ty + 22 * scale} L${tx + 50 * scale} ${ty + 27 * scale} L${tx + 54 * scale} ${ty + 22 * scale} Z`,
`M ${baseTopX} ${baseY} L ${baseTopX + baseTopWidth} ${baseY} L ${baseBottomX + baseBottomWidth} ${baseY + baseHeight} L ${baseBottomX} ${baseY + baseHeight} Z`,
)
.attr("fill", "#E8A317");
.attr("fill", "#2c2c2c")
.attr("stroke", wireColor)
.attr("stroke-width", 1);
// Feet
// Keyboard area
const keyboardX = baseTopX + 6;
const keyboardY = baseY + 3;
const keyboardWidth = baseTopWidth - 12;
const keyboardHeight = baseHeight * 0.55;
nodeG
.append("ellipse")
.attr("cx", tx + 42 * scale)
.attr("cy", ty + 94 * scale)
.attr("rx", 6 * scale)
.attr("ry", 2.5 * scale)
.attr("fill", "#E8A317");
.append("rect")
.attr("x", keyboardX)
.attr("y", keyboardY)
.attr("width", keyboardWidth)
.attr("height", keyboardHeight)
.attr("fill", "rgba(0,0,0,0.2)")
.attr("rx", 2);
// Trackpad
const trackpadWidth = baseTopWidth * 0.4;
const trackpadX = nodeInfo.x - trackpadWidth / 2;
const trackpadY = baseY + keyboardHeight + 5;
const trackpadHeight = baseHeight * 0.3;
nodeG
.append("ellipse")
.attr("cx", tx + 58 * scale)
.attr("cy", ty + 94 * scale)
.attr("rx", 6 * scale)
.attr("ry", 2.5 * scale)
.attr("fill", "#E8A317");
.append("rect")
.attr("x", trackpadX)
.attr("y", trackpadY)
.attr("width", trackpadWidth)
.attr("height", trackpadHeight)
.attr("fill", "rgba(255,255,255,0.08)")
.attr("rx", 2);
} else if (isLinux) {
// Linux Desktop — same shape as Mac Studio but with Tux logo
iconBaseWidth = nodeRadius * 1.25;
iconBaseHeight = nodeRadius * 0.85;
const x = nodeInfo.x - iconBaseWidth / 2;
const y = nodeInfo.y - iconBaseHeight / 2;
const cornerRadius = 4;
const topSurfaceHeight = iconBaseHeight * 0.15;
const linuxDesktopClipId = `linux-desktop-${nodeInfo.id.replace(/[^a-zA-Z0-9]/g, "-")}`;
defs
.append("clipPath")
.attr("id", linuxDesktopClipId)
.append("rect")
.attr("x", x)
.attr("y", y + topSurfaceHeight)
.attr("width", iconBaseWidth)
.attr("height", iconBaseHeight - topSurfaceHeight)
.attr("rx", cornerRadius - 1);
// Main body
nodeG
.append("rect")
.attr("class", "node-outline")
.attr("x", x)
.attr("y", y)
.attr("width", iconBaseWidth)
.attr("height", iconBaseHeight)
.attr("rx", cornerRadius)
.attr("fill", "#1a1a1a")
.attr("stroke", wireColor)
.attr("stroke-width", strokeWidth);
// Memory fill
if (ramUsagePercent > 0) {
const memFillTotalHeight = iconBaseHeight - topSurfaceHeight;
const memFillActualHeight =
(ramUsagePercent / 100) * memFillTotalHeight;
nodeG
.append("rect")
.attr("x", x)
.attr(
"y",
y + topSurfaceHeight + (memFillTotalHeight - memFillActualHeight),
)
.attr("width", iconBaseWidth)
.attr("height", memFillActualHeight)
.attr("fill", "rgba(255,215,0,0.75)")
.attr("clip-path", `url(#${linuxDesktopClipId})`);
}
// Tux logo centered on front face
const tuxLogoH = (iconBaseHeight - topSurfaceHeight) * 0.55;
const tuxLogoW = tuxLogoH;
const tuxLogoX = nodeInfo.x - tuxLogoW / 2;
const tuxLogoY =
y +
topSurfaceHeight +
(iconBaseHeight - topSurfaceHeight) / 2 -
tuxLogoH / 2;
nodeG
.append("svg")
.attr("x", tuxLogoX)
.attr("y", tuxLogoY)
.attr("width", tuxLogoW)
.attr("height", tuxLogoH)
.attr("viewBox", TUX_LOGO_VIEWBOX)
.append("path")
.attr("d", TUX_LOGO_PATH)
.attr("fill", "rgba(255,255,255,0.6)");
} else if (modelLower === "mac studio") {
// Mac Studio - classic cube with memory fill
iconBaseWidth = nodeRadius * 1.25;
+14 -1
View File
@@ -170,7 +170,20 @@ async def _get_linux_model_and_chip() -> tuple[str, str]:
pass
return (model, chip)
# Generic Linux — use /proc/cpuinfo for chip
# Generic Linux — detect laptop vs desktop via chassis_type
# SMBIOS chassis types: 8,9,10,14,31,32 = portable/laptop
chassis_type = _read_dmi_field("chassis_type")
laptop_chassis_types = {"8", "9", "10", "14", "31", "32"}
if chassis_type in laptop_chassis_types:
model = "Linux Laptop"
elif chassis_type is not None:
model = "Linux Desktop"
# Also check for battery as a fallback laptop indicator
if model == "Linux" and Path("/sys/class/power_supply/BAT0").exists():
model = "Linux Laptop"
# Use /proc/cpuinfo for chip
cpuinfo_path = Path("/proc/cpuinfo")
if cpuinfo_path.exists():
try: