From c0bb81a49075dfc46db7c1f0926c0cf75d000133 Mon Sep 17 00:00:00 2001 From: Serhiy Mytrovtsiy Date: Tue, 25 Jun 2019 00:42:52 +0200 Subject: [PATCH 1/8] initialized network module; critical fixes in CPU and Memory modules --- Stats.xcodeproj/project.pbxproj | 22 +- Stats/AppDelegate.swift | 2 +- Stats/MenuBar.swift | 5 +- Stats/Modules/Battery/BatteryReader.swift | 4 +- Stats/Modules/CPU/CPU.swift | 4 +- Stats/Modules/CPU/CPUReader.swift | 4 +- Stats/Modules/Disk/DiskReader.swift | 4 +- Stats/Modules/Memory/Memory.swift | 2 + Stats/Modules/Memory/MemoryReader.swift | 4 +- Stats/Modules/Network/Network.swift | 131 +++++++ Stats/Modules/Network/NetworkReader.swift | 66 ++++ Stats/Widgets/BatteryView.swift | 5 +- Stats/Widgets/Chart.swift | 12 +- Stats/Widgets/Mini.swift | 10 +- Stats/Widgets/NetworkView.swift | 424 ++++++++++++++++++++++ Stats/libs/Extensions.swift | 76 +++- Stats/libs/Module.swift | 28 +- 17 files changed, 769 insertions(+), 34 deletions(-) create mode 100644 Stats/Modules/Network/Network.swift create mode 100644 Stats/Modules/Network/NetworkReader.swift create mode 100644 Stats/Widgets/NetworkView.swift diff --git a/Stats.xcodeproj/project.pbxproj b/Stats.xcodeproj/project.pbxproj index 32d4ae40..ab791504 100755 --- a/Stats.xcodeproj/project.pbxproj +++ b/Stats.xcodeproj/project.pbxproj @@ -15,6 +15,9 @@ 9A57A18522A1D26D0033E318 /* MenuBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A57A18422A1D26D0033E318 /* MenuBar.swift */; }; 9A57A19B22A1E1C50033E318 /* Module.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A57A19A22A1E1C50033E318 /* Module.swift */; }; 9A57A19D22A1E3270033E318 /* CPU.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A57A19C22A1E3270033E318 /* CPU.swift */; }; + 9A58D1B022C150C800405315 /* Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A58D1AF22C150C800405315 /* Network.swift */; }; + 9A58D1B222C150D700405315 /* NetworkReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A58D1B122C150D700405315 /* NetworkReader.swift */; }; + 9A58D1B422C179B200405315 /* NetworkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A58D1B322C179B200405315 /* NetworkView.swift */; }; 9A5B1CBF229E78F0008B9D3C /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A5B1CBE229E78F0008B9D3C /* Observable.swift */; }; 9A5B1CC5229E7B40008B9D3C /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A5B1CC4229E7B40008B9D3C /* Extensions.swift */; }; 9A6CFC0122A1C9F5001E782D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9A6CFC0022A1C9F5001E782D /* Assets.xcassets */; }; @@ -58,6 +61,9 @@ 9A57A18422A1D26D0033E318 /* MenuBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBar.swift; sourceTree = ""; }; 9A57A19A22A1E1C50033E318 /* Module.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Module.swift; sourceTree = ""; }; 9A57A19C22A1E3270033E318 /* CPU.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CPU.swift; sourceTree = ""; }; + 9A58D1AF22C150C800405315 /* Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Network.swift; sourceTree = ""; }; + 9A58D1B122C150D700405315 /* NetworkReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkReader.swift; sourceTree = ""; }; + 9A58D1B322C179B200405315 /* NetworkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkView.swift; sourceTree = ""; }; 9A5B1CBE229E78F0008B9D3C /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; 9A5B1CC4229E7B40008B9D3C /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; 9A6CFC0022A1C9F5001E782D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -138,6 +144,15 @@ path = Stats; sourceTree = ""; }; + 9A58D1AE22C150B800405315 /* Network */ = { + isa = PBXGroup; + children = ( + 9A58D1AF22C150C800405315 /* Network.swift */, + 9A58D1B122C150D700405315 /* NetworkReader.swift */, + ); + path = Network; + sourceTree = ""; + }; 9A5B1CB3229E72A7008B9D3C /* Supporting Files */ = { isa = PBXGroup; children = ( @@ -153,10 +168,11 @@ 9A5B1CBA229E7892008B9D3C /* Modules */ = { isa = PBXGroup; children = ( - 9A09C89C22B3A7BB0018426F /* Battery */, 9A7B8F5C22A2926500DEB352 /* CPU */, 9A7B8F6222A2C17000DEB352 /* Memory */, 9A7B8F6322A2C17500DEB352 /* Disk */, + 9A09C89C22B3A7BB0018426F /* Battery */, + 9A58D1AE22C150B800405315 /* Network */, ); path = Modules; sourceTree = ""; @@ -177,6 +193,7 @@ 9A09C8A122B3D94D0018426F /* BatteryView.swift */, 9A74D59322B4315C004FE1FA /* Chart.swift */, 9A74D59622B44498004FE1FA /* Mini.swift */, + 9A58D1B322C179B200405315 /* NetworkView.swift */, ); path = Widgets; sourceTree = ""; @@ -336,11 +353,14 @@ 9A7B8F6922A2C3A100DEB352 /* Memory.swift in Sources */, 9A7B8F5E22A2A57600DEB352 /* CPUReader.swift in Sources */, 9A74D59422B4315C004FE1FA /* Chart.swift in Sources */, + 9A58D1B422C179B200405315 /* NetworkView.swift in Sources */, 9A09C89E22B3A7C90018426F /* Battery.swift in Sources */, 9A7B8F6D22A2C3D600DEB352 /* MemoryReader.swift in Sources */, 9A57A18522A1D26D0033E318 /* MenuBar.swift in Sources */, 9A57A19D22A1E3270033E318 /* CPU.swift in Sources */, + 9A58D1B222C150D700405315 /* NetworkReader.swift in Sources */, 9A09C8A022B3A7E20018426F /* BatteryReader.swift in Sources */, + 9A58D1B022C150C800405315 /* Network.swift in Sources */, 9A57A19B22A1E1C50033E318 /* Module.swift in Sources */, 9A5B1CBF229E78F0008B9D3C /* Observable.swift in Sources */, 9A7B8F6B22A2C3A700DEB352 /* Disk.swift in Sources */, diff --git a/Stats/AppDelegate.swift b/Stats/AppDelegate.swift index f89bc787..fe91ae30 100755 --- a/Stats/AppDelegate.swift +++ b/Stats/AppDelegate.swift @@ -13,7 +13,7 @@ extension Notification.Name { static let killLauncher = Notification.Name("killLauncher") } -let modules: Observable<[Module]> = Observable([CPU(), Memory(), Disk(), Battery()]) +let modules: Observable<[Module]> = Observable([CPU(), Memory(), Disk(), Battery(), Network()]) let colors: Observable = Observable(true) @NSApplicationMain diff --git a/Stats/MenuBar.swift b/Stats/MenuBar.swift index 2dc09aad..40b8f0a6 100644 --- a/Stats/MenuBar.swift +++ b/Stats/MenuBar.swift @@ -9,8 +9,9 @@ import Cocoa import ServiceManagement -let MODULE_HEIGHT = CGFloat(NSApplication.shared.mainMenu?.menuBarHeight ?? 22) -let MODULE_WIDTH = CGFloat(32) +let MODULE_HEIGHT: CGFloat = NSApplication.shared.mainMenu?.menuBarHeight ?? 22 +let MODULE_WIDTH: CGFloat = 32 +let MODULE_MARGIN: CGFloat = 2 class MenuBar { let defaults = UserDefaults.standard diff --git a/Stats/Modules/Battery/BatteryReader.swift b/Stats/Modules/Battery/BatteryReader.swift index 668caa43..092d166b 100644 --- a/Stats/Modules/Battery/BatteryReader.swift +++ b/Stats/Modules/Battery/BatteryReader.swift @@ -10,7 +10,7 @@ import Foundation import IOKit.ps class BatteryReader: Reader { - var usage: Observable! + var usage: Observable! var available: Bool = false var updateTimer: Timer! @@ -49,7 +49,7 @@ class BatteryReader: Reader { cap = 0 - cap } - self.usage << Float(cap) + self.usage << Double(cap) } } } diff --git a/Stats/Modules/CPU/CPU.swift b/Stats/Modules/CPU/CPU.swift index cd6a5ef7..24a996c7 100644 --- a/Stats/Modules/CPU/CPU.swift +++ b/Stats/Modules/CPU/CPU.swift @@ -101,6 +101,8 @@ class CPU: Module { sender.state = sender.state == NSControl.StateValue.on ? NSControl.StateValue.off : NSControl.StateValue.on self.defaults.set(widgetCode, forKey: "\(name)_widget") self.widgetType = widgetCode - self.initWidget() + self.active << false + initWidget() + self.active << true } } diff --git a/Stats/Modules/CPU/CPUReader.swift b/Stats/Modules/CPU/CPUReader.swift index 45cf4fb0..96f155e3 100644 --- a/Stats/Modules/CPU/CPUReader.swift +++ b/Stats/Modules/CPU/CPUReader.swift @@ -9,7 +9,7 @@ import Foundation class CPUReader: Reader { - var usage: Observable! + var usage: Observable! var available: Bool = true var cpuInfo: processor_info_array_t! var prevCpuInfo: processor_info_array_t? @@ -77,7 +77,7 @@ class CPUReader: Reader { inUseOnAllCores = inUseOnAllCores + inUse totalOnAllCores = totalOnAllCores + total } - self.usage << (Float(inUseOnAllCores) / Float(totalOnAllCores)) + self.usage << (Double(inUseOnAllCores) / Double(totalOnAllCores)) CPUUsageLock.unlock() diff --git a/Stats/Modules/Disk/DiskReader.swift b/Stats/Modules/Disk/DiskReader.swift index 824c0e6b..2d69be2f 100644 --- a/Stats/Modules/Disk/DiskReader.swift +++ b/Stats/Modules/Disk/DiskReader.swift @@ -9,7 +9,7 @@ import Foundation class DiskReader: Reader { - var usage: Observable! + var usage: Observable! var available: Bool = true var updateTimer: Timer! @@ -38,7 +38,7 @@ class DiskReader: Reader { let free = freeDiskSpaceInBytes() let usedSpace = total - free - self.usage << (Float(usedSpace) / Float(total)) + self.usage << (Double(usedSpace) / Double(total)) } func totalDiskSpaceInBytes() -> Int64 { diff --git a/Stats/Modules/Memory/Memory.swift b/Stats/Modules/Memory/Memory.swift index f229ad47..28e11d3a 100644 --- a/Stats/Modules/Memory/Memory.swift +++ b/Stats/Modules/Memory/Memory.swift @@ -102,6 +102,8 @@ class Memory: Module { sender.state = sender.state == NSControl.StateValue.on ? NSControl.StateValue.off : NSControl.StateValue.on self.defaults.set(widgetCode, forKey: "\(name)_widget") self.widgetType = widgetCode + self.active << false self.initWidget() + self.active << true } } diff --git a/Stats/Modules/Memory/MemoryReader.swift b/Stats/Modules/Memory/MemoryReader.swift index 14d08c22..fbcffb51 100644 --- a/Stats/Modules/Memory/MemoryReader.swift +++ b/Stats/Modules/Memory/MemoryReader.swift @@ -9,7 +9,7 @@ import Foundation class MemoryReader: Reader { - var usage: Observable! + var usage: Observable! var available: Bool = true var updateTimer: Timer! var totalSize: Float @@ -68,7 +68,7 @@ class MemoryReader: Reader { let compressed = Float(stats.compressor_page_count) * Float(PAGE_SIZE) let free = totalSize - (active + wired + compressed) - self.usage << ((totalSize - free) / totalSize) + self.usage << Double((totalSize - free) / totalSize) } else { print("Error with host_statistics64(): " + (String(cString: mach_error_string(kerr), encoding: String.Encoding.ascii) ?? "unknown error")) diff --git a/Stats/Modules/Network/Network.swift b/Stats/Modules/Network/Network.swift new file mode 100644 index 00000000..7670ed72 --- /dev/null +++ b/Stats/Modules/Network/Network.swift @@ -0,0 +1,131 @@ +// +// Network.swift +// Stats +// +// Created by Serhiy Mytrovtsiy on 24.06.2019. +// Copyright © 2019 Serhiy Mytrovtsiy. All rights reserved. +// + +import Cocoa + +class Network: Module { + var name: String = "Network" + var shortName: String = "NET" + var view: NSView = NSView() + var menu: NSMenuItem = NSMenuItem() + var submenu: NSMenu = NSMenu() + var active: Observable + var available: Observable + var reader: Reader = NetworkReader() + var widgetType: WidgetType = 2.0 + + let defaults = UserDefaults.standard + + init() { + self.available = Observable(self.reader.available) + self.active = Observable(defaults.object(forKey: name) != nil ? defaults.bool(forKey: name) : true) + self.widgetType = defaults.object(forKey: "\(name)_widget") != nil ? defaults.float(forKey: "\(name)_widget") : Widgets.Dots + initMenu() + initWidget() + } + + func start() { + self.reader.start() + + self.reader.usage.subscribe(observer: self) { (value, _) in + if !value.isNaN { + (self.view as! Widget).value(value: value) + } + } + } + + 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.widgetType == Widgets.Dots ? NSControl.StateValue.on : NSControl.StateValue.off + dots.target = self + + let arrows = NSMenuItem(title: "Arrows", action: #selector(toggleWidget), keyEquivalent: "") + arrows.state = self.widgetType == Widgets.Arrows ? NSControl.StateValue.on : NSControl.StateValue.off + arrows.target = self + + let text = NSMenuItem(title: "Text", action: #selector(toggleWidget), keyEquivalent: "") + text.state = self.widgetType == Widgets.Text ? NSControl.StateValue.on : NSControl.StateValue.off + text.target = self + + let dotsWithText = NSMenuItem(title: "Dots with text", action: #selector(toggleWidget), keyEquivalent: "") + dotsWithText.state = self.widgetType == Widgets.DotsWithText ? NSControl.StateValue.on : NSControl.StateValue.off + dotsWithText.target = self + + let arrowsWithText = NSMenuItem(title: "Arrows with text", action: #selector(toggleWidget), keyEquivalent: "") + arrowsWithText.state = self.widgetType == Widgets.ArrowsWithText ? NSControl.StateValue.on : NSControl.StateValue.off + arrowsWithText.target = self + + submenu.addItem(dots) + submenu.addItem(arrows) + submenu.addItem(text) + submenu.addItem(dotsWithText) + submenu.addItem(arrowsWithText) + + menu.submenu = submenu + } + + @objc func toggle(_ sender: NSMenuItem) { + let state = sender.state != NSControl.StateValue.on + + sender.state = sender.state == NSControl.StateValue.on ? NSControl.StateValue.off : NSControl.StateValue.on + self.defaults.set(state, forKey: name) + self.active << state + + if !state { + self.stop() + } else { + self.start() + } + } + + @objc func toggleWidget(_ sender: NSMenuItem) { + var widgetCode: Float = 0.0 + + switch sender.title { + case "Dots": + widgetCode = Widgets.Dots + case "Arrows": + widgetCode = Widgets.Arrows + case "Text": + widgetCode = Widgets.Text + case "Dots with text": + widgetCode = Widgets.DotsWithText + case "Arrows with text": + widgetCode = Widgets.ArrowsWithText + default: + break + } + + if self.widgetType == 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.state = NSControl.StateValue.off + } + } + + sender.state = sender.state == NSControl.StateValue.on ? NSControl.StateValue.off : NSControl.StateValue.on + self.defaults.set(widgetCode, forKey: "\(name)_widget") + self.widgetType = widgetCode + self.active << false + initWidget() + self.active << true + } +} diff --git a/Stats/Modules/Network/NetworkReader.swift b/Stats/Modules/Network/NetworkReader.swift new file mode 100644 index 00000000..1e10b42b --- /dev/null +++ b/Stats/Modules/Network/NetworkReader.swift @@ -0,0 +1,66 @@ +// +// NetworkReader.swift +// Stats +// +// Created by Serhiy Mytrovtsiy on 24.06.2019. +// Copyright © 2019 Serhiy Mytrovtsiy. All rights reserved. +// + +import Cocoa + +class NetworkReader: Reader { + var usage: Observable! + var available: Bool = true + var updateTimer: Timer! + + var netProcess: Process = Process() + var pipe: Pipe = Pipe() + + init() { + self.usage = Observable(0) + netProcess.launchPath = "/usr/bin/env" + netProcess.arguments = ["netstat", "-w1", "-l", "en0"] + netProcess.standardOutput = pipe + } + + func start() { + if netProcess.isRunning { + return + } + self.pipe.fileHandleForReading.waitForDataInBackgroundAndNotify() + + NotificationCenter.default.addObserver(forName: NSNotification.Name.NSFileHandleDataAvailable, object: self.pipe.fileHandleForReading , queue: nil) { _ -> Void in + defer { + self.pipe.fileHandleForReading.waitForDataInBackgroundAndNotify() + } + + let output = self.pipe.fileHandleForReading.availableData + if output.isEmpty { + return + } + + let outputString = String(data: output, encoding: String.Encoding.utf8) ?? "" + let arr = outputString.condenseWhitespace().split(separator: " ") + + if !arr.isEmpty && Int64(arr[0]) != nil { + guard let download = Int64(arr[2]), let upload = Int64(arr[5]) else { + return + } + + guard let value: Double = Double("\(download).\(upload)") else { + return + } + + self.usage << value + } + } + + netProcess.launch() + } + + func stop() { + netProcess.interrupt() + } + + func read() {} +} diff --git a/Stats/Widgets/BatteryView.swift b/Stats/Widgets/BatteryView.swift index b85be446..8db7eb15 100644 --- a/Stats/Widgets/BatteryView.swift +++ b/Stats/Widgets/BatteryView.swift @@ -12,7 +12,7 @@ class BatteryView: NSView, Widget { let batteryWidth: CGFloat = 32 let percentageWidth: CGFloat = 40 - var value: Float { + var value: Double { didSet { self.redraw() } @@ -90,7 +90,6 @@ class BatteryView: NSView, Widget { func percentageView() { if self.percentage { percentageValue = NSTextField(frame: NSMakeRect(0, 0, percentageWidth, self.frame.size.height - 2)) - percentageValue.textColor = NSColor.red percentageValue.isEditable = false percentageValue.isSelectable = false percentageValue.isBezeled = false @@ -118,7 +117,7 @@ class BatteryView: NSView, Widget { setNeedsDisplay(self.frame) } - func value(value: Float) { + func value(value: Double) { if self.value != value { self.value = value diff --git a/Stats/Widgets/Chart.swift b/Stats/Widgets/Chart.swift index a5244493..96e5e441 100644 --- a/Stats/Widgets/Chart.swift +++ b/Stats/Widgets/Chart.swift @@ -10,7 +10,7 @@ import Cocoa class Chart: NSView, Widget { var height: CGFloat = 0.0 - var points: [Float] { + var points: [Double] { didSet { self.redraw() } @@ -30,7 +30,7 @@ class Chart: NSView, Widget { override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) - let lineColor: NSColor = NSColor.selectedMenuItemColor + 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) let context = NSGraphicsContext.current!.cgContext @@ -83,7 +83,7 @@ class Chart: NSView, Widget { setNeedsDisplay(self.frame) } - func value(value: Float) { + func value(value: Double) { if self.points.count < 50 { self.points.append(value) return @@ -129,9 +129,9 @@ class ChartWithValue: Chart { fatalError("init(coder:) has not been implemented") } - override func value(value: Float) { - self.valueLabel.stringValue = "\(Int(Float(Float(value).roundTo(decimalPlaces: 2))! * 100))%" - self.valueLabel.textColor = Float(value).usageColor() + override func value(value: Double) { + self.valueLabel.stringValue = "\(Int(Float(value.roundTo(decimalPlaces: 2))! * 100))%" + self.valueLabel.textColor = value.usageColor() if self.points.count < 50 { self.points.append(value) diff --git a/Stats/Widgets/Mini.swift b/Stats/Widgets/Mini.swift index 64003cd3..a057455e 100644 --- a/Stats/Widgets/Mini.swift +++ b/Stats/Widgets/Mini.swift @@ -12,7 +12,7 @@ class Mini: NSView, Widget { var valueView: NSTextField = NSTextField() var labelView: NSTextField = NSTextField() - var value: Float = 0 + var value: Double = 0 var label: String = "" { didSet { self.labelView.stringValue = label @@ -66,17 +66,17 @@ class Mini: NSView, Widget { } func redraw() { - self.valueView.textColor = Float(self.value).usageColor() + self.valueView.textColor = self.value.usageColor() self.needsDisplay = true setNeedsDisplay(self.frame) } - func value(value: Float) { + func value(value: Double) { if self.value != value { self.value = value - self.valueView.stringValue = "\(Int(Float(Float(value).roundTo(decimalPlaces: 2))! * 100))%" - self.valueView.textColor = Float(value).usageColor() + self.valueView.stringValue = "\(Int(Float(value.roundTo(decimalPlaces: 2))! * 100))%" + self.valueView.textColor = value.usageColor() } } } diff --git a/Stats/Widgets/NetworkView.swift b/Stats/Widgets/NetworkView.swift new file mode 100644 index 00000000..ada4c2fe --- /dev/null +++ b/Stats/Widgets/NetworkView.swift @@ -0,0 +1,424 @@ +// +// NetworkView.swift +// Stats +// +// Created by Samuel Grant on 24.06.2019. +// Copyright © 2019 Serhiy Mytrovtsiy. All rights reserved. +// + +import Cocoa + +class NetworkDotsView: NSView, Widget { + var download: Int64 { + didSet { + self.redraw() + } + } + var upload: Int64 { + didSet { + self.redraw() + } + } + + override init(frame: NSRect) { + self.download = 0 + self.upload = 0 + super.init(frame: CGRect(x: 0, y: 0, width: 12, height: frame.size.height)) + self.wantsLayer = true + self.addSubview(NSView()) + } + + required init?(coder decoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func draw(_ dirtyRect: NSRect) { + super.draw(dirtyRect) + + let workingHeight: CGFloat = (self.frame.size.height - (MODULE_MARGIN * 2)) + let height: CGFloat = ((workingHeight - MODULE_MARGIN) / 2) - 1 + + var uploadCircle = NSBezierPath() + uploadCircle = NSBezierPath(ovalIn: CGRect(x: MODULE_MARGIN, y: height + (MODULE_MARGIN * 2) + 1, width: height, height: height)) + if self.upload != 0 { + NSColor.red.setFill() + } else { + NSColor.labelColor.setFill() + } + uploadCircle.fill() + + var downloadCircle = NSBezierPath() + downloadCircle = NSBezierPath(ovalIn: CGRect(x: MODULE_MARGIN, y: MODULE_MARGIN, width: height, height: height)) + if self.download != 0 { + NSColor(red: (26/255.0), green: (126/255.0), blue: (252/255.0), alpha: 0.8).setFill() + } else { + NSColor.labelColor.setFill() + } + downloadCircle.fill() + } + + func redraw() { + self.needsDisplay = true + setNeedsDisplay(self.frame) + } + + func value(value: Double) { + let values = value.splitAtDecimal() + if self.download != values[0] { + self.download = values[0] + } + if self.upload != values[1] { + self.upload = values[1] + } + } +} + +class NetworkTextView: NSView, Widget { + var downloadValue: NSTextField = NSTextField() + var uploadValue: NSTextField = NSTextField() + + override init(frame: NSRect) { + super.init(frame: CGRect(x: 0, y: 0, width: MODULE_WIDTH + 20, height: frame.size.height)) + self.wantsLayer = true + self.valueView() + } + + required init?(coder decoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func draw(_ dirtyRect: NSRect) { + super.draw(dirtyRect) + } + + func redraw() { + self.needsDisplay = true + setNeedsDisplay(self.frame) + } + + func value(value: Double) { + let values = value.splitAtDecimal() + downloadValue.stringValue = Units(bytes: values[0]).getReadableUnit() + uploadValue.stringValue = Units(bytes: values[1]).getReadableUnit() + } + + func valueView() { + downloadValue = NSTextField(frame: NSMakeRect(MODULE_MARGIN, MODULE_MARGIN, self.frame.size.width - MODULE_MARGIN, 9)) + downloadValue.isEditable = false + downloadValue.isSelectable = false + downloadValue.isBezeled = false + downloadValue.wantsLayer = true + downloadValue.textColor = .labelColor + downloadValue.backgroundColor = .controlColor + downloadValue.canDrawSubviewsIntoLayer = true + downloadValue.alignment = .right + downloadValue.font = NSFont.systemFont(ofSize: 9, weight: .light) + downloadValue.stringValue = "0 KB/s" + + uploadValue = NSTextField(frame: NSMakeRect(MODULE_MARGIN, self.frame.size.height - 10, self.frame.size.width - MODULE_MARGIN, 9)) + uploadValue.isEditable = false + uploadValue.isSelectable = false + uploadValue.isBezeled = false + uploadValue.wantsLayer = true + uploadValue.textColor = .labelColor + uploadValue.backgroundColor = .controlColor + uploadValue.canDrawSubviewsIntoLayer = true + uploadValue.alignment = .right + uploadValue.font = NSFont.systemFont(ofSize: 9, weight: .light) + uploadValue.stringValue = "0 KB/s" + + self.addSubview(downloadValue) + self.addSubview(uploadValue) + } +} + +class NetworkArrowsView: NSView, Widget { + var download: Int64 { + didSet { + self.redraw() + } + } + var upload: Int64 { + didSet { + self.redraw() + } + } + + override init(frame: NSRect) { + self.download = 0 + self.upload = 0 + super.init(frame: CGRect(x: 0, y: 0, width: 8, height: frame.size.height)) + self.wantsLayer = true + self.addSubview(NSView()) + } + + required init?(coder decoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func draw(_ dirtyRect: NSRect) { + super.draw(dirtyRect) + + let arrowAngle = CGFloat(Double.pi / 5) + let pointerLineLength: CGFloat = 3.5 + let workingHeight: CGFloat = (self.frame.size.height - (MODULE_MARGIN * 2)) + let height: CGFloat = ((workingHeight - MODULE_MARGIN) / 2) + + let downloadArrow = NSBezierPath() + let downloadStart = CGPoint(x: MODULE_MARGIN + (pointerLineLength/2), y: height + MODULE_MARGIN) + let downloadEnd = CGPoint(x: MODULE_MARGIN + (pointerLineLength/2), y: MODULE_MARGIN) + + downloadArrow.addArrow(start: downloadStart, end: downloadEnd, pointerLineLength: pointerLineLength, arrowAngle: arrowAngle) + + if self.download != 0 { + 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: MODULE_MARGIN + (pointerLineLength/2), y: height + (MODULE_MARGIN * 2)) + let uploadEnd = CGPoint(x: MODULE_MARGIN + (pointerLineLength/2), y: (MODULE_MARGIN * 2) + (height * 2)) + + uploadArrow.addArrow(start: uploadStart, end: uploadEnd, pointerLineLength: pointerLineLength, arrowAngle: arrowAngle) + + if self.upload != 0 { + NSColor.red.set() + } else { + NSColor.labelColor.set() + } + uploadArrow.lineWidth = 1 + uploadArrow.stroke() + uploadArrow.close() + } + + func redraw() { + self.needsDisplay = true + setNeedsDisplay(self.frame) + } + + func value(value: Double) { + let values = value.splitAtDecimal() + if self.download != values[0] { + self.download = values[0] + } + if self.upload != values[1] { + self.upload = values[1] + } + } +} + +class NetworkDotsTextView: NSView, Widget { + var download: Int64 { + didSet { + self.redraw() + } + } + var upload: Int64 { + didSet { + self.redraw() + } + } + + var downloadValue: NSTextField = NSTextField() + var uploadValue: NSTextField = NSTextField() + + override init(frame: NSRect) { + self.download = 0 + self.upload = 0 + super.init(frame: CGRect(x: 0, y: 0, width: MODULE_WIDTH + 26, height: frame.size.height)) + self.wantsLayer = true + self.valueView() + } + + required init?(coder decoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func draw(_ dirtyRect: NSRect) { + super.draw(dirtyRect) + + let workingHeight: CGFloat = (self.frame.size.height - (MODULE_MARGIN * 2)) + let height: CGFloat = ((workingHeight - MODULE_MARGIN) / 2) - 1 + + var uploadCircle = NSBezierPath() + uploadCircle = NSBezierPath(ovalIn: CGRect(x: MODULE_MARGIN, y: height + (MODULE_MARGIN * 2) + 1, width: height, height: height)) + if self.upload != 0 { + NSColor.red.setFill() + } else { + NSColor.labelColor.setFill() + } + uploadCircle.fill() + + var downloadCircle = NSBezierPath() + downloadCircle = NSBezierPath(ovalIn: CGRect(x: MODULE_MARGIN, y: MODULE_MARGIN, width: height, height: height)) + if self.download != 0 { + NSColor(red: (26/255.0), green: (126/255.0), blue: (252/255.0), alpha: 0.8).setFill() + } else { + NSColor.labelColor.setFill() + } + downloadCircle.fill() + } + + func redraw() { + self.needsDisplay = true + setNeedsDisplay(self.frame) + } + + func value(value: Double) { + let values = value.splitAtDecimal() + if self.download != values[0] { + self.download = values[0] + downloadValue.stringValue = Units(bytes: self.download).getReadableUnit() + } + if self.upload != values[1] { + self.upload = values[1] + uploadValue.stringValue = Units(bytes: self.upload).getReadableUnit() + } + } + + func valueView() { + downloadValue = NSTextField(frame: NSMakeRect(MODULE_MARGIN, MODULE_MARGIN, self.frame.size.width - MODULE_MARGIN, 9)) + downloadValue.isEditable = false + downloadValue.isSelectable = false + downloadValue.isBezeled = false + downloadValue.wantsLayer = true + downloadValue.textColor = .labelColor + downloadValue.backgroundColor = .controlColor + downloadValue.canDrawSubviewsIntoLayer = true + downloadValue.alignment = .right + downloadValue.font = NSFont.systemFont(ofSize: 9, weight: .light) + downloadValue.stringValue = "0 KB/s" + + uploadValue = NSTextField(frame: NSMakeRect(MODULE_MARGIN, self.frame.size.height - 10, self.frame.size.width - MODULE_MARGIN, 9)) + uploadValue.isEditable = false + uploadValue.isSelectable = false + uploadValue.isBezeled = false + uploadValue.wantsLayer = true + uploadValue.textColor = .labelColor + uploadValue.backgroundColor = .controlColor + uploadValue.canDrawSubviewsIntoLayer = true + uploadValue.alignment = .right + uploadValue.font = NSFont.systemFont(ofSize: 9, weight: .light) + uploadValue.stringValue = "0 KB/s" + + self.addSubview(downloadValue) + self.addSubview(uploadValue) + } +} + +class NetworkArrowsTextView: NSView, Widget { + var download: Int64 { + didSet { + self.redraw() + } + } + var upload: Int64 { + didSet { + self.redraw() + } + } + + var downloadValue: NSTextField = NSTextField() + var uploadValue: NSTextField = NSTextField() + + override init(frame: NSRect) { + self.download = 0 + self.upload = 0 + super.init(frame: CGRect(x: 0, y: 0, width: MODULE_WIDTH + 24, height: frame.size.height)) + self.wantsLayer = true + self.valueView() + } + + required init?(coder decoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func draw(_ dirtyRect: NSRect) { + super.draw(dirtyRect) + + let arrowAngle = CGFloat(Double.pi / 5) + let pointerLineLength: CGFloat = 3.5 + let workingHeight: CGFloat = (self.frame.size.height - (MODULE_MARGIN * 2)) + let height: CGFloat = ((workingHeight - MODULE_MARGIN) / 2) + + let downloadArrow = NSBezierPath() + let downloadStart = CGPoint(x: MODULE_MARGIN + (pointerLineLength/2), y: height + MODULE_MARGIN) + let downloadEnd = CGPoint(x: MODULE_MARGIN + (pointerLineLength/2), y: MODULE_MARGIN) + + downloadArrow.addArrow(start: downloadStart, end: downloadEnd, pointerLineLength: pointerLineLength, arrowAngle: arrowAngle) + + if self.download != 0 { + 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: MODULE_MARGIN + (pointerLineLength/2), y: height + (MODULE_MARGIN * 2)) + let uploadEnd = CGPoint(x: MODULE_MARGIN + (pointerLineLength/2), y: (MODULE_MARGIN * 2) + (height * 2)) + + uploadArrow.addArrow(start: uploadStart, end: uploadEnd, pointerLineLength: pointerLineLength, arrowAngle: arrowAngle) + + if self.upload != 0 { + NSColor.red.set() + } else { + NSColor.labelColor.set() + } + uploadArrow.lineWidth = 1 + uploadArrow.stroke() + uploadArrow.close() + } + + func redraw() { + self.needsDisplay = true + setNeedsDisplay(self.frame) + } + + func value(value: Double) { + let values = value.splitAtDecimal() + if self.download != values[0] { + self.download = values[0] + downloadValue.stringValue = Units(bytes: self.download).getReadableUnit() + } + if self.upload != values[1] { + self.upload = values[1] + uploadValue.stringValue = Units(bytes: self.upload).getReadableUnit() + } + } + + func valueView() { + downloadValue = NSTextField(frame: NSMakeRect(MODULE_MARGIN, MODULE_MARGIN, self.frame.size.width - MODULE_MARGIN, 9)) + downloadValue.isEditable = false + downloadValue.isSelectable = false + downloadValue.isBezeled = false + downloadValue.wantsLayer = true + downloadValue.textColor = .labelColor + downloadValue.backgroundColor = .controlColor + downloadValue.canDrawSubviewsIntoLayer = true + downloadValue.alignment = .right + downloadValue.font = NSFont.systemFont(ofSize: 9, weight: .light) + downloadValue.stringValue = "0 KB/s" + + uploadValue = NSTextField(frame: NSMakeRect(MODULE_MARGIN, self.frame.size.height - 10, self.frame.size.width - MODULE_MARGIN, 9)) + uploadValue.isEditable = false + uploadValue.isSelectable = false + uploadValue.isBezeled = false + uploadValue.wantsLayer = true + uploadValue.textColor = .labelColor + uploadValue.backgroundColor = .controlColor + uploadValue.canDrawSubviewsIntoLayer = true + uploadValue.alignment = .right + uploadValue.font = NSFont.systemFont(ofSize: 9, weight: .light) + uploadValue.stringValue = "0 KB/s" + + self.addSubview(downloadValue) + self.addSubview(uploadValue) + } +} diff --git a/Stats/libs/Extensions.swift b/Stats/libs/Extensions.swift index 3e05969e..e6cc8819 100755 --- a/Stats/libs/Extensions.swift +++ b/Stats/libs/Extensions.swift @@ -9,7 +9,7 @@ import Foundation import Cocoa -extension Float { +extension Double { func roundTo(decimalPlaces: Int) -> String { return NSString(format: "%.\(decimalPlaces)f" as NSString, self) as String } @@ -59,6 +59,10 @@ extension Float { return NSColor.systemRed } } + + func splitAtDecimal() -> [Int64] { + return "\(self)".split(separator: ".").map{Int64($0)!} + } } public enum Unit : Float { @@ -68,6 +72,76 @@ public enum Unit : Float { case gigabyte = 1073741824 } +public struct Units { + public let bytes: Int64 + + public init(bytes: Int64) { + self.bytes = bytes + } + + public var kilobytes: Double { + return Double(bytes) / 1_024 + } + public var megabytes: Double { + return kilobytes / 1_024 + } + public var gigabytes: Double { + return megabytes / 1_024 + } + + public func getReadableTuple() -> (Double, String) { + switch bytes { + case 0..<1_024: + return (0, "KB/s") + case 1_024..<(1_024 * 1_024): + return (Double(String(format: "%.2f", kilobytes))!, "KB/s") + case 1_024..<(1_024 * 1_024 * 1_024): + return (Double(String(format: "%.2f", megabytes))!, "MB/s") + case (1_024 * 1_024 * 1_024)...Int64.max: + return (Double(String(format: "%.2f", gigabytes))!, "GB/s") + default: + return (Double(String(format: "%.2f", kilobytes))!, "KB/s") + } + } + + public func getReadableUnit() -> String { + switch bytes { + case 0..<1_024: + return "0 KB/s" + case 1_024..<(1_024 * 1_024): + return String(format: "%.0f KB/s", kilobytes) + case 1_024..<(1_024 * 1_024 * 1_024): + return String(format: "%.2f MB/s", megabytes) + case (1_024 * 1_024 * 1_024)...Int64.max: + return String(format: "%.2f GB/s", gigabytes) + default: + return String(format: "%.0f KB/s", kilobytes) + } + } +} + +extension String { + func condenseWhitespace() -> String { + let components = self.components(separatedBy: .whitespacesAndNewlines) + return components.filter { !$0.isEmpty }.joined(separator: " ") + } +} + +extension NSBezierPath { + func addArrow(start: CGPoint, end: CGPoint, pointerLineLength: CGFloat, arrowAngle: CGFloat) { + self.move(to: start) + self.line(to: end) + + let startEndAngle = atan((end.y - start.y) / (end.x - start.x)) + ((end.x - start.x) < 0 ? CGFloat(Double.pi) : 0) + let arrowLine1 = CGPoint(x: end.x + pointerLineLength * cos(CGFloat(Double.pi) - startEndAngle + arrowAngle), y: end.y - pointerLineLength * sin(CGFloat(Double.pi) - startEndAngle + arrowAngle)) + let arrowLine2 = CGPoint(x: end.x + pointerLineLength * cos(CGFloat(Double.pi) - startEndAngle - arrowAngle), y: end.y - pointerLineLength * sin(CGFloat(Double.pi) - startEndAngle - arrowAngle)) + + self.line(to: arrowLine1) + self.move(to: end) + self.line(to: arrowLine2) + } +} + //extension NSView { // var backgroundColor: NSColor? { // get { diff --git a/Stats/libs/Module.swift b/Stats/libs/Module.swift index 1e610d2c..013378d1 100644 --- a/Stats/libs/Module.swift +++ b/Stats/libs/Module.swift @@ -24,7 +24,6 @@ protocol Module: class { extension Module { func initWidget() { - self.active << false switch self.widgetType { case Widgets.Mini: let widget = Mini(frame: NSMakeRect(0, 0, MODULE_WIDTH, MODULE_HEIGHT)) @@ -34,15 +33,26 @@ extension Module { case Widgets.Chart: self.view = Chart(frame: NSMakeRect(0, 0, MODULE_WIDTH + 7, MODULE_HEIGHT)) break - case Widgets.ChartWithValue: - self.view = ChartWithValue(frame: NSMakeRect(0, 0, MODULE_WIDTH + 7, MODULE_HEIGHT)) + case Widgets.Dots: + self.view = NetworkDotsView(frame: NSMakeRect(0, 0, MODULE_WIDTH, MODULE_HEIGHT)) + break + case Widgets.Arrows: + self.view = NetworkArrowsView(frame: NSMakeRect(0, 0, MODULE_WIDTH, MODULE_HEIGHT)) + break + case Widgets.Text: + self.view = NetworkTextView(frame: NSMakeRect(0, 0, MODULE_WIDTH, MODULE_HEIGHT)) + break + case Widgets.DotsWithText: + self.view = NetworkDotsTextView(frame: NSMakeRect(0, 0, MODULE_WIDTH, MODULE_HEIGHT)) + break + case Widgets.ArrowsWithText: + self.view = NetworkArrowsTextView(frame: NSMakeRect(0, 0, MODULE_WIDTH, MODULE_HEIGHT)) break default: let widget = Mini(frame: NSMakeRect(0, 0, MODULE_WIDTH, MODULE_HEIGHT)) widget.label = self.shortName self.view = widget } - self.active << true } func start() { @@ -79,7 +89,7 @@ extension Module { } protocol Reader { - var usage: Observable! { get } + var usage: Observable! { get } var available: Bool { get } var updateTimer: Timer! { get set } func start() @@ -88,7 +98,7 @@ protocol Reader { } protocol Widget { - func value(value: Float) + func value(value: Double) func redraw() } @@ -98,4 +108,10 @@ struct Widgets { static let Mini: WidgetType = 0.0 static let Chart: WidgetType = 1.0 static let ChartWithValue: WidgetType = 1.1 + + static let Dots: WidgetType = 2.0 + static let Arrows: WidgetType = 2.1 + static let Text: WidgetType = 2.2 + static let DotsWithText: WidgetType = 2.3 + static let ArrowsWithText: WidgetType = 2.4 } From f0d4cd7503b1321c5b61bafc729060bdcbc50164 Mon Sep 17 00:00:00 2001 From: Serhiy Mytrovtsiy Date: Tue, 25 Jun 2019 00:48:37 +0200 Subject: [PATCH 2/8] initialized network module; critical fixes in CPU and Memory modules --- Stats/Widgets/NetworkView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Stats/Widgets/NetworkView.swift b/Stats/Widgets/NetworkView.swift index ada4c2fe..efd327ea 100644 --- a/Stats/Widgets/NetworkView.swift +++ b/Stats/Widgets/NetworkView.swift @@ -2,7 +2,7 @@ // NetworkView.swift // Stats // -// Created by Samuel Grant on 24.06.2019. +// Created by Serhiy Mytrovtsiy on 24.06.2019. // Copyright © 2019 Serhiy Mytrovtsiy. All rights reserved. // From d9ab8551d343c096ccc47bcfa8b18405b304b78b Mon Sep 17 00:00:00 2001 From: Serhiy Mytrovtsiy Date: Tue, 25 Jun 2019 23:54:10 +0200 Subject: [PATCH 3/8] created view which check if new version is available --- Stats.xcodeproj/project.pbxproj | 23 +- Stats/AppDelegate.swift | 62 +++++ Stats/MenuBar.swift | 13 + Stats/Supporting Files/Stats.entitlements | 10 +- Stats/Supporting Files/Updates.storyboard | 287 ++++++++++++++++++++++ Stats/libs/macAppUpdater.swift | 136 ++++++++++ 6 files changed, 526 insertions(+), 5 deletions(-) create mode 100644 Stats/Supporting Files/Updates.storyboard create mode 100644 Stats/libs/macAppUpdater.swift diff --git a/Stats.xcodeproj/project.pbxproj b/Stats.xcodeproj/project.pbxproj index 32d4ae40..d11a9b24 100755 --- a/Stats.xcodeproj/project.pbxproj +++ b/Stats.xcodeproj/project.pbxproj @@ -12,6 +12,8 @@ 9A09C8A222B3D94D0018426F /* BatteryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A09C8A122B3D94D0018426F /* BatteryView.swift */; }; 9A1410F9229E721100D29793 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1410F8229E721100D29793 /* AppDelegate.swift */; }; 9A141100229E721200D29793 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9A1410FE229E721200D29793 /* Main.storyboard */; }; + 9A426DB822C2B5EE00C064C4 /* macAppUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A426DB722C2B5EE00C064C4 /* macAppUpdater.swift */; }; + 9A426DBE22C2BE0000C064C4 /* Updates.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9A426DBD22C2BE0000C064C4 /* Updates.storyboard */; }; 9A57A18522A1D26D0033E318 /* MenuBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A57A18422A1D26D0033E318 /* MenuBar.swift */; }; 9A57A19B22A1E1C50033E318 /* Module.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A57A19A22A1E1C50033E318 /* Module.swift */; }; 9A57A19D22A1E3270033E318 /* CPU.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A57A19C22A1E3270033E318 /* CPU.swift */; }; @@ -55,6 +57,8 @@ 9A1410FF229E721200D29793 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 9A141101229E721200D29793 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9A141102229E721200D29793 /* Stats.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Stats.entitlements; sourceTree = ""; }; + 9A426DB722C2B5EE00C064C4 /* macAppUpdater.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = macAppUpdater.swift; sourceTree = ""; }; + 9A426DBD22C2BE0000C064C4 /* Updates.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Updates.storyboard; sourceTree = ""; }; 9A57A18422A1D26D0033E318 /* MenuBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBar.swift; sourceTree = ""; }; 9A57A19A22A1E1C50033E318 /* Module.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Module.swift; sourceTree = ""; }; 9A57A19C22A1E3270033E318 /* CPU.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CPU.swift; sourceTree = ""; }; @@ -142,8 +146,9 @@ isa = PBXGroup; children = ( 9A6CFC0022A1C9F5001E782D /* Assets.xcassets */, - 9AFFCB3A22B3FD0500B0E6D8 /* About.storyboard */, 9A1410FE229E721200D29793 /* Main.storyboard */, + 9AFFCB3A22B3FD0500B0E6D8 /* About.storyboard */, + 9A426DBD22C2BE0000C064C4 /* Updates.storyboard */, 9A141101229E721200D29793 /* Info.plist */, 9A141102229E721200D29793 /* Stats.entitlements */, ); @@ -167,6 +172,7 @@ 9A5B1CBE229E78F0008B9D3C /* Observable.swift */, 9A57A19A22A1E1C50033E318 /* Module.swift */, 9A5B1CC4229E7B40008B9D3C /* Extensions.swift */, + 9A426DB722C2B5EE00C064C4 /* macAppUpdater.swift */, ); path = libs; sourceTree = ""; @@ -279,6 +285,11 @@ TargetAttributes = { 9A1410F4229E721100D29793 = { CreatedOnToolsVersion = 10.2.1; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; }; 9AFA401D22AE49A100FE90BC = { CreatedOnToolsVersion = 10.2.1; @@ -312,6 +323,7 @@ 9A6CFC0122A1C9F5001E782D /* Assets.xcassets in Resources */, 9AFFCB3B22B3FD0500B0E6D8 /* About.storyboard in Resources */, 9A141100229E721200D29793 /* Main.storyboard in Resources */, + 9A426DBE22C2BE0000C064C4 /* Updates.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -332,6 +344,7 @@ buildActionMask = 2147483647; files = ( 9A09C8A222B3D94D0018426F /* BatteryView.swift in Sources */, + 9A426DB822C2B5EE00C064C4 /* macAppUpdater.swift in Sources */, 9A7B8F6F22A2C57000DEB352 /* DiskReader.swift in Sources */, 9A7B8F6922A2C3A100DEB352 /* Memory.swift in Sources */, 9A7B8F5E22A2A57600DEB352 /* CPUReader.swift in Sources */, @@ -505,6 +518,10 @@ COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = RP2S87B72W; ENABLE_HARDENED_RUNTIME = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + ); INFOPLIST_FILE = "$(SRCROOT)/Stats/Supporting Files/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -528,6 +545,10 @@ COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = RP2S87B72W; ENABLE_HARDENED_RUNTIME = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + ); INFOPLIST_FILE = "$(SRCROOT)/Stats/Supporting Files/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/Stats/AppDelegate.swift b/Stats/AppDelegate.swift index f89bc787..b4827a9a 100755 --- a/Stats/AppDelegate.swift +++ b/Stats/AppDelegate.swift @@ -81,3 +81,65 @@ class AboutVC: NSViewController { } } } + +class UpdatesVC: NSViewController { + @IBOutlet weak var mainView: NSStackView! + @IBOutlet weak var spinnerView: NSView! + @IBOutlet weak var noInternetView: NSView! + @IBOutlet weak var mainTextLabel: NSTextFieldCell! + @IBOutlet weak var currentVersionLabel: NSTextField! + @IBOutlet weak var latestVersionLabel: NSTextField! + @IBOutlet weak var downloadButton: NSButton! + @IBOutlet weak var spinner: NSProgressIndicator! + + let updater = macAppUpdater(user: "exelban", repo: "stats") + var url: String? + + override func viewDidLoad() { + super.viewDidLoad() + self.view.wantsLayer = true + + self.spinner.startAnimation(self) + + updater.check() { result, error in + if error != nil && error as! String == "No internet connection" { + DispatchQueue.main.async(execute: { + self.spinnerView.isHidden = true + self.noInternetView.isHidden = false + }) + return + } + + guard error == nil, let version: version = result else { + print("Error: \(error ?? "check error")") + return + } + + DispatchQueue.main.async(execute: { + self.spinner.stopAnimation(self) + self.spinnerView.isHidden = true + self.mainView.isHidden = false + self.currentVersionLabel.stringValue = version.current + self.latestVersionLabel.stringValue = version.latest + self.url = version.url + + if !version.newest { + self.mainTextLabel.stringValue = "No new version available" + self.downloadButton.isEnabled = false + } + }) + } + } + + @IBAction func download(_ sender: Any) { + guard let urlString = self.url, let url = URL(string: urlString) else { + return + } + NSWorkspace.shared.open(url) + self.view.window?.close() + } + + @IBAction func exit(_ sender: Any) { + self.view.window?.close() + } +} diff --git a/Stats/MenuBar.swift b/Stats/MenuBar.swift index 2dc09aad..db305794 100644 --- a/Stats/MenuBar.swift +++ b/Stats/MenuBar.swift @@ -73,14 +73,27 @@ class MenuBar { menu.addItem(preferences) menu.addItem(NSMenuItem.separator()) + + let updateMenu = NSMenuItem(title: "Check for updates", action: #selector(checkUpdate), keyEquivalent: "") + updateMenu.target = self + let aboutMenu = NSMenuItem(title: "About Stats", action: #selector(openAbout), keyEquivalent: "") aboutMenu.target = self + + menu.addItem(updateMenu) menu.addItem(aboutMenu) menu.addItem(NSMenuItem(title: "Quit Stats", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "")) return menu } + @objc func checkUpdate(_ sender : NSMenuItem) { + let updatesVC: NSWindowController? = NSStoryboard(name: "Updates", bundle: nil).instantiateController(withIdentifier: "UpdatesVC") as? NSWindowController + updatesVC?.window?.center() + updatesVC?.window?.level = .floating + updatesVC!.showWindow(self) + } + @objc func openAbout(_ sender : NSMenuItem) { let aboutVC: NSWindowController? = NSStoryboard(name: "About", bundle: nil).instantiateController(withIdentifier: "AboutVC") as? NSWindowController aboutVC?.window?.center() diff --git a/Stats/Supporting Files/Stats.entitlements b/Stats/Supporting Files/Stats.entitlements index f2ef3ae0..625af03d 100755 --- a/Stats/Supporting Files/Stats.entitlements +++ b/Stats/Supporting Files/Stats.entitlements @@ -2,9 +2,11 @@ - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + com.apple.security.network.client + diff --git a/Stats/Supporting Files/Updates.storyboard b/Stats/Supporting Files/Updates.storyboard new file mode 100644 index 00000000..7269c41f --- /dev/null +++ b/Stats/Supporting Files/Updates.storyboard @@ -0,0 +1,287 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Stats/libs/macAppUpdater.swift b/Stats/libs/macAppUpdater.swift new file mode 100644 index 00000000..f86432d7 --- /dev/null +++ b/Stats/libs/macAppUpdater.swift @@ -0,0 +1,136 @@ +// +// macAppUpdater.swift +// Stats +// +// Created by Serhiy Mytrovtsiy on 25.06.2019. +// Copyright © 2019 Serhiy Mytrovtsiy. All rights reserved. +// + +import Foundation +import SystemConfiguration + +extension String: Error {} + +struct version { + let current: String + let latest: String + let newest: Bool + let url: String +} + +public class macAppUpdater { + let user: String + let repo: String + + let appName: String = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as! String + let currentVersion: String = "v\(Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String)" + + var url: String { + return "https://api.github.com/repos/\(user)/\(repo)/releases/latest" + } + + init(user: String, repo: String) { + self.user = user + self.repo = repo + } + + func fetchLastVersion(completionHandler: @escaping (_ result: [String]?, _ error: Error?) -> Void) { + let task = URLSession.shared.dataTask(with: URL(string: self.url)!) { data, response, error in + guard let data = data, error == nil else { return } + + do { + let jsonResponse = try JSONSerialization.jsonObject(with: data, options: []) + guard let jsonArray = jsonResponse as? [String: Any] else { + completionHandler(nil, "parse json") + return + } + let lastVersion = jsonArray["tag_name"] as? String + + guard let assets = jsonArray["assets"] as? [[String: Any]] else { + completionHandler(nil, "parse assets") + return + } + if let asset = assets.first(where: {$0["name"] as! String == "\(self.appName).dmg"}) { + let downloadURL = asset["browser_download_url"] as? String + completionHandler([lastVersion!, downloadURL!], nil) + } + } catch let parsingError { + completionHandler(nil, parsingError) + } + } + task.resume() + } + + func checkIfNewer(current: String, latest: String) -> Bool { + guard let currentNumber: Int64 = Int64(current.replacingOccurrences(of: "[v.]", with: "", options: [.regularExpression])) else { + print("Error: wrong version tag \(current)") + return false + } + guard let latestNumber: Int64 = Int64(latest.replacingOccurrences(of: "[v.]", with: "", options: [.regularExpression])) else { + print("Error: wrong version tag \(latest)") + return false + } + return latestNumber>currentNumber + } + + func check(completionHandler: @escaping (_ result: version?, _ error: Error?) -> Void) { + if !Reachability.isConnectedToNetwork() { + completionHandler(nil, "No internet connection") + return + } + + fetchLastVersion() { result, error in + guard error == nil else { + completionHandler(nil, error) + return + } + + guard let results = result, results.count > 1 else { + completionHandler(nil, "wrong results") + return + } + + let downloadURL: String = result![1] + let lastVersion: String = result![0] + let newVersion: Bool = self.checkIfNewer(current: self.currentVersion, latest: lastVersion) + + completionHandler(version(current: self.currentVersion, latest: lastVersion, newest: newVersion, url: downloadURL), nil) + } + } +} + + +// https://stackoverflow.com/questions/30743408/check-for-internet-connection-with-swift +public class Reachability { + class func isConnectedToNetwork() -> Bool { + var zeroAddress = sockaddr_in(sin_len: 0, sin_family: 0, sin_port: 0, sin_addr: in_addr(s_addr: 0), sin_zero: (0, 0, 0, 0, 0, 0, 0, 0)) + zeroAddress.sin_len = UInt8(MemoryLayout.size(ofValue: zeroAddress)) + zeroAddress.sin_family = sa_family_t(AF_INET) + + let defaultRouteReachability = withUnsafePointer(to: &zeroAddress) { + $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {zeroSockAddress in + SCNetworkReachabilityCreateWithAddress(nil, zeroSockAddress) + } + } + + var flags: SCNetworkReachabilityFlags = SCNetworkReachabilityFlags(rawValue: 0) + if SCNetworkReachabilityGetFlags(defaultRouteReachability!, &flags) == false { + return false + } + + /* Only Working for WIFI + let isReachable = flags == .reachable + let needsConnection = flags == .connectionRequired + + return isReachable && !needsConnection + */ + + // Working for Cellular and WIFI + let isReachable = (flags.rawValue & UInt32(kSCNetworkFlagsReachable)) != 0 + let needsConnection = (flags.rawValue & UInt32(kSCNetworkFlagsConnectionRequired)) != 0 + let ret = (isReachable && !needsConnection) + + return ret + + } +} From 43570fa8f60d9fa08b18fd088057208df476c610 Mon Sep 17 00:00:00 2001 From: Serhiy Mytrovtsiy Date: Tue, 25 Jun 2019 23:58:54 +0200 Subject: [PATCH 4/8] v1.2.0 --- Stats/Supporting Files/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Stats/Supporting Files/Info.plist b/Stats/Supporting Files/Info.plist index acc461d3..ec5d4c34 100755 --- a/Stats/Supporting Files/Info.plist +++ b/Stats/Supporting Files/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.1.0 + 1.2.0 CFBundleVersion 1 LSApplicationCategoryType From 2326e5573cd520f8b49936f6859360ab251a58b0 Mon Sep 17 00:00:00 2001 From: Serhiy Mytrovtsiy Date: Wed, 26 Jun 2019 00:24:09 +0200 Subject: [PATCH 5/8] white background in updates storyboard --- Stats/AppDelegate.swift | 7 +++++++ Stats/Supporting Files/Info.plist | 2 +- Stats/Supporting Files/Updates.storyboard | 4 ++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Stats/AppDelegate.swift b/Stats/AppDelegate.swift index b4e7d474..85c61af1 100755 --- a/Stats/AppDelegate.swift +++ b/Stats/AppDelegate.swift @@ -131,6 +131,13 @@ class UpdatesVC: NSViewController { } } + override func awakeFromNib() { + if self.view.layer != nil { + self.view.window?.backgroundColor = .white + self.view.layer?.backgroundColor = .white + } + } + @IBAction func download(_ sender: Any) { guard let urlString = self.url, let url = URL(string: urlString) else { return diff --git a/Stats/Supporting Files/Info.plist b/Stats/Supporting Files/Info.plist index ec5d4c34..4b3490c4 100755 --- a/Stats/Supporting Files/Info.plist +++ b/Stats/Supporting Files/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.2.0 CFBundleVersion - 1 + 2 LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion diff --git a/Stats/Supporting Files/Updates.storyboard b/Stats/Supporting Files/Updates.storyboard index 7269c41f..15a7697b 100644 --- a/Stats/Supporting Files/Updates.storyboard +++ b/Stats/Supporting Files/Updates.storyboard @@ -10,8 +10,8 @@ - - + + From cd5f64a12ce524edf2b0d0ef206ed11add488764 Mon Sep 17 00:00:00 2001 From: Serhiy Mytrovtsiy Date: Wed, 26 Jun 2019 14:10:52 +0200 Subject: [PATCH 6/8] README updated; fix in network module --- README.md | 29 +++++++++++++++++------------ Stats/Widgets/NetworkView.swift | 12 ++++++------ 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 47544dfc..4987368f 100644 --- a/README.md +++ b/README.md @@ -3,16 +3,15 @@ Simple macOS system monitor in your menu bar [](https://github.com/exelban/stats/releases) -## Why +## Features Stats is a application which allows you to monitor your macOS system. -Also its: - - free - - easy to use - - no advertisement - - no tracking - - few types of widgets - - black theme compatible + - CPU Usage + - Memory Usage + - Disk utilization + - Battery level + - Network usage + - Black theme compatible ## Installation You can download latest version [here](https://github.com/exelban/stats/releases). @@ -23,8 +22,9 @@ You can download latest version [here](https://github.com/exelban/stats/releases | --- | --- | --- | | **CPU** | Percentage / Chart / Chart with value | Shows CPU usage | | **Memory** | Percentage / Chart / Chart with value | Shows RAM usage | -| **Disk** | Percentage | Shows disk filling | +| **Disk** | Percentage | Shows disk utilization | | **Battery** | Graphic / Percentage | Shows battery level and charging status | +| **Newtork** | Dots / Upload/Download traffic | Shows network activity | ## Compatibility | macOS | Compatible | @@ -33,7 +33,7 @@ You can download latest version [here](https://github.com/exelban/stats/releases | 10.14.1 *(Mojave)* | **true** | ## Todo - - [ ] Battery percentage + - [X] Battery percentage - [ ] Create new logo - [ ] Window with preferences - [ ] Save last modules values @@ -41,7 +41,7 @@ You can download latest version [here](https://github.com/exelban/stats/releases - [ ] temperature module - [X] battery module - [X] move to module system (CPU, RAM, DISK) - - [ ] network module + - [X] network module - [X] save settings - [ ] OTA updates - [X] charts @@ -49,6 +49,11 @@ You can download latest version [here](https://github.com/exelban/stats/releases ## What's new +### v1.2.0 + - added network module + - added Check for updates window + - fixed few bugs + ### v1.1.0 - added battery module - added chart widget for CPU and Memory @@ -58,4 +63,4 @@ You can download latest version [here](https://github.com/exelban/stats/releases - first release ## License -[GNU General Public License](https://github.com/exelban/stats/blob/master/LICENSE) +[MIT License](https://github.com/exelban/stats/blob/master/LICENSE) diff --git a/Stats/Widgets/NetworkView.swift b/Stats/Widgets/NetworkView.swift index efd327ea..53f8b4c9 100644 --- a/Stats/Widgets/NetworkView.swift +++ b/Stats/Widgets/NetworkView.swift @@ -40,7 +40,7 @@ class NetworkDotsView: NSView, Widget { var uploadCircle = NSBezierPath() uploadCircle = NSBezierPath(ovalIn: CGRect(x: MODULE_MARGIN, y: height + (MODULE_MARGIN * 2) + 1, width: height, height: height)) - if self.upload != 0 { + if self.upload >= 1_024 { NSColor.red.setFill() } else { NSColor.labelColor.setFill() @@ -49,7 +49,7 @@ class NetworkDotsView: NSView, Widget { var downloadCircle = NSBezierPath() downloadCircle = NSBezierPath(ovalIn: CGRect(x: MODULE_MARGIN, y: MODULE_MARGIN, width: height, height: height)) - if self.download != 0 { + if self.download >= 1_024 { NSColor(red: (26/255.0), green: (126/255.0), blue: (252/255.0), alpha: 0.8).setFill() } else { NSColor.labelColor.setFill() @@ -170,7 +170,7 @@ class NetworkArrowsView: NSView, Widget { downloadArrow.addArrow(start: downloadStart, end: downloadEnd, pointerLineLength: pointerLineLength, arrowAngle: arrowAngle) - if self.download != 0 { + if self.download >= 1_024 { NSColor(red: (26/255.0), green: (126/255.0), blue: (252/255.0), alpha: 0.8).set() } else { NSColor.labelColor.set() @@ -246,7 +246,7 @@ class NetworkDotsTextView: NSView, Widget { var uploadCircle = NSBezierPath() uploadCircle = NSBezierPath(ovalIn: CGRect(x: MODULE_MARGIN, y: height + (MODULE_MARGIN * 2) + 1, width: height, height: height)) - if self.upload != 0 { + if self.upload >= 1_024 { NSColor.red.setFill() } else { NSColor.labelColor.setFill() @@ -351,7 +351,7 @@ class NetworkArrowsTextView: NSView, Widget { downloadArrow.addArrow(start: downloadStart, end: downloadEnd, pointerLineLength: pointerLineLength, arrowAngle: arrowAngle) - if self.download != 0 { + if self.download >= 1_024 { NSColor(red: (26/255.0), green: (126/255.0), blue: (252/255.0), alpha: 0.8).set() } else { NSColor.labelColor.set() @@ -366,7 +366,7 @@ class NetworkArrowsTextView: NSView, Widget { uploadArrow.addArrow(start: uploadStart, end: uploadEnd, pointerLineLength: pointerLineLength, arrowAngle: arrowAngle) - if self.upload != 0 { + if self.upload >= 1_024 { NSColor.red.set() } else { NSColor.labelColor.set() From cb0f03cfd9fe56bcba4dc4bda85834665094142f Mon Sep 17 00:00:00 2001 From: Serhiy Mytrovtsiy Date: Wed, 26 Jun 2019 15:21:48 +0200 Subject: [PATCH 7/8] network small fixes --- Stats/Widgets/NetworkView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Stats/Widgets/NetworkView.swift b/Stats/Widgets/NetworkView.swift index 53f8b4c9..90f9d984 100644 --- a/Stats/Widgets/NetworkView.swift +++ b/Stats/Widgets/NetworkView.swift @@ -185,7 +185,7 @@ class NetworkArrowsView: NSView, Widget { uploadArrow.addArrow(start: uploadStart, end: uploadEnd, pointerLineLength: pointerLineLength, arrowAngle: arrowAngle) - if self.upload != 0 { + if self.upload >= 1_024 { NSColor.red.set() } else { NSColor.labelColor.set() @@ -255,7 +255,7 @@ class NetworkDotsTextView: NSView, Widget { var downloadCircle = NSBezierPath() downloadCircle = NSBezierPath(ovalIn: CGRect(x: MODULE_MARGIN, y: MODULE_MARGIN, width: height, height: height)) - if self.download != 0 { + if self.download >= 1_024 { NSColor(red: (26/255.0), green: (126/255.0), blue: (252/255.0), alpha: 0.8).setFill() } else { NSColor.labelColor.setFill() From 9119b20d279f7366b2cde208ff59a083b7308d10 Mon Sep 17 00:00:00 2001 From: Serhiy Mytrovtsiy Date: Wed, 26 Jun 2019 17:52:16 +0200 Subject: [PATCH 8/8] build number changed --- Stats/Supporting Files/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Stats/Supporting Files/Info.plist b/Stats/Supporting Files/Info.plist index 4b3490c4..212372ee 100755 --- a/Stats/Supporting Files/Info.plist +++ b/Stats/Supporting Files/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.2.0 CFBundleVersion - 2 + 3 LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion