diff --git a/Makefile b/Makefile index e8d9477..8d96084 100644 --- a/Makefile +++ b/Makefile @@ -42,5 +42,6 @@ test: verify-map: python3 map/mktelex.py | cmp - map/telex.map + python3 -B map/mkemoji | cmp - map/emoji.dict .PHONY: all check test verify-map clean xim bench diff --git a/README.md b/README.md index 8d592d5..aea01e3 100644 --- a/README.md +++ b/README.md @@ -66,12 +66,32 @@ Switch input modes with Ctrl + key: S Hangul T English V Vietnamese (Telex) - E Emoji - P Toggle preedit/candidate popup window + P Toggle preedit echo Type romanized input. Select candidates with 1-9 or arrow keys. Tab or Enter to commit. +### Emoji and symbols + +Ctrl+E starts a one-shot symbol search; it is not a mode. Any current preedit +is committed, then the previous language is kept for the next input. Type an +English alias or the phonetic keys of the current layout (Korean aliases work, +for example). The candidate list is shown even when Ctrl+P has hidden normal +preedit echo. + + 안녕 → Ctrl+E → smile → Enter → 😀 + +There is no auto-insert. Enter commits the first candidate (or the current +one); arrows and Tab move, Alt+1 through Alt+9 select, Esc cancels. Plain +digits are part of the query. + +`map/emoji.src` is the source of truth: each UTF-8 TSV row is a result followed +by one or more tab-separated aliases. Aliases may be multilingual. Run +`map/mkemoji > map/emoji.dict` after editing it; `make verify-map` checks the +generated dictionary and the Telex map. + + 😀 웃음 웃다 스마일 smile grin + ## Architecture Threads communicate via CSP channels: diff --git a/dat.h b/dat.h index 01a3846..ef33e5e 100644 --- a/dat.h +++ b/dat.h @@ -156,6 +156,7 @@ struct Dictres Str key; Str kouho[Maxkouho]; int nkouho; + int lang; }; extern Lang langs[]; diff --git a/dict.c b/dict.c index 9d5917a..b0a82d4 100644 --- a/dict.c +++ b/dict.c @@ -12,6 +12,7 @@ dictlookup(Dictreq *req, Dictres *res) res->key = req->pre; res->nkouho = 0; + res->lang = req->lang; if(req->key.n == 0) return; l = getlang(req->lang); diff --git a/map/emoji.dict b/map/emoji.dict index e69de29..e1077b5 100644 --- a/map/emoji.dict +++ b/map/emoji.dict @@ -0,0 +1,226 @@ +! ⚠ ≠ +!! ⚠ +!= ≠ +* ★ +** ★ ++ ± ++- ± +- → +-> → +. · … ÷ +.. · … +... … +./ ÷ +: ☹ ☺ +:( ☹ +:) ☺ +< ← ≤ ≠ ♥ +<- ← +<3 ♥ +<= ≤ +<> ≠ += ≡ ⇒ +== ≡ +=> ⇒ +> ≥ +>= ≥ +^ ⁽ ⁾ ⁺ ⁻ ⁼ ⁰ ¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁱ ⁿ +^( ⁽ +^) ⁾ +^+ ⁺ +^- ⁻ +^0 ⁰ +^1 ¹ +^2 ² +^3 ³ +^4 ⁴ +^5 ⁵ +^6 ⁶ +^7 ⁷ +^8 ⁸ +^9 ⁹ +^= ⁼ +^i ⁱ +^n ⁿ +_ ₍ ₎ ₊ ₋ ₌ ₀ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ₐ ₑ ₒ ₓ +_( ₍ +_) ₎ +_+ ₊ +_- ₋ +_0 ₀ +_1 ₁ +_2 ₂ +_3 ₃ +_4 ₄ +_5 ₅ +_6 ₆ +_7 ₇ +_8 ₈ +_9 ₉ +_= ₌ +_a ₐ +_e ₑ +_o ₒ +_x ₓ +a α +al α +alp α +alph α +alpha α +b β 😊 +be β +bet β +beta β +bl 😊 +blu 😊 +blus 😊 +blush 😊 +c χ +ch χ +chi χ +d ° δ Δ ↓ +de Δ ° δ +deg ° +del δ +delt δ +delta δ +dn ↓ +e ε η +ep ε +eps ε +et η +eta η +f 凸 +fu 凸 +fuc 凸 +fuck 凸 +g γ Γ 😀 +ga Γ γ +gam γ +gamm γ +gamma γ +gr 😀 +gri 😀 +grin 😀 +h 😄 😊 ❤️ +ha 😄 😊 +hap 😄 😊 +happ 😄 😊 +happy 😄 😊 +he ❤️ +hea ❤️ +hear ❤️ +heart ❤️ +i ∫ ∞ ι +ii ∫ +in ∞ +inf ∞ +io ι +iot ι +iota ι +j 😂 +jo 😂 +joy 😂 +k κ +ka κ +kap κ +kapp κ +kappa κ +l λ Λ 😂 ❤️ 👍 +la Λ λ +lam λ +lamb λ +lambd λ +lambda λ +li 👍 +lik 👍 +like 👍 +lo 😂 ❤️ +lol 😂 +lov ❤️ +love ❤️ +m × μ +mu μ × +mul × +n ν +nu ν +o ω Ω ● +om Ω ω +ome ω +omeg ω +omega ω +oo ● +p φ Φ π Π ∏ ψ Ψ +ph Φ φ +phi φ +pi π Π +pp ∏ +ps Ψ ψ +psi ψ +r ρ +rh ρ +rho ρ +s σ Σ √ ∑ 😀 😄 😊 +si Σ σ +sig σ +sigm σ +sigma σ +sm 😀 😄 😊 +smi 😀 😄 😊 +smil 😀 😄 😊 +smile 😀 😄 😊 +sq √ +ss ∑ +t τ θ Θ 👍 +ta τ +tau τ +th Θ θ 👍 +the θ +thet θ +theta θ +thu 👍 +thum 👍 +thumb 👍 +thumbs 👍 +thumbsu 👍 +thumbsup 👍 +u ↑ υ +up ↑ υ +ups υ +v ✓ +vv ✓ +x ξ Ξ ✗ +xi ξ Ξ +xx ✗ +z ζ +ze ζ +zet ζ +zeta ζ +~ ≈ +~= ≈ +따 👍 +따봉 👍 +미 😄 😊 +미소 😄 😊 +방 😊 +방긋 😊 +사 ❤️ +사랑 ❤️ +스 😀 😄 +스마 😀 😄 +스마일 😀 😄 +엄 👍 +엄지 👍 +웃 😀 😄 😂 +웃겨 😂 +웃다 😀 +웃음 😀 😄 😂 +좋 👍 +좋아 👍 +좋아요 👍 +찬 👍 +찬성 👍 +폭 😂 +폭소 😂 +하 ❤️ +하트 ❤️ diff --git a/map/emoji.map b/map/emoji.map deleted file mode 100644 index be10f74..0000000 --- a/map/emoji.map +++ /dev/null @@ -1,183 +0,0 @@ -! ! -!! ⚠ -!= ≠ -* * -** ★ -+ + -+- ± -- - --> → -. . -.. · -... … -./ ÷ -: : -:( ☹ -:) ☺ -< < -<- ← -<3 ♥ -<= ≤ -<> ≠ -= = -== ≡ -=> ⇒ -> > ->= ≥ -D D -De Δ -G G -Ga Γ -I I -II ∫ -L L -La Λ -O O -Om Ω -P P -PP ∏ -Ph Φ -Pi Π -Ps Ψ -S S -SS ∑ -Si Σ -T T -Th Θ -X X -Xi Ξ -^ ^ -^( ⁽ -^) ⁾ -^+ ⁺ -^- ⁻ -^0 ⁰ -^1 ¹ -^2 ² -^3 ³ -^4 ⁴ -^5 ⁵ -^6 ⁶ -^7 ⁷ -^8 ⁸ -^9 ⁹ -^= ⁼ -^i ⁱ -^n ⁿ -_ _ -_( ₍ -_) ₎ -_+ ₊ -_- ₋ -_0 ₀ -_1 ₁ -_2 ₂ -_3 ₃ -_4 ₄ -_5 ₅ -_6 ₆ -_7 ₇ -_8 ₈ -_9 ₉ -_= ₌ -_a ₐ -_e ₑ -_o ₒ -_x ₓ -a a -al al -alp alp -alph alph -alpha α -b b -be be -bet bet -beta β -c c -ch ch -chi χ -d d -de de -deg ° -del del -delt delt -delta δ -dn ↓ -e e -ep ep -eps ε -et et -eta η -f f -fu fu -fuc fuc -fuck 凸 -g g -ga ga -gam gam -gamm gamm -gamma γ -i i -in in -inf ∞ -io io -iot iot -iota ι -k k -ka ka -kap kap -kapp kapp -kappa κ -l l -la la -lam lam -lamb lamb -lambd lambd -lambda λ -m m -mu μ -mul × -n n -nu ν -o o -om om -ome ome -omeg omeg -omega ω -oo ● -p p -ph ph -phi φ -pi π -ps ps -psi ψ -r r -rh rh -rho ρ -s s -si si -sig sig -sigm sigm -sigma σ -sq √ -t t -ta ta -tau τ -th th -the the -thet thet -theta θ -u u -up ↑ -ups υ -v v -vv ✓ -x x -xi ξ -xx ✗ -z z -ze ze -zet zet -zeta ζ -~ ~ -~= ≈ diff --git a/map/emoji.src b/map/emoji.src index 0290fe1..e1c34fc 100644 --- a/map/emoji.src +++ b/map/emoji.src @@ -1,100 +1,108 @@ -!! ⚠ -!= ≠ -** ★ -+- ± --> → -.. · -... … -./ ÷ -:( ☹ -:) ☺ -<- ← -<= ≤ -<> ≠ -== ≡ -=> ⇒ ->= ≥ -^( ⁽ -^) ⁾ -^+ ⁺ -^- ⁻ -^= ⁼ -_( ₍ -_) ₎ -_+ ₊ -_- ₋ -_= ₌ -~= ≈ -^0 ⁰ -_0 ₀ -^1 ¹ -_1 ₁ -^2 ² -_2 ₂ -<3 ♥ -^3 ³ -_3 ₃ -^4 ⁴ -_4 ₄ -^5 ⁵ -_5 ₅ -^6 ⁶ -_6 ₆ -^7 ⁷ -_7 ₇ -^8 ⁸ -_8 ₈ -^9 ⁹ -_9 ₉ -_a ₐ -alpha α -beta β -chi χ -deg ° -delta δ -De Δ -dn ↓ -_e ₑ -eps ε -eta η -fuck 凸 -gamma γ -Ga Γ -^i ⁱ -II ∫ -inf ∞ -iota ι -kappa κ -lambda λ -La Λ -mul × -mu μ -^n ⁿ -nu ν -omega ω -Om Ω -oo ● -_o ₒ -phi φ -Ph Φ -pi π -Pi Π -PP ∏ -psi ψ -Ps Ψ -rho ρ -sigma σ -Si Σ -sq √ -SS ∑ -tau τ -theta θ -Th Θ -up ↑ -ups υ -vv ✓ -xi ξ -Xi Ξ -xx ✗ -_x ₓ -zeta ζ +# Result first, followed by one or more TAB-separated aliases. +⚠ !! +★ ** +± +- +→ -> +· .. +… ... +÷ ./ +☹ :( +☺ :) +← <- +≤ <= +≠ <> != +≡ == +⇒ => +≥ >= +⁽ ^( +⁾ ^) +⁺ ^+ +⁻ ^- +⁼ ^= +₍ _( +₎ _) +₊ _+ +₋ _- +₌ _= +≈ ~= +⁰ ^0 +₀ _0 +¹ ^1 +₁ _1 +² ^2 +₂ _2 +♥ <3 +³ ^3 +₃ _3 +⁴ ^4 +₄ _4 +⁵ ^5 +₅ _5 +⁶ ^6 +₆ _6 +⁷ ^7 +₇ _7 +⁸ ^8 +₈ _8 +⁹ ^9 +₉ _9 +ₐ _a +α alpha +β beta +χ chi +° deg +δ delta +Δ De +↓ dn +ₑ _e +ε eps +η eta +凸 fuck +γ gamma +Γ Ga +ⁱ ^i +∫ II +∞ inf +ι iota +κ kappa +λ lambda +Λ La +× mul +μ mu +ⁿ ^n +ν nu +ω omega +Ω Om +● oo +ₒ _o +φ phi +Φ Ph +π pi +Π Pi +∏ PP +ψ psi +Ψ Ps +ρ rho +σ sigma +Σ Si +√ sq +∑ SS +τ tau +θ theta +Θ Th +↑ up +υ ups +✓ vv +ξ xi +Ξ Xi +✗ xx +ₓ _x +ζ zeta + +# Small multilingual emoji seed. Shared aliases intentionally produce choices. +😀 웃음 웃다 스마일 smile grin +😄 웃음 미소 스마일 smile happy +😂 웃음 폭소 웃겨 joy lol +😊 미소 방긋 smile blush happy +❤️ 사랑 하트 heart love +👍 좋아요 찬성 따봉 엄지 thumbsup like diff --git a/map/mkemoji b/map/mkemoji index c3bba1b..4925bfd 100755 --- a/map/mkemoji +++ b/map/mkemoji @@ -1,39 +1,71 @@ -#!/bin/sh +#!/usr/bin/env python3 +"""Write emoji.dict to stdout from a result-first UTF-8 TSV file.""" -awk ' -BEGIN { FS = "\t" } -/^$/ { next } -{ - key = $1 - val = $2 - map[key] = val - for(i = 1; i < length(key); i++){ - p = substr(key, 1, i) - if(!(p in map)) - map[p] = p - } -} -END { - for(k in map) - print k "\t" map[k] -} -' $1 | sort -k1,1 > emoji.map +import sys +import unicodedata +from pathlib import Path -awk ' -BEGIN { FS = "\t" } -/^$/ { next } -{ - key = $1 - val = $2 - for(i = 1; i <= length(key); i++){ - p = substr(key, 1, i) - dict[p] = dict[p] " " val - } -} -END { - for(p in dict){ - sub(/^ /, "", dict[p]) - print p "\t" dict[p] - } -} -' $1 | sort -k1,1 > emoji.dict + +SOURCE = Path(__file__).with_name("emoji.src") + + +def fold(s): + s = "".join(chr(ord(c) + 32) if "A" <= c <= "Z" else c for c in s) + return unicodedata.normalize("NFC", s) + + +def read(path): + entries = [] + with path.open(encoding="utf-8") as src: + for lineno, raw in enumerate(src, 1): + line = raw.rstrip("\r\n") + if not line.strip() or line.lstrip().startswith("#"): + continue + fields = line.split("\t") + if len(fields) < 2: + raise ValueError(f"{path}:{lineno}: need a result and an alias") + result = unicodedata.normalize("NFC", fields[0]) + if not result or any(c.isspace() for c in result): + raise ValueError(f"{path}:{lineno}: bad result") + for field in fields[1:]: + alias = fold(field) + if not alias or alias != alias.strip(): + raise ValueError(f"{path}:{lineno}: bad alias") + entries.append((result, alias)) + return entries + + +def add(table, key, result): + values = table.setdefault(key, []) + if result not in values: + values.append(result) + + +def build(entries): + exact = {} + prefix = {} + for result, alias in entries: + for n in range(1, len(alias) + 1): + add(exact if n == len(alias) else prefix, alias[:n], result) + for key in sorted(exact.keys() | prefix.keys()): + values = exact.get(key, []) + prefix.get(key, []) + values = list(dict.fromkeys(values)) + yield f"{key}\t{' '.join(values)}" + + +def main(): + if len(sys.argv) > 2: + print(f"usage: {sys.argv[0]} [emoji.src]", file=sys.stderr) + return 2 + path = Path(sys.argv[1]) if len(sys.argv) == 2 else SOURCE + try: + for line in build(read(path)): + print(line) + except (OSError, UnicodeError, ValueError) as error: + print(error, file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/strans.c b/strans.c index 50ddbc9..3e0fc6c 100644 --- a/strans.c +++ b/strans.c @@ -3,8 +3,18 @@ static Im im; static int popup = 0; +static int visible = 0; static void dictqmap(Im*); +typedef struct Search Search; +struct Search +{ + int on; + Str raw; + Str text; +}; +static Search search; + static void backmap(Im *im) { @@ -16,7 +26,7 @@ Lang langs[] = { {LangJP, "hira", "kanji", transmap, backmap, dictqmap, nil, nil}, {LangJPK, "kata", "kanji", transmap, backmap, dictqmap, nil, nil}, {LangKO, "hangul", nil, transko, backko, dictqmap, nil, nil}, - {LangEMOJI, "emoji", "emoji", transmap, backmap, dictqmap, nil, nil}, + {LangEMOJI, nil, "emoji", nil, nil, nil, nil, nil}, {LangVI, "telex", nil, transvi, backvi, dictqmap, nil, nil}, }; int nlang = nelem(langs); @@ -44,13 +54,15 @@ static void show(void) { Drawcmd dc; + Str *pre; int i, first, n; - if(!popup) - return; sclear(&dc.pre); - if(!mapget(im.l->map, &im.pre, &dc.pre)) - dc.pre = im.pre; + pre = search.on ? &search.text : &im.pre; + if(popup){ + if(search.on || im.l->map == nil || !mapget(im.l->map, pre, &dc.pre)) + dc.pre = *pre; + } first = im.sel >= Maxdisp ? im.sel - Maxdisp + 1 : 0; n = im.nkouho - first; if(n > Maxdisp) n = Maxdisp; @@ -58,6 +70,9 @@ show(void) dc.sel = im.sel >= 0 ? im.sel - first : -1; for(i = 0; i < n; i++) dc.kouho[i] = im.kouho[first + i]; + if(dc.pre.n == 0 && n == 0 && !visible) + return; + visible = dc.pre.n != 0 || n != 0; chansend(drawc, &dc); } @@ -110,7 +125,7 @@ commit(Str *com) { Str val; - if(mapget(im.l->map, &im.pre, &val)) + if(im.l->map != nil && mapget(im.l->map, &im.pre, &val)) sappend(com, &val); else sappend(com, &im.pre); @@ -209,13 +224,240 @@ transmap(Im *im, Rune c) return e; } +static Rune +ctrlkey(Rune c) +{ + if(c >= 'a' && c <= 'z') + return c - 'a' + 1; + return c; +} + +static void +foldascii(Str *s) +{ + int i; + + for(i = 0; i < s->n; i++) + if(s->r[i] >= 'A' && s->r[i] <= 'Z') + s->r[i] += 'a' - 'A'; +} + +static void +transstr(Lang *l, Str *raw, Str *out) +{ + static Im q; + Emit e; + Str mapped; + int i; + + sclear(out); + if(l == nil || l->trans == nil || (l->trans == transmap && l->map == nil)){ + *out = *raw; + return; + } + memset(&q, 0, sizeof q); + q.l = l; + for(i = 0; i < raw->n; i++){ + e = l->trans(&q, raw->r[i]); + sappend(out, &e.s); + q.pre = e.next; + if(!e.eat && out->n < Maxrunes) + sputr(out, raw->r[i]); + } + if(q.pre.n == 0) + return; + if(l->map != nil && mapget(l->map, &q.pre, &mapped)) + sappend(out, &mapped); + else + sappend(out, &q.pre); +} + +static void +addkouho(Str *s) +{ + int i; + + for(i = 0; i < im.nkouho; i++) + if(scmp(&im.kouho[i], s) == 0) + return; + if(im.nkouho < Maxkouho) + im.kouho[im.nkouho++] = *s; +} + +static void +emojilookup(Str *key, Dictres *res) +{ + Dictreq req; + + req.key = *key; + req.pre = req.key; + req.lang = LangEMOJI; + dictlookup(&req, res); +} + +static void +emojiquery(void) +{ + static Dictres res; + Str raw, local; + int i, rawhit, localhit; + + raw = search.raw; + foldascii(&raw); + transstr(im.l, &search.raw, &local); + foldascii(&local); + clearkouho(); + emojilookup(&raw, &res); + rawhit = res.nkouho != 0; + for(i = 0; i < res.nkouho; i++) + addkouho(&res.kouho[i]); + localhit = 0; + if(scmp(&raw, &local) != 0){ + emojilookup(&local, &res); + localhit = res.nkouho != 0; + for(i = 0; i < res.nkouho; i++) + addkouho(&res.kouho[i]); + } + search.text = rawhit || !localhit ? raw : local; + show(); +} + +static void +endsearch(void) +{ + search.on = 0; + sclear(&search.raw); + sclear(&search.text); + clearkouho(); + show(); +} + +static void +startsearch(Str *com) +{ + if(im.pre.n > 0) + commit(com); + sclear(&im.pre); + sclear(&im.raw); + clearkouho(); + search.on = 1; + sclear(&search.raw); + sclear(&search.text); + show(); +} + +static void +commitsearch(Str *com) +{ + sappend(com, &search.text); + endsearch(); +} + +static int +pickemoji(int n, Str *com) +{ + if(n < 0 || n >= im.nkouho) + return 0; + sappend(com, &im.kouho[n]); + endsearch(); + return 1; +} + +static int +searchkey(u32int ks, u32int mod, Str *com) +{ + int n, off; + Rune c; + + if(ks == Ksuper || ks == Kshift) + return 1; + if(ks == Kdown || ks == Kup){ + if(im.nkouho == 0) + return 1; + if(ks == Kdown && im.sel < im.nkouho - 1) + im.sel++; + if(ks == Kup && im.sel > 0) + im.sel--; + show(); + return 1; + } + n = ks - '1'; + off = im.sel >= Maxdisp ? im.sel - Maxdisp + 1 : 0; + if((mod & Malt) && n >= 0 && n < Maxdisp){ + pickemoji(off + n, com); + return 1; + } + if(ks == Ktab){ + if(im.nkouho == 0) + return 1; + if(mod & Mshift){ + if(im.sel <= 0) + im.sel = im.nkouho - 1; + else + im.sel--; + }else if(im.sel >= im.nkouho - 1) + im.sel = 0; + else + im.sel++; + show(); + return 1; + } + if(ks == Kret){ + if(im.nkouho > 0) + return pickemoji(im.sel >= 0 ? im.sel : 0, com); + commitsearch(com); + return 1; + } + if(ks == Kback){ + if(search.raw.n == 0) + return 1; + spopr(&search.raw); + emojiquery(); + return 1; + } + if(ks == Kesc){ + endsearch(); + return 1; + } + if(mod & Mctrl){ + c = ctrlkey(ks); + if(c == LangEMOJI){ + endsearch(); + return 1; + } + if(c == 0x10){ + popup = !popup; + show(); + return 1; + } + endsearch(); + if(setlang(c)) + return 1; + return 0; + } + if(ks >= Kspec || ks < ' ' || (mod & (Malt|Msuper))){ + commitsearch(com); + return 0; + } + if(search.raw.n >= Maxrunes){ + commitsearch(com); + return 0; + } + sputr(&search.raw, ks); + emojiquery(); + return 1; +} + static int keystroke(u32int ks, u32int mod, Str *com) { int n, off; + Rune c; if(ks == Ksuper || ks == Kshift) return 1; + if(search.on) + return searchkey(ks, mod, com); if(ks == Kdown || ks == Kup){ if(im.nkouho == 0) return 0; @@ -271,14 +513,18 @@ keystroke(u32int ks, u32int mod, Str *com) return 0; } if(mod & Mctrl){ - reset(); - if(ks >= 'a' && ks <= 'z') - ks -= 'a' - 1; - if(ks == 0x10){ - popup = !popup; + c = ctrlkey(ks); + if(c == LangEMOJI){ + startsearch(com); return 1; } - if(setlang(ks)) + if(c == 0x10){ + popup = !popup; + show(); + return 1; + } + reset(); + if(setlang(c)) return 1; return 0; } @@ -305,6 +551,8 @@ init(void) { memset(&im, 0, sizeof(im)); im.l = getlang(LangEN); + memset(&search, 0, sizeof search); + visible = 0; } static void @@ -316,15 +564,15 @@ imhandlekey(Keyreq *kr) sclear(&res.preedit); res.eaten = keystroke(kr->ks, kr->mod, &res.commit); if(kr->want) - res.preedit = im.pre; + res.preedit = search.on ? search.text : im.pre; chansend(kr->reply, &res); } void imthread(void*) { + static Dictres res; Keyreq kr; - Dictres res; Alt alts[] = { {keyc, &kr, CHANRCV, nil}, {dictresc, &res, CHANRCV, nil}, @@ -339,7 +587,8 @@ imthread(void*) imhandlekey(&kr); break; case 1: - if(scmp(&res.key, &im.pre) == 0){ + if(!search.on && res.lang == im.l->lang && + scmp(&res.key, &im.pre) == 0){ setkouho(&res); show(); } diff --git a/tests/dict_test.c b/tests/dict_test.c index 0f9a735..8476529 100644 --- a/tests/dict_test.c +++ b/tests/dict_test.c @@ -25,6 +25,7 @@ dictionary_candidates(struct ct *t) dictlookup(&req, &res); if(!CT_EQ_INT(t, 2, res.nkouho)) goto cleanup; + CT_EQ_INT(t, LangJP, res.lang); 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]); @@ -44,6 +45,7 @@ dictionary_candidates(struct ct *t) dictlookup(&req, &res); if(!CT_EQ_INT(t, Maxkouho, res.nkouho)) goto cleanup; + CT_EQ_INT(t, LangJP, res.lang); 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]); @@ -81,6 +83,7 @@ dictionary_misses_clear_result(struct ct *t) if(res.nkouho != 0) CT_ERRORF(t, "%s: want 0 candidates, got %d", cases[i].name, res.nkouho); + CT_EQ_INT(t, LangJP, res.lang); checkstr(t, cases[i].name, cases[i].pre, &res.key); } hmapfree(lang->dict); diff --git a/tests/engine_test.c b/tests/engine_test.c index 710ac9a..3402d36 100644 --- a/tests/engine_test.c +++ b/tests/engine_test.c @@ -255,3 +255,177 @@ engine_language_switch_state(struct ct *t) } vi->map = saved; } + +typedef struct Emojifix Emojifix; +struct Emojifix +{ + Im im; + Search search; + Hmap *dict; + int popup; + int visible; +}; + +static int +draindraw(Drawcmd *last) +{ + Drawcmd dc = {0}; + int n; + + n = 0; + while(channbrecv(drawc, &dc) > 0){ + n++; + if(last != nil) + *last = dc; + } + return n; +} + +static void +setemoji(Hmap **dict, char *key, char *val) +{ + Str s; + + s = mkstr(key); + hmapset(dict, &s, val, strlen(val)); +} + +static void +emojibegin(Emojifix *f, int showpre) +{ + Lang *l; + + l = getlang(LangEMOJI); + f->im = im; + f->search = search; + f->dict = l->dict; + f->popup = popup; + f->visible = visible; + draindraw(nil); + l->dict = hmapalloc(8); + setemoji(&l->dict, "alpha", "α"); + setemoji(&l->dict, "smil", "😀 😄"); + setemoji(&l->dict, "smile", "😀 😄"); + setemoji(&l->dict, "웃음", "😀 😄 😂"); + init(); + im.l = getlang(LangKO); + popup = showpre; +} + +static void +emojiend(Emojifix *f) +{ + Lang *l; + Hmap *dict; + + l = getlang(LangEMOJI); + dict = l->dict; + l->dict = f->dict; + hmapfree(dict); + draindraw(nil); + im = f->im; + search = f->search; + popup = f->popup; + visible = f->visible; +} + +static int +typekeys(struct ct *t, char *keys, Str *com) +{ + for(; *keys != '\0'; keys++) + if(!CT_CHECK(t, keystroke((uchar)*keys, 0, com))) + return 0; + return 1; +} + +void +engine_emoji_single_candidate(struct ct *t) +{ + Drawcmd dc = {0}; + Emojifix f; + Str com; + + emojibegin(&f, 0); + sclear(&com); + CT_CHECK(t, keystroke('e', Mctrl, &com)); + CT_CHECK(t, search.on); + CT_EQ_INT(t, LangKO, im.l->lang); + if(typekeys(t, "alpha", &com)){ + checkstr(t, "no automatic commit", "", &com); + checkstr(t, "raw English query", "alpha", &search.text); + CT_CHECK(t, search.on); + CT_EQ_INT(t, 1, im.nkouho); + checkstr(t, "single candidate", "α", &im.kouho[0]); + CT_CHECK(t, draindraw(&dc) > 0); + CT_EQ_INT(t, 0, dc.pre.n); + CT_EQ_INT(t, 1, dc.nkouho); + CT_CHECK(t, keystroke(Kret, 0, &com)); + checkstr(t, "explicit candidate commit", "α", &com); + CT_CHECK(t, !search.on); + CT_EQ_INT(t, LangKO, im.l->lang); + } + emojiend(&f); +} + +void +engine_emoji_queries(struct ct *t) +{ + Drawcmd dc = {0}; + Emojifix f; + Str com; + + emojibegin(&f, 1); + sclear(&com); + CT_CHECK(t, keystroke('e', Mctrl, &com)); + if(typekeys(t, "smile", &com)){ + checkstr(t, "English alias", "smile", &search.text); + CT_EQ_INT(t, 2, im.nkouho); + CT_CHECK(t, keystroke(Kback, 0, &com)); + checkstr(t, "backspaced query", "smil", &search.text); + CT_EQ_INT(t, 2, im.nkouho); + CT_CHECK(t, keystroke('e', 0, &com)); + CT_CHECK(t, keystroke('2', Malt, &com)); + checkstr(t, "selected English alias", "😄", &com); + CT_EQ_INT(t, LangKO, im.l->lang); + } + + sclear(&com); + CT_CHECK(t, keystroke('e', Mctrl, &com)); + if(typekeys(t, "dntdma", &com)){ + checkstr(t, "localized Korean query", "웃음", &search.text); + CT_EQ_INT(t, 3, im.nkouho); + CT_CHECK(t, draindraw(&dc) > 0); + checkstr(t, "popup query", "웃음", &dc.pre); + CT_EQ_INT(t, 3, dc.nkouho); + CT_CHECK(t, keystroke(Kesc, 0, &com)); + checkstr(t, "cancel commit", "", &com); + CT_CHECK(t, !search.on); + CT_EQ_INT(t, LangKO, im.l->lang); + } + emojiend(&f); +} + +void +engine_emoji_start_and_unknown(struct ct *t) +{ + Emojifix f; + Str com; + + emojibegin(&f, 0); + sclear(&com); + if(typekeys(t, "dkssud", &com)){ + CT_CHECK(t, keystroke('e', Mctrl, &com)); + checkstr(t, "committed Korean preedit", "안녕", &com); + CT_CHECK(t, search.on); + CT_EQ_INT(t, LangKO, im.l->lang); + if(typekeys(t, "xyz", &com)){ + checkstr(t, "unknown displayed query", "xyz", &search.text); + CT_EQ_INT(t, 0, im.nkouho); + CT_CHECK(t, keystroke(Kret, 0, &com)); + checkstr(t, "unknown query commit", "안녕xyz", &com); + CT_CHECK(t, !search.on); + CT_EQ_INT(t, LangKO, im.l->lang); + } + } + emojiend(&f); +} diff --git a/tests/test.h b/tests/test.h index cc80a2d..a2d57d8 100644 --- a/tests/test.h +++ b/tests/test.h @@ -33,6 +33,9 @@ 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 engine_emoji_single_candidate(struct ct*); +void engine_emoji_queries(struct ct*); +void engine_emoji_start_and_unknown(struct ct*); void dictionary_candidates(struct ct*); void dictionary_misses_clear_result(struct ct*); diff --git a/tests/unit_test.c b/tests/unit_test.c index 19433bd..befe703 100644 --- a/tests/unit_test.c +++ b/tests/unit_test.c @@ -80,6 +80,9 @@ static const struct ct_test tests[] = { { "engine/commit-contract", engine_commit_contract }, { "engine/language-switch-state", engine_language_switch_state }, { "engine/telex-history-bound", engine_telex_history_bound }, + { "engine/emoji-single-candidate", engine_emoji_single_candidate }, + { "engine/emoji-queries", engine_emoji_queries }, + { "engine/emoji-start-and-unknown", engine_emoji_start_and_unknown }, { "dict/candidates", dictionary_candidates }, { "dict/misses-clear-result", dictionary_misses_clear_result }, }; @@ -89,7 +92,7 @@ threadmain(int argc, char **argv) { int i, status; - drawc = chancreate(sizeof(Drawcmd), 4); + drawc = chancreate(sizeof(Drawcmd), 1024); keyc = chancreate(sizeof(Keyreq), 0); dictreqc = chancreate(sizeof(Dictreq), 64); dictresc = chancreate(sizeof(Dictres), 0); diff --git a/win.c b/win.c index b588405..802f8e3 100644 --- a/win.c +++ b/win.c @@ -90,16 +90,18 @@ fill(u32int *buf, int n, u32int color) static void drawkouho(Drawcmd *dc, int first, int n, int w, int h) { - int sely, y, i; + int npre, sely, y, i; Str *s; 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; + npre = dc->pre.n != 0; + if(npre) + drawstr(img, 0, 0, dc->pre.r, dc->pre.n, w, h); + if(dc->sel >= 0 && dc->sel < n){ + sely = (npre + dc->sel) * Fontsz; fill(img + sely * w, Fontsz * w, Colsel); } - for(i = 0, y = Fontsz; i < n; i++, y += Fontsz){ + for(i = 0, y = npre * Fontsz; i < n; i++, y += Fontsz){ s = &dc->kouho[first+i]; putfont(img, w, h, 0, y, '1' + i + Asciitofull); drawstr(img, 2*Fontsz, y, s->r, s->n, w, h); @@ -124,13 +126,18 @@ winhide(void) static void winshow(Drawcmd *dc) { - int px, py, w, h, i, n, maxw; + int npre, px, py, w, h, i, n, maxw; u32int vals[4]; xcb_query_pointer_reply_t *ptr; xcb_query_pointer_cookie_t cookie; + n = min(max(dc->nkouho, 0), Maxdisp); + npre = dc->pre.n != 0; + if(n == 0 && npre == 0){ + winhide(); + return; + } cookie = xcb_query_pointer(conn, scr->root); - n = dc->nkouho; maxw = dc->pre.n; for(i = 0; i < n; i++) maxw = max(maxw, dc->kouho[i].n); @@ -140,7 +147,7 @@ winshow(Drawcmd *dc) px = ptr->root_x + 10; py = ptr->root_y + 10; free(ptr); - vals[3] = h = (n + 1) * Fontsz; + vals[3] = h = (n + npre) * Fontsz; vals[2] = w = (maxw + 3) * Fontsz; vals[1] = py = max(0, min(py, scr->height_in_pixels - h)); vals[0] = px = max(0, min(px, scr->width_in_pixels - w));