mirror of
https://github.com/morgan9e/macos-stats
synced 2026-04-14 00:04:15 +09:00
591 lines
19 KiB
Swift
591 lines
19 KiB
Swift
//
|
|
// extensions.swift
|
|
// StatsKit
|
|
//
|
|
// Created by Serhiy Mytrovtsiy on 10/04/2020.
|
|
// Using Swift 5.0.
|
|
// Running on macOS 10.15.
|
|
//
|
|
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
|
//
|
|
|
|
import Cocoa
|
|
|
|
public enum Unit : Float {
|
|
case byte = 1
|
|
case kilobyte = 1024
|
|
case megabyte = 1048576
|
|
case gigabyte = 1073741824
|
|
}
|
|
|
|
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() -> (String, String) {
|
|
switch bytes {
|
|
case 0..<1_024:
|
|
return ("0", "KB/s")
|
|
case 1_024..<(1_024 * 1_024):
|
|
return (String(format: "%.0f", kilobytes), "KB/s")
|
|
case 1_024..<(1_024 * 1_024 * 100):
|
|
return (String(format: "%.1f", megabytes), "MB/s")
|
|
case (1_024 * 1_024 * 100)..<(1_024 * 1_024 * 1_024):
|
|
return (String(format: "%.0f", megabytes), "MB/s")
|
|
case (1_024 * 1_024 * 1_024)...Int64.max:
|
|
return (String(format: "%.1f", gigabytes), "GB/s")
|
|
default:
|
|
return (String(format: "%.0f", kilobytes), "KB/s")
|
|
}
|
|
}
|
|
|
|
public func getReadableSpeed() -> String {
|
|
switch bytes {
|
|
case 0..<1_024:
|
|
return "0 KB/s"
|
|
case 1_024..<(1_024 * 1_024):
|
|
return String(format: "%.0f KB/s", kilobytes)
|
|
case 1_024..<(1_024 * 1_024 * 100):
|
|
return String(format: "%.1f MB/s", megabytes)
|
|
case (1_024 * 1_024 * 100)..<(1_024 * 1_024 * 1_024):
|
|
return String(format: "%.0f MB/s", megabytes)
|
|
case (1_024 * 1_024 * 1_024)...Int64.max:
|
|
return String(format: "%.1f GB/s", gigabytes)
|
|
default:
|
|
return String(format: "%.0f KB/s", kilobytes)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension String: LocalizedError {
|
|
public var errorDescription: String? { return self }
|
|
|
|
public func widthOfString(usingFont font: NSFont) -> CGFloat {
|
|
let fontAttributes = [NSAttributedString.Key.font: font]
|
|
let size = self.size(withAttributes: fontAttributes)
|
|
return size.width
|
|
}
|
|
|
|
public func heightOfString(usingFont font: NSFont) -> CGFloat {
|
|
let fontAttributes = [NSAttributedString.Key.font: font]
|
|
let size = self.size(withAttributes: fontAttributes)
|
|
return size.height
|
|
}
|
|
|
|
public func sizeOfString(usingFont font: NSFont) -> CGSize {
|
|
let fontAttributes = [NSAttributedString.Key.font: font]
|
|
return self.size(withAttributes: fontAttributes)
|
|
}
|
|
|
|
public func condenseWhitespace() -> String {
|
|
let components = self.components(separatedBy: .whitespacesAndNewlines)
|
|
return components.filter { !$0.isEmpty }.joined(separator: " ")
|
|
}
|
|
|
|
public mutating func findAndCrop(pattern: String) -> String {
|
|
let regex = try! NSRegularExpression(pattern: pattern)
|
|
let stringRange = NSRange(location: 0, length: self.utf16.count)
|
|
var line = self
|
|
|
|
if let searchRange = regex.firstMatch(in: self, options: [], range: stringRange) {
|
|
let start = self.index(self.startIndex, offsetBy: searchRange.range.lowerBound)
|
|
let end = self.index(self.startIndex, offsetBy: searchRange.range.upperBound)
|
|
let value = String(self[start..<end]).trimmingCharacters(in: .whitespaces)
|
|
line = self.replacingOccurrences(
|
|
of: value,
|
|
with: "",
|
|
options: .regularExpression
|
|
)
|
|
self = line.trimmingCharacters(in: .whitespaces)
|
|
return value.trimmingCharacters(in: .whitespaces)
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
public var trimmed: String {
|
|
var buf = [UInt8]()
|
|
var trimming = true
|
|
for c in self.utf8 {
|
|
if trimming && c < 33 { continue }
|
|
trimming = false
|
|
buf.append(c)
|
|
}
|
|
|
|
while let last = buf.last, last < 33 {
|
|
buf.removeLast()
|
|
}
|
|
|
|
buf.append(0)
|
|
return String(cString: buf)
|
|
}
|
|
}
|
|
|
|
public extension Int {
|
|
func pressureColor() -> NSColor {
|
|
switch self {
|
|
case 1:
|
|
return NSColor.systemGreen
|
|
case 2:
|
|
return NSColor.systemYellow
|
|
case 3:
|
|
return NSColor.systemRed
|
|
default:
|
|
return NSColor.controlAccentColor
|
|
}
|
|
}
|
|
}
|
|
|
|
extension Float {
|
|
init?(_ bytes: [UInt8]) {
|
|
self = bytes.withUnsafeBytes {
|
|
return $0.load(fromByteOffset: 0, as: Self.self)
|
|
}
|
|
}
|
|
}
|
|
|
|
public extension Double {
|
|
func roundTo(decimalPlaces: Int) -> String {
|
|
return NSString(format: "%.\(decimalPlaces)f" as NSString, self) as String
|
|
}
|
|
|
|
func rounded(toPlaces places:Int) -> Double {
|
|
let divisor = pow(10.0, Double(places))
|
|
return (self * divisor).rounded() / divisor
|
|
}
|
|
|
|
func usageColor(reversed: Bool = false) -> NSColor {
|
|
let firstColor: NSColor = NSColor.controlAccentColor
|
|
let secondColor: NSColor = NSColor.orange
|
|
let thirdColor: NSColor = NSColor.red
|
|
|
|
if reversed {
|
|
switch self {
|
|
case 0.6...0.8:
|
|
return secondColor
|
|
case 0.8...1:
|
|
return firstColor
|
|
default:
|
|
return thirdColor
|
|
}
|
|
} else {
|
|
switch self {
|
|
case 0.6...0.8:
|
|
return secondColor
|
|
case 0.8...1:
|
|
return thirdColor
|
|
default:
|
|
return firstColor
|
|
}
|
|
}
|
|
}
|
|
|
|
func percentageColor(color: Bool) -> NSColor {
|
|
if !color {
|
|
return NSColor.textColor
|
|
}
|
|
|
|
switch self {
|
|
case 0.6...0.8:
|
|
return NSColor.systemOrange
|
|
case 0.8...1:
|
|
return NSColor.systemRed
|
|
default:
|
|
return NSColor.systemGreen
|
|
}
|
|
}
|
|
|
|
func batteryColor(color: Bool = false) -> NSColor {
|
|
switch self {
|
|
case 0.2...0.4:
|
|
if !color {
|
|
return NSColor.textColor
|
|
}
|
|
return NSColor.systemOrange
|
|
case 0.4...1:
|
|
if self == 1 {
|
|
return NSColor.textColor
|
|
}
|
|
if !color {
|
|
return NSColor.textColor
|
|
}
|
|
return NSColor.systemGreen
|
|
default:
|
|
return NSColor.systemRed
|
|
}
|
|
}
|
|
|
|
func secondsToHoursMinutesSeconds () -> (Int?, Int?, Int?) {
|
|
let hrs = self / 3600
|
|
let mins = (self.truncatingRemainder(dividingBy: 3600)) / 60
|
|
let seconds = (self.truncatingRemainder(dividingBy:3600)).truncatingRemainder(dividingBy:60)
|
|
return (Int(hrs) > 0 ? Int(hrs) : nil , Int(mins) > 0 ? Int(mins) : nil, Int(seconds) > 0 ? Int(seconds) : nil)
|
|
}
|
|
|
|
func printSecondsToHoursMinutesSeconds () -> String {
|
|
let time = self.secondsToHoursMinutesSeconds()
|
|
|
|
switch time {
|
|
case (nil, let x? , let y?):
|
|
return "\(x)min \(y)sec"
|
|
case (nil, let x?, nil):
|
|
return "\(x)min"
|
|
case (let x?, nil, nil):
|
|
return "\(x)h"
|
|
case (nil, nil, let x?):
|
|
return "\(x)sec"
|
|
case (let x?, nil, let z?):
|
|
return "\(x)h \(z)sec"
|
|
case (let x?, let y?, nil):
|
|
return "\(x)h \(y)min"
|
|
case (let x?, let y?, let z?):
|
|
return "\(x)h \(y)min \(z)sec"
|
|
default:
|
|
return "n/a"
|
|
}
|
|
}
|
|
|
|
func localizeTemperature() -> Measurement<UnitTemperature> {
|
|
let locale = NSLocale.current as NSLocale
|
|
var unit = UnitTemperature.celsius
|
|
if let unitLocale = locale.object(forKey: NSLocale.Key(rawValue: "kCFLocaleTemperatureUnitKey")) {
|
|
unit = "\(unitLocale)" == "Celsius" ? UnitTemperature.celsius : UnitTemperature.fahrenheit
|
|
}
|
|
let measurement = Measurement(value: self, unit: unit)
|
|
|
|
return measurement
|
|
}
|
|
}
|
|
|
|
public extension NSView {
|
|
var isDarkMode: Bool {
|
|
if #available(OSX 10.14, *) {
|
|
switch effectiveAppearance.name {
|
|
case .darkAqua, .vibrantDark, .accessibilityHighContrastDarkAqua, .accessibilityHighContrastVibrantDark:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
} else {
|
|
switch effectiveAppearance.name {
|
|
case .vibrantDark:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
func ToggleTitleRow(frame: NSRect, title: String, action: Selector, state: Bool) -> NSView {
|
|
let row: NSView = NSView(frame: frame)
|
|
let state: NSControl.StateValue = state ? .on : .off
|
|
|
|
let rowTitle: NSTextField = LabelField(frame: NSRect(x: 0, y: (row.frame.height - 16)/2, width: row.frame.width - 52, height: 17), title)
|
|
rowTitle.font = NSFont.systemFont(ofSize: 13, weight: .light)
|
|
rowTitle.textColor = .textColor
|
|
|
|
var toggle: NSControl = NSControl()
|
|
if #available(OSX 10.15, *) {
|
|
let switchButton = NSSwitch(frame: NSRect(x: row.frame.width - 50, y: 0, width: 50, height: row.frame.height))
|
|
switchButton.state = state
|
|
switchButton.action = action
|
|
switchButton.target = self
|
|
|
|
toggle = switchButton
|
|
} else {
|
|
let button: NSButton = NSButton(frame: NSRect(x: row.frame.width - 30, y: 0, width: 30, height: row.frame.height))
|
|
button.setButtonType(.switch)
|
|
button.state = state
|
|
button.title = ""
|
|
button.action = action
|
|
button.isBordered = false
|
|
button.isTransparent = true
|
|
button.target = self
|
|
|
|
toggle = button
|
|
}
|
|
|
|
row.addSubview(toggle)
|
|
row.addSubview(rowTitle)
|
|
|
|
return row
|
|
}
|
|
|
|
func SelectTitleRow(frame: NSRect, title: String, action: Selector, items: [String], selected: String) -> NSView {
|
|
let row: NSView = NSView(frame: frame)
|
|
|
|
let rowTitle: NSTextField = LabelField(frame: NSRect(x: 0, y: (row.frame.height - 16)/2, width: row.frame.width - 52, height: 17), title)
|
|
rowTitle.font = NSFont.systemFont(ofSize: 13, weight: .light)
|
|
rowTitle.textColor = .textColor
|
|
|
|
let select: NSPopUpButton = NSPopUpButton(frame: NSRect(x: row.frame.width - 50, y: 0, width: 50, height: row.frame.height))
|
|
select.target = self
|
|
select.action = action
|
|
select.addItems(withTitles: items)
|
|
select.selectItem(withTitle: selected)
|
|
select.sizeToFit()
|
|
|
|
rowTitle.setFrameSize(NSSize(width: row.frame.width - select.frame.width, height: rowTitle.frame.height))
|
|
select.setFrameOrigin(NSPoint(x: row.frame.width - select.frame.width, y: 0))
|
|
|
|
row.addSubview(select)
|
|
row.addSubview(rowTitle)
|
|
|
|
return row
|
|
}
|
|
}
|
|
|
|
public extension Notification.Name {
|
|
static let toggleSettings = Notification.Name("toggleSettings")
|
|
static let toggleModule = Notification.Name("toggleModule")
|
|
static let openSettingsView = Notification.Name("openSettingsView")
|
|
static let switchWidget = Notification.Name("switchWidget")
|
|
static let checkForUpdates = Notification.Name("checkForUpdates")
|
|
static let clickInSettings = Notification.Name("clickInSettings")
|
|
}
|
|
|
|
public class NSButtonWithPadding: NSButton {
|
|
public var horizontalPadding: CGFloat = 0
|
|
public var verticalPadding: CGFloat = 0
|
|
|
|
public override var intrinsicContentSize: NSSize {
|
|
var size = super.intrinsicContentSize
|
|
size.width += self.horizontalPadding
|
|
size.height += self.verticalPadding
|
|
return size;
|
|
}
|
|
}
|
|
|
|
public class TextView: NSTextField {
|
|
public override init(frame: NSRect) {
|
|
super.init(frame: frame)
|
|
|
|
self.isEditable = false
|
|
self.isSelectable = false
|
|
self.isBezeled = false
|
|
self.wantsLayer = true
|
|
self.textColor = .labelColor
|
|
self.backgroundColor = .clear
|
|
self.canDrawSubviewsIntoLayer = true
|
|
self.alignment = .natural
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|
|
|
|
public extension OperatingSystemVersion {
|
|
func getFullVersion(separator: String = ".") -> String {
|
|
return "\(majorVersion)\(separator)\(minorVersion)\(separator)\(patchVersion)"
|
|
}
|
|
}
|
|
|
|
extension URL {
|
|
func checkFileExist() -> Bool {
|
|
return FileManager.default.fileExists(atPath: self.path)
|
|
}
|
|
}
|
|
|
|
extension UInt32 {
|
|
init(bytes: (UInt8, UInt8, UInt8, UInt8)) {
|
|
self = UInt32(bytes.0) << 24 | UInt32(bytes.1) << 16 | UInt32(bytes.2) << 8 | UInt32(bytes.3)
|
|
}
|
|
}
|
|
|
|
extension UInt16 {
|
|
init(bytes: (UInt8, UInt8)) {
|
|
self = UInt16(bytes.0) << 8 | UInt16(bytes.1)
|
|
}
|
|
}
|
|
|
|
extension FourCharCode {
|
|
init(fromString str: String) {
|
|
precondition(str.count == 4)
|
|
|
|
self = str.utf8.reduce(0) { sum, character in
|
|
return sum << 8 | UInt32(character)
|
|
}
|
|
}
|
|
|
|
func toString() -> String {
|
|
return String(describing: UnicodeScalar(self >> 24 & 0xff)!) +
|
|
String(describing: UnicodeScalar(self >> 16 & 0xff)!) +
|
|
String(describing: UnicodeScalar(self >> 8 & 0xff)!) +
|
|
String(describing: UnicodeScalar(self & 0xff)!)
|
|
}
|
|
}
|
|
|
|
public extension NSColor {
|
|
convenience init(hexString: String, alpha: CGFloat = 1.0) {
|
|
let hexString: String = hexString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
|
let scanner = Scanner(string: hexString)
|
|
if (hexString.hasPrefix("#")) {
|
|
scanner.scanLocation = 1
|
|
}
|
|
var color: UInt32 = 0
|
|
scanner.scanHexInt32(&color)
|
|
let mask = 0x000000FF
|
|
let r = Int(color >> 16) & mask
|
|
let g = Int(color >> 8) & mask
|
|
let b = Int(color) & mask
|
|
let red = CGFloat(r) / 255.0
|
|
let green = CGFloat(g) / 255.0
|
|
let blue = CGFloat(b) / 255.0
|
|
self.init(red:red, green:green, blue:blue, alpha:alpha)
|
|
}
|
|
|
|
func toHexString() -> String {
|
|
var r:CGFloat = 0
|
|
var g:CGFloat = 0
|
|
var b:CGFloat = 0
|
|
var a:CGFloat = 0
|
|
getRed(&r, green: &g, blue: &b, alpha: &a)
|
|
let rgb:Int = (Int)(r*255)<<16 | (Int)(g*255)<<8 | (Int)(b*255)<<0
|
|
return String(format:"#%06x", rgb)
|
|
}
|
|
}
|
|
|
|
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))
|
|
|
|
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, title: String, value: String) -> 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)) + 5
|
|
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-16)/2, width: rowView.frame.width - labelWidth, height: 16), value)
|
|
|
|
rowView.addSubview(labelView)
|
|
rowView.addSubview(valueView)
|
|
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 func ToggleNSControlState(_ view: NSView?, state: NSControl.StateValue) {
|
|
if let control = view?.subviews.first(where: { $0 is NSControl }) {
|
|
if #available(OSX 10.15, *) {
|
|
if let checkbox = control as? NSSwitch {
|
|
checkbox.state = state
|
|
}
|
|
} else {
|
|
if let checkbox = control as? NSButton {
|
|
checkbox.state = state
|
|
}
|
|
}
|
|
}
|
|
}
|