I'm trying to set the colored labels shown by the finder. The only function I know is setResourceValue. But this needs localized names!
I could image my mother language and english as well, but all others I don't know. I can't believe, that this should be the way.
Is the are translation function, which takes a standard parameter like an enum or int and delivers the localized color name?
I have an running part, but only for two languages (German and English):
let colorNamesEN = [ "None", "Gray", "Green", "Purple", "Blue", "Yellow", "Red", "Orange" ]
let colorNamesDE = [ "", "Grau", "Grün", "Lila", "Blau", "Gelb", "Rot", "Orange" ]
public enum TagColors : Int8 {
case None = -1, Gray, Green, Purple, Blue, Yellow, Red, Orange, Max
}
//let theURL : NSURL = NSURL.fileURLWithPath("/Users/dirk/Documents/MyLOG.txt")
extension NSURL {
// e.g. theURL.setColors(0b01010101)
func tagColorValue(tagcolor : TagColors) -> UInt16 {
return 1 << UInt16(tagcolor.rawValue)
}
func addTagColor(tagcolor : TagColors) -> Bool {
let bits : UInt16 = tagColorValue(tagcolor) | self.getTagColors()
return setTagColors(bits)
}
func remTagColor(tagcolor : TagColors) -> Bool {
let bits : UInt16 = ~tagColorValue(tagcolor) & self.getTagColors()
return setTagColors(bits)
}
func setColors(tagcolor : TagColors) -> Bool {
let bits : UInt16 = tagColorValue(tagcolor)
return setTagColors(bits)
}
func setTagColors(colorMask : UInt16) -> Bool {
// get string for all available and requested bits
let arr = colorBitsToStrings(colorMask & (tagColorValue(TagColors.Max)-1))
do {
try self.setResourceValue(arr, forKey: NSURLTagNamesKey)
return true
}
catch {
print("Could not write to file \(self.absoluteURL)")
return false
}
}
func getTagColors() -> UInt16 {
return getAllTagColors(self.absoluteURL)
}
}
// let initialBits: UInt8 = 0b00001111
func colorBitsToStrings(colorMask : UInt16) -> NSArray {
// translate bits to (localized!) color names
let countryCode = NSLocale.currentLocale().objectForKey(NSLocaleLanguageCode)!
// I don't know how to automate it for all languages possible!!!!
let colorNames = countryCode as! String == "de" ? colorNamesDE : colorNamesEN
var tagArray = [String]()
var bitNumber : Int = -1 // ignore first loop
for colorName in colorNames {
if bitNumber >= 0 {
if colorMask & UInt16(1<<bitNumber) > 0 {
tagArray.append(colorName)
}
}
bitNumber += 1
}
return tagArray
}
func getAllTagColors(file : NSURL) -> UInt16 {
var colorMask : UInt16 = 0
// translate (localized!) color names to bits
let countryCode = NSLocale.currentLocale().objectForKey(NSLocaleLanguageCode)!
// I don't know how to automate it for all languages possible!!!!
let colorNames = countryCode as! String == "de" ? colorNamesDE : colorNamesEN
var bitNumber : Int = -1 // ignore first loop
var tags : AnyObject?
do {
try file.getResourceValue(&tags, forKey: NSURLTagNamesKey)
if tags != nil {
let tagArray = tags as! [String]
for colorName in colorNames {
if bitNumber >= 0 {
// color name listed?
if tagArray.filter( { $0 == colorName } ).count > 0 {
colorMask |= UInt16(1<<bitNumber)
}
}
bitNumber += 1
}
}
} catch {
// process the error here
}
return colorMask
}
To set a single color, the setResourceValue
API call is indeed what you should use. However, the resource key you should use is NSURLLabelNumberKey
, or URLResourceKey.labelNumberKey
in Swift 3 (not NSURLTagNamesKey
):
enum LabelNumber: Int {
case none
case grey
case green
case purple
case blue
case yellow
case red
case orange
}
do {
// casting to NSURL here as the equivalent API in the URL value type appears borked:
// setResourceValue(_, forKey:) is not available there,
// and setResourceValues(URLResourceValues) appears broken at least as of Xcode 8.1…
// fix-it for setResourceValues(URLResourceValues) is saying to use [URLResourceKey: AnyObject],
// and the dictionary equivalent also gives an opposite compiler error. Looks like an SDK / compiler bug.
try (fileURL as NSURL).setResourceValue(LabelNumber.purple.rawValue, forKey: .labelNumberKey)
}
catch {
print("Error when setting the label number: \(error)")
}
(This is a Swift 3 port of an answer to a related Objective-C question. Tested with Xcode 8.1, macOS Sierra 10.12.1)
To set multiple colors, you can either use the API you've used with setting resource values with the label key. The distinction between these two encodings is described here: http://arstechnica.com/apple/2013/10/os-x-10-9/9/ – basically the label key is internally setting the extended attribute "com.apple.metadata:_kMDItemUserTags" which stores an array of those label strings as a binary plist, whereas the single colour option shown above is setting the 10th byte of 32 byte long extended attribute value "com.apple.FinderInfo".
The "localized" in that key name is a bit confusing in the sense that what is actually being set with it is the set of labels chosen by the user, amongst the label names set by the user. Those label values are indeed localized, but only to the extent where they are set according to the localisation setting when you initially created your account. To demonstrate, these are the label values used by Finder on my system, which I'd set to Finnish localization as a test and restarted Finder, rebooted machine etc:
➜ defaults read com.apple.Finder FavoriteTagNames
(
"",
Red,
Orange,
Yellow,
Green,
Blue,
Purple,
Gray
)
The way the data is encoded in that binary plist value is simply the favourite tag name followed by its index in the array (which is fixed to be of length 8, with actual values starting from 1, i.e. matching the seven colours in the order Red, Orange, Yellow, Green, Blue, Purple, Gray). For example:
xattr -p com.apple.metadata:_kMDItemUserTags foobar.png | xxd -r -p | plutil -convert xml1 - -o -
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
<string>Gray
1</string>
<string>Purple
3</string>
<string>Green
2</string>
<string>Red
6</string>
</array>
</plist>
So, the system localisation is not taken into account, and in fact setting the tag with any string followed by a linefeed, followed by a number between 1–7 will show up in Finder with the colour indicated by the tag's index. However, to know the correct current values to apply to get the tags to be applied from the set of favorite tags (such that both colour and the label match up) you would need to read that key from Finder preferences (key 'FavoriteTagNames' from domain 'com.apple.Finder' which encodes an array of those favourite tag names as shown above).
Ignoring the above complication in case you want to get the label name and colour correct, requiring reading from Finder preferences domain (which you may or may not be able to do, depending on whether your app is sandboxed or not), should you wish to use multiple colours, here's an example solution that sets the colour using extended attribute values directly (I used SOExtendedAttributes to avoid having to touch the unwieldy xattr C APIs):
enum LabelNumber: Int {
case none
case gray
case green
case purple
case blue
case yellow
case red
case orange
// using an enum here is really for illustrative purposes:
// to know the correct values to apply you would need to read Finder preferences (see body of my response for more detail).
var label:String? {
switch self {
case .none: return nil
case .gray: return "Gray\n1"
case .green: return "Green\n2"
case .purple: return "Purple\n3"
case .blue: return "Blue\n4"
case .yellow: return "Yellow\n5"
case .red: return "Red\n6"
case .orange: return "Orange\n7"
}
}
static func propertyListData(labels: [LabelNumber]) throws -> Data {
let labelStrings = labels.flatMap { $0.label }
let propData = try! PropertyListSerialization.data(fromPropertyList: labelStrings,
format: PropertyListSerialization.PropertyListFormat.binary,
options: 0)
return propData
}
}
do {
try (fileURL as NSURL).setExtendedAttributeData(LabelNumber.propertyListData(labels: [.gray, .green]),
name: "com.apple.metadata:_kMDItemUserTags")
}
catch {
print("Error when setting the label number: \(error)")
}
I got it working without having to know the color name, thanks to the new URLResourceValues()
struct and the tag numbers.
Knowing that each of these tag numbers represents a tag color:
0 None
1 Grey
2 Green
3 Purple
4 Blue
5 Yellow
6 Red
7 Orange
Make a URL of your file:
var url = URL(fileURLWithPath: pathToYourFile)
It has to be a var because we are going to mutate it.
Create a new URLResourceValues
instance (also needs to be a variable):
var rv = URLResourceValues()
Set the label number like this:
rv.labelNumber = 2 // green
Finally, write the tag to the file:
do {
try url.setResourceValues(rv)
} catch {
print(error.localizedDescription)
}
In our example we've set the number tag to 2 so now this file is labeled with green color.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With