From c0bb81a49075dfc46db7c1f0926c0cf75d000133 Mon Sep 17 00:00:00 2001 From: Serhiy Mytrovtsiy Date: Tue, 25 Jun 2019 00:42:52 +0200 Subject: [PATCH] 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 }