Получение уведомлений об изменениях заголовка окна

9

... без опроса.

Я хочу определить, когда текущее окно изменяется, чтобы я мог обновить часть пользовательского интерфейса в моей системе.

Точки интересов:

  • уведомления в реальном времени. Лаг с 0,2 с - это нормально, с лагом в 1 с - с задержкой в ​​5 с - абсолютно недопустимо.
  • удобство использования ресурсов: по этой причине я хочу избежать голосования. Запуск xdotool getactivewindow getwindownameкаждые, скажем, полсекунды, работает вполне нормально ... но порождает ли 2 процесса в секунду все, что дружественно для моей системы?

В bspwm, можно использовать, bspc subscribeкоторый печатает строку с некоторыми (очень) базовыми характеристиками, каждый раз, когда фокус окна меняется. Этот подход на первый взгляд кажется хорошим, но его прослушивание не позволяет определить, когда заголовок окна изменяется сам по себе (например, изменение вкладок в веб-браузере останется незамеченным).

Итак, порождает ли новый процесс каждые полсекунды нормально в Linux, и если нет, то как я могу улучшить ситуацию?

Одна вещь, которая приходит мне в голову, это попытаться подражать тому, что делают оконные менеджеры. Но могу ли я написать хуки для таких событий, как «создание окна», «запрос на изменение заголовка» и т. Д., Независимо от рабочего оконного менеджера, или мне нужно стать самим оконным менеджером? Нужен ли для этого рут?

(Еще одна вещь, которая пришла мне в голову, - это посмотреть на xdotoolкод и эмулировать только те вещи, которые меня интересуют, чтобы я мог избежать всего процесса, порождающего шаблон, но это все равно будет опрос.)

rr-
источник

Ответы:

4

Я не мог заставить ваш подход с изменением фокуса работать надежно в Kwin 4.x, но современные оконные менеджеры поддерживают _NET_ACTIVE_WINDOWсвойство в корневом окне, к которому вы можете прослушивать изменения.

Вот реализация Python только этого:

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

Более комментируемая версия, которую я написал в качестве примера для кого-то, находится в этой сути .

ОБНОВЛЕНИЕ: Теперь, это также демонстрирует, что вторая половина (слушая _NET_WM_NAME) делает именно то, что было запрошено.

ОБНОВЛЕНИЕ № 2: ... и третья часть: возвращаемся к тому, WM_NAMEчто что-то вроде xterm не установлено _NET_WM_NAME. (Последний кодируется в кодировке UTF-8, в то время как первый должен использовать унаследованную кодировку символов, называемую составным текстом, но, поскольку никто, кажется, не знает, как с ним работать, вы получаете программы, выбрасывающие любой поток байтов, который они имеют, и xprop просто предполагая, что это будет ISO-8859-1.)

ssokolow
источник
Спасибо, это явно более чистый подход. Я не знал об этой собственности.
rr-
@ rr- Я обновил его, чтобы также продемонстрировать просмотр, _NET_WM_NAMEпоэтому мой код теперь служит подтверждением концепции именно того, о чем вы просили.
Соколов
6

Что ж, благодаря комментарию @ Basile я многому научился и придумал следующий рабочий пример:

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

Вместо того, чтобы работать xdotoolнаивно, он синхронно слушает события, сгенерированные X, что я и делал.

rr-
источник
если вы используете оконный менеджер xmonad, вам нужно включить XMonad.Hooks.EwmhDesktops в вашу конфигурацию
Василий Кевролетин