code: plan9front

Download patch

ref: f4122cfbc9ee6b99f69f1194ea38565fdfad36bb
parent: 578af37678b2ea16e29d950cbc352823cd2491ab
author: Jacob Moody <moody@posixcafe.org>
date: Tue Apr 2 00:01:56 EDT 2024

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

--- a/lib/ktrans/skk2ktrans
+++ b/lib/ktrans/skk2ktrans
@@ -1,2 +1,2 @@
 #!/bin/rc
-tcs -sf jis | awk '$1 !~ /;;/ {gsub("(^\/|\/$)", "", $2); gsub(" ", "	"); gsub("\/", " ", $2);} {print}'
+tcs -sf ujis | awk '$1 !~ /;;/ {gsub("(^\/|\/$)", "", $2); gsub(" ", "	"); gsub("\/", " ", $2);} {print}'
--- a/sys/man/1/ktrans
+++ b/sys/man/1/ktrans
@@ -24,29 +24,47 @@
 .I kbdtap
 file is given, it is used for both
 input and output instead.
-.I Ktrans
-starts in a passthrough mode, echoing out
-the input with no conversions. Control characters
-are used to give instructions, the following
-control sequences are used to switch between languages:
+By default
+.I ktrans
+starts in passthrough mode, echoing out
+the input with no conversions. The initial
+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
-.B ctl-t
 English (Passthrough).
+ctl-t and en
 .TP
-.B ctl-n
 Japanese Hiragana.
+ctl-n and jp
 .TP
-.B ctl-k
 Japanese Katakana.
+ctl-k and jpk
 .TP
-.B ctl-c
 Chinese.
+ctl-c and zh
 .TP
-.B ctl-s
 Korean.
+ctl-k and ko
 .TP
-.B ctl-v
 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
 Conversion is done in two layers, an implicit
 layer for unambiguous mappings, and an explicit
@@ -74,7 +92,10 @@
 Input is always passed along, when a match is found
 .I Ktrans
 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
 .I Ktrans
 will provide a graphical display of current explicit conversion
@@ -81,9 +102,17 @@
 candidates as implicit conversion is done. Candidates are highlighted
 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
-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
-option disables this display.
+flag disables the graphical display entirely.
 .SH "KEY MAPPING"
 For convenience, the control characters used by
 .I ktrans
@@ -123,14 +152,17 @@
 .BR /lib/ktrans .
 The formats of which are specified within
 .IR ktrans (6).
-Users may create and or modify existing dictionaries by binding over
-the system defaults.
+Additionally, dictionaries located in
+.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
 For backwards compatibility the
 .B jisho
 and
 .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.
 .SH LANGUAGES
 .SS JAPANESE
--- a/sys/src/cmd/ktrans/main.c
+++ b/sys/src/cmd/ktrans/main.c
@@ -160,8 +160,6 @@
 
 	if(h == nil)
 		h = hmapalloc(8192, sizeof(kouho));
