diff --git a/MTMR.xcodeproj/project.pbxproj b/MTMR.xcodeproj/project.pbxproj index 1d6e959..603b705 100644 --- a/MTMR.xcodeproj/project.pbxproj +++ b/MTMR.xcodeproj/project.pbxproj @@ -18,6 +18,8 @@ 36C2ECDD207C723B003CDA33 /* ParseConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36C2ECDC207C723B003CDA33 /* ParseConfigTests.swift */; }; 36C2ECDE207C82DE003CDA33 /* ItemsParsing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36C2ECDA207C3FE7003CDA33 /* ItemsParsing.swift */; }; 36C2ECE0207CB1B0003CDA33 /* defaultPreset.json in Resources */ = {isa = PBXBuildFile; fileRef = 36C2ECDF207CB1B0003CDA33 /* defaultPreset.json */; }; + 4CC9FEDC22FDEA65001512EB /* AMR_ANSIEscapeHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CC9FEDB22FDEA65001512EB /* AMR_ANSIEscapeHelper.m */; }; + 4CDC6E5022FCA93F0069ADD4 /* ShellScriptTouchBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDC6E4F22FCA93F0069ADD4 /* ShellScriptTouchBarItem.swift */; }; 4CFF5E5C22E623DD00BFB1EE /* YandexWeatherBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF5E5B22E623DD00BFB1EE /* YandexWeatherBarItem.swift */; }; 60173D3E20C0031B002C305F /* LaunchAtLoginController.m in Sources */ = {isa = PBXBuildFile; fileRef = 60173D3D20C0031B002C305F /* LaunchAtLoginController.m */; }; 6027D1B92080E52A004FFDC7 /* BrightnessViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6027D1B72080E52A004FFDC7 /* BrightnessViewController.swift */; }; @@ -92,6 +94,9 @@ 36C2ECDA207C3FE7003CDA33 /* ItemsParsing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemsParsing.swift; sourceTree = ""; }; 36C2ECDC207C723B003CDA33 /* ParseConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseConfigTests.swift; sourceTree = ""; }; 36C2ECDF207CB1B0003CDA33 /* defaultPreset.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = defaultPreset.json; sourceTree = ""; }; + 4CC9FEDA22FDEA65001512EB /* AMR_ANSIEscapeHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AMR_ANSIEscapeHelper.h; sourceTree = ""; }; + 4CC9FEDB22FDEA65001512EB /* AMR_ANSIEscapeHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AMR_ANSIEscapeHelper.m; sourceTree = ""; }; + 4CDC6E4F22FCA93F0069ADD4 /* ShellScriptTouchBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellScriptTouchBarItem.swift; sourceTree = ""; }; 4CFF5E5B22E623DD00BFB1EE /* YandexWeatherBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YandexWeatherBarItem.swift; sourceTree = ""; }; 60173D3C20C0031B002C305F /* LaunchAtLoginController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LaunchAtLoginController.h; sourceTree = ""; }; 60173D3D20C0031B002C305F /* LaunchAtLoginController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LaunchAtLoginController.m; sourceTree = ""; }; @@ -222,6 +227,7 @@ B0A7E9A9205D6AA400EEF070 /* KeyPress.swift */, B059D623205E04F3006E6B86 /* CustomButtonTouchBarItem.swift */, 36C2ECD8207B74B4003CDA33 /* AppleScriptTouchBarItem.swift */, + 4CDC6E4F22FCA93F0069ADD4 /* ShellScriptTouchBarItem.swift */, B059D621205E03F5006E6B86 /* TouchBarController.swift */, 36C2ECDA207C3FE7003CDA33 /* ItemsParsing.swift */, B0008E542080286C003AD4DD /* SupportHelpers.swift */, @@ -266,6 +272,8 @@ B0B1743B207D6ED40004B740 /* CBridge */ = { isa = PBXGroup; children = ( + 4CC9FEDA22FDEA65001512EB /* AMR_ANSIEscapeHelper.h */, + 4CC9FEDB22FDEA65001512EB /* AMR_ANSIEscapeHelper.m */, B059D629205E13E5006E6B86 /* TouchBarPrivateApi.h */, B059D62A205F0E7D006E6B86 /* TouchBarPrivateApi-Bridging.h */, B0F87719207AC1EA00D6E430 /* TouchBarSupport.m */, @@ -459,6 +467,7 @@ B059D624205E04F3006E6B86 /* CustomButtonTouchBarItem.swift in Sources */, 60173D3E20C0031B002C305F /* LaunchAtLoginController.m in Sources */, 6027D1BA2080E52A004FFDC7 /* VolumeViewController.swift in Sources */, + 4CDC6E5022FCA93F0069ADD4 /* ShellScriptTouchBarItem.swift in Sources */, 607EEA4B2087835F009DA5F0 /* WeatherBarItem.swift in Sources */, B0F3112520C9E35F0076BB88 /* SupportNSTouchBar.swift in Sources */, 4CFF5E5C22E623DD00BFB1EE /* YandexWeatherBarItem.swift in Sources */, @@ -472,6 +481,7 @@ B0A7E9AA205D6AA400EEF070 /* KeyPress.swift in Sources */, 36C2ECD7207B6DAE003CDA33 /* TimeTouchBarItem.swift in Sources */, 607EEA4D2087A8DA009DA5F0 /* CurrencyBarItem.swift in Sources */, + 4CC9FEDC22FDEA65001512EB /* AMR_ANSIEscapeHelper.m in Sources */, 6027D1B92080E52A004FFDC7 /* BrightnessViewController.swift in Sources */, 368EDDE720812A1D00E10953 /* ScrollViewItem.swift in Sources */, B04B7BB72087398C00C835D0 /* BatteryBarItem.swift in Sources */, diff --git a/MTMR/CBridge/AMR_ANSIEscapeHelper.h b/MTMR/CBridge/AMR_ANSIEscapeHelper.h new file mode 100644 index 0000000..d2fa06c --- /dev/null +++ b/MTMR/CBridge/AMR_ANSIEscapeHelper.h @@ -0,0 +1,326 @@ +// +// ANSIEscapeHelper.h +// AnsiColorsTest +// +// Created by Ali Rantakari on 18.3.09. +// +// Version 0.9.6 +// +/* +The MIT License + +Copyright (c) 2008-2009,2013 Ali Rantakari + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#import + + +#if !__has_feature(objc_arc) +#warning "This code requires ARC to be enabled." +#endif + + +// dictionary keys for the SGR code dictionaries that the array +// escapeCodesForString:cleanString: returns contains +#define kAMRCodeDictKey_code @"code" +#define kAMRCodeDictKey_location @"location" + +// dictionary keys for the string formatting attribute +// dictionaries that the array attributesForString:cleanString: +// returns contains +#define kAMRAttrDictKey_range @"range" +#define kAMRAttrDictKey_attrName @"attributeName" +#define kAMRAttrDictKey_attrValue @"attributeValue" + + +/*! + @enum AMR_SGRCode + + @abstract SGR (Select Graphic Rendition) ANSI control codes. + */ +typedef enum +{ + AMR_SGRCodeNoneOrInvalid = -1, + + AMR_SGRCodeAllReset = 0, + + AMR_SGRCodeIntensityBold = 1, + AMR_SGRCodeIntensityFaint = 2, + AMR_SGRCodeIntensityNormal = 22, + + AMR_SGRCodeItalicOn = 3, + + AMR_SGRCodeUnderlineSingle = 4, + AMR_SGRCodeUnderlineDouble = 21, + AMR_SGRCodeUnderlineNone = 24, + + AMR_SGRCodeFgBlack = 30, + AMR_SGRCodeFgRed = 31, + AMR_SGRCodeFgGreen = 32, + AMR_SGRCodeFgYellow = 33, + AMR_SGRCodeFgBlue = 34, + AMR_SGRCodeFgMagenta = 35, + AMR_SGRCodeFgCyan = 36, + AMR_SGRCodeFgWhite = 37, + AMR_SGRCodeFgReset = 39, + + AMR_SGRCodeBgBlack = 40, + AMR_SGRCodeBgRed = 41, + AMR_SGRCodeBgGreen = 42, + AMR_SGRCodeBgYellow = 43, + AMR_SGRCodeBgBlue = 44, + AMR_SGRCodeBgMagenta = 45, + AMR_SGRCodeBgCyan = 46, + AMR_SGRCodeBgWhite = 47, + AMR_SGRCodeBgReset = 49, + + AMR_SGRCodeFgBrightBlack = 90, + AMR_SGRCodeFgBrightRed = 91, + AMR_SGRCodeFgBrightGreen = 92, + AMR_SGRCodeFgBrightYellow = 93, + AMR_SGRCodeFgBrightBlue = 94, + AMR_SGRCodeFgBrightMagenta = 95, + AMR_SGRCodeFgBrightCyan = 96, + AMR_SGRCodeFgBrightWhite = 97, + + AMR_SGRCodeBgBrightBlack = 100, + AMR_SGRCodeBgBrightRed = 101, + AMR_SGRCodeBgBrightGreen = 102, + AMR_SGRCodeBgBrightYellow = 103, + AMR_SGRCodeBgBrightBlue = 104, + AMR_SGRCodeBgBrightMagenta = 105, + AMR_SGRCodeBgBrightCyan = 106, + AMR_SGRCodeBgBrightWhite = 107 +} AMR_SGRCode; + + + + + + +/*! + @class AMR_ANSIEscapeHelper + + @abstract Contains helper methods for dealing with strings + that contain ANSI escape sequences for formatting (colors, + underlining, bold etc.) + */ +@interface AMR_ANSIEscapeHelper : NSObject + +/*! + @property defaultStringColor + + @abstract The default color used when creating an attributed string (default is black). + */ +@property(copy) NSColor *defaultStringColor; + + +/*! + @property font + + @abstract The font to use when creating string formatting attribute values. + */ +@property(copy) NSFont *font; + +/*! + @property ansiColors + + @abstract The colors to use for displaying ANSI colors. + + @discussion Keys in this dictionary should be NSNumber objects containing SGR code + values from the AMR_SGRCode enum. The corresponding values for these keys + should be NSColor objects. If this property is nil or if it doesn't + contain a key for a specific SGR code, the default color will be used + instead. + */ +@property(retain) NSMutableDictionary *ansiColors; + + +/*! + @method attributedStringWithANSIEscapedString: + + @abstract Returns an attributed string that corresponds both in contents + and formatting to a given string that contains ANSI escape + sequences. + + @param aString A String containing ANSI escape sequences + + @result An attributed string that mimics as closely as possible + the formatting of the given ANSI-escaped string. + */ +- (NSAttributedString*) attributedStringWithANSIEscapedString:(NSString*)aString; + + +/*! + @method ansiEscapedStringWithAttributedString: + + @abstract Returns a string containing ANSI escape sequences that corresponds + both in contents and formatting to a given attributed string. + + @param aAttributedString An attributed string + + @result A string that mimics as closely as possible + the formatting of the given attributed string with + ANSI escape sequences. + */ +- (NSString*) ansiEscapedStringWithAttributedString:(NSAttributedString*)aAttributedString; + + +/*! + @method escapeCodesForString:cleanString: + + @abstract Returns an array of SGR codes and their locations from a + string containing ANSI escape sequences as well as a "clean" + version of the string (i.e. one without the ANSI escape + sequences.) + + @param aString A String containing ANSI escape sequences + @param aCleanString Upon return, contains a "clean" version of aString (i.e. aString + without the ANSI escape sequences) + + @result An array of NSDictionary objects, each of which has + an NSNumber value for the key "code" (specifying an SGR code) and + another NSNumber value for the key "location" (specifying the + location of the code within aCleanString.) + */ +- (NSArray*) escapeCodesForString:(NSString*)aString cleanString:(NSString**)aCleanString; + + +/*! + @method ansiEscapedStringWithCodesAndLocations:cleanString: + + @abstract Returns a string containing ANSI escape codes for formatting based + on a string and an array of SGR codes and their locations within + the given string. + + @param aCodesArray An array of NSDictionary objects, each of which should have + an NSNumber value for the key "code" (specifying an SGR + code) and another NSNumber value for the key "location" + (specifying the location of this SGR code in aCleanString.) + @param aCleanString The string to which to insert the ANSI escape codes + described in aCodesArray. + + @result A string containing ANSI escape sequences. + */ +- (NSString*) ansiEscapedStringWithCodesAndLocations:(NSArray*)aCodesArray cleanString:(NSString*)aCleanString; + + +/*! + @method attributesForString:cleanString: + + @abstract Convert ANSI escape sequences in a string to string formatting attributes. + + @discussion Given a string with some ANSI escape sequences in it, this method returns + attributes for formatting the specified string according to those ANSI + escape sequences as well as a "clean" (i.e. free of the escape sequences) + version of this string. + + @param aString A String containing ANSI escape sequences + @param aCleanString Upon return, contains a "clean" version of aString (i.e. aString + without the ANSI escape sequences.) Pass in NULL if you're not + interested in this. + + @result An array containing NSDictionary objects, each of which has keys "range" + (an NSValue containing an NSRange, specifying the range for the + attribute within the "clean" version of aString), "attributeName" (an + NSString) and "attributeValue" (an NSObject). You may use these as + arguments for NSMutableAttributedString's methods for setting the + visual formatting. + */ +- (NSArray*) attributesForString:(NSString*)aString cleanString:(NSString**)aCleanString; + + +/*! + @method AMR_SGRCode:endsFormattingIntroducedByCode: + + @abstract Whether the occurrence of a given SGR code would end the formatting run + introduced by another SGR code. + + @discussion For example, AMR_SGRCodeFgReset, AMR_SGRCodeAllReset or any SGR code + specifying a foreground color would end the formatting run + introduced by a foreground color -specifying SGR code. + + @param endCode The SGR code to test as a candidate for ending the formatting run + introduced by startCode + @param startCode The SGR code that has introduced a formatting run + + @result YES if the occurrence of endCode would end the formatting run + introduced by startCode, NO otherwise. + */ +- (BOOL) AMR_SGRCode:(AMR_SGRCode)endCode endsFormattingIntroducedByCode:(AMR_SGRCode)startCode; + + +/*! + @method colorForSGRCode: + + @abstract Returns the color to use for displaying a specific ANSI color. + + @discussion This method first considers the values set in the ansiColors + property and only then the standard basic colors (NSColor's + redColor, blueColor etc.) + + @param code An SGR code that specifies an ANSI color. + + @result The color to use for displaying the ANSI color specified by code. + */ +- (NSColor*) colorForSGRCode:(AMR_SGRCode)code; + + +/*! + @method AMR_SGRCodeForColor:isForegroundColor: + + @abstract Returns a color SGR code that corresponds to a given color. + + @discussion This method matches colors to their equivalent SGR codes + by going through the colors specified in the ansiColors + dictionary, and if ansiColors is null or if a match is + not found there, by comparing the given color to the + standard basic colors (NSColor's redColor, blueColor + etc.) The comparison is done simply by checking for + equality. + + @param aColor The color to get a corresponding SGR code for + @param aForeground Whether you want a foreground or background color code + + @result SGR code that corresponds with aColor. + */ +- (AMR_SGRCode) AMR_SGRCodeForColor:(NSColor*)aColor isForegroundColor:(BOOL)aForeground; + + +/*! + @method closestSGRCodeForColor:isForegroundColor: + + @abstract Returns a color SGR code that represents the closest ANSI + color to a given color. + + @discussion This method attempts to find the closest ANSI color to + aColor and return its SGR code. + + @param color The color to get a closest color SGR code match for + @param foreground Whether you want a foreground or background color code + + @result SGR code for the ANSI color that is closest to aColor. + */ +- (AMR_SGRCode) closestSGRCodeForColor:(NSColor *)color isForegroundColor:(BOOL)foreground; + + + +@end diff --git a/MTMR/CBridge/AMR_ANSIEscapeHelper.m b/MTMR/CBridge/AMR_ANSIEscapeHelper.m new file mode 100644 index 0000000..d590b78 --- /dev/null +++ b/MTMR/CBridge/AMR_ANSIEscapeHelper.m @@ -0,0 +1,996 @@ +// +// ANSIEscapeHelper.m +// +// Created by Ali Rantakari on 18.3.09. + +/* +The MIT License + +Copyright (c) 2008-2009,2013 Ali Rantakari + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +/* + todo: + + - don't add useless "reset" escape codes to the string in + -ansiEscapedStringWithAttributedString: + + */ + + + +#import "AMR_ANSIEscapeHelper.h" + + +// the CSI (Control Sequence Initiator) -- i.e. "escape sequence prefix". +// (add your own CSI:Miami joke here) +#define kANSIEscapeCSI @"\033[" + +// the end byte of an SGR (Select Graphic Rendition) +// ANSI Escape Sequence +#define kANSIEscapeSGREnd @"m" + + +// color definition helper macros +#define kBrightColorBrightness 1.0 +#define kBrightColorSaturation 0.4 +#define kBrightColorAlpha 1.0 +#define kBrightColorWithHue(h) [NSColor colorWithCalibratedHue:(h) saturation:kBrightColorSaturation brightness:kBrightColorBrightness alpha:kBrightColorAlpha] + +// default colors +#define kDefaultANSIColorFgBlack NSColor.blackColor +#define kDefaultANSIColorFgRed NSColor.redColor +#define kDefaultANSIColorFgGreen NSColor.greenColor +#define kDefaultANSIColorFgYellow NSColor.yellowColor +#define kDefaultANSIColorFgBlue NSColor.blueColor +#define kDefaultANSIColorFgMagenta NSColor.magentaColor +#define kDefaultANSIColorFgCyan NSColor.cyanColor +#define kDefaultANSIColorFgWhite NSColor.whiteColor + +#define kDefaultANSIColorFgBrightBlack [NSColor colorWithCalibratedWhite:0.337 alpha:1.0] +#define kDefaultANSIColorFgBrightRed kBrightColorWithHue(1.0) +#define kDefaultANSIColorFgBrightGreen kBrightColorWithHue(1.0/3.0) +#define kDefaultANSIColorFgBrightYellow kBrightColorWithHue(1.0/6.0) +#define kDefaultANSIColorFgBrightBlue kBrightColorWithHue(2.0/3.0) +#define kDefaultANSIColorFgBrightMagenta kBrightColorWithHue(5.0/6.0) +#define kDefaultANSIColorFgBrightCyan kBrightColorWithHue(0.5) +#define kDefaultANSIColorFgBrightWhite NSColor.whiteColor + +#define kDefaultANSIColorBgBlack NSColor.blackColor +#define kDefaultANSIColorBgRed NSColor.redColor +#define kDefaultANSIColorBgGreen NSColor.greenColor +#define kDefaultANSIColorBgYellow NSColor.yellowColor +#define kDefaultANSIColorBgBlue NSColor.blueColor +#define kDefaultANSIColorBgMagenta NSColor.magentaColor +#define kDefaultANSIColorBgCyan NSColor.cyanColor +#define kDefaultANSIColorBgWhite NSColor.whiteColor + +#define kDefaultANSIColorBgBrightBlack kDefaultANSIColorFgBrightBlack +#define kDefaultANSIColorBgBrightRed kDefaultANSIColorFgBrightRed +#define kDefaultANSIColorBgBrightGreen kDefaultANSIColorFgBrightGreen +#define kDefaultANSIColorBgBrightYellow kDefaultANSIColorFgBrightYellow +#define kDefaultANSIColorBgBrightBlue kDefaultANSIColorFgBrightBlue +#define kDefaultANSIColorBgBrightMagenta kDefaultANSIColorFgBrightMagenta +#define kDefaultANSIColorBgBrightCyan kDefaultANSIColorFgBrightCyan +#define kDefaultANSIColorBgBrightWhite kDefaultANSIColorFgBrightWhite + +#define kDefaultFontSize [NSFont systemFontOfSize:NSFont.systemFontSize] +#define kDefaultForegroundColor NSColor.blackColor + +// minimum weight for an NSFont for it to be considered bold +#define kBoldFontMinWeight 9 + + +@implementation AMR_ANSIEscapeHelper + +- (id) init +{ + if (!(self = [super init])) + return nil; + + self.ansiColors = [NSMutableDictionary dictionary]; + + return self; +} + + + +- (NSAttributedString*) attributedStringWithANSIEscapedString:(NSString*)aString +{ + if (aString == nil) + return nil; + + NSString *cleanString; + NSArray *attributesAndRanges = [self attributesForString:aString cleanString:&cleanString]; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] + initWithString:cleanString + attributes:@{ + NSFontAttributeName: self.font ?: kDefaultFontSize, + NSForegroundColorAttributeName: self.defaultStringColor ?: kDefaultForegroundColor + }]; + + for (NSDictionary *thisAttributeDict in attributesAndRanges) + { + [attributedString + addAttribute:thisAttributeDict[kAMRAttrDictKey_attrName] + value:thisAttributeDict[kAMRAttrDictKey_attrValue] + range:[thisAttributeDict[kAMRAttrDictKey_range] rangeValue] + ]; + } + + return attributedString; +} + + + +- (NSString*) ansiEscapedStringWithAttributedString:(NSAttributedString*)aAttributedString +{ + NSMutableArray *codesAndLocations = [NSMutableArray array]; + + NSArray *attrNames = @[ + NSFontAttributeName, NSForegroundColorAttributeName, + NSBackgroundColorAttributeName, NSUnderlineStyleAttributeName, + ]; + + for (NSString *thisAttrName in attrNames) + { + NSRange limitRange = NSMakeRange(0, aAttributedString.length); + id attributeValue; + NSRange effectiveRange; + + while (limitRange.length > 0) + { + attributeValue = [aAttributedString + attribute:thisAttrName + atIndex:limitRange.location + longestEffectiveRange:&effectiveRange + inRange:limitRange + ]; + + AMR_SGRCode thisSGRCode = AMR_SGRCodeNoneOrInvalid; + + if ([thisAttrName isEqualToString:NSForegroundColorAttributeName]) + { + if (attributeValue != nil) + thisSGRCode = [self closestSGRCodeForColor:attributeValue isForegroundColor:YES]; + else + thisSGRCode = AMR_SGRCodeFgReset; + } + else if ([thisAttrName isEqualToString:NSBackgroundColorAttributeName]) + { + if (attributeValue != nil) + thisSGRCode = [self closestSGRCodeForColor:attributeValue isForegroundColor:NO]; + else + thisSGRCode = AMR_SGRCodeBgReset; + } + else if ([thisAttrName isEqualToString:NSFontAttributeName]) + { + // we currently only use NSFontAttributeName for bolding so + // here we assume that the formatting "type" in ANSI SGR + // terms is indeed intensity + if (attributeValue != nil) + thisSGRCode = ([NSFontManager.sharedFontManager weightOfFont:attributeValue] >= kBoldFontMinWeight) + ? AMR_SGRCodeIntensityBold : AMR_SGRCodeIntensityNormal; + else + thisSGRCode = AMR_SGRCodeIntensityNormal; + } + else if ([thisAttrName isEqualToString:NSUnderlineStyleAttributeName]) + { + if (attributeValue != nil) + { + if ([attributeValue intValue] == NSUnderlineStyleSingle) + thisSGRCode = AMR_SGRCodeUnderlineSingle; + else if ([attributeValue intValue] == NSUnderlineStyleDouble) + thisSGRCode = AMR_SGRCodeUnderlineDouble; + else + thisSGRCode = AMR_SGRCodeUnderlineNone; + } + else + thisSGRCode = AMR_SGRCodeUnderlineNone; + } + + if (thisSGRCode != AMR_SGRCodeNoneOrInvalid) + { + [codesAndLocations addObject: @{ + kAMRCodeDictKey_code: @(thisSGRCode), + kAMRCodeDictKey_location: @(effectiveRange.location), + }]; + } + + limitRange = NSMakeRange(NSMaxRange(effectiveRange), + NSMaxRange(limitRange) - NSMaxRange(effectiveRange)); + } + } + + return [self ansiEscapedStringWithCodesAndLocations:codesAndLocations cleanString:aAttributedString.string]; +} + + +- (NSArray*) escapeCodesForString:(NSString*)aString cleanString:(NSString**)aCleanString +{ + if (aString == nil) + return nil; + if (aString.length <= kANSIEscapeCSI.length) + { + if (aCleanString) + *aCleanString = aString.copy; + return @[]; + } + + NSString *cleanString = @""; + + // find all escape sequence codes from aString and put them in this array + // along with their start locations within the "clean" version of aString + NSMutableArray *formatCodes = [NSMutableArray array]; + + NSUInteger aStringLength = aString.length; + NSUInteger coveredLength = 0; + NSRange searchRange = NSMakeRange(0,aStringLength); + NSRange thisEscapeSequenceRange; + do + { + thisEscapeSequenceRange = [aString rangeOfString:kANSIEscapeCSI options:NSLiteralSearch range:searchRange]; + if (thisEscapeSequenceRange.location != NSNotFound) + { + // adjust range's length so that it encompasses the whole ANSI escape sequence + // and not just the Control Sequence Initiator (the "prefix") by finding the + // final byte of the control sequence (one that has an ASCII decimal value + // between 64 and 126.) at the same time, read all formatting codes from inside + // this escape sequence (there may be several, separated by semicolons.) + NSMutableArray *codes = [NSMutableArray array]; + unsigned int code = 0; + unsigned int lengthAddition = 1; + NSUInteger thisIndex; + for (;;) + { + thisIndex = (NSMaxRange(thisEscapeSequenceRange)+lengthAddition-1); + if (thisIndex >= aStringLength) + break; + + unichar c = [aString characterAtIndex:thisIndex]; + + if (('0' <= c) && (c <= '9')) + { + int digit = c - '0'; + code = (code == 0) ? digit : code*10+digit; + } + + // ASCII decimal 109 is the SGR (Select Graphic Rendition) final byte + // ("m"). this means that the code value we've just read specifies formatting + // for the output; exactly what we're interested in. + if (c == 'm') + { + [codes addObject:@(code)]; + break; + } + else if ((64 <= c) && (c <= 126)) // any other valid final byte + { + [codes removeAllObjects]; + break; + } + else if (c == ';') // separates codes within the same sequence + { + [codes addObject:@(code)]; + code = 0; + } + + lengthAddition++; + } + thisEscapeSequenceRange.length += lengthAddition; + + NSUInteger locationInCleanString = coveredLength+thisEscapeSequenceRange.location-searchRange.location; + + for (NSNumber *codeToAdd in codes) + { + [formatCodes addObject: @{ + kAMRCodeDictKey_code: codeToAdd, + kAMRCodeDictKey_location: @(locationInCleanString) + }]; + } + + NSUInteger thisCoveredLength = thisEscapeSequenceRange.location-searchRange.location; + if (thisCoveredLength > 0) + cleanString = [cleanString stringByAppendingString:[aString substringWithRange:NSMakeRange(searchRange.location, thisCoveredLength)]]; + + coveredLength += thisCoveredLength; + searchRange.location = NSMaxRange(thisEscapeSequenceRange); + searchRange.length = aStringLength-searchRange.location; + } + } + while(thisEscapeSequenceRange.location != NSNotFound); + + if (searchRange.length > 0) + cleanString = [cleanString stringByAppendingString:[aString substringWithRange:searchRange]]; + + if (aCleanString) + *aCleanString = cleanString; + return formatCodes; +} + + + + +- (NSString*) ansiEscapedStringWithCodesAndLocations:(NSArray*)aCodesArray cleanString:(NSString*)aCleanString +{ + NSMutableString* retStr = [NSMutableString stringWithCapacity:aCleanString.length]; + + NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:kAMRCodeDictKey_location ascending:YES]; + NSArray *codesArray = [aCodesArray sortedArrayUsingDescriptors:@[sortDescriptor]]; + + NSUInteger aCleanStringIndex = 0; + NSUInteger aCleanStringLength = aCleanString.length; + for (NSDictionary *thisCodeDict in codesArray) + { + if (!( thisCodeDict[kAMRCodeDictKey_code] && + thisCodeDict[kAMRCodeDictKey_location] + )) + continue; + + AMR_SGRCode thisCode = [thisCodeDict[kAMRCodeDictKey_code] unsignedIntValue]; + NSUInteger formattingRunStartLocation = [thisCodeDict[kAMRCodeDictKey_location] unsignedIntegerValue]; + + if (formattingRunStartLocation > aCleanStringLength) + continue; + + if (aCleanStringIndex < formattingRunStartLocation) + [retStr appendString:[aCleanString substringWithRange:NSMakeRange(aCleanStringIndex, formattingRunStartLocation-aCleanStringIndex)]]; + [retStr appendFormat:@"%@%d%@", kANSIEscapeCSI, thisCode, kANSIEscapeSGREnd]; + + aCleanStringIndex = formattingRunStartLocation; + } + + if (aCleanStringIndex < aCleanStringLength) + [retStr appendString:[aCleanString substringFromIndex:aCleanStringIndex]]; + + [retStr appendFormat:@"%@%d%@", kANSIEscapeCSI, AMR_SGRCodeAllReset, kANSIEscapeSGREnd]; + + return retStr; +} + + + + + +- (NSArray*) attributesForString:(NSString*)aString cleanString:(NSString**)aCleanString +{ + if (aString == nil) + return nil; + if (aString.length <= kANSIEscapeCSI.length) + { + if (aCleanString) + *aCleanString = aString.copy; + return @[]; + } + + NSMutableArray *attrsAndRanges = [NSMutableArray array]; + + NSString *cleanString; + NSArray *formatCodes = [self escapeCodesForString:aString cleanString:&cleanString]; + + // go through all the found escape sequence codes and for each one, create + // the string formatting attribute name and value, find the next escape + // sequence that specifies the end of the formatting run started by + // the currently handled code, and generate a range from the difference + // in those codes' locations within the clean aString. + for (NSUInteger iCode = 0; iCode < formatCodes.count; iCode++) + { + NSDictionary *thisCodeDict = formatCodes[iCode]; + AMR_SGRCode thisCode = [thisCodeDict[kAMRCodeDictKey_code] unsignedIntValue]; + NSUInteger formattingRunStartLocation = [thisCodeDict[kAMRCodeDictKey_location] unsignedIntegerValue]; + + // the attributed string attribute name for the formatting run introduced + // by this code + NSString *thisAttributeName = nil; + + // the attributed string attribute value for this formatting run introduced + // by this code + NSObject *thisAttributeValue = nil; + + // set attribute name + switch(thisCode) + { + case AMR_SGRCodeFgBlack: + case AMR_SGRCodeFgRed: + case AMR_SGRCodeFgGreen: + case AMR_SGRCodeFgYellow: + case AMR_SGRCodeFgBlue: + case AMR_SGRCodeFgMagenta: + case AMR_SGRCodeFgCyan: + case AMR_SGRCodeFgWhite: + case AMR_SGRCodeFgBrightBlack: + case AMR_SGRCodeFgBrightRed: + case AMR_SGRCodeFgBrightGreen: + case AMR_SGRCodeFgBrightYellow: + case AMR_SGRCodeFgBrightBlue: + case AMR_SGRCodeFgBrightMagenta: + case AMR_SGRCodeFgBrightCyan: + case AMR_SGRCodeFgBrightWhite: + thisAttributeName = NSForegroundColorAttributeName; + break; + case AMR_SGRCodeBgBlack: + case AMR_SGRCodeBgRed: + case AMR_SGRCodeBgGreen: + case AMR_SGRCodeBgYellow: + case AMR_SGRCodeBgBlue: + case AMR_SGRCodeBgMagenta: + case AMR_SGRCodeBgCyan: + case AMR_SGRCodeBgWhite: + case AMR_SGRCodeBgBrightBlack: + case AMR_SGRCodeBgBrightRed: + case AMR_SGRCodeBgBrightGreen: + case AMR_SGRCodeBgBrightYellow: + case AMR_SGRCodeBgBrightBlue: + case AMR_SGRCodeBgBrightMagenta: + case AMR_SGRCodeBgBrightCyan: + case AMR_SGRCodeBgBrightWhite: + thisAttributeName = NSBackgroundColorAttributeName; + break; + case AMR_SGRCodeIntensityBold: + case AMR_SGRCodeIntensityNormal: + case AMR_SGRCodeIntensityFaint: + thisAttributeName = NSFontAttributeName; + break; + case AMR_SGRCodeUnderlineSingle: + case AMR_SGRCodeUnderlineDouble: + case AMR_SGRCodeUnderlineNone: + thisAttributeName = NSUnderlineStyleAttributeName; + break; + case AMR_SGRCodeAllReset: + case AMR_SGRCodeFgReset: + case AMR_SGRCodeBgReset: + case AMR_SGRCodeNoneOrInvalid: + case AMR_SGRCodeItalicOn: + continue; + } + + // set attribute value + switch(thisCode) + { + case AMR_SGRCodeBgBlack: + case AMR_SGRCodeFgBlack: + case AMR_SGRCodeBgRed: + case AMR_SGRCodeFgRed: + case AMR_SGRCodeBgGreen: + case AMR_SGRCodeFgGreen: + case AMR_SGRCodeBgYellow: + case AMR_SGRCodeFgYellow: + case AMR_SGRCodeBgBlue: + case AMR_SGRCodeFgBlue: + case AMR_SGRCodeBgMagenta: + case AMR_SGRCodeFgMagenta: + case AMR_SGRCodeBgCyan: + case AMR_SGRCodeFgCyan: + case AMR_SGRCodeBgWhite: + case AMR_SGRCodeFgWhite: + case AMR_SGRCodeBgBrightBlack: + case AMR_SGRCodeFgBrightBlack: + case AMR_SGRCodeBgBrightRed: + case AMR_SGRCodeFgBrightRed: + case AMR_SGRCodeBgBrightGreen: + case AMR_SGRCodeFgBrightGreen: + case AMR_SGRCodeBgBrightYellow: + case AMR_SGRCodeFgBrightYellow: + case AMR_SGRCodeBgBrightBlue: + case AMR_SGRCodeFgBrightBlue: + case AMR_SGRCodeBgBrightMagenta: + case AMR_SGRCodeFgBrightMagenta: + case AMR_SGRCodeBgBrightCyan: + case AMR_SGRCodeFgBrightCyan: + case AMR_SGRCodeBgBrightWhite: + case AMR_SGRCodeFgBrightWhite: + thisAttributeValue = [self colorForSGRCode:thisCode]; + break; + case AMR_SGRCodeIntensityBold: + { + NSFont *boldFont = [NSFontManager.sharedFontManager convertFont:self.font toHaveTrait:NSBoldFontMask]; + thisAttributeValue = boldFont; + } + break; + case AMR_SGRCodeIntensityNormal: + case AMR_SGRCodeIntensityFaint: + { + NSFont *unboldFont = [NSFontManager.sharedFontManager convertFont:self.font toHaveTrait:NSUnboldFontMask]; + thisAttributeValue = unboldFont; + } + break; + case AMR_SGRCodeUnderlineSingle: + thisAttributeValue = @(NSUnderlineStyleSingle); + break; + case AMR_SGRCodeUnderlineDouble: + thisAttributeValue = @(NSUnderlineStyleDouble); + break; + case AMR_SGRCodeUnderlineNone: + thisAttributeValue = @(NSUnderlineStyleNone); + break; + case AMR_SGRCodeAllReset: + case AMR_SGRCodeFgReset: + case AMR_SGRCodeBgReset: + case AMR_SGRCodeNoneOrInvalid: + case AMR_SGRCodeItalicOn: + break; + } + + + // find the next sequence that specifies the end of this formatting run + NSInteger formattingRunEndLocation = -1; + if (iCode < (formatCodes.count - 1)) + { + NSDictionary *thisEndCodeCandidateDict; + unichar thisEndCodeCandidate; + for (NSUInteger iEndCode = iCode+1; iEndCode < formatCodes.count; iEndCode++) + { + thisEndCodeCandidateDict = formatCodes[iEndCode]; + thisEndCodeCandidate = [thisEndCodeCandidateDict[kAMRCodeDictKey_code] unsignedIntValue]; + + if ([self AMR_SGRCode:thisEndCodeCandidate endsFormattingIntroducedByCode:thisCode]) + { + formattingRunEndLocation = [thisEndCodeCandidateDict[kAMRCodeDictKey_location] unsignedIntegerValue]; + break; + } + } + } + if (formattingRunEndLocation == -1) + formattingRunEndLocation = cleanString.length; + + if (thisAttributeName && thisAttributeValue) + { + [attrsAndRanges addObject:@{ + kAMRAttrDictKey_range: [NSValue valueWithRange:NSMakeRange(formattingRunStartLocation, (formattingRunEndLocation-formattingRunStartLocation))], + kAMRAttrDictKey_attrName: thisAttributeName, + kAMRAttrDictKey_attrValue: thisAttributeValue, + }]; + } + } + + if (aCleanString) + *aCleanString = cleanString; + return attrsAndRanges; +} + + + + + +- (BOOL) AMR_SGRCode:(AMR_SGRCode)endCode endsFormattingIntroducedByCode:(AMR_SGRCode)startCode +{ + switch(startCode) + { + case AMR_SGRCodeFgBlack: + case AMR_SGRCodeFgRed: + case AMR_SGRCodeFgGreen: + case AMR_SGRCodeFgYellow: + case AMR_SGRCodeFgBlue: + case AMR_SGRCodeFgMagenta: + case AMR_SGRCodeFgCyan: + case AMR_SGRCodeFgWhite: + case AMR_SGRCodeFgBrightBlack: + case AMR_SGRCodeFgBrightRed: + case AMR_SGRCodeFgBrightGreen: + case AMR_SGRCodeFgBrightYellow: + case AMR_SGRCodeFgBrightBlue: + case AMR_SGRCodeFgBrightMagenta: + case AMR_SGRCodeFgBrightCyan: + case AMR_SGRCodeFgBrightWhite: + return (endCode == AMR_SGRCodeAllReset || endCode == AMR_SGRCodeFgReset || + endCode == AMR_SGRCodeFgBlack || endCode == AMR_SGRCodeFgRed || + endCode == AMR_SGRCodeFgGreen || endCode == AMR_SGRCodeFgYellow || + endCode == AMR_SGRCodeFgBlue || endCode == AMR_SGRCodeFgMagenta || + endCode == AMR_SGRCodeFgCyan || endCode == AMR_SGRCodeFgWhite || + endCode == AMR_SGRCodeFgBrightBlack || endCode == AMR_SGRCodeFgBrightRed || + endCode == AMR_SGRCodeFgBrightGreen || endCode == AMR_SGRCodeFgBrightYellow || + endCode == AMR_SGRCodeFgBrightBlue || endCode == AMR_SGRCodeFgBrightMagenta || + endCode == AMR_SGRCodeFgBrightCyan || endCode == AMR_SGRCodeFgBrightWhite); + case AMR_SGRCodeBgBlack: + case AMR_SGRCodeBgRed: + case AMR_SGRCodeBgGreen: + case AMR_SGRCodeBgYellow: + case AMR_SGRCodeBgBlue: + case AMR_SGRCodeBgMagenta: + case AMR_SGRCodeBgCyan: + case AMR_SGRCodeBgWhite: + case AMR_SGRCodeBgBrightBlack: + case AMR_SGRCodeBgBrightRed: + case AMR_SGRCodeBgBrightGreen: + case AMR_SGRCodeBgBrightYellow: + case AMR_SGRCodeBgBrightBlue: + case AMR_SGRCodeBgBrightMagenta: + case AMR_SGRCodeBgBrightCyan: + case AMR_SGRCodeBgBrightWhite: + return (endCode == AMR_SGRCodeAllReset || endCode == AMR_SGRCodeBgReset || + endCode == AMR_SGRCodeBgBlack || endCode == AMR_SGRCodeBgRed || + endCode == AMR_SGRCodeBgGreen || endCode == AMR_SGRCodeBgYellow || + endCode == AMR_SGRCodeBgBlue || endCode == AMR_SGRCodeBgMagenta || + endCode == AMR_SGRCodeBgCyan || endCode == AMR_SGRCodeBgWhite || + endCode == AMR_SGRCodeBgBrightBlack || endCode == AMR_SGRCodeBgBrightRed || + endCode == AMR_SGRCodeBgBrightGreen || endCode == AMR_SGRCodeBgBrightYellow || + endCode == AMR_SGRCodeBgBrightBlue || endCode == AMR_SGRCodeBgBrightMagenta || + endCode == AMR_SGRCodeBgBrightCyan || endCode == AMR_SGRCodeBgBrightWhite); + case AMR_SGRCodeIntensityBold: + case AMR_SGRCodeIntensityNormal: + return (endCode == AMR_SGRCodeAllReset || endCode == AMR_SGRCodeIntensityNormal || + endCode == AMR_SGRCodeIntensityBold || endCode == AMR_SGRCodeIntensityFaint); + case AMR_SGRCodeUnderlineSingle: + case AMR_SGRCodeUnderlineDouble: + return (endCode == AMR_SGRCodeAllReset || endCode == AMR_SGRCodeUnderlineNone || + endCode == AMR_SGRCodeUnderlineSingle || endCode == AMR_SGRCodeUnderlineDouble); + case AMR_SGRCodeNoneOrInvalid: + case AMR_SGRCodeItalicOn: + case AMR_SGRCodeUnderlineNone: + case AMR_SGRCodeIntensityFaint: + case AMR_SGRCodeAllReset: + case AMR_SGRCodeBgReset: + case AMR_SGRCodeFgReset: + return NO; + } + + return NO; +} + + + + +- (NSColor*) colorForSGRCode:(AMR_SGRCode)code +{ + if (self.ansiColors) + { + NSColor *preferredColor = self.ansiColors[@(code)]; + if (preferredColor) + return preferredColor; + } + + switch(code) + { + case AMR_SGRCodeFgBlack: + return kDefaultANSIColorFgBlack; + case AMR_SGRCodeFgRed: + return kDefaultANSIColorFgRed; + case AMR_SGRCodeFgGreen: + return kDefaultANSIColorFgGreen; + case AMR_SGRCodeFgYellow: + return kDefaultANSIColorFgYellow; + case AMR_SGRCodeFgBlue: + return kDefaultANSIColorFgBlue; + case AMR_SGRCodeFgMagenta: + return kDefaultANSIColorFgMagenta; + case AMR_SGRCodeFgCyan: + return kDefaultANSIColorFgCyan; + case AMR_SGRCodeFgWhite: + return kDefaultANSIColorFgWhite; + case AMR_SGRCodeFgBrightBlack: + return kDefaultANSIColorFgBrightBlack; + case AMR_SGRCodeFgBrightRed: + return kDefaultANSIColorFgBrightRed; + case AMR_SGRCodeFgBrightGreen: + return kDefaultANSIColorFgBrightGreen; + case AMR_SGRCodeFgBrightYellow: + return kDefaultANSIColorFgBrightYellow; + case AMR_SGRCodeFgBrightBlue: + return kDefaultANSIColorFgBrightBlue; + case AMR_SGRCodeFgBrightMagenta: + return kDefaultANSIColorFgBrightMagenta; + case AMR_SGRCodeFgBrightCyan: + return kDefaultANSIColorFgBrightCyan; + case AMR_SGRCodeFgBrightWhite: + return kDefaultANSIColorFgBrightWhite; + case AMR_SGRCodeBgBlack: + return kDefaultANSIColorBgBlack; + case AMR_SGRCodeBgRed: + return kDefaultANSIColorBgRed; + case AMR_SGRCodeBgGreen: + return kDefaultANSIColorBgGreen; + case AMR_SGRCodeBgYellow: + return kDefaultANSIColorBgYellow; + case AMR_SGRCodeBgBlue: + return kDefaultANSIColorBgBlue; + case AMR_SGRCodeBgMagenta: + return kDefaultANSIColorBgMagenta; + case AMR_SGRCodeBgCyan: + return kDefaultANSIColorBgCyan; + case AMR_SGRCodeBgWhite: + return kDefaultANSIColorBgWhite; + case AMR_SGRCodeBgBrightBlack: + return kDefaultANSIColorBgBrightBlack; + case AMR_SGRCodeBgBrightRed: + return kDefaultANSIColorBgBrightRed; + case AMR_SGRCodeBgBrightGreen: + return kDefaultANSIColorBgBrightGreen; + case AMR_SGRCodeBgBrightYellow: + return kDefaultANSIColorBgBrightYellow; + case AMR_SGRCodeBgBrightBlue: + return kDefaultANSIColorBgBrightBlue; + case AMR_SGRCodeBgBrightMagenta: + return kDefaultANSIColorBgBrightMagenta; + case AMR_SGRCodeBgBrightCyan: + return kDefaultANSIColorBgBrightCyan; + case AMR_SGRCodeBgBrightWhite: + return kDefaultANSIColorBgBrightWhite; + case AMR_SGRCodeNoneOrInvalid: + case AMR_SGRCodeItalicOn: + case AMR_SGRCodeUnderlineNone: + case AMR_SGRCodeIntensityFaint: + case AMR_SGRCodeAllReset: + case AMR_SGRCodeBgReset: + case AMR_SGRCodeFgReset: + case AMR_SGRCodeIntensityBold: + case AMR_SGRCodeIntensityNormal: + case AMR_SGRCodeUnderlineSingle: + case AMR_SGRCodeUnderlineDouble: + break; + } + + return kDefaultANSIColorFgBlack; +} + + +- (AMR_SGRCode) AMR_SGRCodeForColor:(NSColor*)aColor isForegroundColor:(BOOL)aForeground +{ + if (self.ansiColors) + { + NSArray *codesForGivenColor = [self.ansiColors allKeysForObject:aColor]; + + if (codesForGivenColor != nil && 0 < codesForGivenColor.count) + { + for (NSNumber *thisCode in codesForGivenColor) + { + BOOL thisIsForegroundColor = (thisCode.intValue < 40); + if (aForeground == thisIsForegroundColor) + return thisCode.intValue; + } + } + } + + if (aForeground) + { + if ([aColor isEqual:kDefaultANSIColorFgBlack]) + return AMR_SGRCodeFgBlack; + else if ([aColor isEqual:kDefaultANSIColorFgRed]) + return AMR_SGRCodeFgRed; + else if ([aColor isEqual:kDefaultANSIColorFgGreen]) + return AMR_SGRCodeFgGreen; + else if ([aColor isEqual:kDefaultANSIColorFgYellow]) + return AMR_SGRCodeFgYellow; + else if ([aColor isEqual:kDefaultANSIColorFgBlue]) + return AMR_SGRCodeFgBlue; + else if ([aColor isEqual:kDefaultANSIColorFgMagenta]) + return AMR_SGRCodeFgMagenta; + else if ([aColor isEqual:kDefaultANSIColorFgCyan]) + return AMR_SGRCodeFgCyan; + else if ([aColor isEqual:kDefaultANSIColorFgWhite]) + return AMR_SGRCodeFgWhite; + else if ([aColor isEqual:kDefaultANSIColorFgBrightBlack]) + return AMR_SGRCodeFgBrightBlack; + else if ([aColor isEqual:kDefaultANSIColorFgBrightRed]) + return AMR_SGRCodeFgBrightRed; + else if ([aColor isEqual:kDefaultANSIColorFgBrightGreen]) + return AMR_SGRCodeFgBrightGreen; + else if ([aColor isEqual:kDefaultANSIColorFgBrightYellow]) + return AMR_SGRCodeFgBrightYellow; + else if ([aColor isEqual:kDefaultANSIColorFgBrightBlue]) + return AMR_SGRCodeFgBrightBlue; + else if ([aColor isEqual:kDefaultANSIColorFgBrightMagenta]) + return AMR_SGRCodeFgBrightMagenta; + else if ([aColor isEqual:kDefaultANSIColorFgBrightCyan]) + return AMR_SGRCodeFgBrightCyan; + else if ([aColor isEqual:kDefaultANSIColorFgBrightWhite]) + return AMR_SGRCodeFgBrightWhite; + } + else + { + if ([aColor isEqual:kDefaultANSIColorBgBlack]) + return AMR_SGRCodeBgBlack; + else if ([aColor isEqual:kDefaultANSIColorBgRed]) + return AMR_SGRCodeBgRed; + else if ([aColor isEqual:kDefaultANSIColorBgGreen]) + return AMR_SGRCodeBgGreen; + else if ([aColor isEqual:kDefaultANSIColorBgYellow]) + return AMR_SGRCodeBgYellow; + else if ([aColor isEqual:kDefaultANSIColorBgBlue]) + return AMR_SGRCodeBgBlue; + else if ([aColor isEqual:kDefaultANSIColorBgMagenta]) + return AMR_SGRCodeBgMagenta; + else if ([aColor isEqual:kDefaultANSIColorBgCyan]) + return AMR_SGRCodeBgCyan; + else if ([aColor isEqual:kDefaultANSIColorBgWhite]) + return AMR_SGRCodeBgWhite; + else if ([aColor isEqual:kDefaultANSIColorBgBrightBlack]) + return AMR_SGRCodeBgBrightBlack; + else if ([aColor isEqual:kDefaultANSIColorBgBrightRed]) + return AMR_SGRCodeBgBrightRed; + else if ([aColor isEqual:kDefaultANSIColorBgBrightGreen]) + return AMR_SGRCodeBgBrightGreen; + else if ([aColor isEqual:kDefaultANSIColorBgBrightYellow]) + return AMR_SGRCodeBgBrightYellow; + else if ([aColor isEqual:kDefaultANSIColorBgBrightBlue]) + return AMR_SGRCodeBgBrightBlue; + else if ([aColor isEqual:kDefaultANSIColorBgBrightMagenta]) + return AMR_SGRCodeBgBrightMagenta; + else if ([aColor isEqual:kDefaultANSIColorBgBrightCyan]) + return AMR_SGRCodeBgBrightCyan; + else if ([aColor isEqual:kDefaultANSIColorBgBrightWhite]) + return AMR_SGRCodeBgBrightWhite; + } + + return AMR_SGRCodeNoneOrInvalid; +} + + + +// helper struct typedef and a few functions for +// -closestSGRCodeForColor:isForegroundColor: + +typedef struct { + CGFloat hue; + CGFloat saturation; + CGFloat brightness; +} AMR_HSB; + +AMR_HSB makeHSB(CGFloat hue, CGFloat saturation, CGFloat brightness) +{ + AMR_HSB outHSB; + outHSB.hue = hue; + outHSB.saturation = saturation; + outHSB.brightness = brightness; + return outHSB; +} + +AMR_HSB getHSBFromColor(NSColor *color) +{ + CGFloat hue = 0.0; + CGFloat saturation = 0.0; + CGFloat brightness = 0.0; + [[color colorUsingColorSpaceName:NSCalibratedRGBColorSpace] + getHue:&hue + saturation:&saturation + brightness:&brightness + alpha:NULL + ]; + return makeHSB(hue, saturation, brightness); +} + +BOOL floatsEqual(CGFloat first, CGFloat second, CGFloat maxAbsError) +{ + return (fabs(first-second)) < maxAbsError; +} + +#define MAX_HUE_FLOAT_EQUALITY_ABS_ERROR 0.000001 + +- (AMR_SGRCode) closestSGRCodeForColor:(NSColor *)color isForegroundColor:(BOOL)foreground +{ + if (color == nil) + return AMR_SGRCodeNoneOrInvalid; + + AMR_SGRCode closestColorSGRCode = [self AMR_SGRCodeForColor:color isForegroundColor:foreground]; + if (closestColorSGRCode != AMR_SGRCodeNoneOrInvalid) + return closestColorSGRCode; + + AMR_HSB givenColorHSB = getHSBFromColor(color); + + CGFloat closestColorHueDiff = FLT_MAX; + CGFloat closestColorSaturationDiff = FLT_MAX; + CGFloat closestColorBrightnessDiff = FLT_MAX; + + // (background SGR codes are +10 from foreground ones:) + NSUInteger AMR_SGRCodeShift = (foreground)?0:10; + NSArray *ansiFgColorCodes = @[ + @(AMR_SGRCodeFgBlack+AMR_SGRCodeShift), + @(AMR_SGRCodeFgRed+AMR_SGRCodeShift), + @(AMR_SGRCodeFgGreen+AMR_SGRCodeShift), + @(AMR_SGRCodeFgYellow+AMR_SGRCodeShift), + @(AMR_SGRCodeFgBlue+AMR_SGRCodeShift), + @(AMR_SGRCodeFgMagenta+AMR_SGRCodeShift), + @(AMR_SGRCodeFgCyan+AMR_SGRCodeShift), + @(AMR_SGRCodeFgWhite+AMR_SGRCodeShift), + @(AMR_SGRCodeFgBrightBlack+AMR_SGRCodeShift), + @(AMR_SGRCodeFgBrightRed+AMR_SGRCodeShift), + @(AMR_SGRCodeFgBrightGreen+AMR_SGRCodeShift), + @(AMR_SGRCodeFgBrightYellow+AMR_SGRCodeShift), + @(AMR_SGRCodeFgBrightBlue+AMR_SGRCodeShift), + @(AMR_SGRCodeFgBrightMagenta+AMR_SGRCodeShift), + @(AMR_SGRCodeFgBrightCyan+AMR_SGRCodeShift), + @(AMR_SGRCodeFgBrightWhite+AMR_SGRCodeShift), + ]; + for (NSNumber *thisSGRCodeNumber in ansiFgColorCodes) + { + AMR_SGRCode thisSGRCode = thisSGRCodeNumber.intValue; + NSColor *thisColor = [self colorForSGRCode:thisSGRCode]; + + AMR_HSB thisColorHSB = getHSBFromColor(thisColor); + + CGFloat hueDiff = fabs(givenColorHSB.hue - thisColorHSB.hue); + CGFloat saturationDiff = fabs(givenColorHSB.saturation - thisColorHSB.saturation); + CGFloat brightnessDiff = fabs(givenColorHSB.brightness - thisColorHSB.brightness); + + // comparison depends on hue, saturation and brightness + // (strictly in that order): + + if (!floatsEqual(hueDiff, closestColorHueDiff, MAX_HUE_FLOAT_EQUALITY_ABS_ERROR)) + { + if (hueDiff > closestColorHueDiff) + continue; + closestColorSGRCode = thisSGRCode; + closestColorHueDiff = hueDiff; + closestColorSaturationDiff = saturationDiff; + closestColorBrightnessDiff = brightnessDiff; + continue; + } + + if (!floatsEqual(saturationDiff, closestColorSaturationDiff, MAX_HUE_FLOAT_EQUALITY_ABS_ERROR)) + { + if (saturationDiff > closestColorSaturationDiff) + continue; + closestColorSGRCode = thisSGRCode; + closestColorHueDiff = hueDiff; + closestColorSaturationDiff = saturationDiff; + closestColorBrightnessDiff = brightnessDiff; + continue; + } + + if (!floatsEqual(brightnessDiff, closestColorBrightnessDiff, MAX_HUE_FLOAT_EQUALITY_ABS_ERROR)) + { + if (brightnessDiff > closestColorBrightnessDiff) + continue; + closestColorSGRCode = thisSGRCode; + closestColorHueDiff = hueDiff; + closestColorSaturationDiff = saturationDiff; + closestColorBrightnessDiff = brightnessDiff; + continue; + } + + // If hue (especially hue!), saturation and brightness diffs all + // are equal to some other color, we need to prefer one or the + // other so we'll select the more 'distinctive' color of the + // two (this is *very* subjective, obviously). I basically just + // looked at the hue chart, went through all the points between + // our main ANSI colors and decided which side the middle point + // would lean on. (e.g. the purple color that is exactly between + // the blue and magenta ANSI colors looks more magenta than + // blue to me so I put magenta higher than blue in the list + // below.) + // + // subjective ordering of colors from most to least 'distinctive': + long colorDistinctivenessOrder[6] = { + AMR_SGRCodeFgRed+AMR_SGRCodeShift, + AMR_SGRCodeFgMagenta+AMR_SGRCodeShift, + AMR_SGRCodeFgBlue+AMR_SGRCodeShift, + AMR_SGRCodeFgGreen+AMR_SGRCodeShift, + AMR_SGRCodeFgCyan+AMR_SGRCodeShift, + AMR_SGRCodeFgYellow+AMR_SGRCodeShift + }; + for (int i = 0; i < 6; i++) + { + if (colorDistinctivenessOrder[i] == closestColorSGRCode) + break; + else if (colorDistinctivenessOrder[i] == thisSGRCode) + { + closestColorSGRCode = thisSGRCode; + closestColorHueDiff = hueDiff; + closestColorSaturationDiff = saturationDiff; + closestColorBrightnessDiff = brightnessDiff; + } + } + } + + return closestColorSGRCode; +} + + + +@end diff --git a/MTMR/CBridge/TouchBarPrivateApi-Bridging.h b/MTMR/CBridge/TouchBarPrivateApi-Bridging.h index ad4c8ff..039aff5 100644 --- a/MTMR/CBridge/TouchBarPrivateApi-Bridging.h +++ b/MTMR/CBridge/TouchBarPrivateApi-Bridging.h @@ -6,6 +6,7 @@ // Copyright © 2018 Anton Palgunov. All rights reserved. // +#import "AMR_ANSIEscapeHelper.h" #import "TouchBarPrivateApi.h" #import "TouchBarSupport.h" #import "DeprecatedCarbonAPI.h" diff --git a/MTMR/CustomButtonTouchBarItem.swift b/MTMR/CustomButtonTouchBarItem.swift index c6aa049..238d0cd 100644 --- a/MTMR/CustomButtonTouchBarItem.swift +++ b/MTMR/CustomButtonTouchBarItem.swift @@ -85,6 +85,7 @@ class CustomButtonTouchBarItem: NSCustomTouchBarItem, NSGestureRecognizerDelegat if let color = backgroundColor { cell.isBordered = true button.bezelColor = color + button.bezelStyle = .rounded cell.backgroundColor = color } else { button.isBordered = isBordered @@ -157,6 +158,10 @@ class CustomButtonCell: NSButtonCell { } } } + + override func drawingRect(forBounds rect: NSRect) -> NSRect { + return rect // need that so content may better fit in button with very limited width + } required init(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") diff --git a/MTMR/ItemsParsing.swift b/MTMR/ItemsParsing.swift index 6591b40..0861aca 100644 --- a/MTMR/ItemsParsing.swift +++ b/MTMR/ItemsParsing.swift @@ -343,6 +343,7 @@ class SupportedTypesHolder { enum ItemType: Decodable { case staticButton(title: String) case appleScriptTitledButton(source: SourceProtocol, refreshInterval: Double) + case shellScriptTitledButton(source: SourceProtocol, refreshInterval: Double) case timeButton(formatTemplate: String, timeZone: String?) case battery() case dock(autoResize: Bool) @@ -387,6 +388,7 @@ enum ItemType: Decodable { enum ItemTypeRaw: String, Decodable { case staticButton case appleScriptTitledButton + case shellScriptTitledButton case timeButton case battery case dock @@ -413,6 +415,11 @@ enum ItemType: Decodable { 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 .shellScriptTitledButton: + let source = try container.decode(Source.self, forKey: .source) + let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 1800.0 + self = .shellScriptTitledButton(source: source, refreshInterval: interval) case .staticButton: let title = try container.decode(String.self, forKey: .title) diff --git a/MTMR/ShellScriptTouchBarItem.swift b/MTMR/ShellScriptTouchBarItem.swift new file mode 100644 index 0000000..01dca4b --- /dev/null +++ b/MTMR/ShellScriptTouchBarItem.swift @@ -0,0 +1,74 @@ +// +// ShellScriptTouchBarItem.swift +// MTMR +// +// Created by bobr on 08/08/2019. +// Copyright © 2019 Anton Palgunov. All rights reserved. +// +import Foundation + +class ShellScriptTouchBarItem: CustomButtonTouchBarItem { + private let interval: TimeInterval + private let source: String + private var forceHideConstraint: NSLayoutConstraint! + + init?(identifier: NSTouchBarItem.Identifier, source: SourceProtocol, interval: TimeInterval) { + self.interval = interval + self.source = source.string ?? "echo No \"source\"" + super.init(identifier: identifier, title: "⏳") + + forceHideConstraint = view.widthAnchor.constraint(equalToConstant: 0) + + DispatchQueue.shellScriptQueue.async { + self.refreshAndSchedule() + } + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func refreshAndSchedule() { + // Execute script and get result + let scriptResult = execute(source) + + // Apply returned text attributes (if they were returned) to our result string + let helper = AMR_ANSIEscapeHelper.init() + helper.defaultStringColor = NSColor.white + helper.font = "1".defaultTouchbarAttributedString.attribute(.font, at: 0, effectiveRange: nil) as? NSFont + let title = NSMutableAttributedString.init(attributedString: helper.attributedString(withANSIEscapedString: scriptResult) ?? NSAttributedString(string: "")) + title.addAttributes([.baselineOffset: 1], range: NSRange(location: 0, length: title.length)) + let newBackgoundColor = title.attribute(.backgroundColor, at: 0, effectiveRange: nil) as? NSColor + + // Update UI + DispatchQueue.main.async { [weak self, newBackgoundColor] in + self?.backgroundColor = newBackgoundColor + self?.attributedTitle = title + self?.forceHideConstraint.isActive = scriptResult == "" + } + + // Schedule next update + DispatchQueue.shellScriptQueue.asyncAfter(deadline: .now() + interval) { [weak self] in + self?.refreshAndSchedule() + } + } + + func execute(_ command: String) -> String { + let task = Process() + task.launchPath = "/bin/bash" + task.arguments = ["-c", command] + + let pipe = Pipe() + task.standardOutput = pipe + task.launch() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + let output: String = NSString(data: data, encoding: String.Encoding.utf8.rawValue) as String? ?? "" + + return output.replacingOccurrences(of: "\\n+$", with: "", options: .regularExpression) + } +} + +extension DispatchQueue { + static let shellScriptQueue = DispatchQueue(label: "mtmr.shellscript") +} diff --git a/MTMR/TouchBarController.swift b/MTMR/TouchBarController.swift index 09b79d4..944fc7a 100644 --- a/MTMR/TouchBarController.swift +++ b/MTMR/TouchBarController.swift @@ -23,6 +23,8 @@ extension ItemType { return "com.toxblh.mtmr.staticButton." case .appleScriptTitledButton(source: _): return "com.toxblh.mtmr.appleScriptButton." + case .shellScriptTitledButton(source: _): + return "com.toxblh.mtmr.shellScriptButton." case .timeButton(formatTemplate: _, timeZone: _): return "com.toxblh.mtmr.timeButton." case .battery(): @@ -251,6 +253,8 @@ class TouchBarController: NSObject, NSTouchBarDelegate { barItem = CustomButtonTouchBarItem(identifier: identifier, title: title) case let .appleScriptTitledButton(source: source, refreshInterval: interval): barItem = AppleScriptTouchBarItem(identifier: identifier, source: source, interval: interval) + case let .shellScriptTitledButton(source: source, refreshInterval: interval): + barItem = ShellScriptTouchBarItem(identifier: identifier, source: source, interval: interval) case let .timeButton(formatTemplate: template, timeZone: timeZone): barItem = TimeTouchBarItem(identifier: identifier, formatTemplate: template, timeZone: timeZone) case .battery(): diff --git a/MTMR/Widgets/YandexWeatherBarItem.swift b/MTMR/Widgets/YandexWeatherBarItem.swift index 6df2149..dd5f933 100644 --- a/MTMR/Widgets/YandexWeatherBarItem.swift +++ b/MTMR/Widgets/YandexWeatherBarItem.swift @@ -12,7 +12,7 @@ import CoreLocation class YandexWeatherBarItem: CustomButtonTouchBarItem, CLLocationManagerDelegate { private let activity: NSBackgroundActivityScheduler private let unitsStr = "°C" - private let iconsSource = ["Ясно": "☀️", "Малооблачно": "🌤", "Облачно с прояснениями": "⛅️", "Пасмурно": "☁️", "Небольшой дождь": "🌦", "Дождь": "🌧", "Ливень": "⛈", "Гроза": "🌩", "Небольшой снег": "❄️", "Снег": "🌨", "Туман": "🌫"] + private let iconsSource = ["Ясно": "☀️", "Малооблачно": "🌤", "Облачно с прояснениями": "⛅️", "Пасмурно": "☁️", "Небольшой дождь": "🌦", "Дождь": "🌧", "Ливень": "⛈", "Гроза": "🌩", "Дождь со снегом": "☔️", "Небольшой снег": "❄️", "Снег": "🌨", "Туман": "🌫"] private var location: CLLocation! private var prevLocation: CLLocation! private var manager: CLLocationManager! diff --git a/README.md b/README.md index c7aa98a..095979d 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,13 @@ The pre-installed configuration contains less or more than you'll probably want, - sleep - displaySleep +> Custom buttons + +- staticButton +- appleScriptTitledButton +- shellScriptTitledButton +- timeButton + ## Gestures on central part: - two finger slide: change you Volume @@ -110,16 +117,17 @@ The pre-installed configuration contains less or more than you'll probably want, ### You can also make custom buttons using these types -- `staticButton` +#### `staticButton` ```json "type": "staticButton", "title": "esc", ``` -- `appleScriptTitledButton` +#### `appleScriptTitledButton` ```js + { "type": "appleScriptTitledButton", "refreshInterval": 60, //optional "source": { @@ -128,26 +136,58 @@ The pre-installed configuration contains less or more than you'll probably want, "inline": "tell application \"Finder\"\rmake new Finder window\rset target of front window to path to home folder as string\ractivate\rend tell", // or "base64": "StringInbase64" - }, + } + } ``` -- `timeButton` +#### `shellScriptTitledButton` +> Note: script may return also colors using escape sequences (read more here https://misc.flogisoft.com/bash/tip_colors_and_formatting) +> Only "16 Colors" mode supported atm. If background color returned, button will pick it up as own background color. + +Example of "CPU load" button which also changes color based on load value. +```js +{ + "type": "shellScriptTitledButton", + "width": 80, + "refreshInterval": 2, + "source": { + "inline": "top -l 2 -n 0 -F | egrep -o ' \\d*\\.\\d+% idle' | tail -1 | awk -F% '{p = 100 - $1; if (p > 30) c = \"\\033[33m\"; if (p > 70) c = \"\\033[30;43m\"; printf \"%s%4.1f%%\\n\", c, p}'" + }, + "action": "appleScript", + "actionAppleScript": { + "inline": "activate application \"Activity Monitor\"\rtell application \"System Events\"\r\ttell process \"Activity Monitor\"\r\t\ttell radio button \"CPU\" of radio group 1 of group 2 of toolbar 1 of window 1 to perform action \"AXPress\"\r\tend tell\rend tell" + }, + "align": "right", + "image": { + "base64": + "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAA/1BMVEUAAADaACbYACfYACfjABzXACjYACfXACjYACfYACfYACfYACfdACLYACfXACjYACfVACv/AADXACjYACfYACfXACjYACfXACjaACXYACfYACfVACvYACfYACfZACbZACbYACfYACfZACb/AADYACfYACfVACrXACjVACu/AEDYACfYACfYACfXACjXACjYACfXACjYACfYACfYACfXACjYACfXACjYACfYACfZACbYACfYACfMADPYACfYACfYACfYACfYACfZACbXACjYACfYACfRAC7XACjYACfZACbWACnXACjXACjYACfTACzZACb/AADYACfYACfYACcAAAA+zneGAAAAU3RSTlMAItK+CVPjh3xUxPwPiGDQGAMtSKmN3Vk+wPQG/e26oIJBnwJCdiuAHgTmw+6BX+IgfaqLUvKOW8VKnagK+vBwYrhlc/urCznvhSyUbOEXPAFjGh/ektAAAAABYktHRACIBR1IAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH4ggWETQWgEDcSgAAAqVJREFUWMPtl4ly2jAQhsUNNlcw5r4SICEHLSQhCQRyX73T/u//LpUlLIyxbMAznWmn/0ywo5U+27tr7ZoQuwLBUJidRKIxPhKLRtgxHAoGiLfiQIKdKFCTxjGpQmEDCSC+BiAFpNlJBsgaxyyQYQNpIPUf8AcAOzktD+iaoQJQNI5FoMAGdCCv5XZclpfKFXiqUi5Jllf1mvdyQzW96gigd4h6o+mhRp1O0x3vvwa1VSWeqrZU1Jyeogy01ggSVQsoO/i/gjq9/u6u+2LDXq2jshqLHNCgdsCVwO0NILdi0oDmuoAmoImhQDzFRPNnb36L7U43NVfc2EH2D9h5t9OePyIF5IU9uIhvkyN7iiXmQUIOj8x/lB6f0bTaQ3ZA+9iaNCH2Lpg6btsBIRJOpJl0E9ABTvof5kqEGeCjMaN/AnRMgM5XJcI2J1J1gf6S48Tb2Ae6JkAjdgmAeJ1XAOJ1Xg8wGJ6elXwAzkeGjy62BgxG3MuXnoCIkmEq8EQyAUPgajyhPxJAga9SIiRqzwMOuAbGZDrDjQRgKkpiqiPgFphM74B7d4BKy2cyy1RcBvSodUb/HiSAIl+VlEfh8cm4wvPL9nnw+gbc+kkkUVioO95etwe8PBuP8vQoBzg7UQAe5t7syZwoCaMA3AN30wlzh3MYJYkkADeYTckYuJYlkiSVBeCKZtSY/gxlqezlxEt+pdFg6zBesPXn1ih8Aj5vkAels9PhYCkPsl++kg0AQu4dyuqmugIQm+qS5Nv6N+D7wm7d1skPc4xu666Fhd6BxU6r+jub8tNaWNxK29EhsdpR/sVn7FlLm0txPdgni+JrFNd3p+K67MQtyrsp3w2G7xbHd5Plv83z3Wj6b3V9N9ssFv7afaa//ZPn3wD4/vje8PP/N7TebS0hgZhEAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE4LTA4LTIyVDE3OjUyOjIyKzAyOjAwc2qUYAAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxOC0wOC0yMlQxNzo1MjoyMiswMjowMAI3LNwAAAAZdEVYdFNvZnR3YXJlAHd3dy5pbmtzY2FwZS5vcmeb7jwaAAAAAElFTkSuQmCC" + }, + "bordered": false +} +``` + +#### `timeButton` ```js +{ "type": "timeButton", "formatTemplate": "HH:mm" //optional +} ``` ## Groups ```js { -"type": "group", -"align": "center", -"bordered": true, -"title": "stats", -"items": [ - { "type": "play" }, { "type": "mute" }, ...] + "type": "group", + "align": "center", + "bordered": true, + "title": "stats", + "items": [ + { "type": "play" }, + { "type": "mute" }, + ... + ] } ``` @@ -155,8 +195,8 @@ To close a group, use the button: ``` { -"type": "close", -"width": 64 + "type": "close", + "width": 64 }, ```