Initial, holiday fun

This commit is contained in:
Ryan McGrath 2018-12-26 18:34:03 -08:00
commit abcab0c509
No known key found for this signature in database
GPG key ID: 811674B62B666830
18 changed files with 2638 additions and 0 deletions

44
four/AppDelegate.swift Normal file
View file

@ -0,0 +1,44 @@
//
// AppDelegate.swift
// four
//
// Created by Ryan McGrath on 12/23/18.
// Copyright © 2018 Ryan McGrath. All rights reserved.
//
import Cocoa
class AppDelegate: NSObject, NSApplicationDelegate {
lazy var popover: NSPopover = {
return NSPopover()
}()
lazy var statusItem: NSStatusItem = {
return NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
}()
func applicationDidFinishLaunching(_ aNotification: Notification) {
popover.behavior = .transient
popover.appearance = NSAppearance.current
popover.contentSize = CGSize(width: 300.0, height: 300.0)
popover.contentViewController = ViewController()
let statusBarIcon = Icons.statusBarIcon()
statusBarIcon.isTemplate = true
statusItem.button?.image = statusBarIcon
statusItem.button?.alternateImage = statusBarIcon
statusItem.button?.action = #selector(togglePopover)
}
func applicationWillTerminate(_ aNotification: Notification) {
}
@objc
func togglePopover(sender: Any?) {
if(popover.isShown) {
popover.performClose(sender)
} else {
popover.show(relativeTo: statusItem.button!.bounds, of: statusItem.button!, preferredEdge: .minY)
}
}
}

View file

