├── .gitignore ├── NeTool.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── taliu.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── NeTool ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── Main.storyboard ├── Info.plist ├── NeTool.entitlements ├── NetSpeedMonitor.swift ├── SpeedInfoView.swift ├── SpeedInfoView.xib ├── StatusBarView.swift └── ViewController.swift ├── NeToolTests ├── Info.plist └── NeToolTests.swift ├── NeToolUITests ├── Info.plist └── NeToolUITests.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | NeTool.xcodeproj/project.xcworkspace/xcuserdata/ 3 | NeTool.xcodeproj/xcuserdata/ 4 | -------------------------------------------------------------------------------- /NeTool.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 7A431F392508E1D20050327A /* SpeedInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A431F382508E1D20050327A /* SpeedInfoView.swift */; }; 11 | 7A431F3B2509BDFA0050327A /* SpeedInfoView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7A431F3A2509BDFA0050327A /* SpeedInfoView.xib */; }; 12 | 7A6E0090214CBD690020ED6D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6E008F214CBD690020ED6D /* AppDelegate.swift */; }; 13 | 7A6E0092214CBD690020ED6D /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6E0091214CBD690020ED6D /* ViewController.swift */; }; 14 | 7A6E0094214CBD6A0020ED6D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7A6E0093214CBD6A0020ED6D /* Assets.xcassets */; }; 15 | 7A6E0097214CBD6A0020ED6D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7A6E0095214CBD6A0020ED6D /* Main.storyboard */; }; 16 | 7A6E00A3214CBD6A0020ED6D /* NeToolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6E00A2214CBD6A0020ED6D /* NeToolTests.swift */; }; 17 | 7A6E00AE214CBD6A0020ED6D /* NeToolUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6E00AD214CBD6A0020ED6D /* NeToolUITests.swift */; }; 18 | 7A6E00BC214CBD9B0020ED6D /* StatusBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6E00BB214CBD9B0020ED6D /* StatusBarView.swift */; }; 19 | 7A6E00BE214CDDA30020ED6D /* NetSpeedMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6E00BD214CDDA30020ED6D /* NetSpeedMonitor.swift */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXContainerItemProxy section */ 23 | 7A6E009F214CBD6A0020ED6D /* PBXContainerItemProxy */ = { 24 | isa = PBXContainerItemProxy; 25 | containerPortal = 7A6E0084214CBD690020ED6D /* Project object */; 26 | proxyType = 1; 27 | remoteGlobalIDString = 7A6E008B214CBD690020ED6D; 28 | remoteInfo = NeTool; 29 | }; 30 | 7A6E00AA214CBD6A0020ED6D /* PBXContainerItemProxy */ = { 31 | isa = PBXContainerItemProxy; 32 | containerPortal = 7A6E0084214CBD690020ED6D /* Project object */; 33 | proxyType = 1; 34 | remoteGlobalIDString = 7A6E008B214CBD690020ED6D; 35 | remoteInfo = NeTool; 36 | }; 37 | /* End PBXContainerItemProxy section */ 38 | 39 | /* Begin PBXFileReference section */ 40 | 7A431F382508E1D20050327A /* SpeedInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeedInfoView.swift; sourceTree = ""; }; 41 | 7A431F3A2509BDFA0050327A /* SpeedInfoView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SpeedInfoView.xib; sourceTree = ""; }; 42 | 7A6E008C214CBD690020ED6D /* NeTool.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NeTool.app; sourceTree = BUILT_PRODUCTS_DIR; }; 43 | 7A6E008F214CBD690020ED6D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 44 | 7A6E0091214CBD690020ED6D /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 45 | 7A6E0093214CBD6A0020ED6D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 46 | 7A6E0096214CBD6A0020ED6D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 47 | 7A6E0098214CBD6A0020ED6D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 48 | 7A6E0099214CBD6A0020ED6D /* NeTool.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NeTool.entitlements; sourceTree = ""; }; 49 | 7A6E009E214CBD6A0020ED6D /* NeToolTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NeToolTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 50 | 7A6E00A2214CBD6A0020ED6D /* NeToolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NeToolTests.swift; sourceTree = ""; }; 51 | 7A6E00A4214CBD6A0020ED6D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 52 | 7A6E00A9214CBD6A0020ED6D /* NeToolUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NeToolUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 53 | 7A6E00AD214CBD6A0020ED6D /* NeToolUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NeToolUITests.swift; sourceTree = ""; }; 54 | 7A6E00AF214CBD6A0020ED6D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 55 | 7A6E00BB214CBD9B0020ED6D /* StatusBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusBarView.swift; sourceTree = ""; }; 56 | 7A6E00BD214CDDA30020ED6D /* NetSpeedMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetSpeedMonitor.swift; sourceTree = ""; }; 57 | /* End PBXFileReference section */ 58 | 59 | /* Begin PBXFrameworksBuildPhase section */ 60 | 7A6E0089214CBD690020ED6D /* Frameworks */ = { 61 | isa = PBXFrameworksBuildPhase; 62 | buildActionMask = 2147483647; 63 | files = ( 64 | ); 65 | runOnlyForDeploymentPostprocessing = 0; 66 | }; 67 | 7A6E009B214CBD6A0020ED6D /* Frameworks */ = { 68 | isa = PBXFrameworksBuildPhase; 69 | buildActionMask = 2147483647; 70 | files = ( 71 | ); 72 | runOnlyForDeploymentPostprocessing = 0; 73 | }; 74 | 7A6E00A6214CBD6A0020ED6D /* Frameworks */ = { 75 | isa = PBXFrameworksBuildPhase; 76 | buildActionMask = 2147483647; 77 | files = ( 78 | ); 79 | runOnlyForDeploymentPostprocessing = 0; 80 | }; 81 | /* End PBXFrameworksBuildPhase section */ 82 | 83 | /* Begin PBXGroup section */ 84 | 7A6E0083214CBD690020ED6D = { 85 | isa = PBXGroup; 86 | children = ( 87 | 7A6E008E214CBD690020ED6D /* NeTool */, 88 | 7A6E00A1214CBD6A0020ED6D /* NeToolTests */, 89 | 7A6E00AC214CBD6A0020ED6D /* NeToolUITests */, 90 | 7A6E008D214CBD690020ED6D /* Products */, 91 | ); 92 | sourceTree = ""; 93 | }; 94 | 7A6E008D214CBD690020ED6D /* Products */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | 7A6E008C214CBD690020ED6D /* NeTool.app */, 98 | 7A6E009E214CBD6A0020ED6D /* NeToolTests.xctest */, 99 | 7A6E00A9214CBD6A0020ED6D /* NeToolUITests.xctest */, 100 | ); 101 | name = Products; 102 | sourceTree = ""; 103 | }; 104 | 7A6E008E214CBD690020ED6D /* NeTool */ = { 105 | isa = PBXGroup; 106 | children = ( 107 | 7A6E008F214CBD690020ED6D /* AppDelegate.swift */, 108 | 7A6E0091214CBD690020ED6D /* ViewController.swift */, 109 | 7A6E0093214CBD6A0020ED6D /* Assets.xcassets */, 110 | 7A6E0095214CBD6A0020ED6D /* Main.storyboard */, 111 | 7A6E0098214CBD6A0020ED6D /* Info.plist */, 112 | 7A6E0099214CBD6A0020ED6D /* NeTool.entitlements */, 113 | 7A6E00BB214CBD9B0020ED6D /* StatusBarView.swift */, 114 | 7A6E00BD214CDDA30020ED6D /* NetSpeedMonitor.swift */, 115 | 7A431F382508E1D20050327A /* SpeedInfoView.swift */, 116 | 7A431F3A2509BDFA0050327A /* SpeedInfoView.xib */, 117 | ); 118 | path = NeTool; 119 | sourceTree = ""; 120 | }; 121 | 7A6E00A1214CBD6A0020ED6D /* NeToolTests */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | 7A6E00A2214CBD6A0020ED6D /* NeToolTests.swift */, 125 | 7A6E00A4214CBD6A0020ED6D /* Info.plist */, 126 | ); 127 | path = NeToolTests; 128 | sourceTree = ""; 129 | }; 130 | 7A6E00AC214CBD6A0020ED6D /* NeToolUITests */ = { 131 | isa = PBXGroup; 132 | children = ( 133 | 7A6E00AD214CBD6A0020ED6D /* NeToolUITests.swift */, 134 | 7A6E00AF214CBD6A0020ED6D /* Info.plist */, 135 | ); 136 | path = NeToolUITests; 137 | sourceTree = ""; 138 | }; 139 | /* End PBXGroup section */ 140 | 141 | /* Begin PBXNativeTarget section */ 142 | 7A6E008B214CBD690020ED6D /* NeTool */ = { 143 | isa = PBXNativeTarget; 144 | buildConfigurationList = 7A6E00B2214CBD6A0020ED6D /* Build configuration list for PBXNativeTarget "NeTool" */; 145 | buildPhases = ( 146 | 7A6E0088214CBD690020ED6D /* Sources */, 147 | 7A6E0089214CBD690020ED6D /* Frameworks */, 148 | 7A6E008A214CBD690020ED6D /* Resources */, 149 | ); 150 | buildRules = ( 151 | ); 152 | dependencies = ( 153 | ); 154 | name = NeTool; 155 | productName = NeTool; 156 | productReference = 7A6E008C214CBD690020ED6D /* NeTool.app */; 157 | productType = "com.apple.product-type.application"; 158 | }; 159 | 7A6E009D214CBD6A0020ED6D /* NeToolTests */ = { 160 | isa = PBXNativeTarget; 161 | buildConfigurationList = 7A6E00B5214CBD6A0020ED6D /* Build configuration list for PBXNativeTarget "NeToolTests" */; 162 | buildPhases = ( 163 | 7A6E009A214CBD6A0020ED6D /* Sources */, 164 | 7A6E009B214CBD6A0020ED6D /* Frameworks */, 165 | 7A6E009C214CBD6A0020ED6D /* Resources */, 166 | ); 167 | buildRules = ( 168 | ); 169 | dependencies = ( 170 | 7A6E00A0214CBD6A0020ED6D /* PBXTargetDependency */, 171 | ); 172 | name = NeToolTests; 173 | productName = NeToolTests; 174 | productReference = 7A6E009E214CBD6A0020ED6D /* NeToolTests.xctest */; 175 | productType = "com.apple.product-type.bundle.unit-test"; 176 | }; 177 | 7A6E00A8214CBD6A0020ED6D /* NeToolUITests */ = { 178 | isa = PBXNativeTarget; 179 | buildConfigurationList = 7A6E00B8214CBD6A0020ED6D /* Build configuration list for PBXNativeTarget "NeToolUITests" */; 180 | buildPhases = ( 181 | 7A6E00A5214CBD6A0020ED6D /* Sources */, 182 | 7A6E00A6214CBD6A0020ED6D /* Frameworks */, 183 | 7A6E00A7214CBD6A0020ED6D /* Resources */, 184 | ); 185 | buildRules = ( 186 | ); 187 | dependencies = ( 188 | 7A6E00AB214CBD6A0020ED6D /* PBXTargetDependency */, 189 | ); 190 | name = NeToolUITests; 191 | productName = NeToolUITests; 192 | productReference = 7A6E00A9214CBD6A0020ED6D /* NeToolUITests.xctest */; 193 | productType = "com.apple.product-type.bundle.ui-testing"; 194 | }; 195 | /* End PBXNativeTarget section */ 196 | 197 | /* Begin PBXProject section */ 198 | 7A6E0084214CBD690020ED6D /* Project object */ = { 199 | isa = PBXProject; 200 | attributes = { 201 | LastSwiftUpdateCheck = 0940; 202 | LastUpgradeCheck = 0940; 203 | ORGANIZATIONNAME = "Liu, Tao (Toni)"; 204 | TargetAttributes = { 205 | 7A6E008B214CBD690020ED6D = { 206 | CreatedOnToolsVersion = 9.4.1; 207 | SystemCapabilities = { 208 | com.apple.Sandbox = { 209 | enabled = 0; 210 | }; 211 | }; 212 | }; 213 | 7A6E009D214CBD6A0020ED6D = { 214 | CreatedOnToolsVersion = 9.4.1; 215 | TestTargetID = 7A6E008B214CBD690020ED6D; 216 | }; 217 | 7A6E00A8214CBD6A0020ED6D = { 218 | CreatedOnToolsVersion = 9.4.1; 219 | TestTargetID = 7A6E008B214CBD690020ED6D; 220 | }; 221 | }; 222 | }; 223 | buildConfigurationList = 7A6E0087214CBD690020ED6D /* Build configuration list for PBXProject "NeTool" */; 224 | compatibilityVersion = "Xcode 9.3"; 225 | developmentRegion = en; 226 | hasScannedForEncodings = 0; 227 | knownRegions = ( 228 | en, 229 | Base, 230 | ); 231 | mainGroup = 7A6E0083214CBD690020ED6D; 232 | productRefGroup = 7A6E008D214CBD690020ED6D /* Products */; 233 | projectDirPath = ""; 234 | projectRoot = ""; 235 | targets = ( 236 | 7A6E008B214CBD690020ED6D /* NeTool */, 237 | 7A6E009D214CBD6A0020ED6D /* NeToolTests */, 238 | 7A6E00A8214CBD6A0020ED6D /* NeToolUITests */, 239 | ); 240 | }; 241 | /* End PBXProject section */ 242 | 243 | /* Begin PBXResourcesBuildPhase section */ 244 | 7A6E008A214CBD690020ED6D /* Resources */ = { 245 | isa = PBXResourcesBuildPhase; 246 | buildActionMask = 2147483647; 247 | files = ( 248 | 7A6E0094214CBD6A0020ED6D /* Assets.xcassets in Resources */, 249 | 7A431F3B2509BDFA0050327A /* SpeedInfoView.xib in Resources */, 250 | 7A6E0097214CBD6A0020ED6D /* Main.storyboard in Resources */, 251 | ); 252 | runOnlyForDeploymentPostprocessing = 0; 253 | }; 254 | 7A6E009C214CBD6A0020ED6D /* Resources */ = { 255 | isa = PBXResourcesBuildPhase; 256 | buildActionMask = 2147483647; 257 | files = ( 258 | ); 259 | runOnlyForDeploymentPostprocessing = 0; 260 | }; 261 | 7A6E00A7214CBD6A0020ED6D /* Resources */ = { 262 | isa = PBXResourcesBuildPhase; 263 | buildActionMask = 2147483647; 264 | files = ( 265 | ); 266 | runOnlyForDeploymentPostprocessing = 0; 267 | }; 268 | /* End PBXResourcesBuildPhase section */ 269 | 270 | /* Begin PBXSourcesBuildPhase section */ 271 | 7A6E0088214CBD690020ED6D /* Sources */ = { 272 | isa = PBXSourcesBuildPhase; 273 | buildActionMask = 2147483647; 274 | files = ( 275 | 7A6E00BC214CBD9B0020ED6D /* StatusBarView.swift in Sources */, 276 | 7A6E0092214CBD690020ED6D /* ViewController.swift in Sources */, 277 | 7A431F392508E1D20050327A /* SpeedInfoView.swift in Sources */, 278 | 7A6E0090214CBD690020ED6D /* AppDelegate.swift in Sources */, 279 | 7A6E00BE214CDDA30020ED6D /* NetSpeedMonitor.swift in Sources */, 280 | ); 281 | runOnlyForDeploymentPostprocessing = 0; 282 | }; 283 | 7A6E009A214CBD6A0020ED6D /* Sources */ = { 284 | isa = PBXSourcesBuildPhase; 285 | buildActionMask = 2147483647; 286 | files = ( 287 | 7A6E00A3214CBD6A0020ED6D /* NeToolTests.swift in Sources */, 288 | ); 289 | runOnlyForDeploymentPostprocessing = 0; 290 | }; 291 | 7A6E00A5214CBD6A0020ED6D /* Sources */ = { 292 | isa = PBXSourcesBuildPhase; 293 | buildActionMask = 2147483647; 294 | files = ( 295 | 7A6E00AE214CBD6A0020ED6D /* NeToolUITests.swift in Sources */, 296 | ); 297 | runOnlyForDeploymentPostprocessing = 0; 298 | }; 299 | /* End PBXSourcesBuildPhase section */ 300 | 301 | /* Begin PBXTargetDependency section */ 302 | 7A6E00A0214CBD6A0020ED6D /* PBXTargetDependency */ = { 303 | isa = PBXTargetDependency; 304 | target = 7A6E008B214CBD690020ED6D /* NeTool */; 305 | targetProxy = 7A6E009F214CBD6A0020ED6D /* PBXContainerItemProxy */; 306 | }; 307 | 7A6E00AB214CBD6A0020ED6D /* PBXTargetDependency */ = { 308 | isa = PBXTargetDependency; 309 | target = 7A6E008B214CBD690020ED6D /* NeTool */; 310 | targetProxy = 7A6E00AA214CBD6A0020ED6D /* PBXContainerItemProxy */; 311 | }; 312 | /* End PBXTargetDependency section */ 313 | 314 | /* Begin PBXVariantGroup section */ 315 | 7A6E0095214CBD6A0020ED6D /* Main.storyboard */ = { 316 | isa = PBXVariantGroup; 317 | children = ( 318 | 7A6E0096214CBD6A0020ED6D /* Base */, 319 | ); 320 | name = Main.storyboard; 321 | sourceTree = ""; 322 | }; 323 | /* End PBXVariantGroup section */ 324 | 325 | /* Begin XCBuildConfiguration section */ 326 | 7A6E00B0214CBD6A0020ED6D /* Debug */ = { 327 | isa = XCBuildConfiguration; 328 | buildSettings = { 329 | ALWAYS_SEARCH_USER_PATHS = NO; 330 | CLANG_ANALYZER_NONNULL = YES; 331 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 332 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 333 | CLANG_CXX_LIBRARY = "libc++"; 334 | CLANG_ENABLE_MODULES = YES; 335 | CLANG_ENABLE_OBJC_ARC = YES; 336 | CLANG_ENABLE_OBJC_WEAK = YES; 337 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 338 | CLANG_WARN_BOOL_CONVERSION = YES; 339 | CLANG_WARN_COMMA = YES; 340 | CLANG_WARN_CONSTANT_CONVERSION = YES; 341 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 342 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 343 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 344 | CLANG_WARN_EMPTY_BODY = YES; 345 | CLANG_WARN_ENUM_CONVERSION = YES; 346 | CLANG_WARN_INFINITE_RECURSION = YES; 347 | CLANG_WARN_INT_CONVERSION = YES; 348 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 349 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 350 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 351 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 352 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 353 | CLANG_WARN_STRICT_PROTOTYPES = YES; 354 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 355 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 356 | CLANG_WARN_UNREACHABLE_CODE = YES; 357 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 358 | CODE_SIGN_IDENTITY = "-"; 359 | COPY_PHASE_STRIP = NO; 360 | DEBUG_INFORMATION_FORMAT = dwarf; 361 | ENABLE_STRICT_OBJC_MSGSEND = YES; 362 | ENABLE_TESTABILITY = YES; 363 | GCC_C_LANGUAGE_STANDARD = gnu11; 364 | GCC_DYNAMIC_NO_PIC = NO; 365 | GCC_NO_COMMON_BLOCKS = YES; 366 | GCC_OPTIMIZATION_LEVEL = 0; 367 | GCC_PREPROCESSOR_DEFINITIONS = ( 368 | "DEBUG=1", 369 | "$(inherited)", 370 | ); 371 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 372 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 373 | GCC_WARN_UNDECLARED_SELECTOR = YES; 374 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 375 | GCC_WARN_UNUSED_FUNCTION = YES; 376 | GCC_WARN_UNUSED_VARIABLE = YES; 377 | MACOSX_DEPLOYMENT_TARGET = 10.13; 378 | MTL_ENABLE_DEBUG_INFO = YES; 379 | ONLY_ACTIVE_ARCH = YES; 380 | SDKROOT = macosx; 381 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 382 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 383 | }; 384 | name = Debug; 385 | }; 386 | 7A6E00B1214CBD6A0020ED6D /* Release */ = { 387 | isa = XCBuildConfiguration; 388 | buildSettings = { 389 | ALWAYS_SEARCH_USER_PATHS = NO; 390 | CLANG_ANALYZER_NONNULL = YES; 391 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 392 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 393 | CLANG_CXX_LIBRARY = "libc++"; 394 | CLANG_ENABLE_MODULES = YES; 395 | CLANG_ENABLE_OBJC_ARC = YES; 396 | CLANG_ENABLE_OBJC_WEAK = YES; 397 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 398 | CLANG_WARN_BOOL_CONVERSION = YES; 399 | CLANG_WARN_COMMA = YES; 400 | CLANG_WARN_CONSTANT_CONVERSION = YES; 401 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 402 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 403 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 404 | CLANG_WARN_EMPTY_BODY = YES; 405 | CLANG_WARN_ENUM_CONVERSION = YES; 406 | CLANG_WARN_INFINITE_RECURSION = YES; 407 | CLANG_WARN_INT_CONVERSION = YES; 408 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 409 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 410 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 411 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 412 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 413 | CLANG_WARN_STRICT_PROTOTYPES = YES; 414 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 415 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 416 | CLANG_WARN_UNREACHABLE_CODE = YES; 417 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 418 | CODE_SIGN_IDENTITY = "-"; 419 | COPY_PHASE_STRIP = NO; 420 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 421 | ENABLE_NS_ASSERTIONS = NO; 422 | ENABLE_STRICT_OBJC_MSGSEND = YES; 423 | GCC_C_LANGUAGE_STANDARD = gnu11; 424 | GCC_NO_COMMON_BLOCKS = YES; 425 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 426 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 427 | GCC_WARN_UNDECLARED_SELECTOR = YES; 428 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 429 | GCC_WARN_UNUSED_FUNCTION = YES; 430 | GCC_WARN_UNUSED_VARIABLE = YES; 431 | MACOSX_DEPLOYMENT_TARGET = 10.13; 432 | MTL_ENABLE_DEBUG_INFO = NO; 433 | SDKROOT = macosx; 434 | SWIFT_COMPILATION_MODE = wholemodule; 435 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 436 | }; 437 | name = Release; 438 | }; 439 | 7A6E00B3214CBD6A0020ED6D /* Debug */ = { 440 | isa = XCBuildConfiguration; 441 | buildSettings = { 442 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 443 | CODE_SIGN_STYLE = Automatic; 444 | COMBINE_HIDPI_IMAGES = YES; 445 | CURRENT_PROJECT_VERSION = 3; 446 | INFOPLIST_FILE = NeTool/Info.plist; 447 | LD_RUNPATH_SEARCH_PATHS = ( 448 | "$(inherited)", 449 | "@executable_path/../Frameworks", 450 | ); 451 | MACOSX_DEPLOYMENT_TARGET = 10.12; 452 | MARKETING_VERSION = 1.2; 453 | PRODUCT_BUNDLE_IDENTIFIER = MicroStrategy.NeTool; 454 | PRODUCT_NAME = "$(TARGET_NAME)"; 455 | SWIFT_VERSION = 5.0; 456 | }; 457 | name = Debug; 458 | }; 459 | 7A6E00B4214CBD6A0020ED6D /* Release */ = { 460 | isa = XCBuildConfiguration; 461 | buildSettings = { 462 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 463 | CODE_SIGN_STYLE = Automatic; 464 | COMBINE_HIDPI_IMAGES = YES; 465 | CURRENT_PROJECT_VERSION = 3; 466 | INFOPLIST_FILE = NeTool/Info.plist; 467 | LD_RUNPATH_SEARCH_PATHS = ( 468 | "$(inherited)", 469 | "@executable_path/../Frameworks", 470 | ); 471 | MACOSX_DEPLOYMENT_TARGET = 10.12; 472 | MARKETING_VERSION = 1.2; 473 | PRODUCT_BUNDLE_IDENTIFIER = MicroStrategy.NeTool; 474 | PRODUCT_NAME = "$(TARGET_NAME)"; 475 | SWIFT_VERSION = 5.0; 476 | }; 477 | name = Release; 478 | }; 479 | 7A6E00B6214CBD6A0020ED6D /* Debug */ = { 480 | isa = XCBuildConfiguration; 481 | buildSettings = { 482 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 483 | BUNDLE_LOADER = "$(TEST_HOST)"; 484 | CODE_SIGN_STYLE = Automatic; 485 | COMBINE_HIDPI_IMAGES = YES; 486 | INFOPLIST_FILE = NeToolTests/Info.plist; 487 | LD_RUNPATH_SEARCH_PATHS = ( 488 | "$(inherited)", 489 | "@executable_path/../Frameworks", 490 | "@loader_path/../Frameworks", 491 | ); 492 | PRODUCT_BUNDLE_IDENTIFIER = MicroStrategy.NeToolTests; 493 | PRODUCT_NAME = "$(TARGET_NAME)"; 494 | SWIFT_VERSION = 4.0; 495 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/NeTool.app/Contents/MacOS/NeTool"; 496 | }; 497 | name = Debug; 498 | }; 499 | 7A6E00B7214CBD6A0020ED6D /* Release */ = { 500 | isa = XCBuildConfiguration; 501 | buildSettings = { 502 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 503 | BUNDLE_LOADER = "$(TEST_HOST)"; 504 | CODE_SIGN_STYLE = Automatic; 505 | COMBINE_HIDPI_IMAGES = YES; 506 | INFOPLIST_FILE = NeToolTests/Info.plist; 507 | LD_RUNPATH_SEARCH_PATHS = ( 508 | "$(inherited)", 509 | "@executable_path/../Frameworks", 510 | "@loader_path/../Frameworks", 511 | ); 512 | PRODUCT_BUNDLE_IDENTIFIER = MicroStrategy.NeToolTests; 513 | PRODUCT_NAME = "$(TARGET_NAME)"; 514 | SWIFT_VERSION = 4.0; 515 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/NeTool.app/Contents/MacOS/NeTool"; 516 | }; 517 | name = Release; 518 | }; 519 | 7A6E00B9214CBD6A0020ED6D /* Debug */ = { 520 | isa = XCBuildConfiguration; 521 | buildSettings = { 522 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 523 | CODE_SIGN_STYLE = Automatic; 524 | COMBINE_HIDPI_IMAGES = YES; 525 | INFOPLIST_FILE = NeToolUITests/Info.plist; 526 | LD_RUNPATH_SEARCH_PATHS = ( 527 | "$(inherited)", 528 | "@executable_path/../Frameworks", 529 | "@loader_path/../Frameworks", 530 | ); 531 | PRODUCT_BUNDLE_IDENTIFIER = MicroStrategy.NeToolUITests; 532 | PRODUCT_NAME = "$(TARGET_NAME)"; 533 | SWIFT_VERSION = 4.0; 534 | TEST_TARGET_NAME = NeTool; 535 | }; 536 | name = Debug; 537 | }; 538 | 7A6E00BA214CBD6A0020ED6D /* Release */ = { 539 | isa = XCBuildConfiguration; 540 | buildSettings = { 541 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 542 | CODE_SIGN_STYLE = Automatic; 543 | COMBINE_HIDPI_IMAGES = YES; 544 | INFOPLIST_FILE = NeToolUITests/Info.plist; 545 | LD_RUNPATH_SEARCH_PATHS = ( 546 | "$(inherited)", 547 | "@executable_path/../Frameworks", 548 | "@loader_path/../Frameworks", 549 | ); 550 | PRODUCT_BUNDLE_IDENTIFIER = MicroStrategy.NeToolUITests; 551 | PRODUCT_NAME = "$(TARGET_NAME)"; 552 | SWIFT_VERSION = 4.0; 553 | TEST_TARGET_NAME = NeTool; 554 | }; 555 | name = Release; 556 | }; 557 | /* End XCBuildConfiguration section */ 558 | 559 | /* Begin XCConfigurationList section */ 560 | 7A6E0087214CBD690020ED6D /* Build configuration list for PBXProject "NeTool" */ = { 561 | isa = XCConfigurationList; 562 | buildConfigurations = ( 563 | 7A6E00B0214CBD6A0020ED6D /* Debug */, 564 | 7A6E00B1214CBD6A0020ED6D /* Release */, 565 | ); 566 | defaultConfigurationIsVisible = 0; 567 | defaultConfigurationName = Release; 568 | }; 569 | 7A6E00B2214CBD6A0020ED6D /* Build configuration list for PBXNativeTarget "NeTool" */ = { 570 | isa = XCConfigurationList; 571 | buildConfigurations = ( 572 | 7A6E00B3214CBD6A0020ED6D /* Debug */, 573 | 7A6E00B4214CBD6A0020ED6D /* Release */, 574 | ); 575 | defaultConfigurationIsVisible = 0; 576 | defaultConfigurationName = Release; 577 | }; 578 | 7A6E00B5214CBD6A0020ED6D /* Build configuration list for PBXNativeTarget "NeToolTests" */ = { 579 | isa = XCConfigurationList; 580 | buildConfigurations = ( 581 | 7A6E00B6214CBD6A0020ED6D /* Debug */, 582 | 7A6E00B7214CBD6A0020ED6D /* Release */, 583 | ); 584 | defaultConfigurationIsVisible = 0; 585 | defaultConfigurationName = Release; 586 | }; 587 | 7A6E00B8214CBD6A0020ED6D /* Build configuration list for PBXNativeTarget "NeToolUITests" */ = { 588 | isa = XCConfigurationList; 589 | buildConfigurations = ( 590 | 7A6E00B9214CBD6A0020ED6D /* Debug */, 591 | 7A6E00BA214CBD6A0020ED6D /* Release */, 592 | ); 593 | defaultConfigurationIsVisible = 0; 594 | defaultConfigurationName = Release; 595 | }; 596 | /* End XCConfigurationList section */ 597 | }; 598 | rootObject = 7A6E0084214CBD690020ED6D /* Project object */; 599 | } 600 | -------------------------------------------------------------------------------- /NeTool.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /NeTool.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /NeTool.xcodeproj/xcuserdata/taliu.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | NeTool.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | NeTool.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 0 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /NeTool/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // NeTool 4 | // 5 | // Created by Liu, Tao (Toni) on 9/15/18. 6 | // Copyright © 2018 Liu, Tao (Toni). All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | @NSApplicationMain 12 | class AppDelegate: NSObject, NSApplicationDelegate { 13 | 14 | let speedMonitor: NetSpeedMonitor 15 | 16 | override init() { 17 | let statusItem = NSStatusBar.system.statusItem(withLength: 72) 18 | let menu = NSMenu() 19 | 20 | // the menu item to show apps with top net speed (sum of upload and download) 21 | let menuItem = NSMenuItem() 22 | menuItem.view = SpeedInfoView() 23 | menu.addItem(menuItem) 24 | 25 | // the menu item to quit app. 26 | menu.addItem(withTitle: "Quit NeTool", action: #selector(menuItemQuitClick), keyEquivalent: "q") 27 | 28 | // the view for menuBar icon 29 | let menuBarIconView = StatusBarView(statusItem: statusItem, menu: menu) 30 | statusItem.view = menuBarIconView 31 | 32 | // logic class to monitor net speed. 33 | speedMonitor = NetSpeedMonitor(statusBarView: menuBarIconView, speedInfoView: menuItem.view as! SpeedInfoView) 34 | menuBarIconView.speedMonitor = speedMonitor 35 | } 36 | 37 | func applicationDidFinishLaunching(_ aNotification: Notification) { 38 | // 39 | speedMonitor.start() 40 | 41 | // observer event of system sleep and wake. 42 | // we would pause monitoring on sleep, and resume monitoring on wake. 43 | NSWorkspace.shared.notificationCenter.addObserver( 44 | self, selector: #selector(onWake(notification:)), 45 | name: NSWorkspace.didWakeNotification, object: nil) 46 | 47 | NSWorkspace.shared.notificationCenter.addObserver( 48 | self, selector: #selector(onSleep(notification:)), 49 | name: NSWorkspace.willSleepNotification, object: nil) 50 | } 51 | 52 | func applicationWillTerminate(_ aNotification: Notification) { 53 | 54 | } 55 | 56 | @objc func onSleep(notification: NSNotification) { 57 | speedMonitor.stop() 58 | } 59 | 60 | @objc func onWake(notification: NSNotification) { 61 | speedMonitor.start() 62 | } 63 | } 64 | 65 | extension AppDelegate { 66 | @objc func menuItemQuitClick() { 67 | NSApp.terminate(nil) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /NeTool/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /NeTool/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /NeTool/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | Default 509 | 510 | 511 | 512 | 513 | 514 | 515 | Left to Right 516 | 517 | 518 | 519 | 520 | 521 | 522 | Right to Left 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | Default 534 | 535 | 536 | 537 | 538 | 539 | 540 | Left to Right 541 | 542 | 543 | 544 | 545 | 546 | 547 | Right to Left 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | -------------------------------------------------------------------------------- /NeTool/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | LSUIElement 26 | 27 | NSHumanReadableCopyright 28 | Copyright © 2018 Liu, Tao (Toni). All rights reserved. 29 | NSMainStoryboardFile 30 | Main 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /NeTool/NeTool.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /NeTool/NetSpeedMonitor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetSpeedUtils.swift 3 | // NeTool 4 | // 5 | // Created by Liu, Tao (Toni) on 9/15/18. 6 | // Copyright © 2018 Liu, Tao (Toni). All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | open class NetSpeedMonitor { 12 | static let interval: Int = 1400 13 | static let KB: Double = 1024 14 | static let MB: Double = KB * 1024 15 | static let GB: Double = MB * 1024 16 | static let TB: Double = GB * 1024 17 | static let TOP_ITEM_COUNT = 5; 18 | 19 | // sum of upload bytes by all apps in last sample data. 20 | var upBytesOfLast = 0 21 | var downBytesOfLast = 0 22 | 23 | // sum of upload bytes by all apps in current sample data. 24 | var upBytesOfCur = 0 25 | var downBytesOfCur = 0 26 | 27 | // process ids of apps appeared in last sample data. 28 | var pidsOfLastOutput = Array() 29 | var pidsOfCurOutput = Array() 30 | 31 | // stores info of bytes and speed of multiple apps. 32 | var pbArray = Array() 33 | 34 | // the following 2 are for using "-l 0" argument of nettop command, which means the command 35 | // would continuously output sample data, and we only execute this command once, and continuously 36 | // consume the data. 37 | // but currently we still use "-l 1" due to stability problem. 38 | // the last line of last output, but the line is incomplete, hence we save and use it when next output is available. 39 | var inCompleteLastLineOfLastOutput = "" 40 | // the length of last header line. a header line starts with word "time", ends with word "bytes_out" 41 | var lenOflastHeaderLine = 0 42 | 43 | // timer to periodiclly execute nettop command. 44 | var timer: DispatchSourceTimer? = nil 45 | 46 | let statusBarView: StatusBarView 47 | let speedInfoView: SpeedInfoView 48 | 49 | init(statusBarView: StatusBarView, speedInfoView: SpeedInfoView) { 50 | self.statusBarView = statusBarView 51 | self.speedInfoView = speedInfoView 52 | } 53 | 54 | func start() { 55 | if (timer != nil) { 56 | timer?.resume() 57 | return 58 | } 59 | 60 | timer = DispatchSource.makeTimerSource(flags: [], queue: DispatchQueue.global()) 61 | timer?.schedule( 62 | deadline: .now(), 63 | repeating: DispatchTimeInterval.milliseconds(NetSpeedMonitor.interval), 64 | leeway: DispatchTimeInterval.milliseconds(NetSpeedMonitor.interval)) 65 | timer?.setEventHandler { 66 | // Create a Task instance 67 | let task = Process() 68 | task.launchPath = "/usr/bin/nettop" 69 | // -x to get value with Byte as unit, rather than MB, GB etc. 70 | // -t wifi -t wired to choose type of network interface we want. 71 | // -J to pick columns of output we want. 72 | // -l 1 to get only one sample data. 73 | task.arguments = [ 74 | "-x", "-t", "wifi", "-t", "wired", "-J","time,bytes_in,bytes_out", "-P", "-l", "1"] 75 | let pipe = Pipe() 76 | task.standardOutput = pipe 77 | // Launch the task 78 | task.launch() 79 | 80 | // Get the data 81 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 82 | if #available(macOS 10.15, *) { 83 | do { 84 | try pipe.fileHandleForReading.close() 85 | } catch { } 86 | } else { 87 | pipe.fileHandleForReading.closeFile() 88 | } 89 | let output = String(data: data, encoding: String.Encoding.utf8) ?? "" 90 | self.handleOutput(fetchedData: output) 91 | } 92 | timer?.resume() 93 | } 94 | 95 | func stop() { 96 | timer?.suspend() 97 | 98 | // reset the following variables. 99 | 100 | upBytesOfLast = 0 101 | downBytesOfLast = 0 102 | 103 | upBytesOfCur = 0 104 | downBytesOfCur = 0 105 | 106 | inCompleteLastLineOfLastOutput = "" 107 | lenOflastHeaderLine = 0 108 | 109 | pidsOfLastOutput.removeAll() 110 | pidsOfCurOutput.removeAll() 111 | 112 | pbArray.removeAll() 113 | 114 | self.statusBarView.updateData(up: StatusBarView.INITIAL_RATE_TEXT, down: StatusBarView.INITIAL_RATE_TEXT) 115 | } 116 | 117 | // this methods contains logic to consider unexpected output of nettop with "-l 0" argument, 118 | // see comment of inCompleteLastLineOfLastOutput. 119 | // many code are unnecessary for nettop with "-l 1" argument, i.e. current situation. 120 | func handleOutput(fetchedData: String) { 121 | 122 | // the original nettop with previous arguments would produce output intervally and neatly. 123 | // i.e. if you set "-s 1" argument for the command, the output would be produced per second, 124 | // and each output is just sample of that second. 125 | // however, each batch of output text we fetched from the data observer might be not one complete sample output. 126 | 127 | var validLines = Array() 128 | // first, split the output into several lines. the 1st line and last line might be incomplete. 129 | // so we insert the inCompleteLastLineOfLastOutput in front of it, to mke sure 1st line is complete. 130 | let lines = (inCompleteLastLineOfLastOutput + fetchedData).split(separator: "\n") 131 | inCompleteLastLineOfLastOutput = "" // clear after usage. 132 | 133 | for i in 0...(lines.count - 1) { 134 | let line = lines[i] 135 | if line.count == 0 { // skip empty lines. 136 | continue 137 | } 138 | 139 | if line.starts(with: "time") { 140 | // if this is a header line and is complete, update the lenOflastHeaderLine 141 | if String(line).substring(fromIndex: line.count - 3) == "out" { 142 | validLines.append(String(line)) 143 | lenOflastHeaderLine = line.count 144 | } else { 145 | if (i == lines.count - 1) { 146 | inCompleteLastLineOfLastOutput = String(line) 147 | } else { 148 | // unexpected output 149 | } 150 | } 151 | } else { 152 | // condition to check whether the line is complete 153 | if line.count == lenOflastHeaderLine { 154 | validLines.append(String(line)) 155 | } else { 156 | if (i == lines.count - 1) { 157 | inCompleteLastLineOfLastOutput = String(line) 158 | } else { 159 | // unexpected output 160 | } 161 | } 162 | } 163 | } 164 | 165 | // clear 166 | upBytesOfCur = 0; 167 | downBytesOfCur = 0; 168 | pidsOfCurOutput.removeAll() 169 | 170 | if pbArray.count > 0 { 171 | // iterate for each pbArray element. 172 | for i in 0...(pbArray.count - 1) { 173 | pbArray[i].upBytes1 = pbArray[i].upBytes2 174 | pbArray[i].upBytes2 = 0 175 | pbArray[i].downBytes1 = pbArray[i].downBytes2 176 | pbArray[i].downBytes2 = 0 177 | } 178 | } 179 | 180 | // now all lines inside validLines are complete, including the header line. handle them. 181 | for line in validLines { 182 | handleOneLineOutput(line: line) 183 | } 184 | // update the menubar icon. 185 | if (upBytesOfLast > 0 && downBytesOfLast > 0) { 186 | let upStr = NetSpeedMonitor.getSpeedString(bytes1: upBytesOfLast, bytes2: upBytesOfCur) 187 | let downStr = NetSpeedMonitor.getSpeedString(bytes1: downBytesOfLast, bytes2: downBytesOfCur) 188 | self.statusBarView.updateData(up: upStr, down: downStr) 189 | } 190 | // iterate. 191 | upBytesOfLast = upBytesOfCur 192 | downBytesOfLast = downBytesOfCur 193 | 194 | // sort by sum of up & down bytes. 195 | pbArray.sort(by: {pb1, pb2 in 196 | (pb1.downBytes2 - pb1.downBytes1 + pb1.upBytes2 - pb1.upBytes1) > 197 | (pb2.downBytes2 - pb2.downBytes1 + pb2.upBytes2 - pb2.upBytes1) 198 | }) 199 | 200 | // check if pids of current output mostly appear in last output. 201 | // if not so, restart the monitoring. 202 | if pidsOfLastOutput.count > 0 { 203 | var appearedCount = 0 204 | for pid in pidsOfCurOutput { 205 | if pidsOfLastOutput.contains(pid) { 206 | appearedCount += 1 207 | } 208 | } 209 | // more than 3 process not appear in last sample 210 | if pidsOfCurOutput.count - appearedCount > 3 { 211 | // unexpected output 212 | } 213 | } 214 | 215 | // iterate 216 | pidsOfLastOutput.removeAll() 217 | for pid in pidsOfCurOutput { 218 | pidsOfLastOutput.append(pid) 219 | } 220 | 221 | // if dropdown menu is expanded, calculate TOP_ITEM_COUNT processes with top download speed. 222 | if (statusBarView.isMenuShown()) { 223 | updateTopSpeedItems() 224 | } 225 | } 226 | 227 | // header line is like "time bytes_in bytes_out" 228 | // other lines are like "16:59:11.290649 UserEventAgent.104 313206 431240", which contains time, process name, process id, bytes downloaded and bytes uploaded. 229 | func handleOneLineOutput(line: String) { 230 | if line.starts(with: "time") { // skip header line. 231 | return 232 | } 233 | 234 | let lineParts = line.split(separator: " ") 235 | let downBytes:Int = Int(lineParts[lineParts.count - 2])! 236 | let upBytes:Int = Int(lineParts[lineParts.count - 1])! 237 | upBytesOfCur += upBytes 238 | downBytesOfCur += downBytes 239 | 240 | // process name and process id, like "Google Chrome H.1567", we need to get the pid. 241 | let pNameAndPid = String(lineParts[lineParts.count - 3]) 242 | let pid = String(pNameAndPid[pNameAndPid.index(after: pNameAndPid.lastIndex(of: ".")!)...]) 243 | let pbIdx = getPbIndexByPid(pid: pid) 244 | // check whether there is already a ProcessBytes object for this process. 245 | if pbIdx == nil { 246 | // no, then create a new one. 247 | let pb = ProcessBytes(pid: pid, upBytes1: 0, upBytes2: upBytes, downBytes1: 0, downBytes2: downBytes) 248 | pbArray.append(pb) 249 | } else { 250 | pbArray[pbIdx!].upBytes2 = upBytes 251 | pbArray[pbIdx!].downBytes2 = downBytes 252 | } 253 | // store the process id 254 | pidsOfCurOutput.append(pid) 255 | } 256 | 257 | // bytes1: accumulated bytes of last sample 258 | // bytes2: accumulated bytes of current sample 259 | static func getSpeedString(bytes1: Int, bytes2: Int) -> String { 260 | let bytesPerSecond = (bytes2 - bytes1) * 1000 / interval 261 | 262 | var result:Double 263 | var unit: String 264 | 265 | if (bytesPerSecond < 10) { 266 | return "0 B/S" 267 | } else if bytesPerSecond < 1000 { 268 | return String(bytesPerSecond) + " B/S" 269 | } 270 | let bytesPerSecondDouble = (Double)(bytesPerSecond) 271 | if bytesPerSecondDouble < 1000 * KB { 272 | result = bytesPerSecondDouble / KB 273 | unit = "K/S" 274 | } else if bytesPerSecondDouble < 1000 * MB { 275 | result = bytesPerSecondDouble / MB 276 | unit = "M/S" 277 | } else if bytesPerSecondDouble < 1000 * GB { 278 | result = bytesPerSecondDouble / GB 279 | unit = "G/S" 280 | } else { 281 | return "MAX /S" 282 | } 283 | 284 | if result < 100 { 285 | // keep at most 2 decimals. 286 | return String((result * 100).rounded() / 100) + " " + unit 287 | } else { 288 | // keep at most 1 decimal. 289 | return String((result * 10).rounded() / 10) + " " + unit 290 | } 291 | } 292 | 293 | func getPbIndexByPid(pid: String) -> Int? { 294 | if (pbArray.count > 0) { 295 | for i in 0...(pbArray.count - 1) { 296 | if pbArray[i].pid == pid { 297 | return i 298 | } 299 | } 300 | } 301 | return nil 302 | } 303 | 304 | // get an array of SpeedInfo objects which represent apps with top net speed 305 | func getTopSpeedInfo() -> Array? { 306 | if (pbArray.count < 5) { 307 | return nil 308 | } 309 | // use ps command to get all process path of the top 5 processes. 310 | let pidsTemplate = "%@,%@,%@,%@,%@" 311 | let pids = String(format: pidsTemplate, pbArray[0].pid, pbArray[1].pid, pbArray[2].pid, pbArray[3].pid, pbArray[4].pid) 312 | let task = Process() 313 | task.launchPath = "/bin/ps" 314 | task.arguments = ["-p", pids] 315 | let pipe = Pipe() 316 | task.standardOutput = pipe 317 | task.launch() 318 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 319 | let output = String(data: data, encoding: String.Encoding.utf8) ?? "" 320 | let lines = output.split(separator: "\n") 321 | var result = Array() 322 | 323 | let pathStartIdx = String(lines[0]).indexOf(str: "CMD") 324 | for i in 0...(min(NetSpeedMonitor.TOP_ITEM_COUNT, pbArray.count) - 1) { 325 | // find the line which contains pid of pbArray[i]. 326 | var line: String? = nil 327 | for ln in lines { 328 | if ln.trimmingCharacters(in: .whitespacesAndNewlines).starts(with: pbArray[i].pid) { 329 | line = String(ln) 330 | break 331 | } 332 | } 333 | if line != nil { 334 | // for XXX.app case, only take XXX.app as path. 335 | let idx = line!.range(of: ".app/Contents/")?.lowerBound 336 | var path: String 337 | if idx == nil { 338 | let wholeCmd = line!.substring(fromIndex: pathStartIdx) 339 | // only take characters before the first space, because characters after 340 | // the space might be arguments of the process. 341 | let spaceIdx = wholeCmd.indexOf(str: " ") 342 | if spaceIdx > 0 { 343 | path = wholeCmd.substring(toIndex: spaceIdx) 344 | } else { 345 | path = wholeCmd 346 | } 347 | } else { 348 | let trimedDotApp = String(line![.. String { 392 | return self[i ..< i + 1] 393 | } 394 | 395 | func substring(fromIndex: Int) -> String { 396 | return self[min(fromIndex, length) ..< length] 397 | } 398 | 399 | func substring(toIndex: Int) -> String { 400 | return self[0 ..< max(0, toIndex)] 401 | } 402 | 403 | subscript (r: Range) -> String { 404 | let range = Range(uncheckedBounds: (lower: max(0, min(length, r.lowerBound)), 405 | upper: min(length, max(0, r.upperBound)))) 406 | let start = index(startIndex, offsetBy: range.lowerBound) 407 | let end = index(start, offsetBy: range.upperBound - range.lowerBound) 408 | return String(self[start ..< end]) 409 | } 410 | 411 | func indexOf(str: String) -> Int { 412 | return range(of: str)?.lowerBound.utf16Offset(in: self) ?? -1 413 | } 414 | } 415 | -------------------------------------------------------------------------------- /NeTool/SpeedInfoView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpeedInfoView.swift 3 | // NeTool 4 | // 5 | // Created by Liu, Tao (Toni) on 9/9/20. 6 | // Copyright © 2020 Liu, Tao (Toni). All rights reserved. 7 | // 8 | 9 | import AppKit 10 | 11 | class SpeedInfoView: NSControl { 12 | 13 | @IBOutlet weak var hintHabel: NSTextField! 14 | 15 | @IBOutlet weak var pathLabel0: NSTextField! 16 | @IBOutlet weak var pathLabel1: NSTextField! 17 | @IBOutlet weak var pathLabel2: NSTextField! 18 | @IBOutlet weak var pathLabel3: NSTextField! 19 | 20 | @IBOutlet weak var speedLabel0: NSTextField! 21 | @IBOutlet weak var speedLabel1: NSTextField! 22 | @IBOutlet weak var speedLabel2: NSTextField! 23 | @IBOutlet weak var speedLabel3: NSTextField! 24 | 25 | var pathLabelArr: Array = [] 26 | var speedLabelArr: Array = [] 27 | 28 | init() { 29 | 30 | super.init(frame: NSMakeRect(0, 0, 300, 148)) 31 | 32 | // load from xib file. 33 | let newNib = NSNib(nibNamed: "SpeedInfoView", bundle: Bundle(for: type(of: self))) 34 | newNib!.instantiate(withOwner: self, topLevelObjects: nil) 35 | 36 | pathLabelArr = [pathLabel0, pathLabel1, pathLabel2, pathLabel3] 37 | speedLabelArr = [speedLabel0, speedLabel1, speedLabel2, speedLabel3] 38 | 39 | addSubview(hintHabel) 40 | hintHabel.stringValue = "Click to copy path" 41 | for label in pathLabelArr { 42 | addSubview(label) 43 | label.stringValue = "- - -" 44 | } 45 | for label in speedLabelArr { 46 | addSubview(label) 47 | label.stringValue = "- - B/S ▲\n- - B/S ▼" 48 | } 49 | 50 | setNeedsDisplay() 51 | } 52 | 53 | required public init?(coder: NSCoder) { 54 | fatalError("init(coder:) has not been implemented") 55 | } 56 | 57 | open override func draw(_ dirtyRect: NSRect) { 58 | // draw two divider lines. 59 | NSColor.gray.set() 60 | let figure = NSBezierPath() 61 | figure.lineWidth = 1 62 | figure.move(to: NSMakePoint(18, 6)) 63 | figure.line(to: NSMakePoint(290, 6)) 64 | figure.stroke() 65 | 66 | figure.move(to: NSMakePoint(18, 126)) 67 | figure.line(to: NSMakePoint(290, 126)) 68 | figure.stroke() 69 | } 70 | 71 | override func mouseDown(with event: NSEvent) { 72 | 73 | // event.locationInWindow is relative to window, convert that to be relative this view. 74 | let clickLocation = convert(event.locationInWindow, from: nil) 75 | 76 | for label in pathLabelArr { 77 | if (label.frame.contains(clickLocation)) { 78 | // copy path to clipborad, then users could use it. 79 | copyToClipBoard(textToCopy: label.stringValue) 80 | } 81 | } 82 | } 83 | 84 | func updateTopSpeedItems(infoArr: Array?) { 85 | DispatchQueue.main.async { 86 | if (infoArr != nil) { 87 | let count = min(infoArr!.count, self.pathLabelArr.count) 88 | for i in 0...(count - 1) { 89 | self.pathLabelArr[i].stringValue = infoArr![i].path 90 | self.speedLabelArr[i].stringValue = infoArr![i].upSpeed + " ▲\n" + infoArr![i].downSpeed + " ▼" 91 | //self.speedLabelArr[i].stringValue = "440.0 K/S" + " ▲\n" + "1000.8 M/S" + " ▼" 92 | } 93 | } 94 | } 95 | } 96 | 97 | private func copyToClipBoard(textToCopy: String) { 98 | let pasteBoard = NSPasteboard.general 99 | pasteBoard.clearContents() 100 | pasteBoard.setString(textToCopy, forType: .string) 101 | 102 | setHintText(new: "Path copied", duration: 2, recoverTo: "Click to copy path") 103 | } 104 | 105 | // new: new hint text to show 106 | // duration: time seconds the new hint would last. 107 | // recoverTo: hint text to shown after duration. 108 | private func setHintText(new: String, duration: Int, recoverTo: String) { 109 | hintHabel.stringValue = new 110 | 111 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(duration), execute: { 112 | self.hintHabel.stringValue = recoverTo 113 | }) 114 | } 115 | } 116 | 117 | -------------------------------------------------------------------------------- /NeTool/SpeedInfoView.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /NeTool/StatusBarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusBarView.swift 3 | // NeTool 4 | // 5 | // Created by Liu, Tao (Toni) on 9/15/18. 6 | // Copyright © 2018 Liu, Tao (Toni). All rights reserved. 7 | // 8 | 9 | import AppKit 10 | import Foundation 11 | 12 | open class StatusBarView: NSControl { 13 | public static let INITIAL_RATE_TEXT = "- - B/S" 14 | 15 | var statusItem: NSStatusItem 16 | // true if users clicked menubar icon, and dropdown menu is shown. 17 | var clicked: Bool = false 18 | // dark menu bar & dock style of OS before Mojave. 19 | var darkMenuBar: Bool = false 20 | var upRate: String = INITIAL_RATE_TEXT 21 | var downRate: String = INITIAL_RATE_TEXT 22 | 23 | public var speedMonitor: NetSpeedMonitor? 24 | 25 | init(statusItem: NSStatusItem, menu: NSMenu?) { 26 | self.statusItem = statusItem 27 | super.init(frame: NSMakeRect(0, 0, statusItem.length, NSStatusItem.squareLength)) 28 | self.menu = menu 29 | 30 | menu?.delegate = self 31 | 32 | darkMenuBar = isDarkMode() 33 | DistributedNotificationCenter.default().addObserver(self, selector: #selector(change), name:NSNotification.Name(rawValue: "AppleInterfaceThemeChangedNotification"), object: nil) 34 | } 35 | 36 | required public init?(coder: NSCoder) { 37 | fatalError("init(coder:) has not been implemented") 38 | } 39 | 40 | 41 | open override func draw(_ dirtyRect: NSRect) { 42 | // draw the background 43 | statusItem.drawStatusBarBackground(in: dirtyRect, withHighlight: clicked) 44 | 45 | // draw up speed string and down speed string. 46 | 47 | var textColor: NSColor 48 | if #available(macOS 11.0, *) { 49 | if (effectiveAppearance.name.rawValue.lowercased().contains("dark")) { 50 | textColor = NSColor.white 51 | } else { 52 | textColor = NSColor.black 53 | } 54 | } else { 55 | textColor = (clicked || darkMenuBar) ? NSColor.white : NSColor.black 56 | } 57 | 58 | let textAttr = [NSAttributedString.Key.font: NSFont.systemFont(ofSize: 9), NSAttributedString.Key.foregroundColor: textColor] 59 | 60 | let upRateStr = NSAttributedString(string: upRate + " ▲", attributes: textAttr) 61 | let upRateRect = upRateStr.boundingRect(with: NSSize(width: 100, height: 100), options: .usesLineFragmentOrigin) 62 | upRateStr.draw(at: NSMakePoint(bounds.width - upRateRect.width - 5, 10)) 63 | 64 | let downRateStr = NSAttributedString(string: downRate+" ▼", attributes: textAttr) 65 | let downRateRect = downRateStr.boundingRect(with: NSSize(width: 100, height: 100), options: .usesLineFragmentOrigin) 66 | downRateStr.draw(at: NSMakePoint(bounds.width - downRateRect.width - 5, 0)) 67 | } 68 | 69 | 70 | @objc func change() { 71 | darkMenuBar = isDarkMode() 72 | setNeedsDisplay() 73 | } 74 | 75 | func isDarkMode() -> Bool { 76 | let dict = UserDefaults.standard.persistentDomain(forName: UserDefaults.globalDomain) 77 | if let style:AnyObject = dict!["AppleInterfaceStyle"] as AnyObject? { 78 | if (style as! String).caseInsensitiveCompare("dark") == ComparisonResult.orderedSame { 79 | return true 80 | } 81 | } 82 | return false 83 | } 84 | 85 | func updateData(up: String, down: String) { 86 | upRate = up 87 | downRate = down 88 | 89 | // run in the main thread 90 | DispatchQueue.main.async(execute: { 91 | self.setNeedsDisplay() 92 | }) 93 | } 94 | 95 | func isMenuShown() -> Bool { 96 | return self.clicked 97 | } 98 | } 99 | 100 | //action 101 | extension StatusBarView: NSMenuDelegate{ 102 | open override func mouseDown(with theEvent: NSEvent) { 103 | 104 | statusItem.popUpMenu(menu!) 105 | } 106 | 107 | public func menuWillOpen(_ menu: NSMenu) { 108 | setNeedsDisplay() 109 | self.clicked = true 110 | 111 | // fetch the top speed info of the last sample, hence users 112 | // can see the results once menu is shown, rather than waiting for results of next sapmple. 113 | DispatchQueue.global().async { 114 | self.speedMonitor?.updateTopSpeedItems() 115 | } 116 | } 117 | 118 | public func menuDidClose(_ menu: NSMenu) { 119 | setNeedsDisplay() 120 | self.clicked = false 121 | } 122 | } 123 | 124 | -------------------------------------------------------------------------------- /NeTool/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // NeTool 4 | // 5 | // Created by Liu, Tao (Toni) on 9/15/18. 6 | // Copyright © 2018 Liu, Tao (Toni). All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ViewController: NSViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | // Do any additional setup after loading the view. 17 | } 18 | 19 | override var representedObject: Any? { 20 | didSet { 21 | // Update the view, if already loaded. 22 | } 23 | } 24 | 25 | 26 | } 27 | 28 | -------------------------------------------------------------------------------- /NeToolTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /NeToolTests/NeToolTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NeToolTests.swift 3 | // NeToolTests 4 | // 5 | // Created by Liu, Tao (Toni) on 9/15/18. 6 | // Copyright © 2018 Liu, Tao (Toni). All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NeTool 11 | 12 | class NeToolTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /NeToolUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /NeToolUITests/NeToolUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NeToolUITests.swift 3 | // NeToolUITests 4 | // 5 | // Created by Liu, Tao (Toni) on 9/15/18. 6 | // Copyright © 2018 Liu, Tao (Toni). All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class NeToolUITests: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | 18 | // In UI tests it is usually best to stop immediately when a failure occurs. 19 | continueAfterFailure = false 20 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 21 | XCUIApplication().launch() 22 | 23 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 24 | } 25 | 26 | override func tearDown() { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | super.tearDown() 29 | } 30 | 31 | func testExample() { 32 | // Use recording to get started writing UI tests. 33 | // Use XCTAssert and related functions to verify your tests produce the correct results. 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### What's this 2 | 3 | macOS 状态栏显示实时网速的小工具, macOS menubar app to minitor internet speed. 4 | 5 | #### version 1.1: 6 | 7 | ![](https://raw.githubusercontent.com/Tao93/Tao93.github.io/master/images/2020/09/12/1599909886.jpg) 8 | 9 | #### version 1.2: 10 | 11 | ![](http://tao93.top/images/2018/09/15/1537000487.png) 12 | 13 | #### 原理: 14 | 15 | 使用 macOS 中的 nettop 命令,即可查看当前时刻各进程已经 download 和 upload 的字节数,持续按时执行 nettop 命令然后求差,即可得知网速详情。 16 | 17 | Use the nettop command of macOS, we can get the uploaded and downloaded bytes of each process. By continuously execute the nettop command, we can know the net speed of each process. 18 | --------------------------------------------------------------------------------