ホットキーとかショートカットキーとかキーコンビネーションとか、キーボードのいくつかのキーが同時に押さたことをバックグラウンドで検知する方法です。先に自分で作らずともサードパーティ製モジュールがあることをお知らせしておきます。このほかにも同じような機能のモジュールがいくつかあるとおもいますが、困ったことに執筆時点でキーボードの抜き差しにうまく対応できるものが見つかりませんでした。
さて、前回の記事で入力デバイスイベントファイルが作られることを監視してキーボードの抜き差しを検出していましたが、今回はその中身を解析してキーイベントを検出します。
まずは、キーボードのイベントファイルの中身を眺めてみましょう。指定するイベントファイルevent0は環境によって名前が違うかもしれません。コマンドを実行して適当なキーを押す、とつらつら呪文が流れてきます。
$ od -tx1z /dev/input/event0 0000000 1c 71 e7 5d 65 45 0d 00 04 00 04 00 14 00 07 00 >.q.]eE..........< 0000020 1c 71 e7 5d 65 45 0d 00 01 00 10 00 01 00 00 00 >.q.]eE..........< 0000040 1c 71 e7 5d 65 45 0d 00 00 00 00 00 00 00 00 00 >.q.]eE..........< 0000060 1c 71 e7 5d de 7d 0e 00 04 00 04 00 14 00 07 00 >.q.].}..........< 0000100 1c 71 e7 5d de 7d 0e 00 01 00 10 00 00 00 00 00 >.q.].}..........<
パッと見わかりませんが、各イベントはPythonで書くとデータ構造をしています。
class InputEvent(Structure):
_fields_ = (
('time', TimeVal),
('type', c_uint16),
('code', c_uint16),
('value', c_int32),
)
そして、入れ子のTimeValはCでおなじみの固定小数点の時刻構造体です。
class TimeVal(Structure):
_fields_ = (
("tv_sec", c_long),
("tv_usec", c_long),
)
このInputEvent構造体のtypeが0x01のときにキーボードイベント、valueが0ならキーアップ、1ならキーダウン、2なら長押しです。そしてcodeがキーコード。Linuxのキーコードの定義は /usr/include/linux/input-event-codes.h に書いてあります。
これを解析すると、どのキーが押されていて、どのキーが離されたかがわかります。以下実装例です。長いのはキーコードの定義で、やっていることは後半の40行くらいです。
#
# Event types
#
types = {
0x01 : 'KEY'
}
#
# Keys and buttons
#
keys = {
0 : 'RESERVED',
1 : 'ESC',
2 : '1',
3 : '2',
4 : '3',
5 : '4',
6 : '5',
7 : '6',
8 : '7',
9 : '8',
10 : '9',
11 : '0',
12 : 'MINUS',
13 : 'EQUAL',
14 : 'BACKSPACE',
15 : 'TAB',
16 : 'Q',
17 : 'W',
18 : 'E',
19 : 'R',
20 : 'T',
21 : 'Y',
22 : 'U',
23 : 'I',
24 : 'O',
25 : 'P',
26 : 'LEFTBRACE',
27 : 'RIGHTBRACE',
28 : 'ENTER',
29 : 'LEFTCTRL',
30 : 'A',
31 : 'S',
32 : 'D',
33 : 'F',
34 : 'G',
35 : 'H',
36 : 'J',
37 : 'K',
38 : 'L',
39 : 'SEMICOLON',
40 : 'APOSTROPHE',
41 : 'GRAVE',
42 : 'LEFTSHIFT',
43 : 'BACKSLASH',
44 : 'Z',
45 : 'X',
46 : 'C',
47 : 'V',
48 : 'B',
49 : 'N',
50 : 'M',
51 : 'COMMA',
52 : 'DOT',
53 : 'SLASH',
54 : 'RIGHTSHIFT',
55 : 'KPASTERISK',
56 : 'LEFTALT',
57 : 'SPACE',
58 : 'CAPSLOCK',
59 : 'F1',
60 : 'F2',
61 : 'F3',
62 : 'F4',
63 : 'F5',
64 : 'F6',
65 : 'F7',
66 : 'F8',
67 : 'F9',
68 : 'F10',
69 : 'NUMLOCK',
70 : 'SCROLLLOCK',
71 : 'KP7',
72 : 'KP8',
73 : 'KP9',
74 : 'KPMINUS',
75 : 'KP4',
76 : 'KP5',
77 : 'KP6',
78 : 'KPPLUS',
79 : 'KP1',
80 : 'KP2',
81 : 'KP3',
82 : 'KP0',
83 : 'KPDOT',
85 : 'ZENKAKUHANKAKU',
86 : '102ND',
87 : 'F11',
88 : 'F12',
89 : 'RO',
90 : 'KATAKANA',
91 : 'HIRAGANA',
92 : 'HENKAN',
93 : 'KATAKANAHIRAGANA',
94 : 'MUHENKAN',
95 : 'KPJPCOMMA',
96 : 'KPENTER',
97 : 'RIGHTCTRL',
98 : 'KPSLASH',
99 : 'SYSRQ',
100 : 'RIGHTALT',
101 : 'LINEFEED',
102 : 'HOME',
103 : 'UP',
104 : 'PAGEUP',
105 : 'LEFT',
106 : 'RIGHT',
107 : 'END',
108 : 'DOWN',
109 : 'PAGEDOWN',
110 : 'INSERT',
111 : 'DELETE',
112 : 'MACRO',
113 : 'MUTE',
114 : 'VOLUMEDOWN',
115 : 'VOLUMEUP',
116 : 'POWER', # SC System Power Down
117 : 'KPEQUAL',
118 : 'KPPLUSMINUS',
119 : 'PAUSE',
120 : 'SCALE', # AL Compiz Scale (Expose)
121 : 'KPCOMMA',
122 : 'HANGEUL',
123 : 'HANJA',
124 : 'YEN',
125 : 'LEFTMETA',
126 : 'RIGHTMETA',
127 : 'COMPOSE',
128 : 'STOP', # AC Stop
129 : 'AGAIN',
130 : 'PROPS', # AC Properties
131 : 'UNDO', # AC Undo
132 : 'FRONT',
133 : 'COPY', # AC Copy
134 : 'OPEN', # AC Open
135 : 'PASTE', # AC Paste
136 : 'FIND', # AC Search
137 : 'CUT', # AC Cut
138 : 'HELP', # AL Integrated Help Center
139 : 'MENU', # Menu (show menu)
140 : 'CALC', # AL Calculator
141 : 'SETUP',
142 : 'SLEEP', # SC System Sleep
143 : 'WAKEUP', # System Wake Up
144 : 'FILE', # AL Local Machine Browser
145 : 'SENDFILE',
146 : 'DELETEFILE',
147 : 'XFER',
148 : 'PROG1',
149 : 'PROG2',
150 : 'WWW', # AL Internet Browser
151 : 'MSDOS',
152 : 'SCREENLOCK', # AL Terminal Lock/Screensaver
153 : 'DIRECTION',
154 : 'CYCLEWINDOWS',
155 : 'MAIL',
156 : 'BOOKMARKS', # AC Bookmarks
157 : 'COMPUTER',
158 : 'BACK', # AC Back
159 : 'FORWARD', # AC Forward
160 : 'CLOSECD',
161 : 'EJECTCD',
162 : 'EJECTCLOSECD',
163 : 'NEXTSONG',
164 : 'PLAYPAUSE',
165 : 'PREVIOUSSONG',
166 : 'STOPCD',
167 : 'RECORD',
168 : 'REWIND',
169 : 'PHONE', # Media Select Telephone
170 : 'ISO',
171 : 'CONFIG', # AL Consumer Control Configuration
172 : 'HOMEPAGE', # AC Home
173 : 'REFRESH', # AC Refresh
174 : 'EXIT', # AC Exit
175 : 'MOVE',
176 : 'EDIT',
177 : 'SCROLLUP',
178 : 'SCROLLDOWN',
179 : 'KPLEFTPAREN',
180 : 'KPRIGHTPAREN',
181 : 'NEW', # AC New
182 : 'REDO', # AC Redo/Repeat
183 : 'F13',
184 : 'F14',
185 : 'F15',
186 : 'F16',
187 : 'F17',
188 : 'F18',
189 : 'F19',
190 : 'F20',
191 : 'F21',
192 : 'F22',
193 : 'F23',
194 : 'F24',
200 : 'PLAYCD',
201 : 'PAUSECD',
202 : 'PROG3',
203 : 'PROG4',
204 : 'DASHBOARD', # AL Dashboard
205 : 'SUSPEND',
206 : 'CLOSE', # AC Close
207 : 'PLAY',
208 : 'FASTFORWARD',
209 : 'BASSBOOST',
210 : 'PRINT', # AC Print
211 : 'HP',
212 : 'CAMERA',
213 : 'SOUND',
214 : 'QUESTION',
215 : 'EMAIL',
216 : 'CHAT',
217 : 'SEARCH',
218 : 'CONNECT',
219 : 'FINANCE', # AL Checkbook/Finance
220 : 'SPORT',
221 : 'SHOP',
222 : 'ALTERASE',
223 : 'CANCEL', # AC Cancel
224 : 'BRIGHTNESSDOWN',
225 : 'BRIGHTNESSUP',
226 : 'MEDIA',
227 : 'SWITCHVIDEOMODE', # Cycle between available video
# outputs (Monitor/LCD/TV-out/etc)
228 : 'KBDILLUMTOGGLE',
229 : 'KBDILLUMDOWN',
230 : 'KBDILLUMUP',
231 : 'SEND', # AC Send
232 : 'REPLY', # AC Reply
233 : 'FORWARDMAIL', # AC Forward Msg
234 : 'SAVE', # AC Save
235 : 'DOCUMENTS',
236 : 'BATTERY',
237 : 'BLUETOOTH',
238 : 'WLAN',
239 : 'UWB',
240 : 'UNKNOWN',
241 : 'VIDEO_NEXT', # drive next video source
242 : 'VIDEO_PREV', # drive previous video source
243 : 'BRIGHTNESS_CYCLE', # brightness up, after max is min
244 : 'BRIGHTNESS_ZERO', # brightness off, use ambient
245 : 'DISPLAY_OFF', # display device to off state
246 : 'WIMAX',
}
# Range 248 - 255 is reserved for special needs of AT keyboard driver
actions = {
0 : 'UP',
1 : 'DOWN',
2 : 'HELD',
}
#############
import sys
import io
from ctypes import Structure, sizeof, c_long, c_int32, c_uint16
class TimeVal(Structure):
_fields_ = (
("tv_sec", c_long),
("tv_usec", c_long),
)
class InputEvent(Structure):
_fields_ = (
('time', TimeVal),
('type', c_uint16),
('code', c_uint16),
('value', c_int32),
)
event_size = sizeof(InputEvent)
# イベントを読み込んで構造体を作って返す関数
def get_input_event_struct(f):
ie = io.BytesIO(f.read(event_size))
struct_ie = InputEvent()
ie.readinto(struct_ie)
return struct_ie
# 押されているキーを格納する集合
pressed_keys = set()
# ホットキーを検知する関数
def detect_hotkey(hotkeys_dict, key, action):
global pressed_keys
# キーが放されたら集合から消す
if action == 'UP':
pressed_keys.remove(key)
# キーが押されたら集合に加える
elif action == 'DOWN':
pressed_keys.add(key)
else: # 長押しは無視
return None
# 押されているキーがホットキーとマッチするか確認
for message, hotkey in hotkeys_dict.items():
if hotkey == pressed_keys:
# マッチしていたら対応するメッセージを返す
return message
return None
if __name__ == '__main__':
# 入力デバイスイベントファイル
dev_input_file = '/dev/input/event0'
# メッセージに対応するホットキー集合のマッピング
hotkeys_dict = {'A kind of magic.' : {'F12'},
'Come on!' : {'ESC'},
'Powerrrr!!' : {'LEFTCTRL', 'E'},
'God mode...' : {'SPACE', 'G', 'O', 'D'}}
print('Hotkeys:', list(hotkeys_dict.values()))
# イベントファイルをオープン
with open(dev_input_file, mode='rb', buffering=1) as f:
# 無限ループで監視し続けること
while True:
# バイト列を構造体に入れる
struct_ie = get_input_event_struct(f)
# キーイベントかどうかの判定
if struct_ie.type == 0x01: # key event type
# キーコードからキー名に変換
key = keys.get(struct_ie.code)
if key is None:
continue
# バリューからアクション名に変換
action = actions.get(struct_ie.value)
if action is None:
continue
# ホットキー検出関数に渡す
message = detect_hotkey(hotkeys_dict, key, action)
if message is not None:
print(message)
これをPython3で実行し、書いてあるホットキーを押して見ると…
$ python3 key_parser.py
Hotkeys: [{'F12'}, {'ESC'}, {'E', 'LEFTCTRL'}, {'O', 'G', 'SPACE', 'D'}]
A kind of magic.
Come on!
Powerrrr!!
God mode...
ね、かんたんでしょ。
ちなみにキーボードが抜かれると例外が飛んでくるので、それをハンドリングすれば前回の記事の内容と組み合わせて、キーボードが差されるまで待つことができます。
Traceback (most recent call last):
File "./key_parser.py", line 338, in <module>
struct_ie = event_parser(f)
File "./key_parser.py", line 304, in event_parser
ie = io.BytesIO(f.read(event_size))
OSError: [Errno 19] No such device
