Better poly than sorry!

My adventure in X screen locking - slock re-implementation in Nim, part 1

NOTE: This part covers project motivation and setup
NOTE: The full source code is on GitHub

TL;DR or What is this post about?

In short: it's about me exploring - or at least getting into contact with - a couple of interesting things:

  • X Window System APIs via Xlib bindings
  • low-level Linux APIs for elevating privilages, checking passwords
  • GCC options and some C
  • and of course the Nim language


At my work we strongly discourage leaving logged-in accounts and/or unlocked screens of computers. I happen to agree that locking your computer is a good habit to have, so I've had no problems with this rule... Up to the point when I switched from KDE to StumpWM (I wrote about it some time ago, in this, this and this posts) and my trusty Ctrl + Alt + L stopped working.

The only idea that came to my mind was to use the venerable xscreensaver, but: a) I didn't really need any of the 200+ animations (I just wanted a blank screen) and b) I didn't like how the unlock dialog looked like[1].

XScreenSaver and its dialog straight from the 80ties.

I needed something more lightweight (xscreensaver is ~30k loc of C[2]), simpler and either better looking or without any UI altogether.

slock to the rescue

There's a site called suckless.org where you can find some impressively clean and simple tools, implemented mostly in C. You can read more about their philosophy here, which I recommend as an eye opening experience. Anyway, among the tools being developed there, there is also slock - a very simple and basic screen locker for X. It's 310 lines of code long and it's all pretty straightforward C.

The program suited my needs very well: minimal, fast, and good looking. Well, the last part it got through cheating, as slock simply has no interface at all - but this means there's no ugly unlock dialog, so it's all good.

Why Nim?

As I used slock I read through its code a couple of times. It seemed simple and learnable, despite the fact that I knew nothing about X and didn't use C seriously in 15 years. Fast forward to last week and I finally found some free time and decided to learn some Xlib stuff. Re-implementing slock in Nim looked like a good way of learning it: this may sound a bit extreme, but it allowed me to gain exp points in two stats simultaneously[3] - in Xlib usage and Nim knowledge!

Nim is a very interesting language. I'd call it "unorthodox" - not yet radical, like Lisp or Smalltalk, but also not exactly aligned with Java and the like. For one tiny example: return statement in functions is optional and if omitted, a function returns its last expression's value. That's rather normal way to go about it; hovewer, in Nim you can also use a special variable name result and assign to it to have the assigned thing returned. It looks like this:

  proc fun1() : int =
    return 1

  proc fun2() : int =

  proc fun3() : int =
    result = 1

  assert fun1() == fun2() == fun3()

At a first glance this may look strange, but consider this in Python:

  def fun():
      ret = []
      for x in something:
      return ret

It's practically an idiom, a very common construct. Nim takes this idiom, adds some sugar, adapts it to the world of static typing and includes in the language itself. We can translate the above Python to Nim with very minor changes[4]:

  proc fun4() : seq[int] =
    result = @[]
    for x in something:

As I said, it's not a groundbreaking feature, but it is nice and good to have, and it shows that Nim doesn't hesitate much when choosing language features to include. In effect Nim includes many such conveniences, which may be seen both as a strength and as a weakness. While it makes common tasks very easy, it also makes Nim a larger language than some others. It's not bad in itself, rather, depending on how well the features fit together it's harder or easier to remember and use them all. Nim manages well in this area, and also it most definitely is not like C++ with its backwards compatibility problem, so I think even Pythonistas with taste for minimalism will be able to work with Nim.

Being Nimble - the project setup

Many modern languages include some kind of task runner and package manager either as part of stadard distribution or as downloadable packages. Nim has Nimble, which takes care of installing, creating and publishing Nim packages. Assumming that you have Nim already installed[5], you can install Nimble with:

  $ git clone https://github.com/nim-lang/nimble.git
  $ cd nimble
  $ git clone -b v0.13.0 --depth 1 https://github.com/nim-lang/nim vendor/nim
  $ nim c -r src/nimble install
  $ export PATH="$PATH:~/.nimble/bin/"

Note the addition to the PATH variable: ~/.nimble/bin/ is where Nimble installed itself and where it will place other binaries it installs. Make sure to have this directory in your PATH before working with nimble.

Creating a project

