6 minute read

IBus를 사용하여 텍스트를 입력하는 과정을 정리했다. DBus에 기반했다고 해서 d-feet나 dbus-monitor 등 dbus 도구를 사용하여 프로토콜을 분색할 수 있을 것으로 기대했는데 private session으로 통신하여 도움이 되지 않았다.

System and Tools Version

$ ibus version
IBus 1.5.22

$ ibus engine
hangul

$ ibus list-engine
언어: 한국어
  hangul - Hangul
언어: Dutch
  xkb:be::nld - Belgian
...

IBus 주소 알아내기

IBus에 private 통신을 하기 위해서 IBUS 주소를 알아야 한다. 홈 디렉토리의 특정 파일을 읽어야 하는데 DBus의 session bus 등을 사용하지 않고 파일로 하는지 이해할 수 없다. 이래서야 표준 protocol이라고 할 수 있나?

아무튼…

import os
import configparser

# 1. libdbus
# - get_local_machine_id()
# 2. python-dbus
# >> bus = dbus.SessionBus()
# >> ibus = bus.get_object("org.freedesktop.IBus", "/org/freedesktop/IBus")
# >> ibus.GetMachineId(dbus_interface='org.freedesktop.DBus.Peer')
# dbus.String('6209f12f15ce4b16bcd09203f6e97ecf')
# 3. read machine-id file
def get_machine_id():
    try:
        with open("/etc/machine-id") as f:
            return f.read().strip()
    except:
        pass
    try:
        with open("/var/lib/dbus/machine-id") as f:
            return f.read().strip()
    except:
        pass
    return ""


def IBus_AddressFilename():
    if 'IBUS_ADDRESS' in os.environ:
        return os.environ["IBUS_ADDRESS"]

    if 'DISPLAY' in os.environ:
        display = os.environ["DISPLAY"]
    else:
        display = ":0.0"
    host, disp_num  = display.split(':')
    if not host:
        host = 'unix'
    disp_num  = disp_num.split('.')[0]

    if 'XDG_CONFIG_HOME' in os.environ:
        config_dir = os.environ["XDG_CONFIG_HOME"]
    else:
        if not 'HOME' in os.environ:
            return None
        config_dir = os.environ['HOME'] + '/.config'

    key = get_machine_id()
    return format("%s/ibus/bus/%s-%s-%s" % (config_dir, key, host, disp_num))


def IBus_Address():
    config_path = IBus_AddressFilename()
    with open(config_path, 'r') as f:
        config_string = '[dummy]\n' + f.read()
    config = configparser.ConfigParser()
    config.read_string(config_string)
    return config.get('dummy', 'IBUS_ADDRESS')

참고로 현재의 파일 이름과 내용은 다음과 같다.

$ cat ~/.config/ibus/bus/6209f12f15ce4b16bcd09203f6e97ecf-unix-0 
# This file is created by ibus-daemon, please do not modify it.
# This file allows processes on the machine to find the
# ibus session bus with the below address.
# If the IBUS_ADDRESS environment variable is set, it will
# be used rather than this file.
IBUS_ADDRESS=unix:abstract=/home/yjseo/.cache/ibus/dbus-neVlfK8L,guid=b7b321c06750fb493ec9e8695f35e401
IBUS_DAEMON_PID=1560

IBus 통신 연결 및 Introspecttion

위에서 알아낸 IBUS_ADDRESS를 사용하여 다음과 같이 IBus 통신을 연결한다.

import dbus
from ibus_config import IBus_Address

bus = dbus.connection.Connection(IBus_Address())
ibus = bus.get_object('org.freedesktop.IBus', '/org/freedesktop/IBus')

DBus의 인트로스펙션 API를 사용하여 어떤 method가 있는지 또 그 method의 입력 argument와 return 값 등을 알 수 있다.

>>> print(ibus.Introspect(dbus_interface='org.freedesktop.DBus.Introspectable'))
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
                      "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<!-- GDBus 2.64.3 -->
