rkremap 0.1.0 → 0.2.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 945483dcd11240d5c54b359a15055219532f2f311275633503e7d485aa68917b
4
- data.tar.gz: d594c9b549696f964cde750094169ba7198c0ac9ab5f5dfc59a7936d9610a09d
3
+ metadata.gz: b9cb72d3bf69bf2d0641eb7856903d110f0a253d5377c2d97ea452a1f0fd5c13
4
+ data.tar.gz: 1dd89d3569fc5396a6c314618bcc975e04b63b0f2a55d9c3dd8cd5497f1e2164
5
5
  SHA512:
6
- metadata.gz: a1412a373a0673172a9eba0449613ab9a76debf400b622977887031c294f8381a58a7c060ef6451e6264703690974aeddb305ce35619c07958a2b5a1239ecf12
7
- data.tar.gz: 619ee1de0814c2542c2a4f347dca7c48e640590a01ff58f2b7362ed80ef447acaf1576df2a2a659eaac95c9a7ec0a8f52d732d8024356613fbef43f055b16dd2
6
+ metadata.gz: 8cf20b635b0aa1584e81cc0317040931f62f011e004189e30428534c04456159cde7ac37c5f790cc1af06d396b9d60c36c4398a5d51947dcf99309eb3edb584b
7
+ data.tar.gz: cde725ba50b0d5b2fd3daf837af9e1244a1cc5cb3d40f497d9dbf00e6655edc0e436dcdc94056b8bc9efa3887ec2419fd7e53b11e8289c142217be2dd577f8ef
data/README.md CHANGED
@@ -25,7 +25,9 @@ KERNEL=="uinput", GROUP="input"
25
25
 
26
26
  ### `Rkremap.new`
27
27
 
28
- 引数に `/dev/input/event*` を指定する。キーボードデバイス以外を指定してもたぶん無意味。
28
+ 引数を指定しなければ自動的にキーボードデバイスの検出を試みる。
29
+
30
+ 引数を指定する場合は `/dev/input/event*` を指定する。キーボードデバイス以外を指定してもたぶん無意味。
29
31
  キーボードのデバイスファイル名を調べるには
