Skip to content

Commit 8729b54

Browse files
committed
Release Mectrics 1.2.0
Add independent CPU, memory, and GPU temperature menu bar items while hiding unsupported sensor domains. Include localized labels, memory temperature details, tests, and updated release documentation.
1 parent 076a8e7 commit 8729b54

14 files changed

Lines changed: 263 additions & 29 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ All notable changes to this project are documented here.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
66
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.2.0] — 2026-07-30
9+
10+
### Added
11+
12+
- Independent temperature menu bar components for CPU, Memory, and GPU, plus Memory
13+
temperature in its detail popover when the Mac exposes a recognized sensor.
14+
815
## [1.1.0] — 2026-07-30
916

1017
### Added
@@ -64,5 +71,6 @@ roughly one idle wake per second.
6471
- Automatic update checks are disabled; the Sparkle appcast is fetched only on an explicit
6572
**Check for Updates…** and verified against a pinned EdDSA public key.
6673

74+
[1.2.0]: https://github.com/farukkamcici/mectrics/compare/v1.1.0...v1.2.0
6775
[1.1.0]: https://github.com/farukkamcici/mectrics/compare/v1.0.0...v1.1.0
6876
[1.0.0]: https://github.com/farukkamcici/mectrics/releases/tag/v1.0.0

