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 return view
} }
public func popupRow(_ view: NSView, title: String, value: String) -> (LabelField, ValueField, NSView) { public func popupRow(_ view: NSView? = nil, title: String, value: String, multiline: Bool = false) -> (LabelField, ValueField, NSView) {
let rowView: NSView = NSView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 22)) 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 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 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: 16), value) 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(labelView)
rowView.addSubview(valueView) rowView.addSubview(valueView)
@@ -352,7 +360,7 @@ public func popupRow(_ view: NSView, title: String, value: String) -> (LabelFiel
if let view = view as? NSStackView { if let view = view as? NSStackView {
rowView.heightAnchor.constraint(equalToConstant: rowView.bounds.height).isActive = true rowView.heightAnchor.constraint(equalToConstant: rowView.bounds.height).isActive = true
view.addArrangedSubview(rowView) view.addArrangedSubview(rowView)
} else { } else if let view {
view.addSubview(rowView) view.addSubview(rowView)
} }

View File

@@ -22,9 +22,11 @@ public enum Network_t: String, Codable {
} }
public struct Network_interface: Codable { public struct Network_interface: Codable {
var status: Bool = false
var displayName: String = "" var displayName: String = ""
var BSDName: String = "" var BSDName: String = ""
var address: String = "" var address: String = ""
var transmitRate: Double = 0
} }
public struct Network_addr: Codable { public struct Network_addr: Codable {
@@ -39,7 +41,6 @@ public struct Network_wifi: Codable {
var bssid: String? = nil var bssid: String? = nil
var RSSI: Int? = nil var RSSI: Int? = nil
var noise: Int? = nil var noise: Int? = nil
var transmitRate: Double? = nil
var standard: String? = nil var standard: String? = nil
var mode: String? = nil var mode: String? = nil
@@ -55,7 +56,6 @@ public struct Network_wifi: Codable {
self.ssid = nil self.ssid = nil
self.RSSI = nil self.RSSI = nil
self.noise = nil self.noise = nil
self.transmitRate = nil
self.standard = nil self.standard = nil
self.mode = nil self.mode = nil
self.security = nil self.security = nil
@@ -75,6 +75,8 @@ public struct Network_Usage: Codable, RemoteType {
var laddr: Network_addr = Network_addr() // local ip var laddr: Network_addr = Network_addr() // local ip
var raddr: Network_addr = Network_addr() // remote ip var raddr: Network_addr = Network_addr() // remote ip
var dns: [String] = []
var interface: Network_interface? = nil var interface: Network_interface? = nil
var connectionType: Network_t? = nil var connectionType: Network_t? = nil
var status: Bool = false var status: Bool = false
@@ -87,6 +89,8 @@ public struct Network_Usage: Codable, RemoteType {
self.laddr = Network_addr() self.laddr = Network_addr()
self.raddr = Network_addr() self.raddr = Network_addr()
self.dns = []
self.interface = nil self.interface = nil
self.connectionType = nil self.connectionType = nil
@@ -277,6 +281,7 @@ public class Network: Module {
case "displayName": replacement = value.interface?.displayName ?? "-" case "displayName": replacement = value.interface?.displayName ?? "-"
case "BSDName": replacement = value.interface?.BSDName ?? "-" case "BSDName": replacement = value.interface?.BSDName ?? "-"
case "address": replacement = value.interface?.address ?? "-" case "address": replacement = value.interface?.address ?? "-"
case "transmitRate": replacement = "\(value.interface?.transmitRate ?? 0)"
default: return default: return
} }
case "$wifi": case "$wifi":
@@ -285,7 +290,6 @@ public class Network: Module {
case "bssid": replacement = value.wifiDetails.bssid ?? "-" case "bssid": replacement = value.wifiDetails.bssid ?? "-"
case "RSSI": replacement = "\(value.wifiDetails.RSSI ?? 0)" case "RSSI": replacement = "\(value.wifiDetails.RSSI ?? 0)"
case "noise": replacement = "\(value.wifiDetails.noise ?? 0)" case "noise": replacement = "\(value.wifiDetails.noise ?? 0)"
case "transmitRate": replacement = "\(value.wifiDetails.transmitRate ?? 0)"
case "standard": replacement = value.wifiDetails.standard ?? "-" case "standard": replacement = value.wifiDetails.standard ?? "-"
case "mode": replacement = value.wifiDetails.mode ?? "-" case "mode": replacement = value.wifiDetails.mode ?? "-"
case "security": replacement = value.wifiDetails.security ?? "-" case "security": replacement = value.wifiDetails.security ?? "-"

View File

@@ -12,6 +12,7 @@
import Cocoa import Cocoa
import Kit import Kit
// swiftlint:disable:next type_body_length
internal class Popup: PopupWrapper { internal class Popup: PopupWrapper {
private var uploadContainerView: NSView? = nil private var uploadContainerView: NSView? = nil
private var uploadView: NSView? = nil private var uploadView: NSView? = nil
@@ -30,23 +31,30 @@ internal class Popup: PopupWrapper {
private var downloadColorView: NSView? = nil private var downloadColorView: NSView? = nil
private var uploadColorView: NSView? = nil private var uploadColorView: NSView? = nil
private var detailsView: NSStackView? = nil
private var totalUploadLabel: LabelField? = nil private var totalUploadLabel: LabelField? = nil
private var totalUploadField: ValueField? = nil private var totalUploadField: ValueField? = nil
private var totalDownloadLabel: LabelField? = nil private var totalDownloadLabel: LabelField? = nil
private var totalDownloadField: ValueField? = nil private var totalDownloadField: ValueField? = nil
private var statusField: ValueField? = nil private var statusField: ValueField? = nil
private var connectivityField: ValueField? = nil private var connectivityField: ValueField? = nil
private var interfaceField: ValueField? = nil
private var macAddressField: ValueField? = nil
private var latencyField: 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 ssidField: ValueField? = nil
private var standardField: ValueField? = nil private var standardField: ValueField? = nil
private var channelField: ValueField? = nil private var channelField: ValueField? = nil
private var ssidView: NSView? = nil private var ssidView: NSView? = nil
private var interfaceDetailsState: Bool = false
private var standardView: NSView? = nil private var standardView: NSView? = nil
private var channelView: 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 addressView: NSStackView? = nil
private var localIPField: ValueField? = 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.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.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.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.initDashboard())
self.addArrangedSubview(self.initChart()) self.addArrangedSubview(self.initChart())
self.addArrangedSubview(self.initConnectivityChart()) self.addArrangedSubview(self.initConnectivityChart())
self.addArrangedSubview(self.initDetails()) self.addArrangedSubview(self.initDetails())
self.addArrangedSubview(self.initInterface())
self.addArrangedSubview(self.initAddress()) self.addArrangedSubview(self.initAddress())
self.addArrangedSubview(self.initProcesses()) self.addArrangedSubview(self.initProcesses())
@@ -264,22 +274,61 @@ internal class Popup: PopupWrapper {
self.statusField = popupRow(view, title: "\(localizedString("Status")):", value: localizedString("Unknown")).1 self.statusField = popupRow(view, title: "\(localizedString("Status")):", value: localizedString("Unknown")).1
self.connectivityField = popupRow(view, title: "\(localizedString("Internet connection")):", 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 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.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 = popupRow(view, title: "\(localizedString("Physical address")):", value: localizedString("Unknown")).1
self.macAddressField?.isSelectable = true self.macAddressField?.isSelectable = true
let ssid = popupRow(view, title: "\(localizedString("Network")):", value: localizedString("Unknown")) let ssid = popupRow(view, title: "\(localizedString("Network")):", value: localizedString("Unknown"))
let standard = popupRow(view, title: "\(localizedString("Standard")):", value: localizedString("Unknown")) let standard = popupRow(view, title: "\(localizedString("Standard")):", value: localizedString("Unavailable"))
let channel = popupRow(view, title: "\(localizedString("Channel")):", value: localizedString("Unknown")) 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.ssidField = ssid.1
self.standardField = standard.1 self.standardField = standard.1
self.channelField = channel.1 self.channelField = channel.1
self.interfaceSpeedField = speed.1
self.ssidView = ssid.2 self.ssidView = ssid.2
self.standardView = standard.2 self.standardView = standard.2
self.channelView = channel.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 return view
} }
@@ -393,23 +442,27 @@ internal class Popup: PopupWrapper {
self.interfaceField?.stringValue += ", \(cc)" self.interfaceField?.stringValue += ", \(cc)"
} }
self.interfaceField?.stringValue += ")" self.interfaceField?.stringValue += ")"
self.interfaceStatusField?.stringValue = localizedString(interface.status ? "UP" : "DOWN")
self.macAddressField?.stringValue = interface.address self.macAddressField?.stringValue = interface.address
self.interfaceSpeedField?.stringValue = "\(Int(interface.transmitRate.rounded()))baseT"
} else { } else {
self.interfaceField?.stringValue = localizedString("Unknown") self.interfaceField?.stringValue = localizedString("Unknown")
self.interfaceStatusField?.stringValue = localizedString("Unknown")
self.macAddressField?.stringValue = localizedString("Unknown") self.macAddressField?.stringValue = localizedString("Unknown")
self.interfaceSpeedField?.stringValue = localizedString("Unknown")
} }
if value.connectionType == .wifi { if value.connectionType == .wifi {
if let view = self.ssidView, view.superview == nil && value.wifiDetails.ssid != nil { if let view = self.ssidView, view.superview == nil && value.wifiDetails.ssid != nil {
self.detailsView?.addArrangedSubview(view) self.interfaceView?.addArrangedSubview(view)
resized = true resized = true
} }
if let view = self.standardView, view.superview == nil && value.wifiDetails.standard != nil { if self.interfaceDetailsState, let view = self.standardView, view.superview == nil && value.wifiDetails.standard != nil {
self.detailsView?.addArrangedSubview(view) self.interfaceView?.addArrangedSubview(view)
resized = true resized = true
} }
if let view = self.channelView, view.superview == nil && value.wifiDetails.channel != nil { if self.interfaceDetailsState, let view = self.channelView, view.superview == nil && value.wifiDetails.channel != nil {
self.detailsView?.addArrangedSubview(view) self.interfaceView?.addArrangedSubview(view)
resized = true resized = true
} }
@@ -428,15 +481,11 @@ internal class Popup: PopupWrapper {
if let v = value.wifiDetails.noise { if let v = value.wifiDetails.noise {
noise = "\(v) dBm" noise = "\(v) dBm"
} }
var txRate = localizedString("Unknown")
if let v = value.wifiDetails.transmitRate {
txRate = "\(v) Mbps"
}
let number = value.wifiDetails.channelNumber ?? localizedString("Unknown") let number = value.wifiDetails.channelNumber ?? localizedString("Unknown")
let band = value.wifiDetails.channelBand ?? localizedString("Unknown") let band = value.wifiDetails.channelBand ?? localizedString("Unknown")
let width = value.wifiDetails.channelWidth ?? 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 { } else {
if self.ssidView?.superview != nil { if self.ssidView?.superview != nil {
self.ssidField?.stringValue = localizedString("Unavailable") self.ssidField?.stringValue = localizedString("Unavailable")
@@ -469,7 +518,7 @@ internal class Popup: PopupWrapper {
if let addr = value.raddr.v4 { if let addr = value.raddr.v4 {
if view.superview == nil { if view.superview == nil {
self.addressView?.addArrangedSubview(view) self.addressView?.addArrangedSubview(view)
self.recalculateHeight() resized = true
} }
var ip = addr var ip = addr
if let cc = value.raddr.countryCode, !cc.isEmpty { if let cc = value.raddr.countryCode, !cc.isEmpty {
@@ -480,7 +529,7 @@ internal class Popup: PopupWrapper {
} }
} else if view.superview != nil { } else if view.superview != nil {
view.removeFromSuperview() view.removeFromSuperview()
self.recalculateHeight() resized = true
self.publicIPv4Field?.stringValue = localizedString("Unknown") 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") self.statusField?.stringValue = localizedString(value.status ? "UP" : "DOWN")
if resized { if resized {
@@ -706,6 +780,32 @@ internal class Popup: PopupWrapper {
Store.shared.set(key: "\(self.title)_chartFixedScaleSize", value: self.chartFixedScaleSize.key) Store.shared.set(key: "\(self.title)_chartFixedScaleSize", value: self.chartFixedScaleSize.key)
self.display() 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 // MARK: - helpers

View File

@@ -234,6 +234,17 @@ internal class UsageReader: Reader<Network_Usage>, CWEventDelegate {
if String(cString: pointer.pointee.ifa_name) != self.interfaceID { if String(cString: pointer.pointee.ifa_name) != self.interfaceID {
continue 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) self.getLocalIP(pointer)
if let info = self.getBytesInfo(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 } guard self.usage.interface != nil else { return }
if self.usage.wifiDetails.ssid != nil && (self.usage.wifiDetails.ssid == "" || self.usage.wifiDetails.ssid == "<redacted>") { 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.RSSI = interface.rssiValue()
self.usage.wifiDetails.noise = interface.noiseMeasurement() self.usage.wifiDetails.noise = interface.noiseMeasurement()
self.usage.wifiDetails.transmitRate = interface.transmitRate()
self.usage.wifiDetails.standard = interface.activePHYMode().description self.usage.wifiDetails.standard = interface.activePHYMode().description
self.usage.wifiDetails.mode = interface.interfaceMode().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() 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]> { public class ProcessReader: Reader<[Network_Process]> {