v2.0.0 (#34)
* v2.0.0 * rewritten application from scratch * new Settings * new custom popup view * moved to own implementation of chart * added more option to configure a widget * now each module has own widget in the menu bar * a lot of new features...
3
.github/FUNDING.yml
vendored
@@ -1,3 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: exelban
|
||||
2
.gitignore
vendored
@@ -8,3 +8,5 @@ xcuserdata
|
||||
Stats.dmg
|
||||
Stats.app
|
||||
create-dmg
|
||||
|
||||
Cartfile.resolved
|
||||
@@ -1,76 +0,0 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at mitrovtsiy@ukr.net. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
1
Cartfile
@@ -1,3 +1,2 @@
|
||||
github "danielgindi/Charts" ~> 3.4.0
|
||||
github "ashleymills/Reachability.swift" ~> 5.0.0
|
||||
github "malcommac/Repeat" ~> 0.6.0
|
||||
8
Makefile
@@ -59,7 +59,7 @@ build: sign
|
||||
|
||||
./create-dmg/create-dmg \
|
||||
--volname $(APP) \
|
||||
--background "./resources/background.png" \
|
||||
--background "./Stats/Supporting Files/background.png" \
|
||||
--window-pos 200 120 \
|
||||
--window-size 500 320 \
|
||||
--icon-size 80 \
|
||||
@@ -90,3 +90,9 @@ history:
|
||||
.PHONY: dep
|
||||
dep:
|
||||
carthage update --platform macOS
|
||||
|
||||
.PHONY: zip
|
||||
zip:
|
||||
cd ../
|
||||
zip -r archive.zip ./
|
||||
open $(PWD)
|
||||
43
ModuleKit/Constants.swift
Normal file
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// Constants.swift
|
||||
// ModuleKit
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 15/04/2020.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
public struct Popup_c_s {
|
||||
public let width: CGFloat = 264
|
||||
public let height: CGFloat = 300
|
||||
public let margins: CGFloat = 8
|
||||
public let headerHeight: CGFloat = 42
|
||||
public let separatorHeight: CGFloat = 30
|
||||
}
|
||||
|
||||
public struct Settings_c_s {
|
||||
public let width: CGFloat = 539
|
||||
public let height: CGFloat = 479
|
||||
public let margin: CGFloat = 10
|
||||
}
|
||||
|
||||
public struct Widget_c_s {
|
||||
public let width: CGFloat = 32
|
||||
public var height: CGFloat {
|
||||
get {
|
||||
let systemHeight = NSApplication.shared.mainMenu?.menuBarHeight
|
||||
return (systemHeight == 0 ? 22 : systemHeight) ?? 22
|
||||
}
|
||||
}
|
||||
public let margin: CGFloat = 2
|
||||
}
|
||||
|
||||
public struct Constants {
|
||||
public static let Popup: Popup_c_s = Popup_c_s()
|
||||
public static let Settings: Settings_c_s = Settings_c_s()
|
||||
public static let Widget: Widget_c_s = Widget_c_s()
|
||||
}
|
||||
6
ModuleKit/Supporting Files/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
26
ModuleKit/Supporting Files/Assets.xcassets/settings.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "baseline_settings_black_24pt_1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "baseline_settings_black_24pt_2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "baseline_settings_black_24pt_3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
ModuleKit/Supporting Files/Assets.xcassets/settings.imageset/baseline_settings_black_24pt_1x.png
vendored
Normal file
|
After Width: | Height: | Size: 330 B |
BIN
ModuleKit/Supporting Files/Assets.xcassets/settings.imageset/baseline_settings_black_24pt_2x.png
vendored
Normal file
|
After Width: | Height: | Size: 551 B |
BIN
ModuleKit/Supporting Files/Assets.xcassets/settings.imageset/baseline_settings_black_24pt_3x.png
vendored
Normal file
|
After Width: | Height: | Size: 772 B |
24
ModuleKit/Supporting Files/Info.plist
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
240
ModuleKit/Widgets/BarChart.swift
Normal file
@@ -0,0 +1,240 @@
|
||||
//
|
||||
// BarChart.swift
|
||||
// ModuleKit
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 26/04/2020.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import StatsKit
|
||||
|
||||
public class BarChart: Widget {
|
||||
private var labelState: Bool = true
|
||||
private var boxState: Bool = true
|
||||
private var colorState: Bool = false
|
||||
|
||||
private let store: UnsafePointer<Store>?
|
||||
private var value: [Double] = []
|
||||
|
||||
public init(preview: Bool, title: String, config: NSDictionary?, store: UnsafePointer<Store>?) {
|
||||
var widgetTitle: String = title
|
||||
self.store = store
|
||||
if config != nil {
|
||||
var configuration = config!
|
||||
if let titleFromConfig = config!["Title"] as? String {
|
||||
widgetTitle = titleFromConfig
|
||||
}
|
||||
|
||||
if preview {
|
||||
if let previewConfig = config!["Preview"] as? NSDictionary {
|
||||
configuration = previewConfig
|
||||
if let value = configuration["Value"] as? String {
|
||||
self.value = value.split(separator: ",").map{ (Double($0) ?? 0) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let label = configuration["Label"] as? Bool {
|
||||
self.labelState = label
|
||||
}
|
||||
if let box = configuration["Box"] as? Bool {
|
||||
self.boxState = box
|
||||
}
|
||||
if let color = configuration["Color"] as? Bool {
|
||||
self.colorState = color
|
||||
}
|
||||
}
|
||||
super.init(frame: CGRect(x: 0, y: Constants.Widget.margin, width: Constants.Widget.width, height: Constants.Widget.height - (2*Constants.Widget.margin)))
|
||||
self.preview = preview
|
||||
self.title = widgetTitle
|
||||
self.type = .barChart
|
||||
self.canDrawConcurrently = true
|
||||
|
||||
if self.store != nil && !preview {
|
||||
self.boxState = store!.pointee.bool(key: "\(self.title)_\(self.type.rawValue)_box", defaultValue: self.boxState)
|
||||
self.labelState = store!.pointee.bool(key: "\(self.title)_\(self.type.rawValue)_label", defaultValue: self.labelState)
|
||||
self.colorState = store!.pointee.bool(key: "\(self.title)_\(self.type.rawValue)_color", defaultValue: self.colorState)
|
||||
}
|
||||
|
||||
if preview {
|
||||
if self.value.count == 0 {
|
||||
self.value = [0.72, 0.38]
|
||||
}
|
||||
self.setFrameSize(NSSize(width: 36, height: self.frame.size.height))
|
||||
self.invalidateIntrinsicContentSize()
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public override func draw(_ dirtyRect: NSRect) {
|
||||
super.draw(dirtyRect)
|
||||
|
||||
let ctx = NSGraphicsContext.current!.cgContext
|
||||
ctx.saveGState()
|
||||
|
||||
var width: CGFloat = 0
|
||||
var x: CGFloat = Constants.Widget.margin
|
||||
var chartPadding: CGFloat = 0
|
||||
|
||||
if self.labelState {
|
||||
let style = NSMutableParagraphStyle()
|
||||
style.alignment = .center
|
||||
let stringAttributes = [
|
||||
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 7, weight: .regular),
|
||||
NSAttributedString.Key.foregroundColor: NSColor.labelColor,
|
||||
NSAttributedString.Key.paragraphStyle: style
|
||||
]
|
||||
|
||||
let letterHeight = self.frame.height / 3
|
||||
let letterWidth: CGFloat = 6.0
|
||||
|
||||
var yMargin: CGFloat = 0
|
||||
for char in String(self.title.prefix(3)).uppercased().reversed() {
|
||||
let rect = CGRect(x: x, y: yMargin, width: letterWidth, height: letterHeight)
|
||||
let str = NSAttributedString.init(string: "\(char)", attributes: stringAttributes)
|
||||
str.draw(with: rect)
|
||||
yMargin += letterHeight
|
||||
}
|
||||
width = width + letterWidth + (Constants.Widget.margin*2)
|
||||
x = letterWidth + (Constants.Widget.margin*3)
|
||||
}
|
||||
|
||||
switch self.value.count {
|
||||
case 0, 1:
|
||||
width += 14
|
||||
break
|
||||
case 2:
|
||||
width += 26
|
||||
break
|
||||
case 3...4: // 3,4
|
||||
width += 32
|
||||
break
|
||||
case 5...8: // 5,6,7,8
|
||||
width += 42
|
||||
break
|
||||
case 9...12: // 9..12
|
||||
width += 52
|
||||
break
|
||||
case 13...16: // 13..16
|
||||
width += 78
|
||||
break
|
||||
case 17...32: // 17..32
|
||||
width += 86
|
||||
break
|
||||
default: // > 32
|
||||
width += 120
|
||||
break
|
||||
}
|
||||
|
||||
let box = NSBezierPath(roundedRect: NSRect(x: x, y: 0, width: width - x - Constants.Widget.margin, height: self.frame.size.height), xRadius: 2, yRadius: 2)
|
||||
if self.boxState {
|
||||
NSColor.black.set()
|
||||
box.stroke()
|
||||
box.fill()
|
||||
chartPadding = 1
|
||||
}
|
||||
|
||||
let widthForBarChart = box.bounds.width - chartPadding
|
||||
let partitionMargin: CGFloat = 0.5
|
||||
let partitionsMargin: CGFloat = (CGFloat(self.value.count - 1)) * partitionMargin / CGFloat(self.value.count - 1)
|
||||
let partitionWidth: CGFloat = (widthForBarChart / CGFloat(self.value.count)) - CGFloat(partitionsMargin.isNaN ? 0 : partitionsMargin)
|
||||
let maxPartitionHeight: CGFloat = box.bounds.height - (chartPadding*2)
|
||||
|
||||
x += partitionMargin
|
||||
for i in 0..<self.value.count {
|
||||
let partitionValue = self.value[i]
|
||||
let partitonHeight = maxPartitionHeight * CGFloat(partitionValue)
|
||||
let partition = NSBezierPath(rect: NSRect(x: x, y: chartPadding, width: partitionWidth, height: partitonHeight))
|
||||
|
||||
partitionValue.usageColor().setFill()
|
||||
partition.fill()
|
||||
partition.close()
|
||||
|
||||
x += partitionWidth + partitionMargin
|
||||
}
|
||||
|
||||
ctx.restoreGState()
|
||||
self.setWidth(width)
|
||||
}
|
||||
|
||||
public func setValue(_ value: [Double]) {
|
||||
self.value = value
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.display()
|
||||
})
|
||||
}
|
||||
|
||||
public override func settings(superview: NSView) {
|
||||
let rowHeight: CGFloat = 30
|
||||
let height: CGFloat = ((rowHeight + Constants.Settings.margin) * 3) + Constants.Settings.margin
|
||||
superview.setFrameSize(NSSize(width: superview.frame.width, height: height))
|
||||
|
||||
let view: NSView = NSView(frame: NSRect(x: Constants.Settings.margin, y: Constants.Settings.margin, width: superview.frame.width - (Constants.Settings.margin*2), height: superview.frame.height - (Constants.Settings.margin*2)))
|
||||
|
||||
view.addSubview(ToggleTitleRow(
|
||||
frame: NSRect(x: 0, y: (rowHeight + Constants.Settings.margin) * 2, width: view.frame.width, height: rowHeight),
|
||||
title: "Label",
|
||||
action: #selector(toggleLabel),
|
||||
state: self.labelState
|
||||
))
|
||||
|
||||
view.addSubview(ToggleTitleRow(
|
||||
frame: NSRect(x: 0, y: (rowHeight + Constants.Settings.margin) * 1, width: view.frame.width, height: rowHeight),
|
||||
title: "Box",
|
||||
action: #selector(toggleBox),
|
||||
state: self.boxState
|
||||
))
|
||||
|
||||
view.addSubview(ToggleTitleRow(
|
||||
frame: NSRect(x: 0, y: (rowHeight + Constants.Settings.margin) * 0, width: view.frame.width, height: rowHeight),
|
||||
title: "Colorize",
|
||||
action: #selector(toggleColor),
|
||||
state: self.colorState
|
||||
))
|
||||
|
||||
superview.addSubview(view)
|
||||
}
|
||||
|
||||
@objc private func toggleLabel(_ sender: NSControl) {
|
||||
var state: NSControl.StateValue? = nil
|
||||
if #available(OSX 10.15, *) {
|
||||
state = sender is NSSwitch ? (sender as! NSSwitch).state: nil
|
||||
} else {
|
||||
state = sender is NSButton ? (sender as! NSButton).state: nil
|
||||
}
|
||||
self.labelState = state! == .on ? true : false
|
||||
self.store?.pointee.set(key: "\(self.title)_\(self.type.rawValue)_label", value: self.labelState)
|
||||
self.display()
|
||||
}
|
||||
|
||||
@objc private func toggleBox(_ sender: NSControl) {
|
||||
var state: NSControl.StateValue? = nil
|
||||
if #available(OSX 10.15, *) {
|
||||
state = sender is NSSwitch ? (sender as! NSSwitch).state: nil
|
||||
} else {
|
||||
state = sender is NSButton ? (sender as! NSButton).state: nil
|
||||
}
|
||||
self.boxState = state! == .on ? true : false
|
||||
self.store?.pointee.set(key: "\(self.title)_\(self.type.rawValue)_box", value: self.boxState)
|
||||
self.display()
|
||||
}
|
||||
|
||||
@objc private func toggleColor(_ sender: NSControl) {
|
||||
var state: NSControl.StateValue? = nil
|
||||
if #available(OSX 10.15, *) {
|
||||
state = sender is NSSwitch ? (sender as! NSSwitch).state: nil
|
||||
} else {
|
||||
state = sender is NSButton ? (sender as! NSButton).state: nil
|
||||
}
|
||||
self.colorState = state! == .on ? true : false
|
||||
self.store?.pointee.set(key: "\(self.title)_\(self.type.rawValue)_color", value: self.colorState)
|
||||
self.display()
|
||||
}
|
||||
}
|
||||
190
ModuleKit/Widgets/Battery.swift
Normal file
@@ -0,0 +1,190 @@
|
||||
//
|
||||
// Battery.swift
|
||||
// ModuleKit
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 06/06/2020.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import StatsKit
|
||||
|
||||
public enum battery_additional_t: String {
|
||||
case none = "None"
|
||||
case percentage = "Percentage"
|
||||
case time = "Time"
|
||||
}
|
||||
extension battery_additional_t: CaseIterable {}
|
||||
|
||||
public class BatterykWidget: Widget {
|
||||
private var additional: battery_additional_t = .none
|
||||
private var iconState: Bool = true
|
||||
private var colorState: Bool = false
|
||||
|
||||
private let store: UnsafePointer<Store>?
|
||||
|
||||
private var percentage: Double = 1
|
||||
private var time: Int = 0
|
||||
private var charging: Bool = false
|
||||
|
||||
public init(preview: Bool, title: String, config: NSDictionary?, store: UnsafePointer<Store>?) {
|
||||
let widgetTitle: String = title
|
||||
self.store = store
|
||||
super.init(frame: CGRect(x: 0, y: Constants.Widget.margin, width: 30, height: Constants.Widget.height - (2*Constants.Widget.margin)))
|
||||
self.title = widgetTitle
|
||||
self.type = .battery
|
||||
self.preview = preview
|
||||
self.canDrawConcurrently = true
|
||||
|
||||
if self.store != nil {
|
||||
self.additional = battery_additional_t(rawValue: store!.pointee.string(key: "\(self.title)_\(self.type.rawValue)_additional", defaultValue: self.additional.rawValue)) ?? self.additional
|
||||
self.iconState = store!.pointee.bool(key: "\(self.title)_\(self.type.rawValue)_icon", defaultValue: self.iconState)
|
||||
self.colorState = store!.pointee.bool(key: "\(self.title)_\(self.type.rawValue)_color", defaultValue: self.colorState)
|
||||
}
|
||||
|
||||
if self.preview {
|
||||
self.percentage = 0.72
|
||||
self.additional = .none
|
||||
self.iconState = true
|
||||
self.colorState = false
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public override func draw(_ dirtyRect: NSRect) {
|
||||
super.draw(dirtyRect)
|
||||
|
||||
var width: CGFloat = 30
|
||||
var x: CGFloat = Constants.Widget.margin
|
||||
|
||||
let stringAttributes = [
|
||||
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 12, weight: .regular),
|
||||
NSAttributedString.Key.foregroundColor: isDarkMode ? NSColor.white : NSColor.textColor,
|
||||
NSAttributedString.Key.paragraphStyle: NSMutableParagraphStyle()
|
||||
]
|
||||
|
||||
if self.additional == .percentage {
|
||||
let string = "\(Int((self.percentage.rounded(toPlaces: 2)) * 100))%"
|
||||
let stringWidth = string.widthOfString(usingFont: .systemFont(ofSize: 12, weight: .regular))
|
||||
let rect = CGRect(x: x, y: (Constants.Widget.height-12)/2, width: stringWidth, height: 12)
|
||||
let str = NSAttributedString.init(string: string, attributes: stringAttributes)
|
||||
str.draw(with: rect)
|
||||
|
||||
width += stringWidth + Constants.Widget.margin
|
||||
x += stringWidth + Constants.Widget.margin
|
||||
} else if self.additional == .time {
|
||||
let string = Double(self.time*60).printSecondsToHoursMinutesSeconds()
|
||||
let stringWidth = string.widthOfString(usingFont: .systemFont(ofSize: 12, weight: .regular))
|
||||
let rect = CGRect(x: x, y: (Constants.Widget.height-12)/2, width: stringWidth, height: 12)
|
||||
let str = NSAttributedString.init(string: string, attributes: stringAttributes)
|
||||
str.draw(with: rect)
|
||||
|
||||
width += stringWidth + Constants.Widget.margin
|
||||
x += stringWidth + Constants.Widget.margin
|
||||
}
|
||||
|
||||
let w: CGFloat = 30 - (Constants.Widget.margin*2) - 4
|
||||
let h: CGFloat = 11
|
||||
let y: CGFloat = (dirtyRect.size.height - h) / 2
|
||||
let batteryFrame = NSBezierPath(roundedRect: NSRect(x: x+1, y: y, width: w, height: h), xRadius: 1, yRadius: 1)
|
||||
|
||||
if self.charging {
|
||||
NSColor.systemGreen.set()
|
||||
} else {
|
||||
NSColor.black.set()
|
||||
}
|
||||
|
||||
let bPX: CGFloat = x+w+1
|
||||
let bPY: CGFloat = (dirtyRect.size.height / 2) - 2
|
||||
let batteryPoint = NSBezierPath(roundedRect: NSRect(x: bPX, y: bPY, width: 2, height: 4), xRadius: 1, yRadius: 1)
|
||||
batteryPoint.lineWidth = 1.1
|
||||
batteryPoint.stroke()
|
||||
batteryPoint.fill()
|
||||
|
||||
batteryFrame.lineWidth = 1
|
||||
batteryFrame.stroke()
|
||||
|
||||
let maxWidth = w - 3
|
||||
let inner = NSBezierPath(roundedRect: NSRect(x: x+2.5, y: y+1.5, width: maxWidth * CGFloat(self.percentage), height: h-3), xRadius: 0.5, yRadius: 0.5)
|
||||
self.percentage.batteryColor(color: self.colorState).set()
|
||||
inner.lineWidth = 0
|
||||
inner.stroke()
|
||||
inner.close()
|
||||
inner.fill()
|
||||
|
||||
self.setWidth(width)
|
||||
}
|
||||
|
||||
public func setValue(percentage: Double, isCharging: Bool, time: Int) {
|
||||
var updated: Bool = false
|
||||
|
||||
if self.percentage != percentage {
|
||||
self.percentage = abs(percentage)
|
||||
updated = true
|
||||
}
|
||||
if self.charging != isCharging {
|
||||
self.charging = isCharging
|
||||
updated = true
|
||||
}
|
||||
if self.time != time {
|
||||
self.time = time
|
||||
updated = true
|
||||
}
|
||||
|
||||
if updated {
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.display()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public override func settings(superview: NSView) {
|
||||
let rowHeight: CGFloat = 30
|
||||
let height: CGFloat = ((rowHeight + Constants.Settings.margin) * 2) + Constants.Settings.margin
|
||||
superview.setFrameSize(NSSize(width: superview.frame.width, height: height))
|
||||
|
||||
let view: NSView = NSView(frame: NSRect(x: Constants.Settings.margin, y: Constants.Settings.margin, width: superview.frame.width - (Constants.Settings.margin*2), height: superview.frame.height - (Constants.Settings.margin*2)))
|
||||
|
||||
view.addSubview(SelectTitleRow(
|
||||
frame: NSRect(x: 0, y: rowHeight + Constants.Settings.margin, width: view.frame.width, height: rowHeight),
|
||||
title: "Additional information",
|
||||
action: #selector(toggleAdditional),
|
||||
items: battery_additional_t.allCases.map{ return $0.rawValue },
|
||||
selected: self.additional.rawValue
|
||||
))
|
||||
|
||||
view.addSubview(ToggleTitleRow(
|
||||
frame: NSRect(x: 0, y: (rowHeight + Constants.Settings.margin) * 0, width: view.frame.width, height: rowHeight),
|
||||
title: "Colorize",
|
||||
action: #selector(toggleColor),
|
||||
state: self.colorState
|
||||
))
|
||||
|
||||
superview.addSubview(view)
|
||||
}
|
||||
|
||||
@objc private func toggleAdditional(_ sender: NSMenuItem) {
|
||||
let newValue: battery_additional_t = battery_additional_t(rawValue: sender.title) ?? .none
|
||||
self.additional = newValue
|
||||
self.store?.pointee.set(key: "\(self.title)_\(self.type.rawValue)_additional", value: self.additional.rawValue)
|
||||
self.display()
|
||||
}
|
||||
|
||||
@objc private func toggleColor(_ sender: NSControl) {
|
||||
var state: NSControl.StateValue? = nil
|
||||
if #available(OSX 10.15, *) {
|
||||
state = sender is NSSwitch ? (sender as! NSSwitch).state: nil
|
||||
} else {
|
||||
state = sender is NSButton ? (sender as! NSButton).state: nil
|
||||
}
|
||||
self.colorState = state! == .on ? true : false
|
||||
self.store?.pointee.set(key: "\(self.title)_\(self.type.rawValue)_color", value: self.colorState)
|
||||
self.display()
|
||||
}
|
||||
}
|
||||
255
ModuleKit/Widgets/LineChart.swift
Normal file
@@ -0,0 +1,255 @@
|
||||
//
|
||||
// Chart.swift
|
||||
// ModuleKit
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 18/04/2020.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import StatsKit
|
||||
|
||||
public class LineChart: Widget {
|
||||
private var labelState: Bool = true
|
||||
private var boxState: Bool = true
|
||||
private var valueState: Bool = false
|
||||
private var colorState: Bool = false
|
||||
|
||||
private let store: UnsafePointer<Store>?
|
||||
private var chart: LineChartView
|
||||
private var value: Double = 0
|
||||
|
||||
public init(preview: Bool, title: String, config: NSDictionary?, store: UnsafePointer<Store>?) {
|
||||
var widgetTitle: String = title
|
||||
self.store = store
|
||||
if config != nil {
|
||||
if let titleFromConfig = config!["Title"] as? String {
|
||||
widgetTitle = titleFromConfig
|
||||
}
|
||||
if let label = config!["Label"] as? Bool {
|
||||
self.labelState = label
|
||||
}
|
||||
if let box = config!["Box"] as? Bool {
|
||||
self.boxState = box
|
||||
}
|
||||
if let value = config!["Value"] as? Bool {
|
||||
self.valueState = value
|
||||
}
|
||||
if let color = config!["Color"] as? Bool {
|
||||
self.colorState = color
|
||||
}
|
||||
}
|
||||
self.chart = LineChartView(frame: NSRect(x: 0, y: 0, width: Constants.Widget.width, height: Constants.Widget.height - (2*Constants.Widget.margin)), num: 60)
|
||||
super.init(frame: CGRect(x: 0, y: Constants.Widget.margin, width: Constants.Widget.width, height: Constants.Widget.height - (2*Constants.Widget.margin)))
|
||||
self.preview = preview
|
||||
self.title = widgetTitle
|
||||
self.type = .lineChart
|
||||
self.canDrawConcurrently = true
|
||||
|
||||
if self.store != nil && !preview {
|
||||
self.boxState = store!.pointee.bool(key: "\(self.title)_\(self.type.rawValue)_box", defaultValue: self.boxState)
|
||||
self.valueState = store!.pointee.bool(key: "\(self.title)_\(self.type.rawValue)_value", defaultValue: self.valueState)
|
||||
self.labelState = store!.pointee.bool(key: "\(self.title)_\(self.type.rawValue)_label", defaultValue: self.labelState)
|
||||
self.colorState = store!.pointee.bool(key: "\(self.title)_\(self.type.rawValue)_color", defaultValue: self.colorState)
|
||||
}
|
||||
|
||||
if self.labelState {
|
||||
self.setFrameSize(NSSize(width: Constants.Widget.width + 6 + (Constants.Widget.margin*2), height: self.frame.size.height))
|
||||
}
|
||||
|
||||
if preview {
|
||||
var list: [Double] = []
|
||||
for _ in 0..<16 {
|
||||
list.append(Double(CGFloat(Float(arc4random()) / Float(UINT32_MAX))))
|
||||
}
|
||||
self.chart.points = list
|
||||
self.value = 0.38
|
||||
}
|
||||
}
|
||||
|
||||
public override func draw(_ dirtyRect: NSRect) {
|
||||
super.draw(dirtyRect)
|
||||
|
||||
let ctx = NSGraphicsContext.current!.cgContext
|
||||
ctx.saveGState()
|
||||
|
||||
var width = Constants.Widget.width
|
||||
var x: CGFloat = Constants.Widget.margin
|
||||
var chartPadding: CGFloat = 0
|
||||
|
||||
if self.labelState {
|
||||
let style = NSMutableParagraphStyle()
|
||||
style.alignment = .center
|
||||
let stringAttributes = [
|
||||
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 7, weight: .regular),
|
||||
NSAttributedString.Key.foregroundColor: NSColor.labelColor,
|
||||
NSAttributedString.Key.paragraphStyle: style
|
||||
]
|
||||
|
||||
let letterHeight = self.frame.height / 3
|
||||
let letterWidth: CGFloat = 6.0
|
||||
|
||||
var yMargin: CGFloat = 0
|
||||
for char in String(self.title.prefix(3)).uppercased().reversed() {
|
||||
let rect = CGRect(x: x, y: yMargin, width: letterWidth, height: letterHeight)
|
||||
let str = NSAttributedString.init(string: "\(char)", attributes: stringAttributes)
|
||||
str.draw(with: rect)
|
||||
yMargin += letterHeight
|
||||
}
|
||||
width = width + letterWidth + (Constants.Widget.margin*2)
|
||||
x = letterWidth + (Constants.Widget.margin*3)
|
||||
}
|
||||
|
||||
var boxHeight: CGFloat = self.frame.size.height
|
||||
var boxRadius: CGFloat = 2
|
||||
let boxWidth: CGFloat = Constants.Widget.width - (Constants.Widget.margin*2)
|
||||
|
||||
if self.valueState {
|
||||
let style = NSMutableParagraphStyle()
|
||||
style.alignment = .right
|
||||
|
||||
var color = isDarkMode ? NSColor.white : NSColor.black
|
||||
if self.colorState {
|
||||
color = self.value.textUsageColor(color: self.colorState)
|
||||
}
|
||||
|
||||
let stringAttributes = [
|
||||
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 8, weight: .regular),
|
||||
NSAttributedString.Key.foregroundColor: color,
|
||||
NSAttributedString.Key.paragraphStyle: style
|
||||
]
|
||||
|
||||
let rect = CGRect(x: x, y: boxHeight-7, width: boxWidth - chartPadding, height: 7)
|
||||
let str = NSAttributedString.init(string: "\(Int((value.rounded(toPlaces: 2)) * 100))%", attributes: stringAttributes)
|
||||
str.draw(with: rect)
|
||||
|
||||
boxHeight = 9
|
||||
boxRadius = 1
|
||||
}
|
||||
|
||||
let box = NSBezierPath(roundedRect: NSRect(x: x, y: 0, width: boxWidth, height: boxHeight), xRadius: boxRadius, yRadius: boxRadius)
|
||||
if self.boxState {
|
||||
NSColor.black.set()
|
||||
box.stroke()
|
||||
box.fill()
|
||||
self.chart.transparent = false
|
||||
chartPadding = 1
|
||||
} else {
|
||||
self.chart.transparent = true
|
||||
}
|
||||
|
||||
chart.setFrameSize(NSSize(width: box.bounds.width - chartPadding, height: box.bounds.height - (chartPadding*2)))
|
||||
chart.draw(NSRect(x: box.bounds.origin.x + 1, y: chartPadding, width: chart.frame.width, height: chart.frame.height))
|
||||
|
||||
ctx.restoreGState()
|
||||
self.setWidth(width)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public override func settings(superview: NSView) {
|
||||
let rowHeight: CGFloat = 30
|
||||
let height: CGFloat = ((rowHeight + Constants.Settings.margin) * 4) + Constants.Settings.margin
|
||||
superview.setFrameSize(NSSize(width: superview.frame.width, height: height))
|
||||
|
||||
let view: NSView = NSView(frame: NSRect(x: Constants.Settings.margin, y: Constants.Settings.margin, width: superview.frame.width - (Constants.Settings.margin*2), height: superview.frame.height - (Constants.Settings.margin*2)))
|
||||
|
||||
view.addSubview(ToggleTitleRow(
|
||||
frame: NSRect(x: 0, y: (rowHeight + Constants.Settings.margin) * 3, width: view.frame.width, height: rowHeight),
|
||||
title: "Label",
|
||||
action: #selector(toggleLabel),
|
||||
state: self.labelState
|
||||
))
|
||||
|
||||
view.addSubview(ToggleTitleRow(
|
||||
frame: NSRect(x: 0, y: (rowHeight + Constants.Settings.margin) * 2, width: view.frame.width, height: rowHeight),
|
||||
title: "Box",
|
||||
action: #selector(toggleBox),
|
||||
state: self.boxState
|
||||
))
|
||||
|
||||
view.addSubview(ToggleTitleRow(
|
||||
frame: NSRect(x: 0, y: (rowHeight + Constants.Settings.margin) * 1, width: view.frame.width, height: rowHeight),
|
||||
title: "Value",
|
||||
action: #selector(toggleValue),
|
||||
state: self.valueState
|
||||
))
|
||||
|
||||
view.addSubview(ToggleTitleRow(
|
||||
frame: NSRect(x: 0, y: (rowHeight + Constants.Settings.margin) * 0, width: view.frame.width, height: rowHeight),
|
||||
title: "Colorize",
|
||||
action: #selector(toggleColor),
|
||||
state: self.colorState
|
||||
))
|
||||
|
||||
superview.addSubview(view)
|
||||
}
|
||||
|
||||
public override func setValues(_ values: [value_t]) {
|
||||
let historyValues = values.map{ $0.widget_value }.suffix(60)
|
||||
let end = self.chart.points!.count
|
||||
self.chart.points!.replaceSubrange(end-historyValues.count...end-1, with: historyValues)
|
||||
self.display()
|
||||
}
|
||||
|
||||
public func setValue(_ value: Double) {
|
||||
self.value = value
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.chart.addValue(value)
|
||||
self.display()
|
||||
})
|
||||
}
|
||||
|
||||
@objc private func toggleLabel(_ sender: NSControl) {
|
||||
var state: NSControl.StateValue? = nil
|
||||
if #available(OSX 10.15, *) {
|
||||
state = sender is NSSwitch ? (sender as! NSSwitch).state: nil
|
||||
} else {
|
||||
state = sender is NSButton ? (sender as! NSButton).state: nil
|
||||
}
|
||||
self.labelState = state! == .on ? true : false
|
||||
self.store?.pointee.set(key: "\(self.title)_\(self.type.rawValue)_label", value: self.labelState)
|
||||
self.display()
|
||||
}
|
||||
|
||||
@objc private func toggleBox(_ sender: NSControl) {
|
||||
var state: NSControl.StateValue? = nil
|
||||
if #available(OSX 10.15, *) {
|
||||
state = sender is NSSwitch ? (sender as! NSSwitch).state: nil
|
||||
} else {
|
||||
state = sender is NSButton ? (sender as! NSButton).state: nil
|
||||
}
|
||||
self.boxState = state! == .on ? true : false
|
||||
self.store?.pointee.set(key: "\(self.title)_\(self.type.rawValue)_box", value: self.boxState)
|
||||
self.display()
|
||||
}
|
||||
|
||||
@objc private func toggleValue(_ sender: NSControl) {
|
||||
var state: NSControl.StateValue? = nil
|
||||
if #available(OSX 10.15, *) {
|
||||
state = sender is NSSwitch ? (sender as! NSSwitch).state: nil
|
||||
} else {
|
||||
state = sender is NSButton ? (sender as! NSButton).state: nil
|
||||
}
|
||||
self.valueState = state! == .on ? true : false
|
||||
self.store?.pointee.set(key: "\(self.title)_\(self.type.rawValue)_value", value: self.valueState)
|
||||
self.display()
|
||||
}
|
||||
|
||||
@objc private func toggleColor(_ sender: NSControl) {
|
||||
var state: NSControl.StateValue? = nil
|
||||
if #available(OSX 10.15, *) {
|
||||
state = sender is NSSwitch ? (sender as! NSSwitch).state: nil
|
||||
} else {
|
||||
state = sender is NSButton ? (sender as! NSButton).state: nil
|
||||
}
|
||||
self.colorState = state! == .on ? true : false
|
||||
self.store?.pointee.set(key: "\(self.title)_\(self.type.rawValue)_color", value: self.colorState)
|
||||
self.display()
|
||||
}
|
||||
}
|
||||
168
ModuleKit/Widgets/Mini.swift
Normal file
@@ -0,0 +1,168 @@
|
||||
//
|
||||
// Mini.swift
|
||||
// ModuleKit
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 10/04/2020.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import StatsKit
|
||||
|
||||
public class Mini: Widget {
|
||||
private var valueView: NSTextField = NSTextField()
|
||||
private var labelView: NSTextField = NSTextField()
|
||||
|
||||
public var colorState: Bool = false
|
||||
public var labelState: Bool = true
|
||||
|
||||
private let onlyValueWidth: CGFloat = 38
|
||||
private var value: Double = 0
|
||||
private let store: UnsafePointer<Store>?
|
||||
|
||||
public init(preview: Bool, title: String, config: NSDictionary?, store: UnsafePointer<Store>?) {
|
||||
var widgetTitle: String = title
|
||||
self.store = store
|
||||
if config != nil {
|
||||
var configuration = config!
|
||||
|
||||
if preview {
|
||||
if let previewConfig = config!["Preview"] as? NSDictionary {
|
||||
configuration = previewConfig
|
||||
if let value = configuration["Value"] as? String {
|
||||
self.value = Double(value) ?? 0.38
|
||||
} else {
|
||||
self.value = 0.38
|
||||
}
|
||||
} else {
|
||||
self.value = 0.38
|
||||
}
|
||||
}
|
||||
|
||||
if let titleFromConfig = configuration["Title"] as? String {
|
||||
widgetTitle = titleFromConfig
|
||||
}
|
||||
if let label = configuration["Label"] as? Bool {
|
||||
self.labelState = label
|
||||
}
|
||||
if let color = configuration["Color"] as? Bool {
|
||||
self.colorState = color
|
||||
}
|
||||
}
|
||||
super.init(frame: CGRect(x: 0, y: Constants.Widget.margin, width: Constants.Widget.width, height: Constants.Widget.height - (2*Constants.Widget.margin)))
|
||||
self.title = widgetTitle
|
||||
self.type = .mini
|
||||
self.preview = preview
|
||||
self.canDrawConcurrently = true
|
||||
|
||||
if self.store != nil {
|
||||
self.colorState = store!.pointee.bool(key: "\(self.title)_\(self.type.rawValue)_color", defaultValue: self.colorState)
|
||||
self.labelState = store!.pointee.bool(key: "\(self.title)_\(self.type.rawValue)_label", defaultValue: self.labelState)
|
||||
}
|
||||
}
|
||||
|
||||
public override func draw(_ dirtyRect: NSRect) {
|
||||
super.draw(dirtyRect)
|
||||
|
||||
var width: CGFloat = onlyValueWidth
|
||||
let x: CGFloat = Constants.Widget.margin
|
||||
var valueSize: CGFloat = 13
|
||||
var y: CGFloat = (Constants.Widget.height-valueSize)/2
|
||||
let style = NSMutableParagraphStyle()
|
||||
style.alignment = .center
|
||||
|
||||
if self.labelState {
|
||||
let stringAttributes = [
|
||||
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 7, weight: .light),
|
||||
NSAttributedString.Key.foregroundColor: isDarkMode ? NSColor.white : NSColor.textColor,
|
||||
NSAttributedString.Key.paragraphStyle: NSMutableParagraphStyle()
|
||||
]
|
||||
let rect = CGRect(x: x, y: 12, width: 20, height: 7)
|
||||
let str = NSAttributedString.init(string: self.title, attributes: stringAttributes)
|
||||
str.draw(with: rect)
|
||||
|
||||
y = 1
|
||||
valueSize = 11
|
||||
width = Constants.Widget.width
|
||||
style.alignment = .left
|
||||
}
|
||||
|
||||
let stringAttributes = [
|
||||
NSAttributedString.Key.font: NSFont.systemFont(ofSize: valueSize, weight: .regular),
|
||||
NSAttributedString.Key.foregroundColor: isDarkMode ? NSColor.white : NSColor.textColor,
|
||||
NSAttributedString.Key.paragraphStyle: style
|
||||
]
|
||||
let rect = CGRect(x: x, y: y, width: width - (Constants.Widget.margin*2), height: valueSize)
|
||||
let str = NSAttributedString.init(string: "\(Int(self.value.rounded(toPlaces: 2) * 100))%", attributes: stringAttributes)
|
||||
str.draw(with: rect)
|
||||
|
||||
self.setWidth(width)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public override func settings(superview: NSView) {
|
||||
let height: CGFloat = 60 + (Constants.Settings.margin*3)
|
||||
let rowHeight: CGFloat = 30
|
||||
superview.setFrameSize(NSSize(width: superview.frame.width, height: height))
|
||||
|
||||
let view: NSView = NSView(frame: NSRect(x: Constants.Settings.margin, y: Constants.Settings.margin, width: superview.frame.width - (Constants.Settings.margin*2), height: superview.frame.height - (Constants.Settings.margin*2)))
|
||||
|
||||
view.addSubview(ToggleTitleRow(
|
||||
frame: NSRect(x: 0, y: rowHeight + Constants.Settings.margin, width: view.frame.width, height: rowHeight),
|
||||
title: "Label",
|
||||
action: #selector(toggleLabel),
|
||||
state: self.labelState
|
||||
))
|
||||
|
||||
view.addSubview(ToggleTitleRow(
|
||||
frame: NSRect(x: 0, y: 0, width: view.frame.width, height: rowHeight),
|
||||
title: "Colorize",
|
||||
action: #selector(toggleColor),
|
||||
state: self.colorState
|
||||
))
|
||||
|
||||
superview.addSubview(view)
|
||||
}
|
||||
|
||||
@objc private func toggleColor(_ sender: NSControl) {
|
||||
var state: NSControl.StateValue? = nil
|
||||
if #available(OSX 10.15, *) {
|
||||
state = sender is NSSwitch ? (sender as! NSSwitch).state: nil
|
||||
} else {
|
||||
state = sender is NSButton ? (sender as! NSButton).state: nil
|
||||
}
|
||||
self.colorState = state! == .on ? true : false
|
||||
self.valueView.textColor = value.textUsageColor(color: self.colorState)
|
||||
self.store?.pointee.set(key: "\(self.title)_\(self.type.rawValue)_color", value: self.colorState)
|
||||
self.display()
|
||||
}
|
||||
|
||||
@objc private func toggleLabel(_ sender: NSControl) {
|
||||
var state: NSControl.StateValue? = nil
|
||||
if #available(OSX 10.15, *) {
|
||||
state = sender is NSSwitch ? (sender as! NSSwitch).state: nil
|
||||
} else {
|
||||
state = sender is NSButton ? (sender as! NSButton).state: nil
|
||||
}
|
||||
self.labelState = state! == .on ? true : false
|
||||
self.store?.pointee.set(key: "\(self.title)_\(self.type.rawValue)_label", value: self.labelState)
|
||||
self.display()
|
||||
}
|
||||
|
||||
public func setValue(_ value: Double, sufix: String) {
|
||||
if value == self.value {
|
||||
return
|
||||
}
|
||||
|
||||
self.value = value
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.display()
|
||||
})
|
||||
}
|
||||
}
|
||||
251
ModuleKit/Widgets/Network.swift
Normal file
@@ -0,0 +1,251 @@
|
||||
//
|
||||
// Network.swift
|
||||
// ModuleKit
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 24/05/2020.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import StatsKit
|
||||
|
||||
public enum network_icon_t: String {
|
||||
case no = ""
|
||||
case dot = "Dots"
|
||||
case arrow = "Arrows"
|
||||
case char = "Character"
|
||||
}
|
||||
extension network_icon_t: CaseIterable {}
|
||||
|
||||
public class NetworkWidget: Widget {
|
||||
private var icon: network_icon_t = .dot
|
||||
private var valueState: Bool = true
|
||||
|
||||
private var uploadField: NSTextField? = nil
|
||||
private var downloadField: NSTextField? = nil
|
||||
|
||||
private var uploadValue: Int64 = 0
|
||||
private var downloadValue: Int64 = 0
|
||||
|
||||
private let store: UnsafePointer<Store>?
|
||||
private var width: CGFloat = 52
|
||||
|
||||
public init(preview: Bool, title: String, config: NSDictionary?, store: UnsafePointer<Store>?) {
|
||||
let widgetTitle: String = title
|
||||
self.store = store
|
||||
super.init(frame: CGRect(x: 0, y: Constants.Widget.margin, width: width, height: Constants.Widget.height - (2*Constants.Widget.margin)))
|
||||
self.title = widgetTitle
|
||||
self.type = .network
|
||||
self.preview = preview
|
||||
self.canDrawConcurrently = true
|
||||
|
||||
if self.store != nil {
|
||||
self.valueState = store!.pointee.bool(key: "\(self.title)_\(self.type.rawValue)_value", defaultValue: self.valueState)
|
||||
self.icon = network_icon_t(rawValue: store!.pointee.string(key: "\(self.title)_\(self.type.rawValue)_icon", defaultValue: self.icon.rawValue)) ?? self.icon
|
||||
}
|
||||
|
||||
if preview {
|
||||
self.downloadValue = 8947141
|
||||
self.uploadValue = 478678
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public override func draw(_ dirtyRect: NSRect) {
|
||||
// guard let ctx = NSGraphicsContext.current?.cgContext else { return }
|
||||
super.draw(dirtyRect)
|
||||
|
||||
var width: CGFloat = 10
|
||||
var x: CGFloat = 10
|
||||
|
||||
switch self.icon {
|
||||
case .dot:
|
||||
self.drawDots(dirtyRect)
|
||||
case .arrow:
|
||||
self.drawArrows(dirtyRect)
|
||||
case .char:
|
||||
self.drawChars(dirtyRect)
|
||||
default:
|
||||
x = 0
|
||||
width = 0
|
||||
break
|
||||
}
|
||||
|
||||
if self.valueState {
|
||||
let rowWidth: CGFloat = 42
|
||||
let rowHeight: CGFloat = self.frame.height / 2
|
||||
let style = NSMutableParagraphStyle()
|
||||
style.alignment = .right
|
||||
let stringAttributes = [
|
||||
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 9, weight: .light),
|
||||
NSAttributedString.Key.foregroundColor: NSColor.textColor,
|
||||
NSAttributedString.Key.paragraphStyle: style
|
||||
]
|
||||
|
||||
var rect = CGRect(x: Constants.Widget.margin + x, y: 1, width: rowWidth - (Constants.Widget.margin*2), height: rowHeight)
|
||||
let download = NSAttributedString.init(string: Units(bytes: self.downloadValue).getReadableSpeed(), attributes: stringAttributes)
|
||||
download.draw(with: rect)
|
||||
|
||||
rect = CGRect(x: Constants.Widget.margin + x, y: rect.height+1, width: rowWidth - (Constants.Widget.margin*2), height: rowHeight)
|
||||
let upload = NSAttributedString.init(string: Units(bytes: self.uploadValue).getReadableSpeed(), attributes: stringAttributes)
|
||||
upload.draw(with: rect)
|
||||
|
||||
width += rowWidth
|
||||
}
|
||||
|
||||
if width == 0 {
|
||||
width = 1
|
||||
}
|
||||
self.setWidth(width)
|
||||
}
|
||||
|
||||
private func drawDots(_ dirtyRect: NSRect) {
|
||||
let rowHeight: CGFloat = self.frame.height / 2
|
||||
let size: CGFloat = 6
|
||||
let y: CGFloat = (rowHeight-size)/2
|
||||
|
||||
var downloadCircle = NSBezierPath()
|
||||
downloadCircle = NSBezierPath(ovalIn: CGRect(x: Constants.Widget.margin, y: y-0.2, width: size, height: size))
|
||||
if self.downloadValue >= 1_024 {
|
||||
NSColor(red: (26/255.0), green: (126/255.0), blue: (252/255.0), alpha: 0.8).setFill()
|
||||
} else {
|
||||
NSColor.labelColor.setFill()
|
||||
}
|
||||
downloadCircle.fill()
|
||||
|
||||
var uploadCircle = NSBezierPath()
|
||||
uploadCircle = NSBezierPath(ovalIn: CGRect(x: Constants.Widget.margin, y: 10.5, width: size, height: size))
|
||||
if self.uploadValue >= 1_024 {
|
||||
NSColor.red.setFill()
|
||||
} else {
|
||||
NSColor.labelColor.setFill()
|
||||
}
|
||||
uploadCircle.fill()
|
||||
}
|
||||
|
||||
private func drawArrows(_ dirtyRect: NSRect) {
|
||||
let arrowAngle = CGFloat(Double.pi / 5)
|
||||
let pointerLineLength: CGFloat = 3.5
|
||||
let workingHeight: CGFloat = (self.frame.size.height - (Constants.Widget.margin * 2))
|
||||
let height: CGFloat = ((workingHeight - Constants.Widget.margin) / 2)
|
||||
|
||||
let downloadArrow = NSBezierPath()
|
||||
let downloadStart = CGPoint(x: Constants.Widget.margin + (pointerLineLength/2), y: height + Constants.Widget.margin)
|
||||
let downloadEnd = CGPoint(x: Constants.Widget.margin + (pointerLineLength/2), y: Constants.Widget.margin)
|
||||
downloadArrow.addArrow(start: downloadStart, end: downloadEnd, pointerLineLength: pointerLineLength, arrowAngle: arrowAngle)
|
||||
|
||||
if self.downloadValue >= 1_024 {
|
||||
NSColor(red: (26/255.0), green: (126/255.0), blue: (252/255.0), alpha: 0.8).set()
|
||||
} else {
|
||||
NSColor.labelColor.set()
|
||||
}
|
||||
downloadArrow.lineWidth = 1
|
||||
downloadArrow.stroke()
|
||||
downloadArrow.close()
|
||||
|
||||
let uploadArrow = NSBezierPath()
|
||||
let uploadStart = CGPoint(x: Constants.Widget.margin + (pointerLineLength/2), y: height + (Constants.Widget.margin * 2))
|
||||
let uploadEnd = CGPoint(x: Constants.Widget.margin + (pointerLineLength/2), y: (Constants.Widget.margin * 2) + (height * 2))
|
||||
uploadArrow.addArrow(start: uploadStart, end: uploadEnd, pointerLineLength: pointerLineLength, arrowAngle: arrowAngle)
|
||||
|
||||
if self.uploadValue >= 1_024 {
|
||||
NSColor.red.set()
|
||||
} else {
|
||||
NSColor.labelColor.set()
|
||||
}
|
||||
uploadArrow.lineWidth = 1
|
||||
uploadArrow.stroke()
|
||||
uploadArrow.close()
|
||||
}
|
||||
|
||||
private func drawChars(_ dirtyRect: NSRect) {
|
||||
let rowHeight: CGFloat = self.frame.height / 2
|
||||
|
||||
let downloadAttributes = [
|
||||
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 9, weight: .regular),
|
||||
NSAttributedString.Key.foregroundColor: downloadValue >= 1_024 ? NSColor(red: (26/255.0), green: (126/255.0), blue: (252/255.0), alpha: 0.8) : NSColor.labelColor,
|
||||
NSAttributedString.Key.paragraphStyle: NSMutableParagraphStyle()
|
||||
]
|
||||
var rect = CGRect(x: Constants.Widget.margin, y: 1, width: 8, height: rowHeight)
|
||||
var str = NSAttributedString.init(string: "D", attributes: downloadAttributes)
|
||||
str.draw(with: rect)
|
||||
|
||||
let uploadAttributes = [
|
||||
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 9, weight: .regular),
|
||||
NSAttributedString.Key.foregroundColor: uploadValue >= 1_024 ? NSColor.red : NSColor.labelColor,
|
||||
NSAttributedString.Key.paragraphStyle: NSMutableParagraphStyle()
|
||||
]
|
||||
rect = CGRect(x: Constants.Widget.margin, y: rect.height+1, width: 8, height: rowHeight)
|
||||
str = NSAttributedString.init(string: "U", attributes: uploadAttributes)
|
||||
str.draw(with: rect)
|
||||
}
|
||||
|
||||
public override func settings(superview: NSView) {
|
||||
let height: CGFloat = 60 + (Constants.Settings.margin*3)
|
||||
let rowHeight: CGFloat = 30
|
||||
superview.setFrameSize(NSSize(width: superview.frame.width, height: height))
|
||||
|
||||
let view: NSView = NSView(frame: NSRect(x: Constants.Settings.margin, y: Constants.Settings.margin, width: superview.frame.width - (Constants.Settings.margin*2), height: superview.frame.height - (Constants.Settings.margin*2)))
|
||||
|
||||
view.addSubview(SelectTitleRow(
|
||||
frame: NSRect(x: 0, y: rowHeight + Constants.Settings.margin, width: view.frame.width, height: rowHeight),
|
||||
title: "Pictogram",
|
||||
action: #selector(toggleIcon),
|
||||
items: network_icon_t.allCases.map{ return $0.rawValue },
|
||||
selected: self.icon.rawValue
|
||||
))
|
||||
|
||||
view.addSubview(ToggleTitleRow(
|
||||
frame: NSRect(x: 0, y: 0, width: view.frame.width, height: rowHeight),
|
||||
title: "Value",
|
||||
action: #selector(toggleValue),
|
||||
state: self.valueState
|
||||
))
|
||||
|
||||
superview.addSubview(view)
|
||||
}
|
||||
|
||||
@objc private func toggleValue(_ sender: NSControl) {
|
||||
var state: NSControl.StateValue? = nil
|
||||
if #available(OSX 10.15, *) {
|
||||
state = sender is NSSwitch ? (sender as! NSSwitch).state: nil
|
||||
} else {
|
||||
state = sender is NSButton ? (sender as! NSButton).state: nil
|
||||
}
|
||||
self.valueState = state! == .on ? true : false
|
||||
self.store?.pointee.set(key: "\(self.title)_\(self.type.rawValue)_value", value: self.valueState)
|
||||
self.display()
|
||||
}
|
||||
|
||||
@objc private func toggleIcon(_ sender: NSMenuItem) {
|
||||
let newIcon: network_icon_t = network_icon_t(rawValue: sender.title) ?? .no
|
||||
self.icon = newIcon
|
||||
self.store?.pointee.set(key: "\(self.title)_\(self.type.rawValue)_icon", value: self.icon.rawValue)
|
||||
self.display()
|
||||
}
|
||||
|
||||
public func setValue(upload: Int64, download: Int64) {
|
||||
var updated: Bool = false
|
||||
|
||||
if self.downloadValue != download {
|
||||
self.downloadValue = download
|
||||
updated = true
|
||||
}
|
||||
if self.uploadValue != upload {
|
||||
self.uploadValue = upload
|
||||
updated = true
|
||||
}
|
||||
|
||||
if updated {
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.display()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
287
ModuleKit/module.swift
Normal file
@@ -0,0 +1,287 @@
|
||||
//
|
||||
// module.swift
|
||||
// ModuleKit
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 09/04/2020.
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import os.log
|
||||
import StatsKit
|
||||
|
||||
public protocol Module_p {
|
||||
var available: Bool { get }
|
||||
var enabled: Bool { get }
|
||||
|
||||
var widget: Widget_p? { get }
|
||||
var settings: Settings_p? { get }
|
||||
|
||||
func load()
|
||||
func terminate()
|
||||
}
|
||||
|
||||
public struct module_c {
|
||||
public var name: String = ""
|
||||
public var icon: NSImage? = nil
|
||||
|
||||
var defaultState: Bool = false
|
||||
var defaultWidget: widget_t = .unknown
|
||||
var availableWidgets: [widget_t] = []
|
||||
|
||||
var widgetsConfig: NSDictionary = NSDictionary()
|
||||
|
||||
init(in path: String) {
|
||||
let dict: NSDictionary = NSDictionary(contentsOfFile: path)!
|
||||
|
||||
if let name = dict["Name"] as? String {
|
||||
self.name = name
|
||||
}
|
||||
if let state = dict["State"] as? Bool {
|
||||
self.defaultState = state
|
||||
}
|
||||
|
||||
if let widgetsDict = dict["Widgets"] as? NSDictionary {
|
||||
self.widgetsConfig = widgetsDict
|
||||
for widgetName in widgetsDict.allKeys {
|
||||
if let widget = widget_t(rawValue: widgetName as! String) {
|
||||
self.availableWidgets.append(widget)
|
||||
|
||||
let widgetDict = widgetsDict[widgetName as! String] as! NSDictionary
|
||||
if widgetDict["Default"] as! Bool {
|
||||
self.defaultWidget = widget
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class Module: Module_p {
|
||||
public var config: module_c
|
||||
|
||||
public var available: Bool = false
|
||||
public var enabled: Bool = false
|
||||
|
||||
public var widget: Widget_p? = nil
|
||||
public var settings: Settings_p? = nil
|
||||
|
||||
private var settingsView: Settings_v? = nil
|
||||
private var popup: NSWindow = NSWindow()
|
||||
|
||||
private let log: OSLog
|
||||
private var store: UnsafePointer<Store>? = nil
|
||||
private var readers: [Reader_p] = []
|
||||
private var menuBarItem: NSStatusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
|
||||
private var activeWidget: widget_t {
|
||||
get {
|
||||
let widgetStr = self.store?.pointee.string(key: "\(self.config.name)_widget", defaultValue: self.config.defaultWidget.rawValue)
|
||||
return widget_t.allCases.first{ $0.rawValue == widgetStr } ?? widget_t.unknown
|
||||
}
|
||||
set {}
|
||||
}
|
||||
private var ready: Bool = false
|
||||
private var widgetLoaded: Bool = false
|
||||
|
||||
public init(store: UnsafePointer<Store>?, popup: NSView?, settings: Settings_v?) {
|
||||
self.config = module_c(in: Bundle(for: type(of: self)).path(forResource: "config", ofType: "plist")!)
|
||||
|
||||
self.log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: self.config.name)
|
||||
self.store = store
|
||||
self.settingsView = settings
|
||||
self.available = self.isAvailable()
|
||||
self.enabled = self.store?.pointee.bool(key: "\(self.config.name)_state", defaultValue: self.config.defaultState) ?? false
|
||||
self.menuBarItem.isVisible = self.enabled
|
||||
self.menuBarItem.autosaveName = self.config.name
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(listenForWidgetSwitch), name: .switchWidget, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(listenForMouseDownInSettings), name: .clickInSettings, object: nil)
|
||||
|
||||
if self.config.widgetsConfig.count != 0 {
|
||||
self.setWidget()
|
||||
} else {
|
||||
os_log(.debug, log: log, "Module started without widget")
|
||||
}
|
||||
|
||||
self.settings = Settings(config: &self.config, enabled: self.enabled, activeWidget: self.widget, moduleSettings: { [weak self] (_ superview: NSView) in
|
||||
if self != nil && self?.settingsView != nil {
|
||||
self!.settingsView!.load(rect: superview.frame, widget: self!.activeWidget)
|
||||
superview.setFrameSize(NSSize(width: superview.frame.width, height: self!.settingsView!.frame.height))
|
||||
superview.addSubview(self!.settingsView!)
|
||||
}
|
||||
})
|
||||
self.settings?.toggleCallback = { [weak self] in
|
||||
self?.toggleEnabled()
|
||||
}
|
||||
|
||||
self.popup = PopupWindow(title: self.config.name, view: popup)
|
||||
}
|
||||
|
||||
// load function which call when app start
|
||||
public func load() {
|
||||
if self.enabled && self.widget != nil && self.ready {
|
||||
DispatchQueue.main.async {
|
||||
self.menuBarItem.button?.target = self
|
||||
self.menuBarItem.button?.action = #selector(self.togglePopup)
|
||||
self.menuBarItem.button?.sendAction(on: [.leftMouseDown, .rightMouseDown])
|
||||
|
||||
self.menuBarItem.length = self.widget!.frame.width
|
||||
self.menuBarItem.button?.addSubview(self.widget!)
|
||||
self.widgetLoaded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// terminate function which call before app termination
|
||||
public func terminate() {
|
||||
self.willTerminate()
|
||||
self.readers.forEach{
|
||||
$0.stop()
|
||||
$0.terminate()
|
||||
}
|
||||
NSStatusBar.system.removeStatusItem(self.menuBarItem)
|
||||
os_log(.debug, log: log, "Module terminated")
|
||||
}
|
||||
|
||||
// function to call before module terminate
|
||||
open func willTerminate() {}
|
||||
|
||||
// set module state to enabled
|
||||
public func enable() {
|
||||
self.enabled = true
|
||||
self.store?.pointee.set(key: "\(self.config.name)_state", value: true)
|
||||
self.readers.forEach{ $0.start() }
|
||||
self.menuBarItem.isVisible = true
|
||||
if self.menuBarItem.length < 0 {
|
||||
self.load()
|
||||
}
|
||||
os_log(.debug, log: log, "Module enabled")
|
||||
}
|
||||
|
||||
// set module state to disabled
|
||||
public func disable() {
|
||||
self.enabled = false
|
||||
self.store?.pointee.set(key: "\(self.config.name)_state", value: false)
|
||||
self.readers.forEach{ $0.pause() }
|
||||
self.menuBarItem.isVisible = false
|
||||
self.popup.setIsVisible(false)
|
||||
os_log(.debug, log: log, "Module disabled")
|
||||
}
|
||||
|
||||
// toggle module state
|
||||
private func toggleEnabled() {
|
||||
if self.enabled {
|
||||
self.disable()
|
||||
} else {
|
||||
self.enable()
|
||||
}
|
||||
}
|
||||
|
||||
// add reader to module. If module is enabled will fire a read function and start a reader
|
||||
public func addReader(_ reader: Reader_p) {
|
||||
if self.enabled {
|
||||
reader.start()
|
||||
}
|
||||
self.readers.append(reader)
|
||||
|
||||
os_log(.debug, log: log, "Successfully add reader %s", "\(reader.self)")
|
||||
}
|
||||
|
||||
// handler for reader, calls when main reader is ready, and return first value
|
||||
public func readyHandler() {
|
||||
os_log(.debug, log: log, "Reader report readiness")
|
||||
self.ready = true
|
||||
if !self.widgetLoaded {
|
||||
self.load()
|
||||
}
|
||||
}
|
||||
|
||||
// change menu item width
|
||||
public func widgetWidthHandler(_ width: CGFloat) {
|
||||
os_log(.debug, log: log, "Widget %s change width to %.2f", "\(type(of: self.widget!))", width)
|
||||
self.menuBarItem.length = width
|
||||
}
|
||||
|
||||
// determine if module is available (can be overrided in module)
|
||||
open func isAvailable() -> Bool { return true }
|
||||
|
||||
// load and setup widget
|
||||
private func setWidget() {
|
||||
self.widget = LoadWidget(self.activeWidget, preview: false, title: self.config.name, config: self.config.widgetsConfig, store: self.store)
|
||||
if self.widget == nil {
|
||||
self.enabled = false
|
||||
os_log(.error, log: log, "widget with type %s not found", "\(self.activeWidget)")
|
||||
return
|
||||
}
|
||||
os_log(.debug, log: log, "Successfully initialize widget: %s", "\(String(describing: self.widget!))")
|
||||
|
||||
self.widget?.widthHandler = { [weak self] value in
|
||||
self?.widgetWidthHandler(value)
|
||||
}
|
||||
|
||||
self.readers.forEach{ $0.read() }
|
||||
if let mainReader = self.readers.first(where: { !$0.optional }) {
|
||||
self.widget?.setValues(mainReader.getHistory())
|
||||
}
|
||||
|
||||
if self.ready {
|
||||
self.menuBarItem.length = self.widget!.frame.width
|
||||
self.menuBarItem.button?.subviews.forEach{ $0.removeFromSuperview() }
|
||||
self.menuBarItem.button?.addSubview(self.widget!)
|
||||
}
|
||||
|
||||
self.settings?.setActiveWidget(self.widget)
|
||||
}
|
||||
|
||||
@objc private func togglePopup(_ sender: Any?) {
|
||||
let openedWindows = NSApplication.shared.windows.filter{ $0 is NSPanel }
|
||||
openedWindows.forEach{ $0.setIsVisible(false) }
|
||||
|
||||
if self.popup.occlusionState.rawValue == 8192 {
|
||||
NSApplication.shared.activate(ignoringOtherApps: true)
|
||||
|
||||
let buttonOrigin = self.menuBarItem.button?.window?.frame.origin
|
||||
let buttonCenter = (self.menuBarItem.button?.window?.frame.width)! / 2
|
||||
let windowCenter = self.popup.frame.width / 2
|
||||
|
||||
self.popup.contentView?.invalidateIntrinsicContentSize()
|
||||
var x = buttonOrigin!.x - windowCenter + buttonCenter
|
||||
let y = buttonOrigin!.y - self.popup.contentView!.intrinsicContentSize.height - 3
|
||||
|
||||
if let screen = NSScreen.main {
|
||||
let width = screen.frame.size.width
|
||||
|
||||
if x + self.popup.frame.width > width {
|
||||
x = width - self.popup.frame.width
|
||||
}
|
||||
}
|
||||
if buttonOrigin!.x - self.popup.frame.width < 0 {
|
||||
x = 0
|
||||
}
|
||||
|
||||
self.popup.setFrameOrigin(NSPoint(x: x, y: y))
|
||||
self.popup.setIsVisible(true)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func listenForWidgetSwitch(_ notification: Notification) {
|
||||
if let moduleName = notification.userInfo?["module"] as? String {
|
||||
if let widgetName = notification.userInfo?["widget"] as? String {
|
||||
if moduleName == self.config.name {
|
||||
if let widgetType = widget_t.allCases.first(where: { $0.rawValue == widgetName }) {
|
||||
self.activeWidget = widgetType
|
||||
self.store?.pointee.set(key: "\(self.config.name)_widget", value: widgetType.rawValue)
|
||||
self.setWidget()
|
||||
os_log(.debug, log: log, "Widget is changed to: %s", "\(widgetName)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func listenForMouseDownInSettings(_ notification: Notification) {
|
||||
if self.popup.isVisible {
|
||||
self.popup.setIsVisible(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
220
ModuleKit/popup.swift
Normal file
@@ -0,0 +1,220 @@
|
||||
//
|
||||
// popup.swift
|
||||
// ModuleKit
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 11/04/2020.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import StatsKit
|
||||
|
||||
internal class PopupWindow: NSPanel, NSWindowDelegate {
|
||||
private let viewController: PopupViewController = PopupViewController()
|
||||
|
||||
init(title: String, view: NSView?) {
|
||||
self.viewController.setup(title: title, view: view)
|
||||
|
||||
super.init(
|
||||
contentRect: NSMakeRect(0, 0, self.viewController.view.frame.width, self.viewController.view.frame.height),
|
||||
styleMask: [],
|
||||
backing: .buffered,
|
||||
defer: true
|
||||
)
|
||||
|
||||
self.contentViewController = viewController
|
||||
self.backingType = .buffered
|
||||
self.isFloatingPanel = true
|
||||
self.becomesKeyOnlyIfNeeded = true
|
||||
self.styleMask = .borderless
|
||||
self.animationBehavior = .default
|
||||
self.collectionBehavior = .transient
|
||||
self.backgroundColor = .clear
|
||||
self.hasShadow = true
|
||||
self.setIsVisible(false)
|
||||
}
|
||||
}
|
||||
|
||||
internal class PopupViewController: NSViewController {
|
||||
private var popup: PopupView
|
||||
|
||||
public init() {
|
||||
self.popup = PopupView(frame: NSRect(x: 0, y: 0, width: Constants.Popup.width + (Constants.Popup.margins * 2), height: Constants.Popup.height+Constants.Popup.headerHeight))
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func loadView() {
|
||||
self.view = self.popup
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
}
|
||||
|
||||
override func viewWillAppear() {
|
||||
self.popup.appear()
|
||||
}
|
||||
|
||||
override func viewWillDisappear() {
|
||||
self.popup.disappear()
|
||||
}
|
||||
|
||||
public func setup(title: String, view: NSView?) {
|
||||
self.title = title
|
||||
self.popup.headerView?.titleView?.stringValue = title
|
||||
self.popup.setView(view)
|
||||
}
|
||||
}
|
||||
|
||||
internal class PopupView: NSView {
|
||||
public var headerView: HeaderView? = nil
|
||||
private var mainView: NSView? = nil
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
var h: CGFloat = self.mainView?.subviews.first?.frame.height ?? 0
|
||||
if h != 0 {
|
||||
h += Constants.Popup.margins*2
|
||||
}
|
||||
return CGSize(width: self.frame.size.width, height: h + Constants.Popup.headerHeight)
|
||||
}
|
||||
|
||||
override init(frame: NSRect) {
|
||||
super.init(frame: CGRect(x: frame.origin.x, y: frame.origin.y, width: frame.width, height: frame.height))
|
||||
self.wantsLayer = true
|
||||
self.canDrawConcurrently = true
|
||||
self.layer!.cornerRadius = 3
|
||||
|
||||
self.headerView = HeaderView(frame: NSRect(x: 0, y: frame.height - Constants.Popup.headerHeight, width: frame.width, height: Constants.Popup.headerHeight))
|
||||
|
||||
let mainView: NSView = NSView(frame: NSRect(x: Constants.Popup.margins, y: Constants.Popup.margins, width: frame.width - (Constants.Popup.margins*2), height: 0))
|
||||
|
||||
self.addSubview(self.headerView!)
|
||||
self.addSubview(mainView)
|
||||
|
||||
self.mainView = mainView
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func updateLayer() {
|
||||
if self.mainView!.subviews.count != 0 {
|
||||
if self.mainView?.frame.height != self.mainView!.subviews.first!.frame.size.height {
|
||||
self.setHeight(self.mainView!.subviews.first!.frame.size)
|
||||
}
|
||||
}
|
||||
self.layer!.backgroundColor = self.isDarkMode ? NSColor.windowBackgroundColor.cgColor : NSColor.white.cgColor
|
||||
}
|
||||
|
||||
public func setView(_ view: NSView?) {
|
||||
if view == nil {
|
||||
self.setFrameSize(NSSize(width: Constants.Popup.width+(Constants.Popup.margins*2), height: Constants.Popup.headerHeight))
|
||||
self.headerView?.setFrameOrigin(NSPoint(x: 0, y: 0))
|
||||
return
|
||||
}
|
||||
|
||||
self.mainView?.addSubview(view!)
|
||||
self.setHeight(view!.frame.size)
|
||||
}
|
||||
|
||||
private func setHeight(_ size: CGSize) {
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.mainView?.setFrameSize(NSSize(width: self.mainView!.frame.width, height: size.height))
|
||||
self.setFrameSize(NSSize(width: size.width + (Constants.Popup.margins*2), height: size.height + Constants.Popup.headerHeight + Constants.Popup.margins*2))
|
||||
self.headerView?.setFrameOrigin(NSPoint(x: 0, y: self.frame.height - Constants.Popup.headerHeight))
|
||||
|
||||
var frame = self.window?.frame
|
||||
frame?.size = self.frame.size
|
||||
self.window?.setFrame(frame!, display: true)
|
||||
})
|
||||
}
|
||||
|
||||
internal func appear() {
|
||||
self.display()
|
||||
self.mainView?.subviews.first{ !($0 is HeaderView) }?.display()
|
||||
}
|
||||
internal func disappear() {}
|
||||
}
|
||||
|
||||
internal class HeaderView: NSView {
|
||||
public var titleView: NSTextField? = nil
|
||||
|
||||
private var settingsButton: NSButton?
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override init(frame: NSRect) {
|
||||
super.init(frame: CGRect(x: frame.origin.x, y: frame.origin.y, width: frame.width, height: frame.height))
|
||||
|
||||
let titleView = NSTextField(frame: NSMakeRect(frame.width/4, (frame.height - 18)/2, frame.width/2, 18))
|
||||
titleView.isEditable = false
|
||||
titleView.isSelectable = false
|
||||
titleView.isBezeled = false
|
||||
titleView.wantsLayer = true
|
||||
titleView.textColor = .labelColor
|
||||
titleView.backgroundColor = .clear
|
||||
titleView.canDrawSubviewsIntoLayer = true
|
||||
titleView.alignment = .center
|
||||
titleView.font = NSFont.systemFont(ofSize: 16, weight: .medium)
|
||||
titleView.stringValue = ""
|
||||
|
||||
self.titleView = titleView
|
||||
self.addSubview(titleView)
|
||||
|
||||
let button = NSButtonWithPadding()
|
||||
button.frame = CGRect(x: frame.width - 38, y: 2, width: 30, height: 30)
|
||||
button.verticalPadding = 14
|
||||
button.horizontalPadding = 14
|
||||
button.bezelStyle = .regularSquare
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
button.imageScaling = .scaleNone
|
||||
button.image = Bundle(for: type(of: self)).image(forResource: "settings")!
|
||||
button.contentTintColor = .lightGray
|
||||
button.isBordered = false
|
||||
button.action = #selector(openMenu)
|
||||
button.target = self
|
||||
|
||||
let trackingArea = NSTrackingArea(rect: button.frame, options: [NSTrackingArea.Options.activeAlways, NSTrackingArea.Options.mouseEnteredAndExited, NSTrackingArea.Options.activeInActiveApp], owner: self, userInfo: nil)
|
||||
self.addTrackingArea(trackingArea)
|
||||
|
||||
self.addSubview(button)
|
||||
|
||||
self.settingsButton = button
|
||||
}
|
||||
|
||||
override func draw(_ dirtyRect: NSRect) {
|
||||
super.draw(dirtyRect)
|
||||
|
||||
NSColor.gridColor.set()
|
||||
let line = NSBezierPath()
|
||||
line.move(to: NSMakePoint(0, 0))
|
||||
line.line(to: NSMakePoint(self.frame.width, 0))
|
||||
line.lineWidth = 1
|
||||
line.stroke()
|
||||
}
|
||||
|
||||
override func mouseEntered(with: NSEvent) {
|
||||
self.settingsButton!.contentTintColor = .gray
|
||||
NSCursor.pointingHand.set()
|
||||
}
|
||||
|
||||
override func mouseExited(with: NSEvent) {
|
||||
self.settingsButton!.contentTintColor = .lightGray
|
||||
NSCursor.arrow.set()
|
||||
}
|
||||
|
||||
@objc func openMenu(_ sender: Any) {
|
||||
self.window?.setIsVisible(false)
|
||||
NotificationCenter.default.post(name: .toggleSettings, object: nil, userInfo: ["module": self.titleView?.stringValue ?? ""])
|
||||
}
|
||||
}
|
||||
129
ModuleKit/reader.swift
Normal file
@@ -0,0 +1,129 @@
|
||||
//
|
||||
// reader.swift
|
||||
// ModuleKit
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 10/04/2020.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import Repeat
|
||||
import os.log
|
||||
|
||||
public protocol value_t {
|
||||
var widget_value: Double { get }
|
||||
}
|
||||
|
||||
public protocol Reader_p {
|
||||
var optional: Bool { get }
|
||||
|
||||
func setup() -> Void
|
||||
func read() -> Void
|
||||
func terminate() -> Void
|
||||
|
||||
func getValue<T>() -> T
|
||||
func getHistory() -> [value_t]
|
||||
|
||||
func start() -> Void
|
||||
func pause() -> Void
|
||||
func stop() -> Void
|
||||
}
|
||||
|
||||
public protocol ReaderInternal_p {
|
||||
associatedtype T
|
||||
|
||||
var value: T? { get }
|
||||
func read() -> Void
|
||||
}
|
||||
|
||||
open class Reader<T>: ReaderInternal_p {
|
||||
public let log: OSLog
|
||||
public var value: T?
|
||||
public var interval: Int = 1000
|
||||
public var optional: Bool = false
|
||||
|
||||
public var readyCallback: () -> Void = {}
|
||||
public var callbackHandler: (T?) -> Void = {_ in }
|
||||
|
||||
private var repeatTask: Repeater?
|
||||
private var nilCallbackCounter: Int = 0
|
||||
private var ready: Bool = false
|
||||
|
||||
private var history: [T]? = []
|
||||
|
||||
public init() {
|
||||
self.log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "\(T.self)")
|
||||
|
||||
self.setup()
|
||||
|
||||
self.repeatTask = Repeater.init(interval: .milliseconds(self.interval), observer: { _ in
|
||||
self.read()
|
||||
})
|
||||
|
||||
os_log(.debug, log: self.log, "Successfully initialize reader")
|
||||
}
|
||||
|
||||
public func callback(_ value: T?) {
|
||||
if !self.optional && !self.ready {
|
||||
if self.value == nil && value != nil {
|
||||
self.readyCallback()
|
||||
} else if self.value == nil && value == nil {
|
||||
if self.nilCallbackCounter > 5 {
|
||||
os_log(.error, log: self.log, "Callback receive nil value more than 5 times. Please check this reader!")
|
||||
self.stop()
|
||||
return
|
||||
} else {
|
||||
os_log(.debug, log: self.log, "Restarting initial read")
|
||||
self.nilCallbackCounter += 1
|
||||
self.read()
|
||||
return
|
||||
}
|
||||
} else if self.nilCallbackCounter != 0 && value != nil {
|
||||
self.nilCallbackCounter = 0
|
||||
}
|
||||
}
|
||||
|
||||
self.value = value
|
||||
if !self.ready {
|
||||
self.ready = true
|
||||
os_log(.debug, log: self.log, "Reader is ready")
|
||||
}
|
||||
if value != nil {
|
||||
if self.history?.count ?? 0 >= 300 {
|
||||
self.history!.remove(at: 0)
|
||||
}
|
||||
self.history?.append(value!)
|
||||
self.callbackHandler(value!)
|
||||
}
|
||||
}
|
||||
|
||||
open func read() {}
|
||||
open func setup() {}
|
||||
open func terminate() {}
|
||||
|
||||
open func start() {
|
||||
self.read()
|
||||
self.repeatTask!.start()
|
||||
}
|
||||
|
||||
open func pause() {
|
||||
self.repeatTask!.pause()
|
||||
}
|
||||
|
||||
open func stop() {
|
||||
self.repeatTask!.removeAllObservers(thenStop: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension Reader: Reader_p {
|
||||
public func getValue<T>() -> T {
|
||||
return self.value as! T
|
||||
}
|
||||
|
||||
public func getHistory<T>() -> [T] {
|
||||
return self.history as! [T]
|
||||
}
|
||||
}
|
||||
268
ModuleKit/settings.swift
Normal file
@@ -0,0 +1,268 @@
|
||||
//
|
||||
// settings.swift
|
||||
// ModuleKit
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 13/04/2020.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
public protocol Settings_p: NSView {
|
||||
var toggleCallback: () -> () { get set }
|
||||
func setActiveWidget(_ widget: Widget_p?)
|
||||
}
|
||||
|
||||
public protocol Settings_v: NSView {
|
||||
func load(rect: NSRect, widget: widget_t)
|
||||
}
|
||||
|
||||
open class Settings: NSView, Settings_p {
|
||||
public var toggleCallback: () -> () = {}
|
||||
|
||||
private let headerHeight: CGFloat = 42
|
||||
private var widgetSelectorHeight: CGFloat = Constants.Widget.height + (Constants.Settings.margin*2)
|
||||
|
||||
private var widgetSelectorView: NSView? = nil
|
||||
private var widgetSettingsView: NSView? = nil
|
||||
private var moduleSettingsView: NSView? = nil
|
||||
|
||||
private var config: UnsafePointer<module_c>
|
||||
private var activeWidget: Widget_p?
|
||||
|
||||
private var moduleSettings: (_ superview: NSView) -> ()
|
||||
|
||||
init(config: UnsafePointer<module_c>, enabled: Bool, activeWidget: Widget_p?, moduleSettings: @escaping (_ superview: NSView) -> ()) {
|
||||
self.config = config
|
||||
self.activeWidget = activeWidget
|
||||
self.moduleSettings = moduleSettings
|
||||
super.init(frame: NSRect(x: 0, y: 0, width: Constants.Settings.width, height: Constants.Settings.height))
|
||||
self.wantsLayer = true
|
||||
self.layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor
|
||||
|
||||
addHeader(state: enabled)
|
||||
addWidgetSelector()
|
||||
addWidgetSettings()
|
||||
addModuleSettings()
|
||||
}
|
||||
|
||||
private func addModuleSettings() {
|
||||
let y: CGFloat = self.frame.height - headerHeight - widgetSelectorHeight - (self.widgetSettingsView?.frame.height ?? 0)
|
||||
let view: NSView = NSView(frame: NSRect(x: Constants.Settings.margin, y: y - (Constants.Settings.margin*3), width: self.frame.width - (Constants.Settings.margin*2), height: 0))
|
||||
view.wantsLayer = true
|
||||
view.layer?.backgroundColor = .white
|
||||
view.layer!.cornerRadius = 3
|
||||
|
||||
self.appearance = NSAppearance(named: .aqua)
|
||||
|
||||
self.moduleSettings(view)
|
||||
|
||||
if view.frame.height != 0 {
|
||||
view.setFrameOrigin(NSPoint(x: view.frame.origin.x, y: view.frame.origin.y - view.frame.height))
|
||||
self.addSubview(view)
|
||||
self.moduleSettingsView = view
|
||||
}
|
||||
}
|
||||
|
||||
private func addWidgetSettings() {
|
||||
if self.activeWidget == nil {
|
||||
return
|
||||
}
|
||||
|
||||
let view: NSView = NSView(frame: NSRect(x: Constants.Settings.margin, y: self.frame.height - headerHeight - widgetSelectorHeight - (Constants.Settings.margin*2), width: self.frame.width - (Constants.Settings.margin*2), height: 0))
|
||||
view.wantsLayer = true
|
||||
view.layer?.backgroundColor = .white
|
||||
view.layer!.cornerRadius = 3
|
||||
|
||||
self.activeWidget?.settings(superview: view)
|
||||
|
||||
if view.frame.height != 0 {
|
||||
view.setFrameOrigin(NSPoint(x: view.frame.origin.x, y: view.frame.origin.y - view.frame.height))
|
||||
self.addSubview(view)
|
||||
self.widgetSettingsView = view
|
||||
}
|
||||
}
|
||||
|
||||
private func addWidgetSelector() {
|
||||
if self.config.pointee.availableWidgets.count == 0 {
|
||||
self.widgetSelectorHeight = 0
|
||||
return
|
||||
}
|
||||
|
||||
let view: NSView = NSView(frame: NSRect(x: Constants.Settings.margin, y: self.frame.height - self.headerHeight - self.widgetSelectorHeight - Constants.Settings.margin, width: self.frame.width - (Constants.Settings.margin*2), height: self.widgetSelectorHeight))
|
||||
view.wantsLayer = true
|
||||
view.layer?.backgroundColor = .white
|
||||
view.layer!.cornerRadius = 3
|
||||
|
||||
var x: CGFloat = Constants.Settings.margin
|
||||
for i in 0...self.config.pointee.availableWidgets.count - 1 {
|
||||
let widgetType = self.config.pointee.availableWidgets[i]
|
||||
if let widget = LoadWidget(widgetType, preview: true, title: self.config.pointee.name, config: self.config.pointee.widgetsConfig, store: nil) {
|
||||
let preview = WidgetPreview(
|
||||
frame: NSRect(x: x, y: Constants.Settings.margin, width: widget.frame.width, height: self.widgetSelectorHeight - (Constants.Settings.margin*2)),
|
||||
title: self.config.pointee.name,
|
||||
widget: widget,
|
||||
state: self.activeWidget?.type == widgetType
|
||||
)
|
||||
preview.widthCallback = { [weak self] in
|
||||
self?.recalculateWidgetSelectorOptionsWidth()
|
||||
}
|
||||
view.addSubview(preview)
|
||||
x += widget.frame.width + Constants.Settings.margin
|
||||
}
|
||||
}
|
||||
|
||||
self.addSubview(view)
|
||||
self.widgetSelectorView = view
|
||||
}
|
||||
|
||||
private func recalculateWidgetSelectorOptionsWidth() {
|
||||
var x: CGFloat = Constants.Settings.margin
|
||||
self.widgetSelectorView?.subviews.forEach({ (v: NSView) in
|
||||
v.setFrameOrigin(NSPoint(x: x, y: v.frame.origin.y))
|
||||
x += v.frame.width + Constants.Settings.margin
|
||||
})
|
||||
}
|
||||
|
||||
private func addHeader(state: Bool) {
|
||||
let view: NSView = NSView(frame: NSRect(x: 0, y: self.frame.height - self.headerHeight, width: self.frame.width, height: self.headerHeight))
|
||||
view.wantsLayer = true
|
||||
|
||||
let titleView = NSTextField(frame: NSRect(x: Constants.Settings.margin, y: (view.frame.height-20)/2, width: self.frame.width - 65, height: 20))
|
||||
titleView.isEditable = false
|
||||
titleView.isSelectable = false
|
||||
titleView.isBezeled = false
|
||||
titleView.wantsLayer = true
|
||||
titleView.textColor = .black
|
||||
titleView.backgroundColor = .clear
|
||||
titleView.canDrawSubviewsIntoLayer = true
|
||||
titleView.alignment = .natural
|
||||
titleView.font = NSFont.systemFont(ofSize: 18, weight: .light)
|
||||
titleView.stringValue = self.config.pointee.name
|
||||
|
||||
var toggle: NSControl = NSControl()
|
||||
if #available(OSX 10.15, *) {
|
||||
let switchButton = NSSwitch(frame: NSRect(x: self.frame.width-55, y: 0, width: 50, height: view.frame.height))
|
||||
switchButton.state = state ? .on : .off
|
||||
switchButton.action = #selector(self.toggleEnable)
|
||||
switchButton.target = self
|
||||
|
||||
toggle = switchButton
|
||||
} else {
|
||||
let button: NSButton = NSButton(frame: NSRect(x: self.frame.width-55, y: 0, width: 30, height: view.frame.height))
|
||||
button.setButtonType(.switch)
|
||||
button.state = state ? .on : .off
|
||||
button.title = ""
|
||||
button.action = #selector(self.toggleEnable)
|
||||
button.isBordered = false
|
||||
button.isTransparent = true
|
||||
|
||||
toggle = button
|
||||
}
|
||||
|
||||
let line: NSView = NSView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 1))
|
||||
line.wantsLayer = true
|
||||
line.layer?.backgroundColor = NSColor(hexString: "#d1d1d1").cgColor
|
||||
|
||||
view.addSubview(titleView)
|
||||
view.addSubview(toggle)
|
||||
view.addSubview(line)
|
||||
|
||||
self.addSubview(view)
|
||||
}
|
||||
|
||||
@objc func toggleEnable(_ sender: Any) {
|
||||
self.toggleCallback()
|
||||
}
|
||||
|
||||
public func setActiveWidget(_ widget: Widget_p?) {
|
||||
self.activeWidget = widget
|
||||
|
||||
self.subviews.filter{ $0 == self.widgetSettingsView || $0 == self.moduleSettingsView }.forEach{ $0.removeFromSuperview() }
|
||||
self.addWidgetSettings()
|
||||
self.addModuleSettings()
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
class WidgetPreview: NSView {
|
||||
private let type: widget_t
|
||||
private var state: Bool
|
||||
private let title: String
|
||||
|
||||
public var widthCallback: () -> Void = {}
|
||||
|
||||
public init(frame: NSRect, title: String, widget: Widget_p, state: Bool) {
|
||||
self.type = widget.type
|
||||
self.state = state
|
||||
self.title = title
|
||||
super.init(frame: frame)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(maybeActivate), name: .switchWidget, object: nil)
|
||||
|
||||
self.wantsLayer = true
|
||||
self.layer?.cornerRadius = 2
|
||||
self.layer?.borderColor = self.state ? NSColor.systemBlue.cgColor : NSColor(hexString: "#dddddd").cgColor
|
||||
self.layer?.borderWidth = 1
|
||||
|
||||
widget.widthHandler = { [weak self] value in
|
||||
self?.removeTrackingArea((self?.trackingAreas.first)!)
|
||||
|
||||
let rect = NSRect(x: 0, y: 0, width: value, height: self!.frame.height)
|
||||
let trackingArea = NSTrackingArea(rect: rect, options: [NSTrackingArea.Options.activeAlways, NSTrackingArea.Options.mouseEnteredAndExited, NSTrackingArea.Options.activeInActiveApp], owner: self, userInfo: ["menu": self!.type])
|
||||
self?.addTrackingArea(trackingArea)
|
||||
|
||||
DispatchQueue.main.async(execute: {
|
||||
self?.setFrameSize(NSSize(width: value, height: self?.frame.height ?? Constants.Widget.height))
|
||||
self?.widthCallback()
|
||||
})
|
||||
}
|
||||
self.addSubview(widget)
|
||||
|
||||
let rect = NSRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height)
|
||||
let trackingArea = NSTrackingArea(rect: rect, options: [NSTrackingArea.Options.activeAlways, NSTrackingArea.Options.mouseEnteredAndExited, NSTrackingArea.Options.activeInActiveApp], owner: self, userInfo: ["menu": self.type])
|
||||
self.addTrackingArea(trackingArea)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func mouseEntered(with: NSEvent) {
|
||||
self.layer?.borderColor = NSColor.systemBlue.cgColor
|
||||
NSCursor.pointingHand.set()
|
||||
}
|
||||
|
||||
override func mouseExited(with: NSEvent) {
|
||||
self.layer?.borderColor = self.state ? NSColor.systemBlue.cgColor : NSColor.tertiaryLabelColor.cgColor
|
||||
NSCursor.arrow.set()
|
||||
}
|
||||
|
||||
override func mouseDown(with: NSEvent) {
|
||||
if !self.state {
|
||||
NotificationCenter.default.post(name: .switchWidget, object: nil, userInfo: ["module": self.title, "widget": self.type.rawValue])
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func maybeActivate(_ notification: Notification) {
|
||||
if let moduleName = notification.userInfo?["module"] as? String {
|
||||
if moduleName == self.title {
|
||||
if let widgetName = notification.userInfo?["widget"] as? String {
|
||||
if widgetName == self.type.rawValue {
|
||||
self.layer?.borderColor = NSColor.systemBlue.cgColor
|
||||
self.state = true
|
||||
} else {
|
||||
self.layer?.borderColor = NSColor.tertiaryLabelColor.cgColor
|
||||
self.state = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
97
ModuleKit/widget.swift
Normal file
@@ -0,0 +1,97 @@
|
||||
//
|
||||
// widget.swift
|
||||
// ModuleKit
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 10/04/2020.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import StatsKit
|
||||
|
||||
public enum widget_t: String {
|
||||
case unknown = ""
|
||||
case mini = "mini"
|
||||
case lineChart = "line_chart"
|
||||
case barChart = "bar_chart"
|
||||
case network = "network"
|
||||
case battery = "battery"
|
||||
}
|
||||
extension widget_t: CaseIterable {}
|
||||
|
||||
public protocol Widget_p: NSView {
|
||||
var title: String { get }
|
||||
var preview: Bool { get }
|
||||
var type: widget_t { get }
|
||||
var widthHandler: ((CGFloat) -> Void)? { get set }
|
||||
|
||||
func setValues(_ values: [value_t])
|
||||
func settings(superview: NSView)
|
||||
}
|
||||
|
||||
open class Widget: NSView, Widget_p {
|
||||
public var widthHandler: ((CGFloat) -> Void)? = nil
|
||||
public var title: String = ""
|
||||
public var preview: Bool = false
|
||||
public var type: widget_t = .unknown
|
||||
|
||||
private var widthHandlerRetry: Int8 = 0
|
||||
|
||||
open override var intrinsicContentSize: CGSize {
|
||||
return CGSize(width: self.frame.size.width, height: self.frame.size.height)
|
||||
}
|
||||
|
||||
public func setWidth(_ width: CGFloat) {
|
||||
if self.frame.width == width || self.widthHandlerRetry >= 3 {
|
||||
return
|
||||
}
|
||||
|
||||
if self.widthHandler == nil {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .microseconds(10)) {
|
||||
self.setWidth(width)
|
||||
self.widthHandlerRetry += 1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.setFrameSize(NSSize(width: width, height: self.frame.size.height))
|
||||
self.invalidateIntrinsicContentSize()
|
||||
self.display()
|
||||
}
|
||||
|
||||
self.widthHandler!(width)
|
||||
}
|
||||
|
||||
open func settings(superview: NSView) {}
|
||||
open func setValues(_ values: [value_t]) {}
|
||||
}
|
||||
|
||||
func LoadWidget(_ type: widget_t, preview: Bool, title: String, config: NSDictionary?, store: UnsafePointer<Store>?) -> Widget_p? {
|
||||
var widget: Widget_p? = nil
|
||||
let widgetConfig: NSDictionary? = config?[type.rawValue] as? NSDictionary
|
||||
|
||||
switch type {
|
||||
case .mini:
|
||||
widget = Mini(preview: preview, title: title, config: widgetConfig, store: store)
|
||||
break
|
||||
case .lineChart:
|
||||
widget = LineChart(preview: preview, title: title, config: widgetConfig, store: store)
|
||||
break
|
||||
case .barChart:
|
||||
widget = BarChart(preview: preview, title: title, config: widgetConfig, store: store)
|
||||
break
|
||||
case .network:
|
||||
widget = NetworkWidget(preview: preview, title: title, config: widgetConfig, store: store)
|
||||
break
|
||||
case .battery:
|
||||
widget = BatterykWidget(preview: preview, title: title, config: widgetConfig, store: store)
|
||||
break
|
||||
default: break
|
||||
}
|
||||
|
||||
return widget
|
||||
}
|
||||
24
Modules/Battery/Info.plist
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
36
Modules/Battery/config.plist
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Name</key>
|
||||
<string>Battery</string>
|
||||
<key>State</key>
|
||||
<true/>
|
||||
<key>Widgets</key>
|
||||
<dict>
|
||||
<key>mini</key>
|
||||
<dict>
|
||||
<key>Default</key>
|
||||
<false/>
|
||||
<key>Label</key>
|
||||
<false/>
|
||||
<key>Title</key>
|
||||
<string>BAT</string>
|
||||
<key>Preview</key>
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<false/>
|
||||
<key>Title</key>
|
||||
<string>BAT</string>
|
||||
<key>Value</key>
|
||||
<string>0.72</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>battery</key>
|
||||
<dict>
|
||||
<key>Default</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
86
Modules/Battery/main.swift
Normal file
@@ -0,0 +1,86 @@
|
||||
//
|
||||
// main.swift
|
||||
// Battery
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 06/06/2020.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import StatsKit
|
||||
import ModuleKit
|
||||
import IOKit.ps
|
||||
|
||||
struct Usage: value_t {
|
||||
var powerSource: String = ""
|
||||
var state: String = ""
|
||||
var isCharged: Bool = false
|
||||
var level: Double = 0
|
||||
var cycles: Int = 0
|
||||
var health: Int = 0
|
||||
|
||||
var amperage: Int = 0
|
||||
var voltage: Double = 0
|
||||
var temperature: Double = 0
|
||||
|
||||
var ACwatts: Int = 0
|
||||
var ACstatus: Bool = true
|
||||
|
||||
var timeToEmpty: Int = 0
|
||||
var timeToCharge: Int = 0
|
||||
|
||||
public var widget_value: Double {
|
||||
get {
|
||||
return self.level
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class Battery: Module {
|
||||
private var usageReader: UsageReader = UsageReader()
|
||||
private let popupView: Popup = Popup()
|
||||
|
||||
public init(_ store: UnsafePointer<Store>?) {
|
||||
super.init(
|
||||
store: store,
|
||||
popup: self.popupView,
|
||||
settings: nil
|
||||
)
|
||||
|
||||
self.usageReader.readyCallback = { [unowned self] in
|
||||
self.readyHandler()
|
||||
}
|
||||
self.usageReader.callbackHandler = { [unowned self] value in
|
||||
self.usageCallback(value)
|
||||
}
|
||||
|
||||
self.addReader(self.usageReader)
|
||||
}
|
||||
|
||||
public override func isAvailable() -> Bool {
|
||||
let snapshot = IOPSCopyPowerSourcesInfo().takeRetainedValue()
|
||||
let sources = IOPSCopyPowerSourcesList(snapshot).takeRetainedValue() as Array
|
||||
return sources.count > 0
|
||||
}
|
||||
|
||||
private func usageCallback(_ value: Usage?) {
|
||||
if value == nil {
|
||||
return
|
||||
}
|
||||
|
||||
self.popupView.usageCallback(value!)
|
||||
if let widget = self.widget as? Mini {
|
||||
widget.setValue(value!.level, sufix: "%")
|
||||
}
|
||||
if let widget = self.widget as? BatterykWidget {
|
||||
widget.setValue(
|
||||
percentage: value?.level ?? 0,
|
||||
isCharging: false,
|
||||
time: (value?.timeToEmpty == 0 && value?.timeToCharge != 0 ? value?.timeToCharge : value?.timeToEmpty) ?? 0
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
229
Modules/Battery/popup.swift
Normal file
@@ -0,0 +1,229 @@
|
||||
//
|
||||
// popup.swift
|
||||
// Battery
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 06/06/2020.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import ModuleKit
|
||||
import StatsKit
|
||||
|
||||
internal class Popup: NSView {
|
||||
let dashboardHeight: CGFloat = 90
|
||||
let detailsHeight: CGFloat = 88
|
||||
let batteryHeight: CGFloat = 66
|
||||
let adapterHeight: CGFloat = 44
|
||||
|
||||
private var dashboardView: NSView? = nil
|
||||
private var dashboardBatteryView: BatteryView? = nil
|
||||
private var detailsView: NSView? = nil
|
||||
private var batteryView: NSView? = nil
|
||||
private var adapterView: NSView? = nil
|
||||
|
||||
private var levelField: NSTextField? = nil
|
||||
private var sourceField: NSTextField? = nil
|
||||
private var timeLabelField: NSTextField? = nil
|
||||
private var timeField: NSTextField? = nil
|
||||
private var healthField: NSTextField? = nil
|
||||
|
||||
private var amperageField: NSTextField? = nil
|
||||
private var voltageField: NSTextField? = nil
|
||||
private var temperatureField: NSTextField? = nil
|
||||
|
||||
private var powerField: NSTextField? = nil
|
||||
private var chargingStateField: NSTextField? = nil
|
||||
|
||||
private var initialized: Bool = false
|
||||
|
||||
public init() {
|
||||
super.init(frame: NSRect(
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: Constants.Popup.width,
|
||||
height: dashboardHeight + detailsHeight + batteryHeight + adapterHeight + (Constants.Popup.separatorHeight * 3)
|
||||
))
|
||||
|
||||
self.initDashboard()
|
||||
self.initDetails()
|
||||
self.initBattery()
|
||||
self.initAdapter()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func initDashboard() {
|
||||
let view: NSView = NSView(frame: NSRect(x: 0, y: self.frame.height - self.dashboardHeight, width: self.frame.width, height: self.dashboardHeight))
|
||||
|
||||
let batteryView: BatteryView = BatteryView(frame: NSRect(x: Constants.Popup.margins, y: Constants.Popup.margins, width: view.frame.width - (Constants.Popup.margins*2), height: view.frame.height - (Constants.Popup.margins*2)))
|
||||
view.addSubview(batteryView)
|
||||
|
||||
self.addSubview(view)
|
||||
self.dashboardView = view
|
||||
self.dashboardBatteryView = batteryView
|
||||
}
|
||||
|
||||
private func initDetails() {
|
||||
let y: CGFloat = self.dashboardView!.frame.origin.y - Constants.Popup.separatorHeight
|
||||
let separator = SeparatorView("Details", origin: NSPoint(x: 0, y: y), width: self.frame.width)
|
||||
self.addSubview(separator)
|
||||
|
||||
let view: NSView = NSView(frame: NSRect(x: 0, y: separator.frame.origin.y - self.detailsHeight, width: self.frame.width, height: self.detailsHeight))
|
||||
|
||||
self.levelField = PopupRow(view, n: 3, title: "Level:", value: "")
|
||||
self.sourceField = PopupRow(view, n: 2, title: "Source:", value: "")
|
||||
let t = self.labelValue(view, n: 1, title: "Time:", value: "")
|
||||
self.timeLabelField = t.0
|
||||
self.timeField = t.1
|
||||
self.healthField = PopupRow(view, n: 0, title: "Health:", value: "")
|
||||
|
||||
self.addSubview(view)
|
||||
self.detailsView = view
|
||||
}
|
||||
|
||||
private func labelValue(_ view: NSView, n: CGFloat, title: String, value: String) -> (NSTextField, NSTextField) {
|
||||
let rowView: NSView = NSView(frame: NSRect(x: 0, y: 22*n, width: view.frame.width, height: 22))
|
||||
|
||||
let labelView: LabelField = LabelField(frame: NSRect(x: 0, y: (22-15)/2, width: view.frame.width/2, height: 15), title)
|
||||
let valueView: ValueField = ValueField(frame: NSRect(x: view.frame.width/2, y: (22-16)/2, width: view.frame.width/2, height: 16), value)
|
||||
|
||||
rowView.addSubview(labelView)
|
||||
rowView.addSubview(valueView)
|
||||
view.addSubview(rowView)
|
||||
|
||||
return (labelView, valueView)
|
||||
}
|
||||
|
||||
private func initBattery() {
|
||||
let y: CGFloat = self.detailsView!.frame.origin.y - Constants.Popup.separatorHeight
|
||||
let separator = SeparatorView("Battery", origin: NSPoint(x: 0, y: y), width: self.frame.width)
|
||||
self.addSubview(separator)
|
||||
|
||||
let view: NSView = NSView(frame: NSRect(x: 0, y: separator.frame.origin.y - self.batteryHeight, width: self.frame.width, height: self.batteryHeight))
|
||||
|
||||
self.amperageField = PopupRow(view, n: 2, title: "Amperage:", value: "")
|
||||
self.voltageField = PopupRow(view, n: 1, title: "Voltage:", value: "")
|
||||
self.temperatureField = PopupRow(view, n: 0, title: "Temperatrure:", value: "")
|
||||
|
||||
self.addSubview(view)
|
||||
self.batteryView = view
|
||||
}
|
||||
|
||||
private func initAdapter() {
|
||||
let y: CGFloat = self.batteryView!.frame.origin.y - Constants.Popup.separatorHeight
|
||||
let separator = SeparatorView("Power adapter", origin: NSPoint(x: 0, y: y), width: self.frame.width)
|
||||
self.addSubview(separator)
|
||||
|
||||
let view: NSView = NSView(frame: NSRect(x: 0, y: separator.frame.origin.y - self.adapterHeight, width: self.frame.width, height: self.adapterHeight))
|
||||
|
||||
self.powerField = PopupRow(view, n: 1, title: "Power:", value: "")
|
||||
self.chargingStateField = PopupRow(view, n: 0, title: "Is charging:", value: "")
|
||||
|
||||
self.addSubview(view)
|
||||
self.adapterView = view
|
||||
}
|
||||
|
||||
public func usageCallback(_ value: Usage) {
|
||||
DispatchQueue.main.async(execute: {
|
||||
if !self.window!.isVisible && self.initialized {
|
||||
return
|
||||
}
|
||||
|
||||
self.dashboardBatteryView?.setValue(abs(value.level))
|
||||
|
||||
self.levelField?.stringValue = "\(Int(abs(value.level) * 100)) %"
|
||||
self.sourceField?.stringValue = value.powerSource
|
||||
self.timeField?.stringValue = ""
|
||||
|
||||
if value.powerSource == "Battery Power" {
|
||||
self.timeLabelField?.stringValue = "Time to discharge:"
|
||||
if value.timeToEmpty != -1 && value.timeToEmpty != 0 {
|
||||
self.timeField?.stringValue = Double(value.timeToEmpty*60).printSecondsToHoursMinutesSeconds()
|
||||
}
|
||||
} else {
|
||||
self.timeLabelField?.stringValue = "Time to charge:"
|
||||
if value.timeToCharge != -1 && value.timeToCharge != 0 {
|
||||
self.timeField?.stringValue = Double(value.timeToCharge*60).printSecondsToHoursMinutesSeconds()
|
||||
}
|
||||
}
|
||||
|
||||
if value.timeToEmpty == -1 || value.timeToEmpty == -1 {
|
||||
self.timeField?.stringValue = "Calculating"
|
||||
}
|
||||
|
||||
if value.isCharged {
|
||||
self.timeField?.stringValue = "Fully charged"
|
||||
}
|
||||
|
||||
self.healthField?.stringValue = "\(value.health) % (\(value.state))"
|
||||
|
||||
self.amperageField?.stringValue = "\(abs(value.amperage)) mA"
|
||||
self.voltageField?.stringValue = "\(value.voltage.roundTo(decimalPlaces: 2)) V"
|
||||
self.temperatureField?.stringValue = "\(value.temperature) °C"
|
||||
|
||||
self.powerField?.stringValue = value.powerSource == "Battery Power" ? "Not connected" : "\(value.ACwatts) W"
|
||||
self.chargingStateField?.stringValue = value.level > 0 ? "Yes" : "No"
|
||||
|
||||
self.initialized = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private class BatteryView: NSView {
|
||||
private var percentage: Double = 0
|
||||
|
||||
public override init(frame: NSRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public override func draw(_ dirtyRect: NSRect) {
|
||||
super.draw(dirtyRect)
|
||||
|
||||
let w: CGFloat = 130
|
||||
let h: CGFloat = 50
|
||||
let x: CGFloat = (dirtyRect.width - w)/2
|
||||
let y: CGFloat = (dirtyRect.size.height - h) / 2
|
||||
let radius: CGFloat = 3
|
||||
let batteryFrame = NSBezierPath(roundedRect: NSRect(x: x+1, y: y, width: w, height: h), xRadius: radius, yRadius: radius)
|
||||
NSColor.black.set()
|
||||
|
||||
let bPX: CGFloat = x+w+1
|
||||
let bPY: CGFloat = (dirtyRect.size.height / 2) - 4
|
||||
let batteryPoint = NSBezierPath(roundedRect: NSRect(x: bPX, y: bPY, width: 4, height: 8), xRadius: radius, yRadius: radius)
|
||||
batteryPoint.lineWidth = 1.1
|
||||
batteryPoint.stroke()
|
||||
batteryPoint.fill()
|
||||
|
||||
batteryFrame.lineWidth = 1
|
||||
batteryFrame.stroke()
|
||||
|
||||
let maxWidth = w-2
|
||||
let inner = NSBezierPath(roundedRect: NSRect(x: x+2, y: y+1, width: maxWidth * CGFloat(self.percentage), height: h-2), xRadius: radius, yRadius: radius)
|
||||
self.percentage.batteryColor(color: true).set()
|
||||
inner.lineWidth = 0
|
||||
inner.stroke()
|
||||
inner.close()
|
||||
inner.fill()
|
||||
}
|
||||
|
||||
public func setValue(_ value: Double) {
|
||||
if self.percentage == value {
|
||||
return
|
||||
}
|
||||
|
||||
self.percentage = value
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.display()
|
||||
})
|
||||
}
|
||||
}
|
||||
135
Modules/Battery/readers.swift
Normal file
@@ -0,0 +1,135 @@
|
||||
//
|
||||
// readers.swift
|
||||
// Battery
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 06/06/2020.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import ModuleKit
|
||||
|
||||
internal class UsageReader: Reader<Usage> {
|
||||
private var service: io_connect_t = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("AppleSmartBattery"))
|
||||
|
||||
private var source: CFRunLoopSource?
|
||||
private var loop: CFRunLoop?
|
||||
|
||||
private var usage: Usage = Usage()
|
||||
|
||||
public override func start() {
|
||||
let context = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
|
||||
|
||||
self.source = IOPSNotificationCreateRunLoopSource({ (context) in
|
||||
guard let ctx = context else {
|
||||
return
|
||||
}
|
||||
|
||||
let watcher = Unmanaged<UsageReader>.fromOpaque(ctx).takeUnretainedValue()
|
||||
watcher.read()
|
||||
}, context).takeRetainedValue()
|
||||
|
||||
self.loop = RunLoop.current.getCFRunLoop()
|
||||
CFRunLoopAddSource(self.loop, source, .defaultMode)
|
||||
|
||||
self.read()
|
||||
}
|
||||
|
||||
public override func stop() {
|
||||
guard let runLoop = loop, let source = source else {
|
||||
return
|
||||
}
|
||||
|
||||
CFRunLoopRemoveSource(runLoop, source, .defaultMode)
|
||||
}
|
||||
|
||||
public override func read() {
|
||||
let psInfo = IOPSCopyPowerSourcesInfo().takeRetainedValue()
|
||||
let psList = IOPSCopyPowerSourcesList(psInfo).takeRetainedValue() as [CFTypeRef]
|
||||
|
||||
if psList.count == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for ps in psList {
|
||||
if let list = IOPSGetPowerSourceDescription(psInfo, ps).takeUnretainedValue() as? Dictionary<String, Any> {
|
||||
self.usage.powerSource = list[kIOPSPowerSourceStateKey] as? String ?? "AC Power"
|
||||
self.usage.state = list[kIOPSBatteryHealthKey] as! String
|
||||
self.usage.isCharged = list[kIOPSIsChargedKey] as? Bool ?? false
|
||||
var cap = Double(list[kIOPSCurrentCapacityKey] as! Int) / 100
|
||||
|
||||
self.usage.timeToEmpty = Int(list[kIOPSTimeToEmptyKey] as! Int)
|
||||
self.usage.timeToCharge = Int(list[kIOPSTimeToFullChargeKey] as! Int)
|
||||
|
||||
self.usage.cycles = self.getIntValue("CycleCount" as CFString) ?? 0
|
||||
|
||||
let maxCapacity = self.getIntValue("MaxCapacity" as CFString) ?? 1
|
||||
let designCapacity = self.getIntValue("DesignCapacity" as CFString) ?? 1
|
||||
self.usage.health = (100 * maxCapacity) / designCapacity
|
||||
|
||||
self.usage.amperage = self.getIntValue("Amperage" as CFString) ?? 0
|
||||
self.usage.voltage = self.getVoltage() ?? 0
|
||||
self.usage.temperature = self.getTemperature() ?? 0
|
||||
|
||||
var ACwatts: Int = 0
|
||||
if let ACDetails = IOPSCopyExternalPowerAdapterDetails() {
|
||||
if let ACList = ACDetails.takeUnretainedValue() as? Dictionary<String, Any> {
|
||||
guard let watts = ACList[kIOPSPowerAdapterWattsKey] else {
|
||||
return
|
||||
}
|
||||
ACwatts = Int(watts as! Int)
|
||||
}
|
||||
}
|
||||
self.usage.ACwatts = ACwatts
|
||||
self.usage.ACstatus = self.getBoolValue("IsCharging" as CFString) ?? false
|
||||
|
||||
if self.usage.powerSource == "Battery Power" {
|
||||
cap = 0 - cap
|
||||
}
|
||||
self.usage.level = cap
|
||||
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.callback(self.usage)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func getBoolValue(_ forIdentifier: CFString) -> Bool? {
|
||||
if let value = IORegistryEntryCreateCFProperty(self.service, forIdentifier, kCFAllocatorDefault, 0) {
|
||||
return value.takeRetainedValue() as? Bool
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func getIntValue(_ identifier: CFString) -> Int? {
|
||||
if let value = IORegistryEntryCreateCFProperty(self.service, identifier, kCFAllocatorDefault, 0) {
|
||||
return value.takeRetainedValue() as? Int
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func getDoubleValue(_ identifier: CFString) -> Double? {
|
||||
if let value = IORegistryEntryCreateCFProperty(self.service, identifier, kCFAllocatorDefault, 0) {
|
||||
return value.takeRetainedValue() as? Double
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func getVoltage() -> Double? {
|
||||
if let value = self.getDoubleValue("Voltage" as CFString) {
|
||||
return value / 1000.0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func getTemperature() -> Double? {
|
||||
if let value = IORegistryEntryCreateCFProperty(self.service, "Temperature" as CFString, kCFAllocatorDefault, 0) {
|
||||
return value.takeRetainedValue() as! Double / 100.0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
24
Modules/CPU/Info.plist
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
38
Modules/CPU/config.plist
Normal file
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Name</key>
|
||||
<string>CPU</string>
|
||||
<key>State</key>
|
||||
<true/>
|
||||
<key>Widgets</key>
|
||||
<dict>
|
||||
<key>mini</key>
|
||||
<dict>
|
||||
<key>Default</key>
|
||||
<true/>
|
||||
<key>Preview</key>
|
||||
<dict>
|
||||
<key>Value</key>
|
||||
<string>0.08</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>line_chart</key>
|
||||
<dict>
|
||||
<key>Default</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<key>bar_chart</key>
|
||||
<dict>
|
||||
<key>Default</key>
|
||||
<false/>
|
||||
<key>Preview</key>
|
||||
<dict>
|
||||
<key>Value</key>
|
||||
<string>0.36,0.28</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
80
Modules/CPU/main.swift
Normal file
@@ -0,0 +1,80 @@
|
||||
//
|
||||
// main.swift
|
||||
// CPU
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 09/04/2020.
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import ModuleKit
|
||||
import StatsKit
|
||||
|
||||
public struct Load: value_t {
|
||||
var totalUsage: Double = 0
|
||||
var usagePerCore: [Double] = []
|
||||
|
||||
var systemLoad: Double = 0
|
||||
var userLoad: Double = 0
|
||||
var idleLoad: Double = 0
|
||||
|
||||
public var widget_value: Double {
|
||||
get {
|
||||
return self.totalUsage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct TopProcess {
|
||||
var pid: Int = 0
|
||||
var command: String = ""
|
||||
var usage: Double = 0
|
||||
}
|
||||
|
||||
public class CPU: Module {
|
||||
private let popupView: Popup = Popup()
|
||||
private var settingsView: Settings
|
||||
|
||||
private var loadReader: LoadReader = LoadReader()
|
||||
private let smc: UnsafePointer<SMCService>?
|
||||
|
||||
public init(_ store: UnsafePointer<Store>, _ smc: UnsafePointer<SMCService>) {
|
||||
self.smc = smc
|
||||
self.settingsView = Settings("CPU", store: store)
|
||||
self.loadReader.store = store
|
||||
|
||||
super.init(
|
||||
store: store,
|
||||
popup: self.popupView,
|
||||
settings: self.settingsView
|
||||
)
|
||||
|
||||
self.loadReader.readyCallback = { [unowned self] in
|
||||
self.readyHandler()
|
||||
}
|
||||
self.loadReader.callbackHandler = { [unowned self] value in
|
||||
self.loadCallback(value)
|
||||
}
|
||||
|
||||
self.addReader(self.loadReader)
|
||||
}
|
||||
|
||||
private func loadCallback(_ value: Load?) {
|
||||
if value == nil {
|
||||
return
|
||||
}
|
||||
|
||||
let temperature = self.smc?.pointee.getValue("TC0F") ?? self.smc?.pointee.getValue("TC0P") ?? self.smc?.pointee.getValue("TC0H")
|
||||
self.popupView.loadCallback(value!, tempValue: temperature)
|
||||
|
||||
if let widget = self.widget as? Mini {
|
||||
widget.setValue(value!.totalUsage, sufix: "%")
|
||||
}
|
||||
if let widget = self.widget as? LineChart {
|
||||
widget.setValue(value!.totalUsage)
|
||||
}
|
||||
if let widget = self.widget as? BarChart {
|
||||
widget.setValue(value!.usagePerCore)
|
||||
}
|
||||
}
|
||||
}
|
||||
169
Modules/CPU/popup.swift
Normal file
@@ -0,0 +1,169 @@
|
||||
//
|
||||
// popup.swift
|
||||
// CPU
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 15/04/2020.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import ModuleKit
|
||||
import StatsKit
|
||||
|
||||
internal class Popup: NSView {
|
||||
private let dashboardHeight: CGFloat = 90
|
||||
private let detailsHeight: CGFloat = 66 // -26
|
||||
|
||||
private var loadField: NSTextField? = nil
|
||||
private var temperatureField: NSTextField? = nil
|
||||
|
||||
private var systemField: NSTextField? = nil
|
||||
private var userField: NSTextField? = nil
|
||||
private var idleField: NSTextField? = nil
|
||||
|
||||
public var chart: LineChartView? = nil
|
||||
private var ready: Bool = false
|
||||
|
||||
public init() {
|
||||
super.init(frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: dashboardHeight + Constants.Popup.separatorHeight + detailsHeight))
|
||||
|
||||
initDashboard()
|
||||
initDetails()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public override func updateLayer() {
|
||||
self.chart?.display()
|
||||
}
|
||||
|
||||
private func initDashboard() {
|
||||
let rightWidth: CGFloat = 110
|
||||
let view: NSView = NSView(frame: NSRect(x: 0, y: self.frame.height - self.dashboardHeight, width: self.frame.width, height: self.dashboardHeight))
|
||||
|
||||
let leftPanel = NSView(frame: NSRect(x: 0, y: 0, width: view.frame.width - rightWidth - Constants.Popup.margins, height: view.frame.height))
|
||||
|
||||
self.chart = LineChartView(frame: NSRect(x: 4, y: 3, width: leftPanel.frame.width, height: leftPanel.frame.height), num: 120)
|
||||
leftPanel.addSubview(self.chart!)
|
||||
|
||||
let rightPanel: NSView = NSView(frame: NSRect(x: view.frame.width - rightWidth, y: 0, width: rightWidth, height: view.frame.height))
|
||||
self.loadField = addFirstRow(mView: rightPanel, y: ((rightPanel.frame.height - 16)/2)+9, title: "Load:", value: "")
|
||||
self.temperatureField = addFirstRow(mView: rightPanel, y: ((rightPanel.frame.height - 16)/2)-9, title: "Temperature:", value: "")
|
||||
|
||||
view.addSubview(leftPanel)
|
||||
view.addSubview(rightPanel)
|
||||
self.addSubview(view)
|
||||
}
|
||||
|
||||
private func initDetails() {
|
||||
let y: CGFloat = self.frame.height - self.dashboardHeight - Constants.Popup.separatorHeight
|
||||
let separator = SeparatorView("Details", origin: NSPoint(x: 0, y: y), width: self.frame.width)
|
||||
self.addSubview(separator)
|
||||
|
||||
let view: NSView = NSView(frame: NSRect(x: 0, y: separator.frame.origin.y - self.detailsHeight, width: self.frame.width, height: self.detailsHeight))
|
||||
|
||||
self.systemField = PopupRow(view, n: 2, title: "System:", value: "")
|
||||
self.userField = PopupRow(view, n: 1, title: "User:", value: "")
|
||||
self.idleField = PopupRow(view, n: 0, title: "Idle:", value: "")
|
||||
|
||||
self.addSubview(view)
|
||||
}
|
||||
|
||||
private func addFirstRow(mView: NSView, y: CGFloat, title: String, value: String) -> NSTextField {
|
||||
let rowView: NSView = NSView(frame: NSRect(x: 0, y: y, width: mView.frame.width, height: 16))
|
||||
|
||||
let labelWidth = title.widthOfString(usingFont: .systemFont(ofSize: 10, weight: .light)) + 4
|
||||
let labelView: NSTextField = TextView(frame: NSRect(x: 0, y: 1.5, width: labelWidth, height: 13))
|
||||
labelView.stringValue = title
|
||||
labelView.alignment = .natural
|
||||
labelView.font = NSFont.systemFont(ofSize: 10, weight: .light)
|
||||
|
||||
let valueView: NSTextField = TextView(frame: NSRect(x: labelWidth, y: 1, width: mView.frame.width - labelWidth, height: 14))
|
||||
valueView.stringValue = value
|
||||
valueView.alignment = .right
|
||||
valueView.font = NSFont.systemFont(ofSize: 11, weight: .medium)
|
||||
|
||||
rowView.addSubview(labelView)
|
||||
rowView.addSubview(valueView)
|
||||
mView.addSubview(rowView)
|
||||
|
||||
return valueView
|
||||
}
|
||||
|
||||
public func loadCallback(_ value: Load, tempValue: Double?) {
|
||||
var temperature: String = "Unknown"
|
||||
|
||||
DispatchQueue.main.async(execute: {
|
||||
if self.window!.isVisible || !self.ready {
|
||||
if tempValue != nil {
|
||||
let formatter = MeasurementFormatter()
|
||||
let measurement = Measurement(value: tempValue!.rounded(toPlaces: 0), unit: UnitTemperature.celsius)
|
||||
temperature = formatter.string(from: measurement)
|
||||
}
|
||||
|
||||
self.temperatureField?.stringValue = temperature
|
||||
|
||||
self.systemField?.stringValue = "\(Int(value.systemLoad.rounded(toPlaces: 2) * 100)) %"
|
||||
self.userField?.stringValue = "\(Int(value.userLoad.rounded(toPlaces: 2) * 100)) %"
|
||||
self.idleField?.stringValue = "\(Int(value.idleLoad.rounded(toPlaces: 2) * 100)) %"
|
||||
|
||||
let v = Int(value.totalUsage.rounded(toPlaces: 2) * 100)
|
||||
self.loadField?.stringValue = "\(v) %"
|
||||
self.ready = true
|
||||
}
|
||||
|
||||
self.chart?.addValue(value.totalUsage)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private class ProcessView: NSView {
|
||||
public var width: CGFloat {
|
||||
get { return 0 }
|
||||
set {
|
||||
self.setFrameSize(NSSize(width: newValue, height: self.frame.height))
|
||||
}
|
||||
}
|
||||
|
||||
public var label: String {
|
||||
get { return "" }
|
||||
set {
|
||||
self.labelView?.stringValue = newValue
|
||||
}
|
||||
}
|
||||
public var value: String {
|
||||
get { return "" }
|
||||
set {
|
||||
self.valueView?.stringValue = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var labelView: LabelField? = nil
|
||||
private var valueView: ValueField? = nil
|
||||
|
||||
init(_ n: CGFloat) {
|
||||
super.init(frame: NSRect(x: 0, y: n*22, width: Constants.Popup.width, height: 16))
|
||||
|
||||
let rowView: NSView = NSView(frame: NSRect(x: Constants.Popup.margins, y: 0, width: self.frame.width - (Constants.Popup.margins*2), height: 16))
|
||||
|
||||
let labelView: LabelField = LabelField(frame: NSRect(x: 0, y: 0.5, width: 50, height: 15), "")
|
||||
let valueView: ValueField = ValueField(frame: NSRect(x: 50, y: 0, width: rowView.frame.width - 50, height: 16), "")
|
||||
|
||||
rowView.addSubview(labelView)
|
||||
rowView.addSubview(valueView)
|
||||
|
||||
self.labelView = labelView
|
||||
self.valueView = valueView
|
||||
|
||||
self.addSubview(rowView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
152
Modules/CPU/readers.swift
Normal file
@@ -0,0 +1,152 @@
|
||||
//
|
||||
// readers.swift
|
||||
// CPU
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 10/04/2020.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import StatsKit
|
||||
import ModuleKit
|
||||
|
||||
internal class LoadReader: Reader<Load> {
|
||||
public var store: UnsafePointer<Store>? = nil
|
||||
|
||||
private var cpuInfo: processor_info_array_t!
|
||||
private var prevCpuInfo: processor_info_array_t?
|
||||
private var numCpuInfo: mach_msg_type_number_t = 0
|
||||
private var numPrevCpuInfo: mach_msg_type_number_t = 0
|
||||
private var numCPUs: uint = 0
|
||||
private let CPUUsageLock: NSLock = NSLock()
|
||||
private var previousInfo = host_cpu_load_info()
|
||||
|
||||
private var response: Load = Load()
|
||||
private var numCPUsU: natural_t = 0
|
||||
private var usagePerCore: [Double] = []
|
||||
|
||||
public override func setup() {
|
||||
self.interval = 1500
|
||||
|
||||
[CTL_HW, HW_NCPU].withUnsafeBufferPointer() { mib in
|
||||
var sizeOfNumCPUs: size_t = MemoryLayout<uint>.size
|
||||
let status = sysctl(processor_info_array_t(mutating: mib.baseAddress), 2, &numCPUs, &sizeOfNumCPUs, nil, 0)
|
||||
if status != 0 {
|
||||
self.numCPUs = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override func read() {
|
||||
let result: kern_return_t = host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO, &self.numCPUsU, &self.cpuInfo, &self.numCpuInfo)
|
||||
if result == KERN_SUCCESS {
|
||||
self.CPUUsageLock.lock()
|
||||
self.usagePerCore = []
|
||||
|
||||
for i in 0 ..< Int32(numCPUs) {
|
||||
var inUse: Int32
|
||||
var total: Int32
|
||||
if let prevCpuInfo = self.prevCpuInfo {
|
||||
inUse = self.cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_USER)]
|
||||
- prevCpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_USER)]
|
||||
+ self.cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_SYSTEM)]
|
||||
- prevCpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_SYSTEM)]
|
||||
+ self.cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_NICE)]
|
||||
- prevCpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_NICE)]
|
||||
total = inUse + (self.cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_IDLE)]
|
||||
- prevCpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_IDLE)])
|
||||
} else {
|
||||
inUse = self.cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_USER)]
|
||||
+ self.cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_SYSTEM)]
|
||||
+ self.cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_NICE)]
|
||||
total = inUse + self.cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_IDLE)]
|
||||
}
|
||||
|
||||
if total != 0 {
|
||||
self.usagePerCore.append(Double(inUse) / Double(total))
|
||||
}
|
||||
}
|
||||
self.CPUUsageLock.unlock()
|
||||
|
||||
if self.store?.pointee.bool(key: "CPU_hyperhreading", defaultValue: false) ?? false {
|
||||
self.response.usagePerCore = self.usagePerCore
|
||||
} else {
|
||||
var i = 0
|
||||
var a = 0
|
||||
|
||||
self.response.usagePerCore = []
|
||||
while i < Int(self.usagePerCore.count/2) {
|
||||
a = i*2
|
||||
if self.usagePerCore.indices.contains(a) && self.usagePerCore.indices.contains(a+1) {
|
||||
self.response.usagePerCore.append((Double(self.usagePerCore[a]) + Double(self.usagePerCore[a+1])) / 2)
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
|
||||
if let prevCpuInfo = self.prevCpuInfo {
|
||||
let prevCpuInfoSize: size_t = MemoryLayout<integer_t>.stride * Int(self.numPrevCpuInfo)
|
||||
vm_deallocate(mach_task_self_, vm_address_t(bitPattern: prevCpuInfo), vm_size_t(prevCpuInfoSize))
|
||||
}
|
||||
|
||||
self.prevCpuInfo = self.cpuInfo
|
||||
self.numPrevCpuInfo = self.numCpuInfo
|
||||
|
||||
self.cpuInfo = nil
|
||||
self.numCpuInfo = 0
|
||||
} else {
|
||||
print("ERROR host_processor_info(): " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
|
||||
}
|
||||
|
||||
let cpuInfo = hostCPULoadInfo()
|
||||
if cpuInfo == nil {
|
||||
self.callback(nil)
|
||||
return
|
||||
}
|
||||
|
||||
let userDiff = Double(cpuInfo!.cpu_ticks.0 - self.previousInfo.cpu_ticks.0)
|
||||
let sysDiff = Double(cpuInfo!.cpu_ticks.1 - self.previousInfo.cpu_ticks.1)
|
||||
let idleDiff = Double(cpuInfo!.cpu_ticks.2 - self.previousInfo.cpu_ticks.2)
|
||||
let niceDiff = Double(cpuInfo!.cpu_ticks.3 - self.previousInfo.cpu_ticks.3)
|
||||
let totalTicks = sysDiff + userDiff + niceDiff + idleDiff
|
||||
|
||||
let system = sysDiff / totalTicks
|
||||
let user = userDiff / totalTicks
|
||||
let idle = idleDiff / totalTicks
|
||||
|
||||
if !system.isNaN {
|
||||
self.response.systemLoad = system
|
||||
}
|
||||
if !user.isNaN {
|
||||
self.response.userLoad = user
|
||||
}
|
||||
if !idle.isNaN {
|
||||
self.response.idleLoad = idle
|
||||
}
|
||||
self.previousInfo = cpuInfo!
|
||||
self.response.totalUsage = self.response.systemLoad + self.response.userLoad
|
||||
|
||||
self.callback(self.response)
|
||||
}
|
||||
|
||||
private func hostCPULoadInfo() -> host_cpu_load_info? {
|
||||
let HOST_CPU_LOAD_INFO_COUNT = MemoryLayout<host_cpu_load_info>.stride/MemoryLayout<integer_t>.stride
|
||||
var size = mach_msg_type_number_t(HOST_CPU_LOAD_INFO_COUNT)
|
||||
var cpuLoadInfo = host_cpu_load_info()
|
||||
|
||||
let result: kern_return_t = withUnsafeMutablePointer(to: &cpuLoadInfo) {
|
||||
$0.withMemoryRebound(to: integer_t.self, capacity: HOST_CPU_LOAD_INFO_COUNT) {
|
||||
host_statistics(mach_host_self(), HOST_CPU_LOAD_INFO, $0, &size)
|
||||
}
|
||||
}
|
||||
if result != KERN_SUCCESS {
|
||||
print("Error - \(#file): \(#function) - kern_result_t = \(result)")
|
||||
return nil
|
||||
}
|
||||
|
||||
return cpuLoadInfo
|
||||
}
|
||||
}
|
||||
71
Modules/CPU/settings.swift
Normal file
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// Settings.swift
|
||||
// CPU
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 18/04/2020.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import StatsKit
|
||||
import ModuleKit
|
||||
|
||||
internal class Settings: NSView, Settings_v {
|
||||
private var hyperthreadState: Bool = false
|
||||
|
||||
private let title: String
|
||||
private let store: UnsafePointer<Store>?
|
||||
|
||||
public init(_ title: String, store: UnsafePointer<Store>?) {
|
||||
self.title = title
|
||||
self.store = store
|
||||
super.init(frame: CGRect(x: Constants.Settings.margin, y: Constants.Settings.margin, width: 0, height: 0))
|
||||
self.wantsLayer = true
|
||||
self.canDrawConcurrently = true
|
||||
|
||||
if self.store != nil {
|
||||
self.hyperthreadState = store!.pointee.bool(key: "\(self.title)_hyperhreading", defaultValue: self.hyperthreadState)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public func load(rect: NSRect, widget: widget_t) {
|
||||
self.subviews.forEach{ $0.removeFromSuperview() }
|
||||
|
||||
let rowHeight: CGFloat = 30
|
||||
var height: CGFloat = 0
|
||||
|
||||
if widget == .barChart {
|
||||
self.addSubview(ToggleTitleRow(
|
||||
frame: NSRect(x: 0, y: (rowHeight + Constants.Settings.margin) * 0, width: rect.width - (Constants.Settings.margin*2), height: rowHeight),
|
||||
title: "Show hyper-threading cores",
|
||||
action: #selector(toggleMultithreading),
|
||||
state: self.hyperthreadState
|
||||
))
|
||||
height += rowHeight
|
||||
}
|
||||
|
||||
if height != 0 {
|
||||
height += (Constants.Settings.margin*2)
|
||||
}
|
||||
self.setFrameSize(NSSize(width: rect.width - (Constants.Settings.margin*2), height: height))
|
||||
}
|
||||
|
||||
@objc func toggleMultithreading(_ sender: NSControl) {
|
||||
var state: NSControl.StateValue? = nil
|
||||
if #available(OSX 10.15, *) {
|
||||
state = sender is NSSwitch ? (sender as! NSSwitch).state: nil
|
||||
} else {
|
||||
state = sender is NSButton ? (sender as! NSButton).state: nil
|
||||
}
|
||||
|
||||
self.hyperthreadState = state! == .on ? true : false
|
||||
self.store?.pointee.set(key: "\(self.title)_hyperhreading", value: self.hyperthreadState)
|
||||
}
|
||||
}
|
||||
24
Modules/Disk/Info.plist
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
51
Modules/Disk/config.plist
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Name</key>
|
||||
<string>Disk</string>
|
||||
<key>State</key>
|
||||
<true/>
|
||||
<key>Widgets</key>
|
||||
<dict>
|
||||
<key>mini</key>
|
||||
<dict>
|
||||
<key>Default</key>
|
||||
<true/>
|
||||
<key>Title</key>
|
||||
<string>SSD</string>
|
||||
<key>Preview</key>
|
||||
<dict>
|
||||
<key>Title</key>
|
||||
<string>SSD</string>
|
||||
<key>Value</key>
|
||||
<string>0.36</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>bar_chart</key>
|
||||
<dict>
|
||||
<key>Default</key>
|
||||
<false/>
|
||||
<key>Title</key>
|
||||
<string>SSD</string>
|
||||
<key>Label</key>
|
||||
<true/>
|
||||
<key>Box</key>
|
||||
<true/>
|
||||
<key>Color</key>
|
||||
<true/>
|
||||
<key>Preview</key>
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<true/>
|
||||
<key>Box</key>
|
||||
<true/>
|
||||
<key>Color</key>
|
||||
<true/>
|
||||
<key>Value</key>
|
||||
<string>0.36</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
123
Modules/Disk/main.swift
Normal file
@@ -0,0 +1,123 @@
|
||||
//
|
||||
// main.swift
|
||||
// Disk
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 07/05/2020.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import StatsKit
|
||||
import ModuleKit
|
||||
|
||||
struct diskInfo {
|
||||
var name: String = ""
|
||||
var model: String = ""
|
||||
var path: URL?
|
||||
var connection: String = ""
|
||||
var fileSystem: String = ""
|
||||
|
||||
var totalSize: Int64 = 0
|
||||
var freeSize: Int64 = 0
|
||||
|
||||
var mediaBSDName: String = ""
|
||||
var root: Bool = false
|
||||
}
|
||||
|
||||
struct DiskList: value_t {
|
||||
var list: [diskInfo] = []
|
||||
|
||||
public var widget_value: Double {
|
||||
get {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func getDiskByBSDName(_ name: String) -> diskInfo? {
|
||||
if let idx = self.list.firstIndex(where: { $0.mediaBSDName == name }) {
|
||||
return self.list[idx]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getDiskByName(_ name: String) -> diskInfo? {
|
||||
if let idx = self.list.firstIndex(where: { $0.name == name }) {
|
||||
return self.list[idx]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getRootDisk() -> diskInfo? {
|
||||
if let idx = self.list.firstIndex(where: { $0.root }) {
|
||||
return self.list[idx]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public class Disk: Module {
|
||||
private let popupView: Popup = Popup()
|
||||
private var capacityReader: CapacityReader = CapacityReader()
|
||||
private var settingsView: Settings
|
||||
private var selectedDisk: String = ""
|
||||
|
||||
public init(_ store: UnsafePointer<Store>?) {
|
||||
self.settingsView = Settings("Disk", store: store!)
|
||||
|
||||
super.init(
|
||||
store: store,
|
||||
popup: self.popupView,
|
||||
settings: self.settingsView
|
||||
)
|
||||
self.selectedDisk = store!.pointee.string(key: "\(self.config.name)_disk", defaultValue: self.selectedDisk)
|
||||
|
||||
self.capacityReader.readyCallback = { [unowned self] in
|
||||
self.readyHandler()
|
||||
}
|
||||
self.capacityReader.callbackHandler = { [unowned self] value in
|
||||
self.capacityCallback(value: value)
|
||||
}
|
||||
|
||||
self.settingsView.selectedDiskHandler = { [unowned self] value in
|
||||
self.selectedDisk = value
|
||||
self.capacityReader.read()
|
||||
}
|
||||
|
||||
self.addReader(self.capacityReader)
|
||||
}
|
||||
|
||||
private func capacityCallback(value: DiskList?) {
|
||||
if value == nil {
|
||||
return
|
||||
}
|
||||
self.popupView.usageCallback(value!)
|
||||
self.settingsView.setList(value!)
|
||||
|
||||
var d: diskInfo? = value!.getDiskByName(self.selectedDisk)
|
||||
if d == nil {
|
||||
d = value!.getRootDisk()
|
||||
}
|
||||
|
||||
if d == nil {
|
||||
return
|
||||
}
|
||||
|
||||
let total = d!.totalSize
|
||||
let free = d!.freeSize
|
||||
let usedSpace = total - free
|
||||
let percentage = Double(usedSpace) / Double(total)
|
||||
|
||||
if let widget = self.widget as? Mini {
|
||||
widget.setValue(percentage, sufix: "%")
|
||||
}
|
||||
if let widget = self.widget as? BarChart {
|
||||
widget.setValue([percentage])
|
||||
}
|
||||
}
|
||||
}
|
||||
176
Modules/Disk/popup.swift
Normal file
@@ -0,0 +1,176 @@
|
||||
//
|
||||
// popup.swift
|
||||
// Disk
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 11/05/2020.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import ModuleKit
|
||||
import StatsKit
|
||||
|
||||
internal class Popup: NSView {
|
||||
let diskFullHeight: CGFloat = 60
|
||||
var list: [String: DiskView] = [:]
|
||||
|
||||
public init() {
|
||||
super.init(frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 0))
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
internal func usageCallback(_ value: DiskList) {
|
||||
value.list.reversed().forEach { (d: diskInfo) in
|
||||
if self.list[d.name] == nil {
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.list[d.name] = DiskView(
|
||||
NSRect(x: 0, y: (self.diskFullHeight + Constants.Popup.margins) * CGFloat(self.list.count), width: self.frame.width, height: self.diskFullHeight),
|
||||
name: d.name,
|
||||
size: d.totalSize,
|
||||
free: d.freeSize,
|
||||
path: d.path
|
||||
)
|
||||
self.addSubview(self.list[d.name]!)
|
||||
|
||||
self.setFrameSize(NSSize(width: self.frame.width, height: ((self.diskFullHeight + Constants.Popup.margins) * CGFloat(self.list.count)) - Constants.Popup.margins))
|
||||
})
|
||||
} else {
|
||||
self.list[d.name]?.update(free: d.freeSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class DiskView: NSView {
|
||||
public let name: String
|
||||
public let size: Int64
|
||||
private let uri: URL?
|
||||
|
||||
private let nameHeight: CGFloat = 20
|
||||
private let legendHeight: CGFloat = 12
|
||||
private let barHeight: CGFloat = 10
|
||||
|
||||
private var legendField: NSTextField? = nil
|
||||
private var percentageField: NSTextField? = nil
|
||||
private var usedBarSpace: NSView? = nil
|
||||
|
||||
private var mainView: NSView
|
||||
|
||||
private var initialized: Bool = false
|
||||
|
||||
public init(_ frame: NSRect, name: String, size: Int64, free: Int64, path: URL?) {
|
||||
self.mainView = NSView(frame: NSRect(x: 5, y: 5, width: frame.width - 10, height: frame.height - 10))
|
||||
self.name = name
|
||||
self.size = size
|
||||
self.uri = path
|
||||
super.init(frame: frame)
|
||||
|
||||
self.wantsLayer = true
|
||||
self.layer?.cornerRadius = 2
|
||||
|
||||
self.addName()
|
||||
self.addHorizontalBar(free: free)
|
||||
self.addLegend(free: free)
|
||||
|
||||
self.addSubview(self.mainView)
|
||||
|
||||
let rect: CGRect = CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height)
|
||||
let trackingArea = NSTrackingArea(rect: rect, options: [NSTrackingArea.Options.activeAlways, NSTrackingArea.Options.mouseEnteredAndExited, NSTrackingArea.Options.activeInActiveApp], owner: self, userInfo: nil)
|
||||
self.addTrackingArea(trackingArea)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func updateLayer() {
|
||||
self.layer?.backgroundColor = isDarkMode ? NSColor(hexString: "#111111", alpha: 0.25).cgColor : NSColor(hexString: "#f5f5f5", alpha: 1).cgColor
|
||||
}
|
||||
|
||||
private func addName() {
|
||||
let nameWidth = self.name.widthOfString(usingFont: .systemFont(ofSize: 13, weight: .light)) + 4
|
||||
let view: NSView = NSView(frame: NSRect(x: 0, y: self.mainView.frame.height - nameHeight, width: nameWidth, height: nameHeight))
|
||||
|
||||
let nameField: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: nameWidth, height: view.frame.height))
|
||||
nameField.stringValue = self.name
|
||||
|
||||
view.addSubview(nameField)
|
||||
self.mainView.addSubview(view)
|
||||
}
|
||||
|
||||
private func addLegend(free: Int64) {
|
||||
let view: NSView = NSView(frame: NSRect(x: 0, y: 2, width: self.mainView.frame.width, height: self.legendHeight))
|
||||
|
||||
self.legendField = TextView(frame: NSRect(x: 0, y: 0, width: view.frame.width - 40, height: view.frame.height))
|
||||
self.legendField?.font = NSFont.systemFont(ofSize: 11, weight: .light)
|
||||
self.legendField?.stringValue = "Used \(Units(bytes: (self.size - free)).getReadableMemory()) from \(Units(bytes: self.size).getReadableMemory())"
|
||||
|
||||
self.percentageField = TextView(frame: NSRect(x: view.frame.width - 40, y: 0, width: 40, height: view.frame.height))
|
||||
self.percentageField?.font = NSFont.systemFont(ofSize: 11, weight: .regular)
|
||||
self.percentageField?.alignment = .right
|
||||
self.percentageField?.stringValue = "\(Int8((Double(self.size - free) / Double(self.size)) * 100))%"
|
||||
|
||||
view.addSubview(self.legendField!)
|
||||
view.addSubview(self.percentageField!)
|
||||
self.mainView.addSubview(view)
|
||||
}
|
||||
|
||||
private func addHorizontalBar(free: Int64) {
|
||||
let view: NSView = NSView(frame: NSRect(x: 1, y: self.mainView.frame.height - self.nameHeight - 11, width: self.mainView.frame.width - 2, height: self.barHeight))
|
||||
view.wantsLayer = true
|
||||
view.layer?.backgroundColor = NSColor.white.cgColor
|
||||
view.layer?.borderColor = NSColor.secondaryLabelColor.cgColor
|
||||
view.layer?.borderWidth = 0.25
|
||||
view.layer?.cornerRadius = 3
|
||||
|
||||
let percentage = CGFloat(self.size - free) / CGFloat(self.size)
|
||||
let width: CGFloat = (view.frame.width * percentage) / 1
|
||||
self.usedBarSpace = NSView(frame: NSRect(x: 0, y: 0, width: width, height: view.frame.height))
|
||||
self.usedBarSpace?.wantsLayer = true
|
||||
self.usedBarSpace?.layer?.backgroundColor = NSColor(red: (26/255.0), green: (126/255.0), blue: (252/255.0), alpha: 1).cgColor
|
||||
|
||||
view.addSubview(self.usedBarSpace!)
|
||||
self.mainView.addSubview(view)
|
||||
}
|
||||
|
||||
public func update(free: Int64) {
|
||||
DispatchQueue.main.async(execute: {
|
||||
if !self.window!.isVisible && self.initialized {
|
||||
return
|
||||
}
|
||||
|
||||
if self.legendField != nil {
|
||||
self.legendField?.stringValue = "Used \(Units(bytes: (self.size - free)).getReadableMemory()) from \(Units(bytes: self.size).getReadableMemory())"
|
||||
self.percentageField?.stringValue = "\(Int8((Double(self.size - free) / Double(self.size)) * 100))%"
|
||||
}
|
||||
|
||||
if self.usedBarSpace != nil {
|
||||
let percentage = CGFloat(self.size - free) / CGFloat(self.size)
|
||||
let width: CGFloat = ((self.mainView.frame.width - 2) * percentage) / 1
|
||||
self.usedBarSpace?.setFrameSize(NSSize(width: width, height: self.usedBarSpace!.frame.height))
|
||||
}
|
||||
|
||||
self.initialized = true
|
||||
})
|
||||
}
|
||||
|
||||
override func mouseEntered(with: NSEvent) {
|
||||
NSCursor.pointingHand.set()
|
||||
}
|
||||
|
||||
override func mouseExited(with: NSEvent) {
|
||||
NSCursor.arrow.set()
|
||||
}
|
||||
|
||||
override func mouseDown(with: NSEvent) {
|
||||
if let uri = self.uri {
|
||||
NSWorkspace.shared.openFile(uri.absoluteString, withApplication: "Finder")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,81 +1,25 @@
|
||||
//
|
||||
// DiskCapacityReader.swift
|
||||
// Stats
|
||||
// readers.swift
|
||||
// Disk
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 07/05/2020.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 14/01/2020.
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import ModuleKit
|
||||
|
||||
struct diskInfo {
|
||||
var ID: String = "";
|
||||
internal class CapacityReader: Reader<DiskList> {
|
||||
private var disks: DiskList = DiskList()
|
||||
|
||||
var name: String = "";
|
||||
var model: String = "";
|
||||
var path: URL?;
|
||||
var connection: String = "";
|
||||
var fileSystem: String = "";
|
||||
|
||||
var totalSize: Int64 = 0;
|
||||
var freeSize: Int64 = 0;
|
||||
|
||||
var mediaBSDName: String = "";
|
||||
var root: Bool = false;
|
||||
public override func setup() {
|
||||
self.interval = 10000
|
||||
}
|
||||
|
||||
struct disksList {
|
||||
var list: [diskInfo] = []
|
||||
|
||||
func getDiskByBSDName(_ name: String) -> diskInfo? {
|
||||
if let idx = self.list.firstIndex(where: { $0.mediaBSDName == name }) {
|
||||
return self.list[idx]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getDiskByName(_ name: String) -> diskInfo? {
|
||||
if let idx = self.list.firstIndex(where: { $0.name == name }) {
|
||||
return self.list[idx]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getRootDisk() -> diskInfo? {
|
||||
if let idx = self.list.firstIndex(where: { $0.root }) {
|
||||
return self.list[idx]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
class DiskCapacityReader: Reader {
|
||||
public var name: String = "Capacity"
|
||||
public var enabled: Bool = true
|
||||
public var available: Bool = true
|
||||
public var optional: Bool = false
|
||||
public var initialized: Bool = false
|
||||
public var callback: (disksList) -> Void = {_ in}
|
||||
|
||||
private var disks: disksList = disksList()
|
||||
|
||||
init(_ updater: @escaping (disksList) -> Void) {
|
||||
self.callback = updater
|
||||
|
||||
if self.available {
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
self.read()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func read() {
|
||||
if !self.enabled && self.initialized { return }
|
||||
self.initialized = true
|
||||
|
||||
public override func read() {
|
||||
let keys: [URLResourceKey] = [.volumeNameKey]
|
||||
let paths = FileManager.default.mountedVolumeURLs(includingResourceValuesForKeys: keys)!
|
||||
if let session = DASessionCreate(kCFAllocatorDefault) {
|
||||
@@ -103,13 +47,7 @@ class DiskCapacityReader: Reader {
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.callback(self.disks)
|
||||
})
|
||||
}
|
||||
|
||||
public func toggleEnable(_ value: Bool) {
|
||||
self.enabled = value
|
||||
}
|
||||
|
||||
private func getDisk(_ disk: DADisk) -> diskInfo? {
|
||||
80
Modules/Disk/settings.swift
Normal file
@@ -0,0 +1,80 @@
|
||||
//
|
||||
// settings.swift
|
||||
// Disk
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 12/05/2020.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import StatsKit
|
||||
import ModuleKit
|
||||
|
||||
internal class Settings: NSView, Settings_v {
|
||||
public var selectedDiskHandler: (String) -> Void = {_ in }
|
||||
|
||||
private let title: String
|
||||
private let store: UnsafePointer<Store>
|
||||
private var selectedDisk: String
|
||||
private var button: NSPopUpButton?
|
||||
|
||||
public init(_ title: String, store: UnsafePointer<Store>) {
|
||||
self.title = title
|
||||
self.store = store
|
||||
self.selectedDisk = store.pointee.string(key: "\(self.title)_disk", defaultValue: "")
|
||||
super.init(frame: CGRect(x: Constants.Settings.margin, y: Constants.Settings.margin, width: 0, height: 0))
|
||||
self.wantsLayer = true
|
||||
self.canDrawConcurrently = true
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public func load(rect: NSRect, widget: widget_t) {
|
||||
self.subviews.forEach{ $0.removeFromSuperview() }
|
||||
|
||||
self.addDiskSelector(rect: rect)
|
||||
|
||||
self.setFrameSize(NSSize(width: rect.width - (Constants.Settings.margin*2), height: 30 + (Constants.Settings.margin*2)))
|
||||
}
|
||||
|
||||
private func addDiskSelector(rect: NSRect) {
|
||||
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: rect.width, height: 30))
|
||||
|
||||
let rowTitle: NSTextField = LabelField(frame: NSRect(x: 0, y: (view.frame.height - 16)/2, width: view.frame.width - 52, height: 17), "Disk to show")
|
||||
rowTitle.font = NSFont.systemFont(ofSize: 13, weight: .light)
|
||||
rowTitle.textColor = .labelColor
|
||||
|
||||
self.button = NSPopUpButton(frame: NSRect(x: view.frame.width - 164, y: 0, width: 140, height: 30))
|
||||
self.button!.target = self
|
||||
self.button?.action = #selector(self.handleSelection)
|
||||
|
||||
view.addSubview(rowTitle)
|
||||
view.addSubview(self.button!)
|
||||
|
||||
self.addSubview(view)
|
||||
}
|
||||
|
||||
internal func setList(_ list: DiskList) {
|
||||
let disks = list.list.map{ $0.name }
|
||||
DispatchQueue.main.async(execute: {
|
||||
if disks != self.button?.itemTitles {
|
||||
self.button?.addItems(withTitles: disks)
|
||||
if self.selectedDisk != "" {
|
||||
self.button?.selectItem(withTitle: self.selectedDisk)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@objc func handleSelection(_ sender: NSPopUpButton) {
|
||||
guard let item = sender.selectedItem else { return }
|
||||
self.selectedDisk = item.title
|
||||
self.store.pointee.set(key: "\(self.title)_disk", value: item.title)
|
||||
self.selectedDiskHandler(item.title)
|
||||
}
|
||||
}
|
||||
24
Modules/Memory/Info.plist
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
50
Modules/Memory/config.plist
Normal file
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Name</key>
|
||||
<string>RAM</string>
|
||||
<key>State</key>
|
||||
<true/>
|
||||
<key>Widgets</key>
|
||||
<dict>
|
||||
<key>mini</key>
|
||||
<dict>
|
||||
<key>Default</key>
|
||||
<true/>
|
||||
<key>Preview</key>
|
||||
<dict>
|
||||
<key>Value</key>
|
||||
<string>0.58</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>line_chart</key>
|
||||
<dict>
|
||||
<key>Default</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<key>bar_chart</key>
|
||||
<dict>
|
||||
<key>Default</key>
|
||||
<false/>
|
||||
<key>Label</key>
|
||||
<true/>
|
||||
<key>Box</key>
|
||||
<true/>
|
||||
<key>Color</key>
|
||||
<true/>
|
||||
<key>Preview</key>
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<true/>
|
||||
<key>Box</key>
|
||||
<true/>
|
||||
<key>Color</key>
|
||||
<true/>
|
||||
<key>Value</key>
|
||||
<string>0.48</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
71
Modules/Memory/main.swift
Normal file
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// main.swift
|
||||
// Memory
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 12/04/2020.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import StatsKit
|
||||
import ModuleKit
|
||||
|
||||
public struct Usage: value_t {
|
||||
var active: Double?
|
||||
var inactive: Double?
|
||||
var wired: Double?
|
||||
var compressed: Double?
|
||||
|
||||
var usage: Double?
|
||||
var total: Double?
|
||||
var used: Double?
|
||||
var free: Double?
|
||||
|
||||
public var widget_value: Double {
|
||||
get {
|
||||
return self.usage ?? 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class Memory: Module {
|
||||
private let popupView: Popup = Popup()
|
||||
private var usageReader: UsageReader = UsageReader()
|
||||
|
||||
public init(_ store: UnsafePointer<Store>?) {
|
||||
super.init(
|
||||
store: store,
|
||||
popup: self.popupView,
|
||||
settings: nil
|
||||
)
|
||||
|
||||
self.usageReader.readyCallback = { [unowned self] in
|
||||
self.readyHandler()
|
||||
}
|
||||
self.usageReader.callbackHandler = { [unowned self] value in
|
||||
self.loadCallback(value: value)
|
||||
}
|
||||
|
||||
self.addReader(self.usageReader)
|
||||
}
|
||||
|
||||
private func loadCallback(value: Usage?) {
|
||||
if value == nil {
|
||||
return
|
||||
}
|
||||
|
||||
self.popupView.loadCallback(value!)
|
||||
if let widget = self.widget as? Mini {
|
||||
widget.setValue(value!.usage ?? 0, sufix: "%")
|
||||
}
|
||||
if let widget = self.widget as? LineChart {
|
||||
widget.setValue(value!.usage ?? 0)
|
||||
}
|
||||
if let widget = self.widget as? BarChart {
|
||||
widget.setValue([value!.usage ?? 0])
|
||||
}
|
||||
}
|
||||
}
|
||||
119
Modules/Memory/popup.swift
Normal file
@@ -0,0 +1,119 @@
|
||||
//
|
||||
// popup.swift
|
||||
// Memory
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 18/04/2020.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import ModuleKit
|
||||
import StatsKit
|
||||
|
||||
internal class Popup: NSView {
|
||||
private let dashboardHeight: CGFloat = 90
|
||||
private let detailsHeight: CGFloat = 66
|
||||
|
||||
private var totalField: NSTextField? = nil
|
||||
private var usedField: NSTextField? = nil
|
||||
private var freeField: NSTextField? = nil
|
||||
|
||||
private var activeField: NSTextField? = nil
|
||||
private var inactiveField: NSTextField? = nil
|
||||
private var wiredField: NSTextField? = nil
|
||||
private var compressedField: NSTextField? = nil
|
||||
|
||||
private var chart: LineChartView? = nil
|
||||
private var initialized: Bool = false
|
||||
|
||||
public init() {
|
||||
super.init(frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: dashboardHeight + Constants.Popup.separatorHeight + detailsHeight))
|
||||
|
||||
initFirstView()
|
||||
initDetails()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public override func updateLayer() {
|
||||
self.chart?.display()
|
||||
}
|
||||
|
||||
private func initFirstView() {
|
||||
let rightWidth: CGFloat = 116
|
||||
let view: NSView = NSView(frame: NSRect(x: 0, y: self.frame.height - self.dashboardHeight, width: self.frame.width, height: self.dashboardHeight))
|
||||
|
||||
let leftPanel = NSView(frame: NSRect(x: 0, y: 0, width: view.frame.width - rightWidth - Constants.Popup.margins, height: view.frame.height))
|
||||
|
||||
self.chart = LineChartView(frame: NSRect(x: 4, y: 3, width: leftPanel.frame.width, height: leftPanel.frame.height), num: 120)
|
||||
leftPanel.addSubview(self.chart!)
|
||||
|
||||
let rightPanel: NSView = NSView(frame: NSRect(x: view.frame.width - rightWidth, y: 0, width: rightWidth, height: view.frame.height))
|
||||
self.activeField = addFirstRow(mView: rightPanel, y: ((rightPanel.frame.height - 16)/2)+29, title: "Active:", value: "")
|
||||
self.inactiveField = addFirstRow(mView: rightPanel, y: (rightPanel.frame.height - 16)/2+10, title: "Inactive:", value: "")
|
||||
self.wiredField = addFirstRow(mView: rightPanel, y: ((rightPanel.frame.height - 16)/2)-10, title: "Wired:", value: "")
|
||||
self.compressedField = addFirstRow(mView: rightPanel, y: ((rightPanel.frame.height - 16)/2)-29, title: "Compressed:", value: "")
|
||||
|
||||
view.addSubview(leftPanel)
|
||||
view.addSubview(rightPanel)
|
||||
self.addSubview(view)
|
||||
}
|
||||
|
||||
private func initDetails() {
|
||||
let y: CGFloat = self.frame.height - self.dashboardHeight - Constants.Popup.separatorHeight
|
||||
let separator = SeparatorView("Details", origin: NSPoint(x: 0, y: y), width: self.frame.width)
|
||||
self.addSubview(separator)
|
||||
|
||||
let view: NSView = NSView(frame: NSRect(x: 0, y: separator.frame.origin.y - self.detailsHeight, width: self.frame.width, height: self.detailsHeight))
|
||||
|
||||
self.totalField = PopupRow(view, n: 2, title: "Total:", value: "")
|
||||
self.usedField = PopupRow(view, n: 1, title: "Used:", value: "")
|
||||
self.freeField = PopupRow(view, n: 0, title: "Free:", value: "")
|
||||
|
||||
self.addSubview(view)
|
||||
}
|
||||
|
||||
private func addFirstRow(mView: NSView, y: CGFloat, title: String, value: String) -> NSTextField {
|
||||
let rowView: NSView = NSView(frame: NSRect(x: 0, y: y, width: mView.frame.width, height: 16))
|
||||
|
||||
let labelWidth = title.widthOfString(usingFont: .systemFont(ofSize: 10, weight: .light)) + 4
|
||||
let labelView: NSTextField = TextView(frame: NSRect(x: 0, y: 1.5, width: labelWidth, height: 13))
|
||||
labelView.stringValue = title
|
||||
labelView.alignment = .natural
|
||||
labelView.font = NSFont.systemFont(ofSize: 10, weight: .light)
|
||||
|
||||
let valueView: NSTextField = TextView(frame: NSRect(x: labelWidth, y: 1, width: mView.frame.width - labelWidth, height: 14))
|
||||
valueView.stringValue = value
|
||||
valueView.alignment = .right
|
||||
valueView.font = NSFont.systemFont(ofSize: 11, weight: .medium)
|
||||
|
||||
rowView.addSubview(labelView)
|
||||
rowView.addSubview(valueView)
|
||||
mView.addSubview(rowView)
|
||||
|
||||
return valueView
|
||||
}
|
||||
|
||||
public func loadCallback(_ value: Usage) {
|
||||
DispatchQueue.main.async(execute: {
|
||||
if self.window!.isVisible || !self.initialized {
|
||||
self.activeField?.stringValue = Units(bytes: Int64(value.active!)).getReadableMemory()
|
||||
self.inactiveField?.stringValue = Units(bytes: Int64(value.inactive!)).getReadableMemory()
|
||||
self.wiredField?.stringValue = Units(bytes: Int64(value.wired!)).getReadableMemory()
|
||||
self.compressedField?.stringValue = Units(bytes: Int64(value.compressed!)).getReadableMemory()
|
||||
|
||||
self.totalField?.stringValue = Units(bytes: Int64(value.total!)).getReadableMemory()
|
||||
self.usedField?.stringValue = Units(bytes: Int64(value.used!)).getReadableMemory()
|
||||
self.freeField?.stringValue = Units(bytes: Int64(value.free!)).getReadableMemory()
|
||||
self.initialized = true
|
||||
}
|
||||
|
||||
self.chart?.addValue(value.usage!)
|
||||
})
|
||||
}
|
||||
}
|
||||
72
Modules/Memory/readers.swift
Normal file
@@ -0,0 +1,72 @@
|
||||
//
|
||||
// readers.swift
|
||||
// Memory
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 12/04/2020.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import ModuleKit
|
||||
|
||||
internal class UsageReader: Reader<Usage> {
|
||||
public var totalSize: Double = 0
|
||||
|
||||
public override func setup() {
|
||||
var stats = host_basic_info()
|
||||
var count = UInt32(MemoryLayout<host_basic_info_data_t>.size / MemoryLayout<integer_t>.size)
|
||||
|
||||
let kerr: kern_return_t = withUnsafeMutablePointer(to: &stats) {
|
||||
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
|
||||
host_info(mach_host_self(), HOST_BASIC_INFO, $0, &count)
|
||||
}
|
||||
}
|
||||
|
||||
if kerr == KERN_SUCCESS {
|
||||
self.totalSize = Double(stats.max_mem)
|
||||
return
|
||||
}
|
||||
|
||||
self.totalSize = 0
|
||||
print("Error with host_info(): " + (String(cString: mach_error_string(kerr), encoding: String.Encoding.ascii) ?? "unknown error"))
|
||||
}
|
||||
|
||||
public override func read() {
|
||||
var stats = vm_statistics64()
|
||||
var count = UInt32(MemoryLayout<vm_statistics64_data_t>.size / MemoryLayout<integer_t>.size)
|
||||
|
||||
let result: kern_return_t = withUnsafeMutablePointer(to: &stats) {
|
||||
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
|
||||
host_statistics64(mach_host_self(), HOST_VM_INFO64, $0, &count)
|
||||
}
|
||||
}
|
||||
|
||||
if result == KERN_SUCCESS {
|
||||
let active = Double(stats.active_count) * Double(PAGE_SIZE)
|
||||
let inactive = Double(stats.inactive_count) * Double(PAGE_SIZE)
|
||||
let wired = Double(stats.wire_count) * Double(PAGE_SIZE)
|
||||
let compressed = Double(stats.compressor_page_count) * Double(PAGE_SIZE)
|
||||
|
||||
let used = active + wired + compressed
|
||||
let free = self.totalSize - used
|
||||
|
||||
self.callback(Usage(
|
||||
active: active,
|
||||
inactive: inactive,
|
||||
wired: wired,
|
||||
compressed: compressed,
|
||||
|
||||
usage: Double((self.totalSize - free) / self.totalSize),
|
||||
total: Double(self.totalSize),
|
||||
used: Double(used),
|
||||
free: Double(free))
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
print("Error with host_statistics64(): " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
|
||||
}
|
||||
}
|
||||
24
Modules/Net/Info.plist
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
18
Modules/Net/config.plist
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Name</key>
|
||||
<string>Network</string>
|
||||
<key>State</key>
|
||||
<true/>
|
||||
<key>Widgets</key>
|
||||
<dict>
|
||||
<key>network</key>
|
||||
<dict>
|
||||
<key>Default</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
86
Modules/Net/main.swift
Normal file
@@ -0,0 +1,86 @@
|
||||
//
|
||||
// main.swift
|
||||
// Net
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 24/05/2020.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import StatsKit
|
||||
import ModuleKit
|
||||
|
||||
public enum Network_t: String {
|
||||
case wifi
|
||||
case ethernet
|
||||
}
|
||||
|
||||
public struct Usage: value_t {
|
||||
var active: Bool = false
|
||||
|
||||
var download: Int64 = 0
|
||||
var upload: Int64 = 0
|
||||
|
||||
var laddr: String? = nil // local ip
|
||||
var paddr: String? = nil // remote ip
|
||||
var iaddr: String? = nil // mac adress
|
||||
|
||||
var connectionType: Network_t? = nil
|
||||
|
||||
var countryCode: String? = nil
|
||||
var networkName: String? = nil
|
||||
|
||||
mutating func reset() {
|
||||
self.active = false
|
||||
|
||||
self.download = 0
|
||||
self.upload = 0
|
||||
|
||||
self.laddr = nil
|
||||
self.paddr = nil
|
||||
self.iaddr = nil
|
||||
|
||||
self.connectionType = nil
|
||||
|
||||
self.countryCode = nil
|
||||
self.networkName = nil
|
||||
}
|
||||
|
||||
public var widget_value: Double = 0
|
||||
}
|
||||
|
||||
public class Network: Module {
|
||||
private var usageReader: UsageReader = UsageReader()
|
||||
private let popupView: Popup = Popup()
|
||||
|
||||
public init(_ store: UnsafePointer<Store>?) {
|
||||
super.init(
|
||||
store: store,
|
||||
popup: self.popupView,
|
||||
settings: nil
|
||||
)
|
||||
|
||||
self.usageReader.readyCallback = { [unowned self] in
|
||||
self.readyHandler()
|
||||
}
|
||||
self.usageReader.callbackHandler = { [unowned self] value in
|
||||
self.usageCallback(value)
|
||||
}
|
||||
|
||||
self.addReader(self.usageReader)
|
||||
}
|
||||
|
||||
private func usageCallback(_ value: Usage?) {
|
||||
if value == nil {
|
||||
return
|
||||
}
|
||||
|
||||
self.popupView.usageCallback(value!)
|
||||
if let widget = self.widget as? NetworkWidget {
|
||||
widget.setValue(upload: value!.upload, download: value!.download)
|
||||
}
|
||||
}
|
||||
}
|
||||
195
Modules/Net/popup.swift
Normal file
@@ -0,0 +1,195 @@
|
||||
//
|
||||
// popup.swift
|
||||
// Net
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 24/05/2020.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import ModuleKit
|
||||
import StatsKit
|
||||
|
||||
internal class Popup: NSView {
|
||||
let dashboardHeight: CGFloat = 90
|
||||
let detailsHeight: CGFloat = 88
|
||||
|
||||
private var dashboardView: NSView? = nil
|
||||
|
||||
private var uploadView: NSView? = nil
|
||||
private var uploadValue: Int64 = 0
|
||||
private var uploadValueField: NSTextField? = nil
|
||||
private var uploadUnitField: NSTextField? = nil
|
||||
|
||||
private var downloadView: NSView? = nil
|
||||
private var downloadValue: Int64 = 0
|
||||
private var downloadValueField: NSTextField? = nil
|
||||
private var downloadUnitField: NSTextField? = nil
|
||||
|
||||
private var publicIPField: NSTextField? = nil
|
||||
private var localIPField: NSTextField? = nil
|
||||
private var networkTypeField: NSTextField? = nil
|
||||
private var macAdressField: NSTextField? = nil
|
||||
|
||||
private var initialized: Bool = false
|
||||
|
||||
public init() {
|
||||
super.init(frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: dashboardHeight + Constants.Popup.separatorHeight + detailsHeight))
|
||||
|
||||
initDashboard()
|
||||
initDetails()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func initDashboard() {
|
||||
let view: NSView = NSView(frame: NSRect(x: 0, y: self.frame.height - self.dashboardHeight, width: self.frame.width, height: self.dashboardHeight))
|
||||
|
||||
let leftPart: NSView = NSView(frame: NSRect(x: 0, y: 0, width: view.frame.width / 2, height: view.frame.height))
|
||||
let uploadFields = self.topValueView(leftPart, title: "Upload")
|
||||
self.uploadView = uploadFields.0
|
||||
self.uploadValueField = uploadFields.1
|
||||
self.uploadUnitField = uploadFields.2
|
||||
|
||||
let rightPart: NSView = NSView(frame: NSRect(x: view.frame.width / 2, y: 0, width: view.frame.width / 2, height: view.frame.height))
|
||||
let downloadFields = self.topValueView(rightPart, title: "Download")
|
||||
self.downloadView = downloadFields.0
|
||||
self.downloadValueField = downloadFields.1
|
||||
self.downloadUnitField = downloadFields.2
|
||||
|
||||
view.addSubview(leftPart)
|
||||
view.addSubview(rightPart)
|
||||
self.addSubview(view)
|
||||
self.dashboardView = view
|
||||
}
|
||||
|
||||
private func topValueView(_ view: NSView, title: String) -> (NSView, NSTextField, NSTextField) {
|
||||
let topHeight: CGFloat = 30
|
||||
let titleHeight: CGFloat = 15
|
||||
|
||||
let valueWidth = "0".widthOfString(usingFont: .systemFont(ofSize: 26, weight: .light)) + 5
|
||||
let unitWidth = "KB/s".widthOfString(usingFont: .systemFont(ofSize: 13, weight: .light)) + 5
|
||||
let topPartWidth = valueWidth + unitWidth
|
||||
|
||||
let topPart: NSView = NSView(frame: NSRect(x: (view.frame.width-topPartWidth)/2, y: (view.frame.height - topHeight - titleHeight)/2 + titleHeight, width: topPartWidth, height: topHeight))
|
||||
|
||||
let valueField = LabelField(frame: NSRect(x: 0, y: 0, width: valueWidth, height: 30), "0")
|
||||
valueField.font = NSFont.systemFont(ofSize: 26, weight: .light)
|
||||
valueField.textColor = .labelColor
|
||||
valueField.alignment = .right
|
||||
|
||||
let unitField = LabelField(frame: NSRect(x: valueField.frame.width, y: 4, width: unitWidth, height: 15), "KB/s")
|
||||
unitField.font = NSFont.systemFont(ofSize: 13, weight: .light)
|
||||
unitField.textColor = .labelColor
|
||||
unitField.alignment = .left
|
||||
|
||||
let titleField = LabelField(frame: NSRect(x: 0, y: topPart.frame.origin.y - titleHeight, width: view.frame.width, height: titleHeight), title)
|
||||
titleField.alignment = .center
|
||||
|
||||
topPart.addSubview(valueField)
|
||||
topPart.addSubview(unitField)
|
||||
view.addSubview(topPart)
|
||||
view.addSubview(titleField)
|
||||
|
||||
return (topPart, valueField, unitField)
|
||||
}
|
||||
|
||||
private func setUploadDownloadFields() {
|
||||
let upload = Units(bytes: self.uploadValue).getReadableTuple()
|
||||
let download = Units(bytes: self.downloadValue).getReadableTuple()
|
||||
|
||||
var valueWidth = "\(upload.0)".widthOfString(usingFont: .systemFont(ofSize: 26, weight: .light)) + 5
|
||||
var unitWidth = upload.1.widthOfString(usingFont: .systemFont(ofSize: 13, weight: .light)) + 5
|
||||
var topPartWidth = valueWidth + unitWidth
|
||||
|
||||
self.uploadView?.setFrameSize(NSSize(width: topPartWidth, height: self.uploadView!.frame.height))
|
||||
self.uploadView?.setFrameOrigin(NSPoint(x: ((self.frame.width/2)-topPartWidth)/2, y: self.uploadView!.frame.origin.y))
|
||||
|
||||
self.uploadValueField?.setFrameSize(NSSize(width: valueWidth, height: self.uploadValueField!.frame.height))
|
||||
self.uploadValueField?.stringValue = "\(upload.0)"
|
||||
self.uploadUnitField?.setFrameSize(NSSize(width: unitWidth, height: self.uploadUnitField!.frame.height))
|
||||
self.uploadUnitField?.setFrameOrigin(NSPoint(x: self.uploadValueField!.frame.width, y: self.uploadUnitField!.frame.origin.y))
|
||||
self.uploadUnitField?.stringValue = upload.1
|
||||
|
||||
valueWidth = "\(download.0)".widthOfString(usingFont: .systemFont(ofSize: 26, weight: .light)) + 5
|
||||
unitWidth = download.1.widthOfString(usingFont: .systemFont(ofSize: 13, weight: .light)) + 5
|
||||
topPartWidth = valueWidth + unitWidth
|
||||
|
||||
self.downloadView?.setFrameSize(NSSize(width: topPartWidth, height: self.downloadView!.frame.height))
|
||||
self.downloadView?.setFrameOrigin(NSPoint(x: ((self.frame.width/2)-topPartWidth)/2, y: self.downloadView!.frame.origin.y))
|
||||
|
||||
self.downloadValueField?.setFrameSize(NSSize(width: valueWidth, height: self.downloadValueField!.frame.height))
|
||||
self.downloadValueField?.stringValue = "\(download.0)"
|
||||
self.downloadUnitField?.setFrameSize(NSSize(width: unitWidth, height: self.downloadUnitField!.frame.height))
|
||||
self.downloadUnitField?.setFrameOrigin(NSPoint(x: self.downloadValueField!.frame.width, y: self.downloadUnitField!.frame.origin.y))
|
||||
self.downloadUnitField?.stringValue = download.1
|
||||
}
|
||||
|
||||
private func initDetails() {
|
||||
let y: CGFloat = self.dashboardView!.frame.origin.y - Constants.Popup.separatorHeight
|
||||
let separator = SeparatorView("Details", origin: NSPoint(x: 0, y: y), width: self.frame.width)
|
||||
self.addSubview(separator)
|
||||
|
||||
let view: NSView = NSView(frame: NSRect(x: 0, y: separator.frame.origin.y - self.detailsHeight, width: self.frame.width, height: self.detailsHeight))
|
||||
|
||||
self.publicIPField = PopupRow(view, n: 3, title: "Public IP:", value: "")
|
||||
self.localIPField = PopupRow(view, n: 2, title: "Local IP:", value: "")
|
||||
self.networkTypeField = PopupRow(view, n: 1, title: "Network:", value: "")
|
||||
self.macAdressField = PopupRow(view, n: 0, title: "Physical address:", value: "")
|
||||
|
||||
self.addSubview(view)
|
||||
}
|
||||
|
||||
public func usageCallback(_ value: Usage) {
|
||||
DispatchQueue.main.async(execute: {
|
||||
if !self.window!.isVisible && self.initialized {
|
||||
return
|
||||
}
|
||||
|
||||
self.uploadValue = value.upload
|
||||
self.downloadValue = value.download
|
||||
self.setUploadDownloadFields()
|
||||
|
||||
if !value.active {
|
||||
self.publicIPField?.stringValue = "No connection"
|
||||
self.localIPField?.stringValue = "No connection"
|
||||
self.networkTypeField?.stringValue = "No connection"
|
||||
self.macAdressField?.stringValue = "No connection"
|
||||
return
|
||||
}
|
||||
|
||||
if var publicIP = value.paddr, self.publicIPField?.stringValue != publicIP {
|
||||
if value.countryCode != nil {
|
||||
publicIP = "\(publicIP) (\(value.countryCode!))"
|
||||
}
|
||||
self.publicIPField?.stringValue = publicIP
|
||||
}
|
||||
if value.laddr != nil && self.localIPField?.stringValue != value.laddr {
|
||||
self.localIPField?.stringValue = value.laddr!
|
||||
}
|
||||
if value.iaddr != nil && self.macAdressField?.stringValue != value.iaddr {
|
||||
self.macAdressField?.stringValue = value.iaddr!
|
||||
}
|
||||
|
||||
if value.connectionType != nil {
|
||||
var networkType = ""
|
||||
if value.connectionType == .wifi {
|
||||
networkType = "\(value.networkName!) (WiFi)"
|
||||
} else if value.connectionType == .ethernet {
|
||||
networkType = "Ethernet"
|
||||
}
|
||||
|
||||
if self.networkTypeField?.stringValue != networkType {
|
||||
self.networkTypeField?.stringValue = networkType
|
||||
}
|
||||
}
|
||||
|
||||
self.initialized = true
|
||||
})
|
||||
}
|
||||
}
|
||||
214
Modules/Net/readers.swift
Normal file
@@ -0,0 +1,214 @@
|
||||
//
|
||||
// readers.swift
|
||||
// Net
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 24/05/2020.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import ModuleKit
|
||||
import SystemConfiguration
|
||||
import Reachability
|
||||
import os.log
|
||||
import CoreWLAN
|
||||
|
||||
internal class UsageReader: Reader<Usage> {
|
||||
private var reachability: Reachability? = nil
|
||||
private var usage: Usage = Usage()
|
||||
|
||||
private var interfaceID: String? = nil
|
||||
|
||||
public override func setup() {
|
||||
do {
|
||||
self.reachability = try Reachability()
|
||||
try self.reachability!.startNotifier()
|
||||
} catch let error {
|
||||
os_log(.error, log: log, "initialize Reachability error %s", "\(error)")
|
||||
}
|
||||
|
||||
self.reachability!.whenReachable = { _ in
|
||||
self.readInformation()
|
||||
self.start()
|
||||
}
|
||||
self.reachability!.whenUnreachable = { _ in
|
||||
self.usage.reset()
|
||||
self.callback(self.usage)
|
||||
self.stop()
|
||||
}
|
||||
}
|
||||
|
||||
public override func read() {
|
||||
guard self.reachability?.connection != .unavailable else {
|
||||
if self.usage.active {
|
||||
self.usage.reset()
|
||||
self.callback(self.usage)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var interfaceAddresses: UnsafeMutablePointer<ifaddrs>? = nil
|
||||
var upload: Int64 = 0
|
||||
var download: Int64 = 0
|
||||
guard getifaddrs(&interfaceAddresses) == 0 else { return }
|
||||
|
||||
var pointer = interfaceAddresses
|
||||
while pointer != nil {
|
||||
defer { pointer = pointer?.pointee.ifa_next }
|
||||
|
||||
if String(cString: pointer!.pointee.ifa_name) != self.interfaceID {
|
||||
continue
|
||||
}
|
||||
|
||||
if let info = getBytesInfo(pointer!) {
|
||||
upload += info.upload
|
||||
download += info.download
|
||||
}
|
||||
|
||||
if let ip = getLocalIP(pointer!), self.usage.laddr != ip {
|
||||
self.usage.laddr = ip
|
||||
}
|
||||
}
|
||||
freeifaddrs(interfaceAddresses)
|
||||
|
||||
if self.usage.upload != 0 && self.usage.download != 0 {
|
||||
self.usage.upload = upload - self.usage.upload
|
||||
self.usage.download = download - self.usage.download
|
||||
}
|
||||
|
||||
self.callback(self.usage)
|
||||
self.usage.upload = upload
|
||||
self.usage.download = download
|
||||
}
|
||||
|
||||
private func readInformation() {
|
||||
guard self.reachability != nil && self.reachability!.connection != .unavailable else { return }
|
||||
|
||||
if let global = SCDynamicStoreCopyValue(nil, "State:/Network/Global/IPv4" as CFString) {
|
||||
self.interfaceID = global["PrimaryInterface"] as? String
|
||||
}
|
||||
|
||||
self.usage.active = true
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
self.usage.paddr = self.getPublicIP()
|
||||
}
|
||||
|
||||
if self.reachability!.connection == .wifi && CWWiFiClient.shared().interface() != nil {
|
||||
self.usage.connectionType = .wifi
|
||||
self.usage.networkName = CWWiFiClient.shared().interface()!.ssid()
|
||||
self.usage.countryCode = CWWiFiClient.shared().interface()!.countryCode()
|
||||
self.usage.iaddr = CWWiFiClient.shared().interface()!.hardwareAddress()
|
||||
} else {
|
||||
self.usage.connectionType = .ethernet
|
||||
self.usage.iaddr = getMacAddress()
|
||||
}
|
||||
}
|
||||
|
||||
private func getDataUsageInfo(from infoPointer: UnsafeMutablePointer<ifaddrs>) -> (upload: Int64, download: Int64)? {
|
||||
let pointer = infoPointer
|
||||
|
||||
let addr = pointer.pointee.ifa_addr.pointee
|
||||
guard addr.sa_family == UInt8(AF_LINK) else { return nil }
|
||||
var networkData: UnsafeMutablePointer<if_data>? = nil
|
||||
|
||||
networkData = unsafeBitCast(pointer.pointee.ifa_data, to: UnsafeMutablePointer<if_data>.self)
|
||||
return (upload: Int64(networkData?.pointee.ifi_obytes ?? 0), download: Int64(networkData?.pointee.ifi_ibytes ?? 0))
|
||||
}
|
||||
|
||||
private func getBytesInfo(_ pointer: UnsafeMutablePointer<ifaddrs>) -> (upload: Int64, download: Int64)? {
|
||||
let addr = pointer.pointee.ifa_addr.pointee
|
||||
|
||||
guard addr.sa_family == UInt8(AF_LINK) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let data: UnsafeMutablePointer<if_data>? = unsafeBitCast(pointer.pointee.ifa_data, to: UnsafeMutablePointer<if_data>.self)
|
||||
return (upload: Int64(data?.pointee.ifi_obytes ?? 0), download: Int64(data?.pointee.ifi_ibytes ?? 0))
|
||||
}
|
||||
|
||||
private func getLocalIP(_ pointer: UnsafeMutablePointer<ifaddrs>) -> String? {
|
||||
var addr = pointer.pointee.ifa_addr.pointee
|
||||
|
||||
guard addr.sa_family == UInt8(AF_INET) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var ip = [CChar](repeating: 0, count: Int(NI_MAXHOST))
|
||||
getnameinfo(&addr, socklen_t(addr.sa_len), &ip, socklen_t(ip.count), nil, socklen_t(0), NI_NUMERICHOST)
|
||||
|
||||
return String(cString: ip)
|
||||
}
|
||||
|
||||
private func getPublicIP() -> String? {
|
||||
let url = URL(string: "https://api.ipify.org")
|
||||
var address: String? = nil
|
||||
|
||||
do {
|
||||
if let url = url {
|
||||
address = try String(contentsOf: url)
|
||||
if address!.contains("<") {
|
||||
address = nil
|
||||
}
|
||||
}
|
||||
} catch let error {
|
||||
os_log(.error, log: log, "get public ip %s", "\(error)")
|
||||
}
|
||||
|
||||
return address
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/31835418/how-to-get-mac-address-from-os-x-with-swift
|
||||
private func getMacAddress() -> String? {
|
||||
var macAddressAsString : String?
|
||||
if let intfIterator = findEthernetInterfaces() {
|
||||
if let macAddress = getMACAddress(intfIterator) {
|
||||
macAddressAsString = macAddress.map( { String(format:"%02x", $0) } ).joined(separator: ":")
|
||||
}
|
||||
IOObjectRelease(intfIterator)
|
||||
}
|
||||
return macAddressAsString
|
||||
}
|
||||
|
||||
private func findEthernetInterfaces() -> io_iterator_t? {
|
||||
let matchingDictUM = IOServiceMatching("IOEthernetInterface");
|
||||
if matchingDictUM == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
let matchingDict = matchingDictUM! as NSMutableDictionary
|
||||
matchingDict["IOPropertyMatch"] = [ "IOPrimaryInterface" : true]
|
||||
|
||||
var matchingServices : io_iterator_t = 0
|
||||
if IOServiceGetMatchingServices(kIOMasterPortDefault, matchingDict, &matchingServices) != KERN_SUCCESS {
|
||||
return nil
|
||||
}
|
||||
|
||||
return matchingServices
|
||||
}
|
||||
|
||||
private func getMACAddress(_ intfIterator : io_iterator_t) -> [UInt8]? {
|
||||
var macAddress : [UInt8]?
|
||||
var intfService = IOIteratorNext(intfIterator)
|
||||
|
||||
while intfService != 0 {
|
||||
var controllerService : io_object_t = 0
|
||||
if IORegistryEntryGetParentEntry(intfService, kIOServicePlane, &controllerService) == KERN_SUCCESS {
|
||||
let dataUM = IORegistryEntryCreateCFProperty(controllerService, "IOMACAddress" as CFString, kCFAllocatorDefault, 0)
|
||||
if dataUM != nil {
|
||||
let data = (dataUM!.takeRetainedValue() as! CFData) as Data
|
||||
macAddress = [0, 0, 0, 0, 0, 0]
|
||||
data.copyBytes(to: &macAddress!, count: macAddress!.count)
|
||||
}
|
||||
IOObjectRelease(controllerService)
|
||||
}
|
||||
|
||||
IOObjectRelease(intfService)
|
||||
intfService = IOIteratorNext(intfIterator)
|
||||
}
|
||||
|
||||
return macAddress
|
||||
}
|
||||
}
|
||||
39
README.md
@@ -1,40 +1,39 @@
|
||||
# Stats
|
||||
Simple macOS system monitor in your menu bar
|
||||
|
||||
<p align="center"><img src="https://serhiy.s3.eu-central-1.amazonaws.com/Github_repo/stats/logo.png?raw=true" width="120"></p>
|
||||
|
||||
[](https://github.com/exelban/stats/releases)
|
||||
|
||||
Simple macOS system monitor in your menu bar
|
||||
|
||||
## Installation
|
||||
You can download latest version [here](https://github.com/exelban/stats/releases).
|
||||
|
||||
## Requirements
|
||||
|
||||
Stats is currently supported on macOS 10.14 (Mojave) and higher.
|
||||
|
||||
## Features
|
||||
Stats is a application which allows you to monitor your macOS system.
|
||||
|
||||
- CPU Usage
|
||||
- Memory Usage
|
||||
- Sensors (Temperature/Voltage/Power)
|
||||
- Disk utilization
|
||||
- Battery level
|
||||
- Network usage
|
||||
- Black theme compatible
|
||||
|
||||
## Installation
|
||||
You can download latest version [here](https://github.com/exelban/stats/releases).
|
||||
## Developing
|
||||
|
||||
## Modules
|
||||
Pull requests and impovment proposals are welcomed.
|
||||
|
||||
| Name | Available widgets | Description |
|
||||
| --- | --- | --- |
|
||||
| **CPU** | Percentage / Chart / Chart with value / Chart Bar | Shows CPU usage |
|
||||
| **Memory** | Percentage / Chart / Chart with value / Chart Bar | Shows RAM usage |
|
||||
| **Sensors** | Text | Shows data from internal sensors |
|
||||
| **Disk** | Percentage / Chart Bar | Shows disk utilization |
|
||||
| **Battery** | Graphic / Percentage | Shows battery level and charging status |
|
||||
| **Newtork** | Dots / Upload/Download traffic | Shows network activity |
|
||||
If you want to run the project locally you need to have [carthage](https://github.com/Carthage/Carthage#installing-carthage) installed.
|
||||
|
||||
## Compatibility
|
||||
| macOS | Compatible |
|
||||
| --- | --- |
|
||||
| 10.15.3 *(Catalina)* | **true** |
|
||||
| 10.14.6 *(Mojave)* | **true** |
|
||||
| 10.13.6 *(High Sierra)* | **true** |
|
||||
```bash
|
||||
git clone https://github.com/exelban/stats
|
||||
cd stats
|
||||
make dep
|
||||
open ./Stats.xcodeproj
|
||||
```
|
||||
|
||||
## License
|
||||
[MIT License](https://github.com/exelban/stats/blob/master/LICENSE)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1140"
|
||||
LastUpgradeVersion = "1150"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -48,7 +48,7 @@
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
allowLocationSimulation = "NO">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
@@ -59,11 +59,30 @@
|
||||
ReferencedContainer = "container:Stats.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "DYLD_PRINT_STATISTICS"
|
||||
value = "true"
|
||||
isEnabled = "NO">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "DYLD_PRINT_LIBRARIES"
|
||||
value = "true"
|
||||
isEnabled = "NO">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
<AdditionalOptions>
|
||||
<AdditionalOption
|
||||
key = "MallocStackLogging"
|
||||
value = ""
|
||||
isEnabled = "YES">
|
||||
</AdditionalOption>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
buildConfiguration = "Debug"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
savedToolIdentifier = "Leaks"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
|
||||
@@ -7,101 +7,125 @@
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import ServiceManagement
|
||||
import os.log
|
||||
import StatsKit
|
||||
import ModuleKit
|
||||
import CPU
|
||||
import Memory
|
||||
import Disk
|
||||
import Net
|
||||
import Battery
|
||||
|
||||
var store: Store = Store()
|
||||
let updater = macAppUpdater(user: "exelban", repo: "stats")
|
||||
var menuBar: MenuBar?
|
||||
let smc = SMCService()
|
||||
let systemKit: SystemKit = SystemKit()
|
||||
var smc: SMCService = SMCService()
|
||||
var modules: [Module] = [Battery(&store), Network(&store), Disk(&store), Memory(&store), CPU(&store, &smc)].reversed()
|
||||
var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Stats")
|
||||
|
||||
@NSApplicationMain
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
private let defaults = UserDefaults.standard
|
||||
private var menuBarItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
|
||||
private let popover = NSPopover()
|
||||
private let settingsWindow: SettingsWindow = SettingsWindow()
|
||||
private let updateWindow: UpdateWindow = UpdateWindow()
|
||||
|
||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||
let res = smc.open()
|
||||
if res != kIOReturnSuccess {
|
||||
print("ERROR open SMC")
|
||||
NSApp.terminate(nil)
|
||||
return
|
||||
}
|
||||
let startingPoint = Date()
|
||||
|
||||
guard let menuBarButton = self.menuBarItem.button else {
|
||||
NSApp.terminate(nil)
|
||||
return
|
||||
}
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(toggleSettingsHandler), name: .toggleSettings, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(checkForUpdates), name: .checkForUpdates, object: nil)
|
||||
|
||||
menuBarButton.action = #selector(toggleMenu)
|
||||
menuBarButton.sendAction(on: [.leftMouseDown, .rightMouseDown])
|
||||
modules.forEach{ $0.load() }
|
||||
|
||||
let mcv = MainViewController.Init()
|
||||
self.popover.contentViewController = mcv
|
||||
self.popover.behavior = .transient
|
||||
self.popover.animates = true
|
||||
|
||||
menuBar = MenuBar(menuBarItem, menuBarButton: menuBarButton, popup: mcv)
|
||||
menuBar!.build()
|
||||
self.settingsWindow.setModules()
|
||||
|
||||
self.setVersion()
|
||||
self.defaultValues()
|
||||
os_log(.info, log: log, "Stats started in %.4f seconds", startingPoint.timeIntervalSinceNow * -1)
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ aNotification: Notification) {
|
||||
modules.forEach{ $0.terminate() }
|
||||
_ = smc.close()
|
||||
menuBar?.destroy()
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
func applicationWillResignActive(_ notification: Notification) {
|
||||
self.popover.performClose(self)
|
||||
@objc private func toggleSettingsHandler(_ notification: Notification) {
|
||||
if !self.settingsWindow.isVisible {
|
||||
self.settingsWindow.setIsVisible(true)
|
||||
self.settingsWindow.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
|
||||
@objc func toggleMenu(_ sender: Any?) {
|
||||
if self.popover.isShown {
|
||||
self.popover.performClose(sender)
|
||||
} else {
|
||||
if let button = self.menuBarItem.button {
|
||||
NSApplication.shared.activate(ignoringOtherApps: true)
|
||||
self.popover.show(relativeTo: .zero, of: button, preferredEdge: .maxY)
|
||||
self.popover.becomeFirstResponder()
|
||||
}
|
||||
if let name = notification.userInfo?["module"] as? String {
|
||||
self.settingsWindow.openMenu(name)
|
||||
}
|
||||
}
|
||||
|
||||
private func defaultValues() {
|
||||
if self.defaults.object(forKey: "runAtLoginInitialized") == nil {
|
||||
LaunchAtLogin.isEnabled = true
|
||||
}
|
||||
|
||||
if defaults.object(forKey: "dockIcon") != nil {
|
||||
let dockIconStatus = defaults.bool(forKey: "dockIcon") ? NSApplication.ActivationPolicy.regular : NSApplication.ActivationPolicy.accessory
|
||||
NSApp.setActivationPolicy(dockIconStatus)
|
||||
}
|
||||
|
||||
if defaults.object(forKey: "checkUpdatesOnLogin") == nil || defaults.bool(forKey: "checkUpdatesOnLogin") {
|
||||
self.checkForNewVersion()
|
||||
}
|
||||
}
|
||||
|
||||
private func checkForNewVersion() {
|
||||
@objc private func checkForUpdates(_ notification: Notification) {
|
||||
updater.check() { result, error in
|
||||
if error != nil && error as! String == "No internet connection" {
|
||||
print("Error: \(error ?? "check error")")
|
||||
if error != nil {
|
||||
os_log(.error, log: log, "error updater.check(): %s", "\(error!.localizedDescription)")
|
||||
return
|
||||
}
|
||||
|
||||
guard error == nil, let version: version = result else {
|
||||
print("Error: \(error ?? "download error")")
|
||||
os_log(.error, log: log, "download error(): %s", "\(error!.localizedDescription)")
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async(execute: {
|
||||
os_log(.error, log: log, "open update window: %s", "\(version.latest)")
|
||||
self.updateWindow.open(version)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func setVersion() {
|
||||
let key = "version"
|
||||
let currentVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String
|
||||
|
||||
if !store.exist(key: key) {
|
||||
store.reset()
|
||||
os_log(.info, log: log, "Previous version not detected. Current version (%s) set", currentVersion)
|
||||
} else {
|
||||
let prevVersion = store.string(key: key, defaultValue: "")
|
||||
if prevVersion == currentVersion {
|
||||
return
|
||||
}
|
||||
os_log(.info, log: log, "Detected previous version %s. Current version (%s) set", prevVersion, currentVersion)
|
||||
}
|
||||
|
||||
store.set(key: key, value: currentVersion)
|
||||
}
|
||||
|
||||
private func defaultValues() {
|
||||
if !store.exist(key: "runAtLoginInitialized") {
|
||||
store.set(key: "runAtLoginInitialized", value: true)
|
||||
LaunchAtLogin.isEnabled = true
|
||||
}
|
||||
|
||||
if store.exist(key: "dockIcon") {
|
||||
let dockIconStatus = store.bool(key: "dockIcon", defaultValue: false) ? NSApplication.ActivationPolicy.regular : NSApplication.ActivationPolicy.accessory
|
||||
NSApp.setActivationPolicy(dockIconStatus)
|
||||
}
|
||||
|
||||
if store.bool(key: "checkUpdatesOnLogin", defaultValue: true) {
|
||||
updater.check() { result, error in
|
||||
if error != nil {
|
||||
os_log(.error, log: log, "error updater.check(): %s", "\(error!.localizedDescription)")
|
||||
return
|
||||
}
|
||||
|
||||
guard error == nil, let version: version = result else {
|
||||
os_log(.error, log: log, "download error(): %s", "\(error!.localizedDescription)")
|
||||
return
|
||||
}
|
||||
|
||||
if version.newest {
|
||||
DispatchQueue.main.async(execute: {
|
||||
let updatesVC: NSWindowController? = NSStoryboard(name: "Updates", bundle: nil).instantiateController(withIdentifier: "UpdatesVC") as? NSWindowController
|
||||
updatesVC?.window?.center()
|
||||
updatesVC?.window?.level = .floating
|
||||
updatesVC!.showWindow(self)
|
||||
os_log(.error, log: log, "show update window because new version of app found: %s", "\(version.latest)")
|
||||
self.updateWindow.open(version)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
//
|
||||
// MenuBar.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 31.05.2019.
|
||||
// Copyright © 2019 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import ServiceManagement
|
||||
|
||||
/*
|
||||
Class keeps a status bar item and has the main function for updating widgets.
|
||||
*/
|
||||
class MenuBar {
|
||||
public let modules: [Module] = [CPU(), RAM(), Sensors(), Disk(), Battery(), Network()]
|
||||
|
||||
private let menuBarItem: NSStatusItem
|
||||
private var menuBarButton: NSButton = NSButton()
|
||||
private var stackView: NSStackView = NSStackView()
|
||||
private var popup: MainViewController
|
||||
|
||||
/*
|
||||
Init main variables.
|
||||
*/
|
||||
init(_ menuBarItem: NSStatusItem, menuBarButton: NSButton, popup: MainViewController) {
|
||||
self.menuBarItem = menuBarItem
|
||||
self.menuBarButton = menuBarButton
|
||||
self.popup = popup
|
||||
}
|
||||
|
||||
/*
|
||||
Build status bar view with all widgets. All widgets must be initialized before.
|
||||
*/
|
||||
public func build() {
|
||||
let stackView = NSStackView(frame: NSRect(x: 0, y: 0, width: widgetSize.width, height: widgetSize.height))
|
||||
stackView.wantsLayer = true
|
||||
stackView.orientation = .horizontal
|
||||
stackView.distribution = .fillProportionally
|
||||
stackView.spacing = 0
|
||||
self.stackView = stackView
|
||||
|
||||
var WIDTH: CGFloat = 0
|
||||
for module in self.modules {
|
||||
if module.available {
|
||||
if module.enabled {
|
||||
module.start()
|
||||
stackView.addArrangedSubview(module.widget.view)
|
||||
WIDTH = WIDTH + module.widget.view.frame.size.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.menuBarButton.addSubview(stackView)
|
||||
|
||||
if self.stackView.subviews.count == 0 || WIDTH == 0 {
|
||||
self.menuBarButton.image = NSImage(named:NSImage.Name("tray_icon"))
|
||||
self.stackView.frame.size.width = widgetSize.width
|
||||
self.menuBarItem.length = widgetSize.width
|
||||
return
|
||||
}
|
||||
|
||||
self.menuBarButton.image = nil
|
||||
self.stackView.frame.size.width = WIDTH
|
||||
self.menuBarItem.length = WIDTH
|
||||
}
|
||||
|
||||
/*
|
||||
Realod status bar view. Using to enable/disable widgets. Use this function when enable/disable modules.
|
||||
Or if widget type is changed.
|
||||
*/
|
||||
public func reload(name: String) {
|
||||
let module = self.modules.filter{ $0.name == name }
|
||||
if module.isEmpty {
|
||||
return
|
||||
}
|
||||
|
||||
let view = self.stackView.subviews.filter{ $0 is Widget && ($0 as! Widget).name == name }
|
||||
if view.isEmpty {
|
||||
// if module is active but not exist in stack, add it to stack (enable module)
|
||||
if module.first!.enabled {
|
||||
let activeModules = self.modules.filter{ $0.enabled && $0.available }
|
||||
let position = activeModules.firstIndex { $0.name == name }
|
||||
|
||||
module.first!.start()
|
||||
|
||||
if position! >= activeModules.count-1 {
|
||||
stackView.addArrangedSubview(module.first!.widget.view)
|
||||
} else {
|
||||
stackView.insertArrangedSubview(module.first!.widget.view, at: position!)
|
||||
stackView.updateLayer()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if module not active but exist, remove from stack (disable module), else replace
|
||||
if !module.first!.enabled {
|
||||
view.first!.removeFromSuperview()
|
||||
} else {
|
||||
let newView = module.first!.widget.view
|
||||
newView.invalidateIntrinsicContentSize()
|
||||
self.stackView.replaceSubview(view.first!, with: newView)
|
||||
}
|
||||
}
|
||||
|
||||
self.updateWidth()
|
||||
self.popup.reload()
|
||||
}
|
||||
|
||||
/*
|
||||
Refresh wigets views if size of view was changed.
|
||||
For enabling/disabling widgets, please use reload().
|
||||
*/
|
||||
public func refresh() {
|
||||
self.stackView.subviews.forEach { view in
|
||||
if !(view is Widget) { return }
|
||||
|
||||
let module = self.modules.first { $0.name == (view as! Widget).name }
|
||||
if module == nil {
|
||||
return
|
||||
}
|
||||
|
||||
module!.widget.view.invalidateIntrinsicContentSize()
|
||||
self.stackView.replaceSubview(view, with: module!.widget.view)
|
||||
self.updateWidth()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Destroy will destroy status bar view.
|
||||
*/
|
||||
public func destroy() {
|
||||
for module in self.modules {
|
||||
module.stop()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateWidth() {
|
||||
var WIDTH: CGFloat = 0
|
||||
for module in self.modules {
|
||||
if module.enabled && module.available {
|
||||
WIDTH = WIDTH + module.widget.view.frame.size.width
|
||||
}
|
||||
}
|
||||
|
||||
if self.stackView.subviews.count == 0 || WIDTH == 0 {
|
||||
self.menuBarButton.image = NSImage(named:NSImage.Name("tray_icon"))
|
||||
self.menuBarItem.length = widgetSize.width
|
||||
self.stackView.frame.size.width = widgetSize.width
|
||||
} else {
|
||||
self.menuBarButton.image = nil
|
||||
self.stackView.frame.size.width = WIDTH
|
||||
self.menuBarItem.length = WIDTH
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
//
|
||||
// Battery.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 14/01/2020.
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import IOKit.ps
|
||||
import Repeat
|
||||
|
||||
class Battery: Module {
|
||||
public var name: String = "Battery"
|
||||
|
||||
public var enabled: Bool = true
|
||||
public var available: Bool {
|
||||
get {
|
||||
let snapshot = IOPSCopyPowerSourcesInfo().takeRetainedValue()
|
||||
let sources = IOPSCopyPowerSourcesList(snapshot).takeRetainedValue() as Array
|
||||
return sources.count > 0
|
||||
}
|
||||
}
|
||||
|
||||
public var readers: [Reader] = []
|
||||
public var task: Repeater?
|
||||
|
||||
public var widget: ModuleWidget = ModuleWidget()
|
||||
public var popup: ModulePopup = ModulePopup(true)
|
||||
public var menu: NSMenuItem = NSMenuItem()
|
||||
|
||||
internal let defaults = UserDefaults.standard
|
||||
internal var submenu: NSMenu = NSMenu()
|
||||
|
||||
internal var cyclesValue: NSTextField = NSTextField()
|
||||
internal var stateValue: NSTextField = NSTextField()
|
||||
internal var healthValue: NSTextField = NSTextField()
|
||||
internal var amperageValue: NSTextField = NSTextField()
|
||||
internal var voltageValue: NSTextField = NSTextField()
|
||||
internal var temperatureValue: NSTextField = NSTextField()
|
||||
internal var powerValue: NSTextField = NSTextField()
|
||||
internal var chargingValue: NSTextField = NSTextField()
|
||||
internal var levelValue: NSTextField = NSTextField()
|
||||
internal var sourceValue: NSTextField = NSTextField()
|
||||
internal var timeLabel: NSTextField = NSTextField()
|
||||
internal var timeValue: NSTextField = NSTextField()
|
||||
|
||||
init() {
|
||||
if !self.available { return }
|
||||
|
||||
self.enabled = defaults.object(forKey: name) != nil ? defaults.bool(forKey: name) : true
|
||||
self.widget.type = defaults.object(forKey: "\(name)_widget") != nil ? defaults.float(forKey: "\(name)_widget") : Widgets.Battery
|
||||
|
||||
self.initWidget()
|
||||
self.initMenu()
|
||||
self.initPopup()
|
||||
|
||||
readers.append(BatteryReader(self.usageUpdater))
|
||||
}
|
||||
|
||||
public func start() {
|
||||
(readers[0] as! BatteryReader).start()
|
||||
}
|
||||
|
||||
public func stop() {
|
||||
if readers.count > 0 {
|
||||
(readers[0] as! BatteryReader).stop()
|
||||
}
|
||||
}
|
||||
|
||||
public func restart() {
|
||||
self.stop()
|
||||
self.start()
|
||||
}
|
||||
|
||||
private func usageUpdater(value: BatteryUsage) {
|
||||
self.popupUpdater(value: value)
|
||||
|
||||
var time = value.timeToEmpty
|
||||
if time == 0 && value.timeToCharge != 0 {
|
||||
time = value.timeToCharge
|
||||
}
|
||||
|
||||
if self.widget.view is Widget {
|
||||
(self.widget.view as! Widget).setValue(data: [abs(value.level), Double(time)])
|
||||
|
||||
if self.widget.view is BatteryWidget && value.level != 100 {
|
||||
(self.widget.view as! BatteryWidget).setCharging(value: value.level > 0)
|
||||
} else if self.widget.view is BatteryWidget && value.level == 100 {
|
||||
(self.widget.view as! BatteryWidget).setCharging(value: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
//
|
||||
// BatteryMenu.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 14/01/2020.
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
extension Battery {
|
||||
public 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(toggleWidget), keyEquivalent: "")
|
||||
percentage.state = self.widget.type == Widgets.BatteryPercentage ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
percentage.target = self
|
||||
|
||||
let time = NSMenuItem(title: "Time", action: #selector(toggleWidget), keyEquivalent: "")
|
||||
time.state = self.widget.type == Widgets.BatteryTime ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
time.target = self
|
||||
|
||||
submenu.addItem(percentage)
|
||||
submenu.addItem(time)
|
||||
|
||||
submenu.addItem(NSMenuItem.separator())
|
||||
|
||||
if let view = self.widget.view as? Widget {
|
||||
for widgetMenu in view.menus {
|
||||
submenu.addItem(widgetMenu)
|
||||
}
|
||||
}
|
||||
|
||||
if self.enabled {
|
||||
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.enabled = state
|
||||
menuBar!.reload(name: self.name)
|
||||
|
||||
if !state {
|
||||
menu.submenu = nil
|
||||
} else {
|
||||
menu.submenu = submenu
|
||||
}
|
||||
|
||||
self.restart()
|
||||
}
|
||||
|
||||
@objc func toggleWidget(_ sender: NSMenuItem) {
|
||||
var widgetCode: Float = 0.0
|
||||
|
||||
switch sender.title {
|
||||
case "Percentage":
|
||||
widgetCode = Widgets.BatteryPercentage
|
||||
case "Time":
|
||||
widgetCode = Widgets.BatteryTime
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if self.widget.type == widgetCode {
|
||||
widgetCode = Widgets.Battery
|
||||
}
|
||||
|
||||
let state = sender.state == NSControl.StateValue.on
|
||||
for item in self.submenu.items {
|
||||
if item.title == "Percentage" || item.title == "Time" {
|
||||
item.state = NSControl.StateValue.off
|
||||
}
|
||||
}
|
||||
|
||||
sender.state = state ? NSControl.StateValue.off : NSControl.StateValue.on
|
||||
self.defaults.set(widgetCode, forKey: "\(name)_widget")
|
||||
self.widget.type = widgetCode
|
||||
self.initWidget()
|
||||
self.initMenu()
|
||||
menuBar!.reload(name: self.name)
|
||||
}
|
||||
}
|
||||
@@ -1,257 +0,0 @@
|
||||
//
|
||||
// BatteryPopup.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 14/01/2020.
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
extension Battery {
|
||||
public func initPopup() {
|
||||
self.popup.view.view?.frame = NSRect(x: 0, y: 0, width: TabWidth, height: TabHeight)
|
||||
|
||||
self.makeMain()
|
||||
self.makeOverview()
|
||||
self.makeBattery()
|
||||
self.makePowerAdapter()
|
||||
}
|
||||
|
||||
private func makeMain() {
|
||||
let stackHeight: CGFloat = 22
|
||||
let vertical: NSStackView = NSStackView(frame: NSRect(x: 0, y: TabHeight - stackHeight*3 - 4, width: TabWidth, height: stackHeight*3))
|
||||
vertical.orientation = .vertical
|
||||
|
||||
let level: NSStackView = NSStackView(frame: NSRect(x: 11, y: stackHeight*2, width: TabWidth - 19, height: stackHeight))
|
||||
level.orientation = .horizontal
|
||||
level.distribution = .equalCentering
|
||||
let levelLabel = LabelField(string: "Level")
|
||||
self.levelValue = ValueField(string: "0 %")
|
||||
level.addView(levelLabel, in: .center)
|
||||
level.addView(self.levelValue, in: .center)
|
||||
|
||||
let source: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*1, width: TabWidth - 20, height: stackHeight))
|
||||
source.orientation = .horizontal
|
||||
source.distribution = .equalCentering
|
||||
let sourceLabel = LabelField(string: "Source")
|
||||
self.sourceValue = ValueField(string: "AC Power")
|
||||
source.addView(sourceLabel, in: .center)
|
||||
source.addView(self.sourceValue, in: .center)
|
||||
|
||||
let time: NSStackView = NSStackView(frame: NSRect(x: 10, y: 0, width: TabWidth - 20, height: stackHeight))
|
||||
time.orientation = .horizontal
|
||||
time.distribution = .equalCentering
|
||||
self.timeLabel = LabelField(string: "Time to charge")
|
||||
self.timeValue = ValueField(string: "Calculating")
|
||||
time.addView(self.timeLabel, in: .center)
|
||||
time.addView(self.timeValue, in: .center)
|
||||
|
||||
vertical.addSubview(level)
|
||||
vertical.addSubview(source)
|
||||
vertical.addSubview(time)
|
||||
|
||||
self.popup.view.view?.addSubview(vertical)
|
||||
}
|
||||
|
||||
private func makeOverview() {
|
||||
let overviewLabel: NSView = NSView(frame: NSRect(x: 0, y: TabHeight - 102, width: TabWidth, height: 25))
|
||||
|
||||
overviewLabel.wantsLayer = true
|
||||
overviewLabel.layer?.backgroundColor = NSColor(hexString: "#eeeeee", alpha: 0.5).cgColor
|
||||
|
||||
let overviewText: NSTextField = NSTextField(string: "Overview")
|
||||
overviewText.frame = NSRect(x: 0, y: 0, width: TabWidth, height: overviewLabel.frame.size.height - 4)
|
||||
overviewText.isEditable = false
|
||||
overviewText.isSelectable = false
|
||||
overviewText.isBezeled = false
|
||||
overviewText.wantsLayer = true
|
||||
overviewText.textColor = .darkGray
|
||||
overviewText.canDrawSubviewsIntoLayer = true
|
||||
overviewText.alignment = .center
|
||||
overviewText.backgroundColor = NSColor(hexString: "#dddddd", alpha: 0)
|
||||
overviewText.font = NSFont.systemFont(ofSize: 12, weight: .medium)
|
||||
|
||||
overviewLabel.addSubview(overviewText)
|
||||
self.popup.view.view?.addSubview(overviewLabel)
|
||||
|
||||
let stackHeight: CGFloat = 22
|
||||
let vertical: NSStackView = NSStackView(frame: NSRect(x: 0, y: 184, width: TabWidth, height: stackHeight*3))
|
||||
vertical.orientation = .vertical
|
||||
|
||||
let cycles: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*2, width: TabWidth - 20, height: stackHeight))
|
||||
cycles.orientation = .horizontal
|
||||
cycles.distribution = .equalCentering
|
||||
let cyclesLabel = LabelField(string: "Cycles")
|
||||
self.cyclesValue = ValueField(string: "0")
|
||||
cycles.addView(cyclesLabel, in: .center)
|
||||
cycles.addView(self.cyclesValue, in: .center)
|
||||
|
||||
let health: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*1, width: TabWidth - 20, height: stackHeight))
|
||||
health.orientation = .horizontal
|
||||
health.distribution = .equalCentering
|
||||
let healthLabel = LabelField(string: "Health")
|
||||
self.healthValue = ValueField(string: "Calculating")
|
||||
health.addView(healthLabel, in: .center)
|
||||
health.addView(self.healthValue, in: .center)
|
||||
|
||||
let state: NSStackView = NSStackView(frame: NSRect(x: 10, y: 0, width: TabWidth - 20, height: stackHeight))
|
||||
state.orientation = .horizontal
|
||||
state.distribution = .equalCentering
|
||||
let stateLabel = LabelField(string: "State")
|
||||
self.stateValue = ValueField(string: "Calculating")
|
||||
state.addView(stateLabel, in: .center)
|
||||
state.addView(self.stateValue, in: .center)
|
||||
|
||||
vertical.addSubview(cycles)
|
||||
vertical.addSubview(health)
|
||||
vertical.addSubview(state)
|
||||
|
||||
self.popup.view.view?.addSubview(vertical)
|
||||
}
|
||||
|
||||
private func makeBattery() {
|
||||
let batteryLabel: NSView = NSView(frame: NSRect(x: 0, y: TabHeight - 202, width: TabWidth, height: 25))
|
||||
|
||||
batteryLabel.wantsLayer = true
|
||||
batteryLabel.layer?.backgroundColor = NSColor(hexString: "#eeeeee", alpha: 0.5).cgColor
|
||||
|
||||
let overviewText: NSTextField = NSTextField(string: "Battery")
|
||||
overviewText.frame = NSRect(x: 0, y: 0, width: TabWidth, height: batteryLabel.frame.size.height - 4)
|
||||
overviewText.isEditable = false
|
||||
overviewText.isSelectable = false
|
||||
overviewText.isBezeled = false
|
||||
overviewText.wantsLayer = true
|
||||
overviewText.textColor = .darkGray
|
||||
overviewText.canDrawSubviewsIntoLayer = true
|
||||
overviewText.alignment = .center
|
||||
overviewText.backgroundColor = NSColor(hexString: "#dddddd", alpha: 0)
|
||||
overviewText.font = NSFont.systemFont(ofSize: 12, weight: .medium)
|
||||
|
||||
batteryLabel.addSubview(overviewText)
|
||||
self.popup.view.view?.addSubview(batteryLabel)
|
||||
|
||||
let stackHeight: CGFloat = 22
|
||||
let vertical: NSStackView = NSStackView(frame: NSRect(x: 0, y: TabHeight - 273, width: TabWidth, height: stackHeight*3))
|
||||
vertical.orientation = .vertical
|
||||
|
||||
let amperage: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*2, width: TabWidth - 20, height: stackHeight))
|
||||
amperage.orientation = .horizontal
|
||||
amperage.distribution = .equalCentering
|
||||
let amperageLabel = LabelField(string: "Amperage")
|
||||
self.amperageValue = ValueField(string: "0 mA")
|
||||
amperage.addView(amperageLabel, in: .center)
|
||||
amperage.addView(self.amperageValue, in: .center)
|
||||
|
||||
let voltage: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*1, width: TabWidth - 20, height: stackHeight))
|
||||
voltage.orientation = .horizontal
|
||||
voltage.distribution = .equalCentering
|
||||
let voltageLabel = LabelField(string: "Voltage")
|
||||
self.voltageValue = ValueField(string: "0 V")
|
||||
voltage.addView(voltageLabel, in: .center)
|
||||
voltage.addView(self.voltageValue, in: .center)
|
||||
|
||||
let temperature: NSStackView = NSStackView(frame: NSRect(x: 10, y: 0, width: TabWidth - 20, height: stackHeight))
|
||||
temperature.orientation = .horizontal
|
||||
temperature.distribution = .equalCentering
|
||||
let temperatureLabel = LabelField(string: "Temperature")
|
||||
self.temperatureValue = ValueField(string: "0 °C")
|
||||
temperature.addView(temperatureLabel, in: .center)
|
||||
temperature.addView(self.temperatureValue, in: .center)
|
||||
|
||||
vertical.addSubview(amperage)
|
||||
vertical.addSubview(voltage)
|
||||
vertical.addSubview(temperature)
|
||||
|
||||
self.popup.view.view?.addSubview(vertical)
|
||||
}
|
||||
|
||||
private func makePowerAdapter() {
|
||||
let powerAdapterLabel: NSView = NSView(frame: NSRect(x: 0, y: 52, width: TabWidth, height: 25))
|
||||
|
||||
powerAdapterLabel.wantsLayer = true
|
||||
powerAdapterLabel.layer?.backgroundColor = NSColor(hexString: "#eeeeee", alpha: 0.5).cgColor
|
||||
|
||||
let overviewText: NSTextField = NSTextField(string: "Power adapter")
|
||||
overviewText.frame = NSRect(x: 0, y: 0, width: TabWidth, height: powerAdapterLabel.frame.size.height - 4)
|
||||
overviewText.isEditable = false
|
||||
overviewText.isSelectable = false
|
||||
overviewText.isBezeled = false
|
||||
overviewText.wantsLayer = true
|
||||
overviewText.textColor = .darkGray
|
||||
overviewText.canDrawSubviewsIntoLayer = true
|
||||
overviewText.alignment = .center
|
||||
overviewText.backgroundColor = NSColor(hexString: "#dddddd", alpha: 0)
|
||||
overviewText.font = NSFont.systemFont(ofSize: 12, weight: .medium)
|
||||
|
||||
powerAdapterLabel.addSubview(overviewText)
|
||||
self.popup.view.view?.addSubview(powerAdapterLabel)
|
||||
|
||||
let stackHeight: CGFloat = 22
|
||||
let vertical: NSStackView = NSStackView(frame: NSRect(x: 0, y: 4, width: TabWidth, height: stackHeight*2))
|
||||
vertical.orientation = .vertical
|
||||
|
||||
let power: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*1, width: TabWidth - 20, height: stackHeight))
|
||||
power.orientation = .horizontal
|
||||
power.distribution = .equalCentering
|
||||
let powerLabel = LabelField(string: "Power")
|
||||
self.powerValue = ValueField(string: "0 W")
|
||||
power.addView(powerLabel, in: .center)
|
||||
power.addView(self.powerValue, in: .center)
|
||||
|
||||
let charging: NSStackView = NSStackView(frame: NSRect(x: 10, y: 0, width: TabWidth - 20, height: stackHeight))
|
||||
charging.orientation = .horizontal
|
||||
charging.distribution = .equalCentering
|
||||
let chargingLabel = LabelField(string: "Is charging")
|
||||
self.chargingValue = ValueField(string: "No")
|
||||
charging.addView(chargingLabel, in: .center)
|
||||
charging.addView(self.chargingValue, in: .center)
|
||||
|
||||
vertical.addSubview(power)
|
||||
vertical.addSubview(charging)
|
||||
|
||||
self.popup.view.view?.addSubview(vertical)
|
||||
}
|
||||
|
||||
public func popupUpdater(value: BatteryUsage) {
|
||||
if !self.popup.active && self.popup.initialized { return }
|
||||
self.popup.initialized = true
|
||||
|
||||
// makeMain
|
||||
self.levelValue.stringValue = "\(Int(abs(value.level) * 100)) %"
|
||||
self.sourceValue.stringValue = value.powerSource
|
||||
if value.powerSource == "Battery Power" {
|
||||
self.timeLabel.stringValue = "Time to discharge"
|
||||
if value.timeToEmpty != -1 && value.timeToEmpty != 0 {
|
||||
self.timeValue.stringValue = Double(value.timeToEmpty*60).printSecondsToHoursMinutesSeconds()
|
||||
}
|
||||
} else {
|
||||
self.timeLabel.stringValue = "Time to charge"
|
||||
if value.timeToCharge != -1 && value.timeToCharge != 0 {
|
||||
self.timeValue.stringValue = Double(value.timeToCharge*60).printSecondsToHoursMinutesSeconds()
|
||||
}
|
||||
}
|
||||
|
||||
if value.timeToEmpty == -1 || value.timeToEmpty == -1 {
|
||||
self.timeValue.stringValue = "Calculating"
|
||||
}
|
||||
|
||||
if value.isCharged {
|
||||
self.timeValue.stringValue = "Fully charged"
|
||||
}
|
||||
|
||||
// makeOverview
|
||||
self.cyclesValue.stringValue = "\(value.cycles)"
|
||||
self.stateValue.stringValue = value.state
|
||||
self.healthValue.stringValue = "\(value.health) %"
|
||||
|
||||
// makeBattery
|
||||
self.amperageValue.stringValue = "\(abs(value.amperage)) mA"
|
||||
self.voltageValue.stringValue = "\(value.voltage.roundTo(decimalPlaces: 2)) V"
|
||||
self.temperatureValue.stringValue = "\(value.temperature) °C"
|
||||
|
||||
// makePowerAdapter
|
||||
self.powerValue.stringValue = value.powerSource == "Battery Power" ? "Not connected" : "\(value.ACwatts) W"
|
||||
self.chargingValue.stringValue = value.level > 0 ? "Yes" : "No"
|
||||
}
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
//
|
||||
// BatteryReader.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 14/01/2020.
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import IOKit.ps
|
||||
|
||||
struct BatteryUsage {
|
||||
var powerSource: String = ""
|
||||
var state: String = ""
|
||||
var isCharged: Bool = false
|
||||
var level: Double = 0
|
||||
var cycles: Int = 0
|
||||
var health: Int = 0
|
||||
|
||||
var amperage: Int = 0
|
||||
var voltage: Double = 0
|
||||
var temperature: Double = 0
|
||||
|
||||
var ACwatts: Int = 0
|
||||
var ACstatus: Bool = false
|
||||
|
||||
var timeToEmpty: Int = 0
|
||||
var timeToCharge: Int = 0
|
||||
}
|
||||
|
||||
class BatteryReader: Reader {
|
||||
public var name: String = "Battery"
|
||||
public var enabled: Bool = true
|
||||
public var available: Bool {
|
||||
get {
|
||||
if !self.internalChecked {
|
||||
let snapshot = IOPSCopyPowerSourcesInfo().takeRetainedValue()
|
||||
let sources = IOPSCopyPowerSourcesList(snapshot).takeRetainedValue() as Array
|
||||
self.hasInternalBattery = sources.count > 0
|
||||
self.internalChecked = true
|
||||
}
|
||||
return self.hasInternalBattery
|
||||
}
|
||||
}
|
||||
public var optional: Bool = false
|
||||
public var initialized: Bool = false
|
||||
public var callback: (BatteryUsage) -> Void = {_ in}
|
||||
|
||||
private var service: io_connect_t = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("AppleSmartBattery"))
|
||||
private var internalChecked: Bool = false
|
||||
private var hasInternalBattery: Bool = false
|
||||
|
||||
private var source: CFRunLoopSource?
|
||||
private var loop: CFRunLoop?
|
||||
|
||||
init(_ updater: @escaping (BatteryUsage) -> Void) {
|
||||
self.callback = updater
|
||||
self.read()
|
||||
}
|
||||
|
||||
public func start() {
|
||||
let context = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
|
||||
|
||||
source = IOPSNotificationCreateRunLoopSource({ (context) in
|
||||
guard let ctx = context else {
|
||||
return
|
||||
}
|
||||
|
||||
let watcher = Unmanaged<BatteryReader>.fromOpaque(ctx).takeUnretainedValue()
|
||||
watcher.read()
|
||||
}, context).takeRetainedValue()
|
||||
|
||||
loop = RunLoop.current.getCFRunLoop()
|
||||
CFRunLoopAddSource(loop, source, .defaultMode)
|
||||
}
|
||||
|
||||
public func stop() {
|
||||
guard let runLoop = loop, let source = source else {
|
||||
return
|
||||
}
|
||||
|
||||
CFRunLoopRemoveSource(runLoop, source, .defaultMode)
|
||||
}
|
||||
|
||||
public func read() {
|
||||
if !self.enabled && self.initialized { return }
|
||||
self.initialized = true
|
||||
|
||||
let psInfo = IOPSCopyPowerSourcesInfo().takeRetainedValue()
|
||||
let psList = IOPSCopyPowerSourcesList(psInfo).takeRetainedValue() as [CFTypeRef]
|
||||
|
||||
for ps in psList {
|
||||
if let list = IOPSGetPowerSourceDescription(psInfo, ps).takeUnretainedValue() as? Dictionary<String, Any> {
|
||||
let powerSource = list[kIOPSPowerSourceStateKey] as? String ?? "AC Power"
|
||||
let state = list[kIOPSBatteryHealthKey] as! String
|
||||
let isCharged = list[kIOPSIsChargedKey] as? Bool ?? false
|
||||
var cap = Float(list[kIOPSCurrentCapacityKey] as! Int) / 100
|
||||
|
||||
let timeToEmpty = Int(list[kIOPSTimeToEmptyKey] as! Int)
|
||||
let timeToCharged = Int(list[kIOPSTimeToFullChargeKey] as! Int)
|
||||
|
||||
let cycles = self.getIntValue("CycleCount" as CFString) ?? 0
|
||||
|
||||
let maxCapacity = self.getIntValue("MaxCapacity" as CFString) ?? 1
|
||||
let designCapacity = self.getIntValue("DesignCapacity" as CFString) ?? 1
|
||||
|
||||
let amperage = self.getIntValue("Amperage" as CFString) ?? 0
|
||||
let voltage = self.getVoltage() ?? 0
|
||||
let temperature = self.getTemperature() ?? 0
|
||||
|
||||
var ACwatts: Int = 0
|
||||
if let ACDetails = IOPSCopyExternalPowerAdapterDetails() {
|
||||
if let ACList = ACDetails.takeUnretainedValue() as? Dictionary<String, Any> {
|
||||
guard let watts = ACList[kIOPSPowerAdapterWattsKey] else {
|
||||
return
|
||||
}
|
||||
ACwatts = Int(watts as! Int)
|
||||
}
|
||||
}
|
||||
let ACstatus = self.getBoolValue("IsCharging" as CFString) ?? false
|
||||
|
||||
if powerSource == "Battery Power" {
|
||||
cap = 0 - cap
|
||||
}
|
||||
|
||||
DispatchQueue.main.async(execute: {
|
||||
let usage = BatteryUsage(
|
||||
powerSource: powerSource,
|
||||
state: state,
|
||||
isCharged: isCharged,
|
||||
level: Double(cap),
|
||||
cycles: cycles,
|
||||
health: (100 * maxCapacity) / designCapacity,
|
||||
|
||||
amperage: amperage,
|
||||
voltage: voltage,
|
||||
temperature: temperature,
|
||||
|
||||
ACwatts: ACwatts,
|
||||
ACstatus: ACstatus,
|
||||
|
||||
timeToEmpty: timeToEmpty,
|
||||
timeToCharge: timeToCharged
|
||||
)
|
||||
self.callback(usage)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func toggleEnable(_ value: Bool) {
|
||||
self.enabled = value
|
||||
}
|
||||
|
||||
private func getBoolValue(_ forIdentifier: CFString) -> Bool? {
|
||||
if let value = IORegistryEntryCreateCFProperty(self.service, forIdentifier, kCFAllocatorDefault, 0) {
|
||||
return value.takeRetainedValue() as? Bool
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func getIntValue(_ identifier: CFString) -> Int? {
|
||||
if let value = IORegistryEntryCreateCFProperty(self.service, identifier, kCFAllocatorDefault, 0) {
|
||||
return value.takeRetainedValue() as? Int
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func getDoubleValue(_ identifier: CFString) -> Double? {
|
||||
if let value = IORegistryEntryCreateCFProperty(self.service, identifier, kCFAllocatorDefault, 0) {
|
||||
return value.takeRetainedValue() as? Double
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func getVoltage() -> Double? {
|
||||
if let value = self.getDoubleValue("Voltage" as CFString) {
|
||||
return value / 1000.0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func getTemperature() -> Double? {
|
||||
if let value = IORegistryEntryCreateCFProperty(self.service, "Temperature" as CFString, kCFAllocatorDefault, 0) {
|
||||
return value.takeRetainedValue() as! Double / 100.0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
//
|
||||
// CPU.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 01.06.2019.
|
||||
// Copyright © 2019 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import Charts
|
||||
import Repeat
|
||||
|
||||
class CPU: Module {
|
||||
public var name: String = "CPU"
|
||||
public var updateInterval: Double = 1
|
||||
|
||||
public var enabled: Bool = true
|
||||
public var available: Bool = true
|
||||
|
||||
public var readers: [Reader] = []
|
||||
public var task: Repeater?
|
||||
|
||||
public var widget: ModuleWidget = ModuleWidget()
|
||||
public var popup: ModulePopup = ModulePopup(true)
|
||||
public var menu: NSMenuItem = NSMenuItem()
|
||||
|
||||
internal let defaults = UserDefaults.standard
|
||||
internal var submenu: NSMenu = NSMenu()
|
||||
|
||||
internal var systemValue: NSTextField = NSTextField()
|
||||
internal var userValue: NSTextField = NSTextField()
|
||||
internal var idleValue: NSTextField = NSTextField()
|
||||
internal var processViewList: [NSStackView] = []
|
||||
internal var chart: LineChartView = LineChartView()
|
||||
|
||||
init() {
|
||||
if !self.available { return }
|
||||
|
||||
self.enabled = defaults.object(forKey: name) != nil ? defaults.bool(forKey: name) : true
|
||||
self.updateInterval = defaults.object(forKey: "\(name)_interval") != nil ? defaults.double(forKey: "\(name)_interval") : self.updateInterval
|
||||
self.widget.type = defaults.object(forKey: "\(name)_widget") != nil ? defaults.float(forKey: "\(name)_widget") : Widgets.Mini
|
||||
|
||||
self.initWidget()
|
||||
self.initMenu()
|
||||
self.initPopup()
|
||||
|
||||
readers.append(CPULoadReader(self.name, self.loadUpdater, self.chartUpdater, true))
|
||||
readers.append(CPUUsageReader(self.usageUpdater))
|
||||
readers.append(CPUProcessReader(self.processesUpdater))
|
||||
|
||||
self.task = Repeater.init(interval: .seconds(self.updateInterval), observer: { _ in
|
||||
self.readers.forEach { reader in
|
||||
reader.read()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func start() {
|
||||
if self.task != nil && self.task!.state.isRunning == false {
|
||||
self.task!.start()
|
||||
}
|
||||
}
|
||||
|
||||
public func stop() {
|
||||
if self.task!.state.isRunning {
|
||||
self.task?.pause()
|
||||
}
|
||||
}
|
||||
|
||||
public func restart () {
|
||||
self.stop()
|
||||
self.start()
|
||||
}
|
||||
|
||||
private func loadUpdater(value: [Double]) {
|
||||
if !value.isEmpty && self.widget.view is Widget {
|
||||
(self.widget.view as! Widget).setValue(data: value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
//
|
||||
// CPUUsageReader.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 13/01/2020.
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
class CPULoadReader: Reader {
|
||||
public var name: String = "Load"
|
||||
public var enabled: Bool = true
|
||||
public var available: Bool = true
|
||||
public var optional: Bool = false
|
||||
public var initialized: Bool = false
|
||||
public var callback: ([Double]) -> Void = {_ in}
|
||||
public var chartCallback: (Double) -> Void = {_ in}
|
||||
|
||||
public var perCoreMode: Bool = true
|
||||
public var hyperthreading: Bool = false
|
||||
|
||||
private var cpuInfo: processor_info_array_t!
|
||||
private var prevCpuInfo: processor_info_array_t?
|
||||
private var numCpuInfo: mach_msg_type_number_t = 0
|
||||
private var numPrevCpuInfo: mach_msg_type_number_t = 0
|
||||
private var numCPUs: uint = 0
|
||||
private let CPUUsageLock: NSLock = NSLock()
|
||||
private var loadPrevious = host_cpu_load_info()
|
||||
|
||||
init(_ name: String, _ updater: @escaping ([Double]) -> Void, _ chartUpdater: @escaping (Double) -> Void, _ coreMode: Bool = false) {
|
||||
self.callback = updater
|
||||
self.chartCallback = chartUpdater
|
||||
self.perCoreMode = coreMode
|
||||
self.hyperthreading = UserDefaults.standard.object(forKey: "\(name)_hyperthreading") != nil ? UserDefaults.standard.bool(forKey: "\(name)_hyperthreading") : false
|
||||
|
||||
let mibKeys: [Int32] = [ CTL_HW, HW_NCPU ]
|
||||
|
||||
mibKeys.withUnsafeBufferPointer() { mib in
|
||||
var sizeOfNumCPUs: size_t = MemoryLayout<uint>.size
|
||||
let status = sysctl(processor_info_array_t(mutating: mib.baseAddress), 2, &numCPUs, &sizeOfNumCPUs, nil, 0)
|
||||
if status != 0 {
|
||||
numCPUs = 1
|
||||
}
|
||||
}
|
||||
|
||||
if self.available {
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
self.read()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func toggleEnable(_ value: Bool) {
|
||||
self.enabled = value
|
||||
}
|
||||
|
||||
public func read() {
|
||||
if !self.enabled && self.initialized { return }
|
||||
|
||||
var numCPUsU: natural_t = 0
|
||||
let err: kern_return_t = host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO, &numCPUsU, &cpuInfo, &numCpuInfo);
|
||||
|
||||
if err == KERN_SUCCESS {
|
||||
CPUUsageLock.lock()
|
||||
|
||||
var inUseOnAllCores: Int32 = 0
|
||||
var totalOnAllCores: Int32 = 0
|
||||
var usagePerCore: [Double] = []
|
||||
|
||||
var incrementNumber = 1
|
||||
if !self.hyperthreading && self.perCoreMode {
|
||||
incrementNumber = 2
|
||||
}
|
||||
|
||||
for i in stride(from: 0, to: Int32(numCPUs), by: incrementNumber) {
|
||||
var inUse: Int32
|
||||
var total: Int32
|
||||
if let prevCpuInfo = prevCpuInfo {
|
||||
inUse = cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_USER)]
|
||||
- prevCpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_USER)]
|
||||
+ cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_SYSTEM)]
|
||||
- prevCpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_SYSTEM)]
|
||||
+ cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_NICE)]
|
||||
- prevCpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_NICE)]
|
||||
total = inUse + (cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_IDLE)]
|
||||
- prevCpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_IDLE)])
|
||||
} else {
|
||||
inUse = cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_USER)]
|
||||
+ cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_SYSTEM)]
|
||||
+ cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_NICE)]
|
||||
total = inUse + cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_IDLE)]
|
||||
}
|
||||
|
||||
inUseOnAllCores = inUseOnAllCores + inUse
|
||||
totalOnAllCores = totalOnAllCores + total
|
||||
if total != 0 {
|
||||
usagePerCore.append(Double(inUse) / Double(total))
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async(execute: {
|
||||
if self.perCoreMode {
|
||||
self.callback(usagePerCore)
|
||||
} else {
|
||||
self.callback([(Double(inUseOnAllCores) / Double(totalOnAllCores))])
|
||||
}
|
||||
self.chartCallback(Double(inUseOnAllCores) / Double(totalOnAllCores))
|
||||
})
|
||||
|
||||
CPUUsageLock.unlock()
|
||||
|
||||
if let prevCpuInfo = prevCpuInfo {
|
||||
let prevCpuInfoSize: size_t = MemoryLayout<integer_t>.stride * Int(numPrevCpuInfo)
|
||||
vm_deallocate(mach_task_self_, vm_address_t(bitPattern: prevCpuInfo), vm_size_t(prevCpuInfoSize))
|
||||
}
|
||||
|
||||
prevCpuInfo = cpuInfo
|
||||
numPrevCpuInfo = numCpuInfo
|
||||
|
||||
cpuInfo = nil
|
||||
numCpuInfo = 0
|
||||
} else {
|
||||
print("Error KERN_SUCCESS!")
|
||||
}
|
||||
}
|
||||
|
||||
private func hostCPULoadInfo() -> host_cpu_load_info? {
|
||||
let HOST_CPU_LOAD_INFO_COUNT = MemoryLayout<host_cpu_load_info>.stride/MemoryLayout<integer_t>.stride
|
||||
var size = mach_msg_type_number_t(HOST_CPU_LOAD_INFO_COUNT)
|
||||
var cpuLoadInfo = host_cpu_load_info()
|
||||
|
||||
let result = withUnsafeMutablePointer(to: &cpuLoadInfo) {
|
||||
$0.withMemoryRebound(to: integer_t.self, capacity: HOST_CPU_LOAD_INFO_COUNT) {
|
||||
host_statistics(mach_host_self(), HOST_CPU_LOAD_INFO, $0, &size)
|
||||
}
|
||||
}
|
||||
if result != KERN_SUCCESS {
|
||||
print("Error - \(#file): \(#function) - kern_result_t = \(result)")
|
||||
return nil
|
||||
}
|
||||
return cpuLoadInfo
|
||||
}
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
//
|
||||
// CPUMenu.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 13/01/2020.
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
extension CPU {
|
||||
public func initMenu() {
|
||||
self.menu = NSMenuItem(title: name, action: #selector(toggle), keyEquivalent: "")
|
||||
self.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
|
||||
|
||||
let mini = NSMenuItem(title: "Mini", action: #selector(toggleWidget), keyEquivalent: "")
|
||||
mini.state = self.widget.type == Widgets.Mini ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
mini.target = self
|
||||
|
||||
let chart = NSMenuItem(title: "Chart", action: #selector(toggleWidget), keyEquivalent: "")
|
||||
chart.state = self.widget.type == 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.widget.type == Widgets.ChartWithValue ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
chartWithValue.target = self
|
||||
|
||||
let barChart = NSMenuItem(title: "Bar chart", action: #selector(toggleWidget), keyEquivalent: "")
|
||||
barChart.state = self.widget.type == Widgets.BarChart ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
barChart.target = self
|
||||
|
||||
let hyperthreading = NSMenuItem(title: "Hyperthreading", action: #selector(toggleHyperthreading), keyEquivalent: "")
|
||||
let hyper = UserDefaults.standard.object(forKey: "\(name)_hyperthreading") != nil ? UserDefaults.standard.bool(forKey: "\(name)_hyperthreading") : false
|
||||
hyperthreading.state = hyper ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
hyperthreading.target = self
|
||||
|
||||
submenu.addItem(mini)
|
||||
submenu.addItem(chart)
|
||||
submenu.addItem(chartWithValue)
|
||||
submenu.addItem(barChart)
|
||||
|
||||
submenu.addItem(NSMenuItem.separator())
|
||||
|
||||
if let view = self.widget.view as? Widget {
|
||||
for widgetMenu in view.menus {
|
||||
submenu.addItem(widgetMenu)
|
||||
}
|
||||
}
|
||||
|
||||
if self.widget.type == Widgets.BarChart {
|
||||
submenu.addItem(hyperthreading)
|
||||
}
|
||||
|
||||
submenu.addItem(NSMenuItem.separator())
|
||||
submenu.addItem(generateIntervalMenu())
|
||||
|
||||
if self.enabled {
|
||||
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.enabled = state
|
||||
menuBar!.reload(name: self.name)
|
||||
|
||||
if !state {
|
||||
menu.submenu = nil
|
||||
} else {
|
||||
menu.submenu = submenu
|
||||
}
|
||||
|
||||
self.restart()
|
||||
}
|
||||
|
||||
@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
|
||||
case "Bar chart":
|
||||
widgetCode = Widgets.BarChart
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if widgetCode == Widgets.BarChart {
|
||||
self.readers.forEach { reader in
|
||||
if reader is CPULoadReader {
|
||||
(reader as! CPULoadReader).perCoreMode = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.readers.filter{ $0 is CPULoadReader }.forEach { reader in
|
||||
(reader as! CPULoadReader).perCoreMode = false
|
||||
}
|
||||
}
|
||||
|
||||
if self.widget.type == widgetCode {
|
||||
return
|
||||
}
|
||||
|
||||
for item in self.submenu.items {
|
||||
if item.title == "Mini" || item.title == "Chart" || item.title == "Chart with value" || item.title == "Bar chart" {
|
||||
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.self.widget.type = widgetCode
|
||||
self.initWidget()
|
||||
self.initMenu()
|
||||
menuBar!.reload(name: self.name)
|
||||
}
|
||||
|
||||
@objc func toggleHyperthreading(_ sender: NSMenuItem) {
|
||||
sender.state = sender.state == NSControl.StateValue.on ? NSControl.StateValue.off : NSControl.StateValue.on
|
||||
self.defaults.set(sender.state == NSControl.StateValue.on, forKey: "\(name)_hyperthreading")
|
||||
self.readers.filter{ $0 is CPULoadReader }.forEach { reader in
|
||||
(reader as! CPULoadReader).hyperthreading = sender.state == NSControl.StateValue.on
|
||||
}
|
||||
}
|
||||
|
||||
private func generateIntervalMenu() -> NSMenuItem {
|
||||
let updateInterval = NSMenuItem(title: "Update interval", action: nil, keyEquivalent: "")
|
||||
|
||||
let updateIntervals = NSMenu()
|
||||
let updateInterval_1 = NSMenuItem(title: "1s", action: #selector(changeInterval), keyEquivalent: "")
|
||||
updateInterval_1.state = self.updateInterval == 1 ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
updateInterval_1.target = self
|
||||
let updateInterval_2 = NSMenuItem(title: "3s", action: #selector(changeInterval), keyEquivalent: "")
|
||||
updateInterval_2.state = self.updateInterval == 3 ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
updateInterval_2.target = self
|
||||
let updateInterval_3 = NSMenuItem(title: "5s", action: #selector(changeInterval), keyEquivalent: "")
|
||||
updateInterval_3.state = self.updateInterval == 5 ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
updateInterval_3.target = self
|
||||
let updateInterval_4 = NSMenuItem(title: "10s", action: #selector(changeInterval), keyEquivalent: "")
|
||||
updateInterval_4.state = self.updateInterval == 10 ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
updateInterval_4.target = self
|
||||
let updateInterval_5 = NSMenuItem(title: "15s", action: #selector(changeInterval), keyEquivalent: "")
|
||||
updateInterval_5.state = self.updateInterval == 15 ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
updateInterval_5.target = self
|
||||
|
||||
updateIntervals.addItem(updateInterval_1)
|
||||
updateIntervals.addItem(updateInterval_2)
|
||||
updateIntervals.addItem(updateInterval_3)
|
||||
updateIntervals.addItem(updateInterval_4)
|
||||
updateIntervals.addItem(updateInterval_5)
|
||||
|
||||
updateInterval.submenu = updateIntervals
|
||||
|
||||
return updateInterval
|
||||
}
|
||||
|
||||
@objc func changeInterval(_ sender: NSMenuItem) {
|
||||
var interval: Double = self.updateInterval
|
||||
|
||||
switch sender.title {
|
||||
case "1s":
|
||||
interval = 1
|
||||
case "3s":
|
||||
interval = 3
|
||||
case "5s":
|
||||
interval = 5
|
||||
case "10s":
|
||||
interval = 10
|
||||
case "15s":
|
||||
interval = 15
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
if interval == self.updateInterval {
|
||||
return
|
||||
}
|
||||
|
||||
for item in self.submenu.items {
|
||||
if item.title == "Update interval" {
|
||||
for subitem in item.submenu!.items {
|
||||
subitem.state = NSControl.StateValue.off
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sender.state = NSControl.StateValue.on
|
||||
self.updateInterval = interval
|
||||
self.defaults.set(interval, forKey: "\(name)_interval")
|
||||
self.task?.reset(.seconds(interval), restart: self.task!.state.isRunning)
|
||||
}
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
//
|
||||
// CPUPopup.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 03/09/2019.
|
||||
// Copyright © 2019 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import Charts
|
||||
|
||||
extension CPU {
|
||||
public func initPopup() {
|
||||
self.popup.view.view?.frame = NSRect(x: 0, y: 0, width: TabWidth, height: TabHeight)
|
||||
|
||||
makeChart()
|
||||
makeOverview()
|
||||
makeProcesses()
|
||||
}
|
||||
|
||||
private func makeChart() {
|
||||
let lineColor: NSColor = NSColor(red: (26/255.0), green: (126/255.0), blue: (252/255.0), alpha: 1.0)
|
||||
let gradientColor: NSColor = NSColor(red: (26/255.0), green: (126/255.0), blue: (252/255.0), alpha: 0.5)
|
||||
|
||||
self.chart = LineChartView(frame: CGRect(x: 0, y: TabHeight - 110, width: TabWidth, height: 102))
|
||||
self.chart.animate(xAxisDuration: 2.0, yAxisDuration: 2.0, easingOption: .easeInCubic)
|
||||
self.chart.backgroundColor = .white
|
||||
self.chart.noDataText = "No \(self.name) usage data"
|
||||
self.chart.legend.enabled = false
|
||||
self.chart.scaleXEnabled = false
|
||||
self.chart.scaleYEnabled = false
|
||||
self.chart.pinchZoomEnabled = false
|
||||
self.chart.doubleTapToZoomEnabled = false
|
||||
self.chart.drawBordersEnabled = false
|
||||
self.chart.autoScaleMinMaxEnabled = true
|
||||
|
||||
self.chart.rightAxis.enabled = false
|
||||
|
||||
self.chart.leftAxis.axisMinimum = 0
|
||||
self.chart.leftAxis.axisMaximum = 100
|
||||
self.chart.leftAxis.labelCount = 6
|
||||
self.chart.leftAxis.drawGridLinesEnabled = false
|
||||
self.chart.leftAxis.drawAxisLineEnabled = false
|
||||
|
||||
self.chart.leftAxis.gridColor = NSColor(red:220/255, green:220/255, blue:220/255, alpha:1)
|
||||
self.chart.leftAxis.gridLineWidth = 0.5
|
||||
self.chart.leftAxis.drawGridLinesEnabled = true
|
||||
self.chart.leftAxis.labelTextColor = NSColor(red:150/255, green:150/255, blue:150/255, alpha:1)
|
||||
|
||||
self.chart.xAxis.drawAxisLineEnabled = false
|
||||
self.chart.xAxis.drawLimitLinesBehindDataEnabled = false
|
||||
self.chart.xAxis.gridLineWidth = 0.5
|
||||
self.chart.xAxis.drawGridLinesEnabled = false
|
||||
self.chart.xAxis.drawLabelsEnabled = false
|
||||
|
||||
let marker = ChartMarker()
|
||||
marker.chartView = self.chart
|
||||
self.chart.marker = marker
|
||||
|
||||
var lineChartEntry = [ChartDataEntry]()
|
||||
lineChartEntry.append(ChartDataEntry(x: 0, y: 0))
|
||||
let chartDataSet = LineChartDataSet(entries: lineChartEntry, label: "\(self.name) Usage")
|
||||
chartDataSet.drawCirclesEnabled = false
|
||||
chartDataSet.mode = .cubicBezier
|
||||
chartDataSet.cubicIntensity = 0.1
|
||||
chartDataSet.colors = [lineColor]
|
||||
chartDataSet.fillColor = gradientColor
|
||||
chartDataSet.drawFilledEnabled = true
|
||||
|
||||
let data = LineChartData()
|
||||
data.addDataSet(chartDataSet)
|
||||
data.setDrawValues(false)
|
||||
|
||||
self.chart.data = LineChartData(dataSet: chartDataSet)
|
||||
self.popup.view.view?.addSubview(self.chart)
|
||||
}
|
||||
|
||||
public func chartUpdater(value: Double) {
|
||||
if self.chart.data == nil { return }
|
||||
|
||||
let v: Double = Double((value * 100).roundTo(decimalPlaces: 2))!
|
||||
|
||||
let index = Double((self.chart.data?.getDataSetByIndex(0)?.entryCount)!)
|
||||
self.chart.data?.addEntry(ChartDataEntry(x: index, y: v), dataSetIndex: 0)
|
||||
|
||||
if index > 120 {
|
||||
self.chart.xAxis.axisMinimum = index - 120
|
||||
}
|
||||
self.chart.xAxis.axisMaximum = index
|
||||
|
||||
if self.popup.active {
|
||||
self.chart.notifyDataSetChanged()
|
||||
self.chart.moveViewToX(index)
|
||||
}
|
||||
}
|
||||
|
||||
private func makeOverview() {
|
||||
let overviewLabel: NSView = NSView(frame: NSRect(x: 0, y: TabHeight - 140, width: TabWidth, height: 25))
|
||||
|
||||
overviewLabel.wantsLayer = true
|
||||
overviewLabel.layer?.backgroundColor = NSColor(hexString: "#eeeeee", alpha: 0.5).cgColor
|
||||
|
||||
let overviewText: NSTextField = NSTextField(string: "Overview")
|
||||
overviewText.frame = NSRect(x: 0, y: 0, width: TabWidth, height: overviewLabel.frame.size.height - 4)
|
||||
overviewText.isEditable = false
|
||||
overviewText.isSelectable = false
|
||||
overviewText.isBezeled = false
|
||||
overviewText.wantsLayer = true
|
||||
overviewText.textColor = .darkGray
|
||||
overviewText.canDrawSubviewsIntoLayer = true
|
||||
overviewText.alignment = .center
|
||||
overviewText.backgroundColor = NSColor(hexString: "#dddddd", alpha: 0)
|
||||
overviewText.font = NSFont.systemFont(ofSize: 12, weight: .medium)
|
||||
|
||||
overviewLabel.addSubview(overviewText)
|
||||
self.popup.view.view?.addSubview(overviewLabel)
|
||||
|
||||
let stackHeight: CGFloat = 22
|
||||
let vertical: NSStackView = NSStackView(frame: NSRect(x: 0, y: 147, width: TabWidth, height: stackHeight*3))
|
||||
vertical.orientation = .vertical
|
||||
|
||||
let system: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*2, width: TabWidth - 20, height: stackHeight))
|
||||
system.orientation = .horizontal
|
||||
system.distribution = .equalCentering
|
||||
let systemLabel = LabelField(string: "System")
|
||||
self.systemValue = ValueField(string: "0 %")
|
||||
system.addView(systemLabel, in: .center)
|
||||
system.addView(self.systemValue, in: .center)
|
||||
|
||||
let user: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*1, width: TabWidth - 20, height: stackHeight))
|
||||
user.orientation = .horizontal
|
||||
user.distribution = .equalCentering
|
||||
let userLabel = LabelField(string: "User")
|
||||
self.userValue = ValueField(string: "0 %")
|
||||
user.addView(userLabel, in: .center)
|
||||
user.addView(self.userValue, in: .center)
|
||||
|
||||
let idle: NSStackView = NSStackView(frame: NSRect(x: 10, y: 0, width: TabWidth - 20, height: stackHeight))
|
||||
idle.orientation = .horizontal
|
||||
idle.distribution = .equalCentering
|
||||
let idleLabel = LabelField(string: "Idle")
|
||||
self.idleValue = ValueField(string: "0 %")
|
||||
idle.addView(idleLabel, in: .center)
|
||||
idle.addView(self.idleValue, in: .center)
|
||||
|
||||
vertical.addSubview(system)
|
||||
vertical.addSubview(user)
|
||||
vertical.addSubview(idle)
|
||||
|
||||
self.popup.view.view?.addSubview(vertical)
|
||||
}
|
||||
|
||||
public func usageUpdater(value: CPUUsage) {
|
||||
if !self.popup.active && self.popup.initialized { return }
|
||||
|
||||
self.systemValue.stringValue = "\(value.system.roundTo(decimalPlaces: 2)) %"
|
||||
self.userValue.stringValue = "\(value.user.roundTo(decimalPlaces: 2)) %"
|
||||
self.idleValue.stringValue = "\(value.idle.roundTo(decimalPlaces: 2)) %"
|
||||
}
|
||||
|
||||
private func makeProcesses() {
|
||||
let label: NSView = NSView(frame: NSRect(x: 0, y: 0, width: TabWidth, height: 25))
|
||||
|
||||
label.wantsLayer = true
|
||||
label.layer?.backgroundColor = NSColor(hexString: "#eeeeee", alpha: 0.5).cgColor
|
||||
|
||||
let text: NSTextField = NSTextField(string: "Top Processes")
|
||||
text.frame = NSRect(x: 0, y: 0, width: TabWidth, height: label.frame.size.height - 4)
|
||||
text.isEditable = false
|
||||
text.isSelectable = false
|
||||
text.isBezeled = false
|
||||
text.wantsLayer = true
|
||||
text.textColor = .darkGray
|
||||
text.canDrawSubviewsIntoLayer = true
|
||||
text.alignment = .center
|
||||
text.backgroundColor = NSColor(hexString: "#dddddd", alpha: 0)
|
||||
text.font = NSFont.systemFont(ofSize: 12, weight: .medium)
|
||||
|
||||
label.addSubview(text)
|
||||
self.popup.view.view?.addSubview(label)
|
||||
|
||||
let stackHeight: CGFloat = 22
|
||||
let vertical: NSStackView = NSStackView(frame: NSRect(x: 0, y: 4, width: TabWidth, height: stackHeight*5))
|
||||
vertical.orientation = .vertical
|
||||
vertical.distribution = .fill
|
||||
|
||||
self.processViewList = []
|
||||
let process_1 = makeProcessView(num: 4, height: stackHeight, label: "", value: "")
|
||||
let process_2 = makeProcessView(num: 3, height: stackHeight, label: "", value: "")
|
||||
let process_3 = makeProcessView(num: 2, height: stackHeight, label: "", value: "")
|
||||
let process_4 = makeProcessView(num: 1, height: stackHeight, label: "", value: "")
|
||||
let process_5 = makeProcessView(num: 0, height: stackHeight, label: "", value: "")
|
||||
|
||||
self.processViewList.append(process_1)
|
||||
self.processViewList.append(process_2)
|
||||
self.processViewList.append(process_3)
|
||||
self.processViewList.append(process_4)
|
||||
self.processViewList.append(process_5)
|
||||
|
||||
vertical.addSubview(process_1)
|
||||
vertical.addSubview(process_2)
|
||||
vertical.addSubview(process_3)
|
||||
vertical.addSubview(process_4)
|
||||
vertical.addSubview(process_5)
|
||||
self.popup.view.view?.addSubview(vertical)
|
||||
|
||||
label.frame = NSRect(x: 0, y: vertical.frame.origin.y + vertical.frame.size.height + 2, width: TabWidth, height: 25)
|
||||
self.popup.view.view?.addSubview(label)
|
||||
}
|
||||
|
||||
public func processesUpdater(value: [TopProcess]) {
|
||||
if self.processViewList.isEmpty || !self.popup.active && self.popup.initialized { return }
|
||||
self.popup.initialized = true
|
||||
|
||||
for (i, process) in value.enumerated() {
|
||||
if i < 5 {
|
||||
let processView = self.processViewList[i]
|
||||
|
||||
(processView.subviews[0] as! NSTextField).stringValue = process.command
|
||||
(processView.subviews[1] as! NSTextField).stringValue = "\(process.usage.roundTo(decimalPlaces: 2)) %"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func makeProcessView(num: Int, height: CGFloat, label: String, value: String) -> NSStackView {
|
||||
let view: NSStackView = NSStackView(frame: NSRect(x: 10, y: CGFloat(num)*height, width: TabWidth - 20, height: height))
|
||||
view.orientation = .horizontal
|
||||
view.distribution = .equalCentering
|
||||
let viewLabel = LabelField(string: label)
|
||||
let viewValue = ValueField(string: value)
|
||||
view.addView(viewLabel, in: .center)
|
||||
view.addView(viewValue, in: .center)
|
||||
|
||||
return view
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
//
|
||||
// CPUProcessReader.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 13/01/2020.
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
struct TopProcess {
|
||||
var pid: Int = 0
|
||||
var command: String = ""
|
||||
var usage: Double = 0
|
||||
}
|
||||
|
||||
class CPUProcessReader: Reader {
|
||||
public var name: String = "Process"
|
||||
public var enabled: Bool = false
|
||||
public var available: Bool = true
|
||||
public var optional: Bool = true
|
||||
public var initialized: Bool = false
|
||||
public var callback: ([TopProcess]) -> Void = {_ in}
|
||||
|
||||
private var loadPrevious = host_cpu_load_info()
|
||||
|
||||
init(_ updater: @escaping ([TopProcess]) -> Void) {
|
||||
self.callback = updater
|
||||
if self.available {
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
self.read()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func toggleEnable(_ value: Bool) {
|
||||
self.enabled = value
|
||||
}
|
||||
|
||||
public func read() {
|
||||
if !self.enabled && self.initialized { return }
|
||||
self.initialized = true
|
||||
|
||||
let task = Process()
|
||||
task.launchPath = "/bin/ps"
|
||||
task.arguments = ["-Aceo pid,pcpu,comm", "-r"]
|
||||
|
||||
let outputPipe = Pipe()
|
||||
let errorPipe = Pipe()
|
||||
|
||||
task.standardOutput = outputPipe
|
||||
task.standardError = errorPipe
|
||||
|
||||
do {
|
||||
try task.run()
|
||||
} catch let error {
|
||||
print(error)
|
||||
return
|
||||
}
|
||||
|
||||
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let output = String(decoding: outputData, as: UTF8.self)
|
||||
_ = String(decoding: errorData, as: UTF8.self)
|
||||
|
||||
if output.isEmpty {
|
||||
return
|
||||
}
|
||||
|
||||
var index = 0
|
||||
var processes: [TopProcess] = []
|
||||
output.enumerateLines { (line, stop) -> () in
|
||||
if index != 0 {
|
||||
var str = line.trimmingCharacters(in: .whitespaces)
|
||||
let pidString = str.findAndCrop(pattern: "^\\d+")
|
||||
let usageString = str.findAndCrop(pattern: "^[0-9,.]+ ")
|
||||
let command = str.trimmingCharacters(in: .whitespaces)
|
||||
|
||||
let pid = Int(pidString) ?? 0
|
||||
let usage = Double(usageString.replacingOccurrences(of: ",", with: ".")) ?? 0
|
||||
|
||||
processes.append(TopProcess(pid: pid, command: command, usage: usage))
|
||||
}
|
||||
|
||||
if index == 5 { stop = true }
|
||||
index += 1
|
||||
}
|
||||
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.callback(processes)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
//
|
||||
// CPUUsageReader.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 13/01/2020.
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
struct CPUUsage {
|
||||
var system: Double = 0
|
||||
var user: Double = 0
|
||||
var idle: Double = 0
|
||||
}
|
||||
|
||||
class CPUUsageReader: Reader {
|
||||
public var name: String = "Usage"
|
||||
public var enabled: Bool = false
|
||||
public var available: Bool = true
|
||||
public var optional: Bool = true
|
||||
public var initialized: Bool = false
|
||||
public var callback: (CPUUsage) -> Void = {_ in}
|
||||
|
||||
private var loadPrevious = host_cpu_load_info()
|
||||
|
||||
init(_ updater: @escaping (CPUUsage) -> Void) {
|
||||
self.callback = updater
|
||||
if self.available {
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
self.read()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func toggleEnable(_ value: Bool) {
|
||||
self.enabled = value
|
||||
}
|
||||
|
||||
public func read() {
|
||||
if !self.enabled && self.initialized { return }
|
||||
|
||||
let load = hostCPULoadInfo()
|
||||
let userDiff = Double(load!.cpu_ticks.0 - loadPrevious.cpu_ticks.0)
|
||||
let sysDiff = Double(load!.cpu_ticks.1 - loadPrevious.cpu_ticks.1)
|
||||
let idleDiff = Double(load!.cpu_ticks.2 - loadPrevious.cpu_ticks.2)
|
||||
let niceDiff = Double(load!.cpu_ticks.3 - loadPrevious.cpu_ticks.3)
|
||||
|
||||
let totalTicks = sysDiff + userDiff + niceDiff + idleDiff
|
||||
|
||||
let sys = sysDiff / totalTicks * 100.0
|
||||
let user = userDiff / totalTicks * 100.0
|
||||
let idle = idleDiff / totalTicks * 100.0
|
||||
|
||||
self.loadPrevious = load!
|
||||
self.initialized = true
|
||||
|
||||
if !sys.isNaN && !user.isNaN && !idle.isNaN {
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.callback(CPUUsage(system: sys, user: user, idle: idle))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func hostCPULoadInfo() -> host_cpu_load_info? {
|
||||
let HOST_CPU_LOAD_INFO_COUNT = MemoryLayout<host_cpu_load_info>.stride/MemoryLayout<integer_t>.stride
|
||||
var size = mach_msg_type_number_t(HOST_CPU_LOAD_INFO_COUNT)
|
||||
var cpuLoadInfo = host_cpu_load_info()
|
||||
|
||||
let result = withUnsafeMutablePointer(to: &cpuLoadInfo) {
|
||||
$0.withMemoryRebound(to: integer_t.self, capacity: HOST_CPU_LOAD_INFO_COUNT) {
|
||||
host_statistics(mach_host_self(), HOST_CPU_LOAD_INFO, $0, &size)
|
||||
}
|
||||
}
|
||||
if result != KERN_SUCCESS {
|
||||
print("Error - \(#file): \(#function) - kern_result_t = \(result)")
|
||||
return nil
|
||||
}
|
||||
|
||||
return cpuLoadInfo
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
//
|
||||
// Disk.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 14/01/2020.
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import Repeat
|
||||
|
||||
class Disk: Module {
|
||||
public var name: String = "SSD"
|
||||
public var updateInterval: Double = 5
|
||||
|
||||
public var enabled: Bool = true
|
||||
public var available: Bool = true
|
||||
|
||||
public var readers: [Reader] = []
|
||||
public var task: Repeater?
|
||||
|
||||
public var widget: ModuleWidget = ModuleWidget()
|
||||
public var popup: ModulePopup = ModulePopup(false)
|
||||
public var menu: NSMenuItem = NSMenuItem()
|
||||
|
||||
internal let defaults = UserDefaults.standard
|
||||
internal var submenu: NSMenu = NSMenu()
|
||||
internal var selectedDisk: String = ""
|
||||
internal var disks: disksList = disksList()
|
||||
|
||||
init() {
|
||||
if !self.available { return }
|
||||
|
||||
self.enabled = defaults.object(forKey: name) != nil ? defaults.bool(forKey: name) : true
|
||||
self.updateInterval = defaults.object(forKey: "\(name)_interval") != nil ? defaults.double(forKey: "\(name)_interval") : self.updateInterval
|
||||
self.widget.type = defaults.object(forKey: "\(name)_widget") != nil ? defaults.float(forKey: "\(name)_widget") : Widgets.Mini
|
||||
self.selectedDisk = defaults.object(forKey: "\(name)_disk") != nil ? defaults.string(forKey: "\(name)_disk")! : self.selectedDisk
|
||||
|
||||
self.initWidget()
|
||||
self.initMenu()
|
||||
|
||||
readers.append(DiskCapacityReader(self.usageUpdater))
|
||||
|
||||
self.task = Repeater.init(interval: .seconds(self.updateInterval), observer: { _ in
|
||||
self.readers.forEach { reader in
|
||||
reader.read()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func start() {
|
||||
if self.task != nil && self.task!.state.isRunning == false {
|
||||
self.task!.start()
|
||||
}
|
||||
}
|
||||
|
||||
public func stop() {
|
||||
if self.task!.state.isRunning {
|
||||
self.task?.pause()
|
||||
}
|
||||
}
|
||||
|
||||
public func restart() {
|
||||
self.stop()
|
||||
self.start()
|
||||
}
|
||||
|
||||
private func usageUpdater(disks: disksList) {
|
||||
if self.disks.list.count != disks.list.count && disks.list.count != 0 {
|
||||
self.disks = disks
|
||||
self.initMenu()
|
||||
}
|
||||
|
||||
if self.widget.view is Widget {
|
||||
var d: diskInfo? = disks.getDiskByBSDName(self.selectedDisk)
|
||||
if d == nil {
|
||||
d = disks.getRootDisk()
|
||||
}
|
||||
|
||||
if d != nil {
|
||||
let total = d!.totalSize
|
||||
let free = d!.freeSize
|
||||
let usedSpace = total - free
|
||||
let percentage = Double(usedSpace) / Double(total)
|
||||
|
||||
(self.widget.view as! Widget).setValue(data: [percentage])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
//
|
||||
// DiskMenu.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 14/01/2020.
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
extension Disk {
|
||||
public 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
|
||||
|
||||
if self.disks.list.count > 1 {
|
||||
self.disks.list.forEach { (d: diskInfo) in
|
||||
let disk = NSMenuItem(title: d.name, action: #selector(toggleDisk), keyEquivalent: "")
|
||||
if self.selectedDisk == "" && d.root {
|
||||
disk.state = NSControl.StateValue.on
|
||||
} else {
|
||||
disk.state = self.selectedDisk == d.mediaBSDName ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
}
|
||||
disk.target = self
|
||||
|
||||
submenu.addItem(disk)
|
||||
}
|
||||
}
|
||||
|
||||
let mini = NSMenuItem(title: "Mini", action: #selector(toggleWidget), keyEquivalent: "")
|
||||
mini.state = self.widget.type == Widgets.Mini ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
mini.target = self
|
||||
|
||||
let barChart = NSMenuItem(title: "Bar chart", action: #selector(toggleWidget), keyEquivalent: "")
|
||||
barChart.state = self.widget.type == Widgets.BarChart ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
barChart.target = self
|
||||
|
||||
submenu.addItem(NSMenuItem.separator())
|
||||
|
||||
submenu.addItem(mini)
|
||||
submenu.addItem(barChart)
|
||||
|
||||
submenu.addItem(NSMenuItem.separator())
|
||||
|
||||
if let view = self.widget.view as? Widget {
|
||||
for widgetMenu in view.menus {
|
||||
submenu.addItem(widgetMenu)
|
||||
}
|
||||
}
|
||||
|
||||
submenu.addItem(NSMenuItem.separator())
|
||||
submenu.addItem(generateIntervalMenu())
|
||||
|
||||
if self.enabled {
|
||||
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.enabled = state
|
||||
menuBar!.reload(name: self.name)
|
||||
|
||||
if !state {
|
||||
menu.submenu = nil
|
||||
} else {
|
||||
menu.submenu = submenu
|
||||
}
|
||||
|
||||
self.restart()
|
||||
}
|
||||
|
||||
@objc func toggleWidget(_ sender: NSMenuItem) {
|
||||
var widgetCode: Float = 0.0
|
||||
|
||||
switch sender.title {
|
||||
case "Mini":
|
||||
widgetCode = Widgets.Mini
|
||||
case "Bar chart":
|
||||
widgetCode = Widgets.BarChart
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if self.widget.type == widgetCode {
|
||||
return
|
||||
}
|
||||
|
||||
for item in self.submenu.items {
|
||||
if item.title == "Mini" || item.title == "Bar chart" {
|
||||
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.widget.type = widgetCode
|
||||
self.initWidget()
|
||||
self.initMenu()
|
||||
menuBar!.reload(name: self.name)
|
||||
}
|
||||
|
||||
private func generateIntervalMenu() -> NSMenuItem {
|
||||
let updateInterval = NSMenuItem(title: "Update interval", action: nil, keyEquivalent: "")
|
||||
|
||||
let updateIntervals = NSMenu()
|
||||
let updateInterval_1 = NSMenuItem(title: "1s", action: #selector(changeInterval), keyEquivalent: "")
|
||||
updateInterval_1.state = self.updateInterval == 1 ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
updateInterval_1.target = self
|
||||
let updateInterval_2 = NSMenuItem(title: "3s", action: #selector(changeInterval), keyEquivalent: "")
|
||||
updateInterval_2.state = self.updateInterval == 3 ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
updateInterval_2.target = self
|
||||
let updateInterval_3 = NSMenuItem(title: "5s", action: #selector(changeInterval), keyEquivalent: "")
|
||||
updateInterval_3.state = self.updateInterval == 5 ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
updateInterval_3.target = self
|
||||
let updateInterval_4 = NSMenuItem(title: "10s", action: #selector(changeInterval), keyEquivalent: "")
|
||||
updateInterval_4.state = self.updateInterval == 10 ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
updateInterval_4.target = self
|
||||
let updateInterval_5 = NSMenuItem(title: "15s", action: #selector(changeInterval), keyEquivalent: "")
|
||||
updateInterval_5.state = self.updateInterval == 15 ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
updateInterval_5.target = self
|
||||
|
||||
updateIntervals.addItem(updateInterval_1)
|
||||
updateIntervals.addItem(updateInterval_2)
|
||||
updateIntervals.addItem(updateInterval_3)
|
||||
updateIntervals.addItem(updateInterval_4)
|
||||
updateIntervals.addItem(updateInterval_5)
|
||||
|
||||
updateInterval.submenu = updateIntervals
|
||||
|
||||
return updateInterval
|
||||
}
|
||||
|
||||
@objc func changeInterval(_ sender: NSMenuItem) {
|
||||
var interval: Double = self.updateInterval
|
||||
|
||||
switch sender.title {
|
||||
case "1s":
|
||||
interval = 1
|
||||
case "3s":
|
||||
interval = 3
|
||||
case "5s":
|
||||
interval = 5
|
||||
case "10s":
|
||||
interval = 10
|
||||
case "15s":
|
||||
interval = 15
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if interval == self.updateInterval {
|
||||
return
|
||||
}
|
||||
|
||||
for item in self.submenu.items {
|
||||
if item.title == "Update interval" {
|
||||
for subitem in item.submenu!.items {
|
||||
subitem.state = NSControl.StateValue.off
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sender.state = NSControl.StateValue.on
|
||||
self.updateInterval = interval
|
||||
self.defaults.set(interval, forKey: "\(name)_interval")
|
||||
self.task?.reset(.seconds(interval), restart: self.task!.state.isRunning)
|
||||
}
|
||||
|
||||
@objc func toggleDisk(_ sender: NSMenuItem) {
|
||||
let name: String = sender.title
|
||||
let d: diskInfo? = self.disks.getDiskByName(name)
|
||||
if d == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if d!.mediaBSDName == self.selectedDisk {
|
||||
return
|
||||
}
|
||||
|
||||
for item in self.submenu.items {
|
||||
if self.disks.getDiskByName(item.title) != nil {
|
||||
item.state = NSControl.StateValue.off
|
||||
}
|
||||
}
|
||||
|
||||
sender.state = NSControl.StateValue.on
|
||||
self.selectedDisk = d!.mediaBSDName
|
||||
self.defaults.set(d!.mediaBSDName, forKey: "\(name)_disk")
|
||||
menuBar!.reload(name: self.name)
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
//
|
||||
// Module.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 08.07.2019.
|
||||
// Copyright © 2019 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import Charts
|
||||
import Repeat
|
||||
|
||||
protocol Module: class {
|
||||
var name: String { get } // module name
|
||||
|
||||
var enabled: Bool { get } // determine if module is enabled or disabled
|
||||
var available: Bool { get } // determine if module is available on this PC
|
||||
|
||||
var widget: ModuleWidget { get set } // view for widget
|
||||
var menu: NSMenuItem { get } // view for menu
|
||||
var popup: ModulePopup { get set } // popup
|
||||
|
||||
var readers: [Reader] { get } // list of readers available for module
|
||||
var task: Repeater? { get set } // reader cron task
|
||||
|
||||
func start() // start module internal processes
|
||||
func stop() // stop module internal processes
|
||||
func restart() // restart module internal processes
|
||||
|
||||
func initWidget()
|
||||
}
|
||||
|
||||
protocol Reader {
|
||||
var name: String { get } // reader name
|
||||
var enabled: Bool { get set } // determine if reader is enabled or disabled
|
||||
var available: Bool { get } // determine if reader is available on this PC
|
||||
var optional: Bool { get } // say if reader are optional (additional information)
|
||||
var initialized: Bool { get } // to check if first read already done
|
||||
|
||||
func read() // make one read
|
||||
|
||||
func toggleEnable(_ value: Bool) -> Void // enable/disable optional reader
|
||||
}
|
||||
|
||||
struct ModulePopup {
|
||||
var available: Bool = true // say if module have popup view
|
||||
var view: NSTabViewItem = NSTabViewItem() // module popup view
|
||||
var active: Bool = false // indicate that popup is opened and selected this view
|
||||
var initialized: Bool = false // allows to set some value when on first load
|
||||
|
||||
init(_ a: Bool = true) {
|
||||
available = a
|
||||
}
|
||||
|
||||
mutating func setActive(_ state: Bool) {
|
||||
if self.active != state {
|
||||
self.active = state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ModuleWidget {
|
||||
var type: WidgetType = Widgets.Mini // determine a widget typ
|
||||
var view: NSView = NSView() // widget view
|
||||
|
||||
init(_ t: WidgetType = Widgets.Mini) {
|
||||
type = t
|
||||
}
|
||||
}
|
||||
|
||||
extension Module {
|
||||
func initWidget() {
|
||||
var widget: Widget = Mini()
|
||||
|
||||
switch self.widget.type {
|
||||
case Widgets.Mini:
|
||||
widget = Mini()
|
||||
case Widgets.Sensors:
|
||||
widget = SensorsWidget()
|
||||
case Widgets.Chart:
|
||||
widget = Chart()
|
||||
case Widgets.ChartWithValue:
|
||||
widget = ChartWithValue()
|
||||
case Widgets.NetworkDots:
|
||||
widget = NetworkDotsView()
|
||||
case Widgets.NetworkArrows:
|
||||
widget = NetworkArrowsView()
|
||||
case Widgets.NetworkText:
|
||||
widget = NetworkTextView()
|
||||
case Widgets.NetworkDotsWithText:
|
||||
widget = NetworkDotsTextView()
|
||||
case Widgets.NetworkArrowsWithText:
|
||||
widget = NetworkArrowsTextView()
|
||||
case Widgets.BarChart:
|
||||
widget = BarChart()
|
||||
case Widgets.Battery:
|
||||
widget = BatteryWidget()
|
||||
case Widgets.BatteryPercentage:
|
||||
widget = BatteryPercentageWidget()
|
||||
case Widgets.BatteryTime:
|
||||
widget = BatteryTimeWidget()
|
||||
default:
|
||||
widget = Mini()
|
||||
}
|
||||
|
||||
widget.name = self.name
|
||||
widget.start()
|
||||
|
||||
self.readers.forEach { reader in
|
||||
reader.read()
|
||||
}
|
||||
|
||||
self.widget.view = widget as! NSView
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
//
|
||||
// Network.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 14/01/2020.
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import Charts
|
||||
import Repeat
|
||||
|
||||
class Network: Module {
|
||||
public var name: String = "Network"
|
||||
public var updateInterval: Double = 1
|
||||
|
||||
public var enabled: Bool = true
|
||||
public var available: Bool = true
|
||||
|
||||
public var readers: [Reader] = []
|
||||
public var task: Repeater?
|
||||
|
||||
public var widget: ModuleWidget = ModuleWidget()
|
||||
public var popup: ModulePopup = ModulePopup(true)
|
||||
public var menu: NSMenuItem = NSMenuItem()
|
||||
|
||||
internal let defaults = UserDefaults.standard
|
||||
internal var submenu: NSMenu = NSMenu()
|
||||
internal var chart: LineChartView = LineChartView()
|
||||
|
||||
internal var publicIPValue: NSTextField = NSTextField()
|
||||
internal var localIPValue: NSTextField = NSTextField()
|
||||
internal var networkValue: NSTextField = NSTextField()
|
||||
internal var physicalValue: NSTextField = NSTextField()
|
||||
internal var downloadValue: NSTextField = NSTextField()
|
||||
internal var uploadValue: NSTextField = NSTextField()
|
||||
internal var totalDownloadValue: NSTextField = NSTextField()
|
||||
internal var totalUploadValue: NSTextField = NSTextField()
|
||||
|
||||
init() {
|
||||
if !self.available { return }
|
||||
|
||||
self.enabled = defaults.object(forKey: name) != nil ? defaults.bool(forKey: name) : true
|
||||
self.widget.type = defaults.object(forKey: "\(name)_widget") != nil ? defaults.float(forKey: "\(name)_widget") : Widgets.NetworkDots
|
||||
|
||||
self.initWidget()
|
||||
self.initMenu()
|
||||
self.initPopup()
|
||||
|
||||
readers.append(NetworkReader(self.usageUpdater))
|
||||
readers.append(NetworkInterfaceReader(self.overviewUpdater))
|
||||
|
||||
self.task = Repeater.init(interval: .seconds(self.updateInterval), observer: { _ in
|
||||
self.readers.forEach { reader in
|
||||
reader.read()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func start() {
|
||||
if self.task != nil && self.task!.state.isRunning == false {
|
||||
self.task!.start()
|
||||
}
|
||||
}
|
||||
|
||||
public func stop() {
|
||||
if self.task!.state.isRunning {
|
||||
self.task?.pause()
|
||||
}
|
||||
}
|
||||
|
||||
public func restart() {
|
||||
self.stop()
|
||||
self.start()
|
||||
}
|
||||
|
||||
private func usageUpdater(value: NetworkUsage) {
|
||||
self.dataUpdater(value: value)
|
||||
self.chartUpdater(value: value)
|
||||
|
||||
if self.widget.view is Widget {
|
||||
(self.widget.view as! Widget).setValue(data: [Double(value.download), Double(value.upload)])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
//
|
||||
// NetworkInterfaceReader.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 22/01/2020.
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import CoreWLAN
|
||||
import SystemConfiguration
|
||||
import Reachability
|
||||
|
||||
struct NetworkInterface {
|
||||
var active: Bool
|
||||
|
||||
var localIP: String?
|
||||
var publicIP: String?
|
||||
var countryCode: String?
|
||||
|
||||
var networkType: String?
|
||||
var macAddress: String?
|
||||
var wifiName: String?
|
||||
|
||||
var force: Bool = false
|
||||
|
||||
init(
|
||||
active: Bool = false,
|
||||
localIP: String? = nil,
|
||||
publicIP: String? = nil,
|
||||
countryCode: String? = nil,
|
||||
networkType: String? = nil,
|
||||
macAddress: String? = nil,
|
||||
wifiName: String? = nil,
|
||||
force: Bool = false
|
||||
) {
|
||||
self.active = active
|
||||
|
||||
self.localIP = localIP
|
||||
self.publicIP = publicIP
|
||||
self.countryCode = countryCode
|
||||
|
||||
self.networkType = networkType
|
||||
self.macAddress = macAddress
|
||||
self.wifiName = wifiName
|
||||
|
||||
self.force = force
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkInterfaceReader: Reader {
|
||||
public var name: String = "Interface"
|
||||
public var enabled: Bool = false
|
||||
public var available: Bool = true
|
||||
public var optional: Bool = true
|
||||
public var initialized: Bool = false
|
||||
public var callback: (NetworkInterface) -> Void = {_ in}
|
||||
|
||||
private var uploadValue: Int64 = 0
|
||||
private var downloadValue: Int64 = 0
|
||||
|
||||
private var publicIP: String? = nil
|
||||
private var reachability: Reachability? = nil
|
||||
private var forceRead: Bool = false
|
||||
|
||||
private var repeatCounter: Int8 = 0
|
||||
|
||||
init(_ updater: @escaping (NetworkInterface) -> Void) {
|
||||
do {
|
||||
self.reachability = try Reachability()
|
||||
} catch let error {
|
||||
print("initialize Reachability \(error)")
|
||||
}
|
||||
self.callback = updater
|
||||
|
||||
if self.available {
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
self.read()
|
||||
}
|
||||
}
|
||||
|
||||
if self.reachability != nil {
|
||||
self.reachability!.whenReachable = { reachability in
|
||||
self.repeatCounter = 0
|
||||
self.forceRead = true
|
||||
self.read()
|
||||
}
|
||||
self.reachability!.whenUnreachable = { _ in
|
||||
self.forceRead = true
|
||||
self.read()
|
||||
}
|
||||
|
||||
do {
|
||||
try self.reachability!.startNotifier()
|
||||
} catch {
|
||||
print("Unable to start notifier")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func read() {
|
||||
if (!self.enabled && self.initialized && !self.forceRead) || self.reachability == nil { return }
|
||||
self.initialized = true
|
||||
|
||||
var result = NetworkInterface(active: false)
|
||||
result.force = self.forceRead
|
||||
if self.forceRead {
|
||||
self.forceRead = false
|
||||
}
|
||||
|
||||
if self.reachability!.connection != .unavailable && isConnectedToNetwork() {
|
||||
if self.publicIP == nil {
|
||||
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 3, execute: {
|
||||
if self.repeatCounter < 5 {
|
||||
self.publicIP = self.getPublicIP()
|
||||
self.forceRead = true
|
||||
self.read()
|
||||
self.repeatCounter += 1
|
||||
} else {
|
||||
self.publicIP = "Unknown"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
result.active = true
|
||||
|
||||
if self.reachability!.connection == .wifi && CWWiFiClient.shared().interface() != nil {
|
||||
result.networkType = "Wi-Fi"
|
||||
result.wifiName = CWWiFiClient.shared().interface()!.ssid()
|
||||
result.countryCode = CWWiFiClient.shared().interface()!.countryCode()
|
||||
result.macAddress = CWWiFiClient.shared().interface()!.hardwareAddress()
|
||||
} else {
|
||||
result.networkType = "Ethernet"
|
||||
result.macAddress = getMacAddress()
|
||||
}
|
||||
|
||||
result.localIP = getLocalIP()
|
||||
result.publicIP = publicIP
|
||||
} else {
|
||||
self.publicIP = nil
|
||||
}
|
||||
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.callback(result)
|
||||
})
|
||||
}
|
||||
|
||||
private func isWIFIActive() -> Bool {
|
||||
guard let interfaceNames = CWWiFiClient.interfaceNames() else {
|
||||
return false
|
||||
}
|
||||
|
||||
for interfaceName in interfaceNames {
|
||||
let interface = CWWiFiClient.shared().interface(withName: interfaceName)
|
||||
|
||||
if interface?.ssid() != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/31835418/how-to-get-mac-address-from-os-x-with-swift
|
||||
private func getMacAddress() -> String? {
|
||||
var macAddressAsString : String?
|
||||
if let intfIterator = FindEthernetInterfaces() {
|
||||
if let macAddress = GetMACAddress(intfIterator) {
|
||||
macAddressAsString = macAddress.map( { String(format:"%02x", $0) } ).joined(separator: ":")
|
||||
}
|
||||
IOObjectRelease(intfIterator)
|
||||
}
|
||||
return macAddressAsString
|
||||
}
|
||||
|
||||
private func FindEthernetInterfaces() -> io_iterator_t? {
|
||||
let matchingDictUM = IOServiceMatching("IOEthernetInterface");
|
||||
if matchingDictUM == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
let matchingDict = matchingDictUM! as NSMutableDictionary
|
||||
matchingDict["IOPropertyMatch"] = [ "IOPrimaryInterface" : true]
|
||||
|
||||
var matchingServices : io_iterator_t = 0
|
||||
if IOServiceGetMatchingServices(kIOMasterPortDefault, matchingDict, &matchingServices) != KERN_SUCCESS {
|
||||
return nil
|
||||
}
|
||||
|
||||
return matchingServices
|
||||
}
|
||||
|
||||
private func GetMACAddress(_ intfIterator : io_iterator_t) -> [UInt8]? {
|
||||
var macAddress : [UInt8]?
|
||||
var intfService = IOIteratorNext(intfIterator)
|
||||
|
||||
while intfService != 0 {
|
||||
var controllerService : io_object_t = 0
|
||||
if IORegistryEntryGetParentEntry(intfService, kIOServicePlane, &controllerService) == KERN_SUCCESS {
|
||||
let dataUM = IORegistryEntryCreateCFProperty(controllerService, "IOMACAddress" as CFString, kCFAllocatorDefault, 0)
|
||||
if dataUM != nil {
|
||||
let data = (dataUM!.takeRetainedValue() as! CFData) as Data
|
||||
macAddress = [0, 0, 0, 0, 0, 0]
|
||||
data.copyBytes(to: &macAddress!, count: macAddress!.count)
|
||||
}
|
||||
IOObjectRelease(controllerService)
|
||||
}
|
||||
|
||||
IOObjectRelease(intfService)
|
||||
intfService = IOIteratorNext(intfIterator)
|
||||
}
|
||||
|
||||
return macAddress
|
||||
}
|
||||
|
||||
private func getPublicIP() -> String? {
|
||||
let url = URL(string: "https://api.ipify.org")
|
||||
var address: String? = nil
|
||||
|
||||
do {
|
||||
if let url = url {
|
||||
address = try String(contentsOf: url)
|
||||
if address!.contains("<") {
|
||||
address = nil
|
||||
}
|
||||
}
|
||||
} catch let error {
|
||||
print("get public ip \(error)")
|
||||
}
|
||||
|
||||
return address
|
||||
}
|
||||
|
||||
private func getLocalIP() -> String {
|
||||
var address: String = ""
|
||||
|
||||
// Get list of all interfaces on the local machine:
|
||||
var ifaddr : UnsafeMutablePointer<ifaddrs>?
|
||||
guard getifaddrs(&ifaddr) == 0 else { return "" }
|
||||
guard let firstAddr = ifaddr else { return "" }
|
||||
|
||||
// For each interface ...
|
||||
for ifptr in sequence(first: firstAddr, next: { $0.pointee.ifa_next }) {
|
||||
let interface = ifptr.pointee
|
||||
|
||||
// Check for IPv4 or IPv6 interface:
|
||||
let addrFamily = interface.ifa_addr.pointee.sa_family
|
||||
if addrFamily == UInt8(AF_INET) || addrFamily == UInt8(AF_INET6) {
|
||||
|
||||
// Check interface name:
|
||||
let name = String(cString: interface.ifa_name)
|
||||
if name == "en0" {
|
||||
|
||||
// Convert interface address to a human readable string:
|
||||
var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
|
||||
getnameinfo(interface.ifa_addr, socklen_t(interface.ifa_addr.pointee.sa_len),
|
||||
&hostname, socklen_t(hostname.count),
|
||||
nil, socklen_t(0), NI_NUMERICHOST)
|
||||
address = String(cString: hostname)
|
||||
} else if name == "en1" {
|
||||
// Convert interface address to a human readable string:
|
||||
var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
|
||||
getnameinfo(interface.ifa_addr, socklen_t(interface.ifa_addr.pointee.sa_len),
|
||||
&hostname, socklen_t(hostname.count),
|
||||
nil, socklen_t(1), NI_NUMERICHOST)
|
||||
address = String(cString: hostname)
|
||||
}
|
||||
}
|
||||
}
|
||||
freeifaddrs(ifaddr)
|
||||
|
||||
return address
|
||||
}
|
||||
|
||||
public func toggleEnable(_ value: Bool) {
|
||||
self.enabled = value
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
//
|
||||
// NetworkMenu.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 14/01/2020.
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
extension Network {
|
||||
public 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
|
||||
|
||||
let dots = NSMenuItem(title: "Dots", action: #selector(toggleWidget), keyEquivalent: "")
|
||||
dots.state = self.widget.type == Widgets.NetworkDots ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
dots.target = self
|
||||
|
||||
let arrows = NSMenuItem(title: "Arrows", action: #selector(toggleWidget), keyEquivalent: "")
|
||||
arrows.state = self.widget.type == Widgets.NetworkArrows ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
arrows.target = self
|
||||
|
||||
let text = NSMenuItem(title: "Text", action: #selector(toggleWidget), keyEquivalent: "")
|
||||
text.state = self.widget.type == Widgets.NetworkText ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
text.target = self
|
||||
|
||||
let dotsWithText = NSMenuItem(title: "Dots with text", action: #selector(toggleWidget), keyEquivalent: "")
|
||||
dotsWithText.state = self.widget.type == Widgets.NetworkDotsWithText ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
dotsWithText.target = self
|
||||
|
||||
let arrowsWithText = NSMenuItem(title: "Arrows with text", action: #selector(toggleWidget), keyEquivalent: "")
|
||||
arrowsWithText.state = self.widget.type == Widgets.NetworkArrowsWithText ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
arrowsWithText.target = self
|
||||
|
||||
let chart = NSMenuItem(title: "Chart", action: #selector(toggleWidget), keyEquivalent: "")
|
||||
chart.state = self.widget.type == Widgets.NetworkChart ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
chart.target = self
|
||||
|
||||
submenu.addItem(dots)
|
||||
submenu.addItem(arrows)
|
||||
submenu.addItem(text)
|
||||
submenu.addItem(dotsWithText)
|
||||
submenu.addItem(arrowsWithText)
|
||||
|
||||
submenu.addItem(NSMenuItem.separator())
|
||||
|
||||
if let view = self.widget.view as? Widget {
|
||||
for widgetMenu in view.menus {
|
||||
submenu.addItem(widgetMenu)
|
||||
}
|
||||
}
|
||||
|
||||
if self.enabled {
|
||||
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.enabled = state
|
||||
menuBar!.reload(name: self.name)
|
||||
|
||||
if !state {
|
||||
menu.submenu = nil
|
||||
} else {
|
||||
menu.submenu = submenu
|
||||
}
|
||||
|
||||
self.restart()
|
||||
}
|
||||
|
||||
@objc func toggleWidget(_ sender: NSMenuItem) {
|
||||
var widgetCode: Float = 0.0
|
||||
|
||||
switch sender.title {
|
||||
case "Dots":
|
||||
widgetCode = Widgets.NetworkDots
|
||||
case "Arrows":
|
||||
widgetCode = Widgets.NetworkArrows
|
||||
case "Text":
|
||||
widgetCode = Widgets.NetworkText
|
||||
case "Dots with text":
|
||||
widgetCode = Widgets.NetworkDotsWithText
|
||||
case "Arrows with text":
|
||||
widgetCode = Widgets.NetworkArrowsWithText
|
||||
case "Chart":
|
||||
widgetCode = Widgets.NetworkChart
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if self.widget.type == widgetCode {
|
||||
return
|
||||
}
|
||||
|
||||
for item in self.submenu.items {
|
||||
if item.title == "Dots" || item.title == "Arrows" || item.title == "Text" || item.title == "Dots with text" || item.title == "Arrows with text" || item.title == "Chart" {
|
||||
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.widget.type = widgetCode
|
||||
initWidget()
|
||||
menuBar!.reload(name: self.name)
|
||||
}
|
||||
}
|
||||
@@ -1,284 +0,0 @@
|
||||
//
|
||||
// NetworkPopup.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 22/01/2020.
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import Charts
|
||||
|
||||
extension Network {
|
||||
public func initPopup() {
|
||||
self.popup.view.view?.frame = NSRect(x: 0, y: 0, width: TabWidth, height: TabHeight)
|
||||
|
||||
makeChart()
|
||||
makeOverview()
|
||||
makeDataOverview()
|
||||
}
|
||||
|
||||
private func makeChart() {
|
||||
let downloadLineColor: NSColor = NSColor(red: (26/255.0), green: (126/255.0), blue: (252/255.0), alpha: 1.0)
|
||||
let downloadGradientColor: NSColor = NSColor(red: (26/255.0), green: (126/255.0), blue: (252/255.0), alpha: 0.5)
|
||||
|
||||
let uploadLineColor: NSColor = NSColor(red: (1), green: (0), blue: (0), alpha: 1.0)
|
||||
let uploadGradientColor: NSColor = NSColor(red: (1), green: (0), blue: (0), alpha: 0.5)
|
||||
|
||||
self.chart = LineChartView(frame: CGRect(x: 0, y: TabHeight - 110, width: TabWidth, height: 102))
|
||||
self.chart.animate(xAxisDuration: 2.0, yAxisDuration: 2.0, easingOption: .easeInCubic)
|
||||
self.chart.backgroundColor = .white
|
||||
self.chart.noDataText = "No \(self.name) usage data"
|
||||
self.chart.legend.enabled = false
|
||||
self.chart.scaleXEnabled = false
|
||||
self.chart.scaleYEnabled = false
|
||||
self.chart.pinchZoomEnabled = false
|
||||
self.chart.doubleTapToZoomEnabled = false
|
||||
self.chart.drawBordersEnabled = false
|
||||
self.chart.autoScaleMinMaxEnabled = true
|
||||
|
||||
self.chart.rightAxis.enabled = false
|
||||
|
||||
self.chart.leftAxis.valueFormatter = ChartsNetworkAxisFormatter()
|
||||
self.chart.leftAxis.axisMinimum = 0
|
||||
self.chart.leftAxis.drawGridLinesEnabled = false
|
||||
self.chart.leftAxis.drawAxisLineEnabled = false
|
||||
|
||||
self.chart.leftAxis.gridColor = NSColor(red:220/255, green:220/255, blue:220/255, alpha:1)
|
||||
self.chart.leftAxis.gridLineWidth = 0.5
|
||||
self.chart.leftAxis.drawGridLinesEnabled = true
|
||||
self.chart.leftAxis.labelTextColor = NSColor(red:150/255, green:150/255, blue:150/255, alpha:1)
|
||||
|
||||
self.chart.xAxis.drawAxisLineEnabled = false
|
||||
self.chart.xAxis.drawLimitLinesBehindDataEnabled = false
|
||||
self.chart.xAxis.gridLineWidth = 0.5
|
||||
self.chart.xAxis.drawGridLinesEnabled = false
|
||||
self.chart.xAxis.drawLabelsEnabled = false
|
||||
|
||||
let marker = ChartNetworkMarker()
|
||||
marker.chartView = self.chart
|
||||
self.chart.marker = marker
|
||||
|
||||
var downloadLineChartEntry = [ChartDataEntry]()
|
||||
downloadLineChartEntry.append(ChartDataEntry(x: 0, y: 0))
|
||||
let download = LineChartDataSet(entries: downloadLineChartEntry, label: "Download")
|
||||
download.drawCirclesEnabled = false
|
||||
download.mode = .cubicBezier
|
||||
download.cubicIntensity = 0.1
|
||||
download.colors = [downloadLineColor]
|
||||
download.fillColor = downloadGradientColor
|
||||
download.drawFilledEnabled = true
|
||||
|
||||
var uploadLineChartEntry = [ChartDataEntry]()
|
||||
uploadLineChartEntry.append(ChartDataEntry(x: 0, y: 0))
|
||||
let upload = LineChartDataSet(entries: uploadLineChartEntry, label: "Upload")
|
||||
upload.drawCirclesEnabled = false
|
||||
upload.mode = .cubicBezier
|
||||
upload.cubicIntensity = 0.1
|
||||
upload.colors = [uploadLineColor]
|
||||
upload.fillColor = uploadGradientColor
|
||||
upload.drawFilledEnabled = true
|
||||
|
||||
let data = LineChartData()
|
||||
data.addDataSet(download)
|
||||
data.addDataSet(upload)
|
||||
data.setDrawValues(false)
|
||||
|
||||
self.chart.data = data
|
||||
self.popup.view.view?.addSubview(self.chart)
|
||||
}
|
||||
|
||||
public func chartUpdater(value: NetworkUsage) {
|
||||
if self.chart.data == nil { return }
|
||||
|
||||
let index = Double((self.chart.data?.getDataSetByIndex(0)?.entryCount)!)
|
||||
self.chart.data?.addEntry(ChartDataEntry(x: index, y: Double(value.download)), dataSetIndex: 0)
|
||||
self.chart.data?.addEntry(ChartDataEntry(x: index, y: Double(value.upload)), dataSetIndex: 1)
|
||||
|
||||
if index > 120 {
|
||||
self.chart.xAxis.axisMinimum = index - 120
|
||||
}
|
||||
self.chart.xAxis.axisMaximum = index
|
||||
|
||||
if self.popup.active {
|
||||
self.chart.notifyDataSetChanged()
|
||||
self.chart.moveViewToX(index)
|
||||
}
|
||||
}
|
||||
|
||||
private func makeOverview() {
|
||||
let overviewLabel: NSView = NSView(frame: NSRect(x: 0, y: TabHeight - 140, width: TabWidth, height: 25))
|
||||
|
||||
overviewLabel.wantsLayer = true
|
||||
overviewLabel.layer?.backgroundColor = NSColor(hexString: "#eeeeee", alpha: 0.5).cgColor
|
||||
|
||||
let overviewText: NSTextField = NSTextField(string: "Overview")
|
||||
overviewText.frame = NSRect(x: 0, y: 0, width: TabWidth, height: overviewLabel.frame.size.height - 4)
|
||||
overviewText.isEditable = false
|
||||
overviewText.isSelectable = false
|
||||
overviewText.isBezeled = false
|
||||
overviewText.wantsLayer = true
|
||||
overviewText.textColor = .darkGray
|
||||
overviewText.canDrawSubviewsIntoLayer = true
|
||||
overviewText.alignment = .center
|
||||
overviewText.backgroundColor = NSColor(hexString: "#dddddd", alpha: 0)
|
||||
overviewText.font = NSFont.systemFont(ofSize: 12, weight: .medium)
|
||||
|
||||
overviewLabel.addSubview(overviewText)
|
||||
self.popup.view.view?.addSubview(overviewLabel)
|
||||
|
||||
let stackHeight: CGFloat = 22
|
||||
let vertical: NSStackView = NSStackView(frame: NSRect(x: 0, y: 125, width: TabWidth, height: stackHeight*4))
|
||||
vertical.orientation = .vertical
|
||||
|
||||
let publicIP: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*3, width: TabWidth - 20, height: stackHeight))
|
||||
publicIP.orientation = .horizontal
|
||||
publicIP.distribution = .equalCentering
|
||||
let publicIPLabel = LabelField(string: "Public IP")
|
||||
self.publicIPValue = ValueField(string: "No connection")
|
||||
publicIP.addView(publicIPLabel, in: .center)
|
||||
publicIP.addView(self.publicIPValue, in: .center)
|
||||
|
||||
let localIP: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*2, width: TabWidth - 20, height: stackHeight))
|
||||
localIP.orientation = .horizontal
|
||||
localIP.distribution = .equalCentering
|
||||
let localIPLabel = LabelField(string: "Local IP")
|
||||
self.localIPValue = ValueField(string: "No connection")
|
||||
localIP.addView(localIPLabel, in: .center)
|
||||
localIP.addView(self.localIPValue, in: .center)
|
||||
|
||||
let network: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*1, width: TabWidth - 20, height: stackHeight))
|
||||
network.orientation = .horizontal
|
||||
network.distribution = .equalCentering
|
||||
let networkLabel = LabelField(string: "Network")
|
||||
self.networkValue = ValueField(string: "No connection")
|
||||
network.addView(networkLabel, in: .center)
|
||||
network.addView(self.networkValue, in: .center)
|
||||
|
||||
let physical: NSStackView = NSStackView(frame: NSRect(x: 10, y: 0, width: TabWidth - 20, height: stackHeight))
|
||||
physical.orientation = .horizontal
|
||||
physical.distribution = .equalCentering
|
||||
let physicalLabel = LabelField(string: "Physical address")
|
||||
self.physicalValue = ValueField(string: "No connection")
|
||||
physical.addView(physicalLabel, in: .center)
|
||||
physical.addView(self.physicalValue, in: .center)
|
||||
|
||||
vertical.addSubview(publicIP)
|
||||
vertical.addSubview(localIP)
|
||||
vertical.addSubview(network)
|
||||
vertical.addSubview(physical)
|
||||
|
||||
self.popup.view.view?.addSubview(vertical)
|
||||
}
|
||||
|
||||
public func overviewUpdater(value: NetworkInterface) {
|
||||
if !self.popup.active && self.popup.initialized && !value.force { return }
|
||||
self.popup.initialized = true
|
||||
|
||||
if !value.active {
|
||||
self.clearOverview()
|
||||
return
|
||||
}
|
||||
|
||||
if let publicIP = value.publicIP {
|
||||
// if value.countryCode != nil {
|
||||
// publicIP = "\(publicIP) (\(value.countryCode!))"
|
||||
// }
|
||||
self.publicIPValue.stringValue = publicIP
|
||||
}
|
||||
if let localIP = value.localIP {
|
||||
self.localIPValue.stringValue = localIP
|
||||
}
|
||||
if var networkType = value.networkType {
|
||||
if value.wifiName != nil {
|
||||
networkType = "\(value.wifiName!) (\(networkType))"
|
||||
}
|
||||
self.networkValue.stringValue = networkType
|
||||
}
|
||||
if let macAddress = value.macAddress {
|
||||
self.physicalValue.stringValue = macAddress.uppercased()
|
||||
}
|
||||
}
|
||||
|
||||
private func clearOverview() {
|
||||
self.publicIPValue.stringValue = "No connection"
|
||||
self.localIPValue.stringValue = "No connection"
|
||||
self.networkValue.stringValue = "No connection"
|
||||
self.physicalValue.stringValue = "No connection"
|
||||
}
|
||||
|
||||
private func makeDataOverview() {
|
||||
let label: NSView = NSView(frame: NSRect(x: 0, y: 95, width: TabWidth, height: 25))
|
||||
|
||||
label.wantsLayer = true
|
||||
label.layer?.backgroundColor = NSColor(hexString: "#eeeeee", alpha: 0.5).cgColor
|
||||
|
||||
let text: NSTextField = NSTextField(string: "Data overview")
|
||||
text.frame = NSRect(x: 0, y: 0, width: TabWidth, height: label.frame.size.height - 4)
|
||||
text.isEditable = false
|
||||
text.isSelectable = false
|
||||
text.isBezeled = false
|
||||
text.wantsLayer = true
|
||||
text.textColor = .darkGray
|
||||
text.canDrawSubviewsIntoLayer = true
|
||||
text.alignment = .center
|
||||
text.backgroundColor = NSColor(hexString: "#dddddd", alpha: 0)
|
||||
text.font = NSFont.systemFont(ofSize: 12, weight: .medium)
|
||||
|
||||
label.addSubview(text)
|
||||
self.popup.view.view?.addSubview(label)
|
||||
|
||||
let stackHeight: CGFloat = 22
|
||||
let vertical: NSStackView = NSStackView(frame: NSRect(x: 0, y: 4, width: TabWidth, height: stackHeight*4))
|
||||
vertical.orientation = .vertical
|
||||
|
||||
let upload: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*3, width: TabWidth - 20, height: stackHeight))
|
||||
upload.orientation = .horizontal
|
||||
upload.distribution = .equalCentering
|
||||
let uploadLabel = LabelField(string: "Upload")
|
||||
self.uploadValue = ValueField(string: "0 KB/s")
|
||||
upload.addView(uploadLabel, in: .center)
|
||||
upload.addView(self.uploadValue, in: .center)
|
||||
|
||||
let download: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*2, width: TabWidth - 20, height: stackHeight))
|
||||
download.orientation = .horizontal
|
||||
download.distribution = .equalCentering
|
||||
let downloadLabel = LabelField(string: "Download")
|
||||
self.downloadValue = ValueField(string: "0 KB/s")
|
||||
download.addView(downloadLabel, in: .center)
|
||||
download.addView(self.downloadValue, in: .center)
|
||||
|
||||
let totalUpload: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*1, width: TabWidth - 20, height: stackHeight))
|
||||
totalUpload.orientation = .horizontal
|
||||
totalUpload.distribution = .equalCentering
|
||||
let totalUploadLabel = LabelField(string: "Total upload")
|
||||
self.totalUploadValue = ValueField(string: "0 KB")
|
||||
totalUpload.addView(totalUploadLabel, in: .center)
|
||||
totalUpload.addView(self.totalUploadValue, in: .center)
|
||||
|
||||
let totalDownload: NSStackView = NSStackView(frame: NSRect(x: 10, y: 0, width: TabWidth - 20, height: stackHeight))
|
||||
totalDownload.orientation = .horizontal
|
||||
totalDownload.distribution = .equalCentering
|
||||
let totalDownloadLabel = LabelField(string: "Total download")
|
||||
self.totalDownloadValue = ValueField(string: "0 KB")
|
||||
totalDownload.addView(totalDownloadLabel, in: .center)
|
||||
totalDownload.addView(self.totalDownloadValue, in: .center)
|
||||
|
||||
vertical.addSubview(upload)
|
||||
vertical.addSubview(download)
|
||||
vertical.addSubview(totalUpload)
|
||||
vertical.addSubview(totalDownload)
|
||||
|
||||
self.popup.view.view?.addSubview(vertical)
|
||||
}
|
||||
|
||||
public func dataUpdater(value: NetworkUsage) {
|
||||
if !self.popup.active && self.popup.initialized { return }
|
||||
|
||||
self.downloadValue.stringValue = Units(bytes: value.download).getReadableSpeed()
|
||||
self.uploadValue.stringValue = Units(bytes: value.upload).getReadableSpeed()
|
||||
|
||||
self.totalDownloadValue.stringValue = Units(bytes: value.totalDownload).getReadableMemory()
|
||||
self.totalUploadValue.stringValue = Units(bytes: value.totalUpload).getReadableMemory()
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
//
|
||||
// NetworkReader.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 14/01/2020.
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
struct NetworkUsage {
|
||||
var download: Int64 = 0
|
||||
var upload: Int64 = 0
|
||||
|
||||
var totalDownload: Int64 = 0
|
||||
var totalUpload: Int64 = 0
|
||||
}
|
||||
|
||||
class NetworkReader: Reader {
|
||||
public var name: String = "Network"
|
||||
public var enabled: Bool = true
|
||||
public var available: Bool = true
|
||||
public var optional: Bool = false
|
||||
public var initialized: Bool = false
|
||||
public var callback: (NetworkUsage) -> Void = {_ in}
|
||||
|
||||
private var uploadValue: Int64 = 0
|
||||
private var downloadValue: Int64 = 0
|
||||
|
||||
init(_ updater: @escaping (NetworkUsage) -> Void) {
|
||||
self.callback = updater
|
||||
|
||||
if self.available {
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
self.read()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func read() {
|
||||
if !self.enabled && self.initialized { return }
|
||||
self.initialized = true
|
||||
|
||||
var interfaceAddresses: UnsafeMutablePointer<ifaddrs>? = nil
|
||||
|
||||
var upload: Int64 = 0
|
||||
var download: Int64 = 0
|
||||
guard getifaddrs(&interfaceAddresses) == 0 else { return }
|
||||
|
||||
var pointer = interfaceAddresses
|
||||
while pointer != nil {
|
||||
guard let info = getDataUsageInfo(from: pointer!) else {
|
||||
pointer = pointer!.pointee.ifa_next
|
||||
continue
|
||||
}
|
||||
pointer = pointer!.pointee.ifa_next
|
||||
upload += info[0]
|
||||
download += info[1]
|
||||
}
|
||||
freeifaddrs(interfaceAddresses)
|
||||
|
||||
let lastUpload = self.uploadValue
|
||||
let lastDownload = self.downloadValue
|
||||
|
||||
if lastUpload != 0 && lastDownload != 0 {
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.callback(NetworkUsage(
|
||||
download: download - lastDownload,
|
||||
upload: upload - lastUpload,
|
||||
totalDownload: download,
|
||||
totalUpload: upload
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
self.uploadValue = upload
|
||||
self.downloadValue = download
|
||||
}
|
||||
|
||||
public func toggleEnable(_ value: Bool) {
|
||||
self.enabled = value
|
||||
}
|
||||
|
||||
private func getDataUsageInfo(from infoPointer: UnsafeMutablePointer<ifaddrs>) -> [Int64]? {
|
||||
let pointer = infoPointer
|
||||
|
||||
let name: String! = String(cString: infoPointer.pointee.ifa_name)
|
||||
let addr = pointer.pointee.ifa_addr.pointee
|
||||
guard addr.sa_family == UInt8(AF_LINK) else { return nil }
|
||||
var networkData: UnsafeMutablePointer<if_data>? = nil
|
||||
|
||||
if name.hasPrefix("en") {
|
||||
networkData = unsafeBitCast(pointer.pointee.ifa_data, to: UnsafeMutablePointer<if_data>.self)
|
||||
return [Int64(networkData?.pointee.ifi_obytes ?? 0), Int64(networkData?.pointee.ifi_ibytes ?? 0)] // upload, download
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
//
|
||||
// RAM.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 14/01/2020.
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import Charts
|
||||
import Repeat
|
||||
|
||||
class RAM: Module {
|
||||
public var name: String = "RAM"
|
||||
public var updateInterval: Double = 1
|
||||
|
||||
public var enabled: Bool = true
|
||||
public var available: Bool = true
|
||||
|
||||
public var readers: [Reader] = []
|
||||
public var task: Repeater?
|
||||
|
||||
public var widget: ModuleWidget = ModuleWidget()
|
||||
public var popup: ModulePopup = ModulePopup(true)
|
||||
public var menu: NSMenuItem = NSMenuItem()
|
||||
|
||||
internal let defaults = UserDefaults.standard
|
||||
internal var submenu: NSMenu = NSMenu()
|
||||
|
||||
internal var totalValue: NSTextField = NSTextField()
|
||||
internal var usedValue: NSTextField = NSTextField()
|
||||
internal var freeValue: NSTextField = NSTextField()
|
||||
internal var processViewList: [NSStackView] = []
|
||||
internal var chart: LineChartView = LineChartView()
|
||||
|
||||
init() {
|
||||
if !self.available { return }
|
||||
|
||||
self.enabled = defaults.object(forKey: name) != nil ? defaults.bool(forKey: name) : true
|
||||
self.updateInterval = defaults.object(forKey: "\(name)_interval") != nil ? defaults.double(forKey: "\(name)_interval") : self.updateInterval
|
||||
self.widget.type = defaults.object(forKey: "\(name)_widget") != nil ? defaults.float(forKey: "\(name)_widget") : Widgets.Mini
|
||||
|
||||
readers.append(RAMUsageReader(self.usageUpdater))
|
||||
readers.append(RAMProcessReader(self.processesUpdater))
|
||||
|
||||
self.initWidget()
|
||||
self.initMenu()
|
||||
self.initPopup()
|
||||
|
||||
self.task = Repeater.init(interval: .seconds(self.updateInterval), observer: { _ in
|
||||
self.readers.forEach { reader in
|
||||
reader.read()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func start() {
|
||||
if self.task != nil && self.task!.state.isRunning == false {
|
||||
self.task!.start()
|
||||
}
|
||||
}
|
||||
|
||||
public func stop() {
|
||||
if self.task!.state.isRunning {
|
||||
self.task?.pause()
|
||||
}
|
||||
}
|
||||
|
||||
public func restart() {
|
||||
self.stop()
|
||||
self.start()
|
||||
}
|
||||
|
||||
private func usageUpdater(value: RAMUsage) {
|
||||
self.chartUpdater(value: value)
|
||||
self.overviewUpdater(value: value)
|
||||
|
||||
if self.widget.view is Widget {
|
||||
(self.widget.view as! Widget).setValue(data: [value.usage])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
//
|
||||
// RAMMenu.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 14/01/2020.
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
extension RAM {
|
||||
public 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
|
||||
|
||||
let mini = NSMenuItem(title: "Mini", action: #selector(toggleWidget), keyEquivalent: "")
|
||||
mini.state = self.widget.type == Widgets.Mini ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
mini.target = self
|
||||
|
||||
let chart = NSMenuItem(title: "Chart", action: #selector(toggleWidget), keyEquivalent: "")
|
||||
chart.state = self.widget.type == 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.widget.type == Widgets.ChartWithValue ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
chartWithValue.target = self
|
||||
|
||||
let barChart = NSMenuItem(title: "Bar chart", action: #selector(toggleWidget), keyEquivalent: "")
|
||||
barChart.state = self.widget.type == Widgets.BarChart ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
barChart.target = self
|
||||
|
||||
submenu.addItem(mini)
|
||||
submenu.addItem(chart)
|
||||
submenu.addItem(chartWithValue)
|
||||
submenu.addItem(barChart)
|
||||
|
||||
submenu.addItem(NSMenuItem.separator())
|
||||
|
||||
if let view = self.widget.view as? Widget {
|
||||
for widgetMenu in view.menus {
|
||||
submenu.addItem(widgetMenu)
|
||||
}
|
||||
}
|
||||
|
||||
submenu.addItem(NSMenuItem.separator())
|
||||
submenu.addItem(generateIntervalMenu())
|
||||
|
||||
if self.enabled {
|
||||
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.enabled = state
|
||||
menuBar!.reload(name: self.name)
|
||||
|
||||
if !state {
|
||||
menu.submenu = nil
|
||||
} else {
|
||||
menu.submenu = submenu
|
||||
}
|
||||
|
||||
self.restart()
|
||||
}
|
||||
|
||||
@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
|
||||
case "Bar chart":
|
||||
widgetCode = Widgets.BarChart
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if self.widget.type == widgetCode {
|
||||
return
|
||||
}
|
||||
|
||||
for item in self.submenu.items {
|
||||
if item.title == "Mini" || item.title == "Chart" || item.title == "Chart with value" || item.title == "Bar chart" {
|
||||
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.widget.type = widgetCode
|
||||
self.initWidget()
|
||||
self.initMenu()
|
||||
menuBar!.reload(name: self.name)
|
||||
}
|
||||
|
||||
func generateIntervalMenu() -> NSMenuItem {
|
||||
let updateInterval = NSMenuItem(title: "Update interval", action: nil, keyEquivalent: "")
|
||||
|
||||
let updateIntervals = NSMenu()
|
||||
let updateInterval_1 = NSMenuItem(title: "1s", action: #selector(changeInterval), keyEquivalent: "")
|
||||
updateInterval_1.state = self.updateInterval == 1 ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
updateInterval_1.target = self
|
||||
let updateInterval_2 = NSMenuItem(title: "3s", action: #selector(changeInterval), keyEquivalent: "")
|
||||
updateInterval_2.state = self.updateInterval == 3 ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
updateInterval_2.target = self
|
||||
let updateInterval_3 = NSMenuItem(title: "5s", action: #selector(changeInterval), keyEquivalent: "")
|
||||
updateInterval_3.state = self.updateInterval == 5 ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
updateInterval_3.target = self
|
||||
let updateInterval_4 = NSMenuItem(title: "10s", action: #selector(changeInterval), keyEquivalent: "")
|
||||
updateInterval_4.state = self.updateInterval == 10 ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
updateInterval_4.target = self
|
||||
let updateInterval_5 = NSMenuItem(title: "15s", action: #selector(changeInterval), keyEquivalent: "")
|
||||
updateInterval_5.state = self.updateInterval == 15 ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
updateInterval_5.target = self
|
||||
|
||||
updateIntervals.addItem(updateInterval_1)
|
||||
updateIntervals.addItem(updateInterval_2)
|
||||
updateIntervals.addItem(updateInterval_3)
|
||||
updateIntervals.addItem(updateInterval_4)
|
||||
updateIntervals.addItem(updateInterval_5)
|
||||
|
||||
updateInterval.submenu = updateIntervals
|
||||
|
||||
return updateInterval
|
||||
}
|
||||
|
||||
@objc func changeInterval(_ sender: NSMenuItem) {
|
||||
var interval: Double = self.updateInterval
|
||||
|
||||
switch sender.title {
|
||||
case "1s":
|
||||
interval = 1
|
||||
case "3s":
|
||||
interval = 3
|
||||
case "5s":
|
||||
interval = 5
|
||||
case "10s":
|
||||
interval = 10
|
||||
case "15s":
|
||||
interval = 15
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if interval == self.updateInterval {
|
||||
return
|
||||
}
|
||||
|
||||
for item in self.submenu.items {
|
||||
if item.title == "Update interval" {
|
||||
for subitem in item.submenu!.items {
|
||||
subitem.state = NSControl.StateValue.off
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sender.state = NSControl.StateValue.on
|
||||
self.updateInterval = interval
|
||||
self.defaults.set(interval, forKey: "\(name)_interval")
|
||||
self.task?.reset(.seconds(interval), restart: self.task!.state.isRunning)
|
||||
}
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
//
|
||||
// RAMPopup.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 14/01/2020.
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import Charts
|
||||
|
||||
extension RAM {
|
||||
public func initPopup() {
|
||||
self.popup.view.view?.frame = NSRect(x: 0, y: 0, width: TabWidth, height: TabHeight)
|
||||
|
||||
makeChart()
|
||||
makeOverview()
|
||||
makeProcesses()
|
||||
}
|
||||
|
||||
private func makeChart() {
|
||||
let lineColor: NSColor = NSColor(red: (26/255.0), green: (126/255.0), blue: (252/255.0), alpha: 1.0)
|
||||
let gradientColor: NSColor = NSColor(red: (26/255.0), green: (126/255.0), blue: (252/255.0), alpha: 0.5)
|
||||
|
||||
self.chart = LineChartView(frame: CGRect(x: 0, y: TabHeight - 110, width: TabWidth, height: 102))
|
||||
self.chart.animate(xAxisDuration: 2.0, yAxisDuration: 2.0, easingOption: .easeInCubic)
|
||||
self.chart.backgroundColor = .white
|
||||
self.chart.noDataText = "No \(self.name) usage data"
|
||||
self.chart.legend.enabled = false
|
||||
self.chart.scaleXEnabled = false
|
||||
self.chart.scaleYEnabled = false
|
||||
self.chart.pinchZoomEnabled = false
|
||||
self.chart.doubleTapToZoomEnabled = false
|
||||
self.chart.drawBordersEnabled = false
|
||||
self.chart.autoScaleMinMaxEnabled = true
|
||||
|
||||
self.chart.rightAxis.enabled = false
|
||||
|
||||
let v = self.readers.filter{ $0 is RAMUsageReader }.first as! RAMUsageReader
|
||||
self.chart.leftAxis.axisMinimum = 0
|
||||
self.chart.leftAxis.axisMaximum = Units(bytes: Int64(v.totalSize)).gigabytes
|
||||
self.chart.leftAxis.labelCount = Units(bytes: Int64(v.totalSize)).gigabytes > 16 ? 6 : 4
|
||||
self.chart.leftAxis.drawGridLinesEnabled = false
|
||||
self.chart.leftAxis.drawAxisLineEnabled = false
|
||||
|
||||
self.chart.leftAxis.gridColor = NSColor(red:220/255, green:220/255, blue:220/255, alpha:1)
|
||||
self.chart.leftAxis.gridLineWidth = 0.5
|
||||
self.chart.leftAxis.drawGridLinesEnabled = true
|
||||
self.chart.leftAxis.labelTextColor = NSColor(red:150/255, green:150/255, blue:150/255, alpha:1)
|
||||
|
||||
self.chart.xAxis.drawAxisLineEnabled = false
|
||||
self.chart.xAxis.drawLimitLinesBehindDataEnabled = false
|
||||
self.chart.xAxis.gridLineWidth = 0.5
|
||||
self.chart.xAxis.drawGridLinesEnabled = false
|
||||
self.chart.xAxis.drawLabelsEnabled = false
|
||||
|
||||
let marker = ChartMarker()
|
||||
marker.chartView = self.chart
|
||||
self.chart.marker = marker
|
||||
|
||||
var lineChartEntry = [ChartDataEntry]()
|
||||
lineChartEntry.append(ChartDataEntry(x: 0, y: 0))
|
||||
let chartDataSet = LineChartDataSet(entries: lineChartEntry, label: "\(self.name) Usage")
|
||||
chartDataSet.drawCirclesEnabled = false
|
||||
chartDataSet.mode = .cubicBezier
|
||||
chartDataSet.cubicIntensity = 0.1
|
||||
chartDataSet.colors = [lineColor]
|
||||
chartDataSet.fillColor = gradientColor
|
||||
chartDataSet.drawFilledEnabled = true
|
||||
|
||||
let data = LineChartData()
|
||||
data.addDataSet(chartDataSet)
|
||||
data.setDrawValues(false)
|
||||
|
||||
self.chart.data = LineChartData(dataSet: chartDataSet)
|
||||
|
||||
self.popup.view.view?.addSubview(self.chart)
|
||||
}
|
||||
|
||||
public func chartUpdater(value: RAMUsage) {
|
||||
if self.chart.data == nil { return }
|
||||
|
||||
let index = Double((self.chart.data?.getDataSetByIndex(0)?.entryCount)!)
|
||||
let usage = Units(bytes: Int64(value.used)).getReadableTuple().0
|
||||
self.chart.data?.addEntry(ChartDataEntry(x: index, y: usage), dataSetIndex: 0)
|
||||
|
||||
if index > 120 {
|
||||
self.chart.xAxis.axisMinimum = index - 120
|
||||
}
|
||||
self.chart.xAxis.axisMaximum = index
|
||||
|
||||
if self.popup.active {
|
||||
self.chart.notifyDataSetChanged()
|
||||
self.chart.moveViewToX(index)
|
||||
}
|
||||
}
|
||||
|
||||
private func makeOverview() {
|
||||
let overviewLabel: NSView = NSView(frame: NSRect(x: 0, y: TabHeight - 140, width: TabWidth, height: 25))
|
||||
|
||||
overviewLabel.wantsLayer = true
|
||||
overviewLabel.layer?.backgroundColor = NSColor(hexString: "#eeeeee", alpha: 0.5).cgColor
|
||||
|
||||
let overviewText: NSTextField = NSTextField(string: "Overview")
|
||||
overviewText.frame = NSRect(x: 0, y: 0, width: TabWidth, height: overviewLabel.frame.size.height - 4)
|
||||
overviewText.isEditable = false
|
||||
overviewText.isSelectable = false
|
||||
overviewText.isBezeled = false
|
||||
overviewText.wantsLayer = true
|
||||
overviewText.textColor = .darkGray
|
||||
overviewText.canDrawSubviewsIntoLayer = true
|
||||
overviewText.alignment = .center
|
||||
overviewText.backgroundColor = NSColor(hexString: "#dddddd", alpha: 0)
|
||||
overviewText.font = NSFont.systemFont(ofSize: 12, weight: .medium)
|
||||
|
||||
overviewLabel.addSubview(overviewText)
|
||||
self.popup.view.view?.addSubview(overviewLabel)
|
||||
|
||||
let stackHeight: CGFloat = 22
|
||||
let vertical: NSStackView = NSStackView(frame: NSRect(x: 0, y: 147, width: TabWidth, height: stackHeight*3))
|
||||
vertical.orientation = .vertical
|
||||
|
||||
let total: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*2, width: TabWidth - 20, height: stackHeight))
|
||||
total.orientation = .horizontal
|
||||
total.distribution = .equalCentering
|
||||
let totalLabel = LabelField(string: "Total")
|
||||
self.totalValue = ValueField(string: "0 GB")
|
||||
total.addView(totalLabel, in: .center)
|
||||
total.addView(self.totalValue, in: .center)
|
||||
|
||||
let used: NSStackView = NSStackView(frame: NSRect(x: 10, y: stackHeight*1, width: TabWidth - 20, height: stackHeight))
|
||||
used.orientation = .horizontal
|
||||
used.distribution = .equalCentering
|
||||
let usedLabel = LabelField(string: "Used")
|
||||
self.usedValue = ValueField(string: "0 GB")
|
||||
used.addView(usedLabel, in: .center)
|
||||
used.addView(self.usedValue, in: .center)
|
||||
|
||||
let free: NSStackView = NSStackView(frame: NSRect(x: 10, y: 0, width: TabWidth - 20, height: stackHeight))
|
||||
free.orientation = .horizontal
|
||||
free.distribution = .equalCentering
|
||||
let freeLabel = LabelField(string: "Free")
|
||||
self.freeValue = ValueField(string: "0 GB")
|
||||
free.addView(freeLabel, in: .center)
|
||||
free.addView(self.freeValue, in: .center)
|
||||
|
||||
vertical.addSubview(total)
|
||||
vertical.addSubview(used)
|
||||
vertical.addSubview(free)
|
||||
|
||||
self.popup.view.view?.addSubview(vertical)
|
||||
}
|
||||
|
||||
public func overviewUpdater(value: RAMUsage) {
|
||||
if !self.popup.active && self.popup.initialized { return }
|
||||
|
||||
self.totalValue.stringValue = Units(bytes: Int64(value.total)).getReadableMemory()
|
||||
self.usedValue.stringValue = Units(bytes: Int64(value.used)).getReadableMemory()
|
||||
self.freeValue.stringValue = Units(bytes: Int64(value.free)).getReadableMemory()
|
||||
}
|
||||
|
||||
private func makeProcesses() {
|
||||
let label: NSView = NSView(frame: NSRect(x: 0, y: 0, width: TabWidth, height: 25))
|
||||
|
||||
label.wantsLayer = true
|
||||
label.layer?.backgroundColor = NSColor(hexString: "#eeeeee", alpha: 0.5).cgColor
|
||||
|
||||
let text: NSTextField = NSTextField(string: "Top Processes")
|
||||
text.frame = NSRect(x: 0, y: 0, width: TabWidth, height: label.frame.size.height - 4)
|
||||
text.isEditable = false
|
||||
text.isSelectable = false
|
||||
text.isBezeled = false
|
||||
text.wantsLayer = true
|
||||
text.textColor = .darkGray
|
||||
text.canDrawSubviewsIntoLayer = true
|
||||
text.alignment = .center
|
||||
text.backgroundColor = NSColor(hexString: "#dddddd", alpha: 0)
|
||||
text.font = NSFont.systemFont(ofSize: 12, weight: .medium)
|
||||
|
||||
label.addSubview(text)
|
||||
self.popup.view.view?.addSubview(label)
|
||||
|
||||
let stackHeight: CGFloat = 22
|
||||
let vertical: NSStackView = NSStackView(frame: NSRect(x: 0, y: 4, width: TabWidth, height: stackHeight*5))
|
||||
vertical.orientation = .vertical
|
||||
vertical.distribution = .fill
|
||||
|
||||
self.processViewList = []
|
||||
let process_1 = makeProcessView(num: 4, height: stackHeight, label: "", value: "")
|
||||
let process_2 = makeProcessView(num: 3, height: stackHeight, label: "", value: "")
|
||||
let process_3 = makeProcessView(num: 2, height: stackHeight, label: "", value: "")
|
||||
let process_4 = makeProcessView(num: 1, height: stackHeight, label: "", value: "")
|
||||
let process_5 = makeProcessView(num: 0, height: stackHeight, label: "", value: "")
|
||||
|
||||
self.processViewList.append(process_1)
|
||||
self.processViewList.append(process_2)
|
||||
self.processViewList.append(process_3)
|
||||
self.processViewList.append(process_4)
|
||||
self.processViewList.append(process_5)
|
||||
|
||||
vertical.addSubview(process_1)
|
||||
vertical.addSubview(process_2)
|
||||
vertical.addSubview(process_3)
|
||||
vertical.addSubview(process_4)
|
||||
vertical.addSubview(process_5)
|
||||
self.popup.view.view?.addSubview(vertical)
|
||||
|
||||
label.frame = NSRect(x: 0, y: vertical.frame.origin.y + vertical.frame.size.height + 2, width: TabWidth, height: 25)
|
||||
self.popup.view.view?.addSubview(label)
|
||||
}
|
||||
|
||||
public func processesUpdater(value: [TopProcess]) {
|
||||
if self.processViewList.isEmpty || !self.popup.active && self.popup.initialized { return }
|
||||
self.popup.initialized = true
|
||||
|
||||
for (i, process) in value.enumerated() {
|
||||
if i < 5 {
|
||||
let processView = self.processViewList[i]
|
||||
|
||||
(processView.subviews[0] as! NSTextField).stringValue = process.command
|
||||
(processView.subviews[1] as! NSTextField).stringValue = Units(bytes: Int64(process.usage)).getReadableMemory()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func makeProcessView(num: Int, height: CGFloat, label: String, value: String) -> NSStackView {
|
||||
let view: NSStackView = NSStackView(frame: NSRect(x: 10, y: CGFloat(num)*height, width: TabWidth - 20, height: height))
|
||||
view.orientation = .horizontal
|
||||
view.distribution = .equalCentering
|
||||
let viewLabel = LabelField(string: label)
|
||||
let viewValue = ValueField(string: value)
|
||||
view.addView(viewLabel, in: .center)
|
||||
view.addView(viewValue, in: .center)
|
||||
|
||||
return view
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
//
|
||||
// RAMProcessReader.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 14/01/2020.
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
class RAMProcessReader: Reader {
|
||||
public var name: String = "Process"
|
||||
public var enabled: Bool = false
|
||||
public var available: Bool = true
|
||||
public var optional: Bool = true
|
||||
public var initialized: Bool = false
|
||||
public var callback: ([TopProcess]) -> Void = {_ in}
|
||||
|
||||
init(_ updater: @escaping ([TopProcess]) -> Void) {
|
||||
self.callback = updater
|
||||
|
||||
if self.available {
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
self.read()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func read() {
|
||||
if !self.enabled && self.initialized { return }
|
||||
self.initialized = true
|
||||
|
||||
let task = Process()
|
||||
task.launchPath = "/usr/bin/top"
|
||||
task.arguments = ["-l", "1", "-o", "mem", "-n", "5", "-stats", "pid,command,mem"]
|
||||
|
||||
let outputPipe = Pipe()
|
||||
let errorPipe = Pipe()
|
||||
|
||||
task.standardOutput = outputPipe
|
||||
task.standardError = errorPipe
|
||||
|
||||
do {
|
||||
try task.run()
|
||||
} catch let error {
|
||||
print(error)
|
||||
return
|
||||
}
|
||||
|
||||
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let output = String(decoding: outputData, as: UTF8.self)
|
||||
_ = String(decoding: errorData, as: UTF8.self)
|
||||
|
||||
if output.isEmpty {
|
||||
return
|
||||
}
|
||||
|
||||
var processes: [TopProcess] = []
|
||||
output.enumerateLines { (line, stop) -> () in
|
||||
if line.matches("^\\d+ + .+ +\\d+.\\d[M\\+\\-]+ *$") {
|
||||
var str = line.trimmingCharacters(in: .whitespaces)
|
||||
let pidString = str.findAndCrop(pattern: "^\\d+")
|
||||
let usageString = str.findAndCrop(pattern: " [0-9]+M(\\+|\\-)*$")
|
||||
var command = str.trimmingCharacters(in: .whitespaces)
|
||||
|
||||
if let regex = try? NSRegularExpression(pattern: " (\\+|\\-)*$", options: .caseInsensitive) {
|
||||
command = regex.stringByReplacingMatches(in: command, options: [], range: NSRange(location: 0, length: command.count), withTemplate: "")
|
||||
}
|
||||
|
||||
let pid = Int(pidString) ?? 0
|
||||
guard let usage = Double(usageString.filter("01234567890.".contains)) else {
|
||||
return
|
||||
}
|
||||
let process = TopProcess(pid: pid, command: command, usage: usage * Double(1024 * 1024))
|
||||
processes.append(process)
|
||||
}
|
||||
}
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.callback(processes)
|
||||
})
|
||||
}
|
||||
|
||||
func toggleEnable(_ value: Bool) {
|
||||
self.enabled = value
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
//
|
||||
// RAMUsageReader.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 14/01/2020.
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
struct RAMUsage {
|
||||
var usage: Double = 0
|
||||
var total: Double = 0
|
||||
var used: Double = 0
|
||||
var free: Double = 0
|
||||
}
|
||||
|
||||
class RAMUsageReader: Reader {
|
||||
public var name: String = "Usage"
|
||||
public var enabled: Bool = true
|
||||
public var available: Bool = true
|
||||
public var optional: Bool = false
|
||||
public var initialized: Bool = false
|
||||
public var callback: (RAMUsage) -> Void = {_ in}
|
||||
|
||||
public var totalSize: Float = 0
|
||||
public var usage: RAMUsage = RAMUsage()
|
||||
|
||||
init(_ updater: @escaping (RAMUsage) -> Void) {
|
||||
self.callback = updater
|
||||
|
||||
var stats = host_basic_info()
|
||||
var count = UInt32(MemoryLayout<host_basic_info_data_t>.size / MemoryLayout<integer_t>.size)
|
||||
|
||||
let kerr: kern_return_t = withUnsafeMutablePointer(to: &stats) {
|
||||
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
|
||||
host_info(mach_host_self(), HOST_BASIC_INFO, $0, &count)
|
||||
}
|
||||
}
|
||||
|
||||
if kerr == KERN_SUCCESS {
|
||||
self.totalSize = Float(stats.max_mem)
|
||||
}
|
||||
else {
|
||||
self.totalSize = 0
|
||||
print("Error with host_info(): " + (String(cString: mach_error_string(kerr), encoding: String.Encoding.ascii) ?? "unknown error"))
|
||||
}
|
||||
|
||||
if self.available {
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
self.read()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func read() {
|
||||
if !self.enabled && self.initialized { return }
|
||||
self.initialized = true
|
||||
|
||||
var stats = vm_statistics64()
|
||||
var count = UInt32(MemoryLayout<vm_statistics64_data_t>.size / MemoryLayout<integer_t>.size)
|
||||
|
||||
let kerr: kern_return_t = withUnsafeMutablePointer(to: &stats) {
|
||||
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
|
||||
host_statistics64(mach_host_self(), HOST_VM_INFO64, $0, &count)
|
||||
}
|
||||
}
|
||||
|
||||
if kerr == KERN_SUCCESS {
|
||||
let active = Float(stats.active_count) * Float(PAGE_SIZE)
|
||||
// let inactive = Float(stats.inactive_count) * Float(PAGE_SIZE)
|
||||
let wired = Float(stats.wire_count) * Float(PAGE_SIZE)
|
||||
let compressed = Float(stats.compressor_page_count) * Float(PAGE_SIZE)
|
||||
|
||||
let used = active + wired + compressed
|
||||
let free = totalSize - used
|
||||
|
||||
DispatchQueue.main.async(execute: {
|
||||
self.usage = RAMUsage(usage: Double((self.totalSize - free) / self.totalSize), total: Double(self.totalSize), used: Double(used), free: Double(free))
|
||||
self.callback(self.usage)
|
||||
})
|
||||
} else {
|
||||
print("Error with host_statistics64(): " + (String(cString: mach_error_string(kerr), encoding: String.Encoding.ascii) ?? "unknown error"))
|
||||
}
|
||||
}
|
||||
|
||||
func toggleEnable(_ value: Bool) {
|
||||
self.enabled = value
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
//
|
||||
// Sensors.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 03/04/2020.
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import Repeat
|
||||
|
||||
class Sensors: Module {
|
||||
public var name: String = "Sensors"
|
||||
public var updateInterval: Double = 1
|
||||
|
||||
public var enabled: Bool = true
|
||||
public var available: Bool = true
|
||||
|
||||
public var widget: ModuleWidget = ModuleWidget()
|
||||
public var popup: ModulePopup = ModulePopup(false)
|
||||
public var menu: NSMenuItem = NSMenuItem()
|
||||
|
||||
public var readers: [Reader] = []
|
||||
public var task: Repeater?
|
||||
|
||||
internal let defaults = UserDefaults.standard
|
||||
internal var submenu: NSMenu = NSMenu()
|
||||
|
||||
internal var value_1: String = "TC0P"
|
||||
internal var value_2: String = "TG0D"
|
||||
internal var once: Int = 0
|
||||
|
||||
internal var sensors: Sensors_t = Sensors_t()
|
||||
|
||||
init() {
|
||||
if !self.available { return }
|
||||
|
||||
self.enabled = defaults.object(forKey: name) != nil ? defaults.bool(forKey: name) : true
|
||||
self.widget.type = defaults.object(forKey: "\(name)_widget") != nil ? defaults.float(forKey: "\(name)_widget") : Widgets.Sensors
|
||||
self.value_1 = (defaults.object(forKey: "\(name)_value_1") != nil ? defaults.string(forKey: "\(name)_value_1")! : value_1)
|
||||
self.value_2 = (defaults.object(forKey: "\(name)_value_2") != nil ? defaults.string(forKey: "\(name)_value_2")! : value_2)
|
||||
|
||||
self.initWidget()
|
||||
self.initMenu()
|
||||
|
||||
if self.enabled {
|
||||
self.update()
|
||||
}
|
||||
|
||||
self.task = Repeater.init(interval: .seconds(self.updateInterval), observer: { _ in
|
||||
if self.enabled {
|
||||
self.update()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func start() {
|
||||
if self.task != nil && self.task!.state.isRunning == false {
|
||||
self.task!.start()
|
||||
}
|
||||
}
|
||||
|
||||
public func stop() {
|
||||
if self.task!.state.isRunning {
|
||||
self.task?.pause()
|
||||
}
|
||||
}
|
||||
|
||||
public func restart() {
|
||||
self.stop()
|
||||
self.start()
|
||||
}
|
||||
|
||||
private func update() {
|
||||
var value_1_unit: Double = 0
|
||||
var value_1_value: Double = 0
|
||||
var value_2_unit: Double = 0
|
||||
var value_2_value: Double = 0
|
||||
|
||||
var sensor_1: Sensor_t? = self.sensors.find(byKey: self.value_1)
|
||||
var sensor_2: Sensor_t? = self.sensors.find(byKey: self.value_2)
|
||||
|
||||
if sensor_1 != nil {
|
||||
sensor_1!.update()
|
||||
if sensor_1!.value != nil {
|
||||
value_1_value = sensor_1!.value!
|
||||
value_1_unit = Double(sensor_1!.unit[0].unicodeScalarCodePoint())
|
||||
}
|
||||
}
|
||||
if sensor_2 != nil {
|
||||
sensor_2!.update()
|
||||
if sensor_2!.value != nil {
|
||||
value_2_value = sensor_2!.value!
|
||||
value_2_unit = Double(sensor_2!.unit[0].unicodeScalarCodePoint())
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async(execute: {
|
||||
(self.widget.view as! Widget).setValue(data: [value_1_value, value_1_unit, value_2_value, value_2_unit])
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
//
|
||||
// SensorsMenu.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 03/04/2020.
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
extension Sensors {
|
||||
public 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
|
||||
|
||||
let sensor_1: NSMenuItem = NSMenuItem(title: "Sensor #1", action: nil, keyEquivalent: "")
|
||||
sensor_1.target = self
|
||||
sensor_1.submenu = NSMenu()
|
||||
addSensorsMennu(sensor_1.submenu!, value: self.value_1, action: #selector(toggleValue1))
|
||||
|
||||
let sensor_2: NSMenuItem = NSMenuItem(title: "Sensor #2", action: nil, keyEquivalent: "")
|
||||
sensor_2.target = self
|
||||
sensor_2.submenu = NSMenu()
|
||||
addSensorsMennu(sensor_2.submenu!, value: self.value_2, action: #selector(toggleValue2))
|
||||
|
||||
submenu.addItem(sensor_1)
|
||||
submenu.addItem(sensor_2)
|
||||
|
||||
submenu.addItem(NSMenuItem.separator())
|
||||
|
||||
if let view = self.widget.view as? Widget {
|
||||
for widgetMenu in view.menus {
|
||||
submenu.addItem(widgetMenu)
|
||||
}
|
||||
}
|
||||
|
||||
if self.enabled {
|
||||
menu.submenu = submenu
|
||||
}
|
||||
}
|
||||
|
||||
private func addSensorsMennu(_ menu: NSMenu, value: String, action: Selector?) {
|
||||
var sensorsMenu: NSMenuItem? = generateSensorsMenu(type: SensorType.Temperature, value: value, action: action)
|
||||
if sensorsMenu != nil {
|
||||
menu.addItem(sensorsMenu!)
|
||||
}
|
||||
sensorsMenu = generateSensorsMenu(type: SensorType.Voltage, value: value, action: action)
|
||||
if sensorsMenu != nil {
|
||||
menu.addItem(sensorsMenu!)
|
||||
}
|
||||
sensorsMenu = generateSensorsMenu(type: SensorType.Power, value: value, action: action)
|
||||
if sensorsMenu != nil {
|
||||
menu.addItem(sensorsMenu!)
|
||||
}
|
||||
}
|
||||
|
||||
private func generateSensorsMenu(type: SensorType, value: String, action: Selector?) -> NSMenuItem? {
|
||||
let list: [Sensor_t] = self.sensors.list.filter{ $0.type == type.rawValue }
|
||||
if list.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
let mainItem: NSMenuItem = NSMenuItem(title: type.rawValue, action: nil, keyEquivalent: "")
|
||||
mainItem.target = self
|
||||
mainItem.submenu = NSMenu()
|
||||
|
||||
var groups: [SensorGroup_t] = []
|
||||
list.forEach { (s: Sensor_t) in
|
||||
if !groups.contains(s.group) {
|
||||
groups.append(s.group)
|
||||
}
|
||||
}
|
||||
groups.sort()
|
||||
|
||||
groups.forEach { (g: SensorGroup_t) in
|
||||
mainItem.submenu!.addItem(NSMenuItem(title: g, action: nil, keyEquivalent: ""))
|
||||
|
||||
list.filter{ $0.group == g }.forEach { (s: Sensor_t) in
|
||||
let menuPoint: NSMenuItem = NSMenuItem(title: s.name, action: action, keyEquivalent: "")
|
||||
menuPoint.state = s.key == value ? NSControl.StateValue.on : NSControl.StateValue.off
|
||||
menuPoint.target = self
|
||||
menuPoint.extraString = s.key
|
||||
|
||||
mainItem.submenu!.addItem(menuPoint)
|
||||
}
|
||||
|
||||
mainItem.submenu!.addItem(NSMenuItem.separator())
|
||||
}
|
||||
|
||||
return mainItem
|
||||
}
|
||||
|
||||
@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.enabled = state
|
||||
menuBar!.reload(name: self.name)
|
||||
|
||||
if !state {
|
||||
menu.submenu = nil
|
||||
} else {
|
||||
menu.submenu = submenu
|
||||
}
|
||||
|
||||
self.restart()
|
||||
}
|
||||
|
||||
@objc func toggleValue1(_ sender: NSMenuItem) {
|
||||
let val: String = sender.extraString
|
||||
if self.value_1 == val {
|
||||
return
|
||||
}
|
||||
|
||||
let state = sender.state == NSControl.StateValue.on
|
||||
for item in self.submenu.items {
|
||||
item.state = NSControl.StateValue.off
|
||||
}
|
||||
|
||||
sender.state = state ? NSControl.StateValue.off : NSControl.StateValue.on
|
||||
self.defaults.set(val, forKey: "\(name)_value_1")
|
||||
self.value_1 = val
|
||||
self.initWidget()
|
||||
self.initMenu()
|
||||
menuBar!.reload(name: self.name)
|
||||
}
|
||||
|
||||
@objc func toggleValue2(_ sender: NSMenuItem) {
|
||||
let val: String = sender.extraString
|
||||
if self.value_2 == val {
|
||||
return
|
||||
}
|
||||
|
||||
let state = sender.state == NSControl.StateValue.on
|
||||
for item in self.submenu.items {
|
||||
item.state = NSControl.StateValue.off
|
||||
}
|
||||
|
||||
sender.state = state ? NSControl.StateValue.off : NSControl.StateValue.on
|
||||
self.defaults.set(val, forKey: "\(name)_value_2")
|
||||
self.value_2 = val
|
||||
self.initWidget()
|
||||
self.initMenu()
|
||||
menuBar!.reload(name: self.name)
|
||||
}
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
//
|
||||
// SensorsType.swift
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 06/04/2020.
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
typealias SensorGroup_t = String
|
||||
enum SensorGroup: SensorGroup_t {
|
||||
case CPU = "CPU"
|
||||
case GPU = "GPU"
|
||||
case System = "Systems"
|
||||
case Sensor = "Sensors"
|
||||
}
|
||||
|
||||
typealias SensorType_t = String
|
||||
enum SensorType: SensorType_t {
|
||||
case Temperature = "Temperature"
|
||||
case Voltage = "Voltage"
|
||||
case Power = "Power"
|
||||
case Frequency = "Frequency"
|
||||
case Battery = "Battery"
|
||||
}
|
||||
|
||||
struct Sensor_t {
|
||||
var name: String
|
||||
var key: String = ""
|
||||
|
||||
var group: SensorGroup_t
|
||||
var type: SensorType_t
|
||||
var unit: String {
|
||||
get {
|
||||
switch self.type{
|
||||
case SensorType.Temperature.rawValue:
|
||||
return "°"
|
||||
case SensorType.Voltage.rawValue:
|
||||
return "V"
|
||||
case SensorType.Power.rawValue:
|
||||
return "W"
|
||||
default: return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var value: Double? = nil
|
||||
|
||||
public mutating func update() {
|
||||
self.value = smc.getValue(self.key)
|
||||
}
|
||||
}
|
||||
|
||||
struct Sensors_t {
|
||||
var list: [Sensor_t] = []
|
||||
|
||||
init() {
|
||||
var available: [String] = smc.getAllKeys()
|
||||
var sensor: Sensor_t? = nil
|
||||
|
||||
available = available.filter({ (key: String) -> Bool in
|
||||
switch key.prefix(1) {
|
||||
case "T", "V", "P": return SensorsDict[key] != nil
|
||||
default: return false
|
||||
}
|
||||
})
|
||||
|
||||
available.forEach { (key: String) in
|
||||
sensor = SensorsDict[key]
|
||||
if sensor != nil {
|
||||
sensor!.value = smc.getValue(key)
|
||||
if sensor!.value != nil {
|
||||
sensor!.key = key
|
||||
self.list.append(sensor!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func find(byKey key: String) -> Sensor_t? {
|
||||
return self.list.first{ $0.key == key}
|
||||
}
|
||||
}
|
||||
|
||||
// List of keys: https://github.com/acidanthera/VirtualSMC/blob/master/Docs/SMCSensorKeys.txt
|
||||
let SensorsDict: [String: Sensor_t] = [
|
||||
/// Temperature
|
||||
"TA0P": Sensor_t(name: "Ambient 1", group: SensorGroup.Sensor.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"TA1P": Sensor_t(name: "Ambient 2", group: SensorGroup.Sensor.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"Th0H": Sensor_t(name: "Heatpipe 1", group: SensorGroup.Sensor.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"Th1H": Sensor_t(name: "Heatpipe 2", group: SensorGroup.Sensor.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"Th2H": Sensor_t(name: "Heatpipe 3", group: SensorGroup.Sensor.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"Th3H": Sensor_t(name: "Heatpipe 4", group: SensorGroup.Sensor.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"TZ0C": Sensor_t(name: "Termal zone 1", group: SensorGroup.Sensor.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"TZ1C": Sensor_t(name: "Termal zone 2", group: SensorGroup.Sensor.rawValue, type: SensorType.Temperature.rawValue),
|
||||
|
||||
"TC0F": Sensor_t(name: "CPU die", group: SensorGroup.CPU.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"TC0H": Sensor_t(name: "CPU heatsink", group: SensorGroup.CPU.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"TC0P": Sensor_t(name: "CPU proximity", group: SensorGroup.CPU.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"TC1C": Sensor_t(name: "CPU core 1", group: SensorGroup.CPU.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"TC2C": Sensor_t(name: "CPU core 2", group: SensorGroup.CPU.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"TC3C": Sensor_t(name: "CPU core 3", group: SensorGroup.CPU.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"TC4C": Sensor_t(name: "CPU core 4", group: SensorGroup.CPU.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"TC5C": Sensor_t(name: "CPU core 5", group: SensorGroup.CPU.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"TC6C": Sensor_t(name: "CPU core 6", group: SensorGroup.CPU.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"TC7C": Sensor_t(name: "CPU core 7", group: SensorGroup.CPU.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"TC8C": Sensor_t(name: "CPU core 8", group: SensorGroup.CPU.rawValue, type: SensorType.Temperature.rawValue),
|
||||
|
||||
"TCGC": Sensor_t(name: "GPU Intel Graphics", group: SensorGroup.GPU.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"TG0D": Sensor_t(name: "GPU die", group: SensorGroup.GPU.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"TG0H": Sensor_t(name: "GPU heatsink", group: SensorGroup.GPU.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"TG0P": Sensor_t(name: "GPU proximity", group: SensorGroup.GPU.rawValue, type: SensorType.Temperature.rawValue),
|
||||
|
||||
"Tm0P": Sensor_t(name: "Mainboard", group: SensorGroup.System.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"Tp0P": Sensor_t(name: "Powerboard", group: SensorGroup.System.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"TB1T": Sensor_t(name: "Battery", group: SensorGroup.System.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"TW0P": Sensor_t(name: "Airport", group: SensorGroup.System.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"TL0P": Sensor_t(name: "Display", group: SensorGroup.System.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"TI0P": Sensor_t(name: "Thunderbold 1", group: SensorGroup.System.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"TI1P": Sensor_t(name: "Thunderbold 2", group: SensorGroup.System.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"TI2P": Sensor_t(name: "Thunderbold 3", group: SensorGroup.System.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"TI3P": Sensor_t(name: "Thunderbold 4", group: SensorGroup.System.rawValue, type: SensorType.Temperature.rawValue),
|
||||
|
||||
"TN0D": Sensor_t(name: "Northbridge die", group: SensorGroup.System.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"TN0H": Sensor_t(name: "Northbridge heatsink", group: SensorGroup.System.rawValue, type: SensorType.Temperature.rawValue),
|
||||
"TN0P": Sensor_t(name: "Northbridge proximity", group: SensorGroup.System.rawValue, type: SensorType.Temperature.rawValue),
|
||||
|
||||
/// Voltage
|
||||
"VCAC": Sensor_t(name: "CPU IA", group: SensorGroup.CPU.rawValue, type: SensorType.Voltage.rawValue),
|
||||
"VCSC": Sensor_t(name: "CPU System Agent", group: SensorGroup.CPU.rawValue, type: SensorType.Voltage.rawValue),
|
||||
"VC0C": Sensor_t(name: "CPU Core 1", group: SensorGroup.CPU.rawValue, type: SensorType.Voltage.rawValue),
|
||||
"VC1C": Sensor_t(name: "CPU Core 2", group: SensorGroup.CPU.rawValue, type: SensorType.Voltage.rawValue),
|
||||
"VC2C": Sensor_t(name: "CPU Core 3", group: SensorGroup.CPU.rawValue, type: SensorType.Voltage.rawValue),
|
||||
"VC3C": Sensor_t(name: "CPU Core 4", group: SensorGroup.CPU.rawValue, type: SensorType.Voltage.rawValue),
|
||||
"VC4C": Sensor_t(name: "CPU Core 5", group: SensorGroup.CPU.rawValue, type: SensorType.Voltage.rawValue),
|
||||
"VC5C": Sensor_t(name: "CPU Core 6", group: SensorGroup.CPU.rawValue, type: SensorType.Voltage.rawValue),
|
||||
"VC6C": Sensor_t(name: "CPU Core 7", group: SensorGroup.CPU.rawValue, type: SensorType.Voltage.rawValue),
|
||||
"VC7C": Sensor_t(name: "CPU Core 8", group: SensorGroup.CPU.rawValue, type: SensorType.Voltage.rawValue),
|
||||
|
||||
"VCTC": Sensor_t(name: "GPU Intel Graphics", group: SensorGroup.GPU.rawValue, type: SensorType.Voltage.rawValue),
|
||||
"VG0C": Sensor_t(name: "GPU", group: SensorGroup.GPU.rawValue, type: SensorType.Voltage.rawValue),
|
||||
|
||||
"VM0R": Sensor_t(name: "Memory", group: SensorGroup.System.rawValue, type: SensorType.Voltage.rawValue),
|
||||
"Vb0R": Sensor_t(name: "CMOS", group: SensorGroup.System.rawValue, type: SensorType.Voltage.rawValue),
|
||||
|
||||
"VD0R": Sensor_t(name: "DC In", group: SensorGroup.Sensor.rawValue, type: SensorType.Voltage.rawValue),
|
||||
"VP0R": Sensor_t(name: "12V rail", group: SensorGroup.Sensor.rawValue, type: SensorType.Voltage.rawValue),
|
||||
"Vp0C": Sensor_t(name: "12V vcc", group: SensorGroup.Sensor.rawValue, type: SensorType.Voltage.rawValue),
|
||||
"VV2S": Sensor_t(name: "3V", group: SensorGroup.Sensor.rawValue, type: SensorType.Voltage.rawValue),
|
||||
"VR3R": Sensor_t(name: "3.3V", group: SensorGroup.Sensor.rawValue, type: SensorType.Voltage.rawValue),
|
||||
"VV1S": Sensor_t(name: "5V", group: SensorGroup.Sensor.rawValue, type: SensorType.Voltage.rawValue),
|
||||
"VV9S": Sensor_t(name: "12V", group: SensorGroup.Sensor.rawValue, type: SensorType.Voltage.rawValue),
|
||||
"VeES": Sensor_t(name: "PCI 12V", group: SensorGroup.Sensor.rawValue, type: SensorType.Voltage.rawValue),
|
||||
|
||||
/// Power
|
||||
"PCPC": Sensor_t(name: "CPU Package", group: SensorGroup.CPU.rawValue, type: SensorType.Power.rawValue),
|
||||
"PCPT": Sensor_t(name: "CPU Package total", group: SensorGroup.CPU.rawValue, type: SensorType.Power.rawValue),
|
||||
"PC0R": Sensor_t(name: "CPU Computing high side", group: SensorGroup.CPU.rawValue, type: SensorType.Power.rawValue),
|
||||
|
||||
"PCPG": Sensor_t(name: "GPU Intel Graphics", group: SensorGroup.GPU.rawValue, type: SensorType.Power.rawValue),
|
||||
"PG0R": Sensor_t(name: "GPU", group: SensorGroup.GPU.rawValue, type: SensorType.Power.rawValue),
|
||||
|
||||
"PPBR": Sensor_t(name: "Battery", group: SensorGroup.Sensor.rawValue, type: SensorType.Power.rawValue),
|
||||
"PDTR": Sensor_t(name: "DC In", group: SensorGroup.Sensor.rawValue, type: SensorType.Power.rawValue),
|
||||
"PSTR": Sensor_t(name: "System total", group: SensorGroup.Sensor.rawValue, type: SensorType.Power.rawValue),
|
||||
|
||||
/// Frequency
|
||||
"FRC0": Sensor_t(name: "CPU 1", group: SensorGroup.CPU.rawValue, type: SensorType.Frequency.rawValue),
|
||||
"FRC1": Sensor_t(name: "CPU 2", group: SensorGroup.CPU.rawValue, type: SensorType.Frequency.rawValue),
|
||||
"FRC2": Sensor_t(name: "CPU 3", group: SensorGroup.CPU.rawValue, type: SensorType.Frequency.rawValue),
|
||||
"FRC3": Sensor_t(name: "CPU 4", group: SensorGroup.CPU.rawValue, type: SensorType.Frequency.rawValue),
|
||||
"FRC4": Sensor_t(name: "CPU 5", group: SensorGroup.CPU.rawValue, type: SensorType.Frequency.rawValue),
|
||||
"FRC5": Sensor_t(name: "CPU 6", group: SensorGroup.CPU.rawValue, type: SensorType.Frequency.rawValue),
|
||||
"FRC6": Sensor_t(name: "CPU 7", group: SensorGroup.CPU.rawValue, type: SensorType.Frequency.rawValue),
|
||||
"FRC7": Sensor_t(name: "CPU 8", group: SensorGroup.CPU.rawValue, type: SensorType.Frequency.rawValue),
|
||||
|
||||
"CG0C": Sensor_t(name: "GPU", group: SensorGroup.GPU.rawValue, type: SensorType.Frequency.rawValue),
|
||||
"CG0S": Sensor_t(name: "GPU shader", group: SensorGroup.GPU.rawValue, type: SensorType.Frequency.rawValue),
|
||||
"CG0M": Sensor_t(name: "GPU memory", group: SensorGroup.GPU.rawValue, type: SensorType.Frequency.rawValue),
|
||||
|
||||
/// Battery
|
||||
"B0AV": Sensor_t(name: "Voltage", group: SensorGroup.Sensor.rawValue, type: SensorType.Battery.rawValue),
|
||||
"B0AC": Sensor_t(name: "Amperage", group: SensorGroup.Sensor.rawValue, type: SensorType.Battery.rawValue),
|
||||
]
|
||||
@@ -1,183 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="QiE-cC-2Ak">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14460.31"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Window Controller-->
|
||||
<scene sceneID="8HR-Pk-O94">
|
||||
<objects>
|
||||
<windowController storyboardIdentifier="AboutVC" showSeguePresentationStyle="single" id="LUd-pu-twQ" sceneMemberID="viewController">
|
||||
<window key="window" title="About Stats" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" titlebarAppearsTransparent="YES" titleVisibility="hidden" id="QOQ-ZU-enD">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="425" y="313" width="440" height="180"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1057"/>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="LUd-pu-twQ" id="h2N-hZ-Kra"/>
|
||||
</connections>
|
||||
</window>
|
||||
<connections>
|
||||
<segue destination="QiE-cC-2Ak" kind="relationship" relationship="window.shadowedContentViewController" id="CVD-cp-pGq"/>
|
||||
</connections>
|
||||
</windowController>
|
||||
<customObject id="iDr-bC-G86" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-163" y="55"/>
|
||||
</scene>
|
||||
<!--AboutVC-->
|
||||
<scene sceneID="acX-Wz-Ppn">
|
||||
<objects>
|
||||
<viewController showSeguePresentationStyle="single" id="QiE-cC-2Ak" customClass="AboutVC" customModule="Stats" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" id="rt1-Hc-A69">
|
||||
<rect key="frame" x="0.0" y="0.0" width="440" height="180"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<stackView distribution="fill" orientation="horizontal" alignment="top" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="d8s-oR-aVd">
|
||||
<rect key="frame" x="20" y="20" width="400" height="160"/>
|
||||
<subviews>
|
||||
<customView translatesAutoresizingMaskIntoConstraints="NO" id="GM4-w7-FBX">
|
||||
<rect key="frame" x="0.0" y="0.0" width="140" height="160"/>
|
||||
<subviews>
|
||||
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Bvo-ng-CVb">
|
||||
<rect key="frame" x="0.0" y="16" width="128" height="128"/>
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="AppIcon" id="T1x-rH-I2h"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="Bvo-ng-CVb" firstAttribute="centerY" secondItem="GM4-w7-FBX" secondAttribute="centerY" id="QxO-no-u5V"/>
|
||||
<constraint firstItem="Bvo-ng-CVb" firstAttribute="leading" secondItem="GM4-w7-FBX" secondAttribute="leading" id="t8e-fb-8bS"/>
|
||||
<constraint firstAttribute="width" constant="140" id="zzZ-Yq-fyL"/>
|
||||
</constraints>
|
||||
</customView>
|
||||
<stackView distribution="fill" orientation="vertical" alignment="leading" spacing="20" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="xYZ-jd-NgM">
|
||||
<rect key="frame" x="148" y="4" width="252" height="156"/>
|
||||
<subviews>
|
||||
<stackView distribution="fill" orientation="vertical" alignment="leading" spacing="0.0" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="5Xy-ma-TDb">
|
||||
<rect key="frame" x="0.0" y="95" width="252" height="61"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="I2K-b4-4R0">
|
||||
<rect key="frame" x="-2" y="17" width="78" height="44"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="Stats" id="4dC-76-a7n">
|
||||
<font key="font" metaFont="systemThin" size="36"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="GVE-z9-HI2">
|
||||
<rect key="frame" x="-2" y="0.0" width="49" height="17"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="Version" id="Nkt-Z9-Av0">
|
||||
<font key="font" metaFont="systemLight" size="13"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
</subviews>
|
||||
<visibilityPriorities>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
</visibilityPriorities>
|
||||
<customSpacing>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="hXC-tE-nSc">
|
||||
<rect key="frame" x="-2" y="41" width="234" height="34"/>
|
||||
<textFieldCell key="cell" title="Simple macOS system monitor in your menu bar" id="w6j-75-PxU">
|
||||
<font key="font" metaFont="systemLight" size="13"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<stackView distribution="fillEqually" orientation="horizontal" alignment="top" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="IBu-iz-oac">
|
||||
<rect key="frame" x="0.0" y="0.0" width="252" height="21"/>
|
||||
<subviews>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="jBn-hr-r9y">
|
||||
<rect key="frame" x="-6" y="-7" width="134" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Webpage" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="HtX-JA-Ozy">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="openLink:" target="QiE-cC-2Ak" id="7El-Ks-Xau"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ewS-nM-fd0">
|
||||
<rect key="frame" x="124" y="-7" width="134" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Close" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="xDy-hu-mXO">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="exit:" target="QiE-cC-2Ak" id="MUD-YE-JiL"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<visibilityPriorities>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
</visibilityPriorities>
|
||||
<customSpacing>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="IBu-iz-oac" secondAttribute="trailing" id="2r4-03-6C6"/>
|
||||
<constraint firstAttribute="trailing" secondItem="5Xy-ma-TDb" secondAttribute="trailing" id="BHW-gJ-qdK"/>
|
||||
<constraint firstItem="5Xy-ma-TDb" firstAttribute="leading" secondItem="xYZ-jd-NgM" secondAttribute="leading" id="TpI-3d-3ed"/>
|
||||
<constraint firstItem="IBu-iz-oac" firstAttribute="leading" secondItem="xYZ-jd-NgM" secondAttribute="leading" id="shP-Sm-N1e"/>
|
||||
</constraints>
|
||||
<visibilityPriorities>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
</visibilityPriorities>
|
||||
<customSpacing>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="GM4-w7-FBX" firstAttribute="leading" secondItem="d8s-oR-aVd" secondAttribute="leading" id="HVl-6t-W0k"/>
|
||||
<constraint firstAttribute="height" constant="160" id="ev6-MZ-oMf"/>
|
||||
<constraint firstAttribute="width" constant="400" id="jZQ-gt-h4Y"/>
|
||||
<constraint firstAttribute="bottom" secondItem="GM4-w7-FBX" secondAttribute="bottom" id="o0Z-kY-6vo"/>
|
||||
<constraint firstItem="GM4-w7-FBX" firstAttribute="top" secondItem="d8s-oR-aVd" secondAttribute="top" id="sdB-TX-uLp"/>
|
||||
</constraints>
|
||||
<visibilityPriorities>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
</visibilityPriorities>
|
||||
<customSpacing>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="d8s-oR-aVd" secondAttribute="trailing" constant="20" id="J2E-0b-nZb"/>
|
||||
<constraint firstAttribute="bottom" secondItem="d8s-oR-aVd" secondAttribute="bottom" constant="20" id="S3Y-bd-xoh"/>
|
||||
<constraint firstItem="d8s-oR-aVd" firstAttribute="leading" secondItem="rt1-Hc-A69" secondAttribute="leading" constant="20" id="nCP-al-GOl"/>
|
||||
<constraint firstItem="d8s-oR-aVd" firstAttribute="top" secondItem="rt1-Hc-A69" secondAttribute="top" id="snD-Ul-jpB"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="versionLabel" destination="GVE-z9-HI2" id="44o-1s-1eU"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<customObject id="orR-wl-rJd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-163" y="400"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="AppIcon" width="128" height="128"/>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -1,24 +1,24 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "baseline_apps_white_24pt_1x.png",
|
||||
"idiom" : "universal",
|
||||
"filename" : "baseline_build_black_18pt_1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "baseline_apps_white_24pt_2x.png",
|
||||
"idiom" : "universal",
|
||||
"filename" : "baseline_build_black_18pt_2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "baseline_apps_white_24pt_3x.png",
|
||||
"idiom" : "universal",
|
||||
"filename" : "baseline_build_black_18pt_3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
BIN
Stats/Supporting Files/Assets.xcassets/apps.imageset/baseline_apps_white_24pt_1x.png
vendored
Normal file
|
After Width: | Height: | Size: 90 B |
BIN
Stats/Supporting Files/Assets.xcassets/apps.imageset/baseline_apps_white_24pt_2x.png
vendored
Normal file
|
After Width: | Height: | Size: 93 B |
BIN
Stats/Supporting Files/Assets.xcassets/apps.imageset/baseline_apps_white_24pt_3x.png
vendored
Normal file
|
After Width: | Height: | Size: 98 B |
26
Stats/Supporting Files/Assets.xcassets/bug.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "baseline_bug_report_white_24pt_1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "baseline_bug_report_white_24pt_2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "baseline_bug_report_white_24pt_3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
Stats/Supporting Files/Assets.xcassets/bug.imageset/baseline_bug_report_white_24pt_1x.png
vendored
Normal file
|
After Width: | Height: | Size: 200 B |
BIN
Stats/Supporting Files/Assets.xcassets/bug.imageset/baseline_bug_report_white_24pt_2x.png
vendored
Normal file
|
After Width: | Height: | Size: 317 B |
BIN
Stats/Supporting Files/Assets.xcassets/bug.imageset/baseline_bug_report_white_24pt_3x.png
vendored
Normal file
|
After Width: | Height: | Size: 445 B |
26
Stats/Supporting Files/Assets.xcassets/chart.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "baseline_insert_chart_outlined_white_24pt_1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "baseline_insert_chart_outlined_white_24pt_2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "baseline_insert_chart_outlined_white_24pt_3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 162 B |
|
After Width: | Height: | Size: 215 B |
|
After Width: | Height: | Size: 305 B |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
21
Stats/Supporting Files/Assets.xcassets/devices/imac.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "imac.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Stats/Supporting Files/Assets.xcassets/devices/imac.imageset/imac.png
vendored
Normal file
|
After Width: | Height: | Size: 48 KiB |
21
Stats/Supporting Files/Assets.xcassets/devices/imacPro.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "imacPro.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Stats/Supporting Files/Assets.xcassets/devices/imacPro.imageset/imacPro.png
vendored
Normal file
|
After Width: | Height: | Size: 49 KiB |
21
Stats/Supporting Files/Assets.xcassets/devices/macMini.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "macMini.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Stats/Supporting Files/Assets.xcassets/devices/macMini.imageset/macMini.png
vendored
Normal file
|
After Width: | Height: | Size: 42 KiB |