keyboard stuff
1"""Functions for working with config.h files.
2"""
3from pygments.lexers.c_cpp import CLexer
4from pygments.token import Token
5from pygments import lex
6from itertools import islice
7from pathlib import Path
8import re
9
10from milc import cli
11
12from qmk.comment_remover import comment_remover
13
14default_key_entry = {'x': -1, 'y': 0}
15single_comment_regex = re.compile(r'\s+/[/*].*$')
16multi_comment_regex = re.compile(r'/\*(.|\n)*?\*/', re.MULTILINE)
17layout_macro_define_regex = re.compile(r'^#\s*define')
18
19
20def _get_chunks(it, size):
21 """Break down a collection into smaller parts
22 """
23 it = iter(it)
24 return iter(lambda: tuple(islice(it, size)), ())
25
26
27def preprocess_c_file(file):
28 """Load file and strip comments
29 """
30 file_contents = file.read_text(encoding='utf-8')
31 file_contents = comment_remover(file_contents)
32 return file_contents.replace('\\\n', '')
33
34
35def strip_line_comment(string):
36 """Removes comments from a single line string.
37 """
38 return single_comment_regex.sub('', string)
39
40
41def strip_multiline_comment(string):
42 """Removes comments from a single line string.
43 """
44 return multi_comment_regex.sub('', string)
45
46
47def c_source_files(dir_names):
48 """Returns a list of all *.c, *.h, and *.cpp files for a given list of directories
49
50 Args:
51
52 dir_names
53 List of directories relative to `qmk_firmware`.
54 """
55 files = []
56 for dir in dir_names:
57 files.extend(file for file in Path(dir).glob('**/*') if file.suffix in ['.c', '.h', '.cpp'])
58 return files
59
60
61def find_layouts(file):
62 """Returns list of parsed LAYOUT preprocessor macros found in the supplied include file.
63 """
64 file = Path(file)
65 aliases = {} # Populated with all `#define`s that aren't functions
66 parsed_layouts = {}
67
68 # Search the file for LAYOUT macros and aliases
69 file_contents = preprocess_c_file(file)
70
71 for line in file_contents.split('\n'):
72 if layout_macro_define_regex.match(line.lstrip()) and '(' in line and 'LAYOUT' in line:
73 # We've found a LAYOUT macro
74 macro_name, layout, matrix = _parse_layout_macro(line.strip())
75
76 # Reject bad macro names
77 if macro_name.startswith('LAYOUT_kc') or not macro_name.startswith('LAYOUT'):
78 continue
79
80 # Parse the matrix data
81 matrix_locations = _parse_matrix_locations(matrix, file, macro_name)
82
83 # Parse the layout entries into a basic structure
84 default_key_entry['x'] = -1 # Set to -1 so _default_key(key) will increment it to 0
85 layout = layout.strip()
86 parsed_layout = [_default_key(key) for key in layout.split(',')]
87
88 for i, key in enumerate(parsed_layout):
89 if 'label' not in key:
90 cli.log.error('Invalid LAYOUT macro in %s: Empty parameter name in macro %s at pos %s.', file, macro_name, i)
91 elif key['label'] not in matrix_locations:
92 cli.log.error('Invalid LAYOUT macro in %s: Key %s in macro %s has no matrix position!', file, key['label'], macro_name)
93 elif len(matrix_locations.get(key['label'])) > 1:
94 cli.log.error('Invalid LAYOUT macro in %s: Key %s in macro %s has multiple matrix positions (%s)', file, key['label'], macro_name, ', '.join(str(x) for x in matrix_locations[key['label']]))
95 else:
96 key['matrix'] = matrix_locations[key['label']][0]
97
98 parsed_layouts[macro_name] = {
99 'layout': parsed_layout,
100 'filename': str(file),
101 }
102
103 elif '#define' in line:
104 # Attempt to extract a new layout alias
105 try:
106 _, pp_macro_name, pp_macro_text = line.strip().split(' ', 2)
107 aliases[pp_macro_name] = pp_macro_text
108 except ValueError:
109 continue
110
111 return parsed_layouts, aliases
112
113
114def parse_config_h_file(config_h_file, config_h=None):
115 """Extract defines from a config.h file.
116 """
117 if not config_h:
118 config_h = {}
119
120 config_h_file = Path(config_h_file)
121
122 if config_h_file.exists():
123 config_h_text = config_h_file.read_text(encoding='utf-8')
124 config_h_text = config_h_text.replace('\\\n', '')
125 config_h_text = strip_multiline_comment(config_h_text)
126
127 for linenum, line in enumerate(config_h_text.split('\n')):
128 line = strip_line_comment(line).strip()
129
130 if not line:
131 continue
132
133 line = line.split()
134
135 if line[0] == '#define':
136 if len(line) == 1:
137 cli.log.error('%s: Incomplete #define! On or around line %s' % (config_h_file, linenum))
138 elif len(line) == 2:
139 config_h[line[1]] = True
140 else:
141 config_h[line[1]] = ' '.join(line[2:])
142
143 elif line[0] == '#undef':
144 if len(line) == 2:
145 if line[1] in config_h:
146 if config_h[line[1]] is True:
147 del config_h[line[1]]
148 else:
149 config_h[line[1]] = False
150 else:
151 cli.log.error('%s: Incomplete #undef! On or around line %s' % (config_h_file, linenum))
152
153 return config_h
154
155
156def _default_key(label=None):
157 """Increment x and return a copy of the default_key_entry.
158 """
159 default_key_entry['x'] += 1
160 new_key = default_key_entry.copy()
161
162 if label:
163 new_key['label'] = label
164
165 return new_key
166
167
168def _parse_layout_macro(layout_macro):
169 """Split the LAYOUT macro into its constituent parts
170 """
171 layout_macro = layout_macro.replace('\\', '').replace(' ', '').replace('\t', '').replace('#define', '')
172 macro_name, layout = layout_macro.split('(', 1)
173 layout, matrix = layout.split(')', 1)
174
175 return macro_name, layout, matrix
176
177
178def _parse_matrix_locations(matrix, file, macro_name):
179 """Parse raw matrix data into a dictionary keyed by the LAYOUT identifier.
180 """
181 matrix_locations = {}
182
183 for row_num, row in enumerate(matrix.split('},{')):
184 if row.startswith('LAYOUT'):
185 cli.log.error('%s: %s: Nested layout macro detected. Matrix data not available!', file, macro_name)
186 break
187
188 row = row.replace('{', '').replace('}', '')
189 for col_num, identifier in enumerate(row.split(',')):
190 if identifier != 'KC_NO':
191 if identifier not in matrix_locations:
192 matrix_locations[identifier] = []
193 matrix_locations[identifier].append([row_num, col_num])
194
195 return matrix_locations
196
197
198def _coerce_led_token(_type, value):
199 """ Convert token to valid info.json content
200 """
201 value_map = {
202 'NO_LED': None,
203 'LED_FLAG_ALL': 0xFF,
204 'LED_FLAG_NONE': 0x00,
205 'LED_FLAG_MODIFIER': 0x01,
206 'LED_FLAG_UNDERGLOW': 0x02,
207 'LED_FLAG_KEYLIGHT': 0x04,
208 'LED_FLAG_INDICATOR': 0x08,
209 }
210 if _type is Token.Literal.Number.Integer:
211 return int(value)
212 if _type is Token.Literal.Number.Float:
213 return float(value)
214 if _type is Token.Literal.Number.Hex:
215 return int(value, 0)
216 if _type is Token.Name and value in value_map.keys():
217 return value_map[value]
218
219
220def _validate_led_config(matrix, matrix_rows, matrix_cols, matrix_indexes, position, position_raw, flags):
221 # TODO: Improve crude parsing/validation
222 if len(matrix) != matrix_rows and len(matrix) != (matrix_rows / 2):
223 raise ValueError("Unable to parse g_led_config matrix data")
224 for index, row in enumerate(matrix):
225 if len(row) != matrix_cols:
226 raise ValueError(f"Number of columns in row {index} ({len(row)}) does not match matrix ({matrix_cols})")
227 if len(position) != len(flags):
228 raise ValueError(f"Number of g_led_config physical positions ({len(position)}) does not match number of flags ({len(flags)})")
229 if len(matrix_indexes) and (max(matrix_indexes) >= len(flags)):
230 raise ValueError(f"LED index {max(matrix_indexes)} is OOB in g_led_config - should be < {len(flags)}")
231 if not all(isinstance(n, int) for n in matrix_indexes):
232 raise ValueError("matrix indexes are not all ints")
233 if (len(position_raw) % 2) != 0:
234 raise ValueError("Malformed g_led_config position data")
235
236
237def _parse_led_config(file, matrix_cols, matrix_rows):
238 """Return any 'raw' led/rgb matrix config
239 """
240 matrix = []
241 position_raw = []
242 flags = []
243
244 found_led_config_t = False
245 found_g_led_config = False
246 bracket_count = 0
247 section = 0
248 current_row_index = 0
249 current_row = []
250
251 for _type, value in lex(preprocess_c_file(file), CLexer()):
252 if not found_g_led_config:
253 # Check for type
254 if value == 'led_config_t':
255 found_led_config_t = True
256 # Type found, now check for name
257 elif found_led_config_t and value == 'g_led_config':
258 found_g_led_config = True
259 elif value == ';':
260 found_g_led_config = False
261 else:
262 # Assume bracket count hints to section of config we are within
263 if value == '{':
264 bracket_count += 1
265 if bracket_count == 2:
266 section += 1
267 elif value == '}':
268 if section == 1 and bracket_count == 3:
269 matrix.append(current_row)
270 current_row = []
271 current_row_index += 1
272 bracket_count -= 1
273 else:
274 # Assume any non whitespace value here is important enough to stash
275 if _type in [Token.Literal.Number.Integer, Token.Literal.Number.Float, Token.Literal.Number.Hex, Token.Name]:
276 if section == 1 and bracket_count == 3:
277 current_row.append(_coerce_led_token(_type, value))
278 if section == 2 and bracket_count == 3:
279 position_raw.append(_coerce_led_token(_type, value))
280 if section == 3 and bracket_count == 2:
281 flags.append(_coerce_led_token(_type, value))
282 elif _type in [Token.Comment.Preproc]:
283 # TODO: Promote to error
284 return None
285
286 # Slightly better intrim format
287 position = list(_get_chunks(position_raw, 2))
288 matrix_indexes = list(filter(lambda x: x is not None, sum(matrix, [])))
289
290 # If we have not found anything - bail with no error
291 if not section:
292 return None
293
294 # Throw any validation errors
295 _validate_led_config(matrix, matrix_rows, matrix_cols, matrix_indexes, position, position_raw, flags)
296
297 return (matrix, position, flags)
298
299
300def find_led_config(file, matrix_cols, matrix_rows):
301 """Search file for led/rgb matrix config
302 """
303 found = _parse_led_config(file, matrix_cols, matrix_rows)
304 if not found:
305 return None
306
307 # Expand collected content
308 (matrix, position, flags) = found
309
310 # Align to output format
311 led_config = []
312 for index, item in enumerate(position, start=0):
313 led_config.append({
314 'x': item[0],
315 'y': item[1],
316 'flags': flags[index],
317 })
318 for r in range(len(matrix)):
319 for c in range(len(matrix[r])):
320 index = matrix[r][c]
321 if index is not None:
322 led_config[index]['matrix'] = [r, c]
323
324 return led_config