From 2bbd118516528d201f390abf19522061f5641661 Mon Sep 17 00:00:00 2001 From: Serhiy Mytrovtsiy Date: Fri, 26 Dec 2025 18:14:10 +0100 Subject: [PATCH] feat: small redesign in the Network popup view. Moved interface details to the dedicated section, added interface dns servers (#2789), added interface speed (#2702) --- Kit/helpers.swift | 18 +++-- Modules/Net/main.swift | 10 ++- Modules/Net/popup.swift | 136 +++++++++++++++++++++++++++++++++----- Modules/Net/readers.swift | 46 ++++++++++++- 4 files changed, 182 insertions(+), 28 deletions(-) diff --git a/Kit/helpers.swift b/Kit/helpers.swift index 7fe5695e..b4d5f592 100644 --- a/Kit/helpers.swift +++ b/Kit/helpers.swift @@ -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) } diff --git a/Modules/Net/main.swift b/Modules/Net/main.swift index 5f7dd3eb..6e43baae 100644 --- a/Modules/Net/main.swift +++ b/Modules/Net/main.swift @@ -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 ?? "-" diff --git a/Modules/Net/popup.swift b/Modules/Net/popup.swift index d764cca4..e2117fb6 100644 --- a/Modules/Net/popup.swift +++ b/Modules/Net/popup.swift @@ -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 diff --git a/Modules/Net/readers.swift b/Modules/Net/readers.swift index 47e7dabc..1187f480 100644 --- a/Modules/Net/readers.swift +++ b/Modules/Net/readers.swift @@ -234,6 +234,17 @@ internal class UsageReader: Reader, 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, 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 == "") { @@ -360,7 +383,6 @@ internal class UsageReader: Reader, 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, CWEventDelegate { } } - func ssidDidChangeForWiFiInterface(withName interfaceName: String) { + public func ssidDidChangeForWiFiInterface(withName interfaceName: String) { self.getWiFiDetails() } + + private func isInterfaceUp(_ ifName: String) -> Bool { + var addrs: UnsafeMutablePointer? = 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]> {