├── .gitignore ├── LICENSE ├── MacUserGenerator.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── xcschemes │ │ └── MacUserGenerator.xcscheme └── xcuserdata │ ├── nindig.xcuserdatad │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ │ └── xcschememanagement.plist │ └── ninxsoft.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── MacUserGenerator ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x.png │ ├── Certificate.imageset │ │ ├── Certificate.png │ │ ├── Certificate@2x.png │ │ └── Contents.json │ ├── Contents.json │ ├── Document.iconset │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x.png │ ├── Image.imageset │ │ └── Contents.json │ └── Picture.imageset │ │ ├── Contents.json │ │ ├── Picture.png │ │ └── Picture@2x.png ├── Base.lproj │ └── Main.storyboard ├── Document │ ├── Document.swift │ └── DocumentObject.swift ├── Export │ ├── Export.py │ ├── ExportType.swift │ ├── Exporter.swift │ ├── PackageOptions.swift │ └── ScriptOptions.swift ├── Extensions │ ├── Data+Extension.swift │ ├── NSBezierPath+Extension.swift │ ├── NSColor+Extension.swift │ ├── NSImage+Extension.swift │ └── String+Extension.swift ├── Info.plist ├── MacUserGenerator-Bridging-Header.h ├── MacUserGenerator.entitlements └── Views & Controllers │ ├── ExportTabBarView.swift │ ├── ExportViewController.swift │ ├── PictureView.swift │ └── ViewController.swift ├── README.md └── Readme Resources ├── Export.png ├── Mojave Packages.png ├── Mojave Scripts.png ├── Sample User Dark.png └── Sample User Light.png /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | xcuserdata/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2021 Nindi Gill 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /MacUserGenerator.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 353B06951F8E033E00E23802 /* DocumentObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 353B06941F8E033E00E23802 /* DocumentObject.swift */; }; 11 | 355514C51F8B830C00A44996 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 355514C41F8B830C00A44996 /* AppDelegate.swift */; }; 12 | 355514C71F8B830C00A44996 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 355514C61F8B830C00A44996 /* ViewController.swift */; }; 13 | 355514C91F8B830C00A44996 /* Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = 355514C81F8B830C00A44996 /* Document.swift */; }; 14 | 355514CB1F8B830C00A44996 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 355514CA1F8B830C00A44996 /* Assets.xcassets */; }; 15 | 355514CE1F8B830C00A44996 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 355514CC1F8B830C00A44996 /* Main.storyboard */; }; 16 | 3556DCD21FA202B400F6E1CA /* ExportTabBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3556DCD11FA202B400F6E1CA /* ExportTabBarView.swift */; }; 17 | 356A4DAE1F96A033007FAAB7 /* Exporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 356A4DAD1F96A033007FAAB7 /* Exporter.swift */; }; 18 | 356A4DB31F96A93D007FAAB7 /* ScriptOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 356A4DB21F96A93D007FAAB7 /* ScriptOptions.swift */; }; 19 | 356A4DB51F96A9AF007FAAB7 /* PackageOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 356A4DB41F96A9AF007FAAB7 /* PackageOptions.swift */; }; 20 | 357DD6D51FA1A5390018B944 /* PictureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 357DD6D41FA1A5390018B944 /* PictureView.swift */; }; 21 | 357E293C1FA144D40016E831 /* NSBezierPath+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 357E293B1FA144D40016E831 /* NSBezierPath+Extension.swift */; }; 22 | 357F04781F8F26AD00B1006D /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 357F04771F8F26AD00B1006D /* String+Extension.swift */; }; 23 | 35C56C551F902C6E002458C6 /* NSImage+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C56C541F902C6E002458C6 /* NSImage+Extension.swift */; }; 24 | 35E192FA1FA81A4500F3CF32 /* Data+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35E192F81FA81A4500F3CF32 /* Data+Extension.swift */; }; 25 | 35F4469A1FA97573001B3290 /* ExportType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F446991FA97573001B3290 /* ExportType.swift */; }; 26 | 35FC8D8C22A1F0220068F023 /* NSColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35FC8D8B22A1F0220068F023 /* NSColor+Extension.swift */; }; 27 | 57408C711F94B163009736A3 /* ExportViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57408C701F94B163009736A3 /* ExportViewController.swift */; }; 28 | 57B52ADE22904F1D00ABF920 /* Export.py in Resources */ = {isa = PBXBuildFile; fileRef = 57B52ADD22904F1D00ABF920 /* Export.py */; }; 29 | /* End PBXBuildFile section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | 353B06941F8E033E00E23802 /* DocumentObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentObject.swift; sourceTree = ""; }; 33 | 355514C11F8B830C00A44996 /* MacUserGenerator.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MacUserGenerator.app; sourceTree = BUILT_PRODUCTS_DIR; }; 34 | 355514C41F8B830C00A44996 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 35 | 355514C61F8B830C00A44996 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 36 | 355514C81F8B830C00A44996 /* Document.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Document.swift; sourceTree = ""; }; 37 | 355514CA1F8B830C00A44996 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 38 | 355514CD1F8B830C00A44996 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 39 | 355514CF1F8B830C00A44996 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 40 | 355514D01F8B830C00A44996 /* MacUserGenerator.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MacUserGenerator.entitlements; sourceTree = ""; }; 41 | 3556DCD11FA202B400F6E1CA /* ExportTabBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportTabBarView.swift; sourceTree = ""; }; 42 | 356A4DAD1F96A033007FAAB7 /* Exporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Exporter.swift; sourceTree = ""; }; 43 | 356A4DB21F96A93D007FAAB7 /* ScriptOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptOptions.swift; sourceTree = ""; }; 44 | 356A4DB41F96A9AF007FAAB7 /* PackageOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageOptions.swift; sourceTree = ""; }; 45 | 357DD6D41FA1A5390018B944 /* PictureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PictureView.swift; sourceTree = ""; }; 46 | 357E293B1FA144D40016E831 /* NSBezierPath+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSBezierPath+Extension.swift"; sourceTree = ""; }; 47 | 357F04771F8F26AD00B1006D /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; 48 | 35C56C541F902C6E002458C6 /* NSImage+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSImage+Extension.swift"; sourceTree = ""; }; 49 | 35D41A261FA712120006FEBE /* MacUserGenerator-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MacUserGenerator-Bridging-Header.h"; sourceTree = ""; }; 50 | 35E192F81FA81A4500F3CF32 /* Data+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Extension.swift"; sourceTree = ""; }; 51 | 35F446991FA97573001B3290 /* ExportType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportType.swift; sourceTree = ""; }; 52 | 35FC8D8B22A1F0220068F023 /* NSColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSColor+Extension.swift"; sourceTree = ""; }; 53 | 57408C701F94B163009736A3 /* ExportViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportViewController.swift; sourceTree = ""; }; 54 | 57B52ADD22904F1D00ABF920 /* Export.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = Export.py; sourceTree = ""; }; 55 | /* End PBXFileReference section */ 56 | 57 | /* Begin PBXFrameworksBuildPhase section */ 58 | 355514BE1F8B830C00A44996 /* Frameworks */ = { 59 | isa = PBXFrameworksBuildPhase; 60 | buildActionMask = 2147483647; 61 | files = ( 62 | ); 63 | runOnlyForDeploymentPostprocessing = 0; 64 | }; 65 | /* End PBXFrameworksBuildPhase section */ 66 | 67 | /* Begin PBXGroup section */ 68 | 355514B81F8B830C00A44996 = { 69 | isa = PBXGroup; 70 | children = ( 71 | 355514C31F8B830C00A44996 /* MacUserGenerator */, 72 | 355514C21F8B830C00A44996 /* Products */, 73 | ); 74 | sourceTree = ""; 75 | }; 76 | 355514C21F8B830C00A44996 /* Products */ = { 77 | isa = PBXGroup; 78 | children = ( 79 | 355514C11F8B830C00A44996 /* MacUserGenerator.app */, 80 | ); 81 | name = Products; 82 | sourceTree = ""; 83 | }; 84 | 355514C31F8B830C00A44996 /* MacUserGenerator */ = { 85 | isa = PBXGroup; 86 | children = ( 87 | 355514C41F8B830C00A44996 /* AppDelegate.swift */, 88 | 355514CA1F8B830C00A44996 /* Assets.xcassets */, 89 | 355514D01F8B830C00A44996 /* MacUserGenerator.entitlements */, 90 | 35D41A261FA712120006FEBE /* MacUserGenerator-Bridging-Header.h */, 91 | 355514CF1F8B830C00A44996 /* Info.plist */, 92 | 355514CC1F8B830C00A44996 /* Main.storyboard */, 93 | 57408C731F94B175009736A3 /* Document */, 94 | 356A4DB11F96A509007FAAB7 /* Export */, 95 | 57408C721F94B16B009736A3 /* Extensions */, 96 | 57408C741F94B181009736A3 /* Views & Controllers */, 97 | ); 98 | path = MacUserGenerator; 99 | sourceTree = ""; 100 | }; 101 | 356A4DB11F96A509007FAAB7 /* Export */ = { 102 | isa = PBXGroup; 103 | children = ( 104 | 57B52ADD22904F1D00ABF920 /* Export.py */, 105 | 356A4DAD1F96A033007FAAB7 /* Exporter.swift */, 106 | 35F446991FA97573001B3290 /* ExportType.swift */, 107 | 356A4DB41F96A9AF007FAAB7 /* PackageOptions.swift */, 108 | 356A4DB21F96A93D007FAAB7 /* ScriptOptions.swift */, 109 | ); 110 | path = Export; 111 | sourceTree = ""; 112 | }; 113 | 57408C721F94B16B009736A3 /* Extensions */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | 35E192F81FA81A4500F3CF32 /* Data+Extension.swift */, 117 | 357E293B1FA144D40016E831 /* NSBezierPath+Extension.swift */, 118 | 35FC8D8B22A1F0220068F023 /* NSColor+Extension.swift */, 119 | 35C56C541F902C6E002458C6 /* NSImage+Extension.swift */, 120 | 357F04771F8F26AD00B1006D /* String+Extension.swift */, 121 | ); 122 | path = Extensions; 123 | sourceTree = ""; 124 | }; 125 | 57408C731F94B175009736A3 /* Document */ = { 126 | isa = PBXGroup; 127 | children = ( 128 | 355514C81F8B830C00A44996 /* Document.swift */, 129 | 353B06941F8E033E00E23802 /* DocumentObject.swift */, 130 | ); 131 | path = Document; 132 | sourceTree = ""; 133 | }; 134 | 57408C741F94B181009736A3 /* Views & Controllers */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | 57408C701F94B163009736A3 /* ExportViewController.swift */, 138 | 3556DCD11FA202B400F6E1CA /* ExportTabBarView.swift */, 139 | 357DD6D41FA1A5390018B944 /* PictureView.swift */, 140 | 355514C61F8B830C00A44996 /* ViewController.swift */, 141 | ); 142 | path = "Views & Controllers"; 143 | sourceTree = ""; 144 | }; 145 | /* End PBXGroup section */ 146 | 147 | /* Begin PBXNativeTarget section */ 148 | 355514C01F8B830C00A44996 /* MacUserGenerator */ = { 149 | isa = PBXNativeTarget; 150 | buildConfigurationList = 355514D31F8B830C00A44996 /* Build configuration list for PBXNativeTarget "MacUserGenerator" */; 151 | buildPhases = ( 152 | 355514BD1F8B830C00A44996 /* Sources */, 153 | 355514BE1F8B830C00A44996 /* Frameworks */, 154 | 355514BF1F8B830C00A44996 /* Resources */, 155 | 3522DFF5229DF099009801EA /* ShellScript */, 156 | ); 157 | buildRules = ( 158 | ); 159 | dependencies = ( 160 | ); 161 | name = MacUserGenerator; 162 | productName = macOSUserGenerator; 163 | productReference = 355514C11F8B830C00A44996 /* MacUserGenerator.app */; 164 | productType = "com.apple.product-type.application"; 165 | }; 166 | /* End PBXNativeTarget section */ 167 | 168 | /* Begin PBXProject section */ 169 | 355514B91F8B830C00A44996 /* Project object */ = { 170 | isa = PBXProject; 171 | attributes = { 172 | LastSwiftUpdateCheck = 0900; 173 | LastUpgradeCheck = 1020; 174 | ORGANIZATIONNAME = "Nindi Gill"; 175 | TargetAttributes = { 176 | 355514C01F8B830C00A44996 = { 177 | CreatedOnToolsVersion = 9.0; 178 | LastSwiftMigration = 1020; 179 | ProvisioningStyle = Manual; 180 | SystemCapabilities = { 181 | com.apple.Sandbox = { 182 | enabled = 1; 183 | }; 184 | }; 185 | }; 186 | }; 187 | }; 188 | buildConfigurationList = 355514BC1F8B830C00A44996 /* Build configuration list for PBXProject "MacUserGenerator" */; 189 | compatibilityVersion = "Xcode 8.0"; 190 | developmentRegion = en; 191 | hasScannedForEncodings = 0; 192 | knownRegions = ( 193 | en, 194 | Base, 195 | ); 196 | mainGroup = 355514B81F8B830C00A44996; 197 | productRefGroup = 355514C21F8B830C00A44996 /* Products */; 198 | projectDirPath = ""; 199 | projectRoot = ""; 200 | targets = ( 201 | 355514C01F8B830C00A44996 /* MacUserGenerator */, 202 | ); 203 | }; 204 | /* End PBXProject section */ 205 | 206 | /* Begin PBXResourcesBuildPhase section */ 207 | 355514BF1F8B830C00A44996 /* Resources */ = { 208 | isa = PBXResourcesBuildPhase; 209 | buildActionMask = 2147483647; 210 | files = ( 211 | 355514CB1F8B830C00A44996 /* Assets.xcassets in Resources */, 212 | 57B52ADE22904F1D00ABF920 /* Export.py in Resources */, 213 | 355514CE1F8B830C00A44996 /* Main.storyboard in Resources */, 214 | ); 215 | runOnlyForDeploymentPostprocessing = 0; 216 | }; 217 | /* End PBXResourcesBuildPhase section */ 218 | 219 | /* Begin PBXShellScriptBuildPhase section */ 220 | 3522DFF5229DF099009801EA /* ShellScript */ = { 221 | isa = PBXShellScriptBuildPhase; 222 | buildActionMask = 2147483647; 223 | files = ( 224 | ); 225 | inputFileListPaths = ( 226 | ); 227 | inputPaths = ( 228 | ); 229 | outputFileListPaths = ( 230 | ); 231 | outputPaths = ( 232 | ); 233 | runOnlyForDeploymentPostprocessing = 0; 234 | shellPath = "/usr/bin/env bash"; 235 | shellScript = "if which swiftlint > /dev/null ; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; 236 | }; 237 | /* End PBXShellScriptBuildPhase section */ 238 | 239 | /* Begin PBXSourcesBuildPhase section */ 240 | 355514BD1F8B830C00A44996 /* Sources */ = { 241 | isa = PBXSourcesBuildPhase; 242 | buildActionMask = 2147483647; 243 | files = ( 244 | 3556DCD21FA202B400F6E1CA /* ExportTabBarView.swift in Sources */, 245 | 355514C71F8B830C00A44996 /* ViewController.swift in Sources */, 246 | 35E192FA1FA81A4500F3CF32 /* Data+Extension.swift in Sources */, 247 | 353B06951F8E033E00E23802 /* DocumentObject.swift in Sources */, 248 | 356A4DB31F96A93D007FAAB7 /* ScriptOptions.swift in Sources */, 249 | 357DD6D51FA1A5390018B944 /* PictureView.swift in Sources */, 250 | 357E293C1FA144D40016E831 /* NSBezierPath+Extension.swift in Sources */, 251 | 355514C51F8B830C00A44996 /* AppDelegate.swift in Sources */, 252 | 356A4DB51F96A9AF007FAAB7 /* PackageOptions.swift in Sources */, 253 | 35C56C551F902C6E002458C6 /* NSImage+Extension.swift in Sources */, 254 | 35FC8D8C22A1F0220068F023 /* NSColor+Extension.swift in Sources */, 255 | 357F04781F8F26AD00B1006D /* String+Extension.swift in Sources */, 256 | 355514C91F8B830C00A44996 /* Document.swift in Sources */, 257 | 356A4DAE1F96A033007FAAB7 /* Exporter.swift in Sources */, 258 | 57408C711F94B163009736A3 /* ExportViewController.swift in Sources */, 259 | 35F4469A1FA97573001B3290 /* ExportType.swift in Sources */, 260 | ); 261 | runOnlyForDeploymentPostprocessing = 0; 262 | }; 263 | /* End PBXSourcesBuildPhase section */ 264 | 265 | /* Begin PBXVariantGroup section */ 266 | 355514CC1F8B830C00A44996 /* Main.storyboard */ = { 267 | isa = PBXVariantGroup; 268 | children = ( 269 | 355514CD1F8B830C00A44996 /* Base */, 270 | ); 271 | name = Main.storyboard; 272 | sourceTree = ""; 273 | }; 274 | /* End PBXVariantGroup section */ 275 | 276 | /* Begin XCBuildConfiguration section */ 277 | 355514D11F8B830C00A44996 /* Debug */ = { 278 | isa = XCBuildConfiguration; 279 | buildSettings = { 280 | ALWAYS_SEARCH_USER_PATHS = NO; 281 | CLANG_ANALYZER_NONNULL = YES; 282 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 283 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 284 | CLANG_CXX_LIBRARY = "libc++"; 285 | CLANG_ENABLE_MODULES = YES; 286 | CLANG_ENABLE_OBJC_ARC = YES; 287 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 288 | CLANG_WARN_BOOL_CONVERSION = YES; 289 | CLANG_WARN_COMMA = YES; 290 | CLANG_WARN_CONSTANT_CONVERSION = YES; 291 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 292 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 293 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 294 | CLANG_WARN_EMPTY_BODY = YES; 295 | CLANG_WARN_ENUM_CONVERSION = YES; 296 | CLANG_WARN_INFINITE_RECURSION = YES; 297 | CLANG_WARN_INT_CONVERSION = YES; 298 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 299 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 300 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 301 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 302 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 303 | CLANG_WARN_STRICT_PROTOTYPES = YES; 304 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 305 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 306 | CLANG_WARN_UNREACHABLE_CODE = YES; 307 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 308 | CODE_SIGN_IDENTITY = "Mac Developer"; 309 | COPY_PHASE_STRIP = NO; 310 | DEBUG_INFORMATION_FORMAT = dwarf; 311 | ENABLE_HARDENED_RUNTIME = YES; 312 | ENABLE_STRICT_OBJC_MSGSEND = YES; 313 | ENABLE_TESTABILITY = YES; 314 | GCC_C_LANGUAGE_STANDARD = gnu11; 315 | GCC_DYNAMIC_NO_PIC = NO; 316 | GCC_NO_COMMON_BLOCKS = YES; 317 | GCC_OPTIMIZATION_LEVEL = 0; 318 | GCC_PREPROCESSOR_DEFINITIONS = ( 319 | "DEBUG=1", 320 | "$(inherited)", 321 | ); 322 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 323 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 324 | GCC_WARN_UNDECLARED_SELECTOR = YES; 325 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 326 | GCC_WARN_UNUSED_FUNCTION = YES; 327 | GCC_WARN_UNUSED_VARIABLE = YES; 328 | MACOSX_DEPLOYMENT_TARGET = 10.11; 329 | MTL_ENABLE_DEBUG_INFO = YES; 330 | ONLY_ACTIVE_ARCH = YES; 331 | SDKROOT = macosx; 332 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 333 | SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/$(PROJECT_NAME)/$(PROJECT_NAME)-Bridging-Header.h"; 334 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 335 | }; 336 | name = Debug; 337 | }; 338 | 355514D21F8B830C00A44996 /* Release */ = { 339 | isa = XCBuildConfiguration; 340 | buildSettings = { 341 | ALWAYS_SEARCH_USER_PATHS = NO; 342 | CLANG_ANALYZER_NONNULL = YES; 343 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 344 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 345 | CLANG_CXX_LIBRARY = "libc++"; 346 | CLANG_ENABLE_MODULES = YES; 347 | CLANG_ENABLE_OBJC_ARC = YES; 348 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 349 | CLANG_WARN_BOOL_CONVERSION = YES; 350 | CLANG_WARN_COMMA = YES; 351 | CLANG_WARN_CONSTANT_CONVERSION = YES; 352 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 353 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 354 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 355 | CLANG_WARN_EMPTY_BODY = YES; 356 | CLANG_WARN_ENUM_CONVERSION = YES; 357 | CLANG_WARN_INFINITE_RECURSION = YES; 358 | CLANG_WARN_INT_CONVERSION = YES; 359 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 360 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 361 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 362 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 363 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 364 | CLANG_WARN_STRICT_PROTOTYPES = YES; 365 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 366 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 367 | CLANG_WARN_UNREACHABLE_CODE = YES; 368 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 369 | CODE_SIGN_IDENTITY = "Mac Developer"; 370 | COPY_PHASE_STRIP = NO; 371 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 372 | ENABLE_HARDENED_RUNTIME = YES; 373 | ENABLE_NS_ASSERTIONS = NO; 374 | ENABLE_STRICT_OBJC_MSGSEND = YES; 375 | GCC_C_LANGUAGE_STANDARD = gnu11; 376 | GCC_NO_COMMON_BLOCKS = YES; 377 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 378 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 379 | GCC_WARN_UNDECLARED_SELECTOR = YES; 380 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 381 | GCC_WARN_UNUSED_FUNCTION = YES; 382 | GCC_WARN_UNUSED_VARIABLE = YES; 383 | MACOSX_DEPLOYMENT_TARGET = 10.11; 384 | MTL_ENABLE_DEBUG_INFO = NO; 385 | SDKROOT = macosx; 386 | SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/$(PROJECT_NAME)/$(PROJECT_NAME)-Bridging-Header.h"; 387 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 388 | }; 389 | name = Release; 390 | }; 391 | 355514D41F8B830C00A44996 /* Debug */ = { 392 | isa = XCBuildConfiguration; 393 | buildSettings = { 394 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 395 | CODE_SIGN_ENTITLEMENTS = MacUserGenerator/MacUserGenerator.entitlements; 396 | CODE_SIGN_IDENTITY = "Mac Developer"; 397 | CODE_SIGN_STYLE = Manual; 398 | COMBINE_HIDPI_IMAGES = YES; 399 | DEVELOPMENT_TEAM = 7K3HVCLV7Z; 400 | INFOPLIST_FILE = "$(PROJECT_NAME)/Info.plist"; 401 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 402 | MACOSX_DEPLOYMENT_TARGET = 10.11; 403 | PRODUCT_BUNDLE_IDENTIFIER = com.ninxsoft.MacUserGenerator; 404 | PRODUCT_NAME = "$(TARGET_NAME)"; 405 | PROVISIONING_PROFILE_SPECIFIER = ""; 406 | SWIFT_VERSION = 5.0; 407 | }; 408 | name = Debug; 409 | }; 410 | 355514D51F8B830C00A44996 /* Release */ = { 411 | isa = XCBuildConfiguration; 412 | buildSettings = { 413 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 414 | CODE_SIGN_ENTITLEMENTS = MacUserGenerator/MacUserGenerator.entitlements; 415 | CODE_SIGN_IDENTITY = "Mac Developer"; 416 | CODE_SIGN_STYLE = Manual; 417 | COMBINE_HIDPI_IMAGES = YES; 418 | DEVELOPMENT_TEAM = 7K3HVCLV7Z; 419 | INFOPLIST_FILE = "$(PROJECT_NAME)/Info.plist"; 420 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 421 | MACOSX_DEPLOYMENT_TARGET = 10.11; 422 | PRODUCT_BUNDLE_IDENTIFIER = com.ninxsoft.MacUserGenerator; 423 | PRODUCT_NAME = "$(TARGET_NAME)"; 424 | PROVISIONING_PROFILE_SPECIFIER = ""; 425 | SWIFT_VERSION = 5.0; 426 | }; 427 | name = Release; 428 | }; 429 | /* End XCBuildConfiguration section */ 430 | 431 | /* Begin XCConfigurationList section */ 432 | 355514BC1F8B830C00A44996 /* Build configuration list for PBXProject "MacUserGenerator" */ = { 433 | isa = XCConfigurationList; 434 | buildConfigurations = ( 435 | 355514D11F8B830C00A44996 /* Debug */, 436 | 355514D21F8B830C00A44996 /* Release */, 437 | ); 438 | defaultConfigurationIsVisible = 0; 439 | defaultConfigurationName = Release; 440 | }; 441 | 355514D31F8B830C00A44996 /* Build configuration list for PBXNativeTarget "MacUserGenerator" */ = { 442 | isa = XCConfigurationList; 443 | buildConfigurations = ( 444 | 355514D41F8B830C00A44996 /* Debug */, 445 | 355514D51F8B830C00A44996 /* Release */, 446 | ); 447 | defaultConfigurationIsVisible = 0; 448 | defaultConfigurationName = Release; 449 | }; 450 | /* End XCConfigurationList section */ 451 | }; 452 | rootObject = 355514B91F8B830C00A44996 /* Project object */; 453 | } 454 | -------------------------------------------------------------------------------- /MacUserGenerator.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MacUserGenerator.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MacUserGenerator.xcodeproj/xcshareddata/xcschemes/MacUserGenerator.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /MacUserGenerator.xcodeproj/xcuserdata/nindig.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /MacUserGenerator.xcodeproj/xcuserdata/nindig.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | macOSUserGenerator.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 355514C01F8B830C00A44996 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /MacUserGenerator.xcodeproj/xcuserdata/ninxsoft.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | macOSUserGenerator.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /MacUserGenerator/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MacUserGenerator 4 | // 5 | // Created by Nindi Gill on 9/10/17. 6 | // Copyright © 2019 Nindi Gill. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | @NSApplicationMain 12 | class AppDelegate: NSObject, NSApplicationDelegate { 13 | 14 | func applicationDidFinishLaunching(_ aNotification: Notification) { 15 | cleanupTemporaryPackageFiles() 16 | } 17 | 18 | func applicationWillTerminate(_ aNotification: Notification) { 19 | cleanupTemporaryPackageFiles() 20 | } 21 | 22 | /** 23 | Cleans up any temporary files or directories used when creating packages. 24 | */ 25 | private func cleanupTemporaryPackageFiles() { 26 | 27 | let path = NSTemporaryDirectory() + "Packages" 28 | 29 | guard FileManager.default.fileExists(atPath: path) else { 30 | return 31 | } 32 | 33 | do { 34 | try FileManager.default.removeItem(atPath: path) 35 | } catch { 36 | print(error) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /MacUserGenerator/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "icon_16x16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "icon_16x16@2x.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "icon_32x32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "icon_32x32@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "icon_128x128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "icon_128x128@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "icon_256x256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "icon_256x256@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "icon_512x512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "icon_512x512@2x.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /MacUserGenerator/Assets.xcassets/AppIcon.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninxsoft/MacUserGenerator/9976c6f8f7975568ffc857c968eed133574e4516/MacUserGenerator/Assets.xcassets/AppIcon.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /MacUserGenerator/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninxsoft/MacUserGenerator/9976c6f8f7975568ffc857c968eed133574e4516/MacUserGenerator/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /MacUserGenerator/Assets.xcassets/AppIcon.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninxsoft/MacUserGenerator/9976c6f8f7975568ffc857c968eed133574e4516/MacUserGenerator/Assets.xcassets/AppIcon.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /MacUserGenerator/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninxsoft/MacUserGenerator/9976c6f8f7975568ffc857c968eed133574e4516/MacUserGenerator/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /MacUserGenerator/Assets.xcassets/AppIcon.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninxsoft/MacUserGenerator/9976c6f8f7975568ffc857c968eed133574e4516/MacUserGenerator/Assets.xcassets/AppIcon.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /MacUserGenerator/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninxsoft/MacUserGenerator/9976c6f8f7975568ffc857c968eed133574e4516/MacUserGenerator/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /MacUserGenerator/Assets.xcassets/AppIcon.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninxsoft/MacUserGenerator/9976c6f8f7975568ffc857c968eed133574e4516/MacUserGenerator/Assets.xcassets/AppIcon.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /MacUserGenerator/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninxsoft/MacUserGenerator/9976c6f8f7975568ffc857c968eed133574e4516/MacUserGenerator/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /MacUserGenerator/Assets.xcassets/AppIcon.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninxsoft/MacUserGenerator/9976c6f8f7975568ffc857c968eed133574e4516/MacUserGenerator/Assets.xcassets/AppIcon.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /MacUserGenerator/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninxsoft/MacUserGenerator/9976c6f8f7975568ffc857c968eed133574e4516/MacUserGenerator/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /MacUserGenerator/Assets.xcassets/Certificate.imageset/Certificate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninxsoft/MacUserGenerator/9976c6f8f7975568ffc857c968eed133574e4516/MacUserGenerator/Assets.xcassets/Certificate.imageset/Certificate.png -------------------------------------------------------------------------------- /MacUserGenerator/Assets.xcassets/Certificate.imageset/Certificate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninxsoft/MacUserGenerator/9976c6f8f7975568ffc857c968eed133574e4516/MacUserGenerator/Assets.xcassets/Certificate.imageset/Certificate@2x.png -------------------------------------------------------------------------------- /MacUserGenerator/Assets.xcassets/Certificate.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "filename" : "Certificate.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "filename" : "Certificate@2x.png", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "version" : 1, 16 | "author" : "xcode" 17 | } 18 | } -------------------------------------------------------------------------------- /MacUserGenerator/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /MacUserGenerator/Assets.xcassets/Document.iconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninxsoft/MacUserGenerator/9976c6f8f7975568ffc857c968eed133574e4516/MacUserGenerator/Assets.xcassets/Document.iconset/icon_128x128.png -------------------------------------------------------------------------------- /MacUserGenerator/Assets.xcassets/Document.iconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninxsoft/MacUserGenerator/9976c6f8f7975568ffc857c968eed133574e4516/MacUserGenerator/Assets.xcassets/Document.iconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /MacUserGenerator/Assets.xcassets/Document.iconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninxsoft/MacUserGenerator/9976c6f8f7975568ffc857c968eed133574e4516/MacUserGenerator/Assets.xcassets/Document.iconset/icon_16x16.png -------------------------------------------------------------------------------- /MacUserGenerator/Assets.xcassets/Document.iconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninxsoft/MacUserGenerator/9976c6f8f7975568ffc857c968eed133574e4516/MacUserGenerator/Assets.xcassets/Document.iconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /MacUserGenerator/Assets.xcassets/Document.iconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninxsoft/MacUserGenerator/9976c6f8f7975568ffc857c968eed133574e4516/MacUserGenerator/Assets.xcassets/Document.iconset/icon_256x256.png -------------------------------------------------------------------------------- /MacUserGenerator/Assets.xcassets/Document.iconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninxsoft/MacUserGenerator/9976c6f8f7975568ffc857c968eed133574e4516/MacUserGenerator/Assets.xcassets/Document.iconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /MacUserGenerator/Assets.xcassets/Document.iconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninxsoft/MacUserGenerator/9976c6f8f7975568ffc857c968eed133574e4516/MacUserGenerator/Assets.xcassets/Document.iconset/icon_32x32.png -------------------------------------------------------------------------------- /MacUserGenerator/Assets.xcassets/Document.iconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninxsoft/MacUserGenerator/9976c6f8f7975568ffc857c968eed133574e4516/MacUserGenerator/Assets.xcassets/Document.iconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /MacUserGenerator/Assets.xcassets/Document.iconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninxsoft/MacUserGenerator/9976c6f8f7975568ffc857c968eed133574e4516/MacUserGenerator/Assets.xcassets/Document.iconset/icon_512x512.png -------------------------------------------------------------------------------- /MacUserGenerator/Assets.xcassets/Document.iconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninxsoft/MacUserGenerator/9976c6f8f7975568ffc857c968eed133574e4516/MacUserGenerator/Assets.xcassets/Document.iconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /MacUserGenerator/Assets.xcassets/Image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "scale" : "3x" 14 | } 15 | ], 16 | "info" : { 17 | "version" : 1, 18 | "author" : "xcode" 19 | } 20 | } -------------------------------------------------------------------------------- /MacUserGenerator/Assets.xcassets/Picture.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "filename" : "Picture.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "filename" : "Picture@2x.png", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "version" : 1, 16 | "author" : "xcode" 17 | } 18 | } -------------------------------------------------------------------------------- /MacUserGenerator/Assets.xcassets/Picture.imageset/Picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninxsoft/MacUserGenerator/9976c6f8f7975568ffc857c968eed133574e4516/MacUserGenerator/Assets.xcassets/Picture.imageset/Picture.png -------------------------------------------------------------------------------- /MacUserGenerator/Assets.xcassets/Picture.imageset/Picture@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninxsoft/MacUserGenerator/9976c6f8f7975568ffc857c968eed133574e4516/MacUserGenerator/Assets.xcassets/Picture.imageset/Picture@2x.png -------------------------------------------------------------------------------- /MacUserGenerator/Document/Document.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Document.swift 3 | // MacUserGenerator 4 | // 5 | // Created by Nindi Gill on 9/10/17. 6 | // Copyright © 2019 Nindi Gill. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class Document: NSDocument { 12 | 13 | var dictionary = NSDictionary() 14 | 15 | override class var autosavesInPlace: Bool { 16 | return true 17 | } 18 | 19 | override func makeWindowControllers() { 20 | 21 | let storyboard = NSStoryboard(name: "Main", bundle: nil) 22 | let identifier = "Document Window Controller" 23 | 24 | guard let controller = storyboard.instantiateController(withIdentifier: identifier) as? NSWindowController else { 25 | return 26 | } 27 | 28 | guard let viewController = controller.contentViewController as? ViewController else { 29 | return 30 | } 31 | 32 | viewController.documentObject = DocumentObject(dictionary: dictionary) 33 | self.addWindowController(controller) 34 | } 35 | 36 | override func read(from url: URL, ofType typeName: String) throws { 37 | 38 | guard let dictionary = NSDictionary(contentsOf: url) else { 39 | throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil) 40 | } 41 | 42 | self.dictionary = dictionary 43 | } 44 | 45 | override func write(to url: URL, ofType typeName: String) throws { 46 | 47 | guard let viewController = self.windowControllers.first?.contentViewController as? ViewController else { 48 | throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil) 49 | } 50 | 51 | let dictionary = viewController.documentObject.dictionary 52 | 53 | guard dictionary.write(to: url, atomically: true) else { 54 | return 55 | } 56 | 57 | self.windowControllers.first?.setDocumentEdited(false) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /MacUserGenerator/Document/DocumentObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DocumentObject.swift 3 | // MacUserGenerator 4 | // 5 | // Created by Nindi Gill on 11/10/17. 6 | // Copyright © 2019 Nindi Gill. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class DocumentObject: NSObject { 12 | 13 | enum Key: String { 14 | case accountType 15 | case fullName 16 | case accountName 17 | case picture 18 | case password 19 | case verify 20 | case passwordHint 21 | case userID 22 | case loginShell 23 | case homeDirectory 24 | case hideUserAccount 25 | case hideHomeDirectory 26 | case loginAutomatically 27 | case skipSetupAssistant 28 | } 29 | 30 | enum AccountType: String { 31 | case administrator 32 | case standard 33 | } 34 | 35 | enum LoginShell: String { 36 | case bash = "/bin/bash" 37 | case tcsh = "/bin/tcsh" 38 | case shell = "/bin/sh" 39 | case csh = "/bin/csh" 40 | case zsh = "/bin/zsh" 41 | } 42 | 43 | // document defaults 44 | var accountType = AccountType.administrator 45 | var fullName = "" 46 | var accountName = "" 47 | var picture = NSImage() 48 | var password = "" 49 | var verify = "" 50 | var passwordHint = "" 51 | var userID = "" 52 | var loginShell = LoginShell.bash 53 | var homeDirectory = "" 54 | var hideUserAccount = false 55 | var hideHomeDirectory = false 56 | var loginAutomatically = false 57 | var skipSetupAssistant = false 58 | 59 | convenience init(dictionary: NSDictionary) { 60 | self.init() 61 | 62 | if let string = dictionary[Key.accountType.rawValue] as? String, 63 | let type = AccountType(rawValue: string) { 64 | accountType = type 65 | } 66 | 67 | if let string = dictionary[Key.fullName.rawValue] as? String { 68 | fullName = string 69 | } 70 | 71 | if let string = dictionary[Key.accountName.rawValue] as? String { 72 | accountName = string 73 | } 74 | 75 | if let data = dictionary[Key.picture.rawValue] as? Data, 76 | let image = NSImage(data: data) { 77 | picture = image 78 | } else if let image = NSImage(named: "Picture") { 79 | picture = image 80 | } 81 | 82 | if let string = dictionary[Key.passwordHint.rawValue] as? String { 83 | passwordHint = string 84 | } 85 | 86 | if let string = dictionary[Key.userID.rawValue] as? String { 87 | userID = string 88 | } 89 | 90 | if let string = dictionary[Key.loginShell.rawValue] as? String, 91 | let shell = LoginShell(rawValue: string) { 92 | loginShell = shell 93 | } 94 | 95 | if let string = dictionary[Key.homeDirectory.rawValue] as? String { 96 | homeDirectory = string 97 | } 98 | 99 | if let userAccount = dictionary[Key.hideUserAccount.rawValue] as? Bool, 100 | let homeDirectory = dictionary[Key.hideHomeDirectory.rawValue] as? Bool, 101 | let automatically = dictionary[Key.loginAutomatically.rawValue] as? Bool, 102 | let setupAssistant = dictionary[Key.skipSetupAssistant.rawValue] as? Bool { 103 | hideUserAccount = userAccount 104 | hideHomeDirectory = homeDirectory 105 | loginAutomatically = automatically 106 | skipSetupAssistant = setupAssistant 107 | } 108 | } 109 | 110 | /** 111 | NSDictionary representation of document object 112 | */ 113 | var dictionary: NSDictionary { 114 | let dictionary = NSMutableDictionary() 115 | dictionary[Key.accountType.rawValue] = accountType.rawValue 116 | dictionary[Key.fullName.rawValue] = fullName 117 | dictionary[Key.accountName.rawValue] = accountName 118 | dictionary[Key.picture.rawValue] = picture.isValid ? picture.tiffRepresentation : nil 119 | dictionary[Key.passwordHint.rawValue] = passwordHint 120 | dictionary[Key.userID.rawValue] = userID 121 | dictionary[Key.loginShell.rawValue] = loginShell.rawValue 122 | dictionary[Key.homeDirectory.rawValue] = homeDirectory 123 | dictionary[Key.hideUserAccount.rawValue] = hideUserAccount 124 | dictionary[Key.hideHomeDirectory.rawValue] = hideHomeDirectory 125 | dictionary[Key.loginAutomatically.rawValue] = loginAutomatically 126 | dictionary[Key.skipSetupAssistant.rawValue] = skipSetupAssistant 127 | return dictionary 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /MacUserGenerator/Export/Export.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | MacUserGenerator - Export.py 5 | Copyright 2019 Nindi Gill. All rights reserved. 6 | 7 | A template python script used to create macOS user accounts. 8 | """ 9 | 10 | import os 11 | import plistlib 12 | import subprocess 13 | import sys 14 | import uuid 15 | from Foundation import NSArray, NSData 16 | from OpenDirectory import ODSession, ODNode 17 | from OpenDirectory import kODNodeTypeLocalNodes, kODRecordTypeUsers 18 | 19 | USER_DATA = { 20 | "authentication_authority": ";ShadowHash;HASHLIST:", 21 | "generateduid": str(uuid.uuid4()).upper(), 22 | "gid": "20", 23 | "IsHidden": "#ISHIDDEN", 24 | "home": "#HOME", 25 | "jpegphoto": "#JPEGPHOTO".decode('base64'), 26 | "name": "#NAME", 27 | "passwd": "********", 28 | "realname": "#REALNAME", 29 | "ShadowHashData": """#SHADOWHASHDATA""".decode('base64'), 30 | "shell": "#SHELL", 31 | "uid": "#UID", 32 | "_writers_hint": "#NAME", 33 | "_writers_jpegphoto": "#NAME", 34 | "_writers_passwd": "#NAME", 35 | "_writers_picture": "#NAME", 36 | "_writers_realname": "#NAME", 37 | "_writers_UserCertificate": "#NAME" 38 | } 39 | 40 | USER_PREFERENCES = { 41 | "admin": "#ADMIN", 42 | "autologin": "#AUTOLOGIN", 43 | "kcpassword": "#KCPASSWORD".decode('base64'), 44 | "skipsetupassistant": "#SKIPSETUPASSISTANT" 45 | } 46 | 47 | 48 | def is_booted_volume(): 49 | """ 50 | Returns True if the script / package (pkg) is being executed on 51 | the booted volume, or False if being executed on a targeted volume. 52 | """ 53 | 54 | # sys.argv will only have more than 3 arguments when running within a pkg 55 | # (which means scripts are always targeting the booted bolume) 56 | if len(sys.argv) < 4: 57 | return True 58 | 59 | # grab the target destination 60 | target = sys.argv[3] 61 | local_disk = "/" 62 | # true if installing pkg to booted volume, otherwise false 63 | return target is local_disk 64 | 65 | 66 | def get_target(): 67 | """ 68 | Returns the target volume path: 69 | eg. Booted volume: / 70 | eg. Targeted volume: /Volumes/Mac HD 71 | """ 72 | 73 | # sys.argv will only have more than 3 arguments when running within a pkg 74 | # (which means scripts are always targeting the booted bolume) 75 | if len(sys.argv) < 4: 76 | return "" 77 | 78 | # grab the target destination 79 | target = sys.argv[3] 80 | local_disk = "/" 81 | # blank if installing pkg to booted volume, otherwise target volume path 82 | return "" if target == local_disk else target 83 | 84 | 85 | def get_od_node(): 86 | """ 87 | Returns an ODNode object, used to communicate with the Open Directory API. 88 | """ 89 | 90 | session = ODSession.defaultSession() 91 | 92 | if not session: 93 | return None 94 | 95 | node, error = ODNode.nodeWithSession_type_error_( 96 | session, kODNodeTypeLocalNodes, None 97 | ) 98 | 99 | if error: 100 | print error 101 | return None 102 | 103 | return node 104 | 105 | 106 | def get_user_plist_path(): 107 | """ 108 | Helper function returning the path to the user account property list. 109 | """ 110 | 111 | user_plist_path = "/private/var/db/dslocal/nodes/Default/users/" 112 | return user_plist_path 113 | 114 | 115 | def get_dictionary_from_plist(path): 116 | """ 117 | Loads a Property List located at the provided path, converts from binary to 118 | xml, and returns the converted contents as a dictionary. 119 | """ 120 | 121 | with open(path, "rb") as file: 122 | filecontents = file.read() 123 | arguments = ["plutil", "-convert", "xml1", "-o", "-", "--", "-"] 124 | process = subprocess.Popen(arguments, 125 | stdin=subprocess.PIPE, 126 | stdout=subprocess.PIPE) 127 | string, error = process.communicate(filecontents) 128 | dictionary = plistlib.readPlistFromString(string) 129 | return dictionary 130 | 131 | 132 | def create_directory(path, mode, uid, gid): 133 | """ 134 | Creates a directory located at the provided path, setting the mode and 135 | ownership with the provided mode, uid and gid. 136 | Returns True if successful, otherwise returns False. 137 | """ 138 | 139 | if not os.path.isdir(path): 140 | try: 141 | os.mkdir(path, mode) 142 | except OSError: 143 | return False 144 | 145 | os.chown(path, int(uid), int(gid)) 146 | return True 147 | 148 | 149 | def check_if_user_is_root(): 150 | """ 151 | Determines, if the script is being executed as root. When installing a 152 | package (pkg), the script within the package is being executed as root. 153 | The script will exit early if it is not being executed as root. 154 | """ 155 | 156 | # root has a uid of 0 157 | if os.geteuid() != 0: 158 | file = os.path.basename(__file__) 159 | print "Please execute '" + file + "' as a user with sudo priviliges!" 160 | exit(1) 161 | 162 | 163 | def check_if_user_name_exists(name): 164 | """ 165 | Determines if the provided user name exists. When being executed on a 166 | booted volume, performs lookup via the Open Directory API. When being 167 | executed on a targeted volume, performs lookup via reading Property Lists. 168 | """ 169 | 170 | if is_booted_volume(): 171 | node = get_od_node() 172 | 173 | if not node: 174 | print "Unable to look up OpenDirectory node, aborting..." 175 | exit(1) 176 | 177 | record, error = node.recordWithRecordType_name_attributes_error_( 178 | kODRecordTypeUsers, 179 | name, 180 | None, 181 | None 182 | ) 183 | 184 | if error: 185 | print error 186 | exit(1) 187 | 188 | if record is not None: 189 | print "User account '" + name + "' already exists, aborting..." 190 | exit(1) 191 | 192 | else: # Property List logic 193 | path = get_target() + get_user_plist_path() + name + ".plist" 194 | 195 | if os.path.isfile(path): 196 | print "User account '" + name + "' already exists, aborting..." 197 | exit(1) 198 | 199 | print "User account '" + name + "' does not exist, continuing..." 200 | 201 | 202 | def check_if_user_id_exists(uid): 203 | """ 204 | Determines if the provided user id exists. When being executed on a booted 205 | volume, performs lookup via dscl. When being executed on a targeted volume, 206 | performs lookup via reading Property Lists. 207 | """ 208 | 209 | if is_booted_volume(): 210 | # grab all booted volume user ids 211 | # dscl . list /Users UniqueID 212 | dscl_arguments = ["dscl", ".", "list", "/Users", "UniqueID"] 213 | dscl_process = subprocess.Popen(dscl_arguments, 214 | stdout=subprocess.PIPE) 215 | # pipe them into awk, grab the last column 216 | # dscl . list /Users UniqueID | awk '{ print $NF }' 217 | awk_arguments = ["awk", "{ print $NF }"] 218 | awk_process = subprocess.Popen(awk_arguments, 219 | stdin=dscl_process.stdout, 220 | stdout=subprocess.PIPE) 221 | dscl_process.stdout.close() 222 | output, error = awk_process.communicate() 223 | # separate the user ids into an array 224 | uids = output.split() 225 | 226 | if uid in uids: 227 | print "User ID '" + uid + "' already exists, aborting..." 228 | exit(1) 229 | else: # Property List logic 230 | path = get_target() + get_user_plist_path() 231 | 232 | for filename in os.listdir(path): 233 | plist = path + filename 234 | dictionary = get_dictionary_from_plist(plist) 235 | 236 | if uid == dictionary["uid"][0]: 237 | print "User ID '" + uid + "' already exists, aborting..." 238 | exit(1) 239 | 240 | print "User ID '" + uid + "' does not exist, continuing..." 241 | 242 | 243 | def create_user_account(name): 244 | """ 245 | Creates the user account 'stub'. When being executed on a booted volume, 246 | creates a user account stub via the Open Directory API. When being executed 247 | on a targeted volume, creates a user account stub Property List. 248 | """ 249 | 250 | if is_booted_volume(): 251 | node = get_od_node() 252 | 253 | if not node: 254 | print "Unable to look up OpenDirectory node, aborting..." 255 | exit(1) 256 | 257 | record, error = node.createRecordWithRecordType_name_attributes_error_( 258 | kODRecordTypeUsers, 259 | name, 260 | None, 261 | None 262 | ) 263 | 264 | if error: 265 | print error 266 | exit(1) 267 | 268 | print "User account '" + name + "' created via Open Directory" 269 | else: # Property List logic 270 | dictionary = { 271 | "name": name 272 | } 273 | path = get_target() + get_user_plist_path() + name + ".plist" 274 | plistlib.writePlist(dictionary, path) 275 | os.chmod(path, 0600) 276 | print "User account '" + name + "' created via Property List" 277 | 278 | 279 | def update_user_account(user_data): 280 | """ 281 | Updates the user account. When being executed on a booted volume, updates 282 | via the Open Directory API. When being executed on a targeted volume, 283 | updates via the user account Property List. 284 | """ 285 | 286 | name = user_data["name"] 287 | 288 | if is_booted_volume(): 289 | node = get_od_node() 290 | 291 | if not node: 292 | print "Unable to look up OpenDirectory node, aborting..." 293 | exit(1) 294 | 295 | record, error = node.recordWithRecordType_name_attributes_error_( 296 | kODRecordTypeUsers, 297 | name, 298 | None, 299 | None 300 | ) 301 | 302 | if error: 303 | print error 304 | exit(1) 305 | 306 | for attribute, value in user_data.items(): 307 | 308 | # jpegphoto and ShadowHashData are data blobs, not strings, so a 309 | # little conversion is required 310 | if attribute == "jpegphoto" or attribute == "ShadowHashData": 311 | data = NSData.dataWithBytes_length_(value, len(value)) 312 | value = NSArray.alloc().initWithObjects_(data) 313 | 314 | success, error = record.setValue_forAttribute_error_( 315 | value, 316 | attribute, 317 | None 318 | ) 319 | 320 | if error: 321 | print error 322 | exit(1) 323 | 324 | # we don't want to spew out the data blobs to stdout, so we just 325 | # replace it with something simple. this is purely for formatting 326 | # reasons 327 | if attribute == "jpegphoto" or attribute == "ShadowHashData": 328 | value = "DATA" 329 | 330 | print "User account '" + name + "' updated attribute " + \ 331 | attribute + ": " + str(value) 332 | else: # Property List logic 333 | path = get_target() + get_user_plist_path() + name + ".plist" 334 | dictionary = plistlib.readPlist(path) 335 | 336 | for attribute, value in user_data.items(): 337 | 338 | # jpegphoto and ShadowHashData are data blobs, not strings, so a 339 | # little conversion is required 340 | if attribute == "jpegphoto" or attribute == "ShadowHashData": 341 | value = plistlib.Data(value) 342 | 343 | dictionary[attribute] = [value] 344 | 345 | # we don't want to spew out the data blobs to stdout, so we just 346 | # replace it with something simple. this is purely for formatting 347 | # reasons 348 | if attribute == "jpegphoto" or attribute == "ShadowHashData": 349 | value = "DATA" 350 | 351 | print "User account '" + name + "' updated attribute " + \ 352 | attribute + ": " + str(value) 353 | 354 | plistlib.writePlist(dictionary, path) 355 | 356 | 357 | def set_admin(state, name, generateduid): 358 | """ 359 | Grants or removes administrator privileges for the user account. When being 360 | executed on a booted volume, uses the 'dseditgroup' command. When being 361 | executed on a targeted volume, updates via the user account Property List. 362 | """ 363 | 364 | if is_booted_volume(): 365 | member_type = "-a" if state == "TRUE" else "-d" 366 | subprocess.call(["dseditgroup", "-o", "edit", member_type, 367 | name, "-t", "user", "admin"]) 368 | granted = ("Granted" if state == "TRUE" else "Removed") 369 | print "User account '" + name + "' admin privileges " + granted 370 | else: # Property List logic 371 | plist = "/private/var/db/dslocal/nodes/Default/groups/admin.plist" 372 | path = get_target() + plist 373 | dictionary = get_dictionary_from_plist(path) 374 | 375 | if state == "TRUE": # add administrator privileges 376 | if name not in dictionary["users"]: 377 | dictionary["users"].append(name) 378 | 379 | if generateduid not in dictionary["groupmembers"]: 380 | dictionary["groupmembers"].append(generateduid) 381 | else: # remove administrator privileges 382 | if name in dictionary["users"]: 383 | dictionary["users"].remove(name) 384 | 385 | if generateduid in dictionary["groupmembers"]: 386 | dictionary["groupmembers"].remove(generateduid) 387 | 388 | plistlib.writePlist(dictionary, path) 389 | granted = ("granted" if state == "TRUE" else "removed") 390 | print "User account '" + name + "' administrator privileges " + granted 391 | 392 | 393 | def set_autologin(state, kcpassword, name): 394 | """ 395 | Sets Auto Login for the user account. This involves writing the 396 | obfuscated password to '/private/etc/kcpassword', and also updating the 397 | Property List '/Library/Preferences/com.apple.loginwindow.plist'. 398 | """ 399 | 400 | if state != "TRUE": 401 | return 402 | 403 | path = get_target() + "/private/etc/kcpassword" 404 | 405 | with open(path, "w") as file: 406 | file.write(kcpassword) 407 | 408 | os.chmod(path, 0600) 409 | 410 | plist = "/Library/Preferences/com.apple.loginwindow.plist" 411 | path = get_target() + plist 412 | dictionary = get_dictionary_from_plist(path) 413 | dictionary["autoLoginUser"] = name 414 | dictionary["lastUserName"] = name 415 | plistlib.writePlist(dictionary, path) 416 | 417 | if is_booted_volume(): # not required on targeted volumes 418 | subprocess.call(["killall", "cfprefsd"]) 419 | 420 | print "User account '" + name + "' set autologin" 421 | 422 | 423 | def skip_setup_assistant(state, name, uid, gid, home): 424 | """ 425 | Skips Setup Assistant for the user account. This involves creating 426 | '/private/var/db/.AppleSetupDone', as well as 427 | '~/Library/Preferences/com.apple.SetupAssistant.plist'. When being executed 428 | on a booted volume, the user account is also populated via the 429 | 'createhomedir' command. When being executed on a targeted volume, the 430 | '~/Library/Preferences' directory structure is created. 431 | """ 432 | 433 | if state != "TRUE": 434 | return 435 | 436 | # creating .AppleSetupDone 437 | path = get_target() + "/private/var/db/.AppleSetupDone" 438 | if not os.path.isfile(path): 439 | os.mknod(path, 0644) 440 | 441 | # creating the home directory strucure 442 | if is_booted_volume(): 443 | os.system("createhomedir -c -u " + name) 444 | 445 | if not os.path.isdir(home): 446 | print "User account '" + name + \ 447 | "' home directory was not created, aborting..." 448 | exit(1) 449 | else: # targeted volume logic 450 | path = get_target() + home 451 | if not create_directory(path, 0755, uid, gid): 452 | print "User account '" + name + \ 453 | "' home directory was not created, aborting..." 454 | exit(1) 455 | 456 | path = get_target() + home + "/Library" 457 | if not create_directory(path, 0700, uid, gid): 458 | print "User account '" + name + \ 459 | "' home directory was not created, aborting..." 460 | exit(1) 461 | 462 | path = get_target() + home + "/Library/Preferences" 463 | if not create_directory(path, 0700, uid, gid): 464 | print "User account '" + name + \ 465 | "' home directory was not created, aborting..." 466 | exit(1) 467 | 468 | print "User account '" + name + "' home directory created" 469 | 470 | # determine productversion and buildversion 471 | plist = "/System/Library/CoreServices/SystemVersion.plist" 472 | path = get_target() + plist 473 | dictionary = get_dictionary_from_plist(path) 474 | product_version = dictionary["ProductVersion"] 475 | build_version = dictionary["ProductBuildVersion"] 476 | 477 | dictionary = { 478 | "DidSeeCloudSetup": True, 479 | "DidSeeSiriSetup": True, 480 | "LastSeenCloudProductVersion": product_version, 481 | "LastSeenBuddyBuildVersion": build_version, 482 | "DidSeePrivacy": True 483 | } 484 | 485 | # creating com.apple.SetupAssistant.plist 486 | plist = "/Library/Preferences/com.apple.SetupAssistant.plist" 487 | path = get_target() + home + plist 488 | plistlib.writePlist(dictionary, path) 489 | os.chown(path, int(uid), -1) 490 | os.chmod(path, 0600) 491 | print "User account '" + name + "' skipped Setup Assistant" 492 | 493 | 494 | def restart_directory_services(): 495 | """ 496 | Restarts directory services (opendirectoryd). Only required when executing 497 | script on booted volume. 498 | """ 499 | 500 | if is_booted_volume(): 501 | subprocess.call(["killall", "opendirectoryd"]) 502 | print "Restarted Directory Services" 503 | 504 | 505 | def main(): 506 | """ 507 | Here is where the fun begins... each of the following functions will exit 508 | the script if conditions are not met or an error occurs. 509 | """ 510 | 511 | check_if_user_is_root() 512 | 513 | check_if_user_name_exists(USER_DATA["name"]) 514 | check_if_user_id_exists(USER_DATA["uid"]) 515 | 516 | create_user_account(USER_DATA["name"]) 517 | update_user_account(USER_DATA) 518 | 519 | set_admin(USER_PREFERENCES["admin"], 520 | USER_DATA["name"], 521 | USER_DATA["generateduid"]) 522 | 523 | set_autologin(USER_PREFERENCES["autologin"], 524 | USER_PREFERENCES["kcpassword"], 525 | USER_DATA["name"]) 526 | 527 | skip_setup_assistant(USER_PREFERENCES["skipsetupassistant"], 528 | USER_DATA["name"], 529 | USER_DATA["uid"], 530 | USER_DATA["gid"], 531 | USER_DATA["home"]) 532 | 533 | restart_directory_services() 534 | 535 | 536 | if __name__ == '__main__': 537 | main() 538 | -------------------------------------------------------------------------------- /MacUserGenerator/Export/ExportType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExportType.swift 3 | // MacUserGenerator 4 | // 5 | // Created by Nindi Gill on 1/11/17. 6 | // Copyright © 2019 Nindi Gill. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | enum ExportType: Int { 12 | 13 | case package 14 | case script 15 | 16 | var title: String { 17 | 18 | switch self { 19 | case .package: 20 | return "Package" 21 | case .script: 22 | return "Script" 23 | } 24 | } 25 | 26 | var fileExtension: String { 27 | 28 | switch self { 29 | case .package: 30 | return "pkg" 31 | case .script: 32 | return "py" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /MacUserGenerator/Export/Exporter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Exporter.swift 3 | // MacUserGenerator 4 | // 5 | // Created by Nindi Gill on 16/10/17. 6 | // Copyright © 2019 Nindi Gill. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Security 11 | 12 | class Exporter: NSObject { 13 | 14 | /** 15 | Creates a script at the specified url, using the provided document object and any options that are specified. 16 | - parameters: 17 | - url: The url at which to create the script. 18 | - documentObject: The document object to be used as the basis of the script. 19 | - options: Any script options that are specified when exporting. 20 | */ 21 | class func createScriptAt(url: URL, documentObject: DocumentObject, options: ScriptOptions) { 22 | 23 | let scriptURL = Bundle.main.url(forResource: "Export", withExtension: "py") 24 | 25 | do { 26 | var script = try String(contentsOf: scriptURL!) 27 | script = script.replacingOccurrences(of: "#NAME", with: documentObject.accountName) 28 | script = script.replacingOccurrences(of: "#REALNAME", with: documentObject.fullName) 29 | script = script.replacingOccurrences(of: "#UID", with: documentObject.userID) 30 | script = script.replacingOccurrences(of: "#SHELL", with: documentObject.loginShell.rawValue) 31 | script = script.replacingOccurrences(of: "#HINT", with: documentObject.passwordHint) 32 | script = script.replacingOccurrences(of: "#SHADOWHASHDATA", with: documentObject.password.shadowHash) 33 | 34 | let hiddenHomeDirectory = "/private/var/\(documentObject.accountName)" 35 | let home = documentObject.hideHomeDirectory ? hiddenHomeDirectory : documentObject.homeDirectory 36 | script = script.replacingOccurrences(of: "#HOME", with: home) 37 | 38 | let admin = documentObject.accountType == .administrator ? "TRUE" : "FALSE" 39 | script = script.replacingOccurrences(of: "#ADMIN", with: admin) 40 | 41 | let isHidden = documentObject.hideUserAccount ? "TRUE" : "FALSE" 42 | script = script.replacingOccurrences(of: "#ISHIDDEN", with: isHidden) 43 | 44 | let autoLogin = documentObject.loginAutomatically ? "TRUE" : "FALSE" 45 | script = script.replacingOccurrences(of: "#AUTOLOGIN", with: autoLogin) 46 | 47 | let kcpassword = documentObject.loginAutomatically ? documentObject.password.kcpassword : "" 48 | script = script.replacingOccurrences(of: "#KCPASSWORD", with: kcpassword) 49 | 50 | let skipSetupAssistant = documentObject.skipSetupAssistant ? "TRUE" : "FALSE" 51 | script = script.replacingOccurrences(of: "#SKIPSETUPASSISTANT", with: skipSetupAssistant) 52 | 53 | if documentObject.picture.isValid, 54 | let tiff = documentObject.picture.tiffRepresentation { 55 | let string = tiff.base64EncodedString() 56 | script = script.replacingOccurrences(of: "#JPEGPHOTO", with: string) 57 | } 58 | 59 | try script.write(to: url, atomically: true, encoding: .utf8) 60 | 61 | // make the script executable 62 | let command = "chmod +x \(url.path)" 63 | let task = Process() 64 | let outputPipe = Pipe() 65 | let errorPipe = Pipe() 66 | task.standardOutput = outputPipe 67 | task.standardError = errorPipe 68 | task.launchPath = "/bin/bash" 69 | task.arguments = ["-l", "-c", command] 70 | task.launch() 71 | task.waitUntilExit() 72 | } catch { 73 | print(error) 74 | } 75 | } 76 | 77 | /** 78 | Creates a package at the specified url, using the provided document object and any options that are specified. 79 | - parameters: 80 | - url: The url at which to create the package. 81 | - documentObject: The document object to be used as the basis of the package. 82 | - options: Any package options that are specified when exporting. 83 | */ 84 | class func createPackageAt(url: URL, documentObject: DocumentObject, options: PackageOptions) { 85 | 86 | let packagesPath = "\(NSTemporaryDirectory())/Packages" 87 | let packagePath = "\(packagesPath)/\(options.identifier.replacingOccurrences(of: ".", with: ""))" 88 | let packageURL = URL(fileURLWithPath: "\(packagePath)/\(url.lastPathComponent)") 89 | let scriptsPath = "\(packagePath)/Scripts" 90 | let scriptsURL = URL(fileURLWithPath: scriptsPath, isDirectory: true) 91 | let scriptPath = "\(scriptsPath)/postinstall" 92 | let scriptURL = URL(fileURLWithPath: scriptPath, isDirectory: false) 93 | 94 | do { 95 | try FileManager.default.createDirectory(at: scriptsURL, 96 | withIntermediateDirectories: true, 97 | attributes: nil) 98 | 99 | createScriptAt(url: scriptURL, documentObject: documentObject, options: ScriptOptions()) 100 | 101 | let packageDictionary = ["identifier": options.identifier, 102 | "version": options.version, 103 | "scripts": scriptsPath, 104 | "certificate": options.certificate] 105 | 106 | buildPackage(packageDictionary: packageDictionary, 107 | fromURL: packageURL, 108 | toURL: url) 109 | } catch { 110 | print(error) 111 | } 112 | } 113 | 114 | /** 115 | Builds a package with the provided options in a temporary location, 116 | - parameters: 117 | - packageDictionary: The dictionary containing the following: 118 | - identifier: The unique identifier to associate with the package. Uses reverse domain name notation. 119 | - version: The version number to associate with the package. 120 | - scripts: The folder containing preinstall and postinstall scripts to be added to the package. 121 | - certificate: The full name of a Developer ID Certificate, used to sign the package. 122 | - fromURL: The temporary url location of the package. 123 | - toURL: The destination url location of the package. 124 | */ 125 | private static func buildPackage(packageDictionary: [String: String], fromURL: URL, toURL: URL) { 126 | 127 | guard let identifier = packageDictionary["identifier"], 128 | let version = packageDictionary["version"], 129 | let scripts = packageDictionary["scripts"], 130 | let certificate = packageDictionary["certificate"] else { 131 | return 132 | } 133 | 134 | let command = "pkgbuild --identifier \(identifier)" + 135 | " --version \(version)" + 136 | " --nopayload --scripts \(scripts) " + 137 | (certificate.isEmpty ? "" : "--sign \"\(certificate)\"") + 138 | " --timestamp=none" + 139 | " \(fromURL.path)" 140 | 141 | DispatchQueue.global(qos: .background).async { () -> Void in 142 | 143 | let task = Process() 144 | let outputPipe = Pipe() 145 | let errorPipe = Pipe() 146 | task.standardOutput = outputPipe 147 | task.standardError = errorPipe 148 | task.launchPath = "/bin/bash" 149 | task.arguments = ["-l", "-c", command] 150 | task.terminationHandler = { process in 151 | 152 | // let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile() 153 | // 154 | // if let string = String(data: outputData, encoding: .utf8) { 155 | // print("OUTPUT: \(string)") 156 | // } 157 | // 158 | // let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() 159 | // 160 | // if let string = String(data: errorData, encoding: .utf8) { 161 | // print("ERROR: \(string)") 162 | // } 163 | 164 | DispatchQueue.main.async { 165 | 166 | do { 167 | // overwrite the destination package, if required 168 | _ = try FileManager.default.replaceItemAt(toURL, withItemAt: fromURL) 169 | // delete the temporary package, if required 170 | try FileManager.default.removeItem(atPath: fromURL.deletingLastPathComponent().path) 171 | } catch { 172 | print(error) 173 | } 174 | } 175 | } 176 | 177 | task.launch() 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /MacUserGenerator/Export/PackageOptions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PackageOptions.swift 3 | // MacUserGenerator 4 | // 5 | // Created by Nindi Gill on 18/10/17. 6 | // Copyright © 2019 Nindi Gill. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class PackageOptions: NSObject { 12 | var identifier = "" 13 | var version = "" 14 | var certificate = "" 15 | } 16 | -------------------------------------------------------------------------------- /MacUserGenerator/Export/ScriptOptions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScriptOptions.swift 3 | // MacUserGenerator 4 | // 5 | // Created by Nindi Gill on 18/10/17. 6 | // Copyright © 2019 Nindi Gill. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ScriptOptions: NSObject { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /MacUserGenerator/Extensions/Data+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data+Extension.swift 3 | // MacUserGenerator 4 | // 5 | // Created by Nindi Gill on 31/10/17. 6 | // Copyright © 2019 Nindi Gill. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | extension Data { 12 | 13 | init(hex: String) { 14 | 15 | let scalars = hex.unicodeScalars 16 | var bytes = [UInt8](repeating: 0, count: (scalars.count + 1) >> 1) 17 | 18 | for (index, scalar) in scalars.enumerated() { 19 | 20 | var nibble = scalar.hexNibble 21 | 22 | if index & 1 == 0 { 23 | nibble <<= 4 24 | } 25 | 26 | bytes[index >> 1] |= nibble 27 | } 28 | self = Data(bytes: bytes, count: bytes.count) 29 | } 30 | } 31 | 32 | extension UnicodeScalar { 33 | 34 | var hexNibble: UInt8 { 35 | 36 | let value = self.value 37 | 38 | if 48 <= value && value <= 57 { 39 | return UInt8(value - 48) 40 | } else if 65 <= value && value <= 70 { 41 | return UInt8(value - 55) 42 | } else if 97 <= value && value <= 102 { 43 | return UInt8(value - 87) 44 | } 45 | 46 | fatalError("\(self) not a legal hex nibble") 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /MacUserGenerator/Extensions/NSBezierPath+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSBezierPath+Extension.swift 3 | // MacUserGenerator 4 | // 5 | // Created by Nindi Gill on 26/10/17. 6 | // Copyright © 2019 Nindi Gill. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | extension NSBezierPath { 12 | 13 | /** 14 | Returns a CGPath converted from the provided NSBezierPath 15 | */ 16 | var CGPath: CGPath { 17 | 18 | let path = CGMutablePath() 19 | var points = [CGPoint.zero, CGPoint.zero, CGPoint.zero] 20 | 21 | for index in 0.. NSImage { 19 | 20 | let sourceRect = NSRect(origin: NSPoint.zero, size: self.size) 21 | let destinationRect = NSRect(origin: NSPoint.zero, size: size) 22 | let image = NSImage(size: size) 23 | 24 | image.lockFocus() 25 | self.draw(in: destinationRect, from: sourceRect, operation: .sourceOver, fraction: 1.0) 26 | image.unlockFocus() 27 | image.size = size 28 | 29 | return NSImage(data: image.tiffRepresentation!)! 30 | } 31 | 32 | /** 33 | An array of valid image path extentions, used to filter image types. 34 | */ 35 | class var validPathExtensions: [String] { 36 | return ["jpg", "jpeg", "gif", "bmp", "png", "tiff", "svg", "ico", "icns"] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /MacUserGenerator/Extensions/String+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extension.swift 3 | // MacUserGenerator 4 | // 5 | // Created by Nindi Gill on 12/10/17. 6 | // Copyright © 2019 Nindi Gill. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | extension String { 12 | 13 | /** 14 | A boolean value indicating if the provided string is a valid macOS account name (shortname). 15 | 16 | Must only contain alphanumberic characters, hyphens, underscores or periods. 17 | */ 18 | var isValidAccountName: Bool { 19 | let pattern = "^[0-9a-zA-Z\\_\\-\\.]*[a-zA-Z]+[0-9a-zA-Z\\_\\-\\.]*$" 20 | return self.isValidWithPattern(pattern) 21 | } 22 | 23 | /** 24 | A boolean value indicating if the provided string is a valid User ID (uid). 25 | 26 | Must only contain digits. 27 | */ 28 | var isValidUserID: Bool { 29 | let pattern = "^[0-9]+$" 30 | return self.isValidWithPattern(pattern) 31 | } 32 | 33 | /** 34 | A boolean value indicating if the provided string is a valid home directory. 35 | 36 | Must be a valid directory path. 37 | */ 38 | var isValidHomeDirectory: Bool { 39 | let pattern = "^[\\/]([^\\:\\/]+[\\/]?)*$" 40 | return self.isValidWithPattern(pattern) 41 | } 42 | 43 | /** 44 | A boolean value indicating if the provided string is a valid pattern (regex). 45 | */ 46 | private func isValidWithPattern(_ pattern: String) -> Bool { 47 | 48 | do { 49 | let regex = try NSRegularExpression(pattern: pattern, options: []) 50 | let length = self.distance(from: self.startIndex, to: self.endIndex) 51 | let matches = regex.matches(in: self, options: [], range: NSRange(location: 0, length: length)) 52 | return !matches.isEmpty 53 | } catch { 54 | return false 55 | } 56 | } 57 | 58 | /** 59 | Returns a string replacing all matches of a pattern string with a given string. 60 | - Parameters: 61 | - pattern: The substring being searched for. 62 | - newString: The substring to replace all matches with. 63 | */ 64 | func replacePatternMatches(of pattern: String, with newString: String) -> String { 65 | 66 | let distance = self.distance(from: self.startIndex, to: self.endIndex) 67 | 68 | do { 69 | let pattern = try NSRegularExpression(pattern: pattern, options: []) 70 | let range = NSRange(location: 0, length: distance) 71 | return pattern.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: newString) 72 | } catch { 73 | return self 74 | } 75 | } 76 | 77 | /** 78 | Converts the provided string to a valid account name (shortname) string. 79 | */ 80 | var convertedToAccountName: String { 81 | return self.replacePatternMatches(of: " ", with: "").lowercased() 82 | } 83 | 84 | /** 85 | Converts the provided string to a base64 encoded shadow hash, based on PBKDF2. 86 | */ 87 | var shadowHash: String { 88 | 89 | let algorithm = CCPBKDFAlgorithm(kCCPBKDF2) 90 | 91 | guard let passwordData = self.data(using: .utf8) else { 92 | return "" 93 | } 94 | 95 | let password = [UInt8](passwordData).map { Int8(bitPattern: $0) } 96 | let passwordLen = password.count 97 | let saltString = String.randomSalt() 98 | let saltData = Data(hex: saltString) 99 | let salt = [UInt8](saltData) 100 | let saltLen = salt.count 101 | let prf = CCPseudoRandomAlgorithm(CCPBKDFAlgorithm(kCCPRFHmacAlgSHA512)) 102 | let rounds = arc4random_uniform(UInt32(2^32-1)) 103 | var derivedKey = [UInt8](repeating: 0, count: 128) 104 | let derivedKeyLen = derivedKey.count 105 | let status = CCKeyDerivationPBKDF(algorithm, 106 | password, 107 | passwordLen, 108 | salt, 109 | saltLen, 110 | prf, 111 | rounds, 112 | &derivedKey, 113 | derivedKeyLen) 114 | 115 | guard status == 0 else { 116 | return "" 117 | } 118 | 119 | let entropyData = Data(bytes: derivedKey, count: derivedKey.count) 120 | 121 | let string = "" + 122 | "" + 124 | "" + 125 | "" + 126 | "SALTED-SHA512-PBKDF2" + 127 | "" + 128 | "entropy" + 129 | "" + 130 | entropyData.base64EncodedString() + 131 | "" + 132 | "iterations" + 133 | "" + 134 | "\(rounds)" + 135 | "" + 136 | "salt" + 137 | "" + 138 | saltData.base64EncodedString() + 139 | "" + 140 | "" + 141 | "" + 142 | "" 143 | 144 | guard let data = string.data(using: .utf8) else { 145 | return "" 146 | } 147 | 148 | return data.base64EncodedString(options: .lineLength64Characters) 149 | } 150 | 151 | /** 152 | Generates a random 64 character hex salt string 153 | */ 154 | static func randomSalt() -> String { 155 | 156 | let characters = "0123456789abcdef" 157 | let length = UInt32(characters.count) 158 | var string = "" 159 | 160 | for _ in 0..<64 { 161 | let rand = Int(arc4random_uniform(length)) 162 | let index = characters.index(characters.startIndex, offsetBy: rand) 163 | let character = characters[index] 164 | string += String(character) 165 | } 166 | 167 | return string 168 | } 169 | 170 | /** 171 | Converts the provided string to a base64 encoded kcpassword 172 | */ 173 | var kcpassword: String { 174 | 175 | let characters = self.utf8.map { UInt8($0) } 176 | let keys: [UInt8] = [125, 137, 82, 35, 210, 188, 221, 234, 163, 185, 31] 177 | var xors = [UInt8]() 178 | 179 | // xor each byte of the password 180 | for index in 0.. 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDocumentTypes 8 | 9 | 10 | CFBundleTypeExtensions 11 | 12 | mug 13 | 14 | CFBundleTypeIconFile 15 | Document 16 | CFBundleTypeName 17 | MacUserGenerator User Document 18 | CFBundleTypeOSTypes 19 | 20 | ???? 21 | 22 | CFBundleTypeRole 23 | Editor 24 | NSDocumentClass 25 | $(PRODUCT_MODULE_NAME).Document 26 | 27 | 28 | CFBundleExecutable 29 | $(EXECUTABLE_NAME) 30 | CFBundleIconFile 31 | 32 | CFBundleIdentifier 33 | $(PRODUCT_BUNDLE_IDENTIFIER) 34 | CFBundleInfoDictionaryVersion 35 | 6.0 36 | CFBundleName 37 | $(PRODUCT_NAME) 38 | CFBundlePackageType 39 | APPL 40 | CFBundleShortVersionString 41 | 0.3 42 | CFBundleVersion 43 | 300 44 | LSApplicationCategoryType 45 | public.app-category.utilities 46 | LSMinimumSystemVersion 47 | $(MACOSX_DEPLOYMENT_TARGET) 48 | NSHumanReadableCopyright 49 | Copyright © 2019 Ninxsoft. All rights reserved. 50 | NSMainStoryboardFile 51 | Main 52 | NSPrincipalClass 53 | NSApplication 54 | 55 | 56 | -------------------------------------------------------------------------------- /MacUserGenerator/MacUserGenerator-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // MacUserGenerator-Bridging-Header.h 3 | // MacUserGenerator 4 | // 5 | // Created by Nindi Gill on 30/10/17. 6 | // Copyright © 2019 Nindi Gill. All rights reserved. 7 | // 8 | 9 | #ifndef Header_h 10 | #define Header_h 11 | 12 | #import 13 | 14 | #endif 15 | -------------------------------------------------------------------------------- /MacUserGenerator/MacUserGenerator.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-write 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /MacUserGenerator/Views & Controllers/ExportTabBarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExportTabBarView.swift 3 | // MacUserGenerator 4 | // 5 | // Created by Nindi Gill on 26/10/17. 6 | // Copyright © 2019 Nindi Gill. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | @IBDesignable class ExportTabBarView: NSView { 12 | 13 | private var inDarkMode: Bool { 14 | let mode = UserDefaults.standard.string(forKey: "AppleInterfaceStyle") 15 | return mode == "Dark" 16 | } 17 | 18 | @IBInspectable var title: NSString = "Title" 19 | @IBInspectable var fontSize: CGFloat = 14.0 20 | @IBInspectable var selected: Bool = false 21 | 22 | override func draw(_ dirtyRect: NSRect) { 23 | super.draw(dirtyRect) 24 | 25 | // highlight background if tab bar view is selected 26 | if selected { 27 | let selectedBackgroundColor = inDarkMode ? NSColor.darkAppearanceBackground : NSColor.lightAppearanceBackground 28 | selectedBackgroundColor.setFill() 29 | dirtyRect.fill() 30 | } 31 | 32 | // horizontally center the title within the dirtyRect 33 | let paragraphStyle = NSMutableParagraphStyle() 34 | paragraphStyle.alignment = .center 35 | 36 | // title attributes (font size, color, alignment) 37 | let textColor = inDarkMode ? NSColor.white : NSColor.black 38 | let selectedTextColor = inDarkMode ? NSColor.systemBlue : NSColor.systemBlue 39 | let attributes: [NSAttributedString.Key: Any] = [.font: NSFont.systemFont(ofSize: fontSize), 40 | .foregroundColor: selected ? selectedTextColor : textColor, 41 | .paragraphStyle: paragraphStyle] 42 | 43 | // vertically center the title within the dirtyRect 44 | let height = title.size(withAttributes: attributes).height 45 | var rect = dirtyRect 46 | rect.origin.y -= (dirtyRect.size.height - height) / 2 47 | 48 | title.draw(in: rect, withAttributes: attributes) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /MacUserGenerator/Views & Controllers/ExportViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExportViewController.swift 3 | // MacUserGenerator 4 | // 5 | // Created by Nindi Gill on 16/10/17. 6 | // Copyright © 2019 Nindi Gill. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ExportViewController: NSViewController { 12 | 13 | @IBOutlet var packageTabBarView: ExportTabBarView? 14 | @IBOutlet var scriptTabBarView: ExportTabBarView? 15 | @IBOutlet var tabView: NSTabView? 16 | @IBOutlet var packageIdentifierTextField: NSTextField? 17 | @IBOutlet var packageVersionTextField: NSTextField? 18 | @IBOutlet var packageCertificatesPopUpButton: NSPopUpButton? 19 | @IBOutlet var cancelButton: NSButton? 20 | @IBOutlet var nextButton: NSButton? 21 | var certificates = [String]() 22 | var exportType = ExportType.package 23 | var packageOptions = PackageOptions() 24 | var scriptOptions = ScriptOptions() 25 | 26 | override func viewWillAppear() { 27 | certificates = getCertificates() 28 | populateCertificatePopupButton(with: certificates) 29 | } 30 | 31 | /** 32 | Returns an array of strings containing any Developer Installer Certificates found on the machine. 33 | */ 34 | private func getCertificates() -> [String] { 35 | 36 | var certificates = [String]() 37 | 38 | let command = "security find-identity -v | grep \"Developer ID Installer:\"" 39 | let task = Process() 40 | let outputPipe = Pipe() 41 | let errorPipe = Pipe() 42 | task.standardOutput = outputPipe 43 | task.standardError = errorPipe 44 | task.launchPath = "/bin/bash" 45 | task.arguments = ["-l", "-c", command] 46 | task.launch() 47 | task.waitUntilExit() 48 | 49 | let data = outputPipe.fileHandleForReading.readDataToEndOfFile() 50 | 51 | guard let string = String(data: data, encoding: .utf8) else { 52 | return certificates 53 | } 54 | 55 | let strings = string.split { $0 == "\n" }.map(String.init) 56 | 57 | for string in strings { 58 | // strip out the leading whitespace and the uuid 59 | var certificate = string.replacePatternMatches(of: "^.*[0-9A-F]{40} ", with: "") 60 | certificate = certificate.replacingOccurrences(of: "\"", with: "") 61 | certificates.append(certificate) 62 | } 63 | 64 | return certificates 65 | } 66 | 67 | /** 68 | Populates the NSPopupButton with items containing Developer Installer Certificate names. 69 | - Parameters: 70 | - certificates: The array of strings contaning Developer Installer Certificate names. 71 | */ 72 | private func populateCertificatePopupButton(with certificates: [String]) { 73 | 74 | guard !certificates.isEmpty else { 75 | packageCertificatesPopUpButton?.addItem(withTitle: "No certificates found") 76 | return 77 | } 78 | 79 | for certificate in certificates { 80 | 81 | packageCertificatesPopUpButton?.addItem(withTitle: certificate) 82 | 83 | if let image = NSImage(named: "Certificate"), 84 | let item = self.packageCertificatesPopUpButton?.itemArray.last { 85 | item.image = image 86 | } 87 | } 88 | 89 | if let title = packageCertificatesPopUpButton?.itemTitles[2] { 90 | packageCertificatesPopUpButton?.selectItem(withTitle: title) 91 | } 92 | } 93 | 94 | /** 95 | Selects the Tab Bar view that was clicked. Also unselects all other Tab Bar Views. 96 | - Parameters: 97 | - sender: The NSClickGestureRecognizer that was clicked. 98 | */ 99 | @IBAction func exportTabBarViewClicked(sender: NSClickGestureRecognizer) { 100 | 101 | for (index, tabBarView) in [packageTabBarView, scriptTabBarView].enumerated() { 102 | 103 | tabBarView?.selected = sender.view == tabBarView 104 | tabBarView?.needsDisplay = true 105 | 106 | if sender.view == tabBarView { 107 | 108 | if let type = ExportType(rawValue: index) { 109 | exportType = type 110 | } 111 | 112 | tabView?.selectTabViewItem(at: index) 113 | } 114 | } 115 | 116 | validateNextButton() 117 | } 118 | 119 | /** 120 | Validates the Next button based on the options selected in the Tab Views. 121 | */ 122 | private func validateNextButton() { 123 | 124 | switch exportType { 125 | case .package: 126 | // if export type is package, ensure package identifier and version fields are not empty 127 | let identifierIsNotEmpty = !(packageIdentifierTextField?.stringValue.isEmpty)! 128 | let versionIsNotEmpty = !(packageVersionTextField?.stringValue.isEmpty)! 129 | nextButton?.isEnabled = identifierIsNotEmpty && versionIsNotEmpty 130 | case .script: 131 | nextButton?.isEnabled = true 132 | } 133 | } 134 | 135 | /** 136 | Closes the Export Window, by selecting either the Cancel or Next... buttons 137 | - Parameters: 138 | - sender: The button that was clicked. 139 | */ 140 | @IBAction func buttonClicked(sender: NSButton) { 141 | 142 | switch exportType { 143 | case .package: 144 | packageOptions.identifier = (packageIdentifierTextField?.stringValue)! 145 | packageOptions.version = (packageVersionTextField?.stringValue)! 146 | 147 | if let certificate = packageCertificatesPopUpButton?.titleOfSelectedItem { 148 | 149 | if certificates.contains(certificate) { 150 | packageOptions.certificate = certificate 151 | } 152 | } 153 | case .script: 154 | // script options go here 155 | break 156 | } 157 | 158 | let returnCode: NSApplication.ModalResponse = sender == cancelButton ? .cancel : .OK 159 | view.window?.sheetParent?.endSheet(view.window!, returnCode: returnCode) 160 | } 161 | } 162 | 163 | extension ExportViewController: NSTextFieldDelegate { 164 | 165 | func controlTextDidChange(_ obj: Notification) { 166 | validateNextButton() 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /MacUserGenerator/Views & Controllers/PictureView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PictureView.swift 3 | // MacUserGenerator 4 | // 5 | // Created by Nindi Gill on 26/10/17. 6 | // Copyright © 2019 Nindi Gill. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class PictureView: NSView { 12 | 13 | var imageLayer = CALayer() 14 | var shapeLayer = CAShapeLayer() 15 | var textLayer = CATextLayer() 16 | 17 | required init?(coder: NSCoder) { 18 | super.init(coder: coder) 19 | 20 | // layer 21 | self.wantsLayer = true 22 | self.layer?.borderWidth = 0.5 23 | self.layer?.borderColor = NSColor.systemGray.cgColor 24 | self.layer?.cornerRadius = self.frame.width / 2 25 | self.layer?.masksToBounds = true 26 | 27 | // image layer 28 | imageLayer.frame = NSRect(origin: CGPoint.zero, size: frame.size) 29 | imageLayer.contentsGravity = .resizeAspect 30 | self.layer?.addSublayer(imageLayer) 31 | 32 | // shape layer 33 | let path = NSBezierPath(rect: self.bounds) 34 | shapeLayer.path = path.CGPath 35 | shapeLayer.fillColor = NSColor.systemGray.cgColor 36 | shapeLayer.opacity = 0.25 37 | shapeLayer.isHidden = true 38 | self.layer?.addSublayer(shapeLayer) 39 | 40 | // text layer 41 | let attributes = [NSAttributedString.Key.font: NSFont.systemFont(ofSize: 14.0), 42 | NSAttributedString.Key.foregroundColor: NSColor.white.cgColor] as [NSAttributedString.Key: Any] 43 | let attributedString = NSAttributedString(string: "Edit", attributes: attributes) 44 | textLayer.string = attributedString 45 | textLayer.backgroundColor = NSColor.black.cgColor 46 | textLayer.alignmentMode = .center 47 | textLayer.contentsScale = (NSScreen.main?.backingScaleFactor)! 48 | let size = CGSize(width: self.frame.size.width, 49 | height: self.frame.size.height * 0.25) 50 | let textRect = NSRect(origin: CGPoint.zero, size: size) 51 | textLayer.frame = textRect 52 | textLayer.isHidden = true 53 | self.layer?.addSublayer(textLayer) 54 | 55 | // tracking 56 | let trackingRect = NSRect(origin: CGPoint.zero, size: self.frame.size) 57 | let options: NSTrackingArea.Options = [.activeInKeyWindow, .mouseEnteredAndExited, .mouseMoved] 58 | let trackingArea = NSTrackingArea(rect: trackingRect, options: options, owner: self, userInfo: nil) 59 | self.addTrackingArea(trackingArea) 60 | 61 | // dragging 62 | if #available(OSX 10.13, *) { 63 | self.registerForDraggedTypes([.URL]) 64 | } 65 | } 66 | 67 | override func mouseEntered(with event: NSEvent) { 68 | shapeLayer.isHidden = false 69 | textLayer.isHidden = false 70 | } 71 | 72 | override func mouseExited(with event: NSEvent) { 73 | shapeLayer.isHidden = true 74 | textLayer.isHidden = true 75 | } 76 | 77 | override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { 78 | 79 | let pasteboard = sender.draggingPasteboard 80 | let url = NSURL(from: pasteboard) 81 | 82 | guard let pathExtension = url?.pathExtension else { 83 | return .delete 84 | } 85 | 86 | return NSImage.validPathExtensions.contains(pathExtension) ? .copy : .delete 87 | } 88 | 89 | override func performDragOperation(_ sender: NSDraggingInfo) -> Bool { 90 | 91 | let pasteboard = sender.draggingPasteboard 92 | let url = NSURL(from: pasteboard) 93 | 94 | guard let pathExtension = url?.pathExtension else { 95 | return false 96 | } 97 | 98 | return NSImage.validPathExtensions.contains(pathExtension) 99 | } 100 | 101 | override func concludeDragOperation(_ sender: NSDraggingInfo?) { 102 | 103 | guard let pasteboard = sender?.draggingPasteboard else { 104 | return 105 | } 106 | 107 | guard let url = NSURL(from: pasteboard) else { 108 | return 109 | } 110 | 111 | _ = validateAndUpdateImageFromURL(url as URL) 112 | } 113 | 114 | /** 115 | Validates and updates the image for the picture view. 116 | - Parameters: 117 | - url: The URL of the image. 118 | */ 119 | func validateAndUpdateImageFromURL(_ url: URL) -> Bool { 120 | 121 | guard let viewController = window?.contentViewController as? ViewController, 122 | let image = NSImage(contentsOf: url) else { 123 | return false 124 | } 125 | 126 | let size = NSSize(width: 128, height: 128) 127 | let resizedImage = image.resizedImage(with: size) 128 | imageLayer.contents = resizedImage 129 | viewController.documentObject.picture = resizedImage 130 | return true 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /MacUserGenerator/Views & Controllers/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // MacUserGenerator 4 | // 5 | // Created by Nindi Gill on 9/10/17. 6 | // Copyright © 2019 Nindi Gill. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ViewController: NSViewController { 12 | 13 | @IBOutlet var administratorButton: NSButton? 14 | @IBOutlet var standardButton: NSButton? 15 | @IBOutlet var fullNameTextField: NSTextField? 16 | @IBOutlet var accountNameTextField: NSTextField? 17 | @IBOutlet var accountNameImageView: NSImageView? 18 | @IBOutlet var pictureView: PictureView? 19 | @IBOutlet var passwordSecureTextField: NSSecureTextField? 20 | @IBOutlet var verifySecureTextField: NSSecureTextField? 21 | @IBOutlet var verifyImageView: NSImageView? 22 | @IBOutlet var passwordHintTextField: NSTextField? 23 | @IBOutlet var userIDTextField: NSTextField? 24 | @IBOutlet var userIDImageView: NSImageView? 25 | @IBOutlet var bashButton: NSButton? 26 | @IBOutlet var tcshButton: NSButton? 27 | @IBOutlet var shButton: NSButton? 28 | @IBOutlet var cshButton: NSButton? 29 | @IBOutlet var zshButton: NSButton? 30 | @IBOutlet var homeDirectoryTextField: NSTextField? 31 | @IBOutlet var homeDirectoryImageView: NSImageView? 32 | @IBOutlet var homeDirectorySelectButton: NSButton? 33 | @IBOutlet var hideUserAccountCheckbox: NSButton? 34 | @IBOutlet var hideHomeDirectoryCheckbox: NSButton? 35 | @IBOutlet var loginAutomaticallyCheckbox: NSButton? 36 | @IBOutlet var skipSetupAssistantCheckbox: NSButton? 37 | @IBOutlet var exportButton: NSButton? 38 | @IBOutlet var helpButton: NSButton? 39 | var documentObject = DocumentObject() 40 | 41 | override func viewDidAppear() { 42 | administratorButton?.state = documentObject.accountType == .administrator ? .on : .off 43 | standardButton?.state = documentObject.accountType == .standard ? .on : .off 44 | fullNameTextField?.stringValue = documentObject.fullName 45 | accountNameTextField?.stringValue = documentObject.accountName 46 | pictureView?.imageLayer.contents = documentObject.picture 47 | passwordSecureTextField?.stringValue = documentObject.password 48 | verifySecureTextField?.stringValue = documentObject.verify 49 | passwordHintTextField?.stringValue = documentObject.passwordHint 50 | userIDTextField?.stringValue = documentObject.userID 51 | bashButton?.state = documentObject.loginShell == .bash ? .on : .off 52 | tcshButton?.state = documentObject.loginShell == .tcsh ? .on : .off 53 | shButton?.state = documentObject.loginShell == .shell ? .on : .off 54 | cshButton?.state = documentObject.loginShell == .csh ? .on : .off 55 | zshButton?.state = documentObject.loginShell == .zsh ? .on : .off 56 | homeDirectoryTextField?.stringValue = documentObject.homeDirectory 57 | hideUserAccountCheckbox?.state = documentObject.hideUserAccount ? .on : .off 58 | hideHomeDirectoryCheckbox?.state = documentObject.hideHomeDirectory ? .on : .off 59 | loginAutomaticallyCheckbox?.state = documentObject.loginAutomatically ? .on : .off 60 | skipSetupAssistantCheckbox?.state = documentObject.skipSetupAssistant ? .on : .off 61 | validateExportButton() 62 | } 63 | 64 | /** 65 | Pulls down an NSOpenPanel to select an image for the picture view. 66 | - Parameters: 67 | - sender: The NSClickGestureRecognizer that was clicked. 68 | */ 69 | @IBAction func pictureViewClicked(sender: NSClickGestureRecognizer) { 70 | 71 | let panel = NSOpenPanel() 72 | panel.canChooseFiles = true 73 | panel.canChooseDirectories = false 74 | panel.resolvesAliases = false 75 | panel.allowsMultipleSelection = false 76 | panel.canCreateDirectories = false 77 | panel.allowedFileTypes = NSImage.validPathExtensions 78 | 79 | panel.beginSheetModal(for: view.window!, completionHandler: { response -> Void in 80 | 81 | guard response == .OK else { 82 | return 83 | } 84 | 85 | guard let url = panel.url else { 86 | return 87 | } 88 | 89 | guard self.pictureView?.validateAndUpdateImageFromURL(url) == true else { 90 | return 91 | } 92 | 93 | self.documentHasBeenEdited() 94 | self.validateExportButton() 95 | }) 96 | } 97 | 98 | /** 99 | Invokes the action associated with the button that was clicked. 100 | - Parameters: 101 | - sender: The NSButton that was clicked. 102 | */ 103 | @IBAction func buttonClicked(sender: NSButton) { 104 | 105 | if sender == administratorButton { 106 | documentObject.accountType = .administrator 107 | documentHasBeenEdited() 108 | } 109 | 110 | if sender == standardButton { 111 | documentObject.accountType = .standard 112 | documentHasBeenEdited() 113 | } 114 | 115 | if sender == bashButton { 116 | documentObject.loginShell = .bash 117 | documentHasBeenEdited() 118 | } 119 | 120 | if sender == tcshButton { 121 | documentObject.loginShell = .tcsh 122 | documentHasBeenEdited() 123 | } 124 | 125 | if sender == shButton { 126 | documentObject.loginShell = .shell 127 | documentHasBeenEdited() 128 | } 129 | 130 | if sender == cshButton { 131 | documentObject.loginShell = .csh 132 | documentHasBeenEdited() 133 | } 134 | 135 | if sender == zshButton { 136 | documentObject.loginShell = .zsh 137 | documentHasBeenEdited() 138 | } 139 | 140 | if sender == homeDirectorySelectButton { 141 | showHomeDirectoryOpenPanel() 142 | } 143 | 144 | if sender == exportButton { 145 | showExportWindow() 146 | } 147 | 148 | if sender == helpButton { 149 | showHelp() 150 | } 151 | } 152 | 153 | /** 154 | Shows the NSOpenPanel to select the Users home directory. 155 | */ 156 | private func showHomeDirectoryOpenPanel() { 157 | 158 | let panel = NSOpenPanel() 159 | panel.canChooseFiles = false 160 | panel.canChooseDirectories = true 161 | panel.resolvesAliases = false 162 | panel.allowsMultipleSelection = false 163 | panel.canCreateDirectories = false 164 | 165 | panel.beginSheetModal(for: view.window!, completionHandler: { response in 166 | 167 | guard response == .OK else { 168 | return 169 | } 170 | 171 | guard let path = panel.url?.path else { 172 | return 173 | } 174 | 175 | self.documentObject.homeDirectory = path 176 | self.homeDirectoryTextField?.stringValue = path 177 | self.documentHasBeenEdited() 178 | self.validateExportButton() 179 | }) 180 | } 181 | 182 | /** 183 | Shows the Export window (and the attached ExportViewController), allowing for export type selection and options. 184 | */ 185 | private func showExportWindow() { 186 | 187 | let storyboard = NSStoryboard(name: "Main", bundle: nil) 188 | let identifier = "ExportWindowController" 189 | 190 | guard let controller = storyboard.instantiateController(withIdentifier: identifier) as? NSWindowController else { 191 | return 192 | } 193 | 194 | view.window?.beginSheet((controller.window!), completionHandler: { response in 195 | 196 | guard response == .OK else { 197 | return 198 | } 199 | 200 | guard let viewController = controller.contentViewController as? ExportViewController else { 201 | return 202 | } 203 | 204 | let type = viewController.exportType 205 | let packageOptions = viewController.packageOptions 206 | let scriptOptions = viewController.scriptOptions 207 | self.showExportSavePanel(type: type, packageOptions: packageOptions, scriptOptions: scriptOptions) 208 | }) 209 | } 210 | 211 | /** 212 | Shows the NSSavePanel to select a name and location for the export file. 213 | - Parameters: 214 | - type: The type of export (package or script). 215 | */ 216 | private func showExportSavePanel(type: ExportType, packageOptions: PackageOptions, scriptOptions: ScriptOptions) { 217 | 218 | let panel = NSSavePanel() 219 | panel.nameFieldLabel = "Name:" 220 | panel.nameFieldStringValue = self.documentObject.accountName 221 | panel.allowedFileTypes = [type.fileExtension] 222 | 223 | panel.beginSheetModal(for: self.view.window!, completionHandler: { response -> Void in 224 | 225 | guard response == .OK else { 226 | return 227 | } 228 | 229 | switch type { 230 | case .package: 231 | Exporter.createPackageAt(url: panel.url!, documentObject: self.documentObject, options: packageOptions) 232 | case .script: 233 | Exporter.createScriptAt(url: panel.url!, documentObject: self.documentObject, options: scriptOptions) 234 | } 235 | }) 236 | } 237 | 238 | /** 239 | Opens the Help URL in Safari.app. 240 | */ 241 | private func showHelp() { 242 | let url = URL(fileURLWithPath: "https://github.com/ninxsoft/MacUserGenerator/") 243 | NSWorkspace.shared.open(url) 244 | } 245 | 246 | /** 247 | Toggles the checkbox that was selected. 248 | - Parameters: 249 | - sender: The NSButton (checkbox) that was selected. 250 | */ 251 | @IBAction func checkBoxSelected(sender: NSButton) { 252 | 253 | let selected = sender.state == .on 254 | 255 | // if the option key was held down during a click 256 | guard !NSEvent.modifierFlags.contains(.option) else { 257 | documentObject.hideUserAccount = selected 258 | documentObject.hideHomeDirectory = selected 259 | documentObject.loginAutomatically = selected 260 | documentObject.skipSetupAssistant = selected 261 | hideUserAccountCheckbox?.state = selected ? .on : .off 262 | hideHomeDirectoryCheckbox?.state = selected ? .on : .off 263 | hideUserAccountCheckbox?.state = selected ? .on : .off 264 | loginAutomaticallyCheckbox?.state = selected ? .on : .off 265 | skipSetupAssistantCheckbox?.state = selected ? .on : .off 266 | return 267 | } 268 | 269 | if sender == hideUserAccountCheckbox { 270 | documentObject.hideUserAccount = selected 271 | } else if sender == hideHomeDirectoryCheckbox { 272 | documentObject.hideHomeDirectory = selected 273 | } else if sender == loginAutomaticallyCheckbox { 274 | documentObject.loginAutomatically = selected 275 | } else if sender == skipSetupAssistantCheckbox { 276 | documentObject.skipSetupAssistant = selected 277 | } 278 | 279 | documentHasBeenEdited() 280 | validateExportButton() 281 | } 282 | 283 | /** 284 | Flags the document window as edited. 285 | */ 286 | func documentHasBeenEdited() { 287 | view.window?.windowController?.setDocumentEdited(true) 288 | view.window?.windowController?.document?.updateChangeCount(.changeDone) 289 | } 290 | 291 | /** 292 | Validates whether the Export button should be enabled or not, based on the input of the textfields and checkboxes. 293 | */ 294 | private func validateExportButton() { 295 | 296 | if documentObject.accountName.isEmpty { 297 | accountNameImageView?.image = NSImage(named: "NSStatusPartiallyAvailable") 298 | } else if documentObject.accountName.isValidAccountName { 299 | accountNameImageView?.image = NSImage(named: "NSStatusAvailable") 300 | } else { 301 | accountNameImageView?.image = NSImage(named: "NSStatusUnavailable") 302 | } 303 | 304 | if documentObject.verify.isEmpty { 305 | verifyImageView?.image = NSImage(named: "NSStatusPartiallyAvailable") 306 | } else if documentObject.password == documentObject.verify { 307 | verifyImageView?.image = NSImage(named: "NSStatusAvailable") 308 | } else { 309 | verifyImageView?.image = NSImage(named: "NSStatusUnavailable") 310 | } 311 | 312 | if documentObject.userID.isEmpty { 313 | userIDImageView?.image = NSImage(named: "NSStatusPartiallyAvailable") 314 | } else if documentObject.userID.isValidUserID { 315 | userIDImageView?.image = NSImage(named: "NSStatusAvailable") 316 | } else { 317 | userIDImageView?.image = NSImage(named: "NSStatusUnavailable") 318 | } 319 | 320 | if documentObject.homeDirectory.isEmpty { 321 | homeDirectoryImageView?.image = NSImage(named: "NSStatusPartiallyAvailable") 322 | } else if documentObject.homeDirectory.isValidHomeDirectory { 323 | homeDirectoryImageView?.image = NSImage(named: "NSStatusAvailable") 324 | } else { 325 | homeDirectoryImageView?.image = NSImage(named: "NSStatusUnavailable") 326 | } 327 | 328 | exportButton?.isEnabled = documentObject.accountName.isValidAccountName && 329 | documentObject.userID.isValidUserID && 330 | documentObject.password == documentObject.verify && 331 | documentObject.homeDirectory.isValidHomeDirectory 332 | } 333 | } 334 | 335 | extension ViewController: NSTextFieldDelegate { 336 | 337 | func controlTextDidChange(_ obj: Notification) { 338 | 339 | guard let textField = obj.object as? NSTextField else { 340 | return 341 | } 342 | 343 | let string = textField.stringValue 344 | 345 | if textField == fullNameTextField { 346 | documentObject.fullName = string 347 | } else if textField == accountNameTextField { 348 | documentObject.accountName = string 349 | } else if textField == passwordSecureTextField { 350 | documentObject.password = string 351 | } else if textField == verifySecureTextField { 352 | documentObject.verify = string 353 | } else if textField == passwordHintTextField { 354 | documentObject.passwordHint = string 355 | } else if textField == userIDTextField { 356 | documentObject.userID = string 357 | } else if textField == homeDirectoryTextField { 358 | documentObject.homeDirectory = string 359 | } 360 | 361 | documentHasBeenEdited() 362 | validateExportButton() 363 | } 364 | 365 | func controlTextDidEndEditing(_ obj: Notification) { 366 | 367 | guard let textField = obj.object as? NSTextField else { 368 | return 369 | } 370 | 371 | let string = textField.stringValue 372 | 373 | // auto populate account name if full name has been entered, and account name is blank 374 | if textField == fullNameTextField && (accountNameTextField?.stringValue.isEmpty)! { 375 | let accountName = string.convertedToAccountName 376 | documentObject.accountName = accountName 377 | accountNameTextField?.stringValue = accountName 378 | } 379 | // auto populate home directory if account name has been entered and is valid and home directory is blank 380 | else if textField == accountNameTextField && 381 | documentObject.accountName.isValidAccountName && 382 | (homeDirectoryTextField?.stringValue.isEmpty)! { 383 | let homeDirectory = "/Users/\(string)" 384 | documentObject.homeDirectory = homeDirectory 385 | homeDirectoryTextField?.stringValue = homeDirectory 386 | } 387 | 388 | validateExportButton() 389 | } 390 | } 391 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MacUserGenerator 2 | 3 | A Mac utility that assists in automating the creation of macOS User Accounts. 4 | 5 | **MacUserGenerator** (MUG) is written from the ground up, with ideas ***heavily*** inspired from [MagerValp's](https://github.com/MagerValp) [CreateUserPkg](https://github.com/MagerValp/CreateUserPkg): 6 | 7 | | **Light Appearance** | **Dark Appearance** | 8 | | :----------------------------------------------------------: | :---------------------------------------------------------: | 9 | | ![Sample User](Readme%20Resources/Sample%20User%20Light.png) | ![Sample User](Readme%20Resources/Sample%20User%20Dark.png) | 10 | 11 | ## Features 12 | 13 | * [x] Export a user account as a **Python script (PY)**. 14 | * [x] Export a user account as an **Installer Package (PKG)**. 15 | * [x] Ability to Open / Save the user account document / snapshot (MUGshot? 🙃). 16 | 17 | | | **Booted Volumes** | **Targeted Volumes** | 18 | | ---------------------------------------: | :----------------------------------: | :------------------: | 19 | | **Create User Accounts** | Yes [*](#-macos-mojave-1014-caveats) | Yes | 20 | | **Set Administrator** | Yes | Yes | 21 | | **Hide User Account** | Yes | Yes | 22 | | **Hide User Account Home Folder** | Yes | Yes | 23 | | **Login Automatically** | Yes | Yes | 24 | | **Skip Setup Assistant** | Yes | Yes | 25 | | **Abort if user account already exists** | Yes | Yes | 26 | 27 | ### * macOS Mojave 10.14 Caveats: 28 | 29 | * Supported when [System Integrity Protection (SIP)](https://support.apple.com/en-us/HT204899) is **disabled** 30 | * When SIP is **enabled**, attempting to update the **User ID** and **Home Directory** attributes will result in the following prompts: 31 | 32 | | **Scripts** | **Packages** | 33 | | :--------------------------------------------------------: | :----------------------------------------------------------: | 34 | | ![Mojave Scripts](Readme%20Resources/Mojave%20Scripts.png) | ![Mojave Packages](Readme%20Resources/Mojave%20Packages.png) | 35 | 36 | ## Under the hood (the nerdy stuff) 37 | * Export a user account as a **Python script (PY):** 38 | * The user account password is hashed using [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2). Your password does not appear in plain text anywhere! 39 | * The user account picture is embedded into the python script (base64). No need for a separate image file! 40 | * Export a user account as an **Installer Package (PKG):** 41 | * Package is payload free - it uses the script export mentioned above as a **postinstall** script within the package. 42 | * Package will be signed with a [Developer ID Installer Certificate](https://developer.apple.com/developer-id/) (if selected) during export: 43 | 44 | ![Export](Readme%20Resources/Export.png) 45 | 46 | ## Things I would love some help with 47 | * [ ] Ability for the user account to be allowed to unlock FileVault (FDE). 48 | 49 | ## Requirements 50 | * Written in Swift 5.0.1. 51 | * Built using Xcode 10.2.1. 52 | * Builds run on OS X El Capitan 10.11 or later. 53 | 54 | ## Download 55 | Grab the latest version of MacUserGenerator from the [releases page](https://github.com/ninxsoft/MacUserGenerator/releases). 56 | 57 | ## Credits / Thank You 58 | * Project created and maintained by Nindi Gill ([ninxsoft](https://github.com/ninxsoft)). 59 | * Per Olofsson ([MagerValp](https://github.com/MagerValp)) for his blessing and advice on the project. 60 | * Greg Neagle ([gregneagle](https://github.com/gregneagle)) for his work on [pycreateuserpackage](https://github.com/gregneagle/pycreateuserpkg) - his project has really helped me understand how to communicate with the OS via the OpenDirectory API. 61 | * Marcus Ransom ([@marcusransom](https://twitter.com/marcusransom)) for his advice and help with testing. 62 | 63 | ## Version History 64 | * 0.3 65 | * Export script rewritten in **Python** 66 | * Home directory is now created correctly 67 | * Full support for macOS Mojave **10.14**! 68 | * User account creation will abort if **username** or **uid** already exists 69 | * Dark appearance support 70 | * Removed all unnecessary Setup Assistant checkboxes 71 | * Full name conversion to account no longer capitalises 72 | * A new UUID is generated every time the exported package / script is run 73 | * Xcode project updated to support Swift 5.0 74 | * 0.2 75 | * Selecting Administrator for **Account Type** should now work correctly 76 | * Added option to skip the **Data & Privacy** screen 77 | * Photo support 78 | * Rewrote the export script: 79 | * No longer uses `defaults write` 80 | * Uses `PlistBuddy` instead 81 | * 0.1 82 | * Initial release 83 | 84 | ## License 85 | Copyright © 2021 Nindi Gill 86 | 87 | Permission is hereby granted, free of charge, to any person obtaining a copy 88 | of this software and associated documentation files (the "Software"), to deal 89 | in the Software without restriction, including without limitation the rights 90 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 91 | copies of the Software, and to permit persons to whom the Software is 92 | furnished to do so, subject to the following conditions: 93 | 94 | The above copyright notice and this permission notice shall be included in all 95 | copies or substantial portions of the Software. 96 | 97 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 98 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 99 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 100 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 101 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 102 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 103 | SOFTWARE. 104 | -------------------------------------------------------------------------------- /Readme Resources/Export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninxsoft/MacUserGenerator/9976c6f8f7975568ffc857c968eed133574e4516/Readme Resources/Export.png -------------------------------------------------------------------------------- /Readme Resources/Mojave Packages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninxsoft/MacUserGenerator/9976c6f8f7975568ffc857c968eed133574e4516/Readme Resources/Mojave Packages.png -------------------------------------------------------------------------------- /Readme Resources/Mojave Scripts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninxsoft/MacUserGenerator/9976c6f8f7975568ffc857c968eed133574e4516/Readme Resources/Mojave Scripts.png -------------------------------------------------------------------------------- /Readme Resources/Sample User Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninxsoft/MacUserGenerator/9976c6f8f7975568ffc857c968eed133574e4516/Readme Resources/Sample User Dark.png -------------------------------------------------------------------------------- /Readme Resources/Sample User Light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ninxsoft/MacUserGenerator/9976c6f8f7975568ffc857c968eed133574e4516/Readme Resources/Sample User Light.png --------------------------------------------------------------------------------