<node>
  <interface name="org.freedesktop.DBus.Properties">
    <method name="Get">
      <arg type="s" name="interface_name" direction="in"/>
      <arg type="s" name="property_name" direction="in"/>
      <arg type="v" name="value" direction="out"/>
    </method>
    <method name="GetAll">
      <arg type="s" name="interface_name" direction="in"/>
      <arg type="a{sv}" name="properties" direction="out"/>
    </method>
    <method name="Set">
      <arg type="s" name="interface_name" direction="in"/>
      <arg type="s" name="property_name" direction="in"/>
      <arg type="v" name="value" direction="in"/>
    </method>
    <signal name="PropertiesChanged">
      <arg type="s" name="interface_name"/>
      <arg type="a{sv}" name="changed_properties"/>
      <arg type="as" name="invalidated_properties"/>
    </signal>
  </interface>
  <interface name="org.freedesktop.DBus.Introspectable">
    <method name="Introspect">
      <arg type="s" name="xml_data" direction="out"/>
    </method>
  </interface>
  <interface name="org.freedesktop.DBus.Peer">
    <method name="Ping"/>
    <method name="GetMachineId">
      <arg type="s" name="machine_uuid" direction="out"/>
    </method>
  </interface>
  <interface name="org.freedesktop.IBus.Service">
    <method name="Destroy">
    </method>
  </interface>
  <interface name="org.freedesktop.IBus">
    <method name="CreateInputContext">
      <arg type="s" name="client_name" direction="in">
      </arg>
      <arg type="o" name="object_path" direction="out">
      </arg>
    </method>
    <method name="RegisterComponent">
      <arg type="v" name="component" direction="in">
      </arg>
    </method>
    <method name="GetEnginesByNames">
      <arg type="as" name="names" direction="in">
      </arg>
      <arg type="av" name="engines" direction="out">
      </arg>
    </method>
    <method name="Exit">
      <arg type="b" name="restart" direction="in">
      </arg>
    </method>
    <method name="Ping">
      <arg type="v" name="data" direction="in">
      </arg>
      <arg type="v" name="data" direction="out">
      </arg>
    </method>
    <method name="SetGlobalEngine">
      <arg type="s" name="engine_name" direction="in">
      </arg>
    </method>
    <method name="GetAddress">
      <annotation name="org.freedesktop.DBus.Deprecated" value="true">
      </annotation>
      <arg type="s" name="address" direction="out">
      </arg>
    </method>
    <method name="CurrentInputContext">
      <annotation name="org.freedesktop.DBus.Deprecated" value="true">
      </annotation>
      <arg type="o" name="object_path" direction="out">
      </arg>
    </method>
    <method name="ListEngines">
      <annotation name="org.freedesktop.DBus.Deprecated" value="true">
      </annotation>
      <arg type="av" name="engines" direction="out">
      </arg>
    </method>
    <method name="ListActiveEngines">
      <annotation name="org.freedesktop.DBus.Deprecated" value="true">
      </annotation>
      <arg type="av" name="engines" direction="out">
      </arg>
    </method>
    <method name="GetUseSysLayout">
      <annotation name="org.freedesktop.DBus.Deprecated" value="true">
      </annotation>
      <arg type="b" name="enabled" direction="out">
      </arg>
    </method>
    <method name="GetUseGlobalEngine">
      <arg type="b" name="enabled" direction="out">
      </arg>
    </method>
    <method name="IsGlobalEngineEnabled">
      <annotation name="org.freedesktop.DBus.Deprecated" value="true">
      </annotation>
      <arg type="b" name="enabled" direction="out">
      </arg>
    </method>
    <method name="GetGlobalEngine">
      <annotation name="org.freedesktop.DBus.Deprecated" value="true">
      </annotation>
      <arg type="v" name="desc" direction="out">
      </arg>
    </method>
    <signal name="RegistryChanged">
    </signal>
    <signal name="GlobalEngineChanged">
      <arg type="s" name="engine_name">
      </arg>
    </signal>
    <property type="s" name="Address" access="read">
    </property>
    <property type="o" name="CurrentInputContext" access="read">
    </property>
    <property type="av" name="Engines" access="read">
    </property>
    <property type="v" name="GlobalEngine" access="read">
    </property>
    <property type="as" name="PreloadEngines" access="write">
      <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true">
      </annotation>
    </property>
    <property type="b" name="EmbedPreeditText" access="readwrite">
      <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true">
      </annotation>
    </property>
    <property type="av" name="ActiveEngines" access="read">
    </property>
  </interface>
  <node name="InputContext_8"/>
  <node name="InputContext_14"/>
  <node name="InputContext_6"/>
  <node name="InputContext_1"/>
  <node name="InputContext_9"/>
  <node name="InputContext_7"/>
  <node name="InputContext_5"/>
