mirror of
https://github.com/morgan9e/macos-stats
synced 2026-04-14 00:04:15 +09:00
- add an option to select a base (byte or bit) for speed (87)
This commit is contained in:
@@ -25,6 +25,7 @@ public class SpeedWidget: Widget {
|
||||
private var icon: speed_icon_t = .dot
|
||||
private var state: Bool = false
|
||||
private var valueState: Bool = true
|
||||
private var baseValue: String = "byte"
|
||||
|
||||
private var symbols: [String] = ["U", "D"]
|
||||
|
||||
@@ -57,6 +58,7 @@ public class SpeedWidget: Widget {
|
||||
if self.store != nil {
|
||||
self.valueState = store!.pointee.bool(key: "\(self.title)_\(self.type.rawValue)_value", defaultValue: self.valueState)
|
||||
self.icon = speed_icon_t(rawValue: store!.pointee.string(key: "\(self.title)_\(self.type.rawValue)_icon", defaultValue: self.icon.rawValue)) ?? self.icon
|
||||
self.baseValue = store!.pointee.string(key: "\(self.title)_base", defaultValue: self.baseValue)
|
||||
}
|
||||
|
||||
if self.valueState && self.icon != .none {
|
||||
@@ -103,12 +105,13 @@ public class SpeedWidget: Widget {
|
||||
NSAttributedString.Key.paragraphStyle: style
|
||||
]
|
||||
|
||||
let base: DataSizeBase = DataSizeBase(rawValue: self.baseValue) ?? .byte
|
||||
var rect = CGRect(x: Constants.Widget.margin + x, y: 1, width: rowWidth - (Constants.Widget.margin*2), height: rowHeight)
|
||||
let download = NSAttributedString.init(string: Units(bytes: self.downloadValue).getReadableSpeed(), attributes: stringAttributes)
|
||||
let download = NSAttributedString.init(string: Units(bytes: self.downloadValue).getReadableSpeed(base: base), attributes: stringAttributes)
|
||||
download.draw(with: rect)
|
||||
|
||||
rect = CGRect(x: Constants.Widget.margin + x, y: rect.height+1, width: rowWidth - (Constants.Widget.margin*2), height: rowHeight)
|
||||
let upload = NSAttributedString.init(string: Units(bytes: self.uploadValue).getReadableSpeed(), attributes: stringAttributes)
|
||||
let upload = NSAttributedString.init(string: Units(bytes: self.uploadValue).getReadableSpeed(base: base), attributes: stringAttributes)
|
||||
upload.draw(with: rect)
|
||||
|
||||
width += rowWidth
|
||||
@@ -206,20 +209,33 @@ public class SpeedWidget: Widget {
|
||||
}
|
||||
|
||||
public override func settings(superview: NSView) {
|
||||
let height: CGFloat = 60 + (Constants.Settings.margin*3)
|
||||
let height: CGFloat = 90 + (Constants.Settings.margin*4)
|
||||
let rowHeight: CGFloat = 30
|
||||
superview.setFrameSize(NSSize(width: superview.frame.width, height: height))
|
||||
|
||||
let view: NSView = NSView(frame: NSRect(x: Constants.Settings.margin, y: Constants.Settings.margin, width: superview.frame.width - (Constants.Settings.margin*2), height: superview.frame.height - (Constants.Settings.margin*2)))
|
||||
let view: NSView = NSView(frame: NSRect(
|
||||
x: Constants.Settings.margin,
|
||||
y: Constants.Settings.margin,
|
||||
width: superview.frame.width - (Constants.Settings.margin*2),
|
||||
height: superview.frame.height - (Constants.Settings.margin*2)
|
||||
))
|
||||
|
||||
view.addSubview(SelectTitleRow(
|
||||
frame: NSRect(x: 0, y: rowHeight + Constants.Settings.margin, width: view.frame.width, height: rowHeight),
|
||||
frame: NSRect(x: 0, y: (rowHeight+Constants.Settings.margin) * 2, width: view.frame.width, height: rowHeight),
|
||||
title: LocalizedString("Pictogram"),
|
||||
action: #selector(toggleIcon),
|
||||
items: speed_icon_t.allCases.map{ return $0.rawValue },
|
||||
selected: self.icon.rawValue
|
||||
))
|
||||
|
||||
view.addSubview(SelectRow(
|
||||
frame: NSRect(x: 0, y: rowHeight + Constants.Settings.margin, width: view.frame.width, height: rowHeight),
|
||||
title: LocalizedString("Base"),
|
||||
action: #selector(toggleBase),
|
||||
items: SpeedBase,
|
||||
selected: self.baseValue
|
||||
))
|
||||
|
||||
view.addSubview(ToggleTitleRow(
|
||||
frame: NSRect(x: 0, y: 0, width: view.frame.width, height: rowHeight),
|
||||
title: LocalizedString("Value"),
|
||||
@@ -265,6 +281,14 @@ public class SpeedWidget: Widget {
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func toggleBase(_ sender: NSMenuItem) {
|
||||
guard let key = sender.representedObject as? String else {
|
||||
return
|
||||
}
|
||||
self.baseValue = key
|
||||
self.store?.pointee.set(key: "\(self.title)_base", value: self.baseValue)
|
||||
}
|
||||
|
||||
public func setValue(upload: Int64, download: Int64) {
|
||||
var updated: Bool = false
|
||||
|
||||
|
||||
@@ -69,11 +69,12 @@ public struct Network_Process {
|
||||
public class Network: Module {
|
||||
private var usageReader: UsageReader? = nil
|
||||
private var processReader: ProcessReader? = nil
|
||||
private let popupView: Popup = Popup()
|
||||
private var popupView: Popup? = nil
|
||||
private var settingsView: Settings
|
||||
|
||||
public init(_ store: UnsafePointer<Store>) {
|
||||
self.settingsView = Settings("Network", store: store)
|
||||
self.popupView = Popup(store: store, title: "Network")
|
||||
|
||||
super.init(
|
||||
store: store,
|
||||
@@ -96,7 +97,7 @@ public class Network: Module {
|
||||
|
||||
self.processReader?.callbackHandler = { [unowned self] value in
|
||||
if let list = value {
|
||||
self.popupView.processCallback(list)
|
||||
self.popupView?.processCallback(list)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +129,7 @@ public class Network: Module {
|
||||
return
|
||||
}
|
||||
|
||||
self.popupView.usageCallback(value!)
|
||||
self.popupView?.usageCallback(value!)
|
||||
if let widget = self.widget as? SpeedWidget {
|
||||
widget.setValue(upload: value!.upload, download: value!.download)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ import ModuleKit
|
||||
import StatsKit
|
||||
|
||||
internal class Popup: NSView {
|
||||
private var store: UnsafePointer<Store>? = nil
|
||||
private var title: String
|
||||
|
||||
private let dashboardHeight: CGFloat = 90
|
||||
private let chartHeight: CGFloat = 90
|
||||
private let detailsHeight: CGFloat = 110
|
||||
@@ -45,7 +48,16 @@ internal class Popup: NSView {
|
||||
private var chart: NetworkChartView? = nil
|
||||
private var processes: [NetworkProcessView] = []
|
||||
|
||||
public init() {
|
||||
private var base: String {
|
||||
get {
|
||||
return store?.pointee.string(key: "\(self.title)_base", defaultValue: "byte") ?? "byte"
|
||||
}
|
||||
}
|
||||
|
||||
public init(store: UnsafePointer<Store>?, title: String) {
|
||||
self.store = store
|
||||
self.title = title
|
||||
|
||||
super.init(frame: NSRect(
|
||||
x: 0,
|
||||
y: 0,
|
||||
@@ -277,8 +289,8 @@ internal class Popup: NSView {
|
||||
let index = list.count-i-1
|
||||
if self.processes.indices.contains(index) {
|
||||
self.processes[index].label = process.name
|
||||
self.processes[index].upload = Units(bytes: Int64(process.upload)).getReadableSpeed()
|
||||
self.processes[index].download = Units(bytes: Int64(process.download)).getReadableSpeed()
|
||||
self.processes[index].upload = Units(bytes: Int64(process.upload)).getReadableSpeed(base: DataSizeBase(rawValue: self.base) ?? .byte)
|
||||
self.processes[index].download = Units(bytes: Int64(process.download)).getReadableSpeed(base: DataSizeBase(rawValue: self.base) ?? .byte)
|
||||
self.processes[index].icon = process.icon
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
9A0C82EB24460FB100FAE3D4 /* StatsKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9A0C82DA24460F7200FAE3D4 /* StatsKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
9A0C82EE2446124800FAE3D4 /* SystemKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A7D0CB62444C2C800B09070 /* SystemKit.swift */; };
|
||||
9A1A7ABA24561F0B00A84F7A /* BarChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1A7AB924561F0B00A84F7A /* BarChart.swift */; };
|
||||
9A1D5E4B25235C8100B82BFC /* helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1D5E4A25235C8100B82BFC /* helpers.swift */; };
|
||||
9A313BF7247EF01800DB5101 /* Reachability.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A5349CD23D8832E00C23824 /* Reachability.framework */; };
|
||||
9A313BF8247EF01800DB5101 /* Reachability.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9A5349CD23D8832E00C23824 /* Reachability.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
9A34353B243E278D006B19F9 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A34353A243E278D006B19F9 /* main.swift */; };
|
||||
@@ -455,6 +456,7 @@
|
||||
9A1410F5229E721100D29793 /* Stats.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Stats.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
9A141101229E721200D29793 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
9A1A7AB924561F0B00A84F7A /* BarChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarChart.swift; sourceTree = "<group>"; };
|
||||
9A1D5E4A25235C8100B82BFC /* helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = helpers.swift; sourceTree = "<group>"; };
|
||||
9A343527243E26A0006B19F9 /* LaunchAtLogin.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LaunchAtLogin.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
9A343535243E26A0006B19F9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
9A343536243E26A0006B19F9 /* LaunchAtLogin.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = LaunchAtLogin.entitlements; sourceTree = "<group>"; };
|
||||
@@ -663,6 +665,7 @@
|
||||
9A65492224407EA600E30B74 /* store.swift */,
|
||||
9A0C82D324460E4400FAE3D4 /* launchAtLogin.swift */,
|
||||
9A654920244074B500E30B74 /* extensions.swift */,
|
||||
9A1D5E4A25235C8100B82BFC /* helpers.swift */,
|
||||
9A0C82DC24460F7200FAE3D4 /* StatsKit.h */,
|
||||
9A0C82DD24460F7200FAE3D4 /* Info.plist */,
|
||||
9A9D728924471FAE005CF997 /* SMC.swift */,
|
||||
@@ -1400,6 +1403,7 @@
|
||||
9A81C7702449B8D500825D92 /* Charts.swift in Sources */,
|
||||
9A0C82E624460F9A00FAE3D4 /* extensions.swift in Sources */,
|
||||
9A0C82E724460F9C00FAE3D4 /* updater.swift in Sources */,
|
||||
9A1D5E4B25235C8100B82BFC /* helpers.swift in Sources */,
|
||||
9A0C82EE2446124800FAE3D4 /* SystemKit.swift in Sources */,
|
||||
9A9D728A24471FAE005CF997 /* SMC.swift in Sources */,
|
||||
9A0C82E824460F9E00FAE3D4 /* launchAtLogin.swift in Sources */,
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
"Colorize value" = "Colorize value";
|
||||
"Additional information" = "Additional information";
|
||||
"Reverse values order" = "Reverse values order";
|
||||
"Base" = "Base";
|
||||
|
||||
// Module Kit
|
||||
"Open module settings" = "Open module settings";
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
"Colorize value" = "Kolorowanie wartości";
|
||||
"Additional information" = "Informacja dodatkowa";
|
||||
"Reverse values order" = "Zmień kolejność wyświetlania";
|
||||
"Base" = "Podstawa";
|
||||
|
||||
// Module Kit
|
||||
"Open module settings" = "Otwórz ustawienie modulu";
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
"Colorize value" = "Раскрасить значение";
|
||||
"Additional information" = "Дополнительная информация";
|
||||
"Reverse values order" = "Изменить порядок сортировки";
|
||||
"Base" = "Основа";
|
||||
|
||||
// Module Kit
|
||||
"Open module settings" = "Открыть настройки модуля";
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
"Colorize value" = "Değeri renklendir";
|
||||
"Additional information" = "Ek bilgi";
|
||||
"Reverse values order" = "Değerler sırasını tersine çevir";
|
||||
"Base" = "Temel";
|
||||
|
||||
// Module Kit
|
||||
"Open module settings" = "Modül ayarlarını aç";
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
"Colorize value" = "Розфарбувати значення";
|
||||
"Additional information" = "Додаткова інформація";
|
||||
"Reverse values order" = "Змінити порядок сортування";
|
||||
"Base" = "Основа";
|
||||
|
||||
// Module Kit
|
||||
"Open module settings" = "Відкрити налаштування модуля";
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
"Colorize value" = "数值颜色";
|
||||
"Additional information" = "附加信息";
|
||||
"Reverse values order" = "数值反序";
|
||||
"Base" = "基础";
|
||||
|
||||
// Module Kit
|
||||
"Open module settings" = "打开模块设置";
|
||||
|
||||
@@ -11,85 +11,6 @@
|
||||
|
||||
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 }
|
||||
|
||||
@@ -569,525 +490,3 @@ public extension NSColor {
|
||||
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 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)
|
||||
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 colorFromString(_ colorString: String) -> NSColor {
|
||||
switch colorString {
|
||||
case "black":
|
||||
return NSColor.black
|
||||
case "darkGray":
|
||||
return NSColor.darkGray
|
||||
case "lightGray":
|
||||
return NSColor.lightGray
|
||||
case "gray":
|
||||
return NSColor.gray
|
||||
case "secondGray":
|
||||
return NSColor.systemGray
|
||||
case "white":
|
||||
return NSColor.white
|
||||
case "red":
|
||||
return NSColor.red
|
||||
case "secondRed":
|
||||
return NSColor.systemRed
|
||||
case "green":
|
||||
return NSColor.green
|
||||
case "secondGreen":
|
||||
return NSColor.systemGreen
|
||||
case "blue":
|
||||
return NSColor.blue
|
||||
case "secondBlue":
|
||||
return NSColor.systemBlue
|
||||
case "yellow":
|
||||
return NSColor.yellow
|
||||
case "secondYellow":
|
||||
return NSColor.systemYellow
|
||||
case "orange":
|
||||
return NSColor.orange
|
||||
case "secondOrange":
|
||||
return NSColor.systemOrange
|
||||
case "purple":
|
||||
return NSColor.purple
|
||||
case "secondPurple":
|
||||
return NSColor.systemPurple
|
||||
case "brown":
|
||||
return NSColor.brown
|
||||
case "secondBrown":
|
||||
return NSColor.systemBrown
|
||||
case "cyan":
|
||||
return NSColor.cyan
|
||||
case "magenta":
|
||||
return NSColor.magenta
|
||||
case "clear":
|
||||
return NSColor.clear
|
||||
case "pink":
|
||||
return NSColor.systemPink
|
||||
case "teal":
|
||||
return NSColor.systemTeal
|
||||
case "indigo":
|
||||
if #available(OSX 10.15, *) {
|
||||
return NSColor.systemIndigo
|
||||
} else {
|
||||
return NSColor(hexString: "#4B0082")
|
||||
}
|
||||
default:
|
||||
return NSColor.controlAccentColor
|
||||
}
|
||||
}
|
||||
|
||||
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 typealias updateInterval = String
|
||||
public enum updateIntervals: updateInterval {
|
||||
case atStart = "At start"
|
||||
case separator_1 = "separator_1"
|
||||
case oncePerDay = "Once per day"
|
||||
case oncePerWeek = "Once per week"
|
||||
case oncePerMonth = "Once per month"
|
||||
case separator_2 = "separator_2"
|
||||
case never = "Never"
|
||||
}
|
||||
extension updateIntervals: CaseIterable {}
|
||||
|
||||
public struct KeyValue_t {
|
||||
let key: String
|
||||
let value: String
|
||||
let additional: Any?
|
||||
|
||||
init(key: String, value: String, additional: Any? = nil) {
|
||||
self.key = key
|
||||
self.value = value
|
||||
self.additional = additional
|
||||
}
|
||||
}
|
||||
|
||||
public let TemperatureUnits: [KeyValue_t] = [
|
||||
KeyValue_t(key: "system", value: "System"),
|
||||
KeyValue_t(key: "separator", value: "separator"),
|
||||
KeyValue_t(key: "celsius", value: "Celsius", additional: UnitTemperature.celsius),
|
||||
KeyValue_t(key: "fahrenheit", value: "Fahrenheit", additional: UnitTemperature.fahrenheit)
|
||||
]
|
||||
|
||||
public func showNotification(title: String, subtitle: String, id: String = UUID().uuidString, icon: NSImage? = nil) -> NSUserNotification {
|
||||
let notification = NSUserNotification()
|
||||
|
||||
notification.identifier = id
|
||||
notification.title = title
|
||||
notification.subtitle = subtitle
|
||||
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 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
|
||||
}
|
||||
|
||||
public func Temperature(_ value: Double) -> String {
|
||||
let stringUnit: String = Store.shared.string(key: "temperature_units", defaultValue: "system")
|
||||
let formatter = MeasurementFormatter()
|
||||
formatter.numberFormatter.maximumFractionDigits = 0
|
||||
formatter.unitOptions = .providedUnit
|
||||
|
||||
var measurement = Measurement(value: value, unit: UnitTemperature.celsius)
|
||||
if stringUnit != "system" {
|
||||
if let temperatureUnit = TemperatureUnits.first(where: { $0.key == stringUnit }), let unit = temperatureUnit.additional as? UnitTemperature {
|
||||
measurement.convert(to: unit)
|
||||
}
|
||||
}
|
||||
|
||||
return formatter.string(from: measurement)
|
||||
}
|
||||
|
||||
618
StatsKit/helpers.swift
Normal file
618
StatsKit/helpers.swift
Normal file
@@ -0,0 +1,618 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
public typealias updateInterval = String
|
||||
public enum updateIntervals: updateInterval {
|
||||
case atStart = "At start"
|
||||
case separator_1 = "separator_1"
|
||||
case oncePerDay = "Once per day"
|
||||
case oncePerWeek = "Once per week"
|
||||
case oncePerMonth = "Once per month"
|
||||
case separator_2 = "separator_2"
|
||||
case never = "Never"
|
||||
}
|
||||
extension updateIntervals: CaseIterable {}
|
||||
|
||||
public struct KeyValue_t {
|
||||
let key: String
|
||||
let value: String
|
||||
let additional: Any?
|
||||
|
||||
init(key: String, value: String, additional: Any? = nil) {
|
||||
self.key = key
|
||||
self.value = value
|
||||
self.additional = additional
|
||||
}
|
||||
}
|
||||
|
||||
public let TemperatureUnits: [KeyValue_t] = [
|
||||
KeyValue_t(key: "system", value: "System"),
|
||||
KeyValue_t(key: "separator", value: "separator"),
|
||||
KeyValue_t(key: "celsius", value: "Celsius", additional: UnitTemperature.celsius),
|
||||
KeyValue_t(key: "fahrenheit", value: "Fahrenheit", additional: UnitTemperature.fahrenheit)
|
||||
]
|
||||
|
||||
public enum DataSizeBase: String {
|
||||
case bit = "bit"
|
||||
case byte = "byte"
|
||||
}
|
||||
public let SpeedBase: [KeyValue_t] = [
|
||||
KeyValue_t(key: "bit", value: "Bit", additional: DataSizeBase.bit),
|
||||
KeyValue_t(key: "byte", value: "Byte", additional: DataSizeBase.byte)
|
||||
]
|
||||
|
||||
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(base: DataSizeBase = .byte) -> String {
|
||||
let stringBase = base == .byte ? "B" : "b"
|
||||
let multiplier: Double = base == .byte ? 1 : 8
|
||||
|
||||
switch bytes*Int64(multiplier) {
|
||||
case 0..<1_024:
|
||||
return "0 K\(stringBase)/s"
|
||||
case 1_024..<(1_024 * 1_024):
|
||||
return String(format: "%.0f K\(stringBase)/s", kilobytes*multiplier)
|
||||
case 1_024..<(1_024 * 1_024 * 100):
|
||||
return String(format: "%.1f M\(stringBase)/s", megabytes*multiplier)
|
||||
case (1_024 * 1_024 * 100)..<(1_024 * 1_024 * 1_024):
|
||||
return String(format: "%.0f M\(stringBase)/s", megabytes*multiplier)
|
||||
case (1_024 * 1_024 * 1_024)...Int64.max:
|
||||
return String(format: "%.1f G\(stringBase)/s", gigabytes*multiplier)
|
||||
default:
|
||||
return String(format: "%.0f K\(stringBase)/s", 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 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 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)
|
||||
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 colorFromString(_ colorString: String) -> NSColor {
|
||||
switch colorString {
|
||||
case "black":
|
||||
return NSColor.black
|
||||
case "darkGray":
|
||||
return NSColor.darkGray
|
||||
case "lightGray":
|
||||
return NSColor.lightGray
|
||||
case "gray":
|
||||
return NSColor.gray
|
||||
case "secondGray":
|
||||
return NSColor.systemGray
|
||||
case "white":
|
||||
return NSColor.white
|
||||
case "red":
|
||||
return NSColor.red
|
||||
case "secondRed":
|
||||
return NSColor.systemRed
|
||||
case "green":
|
||||
return NSColor.green
|
||||
case "secondGreen":
|
||||
return NSColor.systemGreen
|
||||
case "blue":
|
||||
return NSColor.blue
|
||||
case "secondBlue":
|
||||
return NSColor.systemBlue
|
||||
case "yellow":
|
||||
return NSColor.yellow
|
||||
case "secondYellow":
|
||||
return NSColor.systemYellow
|
||||
case "orange":
|
||||
return NSColor.orange
|
||||
case "secondOrange":
|
||||
return NSColor.systemOrange
|
||||
case "purple":
|
||||
return NSColor.purple
|
||||
case "secondPurple":
|
||||
return NSColor.systemPurple
|
||||
case "brown":
|
||||
return NSColor.brown
|
||||
case "secondBrown":
|
||||
return NSColor.systemBrown
|
||||
case "cyan":
|
||||
return NSColor.cyan
|
||||
case "magenta":
|
||||
return NSColor.magenta
|
||||
case "clear":
|
||||
return NSColor.clear
|
||||
case "pink":
|
||||
return NSColor.systemPink
|
||||
case "teal":
|
||||
return NSColor.systemTeal
|
||||
case "indigo":
|
||||
if #available(OSX 10.15, *) {
|
||||
return NSColor.systemIndigo
|
||||
} else {
|
||||
return NSColor(hexString: "#4B0082")
|
||||
}
|
||||
default:
|
||||
return NSColor.controlAccentColor
|
||||
}
|
||||
}
|
||||
|
||||
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, id: String = UUID().uuidString, icon: NSImage? = nil) -> NSUserNotification {
|
||||
let notification = NSUserNotification()
|
||||
|
||||
notification.identifier = id
|
||||
notification.title = title
|
||||
notification.subtitle = subtitle
|
||||
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 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
|
||||
}
|
||||
|
||||
public func Temperature(_ value: Double) -> String {
|
||||
let stringUnit: String = Store.shared.string(key: "temperature_units", defaultValue: "system")
|
||||
let formatter = MeasurementFormatter()
|
||||
formatter.numberFormatter.maximumFractionDigits = 0
|
||||
formatter.unitOptions = .providedUnit
|
||||
|
||||
var measurement = Measurement(value: value, unit: UnitTemperature.celsius)
|
||||
if stringUnit != "system" {
|
||||
if let temperatureUnit = TemperatureUnits.first(where: { $0.key == stringUnit }), let unit = temperatureUnit.additional as? UnitTemperature {
|
||||
measurement.convert(to: unit)
|
||||
}
|
||||
}
|
||||
|
||||
return formatter.string(from: measurement)
|
||||
}
|
||||
Reference in New Issue
Block a user