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
50 changes: 49 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,17 @@ concurrency:
cancel-in-progress: true

jobs:
ruff:
runs-on: ubuntu-latest
timeout-minutes: &timeout-minutes 5
steps:
- uses: actions/checkout@v6
- uses: astral-sh/ruff-action@v4.0.0

mypy:
# Arm runners are faster (as long as the same wheels are available)
runs-on: ubuntu-24.04-arm
timeout-minutes: &timeout-minutes 5
timeout-minutes: *timeout-minutes
strategy:
# mypy is os and python-version sensitive. Test on all supported combinations
matrix:
Expand All @@ -41,3 +48,44 @@ jobs:
activate-environment: true
- run: uv sync --locked
- run: mypy . --python-version=${{ matrix.python-version }}

tests:
# Arm runners are faster (as long as the same wheels are available)
# This project doesn't have any code that should act differently per architecture
runs-on: ubuntu-24.04-arm
timeout-minutes: *timeout-minutes
strategy:
# Test on all supported runtime combinations
matrix:
# TODO: Run tests in parallel on free-threaded python to catch free-threading issues
# See: https://py-free-threading.github.io/testing/
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
fail-fast: false
steps:
- uses: actions/checkout@v6
- name: Install Linux Packages
run: |
sudo apt update
sudo apt install xvfb openbox
- uses: astral-sh/setup-uv@v8.2.0
with:
python-version: ${{ matrix.python-version }}
activate-environment: true
- run: uv sync --locked
- name: Run tests
working-directory: tests
run: xvfb-run --server-args="-screen 0 1280x1024x24" bash -c "openbox & sleep 1 && python test_ewmhlib.py"

sphinx:
runs-on: ubuntu-24.04-arm # Keep in sync with build.os in .readthedocs.yaml
timeout-minutes: *timeout-minutes
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v8.2.0
with:
python-version: "3.14" # Keep in sync with build.tools.python in .readthedocs.yaml
activate-environment: true
- run: uv sync --locked --no-default-groups --group=docs
- name: Build docs
# TODO: Add --fail-on-warning, but still too many warnings right now
run: sphinx-build --keep-going --builder html docs/source docs/_build/html
44 changes: 22 additions & 22 deletions docstrings.md
Original file line number Diff line number Diff line change
Expand Up @@ -714,8 +714,8 @@ def setClosed(winId: int, userAction: bool = True)

Close target window

The Window Manager MUST then attempt to close the window specified. See the section called “Source indication
in requests” for details on the source indication.
The Window Manager MUST then attempt to close the window specified. See the section called Source indication
in requests for details on the source indication.

Rationale: A Window Manager might be more clever than the usual method (send WM_DELETE message if the protocol
is selected, XKillClient otherwise). It might introduce a timeout, for example. Instead of duplicating the
Expand Down Expand Up @@ -750,8 +750,8 @@ A gravity of 0 indicates that the Window Manager should use the gravity specifie

The bits 8 to 11 indicate the presence of x, y, width and height.

The bits 12 to 15 indicate the source (see the section called “Source indication
in requests”), so 0001 indicates the application and 0010 indicates a Pager or a Taskbar.
The bits 12 to 15 indicate the source (see the section called Source indication
in requests), so 0001 indicates the application and 0010 indicates a Pager or a Taskbar.

The remaining bits should be set to zero.

Expand Down Expand Up @@ -791,8 +791,8 @@ def setWmMoveResize(winId: int,
This message allows Clients to initiate window movement or resizing. They can define their own move and size

"grips", whilst letting the Window Manager control the actual operation. This means that all moves/resizes
can happen in a consistent manner as defined by the Window Manager. See the section called “Source indication
in requests” for details on the source indication.
can happen in a consistent manner as defined by the Window Manager. See the section called Source indication
in requests for details on the source indication.

When sending this message in response to a button press event, button SHOULD indicate the button which
was pressed, x_root and y_root MUST indicate the position of the button press with respect to the root window
Expand Down Expand Up @@ -830,7 +830,7 @@ def setWmStacking(winId: int,
This request is similar to ConfigureRequest with CWSibling and CWStackMode flags. It should be used only by

pagers, applications can use normal ConfigureRequests. The source indication field should be therefore
set to 2, see the section called “Source indication in requests” for details.
set to 2, see the section called Source indication in requests for details.

To obtain good interoperability between different Desktop Environments, the following layered stacking
order is recommended, from the bottom:
Expand Down Expand Up @@ -1394,14 +1394,14 @@ decorations. Additionally, the Window Manager is responsible for restoring the o
a switch from fullscreen back to normal window. For example, a presentation program would use this hint.

_NET_WM_STATE_ABOVE indicates that the window should be on top of most windows (see the section called
“Stacking order” for details).
Stacking order for details).

_NET_WM_STATE_BELOW indicates that the window should be below most windows (see the section called
“Stacking order” for details).
Stacking order for details).

_NET_WM_STATE_ABOVE and _NET_WM_STATE_BELOW are mainly meant for user preferences and should not be
used by applications e.g. for drawing attention to their dialogs (the Urgency hint should be used in
that case, see the section called “Urgency”).'
that case, see the section called Urgency).'

