├── scripts └── travis │ └── .gitkeep ├── ClashX ├── Resources │ ├── .gitkeep │ ├── menu_icon@2x.png │ └── sampleConfig.yaml ├── goClash │ ├── build.sh │ ├── UIHelper.m │ ├── UIHelper.h │ ├── upgrade_core.py │ ├── go.mod │ ├── build_clash_universal.py │ └── proccess.go ├── Images.xcassets │ ├── Contents.json │ ├── AppIcon.appiconset │ │ ├── icon_16x16.png │ │ ├── icon_32x32.png │ │ ├── icon_128x128.png │ │ ├── icon_256x256.png │ │ ├── icon_512x512.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256@2x.png │ │ ├── icon_32x32@2x.png │ │ ├── icon_512x512@2x.png │ │ └── Contents.json │ ├── icon_connection_done.imageset │ │ ├── icon_connected.png │ │ ├── icon_connected@2x.png │ │ └── Contents.json │ ├── icon_connection_fail.imageset │ │ ├── icon_connection_fail.png │ │ ├── icon_connection_fail@2x.png │ │ └── Contents.json │ └── icon_connection_inprogress.imageset │ │ ├── icon_connecting.png │ │ ├── icon_connecting@2x.png │ │ └── Contents.json ├── ClashX.entitlements ├── ClashX-Bridging-Header.h ├── Extensions │ ├── String+Encode.swift │ ├── NSAlert+Extension.swift │ ├── NSTableView+Reload.swift │ ├── AppDelegate+..swift │ ├── DateFormatter+.swift │ ├── Array+Safe.swift │ ├── Cgo+Convert.swift │ ├── NSTextField+Vibrancy.swift │ └── NSView+Nib.swift ├── ViewControllers │ ├── Connections │ │ ├── DashboardSubViewControllerProtocol.swift │ │ ├── Views │ │ │ ├── Cell │ │ │ │ ├── ConnectionCellProtocol.swift │ │ │ │ ├── ConnectionStatusIconCellView.swift │ │ │ │ ├── ConnectionProxyClientCellView.swift │ │ │ │ ├── ConnectionTextCellView.swift │ │ │ │ └── ConnectionLeftTextCellView.swift │ │ │ ├── ConnectionDetailInfoGeneralView.swift │ │ │ ├── zh-Hans.lproj │ │ │ │ └── ConnectionDetailInfoGeneralView.strings │ │ │ ├── zh-Hant.lproj │ │ │ │ └── ConnectionDetailInfoGeneralView.strings │ │ │ └── ConnectionColume.swift │ │ ├── Requests │ │ │ └── ConnectionsReq.swift │ │ ├── ViewModels │ │ │ ├── ConnectionLeftPannelViewModel.swift │ │ │ ├── ConnectionDetailViewModel.swift │ │ │ └── ConnectionTopListViewModel.swift │ │ └── DashboardViewController.swift │ ├── Settings │ │ ├── SettingTabViewController.swift │ │ └── DebugSettingViewController.swift │ └── AboutViewController.swift ├── General │ ├── Managers │ │ ├── WebPortalManager.swift │ │ ├── ConnectionManager.swift │ │ ├── ConfigFileManager.swift │ │ ├── Settings.swift │ │ ├── SystemProxyManager.swift │ │ ├── PrivilegedHelperManager+Legacy.swift │ │ ├── AutoUpgardeManager.swift │ │ ├── ICloudManager.swift │ │ └── ClashResourceManager.swift │ └── Utils │ │ ├── Command.swift │ │ ├── ClashStatusTool.swift │ │ ├── AppVersionUtil.swift │ │ ├── JSBridge.swift │ │ └── JSBridgeHandler.swift ├── Vendor │ ├── LoginKitWrapper.h │ ├── Witness │ │ ├── Witness.h │ │ ├── Witness.swift │ │ └── EventStream.swift │ ├── LoginKitWrapper.m │ └── UserDefaultWrapper.swift ├── Views │ ├── StatusItem │ │ ├── StatusItemViewProtocol.swift │ │ ├── StatusItemTool.swift │ │ └── StatusItemView.swift │ ├── ProxyGroupMenu.swift │ ├── RemoteConfigUpdateIntervalSettingView.swift │ ├── ProxyDelayHistoryMenu.swift │ ├── NormalMenuItemView.swift │ └── ProxyItemView.swift ├── Basic │ ├── UnsafePointer+bridge.swift │ ├── Combine+Ext.swift │ ├── String+Extension.swift │ ├── LaunchAtLogin.swift │ ├── SpeedUtils.swift │ ├── NSView+Layout.swift │ └── Logger.swift ├── ViewController.swift ├── Macro │ ├── Paths.swift │ └── Notification.swift ├── AppleScript │ ├── ProxySettingCommand.swift │ ├── ProxySetting.sdef │ └── ProxyModeChangeCommand.swift ├── Models │ ├── ClashRule.swift │ ├── ClashProvider.swift │ ├── RemoteConfigModel.swift │ ├── SavedProxyModel.swift │ └── ClashConfig.swift ├── Actions │ ├── UpdateConfigAction.swift │ ├── UpdateExternalResourceAction.swift │ └── TerminalCleanUpAction.swift ├── add_build_info.py ├── ClashWindowController.swift └── Info.plist ├── a_cat_with_eye.png ├── fastlane ├── Appfile ├── Pluginfile ├── README.md └── Fastfile ├── ProxyConfigHelper ├── com.west2online.ClashX.ProxyConfigHelper.entitlements ├── CommonUtils.h ├── ProxyConfigHelper.h ├── main.m ├── Helper-Launchd.plist ├── CommonUtils.m ├── ProxySettingTool.h ├── Helper-Info.plist ├── ProxyConfigRemoteProcessProtocol.h └── ProxyConfigHelper.m ├── ClashX.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── swiftpm │ └── Package.resolved ├── updateLocalization.sh ├── Gemfile ├── .gitignore ├── .github ├── workflows │ └── pull_request.yaml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── certs │ └── dist.p12.enc ├── .bartycrouch.toml ├── install_dependency.sh ├── Shortcuts.md ├── .swiftlint.yml ├── Podfile ├── Podfile.lock ├── ClashX.xcodeproj └── xcshareddata │ └── xcschemes │ ├── com.west2online.ClashX.ProxyConfigHelper.xcscheme │ └── ClashX.xcscheme └── README.md /scripts/travis/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ClashX/Resources/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ClashX/goClash/build.sh: -------------------------------------------------------------------------------- 1 | python3 build_clash_universal.py 2 | -------------------------------------------------------------------------------- /a_cat_with_eye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/passwa11/ClashX/HEAD/a_cat_with_eye.png -------------------------------------------------------------------------------- /ClashX/Resources/menu_icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/passwa11/ClashX/HEAD/ClashX/Resources/menu_icon@2x.png -------------------------------------------------------------------------------- /ClashX/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fastlane/Appfile: -------------------------------------------------------------------------------- 1 | # app_identifier("[[APP_IDENTIFIER]]") # The bundle identifier of your app 2 | # apple_id("[[APPLE_ID]]") # Your Apple email address 3 | -------------------------------------------------------------------------------- /ClashX/Images.xcassets/AppIcon.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/passwa11/ClashX/HEAD/ClashX/Images.xcassets/AppIcon.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /ClashX/Images.xcassets/AppIcon.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/passwa11/ClashX/HEAD/ClashX/Images.xcassets/AppIcon.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /ClashX/Images.xcassets/AppIcon.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/passwa11/ClashX/HEAD/ClashX/Images.xcassets/AppIcon.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /ClashX/Images.xcassets/AppIcon.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/passwa11/ClashX/HEAD/ClashX/Images.xcassets/AppIcon.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /ClashX/Images.xcassets/AppIcon.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/passwa11/ClashX/HEAD/ClashX/Images.xcassets/AppIcon.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /ClashX/Images.xcassets/AppIcon.appiconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/passwa11/ClashX/HEAD/ClashX/Images.xcassets/AppIcon.appiconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /ClashX/Images.xcassets/AppIcon.appiconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/passwa11/ClashX/HEAD/ClashX/Images.xcassets/AppIcon.appiconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /ClashX/Images.xcassets/AppIcon.appiconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/passwa11/ClashX/HEAD/ClashX/Images.xcassets/AppIcon.appiconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /ClashX/Images.xcassets/AppIcon.appiconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/passwa11/ClashX/HEAD/ClashX/Images.xcassets/AppIcon.appiconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /ClashX/Images.xcassets/AppIcon.appiconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/passwa11/ClashX/HEAD/ClashX/Images.xcassets/AppIcon.appiconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /ClashX/Images.xcassets/icon_connection_done.imageset/icon_connected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/passwa11/ClashX/HEAD/ClashX/Images.xcassets/icon_connection_done.imageset/icon_connected.png -------------------------------------------------------------------------------- /ClashX/Images.xcassets/icon_connection_done.imageset/icon_connected@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/passwa11/ClashX/HEAD/ClashX/Images.xcassets/icon_connection_done.imageset/icon_connected@2x.png -------------------------------------------------------------------------------- /ClashX/Images.xcassets/icon_connection_fail.imageset/icon_connection_fail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/passwa11/ClashX/HEAD/ClashX/Images.xcassets/icon_connection_fail.imageset/icon_connection_fail.png -------------------------------------------------------------------------------- /ClashX/Images.xcassets/icon_connection_inprogress.imageset/icon_connecting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/passwa11/ClashX/HEAD/ClashX/Images.xcassets/icon_connection_inprogress.imageset/icon_connecting.png -------------------------------------------------------------------------------- /ClashX/Images.xcassets/icon_connection_fail.imageset/icon_connection_fail@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/passwa11/ClashX/HEAD/ClashX/Images.xcassets/icon_connection_fail.imageset/icon_connection_fail@2x.png -------------------------------------------------------------------------------- /ClashX/Images.xcassets/icon_connection_inprogress.imageset/icon_connecting@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/passwa11/ClashX/HEAD/ClashX/Images.xcassets/icon_connection_inprogress.imageset/icon_connecting@2x.png -------------------------------------------------------------------------------- /ClashX/ClashX.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /fastlane/Pluginfile: -------------------------------------------------------------------------------- 1 | # Autogenerated by fastlane 2 | # 3 | # Ensure this file is checked in to source control! 4 | 5 | gem 'fastlane-plugin-appcenter' 6 | gem 'fastlane-plugin-versioning' 7 | gem 'fastlane-plugin-update_xcodeproj' 8 | -------------------------------------------------------------------------------- /ProxyConfigHelper/com.west2online.ClashX.ProxyConfigHelper.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ClashX/ClashX-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | #import "goClash.h" 5 | #import "ProxyConfigRemoteProcessProtocol.h" 6 | #import "LoginKitWrapper.h" 7 | #import 8 | #import 9 | -------------------------------------------------------------------------------- /ClashX.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /updateLocalization.sh: -------------------------------------------------------------------------------- 1 | BartyCrouch update && BartyCrouch lint 2 | # swiftformat . --disable initCoderUnavailable,wrapArguments, andOperator, enumNamespaces, unusedArguments,numberFormatting,redundantReturn,andOperator,anyObjectProtocol,trailingClosures,redundantFileprivate --ranges nospace --swiftversion "5.0" 3 | -------------------------------------------------------------------------------- /ClashX/goClash/UIHelper.m: -------------------------------------------------------------------------------- 1 | #import "UIHelper.h" 2 | NStringCallback logCallback; 3 | IntCallback trafficCallback; 4 | void clash_setLogBlock(NStringCallback block) { 5 | logCallback = [block copy]; 6 | } 7 | 8 | void clash_setTrafficBlock(IntCallback block) { 9 | trafficCallback = [block copy]; 10 | } -------------------------------------------------------------------------------- /ClashX.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # Autogenerated by fastlane 2 | # 3 | # Ensure this file is checked in to source control! 4 | 5 | source "https://rubygems.org" 6 | 7 | gem 'fastlane' 8 | gem 'cocoapods' 9 | gem "activesupport", "= 7.0.8" 10 | plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') 11 | eval_gemfile(plugins_path) if File.exist?(plugins_path) 12 | -------------------------------------------------------------------------------- /ClashX/Extensions/String+Encode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Encode.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2019/12/11. 6 | // Copyright © 2019 west2online. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | extension String { 12 | var encoded: String { 13 | return addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ClashX/ViewControllers/Connections/DashboardSubViewControllerProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DashboardSubViewControllerProtocol.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2023/7/14. 6 | // Copyright © 2023 west2online. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | 11 | protocol DashboardSubViewControllerProtocol: NSViewController { 12 | func actionSearch(string: String) 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Pods/ 2 | Country.mmdb 3 | Carthage 4 | ClashX.a 5 | ClashX.h 6 | ClashX/Resources/dashboard 7 | ClashX.app 8 | *.dmg 9 | **/xcuserdata/ 10 | .idea 11 | *.pyc 12 | ClashX/clash/ 13 | .DS_Store 14 | .vscode 15 | ClashX/goClash/goClash.h 16 | ClashX/goClash/goClash.a 17 | fastlane/report.xml 18 | ClashX/Resources/Country.mmdb.gz 19 | .bundle/config 20 | *.app/** 21 | ClashX.app.dSYM.zip 22 | build_derived_data 23 | -------------------------------------------------------------------------------- /ClashX/General/Managers/WebPortalManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebPortalManager.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2019/1/11. 6 | // Copyright © 2019 west2online. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class WebPortalManager { 12 | static let shared = WebPortalManager() 13 | static let hasWebProtal = false 14 | 15 | func addWebProtalMenuItem(_ menu: inout NSMenu) {} 16 | } 17 | -------------------------------------------------------------------------------- /ProxyConfigHelper/CommonUtils.h: -------------------------------------------------------------------------------- 1 | // 2 | // CommonUtils.h 3 | // ClashX 4 | // 5 | // Created by yicheng on 2020/4/2. 6 | // Copyright © 2020 west2online. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface CommonUtils : NSObject 14 | + (NSString *)runCommand:(NSString *)path args:(nullable NSArray *)args; 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /ProxyConfigHelper/ProxyConfigHelper.h: -------------------------------------------------------------------------------- 1 | // 2 | // ProxyConfigHelper.h 3 | // com.west2online.ClashX.ProxyConfigHelper 4 | // 5 | // Created by yichengchen on 2019/8/17. 6 | // Copyright © 2019 west2online. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface ProxyConfigHelper : NSObject 14 | - (void)run; 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /ClashX.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "keyboardshortcuts", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/sindresorhus/KeyboardShortcuts.git", 7 | "state" : { 8 | "revision" : "b878f8132be59576fc87e39405b1914eff9f55d3", 9 | "version" : "1.14.1" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /ClashX/Vendor/LoginKitWrapper.h: -------------------------------------------------------------------------------- 1 | // 2 | // LoginKitWrapper.h 3 | // ClashX Pro 4 | // 5 | // Created by yicheng on 2022/3/22. 6 | // Copyright © 2022 west2online. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface LoginKitWrapper : NSObject 14 | +(BOOL) setLogin:(LSSharedFileListRef) inlist path:(NSString *) path; 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /ClashX/ViewControllers/Connections/Views/Cell/ConnectionCellProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectionCellProtocol.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2023/7/6. 6 | // Copyright © 2023 west2online. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | 11 | @available(macOS 10.15, *) 12 | protocol ConnectionCellProtocol: NSView { 13 | func setup(with connection: ClashConnectionSnapShot.Connection, type: ConnectionColume) 14 | } 15 | -------------------------------------------------------------------------------- /ClashX/Views/StatusItem/StatusItemViewProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusItemViewProtocol.swift 3 | // ClashX Pro 4 | // 5 | // Created by yicheng on 2023/3/1. 6 | // Copyright © 2023 west2online. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | 11 | protocol StatusItemViewProtocol: AnyObject { 12 | func updateViewStatus(enableProxy: Bool) 13 | func updateSpeedLabel(up: Int, down: Int) 14 | func showSpeedContainer(show: Bool) 15 | func updateSize(width: CGFloat) 16 | } 17 | -------------------------------------------------------------------------------- /ClashX/Basic/UnsafePointer+bridge.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnsafePointer+bridge.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2019/10/31. 6 | // Copyright © 2019 west2online. All rights reserved. 7 | // 8 | 9 | func bridge(obj: T) -> UnsafeMutableRawPointer { 10 | return UnsafeMutableRawPointer(Unmanaged.passUnretained(obj).toOpaque()) 11 | } 12 | 13 | func bridge(ptr: UnsafeRawPointer) -> T { 14 | return Unmanaged.fromOpaque(ptr).takeUnretainedValue() 15 | } 16 | -------------------------------------------------------------------------------- /ClashX/Images.xcassets/icon_connection_done.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_connected.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "icon_connected@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ClashX/Images.xcassets/icon_connection_inprogress.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_connecting.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "icon_connecting@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ClashX/Images.xcassets/icon_connection_fail.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_connection_fail.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "icon_connection_fail@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ProxyConfigHelper/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // ProxyConfigHelper 4 | // 5 | // Created by yichengchen on 2019/8/16. 6 | // Copyright © 2019 west2online. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "ProxyConfigHelper.h" 11 | int main(int argc, const char * argv[]) { 12 | @autoreleasepool { 13 | [[NSProcessInfo processInfo] disableSuddenTermination]; 14 | [[ProxyConfigHelper new] run]; 15 | NSLog(@"ProxyConfigHelper exit"); 16 | } 17 | return 0; 18 | } 19 | -------------------------------------------------------------------------------- /ClashX/Extensions/NSAlert+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSAlert+Extension.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2019/1/11. 6 | // Copyright © 2019 west2online. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | extension NSAlert { 12 | static func alert(with text: String) { 13 | let alert = NSAlert() 14 | alert.messageText = text 15 | alert.alertStyle = .warning 16 | alert.addButton(withTitle: NSLocalizedString("OK", comment: "")) 17 | alert.runModal() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ProxyConfigHelper/Helper-Launchd.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AssociatedBundleIdentifiers 6 | com.west2online.ClashX 7 | Label 8 | com.west2online.ClashX.ProxyConfigHelper 9 | MachServices 10 | 11 | com.west2online.ClashX.ProxyConfigHelper 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /ClashX/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // ClashX 4 | // 5 | // Created by 称一称 on 2018/6/10. 6 | // Copyright © 2018年 west2online. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ViewController: NSViewController { 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | run() 15 | // Do any additional setup after loading the view. 16 | } 17 | 18 | override var representedObject: Any? { 19 | didSet { 20 | // Update the view, if already loaded. 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ClashX/Basic/Combine+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Combine+Ext.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2023/7/6. 6 | // Copyright © 2023 west2online. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | @available(macOS 10.15, *) 13 | public extension Publisher where Failure == Never { 14 | func weakAssign( 15 | to keyPath: ReferenceWritableKeyPath, 16 | on object: T 17 | ) -> AnyCancellable { 18 | sink { [weak object] value in 19 | object?[keyPath: keyPath] = value 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ClashX/Basic/String+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extension.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2018/10/7. 6 | // Copyright © 2018年 west2online. All rights reserved. 7 | // 8 | import Foundation 9 | 10 | extension String { 11 | func isUrlVaild() -> Bool { 12 | guard !isEmpty else { return false } 13 | guard let url = URL(string: self) else { return false } 14 | 15 | guard url.host != nil, 16 | let scheme = url.scheme else { 17 | return false 18 | } 19 | return ["http", "https"].contains(scheme) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ClashX/Macro/Paths.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Paths.swift 3 | // ClashX 4 | // 5 | // Created by CYC on 2018/8/26. 6 | // Copyright © 2018年 west2online. All rights reserved. 7 | // 8 | import Foundation 9 | 10 | let kConfigFolderPath = "\(NSHomeDirectory())/.config/clash/" 11 | 12 | let kDefaultConfigFilePath = "\(kConfigFolderPath)config.yaml" 13 | 14 | enum Paths { 15 | static func localConfigPath(for name: String) -> String { 16 | return "\(kConfigFolderPath)\(configFileName(for: name))" 17 | } 18 | 19 | static func configFileName(for name: String) -> String { 20 | return "\(name).yaml" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ClashX/Vendor/Witness/Witness.h: -------------------------------------------------------------------------------- 1 | // 2 | // Witness.h 3 | // Witness 4 | // 5 | // Created by Niels de Hoog on 23/09/15. 6 | // Copyright © 2015 Invisible Pixel. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | //! Project version number for Witness. 13 | FOUNDATION_EXPORT double WitnessVersionNumber; 14 | 15 | //! Project version string for Witness. 16 | FOUNDATION_EXPORT const unsigned char WitnessVersionString[]; 17 | 18 | // In this header, you should import all the public headers of your framework using statements like #import 19 | 20 | 21 | -------------------------------------------------------------------------------- /ClashX/Resources/sampleConfig.yaml: -------------------------------------------------------------------------------- 1 | #---------------------------------------------------# 2 | ## 配置文件需要放置在 $HOME/.config/clash/*.yaml 3 | 4 | ## 这份文件是clashX的基础配置文件,请尽量新建配置文件进行修改。 5 | ## 端口设置请在 菜单条图标->配置->更多配置 中进行修改 6 | 7 | ## 如果您不知道如何操作,请参阅 官方Github文档 https://dreamacro.github.io/clash/ 8 | #---------------------------------------------------# 9 | 10 | mode: rule 11 | log-level: info 12 | 13 | proxies: 14 | 15 | proxy-groups: 16 | 17 | rules: 18 | - DOMAIN-SUFFIX,google.com,DIRECT 19 | - DOMAIN-KEYWORD,google,DIRECT 20 | - DOMAIN,google.com,DIRECT 21 | - DOMAIN-SUFFIX,ad.com,REJECT 22 | - GEOIP,CN,DIRECT 23 | - MATCH,DIRECT 24 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yaml: -------------------------------------------------------------------------------- 1 | name: ClashX 2 | 3 | on: [ pull_request ] 4 | 5 | env: 6 | FASTLANE_SKIP_UPDATE_CHECK: true 7 | 8 | jobs: 9 | build: 10 | runs-on: macos-13 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - name: setup Go 15 | uses: actions/setup-go@v3 16 | with: 17 | go-version: 1.20.x 18 | 19 | - uses: maxim-lobanov/setup-xcode@v1 20 | with: 21 | xcode-version: latest-stable 22 | 23 | - name: install deps 24 | run: | 25 | bash install_dependency.sh 26 | 27 | - name: check 28 | run: | 29 | bundle exec fastlane check 30 | -------------------------------------------------------------------------------- /ClashX/General/Managers/ConnectionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectionManager.swift 3 | // ClashX 4 | // 5 | // Created by yichengchen on 2019/10/28. 6 | // Copyright © 2019 west2online. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | enum ConnectionManager { 12 | static func closeConnection(for group: String) { 13 | ApiRequest.getConnections { conns in 14 | for conn in conns where conn.chains.contains(group) { 15 | ApiRequest.closeConnection(conn.id) 16 | } 17 | } 18 | } 19 | 20 | static func closeAllConnection() { 21 | ApiRequest.closeAllConnection() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ClashX/Extensions/NSTableView+Reload.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSTableView+Reload.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2019/7/28. 6 | // Copyright © 2019 west2online. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | extension NSTableView { 12 | func reloadDataKeepingSelection() { 13 | let selectedRowIndexes = selectedRowIndexes 14 | reloadData() 15 | var indexs = IndexSet() 16 | for index in selectedRowIndexes { 17 | if index >= 0 && index <= numberOfRows { 18 | indexs.insert(index) 19 | } 20 | } 21 | selectRowIndexes(indexs, byExtendingSelection: false) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ClashX/Vendor/LoginKitWrapper.m: -------------------------------------------------------------------------------- 1 | // 2 | // LoginKitWrapper.m 3 | // ClashX Pro 4 | // 5 | // Created by yicheng on 2022/3/22. 6 | // Copyright © 2022 west2online. All rights reserved. 7 | // 8 | 9 | #import "LoginKitWrapper.h" 10 | #import 11 | @implementation LoginKitWrapper 12 | +(BOOL) setLogin:(LSSharedFileListRef) inlist path:(NSString *) path { 13 | #pragma clang diagnostic push 14 | #pragma clang diagnostic ignored "-Wdeprecated-declarations" 15 | return LSSharedFileListInsertItemURL(inlist, kLSSharedFileListItemLast, nil, nil, (__bridge CFURLRef _Nonnull)([NSURL fileURLWithPath:path]), nil, nil) != nil; 16 | #pragma clang diagnostic pop 17 | } 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /ClashX/AppleScript/ProxySettingCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProxySettingCommand.swift 3 | // ClashXX 4 | // 5 | // Created by Vince-hz on 2022/1/25. 6 | // Copyright © 2022 west2online. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | import Foundation 11 | 12 | @objc class ProxySettingCommand: NSScriptCommand { 13 | override func performDefaultImplementation() -> Any? { 14 | guard let delegate = NSApplication.shared.delegate as? AppDelegate else { 15 | scriptErrorNumber = -2 16 | scriptErrorString = "can't get application, try again later" 17 | return nil 18 | } 19 | delegate.actionSetSystemProxy(self) 20 | return nil 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ClashX/Extensions/AppDelegate+..swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate+.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2019/10/25. 6 | // Copyright © 2019 west2online. All rights reserved. 7 | // 8 | import AppKit 9 | 10 | extension AppDelegate { 11 | static var shared: AppDelegate { 12 | return NSApplication.shared.delegate as! AppDelegate 13 | } 14 | 15 | static var isAboveMacOS14: Bool { 16 | if #available(macOS 10.14, *) { 17 | return true 18 | } 19 | return false 20 | } 21 | 22 | static var isAboveMacOS152: Bool { 23 | if #available(macOS 10.15.3, *) { 24 | return true 25 | } 26 | return false 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ClashX/ViewControllers/Settings/SettingTabViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingTabViewController.swift 3 | // ClashX Pro 4 | // 5 | // Created by yicheng on 2022/11/20. 6 | // Copyright © 2022 west2online. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class SettingTabViewController: NSTabViewController, NibLoadable { 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | tabStyle = .toolbar 15 | if #unavailable(macOS 10.11) { 16 | tabStyle = .segmentedControlOnTop 17 | tabViewItems.forEach { item in 18 | item.image = nil 19 | } 20 | } 21 | NSApp.activate(ignoringOtherApps: true) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.bartycrouch.toml: -------------------------------------------------------------------------------- 1 | [update] 2 | tasks = ["interfaces", "code", "normalize"] 3 | 4 | [update.interfaces] 5 | path = "./ClashX" 6 | defaultToBase = false 7 | ignoreEmptyStrings = false 8 | unstripped = false 9 | 10 | [update.code] 11 | defaultToKeys = false 12 | additive = false 13 | unstripped = false 14 | 15 | [update.transform] 16 | codePath = "./ClashX" 17 | localizablePath = "./ClashX" 18 | transformer = "foundation" 19 | supportedLanguageEnumPath = "./ClashX" 20 | typeName = "BartyCrouch" 21 | translateMethodName = "translate" 22 | 23 | [update.normalize] 24 | path = "./ClashX" 25 | harmonizeWithSource = true 26 | sortByKeys = true 27 | 28 | [lint] 29 | path = "./ClashX" 30 | duplicateKeys = true 31 | emptyValues = true 32 | -------------------------------------------------------------------------------- /ClashX/goClash/UIHelper.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | typedef void (^NStringCallback)(NSString *,NSString *); 4 | typedef void (^IntCallback)(int64_t,int64_t); 5 | extern NStringCallback logCallback; 6 | extern IntCallback trafficCallback; 7 | void clash_setLogBlock(NStringCallback block); 8 | 9 | void clash_setTrafficBlock(IntCallback block); 10 | 11 | static inline void sendLogToUI(char *s, char *level) { 12 | @autoreleasepool { 13 | if (logCallback) { 14 | logCallback([NSString stringWithUTF8String:s], [NSString stringWithUTF8String:level]); 15 | } 16 | } 17 | } 18 | 19 | static inline void sendTrafficToUI(int64_t up, int64_t down) { 20 | if (trafficCallback) { 21 | trafficCallback(up, down); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ClashX/Extensions/DateFormatter+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateFormatter+.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2019/12/14. 6 | // Copyright © 2019 west2online. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | extension DateFormatter { 12 | static var js: DateFormatter { 13 | let dateFormatter = DateFormatter() 14 | dateFormatter.locale = Locale(identifier: NSCalendar.Identifier.ISO8601.rawValue) 15 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ" 16 | return dateFormatter 17 | } 18 | 19 | static var simple: DateFormatter { 20 | let dateFormatter = DateFormatter() 21 | dateFormatter.dateFormat = "MM-dd HH:mm:ss" 22 | return dateFormatter 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /install_dependency.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | echo "Build Clash core" 4 | 5 | cd ClashX/goClash 6 | python3 build_clash_universal.py 7 | cd ../.. 8 | 9 | echo "Pod install" 10 | bundle install --jobs 4 11 | bundle exec pod install 12 | echo "delete old files" 13 | rm -f ./ClashX/Resources/Country.mmdb 14 | rm -rf ./ClashX/Resources/dashboard 15 | rm -f GeoLite2-Country.* 16 | echo "install mmdb" 17 | curl -LO https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb 18 | gzip Country.mmdb 19 | mv Country.mmdb.gz ./ClashX/Resources/Country.mmdb.gz 20 | echo "install dashboard" 21 | cd ClashX/Resources 22 | git clone -b gh-pages https://github.com/Dreamacro/clash-dashboard.git dashboard 23 | cd dashboard 24 | rm -rf *.webmanifest *.js CNAME .git 25 | -------------------------------------------------------------------------------- /ClashX/Models/ClashRule.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClashRule.swift 3 | // ClashX 4 | // 5 | // Created by CYC on 2018/10/27. 6 | // Copyright © 2018 west2online. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class ClashRule: Codable { 12 | let type: String 13 | let payload: String? 14 | let proxy: String? 15 | } 16 | 17 | class ClashRuleResponse: Codable { 18 | var rules: [ClashRule]? 19 | 20 | static func empty() -> ClashRuleResponse { 21 | return ClashRuleResponse() 22 | } 23 | 24 | static func fromData(_ data: Data) -> ClashRuleResponse { 25 | let decoder = JSONDecoder() 26 | let model = try? decoder.decode(ClashRuleResponse.self, from: data) 27 | return model ?? ClashRuleResponse.empty() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ClashX/Extensions/Array+Safe.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+Safe.swift 3 | // MTEve 4 | // 5 | // Created by CYC on 2019/6/5. 6 | // Copyright © 2019 meitu. All rights reserved. 7 | // 8 | 9 | extension Collection { 10 | /// Returns the element at the specified index iff it is within bounds, otherwise nil. 11 | subscript(safe index: Index) -> Element? { 12 | if indices.contains(index) { 13 | return self[index] 14 | } else { 15 | return nil 16 | } 17 | } 18 | } 19 | 20 | extension Array { 21 | @discardableResult 22 | mutating func safeRemove(at index: Index) -> Bool { 23 | if indices.contains(index) { 24 | remove(at: index) 25 | return true 26 | } else { 27 | return false 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Shortcuts.md: -------------------------------------------------------------------------------- 1 | # 全局快捷键 2 | 3 | ClashX的全局快捷键是通过支持 AppleScript,并以系统的 Automator 程序调用 AppleScript 来完成全局快捷键的实现。 4 | 5 | ClashX目前仅支持以下功能的AppleScript 6 | 7 | 1. 打开(关闭)系统代理 8 | 2. 切换出站模式 9 | 10 | ## 通过 Automator 创建全局快捷键 11 | 12 | [Mac新建全局快捷键](https://www.jianshu.com/p/afee9aeb41a8) 13 | 14 | ## 可用的 AppleScript 15 | 16 | 你可以在这里选择你需要的 AppleScript 代码,以此创建你需要的快捷键。 17 | 18 | **以下示例代码为ClashX程序。如果你正在用ClashX Pro,那么请将ClashX替换为 ClashX Pro** 19 | 20 | --- 21 | 22 | 打开(关闭)系统代理 23 | 24 | `tell application "ClashX" to toggleProxy` 25 | 26 | 切换出站模式为全局代理 27 | 28 | `tell application "ClashX" to proxyMode 'global'` 29 | 30 | 切换出站模式为直连 31 | 32 | `tell application "ClashX" to proxyMode 'direct'` 33 | 34 | 切换出站模式为规则代理 35 | 36 | `tell application "ClashX" to proxyMode 'rule'` 37 | 38 | ## 已知缺陷 39 | 40 | 1. 无法直接在桌面使用快捷键,你需要进入任意程序中才能启动快捷键 41 | 42 | 2. 在任何程序中第一次启用该快捷键都要点击一次确认授权才能启动快捷键 -------------------------------------------------------------------------------- /ProxyConfigHelper/CommonUtils.m: -------------------------------------------------------------------------------- 1 | // 2 | // CommonUtils.m 3 | // ClashX 4 | // 5 | // Created by yicheng on 2020/4/2. 6 | // Copyright © 2020 west2online. All rights reserved. 7 | // 8 | 9 | #import "CommonUtils.h" 10 | 11 | @implementation CommonUtils 12 | + (NSString *)runCommand:(NSString *)path args:(nullable NSArray *)args { 13 | NSTask *task = [[NSTask alloc] init]; 14 | [task setLaunchPath:path]; 15 | [task setArguments:args]; 16 | 17 | NSPipe *pipe = [NSPipe pipe]; 18 | [task setStandardOutput: pipe]; 19 | 20 | NSFileHandle *file = [pipe fileHandleForReading]; 21 | 22 | [task launch]; 23 | 24 | NSData *data = [file readDataToEndOfFile]; 25 | NSString *output = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding]; 26 | #if DEBUG 27 | NSLog(@"%@",output); 28 | #endif 29 | return output; 30 | } 31 | @end 32 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # By default, SwiftLint uses a set of sensible default rules you can adjust: 2 | disabled_rules: # rule identifiers turned on by default to exclude from running 3 | - colon 4 | - identifier_name 5 | - force_cast 6 | - closure_parameter_position 7 | - file_length 8 | - large_tuple 9 | - type_body_length 10 | - cyclomatic_complexity 11 | - function_body_length 12 | - nesting 13 | opt_in_rules: # some rules are turned off by default, so you need to opt-in 14 | - empty_count 15 | - empty_string 16 | included: # paths to include during linting. `--path` is ignored if present. 17 | - ClashX 18 | excluded: # paths to ignore during linting. Takes precedence over `included`. 19 | - ClashX/Vendor 20 | - Pods 21 | analyzer_rules: # Rules run by `swiftlint analyze` 22 | - explicit_self 23 | # implicitly 24 | line_length: 300 25 | # reporter: "xcode" 26 | -------------------------------------------------------------------------------- /ClashX/AppleScript/ProxySetting.sdef: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /ClashX/General/Utils/Command.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Command.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2023/10/13. 6 | // Copyright © 2023 west2online. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Command { 12 | let cmd: String 13 | let args: [String] 14 | 15 | func run() -> String { 16 | var output = "" 17 | 18 | let task = Process() 19 | task.launchPath = cmd 20 | task.arguments = args 21 | 22 | let outpipe = Pipe() 23 | task.standardOutput = outpipe 24 | 25 | task.launch() 26 | 27 | task.waitUntilExit() 28 | let outdata = outpipe.fileHandleForReading.readDataToEndOfFile() 29 | if var string = String(data: outdata, encoding: .utf8) { 30 | output = string.trimmingCharacters(in: .newlines) 31 | } 32 | return output 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ClashX/Basic/LaunchAtLogin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LaunchAtLogin.swift 3 | // ClashX 4 | // 5 | // Created by CYC on 2018/6/14. 6 | // Copyright © 2018年 yichengchen. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxCocoa 11 | import RxSwift 12 | import ServiceManagement 13 | 14 | public class LaunchAtLogin { 15 | static let shared = LaunchAtLogin() 16 | 17 | private init() { 18 | isEnableVirable.accept(isEnabled) 19 | } 20 | 21 | public var isEnabled: Bool { 22 | get { 23 | return LoginServiceKit.isExistLoginItems() 24 | } 25 | set { 26 | if newValue { 27 | LoginServiceKit.addLoginItems() 28 | } else { 29 | LoginServiceKit.removeLoginItems() 30 | } 31 | isEnableVirable.accept(newValue) 32 | } 33 | } 34 | 35 | var isEnableVirable = BehaviorRelay(value: false) 36 | } 37 | -------------------------------------------------------------------------------- /ClashX/Basic/SpeedUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpeedUtils.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2023/7/6. 6 | // Copyright © 2023 west2online. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum SpeedUtils { 12 | static func getSpeedString(for byte: Int) -> String { 13 | return getNetString(for: byte).appending("/s") 14 | } 15 | 16 | static func getNetString(for byte: Int) -> String { 17 | let kb = byte / 1024 18 | if kb < 1024 { 19 | return "\(kb)KB" 20 | } else { 21 | let mb = Double(kb) / 1024.0 22 | if mb >= 100 { 23 | if mb >= 1000 { 24 | return String(format: "%.1fGB", mb / 1024) 25 | } 26 | return String(format: "%.1fMB", mb) 27 | } else { 28 | return String(format: "%.2fMB", mb) 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ClashX/Macro/Notification.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Notification.swift 3 | // ClashX 4 | // 5 | // Created by CYC on 2018/8/4. 6 | // Copyright © 2018年 yichengchen. All rights reserved. 7 | // 8 | import Foundation 9 | 10 | extension Notification.Name { 11 | static let configFileChange = Notification.Name("kConfigFileChange") 12 | static let speedTestFinishForProxy = Notification.Name("kSpeedTestFinishForProxy") 13 | static let reloadDashboard = Notification.Name("kReloadDashboard") 14 | static let systemNetworkStatusIPUpdate = Notification.Name("systemNetworkStatusIPUpdate") 15 | static let systemNetworkStatusDidChange = Notification.Name("kSystemNetworkStatusDidChange") 16 | static let proxyMeneViewShowLeftPadding = Notification.Name("kProxyMeneViewShowLeftPadding") 17 | 18 | static func proxyUpdate(for name: ClashProxyName) -> Notification.Name { 19 | return Notification.Name("kProxyUpdate_\(name)") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ClashX/goClash/upgrade_core.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from build_clash_universal import run 3 | 4 | 5 | def upgrade_version(current_version): 6 | string = open('go.mod').read() 7 | string = string.replace(current_version, "dev") 8 | file = open("go.mod", "w") 9 | file.write(string) 10 | 11 | 12 | def get_full_version(): 13 | with open('./go.mod') as file: 14 | for line in file.readlines(): 15 | if "clash" in line and "ClashX" not in line: 16 | return line.split(" ")[-1].strip() 17 | 18 | def install(): 19 | subprocess.check_output("go mod download", shell=True) 20 | subprocess.check_output("go mod tidy", shell=True) 21 | 22 | 23 | if __name__ == '__main__': 24 | print("start") 25 | current = get_full_version() 26 | print("current version:", current) 27 | upgrade_version(current) 28 | install() 29 | new_version = get_full_version() 30 | print("new version:", new_version, ",start building") 31 | run() 32 | -------------------------------------------------------------------------------- /ProxyConfigHelper/ProxySettingTool.h: -------------------------------------------------------------------------------- 1 | // 2 | // ProxySettingTool.h 3 | // com.west2online.ClashX.ProxyConfigHelper 4 | // 5 | // Created by yichengchen on 2019/8/17. 6 | // Copyright © 2019 west2online. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface ProxySettingTool : NSObject 14 | 15 | - (void)enableProxyWithport:(int)port socksPort:(int)socksPort 16 | pacUrl:(NSString *)pacUrl 17 | filterInterface:(BOOL)filterInterface 18 | ignoreList:(NSArray*)ignoreList; 19 | 20 | - (void)disableProxyWithfilterInterface:(BOOL)filterInterFace; 21 | 22 | - (void)restoreProxySetting:(NSDictionary *)savedInfo 23 | currentPort:(int)port 24 | currentSocksPort:(int)socksPort 25 | filterInterface:(BOOL)filterInterface; 26 | + (NSMutableDictionary *)currentProxySettings; 27 | 28 | @end 29 | 30 | NS_ASSUME_NONNULL_END 31 | -------------------------------------------------------------------------------- /ClashX/Actions/UpdateConfigAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateConfigAction.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2023/9/5. 6 | // Copyright © 2023 west2online. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | import Foundation 11 | 12 | enum UpdateConfigAction { 13 | static func showError(text: String, configName: String) { 14 | let alert = NSAlert() 15 | alert.alertStyle = .critical 16 | alert.messageText = NSLocalizedString("Reload Config Fail", comment: "") 17 | alert.informativeText = text 18 | alert.addButton(withTitle: NSLocalizedString("Edit in Text Mode", comment: "")) 19 | alert.addButton(withTitle: NSLocalizedString("OK", comment: "")) 20 | NSApp.activate(ignoringOtherApps: true) 21 | if alert.runModal() == .alertFirstButtonReturn { 22 | ConfigManager.getConfigPath(configName: configName) { 23 | NSWorkspace.shared.open(URL(fileURLWithPath: $0)) 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ClashX/add_build_info.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import datetime 3 | import plistlib 4 | import os 5 | 6 | 7 | def write_to_info(): 8 | path = "info.plist" 9 | 10 | with open(path, 'rb') as f: 11 | contents = plistlib.load(f) 12 | 13 | if not contents: 14 | exit(-1) 15 | 16 | branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"]).strip().decode() 17 | commit = subprocess.check_output(["git", "describe", "--always"]).strip().decode() 18 | 19 | contents["gitBranch"] = branch 20 | contents["gitCommit"] = commit 21 | contents["buildTime"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M") 22 | 23 | with open(path, 'wb') as f: 24 | plistlib.dump(contents, f, sort_keys=False) 25 | 26 | 27 | def run(): 28 | if os.environ.get("CI", False) or os.environ.get("GITHUB_ACTIONS", False): 29 | print("writing info.plist") 30 | write_to_info() 31 | print("done") 32 | 33 | 34 | if __name__ == "__main__": 35 | run() 36 | -------------------------------------------------------------------------------- /ProxyConfigHelper/Helper-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.west2online.ClashX.ProxyConfigHelper 7 | CFBundleInfoDictionaryVersion 8 | 6.0 9 | CFBundleName 10 | com.west2online.ClashX.ProxyConfigHelper 11 | CFBundleShortVersionString 12 | 2.0 13 | CFBundleVersion 14 | 5 15 | SMAuthorizedClients 16 | 17 | anchor apple generic and identifier "com.west2online.ClashX" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = MEWHFZ92DY) 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ---- 3 | 4 | # Installation 5 | 6 | Make sure you have the latest version of the Xcode command line tools installed: 7 | 8 | ```sh 9 | xcode-select --install 10 | ``` 11 | 12 | For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) 13 | 14 | # Available Actions 15 | 16 | ### build 17 | 18 | ```sh 19 | [bundle exec] fastlane build 20 | ``` 21 | 22 | 23 | 24 | ### check 25 | 26 | ```sh 27 | [bundle exec] fastlane check 28 | ``` 29 | 30 | 31 | 32 | ### beta 33 | 34 | ```sh 35 | [bundle exec] fastlane beta 36 | ``` 37 | 38 | 39 | 40 | ### addKeyChain 41 | 42 | ```sh 43 | [bundle exec] fastlane addKeyChain 44 | ``` 45 | 46 | 47 | 48 | ---- 49 | 50 | This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. 51 | 52 | More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). 53 | 54 | The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 55 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | source 'https://cdn.cocoapods.org/' 2 | 3 | post_install do |installer| 4 | installer.pods_project.targets.each do |target| 5 | target.build_configurations.each do |config| 6 | if ['FlexibleDiff'].include? target.name 7 | target.build_configurations.each do |config| 8 | config.build_settings['SWIFT_VERSION'] = '5' 9 | end 10 | end 11 | if config.build_settings['MACOSX_DEPLOYMENT_TARGET'] == '' || Gem::Version.new(config.build_settings['MACOSX_DEPLOYMENT_TARGET']) < Gem::Version.new("10.14") 12 | config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '10.14' 13 | end 14 | end 15 | end 16 | end 17 | 18 | target 'ClashX' do 19 | inhibit_all_warnings! 20 | use_modular_headers! 21 | pod 'LetsMove' 22 | pod 'Alamofire', '~> 5.0' 23 | pod 'SwiftyJSON' 24 | pod 'RxSwift' 25 | pod 'RxCocoa' 26 | pod 'CocoaLumberjack/Swift' 27 | pod 'Starscream','3.1.1' 28 | pod 'AppCenter/Analytics' 29 | pod 'AppCenter/Crashes' 30 | pod 'Sparkle','~>2.0' 31 | pod "FlexibleDiff" 32 | pod 'GzipSwift' 33 | pod 'SwiftLint' 34 | pod 'SwiftFormat/CLI', '~> 0.49' 35 | end 36 | 37 | -------------------------------------------------------------------------------- /ClashX/Extensions/Cgo+Convert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Cgo+Convert.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2019/10/2. 6 | // Copyright © 2019 west2online. All rights reserved. 7 | // 8 | 9 | extension String { 10 | func goStringBuffer() -> UnsafeMutablePointer { 11 | if let pointer = (self as NSString).utf8String { 12 | return UnsafeMutablePointer(mutating: pointer) 13 | } 14 | Logger.log("Convert goStringBuffer Fail!!!!", level: .error) 15 | let p = ("" as NSString).utf8String! 16 | return UnsafeMutablePointer(mutating: p) 17 | } 18 | } 19 | 20 | extension UnsafeMutablePointer where Pointee == Int8 { 21 | func toString() -> String { 22 | let string = String(cString: self) 23 | deallocate() 24 | return string 25 | } 26 | 27 | func toData() -> Data { 28 | return toString().data(using: .utf8) ?? Data() 29 | } 30 | } 31 | 32 | extension Bool { 33 | func goObject() -> GoUint8 { 34 | return self == true ? 1 : 0 35 | } 36 | } 37 | 38 | extension GoUint8 { 39 | func toBool() -> Bool { 40 | return self == 1 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ClashX/ViewControllers/Connections/Views/ConnectionDetailInfoGeneralView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectionDetailInfoGeneralView.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2023/7/8. 6 | // Copyright © 2023 west2online. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ConnectionDetailInfoGeneralView: NSView, NibLoadable { 12 | @IBOutlet var entryLabel: NSTextField! 13 | @IBOutlet var networkTypeLabel: NSTextField! 14 | @IBOutlet var totalUploadLabel: NSTextField! 15 | @IBOutlet var totalDownloadLabel: NSTextField! 16 | @IBOutlet var maxUploadLabel: NSTextField! 17 | @IBOutlet var maxDownloadLabel: NSTextField! 18 | @IBOutlet var currentUploadLabel: NSTextField! 19 | @IBOutlet var currentDownloadLabel: NSTextField! 20 | 21 | @IBOutlet var ruleLabel: NSTextField! 22 | @IBOutlet var proxyChainLabel: NSTextField! 23 | @IBOutlet var otherTextView: NSTextView! 24 | @IBOutlet var sourceIpLabel: NSTextField! 25 | @IBOutlet var destLabel: NSTextField! 26 | override func awakeFromNib() { 27 | super.awakeFromNib() 28 | otherTextView.backgroundColor = NSColor.clear 29 | otherTextView.font = NSFont.systemFont(ofSize: 10) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ClashX/Vendor/Witness/Witness.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Witness.swift 3 | // Witness 4 | // 5 | // Created by Niels de Hoog on 23/09/15. 6 | // Copyright © 2015 Invisible Pixel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public typealias FileEventHandler = (_ events: [FileEvent]) -> Void 12 | 13 | public struct Witness { 14 | private let stream: EventStream 15 | var paths: [String] { 16 | return stream.paths 17 | } 18 | 19 | public init(paths: [String], flags: EventStreamCreateFlags = .None, latency: TimeInterval = 1.0, changeHandler: @escaping FileEventHandler) { 20 | stream = EventStream(paths: paths, flags: flags, latency: latency, changeHandler: changeHandler) 21 | } 22 | 23 | public init(paths: [String], streamType: StreamType, flags: EventStreamCreateFlags = .None, latency: TimeInterval = 1.0, deviceToWatch: dev_t, changeHandler: @escaping FileEventHandler) { 24 | stream = EventStream(paths: paths, type: streamType, flags: flags, latency: latency, deviceToWatch: deviceToWatch, changeHandler: changeHandler) 25 | } 26 | 27 | public func flush() { 28 | stream.flush() 29 | } 30 | 31 | public func flushAsync() { 32 | stream.flushAsync() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ClashX/General/Utils/ClashStatusTool.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClashStatusTool.swift 3 | // ClashX Pro 4 | // 5 | // Created by yicheng on 2020/4/28. 6 | // Copyright © 2020 west2online. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ClashStatusTool { 12 | static func checkPortConfig(cfg: ClashConfig?) { 13 | guard ConfigManager.shared.isRunning else { return } 14 | guard let cfg = cfg else { return } 15 | if cfg.usedHttpPort == 0 { 16 | Logger.log("checkPortConfig: \(cfg.mixedPort) ", level: .error) 17 | let alert = NSAlert() 18 | alert.messageText = NSLocalizedString("ClashX Start Error!", comment: "") 19 | alert.informativeText = NSLocalizedString("Ports Open Fail, Please try to restart ClashX", comment: "") 20 | alert.addButton(withTitle: NSLocalizedString("Quit", comment: "")) 21 | alert.addButton(withTitle: "Edit Config") 22 | DispatchQueue.main.async { 23 | let ret = alert.runModal() 24 | if ret == .alertSecondButtonReturn { 25 | NSWorkspace.shared.openFile(Paths.localConfigPath(for: "config")) 26 | } 27 | NSApp.terminate(nil) 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ClashX/Views/StatusItem/StatusItemTool.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusItemTool.swift 3 | // ClashX Pro 4 | // 5 | // Created by yicheng on 2023/3/1. 6 | // Copyright © 2023 west2online. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | 11 | enum StatusItemTool { 12 | static let menuImage: NSImage = { 13 | let customImagePath = (NSHomeDirectory() as NSString).appendingPathComponent("/.config/clash/menuImage.png") 14 | if let image = NSImage(contentsOfFile: customImagePath) { 15 | image.isTemplate = true 16 | return image 17 | } 18 | if let imagePath = Bundle.main.path(forResource: "menu_icon@2x", ofType: "png"), 19 | let image = NSImage(contentsOfFile: imagePath) { 20 | image.isTemplate = true 21 | return image 22 | } 23 | return NSImage() 24 | }() 25 | 26 | static let font: NSFont = { 27 | let fontSize: CGFloat = 9 28 | let font: NSFont 29 | if let fontName = UserDefaults.standard.string(forKey: "kStatusMenuFontName"), 30 | let f = NSFont(name: fontName, size: fontSize) { 31 | font = f 32 | } else { 33 | font = NSFont.menuBarFont(ofSize: fontSize) 34 | } 35 | return font 36 | }() 37 | } 38 | -------------------------------------------------------------------------------- /ClashX/Views/ProxyGroupMenu.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProxyGroupMenu.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2020/2/22. 6 | // Copyright © 2020 west2online. All rights reserved. 7 | // 8 | import AppKit 9 | 10 | @objc protocol ProxyGroupMenuHighlightDelegate: AnyObject { 11 | func highlight(item: NSMenuItem?) 12 | } 13 | 14 | class ProxyGroupMenu: NSMenu { 15 | var highlightDelegates = NSHashTable.weakObjects() 16 | 17 | override init(title: String) { 18 | super.init(title: title) 19 | delegate = self 20 | } 21 | 22 | required init(coder: NSCoder) { 23 | super.init(coder: coder) 24 | } 25 | 26 | func add(delegate: ProxyGroupMenuHighlightDelegate) { 27 | highlightDelegates.add(delegate) 28 | } 29 | 30 | func remove(_ delegate: ProxyGroupMenuHighlightDelegate) { 31 | highlightDelegates.remove(delegate) 32 | } 33 | } 34 | 35 | extension ProxyGroupMenu: NSMenuDelegate { 36 | func menuDidClose(_ menu: NSMenu) { 37 | highlightDelegates.allObjects.forEach { $0.highlight(item: nil) } 38 | } 39 | 40 | func menu(_ menu: NSMenu, willHighlight item: NSMenuItem?) { 41 | highlightDelegates.allObjects.forEach { $0.highlight(item: item) } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | lane :build do 2 | 3 | addKeyChain 4 | 5 | build_app( 6 | workspace: "ClashX.xcworkspace", 7 | scheme: "ClashX", 8 | export_method: "developer-id", 9 | skip_package_pkg: "false", 10 | clean: "true", 11 | derived_data_path: "./build_derived_data" 12 | ) 13 | end 14 | 15 | lane :check do 16 | build_app( 17 | workspace: "ClashX.xcworkspace", 18 | scheme: "ClashX", 19 | codesigning_identity: "-", 20 | export_method: "developer-id", 21 | skip_package_pkg: "true", 22 | clean: "true", 23 | derived_data_path: "./build_derived_data" 24 | ) 25 | end 26 | 27 | lane :addKeyChain do 28 | if is_ci? 29 | 30 | puts("create custom keychain") 31 | 32 | delete_keychain( 33 | name: "clashBuild" 34 | ) if File.exist? File.expand_path("~/Library/Keychains/clashBuild-db") 35 | 36 | create_keychain( 37 | name: "clashBuild", 38 | default_keychain: false, 39 | unlock: true, 40 | timeout: 3600, 41 | lock_when_sleeps: false, 42 | password: "password" 43 | ) 44 | 45 | import_certificate( 46 | certificate_path: ".github/certs/dist.p12", 47 | keychain_name:"clashBuild", 48 | keychain_password:"password", 49 | certificate_password:"" 50 | ) 51 | end 52 | end -------------------------------------------------------------------------------- /ClashX/Models/ClashProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClashProvider.swift 3 | // ClashX 4 | // 5 | // Created by yichengchen on 2019/12/14. 6 | // Copyright © 2019 west2online. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ClashProviderResp: Codable { 12 | let allProviders: [ClashProxyName: ClashProvider] 13 | lazy var providers: [ClashProxyName: ClashProvider] = allProviders.filter { $0.value.vehicleType != .Compatible } 14 | 15 | init() { 16 | allProviders = [:] 17 | } 18 | 19 | static var decoder: JSONDecoder { 20 | let decoder = JSONDecoder() 21 | decoder.dateDecodingStrategy = .formatted(DateFormatter.js) 22 | return decoder 23 | } 24 | 25 | private enum CodingKeys: String, CodingKey { 26 | case allProviders = "providers" 27 | } 28 | } 29 | 30 | class ClashProvider: Codable { 31 | enum ProviderType: String, Codable { 32 | case Proxy 33 | case String 34 | } 35 | 36 | enum ProviderVehicleType: String, Codable { 37 | case HTTP 38 | case File 39 | case Compatible 40 | case Unknown 41 | } 42 | 43 | let name: ClashProviderName 44 | let proxies: [ClashProxy] 45 | let type: ProviderType 46 | let vehicleType: ProviderVehicleType 47 | } 48 | -------------------------------------------------------------------------------- /ClashX/Models/RemoteConfigModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RemoteConfigModel.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2019/7/28. 6 | // Copyright © 2019 west2online. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class RemoteConfigModel: Codable { 12 | var url: String 13 | var name: String 14 | var updateTime: Date? 15 | var updating = false 16 | var isPlaceHolderName = false 17 | 18 | init(url: String, name: String, updateTime: Date? = nil) { 19 | self.url = url 20 | self.name = name 21 | self.updateTime = updateTime 22 | } 23 | 24 | private enum CodingKeys: String, CodingKey { 25 | case url, name, updateTime 26 | } 27 | 28 | func displayingTimeString() -> String { 29 | if updating { return NSLocalizedString("Updating", comment: "") } 30 | let dateFormater = DateFormatter() 31 | dateFormater.dateFormat = "MM-dd HH:mm" 32 | if let date = updateTime { 33 | return dateFormater.string(from: date) 34 | } 35 | return NSLocalizedString("Never", comment: "") 36 | } 37 | } 38 | 39 | extension RemoteConfigModel: Equatable { 40 | static func == (lhs: RemoteConfigModel, rhs: RemoteConfigModel) -> Bool { 41 | return lhs.name == rhs.name && lhs.url == rhs.url 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ClashX/Extensions/NSTextField+Vibrancy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSTextField+Vibrancy.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2019/11/1. 6 | // Copyright © 2019 west2online. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class VibrancyTextField: NSTextField { 12 | private var _allowsVibrancy = true 13 | override var allowsVibrancy: Bool { 14 | return _allowsVibrancy 15 | } 16 | 17 | func setup(allowsVibrancy: Bool) -> Self { 18 | _allowsVibrancy = allowsVibrancy 19 | return self 20 | } 21 | } 22 | 23 | class PaddedNSTextFieldCell: NSTextFieldCell { 24 | var widthPadding: CGFloat = 0 25 | var heightPadding: CGFloat = 0 26 | 27 | override func cellSize(forBounds rect: NSRect) -> NSSize { 28 | var size = super.cellSize(forBounds: rect) 29 | size.width += (widthPadding * 2) 30 | size.height += (heightPadding * 2) 31 | return size 32 | } 33 | 34 | override func titleRect(forBounds rect: NSRect) -> NSRect { 35 | return rect.insetBy(dx: widthPadding, dy: heightPadding) 36 | } 37 | 38 | override func drawInterior(withFrame cellFrame: NSRect, in controlView: NSView) { 39 | let rect = cellFrame.insetBy(dx: widthPadding, dy: heightPadding) 40 | super.drawInterior(withFrame: rect, in: controlView) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ProxyConfigHelper/ProxyConfigRemoteProcessProtocol.h: -------------------------------------------------------------------------------- 1 | // 2 | // ProxyConfigRemoteProcessProtocol.h 3 | // com.west2online.ClashX.ProxyConfigHelper 4 | // 5 | // Created by yichengchen on 2019/8/17. 6 | // Copyright © 2019 west2online. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | typedef void(^stringReplyBlock)(NSString *); 12 | typedef void(^boolReplyBlock)(BOOL); 13 | typedef void(^dictReplyBlock)(NSDictionary *); 14 | 15 | @protocol ProxyConfigRemoteProcessProtocol 16 | @required 17 | 18 | - (void)getVersion:(stringReplyBlock)reply; 19 | 20 | - (void)enableProxyWithPort:(int)port 21 | socksPort:(int)socksPort 22 | pac:(NSString *)pac 23 | filterInterface:(BOOL)filterInterface 24 | ignoreList:(NSArray*)ignoreList 25 | error:(stringReplyBlock)reply; 26 | 27 | - (void)disableProxyWithFilterInterface:(BOOL)filterInterface 28 | reply:(stringReplyBlock)reply; 29 | 30 | - (void)restoreProxyWithCurrentPort:(int)port 31 | socksPort:(int)socksPort 32 | info:(NSDictionary *)dict 33 | filterInterface:(BOOL)filterInterface 34 | error:(stringReplyBlock)reply; 35 | 36 | - (void)getCurrentProxySetting:(dictReplyBlock)reply; 37 | @end 38 | -------------------------------------------------------------------------------- /ClashX/Basic/NSView+Layout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSView+Layout.swift 3 | // BilibiliLive 4 | // 5 | // Created by Etan Chen on 2021/4/4. 6 | // 7 | 8 | import AppKit 9 | 10 | extension NSView { 11 | @discardableResult 12 | func makeConstraints(_ block: (NSView) -> [NSLayoutConstraint]) -> Self { 13 | translatesAutoresizingMaskIntoConstraints = false 14 | NSLayoutConstraint.activate(block(self)) 15 | return self 16 | } 17 | 18 | @discardableResult 19 | func makeConstraintsToBindToSuperview(_ inset: NSEdgeInsets = .init(top: 0, left: 0, bottom: 0, right: 0)) -> Self { 20 | return makeConstraints { [ 21 | $0.leftAnchor.constraint(equalTo: $0.superview!.leftAnchor, constant: inset.left), 22 | $0.rightAnchor.constraint(equalTo: $0.superview!.rightAnchor, constant: -inset.right), 23 | $0.topAnchor.constraint(equalTo: $0.superview!.topAnchor, constant: inset.top), 24 | $0.bottomAnchor.constraint(equalTo: $0.superview!.bottomAnchor, constant: -inset.bottom) 25 | ] } 26 | } 27 | 28 | @discardableResult 29 | func makeConstraintsBindToCenterOfSuperview() -> Self { 30 | return makeConstraints { [ 31 | $0.centerXAnchor.constraint(equalTo: $0.superview!.centerXAnchor), 32 | $0.centerYAnchor.constraint(equalTo: $0.superview!.centerYAnchor) 33 | ] } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ClashX/Extensions/NSView+Nib.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSView+Nib.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2019/7/28. 6 | // Copyright © 2019 west2online. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | protocol NibLoadable { 12 | static var nibName: String? { get } 13 | static func createFromNib(in bundle: Bundle) -> Self 14 | } 15 | 16 | extension NibLoadable where Self: NSView { 17 | static var nibName: String? { 18 | return String(describing: Self.self) 19 | } 20 | 21 | static func createFromNib(in bundle: Bundle = Bundle.main) -> Self { 22 | guard let nibName = nibName else { fatalError() } 23 | var topLevelArray: NSArray? 24 | bundle.loadNibNamed(NSNib.Name(nibName), owner: self, topLevelObjects: &topLevelArray) 25 | guard let results = topLevelArray else { fatalError() } 26 | let views = [Any](results).filter { $0 is Self } 27 | return views.last as! Self 28 | } 29 | } 30 | 31 | extension NibLoadable where Self: NSViewController { 32 | static var nibName: String? { 33 | return String(describing: Self.self) 34 | } 35 | 36 | static func createFromNib(in bundle: Bundle = Bundle.main) -> Self { 37 | guard let nibName = nibName else { fatalError() } 38 | let sb = NSStoryboard(name: "Main", bundle: Bundle.main) 39 | return sb.instantiateController(withIdentifier: nibName) as! Self 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ClashX/Models/SavedProxyModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SavedProxyModel.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2019/11/1. 6 | // Copyright © 2019 west2online. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | struct SavedProxyModel: Codable { 12 | let group: ClashProxyName 13 | let selected: ClashProxyName 14 | let config: String 15 | 16 | var key: String { 17 | return "\(group)_\(config)" 18 | } 19 | 20 | static let key = "SavedProxyModels" 21 | 22 | static func loadsFromUserDefault() -> [SavedProxyModel] { 23 | if let data = UserDefaults.standard.object(forKey: key) as? Data, 24 | let models = try? JSONDecoder().decode([SavedProxyModel].self, from: data) { 25 | var set = Set() 26 | let filtered = models.filter { model in 27 | let pass = !set.contains(model.key) 28 | set.insert(model.key) 29 | return pass 30 | } 31 | return filtered 32 | } 33 | return [] 34 | } 35 | 36 | static func save(_ models: [SavedProxyModel]) { 37 | do { 38 | let data = try JSONEncoder().encode(models) 39 | UserDefaults.standard.set(data, forKey: key) 40 | } catch let err { 41 | Logger.log("save model fail,\(err)", level: .error) 42 | assertionFailure() 43 | } 44 | } 45 | } 46 | 47 | extension SavedProxyModel: Equatable {} 48 | -------------------------------------------------------------------------------- /ClashX/AppleScript/ProxyModeChangeCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProxyModeChangeCommand.swift 3 | // ClashX 4 | // 5 | // Created by Vince-hz on 2022/1/25. 6 | // Copyright © 2022 west2online. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | import Foundation 11 | 12 | @objc class ProxyModeChangeCommand: NSScriptCommand { 13 | override func performDefaultImplementation() -> Any? { 14 | guard let directParameter = directParameter as? String, 15 | let mode = ClashProxyMode(rawValue: directParameter) 16 | else { 17 | scriptErrorNumber = -1 18 | scriptErrorString = "please enter a valid parameter. rule, global or direct" 19 | return nil 20 | } 21 | guard let delegate = NSApplication.shared.delegate as? AppDelegate else { 22 | scriptErrorNumber = -2 23 | scriptErrorString = "can't get application, try again later" 24 | return nil 25 | } 26 | let menuItem: NSMenuItem 27 | switch mode { 28 | case .rule: 29 | menuItem = delegate.proxyModeRuleMenuItem 30 | case .global: 31 | menuItem = delegate.proxyModeGlobalMenuItem 32 | case .direct: 33 | menuItem = delegate.proxyModeDirectMenuItem 34 | #if PRO_VERSION 35 | case .script: 36 | menuItem = delegate.proxyModeScriptMenuItem 37 | #endif 38 | } 39 | delegate.actionSwitchProxyMode(menuItem) 40 | return nil 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ClashX/ViewControllers/Connections/Views/Cell/ConnectionStatusIconCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectionStatusIconCellView.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2023/7/6. 6 | // Copyright © 2023 west2online. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | import Combine 11 | 12 | @available(macOS 10.15, *) 13 | class ConnectionStatusIconCellView: NSView, ConnectionCellProtocol { 14 | let imageView = NSImageView() 15 | var cancellable = Set() 16 | override init(frame: CGRect) { 17 | super.init(frame: frame) 18 | setupUI() 19 | } 20 | 21 | required init?(coder: NSCoder) { 22 | super.init(coder: coder) 23 | setupUI() 24 | } 25 | 26 | func setupUI() { 27 | addSubview(imageView) 28 | imageView.makeConstraints { 29 | [$0.heightAnchor.constraint(equalToConstant: 18), 30 | $0.widthAnchor.constraint(equalTo: $0.heightAnchor), 31 | $0.centerYAnchor.constraint(equalTo: centerYAnchor), 32 | $0.leadingAnchor.constraint(equalTo: leadingAnchor)] 33 | } 34 | } 35 | 36 | func setup(with connection: ClashConnectionSnapShot.Connection, type: ConnectionColume) { 37 | cancellable.removeAll() 38 | connection 39 | .$status 40 | .map(\.image) 41 | .weakAssign(to: \.image, on: imageView) 42 | .store(in: &cancellable) 43 | } 44 | 45 | override func prepareForReuse() { 46 | super.prepareForReuse() 47 | cancellable.removeAll() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ClashX/Views/RemoteConfigUpdateIntervalSettingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RemoteConfigUpdateIntervalSettingView.swift 3 | // ClashX Pro 4 | // 5 | // Created by yicheng on 2021/10/4. 6 | // Copyright © 2021 west2online. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | import Foundation 11 | 12 | class RemoteConfigUpdateIntervalSettingView: NSView { 13 | init() { 14 | super.init(frame: .zero) 15 | setup() 16 | } 17 | 18 | @available(*, unavailable) 19 | required init?(coder: NSCoder) { 20 | fatalError("init(coder:) has not been implemented") 21 | } 22 | 23 | let stackView = NSStackView() 24 | let textfield = NSTextField() 25 | func setup() { 26 | stackView.addArrangedSubview(textfield) 27 | stackView.addArrangedSubview(NSTextField(labelWithString: NSLocalizedString("hours", comment: ""))) 28 | addSubview(stackView) 29 | NSLayoutConstraint.activate([ 30 | stackView.topAnchor.constraint(equalTo: topAnchor), 31 | stackView.leadingAnchor.constraint(equalTo: leadingAnchor), 32 | stackView.trailingAnchor.constraint(equalTo: trailingAnchor), 33 | stackView.bottomAnchor.constraint(equalTo: bottomAnchor), 34 | heightAnchor.constraint(equalToConstant: 22), 35 | textfield.widthAnchor.constraint(greaterThanOrEqualToConstant: 50) 36 | ]) 37 | stackView.translatesAutoresizingMaskIntoConstraints = false 38 | textfield.translatesAutoresizingMaskIntoConstraints = false 39 | textfield.stringValue = "\(Int(Settings.configAutoUpdateInterval / 3600))" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ClashX/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_16x16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "icon_16x16@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "icon_32x32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "icon_32x32@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "icon_128x128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "icon_128x128@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "icon_256x256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "icon_256x256@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "icon_512x512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "icon_512x512@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Feature]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 感谢你向 ClashX 提交 Feature Request! 12 | 在提交之前,请确认: 13 | 14 | - [ ] 我已经在 [Issue Tracker](……/) 中找过我要提出的请求 15 | 16 | 请注意,如果你并没有遵照这个 issue template 填写内容,我们将直接关闭这个 issue。 17 | 18 | 26 | 27 | 我都确认过了,我要继续提交。 28 | 29 | ------------------------------------------------------------------ 30 | 31 | 请附上任何可以帮助我们解决这个问题的信息,如果我们收到的信息不足,我们将对这个 issue 加上 *Needs more information* 标记并在收到更多资讯之前关闭 issue。 32 | 33 | 34 | 35 | ### 环境 Environment 36 | 37 | * 使用者的操作系统 (the OS running on the client) 38 | …… 39 | * 网路环境或拓扑 (network conditions/topology) 40 | …… 41 | * iptables,如果适用 (if applicable) 42 | …… 43 | * ISP 有没有进行 DNS 污染 (is your ISP performing DNS pollution?) 44 | …… 45 | * 其他 46 | …… 47 | 48 | ### 说明 Description 49 | 50 | 53 | 54 | ### 可能的解决方案 Possible Solution 55 | 56 | 57 | 58 | 59 | ### 更多信息 More Information 60 | -------------------------------------------------------------------------------- /ClashX/ViewControllers/Connections/Requests/ConnectionsReq.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectionsReq.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2023/7/14. 6 | // Copyright © 2023 west2online. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Starscream 11 | 12 | @available(macOS 10.15, *) 13 | class ConnectionsReq: WebSocketDelegate { 14 | private var socket: WebSocket? 15 | 16 | let decoder = JSONDecoder() 17 | var onSnapshotUpdate: ((ClashConnectionSnapShot) -> Void)? 18 | init() { 19 | if let url = URL(string: ConfigManager.apiUrl.appending("/connections")) { 20 | socket = WebSocket(url: url) 21 | } 22 | for header in ApiRequest.authHeader() { 23 | socket?.request.setValue(header.value, forHTTPHeaderField: header.name) 24 | } 25 | socket?.delegate = self 26 | decoder.dateDecodingStrategy = .formatted(DateFormatter.js) 27 | } 28 | 29 | func connect() { 30 | socket?.connect() 31 | } 32 | 33 | func websocketDidReceiveMessage(socket: WebSocketClient, text: String) { 34 | if let data = text.data(using: .utf8) { 35 | do { 36 | let info = try decoder.decode(ClashConnectionSnapShot.self, from: data) 37 | onSnapshotUpdate?(info) 38 | } catch let err { 39 | Logger.log("decode fail: \(err)", level: .warning) 40 | } 41 | } 42 | } 43 | 44 | func websocketDidConnect(socket: Starscream.WebSocketClient) { 45 | Logger.log("websocketDidConnect") 46 | } 47 | 48 | func websocketDidDisconnect(socket: Starscream.WebSocketClient, error: Error?) { 49 | Logger.log("websocketDidDisconnect: \(String(describing: error))", level: .warning) 50 | } 51 | 52 | func websocketDidReceiveData(socket: Starscream.WebSocketClient, data: Data) {} 53 | } 54 | -------------------------------------------------------------------------------- /ClashX/ViewControllers/Connections/Views/Cell/ConnectionProxyClientCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectionProxyClientCellView.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2023/7/6. 6 | // Copyright © 2023 west2online. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | 11 | @available(macOS 10.15, *) 12 | class ConnectionProxyClientCellView: NSView, ConnectionCellProtocol { 13 | let imageView = NSImageView() 14 | let nameLabel = NSTextField(labelWithString: "") 15 | override init(frame: CGRect) { 16 | super.init(frame: frame) 17 | setupUI() 18 | } 19 | 20 | required init?(coder: NSCoder) { 21 | super.init(coder: coder) 22 | setupUI() 23 | } 24 | 25 | func setupUI() { 26 | addSubview(nameLabel) 27 | addSubview(imageView) 28 | nameLabel.font = NSFont.systemFont(ofSize: 12) 29 | imageView.makeConstraints { 30 | [$0.heightAnchor.constraint(equalToConstant: 18), 31 | $0.widthAnchor.constraint(equalTo: $0.heightAnchor), 32 | $0.centerYAnchor.constraint(equalTo: centerYAnchor), 33 | $0.leadingAnchor.constraint(equalTo: leadingAnchor)] 34 | } 35 | nameLabel.makeConstraints { 36 | [ 37 | $0.centerYAnchor.constraint(equalTo: centerYAnchor), 38 | $0.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 4), 39 | $0.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor) 40 | ] 41 | } 42 | nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) 43 | nameLabel.cell?.truncatesLastVisibleLine = true 44 | } 45 | 46 | func setup(with connection: ClashConnectionSnapShot.Connection, type: ConnectionColume) { 47 | nameLabel.stringValue = connection.metadata.processName ?? NSLocalizedString("Unknown", comment: "") 48 | imageView.image = connection.metadata.processImage 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ClashX/goClash/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yichengchen/clashX/ClashX 2 | 3 | go 1.21 4 | 5 | toolchain go1.21.0 6 | 7 | require ( 8 | github.com/Dreamacro/clash v1.18.1-0.20230911035213-d034a408be42 9 | github.com/oschwald/geoip2-golang v1.9.0 10 | github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 11 | ) 12 | 13 | require ( 14 | github.com/Dreamacro/protobytes v0.0.0-20230617041236-6500a9f4f158 // indirect 15 | github.com/ajg/form v1.5.1 // indirect 16 | github.com/dlclark/regexp2 v1.10.0 // indirect 17 | github.com/go-chi/chi/v5 v5.0.10 // indirect 18 | github.com/go-chi/cors v1.2.1 // indirect 19 | github.com/go-chi/render v1.0.3 // indirect 20 | github.com/gofrs/uuid/v5 v5.0.0 // indirect 21 | github.com/google/go-cmp v0.5.9 // indirect 22 | github.com/gorilla/websocket v1.5.0 // indirect 23 | github.com/insomniacslk/dhcp v0.0.0-20230816195147-b3ca2534940d // indirect 24 | github.com/josharian/native v1.1.0 // indirect 25 | github.com/mdlayher/netlink v1.7.2 // indirect 26 | github.com/mdlayher/socket v0.4.1 // indirect 27 | github.com/miekg/dns v1.1.55 // indirect 28 | github.com/oschwald/maxminddb-golang v1.11.0 // indirect 29 | github.com/pierrec/lz4/v4 v4.1.14 // indirect 30 | github.com/samber/lo v1.38.1 // indirect 31 | github.com/sirupsen/logrus v1.9.3 // indirect 32 | github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect 33 | github.com/vishvananda/netlink v1.2.1-beta.2.0.20230420174744-55c8b9515a01 // indirect 34 | github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect 35 | go.etcd.io/bbolt v1.3.7 // indirect 36 | go.uber.org/atomic v1.11.0 // indirect 37 | golang.org/x/crypto v0.12.0 // indirect 38 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect 39 | golang.org/x/mod v0.8.0 // indirect 40 | golang.org/x/net v0.14.0 // indirect 41 | golang.org/x/sync v0.3.0 // indirect 42 | golang.org/x/sys v0.11.0 // indirect 43 | golang.org/x/text v0.12.0 // indirect 44 | golang.org/x/tools v0.6.0 // indirect 45 | gopkg.in/yaml.v3 v3.0.1 // indirect 46 | ) 47 | -------------------------------------------------------------------------------- /ClashX/Basic/Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logger.swift 3 | // ClashX 4 | // 5 | // Created by CYC on 2018/8/7. 6 | // Copyright © 2018年 yichengchen. All rights reserved. 7 | // 8 | 9 | import CocoaLumberjack 10 | import Foundation 11 | class Logger { 12 | static let shared = Logger() 13 | var fileLogger: DDFileLogger = .init() 14 | 15 | private init() { 16 | #if DEBUG 17 | DDLog.add(DDOSLogger.sharedInstance) 18 | #endif 19 | // default time zone is "UTC" 20 | let dataFormatter = DateFormatter() 21 | dataFormatter.setLocalizedDateFormatFromTemplate("YYYY/MM/dd HH:mm:ss:SSS") 22 | fileLogger.logFormatter = DDLogFileFormatterDefault(dateFormatter: dataFormatter) 23 | fileLogger.rollingFrequency = TimeInterval(60 * 60 * 24) // 24 hours 24 | fileLogger.logFileManager.maximumNumberOfLogFiles = 3 25 | DDLog.add(fileLogger) 26 | dynamicLogLevel = ConfigManager.selectLoggingApiLevel.toDDLogLevel() 27 | } 28 | 29 | private func logToFile(msg: String, level: ClashLogLevel) { 30 | switch level { 31 | case .debug, .silent: 32 | DDLogDebug(DDLogMessageFormat(stringLiteral: msg)) 33 | case .error: 34 | DDLogError(DDLogMessageFormat(stringLiteral: msg)) 35 | case .info: 36 | DDLogInfo(DDLogMessageFormat(stringLiteral: msg)) 37 | case .warning: 38 | DDLogWarn(DDLogMessageFormat(stringLiteral: msg)) 39 | case .unknow: 40 | DDLogWarn(DDLogMessageFormat(stringLiteral: msg)) 41 | } 42 | } 43 | 44 | static func log(_ msg: String, level: ClashLogLevel = .info, file: String = #file, function: String = #function) { 45 | shared.logToFile(msg: "[\(level.rawValue)] \(file) \(function) \(msg)", level: level) 46 | } 47 | 48 | func logFilePath() -> String { 49 | return fileLogger.logFileManager.sortedLogFilePaths.first ?? "" 50 | } 51 | 52 | func logFolder() -> String { 53 | return fileLogger.logFileManager.logsDirectory 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ClashX/ViewControllers/Connections/Views/zh-Hans.lproj/ConnectionDetailInfoGeneralView.strings: -------------------------------------------------------------------------------- 1 | /* Class = "NSTextFieldCell"; title = "NetworkType"; ObjectID = "6p6-n8-rBg"; */ 2 | "6p6-n8-rBg.title" = "网络类型"; 3 | 4 | /* Class = "NSBox"; title = "Current Speed"; ObjectID = "7fZ-OX-aWF"; */ 5 | "7fZ-OX-aWF.title" = "实时速率"; 6 | 7 | /* Class = "NSTextFieldCell"; title = "Source"; ObjectID = "9Id-dT-XYP"; */ 8 | "9Id-dT-XYP.title" = "来源"; 9 | 10 | /* Class = "NSTextFieldCell"; title = "Upload"; ObjectID = "CGD-ut-PXk"; */ 11 | "CGD-ut-PXk.title" = "上传"; 12 | 13 | /* Class = "NSBox"; title = "Rule"; ObjectID = "CiP-Ib-BPd"; */ 14 | "CiP-Ib-BPd.title" = "规则"; 15 | 16 | /* Class = "NSTextFieldCell"; title = "Upload"; ObjectID = "GhE-tw-D8L"; */ 17 | "GhE-tw-D8L.title" = "上传"; 18 | 19 | /* Class = "NSTextFieldCell"; title = "Entry"; ObjectID = "KQt-5l-lyc"; */ 20 | "KQt-5l-lyc.title" = "入口"; 21 | 22 | /* Class = "NSTextFieldCell"; title = "Upload"; ObjectID = "KsS-nl-D2n"; */ 23 | "KsS-nl-D2n.title" = "上传"; 24 | 25 | /* Class = "NSBox"; title = "Proxy Chain"; ObjectID = "MIz-sg-4Rx"; */ 26 | "MIz-sg-4Rx.title" = "代理链"; 27 | 28 | /* Class = "NSBox"; title = "Max Speed"; ObjectID = "NBq-b1-RLL"; */ 29 | "NBq-b1-RLL.title" = "最高速率"; 30 | 31 | /* Class = "NSTextFieldCell"; title = "Download"; ObjectID = "ZEc-E9-AXS"; */ 32 | "ZEc-E9-AXS.title" = "下载"; 33 | 34 | /* Class = "NSTextFieldCell"; title = "Download"; ObjectID = "iem-RP-B0u"; */ 35 | "iem-RP-B0u.title" = "下载"; 36 | 37 | /* Class = "NSBox"; title = "General"; ObjectID = "leq-MF-MFL"; */ 38 | "leq-MF-MFL.title" = "通用"; 39 | 40 | /* Class = "NSTextFieldCell"; title = "Dest."; ObjectID = "m5m-4U-xAi"; */ 41 | "m5m-4U-xAi.title" = "去向"; 42 | 43 | /* Class = "NSBox"; title = "Other"; ObjectID = "nG8-0D-7W1"; */ 44 | "nG8-0D-7W1.title" = "其他"; 45 | 46 | /* Class = "NSBox"; title = "Address"; ObjectID = "obG-yt-Gi8"; */ 47 | "obG-yt-Gi8.title" = "地址"; 48 | 49 | /* Class = "NSTextFieldCell"; title = "Download"; ObjectID = "tes-yR-PKh"; */ 50 | "tes-yR-PKh.title" = "下载"; 51 | 52 | /* Class = "NSBox"; title = "Total"; ObjectID = "xB0-fx-J0y"; */ 53 | "xB0-fx-J0y.title" = "总计"; 54 | -------------------------------------------------------------------------------- /ClashX/ViewControllers/Connections/Views/zh-Hant.lproj/ConnectionDetailInfoGeneralView.strings: -------------------------------------------------------------------------------- 1 | /* Class = "NSTextFieldCell"; title = "NetworkType"; ObjectID = "6p6-n8-rBg"; */ 2 | "6p6-n8-rBg.title" = "網絡類型"; 3 | 4 | /* Class = "NSBox"; title = "Current Speed"; ObjectID = "7fZ-OX-aWF"; */ 5 | "7fZ-OX-aWF.title" = "實時速率"; 6 | 7 | /* Class = "NSTextFieldCell"; title = "Source"; ObjectID = "9Id-dT-XYP"; */ 8 | "9Id-dT-XYP.title" = "來源"; 9 | 10 | /* Class = "NSTextFieldCell"; title = "Upload"; ObjectID = "CGD-ut-PXk"; */ 11 | "CGD-ut-PXk.title" = "上傳"; 12 | 13 | /* Class = "NSBox"; title = "Rule"; ObjectID = "CiP-Ib-BPd"; */ 14 | "CiP-Ib-BPd.title" = "規則"; 15 | 16 | /* Class = "NSTextFieldCell"; title = "Upload"; ObjectID = "GhE-tw-D8L"; */ 17 | "GhE-tw-D8L.title" = "上傳"; 18 | 19 | /* Class = "NSTextFieldCell"; title = "Entry"; ObjectID = "KQt-5l-lyc"; */ 20 | "KQt-5l-lyc.title" = "入口"; 21 | 22 | /* Class = "NSTextFieldCell"; title = "Upload"; ObjectID = "KsS-nl-D2n"; */ 23 | "KsS-nl-D2n.title" = "上傳"; 24 | 25 | /* Class = "NSBox"; title = "Proxy Chain"; ObjectID = "MIz-sg-4Rx"; */ 26 | "MIz-sg-4Rx.title" = "代理鏈"; 27 | 28 | /* Class = "NSBox"; title = "Max Speed"; ObjectID = "NBq-b1-RLL"; */ 29 | "NBq-b1-RLL.title" = "最高速率"; 30 | 31 | /* Class = "NSTextFieldCell"; title = "Download"; ObjectID = "ZEc-E9-AXS"; */ 32 | "ZEc-E9-AXS.title" = "下載"; 33 | 34 | /* Class = "NSTextFieldCell"; title = "Download"; ObjectID = "iem-RP-B0u"; */ 35 | "iem-RP-B0u.title" = "下載"; 36 | 37 | /* Class = "NSBox"; title = "General"; ObjectID = "leq-MF-MFL"; */ 38 | "leq-MF-MFL.title" = "通用"; 39 | 40 | /* Class = "NSTextFieldCell"; title = "Dest."; ObjectID = "m5m-4U-xAi"; */ 41 | "m5m-4U-xAi.title" = "去向"; 42 | 43 | /* Class = "NSBox"; title = "Other"; ObjectID = "nG8-0D-7W1"; */ 44 | "nG8-0D-7W1.title" = "其他"; 45 | 46 | /* Class = "NSBox"; title = "Address"; ObjectID = "obG-yt-Gi8"; */ 47 | "obG-yt-Gi8.title" = "地址"; 48 | 49 | /* Class = "NSTextFieldCell"; title = "Download"; ObjectID = "tes-yR-PKh"; */ 50 | "tes-yR-PKh.title" = "下載"; 51 | 52 | /* Class = "NSBox"; title = "Total"; ObjectID = "xB0-fx-J0y"; */ 53 | "xB0-fx-J0y.title" = "總計"; 54 | -------------------------------------------------------------------------------- /ClashX/Actions/UpdateExternalResourceAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateExternalResourceAction.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2023/9/4. 6 | // Copyright © 2023 west2online. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | enum UpdateExternalResourceAction { 11 | static func run() { 12 | ApiRequest.requestExternalProviderNames { provider in 13 | let group = DispatchGroup() 14 | var successCount = 0 15 | let totalCount = provider.proxies.count + provider.rules.count 16 | if totalCount == 0 { 17 | onFinished(success: 0, total: 0, fails: []) 18 | return 19 | } 20 | var fails = [String]() 21 | for name in provider.proxies { 22 | group.enter() 23 | ApiRequest.updateProvider(name: name, type: .proxy) { success in 24 | if success { successCount += 1 } else { 25 | fails.append(name) 26 | } 27 | group.leave() 28 | } 29 | } 30 | 31 | for name in provider.rules { 32 | group.enter() 33 | ApiRequest.updateProvider(name: name, type: .rule) { success in 34 | if success { successCount += 1 } else { 35 | fails.append(name) 36 | } 37 | group.leave() 38 | } 39 | } 40 | 41 | group.notify(queue: .main) { 42 | onFinished(success: successCount, total: totalCount, fails: fails) 43 | } 44 | } 45 | } 46 | 47 | private static func onFinished(success: Int, total: Int, fails: [String]) { 48 | var info = String(format: NSLocalizedString("total: %d, success: %d", comment: ""), total, success) 49 | if !fails.isEmpty { 50 | info.append(String(format: NSLocalizedString("fails: %@", comment: ""), fails.joined(separator: " "))) 51 | } 52 | NSUserNotificationCenter.default.post(title: NSLocalizedString("Update external resource complete", comment: ""), info: info) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /ClashX/General/Managers/ConfigFileManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigFileManager.swift 3 | // ClashX 4 | // 5 | // Created by CYC on 2018/8/5. 6 | // Copyright © 2018年 yichengchen. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | import Foundation 11 | import SwiftyJSON 12 | 13 | class ConfigFileManager { 14 | static let shared = ConfigFileManager() 15 | private var witness: Witness? 16 | private var pause = false 17 | 18 | func pauseForNextChange() { 19 | pause = true 20 | } 21 | 22 | func watchFile(path: String) { 23 | witness = Witness(paths: [path], flags: .FileEvents, latency: 0.3) { 24 | [weak self] events in 25 | guard let self = self else { return } 26 | guard !self.pause else { 27 | self.pause = false 28 | return 29 | } 30 | for event in events { 31 | if event.flags.contains(.ItemModified) || event.flags.contains(.ItemRenamed) { 32 | NSUserNotificationCenter.default 33 | .postConfigFileChangeDetectionNotice() 34 | NotificationCenter.default 35 | .post(Notification(name: .configFileChange)) 36 | break 37 | } 38 | } 39 | } 40 | } 41 | 42 | func stopWatchConfigFile() { 43 | witness = nil 44 | pause = false 45 | } 46 | 47 | @discardableResult 48 | static func backupAndRemoveConfigFile() -> Bool { 49 | let path = kDefaultConfigFilePath 50 | if FileManager.default.fileExists(atPath: path) { 51 | let newPath = "\(kConfigFolderPath)config_\(Date().timeIntervalSince1970).yaml" 52 | try? FileManager.default.moveItem(atPath: path, toPath: newPath) 53 | } 54 | return true 55 | } 56 | 57 | static func copySampleConfigIfNeed() { 58 | if !FileManager.default.fileExists(atPath: kDefaultConfigFilePath) { 59 | let path = Bundle.main.path(forResource: "sampleConfig", ofType: "yaml")! 60 | try? FileManager.default.copyItem(atPath: path, toPath: kDefaultConfigFilePath) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /ClashX/Views/ProxyDelayHistoryMenu.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProxyDelayHistoryMenu.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2020/4/25. 6 | // Copyright © 2020 west2online. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import FlexibleDiff 11 | 12 | class ProxyDelayHistoryMenu: NSMenu { 13 | var currentHistory: [ClashProxySpeedHistory]? 14 | 15 | init(proxy: ClashProxy) { 16 | super.init(title: "") 17 | updateHistoryMenu(proxy: proxy) 18 | NotificationCenter.default.addObserver(self, selector: #selector(proxyInfoDidUpdate(note:)), name: .proxyUpdate(for: proxy.name), object: nil) 19 | } 20 | 21 | @available(*, unavailable) 22 | required init(coder: NSCoder) { 23 | fatalError("init(coder:) has not been implemented") 24 | } 25 | 26 | deinit { 27 | NotificationCenter.default.removeObserver(self) 28 | } 29 | 30 | @objc private func proxyInfoDidUpdate(note: Notification) { 31 | guard let info = note.object as? ClashProxy else { return } 32 | updateHistoryMenu(proxy: info) 33 | } 34 | 35 | private func updateHistoryMenu(proxy: ClashProxy) { 36 | let historys = Array(proxy.history.reversed()) 37 | let change = Changeset(previous: currentHistory, current: historys, identifier: { $0.time }) 38 | currentHistory = historys 39 | if change.moves.isEmpty && change.mutations.isEmpty { 40 | change.removals.reversed().forEach { idx in 41 | removeItem(at: idx) 42 | } 43 | change.inserts.forEach { idx in 44 | let his = historys[idx] 45 | let item = NSMenuItem(title: his.displayString, action: nil, keyEquivalent: "") 46 | insertItem(item, at: idx) 47 | } 48 | } else { 49 | historys.map { his in 50 | NSMenuItem(title: his.displayString, action: nil, keyEquivalent: "") 51 | }.forEach { item in 52 | addItem(item) 53 | } 54 | } 55 | } 56 | } 57 | 58 | extension ClashProxySpeedHistory: Equatable { 59 | static func == (lhs: ClashProxySpeedHistory, rhs: ClashProxySpeedHistory) -> Bool { 60 | return lhs.displayString == rhs.displayString 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ClashX/Views/NormalMenuItemView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NormalMenuItemView.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2023/6/25. 6 | // Copyright © 2023 west2online. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | 11 | @available(macOS 11.0, *) 12 | class NormalMenuItemView: MenuItemBaseView { 13 | let label: NSTextField 14 | private let arrowLabel: NSControl = { 15 | let image = NSImage(named: NSImage.goForwardTemplateName)!.withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 14, weight: .bold, scale: .small))! 16 | return NSImageView(image: image) 17 | }() 18 | 19 | init(_ title: String, rightArrow: Bool) { 20 | label = NSTextField(labelWithString: title) 21 | label.font = type(of: self).labelFont 22 | label.sizeToFit() 23 | let rect = NSRect(x: 0, y: 0, width: label.bounds.width + 40 + arrowLabel.bounds.width, height: 20) 24 | super.init(frame: rect, autolayout: false) 25 | addSubview(label) 26 | label.frame = NSRect(x: 20, y: 0, width: label.bounds.width, height: 20) 27 | label.textColor = NSColor.labelColor 28 | if rightArrow { 29 | addSubview(arrowLabel) 30 | } 31 | } 32 | 33 | override func layoutSubtreeIfNeeded() { 34 | super.layoutSubtreeIfNeeded() 35 | arrowLabel.frame = NSRect(x: bounds.width - arrowLabel.bounds.width - 12, y: 0, width: arrowLabel.bounds.width, height: 20) 36 | } 37 | 38 | @available(*, unavailable) 39 | required init?(coder: NSCoder) { 40 | fatalError("init(coder:) has not been implemented") 41 | } 42 | 43 | override var cells: [NSCell?] { 44 | return [label.cell, arrowLabel.cell] 45 | } 46 | 47 | override var labels: [NSTextField] { 48 | return [label] 49 | } 50 | } 51 | 52 | @available(macOS 11.0, *) 53 | extension NormalMenuItemView: ProxyGroupMenuHighlightDelegate { 54 | func highlight(item: NSMenuItem?) { 55 | if enclosingMenuItem?.hasSubmenu == true, let item = item { 56 | if enclosingMenuItem?.submenu?.items.contains(item) == true { 57 | isHighlighted = true 58 | return 59 | } 60 | } 61 | isHighlighted = item == enclosingMenuItem 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Alamofire (5.8.0) 3 | - AppCenter/Analytics (5.0.4): 4 | - AppCenter/Core 5 | - AppCenter/Core (5.0.4) 6 | - AppCenter/Crashes (5.0.4): 7 | - AppCenter/Core 8 | - CocoaLumberjack/Core (3.8.1) 9 | - CocoaLumberjack/Swift (3.8.1): 10 | - CocoaLumberjack/Core 11 | - FlexibleDiff (0.0.9) 12 | - GzipSwift (5.1.1) 13 | - LetsMove (1.25) 14 | - RxCocoa (6.6.0): 15 | - RxRelay (= 6.6.0) 16 | - RxSwift (= 6.6.0) 17 | - RxRelay (6.6.0): 18 | - RxSwift (= 6.6.0) 19 | - RxSwift (6.6.0) 20 | - Sparkle (2.5.1) 21 | - Starscream (3.1.1) 22 | - SwiftFormat/CLI (0.52.7) 23 | - SwiftLint (0.53.0) 24 | - SwiftyJSON (5.0.1) 25 | 26 | DEPENDENCIES: 27 | - Alamofire (~> 5.0) 28 | - AppCenter/Analytics 29 | - AppCenter/Crashes 30 | - CocoaLumberjack/Swift 31 | - FlexibleDiff 32 | - GzipSwift 33 | - LetsMove 34 | - RxCocoa 35 | - RxSwift 36 | - Sparkle (~> 2.0) 37 | - Starscream (= 3.1.1) 38 | - SwiftFormat/CLI (~> 0.49) 39 | - SwiftLint 40 | - SwiftyJSON 41 | 42 | SPEC REPOS: 43 | trunk: 44 | - Alamofire 45 | - AppCenter 46 | - CocoaLumberjack 47 | - FlexibleDiff 48 | - GzipSwift 49 | - LetsMove 50 | - RxCocoa 51 | - RxRelay 52 | - RxSwift 53 | - Sparkle 54 | - Starscream 55 | - SwiftFormat 56 | - SwiftLint 57 | - SwiftyJSON 58 | 59 | SPEC CHECKSUMS: 60 | Alamofire: 0e92e751b3e9e66d7982db43919d01f313b8eb91 61 | AppCenter: 85c92db0759d2792a65eb61d6842d2e86611a49a 62 | CocoaLumberjack: 5c7e64cdb877770859bddec4d3d5a0d7c9299df9 63 | FlexibleDiff: b9ee9b8305b42c784f5dd40589203c97c55bbaa0 64 | GzipSwift: 893f3e48e597a1a4f62fafcb6514220fcf8287fa 65 | LetsMove: 7b9fe44737707d984fbd3f47af46609a9a07b461 66 | RxCocoa: 44a80de90e25b739b5aeaae3c8c371a32e3343cc 67 | RxRelay: 45eaa5db8ee4fb50e5ebd57deec0159e97fa51e6 68 | RxSwift: a4b44f7d24599f674deebd1818eab82e58410632 69 | Sparkle: ce9957501a2655dd4c8264312c6134ff478a777c 70 | Starscream: 4bb2f9942274833f7b4d296a55504dcfc7edb7b0 71 | SwiftFormat: 2c4785ad647322b41e027b9d4df160aef526a656 72 | SwiftLint: 5ce4d6a8ff83f1b5fd5ad5dbf30965d35af65e44 73 | SwiftyJSON: 2f33a42c6fbc52764d96f13368585094bfd8aa5e 74 | 75 | PODFILE CHECKSUM: 8c6ef0c5999141047a530cd8b74a41d2161ae11b 76 | 77 | COCOAPODS: 1.13.0 78 | -------------------------------------------------------------------------------- /ClashX/General/Utils/AppVersionUtil.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppVersionUtil.swift 3 | // ClashX 4 | // 5 | // Created by CYC on 2019/2/18. 6 | // Copyright © 2019 west2online. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class AppVersionUtil: NSObject { 12 | private static let shared = AppVersionUtil() 13 | 14 | private static let kLastVersionNumberKey = "com.clashX.lastVersionNumber" 15 | 16 | private let lastVersionNumber: String? 17 | 18 | static var currentVersion: String { 19 | return Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "" 20 | } 21 | 22 | static var currentBuild: String { 23 | return Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "" 24 | } 25 | 26 | static var isBeta: Bool { 27 | return Bundle.main.object(forInfoDictionaryKey: "BETA") as? Bool ?? false 28 | } 29 | 30 | override init() { 31 | lastVersionNumber = UserDefaults.standard.string(forKey: AppVersionUtil.kLastVersionNumberKey) 32 | UserDefaults.standard.set(AppVersionUtil.currentVersion, forKey: AppVersionUtil.kLastVersionNumberKey) 33 | } 34 | 35 | static var isFirstLaunch: Bool { 36 | return shared.lastVersionNumber == nil 37 | } 38 | 39 | static var hasVersionChanged: Bool { 40 | return shared.lastVersionNumber != currentVersion 41 | } 42 | } 43 | 44 | extension AppVersionUtil { 45 | static func showUpgradeAlert() { 46 | if let lastVersion = shared.lastVersionNumber, hasVersionChanged { 47 | WebCacheCleaner.clean() 48 | guard lastVersion.compare("1.30.0", options: .numeric) == .orderedAscending else { return } 49 | let alert = NSAlert() 50 | alert.messageText = NSLocalizedString("This version of ClashX contains a break change due to clash core 1.0 released. Check if your config is not working properly.", comment: "") 51 | alert.alertStyle = .informational 52 | alert.addButton(withTitle: NSLocalizedString("OK", comment: "")) 53 | alert.addButton(withTitle: NSLocalizedString("Details", comment: "")) 54 | if alert.runModal() == .alertSecondButtonReturn { 55 | NSWorkspace.shared.open(URL(string: "https://github.com/Dreamacro/clash/wiki/breaking-changes-in-1.0.0")!) 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /ClashX/goClash/build_clash_universal.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import datetime 3 | import plistlib 4 | import os 5 | import filecmp 6 | 7 | def get_version(): 8 | with open('./go.mod') as file: 9 | for line in file.readlines(): 10 | if "clash" in line and "ClashX" not in line: 11 | return line.split("-")[-1].strip()[:6] 12 | return "unknown" 13 | 14 | go_bin = "go" 15 | 16 | def build_clash(version,build_time,arch): 17 | command = f""" 18 | {go_bin} build -trimpath -ldflags '-X "github.com/Dreamacro/clash/constant.Version={version}" \ 19 | -X "github.com/Dreamacro/clash/constant.BuildTime={build_time}"' \ 20 | -buildmode=c-archive -o goClash_{arch}.a """ 21 | envs = os.environ.copy() 22 | envs.update({ 23 | "GOOS":"darwin", 24 | "GOARCH":arch, 25 | "CGO_ENABLED":"1", 26 | "CGO_LDFLAGS":"-mmacosx-version-min=10.14", 27 | "CGO_CFLAGS":"-mmacosx-version-min=10.14", 28 | }) 29 | subprocess.check_output(command, shell=True,env=envs) 30 | 31 | def mergeLibs(): 32 | if not filecmp.cmp('goClash_amd64.h','goClash_arm64.h'): 33 | exit(-1) 34 | os.rename('goClash_amd64.h', 'goClash.h') 35 | command = "lipo *.a -create -output goClash.a" 36 | subprocess.check_output(command, shell=True) 37 | 38 | def clean(): 39 | cmd = "rm -f *amd* *arm*" 40 | subprocess.check_output(cmd, shell=True) 41 | 42 | 43 | def write_to_info(version): 44 | path = "../info.plist" 45 | 46 | with open(path, 'rb') as f: 47 | contents = plistlib.load(f) 48 | 49 | if not contents: 50 | exit(-1) 51 | 52 | contents["coreVersion"] = version 53 | with open(path, 'wb') as f: 54 | plistlib.dump(contents, f, sort_keys=False) 55 | 56 | 57 | def run(): 58 | version = get_version() 59 | print("current clash version:", version) 60 | build_time = datetime.datetime.now().strftime("%Y-%m-%d-%H%M") 61 | print("clean existing") 62 | subprocess.check_output("rm -f *Clash*.h *.a", shell=True) 63 | print("create arm64") 64 | build_clash(version,build_time,"arm64") 65 | print("create amd64") 66 | build_clash(version,build_time,"amd64") 67 | print("merge") 68 | mergeLibs() 69 | print("clean") 70 | clean() 71 | if os.environ.get("CI", False) or os.environ.get("GITHUB_ACTIONS", False): 72 | print("writing info.plist") 73 | write_to_info(version) 74 | print("done") 75 | 76 | 77 | if __name__ == "__main__": 78 | run() 79 | -------------------------------------------------------------------------------- /ClashX/Vendor/UserDefaultWrapper.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A type safe property wrapper to set and get values from UserDefaults with support for defaults values. 4 | /// 5 | /// Usage: 6 | /// ``` 7 | /// @UserDefault("has_seen_app_introduction", defaultValue: false) 8 | /// static var hasSeenAppIntroduction: Bool 9 | /// ``` 10 | /// 11 | /// [Apple documentation on UserDefaults](https://developer.apple.com/documentation/foundation/userdefaults) 12 | @available(iOS 2.0, OSX 10.0, tvOS 9.0, watchOS 2.0, *) 13 | @propertyWrapper 14 | public struct UserDefault { 15 | let key: String 16 | let defaultValue: Value 17 | var userDefaults: UserDefaults 18 | 19 | public init(_ key: String, defaultValue: Value, userDefaults: UserDefaults = .standard) { 20 | self.key = key 21 | self.defaultValue = defaultValue 22 | self.userDefaults = userDefaults 23 | } 24 | 25 | public var wrappedValue: Value { 26 | get { 27 | return userDefaults.object(forKey: key) as? Value ?? defaultValue 28 | } 29 | set { 30 | userDefaults.set(newValue, forKey: key) 31 | } 32 | } 33 | } 34 | 35 | /// A type than can be stored in `UserDefaults`. 36 | /// 37 | /// - From UserDefaults; 38 | /// The value parameter can be only property list objects: NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. 39 | /// For NSArray and NSDictionary objects, their contents must be property list objects. For more information, see What is a 40 | /// Property List? in Property List Programming Guide. 41 | public protocol PropertyListValue {} 42 | 43 | extension Data: PropertyListValue {} 44 | extension NSData: PropertyListValue {} 45 | 46 | extension String: PropertyListValue {} 47 | extension NSString: PropertyListValue {} 48 | 49 | extension Date: PropertyListValue {} 50 | extension NSDate: PropertyListValue {} 51 | 52 | extension NSNumber: PropertyListValue {} 53 | extension Bool: PropertyListValue {} 54 | extension Int: PropertyListValue {} 55 | extension Int8: PropertyListValue {} 56 | extension Int16: PropertyListValue {} 57 | extension Int32: PropertyListValue {} 58 | extension Int64: PropertyListValue {} 59 | extension UInt: PropertyListValue {} 60 | extension UInt8: PropertyListValue {} 61 | extension UInt16: PropertyListValue {} 62 | extension UInt32: PropertyListValue {} 63 | extension UInt64: PropertyListValue {} 64 | extension Double: PropertyListValue {} 65 | extension Float: PropertyListValue {} 66 | 67 | extension Array: PropertyListValue where Element: PropertyListValue {} 68 | 69 | extension Dictionary: PropertyListValue where Key == String, Value: PropertyListValue {} 70 | -------------------------------------------------------------------------------- /ClashX/ViewControllers/Connections/Views/Cell/ConnectionTextCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectionTextCellView.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2023/7/6. 6 | // Copyright © 2023 west2online. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | import Combine 11 | 12 | @available(macOS 10.15, *) 13 | class ConnectionTextCellView: NSView, ConnectionCellProtocol { 14 | let label = NSTextField(labelWithString: "") 15 | var cancellable = Set() 16 | override init(frame: CGRect) { 17 | super.init(frame: frame) 18 | setupUI() 19 | } 20 | 21 | required init?(coder: NSCoder) { 22 | super.init(coder: coder) 23 | setupUI() 24 | } 25 | 26 | func setupUI() { 27 | clipsToBounds = true 28 | addSubview(label) 29 | label.font = NSFont.systemFont(ofSize: 12) 30 | label.makeConstraints { 31 | [$0.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0), 32 | $0.centerYAnchor.constraint(equalTo: centerYAnchor)] 33 | } 34 | } 35 | 36 | func setup(with connection: ClashConnectionSnapShot.Connection, type: ConnectionColume) { 37 | cancellable.removeAll() 38 | switch type { 39 | case .upload: 40 | connection.$upload.map { SpeedUtils.getNetString(for: $0) }.weakAssign(to: \.stringValue, on: label).store(in: &cancellable) 41 | case .download: 42 | connection.$download.map { SpeedUtils.getNetString(for: $0) }.weakAssign(to: \.stringValue, on: label).store(in: &cancellable) 43 | case .currentUpload: 44 | connection.$uploadSpeed.map { SpeedUtils.getSpeedString(for: $0) }.weakAssign(to: \.stringValue, on: label).store(in: &cancellable) 45 | case .currentDownload: 46 | connection.$downloadSpeed.map { SpeedUtils.getSpeedString(for: $0) }.weakAssign(to: \.stringValue, on: label).store(in: &cancellable) 47 | case .status: 48 | connection.$status.map(\.title).weakAssign(to: \.stringValue, on: label).store(in: &cancellable) 49 | case .statusIcon, .process: 50 | return 51 | case .rule: 52 | label.stringValue = connection.chains.joined(separator: "/") 53 | case .date: 54 | label.stringValue = DateFormatter.simple.string(from: connection.start) 55 | case .url: 56 | label.stringValue = connection.metadata.displayHost 57 | case .type: 58 | label.stringValue = connection.metadata.network 59 | } 60 | } 61 | 62 | override func prepareForReuse() { 63 | super.prepareForReuse() 64 | cancellable.removeAll() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /ClashX/ViewControllers/AboutViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutViewController.swift 3 | // ClashX 4 | // 5 | // Created by CYC on 2018/8/19. 6 | // Copyright © 2018年 west2online. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class AboutViewController: NSViewController { 12 | @IBOutlet var versionLabel: NSTextField! 13 | @IBOutlet var buildTimeLabel: NSTextField! 14 | @IBOutlet var coreVersionLabel: NSTextField! 15 | 16 | lazy var clashCoreVersion: String = { 17 | return Bundle.main.infoDictionary?["coreVersion"] as? String ?? "unknown" 18 | }() 19 | 20 | lazy var commit: String = { 21 | return Bundle.main.infoDictionary?["gitCommit"] as? String ?? "unknown" 22 | }() 23 | 24 | lazy var branch: String = { 25 | return Bundle.main.infoDictionary?["gitBranch"] as? String ?? "unknown" 26 | }() 27 | 28 | lazy var buildTime: String = { 29 | return Bundle.main.infoDictionary?["buildTime"] as? String ?? "unknown" 30 | }() 31 | 32 | override func viewDidLoad() { 33 | super.viewDidLoad() 34 | title = "About" 35 | 36 | let version = AppVersionUtil.currentVersion 37 | let build = AppVersionUtil.currentBuild 38 | let isBeta = AppVersionUtil.isBeta ? " Beta" : "" 39 | 40 | versionLabel.stringValue = "Version: \(version) (\(build))\(isBeta)" 41 | coreVersionLabel.stringValue = clashCoreVersion 42 | buildTimeLabel.stringValue = "\(commit)-\(branch) \(buildTime)" 43 | } 44 | 45 | override func viewWillAppear() { 46 | super.viewWillAppear() 47 | view.window?.styleMask.remove(.resizable) 48 | view.window?.makeKeyAndOrderFront(self) 49 | view.window?.level = .floating 50 | NSApp.activate(ignoringOtherApps: true) 51 | } 52 | } 53 | 54 | @IBDesignable 55 | class HyperlinkTextField: NSTextField { 56 | @IBInspectable var href: String = "" 57 | 58 | override func resetCursorRects() { 59 | discardCursorRects() 60 | addCursorRect(bounds, cursor: NSCursor.pointingHand) 61 | } 62 | 63 | override func awakeFromNib() { 64 | super.awakeFromNib() 65 | let attributes: [NSAttributedString.Key: Any] = [ 66 | NSAttributedString.Key.foregroundColor: NSColor.linkColor, 67 | NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue as AnyObject 68 | ] 69 | attributedStringValue = NSAttributedString(string: stringValue, attributes: attributes) 70 | } 71 | 72 | override func mouseDown(with theEvent: NSEvent) { 73 | if let localHref = URL(string: href) { 74 | NSWorkspace.shared.open(localHref) 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /ClashX/Views/StatusItem/StatusItemView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusItemView.swift 3 | // ClashX 4 | // 5 | // Created by CYC on 2018/6/23. 6 | // Copyright © 2018年 yichengchen. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | import Foundation 11 | import RxCocoa 12 | import RxSwift 13 | 14 | class StatusItemView: NSView, StatusItemViewProtocol { 15 | @IBOutlet var imageView: NSImageView! 16 | 17 | @IBOutlet var uploadSpeedLabel: NSTextField! 18 | @IBOutlet var downloadSpeedLabel: NSTextField! 19 | @IBOutlet var speedContainerView: NSView! 20 | 21 | var up: Int = 0 22 | var down: Int = 0 23 | 24 | static func create(statusItem: NSStatusItem?) -> StatusItemView { 25 | var topLevelObjects: NSArray? 26 | if Bundle.main.loadNibNamed("StatusItemView", owner: self, topLevelObjects: &topLevelObjects) { 27 | let view = (topLevelObjects!.first(where: { $0 is NSView }) as? StatusItemView)! 28 | view.setupView() 29 | view.imageView.image = StatusItemTool.menuImage 30 | 31 | if let button = statusItem?.button { 32 | button.addSubview(view) 33 | button.imagePosition = .imageOverlaps 34 | } else { 35 | Logger.log("button = nil") 36 | AppDelegate.shared.openConfigFolder(self) 37 | } 38 | view.updateViewStatus(enableProxy: false) 39 | return view 40 | } 41 | return NSView() as! StatusItemView 42 | } 43 | 44 | func setupView() { 45 | uploadSpeedLabel.font = StatusItemTool.font 46 | downloadSpeedLabel.font = StatusItemTool.font 47 | 48 | uploadSpeedLabel.textColor = NSColor.labelColor 49 | downloadSpeedLabel.textColor = NSColor.labelColor 50 | } 51 | 52 | func updateSize(width: CGFloat) { 53 | frame = CGRect(x: 0, y: 0, width: width, height: 22) 54 | } 55 | 56 | func updateViewStatus(enableProxy: Bool) { 57 | if enableProxy { 58 | imageView.contentTintColor = NSColor.labelColor 59 | } else { 60 | imageView.contentTintColor = NSColor.labelColor.withSystemEffect(.disabled) 61 | } 62 | } 63 | 64 | func updateSpeedLabel(up: Int, down: Int) { 65 | guard !speedContainerView.isHidden else { return } 66 | if up != self.up { 67 | uploadSpeedLabel.stringValue = SpeedUtils.getSpeedString(for: up) 68 | self.up = up 69 | } 70 | if down != self.down { 71 | downloadSpeedLabel.stringValue = SpeedUtils.getSpeedString(for: down) 72 | self.down = down 73 | } 74 | } 75 | 76 | func showSpeedContainer(show: Bool) { 77 | speedContainerView.isHidden = !show 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /ClashX/goClash/proccess.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "net/netip" 7 | "strconv" 8 | "strings" 9 | "syscall" 10 | "unsafe" 11 | ) 12 | 13 | const ( 14 | procpidpathinfo = 0xb 15 | procpidpathinfosize = 1024 16 | proccallnumpidinfo = 0x2 17 | ) 18 | 19 | var structSize = func() int { 20 | value, _ := syscall.Sysctl("kern.osrelease") 21 | major, _, _ := strings.Cut(value, ".") 22 | n, _ := strconv.ParseInt(major, 10, 64) 23 | switch true { 24 | case n >= 22: 25 | return 408 26 | default: 27 | // from darwin-xnu/bsd/netinet/in_pcblist.c:get_pcblist_n 28 | // size/offset are round up (aligned) to 8 bytes in darwin 29 | // rup8(sizeof(xinpcb_n)) + rup8(sizeof(xsocket_n)) + 30 | // 2 * rup8(sizeof(xsockbuf_n)) + rup8(sizeof(xsockstat_n)) 31 | return 384 32 | } 33 | }() 34 | 35 | func GetTcpNetList() string { 36 | value, err := syscall.Sysctl("net.inet.tcp.pcblist_n") 37 | if err != nil { 38 | return "" 39 | } 40 | 41 | buf := []byte(value) 42 | itemSize := structSize 43 | // tcp 44 | // rup8(sizeof(xtcpcb_n)) 45 | itemSize += 208 46 | 47 | result := "" 48 | for i := 24; i+itemSize <= len(buf); i += itemSize { 49 | // offset of xinpcb_n and xsocket_n 50 | inp, so := i, i+104 51 | srcPort := binary.BigEndian.Uint16(buf[inp+18 : inp+20]) 52 | // xinpcb_n.inp_vflag 53 | flag := buf[inp+44] 54 | 55 | var srcIP netip.Addr 56 | switch { 57 | case flag&0x1 > 0: 58 | // ipv4 59 | srcIP = netip.AddrFrom4([4]byte(buf[inp+76 : inp+80])) 60 | case flag&0x2 > 0: 61 | // ipv6 62 | srcIP = netip.AddrFrom16([16]byte(buf[inp+64 : inp+80])) 63 | default: 64 | continue 65 | } 66 | pid := readNativeUint32(buf[so+68 : so+72]) 67 | result += fmt.Sprintf("%s %d %d\n", srcIP, srcPort, pid) 68 | } 69 | return result 70 | } 71 | 72 | func GetUDpList() string { 73 | value, err := syscall.Sysctl("net.inet.udp.pcblist_n") 74 | if err != nil { 75 | return "" 76 | } 77 | 78 | buf := []byte(value) 79 | itemSize := structSize 80 | result := "" 81 | 82 | for i := 24; i+itemSize <= len(buf); i += itemSize { 83 | // offset of xinpcb_n and xsocket_n 84 | inp, so := i, i+104 85 | srcPort := binary.BigEndian.Uint16(buf[inp+18 : inp+20]) 86 | // xinpcb_n.inp_vflag 87 | flag := buf[inp+44] 88 | var srcIP netip.Addr 89 | switch { 90 | case flag&0x1 > 0: 91 | // ipv4 92 | srcIP = netip.AddrFrom4([4]byte(buf[inp+76 : inp+80])) 93 | case flag&0x2 > 0: 94 | // ipv6 95 | srcIP = netip.AddrFrom16([16]byte(buf[inp+64 : inp+80])) 96 | default: 97 | continue 98 | } 99 | 100 | pid := readNativeUint32(buf[so+68 : so+72]) 101 | result += fmt.Sprintf("%s %d %d\n", srcIP, srcPort, pid) 102 | } 103 | return result 104 | } 105 | 106 | func readNativeUint32(b []byte) uint32 { 107 | return *(*uint32)(unsafe.Pointer(&b[0])) 108 | } 109 | -------------------------------------------------------------------------------- /ClashX/ViewControllers/Connections/Views/ConnectionColume.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectionColume.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2023/7/6. 6 | // Copyright © 2023 west2online. All rights reserved. 7 | // 8 | 9 | enum ConnectionFilter { 10 | case application(path: String) 11 | case source(ip: String) 12 | case hosts(name: String) 13 | } 14 | 15 | @available(macOS 10.15, *) 16 | enum ConnectionColume: String, CaseIterable { 17 | case statusIcon 18 | case process 19 | case status 20 | case date 21 | case url 22 | case rule 23 | case currentUpload 24 | case currentDownload 25 | case upload 26 | case download 27 | case type 28 | 29 | var columeTitle: String { 30 | switch self { 31 | case .statusIcon: return "" 32 | case .process: return NSLocalizedString("Client", comment: "") 33 | case .status: return NSLocalizedString("Status", comment: "") 34 | case .rule: return NSLocalizedString("Rule", comment: "") 35 | case .url: return NSLocalizedString("Host", comment: "") 36 | case .date: return NSLocalizedString("Date", comment: "") 37 | case .upload: return NSLocalizedString("Upload", comment: "") 38 | case .download: return NSLocalizedString("Download", comment: "") 39 | case .currentUpload: return NSLocalizedString("Upload speed", comment: "") 40 | case .currentDownload: return NSLocalizedString("Download speed", comment: "") 41 | case .type: return NSLocalizedString("Type", comment: "") 42 | } 43 | } 44 | 45 | var compareKeyPath: String? { 46 | switch self { 47 | case .statusIcon, .status: return "status" 48 | case .process: return "metadata.processName" 49 | case .rule: return "rule" 50 | case .url: return "metadata.displayHost" 51 | case .date: return "start" 52 | case .upload: return "upload" 53 | case .download: return "download" 54 | case .currentUpload: return "uploadSpeed" 55 | case .currentDownload: return "downloadSpeed" 56 | case .type: return "metadata.network" 57 | } 58 | } 59 | 60 | static func isDynamicSort(for keypath: String) -> Bool { 61 | return keypath == "upload" || keypath == "download" || keypath == "uploadSpeed" || keypath == "downloadSpeed" || keypath == "done" 62 | } 63 | 64 | var minWidth: CGFloat { 65 | switch self { 66 | case .statusIcon: return 16 67 | case .status: return 30 68 | default: return 60 69 | } 70 | } 71 | 72 | var width: CGFloat { 73 | switch self { 74 | case .upload, .download, .currentUpload, .currentDownload: return 80 75 | case .status: return 50 76 | default: return 100 77 | } 78 | } 79 | 80 | var maxWidth: CGFloat { 81 | switch self { 82 | case .statusIcon: return 16 83 | default: return CGFloat.greatestFiniteMagnitude 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /ClashX/Models/ClashConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClashConfig.swift 3 | // ClashX 4 | // 5 | // Created by CYC on 2018/7/30. 6 | // Copyright © 2018年 yichengchen. All rights reserved. 7 | // 8 | import CocoaLumberjack 9 | import Foundation 10 | 11 | enum ClashProxyMode: String, Codable { 12 | case rule 13 | case global 14 | case direct 15 | #if PRO_VERSION 16 | case script 17 | #endif 18 | } 19 | 20 | extension ClashProxyMode { 21 | var name: String { 22 | switch self { 23 | case .rule: return NSLocalizedString("Rule", comment: "") 24 | case .global: return NSLocalizedString("Global", comment: "") 25 | case .direct: return NSLocalizedString("Direct", comment: "") 26 | #if PRO_VERSION 27 | case .script: return NSLocalizedString("Script", comment: "") 28 | #endif 29 | } 30 | } 31 | } 32 | 33 | enum ClashLogLevel: String, Codable { 34 | case info 35 | #if PRO_VERSION 36 | case warning = "warn" 37 | #else 38 | case warning 39 | #endif 40 | case error 41 | case debug 42 | case silent 43 | case unknow = "unknown" 44 | 45 | func toDDLogLevel() -> DDLogLevel { 46 | switch self { 47 | case .info: 48 | return .info 49 | case .warning: 50 | return .warning 51 | case .error: 52 | return .error 53 | case .debug: 54 | return .debug 55 | case .silent: 56 | return .off 57 | case .unknow: 58 | return .error 59 | } 60 | } 61 | } 62 | 63 | class ClashConfig: Codable { 64 | private var port: Int 65 | private var socksPort: Int 66 | var allowLan: Bool 67 | var mixedPort: Int 68 | var mode: ClashProxyMode 69 | var logLevel: ClashLogLevel 70 | 71 | var usedHttpPort: Int { 72 | if mixedPort > 0 { 73 | return mixedPort 74 | } 75 | return port 76 | } 77 | 78 | var usedSocksPort: Int { 79 | if mixedPort > 0 { 80 | return mixedPort 81 | } 82 | return socksPort 83 | } 84 | 85 | private enum CodingKeys: String, CodingKey { 86 | case port, socksPort = "socks-port", mixedPort = "mixed-port", allowLan = "allow-lan", mode, logLevel = "log-level" 87 | } 88 | 89 | static func fromData(_ data: Data) -> ClashConfig? { 90 | let decoder = JSONDecoder() 91 | do { 92 | return try decoder.decode(ClashConfig.self, from: data) 93 | } catch let err { 94 | Logger.log((err as NSError).description, level: .error) 95 | return nil 96 | } 97 | } 98 | 99 | func copy() -> ClashConfig? { 100 | guard let data = try? JSONEncoder().encode(self) else { return nil } 101 | let copy = try? JSONDecoder().decode(ClashConfig.self, from: data) 102 | return copy 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /ClashX/General/Utils/JSBridge.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSBridge.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2023/9/26. 6 | // Copyright © 2023 west2online. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import WebKit 11 | 12 | class JSBridge: NSObject { 13 | typealias ResponseCallback = (Any?) -> Void 14 | typealias BridgeHandler = (Any?, @escaping ResponseCallback) -> Void 15 | 16 | private weak var webView: WKWebView? 17 | private var handlers = [String: BridgeHandler]() 18 | init(_ webView: WKWebView) { 19 | self.webView = webView 20 | super.init() 21 | setup() 22 | } 23 | 24 | deinit { 25 | webView?.configuration.userContentController.removeAllUserScripts() 26 | webView?.configuration.userContentController.removeScriptMessageHandler(forName: "jsBridge") 27 | } 28 | 29 | private func setup() { 30 | addScriptMessageHandler() 31 | } 32 | 33 | private func addScriptMessageHandler() { 34 | let scriptMessageHandler = ClashScriptMessageHandler(delegate: self) 35 | webView?.configuration.userContentController.add(scriptMessageHandler, name: "jsBridge") 36 | } 37 | 38 | private func sendBackMessage(data: Any?, eventID: String) { 39 | let data = ["id": eventID, "data": data, "type": "jsBridge"] as [String: Any?] 40 | do { 41 | let jsonData = try JSONSerialization.data(withJSONObject: data, options: []) 42 | let jsonString = String(data: jsonData, encoding: .utf8)! 43 | let str = "window.postMessage(\(jsonString), window.origin);" 44 | webView?.evaluateJavaScript(str) 45 | } catch let err { 46 | Logger.log(err.localizedDescription, level: .warning) 47 | } 48 | } 49 | 50 | func registerHandler(_ name: String, handler: @escaping BridgeHandler) { 51 | handlers[name] = handler 52 | } 53 | } 54 | 55 | extension JSBridge: WKScriptMessageHandler { 56 | func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { 57 | if 58 | let body = message.body as? [String: Any], 59 | let handlerName = body["name"] as? String, 60 | let handler = handlers[handlerName], 61 | let eventID = body["id"] as? String { 62 | let data = body["data"] 63 | handler(data) { [weak self] res in 64 | self?.sendBackMessage(data: res, eventID: eventID) 65 | } 66 | } 67 | } 68 | } 69 | 70 | private class ClashScriptMessageHandler: NSObject, WKScriptMessageHandler { 71 | weak var delegate: WKScriptMessageHandler? 72 | 73 | public init(delegate: WKScriptMessageHandler) { 74 | self.delegate = delegate 75 | super.init() 76 | } 77 | 78 | public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { 79 | delegate?.userContentController(userContentController, didReceive: message) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[Bug]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 感谢你向 ClashX 提交 issue! 12 | 在提交之前,请确认: 13 | 14 | - [ ] 我已经在 [Issue Tracker](……/) 中找过我要提出的问题 15 | - [ ] 这是 ClashX UI层面的问题,并非 Clash Core 的问题(例如xx软件连不上,无法连接特定服务器等)。其他 Clash 衍生版本没有次问题。 16 | - [ ] 如果你可以自己 debug 并解决的话,提交 PR 吧! 17 | 18 | 请注意,如果你并没有遵照这个 issue template 填写内容,我们将直接关闭这个 issue。 19 | 20 | 30 | 31 | 我都确认过了,我要继续提交。 32 | 33 | ------------------------------------------------------------------ 34 | 35 | 请附上任何可以帮助我们解决这个问题的信息,如果我们收到的信息不足,我们将对这个 issue 加上 *Needs more information* 标记并在收到更多资讯之前关闭 issue。 36 | 37 | 38 | ### clashX config 39 | 43 | ``` 44 | …… 45 | ``` 46 | 47 | ### ClashX log 48 | 52 | ``` 53 | …… 54 | ``` 55 | 56 | 57 | ### ClashX Crash log 58 | 62 | ``` 63 | …… 64 | ``` 65 | 66 | 67 | 68 | ### 环境 Environment 69 | …… 70 | * 使用者的操作系统 (the OS running on the client) 71 | …… 72 | * 网路环境或拓扑 (network conditions/topology) 73 | …… 74 | * ISP 有没有进行 DNS 污染 (is your ISP performing DNS pollution?) 75 | …… 76 | * 其他 77 | …… 78 | 79 | ### 说明 Description 80 | 81 | 84 | 85 | ### 重现问题的具体布骤 Steps to Reproduce 86 | 87 | 1. [First Step] 88 | 2. [Second Step] 89 | 3. …… 90 | 91 | **我预期会发生……?** 92 | 93 | 94 | **实际上发生了什麽?** 95 | 96 | 97 | ### 可能的解决方案 Possible Solution 98 | 99 | 100 | 101 | 102 | ### 更多信息 More Information 103 | -------------------------------------------------------------------------------- /ClashX/General/Utils/JSBridgeHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSBridgeHandler.swift 3 | // ClashX 4 | // 5 | // Created by CYC on 2018/8/29. 6 | // Copyright © 2018年 west2online. All rights reserved. 7 | // 8 | 9 | import Alamofire 10 | import SwiftyJSON 11 | import WebKit 12 | 13 | class JsBridgeUtil { 14 | static func initJSbridge(webview: WKWebView, delegate: Any) -> JSBridge { 15 | let bridge = JSBridge(webview) 16 | 17 | bridge.registerHandler("isSystemProxySet") { _, responseCallback in 18 | responseCallback(ConfigManager.shared.proxyPortAutoSet) 19 | } 20 | 21 | bridge.registerHandler("setSystemProxy") { anydata, responseCallback in 22 | if let enable = anydata as? Bool { 23 | ConfigManager.shared.proxyPortAutoSet = enable 24 | if enable { 25 | SystemProxyManager.shared.saveProxy() 26 | SystemProxyManager.shared.enableProxy() 27 | } else { 28 | SystemProxyManager.shared.disableProxy() 29 | } 30 | responseCallback(true) 31 | } else { 32 | responseCallback(false) 33 | } 34 | } 35 | 36 | bridge.registerHandler("getStartAtLogin") { _, responseCallback in 37 | responseCallback(LaunchAtLogin.shared.isEnabled) 38 | } 39 | 40 | bridge.registerHandler("setStartAtLogin") { anydata, responseCallback in 41 | if let enable = anydata as? Bool { 42 | LaunchAtLogin.shared.isEnabled = enable 43 | responseCallback(true) 44 | } else { 45 | responseCallback(false) 46 | } 47 | } 48 | 49 | bridge.registerHandler("speedTest") { anydata, responseCallback in 50 | if let proxyName = anydata as? String { 51 | ApiRequest.getProxyDelay(proxyName: proxyName) { delay in 52 | var resp: Int 53 | if delay == Int.max { 54 | resp = 0 55 | } else { 56 | resp = delay 57 | } 58 | responseCallback(resp) 59 | } 60 | } else { 61 | responseCallback(nil) 62 | } 63 | } 64 | 65 | bridge.registerHandler("apiInfo") { _, callback in 66 | var host = "127.0.0.1" 67 | var port = ConfigManager.shared.apiPort 68 | if let override = ConfigManager.shared.overrideApiURL, 69 | let overridedHost = override.host { 70 | host = overridedHost 71 | port = "\(override.port ?? 80)" 72 | } 73 | let data = [ 74 | "host": host, 75 | "port": port, 76 | "secret": ConfigManager.shared.overrideSecret ?? ConfigManager.shared.apiSecret 77 | ] 78 | callback(data) 79 | } 80 | 81 | // ping-pong 82 | bridge.registerHandler("ping") { _, responseCallback in 83 | responseCallback("pong") 84 | } 85 | return bridge 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /ClashX/ClashWindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClashWindowController.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2023/7/5. 6 | // Copyright © 2023 west2online. All rights reserved. 7 | // 8 | import AppKit 9 | 10 | private class ClashWindowsRecorder { 11 | static let shared = ClashWindowsRecorder() 12 | var windowControllers = [NSWindowController]() { 13 | didSet { 14 | if windowControllers.isEmpty { 15 | NSApp.setActivationPolicy(.accessory) 16 | } else { 17 | if NSApp.activationPolicy() == .accessory { 18 | NSApp.setActivationPolicy(.regular) 19 | } 20 | } 21 | } 22 | } 23 | } 24 | 25 | class ClashWindowController: NSWindowController, NSWindowDelegate { 26 | var onWindowClose: (() -> Void)? 27 | private var fromCache = false 28 | private var lastSize: CGSize? { 29 | get { 30 | if let str = UserDefaults.standard.value(forKey: "lastSize.\(T.className())") as? String { 31 | return NSSizeFromString(str) as CGSize 32 | } 33 | return nil 34 | } 35 | set { 36 | if let size = newValue { 37 | UserDefaults.standard.set(NSStringFromSize(size), forKey: "lastSize.\(T.className())") 38 | } 39 | } 40 | } 41 | 42 | static func create() -> NSWindowController { 43 | if let wc = ClashWindowsRecorder.shared.windowControllers.first(where: { $0 is Self }) { 44 | (wc as? ClashWindowController)?.fromCache = true 45 | return wc 46 | } 47 | let win = NSWindow() 48 | let wc = ClashWindowController(window: win) 49 | if let X = T.self as? NibLoadable.Type { 50 | wc.contentViewController = (X.createFromNib(in: .main) as! NSViewController) 51 | } else { 52 | wc.contentViewController = T() 53 | } 54 | win.titlebarAppearsTransparent = false 55 | win.styleMask.insert(.closable) 56 | win.styleMask.insert(.resizable) 57 | win.styleMask.insert(.miniaturizable) 58 | if let title = wc.contentViewController?.title { 59 | win.title = title 60 | } 61 | ClashWindowsRecorder.shared.windowControllers.append(wc) 62 | return wc 63 | } 64 | 65 | override func showWindow(_ sender: Any?) { 66 | super.showWindow(sender) 67 | NSApp.activate(ignoringOtherApps: true) 68 | if !fromCache, let lastSize = lastSize, lastSize != .zero { 69 | window?.setContentSize(lastSize) 70 | window?.center() 71 | } 72 | window?.makeKeyAndOrderFront(self) 73 | window?.delegate = self 74 | NSApp.activate(ignoringOtherApps: true) 75 | window?.makeKeyAndOrderFront(nil) 76 | } 77 | 78 | func windowWillClose(_ notification: Notification) { 79 | ClashWindowsRecorder.shared.windowControllers.removeAll(where: { $0 == self }) 80 | onWindowClose?() 81 | if let win = window { 82 | if !win.styleMask.contains(.fullScreen) { 83 | lastSize = win.frame.size 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /ClashX/General/Managers/Settings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Settings.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2020/12/18. 6 | // Copyright © 2020 west2online. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | enum Settings { 11 | static let defaultMmdbDownloadUrl = "https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb" 12 | @UserDefault("mmdbDownloadUrl", defaultValue: defaultMmdbDownloadUrl) 13 | static var mmdbDownloadUrl: String 14 | 15 | @UserDefault("filterInterface", defaultValue: true) 16 | static var filterInterface: Bool 17 | 18 | @UserDefault("disableNoti", defaultValue: false) 19 | static var disableNoti: Bool 20 | 21 | @UserDefault("configAutoUpdateInterval", defaultValue: 48 * 60 * 60) 22 | static var configAutoUpdateInterval: TimeInterval 23 | 24 | static let proxyIgnoreListDefaultValue = ["192.168.0.0/16", 25 | "10.0.0.0/8", 26 | "172.16.0.0/12", 27 | "127.0.0.1", 28 | "localhost", 29 | "*.local", 30 | "timestamp.apple.com", 31 | "sequoia.apple.com", 32 | "seed-sequoia.siri.apple.com"] 33 | @UserDefault("proxyIgnoreList", defaultValue: proxyIgnoreListDefaultValue) 34 | static var proxyIgnoreList: [String] 35 | 36 | @UserDefault("disableMenubarNotice", defaultValue: false) 37 | static var disableMenubarNotice: Bool 38 | 39 | @UserDefault("proxyPort", defaultValue: 0) 40 | static var proxyPort: Int 41 | 42 | @UserDefault("apiPort", defaultValue: 0) 43 | static var apiPort: Int 44 | 45 | @UserDefault("apiPortAllowLan", defaultValue: false) 46 | static var apiPortAllowLan: Bool 47 | 48 | @UserDefault("disableSSIDList", defaultValue: []) 49 | static var disableSSIDList: [String] 50 | 51 | @UserDefault("enableIPV6", defaultValue: false) 52 | static var enableIPV6: Bool 53 | 54 | static let apiSecretKey = "api-secret" 55 | 56 | static var isApiSecretSet: Bool { 57 | return UserDefaults.standard.object(forKey: apiSecretKey) != nil 58 | } 59 | 60 | @UserDefault(apiSecretKey, defaultValue: "") 61 | static var apiSecret: String 62 | 63 | @UserDefault("overrideConfigSecret", defaultValue: false) 64 | static var overrideConfigSecret: Bool 65 | 66 | @UserDefault("kBuiltInApiMode", defaultValue: true) 67 | static var builtInApiMode: Bool 68 | 69 | static let disableShowCurrentProxyInMenu = !AppDelegate.isAboveMacOS14 70 | 71 | static let defaultBenchmarkUrl = "http://cp.cloudflare.com/generate_204" 72 | @UserDefault("benchMarkUrl", defaultValue: defaultBenchmarkUrl) 73 | static var benchMarkUrl: String { 74 | didSet { 75 | if benchMarkUrl.isEmpty { 76 | benchMarkUrl = defaultBenchmarkUrl 77 | } 78 | } 79 | } 80 | 81 | @UserDefault("kDisableRestoreProxy", defaultValue: false) 82 | static var disableRestoreProxy: Bool 83 | } 84 | -------------------------------------------------------------------------------- /ClashX/Actions/TerminalCleanUpAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TerminalCleanUpAction.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2023/9/5. 6 | // Copyright © 2023 west2online. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | import Foundation 11 | import RxSwift 12 | 13 | enum TerminalConfirmAction { 14 | static func run() -> NSApplication.TerminateReply { 15 | guard confirmAction() else { 16 | return .terminateCancel 17 | } 18 | let group = DispatchGroup() 19 | var shouldWait = false 20 | 21 | if ConfigManager.shared.proxyPortAutoSet && !ConfigManager.shared.isProxySetByOtherVariable.value || NetworkChangeNotifier.isCurrentSystemSetToClash(looser: true) || 22 | NetworkChangeNotifier.hasInterfaceProxySetToClash() { 23 | Logger.log("ClashX quit need clean proxy setting") 24 | shouldWait = true 25 | group.enter() 26 | 27 | SystemProxyManager.shared.disableProxy(forceDisable: ConfigManager.shared.isProxySetByOtherVariable.value) { 28 | group.leave() 29 | } 30 | } 31 | 32 | if !shouldWait { 33 | Logger.log("ClashX quit without clean waiting") 34 | return .terminateNow 35 | } 36 | 37 | if let statusItem = AppDelegate.shared.statusItem, statusItem.menu != nil { 38 | statusItem.menu = nil 39 | } 40 | AppDelegate.shared.disposeBag = DisposeBag() 41 | 42 | DispatchQueue.global(qos: .default).async { 43 | let res = group.wait(timeout: .now() + 5) 44 | switch res { 45 | case .success: 46 | Logger.log("ClashX quit after clean up finish") 47 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { 48 | NSApp.reply(toApplicationShouldTerminate: true) 49 | } 50 | DispatchQueue.global().asyncAfter(deadline: .now() + 1) { 51 | NSApp.reply(toApplicationShouldTerminate: true) 52 | } 53 | case .timedOut: 54 | Logger.log("ClashX quit after clean up timeout") 55 | DispatchQueue.main.async { 56 | NSApp.reply(toApplicationShouldTerminate: true) 57 | } 58 | DispatchQueue.global().asyncAfter(deadline: .now() + 1) { 59 | NSApp.reply(toApplicationShouldTerminate: true) 60 | } 61 | } 62 | } 63 | 64 | Logger.log("ClashX quit wait for clean up") 65 | return .terminateLater 66 | } 67 | 68 | static func confirmAction() -> Bool { 69 | if NSApp.activationPolicy() == .regular { 70 | let alert = NSAlert() 71 | alert.messageText = NSLocalizedString("Quit ClashX?", comment: "") 72 | alert.informativeText = NSLocalizedString("The active connections will be interrupted.", comment: "") 73 | alert.alertStyle = .informational 74 | alert.addButton(withTitle: NSLocalizedString("Quit", comment: "")) 75 | alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "")) 76 | return alert.runModal() == .alertFirstButtonReturn 77 | } 78 | return true 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /ClashX/ViewControllers/Settings/DebugSettingViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DebugSettingViewController.swift 3 | // ClashX Pro 4 | // 5 | // Created by yicheng on 2023/5/25. 6 | // Copyright © 2023 west2online. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | import RxSwift 11 | 12 | class DebugSettingViewController: NSViewController { 13 | @IBOutlet var useBuiltinApiButton: NSButton! 14 | @IBOutlet var revertProxyButton: NSButton! 15 | @IBOutlet var updateChannelPopButton: NSPopUpButton! 16 | var disposeBag = DisposeBag() 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | useBuiltinApiButton.state = Settings.builtInApiMode ? .on : .off 20 | revertProxyButton.state = Settings.disableRestoreProxy ? .off : .on 21 | AutoUpgardeManager.shared.addChannelMenuItem(updateChannelPopButton) 22 | } 23 | 24 | @IBAction func actionUnInstallProxyHelper(_ sender: Any) { 25 | PrivilegedHelperManager.shared.removeInstallHelper() 26 | } 27 | 28 | @IBAction func actionOpenLogFolder(_ sender: Any) { 29 | NSWorkspace.shared.openFile(Logger.shared.logFolder()) 30 | } 31 | 32 | @IBAction func actionOpenLocalConfig(_ sender: Any) { 33 | NSWorkspace.shared.openFile(kConfigFolderPath) 34 | } 35 | 36 | @IBAction func actionOpenIcloudConfig(_ sender: Any) { 37 | if ICloudManager.shared.icloudAvailable { 38 | ICloudManager.shared.getUrl { 39 | url in 40 | if let url = url { 41 | NSWorkspace.shared.open(url) 42 | } 43 | } 44 | } else { 45 | NSAlert.alert(with: NSLocalizedString("iCloud not available", comment: "")) 46 | } 47 | } 48 | 49 | @IBAction func actionResetUserDefault(_ sender: Any) { 50 | guard let domain = Bundle.main.bundleIdentifier else { return } 51 | NSAlert.alert(with: NSLocalizedString("Click OK to quit the app and apply change.", comment: "")) 52 | UserDefaults.standard.removePersistentDomain(forName: domain) 53 | UserDefaults.standard.synchronize() 54 | NSApplication.shared.terminate(self) 55 | } 56 | 57 | @IBAction func actionSetUseApiMode(_ sender: Any) { 58 | let alert = NSAlert() 59 | alert.informativeText = NSLocalizedString("Need to Restart the ClashX to Take effect, Please start clashX manually", comment: "") 60 | alert.addButton(withTitle: NSLocalizedString("Apply and Quit", comment: "")) 61 | alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "")) 62 | if alert.runModal() == .alertFirstButtonReturn { 63 | Settings.builtInApiMode = !Settings.builtInApiMode 64 | NSApp.terminate(nil) 65 | } else { 66 | useBuiltinApiButton.state = Settings.builtInApiMode ? .on : .off 67 | } 68 | } 69 | 70 | @IBAction func actionUpdateGeoipDb(_ sender: Any) { 71 | ClashResourceManager.updateGeoIP() 72 | } 73 | 74 | @IBAction func actionRevertProxy(_ sender: Any) { 75 | Settings.disableRestoreProxy.toggle() 76 | revertProxyButton.state = Settings.disableRestoreProxy ? .off : .on 77 | } 78 | 79 | @IBAction func actionOpenCrashLogFolder(_ sender: Any) { 80 | NSWorkspace.shared.open(URL(fileURLWithPath: "\(NSHomeDirectory())/Library/Logs/DiagnosticReports", isDirectory: true)) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /ClashX.xcodeproj/xcshareddata/xcschemes/com.west2online.ClashX.ProxyConfigHelper.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 44 | 46 | 52 | 53 | 54 | 55 | 61 | 63 | 69 | 70 | 71 | 72 | 74 | 75 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /ClashX.xcodeproj/xcshareddata/xcschemes/ClashX.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Clash 3 |
4 | ClashX 5 |
6 |

