ktrans: graphical upgrade and feedback

* scrollbar and mouse selection of candidate
* arrow keys for moving selection cursor after first completion
* user defined dictionaries that are merged on top
* document using the plumber to change languages
* loop candidates when reaching the start/end of the list.
* skk2ktrans was using the wrong from encoding
This commit is contained in:
Jacob Moody 2024-04-02 04:01:56 +00:00
parent 578af37678
commit f4122cfbc9
4 changed files with 210 additions and 66 deletions

View file

@ -1,2 +1,2 @@
#!/bin/rc #!/bin/rc
tcs -sf jis | awk '$1 !~ /;;/ {gsub("(^\/|\/$)", "", $2); gsub(" ", " "); gsub("\/", " ", $2);} {print}' tcs -sf ujis | awk '$1 !~ /;;/ {gsub("(^\/|\/$)", "", $2); gsub(" ", " "); gsub("\/", " ", $2);} {print}'

View file

@ -24,29 +24,47 @@ If a
.I kbdtap .I kbdtap
file is given, it is used for both file is given, it is used for both
input and output instead. input and output instead.
.I Ktrans By default
starts in a passthrough mode, echoing out .I ktrans
the input with no conversions. Control characters starts in passthrough mode, echoing out
are used to give instructions, the following the input with no conversions. The initial
control sequences are used to switch between languages: language is set with the
.B -l
flag. After operation has begun, the language
may be changed by either typing a control sequence
and/or through the plumber.
The following table provides the control
sequence and
.I lang
strings accepted for each supported language respectfully.
.TP .TP
.B ctl-t
English (Passthrough). English (Passthrough).
ctl-t and en
.TP .TP
.B ctl-n
Japanese Hiragana. Japanese Hiragana.
ctl-n and jp
.TP .TP
.B ctl-k
Japanese Katakana. Japanese Katakana.
ctl-k and jpk
.TP .TP
.B ctl-c
Chinese. Chinese.
ctl-c and zh
.TP .TP
.B ctl-s
Korean. Korean.
ctl-k and ko
.TP .TP
.B ctl-v
Vietnamese. Vietnamese.
ctl-v and vn
.PP
.I Ktrans
listens on the
.I lang
plumber port for switching languages. The data accepted
on this port is the same as the
.B -l
flag's
.I lang
argument.
.SH CONVERSION .SH CONVERSION
Conversion is done in two layers, an implicit Conversion is done in two layers, an implicit
layer for unambiguous mappings, and an explicit layer for unambiguous mappings, and an explicit
@ -74,16 +92,27 @@ However in some cases the automatic hinting will be insufficient.
Input is always passed along, when a match is found Input is always passed along, when a match is found
.I Ktrans .I Ktrans
will emit backspaces to clear the input sequence and replace will emit backspaces to clear the input sequence and replace
it with the matched sequence. it with the matched sequence. Once
.B ctl-\e
has been used to start the selection of an explicit match, the
up and down arrow keys may be used to thumb around the options.
.SH DISPLAY .SH DISPLAY
.I Ktrans .I Ktrans
will provide a graphical display of current explicit conversion will provide a graphical display of current explicit conversion
candidates as implicit conversion is done. Candidates are highlighted candidates as implicit conversion is done. Candidates are highlighted
as a user cycles through them. At the bottom of the list is an exit as a user cycles through them. At the bottom of the list is an exit
button for quitting the program. Keyboard input typed in to the window is button for quitting the program. Keyboard input typed in to the window is
transliterated but discarded, providing a scratch input space. The transliterated but discarded, providing a scratch input space. The mouse
may be used to scroll through and select candidates, but it requires that
.I ktrans
is started using
.IR rio (1)'s
.B -k
flag.
.PP
The
.B -G .B -G
option disables this display. flag disables the graphical display entirely.
.SH "KEY MAPPING" .SH "KEY MAPPING"
For convenience, the control characters used by For convenience, the control characters used by
.I ktrans .I ktrans
@ -123,14 +152,17 @@ text files within
.BR /lib/ktrans . .BR /lib/ktrans .
The formats of which are specified within The formats of which are specified within
.IR ktrans (6). .IR ktrans (6).
Users may create and or modify existing dictionaries by binding over Additionally, dictionaries located in
the system defaults. .B $home/lib/ktrans/
will be merged on top of the system dictionaries.
Merging is done at a list level only; Keys that appear
replace all values of the previous definition.
.PP .PP
For backwards compatibility the For backwards compatibility the
.B jisho .B jisho
and and
.B zidian .B zidian
environment variables may also be set to pick explicit lookup dictionaries environment variables may also be set to pick alternate system dictionaries
for Japanese and Chinese respectfully. for Japanese and Chinese respectfully.
.SH LANGUAGES .SH LANGUAGES
.SS JAPANESE .SS JAPANESE

