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 +4 -4
- data/README.md +4 -1
- data/example/henkan-alt.rb +1 -1
- data/example/keylogger.rb +1 -1
- data/example/mac-shortcut.rb +1 -1
- data/example/tmtms.rb +10 -5
- data/lib/rkremap/evdev.rb +124 -0
- data/lib/rkremap/version.rb +1 -1
- data/lib/rkremap/winattr.rb +43 -60
- data/lib/rkremap.rb +12 -35
- metadata +4 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b9cb72d3bf69bf2d0641eb7856903d110f0a253d5377c2d97ea452a1f0fd5c13
|
4
|
+
data.tar.gz: 1dd89d3569fc5396a6c314618bcc975e04b63b0f2a55d9c3dd8cd5497f1e2164
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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)
|
data/example/henkan-alt.rb
CHANGED
data/example/keylogger.rb
CHANGED
data/example/mac-shortcut.rb
CHANGED
data/example/tmtms.rb
CHANGED
@@ -40,7 +40,7 @@ class State
|
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
|
-
rk = Rkremap.new
|
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
|
164
|
-
|
165
|
-
|
166
|
-
|
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
|
data/lib/rkremap/version.rb
CHANGED
data/lib/rkremap/winattr.rb
CHANGED
@@ -1,12 +1,4 @@
|
|
1
|
-
require '
|
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
|
-
|
21
|
-
|
22
|
-
|
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.
|
30
|
-
|
31
|
-
|
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,
|
38
|
-
X11.XFree(children.
|
39
|
-
break unless class_hint
|
40
|
-
win = parent.
|
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
|
43
|
-
win_name = class_hint
|
44
|
-
X11.XFree(class_hint
|
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
|
47
|
-
win_class = class_hint
|
48
|
-
X11.XFree(class_hint
|
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.
|
57
|
-
text_list =
|
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,
|
61
|
-
title = text_list.
|
62
|
-
X11.XFreeStringList(text_list.
|
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
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
31
|
-
def initialize(
|
32
|
-
|
33
|
-
@
|
34
|
-
|
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
|
-
@
|
48
|
+
@inputs.each(&:grab) if @grab
|
57
49
|
while true
|
58
50
|
@keys = []
|
59
|
-
|
60
|
-
|
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.
|
134
|
-
@uinput.
|
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.
|
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-
|
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
|