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
data/formats/v7-cfg.md
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# Twiddler 4 Binary Configuration Format (v7)
|
|
2
|
+
|
|
3
|
+
The Twiddler 4 uses a binary `.cfg` format for its configuration files.
|
|
4
|
+
The format is informally called "v7", after the format version byte in
|
|
5
|
+
the flags field.
|
|
6
|
+
|
|
7
|
+
## Credits
|
|
8
|
+
|
|
9
|
+
The binary format was originally reverse-engineered by the
|
|
10
|
+
[nchorder](https://github.com/GlassOnTin/nchorder) project
|
|
11
|
+
([spec](https://github.com/GlassOnTin/nchorder/blob/master/docs/twiddler4/06-CONFIG_FORMAT.md)).
|
|
12
|
+
This documentation builds on that work, validated and extended using
|
|
13
|
+
configs produced by the official
|
|
14
|
+
[Twiddler tuner](https://tuner.mytwiddler.com).
|
|
15
|
+
|
|
16
|
+
## File structure
|
|
17
|
+
|
|
18
|
+
A v7 config file has three sections laid out sequentially:
|
|
19
|
+
|
|
20
|
+
1. **Header** - 128 bytes (offsets 0x00-0x7F)
|
|
21
|
+
1. **Chord entries** - 8 bytes each (starting at offset 0x80)
|
|
22
|
+
1. **String table** - variable length, immediately after the last chord
|
|
23
|
+
|
|
24
|
+
The string table begins at offset `128 + (chord_count * 8)`. There is no
|
|
25
|
+
pointer to it in the header - its location is computed from the chord
|
|
26
|
+
count.
|
|
27
|
+
|
|
28
|
+
All multi-byte integers are little-endian.
|
|
29
|
+
|
|
30
|
+
## Header (128 bytes)
|
|
31
|
+
|
|
32
|
+
| Offset | Size | Type | Field | Description |
|
|
33
|
+
|--------|------|------|-------|-------------|
|
|
34
|
+
| 0x00 | 4 | u32 | version | Always 0 in observed configs |
|
|
35
|
+
| 0x04 | 1 | u8 | format_version | Always 7 (the "v7" in the format name) |
|
|
36
|
+
| 0x05 | 1 | u8 | flags_1 | See [Flags byte 1](#flags-byte-1) |
|
|
37
|
+
| 0x06 | 1 | u8 | flags_2 | See [Flags byte 2](#flags-byte-2) |
|
|
38
|
+
| 0x07 | 1 | u8 | flags_3 | Always 0x00 in observed configs |
|
|
39
|
+
| 0x08 | 2 | u16 | chord_count | Number of chord entries |
|
|
40
|
+
| 0x0A | 2 | u16 | idle_time | Seconds until sleep (default 600 = 10 min) |
|
|
41
|
+
| 0x0C | 2 | u16 | key_repeat | Repeat threshold in 10ms units (default 100) |
|
|
42
|
+
| 0x0E | 2 | - | reserved_0e | Always 0 in observed configs |
|
|
43
|
+
| 0x10 | 48 | - | reserved_10 | Always all zeros |
|
|
44
|
+
| 0x40 | 4 | u32 | thumb_t1_modifier | Thumb button T1 modifier assignment |
|
|
45
|
+
| 0x44 | 4 | u32 | thumb_t2_modifier | Thumb button T2 modifier assignment |
|
|
46
|
+
| 0x48 | 4 | u32 | thumb_t3_modifier | Thumb button T3 modifier assignment |
|
|
47
|
+
| 0x4C | 4 | u32 | thumb_t4_modifier | Thumb button T4 modifier assignment |
|
|
48
|
+
| 0x50 | 1 | u8 | dedicated_f0l | Dedicated button function for F0L |
|
|
49
|
+
| 0x51 | 1 | u8 | dedicated_f0m | Dedicated button function for F0M |
|
|
50
|
+
| 0x52 | 1 | u8 | dedicated_f0r | Dedicated button function for F0R |
|
|
51
|
+
| 0x53 | 1 | u8 | dedicated_t0 | Dedicated button function for T0 |
|
|
52
|
+
| 0x54 | 12 | - | reserved_54 | Always all zeros |
|
|
53
|
+
| 0x60 | 32 | u8[32] | index_table | See [Index table](#index-table) |
|
|
54
|
+
|
|
55
|
+
### Flags byte 1 (offset 0x05)
|
|
56
|
+
|
|
57
|
+
| Bit | Mask | Meaning | Default |
|
|
58
|
+
|-----|------|---------|---------|
|
|
59
|
+
| 0 | 0x01 | Key repeat enabled | 1 (enabled) |
|
|
60
|
+
| 1 | 0x02 | Button mode: keyboard | 0 (chord mode) |
|
|
61
|
+
| 3 | 0x08 | Haptic feedback enabled | 1 (enabled) |
|
|
62
|
+
|
|
63
|
+
Default value: 0x09 (key repeat + haptic enabled).
|
|
64
|
+
|
|
65
|
+
### Flags byte 2 (offset 0x06)
|
|
66
|
+
|
|
67
|
+
This byte encodes navigation settings as a composite value:
|
|
68
|
+
|
|
69
|
+
```text
|
|
70
|
+
Bits 7-3: Nav sensitivity (0-31, default 4)
|
|
71
|
+
Bit 2: Invert X axis (0 = normal, 1 = inverted)
|
|
72
|
+
Bits 1-0: Nav-up direction (0 = north, 1 = east, ...)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The byte value is: `(sensitivity << 3) | (invert_x << 2) | direction`
|
|
76
|
+
|
|
77
|
+
| Example | Value | Sensitivity | Invert X | Direction |
|
|
78
|
+
|---------|-------|-------------|----------|-----------|
|
|
79
|
+
| Default | 0x20 | 4 | no | north |
|
|
80
|
+
| Sensitivity min | 0x00 | 0 | no | north |
|
|
81
|
+
| Invert X | 0x24 | 4 | yes | north |
|
|
82
|
+
| Nav-up east | 0x21 | 4 | no | east |
|
|
83
|
+
|
|
84
|
+
### Thumb modifier assignments (offsets 0x40-0x4F)
|
|
85
|
+
|
|
86
|
+
Each thumb button can be assigned a modifier key. Stored as four u32 LE
|
|
87
|
+
values (one per thumb button, T1-T4):
|
|
88
|
+
|
|
89
|
+
| Code | Modifier |
|
|
90
|
+
|------|----------|
|
|
91
|
+
| 0 | None (default for T1) |
|
|
92
|
+
| 1 | LControl (default for T3) |
|
|
93
|
+
| 2 | LShift (default for T4) |
|
|
94
|
+
| 3 | LOption / Alt (default for T2) |
|
|
95
|
+
| 4 | LCommand / Windows |
|
|
96
|
+
|
|
97
|
+
Default assignments: T1=None(0), T2=LOption(3), T3=LControl(1), T4=LShift(2).
|
|
98
|
+
|
|
99
|
+
### Dedicated button functions (offsets 0x50-0x53)
|
|
100
|
+
|
|
101
|
+
Each of the four special buttons (three mini-buttons + T0) can have a
|
|
102
|
+
dedicated function that fires on press without requiring a chord. Stored
|
|
103
|
+
as four u8 values:
|
|
104
|
+
|
|
105
|
+
| Offset | Button | Default | Function |
|
|
106
|
+
|--------|--------|---------|----------|
|
|
107
|
+
| 0x50 | F0L | 0x0a | Mouse Button Right |
|
|
108
|
+
| 0x51 | F0M | 0x0b | Mouse Button Middle |
|
|
109
|
+
| 0x52 | F0R | 0x09 | Mouse Button Left |
|
|
110
|
+
| 0x53 | T0 | 0x09 | Mouse Button Left |
|
|
111
|
+
|
|
112
|
+
A value of 0x00 means no dedicated function.
|
|
113
|
+
|
|
114
|
+
Known function codes:
|
|
115
|
+
|
|
116
|
+
| Code | Function |
|
|
117
|
+
|------|----------|
|
|
118
|
+
| 0x00 | None (disabled) |
|
|
119
|
+
| 0x09 | Mouse Button Left |
|
|
120
|
+
| 0x0a | Mouse Button Right |
|
|
121
|
+
| 0x0b | Mouse Button Middle |
|
|
122
|
+
|
|
123
|
+
Note: These codes differ from the chord device function codes.
|
|
124
|
+
|
|
125
|
+
### Index table (offset 0x60, 32 bytes)
|
|
126
|
+
|
|
127
|
+
A lookup table for fast chord matching. Each of the 32 entries
|
|
128
|
+
corresponds to the low 5 bits of a chord's bitmask (the "prefix").
|
|
129
|
+
The value at each entry is the index of the first chord in the sorted
|
|
130
|
+
chord array that has that prefix. A value of 0x80 means no chords exist
|
|
131
|
+
with that prefix.
|
|
132
|
+
|
|
133
|
+
Chords **must** be sorted by bitmask in ascending order for the index
|
|
134
|
+
table to function correctly. The index table must be recomputed whenever
|
|
135
|
+
chords are added or removed.
|
|
136
|
+
|
|
137
|
+
## Chord entry (8 bytes)
|
|
138
|
+
|
|
139
|
+
| Offset | Size | Type | Field |
|
|
140
|
+
|--------|------|------|-------|
|
|
141
|
+
| 0 | 4 | u32 LE | bitmask |
|
|
142
|
+
| 4 | 2 | u16 LE | modifier_type |
|
|
143
|
+
| 6 | 2 | u16 LE | keycode |
|
|
144
|
+
|
|
145
|
+
### Bitmask
|
|
146
|
+
|
|
147
|
+
The bitmask encodes which physical buttons are pressed. The Twiddler 4
|
|
148
|
+
has 21 buttons: 5 thumb buttons (T0-T4), 15 finger buttons (rows 0-4,
|
|
149
|
+
columns L/M/R), plus a mouse-mode flag.
|
|
150
|
+
|
|
151
|
+
| Bit | Button | Bit | Button | Bit | Button |
|
|
152
|
+
|-----|--------|-----|--------|-----|--------|
|
|
153
|
+
| 0 | T1 | 8 | T3 | 16 | F0L |
|
|
154
|
+
| 1 | F1R | 9 | F3R | 17 | F0M |
|
|
155
|
+
| 2 | F1M | 10 | F3M | 18 | F0R |
|
|
156
|
+
| 3 | F1L | 11 | F3L | 19 | mouse-mode-only |
|
|
157
|
+
| 4 | T2 | 12 | T4 | | |
|
|
158
|
+
| 5 | F2R | 13 | F4R | | |
|
|
159
|
+
| 6 | F2M | 14 | F4M | | |
|
|
160
|
+
| 7 | F2L | 15 | F4L | | |
|
|
161
|
+
|
|
162
|
+
Bits 0-18 encode physical buttons. Bit 19 is a flag: when set, the
|
|
163
|
+
chord is only active in mouse mode. Bits 20-31 are unused (always 0 in
|
|
164
|
+
observed configs). T0 does not appear in the bitmask - it only has a
|
|
165
|
+
dedicated button function.
|
|
166
|
+
|
|
167
|
+
The buttons are named using
|
|
168
|
+
[T4 chord notation](https://www.mytwiddler.com/doc/doku.php?id=chordnotation):
|
|
169
|
+
|
|
170
|
+
- Thumb buttons: `T1`, `T2`, `T3`, `T4`
|
|
171
|
+
- Finger buttons: `F<row><L|M|R>` (e.g. `F1R`, `F2L`, `F0M`)
|
|
172
|
+
- Row 0 is the mini-buttons above the main finger pad
|
|
173
|
+
|
|
174
|
+
### Modifier/type field
|
|
175
|
+
|
|
176
|
+
The low byte encodes the chord type. The high byte's meaning depends on
|
|
177
|
+
the type:
|
|
178
|
+
|
|
179
|
+
| Type | Low byte | High byte meaning |
|
|
180
|
+
|------|----------|-------------------|
|
|
181
|
+
| Device function | 0x01 | Function code |
|
|
182
|
+
| Keyboard | 0x02 | Modifier key bits |
|
|
183
|
+
| Multi-char string | 0x07 | String table byte offset |
|
|
184
|
+
|
|
185
|
+
#### Keyboard modifier bits (high byte, type 0x02)
|
|
186
|
+
|
|
187
|
+
| Bit | Mask | Modifier |
|
|
188
|
+
|-----|------|----------|
|
|
189
|
+
| 0 | 0x01 | Ctrl |
|
|
190
|
+
| 2 | 0x04 | Alt |
|
|
191
|
+
| 3 | 0x08 | Cmd / Windows |
|
|
192
|
+
| 5 | 0x20 | Shift |
|
|
193
|
+
|
|
194
|
+
#### Device function codes (high byte, type 0x01)
|
|
195
|
+
|
|
196
|
+
| Code | Function |
|
|
197
|
+
|------|----------|
|
|
198
|
+
| 0x01 | Mouse mode toggle |
|
|
199
|
+
| 0x02 | Left click |
|
|
200
|
+
| 0x04 | Scroll mode toggle |
|
|
201
|
+
| 0x05 | Speed decrease |
|
|
202
|
+
| 0x06 | Speed cycle |
|
|
203
|
+
| 0x0a | Middle click |
|
|
204
|
+
| 0x0b | Speed increase |
|
|
205
|
+
| 0x0c | Right click |
|
|
206
|
+
| 0x0d | Print stats |
|
|
207
|
+
| 0x0e | Config cycle |
|
|
208
|
+
|
|
209
|
+
The keycode field is 0x0000 for device function chords.
|
|
210
|
+
|
|
211
|
+
#### Multi-char string (type 0x07)
|
|
212
|
+
|
|
213
|
+
The high byte of the modifier_type field is the byte offset into the
|
|
214
|
+
string table where the character sequence for this chord begins. The
|
|
215
|
+
keycode field is 0x0000.
|
|
216
|
+
|
|
217
|
+
### Keycode
|
|
218
|
+
|
|
219
|
+
For keyboard chords (type 0x02), this is a standard USB HID keycode:
|
|
220
|
+
|
|
221
|
+
| Range | Keys |
|
|
222
|
+
|-------|------|
|
|
223
|
+
| 0x04-0x1D | a-z |
|
|
224
|
+
| 0x1E-0x27 | 1-0 (number row) |
|
|
225
|
+
| 0x28 | Enter |
|
|
226
|
+
| 0x29 | Escape |
|
|
227
|
+
| 0x2A | Backspace |
|
|
228
|
+
| 0x2B | Tab |
|
|
229
|
+
| 0x2C | Space |
|
|
230
|
+
| 0x2D-0x38 | Punctuation: - = [ ] \ ; ' ` , . / |
|
|
231
|
+
| 0x39 | Caps Lock |
|
|
232
|
+
| 0x3A-0x45 | F1-F12 |
|
|
233
|
+
| 0x49 | Insert |
|
|
234
|
+
| 0x4A | Home |
|
|
235
|
+
| 0x4B | Page Up |
|
|
236
|
+
| 0x4C | Delete |
|
|
237
|
+
| 0x4D | End |
|
|
238
|
+
| 0x4E | Page Down |
|
|
239
|
+
| 0x4F-0x52 | Right, Left, Down, Up arrows |
|
|
240
|
+
| 0x53 | Num Lock |
|
|
241
|
+
|
|
242
|
+
## String table
|
|
243
|
+
|
|
244
|
+
The string table stores character sequences for multi-char chords. It
|
|
245
|
+
immediately follows the chord entries (no header pointer - the location
|
|
246
|
+
is computed from the chord count).
|
|
247
|
+
|
|
248
|
+
Each entry is a sequence of (modifier u16 LE, HID keycode u16 LE) pairs
|
|
249
|
+
terminated by a null pair (0x0000, 0x0000):
|
|
250
|
+
|
|
251
|
+
```text
|
|
252
|
+
[mod1:u16][key1:u16][mod2:u16][key2:u16]...[0x0000][0x0000]
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
The modifier values use the same encoding as keyboard chord modifier_type
|
|
256
|
+
fields: 0x0002 for an unmodified key, 0x2002 for Shift, etc.
|
|
257
|
+
|
|
258
|
+
### Example: "test"
|
|
259
|
+
|
|
260
|
+
```text
|
|
261
|
+
Offset 0: (0x0002, 0x0017) t
|
|
262
|
+
Offset 4: (0x0002, 0x0008) e
|
|
263
|
+
Offset 8: (0x0002, 0x0016) s
|
|
264
|
+
Offset C: (0x0002, 0x0017) t
|
|
265
|
+
Offset 10: (0x0000, 0x0000) terminator
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
The chord entry's modifier_type high byte would be 0x00 (byte offset 0
|
|
269
|
+
into the string table), giving a full modifier_type of 0x0007.
|
|
270
|
+
|
|
271
|
+
A second string entry would start at offset 0x14 (20 bytes), and its
|
|
272
|
+
chord's modifier_type would be 0x1407.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module Twiddling
|
|
2
|
+
module Cli
|
|
3
|
+
# twiddling convert <input> <output>
|
|
4
|
+
#
|
|
5
|
+
# Converts between .cfg and .tw7 formats. The direction is
|
|
6
|
+
# determined by the file extensions.
|
|
7
|
+
class Convert
|
|
8
|
+
VALID_EXTS = %w[.cfg .tw7].freeze
|
|
9
|
+
|
|
10
|
+
HELP_TEXT = <<~TEXT
|
|
11
|
+
Usage: twiddling convert <input> <output>
|
|
12
|
+
|
|
13
|
+
Converts between .cfg (binary) and .tw7 (text) formats.
|
|
14
|
+
The direction is determined by the file extensions.
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
twiddling convert my_config.cfg layout.tw7
|
|
18
|
+
twiddling convert layout.tw7 my_config.cfg
|
|
19
|
+
TEXT
|
|
20
|
+
|
|
21
|
+
def initialize(argv:, stdout: $stdout, stderr: $stderr)
|
|
22
|
+
@argv = argv
|
|
23
|
+
@stdout = stdout
|
|
24
|
+
@stderr = stderr
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def run
|
|
28
|
+
return @stdout.puts(HELP_TEXT) if help?
|
|
29
|
+
validate!
|
|
30
|
+
write_output
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def input_path = @argv[0]
|
|
36
|
+
|
|
37
|
+
def output_path = @argv[1]
|
|
38
|
+
|
|
39
|
+
def help? = @argv.include?("-h") || @argv.include?("--help")
|
|
40
|
+
|
|
41
|
+
def validate!
|
|
42
|
+
raise ExitException, HELP_TEXT unless input_path && output_path
|
|
43
|
+
raise ExitException, "File not found: #{input_path}" unless File.exist?(input_path)
|
|
44
|
+
validate_ext!(input_path)
|
|
45
|
+
validate_ext!(output_path)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def validate_ext!(file)
|
|
49
|
+
return if VALID_EXTS.include?(File.extname(file))
|
|
50
|
+
|
|
51
|
+
raise ExitException, "Unsupported file type: #{file} (expected .cfg or .tw7)"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def config
|
|
55
|
+
@config ||= case File.extname(input_path)
|
|
56
|
+
when ".cfg" then V7::Config.from_file(input_path)
|
|
57
|
+
when ".tw7" then V7::Tw7::Parser.new(File.read(input_path)).parse
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def write_output
|
|
62
|
+
case File.extname(output_path)
|
|
63
|
+
when ".cfg" then config.write(output_path)
|
|
64
|
+
when ".tw7"
|
|
65
|
+
File.open(output_path, "w") { |f| V7::Tw7::Printer.new(config, io: f).print }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
require "optparse"
|
|
2
|
+
|
|
3
|
+
module Twiddling
|
|
4
|
+
module Cli
|
|
5
|
+
# twiddling diff <file_a> <file_b> [--no-color]
|
|
6
|
+
#
|
|
7
|
+
# Shows differences between two configs: settings changes,
|
|
8
|
+
# removed chords, changed chords, and added chords.
|
|
9
|
+
class Diff
|
|
10
|
+
READABLE_EXTS = %w[.cfg .tw7].freeze
|
|
11
|
+
|
|
12
|
+
HELP_TEXT = <<~TEXT
|
|
13
|
+
Usage: twiddling diff <file_a> <file_b> [--no-color]
|
|
14
|
+
|
|
15
|
+
Shows differences between two Twiddler configs.
|
|
16
|
+
|
|
17
|
+
Prints changed settings, then removed, changed, and added
|
|
18
|
+
chords. Output is colorized by default (red=removed,
|
|
19
|
+
yellow=changed, green=added).
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
twiddling diff old.cfg new.cfg
|
|
23
|
+
twiddling diff base.tw7 mine.tw7 --no-color
|
|
24
|
+
TEXT
|
|
25
|
+
|
|
26
|
+
def initialize(argv:, stdout: $stdout, stderr: $stderr)
|
|
27
|
+
@argv = argv
|
|
28
|
+
@stdout = stdout
|
|
29
|
+
@stderr = stderr
|
|
30
|
+
@color = true
|
|
31
|
+
parse_flags!
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def run
|
|
35
|
+
return @stdout.puts(HELP_TEXT) if help?
|
|
36
|
+
validate!
|
|
37
|
+
print_diff
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def path_a = @positional[0]
|
|
43
|
+
|
|
44
|
+
def path_b = @positional[1]
|
|
45
|
+
|
|
46
|
+
def help? = @argv.include?("-h") || @argv.include?("--help")
|
|
47
|
+
|
|
48
|
+
def parse_flags!
|
|
49
|
+
@positional = []
|
|
50
|
+
@argv.each do |arg|
|
|
51
|
+
if arg == "--no-color"
|
|
52
|
+
@color = false
|
|
53
|
+
elsif !arg.start_with?("-")
|
|
54
|
+
@positional << arg
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def validate!
|
|
60
|
+
raise ExitException, HELP_TEXT unless path_a && path_b
|
|
61
|
+
[path_a, path_b].each do |path|
|
|
62
|
+
raise ExitException, "File not found: #{path}" unless File.exist?(path)
|
|
63
|
+
next if READABLE_EXTS.include?(File.extname(path))
|
|
64
|
+
raise ExitException, "Unsupported file type: #{path}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def config_a = @config_a ||= load_config(path_a)
|
|
69
|
+
|
|
70
|
+
def config_b = @config_b ||= load_config(path_b)
|
|
71
|
+
|
|
72
|
+
def load_config(path)
|
|
73
|
+
case File.extname(path)
|
|
74
|
+
when ".cfg" then V7::Config.from_file(path)
|
|
75
|
+
when ".tw7" then V7::Tw7::Parser.new(File.read(path)).parse
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def print_diff
|
|
80
|
+
printed = false
|
|
81
|
+
printed |= print_settings_diff
|
|
82
|
+
printed |= print_chord_diff
|
|
83
|
+
@stdout.puts "No differences." unless printed
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def print_settings_diff
|
|
87
|
+
changes = settings_changes
|
|
88
|
+
return false if changes.empty?
|
|
89
|
+
|
|
90
|
+
@stdout.puts "Settings:"
|
|
91
|
+
changes.each do |key, old_val, new_val|
|
|
92
|
+
@stdout.puts color(" #{key}: #{old_val} -> #{new_val}", :yellow)
|
|
93
|
+
end
|
|
94
|
+
@stdout.puts
|
|
95
|
+
true
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def settings_changes
|
|
99
|
+
old_s = effective_settings(config_a)
|
|
100
|
+
new_s = effective_settings(config_b)
|
|
101
|
+
old_s.filter_map do |key, old_val|
|
|
102
|
+
new_val = new_s[key]
|
|
103
|
+
[key, old_val, new_val] if old_val != new_val
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def effective_settings(config)
|
|
108
|
+
V7::Tw7::SettingsFormatter.extract_settings(config)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def print_chord_diff
|
|
112
|
+
old_chords = chord_map(config_a)
|
|
113
|
+
new_chords = chord_map(config_b)
|
|
114
|
+
buckets = classify_chord_changes(old_chords, new_chords)
|
|
115
|
+
|
|
116
|
+
printed = false
|
|
117
|
+
printed |= print_removed(buckets[:removed], old_chords)
|
|
118
|
+
printed |= print_changed(buckets[:changed], old_chords, new_chords)
|
|
119
|
+
printed |= print_added(buckets[:added], new_chords)
|
|
120
|
+
printed
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def classify_chord_changes(old_chords, new_chords)
|
|
124
|
+
{
|
|
125
|
+
removed: old_chords.keys - new_chords.keys,
|
|
126
|
+
added: new_chords.keys - old_chords.keys,
|
|
127
|
+
changed: (old_chords.keys & new_chords.keys).select { |bm| old_chords[bm] != new_chords[bm] }
|
|
128
|
+
}
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def chord_map(config)
|
|
132
|
+
config.chords.to_h { |c| [c.bitmask, c] }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def print_removed(bitmasks, chords)
|
|
136
|
+
return false if bitmasks.empty?
|
|
137
|
+
|
|
138
|
+
@stdout.puts "Removed:"
|
|
139
|
+
bitmasks.sort.each { |bm| @stdout.puts color(" #{format_chord(chords[bm])}", :red) }
|
|
140
|
+
@stdout.puts
|
|
141
|
+
true
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def print_changed(bitmasks, old_chords, new_chords)
|
|
145
|
+
return false if bitmasks.empty?
|
|
146
|
+
|
|
147
|
+
@stdout.puts "Changed:"
|
|
148
|
+
bitmasks.sort.each do |bm|
|
|
149
|
+
buttons = format_buttons(old_chords[bm])
|
|
150
|
+
old_effect = format_effect(old_chords[bm])
|
|
151
|
+
new_effect = format_effect(new_chords[bm])
|
|
152
|
+
@stdout.puts color(" #{buttons}: #{old_effect} -> #{new_effect}", :yellow)
|
|
153
|
+
end
|
|
154
|
+
@stdout.puts
|
|
155
|
+
true
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def print_added(bitmasks, chords)
|
|
159
|
+
return false if bitmasks.empty?
|
|
160
|
+
|
|
161
|
+
@stdout.puts "Added:"
|
|
162
|
+
bitmasks.sort.each { |bm| @stdout.puts color(" #{format_chord(chords[bm])}", :green) }
|
|
163
|
+
@stdout.puts
|
|
164
|
+
true
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def format_chord(chord)
|
|
168
|
+
"#{format_buttons(chord)}: #{format_effect(chord)}"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def format_buttons(chord)
|
|
172
|
+
prefix = chord.mouse_mode? ? "[MOUSEMODE] " : ""
|
|
173
|
+
buttons = V7::Tw7::ButtonFormatter.format(
|
|
174
|
+
chord.bitmask & ~V7::ChordConstants::MOUSE_MODE_FLAG
|
|
175
|
+
)
|
|
176
|
+
"#{prefix}#{buttons}"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def format_effect(chord)
|
|
180
|
+
V7::Tw7::EffectFormatter.format_effect(chord)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
COLORS = {red: 31, green: 32, yellow: 33}.freeze
|
|
184
|
+
|
|
185
|
+
def color(text, name)
|
|
186
|
+
return text unless @color
|
|
187
|
+
|
|
188
|
+
"\e[#{COLORS[name]}m#{text}\e[0m"
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Twiddling
|
|
2
|
+
module Cli
|
|
3
|
+
class Help
|
|
4
|
+
HELP_TEXT = <<~TEXT
|
|
5
|
+
Usage: twiddling <subcommand> [args]
|
|
6
|
+
|
|
7
|
+
Subcommands:
|
|
8
|
+
help Show this help message
|
|
9
|
+
read <file> Read a .cfg or .tw7 file
|
|
10
|
+
convert <input> <output> Convert between .cfg and .tw7 formats
|
|
11
|
+
search <file> [filters] Search chords by button or result
|
|
12
|
+
diff <file_a> <file_b> Show differences between two configs
|
|
13
|
+
|
|
14
|
+
Run `twiddling <subcommand> -h` for details on each command.
|
|
15
|
+
TEXT
|
|
16
|
+
|
|
17
|
+
def initialize(argv:, stdout: $stdout, stderr: $stderr)
|
|
18
|
+
@stdout = stdout
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def run
|
|
22
|
+
@stdout.puts HELP_TEXT
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module Twiddling
|
|
2
|
+
module Cli
|
|
3
|
+
# twiddling read <file.cfg|file.tw7>
|
|
4
|
+
#
|
|
5
|
+
# Reads a config file and prints it as .tw7 text to stdout.
|
|
6
|
+
# Accepts both .cfg (binary) and .tw7 (text) formats.
|
|
7
|
+
class Read
|
|
8
|
+
READABLE_EXTS = %w[.cfg .tw7].freeze
|
|
9
|
+
|
|
10
|
+
HELP_TEXT = <<~TEXT
|
|
11
|
+
Usage: twiddling read <file>
|
|
12
|
+
|
|
13
|
+
Reads a .cfg or .tw7 config file and prints it as .tw7 text
|
|
14
|
+
to stdout.
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
twiddling read my_config.cfg
|
|
18
|
+
twiddling read layout.tw7
|
|
19
|
+
TEXT
|
|
20
|
+
|
|
21
|
+
def initialize(argv:, stdout: $stdout, stderr: $stderr)
|
|
22
|
+
@argv = argv
|
|
23
|
+
@stdout = stdout
|
|
24
|
+
@stderr = stderr
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def run
|
|
28
|
+
return @stdout.puts(HELP_TEXT) if help?
|
|
29
|
+
validate!
|
|
30
|
+
V7::Tw7::Printer.new(config, io: @stdout).print
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def path = @argv[0]
|
|
36
|
+
|
|
37
|
+
def help? = @argv.include?("-h") || @argv.include?("--help")
|
|
38
|
+
|
|
39
|
+
def validate!
|
|
40
|
+
raise ExitException, HELP_TEXT if path.nil?
|
|
41
|
+
raise ExitException, "File not found: #{path}" unless File.exist?(path)
|
|
42
|
+
|
|
43
|
+
return if READABLE_EXTS.include?(File.extname(path))
|
|
44
|
+
|
|
45
|
+
raise ExitException, "Unsupported file type: #{path} (expected .cfg or .tw7)"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def config
|
|
49
|
+
@config ||= case File.extname(path)
|
|
50
|
+
when ".cfg" then V7::Config.from_file(path)
|
|
51
|
+
when ".tw7" then V7::Tw7::Parser.new(File.read(path)).parse
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|