_NET_WM_STATE_DEMANDS_ATTENTION indicates that some action in or with the window happened. For example,
it may be set by the Window Manager if the window requested activation but the Window Manager refused it,
Expand Down Expand Up @@ -1434,7 +1434,7 @@ See setWmState() documentation for more information on Window States.

This message allows two properties to be changed simultaneously, specifically to allow both horizontal
and vertical maximization to be altered together. l[2] MUST be set to zero if only one property is to
be changed. See the section called “Source indication in requests” for details on the source indication.
be changed. See the section called Source indication in requests for details on the source indication.
l[0], the action, MUST be one of:

_NET_WM_STATE_REMOVE 0 # remove/unset property
Expand Down Expand Up @@ -1656,8 +1656,8 @@ If set, this property MUST contain the process ID of the client owning this wind
the Window Manager to kill windows which do not respond to the _NET_WM_PING protocol.

If _NET_WM_PID is set, the ICCCM-specified property WM_CLIENT_MACHINE MUST also be set. While the ICCCM
only requests that WM_CLIENT_MACHINE is set “ to a string that forms the name of the machine running the
client as seen from the machine running the server” conformance to this specification requires that
only requests that WM_CLIENT_MACHINE is set to a string that forms the name of the machine running the
client as seen from the machine running the server conformance to this specification requires that
WM_CLIENT_MACHINE be set to the fully-qualified domain name of the client's host.

**Returns**:
Expand Down Expand Up @@ -1789,8 +1789,8 @@ def setActive(userAction: bool = True)
Set current window as active (focused).

Source indication should be 1 when the request comes from an application, and 2 when it comes from a pager.
Clients using older version of this spec use 0 as source indication, see the section called “Source indication
in requests” for details. The timestamp is Client's last user activity timestamp (see _NET_WM_USER_TIME) at
Clients using older version of this spec use 0 as source indication, see the section called Source indication
in requests for details. The timestamp is Client's last user activity timestamp (see _NET_WM_USER_TIME) at
the time of the request, and the currently active window is the Client's active toplevel window, if any
(the Window Manager may be e.g. more likely to obey the request if it will mean transferring focus from one
active window to another).
Expand All @@ -1812,8 +1812,8 @@ def setClosed(userAction: bool = True)

Request to close current window.

The Window Manager MUST then attempt to close the window specified. See the section called “Source
indication in requests” for details on the source indication.
The Window Manager MUST then attempt to close the window specified. See the section called Source
indication in requests for details on the source indication.

Rationale: A Window Manager might be more clever than the usual method (send WM_DELETE message if the
protocol is selected, XKillClient otherwise). It might introduce a timeout, for example. Instead of
Expand Down Expand Up @@ -1896,8 +1896,8 @@ A gravity of 0 indicates that the Window Manager should use the gravity specifie

The bits 8 to 11 indicate the presence of x, y, width and height.

The bits 12 to 15 indicate the source (see the section called “Source indication
in requests”), so 0001 indicates the application and 0010 indicates a Pager or a Taskbar.
The bits 12 to 15 indicate the source (see the section called Source indication
in requests), so 0001 indicates the application and 0010 indicates a Pager or a Taskbar.

The remaining bits should be set to zero.

