twiddling 0.1
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 +7 -0
- data/.gitignore +7 -0
- data/.mdl_rules.rb +2 -0
- data/.mdlrc +2 -0
- data/.quiet_quality.yml +9 -0
- data/.rspec +1 -0
- data/.rubocop.yml +21 -0
- data/.standard.yml +3 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +116 -0
- data/bin/cfg2tw7 +13 -0
- data/bin/tw72cfg +9 -0
- data/bin/twiddling +9 -0
- data/configs/v7/default.cfg +0 -0
- data/configs/v7/default.tw7 +168 -0
- data/configs/v7/ericspace2.cfg +0 -0
- data/configs/v7/ericspace2.tw7 +203 -0
- data/fixtures/v7/README.md +31 -0
- data/fixtures/v7/button-mode-keyboard.cfg +0 -0
- data/fixtures/v7/button-mode-keyboard.tw7 +16 -0
- data/fixtures/v7/cycle-config-chord.cfg +0 -0
- data/fixtures/v7/cycle-config-chord.tw7 +12 -0
- data/fixtures/v7/empty.cfg +0 -0
- data/fixtures/v7/empty.tw7 +10 -0
- data/fixtures/v7/haptic-feedback-off.cfg +0 -0
- data/fixtures/v7/haptic-feedback-off.tw7 +16 -0
- data/fixtures/v7/idle-time-8m.cfg +0 -0
- data/fixtures/v7/idle-time-8m.tw7 +12 -0
- data/fixtures/v7/key-repeat-1020.cfg +0 -0
- data/fixtures/v7/key-repeat-1020.tw7 +12 -0
- data/fixtures/v7/key-repeat-disabled.cfg +0 -0
- data/fixtures/v7/key-repeat-disabled.tw7 +12 -0
- data/fixtures/v7/large.cfg +0 -0
- data/fixtures/v7/large.tw7 +142 -0
- data/fixtures/v7/mini-buttons.cfg +0 -0
- data/fixtures/v7/mini-buttons.tw7 +14 -0
- data/fixtures/v7/modifier-key.cfg +0 -0
- data/fixtures/v7/modifier-key.tw7 +12 -0
- data/fixtures/v7/multi-char.cfg +0 -0
- data/fixtures/v7/multi-char.tw7 +12 -0
- data/fixtures/v7/nav-invert-x-axis.cfg +0 -0
- data/fixtures/v7/nav-invert-x-axis.tw7 +16 -0
- data/fixtures/v7/nav-sensitivity-lowered.cfg +0 -0
- data/fixtures/v7/nav-sensitivity-lowered.tw7 +12 -0
- data/fixtures/v7/nav-up-east.cfg +0 -0
- data/fixtures/v7/nav-up-east.tw7 +12 -0
- data/fixtures/v7/no-right-mouse-button.cfg +0 -0
- data/fixtures/v7/no-right-mouse-button.tw7 +12 -0
- data/fixtures/v7/no-t0-dedicated.cfg +0 -0
- data/fixtures/v7/no-t0-dedicated.tw7 +12 -0
- data/fixtures/v7/shifted-key.cfg +0 -0
- data/fixtures/v7/shifted-key.tw7 +12 -0
- data/fixtures/v7/single-unmodified-key.cfg +0 -0
- data/fixtures/v7/single-unmodified-key.tw7 +12 -0
- data/formats/tw7.md +222 -0
- data/formats/v7-cfg.md +272 -0
- data/lib/twiddling/cli/convert.rb +70 -0
- data/lib/twiddling/cli/diff.rb +192 -0
- data/lib/twiddling/cli/help.rb +26 -0
- data/lib/twiddling/cli/read.rb +56 -0
- data/lib/twiddling/cli/search.rb +131 -0
- data/lib/twiddling/cli/twiddling.rb +33 -0
- data/lib/twiddling/cli.rb +8 -0
- data/lib/twiddling/v7/chord.rb +48 -0
- data/lib/twiddling/v7/chord_constants.rb +82 -0
- data/lib/twiddling/v7/config.rb +102 -0
- data/lib/twiddling/v7/config_constants.rb +30 -0
- data/lib/twiddling/v7/data/default_base.cfg +0 -0
- data/lib/twiddling/v7/reader/chord.rb +49 -0
- data/lib/twiddling/v7/reader/config.rb +64 -0
- data/lib/twiddling/v7/reader/settings.rb +26 -0
- data/lib/twiddling/v7/reader/string_table.rb +69 -0
- data/lib/twiddling/v7/settings.rb +21 -0
- data/lib/twiddling/v7/string_table.rb +21 -0
- data/lib/twiddling/v7/string_table_entry.rb +14 -0
- data/lib/twiddling/v7/tw7/button_formatter.rb +63 -0
- data/lib/twiddling/v7/tw7/button_parser.rb +78 -0
- data/lib/twiddling/v7/tw7/effect_formatter.rb +78 -0
- data/lib/twiddling/v7/tw7/effect_parser.rb +121 -0
- data/lib/twiddling/v7/tw7/parser.rb +133 -0
- data/lib/twiddling/v7/tw7/printer.rb +105 -0
- data/lib/twiddling/v7/tw7/settings_formatter.rb +96 -0
- data/lib/twiddling/v7/tw7/settings_parser.rb +97 -0
- data/lib/twiddling/v7/validator.rb +93 -0
- data/lib/twiddling/v7/writer/chord.rb +36 -0
- data/lib/twiddling/v7/writer/config.rb +79 -0
- data/lib/twiddling/v7/writer/settings.rb +19 -0
- data/lib/twiddling/v7/writer/string_table.rb +40 -0
- data/lib/twiddling/v7.rb +34 -0
- data/lib/twiddling/version.rb +3 -0
- data/lib/twiddling.rb +8 -0
- data/tmp/.gitkeep +0 -0
- data/twiddling.gemspec +46 -0
- metadata +284 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Twiddling
|
|
2
|
+
module V7
|
|
3
|
+
class StringTable
|
|
4
|
+
attr_reader :entries
|
|
5
|
+
|
|
6
|
+
def initialize(entries = [])
|
|
7
|
+
@entries = entries
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def entry_at_offset(byte_offset)
|
|
11
|
+
offset_lookup[byte_offset]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def offset_lookup
|
|
17
|
+
@offset_lookup ||= entries.to_h { |e| [e.byte_offset, e] }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module Twiddling
|
|
2
|
+
module V7
|
|
3
|
+
module Tw7
|
|
4
|
+
# Formats a chord bitmask as T4 notation button strings.
|
|
5
|
+
#
|
|
6
|
+
# Produces compact notation: thumb buttons combined (T14),
|
|
7
|
+
# finger buttons grouped by row (F1LR, F2M).
|
|
8
|
+
module ButtonFormatter
|
|
9
|
+
include ConfigConstants
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# Returns the full button string for a bitmask (e.g. "T14 F1L F2M").
|
|
14
|
+
def format(bitmask)
|
|
15
|
+
thumbs, fingers = partition_buttons(bitmask)
|
|
16
|
+
parts = []
|
|
17
|
+
parts << format_thumbs(thumbs) unless thumbs.empty?
|
|
18
|
+
fingers.sort.each { |row, cols| parts << "#{row}#{cols.join}" }
|
|
19
|
+
parts.join(" ")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Returns just the thumb portion of a bitmask (e.g. "T14").
|
|
23
|
+
# Returns nil if no thumb buttons.
|
|
24
|
+
def thumb_key(bitmask)
|
|
25
|
+
thumbs, _ = partition_buttons(bitmask)
|
|
26
|
+
return nil if thumbs.empty?
|
|
27
|
+
format_thumbs(thumbs)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Returns the finger portion of a bitmask (e.g. "F1L F2M"),
|
|
31
|
+
# excluding thumb buttons.
|
|
32
|
+
def finger_part(bitmask)
|
|
33
|
+
_, fingers = partition_buttons(bitmask)
|
|
34
|
+
fingers.sort.map { |row, cols| "#{row}#{cols.join}" }.join(" ")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def partition_buttons(bitmask)
|
|
38
|
+
thumbs = []
|
|
39
|
+
fingers = {}
|
|
40
|
+
|
|
41
|
+
BUTTON_BITS.each do |bit, name|
|
|
42
|
+
next unless bitmask[bit] == 1
|
|
43
|
+
classify_button(name, thumbs, fingers)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
[thumbs, fingers]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def classify_button(name, thumbs, fingers)
|
|
50
|
+
if name.start_with?("T")
|
|
51
|
+
thumbs << name[1..].to_i
|
|
52
|
+
else
|
|
53
|
+
(fingers[name[1].to_i] ||= []) << name[2..].to_sym
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def format_thumbs(thumbs)
|
|
58
|
+
"T#{thumbs.sort.join}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module Twiddling
|
|
2
|
+
module V7
|
|
3
|
+
module Tw7
|
|
4
|
+
# Parses T4 button notation into a bitmask.
|
|
5
|
+
#
|
|
6
|
+
# Accepts various forms:
|
|
7
|
+
# "T14 F1L F2M" - full notation
|
|
8
|
+
# "T14 1L 2M" - F prefix optional for fingers
|
|
9
|
+
# "T14 1L2M" - spaces optional between finger tokens
|
|
10
|
+
# "t14 f1l f2m" - case-insensitive
|
|
11
|
+
# "1LR" - multiple columns in one token
|
|
12
|
+
#
|
|
13
|
+
# A space or F is required between thumb buttons and finger
|
|
14
|
+
# buttons (T141R is ambiguous, use T14 1R).
|
|
15
|
+
module ButtonParser
|
|
16
|
+
include ConfigConstants
|
|
17
|
+
|
|
18
|
+
BUTTON_NAME_TO_BIT = BUTTON_BITS.to_h { |bit, name|
|
|
19
|
+
[name.to_s, bit]
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
def parse(text)
|
|
25
|
+
thumb_part, finger_part = split_thumb_finger(text.strip.upcase)
|
|
26
|
+
mask = 0
|
|
27
|
+
mask |= parse_thumbs(thumb_part) if thumb_part
|
|
28
|
+
mask |= parse_fingers(finger_part) if finger_part
|
|
29
|
+
raise ArgumentError, "Invalid button spec: #{text}" if mask == 0
|
|
30
|
+
mask
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# Split on the boundary between thumb (T...) and finger parts.
|
|
36
|
+
# Returns [thumb_string_or_nil, finger_string_or_nil].
|
|
37
|
+
def split_thumb_finger(text)
|
|
38
|
+
if text.start_with?("T")
|
|
39
|
+
match = text.match(/\A(T\d+)\s*(.*)?\z/)
|
|
40
|
+
raise ArgumentError, "Invalid button spec: #{text}" unless match
|
|
41
|
+
thumb = match[1]
|
|
42
|
+
rest = match[2]&.strip
|
|
43
|
+
[thumb, rest&.empty? ? nil : rest]
|
|
44
|
+
else
|
|
45
|
+
[nil, text]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# "T14" -> T1 + T4
|
|
50
|
+
def parse_thumbs(text)
|
|
51
|
+
text[1..].chars.reduce(0) do |mask, digit|
|
|
52
|
+
bit = BUTTON_NAME_TO_BIT["T#{digit}"]
|
|
53
|
+
raise ArgumentError, "Unknown thumb button: T#{digit}" unless bit
|
|
54
|
+
mask | (1 << bit)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# "F1L2M" or "1L2M" or "F1LR" -> walk chars
|
|
59
|
+
def parse_fingers(text)
|
|
60
|
+
tokens = text.delete(" \t").gsub(/F(?=\d)/, "").scan(/\d[LMR]+/)
|
|
61
|
+
tokens.reduce(0) { |mask, token| mask | resolve_finger_token(token) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def resolve_finger_token(token)
|
|
65
|
+
row = token[0]
|
|
66
|
+
token[1..].chars.reduce(0) do |mask, col|
|
|
67
|
+
bit = BUTTON_NAME_TO_BIT["F#{row}#{col}"]
|
|
68
|
+
raise ArgumentError, "Unknown button: F#{row}#{col}" unless bit
|
|
69
|
+
mask | (1 << bit)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
module_function :split_thumb_finger, :parse_thumbs, :parse_fingers,
|
|
74
|
+
:resolve_finger_token
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module Twiddling
|
|
2
|
+
module V7
|
|
3
|
+
module Tw7
|
|
4
|
+
# Formats a chord's effect (output) as tw7 text.
|
|
5
|
+
module EffectFormatter
|
|
6
|
+
include ChordConstants
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def format_effect(chord)
|
|
11
|
+
case chord.type_name
|
|
12
|
+
when :keyboard then format_keyboard(chord)
|
|
13
|
+
when :device then format_device(chord)
|
|
14
|
+
when :multichar then format_multichar(chord)
|
|
15
|
+
else "0x%04x" % chord.modifier_type
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def format_keyboard(chord)
|
|
20
|
+
mods = chord.modifier_names.map(&:downcase)
|
|
21
|
+
key = chord.key_name || ("0x%04x" % chord.keycode)
|
|
22
|
+
|
|
23
|
+
result = if mods == ["shift"] && SHIFTED_KEYS[key]
|
|
24
|
+
SHIFTED_KEYS[key]
|
|
25
|
+
elsif mods.empty?
|
|
26
|
+
key
|
|
27
|
+
else
|
|
28
|
+
(mods + [key]).join("+")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
format_ambiguous(result, chord)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Characters that can't appear as bare effects.
|
|
35
|
+
# # starts a comment, " starts a string literal.
|
|
36
|
+
QUOTE_CHARS = Set.new(["#"]).freeze
|
|
37
|
+
UNQUOTABLE_CHARS = Set.new(['"']).freeze
|
|
38
|
+
|
|
39
|
+
def format_ambiguous(result, chord)
|
|
40
|
+
if UNQUOTABLE_CHARS.include?(result)
|
|
41
|
+
# Can't quote " inside double quotes - use explicit modifier form
|
|
42
|
+
mods = chord.modifier_names.map(&:downcase)
|
|
43
|
+
key = chord.key_name
|
|
44
|
+
(mods + [key]).join("+")
|
|
45
|
+
elsif QUOTE_CHARS.include?(result)
|
|
46
|
+
%("#{result}")
|
|
47
|
+
else
|
|
48
|
+
result
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def format_device(chord)
|
|
53
|
+
chord.device_function&.to_s || ("device_0x%02x" % chord.modifier_byte)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def format_multichar(chord)
|
|
57
|
+
return "multichar_0x%04x" % chord.modifier_type unless chord.string_keys
|
|
58
|
+
|
|
59
|
+
chars = chord.string_keys.map { |sk| string_key_to_char(sk) }.join
|
|
60
|
+
%("#{chars}")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def string_key_to_char(sk)
|
|
64
|
+
key = HID_KEYS[sk[:hid_code]]
|
|
65
|
+
return "?" unless key
|
|
66
|
+
return KEY_TO_CHAR[key] if KEY_TO_CHAR.key?(key)
|
|
67
|
+
|
|
68
|
+
shifted = ((sk[:modifier] >> 8) & MODIFIER_SHIFT) != 0
|
|
69
|
+
if shifted && key.length == 1
|
|
70
|
+
SHIFTED_KEYS[key] || key.upcase
|
|
71
|
+
else
|
|
72
|
+
key
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
module Twiddling
|
|
2
|
+
module V7
|
|
3
|
+
module Tw7
|
|
4
|
+
# Parses a chord effect string into modifier_type, keycode,
|
|
5
|
+
# and optional string_keys.
|
|
6
|
+
#
|
|
7
|
+
# Accepts:
|
|
8
|
+
# "c" - plain key
|
|
9
|
+
# "@" - shifted symbol
|
|
10
|
+
# "ctrl+c" - modifier+key
|
|
11
|
+
# "cmd+shift+a" - multiple modifiers
|
|
12
|
+
# '"the "' - multi-char string (double-quoted)
|
|
13
|
+
# "speed_cycle" - device function name
|
|
14
|
+
module EffectParser
|
|
15
|
+
include ChordConstants
|
|
16
|
+
|
|
17
|
+
# Reverse lookups
|
|
18
|
+
KEY_TO_HID = HID_KEYS.to_h { |hid, name| [name, hid] }.freeze
|
|
19
|
+
SHIFTED_TO_BASE = SHIFTED_KEYS.to_h { |base, shifted| [shifted, base] }.freeze
|
|
20
|
+
FUNCTION_TO_CODE = DEVICE_FUNCTIONS.to_h { |code, name| [name.to_s, code] }.freeze
|
|
21
|
+
MODIFIER_NAME_TO_BIT = MODIFIERS.to_h { |bit, name| [name.downcase, bit] }.freeze
|
|
22
|
+
CHAR_TO_KEY = KEY_TO_CHAR.to_h { |name, char| [char, name] }.freeze
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# Returns {modifier_type:, keycode:, string_keys:}
|
|
27
|
+
def parse(text)
|
|
28
|
+
text = text.strip
|
|
29
|
+
return parse_quoted(text) if text.start_with?('"') && text.end_with?('"')
|
|
30
|
+
|
|
31
|
+
classify_and_parse(text)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def parse_quoted(text)
|
|
35
|
+
inner = text[1..-2]
|
|
36
|
+
(inner.length == 1) ? parse_quoted_char(inner) : parse_string(inner)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def classify_and_parse(text)
|
|
40
|
+
if FUNCTION_TO_CODE.key?(text) then parse_device_function(text)
|
|
41
|
+
elsif SHIFTED_TO_BASE.key?(text) then parse_key(text)
|
|
42
|
+
elsif text.include?("+") then parse_modified_key(text)
|
|
43
|
+
elsif text.match?(/\A0x[0-9a-f]+\z/i) then parse_hex_key(text)
|
|
44
|
+
else
|
|
45
|
+
parse_key(text)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def parse_key(text)
|
|
52
|
+
if (base = SHIFTED_TO_BASE[text])
|
|
53
|
+
hid = KEY_TO_HID[base] || raise(ArgumentError, "Unknown key: #{text}")
|
|
54
|
+
{modifier_type: TYPE_KEYBOARD | (MODIFIER_SHIFT << 8), keycode: hid}
|
|
55
|
+
else
|
|
56
|
+
hid = KEY_TO_HID[text] || raise(ArgumentError, "Unknown key: #{text}")
|
|
57
|
+
{modifier_type: TYPE_KEYBOARD, keycode: hid}
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def parse_modified_key(text)
|
|
62
|
+
parts = text.split("+")
|
|
63
|
+
key_name = parts.pop
|
|
64
|
+
mod_byte = parts.reduce(0) { |m, name|
|
|
65
|
+
bit = MODIFIER_NAME_TO_BIT[name.downcase]
|
|
66
|
+
raise ArgumentError, "Unknown modifier: #{name}" unless bit
|
|
67
|
+
m | bit
|
|
68
|
+
}
|
|
69
|
+
hid = KEY_TO_HID[key_name] || raise(ArgumentError, "Unknown key: #{key_name}")
|
|
70
|
+
{modifier_type: TYPE_KEYBOARD | (mod_byte << 8), keycode: hid}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# A single-char quoted string like "#" is a keyboard chord,
|
|
74
|
+
# not a multi-char string.
|
|
75
|
+
def parse_quoted_char(char)
|
|
76
|
+
sk = char_to_key(char)
|
|
77
|
+
{modifier_type: sk[:modifier], keycode: sk[:hid_code]}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def parse_hex_key(text)
|
|
81
|
+
{modifier_type: TYPE_KEYBOARD, keycode: Integer(text, 16)}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def parse_device_function(text)
|
|
85
|
+
code = FUNCTION_TO_CODE[text]
|
|
86
|
+
{modifier_type: TYPE_DEVICE | (code << 8), keycode: 0}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def parse_string(text)
|
|
90
|
+
keys = text.chars.map { |ch| char_to_key(ch) }
|
|
91
|
+
{modifier_type: TYPE_MULTICHAR, keycode: 0, string_keys: keys}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def char_to_key(char)
|
|
95
|
+
hid, shifted = resolve_char(char)
|
|
96
|
+
modifier = shifted ? (TYPE_KEYBOARD | (MODIFIER_SHIFT << 8)) : TYPE_KEYBOARD
|
|
97
|
+
{modifier: modifier, hid_code: hid}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def resolve_char(char)
|
|
101
|
+
hid, shifted = lookup_char(char)
|
|
102
|
+
raise ArgumentError, "Unknown character: #{char}" unless hid
|
|
103
|
+
[hid, shifted]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def lookup_char(char)
|
|
107
|
+
if (base = SHIFTED_TO_BASE[char]) then [KEY_TO_HID[base], true]
|
|
108
|
+
elsif char.match?(/[A-Z]/) then [KEY_TO_HID[char.downcase], true]
|
|
109
|
+
else
|
|
110
|
+
[KEY_TO_HID[CHAR_TO_KEY[char] || char], false]
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
module_function :parse_quoted, :classify_and_parse, :parse_key,
|
|
115
|
+
:parse_quoted_char, :parse_hex_key, :parse_modified_key,
|
|
116
|
+
:parse_device_function, :parse_string, :char_to_key,
|
|
117
|
+
:resolve_char, :lookup_char
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
module Twiddling
|
|
2
|
+
module V7
|
|
3
|
+
module Tw7
|
|
4
|
+
# Parses a .tw7 text file into a V7::Config.
|
|
5
|
+
class Parser
|
|
6
|
+
DIVIDER = /\A={5,}\z/
|
|
7
|
+
MOUSEMODE_SCOPE = "[MOUSEMODE]"
|
|
8
|
+
|
|
9
|
+
def initialize(text, base: nil)
|
|
10
|
+
@text = text
|
|
11
|
+
@base = base || default_base
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def parse
|
|
15
|
+
settings_lines, chord_lines = split_sections
|
|
16
|
+
attrs = build_attrs(settings_lines)
|
|
17
|
+
chords = parse_chords(chord_lines)
|
|
18
|
+
sorted = chords.sort_by(&:bitmask)
|
|
19
|
+
attrs[:chords] = sorted
|
|
20
|
+
attrs[:index_table] = Config.compute_index_table(sorted)
|
|
21
|
+
Config.new(attrs)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def default_base
|
|
27
|
+
Config.from_file(
|
|
28
|
+
File.expand_path("../data/default_base.cfg", __dir__)
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def split_sections
|
|
33
|
+
lines = strip_comments(@text.lines.map(&:rstrip))
|
|
34
|
+
divider_idx = lines.index { |l| l.match?(DIVIDER) }
|
|
35
|
+
if divider_idx
|
|
36
|
+
[lines[0...divider_idx], lines[(divider_idx + 1)..]]
|
|
37
|
+
else
|
|
38
|
+
[[], lines]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def strip_comments(lines)
|
|
43
|
+
lines.map { |line| strip_line_comment(line) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Remove # comments. A # starts a comment unless it's
|
|
47
|
+
# inside double quotes.
|
|
48
|
+
def strip_line_comment(line)
|
|
49
|
+
in_quotes = false
|
|
50
|
+
line.each_char.with_index do |ch, i|
|
|
51
|
+
if ch == '"'
|
|
52
|
+
in_quotes = !in_quotes
|
|
53
|
+
elsif ch == "#" && !in_quotes
|
|
54
|
+
return line[0, i].rstrip
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
line
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def build_attrs(settings_lines)
|
|
61
|
+
non_blank = settings_lines.reject(&:empty?)
|
|
62
|
+
parsed = SettingsParser.parse(non_blank)
|
|
63
|
+
settings = Settings.new(
|
|
64
|
+
thumb_modifiers: parsed.delete(:thumb_modifiers),
|
|
65
|
+
dedicated_buttons: parsed.delete(:dedicated_buttons),
|
|
66
|
+
reserved: @base.settings.reserved
|
|
67
|
+
)
|
|
68
|
+
base_attrs.merge(parsed).merge(settings: settings)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def base_attrs
|
|
72
|
+
Config::ATTR_NAMES.to_h { |name| [name, @base.public_send(name)] }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def parse_chords(lines)
|
|
76
|
+
chords = []
|
|
77
|
+
scope_bitmask = 0
|
|
78
|
+
mousemode = false
|
|
79
|
+
|
|
80
|
+
lines.each_with_index do |line, idx|
|
|
81
|
+
next if line.strip.empty?
|
|
82
|
+
parse_chord_line(line, idx, chords, scope_bitmask, mousemode)
|
|
83
|
+
.then { |new_scope, new_mouse| scope_bitmask, mousemode = new_scope, new_mouse }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
chords
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def parse_chord_line(line, idx, chords, scope_bitmask, mousemode)
|
|
90
|
+
indented = line.start_with?(" ", "\t")
|
|
91
|
+
content = line.strip
|
|
92
|
+
|
|
93
|
+
if content.end_with?("::")
|
|
94
|
+
open_scope(content, mousemode)
|
|
95
|
+
elsif indented
|
|
96
|
+
chords << build_chord(content, scope_bitmask, mousemode, idx)
|
|
97
|
+
[scope_bitmask, mousemode]
|
|
98
|
+
else
|
|
99
|
+
chords << build_chord(content, 0, false, idx)
|
|
100
|
+
[0, false]
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def open_scope(content, _mousemode)
|
|
105
|
+
scope_text = content.chomp("::")
|
|
106
|
+
if scope_text == MOUSEMODE_SCOPE
|
|
107
|
+
[0, true]
|
|
108
|
+
else
|
|
109
|
+
[ButtonParser.parse(scope_text), false]
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def build_chord(content, scope_bitmask, mousemode, line_idx)
|
|
114
|
+
buttons_text, effect_text = content.split(":", 2)
|
|
115
|
+
raise ArgumentError, "Line #{line_idx + 1}: missing effect" unless effect_text
|
|
116
|
+
|
|
117
|
+
bitmask = ButtonParser.parse(buttons_text) | scope_bitmask
|
|
118
|
+
bitmask |= ChordConstants::MOUSE_MODE_FLAG if mousemode
|
|
119
|
+
effect = EffectParser.parse(effect_text)
|
|
120
|
+
|
|
121
|
+
Chord.new(
|
|
122
|
+
bitmask: bitmask,
|
|
123
|
+
modifier_type: effect[:modifier_type],
|
|
124
|
+
keycode: effect[:keycode],
|
|
125
|
+
string_keys: effect[:string_keys]
|
|
126
|
+
)
|
|
127
|
+
rescue ArgumentError => e
|
|
128
|
+
raise ArgumentError, "Line #{line_idx + 1}: #{e.message}"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
module Twiddling
|
|
2
|
+
module V7
|
|
3
|
+
module Tw7
|
|
4
|
+
# Prints a V7::Config as .tw7 text format.
|
|
5
|
+
class Printer
|
|
6
|
+
MOUSEMODE_KEY = "[MOUSEMODE]"
|
|
7
|
+
|
|
8
|
+
def initialize(config, io: $stdout)
|
|
9
|
+
@config = config
|
|
10
|
+
@io = io
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def print
|
|
14
|
+
settings_lines = SettingsFormatter.format(@config)
|
|
15
|
+
print_settings(settings_lines) if settings_lines.any?
|
|
16
|
+
print_chords
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def print_settings(lines)
|
|
22
|
+
lines.each { |line| @io.puts line }
|
|
23
|
+
@io.puts "====="
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def print_chords
|
|
27
|
+
groups = group_chords
|
|
28
|
+
groups.each_with_index do |(group_key, chords), idx|
|
|
29
|
+
@io.puts if idx > 0
|
|
30
|
+
print_group(group_key, chords)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def group_chords
|
|
35
|
+
grouped = {}
|
|
36
|
+
@config.chords.each do |chord|
|
|
37
|
+
key = group_key(chord)
|
|
38
|
+
(grouped[key] ||= []) << chord
|
|
39
|
+
end
|
|
40
|
+
sort_groups(grouped)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def group_key(chord)
|
|
44
|
+
if chord.mouse_mode?
|
|
45
|
+
MOUSEMODE_KEY
|
|
46
|
+
else
|
|
47
|
+
ButtonFormatter.thumb_key(chord.bitmask) || ""
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Sort: no-thumb first, then by thumb combo length, then
|
|
52
|
+
# alphabetically. MOUSEMODE sorts last.
|
|
53
|
+
def sort_groups(grouped)
|
|
54
|
+
grouped.sort_by { |key, _| sort_key(key) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def sort_key(key)
|
|
58
|
+
case key
|
|
59
|
+
when "" then [0, 0, key]
|
|
60
|
+
when MOUSEMODE_KEY then [3, 0, key]
|
|
61
|
+
else [1, key.length, key]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def print_group(group_key, chords)
|
|
66
|
+
if group_key.empty?
|
|
67
|
+
chords.each { |chord| print_standalone(chord) }
|
|
68
|
+
elsif chords.length == 1
|
|
69
|
+
print_standalone(chords[0])
|
|
70
|
+
else
|
|
71
|
+
@io.puts "#{group_key}::"
|
|
72
|
+
chords.each { |chord| print_indented(chord, group_key) }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def print_standalone(chord)
|
|
77
|
+
buttons = format_all_buttons(chord)
|
|
78
|
+
@io.puts "#{buttons}: #{EffectFormatter.format_effect(chord)}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def print_indented(chord, group_key)
|
|
82
|
+
buttons = format_nested_buttons(chord, group_key)
|
|
83
|
+
@io.puts " #{buttons}: #{EffectFormatter.format_effect(chord)}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Full button string for standalone chords, stripping mouse flag.
|
|
87
|
+
def format_all_buttons(chord)
|
|
88
|
+
ButtonFormatter.format(chord.bitmask & ~ChordConstants::MOUSE_MODE_FLAG)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# For indented chords, strip the group's contribution.
|
|
92
|
+
# Thumb groups: show only fingers.
|
|
93
|
+
# MOUSEMODE: show all buttons except the mouse flag.
|
|
94
|
+
def format_nested_buttons(chord, group_key)
|
|
95
|
+
clean = chord.bitmask & ~ChordConstants::MOUSE_MODE_FLAG
|
|
96
|
+
if group_key == MOUSEMODE_KEY
|
|
97
|
+
ButtonFormatter.format(clean)
|
|
98
|
+
else
|
|
99
|
+
ButtonFormatter.finger_part(clean)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|