diff --git a/README.md b/README.md index 605c0399..47544dfc 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,30 @@ # Stats -Application for macos that shows CPU, Memory and Disk usage on the menu bar +Simple macOS system monitor in your menu bar -[](https://github.com/exelban/stats/releases) +[](https://github.com/exelban/stats/releases) + +## Why +Stats is a application which allows you to monitor your macOS system. +Also its: + + - free + - easy to use + - no advertisement + - no tracking + - few types of widgets + - black theme compatible ## Installation You can download latest version [here](https://github.com/exelban/stats/releases). -## Widgets -Each widget can be disabled in menu. +## Modules -| Name | Type | Description | +| Name | Available widgets | Description | | --- | --- | --- | -| **CPU** | Percentage | Shows CPU usage | -| **Memory** | Percentage | Shows RAM usage | +| **CPU** | Percentage / Chart / Chart with value | Shows CPU usage | +| **Memory** | Percentage / Chart / Chart with value | Shows RAM usage | | **Disk** | Percentage | Shows disk filling | +| **Battery** | Graphic / Percentage | Shows battery level and charging status | ## Compatibility | macOS | Compatible | @@ -22,18 +33,27 @@ Each widget can be disabled in menu. | 10.14.1 *(Mojave)* | **true** | ## Todo + - [ ] Battery percentage + - [ ] Create new logo + - [ ] Window with preferences + - [ ] Save last modules values + - [ ] Colors toggle for each module - [ ] temperature module - - [ ] battery module + - [X] battery module - [X] move to module system (CPU, RAM, DISK) - [ ] network module - [X] save settings - - [ ] tests - [ ] OTA updates - - [ ] charts + - [X] charts - [X] autostart on boot ## What's new +### v1.1.0 + - added battery module + - added chart widget for CPU and Memory + - added About Stats window + ### v1.0.0 - first release diff --git a/Stats.xcodeproj/project.pbxproj b/Stats.xcodeproj/project.pbxproj index f7169d29..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 */; }; @@ -15,10 +18,9 @@ 9A5B1CBF229E78F0008B9D3C /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A5B1CBE229E78F0008B9D3C /* Observable.swift */; }; 9A5B1CC5229E7B40008B9D3C /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A5B1CC4229E7B40008B9D3C /* Extensions.swift */; }; 9A6CFC0122A1C9F5001E782D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9A6CFC0022A1C9F5001E782D /* Assets.xcassets */; }; - 9A7B8F5B22A290A200DEB352 /* CPU.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9A7B8F5A22A290A200DEB352 /* CPU.xib */; }; + 9A74D59422B4315C004FE1FA /* Chart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A74D59322B4315C004FE1FA /* Chart.swift */; }; + 9A74D59722B44498004FE1FA /* Mini.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A74D59622B44498004FE1FA /* Mini.swift */; }; 9A7B8F5E22A2A57600DEB352 /* CPUReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A7B8F5D22A2A57600DEB352 /* CPUReader.swift */; }; - 9A7B8F6522A2C19D00DEB352 /* Memory.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9A7B8F6422A2C19D00DEB352 /* Memory.xib */; }; - 9A7B8F6722A2C1B900DEB352 /* Disk.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9A7B8F6622A2C1B900DEB352 /* Disk.xib */; }; 9A7B8F6922A2C3A100DEB352 /* Memory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A7B8F6822A2C3A100DEB352 /* Memory.swift */; }; 9A7B8F6B22A2C3A700DEB352 /* Disk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A7B8F6A22A2C3A700DEB352 /* Disk.swift */; }; 9A7B8F6D22A2C3D600DEB352 /* MemoryReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A7B8F6C22A2C3D600DEB352 /* MemoryReader.swift */; }; @@ -27,6 +29,7 @@ 9AFA402822AE49A200FE90BC /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9AFA402622AE49A200FE90BC /* Main.storyboard */; }; 9AFA402F22AE49AE00FE90BC /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AFA402E22AE49AE00FE90BC /* AppDelegate.swift */; }; 9AFA403022AE49DD00FE90BC /* StatsLauncher.app in Copy Files */ = {isa = PBXBuildFile; fileRef = 9AFA401E22AE49A100FE90BC /* StatsLauncher.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 9AFFCB3B22B3FD0500B0E6D8 /* About.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9AFFCB3A22B3FD0500B0E6D8 /* About.storyboard */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -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 = ""; }; @@ -55,10 +61,9 @@ 9A5B1CBE229E78F0008B9D3C /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; 9A5B1CC4229E7B40008B9D3C /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; 9A6CFC0022A1C9F5001E782D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 9A7B8F5A22A290A200DEB352 /* CPU.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CPU.xib; sourceTree = ""; }; + 9A74D59322B4315C004FE1FA /* Chart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Chart.swift; sourceTree = ""; }; + 9A74D59622B44498004FE1FA /* Mini.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mini.swift; sourceTree = ""; }; 9A7B8F5D22A2A57600DEB352 /* CPUReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CPUReader.swift; sourceTree = ""; }; - 9A7B8F6422A2C19D00DEB352 /* Memory.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = Memory.xib; sourceTree = ""; }; - 9A7B8F6622A2C1B900DEB352 /* Disk.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = Disk.xib; sourceTree = ""; }; 9A7B8F6822A2C3A100DEB352 /* Memory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Memory.swift; sourceTree = ""; }; 9A7B8F6A22A2C3A700DEB352 /* Disk.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Disk.swift; sourceTree = ""; }; 9A7B8F6C22A2C3D600DEB352 /* MemoryReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryReader.swift; sourceTree = ""; }; @@ -71,6 +76,7 @@ 9AFA402922AE49A200FE90BC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9AFA402A22AE49A200FE90BC /* StatsLauncher.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = StatsLauncher.entitlements; sourceTree = ""; }; 9AFA402E22AE49AE00FE90BC /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 9AFFCB3A22B3FD0500B0E6D8 /* About.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = About.storyboard; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -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 = ( @@ -113,6 +128,7 @@ 9A1410F7229E721100D29793 /* Stats */ = { isa = PBXGroup; children = ( + 9A74D59522B440D4004FE1FA /* Widgets */, 9A5B1CB3229E72A7008B9D3C /* Supporting Files */, 9A5B1CBA229E7892008B9D3C /* Modules */, 9A5B1CBD229E78D2008B9D3C /* libs */, @@ -126,6 +142,7 @@ isa = PBXGroup; children = ( 9A6CFC0022A1C9F5001E782D /* Assets.xcassets */, + 9AFFCB3A22B3FD0500B0E6D8 /* About.storyboard */, 9A1410FE229E721200D29793 /* Main.storyboard */, 9A141101229E721200D29793 /* Info.plist */, 9A141102229E721200D29793 /* Stats.entitlements */, @@ -136,6 +153,7 @@ 9A5B1CBA229E7892008B9D3C /* Modules */ = { isa = PBXGroup; children = ( + 9A09C89C22B3A7BB0018426F /* Battery */, 9A7B8F5C22A2926500DEB352 /* CPU */, 9A7B8F6222A2C17000DEB352 /* Memory */, 9A7B8F6322A2C17500DEB352 /* Disk */, @@ -153,10 +171,19 @@ path = libs; sourceTree = ""; }; + 9A74D59522B440D4004FE1FA /* Widgets */ = { + isa = PBXGroup; + children = ( + 9A09C8A122B3D94D0018426F /* BatteryView.swift */, + 9A74D59322B4315C004FE1FA /* Chart.swift */, + 9A74D59622B44498004FE1FA /* Mini.swift */, + ); + path = Widgets; + sourceTree = ""; + }; 9A7B8F5C22A2926500DEB352 /* CPU */ = { isa = PBXGroup; children = ( - 9A7B8F5A22A290A200DEB352 /* CPU.xib */, 9A57A19C22A1E3270033E318 /* CPU.swift */, 9A7B8F5D22A2A57600DEB352 /* CPUReader.swift */, ); @@ -166,7 +193,6 @@ 9A7B8F6222A2C17000DEB352 /* Memory */ = { isa = PBXGroup; children = ( - 9A7B8F6422A2C19D00DEB352 /* Memory.xib */, 9A7B8F6822A2C3A100DEB352 /* Memory.swift */, 9A7B8F6C22A2C3D600DEB352 /* MemoryReader.swift */, ); @@ -176,7 +202,6 @@ 9A7B8F6322A2C17500DEB352 /* Disk */ = { isa = PBXGroup; children = ( - 9A7B8F6622A2C1B900DEB352 /* Disk.xib */, 9A7B8F6A22A2C3A700DEB352 /* Disk.swift */, 9A7B8F6E22A2C57000DEB352 /* DiskReader.swift */, ); @@ -284,10 +309,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 9A7B8F5B22A290A200DEB352 /* CPU.xib in Resources */, 9A6CFC0122A1C9F5001E782D /* Assets.xcassets in Resources */, - 9A7B8F6722A2C1B900DEB352 /* Disk.xib in Resources */, - 9A7B8F6522A2C19D00DEB352 /* Memory.xib in Resources */, + 9AFFCB3B22B3FD0500B0E6D8 /* About.storyboard in Resources */, 9A141100229E721200D29793 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -308,16 +331,21 @@ 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 */, 9A1410F9229E721100D29793 /* AppDelegate.swift in Sources */, + 9A74D59722B44498004FE1FA /* Mini.swift in Sources */, 9A5B1CC5229E7B40008B9D3C /* Extensions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Stats/AppDelegate.swift b/Stats/AppDelegate.swift index ab732e83..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 @@ -54,3 +54,30 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } } + +class AboutVC: NSViewController { + @IBOutlet weak var versionLabel: NSTextField! + + override func viewDidLoad() { + super.viewDidLoad() + self.view.wantsLayer = true + } + + @IBAction func openLink(_ sender: Any) { + NSWorkspace.shared.open(URL(string: "https://github.com/exelban/stats")!) + } + + @IBAction func exit(_ sender: Any) { + self.view.window?.close() + } + + override func awakeFromNib() { + if self.view.layer != nil { + self.view.window?.backgroundColor = .white + self.view.layer?.backgroundColor = .white + + let versionNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String + versionLabel.stringValue = "Version \(versionNumber)" + } + } +} diff --git a/Stats/MenuBar.swift b/Stats/MenuBar.swift index a367a5ab..2dc09aad 100644 --- a/Stats/MenuBar.swift +++ b/Stats/MenuBar.swift @@ -10,7 +10,7 @@ import Cocoa import ServiceManagement let MODULE_HEIGHT = CGFloat(NSApplication.shared.mainMenu?.menuBarHeight ?? 22) -let MODULE_WIDTH = CGFloat(28) +let MODULE_WIDTH = CGFloat(32) class MenuBar { let defaults = UserDefaults.standard @@ -34,6 +34,13 @@ class MenuBar { for module in modules.value { module.active.subscribe(observer: self) { (value, _) in self.buildModulesView() + 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() } } } @@ -42,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()) @@ -64,11 +73,21 @@ class MenuBar { menu.addItem(preferences) menu.addItem(NSMenuItem.separator()) + let aboutMenu = NSMenuItem(title: "About Stats", action: #selector(openAbout), keyEquivalent: "") + aboutMenu.target = self + menu.addItem(aboutMenu) menu.addItem(NSMenuItem(title: "Quit Stats", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "")) return menu } + @objc func openAbout(_ sender : NSMenuItem) { + let aboutVC: NSWindowController? = NSStoryboard(name: "About", bundle: nil).instantiateController(withIdentifier: "AboutVC") as? NSWindowController + aboutVC?.window?.center() + aboutVC?.window?.level = .floating + aboutVC!.showWindow(self) + } + @objc func toggleMenu(_ sender : NSMenuItem) { let launcherId = "eu.exelban.StatsLauncher" let status = sender.state != NSControl.StateValue.on @@ -92,32 +111,32 @@ class MenuBar { } self.menuBarButton.image = NSImage(named:NSImage.Name("tray_icon")) - var WIDTH = CGFloat(modules.value.count * 28) - - let view: NSView = NSView(frame: NSMakeRect(0, 0, WIDTH, MODULE_HEIGHT)) - - let stack: NSStackView = NSStackView(frame: NSMakeRect(0, 0, WIDTH, MODULE_HEIGHT)) - stack.orientation = NSUserInterfaceLayoutOrientation.horizontal - stack.distribution = NSStackView.Distribution.fillEqually - stack.spacing = 0 + self.menuBarItem.length = MODULE_WIDTH + var WIDTH = CGFloat(modules.value.count) * MODULE_WIDTH 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) } } - if stack.subviews.count != 0 { + let view: NSView = NSView(frame: NSMakeRect(0, 0, WIDTH, MODULE_HEIGHT)) + + var x: CGFloat = 0 + for module in modules.value { + if module.active.value && module.available.value { + module.view.frame = CGRect(x: x, y: 0, width: module.view.frame.size.width, height: module.view.frame.size.height) + view.addSubview(module.view) + x = x + module.view.frame.size.width + } + } + + if view.subviews.count != 0 { view.frame.size.width = WIDTH - stack.frame.size.width = WIDTH - self.menuBarItem.length = WIDTH - - view.addSubview(stack) - self.menuBarButton.image = nil + self.menuBarItem.length = WIDTH self.menuBarButton.addSubview(view) } } diff --git a/Stats/Modules/Battery/Battery.swift b/Stats/Modules/Battery/Battery.swift new file mode 100644 index 00000000..6d78bb4d --- /dev/null +++ b/Stats/Modules/Battery/Battery.swift @@ -0,0 +1,101 @@ +// +// 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 + let percentageView: Observable + + init() { + 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, MODULE_WIDTH, MODULE_HEIGHT)) + initMenu() + initWidget() + } + + 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 initWidget() { + self.active << false + (self.view as! BatteryView).setPercentage(value: self.percentageView.value) + self.active << true + } + + func initMenu() { + menu = NSMenuItem(title: name, action: #selector(toggle), keyEquivalent: "") + submenu = NSMenu() + + 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 + + let percentage = NSMenuItem(title: "Percentage", action: #selector(togglePercentage), keyEquivalent: "") + percentage.state = defaults.bool(forKey: "\(self.name)_percentage") ? NSControl.StateValue.on : NSControl.StateValue.off + percentage.target = self + + submenu.addItem(percentage) + menu.submenu = submenu + } + + @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 { + menu.submenu = nil + self.stop() + } else { + menu.submenu = submenu + self.start() + } + } + + @objc func togglePercentage(_ 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: "\(self.name)_percentage") + self.percentageView << state + self.initWidget() + } +} + diff --git a/Stats/Modules/Battery/BatteryReader.swift b/Stats/Modules/Battery/BatteryReader.swift new file mode 100644 index 00000000..668caa43 --- /dev/null +++ b/Stats/Modules/Battery/BatteryReader.swift @@ -0,0 +1,56 @@ +// +// 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 powerSourceState = (psDesc[kIOPSPowerSourceStateKey] as? String) + let isCharged = (psDesc[kIOPSIsChargedKey] as? Bool) + var cap: Float = Float(psDesc[kIOPSCurrentCapacityKey] as! Int) / 100 + + if isCharged == nil && powerSourceState! == "Battery Power" { + cap = 0 - cap + } + + self.usage << Float(cap) + } + } + } +} diff --git a/Stats/Modules/CPU/CPU.swift b/Stats/Modules/CPU/CPU.swift index 8bcc2fb2..cd6a5ef7 100644 --- a/Stats/Modules/CPU/CPU.swift +++ b/Stats/Modules/CPU/CPU.swift @@ -10,61 +10,97 @@ import Cocoa class CPU: Module { let name: String = "CPU" + let shortName: String = "CPU" var view: NSView = NSView() - let defaults = UserDefaults.standard - + var menu: NSMenuItem = NSMenuItem() + var submenu: NSMenu = NSMenu() var active: Observable + var available: Observable var reader: Reader = CPUReader() - @IBOutlet weak var value: NSTextField! + 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.view = loadViewFromNib() + self.widgetType = defaults.object(forKey: "\(name)_widget") != nil ? defaults.float(forKey: "\(name)_widget") : Widgets.Mini + initMenu() + initWidget() } - func start() { - if !self.reader.usage.value.isNaN { - self.value.stringValue = "\(Int(Float(self.reader.usage.value.roundTo(decimalPlaces: 2))! * 100))%" - self.value.textColor = self.reader.usage.value.usageColor() - } + func initMenu() { + menu = NSMenuItem(title: name, action: #selector(toggle), keyEquivalent: "") + submenu = NSMenu() - self.reader.start() - self.reader.usage.subscribe(observer: self) { (value, _) in - if !value.isNaN { - self.value.stringValue = "\(Int(Float(value.roundTo(decimalPlaces: 2))! * 100))%" - self.value.textColor = value.usageColor() - } - } - - colors.subscribe(observer: self) { (value, _) in - self.value.textColor = self.reader.usage.value.usageColor() - } - } - - func menu() -> NSMenuItem { - let 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 - return menu + + let mini = NSMenuItem(title: "Mini", action: #selector(toggleWidget), keyEquivalent: "") + mini.state = self.widgetType == Widgets.Mini ? NSControl.StateValue.on : NSControl.StateValue.off + mini.target = self + + let chart = NSMenuItem(title: "Chart", action: #selector(toggleWidget), keyEquivalent: "") + chart.state = self.widgetType == Widgets.Chart ? NSControl.StateValue.on : NSControl.StateValue.off + chart.target = self + + let chartWithValue = NSMenuItem(title: "Chart with value", action: #selector(toggleWidget), keyEquivalent: "") + chartWithValue.state = self.widgetType == Widgets.ChartWithValue ? NSControl.StateValue.on : NSControl.StateValue.off + chartWithValue.target = self + + submenu.addItem(mini) + submenu.addItem(chart) + submenu.addItem(chartWithValue) + + menu.submenu = submenu } @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 { + menu.submenu = nil self.stop() } else { + menu.submenu = submenu self.start() } } + + @objc func toggleWidget(_ sender: NSMenuItem) { + var widgetCode: Float = 0.0 + + switch sender.title { + case "Mini": + widgetCode = Widgets.Mini + case "Chart": + widgetCode = Widgets.Chart + case "Chart with value": + widgetCode = Widgets.ChartWithValue + default: + break + } + + if self.widgetType == widgetCode { + return + } + + for item in self.submenu.items { + if item.title == "Mini" || item.title == "Chart" || item.title == "Chart with value" { + item.state = NSControl.StateValue.off + } + } + + sender.state = sender.state == NSControl.StateValue.on ? NSControl.StateValue.off : NSControl.StateValue.on + self.defaults.set(widgetCode, forKey: "\(name)_widget") + self.widgetType = widgetCode + self.initWidget() + } } diff --git a/Stats/Modules/CPU/CPU.xib b/Stats/Modules/CPU/CPU.xib deleted file mode 100644 index d88e8dad..00000000 --- a/Stats/Modules/CPU/CPU.xib +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 8f2d49c4..9cbea944 100644 --- a/Stats/Modules/Disk/Disk.swift +++ b/Stats/Modules/Disk/Disk.swift @@ -10,40 +10,32 @@ import Cocoa class Disk: Module { let name: String = "Disk" + let shortName: String = "SSD" var view: NSView = NSView() + var menu: NSMenuItem = NSMenuItem() let defaults = UserDefaults.standard + 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.view = loadViewFromNib() + self.widgetType = defaults.object(forKey: "\(name)_widget") != nil ? defaults.float(forKey: "\(name)_widget") : Widgets.Mini + + self.initMenu() + + let widget = Mini(frame: NSMakeRect(0, 0, MODULE_WIDTH, MODULE_HEIGHT)) + widget.label = self.shortName + self.view = widget } - func start() { - if !self.reader.usage.value.isNaN { - self.value.stringValue = "\(Int(Float(self.reader.usage.value.roundTo(decimalPlaces: 2))! * 100))%" - self.value.textColor = self.reader.usage.value.usageColor() - } - - self.reader.start() - self.reader.usage.subscribe(observer: self) { (value, _) in - if !value.isNaN { - self.value.stringValue = "\(Int(Float(value.roundTo(decimalPlaces: 2))! * 100))%" - self.value.textColor = value.usageColor() - } - } - - colors.subscribe(observer: self) { (value, _) in - self.value.textColor = self.reader.usage.value.usageColor() - } - } - - func menu() -> NSMenuItem { - let menu = NSMenuItem(title: name, action: #selector(toggle), keyEquivalent: "") + 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 { @@ -51,7 +43,6 @@ class Disk: Module { } menu.target = self menu.isEnabled = true - return menu } @objc func toggle(_ sender: NSMenuItem) { diff --git a/Stats/Modules/Disk/Disk.xib b/Stats/Modules/Disk/Disk.xib deleted file mode 100644 index d70326bb..00000000 --- a/Stats/Modules/Disk/Disk.xib +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 21354ce8..f229ad47 100644 --- a/Stats/Modules/Memory/Memory.swift +++ b/Stats/Modules/Memory/Memory.swift @@ -10,48 +10,55 @@ import Cocoa class Memory: Module { let name: String = "Memory" + let shortName: String = "MEM" var view: NSView = NSView() - let defaults = UserDefaults.standard - + var menu: NSMenuItem = NSMenuItem() + var submenu: NSMenu = NSMenu() var active: Observable + var available: Observable var reader: Reader = MemoryReader() + var widgetType: WidgetType + + let defaults = UserDefaults.standard @IBOutlet weak var value: NSTextField! init() { + self.available = Observable(true) self.active = Observable(defaults.object(forKey: name) != nil ? defaults.bool(forKey: name) : true) - self.view = loadViewFromNib() + self.widgetType = defaults.object(forKey: "\(name)_widget") != nil ? defaults.float(forKey: "\(name)_widget") : Widgets.Mini + initMenu() + initWidget() } - func start() { - if !self.reader.usage.value.isNaN { - self.value.stringValue = "\(Int(Float(self.reader.usage.value.roundTo(decimalPlaces: 2))! * 100))%" - self.value.textColor = self.reader.usage.value.usageColor() - } + func initMenu() { + menu = NSMenuItem(title: name, action: #selector(toggle), keyEquivalent: "") + submenu = NSMenu() - self.reader.start() - self.reader.usage.subscribe(observer: self) { (value, _) in - if !value.isNaN { - self.value.stringValue = "\(Int(Float(value.roundTo(decimalPlaces: 2))! * 100))%" - self.value.textColor = value.usageColor() - } - } - - colors.subscribe(observer: self) { (value, _) in - self.value.textColor = self.reader.usage.value.usageColor() - } - } - - func menu() -> NSMenuItem { - let 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 - return menu + + let mini = NSMenuItem(title: "Mini", action: #selector(toggleWidget), keyEquivalent: "") + mini.state = self.widgetType == Widgets.Mini ? NSControl.StateValue.on : NSControl.StateValue.off + mini.target = self + + let chart = NSMenuItem(title: "Chart", action: #selector(toggleWidget), keyEquivalent: "") + chart.state = self.widgetType == Widgets.Chart ? NSControl.StateValue.on : NSControl.StateValue.off + chart.target = self + + let chartWithValue = NSMenuItem(title: "Chart with value", action: #selector(toggleWidget), keyEquivalent: "") + chartWithValue.state = self.widgetType == Widgets.ChartWithValue ? NSControl.StateValue.on : NSControl.StateValue.off + chartWithValue.target = self + + submenu.addItem(mini) + submenu.addItem(chart) + submenu.addItem(chartWithValue) + + menu.submenu = submenu } @objc func toggle(_ sender: NSMenuItem) { @@ -67,4 +74,34 @@ class Memory: Module { self.start() } } + + @objc func toggleWidget(_ sender: NSMenuItem) { + var widgetCode: Float = 0.0 + + switch sender.title { + case "Mini": + widgetCode = Widgets.Mini + case "Chart": + widgetCode = Widgets.Chart + case "Chart with value": + widgetCode = Widgets.ChartWithValue + default: + break + } + + if self.widgetType == widgetCode { + return + } + + for item in self.submenu.items { + if item.title == "Mini" || item.title == "Chart" || item.title == "Chart with value" { + item.state = NSControl.StateValue.off + } + } + + sender.state = sender.state == NSControl.StateValue.on ? NSControl.StateValue.off : NSControl.StateValue.on + self.defaults.set(widgetCode, forKey: "\(name)_widget") + self.widgetType = widgetCode + self.initWidget() + } } diff --git a/Stats/Modules/Memory/Memory.xib b/Stats/Modules/Memory/Memory.xib deleted file mode 100644 index 3cf7dd3d..00000000 --- a/Stats/Modules/Memory/Memory.xib +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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/Supporting Files/About.storyboard b/Stats/Supporting Files/About.storyboard new file mode 100644 index 00000000..4f811814 --- /dev/null +++ b/Stats/Supporting Files/About.storyboard @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Stats/Supporting Files/Info.plist b/Stats/Supporting Files/Info.plist index 7d6a28c0..acc461d3 100755 --- a/Stats/Supporting Files/Info.plist +++ b/Stats/Supporting Files/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0.0 + 1.1.0 CFBundleVersion 1 LSApplicationCategoryType diff --git a/Stats/Widgets/BatteryView.swift b/Stats/Widgets/BatteryView.swift new file mode 100644 index 00000000..b85be446 --- /dev/null +++ b/Stats/Widgets/BatteryView.swift @@ -0,0 +1,143 @@ +// +// BatteryView.swift +// Stats +// +// Created by Serhiy Mytrovtsiy on 14/06/2019. +// Copyright © 2019 Serhiy Mytrovtsiy. All rights reserved. +// + +import Cocoa + +class BatteryView: NSView, Widget { + let batteryWidth: CGFloat = 32 + let percentageWidth: CGFloat = 40 + + var value: Float { + didSet { + self.redraw() + } + } + var charging: Bool { + didSet { + self.redraw() + } + } + var percentage: Bool { + didSet { + self.redraw() + } + } + + var percentageValue: NSTextField = NSTextField() + + override init(frame: NSRect) { + self.value = 1.0 + self.charging = false + self.percentage = false + super.init(frame: frame) + self.wantsLayer = true + self.percentageView() + } + + required init?(coder decoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func draw(_ dirtyRect: NSRect) { + super.draw(dirtyRect) + + var x: CGFloat = 4.0 + var 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 + if self.percentage { + w = batteryWidth - (x * 2) + x = percentageWidth + x + } + + 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 + let inner = NSBezierPath(roundedRect: NSRect(x: x+0.5, 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 percentageView() { + if self.percentage { + percentageValue = NSTextField(frame: NSMakeRect(0, 0, percentageWidth, self.frame.size.height - 2)) + percentageValue.textColor = NSColor.red + percentageValue.isEditable = false + percentageValue.isSelectable = false + percentageValue.isBezeled = false + percentageValue.wantsLayer = true + percentageValue.textColor = .labelColor + percentageValue.backgroundColor = .controlColor + percentageValue.canDrawSubviewsIntoLayer = true + percentageValue.alignment = .natural + percentageValue.font = NSFont.systemFont(ofSize: 13, weight: .light) + percentageValue.stringValue = "\(Int(self.value * 100))%" + + self.addSubview(percentageValue) + self.frame = CGRect(x: 0, y: 0, width: batteryWidth + percentageWidth, height: self.frame.size.height) + } else { + for subview in self.subviews { + subview.removeFromSuperview() + } + self.addSubview(NSView()) + self.frame = CGRect(x: 0, y: 0, width: batteryWidth, height: self.frame.size.height) + } + } + + func redraw() { + self.needsDisplay = true + setNeedsDisplay(self.frame) + } + + func value(value: Float) { + if self.value != value { + self.value = value + + if percentage { + self.percentageValue.stringValue = "\(Int(self.value * 100))%" + } + } + } + + func setCharging(value: Bool) { + if self.charging != value { + self.charging = value + } + } + + func setPercentage(value: Bool) { + if self.percentage != value { + self.percentage = value + self.percentageView() + } + } +} diff --git a/Stats/Widgets/Chart.swift b/Stats/Widgets/Chart.swift new file mode 100644 index 00000000..a5244493 --- /dev/null +++ b/Stats/Widgets/Chart.swift @@ -0,0 +1,149 @@ +// +// CPUView.swift +// Stats +// +// Created by Serhiy Mytrovtsiy on 14.06.2019. +// Copyright © 2019 Serhiy Mytrovtsiy. All rights reserved. +// + +import Cocoa + +class Chart: NSView, Widget { + var height: CGFloat = 0.0 + var points: [Float] { + didSet { + self.redraw() + } + } + + override init(frame: NSRect) { + self.points = Array(repeating: 0.0, count: 50) + 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 lineColor: NSColor = NSColor.selectedMenuItemColor + let gradientColor: NSColor = NSColor(red: (26/255.0), green: (126/255.0), blue: (252/255.0), alpha: 0.5) + + let context = NSGraphicsContext.current!.cgContext + let xOffset: CGFloat = 4.0 + let yOffset: CGFloat = 3.0 + if height == 0 { + height = self.frame.size.height - CGFloat((yOffset * 2)) + } + let xRatio = Double(self.frame.size.width - (xOffset * 2)) / (Double(self.points.count) - 1) + + let columnXPoint = { (point: Int) -> CGFloat in + return CGFloat((Double(point) * xRatio)) + xOffset + } + let columnYPoint = { (point: Int) -> CGFloat in + return CGFloat((CGFloat(truncating: self.points[point] as NSNumber) * self.height)) + yOffset + } + + let graphPath = NSBezierPath() + let x: CGFloat = columnXPoint(0) + let y: CGFloat = columnYPoint(0) + graphPath.move(to: CGPoint(x: x, y: y)) + + for i in 1.. 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 self == 1 { + return NSColor.controlTextColor + } + if !colors.value { + return NSColor.controlTextColor + } return NSColor.systemGreen + default: + return NSColor.systemRed } } } @@ -37,3 +68,15 @@ public enum Unit : Float { case gigabyte = 1073741824 } +//extension NSView { +// var backgroundColor: NSColor? { +// get { +// guard let color = layer?.backgroundColor else { return nil } +// return NSColor(cgColor: color) +// } +// set { +// wantsLayer = true +// layer?.backgroundColor = newValue?.cgColor +// } +// } +//} diff --git a/Stats/libs/Module.swift b/Stats/libs/Module.swift index 9b646abe..1e610d2c 100644 --- a/Stats/libs/Module.swift +++ b/Stats/libs/Module.swift @@ -8,35 +8,94 @@ import Cocoa -protocol Module { +protocol Module: class { var name: String { get } + var shortName: String { get } + var view: NSView { get set } + var menu: NSMenuItem { get } var active: Observable { get } + var available: Observable { get } var reader: Reader { get } - var view: NSView { get } + var widgetType: WidgetType { get } - func menu() -> NSMenuItem func start() func stop() } extension Module { - func stop() { - self.reader.stop() - self.reader.usage.unsubscribe(observer: self as AnyObject) + func initWidget() { + self.active << false + switch self.widgetType { + case Widgets.Mini: + let widget = Mini(frame: NSMakeRect(0, 0, MODULE_WIDTH, MODULE_HEIGHT)) + widget.label = self.shortName + self.view = widget + break + case Widgets.Chart: + self.view = Chart(frame: NSMakeRect(0, 0, MODULE_WIDTH + 7, MODULE_HEIGHT)) + break + case Widgets.ChartWithValue: + self.view = ChartWithValue(frame: NSMakeRect(0, 0, MODULE_WIDTH + 7, MODULE_HEIGHT)) + break + default: + let widget = Mini(frame: NSMakeRect(0, 0, MODULE_WIDTH, MODULE_HEIGHT)) + widget.label = self.shortName + self.view = widget + } + self.active << true } - func loadViewFromNib() -> NSView { - var topLevelObjects: NSArray? - if Bundle.main.loadNibNamed(NSNib.Name(String(describing: Self.self)), owner: self, topLevelObjects: &topLevelObjects) { - return (topLevelObjects?.first(where: { $0 is NSView } ) as? NSView)! + func start() { + if !self.reader.usage.value.isNaN { + guard let widget = self.view as? Widget else { + return + } + widget.value(value: self.reader.usage.value) } - return NSView() + + self.reader.start() + self.reader.usage.subscribe(observer: self) { (value, _) in + if !value.isNaN { + guard let widget = self.view as? Widget else { + return + } + 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) + 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 + +struct Widgets { + static let Mini: WidgetType = 0.0 + static let Chart: WidgetType = 1.0 + static let ChartWithValue: WidgetType = 1.1 } diff --git a/resources/widgets.psd b/resources/widgets.psd index 54c0cf7e..ff9e767b 100644 Binary files a/resources/widgets.psd and b/resources/widgets.psd differ