capslockstep 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.
- capslockstep-0.1.0/PKG-INFO +10 -0
- capslockstep-0.1.0/pyproject.toml +20 -0
- capslockstep-0.1.0/src/capslockstep/__init__.py +2 -0
- capslockstep-0.1.0/src/capslockstep/cli.py +49 -0
- capslockstep-0.1.0/src/capslockstep/key.py +55 -0
- capslockstep-0.1.0/src/capslockstep/models.py +12 -0
- capslockstep-0.1.0/src/capslockstep/py.typed +0 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: capslockstep
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author: Jonathan Ehwald
|
|
6
|
+
Author-email: Jonathan Ehwald <github@ehwald.info>
|
|
7
|
+
Requires-Dist: aiohttp>=3.13.3
|
|
8
|
+
Requires-Dist: libevdev>=0.13.1
|
|
9
|
+
Requires-Dist: pydantic>=2.12.5
|
|
10
|
+
Requires-Python: >=3.14
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "capslockstep"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "Jonathan Ehwald", email = "github@ehwald.info" }
|
|
7
|
+
]
|
|
8
|
+
requires-python = ">=3.14"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"aiohttp>=3.13.3",
|
|
11
|
+
"libevdev>=0.13.1",
|
|
12
|
+
"pydantic>=2.12.5",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
capslockstep = "capslockstep.cli:main"
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["uv_build>=0.10.6,<0.11.0"]
|
|
20
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import asyncio
|
|
3
|
+
import platform
|
|
4
|
+
from contextlib import suppress
|
|
5
|
+
|
|
6
|
+
import aiohttp
|
|
7
|
+
|
|
8
|
+
from capslockstep.key import CapsLock, CapsLockLinux
|
|
9
|
+
from capslockstep.models import CapsLockEvent, CapsLockState
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main():
|
|
13
|
+
parser = argparse.ArgumentParser()
|
|
14
|
+
parser.add_argument("room_id", help="The ID of the room to join")
|
|
15
|
+
parser.add_argument("--api-url", default="capslockstep.fastapicloud.dev")
|
|
16
|
+
args = parser.parse_args()
|
|
17
|
+
|
|
18
|
+
match platform.system():
|
|
19
|
+
case "Linux":
|
|
20
|
+
caps_lock = CapsLockLinux()
|
|
21
|
+
case _:
|
|
22
|
+
raise NotImplementedError(f"Unsupported system: {platform.system()}")
|
|
23
|
+
|
|
24
|
+
asyncio.run(stay_lock_step(caps_lock, args.api_url, args.room_id))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def stay_lock_step(caps_lock: CapsLock, api_url: str, room_id: str) -> None:
|
|
28
|
+
async with aiohttp.ClientSession() as session:
|
|
29
|
+
async with session.ws_connect(f"wss://{api_url}/caps-lock/{room_id}") as ws:
|
|
30
|
+
|
|
31
|
+
async def writer():
|
|
32
|
+
with suppress(asyncio.CancelledError):
|
|
33
|
+
async for new_value in caps_lock.watch():
|
|
34
|
+
event = CapsLockEvent(value=new_value)
|
|
35
|
+
await ws.send_str(event.model_dump_json())
|
|
36
|
+
|
|
37
|
+
writer_task = asyncio.create_task(writer())
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
async for message in ws:
|
|
41
|
+
if message.type == aiohttp.WSMsgType.TEXT:
|
|
42
|
+
serialized_state = message.data
|
|
43
|
+
state = CapsLockState.model_validate_json(serialized_state)
|
|
44
|
+
caps_lock.set(state.value)
|
|
45
|
+
except asyncio.CancelledError:
|
|
46
|
+
pass
|
|
47
|
+
finally:
|
|
48
|
+
writer_task.cancel()
|
|
49
|
+
await writer_task
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from collections.abc import AsyncGenerator
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import libevdev
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CapsLock(ABC):
|
|
10
|
+
@abstractmethod
|
|
11
|
+
def watch(self) -> AsyncGenerator[bool]:
|
|
12
|
+
"""Watch for changes to the Caps Lock key state"""
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def set(self, value: bool) -> None:
|
|
16
|
+
"""Set the state of the Caps Lock key to the given value."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CapsLockLinux(CapsLock):
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
self.dev = libevdev.Device()
|
|
22
|
+
self.dev.name = "Caps Lock Step Device"
|
|
23
|
+
self.dev.enable(libevdev.KEY_CAPSLOCK)
|
|
24
|
+
self.uinput = self.dev.create_uinput_device()
|
|
25
|
+
self.old_value = self.get_current_value()
|
|
26
|
+
|
|
27
|
+
async def watch(self) -> AsyncGenerator[bool]:
|
|
28
|
+
while True:
|
|
29
|
+
new_value = self.get_current_value()
|
|
30
|
+
|
|
31
|
+
if new_value != self.old_value:
|
|
32
|
+
yield new_value
|
|
33
|
+
self.old_value = new_value
|
|
34
|
+
|
|
35
|
+
await asyncio.sleep(0.1)
|
|
36
|
+
|
|
37
|
+
def set(self, value: bool) -> None:
|
|
38
|
+
if value != self.get_current_value():
|
|
39
|
+
self.toggle()
|
|
40
|
+
|
|
41
|
+
def toggle(self) -> None:
|
|
42
|
+
self.uinput.send_events(
|
|
43
|
+
[
|
|
44
|
+
libevdev.InputEvent(libevdev.KEY_CAPSLOCK, 1),
|
|
45
|
+
libevdev.InputEvent(libevdev.SYN_REPORT, 0),
|
|
46
|
+
libevdev.InputEvent(libevdev.KEY_CAPSLOCK, 0),
|
|
47
|
+
libevdev.InputEvent(libevdev.SYN_REPORT, 0),
|
|
48
|
+
]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def get_current_value(self) -> bool:
|
|
52
|
+
return any(
|
|
53
|
+
path.read_text().strip() == "1"
|
|
54
|
+
for path in Path("/sys/class/leds").glob("input*::capslock/brightness")
|
|
55
|
+
)
|
|
File without changes
|