mirror of
https://github.com/morgan9e/macos-stats
synced 2026-04-14 00:04:15 +09:00
- disk IO stats (mvp)
This commit is contained in:
@@ -26,6 +26,8 @@ public class SpeedWidget: Widget {
|
||||
private var state: Bool = false
|
||||
private var valueState: Bool = true
|
||||
|
||||
private var symbols: [String] = ["U", "D"]
|
||||
|
||||
private var uploadField: NSTextField? = nil
|
||||
private var downloadField: NSTextField? = nil
|
||||
|
||||
@@ -38,6 +40,14 @@ public class SpeedWidget: Widget {
|
||||
public init(preview: Bool, title: String, config: NSDictionary?, store: UnsafePointer<Store>?) {
|
||||
let widgetTitle: String = title
|
||||
self.store = store
|
||||
if config != nil {
|
||||
if let symbols = config!["Symbols"] as? [String] {
|
||||
self.symbols = symbols
|
||||
}
|
||||
if let iconName = config!["Icon"] as? String, let icon = speed_icon_t(rawValue: iconName) {
|
||||
self.icon = icon
|
||||
}
|
||||
}
|
||||
super.init(frame: CGRect(x: 0, y: Constants.Widget.margin, width: width, height: Constants.Widget.height - (2*Constants.Widget.margin)))
|
||||
self.title = widgetTitle
|
||||
self.type = .speed
|
||||
@@ -172,23 +182,27 @@ public class SpeedWidget: Widget {
|
||||
private func drawChars(_ dirtyRect: NSRect) {
|
||||
let rowHeight: CGFloat = self.frame.height / 2
|
||||
|
||||
let downloadAttributes = [
|
||||
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 9, weight: .regular),
|
||||
NSAttributedString.Key.foregroundColor: downloadValue >= 1_024 ? NSColor(red: (26/255.0), green: (126/255.0), blue: (252/255.0), alpha: 0.8) : NSColor.textColor,
|
||||
NSAttributedString.Key.paragraphStyle: NSMutableParagraphStyle()
|
||||
]
|
||||
var rect = CGRect(x: Constants.Widget.margin, y: 1, width: 8, height: rowHeight)
|
||||
var str = NSAttributedString.init(string: "D", attributes: downloadAttributes)
|
||||
str.draw(with: rect)
|
||||
if self.symbols.count > 1 {
|
||||
let downloadAttributes = [
|
||||
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 9, weight: .regular),
|
||||
NSAttributedString.Key.foregroundColor: downloadValue >= 1_024 ? NSColor(red: (26/255.0), green: (126/255.0), blue: (252/255.0), alpha: 0.8) : NSColor.textColor,
|
||||
NSAttributedString.Key.paragraphStyle: NSMutableParagraphStyle()
|
||||
]
|
||||
let rect = CGRect(x: Constants.Widget.margin, y: 1, width: 8, height: rowHeight)
|
||||
let str = NSAttributedString.init(string: self.symbols[1], attributes: downloadAttributes)
|
||||
str.draw(with: rect)
|
||||
}
|
||||
|
||||
let uploadAttributes = [
|
||||
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 9, weight: .regular),
|
||||
NSAttributedString.Key.foregroundColor: uploadValue >= 1_024 ? NSColor.red : NSColor.textColor,
|
||||
NSAttributedString.Key.paragraphStyle: NSMutableParagraphStyle()
|
||||
]
|
||||
rect = CGRect(x: Constants.Widget.margin, y: rect.height+1, width: 8, height: rowHeight)
|
||||
str = NSAttributedString.init(string: "U", attributes: uploadAttributes)
|
||||
str.draw(with: rect)
|
||||
if self.symbols.count > 0 {
|
||||
let uploadAttributes = [
|
||||
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 9, weight: .regular),
|
||||
NSAttributedString.Key.foregroundColor: uploadValue >= 1_024 ? NSColor.red : NSColor.textColor,
|
||||
NSAttributedString.Key.paragraphStyle: NSMutableParagraphStyle()
|
||||
]
|
||||
let rect = CGRect(x: Constants.Widget.margin, y: rowHeight+1, width: 8, height: rowHeight)
|
||||
let str = NSAttributedString.init(string: self.symbols[0], attributes: uploadAttributes)
|
||||
str.draw(with: rect)
|
||||
}
|
||||
}
|
||||
|
||||
public override func settings(superview: NSView) {
|
||||
@@ -255,11 +269,11 @@ public class SpeedWidget: Widget {
|
||||
var updated: Bool = false
|
||||
|
||||
if self.downloadValue != download {
|
||||
self.downloadValue = download
|
||||
self.downloadValue = abs(download)
|
||||
updated = true
|
||||
}
|
||||
if self.uploadValue != upload {
|
||||
self.uploadValue = upload
|
||||
self.uploadValue = abs(upload)
|
||||
updated = true
|
||||
}
|
||||
|
||||
|
||||
@@ -234,11 +234,11 @@ public class ProcessReader: Reader<[TopProcess]> {
|
||||
output.enumerateLines { (line, _) -> () in
|
||||
if line.matches("^\\d* +.+ \\d*.?\\d*$") {
|
||||
var str = line.trimmingCharacters(in: .whitespaces)
|
||||
|
||||
|
||||
let pidString = str.findAndCrop(pattern: "^\\d+")
|
||||
let usageString = str.findAndCrop(pattern: " +[0-9]+.*[0-9]*$")
|
||||
let command = str.trimmingCharacters(in: .whitespaces)
|
||||
|
||||
|
||||
let pid = Int(pidString) ?? 0
|
||||
guard let usage = Double(usageString.filter("01234567890.".contains)) else {
|
||||
return
|
||||
|
||||
17
Modules/Disk/Bridge.h
Normal file
17
Modules/Disk/Bridge.h
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// Bridge.h
|
||||
// Disk
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 07/08/2020.
|
||||
// Using Swift 5.0.
|
||||
// Running on macOS 10.15.
|
||||
//
|
||||
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Bridge_h
|
||||
#define Bridge_h
|
||||
|
||||
#include <libproc.h>
|
||||
|
||||
#endif /* Bridge_h */
|
||||
@@ -68,6 +68,20 @@
|
||||
<key>Order</key>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<key>speed</key>
|
||||
<dict>
|
||||
<key>Default</key>
|
||||
<false/>
|
||||
<key>Order</key>
|
||||
<integer>3</integer>
|
||||
<key>Icon</key>
|
||||
<string>Character</string>
|
||||
<key>Symbols</key>
|
||||
<array>
|
||||
<string>W</string>
|
||||
<string>R</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -63,9 +63,15 @@ struct DiskList: value_t {
|
||||
}
|
||||
}
|
||||
|
||||
public struct IO {
|
||||
var read: Int = 0
|
||||
var write: Int = 0
|
||||
}
|
||||
|
||||
public class Disk: Module {
|
||||
private let popupView: Popup = Popup()
|
||||
private var capacityReader: CapacityReader? = nil
|
||||
private var ioReader: IOReader? = nil
|
||||
private var settingsView: Settings
|
||||
private var selectedDisk: String = ""
|
||||
|
||||
@@ -83,12 +89,17 @@ public class Disk: Module {
|
||||
self.capacityReader?.store = store
|
||||
self.selectedDisk = store.pointee.string(key: "\(self.config.name)_disk", defaultValue: self.selectedDisk)
|
||||
|
||||
self.ioReader = IOReader()
|
||||
|
||||
self.capacityReader?.readyCallback = { [unowned self] in
|
||||
self.readyHandler()
|
||||
}
|
||||
self.capacityReader?.callbackHandler = { [unowned self] value in
|
||||
self.capacityCallback(value: value)
|
||||
}
|
||||
self.ioReader?.callbackHandler = { [unowned self] value in
|
||||
self.ioCallback(value: value)
|
||||
}
|
||||
|
||||
self.settingsView.selectedDiskHandler = { [unowned self] value in
|
||||
self.selectedDisk = value
|
||||
@@ -104,6 +115,9 @@ public class Disk: Module {
|
||||
if let reader = self.capacityReader {
|
||||
self.addReader(reader)
|
||||
}
|
||||
if let reader = self.ioReader {
|
||||
self.addReader(reader)
|
||||
}
|
||||
}
|
||||
|
||||
private func capacityCallback(value: DiskList?) {
|
||||
@@ -137,4 +151,14 @@ public class Disk: Module {
|
||||
widget.setValue((free, usedSpace))
|
||||
}
|
||||
}
|
||||
|
||||
private func ioCallback(value: IO?) {
|
||||
if value == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if let widget = self.widget as? SpeedWidget {
|
||||
widget.setValue(upload: Int64(value!.write), download: Int64(value!.read))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
import Cocoa
|
||||
import ModuleKit
|
||||
import StatsKit
|
||||
import IOKit
|
||||
import Darwin
|
||||
|
||||
internal class CapacityReader: Reader<DiskList> {
|
||||
private var disks: DiskList = DiskList()
|
||||
@@ -124,3 +126,46 @@ internal class CapacityReader: Reader<DiskList> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://gist.github.com/kainjow/0e7650cc797a52261e0f4ba851477c2f
|
||||
internal class IOReader: Reader<IO> {
|
||||
public var stats: IO = IO()
|
||||
|
||||
public override func read() {
|
||||
let initialNumPids = proc_listallpids(nil, 0)
|
||||
let buffer = UnsafeMutablePointer<pid_t>.allocate(capacity: Int(initialNumPids))
|
||||
defer {
|
||||
buffer.deallocate()
|
||||
}
|
||||
|
||||
let bufferLength = initialNumPids * Int32(MemoryLayout<pid_t>.size)
|
||||
let numPids = proc_listallpids(buffer, bufferLength)
|
||||
|
||||
var read: Int = 0
|
||||
var write: Int = 0
|
||||
for i in 0..<numPids {
|
||||
let pid = buffer[Int(i)]
|
||||
var usage = rusage_info_current()
|
||||
let result = withUnsafeMutablePointer(to: &usage) {
|
||||
$0.withMemoryRebound(to: rusage_info_t?.self, capacity: 1) {
|
||||
proc_pid_rusage(pid, RUSAGE_INFO_CURRENT, $0)
|
||||
}
|
||||
}
|
||||
|
||||
if result == kIOReturnSuccess {
|
||||
read += Int(usage.ri_diskio_bytesread)
|
||||
write += Int(usage.ri_diskio_byteswritten)
|
||||
}
|
||||
}
|
||||
|
||||
if self.stats.read != 0 && self.stats.write != 0 {
|
||||
self.stats.read = read - self.stats.read
|
||||
self.stats.write = write - self.stats.write
|
||||
}
|
||||
|
||||
self.callback(self.stats)
|
||||
|
||||
self.stats.read = read
|
||||
self.stats.write = write
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,16 +53,19 @@ internal class Settings: NSView, Settings_v {
|
||||
self.subviews.forEach{ $0.removeFromSuperview() }
|
||||
|
||||
let rowHeight: CGFloat = 30
|
||||
let num: CGFloat = widget != .speed ? 3 : 1
|
||||
|
||||
self.addSubview(SelectTitleRow(
|
||||
frame: NSRect(x: Constants.Settings.margin, y: Constants.Settings.margin + (rowHeight + Constants.Settings.margin) * 2, width: self.frame.width - (Constants.Settings.margin*2), height: rowHeight),
|
||||
title: "Update interval",
|
||||
action: #selector(changeUpdateInterval),
|
||||
items: self.listOfUpdateIntervals.map{ "\($0) sec" },
|
||||
selected: "\(self.updateIntervalValue) sec"
|
||||
))
|
||||
|
||||
self.addDiskSelector()
|
||||
if widget != .speed {
|
||||
self.addSubview(SelectTitleRow(
|
||||
frame: NSRect(x: Constants.Settings.margin, y: Constants.Settings.margin + (rowHeight + Constants.Settings.margin) * 2, width: self.frame.width - (Constants.Settings.margin*2), height: rowHeight),
|
||||
title: "Update interval",
|
||||
action: #selector(changeUpdateInterval),
|
||||
items: self.listOfUpdateIntervals.map{ "\($0) sec" },
|
||||
selected: "\(self.updateIntervalValue) sec"
|
||||
))
|
||||
|
||||
self.addDiskSelector()
|
||||
}
|
||||
|
||||
self.addSubview(ToggleTitleRow(
|
||||
frame: NSRect(x: Constants.Settings.margin, y: Constants.Settings.margin + (rowHeight + Constants.Settings.margin) * 0, width: self.frame.width - (Constants.Settings.margin*2), height: rowHeight),
|
||||
@@ -71,7 +74,7 @@ internal class Settings: NSView, Settings_v {
|
||||
state: self.removableState
|
||||
))
|
||||
|
||||
self.setFrameSize(NSSize(width: self.frame.width, height: rowHeight*3 + (Constants.Settings.margin*4)))
|
||||
self.setFrameSize(NSSize(width: self.frame.width, height: rowHeight*num + (Constants.Settings.margin*(num+1))))
|
||||
}
|
||||
|
||||
private func addDiskSelector() {
|
||||
|
||||
@@ -14,6 +14,11 @@
|
||||
<true/>
|
||||
<key>Order</key>
|
||||
<integer>0</integer>
|
||||
<key>Symbols</key>
|
||||
<array>
|
||||
<string>U</string>
|
||||
<string>D</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
9AA64260244B274200416A33 /* popup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AA6425F244B274200416A33 /* popup.swift */; };
|
||||
9AA64262244B57C800416A33 /* settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AA64261244B57C800416A33 /* settings.swift */; };
|
||||
9AA64264244B94F300416A33 /* LineChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AA64263244B94F300416A33 /* LineChart.swift */; };
|
||||
9AA95A6424DD7C6300801B69 /* Bridge.h in Headers */ = {isa = PBXBuildFile; fileRef = 9AA95A6324DD7C6300801B69 /* Bridge.h */; };
|
||||
9AABEAE4243FB13500668CB0 /* ModuleKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9AABEADD243FB13500668CB0 /* ModuleKit.framework */; };
|
||||
9AABEAE5243FB13500668CB0 /* ModuleKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9AABEADD243FB13500668CB0 /* ModuleKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
9AABEAEA243FB15E00668CB0 /* module.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AABEAE9243FB15E00668CB0 /* module.swift */; };
|
||||
@@ -451,6 +452,7 @@
|
||||
9AA6425F244B274200416A33 /* popup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = popup.swift; sourceTree = "<group>"; };
|
||||
9AA64261244B57C800416A33 /* settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = settings.swift; sourceTree = "<group>"; };
|
||||
9AA64263244B94F300416A33 /* LineChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineChart.swift; sourceTree = "<group>"; };
|
||||
9AA95A6324DD7C6300801B69 /* Bridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Bridge.h; sourceTree = "<group>"; };
|
||||
9AABEADD243FB13500668CB0 /* ModuleKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ModuleKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
9AABEAE0243FB13500668CB0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
9AABEAE9243FB15E00668CB0 /* module.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = module.swift; sourceTree = "<group>"; };
|
||||
@@ -808,6 +810,7 @@
|
||||
9AB7FD7B246B48DB00387FDA /* settings.swift */,
|
||||
9AF9EE0524648751005D2270 /* Info.plist */,
|
||||
9AF9EE12246492E8005D2270 /* config.plist */,
|
||||
9AA95A6324DD7C6300801B69 /* Bridge.h */,
|
||||
);
|
||||
path = Disk;
|
||||
sourceTree = "<group>";
|
||||
@@ -869,6 +872,7 @@
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9AA95A6424DD7C6300801B69 /* Bridge.h in Headers */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -2214,6 +2218,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = eu.exelban.Stats.Disk;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = Modules/Disk/Bridge.h;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
@@ -2246,6 +2251,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = eu.exelban.Stats.Disk;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = Modules/Disk/Bridge.h;
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
|
||||
@@ -103,21 +103,21 @@ public class SMCService {
|
||||
var result: kern_return_t
|
||||
var iterator: io_iterator_t = 0
|
||||
let device: io_object_t
|
||||
|
||||
|
||||
let matchingDictionary: CFMutableDictionary = IOServiceMatching("AppleSMC")
|
||||
result = IOServiceGetMatchingServices(kIOMasterPortDefault, matchingDictionary, &iterator)
|
||||
if (result != kIOReturnSuccess) {
|
||||
print("Error IOServiceGetMatchingServices(): " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
device = IOIteratorNext(iterator)
|
||||
IOObjectRelease(iterator)
|
||||
if (device == 0) {
|
||||
print("Error IOIteratorNext(): " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
result = IOServiceOpen(device, mach_task_self_, 0, &conn)
|
||||
IOObjectRelease(device)
|
||||
if (result != kIOReturnSuccess) {
|
||||
|
||||
@@ -180,6 +180,8 @@ public class SystemKit {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IOObjectRelease(device)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -156,6 +156,16 @@ extension String: LocalizedError {
|
||||
public func matches(_ regex: String) -> Bool {
|
||||
return self.range(of: regex, options: .regularExpression, range: nil, locale: nil) != nil
|
||||
}
|
||||
|
||||
public func removedRegexMatches(pattern: String, replaceWith: String = "") -> String {
|
||||
do {
|
||||
let regex = try NSRegularExpression(pattern: pattern, options: NSRegularExpression.Options.caseInsensitive)
|
||||
let range = NSMakeRange(0, self.count)
|
||||
return regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: replaceWith)
|
||||
} catch {
|
||||
return self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Int {
|
||||
|
||||
Reference in New Issue
Block a user