Skip to content

Instantly share code, notes, and snippets.

@rbreaves
Last active October 10, 2024 09:00
Show Gist options
  • Save rbreaves/257c3edfa301786e66e964d7ac036269 to your computer and use it in GitHub Desktop.
Save rbreaves/257c3edfa301786e66e964d7ac036269 to your computer and use it in GitHub Desktop.
Grab wmclass name or Window Name/Title under Wayland with Gnome 3.x
# Single Command, runs 2 calls to gdbus to get the currently active Window from Gnome 3.x
# Escaped so you can copy and paste into terminal directly
gdbus call -e -d org.gnome.Shell -o /org/gnome/Shell -m org.gnome.Shell.Eval global.get_window_actors\(\)[`gdbus call -e -d org.gnome.Shell -o /org/gnome/Shell -m org.gnome.Shell.Eval global.get_window_actors\(\).findIndex\(a\=\>a.meta_window.has_focus\(\)===true\) | cut -d"'" -f 2`].get_meta_window\(\).get_wm_class\(\) | cut -d'"' -f 2
# Unescaped version, will not run
# Broken down into 2 commands.
# Call to Gnome to get the array location of the active Application
gdbus call -e -d org.gnome.Shell -o /org/gnome/Shell -m \
org.gnome.Shell.Eval global.get_window_actors().findIndex(a=>a.meta_window.has_focus()===true) \
| cut -d"'" -f 2
# Replace the array number 2 with the one from the previous command and you will get the App Name of the actively focused Window
gdbus call -e -d org.gnome.Shell -o /org/gnome/Shell -m org.gnome.Shell.Eval \
global.get_window_actors()[2].get_meta_window().get_wm_class() \
| cut -d'"' -f 2
@rbreaves
Copy link
Author

Will also make note that this may be a similar alternative on how to do this under KDE.

https://unix.stackexchange.com/questions/385697/how-do-i-run-a-kwin-script-from-the-console-to-set-focus-to-a-specific-window-i

@johan-bjareholt
Copy link

Note that there's also the possibility for race conditions (the user closing the window between call 1 and call 2), so don't expect this to always work.

@rbreaves
Copy link
Author

rbreaves commented Jan 24, 2020

Yea, I have noticed that the inner or 1st call can also fail if you're in a state between applications as the DE knows the new active window or app is not ready and yet the old one is already non-active so you are left with a 1st call that comes back with nothing. It could be written better to try and avoid that.

@RaghavRao
Copy link

Is there a way to re-activate the window retrieved from here?

@rbreaves
Copy link
Author

@RaghavRao You can probably work something out by looking at this thread, they limit alt+tab to only the apps on a particular active monitor - but considering they're able to limit the Windows via a filter for alt+tab.. I would think you might be able to modify the code in some way to simply activate a particular Window/App however you want, but I am not entirely sure.

You may be able to use d-feet or some app to help you dump/view the methods available as well.

https://unix.stackexchange.com/questions/281670/restrict-alttab-to-the-current-monitor-in-gnome-3

For my own purpose I don't need to re-activate an app or window as my own app will only ever monitor the app the user has currently selected. Anything beyond that, for me, is not needed.

@mccabeservant
Copy link

This was very helpful for me! Thank you!

I was easily able to write a second query identical to the first, substituting only get_title() for get_wm_class() to get the title of the currently open webpage in Firefox, Opera, or Chromium as well as the app name, while knowing virtually nothing about bash scripting.

However, if .get_wm_class() (or get_title()) is a string with an apostrophe or double-quote character, the name of the app will get messed up. I had to remove | cut -d'"' -f 2 from the end of your command to get something I found usable, which I was then able to parse in the python app that I was actually using your command in. FWIW, here are the basics of the python code I used (in case someone else happens to be in a similar boat)...

import subprocess
import ast

