mirror of
https://github.com/morgan9e/macos-stats
synced 2026-04-14 00:04:15 +09:00
861 lines
27 KiB
Swift
861 lines
27 KiB
Swift
//
|
|
// helpers.swift
|
|
// StatsKit
|
|
//
|
|
// Created by Serhiy Mytrovtsiy on 29/09/2020.
|
|
// Using Swift 5.0.
|
|
// Running on macOS 10.15.
|
|
//
|
|
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
|
//
|
|
// swiftlint:disable file_length
|
|
|
|
import Cocoa
|
|
import os.log
|
|
|
|
public protocol KeyValue_p {
|
|
var key: String { get }
|
|
var value: String { get }
|
|
var additional: Any? { get }
|
|
}
|
|
|
|
public struct KeyValue_t: KeyValue_p {
|
|
public let key: String
|
|
public let value: String
|
|
public let additional: Any?
|
|
|
|
public init(key: String, value: String, additional: Any? = nil) {
|
|
self.key = key
|
|
self.value = value
|
|
self.additional = additional
|
|
}
|
|
}
|
|
|
|
public struct Units {
|
|
public let bytes: Int64
|
|
|
|
public init(bytes: Int64) {
|
|
self.bytes = bytes
|
|
}
|
|
|
|
public var kilobytes: Double {
|
|
return Double(bytes) / 1_024
|
|
}
|
|
public var megabytes: Double {
|
|
return kilobytes / 1_024
|
|
}
|
|
public var gigabytes: Double {
|
|
return megabytes / 1_024
|
|
}
|
|
public var terabytes: Double {
|
|
return gigabytes / 1_024
|
|
}
|
|
|
|
public func getReadableTuple(base: DataSizeBase = .byte) -> (String, String) {
|
|
let stringBase = base == .byte ? "B" : "b"
|
|
let multiplier: Double = base == .byte ? 1 : 8
|
|
|
|
switch bytes {
|
|
case 0..<1_024:
|
|
return ("0", "K\(stringBase)/s")
|
|
case 1_024..<(1_024 * 1_024):
|
|
return (String(format: "%.0f", kilobytes*multiplier), "K\(stringBase)/s")
|
|
case 1_024..<(1_024 * 1_024 * 100):
|
|
return (String(format: "%.1f", megabytes*multiplier), "M\(stringBase)/s")
|
|
case (1_024 * 1_024 * 100)..<(1_024 * 1_024 * 1_024):
|
|
return (String(format: "%.0f", megabytes*multiplier), "M\(stringBase)/s")
|
|
case (1_024 * 1_024 * 1_024)...Int64.max:
|
|
return (String(format: "%.1f", gigabytes*multiplier), "G\(stringBase)/s")
|
|
default:
|
|
return (String(format: "%.0f", kilobytes*multiplier), "K\(stringBase)B/s")
|
|
}
|
|
}
|
|
|
|
public func getReadableSpeed(base: DataSizeBase = .byte, omitUnits: Bool = false) -> String {
|
|
let stringBase = base == .byte ? "B" : "b"
|
|
let multiplier: Double = base == .byte ? 1 : 8
|
|
|
|
switch bytes*Int64(multiplier) {
|
|
case 0..<1_024:
|
|
let unit = omitUnits ? "" : " K\(stringBase)/s"
|
|
return "0\(unit)"
|
|
case 1_024..<(1_024 * 1_024):
|
|
let unit = omitUnits ? "" : " K\(stringBase)/s"
|
|
return String(format: "%.0f\(unit)", kilobytes*multiplier)
|
|
case 1_024..<(1_024 * 1_024 * 100):
|
|
let unit = omitUnits ? "" : " M\(stringBase)/s"
|
|
return String(format: "%.1f\(unit)", megabytes*multiplier)
|
|
case (1_024 * 1_024 * 100)..<(1_024 * 1_024 * 1_024):
|
|
let unit = omitUnits ? "" : " M\(stringBase)/s"
|
|
return String(format: "%.0f\(unit)", megabytes*multiplier)
|
|
case (1_024 * 1_024 * 1_024)...Int64.max:
|
|
let unit = omitUnits ? "" : " G\(stringBase)/s"
|
|
return String(format: "%.1f\(unit)", gigabytes*multiplier)
|
|
default:
|
|
let unit = omitUnits ? "" : " K\(stringBase)/s"
|
|
return String(format: "%.0f\(unit)", kilobytes*multiplier)
|
|
}
|
|
}
|
|
|
|
public func getReadableMemory() -> String {
|
|
switch bytes {
|
|
case 0..<1_024:
|
|
return "0 KB"
|
|
case 1_024..<(1_024 * 1_024):
|
|
return String(format: "%.0f KB", kilobytes)
|
|
case 1_024..<(1_024 * 1_024 * 1_024):
|
|
return String(format: "%.0f MB", megabytes)
|
|
case 1_024..<(1_024 * 1_024 * 1_024 * 1_024):
|
|
return String(format: "%.2f GB", gigabytes)
|
|
case (1_024 * 1_024 * 1_024 * 1_024)...Int64.max:
|
|
return String(format: "%.2f TB", terabytes)
|
|
default:
|
|
return String(format: "%.0f KB", kilobytes)
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct DiskSize {
|
|
public let value: Int64
|
|
|
|
public init(_ size: Int64) {
|
|
self.value = size
|
|
}
|
|
|
|
public var kilobytes: Double {
|
|
return Double(value) / 1_000
|
|
}
|
|
public var megabytes: Double {
|
|
return kilobytes / 1_000
|
|
}
|
|
public var gigabytes: Double {
|
|
return megabytes / 1_000
|
|
}
|
|
public var terabytes: Double {
|
|
return gigabytes / 1_000
|
|
}
|
|
|
|
public func getReadableMemory() -> String {
|
|
switch value {
|
|
case 0..<1_000:
|
|
return "0 KB"
|
|
case 1_000..<(1_000 * 1_000):
|
|
return String(format: "%.0f KB", kilobytes)
|
|
case 1_000..<(1_000 * 1_000 * 1_000):
|
|
return String(format: "%.0f MB", megabytes)
|
|
case 1_000..<(1_000 * 1_000 * 1_000 * 1_000):
|
|
return String(format: "%.2f GB", gigabytes)
|
|
case (1_000 * 1_000 * 1_000 * 1_000)...Int64.max:
|
|
return String(format: "%.2f TB", terabytes)
|
|
default:
|
|
return String(format: "%.0f KB", kilobytes)
|
|
}
|
|
}
|
|
}
|
|
|
|
public class LabelField: NSTextField {
|
|
public init(frame: NSRect, _ label: String = "") {
|
|
super.init(frame: frame)
|
|
|
|
self.isEditable = false
|
|
self.isSelectable = false
|
|
self.isBezeled = false
|
|
self.wantsLayer = true
|
|
self.backgroundColor = .clear
|
|
self.canDrawSubviewsIntoLayer = true
|
|
|
|
self.stringValue = label
|
|
self.textColor = .secondaryLabelColor
|
|
self.alignment = .natural
|
|
self.font = NSFont.systemFont(ofSize: 12, weight: .regular)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|
|
|
|
public class ValueField: NSTextField {
|
|
public init(frame: NSRect, _ value: String = "") {
|
|
super.init(frame: frame)
|
|
|
|
self.isEditable = false
|
|
self.isSelectable = false
|
|
self.isBezeled = false
|
|
self.wantsLayer = true
|
|
self.backgroundColor = .clear
|
|
self.canDrawSubviewsIntoLayer = true
|
|
|
|
self.stringValue = value
|
|
self.textColor = .textColor
|
|
self.alignment = .right
|
|
self.font = NSFont.systemFont(ofSize: 13, weight: .regular)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|
|
|
|
public extension NSBezierPath {
|
|
func addArrow(start: CGPoint, end: CGPoint, pointerLineLength: CGFloat, arrowAngle: CGFloat) {
|
|
self.move(to: start)
|
|
self.line(to: end)
|
|
|
|
let startEndAngle = atan((end.y - start.y) / (end.x - start.x)) + ((end.x - start.x) < 0 ? CGFloat(Double.pi) : 0)
|
|
let arrowLine1 = CGPoint(
|
|
x: end.x + pointerLineLength * cos(CGFloat(Double.pi) - startEndAngle + arrowAngle),
|
|
y: end.y - pointerLineLength * sin(CGFloat(Double.pi) - startEndAngle + arrowAngle)
|
|
)
|
|
let arrowLine2 = CGPoint(
|
|
x: end.x + pointerLineLength * cos(CGFloat(Double.pi) - startEndAngle - arrowAngle),
|
|
y: end.y - pointerLineLength * sin(CGFloat(Double.pi) - startEndAngle - arrowAngle)
|
|
)
|
|
|
|
self.line(to: arrowLine1)
|
|
self.move(to: end)
|
|
self.line(to: arrowLine2)
|
|
}
|
|
}
|
|
|
|
public func separatorView(_ title: String, origin: NSPoint, width: CGFloat) -> NSView {
|
|
let view: NSView = NSView(frame: NSRect(x: origin.x, y: origin.y, width: width, height: 30))
|
|
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
|
|
|
|
let labelView: NSTextField = TextView(frame: NSRect(x: 0, y: (view.frame.height-15)/2, width: view.frame.width, height: 15))
|
|
labelView.stringValue = title
|
|
labelView.alignment = .center
|
|
labelView.textColor = .secondaryLabelColor
|
|
labelView.font = NSFont.systemFont(ofSize: 12, weight: .medium)
|
|
labelView.stringValue = title
|
|
|
|
view.addSubview(labelView)
|
|
return view
|
|
}
|
|
|
|
public func popupRow(_ view: NSView, n: CGFloat = 0, title: String, value: String) -> (LabelField, ValueField) {
|
|
let rowView: NSView = NSView(frame: NSRect(x: 0, y: 22*n, width: view.frame.width, height: 22))
|
|
|
|
let labelWidth = title.widthOfString(usingFont: .systemFont(ofSize: 13, weight: .regular)) + 4
|
|
let labelView: LabelField = LabelField(frame: NSRect(x: 0, y: (22-15)/2, width: labelWidth, height: 15), title)
|
|
let valueView: ValueField = ValueField(frame: NSRect(x: labelWidth, y: (22-15)/2, width: rowView.frame.width - labelWidth, height: 16), value)
|
|
|
|
rowView.addSubview(labelView)
|
|
rowView.addSubview(valueView)
|
|
|
|
if let view = view as? NSStackView {
|
|
rowView.heightAnchor.constraint(equalToConstant: rowView.bounds.height).isActive = true
|
|
view.addArrangedSubview(rowView)
|
|
} else {
|
|
view.addSubview(rowView)
|
|
}
|
|
|
|
return (labelView, valueView)
|
|
}
|
|
|
|
public func popupWithColorRow(_ view: NSView, color: NSColor, n: CGFloat, title: String, value: String) -> ValueField {
|
|
let rowView: NSView = NSView(frame: NSRect(x: 0, y: 22*n, width: view.frame.width, height: 22))
|
|
|
|
let colorView: NSView = NSView(frame: NSRect(x: 2, y: 5, width: 12, height: 12))
|
|
colorView.wantsLayer = true
|
|
colorView.layer?.backgroundColor = color.cgColor
|
|
colorView.layer?.cornerRadius = 2
|
|
let labelWidth = title.widthOfString(usingFont: .systemFont(ofSize: 13, weight: .regular)) + 5
|
|
let labelView: LabelField = LabelField(frame: NSRect(x: 18, y: (22-15)/2, width: labelWidth, height: 15), title)
|
|
let valueView: ValueField = ValueField(frame: NSRect(x: 18 + labelWidth, y: (22-16)/2, width: rowView.frame.width - labelWidth - 18, height: 16), value)
|
|
|
|
rowView.addSubview(colorView)
|
|
rowView.addSubview(labelView)
|
|
rowView.addSubview(valueView)
|
|
|
|
if let view = view as? NSStackView {
|
|
rowView.heightAnchor.constraint(equalToConstant: rowView.bounds.height).isActive = true
|
|
view.addArrangedSubview(rowView)
|
|
} else {
|
|
view.addSubview(rowView)
|
|
}
|
|
|
|
return valueView
|
|
}
|
|
|
|
public extension Array where Element: Equatable {
|
|
func allEqual() -> Bool {
|
|
if let firstElem = first {
|
|
return !dropFirst().contains { $0 != firstElem }
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
public extension Array where Element: Hashable {
|
|
func difference(from other: [Element]) -> [Element] {
|
|
let thisSet = Set(self)
|
|
let otherSet = Set(other)
|
|
return Array(thisSet.symmetricDifference(otherSet))
|
|
}
|
|
}
|
|
|
|
public func findAndToggleNSControlState(_ view: NSView?, state: NSControl.StateValue) {
|
|
if let control = view?.subviews.first(where: { $0 is NSControl }) {
|
|
toggleNSControlState(control as? NSControl, state: state)
|
|
}
|
|
}
|
|
|
|
public func findAndToggleEnableNSControlState(_ view: NSView?, state: Bool) {
|
|
if let control = view?.subviews.first(where: { $0 is NSControl }) {
|
|
toggleEnableNSControlState(control as? NSControl, state: state)
|
|
}
|
|
}
|
|
|
|
public func toggleNSControlState(_ control: NSControl?, state: NSControl.StateValue) {
|
|
if #available(OSX 10.15, *) {
|
|
if let checkbox = control as? NSSwitch {
|
|
checkbox.state = state
|
|
}
|
|
} else {
|
|
if let checkbox = control as? NSButton {
|
|
checkbox.state = state
|
|
}
|
|
}
|
|
}
|
|
|
|
public func toggleEnableNSControlState(_ control: NSControl?, state: Bool) {
|
|
if #available(OSX 10.15, *) {
|
|
if let checkbox = control as? NSSwitch {
|
|
checkbox.isEnabled = state
|
|
}
|
|
} else {
|
|
if let checkbox = control as? NSButton {
|
|
checkbox.isEnabled = state
|
|
}
|
|
}
|
|
}
|
|
|
|
public func dialogOKCancel(question: String, text: String) {
|
|
let alert = NSAlert()
|
|
alert.messageText = question
|
|
alert.informativeText = text
|
|
alert.alertStyle = .warning
|
|
alert.addButton(withTitle: "OK")
|
|
alert.addButton(withTitle: "Cancel")
|
|
alert.runModal()
|
|
}
|
|
|
|
public func asyncShell(_ args: String) {
|
|
let task = Process()
|
|
task.launchPath = "/bin/sh"
|
|
task.arguments = ["-c", args]
|
|
let pipe = Pipe()
|
|
task.standardOutput = pipe
|
|
task.launch()
|
|
}
|
|
|
|
public func syncShell(_ args: String) -> String {
|
|
let task = Process()
|
|
task.launchPath = "/bin/sh"
|
|
task.arguments = ["-c", args]
|
|
let pipe = Pipe()
|
|
|
|
task.standardOutput = pipe
|
|
task.launch()
|
|
task.waitUntilExit()
|
|
|
|
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
|
let output = String(data: data, encoding: .utf8)!
|
|
|
|
return output
|
|
}
|
|
|
|
public func isNewestVersion(currentVersion: String, latestVersion: String) -> Bool {
|
|
let currentNumber = currentVersion.replacingOccurrences(of: "v", with: "")
|
|
let latestNumber = latestVersion.replacingOccurrences(of: "v", with: "")
|
|
|
|
let currentArray = currentNumber.condenseWhitespace().split(separator: ".")
|
|
let latestArray = latestNumber.condenseWhitespace().split(separator: ".")
|
|
|
|
var current = Version(major: Int(currentArray[0]) ?? 0, minor: Int(currentArray[1]) ?? 0, patch: Int(currentArray[2]) ?? 0)
|
|
var latest = Version(major: Int(latestArray[0]) ?? 0, minor: Int(latestArray[1]) ?? 0, patch: Int(latestArray[2]) ?? 0)
|
|
|
|
if let patch = currentArray.last, patch.contains("-") {
|
|
let arr = patch.split(separator: "-")
|
|
if let patchNumber = arr.first {
|
|
current.patch = Int(patchNumber) ?? 0
|
|
}
|
|
if let beta = arr.last {
|
|
current.beta = Int(beta.replacingOccurrences(of: "beta", with: "")) ?? 0
|
|
}
|
|
}
|
|
|
|
if let patch = latestArray.last, patch.contains("-") {
|
|
let arr = patch.split(separator: "-")
|
|
if let patchNumber = arr.first {
|
|
latest.patch = Int(patchNumber) ?? 0
|
|
}
|
|
if let beta = arr.last {
|
|
latest.beta = Int(beta.replacingOccurrences(of: "beta", with: "")) ?? 0
|
|
}
|
|
}
|
|
|
|
// current is not beta + latest is not beta
|
|
if current.beta == nil && latest.beta == nil {
|
|
if latest.major > current.major {
|
|
return true
|
|
}
|
|
|
|
if latest.minor > current.minor && latest.major >= current.major {
|
|
return true
|
|
}
|
|
|
|
if latest.patch > current.patch && latest.minor >= current.minor && latest.major >= current.major {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// current version is beta + last version is not beta
|
|
if current.beta != nil && latest.beta == nil {
|
|
if latest.major > current.major {
|
|
return true
|
|
}
|
|
|
|
if latest.minor > current.minor && latest.major >= current.major {
|
|
return true
|
|
}
|
|
|
|
if latest.patch >= current.patch && latest.minor >= current.minor && latest.major >= current.major {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// current version is beta + last version is beta
|
|
if current.beta != nil && latest.beta != nil {
|
|
if latest.major > current.major {
|
|
return true
|
|
}
|
|
|
|
if latest.minor > current.minor && latest.major >= current.major {
|
|
return true
|
|
}
|
|
|
|
if latest.patch >= current.patch && latest.minor >= current.minor && latest.major >= current.major {
|
|
return true
|
|
}
|
|
|
|
if latest.beta! > current.beta! && latest.patch >= current.patch && latest.minor >= current.minor && latest.major >= current.major {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
public func showNotification(title: String, subtitle: String? = nil, text: String? = nil, id: String = UUID().uuidString, icon: NSImage? = nil) -> NSUserNotification {
|
|
let notification = NSUserNotification()
|
|
|
|
notification.identifier = id
|
|
notification.title = title
|
|
notification.subtitle = subtitle
|
|
notification.informativeText = text
|
|
notification.soundName = NSUserNotificationDefaultSoundName
|
|
notification.hasActionButton = false
|
|
|
|
if icon != nil {
|
|
notification.setValue(icon, forKey: "_identityImage")
|
|
}
|
|
|
|
NSUserNotificationCenter.default.deliver(notification)
|
|
|
|
return notification
|
|
}
|
|
|
|
public struct TopProcess {
|
|
public var pid: Int
|
|
public var command: String
|
|
public var name: String?
|
|
public var usage: Double
|
|
public var icon: NSImage?
|
|
|
|
public init(pid: Int, command: String, name: String?, usage: Double, icon: NSImage?) {
|
|
self.pid = pid
|
|
self.command = command
|
|
self.name = name
|
|
self.usage = usage
|
|
self.icon = icon
|
|
}
|
|
}
|
|
|
|
public func getIOParent(_ obj: io_registry_entry_t) -> io_registry_entry_t? {
|
|
var parent: io_registry_entry_t = 0
|
|
|
|
if IORegistryEntryGetParentEntry(obj, kIOServicePlane, &parent) != KERN_SUCCESS {
|
|
return nil
|
|
}
|
|
|
|
if IOObjectConformsTo(parent, "IOBlockStorageDriver") == 0 {
|
|
IOObjectRelease(parent)
|
|
return nil
|
|
}
|
|
|
|
return parent
|
|
}
|
|
|
|
public func fetchIOService(_ name: String) -> [NSDictionary]? {
|
|
var iterator: io_iterator_t = io_iterator_t()
|
|
var obj: io_registry_entry_t = 1
|
|
var list: [NSDictionary] = []
|
|
|
|
let result = IOServiceGetMatchingServices(kIOMasterPortDefault, IOServiceMatching(name), &iterator)
|
|
if result == kIOReturnSuccess {
|
|
while obj != 0 {
|
|
obj = IOIteratorNext(iterator)
|
|
if let props = getIOProperties(obj) {
|
|
list.append(props)
|
|
}
|
|
IOObjectRelease(obj)
|
|
}
|
|
IOObjectRelease(iterator)
|
|
}
|
|
|
|
return list.isEmpty ? nil : list
|
|
}
|
|
|
|
public func getIOProperties(_ entry: io_registry_entry_t) -> NSDictionary? {
|
|
var properties: Unmanaged<CFMutableDictionary>? = nil
|
|
|
|
if IORegistryEntryCreateCFProperties(entry, &properties, kCFAllocatorDefault, 0) != kIOReturnSuccess {
|
|
return nil
|
|
}
|
|
|
|
defer {
|
|
properties?.release()
|
|
}
|
|
|
|
return properties?.takeUnretainedValue()
|
|
}
|
|
|
|
public class ColorView: NSView {
|
|
public var inactiveColor: NSColor = NSColor.lightGray.withAlphaComponent(0.75)
|
|
|
|
private let color: NSColor
|
|
private var state: Bool
|
|
|
|
public init(frame: NSRect, color: NSColor, state: Bool = false, radius: CGFloat = 2) {
|
|
self.color = color
|
|
self.state = state
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.wantsLayer = true
|
|
self.layer?.backgroundColor = (state ? self.color : inactiveColor).cgColor
|
|
self.layer?.cornerRadius = radius
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
public func setState(_ newState: Bool) {
|
|
if newState != state {
|
|
self.layer?.backgroundColor = (newState ? self.color : inactiveColor).cgColor
|
|
self.state = newState
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct Log: TextOutputStream {
|
|
public static var log: Log = Log()
|
|
|
|
public func write(_ string: String) {
|
|
let fm = FileManager.default
|
|
let log = fm.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("log.txt")
|
|
if let handle = try? FileHandle(forWritingTo: log) {
|
|
handle.seekToEndOfFile()
|
|
handle.write(string.data(using: .utf8)!)
|
|
handle.closeFile()
|
|
} else {
|
|
try? string.data(using: .utf8)?.write(to: log)
|
|
}
|
|
}
|
|
}
|
|
|
|
public func localizedString(_ key: String, _ params: String..., comment: String = "") -> String {
|
|
var string = NSLocalizedString(key, comment: comment)
|
|
if !params.isEmpty {
|
|
for (index, param) in params.enumerated() {
|
|
string = string.replacingOccurrences(of: "%\(index)", with: param)
|
|
}
|
|
}
|
|
return string
|
|
}
|
|
|
|
extension UnitTemperature {
|
|
static var current: UnitTemperature {
|
|
let measureFormatter = MeasurementFormatter()
|
|
let measurement = Measurement(value: 0, unit: UnitTemperature.celsius)
|
|
return measureFormatter.string(from: measurement).hasSuffix("C") ? .celsius : .fahrenheit
|
|
}
|
|
}
|
|
|
|
// swiftlint:disable identifier_name
|
|
public func Temperature(_ value: Double) -> String {
|
|
let stringUnit: String = Store.shared.string(key: "temperature_units", defaultValue: "system")
|
|
let formatter = MeasurementFormatter()
|
|
formatter.locale = Locale.init(identifier: "en_US")
|
|
formatter.numberFormatter.maximumFractionDigits = 0
|
|
formatter.unitOptions = .providedUnit
|
|
|
|
var measurement = Measurement(value: value, unit: UnitTemperature.celsius)
|
|
if stringUnit == "system" {
|
|
measurement.convert(to: UnitTemperature.current)
|
|
} else {
|
|
if let temperatureUnit = TemperatureUnits.first(where: { $0.key == stringUnit }) {
|
|
if let unit = temperatureUnit.additional as? UnitTemperature {
|
|
measurement.convert(to: unit)
|
|
}
|
|
}
|
|
}
|
|
|
|
return formatter.string(from: measurement)
|
|
}
|
|
|
|
public func sysctlByName(_ name: String) -> Int64 {
|
|
var num: Int64 = 0
|
|
var size = MemoryLayout<Int64>.size
|
|
|
|
if sysctlbyname(name, &num, &size, nil, 0) != 0 {
|
|
print(POSIXError.Code(rawValue: errno).map { POSIXError($0) } ?? CocoaError(.fileReadUnknown))
|
|
}
|
|
|
|
return num
|
|
}
|
|
|
|
public class ProcessView: NSView {
|
|
public var width: CGFloat {
|
|
get { return 0 }
|
|
set {
|
|
self.setFrameSize(NSSize(width: newValue, height: self.frame.height))
|
|
}
|
|
}
|
|
|
|
public var icon: NSImage? {
|
|
get { return NSImage() }
|
|
set {
|
|
self.imageView?.image = newValue
|
|
}
|
|
}
|
|
public var label: String {
|
|
get { return "" }
|
|
set {
|
|
self.labelView?.stringValue = newValue
|
|
}
|
|
}
|
|
public var value: String {
|
|
get { return "" }
|
|
set {
|
|
self.valueView?.stringValue = newValue
|
|
}
|
|
}
|
|
|
|
private var imageView: NSImageView? = nil
|
|
private var labelView: LabelField? = nil
|
|
private var valueView: ValueField? = nil
|
|
|
|
public init(_ n: CGFloat) {
|
|
super.init(frame: NSRect(x: 0, y: n*22, width: 264, height: 16))
|
|
|
|
let rowView: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 16))
|
|
|
|
let imageView: NSImageView = NSImageView(frame: NSRect(x: 2, y: 2, width: 12, height: 12))
|
|
let labelView: LabelField = LabelField(frame: NSRect(x: 18, y: 0.5, width: rowView.frame.width - 70 - 18, height: 15), "")
|
|
let valueView: ValueField = ValueField(frame: NSRect(x: 18 + labelView.frame.width, y: 0, width: 70, height: 16), "")
|
|
|
|
rowView.addSubview(imageView)
|
|
rowView.addSubview(labelView)
|
|
rowView.addSubview(valueView)
|
|
|
|
self.imageView = imageView
|
|
self.labelView = labelView
|
|
self.valueView = valueView
|
|
|
|
self.addSubview(rowView)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
public func attachProcess(_ process: TopProcess) {
|
|
self.label = process.name != nil ? process.name! : process.command
|
|
self.icon = process.icon
|
|
self.toolTip = "pid: \(process.pid)"
|
|
}
|
|
|
|
public func clear() {
|
|
self.label = ""
|
|
self.value = ""
|
|
self.icon = nil
|
|
self.toolTip = ""
|
|
}
|
|
}
|
|
|
|
public class CAText: CATextLayer {
|
|
public init(fontSize: CGFloat = 12, weight: NSFont.Weight = .regular) {
|
|
super.init()
|
|
|
|
self.font = NSFont.systemFont(ofSize: fontSize, weight: weight)
|
|
self.fontSize = fontSize
|
|
|
|
self.allowsFontSubpixelQuantization = true
|
|
self.contentsScale = NSScreen.main?.backingScaleFactor ?? 1
|
|
self.rasterizationScale = NSScreen.main?.backingScaleFactor ?? 1
|
|
|
|
self.foregroundColor = NSColor.textColor.cgColor
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
public override init(layer: Any) {
|
|
super.init(layer: layer)
|
|
}
|
|
|
|
public func getWidth(add: CGFloat = 0) -> CGFloat {
|
|
let value = self.string as? String ?? ""
|
|
return value.widthOfString(usingFont: self.font as! NSFont).rounded(.up) + add
|
|
}
|
|
}
|
|
|
|
public class WidgetLabelView: NSView {
|
|
private var title: String
|
|
|
|
public init(_ title: String, height: CGFloat) {
|
|
self.title = title
|
|
|
|
super.init(frame: NSRect(
|
|
x: 0,
|
|
y: 0,
|
|
width: 6,
|
|
height: height
|
|
))
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
public override func draw(_ dirtyRect: NSRect) {
|
|
super.draw(dirtyRect)
|
|
|
|
let style = NSMutableParagraphStyle()
|
|
style.alignment = .center
|
|
let stringAttributes = [
|
|
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 7, weight: .regular),
|
|
NSAttributedString.Key.foregroundColor: NSColor.textColor,
|
|
NSAttributedString.Key.paragraphStyle: style
|
|
]
|
|
|
|
let title = self.title.prefix(3)
|
|
let letterHeight = self.frame.height / 3
|
|
let letterWidth: CGFloat = self.frame.height / CGFloat(title.count)
|
|
|
|
var yMargin: CGFloat = 0
|
|
for char in title.uppercased().reversed() {
|
|
let rect = CGRect(x: 0, y: yMargin, width: letterWidth, height: letterHeight-1)
|
|
let str = NSAttributedString.init(string: "\(char)", attributes: stringAttributes)
|
|
str.draw(with: rect)
|
|
yMargin += letterHeight
|
|
}
|
|
}
|
|
}
|
|
|
|
public func isRoot() -> Bool {
|
|
return getuid() == 0
|
|
}
|
|
|
|
public func ensureRoot() {
|
|
if isRoot() {
|
|
return
|
|
}
|
|
|
|
let pwd = Bundle.main.bundleURL.absoluteString.replacingOccurrences(of: "file://", with: "")
|
|
guard let script = NSAppleScript(source: "do shell script \"\(pwd)/Contents/MacOS/Stats > /dev/null 2>&1 &\" with administrator privileges") else {
|
|
return
|
|
}
|
|
|
|
var err: NSDictionary? = nil
|
|
script.executeAndReturnError(&err)
|
|
|
|
if err != nil {
|
|
print("cannot run script as root: \(String(describing: err))")
|
|
return
|
|
}
|
|
|
|
NSApp.terminate(nil)
|
|
return
|
|
}
|
|
|
|
public func process(path: String, arguments: [String]) -> String? {
|
|
let task = Process()
|
|
task.launchPath = path
|
|
task.arguments = arguments
|
|
|
|
let outputPipe = Pipe()
|
|
defer {
|
|
outputPipe.fileHandleForReading.closeFile()
|
|
}
|
|
task.standardOutput = outputPipe
|
|
|
|
do {
|
|
try task.run()
|
|
} catch let error {
|
|
os_log(.error, log: .default, "system_profiler SPMemoryDataType: %s", "\(error.localizedDescription)")
|
|
return nil
|
|
}
|
|
|
|
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
|
|
let output = String(decoding: outputData, as: UTF8.self)
|
|
|
|
if output.isEmpty {
|
|
return nil
|
|
}
|
|
|
|
return output
|
|
}
|
|
|
|
public class SettingsContainerView: NSStackView {
|
|
public init(width: CGFloat) {
|
|
super.init(frame: NSRect(
|
|
x: Constants.Settings.margin,
|
|
y: 0,
|
|
width: width - (Constants.Settings.margin*2),
|
|
height: 0
|
|
))
|
|
|
|
self.orientation = .vertical
|
|
self.distribution = .gravityAreas
|
|
self.edgeInsets = NSEdgeInsets(
|
|
top: Constants.Settings.margin,
|
|
left: Constants.Settings.margin,
|
|
bottom: Constants.Settings.margin,
|
|
right: Constants.Settings.margin
|
|
)
|
|
self.spacing = Constants.Settings.margin
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
private func resize() {
|
|
let h = self.arrangedSubviews.map({ $0.bounds.height + self.spacing }).reduce(0, +) - self.spacing + self.edgeInsets.top + self.edgeInsets.bottom
|
|
if self.frame.size.height != h {
|
|
self.setFrameSize(NSSize(width: self.bounds.width, height: h))
|
|
}
|
|
}
|
|
|
|
public override func addArrangedSubview(_ view: NSView) {
|
|
super.addArrangedSubview(view)
|
|
self.resize()
|
|
}
|
|
}
|