</node>

다음과 같이 짧게 볼 수도 있다.

>>> pprint.pprint(ibus._introspect_method_map)
{'org.freedesktop.DBus.Introspectable.Introspect': '',
 'org.freedesktop.DBus.Peer.GetMachineId': '',
 'org.freedesktop.DBus.Peer.Ping': '',
 'org.freedesktop.DBus.Properties.Get': 'ss',
 'org.freedesktop.DBus.Properties.GetAll': 's',
 'org.freedesktop.DBus.Properties.Set': 'ssv',
 'org.freedesktop.IBus.CreateInputContext': 's',
 'org.freedesktop.IBus.CurrentInputContext': '',
 'org.freedesktop.IBus.Exit': 'b',
 'org.freedesktop.IBus.GetAddress': '',
 'org.freedesktop.IBus.GetEnginesByNames': 'as',
 'org.freedesktop.IBus.GetGlobalEngine': '',
 'org.freedesktop.IBus.GetUseGlobalEngine': '',
 'org.freedesktop.IBus.GetUseSysLayout': '',
 'org.freedesktop.IBus.IsGlobalEngineEnabled': '',
 'org.freedesktop.IBus.ListActiveEngines': '',
 'org.freedesktop.IBus.ListEngines': '',
 'org.freedesktop.IBus.Ping': 'v',
 'org.freedesktop.IBus.RegisterComponent': 'v',
 'org.freedesktop.IBus.Service.Destroy': '',
 'org.freedesktop.IBus.SetGlobalEngine': 's'}

InputContext 얻기

ibus 객체를 introspection 해서 CurrentInputContext method가 있다는 것을 알았다. 이제 현재 입력 context를 알아내서 introspect 해 보자.

>>> context_path = ibus.CurrentInputContext(dbus_interface='org.freedesktop.IBus')
>>> context_path
/org/freedesktop/IBus/InputContext_8
>>> ctx = bus.get_object("org.freedesktop.IBus", context_path)
>>> ctx.Introspect(dbus_interface='org.freedesktop.DBus.Introspectable')
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
                      "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<!-- GDBus 2.64.3 -->
