#!/usr/bin/env python3 # Copyright 2021 Johannes Marbach # SPDX-License-Identifier: GPL-3.0-or-later import argparse from typing import Set import git import os import sys import tempfile import yaml ### # Global constants outfile_c = 'sq2lv_layouts.c' outfile_h = 'sq2lv_layouts.h' repository_url = 'https://gitlab.gnome.org/World/Phosh/squeekboard.git' rel_layouts_dir = 'data/keyboards' ### # General helpers ## def die(msg): """Print an error message to STDERR and exit with a non-zero code. msg -- message to output on STDERR """ sys.stderr.write(msg if msg.endswith('\n') else msg + '\n') sys.exit(1) def warn(msg): """Print a warning message to STDERR. msg -- message to output on STDERR """ sys.stderr.write(msg if msg.endswith('\n') else msg + '\n') def parse_arguments(): """ Parse commandline arguments. """ parser = argparse.ArgumentParser(description='Convert squeekboard layouts to LVGL-compatible C code.') parser.add_argument('--input', dest='input', action='append', required=True, help='squeekboard layout to ' + 'use as input for generation. Has to be a YAML file path relative to data/keyboards. ' + 'Can be specified multiple times.') parser.add_argument('--name', dest='name', action='append', required=True, help='name for the layout. ' + 'Needs to be specified once for every --input flag.') parser.add_argument('--extra-top-row-base', dest='extra_top_row_base', type=str, required=False, help='additional ' + 'key row to add at the top of the base layer.') parser.add_argument('--extra-top-row-upper', dest='extra_top_row_upper', type=str, required=False, help='additional ' + 'key row to add at the top of the upper layer.') parser.add_argument('--shift-keycap', dest='shift_keycap', type=str, required=False, help='key caption for ' + 'the Shift key. Defaults to "Shift".') parser.add_argument('--surround-space-with-arrows', action='store_true', dest='arrows_around_space', help='insert left / right arrow before / after space key') parser.add_argument('--generate-scancodes', action='store_true', dest='generate_scancodes', help='also ' + 'generate scancode tables (only works for US layout currently)') parser.add_argument('--output', dest='output', type=str, required=True, help='output directory for generated ' + 'files') args = parser.parse_args() if not args.output or not os.path.isdir(args.output): die('Error: no valid output directory specified') return args def clone_squeekboard_repo(destination): """ Clone the squeekboard git repository. destination -- directory path to clone to """ git.Repo.clone_from(repository_url, destination, depth=1) def load_yaml(layouts_dir, rel_path): """ Load a YAML file and return its dictionary representation. rel_path -- path of the YAML file relative to the layouts root directory """ path = os.path.join(layouts_dir, rel_path) if not os.path.isfile(path): die(f'could not find input file {path}') data = None with open(path, 'r') as stream: try: data = yaml.safe_load(stream) except yaml.YAMLError as exc: die(f'Could not load YAML file {path}: {exc}') if not data: die(f'could not load input file {path}') if not 'views' in data: die(f'no "views" element in YAML data loaded from input file {path}') return data def write_files(lines_c, lines_h): """Write accumulated output to C and header file, respectively. lines_c -- sequence of lines to write to C file lines_h -- sequence of lines to write to header """ with open(os.path.join(args.output, outfile_c), 'w') as fp: fp.write('\n'.join(lines_c)) with open(os.path.join(args.output, outfile_h), 'w') as fp: fp.write('\n'.join(lines_h)) def comma_if_needed(sequence, idx): """Return a comma unless idx points to the last element in sequence. sequence -- a sequence of elements idx -- an index into the sequence """ return ',' if idx < len(sequence) - 1 else '' ### # SourceFileBuilder ## class SourceFileBuilder(object): """Builder for .c and .h files. """ def __init__(self): """Constructor. """ self.lines = [] self._add_header_comment() def add_line(self, line=None): """Add a single line and return the builder. line - string representing the line to add (if None, an empty line will be added) """ self.lines.append(line if line else '') return self def add_lines(self, lines): """Add multiple lines and return the builder. lines - a list of strings representing the lines to add """ self.lines += lines return self def wrap_in_ifndef(self, macro): """Wrap all of the current content in a macro check to prevent double inclusion and return the builder. macro -- name of the macro to use """ idx = 0 while self.lines[idx][:2] in ['/*', ' *']: idx += 1 self.lines.insert(idx, '') self.lines.insert(idx + 1, f'#ifndef {macro}') self.lines.insert(idx + 2, f'#define {macro}') self.add_lines([f'#endif /* {macro} */', '']) def _add_header_comment(self): """Add the generator header comment and return the builder. """ self.add_lines(['/**', ' * Auto-generated with squeek2lvgl', ' **/', '']) return self def add_include(self, header): """Add an include statement and return the builder. header -- path to file to be included """ self.add_line(f'#include "{header}"') return self def add_system_include(self, header): """Add a system include statement and return the builder. header -- path to file to be included """ self.add_line(f'#include <{header}>') return self def add_section_comment(self, title): """Add a comment marking the beginning of a section and return the builder. title -- title of the section """ self.add_lines(['/**', f' * {title}', ' **/']) return self def add_subsection_comment(self, title): """Add a comment marking the beginning of a subsection and return the builder. title -- title of the subsection """ self.add_line(f'/* {title} */') return self def add_array(self, static, type, identifier, values, row_terminator, array_terminator): """Add a row-based C array and return the builder. static -- True if the variable is static type -- variable type identifier -- variable identifier values -- values per row as a list of lists, row_terminator -- element to append to each row except the last array_terminator -- element to append to the last row """ prefix = 'static ' if static else '' if not values or not values[0]: self.add_line(f'{prefix}{type} * const {identifier} = NULL;') return self self.add_line(f'{prefix}{type} {identifier}[] = ' + '{ \\') for i, values_in_row in enumerate(values): elements = values_in_row if i < len(values) - 1 and row_terminator: elements.append(row_terminator) if i == len(values) - 1 and array_terminator: elements.append(array_terminator) joined = ', '.join([f'{e}' for e in elements]) self.add_line(f' {joined}{comma_if_needed(values, i)} \\') self.add_line('};') return self def add_flat_array(self, static, type, identifier, values, array_terminator): """Add a flat C array and return the builder. static -- True if the variable is static type -- variable type identifier -- variable identifier values -- list of values, array_terminator -- element to append after the last element """ return self.add_array(static, type, identifier, [values], '', array_terminator) ### # Layout processing ## def layout_id_to_c_identifier(layout_id): """Return a string for a layout that is suitable to be used in a C identifier. layout_id -- ID of the layout """ return layout_id.lower().replace('/', '_') layer_name_for_view_id = { 'base': 'Lowercase letters', 'upper': 'Uppercase letters', 'numbers': 'Numbers / symbols', 'eschars': 'Special characters', 'symbols': 'Symbols', 'actions': 'Actions' } def view_id_to_layer_name(view_id): """Return a descriptive name for a layer based on its view ID. view_id -- the ID of the view representing the layer """ if view_id not in layer_name_for_view_id: return None return layer_name_for_view_id[view_id] layer_identifier_for_view_id = { 'base': 'lower', 'eschars': 'special' } def view_id_to_c_identifier(view_id): """Return a string for a view that is suitable to be used in a C identifier. view_id -- ID of the view """ return layer_identifier_for_view_id[view_id] if view_id in layer_identifier_for_view_id else view_id ignored_keys = { 'preferences' } def is_key_ignored(key): """Return True if a key should be ignored or False otherwise. key -- the key in question """ return key in ignored_keys keycap_for_key = { '\\': '\\\\', '"': '\\"', '↑': 'LV_SYMBOL_UP', '↓': 'LV_SYMBOL_DOWN', '←': 'LV_SYMBOL_LEFT', '→': 'LV_SYMBOL_RIGHT', 'BackSpace': 'LV_SYMBOL_BACKSPACE', 'colon': ':', 'period': '.', 'Shift_L': 'SQ2LV_SYMBOL_SHIFT', 'space': ' ', 'Return': 'LV_SYMBOL_OK', 'Up': 'LV_SYMBOL_UP', 'Left': 'LV_SYMBOL_LEFT', 'Down': 'LV_SYMBOL_DOWN', 'Right': 'LV_SYMBOL_RIGHT' } def key_to_keycap(args, key): """Return the keycap for a key. args -- commandline arguments key -- the key """ return keycap_for_key[key] if key in keycap_for_key else key def key_is_modifier(key, data_buttons): """Return true if a key acts as a modifier. key -- the key in question data_buttons -- the "buttons" object from the layout's YAML file """ return key in data_buttons and 'modifier' in data_buttons[key] repeatable_keys = { 'BackSpace', 'Del', 'PgUp', 'PgDn', 'Return', 'space', '↑', '←', '↓', '→', 'Up', 'Left', 'Down', 'Right' } def key_can_repeat(key): """Return True if a key can repeatedly emit while being held down. key -- the key """ return key in repeatable_keys def key_to_attributes(key, is_locked, is_lockable, is_extra_top_row, data_buttons): """Return the LVGL button attributes for a key. key -- the key in question is_locked - whether the key is locked in the current view is_lockable - whether the key can be locked in the current view is_extra_top_row - whether the key is in the extra top row data_buttons -- the "buttons" object from the layout's YAML file """ attributes = [] if key_is_modifier(key, data_buttons): attributes.append('SQ2LV_CTRL_MOD_INACTIVE') elif is_locked: attributes.append('SQ2LV_CTRL_MOD_ACTIVE') elif is_lockable: attributes.append('SQ2LV_CTRL_MOD_INACTIVE') elif key in data_buttons and key not in ['"', 'colon', 'period', 'space'] or key in ['↑', '←', '↓', '→']: attributes.append('SQ2LV_CTRL_NON_CHAR') elif key not in ['space']: attributes.append('LV_BUTTONMATRIX_CTRL_POPOVER') if not key_can_repeat(key): attributes.append('LV_BUTTONMATRIX_CTRL_NO_REPEAT') if is_extra_top_row: attributes.append('SQ2LV_CTRL_NON_CHAR') if key == '': attributes.append('LV_BUTTONMATRIX_CTRL_HIDDEN') if key not in data_buttons or key in ['"', 'colon', 'period']: attributes.append('2') elif key == 'space': attributes.append('7') else: attributes.append('3') return ' | '.join(attributes) def keycap_to_c_value(keycap): """Return the right-hand side C value for a keycap """ return keycap if keycap.startswith('LV_') or keycap.startswith('SQ2LV_') else f'"{keycap}"' scancodes_for_keycap = { '0': ['KEY_0'], '1': ['KEY_1'], '2': ['KEY_2'], '3': ['KEY_3'], '4': ['KEY_4'], '5': ['KEY_5'], '6': ['KEY_6'], '7': ['KEY_7'], '8': ['KEY_8'], '9': ['KEY_9'], 'a': ['KEY_A'], 'b': ['KEY_B'], 'c': ['KEY_C'], 'd': ['KEY_D'], 'e': ['KEY_E'], 'f': ['KEY_F'], 'g': ['KEY_G'], 'h': ['KEY_H'], 'i': ['KEY_I'], 'j': ['KEY_J'], 'k': ['KEY_K'], 'l': ['KEY_L'], 'm': ['KEY_M'], 'n': ['KEY_N'], 'o': ['KEY_O'], 'p': ['KEY_P'], 'q': ['KEY_Q'], 'r': ['KEY_R'], 's': ['KEY_S'], 't': ['KEY_T'], 'u': ['KEY_U'], 'v': ['KEY_V'], 'w': ['KEY_W'], 'x': ['KEY_X'], 'y': ['KEY_Y'], 'z': ['KEY_Z'], 'A': ['KEY_LEFTSHIFT', 'KEY_A'], 'B': ['KEY_LEFTSHIFT', 'KEY_B'], 'C': ['KEY_LEFTSHIFT', 'KEY_C'], 'D': ['KEY_LEFTSHIFT', 'KEY_D'], 'E': ['KEY_LEFTSHIFT', 'KEY_E'], 'F': ['KEY_LEFTSHIFT', 'KEY_F'], 'G': ['KEY_LEFTSHIFT', 'KEY_G'], 'H': ['KEY_LEFTSHIFT', 'KEY_H'], 'I': ['KEY_LEFTSHIFT', 'KEY_I'], 'J': ['KEY_LEFTSHIFT', 'KEY_J'], 'K': ['KEY_LEFTSHIFT', 'KEY_K'], 'L': ['KEY_LEFTSHIFT', 'KEY_L'], 'M': ['KEY_LEFTSHIFT', 'KEY_M'], 'N': ['KEY_LEFTSHIFT', 'KEY_N'], 'O': ['KEY_LEFTSHIFT', 'KEY_O'], 'P': ['KEY_LEFTSHIFT', 'KEY_P'], 'Q': ['KEY_LEFTSHIFT', 'KEY_Q'], 'R': ['KEY_LEFTSHIFT', 'KEY_R'], 'S': ['KEY_LEFTSHIFT', 'KEY_S'], 'T': ['KEY_LEFTSHIFT', 'KEY_T'], 'U': ['KEY_LEFTSHIFT', 'KEY_U'], 'V': ['KEY_LEFTSHIFT', 'KEY_V'], 'W': ['KEY_LEFTSHIFT', 'KEY_W'], 'X': ['KEY_LEFTSHIFT', 'KEY_X'], 'Y': ['KEY_LEFTSHIFT', 'KEY_Y'], 'Z': ['KEY_LEFTSHIFT', 'KEY_Z'], 'Alt': ['KEY_LEFTALT'], 'Ctrl': ['KEY_LEFTCTRL'], 'LV_SYMBOL_UP': ['KEY_UP'], 'LV_SYMBOL_DOWN': ['KEY_DOWN'], 'LV_SYMBOL_LEFT': ['KEY_LEFT'], 'LV_SYMBOL_RIGHT': ['KEY_RIGHT'], 'LV_SYMBOL_BACKSPACE': ['KEY_BACKSPACE'], 'LV_SYMBOL_OK': ['KEY_ENTER'], ' ': ['KEY_SPACE'], 'ABC': [], 'abc': [], '123': [], 'PgUp': ['KEY_PAGEUP'], 'PgDn': ['KEY_PAGEDOWN'], 'Home': ['KEY_HOME'], 'End': ['KEY_END'], '*': ['KEY_LEFTSHIFT', 'KEY_8'], '#': ['KEY_LEFTSHIFT', 'KEY_3'], '$': ['KEY_LEFTSHIFT', 'KEY_4'], '/': ['KEY_SLASH'], '&': ['KEY_LEFTSHIFT', 'KEY_7'], '-': ['KEY_MINUS'], '_': ['KEY_LEFTSHIFT', 'KEY_MINUS'], '+': ['KEY_LEFTSHIFT', 'KEY_EQUAL'], '(': ['KEY_LEFTSHIFT', 'KEY_9'], ')': ['KEY_LEFTSHIFT', 'KEY_0'], ',': ['KEY_COMMA'], '\\"': ['KEY_LEFTSHIFT', 'KEY_APOSTROPHE'], '\'': ['KEY_APOSTROPHE'], ':': ['KEY_LEFTSHIFT', 'KEY_SEMICOLON'], ';': ['KEY_SEMICOLON'], '!': ['KEY_LEFTSHIFT', 'KEY_1'], '?': ['KEY_LEFTSHIFT', 'KEY_SLASH'], '.': ['KEY_DOT'], '~': ['KEY_LEFTSHIFT', 'KEY_GRAVE'], '`': ['KEY_GRAVE'], '|': ['KEY_LEFTSHIFT', 'KEY_BACKSLASH'], # '·': [], # '√': [], # 'π': [], # 'τ': [], # '÷': [], # '×': [], # '¶': [], # '©': [], # '®': [], # '£': [], # '€': [], # '¥': [], '\\\\': ['KEY_BACKSLASH'], '^': ['KEY_LEFTSHIFT', 'KEY_6'], # '°': [], '@': ['KEY_LEFTSHIFT', 'KEY_2'], '{': ['KEY_LEFTSHIFT', 'KEY_LEFTBRACE'], '}': ['KEY_LEFTSHIFT', 'KEY_RIGHTBRACE'], '%': ['KEY_LEFTSHIFT', 'KEY_5'], '<': ['KEY_LEFTSHIFT', 'KEY_COMMA'], '>': ['KEY_LEFTSHIFT', 'KEY_DOT'], '=': ['KEY_EQUAL'], '[': ['KEY_LEFTBRACE'], ']': ['KEY_RIGHTBRACE'], 'F1': ['KEY_F1'], 'F2': ['KEY_F2'], 'F3': ['KEY_F3'], 'F4': ['KEY_F4'], 'F5': ['KEY_F5'], 'F6': ['KEY_F6'], 'F7': ['KEY_F7'], 'F8': ['KEY_F8'], 'F9': ['KEY_F9'], 'F10': ['KEY_F10'], 'F11': ['KEY_F11'], 'F12': ['KEY_F12'], 'Esc': ['KEY_ESC'], 'Tab': ['KEY_TAB'], 'Pause': ['KEY_PAUSE'], 'Insert': ['KEY_INSERT'], 'Del': ['KEY_DELETE'], 'Menu': ['KEY_COMPOSE'], 'Break': ['KEY_BREAK'], '↑': ['KEY_UP'], '←': ['KEY_LEFT'], '↓': ['KEY_DOWN'], '→': ['KEY_RIGHT'] } def keycap_to_scancodes(args, keycap, is_switcher): """Return the scancodes needed to produce a keycap args -- commandline arguments keycap -- keycap to produce is_switcher -- whether the key is a layer switcher """ if is_switcher: return [] if keycap not in scancodes_for_keycap: warn(f'Cannot determine scancodes for unknown keycap "{keycap}"') return [] return scancodes_for_keycap[keycap] def get_keycaps_attrs_modifiers_switchers_scancodes(args, view_id, data_views, data_buttons, extra_top_row): """Return keycaps, LVGL button attributes, modifier key indexes, layer switching key indexes, layer switching key destinations and scancodes for a view args -- commandline arguments view_id -- ID of the view data_views -- the "views" object from the layout's YAML file data_buttons -- the "buttons" object from the layout's YAML file extra_top_row -- additional row of keys to insert at the top or "" to insert an empty row """ keycaps = [] attrs = [] modifier_idxs = [] switcher_idxs = [] switcher_dests = [] scancodes = [] idx = 0 rows = data_views[view_id] if extra_top_row: rows = [extra_top_row] + rows for index, row in enumerate(rows): keycaps_in_row = [] attrs_in_row = [] scancodes_in_row = [] keys = row.split() if args.arrows_around_space: space_idx = None try: space_idx = keys.index('space') except ValueError: pass if space_idx != None: keys.insert(space_idx, '←') keys.insert(space_idx + 2, '→') for key in keys: if is_key_ignored(key): continue keycap = None if key in data_buttons and 'label' in data_buttons[key] and key not in ['Up', 'Left', 'Down', 'Right']: keycap = data_buttons[key]['label'].replace('\\', '\\\\') else: keycap = key_to_keycap(args, key) if not keycap: continue keycaps_in_row.append(keycap_to_c_value(keycap)) if key_is_modifier(key, data_buttons): modifier_idxs.append(idx) is_locked = False is_lockable = False is_switcher = False if key in data_buttons and 'action' in data_buttons[key]: action = data_buttons[key]['action'] dest = None if 'set_view' in action: dest = action['set_view'] elif 'locking' in action and 'lock_view' in action['locking'] and 'unlock_view' in action['locking']: if action['locking']['lock_view'] == view_id: dest = action['locking']['unlock_view'] is_locked = True else: dest = action['locking']['lock_view'] is_lockable = True if dest: switcher_idxs.append(idx) switcher_dests.append(dest) is_switcher = True attrs_in_row.append(key_to_attributes(key, is_locked, is_lockable, extra_top_row and index == 0, data_buttons)) if args.generate_scancodes: scancodes_in_row.append(keycap_to_scancodes(args, keycap, is_switcher)) idx += 1 keycaps.append(keycaps_in_row) attrs.append(attrs_in_row) scancodes.append(scancodes_in_row) return keycaps, attrs, modifier_idxs, switcher_idxs, switcher_dests, scancodes def flatten_scancodes(scancodes): """Process a nested list of scancodes per row and key and return a flattened list of scancodes per row, a list of starting indexes and a list of scancode counts. scancodes -- list (rows) of list (keys) of list (scancodes) of scancodes """ flat = [] idxs = [] nums = [] if args.generate_scancodes: idx = 0 num = 0 for scancodes_in_row in scancodes: flat_in_row = [] idxs_in_row = [] nums_in_row = [] for codes in scancodes_in_row: flat_in_row += codes idxs_in_row.append(idx if len(codes) > 0 else -1) nums_in_row.append(len(codes)) idx += len(codes) flat.append(flat_in_row) idxs.append(idxs_in_row) nums.append(nums_in_row) return flat, idxs, nums ### # Main ## if __name__ == '__main__': args = parse_arguments() c_builder = SourceFileBuilder() c_builder.add_include(outfile_h) c_builder.add_include('../squeek2lvgl/sq2lv.h') if args.generate_scancodes: c_builder.add_system_include('linux/input.h') c_builder.add_line() shift_keycap = args.shift_keycap if args.shift_keycap else 'Shift' c_builder.add_line(f'#define SQ2LV_SYMBOL_SHIFT "{shift_keycap}"') c_builder.add_line() h_builder = SourceFileBuilder() h_builder.add_include('lvgl/lvgl.h') h_builder.add_line() h_builder.add_line(f'#define SQ2LV_SCANCODES_ENABLED {1 if args.generate_scancodes else 0}') h_builder.add_line() layouts = [] unique_scancodes = {} with tempfile.TemporaryDirectory() as tmp: clone_squeekboard_repo(tmp) layouts_dir = os.path.join(tmp, rel_layouts_dir) for file, layout_name in zip(args.input, args.name): layout_id, _ = os.path.splitext(file) layout_identifier = layout_id_to_c_identifier(layout_id) data = load_yaml(layouts_dir, file) data_views = data['views'] data_buttons = data['buttons'] if 'buttons' in data else {} c_builder.add_section_comment(f'Layout: {layout_name} - generated from {layout_id}') c_builder.add_line() c_builder.add_line(f'static const char * const name_{layout_identifier} = "{layout_name}";') c_builder.add_line(f'static const char * const short_name_{layout_identifier} = "{layout_id}";') c_builder.add_line() layer_identifiers = [] view_ids = [view_id for view_id in data_views if view_id_to_layer_name(view_id) != None] for view_id in data_views: layer_name = view_id_to_layer_name(view_id) if not layer_name: warn(f'Ignoring unknown view_id {view_id}') continue layer_identifier = f'{view_id_to_c_identifier(view_id)}_{layout_identifier}' layer_identifiers.append(layer_identifier) c_builder.add_subsection_comment(f'Layer: {layer_name} - generated from {view_id}') c_builder.add_line() extra_top_row = None if view_id == "base": extra_top_row = args.extra_top_row_base if view_id == "upper": extra_top_row = args.extra_top_row_upper if not extra_top_row and (args.extra_top_row_base or args.extra_top_row_upper): extra_top_row = "" keycaps, attrs, modifier_idxs, switcher_idxs, switcher_dests, scancodes = get_keycaps_attrs_modifiers_switchers_scancodes( args, view_id, data_views, data_buttons, extra_top_row) for dest in switcher_dests: if dest not in view_ids: die(f'Unhandled layer switch destination {dest}') switcher_dests = [view_ids.index(d) for d in switcher_dests if d in view_ids] c_builder.add_line(f'static const int num_keys_{layer_identifier} = {sum([len(row) for row in keycaps])};') c_builder.add_line() c_builder.add_array(True, 'const char * const', f'keycaps_{layer_identifier}', keycaps, '"\\n"', '""') c_builder.add_line() c_builder.add_array(True, 'const lv_buttonmatrix_ctrl_t', f'attributes_{layer_identifier}', attrs, '', '') c_builder.add_line() c_builder.add_line(f'static const int num_modifiers_{layer_identifier} = {len(modifier_idxs)};') c_builder.add_line() c_builder.add_flat_array(True, 'const int', f'modifier_idxs_{layer_identifier}', modifier_idxs, '') c_builder.add_line() c_builder.add_line(f'static const int num_switchers_{layer_identifier} = {len(switcher_idxs)};') c_builder.add_line() c_builder.add_flat_array(True, 'const int', f'switcher_idxs_{layer_identifier}', switcher_idxs, '') c_builder.add_line() c_builder.add_flat_array(True, 'const int', f'switcher_dests_{layer_identifier}', switcher_dests, '') c_builder.add_line() if args.generate_scancodes: scancodes_flat, scancode_idxs, scancode_nums = flatten_scancodes(scancodes) for scancodes_in_row in scancodes_flat: for scancode in scancodes_in_row: unique_scancodes[scancode] = True c_builder.add_line(f'static const int num_scancodes_{layer_identifier} = {len(scancodes)};') c_builder.add_line() c_builder.add_array(True, 'const int', f'scancodes_{layer_identifier}', scancodes_flat, '', '') c_builder.add_line() c_builder.add_array(True, 'const int', f'scancode_idxs_{layer_identifier}', scancode_idxs, '', '') c_builder.add_line() c_builder.add_array(True, 'const int', f'scancode_nums_{layer_identifier}', scancode_nums, '', '') c_builder.add_line() c_builder.add_subsection_comment(f'Layer array') c_builder.add_line() c_builder.add_line(f'static const int num_layers_{layout_identifier} = {len(layer_identifiers)};') c_builder.add_line() c_builder.add_line(f'static const sq2lv_layer_t layers_{layout_identifier}[] = ' + '{') for i, identifier in enumerate(layer_identifiers): c_builder.add_line(' {') fields = ['num_keys', 'keycaps', 'attributes', 'num_modifiers', 'modifier_idxs', 'num_switchers', 'switcher_idxs', 'switcher_dests'] if args.generate_scancodes: fields += ['num_scancodes', 'scancodes', 'scancode_idxs', 'scancode_nums'] for k, field in enumerate(fields): c_builder.add_line(f' .{field} = {field}_{identifier}{comma_if_needed(fields, k)}') c_builder.add_line(' }' + comma_if_needed(layer_identifiers, i)) c_builder.add_line('};') c_builder.add_line() layouts.append({ 'name': layout_name, 'short_name': layout_id, 'identifier': layout_identifier }) h_builder.add_line('/* Layout IDs, values can be used as indexes into the sq2lv_layouts array */') h_builder.add_line('typedef enum {') h_builder.add_line(' SQ2LV_LAYOUT_NONE = -1,') for i, layout in enumerate(layouts): identifier = layout['identifier'].upper() h_builder.add_line(f' SQ2LV_LAYOUT_{identifier} = {i}{comma_if_needed(layouts, i)}') h_builder.add_line('} sq2lv_layout_id_t;') h_builder.add_line() h_builder.add_line('/* Layer type */') h_builder.add_line('typedef struct {') h_builder.add_line(' /* Number of keys */') h_builder.add_line(' const int num_keys;') h_builder.add_line(' /* Key caps */') h_builder.add_line(' const char * const * const keycaps;') h_builder.add_line(' /* Key attributes */') h_builder.add_line(' const lv_buttonmatrix_ctrl_t * const attributes;') h_builder.add_line(' /* Number of modifier keys */') h_builder.add_line(' const int num_modifiers;') h_builder.add_line(' /* Button indexes of modifier keys */') h_builder.add_line(' const int * const modifier_idxs;') h_builder.add_line(' /* Number of buttons that trigger a layer switch */') h_builder.add_line(' const int num_switchers;') h_builder.add_line(' /* Button indexes that trigger a layer switch */') h_builder.add_line(' const int * const switcher_idxs;') h_builder.add_line(' /* Indexes of layers to jump to when triggering layer switch buttons */') h_builder.add_line(' const int * const switcher_dests;') if args.generate_scancodes: h_builder.add_line(' /* Total number of scancodes */') h_builder.add_line(' const int num_scancodes;') h_builder.add_line(' /* Flat array of scancodes */') h_builder.add_line(' const int * const scancodes;') h_builder.add_line(' /* Start index in scancodes array for key cap */') h_builder.add_line(' const int * const scancode_idxs;') h_builder.add_line(' /* Number of scancodes for key cap */') h_builder.add_line(' const int * const scancode_nums;') h_builder.add_line('} sq2lv_layer_t;') h_builder.add_line() h_builder.add_line('/* Layout type */') h_builder.add_line('typedef struct {') h_builder.add_line(' /* Layout name */') h_builder.add_line(' const char * const name;') h_builder.add_line(' /* Layout short name */') h_builder.add_line(' const char * const short_name;') h_builder.add_line(' /* Total number of layers */') h_builder.add_line(' const int num_layers;') h_builder.add_line(' /* Layers array */') h_builder.add_line(' const sq2lv_layer_t * const layers;') h_builder.add_line('} sq2lv_layout_t;') h_builder.add_line() h_builder.add_line('/* Layouts */') h_builder.add_line('extern const int sq2lv_num_layouts;') h_builder.add_line('extern const sq2lv_layout_t sq2lv_layouts[];') h_builder.add_line() h_builder.add_line('/* Layout names (suitable for use in lv_dropdown_t) */') h_builder.add_line('extern const char * const sq2lv_layout_names;') h_builder.add_line('extern const char * const sq2lv_layout_short_names;') h_builder.add_line() if args.generate_scancodes: h_builder.add_line('/* Unique scancodes from all layout (suitable for setting up uinput devices) */') h_builder.add_line('extern const int sq2lv_num_unique_scancodes;') h_builder.add_line('extern const int sq2lv_unique_scancodes[];') h_builder.add_line() c_builder.add_section_comment('Public interface') c_builder.add_line() c_builder.add_line('const int sq2lv_num_layouts = ' + str(len(layouts)) + ';') c_builder.add_line() c_builder.add_line('const sq2lv_layout_t sq2lv_layouts[] = {') for i, layout in enumerate(layouts): c_builder.add_line(' /* ' + layout['name'] + ' */') c_builder.add_line(' {') fields = ['name', 'short_name', 'num_layers', 'layers'] identifier = layout['identifier'] for j, field in enumerate(fields): c_builder.add_line(f' .{field} = {field}_{identifier}{comma_if_needed(fields, j)}') c_builder.add_line(' }' + comma_if_needed(layouts, i)) c_builder.add_line('};') c_builder.add_line() names = [layout['name'] for layout in layouts] names = '\n ' + ' "\\n"\n '.join([f'"{name}"' for name in names]) c_builder.add_line(f'const char * const sq2lv_layout_names ={names};') c_builder.add_line() short_names = [layout['short_name'] for layout in layouts] short_names = '\n ' + ' "\\n"\n '.join([f'"{short_name}"' for short_name in short_names]) c_builder.add_line(f'const char * const sq2lv_layout_short_names ={short_names};') c_builder.add_line() if args.generate_scancodes: c_builder.add_line(f'const int sq2lv_num_unique_scancodes = {len(unique_scancodes)};') c_builder.add_line() c_builder.add_line('const int sq2lv_unique_scancodes[] = {') scancodes = list(unique_scancodes.keys()) chunks = [scancodes[i:i + 10] for i in range(0, len(scancodes), 10)] for i, chunk in enumerate(chunks): joined = ', '.join(chunk) c_builder.add_line(f' {joined}{comma_if_needed(chunks, i)}') c_builder.add_line('};') c_builder.add_line() h_builder.wrap_in_ifndef('SQ2LV_LAYOUTS_H') write_files(c_builder.lines, h_builder.lines)