MestreLion / ewmh-client

EWMH (Extended Window Manager Hints) Client API
GNU General Public License v3.0
2 stars 0 forks source link

Some examples #3

Open Kalmat opened 1 year ago

Kalmat commented 1 year ago

I'm pasting here the implementation of some ewmh functions (and some others I needed) just in case you can re-use it in any way. As I mentioned in a previous issue, these functions were intended to be used by myself exclusively, so they are not suitable as they are now for a general module and therefore some ideas I was also mentioning are not reflected here. Anyway, if this can be useful in any way, much better than throwing it all away!

class _XWindowWrapper:

    def __init__(self, win: str | int | Window = None, display: Xlib.display.Display = None, root: Window = None):

        if not display:
            display = Xlib.display.Display()
        self.display = display

        if not root:
            root = self.display.screen().root
        self.root = root
        self.rid = self.root.id
        self.xlib = None

        if not win:
            win = self.display.create_resource_object('window', self.rid)
        elif isinstance(win, int):
            win = self.display.create_resource_object('window', win)
        elif isinstance(win, str):
            win = display.create_resource_object('window', int(win, base=16))
        self.win = win
        assert isinstance(self.win, Window)
        self.id = self.win.id
        # self._saveWindowInitValues()  # Store initial Window parameters to allow reset and other actions
        self.transientWindow: _XWindowWrapper | None = None
        self.keepCheckin: bool = False

    def _saveWindowInitValues(self) -> None:
        # Saves initial rect values to allow reset to original position, size, state and hints.
        self._init_rect = self.getWindowRect()
        self._init_state = self.getWmState()
        self._init_hints = self.win.get_wm_hints()
        self._init_normal_hints = self.win.get_wm_normal_hints()
        self._init_attributes = self.getAttributes()  # can't be modified
        self._init_xAttributes = self.XlibAttributes()
        self._init_wm_prots = self.win.get_wm_protocols()
        self._init_states = self.getWmState()
        self._init_types = self.getWmWindowType()

    def renewWindowObject(self) -> _XWindowWrapper:
        # Not sure if this is necessary.
        #     - Window object may change (I can't remember in which cases, but it did)
        #     - It's assuming at least id doesn't change... if it does, well, nothing to do with that window anymore
        self.display.close()  # -> Is this necessary... and when?
        self.display = Xlib.display.Display()
        self.root = self.display.screen().root
        self.rid = self.root.id
        self.win = self.display.create_resource_object('window', self.id)
        return self

    def getWindow(self) -> Window:
        return self.win

    def getDisplay(self) -> Xlib.display.Display:
        return self.display

    def getScreen(self) -> Xlib.protocol.rq.DictWrapper:
        return cast(Xlib.protocol.rq.DictWrapper, self.display.screen())

    def getRoot(self) -> Window:
        return self.root

    def getProperty(self, name: str, prop_type: int = Xlib.X.AnyPropertyType) -> list[int]:

        atom: int = self.display.get_atom(name)
        properties: Xlib.protocol.request.GetProperty = self.win.get_full_property(atom, prop_type)
        if properties:
            props: list[int] = properties.value
            return [p for p in props]
        return []

    def getWmState(self, text: bool = True) -> list[str] | list[int] | list[Any]:

        states = self.win.get_full_property(self.display.get_atom(WM_STATE, False), Xlib.X.AnyPropertyType)
        if states:
            stats: list[int] = states.value
            if not text:
                return [s for s in stats]
            else:
                return [self.display.get_atom_name(s) for s in stats]
        return []

    def getWmWindowType(self, text: bool = True) -> list[str] | list[int]:
        types = self.getProperty(WM_WINDOW_TYPE)
        if not text:
            return [t for t in types]
        else:
            return [self.display.get_atom_name(t) for t in types]

    def setProperty(self, prop: str | int, data: list[int]):

        if isinstance(prop, str):
            prop = self.display.get_atom(prop)

        if type(data) is str:
            dataSize = 8
        else:
            data = (data + [0] * (5 - len(data)))[:5]
            dataSize = 32

        ev = Xlib.protocol.event.ClientMessage(window=self.win, client_type=prop, data=(dataSize, data))
        mask = Xlib.X.SubstructureRedirectMask | Xlib.X.SubstructureNotifyMask
        self.display.send_event(destination=self.rid, event=ev, event_mask=mask)
        self.display.flush()

    def setWmState(self, action: int, state: str | int, state2: str | int = 0):

        if isinstance(state, str):
            state = self.display.get_atom(state, True)
        if isinstance(state2, str):
            state2 = self.display.get_atom(state2, True)
        self.setProperty(WM_STATE, [action, state, state2, 1])
        self.display.flush()

    def setWmType(self, prop: str | int, modeReplace=True):

        if isinstance(prop, str):
            prop = self.display.get_atom(prop, False)

        geom = self.win.get_geometry()
        mode = Xlib.X.PropModeReplace if modeReplace else Xlib.X.PropModeAppend
        self.win.unmap()
        self.win.change_property(self.display.get_atom(WM_WINDOW_TYPE, True), Xlib.Xatom.ATOM, 32, [prop, ], mode)
        self.win.map()
        self.display.flush()
        self.setMoveResize(x=geom.x, y=geom.y, width=geom.width, height=geom.height)

    def setMoveResize(self, x: int, y: int, width: int, height: int):
        self.win.configure(x=x, y=y, width=width, height=height)
        self.display.flush()

    def setStacking(self, stack_mode: int):
        self.win.configure(stack_mode=stack_mode)
        self.display.flush()

    def hide(self):
        self.win.unmap_sub_windows()
        self.display.flush()
        self.win.unmap()
        self.display.flush()

    def show(self):
        self.win.map()
        self.display.flush()
        self.win.map_sub_windows()
        self.display.flush()

    def close(self):
        self.setProperty(CLOSE_WINDOW, [0, 0, 0, 0, 0])

    def getWmName(self) -> str | None:
        return self.win.get_wm_name()

    # def _globalEventListener(self, events):
    #
    #     from Xlib import X
    #     from Xlib.ext import record
    #     from Xlib.display import Display
    #     from Xlib.protocol import rq
    #
    #     def handler(reply):
    #         data = reply.data
    #         while len(data):
    #             event, data = rq.EventField(None).parse_binary_value(data, display.display, None, None)
    #
    #             if event.type == X.KeyPress:
    #                 print('pressed')
    #             elif event.type == X.KeyRelease:
    #                 print('released')
    #
    #     display = Display()
    #     context = display.record_create_context(0, [record.AllClients], [{
    #         'core_requests': (0, 0),
    #         'core_replies': (0, 0),
    #         'ext_requests': (0, 0, 0, 0),
    #         'ext_replies': (0, 0, 0, 0),
    #         'delivered_events': (0, 0),
    #         'device_events': (X.KeyReleaseMask, X.ButtonReleaseMask),
    #         'errors': (0, 0),
    #         'client_started': False,
    #         'client_died': False,
    #     }])
    #     display.record_enable_context(context, handler)
    #     display.record_free_context(context)
    #
    #     while True:
    #         display.screen().root.display.next_event()

    def setAcceptInput(self, setTo: bool):

        if self.xlib is None:
            x11 = find_library('X11')
            self.xlib = cdll.LoadLibrary(str(x11))
        d = self.xlib.XOpenDisplay(0)
        root = self.xlib.XRootWindow(d, self.xlib.XDefaultScreen(d))

        def _createTransient(parent):

            if self.transientWindow is not None:
                _closeTransientWindow()

            geom = self.win.get_geometry()
            window = self.xlib.XCreateSimpleWindow(
                d,
                self.id,
                0, 0, geom.width, geom.height,
                0,
                0,
                0
            )
            self.xlib.XSelectInput(d, window, Xlib.X.ButtonPressMask | Xlib.X.ButtonReleaseMask | Xlib.X.KeyPressMask | Xlib.X.KeyReleaseMask)
            self.xlib.XFlush(d)
            self.xlib.XMapWindow(d, window)
            self.xlib.XFlush(d)
            self.xlib.XSetTransientForHint(d, window, parent)
            self.xlib.XFlush(d)
            self.xlib.XRaiseWindow(d, window)
            self.xlib.XFlush(d)
            return window

        def _closeTransientWindow():
            self.transientWindow.win.unmap()
            self.transientWindow.close()
            self.xlib.XFlush(d)
            self.xlib.XClearWindow(d, self.id)
            self.xlib.XFlush(d)
            time.sleep(0.1)
            self.transientWindow = None

        def _checkDisplayEvents(events: list[int], keep):

            self.keep = keep
            self.root.change_attributes(event_mask=Xlib.X.PropertyChangeMask | Xlib.X.SubstructureNotifyMask)

            while keep.is_set():
                if self.root.display.pending_events():
                    event = self.root.display.next_event()
                    try:
                        child = event.child
                    except:
                        child = None
                    if self.win in (event.window, child) and event.type in events:
                        if event.type == Xlib.X.ConfigureNotify:
                            self.transientWindow = _XWindowWrapper(_createTransient(self.id))
                            # Should be enough just moving/resizing transient window, but it's not
                            # self.transientWindow.win.configure(x=0, y=0, width=event.width, height=event.height, stack_mode=Xlib.X.Above)
                            # xlib.XFlush(d)
                        elif event.type == Xlib.X.DestroyNotify:
                            self.transientWindow.close()
                            self.transientWindow = None
                            keep.is_set.clear()
                time.sleep(0.1)

        if setTo:
            if self.transientWindow is not None:
                self.keep.clear()
                self.checkThread.join()
                _closeTransientWindow()
        else:
            window = _createTransient(self.id)
            self.transientWindow: _XWindowWrapper = _XWindowWrapper(window)
            self.keep = threading.Event()
            self.keep.set()
            self.checkThread = threading.Thread(target=_checkDisplayEvents, args=([Xlib.X.ConfigureNotify, Xlib.X.DestroyNotify], self.keep, ))
            self.checkThread.daemon = True
            self.checkThread.start()

    def setWmHints(self, hint):
        # Leaving this as an example
        # {'flags': 103, 'input': 1, 'initial_state': 1, 'icon_pixmap': <Pixmap 0x02a22304>, 'icon_window': <Window 0x00000000>, 'icon_x': 0, 'icon_y': 0, 'icon_mask': <Pixmap 0x02a2230b>, 'window_group': <Window 0x02a00001>}
        hints: Xlib.protocol.rq.DictWrapper = self.win.get_wm_hints()
        if hints:
            hints.input = 1
        self.win.set_wm_hints(hints)
        self.display.flush()

    def addWmProtocol(self, atom):
        prots = self.win.get_wm_protocols()
        if atom not in prots:
            prots.append(atom)
        self.win.set_wm_protocols(prots)
        self.display.flush()

    def delWmProtocol(self, atom):
        prots = self.win.get_wm_protocols()
        new_prots = [p for p in prots if p != atom]
        prots = new_prots
        self.win.set_wm_protocols(prots)

    def getAttributes(self) -> Xlib.protocol.request.GetWindowAttributes:
        return self.win.get_attributes()

    class _XWindowAttributes(Structure):
        _fields_ = [('x', c_int32), ('y', c_int32),
                    ('width', c_int32), ('height', c_int32), ('border_width', c_int32),
                    ('depth', c_int32), ('visual', c_ulong), ('root', c_ulong),
                    ('class', c_int32), ('bit_gravity', c_int32),
                    ('win_gravity', c_int32), ('backing_store', c_int32),
                    ('backing_planes', c_ulong), ('backing_pixel', c_ulong),
                    ('save_under', c_int32), ('colourmap', c_ulong),
                    ('mapinstalled', c_uint32), ('map_state', c_uint32),
                    ('all_event_masks', c_ulong), ('your_event_mask', c_ulong),
                    ('do_not_propagate_mask', c_ulong), ('override_redirect', c_int32), ('screen', c_ulong)]

    def XlibAttributes(self) -> tuple[bool, _XWindowWrapper._XWindowAttributes]:
        attr = _XWindowWrapper._XWindowAttributes()
        try:
            if self.xlib is None:
                x11 = find_library('X11')
                self.xlib = cdll.LoadLibrary(str(x11))
            d = self.xlib.XOpenDisplay(0)
            self.xlib.XGetWindowAttributes(d, self.id, byref(attr))
            self.xlib.XCloseDisplay(d)
            resOK = True
        except:
            resOK = False
        return resOK, attr

        # Leaving this as reference of using X11 library
        # https://github.com/evocount/display-management/blob/c4f58f6653f3457396e44b8c6dc97636b18e8d8a/displaymanagement/rotation.py
        # https://github.com/nathanlopez/Stitch/blob/master/Configuration/mss/linux.py
        # https://gist.github.com/ssokolow/e7c9aae63fb7973e4d64cff969a78ae8
        # https://stackoverflow.com/questions/36188154/get-x11-window-caption-height
        # https://refspecs.linuxfoundation.org/LSB_1.3.0/gLSB/gLSB/libx11-ddefs.html
        # s = xlib.XDefaultScreen(d)
        # root = xlib.XDefaultRootWindow(d)
        # fg = xlib.XBlackPixel(d, s)
        # bg = xlib.XWhitePixel(d, s)
        # w = xlib.XCreateSimpleWindow(d, root, 600, 300, 400, 200, 0, fg, bg)
        # xlib.XMapWindow(d, w)
        # time.sleep(4)
        # a = xlib.XInternAtom(d, "_GTK_FRAME_EXTENTS", True)
        # if not a:
        #     a = xlib.XInternAtom(d, "_NET_FRAME_EXTENTS", True)
        # t = c_int()
        # f = c_int()
        # n = c_ulong()
        # b = c_ulong()
        # xlib.XGetWindowProperty(d, w, a, 0, 4, False, Xlib.X.AnyPropertyType, byref(t), byref(f), byref(n), byref(b), byref(attr))
        # r = c_ulong()
        # x = c_int()
        # y = c_int()
        # w = c_uint()
        # h = c_uint()
        # b = c_uint()
        # d = c_uint()
        # xlib.XGetGeometry(d, hWnd.id, byref(r), byref(x), byref(y), byref(w), byref(h), byref(b), byref(d))
        # print(x, y, w, h)
        # Other references (send_event and setProperty):
        # prop = DISP.intern_atom(WM_CHANGE_STATE, False)
        # data = (32, [Xlib.Xutil.IconicState, 0, 0, 0, 0])
        # ev = Xlib.protocol.event.ClientMessage(window=self._hWnd.id, client_type=prop, data=data)
        # mask = Xlib.X.SubstructureRedirectMask | Xlib.X.SubstructureNotifyMask
        # DISP.send_event(destination=ROOT, event=ev, event_mask=mask)
        # data = [Xlib.Xutil.IconicState, 0, 0, 0, 0]
        # _setProperty(_type="WM_CHANGE_STATE", data=data, mask=mask)
        # for atom in w.list_properties():
        #     print(DISP.atom_name(atom))
        # props = DISP.xrandr_list_output_properties(output)
        # for atom in props.atoms:
        #     print(atom, DISP.get_atom_name(atom))
        #     print(DISP.xrandr_get_output_property(output, atom, 0, 0, 1000)._data['value'])

    def _getBorderSizes(self):

        class App(tk.Tk):

            def __init__(self):
                super().__init__()
                self.geometry('0x0+200+200')
                self.update_idletasks()

                pos = self.geometry().split('+')
                self.bar_height = self.winfo_rooty() - int(pos[2])
                self.border_width = self.winfo_rootx() - int(pos[1])
                self.destroy()

            def getTitlebarHeight(self):
                return self.bar_height

            def getBorderWidth(self):
                return self.border_width

        app = App()
        # app.mainloop()
        return app.getTitlebarHeight(), app.getBorderWidth()

    def getExtraFrameSize(self, includeBorder: bool = True) -> tuple[int, int, int, int]:
        """
        Get the extra space, in pixels, around the window, including or not the border.
        Notice not all applications/windows will use this property values

        :param includeBorder: set to ''False'' to avoid including borders
        :return: (left, top, right, bottom) additional frame size in pixels, as a tuple of int
        """
        display = self.display
        prop = "_GTK_FRAME_EXTENTS"
        atom = display.intern_atom(prop, True)
        if not atom:
            prop = "_NET_FRAME_EXTENTS"
        ret: list[int] = self.getProperty(prop)
        if not ret: ret = [0, 0, 0, 0]
        borderWidth = 0
        if includeBorder:
            # _, a = self.XlibAttributes()
            # borderWidth = a.border_width
            if includeBorder:
                titleHeight, borderWidth = self._getBorderSizes()
        frame = (ret[0] + borderWidth, ret[2] + borderWidth, ret[1] + borderWidth, ret[3] + borderWidth)
        return frame

    def getClientFrame(self) -> Rect:
        """
        Get the client area of window including scroll, menu and status bars, as a Rect (x, y, right, bottom)
        Notice that this method won't match non-standard window decoration sizes

        :return: Rect struct
        """
        # res, a = self.XlibAttributes()
        # if res:
        #     ret = Rect(a.x, a.y, a.x + a.width, a.y + a.height)
        # else:
        #     ret = Rect(self.left, self.top, self.right, self.bottom)
        # Didn't find a way to get title bar height using Xlib
        titleHeight, borderWidth = self._getBorderSizes()
        geom = self.win.get_geometry()
        ret = Rect(int(geom.left + borderWidth), int(geom.y + titleHeight), int(geom.x + geom.width - borderWidth), int(geom.y + geom.widh - borderWidth))
        return ret

    def getWindowRect(self) -> Rect:
        # https://stackoverflow.com/questions/12775136/get-window-position-and-size-in-python-with-xlib - mgalgs
        win = self.win
        geom = win.get_geometry()
        x = geom.x
        y = geom.y
        while True:
            parent = win.query_tree().parent
            pgeom = parent.get_geometry()
            x += pgeom.x
            y += pgeom.y
            if parent.id == self.rid:
                break
            win = parent
        w = geom.width
        h = geom.height
        return Rect(x, y, x + w, y + h)

