mirror of
https://github.com/morgan9e/macos-stats
synced 2026-04-14 00:04:15 +09:00
feat: added top processes to the Disk module (#1370)
This commit is contained in:
@@ -128,28 +128,62 @@ public class Disks {
|
||||
}
|
||||
}
|
||||
|
||||
public struct Disk_process: IOProcess_p {
|
||||
private var base: DataSizeBase {
|
||||
DataSizeBase(rawValue: Store.shared.string(key: "\(Disk.name)_base", defaultValue: "byte")) ?? .byte
|
||||
}
|
||||
|
||||
public var pid: Int32
|
||||
public var name: String
|
||||
public var icon: NSImage = Constants.defaultProcessIcon
|
||||
|
||||
var read: Int
|
||||
var write: Int
|
||||
|
||||
public var input: String {
|
||||
Units(bytes: Int64(self.read)).getReadableSpeed(base: self.base)
|
||||
}
|
||||
public var output: String {
|
||||
Units(bytes: Int64(self.write)).getReadableSpeed(base: self.base)
|
||||
}
|
||||
|
||||
init(pid: Int32, name: String, read: Int, write: Int) {
|
||||
self.pid = pid
|
||||
self.name = name
|
||||
self.read = read
|
||||
self.write = write
|
||||
|
||||
if let app = NSRunningApplication(processIdentifier: pid) {
|
||||
if let name = app.localizedName {
|
||||
self.name = name
|
||||
}
|
||||
if let icon = app.icon {
|
||||
self.icon = icon
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class Disk: Module {
|
||||
private let popupView: Popup
|
||||
private let settingsView: Settings
|
||||
private let portalView: Portal
|
||||
public static let name: String = "Disk"
|
||||
|
||||
private let popupView: Popup = Popup()
|
||||
private let settingsView: Settings = Settings()
|
||||
private let portalView: Portal = Portal()
|
||||
|
||||
private var capacityReader: CapacityReader? = nil
|
||||
private var activityReader: ActivityReader? = nil
|
||||
private var processReader: ProcessReader? = nil
|
||||
|
||||
private var selectedDisk: String = ""
|
||||
private var notificationLevelState: Bool = false
|
||||
private var notificationID: String? = nil
|
||||
|
||||
private var notificationLevel: String {
|
||||
get {
|
||||
return Store.shared.string(key: "\(self.config.name)_notificationLevel", defaultValue: "Disabled")
|
||||
}
|
||||
Store.shared.string(key: "\(Disk.name)_notificationLevel", defaultValue: "Disabled")
|
||||
}
|
||||
|
||||
public init() {
|
||||
self.popupView = Popup()
|
||||
self.settingsView = Settings("Disk")
|
||||
self.portalView = Portal("Disk")
|
||||
|
||||
super.init(
|
||||
popup: self.popupView,
|
||||
settings: self.settingsView,
|
||||
@@ -159,7 +193,9 @@ public class Disk: Module {
|
||||
|
||||
self.capacityReader = CapacityReader()
|
||||
self.activityReader = ActivityReader()
|
||||
self.selectedDisk = Store.shared.string(key: "\(self.config.name)_disk", defaultValue: self.selectedDisk)
|
||||
self.processReader = ProcessReader()
|
||||
|
||||
self.selectedDisk = Store.shared.string(key: "\(Disk.name)_disk", defaultValue: self.selectedDisk)
|
||||
|
||||
self.capacityReader?.callbackHandler = { [unowned self] value in
|
||||
if let value = value {
|
||||
@@ -175,6 +211,11 @@ public class Disk: Module {
|
||||
self.activityCallback(value)
|
||||
}
|
||||
}
|
||||
self.processReader?.callbackHandler = { [unowned self] value in
|
||||
if let list = value {
|
||||
self.popupView.processCallback(list)
|
||||
}
|
||||
}
|
||||
|
||||
self.settingsView.selectedDiskHandler = { [unowned self] value in
|
||||
self.selectedDisk = value
|
||||
@@ -186,6 +227,12 @@ public class Disk: Module {
|
||||
self.settingsView.setInterval = { [unowned self] value in
|
||||
self.capacityReader?.setInterval(value)
|
||||
}
|
||||
self.settingsView.callbackWhenUpdateNumberOfProcesses = {
|
||||
self.popupView.numberOfProcessesUpdated()
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
self.processReader?.read()
|
||||
}
|
||||
}
|
||||
|
||||
if let reader = self.capacityReader {
|
||||
self.addReader(reader)
|
||||
@@ -193,6 +240,9 @@ public class Disk: Module {
|
||||
if let reader = self.activityReader {
|
||||
self.addReader(reader)
|
||||
}
|
||||
if let reader = self.processReader {
|
||||
self.addReader(reader)
|
||||
}
|
||||
}
|
||||
|
||||
public override func widgetDidSet(_ type: widget_t) {
|
||||
|
||||
@@ -13,8 +13,6 @@ import Cocoa
|
||||
import Kit
|
||||
|
||||
internal class Popup: PopupWrapper {
|
||||
private let emptyView: EmptyView = EmptyView(height: 30, isHidden: false, msg: localizedString("No disks are available"))
|
||||
|
||||
private var readColorState: Color = .secondBlue
|
||||
private var readColor: NSColor {
|
||||
var value = NSColor.systemRed
|
||||
@@ -32,46 +30,67 @@ internal class Popup: PopupWrapper {
|
||||
return value
|
||||
}
|
||||
|
||||
private var disks: NSStackView = {
|
||||
let view = NSStackView()
|
||||
view.spacing = Constants.Popup.margins
|
||||
view.orientation = .vertical
|
||||
return view
|
||||
}()
|
||||
private var processes: IOProcessView = IOProcessView(
|
||||
countKey: "\(Disk.name)_processes",
|
||||
inputColorKey: "\(Disk.name)_readColor",
|
||||
outputColorKey: "\(Disk.name)_writeColor"
|
||||
)
|
||||
|
||||
public init() {
|
||||
super.init(frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 30))
|
||||
super.init(frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 0))
|
||||
|
||||
self.readColorState = Color.fromString(Store.shared.string(key: "\(Disk.name)_readColor", defaultValue: self.readColorState.key))
|
||||
self.writeColorState = Color.fromString(Store.shared.string(key: "\(Disk.name)_writeColor", defaultValue: self.writeColorState.key))
|
||||
|
||||
self.orientation = .vertical
|
||||
self.spacing = Constants.Popup.margins
|
||||
self.distribution = .fill
|
||||
self.spacing = 0
|
||||
|
||||
self.addArrangedSubview(self.disks)
|
||||
self.addArrangedSubview(separatorView(localizedString("Top processes"), width: self.frame.width))
|
||||
self.addArrangedSubview(self.processes)
|
||||
|
||||
self.recalculateHeight()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func recalculateHeight() {
|
||||
let h = self.subviews.map({ $0.bounds.height }).reduce(0, +)
|
||||
if self.frame.size.height != h {
|
||||
self.setFrameSize(NSSize(width: self.frame.width, height: h))
|
||||
self.sizeCallback?(self.frame.size)
|
||||
}
|
||||
}
|
||||
|
||||
internal func capacityCallback(_ value: Disks) {
|
||||
defer {
|
||||
if value.isEmpty && self.emptyView.superview == nil {
|
||||
self.addArrangedSubview(self.emptyView)
|
||||
} else if !value.isEmpty && self.emptyView.superview != nil {
|
||||
self.emptyView.removeFromSuperview()
|
||||
}
|
||||
|
||||
let h = self.arrangedSubviews.map({ $0.bounds.height + self.spacing }).reduce(0, +) - self.spacing
|
||||
if h > 0 && self.frame.size.height != h {
|
||||
self.setFrameSize(NSSize(width: self.frame.width, height: h))
|
||||
self.sizeCallback?(self.frame.size)
|
||||
let h = self.disks.subviews.map({ $0.bounds.height + self.disks.spacing }).reduce(0, +) - self.disks.spacing
|
||||
if h > 0 && self.disks.frame.size.height != h {
|
||||
self.disks.setFrameSize(NSSize(width: self.frame.width, height: h))
|
||||
self.recalculateHeight()
|
||||
}
|
||||
}
|
||||
|
||||
self.subviews.filter{ $0 is DiskView }.map{ $0 as! DiskView }.forEach { (v: DiskView) in
|
||||
self.disks.subviews.filter{ $0 is DiskView }.map{ $0 as! DiskView }.forEach { (v: DiskView) in
|
||||
if !value.map({$0.BSDName}).contains(v.BSDName) {
|
||||
v.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
value.forEach { (drive: drive) in
|
||||
if let view = self.subviews.filter({ $0 is DiskView }).map({ $0 as! DiskView }).first(where: { $0.BSDName == drive.BSDName }) {
|
||||
if let view = self.disks.subviews.filter({ $0 is DiskView }).map({ $0 as! DiskView }).first(where: { $0.BSDName == drive.BSDName }) {
|
||||
view.updateFree(free: drive.free)
|
||||
} else {
|
||||
self.addArrangedSubview(DiskView(
|
||||
self.disks.addArrangedSubview(DiskView(
|
||||
width: self.frame.width,
|
||||
BSDName: drive.BSDName,
|
||||
name: drive.mediaName,
|
||||
@@ -84,7 +103,7 @@ internal class Popup: PopupWrapper {
|
||||
}
|
||||
|
||||
internal func activityCallback(_ value: Disks) {
|
||||
let views = self.subviews.filter{ $0 is DiskView }.map{ $0 as! DiskView }
|
||||
let views = self.disks.subviews.filter{ $0 is DiskView }.map{ $0 as! DiskView }
|
||||
value.reversed().forEach { (drive: drive) in
|
||||
if let view = views.first(where: { $0.name == drive.mediaName }) {
|
||||
view.updateReadWrite(read: drive.activity.read, write: drive.activity.write)
|
||||
@@ -92,6 +111,15 @@ internal class Popup: PopupWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
internal func processCallback(_ list: [Disk_process]) {
|
||||
self.processes.update(list)
|
||||
}
|
||||
|
||||
internal func numberOfProcessesUpdated() {
|
||||
self.processes.reinit()
|
||||
self.recalculateHeight()
|
||||
}
|
||||
|
||||
// MARK: - Settings
|
||||
|
||||
public override func settings() -> NSView? {
|
||||
@@ -122,10 +150,11 @@ internal class Popup: PopupWrapper {
|
||||
self.writeColorState = newValue
|
||||
Store.shared.set(key: "\(Disk.name)_writeColor", value: key)
|
||||
if let color = newValue.additional as? NSColor {
|
||||
for view in self.subviews.filter({ $0 is DiskView }).map({ $0 as! DiskView }) {
|
||||
for view in self.disks.subviews.filter({ $0 is DiskView }).map({ $0 as! DiskView }) {
|
||||
view.setChartColor(write: color)
|
||||
}
|
||||
}
|
||||
self.processes.updateColors()
|
||||
}
|
||||
@objc private func toggleReadColor(_ sender: NSMenuItem) {
|
||||
guard let key = sender.representedObject as? String,
|
||||
@@ -135,10 +164,11 @@ internal class Popup: PopupWrapper {
|
||||
self.readColorState = newValue
|
||||
Store.shared.set(key: "\(Disk.name)_readColor", value: key)
|
||||
if let color = newValue.additional as? NSColor {
|
||||
for view in self.subviews.filter({ $0 is DiskView }).map({ $0 as! DiskView }) {
|
||||
for view in self.disks.subviews.filter({ $0 is DiskView }).map({ $0 as! DiskView }) {
|
||||
view.setChartColor(read: color)
|
||||
}
|
||||
}
|
||||
self.processes.updateColors()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,7 +276,7 @@ internal class NameView: NSStackView {
|
||||
let readView: NSView = NSView(frame: NSRect(x: 0, y: 0, width: 32, height: activity.frame.height))
|
||||
let readField: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: nameField.frame.width, height: readView.frame.height))
|
||||
readField.stringValue = "R"
|
||||
let readState: NSView = NSView(frame: NSRect(x: 13, y: (readView.frame.height-10)/2, width: 9, height: 9))
|
||||
let readState: NSView = NSView(frame: NSRect(x: 13, y: (readView.frame.height-9)/2, width: 10, height: 10))
|
||||
readState.wantsLayer = true
|
||||
readState.layer?.backgroundColor = NSColor.lightGray.withAlphaComponent(0.75).cgColor
|
||||
readState.layer?.cornerRadius = 2
|
||||
@@ -254,7 +284,7 @@ internal class NameView: NSStackView {
|
||||
let writeView: NSView = NSView(frame: NSRect(x: 0, y: 0, width: 32, height: activity.frame.height))
|
||||
let writeField: NSTextField = TextView(frame: NSRect(x: 0, y: 0, width: nameField.frame.width, height: readView.frame.height))
|
||||
writeField.stringValue = "W"
|
||||
let writeState: NSView = NSView(frame: NSRect(x: 17, y: (writeView.frame.height-10)/2, width: 9, height: 9))
|
||||
let writeState: NSView = NSView(frame: NSRect(x: 17, y: (writeView.frame.height-10)/2, width: 10, height: 10))
|
||||
writeState.wantsLayer = true
|
||||
writeState.layer?.backgroundColor = NSColor.lightGray.withAlphaComponent(0.75).cgColor
|
||||
writeState.layer?.cornerRadius = 2
|
||||
@@ -551,3 +581,184 @@ internal class LegendView: NSView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public protocol IOProcess_p {
|
||||
var pid: Int32 { get }
|
||||
var name: String { get }
|
||||
var icon: NSImage { get }
|
||||
|
||||
var input: String { get }
|
||||
var output: String { get }
|
||||
}
|
||||
|
||||
public class IOProcessView: NSStackView {
|
||||
private let countKey: String
|
||||
private let inputColorKey: String
|
||||
private let outputColorKey: String
|
||||
|
||||
private var initialized: Bool = false
|
||||
|
||||
private var count: Int {
|
||||
Store.shared.int(key: countKey, defaultValue: 5)
|
||||
}
|
||||
private var readColor: NSColor {
|
||||
Color.fromString(Store.shared.string(key: inputColorKey, defaultValue: Color.secondBlue.key)).additional as! NSColor
|
||||
}
|
||||
private var writeColor: NSColor {
|
||||
Color.fromString(Store.shared.string(key: outputColorKey, defaultValue: Color.secondRed.key)).additional as! NSColor
|
||||
}
|
||||
|
||||
private var inputBoxView: NSView?
|
||||
private var outputBoxView: NSView?
|
||||
|
||||
public var height: CGFloat {
|
||||
CGFloat((self.count+1) * 22)
|
||||
}
|
||||
|
||||
init(countKey: String, inputColorKey: String, outputColorKey: String) {
|
||||
self.countKey = countKey
|
||||
self.inputColorKey = inputColorKey
|
||||
self.outputColorKey = outputColorKey
|
||||
|
||||
super.init(frame: NSRect.zero)
|
||||
|
||||
self.orientation = .vertical
|
||||
self.spacing = 1
|
||||
|
||||
self.reinit()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public func reinit() {
|
||||
self.subviews.forEach({ $0.removeFromSuperview() })
|
||||
|
||||
self.addArrangedSubview(self.legendRow())
|
||||
for _ in 0..<count {
|
||||
self.addArrangedSubview(TopProcess())
|
||||
}
|
||||
|
||||
self.setFrameSize(NSSize(width: self.frame.width, height: self.height))
|
||||
}
|
||||
|
||||
private func legendRow() -> NSView {
|
||||
let view: NSStackView = NSStackView()
|
||||
view.spacing = 50
|
||||
view.orientation = .horizontal
|
||||
view.heightAnchor.constraint(equalToConstant: 21).isActive = true
|
||||
|
||||
let inputView: NSView = NSView()
|
||||
inputView.widthAnchor.constraint(equalToConstant: 10).isActive = true
|
||||
inputView.heightAnchor.constraint(equalToConstant: 10).isActive = true
|
||||
inputView.wantsLayer = true
|
||||
inputView.layer?.backgroundColor = self.readColor.cgColor
|
||||
inputView.layer?.cornerRadius = 2
|
||||
self.inputBoxView = inputView
|
||||
|
||||
let outputView: NSView = NSView()
|
||||
outputView.widthAnchor.constraint(equalToConstant: 10).isActive = true
|
||||
outputView.heightAnchor.constraint(equalToConstant: 10).isActive = true
|
||||
outputView.wantsLayer = true
|
||||
outputView.layer?.backgroundColor = self.writeColor.cgColor
|
||||
outputView.layer?.cornerRadius = 2
|
||||
self.outputBoxView = outputView
|
||||
|
||||
view.addArrangedSubview(NSView())
|
||||
view.addArrangedSubview(inputView)
|
||||
view.addArrangedSubview(outputView)
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
public func update(_ list: [IOProcess_p]) {
|
||||
DispatchQueue.main.async(execute: {
|
||||
if !(self.window?.isVisible ?? false) && self.initialized {
|
||||
return
|
||||
}
|
||||
|
||||
for (i, p) in self.subviews.compactMap({ $0 as? TopProcess }).enumerated() {
|
||||
if list.count != self.count && self.initialized {
|
||||
p.clear()
|
||||
}
|
||||
if list.indices.contains(i) {
|
||||
p.set(list[i])
|
||||
}
|
||||
}
|
||||
|
||||
self.initialized = true
|
||||
})
|
||||
}
|
||||
|
||||
public func updateColors() {
|
||||
self.inputBoxView?.layer?.backgroundColor = self.readColor.cgColor
|
||||
self.outputBoxView?.layer?.backgroundColor = self.writeColor.cgColor
|
||||
}
|
||||
}
|
||||
|
||||
public class TopProcess: NSStackView {
|
||||
private var imageView: NSImageView = NSImageView()
|
||||
private var labelView: NSTextField = LabelField()
|
||||
private var inputView: NSTextField = ValueField()
|
||||
private var outputView: NSTextField = ValueField()
|
||||
|
||||
init() {
|
||||
super.init(frame: NSRect.zero)
|
||||
|
||||
self.orientation = .horizontal
|
||||
self.spacing = 0
|
||||
self.alignment = .centerY
|
||||
self.layer?.cornerRadius = 3
|
||||
|
||||
self.labelView.cell?.truncatesLastVisibleLine = true
|
||||
self.inputView.font = NSFont.systemFont(ofSize: 10, weight: .regular)
|
||||
self.outputView.font = NSFont.systemFont(ofSize: 10, weight: .regular)
|
||||
|
||||
self.addArrangedSubview(self.imageView)
|
||||
self.addArrangedSubview(self.labelView)
|
||||
self.addArrangedSubview(NSView())
|
||||
self.addArrangedSubview(self.inputView)
|
||||
self.addArrangedSubview(self.outputView)
|
||||
|
||||
self.addTrackingArea(NSTrackingArea(
|
||||
rect: NSRect(x: 0, y: 0, width: 264, height: 21),
|
||||
options: [NSTrackingArea.Options.activeAlways, NSTrackingArea.Options.mouseEnteredAndExited, NSTrackingArea.Options.activeInActiveApp],
|
||||
owner: self,
|
||||
userInfo: nil
|
||||
))
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
self.imageView.widthAnchor.constraint(equalToConstant: 12),
|
||||
self.labelView.heightAnchor.constraint(equalToConstant: 16),
|
||||
self.inputView.widthAnchor.constraint(equalToConstant: 60),
|
||||
self.outputView.widthAnchor.constraint(equalToConstant: 60),
|
||||
self.heightAnchor.constraint(equalToConstant: 21)
|
||||
])
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public override func mouseEntered(with: NSEvent) {
|
||||
self.layer?.backgroundColor = .init(gray: 0.01, alpha: 0.05)
|
||||
}
|
||||
public override func mouseExited(with: NSEvent) {
|
||||
self.layer?.backgroundColor = .none
|
||||
}
|
||||
|
||||
public func set(_ process: IOProcess_p) {
|
||||
self.imageView.image = process.icon
|
||||
self.labelView.stringValue = process.name
|
||||
self.inputView.stringValue = process.input
|
||||
self.outputView.stringValue = process.output
|
||||
self.toolTip = "pid: \(process.pid)"
|
||||
}
|
||||
|
||||
public func clear() {
|
||||
self.inputView.stringValue = "-"
|
||||
self.outputView.stringValue = "-"
|
||||
self.toolTip = ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,15 +13,13 @@ import Cocoa
|
||||
import Kit
|
||||
|
||||
internal class Portal: NSStackView, Portal_p {
|
||||
internal var name: String
|
||||
internal var name: String { Disk.name }
|
||||
|
||||
private var circle: PieChartView? = nil
|
||||
|
||||
private var initialized: Bool = false
|
||||
|
||||
init(_ title: String) {
|
||||
self.name = title
|
||||
|
||||
init() {
|
||||
super.init(frame: NSRect.zero)
|
||||
|
||||
self.wantsLayer = true
|
||||
|
||||
@@ -285,3 +285,104 @@ public func getDeviceIOParent(_ obj: io_registry_entry_t, level: Int) -> io_regi
|
||||
|
||||
return parent
|
||||
}
|
||||
|
||||
struct io {
|
||||
var read: Int
|
||||
var write: Int
|
||||
}
|
||||
|
||||
public class ProcessReader: Reader<[Disk_process]> {
|
||||
private let queue = DispatchQueue(label: "eu.exelban.Disk.processReader")
|
||||
|
||||
private var _list: [Int32: io] = [:]
|
||||
private var list: [Int32: io] {
|
||||
get {
|
||||
self.queue.sync { self._list }
|
||||
}
|
||||
set {
|
||||
self.queue.sync { self._list = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
private var numberOfProcesses: Int {
|
||||
Store.shared.int(key: "\(Disk.name)_processes", defaultValue: 5)
|
||||
}
|
||||
|
||||
public override func read() {
|
||||
guard self.numberOfProcesses != 0 else { return }
|
||||
|
||||
guard let output = runProcess(path: "/bin/ps", args: ["-Aceo pid,args", "-r"]) else { return }
|
||||
|
||||
var processes: [Disk_process] = []
|
||||
output.enumerateLines { (line, _) -> Void in
|
||||
var str = line.trimmingCharacters(in: .whitespaces)
|
||||
let pidString = str.findAndCrop(pattern: "^\\d+")
|
||||
if let range = str.range(of: pidString) {
|
||||
str = str.replacingCharacters(in: range, with: "")
|
||||
}
|
||||
let name = str.findAndCrop(pattern: "^[^ ]+")
|
||||
guard let pid = Int32(pidString) else { return }
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
guard result != -1 else { return }
|
||||
|
||||
let bytesRead = Int(usage.ri_diskio_bytesread)
|
||||
let bytesWritten = Int(usage.ri_diskio_byteswritten)
|
||||
|
||||
if self.list[pid] == nil {
|
||||
self.list[pid] = io(read: bytesRead, write: bytesWritten)
|
||||
}
|
||||
|
||||
if let v = self.list[pid] {
|
||||
let read = bytesRead - v.read
|
||||
let write = bytesWritten - v.write
|
||||
if read != 0 || write != 0 {
|
||||
processes.append(Disk_process(pid: pid, name: name, read: read, write: write))
|
||||
}
|
||||
}
|
||||
|
||||
self.list[pid]?.read = bytesRead
|
||||
self.list[pid]?.write = bytesWritten
|
||||
}
|
||||
|
||||
processes.sort {
|
||||
let firstMax = max($0.read, $0.write)
|
||||
let secondMax = max($1.read, $1.write)
|
||||
let firstMin = min($0.read, $0.write)
|
||||
let secondMin = min($1.read, $1.write)
|
||||
|
||||
if firstMax == secondMax && firstMin != secondMin { // max values are the same, min not. Sort by min values
|
||||
return firstMin < secondMin
|
||||
}
|
||||
return firstMax < secondMax // max values are not the same, sort by max value
|
||||
}
|
||||
|
||||
self.callback(processes.suffix(self.numberOfProcesses).reversed())
|
||||
}
|
||||
}
|
||||
|
||||
private func runProcess(path: String, args: [String] = []) -> String? {
|
||||
let task = Process()
|
||||
task.launchPath = path
|
||||
task.arguments = args
|
||||
|
||||
let outputPipe = Pipe()
|
||||
defer {
|
||||
outputPipe.fileHandleForReading.closeFile()
|
||||
}
|
||||
task.standardOutput = outputPipe
|
||||
|
||||
do {
|
||||
try task.run()
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
|
||||
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
return String(decoding: outputData, as: UTF8.self)
|
||||
}
|
||||
|
||||
@@ -16,24 +16,25 @@ internal class Settings: NSStackView, Settings_v {
|
||||
private var removableState: Bool = false
|
||||
private var updateIntervalValue: Int = 10
|
||||
private var notificationLevel: String = "Disabled"
|
||||
private var numberOfProcesses: Int = 5
|
||||
|
||||
public var selectedDiskHandler: (String) -> Void = {_ in }
|
||||
public var callback: (() -> Void) = {}
|
||||
public var setInterval: ((_ value: Int) -> Void) = {_ in }
|
||||
public var callbackWhenUpdateNumberOfProcesses: (() -> Void) = {}
|
||||
|
||||
private let title: String
|
||||
private var selectedDisk: String
|
||||
private var button: NSPopUpButton?
|
||||
private var intervalSelectView: NSView? = nil
|
||||
|
||||
private var list: [String] = []
|
||||
|
||||
public init(_ title: String) {
|
||||
self.title = title
|
||||
self.selectedDisk = Store.shared.string(key: "\(self.title)_disk", defaultValue: "")
|
||||
self.removableState = Store.shared.bool(key: "\(self.title)_removable", defaultValue: self.removableState)
|
||||
self.updateIntervalValue = Store.shared.int(key: "\(self.title)_updateInterval", defaultValue: self.updateIntervalValue)
|
||||
self.notificationLevel = Store.shared.string(key: "\(self.title)_notificationLevel", defaultValue: self.notificationLevel)
|
||||
public init() {
|
||||
self.selectedDisk = Store.shared.string(key: "\(Disk.name)_disk", defaultValue: "")
|
||||
self.removableState = Store.shared.bool(key: "\(Disk.name)_removable", defaultValue: self.removableState)
|
||||
self.updateIntervalValue = Store.shared.int(key: "\(Disk.name)_updateInterval", defaultValue: self.updateIntervalValue)
|
||||
self.notificationLevel = Store.shared.string(key: "\(Disk.name)_notificationLevel", defaultValue: self.notificationLevel)
|
||||
self.numberOfProcesses = Store.shared.int(key: "\(Disk.name)_processes", defaultValue: self.numberOfProcesses)
|
||||
|
||||
super.init(frame: NSRect(x: 0, y: 0, width: 0, height: 0))
|
||||
|
||||
@@ -56,6 +57,13 @@ internal class Settings: NSStackView, Settings_v {
|
||||
public func load(widgets: [widget_t]) {
|
||||
self.subviews.forEach{ $0.removeFromSuperview() }
|
||||
|
||||
self.addArrangedSubview(selectSettingsRowV1(
|
||||
title: localizedString("Number of top processes"),
|
||||
action: #selector(changeNumberOfProcesses),
|
||||
items: NumbersOfProcesses.map{ "\($0)" },
|
||||
selected: "\(self.numberOfProcesses)"
|
||||
))
|
||||
|
||||
self.intervalSelectView = selectSettingsRowV1(
|
||||
title: localizedString("Update interval"),
|
||||
action: #selector(changeUpdateInterval),
|
||||
@@ -122,10 +130,18 @@ internal class Settings: NSStackView, Settings_v {
|
||||
})
|
||||
}
|
||||
|
||||
@objc private func changeNumberOfProcesses(_ sender: NSMenuItem) {
|
||||
if let value = Int(sender.title) {
|
||||
self.numberOfProcesses = value
|
||||
Store.shared.set(key: "\(Disk.name)_processes", value: value)
|
||||
self.callbackWhenUpdateNumberOfProcesses()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleSelection(_ sender: NSPopUpButton) {
|
||||
guard let item = sender.selectedItem else { return }
|
||||
self.selectedDisk = item.title
|
||||
Store.shared.set(key: "\(self.title)_disk", value: item.title)
|
||||
Store.shared.set(key: "\(Disk.name)_disk", value: item.title)
|
||||
self.selectedDiskHandler(item.title)
|
||||
}
|
||||
|
||||
@@ -138,7 +154,7 @@ internal class Settings: NSStackView, Settings_v {
|
||||
}
|
||||
|
||||
self.removableState = state! == .on ? true : false
|
||||
Store.shared.set(key: "\(self.title)_removable", value: self.removableState)
|
||||
Store.shared.set(key: "\(Disk.name)_removable", value: self.removableState)
|
||||
self.callback()
|
||||
}
|
||||
|
||||
@@ -152,15 +168,15 @@ internal class Settings: NSStackView, Settings_v {
|
||||
guard let key = sender.representedObject as? String else { return }
|
||||
|
||||
if key == "Disabled" {
|
||||
Store.shared.set(key: "\(self.title)_notificationLevel", value: key)
|
||||
Store.shared.set(key: "\(Disk.name)_notificationLevel", value: key)
|
||||
} else if let value = Double(key.replacingOccurrences(of: "%", with: "")) {
|
||||
Store.shared.set(key: "\(self.title)_notificationLevel", value: "\(value/100)")
|
||||
Store.shared.set(key: "\(Disk.name)_notificationLevel", value: "\(value/100)")
|
||||
}
|
||||
}
|
||||
|
||||
public func setUpdateInterval(value: Int) {
|
||||
self.updateIntervalValue = value
|
||||
Store.shared.set(key: "\(self.title)_updateInterval", value: value)
|
||||
Store.shared.set(key: "\(Disk.name)_updateInterval", value: value)
|
||||
self.setInterval(value)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user