diff --git a/builddefs/build_keyboard.mk b/builddefs/build_keyboard.mk index 7a5412ccd2be..83310da7669e 100644 --- a/builddefs/build_keyboard.mk +++ b/builddefs/build_keyboard.mk @@ -387,6 +387,7 @@ ifneq ("$(KEYMAP_H)","") endif OPT_DEFS += -DKEYMAP_C=\"$(KEYMAP_C)\" +OPT_DEFS += -save-temps=obj # If a keymap or userspace places their keymap array in another file instead, allow for it to be included # !!NOTE!! -- For this to work, the source file cannot be part of $(SRC), so users should not add it via `SRC += ` diff --git a/lib/python/qmk/c_parse.py b/lib/python/qmk/c_parse.py index 83ab1d1e6d63..485ca8a557a0 100644 --- a/lib/python/qmk/c_parse.py +++ b/lib/python/qmk/c_parse.py @@ -17,6 +17,51 @@ layout_macro_define_regex = re.compile(r'^#\s*define') +def extract_enum_from_c_file_as_dict(contents: str, enum_identifier: str): + """ + Extracts an enum from the contents of a C file and returns it as a python dictionary. + + Args: + contents (str): The contents of the C file as a string. + enum_identifier (str): The identifier for the enum to extract. + + Returns: + dict: A dictionary representation of the extracted enum. + """ + # define the regex pattern to match the entire enum block + enum_pattern = re.compile(f'enum\\s+{enum_identifier}\\s*{{([^}}]*)}}', re.DOTALL) + + # use the regex pattern to find the enum block + enum_match = enum_pattern.search(contents) + + # if the enum block was found, parse it to extract the desired enum values + if not enum_match: + return False + + enum_block = enum_match.group(1) + enum_values = {} + last_value = -1 + + flat_list = [] + for line in enum_block.splitlines(): + line = line.strip() # remove any leading/trailing whitespaces + if line: # skip empty lines + flat_list.extend(line.split(',')) + for line in flat_list: + if line.strip().startswith('typedef') or line.strip().startswith('#') or line.strip().startswith("//"): + continue + if "=" in line: + enum_name, enum_value = line.split('=') + last_value = int(enum_value.replace(",", "").strip()) + else: + enum_name = line.replace(",", "").strip() + if len(enum_name) == 0: + continue + last_value += 1 + enum_values[enum_name.strip()] = last_value + return enum_values + + def _get_chunks(it, size): """Break down a collection into smaller parts """ diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py index de7b0476a07b..83b3b891c560 100644 --- a/lib/python/qmk/cli/__init__.py +++ b/lib/python/qmk/cli/__init__.py @@ -78,6 +78,7 @@ 'qmk.cli.new.keymap', 'qmk.cli.painter', 'qmk.cli.pytest', + 'qmk.cli.rgb_effects', 'qmk.cli.via2json', ] diff --git a/lib/python/qmk/cli/rgb_effects.py b/lib/python/qmk/cli/rgb_effects.py new file mode 100644 index 000000000000..b2ad6e131a1c --- /dev/null +++ b/lib/python/qmk/cli/rgb_effects.py @@ -0,0 +1,57 @@ +import json +import os + +from milc import cli +from qmk.decorators import automagic_keyboard, automagic_keymap +from qmk.keyboard import keyboard_completer, keyboard_folder +from qmk.path import is_keyboard +from pathlib import Path +from qmk.constants import KEYBOARD_OUTPUT_PREFIX +from qmk.c_parse import extract_enum_from_c_file_as_dict +from qmk.json_encoders import InfoJSONEncoder + + +@cli.argument("-kb", "--keyboard", type=keyboard_folder, completer=keyboard_completer) +@cli.argument('-km', '--keymap', help='keymap to evaluate') +@cli.subcommand("RGB Effect information") +@automagic_keyboard +@automagic_keymap +def rgb_effects(cli): + """Output enabled RGB matrix effects as json list format for use with via""" + + # Determine our keyboard(s) + if not cli.args.keyboard: + cli.log.error('Missing parameter: --keyboard') + cli.subcommands['rgb-effects'].print_help() + return False + + if not is_keyboard(cli.args.keyboard): + cli.log.error('Invalid keyboard: "%s"', cli.args.keyboard) + return False + + if not cli.args.keymap: + cli.log.error('Missing parameter: --keymap') + cli.subcommands['rgb-effects'].print_help() + return False + + keymap = cli.args.keymap + keyboard = cli.args.keyboard + keyboard_filesafe = keyboard.replace('/', '_') + rgb_matrix_path = Path(f'{KEYBOARD_OUTPUT_PREFIX}{keyboard_filesafe}_{keymap}/quantum/rgb_matrix/rgb_matrix.i') + + if not os.path.exists(rgb_matrix_path): + cli.log.error(f"{rgb_matrix_path} does not exist.") + cli.log.error("Please compile the image first.") + return False + + with open(rgb_matrix_path, 'r') as f: + file_contents = f.read() + enum_as_dict = {key: value for (key, value) in extract_enum_from_c_file_as_dict(file_contents, "rgb_matrix_effects").items() if key != "RGB_MATRIX_EFFECT_MAX"} + + max_value = max(enum_as_dict.values()) + + def enum_name_to_effect_name(x): + return x.replace("RGB_MATRIX_", "") + + formatted = [[f"{str(idx).zfill(len(str(max_value)))}. {enum_name_to_effect_name(enum_name)}", value] for idx, [enum_name, value] in enumerate(enum_as_dict.items())] + print(json.dumps(formatted, cls=InfoJSONEncoder)) diff --git a/lib/python/qmk/tests/c_parse.py b/lib/python/qmk/tests/c_parse.py new file mode 100644 index 000000000000..2a9078e62f84 --- /dev/null +++ b/lib/python/qmk/tests/c_parse.py @@ -0,0 +1,94 @@ +from qmk.c_parse import extract_enum_from_c_file_as_dict + + +def test_extract_enum_from_c_file_as_dict(): + test_cases = [ + { + 'input_1': 'enum my_enum {};', + 'input_2': 'my_enum', + 'expected_output': {} + }, + { + 'input_1': 'enum my_enum { A };', + 'input_2': 'my_enum', + 'expected_output': { + 'A': 0 + } + }, + { + 'input_1': 'enum my_enum { A = 5, B = 10, C = 15 };', + 'input_2': 'my_enum', + 'expected_output': { + 'A': 5, + 'B': 10, + 'C': 15 + } + }, + { + 'input_1': 'enum my_enum { A, B = 10, C };', + 'input_2': 'my_enum', + 'expected_output': { + 'A': 0, + 'B': 10, + 'C': 11 + } + }, + { + 'input_1': 'enum my_enum {\n A,\n B, // This comment refers to letter B\n C\n};', + 'input_2': 'my_enum', + 'expected_output': { + 'A': 0, + 'B': 1, + 'C': 2 + } + }, + { + 'input_1': 'enum my_enum { A = 0, B = 0, C = 1 };', + 'input_2': 'my_enum', + 'expected_output': { + 'A': 0, + 'B': 0, + 'C': 1 + } + }, + { + 'input_1': 'enum my_enum {\n A,\n B,\n C\n};\n\nenum my_other_enum {\n D\n};', + 'input_2': 'my_enum', + 'expected_output': { + 'A': 0, + 'B': 1, + 'C': 2 + } + }, + { + 'input_1': '/* Valid C comments */\nenum my_enum {\n A,\n B,\n C\n};', + 'input_2': 'my_enum', + 'expected_output': { + 'A': 0, + 'B': 1, + 'C': 2 + } + }, + { + 'input_1': 'enum my_enum {\n A,\n B,\n C\n};', + 'input_2': 'my_enum', + 'expected_output': { + 'A': 0, + 'B': 1, + 'C': 2 + } + }, + { + 'input_1': 'enum my_enum {\n A = 42,\n B,\n C\n};', + 'input_2': 'my_enum', + 'expected_output': { + 'A': 42, + 'B': 43, + 'C': 44 + } + }, + ] + + for test_case in test_cases: + result = extract_enum_from_c_file_as_dict(test_case["input_1"], test_case["input_2"]) + assert test_case["expected_output"] == result