IBus protocol
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'),
...