commit a9a1574344e872cbb0803ad5b7806e940a5ba07f Author: Pierre-Olivier Mercier Date: Sat Apr 11 15:09:47 2026 +0700 Initial commit: Everything Emoji module Enlightenment Everything plugin to search and copy emoji via the ':' trigger. Parses Unicode emoji-test.txt at startup, with fuzzy matching and the built-in clipboard action. Co-Authored-By: Claude Opus 4.6 (1M context) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fe0e975 --- /dev/null +++ b/LICENSE @@ -0,0 +1,33 @@ + +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 of the Software and its Copyright notices. In addition publicly +documented acknowledgment must be given that this software has been used if no +source code of this software is made available publicly. Making the source +available publicly means including the source for this software with the +distribution, or a method to get this software via some reasonable mechanism +(electronic transfer via a network or media) as well as making an offer to +supply the source on request. This Copyright notice serves as an offer to +supply the source on on request as well. Instead of this, supplying +acknowledgments of use of this software in either Copyright notices, Manuals, +Publicity and Marketing documents or any documentation provided with any +product containing this software. This License does not apply to any software +that links to the libraries provided by this software (statically or +dynamically), but only to the software provided. + +Please see the COPYING-PLAIN for a plain-english explanation of this notice +and its intent. + +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 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. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..157df11 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# Everything Emoji + +An [Enlightenment](https://www.enlightenment.org/) Everything module to search and copy emoji to the clipboard. + +Type `:` followed by an emoji name (e.g. `:smile`, `:fire`, `:thumbs up`) to fuzzy-search through all Unicode emoji. Select one to copy it to your clipboard. + +## Dependencies + +- Enlightenment (>= 0.27) +- [Everything](https://github.com/OpenMandrivaAssociation/everything) module +- Unicode emoji data (`emoji-test.txt`), typically provided by a package such as `unicode-data` or `unicode-emoji` + +## Build & Install + +```sh +meson setup build +ninja -C build +sudo ninja -C build install +``` + +Then enable **Everything Emoji** in Enlightenment's module settings. + +## Configuration + +- **Trigger**: `:` (configurable in Everything settings) +- **Minimum query length**: 2 characters + +## License + +See the [COPYING](COPYING) file. diff --git a/e-module.edc b/e-module.edc new file mode 100644 index 0000000..ea28e86 --- /dev/null +++ b/e-module.edc @@ -0,0 +1,28 @@ +collections +{ + group + { + name: "icon"; + max: 128 128; + parts + { + part + { + name: "image"; + mouse_events: 0; + type: TEXT; + description + { + state: "default" 0.0; + color: 255 255 255 255; + text + { + text: "😀"; + font: "Sans"; + size: 64; + } + } + } + } + } +} diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..a60d2d7 --- /dev/null +++ b/meson.build @@ -0,0 +1,57 @@ +project('evry-emoji', 'c', + version: '0.1.0', + default_options: ['c_std=gnu11', 'warning_level=2'], +) + +cc = meson.get_compiler('c') + +dep_e = dependency('enlightenment') +dep_evry = dependency('everything') + +emoji_test_paths = [ + '/usr/share/unicode/emoji/emoji-test.txt', + '/usr/share/unicode-data/emoji-test.txt', +] + +emoji_test_file = '' +foreach p : emoji_test_paths + if import('fs').exists(p) + emoji_test_file = p + break + endif +endforeach + +if emoji_test_file == '' + error('Could not find emoji-test.txt. Searched: ' + ', '.join(emoji_test_paths)) +endif + +message('Found emoji data: ' + emoji_test_file) + +module_arch = dep_e.get_variable(pkgconfig: 'module_arch') +module_dir = dep_e.get_variable(pkgconfig: 'modules') + +install_dir = join_paths(module_dir, 'evry-emoji', module_arch) +data_dir = join_paths(module_dir, 'evry-emoji') + +edje_cc = find_program('edje_cc') + +edje_file = custom_target('e-module.edj', + input: 'e-module.edc', + output: 'e-module.edj', + command: [edje_cc, '-v', '@INPUT@', '@OUTPUT@'], + install: true, + install_dir: data_dir, +) + +shared_module('module', + sources: ['src/e_mod_main.c'], + dependencies: [dep_e, dep_evry], + c_args: ['-DEMOJI_TEST_FILE="' + emoji_test_file + '"'], + name_prefix: '', + install: true, + install_dir: install_dir, +) + +install_data('module.desktop', + install_dir: data_dir, +) diff --git a/module.desktop b/module.desktop new file mode 100644 index 0000000..a10c97b --- /dev/null +++ b/module.desktop @@ -0,0 +1,6 @@ +[Desktop Entry] +Type=Link +Name=Everything Emoji +Icon=e-module +Comment=Search and copy emoji to clipboard. Trigger ':' followed by emoji name. +X-Enlightenment-ModuleType=launcher diff --git a/src/e_mod_main.c b/src/e_mod_main.c new file mode 100644 index 0000000..05aa268 --- /dev/null +++ b/src/e_mod_main.c @@ -0,0 +1,322 @@ +#include "e.h" +#include "e_mod_main.h" +#include "evry_api.h" + +/* #undef DBG + * #define DBG(...) ERR(__VA_ARGS__) */ + +typedef struct _Plugin Plugin; +typedef struct _Emoji Emoji; + +struct _Plugin +{ + Evry_Plugin base; +}; + +struct _Emoji +{ + char *emoji; /* the emoji character(s) (UTF-8) */ + char *name; /* descriptive name */ +}; + +static const Evry_API *evry = NULL; +static Evry_Module *evry_module = NULL; +static Evry_Plugin *_plug = NULL; + +static Eina_List *emoji_list = NULL; /* list of Emoji* */ + +/***************************************************************************/ +/* emoji-test.txt parser */ +/***************************************************************************/ + +static void +_emoji_free(Emoji *em) +{ + if (!em) return; + free(em->emoji); + free(em->name); + free(em); +} + +/* Convert space-separated hex codepoints (e.g. "1F600") to a UTF-8 string */ +static char * +_codepoints_to_utf8(const char *hex_str) +{ + char buf[64]; + int pos = 0; + const char *p = hex_str; + + while (*p && pos < (int)sizeof(buf) - 5) + { + unsigned long cp; + char *end; + + while (*p == ' ') p++; + if (!*p) break; + + cp = strtoul(p, &end, 16); + if (end == p) break; + p = end; + + /* encode codepoint as UTF-8 */ + if (cp <= 0x7F) + { + buf[pos++] = (char)cp; + } + else if (cp <= 0x7FF) + { + buf[pos++] = (char)(0xC0 | (cp >> 6)); + buf[pos++] = (char)(0x80 | (cp & 0x3F)); + } + else if (cp <= 0xFFFF) + { + buf[pos++] = (char)(0xE0 | (cp >> 12)); + buf[pos++] = (char)(0x80 | ((cp >> 6) & 0x3F)); + buf[pos++] = (char)(0x80 | (cp & 0x3F)); + } + else if (cp <= 0x10FFFF) + { + buf[pos++] = (char)(0xF0 | (cp >> 18)); + buf[pos++] = (char)(0x80 | ((cp >> 12) & 0x3F)); + buf[pos++] = (char)(0x80 | ((cp >> 6) & 0x3F)); + buf[pos++] = (char)(0x80 | (cp & 0x3F)); + } + } + + buf[pos] = '\0'; + return strdup(buf); +} + +static void +_emoji_list_load(void) +{ + FILE *f; + char line[1024]; + const char *path = EMOJI_TEST_FILE; + + if (emoji_list) return; + + f = fopen(path, "r"); + if (!f) + { + ERR("evry-emoji: cannot open %s", path); + return; + } + + while (fgets(line, sizeof(line), f)) + { + char *semi, *hash, *name_start, *emoji_str; + Emoji *em; + + /* skip comments and blank lines */ + if (line[0] == '#' || line[0] == '\n' || line[0] == '\r') + continue; + + /* format: "1F600 ; fully-qualified # 😀 E1.0 grinning face" */ + semi = strchr(line, ';'); + if (!semi) continue; + + /* only keep fully-qualified emoji */ + if (!strstr(semi, "fully-qualified")) + continue; + + hash = strchr(semi, '#'); + if (!hash) continue; + + /* skip "# E " to get the name */ + name_start = hash + 2; /* skip "# " */ + /* skip the emoji character(s) and space */ + while (*name_start && *name_start != ' ') name_start++; + if (*name_start == ' ') name_start++; + /* skip E and space */ + if (*name_start == 'E') + { + while (*name_start && *name_start != ' ') name_start++; + if (*name_start == ' ') name_start++; + } + + /* trim trailing newline */ + { + char *nl = strchr(name_start, '\n'); + if (nl) *nl = '\0'; + nl = strchr(name_start, '\r'); + if (nl) *nl = '\0'; + } + + if (!*name_start) continue; + + /* extract codepoints (before the semicolon) */ + *semi = '\0'; + /* trim trailing spaces from codepoint string */ + { + char *end = semi - 1; + while (end > line && *end == ' ') *end-- = '\0'; + } + + emoji_str = _codepoints_to_utf8(line); + if (!emoji_str || !*emoji_str) + { + free(emoji_str); + continue; + } + + em = calloc(1, sizeof(Emoji)); + em->emoji = emoji_str; + em->name = strdup(name_start); + + emoji_list = eina_list_append(emoji_list, em); + } + + fclose(f); + INF("evry-emoji: loaded %d emoji", eina_list_count(emoji_list)); +} + +static void +_emoji_list_free(void) +{ + Emoji *em; + EINA_LIST_FREE(emoji_list, em) + _emoji_free(em); +} + +/***************************************************************************/ +/* plugin callbacks */ +/***************************************************************************/ + +static Evry_Plugin * +_begin(Evry_Plugin *plugin, const Evry_Item *it EINA_UNUSED) +{ + Plugin *p; + EVRY_PLUGIN_INSTANCE(p, plugin); + return EVRY_PLUGIN(p); +} + +static void +_finish(Evry_Plugin *plugin) +{ + GET_PLUGIN(p, plugin); + EVRY_PLUGIN_ITEMS_FREE(p); + E_FREE(p); +} + +static Evas_Object * +_icon_get(Evry_Item *it, Evas *e) +{ + Evas_Object *o; + + if (!it || !it->label) return NULL; + + o = evas_object_text_add(e); + evas_object_text_font_set(o, "Sans", 24); + evas_object_text_text_set(o, it->data ? (const char *)it->data : ""); + evas_object_show(o); + + return o; +} + +static int +_fetch(Evry_Plugin *plugin, const char *input) +{ + GET_PLUGIN(p, plugin); + Eina_List *l; + Emoji *em; + + EVRY_PLUGIN_ITEMS_FREE(p); + + if (!input || !*input) return 0; + + EINA_LIST_FOREACH(emoji_list, l, em) + { + int score; + + score = evry->fuzzy_match(em->name, input); + if (!score) continue; + + Evry_Item *it = EVRY_ITEM_NEW(Evry_Item, p, em->emoji, _icon_get, NULL); + if (!it) continue; + + it->fuzzy_match = score; + EVRY_ITEM_DETAIL_SET(it, em->name); + EVRY_PLUGIN_ITEM_APPEND(p, it); + + if (eina_list_count(EVRY_PLUGIN(p)->items) >= 100) break; + } + + return EVRY_PLUGIN_HAS_ITEMS(p); +} + +/***************************************************************************/ +/* plugin init/shutdown */ +/***************************************************************************/ + +static int +_plugins_init(const Evry_API *_api) +{ + evry = _api; + + if (!evry->api_version_check(EVRY_API_VERSION)) + return EINA_FALSE; + + _emoji_list_load(); + + _plug = EVRY_PLUGIN_BASE(N_("Emoji"), "face-smile", + EVRY_TYPE_TEXT, + _begin, _finish, _fetch); + + _plug->history = EINA_FALSE; + + if (evry->plugin_register(_plug, EVRY_PLUGIN_SUBJECT, 5)) + { + Plugin_Config *pc = _plug->config; + pc->view_mode = VIEW_MODE_LIST; + pc->trigger = eina_stringshare_add(":"); + pc->trigger_only = EINA_TRUE; + pc->min_query = 2; + pc->aggregate = EINA_FALSE; + pc->top_level = EINA_TRUE; + } + + return EINA_TRUE; +} + +static void +_plugins_shutdown(void) +{ + if (_plug) + EVRY_PLUGIN_FREE(_plug); + _plug = NULL; +} + +/***************************************************************************/ +/* module entry points */ +/***************************************************************************/ + +EAPI E_Module_Api e_modapi = + { + E_MODULE_API_VERSION, + "evry-emoji" + }; + +EAPI void * +e_modapi_init(E_Module *m) +{ + EVRY_MODULE_NEW(evry_module, evry, _plugins_init, _plugins_shutdown); + + return m; +} + +EAPI int +e_modapi_shutdown(E_Module *m EINA_UNUSED) +{ + EVRY_MODULE_FREE(evry_module); + + _emoji_list_free(); + + return 1; +} + +EAPI int +e_modapi_save(E_Module *m EINA_UNUSED) +{ + return 1; +} diff --git a/src/e_mod_main.h b/src/e_mod_main.h new file mode 100644 index 0000000..9ba49fb --- /dev/null +++ b/src/e_mod_main.h @@ -0,0 +1,20 @@ +#ifndef E_MOD_MAIN_H +#define E_MOD_MAIN_H + +#ifdef ENABLE_NLS +# include +# define _(string) dgettext(PACKAGE, string) +#else +# define bindtextdomain(domain,dir) +# define bind_textdomain_codeset(domain,codeset) +# define _(string) (string) +#endif +#define N_(str) (str) + +EAPI extern E_Module_Api e_modapi; + +EAPI void *e_modapi_init (E_Module *m); +EAPI int e_modapi_shutdown (E_Module *m); +EAPI int e_modapi_save (E_Module *m); + +#endif