diff --git a/Kit/extensions.swift b/Kit/extensions.swift
index 7ba857ba..a8af075b 100644
--- a/Kit/extensions.swift
+++ b/Kit/extensions.swift
@@ -558,3 +558,9 @@ public extension TimeZone {
}
}
}
+
+extension CGFloat {
+ func roundedUpToNearestTen() -> CGFloat {
+ return ceil(self / 10) * 10
+ }
+}
diff --git a/Kit/helpers.swift b/Kit/helpers.swift
index 3b45b090..d27b4f0b 100644
--- a/Kit/helpers.swift
+++ b/Kit/helpers.swift
@@ -13,6 +13,7 @@
import Cocoa
import ServiceManagement
import UserNotifications
+import WebKit
public struct LaunchAtLogin {
private static let id = "\(Bundle.main.bundleIdentifier!).LaunchAtLogin"
@@ -1571,3 +1572,27 @@ public class PreferencesSwitch: NSStackView {
self.with.isEnabled = controlState(sender)
}
}
+
+public class HelpHUD: NSPanel {
+ public init(_ text: String, origin: CGPoint = CGPoint(x: 0, y: 0), size: CGSize = CGSize(width: 420, height: 300)) {
+ super.init(
+ contentRect: NSRect(origin: origin, size: size),
+ styleMask: [.hudWindow, .titled, .closable],
+ backing: .buffered, defer: false
+ )
+ self.isFloatingPanel = true
+ self.isMovableByWindowBackground = true
+ self.level = .floating
+ self.title = "Help"
+
+ let webView = WKWebView()
+ webView.setValue(false, forKey: "drawsBackground")
+ webView.loadHTMLString("
\(text)", baseURL: nil)
+ self.contentView = webView
+ }
+
+ public func show() {
+ self.makeKeyAndOrderFront(self)
+ self.center()
+ }
+}
diff --git a/Modules/Net/config.plist b/Modules/Net/config.plist
index 40f1fb05..57db9eb5 100644
--- a/Modules/Net/config.plist
+++ b/Modules/Net/config.plist
@@ -54,6 +54,18 @@
+ text
+
+ Default
+
+ Order
+ 4
+ Preview
+
+ Value
+ 192.168.0.1
+
+
Settings
diff --git a/Modules/Net/main.swift b/Modules/Net/main.swift
index 0a02d516..0167e7e9 100644
--- a/Modules/Net/main.swift
+++ b/Modules/Net/main.swift
@@ -146,6 +146,9 @@ public class Network: Module {
private var publicIPRefreshInterval: String {
Store.shared.string(key: "\(self.name)_publicIPRefreshInterval", defaultValue: "never")
}
+ private var textValue: String {
+ Store.shared.string(key: "\(self.name)_textWidgetValue", defaultValue: "$addr.public - $status")
+ }
public init() {
self.settingsView = Settings(.network)
@@ -233,13 +236,74 @@ public class Network: Module {
switch w.item {
case let widget as SpeedWidget: widget.setValue(upload: upload, download: download)
case let widget as NetworkChart: widget.setValue(upload: Double(upload), download: Double(download))
+ case let widget as TextWidget:
+ var text = self.textValue
+ let pairs = TextWidget.parseText(text)
+ pairs.forEach { pair in
+ var replacement: String? = nil
+
+ switch pair.key {
+ case "$addr":
+ switch pair.value {
+ case "public": replacement = value.raddr.v4 ?? value.raddr.v6 ?? "-"
+ case "publicV4": replacement = value.raddr.v4 ?? "-"
+ case "publicV6": replacement = value.raddr.v6 ?? "-"
+ case "private": replacement = value.laddr ?? "-"
+ default: return
+ }
+ case "$interface":
+ switch pair.value {
+ case "displayName": replacement = value.interface?.displayName ?? "-"
+ case "BSDName": replacement = value.interface?.BSDName ?? "-"
+ case "address": replacement = value.interface?.address ?? "-"
+ default: return
+ }
+ case "$wifi":
+ switch pair.value {
+ case "ssid": replacement = value.wifiDetails.ssid ?? "-"
+ 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 ?? "-"
+ case "channel": replacement = value.wifiDetails.channel ?? "-"
+ case "channelBand": replacement = value.wifiDetails.channelBand ?? "-"
+ case "channelWidth": replacement = value.wifiDetails.channelWidth ?? "-"
+ case "channelNumber": replacement = value.wifiDetails.channelNumber ?? "-"
+ default: return
+ }
+ case "$status":
+ replacement = localizedString(value.status ? "UP" : "DOWN")
+ case "$upload":
+ switch pair.value {
+ case "total": replacement = Units(bytes: value.total.upload).getReadableMemory()
+ default: replacement = Units(bytes: value.bandwidth.upload).getReadableMemory()
+ }
+ case "$download":
+ switch pair.value {
+ case "total": replacement = Units(bytes: value.total.download).getReadableMemory()
+ default: replacement = Units(bytes: value.bandwidth.download).getReadableMemory()
+ }
+ case "$type":
+ replacement = value.connectionType?.rawValue ?? "-"
+ default: return
+ }
+
+ if let replacement {
+ let key = pair.value.isEmpty ? pair.key : "\(pair.key).\(pair.value)"
+ text = text.replacingOccurrences(of: key, with: replacement)
+ }
+ }
+ widget.setValue(text)
default: break
}
}
if #available(macOS 11.0, *) {
- guard let blobData = try? JSONEncoder().encode(raw) else { return }
- self.userDefaults?.set(blobData, forKey: "Network@UsageReader")
+// guard let blobData = try? JSONEncoder().encode(raw) else { return }
+// self.userDefaults?.set(blobData, forKey: "Network@UsageReader")
WidgetCenter.shared.reloadTimelines(ofKind: Network_entry.kind)
}
}
diff --git a/Modules/Net/settings.swift b/Modules/Net/settings.swift
index 9c5f6f9c..02411798 100644
--- a/Modules/Net/settings.swift
+++ b/Modules/Net/settings.swift
@@ -13,6 +13,44 @@ import Cocoa
import Kit
import SystemConfiguration
+var textWidgetHelp = """
+Description
+You can use a combination of any of the variables. There is only one limitation: there must be a space between each variable.
+Examples:
+
+- $addr.public - $status
+- $addr.public - $wifi.ssid - $status
+
+Available variables
+
+- $addr.public: Public IP address.
+- $addr.publicV4: Public IPv4 address.
+- $addr.publicV6: Public IPv6 address.
+- $addr.private: Private/local IP address.
+- $interface.displayName: Network interface name.
+- $interface.BSDName: BSD name of the network interface.
+- $interface.address: MAC address of the network interface.
+- $wifi.ssid: Wi-Fi network name.
+- $wifi.bssid: MAC address of the Wi-Fi access point (BSSID).
+- $wifi.RSSI: Signal strength of the Wi-Fi network (RSSI).
+- $wifi.noise: Noise level of the Wi-Fi network.
+- $wifi.transmitRate: Transmit rate (connection speed) of the Wi-Fi network.
+- $wifi.standard: Wi-Fi standard (e.g., 802.11a/b/g/n/ac).
+- $wifi.mode: Operating mode of the Wi-Fi (e.g., infrastructure, adhoc).
+- $wifi.security: Type of security used by the Wi-Fi network.
+- $wifi.channel: Wi-Fi channel being used.
+- $wifi.channelBand: Frequency band of the Wi-Fi channel (e.g., 2.4 GHz, 5 GHz).
+- $wifi.channelWidth: Channel width used in MHz.
+- $wifi.channelNumber: Channel number used by the Wi-Fi network.
+- $status: Status of the network connection. "UP" if active, "DOWN" if inactive.
+- $upload.total: Total amount of data uploaded over the connection.
+- $upload: Current upload bandwidth used.
+- $download.total: Total amount of data downloaded over the connection.
+- $download: Current download bandwidth used.
+- $type: Type of network connection (e.g., Ethernet, Wi-Fi, Cellular).
+
+"""
+
internal class Settings: NSStackView, Settings_v, NSTextFieldDelegate {
private var numberOfProcesses: Int = 8
private var readerType: String = "interface"
@@ -24,6 +62,7 @@ internal class Settings: NSStackView, Settings_v, NSTextFieldDelegate {
private var ICMPHost: String = "1.1.1.1"
private var publicIPRefreshInterval: String = "never"
private var baseValue: String = "byte"
+ private var textValue: String = "$addr.public - $status"
public var callback: (() -> Void) = {}
public var callbackWhenUpdateNumberOfProcesses: (() -> Void) = {}
@@ -35,6 +74,7 @@ internal class Settings: NSStackView, Settings_v, NSTextFieldDelegate {
private var sliderView: NSView? = nil
private var section: PreferencesSection? = nil
private var widgetThresholdSection: PreferencesSection? = nil
+ private let textWidgetHelpPanel: HelpHUD = HelpHUD(textWidgetHelp)
private var list: [Network_interface] = []
@@ -57,6 +97,7 @@ internal class Settings: NSStackView, Settings_v, NSTextFieldDelegate {
self.ICMPHost = Store.shared.string(key: "\(self.title)_ICMPHost", defaultValue: self.ICMPHost)
self.publicIPRefreshInterval = Store.shared.string(key: "\(self.title)_publicIPRefreshInterval", defaultValue: self.publicIPRefreshInterval)
self.baseValue = Store.shared.string(key: "\(self.title)_base", defaultValue: self.baseValue)
+ self.textValue = Store.shared.string(key: "\(self.title)_textWidgetValue", defaultValue: self.textValue)
super.init(frame: NSRect.zero)
self.orientation = .vertical
@@ -170,9 +211,38 @@ internal class Settings: NSStackView, Settings_v, NSTextFieldDelegate {
valueField.delegate = self
valueField.placeholderString = localizedString("Leave empty to disable the check")
+ let ICMPField = self.inputField(id: "ICMP", value: self.ICMPHost, placeholder: localizedString("Leave empty to disable the check"))
self.addArrangedSubview(PreferencesSection([
- PreferencesRow(localizedString("Connectivity host (ICMP)"), component: valueField)
+ PreferencesRow(localizedString("Connectivity host (ICMP)"), component: ICMPField) {
+ NSWorkspace.shared.open(URL(string: "https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol")!)
+ }
]))
+
+ if widgets.contains(where: { $0 == .text }) {
+ let textField = self.inputField(id: "text", value: self.textValue, placeholder: localizedString("This will be visible in the text widget"))
+ self.addArrangedSubview(PreferencesSection([
+ PreferencesRow(localizedString("Text widget value"), component: textField) { [weak self] in
+ self?.textWidgetHelpPanel.show()
+ }
+ ]))
+ }
+ }
+
+ private func inputField(id: String, value: String, placeholder: String) -> NSView {
+ let field: NSTextField = NSTextField()
+ field.identifier = NSUserInterfaceItemIdentifier(id)
+ field.widthAnchor.constraint(equalToConstant: 250).isActive = true
+ field.font = NSFont.systemFont(ofSize: 12, weight: .regular)
+ field.textColor = .textColor
+ field.isEditable = true
+ field.isSelectable = true
+ field.usesSingleLineMode = true
+ field.maximumNumberOfLines = 1
+ field.focusRingType = .none
+ field.stringValue = value
+ field.delegate = self
+ field.placeholderString = placeholder
+ return field
}
@objc private func handleSelection(_ sender: NSPopUpButton) {
@@ -229,10 +299,15 @@ internal class Settings: NSStackView, Settings_v, NSTextFieldDelegate {
}
func controlTextDidChange(_ notification: Notification) {
- if let textField = notification.object as? NSTextField {
- self.ICMPHost = textField.stringValue
- Store.shared.set(key: "\(self.title)_ICMPHost", value: self.ICMPHost)
- self.ICMPHostCallback(self.ICMPHost.isEmpty)
+ if let field = notification.object as? NSTextField {
+ if field.identifier == NSUserInterfaceItemIdentifier("ICMP") {
+ self.ICMPHost = field.stringValue
+ Store.shared.set(key: "\(self.title)_ICMPHost", value: self.ICMPHost)
+ self.ICMPHostCallback(self.ICMPHost.isEmpty)
+ } else if field.identifier == NSUserInterfaceItemIdentifier("text") {
+ self.textValue = field.stringValue
+ Store.shared.set(key: "\(self.title)_textWidgetValue", value: self.textValue)
+ }
}
}