hyprland-socket 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hyprland_socket-0.1.0/.github/workflows/ci.yml +29 -0
- hyprland_socket-0.1.0/.github/workflows/publish.yml +18 -0
- hyprland_socket-0.1.0/.gitignore +7 -0
- hyprland_socket-0.1.0/LICENSE +18 -0
- hyprland_socket-0.1.0/PKG-INFO +125 -0
- hyprland_socket-0.1.0/README.md +108 -0
- hyprland_socket-0.1.0/pyproject.toml +26 -0
- hyprland_socket-0.1.0/src/hyprland_socket/__init__.py +35 -0
- hyprland_socket-0.1.0/src/hyprland_socket/_socket.py +48 -0
- hyprland_socket-0.1.0/src/hyprland_socket/commands.py +90 -0
- hyprland_socket-0.1.0/src/hyprland_socket/errors.py +13 -0
- hyprland_socket-0.1.0/src/hyprland_socket/events.py +63 -0
- hyprland_socket-0.1.0/src/hyprland_socket/models.py +96 -0
- hyprland_socket-0.1.0/tests/__init__.py +0 -0
- hyprland_socket-0.1.0/tests/test_events.py +32 -0
- hyprland_socket-0.1.0/tests/test_models.py +149 -0
- hyprland_socket-0.1.0/uv.lock +108 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: astral-sh/setup-uv@v6
|
|
15
|
+
- run: uv sync
|
|
16
|
+
- run: uv run ruff check src/ tests/
|
|
17
|
+
- run: uv run ruff format --check src/ tests/
|
|
18
|
+
|
|
19
|
+
test:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
strategy:
|
|
22
|
+
matrix:
|
|
23
|
+
python-version: ["3.12", "3.13"]
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
- uses: astral-sh/setup-uv@v6
|
|
27
|
+
- run: uv python install ${{ matrix.python-version }}
|
|
28
|
+
- run: uv sync --python ${{ matrix.python-version }}
|
|
29
|
+
- run: uv run pytest tests/ -v
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
id-token: write
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
publish:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
environment: pypi
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: astral-sh/setup-uv@v6
|
|
17
|
+
- run: uv build
|
|
18
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ivo Šmerek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
|
6
|
+
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
|
7
|
+
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
|
9
|
+
following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial
|
|
12
|
+
portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
|
15
|
+
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
16
|
+
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
18
|
+
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hyprland-socket
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Typed Python library for Hyprland IPC via Unix sockets
|
|
5
|
+
Project-URL: Repository, https://github.com/BlueManCZ/hyprland-socket
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: compositor,hyprland,ipc,wayland
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Topic :: Desktop Environment
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# hyprland-socket
|
|
19
|
+
|
|
20
|
+
Typed Python library for [Hyprland](https://hyprland.org/) IPC via Unix sockets.
|
|
21
|
+
|
|
22
|
+
Covers both read and write operations — querying state, applying settings live,
|
|
23
|
+
batch commands, and monitoring events.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
pip install hyprland-socket
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
### Query state
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import hyprland_socket
|
|
37
|
+
|
|
38
|
+
# Check if Hyprland is running
|
|
39
|
+
if hyprland_socket.is_running():
|
|
40
|
+
# Read monitors
|
|
41
|
+
for mon in hyprland_socket.get_monitors():
|
|
42
|
+
print(f"{mon.name}: {mon.width}x{mon.height} @ {mon.refresh_rate}Hz")
|
|
43
|
+
|
|
44
|
+
# Read a live option
|
|
45
|
+
option = hyprland_socket.getoption("general:gaps_in")
|
|
46
|
+
print(option)
|
|
47
|
+
|
|
48
|
+
# Read keybinds
|
|
49
|
+
for bind in hyprland_socket.get_binds():
|
|
50
|
+
print(f"{bind.key} -> {bind.dispatcher} {bind.arg}")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Apply settings
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
import hyprland_socket
|
|
57
|
+
|
|
58
|
+
# Set a single option
|
|
59
|
+
hyprland_socket.keyword("general:gaps_in", 5)
|
|
60
|
+
|
|
61
|
+
# Batch multiple settings (single IPC call)
|
|
62
|
+
hyprland_socket.keyword_batch([
|
|
63
|
+
("general:gaps_in", "5"),
|
|
64
|
+
("general:gaps_out", "10"),
|
|
65
|
+
("decoration:rounding", "8"),
|
|
66
|
+
])
|
|
67
|
+
|
|
68
|
+
# Reload config from disk
|
|
69
|
+
hyprland_socket.reload()
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Monitor events
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import hyprland_socket
|
|
76
|
+
|
|
77
|
+
# Blocking iterator over compositor events
|
|
78
|
+
for event in hyprland_socket.events():
|
|
79
|
+
print(f"{event.name}: {event.data}")
|
|
80
|
+
# e.g. "workspace: 2", "monitoradded: DP-3"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
For integration with GTK/GLib event loops, use the raw socket:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
sock = hyprland_socket.connect_event_socket()
|
|
87
|
+
fd = sock.fileno()
|
|
88
|
+
# Use GLib.io_add_watch(fd, ...) or similar
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Error handling
|
|
92
|
+
|
|
93
|
+
All functions raise typed exceptions instead of returning `None`:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from hyprland_socket import ConnectionError, CommandError
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
hyprland_socket.keyword("invalid:option", "value")
|
|
100
|
+
except ConnectionError:
|
|
101
|
+
print("Hyprland is not running")
|
|
102
|
+
except CommandError as e:
|
|
103
|
+
print(f"Rejected: {e}")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Models
|
|
107
|
+
|
|
108
|
+
| Function | Returns |
|
|
109
|
+
|---|---|
|
|
110
|
+
| `get_monitors()` | `list[Monitor]` |
|
|
111
|
+
| `get_binds()` | `list[Bind]` |
|
|
112
|
+
| `get_animations()` | `tuple[list[Animation], list[dict]]` |
|
|
113
|
+
| `getoption(key)` | `dict` |
|
|
114
|
+
|
|
115
|
+
All models are mutable dataclasses with a `from_dict()` classmethod for
|
|
116
|
+
construction from Hyprland's JSON responses.
|
|
117
|
+
|
|
118
|
+
## Requirements
|
|
119
|
+
|
|
120
|
+
- Python >= 3.12
|
|
121
|
+
- A running Hyprland session (the `HYPRLAND_INSTANCE_SIGNATURE` environment variable must be set)
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# hyprland-socket
|
|
2
|
+
|
|
3
|
+
Typed Python library for [Hyprland](https://hyprland.org/) IPC via Unix sockets.
|
|
4
|
+
|
|
5
|
+
Covers both read and write operations — querying state, applying settings live,
|
|
6
|
+
batch commands, and monitoring events.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
pip install hyprland-socket
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
### Query state
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import hyprland_socket
|
|
20
|
+
|
|
21
|
+
# Check if Hyprland is running
|
|
22
|
+
if hyprland_socket.is_running():
|
|
23
|
+
# Read monitors
|
|
24
|
+
for mon in hyprland_socket.get_monitors():
|
|
25
|
+
print(f"{mon.name}: {mon.width}x{mon.height} @ {mon.refresh_rate}Hz")
|
|
26
|
+
|
|
27
|
+
# Read a live option
|
|
28
|
+
option = hyprland_socket.getoption("general:gaps_in")
|
|
29
|
+
print(option)
|
|
30
|
+
|
|
31
|
+
# Read keybinds
|
|
32
|
+
for bind in hyprland_socket.get_binds():
|
|
33
|
+
print(f"{bind.key} -> {bind.dispatcher} {bind.arg}")
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Apply settings
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import hyprland_socket
|
|
40
|
+
|
|
41
|
+
# Set a single option
|
|
42
|
+
hyprland_socket.keyword("general:gaps_in", 5)
|
|
43
|
+
|
|
44
|
+
# Batch multiple settings (single IPC call)
|
|
45
|
+
hyprland_socket.keyword_batch([
|
|
46
|
+
("general:gaps_in", "5"),
|
|
47
|
+
("general:gaps_out", "10"),
|
|
48
|
+
("decoration:rounding", "8"),
|
|
49
|
+
])
|
|
50
|
+
|
|
51
|
+
# Reload config from disk
|
|
52
|
+
hyprland_socket.reload()
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Monitor events
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
import hyprland_socket
|
|
59
|
+
|
|
60
|
+
# Blocking iterator over compositor events
|
|
61
|
+
for event in hyprland_socket.events():
|
|
62
|
+
print(f"{event.name}: {event.data}")
|
|
63
|
+
# e.g. "workspace: 2", "monitoradded: DP-3"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
For integration with GTK/GLib event loops, use the raw socket:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
sock = hyprland_socket.connect_event_socket()
|
|
70
|
+
fd = sock.fileno()
|
|
71
|
+
# Use GLib.io_add_watch(fd, ...) or similar
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Error handling
|
|
75
|
+
|
|
76
|
+
All functions raise typed exceptions instead of returning `None`:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from hyprland_socket import ConnectionError, CommandError
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
hyprland_socket.keyword("invalid:option", "value")
|
|
83
|
+
except ConnectionError:
|
|
84
|
+
print("Hyprland is not running")
|
|
85
|
+
except CommandError as e:
|
|
86
|
+
print(f"Rejected: {e}")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Models
|
|
90
|
+
|
|
91
|
+
| Function | Returns |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `get_monitors()` | `list[Monitor]` |
|
|
94
|
+
| `get_binds()` | `list[Bind]` |
|
|
95
|
+
| `get_animations()` | `tuple[list[Animation], list[dict]]` |
|
|
96
|
+
| `getoption(key)` | `dict` |
|
|
97
|
+
|
|
98
|
+
All models are mutable dataclasses with a `from_dict()` classmethod for
|
|
99
|
+
construction from Hyprland's JSON responses.
|
|
100
|
+
|
|
101
|
+
## Requirements
|
|
102
|
+
|
|
103
|
+
- Python >= 3.12
|
|
104
|
+
- A running Hyprland session (the `HYPRLAND_INSTANCE_SIGNATURE` environment variable must be set)
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hyprland-socket"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Typed Python library for Hyprland IPC via Unix sockets"
|
|
5
|
+
requires-python = ">=3.12"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
keywords = ["hyprland", "ipc", "wayland", "compositor"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 3 - Alpha",
|
|
11
|
+
"License :: OSI Approved :: MIT License",
|
|
12
|
+
"Operating System :: POSIX :: Linux",
|
|
13
|
+
"Programming Language :: Python :: 3.12",
|
|
14
|
+
"Programming Language :: Python :: 3.13",
|
|
15
|
+
"Topic :: Desktop Environment",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Repository = "https://github.com/BlueManCZ/hyprland-socket"
|
|
20
|
+
|
|
21
|
+
[dependency-groups]
|
|
22
|
+
dev = ["pytest>=9.0", "ruff>=0.11"]
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["hatchling"]
|
|
26
|
+
build-backend = "hatchling.build"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""hyprland-socket — Typed Python library for Hyprland IPC."""
|
|
2
|
+
|
|
3
|
+
from .commands import (
|
|
4
|
+
get_animations,
|
|
5
|
+
get_binds,
|
|
6
|
+
get_monitors,
|
|
7
|
+
getoption,
|
|
8
|
+
is_running,
|
|
9
|
+
keyword,
|
|
10
|
+
keyword_batch,
|
|
11
|
+
reload,
|
|
12
|
+
)
|
|
13
|
+
from .errors import CommandError, ConnectionError, HyprlandError
|
|
14
|
+
from .events import Event, connect_event_socket, events
|
|
15
|
+
from .models import Animation, Bind, Monitor
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"Animation",
|
|
19
|
+
"Bind",
|
|
20
|
+
"CommandError",
|
|
21
|
+
"ConnectionError",
|
|
22
|
+
"Event",
|
|
23
|
+
"HyprlandError",
|
|
24
|
+
"Monitor",
|
|
25
|
+
"connect_event_socket",
|
|
26
|
+
"events",
|
|
27
|
+
"get_animations",
|
|
28
|
+
"get_binds",
|
|
29
|
+
"get_monitors",
|
|
30
|
+
"getoption",
|
|
31
|
+
"is_running",
|
|
32
|
+
"keyword",
|
|
33
|
+
"keyword_batch",
|
|
34
|
+
"reload",
|
|
35
|
+
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Low-level Unix socket communication with Hyprland."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import socket
|
|
5
|
+
|
|
6
|
+
from .errors import ConnectionError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _socket_path() -> str:
|
|
10
|
+
"""Return the Hyprland command socket path."""
|
|
11
|
+
runtime = os.environ.get("XDG_RUNTIME_DIR", f"/run/user/{os.getuid()}")
|
|
12
|
+
sig = os.environ["HYPRLAND_INSTANCE_SIGNATURE"]
|
|
13
|
+
return f"{runtime}/hypr/{sig}/.socket.sock"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _event_socket_path() -> str:
|
|
17
|
+
"""Return the Hyprland event socket path (socket2)."""
|
|
18
|
+
runtime = os.environ.get("XDG_RUNTIME_DIR", f"/run/user/{os.getuid()}")
|
|
19
|
+
sig = os.environ["HYPRLAND_INSTANCE_SIGNATURE"]
|
|
20
|
+
return f"{runtime}/hypr/{sig}/.socket2.sock"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _send(command: str, timeout: float = 2.0) -> str:
|
|
24
|
+
"""Send a command to Hyprland's Unix socket and return the response.
|
|
25
|
+
|
|
26
|
+
Opens a fresh connection for each command and closes immediately
|
|
27
|
+
after reading — Hyprland processes connections synchronously and
|
|
28
|
+
an unclosed socket will freeze the compositor.
|
|
29
|
+
|
|
30
|
+
Raises ConnectionError if the socket is unreachable.
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
34
|
+
sock.settimeout(timeout)
|
|
35
|
+
sock.connect(_socket_path())
|
|
36
|
+
try:
|
|
37
|
+
sock.sendall(command.encode())
|
|
38
|
+
chunks = []
|
|
39
|
+
while True:
|
|
40
|
+
chunk = sock.recv(8192)
|
|
41
|
+
if not chunk:
|
|
42
|
+
break
|
|
43
|
+
chunks.append(chunk)
|
|
44
|
+
return b"".join(chunks).decode()
|
|
45
|
+
finally:
|
|
46
|
+
sock.close()
|
|
47
|
+
except Exception as e:
|
|
48
|
+
raise ConnectionError(f"Cannot reach Hyprland socket: {e}") from e
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""High-level command functions for Hyprland IPC."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ._socket import _send
|
|
7
|
+
from .errors import CommandError, ConnectionError
|
|
8
|
+
from .models import Animation, Bind, Monitor
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _query_json(command: str) -> Any:
|
|
12
|
+
"""Send a JSON query and return parsed result."""
|
|
13
|
+
response = _send(f"j/{command}")
|
|
14
|
+
try:
|
|
15
|
+
return json.loads(response)
|
|
16
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
17
|
+
raise CommandError(f"Invalid JSON response for '{command}': {e}") from e
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def getoption(key: str) -> dict:
|
|
21
|
+
"""Read a live option value from Hyprland.
|
|
22
|
+
|
|
23
|
+
Raises ConnectionError or CommandError on failure.
|
|
24
|
+
"""
|
|
25
|
+
return _query_json(f"getoption {key}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def keyword(key: str, value: Any) -> None:
|
|
29
|
+
"""Apply a setting live to the running compositor.
|
|
30
|
+
|
|
31
|
+
Raises CommandError if Hyprland rejects the command.
|
|
32
|
+
"""
|
|
33
|
+
if isinstance(value, bool):
|
|
34
|
+
value = int(value)
|
|
35
|
+
response = _send(f"/keyword {key} {value}")
|
|
36
|
+
output = response.strip().lower()
|
|
37
|
+
if output != "ok" and output != "":
|
|
38
|
+
raise CommandError(f"keyword '{key} {value}' rejected: {response.strip()}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def keyword_batch(commands: list[tuple[str, str]]) -> None:
|
|
42
|
+
"""Apply multiple keyword settings in a single batch call.
|
|
43
|
+
|
|
44
|
+
Raises CommandError on failure.
|
|
45
|
+
"""
|
|
46
|
+
if not commands:
|
|
47
|
+
return
|
|
48
|
+
batch = ";".join(f"keyword {key} {value}" for key, value in commands)
|
|
49
|
+
_send(f"[[BATCH]]{batch}", timeout=5.0)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def reload() -> None:
|
|
53
|
+
"""Tell Hyprland to reload its config.
|
|
54
|
+
|
|
55
|
+
Raises ConnectionError if unreachable.
|
|
56
|
+
"""
|
|
57
|
+
_send("/reload")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_binds() -> list[Bind]:
|
|
61
|
+
"""Read all keybinds from Hyprland."""
|
|
62
|
+
data = _query_json("binds")
|
|
63
|
+
return [Bind.from_dict(b) for b in data]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_monitors() -> list[Monitor]:
|
|
67
|
+
"""Read all monitors from Hyprland."""
|
|
68
|
+
data = _query_json("monitors")
|
|
69
|
+
return [Monitor.from_dict(m) for m in data]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_animations() -> tuple[list[Animation], list[dict]]:
|
|
73
|
+
"""Read all animations and bezier curves from Hyprland.
|
|
74
|
+
|
|
75
|
+
Returns (animations_list, curves_list).
|
|
76
|
+
"""
|
|
77
|
+
data = _query_json("animations")
|
|
78
|
+
if not isinstance(data, list) or len(data) != 2:
|
|
79
|
+
raise CommandError(f"Unexpected animations response format: {type(data)}")
|
|
80
|
+
animations = [Animation.from_dict(a) for a in data[0]]
|
|
81
|
+
return animations, data[1]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def is_running() -> bool:
|
|
85
|
+
"""Check if a Hyprland instance is reachable."""
|
|
86
|
+
try:
|
|
87
|
+
getoption("general:gaps_in")
|
|
88
|
+
return True
|
|
89
|
+
except (ConnectionError, CommandError):
|
|
90
|
+
return False
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Exception hierarchy for Hyprland IPC errors."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class HyprlandError(Exception):
|
|
5
|
+
"""Base exception for all Hyprland IPC errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ConnectionError(HyprlandError):
|
|
9
|
+
"""Cannot reach the Hyprland socket."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CommandError(HyprlandError):
|
|
13
|
+
"""Hyprland rejected a command."""
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Hyprland event monitoring via socket2."""
|
|
2
|
+
|
|
3
|
+
import socket
|
|
4
|
+
from collections.abc import Iterator
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from ._socket import _event_socket_path
|
|
8
|
+
from .errors import ConnectionError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Event:
|
|
13
|
+
name: str
|
|
14
|
+
data: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def connect_event_socket(timeout: float | None = None) -> socket.socket:
|
|
18
|
+
"""Connect to Hyprland's event socket and return the raw socket.
|
|
19
|
+
|
|
20
|
+
The caller owns the socket and must close it. The raw fd can be
|
|
21
|
+
used with external event loops (e.g. GLib.io_add_watch).
|
|
22
|
+
"""
|
|
23
|
+
try:
|
|
24
|
+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
25
|
+
if timeout is not None:
|
|
26
|
+
sock.settimeout(timeout)
|
|
27
|
+
sock.connect(_event_socket_path())
|
|
28
|
+
return sock
|
|
29
|
+
except Exception as e:
|
|
30
|
+
raise ConnectionError(f"Cannot reach Hyprland event socket: {e}") from e
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _parse_event_line(line: str) -> Event | None:
|
|
34
|
+
"""Parse a single event line into an Event."""
|
|
35
|
+
line = line.strip()
|
|
36
|
+
if not line:
|
|
37
|
+
return None
|
|
38
|
+
if ">>" in line:
|
|
39
|
+
name, data = line.split(">>", 1)
|
|
40
|
+
return Event(name=name, data=data)
|
|
41
|
+
return Event(name=line, data="")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def events(timeout: float | None = None) -> Iterator[Event]:
|
|
45
|
+
"""Yield events from Hyprland's event socket. Blocking iterator."""
|
|
46
|
+
sock = connect_event_socket(timeout)
|
|
47
|
+
try:
|
|
48
|
+
buf = ""
|
|
49
|
+
while True:
|
|
50
|
+
try:
|
|
51
|
+
chunk = sock.recv(4096)
|
|
52
|
+
except TimeoutError:
|
|
53
|
+
return
|
|
54
|
+
if not chunk:
|
|
55
|
+
break
|
|
56
|
+
buf += chunk.decode()
|
|
57
|
+
while "\n" in buf:
|
|
58
|
+
line, buf = buf.split("\n", 1)
|
|
59
|
+
event = _parse_event_line(line)
|
|
60
|
+
if event is not None:
|
|
61
|
+
yield event
|
|
62
|
+
finally:
|
|
63
|
+
sock.close()
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Typed dataclasses for Hyprland IPC responses."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class Monitor:
|
|
8
|
+
name: str
|
|
9
|
+
make: str
|
|
10
|
+
model: str
|
|
11
|
+
width: int
|
|
12
|
+
height: int
|
|
13
|
+
refresh_rate: float
|
|
14
|
+
x: int
|
|
15
|
+
y: int
|
|
16
|
+
scale: float
|
|
17
|
+
transform: int = 0
|
|
18
|
+
focused: bool = False
|
|
19
|
+
current_format: str = ""
|
|
20
|
+
available_modes: list[str] = field(default_factory=list)
|
|
21
|
+
bitdepth: str | None = None
|
|
22
|
+
vrr: str | None = None
|
|
23
|
+
cm: str | None = None
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def from_dict(cls, data: dict) -> "Monitor":
|
|
27
|
+
# Infer bitdepth from pixel format
|
|
28
|
+
fmt = data.get("currentFormat", "")
|
|
29
|
+
bitdepth = None
|
|
30
|
+
if "2101010" in fmt or "16161616" in fmt:
|
|
31
|
+
bitdepth = "10"
|
|
32
|
+
|
|
33
|
+
# Color management preset
|
|
34
|
+
cm_raw = data.get("colorManagementPreset")
|
|
35
|
+
cm = cm_raw if cm_raw and cm_raw not in ("default", "srgb") else None
|
|
36
|
+
|
|
37
|
+
return cls(
|
|
38
|
+
name=data["name"],
|
|
39
|
+
make=data.get("make", ""),
|
|
40
|
+
model=data.get("model", ""),
|
|
41
|
+
width=data["width"],
|
|
42
|
+
height=data["height"],
|
|
43
|
+
refresh_rate=data["refreshRate"],
|
|
44
|
+
x=data["x"],
|
|
45
|
+
y=data["y"],
|
|
46
|
+
scale=data["scale"],
|
|
47
|
+
transform=data.get("transform", 0),
|
|
48
|
+
focused=data.get("focused", False),
|
|
49
|
+
current_format=data.get("currentFormat", ""),
|
|
50
|
+
available_modes=[
|
|
51
|
+
m
|
|
52
|
+
if isinstance(m, str)
|
|
53
|
+
else f"{m['width']}x{m['height']}@{m['refreshRate']:.2f}Hz"
|
|
54
|
+
for m in data.get("availableModes", [])
|
|
55
|
+
],
|
|
56
|
+
bitdepth=bitdepth,
|
|
57
|
+
cm=cm,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class Bind:
|
|
63
|
+
modmask: int
|
|
64
|
+
key: str
|
|
65
|
+
dispatcher: str
|
|
66
|
+
arg: str
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def from_dict(cls, data: dict) -> "Bind":
|
|
70
|
+
return cls(
|
|
71
|
+
modmask=data["modmask"],
|
|
72
|
+
key=data["key"],
|
|
73
|
+
dispatcher=data["dispatcher"],
|
|
74
|
+
arg=data["arg"],
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class Animation:
|
|
80
|
+
name: str
|
|
81
|
+
overridden: bool
|
|
82
|
+
enabled: bool
|
|
83
|
+
speed: float
|
|
84
|
+
bezier: str
|
|
85
|
+
style: str = ""
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def from_dict(cls, data: dict) -> "Animation":
|
|
89
|
+
return cls(
|
|
90
|
+
name=data["name"],
|
|
91
|
+
overridden=data["overridden"],
|
|
92
|
+
enabled=data["enabled"],
|
|
93
|
+
speed=data["speed"],
|
|
94
|
+
bezier=data["bezier"],
|
|
95
|
+
style=data.get("style", ""),
|
|
96
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Tests for event parsing."""
|
|
2
|
+
|
|
3
|
+
from hyprland_socket.events import Event, _parse_event_line
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestParseEventLine:
|
|
7
|
+
def test_basic_event(self):
|
|
8
|
+
event = _parse_event_line("workspace>>2")
|
|
9
|
+
assert event == Event(name="workspace", data="2")
|
|
10
|
+
|
|
11
|
+
def test_monitor_added(self):
|
|
12
|
+
event = _parse_event_line("monitoradded>>DP-3")
|
|
13
|
+
assert event == Event(name="monitoradded", data="DP-3")
|
|
14
|
+
|
|
15
|
+
def test_event_with_comma_data(self):
|
|
16
|
+
event = _parse_event_line("openwindow>>80abc,2,kitty,Alacritty")
|
|
17
|
+
assert event.name == "openwindow"
|
|
18
|
+
assert event.data == "80abc,2,kitty,Alacritty"
|
|
19
|
+
|
|
20
|
+
def test_empty_data(self):
|
|
21
|
+
event = _parse_event_line("configreloaded>>")
|
|
22
|
+
assert event == Event(name="configreloaded", data="")
|
|
23
|
+
|
|
24
|
+
def test_empty_line(self):
|
|
25
|
+
assert _parse_event_line("") is None
|
|
26
|
+
|
|
27
|
+
def test_whitespace_only(self):
|
|
28
|
+
assert _parse_event_line(" ") is None
|
|
29
|
+
|
|
30
|
+
def test_no_separator(self):
|
|
31
|
+
event = _parse_event_line("someevent")
|
|
32
|
+
assert event == Event(name="someevent", data="")
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Tests for model construction from Hyprland JSON dicts."""
|
|
2
|
+
|
|
3
|
+
from hyprland_socket.models import Animation, Bind, Monitor
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestMonitorFromDict:
|
|
7
|
+
SAMPLE = {
|
|
8
|
+
"name": "DP-2",
|
|
9
|
+
"make": "Samsung",
|
|
10
|
+
"model": "Odyssey G9",
|
|
11
|
+
"width": 3440,
|
|
12
|
+
"height": 1440,
|
|
13
|
+
"refreshRate": 99.98,
|
|
14
|
+
"x": 0,
|
|
15
|
+
"y": 0,
|
|
16
|
+
"scale": 1.6,
|
|
17
|
+
"transform": 0,
|
|
18
|
+
"focused": True,
|
|
19
|
+
"currentFormat": "XRGB8888",
|
|
20
|
+
"availableModes": ["3440x1440@99.98Hz", "2560x1440@60.00Hz"],
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
def test_basic_fields(self):
|
|
24
|
+
m = Monitor.from_dict(self.SAMPLE)
|
|
25
|
+
assert m.name == "DP-2"
|
|
26
|
+
assert m.width == 3440
|
|
27
|
+
assert m.height == 1440
|
|
28
|
+
assert m.refresh_rate == 99.98
|
|
29
|
+
assert m.scale == 1.6
|
|
30
|
+
|
|
31
|
+
def test_optional_fields(self):
|
|
32
|
+
m = Monitor.from_dict(self.SAMPLE)
|
|
33
|
+
assert m.focused is True
|
|
34
|
+
assert m.current_format == "XRGB8888"
|
|
35
|
+
assert m.transform == 0
|
|
36
|
+
|
|
37
|
+
def test_available_modes(self):
|
|
38
|
+
m = Monitor.from_dict(self.SAMPLE)
|
|
39
|
+
assert m.available_modes == ["3440x1440@99.98Hz", "2560x1440@60.00Hz"]
|
|
40
|
+
|
|
41
|
+
def test_missing_optional_fields(self):
|
|
42
|
+
minimal = {
|
|
43
|
+
"name": "eDP-1",
|
|
44
|
+
"width": 1920,
|
|
45
|
+
"height": 1080,
|
|
46
|
+
"refreshRate": 60.0,
|
|
47
|
+
"x": 0,
|
|
48
|
+
"y": 0,
|
|
49
|
+
"scale": 1.0,
|
|
50
|
+
}
|
|
51
|
+
m = Monitor.from_dict(minimal)
|
|
52
|
+
assert m.make == ""
|
|
53
|
+
assert m.model == ""
|
|
54
|
+
assert m.transform == 0
|
|
55
|
+
assert m.focused is False
|
|
56
|
+
assert m.current_format == ""
|
|
57
|
+
assert m.available_modes == []
|
|
58
|
+
assert m.bitdepth is None
|
|
59
|
+
assert m.vrr is None
|
|
60
|
+
assert m.cm is None
|
|
61
|
+
|
|
62
|
+
def test_bitdepth_inferred_from_format(self):
|
|
63
|
+
data = {**self.SAMPLE, "currentFormat": "XRGB2101010"}
|
|
64
|
+
m = Monitor.from_dict(data)
|
|
65
|
+
assert m.bitdepth == "10"
|
|
66
|
+
|
|
67
|
+
def test_bitdepth_none_for_8bit(self):
|
|
68
|
+
data = {**self.SAMPLE, "currentFormat": "XRGB8888"}
|
|
69
|
+
m = Monitor.from_dict(data)
|
|
70
|
+
assert m.bitdepth is None
|
|
71
|
+
|
|
72
|
+
def test_cm_from_preset(self):
|
|
73
|
+
data = {**self.SAMPLE, "colorManagementPreset": "hdr"}
|
|
74
|
+
m = Monitor.from_dict(data)
|
|
75
|
+
assert m.cm == "hdr"
|
|
76
|
+
|
|
77
|
+
def test_cm_none_for_default(self):
|
|
78
|
+
data = {**self.SAMPLE, "colorManagementPreset": "srgb"}
|
|
79
|
+
m = Monitor.from_dict(data)
|
|
80
|
+
assert m.cm is None
|
|
81
|
+
|
|
82
|
+
def test_available_modes_as_dicts(self):
|
|
83
|
+
data = {
|
|
84
|
+
**self.SAMPLE,
|
|
85
|
+
"availableModes": [
|
|
86
|
+
{"width": 1920, "height": 1080, "refreshRate": 60.0},
|
|
87
|
+
],
|
|
88
|
+
}
|
|
89
|
+
m = Monitor.from_dict(data)
|
|
90
|
+
assert m.available_modes == ["1920x1080@60.00Hz"]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class TestBindFromDict:
|
|
94
|
+
def test_basic(self):
|
|
95
|
+
b = Bind.from_dict(
|
|
96
|
+
{
|
|
97
|
+
"modmask": 64,
|
|
98
|
+
"key": "Q",
|
|
99
|
+
"dispatcher": "killactive",
|
|
100
|
+
"arg": "",
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
assert b.modmask == 64
|
|
104
|
+
assert b.key == "Q"
|
|
105
|
+
assert b.dispatcher == "killactive"
|
|
106
|
+
assert b.arg == ""
|
|
107
|
+
|
|
108
|
+
def test_with_arg(self):
|
|
109
|
+
b = Bind.from_dict(
|
|
110
|
+
{
|
|
111
|
+
"modmask": 64,
|
|
112
|
+
"key": "1",
|
|
113
|
+
"dispatcher": "workspace",
|
|
114
|
+
"arg": "1",
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
assert b.arg == "1"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class TestAnimationFromDict:
|
|
121
|
+
def test_basic(self):
|
|
122
|
+
a = Animation.from_dict(
|
|
123
|
+
{
|
|
124
|
+
"name": "windows",
|
|
125
|
+
"overridden": True,
|
|
126
|
+
"enabled": True,
|
|
127
|
+
"speed": 6.0,
|
|
128
|
+
"bezier": "default",
|
|
129
|
+
"style": "slide",
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
assert a.name == "windows"
|
|
133
|
+
assert a.overridden is True
|
|
134
|
+
assert a.enabled is True
|
|
135
|
+
assert a.speed == 6.0
|
|
136
|
+
assert a.bezier == "default"
|
|
137
|
+
assert a.style == "slide"
|
|
138
|
+
|
|
139
|
+
def test_missing_style(self):
|
|
140
|
+
a = Animation.from_dict(
|
|
141
|
+
{
|
|
142
|
+
"name": "fade",
|
|
143
|
+
"overridden": False,
|
|
144
|
+
"enabled": True,
|
|
145
|
+
"speed": 7.0,
|
|
146
|
+
"bezier": "default",
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
assert a.style == ""
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
revision = 3
|
|
3
|
+
requires-python = ">=3.12"
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "colorama"
|
|
7
|
+
version = "0.4.6"
|
|
8
|
+
source = { registry = "https://pypi.org/simple" }
|
|
9
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
|
10
|
+
wheels = [
|
|
11
|
+
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[[package]]
|
|
15
|
+
name = "hyprland-socket"
|
|
16
|
+
version = "0.1.0"
|
|
17
|
+
source = { editable = "." }
|
|
18
|
+
|
|
19
|
+
[package.dev-dependencies]
|
|
20
|
+
dev = [
|
|
21
|
+
{ name = "pytest" },
|
|
22
|
+
{ name = "ruff" },
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[package.metadata]
|
|
26
|
+
|
|
27
|
+
[package.metadata.requires-dev]
|
|
28
|
+
dev = [
|
|
29
|
+
{ name = "pytest", specifier = ">=9.0" },
|
|
30
|
+
{ name = "ruff", specifier = ">=0.11" },
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[[package]]
|
|
34
|
+
name = "iniconfig"
|
|
35
|
+
version = "2.3.0"
|
|
36
|
+
source = { registry = "https://pypi.org/simple" }
|
|
37
|
+
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
|
38
|
+
wheels = [
|
|
39
|
+
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[[package]]
|
|
43
|
+
name = "packaging"
|
|
44
|
+
version = "26.0"
|
|
45
|
+
source = { registry = "https://pypi.org/simple" }
|
|
46
|
+
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
|
|
47
|
+
wheels = [
|
|
48
|
+
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[[package]]
|
|
52
|
+
name = "pluggy"
|
|
53
|
+
version = "1.6.0"
|
|
54
|
+
source = { registry = "https://pypi.org/simple" }
|
|
55
|
+
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
|
56
|
+
wheels = [
|
|
57
|
+
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
[[package]]
|
|
61
|
+
name = "pygments"
|
|
62
|
+
version = "2.19.2"
|
|
63
|
+
source = { registry = "https://pypi.org/simple" }
|
|
64
|
+
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
|
65
|
+
wheels = [
|
|
66
|
+
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
[[package]]
|
|
70
|
+
name = "pytest"
|
|
71
|
+
version = "9.0.2"
|
|
72
|
+
source = { registry = "https://pypi.org/simple" }
|
|
73
|
+
dependencies = [
|
|
74
|
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
75
|
+
{ name = "iniconfig" },
|
|
76
|
+
{ name = "packaging" },
|
|
77
|
+
{ name = "pluggy" },
|
|
78
|
+
{ name = "pygments" },
|
|
79
|
+
]
|
|
80
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
|
|
81
|
+
wheels = [
|
|
82
|
+
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
[[package]]
|
|
86
|
+
name = "ruff"
|
|
87
|
+
version = "0.15.6"
|
|
88
|
+
source = { registry = "https://pypi.org/simple" }
|
|
89
|
+
sdist = { url = "https://files.pythonhosted.org/packages/51/df/f8629c19c5318601d3121e230f74cbee7a3732339c52b21daa2b82ef9c7d/ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4", size = 4597916, upload-time = "2026-03-12T23:05:47.51Z" }
|
|
90
|
+
wheels = [
|
|
91
|
+
{ url = "https://files.pythonhosted.org/packages/9e/2f/4e03a7e5ce99b517e98d3b4951f411de2b0fa8348d39cf446671adcce9a2/ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff", size = 10508953, upload-time = "2026-03-12T23:05:17.246Z" },
|
|
92
|
+
{ url = "https://files.pythonhosted.org/packages/70/60/55bcdc3e9f80bcf39edf0cd272da6fa511a3d94d5a0dd9e0adf76ceebdb4/ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3", size = 10942257, upload-time = "2026-03-12T23:05:23.076Z" },
|
|
93
|
+
{ url = "https://files.pythonhosted.org/packages/e7/f9/005c29bd1726c0f492bfa215e95154cf480574140cb5f867c797c18c790b/ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb", size = 10322683, upload-time = "2026-03-12T23:05:33.738Z" },
|
|
94
|
+
{ url = "https://files.pythonhosted.org/packages/5f/74/2f861f5fd7cbb2146bddb5501450300ce41562da36d21868c69b7a828169/ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8", size = 10660986, upload-time = "2026-03-12T23:05:53.245Z" },
|
|
95
|
+
{ url = "https://files.pythonhosted.org/packages/c1/a1/309f2364a424eccb763cdafc49df843c282609f47fe53aa83f38272389e0/ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e", size = 10332177, upload-time = "2026-03-12T23:05:56.145Z" },
|
|
96
|
+
{ url = "https://files.pythonhosted.org/packages/30/41/7ebf1d32658b4bab20f8ac80972fb19cd4e2c6b78552be263a680edc55ac/ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15", size = 11170783, upload-time = "2026-03-12T23:06:01.742Z" },
|
|
97
|
+
{ url = "https://files.pythonhosted.org/packages/76/be/6d488f6adca047df82cd62c304638bcb00821c36bd4881cfca221561fdfc/ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9", size = 12044201, upload-time = "2026-03-12T23:05:28.697Z" },
|
|
98
|
+
{ url = "https://files.pythonhosted.org/packages/71/68/e6f125df4af7e6d0b498f8d373274794bc5156b324e8ab4bf5c1b4fc0ec7/ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab", size = 11421561, upload-time = "2026-03-12T23:05:31.236Z" },
|
|
99
|
+
{ url = "https://files.pythonhosted.org/packages/f1/9f/f85ef5fd01a52e0b472b26dc1b4bd228b8f6f0435975442ffa4741278703/ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e", size = 11310928, upload-time = "2026-03-12T23:05:45.288Z" },
|
|
100
|
+
{ url = "https://files.pythonhosted.org/packages/8c/26/b75f8c421f5654304b89471ed384ae8c7f42b4dff58fa6ce1626d7f2b59a/ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c", size = 11235186, upload-time = "2026-03-12T23:05:50.677Z" },
|
|
101
|
+
{ url = "https://files.pythonhosted.org/packages/fc/d4/d5a6d065962ff7a68a86c9b4f5500f7d101a0792078de636526c0edd40da/ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512", size = 10635231, upload-time = "2026-03-12T23:05:37.044Z" },
|
|
102
|
+
{ url = "https://files.pythonhosted.org/packages/d6/56/7c3acf3d50910375349016cf33de24be021532042afbed87942858992491/ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0", size = 10340357, upload-time = "2026-03-12T23:06:04.748Z" },
|
|
103
|
+
{ url = "https://files.pythonhosted.org/packages/06/54/6faa39e9c1033ff6a3b6e76b5df536931cd30caf64988e112bbf91ef5ce5/ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb", size = 10860583, upload-time = "2026-03-12T23:05:58.978Z" },
|
|
104
|
+
{ url = "https://files.pythonhosted.org/packages/cb/1e/509a201b843b4dfb0b32acdedf68d951d3377988cae43949ba4c4133a96a/ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0", size = 11410976, upload-time = "2026-03-12T23:05:39.955Z" },
|
|
105
|
+
{ url = "https://files.pythonhosted.org/packages/6c/25/3fc9114abf979a41673ce877c08016f8e660ad6cf508c3957f537d2e9fa9/ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c", size = 10616872, upload-time = "2026-03-12T23:05:42.451Z" },
|
|
106
|
+
{ url = "https://files.pythonhosted.org/packages/89/7a/09ece68445ceac348df06e08bf75db72d0e8427765b96c9c0ffabc1be1d9/ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406", size = 11787271, upload-time = "2026-03-12T23:05:20.168Z" },
|
|
107
|
+
{ url = "https://files.pythonhosted.org/packages/7f/d0/578c47dd68152ddddddf31cd7fc67dc30b7cdf639a86275fda821b0d9d98/ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837", size = 11060497, upload-time = "2026-03-12T23:05:25.968Z" },
|
|
108
|
+
]
|