From 10e34dbc6f523a543790ea32f2f546573869e23c Mon Sep 17 00:00:00 2001 From: tianma-if Date: Sun, 20 Sep 2026 22:46:06 +0800 Subject: [PATCH] fix(settings): show Mac product name and physical screen resolution --- .../desktop/src/main/desktop-device-model.mjs | 34 +++++++++++++++++- .../src/main/desktop-device-model.test.mjs | 36 ++++++++++++++++++- .../settings/SystemInfoPanel.test.mjs | 1 + apps/web/src/lib/system-diagnostics.ts | 6 ++-- .../shared/src/client-display-size.test.mjs | 25 ++++++++++++- packages/shared/src/client-display-size.ts | 15 ++++++++ 6 files changed, 111 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/main/desktop-device-model.mjs b/apps/desktop/src/main/desktop-device-model.mjs index 42160745..3017a9eb 100644 --- a/apps/desktop/src/main/desktop-device-model.mjs +++ b/apps/desktop/src/main/desktop-device-model.mjs @@ -6,6 +6,36 @@ const trimModel = (value) => { return text || null; }; +const parseIoregQuotedProperty = (output, key) => { + const match = String(output ?? "").match(new RegExp(`"${key}"\\s*=\\s*(?:<"([^"]+)">|"([^"]+)")`)); + return trimModel(match?.[1] || match?.[2]); +}; + +const readDarwinMarketingName = (execFile) => { + try { + const productTree = execFile("ioreg", ["-p", "IODeviceTree", "-n", "product", "-rd1"], { + encoding: "utf8", + timeout: 2_000, + }); + const productName = parseIoregQuotedProperty(productTree, "product-name"); + if (productName) return productName; + } catch { + // Older Macs may not expose the device-tree product node. + } + try { + const profile = JSON.parse(execFile("system_profiler", ["SPHardwareDataType", "-json"], { + encoding: "utf8", + timeout: 5_000, + })); + const hardware = Array.isArray(profile?.SPHardwareDataType) ? profile.SPHardwareDataType[0] : null; + const machineName = trimModel(hardware?.machine_name); + if (machineName) return machineName; + } catch { + // system_profiler is a compatibility fallback for machines without IOKit product-name. + } + return null; +}; + export const readDesktopDeviceModel = ({ execFileSync: execFile = execFileSync, platform = process.platform, @@ -13,7 +43,9 @@ export const readDesktopDeviceModel = ({ } = {}) => { try { if (platform === "darwin") { - return trimModel(execFile("sysctl", ["-n", "hw.model"], { encoding: "utf8", timeout: 2_000 })); + // hw.model is a board identifier (Mac16,11); product-name is the user-facing model. + return readDarwinMarketingName(execFile) + || trimModel(execFile("sysctl", ["-n", "hw.model"], { encoding: "utf8", timeout: 2_000 })); } if (platform === "win32") { return trimModel(execFile("powershell.exe", [ diff --git a/apps/desktop/src/main/desktop-device-model.test.mjs b/apps/desktop/src/main/desktop-device-model.test.mjs index 151032c8..aa51a8a7 100644 --- a/apps/desktop/src/main/desktop-device-model.test.mjs +++ b/apps/desktop/src/main/desktop-device-model.test.mjs @@ -2,9 +2,43 @@ import { describe, expect, test } from "bun:test"; import { readDesktopDeviceModel } from "./desktop-device-model.mjs"; describe("desktop device model", () => { - test("reads the macOS hardware model identifier", () => { + test("prefers the macOS IOKit product name over the board identifier", () => { expect(readDesktopDeviceModel({ execFileSync: (command, args) => { + expect(command).toBe("ioreg"); + expect(args).toEqual(["-p", "IODeviceTree", "-n", "product", "-rd1"]); + return ` ++-o product + { + "product-description" = <"Mac mini (2024)"> + "product-name" = <"Mac mini (2024)"> + "sub-product-type" = <"Mac16,11"> + } +`; + }, + platform: "darwin", + })).toBe("Mac mini (2024)"); + }); + + test("falls back to system_profiler machine name when IOKit product-name is missing", () => { + expect(readDesktopDeviceModel({ + execFileSync: (command, args) => { + if (command === "ioreg") return `"model" = <"Mac16,7">\n`; + expect(command).toBe("system_profiler"); + expect(args).toEqual(["SPHardwareDataType", "-json"]); + return JSON.stringify({ + SPHardwareDataType: [{ machine_model: "Mac16,7", machine_name: "MacBook Pro" }], + }); + }, + platform: "darwin", + })).toBe("MacBook Pro"); + }); + + test("falls back to the macOS hardware model identifier", () => { + expect(readDesktopDeviceModel({ + execFileSync: (command, args) => { + if (command === "ioreg") throw new Error("ioreg unavailable"); + if (command === "system_profiler") throw new Error("system_profiler unavailable"); expect(command).toBe("sysctl"); expect(args).toEqual(["-n", "hw.model"]); return "Mac16,7\n"; diff --git a/apps/web/src/components/settings/SystemInfoPanel.test.mjs b/apps/web/src/components/settings/SystemInfoPanel.test.mjs index bc4cbbc9..60652ca8 100644 --- a/apps/web/src/components/settings/SystemInfoPanel.test.mjs +++ b/apps/web/src/components/settings/SystemInfoPanel.test.mjs @@ -44,6 +44,7 @@ describe("system information diagnostic fields", () => { expect(source).toContain("readBrowserClientDisplaySize"); expect(source).toContain("getClientDisplaySizeParts"); expect(source).toContain('window.addEventListener("resize", onResize)'); + expect(readFileSync(new URL("../../lib/system-diagnostics.ts", import.meta.url), "utf8")).toContain("toDevicePixelScreenSize"); }); test("distinguishes empty sync from never synced", () => { diff --git a/apps/web/src/lib/system-diagnostics.ts b/apps/web/src/lib/system-diagnostics.ts index 252f6709..c11a950c 100644 --- a/apps/web/src/lib/system-diagnostics.ts +++ b/apps/web/src/lib/system-diagnostics.ts @@ -1,4 +1,4 @@ -import { summarizeSyncQueue, type ClientDisplaySizeInput } from "@edgeever/shared"; +import { summarizeSyncQueue, toDevicePixelScreenSize, type ClientDisplaySizeInput } from "@edgeever/shared"; import { api, getConfiguredDesktopApiBaseUrl } from "./api"; import { localDb } from "./local-db"; import { createLocalDataScope } from "./local-mirror"; @@ -66,13 +66,13 @@ const browserOperatingSystem = (userAgent: string, platform: string, platformVer export const readBrowserClientDisplaySize = (): ClientDisplaySizeInput | null => { if (typeof window === "undefined" || typeof screen === "undefined") return null; - return { + return toDevicePixelScreenSize({ devicePixelRatio: Number(window.devicePixelRatio) || 1, screenHeight: Number(screen.height), screenWidth: Number(screen.width), windowHeight: Number(window.innerHeight), windowWidth: Number(window.innerWidth), - }; + }); }; const desktopOperatingSystem = (platform: string, version: string) => { diff --git a/packages/shared/src/client-display-size.test.mjs b/packages/shared/src/client-display-size.test.mjs index 1932e7b8..eb9d1ca3 100644 --- a/packages/shared/src/client-display-size.test.mjs +++ b/packages/shared/src/client-display-size.test.mjs @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { formatClientDisplaySize, formatDevicePixelRatio, getClientDisplaySizeParts } from "./client-display-size"; +import { formatClientDisplaySize, formatDevicePixelRatio, getClientDisplaySizeParts, toDevicePixelScreenSize } from "./client-display-size"; const chineseTemplate = "{{screen}} @{{dpr}}x(窗口 {{windowSize}})"; const englishTemplate = "{{screen}} @{{dpr}}x (window {{windowSize}})"; @@ -49,6 +49,29 @@ describe("client display size formatting", () => { }, chineseTemplate)).toBe("1512×981 @2x(窗口 1279×800)"); }); + test("scales CSS screen size to device pixels and keeps window size in CSS pixels", () => { + expect(toDevicePixelScreenSize({ + devicePixelRatio: 2, + screenHeight: 1080, + screenWidth: 1920, + windowHeight: 925, + windowWidth: 1920, + })).toEqual({ + devicePixelRatio: 2, + screenHeight: 2160, + screenWidth: 3840, + windowHeight: 925, + windowWidth: 1920, + }); + expect(formatClientDisplaySize(toDevicePixelScreenSize({ + devicePixelRatio: 2, + screenHeight: 1080, + screenWidth: 1920, + windowHeight: 925, + windowWidth: 1920, + }), chineseTemplate)).toBe("3840×2160 @2x(窗口 1920×925)"); + }); + test("returns null when a display metric is missing", () => { expect(formatClientDisplaySize({ devicePixelRatio: 2, diff --git a/packages/shared/src/client-display-size.ts b/packages/shared/src/client-display-size.ts index 652f1e2b..3bbe9125 100644 --- a/packages/shared/src/client-display-size.ts +++ b/packages/shared/src/client-display-size.ts @@ -23,6 +23,21 @@ export const formatDevicePixelRatio = (value: number) => { return Number.isInteger(rounded) ? String(rounded) : String(rounded); }; +const scaleCssPixelsToDevicePixels = (cssPixels: number, devicePixelRatio: number) => { + if (!Number.isFinite(cssPixels) || cssPixels <= 0) return cssPixels; + if (!Number.isFinite(devicePixelRatio) || devicePixelRatio <= 0) return cssPixels; + return cssPixels * devicePixelRatio; +}; + +// Browser `screen` APIs report CSS pixels; system info shows the device-pixel panel size. +export const toDevicePixelScreenSize = (input: ClientDisplaySizeInput): ClientDisplaySizeInput => ({ + devicePixelRatio: input.devicePixelRatio, + screenHeight: scaleCssPixelsToDevicePixels(input.screenHeight, input.devicePixelRatio), + screenWidth: scaleCssPixelsToDevicePixels(input.screenWidth, input.devicePixelRatio), + windowHeight: input.windowHeight, + windowWidth: input.windowWidth, +}); + export const getClientDisplaySizeParts = ( input: ClientDisplaySizeInput, ): ClientDisplaySizeParts | null => {