def _xlibGetAllWindows(parent: Window | None = None, title: str = "", klass: tuple[str, str] | None = None) -> list[Window]:

    parent = parent or Xlib.display.Display().screen().root
    allWindows = [parent]

    def findit(hwnd: Window):
        query = hwnd.query_tree()
        for child in query.children:
            allWindows.append(child)
            findit(child)

    findit(parent)
    if not title and not klass:
        return allWindows
    else:
        windows: list[Window] = []
        for window in allWindows:
            try:
                winTitle = window.get_wm_name()
            except:
                winTitle = ""
            try:
                winClass = window.get_wm_class()
            except:
                winClass = ""
            if (title and winTitle == title) or (klass and winClass == klass):
                windows.append(window)
        return windows
        # return [window for window in allWindows if ((title and window.get_wm_name() == title) or
        #                                             (klass and window.get_wm_class() == klass))]
MestreLion commented 1 year ago

Thanks!!! I'll study them and give some feedback on how it can cooperate with my lib.

Kalmat commented 1 year ago

Not at all!!! It's not meant for feedback, just to reuse it if it's useful in any way! Otherwise, you can just ignore it without thinking it twice! In fact, this all will go to the trash bin and will be replaced by your module!!!!

After reviewing all your comments, I separated set_property() and send_event() functions. Whilst set_property() is very simple (like setWmState() or setWmType() are), send_event() is not... That's what I was trying to explain about having simple/pre-defined functions and more "general"/complex functions for advanced users/inevitably complex actions. Like this:

