fix(settings): show Mac product name and physical screen resolution
This commit is contained in:
1 parent
ac142b2af5
commit
10e34dbc6f
6 files changed
+111
-6
No files matched your search
@@ -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", [
|
||||
|
||||
@@ -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 <class IOPlatformDevice>
|
||||
{
|
||||
"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";
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
Reference in new issue
Block a user