30
32
  ```
31
33
  cat /proc/bus/input/devices
@@ -79,6 +81,7 @@ ThinkPad 本体のキーボードは `/dev/input/event3` だった。
79
81
  * [xkeysnail - もうひとつの Linux 向けキーリマッパ - Qiita](https://qiita.com/mooz@github/items/c5f25f27847333dd0b37)
80
82
  * [k0kubun/xremap: Dynamic key remapper for X11 and Wayland](https://github.com/k0kubun/xremap)
81
83
  * [Linux向けの最強のキーリマッパーを作った - k0kubun's blog](https://k0kubun.hatenablog.com/entry/xkremap)
84
+ * [Linux用キーリマッパーxremapをRustで書き直した - k0kubun's blog](https://k0kubun.hatenablog.com/entry/xremap)
82
85
  * [kui/rbindkeys: key remap with ruby](https://github.com/kui/rbindkeys)
83
86
  * [Ruby で設定できる Linux 環境向けキーリマッパー作った — 電卓片手に](http://k-ui.jp/blog/2012/06/06/rbindkeys-configurable-key-remapper-in-ruby/)
84
87
  * [Linux の入力デバイスをカスタマイズ - Qiita](https://qiita.com/propella/items/a73fc333c95d14d06835)
@@ -4,7 +4,7 @@
4
4
  require 'rkremap'
5
5
  include Rkremap::KeyCode
6
6
 
7
- rk = Rkremap.new('/dev/input/event3')
7
+ rk = Rkremap.new
8
8
  rk.grab = true
9
9
 
10
10
  class State
data/example/keylogger.rb CHANGED
@@ -6,7 +6,7 @@ def code2key(code)
6
6
  Rkremap::CODE_KEY[code].to_s.sub(/\AKEY_/, '')
7
7
  end
8
8
 
9
- rk = Rkremap.new('/dev/input/event3')
9
+ rk = Rkremap.new
10
10
  rk.grab = false
11
11
  rk.x11 = true
12
12
  rk.start do |code, mod, app|
@@ -33,7 +33,7 @@ mod_disable_meta = {
33
33
  }
34
34
  mod_disable_all = {}.merge(mod_disable_ctrl, mod_disable_shift, mod_disable_alt, mod_disable_meta)
35
35
 
36
- rk = Rkremap.new("/dev/input/event3")
36
+ rk = Rkremap.new
37
37
  rk.grab = true
38
38
  rk.x11 = true
39
39
  rk.start do |code, mod, app|
data/example/tmtms.rb CHANGED
@@ -40,7 +40,7 @@ class State
40
40
  end
41
41
  end
42
42
 
43
- rk = Rkremap.new("/dev/input/event3")
43
+ rk = Rkremap.new
44
44
  rk.grab = true
45
45
  rk.x11 = true
46
46
 
@@ -160,10 +160,15 @@ rk.start do |code, mod, app|
160
160
  # Ctrl は他のキーに変換
161
161
  if mod[KEY_LEFTCTRL] || mod[KEY_RIGHTCTRL]
162
162
  # Ctrl+K は行末まで削除 (日本語変換時はそのまま)
163
- if code == KEY_K && !File.exist?('/tmp/fcitx-enabled')
164
- rk.key(KEY_END, mod_disable_all.merge({KEY_LEFTSHIFT => true}))
165
- rk.key(KEY_X, mod_disable_all.merge({KEY_LEFTCTRL => true}))
166
- next
163
+ if code == KEY_K
164
+ if File.exist?('/tmp/fcitx-enabled')
165
+ rk.key(code, mod)
166
+ next
167
+ else
168
+ rk.key(KEY_END, mod_disable_all.merge({KEY_LEFTSHIFT => true}))
169
+ rk.key(KEY_X, mod_disable_all.merge({KEY_LEFTCTRL => true}))
170
+ next
171
+ end
167
172
  end
168
173
  # 単純な変換
169
174
  if ctrl_map[code]
@@ -0,0 +1,124 @@
1
+ class Rkremap
2
+ EV_KEY = 1
3
+ EV_SYN = 0
4
+ SYN_REPORT = 0
5
+
6
+ # https://www.kernel.org/doc/html/v4.12/input/input_uapi.html
7
+ class Evdev
8
+ EVIOCGBIT_ANY = 2147566880 # EVIOCGBIT(0, 1)
9
+ EVIOCGBIT_EV_KEY = 2153792801 # EVIOCGBIT(EV_KEY, (KEY_MAX-1)/8+1)
10
+ EVIOCGRAB = 1074021776
11
+
12
+ @io2evdev = {}
13
+ def self.io2evdev
14
+ @io2evdev
15
+ end
16
+
17
+ # @return [Array<Evdev>]
18
+ def self.devices
19
+ Dir.children("/dev/input").map do |dev|
20
+ dev =~ /\Aevent\d+\z/ ? Evdev.new("/dev/input/#{dev}") : nil
21
+ end.compact
22
+ end
23
+
24
+ # @param inputs [Array<Evdev>]
25
+ # @return [Evdev]
26
+ def self.select(inputs)
27
+ h = inputs.map{|ev| [ev.io, ev]}.to_h
28
+ r, = IO.select(inputs.map(&:io), nil, nil, nil)
29
+ @io2evdev[r[0]]
30
+ end
31
+
32
+ # @param path [String]
33
+ def initialize(path)
34
+ @path = path
35
+ @io = nil
36
+ @capa = nil
37
+ @uinput = nil
38
+ end
39
+
40
+ # @return [IO]
41
+ def io
42
+ return @io if @io
43
+ @io = File.open(@path)
44
+ self.class.io2evdev[@io] = self
45
+ @io
46
+ end
47
+
48
+ def close
49
+ @io&.close
50
+ end
51
+
52
+ # @param key [Integer]
53
+ # @return [Boolean]
54
+ def capable?(key)
55
+ unless @capa
56
+ buf = ' ' * ((KeyCode::KEY_MAX-1)/8+1)
57
+ io.ioctl(EVIOCGBIT_EV_KEY, buf)
58
+ @capa = buf.unpack('C*')
59
+ end
60
+ @capa[key/8][key%8] != 0
61
+ end
62
+
63
+ # @return [Boolean]
64
+ def keyboard?
65
+ buf = ' '
66
+ io.ioctl(EVIOCGBIT_ANY, buf)
67
+ return false if buf.unpack1('C')[EV_KEY] == 0
68
+ capable?(KeyCode::KEY_0) && capable?(KeyCode::KEY_9) &&
69
+ capable?(KeyCode::KEY_A) && capable?(KeyCode::KEY_Z) && capable?(KeyCode::KEY_SPACE)
70
+ end
71
+
72
+ def grab
73
+ io.ioctl(EVIOCGRAB, 1)
74
+ end
75
+
76
+ # struct input_event {
77
+ # struct timeval time;
78
+ # unsigned short type;
79
+ # unsigned short code;
80
+ # unsigned int value;
81
+ # };
82
+ # @return [Array<Time, Integer, Integer, Integer>]
83
+ def read_event
84
+ raw = io.sysread(24) # sizeof(struct input_event)
85
+ sec, usec, type, code, value = raw.unpack('Q!Q!SSl')
86
+ time = Time.at(sec, usec)
87
+ return time, type, code, value
88
+ end
89
+ end
90
+
91
+ # https://www.kernel.org/doc/html/v4.12/input/uinput.html
92
+ class Uinput
93
+ UI_SET_EVBIT = 1074025828
94
+ UI_SET_KEYBIT = 1074025829
95
+ UI_DEV_SETUP = 1079792899
96
+ UI_DEV_CREATE = 21761
97
+
98
+ def initialize
99
+ @dev = File.open('/dev/uinput', 'w')
100
+ setup
101
+ end
102
+
103
+ def setup
104
+ @dev.ioctl(UI_SET_EVBIT, EV_KEY)
105
+ KeyCode::KEY_CNT.times{|k| @dev.ioctl(UI_SET_KEYBIT, k)}
106
+ bustype = 0x03 # BUS_USB
107
+ vendor = 0x1234 # てきとー
108
+ product = 0x5678 # てきとー
109
+ version = 1 # てきとー
110
+ name = 'rkremap'
111
+ ff_efects_max = 0
112
+ setup = [bustype, vendor, product, version, name, ff_efects_max].pack("SSSSZ80L") # struct uinput_setup
113
+ @dev.ioctl(UI_DEV_SETUP, setup)
114
+ @dev.ioctl(UI_DEV_CREATE)
115
+ end
116
+
117
+ # @param type [Integer] EV_KEY/ EV_SYN
118
+ # @param code [Integer] キーコード / SYN_REPORT
119
+ # @param value [Integer] 0:release / 1:press / 2:repeat
120
+ def write_event(type, code, value)
121
+ @dev.syswrite(['', type, code, value].pack('a16SSl'))
122
+ end
123
+ end
124
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Rkremap
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -1,12 +1,4 @@
1
- require 'ffi'
2
-
3
- module FFI
4
- typedef :pointer, :Display
5
- typedef :ulong, :XID
6
- typedef :XID, :Window
7
- typedef :int, :Status
8
- typedef :ulong, :Atom
9
- end
1
+ require 'fiddle/import'
10
2
 
11
3
  class Rkremap
12
4
  # アプリの name, class, title を取得する
@@ -14,38 +6,39 @@ class Rkremap
14
6
  def initialize
15
7
  @app_win = {}
16
8
  @display = X11.XOpenDisplay(nil) or raise "Cannot open display: #{ENV['DISPLAY']}"
9
+ @buf = ' '*8
17
10
  end
18
11
 
12
+ # @return [Integer] Window
19
13
  def focus_win
20
- win = FFI::MemoryPointer.new(:Window)
21
- revert_to = FFI::MemoryPointer.new(:int)
22
- X11.XGetInputFocus(@display, win, revert_to)
23
- win.read(:Window)
14
+ win_buf = ' ' * X11.sizeof('Window')
15
+ X11.XGetInputFocus(@display, win_buf, @buf)
16
+ win_buf.unpack1('L')
24
17
  end
25
18
 
26
19
  # @return [Array<window, name, class>]
27
20
  def app_win(window)
28
21
  return @app_win[window] if @app_win[window]
29
- class_hint = XClassHint.new
30
- root = FFI::MemoryPointer.new(:Window)
31
- parent = FFI::MemoryPointer.new(:Window)
32
- children = FFI::MemoryPointer.new(:pointer)
33
- nchildren = FFI::MemoryPointer.new(:int)
22
+ class_hint = X11::XClassHint.malloc
23
+ parent = ' ' * X11.sizeof('Window')
24
+ children = ' ' * X11.sizeof('void*')
34
25
  win = window
35
26
  while win > 0
27
+ class_hint.name = nil
28
+ class_hint.class_name = nil
36
29
  X11.XGetClassHint(@display, win, class_hint)
37
- X11.XQueryTree(@display, win, root, parent, children, nchildren)
38
- X11.XFree(children.read_pointer)
39
- break unless class_hint[:name].null? && class_hint[:class].null?
40
- win = parent.read(:Window)
30
+ X11.XQueryTree(@display, win, @buf, parent, children, @buf)
31
+ X11.XFree(Fiddle::Pointer[children].ptr)
32
+ break unless class_hint.name.null? && class_hint.class_name.null?
33
+ win = parent.unpack1('L')
41
34
  end
42
- unless class_hint[:name].null?
43
- win_name = class_hint[:name].read_string
44
- X11.XFree(class_hint[:name])
35
+ unless class_hint.name.null?
36
+ win_name = class_hint.name.to_s
37
+ X11.XFree(class_hint.name)
45
38
  end
46
- unless class_hint[:class].null?
47
- win_class = class_hint[:class].read_string
48
- X11.XFree(class_hint[:class])
39
+ unless class_hint.class_name.null?
40
+ win_class = class_hint.class_name.to_s
41
+ X11.XFree(class_hint.class_name)
49
42
  end
50
43
  @app_win[window] = [win, win_name, win_class]
51
44
  end
@@ -53,43 +46,33 @@ class Rkremap
53
46
  # @return [String]
54
47
  def app_title(window)
55
48
  win = app_win(window)[0]
56
- prop = XTextProperty.new
57
- text_list = FFI::MemoryPointer.new(:pointer)
58
- count = FFI::MemoryPointer.new(:int)
49
+ prop = X11::XTextProperty.malloc
50
+ text_list = ' ' * X11.sizeof('void*')
59
51
  X11.XGetWMName(@display, win, prop)
60
- X11.Xutf8TextPropertyToTextList(@display, prop, text_list, count)
61
- title = text_list.read(:pointer).read(:string)&.force_encoding('utf-8')
62
- X11.XFreeStringList(text_list.read(:pointer))
52
+ X11.Xutf8TextPropertyToTextList(@display, prop, text_list, @buf)
53
+ title = Fiddle::Pointer[text_list].ptr.ptr.to_s.force_encoding('utf-8')
54
+ X11.XFreeStringList(Fiddle::Pointer[text_list].ptr)
63
55
  title
64
56
  end
65
57
  end
66
58
 
67
- class XClassHint < FFI::Struct
68
- layout(
69
- :name, :pointer,
70
- :class, :pointer,
71
- )
72
- end
73
-
74
- class XTextProperty < FFI::Struct
75
- layout(
76
- :value, :pointer,
77
- :encoding, :Atom,
78
- :format, :int,
79
- :nitems, :ulong,
80
- )
81
- end
82
-
83
59
  module X11
84
- extend FFI::Library
85
- ffi_lib 'X11'
86
- attach_function :XOpenDisplay, [:string], :Display
87
- attach_function :XGetInputFocus, [:Display, :pointer, :pointer], :int
88
- attach_function :XGetClassHint, [:Display, :Window, XClassHint.by_ref], :int
89
- attach_function :XQueryTree, [:Display, :Window, :pointer, :pointer, :pointer, :pointer], :Status
90
- attach_function :XGetWMName, [:Display, :Window, XTextProperty.by_ref], :void
91
- attach_function :Xutf8TextPropertyToTextList, [:Display, XTextProperty.by_ref, :pointer, :pointer], :int
92
- attach_function :XFree, [:pointer], :int
93
- attach_function :XFreeStringList, [:pointer], :void
60
+ extend Fiddle::Importer
61
+ dlload 'libX11.so'
62
+ typealias 'XID', 'unsigned long'
63
+ typealias 'Window', 'XID'
64
+ typealias 'Status', 'int'
65
+ typealias 'Atom', 'unsigned long'
66
+ XClassHint = struct(['char *name', 'char *class_name'])
67
+ XTextProperty = struct(['unsigned char *value', 'Atom encoding', 'int format', 'unsigned long nitems'])
68
+
69
+ extern 'Display* XOpenDisplay(char*)'
70
+ extern 'int XGetInputFocus(Display*, Window*, int*)'
71
+ extern 'int XGetClassHint(Display*, Window, XClassHint*)'
72
+ extern 'Status XQueryTree(Display*, Window, Window*, Window*, Window**, unsigned int*)'
73
+ extern 'Status XGetWMName(Display*, Window, XTextProperty*)'
74
+ extern 'int Xutf8TextPropertyToTextList(Display*, XTextProperty*, char***, int*)'
75
+ extern 'int XFree(void*)'
76
+ extern 'void XFreeStringList(char**)'
94
77
  end
95
78
  end
data/lib/rkremap.rb CHANGED
@@ -3,20 +3,11 @@
3
3
  require_relative "rkremap/version"
4
4
  require_relative 'rkremap/keycode'
5
5
  require_relative 'rkremap/winattr'
6
+ require_relative 'rkremap/evdev'
6
7
 
7
8
  class Rkremap
8
9
  include KeyCode
9
10
 
10
- # ioctl constants
11
- EVIOCGRAB = 1074021776
12
- UI_SET_EVBIT = 1074025828
13
- UI_SET_KEYBIT = 1074025829
14
- UI_DEV_SETUP = 1079792899
15
- UI_DEV_CREATE = 21761
16
- EV_KEY = 1
17
- EV_SYN = 0
18
- SYN_REPORT = 0
19
-
20
11
  EVENT_TYPE_VALUE = {
21
12
  release: 0,
22
13
  press: 1,
@@ -27,11 +18,12 @@ class Rkremap
27
18
  attr_accessor :modifiers
28
19
  attr_accessor :x11
29
20
 
30
- # @param device [String]
31
- def initialize(device)
32
- @input = File.open(device)
33
- @uinput = File.open('/dev/uinput', 'w')
34
- setup_uinput
21
+ # @param devices [Array<String>, String]
22
+ def initialize(devices=nil)
23
+ devices = Array(devices) if devices.is_a? String
24
+ @inputs = devices ? devices.map{|d| Evdev.new(d)} : Evdev.devices.select(&:keyboard?)
25
+ raise 'Unable to detect keyboard device' if @inputs.empty?
26
+ @uinput = Uinput.new
35
27
  @grab = false
36
28
  @x11 = false
37
29
  @modifiers = [
@@ -53,13 +45,12 @@ class Rkremap
53
45
  def start(&block)
54
46
  @mod_state = @modifiers.map{|m| [m, false]}.to_h
55
47
  @winattr = WinAttr.new if @x11
56
- @input.ioctl(EVIOCGRAB, 1) if @grab
48
+ @inputs.each(&:grab) if @grab
57
49
  while true
58
50
  @keys = []
59
- raw = @input.sysread(24)
60
- sec, usec, type, code, value = raw.unpack('Q!Q!SSl')
51
+ input = Evdev.select(@inputs)
52
+ time, type, code, value = input.read_event
61
53
  next if type != EV_KEY
62
- time = Time.at(sec, usec)
63
54
  event = Event.new(time, code, value, @winattr)
64
55
  @events.each do |cond, b|
65
56
  synchronize{ b.call event } if event.match?(**cond)
@@ -100,20 +91,6 @@ class Rkremap
100
91
 
101
92
  private
102
93
 
103
- def setup_uinput
104
- @uinput.ioctl(UI_SET_EVBIT, EV_KEY)
105
- KEY_CNT.times{|k| @uinput.ioctl(UI_SET_KEYBIT, k)}
106
- bustype = 0x03 # BUS_USB
107
- vendor = 0x1234 # てきとー
108
- product = 0x5678 # てきとー
109
- version = 1 # てきとー
110
- name = 'rkremap'
111
- ff_efects_max = 0
112
- setup = [bustype, vendor, product, version, name, ff_efects_max].pack("SSSSZ80L") # struct uinput_setup
113
- @uinput.ioctl(UI_DEV_SETUP, setup)
114
- @uinput.ioctl(UI_DEV_CREATE)
115
- end
116
-
117
94
  def proc_event(code, value, event)
118
95
  if @mod_state.include?(code)
119
96
  @mod_state[code] = value != 0
@@ -130,8 +107,8 @@ class Rkremap
130
107
  # @param code [Integer]
131
108
  # @param value [Integer] 0:release / 1:press / 2:repeat
132
109
  def write_event(code, value)
133
- @uinput.syswrite(['', EV_KEY, code, value].pack('a16SSl'))
134
- @uinput.syswrite(['', EV_SYN, SYN_REPORT, 0].pack('a16SSl'))
110
+ @uinput.write_event(EV_KEY, code, value)
111
+ @uinput.write_event(EV_SYN, SYN_REPORT, 0)
135
112
  end
136
113
 
137
114
  class Event
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rkremap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - TOMITA Masahiro
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-01-10 00:00:00.000000000 Z
12
- dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: ffi
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '1.0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '1.0'
11
+ date: 2022-01-12 00:00:00.000000000 Z
12
+ dependencies: []
27
13
  description: key remapper
28
14
  email:
29
15
  - tommy@tmtm.org
@@ -37,6 +23,7 @@ files:
37
23
  - example/mac-shortcut.rb
38
24
  - example/tmtms.rb
39
25
  - lib/rkremap.rb
26
+ - lib/rkremap/evdev.rb
40
27
  - lib/rkremap/keycode.rb
41
28
  - lib/rkremap/version.rb
42
29
  - lib/rkremap/winattr.rb