def sendMessage(self, prop: str | int, data: list[int]):

    if isinstance(prop, str):
        prop = self.display.get_atom(prop)

    if type(data) is str:
        dataSize = 8
    else:
        data = (data + [0] * (5 - len(data)))[:5]
        dataSize = 32

    ev = Xlib.protocol.event.ClientMessage(window=self.win, client_type=prop, data=(dataSize, data))
    mask = Xlib.X.SubstructureRedirectMask | Xlib.X.SubstructureNotifyMask
    self.display.send_event(destination=self.rid, event=ev, event_mask=mask)
    self.display.flush()

def setProperty(self, prop: str | int, data: list[int], mode: Xlib.X.Atom = Xlib.X.PropModeReplace):

    if isinstance(prop, str):
        prop = self.display.get_atom(prop)

    # Format value can be 8, 16 or 32... depending on the content of data
    format = 32
    self.win.change_property(prop, Xlib.Xatom.ATOM, format, data, mode)
    self.display.flush()

def setWmState(self, action: int, state: str | int, state2: str | int = 0):

    if isinstance(state, str):
        state = self.display.get_atom(state, True)
    if isinstance(state2, str):
        state2 = self.display.get_atom(state2, True)
    self.setProperty(WM_STATE, [action, state, state2, 1])
    self.display.flush()

