diff --git a/Kit/Widgets/Battery.swift b/Kit/Widgets/Battery.swift
index d13b706b..7adc68dc 100644
--- a/Kit/Widgets/Battery.swift
+++ b/Kit/Widgets/Battery.swift
@@ -18,7 +18,7 @@ public class BatterykWidget: WidgetWrapper {
private var colorState: Bool = false
private var hideAdditionalWhenFull: Bool = true
- private var percentage: Double = 1
+ private var percentage: Double? = nil
private var time: Int = 0
private var charging: Bool = false
private var ACStatus: Bool = false
@@ -68,10 +68,11 @@ public class BatterykWidget: WidgetWrapper {
if !self.hideAdditionalWhenFull || (self.hideAdditionalWhenFull && self.percentage != 1) {
switch self.additional {
case "percentage":
- let rowWidth = self.drawOneRow(
- value: "\(Int((self.percentage.rounded(toPlaces: 2)) * 100))%",
- x: x
- ).rounded(.up)
+ var value = "n/a"
+ if let percentage = self.percentage {
+ value = "\(Int((percentage.rounded(toPlaces: 2)) * 100))%"
+ }
+ let rowWidth = self.drawOneRow(value: value, x: x).rounded(.up)
width += rowWidth + Constants.Widget.spacing
x += rowWidth + Constants.Widget.spacing
case "time":
@@ -82,17 +83,25 @@ public class BatterykWidget: WidgetWrapper {
width += rowWidth + Constants.Widget.spacing
x += rowWidth + Constants.Widget.spacing
case "percentageAndTime":
+ var value = "n/a"
+ if let percentage = self.percentage {
+ value = "\(Int((percentage.rounded(toPlaces: 2)) * 100))%"
+ }
let rowWidth = self.drawTwoRows(
- first: "\(Int((self.percentage.rounded(toPlaces: 2)) * 100))%",
+ first: value,
second: Double(self.time*60).printSecondsToHoursMinutesSeconds(short: isShortTimeFormat),
x: x
).rounded(.up)
width += rowWidth + Constants.Widget.spacing
x += rowWidth + Constants.Widget.spacing
case "timeAndPercentage":
+ var value = "n/a"
+ if let percentage = self.percentage {
+ value = "\(Int((percentage.rounded(toPlaces: 2)) * 100))%"
+ }
let rowWidth = self.drawTwoRows(
first: Double(self.time*60).printSecondsToHoursMinutesSeconds(short: isShortTimeFormat),
- second: "\(Int((self.percentage.rounded(toPlaces: 2)) * 100))%",
+ second: value,
x: x
).rounded(.up)
width += rowWidth + Constants.Widget.spacing
@@ -133,17 +142,33 @@ public class BatterykWidget: WidgetWrapper {
ctx.restoreGState()
width += 2 // add battery point width
- let maxWidth = batterySize.width - offset*2 - borderWidth*2 - 1
- let innerWidth: CGFloat = max(1, maxWidth * CGFloat(self.percentage))
- let innerOffset: CGFloat = -offset + borderWidth + 1
- let inner = NSBezierPath(roundedRect: NSRect(
- x: batteryFrame.bounds.origin.x + innerOffset,
- y: batteryFrame.bounds.origin.y + innerOffset,
- width: innerWidth,
- height: batterySize.height - offset*2 - borderWidth*2 - 1
- ), xRadius: 1, yRadius: 1)
- self.percentage.batteryColor(color: self.colorState).set()
- inner.fill()
+ if let percentage = self.percentage {
+ let maxWidth = batterySize.width - offset*2 - borderWidth*2 - 1
+ let innerWidth: CGFloat = max(1, maxWidth * CGFloat(percentage))
+ let innerOffset: CGFloat = -offset + borderWidth + 1
+ let inner = NSBezierPath(roundedRect: NSRect(
+ x: batteryFrame.bounds.origin.x + innerOffset,
+ y: batteryFrame.bounds.origin.y + innerOffset,
+ width: innerWidth,
+ height: batterySize.height - offset*2 - borderWidth*2 - 1
+ ), xRadius: 1, yRadius: 1)
+ percentage.batteryColor(color: self.colorState).set()
+ inner.fill()
+ } else {
+ let attributes = [
+ NSAttributedString.Key.font: NSFont.systemFont(ofSize: 11, weight: .regular),
+ NSAttributedString.Key.foregroundColor: isDarkMode ? NSColor.white : NSColor.textColor,
+ NSAttributedString.Key.paragraphStyle: NSMutableParagraphStyle()
+ ]
+
+ let batteryCenter: CGPoint = CGPoint(
+ x: batteryFrame.bounds.origin.x + (batteryFrame.bounds.width/2),
+ y: batteryFrame.bounds.origin.y + (batteryFrame.bounds.height/2)
+ )
+
+ let rect = CGRect(x: batteryCenter.x-2, y: batteryCenter.y-4, width: 8, height: 12)
+ NSAttributedString.init(string: "?", attributes: attributes).draw(with: rect)
+ }
if self.ACStatus {
let batteryCenter: CGPoint = CGPoint(
@@ -267,7 +292,7 @@ public class BatterykWidget: WidgetWrapper {
return rowWidth
}
- public func setValue(percentage: Double, ACStatus: Bool, isCharging: Bool, time: Int) {
+ public func setValue(percentage: Double? = nil, ACStatus: Bool? = nil, isCharging: Bool? = nil, time: Int? = nil) {
var updated: Bool = false
let timeFormat: String = Store.shared.string(key: "\(self.title)_timeFormat", defaultValue: self.timeFormat)
@@ -275,15 +300,15 @@ public class BatterykWidget: WidgetWrapper {
self.percentage = percentage
updated = true
}
- if self.ACStatus != ACStatus {
- self.ACStatus = ACStatus
+ if let status = ACStatus, self.ACStatus != status {
+ self.ACStatus = status
updated = true
}
- if self.charging != isCharging {
- self.charging = isCharging
+ if let charging = isCharging, self.charging != charging {
+ self.charging = charging
updated = true
}
- if self.time != time {
+ if let time = time, self.time != time {
self.time = time
updated = true
}
@@ -302,33 +327,32 @@ public class BatterykWidget: WidgetWrapper {
// MARK: - Settings
public override func settings(width: CGFloat) -> NSView {
- let rowHeight: CGFloat = 30
- let height: CGFloat = ((rowHeight + Constants.Settings.margin) * 3) + Constants.Settings.margin
+ let view = SettingsContainerView(width: width)
- let view: NSView = NSView(frame: NSRect(
- x: Constants.Settings.margin,
- y: Constants.Settings.margin,
- width: width - (Constants.Settings.margin*2),
- height: height
- ))
+ var additionalOptions = BatteryAdditionals
+ if self.title == "Bluetooth" {
+ additionalOptions = additionalOptions.filter({ $0.key == "none" || $0.key == "percentage" })
+ }
- view.addSubview(selectRow(
- frame: NSRect(x: 0, y: (rowHeight + Constants.Settings.margin) * 2, width: view.frame.width, height: rowHeight),
+ view.addArrangedSubview(selectRow(
+ frame: NSRect(x: 0, y: 0, width: view.frame.width, height: Constants.Settings.row),
title: localizedString("Additional information"),
action: #selector(toggleAdditional),
- items: BatteryAdditionals,
+ items: additionalOptions,
selected: self.additional
))
- view.addSubview(toggleTitleRow(
- frame: NSRect(x: 0, y: (rowHeight + Constants.Settings.margin) * 1, width: view.frame.width, height: rowHeight),
- title: localizedString("Hide additional information when full"),
- action: #selector(toggleHideAdditionalWhenFull),
- state: self.hideAdditionalWhenFull
- ))
+ if self.title != "Bluetooth" {
+ view.addArrangedSubview(toggleTitleRow(
+ frame: NSRect(x: 0, y: 0, width: view.frame.width, height: Constants.Settings.row),
+ title: localizedString("Hide additional information when full"),
+ action: #selector(toggleHideAdditionalWhenFull),
+ state: self.hideAdditionalWhenFull
+ ))
+ }
- view.addSubview(toggleTitleRow(
- frame: NSRect(x: 0, y: (rowHeight + Constants.Settings.margin) * 0, width: view.frame.width, height: rowHeight),
+ view.addArrangedSubview(toggleTitleRow(
+ frame: NSRect(x: 0, y: 0, width: view.frame.width, height: Constants.Settings.row),
title: localizedString("Colorize"),
action: #selector(toggleColor),
state: self.colorState
diff --git a/Kit/module/module.swift b/Kit/module/module.swift
index ecb1bf7d..b0e7ae86 100644
--- a/Kit/module/module.swift
+++ b/Kit/module/module.swift
@@ -80,7 +80,7 @@ open class Module: Module_p {
private let log: NextLog
private var readers: [Reader_p] = []
- public init(popup: Popup_p?, settings: Settings_v?) {
+ public init(popup: Popup_p? = nil, settings: Settings_v? = nil) {
self.config = module_c(in: Bundle(for: type(of: self)).path(forResource: "config", ofType: "plist")!)
self.log = NextLog.shared.copy(category: self.config.name)
diff --git a/Modules/Bluetooth/Info.plist b/Modules/Bluetooth/Info.plist
new file mode 100644
index 00000000..85e3d697
--- /dev/null
+++ b/Modules/Bluetooth/Info.plist
@@ -0,0 +1,24 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ $(CURRENT_PROJECT_VERSION)
+ NSHumanReadableCopyright
+ Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved.
+
+
diff --git a/Modules/Bluetooth/config.plist b/Modules/Bluetooth/config.plist
new file mode 100644
index 00000000..5750e36b
--- /dev/null
+++ b/Modules/Bluetooth/config.plist
@@ -0,0 +1,49 @@
+
+
+
+
+ Name
+ Bluetooth
+ State
+
+ Widgets
+
+ label
+
+ Default
+
+ Title
+ BLE
+ Order
+ 0
+
+ mini
+
+ Title
+ BLE
+ Default
+
+ Preview
+
+ Title
+ BLE
+ Value
+ 0.98
+
+ Unsupported colors
+
+ pressure
+
+ Order
+ 1
+
+ battery
+
+ Default
+
+ Order
+ 2
+
+
+
+
diff --git a/Modules/Bluetooth/main.swift b/Modules/Bluetooth/main.swift
new file mode 100644
index 00000000..503c6985
--- /dev/null
+++ b/Modules/Bluetooth/main.swift
@@ -0,0 +1,116 @@
+//
+// main.swift
+// Bluetooth
+//
+// Created by Serhiy Mytrovtsiy on 08/06/2021.
+// Using Swift 5.0.
+// Running on macOS 10.15.
+//
+// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved.
+//
+
+import Foundation
+import Kit
+import CoreBluetooth
+
+public enum BLEType: String {
+ case iPhone
+ case airPods
+ case unknown
+}
+
+public struct BLEDevice {
+ let uuid: UUID
+ let name: String
+ let type: BLEType
+
+ var RSSI: Int?
+ var batteryLevel: [KeyValue_t]
+
+ var isConnected: Bool
+ var isPaired: Bool
+ var isInitialized: Bool
+
+ var peripheral: CBPeripheral?
+}
+
+public class Bluetooth: Module {
+ private var devicesReader: DevicesReader? = nil
+ private let popupView: Popup = Popup()
+ private let settingsView: Settings
+
+ private var selectedBattery: String = ""
+
+ public init() {
+ self.settingsView = Settings("Bluetooth")
+
+ super.init(
+ popup: self.popupView,
+ settings: self.settingsView
+ )
+ guard self.available else { return }
+
+ self.devicesReader = DevicesReader()
+ self.selectedBattery = Store.shared.string(key: "\(self.config.name)_battery", defaultValue: self.selectedBattery)
+
+ self.settingsView.selectedBatteryHandler = { [unowned self] value in
+ self.selectedBattery = value
+ }
+
+ self.devicesReader?.callbackHandler = { [unowned self] value in
+ self.batteryCallback(value)
+ }
+ self.devicesReader?.readyCallback = { [unowned self] in
+ self.readyHandler()
+ }
+
+ if let reader = self.devicesReader {
+ self.addReader(reader)
+ }
+ }
+
+ private func batteryCallback(_ raw: [BLEDevice]?) {
+ guard let value = raw else {
+ return
+ }
+
+ let active = value.filter{ $0.isPaired && ($0.isConnected || !$0.batteryLevel.isEmpty) }
+ DispatchQueue.main.async(execute: {
+ self.popupView.batteryCallback(active)
+ })
+ self.settingsView.setList(active)
+
+ var battery = active.first?.batteryLevel.first
+ if self.selectedBattery != "" {
+ let pair = self.selectedBattery.split(separator: "@")
+
+ guard let device = value.first(where: { $0.name == pair.first! }) else {
+ error("cannot find selected battery: \(self.selectedBattery)")
+ return
+ }
+
+ if pair.count == 1 {
+ battery = device.batteryLevel.first
+ } else if pair.count == 2 {
+ battery = device.batteryLevel.first{ $0.key == pair.last! }
+ }
+ }
+
+ self.widgets.filter{ $0.isActive }.forEach { (w: Widget) in
+ switch w.item {
+ case let widget as Mini:
+ guard let percentage = Double(battery?.value ?? "0") else {
+ return
+ }
+ widget.setValue(percentage/100)
+ case let widget as BatterykWidget:
+ var percentage: Double? = nil
+ if let value = battery?.value {
+ percentage = (Double(value) ?? 0) / 100
+ }
+ widget.setValue(percentage: percentage)
+ default: break
+ }
+ }
+ }
+}
diff --git a/Modules/Bluetooth/popup.swift b/Modules/Bluetooth/popup.swift
new file mode 100644
index 00000000..8b016ae7
--- /dev/null
+++ b/Modules/Bluetooth/popup.swift
@@ -0,0 +1,111 @@
+//
+// popup.swift
+// Bluetooth
+//
+// Created by Serhiy Mytrovtsiy on 22/06/2021.
+// Using Swift 5.0.
+// Running on macOS 10.15.
+//
+// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved.
+//
+
+import Cocoa
+import Kit
+
+internal class Popup: NSStackView, Popup_p {
+ public var sizeCallback: ((NSSize) -> Void)? = nil
+
+ private var list: [UUID: BLEView] = [:]
+
+ public init() {
+ super.init(frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 0))
+
+ self.orientation = .vertical
+ self.spacing = Constants.Popup.margins
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ internal func batteryCallback(_ list: [BLEDevice]) {
+ let views = self.subviews.filter{ $0 is BLEView }.map{ $0 as! BLEView }
+
+ list.reversed().forEach { (ble: BLEDevice) in
+ if let view = views.first(where: { $0.uuid == ble.uuid }) {
+ view.update(ble.batteryLevel)
+ } else {
+ self.addArrangedSubview(BLEView(
+ width: self.frame.width,
+ uuid: ble.uuid,
+ name: ble.name,
+ batteryLevel: ble.batteryLevel
+ ))
+ }
+ }
+
+ let h = self.arrangedSubviews.map({ $0.bounds.height + self.spacing }).reduce(0, +) - self.spacing
+ if h > 0 && self.frame.size.height != h {
+ self.setFrameSize(NSSize(width: self.frame.width, height: h))
+ self.sizeCallback?(self.frame.size)
+ }
+ }
+}
+
+internal class BLEView: NSStackView {
+ public var uuid: UUID
+
+ open override var intrinsicContentSize: CGSize {
+ return CGSize(width: self.bounds.width, height: self.bounds.height)
+ }
+
+ public init(width: CGFloat, uuid: UUID, name: String, batteryLevel: [KeyValue_t]) {
+ self.uuid = uuid
+
+ super.init(frame: NSRect(x: 0, y: 0, width: width, height: 30))
+
+ self.orientation = .horizontal
+ self.alignment = .centerY
+ self.spacing = 0
+ self.wantsLayer = true
+ self.layer?.cornerRadius = 2
+
+ let nameView: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: 0, height: 16))
+ nameView.font = NSFont.systemFont(ofSize: 13, weight: .light)
+ nameView.stringValue = name
+
+ self.addArrangedSubview(nameView)
+ self.addArrangedSubview(NSView())
+
+ batteryLevel.forEach { (pair: KeyValue_t) in
+ self.addLevel(pair)
+ }
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func updateLayer() {
+ self.layer?.backgroundColor = isDarkMode ? NSColor(hexString: "#111111", alpha: 0.25).cgColor : NSColor(hexString: "#f5f5f5", alpha: 1).cgColor
+ }
+
+ public func update(_ batteryLevel: [KeyValue_t]) {
+ batteryLevel.forEach { (pair: KeyValue_t) in
+ if let view = self.subviews.first(where: { $0.identifier?.rawValue == pair.key }) as? NSTextField {
+ view.stringValue = "\(pair.value)%"
+ } else {
+ self.addLevel(pair)
+ }
+ }
+ }
+
+ private func addLevel(_ pair: KeyValue_t) {
+ let valueView: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: 0, height: 13))
+ valueView.identifier = NSUserInterfaceItemIdentifier(rawValue: pair.key)
+ valueView.font = NSFont.systemFont(ofSize: 12, weight: .regular)
+ valueView.stringValue = "\(pair.value)%"
+ valueView.toolTip = pair.key
+ self.addArrangedSubview(valueView)
+ }
+}
diff --git a/Modules/Bluetooth/readers.swift b/Modules/Bluetooth/readers.swift
new file mode 100644
index 00000000..931f4321
--- /dev/null
+++ b/Modules/Bluetooth/readers.swift
@@ -0,0 +1,179 @@
+//
+// readers.swift
+// Bluetooth
+//
+// Created by Serhiy Mytrovtsiy on 08/06/2021.
+// Using Swift 5.0.
+// Running on macOS 10.15.
+//
+// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved.
+//
+
+import Foundation
+import Kit
+import CoreBluetooth
+import IOBluetooth
+
+internal class DevicesReader: Reader<[BLEDevice]> {
+ private let ble: BluetoothDelegate = BluetoothDelegate()
+
+ init() {
+ super.init()
+ }
+
+ public override func read() {
+ self.ble.read()
+ self.callback(self.ble.devices)
+ }
+}
+
+class BluetoothDelegate: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate {
+ private var manager: CBCentralManager!
+ private let cache = UserDefaults(suiteName: "/Library/Preferences/com.apple.Bluetooth")
+
+ private var peripherals: [CBPeripheral] = []
+ public var devices: [BLEDevice] = []
+ private var characteristicsDict: [UUID: CBCharacteristic] = [:]
+
+ private let batteryServiceUUID = CBUUID(string: "0x180F")
+ private let batteryCharacteristicsUUID = CBUUID(string: "0x2A19")
+
+ override init() {
+ super.init()
+ self.manager = CBCentralManager.init(delegate: self, queue: nil)
+ }
+
+ public func read() {
+ IOBluetoothDevice.pairedDevices().forEach { (d) in
+ guard let device = d as? IOBluetoothDevice,
+ let cache = self.findInCache(address: device.addressString) else {
+ return
+ }
+
+ let rssi = device.rawRSSI() == 127 ? nil : Int(device.rawRSSI())
+
+ if let idx = self.devices.firstIndex(where: { $0.uuid == cache.uuid }) {
+ self.devices[idx].isConnected = device.isConnected()
+ self.devices[idx].isPaired = device.isPaired()
+ self.devices[idx].RSSI = rssi
+ } else {
+ self.devices.append(BLEDevice(
+ uuid: cache.uuid,
+ name: device.nameOrAddress,
+ type: .unknown,
+ RSSI: rssi,
+ batteryLevel: cache.batteryLevel,
+ isConnected: device.isConnected(),
+ isPaired: device.isPaired(),
+ isInitialized: false
+ ))
+ }
+ }
+ }
+
+ private func findInCache(address: String) -> (uuid: UUID, batteryLevel: [KeyValue_t])? {
+ guard let plist = self.cache,
+ let deviceCache = plist.object(forKey: "DeviceCache") as? [String: [String: Any]],
+ let coreCache = plist.object(forKey: "CoreBluetoothCache") as? [String: [String: Any]] else {
+ return nil
+ }
+
+ guard let uuid = coreCache.compactMap({ (key, dict) -> UUID? in
+ guard let field = dict.first(where: { $0.key == "DeviceAddress" }),
+ let value = field.value as? String,
+ value == address else {
+ return nil
+ }
+ return UUID(uuidString: key)
+ }).first else {
+ return nil
+ }
+
+ var batteryLevel: [KeyValue_t] = []
+ if let d = deviceCache.first(where: { $0.key == address }) {
+ d.value.forEach { (key, value) in
+ guard let value = value as? Int, key == "BatteryPercentCase" || key == "BatteryPercentLeft" || key == "BatteryPercentRight" else {
+ return
+ }
+
+ batteryLevel.append(KeyValue_t(key: key, value: "\(value)"))
+ }
+ }
+
+ return (uuid, batteryLevel)
+ }
+
+ // MARK: - CBCentralManagerDelegate
+
+ func centralManagerDidUpdateState(_ central: CBCentralManager) {
+ if central.state == .poweredOff {
+ self.manager.stopScan()
+ } else if central.state == .poweredOn {
+ self.manager.scanForPeripherals(withServices: nil, options: nil)
+ }
+ }
+
+ func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
+ guard let idx = self.devices.firstIndex(where: { $0.uuid == peripheral.identifier }) else {
+ return
+ }
+
+ if self.devices[idx].RSSI == nil {
+ self.devices[idx].RSSI = Int(truncating: RSSI)
+ }
+
+ if self.devices[idx].peripheral == nil {
+ self.devices[idx].peripheral = peripheral
+ }
+
+ if peripheral.state == .disconnected {
+ central.connect(peripheral, options: nil)
+ } else if peripheral.state == .connected && !self.devices[idx].isInitialized {
+ peripheral.delegate = self
+ peripheral.discoverServices([batteryServiceUUID])
+ self.devices[idx].isInitialized = true
+ }
+ }
+
+ // MARK: - CBPeripheralDelegate
+
+ func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
+ guard error == nil else {
+ print("didDiscoverServices: ", error!)
+ return
+ }
+
+ guard let service = peripheral.services?.first(where: { $0.uuid == self.batteryServiceUUID }) else {
+ print("battery service not found, skipping")
+ return
+ }
+
+ peripheral.discoverCharacteristics([self.batteryCharacteristicsUUID], for: service)
+ }
+
+ func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
+ guard error == nil else {
+ print("didDiscoverCharacteristicsFor: ", error!)
+ return
+ }
+
+ guard let batteryCharacteristics = service.characteristics?.first(where: { $0.uuid == self.batteryCharacteristicsUUID }) else {
+ print("characteristics not found")
+ return
+ }
+
+ self.characteristicsDict[peripheral.identifier] = batteryCharacteristics
+ peripheral.readValue(for: batteryCharacteristics)
+ }
+
+ func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
+ guard error == nil else {
+ print("didUpdateValueFor: ", error!)
+ return
+ }
+
+ if let batteryLevel = characteristic.value?[0], let idx = self.devices.firstIndex(where: { $0.uuid == peripheral.identifier }) {
+ self.devices[idx].batteryLevel = [KeyValue_t(key: "battery", value: "\(batteryLevel)")]
+ }
+ }
+}
diff --git a/Modules/Bluetooth/settings.swift b/Modules/Bluetooth/settings.swift
new file mode 100644
index 00000000..a791d390
--- /dev/null
+++ b/Modules/Bluetooth/settings.swift
@@ -0,0 +1,112 @@
+//
+// settings.swift
+// Bluetooth
+//
+// Created by Serhiy Mytrovtsiy on 07/07/2021.
+// Using Swift 5.0.
+// Running on macOS 10.15.
+//
+// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved.
+//
+
+import Cocoa
+import Kit
+
+internal class Settings: NSStackView, Settings_v {
+ public var callback: (() -> Void) = {}
+ public var selectedBatteryHandler: (String) -> Void = {_ in }
+
+ private let title: String
+ private var selectedBattery: String
+ private var button: NSPopUpButton?
+
+ public init(_ title: String) {
+ self.title = title
+ self.selectedBattery = Store.shared.string(key: "\(self.title)_battery", defaultValue: "")
+
+ super.init(frame: NSRect(
+ x: 0,
+ y: 0,
+ width: Constants.Settings.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")
+ }
+
+ internal func load(widgets: [widget_t]) {
+ self.subviews.forEach{ $0.removeFromSuperview() }
+
+ self.addArrangedSubview(self.deviceSelector())
+
+ 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))
+ }
+ }
+
+ private func deviceSelector() -> NSView {
+ let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width - Constants.Settings.margin*2, height: Constants.Settings.row))
+
+ let rowTitle: NSTextField = LabelField(
+ frame: NSRect(x: 0, y: (view.frame.height - 16)/2, width: view.frame.width - 52, height: 17),
+ localizedString("Battery to show")
+ )
+ rowTitle.font = NSFont.systemFont(ofSize: 13, weight: .light)
+ rowTitle.textColor = .textColor
+
+ self.button = NSPopUpButton(frame: NSRect(x: view.frame.width - 140, y: -1, width: 140, height: 30))
+ self.button!.target = self
+ self.button?.action = #selector(self.handleSelection)
+
+ view.addSubview(rowTitle)
+ view.addSubview(self.button!)
+
+ return view
+ }
+
+ internal func setList(_ list: [BLEDevice]) {
+ var batteries: [String] = []
+ list.forEach { (d: BLEDevice) in
+ if d.batteryLevel.count == 1 {
+ batteries.append(d.name)
+ } else {
+ d.batteryLevel.forEach { (pair: KeyValue_t) in
+ batteries.append("\(d.name)@\(pair.key)")
+ }
+ }
+ }
+
+ DispatchQueue.main.async(execute: {
+ if self.button?.itemTitles.count != batteries.count {
+ self.button?.removeAllItems()
+ }
+
+ if batteries != self.button?.itemTitles {
+ self.button?.addItems(withTitles: batteries.map{ $0.replacingOccurrences(of: "@", with: " - ")})
+ if self.selectedBattery != "" {
+ self.button?.selectItem(withTitle: self.selectedBattery.replacingOccurrences(of: "@", with: " - "))
+ }
+ }
+ })
+ }
+
+ @objc private func handleSelection(_ sender: NSPopUpButton) {
+ guard let item = sender.selectedItem else { return }
+ self.selectedBattery = item.title.replacingOccurrences(of: " - ", with: "@")
+ Store.shared.set(key: "\(self.title)_battery", value: self.selectedBattery)
+ self.selectedBatteryHandler(self.selectedBattery)
+ }
+}
diff --git a/Stats.xcodeproj/project.pbxproj b/Stats.xcodeproj/project.pbxproj
index f9fef71c..b17ddbba 100644
--- a/Stats.xcodeproj/project.pbxproj
+++ b/Stats.xcodeproj/project.pbxproj
@@ -8,6 +8,12 @@
/* Begin PBXBuildFile section */
9A045EB72594F8D100ED58F2 /* Dashboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A045EB62594F8D100ED58F2 /* Dashboard.swift */; };
+ 9A11AAD6266FD77F000C1C05 /* Bluetooth.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A11AACF266FD77F000C1C05 /* Bluetooth.framework */; };
+ 9A11AAD7266FD77F000C1C05 /* Bluetooth.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9A11AACF266FD77F000C1C05 /* Bluetooth.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 9A11AAF4266FD7A7000C1C05 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A11AAF3266FD7A7000C1C05 /* main.swift */; };
+ 9A11AB26266FD828000C1C05 /* config.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9A11AB25266FD828000C1C05 /* config.plist */; };
+ 9A11AB36266FD9F4000C1C05 /* readers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A11AB35266FD9F4000C1C05 /* readers.swift */; };
+ 9A11AB67266FDB69000C1C05 /* Kit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A2846F72666A9CC00EC1F6D /* Kit.framework */; };
9A27D5352538A456001BB651 /* Reachability in Frameworks */ = {isa = PBXBuildFile; productRef = 9A27D5342538A456001BB651 /* Reachability */; };
9A2846FE2666A9CC00EC1F6D /* Kit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A2846F72666A9CC00EC1F6D /* Kit.framework */; };
9A2846FF2666A9CC00EC1F6D /* Kit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9A2846F72666A9CC00EC1F6D /* Kit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
@@ -75,6 +81,7 @@
9A81C7692449A43600825D92 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A81C7672449A43600825D92 /* main.swift */; };
9A81C76A2449A43600825D92 /* readers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A81C7682449A43600825D92 /* readers.swift */; };
9A8AE0A326921A2A00B13054 /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8AE0A226921A2A00B13054 /* Server.swift */; };
+ 9A8B923D2696445C00FD6D83 /* settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8B923C2696445C00FD6D83 /* settings.swift */; };
9A8DE58E253DEFA9006A748F /* Fans.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A8DE587253DEFA9006A748F /* Fans.framework */; };
9A8DE58F253DEFA9006A748F /* Fans.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9A8DE587253DEFA9006A748F /* Fans.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
9A8DE5E4253DF4E2006A748F /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A8DE5E3253DF4E2006A748F /* main.swift */; };
@@ -85,6 +92,7 @@
9A90E19624EAD35F00471E9A /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A90E19524EAD35F00471E9A /* main.swift */; };
9A90E19824EAD3B000471E9A /* config.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9A90E19724EAD3B000471E9A /* config.plist */; };
9A90E1A324EAD66600471E9A /* reader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A90E1A224EAD66600471E9A /* reader.swift */; };
+ 9A94B81F26822DE0001F4F2B /* popup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A94B81E26822DE0001F4F2B /* popup.swift */; };
9A953A1424B9D22D0038EF4B /* settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A953A1324B9D22D0038EF4B /* settings.swift */; };
9A97CED12537331B00742D8F /* CPU.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A97CECA2537331B00742D8F /* CPU.framework */; };
9A97CED22537331B00742D8F /* CPU.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9A97CECA2537331B00742D8F /* CPU.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
@@ -129,6 +137,20 @@
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
+ 9A11AAD4266FD77F000C1C05 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 9A1410ED229E721100D29793 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 9A11AACE266FD77F000C1C05;
+ remoteInfo = Bluetooth;
+ };
+ 9A11AB69266FDB69000C1C05 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 9A1410ED229E721100D29793 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 9A2846F62666A9CC00EC1F6D;
+ remoteInfo = Kit;
+ };
9A2846FC2666A9CC00EC1F6D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 9A1410ED229E721100D29793 /* Project object */;
@@ -270,6 +292,7 @@
9AF9EE0A24648751005D2270 /* Disk.framework in Embed Frameworks */,
9A81C75E2449A41400825D92 /* RAM.framework in Embed Frameworks */,
9AE29ADD249A50350071B02D /* Sensors.framework in Embed Frameworks */,
+ 9A11AAD7266FD77F000C1C05 /* Bluetooth.framework in Embed Frameworks */,
9A2846FF2666A9CC00EC1F6D /* Kit.framework in Embed Frameworks */,
9ABFF8FE248BEBCB00C9041A /* Battery.framework in Embed Frameworks */,
9A3E17D4247A94AF00449CD1 /* Net.framework in Embed Frameworks */,
@@ -316,6 +339,11 @@
98BF5451254DF04C004E9DF5 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = ""; };
9A00010025CFF9D6001D02B9 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = ""; };
9A045EB62594F8D100ED58F2 /* Dashboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dashboard.swift; sourceTree = ""; };
+ 9A11AACF266FD77F000C1C05 /* Bluetooth.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Bluetooth.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 9A11AAD2266FD77F000C1C05 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ 9A11AAF3266FD7A7000C1C05 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; };
+ 9A11AB25266FD828000C1C05 /* config.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = config.plist; sourceTree = ""; };
+ 9A11AB35266FD9F4000C1C05 /* readers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = readers.swift; sourceTree = ""; };
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 = ""; };
9A27D4A925389EFD001BB651 /* Stats.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Stats.entitlements; sourceTree = ""; };
@@ -378,6 +406,7 @@
9A81C7672449A43600825D92 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; };
9A81C7682449A43600825D92 /* readers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = readers.swift; sourceTree = ""; };
9A8AE0A226921A2A00B13054 /* Server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = ""; };
+ 9A8B923C2696445C00FD6D83 /* settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = settings.swift; sourceTree = ""; };
9A8DE587253DEFA9006A748F /* Fans.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Fans.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9A8DE58A253DEFA9006A748F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
9A8DE5E3253DF4E2006A748F /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; };
@@ -388,6 +417,7 @@
9A90E19524EAD35F00471E9A /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; };
9A90E19724EAD3B000471E9A /* config.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = config.plist; sourceTree = ""; };
9A90E1A224EAD66600471E9A /* reader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = reader.swift; sourceTree = ""; };
+ 9A94B81E26822DE0001F4F2B /* popup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = popup.swift; sourceTree = ""; };
9A953A1324B9D22D0038EF4B /* settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = settings.swift; sourceTree = ""; };
9A97CE2A25371B2300742D8F /* IntelPowerGadget.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IntelPowerGadget.framework; path = ../../../Library/Frameworks/IntelPowerGadget.framework; sourceTree = ""; };
9A97CECA2537331B00742D8F /* CPU.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CPU.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -441,6 +471,14 @@
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
+ 9A11AACC266FD77F000C1C05 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 9A11AB67266FDB69000C1C05 /* Kit.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
9A1410F2229E721100D29793 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -448,6 +486,7 @@
9AF9EE0924648751005D2270 /* Disk.framework in Frameworks */,
9AE29ADC249A50350071B02D /* Sensors.framework in Frameworks */,
9ABFF8FD248BEBCB00C9041A /* Battery.framework in Frameworks */,
+ 9A11AAD6266FD77F000C1C05 /* Bluetooth.framework in Frameworks */,
9A2846FE2666A9CC00EC1F6D /* Kit.framework in Frameworks */,
9A81C75D2449A41400825D92 /* RAM.framework in Frameworks */,
9A8DE58E253DEFA9006A748F /* Fans.framework in Frameworks */,
@@ -548,6 +587,19 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
+ 9A11AAD0266FD77F000C1C05 /* Bluetooth */ = {
+ isa = PBXGroup;
+ children = (
+ 9A11AAF3266FD7A7000C1C05 /* main.swift */,
+ 9A11AB35266FD9F4000C1C05 /* readers.swift */,
+ 9A94B81E26822DE0001F4F2B /* popup.swift */,
+ 9A8B923C2696445C00FD6D83 /* settings.swift */,
+ 9A11AAD2266FD77F000C1C05 /* Info.plist */,
+ 9A11AB25266FD828000C1C05 /* config.plist */,
+ );
+ path = Bluetooth;
+ sourceTree = "";
+ };
9A1410EC229E721100D29793 = {
isa = PBXGroup;
children = (
@@ -576,6 +628,7 @@
9A8DE587253DEFA9006A748F /* Fans.framework */,
9ADE6FD8265D032100D2FBA8 /* smc */,
9A2846F72666A9CC00EC1F6D /* Kit.framework */,
+ 9A11AACF266FD77F000C1C05 /* Bluetooth.framework */,
);
name = Products;
sourceTree = "";
@@ -787,6 +840,7 @@
9A8DE588253DEFA9006A748F /* Fans */,
9A3E17CD247A94AF00449CD1 /* Net */,
9ABFF8F7248BEBCB00C9041A /* Battery */,
+ 9A11AAD0266FD77F000C1C05 /* Bluetooth */,
);
path = Modules;
sourceTree = "";
@@ -845,6 +899,13 @@
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
+ 9A11AACA266FD77F000C1C05 /* Headers */ = {
+ isa = PBXHeadersBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
9A2846F22666A9CC00EC1F6D /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
@@ -911,6 +972,25 @@
/* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */
+ 9A11AACE266FD77F000C1C05 /* Bluetooth */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 9A11AADA266FD77F000C1C05 /* Build configuration list for PBXNativeTarget "Bluetooth" */;
+ buildPhases = (
+ 9A11AACA266FD77F000C1C05 /* Headers */,
+ 9A11AACB266FD77F000C1C05 /* Sources */,
+ 9A11AACC266FD77F000C1C05 /* Frameworks */,
+ 9A11AACD266FD77F000C1C05 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 9A11AB6A266FDB69000C1C05 /* PBXTargetDependency */,
+ );
+ name = Bluetooth;
+ productName = Bluetooth;
+ productReference = 9A11AACF266FD77F000C1C05 /* Bluetooth.framework */;
+ productType = "com.apple.product-type.framework";
+ };
9A1410F4229E721100D29793 /* Stats */ = {
isa = PBXNativeTarget;
buildConfigurationList = 9A141105229E721200D29793 /* Build configuration list for PBXNativeTarget "Stats" */;
@@ -936,6 +1016,7 @@
9A97CED02537331B00742D8F /* PBXTargetDependency */,
9A8DE58D253DEFA9006A748F /* PBXTargetDependency */,
9A2846FD2666A9CC00EC1F6D /* PBXTargetDependency */,
+ 9A11AAD5266FD77F000C1C05 /* PBXTargetDependency */,
);
name = Stats;
packageProductDependencies = (
@@ -1166,6 +1247,10 @@
LastUpgradeCheck = 1250;
ORGANIZATIONNAME = "Serhiy Mytrovtsiy";
TargetAttributes = {
+ 9A11AACE266FD77F000C1C05 = {
+ CreatedOnToolsVersion = 12.4;
+ LastSwiftMigration = 1240;
+ };
9A1410F4229E721100D29793 = {
CreatedOnToolsVersion = 10.2.1;
LastSwiftMigration = 1030;
@@ -1262,6 +1347,7 @@
targets = (
9A1410F4229E721100D29793 /* Stats */,
9A343526243E26A0006B19F9 /* LaunchAtLogin */,
+ 9ADE6FD7265D032100D2FBA8 /* SMC */,
9A2846F62666A9CC00EC1F6D /* Kit */,
9A97CEC92537331B00742D8F /* CPU */,
9A90E18824EAD2BB00471E9A /* GPU */,
@@ -1271,12 +1357,20 @@
9ABFF8F5248BEBCB00C9041A /* Battery */,
9AE29AD4249A50350071B02D /* Sensors */,
9A8DE586253DEFA9006A748F /* Fans */,
- 9ADE6FD7265D032100D2FBA8 /* SMC */,
+ 9A11AACE266FD77F000C1C05 /* Bluetooth */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
+ 9A11AACD266FD77F000C1C05 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 9A11AB26266FD828000C1C05 /* config.plist in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
9A1410F3229E721100D29793 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -1390,6 +1484,17 @@
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
+ 9A11AACB266FD77F000C1C05 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 9A11AB36266FD9F4000C1C05 /* readers.swift in Sources */,
+ 9A94B81F26822DE0001F4F2B /* popup.swift in Sources */,
+ 9A8B923D2696445C00FD6D83 /* settings.swift in Sources */,
+ 9A11AAF4266FD7A7000C1C05 /* main.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
9A1410F1229E721100D29793 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -1551,6 +1656,16 @@
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
+ 9A11AAD5266FD77F000C1C05 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 9A11AACE266FD77F000C1C05 /* Bluetooth */;
+ targetProxy = 9A11AAD4266FD77F000C1C05 /* PBXContainerItemProxy */;
+ };
+ 9A11AB6A266FDB69000C1C05 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 9A2846F62666A9CC00EC1F6D /* Kit */;
+ targetProxy = 9A11AB69266FDB69000C1C05 /* PBXContainerItemProxy */;
+ };
9A2846FD2666A9CC00EC1F6D /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 9A2846F62666A9CC00EC1F6D /* Kit */;
@@ -1671,6 +1786,67 @@
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
+ 9A11AAD8266FD77F000C1C05 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_IDENTITY = "-";
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ CURRENT_PROJECT_VERSION = 1;
+ DEFINES_MODULE = YES;
+ DEVELOPMENT_TEAM = RP2S87B72W;
+ DYLIB_COMPATIBILITY_VERSION = 1;
+ DYLIB_CURRENT_VERSION = 1;
+ DYLIB_INSTALL_NAME_BASE = "@rpath";
+ INFOPLIST_FILE = Modules/Bluetooth/Info.plist;
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ "@loader_path/Frameworks",
+ );
+ MACOSX_DEPLOYMENT_TARGET = 10.14;
+ PRODUCT_BUNDLE_IDENTIFIER = eu.exelban.Stats.Bluetooth;
+ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+ SKIP_INSTALL = YES;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ VERSION_INFO_PREFIX = "";
+ };
+ name = Debug;
+ };
+ 9A11AAD9266FD77F000C1C05 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_IDENTITY = "-";
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ CURRENT_PROJECT_VERSION = 1;
+ DEFINES_MODULE = YES;
+ DEVELOPMENT_TEAM = RP2S87B72W;
+ DYLIB_COMPATIBILITY_VERSION = 1;
+ DYLIB_CURRENT_VERSION = 1;
+ DYLIB_INSTALL_NAME_BASE = "@rpath";
+ INFOPLIST_FILE = Modules/Bluetooth/Info.plist;
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ "@loader_path/Frameworks",
+ );
+ MACOSX_DEPLOYMENT_TARGET = 10.14;
+ PRODUCT_BUNDLE_IDENTIFIER = eu.exelban.Stats.Bluetooth;
+ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+ SKIP_INSTALL = YES;
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ VERSION_INFO_PREFIX = "";
+ };
+ name = Release;
+ };
9A141103229E721200D29793 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -2528,6 +2704,15 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
+ 9A11AADA266FD77F000C1C05 /* Build configuration list for PBXNativeTarget "Bluetooth" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 9A11AAD8266FD77F000C1C05 /* Debug */,
+ 9A11AAD9266FD77F000C1C05 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
9A1410F0229E721100D29793 /* Build configuration list for PBXProject "Stats" */ = {
isa = XCConfigurationList;
buildConfigurations = (
diff --git a/Stats/AppDelegate.swift b/Stats/AppDelegate.swift
index 82baf4cd..3c53c577 100755
--- a/Stats/AppDelegate.swift
+++ b/Stats/AppDelegate.swift
@@ -18,6 +18,7 @@ import Battery
import Sensors
import GPU
import Fans
+import Bluetooth
let updater = macAppUpdater(user: "exelban", repo: "stats")
var modules: [Module] = [
@@ -28,7 +29,8 @@ var modules: [Module] = [
Sensors(),
Fans(),
Network(),
- Battery()
+ Battery(),
+ Bluetooth()
]
class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDelegate {