+
+
Keyman for Linux Code Coverage Reports
+
+
+
+
+
+
+
+
+
+
+
diff --git a/linux/README.md b/linux/README.md
index e4b50cdc90..3dfb340477 100644
--- a/linux/README.md
+++ b/linux/README.md
@@ -1,5 +1,3 @@
# Keyman for Linux
See [/docs/linux/README.md](../docs/linux/README.md) for documentation.
-
-.
\ No newline at end of file
diff --git a/linux/build.sh b/linux/build.sh
index 3b3648dd14..9a4959c5d0 100755
--- a/linux/build.sh
+++ b/linux/build.sh
@@ -23,8 +23,20 @@ builder_describe \
"test" \
"install install artifacts" \
"uninstall uninstall artifacts" \
- "--no-integration+ don't run integration tests"
+ "--no-integration+ don't run integration tests" \
+ "--coverage+ capture test coverage" \
+ "--report+ create coverage report" \
+ "--open open the coverage reports in the browser"
builder_parse "$@"
builder_run_child_actions clean configure build test install uninstall
+
+test_action() {
+ if builder_has_option --open; then
+ builder_echo "Opening coverage reports in browser..."
+ xdg-open "file://${THIS_SCRIPT_PATH}/CodeCoverageReports.html"
+ fi
+}
+
+builder_run_action test test_action
diff --git a/linux/debian/control b/linux/debian/control
index 624f89fc0a..9b5e217aba 100644
--- a/linux/debian/control
+++ b/linux/debian/control
@@ -107,12 +107,14 @@ Description: Keyman for Linux configuration
This package contains the Python module to install, uninstall and view
information about Keyman keyboard packages.
-Package: libkmnkbp-dev
+Package: libkeymancore-dev
Architecture: amd64 arm64 armel armhf i386 mipsel mips64el ppc64el riscv64
Section: libdevel
Depends:
- libkmnkbp0-0 (= ${binary:Version}),
+ libkeymancore (= ${binary:Version}),
${misc:Depends},
+Conflicts: libkmnkbp-dev
+Replaces: libkmnkbp-dev
Multi-Arch: same
Description: Development files for Keyman keyboard processing library
Originally created in 1993 to type Lao on Windows, Keyman is now a free and
@@ -126,12 +128,12 @@ Description: Development files for Keyman keyboard processing library
Keyboard layouts are distributed through an open catalog to all major desktop
and mobile platforms.
.
- The Keyman keyboardprocessor library processes input from input method engines
+ The Keyman core library processes input from input method engines
and applies rules from compiled Keyman keyboard files.
.
This package contains development headers and libraries.
-Package: libkmnkbp0-0
+Package: libkeymancore
Architecture: amd64 arm64 armel armhf i386 mipsel mips64el ppc64el riscv64
Section: libs
Pre-Depends:
@@ -141,6 +143,8 @@ Depends:
${shlibs:Depends},
Suggests:
keyman,
+Conflicts: libkmnkbp0-0
+Replaces: libkmnkbp0-0
Multi-Arch: same
Description: Keyman keyboard processing library
Originally created in 1993 to type Lao on Windows, Keyman is now a free and
@@ -154,14 +158,14 @@ Description: Keyman keyboard processing library
Keyboard layouts are distributed through an open catalog to all major desktop
and mobile platforms.
.
- The Keyman keyboardprocessor library processes input from input method engines
+ The Keyman core library processes input from input method engines
and applies rules from compiled Keyman keyboard files.
Package: ibus-keyman
Architecture: amd64 arm64 armel armhf i386 mipsel mips64el ppc64el riscv64
Depends:
ibus (>= 1.3.7),
- libkmnkbp0-0 (= ${binary:Version}),
+ libkeymancore (= ${binary:Version}),
keyman-system-service (= ${binary:Version}),
${misc:Depends},
${shlibs:Depends},
diff --git a/linux/debian/libkmnkbp-dev.install b/linux/debian/libkeymancore-dev.install
similarity index 100%
rename from linux/debian/libkmnkbp-dev.install
rename to linux/debian/libkeymancore-dev.install
diff --git a/linux/debian/libkmnkbp0-0.install b/linux/debian/libkeymancore.install
similarity index 100%
rename from linux/debian/libkmnkbp0-0.install
rename to linux/debian/libkeymancore.install
diff --git a/linux/debian/libkeymancore.symbols b/linux/debian/libkeymancore.symbols
new file mode 100644
index 0000000000..9482ded8ae
--- /dev/null
+++ b/linux/debian/libkeymancore.symbols
@@ -0,0 +1,48 @@
+libkeymancore.so.1 libkeymancore #MINVER#
+* Build-Depends-Package: libkeymancore-dev
+
+ km_core_actions_dispose@Base 17.0.197
+ km_core_context_append@Base 17.0.195
+ km_core_context_clear@Base 17.0.195
+ km_core_context_get@Base 17.0.195
+ km_core_context_item_list_size@Base 17.0.195
+ km_core_context_items_dispose@Base 17.0.195
+ km_core_context_items_from_utf16@Base 17.0.195
+ km_core_context_items_from_utf8@Base 17.0.195
+ km_core_context_items_to_utf16@Base 17.0.195
+ km_core_context_items_to_utf32@Base 17.0.197
+ km_core_context_items_to_utf8@Base 17.0.195
+ km_core_context_length@Base 17.0.195
+ km_core_context_set@Base 17.0.195
+ km_core_context_shrink@Base 17.0.195
+ km_core_event@Base 17.0.195
+ km_core_get_engine_attrs@Base 17.0.195
+ km_core_keyboard_dispose@Base 17.0.195
+ km_core_keyboard_get_attrs@Base 17.0.195
+ km_core_keyboard_get_imx_list@Base 17.0.195
+ km_core_keyboard_get_key_list@Base 17.0.195
+ km_core_keyboard_imx_list_dispose@Base 17.0.195
+ km_core_keyboard_key_list_dispose@Base 17.0.195
+ km_core_keyboard_load@Base 17.0.195
+ km_core_options_list_size@Base 17.0.195
+ km_core_process_event@Base 17.0.195
+ km_core_process_queued_actions@Base 17.0.195
+ km_core_state_action_items@Base 17.0.195
+ km_core_state_clone@Base 17.0.195
+ km_core_state_context@Base 17.0.195
+ km_core_state_context_clear@Base 17.0.197
+ km_core_state_context_set_if_needed@Base 17.0.197
+ km_core_state_create@Base 17.0.195
+ km_core_state_debug_get@Base 17.0.195
+ km_core_state_debug_items@Base 17.0.195
+ km_core_state_debug_set@Base 17.0.195
+ km_core_state_dispose@Base 17.0.195
+ km_core_state_get_actions@Base 17.0.197
+ km_core_state_get_intermediate_context@Base 17.0.195
+ km_core_state_imx_deregister_callback@Base 17.0.195
+ km_core_state_imx_register_callback@Base 17.0.195
+ km_core_state_option_lookup@Base 17.0.195
+ km_core_state_options_to_json@Base 17.0.195
+ km_core_state_options_update@Base 17.0.195
+ km_core_state_queue_action_items@Base 17.0.195
+ km_core_state_to_json@Base 17.0.195
diff --git a/linux/debian/libkmnkbp0-0.symbols b/linux/debian/libkmnkbp0-0.symbols
deleted file mode 100644
index cbcbb6f4ce..0000000000
--- a/linux/debian/libkmnkbp0-0.symbols
+++ /dev/null
@@ -1,43 +0,0 @@
-libkmnkbp0.so.0 libkmnkbp0-0 #MINVER#
-* Build-Depends-Package: libkmnkbp-dev
-
- kbp_state_get_intermediate_context@Base 15.0
- km_core_context_append@Base 15.0
- km_core_context_clear@Base 15.0
- km_core_context_get@Base 15.0
- km_core_context_item_list_size@Base 15.0
- km_core_context_items_dispose@Base 15.0
- km_core_context_items_from_utf16@Base 15.0
- km_core_context_items_from_utf8@Base 15.0
- km_core_context_items_to_utf16@Base 15.0
- km_core_context_items_to_utf8@Base 15.0
- km_core_context_length@Base 15.0
- km_core_context_set@Base 15.0
- km_core_context_shrink@Base 15.0
- km_core_event@Base 16.0.105
- km_core_get_engine_attrs@Base 15.0
- km_core_keyboard_dispose@Base 15.0
- km_core_keyboard_get_attrs@Base 15.0
- km_core_keyboard_get_imx_list@Base 15.0
- km_core_keyboard_get_key_list@Base 15.0
- km_core_keyboard_imx_list_dispose@Base 15.0
- km_core_keyboard_key_list_dispose@Base 15.0
- km_core_keyboard_load@Base 15.0
- km_core_options_list_size@Base 15.0
- km_core_process_event@Base 15.0
- km_core_process_queued_actions@Base 15.0
- km_core_state_action_items@Base 15.0
- km_core_state_clone@Base 15.0
- km_core_state_context@Base 15.0
- km_core_state_create@Base 15.0
- km_core_state_debug_get@Base 15.0
- km_core_state_debug_items@Base 15.0
- km_core_state_debug_set@Base 15.0
- km_core_state_dispose@Base 15.0
- km_core_state_imx_deregister_callback@Base 15.0
- km_core_state_imx_register_callback@Base 15.0
- km_core_state_option_lookup@Base 15.0
- km_core_state_options_to_json@Base 15.0
- km_core_state_options_update@Base 15.0
- km_core_state_queue_action_items@Base 15.0
- km_core_state_to_json@Base 15.0
diff --git a/linux/debian/rules b/linux/debian/rules
index 1abfbf004a..6b2e8c75c1 100755
--- a/linux/debian/rules
+++ b/linux/debian/rules
@@ -24,7 +24,7 @@ include /usr/share/dpkg/pkg-info.mk
dh $@ --with-python3
override_dh_auto_configure:
- # keymankeyboardprocessor - we need to configure and build core before
+ # keyman core - we need to configure and build core before
# we can configure ibus-keyman!
core/build.sh --no-tests configure:arch build:arch -- \
--wrap-mode=nodownload --prefix=/usr --sysconfdir=/etc \
diff --git a/linux/debian/tests/control b/linux/debian/tests/control
index c1164c9c8e..ca633c6571 100644
--- a/linux/debian/tests/control
+++ b/linux/debian/tests/control
@@ -1,5 +1,6 @@
Tests: test-build
Depends: @,
build-essential,
+ libicu-dev,
pkg-config,
Architecture: amd64 arm64 armel armhf i386 mips64el mipsel ppc64el riscv64
diff --git a/linux/debian/tests/test-build b/linux/debian/tests/test-build
index e9a989e36f..cdd20dc68e 100644
--- a/linux/debian/tests/test-build
+++ b/linux/debian/tests/test-build
@@ -1,5 +1,5 @@
#!/bin/bash
-# autopkgtest check: Build a program against libkmnkbp-dev to verify that the
+# autopkgtest check: Build a program against libkeymancore-dev to verify that the
# headers and pkg-config file are included and installed correctly.
# cf https://github.com/keymanapp/keyman/issues/7490
@@ -16,7 +16,7 @@ km_core_context* c;
EOF
# shellcheck disable=SC2046
-gcc -c keymantest.c $(pkg-config --cflags --libs keyman_kmn_processor)
+gcc -c keymantest.c $(pkg-config --cflags --libs keyman_core)
echo "build 1: OK"
# Test pkg-config file - include without path
@@ -26,5 +26,5 @@ km_core_context* c;
EOF
# shellcheck disable=SC2046
-gcc -c keymantest.c $(pkg-config --cflags --libs keyman_kmn_processor)
+gcc -c keymantest.c $(pkg-config --cflags --libs keyman_core)
echo "build 2: OK"
diff --git a/linux/ibus-keyman/build.sh b/linux/ibus-keyman/build.sh
index c2c45deec1..5e13ef12e0 100755
--- a/linux/ibus-keyman/build.sh
+++ b/linux/ibus-keyman/build.sh
@@ -49,46 +49,31 @@ else
MESON_COVERAGE=
fi
-if builder_start_action clean; then
- rm -rf "$THIS_SCRIPT_PATH/../build/"
- builder_finish_action success clean
-fi
+configure_action() {
+ # shellcheck disable=SC2086,SC2154
+ meson setup ${MESON_COVERAGE} --werror --buildtype $MESON_TARGET "${builder_extra_params[@]}" "$MESON_PATH"
+}
-if builder_start_action configure; then
- cd "$THIS_SCRIPT_PATH"
- # shellcheck disable=SC2086
- meson setup "$MESON_PATH" --werror --buildtype $MESON_TARGET ${MESON_COVERAGE} "${builder_extra_params[@]}"
- builder_finish_action success configure
-fi
-
-if builder_start_action build; then
- cd "$THIS_SCRIPT_PATH/$MESON_PATH"
- ninja
- builder_finish_action success build
-fi
-
-if builder_start_action test; then
- cd "$THIS_SCRIPT_PATH/$MESON_PATH"
+test_action() {
if builder_has_option --no-integration; then
+ # shellcheck disable=SC2086,SC2154
meson test --print-errorlogs $builder_verbose setup-src-test keymanutil-tests print-kmpdetails-test print-kmp-test bcp47-util-tests teardown-src-test
else
+ # shellcheck disable=SC2086
meson test --print-errorlogs $builder_verbose
fi
if builder_has_option --coverage; then
# Note: requires lcov > 1.16 to properly work (see https://github.com/mesonbuild/meson/issues/6747)
ninja coverage-html
fi
- builder_finish_action success test
-fi
+}
-if builder_start_action install; then
- cd "$THIS_SCRIPT_PATH/$MESON_PATH"
- ninja install
- builder_finish_action success install
-fi
+builder_run_action clean rm -rf "$THIS_SCRIPT_PATH/../build/"
+builder_run_action configure configure_action
-if builder_start_action uninstall; then
- cd "$THIS_SCRIPT_PATH/$MESON_PATH"
- ninja uninstall
- builder_finish_action success uninstall
-fi
+[ -d "$MESON_PATH" ] && cd "$MESON_PATH"
+
+builder_run_action build ninja
+builder_run_action test test_action
+builder_run_action install ninja install
+builder_run_action uninstall ninja uninstall
diff --git a/linux/ibus-keyman/meson.build b/linux/ibus-keyman/meson.build
index 2e402b868e..45539b5ab7 100644
--- a/linux/ibus-keyman/meson.build
+++ b/linux/ibus-keyman/meson.build
@@ -18,8 +18,8 @@ icu = dependency('icu-i18n')
core_dir = meson.current_source_dir() / '../../core'
common_dir = meson.current_source_dir() / '../../common'
-kmnkbp_lib = cc.find_library(
- 'kmnkbp0', # meson will prefix 'lib'
+keymancore_lib = cc.find_library(
+ 'keymancore', # meson will prefix 'lib'
dirs: [ core_dir / 'build/arch' / get_option('buildtype') / 'src' ]
)
diff --git a/linux/ibus-keyman/src/keymanutil.c b/linux/ibus-keyman/src/keymanutil.c
index 4f797aa07b..d8b6b48d60 100644
--- a/linux/ibus-keyman/src/keymanutil.c
+++ b/linux/ibus-keyman/src/keymanutil.c
@@ -65,6 +65,21 @@
#define N_(text) text
+GHashTable *custom_keyboards = NULL;
+
+void free_cust_kbd(gpointer data) {
+ if (data == NULL)
+ return;
+
+ cust_kbd *kbd_data = (cust_kbd *)data;
+
+ g_free(kbd_data->kb_id_with_lang);
+ g_free(kbd_data->lang->id);
+ g_free(kbd_data->lang->name);
+ g_free(kbd_data->lang);
+ g_free(kbd_data);
+}
+
// change to keyman_get_kmpdirs_fromdir
// returns list of directories with kmp.json
GList * keyman_get_kmpdirs_fromdir(GList *kmpdir_list, const gchar * path)
@@ -151,18 +166,17 @@ get_engine_for_language(
kmp_info *info,
keyboard_details *kbd_details,
gchar *kmp_dir,
- gchar *lang_id,
- gchar *lang_name) {
+ kmp_language *lang) {
IBusEngineDesc* engine_desc = NULL;
- if (!lang_id || !strlen(lang_id))
+ if (!lang || !lang->id || !strlen(lang->id))
return engine_desc;
int capacity = 255;
g_autofree gchar *name_with_lang = NULL;
g_autofree gchar *minimized_tag = g_new0(gchar, capacity);
- int result = bcp47_minimize(lang_id, minimized_tag, capacity);
+ int result = bcp47_minimize(lang->id, minimized_tag, capacity);
if (result < 0) {
- g_strlcpy(minimized_tag, lang_id, capacity);
+ g_strlcpy(minimized_tag, lang->id, capacity);
}
g_autofree gchar *lang_code = g_new0(gchar, capacity);
@@ -172,11 +186,11 @@ get_engine_for_language(
// If ibus doesn't know about the language then append the
// language name to the keyboard name
- if (lang_name != NULL) {
+ if (lang->name != NULL) {
g_autofree gchar *ibus_lang = ibus_get_untranslated_language_name(lang_code);
g_debug("%s: untranslated ibus language for %s: %s", __FUNCTION__, minimized_tag, ibus_lang);
if (g_strcmp0(ibus_lang, "Other") == 0) {
- name_with_lang = g_strjoin(" - ", keyboard->name, lang_name, NULL);
+ name_with_lang = g_strjoin(" - ", keyboard->name, lang->name, NULL);
}
}
@@ -198,13 +212,12 @@ get_engine_for_language(
return engine_desc;
}
-// Add a keyboard (ibus engine) to the list of engines
-void
-keyman_add_keyboard(gpointer data, gpointer user_data) {
- kmp_keyboard *keyboard = (kmp_keyboard *)data;
- add_keyboard_data *kb_data = (add_keyboard_data *)user_data;
-
- for (GList *e = kb_data->engines_list; e != NULL; e = e->next) {
+gboolean
+keyman_list_contains_keyboard(
+ GList *engines_list,
+ kmp_keyboard *keyboard
+) {
+ for (GList *e = engines_list; e != NULL; e = e->next) {
IBusEngineDesc *engine_desc = (IBusEngineDesc *)e->data;
const gchar *version = ibus_engine_desc_get_version(engine_desc);
const gchar *engine_name = ibus_engine_desc_get_name(engine_desc);
@@ -215,40 +228,90 @@ keyman_add_keyboard(gpointer data, gpointer user_data) {
// TODO: fix version comparison (#9593)
if (g_strcmp0(kmx_file, keyboard->kmx_file) == 0 && g_strcmp0(version, keyboard->version) >= 0) {
g_debug("keyboard %s already exists at version %s which is newer or same as %s", kmx_file, version, keyboard->version);
- return;
+ return TRUE;
}
}
+ return FALSE;
+}
+
+GList *
+keyman_add_custom_keyboards(
+ kmp_keyboard *keyboard,
+ add_keyboard_data *kb_data,
+ keyboard_details * kbd_details,
+ gchar * kmx_path
+) {
+ GList * engines_list = kb_data->engines_list;
+ GPtrArray *language_keyboards = g_hash_table_lookup(custom_keyboards, kmx_path);
+ if (language_keyboards == NULL)
+ return engines_list;
+
+ for (int i = 0; i < language_keyboards->len; i++) {
+ cust_kbd *data = (cust_kbd *)g_ptr_array_index(language_keyboards, i);
+ IBusEngineDesc *engine_desc = get_engine_for_language(keyboard, kb_data->info, kbd_details, kb_data->kmp_dir, data->lang);
+ if (engine_desc) {
+ engines_list = g_list_append(engines_list, engine_desc);
+ }
+ }
+ return engines_list;
+}
+
+GList *
+keyman_add_keyboards_for_language_if_given(
+ kmp_keyboard *keyboard,
+ add_keyboard_data *kb_data,
+ keyboard_details * kbd_details,
+ gchar * kmx_path
+) {
+ GList * engines_list = kb_data->engines_list;
+ if (keyboard->languages != NULL) {
+ for (GList *l = keyboard->languages; l != NULL; l = l->next) {
+ kmp_language *language = (kmp_language *)l->data;
+ IBusEngineDesc *engine_desc =
+ get_engine_for_language(keyboard, kb_data->info, kbd_details, kb_data->kmp_dir, language);
+ if (engine_desc) {
+ engines_list = g_list_append(engines_list, engine_desc);
+ }
+ }
+ } else {
+ g_message("adding engine %s", kmx_path);
+ engines_list = g_list_append(
+ engines_list,
+ ibus_keyman_engine_desc_new(
+ kmx_path, // kmx full path
+ keyboard->name, // longname
+ kbd_details->description, // description
+ kb_data->info->copyright, // copyright if available
+ NULL, // language, most are ignored by ibus except major languages
+ kbd_details->license, // license
+ kb_data->info->author_desc, // author name only, not email
+ keyman_get_icon_file(kmx_path), // icon full path
+ "us", // layout defaulting to us (en-US)
+ keyboard->version));
+ }
+ return engines_list;
+}
+
+// Add a keyboard (ibus engine) to the list of engines
+void
+keyman_add_keyboard(
+ gpointer data,
+ gpointer user_data
+) {
+ kmp_keyboard *keyboard = (kmp_keyboard *)data;
+ add_keyboard_data *kb_data = (add_keyboard_data *)user_data;
+
+ if (keyman_list_contains_keyboard(kb_data->engines_list, keyboard)) {
+ return;
+ }
g_autofree gchar *json_file = g_strjoin(".", keyboard->id, "json", NULL);
g_autoptr(keyboard_details) kbd_details = g_new0(keyboard_details, 1);
get_keyboard_details(kb_data->kmp_dir, json_file, kbd_details);
g_autofree gchar *abs_kmx = g_strjoin("/", kb_data->kmp_dir, keyboard->kmx_file, NULL);
- if (keyboard->languages != NULL) {
- for (GList *l = keyboard->languages; l != NULL; l = l->next) {
- kmp_language *language = (kmp_language *)l->data;
- IBusEngineDesc *engine_desc =
- get_engine_for_language(keyboard, kb_data->info, kbd_details, kb_data->kmp_dir, language->id, language->name);
- if (engine_desc) {
- kb_data->engines_list = g_list_append(kb_data->engines_list, engine_desc);
- }
- }
- } else {
- g_message("adding engine %s", abs_kmx);
- kb_data->engines_list = g_list_append(
- kb_data->engines_list,
- ibus_keyman_engine_desc_new(
- abs_kmx, // kmx full path
- keyboard->name, // longname
- kbd_details->description, // description
- kb_data->info->copyright, // copyright if available
- NULL, // language, most are ignored by ibus except major languages
- kbd_details->license, // license
- kb_data->info->author_desc, // author name only, not email
- keyman_get_icon_file(abs_kmx), // icon full path
- "us", // layout defaulting to us (en-US)
- keyboard->version));
- }
+ kb_data->engines_list = keyman_add_keyboards_for_language_if_given(keyboard, kb_data, kbd_details, abs_kmx);
+ kb_data->engines_list = keyman_add_custom_keyboards(keyboard, kb_data, kbd_details, abs_kmx);
}
// Add keyboards found in {kmp_dir}/kmp.json to engines_list
@@ -258,6 +321,7 @@ keyman_add_keyboards_from_dir(gpointer data, gpointer user_data) {
GList ** engines_list = (GList **)user_data;
g_autoptr(kmp_details) details = g_new0(kmp_details, 1);
+
if (get_kmp_details(kmp_dir, details) == JSON_OK) {
add_keyboard_data kb_data;
kb_data.engines_list = *engines_list;
@@ -270,13 +334,15 @@ keyman_add_keyboards_from_dir(gpointer data, gpointer user_data) {
}
GList *
-ibus_keyman_list_engines (void)
+ibus_keyman_list_engines()
{
GList *engines = NULL;
GList *kmpdir_list;
gchar *xdgenv;
g_autofree gchar *local_keyboard_path;
+ custom_keyboards = keyman_get_custom_keyboard_dictionary();
+
g_debug("adding from /usr/share/keyman");
kmpdir_list = keyman_get_kmpdirs_fromdir(NULL, "/usr/share/keyman");
g_debug("adding from /usr/local/share/keyman");
@@ -298,7 +364,10 @@ ibus_keyman_list_engines (void)
}
void
-add_engine(gpointer data, gpointer user_data) {
+add_engine(
+ gpointer data,
+ gpointer user_data
+) {
IBusEngineDesc *desc = IBUS_ENGINE_DESC(data);
IBusComponent *component = IBUS_COMPONENT(user_data);
ibus_component_add_engine(component, g_object_ref(desc));
@@ -324,7 +393,6 @@ ibus_keyman_get_component (void)
return component;
}
-
// Obtain Keyboard Options list from DConf
// DConf options are in a list of strings like ['option_key1=value1', 'option_key2=value2']
//
@@ -460,6 +528,119 @@ keyman_put_options_todconf(gchar *package_id,
// kvp got assigned to options[x] and so gets freed when options are freed
}
+static GPtrArray *
+_ptr_array_new_from_array(
+ gpointer *data,
+ gsize len,
+ gboolean null_terminated) {
+ GPtrArray *array;
+
+ g_assert(data != NULL || len == 0);
+ g_assert(len <= G_MAXUINT);
+
+ array = g_ptr_array_new_full(len, NULL);
+
+ for (gsize i = 0; i < len; i++)
+ array->pdata[i] = g_strdup(data[i]);
+
+ if (null_terminated && array->pdata != NULL)
+ array->pdata[len++] = NULL;
+
+ array->len = len;
+
+ return array;
+}
+
+// `g_ptr_array_new_from_null_terminated_array` is only available in GLib 2.76, but we're still
+// stuck to 2.64 (Ubuntu 20.04 Focal) and 2.72 (Ubuntu 22.04 Jammy). Therefore we
+// copy the implementation here (slightly simplified). Once we're past 2.76 we can use the GLib method
+// directly.
+GPtrArray *
+_g_ptr_array_new_from_null_terminated_array(
+ gpointer *data,
+ GCopyFunc copy_func,
+ gpointer copy_func_user_data,
+ GDestroyNotify element_free_func) {
+ gsize len = 0;
+
+ if (data != NULL) {
+ for (gsize i = 0; data[i] != NULL; ++i)
+ len += 1;
+ }
+
+ g_assert(data != NULL || len == 0);
+ g_return_val_if_fail(len <= G_MAXUINT, NULL);
+
+ return _ptr_array_new_from_array(data, len, TRUE);
+}
+
+GPtrArray *
+_keyman_cleanup_custom_keyboards(gchar **keyboards) {
+ GPtrArray *ptr_array = _g_ptr_array_new_from_null_terminated_array((gpointer*)keyboards, NULL, NULL, NULL);
+
+ for (int i = 0; i < ptr_array->len; i++) {
+ if (ptr_array->pdata[i] == NULL) {
+ continue;
+ }
+ g_auto(GStrv) keyboard_tokens = g_strsplit(ptr_array->pdata[i], ":", 2);
+ if (keyboard_tokens == NULL
+ || keyboard_tokens[0] == NULL || strlen(keyboard_tokens[0]) == 0
+ || keyboard_tokens[1] == NULL || strlen(keyboard_tokens[1]) == 0
+ ) {
+ g_ptr_array_remove_index(ptr_array, i--);
+ }
+ }
+ return ptr_array;
+}
+
+gchar **
+keyman_get_custom_keyboards() {
+ g_autoptr(GSettings) settings = g_settings_new(KEYMAN_DCONF_ENGINE_NAME);
+ g_auto(GStrv) result = g_settings_get_strv(settings, KEYMAN_DCONF_KEYBOARDS_KEY);
+ if (result && result[0] == NULL) {
+ return NULL;
+ }
+ g_autoptr(GPtrArray) cleaned_result = _keyman_cleanup_custom_keyboards(result);
+ return (gchar**)g_ptr_array_steal(cleaned_result, NULL);
+}
+
+void
+keyman_set_custom_keyboards(gchar ** keyboards) {
+ g_autoptr(GSettings) settings = g_settings_new(KEYMAN_DCONF_ENGINE_NAME);
+ g_autoptr(GPtrArray) cleaned_keyboards = _keyman_cleanup_custom_keyboards(keyboards);
+ g_settings_set_strv(settings, KEYMAN_DCONF_KEYBOARDS_KEY, (const gchar *const *)cleaned_keyboards->pdata);
+}
+
+GHashTable *
+keyman_get_custom_keyboard_dictionary() {
+ g_auto(GStrv) custom_keyboards = keyman_get_custom_keyboards();
+ if (!custom_keyboards)
+ return NULL;
+
+ GHashTable *hash_table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_ptr_array_unref);
+
+ for (int i = 0; custom_keyboards[i]; i++) {
+ g_auto(GStrv) keyboard_tokens = g_strsplit(custom_keyboards[i], ":", 2);
+
+ if (keyboard_tokens != NULL && keyboard_tokens[0] != NULL && keyboard_tokens[1] != NULL) {
+ GPtrArray *language_keyboards = g_hash_table_lookup(hash_table, keyboard_tokens[1]);
+ if (!language_keyboards) {
+ language_keyboards = g_ptr_array_new_full(1, free_cust_kbd);
+ g_hash_table_insert(hash_table, g_strdup(keyboard_tokens[1]), language_keyboards);
+ }
+
+ cust_kbd *data = g_new0(cust_kbd, 1);
+ data->kb_id_with_lang = g_strdup(custom_keyboards[i]);
+ data->lang = g_new0(kmp_language, 1);
+ data->lang->id = g_strdup(keyboard_tokens[0]);
+ g_ptr_array_add(language_keyboards, data);
+ } else {
+ g_debug("%s: Invalid keyboard: '%s'", __FUNCTION__, custom_keyboards[i]);
+ }
+ }
+
+ return hash_table;
+}
#ifdef DEBUG
#include