├── LICENSE ├── Patchman.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcuserdata │ │ └── praneets.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ └── praneets.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── Patchman ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── ContentView.swift ├── Info.plist ├── Patchman.entitlements ├── Patchman.swift ├── PatchmanApp.swift └── Preview Content │ └── Preview Assets.xcassets │ └── Contents.json ├── README.md └── Snaps ├── Home1.png ├── Home2.png └── Screenshot 2021-04-07 at 7.06.52 PM.png /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Praneet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Patchman.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | B780945F26004E8C00AEB870 /* PatchmanApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B780945E26004E8C00AEB870 /* PatchmanApp.swift */; }; 11 | B780946126004E8C00AEB870 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B780946026004E8C00AEB870 /* ContentView.swift */; }; 12 | B780946326004E8D00AEB870 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B780946226004E8D00AEB870 /* Assets.xcassets */; }; 13 | B780946626004E8D00AEB870 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B780946526004E8D00AEB870 /* Preview Assets.xcassets */; }; 14 | B780947026004EA200AEB870 /* Patchman.swift in Sources */ = {isa = PBXBuildFile; fileRef = B780946F26004EA200AEB870 /* Patchman.swift */; }; 15 | B7BBE58A2609B4160087E461 /* CodeViewer in Frameworks */ = {isa = PBXBuildFile; productRef = B7BBE5892609B4160087E461 /* CodeViewer */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | B780945B26004E8C00AEB870 /* Patchman.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Patchman.app; sourceTree = BUILT_PRODUCTS_DIR; }; 20 | B780945E26004E8C00AEB870 /* PatchmanApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatchmanApp.swift; sourceTree = ""; }; 21 | B780946026004E8C00AEB870 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 22 | B780946226004E8D00AEB870 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 23 | B780946526004E8D00AEB870 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 24 | B780946726004E8D00AEB870 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 25 | B780946826004E8D00AEB870 /* Patchman.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Patchman.entitlements; sourceTree = ""; }; 26 | B780946F26004EA200AEB870 /* Patchman.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Patchman.swift; sourceTree = ""; }; 27 | /* End PBXFileReference section */ 28 | 29 | /* Begin PBXFrameworksBuildPhase section */ 30 | B780945826004E8C00AEB870 /* Frameworks */ = { 31 | isa = PBXFrameworksBuildPhase; 32 | buildActionMask = 2147483647; 33 | files = ( 34 | B7BBE58A2609B4160087E461 /* CodeViewer in Frameworks */, 35 | ); 36 | runOnlyForDeploymentPostprocessing = 0; 37 | }; 38 | /* End PBXFrameworksBuildPhase section */ 39 | 40 | /* Begin PBXGroup section */ 41 | B780945226004E8C00AEB870 = { 42 | isa = PBXGroup; 43 | children = ( 44 | B780945D26004E8C00AEB870 /* Patchman */, 45 | B780945C26004E8C00AEB870 /* Products */, 46 | ); 47 | sourceTree = ""; 48 | }; 49 | B780945C26004E8C00AEB870 /* Products */ = { 50 | isa = PBXGroup; 51 | children = ( 52 | B780945B26004E8C00AEB870 /* Patchman.app */, 53 | ); 54 | name = Products; 55 | sourceTree = ""; 56 | }; 57 | B780945D26004E8C00AEB870 /* Patchman */ = { 58 | isa = PBXGroup; 59 | children = ( 60 | B780945E26004E8C00AEB870 /* PatchmanApp.swift */, 61 | B780946026004E8C00AEB870 /* ContentView.swift */, 62 | B780946226004E8D00AEB870 /* Assets.xcassets */, 63 | B780946726004E8D00AEB870 /* Info.plist */, 64 | B780946826004E8D00AEB870 /* Patchman.entitlements */, 65 | B780946426004E8D00AEB870 /* Preview Content */, 66 | B780946F26004EA200AEB870 /* Patchman.swift */, 67 | ); 68 | path = Patchman; 69 | sourceTree = ""; 70 | }; 71 | B780946426004E8D00AEB870 /* Preview Content */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | B780946526004E8D00AEB870 /* Preview Assets.xcassets */, 75 | ); 76 | path = "Preview Content"; 77 | sourceTree = ""; 78 | }; 79 | /* End PBXGroup section */ 80 | 81 | /* Begin PBXNativeTarget section */ 82 | B780945A26004E8C00AEB870 /* Patchman */ = { 83 | isa = PBXNativeTarget; 84 | buildConfigurationList = B780946B26004E8D00AEB870 /* Build configuration list for PBXNativeTarget "Patchman" */; 85 | buildPhases = ( 86 | B780945726004E8C00AEB870 /* Sources */, 87 | B780945826004E8C00AEB870 /* Frameworks */, 88 | B780945926004E8C00AEB870 /* Resources */, 89 | ); 90 | buildRules = ( 91 | ); 92 | dependencies = ( 93 | ); 94 | name = Patchman; 95 | packageProductDependencies = ( 96 | B7BBE5892609B4160087E461 /* CodeViewer */, 97 | ); 98 | productName = Patchman; 99 | productReference = B780945B26004E8C00AEB870 /* Patchman.app */; 100 | productType = "com.apple.product-type.application"; 101 | }; 102 | /* End PBXNativeTarget section */ 103 | 104 | /* Begin PBXProject section */ 105 | B780945326004E8C00AEB870 /* Project object */ = { 106 | isa = PBXProject; 107 | attributes = { 108 | LastSwiftUpdateCheck = 1230; 109 | LastUpgradeCheck = 1230; 110 | TargetAttributes = { 111 | B780945A26004E8C00AEB870 = { 112 | CreatedOnToolsVersion = 12.3; 113 | }; 114 | }; 115 | }; 116 | buildConfigurationList = B780945626004E8C00AEB870 /* Build configuration list for PBXProject "Patchman" */; 117 | compatibilityVersion = "Xcode 9.3"; 118 | developmentRegion = en; 119 | hasScannedForEncodings = 0; 120 | knownRegions = ( 121 | en, 122 | Base, 123 | ); 124 | mainGroup = B780945226004E8C00AEB870; 125 | packageReferences = ( 126 | B7BBE5882609B4160087E461 /* XCRemoteSwiftPackageReference "CodeViewer" */, 127 | ); 128 | productRefGroup = B780945C26004E8C00AEB870 /* Products */; 129 | projectDirPath = ""; 130 | projectRoot = ""; 131 | targets = ( 132 | B780945A26004E8C00AEB870 /* Patchman */, 133 | ); 134 | }; 135 | /* End PBXProject section */ 136 | 137 | /* Begin PBXResourcesBuildPhase section */ 138 | B780945926004E8C00AEB870 /* Resources */ = { 139 | isa = PBXResourcesBuildPhase; 140 | buildActionMask = 2147483647; 141 | files = ( 142 | B780946626004E8D00AEB870 /* Preview Assets.xcassets in Resources */, 143 | B780946326004E8D00AEB870 /* Assets.xcassets in Resources */, 144 | ); 145 | runOnlyForDeploymentPostprocessing = 0; 146 | }; 147 | /* End PBXResourcesBuildPhase section */ 148 | 149 | /* Begin PBXSourcesBuildPhase section */ 150 | B780945726004E8C00AEB870 /* Sources */ = { 151 | isa = PBXSourcesBuildPhase; 152 | buildActionMask = 2147483647; 153 | files = ( 154 | B780947026004EA200AEB870 /* Patchman.swift in Sources */, 155 | B780946126004E8C00AEB870 /* ContentView.swift in Sources */, 156 | B780945F26004E8C00AEB870 /* PatchmanApp.swift in Sources */, 157 | ); 158 | runOnlyForDeploymentPostprocessing = 0; 159 | }; 160 | /* End PBXSourcesBuildPhase section */ 161 | 162 | /* Begin XCBuildConfiguration section */ 163 | B780946926004E8D00AEB870 /* Debug */ = { 164 | isa = XCBuildConfiguration; 165 | buildSettings = { 166 | ALWAYS_SEARCH_USER_PATHS = NO; 167 | CLANG_ANALYZER_NONNULL = YES; 168 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 169 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 170 | CLANG_CXX_LIBRARY = "libc++"; 171 | CLANG_ENABLE_MODULES = YES; 172 | CLANG_ENABLE_OBJC_ARC = YES; 173 | CLANG_ENABLE_OBJC_WEAK = YES; 174 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 175 | CLANG_WARN_BOOL_CONVERSION = YES; 176 | CLANG_WARN_COMMA = YES; 177 | CLANG_WARN_CONSTANT_CONVERSION = YES; 178 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 179 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 180 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 181 | CLANG_WARN_EMPTY_BODY = YES; 182 | CLANG_WARN_ENUM_CONVERSION = YES; 183 | CLANG_WARN_INFINITE_RECURSION = YES; 184 | CLANG_WARN_INT_CONVERSION = YES; 185 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 186 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 187 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 188 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 189 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 190 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 191 | CLANG_WARN_STRICT_PROTOTYPES = YES; 192 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 193 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 194 | CLANG_WARN_UNREACHABLE_CODE = YES; 195 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 196 | COPY_PHASE_STRIP = NO; 197 | DEBUG_INFORMATION_FORMAT = dwarf; 198 | ENABLE_STRICT_OBJC_MSGSEND = YES; 199 | ENABLE_TESTABILITY = YES; 200 | GCC_C_LANGUAGE_STANDARD = gnu11; 201 | GCC_DYNAMIC_NO_PIC = NO; 202 | GCC_NO_COMMON_BLOCKS = YES; 203 | GCC_OPTIMIZATION_LEVEL = 0; 204 | GCC_PREPROCESSOR_DEFINITIONS = ( 205 | "DEBUG=1", 206 | "$(inherited)", 207 | ); 208 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 209 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 210 | GCC_WARN_UNDECLARED_SELECTOR = YES; 211 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 212 | GCC_WARN_UNUSED_FUNCTION = YES; 213 | GCC_WARN_UNUSED_VARIABLE = YES; 214 | MACOSX_DEPLOYMENT_TARGET = 11.1; 215 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 216 | MTL_FAST_MATH = YES; 217 | ONLY_ACTIVE_ARCH = YES; 218 | SDKROOT = macosx; 219 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 220 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 221 | }; 222 | name = Debug; 223 | }; 224 | B780946A26004E8D00AEB870 /* Release */ = { 225 | isa = XCBuildConfiguration; 226 | buildSettings = { 227 | ALWAYS_SEARCH_USER_PATHS = NO; 228 | CLANG_ANALYZER_NONNULL = YES; 229 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 230 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 231 | CLANG_CXX_LIBRARY = "libc++"; 232 | CLANG_ENABLE_MODULES = YES; 233 | CLANG_ENABLE_OBJC_ARC = YES; 234 | CLANG_ENABLE_OBJC_WEAK = YES; 235 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 236 | CLANG_WARN_BOOL_CONVERSION = YES; 237 | CLANG_WARN_COMMA = YES; 238 | CLANG_WARN_CONSTANT_CONVERSION = YES; 239 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 240 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 241 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 242 | CLANG_WARN_EMPTY_BODY = YES; 243 | CLANG_WARN_ENUM_CONVERSION = YES; 244 | CLANG_WARN_INFINITE_RECURSION = YES; 245 | CLANG_WARN_INT_CONVERSION = YES; 246 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 247 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 248 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 249 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 250 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 251 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 252 | CLANG_WARN_STRICT_PROTOTYPES = YES; 253 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 254 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 255 | CLANG_WARN_UNREACHABLE_CODE = YES; 256 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 257 | COPY_PHASE_STRIP = NO; 258 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 259 | ENABLE_NS_ASSERTIONS = NO; 260 | ENABLE_STRICT_OBJC_MSGSEND = YES; 261 | GCC_C_LANGUAGE_STANDARD = gnu11; 262 | GCC_NO_COMMON_BLOCKS = YES; 263 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 264 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 265 | GCC_WARN_UNDECLARED_SELECTOR = YES; 266 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 267 | GCC_WARN_UNUSED_FUNCTION = YES; 268 | GCC_WARN_UNUSED_VARIABLE = YES; 269 | MACOSX_DEPLOYMENT_TARGET = 11.1; 270 | MTL_ENABLE_DEBUG_INFO = NO; 271 | MTL_FAST_MATH = YES; 272 | SDKROOT = macosx; 273 | SWIFT_COMPILATION_MODE = wholemodule; 274 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 275 | }; 276 | name = Release; 277 | }; 278 | B780946C26004E8D00AEB870 /* Debug */ = { 279 | isa = XCBuildConfiguration; 280 | buildSettings = { 281 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 282 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 283 | CODE_SIGN_ENTITLEMENTS = Patchman/Patchman.entitlements; 284 | CODE_SIGN_STYLE = Automatic; 285 | COMBINE_HIDPI_IMAGES = YES; 286 | DEVELOPMENT_ASSET_PATHS = "\"Patchman/Preview Content\""; 287 | DEVELOPMENT_TEAM = TR3K2EP48P; 288 | ENABLE_HARDENED_RUNTIME = YES; 289 | ENABLE_PREVIEWS = YES; 290 | INFOPLIST_FILE = Patchman/Info.plist; 291 | LD_RUNPATH_SEARCH_PATHS = ( 292 | "$(inherited)", 293 | "@executable_path/../Frameworks", 294 | ); 295 | MACOSX_DEPLOYMENT_TARGET = 11.0; 296 | PRODUCT_BUNDLE_IDENTIFIER = com.luby.Patchman; 297 | PRODUCT_NAME = "$(TARGET_NAME)"; 298 | SWIFT_VERSION = 5.0; 299 | }; 300 | name = Debug; 301 | }; 302 | B780946D26004E8D00AEB870 /* Release */ = { 303 | isa = XCBuildConfiguration; 304 | buildSettings = { 305 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 306 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 307 | CODE_SIGN_ENTITLEMENTS = Patchman/Patchman.entitlements; 308 | CODE_SIGN_STYLE = Automatic; 309 | COMBINE_HIDPI_IMAGES = YES; 310 | DEVELOPMENT_ASSET_PATHS = "\"Patchman/Preview Content\""; 311 | DEVELOPMENT_TEAM = TR3K2EP48P; 312 | ENABLE_HARDENED_RUNTIME = YES; 313 | ENABLE_PREVIEWS = YES; 314 | INFOPLIST_FILE = Patchman/Info.plist; 315 | LD_RUNPATH_SEARCH_PATHS = ( 316 | "$(inherited)", 317 | "@executable_path/../Frameworks", 318 | ); 319 | MACOSX_DEPLOYMENT_TARGET = 11.0; 320 | PRODUCT_BUNDLE_IDENTIFIER = com.luby.Patchman; 321 | PRODUCT_NAME = "$(TARGET_NAME)"; 322 | SWIFT_VERSION = 5.0; 323 | }; 324 | name = Release; 325 | }; 326 | /* End XCBuildConfiguration section */ 327 | 328 | /* Begin XCConfigurationList section */ 329 | B780945626004E8C00AEB870 /* Build configuration list for PBXProject "Patchman" */ = { 330 | isa = XCConfigurationList; 331 | buildConfigurations = ( 332 | B780946926004E8D00AEB870 /* Debug */, 333 | B780946A26004E8D00AEB870 /* Release */, 334 | ); 335 | defaultConfigurationIsVisible = 0; 336 | defaultConfigurationName = Release; 337 | }; 338 | B780946B26004E8D00AEB870 /* Build configuration list for PBXNativeTarget "Patchman" */ = { 339 | isa = XCConfigurationList; 340 | buildConfigurations = ( 341 | B780946C26004E8D00AEB870 /* Debug */, 342 | B780946D26004E8D00AEB870 /* Release */, 343 | ); 344 | defaultConfigurationIsVisible = 0; 345 | defaultConfigurationName = Release; 346 | }; 347 | /* End XCConfigurationList section */ 348 | 349 | /* Begin XCRemoteSwiftPackageReference section */ 350 | B7BBE5882609B4160087E461 /* XCRemoteSwiftPackageReference "CodeViewer" */ = { 351 | isa = XCRemoteSwiftPackageReference; 352 | repositoryURL = "https://github.com/dwarvesf/CodeViewer"; 353 | requirement = { 354 | kind = upToNextMajorVersion; 355 | minimumVersion = 1.2.4; 356 | }; 357 | }; 358 | /* End XCRemoteSwiftPackageReference section */ 359 | 360 | /* Begin XCSwiftPackageProductDependency section */ 361 | B7BBE5892609B4160087E461 /* CodeViewer */ = { 362 | isa = XCSwiftPackageProductDependency; 363 | package = B7BBE5882609B4160087E461 /* XCRemoteSwiftPackageReference "CodeViewer" */; 364 | productName = CodeViewer; 365 | }; 366 | /* End XCSwiftPackageProductDependency section */ 367 | }; 368 | rootObject = B780945326004E8C00AEB870 /* Project object */; 369 | } 370 | -------------------------------------------------------------------------------- /Patchman.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Patchman.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Patchman.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CodeViewer", 6 | "repositoryURL": "https://github.com/dwarvesf/CodeViewer", 7 | "state": { 8 | "branch": null, 9 | "revision": "44bd04af81046ce65a4e38dc97a9dd3d387f069e", 10 | "version": "1.2.4" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Patchman.xcodeproj/project.xcworkspace/xcuserdata/praneets.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PraneetNeuro/Patchman/ba8b610beab4967fe16967b6f1e0d2453c6f662a/Patchman.xcodeproj/project.xcworkspace/xcuserdata/praneets.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Patchman.xcodeproj/xcuserdata/praneets.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /Patchman.xcodeproj/xcuserdata/praneets.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Patchman.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Patchman/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Patchman/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Patchman/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Patchman/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Patchman 4 | // 5 | // Created by Praneet S on 16/03/21. 6 | // 7 | 8 | import SwiftUI 9 | import CodeViewer 10 | 11 | struct LogoView: View { 12 | var body: some View { 13 | HStack { 14 | Label("Patchman", systemImage: "envelope.open.fill") 15 | .font(.title) 16 | .padding(.top) 17 | Spacer() 18 | }.padding(.horizontal) 19 | } 20 | } 21 | 22 | struct JSONEditorOptionsView: View { 23 | @Binding var isHeaderAsJson: Bool 24 | @Binding var isHeaderFieldsEnabled: Bool 25 | @Binding var headerKey: String 26 | @Binding var headerValue: String 27 | @Binding var headers: [String : String] 28 | @Binding var isBulkRequest: Bool 29 | @Binding var isBodyAsJson: Bool 30 | @Binding var method: Int 31 | @Binding var bodyKey: String 32 | @Binding var bodyValue: String 33 | @Binding var requestBody: [String : Any] 34 | @Binding var editorFor: Int 35 | @Binding var headersJson: String 36 | @Binding var bodyJson: String 37 | var body: some View { 38 | if !isHeaderAsJson { 39 | HeadersEnabledView(isHeaderFieldsEnabled: $isHeaderFieldsEnabled, headerKey: $headerKey, headerValue: $headerValue, headers: $headers, headersJson: $headersJson) 40 | } 41 | if !isBulkRequest && !isBodyAsJson { 42 | BodyFillerView(method: $method, bodyKey: $bodyKey, bodyValue: $bodyValue, requestBody: $requestBody, requestBodyJson: $bodyJson) 43 | } 44 | 45 | if isHeaderFieldsEnabled || !isBulkRequest { 46 | VStack(alignment: .leading){ 47 | Text("JSON Editor") 48 | .bold() 49 | .font(.subheadline) 50 | HStack { 51 | if isHeaderFieldsEnabled { 52 | Toggle("Headers", isOn: $isHeaderAsJson) 53 | .onChange(of: isHeaderAsJson) { value in 54 | if value { 55 | editorFor = 1 56 | } 57 | } 58 | } 59 | if !isBulkRequest { 60 | Toggle("Request Body", isOn: $isBodyAsJson) 61 | .onChange(of: isBodyAsJson) { value in 62 | if value { 63 | editorFor = 2 64 | } 65 | } 66 | } 67 | Spacer() 68 | }.padding(.leading) 69 | }.padding(.top) 70 | } 71 | } 72 | } 73 | 74 | struct ActionBarView: View { 75 | 76 | @Binding var url: String 77 | @Binding var methods: [String] 78 | @Binding var method: Int 79 | @Binding var isProcessing: Bool 80 | @Binding var isBulkRequest: Bool 81 | @Binding var response: String 82 | @Binding var bulkResponsesStatusCodes: [Int] 83 | @Binding var bulkRequestBody: [[String : Any]] 84 | @Binding var headers: [String : String] 85 | @Binding var requestBody: [String : Any] 86 | @Binding var responseStatus: HTTPURLResponse? 87 | @Binding var cachePolicy: String 88 | 89 | func execute(method: RequestMethod) { 90 | isProcessing = true 91 | var cachePolicySelected: URLRequest.CachePolicy { 92 | switch cachePolicy { 93 | case cachePolicies.reloadIgnoringCacheData.rawValue: 94 | return .reloadIgnoringCacheData 95 | case cachePolicies.reloadIgnoringLocalAndRemoteCacheData.rawValue: 96 | return .reloadIgnoringLocalAndRemoteCacheData 97 | case cachePolicies.reloadIgnoringLocalCacheData.rawValue: 98 | return .reloadIgnoringLocalCacheData 99 | case cachePolicies.reloadRevalidatingCacheData.rawValue: 100 | return .reloadRevalidatingCacheData 101 | case cachePolicies.returnCacheDataDontLoad.rawValue: 102 | return .returnCacheDataDontLoad 103 | case cachePolicies.returnCacheDataElseLoad.rawValue: 104 | return .returnCacheDataElseLoad 105 | default: 106 | return .useProtocolCachePolicy 107 | } 108 | } 109 | if isBulkRequest { 110 | DispatchQueue.global().async { 111 | response = "" 112 | bulkResponsesStatusCodes = [] 113 | for reqBody in bulkRequestBody { 114 | let req: Request = Request(url: url, method: method, cachingPolicy: cachePolicySelected, requestBody: reqBody, requestHeaders: headers) 115 | let res = req.executeRequest() 116 | responseStatus = res.responseStatus 117 | response += "\t\t---START OF RESPONSE---\n" + (res.response.prettyPrintedJSONString ?? "") + "\n\t\t---END OF RESPONSE---\n\n" 118 | bulkResponsesStatusCodes.append(res.responseStatus?.statusCode ?? -1) 119 | } 120 | } 121 | } else { 122 | let req: Request = Request(url: url, method: method, requestBody: requestBody, requestHeaders: headers) 123 | let res = req.executeRequest() 124 | responseStatus = res.responseStatus 125 | response = res.response.prettyPrintedJSONString ?? "" 126 | isProcessing = false 127 | } 128 | } 129 | 130 | var body: some View { 131 | HStack { 132 | TextField("URL", text: $url) 133 | .textFieldStyle(RoundedBorderTextFieldStyle()) 134 | Picker("Method", selection: $method) { 135 | ForEach(0.. Bool { 176 | return statusCode >= 200 && statusCode < 300 177 | } 178 | 179 | var body: some View { 180 | HStack { 181 | ScrollView { 182 | VStack(alignment: .leading) { 183 | ForEach(bulkResponses, id: \.self) { response in 184 | Text("Status: \(response)") 185 | .bold() 186 | .background( 187 | Rectangle() 188 | .frame(width: 90, height: 30) 189 | .foregroundColor(isStatusOkay(statusCode: response) ? green : red ) 190 | .border(isStatusOkay(statusCode: response) ? Color.green : Color.red, width: 3) 191 | .cornerRadius(6)) 192 | .padding() 193 | } 194 | }.padding() 195 | }.onChange(of: bulkResponses, perform: { _ in 196 | isProcessing = bulkResponses.count != bulkRequestBody.count 197 | print(isProcessing) 198 | }) 199 | Spacer() 200 | }.padding() 201 | } 202 | } 203 | 204 | struct FieldsToggleView: View { 205 | @Binding var isParamsEnabled: Bool 206 | @Binding var isHeaderFieldsEnabled: Bool 207 | @Binding var isBulkRequest: Bool 208 | @Binding var bulkRequestBody: [[String : Any]] 209 | @Binding var cachePolicy: String 210 | @State var cachePoliciesList: [String] = cachePolicies.allCases.map({ $0.rawValue }) 211 | 212 | func pickFile() -> String? { 213 | let dialog = NSOpenPanel(); 214 | 215 | dialog.title = "Choose a CSV file"; 216 | dialog.showsResizeIndicator = true; 217 | dialog.showsHiddenFiles = false; 218 | dialog.allowsMultipleSelection = false; 219 | dialog.canChooseDirectories = false; 220 | dialog.allowedFileTypes = ["csv"]; 221 | 222 | if (dialog.runModal() == NSApplication.ModalResponse.OK) { 223 | let result = dialog.url 224 | if (result != nil) { 225 | let path: String = result!.path 226 | return path 227 | } 228 | } else { 229 | return nil 230 | } 231 | return nil 232 | } 233 | 234 | var body: some View { 235 | HStack { 236 | Toggle("Query Params", isOn: $isParamsEnabled) 237 | Toggle("Header Fields", isOn: $isHeaderFieldsEnabled) 238 | Toggle("Bulk requests", isOn: $isBulkRequest) 239 | Picker("Cache policy", selection: $cachePolicy) { 240 | ForEach(cachePoliciesList, id: \.self){ policy in 241 | Text(policy) 242 | } 243 | } 244 | if isBulkRequest { 245 | Button(action: { 246 | guard let bulkRequestBodyFilePath: String = pickFile() else { return } 247 | do { 248 | let bulkRequestBodyContents = try String(contentsOf: URL(fileURLWithPath: bulkRequestBodyFilePath)) 249 | var bulkRequestBodyCSVs = bulkRequestBodyContents.split(separator: "\n").map({ String($0) }) 250 | let keys = bulkRequestBodyCSVs.first!.split(separator: ",") 251 | bulkRequestBodyCSVs = bulkRequestBodyCSVs.dropFirst().map({ String($0) }) 252 | for field in bulkRequestBodyCSVs { 253 | 254 | let fieldDecoded = field.split(separator: ",") 255 | if fieldDecoded.count == keys.count { 256 | var bodyField: [String : Any] = [:] 257 | for index in 0.. 0 { 341 | HStack{ 342 | Label("Response", systemImage: "icloud.and.arrow.down") 343 | .font(.title2) 344 | Toggle("View response headers", isOn: $isResponseHeadersShown) 345 | if isBulkRequest { 346 | Toggle("View bulk response statuses", isOn: $isBulkResponseStatusesShown) 347 | } 348 | Spacer() 349 | if !isBulkResponseStatusesShown { 350 | Text("Status: \(responseStatus?.statusCode ?? -1)") 351 | .foregroundColor(responseStatus?.statusCode ?? -1 >= 200 && responseStatus?.statusCode ?? -1 < 300 ? .green : .orange) 352 | } 353 | }.padding() 354 | } 355 | } 356 | } 357 | 358 | struct BodyFillerView: View { 359 | @Binding var method: Int 360 | @Binding var bodyKey: String 361 | @Binding var bodyValue: String 362 | @Binding var requestBody: [String : Any] 363 | @Binding var requestBodyJson: String 364 | var body: some View { 365 | HStack { 366 | TextField("Request body key", text: $bodyKey) 367 | .frame(width: 130) 368 | .textFieldStyle(RoundedBorderTextFieldStyle()) 369 | TextField("Value", text: $bodyValue) 370 | .frame(width: 130) 371 | .textFieldStyle(RoundedBorderTextFieldStyle()) 372 | Button("Add", action: { 373 | requestBody[bodyKey] = bodyValue 374 | do { 375 | let jsonData = try JSONSerialization.data(withJSONObject: requestBody, options: .prettyPrinted) 376 | requestBodyJson = String(data: jsonData, encoding: .utf8) ?? "" 377 | } catch {} 378 | }) 379 | Spacer() 380 | }.padding([.horizontal, .top]) 381 | } 382 | } 383 | 384 | struct ProfileView: View { 385 | var profile: Profile 386 | var body: some View { 387 | VStack(alignment: .leading) { 388 | HStack { 389 | Text(profile.profileName) 390 | .bold() 391 | .lineLimit(2) 392 | .padding(.leading, 4) 393 | Spacer() 394 | } 395 | } 396 | .frame(width: 165, height: 35) 397 | .background(Rectangle().frame(width: 165, height: 35).foregroundColor(.gray).opacity(0.45).cornerRadius(6)) 398 | } 399 | } 400 | 401 | struct PresetView: View { 402 | var preset: Preset 403 | var body: some View { 404 | VStack(alignment: .leading) { 405 | HStack { 406 | Text(preset.presetType == 0 ? "Header Field" : "Body Field") 407 | .font(.subheadline) 408 | .bold() 409 | .padding(.leading, 4) 410 | Spacer() 411 | } 412 | HStack { 413 | Text(preset.presetName) 414 | .bold() 415 | .lineLimit(2) 416 | .padding(.leading, 4) 417 | Spacer() 418 | } 419 | } 420 | .frame(width: 165, height: 55) 421 | .background(Rectangle().frame(width: 165, height: 55).foregroundColor(.gray).opacity(0.45).cornerRadius(6)) 422 | } 423 | } 424 | 425 | struct ContentView: View { 426 | 427 | @State var dragOver: Bool = false 428 | 429 | @ObservedObject var defaults: Defaults = Defaults.shared 430 | 431 | @State var url:String = "" 432 | @State var response: String = "" 433 | @State var methods: [String] = RequestMethod.allCases.map({ $0.rawValue }) 434 | @State var requestBody: [String : Any] = [:] 435 | @State var headers: [String : String] = [:] 436 | @State var method: Int = 0 437 | @State var isParamsEnabled: Bool = false 438 | @State var isHeaderFieldsEnabled: Bool = false 439 | @State var headerKey: String = "" 440 | @State var headerValue: String = "" 441 | @State var key: String = "" 442 | @State var value: String = "" 443 | @State var bodyKey: String = "" 444 | @State var bodyValue: String = "" 445 | @State var view: Int = 0 446 | @State var responseStatus: HTTPURLResponse? 447 | @State var isResponseHeadersShown: Bool = false 448 | @State var isBulkResponseStatusesShown: Bool = false 449 | @State var bulkResponsesStatusCodes: [Int] = [] 450 | @State var isProcessing: Bool = false 451 | @State var presetType: Int = 0 452 | @State var presetKey: String = "" 453 | @State var presetValue: String = "" 454 | @State var presetName: String = "" 455 | @State var isPresetAddShown: Bool = false 456 | @State var bulkRequestBody: [[String : Any]] = [] 457 | @State var isBulkRequest: Bool = false 458 | @State var profileName: String = "" 459 | @State var cachePolicy: String = cachePolicies.useProtocolCachePolicy.rawValue 460 | @State var isHeaderAsJson: Bool = false 461 | @State var isBodyAsJson: Bool = false 462 | @State var headerJson: String = "" 463 | @State var bodyJson: String = "" 464 | @State var editorFor: Int = -1 465 | 466 | func getResponseHeaders(response: String) -> String { 467 | let regex = try! NSRegularExpression(pattern: "Optional\\(", options: NSRegularExpression.Options.caseInsensitive) 468 | let range = NSMakeRange(0, response.count) 469 | return regex.stringByReplacingMatches(in: response, options: [], range: range, withTemplate: "") 470 | } 471 | 472 | func saveProfile() { 473 | let p = Profile(profileName: profileName, method: method, url: url, headers: headers, requestBody: requestBody as? [String : JSONValue] ?? [:], isHeadersEnabled: isHeaderFieldsEnabled, isBulkRequest: isBulkRequest, bulkRequestBody: bulkRequestBody as? [[String : JSONValue]] ?? [[:]]) 474 | p.save() 475 | defaults.profiles.append(p) 476 | } 477 | 478 | var body: some View { 479 | return HStack { 480 | VStack { 481 | List { 482 | Text("Presets") 483 | .bold() 484 | .font(.title2) 485 | Toggle("Add preset", isOn: $isPresetAddShown) 486 | if isPresetAddShown { 487 | Picker("", selection: $presetType){ 488 | Text("Header Field").tag(0) 489 | Text("Body").tag(1) 490 | }.pickerStyle(SegmentedPickerStyle()) 491 | Text("Preset Name") 492 | TextEditor(text: $presetName) 493 | .cornerRadius(6) 494 | Text("Key") 495 | TextEditor(text: $presetKey) 496 | .cornerRadius(6) 497 | Text("Value") 498 | TextEditor(text: $presetValue) 499 | .cornerRadius(6) 500 | Button("Add preset"){ 501 | if presetKey.count > 0 && presetValue.count > 0 && presetName.count > 0 { 502 | let preset = Preset(presetType: presetType, key: presetKey, value: presetValue, presetName: presetName) 503 | preset.save() 504 | defaults.presets.append(preset) 505 | } 506 | }.padding(.bottom) 507 | } 508 | ForEach(defaults.presets, id: \.key){ preset in 509 | PresetView(preset: preset) 510 | .onTapGesture { 511 | switch preset.presetType { 512 | case 0: 513 | headers[preset.key] = preset.value 514 | break 515 | case 1: 516 | requestBody[preset.key] = preset.value 517 | break 518 | default: 519 | break 520 | } 521 | } 522 | } 523 | Text(defaults.profiles.count > 0 ? "Profiles" : "") 524 | .bold() 525 | .font(.title2) 526 | .padding(.top) 527 | ForEach(defaults.profiles, id: \.profileName){ profile in 528 | ProfileView(profile: profile) 529 | .onTapGesture { 530 | url = profile.url 531 | method = profile.method 532 | headers = profile.headers 533 | requestBody = profile.requestBody 534 | isHeaderFieldsEnabled = profile.isHeadersEnabled 535 | isBulkRequest = profile.isBulkRequest 536 | bulkRequestBody = profile.bulkRequestBody 537 | do { 538 | let jsonHeaderData = try JSONSerialization.data(withJSONObject: headers, options: .prettyPrinted) 539 | headerJson = String(data: jsonHeaderData, encoding: .utf8) ?? "" 540 | let jsonBodyData = try JSONSerialization.data(withJSONObject: requestBody, options: .prettyPrinted) 541 | bodyJson = String(data: jsonBodyData, encoding: .utf8) ?? "" 542 | } catch {} 543 | } 544 | .contextMenu(menuItems: { 545 | Button("Save on disk", action: { 546 | profile.saveOnDisk() 547 | }) 548 | }) 549 | } 550 | }.frame(minWidth: 200, maxWidth: 200) 551 | }.listStyle(SidebarListStyle()) 552 | 553 | VStack { 554 | HStack{ 555 | LogoView() 556 | Spacer() 557 | TextField("Profile name", text: $profileName) 558 | .textFieldStyle(RoundedBorderTextFieldStyle()) 559 | .frame(width: 145) 560 | Button(action: { 561 | if profileName.count > 0 { 562 | saveProfile() 563 | } 564 | }, label: { 565 | Image(systemName: "square.and.arrow.down") 566 | }).padding() 567 | } 568 | 569 | ActionBarView(url: $url, methods: $methods, method: $method, isProcessing: $isProcessing, isBulkRequest: $isBulkRequest, response: $response, bulkResponsesStatusCodes: $bulkResponsesStatusCodes, bulkRequestBody: $bulkRequestBody, headers: $headers, requestBody: $requestBody, responseStatus: $responseStatus, cachePolicy: $cachePolicy) 570 | 571 | FieldsToggleView(isParamsEnabled: $isParamsEnabled, isHeaderFieldsEnabled: $isHeaderFieldsEnabled, isBulkRequest: $isBulkRequest, bulkRequestBody: $bulkRequestBody, cachePolicy: $cachePolicy) 572 | 573 | ParamsEnabledView(isParamsEnabled: $isParamsEnabled, key: $key, value: $value, url: $url) 574 | 575 | JSONEditorOptionsView(isHeaderAsJson: $isHeaderAsJson, isHeaderFieldsEnabled: $isHeaderFieldsEnabled, headerKey: $headerKey, headerValue: $headerValue, headers: $headers, isBulkRequest: $isBulkRequest, isBodyAsJson: $isBodyAsJson, method: $method, bodyKey: $bodyKey, bodyValue: $bodyValue, requestBody: $requestBody, editorFor: $editorFor, headersJson: $headerJson, bodyJson: $bodyJson) 576 | 577 | if isHeaderAsJson && isBodyAsJson { 578 | Picker(selection: $editorFor, label: Text("")) { 579 | Text("Headers").tag(1) 580 | Text("Request Body").tag(2) 581 | }.pickerStyle(SegmentedPickerStyle()) 582 | .padding(.horizontal) 583 | } 584 | 585 | if editorFor == 1 && isHeaderAsJson { 586 | CodeViewer( 587 | content: $headerJson, 588 | textDidChanged: { json in 589 | if let data = json.data(using: .utf8) { 590 | do { 591 | if let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String:String] { 592 | headers = json 593 | } 594 | } catch { 595 | print("Something went wrong") 596 | } 597 | } 598 | } 599 | ) 600 | .cornerRadius(6) 601 | .padding(.horizontal) 602 | } else if editorFor == 2 && isBodyAsJson { 603 | CodeViewer( 604 | content: $bodyJson, 605 | textDidChanged: { json in 606 | if let data = json.data(using: .utf8) { 607 | do { 608 | if let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String:Any]{ 609 | requestBody = json 610 | } 611 | } catch { 612 | print("Something went wrong") 613 | } 614 | } 615 | } 616 | ) 617 | .cornerRadius(6) 618 | .padding(.horizontal) 619 | } 620 | 621 | ResponseHeaderAndOptionsView(response: $response, responseStatus: $responseStatus, isResponseHeadersShown: $isResponseHeadersShown, isBulkResponseStatusesShown: $isBulkResponseStatusesShown, isBulkRequest: $isBulkRequest) 622 | 623 | if isBulkResponseStatusesShown { 624 | BulkStatusListView(bulkResponses: $bulkResponsesStatusCodes, isProcessing: $isProcessing, bulkRequestBody: $bulkRequestBody) 625 | Text("\(bulkResponsesStatusCodes.filter({ $0 >= 200 && $0 < 300 }).count) / \(bulkRequestBody.count) requests successful") 626 | .bold() 627 | .padding() 628 | } else { 629 | ScrollView { 630 | HStack { 631 | Text(isResponseHeadersShown ? getResponseHeaders(response: "\(responseStatus)") : response) 632 | .lineLimit(nil) 633 | .onChange(of: bulkResponsesStatusCodes, perform: { _ in 634 | if bulkResponsesStatusCodes.count == bulkRequestBody.count { 635 | isProcessing = false 636 | } 637 | }) 638 | Spacer() 639 | } 640 | }.padding() 641 | } 642 | }.frame(height: 550) 643 | .frame(minWidth: 600) 644 | VStack{ 645 | 646 | List { 647 | Text(headers.count > 0 ? "Header fields" : "") 648 | .font(.title2) 649 | .bold() 650 | ForEach(headers.keys.map({ String($0) }), id: \.self) { key in 651 | HStack { 652 | Button(action: { 653 | headers.removeValue(forKey: key) 654 | }, label: { 655 | Image(systemName: "trash") 656 | }) 657 | Text("\(key) : \(headers[key]!)") 658 | Spacer() 659 | } 660 | } 661 | .frame(width: 200) 662 | 663 | 664 | Text(requestBody.count > 0 ? "Request Body fields" : "") 665 | .font(.title2) 666 | .bold() 667 | ForEach(requestBody.keys.map({ String($0) }), id: \.self) { key in 668 | HStack { 669 | Button(action: { 670 | requestBody.removeValue(forKey: key) 671 | }, label: { 672 | Image(systemName: "trash") 673 | }) 674 | Text("\(key) : \(requestBody[key]! as? String ?? "Not representable")") 675 | Spacer() 676 | } 677 | } 678 | .frame(width: 200) 679 | } 680 | .listStyle(SidebarListStyle()) 681 | }.frame(minWidth: 200, maxWidth: 200) 682 | }.onDrop(of: ["public.file-url"], isTargeted: $dragOver) { providers -> Bool in 683 | providers.first?.loadDataRepresentation(forTypeIdentifier: "public.file-url", completionHandler: { (data, error) in 684 | if let data = data, let path = NSString(data: data, encoding: 4), let url = URL(string: path as String) { 685 | guard let data: Data = try? Data(contentsOf: url) else { return } 686 | guard let profile: Profile = try? JSONDecoder().decode(Profile.self, from: data) else { return } 687 | DispatchQueue.main.async { 688 | self.profileName = profile.profileName 689 | self.method = profile.method 690 | self.url = profile.url 691 | self.headers = profile.headers 692 | self.requestBody = profile.requestBody 693 | self.isHeaderFieldsEnabled = profile.isHeadersEnabled 694 | self.isBulkRequest = profile.isBulkRequest 695 | self.bulkRequestBody = profile.bulkRequestBody 696 | } 697 | } 698 | }) 699 | return true 700 | } 701 | } 702 | } 703 | 704 | extension Data { 705 | var prettyPrintedJSONString: String? { 706 | guard let object = try? JSONSerialization.jsonObject(with: self, options: []), 707 | let data = try? JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted]), 708 | let prettyPrintedString = String(data: data, encoding: .utf8) else { return nil } 709 | 710 | return prettyPrintedString 711 | } 712 | } 713 | -------------------------------------------------------------------------------- /Patchman/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 2.0.1 19 | CFBundleVersion 20 | 1 21 | LSMinimumSystemVersion 22 | $(MACOSX_DEPLOYMENT_TARGET) 23 | UTExportedTypeDeclarations 24 | 25 | 26 | UTTypeDescription 27 | 28 | UTTypeIcons 29 | 30 | UTTypeIconText 31 | 32 | 33 | UTTypeIdentifier 34 | 35 | UTTypeTagSpecification 36 | 37 | public.filename-extension 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /Patchman/Patchman.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.assets.movies.read-write 8 | 9 | com.apple.security.assets.music.read-write 10 | 11 | com.apple.security.assets.pictures.read-write 12 | 13 | com.apple.security.files.downloads.read-write 14 | 15 | com.apple.security.files.user-selected.read-write 16 | 17 | com.apple.security.network.client 18 | 19 | com.apple.security.network.server 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Patchman/Patchman.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Patchman.swift 3 | // Patchman 4 | // 5 | // Created by Praneet S on 16/03/21. 6 | // 7 | 8 | import Foundation 9 | import Cocoa 10 | 11 | class Defaults : ObservableObject { 12 | private init() { 13 | profiles = retreiveDefaultProfiles() 14 | presets = retreiveDefaultPresets() 15 | } 16 | public static var shared: Defaults = Defaults() 17 | @Published var profiles: [Profile] = [] 18 | @Published var presets: [Preset] = [] 19 | } 20 | 21 | func retreiveDefaultPresets() -> [Preset] { 22 | if let presets = UserDefaults.standard.object(forKey: "presets") as? Data { 23 | let decoder = JSONDecoder() 24 | if let presetsArray = try? decoder.decode([Preset].self, from: presets) { 25 | return presetsArray 26 | } 27 | } 28 | return [] 29 | } 30 | 31 | func retreiveDefaultProfiles() -> [Profile] { 32 | if let profiles = UserDefaults.standard.object(forKey: "profiles") as? Data { 33 | let decoder = JSONDecoder() 34 | if let profilesArray = try? decoder.decode([Profile].self, from: profiles) { 35 | return profilesArray 36 | } 37 | } 38 | return [] 39 | } 40 | 41 | struct Preset: Codable { 42 | let presetType: Int 43 | let key: String 44 | let value: String 45 | let presetName: String 46 | 47 | func save() { 48 | let encoder = JSONEncoder() 49 | if let encoded = try? encoder.encode([self]) { 50 | let defaults = UserDefaults.standard 51 | if let presets = defaults.object(forKey: "presets") as? Data { 52 | let decoder = JSONDecoder() 53 | if var presetsArray = try? decoder.decode([Preset].self, from: presets) { 54 | presetsArray.append(self) 55 | Defaults.shared.presets = presetsArray 56 | if let presetsArrayObj = try? encoder.encode(presetsArray) { 57 | defaults.set(presetsArrayObj, forKey: "presets") 58 | } 59 | } 60 | } else { 61 | defaults.set(encoded, forKey: "presets") 62 | } 63 | } 64 | } 65 | } 66 | 67 | enum CodingKeys: CodingKey { 68 | case string 69 | case int 70 | case double 71 | case bool 72 | case object 73 | case array 74 | } 75 | 76 | public enum JSONValue: Codable { 77 | 78 | public func encode(to encoder: Encoder) throws { 79 | var container = encoder.container(keyedBy: CodingKeys.self) 80 | switch self { 81 | case .string(let str): 82 | try container.encode(str, forKey: .string) 83 | case .int(let int): 84 | try container.encode(int, forKey: .int) 85 | case .double(let dbl): 86 | try container.encode(dbl, forKey: .double) 87 | case .bool(let bool): 88 | try container.encode(bool, forKey: .bool) 89 | case .object(let obj): 90 | try container.encode(obj, forKey: .object) 91 | case .array(let array): 92 | try container.encode(array, forKey: .array) 93 | } 94 | } 95 | 96 | case string(String) 97 | case int(Int) 98 | case double(Double) 99 | case bool(Bool) 100 | case object([String: JSONValue]) 101 | case array([JSONValue]) 102 | 103 | public init(from decoder: Decoder) throws { 104 | let container = try decoder.singleValueContainer() 105 | if let value = try? container.decode(String.self) { 106 | self = .string(value) 107 | } else if let value = try? container.decode(Int.self) { 108 | self = .int(value) 109 | } else if let value = try? container.decode(Double.self) { 110 | self = .double(value) 111 | } else if let value = try? container.decode(Bool.self) { 112 | self = .bool(value) 113 | } else if let value = try? container.decode([String: JSONValue].self) { 114 | self = .object(value) 115 | } else if let value = try? container.decode([JSONValue].self) { 116 | self = .array(value) 117 | } else { 118 | throw DecodingError.typeMismatch(JSONValue.self, DecodingError.Context(codingPath: container.codingPath, debugDescription: "Not a JSON")) 119 | } 120 | } 121 | } 122 | 123 | struct Profile: Codable { 124 | let profileName: String 125 | let method: Int 126 | let url: String 127 | let headers: [String : String] 128 | let requestBody: [String : JSONValue] 129 | let isHeadersEnabled: Bool 130 | let isBulkRequest: Bool 131 | let bulkRequestBody: [[String : JSONValue]] 132 | 133 | func saveOnDisk() { 134 | let encodedProfile = try? JSONEncoder().encode(self) 135 | let savePanel = NSSavePanel() 136 | savePanel.canCreateDirectories = true 137 | savePanel.showsTagField = false 138 | savePanel.nameFieldStringValue = "\(self.profileName)_request.patchman" 139 | savePanel.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.modalPanelWindow))) 140 | savePanel.begin { (result) in 141 | if result.rawValue == NSApplication.ModalResponse.OK.rawValue { 142 | guard let url = savePanel.url else { 143 | return 144 | } 145 | try? encodedProfile?.write(to: url) 146 | } 147 | } 148 | } 149 | 150 | func save() { 151 | let encoder = JSONEncoder() 152 | if let encoded = try? encoder.encode([self]) { 153 | let defaults = UserDefaults.standard 154 | if let profiles = defaults.object(forKey: "profiles") as? Data { 155 | let decoder = JSONDecoder() 156 | if var profilesArray = try? decoder.decode([Profile].self, from: profiles) { 157 | profilesArray.append(self) 158 | Defaults.shared.profiles = profilesArray 159 | if let profilesArrayObj = try? encoder.encode(profilesArray) { 160 | defaults.set(profilesArrayObj, forKey: "profiles") 161 | } 162 | } 163 | } else { 164 | defaults.set(encoded, forKey: "profiles") 165 | } 166 | } 167 | } 168 | 169 | } 170 | 171 | enum RequestMethod: String, CaseIterable { 172 | case GET = "GET" 173 | case POST = "POST" 174 | case PUT = "PUT" 175 | case PATCH = "PATCH" 176 | case DELETE = "DELETE" 177 | } 178 | 179 | enum cachePolicies: String, CaseIterable { 180 | case reloadIgnoringLocalAndRemoteCacheData = "Reload ignoring local and remote cache data" 181 | case reloadIgnoringLocalCacheData = "Reload ignoring local cache data" 182 | case reloadRevalidatingCacheData = "Reload revalidating cache data" 183 | case returnCacheDataDontLoad = "Return cache data don't load" 184 | case returnCacheDataElseLoad = "Return cache data else load" 185 | case useProtocolCachePolicy = "Use protocol cache policy" 186 | case reloadIgnoringCacheData = "Reload ignoring cache data" 187 | } 188 | 189 | struct Response{ 190 | let response: Data 191 | let responseStatus: HTTPURLResponse? 192 | } 193 | 194 | class Request { 195 | public var url: URL? 196 | public var method: RequestMethod 197 | public var requestPolicy: URLRequest.CachePolicy 198 | public var requestBody: [String : Any]? 199 | public var requestHeaders: [String : String]? 200 | 201 | init(url: String, method: RequestMethod, cachingPolicy: URLRequest.CachePolicy = .useProtocolCachePolicy, requestBody: [String : Any], requestHeaders: Dictionary) { 202 | self.url = URL(string: url) 203 | self.method = method 204 | self.requestPolicy = cachingPolicy 205 | if !requestBody.isEmpty { 206 | self.requestBody = requestBody 207 | } 208 | self.requestHeaders = requestHeaders 209 | } 210 | 211 | func executeRequest() -> Response { 212 | guard let instanceURL = url else { return Response(response: "Invalid URL".data(using: .utf8)!, responseStatus: nil) } 213 | let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0) 214 | var request = URLRequest(url: instanceURL, cachePolicy: requestPolicy) 215 | request.httpMethod = self.method.rawValue 216 | if let requestBody = self.requestBody { 217 | let jsonData = try? JSONSerialization.data(withJSONObject: requestBody) 218 | request.httpBody = jsonData 219 | } 220 | request.allHTTPHeaderFields = self.requestHeaders 221 | var responseData: Data? 222 | var responseStatus: HTTPURLResponse? 223 | let task = URLSession.shared.dataTask(with: request) { (data, response, error) in 224 | if let data = data { 225 | responseData = data 226 | } else { 227 | responseData = error?.localizedDescription.data(using: .utf8) 228 | } 229 | if let response = response { 230 | responseStatus = response as? HTTPURLResponse 231 | } 232 | semaphore.signal() 233 | } 234 | task.resume() 235 | semaphore.wait() 236 | return Response(response: responseData ?? "Unknown error".data(using: .utf8)!, responseStatus: responseStatus) 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /Patchman/PatchmanApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PatchmanApp.swift 3 | // Patchman 4 | // 5 | // Created by Praneet S on 16/03/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | func openRequestProfile() -> String? { 11 | let dialog = NSOpenPanel(); 12 | 13 | dialog.title = "Choose a CSV file"; 14 | dialog.showsResizeIndicator = true; 15 | dialog.showsHiddenFiles = false; 16 | dialog.allowsMultipleSelection = false; 17 | dialog.canChooseDirectories = false; 18 | dialog.allowedFileTypes = ["patchman"]; 19 | 20 | if (dialog.runModal() == NSApplication.ModalResponse.OK) { 21 | let result = dialog.url 22 | if (result != nil) { 23 | let path: String = result!.path 24 | return path 25 | } 26 | } else { 27 | return nil 28 | } 29 | return nil 30 | } 31 | 32 | @main 33 | struct PatchmanApp: App { 34 | var body: some Scene { 35 | WindowGroup { 36 | ContentView() 37 | } 38 | .commands(content: { 39 | CommandMenu("Quickies", content: { 40 | Button("Open", action: { 41 | let requestProfilePath = openRequestProfile() 42 | guard let requestProfileURL = requestProfilePath else { 43 | return 44 | } 45 | let profile = try? JSONDecoder().decode(Profile.self, from: Data(contentsOf: URL(fileURLWithPath: requestProfileURL), options: [])) 46 | guard let profileUnwrapped = profile else { 47 | return 48 | } 49 | profileUnwrapped.save() 50 | Defaults.shared.profiles.append(profileUnwrapped) 51 | }) 52 | }) 53 | }) 54 | .windowStyle(HiddenTitleBarWindowStyle()) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Patchman/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Patchman 2 | A macOS application to test APIs with HTTP methods (Decluttered Postman), built with SwiftUI. 3 | ## Features: 4 | 1. Supports the GET, POST, PUT, PATCH, DELETE methods 5 | 2. Easy to use / adaptive UI 6 | 3. Supports bulk requests (takes in all request bodies in the form of a CSV) 7 | 4. Can save request profiles as well as presets for commonly used headers and request body fields 8 | 5. Can save / open request profiles on / from disk to share with your team! 9 | ## JSON Editor support for request body / headers 10 | ![Home](https://github.com/PraneetNeuro/Patchman/blob/main/Snaps/Screenshot%202021-04-07%20at%207.06.52%20PM.png?raw=true) 11 | ## Easy to use UI 12 | ![Home](https://github.com/PraneetNeuro/Patchman/blob/main/Snaps/Home1.png?raw=true) 13 | ## Supports bulk requests 14 | ![Home](https://github.com/PraneetNeuro/Patchman/blob/main/Snaps/Home2.png?raw=true) 15 | -------------------------------------------------------------------------------- /Snaps/Home1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PraneetNeuro/Patchman/ba8b610beab4967fe16967b6f1e0d2453c6f662a/Snaps/Home1.png -------------------------------------------------------------------------------- /Snaps/Home2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PraneetNeuro/Patchman/ba8b610beab4967fe16967b6f1e0d2453c6f662a/Snaps/Home2.png -------------------------------------------------------------------------------- /Snaps/Screenshot 2021-04-07 at 7.06.52 PM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PraneetNeuro/Patchman/ba8b610beab4967fe16967b6f1e0d2453c6f662a/Snaps/Screenshot 2021-04-07 at 7.06.52 PM.png --------------------------------------------------------------------------------