def setWmType(self, prop: str | int, mode: Xlib.X.Atom = Xlib.X.PropModeReplace):

    if isinstance(prop, str):
        prop = self.display.get_atom(prop, False)

    geom = self.win.get_geometry()
    self.win.unmap()
    self.setProperty(WM_WINDOW_TYPE, [prop], mode)
    self.win.map()
    self.display.flush()
    self.setMoveResize(x=geom.x, y=geom.y, width=geom.width, height=geom.height)
MestreLion commented 1 year ago
def sendMessage(self, prop: str | int, data: list[int]):
    ...
    if type(data) is str:
        dataSize = 8
    else:
        data = (data + [0] * (5 - len(data)))[:5]
        dataSize = 32
    ...

Xlib's send_event (and its usage in EWMH) has no dataSize or data: str type. That's for get/change_property. A message is always for requests / actions, data is always a 5-length list of ints, always never used with strings.

self.display.send_event(destination=self.rid, event=ev, event_mask=mask)

There's no display.send_event(destination) in Xlib. There's Window.send_event(), self being the destination. So, for a window to send an event to the Root Window (as needed by many EWMH methods), that window needs a reference to the root window.

And it's not trivial to get a root reference from an Xlib's Window: you have Window.display, but no reference to the sno or screen you need.

