Try   HackMD

How to port an IME to IBus

in Vala!

Reference impls:

Additional testing environments:

  • ibus 1.5.27 (Fedora 37) with Xfce4 and GNOME wayland

Screenshots of McBopomofo on Mac

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Subprojects

  • https://github.com/qbane/gnome-disable-ibus-manager: For raw IBus development experience

    TODO:
    js/ui/status/keyboard.js: the controller indicating current input source is called InputSourceSettings.

    when setting global engine by clicking on menu, activateInputSource calls this._ibusManager.setEngine(engine); and then _setEngine sets global engine (but not per-window engine?), but that is not effective, needs further diagnosis:

    set_global_engine_async will be called twice, one in _sourcesChanged is correct, and the other _setPerWindowInputSource is reverting it back.

    JS ERROR: Gio.DBusError: GDBus.Error:org.freedesktop.DBus.Error.Failed: Set global engine failed: 操作已被取消#012_promisify/proto[asyncFunc]/</<@resource:///org/gnome/dules/core/overrides/Gio.js:425:45
    ### Promise created here: ###
    setEngine@resource:///org/gnome/shell/misc/ibusManager.js:293:30
    activateInputSource@resource:///org/gnome/shell/ui/status/keyboard.js:480:31
    _emit@resource:///org/gnome/gjs/modules/core/_signals.js:114:47
    activate@resource:///org/gnome/shell/ui/status/keyboard.js:58:14
    _inputSourcesChanged@resource:///org/gnome/shell/ui/status/keyboard.js:613:33
    _ibusReadyCallback@resource:///org/gnome/shell/ui/status/keyboard.js:371:14
    _emit@resource:///org/gnome/gjs/modules/core/_signals.js:114:47
    _updateReadiness@/home/qbane/.local/share/gnome-shell/extensions/disable-ibus-manager@qbane.github.io/extension.js:45:14
    _initEngines@resource:///org/gnome/shell/misc/ibusManager.js:178:18
    async*_onConnected@resource:///org/gnome/shell/misc/ibusManager.js:166:14
    

    GNOME seems to ignore global/per-window settings in ibus-setup, too

  • https://github.com/qbane/mcbopomofo-core: Maintained as a submodule.
  • https://glib-doc.netlify.app/: GLib docs from Fedora 37.

Vala docs

gir:

  • Replace +<constant name="[\w_]+"\s+value="\d+"\s+c:type="IBUS_[\w_]+"\s*>\s+<source-position\s+filename="ibuskeysyms-compat.h"\s+line="\d+"/>\s+<type\s+name="gint"\s+c:type="gint"/>\s+</constant>\n to empty
  • Replace +<constant name="KEY_[\w_]+"\s+value="\d+"\s+c:type="IBUS_KEY_[\w_]+">\s+<source-position filename="ibuskeysyms.h" line="\d+"/>\s+<type name="gint" c:type="gint"/>\s+</constant>\n
  • Some entries with comments are removed manually

vapi:

  • Replace \t+\[CCode .+, cname = "IBUS_[\w_]+"\)]\n\s+public const int [@\w]+;\n to empty

see https://gist.github.com/andy0130tw/dc28fbeca31cde988475c60682aa55c2

Caveats

Installation without root permission

See: https://github.com/ibus/ibus/blob/1.5.27/src/ibusregistry.c#L271

Lifecycle of ibus engines

Every implementation has different handling on various lifecycle signals, in the following order. The document tells nothing about what the engine is expected to do.

  • construct (the GObject-style construction)/destroy
  • focus_in/focus_out
  • enable/disable
  • reset

e.g. ibus-skk implements all, but ibus-rime implements only focus_in and disable. Hmm. Should be interesting to organize them in a table. ibus-chewing. ibus-libzhuyin.

The trace on my machine (excerpted):

14:56:40.333: engine.vala:37: construct!
14:56:40.571: engine.vala:462: focus_in!
14:56:40.574: engine.vala:476: enable!
14:56:40.585: engine.vala:545: set_capabilities 41!
14:56:40.590: engine.vala:556: set_content_type 10 0!
14:56:57.259: engine.vala:466: focus_out!
14:56:57.310: engine.vala:462: focus_in!
14:56:57.379: engine.vala:466: focus_out!
14:56:57.389: engine.vala:480: disable!
14:56:57.410: engine.vala:90: destruct!

14:57:08.350: engine.vala:37: construct!
14:57:08.375: engine.vala:462: focus_in!
14:57:08.378: engine.vala:476: enable!
14:57:08.379: engine.vala:545: set_capabilities 41!
14:57:08.383: engine.vala:556: set_content_type 10 0!
14:57:08.386: engine.vala:466: focus_out!
14:57:08.391: engine.vala:462: focus_in!

what is ibus-portal?

d-feet:

Need a patch to test ibus: https://bugzilla.redhat.com/attachment.cgi?id=910263&action=diff

Implementations

https://github.com/ibus/ibus-tmpl (enchant)
https://github.com/libzhuyin
https://github.com/rime/ibus-rime

ibus-skk
https://github.com/ueno/ibus-skk (also in Vala!)
Backed by: https://github.com/ueno/libskk

IBusChewing
https://github.com/definite/ibus-chewing
Backed by: https://github.com/chewing/libchewing
GOB:
https://github.com/definite/ibus-chewing/blob/master/src/IBusChewingEngine.gob

  • no cursor in lookup table, but reacts to cursor click
  • used gconf and then migrated to dconf

Handmade:
https://github.com/fourdollars/ibus-zhuyin

