Recibir notificaciones sobre los cambios en el título de la ventana


9

... sin sondeo.

Quiero detectar cuándo cambia la ventana actualmente enfocada para poder actualizar una parte de la GUI personalizada en mi sistema.

Puntos de interés:

  • notificaciones en tiempo real Tener un retraso de 0.2s está bien, tener un retraso de 1s es meh, tener un retraso de 5s es totalmente inaceptable.
  • Amabilidad de los recursos: por esta razón, quiero evitar las encuestas. Ejecutar xdotool getactivewindow getwindownamecada medio segundo, por ejemplo, funciona bastante bien ... pero ¿generar 2 procesos por segundo es tan amigable para mi sistema?

En bspwm, se puede usar bspc subscribeque imprime una línea con algunas (muy) estadísticas básicas, cada vez que cambia el foco de la ventana. Este enfoque parece bueno al principio, pero escuchar esto no detectará cuándo el título de la ventana cambia por sí solo (por ejemplo, cambiar las pestañas en el navegador web pasará desapercibido de esta manera).

Entonces, generar nuevos procesos cada medio segundo está bien en Linux, y si no, ¿cómo puedo hacer las cosas mejor?

Una cosa que me viene a la mente es tratar de emular lo que hacen los administradores de ventanas. Pero, ¿puedo escribir ganchos para eventos como "creación de ventanas", "solicitud de cambio de título", etc., independientemente del administrador de ventanas en funcionamiento, o necesito convertirme en un administrador de ventanas en sí mismo? ¿Necesito root para esto?

(Otra cosa que me vino a la mente es mirar xdotoolel código y emular solo las cosas que me interesan para poder evitar todo el proceso de generación de repeticiones, pero aún sería un sondeo).

Respuestas:


4

No pude lograr que su enfoque de cambio de enfoque funcione de manera confiable con Kwin 4.x, pero los administradores de ventanas modernos mantienen una _NET_ACTIVE_WINDOWpropiedad en la ventana raíz en la que puede escuchar los cambios.

Aquí hay una implementación de Python de eso:

#!/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 versión más comentada que escribí como ejemplo para alguien está en esta esencia .

ACTUALIZACIÓN: Ahora, también demuestra que la segunda mitad (escuchando _NET_WM_NAME) hace exactamente lo que se solicitó.

ACTUALIZACIÓN # 2: ... y la tercera parte: recurriendo a WM_NAMEsi algo como xterm no se ha configurado _NET_WM_NAME. (El último está codificado en UTF-8, mientras que el primero debe usar una codificación de caracteres heredada llamada texto compuesto , pero, dado que nadie parece saber cómo trabajar con él, obtienes programas que arrojan la secuencia de bytes que tienen allí y xprop simplemente suponiendo será ISO-8859-1.)


Gracias, ese es un enfoque claramente más limpio. No estaba al tanto de esta propiedad.
r-

@ rr: lo actualicé para demostrar también la observación, _NET_WM_NAMEpor lo que mi código ahora proporciona una prueba de concepto de exactamente lo que solicitó.
ssokolow

6

Bueno, gracias al comentario de @ Basile, aprendí mucho y se me ocurrió la siguiente muestra de trabajo:

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

En lugar de ejecutarse xdotoolingenuamente, escucha sincrónicamente los eventos generados por X, que es exactamente lo que estaba buscando.


si está utilizando el administrador de ventanas xmonad, entonces necesita incluir XMonad.Hooks.EwmhDesktops en su configuración
Vasiliy Kevroletin
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.