"""Functions for working with config.h files. """ from pathlib import Path import re from milc import cli from qmk.comment_remover import comment_remover default_key_entry = {'x': -1, 'y': 0, 'w': 1} single_comment_regex = re.compile(r' */[/*].*$') multi_comment_regex = re.compile(r'/\*(.|\n)*\*/', re.MULTILINE) 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 = file.read_text() file_contents = comment_remover(file_contents) file_contents = file_contents.replace('\\\n', '') for line in file_contents.split('\n'): if line.startswith('#define') 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 key in parsed_layout: if key['label'] in matrix_locations: key['matrix'] = matrix_locations[key['label']] parsed_layouts[macro_name] = { 'key_count': len(parsed_layout), '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 # Populate our aliases for alias, text in aliases.items(): if text in parsed_layouts and 'KEYMAP' not in alias: parsed_layouts[alias] = parsed_layouts[text] return parsed_layouts 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() 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': matrix_locations[identifier] = [row_num, col_num] return matrix_locations