gogo_keyboard 0.0.0__py3-none-any.whl
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.
- gogo_keyboard/__init__.py +1 -0
- gogo_keyboard/_zenoh_simple_listener.py +50 -0
- gogo_keyboard/codes.py +263 -0
- gogo_keyboard/example.py +60 -0
- gogo_keyboard/icons/gogo.png +0 -0
- gogo_keyboard/icons/gogo_happy.png +0 -0
- gogo_keyboard/icons/gogo_happy2.png +0 -0
- gogo_keyboard/keyboard.py +184 -0
- gogo_keyboard/mini_example.py +9 -0
- gogo_keyboard/py.typed +0 -0
- gogo_keyboard/ros_node.py +75 -0
- gogo_keyboard/zenoh_node.py +75 -0
- gogo_keyboard-0.0.0.dist-info/METADATA +103 -0
- gogo_keyboard-0.0.0.dist-info/RECORD +17 -0
- gogo_keyboard-0.0.0.dist-info/WHEEL +5 -0
- gogo_keyboard-0.0.0.dist-info/licenses/LICENSE +21 -0
- gogo_keyboard-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import zenoh
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main(topic: str = "key_press", config_path: Optional[str] = None):
|
|
10
|
+
if config_path is None:
|
|
11
|
+
conf = zenoh.Config()
|
|
12
|
+
else:
|
|
13
|
+
filepath = os.path.expanduser(config_path)
|
|
14
|
+
conf = zenoh.Config.from_file(filepath)
|
|
15
|
+
|
|
16
|
+
with zenoh.open(conf) as session:
|
|
17
|
+
print(f"Subscribing to '{topic}'")
|
|
18
|
+
def listener(sample: zenoh.Sample):
|
|
19
|
+
print(
|
|
20
|
+
f"Received {sample.kind} ('{sample.key_expr}': '{sample.payload.to_string()}')"
|
|
21
|
+
)
|
|
22
|
+
session.declare_subscriber(topic, listener)
|
|
23
|
+
while True:
|
|
24
|
+
time.sleep(1)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# --- Command line argument parsing --- --- --- --- --- ---
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
import argparse
|
|
30
|
+
|
|
31
|
+
parser = argparse.ArgumentParser(prog="z_sub", description="zenoh sub example")
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"-c",
|
|
34
|
+
"--config",
|
|
35
|
+
dest="config",
|
|
36
|
+
default=None,
|
|
37
|
+
help="Zenoh config path",
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"-t",
|
|
41
|
+
"--topic",
|
|
42
|
+
dest="topic",
|
|
43
|
+
default="key_press",
|
|
44
|
+
help="ROS2 topic to publish key events to",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
args = parser.parse_args()
|
|
48
|
+
|
|
49
|
+
with suppress(KeyboardInterrupt):
|
|
50
|
+
main(args.topic, args.config)
|
gogo_keyboard/codes.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
KEY_UNKNOWN = 0
|
|
2
|
+
KEY_A = 4
|
|
3
|
+
KEY_B = 5
|
|
4
|
+
KEY_C = 6
|
|
5
|
+
KEY_D = 7
|
|
6
|
+
KEY_E = 8
|
|
7
|
+
KEY_F = 9
|
|
8
|
+
KEY_G = 10
|
|
9
|
+
KEY_H = 11
|
|
10
|
+
KEY_I = 12
|
|
11
|
+
KEY_J = 13
|
|
12
|
+
KEY_K = 14
|
|
13
|
+
KEY_L = 15
|
|
14
|
+
KEY_M = 16
|
|
15
|
+
KEY_N = 17
|
|
16
|
+
KEY_O = 18
|
|
17
|
+
KEY_P = 19
|
|
18
|
+
KEY_Q = 20
|
|
19
|
+
KEY_R = 21
|
|
20
|
+
KEY_S = 22
|
|
21
|
+
KEY_T = 23
|
|
22
|
+
KEY_U = 24
|
|
23
|
+
KEY_V = 25
|
|
24
|
+
KEY_W = 26
|
|
25
|
+
KEY_X = 27
|
|
26
|
+
KEY_Y = 28
|
|
27
|
+
KEY_Z = 29
|
|
28
|
+
KEY_1 = 30
|
|
29
|
+
KEY_2 = 31
|
|
30
|
+
KEY_3 = 32
|
|
31
|
+
KEY_4 = 33
|
|
32
|
+
KEY_5 = 34
|
|
33
|
+
KEY_6 = 35
|
|
34
|
+
KEY_7 = 36
|
|
35
|
+
KEY_8 = 37
|
|
36
|
+
KEY_9 = 38
|
|
37
|
+
KEY_0 = 39
|
|
38
|
+
KEY_RETURN = 40
|
|
39
|
+
KEY_ESCAPE = 41
|
|
40
|
+
KEY_BACKSPACE = 42
|
|
41
|
+
KEY_TAB = 43
|
|
42
|
+
KEY_SPACE = 44
|
|
43
|
+
KEY_MINUS = 45
|
|
44
|
+
KEY_EQUALS = 46
|
|
45
|
+
KEY_LEFTBRACKET = 47
|
|
46
|
+
KEY_RIGHTBRACKET = 48
|
|
47
|
+
KEY_BACKSLASH = 49
|
|
48
|
+
KEY_NONUSHASH = 50
|
|
49
|
+
KEY_SEMICOLON = 51
|
|
50
|
+
KEY_APOSTROPHE = 52
|
|
51
|
+
KEY_GRAVE = 53
|
|
52
|
+
KEY_COMMA = 54
|
|
53
|
+
KEY_PERIOD = 55
|
|
54
|
+
KEY_SLASH = 56
|
|
55
|
+
KEY_CAPSLOCK = 57
|
|
56
|
+
KEY_F1 = 58
|
|
57
|
+
KEY_F2 = 59
|
|
58
|
+
KEY_F3 = 60
|
|
59
|
+
KEY_F4 = 61
|
|
60
|
+
KEY_F5 = 62
|
|
61
|
+
KEY_F6 = 63
|
|
62
|
+
KEY_F7 = 64
|
|
63
|
+
KEY_F8 = 65
|
|
64
|
+
KEY_F9 = 66
|
|
65
|
+
KEY_F10 = 67
|
|
66
|
+
KEY_F11 = 68
|
|
67
|
+
KEY_F12 = 69
|
|
68
|
+
KEY_PRINTSCREEN = 70
|
|
69
|
+
KEY_SCROLLLOCK = 71
|
|
70
|
+
KEY_PAUSE = 72
|
|
71
|
+
KEY_INSERT = 73
|
|
72
|
+
KEY_HOME = 74
|
|
73
|
+
KEY_PAGEUP = 75
|
|
74
|
+
KEY_DELETE = 76
|
|
75
|
+
KEY_END = 77
|
|
76
|
+
KEY_PAGEDOWN = 78
|
|
77
|
+
KEY_RIGHT = 79
|
|
78
|
+
KEY_LEFT = 80
|
|
79
|
+
KEY_DOWN = 81
|
|
80
|
+
KEY_UP = 82
|
|
81
|
+
KEY_NUMLOCKCLEAR = 83
|
|
82
|
+
KEY_KP_DIVIDE = 84
|
|
83
|
+
KEY_KP_MULTIPLY = 85
|
|
84
|
+
KEY_KP_MINUS = 86
|
|
85
|
+
KEY_KP_PLUS = 87
|
|
86
|
+
KEY_KP_ENTER = 88
|
|
87
|
+
KEY_KP_1 = 89
|
|
88
|
+
KEY_KP_2 = 90
|
|
89
|
+
KEY_KP_3 = 91
|
|
90
|
+
KEY_KP_4 = 92
|
|
91
|
+
KEY_KP_5 = 93
|
|
92
|
+
KEY_KP_6 = 94
|
|
93
|
+
KEY_KP_7 = 95
|
|
94
|
+
KEY_KP_8 = 96
|
|
95
|
+
KEY_KP_9 = 97
|
|
96
|
+
KEY_KP_0 = 98
|
|
97
|
+
KEY_KP_PERIOD = 99
|
|
98
|
+
KEY_NONUSBACKSLASH = 100
|
|
99
|
+
KEY_APPLICATION = 101
|
|
100
|
+
KEY_POWER = 102
|
|
101
|
+
KEY_KP_EQUALS = 103
|
|
102
|
+
KEY_F13 = 104
|
|
103
|
+
KEY_F14 = 105
|
|
104
|
+
KEY_F15 = 106
|
|
105
|
+
KEY_F16 = 107
|
|
106
|
+
KEY_F17 = 108
|
|
107
|
+
KEY_F18 = 109
|
|
108
|
+
KEY_F19 = 110
|
|
109
|
+
KEY_F20 = 111
|
|
110
|
+
KEY_F21 = 112
|
|
111
|
+
KEY_F22 = 113
|
|
112
|
+
KEY_F23 = 114
|
|
113
|
+
KEY_F24 = 115
|
|
114
|
+
KEY_EXECUTE = 116
|
|
115
|
+
KEY_HELP = 117
|
|
116
|
+
KEY_MENU = 118
|
|
117
|
+
KEY_SELECT = 119
|
|
118
|
+
KEY_STOP = 120
|
|
119
|
+
KEY_AGAIN = 121
|
|
120
|
+
KEY_UNDO = 122
|
|
121
|
+
KEY_CUT = 123
|
|
122
|
+
KEY_COPY = 124
|
|
123
|
+
KEY_PASTE = 125
|
|
124
|
+
KEY_FIND = 126
|
|
125
|
+
KEY_MUTE = 127
|
|
126
|
+
KEY_VOLUMEUP = 128
|
|
127
|
+
KEY_VOLUMEDOWN = 129
|
|
128
|
+
KEY_KP_COMMA = 133
|
|
129
|
+
KEY_KP_EQUALSAS400 = 134
|
|
130
|
+
KEY_INTERNATIONAL1 = 135
|
|
131
|
+
KEY_INTERNATIONAL2 = 136
|
|
132
|
+
KEY_INTERNATIONAL3 = 137
|
|
133
|
+
KEY_INTERNATIONAL4 = 138
|
|
134
|
+
KEY_INTERNATIONAL5 = 139
|
|
135
|
+
KEY_INTERNATIONAL6 = 140
|
|
136
|
+
KEY_INTERNATIONAL7 = 141
|
|
137
|
+
KEY_INTERNATIONAL8 = 142
|
|
138
|
+
KEY_INTERNATIONAL9 = 143
|
|
139
|
+
KEY_LANG1 = 144
|
|
140
|
+
KEY_LANG2 = 145
|
|
141
|
+
KEY_LANG3 = 146
|
|
142
|
+
KEY_LANG4 = 147
|
|
143
|
+
KEY_LANG5 = 148
|
|
144
|
+
KEY_LANG6 = 149
|
|
145
|
+
KEY_LANG7 = 150
|
|
146
|
+
KEY_LANG8 = 151
|
|
147
|
+
KEY_LANG9 = 152
|
|
148
|
+
KEY_ALTERASE = 153
|
|
149
|
+
KEY_SYSREQ = 154
|
|
150
|
+
KEY_CANCEL = 155
|
|
151
|
+
KEY_CLEAR = 156
|
|
152
|
+
KEY_PRIOR = 157
|
|
153
|
+
KEY_RETURN2 = 158
|
|
154
|
+
KEY_SEPARATOR = 159
|
|
155
|
+
KEY_OUT = 160
|
|
156
|
+
KEY_OPER = 161
|
|
157
|
+
KEY_CLEARAGAIN = 162
|
|
158
|
+
KEY_CRSEL = 163
|
|
159
|
+
KEY_EXSEL = 164
|
|
160
|
+
KEY_KP_00 = 176
|
|
161
|
+
KEY_KP_000 = 177
|
|
162
|
+
KEY_THOUSANDSSEPARATOR = 178
|
|
163
|
+
KEY_DECIMALSEPARATOR = 179
|
|
164
|
+
KEY_CURRENCYUNIT = 180
|
|
165
|
+
KEY_CURRENCYSUBUNIT = 181
|
|
166
|
+
KEY_KP_LEFTPAREN = 182
|
|
167
|
+
KEY_KP_RIGHTPAREN = 183
|
|
168
|
+
KEY_KP_LEFTBRACE = 184
|
|
169
|
+
KEY_KP_RIGHTBRACE = 185
|
|
170
|
+
KEY_KP_TAB = 186
|
|
171
|
+
KEY_KP_BACKSPACE = 187
|
|
172
|
+
KEY_KP_A = 188
|
|
173
|
+
KEY_KP_B = 189
|
|
174
|
+
KEY_KP_C = 190
|
|
175
|
+
KEY_KP_D = 191
|
|
176
|
+
KEY_KP_E = 192
|
|
177
|
+
KEY_KP_F = 193
|
|
178
|
+
KEY_KP_XOR = 194
|
|
179
|
+
KEY_KP_POWER = 195
|
|
180
|
+
KEY_KP_PERCENT = 196
|
|
181
|
+
KEY_KP_LESS = 197
|
|
182
|
+
KEY_KP_GREATER = 198
|
|
183
|
+
KEY_KP_AMPERSAND = 199
|
|
184
|
+
KEY_KP_DBLAMPERSAND = 200
|
|
185
|
+
KEY_KP_VERTICALBAR = 201
|
|
186
|
+
KEY_KP_DBLVERTICALBAR = 202
|
|
187
|
+
KEY_KP_COLON = 203
|
|
188
|
+
KEY_KP_HASH = 204
|
|
189
|
+
KEY_KP_SPACE = 205
|
|
190
|
+
KEY_KP_AT = 206
|
|
191
|
+
KEY_KP_EXCLAM = 207
|
|
192
|
+
KEY_KP_MEMSTORE = 208
|
|
193
|
+
KEY_KP_MEMRECALL = 209
|
|
194
|
+
KEY_KP_MEMCLEAR = 210
|
|
195
|
+
KEY_KP_MEMADD = 211
|
|
196
|
+
KEY_KP_MEMSUBTRACT = 212
|
|
197
|
+
KEY_KP_MEMMULTIPLY = 213
|
|
198
|
+
KEY_KP_MEMDIVIDE = 214
|
|
199
|
+
KEY_KP_PLUSMINUS = 215
|
|
200
|
+
KEY_KP_CLEAR = 216
|
|
201
|
+
KEY_KP_CLEARENTRY = 217
|
|
202
|
+
KEY_KP_BINARY = 218
|
|
203
|
+
KEY_KP_OCTAL = 219
|
|
204
|
+
KEY_KP_DECIMAL = 220
|
|
205
|
+
KEY_KP_HEXADECIMAL = 221
|
|
206
|
+
KEY_LCTRL = 224
|
|
207
|
+
KEY_LSHIFT = 225
|
|
208
|
+
KEY_LALT = 226
|
|
209
|
+
KEY_LGUI = 227
|
|
210
|
+
KEY_RCTRL = 228
|
|
211
|
+
KEY_RSHIFT = 229
|
|
212
|
+
KEY_RALT = 230
|
|
213
|
+
KEY_RGUI = 231
|
|
214
|
+
KEY_MODE = 257
|
|
215
|
+
KEY_AUDIONEXT = 258
|
|
216
|
+
KEY_AUDIOPREV = 259
|
|
217
|
+
KEY_AUDIOSTOP = 260
|
|
218
|
+
KEY_AUDIOPLAY = 261
|
|
219
|
+
KEY_AUDIOMUTE = 262
|
|
220
|
+
KEY_MEDIASELECT = 263
|
|
221
|
+
KEY_WWW = 264
|
|
222
|
+
KEY_MAIL = 265
|
|
223
|
+
KEY_CALCULATOR = 266
|
|
224
|
+
KEY_COMPUTER = 267
|
|
225
|
+
KEY_AC_SEARCH = 268
|
|
226
|
+
KEY_AC_HOME = 269
|
|
227
|
+
KEY_AC_BACK = 270
|
|
228
|
+
KEY_AC_FORWARD = 271
|
|
229
|
+
KEY_AC_STOP = 272
|
|
230
|
+
KEY_AC_REFRESH = 273
|
|
231
|
+
KEY_AC_BOOKMARKS = 274
|
|
232
|
+
KEY_BRIGHTNESSDOWN = 275
|
|
233
|
+
KEY_BRIGHTNESSUP = 276
|
|
234
|
+
KEY_DISPLAYSWITCH = 277
|
|
235
|
+
KEY_KBDILLUMTOGGLE = 278
|
|
236
|
+
KEY_KBDILLUMDOWN = 279
|
|
237
|
+
KEY_KBDILLUMUP = 280
|
|
238
|
+
KEY_EJECT = 281
|
|
239
|
+
KEY_SLEEP = 282
|
|
240
|
+
KEY_APP1 = 283
|
|
241
|
+
KEY_APP2 = 284
|
|
242
|
+
KEY_AUDIOREWIND = 285
|
|
243
|
+
KEY_AUDIOFASTFORWARD = 286
|
|
244
|
+
KEY_SOFTLEFT = 287
|
|
245
|
+
KEY_SOFTRIGHT = 288
|
|
246
|
+
KEY_CALL = 289
|
|
247
|
+
KEY_ENDCALL = 290
|
|
248
|
+
|
|
249
|
+
MODIFIER_NONE=0
|
|
250
|
+
MODIFIER_LSHIFT=1
|
|
251
|
+
MODIFIER_RSHIFT=2
|
|
252
|
+
MODIFIER_LCTRL=64
|
|
253
|
+
MODIFIER_RCTRL=128
|
|
254
|
+
MODIFIER_LALT=256
|
|
255
|
+
MODIFIER_RALT=512
|
|
256
|
+
MODIFIER_LMETA=1024
|
|
257
|
+
MODIFIER_RMETA=2048
|
|
258
|
+
MODIFIER_NUM=4096
|
|
259
|
+
MODIFIER_CAPS=8192
|
|
260
|
+
MODIFIER_MODE=16384
|
|
261
|
+
MODIFIER_RESERVED=32768
|
|
262
|
+
|
|
263
|
+
SDL_NUM_SCANCODES = 512
|
gogo_keyboard/example.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
from dataclasses import asdict
|
|
4
|
+
|
|
5
|
+
import sdl2
|
|
6
|
+
import sdl2.ext
|
|
7
|
+
|
|
8
|
+
from . import codes
|
|
9
|
+
from .keyboard import Key, KeySub
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def detect_window_closed(event: asyncio.Event):
|
|
13
|
+
await event.wait()
|
|
14
|
+
print("Gorilla window closed by user.")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def detect_CtrlC(key_sub: KeySub):
|
|
18
|
+
async for k in key_sub.listen_reliable():
|
|
19
|
+
if k.symbol == "C" and (k.modifiers & codes.MODIFIER_LCTRL) and k.is_pressed:
|
|
20
|
+
print("Ctrl+C in Gorilla window by user.")
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def print_keys(key_sub: KeySub):
|
|
25
|
+
async for k in key_sub.listen_reliable():
|
|
26
|
+
print(json.dumps({k: str(v) for k,v in asdict(k).items()}, indent=4))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def async_main():
|
|
30
|
+
window_closed_event = asyncio.Event()
|
|
31
|
+
key_sub = KeySub(
|
|
32
|
+
termination_callback=window_closed_event.set,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
print_task = asyncio.create_task(print_keys(key_sub))
|
|
37
|
+
close_task = asyncio.create_task(detect_window_closed(window_closed_event))
|
|
38
|
+
ctrl_task = asyncio.create_task(detect_CtrlC(key_sub))
|
|
39
|
+
await asyncio.wait(
|
|
40
|
+
[print_task, close_task, ctrl_task], return_when=asyncio.FIRST_COMPLETED
|
|
41
|
+
)
|
|
42
|
+
print_task.cancel()
|
|
43
|
+
close_task.cancel()
|
|
44
|
+
ctrl_task.cancel()
|
|
45
|
+
finally:
|
|
46
|
+
key_sub.close()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def main():
|
|
50
|
+
try:
|
|
51
|
+
asyncio.run(async_main())
|
|
52
|
+
except KeyboardInterrupt:
|
|
53
|
+
print("KeyboardInterrupt in python process")
|
|
54
|
+
finally:
|
|
55
|
+
sdl2.ext.quit()
|
|
56
|
+
print("Exited cleanly :)")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
main()
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import colorsys
|
|
3
|
+
import copy
|
|
4
|
+
import dataclasses
|
|
5
|
+
import random
|
|
6
|
+
from importlib.resources import files
|
|
7
|
+
from typing import Any, Callable, Dict, Tuple
|
|
8
|
+
|
|
9
|
+
import asyncio_for_robotics
|
|
10
|
+
import sdl2
|
|
11
|
+
import sdl2.ext
|
|
12
|
+
from asyncio_for_robotics.core.sub import BaseSub
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def scancode_to_color(scancode: int) -> Tuple[int, int, int]:
|
|
16
|
+
if scancode == 0:
|
|
17
|
+
return 255, 0, 0
|
|
18
|
+
brightness = 255
|
|
19
|
+
r, g, b = colorsys.hsv_to_rgb((scancode % 30) / 30, 0.5, 1)
|
|
20
|
+
return int(r * brightness), int(g * brightness), int(b * brightness)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclasses.dataclass(frozen=True)
|
|
24
|
+
class Key:
|
|
25
|
+
symbol: str
|
|
26
|
+
code: int
|
|
27
|
+
modifiers: int
|
|
28
|
+
is_pressed: bool
|
|
29
|
+
sdl_event: sdl2.SDL_KeyboardEvent
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def from_sdl(cls, sdl_event: sdl2.SDL_KeyboardEvent) -> "Key":
|
|
33
|
+
return cls(
|
|
34
|
+
symbol=sdl2.SDL_GetKeyName(sdl_event.keysym.sym).decode(),
|
|
35
|
+
code=sdl_event.keysym.scancode,
|
|
36
|
+
modifiers=sdl_event.keysym.mod,
|
|
37
|
+
is_pressed=bool(sdl_event.state),
|
|
38
|
+
sdl_event=sdl_event,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def raise_keyboard_interupt():
|
|
43
|
+
raise KeyboardInterrupt
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class KeySub(BaseSub[Key]):
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
termination_callback: Callable[[], Any] = raise_keyboard_interupt,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Creates a sdl2 window with an asyncio_for_robotics subcriber getting
|
|
52
|
+
the key presses inputed in the window.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
termination_callback: Will be called when the Gorilla window is
|
|
56
|
+
closed by the user.
|
|
57
|
+
"""
|
|
58
|
+
self.termination_callback: Callable[[], Any] = termination_callback
|
|
59
|
+
r, g, b = colorsys.hsv_to_rgb(random.random(), (random.random() + 1) / 2, 1)
|
|
60
|
+
self.idle_color: Tuple[int, int, int] = int(r * 255), int(g * 255), int(b * 255)
|
|
61
|
+
|
|
62
|
+
self._pressed_keys: Dict[int, Key] = dict()
|
|
63
|
+
self._surface_icon = sdl2.ext.load_img(
|
|
64
|
+
str(files("gogo_keyboard").joinpath("icons/gogo.png"))
|
|
65
|
+
)
|
|
66
|
+
super().__init__()
|
|
67
|
+
self.window: sdl2.ext.Window
|
|
68
|
+
self.renderer: sdl2.ext.Renderer
|
|
69
|
+
self._init_sdl()
|
|
70
|
+
self._sdl_thread: asyncio.Task = asyncio.create_task(self._sdl_loop())
|
|
71
|
+
|
|
72
|
+
self.texture_idle = sdl2.SDL_CreateTextureFromSurface(
|
|
73
|
+
self.renderer.sdlrenderer,
|
|
74
|
+
sdl2.ext.load_img(
|
|
75
|
+
str(files("gogo_keyboard").joinpath("icons/gogo.png")),
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
self.texture_loop = [
|
|
79
|
+
sdl2.SDL_CreateTextureFromSurface(
|
|
80
|
+
self.renderer.sdlrenderer,
|
|
81
|
+
sdl2.ext.load_img(
|
|
82
|
+
str(files("gogo_keyboard").joinpath("icons/gogo_happy.png"))
|
|
83
|
+
),
|
|
84
|
+
),
|
|
85
|
+
sdl2.SDL_CreateTextureFromSurface(
|
|
86
|
+
self.renderer.sdlrenderer,
|
|
87
|
+
sdl2.ext.load_img(
|
|
88
|
+
str(files("gogo_keyboard").joinpath("icons/gogo_happy2.png"))
|
|
89
|
+
),
|
|
90
|
+
),
|
|
91
|
+
]
|
|
92
|
+
self.tex_ind: int = 0
|
|
93
|
+
self._draw()
|
|
94
|
+
self.renderer.present()
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def pressed_keys(self) -> Dict[int, Key]:
|
|
98
|
+
return copy.deepcopy(self._pressed_keys)
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def name(self) -> str:
|
|
102
|
+
return "Gorillinput"
|
|
103
|
+
|
|
104
|
+
def close(self):
|
|
105
|
+
self.window.close()
|
|
106
|
+
sdl2.ext.quit()
|
|
107
|
+
self._sdl_thread.cancel()
|
|
108
|
+
|
|
109
|
+
def _scancode_to_color(self, scancode):
|
|
110
|
+
return scancode_to_color(scancode)
|
|
111
|
+
|
|
112
|
+
def _init_sdl(self):
|
|
113
|
+
sdl2.ext.init()
|
|
114
|
+
self.window = sdl2.ext.Window(
|
|
115
|
+
"Input",
|
|
116
|
+
size=(150, 150),
|
|
117
|
+
# flags=sdl2.SDL_WINDOW_RESIZABLE, # icon disapears if used
|
|
118
|
+
)
|
|
119
|
+
sdl2.SDL_SetWindowIcon(self.window.window, self._surface_icon)
|
|
120
|
+
self.renderer = sdl2.ext.Renderer(self.window)
|
|
121
|
+
self.window.show()
|
|
122
|
+
self.texture_frame = sdl2.SDL_Rect(0, 0, 150, 150) # x, y, width, height
|
|
123
|
+
|
|
124
|
+
def _on_window_close(self):
|
|
125
|
+
self.close()
|
|
126
|
+
self.termination_callback()
|
|
127
|
+
|
|
128
|
+
def _draw(self):
|
|
129
|
+
self.renderer.color = (
|
|
130
|
+
self._scancode_to_color(list(self._pressed_keys.keys())[-1])
|
|
131
|
+
if len(self._pressed_keys) > 0
|
|
132
|
+
else self.idle_color
|
|
133
|
+
)
|
|
134
|
+
self.renderer.clear()
|
|
135
|
+
if len(self._pressed_keys) > 0 and self.texture_loop != []:
|
|
136
|
+
self.tex_ind = (self.tex_ind + 1) % len(self.texture_loop)
|
|
137
|
+
sdl2.SDL_RenderCopy(
|
|
138
|
+
self.renderer.sdlrenderer,
|
|
139
|
+
self.texture_loop[self.tex_ind],
|
|
140
|
+
None,
|
|
141
|
+
self.texture_frame,
|
|
142
|
+
)
|
|
143
|
+
else:
|
|
144
|
+
sdl2.SDL_RenderCopy(
|
|
145
|
+
self.renderer.sdlrenderer, self.texture_idle, None, self.texture_frame
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
async def _sdl_loop(self):
|
|
149
|
+
async for t in asyncio_for_robotics.Rate(30).listen():
|
|
150
|
+
events = sdl2.ext.get_events()
|
|
151
|
+
for e in events:
|
|
152
|
+
if e.type == sdl2.SDL_QUIT:
|
|
153
|
+
|
|
154
|
+
self._on_window_close()
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
elif e.type == sdl2.SDL_KEYDOWN:
|
|
158
|
+
if e.key.repeat:
|
|
159
|
+
continue
|
|
160
|
+
k = Key.from_sdl(e.key)
|
|
161
|
+
self.input_data(k)
|
|
162
|
+
self._pressed_keys[k.code] = k
|
|
163
|
+
|
|
164
|
+
elif e.type == sdl2.SDL_KEYUP:
|
|
165
|
+
k = Key.from_sdl(e.key)
|
|
166
|
+
self.input_data(k)
|
|
167
|
+
del self._pressed_keys[k.code]
|
|
168
|
+
|
|
169
|
+
elif e.type in [
|
|
170
|
+
sdl2.SDL_WINDOWEVENT,
|
|
171
|
+
]:
|
|
172
|
+
sdl2.SDL_SetWindowIcon(self.window.window, self._surface_icon)
|
|
173
|
+
self.window.show()
|
|
174
|
+
if e.window.event in [
|
|
175
|
+
sdl2.SDL_WINDOWEVENT_SIZE_CHANGED,
|
|
176
|
+
sdl2.SDL_WINDOWEVENT_RESIZED,
|
|
177
|
+
]:
|
|
178
|
+
pass # continues to update the window to new size
|
|
179
|
+
else:
|
|
180
|
+
continue # does nothing
|
|
181
|
+
else:
|
|
182
|
+
continue # does nothing
|
|
183
|
+
self._draw()
|
|
184
|
+
self.renderer.present()
|
gogo_keyboard/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""ROS2 node that publishes keyboard events as JSON-encoded std_msgs/String messages.
|
|
2
|
+
|
|
3
|
+
Run using:
|
|
4
|
+
- python3 -m gogo_keyboard.ros_node
|
|
5
|
+
- python3 -m gogo_keyboard.ros_node -t my_topic
|
|
6
|
+
- python3 -m gogo_keyboard.ros_node --topic my_topic
|
|
7
|
+
"""
|
|
8
|
+
import argparse
|
|
9
|
+
import asyncio
|
|
10
|
+
import json
|
|
11
|
+
from contextlib import suppress
|
|
12
|
+
from dataclasses import asdict
|
|
13
|
+
|
|
14
|
+
import rclpy
|
|
15
|
+
from rclpy.qos import DurabilityPolicy, HistoryPolicy, QoSProfile, ReliabilityPolicy
|
|
16
|
+
from std_msgs.msg import String
|
|
17
|
+
|
|
18
|
+
from gogo_keyboard.keyboard import Key, KeySub
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def make_ros_msg(key: Key) -> String:
|
|
22
|
+
"""Convert a Key object into a JSON-encoded ROS String message."""
|
|
23
|
+
dict_key = asdict(key)
|
|
24
|
+
del dict_key["sdl_event"]
|
|
25
|
+
return String(data=json.dumps(dict_key))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def async_main(topic: str = "key_press"):
|
|
29
|
+
"""Run the async keyboard listener and publish key events to a ROS2 topic.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
topic: ROS2 topic to publish key events to
|
|
33
|
+
"""
|
|
34
|
+
rclpy.init()
|
|
35
|
+
node = rclpy.create_node("gogo_keyboard")
|
|
36
|
+
pub = node.create_publisher(
|
|
37
|
+
String,
|
|
38
|
+
topic,
|
|
39
|
+
QoSProfile( # no message lost (hopefuly)
|
|
40
|
+
reliability=ReliabilityPolicy.RELIABLE,
|
|
41
|
+
history=HistoryPolicy.KEEP_ALL,
|
|
42
|
+
durability=DurabilityPolicy.VOLATILE,
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
key_sub = KeySub()
|
|
46
|
+
print(
|
|
47
|
+
f"🦍🦍_keyboard publishing onto `{node.resolve_topic_name(pub.topic)}`. \nListen using `ros2 topic echo {node.resolve_topic_name(pub.topic)}`"
|
|
48
|
+
)
|
|
49
|
+
try:
|
|
50
|
+
async for key in key_sub.listen_reliable():
|
|
51
|
+
msg = make_ros_msg(key)
|
|
52
|
+
pub.publish(msg)
|
|
53
|
+
finally:
|
|
54
|
+
print(f"gogo_keyboard exiting.")
|
|
55
|
+
key_sub.close()
|
|
56
|
+
rclpy.shutdown()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main():
|
|
60
|
+
parser = argparse.ArgumentParser()
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"-t",
|
|
63
|
+
"--topic",
|
|
64
|
+
default="key_press",
|
|
65
|
+
help="ROS2 topic to publish key events to",
|
|
66
|
+
)
|
|
67
|
+
args = parser.parse_args()
|
|
68
|
+
with suppress(
|
|
69
|
+
asyncio.CancelledError, KeyboardInterrupt, rclpy._rclpy_pybind11.RCLError
|
|
70
|
+
):
|
|
71
|
+
asyncio.run(async_main(args.topic))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
main()
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Zenoh publisher that publishes keyboard events as JSON-encoded messages.
|
|
2
|
+
|
|
3
|
+
Run using:
|
|
4
|
+
- python3 -m gogo_keyboard.zenoh_node
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from contextlib import suppress
|
|
12
|
+
from dataclasses import asdict
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
import zenoh
|
|
16
|
+
|
|
17
|
+
from gogo_keyboard.keyboard import Key, KeySub
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def make_msg(key: Key) -> str:
|
|
21
|
+
"""Convert a Key object into a JSON-encoded message."""
|
|
22
|
+
dict_key = asdict(key)
|
|
23
|
+
del dict_key["sdl_event"]
|
|
24
|
+
return json.dumps(dict_key)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def async_main(topic: str = "key_press", config_path: Optional[str] = None):
|
|
28
|
+
"""Run the async keyboard listener and publish key events to a zenoh key_expr.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
topic: key_expr to publish key events to
|
|
32
|
+
"""
|
|
33
|
+
if config_path is None:
|
|
34
|
+
config = zenoh.Config()
|
|
35
|
+
else:
|
|
36
|
+
filepath = os.path.expanduser(config_path)
|
|
37
|
+
config = zenoh.Config.from_file(filepath)
|
|
38
|
+
with zenoh.open(config) as ses:
|
|
39
|
+
pub = ses.declare_publisher(topic, reliability=zenoh.Reliability.RELIABLE)
|
|
40
|
+
key_sub = KeySub()
|
|
41
|
+
print(
|
|
42
|
+
f"🦍🦍_keyboard publishing onto `{pub.key_expr}`. \nListen using `python3 -m gogo_keyboard._zenoh_simple_listener`"
|
|
43
|
+
)
|
|
44
|
+
try:
|
|
45
|
+
async for key in key_sub.listen_reliable():
|
|
46
|
+
msg = make_msg(key)
|
|
47
|
+
pub.put(msg)
|
|
48
|
+
finally:
|
|
49
|
+
print(f"gogo_keyboard exiting.")
|
|
50
|
+
key_sub.close()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main():
|
|
54
|
+
parser = argparse.ArgumentParser()
|
|
55
|
+
parser.add_argument(
|
|
56
|
+
"-c",
|
|
57
|
+
"--config",
|
|
58
|
+
dest="config",
|
|
59
|
+
default=None,
|
|
60
|
+
help="Zenoh config path",
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"-t",
|
|
64
|
+
"--topic",
|
|
65
|
+
dest="topic",
|
|
66
|
+
default="key_press",
|
|
67
|
+
help="ROS2 topic to publish key events to",
|
|
68
|
+
)
|
|
69
|
+
args = parser.parse_args()
|
|
70
|
+
with suppress(asyncio.CancelledError, KeyboardInterrupt):
|
|
71
|
+
asyncio.run(async_main(args.topic, args.config))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
main()
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gogo_keyboard
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Press keyboard 🦍 Get key 🦍 Python Asyncio for Robotics
|
|
5
|
+
Author-email: Elian NEPPEL <elian.dev@posteo.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/2lian/gogo_keyboard
|
|
8
|
+
Project-URL: Repository, https://github.com/2lian/gogo_keyboard
|
|
9
|
+
Project-URL: Issues, https://github.com/2lian/gogo_keyboard/issues
|
|
10
|
+
Keywords: asyncio,robotics,ros2,zenoh,publisher,subscriber
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Natural Language :: English
|
|
15
|
+
Classifier: Environment :: Console
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
23
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
24
|
+
Classifier: Operating System :: MacOS
|
|
25
|
+
Classifier: Topic :: Scientific/Engineering
|
|
26
|
+
Classifier: Framework :: AsyncIO
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: asyncio_for_robotics>=1.0.0
|
|
31
|
+
Requires-Dist: pysdl2>=0.9.0
|
|
32
|
+
Provides-Extra: dll
|
|
33
|
+
Requires-Dist: pysdl2-dll; extra == "dll"
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: colorama; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest; extra == "dev"
|
|
37
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
38
|
+
Provides-Extra: build
|
|
39
|
+
Requires-Dist: build; extra == "build"
|
|
40
|
+
Requires-Dist: twine; extra == "build"
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
# Gogo Keyboard
|
|
44
|
+
## Press keyboard 🦍 Get key 🦍 Unga Bunga
|
|
45
|
+
|
|
46
|
+
| Requirements | Compatibility |
|
|
47
|
+
|---|---|
|
|
48
|
+
| [](https://pypi.org/project/asyncio_for_robotics/)<br><br>[](https://opensource.org/license/mit)|  <br>[](https://github.com/ros2) <br>[](https://zenoh.io/) |
|
|
49
|
+
|
|
50
|
+
Python Asyncio library to simply get keyboard presses and releases. Gogo Keyboard creates a new independent SDL2 window that captures the key events.
|
|
51
|
+
|
|
52
|
+
```python3
|
|
53
|
+
pip install https://github.com/2lian/gogo_keyboard.git[dll]
|
|
54
|
+
python3 -m gogo_keyboard.example
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Motivation:
|
|
58
|
+
- Keyboard presses and releases in Asyncio.
|
|
59
|
+
- Only when clicking on the Gorilla window.
|
|
60
|
+
- Python terminal is free for other tasks.
|
|
61
|
+
- Based on [`asyncio_for_robotics`](https://github.com/2lian/asyncio-for-robotics) for seamless compatibility with:
|
|
62
|
+
- ROS 2
|
|
63
|
+
- Zenoh
|
|
64
|
+
- More
|
|
65
|
+
|
|
66
|
+
|  |  |  |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
|
|
69
|
+
## Installation
|
|
70
|
+
|
|
71
|
+
This library requires `sdl2` and `sdl2_image`. By specifying the `[dll]` optional dependency, those will be installed by pip.
|
|
72
|
+
|
|
73
|
+
```python3
|
|
74
|
+
pip install gogo_keyboard[dll]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Conda pacakge: soon!
|
|
78
|
+
|
|
79
|
+
## Python Example
|
|
80
|
+
|
|
81
|
+
Example is [provided here](./src/gogo_keyboard/example.py) and can be run with `python3 -m gogo_keyboard.example`.
|
|
82
|
+
|
|
83
|
+
Here is a minimal piece of working code:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
import asyncio
|
|
87
|
+
from gogo_keyboard.keyboard import KeySub
|
|
88
|
+
|
|
89
|
+
async def async_main():
|
|
90
|
+
key_sub = KeySub()
|
|
91
|
+
async for key in key_sub.listen_reliable():
|
|
92
|
+
print(key)
|
|
93
|
+
|
|
94
|
+
asyncio.run(async_main())
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## ROS 2 Example (Humble, Jazzy, Kilted)
|
|
98
|
+
|
|
99
|
+
A very simple ROS 2 node is [provided here](./src/gogo_keyboard/ros_node.py), run it with `python3 -m gogo_keyboard.ros_node`. The messages format is a `json` formatted `String`, 🦍 simple 🦍 Unga Bunga.
|
|
100
|
+
|
|
101
|
+
## Zenoh Example
|
|
102
|
+
|
|
103
|
+
A very simple Zenoh publisher is [provided here](./src/gogo_keyboard/zenoh_node.py), run it with `python3 -m gogo_keyboard.zenoh_node`. The messages format is a `json` formatted `String`, 🦍 simple 🦍 Unga Bunga.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
gogo_keyboard/__init__.py,sha256=4W8VliAYUP1KY2gLJ_YDy2TmcXYVm-PY7XikQD_bFwA,2
|
|
2
|
+
gogo_keyboard/_zenoh_simple_listener.py,sha256=ALLACoGxLugo7ICrEkZwaH_xsynmZ_XIyQyxoS1TcHY,1303
|
|
3
|
+
gogo_keyboard/codes.py,sha256=GDGXKh6D0wPVsrFM8UglDkCrjH7ayBki484X_MhDQJs,4465
|
|
4
|
+
gogo_keyboard/example.py,sha256=m8KyUvp1Bz6avF3nQwI23bwvyfvSWZPNzH19V9UUwqk,1539
|
|
5
|
+
gogo_keyboard/keyboard.py,sha256=Fj0FjPNW2rD3-hde5-q-6srCvO3suGd_PP9ml0Ezm4A,6048
|
|
6
|
+
gogo_keyboard/mini_example.py,sha256=1zWGsAVTrHgPwUQYGV4anN9O7x4nriOvUbagO2SC7LM,199
|
|
7
|
+
gogo_keyboard/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
gogo_keyboard/ros_node.py,sha256=6_x3QAmEvsxYwBTxgXZzc6etdRjl2jxkpCpQXiXR_5U,2133
|
|
9
|
+
gogo_keyboard/zenoh_node.py,sha256=DTk014KQGkwFlNcnzRPYdESjqBEMybPHvUVYPs4-pCo,2011
|
|
10
|
+
gogo_keyboard/icons/gogo.png,sha256=Csa0zE_0SNgSGvBqBKAyzJ8LT-abBTCldKFrUgL7Z08,918
|
|
11
|
+
gogo_keyboard/icons/gogo_happy.png,sha256=LywojJg3pYiTtdLJhFPHehhqgQl3V5GBX64Mvnkgkxo,940
|
|
12
|
+
gogo_keyboard/icons/gogo_happy2.png,sha256=uNG6elJSZAkSDSW4RRTjgCZWajJnZZ3IpI53jmmFkLQ,1113
|
|
13
|
+
gogo_keyboard-0.0.0.dist-info/licenses/LICENSE,sha256=mLZSGzYp72Ys0WWGIGWxPamHm1loDM6dDtG-4nQOI-0,1069
|
|
14
|
+
gogo_keyboard-0.0.0.dist-info/METADATA,sha256=Jb3awk5-ZvKXcXKJjV8y3TgG3fseD75C_FH_5exByZ4,4238
|
|
15
|
+
gogo_keyboard-0.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
16
|
+
gogo_keyboard-0.0.0.dist-info/top_level.txt,sha256=RC5VsKo6kb70BmF7AIkFPB7mguExOHb5QAMMZNlxEk0,14
|
|
17
|
+
gogo_keyboard-0.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Elian NEPPEL
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
gogo_keyboard
|