Generating KWin window rules through scripting


Introduction #


If you're a KDE user you might have played around with the window rules system. It's a powerful system that allows you to fine-tune the behavior of windows on your desktop. It's a neat utility, especially since certain tweaks can only be accessed through window rules. But after tweaking window behavior to my heart's content, what if I want to use these rules elsewhere? What about the portability of these window rule configurations, and what can we do to make it better (and does it really matter)?

Using the GUI #


Firstly, what do I mean by portability here? If you've got multiple computers in use, you likely also want largely the same setup across all of them. Perhaps you want to share your window rules as part of some application. In any case, you'd want a quick and easy way of moving and applying these rules. So what are we offered by KDE? Let's look at the system settings:

Obviously, we have options to import and export our rules. Exporting any number of rules leaves us with a .kwinrule file, with all our rules compiled together, looking something like this:

[LINVER_RULES]
Description=LINVER_RULES
clientmachine=localhost
minimizerule=2
wmclass=linver
wmclassmatch=1
[POLKIT_RULES]
Description=POLKIT_RULES
clientmachine=localhost
minimizerule=2
wmclass=(polkit-kde-authentication-agent-1)|(polkit-kde-manager)|(org.kde.polkit-kde-authentication-agent-1)
wmclassmatch=3
I'm not exactly sure how clientmachine is used in practice, so for the sake of this blogpost, we'll conveniently ignore it.

Importing from the GUI also works perfectly as expected, correctly skipping rules with the same name that have already been added.


Simple and intuitive.


Using the terminal #


Obviously, the GUI works perfectly and the entire section above is redundant as everyone already intuitively knew these things. It's the easiest way to include any set of window rules. But it requires opening the system settings, navigating to the window rules page, clicking on the import button and selecting the right file. It's kinda slow, especially if there are multiple files that need to be imported, as the import file chooser doesn't enable multi-select. Maybe I just want to be a Linux nerd and use the terminal to solve all my problems.

Unfortunately, as far as I'm aware, there is no such way of adding KWin rules through the terminal. We'll have to come up with our own.

What's the use case? #

KWin is incredibly versatile and powerful in many aspects, while paradoxically also being rather limiting in other aspects, for reasons unclear to me. One such example, and the primary reason behind all this, is the (in)ability to assign a minimize policy for windows. No such thing is available in QtWidgets or QtQuick, and KDE APIs don't provide this either*. The only way to do this reliably is through KWin rules. If I'm developing a graphical application that shouldn't be minimizable, it would be neat to provide a KWin rule that gets automatically included upon compilation/installation.

*At least, not portably. KX11Extras in theory provides methods that allow changing window attributes, some of which might indirectly cause the window to not be minimizable. I haven't tested this, so it's purely speculation. Even so, the Wayland side provides no front-facing solution whatsoever.

The particular example of this is Linver:

The original Winver program was never minimizable, and had its minimize and maximize caption buttons omitted as a result. In order for Linver to achieve the same effect, a KWin rule must be used.

Admittedly, this is a pretty niche use case. How often do you really mess with KWin rules to begin with? Probably almost never. Regardless, in rare situations like this, having a command or shell script that could automate the addition of KWin rules probably makes sense. Most features in KDE (and Linux in general) have a command alternative, at the very least in the form of a dbus call, so it's surprising that that isn't the case here.

kwinrulesrc #

Taking a look at ~/.config/kwinrulesrc, we can figure out how KWin rules are stored:

[13b2019c-7854-4d2c-b952-963254634a52]
Description=LINVER_RULES
clientmachine=localhost
minimizerule=2
wmclass=linver
wmclassmatch=1

[General]
count=2
rules=b8ecb85c-a426-4813-aefe-83b920b8b377,13b2019c-7854-4d2c-b952-963254634a52

[b8ecb85c-a426-4813-aefe-83b920b8b377]
Description=POLKIT_RULES
clientmachine=localhost
minimizerule=2
wmclass=(polkit-kde-authentication-agent-1)|(polkit-kde-manager)|(org.kde.polkit-kde-authentication-agent-1)
wmclassmatch=3

From this example, we can take note of the following:

  • Window rules are identified using UUIDs
  • The General group keeps track of the total available rules

In theory, adding window rules to this file and reloading KWin should be enough, but we can't simply append the contents of our exported files, as they need to be tied to a UUID which is appended to the list of rules in General.

Writing the shell script #

To keep things simple, the scope of the script is simply to add one specific window rule group. Generalizing this further (being able to add, edit, remove, import and export one or more rules) is possible, but that's out of scope for now. The general way to add a window rule group goes like this:

  1. Check if the rule has already been added by searching for the rule's description in kwinrulesrc. The description should be something easily identifiable.
  2. Generate a UUID for the window rule group. Due to the nature of UUIDs, the chances of generating a conflicting UUID is astronomically low.
  3. Read count and rules from General using kreadconfig6.
  4. Increment count and append the newly generated UUID to rules.
  5. Write all the rules and other relevant information to the config file using kwriteconfig6.
  6. Reload KWin configuration using dbus.

This approach effectively generates the window rule as the information is hardcoded into the script, acting as a sort of executable variant of a .kwinrule file. Using Linver again as an example, adding a KWin rule for it would look like this:

#!/bin/bash

RULE_DESC=LINVER_RULES # Description of the rule that we'll treat as unique to prevent duplicates 
CONFIG_DIR=~/.config/kwinrulesrc
SHOULD_ADD=$(grep $RULE_DESC $CONFIG_DIR) # Search for any occurrences of the rule description

if [[ -n "$SHOULD_ADD" ]]; then
	echo "Rule already added, closing..."
	exit
fi

# Read the count value. If it doesn't exist, assume the value is 0
COUNT=$(kreadconfig6 --file $CONFIG_DIR --group General --key count --default 0)
# Likewise for reading the rules list, defaulting to an empty string otherwise
RULES=$(kreadconfig6 --file $CONFIG_DIR --group General --key rules)
UUID=$(uuidgen) # Generate UUID
COUNT=$((COUNT+1)) # Increment count

# Write the rule
echo "Adding KWin rule $RULE_DESC..."
kwriteconfig6 --file $CONFIG_DIR --group $UUID --key Description $RULE_DESC
kwriteconfig6 --file $CONFIG_DIR --group $UUID --key clientmachine localhost
kwriteconfig6 --file $CONFIG_DIR --group $UUID --key minimizerule 2
kwriteconfig6 --file $CONFIG_DIR --group $UUID --key wmclass linver
kwriteconfig6 --file $CONFIG_DIR --group $UUID --key wmclassmatch 1

# Update the General group
kwriteconfig6 --file $CONFIG_DIR --group General --key count $COUNT
kwriteconfig6 --file $CONFIG_DIR --group General --key rules $RULES,$UUID

echo "Reloading KWin..."

qdbus6 org.kde.KWin /KWin reconfigure # Reload KWin's configurations

echo "Done."

The code can be found here.

Conclusion #


The shell script surprisingly works well for what it is, but in all fairness, this is a pretty silly way to add KWin rules, even with the motivation laid out in previous sections of this blogpost. This idea was mostly born out of an urge to automate things that really don't need automating, and slight frustration that something like this isn't already a thing for KWin rules in the first place. Perhaps in the future I will write a more robust program that deals with KWin rules through the terminal, unless KDE developers or someone else beats me to it. Would it be any useful? Maybe.