Files
buffybox/squeek2lvgl/squeek2lvgl.py
2024-03-30 08:06:29 +01:00

951 lines
33 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 == '<hidden>':
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()
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)