More fun scripting with Swift and Xcode: Radio buttons and more

I have at last found a little time to work on Consolation, my Swift 3 implementation of what LogLogger does using AppleScript. Given that Apple shows no sign of adding features to Sierra’s Console app to browse previous logs meaningfully, some of us need this facility. Having come close to the limits of AppleScript, it’s time to try to do the job properly using Swift.

In my previous explorations leading to the beginnings of Consolation, I had discovered how to build the command and arguments needed to call the log command successfully. One of the critical factors there is understanding how to pass the arguments. log is a complex command, with verbs, predicates, and regular options. A typical command line in Terminal might look like
log show --predicate 'subsystem == "com.apple.TimeMachine"' --style syslog --info --last 2h | cut -c 1-22,43-999 > /Users/hoakley/Documents/0newDownloads/LogLoggerOut00.text

The first problems arise from the fact that, when we call this process in Swift, there is no shell environment to handle pipes and the like: we have to handle those in the app. So that shrinks to
log show --predicate 'subsystem == "com.apple.TimeMachine"' --style syslog --info --last 2h

When this is called using Swift 3, it is broken down thus:
let task = Process()
task.launchPath = "/usr/bin/log"
task.arguments = theFullCmdStr
let outPipe = Pipe()
task.standardOutput = outPipe
task.launch()
let fileHandle = outPipe.fileHandleForReading
let data = fileHandle.readDataToEndOfFile()
task.waitUntilExit()

task.launchPath is set to the full path to the command tool itself, and task.arguments is set to an array of strings which together make up the rest of the command. Throw it a single string with everything concatenated into it, and all you’ll see is an error. That string has to be divided up into sub-strings in just the right way.

Building that array of strings is the purpose of the controls in the interface. So my first task was to implement the controls which are provided in LogLogger.

Radio Buttons

Most of the controls that I needed – popup menus, checkboxes, text boxes, buttons – are now quite familiar and very straightforward in Xcode with Swift. The new control which I needed to get to grips with was the (grouped) radio button. As in other interface builders, placing individual radio buttons is very simple, and they’re easy to handle on their own. What I had to learn was how to group them, and work with the group.

So, having placed several radio buttons, the next task is to group them into a single Action. The way to do this is to select the first button and Control-drag from it to your source to make that first connection. This must be strongly typed to an NSButton sender, with a function name which will apply to the group as a whole, such as filterRadioButton to apply to all three of the filter options.

You then select the next radio button in the group, and Control-drag a connection from it to the same Action. Instead of seeing a blue insertion line in the source code, you’ll then see a shaded box, which indicates that you’re on target. Repeat that with each of the remaining radio buttons in the group, and they’re grouped. Run your app and you will now see they behave properly as a group.

consolscript11

I was impressed at this, but unfortunately there is some additional coding which needs to be performed, and you also need to add each individual radio button as an Outlet, and set their tags.

consolscript12

The last of these is simple: in each button’s attributes is a tag, set by default to 0. You need to number them, setting the tags to 1, 2 … Adding them as Outlets is tedious but straightforward.

The code that is required is not difficult, but makes the whole procedure just a little more kludgy than it might otherwise be. First, add a variable to contain the index of the button selected with a group:
var selectedFilterButton = 1
at the top level of the ViewController.

Then, add a line to set that variable in its appropriate Action function:
@IBAction func filterRadioButton(_ sender: NSButton) {
selectedFilterButton = sender.tag
}

Then you can test which of the radio buttons in a group is selected using code such as
if (selectedFilterButton == 1)
and you can set the selected radio button using
filterTMbutton.state = NSOnState

Big Popups

One of the immediate advantages to working in Swift in this particular case is that it is now easy to build substantial popup menus, which support more features of the filter predicates. It is just a matter of adding them to the array of strings used for each menu. Consolation will therefore give almost complete access to the power of these filters, which is pleasing.

LogLogger, for example, limits the operators quite severely. The current list offered in Consolation is:
"==", "==[c]", "==[cd]", "!=", "BEGINSWITH", "CONTAINS", "CONTAINS[c]", "CONTAINS[cd]", "ENDSWITH", "LIKE", "MATCHES"

consolscript13

Who’s for a beta?

Already, much of Consolation seems to be working well. Here is the first beta version (updated from the original alpha): consolationb1

Much of it works just like LogLogger. Currently the trim syslog output option does nothing, and I may well take it away because of the difficulty in implementing it without shell piping.

The Show command button does not run anything, but just displays the array which would be passed as the process parameters. The Run command button both builds the command (you don’t have to Show command first) and then runs it. The standard output is then inserted into the scrolling text box at the bottom of the window. When you want to save its contents as text, just click on the Save button.

There is one slight catch in setting the time period (number): after editing that figure, you must press Tab or Return, or any changes will not be reflected in the command.

As usual with beta software, please be wise and careful in its use. It does not acquire any privileges, and other than text output files will not write anything to your Mac. But it could, of course, crash, and does not yet provide full error handling.

Updated 2 February 2017 with beta release.