7 | 8 | 9 | A rule based proxy For Mac base on [Clash](https://github.com/Dreamacro/clash). 10 | 11 | ClashX 旨在提供一个简单轻量化的代理客户端,如果需要更多的定制化,可以考虑使用 [CFW Mac 版](https://github.com/Fndroid/clash_for_windows_pkg/releases) 12 | 13 | 14 | ## 注意 15 | - ClashX / ClashX Pro 只是一个代理工具,不提供任何代理服务器。如果服务器不可用或与服务器续费有关的问题,请与您的提供商联系。 16 | - ClashX / ClashX Pro 目前并没有创建官网。凡是声称是 ClashX / ClashX Pro 官网的一定是骗子。 17 | 18 | ## Features 19 | 20 | - HTTP/HTTPS and SOCKS protocol 21 | - Surge like configuration 22 | - GeoIP rule support 23 | - Support Vmess/Shadowsocks/Socks5/Trojan 24 | - Support for Netfilter TCP redirect 25 | 26 | ## Install 27 | 28 | You can download from [Release](https://github.com/yichengchen/clashX/releases) page 29 | 30 | **Download ClashX Pro With enhanced mode and other clash premium feature at [AppCenter](https://install.appcenter.ms/users/clashx/apps/clashx-pro/distribution_groups/public) for free permanently.** 31 | 32 | **在 [AppCenter](https://install.appcenter.ms/users/clashx/apps/clashx-pro/distribution_groups/public) 免费下载ClashX Pro版本,支持增强模式以及更多Clash Premium Core特性。** 33 | 34 | ## Build 35 | - Make sure have python3 and golang installed in your computer. 36 | 37 | - Install Golang 38 | ``` 39 | brew install golang 40 | 41 | or download from https://golang.org 42 | ``` 43 | 44 | - Download deps 45 | ``` 46 | bash install_dependency.sh 47 | ``` 48 | 49 | - Build and run. 50 | 51 | ## Config 52 | 53 | 54 | The default configuration directory is `$HOME/.config/clash` 55 | 56 | The default name of the configuration file is `config.yaml`. You can use your custom config name and switch config in menu `Config` section. 57 | 58 | 59 | Checkout [Clash](https://github.com/Dreamacro/clash) or [SS-Rule-Snippet for Clash](https://github.com/Hackl0us/SS-Rule-Snippet/blob/master/LAZY_RULES/clash.yaml) or [lancellc's gitbook](https://lancellc.gitbook.io/clash/) for more detail. 60 | 61 | ## Advance Config 62 | 63 | ### 修改代理端口号 64 | 1. 在菜单栏->配置->更多设置中修改对应端口号 65 | 66 | 67 | 68 | ### Change your status menu icon 69 | 70 | Place your icon file in the `~/.config/clash/menuImage.png` then restart ClashX 71 | 72 | ### Change default system ignore list. 73 | 74 | - Change by menu -> Config -> Setting -> Bypass proxy settings for these Hosts & Domains 75 | 76 | ### URL Schemes. 77 | 78 | - Using url scheme to import remote config. 79 | 80 | ``` 81 | clash://install-config?url=http%3A%2F%2Fexample.com&name=example 82 | ``` 83 | - Using url scheme to reload current config. 84 | 85 | ``` 86 | clash://update-config 87 | ``` 88 | 89 | ### Get process name 90 | 91 | You can add the follow config in your config file, and set your proxy mode to rule. Then open the log via help menu in ClashX. 92 | ``` 93 | script: 94 | code: | 95 | def main(ctx, metadata): 96 | # Log ProcessName 97 | ctx.log('Process Name: ' + ctx.resolve_process_name(metadata)) 98 | return 'DIRECT' 99 | ``` 100 | 101 | ### FAQ 102 | 103 | - Q: How to get shell command with external IP? 104 | A: Click the clashX menu icon and then press `Option-Command-C` 105 | 106 | ### 关闭ClashX的通知 107 | 108 | 1. 在系统设置中关闭 clashx 的推送权限 109 | 2. 在菜单栏->配置->更多设置中选中减少通知 110 | 111 | Note:强烈不推荐这么做,这可能导致clashx的很多重要错误提醒无法显示。 112 | 113 | ### 全局快捷键 114 | - 在菜单栏配置->更多配置中,自定义对应功能的快捷键。(需要1.116.1之后的版本) 115 | - 使用AppleScript设置, 详情点击 [全局快捷键](Shortcuts.md) 116 | -------------------------------------------------------------------------------- /ClashX/General/Managers/SystemProxyManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SystemProxyManager.swift 3 | // ClashX 4 | // 5 | // Created by yichengchen on 2019/8/17. 6 | // Copyright © 2019 west2online. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | import ServiceManagement 11 | 12 | class SystemProxyManager: NSObject { 13 | static let shared = SystemProxyManager() 14 | 15 | private var savedProxyInfo: [String: Any] { 16 | get { 17 | return UserDefaults.standard.dictionary(forKey: "kSavedProxyInfo") ?? [:] 18 | } 19 | set { 20 | UserDefaults.standard.set(newValue, forKey: "kSavedProxyInfo") 21 | } 22 | } 23 | 24 | private var helper: ProxyConfigRemoteProcessProtocol? { 25 | PrivilegedHelperManager.shared.helper() 26 | } 27 | 28 | func saveProxy() { 29 | guard !Settings.disableRestoreProxy else { return } 30 | Logger.log("saveProxy", level: .debug) 31 | helper?.getCurrentProxySetting { [weak self] info in 32 | Logger.log("saveProxy done", level: .debug) 33 | if let info = info as? [String: Any] { 34 | self?.savedProxyInfo = info 35 | } 36 | } 37 | } 38 | 39 | func enableProxy() { 40 | let port = ConfigManager.shared.currentConfig?.usedHttpPort ?? 0 41 | let socketPort = ConfigManager.shared.currentConfig?.usedSocksPort ?? 0 42 | enableProxy(port: port, socksPort: socketPort) 43 | } 44 | 45 | func enableProxy(port: Int, socksPort: Int) { 46 | guard port > 0 && socksPort > 0 else { 47 | Logger.log("enableProxy fail: \(port) \(socksPort)", level: .error) 48 | return 49 | } 50 | if SSIDSuspendTool.shared.shouldSuspend() { 51 | Logger.log("not enableProxy due to ssid in disabled list", level: .info) 52 | return 53 | } 54 | Logger.log("enableProxy", level: .debug) 55 | helper?.enableProxy(withPort: Int32(port), 56 | socksPort: Int32(socksPort), 57 | pac: nil, 58 | filterInterface: Settings.filterInterface, 59 | ignoreList: Settings.proxyIgnoreList, 60 | error: { error in 61 | if let error = error { 62 | Logger.log("enableProxy \(error)", level: .error) 63 | } 64 | }) 65 | } 66 | 67 | func disableProxy(forceDisable: Bool = false, complete: (() -> Void)? = nil) { 68 | let port = ConfigManager.shared.currentConfig?.usedHttpPort ?? 0 69 | let socketPort = ConfigManager.shared.currentConfig?.usedSocksPort ?? 0 70 | SystemProxyManager.shared.disableProxy(port: port, socksPort: socketPort, forceDisable: forceDisable, complete: complete) 71 | } 72 | 73 | func disableProxy(port: Int, socksPort: Int, forceDisable: Bool = false, complete: (() -> Void)? = nil) { 74 | Logger.log("disableProxy", level: .debug) 75 | 76 | if Settings.disableRestoreProxy || forceDisable { 77 | helper?.disableProxy(withFilterInterface: Settings.filterInterface) { error in 78 | if let error = error { 79 | Logger.log("disableProxy \(error)", level: .error) 80 | } 81 | complete?() 82 | } 83 | return 84 | } 85 | 86 | helper?.restoreProxy(withCurrentPort: Int32(port), socksPort: Int32(socksPort), info: savedProxyInfo, filterInterface: Settings.filterInterface, error: { error in 87 | if let error = error { 88 | Logger.log("restoreProxy \(error)", level: .error) 89 | } 90 | complete?() 91 | }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /ClashX/ViewControllers/Connections/ViewModels/ConnectionLeftPannelViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectionLeftPannelViewModel.swift 3 | // ClashX 4 | // 5 | // Created by miniLV on 2023-07-10. 6 | // Copyright © 2023 west2online. All rights reserved. 7 | // 8 | 9 | import Combine 10 | 11 | @available(macOS 10.15, *) 12 | class ConnectionLeftPannelViewModel { 13 | enum Section: Int, CaseIterable { 14 | case all 15 | case local 16 | case remote 17 | case hosts 18 | } 19 | 20 | private(set) var currentSections = [Section.all, Section.local, Section.remote] 21 | private(set) var localApplications = [ConnectionApplication]() 22 | private(set) var sources = [String]() 23 | private(set) var hosts = [String]() 24 | private(set) var isHostMode = false 25 | var onReloadTable: ((IndexPath) -> Void)? 26 | var onSelectedFilter: ((ConnectionFilter?) -> Void)? 27 | var selectedFilter: ConnectionFilter? 28 | 29 | func accept(connections new: [ConnectionApplication]) { 30 | var dupSet = Set() 31 | localApplications = new.filter { dupSet.insert($0.path ?? $0.pid).inserted } 32 | .sorted(by: { $0.name ?? "" < $1.name ?? "" }) 33 | if !isHostMode { 34 | onReloadTable?(getSelectedIndexPath()) 35 | } 36 | } 37 | 38 | func accept(sources new: [String]) { 39 | sources = new.sorted() 40 | if !isHostMode { 41 | onReloadTable?(getSelectedIndexPath()) 42 | } 43 | } 44 | 45 | func accept(hosts new: [String]) { 46 | hosts = new.sorted() 47 | if isHostMode { 48 | onReloadTable?(getSelectedIndexPath()) 49 | } 50 | } 51 | 52 | func accept(apps: [ConnectionApplication], sources: [String], hosts: [String]) { 53 | var dupSet = Set() 54 | localApplications = apps.filter { dupSet.insert($0.path ?? $0.pid).inserted } 55 | .sorted(by: { $0.name ?? "" < $1.name ?? "" }) 56 | self.sources = sources.sorted() 57 | self.hosts = hosts.sorted() 58 | onReloadTable?(getSelectedIndexPath()) 59 | } 60 | 61 | func setHostMode(enable: Bool) { 62 | isHostMode = enable 63 | selectedFilter = nil 64 | onSelectedFilter?(nil) 65 | currentSections = enable ? [.all, .hosts] : [.all, .local, .remote] 66 | onReloadTable?(getSelectedIndexPath()) 67 | } 68 | 69 | func getSelectedIndexPath() -> IndexPath { 70 | switch selectedFilter { 71 | case .none: 72 | break 73 | case let .application(path): 74 | if let idx = localApplications.firstIndex(where: { ($0.path ?? $0.pid) == path }) { 75 | return IndexPath(item: idx, section: 1) 76 | } 77 | case let .source(ip): 78 | if let idx = sources.firstIndex(where: { $0 == ip }) { 79 | return IndexPath(item: idx, section: 2) 80 | } 81 | case let .hosts(name): 82 | if let idx = hosts.firstIndex(where: { $0 == name }) { 83 | return IndexPath(item: idx, section: 1) 84 | } 85 | } 86 | return IndexPath(item: 0, section: 0) 87 | } 88 | 89 | func setSelect(indexPath: IndexPath) { 90 | if indexPath.item < 0 || indexPath.section < 0 { 91 | selectedFilter = nil 92 | onSelectedFilter?(nil) 93 | return 94 | } 95 | 96 | let type = currentSections[indexPath.section] 97 | switch type { 98 | case Section.local: 99 | let app = localApplications[indexPath.item] 100 | selectedFilter = .application(path: app.path ?? app.pid) 101 | case Section.remote: 102 | selectedFilter = .source(ip: sources[indexPath.item]) 103 | case .hosts: 104 | selectedFilter = .hosts(name: hosts[indexPath.item]) 105 | case .all: 106 | selectedFilter = nil 107 | } 108 | onSelectedFilter?(selectedFilter) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /ClashX/General/Managers/PrivilegedHelperManager+Legacy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrivilegedHelperManager+Legacy.swift 3 | // ClashX 4 | // 5 | // Created by yicheng 2020/4/22. 6 | // Copyright © 2020 west2online. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | extension PrivilegedHelperManager { 12 | func getInstallScript() -> String { 13 | let appPath = Bundle.main.bundlePath 14 | let bash = """ 15 | #!/bin/bash 16 | set -e 17 | 18 | plistPath=/Library/LaunchDaemons/\(PrivilegedHelperManager.machServiceName).plist 19 | rm -rf /Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName) 20 | if [ -e ${plistPath} ]; then 21 | launchctl unload -w ${plistPath} 22 | rm ${plistPath} 23 | fi 24 | launchctl remove \(PrivilegedHelperManager.machServiceName) || true 25 | 26 | mkdir -p /Library/PrivilegedHelperTools/ 27 | rm -f /Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName) 28 | 29 | cp "\(appPath)/Contents/Library/LaunchServices/\(PrivilegedHelperManager.machServiceName)" "/Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName)" 30 | 31 | echo ' 32 | 33 | 34 | 35 | 36 | Label 37 | \(PrivilegedHelperManager.machServiceName) 38 | MachServices 39 | 40 | \(PrivilegedHelperManager.machServiceName) 41 | 42 | 43 | Program 44 | /Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName) 45 | ProgramArguments 46 | 47 | /Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName) 48 | 49 | 50 | 51 | ' > ${plistPath} 52 | 53 | launchctl load -w ${plistPath} 54 | """ 55 | return bash 56 | } 57 | 58 | func runScriptWithRootPermission(script: String) { 59 | let tmpPath = FileManager.default.temporaryDirectory.appendingPathComponent(NSUUID().uuidString).appendingPathExtension("sh") 60 | do { 61 | try script.write(to: tmpPath, atomically: true, encoding: .utf8) 62 | let appleScriptStr = "do shell script \"bash \(tmpPath.path) \" with administrator privileges" 63 | let appleScript = NSAppleScript(source: appleScriptStr) 64 | var dict: NSDictionary? 65 | if appleScript?.executeAndReturnError(&dict) == nil { 66 | Logger.log("apple script failed") 67 | } else { 68 | Logger.log("apple script result: \(String(describing: dict))") 69 | } 70 | } catch let err { 71 | Logger.log("legacyInstallHelper create script fail: \(err)") 72 | } 73 | try? FileManager.default.removeItem(at: tmpPath) 74 | } 75 | 76 | func legacyInstallHelper() { 77 | defer { 78 | resetConnection() 79 | Thread.sleep(forTimeInterval: 1) 80 | } 81 | let script = getInstallScript() 82 | runScriptWithRootPermission(script: script) 83 | } 84 | 85 | func removeInstallHelper() { 86 | defer { 87 | resetConnection() 88 | Thread.sleep(forTimeInterval: 5) 89 | } 90 | let script = """ 91 | /bin/launchctl remove \(PrivilegedHelperManager.machServiceName) || true 92 | /usr/bin/killall -u root -9 \(PrivilegedHelperManager.machServiceName) 93 | /bin/rm -rf /Library/LaunchDaemons/\(PrivilegedHelperManager.machServiceName).plist 94 | /bin/rm -rf /Library/PrivilegedHelperTools/\(PrivilegedHelperManager.machServiceName) 95 | """ 96 | 97 | runScriptWithRootPermission(script: script) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /ClashX/General/Managers/AutoUpgardeManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AutoUpgardeManager.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2019/10/28. 6 | // Copyright © 2019 west2online. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Sparkle 11 | 12 | class AutoUpgardeManager: NSObject { 13 | var checkForUpdatesMenuItem: NSMenuItem? 14 | static let shared = AutoUpgardeManager() 15 | private var controller: SPUStandardUpdaterController? 16 | private var current: Channel = { 17 | if let value = UserDefaults.standard.object(forKey: "AutoUpgardeManager.current") as? Int, 18 | let channel = Channel(rawValue: value) { return channel } 19 | #if PRO_VERSION 20 | return .appcenter 21 | #else 22 | return .stable 23 | #endif 24 | }() { 25 | didSet { 26 | UserDefaults.standard.set(current.rawValue, forKey: "AutoUpgardeManager.current") 27 | } 28 | } 29 | 30 | private var allowSelectChannel: Bool { 31 | return Bundle.main.object(forInfoDictionaryKey: "SUDisallowSelectChannel") as? Bool != true 32 | } 33 | 34 | // MARK: Public 35 | 36 | func setup() { 37 | controller = SPUStandardUpdaterController(updaterDelegate: self, userDriverDelegate: nil) 38 | } 39 | 40 | func setupCheckForUpdatesMenuItem(_ item: NSMenuItem) { 41 | checkForUpdatesMenuItem = item 42 | checkForUpdatesMenuItem?.target = controller 43 | checkForUpdatesMenuItem?.action = #selector(SPUStandardUpdaterController.checkForUpdates(_:)) 44 | } 45 | 46 | func addChannelMenuItem(_ button: NSPopUpButton) { 47 | for channel in Channel.allCases { 48 | button.addItem(withTitle: channel.title) 49 | button.lastItem?.tag = channel.rawValue 50 | } 51 | button.target = self 52 | button.action = #selector(didselectChannel(sender:)) 53 | button.selectItem(withTag: current.rawValue) 54 | } 55 | 56 | @objc func didselectChannel(sender: NSPopUpButton) { 57 | guard let tag = sender.selectedItem?.tag, let channel = Channel(rawValue: tag) else { return } 58 | current = channel 59 | } 60 | } 61 | 62 | extension AutoUpgardeManager: SPUUpdaterDelegate { 63 | func feedURLString(for updater: SPUUpdater) -> String? { 64 | guard WebPortalManager.hasWebProtal == false, allowSelectChannel else { return nil } 65 | return current.urlString 66 | } 67 | 68 | func updaterWillRelaunchApplication(_ updater: SPUUpdater) { 69 | SystemProxyManager.shared.disableProxy(port: 0, socksPort: 0, forceDisable: true) 70 | } 71 | } 72 | 73 | // MARK: - Channel Enum 74 | 75 | extension AutoUpgardeManager { 76 | enum Channel: Int, CaseIterable { 77 | #if !PRO_VERSION 78 | case stable 79 | case prelease 80 | #endif 81 | case appcenter 82 | } 83 | } 84 | 85 | extension AutoUpgardeManager.Channel { 86 | var title: String { 87 | switch self { 88 | #if !PRO_VERSION 89 | case .stable: 90 | return NSLocalizedString("Stable", comment: "") 91 | case .prelease: 92 | return NSLocalizedString("Prelease", comment: "") 93 | #endif 94 | case .appcenter: 95 | return "Appcenter" 96 | } 97 | } 98 | 99 | var urlString: String { 100 | switch self { 101 | #if !PRO_VERSION 102 | case .stable: 103 | return "https://yichengchen.github.io/clashX/appcast.xml" 104 | case .prelease: 105 | return "https://yichengchen.github.io/clashX/appcast_pre.xml" 106 | #endif 107 | case .appcenter: 108 | #if PRO_VERSION 109 | return "https://api.appcenter.ms/v0.1/public/sparkle/apps/1cd052f7-e118-4d13-87fb-35176f9702c1" 110 | #else 111 | return "https://api.appcenter.ms/v0.1/public/sparkle/apps/dce6e9a3-b6e3-4fd2-9f2d-35c767a99663" 112 | #endif 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /ClashX/ViewControllers/Connections/Views/Cell/ConnectionLeftTextCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectionLeftTextCellView.swift 3 | // ClashX 4 | // 5 | // Created by miniLV on 2023-07-10. 6 | // Copyright © 2023 west2online. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | 11 | @available(macOS 10.15, *) 12 | class ConnectionApplicationCellView: NSView { 13 | let imageView = NSImageView() 14 | let nameLabel = NSTextField(labelWithString: "") 15 | override init(frame: CGRect) { 16 | super.init(frame: frame) 17 | setupUI() 18 | } 19 | 20 | required init?(coder: NSCoder) { 21 | super.init(coder: coder) 22 | setupUI() 23 | } 24 | 25 | func setupUI() { 26 | addSubview(nameLabel) 27 | addSubview(imageView) 28 | nameLabel.font = NSFont.systemFont(ofSize: 13) 29 | imageView.makeConstraints { 30 | [$0.heightAnchor.constraint(equalToConstant: 23), 31 | $0.widthAnchor.constraint(equalTo: $0.heightAnchor), 32 | $0.centerYAnchor.constraint(equalTo: centerYAnchor), 33 | $0.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 6)] 34 | } 35 | nameLabel.makeConstraints { 36 | [ 37 | $0.centerYAnchor.constraint(equalTo: centerYAnchor), 38 | $0.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 5), 39 | $0.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor) 40 | ] 41 | } 42 | nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) 43 | nameLabel.cell?.truncatesLastVisibleLine = true 44 | } 45 | 46 | func setup(with connection: ConnectionApplication) { 47 | nameLabel.stringValue = connection.name ?? NSLocalizedString("Unknown", comment: "") 48 | imageView.image = connection.image 49 | } 50 | } 51 | 52 | class ConnectionLeftTextCellView: NSView { 53 | let nameLabel = NSTextField(labelWithString: "") 54 | override init(frame: CGRect) { 55 | super.init(frame: frame) 56 | setupUI() 57 | } 58 | 59 | required init?(coder: NSCoder) { 60 | super.init(coder: coder) 61 | setupUI() 62 | } 63 | 64 | func setupUI() { 65 | addSubview(nameLabel) 66 | nameLabel.font = NSFont.systemFont(ofSize: 13) 67 | nameLabel.makeConstraints { 68 | [ 69 | $0.centerYAnchor.constraint(equalTo: centerYAnchor), 70 | $0.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 6), 71 | $0.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor) 72 | ] 73 | } 74 | nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) 75 | nameLabel.cell?.truncatesLastVisibleLine = true 76 | } 77 | 78 | func setup(with text: String) { 79 | nameLabel.stringValue = text 80 | } 81 | } 82 | 83 | class ApplicationClientSectionCell: NSTableCellView { 84 | let titleLabel = NSTextField(labelWithString: "") 85 | override init(frame: CGRect) { 86 | super.init(frame: frame) 87 | setupUI() 88 | } 89 | 90 | required init?(coder: NSCoder) { 91 | super.init(coder: coder) 92 | setupUI() 93 | } 94 | 95 | func setupUI() { 96 | addSubview(titleLabel) 97 | titleLabel.font = NSFont.systemFont(ofSize: 10) 98 | titleLabel.textColor = NSColor.secondaryLabelColor 99 | titleLabel.makeConstraints { 100 | [ 101 | $0.centerYAnchor.constraint(equalTo: centerYAnchor), 102 | $0.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0), 103 | $0.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor) 104 | ] 105 | } 106 | titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) 107 | titleLabel.cell?.truncatesLastVisibleLine = true 108 | } 109 | 110 | func setup(with title: String) { 111 | titleLabel.stringValue = title 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /ClashX/Vendor/Witness/EventStream.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventStream.swift 3 | // Witness 4 | // 5 | // Created by Niels de Hoog on 23/09/15. 6 | // Copyright © 2015 Invisible Pixel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | * The type of event stream to be used. For more information, please refer to the File System Events Programming Guide: https://developer.apple.com/library/mac/documentation/Darwin/Conceptual/FSEvents_ProgGuide/UsingtheFSEventsFramework/UsingtheFSEventsFramework.html#//apple_ref/doc/uid/TP40005289-CH4-SW6 13 | */ 14 | 15 | public enum StreamType { 16 | case hostBased // default 17 | case diskBased 18 | } 19 | 20 | class EventStream { 21 | let paths: [String] 22 | 23 | // use explicitly unwrapped optional so we can pass self as context to stream 24 | private var stream: FSEventStreamRef! 25 | private let changeHandler: FileEventHandler 26 | 27 | init(paths: [String], type: StreamType = .hostBased, flags: EventStreamCreateFlags, latency: TimeInterval, deviceToWatch: dev_t = 0, changeHandler: @escaping FileEventHandler) { 28 | self.paths = paths 29 | self.changeHandler = changeHandler 30 | 31 | func callBack(stream: ConstFSEventStreamRef, clientCallbackInfo: UnsafeMutableRawPointer?, numEvents: Int, eventPaths: UnsafeMutableRawPointer, eventFlags: UnsafePointer, eventIDs: UnsafePointer) { 32 | let eventStream = unsafeBitCast(clientCallbackInfo, to: EventStream.self) 33 | let paths = unsafeBitCast(eventPaths, to: NSArray.self) 34 | 35 | var events = [FileEvent]() 36 | for i in 0 ..< Int(numEvents) { 37 | let event = FileEvent(path: paths[i] as! String, flags: FileEventFlags(rawValue: eventFlags[i])) 38 | events.append(event) 39 | } 40 | 41 | eventStream.changeHandler(events) 42 | } 43 | 44 | var context = FSEventStreamContext() 45 | context.info = unsafeBitCast(self, to: UnsafeMutableRawPointer.self) 46 | 47 | let combinedFlags = flags.union(.UseCFTypes) 48 | 49 | switch type { 50 | case .hostBased: 51 | stream = FSEventStreamCreate(nil, callBack, &context, paths as CFArray, FSEventStreamEventId(kFSEventStreamEventIdSinceNow), latency, combinedFlags.rawValue) 52 | case .diskBased: 53 | stream = FSEventStreamCreateRelativeToDevice(nil, callBack, &context, deviceToWatch, paths as CFArray, FSEventStreamEventId(kFSEventStreamEventIdSinceNow), latency, combinedFlags.rawValue) 54 | } 55 | 56 | FSEventStreamScheduleWithRunLoop(stream, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue) 57 | FSEventStreamStart(stream) 58 | } 59 | 60 | func flush() { 61 | FSEventStreamFlushSync(stream) 62 | } 63 | 64 | func flushAsync() { 65 | FSEventStreamFlushAsync(stream) 66 | } 67 | 68 | deinit { 69 | // stop stream 70 | FSEventStreamStop(stream) 71 | 72 | // unschedule from all run loops 73 | FSEventStreamInvalidate(stream) 74 | 75 | // release 76 | FSEventStreamRelease(stream) 77 | } 78 | } 79 | 80 | public struct EventStreamCreateFlags: OptionSet { 81 | public let rawValue: FSEventStreamCreateFlags 82 | public init(rawValue: FSEventStreamCreateFlags) { self.rawValue = rawValue } 83 | init(_ value: Int) { rawValue = FSEventStreamCreateFlags(value) } 84 | 85 | public static let None = EventStreamCreateFlags(kFSEventStreamCreateFlagNone) 86 | 87 | // setting the UseCFTypes flag has no consequences, because Witness will always enable it 88 | public static let UseCFTypes = EventStreamCreateFlags(kFSEventStreamCreateFlagUseCFTypes) 89 | public static let NoDefer = EventStreamCreateFlags(kFSEventStreamCreateFlagNoDefer) 90 | public static let WatchRoot = EventStreamCreateFlags(kFSEventStreamCreateFlagWatchRoot) 91 | public static let IgnoreSelf = EventStreamCreateFlags(kFSEventStreamCreateFlagIgnoreSelf) 92 | public static let FileEvents = EventStreamCreateFlags(kFSEventStreamCreateFlagFileEvents) 93 | public static let MarkSelf = EventStreamCreateFlags(kFSEventStreamCreateFlagMarkSelf) 94 | } 95 | -------------------------------------------------------------------------------- /ClashX/ViewControllers/Connections/ViewModels/ConnectionDetailViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectionDetailViewModel.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2023/7/8. 6 | // Copyright © 2023 west2online. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | import Combine 11 | 12 | @available(macOS 10.15, *) 13 | class ConnectionDetailViewModel { 14 | @Published var processName = "" 15 | @Published var processImage: NSImage? 16 | @Published var remoteHost = "" 17 | 18 | @Published var entry = "" 19 | @Published var networkType = "" 20 | @Published var totalUpload = "" 21 | @Published var totalDownload = "" 22 | @Published var maxUpload = "" 23 | @Published var maxDownload = "" 24 | @Published var currentUpload = "" 25 | @Published var currentDownload = "" 26 | @Published var rule = "" 27 | @Published var chain = "" 28 | @Published var sourceIP = "" 29 | @Published var destination = "" 30 | @Published var applicationPath: String? = "" 31 | @Published var otherText = "" 32 | @Published var showCloseButton = false 33 | 34 | private var uuid = "" 35 | var cancellable = Set() 36 | 37 | func accept(connection: ClashConnectionSnapShot.Connection?) { 38 | cancellable.removeAll() 39 | guard let connection else { return } 40 | if let pid = connection.metadata.pid { 41 | processName = "\(connection.metadata.processName ?? NSLocalizedString("Unknown", comment: "")) (\(pid))" 42 | } else { 43 | processName = connection.metadata.processName ?? NSLocalizedString("Unknown", comment: "") 44 | } 45 | uuid = connection.id 46 | showCloseButton = connection.status == .connecting 47 | processImage = connection.metadata.processImage 48 | applicationPath = connection.metadata.processPath 49 | let area = clash_getCountryForIp(connection.metadata.destinationIP.goStringBuffer()).toString() 50 | let areaString = "\(flag(from: area))\(area)" 51 | if connection.metadata.host.isEmpty { 52 | remoteHost = "\(connection.metadata.destinationIP):\(connection.metadata.destinationPort) \(areaString)" 53 | } else { 54 | remoteHost = "\(connection.metadata.host):\(connection.metadata.destinationPort) \(areaString)" 55 | } 56 | 57 | entry = connection.metadata.type 58 | networkType = connection.metadata.network 59 | 60 | connection.$download.map { SpeedUtils.getNetString(for: $0) }.weakAssign(to: \.totalDownload, on: self).store(in: &cancellable) 61 | connection.$upload.map { SpeedUtils.getNetString(for: $0) }.weakAssign(to: \.totalUpload, on: self).store(in: &cancellable) 62 | 63 | connection.$maxUploadSpeed.map { SpeedUtils.getSpeedString(for: $0) }.weakAssign(to: \.maxUpload, on: self).store(in: &cancellable) 64 | connection.$maxDownloadSpeed.map { SpeedUtils.getSpeedString(for: $0) }.weakAssign(to: \.maxDownload, on: self).store(in: &cancellable) 65 | 66 | connection.$uploadSpeed.map { SpeedUtils.getSpeedString(for: $0) }.weakAssign(to: \.currentUpload, on: self).store(in: &cancellable) 67 | connection.$downloadSpeed.map { SpeedUtils.getSpeedString(for: $0) }.weakAssign(to: \.currentDownload, on: self).store(in: &cancellable) 68 | 69 | rule = connection.rule + "\n" + connection.rulePayload 70 | chain = connection.chains.joined(separator: "\n") 71 | sourceIP = connection.metadata.sourceIP.appending(":").appending(connection.metadata.sourcePort) 72 | destination = connection.metadata.destinationIP.appending(":").appending(connection.metadata.destinationPort) 73 | if let error = connection.error { 74 | otherText = error 75 | } else { 76 | otherText = "" 77 | } 78 | } 79 | 80 | func flag(from country: String) -> String { 81 | if country.isEmpty { return "" } 82 | let base: UInt32 = 127397 83 | var s = "" 84 | for v in country.uppercased().unicodeScalars { 85 | s.unicodeScalars.append(UnicodeScalar(base + v.value)!) 86 | } 87 | return s 88 | } 89 | 90 | func closeConnection() { 91 | ApiRequest.closeConnection(uuid) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /ClashX/General/Managers/ICloudManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ICloudManager.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2020/5/10. 6 | // Copyright © 2020 west2online. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import RxCocoa 11 | import RxSwift 12 | 13 | class ICloudManager { 14 | static let shared = ICloudManager() 15 | private let queue = DispatchQueue(label: "com.clashx.icloud") 16 | private var metaQuery: NSMetadataQuery? 17 | private var enableMenuItem: NSMenuItem? 18 | private(set) var icloudAvailable = false { 19 | didSet { useiCloud.accept(userEnableiCloud && icloudAvailable) } 20 | } 21 | 22 | private var disposeBag = DisposeBag() 23 | 24 | let useiCloud = BehaviorRelay(value: false) 25 | 26 | var userEnableiCloud: Bool = UserDefaults.standard.bool(forKey: "kUserEnableiCloud") { 27 | didSet { 28 | UserDefaults.standard.set(userEnableiCloud, forKey: "kUserEnableiCloud") 29 | useiCloud.accept(userEnableiCloud && icloudAvailable) 30 | } 31 | } 32 | 33 | func setup() { 34 | addNotification() 35 | useiCloud.distinctUntilChanged().filter { $0 }.subscribe { 36 | [weak self] _ in 37 | self?.checkiCloud() 38 | }.disposed(by: disposeBag) 39 | 40 | icloudAvailable = isICloudAvailable() 41 | useiCloud.accept(userEnableiCloud && icloudAvailable) 42 | } 43 | 44 | func getConfigFilesList(configs: @escaping (([String]) -> Void)) { 45 | getUrl { url in 46 | guard let url = url, 47 | let fileURLs = try? FileManager.default.contentsOfDirectory(atPath: url.path) else { 48 | configs([]) 49 | return 50 | } 51 | let list = fileURLs 52 | .filter { String($0.split(separator: ".").last ?? "") == "yaml" } 53 | .map { $0.split(separator: ".").dropLast().joined(separator: ".") } 54 | configs(list) 55 | } 56 | } 57 | 58 | private func checkiCloud() { 59 | getUrl { url in 60 | guard let url = url else { 61 | self.icloudAvailable = false 62 | return 63 | } 64 | let files = try? FileManager.default.contentsOfDirectory(atPath: url.path) 65 | if files?.isEmpty == true { 66 | let path = Bundle.main.path(forResource: "sampleConfig", ofType: "yaml")! 67 | try? FileManager.default.copyItem(atPath: path, toPath: kDefaultConfigFilePath) 68 | try? FileManager.default.copyItem(atPath: Bundle.main.path(forResource: "sampleConfig", ofType: "yaml")!, toPath: url.appendingPathComponent("config.yaml").path) 69 | } 70 | } 71 | } 72 | 73 | private func isICloudAvailable() -> Bool { 74 | return FileManager.default.ubiquityIdentityToken != nil 75 | } 76 | 77 | func getUrl(complete: ((URL?) -> Void)? = nil) { 78 | queue.async { 79 | guard var url = FileManager.default.url(forUbiquityContainerIdentifier: nil) else { 80 | DispatchQueue.main.async { 81 | complete?(nil) 82 | } 83 | return 84 | } 85 | url.appendPathComponent("Documents") 86 | do { 87 | if !FileManager.default.fileExists(atPath: url.path) { 88 | try FileManager.default.createDirectory(at: url, withIntermediateDirectories: false, attributes: nil) 89 | } 90 | DispatchQueue.main.async { 91 | complete?(url) 92 | } 93 | } catch let err { 94 | Logger.log("\(err)") 95 | DispatchQueue.main.async { 96 | complete?(nil) 97 | } 98 | return 99 | } 100 | } 101 | } 102 | 103 | private func addNotification() { 104 | NotificationCenter.default.addObserver(self, selector: #selector(iCloudAccountAvailabilityChanged), name: NSNotification.Name.NSUbiquityIdentityDidChange, object: nil) 105 | } 106 | 107 | @objc func iCloudAccountAvailabilityChanged() { 108 | icloudAvailable = isICloudAvailable() 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /ClashX/ViewControllers/Connections/ViewModels/ConnectionTopListViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectionTopListViewModel.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2023/7/8. 6 | // Copyright © 2023 west2online. All rights reserved. 7 | // 8 | 9 | import Combine 10 | 11 | @available(macOS 10.15, *) 12 | class ConnectionTopListViewModel { 13 | private var fullConnections = [ClashConnectionSnapShot.Connection]() 14 | private(set) var connections = [ClashConnectionSnapShot.Connection]() 15 | private var selectedUUIDs = [String]() 16 | private var updateDebounceDate = Date() 17 | 18 | var onReloadTable: (() -> Void)? 19 | var onSelectedConnection: ((ClashConnectionSnapShot.Connection?) -> Void)? 20 | var applicationFilter: ConnectionFilter? { 21 | didSet { 22 | updateData() 23 | } 24 | } 25 | 26 | var textFilter: String? { 27 | didSet { 28 | updateData() 29 | } 30 | } 31 | 32 | var currentSortDescriptor: NSSortDescriptor? { 33 | didSet { 34 | updateData() 35 | } 36 | } 37 | 38 | func accept(connections new: [ClashConnectionSnapShot.Connection]) { 39 | fullConnections = new 40 | updateData() 41 | } 42 | 43 | func connectionDidUpdate() { 44 | if let key = currentSortDescriptor?.key, ConnectionColume.isDynamicSort(for: key) { 45 | updateData(applyDebounce: true) 46 | } 47 | } 48 | 49 | func currentSelection() -> IndexSet { 50 | let indexs = selectedUUIDs.compactMap { uuid in connections.firstIndex(where: { $0.id == uuid }) } 51 | return IndexSet(indexs) 52 | } 53 | 54 | func closeConnection(for indexs: IndexSet) { 55 | for idx in indexs { 56 | let conn = connections[idx] 57 | ApiRequest.closeConnection(conn.id) 58 | } 59 | } 60 | 61 | private func updateData(applyDebounce: Bool = false) { 62 | let current = Date() 63 | if applyDebounce, current.timeIntervalSince(updateDebounceDate) < 0.2 { 64 | return 65 | } 66 | updateDebounceDate = current 67 | connections = fullConnections 68 | 69 | switch applicationFilter { 70 | case .none: 71 | break 72 | case let .application(pathOrPid): 73 | connections = connections.filter { conn in 74 | conn.metadata.processPath == pathOrPid || conn.metadata.pid == pathOrPid 75 | } 76 | 77 | case let .source(ip): 78 | connections = connections.filter { conn in 79 | conn.metadata.sourceIP == ip 80 | } 81 | case let .hosts(name): 82 | connections = connections.filter { conn in 83 | conn.metadata.displayHost == name 84 | } 85 | } 86 | 87 | if let textFilter = textFilter?.lowercased(), !textFilter.isEmpty { 88 | connections = connections.filter { conn in 89 | conn.metadata.displayHost.contains(textFilter) || 90 | conn.metadata.network.contains(textFilter) || 91 | conn.chains.joined().lowercased().contains(textFilter) || 92 | conn.metadata.processName?.lowercased().contains(textFilter) ?? false || 93 | conn.rule.lowercased().contains(textFilter) 94 | } 95 | } 96 | 97 | connections = (connections as NSArray).sortedArray(using: [currentSortDescriptor].compactMap { $0 }) as! [ClashConnectionSnapShot.Connection] 98 | onReloadTable?() 99 | } 100 | 101 | func setSelect(row: IndexSet) { 102 | selectedUUIDs = row.map { connections[$0].id } 103 | if selectedUUIDs.count == 1, let idx = row.first { 104 | onSelectedConnection?(connections[idx]) 105 | } else { 106 | onSelectedConnection?(nil) 107 | } 108 | } 109 | 110 | func sortSortDescriptor(for columnType: ConnectionColume) -> NSSortDescriptor? { 111 | if let keypath = columnType.compareKeyPath { 112 | let sort = NSSortDescriptor(key: keypath, ascending: true) 113 | if columnType == .date { 114 | currentSortDescriptor = sort 115 | } 116 | return sort 117 | } 118 | return nil 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /.github/certs/dist.p12.enc: -------------------------------------------------------------------------------- 1 | U2FsdGVkX18W6VXBAkhU0bUPLkGdQImB8hBlMUKJ4U4hxo8HKiM8aIJzQWmIyrST 2 | 2H1wHucLy+f5F7T/+0oXnbTvfbCoM5l6dieBPrIxXSQuA1cp5/ImEvcLLy3h7B/i 3 | 6Xbq/F627+yByDxDbyPws19O6TRcvskoR/FXrUIs9zMtdgk9Q3FojoZqu0etQKuu 4 | WGusxTfqSikxWdoqdhX2PaaATpry4TiiH7QRjlTbe0rE7sXvepZmAn6RzUXQIWRz 5 | uwZqddTpjDcPTnVufca++na6fwbMT+meIJVJvY+44H7N4A593ZZ2hUTtbHM6ltii 6 | AF4dG/oKyb9XjfXH+Dke4nKdHENRGmdUw9n/hxWzkB/5ECjNYKk+9Er30DU7Rg9H 7 | jwE4XfaNlWdf/QifDLO7rkZ5LtVyaSgxUHJiCkf7+oCc/feELlCI8FbfRA7LHHjO 8 | tO1sjmKxPlVwP8KsxgPexk+fh+r83HoXPntmBe72ZMg0kvhlgkiK6V6cFHmtL1qR 9 | k6RrZlfxF387VB6g6jYv7qkf7tMeHdFxV3OcI7UJQRIB6PnLFE48IdubVYbv3Yq2 10 | GVkzGjV8aOoPr4uarGMyrIM5MSSC04Gh5yGeWD1r0WSM34d7MdC2vdRS5YyG+hw1 11 | Yd52gF1dLiHVewD047tG6UFxP1f8NHx73p6vFdXBWay0K9J4zxBp2KDqIvKmq6Je 12 | yQFd3wpETWsvpHPofvyzvNokkMVv/QeFxCIJJohXo/syyOFd3uvnXOKoHLwVJtSt 13 | uoCyA1nO+U8qAHdnQ3bmIk1rFbxBUZv43ssqGxWuxI8gDrrFmt7Af9oli53jkr67 14 | Q45DHSRC+sEqRTPxq5yk4TJ1Y6ufZWh6FlINiOzyLV5mTXy1fe0jLp6p93ZPLZTn 15 | WdlKyAyKwMqoQyaJB9xDGVKoO3CDC9BI3Qwjm+vRMI94AFblKgjlDOtnq/T3DjaO 16 | TubyNCpsXoM9leC/eQHp9wVigNY9XoBVCMyU8Vt7WbFzqjckNjYIVd14eq7aIJSn 17 | z/c1ppaMJaMqNhRCKzjyKXQjurVeiNo/xkSR4rbS41nuyPGW+sXcJslGurNOiWYP 18 | Kmr6H6h5cMuAv4F18XrStvgouw+H2cPOgLvuomgt53izOfAgFs2YZEqPANfO0RxX 19 | yBg6NH+tq9dozHMXPbL4CKwcXATshs9ASb8Y4s00IjdTs9aNRth8sonY7qwH7SpU 20 | hImbHZI6kJJD/RoEwm1xMOfa6IjW9nyVcKu8YGhx1QPhKJA+52lbp28F6OhFQOmu 21 | +3CnncSZIpv/KIREIYzB8fCMIMS6QZr5KONEfpZUqSA9Y9ltwveU7Vt4Jnhnx0W5 22 | YVQ+rk0MaARKxbNDNiXnNA+YuDu+LdkdO8KUyzapo8nL1lpcwrqOOFdlWbDv+/rM 23 | yYyUY0rACuXVW3dZBEKsnZi6o5+Pq1XMWYbHw7yBSMWmpceQTQZpBWoqXB1QuUz8 24 | PoE1iWDdaFZcFquqUmSuMZQ9+4t/s8H80D+jM1XG+tc1GuKXT4jrYbuU13L19z6h 25 | 3IujRBy76yr1KHcm8FVX1hr2k0PKA6sUiDi7g0tpM0WfuQh0xkKIFTMq9ms1b+yg 26 | 8ieTDd/8y1eQ+KHWcHU2IJ8GFVyVRB8UtM+nPg3VGpDVV4uT7/ly2qMMOj39nHNi 27 | Ss/rNDDSqL6lomf4riLf/DI2fH5aPxCVFo47wa8qUmCOiOteuKJ+lD3PyoX7A/Yi 28 | mhPFXIsHqtlw/P7huzspJsCNFWzf4vOT1kEggVoHlbVtGY9M46/fMbRRVNpMA2cJ 29 | 4/+z+mlfkQh2DbnMnkGVzfJ1Ho6kBpZX9GIC2NUIZ62pLBZng9ITB7WtyD0ATpC8 30 | 70hoSe91J1yXkUcmTjrf3aOIBYPFP7HnL/fhiaXOtYGR8zm3UvsfjhfDTWsND4Xg 31 | snl6cSPMbeDTwNmoj7SkLoJhNEwGEAS1S/Dl79XwF/pWPTyBh+9wVNN8Gb7Og9h0 32 | 8L+hJoF4d1YzFrLJ0f+5YFf7TRhjUMceMsAg7OHnGLhfx+RvGG1JquWzcw1hNdIJ 33 | yk1xuPd+g6/h/ECUQ1HwwMHnvhXHdRAkEqsP7ntFIdDXDzacxMg54kLLZa5Z6hTw 34 | nxXA1c0D4tgD8OQpZavQ+q3GoZtfn8+wM7PrDjRKAqsJB4OdKWwucLBh+GIdCAc7 35 | gePlcpWfdP82KwLSzu3AjHzbtFzrNQcE1/KwZ2GKCG5IqHeaQRpvMlOzrc5cJQNi 36 | UrfuVkQLXuiCjm2ZhkiGz89GzSYlxiJ0lEBYqnWiUXSeA6LNldU/Ezua40rEit2E 37 | gn5d0Z9hIcxI0nqFZU+FMJwj/WM4JT3Tj0wnx/rPxYkLIfx77mfmYdALZyYdsLbc 38 | XRmwa2BjyNY9VHXHZ9HCb3ebT/Xk2f395EwC9E4QvMkcsvSPe3UUj0fwQ1SWbX1B 39 | +ySa244MoOuE/Q8ugnWEX1RHZzLPnA1p5MU1eqysnSgUGsYSIMuWZjyEo453z9Sd 40 | O1yGKnrxXbYAOsrIn60CmCStr/Zs0KdqyQqNb2nrdifp4RPk+bPqnrIOiNfemUv5 41 | VQtFYpUnHhr1sUpFzRsrUAn2GY50w1XrvaXnz9KFzoS3yesYRoPdlq8KTJ5o0sIz 42 | cbHvld4XwbSw6RXr4E3cH10xgSl3LboB2PO4dxfSjsXrNh78s01cYD0lTj7OCLea 43 | /ekaILS5GTuvqUzbiWqsS7T/G0HZ02ZJSoCjpYUr1goy2F1jrjqsDnBTSOuTA6xE 44 | W4WWjz9+VZzLY9GcH43lwbkNRMfbOdyz35vOxTRSHZt63xZyz37F4qB83E+9d5XN 45 | v4s+l2m+/+wiGchD90nF0EZ+elglzIpP7ZBbAjSsH545Zuk3XwMrhKJzRlcMyGsn 46 | tdejb/v6Xv0lflFD1jGSFhkE9ZxkAg7h90M0cS4/jAw2nOKMXWJIiCXJoETC+Cep 47 | o4Q5CYAkLGpPBuy0H1tIFbJ0auQ6cEJUy3YulphleALoz4Awz8fF8LRN77If9KST 48 | CpQJc1lGtDX32sW1bYcn3O9EhjxJsjBnvhyS0J39euI3v3yBCifoQQGSsCCEr/br 49 | XJnEZnvw/34bmW+o7AWkQhZN8z32a4QOwX2e0fYrGWXeGHIrmevrWUngzpKhyB5N 50 | jhymZSY1kDhCBB3v/8o7vkEfW8LQ5YSzjJYnHrS45r244263LOxQvj3rV+UFPpzR 51 | YclDATMm6WxhXN5xreYJ4ROzHRjY8JmVicLNP1fQDc5x8GRNHruOzM83iZ/wYL5e 52 | IRpVF8JSOUXPePnL5HoHmcdc/kgeT3VPDzbO/4NSjw8fGFjQFLQf7AyVCLjxIy7J 53 | 7C4UJGqoZJYAw0/2bDOBEq3FA3iaxynOhbY8Nspp80M3vesYVGoIR/5DBwJzdc+0 54 | DExmnH3DUXM+H82Pp9PAtpvqsbmH836sWvlamMV37KdSLNh2hkNwa/hYEN2y/Blc 55 | 41mQDFiD6Qm++w7tmQwxoo5RNg81HkxYADWkiR1l0/iMngh/+C/0gA9cV31BaPih 56 | gRwGyFvNjfvVR/HvZCUjunJBYb6iP3imNguKPO4c/ue2opulQ0p7/wL+RV8AVzKZ 57 | yvrNnzwpN528SlMTADUnNXpEaI83iGIVvqIQ31K9E7re13XT6xTiRTUWd/SkS+5T 58 | QLlCb8JS42LeY4yPZRM9kiIlTx7BBxjs1NeCjIqQaQfM+QSZIdMHF49Ra2jRkE3/ 59 | 4DFhcF69lKsx7R2GfAPQx70kNbnsiKi/QYPJ0bknJ8VHMhbFhmp2i3BwCjS3e0XS 60 | DaB6ehkWBOB8AbjaZ2DWcJZdcL3dKeILngdTsznbMfkVjkmLUiG5oWoRJ38/lGdF 61 | EKWnqV/lQ6pQbaOotQ3viCsinZJaHPdwj+semiyVhk7Yu7XTySskCIcFoXTgeirF 62 | /7wfjc+69yfh5+zigDdy5nIYXRt3qDfK6IspGxBENb3Vb/TeNo4iG2V6Gsv0lLo1 63 | Px/ir1S1trwbwExHlYe5408IiNPnsNDidpjtvX7vEwHeUxXsVw23+A8WdbHoY6YM 64 | rUFAbfy1RzXLG4rJYEs/Fm9fFj56DkW+QJZn3SmibF3fvOLxdClRNauGmQRQvUTy 65 | t1lde7u7vTqiy8VKEmEKSI7C7VYKprzuzJq7rDYw3foMQqhXP6qdyfv4Ebz3EzNq 66 | QfipwApRLG6mx8SXhANhuuQBwi3QRdQ0YBSzRNle6RvtdoGGHdQ8QnwOuJLB4dWV 67 | 28lBA63XMGeNQ9UbNGHlCH7Lv6w1k0A0rGKu2LwokMXDePMvtlSer1SuG5XEjjQg 68 | 59V4FWidJOuaUW9iGF326QfhUZPjMKwf85nMlAnt/IrkFER4ghsS/8Fb1iCSify7 69 | v+MzmhNYJFZLLVM5VxsZjaZNU3+SW2pAJYIKP1z9T30+22tpBmtg9JNqVk8mc5AO 70 | rmROCBhbU9b/V+uonKyg8HkPh7cXLfTKE8aNutliDkPbaD/TWIhaHgC96wu/eWUo 71 | 8MKTfqu9ZeN69pUEfiRM5/8SzV6iVRWWq8LxzZc9/fkRR4PlJHfoRCP0DO1IeSW6 72 | 7ZyaCUt2XMQU4x+dVz2zdQ== 73 | -------------------------------------------------------------------------------- /ClashX/ViewControllers/Connections/DashboardViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DashboardViewController.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2023/7/14. 6 | // Copyright © 2023 west2online. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | enum DashboardContentType: Int, CaseIterable { 12 | case allConnection 13 | case activeConnection 14 | 15 | var title: String { 16 | switch self { 17 | case .allConnection: 18 | return NSLocalizedString("Recent Connections", comment: "") 19 | case .activeConnection: 20 | return NSLocalizedString("Active Connections", comment: "") 21 | } 22 | } 23 | } 24 | 25 | @available(macOS 10.15, *) 26 | class DashboardViewController: NSViewController { 27 | private let toolbar = NSToolbar() 28 | private var segmentControl: NSSegmentedControl! 29 | private let searchField = NSSearchField() 30 | 31 | private let connectionVC = ConnectionsViewController() 32 | 33 | private var currentContentVC: DashboardSubViewControllerProtocol? 34 | 35 | override func viewDidLoad() { 36 | super.viewDidLoad() 37 | segmentControl = NSSegmentedControl(labels: DashboardContentType.allCases.map(\.title), 38 | trackingMode: .selectOne, 39 | target: self, 40 | action: #selector(actionSwitchSegmentControl(sender:))) 41 | segmentControl.selectedSegment = 0 42 | searchField.delegate = self 43 | setCurrentVC(connectionVC) 44 | } 45 | 46 | override func loadView() { 47 | view = NSView() 48 | } 49 | 50 | override func viewWillAppear() { 51 | super.viewWillAppear() 52 | toolbar.delegate = self 53 | view.window?.toolbar = toolbar 54 | view.window?.backgroundColor = NSColor.clear 55 | if #available(macOS 11.0, *) { 56 | view.window?.toolbarStyle = .unifiedCompact 57 | } else { 58 | view.window?.toolbar?.sizeMode = .small 59 | } 60 | } 61 | 62 | func setCurrentVC(_ vc: DashboardSubViewControllerProtocol) { 63 | currentContentVC?.removeFromParent() 64 | currentContentVC?.view.removeFromSuperview() 65 | addChild(vc) 66 | view.addSubview(vc.view) 67 | vc.view.makeConstraintsToBindToSuperview() 68 | currentContentVC = vc 69 | } 70 | 71 | @objc func actionSwitchSegmentControl(sender: NSSegmentedControl) { 72 | guard let contentType = DashboardContentType(rawValue: sender.selectedSegment) else { return } 73 | switch contentType { 74 | case .allConnection: 75 | connectionVC.setActiveMode(enable: false) 76 | case .activeConnection: 77 | connectionVC.setActiveMode(enable: true) 78 | } 79 | } 80 | } 81 | 82 | @available(macOS 10.15, *) 83 | extension DashboardViewController: NSSearchFieldDelegate { 84 | func controlTextDidChange(_ obj: Notification) { 85 | if let textField = obj.object as? NSTextField { 86 | currentContentVC?.actionSearch(string: textField.stringValue) 87 | } 88 | } 89 | 90 | func searchFieldDidEndSearching(_ sender: NSSearchField) { 91 | currentContentVC?.actionSearch(string: sender.stringValue) 92 | } 93 | } 94 | 95 | extension NSToolbarItem.Identifier { 96 | static let toolbarSearchItem = NSToolbarItem.Identifier("ToolbarSearchItem") 97 | static let toolbarSegmentItem = NSToolbarItem.Identifier("toolbarSegmentItem") 98 | } 99 | 100 | @available(macOS 10.15, *) 101 | extension DashboardViewController: NSToolbarDelegate { 102 | func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { 103 | return [.toolbarSegmentItem, .flexibleSpace, .toolbarSearchItem] 104 | } 105 | 106 | func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { 107 | return [.toolbarSegmentItem, .flexibleSpace, .toolbarSearchItem] 108 | } 109 | 110 | func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { 111 | let item = NSToolbarItem(itemIdentifier: itemIdentifier) 112 | if itemIdentifier == .toolbarSearchItem { 113 | item.maxSize = NSSize(width: 200, height: 40) 114 | searchField.sizeToFit() 115 | item.view = searchField 116 | } else if itemIdentifier == .toolbarSegmentItem { 117 | if #available(macOS 11.0, *) { 118 | item.isNavigational = true 119 | } 120 | item.minSize = CGSize(width: 300, height: 34) 121 | item.view = segmentControl 122 | } 123 | 124 | return item 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /ClashX/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSLocationAlwaysAndWhenInUseUsageDescription 6 | ClashX use location info to detect your current WiFi network SSID name and provide the auto suspend services. 7 | NSLocationWhenInUseUsageDescription 8 | ClashX use location info to detect your current WiFi network SSID name and provide the auto suspend services. 9 | BETA 10 | 11 | CFBundleDevelopmentRegion 12 | $(DEVELOPMENT_LANGUAGE) 13 | CFBundleDocumentTypes 14 | 15 | 16 | CFBundleTypeExtensions 17 | 18 | yaml 19 | 20 | CFBundleTypeName 21 | YAML 22 | CFBundleTypeRole 23 | Viewer 24 | LSHandlerRank 25 | None 26 | 27 | 28 | CFBundleExecutable 29 | $(EXECUTABLE_NAME) 30 | CFBundleIdentifier 31 | $(PRODUCT_BUNDLE_IDENTIFIER) 32 | CFBundleInfoDictionaryVersion 33 | 6.0 34 | CFBundleName 35 | $(PRODUCT_NAME) 36 | CFBundlePackageType 37 | APPL 38 | CFBundleShortVersionString 39 | $(MARKETING_VERSION) 40 | CFBundleURLTypes 41 | 42 | 43 | CFBundleTypeRole 44 | Editor 45 | CFBundleURLIconFile 46 | Icon 47 | CFBundleURLName 48 | com.west2online.ClashX 49 | CFBundleURLSchemes 50 | 51 | clashx 52 | 53 | 54 | 55 | CFBundleTypeRole 56 | Editor 57 | CFBundleURLIconFile 58 | Icon 59 | CFBundleURLName 60 | com.west2online.Clash 61 | CFBundleURLSchemes 62 | 63 | clash 64 | 65 | 66 | 67 | CFBundleVersion 68 | $(CURRENT_PROJECT_VERSION) 69 | Fabric 70 | 71 | APIKey 72 | e5990c61f53f0e5713e37f78458291760d631147 73 | Kits 74 | 75 | 76 | KitInfo 77 | 78 | KitName 79 | Crashlytics 80 | 81 | 82 | 83 | LSApplicationCategoryType 84 | public.app-category.utilities 85 | LSMinimumSystemVersion 86 | $(MACOSX_DEPLOYMENT_TARGET) 87 | LSUIElement 88 | 89 | NSAppTransportSecurity 90 | 91 | NSAllowsArbitraryLoads 92 | 93 | 94 | NSAppleScriptEnabled 95 | 96 | NSHumanReadableCopyright 97 | Copyright © 2021年 yichengchen. All rights reserved. 98 | NSMainStoryboardFile 99 | Main 100 | NSPrincipalClass 101 | NSApplication 102 | NSSupportsAutomaticTermination 103 | 104 | NSSupportsSuddenTermination 105 | 106 | NSUbiquitousContainers 107 | 108 | iCloud.com.west2online.ClashX 109 | 110 | NSUbiquitousContainerIsDocumentScopePublic 111 | 112 | NSUbiquitousContainerName 113 | ClashX 114 | NSUbiquitousContainerSupportedFolderLevels 115 | Any 116 | 117 | 118 | OSAScriptingDefinition 119 | ProxySetting.sdef 120 | SMAuthorizedClients 121 | 122 | identifier "com.west2online.ClashX" and anchor apple generic and certificate leaf[subject.CN] = "Mac Developer: chen yicheng (96U846XGYH)" and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */ 123 | 124 | SMPrivilegedExecutables 125 | 126 | com.west2online.ClashX.ProxyConfigHelper 127 | anchor apple generic and identifier "com.west2online.ClashX.ProxyConfigHelper" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = MEWHFZ92DY) 128 | 129 | SUDisallowSelectChannel 130 | 131 | SUFeedURL 132 | https://yichengchen.github.io/clashX/appcast.xml 133 | 134 | 135 | -------------------------------------------------------------------------------- /ClashX/General/Managers/ClashResourceManager.swift: -------------------------------------------------------------------------------- 1 | import Alamofire 2 | import AppKit 3 | import Foundation 4 | import Gzip 5 | 6 | class ClashResourceManager { 7 | static func check() -> Bool { 8 | checkConfigDir() 9 | checkMMDB() 10 | return true 11 | } 12 | 13 | static func checkConfigDir() { 14 | var isDir: ObjCBool = true 15 | 16 | if !FileManager.default.fileExists(atPath: kConfigFolderPath, isDirectory: &isDir) { 17 | do { 18 | try FileManager.default.createDirectory(atPath: kConfigFolderPath, withIntermediateDirectories: true, attributes: nil) 19 | } catch let err { 20 | Logger.log("\(err.localizedDescription) \(kConfigFolderPath)") 21 | showCreateConfigDirFailAlert(err: err.localizedDescription) 22 | } 23 | } 24 | } 25 | 26 | static func checkMMDB() { 27 | let fileManage = FileManager.default 28 | let destMMDBPath = "\(kConfigFolderPath)/Country.mmdb" 29 | 30 | // Remove old mmdb file after version update. 31 | if fileManage.fileExists(atPath: destMMDBPath) { 32 | let vaild = verifyGEOIPDataBase().toBool() 33 | let versionChange = AppVersionUtil.hasVersionChanged || AppVersionUtil.isFirstLaunch 34 | if !vaild || versionChange { 35 | Logger.log("removing new mmdb file") 36 | try? fileManage.removeItem(atPath: destMMDBPath) 37 | } 38 | } 39 | 40 | if !fileManage.fileExists(atPath: destMMDBPath) { 41 | Logger.log("installing new mmdb file") 42 | if let mmdbUrl = Bundle.main.url(forResource: "Country.mmdb", withExtension: "gz") { 43 | do { 44 | let data = try Data(contentsOf: mmdbUrl).gunzipped() 45 | try data.write(to: URL(fileURLWithPath: destMMDBPath)) 46 | } catch let err { 47 | Logger.log("add mmdb fail:\(err)", level: .error) 48 | } 49 | } 50 | } 51 | } 52 | 53 | static func showCreateConfigDirFailAlert(err: String) { 54 | let alert = NSAlert() 55 | alert.messageText = NSLocalizedString("ClashX fail to create ~/.config/clash folder. Please check privileges or manually create folder and restart ClashX." + err, comment: "") 56 | alert.alertStyle = .warning 57 | alert.addButton(withTitle: NSLocalizedString("Quit", comment: "")) 58 | alert.runModal() 59 | NSApplication.shared.terminate(nil) 60 | } 61 | } 62 | 63 | extension ClashResourceManager { 64 | static func updateGeoIP() { 65 | guard let url = showCustomAlert() else { return } 66 | AF.download(url, to: { _, _ in 67 | let path = kConfigFolderPath.appending("/Country.mmdb") 68 | return (URL(fileURLWithPath: path), .removePreviousFile) 69 | }).response { res in 70 | var info: String 71 | switch res.result { 72 | case .success: 73 | info = NSLocalizedString("Success!", comment: "") 74 | Logger.log("update success") 75 | case let .failure(err): 76 | info = NSLocalizedString("Fail:", comment: "") + err.localizedDescription 77 | Logger.log("update fail \(err)") 78 | } 79 | if !verifyGEOIPDataBase().toBool() { 80 | info = "Database verify fail" 81 | checkMMDB() 82 | } 83 | let alert = NSAlert() 84 | alert.messageText = NSLocalizedString("Update GEOIP Database", comment: "") 85 | alert.informativeText = info 86 | alert.runModal() 87 | } 88 | } 89 | 90 | private static func showCustomAlert() -> String? { 91 | let alert = NSAlert() 92 | alert.messageText = NSLocalizedString("Custom your GEOIP MMDB download address.", comment: "") 93 | let inputView = NSTextField(frame: NSRect(x: 0, y: 0, width: 250, height: 24)) 94 | inputView.placeholderString = Settings.defaultMmdbDownloadUrl 95 | if Settings.mmdbDownloadUrl.isEmpty { 96 | inputView.stringValue = Settings.defaultMmdbDownloadUrl 97 | } else { 98 | inputView.stringValue = Settings.mmdbDownloadUrl 99 | } 100 | alert.accessoryView = inputView 101 | alert.addButton(withTitle: NSLocalizedString("OK", comment: "")) 102 | alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "")) 103 | if alert.runModal() == .alertFirstButtonReturn { 104 | if inputView.stringValue.isEmpty { 105 | return inputView.placeholderString 106 | } 107 | Settings.mmdbDownloadUrl = inputView.stringValue 108 | return inputView.stringValue 109 | } 110 | return nil 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /ClashX/Views/ProxyItemView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProxyItemView.swift 3 | // ClashX 4 | // 5 | // Created by yicheng on 2019/11/2. 6 | // Copyright © 2019 west2online. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ProxyItemView: MenuItemBaseView { 12 | let nameLabel: NSTextField 13 | let delayLabel: NSTextField 14 | var imageView: NSImageView? 15 | 16 | static let fixedPlaceHolderWidth: CGFloat = 20 + 50 + 25 17 | 18 | init(proxy: ClashProxy) { 19 | nameLabel = VibrancyTextField(labelWithString: proxy.name) 20 | delayLabel = VibrancyTextField(labelWithString: "").setup(allowsVibrancy: false) 21 | let cell = PaddedNSTextFieldCell() 22 | cell.widthPadding = 2 23 | if #available(macOS 11, *) { 24 | cell.heightPadding = 2 25 | } else { 26 | cell.heightPadding = 1 27 | } 28 | delayLabel.cell = cell 29 | super.init(autolayout: false) 30 | effectView.addSubview(nameLabel) 31 | effectView.addSubview(delayLabel) 32 | 33 | nameLabel.translatesAutoresizingMaskIntoConstraints = false 34 | delayLabel.translatesAutoresizingMaskIntoConstraints = false 35 | 36 | nameLabel.font = type(of: self).labelFont 37 | if #available(macOS 11, *) { 38 | delayLabel.font = NSFont.monospacedDigitSystemFont(ofSize: 9, weight: .medium) 39 | } else { 40 | delayLabel.font = NSFont.monospacedDigitSystemFont(ofSize: 10, weight: .medium) 41 | } 42 | nameLabel.alignment = .left 43 | delayLabel.alignment = .right 44 | 45 | delayLabel.wantsLayer = true 46 | delayLabel.layer?.cornerRadius = 2 47 | delayLabel.textColor = NSColor.white 48 | 49 | update(str: proxy.history.last?.delayDisplay, value: proxy.history.last?.delay) 50 | } 51 | 52 | override func layout() { 53 | super.layout() 54 | nameLabel.sizeToFit() 55 | delayLabel.sizeToFit() 56 | imageView?.frame = CGRect(x: 5, y: effectView.bounds.height / 2 - 6, width: 12, height: 12) 57 | nameLabel.frame = CGRect(x: 18, 58 | y: (effectView.bounds.height - nameLabel.bounds.height) / 2, 59 | width: nameLabel.bounds.width, 60 | height: nameLabel.bounds.height) 61 | delayLabel.frame = CGRect(x: effectView.bounds.width - delayLabel.bounds.width - 8, 62 | y: (effectView.bounds.height - delayLabel.bounds.height) / 2, 63 | width: delayLabel.bounds.width, 64 | height: delayLabel.bounds.height) 65 | } 66 | 67 | func update(str: String?, value: Int?) { 68 | delayLabel.stringValue = str ?? "" 69 | needsLayout = true 70 | 71 | guard let delay = value, str != nil else { 72 | delayLabel.layer?.backgroundColor = NSColor.clear.cgColor 73 | return 74 | } 75 | switch delay { 76 | case 0: 77 | delayLabel.layer?.backgroundColor = CGColor.fail 78 | case 0 ..< 300: 79 | delayLabel.layer?.backgroundColor = CGColor.good 80 | default: 81 | delayLabel.layer?.backgroundColor = CGColor.meduim 82 | } 83 | } 84 | 85 | func update(selected: Bool) { 86 | if selected { 87 | if imageView == nil { 88 | let image: NSImage 89 | if #available(OSX 11.0, *) { 90 | image = NSImage(named: NSImage.menuOnStateTemplateName)!.withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 13, weight: .bold, scale: .small))! 91 | } else { 92 | image = NSImage(named: NSImage.menuOnStateTemplateName)! 93 | } 94 | imageView = NSImageView(image: image) 95 | imageView?.translatesAutoresizingMaskIntoConstraints = false 96 | effectView.addSubview(imageView!) 97 | } 98 | } else { 99 | imageView?.removeFromSuperview() 100 | imageView = nil 101 | } 102 | } 103 | 104 | @available(*, unavailable) 105 | required init?(coder: NSCoder) { 106 | fatalError("init(coder:) has not been implemented") 107 | } 108 | 109 | override func didClickView() { 110 | (enclosingMenuItem as? ProxyMenuItem)?.didClick() 111 | } 112 | 113 | override var cells: [NSCell?] { 114 | return [nameLabel.cell, imageView?.cell] 115 | } 116 | } 117 | 118 | private extension CGColor { 119 | static let good = CGColor(red: 30.0 / 255, green: 181.0 / 255, blue: 30.0 / 255, alpha: 1) 120 | static let meduim = CGColor(red: 1, green: 135.0 / 255, blue: 0, alpha: 1) 121 | static let fail = CGColor(red: 218.0 / 255, green: 0.0, blue: 3.0 / 255, alpha: 1) 122 | } 123 | -------------------------------------------------------------------------------- /ProxyConfigHelper/ProxyConfigHelper.m: -------------------------------------------------------------------------------- 1 | // 2 | // ProxyConfigHelper.m 3 | // com.west2online.ClashX.ProxyConfigHelper 4 | // 5 | // Created by yichengchen on 2019/8/17. 6 | // Copyright © 2019 west2online. All rights reserved. 7 | // 8 | 9 | #import "ProxyConfigHelper.h" 10 | #import 11 | #import "ProxyConfigRemoteProcessProtocol.h" 12 | #import "ProxySettingTool.h" 13 | 14 | @interface ProxyConfigHelper() 15 | < 16 | NSXPCListenerDelegate, 17 | ProxyConfigRemoteProcessProtocol 18 | > 19 | 20 | @property (nonatomic, strong) NSXPCListener *listener; 21 | @property (nonatomic, strong) NSMutableSet *connections; 22 | @property (nonatomic, strong) NSTimer *checkTimer; 23 | @property (nonatomic, assign) BOOL shouldQuit; 24 | 25 | @end 26 | 27 | @implementation ProxyConfigHelper 28 | - (instancetype)init { 29 | 30 | if (self = [super init]) { 31 | self.connections = [NSMutableSet new]; 32 | self.shouldQuit = NO; 33 | self.listener = [[NSXPCListener alloc] initWithMachServiceName:@"com.west2online.ClashX.ProxyConfigHelper"]; 34 | self.listener.delegate = self; 35 | } 36 | return self; 37 | } 38 | 39 | - (void)run { 40 | [self.listener resume]; 41 | self.checkTimer = 42 | [NSTimer timerWithTimeInterval:5.f target:self selector:@selector(connectionCheckOnLaunch) userInfo:nil repeats:NO]; 43 | [[NSRunLoop currentRunLoop] addTimer:self.checkTimer forMode:NSDefaultRunLoopMode]; 44 | while (!self.shouldQuit) { 45 | [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:2.0]]; 46 | } 47 | } 48 | 49 | - (void)connectionCheckOnLaunch { 50 | if (self.connections.count == 0) { 51 | self.shouldQuit = YES; 52 | } 53 | } 54 | 55 | - (BOOL)connectionIsVaild: (NSXPCConnection *)connection { 56 | NSRunningApplication *remoteApp = 57 | [NSRunningApplication runningApplicationWithProcessIdentifier:connection.processIdentifier]; 58 | return remoteApp != nil; 59 | } 60 | 61 | // MARK: - NSXPCListenerDelegate 62 | 63 | - (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection { 64 | // if (![self connectionIsVaild:newConnection]) { 65 | // return NO; 66 | // } 67 | newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(ProxyConfigRemoteProcessProtocol)]; 68 | newConnection.exportedObject = self; 69 | __weak NSXPCConnection *weakConnection = newConnection; 70 | __weak ProxyConfigHelper *weakSelf = self; 71 | newConnection.invalidationHandler = ^{ 72 | [weakSelf.connections removeObject:weakConnection]; 73 | if (weakSelf.connections.count == 0) { 74 | weakSelf.shouldQuit = YES; 75 | } 76 | }; 77 | [self.connections addObject:newConnection]; 78 | [newConnection resume]; 79 | return YES; 80 | } 81 | 82 | // MARK: - ProxyConfigRemoteProcessProtocol 83 | - (void)getVersion:(stringReplyBlock)reply { 84 | NSString *version = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; 85 | if (version == nil) { 86 | version = @"unknown"; 87 | } 88 | reply(version); 89 | } 90 | 91 | - (void)enableProxyWithPort:(int)port 92 | socksPort:(int)socksPort 93 | pac:(NSString *)pac 94 | filterInterface:(BOOL)filterInterface 95 | ignoreList:(NSArray*)ignoreList 96 | error:(stringReplyBlock)reply { 97 | dispatch_async(dispatch_get_main_queue(), ^{ 98 | ProxySettingTool *tool = [ProxySettingTool new]; 99 | [tool enableProxyWithport:port socksPort:socksPort pacUrl:pac filterInterface:filterInterface ignoreList:ignoreList]; 100 | reply(nil); 101 | }); 102 | } 103 | 104 | - (void)disableProxyWithFilterInterface:(BOOL)filterInterface reply:(stringReplyBlock)reply { 105 | dispatch_async(dispatch_get_main_queue(), ^{ 106 | ProxySettingTool *tool = [ProxySettingTool new]; 107 | [tool disableProxyWithfilterInterface:filterInterface]; 108 | reply(nil); 109 | }); 110 | } 111 | 112 | 113 | - (void)restoreProxyWithCurrentPort:(int)port 114 | socksPort:(int)socksPort 115 | info:(NSDictionary *)dict 116 | filterInterface:(BOOL)filterInterface 117 | error:(stringReplyBlock)reply { 118 | dispatch_async(dispatch_get_main_queue(), ^{ 119 | ProxySettingTool *tool = [ProxySettingTool new]; 120 | [tool restoreProxySetting:dict currentPort:port currentSocksPort:socksPort filterInterface:filterInterface]; 121 | reply(nil); 122 | }); 123 | } 124 | 125 | - (void)getCurrentProxySetting:(dictReplyBlock)reply { 126 | dispatch_async(dispatch_get_main_queue(), ^{ 127 | NSDictionary *info = [ProxySettingTool currentProxySettings]; 128 | reply(info); 129 | }); 130 | } 131 | 132 | 133 | @end 134 | --------------------------------------------------------------------------------