Messing Around with JavaScript Decorators

I’ve been looking at the new features in ES6 and boy has JavaScript changed a lot in the last few years. The ECMAScript standards team is really doing a great job making the language more comfortable to work with. My favorite features are block scoped variables with let, a better syntax for defining a class (I never really understood what a “prototype” was), built-in support for loading and exporting modules, and native support for promises. Proxy objects seem like they could be a powerful tool as well. My first impression is that JavaScript is starting to look like a more liberal version of Python, which isn’t a bad thing.

One of the features I looked for in ES6 and could not find was support for function decorators. Decorators can be a great thing to have in a language sometimes. When they fit into an api, they really fit in well and I often use them in my Python library code. I was surprised this feature didn’t make it into ES6 because they are used extensively as part of Angular and React and have native support in TypeScript.

The proposal for decorators can be found in this repository with user guide located here. The proposal is currently at stage two which means it will likely be included in the language in the next major update but are not ready to be included in production code yet. I wrote some sample decorators to test out the new features which you can find in my notes repository here and I’d like to explain to you how they work.

Note: I am not an expert JavaScript, front-end, or Node developer so if you see anything I’m doing wrong in these examples, please let me know in the comments or in an email.

Building the Project

Unfortunately, there is no native implementation for decorators in Node (currently version 11) or any browser yet so you must compile your decorator code with a project called Babel. This project requires two additional plugins, @babel/plugin-propsal-decorators and @babel/plugin-proposal-class-properties.

npm install --save-dev @babel/cli \
    @babel/core \
    @babel/plugin-proposal-class-properties \
    @babel/plugin-proposal-decorators

Babel must also be configured with some options in a .babelrc file you can find here.

Now you can compile and run your code with babel like this and it should work correctly:

babel ./index.js -o index-compiled.js && node ./index-compiled.js

Anatomy of a Decorator

A decorator is basically just a function that gets called in the context of a target method or property that is able to change its state somehow. Here is an annotated example of a decorator which does nothing:

function(descriptor) {
  // alter the descriptor to change its properties and return it
  descriptor.finisher = function(klass) {
    // now you get the class it was defined on at the end
  }
  return descriptor;
}
 
class Example {
  @decorator
  decorated() {
    return 'foo';
  }
}

The descriptor that is returned from the decorator is an object that you can mutate to change the target method. The important properties of this object are:

  • kind – whether this is a ‘method’, a ‘field’, or something else
  • key – the name of what is being decorated (in this case, ‘decorated’)
  • descriptor – contains configuration for the property, and the value which you can hook into with custom behavior
  • finisher – add a function to be called after the class is defined for customization of the class

To have your decorator take parameters, use a function that returns a decorator like the one above.

function decorator(options) {
  // options are passed with the decorator method
  return function(descriptor) {
    return descriptor;
  }
}
 
class Example {
  @decorator({foo:'bar'})
  decorated() {
    return 'foo';
  }
}

Example: Log a Warning When Calling a Deprecated Method

function deprecated(descriptor) {
  // save the given function itself and replace it with a wrapped version that
     logs the warning
  let fn = descriptor.descriptor.value;
  descriptor.descriptor.value = function() {
    console.log('this function is deprecated!');
    return fn.apply(this, arguments);
  }
  return descriptor;
}
 
class Example {
  @deprecated
  oldFunc(val) {
    return 'oldFunc: ' + val;
  }
}
 
let ex = new Example();
ex.oldFun('foo')
// > this function is deprecated!
// > oldFunc: foo

Example: Make a Property Readonly

function readonly(descriptor) {
  descriptor.descriptor.writable = false;
  return descriptor;
}
 
class Example {
  @readonly
  x = 4;
}
 
let ex = new Example();
ex.x = 8;
ex.x;
// returns 4. note that you don't get a warning for trying to set a readonly property

Example: Reflect a Class

Given a class, we want to find all the methods that are decorated with a certain decorator. We will use the @property decorator for this. This sort of thing would normally be used for a base class in your API that your user is expected to override.

function property(descriptor) {
  descriptor.finisher = function(klass) {
    klass.properties = klass.properties || [];
    klass.properties.push(descriptor.key);
  };
  return descriptor;
}
 