View file

@ -160,8 +160,6 @@ opendict(Hmap *h, char *name)
if(h == nil) if(h == nil)
h = hmapalloc(8192, sizeof(kouho)); h = hmapalloc(8192, sizeof(kouho));
else
hmapreset(h, 1);
while(p = Brdstr(b, '\n', 1)){ while(p = Brdstr(b, '\n', 1)){
if(p[0] == '\0' || p[0] == ';'){ if(p[0] == '\0' || p[0] == ';'){
Err: Err:
@ -275,7 +273,7 @@ maplkup(int lang, char *s, Map *m)
return hmapget(*h, s, m); return hmapget(*h, s, m);
} }
enum { Msgsize = 64 }; enum { Msgsize = 256 };
static Channel *dictch; static Channel *dictch;
static Channel *output; static Channel *output;
static Channel *input; static Channel *input;
@ -291,19 +289,22 @@ displaythread(void*)
Mouse m; Mouse m;
Keyboardctl *kctl; Keyboardctl *kctl;
Rune key; Rune key;
char *kouho[Maxkouho+1], **s; char *kouho[Maxkouho], **s, **e;
Image *back, *text, *board, *high; int i, page, total, height, round;
Image *back, *text, *board, *high, *scroll;
Font *f; Font *f;
Point p; Point p;
Rectangle r, exitr, selr; Rectangle r, exitr, selr, scrlr;
int selected; int selected;
enum { Adisp, Aresize, Amouse, Asel, Akbd, Aend }; char *mp, move[Msgsize];
enum { Adisp, Aresize, Amouse, Asel, Akbd, Amove, Aend };
Alt a[] = { Alt a[] = {
[Adisp] { nil, kouho+1, CHANRCV }, [Adisp] { nil, kouho, CHANRCV },
[Aresize] { nil, nil, CHANRCV }, [Aresize] { nil, nil, CHANRCV },
[Amouse] { nil, &m, CHANRCV }, [Amouse] { nil, &m, CHANRCV },
[Asel] { nil, &selected, CHANRCV }, [Asel] { nil, &selected, CHANRCV },
[Akbd] { nil, &key, CHANRCV }, [Akbd] { nil, &key, CHANRCV },
[Amove] { nil, move, CHANNOP },
[Aend] { nil, nil, CHANEND }, [Aend] { nil, nil, CHANEND },
}; };
@ -324,24 +325,28 @@ displaythread(void*)
sysfatal("failed to get keyboard: %r"); sysfatal("failed to get keyboard: %r");
memset(kouho, 0, sizeof kouho); memset(kouho, 0, sizeof kouho);
kouho[0] = "候補";
selected = -1; selected = -1;
f = display->defaultfont; f = display->defaultfont;
high = allocimagemix(display, DYellowgreen, DWhite); high = allocimagemix(display, DYellowgreen, DWhite);
text = display->black; text = display->black;
back = allocimagemix(display, DPaleyellow, DWhite); back = allocimagemix(display, DPaleyellow, DWhite);
board = allocimagemix(display, DBlack, DWhite); board = allocimagemix(display, DBlack, DWhite);
scroll = allocimage(display, Rect(0,0,1,1), screen->chan, 1, DYellowgreen);
a[Adisp].c = displaych; a[Adisp].c = displaych;
a[Aresize].c = mctl->resizec; a[Aresize].c = mctl->resizec;
a[Amouse].c = mctl->c; a[Amouse].c = mctl->c;
a[Asel].c = selectch; a[Asel].c = selectch;
a[Akbd].c = kctl->c; a[Akbd].c = kctl->c;
a[Amove].c = input;
threadsetname("display"); threadsetname("display");
goto Redraw; goto Redraw;
for(;;) for(;;)
switch(alt(a)){ switch(alt(a)){
case Amove:
a[Amove].op = CHANNOP;
break;
case Akbd: case Akbd:
if(key != Kdel) if(key != Kdel)
break; break;
@ -350,29 +355,98 @@ displaythread(void*)
case Amouse: case Amouse:
if(!m.buttons) if(!m.buttons)
break; break;
if(!ptinrect(m.xy, exitr)) if(ptinrect(m.xy, exitr)){
closedisplay(display);
threadexitsall(nil);
}
if(kouho[0] == nil)
break; break;
closedisplay(display); if(m.xy.x > scrlr.min.x && m.xy.x < scrlr.max.x){
threadexitsall(nil); if(m.xy.y > scrlr.min.y && m.xy.y < scrlr.max.y)
break;
if(m.xy.y < scrlr.min.y)
goto Up;
else
goto Down;
}
if(m.buttons & 7){
m.xy.y -= screen->r.min.y;
m.xy.y -= f->height;
if(m.xy.y < 0)
break;
i = round + m.xy.y/f->height + 1;
if(selected != -1)
i = i - selected - 1;
} else if(m.buttons == 8){
Up:
i = -1 * (selected % height + height);
if(selected + i < 0)
i = -(selected + (total % height));
} else if(m.buttons == 16){
Down:
i = height - (selected % height);
if(selected + i > total)
i = total - selected;
} else
break;
memset(move, 0, sizeof move);
move[0] = 'c';
if(i == 0)
break;
else if(i > 0)
memset(move+1, '', i);
else for(mp = move+1; i < 0; i++)
mp = seprint(mp, move + sizeof move, "%C", Kup);
a[Amove].op = CHANSND;
break;
case Aresize: case Aresize:
getwindow(display, Refnone); getwindow(display, Refnone);
case Adisp: case Adisp:
Redraw: Redraw:
for(s = kouho, total = 0; *s != nil; s++, total++)
;
r = screen->r; r = screen->r;
height = Dy(r)/f->height - 2;
draw(screen, r, back, nil, ZP); draw(screen, r, back, nil, ZP);
r.max.y = r.min.y + f->height; r.max.y = r.min.y + f->height;
draw(screen, r, board, nil, ZP); draw(screen, r, board, nil, ZP);
round = selected - (selected % height);
if(selected+1 > 0 && kouho[selected+1] != nil){ if(selected >= 0 && kouho[selected] != nil){
selr = screen->r; selr = screen->r;
selr.min.y += f->height*(selected+1); selr.min.y += f->height*(selected-round+1);
selr.max.y = selr.min.y + f->height; selr.max.y = selr.min.y + f->height;
draw(screen, selr, high, nil, ZP); draw(screen, selr, high, nil, ZP);
} }
scrlr = screen->r;
scrlr.min.y += f->height;
scrlr.max.y -= f->height;
scrlr.max.x = scrlr.min.x + 10;
draw(screen, scrlr, scroll, nil, ZP);
if(kouho[0] != nil){
scrlr.max.x--;
page = Dy(scrlr) / (total / height + 1);
scrlr.min.y = scrlr.min.y + page*(round / height);
scrlr.max.y = scrlr.min.y + page;
/* fill to the bottom on last page */
if((screen->r.max.y - f->height) - scrlr.max.y < page)
scrlr.max.y = screen->r.max.y - f->height;
draw(screen, scrlr, back, nil, ZP);
}
r.min.x += Dx(r)/2; r.min.x += Dx(r)/2;
p.y = r.min.y; p.y = r.min.y;
for(s = kouho; *s != nil; s++){
p.x = r.min.x - stringwidth(f, "候補")/2;
string(screen, p, text, ZP, f, "候補");
p.y += f->height;
for(s = kouho+round, e = kouho+round+height; *s != nil && s < e; s++){
p.x = r.min.x - stringwidth(f, *s)/2; p.x = r.min.x - stringwidth(f, *s)/2;
string(screen, p, text, ZP, f, *s); string(screen, p, text, ZP, f, *s);
p.y += f->height; p.y += f->height;
@ -414,7 +488,7 @@ dictthread(void*)
Str line; Str line;
Str last; Str last;
Str okuri; Str okuri;
int selected; int selected, loop;
enum{ enum{
Kanji, Kanji,
@ -425,6 +499,7 @@ dictthread(void*)
dict = jisho; dict = jisho;
selected = -1; selected = -1;
loop = 0;
mode = Kanji; mode = Kanji;
memset(kouho, 0, sizeof kouho); memset(kouho, 0, sizeof kouho);
resetstr(&last, &line, &okuri, nil); resetstr(&last, &line, &okuri, nil);
@ -463,6 +538,24 @@ dictthread(void*)
} }
popstr(&line); popstr(&line);
break; break;
case Kup:
if(selected == -1){
emitutf(output, p, 1);
break;
}
if(--selected < 0){
//wrap
while(kouho[++selected] != nil)
;
selected--;
}
loop = 1;
goto Select;
case Kdown:
if(selected == -1){
emitutf(output, p, 1);
break;
}
case '': case '':
selected++; selected++;
if(selected == 0){ if(selected == 0){
@ -472,20 +565,16 @@ dictthread(void*)
line.p[-1] = '\0'; line.p[-1] = '\0';
} }
if(kouho[selected] == nil){ if(kouho[selected] == nil){
/* cycled through all matches; bail */ selected = 0;
if(utflen(okuri.b) != 0) loop = 1;
emitutf(output, backspace, utflen(okuri.b));
emitutf(output, backspace, utflen(last.b));
emitutf(output, line.b, 0);
emitutf(output, okuri.b, 0);
break;
} }
Select:
send(selectch, &selected); send(selectch, &selected);
send(displaych, kouho); send(displaych, kouho);
if(okuri.p != okuri.b) if(okuri.p != okuri.b)
emitutf(output, backspace, utflen(okuri.b)); emitutf(output, backspace, utflen(okuri.b));
if(selected == 0) if(selected == 0 && !loop)
emitutf(output, backspace, utflen(line.b)); emitutf(output, backspace, utflen(line.b));
else else
emitutf(output, backspace, utflen(last.b)); emitutf(output, backspace, utflen(last.b));
@ -494,6 +583,7 @@ dictthread(void*)
last.p = pushutf(last.b, strend(&last), kouho[selected], 0); last.p = pushutf(last.b, strend(&last), kouho[selected], 0);
emitutf(output, okuri.b, 0); emitutf(output, okuri.b, 0);
mode = Kanji; mode = Kanji;
loop = 0;
continue; continue;
case ',': case '.': case ',': case '.':
case L'': case L'': case L'': case L'':
@ -585,6 +675,7 @@ keythread(void*)
{ {
int lang; int lang;
char m[Msgsize]; char m[Msgsize];
char *todict;
Map lkup; Map lkup;
char *p; char *p;
int n; int n;
@ -596,6 +687,7 @@ keythread(void*)
resetstr(&line, nil); resetstr(&line, nil);
if(lang == LangJP || lang == LangZH) if(lang == LangJP || lang == LangZH)
emitutf(dictch, peek, 1); emitutf(dictch, peek, 1);
todict = smprint(" %C%C", Kup, Kdown);
threadsetname("keytrans"); threadsetname("keytrans");
while(recv(input, m) != -1){ while(recv(input, m) != -1){
@ -624,7 +716,7 @@ keythread(void*)
emitutf(output, p, 1); emitutf(output, p, 1);
continue; continue;
} }
if(utfrune(" ", r) != nil){ if(utfrune(todict, r) != nil){
resetstr(&line, nil); resetstr(&line, nil);
emitutf(dictch, p, 1); emitutf(dictch, p, 1);
continue; continue;
@ -684,7 +776,7 @@ static void
kbdtap(void*) kbdtap(void*)
{ {
char m[Msgsize]; char m[Msgsize];
char buf[128]; char buf[Msgsize];
char *p; char *p;
int n; int n;
@ -774,16 +866,27 @@ usage(void)
threadexits("usage"); threadexits("usage");
} }
static char *kdir = "/lib/ktrans";
struct { struct {
char *s; char *s;
Hmap **m; Hmap **m;
} inittab[] = { } initmaptab[] = {
"judou", &judou, "judou", &judou,
"hira", &hira, "hira", &hira,
"kata", &kata, "kata", &kata,
"hangul", &hangul, "hangul", &hangul,
"telex", &telex, "telex", &telex,
}; };
struct {
char *env;
char *def;
Hmap **m;
} initdicttab[] = {
"jisho", "kanji.dict", &jisho,
"zidian", "wubi.dict", &zidian,
};
mainstacksize = 8192*2; mainstacksize = 8192*2;
@ -792,7 +895,8 @@ threadmain(int argc, char *argv[])
{ {
int nogui, i; int nogui, i;
char buf[128]; char buf[128];
char *jishoname, *zidianname; char *e, *home;
Hmap *m;
deflang = LangEN; deflang = LangEN;
nogui = 0; nogui = 0;
@ -822,40 +926,48 @@ threadmain(int argc, char *argv[])
usage(); usage();
} }
dictch = chancreate(Msgsize, 0);
input = chancreate(Msgsize, 0);
output = chancreate(Msgsize, 0);
/* allow gui to warm up while we're busy reading maps */ /* allow gui to warm up while we're busy reading maps */
if(nogui || access("/dev/winid", AEXIST) < 0){ if(nogui || access("/dev/winid", AEXIST) < 0){
displaych = nil; displaych = nil;
selectch = nil; selectch = nil;
} else { } else {
selectch = chancreate(sizeof(int), 1); selectch = chancreate(sizeof(int), 0);
displaych = chancreate(sizeof(char*)*Maxkouho, 1); displaych = chancreate(sizeof(char*)*Maxkouho, 0);
proccreate(displaythread, nil, mainstacksize); proccreate(displaythread, nil, mainstacksize);
} }
memset(backspace, '\b', sizeof backspace-1); memset(backspace, '\b', sizeof backspace-1);
backspace[sizeof backspace-1] = '\0'; backspace[sizeof backspace-1] = '\0';
if((jishoname = getenv("jisho")) == nil) if((home = getenv("home")) == nil)
jishoname = "/lib/ktrans/kanji.dict"; sysfatal("$home undefined");
if((jisho = opendict(nil, jishoname)) == nil) for(i = 0; i < nelem(initdicttab); i++){
sysfatal("failed to open jisho: %r"); e = getenv(initdicttab[i].env);
if(e != nil){
if((zidianname = getenv("zidian")) == nil) snprint(buf, sizeof buf, "%s", e);
zidianname = "/lib/ktrans/wubi.dict"; free(e);
if((zidian = opendict(nil, zidianname)) == nil) } else
sysfatal("failed to open zidian: %r"); snprint(buf, sizeof buf, "%s/%s", kdir, initdicttab[i].def);
if((*initdicttab[i].m = opendict(*initdicttab[i].m, buf)) == nil)
sysfatal("failed to open dict: %r");
snprint(buf, sizeof buf, "%s/%s/%s", home, kdir, initdicttab[i].def);
m = opendict(*initdicttab[i].m, buf);
if(m != nil)
*initdicttab[i].m = m;
}
free(home);
natural = nil; natural = nil;
for(i = 0; i < nelem(inittab); i++){ for(i = 0; i < nelem(initmaptab); i++){
snprint(buf, sizeof buf, "/lib/ktrans/%s.map", inittab[i].s); snprint(buf, sizeof buf, "%s/%s.map", kdir, initmaptab[i].s);
if((*inittab[i].m = openmap(buf)) == nil) if((*initmaptab[i].m = openmap(buf)) == nil)
sysfatal("failed to open map: %r"); sysfatal("failed to open map: %r");
} }
dictch = chancreate(Msgsize, 0);
input = chancreate(Msgsize, 0);
output = chancreate(Msgsize, 0);
plumbfd = plumbopen("lang", OREAD); plumbfd = plumbopen("lang", OREAD);
if(plumbfd >= 0) if(plumbfd >= 0)
proccreate(plumbproc, nil, mainstacksize); proccreate(plumbproc, nil, mainstacksize);

View file

@ -9,7 +9,7 @@ struct {
" no", L"", " no", L"",
" nno", L"んの", " nno", L"んの",
" neko", L"", " neko", L"",
" neko", L"ねこ", " neko", L"",
" watashi", L"", " watashi", L"",
" tanoShi", L"楽し", " tanoShi", L"楽し",
" oreNO", L"俺の", " oreNO", L"俺の",
@ -116,7 +116,7 @@ main(int argc, char **argv)
goto Verify; goto Verify;
case 8: case 8:
if(nstack == 0) if(nstack == 0)
sysfatal("buffer underrun"); sysfatal("buffer underrun on: %s", set[i].input);
nstack--; nstack--;
stack[nstack] = 0; stack[nstack] = 0;
break; break;