If you’re using KWin compiled from trunk, and if you regularly update it or have atleast updated it once in last 40-50 days your installation of KWin already has scripting support. If you’re reading this, you probably already know what benefits from scripting and I’ll just skip that and get to the real thing. So here comes the KWin Scripting tutorial.
EDIT: Fixed a few errors and a mistake in the code. Thanks to hunt0r for finding it out.
S1. The KWin script loading procedure
When KWin loads it looks in a folder for scripts it should run. This folder is given by: $KDEHOME/share/apps/kwin/scripts/. For me, this expands to: /home/kde-devel/.kde4/share/apps/kwin/scripts/, or in most cases, this should expand to something like: /home/<user>/.kde4/share/apps/kwin/scripts. Let’s call this directory ‘KWINSCRIPTS’ for now. All throughout this tutorial, I’ll be referring to this directory as KWINSCRIPTS.
Now, here is how KWin loads the scripts:
- Locate the $KWINSCRIPTS directory.
- Make a list of all the files in the $KWINSCRIPTS directory. For a file to be recognized and executed as a script, it must meet the following conditions:
- It must have one of the following extensions: .kwinscript, .kws or .kwinqs
- It must have the executable flag set.
- For every file with name as ‘scriptfile.kwinscript’, ‘scriptfile.kwinqs’ or ‘scriptfile.kws’, a configuration file ‘scriptfile.kwscfg’ is loaded as its configuration file and all its keys and values are loaded.
- All files recognized as scripts are executed one after the other.
In this tutorial, configuration files and it’s loading is not covered. Details about configuring your KWin scripts are available here.
S2. Setting up your development environment
For the purposes of this tutorial, we’ll create a script file ‘tutorial.kwinscript’ or anything you may prefer. Save it to the KWINSCRIPTS folder. Also, do set the executable bit for the scripting file using: chmod +x tutorial.kwinscript. In the script ‘tutorial.kwinscript’, enter the following code:
print("Hello World");
The phrase ‘Hello World’ may have become too old and repetitive, but we may break laws, we may break promises, but we never ever break tradition. So, Hello World it is. You may either have another account for KDE-development or you may be a KWin user having a single KDE installation. If you have a single KDE installation, I would suggest that you create another account for KDE-development, the details for which can be found here: http://techbase.kde.org/Getting_Started/Build/KDE4.x. This is so because KWin scripting is still in development and some nasty scripts may cause a crash or some other similarly crippling behaviour. Whenever testing a script or changes to a script, use a terminal to reload kwin. The reason to do so is so that you can read the output from a script on the terminal. Any scripting errors are also reported to the stdout, so it is helpful if you run it within a terminal. To load kwin, open Konsole and just type in:
kwin --replace &
This will reload the scripts and execute them again. For your first script, let us follow these steps:
- First create a file in KWINSCRIPTS called ‘tutorial.kwinscript’.
- Edit tutorial.kwinscript and add the following lines:
print("Hello World"); - Save the file, and set executable permissions on the file using
chmod +x tutorial.kwinscript. - Open up Konsole (or switch to the user account you develop on and then start up Konsole) and type in the following line:
kwin --replace &
- In the terminal, you should see the line ‘Hello World’ printed. If you see it, Congrats! You’ve just written and run your first KWin script.
Output from the terminal is pretty useful when it comes to debugging your script. Here is how my terminal looks after attaching an event which prints a line everytime a client is minimized alongwith its caption:
This is the development environment you’ve setup and this is what we’ll be using for the rest of this tutorial. If KWin crashes, you can try moving your mouse over the Konsole window which launched KWin. This will most probably return focus to the window and then you can try entering kwin --replace & again. Since focusing is handled by the Window manager, when KWin crashes focus issues are common and you need a little bit of experience and luck at times
. I suggest you read the KWin/HACKING file located here: http://tinyurl.com/kwinhacking.
S2/1. KWin scripting basics
To follow this tutorial, you must have some idea about ECMAScript [or JavaScript]. A quick introduction can be found on the KDE wikis under Plasma scripting tutorial.
Types: In KWin scripting, there are three kinds of variables:
- Floating
- Singleton
- Instantiable
Instantiable objects are your regular objects, instances of which you can create using the var x = new X(); syntax. Examples of instantiable objects would be QTimer and ClientGroup.
Singletong objects are those objects who have only one instance of their type. For example, there is only one ‘workspace’ objects.
Floating objects, quite unique to the KWin scripting itself are objects that although have many instances, but none of them can be created. These can only be obtained as the return values of various methods.
Events: KWin scripting is completely event driven. Now there are quite a number of events within KWin scripting, and a lot more to be added. The thing to note here is that some events are availabe both system-wide and specific only. The previous sentence might seem a little confusing. Allow me to explain. Take the case of an event ‘A client is minimized’. KWin scripting provides two kinds of events:
workspace.clientMinimizedis emitted system-wide i.e. whenever any client is minimized throughout the workspace.myclient.minimizedis specific to the object it is called from, in this case ‘myclient’. It is emitted only and only when ‘myclient’ is minimized.
To handle events, one must simply use the object.event.connect(..) syntax. Let’s say, whenever I minimize a client, I want to print it’s caption. Here’s the code:
workspace.clientMinimized.connect(function(client) {
print(client.caption());
});
The print function prints to your terminal and is extremely useful for debugging. In the code snippet above, an anonymous function is connected to the event worksapce.clientMinimized, which is emitted whenever a client is minimized. The function which is called is passed as a parameter the client that was minimized. In case of the other event, no such parameter is passed, as we obviously know the specific client which is being called:
myclient.minimized.connect(function() {
print("Minimize a client!!");
});
However, even if we, as programmers do know which client was minimized, the function cannot access that client’s object. This is because the function when it is called does not have the same scope as the connecting function. To overcome this, we can specify a ‘this’ object for the event handler as following:
myclient.minimized.connect(myclient, function() {
print("Minimized: [" + myclient.caption() + "]");
});
To understand which parameters are passed to the event handlers (i.e. the functions we connect to), one can always refer the KWin API docs.
S3. Your first (useful) script
In this tutorial we will be creating a script based on a suggestion by Eike Hein (thanks Eike!). In Eike’s words: “A quick use case question: For many years I’ve desired the behavior of disabling keep-above on a window with keep-above enabled when it is maximized, and re-enabling keep-above when it is restored. Is that be possible with kwin scripting? It’ll need the ability to trigger methods on state changes and store information above a specific window across those state changes, I guess.”
Other than the really function and useful script idea, what is really great about this is that it makes for a perfect tutorial example. I get to cover most of the important aspects of KWin scripting while at the same time creating soemthing useful.
In this tutorial, we will cover the script in two sections: first, we will make the script using plain KWin scripting and in the 2nd part, we will implement the same functionality using Chelate. So let’s get on with it…
NOTE: To follow this tutorial, you must update your kwin from trunk very recently (minimum revision 1191158) as the primary events/methods used in this tutorial have been committed recently.
S3/1. The basic outline
Design statement: For every window that is set to ‘Keep Above’ others, the window should not be above all windows when it is maximized.
To do so, this is how we’ll proceed:
- Create an array of clients whose ‘Keep Above’ property is set.
- Whenever a script loads, make a list of all clients whose ‘Keep Above’ property is set and add it to the array.
- Whenever a client is maximized, if it’s ‘Keep Above’ property is set, remove the keepAbove property.
- Whenever a client is restored, if it is in the ‘array’, set it’s keep above property.
- Whenever a new client is added, check and see if it needs to be added to the array (depending on whether it’s ‘Keep Above’ property is set or not).
S3/1/1: The basic framework
So, for first steps, let us just create an array:
var ka_clients = new Array();
Now, we need to make a list of all available clients whose keep above property is set. From the KWin API docs you can lookup the workspace.getAllClients() method:
Allow me to present a small primer on how to read the KWin API docs. Here, the first line mentions the method, which is ‘workspace.getAllClients’. The ‘ret’ specifies the type of value that will be returned by the method. Here, it says ret: Array(client), which means that an array of client objects will be returned. It takes one parameter desktop_no, which specifies which desktop to fetch the clients from. The rest is clear from the description itself. So, to check all the clients whose ‘Keep Above’ property is set, we will use the following piece of code:
var c = workspace.getAllClients();
for(var i=0; i<c.length; i++) {
if(c[i].keepAbove()) {
print(client.caption() + " (yes)");
}
}
Explanation:
- First, it gets a list of all clients available from all the desktops and stores it in a variable ‘c’.
- Then it loops over the entire array and checks for clients whose ‘keepAbove’ property is set. To do this, one can use the client.keepAbove method. It returns true if the given clients ‘Keep Above’ property is set.
- Then it simply prints the clients caption.
Here, every element of ‘c’ is an object of type ‘client’. Every ‘client’ object represents a window. Here is how my terminal looks after writing this script:
Now, what we need to do, is add every such client to the array ka_clients. So this is how our code will look:
var ka_clients = new Array();
var c = workspace.getAllClients();
for(var i=0; i<c.length; i++) {
if(c[i].keepAbove()) {
ka_clients[ka_clients.length] = c[i];
}
}
What ka_clients[ka_clients.length] does is append an element to that array.
S3/1/2: Manage maximization for clients Now that we have added it to an array, we need to make a provision that whenever the client is maximized, it’s ‘Keep Above’ property does not stay. So, we’ll modify the previous code a little:
function manageKeepAbove(type) {
if((type.h == 1) && (type.v == 1)) {
this.setKeepAbove(0);
}
}
for(var i=0; i<c.length; i++) {
if(c[i].keepAbove()) {
ka_clients[ka_clients.length] = c[i];
ka_clients.onMaximizeSet.connect(c[i], manageKeepAbove);
}
}
For all events, there are two ways to connect it:
object.event.connect(h_function); object.event.connect(f_this, h_function);
In either case, h_function is called whenever the event occurs, but in the second case, the function’s ‘this’ object is set to f_this. We use the second way because as you can see, multiple clients connect to the same function. So whenever a function is called, how does it know which client it must handle? Hence, we must specify the object it must ‘act on’, or in the other words, it’s ‘this object’.
client.onMaximizeSet is an event whenever a client is maximized. There are two parameters while specifying a maximization: horizontal maximization and vertical maximization. The function is passed only one value which has two properties ‘h’ and ‘v’. It specifies in which direction the client occupied the workspace dimension. We want the property to be triggered only when the client is maximized in both the directions. Note here that it is not necessary to write a function of our own in the global scope since we can use anonymous functions also. However, we are writing a function in the global scope so that we can even disconnect this slot later if needed.
Save and run this script and see if it works as one would expect it to.
S3/1/3. Managing clients whose properties which are dynamically set At this point however, if you set the ‘Keep Above’ property for a client, its property won’t be removed on maximization. This is because currently we are only covering the clients which are already present. To cover clients whose property is set after a script has been loaded, we’ll use the event workspace.clientSetKeepAbove. What we’ll do is, for every client whose ‘Keep Above’ property is changed, we’ll add it to the array if it is set, and we’ll remove it from the array if it is unset. This is how we’ll proceed:
workspace.clientSetKeepAbove.connect(function(client, set) {
var found = ka_clients.indexOf(client);
if(set == 1) {
if(found == -1) {
ka_clients[ka_clients.length] = client;
client.maximizeSet.connect(client, manageKeepAbove);
}
} else if(set == 0) {
ka_clients.splice(found, 1);
client.maximizeSet.disconnect(manageKeepAbove);
}
});
Explanation:
- In the event handler for workspace.clientSetKeepAbove, we’ve used an anonymous function, as I mentioned was also possible previously. This is because we want this event handler to be alive throughout the execution of the script i.e. we don’t want to ever disconnect it. In such cases, anonymous functions can be used.
- Here, we simply check for a client. If its property was set, we add it to the array (if it isn’t already there) and it was unset, we remove it from the array (if it is there in the array).
- Also, whenever it is added to the array, we need to connect it’s
maximizeSetevent as we did previously. - Do note that we also disconnect the function if the ‘Keep Above’ property was unset. To disconnect, we need to provide a function name. This is the reason why we wrote a named function in the global scope instead of an anonymous function.
One point to note there is that, the client object is merely a ‘wrapper’ for an actual Client object (in C++). So then how does the ‘==’ operator work for the same object that was wrapped separately (the == relation is used to lookup up a value using the Array.indexOf() method)? Even though the object they wrap is the same, shouldn’t their wrapper be different? The answer is no. A scripting object to a kwin scripting object follows a strict 1:1 relation. Just as a wrapper can be mapped to a client, a client can be mapped back to a unique wrapper. This is achieved using script caching, details for which are here. This is why the ‘==’ and the ‘!=’ operator can be used safely to check if two variables actually wrap the same object or not.
S3/1/4. Restoring it all
Now the last and most important part of it all. Whenver the client is restored, we must set it’s ‘Keep Above’ property if it was set earlier. To do this, we must simply extend our manageKeepAbove code to handle this scenario. In case the client is not maximized both veritcally and horizontally, we check if the client is in our ka_clients arrray and if it is, we set its ‘Keep Above’ property, otherwise we don’t bother:
function manageKeepAbove(type) {
if((type.h == 1) && (type.v == 1)) {
this.setKeepAbove(0);
} else {
if(ka_clients.indexOf(this)) {
this.setKeepAbove(1);
}
}
}
In the end, our entire script looks like:
var ka_clients = new Array();
var c = workspace.getAllClients();
workspace.clientSetKeepAbove.connect(function(client, set) {
var found = ka_clients.indexOf(client);
if(set == 1) {
if(found == -1) {
ka_clients[ka_clients.length] = client;
client.maximizeSet.connect(client, manageKeepAbove);
}
} else if(set == 0) {
ka_clients.splice(found, 1);
client.maximizeSet.disconnect(manageKeepAbove);
}
});
function manageKeepAbove(type) {
if((type.h == 1) && (type.v == 1)) {
this.setKeepAbove(0);
} else {
if(ka_clients.indexOf(this)) {
this.setKeepAbove(1);
}
}
}
for(var i=0; i<c.length; i++) {
if(c[i].keepAbove()) {
ka_clients[ka_clients.length] = c[i];
c[i].maximizeSet.connect(c[i], manageKeepAbove);
}
}
Try out this script and check if it works according to your wishes. On my machine atleast it does
Before going, let me give you a small excercise. Although we’ve covered all the cases, we surely have left one. Whenever I launch a new window, what if its ‘Keep Above’ property is already set? We surely need to manage that also.. HINT: Use the workspace.clientAdded property.
In the 2nd part of this tutorial, we will implement the same functionality using the KWin Chelate framework. I hope to finish the tutorial within a day or two.
For now, I guess this is all. Special thanks go out to Eike Hein. Also, for the KWin docs, the ugly program I wrote is no longer used. Martin Graesslin wrote a nice XSLT stylesheet for handling the XML -> HTML conversion. It is present in the trunk under ‘apidocs.xsl’. Thanks Martin



