* 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...
This commit is contained in:
Serhiy Mytrovtsiy
2020-06-07 12:22:32 +02:00
committed by GitHub
parent ee41e3ec7c
commit 4d6f759d3b
152 changed files with 8546 additions and 7632 deletions

3
.github/FUNDING.yml vendored
View File

@@ -1,3 +0,0 @@
# These are supported funding model platforms
github: exelban

2
.gitignore vendored
View File

@@ -8,3 +8,5 @@ xcuserdata
Stats.dmg
Stats.app
create-dmg
Cartfile.resolved

View File

@@ -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

View File

@@ -1,3 +1,2 @@
github "danielgindi/Charts" ~> 3.4.0
github "ashleymills/Reachability.swift" ~> 5.0.0
github "malcommac/Repeat" ~> 0.6.0

View File

@@ -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
View 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()
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 772 B

View 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>

View 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()
}
}

View 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()
}
}

View 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()
}
}

View 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()
})
}
}

View 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
View 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
View 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
View 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
View 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
View 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
}

View 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>

View 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>

View 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
View 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()
})
}
}

View 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
View 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
View 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
View 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
View 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
View 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
}
}

View 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
View 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
View 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
View 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
View 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")
}
}
}

View File

@@ -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;
}
struct disksList {
var list: [diskInfo] = []
func getDiskByBSDName(_ name: String) -> diskInfo? {
if let idx = self.list.firstIndex(where: { $0.mediaBSDName == name }) {
return self.list[idx]
public override func setup() {
self.interval = 10000
}
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? {

View 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
View 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>

View 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
View 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
View 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!)
})
}
}

View 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
View 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
View 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
View 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
View 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
View 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
}
}

View File

@@ -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>
[![Stats](https://serhiy.s3.eu-central-1.amazonaws.com/Github_repo/stats/cover%3Fv1.6.0.png)](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)

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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)
})
}
}
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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)
}
}

View File

@@ -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"
}
}

View File

@@ -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
}
}

View File

@@ -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)
}
}
}

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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)
})
}
}

View File

@@ -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
}
}

View File

@@ -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])
}
}
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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)])
}
}
}

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -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()
}
}

View File

@@ -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
}
}

View File

@@ -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])
}
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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])
})
}
}

View File

@@ -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)
}
}

View File

@@ -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),
]

View File

@@ -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>

View File

@@ -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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 B

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 B

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 B

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View 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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Some files were not shown because too many files have changed in this diff Show More