feat: show both public IPs: v4 and v6 (#307)

This commit is contained in:
Serhiy Mytrovtsiy
2021-03-03 18:38:22 +01:00
parent 2aebc91fb4
commit 7e08fb901a
4 changed files with 216 additions and 174 deletions

View File

@@ -27,12 +27,17 @@ public struct Network_interface {
var address: String = ""
}
public struct Network_addr {
var v4: String? = nil
var v6: String? = nil
}
public struct Network_Usage: value_t {
var bandwidth: Bandwidth = (0, 0)
var total: Bandwidth = (0, 0)
var laddr: String? = nil // local ip
var raddr: String? = nil // remote ip
var raddr: Network_addr = Network_addr() // remote ip
var interface: Network_interface? = nil
var connectionType: Network_t? = nil
@@ -44,7 +49,7 @@ public struct Network_Usage: value_t {
self.bandwidth = (0, 0)
self.laddr = nil
self.raddr = nil
self.raddr = Network_addr()
self.interface = nil
self.connectionType = nil

View File

@@ -13,19 +13,12 @@ import Cocoa
import ModuleKit
import StatsKit
internal class Popup: NSView, Popup_p {
internal class Popup: NSStackView, Popup_p {
public var sizeCallback: ((NSSize) -> Void)? = nil
private var store: UnsafePointer<Store>
private var title: String
private var grid: NSGridView? = nil
private let dashboardHeight: CGFloat = 90
private let chartHeight: CGFloat = 90 + Constants.Popup.separatorHeight
private let detailsHeight: CGFloat = (22*7) + Constants.Popup.separatorHeight
private let processHeight: CGFloat = 22
private var dashboardView: NSView? = nil
private var uploadView: NSView? = nil
private var uploadValue: Int64 = 0
private var uploadValueField: NSTextField? = nil
@@ -38,7 +31,6 @@ internal class Popup: NSView, Popup_p {
private var downloadUnitField: NSTextField? = nil
private var downloadStateView: ColorView? = nil
private var publicIPField: ValueField? = nil
private var localIPField: ValueField? = nil
private var interfaceField: ValueField? = nil
private var ssidField: ValueField? = nil
@@ -46,6 +38,12 @@ internal class Popup: NSView, Popup_p {
private var totalUploadField: ValueField? = nil
private var totalDownloadField: ValueField? = nil
private var publicIPStackView: NSStackView? = nil
private var publicIPv4Field: ValueField? = nil
private var publicIPv6Field: ValueField? = nil
private var processesView: NSView? = nil
private var initialized: Bool = false
private var processesInitialized: Bool = false
@@ -65,110 +63,90 @@ internal class Popup: NSView, Popup_p {
private var processesHeight: CGFloat {
get {
let num = self.numberOfProcesses
return (self.processHeight*CGFloat(num)) + (num == 0 ? 0 : Constants.Popup.separatorHeight)
return (22*CGFloat(num)) + (num == 0 ? 0 : Constants.Popup.separatorHeight)
}
}
public var sizeCallback: ((NSSize) -> Void)? = nil
public init(_ title: String, store: UnsafePointer<Store>) {
self.store = store
self.title = title
self.store = store
super.init(frame: NSRect(
x: 0,
y: 0,
width: Constants.Popup.width,
height: self.dashboardHeight + self.chartHeight + self.detailsHeight
height: 0
))
self.setFrameSize(NSSize(width: self.frame.width, height: self.frame.height+self.processesHeight))
let gridView: NSGridView = NSGridView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height))
gridView.rowSpacing = 0
gridView.yPlacement = .fill
self.spacing = 0
self.orientation = .vertical
gridView.addRow(with: [self.initDashboard()])
gridView.addRow(with: [self.initChart()])
gridView.addRow(with: [self.initDetails()])
gridView.addRow(with: [self.initProcesses()])
self.addArrangedSubview(self.initDashboard())
self.addArrangedSubview(self.initChart())
self.addArrangedSubview(self.initDetails())
self.addArrangedSubview(self.initPublicIP())
self.addArrangedSubview(self.initProcesses())
gridView.row(at: 0).height = self.dashboardHeight
gridView.row(at: 1).height = self.chartHeight
gridView.row(at: 2).height = self.detailsHeight
self.addSubview(gridView)
self.grid = gridView
self.recalculateHeight()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func numberOfProcessesUpdated() {
if self.processes.count == self.numberOfProcesses {
return
}
DispatchQueue.main.async(execute: {
self.processes = []
let h: CGFloat = self.dashboardHeight + self.chartHeight + self.detailsHeight + self.processesHeight
private func recalculateHeight() {
let h = self.arrangedSubviews.map({ $0.bounds.height }).reduce(0, +)
if self.frame.size.height != h {
self.setFrameSize(NSSize(width: self.frame.width, height: h))
self.grid?.setFrameSize(NSSize(width: self.frame.width, height: h))
self.grid?.row(at: 3).cell(at: 0).contentView?.removeFromSuperview()
self.grid?.removeRow(at: 3)
self.grid?.addRow(with: [self.initProcesses()])
self.processesInitialized = false
self.sizeCallback?(self.frame.size)
})
}
}
// MARK: - views
private func initDashboard() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: self.frame.height - self.dashboardHeight, width: self.frame.width, height: self.dashboardHeight))
let container: NSView = NSView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: self.dashboardHeight))
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 90))
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
let leftPart: NSView = NSView(frame: NSRect(x: 0, y: 0, width: container.frame.width / 2, height: container.frame.height))
let leftPart: NSView = NSView(frame: NSRect(x: 0, y: 0, width: view.frame.width / 2, height: view.frame.height))
let uploadFields = self.topValueView(leftPart, title: LocalizedString("Uploading"), color: NSColor.systemRed)
self.uploadView = uploadFields.0
self.uploadValueField = uploadFields.1
self.uploadUnitField = uploadFields.2
self.uploadStateView = uploadFields.3
let rightPart: NSView = NSView(frame: NSRect(x: container.frame.width / 2, y: 0, width: container.frame.width / 2, height: container.frame.height))
let rightPart: NSView = NSView(frame: NSRect(x: view.frame.width / 2, y: 0, width: view.frame.width / 2, height: view.frame.height))
let downloadFields = self.topValueView(rightPart, title: LocalizedString("Downloading"), color: NSColor.systemBlue)
self.downloadView = downloadFields.0
self.downloadValueField = downloadFields.1
self.downloadUnitField = downloadFields.2
self.downloadStateView = downloadFields.3
container.addSubview(leftPart)
container.addSubview(rightPart)
view.addSubview(container)
self.dashboardView = container
view.addSubview(leftPart)
view.addSubview(rightPart)
return view
}
private func initChart() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.chartHeight))
let separator = SeparatorView(LocalizedString("Usage history"), origin: NSPoint(x: 0, y: self.chartHeight-Constants.Popup.separatorHeight), width: self.frame.width)
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 90 + Constants.Popup.separatorHeight))
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
let separator = SeparatorView(LocalizedString("Usage history"), origin: NSPoint(x: 0, y: 90), width: self.frame.width)
let container: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: separator.frame.origin.y))
container.wantsLayer = true
container.layer?.backgroundColor = NSColor.lightGray.withAlphaComponent(0.1).cgColor
container.layer?.cornerRadius = 3
self.chart = NetworkChartView(frame: NSRect(
let chart = NetworkChartView(frame: NSRect(
x: 0,
y: 1,
width: container.frame.width,
height: container.frame.height - 2
), num: 120)
self.chart?.base = self.base
container.addSubview(self.chart!)
chart.base = self.base
container.addSubview(chart)
self.chart = chart
view.addSubview(separator)
view.addSubview(container)
@@ -177,22 +155,23 @@ internal class Popup: NSView, Popup_p {
}
private func initDetails() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.detailsHeight))
let separator = SeparatorView(LocalizedString("Details"), origin: NSPoint(x: 0, y: self.detailsHeight-Constants.Popup.separatorHeight), width: self.frame.width)
let height: CGFloat = 22*6
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: height + Constants.Popup.separatorHeight))
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
let separator = SeparatorView(LocalizedString("Details"), origin: NSPoint(x: 0, y: height), width: self.frame.width)
let container: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: separator.frame.origin.y))
self.totalUploadField = PopupWithColorRow(container, color: NSColor.systemRed, n: 6, title: "\(LocalizedString("Total upload")):", value: "")
self.totalDownloadField = PopupWithColorRow(container, color: NSColor.systemBlue, n: 5, title: "\(LocalizedString("Total download")):", value: "")
self.totalUploadField = PopupWithColorRow(container, color: NSColor.systemRed, n: 5, title: "\(LocalizedString("Total upload")):", value: "0")
self.totalDownloadField = PopupWithColorRow(container, color: NSColor.systemBlue, n: 4, title: "\(LocalizedString("Total download")):", value: "0")
self.publicIPField = PopupRow(container, n: 4, title: "\(LocalizedString("Public IP")):", value: "").1
self.localIPField = PopupRow(container, n: 3, title: "\(LocalizedString("Local IP")):", value: "").1
self.interfaceField = PopupRow(container, n: 2, title: "\(LocalizedString("Interface")):", value: "").1
self.ssidField = PopupRow(container, n: 1, title: "\(LocalizedString("Network")):", value: "").1
self.macAdressField = PopupRow(container, n: 0, title: "\(LocalizedString("Physical address")):", value: "").1
self.interfaceField = PopupRow(container, n: 3, title: "\(LocalizedString("Interface")):", value: LocalizedString("Unknown")).1
self.ssidField = PopupRow(container, n: 2, title: "\(LocalizedString("Network")):", value: LocalizedString("Unknown")).1
self.macAdressField = PopupRow(container, n: 1, title: "\(LocalizedString("Physical address")):", value: LocalizedString("Unknown")).1
self.localIPField = PopupRow(container, n: 0, title: "\(LocalizedString("Local IP")):", value: LocalizedString("Unknown")).1
self.publicIPField?.toolTip = LocalizedString("Click to copy public IP address")
self.localIPField?.toolTip = LocalizedString("Click to copy local IP address")
self.macAdressField?.toolTip = LocalizedString("Click to copy mac address")
self.localIPField?.isSelectable = true
self.macAdressField?.isSelectable = true
view.addSubview(separator)
view.addSubview(container)
@@ -200,6 +179,36 @@ internal class Popup: NSView, Popup_p {
return view
}
private func initPublicIP() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: 0))
let container: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 0))
container.orientation = .vertical
container.spacing = 0
container.addArrangedSubview(SeparatorView(LocalizedString("Public IP"), origin: NSPoint(x: 0, y: 0), width: self.frame.width))
self.publicIPv4Field = PopupRow(container, title: "\(LocalizedString("v4")):", value: LocalizedString("Unknown")).1
self.publicIPv6Field = PopupRow(container, title: "\(LocalizedString("v6")):", value: LocalizedString("Unknown")).1
self.publicIPv4Field?.isSelectable = true
if let valueView = self.publicIPv6Field {
valueView.isSelectable = true
valueView.font = NSFont.systemFont(ofSize: 10, weight: .regular)
valueView.setFrameOrigin(NSPoint(x: valueView.frame.origin.x, y: 1))
}
view.addSubview(container)
let h = container.arrangedSubviews.map({ $0.bounds.height }).reduce(0, +)
view.setFrameSize(NSSize(width: self.frame.width, height: h))
container.setFrameSize(NSSize(width: self.frame.width, height: view.bounds.height))
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
container.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
self.publicIPStackView = container
return view
}
private func initProcesses() -> NSView {
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.processesHeight))
let separator = SeparatorView(LocalizedString("Top processes"), origin: NSPoint(x: 0, y: self.processesHeight-Constants.Popup.separatorHeight), width: self.frame.width)
@@ -214,9 +223,111 @@ internal class Popup: NSView, Popup_p {
view.addSubview(separator)
view.addSubview(container)
self.processesView = view
return view
}
// MARK: - callbacks
public func numberOfProcessesUpdated() {
if self.processes.count == self.numberOfProcesses {
return
}
DispatchQueue.main.async(execute: {
self.processes = []
if let view = self.processesView {
self.removeView(view)
}
self.addArrangedSubview(self.initProcesses())
self.processesInitialized = false
self.recalculateHeight()
})
}
public func usageCallback(_ value: Network_Usage) {
DispatchQueue.main.async(execute: {
if (self.window?.isVisible ?? false) || !self.initialized {
self.uploadValue = value.bandwidth.upload
self.downloadValue = value.bandwidth.download
self.setUploadDownloadFields()
self.totalUploadField?.stringValue = Units(bytes: value.total.upload).getReadableMemory()
self.totalDownloadField?.stringValue = Units(bytes: value.total.download).getReadableMemory()
if let interface = value.interface {
self.interfaceField?.stringValue = "\(interface.displayName) (\(interface.BSDName))"
self.macAdressField?.stringValue = interface.address
} else {
self.interfaceField?.stringValue = LocalizedString("Unknown")
self.macAdressField?.stringValue = LocalizedString("Unknown")
}
if value.connectionType == .wifi {
self.ssidField?.stringValue = value.ssid ?? "Unknown"
} else {
self.ssidField?.stringValue = LocalizedString("Unavailable")
}
if let view = self.publicIPv4Field, view.stringValue != value.raddr.v4 {
if let addr = value.raddr.v4 {
view.stringValue = (value.countryCode != nil) ? "\(addr) (\(value.countryCode!))" : addr
} else {
view.stringValue = LocalizedString("Unknown")
}
}
if let view = self.publicIPv6Field, view.stringValue != value.raddr.v6 {
if let addr = value.raddr.v6 {
view.stringValue = addr
} else {
view.stringValue = LocalizedString("Unknown")
}
}
if self.localIPField?.stringValue != value.laddr {
self.localIPField?.stringValue = value.laddr ?? LocalizedString("Unknown")
}
self.initialized = true
}
if let chart = self.chart {
if chart.base != self.base {
chart.base = self.base
}
chart.addValue(upload: Double(value.bandwidth.upload), download: Double(value.bandwidth.download))
}
})
}
public func processCallback(_ list: [Network_Process]) {
DispatchQueue.main.async(execute: {
if !(self.window?.isVisible ?? false) && self.processesInitialized {
return
}
if list.count != self.processes.count {
self.processes.forEach { processView in
processView.clear()
}
}
for i in 0..<list.count {
let process = list[i]
let index = list.count-i-1
self.processes[index].attachProcess(process)
self.processes[index].upload = Units(bytes: Int64(process.upload)).getReadableSpeed(base: self.base)
self.processes[index].download = Units(bytes: Int64(process.download)).getReadableSpeed(base: self.base)
}
self.processesInitialized = true
})
}
// MARK: - helpers
private func topValueView(_ view: NSView, title: String, color: NSColor) -> (NSView, NSTextField, NSTextField, ColorView) {
let topHeight: CGFloat = 30
let titleHeight: CGFloat = 15
@@ -301,96 +412,6 @@ internal class Popup: NSView, Popup_p {
self.uploadStateView?.setState(self.uploadValue != 0)
self.downloadStateView?.setState(self.downloadValue != 0)
}
public func usageCallback(_ value: Network_Usage) {
DispatchQueue.main.async(execute: {
if (self.window?.isVisible ?? false) || !self.initialized {
self.uploadValue = value.bandwidth.upload
self.downloadValue = value.bandwidth.download
self.setUploadDownloadFields()
self.totalUploadField?.stringValue = Units(bytes: value.total.upload).getReadableMemory()
self.totalDownloadField?.stringValue = Units(bytes: value.total.download).getReadableMemory()
if let interface = value.interface {
self.interfaceField?.stringValue = "\(interface.displayName) (\(interface.BSDName))"
self.macAdressField?.stringValue = interface.address
} else {
self.interfaceField?.stringValue = LocalizedString("Unknown")
self.macAdressField?.stringValue = LocalizedString("Unknown")
}
if value.connectionType == .wifi {
self.ssidField?.stringValue = value.ssid ?? "Unknown"
} else {
self.ssidField?.stringValue = LocalizedString("Unavailable")
}
if self.publicIPField?.stringValue != value.raddr {
if value.raddr == nil {
self.publicIPField?.stringValue = LocalizedString("Unknown")
} else {
if value.countryCode == nil {
self.publicIPField?.stringValue = value.raddr!
} else {
self.publicIPField?.stringValue = "\(value.raddr!) (\(value.countryCode!))"
}
}
}
if self.localIPField?.stringValue != value.laddr {
self.localIPField?.stringValue = value.laddr ?? LocalizedString("Unknown")
}
self.initialized = true
}
if let chart = self.chart {
if chart.base != self.base {
chart.base = self.base
}
chart.addValue(upload: Double(value.bandwidth.upload), download: Double(value.bandwidth.download))
}
})
}
public func processCallback(_ list: [Network_Process]) {
DispatchQueue.main.async(execute: {
if !(self.window?.isVisible ?? false) && self.processesInitialized {
return
}
if list.count != self.processes.count {
self.processes.forEach { processView in
processView.clear()
}
}
for i in 0..<list.count {
let process = list[i]
let index = list.count-i-1
self.processes[index].attachProcess(process)
self.processes[index].upload = Units(bytes: Int64(process.upload)).getReadableSpeed(base: self.base)
self.processes[index].download = Units(bytes: Int64(process.download)).getReadableSpeed(base: self.base)
}
self.processesInitialized = true
})
}
}
extension ValueField {
public override func mouseDown(with: NSEvent) {
guard self.stringValue != LocalizedString("No connection") && self.stringValue != LocalizedString("Unknown") && self.stringValue != LocalizedString("Unavailable") && self.toolTip != nil else {
return
}
let arr = self.stringValue.split(separator: " ")
let value: String = arr.count > 0 ? String(arr[0]) : self.stringValue
let pasteboard = NSPasteboard.general
pasteboard.declareTypes([.string], owner: nil)
pasteboard.setString(value, forType: .string)
}
}
public class NetworkProcessView: NSView {

View File

@@ -229,14 +229,23 @@ internal class UsageReader: Reader<Network_Usage> {
}
private func getPublicIP() {
let url = URL(string: "https://api.ipify.org")
do {
if let url = url {
self.usage.raddr = try String(contentsOf: url)
if let url = URL(string: "https://api.ipify.org") {
self.usage.raddr.v4 = try String(contentsOf: url)
}
} catch let error {
os_log(.error, log: log, "get public ip %s", "\(error)")
os_log(.error, log: log, "get public ipv4 %s", "\(error)")
}
do {
if let url = URL(string: "https://api64.ipify.org") {
let v6 = try String(contentsOf: url)
if self.usage.raddr.v4 != v6 {
self.usage.raddr.v6 = v6
}
}
} catch let error {
os_log(.error, log: log, "get public ipv6 %s", "\(error)")
}
}

View File

@@ -316,6 +316,7 @@ public extension NSBezierPath {
public func SeparatorView(_ title: String, origin: NSPoint, width: CGFloat) -> NSView {
let view: NSView = NSView(frame: NSRect(x: origin.x, y: origin.y, width: width, height: 30))
view.heightAnchor.constraint(equalToConstant: view.bounds.height).isActive = true
let labelView: NSTextField = TextView(frame: NSRect(x: 0, y: (view.frame.height-15)/2, width: view.frame.width, height: 15))
labelView.stringValue = title
@@ -328,16 +329,22 @@ public func SeparatorView(_ title: String, origin: NSPoint, width: CGFloat) -> N
return view
}
public func PopupRow(_ view: NSView, n: CGFloat, title: String, value: String) -> (LabelField, ValueField) {
public func PopupRow(_ view: NSView, n: CGFloat = 0, title: String, value: String) -> (LabelField, ValueField) {
let rowView: NSView = NSView(frame: NSRect(x: 0, y: 22*n, width: view.frame.width, height: 22))
let labelWidth = title.widthOfString(usingFont: .systemFont(ofSize: 13, weight: .regular)) + 5
let labelWidth = title.widthOfString(usingFont: .systemFont(ofSize: 13, weight: .regular)) + 4
let labelView: LabelField = LabelField(frame: NSRect(x: 0, y: (22-15)/2, width: labelWidth, height: 15), title)
let valueView: ValueField = ValueField(frame: NSRect(x: labelWidth, y: (22-15)/2, width: rowView.frame.width - labelWidth, height: 16), value)
rowView.addSubview(labelView)
rowView.addSubview(valueView)
view.addSubview(rowView)
if let view = view as? NSStackView {
rowView.heightAnchor.constraint(equalToConstant: rowView.bounds.height).isActive = true
view.addArrangedSubview(rowView)
} else {
view.addSubview(rowView)
}
return (labelView, valueView)
}