feat: initialized ProcessesView that will be common for all popups

This commit is contained in:
Serhiy Mytrovtsiy
2024-01-05 21:02:29 +01:00
parent c1d006ada6
commit b21630218c
10 changed files with 330 additions and 177 deletions

View File

@@ -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()

281
Kit/process.swift Normal file
View File

@@ -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..<n {
let view = ProcessView(n: values.count)
self.addArrangedSubview(view)
self.list.append(view)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func generateHeaderView(_ values: [ProcessHeader]) -> 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..<n {
let view: ValueField = ValueField()
view.widthAnchor.constraint(equalToConstant: 60).isActive = true
if n != 1 {
view.font = NSFont.systemFont(ofSize: 10, weight: .regular)
}
list.append(view)
}
self.valueViews = list
return list
}
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: Process_p, _ value: String) {
if self.lock && process.pid != self.pid { return }
self.labelView.stringValue = process.name
self.valueViews.first?.stringValue = value
self.imageView.image = process.icon
self.pid = process.pid
self.toolTip = "pid: \(process.pid)"
}
public func set(_ process: Process_p, _ values: [String]) {
if self.lock && process.pid != self.pid { return }
self.labelView.stringValue = process.name
values.enumerated().forEach({ self.valueViews[$0.offset].stringValue = $0.element })
self.imageView.image = process.icon
self.pid = process.pid
self.toolTip = "pid: \(process.pid)"
}
public func clear(_ symbol: String = "") {
self.labelView.stringValue = symbol
self.valueViews.forEach({ $0.stringValue = symbol })
self.imageView.image = nil
self.pid = nil
self.setLock(false)
self.toolTip = symbol
}
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)")
}
}
}

View File

@@ -199,12 +199,12 @@ public class ProcessReader: Reader<[TopProcess]> {
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))
}
}

View File

@@ -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 }

View File

@@ -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
}

View File

@@ -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))
}
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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))
}
}

View File

@@ -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 = "<group>"; };
5C23BC0F29A3B5AE00DBA990 /* portal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = portal.swift; sourceTree = "<group>"; };
5C5647F72A3F6B100098FFE9 /* Telemetry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Telemetry.swift; sourceTree = "<group>"; };
5C621D812B4770D6004ED7AF /* process.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = process.swift; sourceTree = "<group>"; };
5C9F90A02A76B30500D41748 /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Localizable.strings; sourceTree = "<group>"; };
5CD342F32B2F2FB700225631 /* notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = notifications.swift; sourceTree = "<group>"; };
5CF2210C2B1E7EAF006C583F /* notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = notifications.swift; sourceTree = "<group>"; };
@@ -788,6 +790,7 @@
9A28481A2666AB3500EC1F6D /* extensions.swift */,
9A28481D2666AB3600EC1F6D /* helpers.swift */,
9A28481C2666AB3500EC1F6D /* types.swift */,
5C621D812B4770D6004ED7AF /* process.swift */,
);
path = Kit;
sourceTree = "<group>";
@@ -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 */,