Benachrichtigung über Änderungen des Fenstertitels

9

... ohne abzufragen.

Ich möchte erkennen, wann sich das aktuell fokussierte Fenster ändert, damit ich eine benutzerdefinierte Benutzeroberfläche in meinem System aktualisieren kann.

Punkte von Interessen:

  • Echtzeitbenachrichtigungen. Eine Verzögerung von 0,2 s ist in Ordnung, eine Verzögerung von 1 s ist meh, eine Verzögerung von 5 s ist völlig inakzeptabel.
  • Ressourcenfreundlichkeit: Aus diesem Grund möchte ich Polling vermeiden. xdotool getactivewindow getwindownameJede halbe Sekunde laufen zu lassen , funktioniert ganz gut ... aber ist das Laichen von 2 Prozessen pro Sekunde für mein System so freundlich?

In bspwmkann verwendet werden, bspc subscribewelche eine Linie mit einigen (sehr) grundlegenden Statistiken druckt, jedes Mal, wenn sich der Fensterfokus ändert. Dieser Ansatz scheint zunächst gut zu sein, aber wenn Sie ihn anhören, wird nicht erkannt, wann sich der Fenstertitel von selbst ändert (z. B. bleibt das Ändern der Registerkarten im Webbrowser auf diese Weise unbemerkt.)

Ist es unter Linux in Ordnung, jede halbe Sekunde einen neuen Prozess zu erzeugen, und wenn nicht, wie kann ich die Dinge besser machen?

Eine Sache, die mir in den Sinn kommt, ist zu versuchen, zu emulieren, was Fenstermanager tun. Aber kann ich unabhängig vom Arbeitsfenstermanager Hooks für Ereignisse wie "Fenstererstellung", "Titeländerungsanforderung" usw. schreiben oder muss ich selbst Fenstermanager werden? Benötige ich dafür root?

(Eine andere Sache, die mir in den Sinn kam, war, mir den xdotoolCode anzusehen und nur die Dinge zu emulieren, die mich interessieren, damit ich den gesamten Prozess vermeiden kann, der Boilerplate erzeugt, aber es würde immer noch eine Abfrage geben.)

rr-
quelle

Antworten:

4

Ich konnte Ihren Fokusänderungsansatz unter Kwin 4.x nicht zuverlässig zum Laufen bringen, aber moderne Fenstermanager behalten eine _NET_ACTIVE_WINDOWEigenschaft im Stammfenster bei, auf die Sie nach Änderungen warten können.

Hier ist eine Python-Implementierung von genau dem:

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

Die ausführlichere Version, die ich als Beispiel für jemanden geschrieben habe, ist in diesem Kern enthalten .

UPDATE: Jetzt wird auch die zweite Hälfte (Anhören _NET_WM_NAME) demonstriert , um genau das zu tun, was angefordert wurde.

UPDATE # 2: ... und der dritte Teil: Zurückgreifen auf, WM_NAMEwenn etwas wie xterm nicht eingestellt ist _NET_WM_NAME. (Letzteres ist UTF-8-codiert, während Ersteres eine Legacy-Zeichencodierung verwenden soll, die als zusammengesetzter Text bezeichnet wird. Da jedoch niemand zu wissen scheint, wie man damit arbeitet, erhalten Sie Programme, die den darin enthaltenen Bytestrom werfen und xprop nur annehmen es wird ISO-8859-1 sein.)

ssokolow
quelle
Danke, das ist ein klar saubererer Ansatz. Diese Eigenschaft war mir nicht bekannt.
rr-
@ rr- Ich habe es aktualisiert, um auch das Anschauen zu demonstrieren, _NET_WM_NAMEsodass mein Code jetzt einen Proof of Concept für genau das bietet, was Sie gefragt haben.
ssokolow
6

Dank des Kommentars von @ Basile habe ich viel gelernt und mir das folgende Arbeitsprobe ausgedacht:

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

Anstatt xdotoolnaiv zu laufen , hört es synchron auf Ereignisse, die von X generiert wurden, genau das, wonach ich gesucht habe.

rr-
quelle
Wenn Sie xmonad window manager verwenden, müssen Sie XMonad.Hooks.EwmhDesktops in Ihre Konfiguration aufnehmen
Vasiliy Kevroletin