Skip to content

Commit

Permalink
Added WM_NAME as _NET_WM_NAME fallback (older apps)
Browse files Browse the repository at this point in the history
Separated Props and Structs as submodules
Improved performance while maintaining multi-display, multi-screen and multi-root support
Added typing_extensions dependency at runtime
  • Loading branch information
Kalmat committed Apr 8, 2024
1 parent 1633195 commit b2ebe5e
Show file tree
Hide file tree
Showing 5 changed files with 52 additions and 62 deletions.
5 changes: 2 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,8 @@
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10'
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11'
],
)
6 changes: 3 additions & 3 deletions src/ewmhlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@


__all__ = [
"version", "displaysCount", "getDisplays", "getDisplaysInfo", "getRoots", "getRootsInfo",
"version", "displaysCount", "getDisplays", "getDisplaysInfo", "getRoots",
"defaultDisplay", "defaultScreen", "defaultRoot", "defaultEwmhRoot",
"getDisplayFromRoot", "getScreenFromRoot",
"getDisplayFromWindow", "getScreenFromWindow", "getRootFromWindow",
Expand All @@ -12,15 +12,15 @@
"Props", "Structs"
]

__version__ = "0.0.1"
__version__ = "0.1"


def version(numberOnly: bool = True):
"""Returns the current version of ewmhlib module, in the form ''x.x.xx'' as string"""
return ("" if numberOnly else "EWMHlib-")+__version__


