/* * ---------------------------------------------------------------------------- * "THE BEER-WARE LICENSE" (Revision 42): * wrote this file. As long as you retain this notice you * can do whatever you want with this stuff. If we meet some day, and you think * this stuff is worth it, you can buy me a beer in return., Gregorio Litenstein. * ---------------------------------------------------------------------------- */ import AppKit /** Converts an array of Application URLs to an array of Application Paths. - Parameter inArray: An array of file-system URLs corresponding to applications. - Returns: An array of strings corresponding to the POSIX paths of the supplied applications. */ func convertAppURLsToPaths (_ inArray:Array?) -> Array? { if let URLArray = inArray { var outputArray: Array = [] for (i, app) in URLArray.enumerated() { let temp = app.path outputArray.insert(temp, at:i) } return outputArray } else { return nil } } /** Creates a String representation of a Dictionary of Strings. - Parameter inDict: A dictionary of Strings. - Returns: A string corresponding to the contents of the supplied dictionary. */ func copyDictionaryAsString (_ inDict: [(key:String, value:String)]?) -> String? { var output = "" if let temp = inDict { for (key, value) in temp { output+=("\(key)\t\t\t\t\(value)\n") } } else { return nil } return output } /** Creates a String representation of an Array of Strings. - Parameter inArray: An array of strings. - Parameter separator: The separator to be used. - Returns: A string corresponding to the contents of the supplied array. */ func copyStringArrayAsString (_ inArray: Array?, separator: String = "\n") -> String? { if let temp = inArray { return temp.joined(separator:separator) } else { return nil } } //// REGEX MATCHING CONVENIENCE OPERATORS. infix operator =~ prefix operator / func =~ (string: String, regex: NSRegularExpression?) -> Bool? { guard let matches = regex?.numberOfMatches(in:string, options: [], range: NSMakeRange(0, string.count)) else { return nil } return matches > 0 } prefix func /(pattern:String) -> NSRegularExpression? { let options: NSRegularExpression.Options = NSRegularExpression.Options.dotMatchesLineSeparators guard let retval = try? NSRegularExpression(pattern:pattern, options:options) else { return nil } return retval } //// EXTENSIONS extension String: Error {} extension DispatchQueue { static let labelPrefix = "io.zamzam.ZamzamKit" static let database = DispatchQueue(label: "\(DispatchQueue.labelPrefix).database", qos: .utility) } public extension Collection { /// Element at the given index if it exists. /// /// - Parameter index: index of element. subscript(safe index: Index) -> Element? { // http://www.vadimbulavin.com/handling-out-of-bounds-exception/ return indices.contains(index) ? self[index] : nil } } public extension Collection where Iterator.Element == (String, Any) { /// Converts collection of objects to JSON string var jsonString: String? { guard JSONSerialization.isValidJSONObject(self), let stringData = try? JSONSerialization.data(withJSONObject: self, options: []) else { return nil } return String(data: stringData, encoding: .utf8) } } public extension Array where Element: Equatable { /// Array with all duplicates removed from it. /// /// [1, 3, 3, 5, 7, 9].distinct // [1, 3, 5, 7, 9] var distinct: [Element] { // https://github.com/SwifterSwift/SwifterSwift return reduce(into: [Element]()) { guard !$0.contains($1) else { return } $0.append($1) } } /// Remove all duplicates from array. /// /// var array = [1, 3, 3, 5, 7, 9] /// array.removeDuplicates() /// array // [1, 3, 5, 7, 9] mutating func removeDuplicates() { self = distinct } /// Removes the first occurance of the matched element. /// /// var array = ["a", "b", "c", "d", "e", "a"] /// array.remove("a") /// array // ["b", "c", "d", "e", "a"] /// /// - Parameter element: The element to remove from the array. mutating func remove(_ element: Element) { guard let index = firstIndex(of: element) else { return } remove(at: index) } } extension Bundle { /** Copies a bundle's package type, as specified in its Info.plist. - Parameter outError: Populated with an error-code if the bundle does not correspond to a valid package. - Returns: The four-letter code specifying the bundle's package type, or `nil` if an error is encountered. */ func getType (outError: inout OSStatus) -> String? { if let info = self.infoDictionary { if let type = info["CFBundlePackageType"] { return String(describing: type) } else { outError = errSecInvalidBundleInfo; return nil } } else { outError = kLSUnknownErr; return nil } } } extension Dictionary { /** Create a Dictionary by Merging two Arrays. - Parameter keys: An Array to be used as dictionary keys. - Parameter values: An Array to be used as values. */ public init(keys: [Key], values: [Value]) { precondition(keys.count == values.count) self.init() for (index, key) in keys.enumerated() { self[key] = values[index] } } } #if Prefpane extension LSRolesMask { /** Bridge our custom type to the actual values used in LaunchServices methods. */ init (from value: SourceListRoleTypes) { switch value { case .Viewer: self = .viewer case .Editor: self = .editor case .Shell: self = .shell case .All: self = .all } } } extension NSControl { /** Shrinks a text label until it fits in a single line in its container. */ func fitWidth() { var absoluteMaxWidth: CGFloat = 0.0 if let tempWidth = (self.superview?.frame.width) { absoluteMaxWidth = tempWidth absoluteMaxWidth -= (self.alignmentRectInsets.left + self.alignmentRectInsets.right) } let text = self.stringValue guard self.font != nil else { return } var font = self.font! if ControllersRef.sharedInstance.originalFonts[self] == nil { ControllersRef.sharedInstance.originalFonts[self] = font } else { font = ControllersRef.sharedInstance.originalFonts[self]! } let richText = NSTextFieldCell(textCell:text) richText.font = font var neededWidth: CGFloat = richText.cellSize.width var fontSize = font.pointSize var newFont = font guard absoluteMaxWidth > 0.0 else { return } while (neededWidth >= absoluteMaxWidth) { guard fontSize > 0.0 else { return } fontSize -= 0.5 newFont = NSFont(descriptor:font.fontDescriptor, size: fontSize)! richText.font = newFont neededWidth = richText.cellSize.width } self.setValue(newFont, forKey:"font") } } #endif /** Determines whether the app is running in Prefpane or CLI form by checking the Main Bundle Identifier. */ func areWeCLI() -> Bool { return (Bundle.main.bundleIdentifier == nil) } /** Determines the URL for the location of the SwiftDefaultApps prefpane, is any. */ func prefPaneLocation() -> URL? { if let bundle = Bundle(identifier:"cl.fail.lordkamina.SwiftDefaultApps") { return bundle.bundleURL } let prefpaneSearchPaths: [URL] = FileManager.default.urls(for: FileManager.SearchPathDirectory.preferencePanesDirectory, in: FileManager.SearchPathDomainMask.allDomainsMask) for path in prefpaneSearchPaths { if (FileManager.default.isExecutableFile(atPath: path.appendingPathComponent("SwiftDefaultApps.prefpane").absoluteURL.path) == true) { return path.appendingPathComponent("SwiftDefaultApps.prefpane").absoluteURL } } return nil } /** Display a modal alert - Parameter error: A numeric value indicating the error code as per LSWrappers.LSErrors. - Parameter arg1: String containing info pertaining to the error. - Parameter arg2: String containing info pertaining to the error. */ func displayAlert(error: OSStatus = Int32.min, arg1: String?, arg2: String?) throws { if (error != 0 && (arg1 == nil || arg2 == nil)) { throw ("Arguments cannot be nil when displaying an error.") } if (!areWeCLI()) { let alert = NSAlert() alert.icon = NSWorkspace.shared.icon(forFile: Bundle(identifier: "cl.fail.lordkamina.SwiftDefaultApps")!.bundlePath) alert.addButton(withTitle: "OK") if (error != Int32.min) { alert.informativeText = LSWrappers.LSErrors(value: error).print(argument: (app: arg1, content: arg2)) switch (Int(error)) { case Int(errSecInvalidBundleInfo)..<0: alert.messageText = "Error" alert.alertStyle = .critical case 0: alert.messageText = "Success" alert.alertStyle = .informational case 1...:alert.messageText = "Success" alert.alertStyle = .informational default: alert.messageText = "Warning" alert.alertStyle = .warning } } else { alert.informativeText = "SwiftDefaultApps ERROR: Called displayAlert() with an undefined error code." } DispatchQueue.main.async { alert.runModal() } } else { if (error < 0) { throw(LSWrappers.LSErrors.init(value: error).print(argument: (app: arg1, content: arg2))) } else if (error == 0) { print(LSWrappers.LSErrors.init(value: error).print(argument: (app: arg1, content: arg2))) } else if (error == Int32.min) { guard (arg1 != nil) else { throw("SwiftDefaultApps ERROR: Called displayAlert() with an undefined error code and message.") } print(arg1!) } } } /** NSFont extensions to style the different kinds of row in the NSTreeView. */ extension NSFont { /** Returns a SmallCaps version of the font it is invoked on. */ func smallCaps() -> NSFont? { let settings = [[NSFontDescriptor.FeatureKey.typeIdentifier: kLowerCaseType, NSFontDescriptor.FeatureKey.selectorIdentifier: kLowerCaseSmallCapsSelector]] let attributes: [NSFontDescriptor.AttributeName: AnyObject] = [NSFontDescriptor.AttributeName.featureSettings: settings as AnyObject, NSFontDescriptor.AttributeName.name: fontName as AnyObject] return NSFont(descriptor: NSFontDescriptor(fontAttributes: attributes), size: pointSize) } /** Returns a **Bold** version of the font it is invoked on. */ func bold() -> NSFont? { let fontManager = NSFontManager.shared return fontManager.convert(self, toHaveTrait: .boldFontMask) } /** Returns an _Italic_ version of the font it is invoked on. - Note: Not currently used. */ func italic() -> NSFont? { let fontManager = NSFontManager.shared return fontManager.convert(self, toHaveTrait: .italicFontMask) } }