Is it possible to get a return value from a KWin script?

Is there any way to know if a KWin script executed properly? Here is a simple script to focus on the last active window of a given app:

windows = workspace.windowList();
target_name = 'emacs';
target_window = null;
for (var i = 0; i < windows.length; i++) {
  if (windows[i].resourceName == target_name
      && (target_window == null
       || windows[i].stackingOrder > target_window.stackingOrder)
      && windows[i].desktops[0] == workspace.currentDesktop) {
    target_window = windows[i];
  }
}
if (target_window != null) {
  workspace.activeWindow = target_window;
}

If the app is not found (it’s not running) I would like to start it instead. But I need that information somehow to make the decision. I guess I could print something like “NOT FOUND” and search for that string in journalctl but I wonder if there are better ways. I could also run ps aux | grep emacs but that works across desktops which is close but not quite what I want. I’m calling the script from C++ like this:

QDBusMessage response;
QDBusMessage m;
// LOAD
m = QDBusMessage::createMethodCall("org.kde.KWin",
                                   "/Scripting",
                                   "",
                                   "loadScript");
m << "main.js";
response = QDBusConnection::sessionBus().call(m);
// RUN
m = QDBusMessage::createMethodCall("org.kde.KWin",
                                   "/Scripting",
                                   "",
                                   "start");
response = QDBusConnection::sessionBus().call(m);
qDebug() << response.arguments();

but I haven’t found any way to populate response.arguments() with anything useful. Any comments would be appreciated!

I have no experience with KWin but since I can’t use wmctrl anymore (my OS just upgraded to Plasma 6) I’m trying to recreate some of the functionality with KWin scripts.

Yes you can use journal to catch the result, but a better alternative would be to send the result using callDBus to a service you register like this example I did in python Determine when monitor is turned on or off via python dbus - #7 by luisbocanegra

You may also find this project useful

Thanks, using my own dbus service seems to work. Next, how can I call the script from a shell script instead of C++? This seems to have no effect:

dbus-send --session --dest=org.kde.KWin /Scripting org.kde.kwin.Scripting.loadScript "string:main.js"
dbus-send --session --dest=org.kde.KWin /Scripting org.kde.kwin.Scripting.start

Is there any up to date KWin scripting documentation I could look at?

For dbus stuff I usually explore them with qdbus and sometimes I even look at the xml description files.
There’s KWin scripting tutorial | Developer but doesn’t include how to load scripts manually.

Here’s how a script can be loaded/unloaded using shell script:

SCRIPT="/home/luis/projects/plasma-panel-spacer-extended/moveTopWindow.js"
SCRIPT_NAME="moveTopWindow"
qdbus org.kde.KWin /Scripting org.kde.kwin.Scripting.unloadScript "$SCRIPT_NAME"
script_id=$(qdbus org.kde.KWin /Scripting org.kde.kwin.Scripting.loadScript "${SCRIPT}" "$SCRIPT_NAME")

# run
qdbus org.kde.KWin /Scripting/Script"$script_id" org.kde.kwin.Script.run
sleep 1
qdbus org.kde.KWin /Scripting/Script"$script_id" org.kde.kwin.Script.stop
1 Like

Thanks a lot! Here is my full solution. A script to focus or launch an app,

# Resource name as returned by print(windows[i].resourceName) below
APP_NAME=$1
# Path to the executable that launches the app
EXEC_PATH=$2

if [[ ! $(pgrep -f focus_or_launch_service.py) ]]; then
    python focus_or_launch_service.py &
    sleep 0.1
fi

SCRIPT='main.js'
cat > $SCRIPT <<EOF
resource_name = "${APP_NAME}";
exec_path = "${EXEC_PATH}";
windows = workspace.windowList();
target_window = null;
for (var i = 0; i < windows.length; i++) {
  // Output: journalctl -f QT_CATEGORY=js QT_CATEGORY=kwin_scripting
  // print(windows[i].resourceName);
  if (windows[i].resourceName == resource_name
      && (target_window == null
       || windows[i].stackingOrder > target_window.stackingOrder)
      && windows[i].desktops[0] == workspace.currentDesktop) {
    target_window = windows[i];
  }
}
if (target_window != null) {
  workspace.activeWindow = target_window;
} else {
  callDBus(
    'com.raul', '/com/raul/runcommand', 'com.raul', 'run_command', exec_path);
}
EOF

SCRIPT_NAME='focusOrLaunch'
qdbus org.kde.KWin /Scripting org.kde.kwin.Scripting.unloadScript "$SCRIPT_NAME"
script_id=$(qdbus org.kde.KWin /Scripting org.kde.kwin.Scripting.loadScript \
                  "${SCRIPT}" "$SCRIPT_NAME")
qdbus org.kde.KWin /Scripting/Script"$script_id" org.kde.kwin.Script.run

and the service I need for launching an app if not already running:

from gi.repository import GLib
import asyncio
import dbus
import dbus.mainloop.glib
import dbus.service


class Service(dbus.service.Object):
    def __init__(self):
        self._loop = GLib.MainLoop()

    def run(self):
        dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
        bus_name = dbus.service.BusName("com.raul", dbus.SessionBus())
        dbus.service.Object.__init__(self, bus_name, "/com/raul/runcommand")
        self._loop.run()

    @dbus.service.method("com.raul", in_signature="s", out_signature="")
    def run_command(self, cmd):
        async def run(cmd):
            await asyncio.create_subprocess_shell(cmd)
        asyncio.run(run(cmd))

    @dbus.service.method("com.raul", in_signature="", out_signature="")
    def quit(self):
        self._loop.quit()


if __name__ == "__main__":
    Service().run()

I’m sure many things can be more elegant here but it does exactly what I need. I can bind the shell script with different arguments (e.g. focus_or_launch.sh emacs /usr/bin/emacs) to different keyboard shortcuts.

1 Like