Compare commits

..

8 Commits

Author SHA1 Message Date
82276bf37f strengthen core unit tests
Add table-driven dictionary miss and language-switch state coverage, regress stale candidates after backspace, and clear them before dictionary refresh. Keep IPC and runtime-created file checks outside the unit runner.
2026-08-11 21:12:23 +09:00
9b3f2c46f0 restore unversioned ipc path 2026-08-11 20:05:37 +09:00
099acdf2a6 stop tracking build outputs 2026-08-11 19:35:19 +09:00
42823420ab add focused unit and map checks 2026-08-11 19:35:11 +09:00
9dc116a035 harden ipc and frontend recovery 2026-08-11 19:34:47 +09:00
4bfb659b6d fix candidate popup bounds 2026-08-11 19:34:27 +09:00
7b40ef419f fix vietnamese telex state 2026-08-11 19:34:14 +09:00
e70278a3d2 fix core text and map ownership 2026-08-11 19:32:58 +09:00
41 changed files with 5555 additions and 1665 deletions

2
.gitignore vendored
View File

@@ -1,5 +1,7 @@
strans
strans-xim
/tests/unit_test
/bench/bench
# Prerequisites
*.d

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "cutest"]
path = cutest
url = ../cutest.git

View File

@@ -1,9 +1,15 @@
CC = 9c
LD = 9l
PKGGOALS := $(filter-out check test verify-map clean xim bench,$(MAKECMDGOALS))
ifeq ($(MAKECMDGOALS),)
PKGGOALS := all
endif
ifneq ($(PKGGOALS),)
DBUS_CFLAGS := $(shell pkg-config --cflags dbus-1)
DBUS_LIBS := $(shell pkg-config --libs dbus-1)
WL_CFLAGS := $(shell pkg-config --cflags wayland-client xkbcommon)
WL_LIBS := $(shell pkg-config --libs wayland-client xkbcommon)
endif
CFLAGS = -Wall -Wextra -O2 -g $(DBUS_CFLAGS) $(WL_CFLAGS)
PROG = strans
@@ -19,13 +25,22 @@ $(OBJS): dat.h fn.h ipc.h
clean:
rm -f $(OBJS) $(PROG)
make -C xim/ clean
make -C bench/ clean
$(MAKE) -C tests clean
$(MAKE) -C xim/ clean
$(MAKE) -C bench/ clean
xim:
make -C xim/
$(MAKE) -C xim/
bench:
make -C bench/
$(MAKE) -C bench/
.PHONY: all clean xim bench
check: verify-map test
test:
$(MAKE) -C tests check TESTARGS="$(TESTARGS)"
verify-map:
python3 map/mktelex.py | cmp - map/telex.map
.PHONY: all check test verify-map clean xim bench

View File

