Être informé des changements de titre de fenêtre

9

... sans sondage.

Je veux détecter quand la fenêtre actuellement focalisée change afin de pouvoir mettre à jour un morceau de GUI personnalisé dans mon système.

Points d'intérêt:

  • notifications en temps réel. Avoir un décalage de 0,2 s est correct, avoir un décalage de 1 s est meh, avoir un décalage de 5 s est totalement inacceptable.
  • convivialité des ressources: pour cette raison, je veux éviter les sondages. Exécuter xdotool getactivewindow getwindownametoutes les, disons, une demi-seconde, fonctionne très bien ... mais la génération de 2 processus par seconde est-elle si conviviale pour mon système?

Dans bspwm, on peut utiliser bspc subscribequi imprime une ligne avec des statistiques (très) basiques, chaque fois que le focus de la fenêtre change. Cette approche semble agréable au début, mais l'écouter ne détectera pas quand le titre de la fenêtre change par lui-même (par exemple, changer les onglets dans le navigateur Web passera inaperçu de cette façon.)

Ainsi, le nouveau processus de génération toutes les demi-secondes est-il correct sur Linux, et sinon, comment puis-je faire mieux?

Une chose qui me vient à l'esprit est d'essayer d'imiter ce que font les gestionnaires de fenêtres. Mais puis-je écrire des crochets pour des événements tels que "création de fenêtre", "demande de changement de titre", etc. indépendamment du gestionnaire de fenêtres de travail, ou dois-je devenir un gestionnaire de fenêtres lui-même? Ai-je besoin de root pour cela?

(Une autre chose qui m'est venue à l'esprit est de regarder xdotoolle code de et d'émuler uniquement les choses qui m'intéressent afin que je puisse éviter tout le processus générant du passe-partout, mais ce serait toujours du polling.)

rr-
la source

Réponses:

4

Je ne pouvais pas faire fonctionner votre approche de changement de focus de manière fiable sous Kwin 4.x, mais les gestionnaires de fenêtres modernes conservent une _NET_ACTIVE_WINDOWpropriété sur la fenêtre racine que vous pouvez écouter pour les modifications.

Voici une implémentation Python de cela:

#!/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())

La version la plus commentée que j'ai écrite comme exemple pour quelqu'un est dans cet esprit .

MISE À JOUR: Maintenant, il montre également la seconde moitié (écouter _NET_WM_NAME) pour faire exactement ce qui a été demandé.

MISE À JOUR # 2: ... et la troisième partie: Revenir à WM_NAMEsi quelque chose comme xterm n'a pas encore été défini _NET_WM_NAME. (Ce dernier est UTF-8 codé alors que le premier est censé utiliser un caractère hérité de codage appelé texte composé mais, puisque personne ne semble savoir comment travailler avec elle, vous obtenez des programmes jeter tout flux d'octets qu'ils ont là - bas et xprop juste en supposant ce sera ISO-8859-1.)

ssokolow
la source
Merci, c'est une approche clairement plus propre. Je n'étais pas au courant de cette propriété.
rr-
@ rr- Je l'ai mis à jour pour montrer également _NET_WM_NAMEque je regarde donc mon code fournit maintenant une preuve de concept pour exactement ce que vous avez demandé.
ssokolow
6

Eh bien, grâce au commentaire de @ Basile, j'ai beaucoup appris et j'ai trouvé l'exemple de travail suivant:

#!/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:
        window_name = None
    print(window_name)
    event = disp.next_event()

Plutôt que de fonctionner xdotoolnaïvement, il écoute de manière synchrone les événements générés par X, ce qui est exactement ce que je recherchais.

rr-
la source
si vous utilisez le gestionnaire de fenêtres xmonad, votre besoin d'inclure XMonad.Hooks.EwmhDesktops dans votre configuration
Vasiliy Kevroletin