Compare commits

...

2 Commits

Author SHA1 Message Date
2ae87a3926 emoji: repair one-shot candidate selection 2026-08-12 02:08:01 +09:00
0fff17c8b8 emoji: add one-shot candidate search 2026-08-11 22:24:59 +09:00
19 changed files with 1324 additions and 360 deletions

View File

@@ -42,5 +42,7 @@ test:
verify-map: verify-map:
python3 map/mktelex.py | cmp - map/telex.map python3 map/mktelex.py | cmp - map/telex.map
python3 -B map/mkemoji | cmp - map/emoji.dict
python3 -B tests/mkemoji_test.py
.PHONY: all check test verify-map clean xim bench .PHONY: all check test verify-map clean xim bench

View File

@@ -29,9 +29,9 @@ mirrors must provide the same sibling; initialize it after cloning with
The suite covers the fixed-size UTF-8 string, hash table, trie fixtures, The suite covers the fixed-size UTF-8 string, hash table, trie fixtures,
Japanese/Hangul/Telex state transitions, dictionary candidates, engine Japanese/Hangul/Telex state transitions, dictionary candidates, engine
selection state, and production map loading. Tests use explicit case tables, selection state, IPC request fields, and production map loading. Tests use
boundary values, and small regressions for repaired core contracts. IPC and GUI explicit case tables, boundary values, and small regressions for repaired core
protocol stacks are outside this suite and require separate integration checks. contracts. Full IPC and GUI protocol stacks require separate integration checks.
## Run ## Run
@@ -66,11 +66,35 @@ Switch input modes with Ctrl + key:
S Hangul S Hangul
T English T English
V Vietnamese (Telex) V Vietnamese (Telex)
E Emoji P Toggle preedit echo
P Toggle preedit/candidate popup window
Type romanized input. Select candidates with 1-9 or arrow keys. Type romanized input. Select candidates with 1-9 or arrow keys.
Tab or Enter to commit. Outside one-shot search, Tab or Enter commits.
### 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, and Esc cancels. Bare 1 through 9 select an existing
numbered row; a bare digit with no such row and 0 remain query text. Digit
aliases are kept in matching slots, so `^1`, `_2`, and `<3` produce `¹`, `₂`,
and `♥`.
`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 and ASCII
letters are case-insensitive. Results and aliases are limited to 64 Unicode
code points; results cannot contain whitespace. Run `map/mkemoji > map/emoji.dict`
after editing it; `make verify-map` checks the generated dictionary and the
Telex map.
😀 웃음 웃다 스마일 smile grin
## Architecture ## Architecture

1
dat.h
View File

@@ -156,6 +156,7 @@ struct Dictres
Str key; Str key;
Str kouho[Maxkouho]; Str kouho[Maxkouho];
int nkouho; int nkouho;
int lang;
}; };
extern Lang langs[]; extern Lang langs[];

1
dict.c
View File

@@ -12,6 +12,7 @@ dictlookup(Dictreq *req, Dictres *res)
res->key = req->pre; res->key = req->pre;
res->nkouho = 0; res->nkouho = 0;
res->lang = req->lang;
if(req->key.n == 0) if(req->key.n == 0)
return; return;
l = getlang(req->lang); l = getlang(req->lang);

2
ipc.c
View File

@@ -38,7 +38,7 @@ ipcunpackreq(const unsigned char req[Ipcreqsz], int *want, unsigned int *mod,
unsigned int *key) unsigned int *key)
{ {
*want = req[0] != 0; *want = req[0] != 0;
*mod = req[1]; *mod = req[1] & Mmask;
*key = (unsigned int)req[2] | *key = (unsigned int)req[2] |
((unsigned int)req[3] << 8) | ((unsigned int)req[3] << 8) |
((unsigned int)req[4] << 16) | ((unsigned int)req[4] << 16) |

6
ipc.h
View File

@@ -7,6 +7,7 @@
/* /*
* Request: [want-preedit, modifiers, key byte 0, ..., key byte 3]. * Request: [want-preedit, modifiers, key byte 0, ..., key byte 3].
* Only Mmask modifier bits are retained.
* Response: [eaten, commit-length-low, commit-length-high, commit...], * Response: [eaten, commit-length-low, commit-length-high, commit...],
* followed, when requested, by * followed, when requested, by
* [preedit-length-low, preedit-length-high, preedit...]. * [preedit-length-low, preedit-length-high, preedit...].
@@ -22,13 +23,14 @@ enum
Kesc = Kspec|0x1b, Kesc = Kspec|0x1b,
Kup = Kspec|0x52, Kup = Kspec|0x52,
Kdown = Kspec|0x54, Kdown = Kspec|0x54,
Kshift = Kspec|0xe1, Kmodfirst = Kspec|0xe1,
Ksuper = Kspec|0xeb, Kmodlast = Kspec|0xee,
Mshift = 1<<0, Mshift = 1<<0,
Mctrl = 1<<2, Mctrl = 1<<2,
Malt = 1<<3, Malt = 1<<3,
Msuper = 1<<6, Msuper = 1<<6,
Mmask = Mshift|Mctrl|Malt|Msuper,
Ipcreqsz = 6, Ipcreqsz = 6,
Ipclensz = 2, Ipclensz = 2,

View File

@@ -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 ζ
~ ≈
~= ≈
따 👍
따봉 👍
미 😄 😊
미소 😄 😊
방 😊
방긋 😊
사 ❤️
사랑 ❤️
스 😀 😄
스마 😀 😄
스마일 😀 😄
엄 👍
엄지 👍
웃 😀 😄 😂
웃겨 😂
웃다 😀
웃음 😀 😄 😂
좋 👍
좋아 👍
좋아요 👍
찬 👍
찬성 👍
폭 😂
폭소 😂
하 ❤️
하트 ❤️

View File

@@ -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 ζ
~ ~
~= ≈

View File

@@ -1,100 +1,111 @@
!! ⚠ # Result first, followed by one or more TAB-separated aliases.
!= ≠ ⚠ !!
** **
+- ± ± +-
-> ->
.. · · ..
... ...
./ ÷ ÷ ./
:( :(
:) :)
<- ←
<= ≤ # Bare 1-9 choose prefix candidates; keep digit aliases in matching slots.
<> ≠ ← <-
== ≡ ≤ <=
=> ⇒ ♥ <3
>= ≥ ≠ <> !=
^( ⁽ ≡ ==
^) ⁾ ⇒ =>
^+ ⁺ ≥ >=
^- ⁻ ¹ ^1
^= ⁼ ² ^2
_( ₍ ³ ^3
_) ₎ ⁴ ^4
_+ ₊ ⁵ ^5
_- ₋ ⁶ ^6
_= ₌ ⁷ ^7
~= ≈ ⁸ ^8
^0 ⁰ ⁹ ^9
_0 ₀ ₁ _1
^1 ¹ ₂ _2
_1 ₁ ₃ _3
^2 ² ₄ _4
_2 ₂ ₅ _5
<3 ♥ ₆ _6
^3 ³ ₇ _7
_3 ₃ ₈ _8
^4 ⁴ ₉ _9
_4 ₄
^5 ⁵ ⁽ ^(
_5 ₅ ⁾ ^)
^6 ⁶ ⁺ ^+
_6 ₆ ⁻ ^-
^7 ⁷ ⁼ ^=
_7 ₇ ₍ _(
^8 ⁸ ₎ _)
_8 ₈ ₊ _+
^9 ⁹ ₋ _-
_9 ₉ ₌ _=
_a ₐ ≈ ~=
alpha α ⁰ ^0
beta β ₀ _0
chi χ ₐ _a
deg ° α alpha
delta δ β beta
De Δ χ chi
dn ↓ ° deg
_e ₑ δ delta
eps ε Δ De
eta η ↓ dn
fuck 凸 ₑ _e
gamma γ ε eps
Ga Γ η eta
^i ⁱ 凸 fuck
II ∫ γ gamma
inf ∞ Γ Ga
iota ι ⁱ ^i
kappa κ ∫ II
lambda λ ∞ inf
La Λ ι iota
mul × κ kappa
mu μ λ lambda
^n ⁿ Λ La
nu ν × mul
omega ω μ mu
Om Ω ⁿ ^n
oo ● ν nu
_o ₒ ω omega
phi φ Ω Om
Ph Φ ● oo
pi π ₒ _o
Pi Π φ phi
PP ∏ Φ Ph
psi ψ π pi
Ps Ψ Π Pi
rho ρ ∏ PP
sigma σ ψ psi
Si Σ Ψ Ps
sq √ ρ rho
SS ∑ σ sigma
tau τ Σ Si
theta θ √ sq
Th Θ ∑ SS
up ↑ τ tau
ups υ θ theta
vv ✓ Θ Th
xi ξ ↑ up
Xi Ξ υ ups
xx ✗ ✓ vv
_x ₓ ξ xi
zeta ζ Ξ 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

View File

@@ -1,39 +1,80 @@
#!/bin/sh #!/usr/bin/env python3
"""Write emoji.dict to stdout from a result-first UTF-8 TSV file."""
awk ' import sys
BEGIN { FS = "\t" } import unicodedata
/^$/ { next } from pathlib import Path
{
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
awk '
BEGIN { FS = "\t" } SOURCE = Path(__file__).with_name("emoji.src")
/^$/ { next } MAXRUNES = 64
{
key = $1
val = $2 def fold(s):
for(i = 1; i <= length(key); i++){ s = "".join(chr(ord(c) + 32) if "A" <= c <= "Z" else c for c in s)
p = substr(key, 1, i) return unicodedata.normalize("NFC", s)
dict[p] = dict[p] " " val
}
} def hascontrol(s):
END { return any(unicodedata.category(c) == "Cc" for c in s)
for(p in dict){
sub(/^ /, "", dict[p])
print p "\t" dict[p] def read(path):
} entries = []
} with path.open(encoding="utf-8") as src:
' $1 | sort -k1,1 > emoji.dict for lineno, raw in enumerate(src, 1):
line = raw.rstrip("\r\n")
if not line.strip():
continue
if "\t" not in line and 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 len(result) > MAXRUNES or hascontrol(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 len(alias) > MAXRUNES or hascontrol(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())

309
strans.c
View File

@@ -3,8 +3,18 @@
static Im im; static Im im;
static int popup = 0; static int popup = 0;
static int visible = 0;
static void dictqmap(Im*); static void dictqmap(Im*);
typedef struct Search Search;
struct Search
{
int on;
Str raw;
Str text;
};
static Search search;
static void static void
backmap(Im *im) backmap(Im *im)
{ {
@@ -16,7 +26,7 @@ Lang langs[] = {
{LangJP, "hira", "kanji", transmap, backmap, dictqmap, nil, nil}, {LangJP, "hira", "kanji", transmap, backmap, dictqmap, nil, nil},
{LangJPK, "kata", "kanji", transmap, backmap, dictqmap, nil, nil}, {LangJPK, "kata", "kanji", transmap, backmap, dictqmap, nil, nil},
{LangKO, "hangul", nil, transko, backko, 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}, {LangVI, "telex", nil, transvi, backvi, dictqmap, nil, nil},
}; };
int nlang = nelem(langs); int nlang = nelem(langs);
@@ -43,14 +53,17 @@ setkouho(Dictres *res)
static void static void
show(void) show(void)
{ {
static Drawcmd old;
Drawcmd dc; Drawcmd dc;
Str *pre;
int i, first, n; int i, first, n;
if(!popup)
return;
sclear(&dc.pre); sclear(&dc.pre);
if(!mapget(im.l->map, &im.pre, &dc.pre)) pre = search.on ? &search.text : &im.pre;
dc.pre = 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; first = im.sel >= Maxdisp ? im.sel - Maxdisp + 1 : 0;
n = im.nkouho - first; n = im.nkouho - first;
if(n > Maxdisp) n = Maxdisp; if(n > Maxdisp) n = Maxdisp;
@@ -58,7 +71,13 @@ show(void)
dc.sel = im.sel >= 0 ? im.sel - first : -1; dc.sel = im.sel >= 0 ? im.sel - first : -1;
for(i = 0; i < n; i++) for(i = 0; i < n; i++)
dc.kouho[i] = im.kouho[first + i]; dc.kouho[i] = im.kouho[first + i];
chansend(drawc, &dc); if(dc.pre.n == 0 && n == 0 && !visible)
return;
visible = dc.pre.n != 0 || n != 0;
/* imthread is the sole producer; keep only the newest pending draw. */
while(channbrecv(drawc, &old) > 0)
;
channbsend(drawc, &dc);
} }
static void static void
@@ -110,7 +129,7 @@ commit(Str *com)
{ {
Str val; 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); sappend(com, &val);
else else
sappend(com, &im.pre); sappend(com, &im.pre);
@@ -209,13 +228,248 @@ transmap(Im *im, Rune c)
return e; return e;
} }
static Rune
ctrlkey(Rune c)
{
if(c >= 'A' && c <= 'Z')
c += 'a' - 'A';
if(c >= 'a' && c <= 'z')
return c - 'a' + 1;
return c;
}
static int
ismodkey(u32int ks)
{
return ks >= Kmodfirst && ks <= Kmodlast;
}
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(ismodkey(ks))
return 0;
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(n >= 0 && n < Maxdisp && off + n < im.nkouho && mod == 0){
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 static int
keystroke(u32int ks, u32int mod, Str *com) keystroke(u32int ks, u32int mod, Str *com)
{ {
int n, off; int n, off;
Rune c;
if(ks == Ksuper || ks == Kshift) if(ismodkey(ks))
return 1; return 0;
if(search.on)
return searchkey(ks, mod, com);
if(ks == Kdown || ks == Kup){ if(ks == Kdown || ks == Kup){
if(im.nkouho == 0) if(im.nkouho == 0)
return 0; return 0;
@@ -271,14 +525,18 @@ keystroke(u32int ks, u32int mod, Str *com)
return 0; return 0;
} }
if(mod & Mctrl){ if(mod & Mctrl){
reset(); c = ctrlkey(ks);
if(ks >= 'a' && ks <= 'z') if(c == LangEMOJI){
ks -= 'a' - 1; startsearch(com);
if(ks == 0x10){
popup = !popup;
return 1; return 1;
} }
if(setlang(ks)) if(c == 0x10){
popup = !popup;
show();
return 1;
}
reset();
if(setlang(c))
return 1; return 1;
return 0; return 0;
} }
@@ -305,6 +563,8 @@ init(void)
{ {
memset(&im, 0, sizeof(im)); memset(&im, 0, sizeof(im));
im.l = getlang(LangEN); im.l = getlang(LangEN);
memset(&search, 0, sizeof search);
visible = 0;
} }
static void static void
@@ -316,15 +576,25 @@ imhandlekey(Keyreq *kr)
sclear(&res.preedit); sclear(&res.preedit);
res.eaten = keystroke(kr->ks, kr->mod, &res.commit); res.eaten = keystroke(kr->ks, kr->mod, &res.commit);
if(kr->want) if(kr->want)
res.preedit = im.pre; res.preedit = search.on ? search.text : im.pre;
chansend(kr->reply, &res); chansend(kr->reply, &res);
} }
static void
dictresult(Dictres *res)
{
if(search.on || res->lang != im.l->lang ||
scmp(&res->key, &im.pre) != 0)
return;
setkouho(res);
show();
}
void void
imthread(void*) imthread(void*)
{ {
static Dictres res;
Keyreq kr; Keyreq kr;
Dictres res;
Alt alts[] = { Alt alts[] = {
{keyc, &kr, CHANRCV, nil}, {keyc, &kr, CHANRCV, nil},
{dictresc, &res, CHANRCV, nil}, {dictresc, &res, CHANRCV, nil},
@@ -339,10 +609,7 @@ imthread(void*)
imhandlekey(&kr); imhandlekey(&kr);
break; break;
case 1: case 1:
if(scmp(&res.key, &im.pre) == 0){ dictresult(&res);
setkouho(&res);
show();
}
break; break;
} }
} }

View File

@@ -5,9 +5,9 @@ LIBS = -lthread -lbio
PROG = unit_test PROG = unit_test
TESTSRC = unit_test.c test_util.c str_test.c hash_test.c trie_test.c \ 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 ko_test.c vi_test.c engine_test.c dict_test.c ipc_test.c
TESTOBJ = $(TESTSRC:.c=.o) TESTOBJ = $(TESTSRC:.c=.o)
PARENTSRC = str.c hash.c trie.c dict.c ko.c vi.c PARENTSRC = str.c hash.c trie.c dict.c ko.c vi.c ipc.c
PARENTOBJ = $(PARENTSRC:%.c=unit_%.o) PARENTOBJ = $(PARENTSRC:%.c=unit_%.o)
OBJS = $(TESTOBJ) $(PARENTOBJ) OBJS = $(TESTOBJ) $(PARENTOBJ)

View File

@@ -25,6 +25,7 @@ dictionary_candidates(struct ct *t)
dictlookup(&req, &res); dictlookup(&req, &res);
if(!CT_EQ_INT(t, 2, res.nkouho)) if(!CT_EQ_INT(t, 2, res.nkouho))
goto cleanup; goto cleanup;
CT_EQ_INT(t, LangJP, res.lang);
CT_EQ_INT(t, 0, scmp(&req.pre, &res.key)); CT_EQ_INT(t, 0, scmp(&req.pre, &res.key));
checkstr(t, "candidate 1", "候補1", &res.kouho[0]); checkstr(t, "candidate 1", "候補1", &res.kouho[0]);
checkstr(t, "candidate 2", "候補2", &res.kouho[1]); checkstr(t, "candidate 2", "候補2", &res.kouho[1]);
@@ -44,6 +45,7 @@ dictionary_candidates(struct ct *t)
dictlookup(&req, &res); dictlookup(&req, &res);
if(!CT_EQ_INT(t, Maxkouho, res.nkouho)) if(!CT_EQ_INT(t, Maxkouho, res.nkouho))
goto cleanup; goto cleanup;
CT_EQ_INT(t, LangJP, res.lang);
CT_EQ_INT(t, 0, scmp(&req.pre, &res.key)); CT_EQ_INT(t, 0, scmp(&req.pre, &res.key));
checkstr(t, "first capped candidate", "c00", &res.kouho[0]); checkstr(t, "first capped candidate", "c00", &res.kouho[0]);
checkstr(t, "last capped candidate", "c31", &res.kouho[31]); checkstr(t, "last capped candidate", "c31", &res.kouho[31]);
@@ -81,6 +83,7 @@ dictionary_misses_clear_result(struct ct *t)
if(res.nkouho != 0) if(res.nkouho != 0)
CT_ERRORF(t, "%s: want 0 candidates, got %d", CT_ERRORF(t, "%s: want 0 candidates, got %d",
cases[i].name, res.nkouho); cases[i].name, res.nkouho);
CT_EQ_INT(t, LangJP, res.lang);
checkstr(t, cases[i].name, cases[i].pre, &res.key); checkstr(t, cases[i].name, cases[i].pre, &res.key);
} }
hmapfree(lang->dict); hmapfree(lang->dict);

View File

@@ -255,3 +255,449 @@ engine_language_switch_state(struct ct *t)
} }
vi->map = saved; 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(32);
setemoji(&l->dict, "a", "A B");
setemoji(&l->dict, "", "B C");
setemoji(&l->dict, "alpha", "α");
setemoji(&l->dict, "smil", "😀 😄");
setemoji(&l->dict, "smile", "😀 😄");
setemoji(&l->dict, "웃음", "😀 😄 😂");
setemoji(&l->dict, "^", "¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁽");
setemoji(&l->dict, "^0", "");
setemoji(&l->dict, "_", "₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ₍");
setemoji(&l->dict, "<", "← ≤ ♥ ≠");
setemoji(&l->dict, "many",
"c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11 c12");
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, 1);
sclear(&com);
CT_CHECK(t, keystroke('p', Mctrl, &com));
CT_EQ_INT(t, 0, popup);
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);
show();
CT_CHECK(t, draindraw(&dc) > 0);
CT_EQ_INT(t, 0, dc.pre.n);
CT_EQ_INT(t, 0, dc.nkouho);
}
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));
CT_CHECK(t, keystroke('a', 0, &com));
checkstr(t, "raw query wins", "a", &search.text);
CT_EQ_INT(t, 3, im.nkouho);
checkstr(t, "raw candidate", "A", &im.kouho[0]);
checkstr(t, "deduplicated candidate", "B", &im.kouho[1]);
checkstr(t, "local candidate", "C", &im.kouho[2]);
CT_CHECK(t, draindraw(&dc) > 0);
checkstr(t, "prefix popup query", "a", &dc.pre);
CT_EQ_INT(t, 3, dc.nkouho);
CT_CHECK(t, keystroke(Kesc, 0, &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', 0, &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, "SMILE", &com)){
checkstr(t, "folded English alias", "smile", &search.text);
CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke(Kesc, 0, &com));
}
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_digit_aliases(struct ct *t)
{
static const struct {
char *name;
Rune prefix;
Rune digit;
char *want;
} cases[] = {
{ "^1", '^', '1', "¹" },
{ "_2", '_', '2', "" },
{ "<3", '<', '3', "" },
};
Emojifix f;
Str com;
int i;
emojibegin(&f, 0);
for(i = 0; i < nelem(cases); i++){
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
CT_CHECK(t, keystroke(cases[i].prefix, Mshift, &com));
CT_CHECK(t, im.nkouho >= cases[i].digit - '0');
CT_CHECK(t, keystroke(cases[i].digit, 0, &com));
checkstr(t, cases[i].name, cases[i].want, &com);
CT_CHECK(t, !search.on);
CT_EQ_INT(t, LangKO, im.l->lang);
}
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
CT_CHECK(t, keystroke('^', Mshift, &com));
CT_CHECK(t, keystroke('0', 0, &com));
checkstr(t, "zero query", "^0", &search.text);
checkstr(t, "zero does not commit", "", &com);
CT_EQ_INT(t, 1, im.nkouho);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "zero alias", "", &com);
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "smile", &com)){
CT_CHECK(t, keystroke('9', 0, &com));
checkstr(t, "missing number query", "smile9", &search.text);
checkstr(t, "missing number commit", "", &com);
CT_CHECK(t, search.on);
CT_EQ_INT(t, 0, im.nkouho);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "missing number text", "smile9", &com);
}
emojiend(&f);
}
void
engine_emoji_navigation(struct ct *t)
{
Drawcmd dc = {0};
Emojifix f;
Str com;
int i;
emojibegin(&f, 0);
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "many", &com)){
CT_EQ_INT(t, 12, im.nkouho);
for(i = 0; i < Maxdisp+1; i++)
CT_CHECK(t, keystroke(Kdown, 0, &com));
CT_EQ_INT(t, Maxdisp, im.sel);
CT_CHECK(t, draindraw(&dc) > 0);
checkstr(t, "scrolled first row", "c2", &dc.kouho[0]);
checkstr(t, "scrolled ninth row", "c10", &dc.kouho[8]);
CT_EQ_INT(t, 8, dc.sel);
CT_CHECK(t, keystroke('1', 0, &com));
checkstr(t, "scrolled row one", "c2", &com);
}
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "many", &com)){
for(i = 0; i < 12; i++)
CT_CHECK(t, keystroke(Kdown, 0, &com));
CT_EQ_INT(t, 11, im.sel);
CT_CHECK(t, keystroke('9', 0, &com));
checkstr(t, "scrolled row nine", "c12", &com);
}
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "smile", &com)){
CT_CHECK(t, keystroke(Ktab, 0, &com));
CT_EQ_INT(t, 0, im.sel);
CT_CHECK(t, keystroke(Kdown, 0, &com));
CT_EQ_INT(t, 1, im.sel);
CT_CHECK(t, keystroke(Kup, 0, &com));
CT_EQ_INT(t, 0, im.sel);
CT_CHECK(t, keystroke(Ktab, Mshift, &com));
CT_EQ_INT(t, 1, im.sel);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "navigated candidate", "😄", &com);
}
emojiend(&f);
}
void
engine_emoji_preedit_languages(struct ct *t)
{
Emojifix f;
Str com, shown;
int p;
emojibegin(&f, 0);
init();
im.l = getlang(LangJP);
sclear(&com);
if(typekeys(t, "ka", &com)){
CT_CHECK(t, !keystroke(Kmodfirst+2, 0, &com));
checkstr(t, "modifier preserves Japanese", "ka", &im.pre);
CT_CHECK(t, keystroke('E', Mctrl|Mshift, &com));
checkstr(t, "committed Japanese preedit", "", &com);
CT_CHECK(t, search.on);
CT_EQ_INT(t, LangJP, im.l->lang);
CT_CHECK(t, keystroke('a', 0, &com));
p = popup;
CT_CHECK(t, !keystroke(Kmodfirst+2, 0, &com));
CT_CHECK(t, search.on);
checkstr(t, "modifier preserves search", "a", &search.text);
CT_CHECK(t, keystroke('P', Mctrl|Mshift, &com));
CT_EQ_INT(t, !p, popup);
CT_CHECK(t, search.on);
CT_CHECK(t, !keystroke(Kmodfirst+2, 0, &com));
CT_CHECK(t, keystroke('E', Mctrl|Mshift, &com));
CT_CHECK(t, !search.on);
sclear(&com);
if(typekeys(t, "na", &com)){
shown = shownpre(&im);
checkstr(t, "Japanese resumed", "", &shown);
}
}
init();
im.l = getlang(LangKO);
sclear(&com);
if(typekeys(t, "rk", &com)){
CT_CHECK(t, keystroke('e', Mctrl, &com));
checkstr(t, "committed Korean preedit", "", &com);
if(typekeys(t, "smile", &com)){
CT_CHECK(t, keystroke('1', 0, &com));
checkstr(t, "Korean selection", "가😀", &com);
sclear(&com);
if(typekeys(t, "rk", &com))
checkstr(t, "Korean resumed", "", &im.pre);
}
}
init();
im.l = &testvi;
sclear(&com);
if(typekeys(t, "as", &com)){
CT_CHECK(t, keystroke('e', Mctrl, &com));
checkstr(t, "committed Telex preedit", "á", &com);
CT_CHECK(t, keystroke(Kesc, 0, &com));
CT_CHECK(t, !search.on);
CT_EQ_PTR(t, &testvi, im.l);
sclear(&com);
if(typekeys(t, "as", &com)){
shown = shownpre(&im);
checkstr(t, "Telex resumed", "á", &shown);
checkstr(t, "Telex raw resumed", "as", &im.raw);
}
}
emojiend(&f);
}
void
engine_emoji_start_and_unknown(struct ct *t)
{
Emojifix f;
Str com;
char want[Maxrunes+1];
int i;
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);
}
}
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "xyz", &com)){
CT_CHECK(t, !keystroke(Kspec|0x51, 0, &com));
checkstr(t, "special commits shown query", "xyz", &com);
CT_CHECK(t, !search.on);
CT_EQ_INT(t, LangKO, im.l->lang);
}
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
for(i = 0; i < Maxrunes; i++)
CT_CHECK(t, keystroke('q', 0, &com));
CT_EQ_INT(t, Maxrunes, search.raw.n);
CT_CHECK(t, search.on);
CT_CHECK(t, !keystroke('z', 0, &com));
memset(want, 'q', Maxrunes);
want[Maxrunes] = '\0';
checkstr(t, "bounded query commit", want, &com);
CT_CHECK(t, !search.on);
emojiend(&f);
}
void
engine_emoji_dictionary_identity(struct ct *t)
{
Dictres res;
Emojifix f;
emojibegin(&f, 0);
im.pre = mkstr("same");
im.kouho[0] = mkstr("old");
im.nkouho = 1;
im.sel = 0;
memset(&res, 0, sizeof res);
res.key = im.pre;
res.kouho[0] = mkstr("wrong");
res.nkouho = 1;
res.lang = LangJP;
dictresult(&res);
CT_EQ_INT(t, 1, im.nkouho);
checkstr(t, "wrong language ignored", "old", &im.kouho[0]);
res.lang = LangKO;
res.key = mkstr("other");
dictresult(&res);
CT_EQ_INT(t, 1, im.nkouho);
checkstr(t, "wrong preedit ignored", "old", &im.kouho[0]);
res.key = im.pre;
search.on = 1;
dictresult(&res);
CT_EQ_INT(t, 1, im.nkouho);
checkstr(t, "search response ignored", "old", &im.kouho[0]);
search.on = 0;
res.kouho[0] = mkstr("right");
dictresult(&res);
CT_EQ_INT(t, 1, im.nkouho);
CT_EQ_INT(t, -1, im.sel);
checkstr(t, "matching response accepted", "right", &im.kouho[0]);
emojiend(&f);
}

15
tests/ipc_test.c Normal file
View File

@@ -0,0 +1,15 @@
#include "test.h"
void
ipc_masks_modifiers(struct ct *t)
{
uchar buf[Ipcreqsz];
u32int key, mod;
int want;
ipcpackreq(buf, 1, Mctrl|Malt|(1<<1)|(1<<4)|(1<<7), 0x1f642);
ipcunpackreq(buf, &want, &mod, &key);
CT_EQ_INT(t, 1, want);
CT_EQ_UINT(t, Mctrl|Malt, mod);
CT_EQ_UINT(t, 0x1f642, key);
}

85
tests/mkemoji_test.py Normal file
View File

@@ -0,0 +1,85 @@
#!/usr/bin/env python3
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MKEMOJI = ROOT / "map" / "mkemoji"
def generate(source=None):
args = [sys.executable, "-B", str(MKEMOJI)]
if source is not None:
args.append(str(source))
return subprocess.run(args, capture_output=True, text=True, check=False)
def table(output):
return dict(line.split("\t", 1) for line in output.splitlines())
class MkemojiTest(unittest.TestCase):
def source(self, text):
tmp = tempfile.TemporaryDirectory()
path = Path(tmp.name) / "emoji.src"
path.write_text(text, encoding="utf-8")
self.addCleanup(tmp.cleanup)
return path
def test_production_digit_slots(self):
result = generate()
self.assertEqual(result.returncode, 0, result.stderr)
data = table(result.stdout)
self.assertEqual(data["^"].split()[:9], list("¹²³⁴⁵⁶⁷⁸⁹"))
self.assertEqual(data["_"].split()[:9], list("₁₂₃₄₅₆₇₈₉"))
self.assertEqual(data["<"].split()[2], "")
self.assertEqual(data["^1"], "¹")
self.assertEqual(data["_2"], "")
self.assertEqual(data["<3"], "")
def test_fold_normalize_and_exact_first(self):
source = self.source(
"β\tALPHABET\n"
"α\talpha\n"
"e\u0301\tE\u0301\n"
"#\thash\n"
)
first = generate(source)
second = generate(source)
self.assertEqual(first.returncode, 0, first.stderr)
self.assertEqual(first.stdout, second.stdout)
data = table(first.stdout)
self.assertEqual(data["alpha"].split(), ["α", "β"])
self.assertEqual(data["al"].split(), ["β", "α"])
self.assertEqual(data["é"], "é")
self.assertEqual(data["hash"], "#")
def test_rejects_malformed_and_oversized_rows(self):
bad = [
"x\n",
"bad result\ta\n",
"x\t\n",
"x\t a\n",
"\0\ta\n",
"x\ta\0b\n",
"x" * 65 + "\ta\n",
"x\t" + "a" * 65 + "\n",
]
for text in bad:
with self.subTest(text=repr(text)):
result = generate(self.source(text))
self.assertEqual(result.returncode, 1)
def test_accepts_runtime_boundary(self):
source = self.source("x" * 64 + "\t" + "a" * 64 + "\n")
result = generate(source)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("a" * 64 + "\t" + "x" * 64, result.stdout)
if __name__ == "__main__":
unittest.main()

View File

@@ -33,7 +33,15 @@ void engine_selects_visible_candidate(struct ct*);
void engine_commit_contract(struct ct*); void engine_commit_contract(struct ct*);
void engine_language_switch_state(struct ct*); void engine_language_switch_state(struct ct*);
void engine_telex_history_bound(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_digit_aliases(struct ct*);
void engine_emoji_navigation(struct ct*);
void engine_emoji_preedit_languages(struct ct*);
void engine_emoji_start_and_unknown(struct ct*);
void engine_emoji_dictionary_identity(struct ct*);
void dictionary_candidates(struct ct*); void dictionary_candidates(struct ct*);
void dictionary_misses_clear_result(struct ct*); void dictionary_misses_clear_result(struct ct*);
void ipc_masks_modifiers(struct ct*);
#endif #endif

View File

@@ -80,8 +80,16 @@ static const struct ct_test tests[] = {
{ "engine/commit-contract", engine_commit_contract }, { "engine/commit-contract", engine_commit_contract },
{ "engine/language-switch-state", engine_language_switch_state }, { "engine/language-switch-state", engine_language_switch_state },
{ "engine/telex-history-bound", engine_telex_history_bound }, { "engine/telex-history-bound", engine_telex_history_bound },
{ "engine/emoji-single-candidate", engine_emoji_single_candidate },
{ "engine/emoji-queries", engine_emoji_queries },
{ "engine/emoji-digit-aliases", engine_emoji_digit_aliases },
{ "engine/emoji-navigation", engine_emoji_navigation },
{ "engine/emoji-preedit-languages", engine_emoji_preedit_languages },
{ "engine/emoji-start-and-unknown", engine_emoji_start_and_unknown },
{ "engine/emoji-dictionary-identity", engine_emoji_dictionary_identity },
{ "dict/candidates", dictionary_candidates }, { "dict/candidates", dictionary_candidates },
{ "dict/misses-clear-result", dictionary_misses_clear_result }, { "dict/misses-clear-result", dictionary_misses_clear_result },
{ "ipc/masks-modifiers", ipc_masks_modifiers },
}; };
void void

21
win.c
View File

@@ -90,16 +90,18 @@ fill(u32int *buf, int n, u32int color)
static void static void
drawkouho(Drawcmd *dc, int first, int n, int w, int h) drawkouho(Drawcmd *dc, int first, int n, int w, int h)
{ {
int sely, y, i; int npre, sely, y, i;
Str *s; Str *s;
fill(img, w * h, Colbg); fill(img, w * h, Colbg);
npre = dc->pre.n != 0;
if(npre)
drawstr(img, 0, 0, dc->pre.r, dc->pre.n, w, h); drawstr(img, 0, 0, dc->pre.r, dc->pre.n, w, h);
if(dc->sel >= 0){ if(dc->sel >= 0 && dc->sel < n){
sely = Fontsz + dc->sel * Fontsz; sely = (npre + dc->sel) * Fontsz;
fill(img + sely * w, Fontsz * w, Colsel); 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]; s = &dc->kouho[first+i];
putfont(img, w, h, 0, y, '1' + i + Asciitofull); putfont(img, w, h, 0, y, '1' + i + Asciitofull);
drawstr(img, 2*Fontsz, y, s->r, s->n, w, h); drawstr(img, 2*Fontsz, y, s->r, s->n, w, h);
@@ -124,13 +126,18 @@ winhide(void)
static void static void
winshow(Drawcmd *dc) winshow(Drawcmd *dc)
{ {
int px, py, w, h, i, n, maxw; int npre, px, py, w, h, i, n, maxw;
u32int vals[4]; u32int vals[4];
xcb_query_pointer_reply_t *ptr; xcb_query_pointer_reply_t *ptr;
xcb_query_pointer_cookie_t cookie; 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); cookie = xcb_query_pointer(conn, scr->root);
n = dc->nkouho;
maxw = dc->pre.n; maxw = dc->pre.n;
for(i = 0; i < n; i++) for(i = 0; i < n; i++)
maxw = max(maxw, dc->kouho[i].n); maxw = max(maxw, dc->kouho[i].n);
@@ -140,7 +147,7 @@ winshow(Drawcmd *dc)
px = ptr->root_x + 10; px = ptr->root_x + 10;
py = ptr->root_y + 10; py = ptr->root_y + 10;
free(ptr); free(ptr);
vals[3] = h = (n + 1) * Fontsz; vals[3] = h = (n + npre) * Fontsz;
vals[2] = w = (maxw + 3) * Fontsz; vals[2] = w = (maxw + 3) * Fontsz;
vals[1] = py = max(0, min(py, scr->height_in_pixels - h)); vals[1] = py = max(0, min(py, scr->height_in_pixels - h));
vals[0] = px = max(0, min(px, scr->width_in_pixels - w)); vals[0] = px = max(0, min(px, scr->width_in_pixels - w));