import AppKit import Foundation extension Data { func barItemDefinitions() -> [BarItemDefinition]? { return try? JSONDecoder().decode([BarItemDefinition].self, from: utf8string!.stripComments().data(using: .utf8)!) } } struct BarItemDefinition: Decodable { let type: ItemType let action: ActionType let longAction: LongActionType let additionalParameters: [GeneralParameters.CodingKeys: GeneralParameter] private enum CodingKeys: String, CodingKey { case type } init(type: ItemType, action: ActionType, longAction: LongActionType, additionalParameters: [GeneralParameters.CodingKeys: GeneralParameter]) { self.type = type self.action = action self.longAction = longAction self.additionalParameters = additionalParameters } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let type = try container.decode(String.self, forKey: .type) let parametersDecoder = SupportedTypesHolder.sharedInstance.lookup(by: type) var additionalParameters = try GeneralParameters(from: decoder).parameters if let result = try? parametersDecoder(decoder), case let (itemType, action, longAction, parameters) = result { parameters.forEach { additionalParameters[$0] = $1 } self.init(type: itemType, action: action, longAction: longAction, additionalParameters: additionalParameters) } else { self.init(type: .staticButton(title: "unknown"), action: .none, longAction: .none, additionalParameters: additionalParameters) } } } typealias ParametersDecoder = (Decoder) throws -> ( item: ItemType, action: ActionType, longAction: LongActionType, parameters: [GeneralParameters.CodingKeys: GeneralParameter] ) class SupportedTypesHolder { private var supportedTypes: [String: ParametersDecoder] = [ "escape": { _ in ( item: .staticButton(title: "esc"), action: .keyPress(keycode: 53), longAction: .none, parameters: [.align: .align(.left)] ) }, "delete": { _ in ( item: .staticButton(title: "del"), action: .keyPress(keycode: 117), longAction: .none, parameters: [:] ) }, "brightnessUp": { _ in let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "brightnessUp")) return ( item: .staticButton(title: ""), action: .keyPress(keycode: 144), longAction: .none, parameters: [.image: imageParameter] ) }, "brightnessDown": { _ in let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "brightnessDown")) return ( item: .staticButton(title: ""), action: .keyPress(keycode: 145), longAction: .none, parameters: [.image: imageParameter] ) }, "illuminationUp": { _ in let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "ill_up")) return ( item: .staticButton(title: ""), action: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_UP), longAction: .none, parameters: [.image: imageParameter] ) }, "illuminationDown": { _ in let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "ill_down")) return ( item: .staticButton(title: ""), action: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_DOWN), longAction: .none, parameters: [.image: imageParameter] ) }, "volumeDown": { _ in let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarVolumeDownTemplateName)!) return ( item: .staticButton(title: ""), action: .hidKey(keycode: NX_KEYTYPE_SOUND_DOWN), longAction: .none, parameters: [.image: imageParameter] ) }, "volumeUp": { _ in let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarVolumeUpTemplateName)!) return ( item: .staticButton(title: ""), action: .hidKey(keycode: NX_KEYTYPE_SOUND_UP), longAction: .none, parameters: [.image: imageParameter] ) }, "mute": { _ in let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarAudioOutputMuteTemplateName)!) return ( item: .staticButton(title: ""), action: .hidKey(keycode: NX_KEYTYPE_MUTE), longAction: .none, parameters: [.image: imageParameter] ) }, "previous": { _ in let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarRewindTemplateName)!) return ( item: .staticButton(title: ""), action: .hidKey(keycode: NX_KEYTYPE_PREVIOUS), longAction: .none, parameters: [.image: imageParameter] ) }, "play": { _ in let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarPlayPauseTemplateName)!) return ( item: .staticButton(title: ""), action: .hidKey(keycode: NX_KEYTYPE_PLAY), longAction: .none, parameters: [.image: imageParameter] ) }, "next": { _ in let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarFastForwardTemplateName)!) return ( item: .staticButton(title: ""), action: .hidKey(keycode: NX_KEYTYPE_NEXT), longAction: .none, parameters: [.image: imageParameter] ) }, "weather": { decoder in enum CodingKeys: String, CodingKey { case refreshInterval; case units; case api_key; case icon_type } let container = try decoder.container(keyedBy: CodingKeys.self) let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) let units = try container.decodeIfPresent(String.self, forKey: .units) let api_key = try container.decodeIfPresent(String.self, forKey: .api_key) let icon_type = try container.decodeIfPresent(String.self, forKey: .icon_type) let action = try ActionType(from: decoder) let longAction = try LongActionType(from: decoder) return ( item: .weather(interval: interval ?? 1800.00, units: units ?? "metric", api_key: api_key ?? "32c4256d09a4c52b38aecddba7a078f6", icon_type: icon_type ?? "text"), action, longAction, parameters: [:] ) }, "currency": { decoder in enum CodingKeys: String, CodingKey { case refreshInterval; case from; case to } let container = try decoder.container(keyedBy: CodingKeys.self) let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) let from = try container.decodeIfPresent(String.self, forKey: .from) let to = try container.decodeIfPresent(String.self, forKey: .to) let action = try ActionType(from: decoder) let longAction = try LongActionType(from: decoder) return ( item: .currency(interval: interval ?? 600.00, from: from ?? "RUB", to: to ?? "USD"), action, longAction, parameters: [:] ) }, "dock": { _ in ( item: .dock(), action: .none, longAction: .none, parameters: [:] ) }, "inputsource": { _ in ( item: .inputsource(), action: .none, longAction: .none, parameters: [:] ) }, "volume": { decoder in enum CodingKeys: String, CodingKey { case image } let container = try decoder.container(keyedBy: CodingKeys.self) if var img = try container.decodeIfPresent(Source.self, forKey: .image) { return ( item: .volume(), action: .none, longAction: .none, parameters: [.image: .image(source: img)] ) } else { return ( item: .volume(), action: .none, longAction: .none, parameters: [:] ) } }, "brightness": { decoder in enum CodingKeys: String, CodingKey { case refreshInterval; case image } let container = try decoder.container(keyedBy: CodingKeys.self) let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) if var img = try container.decodeIfPresent(Source.self, forKey: .image) { return ( item: .brightness(refreshInterval: interval ?? 0.5), action: .none, longAction: .none, parameters: [.image: .image(source: img)] ) } else { return ( item: .brightness(refreshInterval: interval ?? 0.5), action: .none, longAction: .none, parameters: [:] ) } }, "sleep": { _ in ( item: .staticButton(title: "☕️"), action: .shellScript(executable: "/usr/bin/pmset", parameters: ["sleepnow"]), longAction: .none, parameters: [:] ) }, "displaySleep": { _ in ( item: .staticButton(title: "☕️"), action: .shellScript(executable: "/usr/bin/pmset", parameters: ["displaysleepnow"]), longAction: .none, parameters: [:] ) }, "music": { decoder in enum CodingKeys: String, CodingKey { case refreshInterval } let container = try decoder.container(keyedBy: CodingKeys.self) let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) return ( item: .music(interval: interval ?? 1800.00), action: .none, longAction: .none, parameters: [:] ) }, "group": { decoder in enum CodingKeys: CodingKey { case items } let container = try decoder.container(keyedBy: CodingKeys.self) let items = try container.decode([BarItemDefinition].self, forKey: .items) return ( item: .groupBar(items: items), action: .none, longAction: .none, parameters: [:] ) }, ] static let sharedInstance = SupportedTypesHolder() func lookup(by type: String) -> ParametersDecoder { return supportedTypes[type] ?? { decoder in ( item: try ItemType(from: decoder), action: try ActionType(from: decoder), longAction: try LongActionType(from: decoder), parameters: [:] )} } func register(typename: String, decoder: @escaping ParametersDecoder) { supportedTypes[typename] = decoder } func register(typename: String, item: ItemType, action: ActionType, longAction: LongActionType) { register(typename: typename) { _ in ( item: item, action, longAction, parameters: [:] ) } } } enum ItemType: Decodable { case staticButton(title: String) case appleScriptTitledButton(source: SourceProtocol, refreshInterval: Double) case timeButton(formatTemplate: String) case battery() case dock() case volume() case brightness(refreshInterval: Double) case weather(interval: Double, units: String, api_key: String, icon_type: String) case currency(interval: Double, from: String, to: String) case inputsource() case music(interval: Double) case groupBar(items: [BarItemDefinition]) case nightShift() case dnd() case pomodoro(workTime: Double, restTime: Double) private enum CodingKeys: String, CodingKey { case type case title case source case refreshInterval case from case to case units case api_key case icon_type case formatTemplate case image case url case longUrl case items case workTime case restTime } enum ItemTypeRaw: String, Decodable { case staticButton case appleScriptTitledButton case timeButton case battery case dock case volume case brightness case weather case currency case inputsource case music case groupBar case nightShift case dnd case pomodoro } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let type = try container.decode(ItemTypeRaw.self, forKey: .type) switch type { case .appleScriptTitledButton: let source = try container.decode(Source.self, forKey: .source) let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 1800.0 self = .appleScriptTitledButton(source: source, refreshInterval: interval) case .staticButton: let title = try container.decode(String.self, forKey: .title) self = .staticButton(title: title) case .timeButton: let template = try container.decodeIfPresent(String.self, forKey: .formatTemplate) ?? "HH:mm" self = .timeButton(formatTemplate: template) case .battery: self = .battery() case .dock: self = .dock() case .volume: self = .volume() case .brightness: let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 0.5 self = .brightness(refreshInterval: interval) case .weather: let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 1800.0 let units = try container.decodeIfPresent(String.self, forKey: .units) ?? "metric" let api_key = try container.decodeIfPresent(String.self, forKey: .api_key) ?? "32c4256d09a4c52b38aecddba7a078f6" let icon_type = try container.decodeIfPresent(String.self, forKey: .icon_type) ?? "text" self = .weather(interval: interval, units: units, api_key: api_key, icon_type: icon_type) case .currency: let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 600.0 let from = try container.decodeIfPresent(String.self, forKey: .from) ?? "RUB" let to = try container.decodeIfPresent(String.self, forKey: .to) ?? "USD" self = .currency(interval: interval, from: from, to: to) case .inputsource: self = .inputsource() case .music: let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 1800.0 self = .music(interval: interval) case .groupBar: let items = try container.decode([BarItemDefinition].self, forKey: .items) self = .groupBar(items: items) case .nightShift: self = .nightShift() case .dnd: self = .dnd() case .pomodoro: let workTime = try container.decodeIfPresent(Double.self, forKey: .workTime) ?? 1500.0 let restTime = try container.decodeIfPresent(Double.self, forKey: .restTime) ?? 600.0 self = .pomodoro(workTime: workTime, restTime: restTime) } } } enum ActionType: Decodable { case none case hidKey(keycode: Int32) case keyPress(keycode: Int) case appleScript(source: SourceProtocol) case shellScript(executable: String, parameters: [String]) case custom(closure: () -> Void) case openUrl(url: String) private enum CodingKeys: String, CodingKey { case action case keycode case actionAppleScript case executablePath case shellArguments case url } private enum ActionTypeRaw: String, Decodable { case hidKey case keyPress case appleScript case shellScript case openUrl } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let type = try container.decodeIfPresent(ActionTypeRaw.self, forKey: .action) switch type { case .some(.hidKey): let keycode = try container.decode(Int32.self, forKey: .keycode) self = .hidKey(keycode: keycode) case .some(.keyPress): let keycode = try container.decode(Int.self, forKey: .keycode) self = .keyPress(keycode: keycode) case .some(.appleScript): let source = try container.decode(Source.self, forKey: .actionAppleScript) self = .appleScript(source: source) case .some(.shellScript): let executable = try container.decode(String.self, forKey: .executablePath) let parameters = try container.decodeIfPresent([String].self, forKey: .shellArguments) ?? [] self = .shellScript(executable: executable, parameters: parameters) case .some(.openUrl): let url = try container.decode(String.self, forKey: .url) self = .openUrl(url: url) case .none: self = .none } } } enum LongActionType: Decodable { case none case hidKey(keycode: Int32) case keyPress(keycode: Int) case appleScript(source: SourceProtocol) case shellScript(executable: String, parameters: [String]) case custom(closure: () -> Void) case openUrl(url: String) private enum CodingKeys: String, CodingKey { case longAction case longKeycode case longActionAppleScript case longExecutablePath case longShellArguments case longUrl } private enum LongActionTypeRaw: String, Decodable { case hidKey case keyPress case appleScript case shellScript case openUrl } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let longType = try container.decodeIfPresent(LongActionTypeRaw.self, forKey: .longAction) switch longType { case .some(.hidKey): let keycode = try container.decode(Int32.self, forKey: .longKeycode) self = .hidKey(keycode: keycode) case .some(.keyPress): let keycode = try container.decode(Int.self, forKey: .longKeycode) self = .keyPress(keycode: keycode) case .some(.appleScript): let source = try container.decode(Source.self, forKey: .longActionAppleScript) self = .appleScript(source: source) case .some(.shellScript): let executable = try container.decode(String.self, forKey: .longExecutablePath) let parameters = try container.decodeIfPresent([String].self, forKey: .longShellArguments) ?? [] self = .shellScript(executable: executable, parameters: parameters) case .some(.openUrl): let longUrl = try container.decode(String.self, forKey: .longUrl) self = .openUrl(url: longUrl) case .none: self = .none } } } enum GeneralParameter { case width(_: CGFloat) case image(source: SourceProtocol) case align(_: Align) case bordered(_: Bool) case background(_: NSColor) case title(_: String) } struct GeneralParameters: Decodable { let parameters: [GeneralParameters.CodingKeys: GeneralParameter] enum CodingKeys: String, CodingKey { case width case image case align case bordered case background case title } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) var result: [GeneralParameters.CodingKeys: GeneralParameter] = [:] if let value = try container.decodeIfPresent(CGFloat.self, forKey: .width) { result[.width] = .width(value) } if let imageSource = try container.decodeIfPresent(Source.self, forKey: .image) { result[.image] = .image(source: imageSource) } let align = try container.decodeIfPresent(Align.self, forKey: .align) ?? .center result[.align] = .align(align) if let borderedFlag = try container.decodeIfPresent(Bool.self, forKey: .bordered) { result[.bordered] = .bordered(borderedFlag) } if let backgroundColor = try container.decodeIfPresent(String.self, forKey: .background)?.hexColor { result[.background] = .background(backgroundColor) } if let title = try container.decodeIfPresent(String.self, forKey: .title) { result[.title] = .title(title) } parameters = result } } protocol SourceProtocol { var data: Data? { get } var string: String? { get } var image: NSImage? { get } var appleScript: NSAppleScript? { get } } struct Source: Decodable, SourceProtocol { let filePath: String? let base64: String? let inline: String? private enum CodingKeys: String, CodingKey { case filePath case base64 case inline } var data: Data? { return base64?.base64Data ?? inline?.data(using: .utf8) ?? filePath?.fileData } var string: String? { return inline ?? filePath?.fileString } var image: NSImage? { return data?.image } var appleScript: NSAppleScript? { return filePath?.fileURL.appleScript ?? string?.appleScript } private init(filePath: String?, base64: String?, inline: String?) { self.filePath = filePath self.base64 = base64 self.inline = inline } init(filePath: String) { self.init(filePath: filePath, base64: nil, inline: nil) } } extension NSImage: SourceProtocol { var data: Data? { return nil } var string: String? { return nil } var image: NSImage? { return self } var appleScript: NSAppleScript? { return nil } } extension String { var base64Data: Data? { return Data(base64Encoded: self) } var fileData: Data? { return try? Data(contentsOf: URL(fileURLWithPath: self)) } var fileString: String? { var encoding: String.Encoding = .utf8 return try? String(contentsOfFile: self, usedEncoding: &encoding) } } extension Data { var utf8string: String? { return String(data: self, encoding: .utf8) } var image: NSImage? { return NSImage(data: self)?.resize(maxSize: NSSize(width: 24, height: 24)) } } enum Align: String, Decodable { case left case center case right } extension String { var fileURL: URL { return URL(fileURLWithPath: self) } var appleScript: NSAppleScript? { return NSAppleScript(source: self) } } extension URL { var appleScript: NSAppleScript? { guard FileManager.default.fileExists(atPath: path) else { return nil } return NSAppleScript(contentsOf: self, error: nil) } }