def setProperty(self, prop: str | int, data: list[int], mode: Xlib.X.Atom = Xlib.X.PropModeReplace):

    if isinstance(prop, str):
        prop = self.display.get_atom(prop)

    # Format value can be 8, 16 or 32... depending on the content of data
    format = 32
    self.win.change_property(prop, Xlib.Xatom.ATOM, format, data, mode)
    self.display.flush()

Without allowing bytes or format != 32, this setProperty you suggest cannot be used for strings or 16-bit ints, and setTextProperty will no longer be able to use it

Kalmat commented 1 year ago

Xlib's send_event (and its usage in EWMH) has no dataSize or data: str type. That's for get/change_property. A message is always for requests / actions, data is always a 5-length list of ints, always never used with strings.

self.display.send_event(destination=self.rid, event=ev, event_mask=mask)

First off, this implementation is totally new (perhaps has some mistakes, though I successfully tested it using PyWinCtl), it's intended for internal use and therefore not adapted to a general module, and third, it will be replaced by your module! Sorry if I led you to a confusion, but I was not suggesting this implementation since it's clearly unsufficient and non-exportable, but the abstract idea of having a number of very simple functions that internally complete the necessary data without the need for the user to provide it (e.g. setWmState or SetWmType, setWmHint, setWmProtocol... and whatever can be simplified this way), completed with a more complex function which inevitably needs more data to fulfill.

