/* vim:set et sts=4: */ /* * Keyman Input Method for IBUS (The Input Bus) * * Copyright (C) 2018 SIL International * * keymanutil is dual licensed under the MIT or GPL licenses as described below. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * MIT license * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * OR * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 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 2 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 library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA * */ #include #include #include #include #include #include #include #include "bcp47util.h" #include "kmpdetails.h" #include "keyman-version.h" #include "keymanutil.h" #include "keymanutil_internal.h" #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) { DIR *dir = opendir(path); if (dir != NULL) { struct dirent *file = readdir(dir); while (file != NULL) { struct stat filestat; g_autofree gchar *absfn = g_strdup_printf("%s/%s", path, file->d_name); stat(absfn, &filestat); if (S_ISDIR(filestat.st_mode)) { if(g_strcmp0(file->d_name, ".") != 0 && g_strcmp0(file->d_name, "..") != 0) kmpdir_list = keyman_get_kmpdirs_fromdir(kmpdir_list, absfn); } // Looking for kmp.json else if (S_ISREG(filestat.st_mode) && g_strcmp0(file->d_name, "kmp.json") == 0) { g_message("adding kmp path %s", path); kmpdir_list=g_list_append(kmpdir_list, g_strdup(path)); } file = readdir(dir); } closedir(dir); } return kmpdir_list; } gchar * keyman_get_icon_file(const gchar *kmx_file) { // Now there will only be the .png // which at some point will get extracted from the .kmx during installation gchar *full_path_to_icon_file, *p; g_autofree gchar *filename; p = rindex(kmx_file, '.'); filename = g_strndup(kmx_file, p-kmx_file); full_path_to_icon_file=g_strdup_printf("%s.bmp.png", filename); if (!g_file_test(full_path_to_icon_file, G_FILE_TEST_EXISTS)) { g_free(full_path_to_icon_file); full_path_to_icon_file=g_strdup("/usr/share/keyman/icons/default.png"); } return full_path_to_icon_file; } IBusEngineDesc * ibus_keyman_engine_desc_new( gchar *file_name, gchar *name, gchar *description, gchar *copyright, gchar *lang, gchar *license, gchar *author, gchar *icon, gchar *layout, gchar *version ) { IBusEngineDesc *engine_desc; g_autofree gchar *desc; if (description == NULL) { desc = g_strdup_printf("%s", copyright); } else { desc = g_strdup_printf("%s\n%s", description, copyright); } engine_desc = ibus_engine_desc_new_varargs( "name", file_name, "longname", name, "description", desc, "language", lang ? lang : "other", "license", license ? license : "", "author", author ? author : "", "icon", icon, "layout", layout, "version", version ? version : "", NULL); g_object_ref(engine_desc); return engine_desc; } IBusEngineDesc * get_engine_for_language( kmp_keyboard *keyboard, kmp_info *info, keyboard_details *kbd_details, gchar *kmp_dir, kmp_language *lang) { IBusEngineDesc* engine_desc = NULL; 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); if (result < 0) { g_strlcpy(minimized_tag, lang->id, capacity); } g_autofree gchar *lang_code = g_new0(gchar, capacity); if (!bcp47_get_language_code(minimized_tag, lang_code, capacity)) { g_strlcpy(lang_code, minimized_tag, capacity); } // If ibus doesn't know about the language then append the // language name to the keyboard name 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); } } g_autofree gchar *abs_kmx = g_strjoin("/", kmp_dir, keyboard->kmx_file, NULL); g_autofree gchar *id_with_lang = g_strjoin(":", minimized_tag, abs_kmx, NULL); g_message("adding engine %s", id_with_lang); engine_desc = ibus_keyman_engine_desc_new( id_with_lang, // lang:kmx full path name_with_lang ? name_with_lang : keyboard->name, // longname kbd_details->description, // description info->copyright, // copyright if available lang_code, // language, most are ignored by ibus except major languages kbd_details->license, // license 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); return engine_desc; } int _get_version(const gchar **pver) { g_assert(pver); const gchar *ver = *pver; int version = 0; while (*ver && *ver != '.') { if (*ver >= '0' && *ver <= '9') { version = version * 10 + (*ver - '0'); ver++; } else { // stop comparison on first non-digit while (*ver) ver++; } } *pver = ver; return version; } int keyman_compare_version(const gchar *ver1, const gchar *ver2) { for (; *ver1 || *ver2; ) { int version1 = _get_version(&ver1); int version2 = _get_version(&ver2); if (version1 < version2) return -1; if (version1 > version2) return +1; if (*ver1) ver1++; if (*ver2) ver2++; } return 0; } 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); g_autofree gchar *kmx_file = g_path_get_basename(engine_name); // If we already have an engine for this keyboard (in a different area), we // don't want to add it again since we wouldn't add anything new // if it's the same version if (g_strcmp0(kmx_file, keyboard->kmx_file) == 0 && keyman_compare_version(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 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); 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 void keyman_add_keyboards_from_dir(gpointer data, gpointer user_data) { gchar * kmp_dir = (gchar *) 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; kb_data.info = &details->info; kb_data.kmp_dir = kmp_dir; g_list_foreach(details->keyboards, keyman_add_keyboard, &kb_data); *engines_list = kb_data.engines_list; } } GList * 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"); kmpdir_list = keyman_get_kmpdirs_fromdir(kmpdir_list, "/usr/local/share/keyman"); xdgenv = getenv("XDG_DATA_HOME"); if (xdgenv != NULL){ local_keyboard_path= g_strdup_printf("%s/keyman", xdgenv); } else { xdgenv = getenv("HOME"); local_keyboard_path= g_strdup_printf("%s/.local/share/keyman", xdgenv); } g_debug("adding from %s", local_keyboard_path); kmpdir_list = keyman_get_kmpdirs_fromdir(kmpdir_list, local_keyboard_path); g_list_foreach(kmpdir_list, keyman_add_keyboards_from_dir, &engines); g_list_free_full(kmpdir_list, g_free); return engines; } void 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)); } IBusComponent * ibus_keyman_get_component (void) { IBusComponent *component; component = ibus_component_new ("org.freedesktop.IBus.Keyman", N_("Keyman"), KEYMAN_VERSION, "GPL", "Keyman team ", "https://keyman.com", "", "ibus-keyman"); g_autolist(IBusEngineDesc) engines = ibus_keyman_list_engines(); g_list_foreach(engines, add_engine, component); return component; } /** * Obtain Keyboard Options list from DConf * * DConf options are in a list of strings like ['option_key1=value1', 'option_key2=value2'] * * @param package_id Package ID * @param keyboard_id Keyboard ID * @return A newly allocated gchar**; free with g_strfreev() */ gchar** keyman_get_keyboard_options_fromdconf( const gchar *package_id, const gchar *keyboard_id ) { g_message(__FUNCTION__); // Obtain keyboard options from DConf g_autofree gchar *path = g_strdup_printf("%s%s/%s/", KEYMAN_DCONF_OPTIONS_PATH, package_id, keyboard_id); GSettings *child_settings = g_settings_new_with_path(KEYMAN_DCONF_OPTIONS_CHILD_NAME, path); gchar **options = NULL; if (child_settings != NULL) { options = g_settings_get_strv(child_settings, KEYMAN_DCONF_OPTIONS_KEY); } g_object_unref(G_OBJECT(child_settings)); return options; } /** * Obtain Keyboard Options from DConf and parse into a GQueue of struct km_core_option_item * * @param package_id Package ID * @param keyboard_id Keyboard ID * @return A newly allocated GQueue; free with g_queue_free_full() */ GQueue* keyman_get_keyboard_options_queue_fromdconf( const gchar *package_id, const gchar *keyboard_id ) { g_message(__FUNCTION__); GQueue *queue_options = g_queue_new(); // Obtain keyboard options from DConf g_auto(GStrv) options = keyman_get_keyboard_options_fromdconf(package_id, keyboard_id); // Parse options into queue_options if (options != NULL) { int index = 0; while (options[index] != NULL) { g_auto(GStrv) option_tokens = g_strsplit(options[index], "=", 2); if (option_tokens != NULL && option_tokens[0] != NULL && option_tokens[1] != NULL) { g_message("Keyboard Option [%d], %s=%s", index, option_tokens[0], option_tokens[1]); km_core_option_item *opt = g_new0(km_core_option_item, 1); opt[0].scope = KM_CORE_OPT_KEYBOARD; km_core_cu *ocp = g_utf8_to_utf16(option_tokens[0], -1, NULL, NULL, NULL); opt[0].key = ocp; ocp = g_utf8_to_utf16 (option_tokens[1], -1, NULL, NULL, NULL); opt[0].value = ocp; g_queue_push_tail(queue_options, opt); } index++; } } return queue_options; } /** * Write new keyboard option to DConf * * DConf options are in a list of strings like ['option_key1=value1', 'option_key2=value2'] * If the option key already exists, the value is updated. Otherwise a new string * 'option_key=option_value' is appended. * * @param package_id Package ID * @param keyboard_id Keyboard ID * @param option_key Key for the new option * @param option_value Value of the new option */ void keyman_put_keyboard_options_todconf( const gchar *package_id, const gchar *keyboard_id, const gchar *option_key, const gchar *option_value ) { g_message(__FUNCTION__); if (package_id == NULL || keyboard_id == NULL || option_key == NULL || option_value == NULL) { return; } // Obtain keyboard options from DConf g_auto(GStrv) options = keyman_get_keyboard_options_fromdconf(package_id, keyboard_id); g_autofree gchar *needle = g_strdup_printf("%s=", option_key); gchar *kvp = g_strdup_printf("%s=%s", option_key, option_value); g_assert(options != NULL); int index = 0; gboolean option_updated = FALSE; while (options[index] != NULL) { // If option_key already exists, update value with option_value if (g_strrstr(options[index], needle) != NULL) { g_free(options[index]); options[index] = kvp; option_updated = TRUE; break; } index++; } if (!option_updated) { // Resize to add new option and null-terminate int size = index + 2; // old size: index + 1, plus 1 new options = g_renew(gchar *, options, size); options[index] = kvp; options[index + 1] = NULL; } // Write to DConf g_autofree gchar *path = g_strdup_printf("%s%s/%s/", KEYMAN_DCONF_OPTIONS_PATH, package_id, keyboard_id); g_autoptr(GSettings) child_settings = g_settings_new_with_path(KEYMAN_DCONF_OPTIONS_CHILD_NAME, path); if (child_settings != NULL) { g_message("writing keyboard options to DConf"); g_settings_set_strv(child_settings, KEYMAN_DCONF_OPTIONS_KEY, (const gchar *const *)options); } // kvp got assigned to options[x] and so gets freed when options are freed } /** * Obtain (general) option value from DConf * * @param option_key Key of the option * @return The current value of the option */ gboolean keyman_get_option_fromdconf(const gchar* option_key) { g_autoptr(GSettings) settings = g_settings_new(KEYMAN_DCONF_OPTIONS_NAME); return g_settings_get_boolean(settings, option_key); } /** * Write new (general) option to DConf * * @param option_key Key for the new option * @param option_value Value of the new option * @return TRUE if setting the key succeeded, FALSE if the key was not writable */ gboolean keyman_put_option_todconf(const gchar* option_key, gboolean option_value) { g_autoptr(GSettings) settings = g_settings_new(KEYMAN_DCONF_OPTIONS_NAME); return g_settings_set_boolean(settings, option_key, option_value); } /** * Subscribe to changes in the (general) Keyman options * * @param callback Callback method. Note that this should be different methods if * this method gets called for different keys. * @param user_data Custom data that will passed to callback * @return A transparent settings object */ void* keyman_subscribe_option_changes(void* callback, gpointer user_data) { GSettings *settings; settings = g_settings_new(KEYMAN_DCONF_OPTIONS_NAME); g_signal_connect(settings, "changed", G_CALLBACK(callback), user_data); return settings; } /** * Unsubscribe from changes in the (general) option settings * * @param settings The settings object * @param callback The callback method used when subscribing * @param user_data Custom data for callback * @return > 0 if successfully unsubscribed, otherwise 0 */ guint keyman_unsubscribe_option_changes(void* settings, void* callback, gpointer user_data) { if (!settings) { return 0; } guint retval = g_signal_handlers_disconnect_by_func(settings, callback, user_data); if (retval > 0) { g_object_unref(settings); } return retval; } 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 int main () { IBusComponent *component; GString *output; setlocale (LC_ALL, ""); ibus_init (); component = ibus_keyman_get_component (); output = g_string_new (""); ibus_component_output (component, output, 1); g_debug ("\n%s", output->str); g_string_free (output, TRUE); g_object_unref (component); return 0; } #endif