Expand Down Expand Up @@ -1935,8 +1935,8 @@ def setWmMoveResize(x_root: int,
This message allows Clients to initiate window movement or resizing. They can define their own move and size

"grips", whilst letting the Window Manager control the actual operation. This means that all moves/resizes
can happen in a consistent manner as defined by the Window Manager. See the section called “Source indication
in requests” for details on the source indication.
can happen in a consistent manner as defined by the Window Manager. See the section called Source indication
in requests for details on the source indication.

When sending this message in response to a button press event, button SHOULD indicate the button which
was pressed, x_root and y_root MUST indicate the position of the button press with respect to the root window
Expand Down Expand Up @@ -1970,7 +1970,7 @@ def setWmStacking(siblingId: int, detail: int, userAction: bool = True)
This request is similar to ConfigureRequest with CWSibling and CWStackMode flags. It should be used only by

pagers, applications can use normal ConfigureRequests. The source indication field should be therefore
set to 2, see the section called “Source indication in requests” for details.
set to 2, see the section called Source indication in requests for details.

Rationale: A Window Manager may put restrictions on configure requests from applications, for example it may
under some conditions refuse to raise a window. This request makes it clear it comes from a pager or similar
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ name = "EWMHlib"
version = "0.2"
description = "Extended Window Manager Hints implementation in Python 3"
authors = [
{ name = "Kalmat", email = "palookjones@gmail.com" }
{ name = "Kalmat", email = "palookjones@gmail.com" },
]
readme = "README.md"
license = "BSD-3-Clause"
Expand Down Expand Up @@ -54,6 +54,7 @@ docs = [
]
dev = [
{ include-group = "docs" },
"ruff>=0.15.16",
"mypy>=0.990,<2",
"types-python-xlib>=0.32",
]
Expand Down
91 changes: 91 additions & 0 deletions ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
[lint]
future-annotations = true
# https://docs.astral.sh/ruff/rules/
extend-select = [
"ANN2", # flake8-annotations: missing-return-type
"C4", # flake8-comprehensions
"F401", # unused-import
"F404", # late-future-import
"FA", # flake8-future-annotations
"FLY", # Flynt
"ICN", # flake8-import-conventions
"PERF", # Perflint
"PGH", # pygrep-hooks (blanket-* rules)
"PYI", # flake8-pyi
"RUF", # Ruff-specific rules
"SIM9", # flake8-simplify: split-static-string + zip-dict-keys-and-values
"SLOT", # flake8-slots
"TC", # flake8-type-checking
"TID", # flake8-tidy-imports
"UP", # pyupgrade
"YTT", # flake8-2020
]
ignore = [
# Only enforce return types on public functions. Where otherwise mypy infers as Any
# Still worth running `ruff check --fix --select=202` once in a while for autofixes
"ANN202", # missing-return-type-private-function
# Explicit is preferred
"UP015", # redundant-open-modes,
# Autofixes print-f style formatting to f-strings,
# which is sometimes simpler, but looses template code reading semantics
"UP032", # f-string
# TC helps prevent circular imports, reduce runtime cost of typing symbols,
# and prevent leaking implementations details into modules
# However stdlib is not at risk of circular import, is clearly not public API,
# and assume it's gonna be included in the import chain at some point anyway
"TC003", # typing-only-standard-library-import
# Typeshed doesn't want complex or non-literal defaults for maintenance and testing reasons.
# This doesn't affect us, let's have more complete stubs.
"PYI011", # typed-argument-default-in-stub
"PYI014", # argument-default-in-stub
"PYI053", # string-or-bytes-too-long
# Good to watch for, but often unfixable
# Anyway Python 3.11 introduced "zero cost" exception handling
"PERF203", # try-except-in-loop,

# TODO: Add and configure isort (I) first
"RUF022",

# TODO: Consider later
"UP031", # printf-string-formatting
"RUF059", # unused-unpacked-variable

# TODO: Not autofixable, address in a separate PR !
"E722",
"E402",
"F401",
"ANN201",
]
# F401 would remove imports not marked as explicit re-exports, which may break API boundaries
extend-unsafe-fixes = ["F401"]

[lint.per-file-ignores]
"**/typings/**/*.pyi" = [
"E402", # https://github.com/astral-sh/ruff/issues/26160
"F811", # Re-exports false positives
# The following can't be controlled for external libraries:
"A", # Shadowing builtin names
"E741", # ambiguous variable name
"F403", # `from . import *` used; unable to detect undefined names
"FBT", # flake8-boolean-trap
"ICN001", # unconventional-import-alias
"N8", # Naming conventions
"PLC2701", # Private name import
"PLE0302", # The special method expects a given signature
"PLR0904", # Too many public methods
"PLR0913", # Argument count
"PLR0917", # Too many positional arguments
"PLW3201", # misspelled dunder method name
"SLOT", # flake8-slots
# Stubs can sometimes re-export entire modules.
# Issues with using a star-imported name will be caught by type-checkers.
"F405", # may be undefined, or defined from star imports
# It's normal to be missing annotations for local stubs.
# If they were complete, we'd upload them to typeshed!
"ANN0",
"ANN2",
]

# https://docs.astral.sh/ruff/settings/#lintflake8-type-checking
[lint.flake8-type-checking]
quote-annotations = true
1 change: 0 additions & 1 deletion src/ewmhlib/Props/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

from ._props import (Root, DesktopLayout, Window, WindowType, State, StateAction,
MoveResize, DataFormat, Mode, StackMode, HintAction)
1 change: 0 additions & 1 deletion src/ewmhlib/Props/_props.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

from enum import IntEnum
import Xlib.X
Expand Down
1 change: 0 additions & 1 deletion src/ewmhlib/Structs/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

from ._structs import (ScreensInfo, DisplaysInfo, WmHints, Aspect, WmNormalHints)
14 changes: 8 additions & 6 deletions src/ewmhlib/Structs/_structs.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-#from typing import List

from __future__ import annotations
from typing_extensions import TypedDict
from typing import List
from typing import TYPE_CHECKING

from ctypes import Structure, c_int32, c_ulong, c_uint32
import Xlib.xobject
from Xlib.protocol.rq import Struct
from Xlib.xobject.drawable import Window as XWindow

if TYPE_CHECKING:
from Xlib.xobject.drawable import Window as XWindow
import Xlib.xobject
from Xlib.protocol.rq import Struct


class ScreensInfo(TypedDict):
Expand Down Expand Up @@ -36,7 +38,7 @@ class DisplaysInfo(TypedDict):
display: Xlib.display.Display
name: str
is_default: bool
screens: List[ScreensInfo]
screens: list[ScreensInfo]


"""
Expand Down
1 change: 0 additions & 1 deletion src/ewmhlib/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
from importlib.metadata import version as _importlib_version

__all__ = [
Expand Down
Loading