Como ser notificado sobre alterações no título da janela

8

... sem pesquisa.

Eu quero detectar quando a janela atualmente focada muda para que eu possa atualizar uma parte da GUI personalizada no meu sistema.

Pontos de interesse:

  • notificações em tempo real. Ter um atraso de 0,2s é bom, ter atraso de 1s é meh, ter atraso de 5s é totalmente inaceitável.
  • facilidade de recurso: por esse motivo, desejo evitar a pesquisa. A execução de xdotool getactivewindow getwindowname a cada meio segundo funciona muito bem ... mas está gerando 2 processos por segundo tão amigáveis para o meu sistema?

Em bspwm , pode-se usar bspc subscribe , que imprime uma linha com algumas estatísticas (muito) básicas, sempre que o foco da janela muda. Essa abordagem parece boa no começo, mas ouvir isso não detectará quando o título da janela mudar sozinho (por exemplo, alterar as guias no navegador da Web passará despercebido dessa maneira).

Então, está gerando novos processos a cada meio segundo no Linux, e se não, como posso fazer melhor?

Uma coisa que me vem à mente é tentar emular o que os gerenciadores de janelas fazem. Mas posso escrever ganchos para eventos como "criação de janelas", "solicitação de mudança de título" etc. independentemente do gerenciador de janelas de trabalho, ou preciso me tornar um gerenciador de janelas? Eu preciso de root para isso?

(Outra coisa que me veio à mente é olhar para o código de xdotool e emular apenas as coisas que me interessam para que eu possa evitar todo o boilerplate de desova do processo, mas ainda seria polling.)

    
por rr- 09.07.2015 / 19:46

2 respostas

5

Eu não consegui fazer com que sua abordagem de mudança de foco funcionasse de forma confiável no Kwin 4.x, mas os gerenciadores de janelas modernos mantêm uma propriedade _NET_ACTIVE_WINDOW na janela raiz para a qual você pode ouvir alterações.

Aqui está uma implementação em Python apenas disso:

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

A versão mais comentada que escrevi como exemplo para alguém está em esta essência .

UPDATE: Agora, ele também demonstra a segunda metade (ouvindo _NET_WM_NAME ) para fazer exatamente o que foi solicitado.

UPDATE # 2: ... e a terceira parte: Voltando a WM_NAME se algo como xterm não tiver definido _NET_WM_NAME . (O último é codificado em UTF-8, enquanto o primeiro deve usar uma codificação de caracteres herdada chamada compound texto mas, como ninguém parece saber como trabalhar com ele, você obtém programas lançando qualquer fluxo de bytes que tenha lá e xprop considerando que será ISO-8859-1.

    
por 02.01.2017 / 12:54
6

Bem, graças ao comentário do @ Basile, aprendi muito e desenvolvi a seguinte amostra de trabalho:

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

Em vez de executar xdotool ingenuamente, ele escuta de forma síncrona os eventos gerados por X, que é exatamente o que eu procurava.

    
por 09.07.2015 / 21:22