import Foundation import Preferences public struct XcodeTheme: Codable { public struct ThemeColor: Codable { public var red: Double public var green: Double public var blue: Double public var alpha: Double public var hexString: String { let red = Int(self.red * 255) let green = Int(self.green * 255) let blue = Int(self.blue * 255) let alpha = Int(self.alpha * 255) return String(format: "#%02X%02X%02X%02X", red, green, blue, alpha) } var storable: StorableColor { .init(red: red, green: green, blue: blue, alpha: alpha) } } public struct ThemeFont: Codable { public var name: String public var size: Double var storable: StorableFont { .init(name: name, size: size) } } public var plainTextColor: ThemeColor public var plainTextFont: ThemeFont public var commentColor: ThemeColor public var documentationMarkupColor: ThemeColor public var documentationMarkupKeywordColor: ThemeColor public var marksColor: ThemeColor public var stringsColor: ThemeColor public var charactersColor: ThemeColor public var numbersColor: ThemeColor public var regexLiteralsColor: ThemeColor public var regexLiteralNumbersColor: ThemeColor public var regexLiteralCaptureNamesColor: ThemeColor public var regexLiteralCharacterClassNamesColor: ThemeColor public var regexLiteralOperatorsColor: ThemeColor public var keywordsColor: ThemeColor public var preprocessorStatementsColor: ThemeColor public var urlsColor: ThemeColor public var attributesColor: ThemeColor public var typeDeclarationsColor: ThemeColor public var otherDeclarationsColor: ThemeColor public var projectClassNamesColor: ThemeColor public var projectFunctionAndMethodNamesColor: ThemeColor public var projectConstantsColor: ThemeColor public var projectTypeNamesColor: ThemeColor public var projectPropertiesAndGlobalsColor: ThemeColor public var projectPreprocessorMacrosColor: ThemeColor public var otherClassNamesColor: ThemeColor public var otherFunctionAndMethodNamesColor: ThemeColor public var otherConstantsColor: ThemeColor public var otherTypeNamesColor: ThemeColor public var otherPropertiesAndGlobalsColor: ThemeColor public var otherPreprocessorMacrosColor: ThemeColor public var headingColor: ThemeColor public var backgroundColor: ThemeColor public var selectionColor: ThemeColor public var cursorColor: ThemeColor public var currentLineColor: ThemeColor public var invisibleCharactersColor: ThemeColor public var debuggerConsolePromptColor: ThemeColor public var debuggerConsoleOutputColor: ThemeColor public var debuggerConsoleInputColor: ThemeColor public var executableConsoleOutputColor: ThemeColor public var executableConsoleInputColor: ThemeColor public func asHighlightJSTheme() -> String { buildHighlightJSTheme(self) .replacingOccurrences(of: "\n", with: "") .replacingOccurrences(of: ": ", with: ":") .replacingOccurrences(of: "} ", with: "}") .replacingOccurrences(of: " {", with: "{") .replacingOccurrences(of: ";}", with: "}") .replacingOccurrences(of: " ", with: "") } } public extension XcodeTheme { /// Color scheme locations: /// ~/Library/Developer/Xcode/UserData/FontAndColorThemes/ /// Xcode.app/Contents/SharedFrameworks/DVTUserInterfaceKit.framework/Versions/A/Resources/FontAndColorThemes init(fileURL: URL) throws { let parser = XcodeThemeParser() self = try parser.parse(fileURL: fileURL) } } struct XcodeThemeParser { enum Error: Swift.Error { case fileNotFound case invalidData } func parse(fileURL: URL) throws -> XcodeTheme { guard let data = try? Data(contentsOf: fileURL) else { throw Error.fileNotFound } if fileURL.pathExtension == "xccolortheme" { return try parseXCColorTheme(data) } else { throw Error.invalidData } } func parseXCColorTheme(_ data: Data) throws -> XcodeTheme { let plist = try? PropertyListSerialization.propertyList( from: data, options: .mutableContainers, format: nil ) as? [String: Any] guard let theme = plist else { throw Error.invalidData } func getRawThemeValue(at path: [String]) -> String? { guard !path.isEmpty else { return nil } let keys = path.dropLast(1) var currentDict = theme for key in keys { guard let value = currentDict[key] as? [String: Any] else { return nil } currentDict = value } return currentDict[path.last!] as? String } /// The source value is an `r g b a` string, for example: `0.5 0.5 0.2 1` func convertColor(source: String) -> XcodeTheme.ThemeColor { let components = source.split(separator: " ") let red = (components[0] as NSString).doubleValue let green = (components[1] as NSString).doubleValue let blue = (components[2] as NSString).doubleValue let alpha = (components[3] as NSString).doubleValue return .init(red: red, green: green, blue: blue, alpha: alpha) } func getThemeValue( at path: [String], defaultValue: XcodeTheme.ThemeColor = .init(red: 0, green: 0, blue: 0, alpha: 1) ) -> XcodeTheme.ThemeColor { if let value = getRawThemeValue(at: path) { return convertColor(source: value) } return defaultValue } /// The source value is an `FontName - size` string, for example: `SFMono-Medium - 12.0` func convertFont(source: String) -> XcodeTheme.ThemeFont? { if let separator = source.range(of: " - ") { let name = String(source.prefix(upTo: separator.lowerBound)) let size = Double(source.suffix(from: separator.upperBound)) ?? 0.0 return .init(name: name, size: size) } return nil } func getThemeFont( at path: [String], defaultValue: XcodeTheme.ThemeFont = .init(name: "SFMono-Medium", size: 12.0) ) -> XcodeTheme.ThemeFont { if let value = getRawThemeValue(at: path) { return convertFont(source: value) ?? defaultValue } return defaultValue } let black = XcodeTheme.ThemeColor(red: 0, green: 0, blue: 0, alpha: 1) let white = XcodeTheme.ThemeColor(red: 1, green: 1, blue: 1, alpha: 1) let xcodeTheme = XcodeTheme( plainTextColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.plain"], defaultValue: black ), plainTextFont: getThemeFont( at: ["DVTSourceTextSyntaxFonts", "xcode.syntax.plain"] ), commentColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.comment"], defaultValue: black ), documentationMarkupColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.comment.doc"], defaultValue: black ), documentationMarkupKeywordColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.comment.doc.keyword"], defaultValue: black ), marksColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.mark"], defaultValue: black ), stringsColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.string"], defaultValue: black ), charactersColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.character"], defaultValue: black ), numbersColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.number"], defaultValue: black ), regexLiteralsColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.plain"], defaultValue: black ), regexLiteralNumbersColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.number"], defaultValue: black ), regexLiteralCaptureNamesColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.plain"], defaultValue: black ), regexLiteralCharacterClassNamesColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.plain"], defaultValue: black ), regexLiteralOperatorsColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.plain"], defaultValue: black ), keywordsColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.keyword"], defaultValue: black ), preprocessorStatementsColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.preprocessor"], defaultValue: black ), urlsColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.url"], defaultValue: black ), attributesColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.attribute"], defaultValue: black ), typeDeclarationsColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.declaration.type"], defaultValue: black ), otherDeclarationsColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.declaration.other"], defaultValue: black ), projectClassNamesColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.class"], defaultValue: black ), projectFunctionAndMethodNamesColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.function"], defaultValue: black ), projectConstantsColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.constant"], defaultValue: black ), projectTypeNamesColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.type"], defaultValue: black ), projectPropertiesAndGlobalsColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.variable"], defaultValue: black ), projectPreprocessorMacrosColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.macro"], defaultValue: black ), otherClassNamesColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.class.system"], defaultValue: black ), otherFunctionAndMethodNamesColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.function.system"], defaultValue: black ), otherConstantsColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.constant.system"], defaultValue: black ), otherTypeNamesColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.type.system"], defaultValue: black ), otherPropertiesAndGlobalsColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.variable.system"], defaultValue: black ), otherPreprocessorMacrosColor: getThemeValue( at: ["DVTSourceTextSyntaxColors", "xcode.syntax.identifier.macro.system"], defaultValue: black ), headingColor: getThemeValue( at: ["DVTMarkupTextPrimaryHeadingColor"], defaultValue: black ), backgroundColor: getThemeValue( at: ["DVTSourceTextBackground"], defaultValue: white ), selectionColor: getThemeValue( at: ["DVTSourceTextSelectionColor"], defaultValue: black ), cursorColor: getThemeValue( at: ["DVTSourceTextInsertionPointColor"], defaultValue: black ), currentLineColor: getThemeValue( at: ["DVTSourceTextCurrentLineHighlightColor"], defaultValue: black ), invisibleCharactersColor: getThemeValue( at: ["DVTSourceTextInvisiblesColor"], defaultValue: black ), debuggerConsolePromptColor: getThemeValue( at: ["DVTConsoleDebuggerPromptTextColor"], defaultValue: black ), debuggerConsoleOutputColor: getThemeValue( at: ["DVTConsoleDebuggerOutputTextColor"], defaultValue: black ), debuggerConsoleInputColor: getThemeValue( at: ["DVTConsoleDebuggerInputTextColor"], defaultValue: black ), executableConsoleOutputColor: getThemeValue( at: ["DVTConsoleExectuableOutputTextColor"], defaultValue: black ), executableConsoleInputColor: getThemeValue( at: ["DVTConsoleExectuableInputTextColor"], defaultValue: black ) ) return xcodeTheme } }