diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0d4cf78..96e84fc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,14 @@
记录 OpenStats 应用的功能新增、样式调整和问题修复,按版本与日期倒序排列。
官网、构建与发布脚本等不影响应用本身的改动不在此记录。
+## 未发布
+
+### 2026-09-21
+
+#### 新增
+
+- 菜单栏多一种“双行居中”风格:两行各自居中。原来的“双行文字”是两行一起靠右对齐,标签比数值宽的时候(风扇 0、温度 54°),数值会被推到右边看着偏。居中之后数值正落在标签下面。设置 · 菜单栏里选整体风格,也可以只给某一项单独用。
+
## 0.6.2 · 2026-09-21
### 2026-09-21
diff --git a/Packages/OpenStatsKit/Sources/Localization/Translations.swift b/Packages/OpenStatsKit/Sources/Localization/Translations.swift
index f27d0e6..c8db8ab 100644
--- a/Packages/OpenStatsKit/Sources/Localization/Translations.swift
+++ b/Packages/OpenStatsKit/Sources/Localization/Translations.swift
@@ -334,6 +334,7 @@ npm 缓存 npm Cache
去批准 Approve
去授权 Grant Access
双行圆点 Dots
+双行居中 Stacked, Centered
双行文字 Stacked Text
双行箭头 Arrows
反馈问题 Report a Problem
@@ -382,6 +383,7 @@ npm 缓存 npm Cache
合盖运行时散热变差,请勿放入包中。使用电池且电量低于 {}% 时会自动关闭。 Cooling is worse with the lid closed, so don't put your Mac in a bag. Turns off automatically on battery below {}%.
合盖运行设置 Lid Mode
合盖运行需要修改系统睡眠设置,需安装辅助工具(管理员授权一次)。 Lid mode changes system sleep settings and needs the helper (one-time admin approval).
+同双行文字,但两行各自居中;标签比数值宽时(风扇、温度)不会偏到一边 Same as stacked, but both lines are centered, so a value narrower than its label (fan, temperature) is not pushed to one side
同步 Sync
同步失败 Sync failed
同步的内容:菜单栏项目与风格、刷新频率、外观与语言、详情弹窗的区块、连接探测、通知、快捷键、风扇安全温度、合盖电量下限。当前页面、辅助工具与历史记录留在本机。 What syncs: menu bar items and styles, refresh rate, appearance and language, popover sections, connection probe, notifications, hotkeys, fan safety temperature and lid-mode battery floor. The current page, the helper and history stay on this Mac.
@@ -450,7 +452,7 @@ npm 缓存 npm Cache
寿命按厂商估算的已用比例计算;写入量越大消耗越快,日常使用通常可用很多年 Life is based on the vendor's wear estimate. Heavy writes wear it faster; typical use lasts many years.
将清理 {} 个项目,共 {}。{} {} items, {} total, will be cleaned. {}
将移到废纸篓 Will move to Trash
-小标签在上、数值在下,最紧凑 Small label above the value; most compact
+小标签在上、数值在下,两行靠右对齐,数值的右边不会跳 Small label on top, value below, both right-aligned so the value's right edge stays put
尚未检查 Not checked yet
屏幕保持常亮 Keep display on
屏幕可按设置关闭,下载、编译等后台任务继续运行 The display can turn off as usual while downloads and builds keep running
diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/MenuBarRenderer.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/MenuBarRenderer.swift
index 418fa2b..5f6c453 100644
--- a/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/MenuBarRenderer.swift
+++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/AppKit/MenuBarRenderer.swift
@@ -243,10 +243,11 @@ enum MenuBarRenderer {
let text = Format.percent(fraction)
let alert = colorizeHighLoad && fraction <= Metrics.lowBatteryLevel
let label = MenuBarItem.battery.menuBarLabel
- let stacked = stackedText(label: label, value: text, sample: "100%", alert: alert)
+ let stacked = stackedText(label: label, value: text, sample: "100%", alert: alert,
+ centered: style == .stackedCenter)
var segment: Segment
switch style {
- case .stacked: segment = stacked
+ case .stacked, .stackedCenter: segment = stacked
case .inline: segment = inlineText(label: label, value: text, sample: "100%", alert: alert)
case .icon:
let symbol = reading.batteryCharging ? "battery.100.bolt" : batterySymbol(fraction)
@@ -283,10 +284,11 @@ enum MenuBarRenderer {
let fraction = min(1, max(0, value ?? 0))
let text = value.map { Format.percent($0) } ?? "—"
let alert = colorizeHighLoad && fraction >= Metrics.highLevel
- let stacked = stackedText(label: item.menuBarLabel, value: text, sample: "100%", alert: alert)
+ let stacked = stackedText(label: item.menuBarLabel, value: text, sample: "100%", alert: alert,
+ centered: style == .stackedCenter)
switch style {
- case .stacked:
+ case .stacked, .stackedCenter:
return stacked
case .inline:
return inlineText(label: item.menuBarLabel, value: text, sample: "100%", alert: alert)
@@ -312,7 +314,8 @@ enum MenuBarRenderer {
switch style {
case .inline: inlineText(label: item.menuBarLabel, value: value, sample: sample, alert: false)
case .icon: combine([symbolSegment(item.symbol), inlineValue(value, sample: sample, alert: false)])
- default: stackedText(label: item.menuBarLabel, value: value, sample: sample, alert: false)
+ default: stackedText(label: item.menuBarLabel, value: value, sample: sample, alert: false,
+ centered: style == .stackedCenter)
}
}
@@ -323,14 +326,24 @@ enum MenuBarRenderer {
}
/// 颜色保持动态(labelColor / secondaryLabelColor),在绘制时才按菜单栏外观解析
- private static func stackedText(label: String, value: String, sample: String, alert: Bool) -> Segment {
+ /// 两行文字:宽度按标签、数值与样本里最宽的算,所以数值变长也不会挤动旁边的项目。
+ /// `centered` 时两行各自居中,标签比数值宽时(风扇、温度)数值不会贴在右边
+ private static func stackedText(label: String, value: String, sample: String, alert: Bool,
+ centered: Bool = false) -> Segment {
let labelAttributes: [NSAttributedString.Key: Any] = [.font: Metrics.labelFont, .foregroundColor: NSColor.secondaryLabelColor]
let valueAttributes: [NSAttributedString.Key: Any] = [.font: Metrics.stackedValueFont, .foregroundColor: foreground(alert)]
let width = ceil(max(textWidth(label, labelAttributes), textWidth(value, valueAttributes), textWidth(sample, valueAttributes)))
return Segment(width: width) { rect in
let base = baselineOrigin(in: rect)
- drawText(label, labelAttributes, rightEdge: rect.maxX, baseline: base + Metrics.upperBaseline)
- drawText(value, valueAttributes, rightEdge: rect.maxX, baseline: base + Metrics.lowerBaseline)
+ let rows = [(label, labelAttributes, base + Metrics.upperBaseline),
+ (value, valueAttributes, base + Metrics.lowerBaseline)]
+ for (text, attributes, baseline) in rows {
+ if centered {
+ drawText(text, attributes, centerX: rect.midX, baseline: baseline)
+ } else {
+ drawText(text, attributes, rightEdge: rect.maxX, baseline: baseline)
+ }
+ }
}
}
@@ -363,6 +376,13 @@ enum MenuBarRenderer {
string.draw(with: NSRect(x: rightEdge - width, y: baseline, width: width, height: 0))
}
+ /// 居中绘制:起点取整像素,文字才不会发虚
+ private static func drawText(_ text: String, _ attributes: [NSAttributedString.Key: Any], centerX: CGFloat, baseline: CGFloat) {
+ let string = NSAttributedString(string: text, attributes: attributes)
+ let width = string.size().width
+ string.draw(with: NSRect(x: (centerX - width / 2).rounded(), y: baseline, width: width, height: 0))
+ }
+
/// 刘海屏的菜单栏更高,两行内容整体在按钮内垂直居中
private static func baselineOrigin(in rect: NSRect) -> CGFloat {
rect.midY - Metrics.barHeight / 2
diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/SettingsPages.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/SettingsPages.swift
index 047880a..9c8aadb 100644
--- a/Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/SettingsPages.swift
+++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/Settings/SettingsPages.swift
@@ -472,8 +472,20 @@ private struct StyleCard: View {
let action: () -> Void
@State private var hovering = false
+ /// 两种双行风格的差别只在标签比数值宽时看得出来,预览里用风扇(标签宽、数值短)代替 GPU
+ private var isStacked: Bool { style == .stacked || style == .stackedCenter }
+
+ private var previewItems: [MenuBarItem] { isStacked ? [.cpu, .memory, .fan] : [.cpu, .memory, .gpu] }
+
+ private var previewReading: MenuBarReading {
+ guard isStacked else { return .sample }
+ var reading = MenuBarReading.sample
+ reading.fanRPM = 0
+ return reading
+ }
+
var body: some View {
- let sample = MenuBarRenderer.image(reading: .sample, items: [.cpu, .memory, .gpu],
+ let sample = MenuBarRenderer.image(reading: previewReading, items: previewItems,
style: { _ in style }, networkStyle: model.settings.networkStyle,
colorizeHighLoad: false, fahrenheit: model.settings.useFahrenheit)
Button(action: action) {
diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/State/AppSettings.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/State/AppSettings.swift
index 47cca0a..d72af71 100644
--- a/Packages/OpenStatsKit/Sources/OpenStatsUI/State/AppSettings.swift
+++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/State/AppSettings.swift
@@ -184,14 +184,14 @@ public enum ProbeTarget: String, CaseIterable, Identifiable, Sendable {
/// 菜单栏风格:整体选一种套用到所有指标,个别指标可以单独指定(AppSettings.styleOverrides);风格内部的排版保持一致
public enum MenuBarStyle: String, CaseIterable, Identifiable, Sendable {
- case stacked, inline, icon, ring, pie, history, line, meter, dot
+ case stacked, stackedCenter, inline, icon, ring, pie, history, line, meter, dot
public var id: String { rawValue }
/// 温度、风扇没有百分比,图形类风格画不出来,只给文字类的三种
static func options(for item: MenuBarItem) -> [MenuBarStyle] {
switch item {
- case .temperature, .fan: [.stacked, .inline, .icon]
+ case .temperature, .fan: [.stacked, .stackedCenter, .inline, .icon]
default: allCases
}
}
@@ -199,6 +199,7 @@ public enum MenuBarStyle: String, CaseIterable, Identifiable, Sendable {
var title: String {
switch self {
case .stacked: tr("双行文字")
+ case .stackedCenter: tr("双行居中")
case .inline: tr("单行文字")
case .icon: tr("图标")
case .ring: tr("圆环")
@@ -212,7 +213,8 @@ public enum MenuBarStyle: String, CaseIterable, Identifiable, Sendable {
var detail: String {
switch self {
- case .stacked: tr("小标签在上、数值在下,最紧凑")
+ case .stacked: tr("小标签在上、数值在下,两行靠右对齐,数值的右边不会跳")
+ case .stackedCenter: tr("同双行文字,但两行各自居中;标签比数值宽时(风扇、温度)不会偏到一边")
case .inline: tr("标签与数值同一行,最易读")
case .icon: tr("用系统图标代替文字标签")
case .ring: tr("圆环表示当前占用比例")
diff --git a/README.md b/README.md
index 9983b08..3606a30 100644
--- a/README.md
+++ b/README.md
@@ -62,9 +62,18 @@ system language by default; switch it in Settings → General → Language.
-Latest release **0.6.2** (2026-09-21) · [full changelog](CHANGELOG.md) (kept in Chinese)
+Latest release **0.6.2** (2026-09-21) · **1** changes in development · [full changelog](CHANGELOG.md) (kept in Chinese)
+2026-09-21 · Unreleased · 1 added
+
+**Added**
+
+- 菜单栏多一种“双行居中”风格:两行各自居中。原来的“双行文字”是两行一起靠右对齐,标签比数值宽的时候(风扇 0、温度 54°),数值会被推到右边看着偏。居中之后数值正落在标签下面。设置 · 菜单栏里选整体风格,也可以只给某一项单独用。
+
+
+
+
2026-09-21 · 0.6.2 · 2 fixed
**Fixed**
@@ -83,23 +92,6 @@ Latest release **0.6.2** (2026-09-21) · [full changelog](CHANGELOG.md) (kept in
-
-2026-09-18 · 0.6.0 · 5 added · 1 style
-
-**Added**
-
-- 新增出口与分流窗口:网络页顶栏和菜单栏网络弹窗各有一个入口。一次检测约十秒,看清 VPN 与代理有没有生效、各个网站分别从哪个出口出去。
-- 结论只用一句话说清:未使用代理、代理出口(地区)、代理没有生效、网络不通四种情况。右侧同时给出经代理、直连、连不上的网站数量。
-- 本机网络:物理网卡的出口 IP、归属地、运营商与 IPv6。归属地数据来自 cleanip.io。
-- 网站分流:25 个常用网站分别从哪个出口出去。按 AI、影音、社交、开发、国内分类筛选;接入 Cloudflare 的网站能实测出口与边缘节点,其余按国内 / 国际规则推断,每个网站带延迟信号条。
-- 代理方式一览:VPN / 隧道、系统代理、自动代理配置、DNS 与检测到的代理软件。DNS 被代理接管时会标出来。
-
-**Style**
-
-- 菜单栏网络弹窗的 DNS 区块重排:网络服务与线路做成两个徽章(Wi-Fi / 有线、直连或经过哪个代理),走隧道时标题旁有叹号提示 DNS 可能被接管,刷新 DNS 缓存改为图标按钮。
-
-
-
## Activity
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 6d9f254..ae64aa0 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -57,9 +57,18 @@ CPU 不分性能核与能效核,部分功耗与频率读数可能不显示。
-最新版本 **0.6.2**(2026-09-21) · [完整更新日志](CHANGELOG.md)
+最新版本 **0.6.2**(2026-09-21) · 开发中 **1** 项改动尚未发布 · [完整更新日志](CHANGELOG.md)
+2026-09-21 · 未发布 · 新增 1
+
+**新增**
+
+- 菜单栏多一种“双行居中”风格:两行各自居中。原来的“双行文字”是两行一起靠右对齐,标签比数值宽的时候(风扇 0、温度 54°),数值会被推到右边看着偏。居中之后数值正落在标签下面。设置 · 菜单栏里选整体风格,也可以只给某一项单独用。
+
+
+
+
2026-09-21 · 0.6.2 · 修复 2
**修复**
@@ -78,23 +87,6 @@ CPU 不分性能核与能效核,部分功耗与频率读数可能不显示。
-
-2026-09-18 · 0.6.0 · 新增 5 · 样式 1
-
-**新增**
-
-- 新增出口与分流窗口:网络页顶栏和菜单栏网络弹窗各有一个入口。一次检测约十秒,看清 VPN 与代理有没有生效、各个网站分别从哪个出口出去。
-- 结论只用一句话说清:未使用代理、代理出口(地区)、代理没有生效、网络不通四种情况。右侧同时给出经代理、直连、连不上的网站数量。
-- 本机网络:物理网卡的出口 IP、归属地、运营商与 IPv6。归属地数据来自 cleanip.io。
-- 网站分流:25 个常用网站分别从哪个出口出去。按 AI、影音、社交、开发、国内分类筛选;接入 Cloudflare 的网站能实测出口与边缘节点,其余按国内 / 国际规则推断,每个网站带延迟信号条。
-- 代理方式一览:VPN / 隧道、系统代理、自动代理配置、DNS 与检测到的代理软件。DNS 被代理接管时会标出来。
-
-**样式**
-
-- 菜单栏网络弹窗的 DNS 区块重排:网络服务与线路做成两个徽章(Wi-Fi / 有线、直连或经过哪个代理),走隧道时标题旁有叹号提示 DNS 可能被接管,刷新 DNS 缓存改为图标按钮。
-
-
-
## 活跃度
diff --git a/project.yml b/project.yml
index e0b2fe3..e909634 100644
--- a/project.yml
+++ b/project.yml
@@ -13,7 +13,7 @@ settings:
SWIFT_VERSION: "6.0"
MARKETING_VERSION: "0.6.2"
# 构建号四位数字,每次 make build 加一;营销版本只在发布时用 Scripts/version.sh patch / minor 修改
- CURRENT_PROJECT_VERSION: "0108"
+ CURRENT_PROJECT_VERSION: "0111"
MACOSX_DEPLOYMENT_TARGET: "14.0"
# 本地开发使用 ad-hoc 签名;发布时改为 Developer ID(见 docs/PLAN.md)
CODE_SIGN_STYLE: Manual