├── .gitignore
├── LICENSE
├── README.md
├── RecipeBuilder.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ ├── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ ├── WorkspaceSettings.xcsettings
│ │ └── swiftpm
│ │ │ └── Package.resolved
│ └── xcuserdata
│ │ └── mikael.xcuserdatad
│ │ ├── UserInterfaceState.xcuserstate
│ │ └── WorkspaceSettings.xcsettings
├── xcshareddata
│ └── xcschemes
│ │ └── RecipeBuilder.xcscheme
└── xcuserdata
│ └── mikael.xcuserdatad
│ ├── xcdebugger
│ └── Breakpoints_v2.xcbkptlist
│ └── xcschemes
│ └── xcschememanagement.plist
├── RecipeBuilder
├── AppDelegate.swift
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── RecipeBuilder_icon-1024.png
│ │ ├── RecipeBuilder_icon-128.png
│ │ ├── RecipeBuilder_icon-16.png
│ │ ├── RecipeBuilder_icon-256.png
│ │ ├── RecipeBuilder_icon-257.png
│ │ ├── RecipeBuilder_icon-32.png
│ │ ├── RecipeBuilder_icon-33.png
│ │ ├── RecipeBuilder_icon-512.png
│ │ ├── RecipeBuilder_icon-513.png
│ │ └── RecipeBuilder_icon-64.png
│ ├── Contents.json
│ └── Warning.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
├── Base.lproj
│ └── MainMenu.xib
├── Buttons.swift
├── Buttons3rdParty.swift
├── CheckAndVerify.swift
├── Functions.swift
├── Info.plist
├── JamfUploaderHelpTexts.swift
├── RecipeBuilder.entitlements
├── UserButtons.swift
├── XMLtoYaml.swift
├── XMLtoYamlSnippets.swift
└── YamlToXML.swift
└── images
├── recipeBuildericon.png
└── recipebuilderinterface.png
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Mikael Löfgren
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | # RecipeBuilder
3 | - Click and create recipes (Choose File - New to get started)
4 | - Open recipes for editing (it always open recipes as a copy, so save and replace when done)
5 | - Shows processor-info as helptext when you click a processor button, sometimes with additional extra note
6 | - Finalize, lint xml and remove XML comments
7 | - Save and auto-pkg run from app
8 | - Save and open in external editor
9 | - Search recipes content to find processors usage
10 | - Open autopkg cache folder
11 | - Create your own buttons
12 | - Check and verify recipes files
13 | - Switch mode between XML and YAML
14 | - Batch convert XML to YAML or YAML to XML
15 | - Set Autopkg Trustinfo
16 |
17 |
18 | System requirements: macOS 11 or later
19 | Dependencies: Highlightr and Yams
20 | Icon: vecteezy.com
21 |
22 | Download: https://github.com/mikaellofgren/RecipeBuilder/releases
23 |
24 | 
25 |
26 | # User buttons
27 | In folder "~/Library/Application Support/RecipeBuilder"
28 | you create folders with name 1,2,3..up to 10. You can skip folders, but folder 1 is required if you
29 | want buttons to autostart, otherwise you have to enable them.
30 | In every folder create title.txt and add text to the file for the button name.
31 | Keep it below 26 characters for best result.
32 |
33 | Add output.txt for the output text.
34 |
35 | And help.txt for the Note text your reading right now (optional).
36 | To easy open "~/Library/Application Support/RecipeBuilder", choose File options - Open user buttons folder
37 | Click the Enable and Reload button to create the first "demo" button.
38 | Use that button to reload if you trying out your new buttons.
--------------------------------------------------------------------------------
/RecipeBuilder.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 54;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 7300825A245308B100F4909F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73008259245308B100F4909F /* AppDelegate.swift */; };
11 | 7300825C245308B600F4909F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7300825B245308B600F4909F /* Assets.xcassets */; };
12 | 7300825F245308B600F4909F /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7300825D245308B600F4909F /* MainMenu.xib */; };
13 | 73008268245309F200F4909F /* Buttons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73008267245309F200F4909F /* Buttons.swift */; };
14 | 733DE0042D160A9800F5CB2D /* XMLtoYaml.swift in Sources */ = {isa = PBXBuildFile; fileRef = 733DE0032D160A8C00F5CB2D /* XMLtoYaml.swift */; };
15 | 733DE0072D197D0D00F5CB2D /* Yams in Frameworks */ = {isa = PBXBuildFile; productRef = 733DE0062D197D0D00F5CB2D /* Yams */; };
16 | 733DE00D2D1D596B00F5CB2D /* YamlToXML.swift in Sources */ = {isa = PBXBuildFile; fileRef = 733DE00C2D1D596B00F5CB2D /* YamlToXML.swift */; };
17 | 733DE00F2D1D863400F5CB2D /* XMLtoYamlSnippets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 733DE00E2D1D863400F5CB2D /* XMLtoYamlSnippets.swift */; };
18 | 734EF16724B2582200C203C9 /* UserButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 734EF16624B2582200C203C9 /* UserButtons.swift */; };
19 | 7352335927CBF82C004FAA8C /* Buttons3rdParty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7352335827CBF82C004FAA8C /* Buttons3rdParty.swift */; };
20 | 7352335B27D2981C004FAA8C /* JamfUploaderHelpTexts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7352335A27D2981C004FAA8C /* JamfUploaderHelpTexts.swift */; };
21 | 73631C1024587182008A6491 /* Highlightr in Frameworks */ = {isa = PBXBuildFile; productRef = 73631C0F24587182008A6491 /* Highlightr */; };
22 | 73A9CE74247A5D2600627DA8 /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73A9CE73247A5D2600627DA8 /* Functions.swift */; };
23 | 73B2E40D27D51F9800CFEA88 /* CheckAndVerify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73B2E40C27D51F9800CFEA88 /* CheckAndVerify.swift */; };
24 | /* End PBXBuildFile section */
25 |
26 | /* Begin PBXFileReference section */
27 | 73008256245308B100F4909F /* RecipeBuilder.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RecipeBuilder.app; sourceTree = BUILT_PRODUCTS_DIR; };
28 | 73008259245308B100F4909F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
29 | 7300825B245308B600F4909F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
30 | 7300825E245308B600F4909F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; };
31 | 73008260245308B600F4909F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
32 | 73008261245308B600F4909F /* RecipeBuilder.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RecipeBuilder.entitlements; sourceTree = ""; };
33 | 73008267245309F200F4909F /* Buttons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Buttons.swift; sourceTree = ""; };
34 | 733DE0032D160A8C00F5CB2D /* XMLtoYaml.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMLtoYaml.swift; sourceTree = ""; };
35 | 733DE00C2D1D596B00F5CB2D /* YamlToXML.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YamlToXML.swift; sourceTree = ""; };
36 | 733DE00E2D1D863400F5CB2D /* XMLtoYamlSnippets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMLtoYamlSnippets.swift; sourceTree = ""; };
37 | 734EF16624B2582200C203C9 /* UserButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserButtons.swift; sourceTree = ""; };
38 | 7352335827CBF82C004FAA8C /* Buttons3rdParty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Buttons3rdParty.swift; sourceTree = ""; };
39 | 7352335A27D2981C004FAA8C /* JamfUploaderHelpTexts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JamfUploaderHelpTexts.swift; sourceTree = ""; };
40 | 73A9CE73247A5D2600627DA8 /* Functions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Functions.swift; sourceTree = ""; };
41 | 73B2E40C27D51F9800CFEA88 /* CheckAndVerify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckAndVerify.swift; sourceTree = ""; };
42 | /* End PBXFileReference section */
43 |
44 | /* Begin PBXFrameworksBuildPhase section */
45 | 73008253245308B100F4909F /* Frameworks */ = {
46 | isa = PBXFrameworksBuildPhase;
47 | buildActionMask = 2147483647;
48 | files = (
49 | 733DE0072D197D0D00F5CB2D /* Yams in Frameworks */,
50 | 73631C1024587182008A6491 /* Highlightr in Frameworks */,
51 | );
52 | runOnlyForDeploymentPostprocessing = 0;
53 | };
54 | /* End PBXFrameworksBuildPhase section */
55 |
56 | /* Begin PBXGroup section */
57 | 7300824D245308B100F4909F = {
58 | isa = PBXGroup;
59 | children = (
60 | 73008258245308B100F4909F /* RecipeBuilder */,
61 | 73008257245308B100F4909F /* Products */,
62 | );
63 | sourceTree = "";
64 | };
65 | 73008257245308B100F4909F /* Products */ = {
66 | isa = PBXGroup;
67 | children = (
68 | 73008256245308B100F4909F /* RecipeBuilder.app */,
69 | );
70 | name = Products;
71 | sourceTree = "";
72 | };
73 | 73008258245308B100F4909F /* RecipeBuilder */ = {
74 | isa = PBXGroup;
75 | children = (
76 | 73008259245308B100F4909F /* AppDelegate.swift */,
77 | 7352335A27D2981C004FAA8C /* JamfUploaderHelpTexts.swift */,
78 | 734EF16624B2582200C203C9 /* UserButtons.swift */,
79 | 73008267245309F200F4909F /* Buttons.swift */,
80 | 7352335827CBF82C004FAA8C /* Buttons3rdParty.swift */,
81 | 73A9CE73247A5D2600627DA8 /* Functions.swift */,
82 | 733DE00C2D1D596B00F5CB2D /* YamlToXML.swift */,
83 | 733DE0032D160A8C00F5CB2D /* XMLtoYaml.swift */,
84 | 733DE00E2D1D863400F5CB2D /* XMLtoYamlSnippets.swift */,
85 | 73B2E40C27D51F9800CFEA88 /* CheckAndVerify.swift */,
86 | 7300825B245308B600F4909F /* Assets.xcassets */,
87 | 7300825D245308B600F4909F /* MainMenu.xib */,
88 | 73008260245308B600F4909F /* Info.plist */,
89 | 73008261245308B600F4909F /* RecipeBuilder.entitlements */,
90 | );
91 | path = RecipeBuilder;
92 | sourceTree = "";
93 | };
94 | /* End PBXGroup section */
95 |
96 | /* Begin PBXNativeTarget section */
97 | 73008255245308B100F4909F /* RecipeBuilder */ = {
98 | isa = PBXNativeTarget;
99 | buildConfigurationList = 73008264245308B600F4909F /* Build configuration list for PBXNativeTarget "RecipeBuilder" */;
100 | buildPhases = (
101 | 73008252245308B100F4909F /* Sources */,
102 | 73008253245308B100F4909F /* Frameworks */,
103 | 73008254245308B100F4909F /* Resources */,
104 | );
105 | buildRules = (
106 | );
107 | dependencies = (
108 | );
109 | name = RecipeBuilder;
110 | packageProductDependencies = (
111 | 73631C0F24587182008A6491 /* Highlightr */,
112 | 733DE0062D197D0D00F5CB2D /* Yams */,
113 | );
114 | productName = autopkgRecipeBuilder;
115 | productReference = 73008256245308B100F4909F /* RecipeBuilder.app */;
116 | productType = "com.apple.product-type.application";
117 | };
118 | /* End PBXNativeTarget section */
119 |
120 | /* Begin PBXProject section */
121 | 7300824E245308B100F4909F /* Project object */ = {
122 | isa = PBXProject;
123 | attributes = {
124 | BuildIndependentTargetsInParallel = YES;
125 | LastSwiftUpdateCheck = 1130;
126 | LastUpgradeCheck = 1620;
127 | ORGANIZATIONNAME = "Mikael Löfgren";
128 | TargetAttributes = {
129 | 73008255245308B100F4909F = {
130 | CreatedOnToolsVersion = 11.3.1;
131 | };
132 | };
133 | };
134 | buildConfigurationList = 73008251245308B100F4909F /* Build configuration list for PBXProject "RecipeBuilder" */;
135 | compatibilityVersion = "Xcode 9.3";
136 | developmentRegion = en;
137 | hasScannedForEncodings = 0;
138 | knownRegions = (
139 | en,
140 | Base,
141 | );
142 | mainGroup = 7300824D245308B100F4909F;
143 | packageReferences = (
144 | 73631C0E24587182008A6491 /* XCRemoteSwiftPackageReference "Highlightr" */,
145 | 733DE0052D197D0D00F5CB2D /* XCRemoteSwiftPackageReference "Yams" */,
146 | );
147 | productRefGroup = 73008257245308B100F4909F /* Products */;
148 | projectDirPath = "";
149 | projectRoot = "";
150 | targets = (
151 | 73008255245308B100F4909F /* RecipeBuilder */,
152 | );
153 | };
154 | /* End PBXProject section */
155 |
156 | /* Begin PBXResourcesBuildPhase section */
157 | 73008254245308B100F4909F /* Resources */ = {
158 | isa = PBXResourcesBuildPhase;
159 | buildActionMask = 2147483647;
160 | files = (
161 | 7300825C245308B600F4909F /* Assets.xcassets in Resources */,
162 | 7300825F245308B600F4909F /* MainMenu.xib in Resources */,
163 | );
164 | runOnlyForDeploymentPostprocessing = 0;
165 | };
166 | /* End PBXResourcesBuildPhase section */
167 |
168 | /* Begin PBXSourcesBuildPhase section */
169 | 73008252245308B100F4909F /* Sources */ = {
170 | isa = PBXSourcesBuildPhase;
171 | buildActionMask = 2147483647;
172 | files = (
173 | 7300825A245308B100F4909F /* AppDelegate.swift in Sources */,
174 | 73008268245309F200F4909F /* Buttons.swift in Sources */,
175 | 733DE0042D160A9800F5CB2D /* XMLtoYaml.swift in Sources */,
176 | 733DE00D2D1D596B00F5CB2D /* YamlToXML.swift in Sources */,
177 | 73B2E40D27D51F9800CFEA88 /* CheckAndVerify.swift in Sources */,
178 | 7352335927CBF82C004FAA8C /* Buttons3rdParty.swift in Sources */,
179 | 7352335B27D2981C004FAA8C /* JamfUploaderHelpTexts.swift in Sources */,
180 | 734EF16724B2582200C203C9 /* UserButtons.swift in Sources */,
181 | 73A9CE74247A5D2600627DA8 /* Functions.swift in Sources */,
182 | 733DE00F2D1D863400F5CB2D /* XMLtoYamlSnippets.swift in Sources */,
183 | );
184 | runOnlyForDeploymentPostprocessing = 0;
185 | };
186 | /* End PBXSourcesBuildPhase section */
187 |
188 | /* Begin PBXVariantGroup section */
189 | 7300825D245308B600F4909F /* MainMenu.xib */ = {
190 | isa = PBXVariantGroup;
191 | children = (
192 | 7300825E245308B600F4909F /* Base */,
193 | );
194 | name = MainMenu.xib;
195 | sourceTree = "";
196 | };
197 | /* End PBXVariantGroup section */
198 |
199 | /* Begin XCBuildConfiguration section */
200 | 73008262245308B600F4909F /* Debug */ = {
201 | isa = XCBuildConfiguration;
202 | buildSettings = {
203 | ALWAYS_SEARCH_USER_PATHS = NO;
204 | CLANG_ANALYZER_NONNULL = YES;
205 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
206 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
207 | CLANG_CXX_LIBRARY = "libc++";
208 | CLANG_ENABLE_MODULES = YES;
209 | CLANG_ENABLE_OBJC_ARC = YES;
210 | CLANG_ENABLE_OBJC_WEAK = YES;
211 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
212 | CLANG_WARN_BOOL_CONVERSION = YES;
213 | CLANG_WARN_COMMA = YES;
214 | CLANG_WARN_CONSTANT_CONVERSION = YES;
215 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
216 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
217 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
218 | CLANG_WARN_EMPTY_BODY = YES;
219 | CLANG_WARN_ENUM_CONVERSION = YES;
220 | CLANG_WARN_INFINITE_RECURSION = YES;
221 | CLANG_WARN_INT_CONVERSION = YES;
222 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
223 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
224 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
225 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
226 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
227 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
228 | CLANG_WARN_STRICT_PROTOTYPES = YES;
229 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
230 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
231 | CLANG_WARN_UNREACHABLE_CODE = YES;
232 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
233 | COPY_PHASE_STRIP = NO;
234 | DEAD_CODE_STRIPPING = YES;
235 | DEBUG_INFORMATION_FORMAT = dwarf;
236 | ENABLE_HARDENED_RUNTIME = YES;
237 | ENABLE_STRICT_OBJC_MSGSEND = YES;
238 | ENABLE_TESTABILITY = YES;
239 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
240 | GCC_C_LANGUAGE_STANDARD = gnu11;
241 | GCC_DYNAMIC_NO_PIC = NO;
242 | GCC_NO_COMMON_BLOCKS = YES;
243 | GCC_OPTIMIZATION_LEVEL = 0;
244 | GCC_PREPROCESSOR_DEFINITIONS = (
245 | "DEBUG=1",
246 | "$(inherited)",
247 | );
248 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
249 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
250 | GCC_WARN_UNDECLARED_SELECTOR = YES;
251 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
252 | GCC_WARN_UNUSED_FUNCTION = YES;
253 | GCC_WARN_UNUSED_VARIABLE = YES;
254 | MACOSX_DEPLOYMENT_TARGET = 10.14;
255 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
256 | MTL_FAST_MATH = YES;
257 | ONLY_ACTIVE_ARCH = YES;
258 | SDKROOT = macosx;
259 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
260 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
261 | };
262 | name = Debug;
263 | };
264 | 73008263245308B600F4909F /* Release */ = {
265 | isa = XCBuildConfiguration;
266 | buildSettings = {
267 | ALWAYS_SEARCH_USER_PATHS = NO;
268 | CLANG_ANALYZER_NONNULL = YES;
269 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
270 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
271 | CLANG_CXX_LIBRARY = "libc++";
272 | CLANG_ENABLE_MODULES = YES;
273 | CLANG_ENABLE_OBJC_ARC = YES;
274 | CLANG_ENABLE_OBJC_WEAK = YES;
275 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
276 | CLANG_WARN_BOOL_CONVERSION = YES;
277 | CLANG_WARN_COMMA = YES;
278 | CLANG_WARN_CONSTANT_CONVERSION = YES;
279 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
280 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
281 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
282 | CLANG_WARN_EMPTY_BODY = YES;
283 | CLANG_WARN_ENUM_CONVERSION = YES;
284 | CLANG_WARN_INFINITE_RECURSION = YES;
285 | CLANG_WARN_INT_CONVERSION = YES;
286 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
287 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
288 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
289 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
290 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
291 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
292 | CLANG_WARN_STRICT_PROTOTYPES = YES;
293 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
294 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
295 | CLANG_WARN_UNREACHABLE_CODE = YES;
296 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
297 | COPY_PHASE_STRIP = NO;
298 | DEAD_CODE_STRIPPING = YES;
299 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
300 | ENABLE_HARDENED_RUNTIME = YES;
301 | ENABLE_NS_ASSERTIONS = NO;
302 | ENABLE_STRICT_OBJC_MSGSEND = YES;
303 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
304 | GCC_C_LANGUAGE_STANDARD = gnu11;
305 | GCC_NO_COMMON_BLOCKS = YES;
306 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
307 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
308 | GCC_WARN_UNDECLARED_SELECTOR = YES;
309 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
310 | GCC_WARN_UNUSED_FUNCTION = YES;
311 | GCC_WARN_UNUSED_VARIABLE = YES;
312 | MACOSX_DEPLOYMENT_TARGET = 10.14;
313 | MTL_ENABLE_DEBUG_INFO = NO;
314 | MTL_FAST_MATH = YES;
315 | SDKROOT = macosx;
316 | SWIFT_COMPILATION_MODE = wholemodule;
317 | SWIFT_OPTIMIZATION_LEVEL = "-O";
318 | };
319 | name = Release;
320 | };
321 | 73008265245308B600F4909F /* Debug */ = {
322 | isa = XCBuildConfiguration;
323 | buildSettings = {
324 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
325 | CODE_SIGN_ENTITLEMENTS = RecipeBuilder/RecipeBuilder.entitlements;
326 | CODE_SIGN_IDENTITY = "Apple Development";
327 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
328 | CODE_SIGN_STYLE = Automatic;
329 | COMBINE_HIDPI_IMAGES = YES;
330 | DEAD_CODE_STRIPPING = YES;
331 | DEVELOPMENT_TEAM = F489D96499;
332 | ENABLE_HARDENED_RUNTIME = YES;
333 | INFOPLIST_FILE = "$(SRCROOT)/RecipeBuilder/Info.plist";
334 | INFOPLIST_KEY_CFBundleDisplayName = RecipeBuilder;
335 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
336 | LD_RUNPATH_SEARCH_PATHS = (
337 | "$(inherited)",
338 | "@executable_path/../Frameworks",
339 | );
340 | MACOSX_DEPLOYMENT_TARGET = 11.0;
341 | MARKETING_VERSION = 1.05;
342 | PRODUCT_BUNDLE_IDENTIFIER = se.mikaellofgren.RecipeBuilder;
343 | PRODUCT_NAME = "$(TARGET_NAME)";
344 | PROVISIONING_PROFILE_SPECIFIER = "";
345 | SWIFT_VERSION = 5.0;
346 | };
347 | name = Debug;
348 | };
349 | 73008266245308B600F4909F /* Release */ = {
350 | isa = XCBuildConfiguration;
351 | buildSettings = {
352 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
353 | CODE_SIGN_ENTITLEMENTS = RecipeBuilder/RecipeBuilder.entitlements;
354 | CODE_SIGN_IDENTITY = "-";
355 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
356 | CODE_SIGN_STYLE = Automatic;
357 | COMBINE_HIDPI_IMAGES = YES;
358 | DEAD_CODE_STRIPPING = YES;
359 | DEVELOPMENT_TEAM = F489D96499;
360 | ENABLE_HARDENED_RUNTIME = YES;
361 | INFOPLIST_FILE = "$(SRCROOT)/RecipeBuilder/Info.plist";
362 | INFOPLIST_KEY_CFBundleDisplayName = RecipeBuilder;
363 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
364 | LD_RUNPATH_SEARCH_PATHS = (
365 | "$(inherited)",
366 | "@executable_path/../Frameworks",
367 | );
368 | MACOSX_DEPLOYMENT_TARGET = 11.0;
369 | MARKETING_VERSION = 1.05;
370 | PRODUCT_BUNDLE_IDENTIFIER = se.mikaellofgren.RecipeBuilder;
371 | PRODUCT_NAME = "$(TARGET_NAME)";
372 | PROVISIONING_PROFILE_SPECIFIER = "";
373 | SWIFT_VERSION = 5.0;
374 | };
375 | name = Release;
376 | };
377 | /* End XCBuildConfiguration section */
378 |
379 | /* Begin XCConfigurationList section */
380 | 73008251245308B100F4909F /* Build configuration list for PBXProject "RecipeBuilder" */ = {
381 | isa = XCConfigurationList;
382 | buildConfigurations = (
383 | 73008262245308B600F4909F /* Debug */,
384 | 73008263245308B600F4909F /* Release */,
385 | );
386 | defaultConfigurationIsVisible = 0;
387 | defaultConfigurationName = Release;
388 | };
389 | 73008264245308B600F4909F /* Build configuration list for PBXNativeTarget "RecipeBuilder" */ = {
390 | isa = XCConfigurationList;
391 | buildConfigurations = (
392 | 73008265245308B600F4909F /* Debug */,
393 | 73008266245308B600F4909F /* Release */,
394 | );
395 | defaultConfigurationIsVisible = 0;
396 | defaultConfigurationName = Release;
397 | };
398 | /* End XCConfigurationList section */
399 |
400 | /* Begin XCRemoteSwiftPackageReference section */
401 | 733DE0052D197D0D00F5CB2D /* XCRemoteSwiftPackageReference "Yams" */ = {
402 | isa = XCRemoteSwiftPackageReference;
403 | repositoryURL = "https://github.com/jpsim/Yams.git";
404 | requirement = {
405 | kind = upToNextMajorVersion;
406 | minimumVersion = 5.1.3;
407 | };
408 | };
409 | 73631C0E24587182008A6491 /* XCRemoteSwiftPackageReference "Highlightr" */ = {
410 | isa = XCRemoteSwiftPackageReference;
411 | repositoryURL = "https://github.com/helje5/Highlightr.git";
412 | requirement = {
413 | kind = upToNextMajorVersion;
414 | minimumVersion = 3.0.0;
415 | };
416 | };
417 | /* End XCRemoteSwiftPackageReference section */
418 |
419 | /* Begin XCSwiftPackageProductDependency section */
420 | 733DE0062D197D0D00F5CB2D /* Yams */ = {
421 | isa = XCSwiftPackageProductDependency;
422 | package = 733DE0052D197D0D00F5CB2D /* XCRemoteSwiftPackageReference "Yams" */;
423 | productName = Yams;
424 | };
425 | 73631C0F24587182008A6491 /* Highlightr */ = {
426 | isa = XCSwiftPackageProductDependency;
427 | package = 73631C0E24587182008A6491 /* XCRemoteSwiftPackageReference "Highlightr" */;
428 | productName = Highlightr;
429 | };
430 | /* End XCSwiftPackageProductDependency section */
431 | };
432 | rootObject = 7300824E245308B100F4909F /* Project object */;
433 | }
434 |
--------------------------------------------------------------------------------
/RecipeBuilder.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/RecipeBuilder.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/RecipeBuilder.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/RecipeBuilder.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "originHash" : "d6664d9d6d3791ee76cab980eddff700b5ba91415e6a27a99334e67f0c0af1e6",
3 | "pins" : [
4 | {
5 | "identity" : "highlightr",
6 | "kind" : "remoteSourceControl",
7 | "location" : "https://github.com/helje5/Highlightr.git",
8 | "state" : {
9 | "revision" : "628094ff81cdebf31da6bf31be4b4ac758c127a4",
10 | "version" : "3.0.0"
11 | }
12 | },
13 | {
14 | "identity" : "yams",
15 | "kind" : "remoteSourceControl",
16 | "location" : "https://github.com/jpsim/Yams.git",
17 | "state" : {
18 | "revision" : "3036ba9d69cf1fd04d433527bc339dc0dc75433d",
19 | "version" : "5.1.3"
20 | }
21 | }
22 | ],
23 | "version" : 3
24 | }
25 |
--------------------------------------------------------------------------------
/RecipeBuilder.xcodeproj/project.xcworkspace/xcuserdata/mikael.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder.xcodeproj/project.xcworkspace/xcuserdata/mikael.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/RecipeBuilder.xcodeproj/project.xcworkspace/xcuserdata/mikael.xcuserdatad/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | BuildLocationStyle
6 | UseAppPreferences
7 | CustomBuildLocationType
8 | RelativeToDerivedData
9 | DerivedDataLocationStyle
10 | Default
11 | IssueFilterStyle
12 | ShowActiveSchemeOnly
13 | LiveSourceIssuesEnabled
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/RecipeBuilder.xcodeproj/xcshareddata/xcschemes/RecipeBuilder.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
44 |
46 |
52 |
53 |
54 |
55 |
61 |
63 |
69 |
70 |
71 |
72 |
74 |
75 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/RecipeBuilder.xcodeproj/xcuserdata/mikael.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
--------------------------------------------------------------------------------
/RecipeBuilder.xcodeproj/xcuserdata/mikael.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | RecipeBuilder.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 | autopkgRecipeBuilder.xcscheme_^#shared#^_
13 |
14 | orderHint
15 | 0
16 |
17 |
18 | SuppressBuildableAutocreation
19 |
20 | 73008255245308B100F4909F
21 |
22 | primary
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/RecipeBuilder/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | //
4 | // Created by Mikael Löfgren on 2024-12-27
5 | // Copyright © 2024 Mikael Löfgren. All rights reserved.
6 | //
7 |
8 | import Cocoa
9 | import AppKit
10 | import Highlightr
11 | @NSApplicationMain
12 | class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
13 | var searchSelectedRecipe = ""
14 | var saveAndOpenExternalEditor = "false"
15 | var selectedExternalEditor = "BBEdit"
16 | var trustInfo = ""
17 |
18 | var recipeDirectlyFileName: String = ""
19 |
20 | @IBOutlet weak var window: NSWindow!
21 |
22 | @IBOutlet var processorsView: NSView!
23 | @IBOutlet var processorsView3rdParty: NSView!
24 | @IBOutlet weak var buttonView: NSView!
25 | @IBOutlet var recipeIdentifierTextField: NSTextField!
26 | @IBOutlet var appPKGTextField: NSTextField!
27 | @IBOutlet var outputTextField: NSTextView!
28 | @IBOutlet var recipeFormatPopup: NSPopUpButton!
29 | @IBOutlet var openFile: NSMenuItem!
30 | @IBOutlet var saveDocument: NSMenuItem!
31 | @IBOutlet var newDocument: NSMenuItem!
32 | @IBOutlet var matchingRecipes: NSPopUpButton!
33 | @IBOutlet var searchField: NSSearchField!
34 | @IBOutlet var fileOptions: NSPopUpButton!
35 | @IBOutlet var finalizeMenuItem: NSMenuItem!
36 | @IBOutlet var trustInfoTrue: NSMenuItem!
37 | @IBOutlet var trustInfoFalse: NSMenuItem!
38 | @IBOutlet var bbedit: NSMenuItem!
39 | @IBOutlet var sublimeText: NSMenuItem!
40 | @IBOutlet var textMate: NSMenuItem!
41 | @IBOutlet var visualStudioCode: NSMenuItem!
42 |
43 | @IBOutlet var modeSwitch: NSSwitch!
44 | @IBOutlet var spinner: NSProgressIndicator!
45 | @IBOutlet var logWindow: NSPanel!
46 | @IBOutlet var logTextView: NSTextView!
47 | @IBOutlet var helpPopover: NSPopover!
48 | @IBOutlet var helpPopoverText: NSTextView!
49 | @IBOutlet weak var enableAndReloadButton: NSButton!
50 |
51 | @IBAction func getIdentifierTextValue(_ sender: NSTextField) {
52 | getIdentifierTextFieldsValues ()
53 | }
54 |
55 | @IBAction func setAppPKGvalue(_ sender: NSTextField) {
56 | getAppPkgTextFieldsValues ()
57 | }
58 |
59 | @IBAction func startSearch(_ sender: NSSearchField) {
60 | searchString = sender.stringValue
61 |
62 | if searchString == "" {
63 | appDelegate().matchingRecipes.removeAllItems()
64 | appDelegate().matchingRecipes.addItem(withTitle: "Matching recipes")
65 | appDelegate().matchingRecipes.selectItem(at: 0)
66 | return
67 | } else {
68 | getAutopkgPlistValues ()
69 | appDelegate().matchingRecipes.removeAllItems()
70 | appDelegate().matchingRecipes.addItem(withTitle: "Searching...")
71 | appDelegate().matchingRecipes.selectItem(at: 0)
72 | appDelegate().spinner.isHidden=false
73 | self.spinner.startAnimation(self)
74 | }
75 |
76 | }
77 |
78 | @IBAction func selectedRecipe(_ sender: NSPopUpButton) {
79 | searchSelectedRecipe = matchingRecipes.titleOfSelectedItem!
80 | let searchSelectedFileURL = URL(fileURLWithPath: appDelegate().searchSelectedRecipe)
81 | openFileInExternalEditor(fileURL: searchSelectedFileURL)
82 | }
83 |
84 | @IBAction func newDocumentAction(_ sender: NSMenuItem) {
85 | createNewDocument ()
86 | }
87 |
88 | @IBAction func openFileAction(_ sender: NSMenuItem) {
89 | openRecipe ()
90 | }
91 |
92 | func application(_ sender: NSApplication, openFile recipeDirectlyFileName: String) -> Bool {
93 | self.recipeDirectlyFileName = recipeDirectlyFileName
94 | openRecipeDirectly ()
95 | return true
96 | }
97 |
98 | @IBAction func saveDocumentAction(_ sender: NSMenuItem) {
99 | saveRecipe ()
100 | }
101 |
102 | @IBAction func finalizeDoc(_ sender: NSMenuItem) {
103 | finalizeDocument ()
104 | }
105 |
106 | @IBAction func verifyRecipes(_ sender: NSMenuItem) {
107 | startCheckAndVerify ()
108 | }
109 |
110 | @IBAction func autopkgRun(_ sender: NSMenuItem) {
111 | appDelegate().spinner.isHidden=false
112 | self.spinner.startAnimation(self)
113 | autoPkgRunner ()
114 | }
115 |
116 | @IBAction func openAutoPkgCacheFolder(_ sender: NSMenuItem) {
117 | openAutoPkgCache ()
118 | }
119 |
120 |
121 | @IBAction func openUserButtonsFolder(_ sender: NSMenuItem) {
122 | openUserButtons ()
123 | }
124 |
125 | @IBAction func trustInfoTrue(_ sender: NSMenuItem) {
126 | setTrustInfo(to: true)
127 | trustInfo = sender.title
128 | toggleTrustInfo()
129 | }
130 |
131 | @IBAction func trustInfoFalse(_ sender: NSMenuItem) {
132 | setTrustInfo(to: false)
133 | trustInfo = sender.title.lowercased()
134 | toggleTrustInfo()
135 | }
136 |
137 | @IBAction func batchXMLtoYaml(_ sender: NSMenuItem) {
138 | selectFolderAndProcessRecipesToYaml()
139 | }
140 |
141 | @IBAction func batchYamltoXML(_ sender: NSMenuItem) {
142 | selectFolderAndProcessRecipesToXML()
143 | }
144 |
145 | @IBAction func openExternalBBEdit(_ sender: NSMenuItem) {
146 | saveAndOpenExternalEditor = "true"
147 | selectedExternalEditor = sender.title
148 | toggleExternalEditor ()
149 | saveRecipe ()
150 | }
151 |
152 | @IBAction func openExternalSublime(_ sender: NSMenuItem) {
153 | saveAndOpenExternalEditor = "true"
154 | selectedExternalEditor = sender.title
155 | toggleExternalEditor ()
156 | saveRecipe ()
157 | }
158 |
159 | @IBAction func openExternalTextMate(_ sender: NSMenuItem) {
160 | saveAndOpenExternalEditor = "true"
161 | selectedExternalEditor = sender.title
162 | toggleExternalEditor ()
163 | saveRecipe ()
164 | }
165 |
166 | @IBAction func openExternalVisualStudioCode (_ sender: NSMenuItem) {
167 | saveAndOpenExternalEditor = "true"
168 | selectedExternalEditor = sender.title
169 | toggleExternalEditor ()
170 | saveRecipe ()
171 | }
172 |
173 | @IBAction func modeSwitch(_ sender: Any) {
174 | yamlModeStatusSwitcher ()
175 | }
176 |
177 | @objc func appDMGVersionerAction (sender: NSButton) {
178 | appDMGVersioner ()
179 | helpPopover.close()
180 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
181 | writePopOvertext(processor: "AppDmgVersioner", extraHelpText: "Name of app found at the root of the disk image. This does not search recursively for a matching app. If you need to specify a path, use Versioner instead.")
182 | }
183 |
184 | @objc func appPkgCreatorAction (sender: NSButton) {
185 | appPkgCreator ()
186 | helpPopover.close()
187 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
188 | writePopOvertext(processor: "AppPkgCreator", extraHelpText: """
189 | This is defaults values that can be removed, unless you want another paths
190 | Arguments
191 |
192 | pkg_path
193 | %RECIPE_CACHE_DIR%/%app_name%-%version%.pkg
194 | app_path
195 | %RECIPE_CACHE_DIR%/%pathname%/*.app
196 |
197 |
198 | Often used in pkg.recipe
199 | """)
200 | }
201 |
202 | @objc func codeSignatureVerifierAppAction(sender: NSButton) {
203 | codeSignatureVerifierApp ()
204 | helpPopover.close()
205 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
206 | writePopOvertext(processor: "CodeSignatureVerifier", extraHelpText: "Used with download.recipes")
207 | }
208 |
209 | @objc func codeSignatureVerifierPkgAction(sender: NSButton) {
210 | codeSignatureVerifierPKG ()
211 | helpPopover.close()
212 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
213 | writePopOvertext(processor: "CodeSignatureVerifier", extraHelpText: "Used with download.recipes")
214 | }
215 |
216 | @objc func copierAction(sender: NSButton) {
217 | copier ()
218 | helpPopover.close()
219 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
220 | writePopOvertext(processor: "Copier", extraHelpText: "")
221 | }
222 |
223 | @objc func deprecationWarningAction(sender: NSButton) {
224 | deprecationWarning ()
225 | helpPopover.close()
226 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
227 | writePopOvertext(processor: "DeprecationWarning", extraHelpText: "This processor outputs a warning that the recipe has been deprecated.")
228 | }
229 |
230 | @objc func dmgCreatorAction(sender: NSButton) {
231 | dmgCreator ()
232 | helpPopover.close()
233 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
234 | writePopOvertext(processor: "DmgCreator", extraHelpText: "")
235 | }
236 |
237 | @objc func endOfCheckPhaseAction(sender: NSButton) {
238 | endOfCheckPhase ()
239 | helpPopover.close()
240 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
241 | writePopOvertext(processor: "EndOfCheckPhase", extraHelpText: "Recommended to be used directly after processsor URLDownloader")
242 | }
243 |
244 | @objc func fileCreatorAction(sender: NSButton) {
245 | fileCreator ()
246 | helpPopover.close()
247 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
248 | writePopOvertext(processor: "FileCreator", extraHelpText: "")
249 | }
250 |
251 | @objc func fileFinderAction(sender: NSButton) {
252 | fileFinder ()
253 | helpPopover.close()
254 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
255 | writePopOvertext(processor: "FileFinder", extraHelpText: "")
256 | }
257 |
258 | @objc func fileMoverAction(sender: NSButton) {
259 | fileMover ()
260 | helpPopover.close()
261 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
262 | writePopOvertext(processor: "FileMover", extraHelpText: "")
263 | }
264 |
265 | @objc func flatPkgPackerAction(sender: NSButton) {
266 | flatPkgPacker ()
267 | helpPopover.close()
268 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
269 | writePopOvertext(processor: "FlatPkgPacker", extraHelpText: "")
270 | }
271 |
272 | @objc func flatPkgUnpackerAction(sender: NSButton) {
273 | flatPkgUnpacker ()
274 | helpPopover.close()
275 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
276 | writePopOvertext(processor: "FlatPkgUnpacker", extraHelpText: "")
277 | }
278 |
279 | @objc func gitHubReleasesInfoProviderAction(sender: NSButton) {
280 | gitHubReleasesInfoProvider ()
281 | helpPopover.close()
282 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
283 | writePopOvertext(processor: "GitHubReleasesInfoProvider", extraHelpText: """
284 | Add as Input key to use the %GITHUB_REPO% variable
285 | GITHUB_REPO
286 | MagerValp/AutoDMG
287 | """)
288 | }
289 |
290 | @objc func installerAction(sender: NSButton) {
291 | installer ()
292 | helpPopover.close()
293 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
294 | writePopOvertext(processor: "Installer", extraHelpText: "Used with install.recipes")
295 | }
296 |
297 | @objc func installFromDMGAction(sender: NSButton) {
298 | installFromDMG ()
299 | helpPopover.close()
300 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
301 | writePopOvertext(processor: "InstallFromDMG", extraHelpText: "Used with install.recipes")
302 | }
303 |
304 | @objc func munkiCatalogBuilderAction(sender: NSButton) {
305 | munkiCatalogBuilder ()
306 | helpPopover.close()
307 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
308 | writePopOvertext(processor: "MunkiCatalogBuilder", extraHelpText: "")
309 | }
310 |
311 | @objc func munkiImporterAction(sender: NSButton) {
312 | munkiImporter ()
313 | helpPopover.close()
314 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
315 | writePopOvertext(processor: "MunkiImporter", extraHelpText: "")
316 | }
317 |
318 | @objc func munkiInfoCreatorAction(sender: NSButton) {
319 | munkiInfoCreator ()
320 | helpPopover.close()
321 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
322 | writePopOvertext(processor: "MunkiInfoCreator", extraHelpText: "")
323 | }
324 |
325 | @objc func MunkiInstallsItemsCreatorAction(sender: NSButton) {
326 | munkiInstallsItemsCreator ()
327 | helpPopover.close()
328 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
329 | writePopOvertext(processor: "MunkiInstallsItemsCreator", extraHelpText: "The faux_root path must include the installs_items_path %RECIPE_CACHE_DIR%/Applications/%NAME%.app. MunkiPkginfoMerger needed afterwards")
330 | }
331 |
332 | @objc func munkiPkginfoMergerAction(sender: NSButton) {
333 | munkiPkginfoMerger ()
334 | helpPopover.close()
335 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
336 | writePopOvertext(processor: "MunkiPkginfoMerger", extraHelpText: "")
337 | }
338 |
339 | @objc func packageRequiredAction(sender: NSButton) {
340 | packageRequired ()
341 | helpPopover.close()
342 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
343 | writePopOvertext(processor: "PackageRequired", extraHelpText: "")
344 | }
345 |
346 | @objc func pathDeleterAction(sender: NSButton) {
347 | pathDeleter ()
348 | helpPopover.close()
349 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
350 | writePopOvertext(processor: "PathDeleter", extraHelpText: "")
351 | }
352 |
353 | @objc func pkgCopierAction(sender: NSButton) {
354 | pkgCopier ()
355 | helpPopover.close()
356 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
357 | writePopOvertext(processor: "PkgCopier", extraHelpText: "")
358 | }
359 |
360 | @objc func pkgCreatorAction(sender: NSButton) {
361 | pkgCreator ()
362 | helpPopover.close()
363 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
364 | writePopOvertext(processor: "PkgCreator", extraHelpText: "Add your reverse domain for the id key")
365 | }
366 |
367 | @objc func pkgExtractorAction(sender: NSButton) {
368 | pkgExtractor ()
369 | helpPopover.close()
370 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
371 | writePopOvertext(processor: "PkgExtractor", extraHelpText: """
372 | You probably should use FlatPkgUnpacker instead.
373 | Bundle-style pkg is the old pkg format and rare.
374 | """)
375 | }
376 |
377 | @objc func pkgInfoCreatorAction(sender: NSButton) {
378 | pkgInfoCreator ()
379 | helpPopover.close()
380 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
381 | writePopOvertext(processor: "PkgInfoCreator", extraHelpText: """
382 | pkgroot and version are required, but likely they are set earlier from another processor like PkgRootCreator
383 | """)
384 | }
385 |
386 | @objc func pkgPayloadUnpackerAction(sender: NSButton) {
387 | pkgPayloadUnpacker ()
388 | helpPopover.close()
389 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
390 | writePopOvertext(processor: "PkgPayloadUnpacker", extraHelpText: "")
391 | }
392 |
393 | @objc func pkgRootCreatorAction(sender: NSButton) {
394 | pkgRootCreator ()
395 | helpPopover.close()
396 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
397 | writePopOvertext(processor: "PkgRootCreator", extraHelpText: "Deletes whole directory don't use %RECIPE_CACHE_DIR% instead use %RECIPE_CACHE_DIR%/%NAME%")
398 | }
399 |
400 | @objc func plistEditorAction(sender: NSButton) {
401 | plistEditor ()
402 | helpPopover.close()
403 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
404 | writePopOvertext(processor: "PlistEditor", extraHelpText: "")
405 | }
406 |
407 | @objc func plistReaderAction(sender: NSButton) {
408 | plistReader ()
409 | helpPopover.close()
410 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
411 | writePopOvertext(processor: "PlistReader", extraHelpText: "")
412 | }
413 |
414 | @objc func sparkleUpdateInfoProviderAction(sender: NSButton) {
415 | sparkleUpdateInfoProvider ()
416 | helpPopover.close()
417 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
418 | writePopOvertext(processor: "SparkleUpdateInfoProvider", extraHelpText: """
419 | Use before URLDownloader to get the %DOWNLOAD_URL% thats passed to URLDownloader
420 | Replace the input key
421 | DOWNLOAD_URL
422 | with
423 | SPARKLE_FEED_URL
424 | and provide the the Sparkle feed URL as the string instead
425 | """)
426 | }
427 |
428 | @objc func stopProcessingIfAction(sender: NSButton) {
429 | stopProcessingIf ()
430 | helpPopover.close()
431 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
432 | writePopOvertext(processor: "StopProcessingIf", extraHelpText: "")
433 | }
434 |
435 | @objc func symlinkerAction(sender: NSButton) {
436 | symlinker ()
437 | helpPopover.close()
438 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
439 | writePopOvertext(processor: "Symlinker", extraHelpText: "")
440 | }
441 |
442 | @objc func unarchiverAction(sender: NSButton) {
443 | unarchiver ()
444 | helpPopover.close()
445 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
446 | writePopOvertext(processor: "Unarchiver", extraHelpText: "")
447 | }
448 |
449 | @objc func urlDownloaderAction(sender: NSButton) {
450 | urlDownloader ()
451 | helpPopover.close()
452 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
453 | writePopOvertext(processor: "URLDownloader", extraHelpText: "")
454 | }
455 |
456 | @objc func urlTextSearcherAction(sender: NSButton) {
457 | urlTextSearcher ()
458 | helpPopover.close()
459 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
460 | writePopOvertext(processor: "URLTextSearcher", extraHelpText: "Often used before URLDownloader get the \"real\" download URL thats passed to URLDownloader")
461 | }
462 |
463 | @objc func versionerAction(sender: NSButton) {
464 | versioner ()
465 | helpPopover.close()
466 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
467 | writePopOvertext(processor: "Versioner", extraHelpText: """
468 | Remove
469 | plist_version_key
470 | CFBundleVersion
471 | if you only want CFBundleShortVersionString
472 | """)
473 | }
474 |
475 | // 3rd party buttons
476 | @objc func JamfAccountUploaderAction(sender: NSButton) {
477 | JamfAccountUploader()
478 | helpPopover.close()
479 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
480 | writePopOvertextJamfUploader(processor: "JamfAccountUploader", extraHelpText: JamfAccountUploaderHelp)
481 | }
482 |
483 | @objc func JamfCategoryUploaderAction(sender: NSButton) {
484 | JamfCategoryUploader ()
485 | helpPopover.close()
486 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
487 | writePopOvertextJamfUploader(processor: "JamfCategoryUploader", extraHelpText: JamfCategoryUploaderHelp)
488 | //writePopOvertext(processor: "", extraHelpText: JamfCategoryUploaderHelp)
489 | }
490 |
491 | @objc func JamfClassicAPIObjectUploaderAction(sender: NSButton) {
492 | JamfClassicAPIObjectUploader()
493 | helpPopover.close()
494 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
495 | writePopOvertextJamfUploader(processor: "JamfClassicAPIObjectUploader", extraHelpText: JamfClassicAPIObjectUploaderHelp)
496 | }
497 |
498 | @objc func JamfComputerGroupDeleterAction(sender: NSButton) {
499 | JamfComputerGroupDeleter()
500 | helpPopover.close()
501 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
502 | writePopOvertextJamfUploader(processor: "JamfComputerGroupDeleter", extraHelpText: JamfComputerGroupDeleterHelp)
503 | }
504 |
505 | @objc func JamfComputerGroupUploaderAction(sender: NSButton) {
506 | JamfComputerGroupUploader ()
507 | helpPopover.close()
508 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
509 | writePopOvertextJamfUploader(processor: "JamfComputerGroupUploader", extraHelpText: JamfComputerGroupUploaderHelp)
510 | //writePopOvertext(processor: "", extraHelpText: JamfComputerGroupUploaderHelp)
511 | }
512 |
513 | @objc func JamfComputerProfileUploaderAction(sender: NSButton) {
514 | JamfComputerProfileUploader ()
515 | helpPopover.close()
516 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
517 | writePopOvertextJamfUploader(processor: "JamfComputerProfileUploader", extraHelpText: JamfComputerProfileUploaderHelp)
518 | //writePopOvertext(processor: "", extraHelpText: JamfComputerProfileUploaderHelp)
519 | }
520 |
521 | @objc func JamfDockItemUploaderAction(sender: NSButton) {
522 | JamfDockItemUploader ()
523 | helpPopover.close()
524 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
525 | writePopOvertextJamfUploader(processor: "JamfDockItemUploader", extraHelpText: JamfDockItemUploaderHelp)
526 | //writePopOvertext(processor: "", extraHelpText: JamfDockItemUploaderHelp)
527 | }
528 |
529 | @objc func JamfExtensionAttributeUploaderAction(sender: NSButton) {
530 | JamfExtensionAttributeUploader ()
531 | helpPopover.close()
532 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
533 | writePopOvertextJamfUploader(processor: "JamfExtensionAttributeUploader", extraHelpText: JamfExtensionAttributeUploaderHelp)
534 | //writePopOvertext(processor: "", extraHelpText: JamfExtensionAttributeUploaderHelp)
535 | }
536 |
537 | @objc func JamfIconUploaderAction(sender: NSButton) {
538 | JamfIconUploader()
539 | helpPopover.close()
540 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
541 | writePopOvertextJamfUploader(processor: "JamfIconUploader", extraHelpText: JamfIconUploaderHelp)
542 | }
543 |
544 | @objc func JamfMacAppUploaderAction(sender: NSButton) {
545 | JamfMacAppUploader ()
546 | helpPopover.close()
547 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
548 | writePopOvertextJamfUploader(processor: "JamfMacAppUploader", extraHelpText: JamfMacAppUploaderHelp)
549 | }
550 |
551 | @objc func JamfMobileDeviceGroupUploaderAction(sender: NSButton) {
552 | JamfMobileDeviceGroupUploader()
553 | helpPopover.close()
554 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
555 | writePopOvertextJamfUploader(processor: "JamfMobileDeviceGroupUploader", extraHelpText: JamfMobileDeviceGroupUploaderHelp)
556 | }
557 |
558 | @objc func JamfMobileDeviceProfileUploaderAction(sender: NSButton) {
559 | JamfMobileDeviceProfileUploader()
560 | helpPopover.close()
561 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
562 | writePopOvertextJamfUploader(processor: "JamfMobileDeviceProfileUploader", extraHelpText: JamfMobileDeviceProfileUploaderHelp)
563 | }
564 |
565 | @objc func JamfPackageCleanerAction(sender: NSButton) {
566 | JamfPackageCleaner()
567 | helpPopover.close()
568 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
569 | writePopOvertextJamfUploader(processor: "JamfPackageCleaner", extraHelpText: JamfPackageCleanerHelp)
570 | }
571 |
572 | @objc func JamfPackageRecalculatorAction(sender: NSButton) {
573 | JamfPackageRecalculator()
574 | helpPopover.close()
575 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
576 | writePopOvertextJamfUploader(processor: "JamfPackageRecalculator", extraHelpText: JamfPackageRecalculatorHelp)
577 | }
578 |
579 | @objc func JamfPackageUploaderAction(sender: NSButton) {
580 | JamfPackageUploader ()
581 | helpPopover.close()
582 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
583 | writePopOvertextJamfUploader(processor: "JamfPackageUploader", extraHelpText: JamfPackageUploaderHelp)
584 | //writePopOvertext(processor: "", extraHelpText: JamfPackageUploaderHelp)
585 | }
586 |
587 | @objc func JamfPatchCheckerAction(sender: NSButton) {
588 | JamfPatchChecker()
589 | helpPopover.close()
590 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
591 | writePopOvertextJamfUploader(processor: "JamfPatchChecker", extraHelpText: JamfPatchCheckerHelp)
592 | }
593 |
594 | @objc func JamfPatchUploaderAction(sender: NSButton) {
595 | JamfPatchUploader ()
596 | helpPopover.close()
597 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
598 | writePopOvertextJamfUploader(processor: "JamfPatchUploader", extraHelpText: JamfPatchUploaderHelp)
599 | //writePopOvertext(processor: "", extraHelpText: JamfPatchUploaderHelp)
600 | }
601 |
602 | @objc func JamfPkgMetadataUploaderAction(sender: NSButton) {
603 | JamfPkgMetadataUploader()
604 | helpPopover.close()
605 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
606 | writePopOvertextJamfUploader(processor: "JamfPkgMetadataUploader", extraHelpText: JamfPkgMetadataUploaderHelp)
607 | }
608 |
609 | @objc func JamfPolicyDeleterAction(sender: NSButton) {
610 | JamfPolicyDeleter ()
611 | helpPopover.close()
612 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
613 | writePopOvertextJamfUploader(processor: "JamfPolicyDeleter", extraHelpText: JamfPolicyDeleterHelp)
614 | //writePopOvertext(processor: "", extraHelpText: JamfPolicyDeleterHelp)
615 | }
616 |
617 | @objc func JamfPolicyLogFlusherAction(sender: NSButton) {
618 | JamfPolicyLogFlusher ()
619 | helpPopover.close()
620 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
621 | writePopOvertextJamfUploader(processor: "JamfPolicyLogFlusher", extraHelpText: JamfPolicyLogFlusherHelp)
622 | //writePopOvertext(processor: "", extraHelpText: JamfPolicyLogFlusherHelp)
623 | }
624 |
625 | @objc func JamfPolicyUploaderAction(sender: NSButton) {
626 | JamfPolicyUploader ()
627 | helpPopover.close()
628 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
629 | writePopOvertextJamfUploader(processor: "JamfPolicyUploader", extraHelpText: JamfPolicyUploaderHelp)
630 | //writePopOvertext(processor: "", extraHelpText: JamfPolicyUploaderHelp)
631 | }
632 |
633 | @objc func JamfScriptUploaderAction(sender: NSButton) {
634 | JamfScriptUploader ()
635 | helpPopover.close()
636 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
637 | writePopOvertextJamfUploader(processor: "JamfScriptUploader", extraHelpText: JamfScriptUploaderHelp)
638 | //writePopOvertext(processor: "", extraHelpText: JamfScriptUploaderHelp)
639 | }
640 |
641 | @objc func JamfSoftwareRestrictionUploaderAction(sender: NSButton) {
642 | JamfSoftwareRestrictionUploader ()
643 | helpPopover.close()
644 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
645 | writePopOvertextJamfUploader(processor: "JamfSoftwareRestrictionUploader", extraHelpText: JamfSoftwareRestrictionUploaderHelp)
646 | //writePopOvertext(processor: "", extraHelpText: JamfSoftwareRestrictionUploaderHelp)
647 | }
648 |
649 | @objc func JamfUploaderSlackerAction(sender: NSButton) {
650 | JamfUploaderSlacker ()
651 | helpPopover.close()
652 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
653 | writePopOvertextJamfUploader(processor: "JamfUploaderSlacker", extraHelpText: JamfUploaderSlackerHelp)
654 | //writePopOvertext(processor: "", extraHelpText: JamfUploaderSlackerHelp)
655 | }
656 |
657 | @objc func JamfUploaderTeamsNotifierAction(sender: NSButton) {
658 | JamfUploaderTeamsNotifier ()
659 | helpPopover.close()
660 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
661 | writePopOvertextJamfUploader(processor: "JamfUploaderTeamsNotifier", extraHelpText: JamfUploaderTeamsNotifierHelp)
662 | //writePopOvertext(processor: "", extraHelpText: JamfUploaderTeamsNotifierHelp)
663 | }
664 |
665 |
666 | @IBAction func enableAndReloadAction(_ sender: NSButton) {
667 | getAllUserButtons ()
668 | }
669 |
670 | @objc func buttonAction1(sender: NSButton!) {
671 | output = output1
672 | writeOutputUserButtons ()
673 | helpPopover.close()
674 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
675 | writePopOvertextUserButtons (helpText: help1)
676 | }
677 |
678 | @objc func buttonAction2(sender: NSButton!) {
679 | output = output2
680 | writeOutputUserButtons ()
681 | helpPopover.close()
682 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
683 | writePopOvertextUserButtons (helpText: help2)
684 | }
685 |
686 | @objc func buttonAction3(sender: NSButton!) {
687 | output = output3
688 | writeOutputUserButtons ()
689 | helpPopover.close()
690 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
691 | writePopOvertextUserButtons (helpText: help3)
692 | }
693 |
694 | @objc func buttonAction4(sender: NSButton!) {
695 | output = output4
696 | writeOutputUserButtons ()
697 | helpPopover.close()
698 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
699 | writePopOvertextUserButtons (helpText: help4)
700 | }
701 |
702 | @objc func buttonAction5(sender: NSButton!) {
703 | output = output5
704 | writeOutputUserButtons ()
705 | helpPopover.close()
706 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
707 | writePopOvertextUserButtons (helpText: help5)
708 | }
709 |
710 | @objc func buttonAction6(sender: NSButton!) {
711 | output = output6
712 | writeOutputUserButtons ()
713 | helpPopover.close()
714 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
715 | writePopOvertextUserButtons (helpText: help6)
716 | }
717 |
718 | @objc func buttonAction7(sender: NSButton!) {
719 | output = output7
720 | writeOutputUserButtons ()
721 | helpPopover.close()
722 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
723 | writePopOvertextUserButtons (helpText: help7)
724 | }
725 |
726 | @objc func buttonAction8(sender: NSButton!) {
727 | output = output8
728 | writeOutputUserButtons ()
729 | helpPopover.close()
730 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
731 | writePopOvertextUserButtons (helpText: help8)
732 | }
733 |
734 | @objc func buttonAction9(sender: NSButton!) {
735 | output = output9
736 | writeOutputUserButtons ()
737 | helpPopover.close()
738 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
739 | writePopOvertextUserButtons (helpText: help9)
740 |
741 | }
742 |
743 | @objc func buttonAction10(sender: NSButton!) {
744 | output = output10
745 | writeOutputUserButtons ()
746 | helpPopover.close()
747 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX)
748 | writePopOvertextUserButtons (helpText: help10)
749 | }
750 |
751 | func showHelpPopoverinTextField() {
752 | guard let textField = outputTextField else { return }
753 | let textFieldFrame = textField.window?.convertToScreen(textField.convert(textField.bounds, to: nil)) ?? .zero
754 | // Show the popover anchored to the text field
755 | // Use the full bounds of the text field for the anchor
756 | let anchorRect = NSRect(
757 | x: textFieldFrame.origin.x - 1000, // Align to the left edge
758 | y: textFieldFrame.origin.y - 1320, // Optional vertical offset
759 | width: textFieldFrame.width, // Full width of the text field
760 | height: textFieldFrame.height // Full height of the text field
761 | )
762 |
763 |
764 | // Set up the popover text
765 | helpPopoverText.string = """
766 | Getting Started
767 | 1. Set Up Your New Recipe
768 | Enter the Identifier, Recipe Format
769 | and App/Package Name in the respective fields.
770 |
771 | 2. Create the New Recipe:
772 | Select File > New (Command-N)
773 |
774 | 3. Choose Your Format:
775 | Use the XML/Yaml switch to toggle between the two supported formats.(Command-Y)
776 |
777 | -----
778 | To Open an existing Recipe file
779 | Select File > Open
780 | """
781 | let attributedText = [ NSAttributedString.Key.font: NSFont(name: "Menlo", size: 12.0)! ]
782 | let processorInfoAttributed = NSAttributedString(string: helpPopoverText.string, attributes: attributedText)
783 | appDelegate().helpPopoverText.string = ""
784 | appDelegate().helpPopoverText.textStorage?.insert(NSAttributedString(attributedString: processorInfoAttributed), at: 0)
785 | helpPopover.show(relativeTo: anchorRect, of: textField, preferredEdge: .maxY)
786 | //maxY: Displays the popover below the rectangle, with the arrow pointing upwards.
787 | //.minY: Displays the popover above the rectangle, with the arrow pointing downwards.
788 | //.maxX = Displays the popover to left
789 | // minX: Displays the popover to the left
790 | }
791 |
792 | func applicationDidFinishLaunching(_ aNotification: Notification) {
793 | outputTextField.isAutomaticQuoteSubstitutionEnabled = false
794 | outputTextField.font = NSFont(name: "Menlo", size: 12)
795 | createAllDefaultButtons ()
796 | createAll3rdPartyButtons ()
797 | checkThatAutopkgExist ()
798 | checkForUserButtonsAndEnable ()
799 | showHelpPopoverinTextField()
800 | getTrustInfo()
801 | }
802 |
803 |
804 | func applicationWillTerminate(_ aNotification: Notification) {
805 | // Insert code here to tear down your application
806 |
807 | }
808 |
809 | }
810 |
--------------------------------------------------------------------------------
/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "16x16",
5 | "idiom" : "mac",
6 | "filename" : "RecipeBuilder_icon-16.png",
7 | "scale" : "1x"
8 | },
9 | {
10 | "size" : "16x16",
11 | "idiom" : "mac",
12 | "filename" : "RecipeBuilder_icon-33.png",
13 | "scale" : "2x"
14 | },
15 | {
16 | "size" : "32x32",
17 | "idiom" : "mac",
18 | "filename" : "RecipeBuilder_icon-32.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "32x32",
23 | "idiom" : "mac",
24 | "filename" : "RecipeBuilder_icon-64.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "128x128",
29 | "idiom" : "mac",
30 | "filename" : "RecipeBuilder_icon-128.png",
31 | "scale" : "1x"
32 | },
33 | {
34 | "size" : "128x128",
35 | "idiom" : "mac",
36 | "filename" : "RecipeBuilder_icon-256.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "256x256",
41 | "idiom" : "mac",
42 | "filename" : "RecipeBuilder_icon-257.png",
43 | "scale" : "1x"
44 | },
45 | {
46 | "size" : "256x256",
47 | "idiom" : "mac",
48 | "filename" : "RecipeBuilder_icon-512.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "512x512",
53 | "idiom" : "mac",
54 | "filename" : "RecipeBuilder_icon-513.png",
55 | "scale" : "1x"
56 | },
57 | {
58 | "size" : "512x512",
59 | "idiom" : "mac",
60 | "filename" : "RecipeBuilder_icon-1024.png",
61 | "scale" : "2x"
62 | }
63 | ],
64 | "info" : {
65 | "version" : 1,
66 | "author" : "xcode"
67 | }
68 | }
--------------------------------------------------------------------------------
/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-1024.png
--------------------------------------------------------------------------------
/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-128.png
--------------------------------------------------------------------------------
/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-16.png
--------------------------------------------------------------------------------
/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-256.png
--------------------------------------------------------------------------------
/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-257.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-257.png
--------------------------------------------------------------------------------
/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-32.png
--------------------------------------------------------------------------------
/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-33.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-33.png
--------------------------------------------------------------------------------
/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-512.png
--------------------------------------------------------------------------------
/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-513.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-513.png
--------------------------------------------------------------------------------
/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-64.png
--------------------------------------------------------------------------------
/RecipeBuilder/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_128x128.png
--------------------------------------------------------------------------------
/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_128x128@2x.png
--------------------------------------------------------------------------------
/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_16x16.png
--------------------------------------------------------------------------------
/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_16x16@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_16x16@2x.png
--------------------------------------------------------------------------------
/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_256x256.png
--------------------------------------------------------------------------------
/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_256x256@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_256x256@2x.png
--------------------------------------------------------------------------------
/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_32x32.png
--------------------------------------------------------------------------------
/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_32x32@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_32x32@2x.png
--------------------------------------------------------------------------------
/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_512x512.png
--------------------------------------------------------------------------------
/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_512x512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_512x512@2x.png
--------------------------------------------------------------------------------
/RecipeBuilder/Buttons3rdParty.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Buttons3rdParty.swift
3 | //
4 | // Created by Mikael Löfgren on 2024-12-27
5 | // Copyright © 2024 Mikael Löfgren. All rights reserved.
6 | //
7 |
8 | import Cocoa
9 | import AppKit
10 | import Foundation
11 | import Highlightr
12 |
13 | func createAll3rdPartyButtons () {
14 | createButtonJamfAccountUploader()
15 | createButtonJamfCategoryUploader()
16 | createButtonJamfClassicAPIObjectUploader()
17 | createButtonJamfComputerGroupDeleter()
18 | createButtonJamfComputerGroupUploader()
19 | createButtonJamfComputerProfileUploader()
20 | createButtonJamfDockItemUploader()
21 | createButtonJamfExtensionAttributeUploader()
22 | createButtonJamfIconUploader()
23 | createButtonJamfMacAppUploader()
24 | createButtonJamfMobileDeviceGroupUploader()
25 | createButtonJamfMobileDeviceProfileUploader()
26 | createButtonJamfPackageCleaner()
27 | createButtonJamfPackageRecalculator()
28 | createButtonJamfPackageUploader()
29 | createButtonJamfPatchChecker()
30 | createButtonJamfPatchUploader()
31 | createButtonJamfPkgMetadataUploader()
32 | createButtonJamfPolicyDeleter()
33 | createButtonJamfPolicyLogFlusher()
34 | createButtonJamfPolicyUploader()
35 | createButtonJamfScriptUploader()
36 | createButtonJamfSoftwareRestrictionUploader()
37 | createButtonJamfUploaderSlacker()
38 | createButtonJamfUploaderTeamsNotifier()
39 | }
40 |
41 | // Functions to create the buttons
42 | // 21 pixels between every button
43 | // https://github.com/autopkg/grahampugh-recipes/blob/main/JamfUploaderProcessors/READMEs/JamfCategoryUploader.md
44 | func createButtonJamfAccountUploader() {
45 | let button = NSButton(frame: NSRect(x: 17, y: 855, width: 191, height: 17))
46 | button.title = "JamfAccountUploader"
47 | button.bezelStyle = .inline
48 | button.setButtonType(.momentaryPushIn)
49 | button.isBordered = true
50 | button.font = .boldSystemFont(ofSize: 11)
51 | button.toolTip = "Upload an account to Jamf"
52 | button.action = #selector(appDelegate().JamfAccountUploaderAction)
53 | appDelegate().processorsView3rdParty.addSubview(button)
54 | }
55 |
56 | func createButtonJamfCategoryUploader() {
57 | let button = NSButton(frame: NSRect(x: 17, y: 834, width: 191, height: 17))
58 | button.title = "JamfCategoryUploader"
59 | button.bezelStyle = .inline
60 | button.setButtonType(.momentaryPushIn)
61 | button.isBordered = true
62 | button.font = .boldSystemFont(ofSize: 11)
63 | button.toolTip = "Upload a category to Jamf"
64 | button.action = #selector(appDelegate().JamfCategoryUploaderAction)
65 | appDelegate().processorsView3rdParty.addSubview(button)
66 | }
67 |
68 | func createButtonJamfClassicAPIObjectUploader() {
69 | let button = NSButton(frame: NSRect(x: 17, y: 813, width: 191, height: 17))
70 | button.title = "JamfClassicAPIObjectUploader"
71 | button.bezelStyle = .inline
72 | button.setButtonType(.momentaryPushIn)
73 | button.isBordered = true
74 | button.font = .boldSystemFont(ofSize: 11)
75 | button.toolTip = "Upload Classic API objects to Jamf"
76 | button.action = #selector(appDelegate().JamfClassicAPIObjectUploaderAction)
77 | appDelegate().processorsView3rdParty.addSubview(button)
78 | }
79 |
80 | func createButtonJamfComputerGroupDeleter() {
81 | let button = NSButton(frame: NSRect(x: 17, y: 792, width: 191, height: 17))
82 | button.title = "JamfComputerGroupDeleter"
83 | button.bezelStyle = .inline
84 | button.setButtonType(.momentaryPushIn)
85 | button.isBordered = true
86 | button.font = .boldSystemFont(ofSize: 11)
87 | button.toolTip = "Delete a computer group in Jamf"
88 | button.action = #selector(appDelegate().JamfComputerGroupDeleterAction)
89 | appDelegate().processorsView3rdParty.addSubview(button)
90 | }
91 |
92 | func createButtonJamfComputerGroupUploader() {
93 | let button = NSButton(frame: NSRect(x: 17, y: 771, width: 191, height: 17))
94 | button.title = "JamfComputerGroupUploader"
95 | button.bezelStyle = .inline
96 | button.setButtonType(.momentaryPushIn)
97 | button.isBordered = true
98 | button.font = .boldSystemFont(ofSize: 11)
99 | button.toolTip = "Upload a computer group to Jamf"
100 | button.action = #selector(appDelegate().JamfComputerGroupUploaderAction)
101 | appDelegate().processorsView3rdParty.addSubview(button)
102 | }
103 |
104 | func createButtonJamfComputerProfileUploader() {
105 | let button = NSButton(frame: NSRect(x: 17, y: 750, width: 191, height: 17))
106 | button.title = "JamfComputerProfileUploader"
107 | button.bezelStyle = .inline
108 | button.setButtonType(.momentaryPushIn)
109 | button.isBordered = true
110 | button.font = .boldSystemFont(ofSize: 11)
111 | button.toolTip = "Upload a computer profile to Jamf"
112 | button.action = #selector(appDelegate().JamfComputerProfileUploaderAction)
113 | appDelegate().processorsView3rdParty.addSubview(button)
114 | }
115 |
116 | func createButtonJamfDockItemUploader() {
117 | let button = NSButton(frame: NSRect(x: 17, y: 729, width: 191, height: 17))
118 | button.title = "JamfDockItemUploader"
119 | button.bezelStyle = .inline
120 | button.setButtonType(.momentaryPushIn)
121 | button.isBordered = true
122 | button.font = .boldSystemFont(ofSize: 11)
123 | button.toolTip = "Upload a dock item to Jamf"
124 | button.action = #selector(appDelegate().JamfDockItemUploaderAction)
125 | appDelegate().processorsView3rdParty.addSubview(button)
126 | }
127 |
128 | func createButtonJamfExtensionAttributeUploader() {
129 | let button = NSButton(frame: NSRect(x: 17, y: 708, width: 191, height: 17))
130 | button.title = "JamfExtensionAttributeUploader"
131 | button.bezelStyle = .inline
132 | button.setButtonType(.momentaryPushIn)
133 | button.isBordered = true
134 | button.font = .boldSystemFont(ofSize: 11)
135 | button.toolTip = "Upload an extension attribute to Jamf"
136 | button.action = #selector(appDelegate().JamfExtensionAttributeUploaderAction)
137 | appDelegate().processorsView3rdParty.addSubview(button)
138 | }
139 |
140 | func createButtonJamfIconUploader() {
141 | let button = NSButton(frame: NSRect(x: 17, y: 687, width: 191, height: 17))
142 | button.title = "JamfIconUploader"
143 | button.bezelStyle = .inline
144 | button.setButtonType(.momentaryPushIn)
145 | button.isBordered = true
146 | button.font = .boldSystemFont(ofSize: 11)
147 | button.toolTip = "Upload an icon to Jamf"
148 | button.action = #selector(appDelegate().JamfIconUploaderAction)
149 | appDelegate().processorsView3rdParty.addSubview(button)
150 | }
151 |
152 | func createButtonJamfMacAppUploader() {
153 | let button = NSButton(frame: NSRect(x: 17, y: 666, width: 191, height: 17))
154 | button.title = "JamfMacAppUploader"
155 | button.bezelStyle = .inline
156 | button.setButtonType(.momentaryPushIn)
157 | button.isBordered = true
158 | button.font = .boldSystemFont(ofSize: 11)
159 | button.toolTip = "Upload a Mac app to Jamf"
160 | button.action = #selector(appDelegate().JamfMacAppUploaderAction)
161 | appDelegate().processorsView3rdParty.addSubview(button)
162 | }
163 |
164 | func createButtonJamfMobileDeviceGroupUploader() {
165 | let button = NSButton(frame: NSRect(x: 17, y: 645, width: 191, height: 17))
166 | button.title = "JamfMobileDeviceGroupUploader"
167 | button.bezelStyle = .inline
168 | button.setButtonType(.momentaryPushIn)
169 | button.isBordered = true
170 | button.font = .boldSystemFont(ofSize: 11)
171 | button.toolTip = "Upload a mobile device group to Jamf"
172 | button.action = #selector(appDelegate().JamfMobileDeviceGroupUploaderAction)
173 | appDelegate().processorsView3rdParty.addSubview(button)
174 | }
175 |
176 | func createButtonJamfMobileDeviceProfileUploader() {
177 | let button = NSButton(frame: NSRect(x: 17, y: 624, width: 191, height: 17))
178 | button.title = "JamfMobileDeviceProfileUpload"
179 | button.bezelStyle = .inline
180 | button.setButtonType(.momentaryPushIn)
181 | button.isBordered = true
182 | button.font = .boldSystemFont(ofSize: 11)
183 | button.toolTip = "Upload a mobile device profile to Jamf"
184 | button.action = #selector(appDelegate().JamfMobileDeviceProfileUploaderAction)
185 | appDelegate().processorsView3rdParty.addSubview(button)
186 | }
187 |
188 | func createButtonJamfPackageCleaner() {
189 | let button = NSButton(frame: NSRect(x: 17, y: 603, width: 191, height: 17))
190 | button.title = "JamfPackageCleaner"
191 | button.bezelStyle = .inline
192 | button.setButtonType(.momentaryPushIn)
193 | button.isBordered = true
194 | button.font = .boldSystemFont(ofSize: 11)
195 | button.toolTip = "Clean up old packages in Jamf"
196 | button.action = #selector(appDelegate().JamfPackageCleanerAction)
197 | appDelegate().processorsView3rdParty.addSubview(button)
198 | }
199 |
200 | func createButtonJamfPackageRecalculator() {
201 | let button = NSButton(frame: NSRect(x: 17, y: 582, width: 191, height: 17))
202 | button.title = "JamfPackageRecalculator"
203 | button.bezelStyle = .inline
204 | button.setButtonType(.momentaryPushIn)
205 | button.isBordered = true
206 | button.font = .boldSystemFont(ofSize: 11)
207 | button.toolTip = "Recalculate package information in Jamf"
208 | button.action = #selector(appDelegate().JamfPackageRecalculatorAction)
209 | appDelegate().processorsView3rdParty.addSubview(button)
210 | }
211 |
212 | func createButtonJamfPackageUploader() {
213 | let button = NSButton(frame: NSRect(x: 17, y: 561, width: 191, height: 17))
214 | button.title = "JamfPackageUploader"
215 | button.bezelStyle = .inline
216 | button.setButtonType(.momentaryPushIn)
217 | button.isBordered = true
218 | button.font = .boldSystemFont(ofSize: 11)
219 | button.toolTip = "Upload a package to Jamf"
220 | button.action = #selector(appDelegate().JamfPackageUploaderAction)
221 | appDelegate().processorsView3rdParty.addSubview(button)
222 | }
223 |
224 | func createButtonJamfPatchChecker() {
225 | let button = NSButton(frame: NSRect(x: 17, y: 540, width: 191, height: 17))
226 | button.title = "JamfPatchChecker"
227 | button.bezelStyle = .inline
228 | button.setButtonType(.momentaryPushIn)
229 | button.isBordered = true
230 | button.font = .boldSystemFont(ofSize: 11)
231 | button.toolTip = "Check for patch updates in Jamf"
232 | button.action = #selector(appDelegate().JamfPatchCheckerAction)
233 | appDelegate().processorsView3rdParty.addSubview(button)
234 | }
235 |
236 | func createButtonJamfPatchUploader() {
237 | let button = NSButton(frame: NSRect(x: 17, y: 519, width: 191, height: 17))
238 | button.title = "JamfPatchUploader"
239 | button.bezelStyle = .inline
240 | button.setButtonType(.momentaryPushIn)
241 | button.isBordered = true
242 | button.font = .boldSystemFont(ofSize: 11)
243 | button.toolTip = "Upload patch definitions to Jamf"
244 | button.action = #selector(appDelegate().JamfPatchUploaderAction)
245 | appDelegate().processorsView3rdParty.addSubview(button)
246 | }
247 |
248 | func createButtonJamfPkgMetadataUploader() {
249 | let button = NSButton(frame: NSRect(x: 17, y: 498, width: 191, height: 17))
250 | button.title = "JamfPkgMetadataUploader"
251 | button.bezelStyle = .inline
252 | button.setButtonType(.momentaryPushIn)
253 | button.isBordered = true
254 | button.font = .boldSystemFont(ofSize: 11)
255 | button.toolTip = "Upload pkg Metadata to Jamf"
256 | button.action = #selector(appDelegate().JamfPkgMetadataUploaderAction)
257 | appDelegate().processorsView3rdParty.addSubview(button)
258 | }
259 |
260 | func createButtonJamfPolicyDeleter() {
261 | let button = NSButton(frame: NSRect(x: 17, y: 477, width: 191, height: 17))
262 | button.title = "JamfPolicyDeleter"
263 | button.bezelStyle = .inline
264 | button.setButtonType(.momentaryPushIn)
265 | button.isBordered = true
266 | button.font = .boldSystemFont(ofSize: 11)
267 | button.toolTip = "Delete policies in Jamf"
268 | button.action = #selector(appDelegate().JamfPolicyDeleterAction)
269 | appDelegate().processorsView3rdParty.addSubview(button)
270 | }
271 |
272 | func createButtonJamfPolicyLogFlusher() {
273 | let button = NSButton(frame: NSRect(x: 17, y: 456, width: 191, height: 17))
274 | button.title = "JamfPolicyLogFlusher"
275 | button.bezelStyle = .inline
276 | button.setButtonType(.momentaryPushIn)
277 | button.isBordered = true
278 | button.font = .boldSystemFont(ofSize: 11)
279 | button.toolTip = "Flush policy logs in Jamf"
280 | button.action = #selector(appDelegate().JamfPolicyLogFlusherAction)
281 | appDelegate().processorsView3rdParty.addSubview(button)
282 | }
283 |
284 | func createButtonJamfPolicyUploader() {
285 | let button = NSButton(frame: NSRect(x: 17, y: 435, width: 191, height: 17))
286 | button.title = "JamfPolicyUploader"
287 | button.bezelStyle = .inline
288 | button.setButtonType(.momentaryPushIn)
289 | button.isBordered = true
290 | button.font = .boldSystemFont(ofSize: 11)
291 | button.toolTip = "Upload a policy to Jamf"
292 | button.action = #selector(appDelegate().JamfPolicyUploaderAction)
293 | appDelegate().processorsView3rdParty.addSubview(button)
294 | }
295 |
296 | func createButtonJamfScriptUploader() {
297 | let button = NSButton(frame: NSRect(x: 17, y: 414, width: 191, height: 17))
298 | button.title = "JamfScriptUploader"
299 | button.bezelStyle = .inline
300 | button.setButtonType(.momentaryPushIn)
301 | button.isBordered = true
302 | button.font = .boldSystemFont(ofSize: 11)
303 | button.toolTip = "Upload a script to Jamf"
304 | button.action = #selector(appDelegate().JamfScriptUploaderAction)
305 | appDelegate().processorsView3rdParty.addSubview(button)
306 | }
307 |
308 | func createButtonJamfSoftwareRestrictionUploader() {
309 | let button = NSButton(frame: NSRect(x: 17, y: 393, width: 191, height: 17))
310 | button.title = "JamfSoftwareRestrictionUpload"
311 | button.bezelStyle = .inline
312 | button.setButtonType(.momentaryPushIn)
313 | button.isBordered = true
314 | button.font = .boldSystemFont(ofSize: 11)
315 | button.toolTip = "Upload software restrictions to Jamf"
316 | button.action = #selector(appDelegate().JamfSoftwareRestrictionUploaderAction)
317 | appDelegate().processorsView3rdParty.addSubview(button)
318 | }
319 |
320 | func createButtonJamfUploaderSlacker() {
321 | let button = NSButton(frame: NSRect(x: 17, y: 372, width: 191, height: 17))
322 | button.title = "JamfUploaderSlacker"
323 | button.bezelStyle = .inline
324 | button.setButtonType(.momentaryPushIn)
325 | button.isBordered = true
326 | button.font = .boldSystemFont(ofSize: 11)
327 | button.toolTip = "Send notifications to Slack"
328 | button.action = #selector(appDelegate().JamfUploaderSlackerAction)
329 | appDelegate().processorsView3rdParty.addSubview(button)
330 | }
331 |
332 | func createButtonJamfUploaderTeamsNotifier() {
333 | let button = NSButton(frame: NSRect(x: 17, y: 351, width: 191, height: 17))
334 | button.title = "JamfUploaderTeamsNotifier"
335 | button.bezelStyle = .inline
336 | button.setButtonType(.momentaryPushIn)
337 | button.isBordered = true
338 | button.font = .boldSystemFont(ofSize: 11)
339 | button.toolTip = "Send notifications to Microsoft Teams"
340 | button.action = #selector(appDelegate().JamfUploaderTeamsNotifierAction)
341 | appDelegate().processorsView3rdParty.addSubview(button)
342 | }
343 |
344 |
345 | // Functions called from button trigger/action (write output etc)
346 | func JamfAccountUploader() {
347 | output = """
348 |
349 | Arguments
350 |
351 | account_name
352 | %ACCOUNT_NAME%
353 | account_type
354 | user
355 | account_template
356 | /path/to/XML
357 |
358 | Processor
359 | com.github.grahampugh.jamf-upload.processors/JamfAccountUploader
360 |
361 | """
362 | writeOutput()
363 | }
364 |
365 | func JamfCategoryUploader () {
366 | output = """
367 |
368 | Arguments
369 |
370 | category_name
371 | %CATEGORY%
372 |
373 | Processor
374 | com.github.grahampugh.jamf-upload.processors/JamfCategoryUploader
375 |
376 | """
377 | writeOutput ()
378 | }
379 |
380 | func JamfClassicAPIObjectUploader() {
381 | output = """
382 |
383 | Arguments
384 |
385 | object_name
386 | %OBJECT_NAME%
387 | object_type
388 | %OBJECT_TYPE%
389 | object_template
390 | /path/to/template.xml
391 |
392 | Processor
393 | com.github.grahampugh.jamf-upload.processors/JamfClassicAPIObjectUploader
394 |
395 | """
396 | writeOutput()
397 | }
398 |
399 | func JamfComputerGroupDeleter() {
400 | output = """
401 |
402 | Arguments
403 |
404 | computergroup_name
405 | %GROUP_NAME%
406 |
407 | Processor
408 | com.github.grahampugh.jamf-upload.processors/JamfComputerGroupDeleter
409 |
410 | """
411 | writeOutput()
412 | }
413 |
414 | func JamfComputerGroupUploader () {
415 | output = """
416 |
417 | Arguments
418 |
419 | computergroup_name
420 | %GROUP_NAME%
421 | computergroup_template
422 | /path/to/template.xml
423 |
424 | Processor
425 | com.github.grahampugh.jamf-upload.processors/JamfComputerGroupUploader
426 |
427 | """
428 | writeOutput ()
429 | }
430 |
431 | func JamfComputerProfileUploader () {
432 | output = """
433 |
434 | Arguments
435 |
436 | profile_name
437 | %PROFILE_NAME%
438 | mobileconfig
439 | /path/to/mobileconfig
440 | identifier
441 | Configuration Profile payload identifier
442 | profile_category
443 | Category to assign to the profile
444 | organization
445 | Organization to assign to the profile
446 | profile_description
447 | Description to assign to the profile
448 | profile_computergroup
449 | Device group that will be scoped to the profile
450 |
451 | Processor
452 | com.github.grahampugh.jamf-upload.processors/JamfComputerProfileUploader
453 |
454 | """
455 | writeOutput ()
456 | }
457 |
458 | func JamfDockItemUploader () {
459 | output = """
460 |
461 | Arguments
462 |
463 | dock_item_name
464 | %NAME%
465 | dock_item_type
466 | App
467 | dock_item_path
468 | file:///Applications/AppName.app/
469 | replace_dock_item
470 | False
471 |
472 | Processor
473 | com.github.grahampugh.jamf-upload.processors/JamfDockItemUploader
474 |
475 | """
476 | writeOutput ()
477 | }
478 |
479 | func JamfExtensionAttributeUploader () {
480 | output = """
481 |
482 | Arguments
483 |
484 | ea_name
485 | %EXTENSION_ATTRIBUTE_NAME%
486 | ea_script_path
487 | %EXTENSION_ATTRIBUTE_SCRIPT%
488 |
489 | Processor
490 | com.github.grahampugh.jamf-upload.processors/JamfExtensionAttributeUploader
491 |
492 | """
493 | writeOutput ()
494 | }
495 |
496 | func JamfIconUploader() {
497 | output = """
498 |
499 | Arguments
500 |
501 | icon_file
502 | /path/to/icon.png
503 |
504 | Processor
505 | com.github.grahampugh.jamf-upload.processors/JamfIconUploader
506 |
507 | """
508 | writeOutput()
509 | }
510 |
511 | func JamfMacAppUploader () {
512 | output = """
513 |
514 | Arguments
515 |
516 | macapp_name
517 | Mac App Store app name
518 | macapp_template
519 | /path/to/template.xml
520 |
521 | Processor
522 | com.github.grahampugh.jamf-upload.processors/JamfMacAppUploader
523 |
524 | """
525 | writeOutput ()
526 | }
527 |
528 |
529 | func JamfMobileDeviceGroupUploader() {
530 | output = """
531 |
532 | Arguments
533 |
534 | mobiledevicegroup_name
535 | %GROUP_NAME%
536 | mobiledevicegroup_template
537 | /path/to/template.xml
538 |
539 | Processor
540 | com.github.grahampugh.jamf-upload.processors/JamfMobileDeviceGroupUploader
541 |
542 | """
543 | writeOutput()
544 | }
545 |
546 | func JamfMobileDeviceProfileUploader() {
547 | output = """
548 |
549 | Arguments
550 |
551 | profile_name
552 | %PROFILE_NAME%
553 | mobileconfig
554 | /path/to/mobileconfig
555 | identifier
556 | Configuration Profile payload identifier
557 | profile_category
558 | Category to assign to the profile
559 | organization
560 | Organization to assign to the profile
561 | profile_description
562 | Description to assign to the profile
563 | profile_mobiledevicegroup
564 | Device group that will be scoped to the profile
565 |
566 | Processor
567 | com.github.grahampugh.jamf-upload.processors/JamfMobileDeviceProfileUploader
568 |
569 | """
570 | writeOutput()
571 | }
572 |
573 | func JamfPackageCleaner() {
574 | output = """
575 |
576 | Arguments
577 |
578 | pkg_name_match
579 | %NAME%-
580 | versions_to_keep
581 | 5
582 |
583 | Processor
584 | com.github.grahampugh.jamf-upload.processors/JamfPackageCleaner
585 |
586 | """
587 | writeOutput()
588 | }
589 |
590 | func JamfPackageRecalculator() {
591 | output = """
592 |
593 | Processor
594 | com.github.grahampugh.jamf-upload.processors/JamfPackageRecalculator
595 |
596 | """
597 | writeOutput()
598 | }
599 |
600 | func JamfPackageUploader () {
601 | output = """
602 |
603 | Arguments
604 |
605 | pkg_category
606 | %CATEGORY%
607 |
608 | Processor
609 | com.github.grahampugh.jamf-upload.processors/JamfPackageUploader
610 |
611 | """
612 | writeOutput ()
613 | }
614 |
615 | func JamfPatchChecker() {
616 | output = """
617 |
618 | Arguments
619 |
620 | patch_softwaretitle
621 | Name of the patch softwaretitle (e.g. 'Mozilla Firefox') used in Jamf.
622 |
623 | Processor
624 | com.github.grahampugh.jamf-upload.processors/JamfPatchChecker
625 |
626 | """
627 | writeOutput()
628 | }
629 |
630 |
631 | func JamfPatchUploader () {
632 | output = """
633 |
634 | Arguments
635 |
636 | patch_softwaretitle
637 | %NAME%
638 | patch_name
639 | %PATCH_NAME%
640 | patch_template
641 | PatchTemplate-selfservice.xml
642 | patch_icon_policy_name
643 | Install Latest %NAME%
644 |
645 | Processor
646 | com.github.grahampugh.jamf-upload.processors/JamfPatchUploader
647 |
648 | """
649 | writeOutput ()
650 | }
651 |
652 |
653 | func JamfPkgMetadataUploader() {
654 | output = """
655 |
656 | Arguments
657 |
658 | pkg_display_name
659 | Package display name
660 | pkg_category
661 | Package category
662 | pkg_info
663 | Package info field
664 | pkg_notes
665 | Package notes field
666 | pkg_priority
667 | Package priority. Default=10
668 | reboot_required
669 | Default='False'
670 | os_requirements
671 | Package OS requirement
672 | required_processor
673 | Package required processor. Acceptable values are 'x86' or 'None'
674 | send_notification
675 | Whether to send a notification when a package is installed. Default='False'
676 | replace_pkg_metadata
677 | Overwrite existing package metadata and continue if True
678 |
679 | Processor
680 | com.github.grahampugh.jamf-upload.processors/JamfPkgMetadataUploader
681 |
682 | """
683 | writeOutput()
684 | }
685 |
686 | func JamfPolicyDeleter() {
687 | output = """
688 |
689 | Arguments
690 |
691 | policy_name
692 | %POLICY_NAME%
693 |
694 | Processor
695 | com.github.grahampugh.jamf-upload.processors/JamfPolicyDeleter
696 |
697 | """
698 | writeOutput ()
699 | }
700 |
701 | func JamfPolicyLogFlusher() {
702 | output = """
703 |
704 | Arguments
705 |
706 | policy_name
707 | %POLICY_NAME%
708 | logflush_interval
709 | Zero Days
710 |
711 | Processor
712 | com.github.grahampugh.jamf-upload.processors/JamfPolicyLogFlusher
713 |
714 | """
715 | writeOutput ()
716 | }
717 |
718 |
719 | func JamfPolicyUploader () {
720 | output = """
721 |
722 | Arguments
723 |
724 | icon
725 | %SELF_SERVICE_ICON%
726 | policy_name
727 | %POLICY_NAME%
728 | policy_template
729 | %POLICY_TEMPLATE%
730 |
731 | Processor
732 | com.github.grahampugh.jamf-upload.processors/JamfPolicyUploader
733 |
734 | """
735 | writeOutput ()
736 | }
737 |
738 | func JamfScriptUploader () {
739 | output = """
740 |
741 | Arguments
742 |
743 | script_path
744 | /path/to/script.sh
745 | script_category
746 | %SCRIPT_CATEGORY%
747 | script_priority
748 | After
749 |
750 | Processor
751 | com.github.grahampugh.jamf-upload.processors/JamfScriptUploader
752 |
753 | """
754 | writeOutput ()
755 | }
756 |
757 | func JamfSoftwareRestrictionUploader () {
758 | output = """
759 |
760 | Arguments
761 |
762 | restriction_name
763 | Name of Restriction
764 | restriction_template
765 | /path/to/template.xml
766 |
767 | Processor
768 | com.github.grahampugh.jamf-upload.processors/JamfSoftwareRestrictionUploader
769 |
770 | """
771 | writeOutput ()
772 | }
773 |
774 | func JamfUploaderSlacker () {
775 | output = """
776 |
777 | Arguments
778 |
779 | NAME
780 | Name of the application
781 | slack_webhook_url
782 | https://slack.webhook.url
783 |
784 | Processor
785 | com.github.grahampugh.jamf-upload.processors/JamfUploaderSlacker
786 |
787 | """
788 | writeOutput ()
789 | }
790 |
791 | func JamfUploaderTeamsNotifier () {
792 | output = """
793 |
794 | Arguments
795 |
796 | NAME
797 | Name of the application
798 | teams_webhook_url
799 | https://teams.webhook.url
800 |
801 | Processor
802 | com.github.grahampugh.jamf-upload.processors/JamfUploaderTeamsNotifier
803 |
804 | """
805 | writeOutput ()
806 | }
807 |
--------------------------------------------------------------------------------
/RecipeBuilder/CheckAndVerify.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CheckAndVerify.swift
3 | //
4 | // Created by Mikael Löfgren on 2024-12-27
5 | // Copyright © 2024 Mikael Löfgren. All rights reserved.
6 | //
7 |
8 | import Foundation
9 | import Cocoa
10 |
11 | // Inspired by: https://github.com/homebysix/pre-commit-macadmin/blob/main/pre_commit_hooks/check_autopkg_recipes.py
12 |
13 | func returnProcessVersion (processName: String) -> String {
14 | var returnVersion = ""
15 |
16 | struct ProcessVersions {
17 | var name: String
18 | var version: String
19 | }
20 |
21 | var processandversions: [ProcessVersions]
22 | processandversions = [
23 | ProcessVersions(name:"AppDmgVersioner", version:"0"), // no version needed?
24 | ProcessVersions(name:"AppPkgCreator", version:"1.0"),
25 | ProcessVersions(name:"CodeSignatureVerifier", version:"0.3.1"),
26 | ProcessVersions(name:"Copier", version:"0"), // no version needed?
27 | ProcessVersions(name:"CURLTextSearcher", version:"0.5.1"),
28 | ProcessVersions(name:"DeprecationWarning", version:"1.1"),
29 | ProcessVersions(name:"DmgCreator", version:"0"), // no version needed?
30 | ProcessVersions(name:"EndOfCheckPhase", version:"0.1.0"),
31 | ProcessVersions(name:"FileCreator", version:"0"), // no version needed?
32 | ProcessVersions(name:"FileFinder", version:"0.2.3"),
33 | ProcessVersions(name:"FileMover", version:"0.2.9"),
34 | ProcessVersions(name:"FlatPkgPacker", version:"0.2.4"),
35 | ProcessVersions(name:"FlatPkgUnpacker", version:"0.1.0"),
36 | ProcessVersions(name:"GitHubReleasesInfoProvider", version:"0.5.0"),
37 | ProcessVersions(name:"Installer", version:"0.4.0"),
38 | ProcessVersions(name:"InstallFromDMG", version:"0.4.0"),
39 | ProcessVersions(name:"MunkiCatalogBuilder", version:"0.1.0"),
40 | ProcessVersions(name:"MunkiImporter", version:"0.1.0"),
41 | ProcessVersions(name:"MunkiInfoCreator", version:"0"), // no version needed?
42 | ProcessVersions(name:"MunkiInstallsItemsCreator", version:"0.1.0"),
43 | ProcessVersions(name:"MunkiPkginfoMerger", version:"0.1.0"),
44 | ProcessVersions(name:"MunkiSetDefaultCatalog", version:"0.4.2"),
45 | ProcessVersions(name:"PackageRequired", version:"0.5.1"),
46 | ProcessVersions(name:"PathDeleter", version:"0.1.0"),
47 | ProcessVersions(name:"PkgCopier", version:"0.1.0"),
48 | ProcessVersions(name:"PkgCreator", version:"0"),// no version needed?
49 | ProcessVersions(name:"PkgExtractor", version:"0.1.0"),
50 | ProcessVersions(name:"PkgInfoCreator", version:"0"),// no version needed?
51 | ProcessVersions(name:"PkgPayloadUnpacker", version:"0.1.0"),
52 | ProcessVersions(name:"PkgRootCreator", version:"0"),// no version needed?
53 | ProcessVersions(name:"PlistEditor", version:"0.1.0"),
54 | ProcessVersions(name:"PlistReader", version:"0.2.5"),
55 | ProcessVersions(name:"SparkleUpdateInfoProvider", version:"0.1.0"),
56 | ProcessVersions(name:"StopProcessingIf", version:"0.1.0"),
57 | ProcessVersions(name:"Symlinker", version:"0.1.0"),
58 | ProcessVersions(name:"Unarchiver", version:"0.1.0"),
59 | ProcessVersions(name:"URLDownloader", version:"0"), // no version needed?
60 | ProcessVersions(name:"URLTextSearcher", version:"0.2.9"),
61 | ProcessVersions(name:"Versioner", version:"0.1.0")
62 | ]
63 |
64 | let nameArray = processandversions.filter{$0.name.hasPrefix("\(processName)")}.map{$0.version}
65 |
66 | if !nameArray.isEmpty {
67 | returnVersion = nameArray[0]
68 | }
69 | return returnVersion
70 | }
71 |
72 | // Not in use
73 | //struct InputValues:Codable {
74 | //var Name:String
75 | //private enum CodingKeys : String, CodingKey {
76 | // case Name = "NAME"
77 | //}
78 | //}
79 |
80 | struct ProcessValues:Codable {
81 | var Processor:String
82 | private enum CodingKeys : String, CodingKey {
83 | case Processor
84 | }
85 | }
86 |
87 | struct RecipePlistConfig:Codable {
88 | var Description: String?
89 | var Identifier: String
90 | var MinimumVersion: String?
91 | var ParentRecipe: String?
92 | //var Input: InputValues?
93 | var Process: [ProcessValues]?
94 |
95 | private enum CodingKeys : String, CodingKey {
96 | case Description = "Description"
97 | case Identifier = "Identifier"
98 | case MinimumVersion = "MinimumVersion"
99 | case ParentRecipe = "ParentRecipe"
100 | //case Input = "Input"
101 | case Process
102 | }
103 | }
104 |
105 |
106 | struct Recipe {
107 | let identifier: String
108 | let path: String
109 | }
110 |
111 | var recipesArraySet: [Recipe] = []
112 | var recipesArray: [String] = []
113 | var checkResults = ""
114 |
115 | func checkDuplicates(array: [String]) -> String {
116 | var encountered = Set()
117 | var duplicate = Set()
118 | var result = ""
119 |
120 | for value in array {
121 | if encountered.contains(value) {
122 | // Add duplicate to the set
123 | duplicate.insert(value)
124 | }
125 | else {
126 | // Add value to the set
127 | encountered.insert(value)
128 | }
129 | }
130 |
131 | for all in duplicate {
132 | result += "\n⚠️ Duplicated identifier\nIdentifier: \(all)\n"
133 | for recipePath in recipesArraySet {
134 | if recipePath.identifier == all {
135 | // Append the filepath to results
136 | result += "File: \(recipePath.path)\n"
137 | }
138 | }
139 | }
140 | return result
141 | }
142 |
143 |
144 |
145 | func startCheckAndVerify () {
146 |
147 | var defaultPath = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/AutoPkg/").path
148 |
149 | if !FileManager.default.fileExists(atPath: defaultPath) { defaultPath = FileManager.default.homeDirectoryForCurrentUser.path }
150 |
151 | let dialog = NSOpenPanel();
152 | dialog.message = "Choose a folder to verify .recipes files from"
153 | dialog.directoryURL = NSURL.fileURL(withPath: defaultPath, isDirectory: true)
154 | dialog.showsResizeIndicator = true;
155 | dialog.showsHiddenFiles = true;
156 | dialog.canCreateDirectories = false;
157 | dialog.canChooseDirectories = true;
158 | dialog.canChooseFiles = false;
159 | dialog.allowsMultipleSelection = false;
160 |
161 |
162 |
163 |
164 | if (dialog.runModal() == NSApplication.ModalResponse.OK) {
165 | let result = dialog.url
166 |
167 | if (result != nil) {
168 | defaultPath = result!.path
169 | }
170 | } else {
171 | // User clicked on "Cancel"
172 | appDelegate().fileOptions.selectItem(at: 0)
173 | appDelegate().spinner.isHidden=true
174 | return
175 | }
176 |
177 | let recipeRepoDir = defaultPath
178 | if !FileManager.default.fileExists(atPath: recipeRepoDir) {
179 | print("Path doesnt exist at: \(recipeRepoDir)")
180 | appDelegate().fileOptions.selectItem(at: 0)
181 | appDelegate().spinner.isHidden=true
182 | return }
183 |
184 |
185 | let enumerator = FileManager.default.enumerator(atPath: recipeRepoDir)
186 | let filePaths = enumerator?.allObjects as! [String]
187 | var recipeFilePaths = filePaths.filter{$0.hasSuffix(".recipe")}
188 | var recipeYamlFilePaths = filePaths.filter{$0.hasSuffix(".recipe.yaml")}
189 | let totalRecipes = recipeFilePaths.count + recipeYamlFilePaths.count
190 |
191 | if recipeFilePaths.isEmpty {
192 | appDelegate().fileOptions.selectItem(at: 0)
193 | appDelegate().spinner.isHidden=true
194 | return
195 | }
196 |
197 | let startText = "Start checking \(totalRecipes) recipes files..."
198 | appDelegate().spinner.isHidden=false
199 | appDelegate().spinner.startAnimation(appDelegate)
200 | appDelegate().logWindow.orderFront(Any?.self)
201 | highlightr!.theme.codeFont = NSFont(name: "Menlo", size: 12)
202 | let highlightedCode = highlightr!.highlight(startText, as: "bash")!
203 | appDelegate().logTextView.string = ""
204 | appDelegate().logTextView.textStorage?.insert(NSAttributedString(attributedString: highlightedCode), at: 0)
205 |
206 | DispatchQueue.global(qos: .userInteractive).async {
207 |
208 |
209 | for recipes in recipeFilePaths {
210 | let path = "\(recipeRepoDir)/\(recipes)"
211 |
212 | func parseRecipe() -> RecipePlistConfig {
213 | let url = URL(fileURLWithPath: path)
214 | guard let data = try? Data(contentsOf: url),
215 | let preferences = try? PropertyListDecoder().decode(RecipePlistConfig.self, from: data)
216 | else {
217 | // If problem parsing file set Identifier to Empty
218 | return RecipePlistConfig(Identifier: "Empty")
219 | }
220 |
221 | return preferences
222 | }
223 | var minimumVersion = "MISSING"
224 | if parseRecipe().MinimumVersion != nil {
225 | // Value from plist
226 | minimumVersion = parseRecipe().MinimumVersion!
227 | }
228 |
229 | func checkProcessors () {
230 | var processArray: [String] = []
231 | if parseRecipe().Process != nil {
232 | for all in parseRecipe().Process! {
233 |
234 | // Value of process minimum (minimum version of autopkg required to run this recipe)
235 | // and value from plist should be higher or equal
236 | let processVersion = returnProcessVersion(processName: all.Processor)
237 |
238 | // if the vaules are Ascending 1..2..3 or same then ok
239 | let versionCheck = processVersion.compare(minimumVersion, options: .numeric)
240 | let ascending = versionCheck == ComparisonResult.orderedAscending
241 | let orderedSame = versionCheck == ComparisonResult.orderedSame
242 | //var descending = versionCheck == ComparisonResult.orderedDescending
243 |
244 | if ascending == false && orderedSame == false {
245 | checkResults += "\n⚠️ Processor: \(all.Processor) needs MinimumVersion: \(processVersion), now version: \(minimumVersion) is set in:\nFile: \(path)\n"
246 |
247 | // print(all.Processor)
248 | // print("Processor version:\(processVersion)")
249 | // print("Plist version:\(minimumVersion)")
250 | }
251 |
252 | // Check deprecated processors
253 | if all.Processor == "CURLDownloader" {
254 | checkResults += "\n⚠️ Processor: \(all.Processor) is deprecated\nFile: \(path)\n"
255 | }
256 | if all.Processor == "BrewCaskInfoProvider" {
257 | checkResults += "\n⚠️ Processor: \(all.Processor) is deprecated\nFile: \(path)\n"
258 | }
259 | // Warn if any superclass processors
260 | if all.Processor == "URLGetter" {
261 | checkResults += "\n⚠️ Processor: \(all.Processor) is a superclass processor\nFile: \(path)\n"
262 | }
263 | processArray.append(all.Processor)
264 | }
265 | }
266 |
267 |
268 | // Check EndofCheckPhase is after URLDownloader using Array with Index
269 | let urlDownloader = processArray.firstIndex(where: {$0 == "URLDownloader"})
270 | let endOfCheckPhase = processArray.firstIndex(where: {$0 == "EndOfCheckPhase"})
271 | if urlDownloader != nil && endOfCheckPhase != nil {
272 | // Convert to Int
273 | let urlDownloaderInt = Int(urlDownloader!)
274 | let endOfCheckPhaseInt = Int(endOfCheckPhase!)
275 |
276 | if urlDownloaderInt > endOfCheckPhaseInt {
277 | checkResults += "\n⚠️ Processor: URLDownloader should be used before Processor: EndOfCheckPhase\nFile: \(path)\n"
278 | }
279 |
280 | if urlDownloaderInt + 1 < endOfCheckPhaseInt {
281 | checkResults += "\n⚠️ Processor: EndOfCheckPhase is recommended to be directly after Processor: URLDownloader\nFile: \(path)\n"
282 | }
283 |
284 | } else {
285 | if urlDownloader != nil && endOfCheckPhase == nil {
286 | checkResults += "\n⚠️ Processor: URLDownloader exist but missing Processor: EndOfCheckPhase\nFile: \(path)\n"
287 | }
288 |
289 | }
290 | }
291 |
292 | checkProcessors ()
293 |
294 | if parseRecipe().Identifier != "Empty" {
295 | // Add Identifier to Array so we can check for duplicates
296 | recipesArraySet.append(Recipe(identifier: parseRecipe().Identifier, path: path))
297 |
298 | if parseRecipe().ParentRecipe != nil {
299 | // If ParentRecipe is the same as Identifier then show warning
300 | if parseRecipe().ParentRecipe == parseRecipe().Identifier {
301 | checkResults += "\n🛑 ParentRecipe is the same as Identifier\nFile: \(path)\n"
302 | }
303 | }
304 | } else {
305 | // We get this error: Failed to set posix_spawn_file_actions for fd -1 at index 0 with errno 9
306 | // if we process to many files like ~3000 with a shell command
307 | let plutil = shell("/usr/bin/plutil \"\(path)\"").replacingOccurrences(of: ":", with: "\nError:", options: [.regularExpression, .caseInsensitive])
308 | checkResults += "\n🛑 Error reading file\nFile: \(plutil)\n"
309 | }
310 | }
311 |
312 | if !recipeYamlFilePaths.isEmpty {
313 | for yamlRecipes in recipeYamlFilePaths {
314 | let path = "\(recipeRepoDir)/\(yamlRecipes)"
315 | var yamlID = ""
316 | if freopen(path, "r", stdin) == nil {
317 | perror(path)
318 | }
319 | while let line = readLine() {
320 | if line.contains("Identifier:") {
321 | yamlID = line.replacingOccurrences(of: "Identifier: ", with: "", options: [.regularExpression, .caseInsensitive])
322 | recipesArraySet.append(Recipe(identifier: yamlID, path: path))
323 | }
324 |
325 | if line.contains("ParentRecipe:") {
326 | let yamlParentRecipe = line.replacingOccurrences(of: "ParentRecipe: ", with: "", options: [.regularExpression, .caseInsensitive])
327 |
328 | if yamlParentRecipe == yamlID {
329 | checkResults += "\n🛑 ParentRecipe is the same as Identifier\nFile: \(path)\n"
330 | }
331 | }
332 |
333 | if line.contains("MinimumVersion:") {
334 | var yamlMinimumVersion: String
335 | yamlMinimumVersion = line.replacingOccurrences(of: "MinimumVersion: ", with: "", options: [.regularExpression, .caseInsensitive])
336 | yamlMinimumVersion = yamlMinimumVersion.replacingOccurrences(of: "\"", with: "", options: [.regularExpression, .caseInsensitive])
337 | yamlMinimumVersion = yamlMinimumVersion.replacingOccurrences(of: "'", with: "", options: [.regularExpression, .caseInsensitive])
338 | let yamlMinimumVersionDouble = Double(yamlMinimumVersion) ?? 0
339 |
340 | if yamlMinimumVersionDouble < 2.3 {
341 | checkResults += "\n⚠️ MinimumVersion is lower then 2.3\nFile: \(path)\n"
342 | }
343 | }
344 | }
345 | }
346 | }
347 |
348 | DispatchQueue.main.async {
349 | // Back on the main thread
350 | recipesArray = recipesArraySet.map({ $0.identifier })
351 |
352 | checkResults += checkDuplicates(array: recipesArray)
353 |
354 | if checkResults.isEmpty {
355 | checkResults += ("✅ No problems found\n")
356 | }
357 |
358 | checkResults += """
359 | --------------------------------------------
360 | Done checking \(totalRecipes) recipes files\n
361 | """
362 | appDelegate().logWindow.orderFront(Any?.self)
363 | highlightr!.theme.codeFont = NSFont(name: "Menlo", size: 12)
364 | let highlightedCode = highlightr!.highlight(checkResults, as: "bash")!
365 | appDelegate().logTextView.string = ""
366 | appDelegate().logTextView.textStorage?.insert(NSAttributedString(attributedString: highlightedCode), at: 0)
367 | appDelegate().fileOptions.selectItem(at: 0)
368 | appDelegate().spinner.isHidden=true
369 | // Reset all values
370 | recipeFilePaths.removeAll()
371 | recipeYamlFilePaths.removeAll()
372 | recipesArray.removeAll()
373 | recipesArraySet.removeAll()
374 | checkResults = ""
375 |
376 | }
377 | }
378 | }
379 |
--------------------------------------------------------------------------------
/RecipeBuilder/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDocumentTypes
8 |
9 |
10 | CFBundleTypeName
11 | RecipeBuilder text document
12 | CFBundleTypeRole
13 | Editor
14 | LSHandlerRank
15 | Alternate
16 | LSItemContentTypes
17 |
18 | com.apple.property-list
19 | com.apple.xml-property-list
20 | public.xml
21 |
22 |
23 |
24 | CFBundleExecutable
25 | $(EXECUTABLE_NAME)
26 | CFBundleIconFile
27 |
28 | CFBundleIdentifier
29 | $(PRODUCT_BUNDLE_IDENTIFIER)
30 | CFBundleInfoDictionaryVersion
31 | 6.0
32 | CFBundleName
33 | $(PRODUCT_NAME)
34 | CFBundlePackageType
35 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
36 | CFBundleShortVersionString
37 | $(MARKETING_VERSION)
38 | CFBundleVersion
39 | 1
40 | LSApplicationCategoryType
41 | public.app-category.utilities
42 | LSMinimumSystemVersion
43 | $(MACOSX_DEPLOYMENT_TARGET)
44 | NSHumanReadableCopyright
45 | Copyright © 2025 Mikael Löfgren. All rights reserved.
46 | NSMainNibFile
47 | MainMenu
48 | NSPrincipalClass
49 | NSApplication
50 | NSSupportsAutomaticTermination
51 |
52 | NSSupportsSuddenTermination
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/RecipeBuilder/JamfUploaderHelpTexts.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JamfUploaderHelpTexts.swift
3 | //
4 | // Created by Mikael Löfgren on 2024-12-27
5 | // Copyright © 2024 Mikael Löfgren. All rights reserved.
6 | //
7 |
8 | import Foundation
9 |
10 | var JamfAccountUploaderHelp = ""
11 | var JamfCategoryUploaderHelp = ""
12 | var JamfClassicAPIObjectUploaderHelp = ""
13 | var JamfComputerGroupDeleterHelp = ""
14 | var JamfComputerGroupUploaderHelp = ""
15 | var JamfComputerProfileUploaderHelp = ""
16 | var JamfDockItemUploaderHelp = ""
17 | var JamfExtensionAttributeUploaderHelp = ""
18 | var JamfIconUploaderHelp = ""
19 | var JamfMacAppUploaderHelp = ""
20 | var JamfMobileDeviceGroupUploaderHelp = ""
21 | var JamfMobileDeviceProfileUploaderHelp = ""
22 | var JamfPackageCleanerHelp = ""
23 | var JamfPackageRecalculatorHelp = ""
24 | var JamfPackageUploaderHelp = ""
25 | var JamfPatchCheckerHelp = ""
26 | var JamfPatchUploaderHelp = ""
27 | var JamfPkgMetadataUploaderHelp = ""
28 | var JamfPolicyDeleterHelp = ""
29 | var JamfPolicyLogFlusherHelp = ""
30 | var JamfPolicyUploaderHelp = ""
31 | var JamfScriptUploaderHelp = ""
32 | var JamfSoftwareRestrictionUploaderHelp = ""
33 | var JamfUploaderSlackerHelp = ""
34 | var JamfUploaderTeamsNotifierHelp = ""
35 |
--------------------------------------------------------------------------------
/RecipeBuilder/RecipeBuilder.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/RecipeBuilder/UserButtons.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserButtons.swift
3 | //
4 | // Created by Mikael Löfgren on 2024-12-27
5 | // Copyright © 2024 Mikael Löfgren. All rights reserved.
6 | //
7 |
8 |
9 | import Cocoa
10 | import AppKit
11 | import Foundation
12 |
13 |
14 | var allButtonsFolders = ""
15 | let recipeBuilderButtonsFolder = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Application Support/RecipeBuilder/").path
16 | var titleFileString = ""
17 | var outputFileString = ""
18 | var helpFileString = ""
19 | var titleFile = URL(fileURLWithPath: "")
20 | var outputFile = URL(fileURLWithPath: "")
21 | var helpFile = URL(fileURLWithPath: "")
22 | var title1 = ""
23 | var output1 = ""
24 | var help1 = ""
25 |
26 | var title2 = ""
27 | var output2 = ""
28 | var help2 = ""
29 |
30 | var title3 = ""
31 | var output3 = ""
32 | var help3 = ""
33 |
34 | var title4 = ""
35 | var output4 = ""
36 | var help4 = ""
37 |
38 | var title5 = ""
39 | var output5 = ""
40 | var help5 = ""
41 |
42 | var title6 = ""
43 | var output6 = ""
44 | var help6 = ""
45 |
46 | var title7 = ""
47 | var output7 = ""
48 | var help7 = ""
49 |
50 | var title8 = ""
51 | var output8 = ""
52 | var help8 = ""
53 |
54 | var title9 = ""
55 | var output9 = ""
56 | var help9 = ""
57 |
58 | var title10 = ""
59 | var output10 = ""
60 | var help10 = ""
61 |
62 | func createButton1 (title: String) {
63 | let button = NSButton(frame: NSRect(x: 17, y: 258, width: 191, height: 17))
64 | button.title = title
65 | button.bezelStyle = NSButton.BezelStyle.inline
66 | button.setButtonType(NSButton.ButtonType.momentaryPushIn)
67 | button.isBordered = true
68 | button.font = .boldSystemFont(ofSize: 11)
69 | button.action = #selector(appDelegate().buttonAction1)
70 | appDelegate().buttonView.addSubview(button)
71 | }
72 |
73 |
74 | func createButton2 (title: String) {
75 | let button = NSButton(frame: NSRect(x: 17, y: 237, width: 191, height: 17))
76 | button.title = title
77 | button.bezelStyle = NSButton.BezelStyle.inline
78 | button.setButtonType(NSButton.ButtonType.momentaryPushIn)
79 | button.isBordered = true
80 | button.font = .boldSystemFont(ofSize: 11)
81 | button.action = #selector(appDelegate().buttonAction2)
82 | appDelegate().buttonView.addSubview(button)
83 | }
84 |
85 |
86 | func createButton3 (title: String) {
87 | let button = NSButton(frame: NSRect(x: 17, y: 216, width: 191, height: 17))
88 | button.title = title
89 | button.bezelStyle = NSButton.BezelStyle.inline
90 | button.setButtonType(NSButton.ButtonType.momentaryPushIn)
91 | button.isBordered = true
92 | button.font = .boldSystemFont(ofSize: 11)
93 | button.action = #selector(appDelegate().buttonAction3)
94 | appDelegate().buttonView.addSubview(button)
95 | }
96 |
97 |
98 | func createButton4 (title: String) {
99 | let button = NSButton(frame: NSRect(x: 17, y: 195, width: 191, height: 17))
100 | button.title = title
101 | button.bezelStyle = NSButton.BezelStyle.inline
102 | button.setButtonType(NSButton.ButtonType.momentaryPushIn)
103 | button.isBordered = true
104 | button.font = .boldSystemFont(ofSize: 11)
105 | button.action = #selector(appDelegate().buttonAction4)
106 | appDelegate().buttonView.addSubview(button)
107 | }
108 |
109 |
110 | func createButton5 (title: String) {
111 | let button = NSButton(frame: NSRect(x: 17, y: 174, width: 191, height: 17))
112 | button.title = title
113 | button.bezelStyle = NSButton.BezelStyle.inline
114 | button.setButtonType(NSButton.ButtonType.momentaryPushIn)
115 | button.isBordered = true
116 | button.font = .boldSystemFont(ofSize: 11)
117 | button.action = #selector(appDelegate().buttonAction5)
118 | appDelegate().buttonView.addSubview(button)
119 | }
120 |
121 |
122 | func createButton6 (title: String) {
123 | let button = NSButton(frame: NSRect(x: 17, y: 153, width: 191, height: 17))
124 | button.title = title
125 | button.bezelStyle = NSButton.BezelStyle.inline
126 | button.setButtonType(NSButton.ButtonType.momentaryPushIn)
127 | button.isBordered = true
128 | button.font = .boldSystemFont(ofSize: 11)
129 | button.action = #selector(appDelegate().buttonAction6)
130 | appDelegate().buttonView.addSubview(button)
131 | }
132 |
133 |
134 | func createButton7 (title: String) {
135 | let button = NSButton(frame: NSRect(x: 17, y: 132, width: 191, height: 17))
136 | button.title = title
137 | button.bezelStyle = NSButton.BezelStyle.inline
138 | button.setButtonType(NSButton.ButtonType.momentaryPushIn)
139 | button.isBordered = true
140 | button.font = .boldSystemFont(ofSize: 11)
141 | button.action = #selector(appDelegate().buttonAction7)
142 | appDelegate().buttonView.addSubview(button)
143 | }
144 |
145 |
146 | func createButton8 (title: String) {
147 | let button = NSButton(frame: NSRect(x: 17, y: 111, width: 191, height: 17))
148 | button.title = title
149 | button.bezelStyle = NSButton.BezelStyle.inline
150 | button.setButtonType(NSButton.ButtonType.momentaryPushIn)
151 | button.isBordered = true
152 | button.font = .boldSystemFont(ofSize: 11)
153 | button.action = #selector(appDelegate().buttonAction8)
154 | appDelegate().buttonView.addSubview(button)
155 | }
156 |
157 |
158 | func createButton9 (title: String) {
159 | let button = NSButton(frame: NSRect(x: 17, y: 90, width: 191, height: 17))
160 | button.title = title
161 | button.bezelStyle = NSButton.BezelStyle.inline
162 | button.setButtonType(NSButton.ButtonType.momentaryPushIn)
163 | button.isBordered = true
164 | button.font = .boldSystemFont(ofSize: 11)
165 | button.action = #selector(appDelegate().buttonAction9)
166 | appDelegate().buttonView.addSubview(button)
167 | }
168 |
169 | func createButton10 (title: String) {
170 | let button = NSButton(frame: NSRect(x: 17, y: 69, width: 191, height: 17))
171 | button.title = title
172 | button.bezelStyle = NSButton.BezelStyle.inline
173 | button.setButtonType(NSButton.ButtonType.momentaryPushIn)
174 | button.isBordered = true
175 | button.font = .boldSystemFont(ofSize: 11)
176 | button.action = #selector(appDelegate().buttonAction10)
177 | appDelegate().buttonView.addSubview(button)
178 | }
179 |
180 |
181 | func buttonWarning (infomessage: String) {
182 | let warning = NSAlert()
183 | warning.icon = NSImage(named: "Warning")
184 | warning.addButton(withTitle: "OK")
185 | warning.messageText = "Button is missing required info"
186 | warning.alertStyle = NSAlert.Style.warning
187 | warning.informativeText = """
188 | \(infomessage)
189 | """
190 | warning.runModal()
191 | }
192 |
193 | func checkForUserButtonsAndEnable () {
194 | titleFileString = "\(recipeBuilderButtonsFolder)/1/title.txt"
195 | outputFileString = "\(recipeBuilderButtonsFolder)/1/output.txt"
196 | helpFileString = "\(recipeBuilderButtonsFolder)/1/help.txt"
197 |
198 | // If folder one is missing file, dont enable
199 | if FileManager.default.fileExists(atPath: titleFileString ) && FileManager.default.fileExists(atPath: outputFileString ) {
200 | getAllUserButtons ()
201 | }
202 |
203 | }
204 |
205 |
206 | func openUserButtons () {
207 | if FileManager.default.fileExists(atPath: recipeBuilderButtonsFolder) {
208 | NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: recipeBuilderButtonsFolder)
209 | } else {
210 | return }
211 | appDelegate().fileOptions.selectItem(at: 0)
212 | }
213 |
214 |
215 |
216 | func getAllUserButtons () {
217 |
218 | if FileManager.default.fileExists(atPath: recipeBuilderButtonsFolder) {
219 | } else {
220 | do {
221 | try FileManager.default.createDirectory(atPath: recipeBuilderButtonsFolder, withIntermediateDirectories: true, attributes: nil)
222 | } catch {
223 | print(error)
224 | }
225 | }
226 |
227 | if FileManager.default.fileExists(atPath: "\(recipeBuilderButtonsFolder)/1") {
228 | } else {
229 | do {
230 | try FileManager.default.createDirectory(atPath: "\(recipeBuilderButtonsFolder)/1", withIntermediateDirectories: true, attributes: nil)
231 | let writeTitle = "Insert empty key and string"
232 | let writeOutput = """
233 |
234 |
235 | """
236 | let writeHelp = """
237 | Welcome to UserButtons!
238 |
239 | In folder \"\(recipeBuilderButtonsFolder)\"
240 | you create folders with name 1,2,3..up to 10. You can skip folders, but folder 1 is required if you
241 | want buttons to autostart, otherwise you have to enable them.
242 | In every folder create title.txt and add text to the file for the button name.
243 | Keep it below 26 characters for best result.
244 |
245 | Add output.txt for the output text.
246 |
247 | And help.txt for the Note text your reading right now (optional).
248 | To easy open \"\(recipeBuilderButtonsFolder)\", choose File options - Open user buttons folder
249 | Click the Enable and reload button to create the first "demo" button.
250 | Use that button to reload if you trying out your new buttons.
251 | """
252 | titleFile = URL(fileURLWithPath: "\(recipeBuilderButtonsFolder)/1/title.txt")
253 | outputFile = URL(fileURLWithPath: "\(recipeBuilderButtonsFolder)/1/output.txt")
254 | helpFile = URL(fileURLWithPath: "\(recipeBuilderButtonsFolder)/1/help.txt")
255 | try writeTitle.write(to: titleFile, atomically: true, encoding: String.Encoding.utf8)
256 | try writeOutput.write(to: outputFile, atomically: true, encoding: String.Encoding.utf8)
257 | try writeHelp.write(to: helpFile, atomically: true, encoding: String.Encoding.utf8)
258 | } catch {
259 | print(error)
260 | }
261 | }
262 |
263 | // Remove all the buttons and add them again if they exist
264 | for subview in appDelegate().buttonView.subviews {
265 | if subview is NSButton {
266 | subview.removeFromSuperview()
267 | appDelegate().buttonView.addSubview(appDelegate().enableAndReloadButton)
268 | }
269 | }
270 |
271 |
272 | let documentsPath = recipeBuilderButtonsFolder
273 | let url = URL(fileURLWithPath: documentsPath)
274 | let fileManager = FileManager.default
275 | let enumerator: FileManager.DirectoryEnumerator = fileManager.enumerator(atPath: url.path)!
276 | while let subFolders = enumerator.nextObject() as? String {
277 |
278 | let folderName = subFolders
279 |
280 | switch folderName {
281 | case let folderName where folderName == "1":
282 | titleFileString = "\(recipeBuilderButtonsFolder)/1/title.txt"
283 | outputFileString = "\(recipeBuilderButtonsFolder)/1/output.txt"
284 | helpFileString = "\(recipeBuilderButtonsFolder)/1/help.txt"
285 | titleFile = URL(fileURLWithPath: titleFileString)
286 | outputFile = URL(fileURLWithPath: outputFileString)
287 | helpFile = URL(fileURLWithPath: helpFileString)
288 |
289 | if FileManager.default.fileExists(atPath: titleFileString ) {
290 | do {
291 | let title = try String(contentsOf: titleFile, encoding: String.Encoding.utf8)
292 | if title != "" {
293 | title1 = title
294 | createButton1(title: title1)
295 | } else {
296 | buttonWarning (infomessage: "File: \"\(titleFileString)\" seems empty")
297 | }
298 | } catch {
299 | print(error)
300 | }
301 | }
302 |
303 | if FileManager.default.fileExists(atPath: outputFileString) {
304 | do {
305 | let output = try String(contentsOf: outputFile, encoding: String.Encoding.utf8)
306 | if output != "" {
307 | output1 = output
308 | } else {
309 | buttonWarning (infomessage: "File: \"\(outputFileString)\" seems empty.\nThis want output anything.")
310 | }
311 | } catch {
312 | print(error)
313 | }
314 | }
315 |
316 | if FileManager.default.fileExists(atPath: helpFileString) {
317 | do {
318 | let help = try String(contentsOf: helpFile, encoding: String.Encoding.utf8)
319 | help1 = help
320 | } catch {
321 | print(error)
322 | }
323 | }
324 |
325 |
326 | case let folderName where folderName == "2":
327 | titleFileString = "\(recipeBuilderButtonsFolder)/2/title.txt"
328 | outputFileString = "\(recipeBuilderButtonsFolder)/2/output.txt"
329 | helpFileString = "\(recipeBuilderButtonsFolder)/2/help.txt"
330 | titleFile = URL(fileURLWithPath: titleFileString)
331 | outputFile = URL(fileURLWithPath: outputFileString)
332 | helpFile = URL(fileURLWithPath: helpFileString)
333 |
334 | if FileManager.default.fileExists(atPath: titleFileString ) {
335 | do {
336 | let title = try String(contentsOf: titleFile, encoding: String.Encoding.utf8)
337 | if title != "" {
338 | title2 = title
339 | createButton2(title: title2)
340 | } else {
341 | buttonWarning (infomessage: "File: \"\(titleFileString)\" seems empty")
342 | }
343 | } catch {
344 | print(error)
345 | }
346 | }
347 |
348 | if FileManager.default.fileExists(atPath: outputFileString) {
349 | do {
350 | let output = try String(contentsOf: outputFile, encoding: String.Encoding.utf8)
351 | if output != "" {
352 | output2 = output
353 | } else {
354 | buttonWarning (infomessage: "File: \"\(outputFileString)\" seems empty.\nThis want output anything.")
355 | }
356 | } catch {
357 | print(error)
358 | }
359 | }
360 |
361 | if FileManager.default.fileExists(atPath: helpFileString) {
362 | do {
363 | let help = try String(contentsOf: helpFile, encoding: String.Encoding.utf8)
364 | help2 = help
365 | } catch {
366 | print(error)
367 | }
368 | }
369 |
370 | case let folderName where folderName == "3":
371 | titleFileString = "\(recipeBuilderButtonsFolder)/3/title.txt"
372 | outputFileString = "\(recipeBuilderButtonsFolder)/3/output.txt"
373 | helpFileString = "\(recipeBuilderButtonsFolder)/3/help.txt"
374 | titleFile = URL(fileURLWithPath: titleFileString)
375 | outputFile = URL(fileURLWithPath: outputFileString)
376 | helpFile = URL(fileURLWithPath: helpFileString)
377 |
378 | if FileManager.default.fileExists(atPath: titleFileString ) {
379 | do {
380 | let title = try String(contentsOf: titleFile, encoding: String.Encoding.utf8)
381 | if title != "" {
382 | title3 = title
383 | createButton3(title: title3)
384 | } else {
385 | buttonWarning (infomessage: "File: \"\(titleFileString)\" seems empty")
386 | }
387 | } catch {
388 | print(error)
389 | }
390 | }
391 |
392 | if FileManager.default.fileExists(atPath: outputFileString) {
393 | do {
394 | let output = try String(contentsOf: outputFile, encoding: String.Encoding.utf8)
395 | if output != "" {
396 | output3 = output
397 | } else {
398 | buttonWarning (infomessage: "File: \"\(outputFileString)\" seems empty.\nThis want output anything.")
399 | }
400 | } catch {
401 | print(error)
402 | }
403 | }
404 |
405 | if FileManager.default.fileExists(atPath: helpFileString) {
406 | do {
407 | let help = try String(contentsOf: helpFile, encoding: String.Encoding.utf8)
408 | help3 = help
409 | } catch {
410 | print(error)
411 | }
412 | }
413 |
414 | case let folderName where folderName == "4":
415 | titleFileString = "\(recipeBuilderButtonsFolder)/4/title.txt"
416 | outputFileString = "\(recipeBuilderButtonsFolder)/4/output.txt"
417 | helpFileString = "\(recipeBuilderButtonsFolder)/4/help.txt"
418 | titleFile = URL(fileURLWithPath: titleFileString)
419 | outputFile = URL(fileURLWithPath: outputFileString)
420 | helpFile = URL(fileURLWithPath: helpFileString)
421 |
422 | if FileManager.default.fileExists(atPath: titleFileString ) {
423 | do {
424 | let title = try String(contentsOf: titleFile, encoding: String.Encoding.utf8)
425 | if title != "" {
426 | title4 = title
427 | createButton4(title: title4)
428 | } else {
429 | buttonWarning (infomessage: "File: \"\(titleFileString)\" seems empty")
430 | }
431 | } catch {
432 | print(error)
433 | }
434 | }
435 |
436 | if FileManager.default.fileExists(atPath: outputFileString) {
437 | do {
438 | let output = try String(contentsOf: outputFile, encoding: String.Encoding.utf8)
439 | if output != "" {
440 | output4 = output
441 | } else {
442 | buttonWarning (infomessage: "File: \"\(outputFileString)\" seems empty.\nThis want output anything.")
443 | }
444 | } catch {
445 | print(error)
446 | }
447 | }
448 |
449 | if FileManager.default.fileExists(atPath: helpFileString) {
450 | do {
451 | let help = try String(contentsOf: helpFile, encoding: String.Encoding.utf8)
452 | help4 = help
453 | } catch {
454 | print(error)
455 | }
456 | }
457 |
458 | case let folderName where folderName == "5":
459 | titleFileString = "\(recipeBuilderButtonsFolder)/5/title.txt"
460 | outputFileString = "\(recipeBuilderButtonsFolder)/5/output.txt"
461 | helpFileString = "\(recipeBuilderButtonsFolder)/5/help.txt"
462 | titleFile = URL(fileURLWithPath: titleFileString)
463 | outputFile = URL(fileURLWithPath: outputFileString)
464 | helpFile = URL(fileURLWithPath: helpFileString)
465 |
466 | if FileManager.default.fileExists(atPath: titleFileString ) {
467 | do {
468 | let title = try String(contentsOf: titleFile, encoding: String.Encoding.utf8)
469 | if title != "" {
470 | title5 = title
471 | createButton5(title: title5)
472 | } else {
473 | buttonWarning (infomessage: "File: \"\(titleFileString)\" seems empty")
474 | }
475 | } catch {
476 | print(error)
477 | }
478 | }
479 |
480 | if FileManager.default.fileExists(atPath: outputFileString) {
481 | do {
482 | let output = try String(contentsOf: outputFile, encoding: String.Encoding.utf8)
483 | if output != "" {
484 | output5 = output
485 | } else {
486 | buttonWarning (infomessage: "File: \"\(outputFileString)\" seems empty.\nThis want output anything.")
487 | }
488 | } catch {
489 | print(error)
490 | }
491 | }
492 |
493 | if FileManager.default.fileExists(atPath: helpFileString) {
494 | do {
495 | let help = try String(contentsOf: helpFile, encoding: String.Encoding.utf8)
496 | help5 = help
497 | } catch {
498 | print(error)
499 | }
500 | }
501 |
502 | case let folderName where folderName == "6":
503 | titleFileString = "\(recipeBuilderButtonsFolder)/6/title.txt"
504 | outputFileString = "\(recipeBuilderButtonsFolder)/6/output.txt"
505 | helpFileString = "\(recipeBuilderButtonsFolder)/6/help.txt"
506 | titleFile = URL(fileURLWithPath: titleFileString)
507 | outputFile = URL(fileURLWithPath: outputFileString)
508 | helpFile = URL(fileURLWithPath: helpFileString)
509 |
510 | if FileManager.default.fileExists(atPath: titleFileString ) {
511 | do {
512 | let title = try String(contentsOf: titleFile, encoding: String.Encoding.utf8)
513 | if title != "" {
514 | title6 = title
515 | createButton6(title: title6)
516 | } else {
517 | buttonWarning (infomessage: "File: \"\(titleFileString)\" seems empty")
518 | }
519 | } catch {
520 | print(error)
521 | }
522 | }
523 |
524 | if FileManager.default.fileExists(atPath: outputFileString) {
525 | do {
526 | let output = try String(contentsOf: outputFile, encoding: String.Encoding.utf8)
527 | if output != "" {
528 | output6 = output
529 | } else {
530 | buttonWarning (infomessage: "File: \"\(outputFileString)\" seems empty.\nThis want output anything.")
531 | }
532 | } catch {
533 | print(error)
534 | }
535 | }
536 |
537 | if FileManager.default.fileExists(atPath: helpFileString) {
538 | do {
539 | let help = try String(contentsOf: helpFile, encoding: String.Encoding.utf8)
540 | help6 = help
541 | } catch {
542 | print(error)
543 | }
544 | }
545 |
546 | case let folderName where folderName == "7":
547 | titleFileString = "\(recipeBuilderButtonsFolder)/7/title.txt"
548 | outputFileString = "\(recipeBuilderButtonsFolder)/7/output.txt"
549 | helpFileString = "\(recipeBuilderButtonsFolder)/7/help.txt"
550 | titleFile = URL(fileURLWithPath: titleFileString)
551 | outputFile = URL(fileURLWithPath: outputFileString)
552 | helpFile = URL(fileURLWithPath: helpFileString)
553 |
554 | if FileManager.default.fileExists(atPath: titleFileString ) {
555 | do {
556 | let title = try String(contentsOf: titleFile, encoding: String.Encoding.utf8)
557 | if title != "" {
558 | title7 = title
559 | createButton7(title: title7)
560 | } else {
561 | buttonWarning (infomessage: "File: \"\(titleFileString)\" seems empty")
562 | }
563 | } catch {
564 | print(error)
565 | }
566 | }
567 |
568 | if FileManager.default.fileExists(atPath: outputFileString) {
569 | do {
570 | let output = try String(contentsOf: outputFile, encoding: String.Encoding.utf8)
571 | if output != "" {
572 | output7 = output
573 | } else {
574 | buttonWarning (infomessage: "File: \"\(outputFileString)\" seems empty.\nThis want output anything.")
575 | }
576 | } catch {
577 | print(error)
578 | }
579 | }
580 |
581 | if FileManager.default.fileExists(atPath: helpFileString) {
582 | do {
583 | let help = try String(contentsOf: helpFile, encoding: String.Encoding.utf8)
584 | help7 = help
585 | } catch {
586 | print(error)
587 | }
588 | }
589 |
590 | case let folderName where folderName == "8":
591 | titleFileString = "\(recipeBuilderButtonsFolder)/8/title.txt"
592 | outputFileString = "\(recipeBuilderButtonsFolder)/8/output.txt"
593 | helpFileString = "\(recipeBuilderButtonsFolder)/8/help.txt"
594 | titleFile = URL(fileURLWithPath: titleFileString)
595 | outputFile = URL(fileURLWithPath: outputFileString)
596 | helpFile = URL(fileURLWithPath: helpFileString)
597 |
598 | if FileManager.default.fileExists(atPath: titleFileString ) {
599 | do {
600 | let title = try String(contentsOf: titleFile, encoding: String.Encoding.utf8)
601 | if title != "" {
602 | title8 = title
603 | createButton8(title: title8)
604 | } else {
605 | buttonWarning (infomessage: "File: \"\(titleFileString)\" seems empty")
606 | }
607 | } catch {
608 | print(error)
609 | }
610 | }
611 |
612 | if FileManager.default.fileExists(atPath: outputFileString) {
613 | do {
614 | let output = try String(contentsOf: outputFile, encoding: String.Encoding.utf8)
615 | if output != "" {
616 | output8 = output
617 | } else {
618 | buttonWarning (infomessage: "File: \"\(outputFileString)\" seems empty.\nThis want output anything.")
619 | }
620 | } catch {
621 | print(error)
622 | }
623 | }
624 |
625 | if FileManager.default.fileExists(atPath: helpFileString) {
626 | do {
627 | let help = try String(contentsOf: helpFile, encoding: String.Encoding.utf8)
628 | help8 = help
629 | } catch {
630 | print(error)
631 | }
632 | }
633 |
634 | case let folderName where folderName == "9":
635 | titleFileString = "\(recipeBuilderButtonsFolder)/9/title.txt"
636 | outputFileString = "\(recipeBuilderButtonsFolder)/9/output.txt"
637 | helpFileString = "\(recipeBuilderButtonsFolder)/9/help.txt"
638 | titleFile = URL(fileURLWithPath: titleFileString)
639 | outputFile = URL(fileURLWithPath: outputFileString)
640 | helpFile = URL(fileURLWithPath: helpFileString)
641 |
642 | if FileManager.default.fileExists(atPath: titleFileString ) {
643 | do {
644 | let title = try String(contentsOf: titleFile, encoding: String.Encoding.utf8)
645 | if title != "" {
646 | title9 = title
647 | createButton9(title: title9)
648 | } else {
649 | buttonWarning (infomessage: "File: \"\(titleFileString)\" seems empty")
650 | }
651 | } catch {
652 | print(error)
653 | }
654 | }
655 |
656 | if FileManager.default.fileExists(atPath: outputFileString) {
657 | do {
658 | let output = try String(contentsOf: outputFile, encoding: String.Encoding.utf8)
659 | if output != "" {
660 | output9 = output
661 | } else {
662 | buttonWarning (infomessage: "File: \"\(outputFileString)\" seems empty.\nThis want output anything.")
663 | }
664 | } catch {
665 | print(error)
666 | }
667 | }
668 |
669 | if FileManager.default.fileExists(atPath: helpFileString) {
670 | do {
671 | let help = try String(contentsOf: helpFile, encoding: String.Encoding.utf8)
672 | help9 = help
673 | } catch {
674 | print(error)
675 | }
676 | }
677 |
678 | case let folderName where folderName == "10":
679 | titleFileString = "\(recipeBuilderButtonsFolder)/10/title.txt"
680 | outputFileString = "\(recipeBuilderButtonsFolder)/10/output.txt"
681 | helpFileString = "\(recipeBuilderButtonsFolder)/10/help.txt"
682 | titleFile = URL(fileURLWithPath: titleFileString)
683 | outputFile = URL(fileURLWithPath: outputFileString)
684 | helpFile = URL(fileURLWithPath: helpFileString)
685 |
686 | if FileManager.default.fileExists(atPath: titleFileString ) {
687 | do {
688 | let title = try String(contentsOf: titleFile, encoding: String.Encoding.utf8)
689 | if title != "" {
690 | title10 = title
691 | createButton10(title: title10)
692 | } else {
693 | buttonWarning (infomessage: "File: \"\(titleFileString)\" seems empty")
694 | }
695 | } catch {
696 | print(error)
697 | }
698 | }
699 |
700 | if FileManager.default.fileExists(atPath: outputFileString) {
701 | do {
702 | let output = try String(contentsOf: outputFile, encoding: String.Encoding.utf8)
703 | if output != "" {
704 | output10 = output
705 | } else {
706 | buttonWarning (infomessage: "File: \"\(outputFileString)\" seems empty.\nThis want output anything.")
707 | }
708 | } catch {
709 | print(error)
710 | }
711 | }
712 |
713 | if FileManager.default.fileExists(atPath: helpFileString) {
714 | do {
715 | let help = try String(contentsOf: helpFile, encoding: String.Encoding.utf8)
716 | help10 = help
717 | } catch {
718 | print(error)
719 | }
720 | }
721 | default: break
722 | }
723 | }
724 |
725 | }
726 |
--------------------------------------------------------------------------------
/RecipeBuilder/XMLtoYaml.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Yams
3 | import AppKit
4 |
5 |
6 | // Function to parse XML into Dictionary
7 | func xmlToDictionary(xmlString: String) -> [String: Any]? {
8 | guard let data = xmlString.data(using: .utf8) else {
9 | print("Error converting XML string to Data")
10 | outputToLogView(logString: "⚠️ Error converting XML string to Data\n")
11 | return nil
12 | }
13 |
14 | do {
15 | let plist = try PropertyListSerialization.propertyList(from: data, options: [], format: nil)
16 | if var dictionary = plist as? [String: Any] {
17 | // Handle Process array
18 | if let processArray = dictionary["Process"] as? [[String: Any]] {
19 | let modifiedProcessArray = processArray.map { processStep -> [String: Any] in
20 | var modifiedStep = processStep
21 |
22 | // Only create Arguments if there's a Comment or if Arguments already exists
23 | if let comment = modifiedStep["Comment"] as? String {
24 | var arguments = modifiedStep["Arguments"] as? [String: Any] ?? [:]
25 | arguments["Comment"] = comment
26 | modifiedStep["Arguments"] = arguments
27 | modifiedStep.removeValue(forKey: "Comment")
28 | }
29 |
30 | return modifiedStep
31 | }
32 | dictionary["Process"] = modifiedProcessArray
33 | }
34 | return dictionary
35 | } else {
36 | print("Error: Expected dictionary but got something else")
37 | outputToLogView(logString: "⚠️ Error: Expected dictionary but got something else\n")
38 | return nil
39 | }
40 | } catch {
41 | print("Error parsing XML: \(error)")
42 | outputToLogView(logString: "⚠️ Error parsing XML: \(error)\n")
43 | return nil
44 | }
45 | }
46 |
47 |
48 | // Function to sanitize values for YAML compatibility
49 | func sanitizeValueForYAML(_ value: Any, key: String? = nil) -> Any {
50 | if let number = value as? NSNumber {
51 | if number === kCFBooleanTrue || number === kCFBooleanFalse {
52 | return number.boolValue
53 | } else {
54 | return number.intValue
55 | }
56 | } else if let string = value as? String {
57 | let nameOfKey = key ?? ""
58 |
59 | // Sub function check if string most likely are a regex
60 | func isLikelyRegex(_ string: String) -> Bool {
61 | let regexIndicators = ["[", "]", "(", ")", "{", "}", "\\", ".", "+", "*", "?", "^", "$", "|"]
62 | return regexIndicators.contains { string.contains($0) }
63 | }
64 |
65 | // Single quote all regex
66 | if (nameOfKey == "re_pattern" || nameOfKey == "asset_regex") && isLikelyRegex(string) &&
67 | !(string.hasPrefix("'") && !string.hasSuffix("'") && !string.dropFirst().dropLast().contains("'")) && !string.contains("'") {
68 | return "'\(string)'"
69 | }
70 |
71 | // Sub function check if string only number
72 | func onlyNumbers(_ string: String) -> Bool {
73 | return string.range(of: "^[0-9]+$", options: .regularExpression) != nil
74 | }
75 |
76 | // Single quote all numbers
77 | if onlyNumbers(string) &&
78 | !(string.hasPrefix("'") && string.hasSuffix("'") && !string.dropFirst().dropLast().contains("'")) {
79 | return "'\(string)'"
80 | }
81 |
82 | // Single quote if requirement contains :
83 | if nameOfKey == "requirement" && string.contains(":") &&
84 | !(string.hasPrefix("'") && string.hasSuffix("'") && !string.dropFirst().dropLast().contains("'")) {
85 | return "'\(string)'"
86 | }
87 |
88 | // Single quote if Developer ID Installer:
89 | if string.contains("Developer ID Installer:") &&
90 | !(string.hasPrefix("'") && string.hasSuffix("'") && !string.dropFirst().dropLast().contains("'")) {
91 | return "'\(string)'"
92 | }
93 |
94 | // Single quote if empty string and key find
95 | if nameOfKey == "find" && string == " " || string == ":" &&
96 | !(string.hasPrefix("'") && string.hasSuffix("'") && !string.dropFirst().dropLast().contains("'")) {
97 | return "'\(string)'"
98 | }
99 |
100 | // Single quote if empty string
101 | if string.isEmpty &&
102 | !(string.hasPrefix("'") && string.hasSuffix("'") && !string.dropFirst().dropLast().contains("'")) {
103 | return "'\(string)'"
104 | }
105 |
106 | // Single quote if key is split_on
107 | if nameOfKey == "split_on" &&
108 | !(string.hasPrefix("'") && string.hasSuffix("'") && !string.dropFirst().dropLast().contains("'")) {
109 | return "'\(string)'"
110 | }
111 |
112 | // If not key is 'requirement' single quote
113 | if nameOfKey != "requirement" && string.contains("%") &&
114 | !(string.hasPrefix("'") && string.hasSuffix("'") && !string.dropFirst().dropLast().contains("'")) &&
115 | !(string.hasPrefix("\"") && string.hasSuffix("\"") && !string.dropFirst().dropLast().contains("\"")) &&
116 | !(string.hasPrefix("(") && string.hasSuffix(")") && !string.dropFirst().dropLast().contains("()")) &&
117 | !string.contains("\n") && !string.contains("\r") {
118 | return "'\(string)'"
119 | }
120 |
121 | // Cleanup any warning messages or Comments messages
122 | if nameOfKey == "warning_message" || nameOfKey == "Comment" || nameOfKey == "comment" {
123 | let sanitizedString = string
124 | .trimmingCharacters(in: .whitespacesAndNewlines)
125 | .replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression)
126 | .replacingOccurrences(of: ": ", with: " ", options: .regularExpression)
127 | .replacingOccurrences(of: "\"", with: "", options: .regularExpression)
128 | return sanitizedString
129 | }
130 |
131 | /* Warn if string contains : - Seems not needed
132 | if string.contains(":") &&
133 | !string.hasPrefix("\"") && !string.hasSuffix("\"") &&
134 | !string.hasPrefix("'") && !string.hasSuffix("'") &&
135 | !string.hasPrefix("(") && !string.hasSuffix(")") &&
136 | !string.contains("://") {
137 | outputToLogView(logString: "⚠️ String(s) contains : might need to be single quoted:\n")
138 | outputToLogView(logString: "\(string)\n")
139 | }
140 |
141 | // Warn if string contains "
142 | if string.contains("\"") &&
143 | !(string.hasPrefix("\"") && string.hasSuffix("\"")) &&
144 | !(string.hasPrefix("'") && string.hasSuffix("'")) {
145 | outputToLogView(logString: "⚠️ String(s) contains double quotes but may not be properly enclosed:\n")
146 | outputToLogView(logString: "\(string)\n")
147 | }
148 | */
149 |
150 | // If Codesignature values are empty, we set a empty requried array value []
151 | if (nameOfKey == "expected_authority_names" || nameOfKey == "codesign_additional_arguments") && string.isEmpty {
152 | outputToLogView(logString: "⚠️ Key '\(key!)' has an empty value, inserting default empty array []\n")
153 | return "[]"
154 | }
155 |
156 | // Fix multiline Description
157 | if nameOfKey == "Description" && (string.contains("\n") || string.contains("\r")) && !string.hasPrefix(" ") {
158 | let indentedString = string.split(separator: "\n").map { line in
159 | return " \(line.trimmingCharacters(in: .whitespaces))"
160 | }.joined(separator: "\n")
161 | return indentedString
162 | }
163 |
164 | // Fix multiline file_content
165 | if nameOfKey == "file_content" || nameOfKey == "installcheck_script" || nameOfKey == "uninstall_script" || nameOfKey == "additional_install_actions" || nameOfKey == "postinstall_script" || nameOfKey == "postuninstall_script"{
166 | // Check if the first line starts with "|"
167 | let trimmedString = string.trimmingCharacters(in: .whitespacesAndNewlines)
168 | if !trimmedString.hasPrefix("|") {
169 | // Add "|" only if it doesn't already exist
170 | let lines = string.split(separator: "\n", omittingEmptySubsequences: false)
171 | let updatedString = "|" + (lines.isEmpty ? "" : "\n") + lines.map { " \($0)" }.joined(separator: "\n")
172 | return updatedString
173 | } else if !string.hasPrefix("|") {
174 | // Ensure the first line starts with exactly " |"
175 | let lines = string.split(separator: "\n", omittingEmptySubsequences: false)
176 | let updatedString = "|" + (lines.count > 1 ? "\n" : "") + lines.dropFirst().map { " \($0)" }.joined(separator: "\n")
177 | return updatedString
178 | }
179 | }
180 |
181 | return string
182 | } else if let array = value as? [Any] {
183 | // If arrays is empty
184 | if array.isEmpty {
185 | outputToLogView(logString: "⚠️ Key '\(key ?? "")' contains an empty array, ensuring default empty array []\n")
186 | return "[]" // Replace with default empty array
187 | }
188 | // End arrays is empty
189 | return array.map { sanitizeValueForYAML($0, key: key) }
190 | } else if value is NSNull {
191 | return ""
192 | } else if let dict = value as? [String: Any] {
193 | if dict.isEmpty {
194 | return "{}"
195 | }
196 | return sanitizeDictionaryForYAML(dict)
197 | } else {
198 | return "\(value)"
199 | }
200 | }
201 |
202 |
203 | // Function to reorder the keys in a Process dictionary
204 | func reorderProcessKeys(_ processStep: [String: Any]) -> [String: Any] {
205 | var orderedStep = [String: Any]()
206 |
207 | // Always put Processor first
208 | if let processor = processStep["Processor"] {
209 | orderedStep["Processor"] = processor
210 | }
211 |
212 | // Only include Arguments if it exists and is not empty
213 | if let arguments = processStep["Arguments"] as? [String: Any], !arguments.isEmpty {
214 | orderedStep["Arguments"] = arguments
215 | }
216 |
217 | return orderedStep
218 | }
219 |
220 |
221 | // Function to sanitize and reorder the Process section for YAML
222 | func formatProcessForYAML(dictionary: [String: Any]) -> [String: Any] {
223 | var modifiedDict = dictionary
224 | if let processArray = dictionary["Process"] as? [[String: Any]] {
225 | let formattedProcessArray = processArray.map { processStep -> [String: Any] in
226 | var sanitizedStep = sanitizeDictionaryForYAML(processStep)
227 |
228 | // Move any remaining Comments to Arguments
229 | if let comment = sanitizedStep["Comment"] as? String {
230 | var arguments = sanitizedStep["Arguments"] as? [String: Any] ?? [:]
231 | arguments["Comment"] = comment
232 | sanitizedStep["Arguments"] = arguments
233 | sanitizedStep.removeValue(forKey: "Comment")
234 | }
235 |
236 | // Remove Arguments if it's empty
237 | if let arguments = sanitizedStep["Arguments"] as? [String: Any], arguments.isEmpty {
238 | sanitizedStep.removeValue(forKey: "Arguments")
239 | }
240 |
241 | return reorderProcessKeys(sanitizedStep)
242 | }
243 | modifiedDict["Process"] = formattedProcessArray
244 | }
245 | return sanitizeDictionaryForYAML(modifiedDict)
246 | }
247 |
248 |
249 | // Function to sanitize an entire dictionary
250 | func sanitizeDictionaryForYAML(_ dictionary: [String: Any]) -> [String: Any] {
251 | var sanitizedDict = [String: Any]()
252 | for (key, value) in dictionary {
253 | if let dictValue = value as? [String: Any] {
254 | sanitizedDict[key] = sanitizeDictionaryForYAML(dictValue)
255 | } else if let arrayValue = value as? [Any] {
256 | if arrayValue.isEmpty {
257 | outputToLogView(logString: "⚠️ Key '\(key)' contains an empty array, ensuring default empty array []\n")
258 | sanitizedDict[key] = [] // Replace with default empty array
259 | } else {
260 | sanitizedDict[key] = arrayValue.map { sanitizeValueForYAML($0, key: key) }
261 | }
262 | } else {
263 | sanitizedDict[key] = sanitizeValueForYAML(value, key: key)
264 | }
265 | }
266 | return sanitizedDict
267 | }
268 |
269 |
270 | // Custom function to convert Dictionary to YAML with specific key order
271 | func dictionaryToOrderedYAML(dict: [String: Any]) -> String? {
272 | do {
273 | var yamlString = ""
274 |
275 | // Ensure these keys come first
276 | let orderedKeys = ["Comment", "Description", "Identifier", "ParentRecipe", "MinimumVersion", "Input"]
277 |
278 | for key in orderedKeys {
279 | if let value = dict[key], !(value is String && (value as! String).isEmpty) {
280 | if key == "Description", let descriptionValue = value as? String {
281 | if descriptionValue.contains("\n") || descriptionValue.contains("\r") && !descriptionValue.contains(":") {
282 | let descriptionValuetrimmed = descriptionValue.trimmingCharacters(in: CharacterSet(charactersIn: "\""))
283 | yamlString += "\(key): |\n\(descriptionValuetrimmed)\n"
284 | } else {
285 | let yamlFragment = try Yams.dump(object: [key: value])
286 | yamlString += yamlFragment
287 | }
288 | } else {
289 | let yamlFragment = try Yams.dump(object: [key: value])
290 | yamlString += yamlFragment.replacingOccurrences(of: "'''", with: "'")
291 | }
292 | }
293 | }
294 |
295 | // Add newline before Input section if it exists
296 | if yamlString.contains("Input:") {
297 | yamlString = yamlString.replacingOccurrences(of: "Input:", with: "\nInput:")
298 | }
299 |
300 | // Handle the Process key separately to ensure it comes last
301 | var processString = "\nProcess:\n"
302 | if let processArray = dict["Process"] as? [[String: Any]] {
303 | for processStep in processArray {
304 | if let processor = processStep["Processor"] as? String {
305 | processString += "- Processor: \(processor)\n"
306 | }
307 |
308 | if let arguments = processStep["Arguments"] as? [String: Any] {
309 | processString += " Arguments:\n"
310 | for (argKey, argValue) in arguments {
311 | let sanitizedValue = sanitizeValueForYAML(argValue, key: argKey)
312 | if argKey == "pkginfo", let pkginfoDict = argValue as? [String: Any] {
313 | // Special handling for pkginfo section
314 | processString += " pkginfo:\n"
315 | processString += formatPkginfoSection(pkginfoDict, indent: 6)
316 | } else if let arrayValue = sanitizedValue as? [Any] {
317 | processString += " \(argKey):\n"
318 | if arrayValue.isEmpty {
319 | processString += " []\n"
320 | } else {
321 | for item in arrayValue {
322 | if let str = item as? String {
323 | processString += " - \(str)\n"
324 | } else if let nestedDict = item as? [String: Any] {
325 | processString += " -\n"
326 | for (nestedKey, nestedValue) in nestedDict {
327 | processString += " \(nestedKey): \(nestedValue)\n"
328 | }
329 | }
330 | }
331 | }
332 | } else if let dictValue = sanitizedValue as? [String: Any] {
333 | if dictValue.isEmpty {
334 | processString += " \(argKey): {}\n"
335 | } else {
336 | processString += " \(argKey):\n"
337 | for (dictKey, dictItem) in dictValue {
338 | if let nestedArray = dictItem as? [[String: Any]] {
339 | processString += " \(dictKey):\n"
340 | if nestedArray.isEmpty {
341 | processString += " []\n"
342 | } else {
343 | for item in nestedArray {
344 | processString += " -\n"
345 | for (k, v) in item {
346 | processString += " \(k): \(v)\n"
347 | }
348 | }
349 | }
350 | } else {
351 | processString += " \(dictKey): \(dictItem)\n"
352 | }
353 | }
354 | }
355 | } else {
356 | processString += " \(argKey): \(sanitizedValue)\n"
357 | }
358 | }
359 | }
360 | processString += "\n"
361 | }
362 | } else {
363 | processString += " []"
364 | }
365 |
366 | // Add any remaining keys that were not explicitly handled
367 | for (key, value) in dict {
368 | if !orderedKeys.contains(key) && key != "Process" && key != "TopComment" && !(value is String && (value as! String).isEmpty) {
369 | let yamlFragment = try Yams.dump(object: [key: value])
370 | yamlString += yamlFragment
371 | }
372 | }
373 |
374 | // Append the Process section at the end
375 | yamlString += processString
376 |
377 | return yamlString
378 | } catch {
379 | print("Error converting Dictionary to YAML: \(error)")
380 | outputToLogView(logString: "⚠️ Error converting Dictionary to YAML: \(error)\n")
381 | return nil
382 | }
383 | }
384 |
385 | // Helper function to format pkginfo sections
386 | func formatPkginfoSection(_ pkginfo: [String: Any], indent: Int) -> String {
387 | var result = ""
388 | let indentStr = String(repeating: " ", count: indent)
389 |
390 | // Define the order of keys we want to process first
391 | let orderedKeys = ["name", "display_name", "description", "version", "catalogs", "category", "developer"]
392 |
393 | // Process ordered keys first
394 | for key in orderedKeys {
395 | if let value = pkginfo[key] {
396 | if let arrayValue = value as? [Any] {
397 | result += "\(indentStr)\(key):\n"
398 | if arrayValue.isEmpty {
399 | result += "\(indentStr) []\n"
400 | } else {
401 | for item in arrayValue {
402 | result += "\(indentStr) - \(item)\n"
403 | }
404 | }
405 | } else {
406 | result += "\(indentStr)\(key): \(value)\n"
407 | }
408 | }
409 | }
410 |
411 | // Process remaining keys
412 | for (key, value) in pkginfo {
413 | if !orderedKeys.contains(key) {
414 | if let arrayValue = value as? [Any] {
415 | result += "\(indentStr)\(key):\n"
416 | if arrayValue.isEmpty {
417 | result += "\(indentStr) []\n"
418 | } else {
419 | for item in arrayValue {
420 | result += "\(indentStr) - \(item)\n"
421 | }
422 | }
423 | } else {
424 | result += "\(indentStr)\(key): \(value)\n"
425 | }
426 | }
427 | }
428 |
429 | return result
430 | }
431 |
432 |
433 | // Function to process XML and output YAML
434 | func xmlToFormattedYAML(xmlString: String) -> String? {
435 | guard let dictionary = xmlToDictionary(xmlString: xmlString) else {
436 | return nil
437 | }
438 | let formattedDict = formatProcessForYAML(dictionary: dictionary)
439 | return dictionaryToOrderedYAML(dict: formattedDict)
440 | }
441 |
442 |
443 | // Function to clean some remaining nested arrays
444 | func cleanRemainingArrays(_ yamlString: String) -> String {
445 | let targetPatterns = [
446 | "catalogs:",
447 | "supported_architectures:",
448 | "requires:",
449 | "update_for:"
450 | ]
451 |
452 | let lines = yamlString.split(separator: "\n", omittingEmptySubsequences: false)
453 | var processedLines = [String]()
454 |
455 | for line in lines {
456 | var updatedLine = String(line)
457 |
458 | // Check if the line matches any of the target patterns
459 | if targetPatterns.contains(where: { updatedLine.trimmingCharacters(in: .whitespaces).hasPrefix($0) }) {
460 | // Remove escaped single quotes and create Yaml Array
461 | updatedLine = updatedLine.replacingOccurrences(of: "\\'", with: "")
462 | updatedLine = updatedLine.replacingOccurrences(of: "[", with: "\n - ")
463 | updatedLine = updatedLine.replacingOccurrences(of: "]", with: "")
464 | }
465 |
466 | processedLines.append(updatedLine)
467 | }
468 |
469 | return processedLines.joined(separator: "\n")
470 | }
471 |
472 | // Main function to convert XML to YAML and output to UI
473 | func XMLtoYaml() {
474 | let wholeDocument = (appDelegate().outputTextField.textStorage as NSAttributedString?)!.string
475 | if wholeDocument.isEmpty { return }
476 |
477 | // Clear Set of error message
478 | loggedMessages.removeAll()
479 |
480 | if var yamlOutput = xmlToFormattedYAML(xmlString: wholeDocument) {
481 | /*/ Ensure Process header exists even if empty
482 | if !yamlOutput.contains("Process:") {
483 | yamlOutput += "\nProcess: []\n"
484 | }
485 | */
486 |
487 | // Workaround for some nested arrays that not get "real" YAML format
488 | yamlOutput = cleanRemainingArrays(yamlOutput)
489 |
490 | appDelegate().outputTextField.string = ""
491 | highlightr!.setTheme(to: "xcode")
492 | highlightr!.theme.codeFont = NSFont(name: "Menlo", size: 12)
493 | let highlightedCode = highlightr!.highlight(yamlOutput, as: "yaml")!
494 | appDelegate().outputTextField.textStorage?.insert(highlightedCode, at: 0)
495 |
496 | // Function to highlight logged messages in the text field
497 | func highlightLoggedMessages() {
498 | // Convert Set to Array
499 | let loggedMessagesArray = Array(loggedMessages)
500 |
501 | // Get the entire text in the NSTextView
502 | guard let wholeDocument = appDelegate().outputTextField.string as NSString? else { return }
503 |
504 | // Iterate over each logged message and apply the highlighting
505 | for string in loggedMessagesArray {
506 | let range = wholeDocument.range(of: string)
507 |
508 | // Ensure the identifier exists in the text
509 | if range.location != NSNotFound {
510 | // Replace the matched text with attributed text
511 | let attributedReplaceText = NSAttributedString(string: string, attributes: yellowBackgroundAttributes)
512 | appDelegate().outputTextField.textStorage?.replaceCharacters(in: range, with: attributedReplaceText)
513 | }
514 | }
515 | }
516 |
517 | // Highlight the logged messages in the text view
518 | highlightLoggedMessages()
519 |
520 | // If we run multiple then we need a check for this
521 | if loggedMessages.isEmpty {
522 | outputToLogView(logString: "✅ No problems found\n")
523 | }
524 | }
525 | }
526 |
--------------------------------------------------------------------------------
/RecipeBuilder/XMLtoYamlSnippets.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Yams
3 | import AppKit
4 |
5 | // Snippets functions
6 | // Function to convert XML snippet to Dictionary
7 | func xmlSnippetToDictionary(xmlString: String) -> [String: Any]? {
8 | guard let data = xmlString.data(using: .utf8) else {
9 | print("Error converting XML string to Data")
10 | outputToLogView (logString: "\n⚠️ Error converting XML string to Data\n")
11 | return nil
12 | }
13 |
14 | do {
15 | let plist = try PropertyListSerialization.propertyList(from: data, options: [], format: nil)
16 | if let dictionary = plist as? [String: Any] {
17 | return dictionary
18 | } else {
19 | print("Error: Expected dictionary but got something else")
20 | outputToLogView (logString: "\n⚠️ Error: Expected dictionary but got something else\n")
21 | return nil
22 | }
23 | } catch {
24 | print("Error parsing XML: \(error)")
25 | outputToLogView (logString: "\n⚠️ Error parsing XML: \(error)\n")
26 | return nil
27 | }
28 | }
29 |
30 | // Function to make dictionary values YAML-compatible
31 | func makeYAMLCompatibleSnippet(_ value: Any) -> Any {
32 | if let dict = value as? [String: Any] {
33 | var yamlCompatibleDict = [String: Any]()
34 | for (key, value) in dict {
35 | yamlCompatibleDict[key] = makeYAMLCompatibleSnippet(value)
36 | }
37 | return yamlCompatibleDict
38 | } else if let array = value as? [Any] {
39 | return array.map { makeYAMLCompatibleSnippet($0) }
40 | } else {
41 | return String(describing: value)
42 | }
43 | }
44 |
45 | // Function to reorder the keys in the dictionary to ensure `Processor` comes first
46 | func reorderProcessKeysSnippet(_ processStep: [String: Any]) -> [String: Any] {
47 | var orderedStep = [String: Any]()
48 | // Ensure Processor comes first
49 | if let processor = processStep["Processor"] as? String {
50 | orderedStep["Processor"] = processor
51 | }
52 | // Then Arguments
53 | if let arguments = processStep["Arguments"] as? [String: Any] {
54 | orderedStep["Arguments"] = arguments
55 | }
56 | // Add any other keys
57 | for (key, value) in processStep where key != "Processor" && key != "Arguments" {
58 | orderedStep[key] = value
59 | }
60 | return orderedStep
61 | }
62 |
63 | // Custom function to convert Dictionary to Ordered YAML
64 | func dictionaryToOrderedYAMLSnippet(dict: [String: Any]) -> String {
65 | var yamlString = ""
66 |
67 | // Ensure Processor comes first
68 | if let processor = dict["Processor"] {
69 | let yamlFragment = (try? Yams.dump(object: ["Processor": processor])) ?? ""
70 | yamlString += yamlFragment
71 | }
72 |
73 | // Then Arguments
74 | if let arguments = dict["Arguments"] {
75 | let yamlFragment = (try? Yams.dump(object: ["Arguments": arguments])) ?? ""
76 | yamlString += yamlFragment
77 | }
78 |
79 | // Add any other keys
80 | for (key, value) in dict where key != "Processor" && key != "Arguments" {
81 | let yamlFragment = (try? Yams.dump(object: [key: value])) ?? ""
82 | yamlString += yamlFragment
83 | }
84 |
85 | return yamlString
86 | }
87 |
88 | // Function to convert XML `` snippet into a YAML list
89 | func xmlDictSnippetToYAMLList(xmlSnippet: String) -> String? {
90 | guard let parsedDict = xmlSnippetToDictionary(xmlString: xmlSnippet) else {
91 | return nil
92 | }
93 |
94 | // Reorder the parsed dictionary to ensure `Processor` comes first
95 | let reorderedDict = reorderProcessKeysSnippet(parsedDict)
96 |
97 | // Wrap the dictionary in an array for YAML list formatting
98 | let yamlCompatibleObject = makeYAMLCompatibleSnippet([reorderedDict])
99 |
100 | if let list = yamlCompatibleObject as? [[String: Any]] {
101 | var yamlString = ""
102 | for item in list {
103 | let itemYAML = dictionaryToOrderedYAMLSnippet(dict: item)
104 | yamlString += "- " + itemYAML.replacingOccurrences(of: "\n", with: "\n ").trimmingCharacters(in: .whitespacesAndNewlines) + "\n\n"
105 | }
106 | return yamlString
107 | }
108 | return nil
109 | }
110 |
111 |
--------------------------------------------------------------------------------
/RecipeBuilder/YamlToXML.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Yams
3 | import AppKit
4 |
5 | // Yaml to XML Functions
6 | // Function to convert Dictionary to XML (Apple plist format)
7 | func dictionaryToXML(dict: [String: Any]) -> String? {
8 | do {
9 | let data = try PropertyListSerialization.data(fromPropertyList: dict, format: .xml, options: 0)
10 | return String(data: data, encoding: .utf8)
11 | } catch {
12 | print("Error converting Dictionary to XML: \(error)")
13 | outputToLogView (logString: "⚠️ Error converting Dictionary to XML: \(error)\n")
14 | return nil
15 | }
16 | }
17 |
18 |
19 | // Function to parse YAML into Dictionary
20 | func yamlToDictionary(yamlString: String) -> [String: Any]? {
21 | do {
22 | if let dictionary = try Yams.load(yaml: yamlString) as? [String: Any] {
23 | return dictionary
24 | } else {
25 | print("Error: YAML did not convert to Dictionary")
26 | outputToLogView (logString: "⚠️ Error: YAML did not convert to Dictionary\n")
27 | return nil
28 | }
29 | } catch {
30 | print("Error parsing YAML: \(error)")
31 | outputToLogView (logString: "⚠️ Error parsing YAML: \(error)\n")
32 | return nil
33 | }
34 | }
35 |
36 |
37 | // Function to convert YAML to XML
38 | func yamlToXML(yamlString: String) -> String? {
39 | if let dictionary = yamlToDictionary(yamlString: yamlString) {
40 | return dictionaryToXML(dict: dictionary)
41 | }
42 | return nil
43 | }
44 |
45 |
46 | // Output the converted Yaml as XML
47 | func YamlToXML() {
48 | var yamlDocument = (appDelegate().outputTextField.textStorage as NSAttributedString?)!.string
49 | if yamlDocument == "" {return }
50 |
51 | // Clear Set of error message
52 | loggedMessages.removeAll()
53 |
54 | // Replace empty array from YAML with empty value for XML [] if existing
55 | yamlDocument = yamlDocument.replacingOccurrences(
56 | of: #"(?m)(?<=\bProcess:)\s*(?=\n\s*\n|\z)"#,
57 | with: " []",
58 | options: [.regularExpression, .caseInsensitive]
59 | )
60 | if let xmlOutput = yamlToXML(yamlString: yamlDocument) {
61 | appDelegate().outputTextField.string = ""
62 | highlightr!.setTheme(to: "xcode")
63 | highlightr!.theme.codeFont = NSFont(name: "Menlo", size: 12)
64 | let highlightedCode = highlightr!.highlight(xmlOutput, as: "xml")!
65 | appDelegate().outputTextField.textStorage?.insert(highlightedCode, at: 0)
66 | }
67 |
68 | // If we run multiple then we need a check for this
69 | if loggedMessages.isEmpty {
70 | outputToLogView(logString: "✅ No problems found\n")
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/images/recipeBuildericon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/images/recipeBuildericon.png
--------------------------------------------------------------------------------
/images/recipebuilderinterface.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/images/recipebuilderinterface.png
--------------------------------------------------------------------------------