Skip to content

Commit 756f5eb

Browse files
authored
Merge pull request #84 from Neradoc/layout-base
Add keyboard_layout_base and switch keyboard_layout_us to it
2 parents c3c0591 + cea6164 commit 756f5eb

File tree

3 files changed

+192
-80
lines changed

3 files changed

+192
-80
lines changed

adafruit_hid/keyboard_layout_base.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
# SPDX-FileCopyrightText: 2017 Dan Halbert for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
"""
6+
`adafruit_hid.keyboard_layout_base.KeyboardLayoutBase`
7+
=======================================================
8+
9+
* Author(s): Dan Halbert, AngainorDev, Neradoc
10+
"""
11+
12+
13+
try:
14+
from typing import Tuple
15+
from .keyboard import Keyboard
16+
except ImportError:
17+
pass
18+
19+
20+
__version__ = "0.0.0-auto.0"
21+
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_HID.git"
22+
23+
24+
class KeyboardLayoutBase:
25+
"""Base class for keyboard layouts. Uses the tables defined in the subclass
26+
to map UTF-8 characters to appropriate keypresses.
27+
28+
Non-supported characters and most control characters will raise an exception.
29+
"""
30+
31+
SHIFT_FLAG = 0x80
32+
"""Bit set in any keycode byte if the shift key is required for the character."""
33+
ALTGR_FLAG = 0x80
34+
"""Bit set in the combined keys table if altgr is required for the first key."""
35+
SHIFT_CODE = 0xE1
36+
"""The SHIFT keycode, to avoid dependency to the Keycode class."""
37+
RIGHT_ALT_CODE = 0xE6
38+
"""The ALTGR keycode, to avoid dependency to the Keycode class."""
39+
ASCII_TO_KEYCODE = ()
40+
"""Bytes string of keycodes for low ASCII characters, indexed by the ASCII value.
41+
Keycodes use the `SHIFT_FLAG` if needed.
42+
Dead keys are excluded by assigning the keycode 0."""
43+
HIGHER_ASCII = {}
44+
"""Dictionary that associates the ord() int value of high ascii and utf8 characters
45+
to their keycode. Keycodes use the `SHIFT_FLAG` if needed."""
46+
NEED_ALTGR = ""
47+
"""Characters in `ASCII_TO_KEYCODE` and `HIGHER_ASCII` that need
48+
the ALTGR key pressed to type."""
49+
COMBINED_KEYS = {}
50+
"""
51+
Dictionary of characters (indexed by ord() value) that can be accessed by typing first
52+
a dead key followed by a regular key, like ``ñ`` as ``~ + n``. The value is a 2-bytes int:
53+
the high byte is the dead-key keycode (including SHIFT_FLAG), the low byte is the ascii code
54+
of the second character, with ALTGR_FLAG set if the dead key (the first key) needs ALTGR.
55+
56+
The combined-key codes bits are: ``0b SDDD DDDD AKKK KKKK``:
57+
``S`` is the shift flag for the **first** key,
58+
``DDD DDDD`` is the keycode for the **first** key,
59+
``A`` is the altgr flag for the **first** key,
60+
``KKK KKKK`` is the (low) ASCII code for the second character.
61+
"""
62+
63+
def __init__(self, keyboard: Keyboard) -> None:
64+
"""Specify the layout for the given keyboard.
65+
66+
:param keyboard: a Keyboard object. Write characters to this keyboard when requested.
67+
68+
Example::
69+
70+
kbd = Keyboard(usb_hid.devices)
71+
layout = KeyboardLayout(kbd)
72+
"""
73+
self.keyboard = keyboard
74+
75+
def _write(self, keycode: int, altgr: bool = False) -> None:
76+
"""Type a key combination based on shift bit and altgr bool
77+
78+
:param keycode: int value of the keycode, with the shift bit.
79+
:param altgr: bool indicating if the altgr key should be pressed too.
80+
"""
81+
# Add altgr modifier if needed
82+
if altgr:
83+
self.keyboard.press(self.RIGHT_ALT_CODE)
84+
# If this is a shifted char, clear the SHIFT flag and press the SHIFT key.
85+
if keycode & self.SHIFT_FLAG:
86+
keycode &= ~self.SHIFT_FLAG
87+
self.keyboard.press(self.SHIFT_CODE)
88+
self.keyboard.press(keycode)
89+
self.keyboard.release_all()
90+
91+
def write(self, string: str) -> None:
92+
"""Type the string by pressing and releasing keys on my keyboard.
93+
94+
:param string: A string of UTF-8 characters to convert to key presses and send.
95+
:raises ValueError: if any of the characters has no keycode
96+
(such as some control characters).
97+
98+
Example::
99+
100+
# Write abc followed by Enter to the keyboard
101+
layout.write('abc\\n')
102+
"""
103+
for char in string:
104+
# find easy ones first
105+
keycode = self._char_to_keycode(char)
106+
if keycode > 0:
107+
self._write(keycode, char in self.NEED_ALTGR)
108+
# find combined keys
109+
elif ord(char) in self.COMBINED_KEYS:
110+
# first key (including shift bit)
111+
cchar = self.COMBINED_KEYS[ord(char)]
112+
self._write(cchar >> 8, cchar & self.ALTGR_FLAG)
113+
# second key (removing the altgr bit)
114+
char = chr(cchar & 0xFF & (~self.ALTGR_FLAG))
115+
keycode = self._char_to_keycode(char)
116+
# assume no altgr needed for second key
117+
self._write(keycode, False)
118+
else:
119+
raise ValueError(
120+
"No keycode available for character {letter} ({num}/0x{num:02x}).".format(
121+
letter=repr(char), num=ord(char)
122+
)
123+
)
124+
125+
def keycodes(self, char: str) -> Tuple[int, ...]:
126+
"""Return a tuple of keycodes needed to type the given character.
127+
128+
:param char: A single UTF8 character in a string.
129+
:type char: str of length one.
130+
:returns: tuple of Keycode keycodes.
131+
:raises ValueError: if there is no keycode for ``char``.
132+
133+
Examples::
134+
135+
# Returns (Keycode.TAB,)
136+
keycodes('\t')
137+
# Returns (Keycode.A,)
138+
keycode('a')
139+
# Returns (Keycode.SHIFT, Keycode.A)
140+
keycode('A')
141+
# Raises ValueError with a US layout because it's an unknown character
142+
keycode('é')
143+
"""
144+
keycode = self._char_to_keycode(char)
145+
if keycode == 0:
146+
raise ValueError(
147+
"No keycode available for character {letter} ({num}/0x{num:02x}).".format(
148+
letter=repr(char), num=ord(char)
149+
)
150+
)
151+
152+
codes = []
153+
if char in self.NEED_ALTGR:
154+
codes.append(self.RIGHT_ALT_CODE)
155+
if keycode & self.SHIFT_FLAG:
156+
codes.extend((self.SHIFT_CODE, keycode & ~self.SHIFT_FLAG))
157+
else:
158+
codes.append(keycode)
159+
160+
return codes
161+
162+
def _above128char_to_keycode(self, char: str) -> int:
163+
"""Return keycode for above 128 utf8 codes.
164+
165+
A character can be indexed by the char itself or its int ord() value.
166+
167+
:param char_val: char value
168+
:return: keycode, with modifiers if needed
169+
"""
170+
if ord(char) in self.HIGHER_ASCII:
171+
return self.HIGHER_ASCII[ord(char)]
172+
if char in self.HIGHER_ASCII:
173+
return self.HIGHER_ASCII[char]
174+
return 0
175+
176+
def _char_to_keycode(self, char: str) -> int:
177+
"""Return the HID keycode for the given character, with the SHIFT_FLAG possibly set.
178+
179+
If the character requires pressing the Shift key, the SHIFT_FLAG bit is set.
180+
You must clear this bit before passing the keycode in a USB report.
181+
"""
182+
char_val = ord(char)
183+
if char_val > len(self.ASCII_TO_KEYCODE):
184+
return self._above128char_to_keycode(char)
185+
keycode = self.ASCII_TO_KEYCODE[char_val]
186+
return keycode

