Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
python-xlib example which reacts to changing the active window
#!/usr/bin/env python3
# Based on code by Stephan Sokolow
# MIT-licensed
# Source:
"""python-xlib example which reacts to changing the active window/title.
- Python
- python-xlib
Tested with Python 2.x because my Kubuntu 14.04 doesn't come with python-xlib
for Python 3.x.
Any modern window manager that isn't horrendously broken maintains an X11
property on the root window named _NET_ACTIVE_WINDOW.
Any modern application toolkit presents the window title via a property
named _NET_WM_NAME.
This listens for changes to both of them and then hides duplicate events
so it only reacts to title changes once.
Known Bugs:
- Under some circumstances, I observed that the first window creation and last
window deletion on on an empty desktop (ie. not even a taskbar/panel) would
go ignored when using this test setup:
Xephyr :3 &
DISPLAY=:3 openbox &
DISPLAY=:3 python3
# ...and then launch one or more of these in other terminals
DISPLAY=:3 leafpad
# pylint: disable=unused-import
from contextlib import contextmanager
from typing import Any, Dict, Optional, Tuple, Union # noqa
from Xlib import X
from Xlib.display import Display
from Xlib.error import XError, BadWindow
from Xlib.xobject.drawable import Window
from Xlib.protocol.rq import Event
# Connect to the X server and get the root window
disp = Display()
root = disp.screen().root
# Ignore BadWindow errors; they mean a window was closed while getting info
def ignore_BadWindow(e, rq):
if not isinstance(e, BadWindow):
# Prepare the property names we use so they can be fed into X11 APIs
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} # type: Dict[str, Any]
def window_obj(win_id: Optional[int]) -> Window:
"""Simplify dealing with BadWindow (make it either valid or None)"""
window_obj = None
if win_id:
window_obj = disp.create_resource_object('window', win_id)
except XError:
yield window_obj
def get_active_window() -> Tuple[Optional[int], bool]:
"""Return a (window_obj, focus_has_changed) tuple for the active window."""
response = root.get_full_property(NET_ACTIVE_WINDOW,
if not response:
return None, False
win_id = response.value[0]
focus_changed = (win_id != last_seen['xid'])
if focus_changed:
with window_obj(last_seen['xid']) as old_win:
if old_win:
last_seen['xid'] = win_id
with window_obj(win_id) as new_win:
if new_win:
return win_id, focus_changed
def _get_window_name_inner(win_obj: Window) -> str:
"""Simplify dealing with _NET_WM_NAME (UTF-8) vs. WM_NAME (legacy)"""
for atom in (NET_WM_NAME, WM_NAME):
window_name = win_obj.get_full_property(atom, 0)
except UnicodeDecodeError: # Apparently a Debian distro package bug
title = "<could not decode characters>"
if window_name:
win_name = window_name.value # type: Union[str, bytes]
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
title = "<unnamed window>"
return "{} (XID: {})".format(title,
def get_window_name(win_id: Optional[int]) -> Tuple[Optional[str], bool]:
"""Look up the window name for a given X11 window ID"""
if not win_id:
last_seen['title'] = None
return last_seen['title'], True
title_changed = False
with window_obj(win_id) as wobj:
if wobj:
win_title = _get_window_name_inner(wobj)
except XError:
title_changed = (win_title != last_seen['title'])
last_seen['title'] = win_title
return last_seen['title'], title_changed
def handle_xevent(event: Event):
"""Handler for X events which ignores anything but focus/title change"""
if event.type != X.PropertyNotify:
changed = False
if event.atom == NET_ACTIVE_WINDOW:
if get_active_window()[1]:
get_window_name(last_seen['xid']) # Rely on the side-effects
changed = True
elif event.atom in (NET_WM_NAME, WM_NAME):
changed = changed or get_window_name(last_seen['xid'])[1]
if changed:
def handle_change(new_state: dict):
"""Replace this with whatever you want to actually do"""
title = new_state['title']
if title:
print(title, flush=True)
if __name__ == '__main__':
# Listen for _NET_ACTIVE_WINDOW changes
# Prime last_seen with whatever window was active when we started this
while True: # next_event() sleeps until we get an event
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.