Morse's Site
1042 字
5 分钟
通过 RIME 学习 C++ 系列:5
2024-06-11

在 RIME 中使用 Yaml 作为配置文件格式,根据 Yaml 语言的特点,在 RIME 中,将配置项的值分为三类:

  1. 基础数据类型:int,double,string,bool。
  2. 列表(List)类型:可以理解为数组。
  3. 映射(Map)类型:可以理解为对象类型,即 key-value 类型。

举个几个例子:

  • 配置项键盘高度,假设它的配置项叫:keyboardHeight,那么这个项对应的值可以用 int 或 double 来表示,如:keyboardHeight: 54

  • 输入方案列表,它的配置项为 schema_list,因为这个配置项的值是个列表,所需要用数组表示,如:

    schema_list:
    - schema: rime_ice
    - schema: flypy
    
  • 配色方案,因为一个配色方案是由多个选项构成,所以这个配置项的值是个对象,即 key-value 类型,如:

solarized_light:
  schemaName: solarized_light
  name: 曬經・日/Solarized Light
  author: 雪齋 <lyc20041@gmail.com>/Morse <morse.hsiao@gmail.com>
  back_color: 0xF0E5F6FB # 键盘背景色
  button_back_color: 0xF0E5F6FB # 按键背景色
  button_pressed_back_color: 0xD7E8ED # 按下时按键背景色

在 RIME 的代码中分别使用:ConfigItemConfigValue, ConfigList, ConfigMap 来表示上述的值模型。

配置项关系

  • ConfigItem 是一个抽象类,用来表示 Yaml 配置文件中的任意配置项的值。
  • ConfigValue 用来表示「基础数据类型」的值。
  • ConfigList 用来表示列表(List)类型的值。
  • ConfigMap 用来表示映射(Map)类型的值。

以上类型的声明代码在 src/rime/config/config_types.h 中。

有了上面的基础,那么就有两个问题:

  1. RIME 中如何读取一个 Yaml 文件。
  2. RIME 中如何编译一个 Yaml 文件。

RIME 中如何读取一个 Yaml 文件?#

我们先下面一段 Example 代码:

void readDefaultConfig() {
  path defaultConfigFilePath;
  defaultConfigFilePath /= "/Users/morse/Downloads/default.yaml";

  the<Config> config(new Config());
  config->loadFromFile(defaultConfigFilePath);

  string config_version;
  if (config->getString("config_version", &config_version)) {
    std::cout << "config version: " << config_version << std::endl;
  } else {
    std::cout << "read config file error" << std::endl;
  }

  int pageSize;
  if (config->getInt("menu/page_size", &pageSize)) {
    std::cout << "menu page_size: " << pageSize << std::endl;
  } else {
    std::cout << "read menu/page_size error" << std::endl;
  }

  if (auto keyBindings =
          as<ConfigList>(config->getItem("key_binder/bindings"))) {
    std::for_each(
        keyBindings->begin(), keyBindings->end(), [](an<ConfigItem> item) {
          if (an<ConfigMap> map = std::dynamic_pointer_cast<ConfigMap>(item)) {
            string accept;
            if (map->getValue("accept")->getString(&accept)) {
              std::cout << "binding accept: " << accept << std::endl;
            }
          }
        });
  }
}

代码中读取固定路径 /Users/morse/Downloads/default.yaml 的 Yaml 文件,在读取后就可以获取每个配置项的值了。

在示例中,分别获取了 string, int, 列表,map 四种类型的值。

从上面代码可以看出,读取 Yaml 文件主要是通过 Config 类来实现的。

Config

还记得系列中之前介绍的「组件的注册与发现」吗,Config 实现了模板类 Class, 所以在 RIME 可以通过注册与发现来获取 Config 实例,进而可以读取 Yaml 文件。

RIME 中如何编译一个 Yaml 文件?#

我们知道 RIME 中对 Yaml 文件格式做了扩展,支持 __include 等指令,方便对 Yaml 进行模块化,补丁化等处理。

那么这些指令是如何实现的呢?其实这里就是对 Yaml 文件一个编译的过程:将这些扩展指令编译为正确的,符合 Yaml 语法规范的内容,然后在 build 目录下生成编译好的 yaml 文件。

我们看个示例, 我们有三个 yaml 文件:hamster_color_schemas.yamlhamster_keyboards.yamlhamster_swipe.yamlhamster.yaml

  • hamster_color_schemas.yaml 定义配色相关内容。
  • hamster_keyboards.yaml 定义键盘布局相关内容。
  • hamster_swipe.yaml 定义按键划动相关内容。
  • hamster.yaml:全部配置内容,并使用 __include 指令将 hamster_color_schemas.yamlhamster_keyboards.yamlhamster_swipe.yaml 中的内容导入进来。

我们看代码如何编译这个过程:

void buildConfig() {
  an<ConfigComponent<ConfigBuilder>> configBuilder(new ConfigComponent<ConfigBuilder>([&](ConfigBuilder* builder) {
    builder->installPlugin(new AutoPatchConfigPlugin);
    builder->installPlugin(new DefaultConfigPlugin);
    builder->installPlugin(new LegacyPresetConfigPlugin);
    builder->installPlugin(new LegacyDictionaryConfigPlugin);
    builder->installPlugin(new BuildInfoPlugin);
    builder->installPlugin(new SaveOutputPlugin);
  }));

  Config* defaultConfig = configBuilder->create("hamster");
  string value = defaultConfig->getValue("keyboard/useKeyboardType")->str();
  printf("value: %s \n", value.c_str());
}

ConfigBuilder

ConfigComponent 是模板类,实现了基类 ConfigComponentBasean<ConfigData> loadConfig(const string& config_id) 虚方法。

同时在方法的实现中会强制调用它的实例化类的 an<ConfigData> loadConfig(ResourceResolver* resource_resolver, const string& config_id) 方法。

而在 ConfigBuilder 中的 loadConfig() 方法会依次调用自身的 plugins_,即上面示例中 new 的各种 plugin。而这些 plugin 的实现中,完成了如 .custom.yaml 文件中 patch 解析,编译生成 yaml 中的 __build_info 内容,保存文件等等。

而向类似 __include 这种指令的解析,是在 loadConfig() 方法中调用了 ConfigCompiler 类的 compile() 方法实现的。

总结#

  • RIME 中有关配置文件的解析,编译等代码逻辑在 src/rime/config 中。
  • RIME 中默认的配置文件注册代码在 src/rime/core_module.cc 中。
通过 RIME 学习 C++ 系列:5
https://fuwari.vercel.app/posts/rime/rime_learn_05/
作者
Morse Hsiao
发布于
2024-06-11
许可协议
CC BY-NC-SA 4.0