PythonでX Window Systemのウィンドウタイトルを取得する

つい先日、X 環境のアプリケーションがファイルを開くまで待機するという処理を Python で書いていたのですが、ウィンドウタイトルを取得するためにわざわざ `wmctrl -l` を何度も起動するのも何だかなーと思ったので、`wmctrl` のソースを読みながら `pyglet` モジュールを使ってそれっぽい処理を書いてみました。

つたないコードですが二条項BSDライセンスで提供しますので、もし何かの参考になるようでしたらご自由にお使いください。

```py3:wmctrl.py
#!/usr/bin/env python3
# vim:fileencoding=utf-8

# Copyright (c) 2014 Masami HIRATA <msmhrt@gmail.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#     1. Redistributions of source code must retain the above copyright notice,
#        this list of conditions and the following disclaimer.
#
#     2. Redistributions in binary form must reproduce the above copyright
#        notice, this list of conditions and the following disclaimer in the
#        documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

__all__ = ["WMCtrl"]

from ctypes import (POINTER,
                    byref,
                    cast,
                    c_buffer,
                    c_char_p,
                    c_int,
                    c_ubyte,
                    c_ulong,
                    memmove,
                    pointer,
                    sizeof)

from pyglet.window.xlib import xlib
from pyglet.compat import asbytes

X_TRUE = xlib.True_
X_FALSE = xlib.False_
X_SUCCESS = xlib.Success
MAX_PROPERTY_VALUE_LEN = 4096

# from X11/Xatom.h
XA_CARDINAL = 6
XA_STRING = 31
XA_WINDOW = 33


class WMCtrl:
    def __init__(self):
        self.display = xlib.XOpenDisplay(None)
        self.root = xlib.XDefaultRootWindow(self.display)
        self.xatom_net_wm_name = self.get_xatom("_NET_WM_NAME")

    def get_xatom(self, name, exists=False):
        only_if_exists = X_TRUE if exists else X_FALSE
        xatom = xlib.XInternAtom(self.display,
                                 asbytes(name),
                                 only_if_exists)
        return xatom

    def get_property(self, window, xatom_property_type, property_name):
        return_xatom_property_type = xlib.Atom()
        return_c_int_format = c_int()
        return_c_ulong_nitems = c_ulong()
        return_c_ulong_bytes_after = c_ulong()
        return_c_ubyte_p_property_orig = pointer(c_ubyte())

        if window is None:
            window = self.root

        xatom_property_name = self.get_xatom(property_name, exists=True)
        if xatom_property_name == 0:
            return None

        status = xlib.XGetWindowProperty(self.display,
                                         window,
                                         xatom_property_name,
                                         0,
                                         MAX_PROPERTY_VALUE_LEN // 4,
                                         X_FALSE,
                                         xatom_property_type,
                                         byref(return_xatom_property_type),
                                         byref(return_c_int_format),
                                         byref(return_c_ulong_nitems),
                                         byref(return_c_ulong_bytes_after),
                                         byref(return_c_ubyte_p_property_orig))

        if status != X_SUCCESS:
            print("Can't get {} property. ({})",
                  property_name,
                  status)

        if xatom_property_type != return_xatom_property_type.value:
            print("Invalid type of {} property. ({} != {})".format(
                  property_name,
                  xatom_property_type,
                  return_xatom_property_type.value))
            xlib.XFree(return_c_ubyte_p_property_orig)
            return None

        bytes_per_item = return_c_int_format.value // 8
        if bytes_per_item == 4:
            bytes_per_item = sizeof(c_ulong)

        property_size = (bytes_per_item * return_c_ulong_nitems.value)
        # +1 is for NUL termination
        return_c_char_p_property = c_buffer(property_size + 1)
        memmove(return_c_char_p_property,
                return_c_ubyte_p_property_orig,
                property_size)
        xlib.XFree(return_c_ubyte_p_property_orig)

        return return_c_char_p_property

    def get_window_list(self):
        c_window_p_client_list = self.get_property(self.root,
                                                   XA_WINDOW,
                                                   "_NET_CLIENT_LIST")

        if c_window_p_client_list is None:
            c_window_p_client_list = self.get_property(self.root,
                                                       XA_CARDINAL,
                                                       "_WIN_CLIENT_LIST")

        if c_window_p_client_list is None:
            print("Can't get _(NET|WIN)_CLIENT_LIST property.")
            return []

        nitems = (len(c_window_p_client_list) - 1) // sizeof(xlib.Window)
        c_window_p_client_list = cast(c_window_p_client_list,
                                      POINTER(xlib.Window * nitems))

        window_list = []
        for window in c_window_p_client_list.contents:
            window_list.append(window)
        return window_list

    def get_window_title(self, window):
        text_property = xlib.XTextProperty()

        status = xlib.XGetTextProperty(self.display,
                                       window,
                                       text_property,
                                       self.xatom_net_wm_name)
        if status == 0 or text_property.nitems < 1 or text_property.value == 0:
            status = xlib.XGetWMName(self.display, window, text_property)
        if status == 0 or text_property.nitems < 1 or text_property.value == 0:
            return None

        c_char_ppp_title = pointer(pointer(c_char_p()))
        c_int_p_count = pointer(c_int())
        xlib.XmbTextPropertyToTextList(self.display,
                                       text_property,
                                       c_char_ppp_title,
                                       c_int_p_count)

        if c_int_p_count.contents.value < 1:
            return None
        return c_char_ppp_title.contents.contents.value.decode('utf-8')

    def get_window_titles(self):
        titles = []
        for window in self.get_window_list():
            title = self.get_window_title(window)
            if title is None:
                continue
            titles.append(title)

        return titles


def main():
    titles = WMCtrl().get_window_titles()
    for title in titles:
        print(title)

if __name__ == '__main__':  # pragma: nocover
    main()
```