Beyond Scripting in Swift: When xattrs are not like files

When I was last working on my extended attributed editor xattred, I had decided to give it a “more friendly and powerful interface”. Before doing so, I needed to work out how that was going to happen.

The consistent and logical approach would be to override NSDocument’s functions to tailor it to working with metadata rather than data. Then the Open command would open the xattrs for the selected file or folder, Save would save them, and so on. As I looked at doing that, it quickly became clear that NSDocument is very strongly attached to the data in data forks of files. Even trying to get it to work with folders as documents looked quite a major feat.

One problem with trying to perform such major surgery on a large class like NSDocument is that its source is not open to view. Apple’s first class library to support object-oriented development, MacApp, was provided in source. If ever you wanted to see how it did something, all you had to do was look at that source. It’s a great shame and an obstacle to development that Apple no longer does this.

So my task now was to clean up the existing commands so that you wouldn’t have to keep selecting the file or folder on which to perform button actions, and to introduce an NSTableView which would list all the xattrs for the selected file/folder.

xattred26

For the latter, this required adding the two key classes to my ViewController:
class ViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate {

I then added some variables to contain key properties, including a dictionary of xattrs to be built when opening them for a file/folder:
var theFilename = ""
var theSourcePath = ""
var theSourceURL = URL.init(string: "")
var theXattrList: [String: Data] = [:]

NSTableViews are data-based. Although I will change this as I develop the functions for xattrs in that list, for a start I made two simple arrays to contain the information to be shown in the NSTableView:
var theXattrKeys: [String] = []
var theXattrSizes: [String] = []

The plan is that, until the user has clicked on the Open (formerly Inspect) button, other actions will be disabled. Clicking Open will open the selected file/folder and read in its xattrs. This required modifications to viewDidLoad():
override func viewDidLoad() {
super.viewDidLoad()

First I need to attach the code to the NSTableView xattrTable:
xattrTable.dataSource = self
xattrTable.delegate = self

Then disable the other buttons until a file/folder has been Opened:
if (theSourceURL == nil) {
addQuarButton.isEnabled = false
addXattrButton.isEnabled = false
removeXattrButton.isEnabled = false
}
}

There are two common functions to each of the buttons which I need to factor. The first is obtaining the xattrs for the selected file/folder from its theXattrList, and populating arrays. This returns the formatted String listing the xattrs, ready to display in the lower scrolling text box:
func getXattrList() -> String {
var theOutStr = ""
theXattrKeys = []
theXattrSizes = []
if (theXattrList.count > 0) {
for (xattrName, xattrData) in theXattrList {
theXattrKeys.append(xattrName)
theXattrSizes.append(xattrData.description)
theOutStr = theOutStr + xattrName + "\n"
if let string2 = String.init(data: xattrData, encoding: .utf8) {
theOutStr = theOutStr + "\"" + string2 + "\"\n"
} else {
let data2 = xattrData as NSData
let string1 = data2.description
theOutStr = theOutStr + string1 + "\n"
if let string3 = String.init(data: xattrData, encoding: .ascii) {
theOutStr = theOutStr + "«" + string3 + "»\n"
}
}
theOutStr = theOutStr + "\n"
}
}
return theOutStr
}

The other calls that, and sets the contents of the text box, as an attributed string:
func updateOutputText() {
var theOutStr = ""
theOutStr = getXattrList()
let attr = NSAttributedString(string: theOutStr)
outputTextcontent.textStorage?.setAttributedString(attr)
xattrTable.reloadData()
}

xattred27

So, for example, the button to add a xattr now calls this simple action:
@IBAction func addXattrButton(_ sender: Any) {
if (theSourceURL != nil) {
let theXattrName = self.xattrTextbox.stringValue
if (theXattrName != "") {
let theXattrString = self.outputTextcontent.textStorage?.string
let theXattrData = theXattrString?.data(using: String.Encoding.utf8)
do {
try self.theSourceURL?.setExtendedAttribute(data: theXattrData!, forName: theXattrName)
self.theXattrList[theXattrName] = theXattrData
self.updateOutputText()
} catch let error {
doErrorAlertModal(message: ("setxattr error: \(error.localizedDescription)"))
}
}
}
}

xattred28

This takes advantage of a useful common property between xattrs and dictionaries: setting an existing xattr with new content overwrites the old content, and setting an existing dictionary entry using
theXattrList[theXattrName] = theXattrData
also overwrites the existing value.

This cleans up xattred very effectively. However, there are two key functions which need to be added to support the NSTableView, which I have set to be Cell Based in its Content Mode (in Interface Builder’s Attributes Inspector). The first returns the number of rows to be populated in the NSTableView, which is simply the number of xattrs we have read in:
func numberOfRows(in tableView: NSTableView) -> Int {
return theXattrKeys.count
}

The other function returns the data to be displayed in each cell in the table. As I have assigned the identifier "xattrname" to the column giving xattr names, this is quickly done with
func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
if (tableColumn?.identifier == "xattrname") {
return theXattrKeys[row]
} else {
return theXattrSizes[row]
}
}

xattred29

All the NSTableView currently supports at this stage is display of the xattr information. To enable functions such as copying and pasting xattrs from it, I’m going to need to make it more sophisticated, but this is a quick hacked start.

When I first implemented this, I was baffled that everything seemed to work properly, but changes were never reflected in the NSTableView. The fix was simple: each of the actions which changed the contents of that view needed to update the view, using updateOutputText(). Then it all magically worked.

The next version needs to start doing more useful things with its NSTableView, which is clearly where my attentions will be focussed next time.