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.
- charz_input-0.1.3/LICENSE +22 -0
- charz_input-0.1.3/PKG-INFO +163 -0
- charz_input-0.1.3/README.md +134 -0
- charz_input-0.1.3/pyproject.toml +69 -0
- charz_input-0.1.3/pyproject.toml.orig +61 -0
- charz_input-0.1.3/src/charz_input/__init__.py +18 -0
- charz_input-0.1.3/src/charz_input/_abstract.py +52 -0
- charz_input-0.1.3/src/charz_input/_controller.py +124 -0
- charz_input-0.1.3/src/charz_input/_interface.py +30 -0
- charz_input-0.1.3/src/charz_input/_keyboard.py +65 -0
- charz_input-0.1.3/src/charz_input/py.typed +0 -0
|
@@ -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
|