Regarding ewwmh's setProperty(), it's in fact a send_event, which seems to need a dataSize, as part of the event structure (or is it a name misleading, which in fact means "format"?);

def _setProperty(self, _type, data, win=None, mask=None):
    """
    Send a ClientMessage event to the root window
    """
    if not win:
        win = self.root
    if type(data) is str:
        dataSize = 8
    else:
        data = (data+[0]*(5-len(data)))[:5]
        dataSize = 32

    ev = protocol.event.ClientMessage(
        window=win,
        client_type=self.display.get_atom(_type), data=(dataSize, data))

    if not mask:
        mask = (X.SubstructureRedirectMask | X.SubstructureNotifyMask)
    self.root.send_event(ev, event_mask=mask)

but, in this case, data it's completed internally as well as its data size (or is it format?). So, I can invoke it in this very simple ways: self.sendMessage(CLOSE_WINDOW, []) or self.sendMessage(WM_CHANGE_STATE, [Xlib.Xutil.IconicState]) or self.sendMessage(WM_CHANGE_STATE, [Xlib.Xutil.IconicState]), using just two input params.

Sorry again, but I'm sure your implementation will be much better than mine. I was pasting this only in case you needed to reuse any piece of it (like using X11 directly, which costed me a lot to get python working examples of it); and to give a practical example of simple/pre-defined/standard functions vs. more general/complex ones.

