ホットキーとかショートカットキーとかキーコンビネーションとか、キーボードのいくつかのキーが同時に押さたことをバックグラウンドで検知する方法です。先に自分で作らずともサードパーティ製モジュールがあることをお知らせしておきます。このほかにも同じような機能のモジュールがいくつかあるとおもいますが、困ったことに執筆時点でキーボードの抜き差しにうまく対応できるものが見つかりませんでした。
さて、前回の記事で入力デバイスイベントファイルが作られることを監視してキーボードの抜き差しを検出していましたが、今回はその中身を解析してキーイベントを検出します。
まずは、キーボードのイベントファイルの中身を眺めてみましょう。指定するイベントファイル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