from ._main import (displaysCount, getDisplays, getDisplaysInfo, getRoots, getRootsInfo,
from ._main import (displaysCount, getDisplays, getDisplaysInfo, getRoots,
defaultDisplay, defaultScreen, defaultRoot, defaultEwmhRoot,
getDisplayFromRoot, getScreenFromRoot,
getDisplayFromWindow, getScreenFromWindow, getRootFromWindow,
Expand Down
99 changes: 44 additions & 55 deletions src/ewmhlib/_ewmhlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,46 +18,47 @@
import Xlib.Xatom
import Xlib.Xutil
import Xlib.ext
from Xlib.ext import randr
import Xlib.xobject
from Xlib.xobject.drawable import Window as XWindow

from ewmhlib.Props import (Root, DesktopLayout, Window, WindowType, State, StateAction,
from .Props import (Root, DesktopLayout, Window, WindowType, State, StateAction,
MoveResize, DataFormat, Mode, HintAction)
from ewmhlib.Structs import DisplaysInfo, ScreensInfo, WmHints, Aspect, WmNormalHints
from .Structs import DisplaysInfo, ScreensInfo, WmHints, Aspect, WmNormalHints


defaultDisplay: Xlib.display.Display = Xlib.display.Display()
defaultScreen: Struct = defaultDisplay.screen()
defaultRoot: XWindow = defaultScreen.root


def getDisplays(forceUpdate: bool = False) -> List[Xlib.display.Display]:
def _getDisplays() -> List[Xlib.display.Display]:
"""
Get a list of connections to all existing Displays.
:param forceUpdate: display connections are set at the time of importing the module, and remains static. Set this to ''True'' to force updating it.
:return: list of display connections
"""
global _displays
if forceUpdate:
_displays = []
if not _displays and os.environ['XDG_SESSION_TYPE'].lower() != "wayland":
displays: List[Xlib.display.Display] = []
if os.environ.get('XDG_SESSION_TYPE', "").lower() != "wayland":

This comment has been minimized.

Copy link
@Avasam

Avasam Jun 16, 2024

Thanks! This fixes an uncaught exception for desktopless environments, like WSL. Which I was working around like this:

if sys.platform == "linux":
    # This variable may be missing in desktopless environment. x11 | wayland
    os.environ.setdefault("XDG_SESSION_TYPE", "x11")

# Must come after the linux XDG_SESSION_TYPE environment variable is set
from pywinctl import getTopWindowAt

This comment has been minimized.

Copy link
@Kalmat

Kalmat Jun 17, 2024

Author Owner

HI! It's a long time no see you. I hope you're doing well!

As always, thank you for your help and support!

# Wayland adds a "fake" display (typically ":1") that freezes when trying to get a connection. Using default
# Thanks to SamuMazzi - https://github.com/SamuMazzi for pointing out this issue
files: List[str] = os.listdir("/tmp/.X11-unix")
for d in files:
if d.startswith("X"):
name: str = d.replace("X", ":", 1)
try:
_displays.append(Xlib.display.Display(name))
displays.append(Xlib.display.Display(name))
except:
pass
if not _displays:
_displays = [defaultDisplay]
if not displays:
displays = [defaultDisplay]
return displays
_displays: List[Xlib.display.Display] = _getDisplays()
displaysCount: int = len(_displays)


def getDisplays() -> List[Xlib.display.Display]:
global _displays
return _displays
_displays: List[Xlib.display.Display] = []
displaysCount: int = len(getDisplays())


def getDisplaysInfo() -> dict[str, DisplaysInfo]:
Expand All @@ -84,11 +85,12 @@ def getDisplaysInfo() -> dict[str, DisplaysInfo]:
for s in range(display.screen_count()):
try:
screen: Struct = display.screen(s)
root = screen.root
screenInfo: ScreensInfo = {
"screen_number": str(s),
"is_default": (screen.root.id == defaultRoot.id),
"is_default": bool(root.id == defaultRoot.id),
"screen": screen,
"root": screen.root
"root": root
}
screens.append(screenInfo)
except:
Expand All @@ -111,9 +113,9 @@ def getDisplayFromWindow(winId: int) -> Tuple[Xlib.display.Display, Struct, XWin
:return: tuple containing display connection, screen struct and root window
"""
if displaysCount > 1 or defaultDisplay.screen_count() > 1:
global _rootsInfo
for rootData in _rootsInfo:
display, screen, root, res = rootData
rootsInfo = getRoots()
for rootData in rootsInfo:
display, screen, root = rootData
atom: int = display.get_atom(Root.CLIENT_LIST)
ret: Optional[Xlib.protocol.request.GetProperty] = root.get_full_property(atom, Xlib.X.AnyPropertyType)
if ret and hasattr(ret, "value") and winId in ret.value:
Expand All @@ -132,61 +134,46 @@ def getDisplayFromRoot(rootId: Optional[int]) -> Tuple[Xlib.display.Display, Str
:return: tuple containing display connection, screen struct and root window
"""
if rootId and rootId != defaultRoot.id and (displaysCount > 1 or defaultDisplay.screen_count() > 1):
global _rootsInfo
for rootData in _rootsInfo:
display, screen, root, res = rootData
rootsInfo = getRoots()
for rootData in rootsInfo:
display, screen, root = rootData
if rootId == root.id:
return display, screen, root
return defaultDisplay, defaultScreen, defaultRoot
getScreenFromRoot = getDisplayFromRoot


def _getRootsInfo() -> Tuple[List[Tuple[Xlib.display.Display, Struct, XWindow, Xlib.ext.randr.GetScreenResources]], List[XWindow]]:
res: Xlib.ext.randr.GetScreenResources = randr.get_screen_resources(defaultRoot)
rootsInfo: List[Tuple[Xlib.display.Display, Struct, XWindow, Xlib.ext.randr.GetScreenResources]] = [(defaultDisplay, defaultScreen, defaultRoot, res)]
roots: List[XWindow] = [defaultRoot]
displays: List[Xlib.display.Display] = getDisplays()
if len(displays) > 1 or defaultDisplay.screen_count() > 1:
def _getRoots(updateDisplays: bool = False) -> List[Tuple[Xlib.display.Display, Struct, XWindow]]:
rootsInfo: List[Tuple[Xlib.display.Display, Struct, XWindow]] = [(defaultDisplay, defaultScreen, defaultRoot)]
global displaysCount
if updateDisplays or displaysCount > 1 or defaultDisplay.screen_count() > 1:
if updateDisplays:
display = Xlib.display.Display()
screen: Struct = display.screen()
rootsInfo = [(display, screen, screen.root)]
displays: List[Xlib.display.Display] = _getDisplays()
else:
displays = getDisplays()
for display in displays:
for i in range(display.screen_count()):
try:
screen: Struct = display.screen(i)
screen = display.screen(i)
root: XWindow = screen.root
if root.id != defaultRoot.id:
res = randr.get_screen_resources(root)
rootsInfo.append((display, screen, root, res))
roots.append(root)
rootsInfo.append((display, screen, root))
except:
pass
return rootsInfo, roots
_rootsInfo, _roots = _getRootsInfo()


def getRootsInfo(forceUpdate: bool = False) -> List[Tuple[Xlib.display.Display, Struct, XWindow, Xlib.ext.randr.GetScreenResources]]:
"""
Get all roots windows objects and related information.
:param forceUpdate: roots info is retrieved at the time of importing the module, and remains static. Set this to ''True'' to force updating it.
:return: list of tuples, each of them containing: display connection, screen struct, root window and screen resources
"""
global _rootsInfo
if forceUpdate:
global _roots
_rootsInfo, _roots = _getRootsInfo()
return _rootsInfo
return rootsInfo
_roots: List[Tuple[Xlib.display.Display, Struct, XWindow]] = _getRoots()


def getRoots(forceUpdate: bool = False) -> List[XWindow]:
def getRoots() -> List[Tuple[Xlib.display.Display, Struct, XWindow]]:
"""
Get root windows objects.
:param forceUpdate: roots info is retrieved at the time of importing the module, and remains static. Set this to ''True'' to force updating it.
:return: list of X-Window objects
"""
global _roots
if forceUpdate:
global _rootsInfo
_rootsInfo, _roots = _getRootsInfo()
return _roots


Expand Down Expand Up @@ -276,9 +263,11 @@ def getPropertyValue(prop: Optional[Xlib.protocol.request.GetProperty], text: bo
:return: extracted property data (as a list of integers or strings) or None
"""
if prop and hasattr(prop, "value"):
# Value is either bytes (separated by '\x00' when multiple values) or array.array of integers.
# Value is either str, bytes (separated by '\x00' when multiple values) or array.array of integers.
# The type of array values is stored in array.typecode ('I' in this case).
valueData: Union[array.array[int], bytes] = prop.value
if isinstance(valueData, str) or isinstance(valueData, int):
return [valueData]
if isinstance(valueData, bytes):
resultStr: List[str] = [a for a in valueData.decode().split("\x00") if a]
return resultStr
Expand Down Expand Up @@ -1044,7 +1033,7 @@ def __init__(self, window: Union[int, XWindow]):
self.ewmhRoot: EwmhRoot = defaultEwmhRoot if self.root.id == defaultRoot.id else EwmhRoot(self.root)
self.extensions = _Extensions(self.id, self.display, self.root)

self._currDesktop = os.environ['XDG_CURRENT_DESKTOP'].lower()
self._currDesktop = os.environ.get('XDG_CURRENT_DESKTOP', '').lower()

def getProperty(self, prop: Union[str, int], prop_type: int = Xlib.X.AnyPropertyType) \
-> Optional[Xlib.protocol.request.GetProperty]:
Expand Down
2 changes: 1 addition & 1 deletion src/ewmhlib/_main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

from ._ewmhlib import (displaysCount, getDisplays, getDisplaysInfo, getRoots, getRootsInfo,
from ._ewmhlib import (displaysCount, getDisplays, getDisplaysInfo, getRoots,
defaultDisplay, defaultScreen, defaultRoot, defaultEwmhRoot,
getDisplayFromRoot, getScreenFromRoot,
getDisplayFromWindow, getScreenFromWindow, getRootFromWindow,
Expand Down
2 changes: 2 additions & 0 deletions tests/test_ewmhlib.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import sys
import time

import Xlib.protocol
import Xlib.X

sys.path.insert(0, "../src/")
from ewmhlib import Props, getDisplaysInfo, EwmhRoot, EwmhWindow


Expand Down

0 comments on commit b2ebe5e

Please sign in to comment.