ipc.c: an AF_UNIX nonblocking connect completes at once or fails with EAGAIN, so the EINPROGRESS/poll/SO_ERROR path and the fcntl juggling were dead; the deadline plumbing checked a clock that cannot fail and re-tested the deadline before every transfer although only waitfd blocks; readfield's truncation and discard loop served the unit test, since every caller owns char[Ipcfieldmax+1]; NULL-argument checks on in-tree encoders are gone (peer validation stays). The primary key frame is Ipckey, not 'legacy'; ipckeysym names the keysym mapping. srv.c: the per-connection capability and caret already lived in the persistent Keyreq; the mirror locals and 'negotiated' flag guarded a protocol rule no client relied on. proccreate never fails in libthread. gtk: focus-out no longer round-trips a reset before closing the socket that releases the engine; the caret dedup compares the packed frame; srvconnect's preedit no-ops and the insimple flag are gone.
529 lines
10 KiB
C
529 lines
10 KiB
C
#include <errno.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <gtk/gtk.h>
|
|
#ifdef GDK_WINDOWING_X11
|
|
#include <gdk/gdkx.h>
|
|
#endif
|
|
#include "ipc.h"
|
|
|
|
typedef struct Im Im;
|
|
struct Im
|
|
{
|
|
GtkIMContextSimple parent;
|
|
int fd;
|
|
int usepreedit;
|
|
int ext;
|
|
int private;
|
|
int simpleactive;
|
|
int simpledone;
|
|
char pre[Ipcfieldmax+1];
|
|
int prelen;
|
|
GdkWindow *win;
|
|
GdkRectangle cursor;
|
|
int cursorvalid;
|
|
int caretsent;
|
|
unsigned char sent[Ipccaretsz];
|
|
};
|
|
|
|
typedef struct ImClass ImClass;
|
|
struct ImClass
|
|
{
|
|
GtkIMContextSimpleClass parent;
|
|
};
|
|
|
|
static GType imtype;
|
|
static GObjectClass *parentobject;
|
|
static GtkIMContextClass *parentim;
|
|
|
|
static void
|
|
setpreedit(Im *im, const char *s, int n)
|
|
{
|
|
int was;
|
|
|
|
if(n < 0 || n > Ipcfieldmax)
|
|
n = 0;
|
|
if(n == 0 && im->prelen == 0)
|
|
return;
|
|
was = im->prelen;
|
|
if(n > 0)
|
|
memcpy(im->pre, s, n);
|
|
im->pre[n] = '\0';
|
|
im->prelen = n;
|
|
if(was == 0 && n > 0)
|
|
g_signal_emit_by_name(im, "preedit-start");
|
|
g_signal_emit_by_name(im, "preedit-changed");
|
|
if(was > 0 && n == 0)
|
|
g_signal_emit_by_name(im, "preedit-end");
|
|
}
|
|
|
|
static void
|
|
srvdrop(Im *im, int notify)
|
|
{
|
|
if(im->fd >= 0)
|
|
close(im->fd);
|
|
im->fd = -1;
|
|
im->ext = 0;
|
|
im->caretsent = 0;
|
|
if(notify)
|
|
setpreedit(im, "", 0);
|
|
else{
|
|
im->pre[0] = '\0';
|
|
im->prelen = 0;
|
|
}
|
|
}
|
|
|
|
static void
|
|
srvclose(Im *im)
|
|
{
|
|
srvdrop(im, 1);
|
|
}
|
|
|
|
/* Both buffers are char[Ipcfieldmax+1]. */
|
|
static int
|
|
readresp(Im *im, int want, char *commit, char *pre, Ipcresp *resp)
|
|
{
|
|
if(ipcreadresp(im->fd, want, commit, pre, resp) < 0)
|
|
return -1;
|
|
if(!g_utf8_validate(commit, resp->commitlen, NULL) ||
|
|
(want && !g_utf8_validate(pre, resp->preeditlen, NULL))){
|
|
errno = EPROTO;
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
dropwindow(Im *im)
|
|
{
|
|
if(im->win != NULL)
|
|
g_object_unref(im->win);
|
|
im->win = NULL;
|
|
im->cursorvalid = 0;
|
|
}
|
|
|
|
/* Root-relative caret in device pixels; returns whether it is known. */
|
|
static int
|
|
caretget(Im *im, int32_t *x, int32_t *y, int32_t *h)
|
|
{
|
|
gint rx, ry, scale;
|
|
int64_t sx, sy, sh;
|
|
|
|
*x = *y = *h = 0;
|
|
if(im->win != NULL && gdk_window_is_destroyed(im->win))
|
|
dropwindow(im);
|
|
if(!im->cursorvalid || im->win == NULL || im->cursor.height < 0)
|
|
return 0;
|
|
#ifdef GDK_WINDOWING_X11
|
|
if(!GDK_IS_X11_WINDOW(im->win))
|
|
return 0;
|
|
#else
|
|
return 0;
|
|
#endif
|
|
scale = gdk_window_get_scale_factor(im->win);
|
|
if(scale <= 0)
|
|
return 0;
|
|
/* GDK translates the client-relative point before device scaling. */
|
|
gdk_window_get_root_coords(im->win, 0, 0, &rx, &ry);
|
|
sx = ((int64_t)rx + im->cursor.x) * scale;
|
|
sy = ((int64_t)ry + im->cursor.y) * scale;
|
|
sh = (int64_t)im->cursor.height * scale;
|
|
if(sx < INT32_MIN || sx > INT32_MAX ||
|
|
sy < INT32_MIN || sy > INT32_MAX || sh > INT32_MAX)
|
|
return 0;
|
|
*x = sx;
|
|
*y = sy;
|
|
*h = sh;
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
sendcaret(Im *im)
|
|
{
|
|
unsigned char buf[Ipccaretsz];
|
|
int32_t x, y, h;
|
|
int valid;
|
|
|
|
if(im->fd < 0 || !im->ext)
|
|
return 0;
|
|
valid = caretget(im, &x, &y, &h);
|
|
ipcpackcaret(buf, valid, x, y, h);
|
|
if(im->caretsent && memcmp(buf, im->sent, sizeof buf) == 0)
|
|
return 0;
|
|
if(ipcsend(im->fd, buf, sizeof buf) < 0)
|
|
return -1;
|
|
memcpy(im->sent, buf, sizeof buf);
|
|
im->caretsent = 1;
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
srvconnect(Im *im)
|
|
{
|
|
unsigned char buf[Ipcreqsz];
|
|
char commit[Ipcfieldmax+1];
|
|
char pre[Ipcfieldmax+1];
|
|
Ipcresp resp;
|
|
|
|
if(im->fd >= 0)
|
|
return 0;
|
|
im->fd = ipcconnect();
|
|
if(im->fd < 0)
|
|
return -1;
|
|
/* Marker zero is an old daemon's harmless key-zero response. */
|
|
ipcpackcap(buf, im->usepreedit);
|
|
if(ipcsend(im->fd, buf, sizeof buf) < 0 ||
|
|
readresp(im, im->usepreedit, commit, pre, &resp) < 0){
|
|
srvclose(im);
|
|
return -1;
|
|
}
|
|
im->ext = resp.eaten != 0;
|
|
if(sendcaret(im) < 0){
|
|
srvclose(im);
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
simplecommit(GtkIMContext *ctx, const char *s, Im *im)
|
|
{
|
|
(void)ctx;
|
|
(void)s;
|
|
im->simpledone = 1;
|
|
}
|
|
|
|
static void
|
|
simpleend(GtkIMContext *ctx, Im *im)
|
|
{
|
|
(void)ctx;
|
|
im->simpledone = 1;
|
|
}
|
|
|
|
/*
|
|
* GtkIMContextSimple composes dead keys and Compose sequences; it keeps
|
|
* the key stream until it commits, ends its preedit, or rejects a press.
|
|
*/
|
|
static gboolean
|
|
simplefilter(GtkIMContext *ctx, GdkEventKey *ev, int release)
|
|
{
|
|
Im *im;
|
|
gboolean r;
|
|
|
|
im = (Im*)ctx;
|
|
im->simpleactive = 1;
|
|
im->simpledone = 0;
|
|
r = parentim->filter_keypress(ctx, ev);
|
|
if((!r && !release) || im->simpledone)
|
|
im->simpleactive = 0;
|
|
return r;
|
|
}
|
|
|
|
static void
|
|
sendreset(Im *im)
|
|
{
|
|
unsigned char buf[Ipcreqsz];
|
|
char commit[Ipcfieldmax+1];
|
|
char pre[Ipcfieldmax+1];
|
|
Ipcresp resp;
|
|
|
|
if(im->fd < 0){
|
|
setpreedit(im, "", 0);
|
|
return;
|
|
}
|
|
ipcpackreset(buf, im->usepreedit);
|
|
if(ipcsend(im->fd, buf, sizeof buf) < 0 ||
|
|
readresp(im, im->usepreedit, commit, pre, &resp) < 0){
|
|
srvclose(im);
|
|
return;
|
|
}
|
|
setpreedit(im, "", 0);
|
|
}
|
|
|
|
static gboolean
|
|
kpress(GtkIMContext *ctx, GdkEventKey *ev)
|
|
{
|
|
Im *im;
|
|
unsigned char buf[Ipcreqsz];
|
|
char commit[Ipcfieldmax+1];
|
|
char pre[Ipcfieldmax+1];
|
|
uint32_t key, mod;
|
|
Ipcresp resp;
|
|
|
|
im = (Im*)ctx;
|
|
if(ev->type != GDK_KEY_PRESS){
|
|
if(im->simpleactive)
|
|
return simplefilter(ctx, ev, 1);
|
|
return FALSE;
|
|
}
|
|
if(im->simpleactive)
|
|
return simplefilter(ctx, ev, 0);
|
|
key = ipckeysym(ev->keyval, gdk_keyval_to_unicode(ev->keyval));
|
|
mod = ipcmod(ev->state);
|
|
if(im->private || key == 0)
|
|
return simplefilter(ctx, ev, 0);
|
|
if(srvconnect(im) < 0)
|
|
return simplefilter(ctx, ev, 0);
|
|
/* A retained GDK window may have moved since the last cursor report. */
|
|
if(sendcaret(im) < 0){
|
|
srvclose(im);
|
|
return simplefilter(ctx, ev, 0);
|
|
}
|
|
ipcpackreq(buf, im->usepreedit, mod, key);
|
|
if(ipcsend(im->fd, buf, sizeof buf) < 0 ||
|
|
readresp(im, im->usepreedit, commit, pre, &resp) < 0){
|
|
srvclose(im);
|
|
return simplefilter(ctx, ev, 0);
|
|
}
|
|
if(im->usepreedit && commit[0] != '\0' && im->prelen > 0)
|
|
setpreedit(im, "", 0);
|
|
if(commit[0] != '\0')
|
|
g_signal_emit_by_name(ctx, "commit", commit);
|
|
if(im->usepreedit)
|
|
setpreedit(im, pre, resp.preeditlen);
|
|
if(resp.eaten)
|
|
return TRUE;
|
|
return simplefilter(ctx, ev, 0);
|
|
}
|
|
|
|
static void
|
|
getpreedit(GtkIMContext *ctx, gchar **str, PangoAttrList **attrs,
|
|
gint *cursor_pos)
|
|
{
|
|
Im *im;
|
|
PangoAttribute *u;
|
|
|
|
im = (Im*)ctx;
|
|
if(im->simpleactive){
|
|
parentim->get_preedit_string(ctx, str, attrs, cursor_pos);
|
|
return;
|
|
}
|
|
if(str)
|
|
*str = g_strdup(im->pre);
|
|
if(attrs){
|
|
*attrs = pango_attr_list_new();
|
|
if(im->prelen > 0){
|
|
u = pango_attr_underline_new(PANGO_UNDERLINE_SINGLE);
|
|
u->start_index = 0;
|
|
u->end_index = im->prelen;
|
|
pango_attr_list_insert(*attrs, u);
|
|
}
|
|
}
|
|
if(cursor_pos)
|
|
*cursor_pos = g_utf8_strlen(im->pre, -1);
|
|
}
|
|
|
|
static void
|
|
reset(GtkIMContext *ctx)
|
|
{
|
|
if(parentim->reset != NULL)
|
|
parentim->reset(ctx);
|
|
((Im*)ctx)->simpleactive = 0;
|
|
sendreset((Im*)ctx);
|
|
}
|
|
|
|
static void
|
|
focusout(GtkIMContext *ctx)
|
|
{
|
|
Im *im;
|
|
|
|
im = (Im*)ctx;
|
|
if(parentim->focus_out != NULL)
|
|
parentim->focus_out(ctx);
|
|
im->simpleactive = 0;
|
|
/* Closing the connection releases the engine, which resets it. */
|
|
srvclose(im);
|
|
}
|
|
|
|
static void
|
|
setusepreedit(GtkIMContext *ctx, gboolean use)
|
|
{
|
|
Im *im;
|
|
unsigned char buf[Ipcreqsz];
|
|
char commit[Ipcfieldmax+1];
|
|
char pre[Ipcfieldmax+1];
|
|
Ipcresp resp;
|
|
|
|
im = (Im*)ctx;
|
|
use = use != FALSE;
|
|
if(parentim->set_use_preedit != NULL)
|
|
parentim->set_use_preedit(ctx, use);
|
|
if(im->usepreedit == use)
|
|
return;
|
|
im->usepreedit = use;
|
|
if(!use)
|
|
setpreedit(im, "", 0);
|
|
if(im->fd < 0 || !im->ext)
|
|
return;
|
|
ipcpackcap(buf, use);
|
|
if(ipcsend(im->fd, buf, sizeof buf) < 0 ||
|
|
readresp(im, use, commit, pre, &resp) < 0 ||
|
|
!resp.eaten){
|
|
srvclose(im);
|
|
return;
|
|
}
|
|
if(use)
|
|
setpreedit(im, pre, resp.preeditlen);
|
|
}
|
|
|
|
static void
|
|
setclientwindow(GtkIMContext *ctx, GdkWindow *win)
|
|
{
|
|
Im *im;
|
|
|
|
im = (Im*)ctx;
|
|
if(win != NULL && gdk_window_is_destroyed(win))
|
|
win = NULL;
|
|
if(parentim->set_client_window != NULL)
|
|
parentim->set_client_window(ctx, win);
|
|
if(im->win == win)
|
|
return;
|
|
if(win != NULL)
|
|
g_object_ref(win);
|
|
dropwindow(im);
|
|
im->win = win;
|
|
if(sendcaret(im) < 0)
|
|
srvclose(im);
|
|
}
|
|
|
|
static void
|
|
setcursorlocation(GtkIMContext *ctx, GdkRectangle *area)
|
|
{
|
|
Im *im;
|
|
|
|
im = (Im*)ctx;
|
|
if(parentim->set_cursor_location != NULL)
|
|
parentim->set_cursor_location(ctx, area);
|
|
if(im->win != NULL && gdk_window_is_destroyed(im->win))
|
|
dropwindow(im);
|
|
if(area == NULL || im->win == NULL)
|
|
im->cursorvalid = 0;
|
|
else{
|
|
im->cursor = *area;
|
|
im->cursorvalid = 1;
|
|
}
|
|
if(sendcaret(im) < 0)
|
|
srvclose(im);
|
|
}
|
|
|
|
static int
|
|
isprivate(GtkInputPurpose purpose)
|
|
{
|
|
return purpose == GTK_INPUT_PURPOSE_PASSWORD ||
|
|
purpose == GTK_INPUT_PURPOSE_PIN;
|
|
}
|
|
|
|
static void
|
|
purposechanged(GObject *obj, GParamSpec *pspec, gpointer data)
|
|
{
|
|
Im *im;
|
|
GtkInputPurpose purpose;
|
|
int private;
|
|
|
|
(void)pspec;
|
|
(void)data;
|
|
im = (Im*)obj;
|
|
g_object_get(obj, "input-purpose", &purpose, NULL);
|
|
private = isprivate(purpose);
|
|
if(private == im->private)
|
|
return;
|
|
im->private = private;
|
|
if(private)
|
|
reset(GTK_IM_CONTEXT(im));
|
|
}
|
|
|
|
static void
|
|
dispose(GObject *obj)
|
|
{
|
|
Im *im;
|
|
|
|
im = (Im*)obj;
|
|
srvdrop(im, 0);
|
|
dropwindow(im);
|
|
parentobject->dispose(obj);
|
|
}
|
|
|
|
static void
|
|
init(Im *im)
|
|
{
|
|
GtkInputPurpose purpose;
|
|
|
|
im->fd = -1;
|
|
im->usepreedit = 1;
|
|
im->pre[0] = '\0';
|
|
g_object_get(im, "input-purpose", &purpose, NULL);
|
|
im->private = isprivate(purpose);
|
|
g_signal_connect(im, "notify::input-purpose",
|
|
G_CALLBACK(purposechanged), NULL);
|
|
g_signal_connect(im, "commit", G_CALLBACK(simplecommit), im);
|
|
g_signal_connect(im, "preedit-end", G_CALLBACK(simpleend), im);
|
|
}
|
|
|
|
static void
|
|
classinit(ImClass *klass)
|
|
{
|
|
GtkIMContextClass *ic;
|
|
GObjectClass *oc;
|
|
|
|
ic = GTK_IM_CONTEXT_CLASS(klass);
|
|
oc = G_OBJECT_CLASS(klass);
|
|
parentim = g_type_class_peek_parent(klass);
|
|
parentobject = G_OBJECT_CLASS(parentim);
|
|
ic->filter_keypress = kpress;
|
|
ic->get_preedit_string = getpreedit;
|
|
ic->reset = reset;
|
|
ic->focus_out = focusout;
|
|
ic->set_use_preedit = setusepreedit;
|
|
ic->set_client_window = setclientwindow;
|
|
ic->set_cursor_location = setcursorlocation;
|
|
oc->dispose = dispose;
|
|
}
|
|
|
|
static const GtkIMContextInfo info = {
|
|
"strans",
|
|
"strans",
|
|
"strans",
|
|
"",
|
|
"*",
|
|
};
|
|
|
|
static const GtkIMContextInfo *infolist[] = { &info };
|
|
|
|
G_MODULE_EXPORT void
|
|
im_module_init(GTypeModule *mod)
|
|
{
|
|
static const GTypeInfo ti = {
|
|
sizeof(ImClass),
|
|
NULL, NULL,
|
|
(GClassInitFunc)classinit,
|
|
NULL, NULL,
|
|
sizeof(Im),
|
|
0,
|
|
(GInstanceInitFunc)init,
|
|
};
|
|
imtype = g_type_module_register_type(mod, GTK_TYPE_IM_CONTEXT_SIMPLE,
|
|
"strans-gtk", &ti, 0);
|
|
}
|
|
|
|
G_MODULE_EXPORT void
|
|
im_module_exit(void)
|
|
{
|
|
}
|
|
|
|
G_MODULE_EXPORT void
|
|
im_module_list(const GtkIMContextInfo ***contexts, int *n)
|
|
{
|
|
*contexts = infolist;
|
|
*n = 1;
|
|
}
|
|
|
|
G_MODULE_EXPORT GtkIMContext*
|
|
im_module_create(const char *id)
|
|
{
|
|
if(strcmp(id, "strans") == 0)
|
|
return g_object_new(imtype, NULL);
|
|
return NULL;
|
|
}
|