app_command = ("gdbus call -e -d org.gnome.Shell -o /org/gnome/Shell -m org.gnome.Shell.Eval global.get_window_actors\(\)[`gdbus call -e -d org.gnome.Shell -o /org/gnome/Shell -m org.gnome.Shell.Eval global.get_window_actors\(\).findIndex\(a\=\>a.meta_window.has_focus\(\)===true\) | cut -d\"'\" -f 2`].get_meta_window\(\).get_wm_class\(\)")
app_process = subprocess.Popen(app_command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
app_retval = app_process.stdout.read()
app_retcode = app_process.wait()

app_tuple_string = app_retval.decode('utf-8').strip()
# We now have a string that looks like:
# (true, "\"Qur'an App"\")

app_tuple_string = app_tuple_string.replace("(true", "(True")
app_tuple_string = app_tuple_string.replace("(false", "(False")
# We now have a string that looks like a python tuple:
# (True, "\"Qur'an App"\")

app_tuple = ast.literal_eval(app_tuple_string)
app = app_tuple[1]
# We now have a string with quotes in it that looks like:
# "Qur'an App"

app = ast.literal_eval(app)
# We now have a string that looks like:
# Qur'an App

print(app)

@zerosign
Copy link

zerosign commented Jul 11, 2021

for people that interest in this too, now, we could use filter function then use optional js chaining method to avoid null

⟩ gjs --version
gjs 1.68.1
gdbus call -e -d org.gnome.Shell -o /org/gnome/Shell -m org.gnome.Shell.Eval "global.get_window_actors().filter(a=>a.meta_window.has_focus()===true)[0]?.get_meta_window()?.get_wm_class()"

or if you only want to get only the title

gdbus call -e -d org.gnome.Shell -o /org/gnome/Shell -m org.gnome.Shell.Eval "global.get_window_actors().filter(a=>a.meta_window.has_focus()===true)[0]?.get_meta_window()?.get_wm_class()" | cut -d'"' -f 2

@canadaduane
Copy link

canadaduane commented Nov 19, 2021

This may be a shorter way to accomplish the goal:

gdbus call -e -d org.gnome.Shell -o /org/gnome/Shell \
  --method org.gnome.Shell.Eval "global.display.focus_window.get_wm_class()"

@dceee
Copy link

dceee commented Nov 23, 2021

How would this work in gnome 41? Looks like the Eval interface has been now restricted to internal calls only :-(

@rbreaves
Copy link
Author

@dceee find where that commit occurred and fork it? I honestly don't know. Tbh - that may be what should happen to Wayland - add the feature we need directly and then just maintain a fork of it. Literally everything could then just be merged by default, but just the single addition of a feature to expose the currently focused Window - at least to authorized apps outside of just DEs.

@rbreaves
Copy link
Author

@canadaduane That solution does look nice! I hope that dceee is wrong about the latest update in Gnome 41 or that something new & useful is replacing it - although I am not sure that the Gnome team or Wayland are paying attention just yet.

I did appreciate your article and the visibility it got - I believe that will likely help us out a lot because I imagine most devs just aren't thinking of the use cases that involves key remappers & other accessibility related apps needing to know the active window name.

@canadaduane
Copy link

Thanks @rbreaves happy to try to make sense of our ecosystem and perhaps nudge towards stronger community and interop.

@dceee The gnome 41 solution seems to be to write a gnome shell extension that adds the needed interface. Here's an example that makes it possible to move windows (something that could also only be achieved with Eval previously):

https://gist.github.com/tuberry/dc651e69d9b7044359d25f1493ee0b39

@dceee
Copy link

dceee commented Nov 24, 2021

@canadaduane Thanks, this is exactly what I did. I moved all calls against the Eval interface to an extension that acts like a proxy for my python scripts.

@canadaduane
Copy link

@dceee Is that extension something you can share? I'd be interested in seeing how you did it.

@dceee
Copy link

dceee commented Nov 24, 2021

Based on the example code above, I replaced the methods with the ones I need:

const { Gio } = imports.gi;

const MR_DBUS_IFACE = `
<node>
    <interface name="org.gnome.Shell.Extensions.Windows">
        <method name="List">
            <arg type="s" direction="out" name="win"/>
        </method>
        <method name="Focus">
            <arg type="u" direction="in" name="winid"/>
        </method>
        <method name="Minimize">
            <arg type="u" direction="in" name="winid"/>
        </method>
    </interface>
</node>`;


class Extension {
    enable() {
        this._dbus = Gio.DBusExportedObject.wrapJSObject(MR_DBUS_IFACE, this);
        this._dbus.export(Gio.DBus.session, '/org/gnome/Shell/Extensions/Windows');
    }

    disable() {
        this._dbus.flush();
        this._dbus.unexport();
        delete this._dbus;
    }
    List() {
        let win = global.get_window_actors()
            .map(a => a.meta_window)
            .map(w => ({ class: w.get_wm_class(), pid: w.get_pid(), id: w.get_id(), max: w.get_maximized(), focus: w.has_focus()}));
        return JSON.stringify(win);
    }
   Focus(winid) {
        let win = global.get_window_actors().map(a=>a.meta_window).find(w=>w.get_id()==winid);
        if (win) {
           win.activate(0);
           } else {
            throw new Error('Not found');
        }
        }
   Minimize(winid) {
        let win = global.get_window_actors().map(a=>a.meta_window).find(w=>w.get_id()==winid);
        if (win) {
           win.minimize(0);
           } else {
            throw new Error('Not found');
        }
        }
}

To get a list of windows, you can now place the DBUS call against /org/gnome/Shell/Extensions/Windows:

gdbus call --session --dest org.gnome.Shell --object-path /org/gnome/Shell/Extensions/Windows --method org.gnome.Shell.Extensions.Windows.List

@canadaduane
Copy link

Thanks!

@DiegoMagdaleno
Copy link

Btw, as for the extension code above, is missing the "init" function, so the code needs to have this at the end

function init() {
     return new Extension();
}

Now with that out of the way, wouldn't the best way to implement this on GNOME, be:

Implement a gdbus "client" inside xkeysnail, this would have to be async I guess, then, we could have our extension, but what we could try to do (I don't know is GNOME has an API or an event for this), but we can detect if the user changed windows, after that, we can emit a dbus event (This example if from the command line, but I could make one programatically) and since we are connected to the system/user bus via our client, we could just "listen" for this signal, the moment the signal is emited, we could just grab the window that is marked as "active" and return that as the window title

@rbreaves
Copy link
Author

rbreaves commented Jan 5, 2022

Maybe I’d have to look into it more. Xremap has solved it apparently for gnome & wayland. Kinto could be redone to use xremap or we can yes patch xkeysnail & do a PR on it.

https://github.com/k0kubun/xremap

@johan-bjareholt
Copy link

@rbreaves Xremap seem to do a similar dbus Eval call according to the README

busctl --user call org.gnome.Shell /org/gnome/Shell org.gnome.Shell Eval s 'global.get_window_actors().map(a => a.get_meta_window().get_wm_class());'

@DiegoMagdaleno
Copy link

@rbreaves I like how xremap looks and I think it is a little bit more modern than xkeysnail, however, I don't know how much effort it would take to rewrite or redo Kinto to use xremap.

@rbreaves
Copy link
Author

rbreaves commented Jan 5, 2022

@DiegoMagdaleno probably not as much as you think. The syntax for it looks quite similar to a yaml config format I’ve been wanting to make part of either Kinto or xkeysnail actually. That would have been a simple translation layer though.

@canadaduane
Copy link

Just wanted to highlight some cool work that rvaiya is doing in keyd here: https://github.com/rvaiya/keyd/blob/master/scripts/keyd-application-mapper

In this latest beta, keyd now offers its hot-swappable keyboard layouts and layers with application focus conveyed by that python script via a socket to the keyd daemon. Works on X, Wayland+Sway, and Wayland+Gnome.

@rbreaves
Copy link
Author

That is very interesting, I've not used sway any.. and sadly my distro of choice, Ubuntu Budgie, does not have access to gnome-extensions even though it works with gnome-shell still.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment