From 53138f1ceeed81060ed6609193baec7182a30755 Mon Sep 17 00:00:00 2001 From: Serhiy Mytrovtsiy Date: Thu, 5 Sep 2019 15:35:18 +0200 Subject: [PATCH] created view for Battery module --- Podfile.lock | 4 +- Stats.xcodeproj/project.pbxproj | 12 +- Stats/AppDelegate.swift | 16 +- Stats/Modules/Battery/Battery.swift | 26 +- Stats/Modules/Battery/BatteryReader.swift | 132 ++++++++- Stats/Modules/Battery/BatteryView.swift | 258 ++++++++++++++++++ Stats/Modules/CPU/CPUReader.swift | 2 +- Stats/Modules/CPU/CPUView.swift | 44 +-- Stats/Modules/Memory/MemoryReader.swift | 8 +- Stats/Modules/Memory/MemoryView.swift | 44 +-- Stats/Views/MainViewController.swift | 30 ++ ...{BatteryView.swift => BatteryWidget.swift} | 2 +- Stats/libs/Extensions.swift | 33 +++ 13 files changed, 488 insertions(+), 123 deletions(-) create mode 100644 Stats/Modules/Battery/BatteryView.swift rename Stats/Widgets/{BatteryView.swift => BatteryWidget.swift} (99%) diff --git a/Podfile.lock b/Podfile.lock index 2aef7da9..944f6ce6 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -17,6 +17,6 @@ SPEC CHECKSUMS: Charts: ec1f57f9340054155691e84d4544a1d239d382c5 LaunchAtLogin: 550b0cbbdaf1b13f87a0fab6a3f8e2fbafe067fe -PODFILE CHECKSUM: dee05cc24d20d667671a5f594bfbb948dcc3d791 +PODFILE CHECKSUM: b73e93f3b5879b0f0bf59fcb6d58288fb63aabc0 -COCOAPODS: 1.7.1 +COCOAPODS: 1.7.5 diff --git a/Stats.xcodeproj/project.pbxproj b/Stats.xcodeproj/project.pbxproj index d79685b2..bfeed53d 100644 --- a/Stats.xcodeproj/project.pbxproj +++ b/Stats.xcodeproj/project.pbxproj @@ -10,7 +10,7 @@ 628D2DE0AAA753E9F47625B0 /* Pods_Stats.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56B63995EBD3A1D1EBD3AF38 /* Pods_Stats.framework */; }; 9A09C89E22B3A7C90018426F /* Battery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A09C89D22B3A7C90018426F /* Battery.swift */; }; 9A09C8A022B3A7E20018426F /* BatteryReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A09C89F22B3A7E20018426F /* BatteryReader.swift */; }; - 9A09C8A222B3D94D0018426F /* BatteryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A09C8A122B3D94D0018426F /* BatteryView.swift */; }; + 9A09C8A222B3D94D0018426F /* BatteryWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A09C8A122B3D94D0018426F /* BatteryWidget.swift */; }; 9A1410F9229E721100D29793 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1410F8229E721100D29793 /* AppDelegate.swift */; }; 9A141100229E721200D29793 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9A1410FE229E721200D29793 /* Main.storyboard */; }; 9A426DB822C2B5EE00C064C4 /* macAppUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A426DB722C2B5EE00C064C4 /* macAppUpdater.swift */; }; @@ -24,6 +24,7 @@ 9A59AE56231EE02F007989D6 /* ChartMarker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A59AE55231EE02F007989D6 /* ChartMarker.swift */; }; 9A5B1CBF229E78F0008B9D3C /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A5B1CBE229E78F0008B9D3C /* Observable.swift */; }; 9A5B1CC5229E7B40008B9D3C /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A5B1CC4229E7B40008B9D3C /* Extensions.swift */; }; + 9A606B482321025C00642F51 /* BatteryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A606B472321025C00642F51 /* BatteryView.swift */; }; 9A6CFC0122A1C9F5001E782D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9A6CFC0022A1C9F5001E782D /* Assets.xcassets */; }; 9A74D59722B44498004FE1FA /* Mini.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A74D59622B44498004FE1FA /* Mini.swift */; }; 9A79B36A22D3BEE600BF1C3A /* Widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A79B36922D3BEE600BF1C3A /* Widget.swift */; }; @@ -67,7 +68,7 @@ 56B63995EBD3A1D1EBD3AF38 /* Pods_Stats.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Stats.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9A09C89D22B3A7C90018426F /* Battery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Battery.swift; sourceTree = ""; }; 9A09C89F22B3A7E20018426F /* BatteryReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryReader.swift; sourceTree = ""; }; - 9A09C8A122B3D94D0018426F /* BatteryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryView.swift; sourceTree = ""; }; + 9A09C8A122B3D94D0018426F /* BatteryWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryWidget.swift; sourceTree = ""; }; 9A1410F5229E721100D29793 /* Stats.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Stats.app; sourceTree = BUILT_PRODUCTS_DIR; }; 9A1410F8229E721100D29793 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 9A1410FF229E721200D29793 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; @@ -84,6 +85,7 @@ 9A59AE55231EE02F007989D6 /* ChartMarker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartMarker.swift; sourceTree = ""; }; 9A5B1CBE229E78F0008B9D3C /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; 9A5B1CC4229E7B40008B9D3C /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; + 9A606B472321025C00642F51 /* BatteryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryView.swift; sourceTree = ""; }; 9A6CFC0022A1C9F5001E782D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 9A74D59622B44498004FE1FA /* Mini.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mini.swift; sourceTree = ""; }; 9A79B36922D3BEE600BF1C3A /* Widget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Widget.swift; sourceTree = ""; }; @@ -126,6 +128,7 @@ children = ( 9A09C89D22B3A7C90018426F /* Battery.swift */, 9A09C89F22B3A7E20018426F /* BatteryReader.swift */, + 9A606B472321025C00642F51 /* BatteryView.swift */, ); path = Battery; sourceTree = ""; @@ -214,7 +217,7 @@ children = ( 9AF0F31922DA923100026AE6 /* Network */, 9AF0F31822DA922800026AE6 /* Charts */, - 9A09C8A122B3D94D0018426F /* BatteryView.swift */, + 9A09C8A122B3D94D0018426F /* BatteryWidget.swift */, 9A74D59622B44498004FE1FA /* Mini.swift */, 9A79B36922D3BEE600BF1C3A /* Widget.swift */, ); @@ -446,7 +449,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 9A09C8A222B3D94D0018426F /* BatteryView.swift in Sources */, + 9A09C8A222B3D94D0018426F /* BatteryWidget.swift in Sources */, 9A426DB822C2B5EE00C064C4 /* macAppUpdater.swift in Sources */, 9A79B36E22D3BEF900BF1C3A /* Reader.swift in Sources */, 9A59AE54231ED1AC007989D6 /* CPUView.swift in Sources */, @@ -470,6 +473,7 @@ 9A1410F9229E721100D29793 /* AppDelegate.swift in Sources */, 9AF0F32722DA92DD00026AE6 /* NetworkDotsText.swift in Sources */, 9AF0F32922DA92E800026AE6 /* NetworkArrowsText.swift in Sources */, + 9A606B482321025C00642F51 /* BatteryView.swift in Sources */, 9AF0F31F22DA925700026AE6 /* BarChart.swift in Sources */, 9AF0F31B22DA924000026AE6 /* LineChart.swift in Sources */, 9A59AE56231EE02F007989D6 /* ChartMarker.swift in Sources */, diff --git a/Stats/AppDelegate.swift b/Stats/AppDelegate.swift index b8977591..495cdeca 100755 --- a/Stats/AppDelegate.swift +++ b/Stats/AppDelegate.swift @@ -12,7 +12,7 @@ import LaunchAtLogin let modules: Observable<[Module]> = Observable([CPU(), Memory(), Disk(), Battery(), Network()]) let updater = macAppUpdater(user: "exelban", repo: "stats") -let menu = NSPopover() +let popover = NSPopover() @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { @@ -26,8 +26,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { } menuBarButton.action = #selector(toggleMenu) - menu.contentViewController = MainViewController.Init() - menu.behavior = NSPopover.Behavior.transient + popover.contentViewController = MainViewController.Init() + popover.behavior = NSPopover.Behavior.transient _ = MenuBar(menuBarItem, menuBarButton: menuBarButton) @@ -72,19 +72,19 @@ class AppDelegate: NSObject, NSApplicationDelegate { } @objc func toggleMenu(_ sender: Any?) { - if menu.isShown { - menu.performClose(sender) + if popover.isShown { + popover.performClose(sender) } else { if let button = self.menuBarItem.button { NSApplication.shared.activate(ignoringOtherApps: true) - menu.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY) - menu.becomeFirstResponder() + popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY) + popover.becomeFirstResponder() } } } func applicationWillResignActive(_ notification: Notification) { - menu.performClose(self) + popover.performClose(self) } } diff --git a/Stats/Modules/Battery/Battery.swift b/Stats/Modules/Battery/Battery.swift index a42f0da5..e04aaeb0 100644 --- a/Stats/Modules/Battery/Battery.swift +++ b/Stats/Modules/Battery/Battery.swift @@ -28,41 +28,23 @@ class Battery: Module { self.available = Observable(self.reader.available) self.active = Observable(defaults.object(forKey: name) != nil ? defaults.bool(forKey: name) : true) self.percentageView = Observable(defaults.object(forKey: "\(self.name)_percentage") != nil ? defaults.bool(forKey: "\(self.name)_percentage") : false) - self.view = BatteryView(frame: NSMakeRect(0, 0, widgetSize.width, widgetSize.height)) + self.view = BatteryWidget(frame: NSMakeRect(0, 0, widgetSize.width, widgetSize.height)) initMenu() initWidget() initTab() } - func initTab() { - self.tabView.view?.frame = NSRect(x: 0, y: 0, width: TabWidth, height: TabHeight) - - let text: NSTextField = NSTextField(string: self.name) - text.isEditable = false - text.isSelectable = false - text.isBezeled = false - text.wantsLayer = true - text.textColor = .labelColor - text.canDrawSubviewsIntoLayer = true - text.alignment = .natural - text.font = NSFont.systemFont(ofSize: 13, weight: .regular) - text.frame.origin.x = ((self.tabView.view?.frame.size.width)! - 50) / 2 - text.frame.origin.y = ((self.tabView.view?.frame.size.height)! - 22) / 2 - - self.tabView.view?.addSubview(text) - } - func start() { if !self.reader.value.value.isEmpty { let value = self.reader.value!.value - (self.view as! BatteryView).setCharging(value: value.first! > 0) + (self.view as! BatteryWidget).setCharging(value: value.first! > 0) (self.view as! Widget).setValue(data: [abs(value.first!)]) } self.reader.start() self.reader.value.subscribe(observer: self) { (value, _) in if !value.isEmpty { - (self.view as! BatteryView).setCharging(value: value.first! > 0) + (self.view as! BatteryWidget).setCharging(value: value.first! > 0) (self.view as! Widget).setValue(data: [abs(value.first!)]) } } @@ -70,7 +52,7 @@ class Battery: Module { func initWidget() { self.active << false - (self.view as! BatteryView).setPercentage(value: self.percentageView.value) + (self.view as! BatteryWidget).setPercentage(value: self.percentageView.value) self.active << true } diff --git a/Stats/Modules/Battery/BatteryReader.swift b/Stats/Modules/Battery/BatteryReader.swift index 8b97b1a0..fcc4cd35 100644 --- a/Stats/Modules/Battery/BatteryReader.swift +++ b/Stats/Modules/Battery/BatteryReader.swift @@ -9,10 +9,45 @@ import Foundation import IOKit.ps +struct BatteryUsage { + var powerSource: String = "" + var state: String = "" + var isCharged: Bool = false + var capacity: Double = 0 + var cycles: Int = 0 + var health: Int = 0 + + var amperage: Int = 0 + var voltage: Double = 0 + var temperature: Double = 0 + + var ACwatts: Int = 0 + var ACstatus: Bool = false + + var timeToEmpty: Int = 0 + var timeToCharge: Int = 0 +} + class BatteryReader: Reader { - var value: Observable<[Double]>! - var available: Bool = false - var updateTimer: Timer! + public var value: Observable<[Double]>! + public var usage: Observable = Observable(BatteryUsage()) + public var updateTimer: Timer! + + private var service: io_connect_t = 0 + private var internalChecked: Bool = false + private var hasInternalBattery: Bool = false + + public var available: Bool { + get { + if !self.internalChecked { + let snapshot = IOPSCopyPowerSourcesInfo().takeRetainedValue() + let sources = IOPSCopyPowerSourcesList(snapshot).takeRetainedValue() as Array + self.hasInternalBattery = sources.count > 0 + self.internalChecked = true + } + return self.hasInternalBattery + } + } init() { self.value = Observable([]) @@ -20,6 +55,7 @@ class BatteryReader: Reader { } func start() { + self.service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("AppleSmartBattery")) if updateTimer != nil { return } @@ -32,25 +68,101 @@ class BatteryReader: Reader { } updateTimer.invalidate() updateTimer = nil + + IOServiceClose(self.service) + IOObjectRelease(self.service) } @objc func read() { let psInfo = IOPSCopyPowerSourcesInfo().takeRetainedValue() let psList = IOPSCopyPowerSourcesList(psInfo).takeRetainedValue() as [CFTypeRef] - self.available = psList.count != 0 for ps in psList { - if let psDesc = IOPSGetPowerSourceDescription(psInfo, ps).takeUnretainedValue() as? [String: Any] { - let powerSourceState = (psDesc[kIOPSPowerSourceStateKey] as? String) - let isCharged = (psDesc[kIOPSIsChargedKey] as? Bool) - var cap: Float = Float(psDesc[kIOPSCurrentCapacityKey] as! Int) / 100 + if let list = IOPSGetPowerSourceDescription(psInfo, ps).takeUnretainedValue() as? Dictionary { + let powerSource = list[kIOPSPowerSourceStateKey] as? String ?? "AC Power" + let state = list[kIOPSBatteryHealthKey] as! String + let isCharged = list[kIOPSIsChargedKey] as? Bool ?? false + var cap = Float(list[kIOPSCurrentCapacityKey] as! Int) / 100 + + let timeToEmpty = Int(list[kIOPSTimeToEmptyKey] as! Int) + let timeToCharged = Int(list[kIOPSTimeToFullChargeKey] as! Int) + + let cycles = self.getIntValue("CycleCount" as CFString) ?? 0 - if isCharged == nil && powerSourceState! == "Battery Power" { + let maxCapacity = self.getIntValue("MaxCapacity" as CFString) ?? 1 + let designCapacity = self.getIntValue("DesignCapacity" as CFString) ?? 1 + + let amperage = self.getIntValue("Amperage" as CFString) ?? 0 + let voltage = self.getVoltage() ?? 0 + let temperature = self.getTemperature() ?? 0 + + var ACwatts: Int = 0 + if let ACDetails = IOPSCopyExternalPowerAdapterDetails() { + if let ACList = ACDetails.takeUnretainedValue() as? Dictionary { + ACwatts = Int(ACList[kIOPSPowerAdapterWattsKey] as! Int) + } + } + let ACstatus = self.getBoolValue("IsCharging" as CFString) ?? false + + self.usage << BatteryUsage( + powerSource: powerSource, + state: state, + isCharged: isCharged, + capacity: Double(cap), + cycles: cycles, + health: (100 * maxCapacity) / designCapacity, + + amperage: amperage, + voltage: voltage, + temperature: temperature, + + ACwatts: ACwatts, + ACstatus: ACstatus, + + timeToEmpty: timeToEmpty, + timeToCharge: timeToCharged + ) + + if powerSource == "Battery Power" { cap = 0 - cap } - self.value << [Double(cap)] } } } + + func getBoolValue(_ forIdentifier: CFString) -> Bool? { + if let value = IORegistryEntryCreateCFProperty(self.service, forIdentifier, kCFAllocatorDefault, 0) { + return value.takeRetainedValue() as? Bool + } + return nil + } + + func getIntValue(_ identifier: CFString) -> Int? { + if let value = IORegistryEntryCreateCFProperty(self.service, identifier, kCFAllocatorDefault, 0) { + return value.takeRetainedValue() as? Int + } + return nil + } + + func getDoubleValue(_ identifier: CFString) -> Double? { + if let value = IORegistryEntryCreateCFProperty(self.service, identifier, kCFAllocatorDefault, 0) { + return value.takeRetainedValue() as? Double + } + return nil + } + + func getVoltage() -> Double? { + if let value = self.getDoubleValue("Voltage" as CFString) { + return value / 1000.0 + } + return nil + } + + func getTemperature() -> Double? { + if let value = IORegistryEntryCreateCFProperty(self.service, "Temperature" as CFString, kCFAllocatorDefault, 0) { + return value.takeRetainedValue() as! Double / 100.0 + } + return nil + } } diff --git a/Stats/Modules/Battery/BatteryView.swift b/Stats/Modules/Battery/BatteryView.swift new file mode 100644 index 00000000..8037d97c --- /dev/null +++ b/Stats/Modules/Battery/BatteryView.swift @@ -0,0 +1,258 @@ +// +// BatteryView.swift +// Stats +// +// Created by Serhiy Mytrovtsiy on 05/09/2019. +// Copyright © 2019 Serhiy Mytrovtsiy. All rights reserved. +// + +import Foundation +import Cocoa + +extension Battery { + + func initTab() { + self.tabView.view?.frame = NSRect(x: 0, y: 0, width: TabWidth, height: 10) + + makeMain() + makeOverview() + makeBattery() + makePowerAdapter() + } + + func makeMain() { + let stackHeight: CGFloat = 22 + let vertical: NSStackView = NSStackView(frame: NSRect(x: 0, y: TabHeight - stackHeight*3 - 4, width: TabWidth, height: stackHeight*3)) + vertical.orientation = .vertical + + let level: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*2, width: TabWidth - 20, height: stackHeight)) + level.orientation = .horizontal + level.distribution = .equalCentering + let levelLabel = LabelField(string: "Level") + let levelValue = ValueField(string: "0%") + level.addView(levelLabel, in: .center) + level.addView(levelValue, in: .center) + + let source: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*1, width: TabWidth - 20, height: stackHeight)) + source.orientation = .horizontal + source.distribution = .equalCentering + let sourceLabel = LabelField(string: "Source") + let sourceValue = ValueField(string: "AC Power") + source.addView(sourceLabel, in: .center) + source.addView(sourceValue, in: .center) + + let time: NSStackView = NSStackView(frame: NSRect(x: 10, y: 0, width: TabWidth - 20, height: stackHeight)) + time.orientation = .horizontal + time.distribution = .equalCentering + let timeLabel = LabelField(string: "Time to charge") + let timeValue = ValueField(string: "Calculating") + time.addView(timeLabel, in: .center) + time.addView(timeValue, in: .center) + + vertical.addSubview(level) + vertical.addSubview(source) + vertical.addSubview(time) + + self.tabView.view?.addSubview(vertical) + + (self.reader as! BatteryReader).usage.subscribe(observer: self) { (value, _) in + levelValue.stringValue = "\(Int(value.capacity * 100))%" + sourceValue.stringValue = value.powerSource + + if value.powerSource == "Battery Power" { + timeLabel.stringValue = "Time to discharge" + if value.timeToEmpty != -1 && value.timeToEmpty != 0 { + timeValue.stringValue = Double(value.timeToEmpty*60).printSecondsToHoursMinutesSeconds() + } + } else { + timeLabel.stringValue = "Time to charge" + if value.timeToCharge != -1 && value.timeToCharge != 0 { + timeValue.stringValue = Double(value.timeToCharge*60).printSecondsToHoursMinutesSeconds() + } + } + + if value.timeToEmpty == -1 || value.timeToEmpty == -1 { + timeValue.stringValue = "Calculating" + } + + if value.isCharged { + timeValue.stringValue = "Fully charged" + } + } + } + + func makeOverview() { + let overviewLabel: NSView = NSView(frame: NSRect(x: 0, y: TabHeight - 102, width: TabWidth, height: 25)) + + overviewLabel.wantsLayer = true + overviewLabel.layer?.backgroundColor = NSColor(hexString: "#eeeeee", alpha: 0.5).cgColor + + let overviewText: NSTextField = NSTextField(string: "Overview") + overviewText.frame = NSRect(x: 0, y: 0, width: TabWidth, height: overviewLabel.frame.size.height - 4) + overviewText.isEditable = false + overviewText.isSelectable = false + overviewText.isBezeled = false + overviewText.wantsLayer = true + overviewText.textColor = .darkGray + overviewText.canDrawSubviewsIntoLayer = true + overviewText.alignment = .center + overviewText.backgroundColor = NSColor(hexString: "#dddddd", alpha: 0) + overviewText.font = NSFont.systemFont(ofSize: 12, weight: .medium) + + overviewLabel.addSubview(overviewText) + self.tabView.view?.addSubview(overviewLabel) + + let stackHeight: CGFloat = 22 + let vertical: NSStackView = NSStackView(frame: NSRect(x: 0, y: 184, width: TabWidth, height: stackHeight*3)) + vertical.orientation = .vertical + + let cycles: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*2, width: TabWidth - 20, height: stackHeight)) + cycles.orientation = .horizontal + cycles.distribution = .equalCentering + let cyclesLabel = LabelField(string: "Cycles") + let cyclesValue = ValueField(string: "0") + cycles.addView(cyclesLabel, in: .center) + cycles.addView(cyclesValue, in: .center) + + let health: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*1, width: TabWidth - 20, height: stackHeight)) + health.orientation = .horizontal + health.distribution = .equalCentering + let healthLabel = LabelField(string: "Health") + let healthValue = ValueField(string: "Calculating") + health.addView(healthLabel, in: .center) + health.addView(healthValue, in: .center) + + let state: NSStackView = NSStackView(frame: NSRect(x: 10, y: 0, width: TabWidth - 20, height: stackHeight)) + state.orientation = .horizontal + state.distribution = .equalCentering + let stateLabel = LabelField(string: "State") + let stateValue = ValueField(string: "Calculating") + state.addView(stateLabel, in: .center) + state.addView(stateValue, in: .center) + + vertical.addSubview(cycles) + vertical.addSubview(health) + vertical.addSubview(state) + + self.tabView.view?.addSubview(vertical) + + (self.reader as! BatteryReader).usage.subscribe(observer: self) { (value, _) in + cyclesValue.stringValue = "\(value.cycles)" + stateValue.stringValue = value.state + healthValue.stringValue = "\(value.health)%" + } + } + + func makeBattery() { + let batteryLabel: NSView = NSView(frame: NSRect(x: 0, y: TabHeight - 202, width: TabWidth, height: 25)) + + batteryLabel.wantsLayer = true + batteryLabel.layer?.backgroundColor = NSColor(hexString: "#eeeeee", alpha: 0.5).cgColor + + let overviewText: NSTextField = NSTextField(string: "Battery") + overviewText.frame = NSRect(x: 0, y: 0, width: TabWidth, height: batteryLabel.frame.size.height - 4) + overviewText.isEditable = false + overviewText.isSelectable = false + overviewText.isBezeled = false + overviewText.wantsLayer = true + overviewText.textColor = .darkGray + overviewText.canDrawSubviewsIntoLayer = true + overviewText.alignment = .center + overviewText.backgroundColor = NSColor(hexString: "#dddddd", alpha: 0) + overviewText.font = NSFont.systemFont(ofSize: 12, weight: .medium) + + batteryLabel.addSubview(overviewText) + self.tabView.view?.addSubview(batteryLabel) + + let stackHeight: CGFloat = 22 + let vertical: NSStackView = NSStackView(frame: NSRect(x: 0, y: TabHeight - 273, width: TabWidth, height: stackHeight*3)) + vertical.orientation = .vertical + + let amperage: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*2, width: TabWidth - 20, height: stackHeight)) + amperage.orientation = .horizontal + amperage.distribution = .equalCentering + let amperageLabel = LabelField(string: "Amperage") + let amperageValue = ValueField(string: "0 mA") + amperage.addView(amperageLabel, in: .center) + amperage.addView(amperageValue, in: .center) + + let voltage: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*1, width: TabWidth - 20, height: stackHeight)) + voltage.orientation = .horizontal + voltage.distribution = .equalCentering + let voltageLabel = LabelField(string: "Voltage") + let voltageValue = ValueField(string: "0 V") + voltage.addView(voltageLabel, in: .center) + voltage.addView(voltageValue, in: .center) + + let temperature: NSStackView = NSStackView(frame: NSRect(x: 10, y: 0, width: TabWidth - 20, height: stackHeight)) + temperature.orientation = .horizontal + temperature.distribution = .equalCentering + let temperatureLabel = LabelField(string: "Temperature") + let temperatureValue = ValueField(string: "0 °C") + temperature.addView(temperatureLabel, in: .center) + temperature.addView(temperatureValue, in: .center) + + vertical.addSubview(amperage) + vertical.addSubview(voltage) + vertical.addSubview(temperature) + + self.tabView.view?.addSubview(vertical) + (self.reader as! BatteryReader).usage.subscribe(observer: self) { (value, _) in + amperageValue.stringValue = "\(value.amperage) mA" + voltageValue.stringValue = "\(value.voltage.roundTo(decimalPlaces: 2)) V" + temperatureValue.stringValue = "\(value.temperature) °C" + } + } + + func makePowerAdapter() { + let powerAdapterLabel: NSView = NSView(frame: NSRect(x: 0, y: 52, width: TabWidth, height: 25)) + + powerAdapterLabel.wantsLayer = true + powerAdapterLabel.layer?.backgroundColor = NSColor(hexString: "#eeeeee", alpha: 0.5).cgColor + + let overviewText: NSTextField = NSTextField(string: "Power adapter") + overviewText.frame = NSRect(x: 0, y: 0, width: TabWidth, height: powerAdapterLabel.frame.size.height - 4) + overviewText.isEditable = false + overviewText.isSelectable = false + overviewText.isBezeled = false + overviewText.wantsLayer = true + overviewText.textColor = .darkGray + overviewText.canDrawSubviewsIntoLayer = true + overviewText.alignment = .center + overviewText.backgroundColor = NSColor(hexString: "#dddddd", alpha: 0) + overviewText.font = NSFont.systemFont(ofSize: 12, weight: .medium) + + powerAdapterLabel.addSubview(overviewText) + self.tabView.view?.addSubview(powerAdapterLabel) + + let stackHeight: CGFloat = 22 + let vertical: NSStackView = NSStackView(frame: NSRect(x: 0, y: 4, width: TabWidth, height: stackHeight*2)) + vertical.orientation = .vertical + + let power: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*1, width: TabWidth - 20, height: stackHeight)) + power.orientation = .horizontal + power.distribution = .equalCentering + let powerLabel = LabelField(string: "Power") + let powerValue = ValueField(string: "0 W") + power.addView(powerLabel, in: .center) + power.addView(powerValue, in: .center) + + let charging: NSStackView = NSStackView(frame: NSRect(x: 10, y: 0, width: TabWidth - 20, height: stackHeight)) + charging.orientation = .horizontal + charging.distribution = .equalCentering + let chargingLabel = LabelField(string: "Is charging") + let chargingValue = ValueField(string: "No") + charging.addView(chargingLabel, in: .center) + charging.addView(chargingValue, in: .center) + + vertical.addSubview(power) + vertical.addSubview(charging) + + self.tabView.view?.addSubview(vertical) + + (self.reader as! BatteryReader).usage.subscribe(observer: self) { (value, _) in + powerValue.stringValue = value.powerSource == "Battery Power" ? "Not connected" : "\(value.ACwatts) W" + chargingValue.stringValue = value.ACstatus ? "Yes" : "No" + } + } +} diff --git a/Stats/Modules/CPU/CPUReader.swift b/Stats/Modules/CPU/CPUReader.swift index 11d144b2..185f928d 100644 --- a/Stats/Modules/CPU/CPUReader.swift +++ b/Stats/Modules/CPU/CPUReader.swift @@ -46,7 +46,7 @@ class CPUReader: Reader { self.value = Observable([]) self.topProcess.launchPath = "/usr/bin/top" - self.topProcess.arguments = ["-s", "1", "-o", "cpu", "-n", "5", "-stats", "pid,command,cpu"] + self.topProcess.arguments = ["-s", "3", "-o", "cpu", "-n", "5", "-stats", "pid,command,cpu"] self.topProcess.standardOutput = pipe mibKeys.withUnsafeBufferPointer() { mib in diff --git a/Stats/Modules/CPU/CPUView.swift b/Stats/Modules/CPU/CPUView.swift index 28f17251..b6c03e77 100644 --- a/Stats/Modules/CPU/CPUView.swift +++ b/Stats/Modules/CPU/CPUView.swift @@ -121,24 +121,24 @@ extension CPU { let system: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*2, width: TabWidth - 20, height: stackHeight)) system.orientation = .horizontal system.distribution = .equalCentering - let systemLabel = labelField(string: "System") - let systemValue = valueField(string: "0 %") + let systemLabel = LabelField(string: "System") + let systemValue = ValueField(string: "0 %") system.addView(systemLabel, in: .center) system.addView(systemValue, in: .center) let user: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*1, width: TabWidth - 20, height: stackHeight)) user.orientation = .horizontal user.distribution = .equalCentering - let userLabel = labelField(string: "User") - let userValue = valueField(string: "0 %") + let userLabel = LabelField(string: "User") + let userValue = ValueField(string: "0 %") user.addView(userLabel, in: .center) user.addView(userValue, in: .center) let idle: NSStackView = NSStackView(frame: NSRect(x: 10, y: 0, width: TabWidth - 20, height: stackHeight)) idle.orientation = .horizontal idle.distribution = .equalCentering - let idleLabel = labelField(string: "Idle") - let idleValue = valueField(string: "0 %") + let idleLabel = LabelField(string: "Idle") + let idleValue = ValueField(string: "0 %") idle.addView(idleLabel, in: .center) idle.addView(idleValue, in: .center) @@ -220,39 +220,11 @@ extension CPU { let view: NSStackView = NSStackView(frame: NSRect(x: 10, y: CGFloat(num)*height, width: TabWidth - 20, height: height)) view.orientation = .horizontal view.distribution = .equalCentering - let viewLabel = labelField(string: label) - let viewValue = valueField(string: value) + let viewLabel = LabelField(string: label) + let viewValue = ValueField(string: value) view.addView(viewLabel, in: .center) view.addView(viewValue, in: .center) return view } - - func labelField(string: String) -> NSTextField { - let label: NSTextField = NSTextField(string: string) - - label.isEditable = false - label.isSelectable = false - label.isBezeled = false - label.textColor = .black - label.alignment = .center - label.font = NSFont.systemFont(ofSize: 12, weight: .regular) - label.backgroundColor = NSColor(hexString: "#dddddd", alpha: 0) - - return label - } - - func valueField(string: String) -> NSTextField { - let label: NSTextField = NSTextField(string: string) - - label.isEditable = false - label.isSelectable = false - label.isBezeled = false - label.textColor = .black - label.alignment = .center - label.font = NSFont.systemFont(ofSize: 13, weight: .regular) - label.backgroundColor = NSColor(hexString: "#dddddd", alpha: 0) - - return label - } } diff --git a/Stats/Modules/Memory/MemoryReader.swift b/Stats/Modules/Memory/MemoryReader.swift index 7603e66c..2f5d010b 100644 --- a/Stats/Modules/Memory/MemoryReader.swift +++ b/Stats/Modules/Memory/MemoryReader.swift @@ -31,7 +31,7 @@ class MemoryReader: Reader { var count = UInt32(MemoryLayout.size / MemoryLayout.size) self.topProcess.launchPath = "/usr/bin/top" - self.topProcess.arguments = ["-s", "1", "-o", "mem", "-n", "5", "-stats", "pid,command,mem"] + self.topProcess.arguments = ["-s", "3", "-o", "mem", "-n", "5", "-stats", "pid,command,mem"] self.topProcess.standardOutput = pipe let kerr: kern_return_t = withUnsafeMutablePointer(to: &stats) { @@ -79,8 +79,10 @@ class MemoryReader: Reader { let arr = line.condenseWhitespace().split(separator: " ") let pid = Int(arr[0]) ?? 0 let command = String(arr[1]) - let usage = Double(arr[2].filter("01234567890.".contains))! * Double(1024 * 1024) - let process = TopProcess(pid: pid, command: command, usage: usage) + guard let usage = Double(arr[2].filter("01234567890.".contains)) else { + return + } + let process = TopProcess(pid: pid, command: command, usage: usage * Double(1024 * 1024)) processes.append(process) } } diff --git a/Stats/Modules/Memory/MemoryView.swift b/Stats/Modules/Memory/MemoryView.swift index ecf1ac20..69bd659e 100644 --- a/Stats/Modules/Memory/MemoryView.swift +++ b/Stats/Modules/Memory/MemoryView.swift @@ -121,24 +121,24 @@ extension Memory { let total: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*2, width: TabWidth - 20, height: stackHeight)) total.orientation = .horizontal total.distribution = .equalCentering - let totalLabel = labelField(string: "Total") - let totalValue = valueField(string: "0 GB") + let totalLabel = LabelField(string: "Total") + let totalValue = ValueField(string: "0 GB") total.addView(totalLabel, in: .center) total.addView(totalValue, in: .center) let used: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*1, width: TabWidth - 20, height: stackHeight)) used.orientation = .horizontal used.distribution = .equalCentering - let usedLabel = labelField(string: "Used") - let usedValue = valueField(string: "0 GB") + let usedLabel = LabelField(string: "Used") + let usedValue = ValueField(string: "0 GB") used.addView(usedLabel, in: .center) used.addView(usedValue, in: .center) let free: NSStackView = NSStackView(frame: NSRect(x: 10, y: 0, width: TabWidth - 20, height: stackHeight)) free.orientation = .horizontal free.distribution = .equalCentering - let freeLabel = labelField(string: "Free") - let freeValue = valueField(string: "0 GB") + let freeLabel = LabelField(string: "Free") + let freeValue = ValueField(string: "0 GB") free.addView(freeLabel, in: .center) free.addView(freeValue, in: .center) @@ -220,39 +220,11 @@ extension Memory { let view: NSStackView = NSStackView(frame: NSRect(x: 10, y: CGFloat(num)*height, width: TabWidth - 20, height: height)) view.orientation = .horizontal view.distribution = .equalCentering - let viewLabel = labelField(string: label) - let viewValue = valueField(string: value) + let viewLabel = LabelField(string: label) + let viewValue = ValueField(string: value) view.addView(viewLabel, in: .center) view.addView(viewValue, in: .center) return view } - - func labelField(string: String) -> NSTextField { - let label: NSTextField = NSTextField(string: string) - - label.isEditable = false - label.isSelectable = false - label.isBezeled = false - label.textColor = .black - label.alignment = .center - label.font = NSFont.systemFont(ofSize: 12, weight: .regular) - label.backgroundColor = NSColor(hexString: "#dddddd", alpha: 0) - - return label - } - - func valueField(string: String) -> NSTextField { - let label: NSTextField = NSTextField(string: string) - - label.isEditable = false - label.isSelectable = false - label.isBezeled = false - label.textColor = .black - label.alignment = .center - label.font = NSFont.systemFont(ofSize: 13, weight: .regular) - label.backgroundColor = NSColor(hexString: "#dddddd", alpha: 0) - - return label - } } diff --git a/Stats/Views/MainViewController.swift b/Stats/Views/MainViewController.swift index 713d06c6..cce6e62d 100644 --- a/Stats/Views/MainViewController.swift +++ b/Stats/Views/MainViewController.swift @@ -57,6 +57,7 @@ class MainViewController: NSViewController { self.segmentsControl = NSSegmentedControl(labels: items, trackingMode: NSSegmentedControl.SwitchTracking.selectOne, target: self, action: #selector(switchTabs)) self.segmentsControl.setSelected(true, forSegment: 0) +// self.tabView.selectTabViewItem(at: 2) self.segmentsControl.segmentDistribution = .fillEqually let button = NSButton(frame: NSRect(x: 0, y: 0, width: 26, height: 20)) @@ -164,3 +165,32 @@ class MainViewController: NSViewController { } } } + + +func LabelField(string: String) -> NSTextField { + let label: NSTextField = NSTextField(string: string) + + label.isEditable = false + label.isSelectable = false + label.isBezeled = false + label.textColor = .black + label.alignment = .center + label.font = NSFont.systemFont(ofSize: 12, weight: .regular) + label.backgroundColor = NSColor(hexString: "#dddddd", alpha: 0) + + return label +} + +func ValueField(string: String) -> NSTextField { + let label: NSTextField = NSTextField(string: string) + + label.isEditable = false + label.isSelectable = false + label.isBezeled = false + label.textColor = .black + label.alignment = .center + label.font = NSFont.systemFont(ofSize: 13, weight: .regular) + label.backgroundColor = NSColor(hexString: "#dddddd", alpha: 0) + + return label +} diff --git a/Stats/Widgets/BatteryView.swift b/Stats/Widgets/BatteryWidget.swift similarity index 99% rename from Stats/Widgets/BatteryView.swift rename to Stats/Widgets/BatteryWidget.swift index 124d9e66..a1ccf773 100644 --- a/Stats/Widgets/BatteryView.swift +++ b/Stats/Widgets/BatteryWidget.swift @@ -8,7 +8,7 @@ import Cocoa -class BatteryView: NSView, Widget { +class BatteryWidget: NSView, Widget { var activeModule: Observable = Observable(false) var size: CGFloat = widgetSize.width var name: String = "" diff --git a/Stats/libs/Extensions.swift b/Stats/libs/Extensions.swift index 5d482afa..558e864b 100755 --- a/Stats/libs/Extensions.swift +++ b/Stats/libs/Extensions.swift @@ -120,6 +120,39 @@ public struct Units { } } +extension Double { + + func secondsToHoursMinutesSeconds () -> (Int?, Int?, Int?) { + let hrs = self / 3600 + let mins = (self.truncatingRemainder(dividingBy: 3600)) / 60 + let seconds = (self.truncatingRemainder(dividingBy:3600)).truncatingRemainder(dividingBy:60) + return (Int(hrs) > 0 ? Int(hrs) : nil , Int(mins) > 0 ? Int(mins) : nil, Int(seconds) > 0 ? Int(seconds) : nil) + } + + func printSecondsToHoursMinutesSeconds () -> String { + let time = self.secondsToHoursMinutesSeconds() + + switch time { + case (nil, let x? , let y?): + return "\(x) min \(y) sec" + case (nil, let x?, nil): + return "\(x) min" + case (let x?, nil, nil): + return "\(x) hr" + case (nil, nil, let x?): + return "\(x) sec" + case (let x?, nil, let z?): + return "\(x) hr \(z) sec" + case (let x?, let y?, nil): + return "\(x) hr \(y) min" + case (let x?, let y?, let z?): + return "\(x) hr \(y) min \(z) sec" + default: + return "n/a" + } + } +} + extension String { func condenseWhitespace() -> String { let components = self.components(separatedBy: .whitespacesAndNewlines)