<node>
  <interface name="org.freedesktop.DBus.Properties">
    <method name="Get">
      <arg type="s" name="interface_name" direction="in"/>
      <arg type="s" name="property_name" direction="in"/>
      <arg type="v" name="value" direction="out"/>
    </method>
    <method name="GetAll">
      <arg type="s" name="interface_name" direction="in"/>
      <arg type="a{sv}" name="properties" direction="out"/>
    </method>
    <method name="Set">
      <arg type="s" name="interface_name" direction="in"/>
      <arg type="s" name="property_name" direction="in"/>
      <arg type="v" name="value" direction="in"/>
    </method>
    <signal name="PropertiesChanged">
      <arg type="s" name="interface_name"/>
      <arg type="a{sv}" name="changed_properties"/>
      <arg type="as" name="invalidated_properties"/>
    </signal>
  </interface>
  <interface name="org.freedesktop.DBus.Introspectable">
    <method name="Introspect">
      <arg type="s" name="xml_data" direction="out"/>
    </method>
  </interface>
  <interface name="org.freedesktop.DBus.Peer">
    <method name="Ping"/>
    <method name="GetMachineId">
      <arg type="s" name="machine_uuid" direction="out"/>
    </method>
  </interface>
  <interface name="org.freedesktop.IBus.Service">
    <method name="Destroy">
    </method>
  </interface>
  <interface name="org.freedesktop.IBus.InputContext">
    <method name="ProcessKeyEvent">
      <arg type="u" name="keyval" direction="in">
      </arg>
      <arg type="u" name="keycode" direction="in">
      </arg>
      <arg type="u" name="state" direction="in">
      </arg>
      <arg type="b" name="handled" direction="out">
      </arg>
    </method>
    <method name="SetCursorLocation">
      <arg type="i" name="x" direction="in">
      </arg>
      <arg type="i" name="y" direction="in">
      </arg>
      <arg type="i" name="w" direction="in">
      </arg>
      <arg type="i" name="h" direction="in">
      </arg>
    </method>
    <method name="SetCursorLocationRelative">
      <arg type="i" name="x" direction="in">
      </arg>
      <arg type="i" name="y" direction="in">
      </arg>
      <arg type="i" name="w" direction="in">
      </arg>
      <arg type="i" name="h" direction="in">
      </arg>
    </method>
    <method name="ProcessHandWritingEvent">
      <arg type="ad" name="coordinates" direction="in">
      </arg>
    </method>
    <method name="CancelHandWriting">
      <arg type="u" name="n_strokes" direction="in">
      </arg>
    </method>
    <method name="FocusIn">
    </method>
    <method name="FocusOut">
    </method>
    <method name="Reset">
    </method>
    <method name="SetCapabilities">
      <arg type="u" name="caps" direction="in">
      </arg>
    </method>
    <method name="PropertyActivate">
      <arg type="s" name="name" direction="in">
      </arg>
      <arg type="u" name="state" direction="in">
      </arg>
    </method>
    <method name="SetEngine">
      <arg type="s" name="name" direction="in">
      </arg>
    </method>
    <method name="GetEngine">
      <arg type="v" name="desc" direction="out">
      </arg>
    </method>
    <method name="SetSurroundingText">
      <arg type="v" name="text" direction="in">
      </arg>
      <arg type="u" name="cursor_pos" direction="in">
      </arg>
      <arg type="u" name="anchor_pos" direction="in">
      </arg>
    </method>
    <signal name="CommitText">
      <arg type="v" name="text">
      </arg>
    </signal>
    <signal name="ForwardKeyEvent">
      <arg type="u" name="keyval">
      </arg>
      <arg type="u" name="keycode">
      </arg>
      <arg type="u" name="state">
      </arg>
    </signal>
    <signal name="UpdatePreeditText">
      <arg type="v" name="text">
      </arg>
      <arg type="u" name="cursor_pos">
      </arg>
      <arg type="b" name="visible">
      </arg>
    </signal>
    <signal name="UpdatePreeditTextWithMode">
      <arg type="v" name="text">
      </arg>
      <arg type="u" name="cursor_pos">
      </arg>
      <arg type="b" name="visible">
      </arg>
      <arg type="u" name="mode">
      </arg>
    </signal>
    <signal name="ShowPreeditText">
    </signal>
    <signal name="HidePreeditText">
    </signal>
    <signal name="UpdateAuxiliaryText">
      <arg type="v" name="text">
      </arg>
      <arg type="b" name="visible">
      </arg>
    </signal>
    <signal name="ShowAuxiliaryText">
    </signal>
    <signal name="HideAuxiliaryText">
    </signal>
    <signal name="UpdateLookupTable">
      <arg type="v" name="table">
      </arg>
      <arg type="b" name="visible">
      </arg>
    </signal>
    <signal name="ShowLookupTable">
    </signal>
    <signal name="HideLookupTable">
    </signal>
    <signal name="PageUpLookupTable">
    </signal>
    <signal name="PageDownLookupTable">
    </signal>
    <signal name="CursorUpLookupTable">
    </signal>
    <signal name="CursorDownLookupTable">
    </signal>
    <signal name="RegisterProperties">
      <arg type="v" name="props">
      </arg>
    </signal>
    <signal name="UpdateProperty">
      <arg type="v" name="prop">
      </arg>
    </signal>
    <property type="(uu)" name="ContentType" access="write">
    </property>
    <property type="(b)" name="ClientCommitPreedit" access="write">
    </property>
  </interface>
</node>

Text Input

이제 InputContext 의 Capabilities 를 설정한후 signal receiver 들을 설정하면 IBus를 통한 입력을 테스트할 수 있다.

iface.SetCapabilities(255) # 9

iface.connect_to_signal("CommitText", __commit_text_cb)

iface.connect_to_signal("UpdatePreeditText", __update_preedit_text_cb)
iface.connect_to_signal("ShowPreeditText", __show_preedit_text_cb)
iface.connect_to_signal("HidePreeditText", __hide_preedit_text_cb)

iface.connect_to_signal("UpdateAuxiliaryText", __update_aux_text_cb)
iface.connect_to_signal("ShowAuxiliaryText", __show_aux_text_cb)
iface.connect_to_signal("HideAuxiliaryText", __hide_aux_text_cb)

iface.connect_to_signal("UpdateLookupTable", __update_lookup_table_cb)
iface.connect_to_signal("ShowLookupTable", __show_lookup_table_cb)
iface.connect_to_signal("HideLookupTable", __hide_lookup_table_cb)

콜백 함수에 입력되는 값들을 print 한 결과는 다음과 같다.

