Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift 3: Set Finder label color

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
}
like image 665
Peter Silie Avatar asked Sep 27 '16 08:09

Peter Silie


2 Answers

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)")
}
like image 156
mz2 Avatar answered Sep 18 '22 13:09

mz2


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.

like image 22
Eric Aya Avatar answered Sep 20 '22 13:09

Eric Aya