tadhg.com
tadhg.com
 

More Slate Tweaking

23:38 Sun 11 Aug 2013
[, , , , , , ]

Following last week’s post, this is about my progress using the OS X window manager Slate. My primary objective is to be able to define a set of window layouts for specific tasks—such as writing a blog post—and then easily invoke them. This is more difficult than it sounds, but I’ve more or less made it work.

This is the layout I’m currently using for writing blog posts:

3-monitor layout

That’s at 25% scale; having a lot of pixels to do things with was one of the key motivators for trying to get the Slate setup working.

A primary goal was to have Slate launch the applications if they weren’t running already, as having to start everything manually would make it far less efficient. Slate isn’t really designed as an application launcher, but it has the ability to run arbitrary shell commands. It also needed to not launch second copies of applications if they were running already.

Obstacle 1: Names Aren’t Everything

The first problem is that Slate identifies applications by their names, which isn’t granular enough for what I want. In particular, I want the Chrome instances for previewing the post and for research browsing to be in specific spots. I also don’t want to necessarily stop VLC and iTerm instances I have running and re-use them for the blog post, but rather to have new instances of them.

If the multiple instances have the same names, Slate can’t distinguish between them. One option would be to use multiple windows and manipulate those by the window titles, but while iTerm is cooperative about this, numbering each window, other applications are not. Another is to have specifically-altered copies of applications with names tailored to their function (which is Seth’s approach), but I don’t like the overhead involved with that.

My solution is to use Slate’s ability to get the process ID for each application and cross-reference that with the output of the ps command in order to associate each process ID with both an application name and the command used to invoke that application. Then it’s a matter of using unique invocations for each of the specific applications, which I was mostly doing anyway.

This is the core of the code to do that:

// (name, command, and pattern are all variables declared above this section.)
var findApp = function () {
    S.eachApp(function (app) {
        if (app.name() == name) {
            apps.push({
                "app": app,
                "name": app.name(),
                "pid": app.pid()
            });
        }
    });

    if (apps.length) {
        _.each(apps, function (element, index, list) {
            pids.push(element["pid"]);
        });
        if (pids.length) {
            var cmd = "/bin/ps -o pid=’’,command=’’ -p " + pids.join(","),
                output = S.shell(cmd, true),
                lines = output.split("\n");
            _(lines).each(function (line, index, list) {
                line = line.trim();
                if (line.length) {
                    var chunks = line.trim().split(" "),
                        pid = chunks[0].trim(),
                        invocation = chunks.splice(1).join(" ");
                    if (invocation.indexOf(pattern) != -1) {
                        S.log(pid);
                        target = pid;
                    }
                }
            });
        }

        if (target) {
            var targetApp = _.filter(apps, function (app) {
                if (parseInt(target) === parseInt(app["pid"])) {
                    return true
                }
            });
            targetApp = targetApp.length ? targetApp[0] : false;
        }
    }
    return targetApp;
};

It goes through all of the running applications using S.eachApp, adding their Slate representations to an array if they match the name of the application we’re looking for. It then runs ps with the -p argument, using the process IDs according to Slate, and compares the output of ps to the commands of the instances we’re looking for. It returns a match, or false if the application isn’t running.

Other code launches the application in question if false is returned here.

This code is the part that solved the basic problem, and I’ll put the full source on GitHub sometime soon.

This does mean starting applications from the command line, which can cause some issues[1].

Obstacle 2: MacVim is Greedy

As mentioned last week, MacVim plays poorly with this approach, because the first MacVim instance will appropriate the windows of later instances and attach them to itself, so that using the above method simply fails because while we technically get the targeted instance, it no longer has any windows to be manipulated.

I couldn’t find a way around this, so I had to treat MacVim as a special case. I was already in the habit of starting instances (which get turned into windows of the main instance) with the --servername flag to easily tell them apart, and MacVim puts the servername in at the start of the window title, making that accessible via Slate. Distinguishing between MacVim windows therefore wasn’t ultimately that hard.

Starting MacVim with that flag also means that it’s listening as a server and can be sent remote messages. MacVim also supports a variety of OS X actions via the :macaction command, so it’s possible to get Slate to run a shell command that sends a remote command to a specific MacVim window. Initially I thought that this would be the key to controlling the various MacVim windows: just send the minimize command to the ones you want to hide.I had the code for that working when I ran into the next problem: Slate doesn’t support minimize at all, and MacVim doesn’t support a “restore” equivalent to that action—and OS X might not expose one. So I could minimize MacVim windows, but then not bring them back without actually clicking on them.

My solution ended up being fairly hacky: I just hide the other MacVim windows behind the one I want to use. In addition, I use the remote capabilities to tell Slate to tell MacVim to hide all other applications as a way of hiding everything[2]. The MacVim windows end up where I want them,and the fact that other windows are lurking behind the one I’m using isn’t really relevant.

Obstacle 3: Firefox is Sneaky

After getting MacVim working, I thought the hard work was done, and started writing the layout for the windows. Initial tests were fine, but then I tried closing everything and hitting the key combination to invoke the blog layout. Slate went into a loop, constantly raising windows from Firefox saying that Firefox was already running.

It took a significant time for me to determine that Slate doesn’t notice Firefox starting. If Firefox has been started when Slate starts, Slate can see and manipulate that Firefox instance. But if Slate starts first, simply starting the Firefox instance isn’t enough—the Firefox instance has to be focused before Slate can see it. Which has to be done manually since Slate can’t see it to run the focus() method on it. And if Slate can’t see it, my launch code assumes it isn’t running, and tries to launch it again.

Presumably there’s something about Firefox’s code that means that NSWorkspace NSWorkspaceDidLaunchApplicationNotification, the OS X listener for application launches, doesn’t register it. I don’t know enough about OS X to determine what that problem is, and I couldn’t find a good workaround. For now, my workaround is simply to launch Firefox manually and make sure it’s been given focus before invoking the blog script. Since the Firefox instance I use is the same one I use for a lot of other things, this requirement is not particularly onerous, but it’s irritating nonetheless.

Note that if Slate could be run from the shell, as well as emitting shell commands, I could solve this via a script that did the pre-Firefox parts, then restarted Slate, and then did the rest. The run-from-shell capability is due in a future version of Slate.

Enhancements

I was able to start playing the VLC playlist by running VLC with its RC interface, a small touch on top of the rest.

It’s very easy to start a Chrome instance at a particular URL, useful for the reStructuredText preview I use.

iTerm doesn’t accept command-line arguments, and as far as I know there’s no way to send commands to it. Luckily it just ignores startup arguments, so these can still be used to distinguish instances of it from each other. It would be nice to start it in the relevant directory, but that’s a minor point.

I haven’t done it yet, but I could also use Vim remote commands to populate a buffer with my blogging template, as I’ve already made commands for that and would just need to send them remotely via Slate rather than typing them in manually.

There were many other annoyances along the way. If I could fix one thing (other than having take command from the shell) it would be whatever the problem with Firefox is. I should file a bug about that.

I hope that any future applications I want to control via Slate don’t have similar—or other—problems. Barring those, the real work is done, and now all that remains is the fairly simple task of specifying more layouts for more monitor configurations[3]. Hopefully I’ll have it all up on GitHub soon.

[1] For example, you have to give Chrome a --user-data-dir option in order to have it not complain—but this turns out to be an excellent way to distinguish between instances.

[2] I could probably tell Slate to hide everything except MacVim and have the same effect.

[3] It’s really only worth doing this for configurations of two or more monitors, I suspect. I’ll try to find useful organizational modes with just one screen, but I think they’re just not going to be that useful.

Leave a Reply