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).
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 xpropsimplemente 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:
_NET_WM_NAME
por lo que mi código ahora proporciona una prueba de concepto de exactamente lo que solicitó.Bueno, gracias al comentario de @ Basile, aprendí mucho y se me ocurrió la siguiente muestra de trabajo:
En lugar de ejecutarse
xdotool
ingenuamente, escucha sincrónicamente los eventos generados por X, que es exactamente lo que estaba buscando.fuente