Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ See Git checking messages for full history.
## 10.1.1.dev0 (2025-xx-xx)
- Linux: check the server for Xrandr support version (#417)
- Linux: improve typing and error messages for X libraries (#418)
- Linux: add a new XCB backend for better thread safety, error-checking, and future development (#425)
- :heart: contributors: @jholveck

## 10.1.0 (2025-08-16)
Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ mss = "mss.__main__:main"
[project.optional-dependencies]
dev = [
"build==1.3.0",
"lxml==6.0.2",
"mypy==1.18.2",
"ruff==0.14.5",
"twine==6.2.0",
Expand Down Expand Up @@ -134,6 +135,7 @@ strict_equality = true

[tool.pytest.ini_options]
pythonpath = "src"
markers = ["without_libraries"]
addopts = """
--showlocals
--strict-markers
Expand Down Expand Up @@ -187,3 +189,6 @@ ignore = [
"S607", # `subprocess` call without explicit paths
"SLF001", # private member accessed
]

[tool.ruff.per-file-target-version]
"src/xcbproto/*" = "py312"
6 changes: 6 additions & 0 deletions src/mss/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def __init__(
self,
/,
*,
backend: str = "default",
compression_level: int = 6,
with_cursor: bool = False,
# Linux only
Expand All @@ -51,6 +52,11 @@ def __init__(
self.compression_level = compression_level
self.with_cursor = with_cursor
self._monitors: Monitors = []
# If there isn't a factory that removed the "backend" argument, make sure that it was set to "default".
# Factories that do backend-specific dispatch should remove that argument.
if backend != "default":
msg = 'The only valid backend on this platform is "default".'
raise ScreenShotError(msg)

def __enter__(self) -> MSSBase: # noqa:PYI034
"""For the cool call `with MSS() as mss:`."""
Expand Down
23 changes: 23 additions & 0 deletions src/mss/linux/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from typing import Any

from mss.base import MSSBase
from mss.exception import ScreenShotError


def mss(backend: str = "default", **kwargs: Any) -> MSSBase:
backend = backend.lower()
if backend in {"default", "xlib"}:
from . import xlib # noqa: PLC0415

return xlib.MSS(**kwargs)
if backend == "xgetimage":
from . import xgetimage # noqa: PLC0415

return xgetimage.MSS(**kwargs)
msg = f"Backend {backend!r} not (yet?) implemented."
raise ScreenShotError(msg)


# Alias in upper-case for backward compatibility. This is a supported name in the docs.
def MSS(*args, **kwargs) -> MSSBase: # type: ignore[no-untyped-def] # noqa: N802, ANN002, ANN003
return mss(*args, **kwargs)
162 changes: 162 additions & 0 deletions src/mss/linux/xcb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
from __future__ import annotations

from ctypes import Structure, c_int, c_uint8, c_uint16, c_uint32

from . import xcbgen

# We import these just so they're re-exported to our users.
# ruff: noqa: F401
from .xcbgen import (
RANDR_MAJOR_VERSION,
RANDR_MINOR_VERSION,
RENDER_MAJOR_VERSION,
RENDER_MINOR_VERSION,
XFIXES_MAJOR_VERSION,
XFIXES_MINOR_VERSION,
Atom,
BackingStore,
Colormap,
Depth,
DepthIterator,
Drawable,
Format,
GetGeometryReply,
GetImageReply,
GetPropertyReply,
ImageFormat,
ImageOrder,
Keycode,
Pixmap,
RandrCrtc,
RandrGetCrtcInfoReply,
RandrGetScreenResourcesCurrentReply,
RandrGetScreenResourcesReply,
RandrMode,
RandrModeInfo,
RandrOutput,
RandrQueryVersionReply,
RandrSetConfig,
RenderDirectformat,
RenderPictdepth,
RenderPictdepthIterator,
RenderPictformat,
RenderPictforminfo,
RenderPictscreen,
RenderPictscreenIterator,
RenderPictType,
RenderPictvisual,
RenderQueryPictFormatsReply,
RenderQueryVersionReply,
RenderSubPixel,
Screen,
ScreenIterator,
Setup,
SetupIterator,
Timestamp,
VisualClass,
Visualid,
Visualtype,
Window,
XfixesGetCursorImageReply,
XfixesQueryVersionReply,
depth_visuals,
get_geometry,
get_image,
get_image_data,
get_property,
get_property_value,
no_operation,
randr_get_crtc_info,
randr_get_crtc_info_outputs,
randr_get_crtc_info_possible,
randr_get_screen_resources,
randr_get_screen_resources_crtcs,
randr_get_screen_resources_current,
randr_get_screen_resources_current_crtcs,
randr_get_screen_resources_current_modes,
randr_get_screen_resources_current_names,
randr_get_screen_resources_current_outputs,
randr_get_screen_resources_modes,
randr_get_screen_resources_names,
randr_get_screen_resources_outputs,
randr_query_version,
render_pictdepth_visuals,
render_pictscreen_depths,
render_query_pict_formats,
render_query_pict_formats_formats,
render_query_pict_formats_screens,
render_query_pict_formats_subpixels,
render_query_version,
screen_allowed_depths,
setup_pixmap_formats,
setup_roots,
setup_vendor,
xfixes_get_cursor_image,
xfixes_get_cursor_image_cursor_image,
xfixes_query_version,
)

# These are also here to re-export.
from .xcbhelpers import LIB, Connection, XError

XCB_CONN_ERROR = 1
XCB_CONN_CLOSED_EXT_NOTSUPPORTED = 2
XCB_CONN_CLOSED_MEM_INSUFFICIENT = 3
XCB_CONN_CLOSED_REQ_LEN_EXCEED = 4
XCB_CONN_CLOSED_PARSE_ERR = 5
XCB_CONN_CLOSED_INVALID_SCREEN = 6
XCB_CONN_CLOSED_FDPASSING_FAILED = 7

# I don't know of error descriptions for the XCB connection errors being accessible through a library (a la strerror),
# and the ones in xcb.h's comments aren't too great, so I wrote these.
XCB_CONN_ERRMSG = {
XCB_CONN_ERROR: "connection lost or could not be established",
XCB_CONN_CLOSED_EXT_NOTSUPPORTED: "extension not supported",
XCB_CONN_CLOSED_MEM_INSUFFICIENT: "memory exhausted",
XCB_CONN_CLOSED_REQ_LEN_EXCEED: "request length longer than server accepts",
XCB_CONN_CLOSED_PARSE_ERR: "display is unset or invalid (check $DISPLAY)",
XCB_CONN_CLOSED_INVALID_SCREEN: "server does not have a screen matching the requested display",
XCB_CONN_CLOSED_FDPASSING_FAILED: "could not pass file descriptor",
}


def initialize() -> None:
LIB.initialize(callbacks=[xcbgen.initialize])


def connect(display: str | bytes | None = None) -> tuple[Connection, int]:
if isinstance(display, str):
display = display.encode("utf-8")

initialize()
pref_screen_num = c_int()
conn_p = LIB.xcb.xcb_connect(display, pref_screen_num)

# We still get a connection object even if the connection fails.
conn_err = LIB.xcb.xcb_connection_has_error(conn_p)
if conn_err != 0:
# XCB won't free its connection structures until we disconnect, even in the event of an error.
LIB.xcb.xcb_disconnect(conn_p)
msg = "Cannot connect to display: "
conn_errmsg = XCB_CONN_ERRMSG.get(conn_err)
if conn_errmsg:
msg += conn_errmsg
else:
msg += f"error code {conn_err}"
raise XError(msg)

return conn_p.contents, pref_screen_num.value


def disconnect(conn: Connection) -> None:
conn_err = LIB.xcb.xcb_connection_has_error(conn)
# XCB won't free its connection structures until we disconnect, even in the event of an error.
LIB.xcb.xcb_disconnect(conn)
if conn_err != 0:
msg = "Connection to X server closed: "
conn_errmsg = XCB_CONN_ERRMSG.get(conn_err)
if conn_errmsg:
msg += conn_errmsg
else:
msg += f"error code {conn_err}"
raise XError(msg)
Loading
Loading