$ python testime.py 
mouse press
focus in event
__update_preedit_text_cb dbus.Struct((dbus.String('IBusText'), dbus.Dictionary({}, ...
__update_preedit_text_cb dbus.Struct((dbus.String('IBusText'), dbus.Dictionary({}, ...
key press
__update_preedit_text_cb dbus.Struct((dbus.String('IBusText'), dbus.Dictionary({}, ...
__update_aux_text_cb
__update_lookup_table_cb

Pros and Cons

  • DBus 설비를 사용하여 Introspect 등 편리한 기능을 사용할 수 있다.
  • method와 signal 등 규격을 정했으면 좋았을텐데 DBus 기반 입력기의 하나의 구현 정도
  • ProcessKeyEvent 에서 commitText와 preeditText 등을 return 했으면 앱에서 구현이 편할텐데 이런 것들을 signal로 비동기적으 받게 되어 있어서 preedit text와 space 순서가 바뀌는 등 고질적인 문제가…

Appendix

  • ibus에서 Engine List 알기
# iface = dbus.Interface(ibus, dbus_interface='org.freedesktop.IBus')
# engines = iface.ListEngines()
engines = ibus.ListEngines(dbus_interface='org.freedesktop.IBus')
>>> engines[0]
dbus.Struct((dbus.String('IBusEngineDesc'), dbus.Dictionary({}, signature=dbus.Signature('sv')), dbus.String('xkb:my::msa'), dbus.String('Malay (Jawi)'), dbus.String('Malay (Jawi)'), dbus.String('ms'), dbus.String('GPL'), dbus.String('Peng Huang <shawn.p.huang@gmail.com>'), dbus.String('ibus-keyboard'), dbus.String('my'), dbus.UInt32(1), dbus.String(''), dbus.String(''), dbus.String(''), dbus.String(''), dbus.String(''), dbus.String(''), dbus.String(''), dbus.String('')), signature=None, variant_level=1)
  • 기타 ibus method 와 property
>>> ibus.GetAddress(dbus_interface='org.freedesktop.IBus')
dbus.String('unix:abstract=/home/yjseo/.cache/ibus/dbus-neVlfK8L,guid=b7b321c06750fb493ec9e8695f35e401')
>>> ibus.GetUseSysLayout(dbus_interface='org.freedesktop.IBus')
dbus.Boolean(True)
>>> ibus.IsGlobalEngineEnabled(dbus_interface='org.freedesktop.IBus')
dbus.Boolean(True)
>>  p = ibus.GetAll('org.freedesktop.IBus', dbus_interface='org.freedesktop.DBus.Properties')
>>> p['Address']
dbus.String('unix:abstract=/home/yjseo/.cache/ibus/dbus-neVlfK8L,guid=b7b321c06750fb493ec9e8695f35e401', variant_level=1)
>>> p['CurrentInputContext']
dbus.ObjectPath('/org/freedesktop/IBus/InputContext_20', variant_level=1)
  • input context
>>> ctx_path = ibus.CreateInputContext("Test", dbus_interface='org.freedesktop.IBus')
>>> ctx = bus.get_object('org.freedesktop.IBus', ctx_path)
>>> iface = dbus.Interface(ctx, dbus_interface='org.freedesktop.IBus.InputContext')
>>> print(iface.GetEngine)

dbus.Struct((dbus.String('IBusEngineDesc'), dbus.Dictionary({},
signature=dbus.Signature('sv')), dbus.String('dummy'), dbus.String(''),
dbus.String(''), dbus.String(''), dbus.String(''), dbus.String(''),
dbus.String('ibus-engine'), dbus.String(''), dbus.UInt32(0), dbus.String(''),
...

>>> iface.SetCapabilities(9)
>>> iface.FocusIn()
>>> print(iface.GetEngine)

dbus.Struct((dbus.String('IBusEngineDesc'),
dbus.Dictionary({}, signature=dbus.Signature('sv')),
dbus.String('hangul'), dbus.String('Hangul'),
dbus.String('Korean Input Method'),
dbus.String('ko'), dbus.String('GPL'),
dbus.String('Peng Huang <shawn.p.huang@gmail.com>'),
dbus.String('ibus-hangul'),
dbus.String('kr'), dbus.UInt32(99), dbus.String(''),
dbus.String('한'), dbus.String(''), dbus.String('kr104'),
...