RequireSurroundingText asks the client to send its text; it arrives through SetSurroundingText as an IBusText and a cursor counted in runes, and DeleteSurroundingText asks for runes back before the commit that replaces them. The official libibus client now proves the whole turn: 한자 typed, the Hanja key, Enter, and 漢字 arrives with the 한 taken away. A client that has set EffectivePostProcessKeyEvent is never asked for its text. It reads the key's commits back after ProcessKeyEvent returns, and a DeleteSurroundingText signal is not one of the things that reply can carry, so the deletion would land after the text it was meant to make room for and eat the wrong runes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
387 lines
10 KiB
C
387 lines
10 KiB
C
#define _POSIX_C_SOURCE 200809L
|
|
#include <ibus.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
typedef struct Log Log;
|
|
struct Log
|
|
{
|
|
GMainLoop *loop;
|
|
int *waiting;
|
|
IBusInputContext *a;
|
|
IBusInputContext *clearctx;
|
|
int legacy;
|
|
int modern;
|
|
int commit;
|
|
int invalid;
|
|
int sawpreedit;
|
|
int sawcommit;
|
|
int done;
|
|
int transfer;
|
|
int atext;
|
|
int aclear;
|
|
int retake;
|
|
int retook;
|
|
char revent[16];
|
|
int rn;
|
|
int repeat;
|
|
int rfirst;
|
|
int rcommit;
|
|
int rdone;
|
|
int flushed;
|
|
int hanja;
|
|
int required;
|
|
int deleted;
|
|
int delbefore;
|
|
int delcount;
|
|
int converted;
|
|
};
|
|
|
|
static void
|
|
revent(Log *log, char c)
|
|
{
|
|
if(log->rn + 1 < (int)sizeof log->revent){
|
|
log->revent[log->rn++] = c;
|
|
log->revent[log->rn] = '\0';
|
|
}
|
|
}
|
|
|
|
static void
|
|
legacy(IBusInputContext *ctx, IBusText *text, guint cursor,
|
|
gboolean visible, void *arg)
|
|
{
|
|
IBusAttrList *attrs;
|
|
IBusAttribute *a;
|
|
Log *log;
|
|
const char *s;
|
|
|
|
(void)ctx;
|
|
log = arg;
|
|
log->legacy++;
|
|
s = ibus_text_get_text(text);
|
|
if(log->repeat == 3){
|
|
if(s[0] != '\0' || cursor != 0 || visible)
|
|
log->invalid = 1;
|
|
return;
|
|
}
|
|
if(log->repeat != 0){
|
|
revent(log, 'P');
|
|
if(log->repeat == 1){
|
|
if(s[0] == '\0'){
|
|
if(cursor != 0 || visible)
|
|
log->invalid = 1;
|
|
}else if(strcmp(s, "ㅋ") != 0 || cursor != 1 || !visible)
|
|
log->invalid = 1;
|
|
else
|
|
log->rfirst = 1;
|
|
}else if(s[0] == '\0'){
|
|
if(cursor != 0 || visible || log->rcommit)
|
|
log->invalid = 1;
|
|
}else if(strcmp(s, "ㅋ") != 0 || cursor != 1 || !visible ||
|
|
!log->rcommit)
|
|
log->invalid = 1;
|
|
else
|
|
log->rdone = 1;
|
|
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
|
|
g_main_loop_quit(log->loop);
|
|
return;
|
|
}
|
|
if(strcmp(s, "k") == 0){
|
|
attrs = ibus_text_get_attributes(text);
|
|
a = attrs == NULL ? NULL : ibus_attr_list_get(attrs, 0);
|
|
if(cursor != 1 || !visible || attrs == NULL || a == NULL ||
|
|
a->type != IBUS_ATTR_TYPE_UNDERLINE ||
|
|
a->value != IBUS_ATTR_UNDERLINE_SINGLE ||
|
|
a->start_index != 0 || a->end_index != 1 ||
|
|
ibus_attr_list_get(attrs, 1) != NULL)
|
|
log->invalid = 1;
|
|
else{
|
|
log->sawpreedit = 1;
|
|
if(log->transfer && ctx == log->a)
|
|
log->atext = 1;
|
|
}
|
|
}
|
|
if(log->transfer && log->atext && !log->aclear && s[0] == '\0'){
|
|
if(cursor != 0 || visible)
|
|
log->invalid = 1;
|
|
else{
|
|
log->clearctx = ctx;
|
|
if(ctx == log->a)
|
|
log->aclear = 1;
|
|
else
|
|
log->invalid = 1;
|
|
}
|
|
}
|
|
log->done = log->sawpreedit && log->sawcommit;
|
|
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
|
|
g_main_loop_quit(log->loop);
|
|
}
|
|
|
|
static void
|
|
modern(IBusInputContext *ctx, IBusText *text, guint cursor,
|
|
gboolean visible, guint mode, void *arg)
|
|
{
|
|
Log *log;
|
|
|
|
(void)ctx;
|
|
(void)text;
|
|
(void)cursor;
|
|
(void)visible;
|
|
(void)mode;
|
|
log = arg;
|
|
log->modern++;
|
|
log->invalid = 1;
|
|
if(log->loop != NULL)
|
|
g_main_loop_quit(log->loop);
|
|
}
|
|
|
|
static void
|
|
commit(IBusInputContext *ctx, IBusText *text, void *arg)
|
|
{
|
|
IBusAttrList *attrs;
|
|
Log *log;
|
|
const char *s;
|
|
|
|
(void)ctx;
|
|
log = arg;
|
|
log->commit++;
|
|
s = ibus_text_get_text(text);
|
|
attrs = ibus_text_get_attributes(text);
|
|
if(log->hanja){
|
|
/* The word converted, with the client\'s own syllable taken back. */
|
|
if(strcmp(s, "漢字") == 0 && log->deleted && log->delbefore == -1 &&
|
|
log->delcount == 1)
|
|
log->converted = 1;
|
|
else
|
|
log->invalid = 1;
|
|
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
|
|
g_main_loop_quit(log->loop);
|
|
return;
|
|
}
|
|
if(log->repeat == 3){
|
|
/* Focus loss hands the pending syllable back as a commit. */
|
|
if(strcmp(s, "ㅋ") == 0)
|
|
log->flushed = 1;
|
|
else
|
|
log->invalid = 1;
|
|
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
|
|
g_main_loop_quit(log->loop);
|
|
return;
|
|
}
|
|
if(log->repeat != 0){
|
|
revent(log, 'K');
|
|
if(log->repeat != 2 || strcmp(s, "ㅋ") != 0 || attrs == NULL ||
|
|
ibus_attr_list_get(attrs, 0) != NULL)
|
|
log->invalid = 1;
|
|
else
|
|
log->rcommit = 1;
|
|
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
|
|
g_main_loop_quit(log->loop);
|
|
return;
|
|
}
|
|
if(log->retake){
|
|
/* Typing again takes back the reading the other context took. */
|
|
if(strcmp(s, "k") == 0)
|
|
log->retook = 1;
|
|
else
|
|
log->invalid = 1;
|
|
log->retake = 0;
|
|
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
|
|
g_main_loop_quit(log->loop);
|
|
return;
|
|
}
|
|
if(strcmp(s, "か") != 0 || attrs == NULL ||
|
|
ibus_attr_list_get(attrs, 0) != NULL)
|
|
log->invalid = 1;
|
|
else
|
|
log->sawcommit = 1;
|
|
log->done = log->sawpreedit && log->sawcommit;
|
|
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
|
|
g_main_loop_quit(log->loop);
|
|
}
|
|
|
|
static void
|
|
required(IBusInputContext *ctx, void *arg)
|
|
{
|
|
Log *log;
|
|
|
|
(void)ctx;
|
|
log = arg;
|
|
log->required = 1;
|
|
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
|
|
g_main_loop_quit(log->loop);
|
|
}
|
|
|
|
static void
|
|
deleted(IBusInputContext *ctx, gint offset, guint n, void *arg)
|
|
{
|
|
Log *log;
|
|
|
|
(void)ctx;
|
|
log = arg;
|
|
log->deleted = 1;
|
|
log->delbefore = offset;
|
|
log->delcount = n;
|
|
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
|
|
g_main_loop_quit(log->loop);
|
|
}
|
|
|
|
static gboolean
|
|
timeout(void *arg)
|
|
{
|
|
g_main_loop_quit(arg);
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
static int
|
|
waitflag(Log *log, int *flag)
|
|
{
|
|
guint timer;
|
|
|
|
if(*flag)
|
|
return 1;
|
|
log->waiting = flag;
|
|
log->loop = g_main_loop_new(NULL, FALSE);
|
|
timer = g_timeout_add_seconds(4, timeout, log->loop);
|
|
g_main_loop_run(log->loop);
|
|
if(g_main_context_find_source_by_id(NULL, timer) != NULL)
|
|
g_source_remove(timer);
|
|
g_main_loop_unref(log->loop);
|
|
log->loop = NULL;
|
|
log->waiting = NULL;
|
|
return *flag;
|
|
}
|
|
|
|
int
|
|
main(int argc, char **argv)
|
|
{
|
|
IBusBus *bus;
|
|
IBusInputContext *a, *b, *c;
|
|
IBusText *around;
|
|
Log log;
|
|
int ok;
|
|
|
|
if(argc != 2){
|
|
fprintf(stderr, "usage: ibus_client_smoke address\n");
|
|
return 2;
|
|
}
|
|
if(setenv("IBUS_ADDRESS", argv[1], 1) < 0 ||
|
|
unsetenv("IBUS_ADDRESS_FILE") < 0 ||
|
|
unsetenv("DBUS_SESSION_BUS_ADDRESS") < 0 || unsetenv("DISPLAY") < 0){
|
|
perror("ibus_client_smoke: environment");
|
|
return 1;
|
|
}
|
|
memset(&log, 0, sizeof log);
|
|
b = c = NULL;
|
|
ibus_init();
|
|
bus = ibus_bus_new();
|
|
if(bus == NULL || !ibus_bus_is_connected(bus)){
|
|
fprintf(stderr, "ibus_client_smoke: cannot connect to private bus\n");
|
|
if(bus != NULL) g_object_unref(bus);
|
|
return 1;
|
|
}
|
|
a = ibus_bus_create_input_context(bus, "strans-libibus-smoke-a");
|
|
if(a == NULL){
|
|
fprintf(stderr, "ibus_client_smoke: cannot create input context\n");
|
|
g_object_unref(bus);
|
|
return 1;
|
|
}
|
|
log.a = a;
|
|
g_signal_connect(a, "update-preedit-text", G_CALLBACK(legacy), &log);
|
|
g_signal_connect(a, "update-preedit-text-with-mode", G_CALLBACK(modern),
|
|
&log);
|
|
g_signal_connect(a, "commit-text", G_CALLBACK(commit), &log);
|
|
ibus_input_context_set_capabilities(a, IBUS_CAP_PREEDIT_TEXT);
|
|
ibus_input_context_focus_in(a);
|
|
ok = ibus_input_context_process_key_event(a, 'n', 0, IBUS_CONTROL_MASK) &&
|
|
ibus_input_context_process_key_event(a, 'k', 0, 0) &&
|
|
ibus_input_context_process_key_event(a, 'a', 0, 0) &&
|
|
ibus_input_context_process_key_event(a, '0', 0, 0) &&
|
|
waitflag(&log, &log.done);
|
|
b = ibus_bus_create_input_context(bus, "strans-libibus-smoke-b");
|
|
if(b == NULL)
|
|
ok = 0;
|
|
else{
|
|
g_signal_connect(b, "update-preedit-text", G_CALLBACK(legacy), &log);
|
|
g_signal_connect(b, "update-preedit-text-with-mode",
|
|
G_CALLBACK(modern), &log);
|
|
ibus_input_context_set_capabilities(b, IBUS_CAP_PREEDIT_TEXT);
|
|
log.transfer = 1;
|
|
ok = ok && ibus_input_context_process_key_event(a, 'k', 0, 0) &&
|
|
waitflag(&log, &log.atext);
|
|
ibus_input_context_focus_in(b);
|
|
ok = ok && ibus_input_context_process_key_event(b, 'n', 0, 0) &&
|
|
waitflag(&log, &log.aclear);
|
|
ibus_input_context_focus_out(b);
|
|
}
|
|
ibus_input_context_focus_in(a);
|
|
log.retake = 1;
|
|
ok = ok && ibus_input_context_process_key_event(a, 's', 0,
|
|
IBUS_CONTROL_MASK) && waitflag(&log, &log.retook);
|
|
log.repeat = 1;
|
|
ok = ok && ibus_input_context_process_key_event(a, 'z', 0, 0) &&
|
|
waitflag(&log, &log.rfirst);
|
|
log.repeat = 2;
|
|
log.revent[0] = '\0';
|
|
log.rn = 0;
|
|
ok = ok && ibus_input_context_process_key_event(a, 'z', 0, 0) &&
|
|
waitflag(&log, &log.rdone) && strcmp(log.revent, "PKP") == 0;
|
|
log.repeat = 3;
|
|
ibus_input_context_focus_out(a);
|
|
ok = ok && waitflag(&log, &log.flushed);
|
|
/*
|
|
* A word is committed a syllable at a time, so the Hanja key must
|
|
* reach into what the client already holds and ask for it back.
|
|
*/
|
|
c = ibus_bus_create_input_context(bus, "strans-libibus-smoke-c");
|
|
if(c == NULL)
|
|
ok = 0;
|
|
else{
|
|
g_signal_connect(c, "commit-text", G_CALLBACK(commit), &log);
|
|
g_signal_connect(c, "require-surrounding-text",
|
|
G_CALLBACK(required), &log);
|
|
g_signal_connect(c, "delete-surrounding-text",
|
|
G_CALLBACK(deleted), &log);
|
|
log.repeat = 0;
|
|
ibus_input_context_set_capabilities(c,
|
|
IBUS_CAP_PREEDIT_TEXT|IBUS_CAP_SURROUNDING_TEXT);
|
|
ibus_input_context_focus_in(c);
|
|
ok = ok && waitflag(&log, &log.required);
|
|
around = ibus_text_new_from_string("저는 한");
|
|
ibus_input_context_set_surrounding_text(c, around, 4, 4);
|
|
log.hanja = 1;
|
|
ok = ok && ibus_input_context_process_key_event(c, 's', 0,
|
|
IBUS_CONTROL_MASK) &&
|
|
ibus_input_context_process_key_event(c, 'w', 0, 0) &&
|
|
ibus_input_context_process_key_event(c, 'k', 0, 0) &&
|
|
ibus_input_context_process_key_event(c, 'h', 0,
|
|
IBUS_CONTROL_MASK) &&
|
|
ibus_input_context_process_key_event(c, IBUS_KEY_Return,
|
|
0, 0) && waitflag(&log, &log.converted);
|
|
log.hanja = 0;
|
|
}
|
|
|
|
if(log.legacy == 0 || log.modern != 0 || log.commit != 5 ||
|
|
log.invalid || !log.done || !log.atext || !log.aclear ||
|
|
!log.retook || log.clearctx != a || !log.rdone || !log.converted)
|
|
ok = 0;
|
|
if(c != NULL)
|
|
g_object_unref(c);
|
|
if(b != NULL)
|
|
g_object_unref(b);
|
|
g_object_unref(a);
|
|
g_object_unref(bus);
|
|
if(!ok){
|
|
fprintf(stderr,
|
|
"ibus_client_smoke: legacy=%d modern=%d commit=%d invalid=%d preedit=%d committed=%d transfer-text=%d a-clear=%d retook=%d repeat=%s flushed=%d required=%d deleted=%d,%d converted=%d\n",
|
|
log.legacy, log.modern, log.commit, log.invalid,
|
|
log.sawpreedit, log.sawcommit, log.atext, log.aclear,
|
|
log.retook, log.revent, log.flushed, log.required,
|
|
log.delbefore, log.delcount, log.converted);
|
|
return 1;
|
|
}
|
|
printf("official libibus client preedit, commit, owner clear, take-back and focus-out hand-back: ok\n");
|
|
return 0;
|
|
}
|