diff --git a/Stats.xcodeproj/project.pbxproj b/Stats.xcodeproj/project.pbxproj index 51c6fdd0..32d4ae40 100755 --- a/Stats.xcodeproj/project.pbxproj +++ b/Stats.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 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 */; }; 9A1410F9229E721100D29793 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1410F8229E721100D29793 /* AppDelegate.swift */; }; 9A141100229E721200D29793 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9A1410FE229E721200D29793 /* Main.storyboard */; }; 9A57A18522A1D26D0033E318 /* MenuBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A57A18422A1D26D0033E318 /* MenuBar.swift */; }; @@ -44,6 +47,9 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 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 = ""; }; 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 = ""; }; @@ -91,6 +97,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 9A09C89C22B3A7BB0018426F /* Battery */ = { + isa = PBXGroup; + children = ( + 9A09C89D22B3A7C90018426F /* Battery.swift */, + 9A09C89F22B3A7E20018426F /* BatteryReader.swift */, + ); + path = Battery; + sourceTree = ""; + }; 9A1410EC229E721100D29793 = { isa = PBXGroup; children = ( @@ -138,6 +153,7 @@ 9A5B1CBA229E7892008B9D3C /* Modules */ = { isa = PBXGroup; children = ( + 9A09C89C22B3A7BB0018426F /* Battery */, 9A7B8F5C22A2926500DEB352 /* CPU */, 9A7B8F6222A2C17000DEB352 /* Memory */, 9A7B8F6322A2C17500DEB352 /* Disk */, @@ -158,6 +174,7 @@ 9A74D59522B440D4004FE1FA /* Widgets */ = { isa = PBXGroup; children = ( + 9A09C8A122B3D94D0018426F /* BatteryView.swift */, 9A74D59322B4315C004FE1FA /* Chart.swift */, 9A74D59622B44498004FE1FA /* Mini.swift */, ); @@ -314,13 +331,16 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9A09C8A222B3D94D0018426F /* BatteryView.swift in Sources */, 9A7B8F6F22A2C57000DEB352 /* DiskReader.swift in Sources */, 9A7B8F6922A2C3A100DEB352 /* Memory.swift in Sources */, 9A7B8F5E22A2A57600DEB352 /* CPUReader.swift in Sources */, 9A74D59422B4315C004FE1FA /* Chart.swift in Sources */, + 9A09C89E22B3A7C90018426F /* Battery.swift in Sources */, 9A7B8F6D22A2C3D600DEB352 /* MemoryReader.swift in Sources */, 9A57A18522A1D26D0033E318 /* MenuBar.swift in Sources */, 9A57A19D22A1E3270033E318 /* CPU.swift in Sources */, + 9A09C8A022B3A7E20018426F /* BatteryReader.swift in Sources */, 9A57A19B22A1E1C50033E318 /* Module.swift in Sources */, 9A5B1CBF229E78F0008B9D3C /* Observable.swift in Sources */, 9A7B8F6B22A2C3A700DEB352 /* Disk.swift in Sources */, diff --git a/Stats/AppDelegate.swift b/Stats/AppDelegate.swift index ccfd8469..f89bc787 100755 --- a/Stats/AppDelegate.swift +++ b/Stats/AppDelegate.swift @@ -13,7 +13,7 @@ extension Notification.Name { static let killLauncher = Notification.Name("killLauncher") } -let modules: Observable<[Module]> = Observable([CPU(), Memory(), Disk()]) +let modules: Observable<[Module]> = Observable([CPU(), Memory(), Disk(), Battery()]) let colors: Observable = Observable(true) @NSApplicationMain diff --git a/Stats/MenuBar.swift b/Stats/MenuBar.swift index 1ac8cdb7..6677cfed 100644 --- a/Stats/MenuBar.swift +++ b/Stats/MenuBar.swift @@ -37,6 +37,11 @@ class MenuBar { self.menuBarItem.menu?.removeAllItems() self.menuBarItem.menu = self.buildMenu() } + module.available.subscribe(observer: self) { (value, _) in + self.buildModulesView() + self.menuBarItem.menu?.removeAllItems() + self.menuBarItem.menu = self.buildMenu() + } } } @@ -44,7 +49,9 @@ class MenuBar { let menu = NSMenu() for module in modules.value { - menu.addItem(module.menu) + if module.available.value { + menu.addItem(module.menu) + } } menu.addItem(NSMenuItem.separator()) @@ -115,7 +122,7 @@ class MenuBar { WIDTH = 0 for module in modules.value { - if module.active.value { + if module.active.value && module.available.value { module.start() WIDTH = WIDTH + module.view.frame.size.width stack.addView(module.view, in: NSStackView.Gravity.center) diff --git a/Stats/Modules/Battery/Battery.swift b/Stats/Modules/Battery/Battery.swift new file mode 100644 index 00000000..d57555c0 --- /dev/null +++ b/Stats/Modules/Battery/Battery.swift @@ -0,0 +1,72 @@ +// +// Battery.swift +// Stats +// +// Created by Serhiy Mytrovtsiy on 14/06/2019. +// Copyright © 2019 Serhiy Mytrovtsiy. All rights reserved. +// + +import Cocoa + +class Battery: Module { + let name: String = "Battery" + let shortName: String = "" + var view: NSView = NSView() + var menu: NSMenuItem = NSMenuItem() + var submenu: NSMenu = NSMenu() + var active: Observable + var available: Observable + var reader: Reader = BatteryReader() + + let defaults = UserDefaults.standard + var widgetType: WidgetType = Widgets.Mini + + init() { + self.available = Observable(self.reader.available) + self.active = Observable(defaults.object(forKey: name) != nil ? defaults.bool(forKey: name) : true) + self.view = BatteryView(frame: NSMakeRect(0, 0, MODULE_WIDTH, MODULE_HEIGHT)) + initMenu() + } + + func start() { + if !self.reader.usage.value.isNaN { + let value = self.reader.usage!.value + (self.view as! BatteryView).setCharging(value: value > 0) + (self.view as! Widget).value(value: abs(value)) + } + + self.reader.start() + self.reader.usage.subscribe(observer: self) { (value, _) in + if !value.isNaN { + (self.view as! BatteryView).setCharging(value: value > 0) + (self.view as! Widget).value(value: abs(value)) + } + } + } + + func initMenu() { + menu = NSMenuItem(title: name, action: #selector(toggle), keyEquivalent: "") + if defaults.object(forKey: name) != nil { + menu.state = defaults.bool(forKey: name) ? NSControl.StateValue.on : NSControl.StateValue.off + } else { + menu.state = NSControl.StateValue.on + } + menu.target = self + menu.isEnabled = true + } + + @objc func toggle(_ sender: NSMenuItem) { + let state = sender.state != NSControl.StateValue.on + + sender.state = sender.state == NSControl.StateValue.on ? NSControl.StateValue.off : NSControl.StateValue.on + self.defaults.set(state, forKey: name) + self.active << state + + if !state { + self.stop() + } else { + self.start() + } + } +} + diff --git a/Stats/Modules/Battery/BatteryReader.swift b/Stats/Modules/Battery/BatteryReader.swift new file mode 100644 index 00000000..3ba7d013 --- /dev/null +++ b/Stats/Modules/Battery/BatteryReader.swift @@ -0,0 +1,55 @@ +// +// BatteryReader.swift +// Stats +// +// Created by Serhiy Mytrovtsiy on 14/06/2019. +// Copyright © 2019 Serhiy Mytrovtsiy. All rights reserved. +// + +import Foundation +import IOKit.ps + +class BatteryReader: Reader { + var usage: Observable! + var available: Bool = false + var updateTimer: Timer! + + init() { + self.usage = Observable(0) + read() + } + + func start() { + if updateTimer != nil { + return + } + updateTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(read), userInfo: nil, repeats: true) + } + + func stop() { + if updateTimer == nil { + return + } + updateTimer.invalidate() + updateTimer = nil + } + + @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 isCharging = (psDesc[kIOPSIsChargingKey] as? Bool) + var cap: Float = Float(psDesc[kIOPSCurrentCapacityKey] as! Int) / 100 + + if !isCharging! { + cap = 0 - cap + } + + self.usage << Float(cap) + } + } + } +} diff --git a/Stats/Modules/CPU/CPU.swift b/Stats/Modules/CPU/CPU.swift index a329235b..cd6a5ef7 100644 --- a/Stats/Modules/CPU/CPU.swift +++ b/Stats/Modules/CPU/CPU.swift @@ -15,12 +15,14 @@ class CPU: Module { var menu: NSMenuItem = NSMenuItem() var submenu: NSMenu = NSMenu() var active: Observable + var available: Observable var reader: Reader = CPUReader() let defaults = UserDefaults.standard var widgetType: WidgetType init() { + self.available = Observable(true) self.active = Observable(defaults.object(forKey: name) != nil ? defaults.bool(forKey: name) : true) self.widgetType = defaults.object(forKey: "\(name)_widget") != nil ? defaults.float(forKey: "\(name)_widget") : Widgets.Mini initMenu() diff --git a/Stats/Modules/CPU/CPUReader.swift b/Stats/Modules/CPU/CPUReader.swift index 877231ab..45cf4fb0 100644 --- a/Stats/Modules/CPU/CPUReader.swift +++ b/Stats/Modules/CPU/CPUReader.swift @@ -10,6 +10,7 @@ import Foundation class CPUReader: Reader { var usage: Observable! + var available: Bool = true var cpuInfo: processor_info_array_t! var prevCpuInfo: processor_info_array_t? var numCpuInfo: mach_msg_type_number_t = 0 diff --git a/Stats/Modules/Disk/Disk.swift b/Stats/Modules/Disk/Disk.swift index 961dbcc7..9cbea944 100644 --- a/Stats/Modules/Disk/Disk.swift +++ b/Stats/Modules/Disk/Disk.swift @@ -17,11 +17,13 @@ class Disk: Module { var widgetType: WidgetType var active: Observable + var available: Observable var reader: Reader = DiskReader() @IBOutlet weak var value: NSTextField! init() { + self.available = Observable(true) self.active = Observable(defaults.object(forKey: name) != nil ? defaults.bool(forKey: name) : true) self.widgetType = defaults.object(forKey: "\(name)_widget") != nil ? defaults.float(forKey: "\(name)_widget") : Widgets.Mini diff --git a/Stats/Modules/Disk/DiskReader.swift b/Stats/Modules/Disk/DiskReader.swift index 2fbf0ada..824c0e6b 100644 --- a/Stats/Modules/Disk/DiskReader.swift +++ b/Stats/Modules/Disk/DiskReader.swift @@ -10,6 +10,7 @@ import Foundation class DiskReader: Reader { var usage: Observable! + var available: Bool = true var updateTimer: Timer! init() { diff --git a/Stats/Modules/Memory/Memory.swift b/Stats/Modules/Memory/Memory.swift index 95bc09bc..f229ad47 100644 --- a/Stats/Modules/Memory/Memory.swift +++ b/Stats/Modules/Memory/Memory.swift @@ -15,6 +15,7 @@ class Memory: Module { var menu: NSMenuItem = NSMenuItem() var submenu: NSMenu = NSMenu() var active: Observable + var available: Observable var reader: Reader = MemoryReader() var widgetType: WidgetType @@ -23,6 +24,7 @@ class Memory: Module { @IBOutlet weak var value: NSTextField! init() { + self.available = Observable(true) self.active = Observable(defaults.object(forKey: name) != nil ? defaults.bool(forKey: name) : true) self.widgetType = defaults.object(forKey: "\(name)_widget") != nil ? defaults.float(forKey: "\(name)_widget") : Widgets.Mini initMenu() diff --git a/Stats/Modules/Memory/MemoryReader.swift b/Stats/Modules/Memory/MemoryReader.swift index 5ba24bfd..14d08c22 100644 --- a/Stats/Modules/Memory/MemoryReader.swift +++ b/Stats/Modules/Memory/MemoryReader.swift @@ -10,6 +10,7 @@ import Foundation class MemoryReader: Reader { var usage: Observable! + var available: Bool = true var updateTimer: Timer! var totalSize: Float diff --git a/Stats/Widgets/BatteryView.swift b/Stats/Widgets/BatteryView.swift new file mode 100644 index 00000000..4b1dbef5 --- /dev/null +++ b/Stats/Widgets/BatteryView.swift @@ -0,0 +1,91 @@ +// +// BatteryView.swift +// Stats +// +// Created by Serhiy Mytrovtsiy on 14/06/2019. +// Copyright © 2019 Serhiy Mytrovtsiy. All rights reserved. +// + +import Cocoa + +class BatteryView: NSView, Widget { + var value: Float { + didSet { + self.redraw() + } + } + var charging: Bool { + didSet { + self.redraw() + } + } + + override init(frame: NSRect) { + self.value = 1.0 + self.charging = false + super.init(frame: frame) + self.wantsLayer = true + self.addSubview(NSView()) + } + + required init?(coder decoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func draw(_ dirtyRect: NSRect) { + super.draw(dirtyRect) + + let x: CGFloat = 4.0 + let w: CGFloat = dirtyRect.size.width - (x * 2) + let h: CGFloat = 11.0 + let y: CGFloat = (dirtyRect.size.height - h) / 2 + let r: CGFloat = 1.0 + + let battery = NSBezierPath(roundedRect: NSRect(x: x-1, y: y, width: w-1, height: h), xRadius: r, yRadius: r) + + let bPX: CGFloat = x+w-2 + let bPY: CGFloat = (dirtyRect.size.height / 2) - 2 + let batteryPoint = NSBezierPath(roundedRect: NSRect(x: bPX, y: bPY, width: 2, height: 4), xRadius: r, yRadius: r) + if self.charging { + NSColor.systemGreen.set() + } else { + NSColor.labelColor.set() + } + batteryPoint.lineWidth = 1.1 + batteryPoint.stroke() + batteryPoint.fill() + + let maxWidth = w-4.25 + let inner = NSBezierPath(roundedRect: NSRect(x: x+0.75, y: y+1.5, width: maxWidth*CGFloat(self.value), height: h-3), xRadius: 0.5, yRadius: 0.5) + self.value.batteryColor().set() + inner.lineWidth = 0 + inner.stroke() + inner.close() + inner.fill() + + if self.charging { + NSColor.systemGreen.set() + } else { + NSColor.labelColor.set() + } + battery.lineWidth = 0.8 + battery.stroke() + } + + func redraw() { + self.needsDisplay = true + setNeedsDisplay(self.frame) + } + + func value(value: Float) { + if self.value != value { + self.value = value + } + } + + func setCharging(value: Bool) { + if self.charging != value { + self.charging = value + } + } +} diff --git a/Stats/Widgets/Chart.swift b/Stats/Widgets/Chart.swift index cdd76ab4..a5244493 100644 --- a/Stats/Widgets/Chart.swift +++ b/Stats/Widgets/Chart.swift @@ -12,8 +12,7 @@ class Chart: NSView, Widget { var height: CGFloat = 0.0 var points: [Float] { didSet { - self.needsDisplay = true - setNeedsDisplay(self.frame) + self.redraw() } } @@ -79,6 +78,11 @@ class Chart: NSView, Widget { graphPath.stroke() } + func redraw() { + self.needsDisplay = true + setNeedsDisplay(self.frame) + } + func value(value: Float) { if self.points.count < 50 { self.points.append(value) diff --git a/Stats/Widgets/Mini.swift b/Stats/Widgets/Mini.swift index dd0622df..64003cd3 100644 --- a/Stats/Widgets/Mini.swift +++ b/Stats/Widgets/Mini.swift @@ -65,6 +65,12 @@ class Mini: NSView, Widget { fatalError("init(coder:) has not been implemented") } + func redraw() { + self.valueView.textColor = Float(self.value).usageColor() + self.needsDisplay = true + setNeedsDisplay(self.frame) + } + func value(value: Float) { if self.value != value { self.value = value diff --git a/Stats/libs/Extensions.swift b/Stats/libs/Extensions.swift index 033f9992..5967b672 100755 --- a/Stats/libs/Extensions.swift +++ b/Stats/libs/Extensions.swift @@ -14,18 +14,46 @@ extension Float { return NSString(format: "%.\(decimalPlaces)f" as NSString, self) as String } - func usageColor() -> NSColor { + func usageColor(reversed: Bool = false) -> NSColor { if !colors.value { return NSColor.textColor } + if reversed { + switch self { + case 0.6...0.8: + return NSColor.systemOrange + case 0.8...1: + return NSColor.systemGreen + default: + return NSColor.systemRed + } + } else { + switch self { + case 0.6...0.8: + return NSColor.systemOrange + case 0.8...1: + return NSColor.systemRed + default: + return NSColor.systemGreen + } + } + } + + func batteryColor() -> NSColor { switch self { - case 0.6...0.8: + case 0.2...0.4: + if !colors.value { + return NSColor.controlTextColor + } return NSColor.systemOrange - case 0.8...1: - return NSColor.systemRed - default: + case 0.4...1: + if !colors.value { + return NSColor.controlTextColor + } return NSColor.systemGreen + default: + return NSColor.systemRed } } } diff --git a/Stats/libs/Module.swift b/Stats/libs/Module.swift index 02839c38..1e610d2c 100644 --- a/Stats/libs/Module.swift +++ b/Stats/libs/Module.swift @@ -14,6 +14,7 @@ protocol Module: class { var view: NSView { get set } var menu: NSMenuItem { get } var active: Observable { get } + var available: Observable { get } var reader: Reader { get } var widgetType: WidgetType { get } @@ -53,7 +54,7 @@ extension Module { } self.reader.start() - self.reader.usage.subscribe(observer: self as AnyObject) { (value, _) in + self.reader.usage.subscribe(observer: self) { (value, _) in if !value.isNaN { guard let widget = self.view as? Widget else { return @@ -61,23 +62,34 @@ extension Module { widget.value(value: value) } } + + colors.subscribe(observer: self) { (value, _) in + guard let widget = self.view as? Widget else { + return + } + widget.redraw() + } } func stop() { self.reader.stop() - self.reader.usage.unsubscribe(observer: self as AnyObject) + self.reader.usage.unsubscribe(observer: self) + colors.unsubscribe(observer: self) } } protocol Reader { var usage: Observable! { get } + var available: Bool { get } + var updateTimer: Timer! { get set } func start() - func read() func stop() + func read() } protocol Widget { func value(value: Float) + func redraw() } typealias WidgetType = Float