There's no display.send_event(destination) in Xlib. There's Window.send_event(), self being the destination. So, for a window to send an event to the Root Window (as needed by many EWMH methods), that window needs a reference to the root window.

Sorry, I think I'm not properly catching your comment. If you write this:

display = Xlib.display.Display()
display.send_event(

the IDE will offer to auto-complete it, and will show this overload:

Xlib.display.Display def send_event(self,
               destination: int,
               event: Event,
               event_mask: int = ...,
               propagate: bool = ...,
               onerror: None = ...) -> None

This is all Xlib (well, python-xlib, I mean), right? Or am I missing something? Anyway, I was just curious, since I guess there are many ways to accomplish the same thing. In my experience, self.display.send_event(destination=root, event) is equivalent to self.root.send_event(event)

And it's not trivial to get a root reference from an Xlib's Window: you have Window.display, but no reference to the sno or screen you need.

Right, that's why it's better to facilitate the job to the user. Thus, root can be an optional parameter. If the user doesn't know, and passes it unfulfilled, then it can be found by "brute force". Take a look to getAllScreens() function, but in this case it would be easier, just retrieving the windows id's from each root, and comparing it with the target id:

dsp = Xlib.display.Display()  # Could window.display be used here to address the proper display, just in case? or get list of displays from d = opendir("/tmp/.X11-unix")
atom: int = dsp.get_atom(WINDOW_LIST_STACKING)
for i in range(dsp.screen_count()):
    try:
        screen = dsp.screen(i)
        root = screen.root
    except:
        continue
    windows = root.get_full_property(atom, Xlib.X.AnyPropertyType)
    for w in windows:
        if w == givenID:
            dsp.close()
            return root
dsp.close()
return None

Without allowing bytes or format != 32, this setProperty you suggest cannot be used for strings or 16-bit ints, and setTextProperty will no longer be able to use it

Totally right. Again, I was not suggesting this implementation, sorry. This implementation is incomplete and adapted to the needs of PyWinCtl solely.

Kalmat commented 1 year ago

Hi!

I am still preparing new version 0.1, but hopefully close to launch it. It's been long overdue mainly because of a multi-monitor issue, which led me to replace PyRect with a custom module (PyWinBox) and build another custom module to control monitors (PyMonCtl). I am still trying to find the way to test that all in an actual multi-monitor macOS setup. Not sure if you received a mail that I sent you (here: github@rodrigosilva.com, but not sure if it is correct) some days ago asking for your opinion (always very sharp and useful) on building those features as separate modules or as pywinctl submodules or completely integrated within (I finally decided to separate them, but still not sure if it's the optimal approach).

