Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ name: Build

on:
push:
branches:
- main
pull_request:

permissions:
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
* Q: How do I have it startup with my Mac?
- A: Go to System Settings -> General -> [add NiceStats to the "Open at Login" section](https://files.catbox.moe/ycsntg.jpg).
* Q: What versions are supported?
- A: Theoretically, it should work on macOS 14.0 and higher but I have not tested it on anything older than 27.0.1 yet.
- A: Theoretically, it should work on macOS 14.0 and higher but the oldest version that was tested one of the latest versions of Sequoia.
- Q: Intel Mac eta wen?
- A: I don't have the time to test functionality on Intel Macs especially considering that they're already deprecated. PRs for Intel Macs are welcome however. If someone can confirm whether the tool works on Intel Macs or not, I will update the GitHub Action to build an Intel version.
- Q: Can I contribute?
- A: Of course! Contributions are more than welcome. Do note that AI usage is restricted here, refer to the CONTRIBUTING.md file.
- Q: Was AI involved in this?
- A: Yes. I have used an LLM (Sonnet 5.5) for the battery icon and GitHub Actions code. Everything else was coded by me with some review and debugging by said LLM.
- A: Yes. I have used an LLM (Sonnet 5.5) for the battery icon, GitHub Actions code, and working on some optimizations. Everything else was coded by me with some review and debugging by said LLM.

## Credits
A good portion of the source code was referenced from [Stats](https://github.com/exelban/stats) (for SMC logic and etc) and [Ampere](https://github.com/MTACS/Ampere) (for the battery icon).
Expand All @@ -24,7 +24,8 @@ Huge thanks to the following people for helping me with translating the app:
| Language | Translator |
| --- | --- |
| German, Russian | [ZomkaDEV](https://github.com/ZomkaDEV/) |
| Spanish (Latin American) | [etonedemid](https://github.com/etonedemid/) |
| Spanish (Latin American) | [etonedemid](https://github.com/etonedemid/), [Snekolotl](https://guns.lol/snekolotl) |
| Korean | [Helloyunho](https://github.com/Helloyunho) |
| French | MrLeRien |
| French | MrLeRien, [Maelstrom](https://maelstrom.dev) |
| Italian | [chic\_luke](https://t.me/chic_luke) |
| Portuguese | [ChrisCoy](https://github.com/ChrisCoy) |
97 changes: 97 additions & 0 deletions src/Drive.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import Foundation
import IOKit

struct DriveInfo {
var solidState: Bool?
var nvme = false
var interconnect: String?
var external: Bool?
var health: Int?
var warning = false
var written: Double?

var typeText: String? {
let parts = [solidState.map { $0 ? "SSD" : "HDD" }, nvme ? "NVMe" : nil].compactMap { $0 }
return parts.isEmpty ? nil : parts.joined(separator: " · ")
}
var connectionText: String? {
let parts = [external.map { $0 ? String(localized: "External") : String(localized: "Internal") }, interconnect].compactMap { $0 }
return parts.isEmpty ? nil : parts.joined(separator: " · ")
}
}

func driveBytes(_ v: Double) -> String { ByteCountFormatter.string(fromByteCount: Int64(v), countStyle: .decimal) }

private func className(_ e: io_object_t) -> String {
var b = [CChar](repeating: 0, count: 128)
IOObjectGetClass(e, &b)
return String(cString: b)
}

func readDrive(path: String) -> DriveInfo? {
var fs = statfs()
guard statfs(path, &fs) == 0 else { return nil }
let from = withUnsafePointer(to: &fs.f_mntfromname) { $0.withMemoryRebound(to: CChar.self, capacity: Int(MAXPATHLEN)) { String(cString: $0) } }
var cur = IOServiceGetMatchingService(kIOMainPortDefault, IOBSDNameMatching(kIOMainPortDefault, 0, (from as NSString).lastPathComponent))
var device: io_registry_entry_t = 0
for _ in 0..<16 where cur != 0 {
if IOObjectConformsTo(cur, "IOBlockStorageDevice") != 0 { device = cur; break }
var parent: io_registry_entry_t = 0
IORegistryEntryGetParentEntry(cur, kIOServicePlane, &parent)
IOObjectRelease(cur); cur = parent
}
guard device != 0 else { if cur != 0 { IOObjectRelease(cur) }; return nil }
defer { IOObjectRelease(device) }

func prop(_ key: String, _ sub: String) -> Any? {
(IORegistryEntryCreateCFProperty(device, key as CFString, kCFAllocatorDefault, 0)?.takeRetainedValue() as? [String: Any])?[sub]
}
var info = DriveInfo()
info.nvme = className(device).contains("NVMe")
switch prop("Device Characteristics", "Medium Type") as? String {
case "Solid State": info.solidState = true
case "Rotational": info.solidState = false
default: break
}
info.interconnect = prop("Protocol Characteristics", "Physical Interconnect") as? String
switch prop("Protocol Characteristics", "Physical Interconnect Location") as? String {
case "Internal": info.external = false
case "External": info.external = true
default: break
}
if info.nvme, let s = readNVMeSMART(device) {
info.health = max(0, 100 - s.percentUsed)
info.warning = s.critical != 0
info.written = s.unitsWritten * 512_000
}
return info
}

private struct SmartVtbl {
var reserved: UnsafeMutableRawPointer?
var queryInterface: UnsafeMutableRawPointer?
var addRef: UnsafeMutableRawPointer?
var release: (@convention(c) (UnsafeMutableRawPointer?) -> UInt32)?
var version: UInt16
var revision: UInt16
var smartReadData: (@convention(c) (UnsafeMutableRawPointer?, UnsafeMutableRawPointer?) -> IOReturn)?
}

private func readNVMeSMART(_ service: io_service_t) -> (percentUsed: Int, critical: UInt8, unitsWritten: Double)? {
let typeID = CFUUIDGetConstantUUIDWithBytes(nil, 0xAA, 0x0F, 0xA6, 0xF9, 0xC2, 0xD6, 0x45, 0x7F, 0xB1, 0x0B, 0x59, 0xA1, 0x32, 0x53, 0x29, 0x2F)
let ifaceID = CFUUIDGetConstantUUIDWithBytes(nil, 0xCC, 0xD1, 0xDB, 0x19, 0xFD, 0x9A, 0x4D, 0xAF, 0xBF, 0x95, 0x12, 0x45, 0x4B, 0x23, 0x0A, 0xB6)
let plugID = CFUUIDGetConstantUUIDWithBytes(nil, 0xC2, 0x44, 0xE8, 0x58, 0x10, 0x9C, 0x11, 0xD4, 0x91, 0xD4, 0x00, 0x50, 0xE4, 0xC6, 0x42, 0x6F)
var plugin: UnsafeMutablePointer<UnsafeMutablePointer<IOCFPlugInInterface>?>?
var score: Int32 = 0
guard IOCreatePlugInInterfaceForService(service, typeID, plugID, &plugin, &score) == KERN_SUCCESS, let plugin else { return nil }
defer { IODestroyPlugInInterface(plugin) }
var out: LPVOID?
guard plugin.pointee!.pointee.QueryInterface(plugin, CFUUIDGetUUIDBytes(ifaceID), &out) == 0, let out else { return nil }
let iface = out.assumingMemoryBound(to: UnsafeMutablePointer<SmartVtbl>.self)
defer { _ = iface.pointee.pointee.release?(iface) }
var buf = [UInt8](repeating: 0, count: 512)
let rc = buf.withUnsafeMutableBytes { iface.pointee.pointee.smartReadData?(iface, $0.baseAddress) ?? -1 }
guard rc == 0 else { return nil }
let written = (0..<8).reduce(0.0) { $0 + Double(buf[48 + $1]) * pow(256, Double($1)) }
return (Int(buf[5]), buf[0], written)
}
Loading
Loading