@@ -7,6 +7,7 @@ Inspired by 9front's ktrans. Threads communicate via CSP channels.
## Dependencies
- plan9port
- Python 3 (for generated-map checks)
- dbus-1
- wayland-client, libxkbcommon (for Wayland support)
- gtk+-3.0 (optional, for GTK IM module)
@@ -17,6 +18,21 @@ Inspired by 9front's ktrans. Threads communicate via CSP channels.
cd xim && make # XIM adapter
cd gtk && make docker && make install # GTK IM module
Unit tests use the sibling `cutest` Git repository as a submodule and do not
require the Wayland, D-Bus, GTK, or X11 development packages. Repository
mirrors must provide the same sibling; initialize it after cloning with
`git submodule update --init`:
make check # generated map + unit tests
make test TESTARGS=hangul # filtered unit tests only
make verify-map # generated-map check only
The suite covers the fixed-size UTF-8 string, hash table, trie fixtures,
Japanese/Hangul/Telex state transitions, dictionary candidates, engine
selection state, and production map loading. Tests use explicit case tables,
boundary values, and small regressions for repaired core contracts. IPC and GUI
protocol stacks are outside this suite and require separate integration checks.
## Run
./strans map font &
@@ -60,10 +76,10 @@ Tab or Enter to commit.
Threads communicate via CSP channels:
- [imthread](strans.c#L271): keystroke processing, transliteration
- [dictthread](dict.c#L38): dictionary lookup
- [drawthread](win.c#L133): preedit window rendering
- [srvthread](srv.c#L42): IPC via unix socket
- [imthread](strans.c): keystroke processing, transliteration
- [dictthread](dict.c): dictionary lookup
- [drawthread](win.c): preedit window rendering
- [srvthread](srv.c): IPC via unix socket
- [ibusthread](ibus.c): IBus D-Bus endpoint for GLFW/kitty
- [waylandthread](wayland.c): Wayland input-method-v2 + virtual-keyboard-v1

View File

@@ -1,8 +1,8 @@
CC = cc
CFLAGS = -Wall -O2
bench: main.c
$(CC) $(CFLAGS) -o $@ main.c
bench: main.c ../ipc.c ../ipc.h
$(CC) $(CFLAGS) -I.. -o $@ main.c ../ipc.c
clean:
rm -f bench

Binary file not shown.

View File

@@ -1,3 +1,4 @@
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -5,13 +6,7 @@
#include <time.h>
#include <sys/socket.h>
#include <sys/un.h>
enum {
Kback = 0xf008,
Kret = 0xf00d,
Kesc = 0xf01b,
Mctrl = 1<<2,
};
#include "../ipc.h"
typedef struct Key Key;
struct Key {
@@ -93,8 +88,7 @@ dial(void)
die("socket");
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
snprintf(addr.sun_path, sizeof(addr.sun_path),
"/tmp/strans.%d", getuid());
snprintf(addr.sun_path, sizeof(addr.sun_path), IPCPATH, getuid());
if(connect(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0)
die("connect");
}
@@ -102,28 +96,20 @@ dial(void)
static void
sendkey(int key, int mod)
{
unsigned char req[4];
unsigned char req[Ipcreqsz];
req[0] = 0;
req[1] = mod;
req[2] = key & 0xff;
req[3] = (key >> 8) & 0xff;
if(write(fd, req, 4) != 4)
ipcpackreq(req, 0, mod, key);
if(ipcsend(fd, req, sizeof req) < 0)
die("write");
}
static int
readresp(void)
{
unsigned char buf[256];
int n;
char buf[Ipcfieldmax+1];
Ipcresp resp;
if(read(fd, buf, 2) != 2)
return -1;
n = buf[1];
if(n > 0 && read(fd, buf + 2, n) != n)
return -1;
return 0;
return ipcreadresp(fd, 0, buf, sizeof buf, NULL, 0, &resp);
}
static double

1
cutest Submodule

Submodule cutest added at 7894ba7a26

23
dat.h
View File

@@ -19,15 +19,17 @@ enum
Fontsz = 32,
Fontbase = 4,
Nglyphs = 0x20000,
Maxrunes = 64,
Maxutf = Maxrunes * UTFmax + 1,
};
enum
{
Maxclients = 64,
Maxkouho = 32,
Maxdisp = 8,
Imgw = 512,
Imgh = Maxdisp * Fontsz + 8,
Maxdisp = 9,
Imgw = (Maxrunes + 3) * Fontsz,
Imgh = (Maxdisp + 1) * Fontsz,
Colfg = 0x000000,
Colbg = 0xffffff,
@@ -37,7 +39,7 @@ enum
typedef struct Str Str;
struct Str
{
Rune r[64];
Rune r[Maxrunes];
int n;
};
@@ -108,6 +110,7 @@ struct Im
{
Lang *l;
Str pre;
Str raw; /* physical keys for the current Telex preedit */
Str kouho[Maxkouho];
int nkouho;
int sel;
@@ -123,12 +126,20 @@ struct Drawcmd
};
typedef struct Keyreq Keyreq;
typedef struct Keyres Keyres;
struct Keyres
{
int eaten;
Str commit;
Str preedit;
};
struct Keyreq
{
int fd;
u32int ks;
u32int mod;
int want; /* nonzero: append preedit after commit */
int want; /* nonzero: include preedit in reply */
Channel *reply;
};
typedef struct Dictreq Dictreq;

18
dict.c
View File

@@ -1,7 +1,7 @@
#include "dat.h"
#include "fn.h"
static void
void
dictlookup(Dictreq *req, Dictres *res)
{
Lang *l;
@@ -10,6 +10,7 @@ dictlookup(Dictreq *req, Dictres *res)
char *p, *e, *sp;
Str tmp;
res->key = req->pre;
res->nkouho = 0;
if(req->key.n == 0)
return;
@@ -23,6 +24,10 @@ dictlookup(Dictreq *req, Dictres *res)
p = n->val;
e = p + n->vlen;
while(res->nkouho < Maxkouho && p < e){
while(p < e && *p == ' ')
p++;
if(p == e)
break;
sp = p;
while(p < e && *p != ' ')
p++;
@@ -47,7 +52,6 @@ dictthread(void*)
while(channbrecv(dictreqc, &req) > 0)
;
dictlookup(&req, &res);
res.key = req.pre;
chansend(dictresc, &res);
}
}
@@ -57,16 +61,18 @@ dictopen(char *path)
{
Hmap *h;
Biobuf *b;
Str key, val;
Str key;
char *line, *tab;
int len;
b = Bopen(path, OREAD);
if(b == nil)
die("can't open: %s", path);
h = hmapalloc(4096, 0);
h = hmapalloc(4096);
while((line = Brdstr(b, '\n', 1)) != nil){
len = strlen(line);
if(len > 0 && line[len-1] == '\r')
line[--len] = '\0';
if(len == 0 || line[0] == ';'){
free(line);
continue;
@@ -78,8 +84,7 @@ dictopen(char *path)
}
*tab = '\0';
sinit(&key, line, tab - line);
sinit(&val, tab+1, len - (tab - line) - 1);
hmapset(&h, &key, &val);
hmapset(&h, &key, tab+1, len - (tab - line) - 1);
free(line);
}
Bterm(b);
@@ -99,4 +104,3 @@ dictinit(char *dir)
langs[i].dict = dictopen(path);
}
}

9
fn.h
View File

@@ -9,12 +9,14 @@ int scmp(Str*, Str*);
int stoutf(Str*, char*, int);
Rune slastr(Str*);
Hmap* hmapalloc(int, int);
int hmapset(Hmap**, Str*, Str*);
Hmap* hmapalloc(int);
void hmapfree(Hmap*);
void hmapset(Hmap**, Str*, const char*, int);
Hnode* hmapget(Hmap*, Str*);
int mapget(Trie*, Str*, Str*);
Trie* trieopen(char*);
void trieclose(Trie*);
char* trieget(Trie*, char*, int, int*);
int trielookup(Trie*, char*, int, char**, int*);
@@ -23,6 +25,7 @@ void mapinit(char*);
void dictinit(char*);
void dictthread(void*);
void dictlookup(Dictreq*, Dictres*);
void drawthread(void*);
void imthread(void*);
@@ -30,8 +33,10 @@ Emit transmap(Im*, Rune);
Emit transko(Im*, Rune);
Emit transvi(Im*, Rune);
void backko(Im*);
void backvi(Im*);
void dictsend(Im*, Str*);
int srvreadkey(int, Keyreq*);
void srvthread(void*);
void ibusthread(void*);
void waylandthread(void*);

View File

@@ -2,8 +2,8 @@ CFLAGS = -Wall -O2 -I.. `pkg-config --cflags gtk+-3.0`
LDFLAGS = `pkg-config --libs gtk+-3.0`
DSTDIR ?= $(shell find /usr/lib* -type d -path "*/gtk-3.0/*/immodules" 2>/dev/null | head -1)
im-strans.so: main.c
$(CC) -shared -fPIC $(CFLAGS) -o $@ $< $(LDFLAGS)
im-strans.so: main.c ../ipc.c ../ipc.h
$(CC) -shared -fPIC $(CFLAGS) -o $@ main.c ../ipc.c $(LDFLAGS)
docker: main.c Dockerfile
docker build -t strans-gtk .

View File

@@ -12,7 +12,7 @@ struct Im
{
GtkIMContext parent;
int fd;
char pre[256];
char pre[Ipcfieldmax+1];
int prelen;
};
@@ -36,52 +36,61 @@ srvconnect(Im *im)
return;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
snprintf(addr.sun_path, sizeof(addr.sun_path), "/tmp/strans.%d", getuid());
snprintf(addr.sun_path, sizeof(addr.sun_path), IPCPATH, getuid());
if(connect(im->fd, (struct sockaddr*)&addr, sizeof(addr)) < 0){
close(im->fd);
im->fd = -1;
}
}
static void
srvclose(Im *im)
{
int was;
if(im->fd >= 0)
close(im->fd);
im->fd = -1;
was = im->prelen;
im->pre[0] = '\0';
im->prelen = 0;
if(was > 0){
g_signal_emit_by_name(im, "preedit-changed");
g_signal_emit_by_name(im, "preedit-end");
}
}
static int
readresp(Im *im, char *buf, int bufsz)
{
unsigned char hdr[2], pl;
int n, was;
Ipcresp resp;
int was;
if(read(im->fd, hdr, 2) != 2)
return -1;
n = hdr[1];
buf[0] = '\0';
if(n > 0 && n < bufsz){
if(read(im->fd, buf, n) != n)
return -1;
buf[n] = '\0';
}
if(read(im->fd, &pl, 1) != 1)
return -1;
if(pl >= sizeof(im->pre))
return -1;
was = im->prelen;
if(pl > 0 && read(im->fd, im->pre, pl) != pl)
if(ipcreadresp(im->fd, 1, buf, bufsz, im->pre,
sizeof(im->pre), &resp) < 0)
return -1;
im->pre[pl] = '\0';
im->prelen = pl;
if(was == 0 && pl > 0)
im->prelen = resp.npreedit;
if(was == 0 && im->prelen > 0)
g_signal_emit_by_name(im, "preedit-start");
if(was != 0 || pl != 0)
if(was != 0 || im->prelen != 0)
g_signal_emit_by_name(im, "preedit-changed");
if(was > 0 && pl == 0)
if(was > 0 && im->prelen == 0)
g_signal_emit_by_name(im, "preedit-end");
return hdr[0];
return resp.eaten;
}
static uint32_t
kget(uint32_t gdk)
{
if(gdk >= 0xff00)
uint32_t u;
u = gdk_keyval_to_unicode(gdk);
if((gdk & 0xff000000) == 0x01000000 && u != 0)
return u;
if(gdk >= 0xff00 && gdk <= 0xffff)
return Kspec + (gdk - 0xff00);
return gdk_keyval_to_unicode(gdk);
return u;
}
static uint32_t
@@ -104,17 +113,17 @@ mget(uint32_t state)
static void
sendreset(Im *im)
{
unsigned char buf[4];
char resp[64];
unsigned char buf[Ipcreqsz];
char resp[Ipcfieldmax+1];
if(im->fd < 0)
if(im->fd < 0){
srvclose(im);
return;
buf[0] = 1;
buf[1] = 0;
buf[2] = Kesc & 0xff;
buf[3] = Kesc >> 8;
if(send(im->fd, buf, 4, MSG_NOSIGNAL) == 4)
readresp(im, resp, sizeof(resp));
}
ipcpackreq(buf, 1, 0, Kesc);
if(ipcsend(im->fd, buf, sizeof buf) < 0 ||
readresp(im, resp, sizeof(resp)) < 0)
srvclose(im);
}
static gboolean
@@ -137,8 +146,8 @@ static gboolean
kpress(GtkIMContext *ctx, GdkEventKey *ev)
{
Im *im;
unsigned char buf[4];
char resp[64];
unsigned char buf[Ipcreqsz];
char resp[Ipcfieldmax+1];
uint32_t key, mod;
int r;
GtkInputPurpose purpose;
@@ -154,21 +163,18 @@ kpress(GtkIMContext *ctx, GdkEventKey *ev)
if(purpose == GTK_INPUT_PURPOSE_PASSWORD || purpose == GTK_INPUT_PURPOSE_PIN)
return plaincommit(ctx, key, mod);
srvconnect(im);
if(im->fd < 0)
if(im->fd < 0){
srvclose(im);
return plaincommit(ctx, key, mod);
buf[0] = 1;
buf[1] = mod;
buf[2] = key & 0xff;
buf[3] = key >> 8;
if(send(im->fd, buf, 4, MSG_NOSIGNAL) != 4){
close(im->fd);
im->fd = -1;
}
ipcpackreq(buf, 1, mod, key);
if(ipcsend(im->fd, buf, sizeof buf) < 0){
srvclose(im);
return plaincommit(ctx, key, mod);
}
r = readresp(im, resp, sizeof(resp));
if(r < 0){
close(im->fd);
im->fd = -1;
srvclose(im);
return plaincommit(ctx, key, mod);
}
if(resp[0] != '\0')
@@ -228,6 +234,8 @@ static void
init(Im *im)
{
im->fd = -1;
im->pre[0] = '\0';
im->prelen = 0;
}
static void

55
hash.c
View File

@@ -18,13 +18,15 @@ hash(Str *s)
}
Hmap*
hmapalloc(int nbuckets, int size)
hmapalloc(int nbuckets)
{
void *store;
Hmap *h;
int nsz;
nsz = Tagsize + size;
if(nbuckets < 1)
die("hmapalloc: no buckets");
nsz = Tagsize;
store = emalloc(sizeof(*h) + nbuckets * nsz);
h = store;
h->nbs = nbuckets;
@@ -37,7 +39,7 @@ hmapalloc(int nbuckets, int size)
static int
keycmp(Hnode *n, Str *key)
{
char buf[256];
char buf[Maxutf];
int len;
len = stoutf(key, buf, sizeof(buf));
@@ -67,7 +69,7 @@ hmapget(Hmap *h, Str *key)
static char*
sdup(Str *s)
{
char buf[256];
char buf[Maxutf];
char *p;
int n;
@@ -78,15 +80,48 @@ sdup(Str *s)
return p;
}
int
hmapset(Hmap **store, Str *key, Str *val)
static char*
memdup(const char *src, int n)
{
char *p;
if(n == 0)
return nil;
p = emalloc(n + 1);
memmove(p, src, n);
p[n] = '\0';
return p;
}
void
hmapfree(Hmap *h)
{
Hnode *n;
int i;
if(h == nil)
return;
for(i = 0; i < h->len; i++){
n = (Hnode*)(h->nodes + i * h->nsz);
if(!n->filled)
continue;
free(n->key);
free(n->val);
}
free(h);
}
void
hmapset(Hmap **store, Str *key, const char *val, int vlen)
{
char *newval;
Hnode *n;
uchar *v;
Hmap *h;
int next;
vlong diff;
newval = memdup(val, vlen);
h = *store;
v = h->nodes + (hash(key) % h->nbs) * h->nsz;
for(;;){
@@ -121,9 +156,7 @@ replace:
n->filled = 1;
}
n->next = next;
if(val->n > 0){
n->val = sdup(val);
n->vlen = strlen(n->val);
}
return 0;
free(n->val);
n->val = newval;
n->vlen = vlen;
}

62
ibus.c
View File

@@ -8,6 +8,7 @@
#include <sys/stat.h>
#include <poll.h>
#include <dbus/dbus.h>
#include <xkbcommon/xkbcommon.h>
enum
{
@@ -30,6 +31,7 @@ static DBusServer *srv;
static char addrfile[256];
static int icctr;
static int busctr;
static Channel *replyc;
static DBusHandlerResult onmsg(DBusConnection*, DBusMessage*, void*);
@@ -182,9 +184,14 @@ togglewatch(DBusWatch *w, void *_)
static u32int
kget(u32int sym)
{
u32int c;
c = xkb_keysym_to_utf32(sym);
if(c >= ' ' && c != 0x7f)
return c;
if(sym >= 0xff00 && sym <= 0xffff)
return Kspec + (sym - 0xff00);
return sym;
return c;
}
static u32int
@@ -201,53 +208,24 @@ mget(u32int state)
}
static void
sendkey(u32int ks, u32int mod, char *com, int csz, char *pre, int psz,
int *eaten)
sendkey(u32int ks, u32int mod, Keyres *res)
{
Keyreq kr;
int p[2];
uchar hdr[2];
uchar pl;
int n;
*eaten = 0;
com[0] = '\0';
pre[0] = '\0';
if(pipe(p) < 0)
return;
kr.fd = p[1];
kr.ks = ks;
kr.mod = mod;
kr.want = 1;
kr.reply = replyc;
chansend(keyc, &kr);
if(read(p[0], hdr, 2) != 2)
goto out;
*eaten = hdr[0];
n = hdr[1];
if(n > 0 && n < csz){
if(read(p[0], com, n) != n)
goto out;
com[n] = '\0';
}
if(read(p[0], &pl, 1) != 1)
goto out;
if(pl > 0 && pl < psz){
if(read(p[0], pre, pl) != pl)
goto out;
pre[pl] = '\0';
}
out:
close(p[0]);
close(p[1]);
chanrecv(replyc, res);
}
static void
sendreset(void)
{
char com[64], pre[256];
int eaten;
Keyres res;
sendkey(Kesc, 0, com, sizeof(com), pre, sizeof(pre), &eaten);
sendkey(Kesc, 0, &res);
}
static void
@@ -305,7 +283,7 @@ emitpreedit(DBusConnection *c, const char *path, const char *text)
return;
dbus_message_iter_init_append(sig, &it);
appendibustext(&it, text);
cursor = utflen(text);
cursor = utflen((char*)text);
visible = text[0] != '\0' ? TRUE : FALSE;
dbus_message_iter_append_basic(&it, DBUS_TYPE_UINT32, &cursor);
dbus_message_iter_append_basic(&it, DBUS_TYPE_BOOLEAN, &visible);
@@ -353,8 +331,8 @@ handlekey(DBusConnection *c, DBusMessage *m)
DBusMessage *r;
DBusError err;
dbus_uint32_t sym, code, state;
char commit[256], preedit[256];
int eaten;
Keyres res;
char commit[Maxutf], preedit[Maxutf];
dbus_bool_t b;
u32int ks, mod;
@@ -378,13 +356,14 @@ handlekey(DBusConnection *c, DBusMessage *m)
}
ks = kget(sym);
mod = mget(state);
sendkey(ks, mod, commit, sizeof(commit), preedit, sizeof(preedit),
&eaten);
sendkey(ks, mod, &res);
stoutf(&res.commit, commit, sizeof commit);
stoutf(&res.preedit, preedit, sizeof preedit);
if(commit[0] != '\0')
emitcommit(c, dbus_message_get_path(m), commit);
emitpreedit(c, dbus_message_get_path(m), preedit);
r = dbus_message_new_method_return(m);
b = eaten ? TRUE : FALSE;
b = res.eaten ? TRUE : FALSE;
dbus_message_append_args(r, DBUS_TYPE_BOOLEAN, &b, DBUS_TYPE_INVALID);
dbus_connection_send(c, r, nil);
dbus_message_unref(r);
@@ -581,6 +560,7 @@ ibusthread(void *_)
threadsetname("ibus");
if(ibusinit() < 0)
return;
replyc = chancreate(sizeof(Keyres), 0);
for(;;){
n = 0;
for(i = 0; i < nwatches && n < Maxwatches; i++){

186
ipc.c Normal file
View File

@@ -0,0 +1,186 @@
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include "ipc.h"
#if !defined(MSG_NOSIGNAL) && !defined(SO_NOSIGPIPE)
#error "ipcsend requires MSG_NOSIGNAL or SO_NOSIGPIPE"
#endif
static void
putlen(unsigned char p[Ipclensz], size_t n)
{
p[0] = n;
p[1] = n >> 8;
}
static size_t
getlen(const unsigned char p[Ipclensz])
{
return p[0] | (p[1] << 8);
}
void
ipcpackreq(unsigned char req[Ipcreqsz], int want, unsigned int mod,
unsigned int key)
{
req[0] = want != 0;
req[1] = mod;
req[2] = key;
req[3] = key >> 8;
req[4] = key >> 16;
req[5] = key >> 24;
}
void
ipcunpackreq(const unsigned char req[Ipcreqsz], int *want, unsigned int *mod,
unsigned int *key)
{
*want = req[0] != 0;
*mod = req[1];
*key = (unsigned int)req[2] |
((unsigned int)req[3] << 8) |
((unsigned int)req[4] << 16) |
((unsigned int)req[5] << 24);
}
int
ipcpackresp(unsigned char *dst, size_t cap, int eaten,
const char *commit, size_t ncommit, const char *preedit, size_t npreedit,
int want)
{
size_t n;
if(ncommit > Ipcfieldmax || npreedit > Ipcfieldmax)
return -1;
if((ncommit > 0 && commit == NULL) ||
(want && npreedit > 0 && preedit == NULL))
return -1;
n = Ipcresphdrsz + ncommit + (want ? Ipclensz + npreedit : 0);
if(dst == NULL || cap < n)
return -1;
dst[0] = eaten != 0;
putlen(dst + 1, ncommit);
if(ncommit > 0)
memcpy(dst + Ipcresphdrsz, commit, ncommit);
if(want){
putlen(dst + Ipcresphdrsz + ncommit, npreedit);
if(npreedit > 0)
memcpy(dst + Ipcresphdrsz + ncommit + Ipclensz,
preedit, npreedit);
}
return n;
}
int
ipcreadn(int fd, void *buf, size_t n)
{
unsigned char *p;
ssize_t r;
p = buf;
while(n > 0){
r = read(fd, p, n);
if(r < 0 && errno == EINTR)
continue;
if(r <= 0)
return -1;
p += r;
n -= r;
}
return 0;
}
int
ipcsend(int fd, const void *buf, size_t n)
{
const unsigned char *p;
ssize_t r;
int flags;
#if !defined(MSG_NOSIGNAL) && defined(SO_NOSIGPIPE)
int one, sr;
#endif
p = buf;
#ifdef MSG_NOSIGNAL
flags = MSG_NOSIGNAL;
#else
flags = 0;
#ifdef SO_NOSIGPIPE
one = 1;
do
sr = setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof one);
while(sr < 0 && errno == EINTR);
if(sr < 0)
return -1;
#endif
#endif
while(n > 0){
r = send(fd, p, n, flags);
if(r < 0 && errno == EINTR)
continue;
if(r <= 0)
return -1;
p += r;
n -= r;
}
return 0;
}
static int
readfield(int fd, size_t n, char *dst, size_t cap)
{
unsigned char discard[128];
size_t keep, part;
if(cap > 0 && dst == NULL)
return -1;
keep = 0;
if(cap > 0)
keep = n >= cap ? cap - 1 : n;
if(keep > 0 && ipcreadn(fd, dst, keep) < 0)
return -1;
if(cap > 0)
dst[keep] = '\0';
n -= keep;
while(n > 0){
part = n < sizeof discard ? n : sizeof discard;
if(ipcreadn(fd, discard, part) < 0)
return -1;
n -= part;
}
return 0;
}
int
ipcreadresp(int fd, int want, char *commit, size_t ccap,
char *preedit, size_t pcap, Ipcresp *resp)
{
unsigned char hdr[Ipcresphdrsz], npreedit[Ipclensz];
if(resp == NULL || (ccap > 0 && commit == NULL) ||
(pcap > 0 && preedit == NULL))
return -1;
if(ccap > 0)
commit[0] = '\0';
if(pcap > 0)
preedit[0] = '\0';
memset(resp, 0, sizeof *resp);
if(ipcreadn(fd, hdr, sizeof hdr) < 0)
return -1;
resp->eaten = hdr[0] != 0;
resp->ncommit = getlen(hdr + 1);
if(resp->ncommit > Ipcfieldmax)
return -1;
if(readfield(fd, resp->ncommit, commit, ccap) < 0)
return -1;
if(!want)
return 0;
if(ipcreadn(fd, npreedit, sizeof npreedit) < 0)
return -1;
resp->npreedit = getlen(npreedit);
if(resp->npreedit > Ipcfieldmax)
return -1;
return readfield(fd, resp->npreedit, preedit, pcap);
}

41
ipc.h
View File

@@ -1,9 +1,21 @@
// req: 4 bytes [type, mod, key lo, key hi]
// resp: 2+n bytes [type, n, ...]
#ifndef STRANS_IPC_H
#define STRANS_IPC_H
#include <stddef.h>
#define IPCPATH "/tmp/strans.%d"
/*
* Request: [want-preedit, modifiers, key byte 0, ..., key byte 3].
* Response: [eaten, commit-length-low, commit-length-high, commit...],
* followed, when requested, by
* [preedit-length-low, preedit-length-high, preedit...].
* Lengths are little-endian byte counts; fields are at most Ipcfieldmax bytes.
*/
enum
{
Kspec = 0xf000,
Kspec = 0x110000,
Kback = Kspec|0x08,
Ktab = Kspec|0x09,
Kret = Kspec|0x0d,
@@ -17,4 +29,27 @@ enum
Mctrl = 1<<2,
Malt = 1<<3,
Msuper = 1<<6,
Ipcreqsz = 6,
Ipclensz = 2,
Ipcresphdrsz = 1 + Ipclensz,
Ipcfieldmax = 256,
Ipcmaxresp = Ipcresphdrsz + Ipcfieldmax + Ipclensz + Ipcfieldmax,
};
typedef struct Ipcresp Ipcresp;
struct Ipcresp
{
int eaten;
size_t ncommit;
size_t npreedit;
};
void ipcpackreq(unsigned char[Ipcreqsz], int, unsigned int, unsigned int);
void ipcunpackreq(const unsigned char[Ipcreqsz], int*, unsigned int*, unsigned int*);
int ipcpackresp(unsigned char*, size_t, int, const char*, size_t, const char*, size_t, int);
int ipcreadn(int, void*, size_t);
int ipcsend(int, const void*, size_t);
int ipcreadresp(int, int, char*, size_t, char*, size_t, Ipcresp*);
#endif

View File

@@ -1,5 +1,9 @@
#!/usr/bin/env python3
import sys
sys.stdout.reconfigure(encoding="utf-8")
# tone marks: s=rising f=falling r=hook x=tilde j=dot
tone = {
's': str.maketrans("aăâeêioôơuưy", "áắấéếíóốớúứý"),
@@ -24,24 +28,21 @@ modcons = [
("dd", "đ"),
]
upper = str.maketrans(
"aăâeêioôơuưyđáắấéếíóốớúứýàằầèềìòồờùừỳảẳẩẻểỉỏổởủửỷãẵẫẽễĩõỗỡũữỹạặậẹệịọộợụựỵ",
"AĂÂEÊIOÔƠUƯYĐÁẮẤÉẾÍÓỐỚÚỨÝÀẰẦÈỀÌÒỒỜÙỪỲẢẲẨẺỂỈỎỔỞỦỬỶÃẴẪẼỄĨÕỖỠŨỮỸẠẶẬẸỆỊỌỘỢỤỰỴ")
def addtone(v, t):
return v.translate(tone[t])
entries = []
def emit(input, output):
entries.append((input, output))
print(f"{input}\t{output}")
def up(s):
c = s[0].translate(upper)
if c == s[0]:
c = s[0].upper()
return c + s[1:]
print(f"{up(input)}\t{up(output)}")
def initialupper(s):
return s[0].upper() + s[1:]
def emit(src, dst):
entries.append((src, dst))
print(f"{src}\t{dst}")
usrc = initialupper(src)
udst = initialupper(dst)
if (usrc, udst) != (src, dst):
print(f"{usrc}\t{udst}")
def vowel1():
for v in "aeiouy":
@@ -97,14 +98,6 @@ def mod1tone():
for t in tone:
emit(i+t, addtone(o, t))
modvowel = [
("aw", "ă"),
("aa", "â"),
("ee", "ê"),
("oo", "ô"),
("ow", "ơ"),
("uw", "ư"),
]
def tone1mod():
# a+s+w -> ắ
for i, o in modvowel:
@@ -178,4 +171,5 @@ tone1mod()
tone2mod()
escape()
final()
emit("\\", "\\")
onsets()

File diff suppressed because it is too large Load Diff

59
srv.c
View File

@@ -2,30 +2,53 @@
#include "fn.h"
static char adir[40];
static Channel *clientc;
static void
sendkey(int fd, u32int ks, u32int mod, int want)
int
srvreadkey(int fd, Keyreq *kr)
{
Keyreq kr;
uchar req[Ipcreqsz];
u32int ks, mod;
int want;
kr.fd = fd;
kr.ks = ks;
kr.mod = mod;
kr.want = want;
chansend(keyc, &kr);
if(ipcreadn(fd, req, sizeof req) < 0)
return -1;
ipcunpackreq(req, &want, &mod, &ks);
kr->ks = ks;
kr->mod = mod;
kr->want = want;
return 0;
}
static void
clientthread(void *arg)
{
Channel *reply;
int fd;
uchar req[4];
Keyreq kr;
Keyres res;
uchar out[Ipcmaxresp];
char commit[Maxutf], preedit[Maxutf];
int n, ncommit, npreedit;
uchar token;
fd = (int)(uintptr)arg;
threadsetname("client %d", fd);
while(read(fd, req, 4) == 4)
sendkey(fd, req[2] | (req[3] << 8), req[1], req[0]);
reply = chancreate(sizeof(Keyres), 0);
kr.reply = reply;
while(srvreadkey(fd, &kr) == 0){
chansend(keyc, &kr);
chanrecv(reply, &res);
ncommit = stoutf(&res.commit, commit, sizeof commit);
npreedit = stoutf(&res.preedit, preedit, sizeof preedit);
n = ipcpackresp(out, sizeof out, res.eaten,
commit, ncommit, preedit, npreedit, kr.want);
if(n < 0 || ipcsend(fd, out, n) < 0)
break;
}
chanfree(reply);
close(fd);
chanrecv(clientc, &token);
}
static void
@@ -33,7 +56,7 @@ srvinit(void)
{
char addr[64];
snprint(addr, sizeof(addr), "unix!/tmp/strans.%d", getuid());
snprint(addr, sizeof(addr), "unix!" IPCPATH, getuid());
remove(addr + 5);
if(announce(addr, adir) < 0)
die("announce: %r");
@@ -44,13 +67,23 @@ srvthread(void*)
{
char ldir[40];
int fd;
uchar token;
threadsetname("srv");
srvinit();
token = 0;
clientc = chancreate(sizeof token, Maxclients);
for(;;){
fd = listen(adir, ldir);
if(fd < 0)
continue;
proccreate(clientthread, (void*)(uintptr)fd, 8192);
if(channbsend(clientc, &token) <= 0){
close(fd);
continue;
}
if(proccreate(clientthread, (void*)(uintptr)fd, 8192) < 0){
chanrecv(clientc, &token);
close(fd);
}
}
}

24
str.c
View File

@@ -1,11 +1,6 @@
#include "dat.h"
#include "fn.h"
enum
{
Maxrunes = nelem(((Str*)0)->r),
};
void
sinit(Str *s, char *src, int n)
{
@@ -13,7 +8,11 @@ sinit(Str *s, char *src, int n)
s->n = 0;
while(n > 0 && s->n < Maxrunes){
if(!fullrune(src, n))
break;
len = chartorune(&s->r[s->n], src);
if(len > n)
break;
s->n++;
src += len;
n -= len;
@@ -44,9 +43,10 @@ spopr(Str *s)
void
sappend(Str *dst, Str *src)
{
int i;
int i, n;
for(i = 0; i < src->n && dst->n < Maxrunes; i++)
n = src->n;
for(i = 0; i < n && dst->n < Maxrunes; i++)
dst->r[dst->n++] = src->r[i];
}
@@ -66,11 +66,17 @@ scmp(Str *a, Str *b)
int
stoutf(Str *s, char *buf, int sz)
{
char tmp[UTFmax];
int i, n, len;
if(sz <= 0)
return 0;
n = 0;
for(i = 0; i < s->n && n < sz - UTFmax; i++){
len = runetochar(buf + n, &s->r[i]);
for(i = 0; i < s->n; i++){
len = runetochar(tmp, &s->r[i]);
if(len > sz - n - 1)
break;
memmove(buf + n, tmp, len);
n += len;
}
buf[n] = '\0';

View File

@@ -17,7 +17,7 @@ Lang langs[] = {
{LangJPK, "kata", "kanji", transmap, backmap, dictqmap, nil, nil},
{LangKO, "hangul", nil, transko, backko, dictqmap, nil, nil},
{LangEMOJI, "emoji", "emoji", transmap, backmap, dictqmap, nil, nil},
// {LangVI, "telex", nil, transvi, backmap, dictqmap, nil, nil},
{LangVI, "telex", nil, transvi, backvi, dictqmap, nil, nil},
};
int nlang = nelem(langs);
@@ -65,6 +65,7 @@ static void
reset(void)
{
sclear(&im.pre);
sclear(&im.raw);
clearkouho();
show();
}
@@ -85,11 +86,10 @@ dictqmap(Im *im)
{
Str dict;
if(!mapget(im->l->map, &im->pre, &dict)){
clearkouho();
show();
clearkouho();
show();
if(!mapget(im->l->map, &im->pre, &dict))
return;
}
dictsend(im, &dict);
}
@@ -115,6 +115,7 @@ commit(Str *com)
else
sappend(com, &im.pre);
sclear(&im.pre);
sclear(&im.raw);
}
static int
@@ -122,8 +123,24 @@ dotrans(Rune c, Str *com)
{
Emit e;
Dictreq req;
Str mapped;
e = im.l->trans(&im, c);
if(im.l->lang == LangVI){
if(e.s.n > 0 || !e.eat)
sclear(&im.raw);
if(e.eat && e.next.n > 0){
if(im.raw.n >= Maxrunes){
if(!mapget(im.l->map, &e.next, &mapped))
mapped = e.next;
sappend(com, &mapped);
reset();
return e.eat;
}
sputr(&im.raw, c);
}
}
clearkouho();
if(e.s.n > 0)
sappend(com, &e.s);
sclear(&im.pre);
@@ -151,7 +168,7 @@ getlang(int lang)
static int
maplookup(Trie *t, Str *key, Str *out)
{
char buf[256], *v;
char buf[Maxutf], *v;
int klen, vlen;
if(key->n == 0)
@@ -290,14 +307,24 @@ init(void)
im.l = getlang(LangEN);
}
static void
imhandlekey(Keyreq *kr)
{
Keyres res;
sclear(&res.commit);
sclear(&res.preedit);
res.eaten = keystroke(kr->ks, kr->mod, &res.commit);
if(kr->want)
res.preedit = im.pre;
chansend(kr->reply, &res);
}
void
imthread(void*)
{
Keyreq kr;
Dictres res;
Str com;
uchar out[256];
int n, len, r;
Alt alts[] = {
{keyc, &kr, CHANRCV, nil},
{dictresc, &res, CHANRCV, nil},
@@ -309,24 +336,7 @@ imthread(void*)
for(;;){
switch(alt(alts)){
case 0:
sclear(&com);
r = keystroke(kr.ks, kr.mod, &com);
out[0] = r;
out[1] = 0;
n = 2;
if(com.n > 0){
len = stoutf(&com, (char*)(out+2), sizeof(out)-2);
out[1] = len;
n += len;
}
if(kr.want){
len = im.pre.n > 0 ?
stoutf(&im.pre, (char*)(out+n+1),
sizeof(out)-n-1) : 0;
out[n] = len;
n += 1 + len;
}
write(kr.fd, out, n);
imhandlekey(&kr);
break;
case 1:
if(scmp(&res.key, &im.pre) == 0){
@@ -341,7 +351,7 @@ imthread(void*)
int
mapget(Trie *t, Str *key, Str *out)
{
char buf[256], *v;
char buf[Maxutf], *v;
int klen, vlen;
if(key->n == 0)
@@ -367,4 +377,3 @@ mapinit(char *dir)
langs[i].map = trieopen(path);
}
}

34
tests/Makefile Normal file
View File

@@ -0,0 +1,34 @@
CC = 9c
LD = 9l
CFLAGS = -std=c99 -Wall -Wextra -O2 -g -I.. -I../cutest
LIBS = -lthread -lbio
PROG = unit_test
TESTSRC = unit_test.c test_util.c str_test.c hash_test.c trie_test.c \
ko_test.c vi_test.c engine_test.c dict_test.c
TESTOBJ = $(TESTSRC:.c=.o)
PARENTSRC = str.c hash.c trie.c dict.c ko.c vi.c
PARENTOBJ = $(PARENTSRC:%.c=unit_%.o)
OBJS = $(TESTOBJ) $(PARENTOBJ)
all: $(PROG)
check test: $(PROG)
./$(PROG) $(TESTARGS)
$(PROG): $(OBJS)
$(LD) -o $@ $(OBJS) $(LIBS)
$(TESTOBJ): test.h ../dat.h ../fn.h ../ipc.h ../cutest/cutest.h
engine_test.o: ../strans.c
unit_%.o: ../%.c ../dat.h ../fn.h ../ipc.h
$(CC) $(CFLAGS) -c -o $@ $<
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
clean:
rm -f $(OBJS) $(PROG)
.PHONY: all check test clean

4
tests/data/hira.map Normal file
View File

@@ -0,0 +1,4 @@
ka か
sa さ
n ん
na な

6
tests/data/trie.map Normal file
View File

@@ -0,0 +1,6 @@
a alpha
ab beta
한 값
duplicate first
duplicate second
tabs one two

88
tests/dict_test.c Normal file
View File

@@ -0,0 +1,88 @@
#include "test.h"
void
dictionary_candidates(struct ct *t)
{
char many[512], item[8];
char *p;
Dictreq req;
Dictres res;
Hmap *saved;
Lang *lang;
Str key;
int i;
lang = getlang(LangJP);
saved = lang->dict;
lang->dict = hmapalloc(1);
key = mkstr("かな");
hmapset(&lang->dict, &key, " 候補1 かな 候補2 ",
strlen(" 候補1 かな 候補2 "));
memset(&req, 0, sizeof req);
req.key = key;
req.pre = mkstr("preedit-one");
req.lang = LangJP;
dictlookup(&req, &res);
if(!CT_EQ_INT(t, 2, res.nkouho))
goto cleanup;
CT_EQ_INT(t, 0, scmp(&req.pre, &res.key));
checkstr(t, "candidate 1", "候補1", &res.kouho[0]);
checkstr(t, "candidate 2", "候補2", &res.kouho[1]);
key = mkstr("key");
p = many;
for(i = 0; i < Maxkouho+1; i++){
snprint(item, sizeof item, "c%02d", i);
if(i > 0)
*p++ = ' ';
memmove(p, item, strlen(item));
p += strlen(item);
}
*p = '\0';
hmapset(&lang->dict, &key, many, strlen(many));
req.key = key;
req.pre = mkstr("preedit-two");
dictlookup(&req, &res);
if(!CT_EQ_INT(t, Maxkouho, res.nkouho))
goto cleanup;
CT_EQ_INT(t, 0, scmp(&req.pre, &res.key));
checkstr(t, "first capped candidate", "c00", &res.kouho[0]);
checkstr(t, "last capped candidate", "c31", &res.kouho[31]);
cleanup:
hmapfree(lang->dict);
lang->dict = saved;
}
void
dictionary_misses_clear_result(struct ct *t)
{
static const struct {
char *name;
char *key;
char *pre;
} cases[] = {
{ "empty key", "", "empty-preedit" },
{ "missing key", "missing", "missing-preedit" },
};
Dictreq req;
Dictres res;
Hmap *saved;
Lang *lang;
int i;
lang = getlang(LangJP);
saved = lang->dict;
lang->dict = hmapalloc(1);
for(i = 0; i < nelem(cases); i++){
memset(&res, 0xa5, sizeof res);
req.key = mkstr(cases[i].key);
req.pre = mkstr(cases[i].pre);
req.lang = LangJP;
dictlookup(&req, &res);
if(res.nkouho != 0)
CT_ERRORF(t, "%s: want 0 candidates, got %d",
cases[i].name, res.nkouho);
checkstr(t, cases[i].name, cases[i].pre, &res.key);
}
hmapfree(lang->dict);
lang->dict = saved;
}

257
tests/engine_test.c Normal file
View File

@@ -0,0 +1,257 @@
#define STRANS_TEST_ENGINE
#include "../strans.c"
#include "test.h"
void
vietnamese_state_lifetime(struct ct *t)
{
Str com;
init();
im.l = &testvi;
im.pre = mkstr("as");
im.raw = mkstr("as");
sclear(&com);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "committed Telex", "á", &com);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
init();
im.l = &testvi;
im.pre = mkstr("as");
im.raw = mkstr("as");
sclear(&com);
CT_CHECK(t, keystroke(Kesc, 0, &com));
CT_EQ_INT(t, 0, com.n);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
init();
im.l = &testvi;
im.pre = mkstr("as");
im.raw = mkstr("as");
sclear(&com);
CT_CHECK(t, dotrans('q', &com));
checkstr(t, "completed Telex", "á", &com);
checkstr(t, "next preedit", "q", &im.pre);
checkstr(t, "next raw input", "q", &im.raw);
}
void
engine_clears_candidates(struct ct *t)
{
Str com;
init();
im.l = getlang(LangKO);
im.pre = mkstr("");
im.nkouho = 2;
im.sel = 1;
sclear(&com);
dotrans('k', &com);
CT_EQ_INT(t, 0, im.nkouho);
CT_EQ_INT(t, -1, im.sel);
}
void
engine_backspace_clears_candidates(struct ct *t)
{
Lang *lang;
Str com, shown;
lang = getlang(LangJP);
init();
im.l = lang;
sclear(&com);
CT_CHECK(t, keystroke('n', 0, &com));
CT_CHECK(t, keystroke('a', 0, &com));
shown = shownpre(&im);
checkstr(t, "before backspace", "", &shown);
im.kouho[0] = mkstr("stale-candidate");
im.nkouho = 1;
im.sel = 0;
CT_CHECK(t, keystroke(Kback, 0, &com));
checkstr(t, "backspace commit", "", &com);
checkstr(t, "raw preedit after backspace", "n", &im.pre);
shown = shownpre(&im);
checkstr(t, "shown preedit after backspace", "", &shown);
CT_EQ_INT(t, 0, im.nkouho);
CT_EQ_INT(t, -1, im.sel);
}
void
engine_selects_visible_candidate(struct ct *t)
{
static const struct { int n, sel; Rune key; char *want; } cases[] = {
{ Maxdisp, -1, '9', "c9" },
{ Maxdisp+3, Maxdisp+1, '1', "c3" },
};
char name[8];
Str com;
int i, j;
for(i = 0; i < nelem(cases); i++){
init();
for(j = 0; j < cases[i].n; j++){
snprint(name, sizeof name, "c%d", j+1);
im.kouho[j] = mkstr(name);
}
im.nkouho = cases[i].n;
im.sel = cases[i].sel;
sclear(&com);
CT_CHECK(t, keystroke(cases[i].key, 0, &com));
checkstr(t, cases[i].want, cases[i].want, &com);
CT_EQ_INT(t, 0, im.nkouho);
}
}
void
engine_commit_contract(struct ct *t)
{
Str com;
init();
im.l = getlang(LangJP);
im.pre = mkstr("ka");
im.nkouho = 1;
im.sel = -1;
sclear(&com);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "return commit", "", &com);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
CT_EQ_INT(t, 0, im.nkouho);
CT_EQ_INT(t, -1, im.sel);
init();
im.l = getlang(LangJP);
im.pre = mkstr("ka");
im.kouho[0] = mkstr("c1");
im.kouho[1] = mkstr("c2");
im.nkouho = 2;
im.sel = 1;
sclear(&com);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "selected candidate", "c2", &com);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.nkouho);
CT_EQ_INT(t, -1, im.sel);
init();
im.l = getlang(LangJP);
im.pre = mkstr("ka");
im.nkouho = 1;
im.sel = -1;
sclear(&com);
CT_CHECK(t, !keystroke('q', 0, &com));
checkstr(t, "uneaten-key commit", "", &com);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
CT_EQ_INT(t, 0, im.nkouho);
CT_EQ_INT(t, -1, im.sel);
init();
im.l = getlang(LangJP);
im.pre = mkstr("ka");
sclear(&com);
CT_CHECK(t, keystroke(0xf008, 0, &com));
CT_EQ_INT(t, 2, com.n);
CT_EQ_UINT(t, 0x304b, com.r[0]);
CT_EQ_UINT(t, 0xf008, com.r[1]);
CT_EQ_INT(t, 0, im.pre.n);
}
void
engine_telex_history_bound(struct ct *t)
{
Str com;
Rune tone;
int i;
init();
im.l = &testvi;
sclear(&com);
CT_CHECK(t, keystroke('a', 0, &com));
for(i = 0; i < Maxrunes; i++){
tone = i % 2 == 0 ? 's' : 'f';
if(!CT_CHECK(t, keystroke(tone, 0, &com)))
break;
}
checkstr(t, "bounded Telex commit", "à", &com);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
}
void
engine_language_switch_state(struct ct *t)
{
static const struct {
char *name;
Rune key;
int mod;
int lang;
char *pre;
char *raw;
char *commit;
int stale;
} steps[] = {
{ "select Vietnamese", 'v', Mctrl, LangVI, "", "", "", 0 },
{ "Vietnamese a", 'a', 0, LangVI, "a", "a", "", 0 },
{ "Vietnamese tone", 's', 0, LangVI, "as", "as", "", 0 },
{ "switch to Korean", 's', Mctrl, LangKO, "", "", "", 1 },
{ "Korean initial", 'r', 0, LangKO, "", "", "", 0 },
{ "Korean syllable", 'k', 0, LangKO, "", "", "", 0 },
{ "switch to Vietnamese", 'v', Mctrl, LangVI, "", "", "", 0 },
{ "Vietnamese a again", 'a', 0, LangVI, "a", "a", "", 0 },
{ "Vietnamese tone again", 's', 0, LangVI, "as", "as", "", 0 },
{ "commit Vietnamese", Kret, 0, LangVI, "", "", "á", 0 },
};
char where[80];
Lang *vi;
Trie *saved;
Str com;
int eaten, i;
vi = getlang(LangVI);
if(!CT_CHECK(t, vi != nil))
return;
saved = vi->map;
vi->map = testvi.map;
init();
for(i = 0; i < nelem(steps); i++){
if(steps[i].stale){
im.kouho[0] = mkstr("stale");
im.nkouho = 1;
im.sel = 0;
}
sclear(&com);
eaten = keystroke(steps[i].key, steps[i].mod, &com);
if(eaten != 1){
CT_ERRORF(t, "%s: want eaten 1, got %d",
steps[i].name, eaten);
break;
}
if(im.l == nil || im.l->lang != steps[i].lang){
CT_ERRORF(t, "%s: want language %d, got %d",
steps[i].name, steps[i].lang,
im.l == nil ? -1 : im.l->lang);
break;
}
snprint(where, sizeof where, "%s preedit", steps[i].name);
if(!checkstr(t, where, steps[i].pre, &im.pre))
break;
snprint(where, sizeof where, "%s raw", steps[i].name);
if(!checkstr(t, where, steps[i].raw, &im.raw))
break;
snprint(where, sizeof where, "%s commit", steps[i].name);
if(!checkstr(t, where, steps[i].commit, &com))
break;
if(im.nkouho != 0 || im.sel != -1){
CT_ERRORF(t, "%s: want candidates 0 and selection -1, got %d and %d",
steps[i].name, im.nkouho, im.sel);
break;
}
}
vi->map = saved;
}

78
tests/hash_test.c Normal file
View File

@@ -0,0 +1,78 @@
#include "test.h"
void
hmap_set_replace_and_grow(struct ct *t)
{
char keybuf[16], valbuf[16], source[] = "copied";
Hmap *h;
Hnode *n;
Str key;
int i;
h = hmapalloc(1);
for(i = 0; i < 4; i++){
snprint(keybuf, sizeof keybuf, "key%d", i);
snprint(valbuf, sizeof valbuf, "value%d", i);
key = mkstr(keybuf);
hmapset(&h, &key, valbuf, strlen(valbuf));
}
for(i = 0; i < 4; i++){
snprint(keybuf, sizeof keybuf, "key%d", i);
snprint(valbuf, sizeof valbuf, "value%d", i);
key = mkstr(keybuf);
n = hmapget(h, &key);
if(n == nil || strcmp(n->val, valbuf) != 0)
CT_ERRORF(t, "%s: collision chain lost value", keybuf);
}
key = mkstr("key1");
hmapset(&h, &key, source, strlen(source));
source[0] = 'X';
n = hmapget(h, &key);
if(!CT_CHECK(t, n != nil))
goto cleanup;
CT_EQ_STR(t, "copied", n->val);
key = mkstr("key2");
n = hmapget(h, &key);
if(!CT_CHECK(t, n != nil))
goto cleanup;
CT_EQ_STR(t, "value2", n->val);
key = mkstr("key1");
hmapset(&h, &key, nil, 0);
n = hmapget(h, &key);
if(!CT_CHECK(t, n != nil))
goto cleanup;
CT_EQ_INT(t, 0, n->vlen);
CT_EQ_PTR(t, nil, n->val);
key = mkstr("missing");
CT_EQ_PTR(t, nil, hmapget(h, &key));
cleanup:
hmapfree(h);
}
void
hmap_long_utf8_keys(struct ct *t)
{
Hmap *h;
Hnode *n;
Str a, b;
int i;
for(i = 0; i < Maxrunes-1; i++)
a.r[i] = b.r[i] = 0x1f600;
a.r[Maxrunes-1] = 0x1f601;
b.r[Maxrunes-1] = 0x1f602;
a.n = b.n = Maxrunes;
h = hmapalloc(1);
hmapset(&h, &a, "first", 5);
hmapset(&h, &b, "second", 6);
n = hmapget(h, &a);
if(!CT_CHECK(t, n != nil))
goto cleanup;
CT_EQ_STR(t, "first", n != nil ? n->val : nil);
n = hmapget(h, &b);
if(!CT_CHECK(t, n != nil))
goto cleanup;
CT_EQ_STR(t, "second", n != nil ? n->val : nil);
cleanup:
hmapfree(h);
}

60
tests/ko_test.c Normal file
View File

@@ -0,0 +1,60 @@
#include "test.h"
void
korean_sequences(struct ct *t)
{
static const struct { char *keys, *want; } cases[] = {
{ "r", "" },
{ "rk", "" },
{ "rkr", "" },
{ "rkrk", "가가" },
{ "rkrtk", "각사" },
{ "rhk", "" },
{ "rr", "ㄱㄱ" },
{ "Rk", "" },
{ "rk1", "가1" },
};
Emit e;
Im state;
Str out;
Rune key;
char *p;
int i;
for(i = 0; i < nelem(cases); i++){
memset(&state, 0, sizeof state);
state.l = getlang(LangKO);
sclear(&out);
for(p = cases[i].keys; *p != '\0'; p++){
key = (uchar)*p;
e = transko(&state, key);
sappend(&out, &e.s);
state.pre = e.next;
if(!e.eat)
sputr(&out, key);
}
sappend(&out, &state.pre);
checkstr(t, cases[i].keys, cases[i].want, &out);
}
}
void
korean_backspace(struct ct *t)
{
static const struct { char *before, *after; } cases[] = {
{ "", "" },
{ "", "" },
{ "", "" },
{ "", "" },
{ "", "" },
};
Im state;
int i;
memset(&state, 0, sizeof state);
for(i = 0; i < nelem(cases); i++){
state.pre = mkstr(cases[i].before);
backko(&state);
checkstr(t, cases[i].before, cases[i].after, &state.pre);
}
}

84
tests/str_test.c Normal file
View File

@@ -0,0 +1,84 @@
#include "test.h"
void
str_init_utf8(struct ct *t)
{
char full[Maxrunes+2];
char incomplete[] = { (char)0xea, (char)0xb0 };
Str s;
s = mkstr("A한😀");
CT_EQ_INT(t, 3, s.n);
checkstr(t, "round trip", "A한😀", &s);
sinit(&s, incomplete, sizeof incomplete);
CT_EQ_INT(t, 0, s.n);
memset(full, 'a', sizeof full);
full[sizeof full-1] = '\0';
sinit(&s, full, strlen(full));
CT_EQ_INT(t, Maxrunes, s.n);
}
void
str_edit_and_alias(struct ct *t)
{
Str s;
int i;
s = mkstr("ab");
sappend(&s, &s);
checkstr(t, "self append", "abab", &s);
for(i = 0; i < 40; i++)
s.r[i] = 'a' + i % 26;
s.n = 40;
sappend(&s, &s);
CT_EQ_INT(t, Maxrunes, s.n);
for(i = 0; i < 24; i++)
if(s.r[40+i] != s.r[i])
CT_ERRORF(t, "capped self append differs at rune %d", i);
}
void
str_utf8_capacity(struct ct *t)
{
static const struct {
int size;
int n;
char *want;
} cases[] = {
{ 0, 0, nil },
{ 1, 0, "" },
{ 2, 1, "a" },
{ 3, 1, "a" },
{ 4, 1, "a" },
{ 5, 4, "a한" },
{ 6, 5, "a한b" },
};
char buf[16];
Str s;
int i, n;
s = mkstr("a한b");
for(i = 0; i < nelem(cases); i++){
memset(buf, 'Z', sizeof buf);
n = stoutf(&s, buf, cases[i].size);
if(n != cases[i].n)
CT_ERRORF(t, "size %d: want length %d, got %d",
cases[i].size, cases[i].n, n);
if(cases[i].size == 0){
CT_EQ_INT(t, 'Z', buf[0]);
continue;
}
if(strcmp(cases[i].want, buf) != 0)
CT_ERRORF(t, "size %d: want \"%s\", got \"%s\"",
cases[i].size, cases[i].want, buf);
CT_EQ_INT(t, 'Z', buf[cases[i].size]);
}
s = mkstr("😀");
memset(buf, 'Z', sizeof buf);
CT_EQ_INT(t, 0, stoutf(&s, buf, 4));
CT_EQ_STR(t, "", buf);
CT_EQ_INT(t, 4, stoutf(&s, buf, 5));
CT_EQ_STR(t, "😀", buf);
CT_EQ_INT(t, 'Z', buf[5]);
}

39
tests/test.h Normal file
View File

@@ -0,0 +1,39 @@
#ifndef STRANS_TEST_H
#define STRANS_TEST_H
#include "../cutest/cutest.h"
#ifndef STRANS_TEST_ENGINE
#include "../dat.h"
#include "../fn.h"
#endif
extern Lang testvi;
Str mkstr(char*);
int checkstr(struct ct*, char*, char*, Str*);
Str shownpre(Im*);
void str_init_utf8(struct ct*);
void str_edit_and_alias(struct ct*);
void str_utf8_capacity(struct ct*);
void hmap_set_replace_and_grow(struct ct*);
void hmap_long_utf8_keys(struct ct*);
void trie_exact_prefix_and_duplicate(struct ct*);
void production_maps_load(struct ct*);
void transmap_states(struct ct*);
void korean_sequences(struct ct*);
void korean_backspace(struct ct*);
void vietnamese_transitions(struct ct*);
void vietnamese_backspace(struct ct*);
void vietnamese_state_lifetime(struct ct*);
void engine_clears_candidates(struct ct*);
void engine_backspace_clears_candidates(struct ct*);
void engine_selects_visible_candidate(struct ct*);
void engine_commit_contract(struct ct*);
void engine_language_switch_state(struct ct*);
void engine_telex_history_bound(struct ct*);
void dictionary_candidates(struct ct*);
void dictionary_misses_clear_result(struct ct*);
#endif

32
tests/test_util.c Normal file
View File

@@ -0,0 +1,32 @@
#include "test.h"
Str
mkstr(char *s)
{
Str r;
sinit(&r, s, strlen(s));
return r;
}
int
checkstr(struct ct *t, char *where, char *want, Str *got)
{
char buf[Maxutf];
stoutf(got, buf, sizeof buf);
if(strcmp(want, buf) == 0)
return 1;
return CT_ERRORF(t, "%s: want \"%s\", got \"%s\"",
where, want, buf);
}
Str
shownpre(Im *state)
{
Str shown;
if(state->l->map != nil && mapget(state->l->map, &state->pre, &shown))
return shown;
return state->pre;
}

93
tests/trie_test.c Normal file
View File

@@ -0,0 +1,93 @@
#include "test.h"
void
trie_exact_prefix_and_duplicate(struct ct *t)
{
char *v, *sentinel;
Trie *trie;
int n;
trie = trieopen("data/trie.map");
v = trieget(trie, "a", 1, &n);
if(!CT_CHECK(t, v != nil))
goto cleanup;
CT_EQ_INT(t, 5, n);
CT_EQ_MEM(t, "alpha", v, n);
sentinel = "unchanged";
v = sentinel;
n = 77;
CT_CHECK(t, trielookup(trie, "dupli", 5, &v, &n));
CT_EQ_PTR(t, sentinel, v);
CT_EQ_INT(t, 77, n);
v = trieget(trie, "duplicate", 9, &n);
if(!CT_CHECK(t, v != nil))
goto cleanup;
CT_EQ_INT(t, 6, n);
CT_EQ_MEM(t, "second", v, n);
v = trieget(trie, "tabs", 4, &n);
if(!CT_CHECK(t, v != nil))
goto cleanup;
CT_EQ_INT(t, 7, n);
CT_EQ_MEM(t, "one\ttwo", v, n);
CT_EQ_PTR(t, nil, trieget(trie, "missing", 7, &n));
cleanup:
trieclose(trie);
}
void
production_maps_load(struct ct *t)
{
char path[256];
Trie *map;
int i, loaded, registered;
loaded = registered = 0;
for(i = 0; i < nlang; i++){
if(langs[i].mapname == nil)
continue;
registered++;
snprint(path, sizeof path, "../map/%s.map", langs[i].mapname);
map = trieopen(path);
if(!CT_CHECK(t, map != nil))
continue;
trieclose(map);
loaded++;
}
CT_CHECK(t, registered > 0);
CT_EQ_INT(t, registered, loaded);
}
void
transmap_states(struct ct *t)
{
static const struct {
char *pre;
Rune key;
int eat;
char *emit;
char *next;
char *mapped;
} cases[] = {
{ "", 'k', 1, "", "k", "" },
{ "k", 'a', 1, "", "ka", "" },
{ "ka", 's', 1, "", "s", "" },
{ "ka", 'q', 0, "", "", "" },
{ "k", 'q', 0, "k", "", "" },
};
Emit e;
Im state;
int i;
memset(&state, 0, sizeof state);
state.l = getlang(LangJP);
for(i = 0; i < nelem(cases); i++){
state.pre = mkstr(cases[i].pre);
e = transmap(&state, cases[i].key);
if(e.eat != cases[i].eat)
CT_ERRORF(t, "case %d: want eat %d, got %d",
i, cases[i].eat, e.eat);
checkstr(t, "emit", cases[i].emit, &e.s);
checkstr(t, "next", cases[i].next, &e.next);
checkstr(t, "mapped", cases[i].mapped, &e.dict);
}
}

109
tests/unit_test.c Normal file
View File

@@ -0,0 +1,109 @@
#define CT_IMPLEMENTATION
#include "test.h"
#include <stdarg.h>
Channel *drawc;
Channel *keyc;
Channel *dictreqc;
Channel *dictresc;
Lang testvi;
void
die(char *fmt, ...)
{
va_list ap;
fprint(2, "unit_test: ");
va_start(ap, fmt);
vfprint(2, fmt, ap);
va_end(ap);
fprint(2, "\n");
threadexitsall("die");
}
void*
emalloc(ulong n)
{
void *p;
p = malloc(n);
if(p == nil)
die("out of memory");
memset(p, 0, n);
return p;
}
void*
erealloc(void *p, ulong n)
{
p = realloc(p, n);
if(p == nil)
die("out of memory");
return p;
}
static void
testmapinit(void)
{
Lang *jp;
jp = getlang(LangJP);
if(jp == nil)
die("test language is not registered");
jp->map = trieopen("data/hira.map");
memset(&testvi, 0, sizeof testvi);
testvi.lang = LangVI;
testvi.mapname = "telex";
testvi.trans = transvi;
testvi.back = backvi;
testvi.map = trieopen("../map/telex.map");
}
static const struct ct_test tests[] = {
{ "str/init-utf8", str_init_utf8 },
{ "str/edit-and-alias", str_edit_and_alias },
{ "str/utf8-capacity", str_utf8_capacity },
{ "hmap/set-replace-grow", hmap_set_replace_and_grow },
{ "hmap/long-utf8-keys", hmap_long_utf8_keys },
{ "trie/exact-prefix-duplicate", trie_exact_prefix_and_duplicate },
{ "map/production-lifecycle", production_maps_load },
{ "transmap/states", transmap_states },
{ "hangul/sequences", korean_sequences },
{ "hangul/backspace", korean_backspace },
{ "telex/transitions", vietnamese_transitions },
{ "telex/backspace", vietnamese_backspace },
{ "telex/state-lifetime", vietnamese_state_lifetime },
{ "engine/clears-candidates", engine_clears_candidates },
{ "engine/backspace-clears-candidates", engine_backspace_clears_candidates },
{ "engine/selects-visible-candidate", engine_selects_visible_candidate },
{ "engine/commit-contract", engine_commit_contract },
{ "engine/language-switch-state", engine_language_switch_state },
{ "engine/telex-history-bound", engine_telex_history_bound },
{ "dict/candidates", dictionary_candidates },
{ "dict/misses-clear-result", dictionary_misses_clear_result },
};
void
threadmain(int argc, char **argv)
{
int i, status;
drawc = chancreate(sizeof(Drawcmd), 4);
keyc = chancreate(sizeof(Keyreq), 0);
dictreqc = chancreate(sizeof(Dictreq), 64);
dictresc = chancreate(sizeof(Dictres), 0);
testmapinit();
status = CT_RUN_ARGS(tests, argc, argv);
for(i = 0; i < nlang; i++){
trieclose(langs[i].map);
langs[i].map = nil;
}
trieclose(testvi.map);
testvi.map = nil;
chanfree(drawc);
chanfree(keyc);
chanfree(dictreqc);
chanfree(dictresc);
threadexitsall(status == 0 ? nil : "tests failed");
}

84
tests/vi_test.c Normal file
View File

@@ -0,0 +1,84 @@
#include "test.h"
static void
typevi(struct ct *t, char *keys, char *want)
{
Emit e;
Im state;
Str out, shown;
char *p;
memset(&state, 0, sizeof state);
state.l = &testvi;
sclear(&out);
for(p = keys; *p != '\0'; p++){
e = transvi(&state, (uchar)*p);
sappend(&out, &e.s);
state.pre = e.next;
if(!e.eat)
sputr(&out, (uchar)*p);
}
shown = shownpre(&state);
sappend(&out, &shown);
checkstr(t, keys, want, &out);
}
void
vietnamese_transitions(struct ct *t)
{
static const struct { char *keys, *want; } cases[] = {
{ "as", "á" },
{ "As", "Á" },
{ "ans", "án" },
{ "Ans", "Án" },
{ "aws", "" },
{ "asw", "" },
{ "aww", "aw" },
{ "ddd", "dd" },
{ "ddas", "đá" },
{ "dds", "đs" },
{ "quas", "quá" },
{ "quaf", "quà" },
{ "quar", "quả" },
{ "quax", "quã" },
{ "quaj", "quạ" },
{ "Quas", "Quá" },
{ "gias", "giá" },
{ "Gias", "Giá" },
{ "gif", "" },
{ "\\s", "s" },
{ "a\\s", "as" },
};
int i;
for(i = 0; i < nelem(cases); i++)
typevi(t, cases[i].keys, cases[i].want);
}
void
vietnamese_backspace(struct ct *t)
{
static const struct {
char *pre, *raw, *rawafter, *after;
} cases[] = {
{ "as", "as", "a", "a" },
{ "quá", "quas", "qua", "qua" },
{ "à", "asf", "as", "á" },
{ "a", "asz", "as", "á" },
{ "quà", "quasf", "quas", "quá" },
};
Im state;
Str shown;
int i;
for(i = 0; i < nelem(cases); i++){
memset(&state, 0, sizeof state);
state.l = &testvi;
state.pre = mkstr(cases[i].pre);
state.raw = mkstr(cases[i].raw);
backvi(&state);
checkstr(t, cases[i].raw, cases[i].rawafter, &state.raw);
shown = shownpre(&state);
checkstr(t, cases[i].raw, cases[i].after, &shown);
}
}

22
trie.c
View File

@@ -53,7 +53,10 @@ insert(Trie *t, char *key, int klen, char *val, int vlen)
ci = add(t, ni, key[i]);
ni = ci;
}
t->nodes[ni].val = val;
free(t->nodes[ni].val);
t->nodes[ni].val = emalloc(vlen + 1);
memmove(t->nodes[ni].val, val, vlen);
t->nodes[ni].val[vlen] = '\0';
t->nodes[ni].vlen = vlen;
}
@@ -74,6 +77,9 @@ trieopen(char *path)
t->n = 0;
t->root = newnode(t);
while((line = Brdstr(b, '\n', 1)) != nil){
vlen = strlen(line);
if(vlen > 0 && line[vlen-1] == '\r')
line[--vlen] = '\0';
if(line[0] == '\0'){
free(line);
continue;
@@ -87,11 +93,25 @@ trieopen(char *path)
val = tab + 1;
vlen = strlen(val);
insert(t, key, klen, val, vlen);
free(line);
}
Bterm(b);
return t;
}
void
trieclose(Trie *t)
{
int i;
if(t == nil)
return;
for(i = 0; i < t->n; i++)
free(t->nodes[i].val);
free(t->nodes);
free(t);
}
char*
trieget(Trie *t, char *key, int klen, int *vlen)
{

27
vi.c
View File

@@ -117,13 +117,16 @@ onsetglide(Str *m)
Emit
transvi(Im *im, Rune c)
{
Emit e;
Emit e, mappedkey;
Str mapped, pre;
int i, tidx, vi, last, penult, glide;
Rune v, b1, b2;
if(!istone(c) && c != 'z')
return transmap(im, c);
mappedkey = transmap(im, c);
if(mappedkey.eat)
return mappedkey;
memset(&e, 0, sizeof e);
if(im->pre.n == 0)
@@ -182,3 +185,25 @@ transvi(Im *im, Rune c)
e.next = mapped;
return e;
}
void
backvi(Im *im)
{
Emit e;
Str raw;
int i;
if(im->raw.n == 0){
spopr(&im->pre);
return;
}
raw = im->raw;
spopr(&raw);
sclear(&im->pre);
for(i = 0; i < raw.n; i++){
e = transvi(im, raw.r[i]);
sclear(&im->pre);
sappend(&im->pre, &e.next);
}
im->raw = raw;
}

View File

@@ -25,55 +25,40 @@ static struct xkb_state *xkbst;
static int active;
static int pending;
static u32int imserial;
static Channel *replyc;
static void
sendkey(u32int ks, u32int mod, char *com, int csz, char *pre, int psz,
int *eaten)
sendkey(u32int ks, u32int mod, Keyres *res)
{
Keyreq kr;
int p[2];
uchar hdr[2];
uchar pl;
int n;
*eaten = 0;
com[0] = '\0';
pre[0] = '\0';
if(pipe(p) < 0)
return;
kr.fd = p[1];
kr.ks = ks;
kr.mod = mod;
kr.want = 1;
kr.reply = replyc;
chansend(keyc, &kr);
if(read(p[0], hdr, 2) != 2)
goto out;
*eaten = hdr[0];
n = hdr[1];
if(n > 0 && n < csz){
if(read(p[0], com, n) != n)
goto out;
com[n] = '\0';
}
if(read(p[0], &pl, 1) != 1)
goto out;
if(pl > 0 && pl < psz){
if(read(p[0], pre, pl) != pl)
goto out;
pre[pl] = '\0';
}
out:
close(p[0]);
close(p[1]);
chanrecv(replyc, res);
}
static void
sendreset(void)
{
char com[64], pre[256];
int eaten;
Keyres res;
sendkey(Kesc, 0, com, sizeof(com), pre, sizeof(pre), &eaten);
sendkey(Kesc, 0, &res);
}
static u32int
kget(xkb_keysym_t ks)
{
u32int c;
c = xkb_keysym_to_utf32(ks);
if(c >= ' ' && c != 0x7f)
return c;
if(ks >= 0xff00 && ks <= 0xffff)
return Kspec + (ks - 0xff00);
return c;
}
static u32int
@@ -104,29 +89,32 @@ kpress(uint32_t time, uint32_t keycode, uint32_t state)
{
xkb_keysym_t ks;
u32int k, mod;
char com[64], pre[256];
int eaten, plen;
Keyres res;
char commit[Maxutf], preedit[Maxutf];
int plen;
if(state != WL_KEYBOARD_KEY_STATE_PRESSED || xkbst == nil){
zwp_virtual_keyboard_v1_key(vk, time, keycode, state);
return;
}
ks = xkb_state_key_get_one_sym(xkbst, keycode + 8);
k = ks >= 0xff00 ? Kspec + (ks - 0xff00) : xkb_keysym_to_utf32(ks);
k = kget(ks);
if(k == 0){
zwp_virtual_keyboard_v1_key(vk, time, keycode, state);
return;
}
mod = mget();
sendkey(k, mod, com, sizeof(com), pre, sizeof(pre), &eaten);
if(com[0] != '\0'){
zwp_input_method_v2_commit_string(im, com);
sendkey(k, mod, &res);
stoutf(&res.commit, commit, sizeof commit);
stoutf(&res.preedit, preedit, sizeof preedit);
if(commit[0] != '\0'){
zwp_input_method_v2_commit_string(im, commit);
zwp_input_method_v2_commit(im, imserial);
}
plen = strlen(pre);
zwp_input_method_v2_set_preedit_string(im, pre, plen, plen);
plen = strlen(preedit);
zwp_input_method_v2_set_preedit_string(im, preedit, plen, plen);
zwp_input_method_v2_commit(im, imserial);
if(!eaten)
if(!res.eaten)
zwp_virtual_keyboard_v1_key(vk, time, keycode, state);
}
@@ -158,6 +146,7 @@ kg_keymap(void *data, struct zwp_input_method_keyboard_grab_v2 *g,
munmap(s, size);
}
zwp_virtual_keyboard_v1_keymap(vk, format, fd, size);
close(fd);
}
static void
@@ -215,14 +204,6 @@ im_deactivate(void *data, struct zwp_input_method_v2 *m)
(void)data;
(void)m;
pending = 0;
if(active){
active = 0;
sendreset();
if(grab != nil){
zwp_input_method_keyboard_grab_v2_release(grab);
grab = nil;
}
}
}
static void
@@ -266,6 +247,13 @@ im_done(void *data, struct zwp_input_method_v2 *m)
if(grab != nil)
zwp_input_method_keyboard_grab_v2_add_listener(grab,
&grab_listener, nil);
}else if(!pending && active){
active = 0;
sendreset();
if(grab != nil){
zwp_input_method_keyboard_grab_v2_release(grab);
grab = nil;
}
}
}
@@ -292,9 +280,9 @@ reg_global(void *data, struct wl_registry *r, uint32_t name,
const char *iface, uint32_t version)
{
(void)data;
(void)version;
if(strcmp(iface, wl_seat_interface.name) == 0)
seat = wl_registry_bind(r, name, &wl_seat_interface, 5);
seat = wl_registry_bind(r, name, &wl_seat_interface,
version < 5 ? version : 5);
else if(strcmp(iface, zwp_input_method_manager_v2_interface.name) == 0)
immgr = wl_registry_bind(r, name,
&zwp_input_method_manager_v2_interface, 1);
@@ -345,6 +333,9 @@ waylandthread(void *_)
im = zwp_input_method_manager_v2_get_input_method(immgr, seat);
zwp_input_method_v2_add_listener(im, &im_listener, nil);
vk = zwp_virtual_keyboard_manager_v1_create_virtual_keyboard(vkmgr, seat);
replyc = chancreate(sizeof(Keyres), 0);
while(wl_display_dispatch(dpy) != -1)
;
chanfree(replyc);
replyc = nil;
}

13
win.c
View File

@@ -78,17 +78,26 @@ drawstr(u32int *buf, int x, int y, Rune *r, int n, int maxw, int maxh)
}
}
static void
fill(u32int *buf, int n, u32int color)
{
int i;
for(i = 0; i < n; i++)
buf[i] = color;
}
static void
drawkouho(Drawcmd *dc, int first, int n, int w, int h)
{
int sely, y, i;
Str *s;
memset(img, (uchar)Colbg, w * h * sizeof(u32int));
fill(img, w * h, Colbg);
drawstr(img, 0, 0, dc->pre.r, dc->pre.n, w, h);
if(dc->sel >= 0){
sely = Fontsz + dc->sel * Fontsz;
memset(img + sely * w, (uchar)Colsel, Fontsz * w * sizeof(u32int));
fill(img + sely * w, Fontsz * w, Colsel);
}
for(i = 0, y = Fontsz; i < n; i++, y += Fontsz){
s = &dc->kouho[first+i];

View File

@@ -1,5 +1,7 @@
CC = cc
CFLAGS = -Wall -Wextra -O2 -I.. -Ixcb-imdkit/src
XKB_CFLAGS := $(shell pkg-config --cflags xkbcommon)
XKB_LIBS := $(shell pkg-config --libs xkbcommon)
CFLAGS = -Wall -Wextra -O2 -I.. -Ixcb-imdkit/src $(XKB_CFLAGS)
PROG = strans-xim
SRCS = $(wildcard *.c)
@@ -9,8 +11,8 @@ LIBS = xcb-imdkit/libxcb-imdkit.a xcb-util/libxcb-util.a
all: $(PROG)
$(PROG): $(OBJS) $(LIBS)
$(CC) -o $@ $(OBJS) $(LIBS) -lxcb
$(PROG): $(OBJS) $(LIBS) ../ipc.c ../ipc.h
$(CC) $(CFLAGS) -o $@ $(OBJS) ../ipc.c $(LIBS) -lxcb $(XKB_LIBS)
$(OBJS): ../ipc.h

View File

@@ -5,13 +5,14 @@
#include <sys/socket.h>
#include <sys/un.h>
#include <xcb/xcb.h>
#include <xkbcommon/xkbcommon.h>
#include "imdkit.h"
#include "encoding.h"
#include "ipc.h"
static xcb_connection_t *conn;
static xcb_im_t *xim;
static int srvfd;
static int srvfd = -1;
static xcb_keysym_t *kmap;
static uint8_t minkc, maxkc;
static uint8_t symsper;
@@ -105,50 +106,74 @@ commit(xcb_im_input_context_t *ic, char *s, int len)
}
static void
srvclose(void)
{
if(srvfd < 0)
return;
close(srvfd);
srvfd = -1;
}
static int
srvconnect(void)
{
struct sockaddr_un addr;
int fd;
srvfd = socket(AF_UNIX, SOCK_STREAM, 0);
if(srvfd < 0)
die("socket failed");
if(srvfd >= 0)
return 0;
fd = socket(AF_UNIX, SOCK_STREAM, 0);
if(fd < 0)
return -1;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
snprintf(addr.sun_path, sizeof(addr.sun_path), "/tmp/strans.%d", getuid());
if(connect(srvfd, (struct sockaddr*)&addr, sizeof(addr)) < 0)
die("can't connect to strans");
snprintf(addr.sun_path, sizeof(addr.sun_path), IPCPATH, getuid());
if(connect(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0){
close(fd);
return -1;
}
srvfd = fd;
return 0;
}
static int
readresp(xcb_im_input_context_t *ic)
{
unsigned char buf[64];
int n;
char buf[Ipcfieldmax+1];
Ipcresp resp;
if(read(srvfd, buf, 2) != 2)
if(ipcreadresp(srvfd, 0, buf, sizeof buf, NULL, 0, &resp) < 0)
return -1;
n = buf[1];
if(n > 0 && (size_t)n < sizeof(buf) && read(srvfd, buf+2, n) == n)
commit(ic, (char*)(buf+2), n);
return buf[0];
if(buf[0] != '\0')
commit(ic, buf, strlen(buf));
return resp.eaten;
}
static void
kpress(xcb_im_input_context_t *ic, xcb_key_press_event_t *ev)
{
unsigned char buf[4];
uint32_t key;
unsigned char buf[Ipcreqsz];
uint32_t key, rune;
int eaten;
key = kget(ev->detail, ev->state);
if(key >= 0xff00)
rune = xkb_keysym_to_utf32(key);
if(rune >= ' ' && rune != 0x7f)
key = rune;
else if(key >= 0xff00 && key <= 0xffff)
key = Kspec + (key - 0xff00);
buf[0] = 0;
buf[1] = ev->state;
buf[2] = key;
buf[3] = key >> 8;
if(write(srvfd, buf, 4) != 4)
die("write failed");
if(readresp(ic) == 0)
else
key = rune;
ipcpackreq(buf, 0, ev->state, key);
eaten = 0;
if(srvconnect() == 0){
if(ipcsend(srvfd, buf, sizeof buf) < 0 ||
(eaten = readresp(ic)) < 0){
srvclose();
eaten = 0;
}
}
if(eaten == 0)
xcb_im_forward_event(xim, ic, ev);
xcb_flush(conn);
}
@@ -156,15 +181,13 @@ kpress(xcb_im_input_context_t *ic, xcb_key_press_event_t *ev)
static void
reset(xcb_im_input_context_t *ic)
{
unsigned char buf[4];
buf[0] = 0;
buf[1] = 0;
buf[2] = Kesc & 0xff;
buf[3] = Kesc >> 8;
if(write(srvfd, buf, 4) != 4)
die("write failed");
readresp(ic);
unsigned char buf[Ipcreqsz];
ipcpackreq(buf, 0, 0, Kesc);
if(srvconnect() < 0)
return;
if(ipcsend(srvfd, buf, sizeof buf) < 0 || readresp(ic) < 0)
srvclose();
}
static void
@@ -173,6 +196,11 @@ callback(xcb_im_t *im, xcb_im_client_t *client, xcb_im_input_context_t *ic,
{
xcb_key_press_event_t *ev;
(void)im;
(void)client;
(void)frame;
(void)user;
switch(hdr->major_opcode){
case XCB_XIM_FORWARD_EVENT:
ev = arg;
@@ -220,7 +248,6 @@ main(void)
{
xcb_generic_event_t *ev;
srvconnect();
ximinit();
for(;;){
ev = xcb_wait_for_event(conn);
@@ -229,5 +256,6 @@ main(void)
xcb_im_filter_event(xim, ev);
free(ev);
}
srvclose();
return 0;
}