From b21630218ccf414839b11541165977a32608330a Mon Sep 17 00:00:00 2001 From: Serhiy Mytrovtsiy Date: Fri, 5 Jan 2024 21:02:29 +0100 Subject: [PATCH] feat: initialized ProcessesView that will be common for all popups --- Kit/helpers.swift | 152 +---------------- Kit/process.swift | 281 ++++++++++++++++++++++++++++++++ Modules/Battery/readers.swift | 8 +- Modules/CPU/readers.swift | 8 +- Modules/Disk/main.swift | 12 +- Modules/Disk/readers.swift | 2 +- Modules/Net/main.swift | 26 ++- Modules/Net/readers.swift | 8 +- Modules/RAM/readers.swift | 6 +- Stats.xcodeproj/project.pbxproj | 4 + 10 files changed, 330 insertions(+), 177 deletions(-) create mode 100644 Kit/process.swift diff --git a/Kit/helpers.swift b/Kit/helpers.swift index 93371a43..ce70319b 100644 --- a/Kit/helpers.swift +++ b/Kit/helpers.swift @@ -8,7 +8,6 @@ // // Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved. // -// swiftlint:disable file_length import Cocoa import ServiceManagement @@ -581,24 +580,21 @@ public func removeNotification(_ id: String) { center.removeDeliveredNotifications(withIdentifiers: [id]) } -public struct TopProcess: Codable { +public struct TopProcess: Codable, Process_p { public var pid: Int - public var command: String - public var name: String? + public var name: String public var usage: Double - - public var icon: NSImage? { + public var icon: NSImage { get { - if let app = NSRunningApplication(processIdentifier: pid_t(self.pid) ) { - return app.icon + if let app = NSRunningApplication(processIdentifier: pid_t(self.pid)), let icon = app.icon { + return icon } return Constants.defaultProcessIcon } } - public init(pid: Int, command: String, name: String?, usage: Double) { + public init(pid: Int, name: String, usage: Double) { self.pid = pid - self.command = command self.name = name self.usage = usage } @@ -781,142 +777,6 @@ public func sysctlByName(_ name: String) -> Int64 { return num } -public class ProcessView: NSStackView { - private var pid: Int? = nil - private var lock: Bool = false - - private var imageView: NSImageView = NSImageView() - private var killView: NSButton = NSButton() - private var labelView: LabelField = { - let view = LabelField() - view.cell?.truncatesLastVisibleLine = true - return view - }() - private var valueView: ValueField = ValueField() - - public init(size: CGSize = CGSize(width: 264, height: 22), valueSize: CGFloat = 55) { - var rect = NSRect(x: 5, y: 5, width: 12, height: 12) - if size.height != 22 { - rect = NSRect(x: 3, y: 3, width: 12, height: 12) - } - self.imageView = NSImageView(frame: rect) - self.killView = NSButton(frame: rect) - - super.init(frame: NSRect(x: 0, y: 0, width: size.width, height: size.height)) - - self.wantsLayer = true - self.orientation = .horizontal - self.distribution = .fillProportionally - self.spacing = 0 - self.layer?.cornerRadius = 3 - - let imageBox: NSView = { - let view = NSView() - - self.killView.bezelStyle = .regularSquare - self.killView.translatesAutoresizingMaskIntoConstraints = false - self.killView.imageScaling = .scaleNone - self.killView.image = Bundle(for: type(of: self)).image(forResource: "cancel")! - self.killView.contentTintColor = .lightGray - self.killView.isBordered = false - self.killView.action = #selector(self.kill) - self.killView.target = self - self.killView.toolTip = localizedString("Kill process") - self.killView.focusRingType = .none - self.killView.isHidden = true - - view.addSubview(self.imageView) - view.addSubview(self.killView) - - return view - }() - - self.addArrangedSubview(imageBox) - self.addArrangedSubview(self.labelView) - self.addArrangedSubview(self.valueView) - - self.addTrackingArea(NSTrackingArea( - rect: NSRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height), - options: [NSTrackingArea.Options.activeAlways, NSTrackingArea.Options.mouseEnteredAndExited, NSTrackingArea.Options.activeInActiveApp], - owner: self, - userInfo: nil - )) - - NSLayoutConstraint.activate([ - imageBox.widthAnchor.constraint(equalToConstant: self.bounds.height), - imageBox.heightAnchor.constraint(equalToConstant: self.bounds.height), - self.labelView.heightAnchor.constraint(equalToConstant: 16), - self.valueView.widthAnchor.constraint(equalToConstant: valueSize), - self.widthAnchor.constraint(equalToConstant: self.bounds.width), - self.heightAnchor.constraint(equalToConstant: self.bounds.height) - ]) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public override func mouseEntered(with: NSEvent) { - if self.lock { - self.imageView.isHidden = true - self.killView.isHidden = false - return - } - self.layer?.backgroundColor = .init(gray: 0.01, alpha: 0.05) - } - - public override func mouseExited(with: NSEvent) { - if self.lock { - self.imageView.isHidden = false - self.killView.isHidden = true - return - } - self.layer?.backgroundColor = .none - } - - public override func mouseDown(with: NSEvent) { - self.setLock(!self.lock) - } - - public func set(_ process: TopProcess, _ value: String) { - if self.lock && process.pid != self.pid { return } - - self.labelView.stringValue = process.name != nil ? process.name! : process.command - self.valueView.stringValue = value - self.imageView.image = process.icon - self.pid = process.pid - self.toolTip = "pid: \(process.pid)" - } - - public func clear() { - self.labelView.stringValue = "" - self.valueView.stringValue = "" - self.imageView.image = nil - self.pid = nil - self.setLock(false) - self.toolTip = "" - } - - public func setLock(_ state: Bool) { - self.lock = state - if self.lock { - self.imageView.isHidden = true - self.killView.isHidden = false - self.layer?.backgroundColor = .init(gray: 0.01, alpha: 0.1) - } else { - self.imageView.isHidden = false - self.killView.isHidden = true - self.layer?.backgroundColor = .none - } - } - - @objc public func kill() { - if let pid = self.pid { - asyncShell("kill \(pid)") - } - } -} - public class CAText: CATextLayer { public init(fontSize: CGFloat = 12, weight: NSFont.Weight = .regular) { super.init() diff --git a/Kit/process.swift b/Kit/process.swift new file mode 100644 index 00000000..1020c3c4 --- /dev/null +++ b/Kit/process.swift @@ -0,0 +1,281 @@ +// +// process.swift +// Kit +// +// Created by Serhiy Mytrovtsiy on 05/01/2024 +// Using Swift 5.0 +// Running on macOS 14.3 +// +// Copyright © 2024 Serhiy Mytrovtsiy. All rights reserved. +// + +import Cocoa + +public protocol Process_p { + var pid: Int { get } + var name: String { get } + var icon: NSImage { get } +} + +public typealias ProcessHeader = (title: String, color: NSColor?) + +public class ProcessesView: NSStackView { + public var count: Int { + self.list.count + } + private var list: [ProcessView] = [] + private var colorViews: [ColorView] = [] + + public init(frame: NSRect, values: [ProcessHeader], n: Int = 0) { + super.init(frame: frame) + + self.orientation = .vertical + self.spacing = 0 + + let header = self.generateHeaderView(values) + self.addArrangedSubview(header) + + for _ in 0.. NSView { + let view = NSStackView() + view.widthAnchor.constraint(equalToConstant: self.bounds.width).isActive = true + view.heightAnchor.constraint(equalToConstant: ProcessView.height).isActive = true + view.orientation = .horizontal + view.distribution = .fillProportionally + view.spacing = 0 + + let iconView: NSImageView = NSImageView() + iconView.widthAnchor.constraint(equalToConstant: ProcessView.height).isActive = true + iconView.heightAnchor.constraint(equalToConstant: ProcessView.height).isActive = true + view.addArrangedSubview(iconView) + + let titleField = LabelField() + titleField.cell?.truncatesLastVisibleLine = true + titleField.toolTip = "Process" + titleField.stringValue = "Process" + titleField.textColor = .tertiaryLabelColor + titleField.font = NSFont.systemFont(ofSize: 12, weight: .medium) + view.addArrangedSubview(titleField) + + if values.count == 1, let v = values.first { + let field = LabelField() + field.cell?.truncatesLastVisibleLine = true + field.toolTip = v.title + field.stringValue = v.title + field.alignment = .right + field.textColor = .tertiaryLabelColor + field.font = NSFont.systemFont(ofSize: 12, weight: .medium) + view.addArrangedSubview(field) + } else { + for v in values { + if let color = v.color { + let container: NSView = NSView() + container.widthAnchor.constraint(equalToConstant: 60).isActive = true + container.heightAnchor.constraint(equalToConstant: ProcessView.height).isActive = true + let colorBlock: ColorView = ColorView(frame: NSRect(x: 48, y: 5, width: 12, height: 12), color: color, state: true, radius: 4) + colorBlock.toolTip = v.title + colorBlock.widthAnchor.constraint(equalToConstant: 12).isActive = true + colorBlock.heightAnchor.constraint(equalToConstant: 12).isActive = true + self.colorViews.append(colorBlock) + container.addSubview(colorBlock) + view.addArrangedSubview(container) + } + } + } + + return view + } + + public func setLock(_ newValue: Bool) { + self.list.forEach{ $0.setLock(newValue) } + } + + public func clear(_ symbol: String = "") { + self.list.forEach{ $0.clear(symbol) } + } + + public func set(_ idx: Int, _ process: Process_p, _ values: [String]) { + if self.list.indices.contains(idx) { + self.list[idx].set(process, values) + } + } + + public func setColor(_ idx: Int, _ newColor: NSColor) { + if self.colorViews.indices.contains(idx) { + self.colorViews[idx].setColor(newColor) + } + } +} + +public class ProcessView: NSStackView { + static let height: CGFloat = 22 + + private var pid: Int? = nil + private var lock: Bool = false + + private var imageView: NSImageView = NSImageView() + private var killView: NSButton = NSButton() + private var labelView: LabelField = { + let view = LabelField() + view.cell?.truncatesLastVisibleLine = true + return view + }() + private var valueViews: [ValueField] = [] + + public init(size: CGSize = CGSize(width: 264, height: 22), n: Int = 1) { + var rect = NSRect(x: 5, y: 5, width: 12, height: 12) + if size.height != 22 { + rect = NSRect(x: 3, y: 3, width: 12, height: 12) + } + self.imageView = NSImageView(frame: rect) + self.killView = NSButton(frame: rect) + + super.init(frame: NSRect(x: 0, y: 0, width: size.width, height: size.height)) + + self.wantsLayer = true + self.orientation = .horizontal + self.distribution = .fillProportionally + self.spacing = 0 + self.layer?.cornerRadius = 3 + + let imageBox: NSView = { + let view = NSView() + + self.killView.bezelStyle = .regularSquare + self.killView.translatesAutoresizingMaskIntoConstraints = false + self.killView.imageScaling = .scaleNone + self.killView.image = Bundle(for: type(of: self)).image(forResource: "cancel")! + self.killView.contentTintColor = .lightGray + self.killView.isBordered = false + self.killView.action = #selector(self.kill) + self.killView.target = self + self.killView.toolTip = localizedString("Kill process") + self.killView.focusRingType = .none + self.killView.isHidden = true + + view.addSubview(self.imageView) + view.addSubview(self.killView) + + return view + }() + + self.addArrangedSubview(imageBox) + self.addArrangedSubview(self.labelView) + self.valuesViews(n).forEach{ self.addArrangedSubview($0) } + + self.addTrackingArea(NSTrackingArea( + rect: NSRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height), + options: [NSTrackingArea.Options.activeAlways, NSTrackingArea.Options.mouseEnteredAndExited, NSTrackingArea.Options.activeInActiveApp], + owner: self, + userInfo: nil + )) + + NSLayoutConstraint.activate([ + imageBox.widthAnchor.constraint(equalToConstant: self.bounds.height), + imageBox.heightAnchor.constraint(equalToConstant: self.bounds.height), + self.labelView.heightAnchor.constraint(equalToConstant: 16), + self.widthAnchor.constraint(equalToConstant: self.bounds.width), + self.heightAnchor.constraint(equalToConstant: self.bounds.height) + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func valuesViews(_ n: Int) -> [NSView] { + var list: [ValueField] = [] + + for _ in 0.. { return } - var name: String? = nil - if let app = NSRunningApplication(processIdentifier: pid_t(pid) ) { - name = app.localizedName ?? nil + var name: String = command + if let app = NSRunningApplication(processIdentifier: pid_t(pid)), let n = app.localizedName { + name = n } - processes.append(TopProcess(pid: pid, command: command, name: name, usage: usage)) + processes.append(TopProcess(pid: pid, name: name, usage: usage)) } } diff --git a/Modules/CPU/readers.swift b/Modules/CPU/readers.swift index 1b12ea64..f966341e 100644 --- a/Modules/CPU/readers.swift +++ b/Modules/CPU/readers.swift @@ -230,12 +230,12 @@ public class ProcessReader: Reader<[TopProcess]> { let pid = Int(pidFind.cropped) ?? 0 let usage = Double(usageFind.cropped.replacingOccurrences(of: ",", with: ".")) ?? 0 - var name: String? = nil - if let app = NSRunningApplication(processIdentifier: pid_t(pid) ) { - name = app.localizedName ?? nil + var name: String = command + if let app = NSRunningApplication(processIdentifier: pid_t(pid)), let n = app.localizedName { + name = n } - processes.append(TopProcess(pid: pid, command: command, name: name, usage: usage)) + processes.append(TopProcess(pid: pid, name: name, usage: usage)) } if index == self.numberOfProcesses { stop = true } diff --git a/Modules/Disk/main.swift b/Modules/Disk/main.swift index 20c3557d..bca93d6e 100644 --- a/Modules/Disk/main.swift +++ b/Modules/Disk/main.swift @@ -158,15 +158,15 @@ public class Disks: Codable { } } -public struct Disk_process: IOProcess_p, Codable { - private var base: DataSizeBase { +public struct Disk_process: Process_p, Codable { + public var base: DataSizeBase { DataSizeBase(rawValue: Store.shared.string(key: "\(Disk.name)_base", defaultValue: "byte")) ?? .byte } - public var pid: Int32 + public var pid: Int public var name: String public var icon: NSImage { - if let app = NSRunningApplication(processIdentifier: self.pid) { + if let app = NSRunningApplication(processIdentifier: pid_t(self.pid)) { return app.icon ?? Constants.defaultProcessIcon } return Constants.defaultProcessIcon @@ -182,13 +182,13 @@ public struct Disk_process: IOProcess_p, Codable { Units(bytes: Int64(self.write)).getReadableSpeed(base: self.base) } - init(pid: Int32, name: String, read: Int, write: Int) { + init(pid: Int, 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 app = NSRunningApplication(processIdentifier: pid_t(pid)) { if let name = app.localizedName { self.name = name } diff --git a/Modules/Disk/readers.swift b/Modules/Disk/readers.swift index 90597a73..7c237559 100644 --- a/Modules/Disk/readers.swift +++ b/Modules/Disk/readers.swift @@ -407,7 +407,7 @@ public class ProcessReader: Reader<[Disk_process]> { 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)) + processes.append(Disk_process(pid: Int(pid), name: name, read: read, write: write)) } } diff --git a/Modules/Net/main.swift b/Modules/Net/main.swift index 4d1e15ec..294e3360 100644 --- a/Modules/Net/main.swift +++ b/Modules/Net/main.swift @@ -99,20 +99,28 @@ public struct Network_Connectivity: Codable { var latency: Double = 0 } -public struct Network_Process: Codable { - var time: Date = Date() - var name: String = "" - var pid: String = "" - var download: Int = 0 - var upload: Int = 0 - var icon: NSImage { +public struct Network_Process: Codable, Process_p { + public var pid: Int + public var name: String + public var time: Date + public var download: Int + public var upload: Int + public var icon: NSImage { get { - if let pid = pid_t(self.pid), let app = NSRunningApplication(processIdentifier: pid) { - return app.icon ?? Constants.defaultProcessIcon + if let app = NSRunningApplication(processIdentifier: pid_t(self.pid)), let icon = app.icon { + return icon } return Constants.defaultProcessIcon } } + + public init(pid: Int = 0, name: String = "", time: Date = Date(), download: Int = 0, upload: Int = 0) { + self.pid = pid + self.name = name + self.time = time + self.download = download + self.upload = upload + } } public class Network: Module { diff --git a/Modules/Net/readers.swift b/Modules/Net/readers.swift index 52fc5ee4..e36906f1 100644 --- a/Modules/Net/readers.swift +++ b/Modules/Net/readers.swift @@ -500,16 +500,16 @@ public class ProcessReader: Reader<[Network_Process]> { let nameArray = parsedLine[0].split(separator: ".") if let pid = nameArray.last { - process.pid = String(pid) + process.pid = Int(pid) ?? 0 } - if let app = NSRunningApplication(processIdentifier: pid_t(process.pid) ?? 0) { + if let app = NSRunningApplication(processIdentifier: pid_t(process.pid) ) { process.name = app.localizedName ?? nameArray.dropLast().joined(separator: ".") } else { process.name = nameArray.dropLast().joined(separator: ".") } if process.name == "" { - process.name = process.pid + process.name = "\(process.pid)" } if let download = Int(parsedLine[1]) { @@ -543,7 +543,7 @@ public class ProcessReader: Reader<[Network_Process]> { upload = 0 } - processes.append(Network_Process(time: time, name: p.name, pid: p.pid, download: download, upload: upload)) + processes.append(Network_Process(pid: p.pid, name: p.name, time: time, download: download, upload: upload)) } } self.previous = list diff --git a/Modules/RAM/readers.swift b/Modules/RAM/readers.swift index f10c948f..363342f5 100644 --- a/Modules/RAM/readers.swift +++ b/Modules/RAM/readers.swift @@ -189,10 +189,10 @@ public class ProcessReader: Reader<[TopProcess]> { } var name: String = command - if let app = NSRunningApplication(processIdentifier: pid_t(pid) ) { - name = app.localizedName ?? command + if let app = NSRunningApplication(processIdentifier: pid_t(pid)), let n = app.localizedName { + name = n } - return TopProcess(pid: pid, command: command, name: name, usage: usage * Double(1024 * 1024)) + return TopProcess(pid: pid, name: name, usage: usage * Double(1024 * 1024)) } } diff --git a/Stats.xcodeproj/project.pbxproj b/Stats.xcodeproj/project.pbxproj index 3b539f45..740e0063 100644 --- a/Stats.xcodeproj/project.pbxproj +++ b/Stats.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ 5C23BC0C29A10BE000DBA990 /* portal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C23BC0B29A10BE000DBA990 /* portal.swift */; }; 5C23BC1029A3B5AE00DBA990 /* portal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C23BC0F29A3B5AE00DBA990 /* portal.swift */; }; 5C5647F82A3F6B100098FFE9 /* Telemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5647F72A3F6B100098FFE9 /* Telemetry.swift */; }; + 5C621D822B4770D6004ED7AF /* process.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C621D812B4770D6004ED7AF /* process.swift */; }; 5C8E001029269C7F0027C75A /* protocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE493829265055000F2856 /* protocol.swift */; }; 5CD342F42B2F2FB700225631 /* notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CD342F32B2F2FB700225631 /* notifications.swift */; }; 5CF2210D2B1E7EAF006C583F /* notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF2210C2B1E7EAF006C583F /* notifications.swift */; }; @@ -402,6 +403,7 @@ 5C23BC0B29A10BE000DBA990 /* portal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = portal.swift; sourceTree = ""; }; 5C23BC0F29A3B5AE00DBA990 /* portal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = portal.swift; sourceTree = ""; }; 5C5647F72A3F6B100098FFE9 /* Telemetry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Telemetry.swift; sourceTree = ""; }; + 5C621D812B4770D6004ED7AF /* process.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = process.swift; sourceTree = ""; }; 5C9F90A02A76B30500D41748 /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Localizable.strings; sourceTree = ""; }; 5CD342F32B2F2FB700225631 /* notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = notifications.swift; sourceTree = ""; }; 5CF2210C2B1E7EAF006C583F /* notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = notifications.swift; sourceTree = ""; }; @@ -788,6 +790,7 @@ 9A28481A2666AB3500EC1F6D /* extensions.swift */, 9A28481D2666AB3600EC1F6D /* helpers.swift */, 9A28481C2666AB3500EC1F6D /* types.swift */, + 5C621D812B4770D6004ED7AF /* process.swift */, ); path = Kit; sourceTree = ""; @@ -1746,6 +1749,7 @@ buildActionMask = 2147483647; files = ( 5C8E001029269C7F0027C75A /* protocol.swift in Sources */, + 5C621D822B4770D6004ED7AF /* process.swift in Sources */, 9AD7F866266F759200E5F863 /* smc.swift in Sources */, 9A2847612666AA2700EC1F6D /* PieChart.swift in Sources */, 9A2847672666AA2700EC1F6D /* BarChart.swift in Sources */,