Skip to content

Commit

Permalink
Add IPv6 support, add auto refresh on network change, add option to s…
Browse files Browse the repository at this point in the history
…elect network interface in local mode
  • Loading branch information
janekbaraniewski committed Apr 8, 2024
1 parent 422b478 commit 9e94e23
Showing 1 changed file with 117 additions and 65 deletions.
182 changes: 117 additions & 65 deletions IPMenuBar/IPMenuBarApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
import SwiftUI
import AppKit
import Foundation
import SystemConfiguration

@main
struct IPMenuBarAppApp: App {
struct IPMenuBarApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

var body: some Scene {
Expand All @@ -21,84 +22,135 @@ struct IPMenuBarAppApp: App {
}

class AppDelegate: NSObject, NSApplicationDelegate {
var statusBarItem: NSStatusItem!
var menu: NSMenu!

enum Mode {
case publicIP
case localIP(interfaceIndex: Int)
}

private var statusBarItem: NSStatusItem!
private var reachability: SCNetworkReachability?
private var localIPMenu: NSMenu!
private var mode: Mode = .publicIP {
didSet {
refreshLocalIPMenu()
}
}

override init() {
super.init()
initializeReachability()
}

func applicationDidFinishLaunching(_ notification: Notification) {
NSApp.setActivationPolicy(.accessory)
self.statusBarItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
statusBarItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
configureMenu()
updatePublicIP()
setupMenu()
}

@objc func updatePublicIP() {
let url = URL(string: "https://api.ipify.org")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else { return }
let publicIP = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
DispatchQueue.main.async {
self.statusBarItem.button?.title = publicIP ?? "IP Not Found"
private func initializeReachability() {
guard let reachability = SCNetworkReachabilityCreateWithName(nil, "www.apple.com") else { return }
self.reachability = reachability
var context = SCNetworkReachabilityContext(version: 0, info: Unmanaged.passUnretained(self).toOpaque(), retain: nil, release: nil, copyDescription: nil)
SCNetworkReachabilitySetCallback(reachability, { _, _, info in
guard let info = info else { return }
let appDelegate = Unmanaged<AppDelegate>.fromOpaque(info).takeUnretainedValue()
appDelegate.refreshLocalIPMenu()
}, &context)
SCNetworkReachabilitySetDispatchQueue(reachability, DispatchQueue.main)
}

private func configureMenu() {
let menu = NSMenu()

menu.addItem(withTitle: "Public IP", action: #selector(updatePublicIP), keyEquivalent: "").target = self

let localIPMenuItem = NSMenuItem(title: "Local IP", action: nil, keyEquivalent: "")
localIPMenu = NSMenu()
localIPMenuItem.submenu = localIPMenu
menu.addItem(localIPMenuItem)
refreshLocalIPMenu()

menu.addItem(.separator())
menu.addItem(withTitle: "About", action: #selector(showAboutPanel), keyEquivalent: "").target = self
menu.addItem(withTitle: "Quit", action: #selector(quitApplication), keyEquivalent: "").target = self

statusBarItem.menu = menu
}

private func refreshLocalIPMenu() {
localIPMenu.removeAllItems()
let interfaces = NetworkInterface.allInterfaces()
for (index, interface) in interfaces.enumerated() {
let item = NSMenuItem(title: "\(interface.name) (\(interface.address ?? "N/A"))", action: #selector(selectInterface(_:)), keyEquivalent: "")
item.tag = index
switch mode {
case .localIP(let interfaceIndex):
item.state = index == interfaceIndex ? .on : .off
case .publicIP:
item.state = .off
}
item.target = self
localIPMenu.addItem(item)
}
task.resume()
}

func setupMenu() {
menu = NSMenu()

let publicIPMenuItem = NSMenuItem(title: "Public IP", action: #selector(updatePublicIP), keyEquivalent: "")
menu.addItem(publicIPMenuItem)

let privateIPMenuItem = NSMenuItem(title: "Local IP", action: #selector(updatePrivateIP), keyEquivalent: "")
menu.addItem(privateIPMenuItem)

menu.addItem(NSMenuItem.separator())

let aboutItem = NSMenuItem(title: "About", action: #selector(showAbout), keyEquivalent: "")
menu.addItem(aboutItem)

let quitMenuItem = NSMenuItem(title: "Quit", action: #selector(quitApp), keyEquivalent: "")
menu.addItem(quitMenuItem)

statusBarItem.menu = menu

publicIPMenuItem.target = self
privateIPMenuItem.target = self
quitMenuItem.target = self

@objc private func updatePublicIP() {
URLSession.shared.dataTask(with: URL(string: "https://api64.ipify.org")!) { [weak self] data, response, error in
guard let self = self else { return }
DispatchQueue.main.async {
if let data = data, let publicIP = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) {
self.statusBarItem.button?.title = publicIP
self.mode = .publicIP
} else {
self.statusBarItem.button?.title = "No connection"
}
}
}.resume()
}

@objc private func selectInterface(_ sender: NSMenuItem) {
let selectedInterface = NetworkInterface.allInterfaces()[sender.tag]
mode = .localIP(interfaceIndex: sender.tag)
DispatchQueue.main.async { [weak self] in
self?.statusBarItem.button?.title = selectedInterface.address ?? "N/A"
}
}
@objc func showAbout() {

@objc private func showAboutPanel() {
NSApp.orderFrontStandardAboutPanel(nil)
}

@objc func updatePrivateIP() {
var address: String?

@objc private func quitApplication() {
NSApplication.shared.terminate(nil)
}
}

struct NetworkInterface {
let name: String
let address: String?

static func allInterfaces() -> [NetworkInterface] {
var interfaces = [NetworkInterface]()
var ifaddr: UnsafeMutablePointer<ifaddrs>?
guard getifaddrs(&ifaddr) == 0 else { return }
guard let firstAddr = ifaddr else { return }

for ifptr in sequence(first: firstAddr, next: { $0.pointee.ifa_next }) {
let interface = ifptr.pointee
let addrFamily = interface.ifa_addr.pointee.sa_family
if addrFamily == UInt8(AF_INET) {
let name = String(cString: interface.ifa_name)
if name != "lo0" {
var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
getnameinfo(interface.ifa_addr, socklen_t(interface.ifa_addr.pointee.sa_len),
&hostname, socklen_t(hostname.count),
nil, 0, NI_NUMERICHOST)
address = String(cString: hostname)
break
if getifaddrs(&ifaddr) == 0, let firstAddr = ifaddr {
for ifptr in sequence(first: firstAddr, next: { $0.pointee.ifa_next }) {
let interface = ifptr.pointee
let addrFamily = interface.ifa_addr.pointee.sa_family
if addrFamily == UInt8(AF_INET) || addrFamily == UInt8(AF_INET6) {
let name = String(cString: interface.ifa_name)
if name != "lo0" {
var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
getnameinfo(interface.ifa_addr, socklen_t(interface.ifa_addr.pointee.sa_len),
&hostname, socklen_t(hostname.count),
nil, 0, NI_NUMERICHOST)
let address = String(cString: hostname)
interfaces.append(NetworkInterface(name: name, address: address))
}
}
}
freeifaddrs(ifaddr)
}
freeifaddrs(ifaddr)
DispatchQueue.main.async {
self.statusBarItem.button?.title = "\(address ?? "Not Found")"
}
}

@objc func quitApp() {
NSApplication.shared.terminate(self)
return interfaces
}
}

0 comments on commit 9e94e23

Please sign in to comment.