Skip to content

Commit

Permalink
Windows support
Browse files Browse the repository at this point in the history
  • Loading branch information
Commandcracker committed Feb 26, 2023
1 parent 5846516 commit d5d9864
Show file tree
Hide file tree
Showing 15 changed files with 313 additions and 71 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
[![Publish](https://github.com/Commandcracker/display-server-interactions/actions/workflows/pypi-publish.yml/badge.svg)](https://github.com/Commandcracker/display-server-interactions/actions/workflows/pypi-publish.yml)

DSI allows you to perform basic interactions on your display server, like screenshotting a window or sending input to it.
Currently, DSI only supports X11/Xorg (GNU/Linux) but it aims to be cross-platform.
Currently, DSI only supports X11/Xorg (GNU/Linux) and Windows but it aims to be cross-platform.

**WARNING: Please Do not use DSI in production, because it's currently in development!**

## Quick overview

Look at the [documentation](https://display-server-interactions.readthedocs.io/en/latest/) for moor information's
Look at the [documentation](https://display-server-interactions.readthedocs.io/en/latest/) for moor information

### Get a window

Expand Down
4 changes: 2 additions & 2 deletions display_server_interactions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from platform import system as __system

__version__ = "0.0.dev4"
__version__ = "0.0.dev5"
__author__ = "Commandcracker"

__os_name = __system().lower()
Expand All @@ -12,7 +12,7 @@
from .linux import DSI

elif __os_name == "windows":
raise NotImplementedError("Windows is not yet implemented.")
from .windows import DSI

elif __os_name == "darwin":
raise NotImplementedError("MacOS is not yet implemented.")
Expand Down
2 changes: 1 addition & 1 deletion display_server_interactions/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def get_active_window(self) -> WindowBase:
pass

@abstractmethod
def get_all_windows(self) -> list:
def get_all_windows(self) -> list[WindowBase]:
"""
Returns a list of all Windows.
"""
Expand Down
46 changes: 46 additions & 0 deletions display_server_interactions/box.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-

class Box(tuple):
def __init__(self, x: int, y: int, width: int, height: int):
"""This is just here for autocompletion"""
pass

def __new__(self, x: int, y: int, width: int, height: int):
return tuple.__new__(Box, (x, y, width, height))

@property
def x(self) -> int:
return self[0]

@property
def y(self) -> int:
return self[1]

@property
def width(self) -> int:
return self[2]

@property
def height(self) -> int:
return self[3]

def __repr__(self) -> str:
return f'Box(x={self.x}, y={self.y}, width={self.width}, height={self.height})'


def main() -> None:
try:
from rich import print
except ImportError:
pass
box = Box(100, 200, 300, 400)
print(box)
print(f"x={box.x}")
print(f"y={box.y}")
print(f"width={box.width}")
print(f"height={box.height}")


if __name__ == "__main__":
main()
13 changes: 13 additions & 0 deletions display_server_interactions/buttons.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-

class MouseButtons(object):
"""
https://tronche.com/gui/x/xlib/events/keyboard-pointer/keyboard-pointer.html\n
/usr/include/X11/X.h: 259-263
"""
LEFT = 1
RIGHT = 2
MIDDLE = 3
FORWARD = 4
BACKWARD = 5
50 changes: 22 additions & 28 deletions display_server_interactions/linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from .base import DSIBase
from .window import WindowBase
from .image import Image
from .buttons import MouseButtons
from .box import Box

# built-in modules
import logging
Expand Down Expand Up @@ -290,19 +292,6 @@ class KeyMasks(object):
Mod5Mask = 128


class ButtonCodes(object):
"""
https://tronche.com/gui/x/xlib/events/keyboard-pointer/keyboard-pointer.html\n
/usr/include/X11/X.h: 259-263
"""
AnyButton = 0
Button1 = 1
Button2 = 2
Button3 = 3
Button4 = 4
Button5 = 5


class Xlib(object):
def __init__(self):
# load libX11.so.6
Expand Down Expand Up @@ -454,34 +443,39 @@ def active(self) -> bool:
return self.xid == get_active_window_xid(self.xlib)

@property
def geometry(self) -> tuple:
def geometry(self) -> Box:
gwa = XWindowAttributes()
self.xlib.XGetWindowAttributes(self.xlib.display, self.xid, byref(gwa))
return (gwa.x, gwa.y, gwa.width, gwa.height)
return Box(
x=gwa.x,
y=gwa.y,
width=gwa.width,
height=gwa.height
)

def get_image(self, geometry: tuple = None) -> Image:
def get_image(self, geometry: Box = None) -> Image:
if geometry is None:
geometry = self.geometry

ximage = self.xlib.XGetImage(
self.xlib.display, # Display
self.xid, # Drawable (Window XID)
geometry[0], # x
geometry[1], # y
geometry[2], # width
geometry[3], # height
geometry.x, # x
geometry.y, # y
geometry.width, # width
geometry.height, # height
0x00FFFFFF, # plane_mask
2 # format = ZPixmap
)

raw_data = ctypes.cast(
ximage.contents.data,
POINTER(c_ubyte * geometry[3] * geometry[2] * 4)
POINTER(c_ubyte * geometry.height * geometry.width * 4)
)

data = bytearray(raw_data.contents)

data = Image(data, geometry[2], geometry[3])
data = Image(data, geometry.width, geometry.height)

# don't forget to free the memory or you will be fucked
self.xlib.XDestroyImage(ximage)
Expand Down Expand Up @@ -524,7 +518,7 @@ def send_str(self, str: str) -> None:
for chr in str:
self.send_chr(chr)

def warp_pointer(self, x: int, y: int, geometry: tuple = None) -> None:
def warp_pointer(self, x: int, y: int, geometry: Box = None) -> None:
if geometry is None:
geometry = self.geometry

Expand All @@ -533,18 +527,18 @@ def warp_pointer(self, x: int, y: int, geometry: tuple = None) -> None:
self.xlib.display,
self.xid, # src_w
self.xid, # dest_w
geometry[0],
geometry[1],
geometry[2],
geometry[3],
geometry.x,
geometry.y,
geometry.width,
geometry.height,
x,
y
)

# flush display or events will run delayed cus thai'r only called on the next update
self.xlib.XFlush(self.xlib.display)

def send_mouse_click(self, x: int, y: int, button: ButtonCodes = ButtonCodes.Button1) -> None:
def send_mouse_click(self, x: int, y: int, button: MouseButtons = MouseButtons.LEFT) -> None:
"""
Send a mouse click to the window at the given coordinates without moving the pointer.
Some applications may not respond to the click so it is recommended to also move the pointer with `warp_pointer`.
Expand Down
14 changes: 8 additions & 6 deletions display_server_interactions/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

from abc import ABCMeta, abstractmethod
from .image import Image
from .buttons import MouseButtons
from .box import Box


class WindowBase(object, metaclass=ABCMeta):
Expand Down Expand Up @@ -32,14 +34,14 @@ def active(self) -> bool:

@property
@abstractmethod
def geometry(self) -> tuple:
def geometry(self) -> Box:
"""
Returns: tuple: (x, y, width, height)
"""
pass

@abstractmethod
def get_image(self, geometry: tuple = None) -> Image:
def get_image(self, geometry: Box = None) -> Image:
"""
Returns an Image of the window.
With the geometry parameter you can specify a sub-region of the window that will be captured.
Expand All @@ -58,13 +60,13 @@ def send_str(self, str: str) -> None:
"""

@abstractmethod
def warp_pointer(self, x: int, y: int, geometry: tuple = None) -> None:
def warp_pointer(self, x: int, y: int, geometry: Box = None) -> None:
"""
Moves the pointer relative to the window to the given coordinates.
"""

@abstractmethod
def send_mouse_click(self, x: int, y: int, button) -> None:
def send_mouse_click(self, x: int, y: int, button: MouseButtons = MouseButtons.LEFT) -> None:
"""
Send a mouse click to the window at the given coordinates.
On some windows/applications you need to move the pointer with warp_pointer() first.
Expand All @@ -73,5 +75,5 @@ def send_mouse_click(self, x: int, y: int, button) -> None:
def __repr__(self) -> str:
name = self.name
if name:
name = f'"{name}"'
return f'Window(name={name}, pid={self.pid}, active={self.active}, geometry={self.geometry})'
return f'Window(name="{self.name}", pid={self.pid}, active={self.active}, geometry={self.geometry})'
return f'Window(pid={self.pid}, active={self.active}, geometry={self.geometry})'
Loading

0 comments on commit d5d9864

Please sign in to comment.