-	else
-		hmapreset(h, 1);
 	while(p = Brdstr(b, '\n', 1)){
 		if(p[0] == '\0' || p[0] == ';'){
 		Err:
@@ -275,7 +273,7 @@
 	return hmapget(*h, s, m);
 }
 
-enum   { Msgsize = 64 };
+enum   { Msgsize = 256 };
 static Channel	*dictch;
 static Channel	*output;
 static Channel	*input;
@@ -291,19 +289,22 @@
 	Mouse m;
 	Keyboardctl *kctl;
 	Rune key;
-	char *kouho[Maxkouho+1], **s;
-	Image *back, *text, *board, *high;
+	char *kouho[Maxkouho], **s, **e;
+	int i, page, total, height, round;
+	Image *back, *text, *board, *high, *scroll;
 	Font *f;
 	Point p;
-	Rectangle r, exitr, selr;
+	Rectangle r, exitr, selr, scrlr;
 	int selected;
-	enum { Adisp, Aresize, Amouse, Asel, Akbd, Aend };
+	char *mp, move[Msgsize];
+	enum { Adisp, Aresize, Amouse, Asel, Akbd, Amove, Aend };
 	Alt a[] = {
-		[Adisp] { nil, kouho+1, CHANRCV },
+		[Adisp] { nil, kouho, CHANRCV },
 		[Aresize] { nil, nil, CHANRCV },
 		[Amouse] { nil, &m, CHANRCV },
 		[Asel] { nil, &selected, CHANRCV },
 		[Akbd] { nil, &key, CHANRCV },
+		[Amove] { nil, move, CHANNOP },
 		[Aend] { nil, nil, CHANEND },
 	};
 
@@ -324,7 +325,6 @@
 		sysfatal("failed to get keyboard: %r");
 
 	memset(kouho, 0, sizeof kouho);
-	kouho[0] = "候補";
 	selected = -1;
 	f = display->defaultfont;
 	high = allocimagemix(display, DYellowgreen, DWhite);
@@ -331,6 +331,7 @@
 	text = display->black;
 	back = allocimagemix(display, DPaleyellow, DWhite);
 	board = allocimagemix(display, DBlack, DWhite);
+	scroll = allocimage(display, Rect(0,0,1,1), screen->chan, 1, DYellowgreen);
 
 	a[Adisp].c = displaych;
 	a[Aresize].c = mctl->resizec;
@@ -337,11 +338,15 @@
 	a[Amouse].c = mctl->c;
 	a[Asel].c = selectch;
 	a[Akbd].c = kctl->c;
+	a[Amove].c = input;
 
 	threadsetname("display");
 	goto Redraw;
 	for(;;)
 		switch(alt(a)){
+		case Amove:
+			a[Amove].op = CHANNOP;
+			break;
 		case Akbd:
 			if(key != Kdel)
 				break;
@@ -350,29 +355,98 @@
 		case Amouse:
 			if(!m.buttons)
 				break;
-			if(!ptinrect(m.xy, exitr))
+			if(ptinrect(m.xy, exitr)){
+				closedisplay(display);
+				threadexitsall(nil);
+			}
+			if(kouho[0] == nil)
 				break;
-			closedisplay(display);
-			threadexitsall(nil);
+			if(m.xy.x > scrlr.min.x && m.xy.x < scrlr.max.x){
+				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:
 			getwindow(display, Refnone);
 		case Adisp:
 		Redraw:
+			for(s = kouho, total = 0; *s != nil; s++, total++)
+				;
+
 			r = screen->r;
+			height = Dy(r)/f->height - 2;
 			draw(screen, r, back, nil, ZP);
 			r.max.y = r.min.y + f->height;
 			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.min.y += f->height*(selected+1);
+				selr.min.y += f->height*(selected-round+1);
 				selr.max.y = selr.min.y + f->height;
 				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;
 			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;
 				string(screen, p, text, ZP, f, *s);
 				p.y += f->height;
@@ -414,7 +488,7 @@
 	Str line;
 	Str last;
 	Str okuri;
-	int selected;
+	int selected, loop;
 
 	enum{
 		Kanji,
@@ -425,6 +499,7 @@
 
 	dict = jisho;
 	selected = -1;
+	loop = 0;
 	mode = Kanji;
 	memset(kouho, 0, sizeof kouho);
 	resetstr(&last, &line, &okuri, nil);
@@ -463,6 +538,24 @@
 				}
 				popstr(&line);
 				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 '':
 				selected++;
 				if(selected == 0){
@@ -472,20 +565,16 @@
 						line.p[-1] = '\0';
 				}
 				if(kouho[selected] == nil){
-					/* cycled through all matches; bail */
-					if(utflen(okuri.b) != 0)
-						emitutf(output, backspace, utflen(okuri.b));
-					emitutf(output, backspace, utflen(last.b));
-					emitutf(output, line.b, 0);
-					emitutf(output, okuri.b, 0);
-					break;
+					selected = 0;
+					loop = 1;
 				}
+			Select:
 				send(selectch, &selected);
 				send(displaych, kouho);
 
 				if(okuri.p != okuri.b)
 					emitutf(output, backspace, utflen(okuri.b));
-				if(selected == 0)
+				if(selected == 0 && !loop)
 					emitutf(output, backspace, utflen(line.b));
 				else
 					emitutf(output, backspace, utflen(last.b));
@@ -494,6 +583,7 @@
 				last.p = pushutf(last.b, strend(&last), kouho[selected], 0);
 				emitutf(output, okuri.b, 0);
 				mode = Kanji;
+				loop = 0;
 				continue;
 			case ',': case '.':
 			case L'。': case L'、':
@@ -585,6 +675,7 @@
 {
 	int lang;
 	char m[Msgsize];
+	char *todict;
 	Map lkup;
 	char *p;
 	int n;
@@ -596,6 +687,7 @@
 	resetstr(&line, nil);
 	if(lang == LangJP || lang == LangZH)
 		emitutf(dictch, peek, 1);
+	todict = smprint("%C%C", Kup, Kdown);
 
 	threadsetname("keytrans");
 	while(recv(input, m) != -1){
@@ -624,7 +716,7 @@
 				emitutf(output, p, 1);
 				continue;
 			}
-			if(utfrune("", r) != nil){
+			if(utfrune(todict, r) != nil){
 				resetstr(&line, nil);
 				emitutf(dictch, p, 1);
 				continue;
@@ -684,7 +776,7 @@
 kbdtap(void*)
 {
 	char m[Msgsize];
-	char buf[128];
+	char buf[Msgsize];
 	char *p;
 	int n;
 
@@ -774,10 +866,13 @@
 	threadexits("usage");
 }
 
+
+static char *kdir = "/lib/ktrans";
+
 struct {
 	char *s;
 	Hmap **m;
-} inittab[] = {
+} initmaptab[] = {
 	"judou", &judou,
 	"hira", &hira,
 	"kata", &kata,
@@ -784,6 +879,14 @@
 	"hangul", &hangul,
 	"telex", &telex,
 };
+struct {
+	char *env;
+	char *def;
+	Hmap **m;
+} initdicttab[] = {
+	"jisho", "kanji.dict", &jisho,
+	"zidian", "wubi.dict", &zidian,
+};
 
 mainstacksize = 8192*2;
 
@@ -792,7 +895,8 @@
 {
 	int nogui, i;
 	char buf[128];
-	char *jishoname, *zidianname;
+	char *e, *home;
+	Hmap *m;
 
 	deflang = LangEN;
 	nogui = 0;
@@ -822,13 +926,17 @@
 		usage();
 	}
 
+	dictch 	= chancreate(Msgsize, 0);
+	input 	= chancreate(Msgsize, 0);
+	output 	= chancreate(Msgsize, 0);
+
 	/* allow gui to warm up while we're busy reading maps */
 	if(nogui || access("/dev/winid", AEXIST) < 0){
 		displaych = nil;
 		selectch = nil;
 	} else {
-		selectch = chancreate(sizeof(int), 1);
-		displaych = chancreate(sizeof(char*)*Maxkouho, 1);
+		selectch = chancreate(sizeof(int), 0);
+		displaych = chancreate(sizeof(char*)*Maxkouho, 0);
 		proccreate(displaythread, nil, mainstacksize);
 	}
 
@@ -835,26 +943,30 @@
 	memset(backspace, '\b', sizeof backspace-1);
 	backspace[sizeof backspace-1] = '\0';
 
-	if((jishoname = getenv("jisho")) == nil)
-		jishoname = "/lib/ktrans/kanji.dict";
-	if((jisho = opendict(nil, jishoname)) == nil)
-		sysfatal("failed to open jisho: %r");
+	if((home = getenv("home")) == nil)
+		sysfatal("$home undefined");
+	for(i = 0; i < nelem(initdicttab); i++){
+		e = getenv(initdicttab[i].env);
+		if(e != nil){
+			snprint(buf, sizeof buf, "%s", e);
+			free(e);
+		} else
+			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);
 
-	if((zidianname = getenv("zidian")) == nil)
-		zidianname = "/lib/ktrans/wubi.dict";
-	if((zidian = opendict(nil, zidianname)) == nil)
-		sysfatal("failed to open zidian: %r");
-
 	natural = nil;
-	for(i = 0; i < nelem(inittab); i++){
-		snprint(buf, sizeof buf, "/lib/ktrans/%s.map", inittab[i].s);
-		if((*inittab[i].m = openmap(buf)) == nil)
+	for(i = 0; i < nelem(initmaptab); i++){
+		snprint(buf, sizeof buf, "%s/%s.map", kdir, initmaptab[i].s);
+		if((*initmaptab[i].m = openmap(buf)) == nil)
 			sysfatal("failed to open map: %r");
 	}
-
-	dictch 	= chancreate(Msgsize, 0);
-	input 	= chancreate(Msgsize, 0);
-	output 	= chancreate(Msgsize, 0);
 
 	plumbfd = plumbopen("lang", OREAD);
 	if(plumbfd >= 0)
--- a/sys/src/cmd/ktrans/test.c
+++ b/sys/src/cmd/ktrans/test.c
@@ -9,7 +9,7 @@
 	"no", L"の",
 	"nno", L"んの",
 	"neko", L"猫",
-	"neko", L"ねこ",
+	"neko", L"猫",
 	"watashi", L"私",
 	"tanoShi", L"楽し",
 	"oreNO", L"俺の",
@@ -116,7 +116,7 @@
 					goto Verify;
 				case 8:
 					if(nstack == 0)
-						sysfatal("buffer underrun");
+						sysfatal("buffer underrun on: %s", set[i].input);
 					nstack--;
 					stack[nstack] = 0;
 					break;