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