diff --git a/README.md b/README.md index becf83e..b29dc07 100644 --- a/README.md +++ b/README.md @@ -7,16 +7,15 @@ Rime input method integration of coc.nvim ## Dependencies - [rime](https://rime.im/) -- [json-c](https://github.com/json-c/json-c) - [pkg-config](http://pkg-config.freedesktop.org/) ```sh # Ubuntu -sudo apt-get -y install pkg-config librime-dev libjson-c-dev librime1 libjson-c5 +sudo apt-get -y install pkg-config librime-dev librime1 # ArchLinux -sudo pacman -S pkg-config librime json-c +sudo pacman -S pkg-config librime # Android Termux -apt-get -y install pkg-config librime json-c +apt-get -y install pkg-config librime # NixOS # don't need to install dependencies due to nix flake ``` diff --git a/binding.c b/binding.c new file mode 100644 index 0000000..17126d2 --- /dev/null +++ b/binding.c @@ -0,0 +1,249 @@ +#include +#include +#include + +#define DEFAULT_BUFFER_SIZE 1024 +#ifndef __STRING +#define __STRING(x) #x +#endif /* ifndef */ +#define STRING(x) __STRING(x) +#define NODE_API_CALL(env, call) \ + do { \ + napi_status status = (call); \ + if (status != napi_ok) { \ + const napi_extended_error_info *error_info = NULL; \ + napi_get_last_error_info((env), &error_info); \ + const char *err_message = error_info->error_message; \ + bool is_pending; \ + napi_is_exception_pending((env), &is_pending); \ + /* If an exception is already pending, don't rethrow it */ \ + if (!is_pending) { \ + const char *message = \ + (err_message == NULL) ? "empty error message" : err_message; \ + napi_throw_error((env), NULL, message); \ + } \ + return NULL; \ + } \ + } while (0) +#define SET_ARGV(env, argv) \ + do { \ + size_t argc = sizeof(argv) / sizeof((argv)[0]); \ + size_t original_argc = argc; \ + NODE_API_CALL(env, \ + napi_get_cb_info((env), info, &argc, (argv), NULL, NULL)); \ + if (argc < original_argc) { \ + char str[DEFAULT_BUFFER_SIZE]; \ + sprintf(str, "need %zd arguments, only get %zd arguments", \ + original_argc, argc); \ + napi_throw_error((env), NULL, str); \ + return NULL; \ + } \ + } while (0) +#define SET_SESSION_ID(env, value, session_id) \ + do { \ + bool lossless; \ + NODE_API_CALL((env), napi_get_value_bigint_uint64( \ + (env), (value), &(session_id), &lossless)); \ + } while (0); +#define SET_STRING(env, value, str) \ + do { \ + size_t size; \ + NODE_API_CALL((env), \ + napi_get_value_string_utf8((env), (value), (str), \ + DEFAULT_BUFFER_SIZE, &size)); \ + if (size == DEFAULT_BUFFER_SIZE) { \ + napi_throw_error((env), NULL, \ + "path length is longer than buffer size " STRING( \ + DEFAULT_BUFFER_SIZE)); \ + return NULL; \ + } \ + } while (0) + +static napi_value init_rime(napi_env env, napi_callback_info info) { + napi_value result, argv[3]; + char dirs[sizeof(argv) / sizeof(argv[0])][DEFAULT_BUFFER_SIZE]; + SET_ARGV(env, argv); + SET_STRING(env, argv[0], dirs[0]); + SET_STRING(env, argv[1], dirs[1]); + SET_STRING(env, argv[2], dirs[2]); + RIME_STRUCT(RimeTraits, rime_traits); + rime_traits.shared_data_dir = dirs[0]; + rime_traits.user_data_dir = dirs[1]; + rime_traits.log_dir = dirs[2]; + rime_traits.distribution_name = "Rime"; + rime_traits.distribution_code_name = PROJECT_NAME; + rime_traits.distribution_version = PROJECT_VERSION; + rime_traits.app_name = "rime." PROJECT_NAME; + RimeSetup(&rime_traits); + RimeInitialize(&rime_traits); + RimeStartMaintenance(false); + RimeSessionId session_id = RimeCreateSession(); + if (session_id == 0) { + napi_throw_error(env, NULL, "rime cannot create session"); + return NULL; + } + NODE_API_CALL(env, napi_create_bigint_uint64(env, session_id, &result)); + return result; +} + +static napi_value deinit_rime(napi_env env, napi_callback_info info) { + napi_value argv[1]; + RimeSessionId session_id; + SET_ARGV(env, argv); + SET_SESSION_ID(env, argv[0], session_id); + if (!RimeDestroySession(session_id)) { + char str[DEFAULT_BUFFER_SIZE]; + sprintf(str, "cannot destroy session %ld", session_id); + napi_throw_error(env, NULL, str); + } + RimeFinalize(); + return NULL; +} + +static napi_value get_current_schema(napi_env env, napi_callback_info info) { + napi_value schema_id, argv[1]; + RimeSessionId session_id; + char buffer[DEFAULT_BUFFER_SIZE]; + SET_ARGV(env, argv); + SET_SESSION_ID(env, argv[0], session_id); + if (!RimeGetCurrentSchema(session_id, buffer, DEFAULT_BUFFER_SIZE)) { + napi_throw_error(env, NULL, "cannot get current schema"); + return NULL; + } + NODE_API_CALL( + env, napi_create_string_utf8(env, buffer, NAPI_AUTO_LENGTH, &schema_id)); + return schema_id; +} + +static napi_value get_schema_list(napi_env env, napi_callback_info info) { + napi_value result, argv[1]; + RimeSessionId session_id; + RimeSchemaList schema_list; + SET_ARGV(env, argv); + SET_SESSION_ID(env, argv[0], session_id); + if (!RimeGetSchemaList(&schema_list)) { + napi_throw_error(env, NULL, "cannot get schema list"); + return NULL; + } + NODE_API_CALL(env, + napi_create_array_with_length(env, schema_list.size, &result)); + for (size_t i = 0; i < schema_list.size; i++) { + napi_value element, schema_id, name; + RimeSchemaListItem schema_list_item = schema_list.list[i]; + NODE_API_CALL(env, napi_create_object(env, &element)); + NODE_API_CALL(env, napi_create_string_utf8(env, schema_list_item.schema_id, + NAPI_AUTO_LENGTH, &schema_id)); + NODE_API_CALL( + env, napi_set_named_property(env, element, "schema_id", schema_id)); + NODE_API_CALL(env, napi_create_string_utf8(env, schema_list_item.name, + NAPI_AUTO_LENGTH, &name)); + NODE_API_CALL(env, napi_set_named_property(env, element, "name", name)); + NODE_API_CALL(env, napi_set_element(env, result, i, element)); + } + return result; +} + +static napi_value select_schema(napi_env env, napi_callback_info info) { + napi_value argv[2]; + RimeSessionId session_id; + char schema_id[DEFAULT_BUFFER_SIZE]; + SET_ARGV(env, argv); + SET_SESSION_ID(env, argv[0], session_id); + SET_STRING(env, argv[1], schema_id); + if (!RimeSelectSchema(session_id, schema_id)) { + char str[DEFAULT_BUFFER_SIZE]; + sprintf(str, "cannot select schema %s", schema_id); + napi_throw_error(env, NULL, str); + } + return NULL; +} + +static napi_value get_context(napi_env env, napi_callback_info info) { + int modifiers; + uint32_t length; + napi_value result, argv[3]; + RimeSessionId session_id; + SET_ARGV(env, argv); + SET_SESSION_ID(env, argv[0], session_id); + NODE_API_CALL(env, napi_get_array_length(env, argv[1], &length)); + NODE_API_CALL(env, napi_get_value_int32(env, argv[2], &modifiers)); + for (uint32_t i = 0; i < length; i++) { + int keycode; + napi_value value; + NODE_API_CALL(env, napi_get_element(env, argv[1], i, &value)); + NODE_API_CALL(env, napi_get_value_int32(env, value, &keycode)); + if (!RimeProcessKey(session_id, keycode, modifiers)) { + char str[DEFAULT_BUFFER_SIZE]; + sprintf(str, "cannot process key %d", keycode); + napi_throw_error(env, NULL, str); + } + } + RIME_STRUCT(RimeContext, context); + if (!RimeGetContext(session_id, &context)) { + napi_throw_error(env, NULL, "cannot get context"); + return NULL; + } + NODE_API_CALL(env, napi_create_object(env, &result)); + if (context.composition.preedit) { + napi_value preedit; + NODE_API_CALL(env, napi_create_string_utf8(env, context.composition.preedit, + NAPI_AUTO_LENGTH, &preedit)); + NODE_API_CALL(env, + napi_set_named_property(env, result, "preEdit", preedit)); + } + if (context.menu.candidates) { + RimeCandidate *p_candidates[DEFAULT_BUFFER_SIZE]; + size_t num_candidates = 0; + while (!context.menu.is_last_page) { + for (int i = 0; i < context.menu.num_candidates; i++) + p_candidates[num_candidates + i] = &context.menu.candidates[i]; + num_candidates += context.menu.num_candidates; + // Turn to next page ... + if (!RimeProcessKey(session_id, (int)'=', 0)) + napi_throw_error(env, NULL, "cannot process key ="); + // and grab content. + if (!RimeGetContext(session_id, &context)) + napi_throw_error(env, NULL, "cannot get context"); + } + napi_value candidates; + NODE_API_CALL( + env, napi_create_array_with_length(env, num_candidates, &candidates)); + NODE_API_CALL( + env, napi_set_named_property(env, result, "candidates", candidates)); + for (size_t i = 0; i < num_candidates; i++) { + napi_value element, text, comment; + NODE_API_CALL(env, napi_create_object(env, &element)); + NODE_API_CALL(env, napi_create_string_utf8(env, p_candidates[i]->text, + NAPI_AUTO_LENGTH, &text)); + NODE_API_CALL(env, napi_set_named_property(env, element, "text", text)); + if (p_candidates[i]->comment) { + NODE_API_CALL(env, + napi_create_string_utf8(env, p_candidates[i]->comment, + NAPI_AUTO_LENGTH, &comment)); + NODE_API_CALL( + env, napi_set_named_property(env, element, "comment", comment)); + } + NODE_API_CALL(env, napi_set_element(env, result, i, element)); + } + } + RimeFreeContext(&context); + return result; +} + +NAPI_MODULE_INIT(/* napi_env env, napi_value exports */) { + char *names[] = {"init_rime", "deinit_rime", "get_current_schema", + "get_schema_list", "select_schema", "get_context"}; + napi_callback callbacks[] = {init_rime, deinit_rime, + get_current_schema, get_schema_list, + select_schema, get_context}; + napi_value functions[sizeof(names) / sizeof(names[0])]; + for (size_t i = 0; i < sizeof(names) / sizeof(names[0]); i++) { + NODE_API_CALL(env, napi_create_function(env, names[i], NAPI_AUTO_LENGTH, + callbacks[i], NULL, &functions[i])); + + NODE_API_CALL( + env, napi_set_named_property(env, exports, names[i], functions[i])); + } + + return exports; +} diff --git a/binding.gyp b/binding.gyp index fb7cd8e..1e8dc60 100644 --- a/binding.gyp +++ b/binding.gyp @@ -4,20 +4,19 @@ "variables": {"android_ndk_path": ""}, "targets": [ { - "target_name": "rime_cli", - "type": "executable", + "target_name": "rime", "sources": [ - "rime-cli.c", + "binding.c", ], "defines": [ "PROJECT_NAME=\"