ActivityWatch/aw-watcher-window

View on GitHub
aw_watcher_window/xlib.py

Summary

Maintainability
A
2 hrs
Test Coverage
import logging
from typing import Optional

import Xlib
import Xlib.display
from Xlib import X
from Xlib.xobject.drawable import Window

from .exceptions import FatalError

logger = logging.getLogger(__name__)

display = Xlib.display.Display()
screen = display.screen()

NET_WM_NAME = display.intern_atom("_NET_WM_NAME")
UTF8_STRING = display.intern_atom("UTF8_STRING")


def _get_current_window_id() -> Optional[int]:
    atom = display.get_atom("_NET_ACTIVE_WINDOW")
    window_prop = screen.root.get_full_property(atom, X.AnyPropertyType)

    if window_prop is None:
        logger.warning("window_prop was None")
        return None

    # window_prop may contain more than one value, but it seems that it's always the first we want.
    # The second has in my attempts always been 0 or rubbish.
    window_id = window_prop.value[0]
    return window_id if window_id != 0 else None


def _get_window(window_id: int) -> Window:
    return display.create_resource_object("window", window_id)


def get_current_window() -> Optional[Window]:
    """
    Returns the current window, or None if no window is active.
    """
    try:
        window_id = _get_current_window_id()
        if window_id is None:
            return None
        else:
            return _get_window(window_id)
    except Xlib.error.ConnectionClosedError:
        # when the X server closes the connection, we should exit
        # note that stdio is probably closed at this point, so we can't print anything (causes OSError)
        try:
            logger.warning("X server closed connection, exiting")
        except OSError:
            pass
        raise FatalError()


# Things that can lead to unknown cls/name:
#  - (cls+name) Empty desktop in xfce (no window focused)
#  - (name) Chrome (fixed, didn't support when WM_NAME was UTF8_STRING)


def get_window_name(window: Window) -> str:
    """After some annoying debugging I resorted to pretty much copying selfspy.
    Source: https://github.com/gurgeh/selfspy/blob/8a34597f81000b3a1be12f8cde092a40604e49cf/selfspy/sniff_x.py#L165"""
    try:
        d = window.get_full_property(NET_WM_NAME, UTF8_STRING)
    except Xlib.error.XError as e:
        logger.warning(
            f"Unable to get window property NET_WM_NAME, got a {type(e).__name__} exception from Xlib"
        )
        # I strongly suspect window.get_wm_name() will also fail and we should return "unknown" right away.
        # But I don't know, so I pass the thing on, for now.
        d = None
    if d is None or d.format != 8:
        # Fallback.
        r = window.get_wm_name()
        if isinstance(r, str):
            return r
        else:
            logger.warning(
                "I don't think this case will ever happen, but not sure so leaving this message here just in case."
            )
            return r.decode("latin1")  # WM_NAME with type=STRING.
    else:
        # Fixing utf8 issue on Ubuntu (https://github.com/gurgeh/selfspy/issues/133)
        # Thanks to https://github.com/gurgeh/selfspy/issues/133#issuecomment-142943681
        try:
            return d.value.decode("utf8")
        except UnicodeError:
            logger.warning(
                f"Failed to decode one or more characters which will be skipped, bytes are: {d.value}"
            )
            if isinstance(d.value, bytes):
                return d.value.decode("utf8", "ignore")
            else:
                return d.value.encode("utf8").decode("utf8", "ignore")


def get_window_class(window: Window) -> str:
    cls = None

    try:
        cls = window.get_wm_class()
    except Xlib.error.BadWindow:
        logger.warning("Unable to get window class, got a BadWindow exception.")

    # TODO: Is this needed?
    # nikanar: Indeed, it seems that it is. But it would be interesting to see how often this succeeds, and if it is low, maybe fail earlier.
    if not cls:
        print("")
        logger.warning("Code made an unclear branch")
        try:
            window = window.query_tree().parent
        except Xlib.error.XError as e:
            logger.warning(
                f"Unable to get window query_tree().parent, got a {type(e).__name__} exception from Xlib"
            )
            return "unknown"
        if window:
            return get_window_class(window)
        else:
            return "unknown"

    cls = cls[1]
    return cls


def get_window_pid(window: Window) -> str:
    atom = display.get_atom("_NET_WM_PID")
    pid_property = window.get_full_property(atom, X.AnyPropertyType)
    if pid_property:
        pid = pid_property.value[-1]
        return pid
    else:
        # TODO: Needed?
        raise Exception("pid_property was None")


if __name__ == "__main__":
    from time import sleep

    while True:
        print("-" * 20)
        window = get_current_window()
        if window is None:
            print("unable to get active window")
            name, cls = "unknown", "unknown"
        else:
            cls = get_window_class(window)
            name = get_window_name(window)
        print("name:", name)
        print("class:", cls)

        sleep(1)