@ -0,0 +1,58 @@
{
"images" : [
{
"idiom" : "mac",
"size" : "16x16",
"scale" : "1x"
},
{
"idiom" : "mac",
"size" : "16x16",
"scale" : "2x"
},
{
"idiom" : "mac",
"size" : "32x32",
"scale" : "1x"
},
{
"idiom" : "mac",
"size" : "32x32",
"scale" : "2x"
},
{
"idiom" : "mac",
"size" : "128x128",
"scale" : "1x"
},
{
"idiom" : "mac",
"size" : "128x128",
"scale" : "2x"
},
{
"idiom" : "mac",
"size" : "256x256",
"scale" : "1x"
},
{
"idiom" : "mac",
"size" : "256x256",
"scale" : "2x"
},
{
"idiom" : "mac",
"size" : "512x512",
"scale" : "1x"
},
{
"idiom" : "mac",
"size" : "512x512",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View file

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

112
four/Icons.swift Normal file
View file

@ -0,0 +1,112 @@
//
// Icons.swift
// four
//
// Created by Ryan McGrath on 12/23/18.
// Copyright © 2018 Ryan McGrath. All rights reserved.
//
import Foundation
import Cocoa
struct Icons {
public static func statusBarIcon(frame targetFrame: NSRect = NSRect(x: 0, y: 0, width: 32, height: 32), resizing: ResizingBehavior = .aspectFit) -> NSImage {
let resizedFrame: NSRect = resizing.apply(rect: NSRect(x: 0, y: 0, width: 64, height: 64), target: targetFrame)
return NSImage(size: resizedFrame.size, flipped: false, drawingHandler: { (destRect: NSRect) -> Bool in
let context = NSGraphicsContext.current?.cgContext
NSGraphicsContext.saveGraphicsState()
context?.translateBy(x: resizedFrame.minX, y: resizedFrame.minY)
context?.scaleBy(x: resizedFrame.size.width / 64.0, y: resizedFrame.size.height / 64.0)
//// Color Declarations
let fillColor = NSColor(red: 1, green: 1, blue: 1, alpha: 1)
//// Bezier Drawing
let bezierPath = NSBezierPath()
bezierPath.move(to: NSPoint(x: 43.62, y: 16))
bezierPath.line(to: NSPoint(x: 18.53, y: 16))
bezierPath.line(to: NSPoint(x: 18.52, y: 16))
bezierPath.curve(to: NSPoint(x: 9.01, y: 25.46), controlPoint1: NSPoint(x: 13.27, y: 16), controlPoint2: NSPoint(x: 9.01, y: 20.24))
bezierPath.curve(to: NSPoint(x: 15.11, y: 34.29), controlPoint1: NSPoint(x: 9.01, y: 29.38), controlPoint2: NSPoint(x: 11.44, y: 32.89))
bezierPath.line(to: NSPoint(x: 15.08, y: 34.24))
bezierPath.curve(to: NSPoint(x: 23.86, y: 42.68), controlPoint1: NSPoint(x: 15.16, y: 38.98), controlPoint2: NSPoint(x: 19.09, y: 42.76))
bezierPath.curve(to: NSPoint(x: 25.51, y: 42.49), controlPoint1: NSPoint(x: 24.41, y: 42.67), controlPoint2: NSPoint(x: 24.97, y: 42.6))
bezierPath.line(to: NSPoint(x: 25.56, y: 42.52))
bezierPath.curve(to: NSPoint(x: 42.59, y: 44.3), controlPoint1: NSPoint(x: 29.77, y: 47.68), controlPoint2: NSPoint(x: 37.39, y: 48.48))
bezierPath.curve(to: NSPoint(x: 47.03, y: 35.99), controlPoint1: NSPoint(x: 45.14, y: 42.24), controlPoint2: NSPoint(x: 46.74, y: 39.24))
bezierPath.line(to: NSPoint(x: 47, y: 36.08))
bezierPath.curve(to: NSPoint(x: 53.43, y: 22.96), controlPoint1: NSPoint(x: 52.42, y: 34.22), controlPoint2: NSPoint(x: 55.3, y: 28.35))
bezierPath.curve(to: NSPoint(x: 43.62, y: 16), controlPoint1: NSPoint(x: 51.99, y: 18.8), controlPoint2: NSPoint(x: 48.05, y: 16))
bezierPath.line(to: NSPoint(x: 43.62, y: 16))
bezierPath.close()
bezierPath.move(to: NSPoint(x: 44.84, y: 33.2))
bezierPath.line(to: NSPoint(x: 44.83, y: 33.21))
bezierPath.curve(to: NSPoint(x: 43.67, y: 34.68), controlPoint1: NSPoint(x: 44.19, y: 33.43), controlPoint2: NSPoint(x: 43.73, y: 34))
bezierPath.line(to: NSPoint(x: 43.59, y: 35.6))
bezierPath.line(to: NSPoint(x: 43.59, y: 35.6))
bezierPath.curve(to: NSPoint(x: 35.03, y: 43.54), controlPoint1: NSPoint(x: 43.2, y: 40.03), controlPoint2: NSPoint(x: 39.5, y: 43.46))
bezierPath.curve(to: NSPoint(x: 27.55, y: 39.51), controlPoint1: NSPoint(x: 31.04, y: 43.58), controlPoint2: NSPoint(x: 28.92, y: 41.23))
bezierPath.line(to: NSPoint(x: 27.54, y: 39.5))
bezierPath.curve(to: NSPoint(x: 25.82, y: 38.91), controlPoint1: NSPoint(x: 27.13, y: 38.99), controlPoint2: NSPoint(x: 26.46, y: 38.76))
bezierPath.line(to: NSPoint(x: 25.83, y: 38.9))
bezierPath.curve(to: NSPoint(x: 23.49, y: 39.23), controlPoint1: NSPoint(x: 25.07, y: 39.12), controlPoint2: NSPoint(x: 24.29, y: 39.23))
bezierPath.line(to: NSPoint(x: 23.55, y: 39.23))
bezierPath.curve(to: NSPoint(x: 18.53, y: 34.16), controlPoint1: NSPoint(x: 20.79, y: 39.14), controlPoint2: NSPoint(x: 18.58, y: 36.91))
bezierPath.line(to: NSPoint(x: 18.51, y: 33.06))
bezierPath.line(to: NSPoint(x: 18.51, y: 33.06))
bezierPath.curve(to: NSPoint(x: 17.43, y: 31.51), controlPoint1: NSPoint(x: 18.5, y: 32.38), controlPoint2: NSPoint(x: 18.07, y: 31.76))
bezierPath.curve(to: NSPoint(x: 12.48, y: 25.17), controlPoint1: NSPoint(x: 15.18, y: 30.63), controlPoint2: NSPoint(x: 12.33, y: 29.21))
bezierPath.line(to: NSPoint(x: 12.48, y: 25.13))
bezierPath.curve(to: NSPoint(x: 18.61, y: 19.44), controlPoint1: NSPoint(x: 12.65, y: 21.9), controlPoint2: NSPoint(x: 15.36, y: 19.39))
bezierPath.line(to: NSPoint(x: 43.53, y: 19.44))
bezierPath.line(to: NSPoint(x: 43.57, y: 19.44))
bezierPath.curve(to: NSPoint(x: 50.54, y: 26.26), controlPoint1: NSPoint(x: 47.37, y: 19.44), controlPoint2: NSPoint(x: 50.47, y: 22.47))
bezierPath.curve(to: NSPoint(x: 44.84, y: 33.2), controlPoint1: NSPoint(x: 50.57, y: 30.89), controlPoint2: NSPoint(x: 46.96, y: 32.48))
bezierPath.close()
bezierPath.windingRule = .evenOdd
fillColor.setFill()
bezierPath.fill()
NSGraphicsContext.restoreGraphicsState()
return true
})
}
public enum ResizingBehavior: Int {
case aspectFit /// The content is proportionally resized to fit into the target rectangle.
case aspectFill /// The content is proportionally resized to completely fill the target rectangle.
case stretch /// The content is stretched to match the entire target rectangle.
case center /// The content is centered in the target rectangle, but it is NOT resized.
public func apply(rect: NSRect, target: NSRect) -> NSRect {
if rect == target || target == NSRect.zero {
return rect
}
var scales = NSSize.zero
scales.width = abs(target.width / rect.width)
scales.height = abs(target.height / rect.height)
switch self {
case .aspectFit:
scales.width = min(scales.width, scales.height)
scales.height = scales.width
case .aspectFill:
scales.width = max(scales.width, scales.height)
scales.height = scales.width
case .stretch:
break
case .center:
scales.width = 1
scales.height = 1
}
var result = rect.standardized
result.size.width *= scales.width
result.size.height *= scales.height
result.origin.x = target.minX + (target.width - result.width) / 2
result.origin.y = target.minY + (target.height - result.height) / 2
return result
}
}
}

32
four/Info.plist Normal file
View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2018 Ryan McGrath. All rights reserved.</string>
<key>LSUIElement</key>
<true/>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>

181
four/JSSwitch.swift Normal file
View file

@ -0,0 +1,181 @@
//
// JSSwitch.swift
// JSSwitch
//
// Created by Julien Sagot on 29/05/16.
// Modified slightly by Ryan McGrath on or about 12/24/2018.
// Copyright © 2016 Julien Sagot. All rights reserved.
//
import AppKit
public class JSSwitch: NSControl {
// MARK: - Properties
private var pressed = false
private let backgroundLayer = CALayer()
private let knobContainer = CALayer()
private let knobLayer = CALayer()
private let knobShadows = (smallStroke: CALayer(), smallShadow: CALayer(), mediumShadow: CALayer(), bigShadow: CALayer())
// MARK: Computed
public override var wantsUpdateLayer: Bool { return true }
public override var intrinsicContentSize: NSSize {
return CGSize(width: 52, height: 32)
}
private var scaleFactor: CGFloat {
return ceil(frame.size.height / 62) // Hardcoded base height
}
public var tintColor = NSColor(deviceRed: 76/255, green: 217/255, blue: 100/255, alpha: 1.0) {
didSet { needsDisplay = true }
}
public var on = false {
didSet { needsDisplay = true }
}
// MARK: - Initializers
public override init(frame: CGRect) {
super.init(frame: frame)
setupLayers()
}
required public init?(coder: NSCoder) {
super.init(coder: coder)
setupLayers()
}
// MARK: - Layers Setup
private func setupLayers() {
wantsLayer = true
layer?.masksToBounds = false
layerContentsRedrawPolicy = .onSetNeedsDisplay
// Background
setupBackgroundLayer()
layer?.addSublayer(backgroundLayer)
// Knob
setupKnobLayers()
layer?.addSublayer(knobContainer)
}
// MARK: Background Layer
private func setupBackgroundLayer() {
backgroundLayer.frame = bounds
backgroundLayer.autoresizingMask = [.layerWidthSizable, .layerHeightSizable]
}
// MARK: Knob
private func setupKnobLayers() {
setupKnobContainerLayer()
setupKnobLayer()
setupKnobLayerShadows()
knobContainer.addSublayer(knobLayer)
knobContainer.insertSublayer(knobShadows.smallStroke, below: knobLayer)
knobContainer.insertSublayer(knobShadows.smallShadow, below: knobShadows.smallStroke)
knobContainer.insertSublayer(knobShadows.mediumShadow, below: knobShadows.smallShadow)
knobContainer.insertSublayer(knobShadows.bigShadow, below: knobShadows.mediumShadow)
}
private func setupKnobContainerLayer() {
knobContainer.frame = knobFrameForState(on: false, pressed: false)
}
private func setupKnobLayer() {
knobLayer.autoresizingMask = [.layerWidthSizable]
knobLayer.backgroundColor = NSColor.white.cgColor
knobLayer.frame = knobContainer.bounds
knobLayer.cornerRadius = ceil(knobContainer.bounds.height / 2)
}
private func setupKnobLayerShadows() {
let effectScale = scaleFactor
// Small Stroke
let smallStroke = knobShadows.smallStroke
smallStroke.frame = knobLayer.frame.insetBy(dx: -1, dy: -1)
smallStroke.autoresizingMask = [.layerWidthSizable]
smallStroke.backgroundColor = NSColor.black.withAlphaComponent(0.06).cgColor
smallStroke.cornerRadius = ceil(smallStroke.bounds.height / 2)
let smallShadow = knobShadows.smallShadow
smallShadow.frame = knobLayer.frame.insetBy(dx: 2, dy: 2)
smallShadow.autoresizingMask = [.layerWidthSizable]
smallShadow.cornerRadius = ceil(smallShadow.bounds.height / 2)
smallShadow.backgroundColor = NSColor.red.cgColor
smallShadow.shadowColor = NSColor.black.cgColor
smallShadow.shadowOffset = CGSize(width: 0, height: -3 * effectScale)
smallShadow.shadowOpacity = 0.12
smallShadow.shadowRadius = 2.0 * effectScale
let mediumShadow = knobShadows.mediumShadow
mediumShadow.frame = smallShadow.frame
mediumShadow.autoresizingMask = [.layerWidthSizable]
mediumShadow.cornerRadius = smallShadow.cornerRadius
mediumShadow.backgroundColor = NSColor.red.cgColor
mediumShadow.shadowColor = NSColor.black.cgColor
mediumShadow.shadowOffset = CGSize(width: 0, height: -9 * effectScale)
mediumShadow.shadowOpacity = 0.16
mediumShadow.shadowRadius = 6.0 * effectScale
let bigShadow = knobShadows.bigShadow
bigShadow.frame = smallShadow.frame
bigShadow.autoresizingMask = [.layerWidthSizable]
bigShadow.cornerRadius = smallShadow.cornerRadius
bigShadow.backgroundColor = NSColor.red.cgColor
bigShadow.shadowColor = NSColor.black.cgColor
bigShadow.shadowOffset = CGSize(width: 0, height: -9 * effectScale)
bigShadow.shadowOpacity = 0.06
bigShadow.shadowRadius = 0.5 * effectScale
}
// MARK: - Drawing
public override func updateLayer() {
// Background
backgroundLayer.cornerRadius = ceil(bounds.height / 2)
backgroundLayer.borderWidth = on ? ceil(bounds.height) : 3.0 * scaleFactor
backgroundLayer.borderColor = on ? tintColor.cgColor : NSColor.black.withAlphaComponent(0.09).cgColor
// Knob
knobContainer.frame = knobFrameForState(on: on, pressed: pressed)
knobLayer.cornerRadius = ceil(knobContainer.bounds.height / 2)
}
// MARK: - Helpers
private func knobFrameForState(on: Bool, pressed: Bool) -> CGRect {
let borderWidth = 3.0 * scaleFactor
var origin: CGPoint
var size: CGSize {
if pressed {
return CGSize(
width: ceil(bounds.width * 0.69) - (2 * borderWidth),
height: bounds.height - (2 * borderWidth)
)
}
return CGSize(width: bounds.height - (2 * borderWidth), height: bounds.height - (2 * borderWidth))
}
if on {
origin = CGPoint(x: bounds.width - size.width - borderWidth, y: borderWidth)
} else {
origin = CGPoint(x: borderWidth, y: borderWidth)
}
return CGRect(origin: origin, size: size)
}
// MARK: - Events
public override func mouseDown(with theEvent: NSEvent) {
super.mouseDown(with: theEvent)
pressed = true
needsDisplay = true
}
public override func mouseUp(with theEvent: NSEvent) {
super.mouseUp(with: theEvent)
pressed = false
on = !on
if(action != nil && target != nil) {
sendAction(action, to: target)
}
}
}

1551
four/TLDList.swift Normal file

File diff suppressed because it is too large Load diff

103
four/VPNManager.swift Normal file
View file

@ -0,0 +1,103 @@
//
// VPNManager.swift
// four
//
// Created by Ryan McGrath on 12/24/18.
// Copyright © 2018 Ryan McGrath. All rights reserved.
//
import Foundation
import NetworkExtension
class VPNManager {
fileprivate var manager = NETunnelProviderManager()
fileprivate static let vpnDescription = "CloudFlare 1.1.1.1 DNS"
fileprivate static let vpnServerDescription = "CloudFlare 1.1.1.1 DNS"
fileprivate static let vpnDNS = ["1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001"]
public var connected: Bool {
get {
return manager.isOnDemandEnabled
}
set {
if newValue == connected { return }
update(body: { [weak self] in
self?.manager.isEnabled = newValue
self?.manager.isOnDemandEnabled = newValue
}, complete: { [weak self] in
let session = self?.manager.connection as? NETunnelProviderSession
if newValue {
do {
try session?.startVPNTunnel(options: nil)
print("STARTED!")
} catch let err as NSError {
print("\(err.localizedDescription)")
}
} else {
session?.stopVPNTunnel()
}
})
}
}
init() {
refreshManager()
}
func refreshManager() -> Void {
NETunnelProviderManager.loadAllFromPreferences(completionHandler: { (managers, error) in
if nil == error {
if let managers = managers {
for manager in managers {
if manager.localizedDescription == VPNManager.vpnDescription {
self.manager = manager
return
}
}
}
}
self.configureDNS()
})
}
private func update(body: @escaping () -> Void, complete: @escaping () -> Void) {
manager.loadFromPreferences { error in
if error != nil {
print("Load error: \(String(describing: error?.localizedDescription))")
return
}
body()
self.manager.saveToPreferences { (error) in
if nil != error {
print("vpn_connect: save error \(error!)")
} else {
complete()
}
}
}
}
private func configureDNS() {
manager.localizedDescription = VPNManager.vpnDescription
let proto = NETunnelProviderProtocol()
proto.providerBundleIdentifier = "com.rymc.vpntunnel.provider"
proto.serverAddress = VPNManager.vpnServerDescription
manager.protocolConfiguration = proto
let evaluationRule = NEEvaluateConnectionRule(matchDomains: TLDS, andAction: .connectIfNeeded)
evaluationRule.useDNSServers = VPNManager.vpnDNS
let onDemandRule = NEOnDemandRuleEvaluateConnection()
onDemandRule.connectionRules = [evaluationRule]
onDemandRule.interfaceTypeMatch = .any
manager.onDemandRules = [onDemandRule]
}
}

92
four/ViewController.swift Normal file
View file

@ -0,0 +1,92 @@
//
// ViewController.swift
// four
//
// Created by Ryan McGrath on 12/23/18.
// Copyright © 2018 Ryan McGrath. All rights reserved.
//
import Cocoa
class View: NSView {
override var isFlipped: Bool {
get { return true }
}
}
class ViewController: NSViewController {
let vpnManager = VPNManager()
lazy var fourLabel: NSTextField = {
let label = NSTextField(labelWithString: "1.1.1.1")
label.translatesAutoresizingMaskIntoConstraints = false
label.alignment = .center
label.font = NSFont.boldSystemFont(ofSize: 30.0)
return label
}()
lazy var toggle: JSSwitch = {
let sw = JSSwitch(frame: CGRect(x: 118.0, y: 95.0, width: 64.0, height: 44.0))
sw.target = self
sw.action = #selector(toggleConnection)
return sw
}()
lazy var connectionStatusLabel: NSTextField = {
let label = NSTextField(labelWithString: "DISCONNECTED")
label.translatesAutoresizingMaskIntoConstraints = false
label.alignment = .center
label.font = NSFont.boldSystemFont(ofSize: 18.0)
return label
}()
lazy var makeThemFeelBadForNotBeingConnectedLabel: NSTextField = {
let label = NSTextField(labelWithString: "Your DNS queries are not private")
label.translatesAutoresizingMaskIntoConstraints = false
label.alignment = .center
label.font = NSFont.systemFont(ofSize: 18.0)
return label
}()
override func loadView() {
let frame = NSRect(x: 0, y: 0, width: 300, height: 300)
let rootView = View(frame: frame)
view = rootView
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(fourLabel)
view.addSubview(toggle)
view.addSubview(connectionStatusLabel)
view.addSubview(makeThemFeelBadForNotBeingConnectedLabel)
NSLayoutConstraint.activate([
fourLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 30.0),
fourLabel.widthAnchor.constraint(equalToConstant: 300.0),
fourLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
connectionStatusLabel.topAnchor.constraint(equalTo: fourLabel.bottomAnchor, constant: 120.0),
connectionStatusLabel.widthAnchor.constraint(equalToConstant: 300.0),
connectionStatusLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
makeThemFeelBadForNotBeingConnectedLabel.topAnchor.constraint(equalTo: connectionStatusLabel.bottomAnchor, constant: 20.0),
makeThemFeelBadForNotBeingConnectedLabel.widthAnchor.constraint(equalToConstant: 300.0),
makeThemFeelBadForNotBeingConnectedLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
@objc
func toggleConnection() {
vpnManager.connected = !vpnManager.connected
if vpnManager.connected {
connectionStatusLabel.stringValue = "CONNECTED"
makeThemFeelBadForNotBeingConnectedLabel.stringValue = "Your DNS queries are now\n private and faster"
} else {
connectionStatusLabel.stringValue = "DISCONNECTED"
makeThemFeelBadForNotBeingConnectedLabel.stringValue = "Your DNS queries are not private"
}
}
}

16
four/four.entitlements Normal file
View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>packet-tunnel-provider</string>
</array>
<key>com.apple.developer.networking.vpn.api</key>
<array>
<string>allow-vpn</string>
</array>
<key>com.apple.security.app-sandbox</key>
<true/>
</dict>
</plist>

20
four/main.swift Normal file
View file

@ -0,0 +1,20 @@
//
// main.swift
// four
//
// Created by Ryan McGrath on 12/23/18.
// Copyright © 2018 Ryan McGrath. All rights reserved.
//
import Cocoa
import Foundation
autoreleasepool {
let delegate = AppDelegate()
withExtendedLifetime(delegate, {
let application = NSApplication.shared
application.delegate = delegate
application.run()
application.delegate = nil
})
}