Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to “correctly” detect application name when changing focus event occurs with python xlib

I want to detect applications window name when changing focus event occurs with python xlib, so in the first step I use this code:

#!/usr/bin/env python
#-*- coding:utf-8 -*-
import Xlib.display
import time


display = Xlib.display.Display()
while True:
    window = display.get_input_focus().focus
    wmname = window.get_wm_name()
    wmclass = window.get_wm_class()
    if wmclass is None and wmname is None:
        window = window.query_tree().parent
        wmname = window.get_wm_name()
    print "WM Name: %s" % ( wmname, )
    time.sleep(3)

But I want a correct way, then I research about xlib events and find Input Focus Events and write this code:

#!/usr/bin/env python
#-*- coding:utf-8 -*-
import Xlib.display
from Xlib import X

def main():
    display = Xlib.display.Display(':0')
    root = display.screen().root
    root.change_attributes(event_mask=Xlib.X.FocusChangeMask)

    while True:
        event = root.display.next_event()
        #if event.type == X.FocusIn or event.type == X.FocusOut:
        if event.type == X.FocusOut :
            window = display.get_input_focus().focus
            wmname = window.get_wm_name()
            wmclass = window.get_wm_class()
            if wmclass is None and wmname is None:
                window = window.query_tree().parent
                wmname = window.get_wm_name()
            print "WM Name: %s" % ( wmname, )

if __name__ == "__main__":
    main()

Sadly it's not work correctly especially in tabbed browsing on google chrome and firefox, so Is there a correct way for this situation?

like image 382
PathSeeker Avatar asked Jan 11 '23 13:01

PathSeeker


2 Answers

Your code is almost right, but it misses two things:

  • rather than listening only to focus changes, it should also listen to window property events which include changes of WM_NAME property, that also happen when you cycle tabs in your browser.
  • rather than listening only in root window, it should listen to every window (that gets focused). You can attach the event handler the same way as you do with the root window.

That being said, here is a working sample:

#!/usr/bin/python3
import Xlib
import Xlib.display

disp = Xlib.display.Display()
root = disp.screen().root

NET_WM_NAME = disp.intern_atom('_NET_WM_NAME')
NET_ACTIVE_WINDOW = disp.intern_atom('_NET_ACTIVE_WINDOW')

root.change_attributes(event_mask=Xlib.X.FocusChangeMask)
while True:
    try:
        window_id = root.get_full_property(NET_ACTIVE_WINDOW, Xlib.X.AnyPropertyType).value[0]
        window = disp.create_resource_object('window', window_id)
        window.change_attributes(event_mask=Xlib.X.PropertyChangeMask)
        window_name = window.get_full_property(NET_WM_NAME, 0).value
    except Xlib.error.XError: #simplify dealing with BadWindow
        window_name = None
    print(window_name)
    event = disp.next_event()
like image 192
rr- Avatar answered Jan 17 '23 16:01

rr-


@rr- As I just corrected elsewhere, you'll want to query both the current _NET_WM_NAME (UTF-8) and the legacy WM_NAME (non-UTF8) properties or the default xterm configuration will return no title.

I just posted a complete working example over on your Unix & Linux StackExchange question.

To avoid sending people on a cross-reference hunt, here's a copy of the code I posted there:

#!/usr/bin/python
from contextlib import contextmanager
import Xlib
import Xlib.display

disp = Xlib.display.Display()
root = disp.screen().root

NET_ACTIVE_WINDOW = disp.intern_atom('_NET_ACTIVE_WINDOW')
NET_WM_NAME = disp.intern_atom('_NET_WM_NAME')  # UTF-8
WM_NAME = disp.intern_atom('WM_NAME')           # Legacy encoding

last_seen = { 'xid': None, 'title': None }

@contextmanager
def window_obj(win_id):
    """Simplify dealing with BadWindow (make it either valid or None)"""
    window_obj = None
    if win_id:
        try:
            window_obj = disp.create_resource_object('window', win_id)
        except Xlib.error.XError:
            pass
    yield window_obj

def get_active_window():
    win_id = root.get_full_property(NET_ACTIVE_WINDOW,
                                       Xlib.X.AnyPropertyType).value[0]

    focus_changed = (win_id != last_seen['xid'])
    if focus_changed:
        with window_obj(last_seen['xid']) as old_win:
            if old_win:
                old_win.change_attributes(event_mask=Xlib.X.NoEventMask)

        last_seen['xid'] = win_id
        with window_obj(win_id) as new_win:
            if new_win:
                new_win.change_attributes(event_mask=Xlib.X.PropertyChangeMask)

    return win_id, focus_changed

def _get_window_name_inner(win_obj):
    """Simplify dealing with _NET_WM_NAME (UTF-8) vs. WM_NAME (legacy)"""
    for atom in (NET_WM_NAME, WM_NAME):
        try:
            window_name = win_obj.get_full_property(atom, 0)
        except UnicodeDecodeError:  # Apparently a Debian distro package bug
            title = "<could not decode characters>"
        else:
            if window_name:
                win_name = window_name.value
                if isinstance(win_name, bytes):
                    # Apparently COMPOUND_TEXT is so arcane that this is how
                    # tools like xprop deal with receiving it these days
                    win_name = win_name.decode('latin1', 'replace')
                return win_name
            else:
                title = "<unnamed window>"

    return "{} (XID: {})".format(title, win_obj.id)

def get_window_name(win_id):
    if not win_id:
        last_seen['title'] = "<no window id>"
        return last_seen['title']

    title_changed = False
    with window_obj(win_id) as wobj:
        if wobj:
            win_title = _get_window_name_inner(wobj)
            title_changed = (win_title != last_seen['title'])
            last_seen['title'] = win_title

    return last_seen['title'], title_changed

def handle_xevent(event):
    if event.type != Xlib.X.PropertyNotify:
        return

    changed = False
    if event.atom == NET_ACTIVE_WINDOW:
        if get_active_window()[1]:
            changed = changed or get_window_name(last_seen['xid'])[1]
    elif event.atom in (NET_WM_NAME, WM_NAME):
        changed = changed or get_window_name(last_seen['xid'])[1]

    if changed:
        handle_change(last_seen)

def handle_change(new_state):
    """Replace this with whatever you want to actually do"""
    print(new_state)

if __name__ == '__main__':
    root.change_attributes(event_mask=Xlib.X.PropertyChangeMask)

    get_window_name(get_active_window()[0])
    handle_change(last_seen)

    while True:  # next_event() sleeps until we get an event
        handle_xevent(disp.next_event())

There's also a more heavily commented version in this GitHub Gist.

like image 40
ssokolow Avatar answered Jan 17 '23 17:01

ssokolow