wmii+node.js, Part 1: Event Handling

This post is part of a series.

Previous post: << Introduction

Next post: Keyboard Events >>

Before I start writing code, here’s some information that will probably be useful:

  • I’m using the latest hg version of wmii. This is different from the wmii that is provided with most Linux distributions. Its config file format is different, and it fixes several bugs present in previous versions. You will probably need to compile it yourself (along with libixp, which it depends on).
  • I’m running Ubuntu 13.04. This will probably work the same on any other Linux distro, though.
  • I plan to model this config after my old bash wmii config. Although it didn’t turn out the best (mainly because bash is a terrible language to work with), I like the modular design, and want to implement a more stable version in node.js.

 

If you haven’t used wmii before, you may want to compile it and try it out with the built-in config first, and read the man page. I’ll explain a lot about wmii’s internals and APIs in this blog, but I’m assuming that anyone reading this already knows the basics of how to use wmii. The first thing I did when approaching this project was to read up on node.js. In order to make this project work, there are 3 parts of node.js that I need to understand thoroughly:

  • Launching external programs
  • Module management (importing other .js files)
  • Event handling and custom events

Luckily, as it turns out, all of these are really simple in Node. Let’s get the basics out of the way and set up Node first. In Ubuntu, you can just sudo apt-get install nodejs npm to install Node and its package manager, npm. For some reason, in Ubuntu the executable for Node is called nodejs instead of node; I created a symlink to fix this. Next, write the classic introductory program:

console.log('Hello, world!');
view raw hello.js hosted with ❤ by GitHub

Save it as hello.js, and run it with node hello.js. Nice and simple.

 

Events and wmiir

Now let’s do something more interesting: reading events from wmii. In order to do this, we’ll need to launch wmiir from Node and read its output. Looking through Node’s documentation shows that I can use child_process.spawn to launch an external program, and that I can then add an event listener to its stdout stream object to run a callback function every time it reads… some amount… of data from the stream. It’s not quite clear how much. So we’ll find out by experiment. This sample program should print wmii events to the console as they occur:

var spawn = require('child_process').spawn;
var stdin = process.openStdin();
var reader = spawn('wmiir', ['read', '/event']);
reader.stdout.on('data', function(data) {
console.log("Got event: " + data);
});
console.log("Press ENTER to exit.");
stdin.on('data', function(data) {
reader.kill();
process.exit();
});
view raw event_reader.js hosted with ❤ by GitHub

Save it as event_reader.js and run it. Note that from here on in, I’m assuming that you’re running wmii. This code won’t work unless wmii is running! Once the program is started, move your mouse around to switch focus between a few different windows. You should see the events start appearing on the console, like this:

Got event: ClientFocus 0x1285e8e

Got event: AreaFocus 1

Got event: ClientFocus 0xe0000a

Got event: AreaFocus 2

 

At first, this looks like Node is reading data one line at a time, but without removing the trailing newlines. I thought this at first, too. Actually, it’s reading data in completely arbitrary “chunks”; they only happen to correspond to lines here because wmii is writing events to the file one line at a time. You can see what’s going on if you replace ‘/event’ with ‘/ctl’ (a settings file with an actual EOF) and run the script again:

Got event: bar on bottom
border 1
colmode stack
focuscolors #000000 #81654f #000000
font fixed
fontpad 0 0 0 0
grabmod 
incmode squeeze
normcolors #000000 #c1c48b #81654f
view 1

 

This time, it read the whole file as a single chunk. This behavior will make reading the /events file line-by-line somewhat tricky, because the callback isn’t guaranteed to receive a line at a time. Because of idiosyncrasies like this, and because calling child_process.spawn is the main way that we’re going to interact with wmii, it makes sense to encapsulate the whole process of talking to wmiir in a few convenience functions.

 

wmiir.js

In fact, these functions would make a good Node module. A module in Node is any JS file that defines an object called exports. Whenever you call require in another file, the require‘d file is executed if it hasn’t been before (otherwise, a cached copy is returned–keep that in mind; it’s important!), and its exports object is returned to the file that called require.

