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.
@@ -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,2 @@
1
+ def hello() -> str:
2
+ return "Hello from capslockstep!"
@@ -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
+ )
@@ -0,0 +1,12 @@
1
+ from datetime import datetime
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class CapsLockEvent(BaseModel):
7
+ value: bool
8
+
9
+
10
+ class CapsLockState(BaseModel):
11
+ value: bool
12
+ date: datetime
File without changes