charz-input 0.1.3__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,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Havsalt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: charz-input
3
+ Version: 0.1.3
4
+ Summary: Versatile input interfaces, for keyboard and console
5
+ Keywords: input,keyboard,console,interface,terminal,pygame
6
+ Author: Havsalt
7
+ Author-email: Havsalt <77575424+Havsalt@users.noreply.github.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Software Development :: Libraries
17
+ Classifier: Topic :: Software Development :: User Interfaces
18
+ Classifier: Typing :: Typed
19
+ Requires-Dist: keyboard~=0.13.5
20
+ Requires-Dist: linflex~=0.3.3
21
+ Requires-Dist: pygame~=2.6
22
+ Requires-Python: >=3.12
23
+ Project-URL: Homepage, https://github.com/havsalt/charz-input
24
+ Project-URL: Repository, https://github.com/havsalt/charz-input
25
+ Project-URL: Documentation, https://havsalt.github.io/charz-input
26
+ Project-URL: Issues, https://github.com/havsalt/charz-input/issues
27
+ Project-URL: Changelog, https://github.com/havsalt/charz-input/blob/main/CHANGELOG.md
28
+ Description-Content-Type: text/markdown
29
+
30
+ # Charz Input
31
+
32
+ Versatile input interfaces, for keyboard and console.
33
+
34
+ ## Examples
35
+
36
+ Defining an **Action Enum**, with _all_ possible actions as variants:
37
+
38
+ ```python
39
+ from enum import Enum, auto
40
+
41
+
42
+ class Action(Enum):
43
+ SWITCH_INPUT = auto()
44
+ MOVE_LEFT = auto()
45
+ MOVE_RIGHT = auto()
46
+ ```
47
+
48
+ Switching between `Keyboard` and `Controller` input methods:
49
+
50
+ ```python
51
+ # ... defined `Action` enum ...
52
+
53
+ from charz_input import Input, Keyboard, Controller
54
+ import pygame
55
+
56
+ handler: Input[Action] = Keyboard({
57
+ Action.SWITCH_INPUT: "Space",
58
+ Action.MOVE_LEFT: "A",
59
+ Action.MOVE_RIGHT: "D",
60
+ })
61
+ player_position_x: int = 0
62
+
63
+ while True:
64
+ handler.capture_states()
65
+
66
+ if handler.is_action_just_pressed(Action.SWITCH_INPUT):
67
+ if isinstance(handler, Keyboard):
68
+ handler = Controller({
69
+ Action.SWITCH_INPUT: pygame.CONTROLLER_BUTTON_A,
70
+ Action.MOVE_LEFT: Controller.Trigger(
71
+ pygame.CONTROLLER_AXIS_LEFTX,
72
+ Controller.Trigger.Limit.NEGATIVE,
73
+ ),
74
+ Action.MOVE_RIGHT: Controller.Trigger(
75
+ pygame.CONTROLLER_AXIS_LEFTX,
76
+ Controller.Trigger.Limit.POSITIVE,
77
+ deadzone=35,
78
+ ),
79
+ })
80
+ elif isinstance(handler, Controller):
81
+ handler = Keyboard({
82
+ Action.SWITCH_INPUT: "Space",
83
+ Action.MOVE_LEFT: "A",
84
+ Action.MOVE_RIGHT: "D",
85
+ })
86
+ continue
87
+
88
+ if handler.is_action_pressed(Action.MOVE_LEFT):
89
+ player_position_x -= 1
90
+ if handler.is_action_pressed(Action.MOVE_RIGHT):
91
+ player_position_x += 1
92
+
93
+ print(f"The player is located at x = {player_position_x}")
94
+ ```
95
+
96
+ Declearing default keybinds:
97
+
98
+ ```python
99
+ import charz_input
100
+
101
+ # ... defined `Action` enum ...
102
+
103
+
104
+ class CustomKeyboard(charz_input.Keyboard):
105
+ default_action_binds = {
106
+ Action.SWITCH_INPUT: "Space",
107
+ Action.MOVE_LEFT: "A",
108
+ Action.MOVE_RIGHT: "D",
109
+ }
110
+
111
+
112
+ handler = CustomKeyboard[Action]()
113
+ different_handler = CustomKeyboard({
114
+ Action.MOVE_RIGHT: "Y", # Overrides keybind
115
+ # NOTE: The rest are set to their defaults,
116
+ # as defined by `default_action_binds`
117
+ })
118
+
119
+ # ... logic to check for action presses ...
120
+ ```
121
+
122
+ You can ensure all actions are mapped at **runtime**, using a **typed class item** when instantiating:
123
+
124
+ ```python
125
+ # ┌──────┬─────> Action enum, as class item
126
+ handler = Keyboard[Action]({
127
+ Action.SWITCH_INPUT: "Space",
128
+ Action.MOVE_LEFT: "A",
129
+ # Missing `Action.MOVE_RIGHT`
130
+ })
131
+ handler.capture_states() # This triggers the check
132
+ >> AssertionError: Missing actions
133
+
134
+ # Annotating the variable, and not the instance, will have different results:
135
+ handler: Keyboard[Action] = Keyboard({...})
136
+ ```
137
+
138
+ Checking for keyboard presses with a modifier:
139
+
140
+ ```python
141
+ handler = Keyboard[Action](
142
+ # ... keybinds ...
143
+ Action.MOVE_LEFT: "W+{modifier}", # Dynamic, in the sense of initializing
144
+ Action.MOVE_RIGHT: "W",
145
+ Action.SWITCH_INPUT: "Ctrl+Space" # Can also be hardcoded
146
+ modifier="Space", # Default: "Shift"
147
+ )
148
+
149
+ handler.capture_states()
150
+ print(handler.is_pressed())
151
+ handler.action_binds
152
+ handler.capture_states()
153
+ print(handler.is_pressed())
154
+ ```
155
+
156
+ ## Roadmap
157
+
158
+ - [ ] Expose action binds at runtime, so they can be changed
159
+ - [ ] Figure out whether automatic device detection for controller is ideal
160
+
161
+ ## License
162
+
163
+ MIT
@@ -0,0 +1,134 @@
1
+ # Charz Input
2
+
3
+ Versatile input interfaces, for keyboard and console.
4
+
5
+ ## Examples
6
+
7
+ Defining an **Action Enum**, with _all_ possible actions as variants:
8
+
9
+ ```python
10
+ from enum import Enum, auto
11
+
12
+
13
+ class Action(Enum):
14
+ SWITCH_INPUT = auto()
15
+ MOVE_LEFT = auto()
16
+ MOVE_RIGHT = auto()
17
+ ```
18
+
19
+ Switching between `Keyboard` and `Controller` input methods:
20
+
21
+ ```python
22
+ # ... defined `Action` enum ...
23
+
24
+ from charz_input import Input, Keyboard, Controller
25
+ import pygame
26
+
27
+ handler: Input[Action] = Keyboard({
28
+ Action.SWITCH_INPUT: "Space",
29
+ Action.MOVE_LEFT: "A",
30
+ Action.MOVE_RIGHT: "D",
31
+ })
32
+ player_position_x: int = 0
33
+
34
+ while True:
35
+ handler.capture_states()
36
+
37
+ if handler.is_action_just_pressed(Action.SWITCH_INPUT):
38
+ if isinstance(handler, Keyboard):
39
+ handler = Controller({
40
+ Action.SWITCH_INPUT: pygame.CONTROLLER_BUTTON_A,
41
+ Action.MOVE_LEFT: Controller.Trigger(
42
+ pygame.CONTROLLER_AXIS_LEFTX,
43
+ Controller.Trigger.Limit.NEGATIVE,
44
+ ),
45
+ Action.MOVE_RIGHT: Controller.Trigger(
46
+ pygame.CONTROLLER_AXIS_LEFTX,
47
+ Controller.Trigger.Limit.POSITIVE,
48
+ deadzone=35,
49
+ ),
50
+ })
51
+ elif isinstance(handler, Controller):
52
+ handler = Keyboard({
53
+ Action.SWITCH_INPUT: "Space",
54
+ Action.MOVE_LEFT: "A",
55
+ Action.MOVE_RIGHT: "D",
56
+ })
57
+ continue
58
+
59
+ if handler.is_action_pressed(Action.MOVE_LEFT):
60
+ player_position_x -= 1
61
+ if handler.is_action_pressed(Action.MOVE_RIGHT):
62
+ player_position_x += 1
63
+
64
+ print(f"The player is located at x = {player_position_x}")
65
+ ```
66
+
67
+ Declearing default keybinds:
68
+
69
+ ```python
70
+ import charz_input
71
+
72
+ # ... defined `Action` enum ...
73
+
74
+
75
+ class CustomKeyboard(charz_input.Keyboard):
76
+ default_action_binds = {
77
+ Action.SWITCH_INPUT: "Space",
78
+ Action.MOVE_LEFT: "A",
79
+ Action.MOVE_RIGHT: "D",
80
+ }
81
+
82
+
83
+ handler = CustomKeyboard[Action]()
84
+ different_handler = CustomKeyboard({
85
+ Action.MOVE_RIGHT: "Y", # Overrides keybind
86
+ # NOTE: The rest are set to their defaults,
87
+ # as defined by `default_action_binds`
88
+ })
89
+
90
+ # ... logic to check for action presses ...
91
+ ```
92
+
93
+ You can ensure all actions are mapped at **runtime**, using a **typed class item** when instantiating:
94
+
95
+ ```python
96
+ # ┌──────┬─────> Action enum, as class item
97
+ handler = Keyboard[Action]({
98
+ Action.SWITCH_INPUT: "Space",
99
+ Action.MOVE_LEFT: "A",
100
+ # Missing `Action.MOVE_RIGHT`
101
+ })
102
+ handler.capture_states() # This triggers the check
103
+ >> AssertionError: Missing actions
104
+
105
+ # Annotating the variable, and not the instance, will have different results:
106
+ handler: Keyboard[Action] = Keyboard({...})
107
+ ```
108
+
109
+ Checking for keyboard presses with a modifier:
110
+
111
+ ```python
112
+ handler = Keyboard[Action](
113
+ # ... keybinds ...
114
+ Action.MOVE_LEFT: "W+{modifier}", # Dynamic, in the sense of initializing
115
+ Action.MOVE_RIGHT: "W",
116
+ Action.SWITCH_INPUT: "Ctrl+Space" # Can also be hardcoded
117
+ modifier="Space", # Default: "Shift"
118
+ )
119
+
120
+ handler.capture_states()
121
+ print(handler.is_pressed())
122
+ handler.action_binds
123
+ handler.capture_states()
124
+ print(handler.is_pressed())
125
+ ```
126
+
127
+ ## Roadmap
128
+
129
+ - [ ] Expose action binds at runtime, so they can be changed
130
+ - [ ] Figure out whether automatic device detection for controller is ideal
131
+
132
+ ## License
133
+
134
+ MIT
@@ -0,0 +1,69 @@
1
+ [build-system]
2
+ requires = ["uv_build~=0.12.0"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "charz-input"
7
+ version = "0.1.3"
8
+ description = "Versatile input interfaces, for keyboard and console"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICEN[CS]E*"]
12
+ keywords = [
13
+ "input",
14
+ "keyboard",
15
+ "console",
16
+ "interface",
17
+ "terminal",
18
+ "pygame",
19
+ ]
20
+ classifiers = [
21
+ "Development Status :: 4 - Beta",
22
+ "Intended Audience :: Developers",
23
+ "Operating System :: OS Independent",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Topic :: Software Development :: Libraries",
28
+ "Topic :: Software Development :: User Interfaces",
29
+ "Typing :: Typed",
30
+ ]
31
+ requires-python = ">=3.12"
32
+ dependencies = [
33
+ "keyboard~=0.13.5",
34
+ "linflex~=0.3.3",
35
+ "pygame~=2.6",
36
+ ]
37
+
38
+ [[project.authors]]
39
+ name = "Havsalt"
40
+ email = "77575424+Havsalt@users.noreply.github.com"
41
+
42
+ [project.urls]
43
+ Homepage = "https://github.com/havsalt/charz-input"
44
+ Repository = "https://github.com/havsalt/charz-input"
45
+ Documentation = "https://havsalt.github.io/charz-input"
46
+ Issues = "https://github.com/havsalt/charz-input/issues"
47
+ Changelog = "https://github.com/havsalt/charz-input/blob/main/CHANGELOG.md"
48
+
49
+ [dependency-groups]
50
+ dev = [
51
+ { include-group = "test" },
52
+ { include-group = "docs" },
53
+ "commitizen",
54
+ ]
55
+ test = [
56
+ "ty==0.0.65",
57
+ "ruff~=0.16.0",
58
+ "pytest~=9.1",
59
+ "tox~=4.58",
60
+ "tox-uv~=1.36",
61
+ ]
62
+ docs = [
63
+ "zensical==0.0.52",
64
+ "pydoc-markdown~=4.8",
65
+ "pydoc-markdown-havsalt~=0.1.3",
66
+ ]
67
+
68
+ [tool.uv.sources.commitizen]
69
+ git = "https://github.com/havsalt/commitizen"
@@ -0,0 +1,61 @@
1
+ [build-system]
2
+ requires = ["uv_build~=0.12.0"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "charz-input"
7
+ version = "0.1.3"
8
+ description = "Versatile input interfaces, for keyboard and console"
9
+ authors = [
10
+ { name = "Havsalt", email = "77575424+Havsalt@users.noreply.github.com" }
11
+ ]
12
+ readme = "README.md"
13
+ license = "MIT"
14
+ license-files = ["LICEN[CS]E*"]
15
+ keywords = ["input", "keyboard", "console", "interface", "terminal", "pygame"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Topic :: Software Development :: Libraries",
24
+ "Topic :: Software Development :: User Interfaces",
25
+ "Typing :: Typed",
26
+ ]
27
+ requires-python = ">=3.12"
28
+ dependencies = [
29
+ "keyboard~=0.13.5",
30
+ "linflex~=0.3.3",
31
+ "pygame~=2.6",
32
+ ]
33
+
34
+ [dependency-groups]
35
+ dev = [
36
+ { include-group = "test" },
37
+ { include-group = "docs" },
38
+ "commitizen", # Use custom version until v5 with overrides
39
+ ]
40
+ test = [
41
+ "ty==0.0.65",
42
+ "ruff~=0.16.0",
43
+ "pytest~=9.1",
44
+ "tox~=4.58",
45
+ "tox-uv~=1.36",
46
+ ]
47
+ docs = [
48
+ "zensical==0.0.52",
49
+ "pydoc-markdown~=4.8",
50
+ "pydoc-markdown-havsalt~=0.1.3",
51
+ ]
52
+
53
+ [tool.uv.sources]
54
+ commitizen = { git = "https://github.com/havsalt/commitizen" }
55
+
56
+ [project.urls]
57
+ Homepage = "https://github.com/havsalt/charz-input"
58
+ Repository = "https://github.com/havsalt/charz-input"
59
+ Documentation = "https://havsalt.github.io/charz-input"
60
+ Issues = "https://github.com/havsalt/charz-input/issues"
61
+ Changelog = "https://github.com/havsalt/charz-input/blob/main/CHANGELOG.md"
@@ -0,0 +1,18 @@
1
+ """
2
+ Charz: Input
3
+ ============
4
+
5
+ Versatile input interfaces, for keyboard and console.
6
+ """
7
+
8
+ __all__ = [
9
+ "AbstractInput",
10
+ "Controller",
11
+ "Input",
12
+ "Keyboard",
13
+ ]
14
+
15
+ from ._abstract import AbstractInput
16
+ from ._controller import Controller
17
+ from ._interface import Input
18
+ from ._keyboard import Keyboard
@@ -0,0 +1,52 @@
1
+ from abc import ABC, abstractmethod
2
+ from enum import Enum
3
+
4
+ from linflex import Vec2
5
+
6
+
7
+ class AbstractInput[Action: Enum](ABC):
8
+ """Abstract base class for [`Input`] implementations.
9
+
10
+ Inheriting from [`AbstractInput`] was chosen over `Protocol`,
11
+ to provide better inferred type relation in subclasses:
12
+
13
+ ```python
14
+ handler: Input = Controller[MyAction]()
15
+ reveal_type(handler) # type is: Input[MyAction]
16
+ ```
17
+
18
+ `Protocol` would have yielded `Input[Unknown]`.
19
+
20
+ An **advantage** to using an **abstract base class**,
21
+ is partial default behaviour, like `get_vector`.
22
+ """
23
+
24
+ @abstractmethod
25
+ def capture_states(self) -> None:
26
+ """Capture press state of all `Action` variants.
27
+
28
+ Used for monitoring changes over time.
29
+
30
+ !!! tip "Technical"
31
+
32
+ Should be called at the start of the frame,
33
+ **before** actions are checked for.
34
+ """
35
+
36
+ @abstractmethod
37
+ def is_action_pressed(self, action: Action) -> bool: ...
38
+
39
+ @abstractmethod
40
+ def is_action_just_pressed(self, action: Action) -> bool: ...
41
+
42
+ def get_vector(
43
+ self,
44
+ negative_x: Action,
45
+ positive_x: Action,
46
+ negative_y: Action,
47
+ positive_y: Action,
48
+ ) -> Vec2:
49
+ return Vec2(
50
+ self.is_action_pressed(positive_x) - self.is_action_pressed(negative_x),
51
+ self.is_action_pressed(positive_y) - self.is_action_pressed(negative_y),
52
+ )
@@ -0,0 +1,124 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from enum import Enum, auto
5
+ from typing import assert_never, get_args
6
+
7
+ import pygame
8
+
9
+ from ._abstract import AbstractInput
10
+
11
+ type Positive[N: (int, float, complex)] = N
12
+ type Percent = int
13
+ """Example: `x: Percent = 10`, meaning _10%_. When computing: `result = x / 100`"""
14
+
15
+
16
+ class Controller[Action: Enum](AbstractInput[Action]):
17
+ """[`Controller`] implementation for [`Input`] handler."""
18
+
19
+ type DeviceID = int
20
+ type Button = int
21
+ type Axis = int
22
+ type ActionBinds = Mapping[Action, Controller.Button | Controller.Trigger]
23
+
24
+ # Can be defined in subclass definition
25
+ default_action_binds: ActionBinds # ClassVar
26
+
27
+ class Trigger:
28
+ class Limit(Enum):
29
+ POSITIVE = auto()
30
+ NEGATIVE = auto()
31
+
32
+ def __init__(
33
+ self,
34
+ axis: Controller.Axis,
35
+ limit: Controller.Trigger.Limit | None = None,
36
+ deadzone: Positive[Percent] = 10,
37
+ ) -> None:
38
+ if deadzone <= 0:
39
+ raise ValueError(f"Argument 'deadzone' <= 0, got {deadzone}")
40
+
41
+ if not pygame.display.get_init():
42
+ pygame.display.init()
43
+ if not pygame.joystick.get_init():
44
+ pygame.joystick.init()
45
+
46
+ self.axis = axis
47
+ self.limit = limit
48
+ self.deadzone = deadzone
49
+
50
+ def __init__(
51
+ self,
52
+ action_binds: Controller.ActionBinds | None = None,
53
+ device_id: Controller.DeviceID | None = None,
54
+ ) -> None:
55
+ if device_id is not None:
56
+ self._joystick = pygame.joystick.Joystick(device_id)
57
+ else: # Attempt to connect to the next available controller
58
+ # NOTE: This branch _might_ be redundant,
59
+ # and param `device_id` should be required
60
+ for attempt_id in range(pygame.joystick.get_count()):
61
+ try:
62
+ joystick = pygame.joystick.Joystick(attempt_id)
63
+ except pygame.error:
64
+ continue
65
+ self._joystick = joystick
66
+ break
67
+ else: # nobreak
68
+ raise ValueError("Could not automatically find an available controller")
69
+ self._action_states = dict[Action, bool]()
70
+ self._last_action_states = dict[Action, bool]()
71
+
72
+ default_binds: Controller.ActionBinds = getattr(
73
+ self,
74
+ "default_action_binds",
75
+ {},
76
+ )
77
+ final_binds = dict(default_binds) # Prevent mutable reference
78
+ override_binds = action_binds or {}
79
+ final_binds.update(override_binds)
80
+ self._action_binds: Controller.ActionBinds = final_binds
81
+
82
+ def capture_states(self) -> None:
83
+ """Capture all action state.
84
+
85
+ If instantiated with `handler = Controller[MyAction]()`,
86
+ then `MyAction` will be available at runtime.
87
+ Therefore, by calling this method,
88
+ all actions are ensured they map correctly from an action variant to its state.
89
+
90
+ `NOTE` Requires `pygame.event.pump` to be called **before** `capture_states`.
91
+ """
92
+ if (orig_class := getattr(self, "__orig_class__", None)) is not None:
93
+ action_enum = get_args(orig_class)[0]
94
+ assert all(map(self._action_binds.__contains__, action_enum)), (
95
+ "Missing actions"
96
+ )
97
+ self._last_action_states = self._action_states
98
+ self._action_states = {
99
+ action: self.is_action_pressed(action) for action in self._action_binds
100
+ }
101
+
102
+ def is_action_pressed(self, action: Action) -> bool:
103
+ trigger = self._action_binds[action]
104
+
105
+ if not isinstance(trigger, Controller.Trigger):
106
+ return self._joystick.get_button(trigger)
107
+
108
+ strength = self._joystick.get_axis(trigger.axis)
109
+ match trigger.limit:
110
+ case Controller.Trigger.Limit.POSITIVE:
111
+ return strength > trigger.deadzone / 100
112
+ case Controller.Trigger.Limit.NEGATIVE:
113
+ return strength < -trigger.deadzone / 100
114
+ case None:
115
+ return abs(strength) > trigger.deadzone / 100
116
+ case _:
117
+ assert_never(trigger.limit)
118
+
119
+ def is_action_just_pressed(self, action: Action) -> bool:
120
+ return ( # fmt: off
121
+ not self._last_action_states.get(action, True)
122
+ and self.is_action_pressed(action)
123
+ )
124
+ # fmt: on
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+ from typing import TYPE_CHECKING, Protocol
5
+
6
+ if TYPE_CHECKING:
7
+ from linflex import Vec2
8
+
9
+
10
+ class Input[Action: Enum](Protocol):
11
+ """Protocol for implementing input handlers.
12
+
13
+ Implementations of [`Input`] are used as input handlers,
14
+ which then can be **swapped at runtime**.
15
+
16
+ Example:
17
+ You have a game that takes input from the keyboard,
18
+ until a controller is detected.
19
+ """
20
+
21
+ def capture_states(self) -> None: ...
22
+ def is_action_pressed(self, action: Action) -> bool: ...
23
+ def is_action_just_pressed(self, action: Action) -> bool: ...
24
+ def get_vector(
25
+ self,
26
+ negative_x: Action,
27
+ positive_x: Action,
28
+ negative_y: Action,
29
+ positive_y: Action,
30
+ ) -> Vec2: ...
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from enum import Enum
5
+ from typing import get_args
6
+
7
+ import keyboard
8
+
9
+ from ._abstract import AbstractInput
10
+
11
+
12
+ class Keyboard[Action: Enum](AbstractInput[Action]):
13
+ """[`Keyboard`] implementation for [`Input`] handler."""
14
+
15
+ type Key = str | int
16
+ type ScanCode = int
17
+ type KeyCombination = str | list[ScanCode]
18
+ type ActionBinds = Mapping[Action, Keyboard.Key | Keyboard.KeyCombination]
19
+
20
+ #: Can be defined in subclass definition
21
+ default_action_binds: ActionBinds # ClassVar
22
+
23
+ def __init__(
24
+ self,
25
+ action_binds: ActionBinds | None = None,
26
+ modifier_key: Keyboard.Key = "Shift",
27
+ ) -> None:
28
+ self._action_states = dict[Action, bool]()
29
+ self._last_action_states = dict[Action, bool]()
30
+ self._modifier_key = modifier_key
31
+
32
+ default_binds: Keyboard.ActionBinds = getattr(self, "default_action_binds", {})
33
+ final_binds = dict(default_binds) # Prevent mutable reference
34
+ override_binds = action_binds or {}
35
+ final_binds.update(override_binds)
36
+ self._action_binds: Keyboard.ActionBinds = final_binds
37
+
38
+ def capture_states(self) -> None:
39
+ # Ensure all actions are mapped.
40
+ # If instantiated with `handler = Keyboard[MyAction]()`,
41
+ # then `MyAction` will be available at runtime
42
+ if (orig_class := getattr(self, "__orig_class__", None)) is not None:
43
+ action_enum = get_args(orig_class)[0]
44
+ assert all(map(self._action_binds.__contains__, action_enum)), (
45
+ "Missing actions"
46
+ )
47
+ self._last_action_states = self._action_states
48
+ self._action_states = {
49
+ action: self.is_action_pressed(action) for action in self._action_binds
50
+ }
51
+
52
+ def is_action_pressed(self, action: Action) -> bool:
53
+ trigger = self._action_binds[action]
54
+ if isinstance(trigger, str):
55
+ keys = trigger.format(modifier=self._modifier_key)
56
+ return keyboard.is_pressed(keys)
57
+ # Single scancode or list of scancodes
58
+ return keyboard.is_pressed(trigger)
59
+
60
+ def is_action_just_pressed(self, action: Action) -> bool:
61
+ return ( # fmt: off
62
+ not self._last_action_states.get(action, True)
63
+ and self.is_action_pressed(action)
64
+ )
65
+ # fmt: on
File without changes