wmiir provides several subcommands, but the most commonly used ones are readwrite, ls, create, and remove. Read man wmiir for a description of how these work; wmii’s manpages are the only reliable documentation available since so much information about wmii on the web is outdated. Creating a wrapper function for each subcommand seems like a good approach. Here are (thoroughly-commented) wrapper functions for the read and write commands:

var spawn = require('child_process').spawn;
// Spawns and returns a child process that runs `wmiir read`.
// The callback takes two parameters: (err, line).
// It is called once for each line in the file read.
// - err is any error that occurred, or null if no error occurred.
// - line is the last line read from the file.
exports.read = function(path, callback) {
// Start the process.
var child = spawn('wmiir', ['read', path]);
// Record the error stream, so that any error messages that wmiir produces
// can be passed on to the callback.
var stderr="";
child.stderr.on('data', function(chunk) {stderr += chunk;});
// If there's an error, pass it to the callback.
child.on('error', function(err) {if (callback) callback(err);});
// Read from the process's stdout until a newline is encountered, then pass
// the line to the callback.
var line="";
function handleData(chunk) {
try {
// Data from stdout is usually returned in the form of a Buffer,
// not a String. Make sure to convert it.
if (chunk instanceof Buffer)
chunk = chunk.toString();
// If a newline is encountered, call the callback with everything
// that was read up until the newline.
var newline = chunk.indexOf('\n');
if (newline > -1) {
if (callback) callback(null, line + chunk.slice(0, newline));
line = "";
var remaining = chunk.slice(newline+1);
if (remaining.length > 0)
handleData(remaining);
} else {
// Otherwise, store the output for later.
line += chunk;
}
} catch (e) {
if (callback) callback(e);
callback = null;
child.kill();
}
}
child.stdout.on('data', handleData);
// If the process exits with a nonzero exit code, throw an error with the
// content of stderr, which should contain a message.
child.on('exit', function(code, signal) {
if (code !== 0) {
if (callback) callback(new Error(stderr));
}
// Also, make sure to return the last line read, if there was one.
else if (line !== "") {
if (callback) callback(null, line);
}
});
// Return the child process, as a handle in case we need to kill it.
return child;
};
// Spawns and returns a child process that runs `wmiir write`.
// The given data is sent to this process's stdin.
// The callback takes one parameter: (err).
// - err is any error that occurred, or null if no error occurred.
exports.write = function(path, data, callback) {
// Some of this is the same as `read`, so I'll only comment the parts that
// are different.
var child = spawn('wmiir', ['write', path]);
var stderr="";
child.stderr.on('data', function(line) {stderr += line;});
child.on('error', function(err) {if (callback) callback(err);});
child.on('exit', function(code, signal) {
if (code === 0) {
// If the process exits normally, execute the callback.
if (callback) callback(null);
} else {
if (callback) callback(new Error(stderr));
}
});
// Write the data to the file.
// This closes stdin, which should also terminate the child process.
child.stdin.end(data);
return child;
};

The full wmiir.js file is on GitHub as well, but the snippet above should be enough to understand what’s going on. Each function calls an external command, then passes its results to a callback. According to Node convention, callbacks should reserve their first parameter for any errors thrown by the caller. Node’s callback-based I/O solves the two biggest problems with my bash config: it prevents the external programs from blocking the event loop, and it doesn’t require forking a bunch of extra listener processes. Node runs in a single thread, and, every time an event occurs, any callbacks that it triggers are added to a queue. When the interpreter runs out of code to execute, it just takes the next callback off of the queue and runs it, or waits for one to show up. This is much more stable and efficient than multithreaded or multiprocess systems, especially for simple tasks.

 

wmii_events.js

Now that we have a wmiir module, let’s create an event handler module. Node already has a concept of event handlers, so, ideally, our wmiir event handler should behave exactly like other eventful Node objects. The Node documentation says that this can be done by extending the EventEmitter class. JavaScript classes and inheritance are complex enough to deserve a blog post of their own, so I won’t describe how they work here, but it’s worth noting that Node provides a convenience function, util.inherits, for creating class constructors that inherit from other classes.