class Example {
  @property
  someProp = 5;
 
  @property
  anotherProp = 'foo';
 
  static listProperties() {
    return this.properties || [];
  }
}
 
Example.listProperties()
// > [ 'someProp', 'anotherProp']

Conclusion

Decorators open up a lot of possibilities in a language and I am looking forward to their inclusion into JavaScript. I plan to use them in a Node library I am writing right now. However, reflection is a very powerful tool and should not be used without careful consideration. Make sure the decorator pattern actually fits your use case before you decide to use them. Have fun with decorators!

Playerctl at Version 2.0

I’ve spent the last month revisiting an older project of mine called Playerctl. I wrote the first version nearly five years ago over a weekend and have been making small tweaks to it here and there in my free time since then. The idea was to make a command line application to control media players so I could bind a command to keyboard key combinations to have media key functionality. I do mostly everything on the keyboard so I found it distracting to reach over to the mouse, find the media player window, and click on a button whenever I wanted to pause the player or skip a track on the radio. Playerctl works great for this. These lines have been in my i3 config for years.

bindsym $mod+space exec --no-startup-id playerctl play-pause
bindsym $mod+$mod2+space exec --no-startup-id playerctl next

Another goal of the project was to access track metadata for a “now playing” statusline indicator in i3bar, tmux, or whatever. I did a few implementations of this, but never really made a satisfying statusline (more on that later).

Other people seemed to have found it useful too, and to this day it’s the most popular project I’ve published under my name on Github. With users come issues when people find bugs and limitations in the interface for their use case. These discussions with users have guided the development of version 2.0 of Playerctl and I think I’ve addressed everyone’s concerns. The main points that drove development for this version were:

  • Make the command line application easier to use with multiple players running at the same time
  • Make it easier to print metadata and properties in the format you want
  • Make it easier to make a statusline application
  • Make the library more usable

It wasn’t easy to do these things, but in the end, I’m pretty happy with how things came out and I hope everybody enjoys the new features. Version 2.0 was a big effort and represents nearly a rewrite of v0.6.2 and a doubling of the size of the code base to accomidate the new features. In this post, I’d like to go over the changes, share some of the rationale for the design choices I made, and alert you to some breaking changes in the new version.

Player Selection Overhaul

In the old version, you could select players by passing a name or comma-separated list of players to the --player flag but I found this feature rather limiting because the behavior was simply to execute the command on all the players. That makes it not very usable for the play command because you almost never want all your players to start playing something at the same time. My thinking is most people have something like a “main music player” which is good at handling large playlists and then a secondary player they use for movies or other random media on their system. So I changed the default behavior of the --player command to only execute the command on the first player in the list that is running and supports the command. That way people can pick the priority of the players they want to control, and if the command is not supported (such as if you command the player to go to the next track but there is no next track), it will skip the player and go to the next one. You can still get the old behavior by combining this flag with the --all-players flag.

Another requested feature was the ability to ignore players instead of explicitly whitelisting them. This is useful for instance to ignore players that are not really media players like Gwenview. Now you can pass those players with --ignore-player and they will simply be ignored.

Another detail about player selection is that now a player name will match all the instances of the players, which it didn’t do before. So if I pass --player=vlc, all the instances of VLC that are open will be selected.

I think that should cover everybody’s needs the best I can, but there are always going to be edge cases that I won’t be able to address without mind reading abilities.

Format Strings

Before format strings, the way to get multiple properties was normally with multiple calls to the CLI like this:

artist=$(playerctl metadata artist)
title=$(playerctl metadata title)
echo "${artist} - ${title}"

But that’s obviously not a very elegant solution and doesn’t scale very well if you want to print more than a few things. I implemented a few features in this version to address this. One feature is that you can now specify multiple keys and each key you specify will be outputted on its own line.

$ playerctl metadata artist title
> Katy Perry
> California Gurls

Then you can split on the newline and they’ll be in an array in that order. I still wasn’t very happy with this because it’s not very semantic for what people are trying to do with it. What people said they wanted was for a raw playerctl metadata to output JSON they could parse, which I wasn’t willing to do because I don’t want to add the dependency just for that feature, and even then, scripts would then need something like jq to parse the output. What I did do is make the metadata call output a table (instead of the serialized gvariant before) which is great for readability, but I wouldn’t recommend parsing it.

