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,96 @@
|
|
|
1
|
+
module Twiddling
|
|
2
|
+
module V7
|
|
3
|
+
module Tw7
|
|
4
|
+
# Formats the settings section of a .tw7 file.
|
|
5
|
+
# Only emits settings that differ from defaults.
|
|
6
|
+
module SettingsFormatter
|
|
7
|
+
include ConfigConstants
|
|
8
|
+
|
|
9
|
+
DEFAULTS = {
|
|
10
|
+
idle_time: 600,
|
|
11
|
+
key_repeat_delay: 100,
|
|
12
|
+
key_repeat: true,
|
|
13
|
+
haptic: true,
|
|
14
|
+
keyboard_mode: false,
|
|
15
|
+
nav_sensitivity: 4,
|
|
16
|
+
nav_invert_x: false,
|
|
17
|
+
nav_direction: 0,
|
|
18
|
+
t1_modifier: :none,
|
|
19
|
+
t2_modifier: :l_option,
|
|
20
|
+
t3_modifier: :l_control,
|
|
21
|
+
t4_modifier: :l_shift,
|
|
22
|
+
f0l_dedicated: :mouse_right,
|
|
23
|
+
f0m_dedicated: :mouse_middle,
|
|
24
|
+
f0r_dedicated: :mouse_left,
|
|
25
|
+
t0_dedicated: :mouse_left
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
def format(config)
|
|
31
|
+
current = extract_settings(config)
|
|
32
|
+
lines = []
|
|
33
|
+
|
|
34
|
+
current.each do |key, value|
|
|
35
|
+
next if DEFAULTS[key] == value
|
|
36
|
+
lines << "#{key}: #{format_value(value)}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
lines
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def extract_settings(config)
|
|
43
|
+
extract_header_settings(config)
|
|
44
|
+
.merge(extract_thumb_settings(config))
|
|
45
|
+
.merge(extract_dedicated_settings(config))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def extract_header_settings(config)
|
|
49
|
+
extract_timing_settings(config).merge(extract_flag_settings(config))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def extract_timing_settings(config)
|
|
53
|
+
{idle_time: config.idle_time, key_repeat_delay: config.key_repeat}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def extract_flag_settings(config)
|
|
57
|
+
{
|
|
58
|
+
key_repeat: (config.flags_1 & 0x01) != 0,
|
|
59
|
+
haptic: (config.flags_1 & 0x08) != 0,
|
|
60
|
+
keyboard_mode: (config.flags_1 & 0x02) != 0,
|
|
61
|
+
nav_sensitivity: (config.flags_2 >> 3) & 0x1F,
|
|
62
|
+
nav_invert_x: (config.flags_2 & 0x04) != 0,
|
|
63
|
+
nav_direction: config.flags_2 & 0x03
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def extract_thumb_settings(config)
|
|
68
|
+
{
|
|
69
|
+
t1_modifier: THUMB_MODIFIERS[config.thumb_modifiers[0]],
|
|
70
|
+
t2_modifier: THUMB_MODIFIERS[config.thumb_modifiers[1]],
|
|
71
|
+
t3_modifier: THUMB_MODIFIERS[config.thumb_modifiers[2]],
|
|
72
|
+
t4_modifier: THUMB_MODIFIERS[config.thumb_modifiers[3]]
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def extract_dedicated_settings(config)
|
|
77
|
+
{
|
|
78
|
+
f0l_dedicated: DEDICATED_FUNCTIONS[config.dedicated_buttons[0]],
|
|
79
|
+
f0m_dedicated: DEDICATED_FUNCTIONS[config.dedicated_buttons[1]],
|
|
80
|
+
f0r_dedicated: DEDICATED_FUNCTIONS[config.dedicated_buttons[2]],
|
|
81
|
+
t0_dedicated: DEDICATED_FUNCTIONS[config.dedicated_buttons[3]]
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def format_value(value)
|
|
86
|
+
case value
|
|
87
|
+
when true then "true"
|
|
88
|
+
when false then "false"
|
|
89
|
+
when Symbol then value.to_s
|
|
90
|
+
else value.to_s
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
module Twiddling
|
|
2
|
+
module V7
|
|
3
|
+
module Tw7
|
|
4
|
+
# Parses the settings section of a .tw7 file into Config attributes.
|
|
5
|
+
module SettingsParser
|
|
6
|
+
include ConfigConstants
|
|
7
|
+
|
|
8
|
+
THUMB_MODIFIER_NAMES = THUMB_MODIFIERS.to_h { |code, name| [name.to_s, code] }.freeze
|
|
9
|
+
DEDICATED_NAMES = DEDICATED_FUNCTIONS.to_h { |code, name| [name.to_s, code] }.freeze
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# Returns a hash of Config-compatible attributes from settings lines.
|
|
14
|
+
# Missing settings use defaults from the empty.cfg baseline.
|
|
15
|
+
def parse(lines)
|
|
16
|
+
raw = parse_lines(lines)
|
|
17
|
+
build_attrs(raw)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def parse_lines(lines)
|
|
23
|
+
lines.each_with_object({}) do |line, hash|
|
|
24
|
+
key, value = line.split(":", 2).map(&:strip)
|
|
25
|
+
hash[key] = value
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def build_attrs(raw) # rubocop:disable Metrics/AbcSize
|
|
30
|
+
{
|
|
31
|
+
idle_time: int_val(raw, "idle_time", 600),
|
|
32
|
+
key_repeat: int_val(raw, "key_repeat_delay", 100),
|
|
33
|
+
flags_1: build_flags_1(raw),
|
|
34
|
+
flags_2: build_flags_2(raw),
|
|
35
|
+
thumb_modifiers: build_thumb_modifiers(raw),
|
|
36
|
+
dedicated_buttons: build_dedicated(raw)
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def build_flags_1(raw)
|
|
41
|
+
flags = 0
|
|
42
|
+
flags |= 0x01 if bool_val(raw, "key_repeat", true)
|
|
43
|
+
flags |= 0x02 if bool_val(raw, "keyboard_mode", false)
|
|
44
|
+
flags |= 0x08 if bool_val(raw, "haptic", true)
|
|
45
|
+
flags
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def build_flags_2(raw)
|
|
49
|
+
sensitivity = int_val(raw, "nav_sensitivity", 4)
|
|
50
|
+
invert_x = bool_val(raw, "nav_invert_x", false) ? 1 : 0
|
|
51
|
+
direction = int_val(raw, "nav_direction", 0)
|
|
52
|
+
(sensitivity << 3) | (invert_x << 2) | direction
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def build_thumb_modifiers(raw)
|
|
56
|
+
[
|
|
57
|
+
thumb_val(raw, "t1_modifier", :none),
|
|
58
|
+
thumb_val(raw, "t2_modifier", :l_option),
|
|
59
|
+
thumb_val(raw, "t3_modifier", :l_control),
|
|
60
|
+
thumb_val(raw, "t4_modifier", :l_shift)
|
|
61
|
+
]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def build_dedicated(raw)
|
|
65
|
+
[
|
|
66
|
+
dedicated_val(raw, "f0l_dedicated", :mouse_right),
|
|
67
|
+
dedicated_val(raw, "f0m_dedicated", :mouse_middle),
|
|
68
|
+
dedicated_val(raw, "f0r_dedicated", :mouse_left),
|
|
69
|
+
dedicated_val(raw, "t0_dedicated", :mouse_left)
|
|
70
|
+
]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def int_val(raw, key, default)
|
|
74
|
+
raw.key?(key) ? raw[key].to_i : default
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def bool_val(raw, key, default)
|
|
78
|
+
raw.key?(key) ? raw[key] == "true" : default
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def thumb_val(raw, key, default)
|
|
82
|
+
name = raw.key?(key) ? raw[key] : default.to_s
|
|
83
|
+
THUMB_MODIFIER_NAMES[name.to_s] || raise(ArgumentError, "Unknown thumb modifier: #{name}")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def dedicated_val(raw, key, default)
|
|
87
|
+
name = raw.key?(key) ? raw[key] : default.to_s
|
|
88
|
+
DEDICATED_NAMES[name.to_s] || raise(ArgumentError, "Unknown dedicated function: #{name}")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
module_function :parse_lines, :build_attrs, :build_flags_1, :build_flags_2,
|
|
92
|
+
:build_thumb_modifiers, :build_dedicated, :int_val, :bool_val,
|
|
93
|
+
:thumb_val, :dedicated_val
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
module Twiddling
|
|
2
|
+
module V7
|
|
3
|
+
class Validator
|
|
4
|
+
Error = Struct.new(:field, :message, keyword_init: true) do
|
|
5
|
+
def to_s = "#{field}: #{message}"
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
ValidationError = Class.new(Twiddling::Error)
|
|
9
|
+
|
|
10
|
+
def initialize(config)
|
|
11
|
+
@config = config
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def validate
|
|
15
|
+
errors = []
|
|
16
|
+
check_format_version(errors)
|
|
17
|
+
check_chord_count(errors)
|
|
18
|
+
check_sorted_bitmasks(errors)
|
|
19
|
+
check_no_duplicate_bitmasks(errors)
|
|
20
|
+
check_string_table_offsets(errors)
|
|
21
|
+
errors
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def validate!
|
|
25
|
+
errors = validate
|
|
26
|
+
return if errors.empty?
|
|
27
|
+
|
|
28
|
+
messages = errors.map(&:to_s).join("; ")
|
|
29
|
+
raise ValidationError, messages
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def check_format_version(errors)
|
|
35
|
+
return if @config.format_version == 7
|
|
36
|
+
|
|
37
|
+
errors << Error.new(
|
|
38
|
+
field: :format_version,
|
|
39
|
+
message: "expected 7, got #{@config.format_version}"
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def check_chord_count(errors)
|
|
44
|
+
return if @config.chord_count <= 0xFFFF
|
|
45
|
+
|
|
46
|
+
errors << Error.new(
|
|
47
|
+
field: :chord_count,
|
|
48
|
+
message: "exceeds u16 max (#{@config.chord_count})"
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def check_sorted_bitmasks(errors)
|
|
53
|
+
bitmasks = @config.chords.map(&:bitmask)
|
|
54
|
+
return if bitmasks == bitmasks.sort
|
|
55
|
+
|
|
56
|
+
errors << Error.new(
|
|
57
|
+
field: :chords,
|
|
58
|
+
message: "chords are not sorted by bitmask"
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def check_no_duplicate_bitmasks(errors)
|
|
63
|
+
seen = {}
|
|
64
|
+
@config.chords.each_with_index do |chord, idx|
|
|
65
|
+
if seen[chord.bitmask]
|
|
66
|
+
errors << Error.new(
|
|
67
|
+
field: :chords,
|
|
68
|
+
message: "duplicate bitmask 0x%08x at indices %d and %d" %
|
|
69
|
+
[chord.bitmask, seen[chord.bitmask], idx]
|
|
70
|
+
)
|
|
71
|
+
else
|
|
72
|
+
seen[chord.bitmask] = idx
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def check_string_table_offsets(errors)
|
|
78
|
+
multichar_chords = @config.chords.select { |c| c.type_name == :multichar }
|
|
79
|
+
return if multichar_chords.empty?
|
|
80
|
+
|
|
81
|
+
multichar_chords.each do |chord|
|
|
82
|
+
next if chord.string_keys && !chord.string_keys.empty?
|
|
83
|
+
|
|
84
|
+
errors << Error.new(
|
|
85
|
+
field: :chords,
|
|
86
|
+
message: "multi-char chord (bitmask 0x%08x) has no string keys" %
|
|
87
|
+
chord.bitmask
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Twiddling
|
|
2
|
+
module V7
|
|
3
|
+
module Writer
|
|
4
|
+
# Serializes a V7::Chord back to 8 bytes of binary data.
|
|
5
|
+
#
|
|
6
|
+
# For multi-char chords, a string_table_offset must be provided
|
|
7
|
+
# to encode the correct byte offset into the modifier_type field.
|
|
8
|
+
# For other chord types, modifier_type is written as-is.
|
|
9
|
+
class Chord
|
|
10
|
+
include ChordConstants
|
|
11
|
+
|
|
12
|
+
def initialize(chord, string_table_offset: nil)
|
|
13
|
+
@chord = chord
|
|
14
|
+
@string_table_offset = string_table_offset
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_binary
|
|
18
|
+
[@chord.bitmask, effective_modifier_type, @chord.keycode].pack("Vvv")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
# For multi-char chords being written with a new offset,
|
|
24
|
+
# rebuild modifier_type from the type byte + offset.
|
|
25
|
+
# Otherwise, preserve the original modifier_type.
|
|
26
|
+
def effective_modifier_type
|
|
27
|
+
if @string_table_offset
|
|
28
|
+
TYPE_MULTICHAR | (@string_table_offset << 8)
|
|
29
|
+
else
|
|
30
|
+
@chord.modifier_type
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module Twiddling
|
|
2
|
+
module V7
|
|
3
|
+
module Writer
|
|
4
|
+
# Serializes a V7::Config back to the v7 binary format.
|
|
5
|
+
#
|
|
6
|
+
# Produces three sections concatenated:
|
|
7
|
+
# 1. 128-byte header
|
|
8
|
+
# 2. 8-byte chord entries
|
|
9
|
+
# 3. String table (for multi-char chords)
|
|
10
|
+
#
|
|
11
|
+
# String table offsets in multi-char chord entries are
|
|
12
|
+
# recomputed during serialization to match the output layout.
|
|
13
|
+
class Config
|
|
14
|
+
include ConfigConstants
|
|
15
|
+
|
|
16
|
+
def initialize(config)
|
|
17
|
+
@config = config
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_binary
|
|
21
|
+
header_binary + chords_binary + string_table_binary
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def header_binary
|
|
27
|
+
header_fields + @config.reserved_10 + settings_binary + index_table_field
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def header_fields
|
|
31
|
+
[
|
|
32
|
+
@config.version, @config.format_version,
|
|
33
|
+
@config.flags_1, @config.flags_2, @config.flags_3,
|
|
34
|
+
@config.chord_count, @config.idle_time,
|
|
35
|
+
@config.key_repeat, @config.reserved_0e
|
|
36
|
+
].pack("VCCCCvvvv")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def settings_binary
|
|
40
|
+
Writer::Settings.new(@config.settings).to_binary
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def index_table_field
|
|
44
|
+
@config.index_table.pack("C32")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def chords_binary
|
|
48
|
+
offsets = compute_string_offsets
|
|
49
|
+
@config.chords.each_with_index.map { |chord, i|
|
|
50
|
+
Writer::Chord.new(chord, string_table_offset: offsets[i]).to_binary
|
|
51
|
+
}.join
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def string_table_binary
|
|
55
|
+
Writer::StringTable.new(build_string_table).to_binary
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def build_string_table
|
|
59
|
+
entries = []
|
|
60
|
+
offset = 0
|
|
61
|
+
@config.chords.each do |chord|
|
|
62
|
+
next unless chord.string_keys
|
|
63
|
+
entries << StringTableEntry.new(keys: chord.string_keys, byte_offset: offset)
|
|
64
|
+
offset += (chord.string_keys.length + 1) * 4
|
|
65
|
+
end
|
|
66
|
+
V7::StringTable.new(entries)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def compute_string_offsets
|
|
70
|
+
table = build_string_table
|
|
71
|
+
@config.chords.map do |chord|
|
|
72
|
+
next unless chord.string_keys
|
|
73
|
+
table.entries.find { |e| e.keys == chord.string_keys }&.byte_offset
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Twiddling
|
|
2
|
+
module V7
|
|
3
|
+
module Writer
|
|
4
|
+
# Serializes a V7::Settings back to 32 bytes of binary data
|
|
5
|
+
# for the config header (offsets 0x40-0x5F).
|
|
6
|
+
class Settings
|
|
7
|
+
def initialize(settings)
|
|
8
|
+
@settings = settings
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def to_binary
|
|
12
|
+
@settings.thumb_modifiers.pack("V4") +
|
|
13
|
+
@settings.dedicated_buttons.pack("C4") +
|
|
14
|
+
@settings.reserved
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Twiddling
|
|
2
|
+
module V7
|
|
3
|
+
module Writer
|
|
4
|
+
# Serializes a V7::StringTable back to the binary format used
|
|
5
|
+
# at the end of a v7 config file.
|
|
6
|
+
#
|
|
7
|
+
# Each entry becomes a sequence of (modifier u16 LE, hid_code
|
|
8
|
+
# u16 LE) pairs followed by a null terminator (0x0000, 0x0000).
|
|
9
|
+
# Entries are concatenated in order - their byte offsets in the
|
|
10
|
+
# output match the offsets stored on each StringTableEntry.
|
|
11
|
+
class StringTable
|
|
12
|
+
def initialize(string_table)
|
|
13
|
+
@string_table = string_table
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Returns the binary string table as a packed byte string.
|
|
17
|
+
def to_binary
|
|
18
|
+
@string_table.entries
|
|
19
|
+
.flat_map { |entry| encode_entry(entry) }
|
|
20
|
+
.pack("v*")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# Encodes a single entry as a flat array of u16 values:
|
|
26
|
+
# [mod1, hid1, mod2, hid2, ..., 0, 0]
|
|
27
|
+
def encode_entry(entry)
|
|
28
|
+
encode_keys(entry.keys) + null_terminator
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Flattens key hashes into [modifier, hid_code, ...] pairs.
|
|
32
|
+
def encode_keys(keys)
|
|
33
|
+
keys.flat_map { |sk| [sk[:modifier], sk[:hid_code]] }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def null_terminator = [0, 0]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/twiddling/v7.rb
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Twiddling
|
|
2
|
+
module V7
|
|
3
|
+
module Reader
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
module Writer
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
require_relative "v7/string_table_entry"
|
|
12
|
+
require_relative "v7/string_table"
|
|
13
|
+
require_relative "v7/chord_constants"
|
|
14
|
+
require_relative "v7/config_constants"
|
|
15
|
+
require_relative "v7/chord"
|
|
16
|
+
require_relative "v7/settings"
|
|
17
|
+
require_relative "v7/validator"
|
|
18
|
+
require_relative "v7/config"
|
|
19
|
+
require_relative "v7/reader/string_table"
|
|
20
|
+
require_relative "v7/reader/chord"
|
|
21
|
+
require_relative "v7/reader/settings"
|
|
22
|
+
require_relative "v7/reader/config"
|
|
23
|
+
require_relative "v7/writer/string_table"
|
|
24
|
+
require_relative "v7/writer/chord"
|
|
25
|
+
require_relative "v7/writer/settings"
|
|
26
|
+
require_relative "v7/writer/config"
|
|
27
|
+
require_relative "v7/tw7/button_formatter"
|
|
28
|
+
require_relative "v7/tw7/button_parser"
|
|
29
|
+
require_relative "v7/tw7/effect_formatter"
|
|
30
|
+
require_relative "v7/tw7/effect_parser"
|
|
31
|
+
require_relative "v7/tw7/settings_formatter"
|
|
32
|
+
require_relative "v7/tw7/settings_parser"
|
|
33
|
+
require_relative "v7/tw7/printer"
|
|
34
|
+
require_relative "v7/tw7/parser"
|
data/lib/twiddling.rb
ADDED
data/tmp/.gitkeep
ADDED
|
File without changes
|
data/twiddling.gemspec
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
require_relative "lib/twiddling/version"
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = "twiddling"
|
|
5
|
+
spec.version = Twiddling::VERSION
|
|
6
|
+
spec.authors = ["Eric Mueller"]
|
|
7
|
+
spec.email = ["nevinera@gmail.com"]
|
|
8
|
+
|
|
9
|
+
spec.summary = "A cli tool for reading/writing/editing Twiddler v7 configuration files"
|
|
10
|
+
spec.description = <<~DESC
|
|
11
|
+
Edit twiddler4 (v7) configuration files as text, rather than using the clunky online tool.
|
|
12
|
+
|
|
13
|
+
Adding a full configuration by hand is _extremely_ tedious, and you shouldn't have to do i.
|
|
14
|
+
DESC
|
|
15
|
+
spec.homepage = "https://github.com/nevinera/twiddling"
|
|
16
|
+
spec.license = "MIT"
|
|
17
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 3.3.8")
|
|
18
|
+
|
|
19
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
20
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
21
|
+
|
|
22
|
+
spec.require_paths = ["lib"]
|
|
23
|
+
spec.bindir = "bin"
|
|
24
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
25
|
+
`git ls-files -z`
|
|
26
|
+
.split("\x0")
|
|
27
|
+
.reject { |f| f.start_with?("spec") }
|
|
28
|
+
end
|
|
29
|
+
spec.executables = Dir.chdir(File.expand_path(__dir__)) do
|
|
30
|
+
`git ls-files -z bin/`
|
|
31
|
+
.split("\x0")
|
|
32
|
+
.map { |path| path.sub(/^bin\//, "") }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
spec.add_development_dependency "rspec", "~> 3.13"
|
|
36
|
+
spec.add_development_dependency "rspec-its", "~> 1.3"
|
|
37
|
+
spec.add_development_dependency "simplecov", "~> 0.22.0"
|
|
38
|
+
spec.add_development_dependency "pry", "~> 0.14"
|
|
39
|
+
spec.add_development_dependency "standard", ">= 1.35.1"
|
|
40
|
+
spec.add_development_dependency "rubocop", ">= 1.62"
|
|
41
|
+
spec.add_development_dependency "debug", "~> 1.7"
|
|
42
|
+
spec.add_development_dependency "mdl", "~> 0.12"
|
|
43
|
+
spec.add_development_dependency "quiet_quality", "~> 1.5"
|
|
44
|
+
|
|
45
|
+
spec.add_dependency "bindata", "~> 2.5"
|
|
46
|
+
end
|