diff --git a/page/Fcitx5.d.ts b/page/Fcitx5.d.ts index 79863d1..fe5d91d 100644 --- a/page/Fcitx5.d.ts +++ b/page/Fcitx5.d.ts @@ -8,6 +8,8 @@ export interface FCITX { getAllInputMethods: () => { name: string, displayName: string, languageCode: string }[] setStatusAreaCallback: (callback: () => void) => void updateStatusArea: () => void + getConfig: (uri: string) => string + setConfig: (uri: string, json: object) => void } export const fcitxReady: Promise diff --git a/page/config.ts b/page/config.ts new file mode 100644 index 0000000..0c4332e --- /dev/null +++ b/page/config.ts @@ -0,0 +1,9 @@ +import Module from './module' + +export function getConfig(uri: string) { + return Module.ccall('get_config', 'string', ['string'], [uri]) +} + +export function setConfig(uri: string, json: object) { + return Module.ccall('set_config', 'void', ['string', 'string'], [uri, JSON.stringify(json)]) +} diff --git a/page/index.ts b/page/index.ts index 8abadaa..fa15d80 100644 --- a/page/index.ts +++ b/page/index.ts @@ -3,6 +3,7 @@ import { blur, clickPanel, focus } from './focus' import { keyEvent } from './keycode' import { commit, hidePanel, placePanel, setPreedit } from './client' import { currentInputMethod, getAllInputMethods, getInputMethods, setCurrentInputMethod, setInputMethods } from './input-method' +import { getConfig, setConfig } from './config' let res: (value: any) => void @@ -40,6 +41,8 @@ window.fcitx = { getInputMethods, setInputMethods, getAllInputMethods, + getConfig, + setConfig, enable() { document.addEventListener('focus', focus, true) document.addEventListener('blur', blur, true) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6568ed1..a745851 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -2,6 +2,7 @@ add_executable(Fcitx5 fcitx.cpp keycode.cpp input_method.cpp + config.cpp ) target_include_directories(Fcitx5 PRIVATE diff --git a/src/config.cpp b/src/config.cpp new file mode 100644 index 0000000..2ebd0d3 --- /dev/null +++ b/src/config.cpp @@ -0,0 +1,267 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fcitx { +extern std::unique_ptr instance; + +constexpr char globalConfigPath[] = "fcitx://config/global"; +constexpr char addonConfigPrefix[] = "fcitx://config/addon/"; +constexpr char imConfigPrefix[] = "fcitx://config/inputmethod/"; + +/// Convert configuration into a json object. +nlohmann::json configToJson(const Configuration &config); + +nlohmann::json configValueToJson(const Configuration &config); + +using namespace std::literals::string_literals; + +static nlohmann::json &jsonLocate(nlohmann::json &j, const std::string &group, + const std::string &option); +static nlohmann::json configValueToJson(const RawConfig &config); +static nlohmann::json configSpecToJson(const RawConfig &config); +static nlohmann::json configSpecToJson(const Configuration &config); +static void mergeSpecAndValue(nlohmann::json &specJson, + const nlohmann::json &valueJson); +static RawConfig jsonToRawConfig(const nlohmann::json &); +static std::tuple +parseAddonUri(const std::string &uri); + +nlohmann::json getConfig(const std::string &uri) { + FCITX_DEBUG() << "getConfig " << uri; + if (uri == globalConfigPath) { + auto &config = instance->globalConfig().config(); + return configToJson(config); + } else if (stringutils::startsWith(uri, addonConfigPrefix)) { + auto [addonName, subPath] = parseAddonUri(uri); + auto *addonInfo = instance->addonManager().addonInfo(addonName); + if (!addonInfo) { + return {{"ERROR", "Addon \""s + addonName + "\" does not exist"}}; + } else if (!addonInfo->isConfigurable()) { + return { + {"ERROR", "Addon \""s + addonName + "\" is not configurable"}}; + } + auto *addon = instance->addonManager().addon(addonName, true); + if (!addon) { + return {{"ERROR", + "Failed to get config for addon \""s + addonName + "\""}}; + } + auto *config = + subPath.empty() ? addon->getConfig() : addon->getSubConfig(subPath); + if (!config) { + return {{"ERROR", + "Failed to get config for addon \""s + addonName + "\""}}; + } + return configToJson(*config); + } else if (stringutils::startsWith(uri, imConfigPrefix)) { + auto imName = uri.substr(sizeof(imConfigPrefix) - 1); + auto *entry = instance->inputMethodManager().entry(imName); + if (!entry) { + return { + {"ERROR", "Input method \""s + imName + "\" doesn't exist"}}; + } + if (!entry->isConfigurable()) { + return {{"ERROR", + "Input method \""s + imName + "\" is not configurable"}}; + } + auto *engine = instance->inputMethodEngine(imName); + if (!engine) { + return {{"ERROR", "Failed to get engine for input method \""s + + imName + "\""}}; + } + auto *config = engine->getConfigForInputMethod(*entry); + if (!config) { + return {{"ERROR", "Failed to get config for input method \""s + + imName + "\""}}; + } + return configToJson(*config); + } else { + return {{"ERROR", "Bad config URI \""s + uri + "\""}}; + } +} + +extern "C" { +EMSCRIPTEN_KEEPALIVE const char *get_config(const char *uri) { + static std::string ret; + ret = getConfig(std::string(uri)).dump(); + return ret.c_str(); +} + +EMSCRIPTEN_KEEPALIVE bool set_config(const char *uri_, const char *json_) { + FCITX_DEBUG() << "setConfig " << uri_; + auto config = jsonToRawConfig(nlohmann::json::parse(json_)); + auto uri = std::string(uri_); + if (uri == globalConfigPath) { + auto &gc = instance->globalConfig(); + gc.load(config, true); + if (gc.safeSave()) { + instance->reloadConfig(); + return true; + } else { + return false; + } + } else if (stringutils::startsWith(uri, addonConfigPrefix)) { + auto [addonName, subPath] = parseAddonUri(uri); + auto *addon = instance->addonManager().addon(addonName, true); + if (addon) { + FCITX_DEBUG() << "Saving addon config to: " << uri; + if (subPath.empty()) { + addon->setConfig(config); + } else { + addon->setSubConfig(subPath, config); + } + return true; + } else { + FCITX_ERROR() << "Failed to get addon"; + return false; + } + } else if (stringutils::startsWith(uri, imConfigPrefix)) { + auto im = uri.substr(sizeof(imConfigPrefix) - 1); + const auto *entry = instance->inputMethodManager().entry(im); + auto *engine = instance->inputMethodEngine(im); + if (entry && engine) { + FCITX_DEBUG() << "Saving input method config to: " << uri; + engine->setConfigForInputMethod(*entry, config); + return true; + } else { + FCITX_ERROR() << "Failed to get input method"; + return false; + } + } else { + return false; + } +} +} + +void jsonFillRawConfigValues(const nlohmann::json &j, RawConfig &config) { + if (j.is_string()) { + config = j.get(); + return; + } + if (j.is_object()) { + for (const auto [key, subJson] : j.items()) { + auto subConfig = config.get(key, true); + jsonFillRawConfigValues(subJson, *subConfig); + } + return; + } + FCITX_FATAL() << "Unknown value json: " << j.dump(); +} + +RawConfig jsonToRawConfig(const nlohmann::json &j) { + RawConfig config; + jsonFillRawConfigValues(j, config); + return config; +} + +nlohmann::json &jsonLocate(nlohmann::json &j, const std::string &groupPath, + const std::string &option) { + auto paths = stringutils::split(groupPath, "$"); + paths.pop_back(); // remove type + paths.push_back(option); + nlohmann::json *cur = &j; + for (const auto &part : paths) { + auto &children = + *cur->emplace("Children", nlohmann::json::array()).first; + bool exist = false; + for (auto &child : children) { + if (child["Option"] == part) { + exist = true; + cur = &child; + break; + } + } + if (!exist) { + cur = &children.emplace_back(nlohmann::json::object()); + } + } + return *cur; +} + +nlohmann::json configValueToJson(const RawConfig &config) { + if (!config.hasSubItems()) { + return nlohmann::json(config.value()); + } + nlohmann::json j; + for (auto &subItem : config.subItems()) { + auto subConfig = config.get(subItem); + j[subItem] = configValueToJson(*subConfig); + } + return j; +} + +nlohmann::json configValueToJson(const Configuration &config) { + RawConfig raw; + config.save(raw); + return configValueToJson(raw); +} + +nlohmann::json configSpecToJson(const RawConfig &config) { + // first level -> Path1$Path2$...$Path_n$ConfigType + // second level -> OptionName + nlohmann::json spec; + auto groups = config.subItems(); + for (const auto &group : groups) { + auto groupConfig = config.get(group); + auto options = groupConfig->subItems(); + for (const auto &option : options) { + auto optionConfig = groupConfig->get(option); + nlohmann::json &optSpec = jsonLocate(spec, group, option); + optSpec["Option"] = option; + optionConfig->visitSubItems( + [&](const RawConfig &config, const std::string &path) { + optSpec[path] = configValueToJson(config); + return true; + }); + } + } + return spec; +} + +void mergeSpecAndValue(nlohmann::json &specJson, + const nlohmann::json &valueJson) { + if (specJson.find("Type") != specJson.end()) { + specJson["Value"] = valueJson; + } + for (auto &child : specJson["Children"]) { + const auto iter = valueJson.find(child["Option"]); + if (iter != valueJson.end()) { + mergeSpecAndValue(child, *iter); + } + } +} + +nlohmann::json configSpecToJson(const Configuration &config) { + RawConfig rawDesc; + config.dumpDescription(rawDesc); + return configSpecToJson(rawDesc); +} + +nlohmann::json configToJson(const Configuration &config) { + // specJson contains config definitions + auto specJson = configSpecToJson(config); + // valueJson contains actual values that user could change + auto valueJson = configValueToJson(config); + mergeSpecAndValue(specJson, valueJson); + return specJson; +} + +static std::tuple +parseAddonUri(const std::string &uri) { + auto addon = uri.substr(sizeof(addonConfigPrefix) - 1); + auto pos = addon.find('/'); + if (pos == std::string::npos) { + return {addon, ""}; + } else { + return {addon.substr(0, pos), addon.substr(pos + 1)}; + } +} +} // namespace fcitx