Whether you’re playing a video game or competing in a constrained attack-defense CTF, your keystroke timings matter. We at waycrate value your precision, to the extent that you can configure your keybindings to perform actions either on a key’s press or a release.

Hi, my name’s Himadri and this post is a part of a series explaining how we (basically just me) are rewriting the config parser for swhkd using EBNF grammar. I highly recommend reading the previous posts because I’ll be referring to them from time to time. In the last post, we talked about regular keys that form the foundation of bindings. However, we glossed over the send and on_release expressions in the code.

The send and on_release attributes are extensions that could be added to regular keys to be more specific about the timing of an event. To make a binding respond to either key presses or releases, they are prefixed with the ~ or the @ characters respectively.

For example, a bindings with that responds to super a can be made to respond specifically to the keypress instead of the key release like the following:

super + ~a
  notify-send 'hello'

Now, to encode this as a formal grammar, we need to observe that these attributes can be used both inside and outside shorthand contexts. This means, the binding declarations super + ~a and super + {@a, ~b} are equally valid.

Intuitively, this begs the question of how keys like ~ or @ could be specified literally. The answer is similar to what we did for commas and dashes in shorthand contexts, we need to escape the keys. The only difference this time is that the keys are escaped both inside and outside shorthand contexts. In retrospective, the plus sign that has been serving as the concatenator also needs to be escaped for literal representation.

To fix this, let’s declare a convenience expression called keys_always_escaped.

keys_always_escaped = _{ "\\~" | "\\@" | "\\+" }

This is how we will allow the user to literally mention a tilde or a plus.

Next, we modify the expression for a regular key to include these escaped literals besides the regular ASCII alphanumeric characters.

We change the expression from

key = { ^"enter" | ^"return" | ASCII_ALPHANUMERIC }

to the following:

key = { keys_always_escaped | ^"enter" | ^"return" | ASCII_ALPHANUMERIC }

Don’t worry, we will add other symbols like semicolons, parentheses and the like to this expression but we are starting off being a bit restrictive so that we can catch errors early.

We have to compensate for this change for the code side as well. This is the first time you’ll see real code from the project besides the formal grammar.

#[derive(Debug, Clone)]
pub struct Key {
    pub key: String,
    pub attribute: KeyAttribute,
}

Notice how the Key variant has a field called attribute of type KeyAttribute. This KeyAttribute is a bitflag represented by a u8 or a single byte. Why a single byte? Because it makes the underlying data fairly inexpensive to copy. Although a boolean value should ideally be represented by a single bit, most modern processor architectures use a single byte to represent them. Bitflags can help us shave off the unused space. We are using macros fromthe bitflag crate since Rust does not natively have C-styled bitflags.

bitflags::bitflags! {
    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
    pub struct KeyAttribute: u8 {
        const None = 0b00000000;
        const Send = 0b00000001;
        const OnRelease = 0b00000010;
        const Both = Self::Send.bits() | Self::OnRelease.bits();
    }
}

According to the bitflag, the variant None is internally represented by a 0, Send is represented as a 1, OnRelease as 2, etc. Since all we care about is whether an attribute is there or not, we can use a single bit as a bin for each attribute. Any time we see one of the variants, we bitwise or the current attribute set to flip the respective bit on.

match inner.as_rule() {
    Rule::send => attribute |= KeyAttribute::Send,
    Rule::on_release => attribute |= KeyAttribute::OnRelease,
    Rule::key => key = pair_to_string(inner),
    _ => {}
}

This saves us from writing cumbersome if statements that would have made more sense if counting the occurrences was involved.

That’s all for today, I hope you were impressed by the bitwise trick. In the next post, I will talk about how I’m implementing the grammar for modifier keys and how they can be different from regular keys. See you soon!