Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(linux): Allow loading of keyboards with arbitrary language 🏘️ #9735

Merged
merged 4 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
240 changes: 211 additions & 29 deletions linux/ibus-keyman/src/keymanutil.c
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,22 @@

#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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we null check kbd_data->lang as well for safety?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We always allocate kbd_data->lang immediately after allocating kbd_data, so I don't think there's a need for that. In fact, checking for kbd_data->lang->name is unnecessary as well since g_free will return if the parameter is NULL.

if (kbd_data->lang->name)
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)
Expand Down Expand Up @@ -151,18 +167,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);
Expand All @@ -172,11 +187,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);
}
}

Expand All @@ -198,13 +213,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);
Expand All @@ -215,40 +229,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;
}

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);
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->id, language->name);
get_engine_for_language(keyboard, kb_data->info, kbd_details, kb_data->kmp_dir, language);
if (engine_desc) {
kb_data->engines_list = g_list_append(kb_data->engines_list, engine_desc);
engines_list = g_list_append(engines_list, engine_desc);
}
}
} else {
g_message("adding engine %s", abs_kmx);
kb_data->engines_list = g_list_append(
kb_data->engines_list,
g_message("adding engine %s", kmx_path);
engines_list = g_list_append(
engines_list,
ibus_keyman_engine_desc_new(
abs_kmx, // kmx full path
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(abs_kmx), // icon full path
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);

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
Expand All @@ -258,6 +322,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;
Expand All @@ -270,13 +335,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");
Expand All @@ -298,7 +365,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));
Expand All @@ -324,7 +394,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']
//
Expand Down Expand Up @@ -460,6 +529,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 <locale.h>
Expand Down
3 changes: 3 additions & 0 deletions linux/ibus-keyman/src/keymanutil.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@
#define KEYMAN_DCONF_OPTIONS_PATH "/desktop/ibus/keyman/options/"
#define KEYMAN_DCONF_OPTIONS_KEY "options"

#define KEYMAN_DCONF_ENGINE_NAME "com.keyman.engine"
#define KEYMAN_DCONF_ENGINE_PATH "/com/keyman/engine/"
#define KEYMAN_DCONF_KEYBOARDS_KEY "additional-keyboards"

G_BEGIN_DECLS

Expand Down
11 changes: 9 additions & 2 deletions linux/ibus-keyman/src/keymanutil_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ typedef struct {
gchar *kmp_dir;
} add_keyboard_data;

typedef struct {
kmp_language *lang;
gchar *kb_id_with_lang;
} cust_kbd;

IBusEngineDesc *ibus_keyman_engine_desc_new(
gchar *file_name,
gchar *name,
Expand All @@ -30,9 +35,11 @@ IBusEngineDesc *get_engine_for_language(
kmp_info *info,
keyboard_details *kbd_details,
gchar *kmp_dir,
gchar *lang_id,
gchar *lang_name);
kmp_language *lang);

gchar** keyman_get_custom_keyboards();
void keyman_set_custom_keyboards(gchar ** keyboards);
GHashTable * keyman_get_custom_keyboard_dictionary();
void keyman_add_keyboard(gpointer data, gpointer user_data);
void keyman_add_keyboards_from_dir(gpointer data, gpointer user_data);

Expand Down
2 changes: 2 additions & 0 deletions linux/ibus-keyman/src/kmpdetails.c
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ void kmp_files_foreach (JsonArray *array,
}
}

// Get kmp_details from `<kbd>.json`
kmp_json_status get_keyboard_details(const char *kmp_dir, const char *id, keyboard_details *keyboard)
{
g_autoptr(JsonParser) parser;
Expand Down Expand Up @@ -267,6 +268,7 @@ kmp_json_status get_keyboard_details(const char *kmp_dir, const char *id, keyboa
return JSON_OK;
}

// Get kmp_details from `kmp.json` file
kmp_json_status get_kmp_details(const char *kmp_dir, kmp_details *details)
{
g_autoptr(JsonParser) parser;
Expand Down
Loading