Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Linux用キーリマッパーを作る技術 / How to make Key Remapper

Linux用キーリマッパーを作る技術 / How to make Key Remapper

Avatar for とみたまさひろ

とみたまさひろ

February 26, 2022
Tweet

More Decks by とみたまさひろ

Other Decks in Technology

Transcript

  1. 経緯 経緯 11月に転職して人生初 Mac キーマップに慣れない でも Mac のキーマップの方が良さそう (Ctrl+N や

    Ctrl+P がブラウザに取られない) 仕事以外で使ってる Linux でも Mac と同じにしよう! 4
  2. Ctrl-N を ↓ Ctrl-P を ↑ Ctrl-F を → Ctrl-B

    を ← Alt-[A-Z] を Ctrl-[A-Z] みたいな感じにすれば良さそう 6
  3. 例 require 'rkremap' include Rkremap::KeyCode rk = Rkremap.new rk.grab =

    true rk.x11 = true rk.start do |code, mod, app| # Emacs や端末ではそのまま if app.class_name == 'Emacs' || app.class_name =~ /terminal/i rk.key(code, mod) next end # ALT+[A-Z] は Ctrl+[A-Z] に変換 if (mod[KEY_LEFTALT] || mod[KEY_RIGHTALT]) && Rkremap::CODE_KEY[code] =~ /\AKEY_[A-Z]\z/ mod[KEY_LEFTALT] = mod[KEY_RIGHTALT] = false mod[KEY_LEFTCTRL] = true rk.key(code, mod) next end end 11
  4. Ctrl-K 問題 日本語入力中かどうかは fcitx-remote コマンドで判定 while :; do if [

    $(fcitx-remote) -eq 2 ]; then touch /tmp/fcitx-enabled else rm -f /tmp/fcitx-enabled fi sleep 0.1 done 12
  5. 状態ファイルの有無で分岐 if mod[KEY_LEFTCTRL] || mod[KEY_RIGHTCTRL] # Ctrl+K/I/O は日本語変換時はそのまま if code

    == KEY_K && File.exist?('/tmp/fcitx-enabled') rk.key(code, mod) next end # Ctrl+K は行末まで削除 if code == KEY_K rk.key(KEY_END, mod_disable_all.merge({KEY_LEFTSHIFT => true})) rk.key(KEY_X, mod_disable_all.merge({KEY_LEFTCTRL => true})) next end ... 13
  6. キーロガー的なやつ require 'rkremap' def code2key(code) Rkremap::CODE_KEY[code].to_s.sub(/\AKEY_/, '') end rk =

    Rkremap.new rk.grab = false rk.x11 = true rk.start do |code, mod, app| key = (mod.select{|_, v| v}.keys + [code]).map{|c| code2key(c)}.join('-') key << " at #{app.title} [#{app.class_name}]" if rk.x11 puts key end 14
  7. /dev/input/event* から24バイト読む Rubyで(要root) struct input_event { struct timeval time; //

    イベント発生日時 // struct timeval { long int tv_usec, long int tv_nsec }; unsigned short type; // イベントタイプ unsigned short code; // キーコード(キーイベントの場合) unsigned int value; // 0:release / 1:press / 2:repeat }; ev = File.open('/dev/input/event3') raw = ev.sysread(24) sec, usec, type, code, value = raw.unpack('Q!Q!SSl') 17
  8. ThinkPad のキーボードで A を押して離すと: type code value EV_MSC(4) 4 30

    # よくわからん EV_KEY(1) KEY_A(30) 1 # 'A' 押す EV_SYN(0) 0 0 # 区切り EV_MSC(4) 4 30 # よくわからん EV_KEY(1) KEY_A(30) 0 # 'A' 離す EV_SYN(0) 0 0 # 区切り 18
  9. Ctrl や Alt 等の修飾キーも普通のキーと同じ Ctrl+A (EV_KEY だけ抜粋) EV_KEY(1) KEY_LEFTCTRL(29) 1

    # 'CTRL' 押す EV_KEY(1) KEY_A(30) 1 # 'A' 押す EV_KEY(1) KEY_A(30) 0 # 'A' 離す EV_KEY(1) KEY_LEFTCTRL(29) 0 # 'CTRL' 離す 19
  10. evtest % sudo evtest No device specified, trying to scan

    all of /dev/input/event* Available devices: /dev/input/event0: Lid Switch /dev/input/event1: Sleep Button /dev/input/event2: Power Button /dev/input/event3: AT Translated Set 2 keyboard /dev/input/event4: Video Bus /dev/input/event5: Synaptics TM3145-003 /dev/input/event6: ThinkPad Extra Buttons /dev/input/event7: HDA Intel PCH Dock Mic /dev/input/event8: HDA Intel PCH Mic /dev/input/event9: HDA Intel PCH Dock Headphone /dev/input/event10: HDA Intel PCH Headphone /dev/input/event11: HDA Intel PCH HDMI/DP,pcm=3 /dev/input/event12: HDA Intel PCH HDMI/DP,pcm=7 /dev/input/event13: HDA Intel PCH HDMI/DP,pcm=8 /dev/input/event14: HDA Intel PCH HDMI/DP,pcm=9 /dev/input/event15: HDA Intel PCH HDMI/DP,pcm=10 /dev/input/event16: TPPS/2 IBM TrackPoint /dev/input/event17: Integrated Camera: Integrated C 22
  11. キーボードデバイスかどうか キー A, Z に対応しているか EV_KEY = 0x01 # /usr/include/linux/input-event-codes.h

    より buf = '' ev.ioctl(2147566880, buf) # EVIOCGBIT(0, 1) buf[0].ord & EV_KEY #=> 1 ならキーボードデバイス KEY_A = 30 KEY_Z = 44 ev.ioctl(2153792801, buf) # EVIOCGBIT(EV_KEY, (KEY_MAX-1)/8+1) buf.unpack('C*')[KEY_A/8][KEY_A%8] != 0 #=> 'A' に対応 buf.unpack('C*')[KEY_Z/8][KEY_Z%8] != 0 #=> 'Z' に対応 23
  12. /dev/uinput で仮想入力デバイスを作れる(要root) udev = File.open('/dev/uinput', 'w') udev.ioctl(1074025828, EV_KEY) # UI_SET_EVBIT

    キーデバイス udev.ioctl(1074025829, KEY_A) # UI_SET_KEYBIT KEY_A を入力可能 udev.ioctl(1074025829, KEY_Z) # UI_SET_KEYBIT KEY_Z を入力可能 setup = [0x03, 0x1234, 0x5678, 1, 'name', 0].pack('SSSSZ80L') # デバイス情報はてきとー udev.ioctl(1079792899, setup) # UI_DEV_SETUP セットアップ udev.ioctl(21761) # UI_DEV_CREATE 作成 25
  13. 作られたデバイスを evtest で見てみる A と Z しか入力できないデバイス % sudo evtest

    /dev/input/event19 Input driver version is 1.0.1 Input device ID: bus 0x3 vendor 0x1234 product 0x5678 version 0x1 Input device name: "name" Supported events: Event type 0 (EV_SYN) Event type 1 (EV_KEY) Event code 30 (KEY_A) Event code 44 (KEY_Z) 26
  14. キー入力イベントの作成 A を押して離す # 時刻は不要 udev.syswrite(['', EV_KEY, KEY_A, 1].pack('a16SSl')) #

    push A udev.syswrite(['', EV_SYN, 0, 0].pack('a16SSl')) # 区切り udev.syswrite(['', EV_KEY, KEY_A, 0].pack('a16SSl')) # release A udev.syswrite(['', EV_SYN, 0, 0].pack('a16SSl')) # 区切り 27
  15. 毎秒 A-Z をランダムに書き込む迷惑なやつ keys = ('A'..'Z').map{eval "KEY_#{_1}"} while true key

    = keys.sample udev.syswrite(['', EV_KEY, key, 1].pack('a16SSl')) udev.syswrite(['', EV_SYN, 0, 0].pack('a16SSl')) udev.syswrite(['', EV_KEY, key, 0].pack('a16SSl')) udev.syswrite(['', EV_SYN, 0, 0].pack('a16SSl')) sleep 1 end 28
  16. X11 で入力フォーカスがあたってるアプリ名の取得 C の場合(ざっくりと): 1. XGetInputFocus() でフォーカス Window 取得 2.

    XGetClassHint() で Window のクラス名を取得 3. クラス名が得られたら XGetWMName() でウィンドウタイ トルを得る 4. クラス名が NULL なら XQueryTree() で親 Window を 得て 2 に戻る 33
  17. Fiddle Fiddle Ruby から C のライブラリ関数を呼び出せる コンパイル不要 require 'fiddle/import' module

    C extend Fiddle::Importer dlload 'libc.so.6' extern 'int atoi(const char *nptr)' end p C.atoi("123") #=> 123 36
  18. 構造体やポインタも扱える require 'fiddle/import' module C extend Fiddle::Importer dlload 'libc.so.6' typealias

    'time_t', 'long int' typealias 'suseconds_t', 'long int' Timeval = struct(['time_t tv_sec', 'suseconds_t tv_usec']) extern 'int gettimeofday(struct timeval *tv, struct timezone *tz)' end timeval = C::Timeval.malloc(Fiddle::RUBY_FREE) # GC時に解放される C.gettimeofday(timeval, nil) p timeval.tv_sec #=> 1970-01-01 00:00:00 UTC からの経過秒数 p timeval.tv_usec #=> マイクロ秒 37
  19. 必要な関数のみ使えるようにして module X11 extend Fiddle::Importer dlload 'libX11.so.6' typealias 'XID', 'unsigned

    long' typealias 'Window', 'XID' typealias 'Status', 'int' typealias 'Atom', 'unsigned long' Window = struct ['Window window'] Pointer = struct ['void *ptr'] XClassHint = struct ['char *name', 'char *class_name'] XTextProperty = struct ['unsigned char *value', 'Atom encoding', 'int format', 'unsigned long nitems'] extern 'Display* XOpenDisplay(char*)' extern 'int XGetInputFocus(Display*, Window*, int*)' extern 'int XGetClassHint(Display*, Window, XClassHint*)' extern 'Status XQueryTree(Display*, Window, Window*, Window*, Window**, unsigned int*)' extern 'Status XGetWMName(Display*, Window, XTextProperty*)' extern 'int Xutf8TextPropertyToTextList(Display*, XTextProperty*, char***, int*)' extern 'int XFree(void*)' extern 'void XFreeStringList(char**)' end 38
  20. ざっくりこんな感じ (ホントはX11が確保したメモリの解放処理も必要) class_hint = X11::XClassHint.malloc(Fiddle::RUBY_FREE) parent = X11::Window.malloc(Fiddle::RUBY_FREE) children =

    X11::Pointer.malloc(Fiddle::RUBY_FREE) _ = ' '*8 display = X11.XOpenDisplay(nil) w = X11::Window.malloc(Fiddle::RUBY_FREE) X11.XGetInputFocus(display, w, _) win = w.window while win > 0 class_hint.name = class_hint.class_name = nil X11.XGetClassHint(display, win, class_hint) break unless class_hint.name.null? && class_hint.class_name.null? X11.XQueryTree(display, win, _, parent, children, _) win = parent.window end prop = X11::XTextProperty.malloc(Fiddle::RUBY_FREE) text_list = X11::Pointer.malloc(Fiddle::RUBY_FREE) X11.XGetWMName(display, win, prop) X11.Xutf8TextPropertyToTextList(display, prop, text_list, _) p [class_hint.class_name.to_s, text_list.ptr.ptr.to_s.force_encoding('utf-8')] 39
  21. まとめ まとめ /dev/input/event* で入力イベントを読める /dev/input/event* を GRAB すると入力がアプリに渡 らなくなる /dev/uinput

    で仮想入力デバイスを作れる 大きなライブラリの関数をつまみ食いするには Fiddle が便利 40