mirror of
https://github.com/morgan9e/macos-stats
synced 2026-04-13 15:54:10 +09:00
feat: small redesign in the Network popup view. Moved interface details to the dedicated section, added interface dns servers (#2789), added interface speed (#2702)
This commit is contained in:
@@ -339,12 +339,20 @@ public func separatorView(_ title: String, origin: NSPoint = NSPoint(x: 0, y: 0)
|
||||
return view
|
||||
}
|
||||
|
||||
public func popupRow(_ view: NSView, title: String, value: String) -> (LabelField, ValueField, NSView) {
|
||||
let rowView: NSView = NSView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 22))
|
||||
public func popupRow(_ view: NSView? = nil, title: String, value: String, multiline: Bool = false) -> (LabelField, ValueField, NSView) {
|
||||
let lines: CGFloat = CGFloat(multiline ? value.filter { $0 == "\n" }.count + 1 : 1)
|
||||
let width = view?.frame.width ?? 0
|
||||
let height = multiline ? ((lines*16) + (22-16)): 22
|
||||
|
||||
let rowView: NSView = NSView(frame: NSRect(x: 0, y: 0, width: width, height: height))
|
||||
|
||||
let labelWidth = title.widthOfString(usingFont: .systemFont(ofSize: 12, weight: .regular)) + 4
|
||||
let labelView: LabelField = LabelField(frame: NSRect(x: 0, y: (22-16)/2, width: labelWidth, height: 16), title)
|
||||
let valueView: ValueField = ValueField(frame: NSRect(x: labelWidth, y: (22-16)/2, width: rowView.frame.width - labelWidth, height: 16), value)
|
||||
let labelView: LabelField = LabelField(frame: NSRect(x: 0, y: ((22-16)/2) + ((lines-1)*16), width: labelWidth, height: 16), title)
|
||||
let valueView: ValueField = ValueField(frame: NSRect(x: labelWidth, y: (22-16)/2, width: rowView.frame.width - labelWidth, height: multiline ? 16*lines : 16), value)
|
||||
|
||||
if multiline {
|
||||
valueView.cell?.usesSingleLineMode = false
|
||||
}
|
||||
|
||||
rowView.addSubview(labelView)
|
||||
rowView.addSubview(valueView)
|
||||
@@ -352,7 +360,7 @@ public func popupRow(_ view: NSView, title: String, value: String) -> (LabelFiel
|
||||
if let view = view as? NSStackView {
|
||||
rowView.heightAnchor.constraint(equalToConstant: rowView.bounds.height).isActive = true
|
||||
view.addArrangedSubview(rowView)
|
||||
} else {
|
||||
} else if let view {
|
||||
view.addSubview(rowView)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,9 +22,11 @@ public enum Network_t: String, Codable {
|
||||
}
|
||||
|
||||
public struct Network_interface: Codable {
|
||||
var status: Bool = false
|
||||
var displayName: String = ""
|
||||
var BSDName: String = ""
|
||||
var address: String = ""
|
||||
var transmitRate: Double = 0
|
||||
}
|
||||
|
||||
public struct Network_addr: Codable {
|
||||
@@ -39,7 +41,6 @@ public struct Network_wifi: Codable {
|
||||
var bssid: String? = nil
|
||||
var RSSI: Int? = nil
|
||||
var noise: Int? = nil
|
||||
var transmitRate: Double? = nil
|
||||
|
||||
var standard: String? = nil
|
||||
var mode: String? = nil
|
||||
@@ -55,7 +56,6 @@ public struct Network_wifi: Codable {
|
||||
self.ssid = nil
|
||||
self.RSSI = nil
|
||||
self.noise = nil
|
||||
self.transmitRate = nil
|
||||
self.standard = nil
|
||||
self.mode = nil
|
||||
self.security = nil
|
||||
@@ -75,6 +75,8 @@ public struct Network_Usage: Codable, RemoteType {
|
||||
var laddr: Network_addr = Network_addr() // local ip
|
||||
var raddr: Network_addr = Network_addr() // remote ip
|
||||
|
||||
var dns: [String] = []
|
||||
|
||||
var interface: Network_interface? = nil
|
||||
var connectionType: Network_t? = nil
|
||||
var status: Bool = false
|
||||
@@ -87,6 +89,8 @@ public struct Network_Usage: Codable, RemoteType {
|
||||
self.laddr = Network_addr()
|
||||
self.raddr = Network_addr()
|
||||
|
||||
self.dns = []
|
||||
|
||||
self.interface = nil
|
||||
self.connectionType = nil
|
||||
|
||||
@@ -277,6 +281,7 @@ public class Network: Module {
|
||||
case "displayName": replacement = value.interface?.displayName ?? "-"
|
||||
case "BSDName": replacement = value.interface?.BSDName ?? "-"
|
||||
case "address": replacement = value.interface?.address ?? "-"
|
||||
case "transmitRate": replacement = "\(value.interface?.transmitRate ?? 0)"
|
||||
default: return
|
||||
}
|
||||
case "$wifi":
|
||||
@@ -285,7 +290,6 @@ public class Network: Module {
|
||||
case "bssid": replacement = value.wifiDetails.bssid ?? "-"
|
||||
case "RSSI": replacement = "\(value.wifiDetails.RSSI ?? 0)"
|
||||
case "noise": replacement = "\(value.wifiDetails.noise ?? 0)"
|
||||
case "transmitRate": replacement = "\(value.wifiDetails.transmitRate ?? 0)"
|
||||
case "standard": replacement = value.wifiDetails.standard ?? "-"
|
||||
case "mode": replacement = value.wifiDetails.mode ?? "-"
|
||||
case "security": replacement = value.wifiDetails.security ?? "-"
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import Cocoa
|
||||
import Kit
|
||||
|
||||
// swiftlint:disable:next type_body_length
|
||||
internal class Popup: PopupWrapper {
|
||||
private var uploadContainerView: NSView? = nil
|
||||
private var uploadView: NSView? = nil
|
||||
@@ -30,23 +31,30 @@ internal class Popup: PopupWrapper {
|
||||
private var downloadColorView: NSView? = nil
|
||||
private var uploadColorView: NSView? = nil
|
||||
|
||||
private var detailsView: NSStackView? = nil
|
||||
private var totalUploadLabel: LabelField? = nil
|
||||
private var totalUploadField: ValueField? = nil
|
||||
private var totalDownloadLabel: LabelField? = nil
|
||||
private var totalDownloadField: ValueField? = nil
|
||||
private var statusField: ValueField? = nil
|
||||
private var connectivityField: ValueField? = nil
|
||||
private var interfaceField: ValueField? = nil
|
||||
private var macAddressField: ValueField? = nil
|
||||
private var latencyField: ValueField? = nil
|
||||
|
||||
private var interfaceView: NSStackView? = nil
|
||||
private var interfaceField: ValueField? = nil
|
||||
private var interfaceStatusField: ValueField? = nil
|
||||
private var macAddressField: ValueField? = nil
|
||||
private var ssidField: ValueField? = nil
|
||||
private var standardField: ValueField? = nil
|
||||
private var channelField: ValueField? = nil
|
||||
private var ssidView: NSView? = nil
|
||||
|
||||
private var interfaceDetailsState: Bool = false
|
||||
private var standardView: NSView? = nil
|
||||
private var channelView: NSView? = nil
|
||||
private var interfaceSpeedView: NSView? = nil
|
||||
private var interfaceSpeedField: ValueField? = nil
|
||||
private var dnsServersView: NSView? = nil
|
||||
private var dnsServersField: ValueField? = nil
|
||||
|
||||
private var addressView: NSStackView? = nil
|
||||
private var localIPField: ValueField? = nil
|
||||
@@ -116,11 +124,13 @@ internal class Popup: PopupWrapper {
|
||||
self.chartFixedScale = Store.shared.int(key: "\(self.title)_chartFixedScale", defaultValue: self.chartFixedScale)
|
||||
self.chartFixedScaleSize = SizeUnit.fromString(Store.shared.string(key: "\(self.title)_chartFixedScaleSize", defaultValue: self.chartFixedScaleSize.key))
|
||||
self.publicIPState = Store.shared.bool(key: "\(self.title)_publicIP", defaultValue: self.publicIPState)
|
||||
self.interfaceDetailsState = Store.shared.bool(key: "\(self.title)_interfaceDetails", defaultValue: self.interfaceDetailsState)
|
||||
|
||||
self.addArrangedSubview(self.initDashboard())
|
||||
self.addArrangedSubview(self.initChart())
|
||||
self.addArrangedSubview(self.initConnectivityChart())
|
||||
self.addArrangedSubview(self.initDetails())
|
||||
self.addArrangedSubview(self.initInterface())
|
||||
self.addArrangedSubview(self.initAddress())
|
||||
self.addArrangedSubview(self.initProcesses())
|
||||
|
||||
@@ -264,22 +274,61 @@ internal class Popup: PopupWrapper {
|
||||
self.statusField = popupRow(view, title: "\(localizedString("Status")):", value: localizedString("Unknown")).1
|
||||
self.connectivityField = popupRow(view, title: "\(localizedString("Internet connection")):", value: localizedString("Unknown")).1
|
||||
self.latencyField = popupRow(view, title: "\(localizedString("Latency")):", value: "0 ms").1
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
private func initInterface() -> NSView {
|
||||
let view = NSStackView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 0))
|
||||
view.orientation = .vertical
|
||||
view.spacing = 0
|
||||
|
||||
let row: NSView = NSView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: Constants.Popup.separatorHeight))
|
||||
row.heightAnchor.constraint(equalToConstant: Constants.Popup.separatorHeight).isActive = true
|
||||
|
||||
let button = NSButtonWithPadding()
|
||||
button.frame = CGRect(x: view.frame.width - 18, y: 6, width: 18, height: 18)
|
||||
button.bezelStyle = .regularSquare
|
||||
button.isBordered = false
|
||||
button.imageScaling = NSImageScaling.scaleAxesIndependently
|
||||
button.contentTintColor = .lightGray
|
||||
button.action = #selector(self.toggleInterfaceDetails)
|
||||
button.target = self
|
||||
button.toolTip = localizedString("Details")
|
||||
button.image = Bundle(for: Module.self).image(forResource: "tune")!
|
||||
|
||||
row.addSubview(separatorView(localizedString("Interface"), width: self.frame.width))
|
||||
row.addSubview(button)
|
||||
|
||||
view.addArrangedSubview(row)
|
||||
|
||||
self.interfaceField = popupRow(view, title: "\(localizedString("Interface")):", value: localizedString("Unknown")).1
|
||||
self.interfaceStatusField = popupRow(view, title: "\(localizedString("Status")):", value: localizedString("Unknown")).1
|
||||
self.macAddressField = popupRow(view, title: "\(localizedString("Physical address")):", value: localizedString("Unknown")).1
|
||||
self.macAddressField?.isSelectable = true
|
||||
|
||||
let ssid = popupRow(view, title: "\(localizedString("Network")):", value: localizedString("Unknown"))
|
||||
let standard = popupRow(view, title: "\(localizedString("Standard")):", value: localizedString("Unknown"))
|
||||
let channel = popupRow(view, title: "\(localizedString("Channel")):", value: localizedString("Unknown"))
|
||||
let standard = popupRow(view, title: "\(localizedString("Standard")):", value: localizedString("Unavailable"))
|
||||
let channel = popupRow(view, title: "\(localizedString("Channel")):", value: localizedString("Unavailable"))
|
||||
let speed = popupRow(view, title: "\(localizedString("Speed")):", value: localizedString("Unknown"))
|
||||
|
||||
self.ssidField = ssid.1
|
||||
self.standardField = standard.1
|
||||
self.channelField = channel.1
|
||||
self.interfaceSpeedField = speed.1
|
||||
|
||||
self.ssidView = ssid.2
|
||||
self.standardView = standard.2
|
||||
self.channelView = channel.2
|
||||
self.interfaceSpeedView = speed.2
|
||||
|
||||
self.detailsView = view
|
||||
if !self.interfaceDetailsState {
|
||||
self.standardView?.removeFromSuperview()
|
||||
self.channelView?.removeFromSuperview()
|
||||
self.interfaceSpeedView?.removeFromSuperview()
|
||||
}
|
||||
|
||||
self.interfaceView = view
|
||||
return view
|
||||
}
|
||||
|
||||
@@ -393,23 +442,27 @@ internal class Popup: PopupWrapper {
|
||||
self.interfaceField?.stringValue += ", \(cc)"
|
||||
}
|
||||
self.interfaceField?.stringValue += ")"
|
||||
self.interfaceStatusField?.stringValue = localizedString(interface.status ? "UP" : "DOWN")
|
||||
self.macAddressField?.stringValue = interface.address
|
||||
self.interfaceSpeedField?.stringValue = "\(Int(interface.transmitRate.rounded()))baseT"
|
||||
} else {
|
||||
self.interfaceField?.stringValue = localizedString("Unknown")
|
||||
self.interfaceStatusField?.stringValue = localizedString("Unknown")
|
||||
self.macAddressField?.stringValue = localizedString("Unknown")
|
||||
self.interfaceSpeedField?.stringValue = localizedString("Unknown")
|
||||
}
|
||||
|
||||
if value.connectionType == .wifi {
|
||||
if let view = self.ssidView, view.superview == nil && value.wifiDetails.ssid != nil {
|
||||
self.detailsView?.addArrangedSubview(view)
|
||||
self.interfaceView?.addArrangedSubview(view)
|
||||
resized = true
|
||||
}
|
||||
if let view = self.standardView, view.superview == nil && value.wifiDetails.standard != nil {
|
||||
self.detailsView?.addArrangedSubview(view)
|
||||
if self.interfaceDetailsState, let view = self.standardView, view.superview == nil && value.wifiDetails.standard != nil {
|
||||
self.interfaceView?.addArrangedSubview(view)
|
||||
resized = true
|
||||
}
|
||||
if let view = self.channelView, view.superview == nil && value.wifiDetails.channel != nil {
|
||||
self.detailsView?.addArrangedSubview(view)
|
||||
if self.interfaceDetailsState, let view = self.channelView, view.superview == nil && value.wifiDetails.channel != nil {
|
||||
self.interfaceView?.addArrangedSubview(view)
|
||||
resized = true
|
||||
}
|
||||
|
||||
@@ -428,15 +481,11 @@ internal class Popup: PopupWrapper {
|
||||
if let v = value.wifiDetails.noise {
|
||||
noise = "\(v) dBm"
|
||||
}
|
||||
var txRate = localizedString("Unknown")
|
||||
if let v = value.wifiDetails.transmitRate {
|
||||
txRate = "\(v) Mbps"
|
||||
}
|
||||
|
||||
let number = value.wifiDetails.channelNumber ?? localizedString("Unknown")
|
||||
let band = value.wifiDetails.channelBand ?? localizedString("Unknown")
|
||||
let width = value.wifiDetails.channelWidth ?? localizedString("Unknown")
|
||||
self.channelField?.toolTip = "RSSI: \(rssi)\nNoise: \(noise)\nChannel number: \(number)\nChannel band: \(band)\nChannel width: \(width)\nTransmit rate: \(txRate)"
|
||||
self.channelField?.toolTip = "RSSI: \(rssi)\nNoise: \(noise)\nChannel number: \(number)\nChannel band: \(band)\nChannel width: \(width)\n"
|
||||
} else {
|
||||
if self.ssidView?.superview != nil {
|
||||
self.ssidField?.stringValue = localizedString("Unavailable")
|
||||
@@ -469,7 +518,7 @@ internal class Popup: PopupWrapper {
|
||||
if let addr = value.raddr.v4 {
|
||||
if view.superview == nil {
|
||||
self.addressView?.addArrangedSubview(view)
|
||||
self.recalculateHeight()
|
||||
resized = true
|
||||
}
|
||||
var ip = addr
|
||||
if let cc = value.raddr.countryCode, !cc.isEmpty {
|
||||
@@ -480,7 +529,7 @@ internal class Popup: PopupWrapper {
|
||||
}
|
||||
} else if view.superview != nil {
|
||||
view.removeFromSuperview()
|
||||
self.recalculateHeight()
|
||||
resized = true
|
||||
self.publicIPv4Field?.stringValue = localizedString("Unknown")
|
||||
}
|
||||
}
|
||||
@@ -505,6 +554,31 @@ internal class Popup: PopupWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
if self.interfaceDetailsState {
|
||||
if !value.dns.isEmpty {
|
||||
let servers = value.dns.joined(separator: "\n")
|
||||
|
||||
if self.dnsServersField == nil || value.dns.count != self.dnsServersField?.stringValue.split(separator: "\n").count {
|
||||
if let view = self.dnsServersView {
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
let view = popupRow(self.interfaceView, title: "\(localizedString("DNS Server")):", value: servers, multiline: true)
|
||||
self.dnsServersField = view.1
|
||||
self.dnsServersView = view.2
|
||||
self.dnsServersField?.isSelectable = true
|
||||
}
|
||||
|
||||
if self.dnsServersField?.stringValue != servers {
|
||||
self.dnsServersField?.stringValue = servers
|
||||
}
|
||||
|
||||
resized = true
|
||||
} else if let view = self.dnsServersView {
|
||||
view.removeFromSuperview()
|
||||
resized = true
|
||||
}
|
||||
}
|
||||
|
||||
self.statusField?.stringValue = localizedString(value.status ? "UP" : "DOWN")
|
||||
|
||||
if resized {
|
||||
@@ -706,6 +780,32 @@ internal class Popup: PopupWrapper {
|
||||
Store.shared.set(key: "\(self.title)_chartFixedScaleSize", value: self.chartFixedScaleSize.key)
|
||||
self.display()
|
||||
}
|
||||
@objc private func toggleInterfaceDetails() {
|
||||
self.interfaceDetailsState = !self.interfaceDetailsState
|
||||
Store.shared.set(key: "\(self.title)_interfaceDetails", value: self.interfaceDetailsState)
|
||||
|
||||
if !self.interfaceDetailsState {
|
||||
self.standardView?.removeFromSuperview()
|
||||
self.channelView?.removeFromSuperview()
|
||||
self.interfaceSpeedView?.removeFromSuperview()
|
||||
self.dnsServersView?.removeFromSuperview()
|
||||
} else {
|
||||
if let view = self.standardView, view.superview == nil && self.standardField?.stringValue != localizedString("Unavailable") {
|
||||
self.interfaceView?.addArrangedSubview(view)
|
||||
}
|
||||
if let view = self.channelView, view.superview == nil && self.channelField?.stringValue != localizedString("Unavailable") {
|
||||
self.interfaceView?.addArrangedSubview(view)
|
||||
}
|
||||
if let view = self.interfaceSpeedView, view.superview == nil {
|
||||
self.interfaceView?.addArrangedSubview(view)
|
||||
}
|
||||
if let view = self.dnsServersView, view.superview == nil {
|
||||
self.interfaceView?.addArrangedSubview(view)
|
||||
}
|
||||
}
|
||||
|
||||
self.recalculateHeight()
|
||||
}
|
||||
|
||||
// MARK: - helpers
|
||||
|
||||
|
||||
@@ -234,6 +234,17 @@ internal class UsageReader: Reader<Network_Usage>, CWEventDelegate {
|
||||
if String(cString: pointer.pointee.ifa_name) != self.interfaceID {
|
||||
continue
|
||||
}
|
||||
self.usage.interface?.status = (pointer.pointee.ifa_flags & UInt32(IFF_UP)) != 0
|
||||
|
||||
if let raw = pointer.pointee.ifa_data {
|
||||
let dataPtr = raw.assumingMemoryBound(to: if_data.self)
|
||||
let ifData = dataPtr.pointee
|
||||
let baud = UInt64(ifData.ifi_baudrate)
|
||||
if baud > 0 {
|
||||
self.usage.interface?.transmitRate = Double(baud) / 1_000_000.0
|
||||
}
|
||||
}
|
||||
|
||||
self.getLocalIP(pointer)
|
||||
|
||||
if let info = self.getBytesInfo(pointer) {
|
||||
@@ -330,6 +341,18 @@ internal class UsageReader: Reader<Network_Usage>, CWEventDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
if let prefs = SCPreferencesCreate(nil, "Stats" as CFString, nil), let services = SCNetworkServiceCopyAll(prefs) as? [SCNetworkService] {
|
||||
for service in services {
|
||||
if let interface = SCNetworkServiceGetInterface(service), let name = SCNetworkInterfaceGetBSDName(interface), name as String == self.interfaceID,
|
||||
let serviceID = SCNetworkServiceGetServiceID(service) {
|
||||
let key = "State:/Network/Service/\(serviceID)/DNS" as CFString
|
||||
if let settings = SCDynamicStoreCopyValue(nil, key) as? [String: Any] {
|
||||
self.usage.dns = settings["ServerAddresses"] as? [String] ?? []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
guard self.usage.interface != nil else { return }
|
||||
|
||||
if self.usage.wifiDetails.ssid != nil && (self.usage.wifiDetails.ssid == "" || self.usage.wifiDetails.ssid == "<redacted>") {
|
||||
@@ -360,7 +383,6 @@ internal class UsageReader: Reader<Network_Usage>, CWEventDelegate {
|
||||
|
||||
self.usage.wifiDetails.RSSI = interface.rssiValue()
|
||||
self.usage.wifiDetails.noise = interface.noiseMeasurement()
|
||||
self.usage.wifiDetails.transmitRate = interface.transmitRate()
|
||||
|
||||
self.usage.wifiDetails.standard = interface.activePHYMode().description
|
||||
self.usage.wifiDetails.mode = interface.interfaceMode().description
|
||||
@@ -505,9 +527,29 @@ internal class UsageReader: Reader<Network_Usage>, CWEventDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func ssidDidChangeForWiFiInterface(withName interfaceName: String) {
|
||||
public func ssidDidChangeForWiFiInterface(withName interfaceName: String) {
|
||||
self.getWiFiDetails()
|
||||
}
|
||||
|
||||
private func isInterfaceUp(_ ifName: String) -> Bool {
|
||||
var addrs: UnsafeMutablePointer<ifaddrs>? = nil
|
||||
guard getifaddrs(&addrs) == 0, let first = addrs else { return false }
|
||||
defer { freeifaddrs(addrs) }
|
||||
|
||||
var ptr = first
|
||||
while true {
|
||||
let name = String(cString: ptr.pointee.ifa_name)
|
||||
if name == ifName {
|
||||
return (ptr.pointee.ifa_flags & UInt32(IFF_UP)) != 0
|
||||
}
|
||||
if let next = ptr.pointee.ifa_next {
|
||||
ptr = next
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public class ProcessReader: Reader<[Network_Process]> {
|
||||
|
||||
Reference in New Issue
Block a user