6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774EE21722D97006A79B3 /* TunnelsManager.swift */; };
6F7774F321774263006A79B3 /* TunnelEditTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */; };
6F7F7E5F21C7D74B00527607 /* TunnelErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7F7E5E21C7D74B00527607 /* TunnelErrors.swift */; };
+ 6F89E17A21EDEB0E00C97BB9 /* StatusItemController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F89E17921EDEB0E00C97BB9 /* StatusItemController.swift */; };
6F919EC3218A2AE90023B400 /* ErrorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */; };
6F919ED9218C65C50023B400 /* wireguard_doc_logo_22x29.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED5218C65C50023B400 /* wireguard_doc_logo_22x29.png */; };
6F919EDA218C65C50023B400 /* wireguard_doc_logo_44x58.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED6218C65C50023B400 /* wireguard_doc_logo_44x58.png */; };
6F7774EE21722D97006A79B3 /* TunnelsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelsManager.swift; sourceTree = "<group>"; };
6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelEditTableViewController.swift; sourceTree = "<group>"; };
6F7F7E5E21C7D74B00527607 /* TunnelErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelErrors.swift; sourceTree = "<group>"; };
+ 6F89E17921EDEB0E00C97BB9 /* StatusItemController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusItemController.swift; sourceTree = "<group>"; };
6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorPresenter.swift; sourceTree = "<group>"; };
6F919ED5218C65C50023B400 /* wireguard_doc_logo_22x29.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_22x29.png; sourceTree = "<group>"; };
6F919ED6218C65C50023B400 /* wireguard_doc_logo_44x58.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_44x58.png; sourceTree = "<group>"; };
6FBA104421D7EA750051C35F /* ViewController */,
6FBA101321D613F30051C35F /* Application.swift */,
6FB1BD5F21D2607A00A991BF /* AppDelegate.swift */,
+ 6F89E17921EDEB0E00C97BB9 /* StatusItemController.swift */,
6FBA101621D655340051C35F /* StatusMenu.swift */,
6FBA104121D6BC210051C35F /* ErrorPresenter.swift */,
6FCD99AE21E0EA1700BA4C82 /* ImportPanelPresenter.swift */,
6FB1BDBC21D50F0200A991BF /* ringlogger.c in Sources */,
6FB1BDBD21D50F0200A991BF /* ringlogger.h in Sources */,
6FBA103F21D6B6FF0051C35F /* TunnelImporter.swift in Sources */,
+ 6F89E17A21EDEB0E00C97BB9 /* StatusItemController.swift in Sources */,
6F4DD16B21DA558800690EAE /* TunnelListRow.swift in Sources */,
5F52D0BF21E3788900283CEA /* NSColor+Hex.swift in Sources */,
6FB1BDBE21D50F0200A991BF /* Logger.swift in Sources */,
return tunnels.first { $0.name == tunnelName }
}
+ func waitingTunnel() -> TunnelContainer? {
+ return tunnels.first { $0.status == .waiting }
+ }
+
func startActivation(of tunnel: TunnelContainer) {
guard tunnels.contains(tunnel) else { return } // Ensure it's not deleted
guard tunnel.status == .inactive else {
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
- var statusItem: NSStatusItem?
+ var statusItemController: StatusItemController?
+ var currentTunnelStatusObserver: AnyObject?
func applicationDidFinishLaunching(_ aNotification: Notification) {
Logger.configureGlobal(withFilePath: FileManager.appLogFileURL?.path)
}
let tunnelsManager: TunnelsManager = result.value!
+ let statusItemController = StatusItemController()
+
let statusMenu = StatusMenu(tunnelsManager: tunnelsManager)
- self.statusItem = createStatusBarItem(with: statusMenu)
+
+ statusItemController.statusItem.menu = statusMenu
+ statusItemController.currentTunnel = statusMenu.currentTunnel
+ self.currentTunnelStatusObserver = statusMenu.observe(\.currentTunnel) { statusMenu, _ in
+ statusItemController.currentTunnel = statusMenu.currentTunnel
+ }
+ self.statusItemController = statusItemController
tunnelsManager.tunnelsListDelegate = statusMenu
tunnelsManager.activationDelegate = statusMenu
}
}
}
-
-func createStatusBarItem(with statusMenu: StatusMenu) -> NSStatusItem {
- let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
- if let statusBarImage = NSImage(named: "WireGuardMacStatusBarIcon") {
- statusBarImage.isTemplate = true
- statusItem.button?.image = statusBarImage
- }
- statusItem.menu = statusMenu
- return statusItem
-}
"images" : [
{
"idiom" : "universal",
- "filename" : "WireGuardMacStatusBarIcon@1x.png",
+ "filename" : "StatusBarIcon@1x.png",
"scale" : "1x"
},
{
"idiom" : "universal",
- "filename" : "WireGuardMacStatusBarIcon@2x.png",
+ "filename" : "StatusBarIcon@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
- "filename" : "WireGuardMacStatusBarIcon@3x.png",
+ "filename" : "StatusBarIcon@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
}
}
\ No newline at end of file
--- /dev/null
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "StatusBarIconDimmed@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "StatusBarIconDimmed@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "StatusBarIconDimmed@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "StatusBarIconDot1@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "StatusBarIconDot1@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "StatusBarIconDot1@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "StatusBarIconDot2@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "StatusBarIconDot2@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "StatusBarIconDot2@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "StatusBarIconDot3@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "StatusBarIconDot3@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "StatusBarIconDot3@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
\ No newline at end of file
--- /dev/null
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+class StatusItemController {
+ var currentTunnel: TunnelContainer? {
+ didSet {
+ updateStatusItemImage()
+ }
+ }
+
+ let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
+ private let statusBarImageWhenActive = NSImage(named: "StatusBarIcon")!
+ private let statusBarImageWhenInactive = NSImage(named: "StatusBarIconDimmed")!
+
+ private let animationImages = [
+ NSImage(named: "StatusBarIconDot1")!,
+ NSImage(named: "StatusBarIconDot2")!,
+ NSImage(named: "StatusBarIconDot3")!
+ ]
+ private var animationImageIndex: Int = 0
+ private var animationTimer: Timer?
+
+ init() {
+ updateStatusItemImage()
+ }
+
+ func updateStatusItemImage() {
+ guard let currentTunnel = currentTunnel else {
+ stopActivatingAnimation()
+ statusItem.button?.image = statusBarImageWhenInactive
+ return
+ }
+ switch currentTunnel.status {
+ case .inactive:
+ stopActivatingAnimation()
+ statusItem.button?.image = statusBarImageWhenInactive
+ case .active:
+ stopActivatingAnimation()
+ statusItem.button?.image = statusBarImageWhenActive
+ case .activating, .waiting, .reasserting, .restarting:
+ startActivatingAnimation()
+ case .deactivating:
+ break
+ }
+ }
+
+ func startActivatingAnimation() {
+ guard animationTimer == nil else { return }
+ let timer = Timer(timeInterval: 0.3, repeats: true) { [weak self] _ in
+ guard let self = self else { return }
+ self.statusItem.button?.image = self.animationImages[self.animationImageIndex]
+ self.animationImageIndex = (self.animationImageIndex + 1) % self.animationImages.count
+ }
+ RunLoop.main.add(timer, forMode: .default)
+ animationTimer = timer
+ }
+
+ func stopActivatingAnimation() {
+ guard let timer = self.animationTimer else { return }
+ timer.invalidate()
+ animationTimer = nil
+ animationImageIndex = 0
+ }
+}
var firstTunnelMenuItemIndex = 0
var numberOfTunnelMenuItems = 0
+ @objc dynamic var currentTunnel: TunnelContainer?
+
var manageTunnelsRootVC: ManageTunnelsRootViewController?
lazy var manageTunnelsWindow: NSWindow = {
manageTunnelsRootVC = ManageTunnelsRootViewController(tunnelsManager: tunnelsManager)
addStatusMenuItems()
addItem(NSMenuItem.separator())
for index in 0 ..< tunnelsManager.numberOfTunnels() {
- let isUpdated = updateStatusMenuItems(with: tunnelsManager.tunnel(at: index), ignoreInactive: true)
+ let tunnel = tunnelsManager.tunnel(at: index)
+ if tunnel.status != .inactive {
+ currentTunnel = tunnel
+ }
+ let isUpdated = updateStatusMenuItems(with: tunnel, ignoreInactive: true)
if isUpdated {
break
}
updateTunnelMenuItem(menuItem)
let statusObservationToken = tunnel.observe(\.status) { [weak self] tunnel, _ in
updateTunnelMenuItem(menuItem)
+ if tunnel.status == .deactivating || tunnel.status == .inactive {
+ if self?.currentTunnel == tunnel {
+ self?.currentTunnel = self?.tunnelsManager.waitingTunnel()
+ }
+ } else {
+ self?.currentTunnel = tunnel
+ }
self?.updateStatusMenuItems(with: tunnel, ignoreInactive: false)
}
tunnelStatusObservers.insert(statusObservationToken, at: tunnelIndex)