830 lines
No EOL
28 KiB
Python
830 lines
No EOL
28 KiB
Python
#!/usr/bin/env python3
|
|
|
|
# Copyright 2021 Johannes Marbach
|
|
#
|
|
# This file is part of squeek2lvgl, hereafter referred to as the program.
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
import argparse
|
|
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('--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 unsqu33kr', ' **/', ''])
|
|
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 ''
|
|
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
|
|
##
|
|
|
|
layout_name_for_layout_id = {
|
|
'terminal/us': 'US English (Terminal)',
|
|
'de': 'German',
|
|
'es': 'Spanish',
|
|
'fr': 'French',
|
|
'us': 'US English'
|
|
}
|
|
|
|
def layout_id_to_layout_name(layout_id):
|
|
"""Return a descriptive name for a layout.
|
|
|
|
layout_id -- ID of the layout
|
|
"""
|
|
return layout_name_for_layout_id[layout_id]
|
|
|
|
|
|
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'
|
|
}
|
|
|
|
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
|
|
|
|
|
|
keycap_for_key = {
|
|
'\\': '\\\\',
|
|
'"': '\\"',
|
|
'↑': 'LV_SYMBOL_UP',
|
|
'↓': 'LV_SYMBOL_DOWN',
|
|
'←': 'LV_SYMBOL_LEFT',
|
|
'→': 'LV_SYMBOL_RIGHT',
|
|
'BackSpace': 'LV_SYMBOL_BACKSPACE',
|
|
'colon': ':',
|
|
'period': '.',
|
|
'preferences': None,
|
|
'Shift_L': {
|
|
'base': 'ABC',
|
|
'upper': 'abc'
|
|
},
|
|
'show_actions': None,
|
|
'show_eschars': {
|
|
'de': 'äöü',
|
|
'es': 'áéí',
|
|
'fr': 'àéô'
|
|
},
|
|
'show_letters': 'abc',
|
|
'show_numbers': '1#',
|
|
'show_numbers_from_symbols': '1#',
|
|
'show_symbols': None,
|
|
'space': ' ',
|
|
'Return': 'LV_SYMBOL_OK',
|
|
}
|
|
|
|
def key_to_keycap(key, view_id, layout_id):
|
|
"""Return the keycap for a key
|
|
|
|
key -- the key
|
|
view_id -- ID of the view the key appears on
|
|
layout_id -- ID of the layout the view is part of
|
|
"""
|
|
keycap = keycap_for_key[key] if key in keycap_for_key else key
|
|
if isinstance(keycap, dict):
|
|
if view_id in keycap:
|
|
keycap = keycap[view_id]
|
|
elif layout_id in keycap:
|
|
keycap = keycap[layout_id]
|
|
else:
|
|
keycap = None
|
|
return keycap
|
|
|
|
|
|
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]
|
|
|
|
|
|
def key_to_attributes(key, data_buttons):
|
|
"""Return the LVGL button attributes for a key.
|
|
|
|
key -- the key in question
|
|
data_buttons -- the "buttons" object from the layout's YAML file
|
|
"""
|
|
attributes = []
|
|
|
|
if key in data_buttons and key not in ['period', 'colon', '"']:
|
|
attributes.append('LV_KEYBOARD_CTRL_BTN_FLAGS')
|
|
|
|
if key_is_modifier(key, data_buttons):
|
|
attributes.append('LV_BTNMATRIX_CTRL_CHECKABLE')
|
|
|
|
if key not in data_buttons or key in ['period', 'colon', '"']:
|
|
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_') 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': [],
|
|
'1#': [],
|
|
'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_LEFTSHIFT', 'KEY_QUESTION'],
|
|
'&': ['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_QUESTION'],
|
|
'.': ['KEY_DOT']
|
|
}
|
|
|
|
def keycap_to_scancodes(keycap):
|
|
"""Return the scancodes needed to produce a keycap
|
|
|
|
keycap -- keycap to produce
|
|
"""
|
|
if keycap not in scancodes_for_keycap:
|
|
die(f'cannot determine scancodes for unknown keycap "{keycap}"')
|
|
return scancodes_for_keycap[keycap]
|
|
|
|
|
|
def get_keycaps_attrs_modifiers_switchers_scancodes(args, layout_id, view_id, data_views, data_buttons):
|
|
"""Return keycaps, LVGL button attributes, modifier key indexes, layer switching key indexes,
|
|
layer switching key destinations and scancodes for a view
|
|
|
|
args -- Commandline arguments
|
|
layout_id -- ID of the layout
|
|
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
|
|
"""
|
|
keycaps = []
|
|
attrs = []
|
|
modifier_idxs = []
|
|
switcher_idxs = []
|
|
switcher_dests = []
|
|
scancodes = []
|
|
|
|
idx = 0
|
|
|
|
for row in data_views[view_id]:
|
|
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:
|
|
keycap = key_to_keycap(key, view_id, layout_id)
|
|
if not keycap:
|
|
continue
|
|
|
|
keycaps_in_row.append(keycap_to_c_value(keycap))
|
|
attrs_in_row.append(key_to_attributes(key, data_buttons))
|
|
|
|
if key_is_modifier(key, data_buttons):
|
|
modifier_idxs.append(idx)
|
|
|
|
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']
|
|
elif action['locking']['unlock_view'] == view_id:
|
|
dest = action['locking']['lock_view']
|
|
if dest:
|
|
switcher_idxs.append(idx)
|
|
switcher_dests.append(dest)
|
|
|
|
if args.generate_scancodes:
|
|
scancodes_in_row.append(keycap_to_scancodes(keycap))
|
|
|
|
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)
|
|
if args.generate_scancodes:
|
|
c_builder.add_system_include('linux/input.h')
|
|
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 in args.input:
|
|
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_id_to_layout_name(layout_id)} - generated from {layout_id}')
|
|
c_builder.add_line()
|
|
c_builder.add_line(f'static const char * const 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()
|
|
|
|
keycaps, attrs, modifier_idxs, switcher_idxs, switcher_dests, scancodes = get_keycaps_attrs_modifiers_switchers_scancodes(
|
|
args, layout_id, view_id, data_views, data_buttons)
|
|
|
|
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_btnmatrix_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_id_to_layout_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 {')
|
|
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 keycaps;')
|
|
h_builder.add_line(' /* Button matrix attributes */')
|
|
h_builder.add_line(' const lv_btnmatrix_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(' /* 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()
|
|
|
|
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', '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()
|
|
|
|
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) |