For this module, we’ll require our existing wmiir.js module (which should be in the same directory), and export an EventEmitter instance. We’ll also override EventEmitter.emit, to send a wmii event through the /event file, rather than just sending a Node event.

// wmii event handler module
// Adam R. Nelson
// August 2013
var events = require('events');
var util = require('util');
var wmiir = require('./wmiir.js');
function WmiiEvents() {
events.EventEmitter.call(this);
var me = this;
me.readProc = wmiir.read('/event', function(err, line) {
var tokens;
if (err) {
// In the event of an error, emit a 'FatalError' event.
tokens = ['FatalError', err.message || err];
} else {
// Split the line (minus the trailing newline) into tokens.
tokens = line.split(" ");
}
// Emit a Node event based on the wmii event.
// (The emit method is renamed below so that it can be overridden).
me.emit_.apply(me, tokens);
});
// Turn off the max-listeners limit, since there will be a LOT of listeners
// attached to this object.
me.setMaxListeners(0);
// The wmiir process will keep running forever until it is killed. Make
// sure it is always killed before Node exits.
function cleanup() {
me.readProc.kill();
}
process.on('exit', cleanup);
me.on('FatalError', function(/* varargs */) {
// The message will be split up into string-separated tokens, which
// need to be rejoined. Array.prototype.slice is needed to convert
// 'arguments' to a real array, because Javascript.
var message = Array.prototype.slice.apply(arguments).join(' ');
console.log("Fatal error occurred! - " + message);
cleanup();
});
}
// Extend EventEmitter.
util.inherits(WmiiEvents, events.EventEmitter);
// Rename the original emit method...
WmiiEvents.prototype.emit_ = WmiiEvents.prototype.emit;
// ... and add a new one that broadcasts events globally.
WmiiEvents.prototype.emit = function(/* No arg names; this is varargs. */) {
// Convert 'arguments' to an array, because it's not by default.
// (*sigh* Javascript...)
var eventParts = Array.prototype.slice.call(arguments);
var me = this;
// Remove any problematic newline-containing elements and non-string
// elements from the array.
eventParts = eventParts.filter(function(e) {
return (typeof e == 'string' || e instanceof String) && e.indexOf('\n') == -1;
});
// Write a space-separated event string to the /event file.
wmiir.write('/event', eventParts.join(' '), function(err) {
if (err) {
// If an error occurs, broadcast an event.
me.emit_('Error', err);
}
});
// Always return true... the original emit() returns true if there was a
// listener for the event, but there may always be another process
// listening in this case.
return true;
};
// Call this function to terminate the event listener without terminating Node.
WmiiEvents.stopListening = function() {
this.readProc.kill('SIGHUP');
};
// Export a single copy of the event handler object.
module.exports = new WmiiEvents();
view raw wmii_events.js hosted with ❤ by GitHub

Notice that this time, rather than adding new functions to module.exports, we replaced it with an entirely new object. (exports is just an alias for module.exports). Whenever another file requires this file, it will start the event loop automatically–and, because require caches already-loaded files, we can use this event loop in as many other files as we want, and still be guaranteed that only one instance is running! Much nicer than bash.

 

Putting it all Together

Finally, let’s write a sample program that uses our event loop module. This program will listen for the ‘Key’ event, which happens whenever the user presses a key that wmii has bound, and print the name of the key.

var events = require('./wmii_events.js');
var stdin = process.openStdin();
events.on('Key', function(key) {
console.log('Keypress: ' + key);
});
console.log("Press ENTER to exit.");
stdin.on('data', function(data) {
process.exit();
});
view raw key_events.js hosted with ❤ by GitHub

Save this file and run it (in the same folder as the other files), and then press some keys to navigate around wmii (like Mod4+hjkl, or Mod4+arrow keys if you’re like me and still use the arrow keys for everything). You should get output like this:

Keypress: Mod4-Left
Keypress: Mod4-Right
Keypress: Mod4-Left
Keypress: Mod4-Right

 

Key events are only sent for bound keys, so pressing random keys won’t output anything; you have to press keys that wmii is actually using. In my next post, I’ll look at handling wmii keybindings and key events.

Next post: Keyboard Events >>

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s