diff --git a/.swiftlint.yml b/.swiftlint.yml index fc2ad98..53b51a2 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -3,6 +3,7 @@ disabled_rules: - todo # conflicts with swiftformat's default - trailing_comma + - function_body_length excluded: # ignore vendor files, will be removed in the future - WireGuardStatusbar/INIParser.swift diff --git a/Makefile b/Makefile index 8e6d8b6..6dc9417 100644 --- a/Makefile +++ b/Makefile @@ -93,12 +93,13 @@ ${build_dest}/WireGuardStatusbar.app: ${sources} | icons ${xcpretty} # install and run the App /Application using the distributable .dmg install: /Applications/WireGuardStatusbar.app -/Applications/WireGuardStatusbar.app: ${build_dest}/WireGuardStatusbar.app | WireGuardStatusbar.dmg +/Applications/WireGuardStatusbar.app: WireGuardStatusbar.dmg -osascript -e 'tell application "WireGuardStatusbar" to quit' - -diskutil umount /Volumes/WireGuardStatusbar + -hdiutil detach -quiet /Volumes/WireGuardStatusbar/ hdiutil attach -quiet WireGuardStatusbar.dmg cp -r /Volumes/WireGuardStatusbar/WireGuardStatusbar.app /Volumes/WireGuardStatusbar/Applications/ hdiutil detach -quiet /Volumes/WireGuardStatusbar/ + touch $@ open "$@" uninstall: diff --git a/Shared/Const.swift b/Shared/Const.swift index 3e8173c..6cc613d 100644 --- a/Shared/Const.swift +++ b/Shared/Const.swift @@ -24,3 +24,8 @@ Please follow the instructions on: and restart this Application afterwards. """ + +let defaultSettings = [ + "showAllTunnelDetails": false, + "showConnectedTunnelDetails": true, +] diff --git a/UnitTests/AppTests.swift b/UnitTests/AppTests.swift index 540e84a..2ba88a2 100644 --- a/UnitTests/AppTests.swift +++ b/UnitTests/AppTests.swift @@ -71,11 +71,19 @@ class AppTests: XCTestCase { XCTAssertEqual(menu.items[4].title, " Allowed IPs: 198.51.100.0/24") } + func testMenuEnabledTunnelNoDetails() { + var tunnels = testTunnels + tunnels[0].interface = "utun1" + + let menu = buildMenu(tunnels: tunnels, connectedTunnelDetails: false) + XCTAssertEqual(menu.items[1].title, "2 Invalid Config") + } + func testMenuDetails() { var tunnels = testTunnels tunnels[0].interface = "utun1" - let menu = buildMenu(tunnels: tunnels, details: true) + let menu = buildMenu(tunnels: tunnels, allTunnelDetails: true) XCTAssertEqual(menu.items[0].title, "1 Tunnel Name") XCTAssertEqual(menu.items[0].state, NSControl.StateValue.on) XCTAssertEqual(menu.items[1].title, " Interface: utun1") @@ -88,7 +96,7 @@ class AppTests: XCTestCase { var tunnels = testTunnels tunnels[1].interface = "utun1" - let menu = buildMenu(tunnels: tunnels, details: true) + let menu = buildMenu(tunnels: tunnels, allTunnelDetails: true) let offset = 4 XCTAssertEqual(menu.items[0 + offset].title, "2 Invalid Config") XCTAssertEqual(menu.items[0 + offset].state, NSControl.StateValue.on) diff --git a/WireGuardStatusbar.xcodeproj/project.pbxproj b/WireGuardStatusbar.xcodeproj/project.pbxproj index e9065c1..89698be 100644 --- a/WireGuardStatusbar.xcodeproj/project.pbxproj +++ b/WireGuardStatusbar.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ @@ -44,6 +44,9 @@ 0456DA6521BB0C9000701CCE /* HelperProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0456DA6321BB0C9000701CCE /* HelperProtocol.swift */; }; 0456DA6621BB0C9A00701CCE /* HelperProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0456DA6321BB0C9000701CCE /* HelperProtocol.swift */; }; 0456DA6721BB0C9C00701CCE /* Const.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0456DA6221BB0C9000701CCE /* Const.swift */; }; + 0457E2FC21DFF1F600DD17A2 /* PreferencesController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0457E2FA21DFF1F600DD17A2 /* PreferencesController.swift */; }; + 0457E2FD21DFF1F600DD17A2 /* PreferencesController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0457E2FB21DFF1F600DD17A2 /* PreferencesController.xib */; }; + 0457E2FE21DFF2D400DD17A2 /* PreferencesController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0457E2FA21DFF1F600DD17A2 /* PreferencesController.swift */; }; 0466E5B821C1AB2D00113B45 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B4BE43211E1E010001213A /* AppDelegate.swift */; }; 0466E5B921C1AB5700113B45 /* Const.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0456DA6221BB0C9000701CCE /* Const.swift */; }; 0466E5BA21C1AB5700113B45 /* HelperProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0456DA6321BB0C9000701CCE /* HelperProtocol.swift */; }; @@ -124,6 +127,8 @@ 04554CBB21C0515900A08A17 /* test-localhost.conf */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "test-localhost.conf"; sourceTree = ""; }; 0456DA6221BB0C9000701CCE /* Const.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Const.swift; path = Shared/Const.swift; sourceTree = ""; }; 0456DA6321BB0C9000701CCE /* HelperProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HelperProtocol.swift; path = Shared/HelperProtocol.swift; sourceTree = ""; }; + 0457E2FA21DFF1F600DD17A2 /* PreferencesController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesController.swift; sourceTree = ""; }; + 0457E2FB21DFF1F600DD17A2 /* PreferencesController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PreferencesController.xib; sourceTree = ""; }; 047AE5D821DE7D390099ABF6 /* HelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelperTests.swift; sourceTree = ""; }; 0490167D21C04B3800299400 /* IntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 0490167F21C04B3900299400 /* IntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationTests.swift; sourceTree = ""; }; @@ -242,6 +247,8 @@ 0401E29621C990FB0030E707 /* HelperXPC.swift */, 0401E29821C9913B0030E707 /* Tunnel.swift */, 0401E2AB21CD853B0030E707 /* Menu.swift */, + 0457E2FA21DFF1F600DD17A2 /* PreferencesController.swift */, + 0457E2FB21DFF1F600DD17A2 /* PreferencesController.xib */, ); path = WireGuardStatusbar; sourceTree = ""; @@ -396,6 +403,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 0457E2FD21DFF1F600DD17A2 /* PreferencesController.xib in Resources */, 04B4BE46211E1E030001213A /* Assets.xcassets in Resources */, 04B4BE49211E1E030001213A /* MainMenu.xib in Resources */, 04B2BD2E21BD5DC800CCAE2F /* README.md in Resources */, @@ -492,6 +500,7 @@ 0401E2AA21CD81210030E707 /* Types.swift in Sources */, 0401E2B321CD93370030E707 /* AppXPC.swift in Sources */, 0401E29B21C992C80030E707 /* Tunnel.swift in Sources */, + 0457E2FE21DFF2D400DD17A2 /* PreferencesController.swift in Sources */, 0401E29D21C992EE0030E707 /* HelperXPC.swift in Sources */, 0401E2B221CD93340030E707 /* WireGuard.swift in Sources */, 0401E2B121CD92A80030E707 /* Helper.swift in Sources */, @@ -508,6 +517,7 @@ 0401E2A921CD811F0030E707 /* Types.swift in Sources */, 0456DA6421BB0C9000701CCE /* Const.swift in Sources */, 0401E29921C9913B0030E707 /* Tunnel.swift in Sources */, + 0457E2FC21DFF1F600DD17A2 /* PreferencesController.swift in Sources */, 0401E2AC21CD853B0030E707 /* Menu.swift in Sources */, 0401E29721C990FB0030E707 /* HelperXPC.swift in Sources */, 0456DA6521BB0C9000701CCE /* HelperProtocol.swift in Sources */, diff --git a/WireGuardStatusbar.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/WireGuardStatusbar.xcodeproj/project.xcworkspace/contents.xcworkspacedata index de94e55..919434a 100644 --- a/WireGuardStatusbar.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/WireGuardStatusbar.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -1,12 +1,6 @@ - - - - diff --git a/WireGuardStatusbar/AppDelegate.swift b/WireGuardStatusbar/AppDelegate.swift index adf4348..50576fd 100644 --- a/WireGuardStatusbar/AppDelegate.swift +++ b/WireGuardStatusbar/AppDelegate.swift @@ -19,6 +19,13 @@ extension NSImage.Name { @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate, AppProtocol { + // don't load persistent defaults during development/ui-testing + #if DEBUG + let defaults = UserDefaults(suiteName: "test")! + #else + let defaults = UserDefaults.standard + #endif + // keep the existence and state of all tunnel(configuration)s var tunnels = Tunnels() @@ -30,6 +37,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, AppProtocol { var privilegedHelper: HelperXPC? func applicationDidFinishLaunching(_: Notification) { + // set default preferences + defaults.register(defaults: defaultSettings) + // set a default icon at startup statusItem.image = NSImage(named: .appInit)! statusItem.image!.isTemplate = true @@ -40,7 +50,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, AppProtocol { button.sendAction(on: [NSEvent.EventTypeMask.leftMouseDown, NSEvent.EventTypeMask.rightMouseDown]) } - // initialize helper, + // initialize helper XPC connection privilegedHelper = HelperXPC(exportedObject: self) // install the Helper or Update it if needed @@ -64,9 +74,16 @@ class AppDelegate: NSObject, NSApplicationDelegate, AppProtocol { // build menu on the fly using tunnels state/configuration @objc func statusBarButtonClicked(sender _: NSStatusBarButton) { let event = NSApp.currentEvent! - let details = event.modifierFlags.contains(.option) + let optionClicked = event.modifierFlags.contains(.option) + + let showAllTunnelDetails = defaults.bool(forKey: "showAllTunnelDetails") + + let showDetails = optionClicked || showAllTunnelDetails + let showConnected = defaults.bool(forKey: "showConnectedTunnelDetails") - statusItem.popUpMenu(buildMenu(tunnels: tunnels, details: details, + statusItem.popUpMenu(buildMenu(tunnels: tunnels, + allTunnelDetails: showDetails, + connectedTunnelDetails: showConnected, showInstallInstructions: !wireguardInstalled)) } @@ -110,15 +127,23 @@ class AppDelegate: NSObject, NSApplicationDelegate, AppProtocol { alert.runModal() } - @objc func quit(_: NSMenuItem) { - NSApplication.shared.terminate(self) - } - @objc func about(_: NSMenuItem) { NSApplication.shared.orderFrontStandardAboutPanel(self) NSApplication.shared.activate(ignoringOtherApps: true) } + var preferences: NSWindowController? + @objc func preferences(_: NSMenuItem) { + if preferences == nil { + preferences = Preferences() + } + preferences!.showWindow(nil) + } + + @objc func quit(_: NSMenuItem) { + NSApplication.shared.terminate(self) + } + func applicationWillTerminate(_: Notification) { // TODO: configurable option to disable tunnels on shutdown } diff --git a/WireGuardStatusbar/Info.plist b/WireGuardStatusbar/Info.plist index f2a067c..575cfa7 100644 --- a/WireGuardStatusbar/Info.plist +++ b/WireGuardStatusbar/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.13 + 1.14 CFBundleVersion 1 LSApplicationCategoryType diff --git a/WireGuardStatusbar/Menu.swift b/WireGuardStatusbar/Menu.swift index a28515b..391419a 100644 --- a/WireGuardStatusbar/Menu.swift +++ b/WireGuardStatusbar/Menu.swift @@ -4,14 +4,19 @@ import Cocoa // contruct menu with all tunnels found in configuration // TODO: find out if it is possible to have a dynamic bound IB menu with variable contents -func buildMenu(tunnels: Tunnels, details: Bool = false, showInstallInstructions: Bool = false) -> NSMenu { +func buildMenu(tunnels: Tunnels, allTunnelDetails: Bool = false, connectedTunnelDetails: Bool = true, + showInstallInstructions: Bool = false) -> NSMenu { // TODO: currently just rebuilding the entire menu, maybe opt for replacing the tunnel entries instead? let statusMenu = NSMenu() statusMenu.minimumWidth = 200 statusMenu.addItem(NSMenuItem.separator()) - statusMenu.addItem(NSMenuItem(title: "About", action: #selector(AppDelegate.about(_:)), keyEquivalent: "")) - statusMenu.addItem(NSMenuItem(title: "Quit", action: #selector(AppDelegate.quit(_:)), keyEquivalent: "q")) + statusMenu.addItem(NSMenuItem(title: "About", action: #selector(AppDelegate.about(_:)), + keyEquivalent: "")) + statusMenu.addItem(NSMenuItem(title: "Preferences...", action: #selector(AppDelegate.preferences(_:)), + keyEquivalent: ",")) + statusMenu.addItem(NSMenuItem(title: "Quit", action: #selector(AppDelegate.quit(_:)), + keyEquivalent: "q")) // WireGaurd missing is a big problem, user should fix this first. TODO, include WireGuard with the App if showInstallInstructions { @@ -33,7 +38,7 @@ func buildMenu(tunnels: Tunnels, details: Bool = false, showInstallInstructions: if tunnel.connected { item.state = NSControl.StateValue.on } - if tunnel.connected || details { + if (tunnel.connected && connectedTunnelDetails) || allTunnelDetails { if let config = tunnel.config { for peer in config.peers { statusMenu.insertItem( @@ -51,7 +56,7 @@ func buildMenu(tunnels: Tunnels, details: Bool = false, showInstallInstructions: } } - if tunnel.connected, let interface = tunnel.interface { + if tunnel.connected && (connectedTunnelDetails || allTunnelDetails), let interface = tunnel.interface { statusMenu.insertItem(NSMenuItem(title: " Interface: \(interface)", action: nil, keyEquivalent: ""), at: 0) } diff --git a/WireGuardStatusbar/PreferencesController.swift b/WireGuardStatusbar/PreferencesController.swift new file mode 100644 index 0000000..a9055f0 --- /dev/null +++ b/WireGuardStatusbar/PreferencesController.swift @@ -0,0 +1,28 @@ +// + +import Cocoa + +class Preferences: NSWindowController { + override var windowNibName: String { + return "PreferencesController" + } + + // make sure window is always brought to the front when it is opened + override func showWindow(_: Any?) { + window?.center() + window?.makeKeyAndOrderFront(nil) + NSApp.activate(ignoringOtherApps: true) + } + + // close on ⌘-w (required because app has no menubar with close window action) + override func keyDown(with event: NSEvent) { + if event.modifierFlags.contains(.command) && event.characters == "w" { + window?.close() + } + } + + // close on esc key + @objc func cancel(_: Any?) { + window?.close() + } +} diff --git a/WireGuardStatusbar/PreferencesController.xib b/WireGuardStatusbar/PreferencesController.xib new file mode 100644 index 0000000..660ace2 --- /dev/null +++ b/WireGuardStatusbar/PreferencesController.xib @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WireGuardStatusbarHelper/Helper.swift b/WireGuardStatusbarHelper/Helper.swift index 29055c3..e81e163 100644 --- a/WireGuardStatusbarHelper/Helper.swift +++ b/WireGuardStatusbarHelper/Helper.swift @@ -129,6 +129,7 @@ class Helper: NSObject, HelperProtocol, SKQueueDelegate { // Dispatch the shutdown of the runloop to at least 10 seconds after starting the application. // This will shutdown immidiately if the deadline already passed. shutdownTask = DispatchWorkItem { CFRunLoopStop(CFRunLoopGetCurrent()) } + // Dispatch to main queue since that is the thread where the runloop is DispatchQueue.main.asyncAfter(deadline: launchdMinimaltimeExpired, execute: shutdownTask!) } diff --git a/WireGuardStatusbarHelper/Info.plist b/WireGuardStatusbarHelper/Info.plist index 12f810c..35ba237 100644 --- a/WireGuardStatusbarHelper/Info.plist +++ b/WireGuardStatusbarHelper/Info.plist @@ -9,7 +9,7 @@ CFBundleName WireGuardStatusbarHelper CFBundleShortVersionString - 1.13 + 1.14 CFBundleVersion 1.0.12 SMAuthorizedClients