Thanks Rohan!
if(set == 1)
{
if(found != -1)
{
ka_clients[ka_clients.length] = client;
client.maximizeSet.connect(client, manageKeepAbove);
}
isnt this wrong? AFAIK You add the client to the array if it was found (found not -1) you should check for found == -1 instead
@hunt0r: noted and fixed. Thanks for reporting!!
Very interesting. I had only intended to read a bit in, but got hooked
Well explained, I guess even those who are not that experienced with programming would be able to follow.
Looking forward to KDE SC 4.6 to try this out (this will be included in 4.6, right?)
@Z: Thanks a lot mate. And yes, mostly scripting would be a part of KDE 4.6. I say ‘mostly’ because as of now I am completely unfamiliar with the release cycles and QA/QC with KDE. So, we’ll see how it goes
Pingback: Optimization in KWin 4.6 « Martin’s Blog
Rather cool stuff. Thanks for the work and this tutorial! I would suggest to add the tutorial to http://techbase.kde.org/Development/Tutorials so others can read+improve. Thanks
Pingback: Interesting Articles for November 4th at Harsh J
Thanks a lot for your job!
When to expect the next part?
@TZ: Can’t really say. My end semester exams will be starting soon.. so don’t really have a lot of time on my hands. Maybe over this weekend?
Why would you make a separate array of _all_ KeepAbove clients?
You only need to keep track of those clients that actually match the criteria;
You can just add a client to a ‘MaximizedAndKeepAbove’ array in the maximize event handler. That way you dont need to keep the array in sync with the actual list of clients that have KeepAbove enabled, have less overhead when looking for the client in the list (’cause it’s much shorter)..
Pingback: KDE 4.6.0 发布 | I, KDE
Pingback: KDE SC 4.6 | "ALLLINUX" г.Ростов-на-Дону
Pingback: KDE Software Compilation 4.6.0 | thecamels.org
Pingback: KDE SC 4.6 ist fertig - Software | News | ZDNet.de
Pingback: KDE 4.6 Duyuruldu!
Wow,
thats exactly what i need for quickly implement active window tracking in ulatencyd so i don’t have to observe the x server and could do nicer tricks like unsetting the active flag when the window is minimized etc.
I will look into that, i hope i can get the _NET_WM_PID property or the pid and information if it is a local client somehow…
for(var i=0; i<c.length; i++) {if(c[i].keepAbove()) {
print(client.caption() + " (yes)");
}
I think you meant:
for(var i=0; i<c.length; i++) {
if(c[i].keepAbove()) {
print(c[i].caption() + " (yes)");
}