Upstream issues

Lack of input mode memoization

Currently only the name of the engine is remembered (Q: is it done by panel or im module?), but nothing else. Engines are destroyed (either by panel or global engine) and recreated after a focus out, if not set to global. Input context IDs, however, remain persistent.

One may request current_input_context when focus-in, but sorry this may cause a deadlock :(

On the other hand, fcitx has "per-program" state in addition to per-window state. This is called propertyPropagatePolicy (All, Program, No), controlled with key shareInputState in settings.

Q: There is a similar intention in ibus: https://github.com/ibus/ibus/commit/92771d0e2cc7e5c12dc4312eff2a106b860deb77. Only GTK program names are exposed, not Qt4/5/XIM. Is this going to help? (v1.5.27+)

The only occurrences I could find (via SourceGraph):

They also use at-spi or xprop to get current window. Also async. Aren't they racy?

Panel UI

In ui/gtk3/candidatepanel.vala, move is not working consistently when invoked synchronously (documentation says "most window managers ignore requests for initial window positions").

The solution is to use Idle.add/add_once:

    private void move(int x, int y) {
        Idle.add(() => {
            m_toplevel.move(x, y);
            return false;
        });
    }

Seems that in GNOME wayland, the preedit text always shows at (0, 0), making it nearly unusable. Need further diagnosis.

Pre-edit text

(spec?) GNOME sets EmbedPreeditText property on ibus to true and does not sync it from dconf. We have to subscribe to it in order to test the integration (hinted from: https://hev.cc/2788.html)

In Vala it looks like:

bus.get_connection().signal_subscribe(
    /* sender */ null,
    /* ifname */ "org.freedesktop.DBus.Properties",
    /* member */ "PropertiesChanged",
    /* objpath */ "/org/freedesktop/IBus",
    /* arg0 */ null,
    DBusSignalFlags.NONE,
    (_conn, _sender, _objpath, _ifname, _signame, args) => {
      VariantIter dict;
      // [String, dict of {string, variant}, String[]]
      args.get("(sa{sv}as)", null, out dict, null);

      string key;
      Variant val;
      while (dict.next("{sv}", out key, out val)) {
        if (key == "EmbedPreeditText") {
          debug("%s is set to [%s]", key, val.get_boolean().to_string());
        }
      }
    });

Moreover, GNOME do not draw any text attrs either when embed-preedit-text is off
👉 in this case, inserting our own caret character is required.

TODOs

  • configurable lookup table selection/key
  • actual cursor & preselect in lookup table
  • double check caret (gnome/ibus-native) & (embed/no-embed)
  • persist and shared LM between sessions
  • restore passthrough/width status in the same application (but is this possible?)
  • configurations through dconf
  • property panel
  • integrate opencc (chttrans in fcitx)
  • fork libmcbpmf and make it redistributable
  • config.h on Meson
  • xdg directory data storage get_user_data_dir
  • Vala bindings / redesign APIs
  • traditional bpmf mode
  • keyboard layout within Bpmf mode (see https://github.com/rime/ibus-rime/issues/15), layout of passthrough mode uses system default, but this disrupts some bpmf mode (e.g., 大千, the "standard" layout) that uses a Latin-agnostic layout
  • setup UI
  • i18n (https://www.rocksaying.tw/archives/15171511.html)

Extension points

  • user phrases
  • excluded phrases
  • associated
  • replacement
  • external converters

Preference toggles

Draft

How can I expand macros? gcc -E ../src/api/McBopomofoApi.h $(pkg-config --cflags glib-2.0) > out.h but the output is not understood by scanner.
then
g-ir-scanner ../src/api/*.h --namespace mcbpmf_api --library src/libMcBopomofoLib.a --pkg glib-2.0 --warn-all > mcbpmf_api.gir

Caddyfile for custom Valadoc

:7777 {

root * ./valadoc.org

route {
  # Add trailing slash for directory requests
  @canonicalPath {
    file {path}/index.php
    not path */
  }
  redir @canonicalPath {http.request.orig_uri.path}/ 308

  # If the requested file does not exist, try index files
  @indexFiles file {
    try_files {path} {path}/index.php index.php
    split_path .php
  }
  rewrite @indexFiles {file_match.relative}

  # Proxy PHP files to the FastCGI responder
  @phpFiles path *.php
  reverse_proxy @phpFiles localhost:9000 {
    transport fastcgi {
      split .php
    }
  }
}

file_server browse

}

occational ibus_serializable_deserialize_object: assertion 'g_type_is_a (type, IBUS_TYPE_SERIALIZABLE)' failed?

https://github.com/ibus/ibus/wiki/Wayland-Colors#current-engines-preedit-color

stateful with ibusbus.current_input_context?
then ibusinputcontext.get_input_context listening for its destroy signal?

ibus-chewing's logic of layout conversion ("keycode-to-keysym"):

  • if passthrough -> no conversion
  • if ibus's pref is use system layout -> no conversion
  • ibus_keymap_lookup_keysym with US layout, use its result iff != voidSymbol
  • no conversion otherwise

I think it is quite decent. But mcbpmf seems to differentiate the layout further. For example, it makes no sense to use 大千 layout in Colemak.
See their wiki for detailed workaround on Mac.

Design choice: Core / impl??

I choose to follow suit of libchewing: Keep the key handler in the core, and handle the candidate popup in the implementation.

Doc of libchewing: https://qbane.keybase.pub/libchewing-doc.html. I cannot find it online, so I build and publish my copy.