"""Functions for working with config.h files. """ from pygments.lexers.c_cpp import CLexer from pygments.token import Token from pygments import lex from itertools import islice from pathlib import Path import re from milc import cli from qmk.comment_remover import comment_remover default_key_entry = {'x': -1, 'y': 0} single_comment_regex = re.compile(r'\s+/[/*].*$') multi_comment_regex = re.compile(r'/\*(.|\n)*?\*/', re.MULTILINE) layout_macro_define_regex = re.compile(r'^#\s*define') def _get_chunks(it, size): """Break down a collection into smaller parts """ it = iter(it) return iter(lambda: tuple(islice(it, size)), ()) def _preprocess_c_file(file): """Load file and strip comments """ file_contents = file.read_text(encoding='utf-8') file_contents = comment_remover(file_contents) return file_contents.replace('\\\n', '') def strip_line_comment(string): """Removes comments from a single line string. """ return single_comment_regex.sub('', string) def strip_multiline_comment(string): """Removes comments from a single line string. """ return multi_comment_regex.sub('', string) def c_source_files(dir_names): """Returns a list of all *.c, *.h, and *.cpp files for a given list of directories Args: dir_names List of directories relative to `qmk_firmware`. """ files = [] for dir in dir_names: files.extend(file for file in Path(dir).glob('**/*') if file.suffix in ['.c', '.h', '.cpp']) return files def find_layouts(file): """Returns list of parsed LAYOUT preprocessor macros found in the supplied include file. """ file = Path(file) aliases = {} # Populated with all `#define`s that aren't functions parsed_layouts = {} # Search the file for LAYOUT macros and aliases file_contents = _preprocess_c_file(file) for line in file_contents.split('\n'): if layout_macro_define_regex.match(line.lstrip()) and '(' in line and 'LAYOUT' in line: # We've found a LAYOUT macro macro_name, layout, matrix = _parse_layout_macro(line.strip()) # Reject bad macro names if macro_name.startswith('LAYOUT_kc') or not macro_name.startswith('LAYOUT'): continue # Parse the matrix data matrix_locations = _parse_matrix_locations(matrix, file, macro_name) # Parse the layout entries into a basic structure default_key_entry['x'] = -1 # Set to -1 so _default_key(key) will increment it to 0 layout = layout.strip() parsed_layout = [_default_key(key) for key in layout.split(',')] for i, key in enumerate(parsed_layout): if 'label' not in key: cli.log.error('Invalid LAYOUT macro in %s: Empty parameter name in macro %s at pos %s.', file, macro_name, i) elif key['label'] not in matrix_locations: cli.log.error('Invalid LAYOUT macro in %s: Key %s in macro %s has no matrix position!', file, key['label'], macro_name) elif len(matrix_locations.get(key['label'])) > 1: 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']])) else: key['matrix'] = matrix_locations[key['label']][0] parsed_layouts[macro_name] = { 'layout': parsed_layout, 'filename': str(file), } elif '#define' in line: # Attempt to extract a new layout alias try: _, pp_macro_name, pp_macro_text = line.strip().split(' ', 2) aliases[pp_macro_name] = pp_macro_text except ValueError: continue return parsed_layouts, aliases def parse_config_h_file(config_h_file, config_h=None): """Extract defines from a config.h file. """ if not config_h: config_h = {} config_h_file = Path(config_h_file) if config_h_file.exists(): config_h_text = config_h_file.read_text(encoding='utf-8') config_h_text = config_h_text.replace('\\\n', '') config_h_text = strip_multiline_comment(config_h_text) for linenum, line in enumerate(config_h_text.split('\n')): line = strip_line_comment(line).strip() if not line: continue line = line.split() if line[0] == '#define': if len(line) == 1: cli.log.error('%s: Incomplete #define! On or around line %s' % (config_h_file, linenum)) elif len(line) == 2: config_h[line[1]] = True else: config_h[line[1]] = ' '.join(line[2:]) elif line[0] == '#undef': if len(line) == 2: if line[1] in config_h: if config_h[line[1]] is True: del config_h[line[1]] else: config_h[line[1]] = False else: cli.log.error('%s: Incomplete #undef! On or around line %s' % (config_h_file, linenum)) return config_h def _default_key(label=None): """Increment x and return a copy of the default_key_entry. """ default_key_entry['x'] += 1 new_key = default_key_entry.copy() if label: new_key['label'] = label return new_key def _parse_layout_macro(layout_macro): """Split the LAYOUT macro into its constituent parts """ layout_macro = layout_macro.replace('\\', '').replace(' ', '').replace('\t', '').replace('#define', '') macro_name, layout = layout_macro.split('(', 1) layout, matrix = layout.split(')', 1) return macro_name, layout, matrix def _parse_matrix_locations(matrix, file, macro_name): """Parse raw matrix data into a dictionary keyed by the LAYOUT identifier. """ matrix_locations = {} for row_num, row in enumerate(matrix.split('},{')): if row.startswith('LAYOUT'): cli.log.error('%s: %s: Nested layout macro detected. Matrix data not available!', file, macro_name) break row = row.replace('{', '').replace('}', '') for col_num, identifier in enumerate(row.split(',')): if identifier != 'KC_NO': if identifier not in matrix_locations: matrix_locations[identifier] = [] matrix_locations[identifier].append([row_num, col_num]) return matrix_locations def _coerce_led_token(_type, value): """ Convert token to valid info.json content """ value_map = { 'NO_LED': None, 'LED_FLAG_ALL': 0xFF, 'LED_FLAG_NONE': 0x00, 'LED_FLAG_MODIFIER': 0x01, 'LED_FLAG_UNDERGLOW': 0x02, 'LED_FLAG_KEYLIGHT': 0x04, 'LED_FLAG_INDICATOR': 0x08, } if _type is Token.Literal.Number.Integer: return int(value) if _type is Token.Literal.Number.Float: return float(value) if _type is Token.Literal.Number.Hex: return int(value, 0) if _type is Token.Name and value in value_map.keys(): return value_map[value] def _validate_led_config(matrix, matrix_rows, matrix_cols, matrix_indexes, position, position_raw, flags): # TODO: Improve crude parsing/validation if len(matrix) != matrix_rows and len(matrix) != (matrix_rows / 2): raise ValueError("Unable to parse g_led_config matrix data") for index, row in enumerate(matrix): if len(row) != matrix_cols: raise ValueError(f"Number of columns in row {index} ({len(row)}) does not match matrix ({matrix_cols})") if len(position) != len(flags): raise ValueError(f"Number of g_led_config physical positions ({len(position)}) does not match number of flags ({len(flags)})") if len(matrix_indexes) and (max(matrix_indexes) >= len(flags)): raise ValueError(f"LED index {max(matrix_indexes)} is OOB in g_led_config - should be < {len(flags)}") if not all(isinstance(n, int) for n in matrix_indexes): raise ValueError("matrix indexes are not all ints") if (len(position_raw) % 2) != 0: raise ValueError("Malformed g_led_config position data") def _parse_led_config(file, matrix_cols, matrix_rows): """Return any 'raw' led/rgb matrix config """ matrix = [] position_raw = [] flags = [] found_led_config_t = False found_g_led_config = False bracket_count = 0 section = 0 current_row_index = 0 current_row = [] for _type, value in lex(_preprocess_c_file(file), CLexer()): if not found_g_led_config: # Check for type if value == 'led_config_t': found_led_config_t = True # Type found, now check for name elif found_led_config_t and value == 'g_led_config': found_g_led_config = True elif value == ';': found_g_led_config = False else: # Assume bracket count hints to section of config we are within if value == '{': bracket_count += 1 if bracket_count == 2: section += 1 elif value == '}': if section == 1 and bracket_count == 3: matrix.append(current_row) current_row = [] current_row_index += 1 bracket_count -= 1 else: # Assume any non whitespace value here is important enough to stash if _type in [Token.Literal.Number.Integer, Token.Literal.Number.Float, Token.Literal.Number.Hex, Token.Name]: if section == 1 and bracket_count == 3: current_row.append(_coerce_led_token(_type, value)) if section == 2 and bracket_count == 3: position_raw.append(_coerce_led_token(_type, value)) if section == 3 and bracket_count == 2: flags.append(_coerce_led_token(_type, value)) elif _type in [Token.Comment.Preproc]: # TODO: Promote to error return None # Slightly better intrim format position = list(_get_chunks(position_raw, 2)) matrix_indexes = list(filter(lambda x: x is not None, sum(matrix, []))) # If we have not found anything - bail with no error if not section: return None # Throw any validation errors _validate_led_config(matrix, matrix_rows, matrix_cols, matrix_indexes, position, position_raw, flags) return (matrix, position, flags) def find_led_config(file, matrix_cols, matrix_rows): """Search file for led/rgb matrix config """ found = _parse_led_config(file, matrix_cols, matrix_rows) if not found: return None # Expand collected content (matrix, position, flags) = found # Align to output format led_config = [] for index, item in enumerate(position, start=0): led_config.append({ 'x': item[0], 'y': item[1], 'flags': flags[index], }) for r in range(len(matrix)): for c in range(len(matrix[r])): index = matrix[r][c] if index is not None: led_config[index]['matrix'] = [r, c] return led_config