Also, as you know, my intention was to remove ewmh module, but it required a lot of investigation and testing. Regarding this, I think I already managed to have a (final/acceptable/preliminary-yet-usable?) version. I also decided to encapsulate it as a submodule. The question is: are you still interested in creating your own EWMH replacement? If so, just three comments: a) please, take a look at ewmhlib submodule within PyWinCtl 'dev' branch and let me know your thoughts, b) use it at your convenience for your new ewmh module (as well as my contribution and help if you need to), and c) I would love to include your new ewmh module within the next pywinctl version (I still have work to do before launching it because of the multi-monitor issue and, apart from this, I don't care about waiting if needed).

MestreLion commented 1 year ago

Hey man, long time no see you! Missing a LOT our discussions!

So, as said a few weeks (months?) ago, I'm in this new company, managing a huge project, with basically zero spare time for my personal projects. Even my prodigy git-restore-mtime has a pile of issues and reports I'm unable to handle right now.

Same goes to my ewhm initiative: as much as I'd love to dive into it once again, if only just to just finish the API and present a working prototype, I simply can't. Yes, I got your friendly email, it was the right address, and I'm truly sorry for not answering it.

So, what all of that means? Will I be able to discuss API and collaborate on an ewhm backend? No, not in the foreseeable future (2-3 months at least, might as well extend to 6-9). Have I abandoned my still unborn project? No, absolutely not, I'm still planning on resuming it as soon as I can: the old ewhm deserves a proper replacement so it can proudly rest in peace. Am I really sad for not collaborating with your amazing PyWinCtrl project and help shaping it so it has a compatible API when I ever come back to ewhm? Hell yeah, a lot!

Kalmat commented 1 year ago

Hey! So good to know you are fine! I will patiently wait in such a case. Dont' worry!!! My best wishes for you in your new project. That's the kind of projects I definitely like and are worth to "suffer": huge and complex. I'm sure you will success!

Kalmat commented 7 months ago

Hi!!! It's a long long time no hearing from you! I really hope you are doing well.

I just wanted to share some things with you, if you have the time, of course:

  1. I've been working on the ewmh-lib that I am using in my three modules (PyWinBox, PyMonCtl and PyWinCtl). Check this: https://github.com/Kalmat/EWMHlib
  2. I have not created a new module from it because:
    • Not sure if it is OK (your opinion is key here)
    • It's "your baby", so I think you should upload it (you can take all my work or parts of it as a base, if you think it's usable)
  3. Since ewmh-lib is not a separate module, I have included it inside the other modules, but keeping it up-to-date and consistent is complex (at least for me, sorry); so I would like to use it as an external module
  4. I am preparing a new version of all those modules, which fixes and improves many issues (I have not been able to do it before, because I've been very busy for some months and, in addtion to that, I am stuck trying to borrow an actual mac to test).

I was just wondering if you will have the time to create this new module, or if you think it is worth I upload this one meanwhile you create yours (I would really appreciate your revision in this case, if possible). As I mentioned, I am not in a hurry yet, since I still have to manage to get a f***g mac!!!!

PS: I had not checked your github profile before... 139 repos?!?!?! and 273 stars! WOW!!! Man, you're a LEGEND!!! HAHAHAHAHA!