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:
Serhiy Mytrovtsiy
2025-12-26 18:14:10 +01:00
parent ab7dffdf3a
commit 2bbd118516
4 changed files with 182 additions and 28 deletions

View File

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

View File

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

View File

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

View File

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