diff --git a/README.md b/README.md index 5d71342..5761ba5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # squeek2lvgl -squeek2lvgl is a Python script that makes it possible to use [squeekboard] keyboard layouts with [LVGL]'s keyboard widget. To achieve this, [squeekboard]'s YAML syntax for layout definitions is converted into a C file (with an accompanying header) that can then be added to an [LVGL] project. +squeek2lvgl is a Python script and an accompanying C library that make it possible to use [squeekboard] keyboard layouts with [LVGL]'s keyboard widget. To achieve this, [squeekboard]'s YAML syntax for layout definitions is converted into C files that, together with the library C files, can then be added to an [LVGL] project. To access [squeekboard]'s layout files, squeek2lvgl shallowly clones the [squeekboard] git repository into a temporary directory and purges it before exiting. @@ -38,6 +38,33 @@ $ pipenv run python squeek2lvgl.py --input us.yaml --output . When the process has finished, `sq2lv_layouts.h` and `sq2lv_layouts.c` will have been written into the current directory. Check the [examples] folder for further details about the generated files. +To facilitate usage, a minimalist C API is available in [sq2lv.h] and [sq2lv.c]. + +### Integrating into a project + +Similar to [LVGL] squeek2lvgl should be added into a project as a git submodule. The generated C files assume that you have integrated [LVGL] as a submodule in the `./lvgl` folder. The C library files in turn assume that you have imported squeek2lvgl as a submodule one folder above the location of the generated files. + +The following is an example directory hierarchy: + +``` +$ tree . +. +├── lvgl # LVGL submodule +│   ├── ... +├── main.c # Project's own source files +├── ... +├── sq2lv_layouts.c # Layouts generated with squeek2lvgl +├── sq2lv_layouts.h +├── ... +├── squeek2lvgl # squeek2lvgl submodule +│   ├── sq2lv.c # squeek2lvgl library code +│   ├── sq2lv.h +│   ├── ... +├── ... +``` + +Using the directory structure above, you can then add `sq2lv_layouts.c` and `squeek2lvgl/sq2lv.c` into your build process just like the rest of your project's sources. + ## License squeek2lvgl is licensed under 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. @@ -48,3 +75,5 @@ squeek2lvgl is licensed under the GNU General Public License as published by the [squeekboard]: https://gitlab.gnome.org/World/Phosh/squeekboard [squeekboard's US layout]: https://gitlab.gnome.org/World/Phosh/squeekboard/-/blob/master/data/keyboards/us.yaml [examples]: ./examples +[sq2lv.h]: ./sq2lv.h +[sq2lv.c]: ./sq2lv.c diff --git a/examples/us-terminal-with-scancodes/sq2lv_layouts.h b/examples/us-terminal-with-scancodes/sq2lv_layouts.h index e8cff27..fc7697b 100644 --- a/examples/us-terminal-with-scancodes/sq2lv_layouts.h +++ b/examples/us-terminal-with-scancodes/sq2lv_layouts.h @@ -7,6 +7,8 @@ #include "lvgl/lvgl.h" +#define SQ2LV_SCANCODES_ENABLED 1 + /* Layout IDs, values can be used as indexes into the sq2lv_layouts array */ typedef enum { SQ2LV_LAYOUT_TERMINAL_US = 0 diff --git a/examples/us/sq2lv_layouts.h b/examples/us/sq2lv_layouts.h index 8ae6969..40fae3a 100644 --- a/examples/us/sq2lv_layouts.h +++ b/examples/us/sq2lv_layouts.h @@ -7,6 +7,8 @@ #include "lvgl/lvgl.h" +#define SQ2LV_SCANCODES_ENABLED 0 + /* Layout IDs, values can be used as indexes into the sq2lv_layouts array */ typedef enum { SQ2LV_LAYOUT_US = 0 diff --git a/sq2lv.c b/sq2lv.c new file mode 100644 index 0000000..66a308b --- /dev/null +++ b/sq2lv.c @@ -0,0 +1,231 @@ +/** + * 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 . + */ + + +#include "sq2lv.h" + + +/** + * Static variables + */ + +static sq2lv_layout_id_t current_layout_id = -1; + + +/** + * Static prototypes + */ + +/** + * Convert a layer index to an LVGL keyboard mode. + * + * @param layer_index layer index to convert + * @return corresponding LVGL keyboard mode + */ +static lv_keyboard_mode_t layer_index_to_keyboard_mode(int layer_index); + +/** + * Convert an LVGL keyboard mode to a layer index. + * + * @param keyboard_mode LVGL keyboard mode to convert + * @return corresponding layer index + */ +static int keyboard_mode_to_layer_index(lv_keyboard_mode_t keyboard_mode); + +/** + * Get the layer index corresponding to a keyboard's current mode. + * + * @param keyboard keyboard widget + * @return layer index + */ +static int get_layer_index(lv_obj_t *keyboard); + +/** + * Get the index of the destination layer for a layer switcher key in the current layout. + * + * @param keyboard keyboard widget + * @param btn_id button index corresponding to the key + * @return the destination layer's index or -1 if the key is not a layer switcher + */ +static int get_destination_layer_index_for_layer_switcher(lv_obj_t *keyboard, uint16_t btn_id); + + +/** + * Static functions + */ + +static lv_keyboard_mode_t layer_index_to_keyboard_mode(int layer_index) { + switch (layer_index) { + case 0: + return LV_KEYBOARD_MODE_TEXT_LOWER; + case 1: + return LV_KEYBOARD_MODE_TEXT_UPPER; + case 2: + return LV_KEYBOARD_MODE_SPECIAL; + case 3: + return LV_KEYBOARD_MODE_NUMBER; + default: + return -1; + } +} + +static int keyboard_mode_to_layer_index(lv_keyboard_mode_t keyboard_mode) { + switch (keyboard_mode) { + case LV_KEYBOARD_MODE_TEXT_LOWER: + return 0; + case LV_KEYBOARD_MODE_TEXT_UPPER: + return 1; + case LV_KEYBOARD_MODE_SPECIAL: + return 2; + case LV_KEYBOARD_MODE_NUMBER: + return 3; + default: + return -1; + } +} + +static int get_layer_index(lv_obj_t *keyboard) { + return keyboard_mode_to_layer_index(lv_keyboard_get_mode(keyboard)); +} + +static int get_destination_layer_index_for_layer_switcher(lv_obj_t *keyboard, uint16_t btn_id) { + if (current_layout_id < 0 || current_layout_id >= sq2lv_num_layouts) { + return -1; + } + + int layer_index = get_layer_index(keyboard); + if (layer_index < 0 || layer_index >= sq2lv_layouts[current_layout_id].num_layers) { + return -1; + } + + for (int i = 0; i < sq2lv_layouts[current_layout_id].layers[layer_index].num_switchers; ++i) { + if (sq2lv_layouts[current_layout_id].layers[layer_index].switcher_idxs[i] == btn_id) { + return sq2lv_layouts[current_layout_id].layers[layer_index].switcher_dests[i]; + } + } + + return -1; +} + + +/** + * Public functions + */ + +void sq2lv_switch_layout(lv_obj_t *keyboard, sq2lv_layout_id_t layout_id) { + if (layout_id < 0 || layout_id >= sq2lv_num_layouts) { + return; + } + + /* Assign layers */ + for (int i = 0; i < sq2lv_layouts[layout_id].num_layers; ++i) { + lv_keyboard_set_map(keyboard, layer_index_to_keyboard_mode(i), + sq2lv_layouts[layout_id].layers[i].keycaps, + sq2lv_layouts[layout_id].layers[i].attributes); + } + + /* Switch to default layer if current layer doesn't exist in new layout */ + int layer_index = get_layer_index(keyboard); + if (layer_index < 0 || layer_index >= sq2lv_layouts[layout_id].num_layers) { + lv_keyboard_set_mode(keyboard, layer_index_to_keyboard_mode(0)); + } + + current_layout_id = layout_id; +} + +bool sq2lv_is_layer_switcher(lv_obj_t *keyboard, uint16_t btn_id) { + return get_destination_layer_index_for_layer_switcher(keyboard, btn_id) >= 0; +} + +bool sq2lv_switch_layer(lv_obj_t *keyboard, uint16_t btn_id) { + int destination_layer_index = get_destination_layer_index_for_layer_switcher(keyboard, btn_id); + if (destination_layer_index < 0 || destination_layer_index >= sq2lv_layouts[current_layout_id].num_layers) { + return false; + } + + lv_keyboard_set_mode(keyboard, layer_index_to_keyboard_mode(destination_layer_index)); + return true; +} + +bool sq2lv_is_modifier(lv_obj_t *keyboard, uint16_t btn_id) { + if (current_layout_id < 0 || current_layout_id >= sq2lv_num_layouts) { + return false; + } + + int layer_index = get_layer_index(keyboard); + if (layer_index < 0 || layer_index >= sq2lv_layouts[current_layout_id].num_layers) { + return false; + } + + for (int i = 0; i < sq2lv_layouts[current_layout_id].layers[layer_index].num_modifiers; ++i) { + if (sq2lv_layouts[current_layout_id].layers[layer_index].modifier_idxs[i] == btn_id) { + return true; + } + } + + return false; +} + +const int * const sq2lv_get_modifier_indexes(lv_obj_t *keyboard, int *num_modifiers) { + if (current_layout_id < 0 || current_layout_id >= sq2lv_num_layouts) { + *num_modifiers = 0; + return NULL; + } + + int layer_index = get_layer_index(keyboard); + if (layer_index < 0 || layer_index >= sq2lv_layouts[current_layout_id].num_layers) { + *num_modifiers = 0; + return NULL; + } + + *num_modifiers = sq2lv_layouts[current_layout_id].layers[layer_index].num_modifiers; + if (*num_modifiers == 0) { + return NULL; + } + + return &(sq2lv_layouts[current_layout_id].layers[layer_index].modifier_idxs[0]); +} + +#if SQ2LV_SCANCODES_ENABLED +const int * const sq2lv_get_scancodes(lv_obj_t *keyboard, uint16_t btn_id, int *num_scancodes) { + if (current_layout_id < 0 || current_layout_id >= sq2lv_num_layouts) { + *num_scancodes = 0; + return NULL; + } + + int layer_index = get_layer_index(keyboard); + if (layer_index < 0 || layer_index >= sq2lv_layouts[current_layout_id].num_layers) { + *num_scancodes = 0; + return NULL; + } + + if (btn_id >= sq2lv_layouts[current_layout_id].layers[layer_index].num_keys) { + *num_scancodes = 0; + return NULL; + } + + *num_scancodes = sq2lv_layouts[current_layout_id].layers[layer_index].scancode_nums[btn_id]; + if (*num_scancodes == 0) { + return NULL; + } + + const int index = sq2lv_layouts[current_layout_id].layers[layer_index].scancode_idxs[btn_id]; + return &(sq2lv_layouts[current_layout_id].layers[layer_index].scancodes[index]); +} +#endif /* SQ2LV_SCANCODES_ENABLED */ diff --git a/sq2lv.h b/sq2lv.h new file mode 100644 index 0000000..e05e883 --- /dev/null +++ b/sq2lv.h @@ -0,0 +1,82 @@ +/** + * 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 . + */ + + +#ifndef SQ2LV_H +#define SQ2LV_H + +#include "../sq2lv_layouts.h" + +/** + * Apply a layout to a keyboard. + * + * @param keyboard keyboard widget + * @param layout_id layout ID + */ +void sq2lv_switch_layout(lv_obj_t *keyboard, sq2lv_layout_id_t layout_id); + +/** + * Check if a key acts as a layer switcher in the current layer. + * + * @param keyboard keyboard widget + * @param btn_id button index corresponding to the key + * @return true if a layer switch would happen if the key is pressed, false otherwise + */ +bool sq2lv_is_layer_switcher(lv_obj_t *keyboard, uint16_t btn_id); + +/** + * Attempt to perform a layer switch after pressing a key in the current layer. + * + * @param keyboard keyboard widget + * @param btn_id button index corresponding to the pressed key + * @return true if a layer switch was performed, false otherwise + */ +bool sq2lv_switch_layer(lv_obj_t *keyboard, uint16_t btn_id); + +/** + * Check if a key is a modifier in the current layer. + * + * @param keyboard keyboard widget + * @param btn_id button index corresponding to the key + * @return true if the key is a modifier, false otherwise + */ +bool sq2lv_is_modifier(lv_obj_t *keyboard, uint16_t btn_id); + +/** + * Get the button indexes for all modifier keys in the current layer. + * + * @param keyboard keyboard widget + * @param num_modifiers pointer to an integer into which the number of modifiers will be written + * @return pointer to the array of button indexes corresponding to modifier keys + */ +const int * const sq2lv_get_modifier_indexes(lv_obj_t *keyboard, int *num_modifiers); + +#if SQ2LV_SCANCODES_ENABLED +/** + * Get scancodes associated with a key in the current layer. + * + * @param keyboard keyboard widget + * @param btn_id button index corresponding to the key + * @param num_scancodes pointer to an integer into which the number of scancodes will be written + * @return pointer into an array of scancodes at the appropriate index + */ +const int * const sq2lv_get_scancodes(lv_obj_t *keyboard, uint16_t btn_id, int *num_scancodes); +#endif /* SQ2LV_SCANCODES_ENABLED */ + +#endif /* SQ2LV_H */ diff --git a/squeek2lvgl.py b/squeek2lvgl.py index 9d2c4b0..7266df0 100644 --- a/squeek2lvgl.py +++ b/squeek2lvgl.py @@ -622,6 +622,8 @@ if __name__ == '__main__': 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 = {}