vlc   mpris:trackid             '/org/videolan/vlc/playlist/37'
vlc   xesam:title               Synthestitch
vlc   xesam:artist              Garoad
vlc   xesam:album               VA-11 HALL-A - Second Round
vlc   xesam:tracknumber         34
vlc   vlc:time                  281
vlc   mpris:length              281538480
vlc   xesam:contentCreated      2016
vlc   vlc:length                281538
vlc   vlc:publisher             3

So after deliberating for awhile, I decided to do the hard thing and just go ahead and write my own template language based loosely on jinja2 but with a lot fewer features. The parser was a lot of fun to write and I’m happy with how it came out. Now you can do this:

$ playerctl metadata --format '{{artist}} - {{title}}'
> Garoad - Your Love is a Drug

I even put template helpers into the language for additional formatting you may want to do to make the variables more readable. This is the format string I use for my statusline generator right now:

fmt='{{playerName}}: {{artist}} - {{title}} \
     {{duration(position)}}|{{duration(mpris:length)}}'
playerctl metadata --format ${fmt}
> vlc: Garoad - Dawn Approaches 3:28|4:10

The duration() helper converts the position from time in microseconds to hh:mm:ss format. There are a few others too that are pretty interesting documented in the man page.

Follow Mode

If you wanted to use the CLI to make a statusline before, you basically had to poll. And if there’s one thing that everybody hates to do, it’s polling. It would be better to have a tail -f style flag that blocks and automatically updates when things change. This was the hardest feature to add because there was a lot of functionality lacking in the library, and the CLI was designed to be mostly stateless because it was only supposed to be a one-off. There’s also a lot of edge cases with players starting, exiting, and changing states which is difficult to get right. I put a lot of detail into making sure the most relevant thing is shown on the statusline based on the input. If you have players passed with --player, it will show them in order of player priority based on the last player that has changed. If you pass --all-players, it just shows whichever one changed last. I think the last one is what I prefer.

It even works with the --format arg and will tick if you give it a position variable. Here is the grand finale:

fmt='{{playerName}}: {{artist}} - {{title}} \
     {{duration(position)}}|{{duration(mpris:length)}}'
playerctl metadata --all-players --format ${fmt} --follow

My own personal statusline implementation of this can be seen here in i3-dstatus, another one of my neglected projects that will get my attention next.

Library Improvements

I originally had bigger plans for the library, but didn’t end up doing as much with it. I still think it’s really cool though, and I want to keep supporting it. The problem was there was no way to listen to when players connect and disconnect to control, so you basically had to run your script when you knew your player was running which is not great. I needed to add this feature for the follow command anyway, so I decided to go ahead and externalize it in the form of a new class called the PlayerctlPlayerManager. This is meant to be a singleton which emits events for when players start, and keeps an up-to-date list of player names that are available to control. It can manage the players for you too and alert you when they exit.

Here is an exmaple of the manager in action:

#!/usr/bin/env python3

from gi.repository import Playerctl, GLib

manager = Playerctl.PlayerManager()

def on_play(player, status, manager):
    print('player is playing: {}'.format(player.props.player_name))

def init_player(name):
    # choose if you want to manage the player based on the name
    if name.name in ['vlc', 'cmus']:
        player = Playerctl.Player.new_from_name(name)
        # connect to whatever you want to listen to
        player.connect('playback-status::playing', on_play, manager)
        # add the player to the list of managed players and be notified when it
        # exits
        manager.manage_player(player)

def on_name_appeared(manager, name):
    # a player is available to control
    init_player(name)

def on_player_vanished(manager, player):
    # a player has exited
    print('player has exited: {}'.format(player.props.player_name))

manager.connect('name-appeared', on_name_appeared)
manager.connect('player-vanished', on_player_vanished)

# manage the initial players
for name in manager.props.player_names:
    init_player(name)

main = GLib.MainLoop()
main.run()

I tried to make as few breaking changes to the library, but a few were inevitable. There are also a few deprecations that will affect anyone who based a script of the previous example code. See the library docs for more details.

There are a lot of other little changes, but those are the main ones. Enjoy Playerctl 2.0!