Thursday, 2020-09-24

Rust in 2021: Leveraging the Type System for Infallible Message Buffers

The Rust programming language features a versatile type system. It gives memory safety by distinguishing between raw pointers, pointers to valid data and pointers to data that may be written to. It helps with concurrency by marking types that may be moved between threads. And it helps keep API users on the right track with typestate programming.

With some features of current nightly builds, this concept can be extended to statically check that a function will succeed. Let's explore that road!

Current state: Fallible message serialization

For a close-to-real-world example, we will look into how CoAP messages are built on embedded devices. CoAP is a networking protocol designed for the smallest of devices, and enables REST style applications on even the tiniest of devices with less than 100KiB of flash memory. The devices can take both the server and the client role.

Writing a response message nowadays may[1] look like this:

fn build_response<W: WritableMessage>(&self, request_data: ..., response: W) -> Result<(), W::Error> {
    let chunk =;
    match chunk {
        Ok((payload, option)) => {
            message.add_opaque_option(option::ETag, &self.etag)?;
            message.add_block_option(option::Block2, option);
        Err(_) => {

As these devices typically don't come with dynamic management of the little RAM they have, it is common to build responses right into the memory area from which they are sent; that memory area is provided in the WritableMessage trait.

A CoAP server for large systems with dynamic memory management may not even need to ever err out here -- it could just grow its message buffer and set its error type to the (unstable) never_type. In a constrained system with a fixed-size message buffer, that is not an option. Moreover, while for some cases this may be client's fault and call for an error response, many such cases indicate programming errors: All the size requirements of a chunk and the options can be known, so if they don't fit in the allocated buffer, that's a programming error.

Typed messages: No runtime errors

Programming errros should trigger at compile time whenever possible. So what could that look like here? Let's dream:

fn build_response<W: WritableMessage<520>>(&self, request_data: ..., response: W) -> impl WritableMessage<0> {
    let chunk =;
    match chunk {
        Ok((payload, option)) => {
                .add_opaque_option(option::ETag, &self.etag)
                .add_block_option(option::Block2, option)
        Err(_) => {

    Checking myserver v0.1.0
error[E0277]: the trait bound `impl WritableMessage<520>: WritableMessage<522_usize>` is not satisfied
  --> src/
1  | fn build_response(&self, request_data: ..., response: W) -> impl WritableMessage<0> {
9  |     .add_payload(payload)
   |          ^^^ the trait `WritableMessage<522_usize>` is not implemented for `impl WritableMessage<520>`
help: consider further restricting this bound
1  | fn build_response<W: WritableMessage<522_usize>>(...) {

error: aborting due to previous error

If the function can indicate clearly in its signature what it needs as a response buffer, static error checking can happen on all sides:

  • Inside the function, operations on the message can be sure to have the required space around.

    Error conditions only arise when inputs need to be converted from unbounded to bounded values. They are, however, not the typical programming errors of underestimating the needed space, but reflect actual application error conditions that are now more visible and do not get buried in boilerplate error handling.

    In the particular example above, the program only asked 520 bytes of memory, but would need 522 bytes in the worst case. Even with some testing, this would not have been noticed, and then suddenly fail once a file larger than 8KiB was transferred.

  • Outside the function, the caller can be sure to provide adaequate space, or will be notified at compile time that build_response is not implemented for too small a buffer.

How to get there

A cornerstone of tracking memory requirements in types are const generics. They are what allows creating types and traits like WritableMessage<522> in the first place.

Aside: typenum and generic-array

The typenum and generic-array crates do provide similar functionality.

Using them gets complicated very quickly, though, and debugging with them even more so.

While they do a great job with the language features available so far, const generics can provide a much smoother development experience due to their integration into the language.

The first bunch of const generics, min_const_generics, is already on the road to stabilzation; withoutboats' article on the topic summarizes well what can and what can't be done with them.

Applications like this will need more of what as I can tell is not even complete behind the const_generics feature gate: computation based on const generics will be essential in matching the input and output types of operations on size-typed buffers.

With those, methods like add_option<const L: usize>(option: u16, data: &[u8; L]) can be implemented on messages, with additional limitations on L being small enough to fit in the current message type.

Why this matters

Managing errors in this way ensures that out-of-memory errors do not come as a surprise, and enhances visibility of throse error case that do need consideration.

Furthermore, each caught error contributes to program size. Without help from the type system, the compiler can only rarely be sure that the size checks are unnecessary. Not only do these checks contribute to the machine code, they often also come with additional error messages (even line numbers or file names) generated for the handler that eventually acts on them. (This is less of an issue when error printing is carefully avoided, or a tool like defmt is employed).

Last but not least, there's a second point to in-place creation of CoAP messages: Options must be added in ascending numeric order by construction of the message; adding a low-number option later is either an error or needs rearranging the options, depending on the implementation. The same const generics that can ensure that sufficient space is available can just as well be used to statically show that all options are added in the right sequence.

Context, summary and further features

This post has been inspired by Rust Core Team's blog post on creating a 2021 roadmap.

The features I have hopes to use more in 2021 are:

[1]While the examples are influenced by the coap-message crate, they do not reflect its current state of error handling. Also, they assume a few convenience functions to be present for sake of brevity.

Friday, 2020-06-26

Resuming suspend-to-disk with missing initrd

This morning two unfortunate things coincided:

  • My /boot had recently run out of disk space, I did my usual workaround of removing all initrd images, and forgot to do the second step of rebuilding them. (Or I just reconfigured grub instead of initramfs-tools).
  • My battery went out and the machine went to hibernation.

Consequently, the machine failed to come up at all, showing a kernel panic about being unable to find the root device.

As I have way more volatile state in a running session than I'd like, I made every effort to resume the session, which (spoilers!) eventually worked. Note that this is not a tutorial, just a report that roughly these steps worked for me.

It's worth pointing out that it's likely one only gets one shot at this. Had I gotten it half-right to the point where the init system could open the encrypted root partition but got the resume partition wrong, it would come up and invalidate any hibernation image there was. (An issue I had run into several years ago where the system would try to resume from there on a subsequent boot and wreck everything has been resolved already as far as I understood).

Common setup

The computer I recovered is running Debian GNU/Linux sid, the helper device is on stable (buster).

Both have their internal SSD (nvme0n1) partitioned into an UEFI partition, a (too small!) boot partition, and a LUKS encrypted LLVM PV that holds at least a root and a swap partition.

Initial error

Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)

(Note that these don't come from screenshots, but from a reconstruction I did afterward, so some details might be off.)

This had me startled for a moment fearing for my disk, but the lack of a Loading initial ramdisk ... in the grub phase pointed me to the first-stage culprit: The grub entry did not have an initrd configured. (And none of the backup kernels had one either).

initrd transfusion

Fortunately, I have a second device nearby with a very similar setup. I copied its initrd to a USB stick and tried to boot from there.

echo 'Loading Linux 5.6.1-1-amd64'
linux /vmlinuz-5.6.0-1-amd64 root=-/dev/mapper/hephaistos--vg-root ro quiet
echo 'Manually added initrd'
initrd (hd0)/initrd.img-5.6.0-0.bpo.2-amd64

No luck though -- and the initrd shell barely showed anything in /dev at all. lsmod revealed no loaded modules. Turned out that the systems were not identical enough, and the kernel versions didn't match. A second run with matching kernel versions fixed this:

echo 'Loading Linux 5.6.1-1-amd64' linux (hd0)/vmlinuz-5.6.0-0.bpo.2-amd64 root=-/dev/mapper/hephaistos--vg-root ro quiet echo 'Manually added initrd' initrd (hd0)/initrd.img-5.6.0-0.bpo.2-amd64


A plain boot attempt from there did not work (it waited for the other computer's disk to show up, which it knew by its UUID to start decrypting it),

At this stage, I could extract the two UUIDs I knew I would later need:

# blkid /dev/nvme0n1p3
/dev/nvme0n1p3: UUID="b47fac73-66d5-42c6-a370-f9f1dce497d1" TYPE="crypto_LUKS" PARTUUID="76773950-442c-4b81-bf70-9b9da41ec5bf"
# cryltsetup luksOpen /dev/nvme0n1p3 decrypted
# blkid /dev/mapper/hephaistos--vg-swap_1
/dev/mapper/hephaistos--vg-swap_1: UUID="3c7f3271-f54f-41f0-8ec5-aee20977418e" TYPE="swap"

With this, I could take the USB stick back to the helper PC.

initrd fine tuning

As both the encrypted and the resume partition are named in the initrd, I tried building a suitable initrd.

On the helper PC, I changed /etc/crypttab to reflect the UUID of the NVM partition, and /etc/initramfs-tools/conf.d/resume to match the name that the encrypted swap partition would have after doing cryptsetup from the crypttab. (The swap partition's UUID did not go in there yet).

I used dpkg-reconfigure initramfs-tools to rebuild the helper's initramfs images. That may not have been a particularly wise move, but trusting that that device wouldn't need a reboot soon I took the risk temporarily. (Those images are a bit pesky to edit by hand; were that easier, I could have edited them on the USB stick and be done with it).

Fortunately, the updateinitramfs process run in that update pointed out an issue to me: It couldn't find the indicated resume device (how would it, it's on another machine), and fell back to a sane default.

That meant that resume would not work -- the gravest danger of this operation, as it would mean I'd lose the resume state.

Final startup

Fortunately, the resume partition can also be specified on the kernel command line, which is where the swap partition's UUID came in handy:

echo 'Loading Linux 5.6.1-1-amd64'
linux (hd0)/vmlinuz-5.6.0-0.bpo.2-amd64 root=-/dev/mapper/hephaistos--vg-root ro quiet resume=UUID=3c7f3271-f54f-41f0-8ec5-aee20977418e
echo 'Manually added initrd'
initrd (hd0)/initrd.img-5.6.0-0.bpo.2-amd64

With that, my system came up as it should be.

Of course, first thing I did there was dpkg-reconfigure initramfs-tools to ensure it would come up again.

Second thing, I restored the helper PC, or I'd have needed to do the same thing the other way 'round once more later.


Monday, 2018-05-07

Using TOR like a VPN to make SSH available

Back in the day, the machines I used to maintain either had public IP addresses or were local to my home network; that has changed. I still want to regularly log in to devices "on the road" by means of SSH.

The approaches I considered were:

  • "Use the (modern) internet": Only consider IPv6 connections where there is no NAT any more, have the devices announce themselves via some kind of dyndns (TSIG'd AXFRs seem to be the way to go)), and tell their firewalls to let SSH traffic pass (UPNP might be the way to go there).

    I did not follow thisapproach because there are still backwards internet providers that don't provide IPv6, and because especially mobile providers have a tendency not to implement port opening protocols.

  • "Use a VPN": Set up a VPN server, assign addresses based on host certificates.

    Would have worked, but I dreaded the configuration effort and running an own central server.

  • "Use TOR hidden services": Run TOR on all boxes and announce a hidden service that runs SSH.

    A helper script and custom DNS data allow using this without any per-server configuration on the clients.

    That's the approach I'm describing here.


Even though this may sound secure due to the use of TOR hidden services, it does not attempt to be. I would have been perfectly OK with just opening the SSH port to the network (relying on SSH to be secure and not needing any more authentication from the client) and announcing that address, and that's the only security level I expect. Do not follow the steps shown here without understanding what they mean and the implications on your device's security.

Server setup

On Debian, making SSH available via TOR is really straightforward:

  • Configure SSH (/etc/ssh/sshd_config) in a way you want to have public-facing. All I changed from the default config was setting PasswordAuthentication no to avoid pwnage from insecure guest / testing accounts.

  • Start a TOR hidden service. In /etc/tor/torrc, there are already commented out examples for that. In line with them, I added:

    HiddenServiceDir /var/lib/tor/hidden_service/
    HiddenServicePort 22

After restarting SSH and TOR, the .onion name for that host can be read from /var/lib/tor/hidden_service/hostnme; I'll be using abc1234567890.onion as an example name here.


It's not a fully hidden service

The service run here should not be mistaken for having any privacy properties in the sense of hiding the server. The service uses the same host key on TOR and in the LAN, so everyone who knows your host key will know who runs this servicE. toR is used only for discovery here.

TOR can probably be configured for the hidden service to only use one hop, this could increase performance and decrease the load on the TOR network; I did not get around to trying this yet.

Client setup I

For a first test, I set up my SSH client manually in ~/.ssh/config:

    ProxyCommand nc -X 5 -x localhost:9050 abc1234567890.onion 22

After that, ssh always went through TOR.

For some setups, that may be sufficient; I chose to take it a little further.

DNS setup

As I prefer not to have explicit configuration about every server on every client machine (as I would need to with the above), I decided to publish the .onion names of the hosts; the most logical choice seems to be in DNS.

To avoid confusing other services, I went for a mechanism that looks a bit like DNS-SD (but is not, as .onion addresses are not "real" host names, and SSH does not use DNS-SD anyway). In my zone file for, it looks like this: IN TXT abc1234567890.onion

Those I publish with knot DNS, but any DNS server should be able to take those as static records.

Client setup II

I now only keep a single stanza in my SSH config entry:

Host *
    ProxyCommand ~/.config/maybe-tunnel %h %p

This makes all connections go through an (executable) helper script of the following content:

set -e

if getent hosts $1 >/dev/null
        exec nc $1 $2
        torhost="`dig +short TXT _ssh._onion."$1" | grep \\.onion | sed 's/"//g'`"

        if [ x = x"$torhost" ]
                echo "Host $1 could not be resolved, and has no _ssh._onion TXT record either." >&2
                exit 1
        exec nc -X 5 -x localhost:9050 "$torhost" $2

This script looks up the configured host name. If it resolves, it uses nc to open a direct connection, emulating what SSH does by default. (If SSH accepted dynamic configuration, I would just not set a ProxyCommand at all in this case). This covers both the case of "regular" servers that have public IPs, and also makes local connections work, because as long as I am inside the LAN and the other machine is too, the internal dnsmasq server will answer the request with the local address.

If the address does not resolve, the script looks for an _ssh._onion record that would tell the address of a hidden service announced as per the above. If one is found, a connection is established using the local TOR instance (running on port 9050 by default).


SSH host keys

As I use this setup right now, I either establish the hosts' identity once-per-client when I first use them in the LAN (so their public host keys get stored in my user's known_hosts file under their name), and/or I store their public keys in the known_hosts file I deploy to my boxes.

If one uses DNSSEC, it should be feasible to announce SSHFP records for (even though there's no A/AAAA record for that host) and trust those.


This does not interact well when both client and server are in the same public WiFi and could discover each other locally (they'd still go via TOR).

It would be interesting to extend this to proper DNS-SD discovery with multicast -- essentially, once a host has a known key (from TOFU or SSHFP records), whatever has no A/AAAA record would be up for local discovery, falling back to TOR based discovery.

TOR was what worked easy and fast, but its onion routing properties are not really being used here. Other schemes that "just" provide a TCP turnover point might be more convenient here. Obviously, if anything were to be usable without per-client customization, that' be great, but as SSH does not really support virtual hosting, that might be tricky.

Final words

This is an experimental setup I have just established, and am rolling out to my mobile devices; I have no practical experience with it yet.

I'd appreciate any comments on it to (preferably by I will post updates here if something significant changes.


Tuesday, 2016-06-28

Obervations from playing around with a Beaglebone Black

I've toyed around with a Beaglebone Black for some days, evaluating its usefulness as a living room box. (Long story short: It's unusable for that, because it can drive only up to 720p due to pixel clock limits, lack of hardware video decoding support and lack of practical [1] 3D acceleration).

(Long-term note: This describes the state of things around 2016-06-28. In some months, this is hopefully only historically relevant, and might be shortened.)

Nevertheless, I've learned some things:

  • Serial Debugging is easy to connect once you have a 3.3V USB UART -- the USB UART's GND goes to J1, RX goes to J5 and TX goes to J4, where J1..6 is the pin header next to the socket bar between USB host and 5V, and only J1 is labelled.

    Except for debugging, it is not really required, though; for most of what is described on the rest of this page, one can do without.

  • The beaglebone by default executes the U-Boot loader stored on the internal flash memory (eMMC); pressing the boot select button S2 during powerup (reset doesn't count) alters the boot sequence to skip that. This first stage of the booting process is goverened by a program in ROM that I haven't seen anyone try to manipulate yet. (Also, why should one -- it behaves sanely and not touching it makes the device unbrickable for all I can tell).

    • When trying to boot from an MMC device (either the builtin eMMC called mmc0 or the microSD card called mmc1; eMMC and SD cards are rather exchangable as far as the CPU is concerned), the startup code (SPL/MLO (see later), U-Boot, U-Boot config) may either reside on the first and FAT formated partition, or at some fixed offsets between the partition table and the first partition. (So if you wind up with arguments in the kernel cmdline you fail to find in the mounted image, have a look at the intermediate space with a hex editor).
    • Failing that (eg. because no microSD card is inserted and S2 was pressed), the ROM fires up an Ethernet interface via the USB gadget side (ie. when the mini-USB port is connected to a PC, a new USB network card appears there) and asks for the SPL/MLO via bootp.

    The Alexander Holler's boot explaination image visualises this well.

    Both the MMC and the USB networking boot flow take an intermediate step (the secondary program loader, SPL; file name MLO) before loading full U-Boot (file name u-boot.img or similar). The SPL is actually a U-Boot itself, just stripped down to run from on-board RAM; it then initializes the rest of the RAM and loads the fully configured U-Boot (with debug prompt and device support for everything else) to continue.

  • It is possible to do the entire flashing without possession of a microSD card, but it's not a widespread way of operation and thus you might encounter some bugs.

    The following workflow shows how to flash the provided Debian image right into eMMC.

    • Obtain a copy of the compressed image file from the latest-images page.

    • Get a copy of the spl, uboot and fit files from the BBBlfs bin/ directory. (There's a copy inside the image at partition 1 in /opt/source/BBBlfs, that should work as well and can be extracted using kpartx and loop mounting). Rename fit to itb because that's the file name needed later.

    • Prepare a DHCP server for the incoming USB connections. I generally find it practical to have a device independent Ethernet setting which I call "I am a WiFi adapter" in NetworkManager, which is configured to have IPv4 "Shared to other computers" -- setting that to "automatically connect to this network when it is available" for the time of playing around is one of the easiest ways to get a DHCP server running.

    • Tell the server to ship the required images by creating a /etc/NetworkManager/dnsmasq-shared.d/spl-uboot-fit.conf file, which modern NetworkManager versions include to every started DHCP server, or include the sniplet into your dnsmasq config if you start it manually:

      dhcp-vendorclass=set:want-spl,AM335x ROM
      dhcp-vendorclass=set:want-uboot,AM335x U-Boot SPL

      This will serve the right file at the right time.

    • Wire up the BeagleBone's mini-USB to your workstation and keep S2 pressed while plugging in. You should see NetworkManager take the interface up and set it to "I am a WiFi adapter" (or manually do that if you didn't configure autoconnect).

      If the connection fails to come up / disappears, check your NetworkManager log files (eg. at /var/log/daemon.log); chances are that dnsmasq does not come up because it can't access the TFTP root path. If it does come up, it should disappear again soon afterwards, and you should see a line like

      dnsmasq-tftp[...]: sent /some/path/bin/spl to 10.42.0.x

      which indikates that the first stage loader asked as expected and got its response. Following that, a new USB network interface should appear, and you should see the same line just with uboot instead of spl. (Without autoconnect, you'd have to nudge NetworkManager again).

    • Due to what I consider a bug in BBBlfs, we need to take a detour (see bug details on the why, this is just the how of the workaround):

      sudo /etc/init.d/network-manager stop
      sudo ip address add dev enp0s20u1c2
      sudo dnsmasq --no-daemon --port=0 --conf-dir=/etc/NetworkManager/dnsmasq-shared.d

      Once that too shows a "sent" line, end dnsmasq with Ctrl-C; you can start NetworkManager again. (There's a timeout of guesstimated one minute in this step, after which the cycle starts from the beginning. If you don't get a "sent" line from the dnsmasq, check whether you properly renamed the fit file.)

    • You should now see a new block device (/dev/sdc for me) appear, which represents the eMMC flash memory on the BeagleBone. If you have a serial console attached, you are dropped in a linux root shell on the BeagleBone. (What is running there is actually "just" a linux kernel with a g_ether module configured to expose /dev/mmcblk0).

      With that, you can flash images onto eMMC (follow the steps further) or mount and modify existing installations (eg. change the root password you were sure not to forget and have no clue about a year later).

    • You can now flash the image using sudo dd if=bone-debian-8.4-lxqt-4gb-armhf-2016-05-13-4gb.img of=/dev/sdX -- and double-check whether you're writing to the correct sd-whatever device, dd is short for "disk destroyer".

    • The current images have a (uboot config?) bug that results in the wrong root= argument being passed to the kernel. This can be circumvented by telling U-Boot to use disk UUIDs instead of device names [2]:

      Mount /dev/sdc1 (current bone Debian images have exactly one partition) and edit its /boot/uEnv.txt. There should be a commented-out line uuid=; remove the comment mark (#) and append the UUID of the partition obtained by the blkid /dev/sdc1 command.

      Unmount the disk, wait until the LEDs on the bone only flicker regularly, and then power-cycle it. The device should now boot into a full Debian that can be used from an HDMI attached monitor and USB mouse / keyboard.

[1]There is only a non-free driver for the SGX/PowerVR builtin 3D accelerator, but that means I'd need to wait for vendor provided drivers for each kernel update the device would see during its regular lifetime and then still not retain full kernel support.

The images' bug tracker requires administrator approval for login, until then here's what it'll say:

The U-Boot config shipped before the Debian images' first partition currently uses U-Boot's MMC numbers to construct an oldroot /dev/mmcblk${mmcdev}p${mmcpart} name. When the image is flashed directly into eMMC and not onto an SD card, the kernel boots into the initrd, but lacking presence of an SD card, the MMC block device of mmcdev=1 gets assigned the device name /dev/mmcblk0; consequently, booting gets to a halt when the initrd's scripts complain that root was not found. (I've only observed the symptom and pieced together the MMC number explanation from the U-Boot environment variables, I didn't step through them precisely).

Until or unless the oldroot mechanism can be fixed to work independent of SD card presence (maybe /dev/disk/by-path is more stable?), the easiest workaround would be to always set the uuid in /boot/uEnv.txt to reflect the disk's UUID; then, the oldroot mechanism doesn't get used.


Wednesday, 2015-03-18

Switching terminal emulator: urxvt to xfce4-terminal

I have just switched my terminal from urxvt to xfce4-terminal.

Some considerations that led to the decision:

  • urxvt's annoying Ctrl-Shift behavior: Just because I want Unicode displayed, I don't necessarily want to use all the input methods, which get in one's way and are hard to disable. 393847 is a workaround, but not much help for 256 color users. (That was what pushed me over the switching inertia.) If I want to input 🂭, I use gucharmap.
  • Switching font sizes is easy with Xfce terminal.
  • Standard desktop software: As much as I like my custom desktop, I try to stick to non-exotic components where practical; Xfce is available on most desktops around me.
  • I don't need terminal features like scrollback or tabs, because I use tmux both locally and remote for scrollback and multiple shells in a single window.
  • Both feel equally snappy, provide support running a single process for many windows, support 256 colors, can be configured not to show anything but the text, and behave well with respect to re-wrapping text (at least when used with tmux).

Monday, 2013-03-04

ARandR released

…with the following changes since 0.1.4 (the intermediate releases were not important enough to get own release announcements):

  • Merged parts of the cglita branch
    • solves ValueError / "1080p" issue (Debian bug #507521)
  • Fix the 'primary' issue
    • ignores the primary keyword
    • makes ARandR compatible with xrandr 1.4.0
  • Show the entire output submenu as disabled instead of the "Active" checkbox
  • New unxrandr tool
  • New translations:
    • Bosnian (by Semsudin Abdic)
    • Breton (by Belvar)
    • Dutch (by wimfeijen)
    • Galician (by Miguel Anxo Bouzada)
    • Greek (by Dimitris Giouroukis)
    • Hungarian (by Tamás Nagy)
    • Japanese (by o-157)
    • Korean (by ParkJS)
    • Lithunian (by Algimantas Margevičius)
    • Persian (by Alireza Savand)
    • Romanian (by sjb and Себастьян Gli ţa Κατινα)
    • Slovak (by Slavko)
    • Swedish (by Ingemar Karlsson)
    • Ukrainian (by Rax Garfield)
  • Updated translations:
    • French (by Bruno Patri)
    • Lithuanian (by Mantas Kriaučiūnas)
    • Persian (by Alireza Savand)
    • Spanish (by Miguel Anxo Bouzada)
  • Other small bugfixes

Saturday, 2012-08-18

Window manager changed to i3: scratchpad tricks

Sticking to what seems to be my two-year window manager changing schedule, I switched again and ended up with i3. Xmonad was ok, performance- and feature-wise, but I didn't find the time to learn Haskell to be really able to configure it properly.

i3 has some properties I particularly like:

  • The way it handles multi-monitor setups: Every workspace gets assigned to an output; switching to a workspace on another output opens the workspace there and warps the focus to that output.
  • Layouting is so much more flexible, though I still somewhat miss my Super+Return "swap the current window with the big one" of Xmonad.
  • The scratchpad: a place to send a window where it can easily be recalled as a popup to the desktop. To be fair, it should be possible to implement the same thing with Xmonad or awesome, but here, it's usable without learning a whole language.

I use the scratchpad as a more powerful replacement for dmenu_run, which under high IO load can take long time to fire up: I keep a terminal with tmux ready at the scratchpad, which is configured to hide itself again as soon as a command is executed, and to spawn another tmux window to be ready for the next input.

Usability-wise, it's like running dmenu or gmrun (which I used some time ago): You press Super-p, a window shows up, you enter your command, press Return, the window vanishes and the command gets executed. In addition, it offers

  • a real shell, with all its tab completion powers, aliases etc., and
  • a way to recover started processes' stdout/err data, even if they crash (the windows are kept around for some minutes).

The code for that is pretty short, and most of it is to cover corner cases. This file is ~/.config/i3/scratchpad/ and is best executed from i3 under a flock guard:



run_inside="'ZDOTDIR=${HOME}/.config/i3/scratchpad zsh'"

while true; do
    # not using urxvtc here, as we're relying on the process to run
    # until either
    # * it gets detached (eg by ^Ad)
    # * it terminates (because someone killed all windows)
    # in any case, we try to reattach to the session, or, if that fails,
    # create a new one.
    urxvt -name scratchpad -e sh -c "tmux attach -t scratchpad || tmux new-session -s scratchpad $run_inside ';' set-option -t scratchpad default-command $run_inside"

And this file, ~/.config/i3/scratchpad/.zshrc, configures the shell that gets executed:


source ~/.zshrc

# reset again what was set outside
export ZDOTDIR=${HOME}


preexec() {
    i3-msg '[instance="^scratchpad$"]' move scratchpad
    tmux new-window

    precmd() {
            echo "=============================="
            echo "exited with $?"
            exec sleep 10m

zshexit() {
    # user pressed ^D or window got killed, will not be executed when the
    # shell gets "killed" by the final exec in the regular workflow
    i3-msg '[instance="^scratchpad$"]' move scratchpad
    tmux new-window

    # when the window gets killed, just die
    zshexit() {}

To activate this from i3, the following lines are placed in the i3 config:

bindsym $mod+p [instance="^scratchpad$"] scratchpad show
exec_always flock -w1 ~/.config/i3/scratchpad/lockfile ~/.config/i3/scratchpad/
for_window [instance="^scratchpad$"] move scratchpad

All of the above requires zsh, tmux and urxvt to be installed, zsh to be configure as login shell and possibly other things I might have forgotten (please let me know if you have trouble reproducing this setup).

Other i3 features that look good and I plan to discover:

  • Easy to use modes -- maybe I'll revive my key combos of old (Super+Space, g, P for "games / pioneers server", Super+Space, Print, u for "take a screenshot, upload it to my server and put the URL into my clipboard" etc.
  • VIM-like marks

An issue I yet have to find a solution for is the status bar: I want nice graphs of CPU usage etc, so the tiling window manager typical solutions like dzen2 or i3's i3status/i3bar won't do the trick. I'm using conky for the moment, but I still need i3bar because it has a realtime display of the active workspace and of urgency notifications. Ideally, I'd want i3bar to provide an X window to conky where it can draw, but for the moment, I'm keeping a single statusline worth of visual information in two distinct panels.

Saturday, 2010-12-11

ARandR 0.1.4 released

This is a bugfix / translation update release with the following changes since 0.1.3:

  • Fix for "unknown connection" bug
  • New translations:
    • Russian (by HsH)
    • French (by Clément Démoulins)
    • Polish (by RooTer)
    • Arabic (by Mohammad Alhargan)
    • Turkish (by Mehmet Gülmen)
    • Spanish (by Ricardo A. Hermosilla Carrillo)
    • Catalan (by el_libre)
    • Chinese (by Carezero)
  • Translation updates:
    • Danish
    • Brazilian
    • Arabic (by aboodilankaboot)

I am sorry to disappoint those that wait for the "next big" release (with enhanced parsing to fix #507521 and support for xrandr 1.3 features like --primary) – while those issues are as important as the bug fixed here, this was a "low hanging fruit" and a welcome occasion to do a release with all the new translations. Big thanks to all translators!

Tuesday, 2010-09-07

Window manager changed to xmonad

After having used awesome for more than two years, I just switched to xmonad. Main reasons for the change were the configuration mess (which rendered my carefully crafted CPU and memory indicators useless in one of the last major upgrades) and the way awesome handles multi-monitor setups (boils down to reloading after adding or removing an output).

The packages I use are:

   suckless-tools # provides dmenu-run configured on Win-P
   gmrun # confiured on Win-Shift-P
   dzen2 # menu bar
   # needed for custom configuration:

My xmonad configuration looks like this:


import XMonad
import XMonad.Hooks.DynamicLog
import XMonad.Hooks.ManageDocks
import XMonad.Util.Run(spawnPipe)
import XMonad.Util.EZConfig(additionalKeys)
import System.IO
import qualified Data.Map as M
import XMonad.Actions.CycleWS
import XMonad.Actions.CycleRecentWS
import XMonad.Hooks.DynamicLog   (PP(..), dynamicLogWithPP, wrap, defaultPP)
import XMonad.Hooks.UrgencyHook

-- keybindings: similar to the awesome default config i got used to
-- ---------------------------------------------------------------------------

myKeys conf@(XConfig {XMonad.modMask = modm}) = [
    ((modm, xK_Down), nextScreen),
    ((modm .|. shiftMask, xK_Down), shiftNextScreen),
    ((modm, xK_Up), prevScreen),
    ((modm .|. shiftMask, xK_Up), shiftPrevScreen),

    ((modm, xK_Left), prevWS),
    ((modm .|. shiftMask, xK_Left), shiftToPrev),
    ((modm, xK_Right), nextWS),
    ((modm .|. shiftMask, xK_Right), shiftToNext),

    ((modm, xK_Escape), toggleWS),

    ((modm .|. shiftMask, xK_b), spawn "epiphany -p")
newKeys x  = M.union (keys defaultConfig x) (M.fromList (myKeys x))

-- main program
-- ---------------------------------------------------------------------------

main = do
    statusBarPipe <- spawnPipe statusBarCmd
    xmonad $ withUrgencyHook dzenUrgencyHook { args = ["-bg", "darkgreen", "-xs", "1"] }
            $ defaultConfig {
            modMask = mod4Mask, -- windows key
            terminal = "uxterm",
            manageHook = manageDocks <+> manageHook defaultConfig, -- autodetection for gnome panel and similar
            layoutHook = avoidStruts  $  layoutHook defaultConfig, -- adds gaps for panels
            keys = newKeys,
            logHook = dynamicLogWithPP $ myPP statusBarPipe

-- status bar on top
-- ---------------------------------------------------------------------------

statusBarCmd= "dzen2 -p -h 16 -ta l -bg '" ++ myNormalBGColor ++ "' -fg '" ++ myNormalFGColor ++ "'"

-- helper function (my first haskell function *g*)
-- as ppUrgent is applied after ppHidden, the dzen markup has to be removed and new stuff added

dzenStripWrap :: String -> String -> String -> String
dzenStripWrap _ _ "" = ""
dzenStripWrap l r m = l ++ dzenStrip m ++ r

-- status bar design
-- based on's_xmonad.hs

myNormalBGColor     = "#2e3436"
myNormalFGColor     = "#babdb6"
myEmptyBGColor      = myNormalBGColor
myEmptyFGColor      = "#7478f6" -- (myNormalBGColor + myNormalFGColor) / 2

myFocusedBGColor    = "#414141"
myFocusedFGColor    = "#73d216"
myOtherscreenBGColor= myFocusedBGColor
myOtherscreenFGColor= myNormalFGColor
mySeperatorColor    = "#000000"
myUrgentBGColor     = "#0000ff"
myUrgentFGColor     = "#f57900"

-- pretty printer

myPP handle = defaultPP {
        ppCurrent                   = wrap ("^fg(" ++ myFocusedFGColor ++ ")^bg(" ++ myFocusedBGColor ++ ")^p(4)") "^p(4)^fg()^bg()",
        ppUrgent                = dzenStripWrap ("^fg(" ++ myUrgentFGColor  ++ ")^bg(" ++ myUrgentBGColor  ++ ")^p(4)") ("^fg(" ++ myUrgentFGColor  ++ ")^bg(" ++ myUrgentBGColor  ++ ")^p(4)^fg()^bg()"),
        ppVisible               = wrap ("^fg(" ++ myOtherscreenFGColor  ++ ")^bg(" ++ myOtherscreenBGColor  ++ ")^p(4)") "^p(4)^fg()^bg()",
        ppHidden                = wrap ("^fg(" ++ myNormalFGColor  ++ ")^bg(" ++ myNormalBGColor  ++ ")^p(4)") "^p(4)^fg()^bg()",
        ppHiddenNoWindows       = wrap ("^fg(" ++ myEmptyFGColor  ++ ")^bg(" ++ myEmptyBGColor  ++ ")^p(4)") "^p(4)^fg()^bg()",
        ppWsSep     = "", -- explicit ^p(4) above provide coloured border
        ppSep     = "^fg(" ++ mySeperatorColor ++ ") * ^fg()",
        ppLayout = (\x -> ""), -- i usually see which layout is used and don't need an extra display
        ppTitle   = wrap ("^fg(" ++ myFocusedFGColor ++ ")") "^fg()" ,
        ppOutput  = hPutStrLn handle

All together, the transition was quite painless. I still miss my window list from awesome (the window list from gnome-panel seems not to work with xmonad) – any ideas how to get one easily?

Friday, 2010-03-12

ARandR 0.1.3 released

… with the following changes since 0.1.2:

  • Prevent too large window size requests
  • New translations:
    • Italian (by Quizzlo)
    • Brasilian (by Phantom X)
    • Danish (by Joe Hansen)
    • Kannada (by gundachandru)
  • Better translation infrastructure
  • man page enhancements
    • man page generation now depends on docutils (>=0.6)
  • New source code repository at