adafruit_hid/keyboard_layout_us.py

100644100755
Lines changed: 3 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,10 @@
99
* Author(s): Dan Halbert
1010
"""
1111

12-
from .keycode import Keycode
12+
from .keyboard_layout_base import KeyboardLayoutBase
1313

14-
try:
15-
from .keyboard import Keyboard
16-
from typing import Tuple
17-
except ImportError:
18-
pass
1914

20-
21-
class KeyboardLayoutUS:
15+
class KeyboardLayoutUS(KeyboardLayoutBase):
2216
"""Map ASCII characters to appropriate keypresses on a standard US PC keyboard.
2317
2418
Non-ASCII characters and most control characters will raise an exception.
@@ -37,7 +31,6 @@ class KeyboardLayoutUS:
3731
# if it's in a .mpy file, so it doesn't use up valuable RAM.
3832
#
3933
# \x00 entries have no keyboard key and so won't be sent.
40-
SHIFT_FLAG = 0x80
4134
ASCII_TO_KEYCODE = (
4235
b"\x00" # NUL
4336
b"\x00" # SOH
@@ -169,75 +162,5 @@ class KeyboardLayoutUS:
169162
b"\x4c" # DEL DELETE (called Forward Delete in usb.org document)
170163
)
171164

172-
def __init__(self, keyboard: Keyboard) -> None:
173-
"""Specify the layout for the given keyboard.
174-
175-
:param keyboard: a Keyboard object. Write characters to this keyboard when requested.
176-
177-
Example::
178-
179-
kbd = Keyboard(usb_hid.devices)
180-
layout = KeyboardLayoutUS(kbd)
181-
"""
182-
183-
self.keyboard = keyboard
184-
185-
def write(self, string: str) -> None:
186-
"""Type the string by pressing and releasing keys on my keyboard.
187-
188-
:param string: A string of ASCII characters.
189-
:raises ValueError: if any of the characters are not ASCII or have no keycode
190-
(such as some control characters).
191-
192-
Example::
193-
194-
# Write abc followed by Enter to the keyboard
195-
layout.write('abc\\n')
196-
"""
197-
for char in string:
198-
keycode = self._char_to_keycode(char)
199-
# If this is a shifted char, clear the SHIFT flag and press the SHIFT key.
200-
if keycode & self.SHIFT_FLAG:
201-
keycode &= ~self.SHIFT_FLAG
202-
self.keyboard.press(Keycode.SHIFT)
203-
self.keyboard.press(keycode)
204-
self.keyboard.release_all()
205-
206-
def keycodes(self, char: str) -> Tuple[int, ...]:
207-
"""Return a tuple of keycodes needed to type the given character.
208-
209-
:param char: A single ASCII character in a string.
210-
:type char: str of length one.
211-
:returns: tuple of Keycode keycodes.
212-
:raises ValueError: if ``char`` is not ASCII or there is no keycode for it.
213-
214-
Examples::
215-
216-
# Returns (Keycode.TAB,)
217-
keycodes('\t')
218-
# Returns (Keycode.A,)
219-
keycode('a')
220-
# Returns (Keycode.SHIFT, Keycode.A)
221-
keycode('A')
222-
# Raises ValueError because it's a accented e and is not ASCII
223-
keycode('é')
224-
"""
225-
keycode = self._char_to_keycode(char)
226-
if keycode & self.SHIFT_FLAG:
227-
return (Keycode.SHIFT, keycode & ~self.SHIFT_FLAG)
228-
229-
return (keycode,)
230-
231-
def _char_to_keycode(self, char: str) -> int:
232-
"""Return the HID keycode for the given ASCII character, with the SHIFT_FLAG possibly set.
233165

234-
If the character requires pressing the Shift key, the SHIFT_FLAG bit is set.
235-
You must clear this bit before passing the keycode in a USB report.
236-
"""
237-
char_val = ord(char)
238-
if char_val > 128:
239-
raise ValueError("Not an ASCII character.")
240-
keycode = self.ASCII_TO_KEYCODE[char_val]
241-
if keycode == 0:
242-
raise ValueError("No keycode available for character.")
243-
return keycode
166+
KeyboardLayout = KeyboardLayoutUS

docs/api.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
.. automodule:: adafruit_hid.keyboard_layout_us
1111
:members:
1212

13+
.. automodule:: adafruit_hid.keyboard_layout_base
14+
:members:
15+
1316
.. automodule:: adafruit_hid.mouse
1417
:members:
1518

0 commit comments

Comments
 (0)