buffybox/squeek2lvgl/squeek2lvgl.py

951 lines
33 KiB
Python
Raw Normal View History

#!/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']:
2024-01-25 09:59:45 +00:00
attributes.append('LV_BUTTONMATRIX_CTRL_POPOVER')
if not key_can_repeat(key):
2024-01-25 09:59:45 +00:00
attributes.append('LV_BUTTONMATRIX_CTRL_NO_REPEAT')
if is_extra_top_row:
attributes.append('SQ2LV_CTRL_NON_CHAR')
if key == '<hidden>':
2024-01-25 09:59:45 +00:00
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 "<hidden>" 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 = "<hidden>"
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()
2024-01-25 09:59:45 +00:00
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 */')
2024-01-25 09:59:45 +00:00
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)