├── 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 |
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 |
--------------------------------------------------------------------------------