Creating a project is easy, similar to npm init:

  $ mkdir slock
  $ cd slock
  $ nimble init
  In order to initialise a new Nimble package, I will need to ask you
  some questions. Default values are shown in square brackets, press
  enter to use them.
  Enter package name [slock]:
  Enter intial version of package [0.1.0]:
  Enter your name [Piotr Klibert]:
  Enter package description: Simplest possible screen locker for X
  Enter package license [MIT]:
  Enter lowest supported Nim version [0.13.0]:

  $ ls
  total 4.0K
  -rw-rw-r--. 1 cji cji 188 13/02/2016 01:02 slock.nimble
  $ cat slock.nimble
  # Package

  version       = "0.1.0"
  author        = "Piotr Klibert"
  description   = "Simplest possible screen locker for X"
  license       = "MIT"

  # Dependencies

  requires "nim >= 0.13.0"

  $ mkdir src
  $ touch src/slock.nim

The generated slock.nimble is a configuration file for Nimble; it's written in a NimScript, which looks like a very recent development in Nim and it replaces the previous INI-style config files. This means that many examples and tutorials on the Internet won't work with this format. The most important difference is the format of dependencies list for your app: it now has to be a seq. For example, to add x11 library to the project:

  requires @["nim >= 0.13.0", "x11 >= 0.1"]

The dependencies should be downloaded by Nimble automatically, but you can also dowload them manually, like in the example below. You'll notice that I also install a c2nim package - I will say more about it later.

  $ nimble install c2nim x11
  Searching in "official" package list...
  Downloading https://github.com/nim-lang/c2nim into /tmp/nimble_18934/githubcom_nimlangc2nim using git...
  Cloning into '/tmp/nimble_18934/githubcom_nimlangc2nim'...
  ...etc, etc, etc...

Workflow and tooling

Nim is a compiled language, but working with it proved to be comparable to working with a dynamic language, mainly thanks to type inference and blazingly fast compiler. You dcan ommit type declarations where they are obvious from the context and Nim will deduce the correct type for you. It's not a full program type inference like in OCaml, but rather a local type inference as used in Scala or modern C++ or Java. Even with this limitation it's immensely useful and reduces code verbosity by a lot.

Compiler speed is important, because it encourages frequent testing. If compilation is going to take a long time you tend to "batch" changes in your code together and only compile and run once in a while instead of after every single change. This, in turn, makes it harder to find and fix regressions if they appear. Dynamic languages work around this issue by rejecting compilation step completely, at the cost of run-time safety and performance. Nim - which is similar to Go in this respect - makes compilation effortless and nearly invisible. The command for compile-and-run looks like this:

  $ nim c -r module.nim

The problem with this command is that it doesn't know about Nimble and dependencies, so in reality I used a bit different commands:

  $ nimble build && sudo ./slock

  # or, if you need to pass some additional parameters to Nim compiler:
  $ nimble c --dynlibOverride:crypt --opt:size --passL:"-lcrypt" -d:release src/slock.nim  -o:./slock && sudo ./slock

While Nim's compiler and Nimble are very good tools, they're not enough to work comfortably on more complex codebases. Nim acknowledges this and provides a couple of additional tools for working with code, like nimsuggest. However, nimsuggest is rather new and it SEGFAULTed on me a couple of times[6]. I used it via - of course - Emacs with nim-mode, and again, I encountered a couple of irritating bugs, frequent "Mismatched parens" errors when trying to jump a word or expression forward. However, when nimsuggest works, it does a good job, particularly its "Go to definition" feature works and is very helpful.

Nim's definitive source of documentation is The Index, which does a surprisingly well as a reference. Ctrl + f works just as well as search boxes other documentation sites provide. Nim docs are also nice in that they link to the source: you get a link to the function implementation beside its documentation. I like this trend and I'm happy that more documentation is like this - a quick peek at the source can sometimes save hours of unneeded work.

In this project, I had to work with Xlib and turns out its documented in man pages, and the pages were already installed on my system. I don't remember installing them, so maybe they come with X by default on Fedora. Anyway, for Xlib tutorial I used Xlib Programming Manual and for reference I simply used the man pages: just type, for example, man XFreePixmap and you get XCreatePixmap (3) man page. The same is true for Linux/POSIX functions - try, for example, man getpwuid.

That's it, for now

In the next part I'm going to show some Nim code, translated - both manually and automatically - from C. I'll focus on Nim's language features that C doesn't have and I'll show how they can be used to write shorter, safer and more readable code. In the later, last part I'm going to write about Nim's interop with C and various ways of connecting the two, using Xlib as an example.

  • And it seems that JWZ doesn't like people modifying the looks of this dialog, so I didn't even try.
  • Very well written C.
  • Every power gamer will understand how nice this is!
  • But note the lack of unnecessary return statement in Nim.
  • I recommend installing from GitHub - it's always good to have the sources for your compiler and stdlib around, and you won't get it if you install just the binaries. And master branch is stable: checkout devel branch for the in-development code.
  • This might be because of my own incompetence, of course.