at master 12 kB view raw
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