Mectrics/App/AppModel.swift

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ final class AppModel {
4949
/// defined by `MenuBarComponent.available(for:)`.
5050
var orderedEnabledItems: [(module: MetricID, component: MenuBarComponent)] {
5151
availableModules.flatMap { id in
52-
MenuBarComponent.available(for: id)
52+
availableComponents(for: id)
5353
.filter { enabledComponents[id]?.contains($0) ?? false }
5454
.map { (id, $0) }
5555
}
@@ -201,21 +201,35 @@ final class AppModel {
201201
}
202202

203203
/// Component choices worth offering for a module. Battery health and cycle count
204-
/// are hidden when this Mac does not report them, so the builder never offers a
205-
/// look that can only ever render a dash.
204+
/// and temperatures are hidden when this Mac does not report them, so the
205+
/// builder never offers a look that can only ever render a dash.
206206
func availableComponents(for id: MetricID) -> [MenuBarComponent] {
207+
let components = MenuBarComponent.available(for: id)
207208
guard let detail = latest[id]?.detail else {
208-
return MenuBarComponent.available(for: id)
209+
return components.filter { $0 != .temperature }
209210
}
210-
return MenuBarComponent.available(for: id).filter { component in
211+
return components.filter { component in
211212
switch component {
212213
case .health: return detail["healthPercent"] != nil
213214
case .cycles: return detail["cycleCount"] != nil
215+
case .temperature: return temperature(for: id) != nil
214216
default: return true
215217
}
216218
}
217219
}
218220

221+
/// Temperature belonging to a hardware-domain module, if the SMC exposes a
222+
/// recognized sensor for that domain on this Mac.
223+
func temperature(for id: MetricID) -> Double? {
224+
let detail = latest[.sensors]?.detail
225+
switch id {
226+
case .cpu: return detail?["cpuMax"]
227+
case .memory: return detail?["memoryMax"]
228+
case .gpu: return detail?["gpuMax"]
229+
default: return nil
230+
}
231+
}
232+
219233
/// Keeps every available module sampled while the menu bar builder is on screen,
220234
/// so its component previews show real values rather than placeholders.
221235
func beginBuilderPreview() {
@@ -297,13 +311,23 @@ final class AppModel {
297311
/// Applies one engine pass while preserving the last valid sample across short
298312
/// provider interruptions.
299313
func apply(_ report: SamplingCycleReport) {
314+
let previousTemperatureModules = availableTemperatureModules
300315
for (id, sample) in report.samples {
301316
latest[id] = sample
302317
consecutiveSamplingFailures[id] = 0
303318
}
304319
for id in report.failedMetricIDs {
305320
consecutiveSamplingFailures[id, default: 0] += 1
306321
}
322+
if availableTemperatureModules != previousTemperatureModules {
323+
onModulesChanged?()
324+
}
325+
}
326+
327+
private var availableTemperatureModules: Set<MetricID> {
328+
Set([MetricID.cpu, .memory, .gpu].filter {
329+
latest[$0] != nil && temperature(for: $0) != nil
330+
})
307331
}
308332

309333
/// Shared state resolver used by menu bar, popover, panel, and Settings preview.
@@ -342,7 +366,8 @@ final class AppModel {
342366
}
343367

344368
/// Samples visible modules plus metrics required by enabled alerts. Temperature
345-
/// readings stay active when CPU/GPU is visible because their popovers show them.
369+
/// readings stay active when CPU, memory, or GPU is visible because their
370+
/// popovers and optional menu bar components show them.
346371
private func refreshActiveMetrics() {
347372
var active = enabledModules
348373
for (id, rule) in alertRules where rule.enabled {
@@ -351,15 +376,15 @@ final class AppModel {
351376
for (signal, rule) in systemAlertRules where rule.enabled {
352377
active.insert(signal.metricID)
353378
}
354-
if active.contains(.cpu) || active.contains(.gpu) {
355-
active.insert(.sensors)
356-
}
357379
if onboardingPreviewActive {
358380
active.formUnion([.cpu, .memory, .battery, .network])
359381
}
360382
if builderPreviewActive {
361383
active.formUnion(availableModules)
362384
}
385+
if !active.isDisjoint(with: [.cpu, .memory, .gpu]) {
386+
active.insert(.sensors)
387+
}
363388
engine.setActiveMetrics(active)
364389
}
365390

Mectrics/MenuBar/MenuBarComponent.swift

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ enum MenuBarComponent: String, CaseIterable, Identifiable {
99
case value, valueGraph
1010
// CPU
1111
case coreBars
12+
// CPU, memory, GPU
13+
case temperature
1214
// Capacity texts (memory used, disk used/free)
1315
case usedBytes, freeBytes
1416
// Disk pictorial
@@ -22,9 +24,9 @@ enum MenuBarComponent: String, CaseIterable, Identifiable {
2224

2325
static func available(for module: MetricID) -> [MenuBarComponent] {
2426
switch module {
25-
case .cpu: return [.value, .valueGraph, .coreBars]
26-
case .memory: return [.value, .valueGraph, .usedBytes]
27-
case .gpu: return [.value, .valueGraph]
27+
case .cpu: return [.value, .valueGraph, .coreBars, .temperature]
28+
case .memory: return [.value, .valueGraph, .usedBytes, .temperature]
29+
case .gpu: return [.value, .valueGraph, .temperature]
2830
case .battery: return [
2931
.value, .batteryIcon, .batteryIconValue, .health, .cycles
3032
]
@@ -47,6 +49,7 @@ enum MenuBarComponent: String, CaseIterable, Identifiable {
4749
case .value: return String(localized: "component.value", defaultValue: "Value")
4850
case .valueGraph: return String(localized: "component.valueGraph", defaultValue: "Value + Graph")
4951
case .coreBars: return String(localized: "component.coreBars", defaultValue: "Cores")
52+
case .temperature: return String(localized: "component.temperature", defaultValue: "Temperature")
5053
case .usedBytes: return String(localized: "component.used", defaultValue: "Used")
5154
case .freeBytes: return String(localized: "component.free", defaultValue: "Free")
5255
case .ring: return String(localized: "component.ring", defaultValue: "Ring")
@@ -85,6 +88,8 @@ enum MenuBarComponent: String, CaseIterable, Identifiable {
8588
return "100%"
8689
case .cycles:
8790
return "9999"
91+
case .temperature:
92+
return "125°"
8893
case .netActivity, .netDown, .netUp:
8994
return "↓999M"
9095
case .value, .valueGraph:
@@ -111,7 +116,8 @@ enum MenuBarVisual {
111116

112117
extension MenuBarText {
113118
static func visual(for id: MetricID, component: MenuBarComponent,
114-
sample: MetricSample) -> MenuBarVisual {
119+
sample: MetricSample,
120+
temperature: Double? = nil) -> MenuBarVisual {
115121
switch component {
116122
case .value:
117123
return .text(string(for: id, sample: sample))
@@ -120,6 +126,9 @@ extension MenuBarText {
120126
case .coreBars:
121127
let cores = Int(sample.detail["coreCount"] ?? 0)
122128
return .coreBars((0..<cores).compactMap { sample.detail["core\($0)"] })
129+
case .temperature:
130+
guard let temperature else { return .text("") }
131+
return .text("\(Int(temperature.rounded()))°")
123132
case .usedBytes:
124133
return .text(MetricFormat.menuRate(sample.detail["used"] ?? 0) + "B")
125134
case .freeBytes:

Mectrics/MenuBar/MenuBarController.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ final class MenuBarController: NSObject, NSPopoverDelegate {
7474
MenuBarText.visual(
7575
for: id,
7676
component: statusItem.component,
77-
sample: $0
77+
sample: $0,
78+
temperature: model.temperature(for: id)
7879
)
7980
},
8081
state: model.metricState(for: id, isEnabled: true),

Mectrics/Resources/Localizable.xcstrings

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4861,6 +4861,47 @@
48614861
}
48624862
}
48634863
},
4864+
"component.temperature": {
4865+
"extractionState": "extracted_with_value",
4866+
"localizations": {
4867+
"en": {
4868+
"stringUnit": {
4869+
"state": "new",
4870+
"value": "Temperature"
4871+
}
4872+
},
4873+
"es": {
4874+
"stringUnit": {
4875+
"state": "translated",
4876+
"value": "Temperatura"
4877+
}
4878+
},
4879+
"fr": {
4880+
"stringUnit": {
4881+
"state": "translated",
4882+
"value": "Température"
4883+
}
4884+
},
4885+
"pt-BR": {
4886+
"stringUnit": {
4887+
"state": "translated",
4888+
"value": "Temperatura"
4889+
}
4890+
},
4891+
"ru": {
4892+
"stringUnit": {
4893+
"state": "translated",
4894+
"value": "Температура"
4895+
}
4896+
},
4897+
"tr": {
4898+
"stringUnit": {
4899+
"state": "translated",
4900+
"value": "Sıcaklık"
4901+
}
4902+
}
4903+
}
4904+
},
48644905
"component.upload": {
48654906
"extractionState": "extracted_with_value",
48664907
"localizations": {
@@ -8683,6 +8724,47 @@
86838724
}
86848725
}
86858726
},
8727+
"mem.temperature": {
8728+
"extractionState": "extracted_with_value",
8729+
"localizations": {
8730+
"en": {
8731+
"stringUnit": {
8732+
"state": "new",
8733+
"value": "Temperature"
8734+
}
8735+
},
8736+
"es": {
8737+
"stringUnit": {
8738+
"state": "translated",
8739+
"value": "Temperatura"
8740+
}
8741+
},
8742+
"fr": {
8743+
"stringUnit": {
8744+
"state": "translated",
8745+
"value": "Température"
8746+
}
8747+
},
8748+
"pt-BR": {
8749+
"stringUnit": {
8750+
"state": "translated",
8751+
"value": "Temperatura"
8752+
}
8753+
},
8754+
"ru": {
8755+
"stringUnit": {
8756+
"state": "translated",
8757+
"value": "Температура"
8758+
}
8759+
},
8760+
"tr": {
8761+
"stringUnit": {
8762+
"state": "translated",
8763+
"value": "Sıcaklık"
8764+
}
8765+
}
8766+
}
8767+
},
86868768
"mem.total": {
86878769
"extractionState": "extracted_with_value",
86888770
"localizations": {
@@ -12360,6 +12442,47 @@
1236012442
}
1236112443
}
1236212444
},
12445+
"sensors.memory": {
12446+
"extractionState": "extracted_with_value",
12447+
"localizations": {
12448+
"en": {
12449+
"stringUnit": {
12450+
"state": "new",
12451+
"value": "Memory (hottest)"
12452+
}
12453+
},
12454+
"es": {
12455+
"stringUnit": {
12456+
"state": "translated",
12457+
"value": "Memoria (máxima)"
12458+
}
12459+
},
12460+
"fr": {
12461+
"stringUnit": {
12462+
"state": "translated",
12463+
"value": "Mémoire (maximale)"
12464+
}
12465+
},
12466+
"pt-BR": {
12467+
"stringUnit": {
12468+
"state": "translated",
12469+
"value": "Memória (mais quente)"
12470+
}
12471+
},
12472+
"ru": {
12473+
"stringUnit": {
12474+
"state": "translated",
12475+
"value": "Память (максимальная)"
12476+
}
12477+
},
12478+
"tr": {
12479+
"stringUnit": {
12480+
"state": "translated",
12481+
"value": "Bellek (en yüksek)"
12482+
}
12483+
}
12484+
}
12485+
},
1236312486
"Settings": {
1236412487
"localizations": {
1236512488
"es": {

Mectrics/Settings/MenuBarBuilderView.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,12 @@ struct MenuBarBuilderView: View {
316316

317317
private func previewText(_ id: MetricID, _ component: MenuBarComponent) -> String {
318318
guard let sample = model.latest[id] else { return "" }
319-
if case .text(let text) = MenuBarText.visual(for: id, component: component, sample: sample) {
319+
if case .text(let text) = MenuBarText.visual(
320+
for: id,
321+
component: component,
322+
sample: sample,
323+
temperature: model.temperature(for: id)
324+
) {
320325
return text.replacingOccurrences(of: "\n", with: " ")
321326
}
322327
return MenuBarText.string(for: id, sample: sample)

Mectrics/UI/DetailPopoverView.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,10 @@ struct DetailPopoverView: View {
287287
r.append((String(localized: "mem.pressure", defaultValue: "Pressure"),
288288
Self.pressureLabel(level)))
289289
}
290+
if let t = model.temperature(for: .memory) {
291+
r.append((String(localized: "mem.temperature", defaultValue: "Temperature"),
292+
String(format: "%.1f°C", t)))
293+
}
290294
return r
291295
case .battery:
292296
let charging = (d["charging"] ?? 0) > 0
@@ -383,6 +387,10 @@ struct DetailPopoverView: View {
383387
r.append((String(localized: "sensors.gpu", defaultValue: "GPU (hottest)"),
384388
String(format: "%.1f°C", g)))
385389
}
390+
if let m = d["memoryMax"] {
391+
r.append((String(localized: "sensors.memory", defaultValue: "Memory (hottest)"),
392+
String(format: "%.1f°C", m)))
393+
}
386394
r.append((String(localized: "sensors.count", defaultValue: "Sensors"),
387395
"\(Int(d["sensorCount"] ?? 0))"))
388396
return r

0 commit comments

Comments
 (0)