Compare commits

...

53 Commits

Author SHA1 Message Date
544ed835b7 gtk: find the module directory through the GTK runtime again
A module built in the container is installed on a host that has no GTK
development files, and there pkg-config knows nothing: dropping the
runtime query left `doas make install` with nowhere to put the module.
gtk-query-immodules-3.0 answers where GTK looks, so ask it when
pkg-config cannot, as before, and say so in the README.
2026-08-17 14:37:57 +09:00
ae7dbae993 engine: Shift on the okurigana asks for that reading, as SKK does
kaku offers the twenty-two readings of かく before 書く, because the SKK
dictionary keys a verb by its stem and gives no frequency to rank the two
lists by.  SKK's own answer is the shift key: kaKu says where the
okurigana starts, and that split now comes first — the reading's own
candidates still follow it.  Caps Lock sends no Shift, so it marks
nothing, and a word typed without Shift is unchanged.
2026-08-17 12:53:12 +09:00
2e53627b7d data(hanja): words, not only syllables
The import kept only readings of one syllable, so the Hanja search could
convert 한 but never 한자, 학교, or 대한민국 — the conversion every other
Korean input method offers.  libhangul's table has 187k readings; both
scripts now keep them all, and the search finds a word as readily as a
syllable.  The daemon pays for it: 24 MB instead of 12, and 170 ms to
start instead of 30.
2026-08-17 12:50:05 +09:00
3527bcd489 trie: one node per rune, children in rune order, and the last path kept
The trie walked a key byte by byte, so a Hangul syllable cost three
nodes and every lookup first spelled its key back into UTF-8.  It walks
runes now: a third of the nodes for Korean data, and no buffer in the
lookup.  Children are kept in rune order, so both the search and the
insert stop early, and a trie remembers the path of the last key put,
which a file sorted by key — hanja.dict is one — walks straight down.
2026-08-17 12:50:05 +09:00
6a6749e824 engine: a mode switch shows the mode it switched to
Ctrl+S, Ctrl+N and the rest changed the mode with nothing to see; the
next key was the only way to tell.  The popup now shows A, 한, あ, ア, or
ă until a key is typed, beside the ☺ and 漢 a search already shows.
2026-08-17 12:38:00 +09:00
c2aec44aa9 engine: the digits type until the candidate list is engaged
Every complete reading shows its candidates, so 1-9 were candidate picks
in the middle of composing: ka then 5 committed 家, and heya then 2
typed a 2 the application never asked for.  They pick only once Space,
Tab, or an arrow has chosen a candidate, as in Mozc; before that they are
ordinary keys.  A search is unchanged: its digits pick from the start.
2026-08-17 12:36:23 +09:00
cf8eca2ff1 docs: fewer claims, the right dependencies 2026-08-17 12:24:34 +09:00
d129f406d5 test: one Xvfb launcher and one daemon for the live tests
The XIM test carried its own Xvfb and daemon spawner, its own child
struct, log files and process-group teardown, and the GTK test a second
Xvfb launcher, because live.c's startdaemon always unset DISPLAY.  Live
now carries the display its own startxvfb reports, startdaemon passes it
on, and both tests use the shared pair: 300 lines fewer, one place that
knows how a child is started, watched, and stopped.
2026-08-17 12:24:12 +09:00
e54783dc92 test: Telex goes in the language table like every other map
The unit suite built a Lang of its own for Telex and loaded telex.map
into it, so the tests that drive the real table had to swap the map in
and out around themselves.  testmapinit loads it where it belongs.
2026-08-17 12:20:39 +09:00
3a90826238 gtk, docs: the module goes where GTK says, and the README says less
The install target guessed the module directory from whichever immodule
was already installed when pkg-config had nothing to say — a guess that
finds nothing on a fresh system and needed seven lines of README to
explain.  It uses GTK's own pkg-config variables now, or the
GTK_MODULE_DIR a packager sets.  The README also loses a negation about
a font argument that no longer exists and two paragraphs that repeat the
dependency list.
2026-08-17 12:19:28 +09:00
0ba7dc7f50 build, data: the Greek capitals by name; one way to run python3; bench.sh checks its own binary
Δ Γ Λ Ω Φ Ψ Σ Θ answered only to De, Ga, La, Om, Ph, Ps, Si, Th, while
their small letters answered to delta, gamma and the rest; they answer to
the names too now.  mkemoji and cldr2emoji write UTF-8 whatever the
locale, as the other generators already did; verify-map calls python3 the
one way; the tests' include path drops a directory nothing includes
through; and bench.sh says which binary is missing instead of blaming the
daemon ten seconds later.
2026-08-17 12:17:18 +09:00
ef7fb627c6 compose: one Compose table for the XIM and IBus frontends
An IBus client throws away a dead key its engine did not take — the GTK
module's own comment says so, and Qt does the same — so é and ü were
lost in every IBus application, while XIM composed them with a table of
its own.  That table moves to compose.c, which both frontends now use;
xim.c is the shorter for it.

A finished sequence is text, not a key: the engine is asked to hand back
what it had pending, and the composed text follows it, so the composed
character can no longer land before the syllable typed before it.
2026-08-17 12:16:07 +09:00
a0e83f98c6 engine: a takeover keeps the text it took for the context it took it from
Two applications' focus events cross — ibus-daemon documents the case —
so the first key of the new one can arrive before the old one says it
lost focus.  The engine dropped whatever that context was composing; it
now keeps it and hands it back with the reset or release that follows,
which every frontend already commits.  Normal ordering is unchanged.
2026-08-17 12:07:19 +09:00
34c0c501aa engine: say what key 0 means 2026-08-17 12:04:12 +09:00
0f684ae6d3 gtk: a dead key hands the pending text back first
GtkIMContextSimple composes dead keys, and while it did the daemon still
held the syllable typed before them: the composed é was inserted first
and 하 reappeared after it.  A key the daemon does not take now resets
the composition when Simple begins composing on it — a modifier press,
which is also no key, leaves it alone.
2026-08-17 11:59:42 +09:00
c102c87d55 ibus: a key stands for the FocusIn a client may never send
A context that had not sent FocusIn had every key rejected outright, so
the input method was silently dead in a client that omits or delays it,
and in the documented case of two applications whose focus events cross.
ibus-daemon and fcitx5 both treat a key as focus; strans, which plays the
daemon here, now does too.  A release still does not focus, and the
engine's own owner rule is unchanged.
2026-08-17 11:58:08 +09:00
bfa919f623 ibus: name the address file as libibus does under Wayland
libibus looks for the file under WAYLAND_DISPLAY when a session has one,
DISPLAY only otherwise; strans always used DISPLAY, so in a Wayland
session it wrote <machine-id>-unix-0 while every IBus client looked for
<machine-id>-unix-wayland-0 and found nothing.  The machine id now comes
from D-Bus's copy first, as libibus reads it, and a host with neither
file gets libibus's own "machine-id" rather than a daemon that dies.
2026-08-17 11:53:40 +09:00
5a6f352d72 docs: where clients find the daemon 2026-08-17 10:59:21 +09:00
88ad5f7630 engine: the one capability is a flag
Cclientpreedit was a one-bit mask that every producer set as "want ?
Cclientpreedit : 0" and every consumer masked out again; nothing else
was ever going to join it.  Keyreq carries clientpre, an int that says
whether the client draws the preedit, and the engine and the XIM
context keep it under that name.
2026-08-17 10:57:15 +09:00
33023d7f51 ipc: a modifier keysym is no key
Shift, Control, and their kin were mapped to special keys that the
engine then had a range and a check to ignore, and the GTK module made a
round trip to the daemon for each press.  ipckeysym maps them to key 0,
which was already the "no key" every frontend and the engine skip;
Kmodfirst, Kmodlast, and ismodkey go.
2026-08-17 10:56:03 +09:00
46bb8b1d46 ibus: the InputContext interface as a table
onmsg matched the member name, then handleplain matched four of the
names again after checking their empty signature, and the handlers that
take arguments each re-checked theirs with a message of their own.  One
table of member, signature, and handler does the matching and the
checking once; the handlers read arguments a signature has already
vouched for.
2026-08-17 10:55:09 +09:00
82cb07eba2 docs: the input modes as they now are, in one piece
The paragraphs on candidates, keys, and searches had grown by patches;
they read as three: Japanese, Korean, and the searches.
2026-08-17 02:01:02 +09:00
b979afc3f7 xim: a spot is a baseline; a popup flipped above it clears the line
An XIM spot has no height, so a popup that flips above it at the bottom
of the screen ended on the baseline and covered the line being typed.
The spot now stands for the row above it, one popup row tall, as GTK
and IBus carets carry their line height; below the spot nothing moves.
2026-08-17 01:59:44 +09:00
dd389edc4f x11: xcb_aux_get_screen instead of two hand-rolled screen walks
win.c and xim.c each iterated the setup's roots to find the screen
xcb_connect had chosen; xcb-util, already linked through imdkit, has
xcb_aux_get_screen for that.
2026-08-17 01:58:30 +09:00
268b687ee6 engine: Backspace and Escape share their way back to the reading 2026-08-17 01:55:57 +09:00
e32cea3296 engine: Backspace deletes the last kana shown; Tab converts like Space
Backspace undid a keystroke: な became ん, かんじ became かんj, きゃ
became ky.  Like every Japanese IME it now deletes the last kana as
shown — な goes, かんじ becomes かん, きゃ becomes き — while a romaji
letter that never became kana still goes one at a time.  Tab stepped
through candidates in a search and committed in Japanese; it steps
through them there too, with Space, and Shift+Tab steps back.
2026-08-17 01:55:25 +09:00
a03fb05324 ibus: PostProcessKeyEvent for clients that process keys synchronously
GTK 4's IBus module, and GTK 3's with IBUS_ENABLE_SYNC_MODE=1, waits for
its ProcessKeyEvent reply and cannot take signals meanwhile; since IBus
1.5.29 it then reads the PostProcessKeyEvent property for what the key
produced.  strans answered that read with an error, so every key logged
a warning and a commit made by a key that passed on arrived after the
key: 한 and a comma became ,한.  A context that sets
EffectivePostProcessKeyEvent now has its commits and preedits held
during the key and handed over as the (yv) list IBus defines.
2026-08-17 01:53:51 +09:00
42568ab020 build, data: skk2ktrans keeps the SKK header; the recipe names every generator; an inert config line goes
An SKK dictionary's ";;" header carries its license notice, which
LICENSES/README.md relies on for kanji.dict, and skk2ktrans dropped
every ";;" line: a re-import as map/README describes lost the grant.
The leading comment block is kept now.  README's regenerate-and-verify
recipe omitted mktelex.py although verify-map checks its output; and
the Dockerfile set a shell variable named egrep in plan9's config that
9c never reads, calling egrep by name.
2026-08-17 01:49:45 +09:00
7c9e736996 xim: Compose results of any length, StatusNone styles; ibus: no cursor size check
A Compose sequence whose result is more than one character has no
keysym, so xkb_compose_state_get_one_sym gave nothing and the result was
lost; the UTF-8 the compose state holds is committed instead.  Clients
that ask for a StatusNone style — fcitx5 offers them — failed to create
an input context; the three preedit styles come in both status flavours
now.  IBus SetCursorLocation returned an error for a negative width or
height that nothing reads and no client ever waits for.
2026-08-17 01:49:06 +09:00
0cb395579f xim: one file at the top level, keymaplookup in it
xim/keymap.c held one 37-line function apart from xim.c, with its own
prototype typed by hand in xim.c and nelem spelt out because it avoided
dat.h — all so that a test could link it without imdkit, though the XIM
adapter test #includes xim.c whole anyway.  It lives in xim.c now, and
xim.c, the last file of its directory, sits beside ibus.c.
2026-08-17 01:47:40 +09:00
33848709a5 data(symbol): the symbols by their names too
II, PP, SS, sq, mul, vv, xx, oo, dn, inf, and deg were the only way to
∫ ∏ ∑ √ × ✓ ✗ ● ↓ ∞ °; integral, prod, sum, sqrt, times, check, cross,
circle, down, infinity, and degree find them as well.
2026-08-17 01:45:30 +09:00
47f9801ad8 engine: a search just begun shows itself
Ctrl+E and Ctrl+H gave no sign until a key produced candidates; with
nothing pending the popup even went away.  An empty search now shows
☺ or 漢 in the popup, so the user knows the next keys are a query.
2026-08-17 01:45:27 +09:00
fcdce7dc16 data(emoji): every Japanese alias in both kana
A query typed in the Hiragana mode is hiragana; CLDR's Japanese
keywords are mostly katakana (スマイル, ハート), so the one could not
find the other.  mkemoji now writes each alias that has kana in both
scripts.
2026-08-17 01:44:29 +09:00
0c4271b8b5 engine: an emoji query shows as typed and Space picks
The emoji search folded the keys for its lookup and then showed and
committed the folded copy: SMILE became smile in the text.  And it
showed the transliteration only while that alone matched, so a Korean
query flipped between 웃 and key soup as it grew.  Now the query shown
is the keys while they match anything, else what they type in the
current language, both as typed; only the lookups fold.  Space in a
search picks the highlighted result as Enter does, instead of adding a
space no alias needs.
2026-08-17 01:44:29 +09:00
bab40c95a7 popup: a border, a preedit as wide as its text, one place by the pointer, and no dying
The popup was a white box on the usually white text it covered, with
no edge to tell them apart; it has a border in the separator's colour.
A syllable being composed came in a bar twelve ems wide, wiping out the
line under it: only candidate rows share that steady width now, a
preedit alone hugs its text.  A popup placed by the pointer, when the
client sends no caret, was placed again on every key and so followed
the mouse; it stays where it came up.  Clicks on it no longer fall
through to the root window and its menu.  And one failed draw — a lost
pointer reply, a pixmap the server refused — ended the draw thread and
the popup for the rest of the session; only a dead connection does now.
2026-08-17 01:41:54 +09:00
87cb03fdf8 popup: no fallbacks for work areas no desktop has; text setup cannot fail
popuplayout had two refit stages for a work area shorter than one
padded row — 40 pixels — and popupdraw re-checked the layout it had
just been handed, against an Imgh that bounded no buffer any more.
Pango and Cairo abort rather than return nil, so textinit is void and
the layout is never nil; the stride Cairo computes for RGB24 is w*4 by
definition; and textdraw set the layout's width and ellipsis twice.
2026-08-17 01:36:16 +09:00
cd637be4ca ibus: die where startup fails; print the Plan 9 way; drop what said nothing
ibusinit unwound its server and un-registered its atexit handler on a
failure whose only sequel was ibusthread dying anyway; now each failure
dies with its own message, and the test that existed to walk that
unwind goes with it.  Also gone: an empty watch-toggle callback where
libdbus takes nil, Maxconns as a second name for Maxclients, fprintf
and strerror where the rest of the daemon says fprint and %r, USED()
where a parameter can simply be unnamed, a second findcontext() for the
Properties branch, and emitcommit() taking a connection and path apart
from the context that has both.  Ownerpoll lives once, in dat.h.
2026-08-17 01:34:06 +09:00
5faefd9a87 dict: lookups take a trie; a prefix search is its own function; no self-key rule
dictlookup took a Lang for two reasons that were not its business: to
know whether to walk below the key (the emoji dictionary) and to drop a
candidate equal to the key except there.  No dictionary lists its key
among its candidates — the rule was left from a first version that put
the reading first itself — so it goes, and the walk is dictprefix(),
called by the emoji search alone.
2026-08-17 01:31:51 +09:00
3d862bdc0d engine: keys outside a map pass on like any other; one candidate query; the caret rides on every key
Space and non-ASCII keys with a Korean or Telex syllable pending were
committed as text along with the syllable, while '.', ',' and digits
committed the syllable and passed on: one path for both now, through the
language's own trans, which commits and passes on.  Japanese Space is
its own key already.

dictqjp() knows which languages have candidates; its two callers no
longer choose between it and clearkouho().  Every request carries its
owner's caret as the frontend knows it, so a Keypress copies it whether
valid or not, and the XIM frontend need not send a Keycaret to say that
its spot went away.
2026-08-17 01:30:56 +09:00
97a838eda1 engine: the Japanese keyboard's own keys; keypad arrows move through candidates
半角/全角, ひらがな/カタカナ, 変換, and 無変換 committed the reading and did
nothing, like any function key.  They now stand for what a Japanese
keyboard means by them: toggling Japanese and English, switching kana
modes, converting (or turning Japanese on), and turning it off; the
Korean keys became a switch of the same shape.  The keypad's arrows
and page keys, with NumLock off, fold onto the plain ones as its Enter
and Tab already did.
2026-08-17 01:29:07 +09:00
1c8d9fac07 engine: Ctrl+Shift chords belong to the application
Ctrl+Shift+V switched to Vietnamese and ate the key, in every terminal
where it pastes; Ctrl+Shift+T and N opened no tab.  Only a plain
Ctrl+letter is a strans command now; a chord with Shift, Alt, or Super
commits what is pending and passes, as Alt and Super chords already did.
Caps Lock still works: the keysym's case never mattered.
2026-08-17 01:27:47 +09:00
97e6f2cf1a engine: one ASCII fold, commitim through impre, no second modifier check
Four places lower-cased ASCII by hand; commitim repeated impre's three
lines to compute the text it commits; and transition() began by
rejecting modifier keys that imhandlekey's keymeaningful() had already
turned away, kept alive only by tests calling transition() directly.
The test helper now goes through the same gate.
2026-08-17 01:24:56 +09:00
852e129f62 engine: the Hanja search composes from its seed, and Escape gives it back
Ctrl+H took the pending syllable as the query, but the keys typed after
it started a composition of their own: gk, Ctrl+H, s showed 하ㄴ, and r,
Ctrl+H, k showed ㄱㅏ.  transstr now composes from a pending text, so
the search goes on where the syllable left off.  Escape ended the search
and dropped the syllable it had taken; it puts it back as pending text.
2026-08-17 01:24:14 +09:00
1285476b41 korean: ㄳ from two consonants, 가 from a vowel typed first; index jamo by scanning
libhangul, and so fcitx5-hangul, joins two lone consonants into the
compound jong they make (rt → ㄳ, split again by a vowel: rtk → ㄱ사),
and by default reorders a vowel typed before its consonant (kr → 가);
strans committed the first jamo and started over.  Backspace on a lone
ㄳ leaves ㄱ, as on a syllable.

The three jamo index tables and their -1 macros were the cho, jung, and
jong arrays inverted by hand, bounded by a Jrange the callers had to
respect; a linear scan of the arrays says the same in ten lines.
2026-08-17 01:22:52 +09:00
f0498868eb engine: Space offers the reading in Katakana last; 0 always commits the reading
Every Japanese IME converts a reading to Katakana on demand; strans made
the user switch to the Katakana mode and type again.  Space now adds
the reading in Katakana after the dictionary candidates, so a loanword
the dictionary lacks converts with one Space and Enter, and 漢字 users
still get 漢字 first.

0 committed the reading only while candidates showed; without them it
typed a 0 as well.  It now commits any Japanese reading, the counterpart
of 1-9 picking candidates, as the README always said.
2026-08-17 01:18:33 +09:00
ab0366b7f7 engine: verbs and adjectives through SKK's okuri-ari entries
A sixth of kanji.dict is SKK's okuri-ari entries, keyed by a stem and
the letter of the okurigana that follows it — かk: 書 描 掛 —, and no
reading ever ends in a letter, so かく offered 確 and 各 but never 書く,
and no verb or adjective could be converted at all.  Now every split of a
complete reading is tried, longest stem first, with the okurigana put
back: かく adds 書く and 描く after the exact candidates, よむ gives
読む, あかい 赤い, おおきい 大きい, いった 言った, かいた 書いた.  A stem
that ends in っ writes it in kana too, so いt and いっt agree.
2026-08-17 01:18:09 +09:00
f61f7ab5f8 engine: a romaji typo stays in the reading
A key that could not go on with the pending romaji, when that was not a
syllable either, committed the whole reading to the application and
passed the key on: one slip dumped かんj into the text with no way back.
The letters typed now stay in the reading as they are, like any other
IME shows them, and Backspace mends them; only a key that starts no
syllable at all still ends the composition.
2026-08-17 01:17:12 +09:00
95dff0b82b data(emoji): every emoji, with its CLDR names in English, Korean, and Japanese
emoji.src was a seed of thirty emoji.  cldr2emoji now generates it from
Unicode's emoji-test.txt (Emoji 17.0) and CLDR 48.2.0's annotations: the
1914 fully-qualified emoji without their skin-tone variants, each with
its names and keywords in the three languages, so that a search finds
what fcitx5's emoji picker finds.  The data is under the Unicode License,
added to LICENSES.
2026-08-17 01:07:24 +09:00
abff5ed122 emoji: search the dictionary by prefix in the engine, not in the data
mkemoji wrote a row for every prefix of every alias, so that a query
matched as it was typed; the trie is a prefix index already, and with a
real emoji list those rows would be four times the aliases themselves.
Now emoji.dict has one row per alias, trienode() names a key's node, and
dictlookup walks the entries at and below it, the key's own first, up to
Maxkouho.  Trie children are appended rather than pushed, so the walk
keeps the file's order and a bare digit still picks the superscript or
subscript it always did.

The hand-written symbol rows move to symbol.src; emoji.src is left to
the emoji.  mkemoji reads both by default, or the files it is given.
2026-08-17 01:07:10 +09:00
221117f93d data(kana): Mozc's romaji table
hira.map and kata.map knew 152 keys of the 320 every Japanese IME
accepts: no nn, no small kana by x or l (xtu, xya, la, ...), no sya, tya,
zya, jya, dya, cya, tsa, thi, dhi, twu, kye, ye, fya, wha, and zi gave ぢ.
Both are now Mozc's default table in full, kata.map in Katakana, so that
they agree (kata.map alone had di → ディ and a lone v).  With Mozc, wi
and we are うぃ and うぇ (wyi and wye the old kana), the v row is ゔ, nn
is ん, and [ ] ~ and the z-prefixed symbols type 「 」 〜 ・ … ← ↓ ↑ →.

n' is a row now, not a case in transjp.  A row whose value is っ keeps
its consonant pending only when it is a doubled consonant or tch: xtu
and ltu are っ itself, so a lone っ can finally be typed.
2026-08-17 00:58:36 +09:00
b77400fa86 engine: the Korean keyboard's 한/영 and 한자 keys
A Korean keyboard sends Hangul and Hangul_Hanja for its two extra keys;
they did nothing but end the composition.  Now 한/영 toggles Korean and
English and 한자 opens the Hanja search, by standing in for Ctrl+S,
Ctrl+T, and Ctrl+H, as fcitx5-hangul binds them.
2026-08-17 00:54:36 +09:00
63fc60dc06 engine: Korean and Telex let Escape through after committing
A pending Hangul syllable is real text, and Escape is what vi users
press to leave insert mode; eating it and dropping the syllable served
nobody.  fcitx5-hangul commits and passes any key it does not consume,
Escape included; do the same for Korean and Telex.  A Japanese reading
is still cancelled: there Escape is the IME's own key.
2026-08-17 00:53:43 +09:00
e8519b4e06 engine: no candidate is chosen until the user moves to one; Space converts
A complete Japanese reading showed its candidates with the first row
highlighted, yet Enter committed the reading unless the user had moved
to a candidate: the highlight lied.  Now sel is -1 while nothing is
chosen, and it alone says what Enter commits; candidatechosen goes.

Every key that ends a composition — Enter, Tab, a language switch, a
special key, a modifier chord, typing on — now commits the chosen
candidate through one flush; before, only Enter and Tab did, and Ctrl+S
after choosing 漢字 committed かんじ.

Space in a Japanese mode is the conversion key, as in every other
Japanese IME: it steps through the candidates (Shift+Space backwards,
both wrapping) and commits a reading that has none, without typing a
space.  Backspace and Escape on a chosen candidate go back to the
reading first.  Tab in a search wraps through the same cyclekouho.

reset() no longer forgets the caret: a chosen candidate confirmed by
typing on would otherwise redraw the next reading at the pointer.
2026-08-17 00:53:32 +09:00
63 changed files with 448192 additions and 2077 deletions

2
.gitignore vendored
View File

@@ -11,8 +11,6 @@
/tests/daemon_collision_test /tests/daemon_collision_test
/tests/daemon_failure_test /tests/daemon_failure_test
/tests/daemon_restart_test /tests/daemon_restart_test
/xim/*.o
/xim/xim_test
/gtk/im-strans.so /gtk/im-strans.so
/bench/bench /bench/bench
/bench/perf.data* /bench/perf.data*

View File

@@ -23,6 +23,7 @@ RUN pacman -Syu --noconfirm --needed \
which \ which \
xorg-server-xvfb \ xorg-server-xvfb \
xcb-imdkit \ xcb-imdkit \
xcb-util \
dbus \ dbus \
&& pacman -Scc --noconfirm && pacman -Scc --noconfirm
@@ -35,7 +36,6 @@ RUN export DEBUGINFOD_URLS=https://debuginfod.archlinux.org \
"$debugdir/${buildid#??}.debug" \ "$debugdir/${buildid#??}.debug" \
&& rm -rf /root/.cache/debuginfod_client && rm -rf /root/.cache/debuginfod_client
RUN printf "egrep='grep -E'\n" > /usr/lib/plan9/config
RUN dbus-uuidgen --ensure=/etc/machine-id RUN dbus-uuidgen --ensure=/etc/machine-id
ENV PLAN9=/usr/lib/plan9 ENV PLAN9=/usr/lib/plan9

View File

@@ -6,4 +6,8 @@
- `GPL-2.0-or-later.txt` contains GNU GPL version 2. The header of - `GPL-2.0-or-later.txt` contains GNU GPL version 2. The header of
`map/kanji.dict` grants the option to use GPL version 2 or any later `map/kanji.dict` grants the option to use GPL version 2 or any later
version. version.
- `Unicode-3.0.txt` contains the Unicode License v3, the terms for the
emoji names and keywords in `map/emoji.src` and generated
`map/emoji.dict`, derived from Unicode's `emoji-test.txt` and CLDR's
annotations.
No repository-wide license has been declared for original strans source. No repository-wide license has been declared for original strans source.

39
LICENSES/Unicode-3.0.txt Normal file
View File

@@ -0,0 +1,39 @@
UNICODE LICENSE V3
COPYRIGHT AND PERMISSION NOTICE
Copyright © 1991-2026 Unicode, Inc.
NOTICE TO USER: Carefully read the following legal agreement. BY
DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
Permission is hereby granted, free of charge, to any person obtaining a
copy of data files and any associated documentation (the "Data Files") or
software and any associated documentation (the "Software") to deal in the
Data Files or Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, and/or sell
copies of the Data Files or Software, and to permit persons to whom the
Data Files or Software are furnished to do so, provided that either (a)
this copyright and permission notice appear with all copies of the Data
Files or Software, or (b) this copyright and permission notice appear in
associated Documentation.
THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
THIRD PARTY RIGHTS.
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA
FILES OR SOFTWARE.
Except as contained in this notice, the name of a copyright holder shall
not be used in advertising or otherwise to promote the sale, use or other
dealings in these Data Files or Software without prior written
authorization of the copyright holder.

View File

@@ -5,12 +5,12 @@ CFLAGS ?= -O2 -g
WARN_CFLAGS = -Wall -Wextra WARN_CFLAGS = -Wall -Wextra
IBUS_CFLAGS = $(shell $(PKG_CONFIG) --cflags dbus-1 xkbcommon) IBUS_CFLAGS = $(shell $(PKG_CONFIG) --cflags dbus-1 xkbcommon)
IBUS_LIBS = $(shell $(PKG_CONFIG) --libs dbus-1 xkbcommon) IBUS_LIBS = $(shell $(PKG_CONFIG) --libs dbus-1 xkbcommon)
XIM_CFLAGS = $(shell $(PKG_CONFIG) --cflags xcb-imdkit xcb-xkb xkbcommon-x11) XIM_CFLAGS = $(shell $(PKG_CONFIG) --cflags xcb-imdkit xcb-aux xcb-xkb xkbcommon-x11)
XIM_LIBS = $(shell $(PKG_CONFIG) --libs xcb-imdkit xcb-xkb xkbcommon-x11) XIM_LIBS = $(shell $(PKG_CONFIG) --libs xcb-imdkit xcb-aux xcb-xkb xkbcommon-x11)
TEXT_CFLAGS = $(shell $(PKG_CONFIG) --cflags pangocairo cairo fontconfig) TEXT_CFLAGS = $(shell $(PKG_CONFIG) --cflags pangocairo cairo fontconfig)
TEXT_LIBS = $(shell $(PKG_CONFIG) --libs pangocairo cairo fontconfig) TEXT_LIBS = $(shell $(PKG_CONFIG) --libs pangocairo cairo fontconfig)
POPUP_CFLAGS = $(shell $(PKG_CONFIG) --cflags xcb-randr) POPUP_CFLAGS = $(shell $(PKG_CONFIG) --cflags xcb-aux xcb-randr)
POPUP_LIBS = $(shell $(PKG_CONFIG) --libs xcb-randr) POPUP_LIBS = $(shell $(PKG_CONFIG) --libs xcb-aux xcb-randr)
PROJECT_CPPFLAGS = PROJECT_CPPFLAGS =
PROJECT_LDLIBS = -lthread -lbio -lxcb $(IBUS_LIBS) $(TEXT_LIBS) \ PROJECT_LDLIBS = -lthread -lbio -lxcb $(IBUS_LIBS) $(TEXT_LIBS) \
$(XIM_LIBS) $(POPUP_LIBS) $(XIM_LIBS) $(POPUP_LIBS)
@@ -19,11 +19,9 @@ DOCKER_IMAGE = strans-build
DOCKER_RUN = docker run --rm --user "$$(id -u):$$(id -g)" \ DOCKER_RUN = docker run --rm --user "$$(id -u):$$(id -g)" \
-v "$(CURDIR):/src" $(DOCKER_IMAGE) -v "$(CURDIR):/src" $(DOCKER_IMAGE)
SRCS = dict.c font.c ibus.c ipc.c ko.c main.c popup_layout.c \ SRCS = compose.c dict.c font.c ibus.c ipc.c ko.c main.c popup_layout.c \
srv.c str.c strans.c trie.c vi.c win.c srv.c str.c strans.c trie.c vi.c win.c xim.c
XIMSRCS = xim/xim.c xim/keymap.c OBJS = $(SRCS:.c=.o)
XIMOBJS = $(XIMSRCS:.c=.o)
OBJS = $(SRCS:.c=.o) $(XIMOBJS)
all: $(PROG) gtk all: $(PROG) gtk
@@ -31,10 +29,10 @@ $(PROG): $(OBJS)
$(LD) $(LDFLAGS) -o $@ $(OBJS) $(PROJECT_LDLIBS) $(LDLIBS) $(LD) $(LDFLAGS) -o $@ $(OBJS) $(PROJECT_LDLIBS) $(LDLIBS)
$(OBJS): dat.h fn.h ipc.h $(OBJS): dat.h fn.h ipc.h
ibus.o: PROJECT_CPPFLAGS += $(IBUS_CFLAGS) compose.o ibus.o: PROJECT_CPPFLAGS += $(IBUS_CFLAGS)
font.o: PROJECT_CPPFLAGS += $(TEXT_CFLAGS) font.o: PROJECT_CPPFLAGS += $(TEXT_CFLAGS)
win.o: PROJECT_CPPFLAGS += $(POPUP_CFLAGS) win.o: PROJECT_CPPFLAGS += $(POPUP_CFLAGS)
$(XIMOBJS): PROJECT_CPPFLAGS += -I. $(XIM_CFLAGS) xim.o: PROJECT_CPPFLAGS += $(XIM_CFLAGS)
%.o: %.c %.o: %.c
$(CC) $(CPPFLAGS) $(PROJECT_CPPFLAGS) $(WARN_CFLAGS) $(CFLAGS) -c -o $@ $< $(CC) $(CPPFLAGS) $(PROJECT_CPPFLAGS) $(WARN_CFLAGS) $(CFLAGS) -c -o $@ $<
@@ -89,13 +87,14 @@ check-stress: $(PROG)
test: check test: check
verify-map: verify-map:
test "$$(cut -f1 map/hira.map)" = "$$(cut -f1 map/kata.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 map/mkemoji | cmp - map/emoji.dict
python3 -B map/mkhanja | cmp - map/hanja.dict python3 map/mkhanja | cmp - map/hanja.dict
python3 -B map/verifymap.py map/*.map map/*.dict python3 map/verifymap.py map/*.map map/*.dict
python3 -B tests/mkemoji_test.py python3 tests/mkemoji_test.py
python3 -B tests/mkhanja_test.py python3 tests/mkhanja_test.py
python3 -B tests/skk2ktrans_test.py python3 tests/skk2ktrans_test.py
.PHONY: all check check-live check-stress test verify-map clean gtk bench \ .PHONY: all check check-live check-stress test verify-map clean gtk bench \
docker docker-image docker-build docker-check docker-check-live \ docker docker-image docker-build docker-check docker-check-live \

134
README.md
View File

@@ -1,8 +1,9 @@
# strans # strans
strans is a small, single-user input method for Korean, Japanese, English, strans is a small, single-user input method for Korean, Japanese, English,
emoji, and symbols. It provides one engine for IBus, XIM, and GTK 3. emoji, and symbols. It provides one engine for IBus (which GTK 4 and
Vietnamese Telex is also available as a compatibility mode. Qt use), XIM, and GTK 3. Vietnamese Telex is also available as a
compatibility mode.
## Input modes ## Input modes
@@ -16,20 +17,56 @@ Vietnamese Telex is also available as a compatibility mode.
| `Ctrl+E` | Emoji and symbol search | | `Ctrl+E` | Emoji and symbol search |
| `Ctrl+H` | One-shot Hanja search | | `Ctrl+H` | One-shot Hanja search |
Use `Up`/`Down` to move through candidates and `PageUp`/`PageDown` to move by Switching mode shows the mode switched to — `A`, `한`, `あ`, `ア`, `ă` — in
nine without wrapping. An unmodified `1`-`9` selects that row on the current the popup until the next key.
page. `Enter` and `Tab` commit a candidate you have moved to; otherwise they
commit the reading, as `0` always does. Japanese consumes the confirming key;
Korean and Vietnamese pass it on to the application. In a temporary Emoji or
Hanja search, `Enter` commits the highlighted result and `Tab`/`Shift-Tab`
wrap through the results. Leaving the field or clicking elsewhere commits
what is pending; `Esc` cancels it.
Hiragana mode composes a complete reading before offering Kanji candidates. A Korean keyboard's 한/영 key toggles Korean and English, and its 한자 key
Katakana mode does not perform Kanji conversion. Emoji and Hanja searches is `Ctrl+H`; a Japanese keyboard's 半角/全角 toggles Japanese and English,
return to the previous mode after use; switching language during a search ひらがな/カタカナ switches between the two kana modes, 変換 turns Japanese
commits the shown query. `Ctrl+H` takes the syllable being composed as its on and then converts like `Space`, and 無変換 turns it off. Only a plain
query; the Hanja dictionary lists one modern Hangul syllable at a time. `Ctrl` chord is a strans key: with `Shift`, `Alt`, or `Super` held it
commits what is pending and goes to the application, so `Ctrl+Shift+V`
still pastes.
Hiragana mode composes a complete reading, then shows its Kanji candidates
with none chosen: the exact readings of the dictionary, then the verbs and
adjectives SKK keys by stem and okurigana (かく offers 確 and 書く), and
under `Space` the reading in Katakana last, so a word the dictionary lacks
converts to Katakana with one `Space`. `Space` or `Tab` chooses the first
candidate and then steps on, wrapping (with `Shift`, back); `Up`/`Down` and
`PageUp`/`PageDown` move without wrapping. Once a candidate is chosen, an unmodified `1`-`9` commits that row of the
current page; before that the digits type. `Enter` commits the chosen candidate, or the
reading when none is chosen, as `0` always does; so does typing on, or any
key that ends the composition. `Backspace` deletes the last kana shown;
romaji that made no kana stays in the reading as typed until it is mended.
The dictionary keys a verb or adjective by its stem, so `kaku` offers the
readings of かく and then 書く and its kin; typing the okurigana with
`Shift`, as SKK does — `kaKu` — asks for that split first. Caps Lock is
not `Shift` and marks nothing.
`Backspace` and `Esc` on a chosen candidate go back to the reading, and
`Esc` on the reading cancels it. Katakana mode does not perform Kanji
conversion.
Korean composes one syllable at a time; `Enter`, `Tab`, `Esc`, and any key
that is not a jamo commit the syllable and go on to the application, so
`Esc` still leaves insert mode. Two lone consonants join into the compound
final they make (rt → ㄳ) and a vowel typed before its consonant is
reordered under it (kr → 가), as in libhangul.
Emoji and Hanja searches take over the keys until `Enter`, `Space`, or
`1`-`9` picks a result or `Esc` cancels; they return to the previous mode
after use, and switching language during a search commits the shown
query. `Tab`/`Shift-Tab` wrap through the results, and a search with
nothing typed yet shows ☺ or 漢 in the popup. The Emoji search matches the
typed keys, and what they spell in the current language, against a prefix
of every emoji's CLDR name and keywords in English, Korean, and Japanese,
and against ASCII symbol aliases such as `->` and `<=`. `Ctrl+H` takes the
syllable being composed as its query and composes on from it; `Esc` gives
the syllable back. The Hanja dictionary converts a word as well as a
syllable, so `Ctrl+H` and then 한자 gives 漢字, and 대한민국 gives 大韓民國;
a syllable already committed is the application's text, not the engine's,
and cannot be converted. Leaving the field or
clicking elsewhere commits what is pending.
## Preedit and candidates ## Preedit and candidates
@@ -40,10 +77,18 @@ query; the Hanja dictionary lists one modern Hangul syllable at a time.
| XIM PreeditPosition | strans popup | strans popup | | XIM PreeditPosition | strans popup | strans popup |
| XIM PreeditNothing | strans popup | strans popup | | XIM PreeditNothing | strans popup | strans popup |
Each XIM style is offered with StatusNothing and with StatusNone.
Dead keys and Compose sequences are composed by strans itself for the XIM
and IBus frontends, from the table `XCOMPOSEFILE` or the locale names; the
GTK 3 module leaves them to GtkIMContextSimple. Their text follows
whatever was pending.
XIM text travels as `COMPOUND_TEXT`, which carries any UTF-8. A client that XIM text travels as `COMPOUND_TEXT`, which carries any UTF-8. A client that
sends `XNSpotLocation` gets the popup at that spot whatever its preedit sends `XNSpotLocation` gets the popup under that spot whatever its preedit
style; otherwise the popup sits under the focus window, or under the client style, and above the line when there is no room below; otherwise the popup
window when no focus window is set. sits under the focus window, or under the client window when no focus
window is set.
The popup is an X11 window sized by `GDK_SCALE` on HiDPI displays, XIM is The popup is an X11 window sized by `GDK_SCALE` on HiDPI displays, XIM is
X11-only, and GTK popup placement converts the GTK caret to X11 root X11-only, and GTK popup placement converts the GTK caret to X11 root
@@ -53,8 +98,7 @@ display environment supports it.
## Build and test ## Build and test
Initialize the bundled test library, then use the pinned Docker environment Initialize the bundled test library, then build in the Docker environment:
for a reproducible build:
```sh ```sh
git submodule update --init git submodule update --init
@@ -83,15 +127,16 @@ tests. `make docker-check` rebuilds and runs all three tiers in the
container, and `make docker-valgrind` runs the unit suite under Valgrind container, and `make docker-valgrind` runs the unit suite under Valgrind
there; run `make docker-image` first after changing `Dockerfile`. there; run `make docker-image` first after changing `Dockerfile`.
Native Linux source builds require a C toolchain, Make, pkg-config, Python 3, Native Linux source builds require a C toolchain, Make, and pkg-config, plus
and cmp/diff, plus development files for: development files for:
- Plan 9 port (`9c`, `9l`, `libthread`, and `libbio`); - Plan 9 port (`9c`, `9l`, `libthread`, and `libbio`);
- D-Bus development files; - D-Bus development files;
- XCB, XCB RandR, XCB XKB, xcb-imdkit, xkbcommon, and xkbcommon-x11; - XCB, XCB RandR, XCB XKB, xcb-util, xcb-imdkit, xkbcommon, and xkbcommon-x11;
- Pango, PangoCairo, Cairo, and Fontconfig; - Pango, PangoCairo, Cairo, and Fontconfig;
- GTK 3, plus libibus and Xlib for the live clients; - GTK 3, plus libibus and Xlib for the live clients;
- Xvfb for the X11 live tests; - Xvfb for the X11 live tests;
- Python 3 and `cmp` for `make check` and the map generators;
- a working `C.UTF-8` locale and fonts covering Latin, CJK, and emoji. - a working `C.UTF-8` locale and fonts covering Latin, CJK, and emoji.
The package names used by the canonical Arch Linux image are listed in The package names used by the canonical Arch Linux image are listed in
@@ -99,9 +144,8 @@ The package names used by the canonical Arch Linux image are listed in
## Run ## Run
The popup uses the normal system fonts found by Pango and Fontconfig; no font The popup uses the normal system fonts found by Pango and Fontconfig. Run
file argument or system-wide strans installation is required. Run it directly it directly from the build tree:
from the build tree:
```sh ```sh
./run.sh ./run.sh
@@ -113,19 +157,18 @@ from the build tree:
build-tree daemon in the background, and returns to the shell. It requires build-tree daemon in the background, and returns to the shell. It requires
`pkill` from procps. Startup and frontend failures remain visible on the `pkill` from procps. Startup and frontend failures remain visible on the
caller's standard error. A service supervisor should instead run caller's standard error. A service supervisor should instead run
`./strans map` directly in the foreground. `./strans map` directly in the foreground, with the session's
`XDG_RUNTIME_DIR` and `DISPLAY`: the GTK module finds the daemon at
`$XDG_RUNTIME_DIR/strans.sock` (else `/tmp/strans.UID`), and IBus clients
through the address file libibus expects under `~/.config/ibus/bus/`,
which strans writes as `ibus-daemon` would, named after `WAYLAND_DISPLAY`
when there is one and `DISPLAY` otherwise.
The build does not copy or install the daemon or its data. `./strans DIR` The build does not copy or install the daemon or its data. `./strans DIR`
opens `hira.map`, `kata.map`, `telex.map`, `kanji.dict`, `emoji.dict`, and opens `hira.map`, `kata.map`, `telex.map`, `kanji.dict`, `emoji.dict`, and
`hanja.dict` from `DIR` at runtime; `run.sh` passes the tracked `map/` `hanja.dict` from `DIR` at runtime; `run.sh` passes the tracked `map/`
directory. Only the GTK subdirectory has install and uninstall targets. directory. Only the GTK subdirectory has install and uninstall targets.
Pango shapes the complete string and selects glyph fallback from the system
font set.
The popup renderer uses Pango/PangoCairo and Cairo. When `DISPLAY` is set,
`strans` starts its XIM worker in the same process.
Configure clients as needed: Configure clients as needed:
```sh ```sh
@@ -140,13 +183,12 @@ export GTK_IM_MODULE=strans
GLFW_IM_MODULE=ibus kitty GLFW_IM_MODULE=ibus kitty
``` ```
Building `gtk/im-strans.so` requires GTK development files. Installing an Building `gtk/im-strans.so` requires GTK development files; installing one
already-built, up-to-date module from this build tree does not: the install built elsewhere, in the container for instance, does not. The install
target first uses an explicit `GTK_MODULE_DIR`, then GTK's pkg-config target uses `GTK_MODULE_DIR` when it is set, else GTK's pkg-config
variables, and finally the module directory reported by the GTK runtime's variables, else the directory the installed `gtk-query-immodules-3.0`
`gtk-query-immodules-3.0`. Packagers and cross builds should set reports. Packagers and cross builds should set `GTK_MODULE_DIR`, and a
`GTK_MODULE_DIR` explicitly. A prebuilt module must still match the target prebuilt module must still match the target architecture and library ABI.
architecture and library ABI.
`DESTDIR` staging is supported and deliberately skips the host module-cache `DESTDIR` staging is supported and deliberately skips the host module-cache
update. A native install needs the GTK runtime query utility so it can refresh update. A native install needs the GTK runtime query utility so it can refresh
@@ -157,21 +199,25 @@ doas make -C gtk uninstall
``` ```
strans provides its own IBus endpoint; `ibus-daemon` and fcitx are not strans provides its own IBus endpoint; `ibus-daemon` and fcitx are not
required. Without `DISPLAY`, the daemon and IBus frontend still work, but XIM required. IBus clients that process keys synchronously — GTK 4, or GTK 3
with `IBUS_ENABLE_SYNC_MODE=1` — get their commits and preedits through
`PostProcessKeyEvent`, in order with the key. Without `DISPLAY`, the daemon and IBus frontend still work, but XIM
is not started and the popup is disabled. is not started and the popup is disabled.
## Dictionary data ## Dictionary data
After changing an input source, regenerate and verify the dictionaries: After changing an input source, regenerate and verify the maps and
dictionaries:
```sh ```sh
python3 map/mktelex.py >map/telex.map
map/mkemoji >map/emoji.dict map/mkemoji >map/emoji.dict
map/mkhanja >map/hanja.dict map/mkhanja >map/hanja.dict
make verify-map make verify-map
``` ```
See [`map/README`](map/README) for Hanja import and Japanese dictionary See [`map/README`](map/README) for the emoji, Hanja, and Japanese data and
generation. where it comes from.
## Benchmark ## Benchmark

View File

@@ -9,6 +9,11 @@ if test "$#" -ne 0; then
exit 1 exit 1
fi fi
if ! test -x ./bench/bench; then
echo "bench.sh: ./bench/bench is not executable; run make bench first" >&2
exit 1
fi
strans_pid= strans_pid=
perf_pid= perf_pid=

54
compose.c Normal file
View File

@@ -0,0 +1,54 @@
#include "dat.h"
#include "fn.h"
#include <locale.h>
#include <xkbcommon/xkbcommon.h>
#include <xkbcommon/xkbcommon-compose.h>
static struct xkb_compose_state *cstate;
/* Dead keys and Compose: no frontend's client does them for us. */
void
composeinit(void)
{
struct xkb_context *ctx;
struct xkb_compose_table *table;
char *locale;
locale = setlocale(LC_CTYPE, "");
ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
if(ctx == nil)
return;
table = xkb_compose_table_new_from_locale(ctx,
locale != nil ? locale : "C", XKB_COMPOSE_COMPILE_NO_FLAGS);
if(table != nil)
cstate = xkb_compose_state_new(table, XKB_COMPOSE_STATE_NO_FLAGS);
xkb_compose_table_unref(table);
xkb_context_unref(ctx);
}
/*
* Feeds sym to the Compose table. Returns 1 while a sequence is
* unfinished: the key belongs to the sequence, not to the engine. A
* finished sequence leaves its text in buf, which the caller commits
* after whatever the engine had pending.
*/
int
composekey(u32int sym, char *buf, int n)
{
buf[0] = '\0';
if(cstate == nil ||
xkb_compose_state_feed(cstate, sym) != XKB_COMPOSE_FEED_ACCEPTED)
return 0;
switch(xkb_compose_state_get_status(cstate)){
case XKB_COMPOSE_COMPOSING:
case XKB_COMPOSE_CANCELLED:
return 1;
case XKB_COMPOSE_COMPOSED:
xkb_compose_state_get_utf8(cstate, buf, n);
break;
default:
break;
}
return 0;
}

16
dat.h
View File

@@ -25,6 +25,7 @@ enum
enum enum
{ {
Maxclients = 64, Maxclients = 64,
Ownerpoll = 200, /* ms between a frontend's owner checks while its preedit shows */
Maxkouho = 128, Maxkouho = 128,
Maxdisp = 9, Maxdisp = 9,
@@ -40,10 +41,10 @@ extern int popupscale;
#define Fontsz (32*popupscale) #define Fontsz (32*popupscale)
#define PopupPad (4*popupscale) #define PopupPad (4*popupscale)
#define PopupSep popupscale #define PopupSep popupscale
#define PopupBorder popupscale
#define PopupNumw Fontsz #define PopupNumw Fontsz
#define PopupTextw (12*Fontsz) #define PopupTextw (12*Fontsz)
#define PopupBasew (2*PopupPad + PopupNumw + PopupTextw) #define PopupBasew (2*PopupPad + PopupNumw + PopupTextw)
#define Imgh (2*PopupPad + (Maxdisp + 2)*Fontsz + PopupSep)
typedef struct Str Str; typedef struct Str Str;
struct Str struct Str
@@ -73,7 +74,7 @@ struct Tnode
int vlen; int vlen;
int child; int child;
int sibling; int sibling;
char c; Rune c;
}; };
typedef struct Trie Trie; typedef struct Trie Trie;
@@ -82,6 +83,9 @@ struct Trie
Tnode *nodes; Tnode *nodes;
int n; int n;
int cap; int cap;
/* the last key put and its path, so a sorted file walks it once */
Str last;
int path[Maxrunes];
}; };
enum enum
@@ -172,10 +176,6 @@ struct Popup
typedef struct Keyreq Keyreq; typedef struct Keyreq Keyreq;
typedef struct Keyres Keyres; typedef struct Keyres Keyres;
enum enum
{
Cclientpreedit = 1<<0,
};
enum
{ {
Keypress, Keypress,
Keyreset, Keyreset,
@@ -185,7 +185,7 @@ enum
}; };
struct Keyres struct Keyres
{ {
int eaten; /* Keycap: caller is active and capability was applied. */ int eaten; /* Keycap: the caller is the owner and clientpre took. */
Str commit; Str commit;
Str preedit; Str preedit;
}; };
@@ -193,7 +193,7 @@ struct Keyres
struct Keyreq struct Keyreq
{ {
void *owner; /* stable until the context's release is acknowledged */ void *owner; /* stable until the context's release is acknowledged */
int cap; int clientpre; /* the client draws the preedit; the popup does not */
int op; int op;
u32int ks; u32int ks;
u32int mod; u32int mod;

61
dict.c
View File

@@ -1,36 +1,67 @@
#include "dat.h" #include "dat.h"
#include "fn.h" #include "fn.h"
/* /* Appends the space-separated words of a node's entry to out[], up to max. */
* Fills out[] with up to max candidates for key: the space-separated words static int
* of the entry, minus the key itself except in the emoji dictionary, where words(Tnode *nd, Str *out, int n, int max)
* a query may name its own answer.
*/
int
dictlookup(Lang *l, Str *key, Str *out, int max)
{ {
char *p, *e, *sp; char *p, *e, *sp;
Str tmp; Str tmp;
int n, vlen;
if(l == nil || key->n == 0 || if(nd->val == nil)
trielookup(l->dict, key, &p, &vlen) != TrieExact) return n;
return 0; p = nd->val;
n = 0; e = p + nd->vlen;
e = p + vlen;
while(n < max && p < e){ while(n < max && p < e){
while(p < e && *p == ' ') while(p < e && *p == ' ')
p++; p++;
sp = p; sp = p;
while(p < e && *p != ' ') while(p < e && *p != ' ')
p++; p++;
if(sinit(&tmp, sp, p - sp) && tmp.n > 0 && if(sinit(&tmp, sp, p - sp) && tmp.n > 0)
(l->lang == LangEMOJI || scmp(&tmp, key) != 0))
out[n++] = tmp; out[n++] = tmp;
} }
return n; return n;
} }
/* The entries at and below a node, the shortest keys first. */
static int
below(Trie *t, int ni, Str *out, int n, int max)
{
Tnode *nd;
int ci;
nd = &t->nodes[ni];
n = words(nd, out, n, max);
for(ci = nd->child; ci >= 0 && n < max; ci = t->nodes[ci].sibling)
n = below(t, ci, out, n, max);
return n;
}
/* Fills out[] with up to max candidates for key: the words of its entry. */
int
dictlookup(Trie *t, Str *key, Str *out, int max)
{
int ni;
ni = key->n == 0 ? -1 : trienode(t, key);
if(ni < 0)
return 0;
return words(&t->nodes[ni], out, 0, max);
}
/* As dictlookup, for every entry key is a prefix of, key's own first. */
int
dictprefix(Trie *t, Str *key, Str *out, int max)
{
int ni;
ni = key->n == 0 ? -1 : trienode(t, key);
if(ni < 0)
return 0;
return below(t, ni, out, 0, max);
}
static Trie* static Trie*
langopen(char *dir, char *name, char *ext) langopen(char *dir, char *name, char *ext)
{ {

11
fn.h
View File

@@ -15,12 +15,14 @@ Trie* trienew(void);
void trieput(Trie*, char*, int, char*, int); void trieput(Trie*, char*, int, char*, int);
Trie* trieopen(char*); Trie* trieopen(char*);
void trieclose(Trie*); void trieclose(Trie*);
int trienode(Trie*, Str*);
int trielookup(Trie*, Str*, char**, int*); int trielookup(Trie*, Str*, char**, int*);
Lang* getlang(int); Lang* getlang(int);
void langinit(char*); void langinit(char*);
int dictlookup(Lang*, Str*, Str*, int); int dictlookup(Trie*, Str*, Str*, int);
int dictprefix(Trie*, Str*, Str*, int);
void drawthread(void*); void drawthread(void*);
void popuparea(Area*, int, Area*, int, int, Area*); void popuparea(Area*, int, Area*, int, int, Area*);
@@ -30,7 +32,7 @@ void popuplayout(Drawcmd*, int, int, Popup*);
void popupdraw(u32int*, Drawcmd*, Popup*); void popupdraw(u32int*, Drawcmd*, Popup*);
void imthread(void*); void imthread(void*);
void impre(Im*, Str*); void impre(Im*, Str*);
void transstr(Lang*, Str*, Str*); void transstr(Lang*, Str*, Str*, Str*);
Emit transmap(Im*, Rune); Emit transmap(Im*, Rune);
Emit transko(Im*, Rune); Emit transko(Im*, Rune);
Emit transvi(Im*, Rune); Emit transvi(Im*, Rune);
@@ -43,10 +45,13 @@ void ibusthread(void*);
void ximthread(void*); void ximthread(void*);
int keymeaningful(u32int); int keymeaningful(u32int);
void composeinit(void);
int composekey(u32int, char*, int);
void* emalloc(ulong); void* emalloc(ulong);
void* erealloc(void*, ulong); void* erealloc(void*, ulong);
int textinit(void); void textinit(void);
void textclose(void); void textclose(void);
int textwidth(Str*); int textwidth(Str*);
void textdraw(u32int*, int, int, int, int, int, u32int, Str*); void textdraw(u32int*, int, int, int, int, int, u32int, Str*);

40
font.c
View File

@@ -7,13 +7,6 @@ static PangoFontMap *fontmap;
static PangoContext *context; static PangoContext *context;
static PangoLayout *layout; static PangoLayout *layout;
static void
resetlayout(void)
{
pango_layout_set_width(layout, -1);
pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_NONE);
}
void void
textclose(void) textclose(void)
{ {
@@ -50,28 +43,16 @@ setfont(void)
pango_font_description_free(font); pango_font_description_free(font);
} }
int void
textinit(void) textinit(void)
{ {
textclose();
FcInit(); FcInit();
fontmap = pango_cairo_font_map_new(); fontmap = pango_cairo_font_map_new();
if(fontmap == nil){
fprint(2, "strans: popup: can't initialize PangoCairo\n");
return 0;
}
pango_cairo_font_map_set_resolution(PANGO_CAIRO_FONT_MAP(fontmap), 96); pango_cairo_font_map_set_resolution(PANGO_CAIRO_FONT_MAP(fontmap), 96);
context = pango_font_map_create_context(fontmap); context = pango_font_map_create_context(fontmap);
if(context != nil) layout = pango_layout_new(context);
layout = pango_layout_new(context);
if(context == nil || layout == nil){
fprint(2, "strans: popup: can't create text layout\n");
textclose();
return 0;
}
pango_layout_set_single_paragraph_mode(layout, TRUE); pango_layout_set_single_paragraph_mode(layout, TRUE);
setfont(); setfont();
return 1;
} }
static int static int
@@ -80,8 +61,6 @@ settext(Str *s)
char utf[Maxutf]; char utf[Maxutf];
int n; int n;
if(layout == nil || s == nil || s->n <= 0)
return 0;
n = stoutf(s, utf, sizeof utf); n = stoutf(s, utf, sizeof utf);
pango_layout_set_text(layout, utf, n); pango_layout_set_text(layout, utf, n);
return n > 0; return n > 0;
@@ -109,9 +88,8 @@ textwidth(Str *s)
{ {
PangoRectangle r; PangoRectangle r;
if(layout == nil) pango_layout_set_width(layout, -1);
return 0; pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_NONE);
resetlayout();
if(!settext(s)) if(!settext(s))
return 0; return 0;
textextents(&r); textextents(&r);
@@ -130,20 +108,16 @@ textdraw(u32int *buf, int w, int h, int x, int y, int fit, u32int color,
PangoRectangle r; PangoRectangle r;
cairo_surface_t *surface; cairo_surface_t *surface;
cairo_t *cr; cairo_t *cr;
int b, g, red, stride; int b, g, red;
if(layout == nil || fit <= 0) if(fit <= 0)
return; return;
resetlayout();
pango_layout_set_width(layout, fit * PANGO_SCALE); pango_layout_set_width(layout, fit * PANGO_SCALE);
pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END); pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END);
if(w <= 0 || h <= 0 || !settext(s)) if(w <= 0 || h <= 0 || !settext(s))
return; return;
stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, w);
if(stride < 0 || (vlong)stride != (vlong)w * sizeof buf[0])
return;
surface = cairo_image_surface_create_for_data((uchar*)buf, surface = cairo_image_surface_create_for_data((uchar*)buf,
CAIRO_FORMAT_RGB24, w, h, stride); CAIRO_FORMAT_RGB24, w, h, w * sizeof buf[0]);
if(cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS){ if(cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS){
cairo_surface_destroy(surface); cairo_surface_destroy(surface);
return; return;

View File

@@ -7,6 +7,7 @@ GTK_QUERY_IMMODULES ?= gtk-query-immodules-3.0
GTK_LIBDIR ?= $(shell $(PKG_CONFIG) --variable=libdir gtk+-3.0 2>/dev/null) GTK_LIBDIR ?= $(shell $(PKG_CONFIG) --variable=libdir gtk+-3.0 2>/dev/null)
GTK_BINARY_VERSION ?= $(shell $(PKG_CONFIG) \ GTK_BINARY_VERSION ?= $(shell $(PKG_CONFIG) \
--variable=gtk_binary_version gtk+-3.0 2>/dev/null) --variable=gtk_binary_version gtk+-3.0 2>/dev/null)
# Without GTK development files, ask the runtime where its modules live.
GTK_RUNTIME_MODULE_DIR = $(shell GTK_PATH= GTK_IM_MODULE_FILE= \ GTK_RUNTIME_MODULE_DIR = $(shell GTK_PATH= GTK_IM_MODULE_FILE= \
$(GTK_QUERY_IMMODULES) 2>/dev/null | \ $(GTK_QUERY_IMMODULES) 2>/dev/null | \
awk -F '"' '/^"\// { sub("/[^/]*$$", "", $$2); print $$2; exit }') awk -F '"' '/^"\// { sub("/[^/]*$$", "", $$2); print $$2; exit }')

View File

@@ -252,6 +252,7 @@ kpress(GtkIMContext *ctx, GdkEventKey *ev)
char pre[Ipcfieldmax+1]; char pre[Ipcfieldmax+1];
uint32_t key, mod; uint32_t key, mod;
Ipcresp resp; Ipcresp resp;
gboolean r;
im = (Im*)ctx; im = (Im*)ctx;
if(ev->type != GDK_KEY_PRESS){ if(ev->type != GDK_KEY_PRESS){
@@ -263,8 +264,13 @@ kpress(GtkIMContext *ctx, GdkEventKey *ev)
return simplefilter(ctx, ev, 0); return simplefilter(ctx, ev, 0);
key = ipckeysym(ev->keyval, gdk_keyval_to_unicode(ev->keyval)); key = ipckeysym(ev->keyval, gdk_keyval_to_unicode(ev->keyval));
mod = ipcmod(ev->state); mod = ipcmod(ev->state);
if(im->private || key == 0) if(im->private || key == 0){
return simplefilter(ctx, ev, 0); r = simplefilter(ctx, ev, 0);
/* A dead key starts a compose: pending text was typed first. */
if(im->simpleactive)
sendreset(im);
return r;
}
if(srvconnect(im) < 0) if(srvconnect(im) < 0)
return simplefilter(ctx, ev, 0); return simplefilter(ctx, ev, 0);
/* A retained GDK window may have moved since the last cursor report. */ /* A retained GDK window may have moved since the last cursor report. */

430
ibus.c
View File

@@ -2,10 +2,7 @@
#include "fn.h" #include "fn.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h> #include <errno.h>
#include <unistd.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <poll.h> #include <poll.h>
#include <dbus/dbus.h> #include <dbus/dbus.h>
@@ -13,12 +10,10 @@
enum enum
{ {
Maxconns = Maxclients, Maxwatches = 2*Maxclients + 1,
Maxwatches = 2*Maxconns + 1,
/* Toolkits keep one context per widget that ever took focus. */ /* Toolkits keep one context per widget that ever took focus. */
Maxcontexts = 1024, Maxcontexts = 1024,
Relmask = 1<<30, Relmask = 1<<30,
Ownerpoll = 200, /* ms between owner checks while a preedit shows */
/* IBus wire constants; the daemon does not link libibus. */ /* IBus wire constants; the daemon does not link libibus. */
Ibuscappreedit = 1<<0, Ibuscappreedit = 1<<0,
Ibuspurposepassword = 8, Ibuspurposepassword = 8,
@@ -26,6 +21,22 @@ enum
Ibusattrunderline = 1, Ibusattrunderline = 1,
Ibusunderlinesingle = 1, Ibusunderlinesingle = 1,
Ibuspreeditclear = 0, Ibuspreeditclear = 0,
Maxpost = 3,
};
/*
* A client in IBus's synchronous mode (GTK 4) cannot take signals while
* it waits for its ProcessKeyEvent reply: it asks afterwards, through the
* PostProcessKeyEvent property, for what the key produced, as typed
* (yv) pairs — 'c' a commit, 'u' or 'm' a preedit and then its
* "cursor,visible[,mode]".
*/
typedef struct Post Post;
struct Post
{
char type;
char text[Maxutf];
char pos[32];
}; };
typedef struct Ictx Ictx; typedef struct Ictx Ictx;
@@ -37,12 +48,16 @@ struct Ictx
u32int cap; u32int cap;
u32int purpose; u32int purpose;
int clientcommitpreedit; int clientcommitpreedit;
int postprocess;
int keying; /* inside ProcessKeyEvent: hold signals for the post */
int npost;
Post post[Maxpost];
Caret caret; Caret caret;
}; };
static DBusWatch *watches[Maxwatches]; static DBusWatch *watches[Maxwatches];
static int nwatches; static int nwatches;
static DBusConnection *conns[Maxconns]; static DBusConnection *conns[Maxclients];
static int nconns; static int nconns;
static DBusServer *srv; static DBusServer *srv;
static Ictx contexts[Maxcontexts]; static Ictx contexts[Maxcontexts];
@@ -70,10 +85,8 @@ unlinkaddr(void)
} }
static int static int
addrnote(void *v, char *note) addrnote(void*, char*)
{ {
USED(v);
USED(note);
unlinkaddr(); unlinkaddr();
return 0; return 0;
} }
@@ -100,52 +113,63 @@ machineidfiles(char *buf, int sz, char **path, int npath)
buf[0] = '\0'; buf[0] = '\0';
} }
/* libibus reads the D-Bus file first and calls it "machine-id" if neither is there. */
static void static void
machineid(char *buf, int sz) machineid(char *buf, int sz)
{ {
char *path[] = { char *path[] = {
"/etc/machine-id",
"/var/lib/dbus/machine-id", "/var/lib/dbus/machine-id",
"/etc/machine-id",
}; };
machineidfiles(buf, sz, path, nelem(path)); machineidfiles(buf, sz, path, nelem(path));
if(buf[0] == '\0')
snprintf(buf, sz, "machine-id");
} }
static void
copyfield(char *dst, int sz, char *src, int n)
{
if(n >= sz)
n = sz - 1;
memcpy(dst, src, n);
dst[n] = '\0';
}
/*
* The display libibus names the file after: the whole WAYLAND_DISPLAY,
* else DISPLAY's host and number, else unix and 0.
*/
static void static void
xdisplay(char *host, int hsz, char *num, int nsz) xdisplay(char *host, int hsz, char *num, int nsz)
{ {
char *d, *colon, *dot, *p; char *d, *colon, *dot, *p;
int n;
strncpy(host, "unix", hsz); copyfield(host, hsz, "unix", 4);
host[hsz-1] = '\0'; copyfield(num, nsz, "0", 1);
strncpy(num, "0", nsz); d = getenv("WAYLAND_DISPLAY");
num[nsz-1] = '\0'; if(d != nil && d[0] != '\0'){
copyfield(num, nsz, d, strlen(d));
return;
}
d = getenv("DISPLAY"); d = getenv("DISPLAY");
if(d == nil || d[0] == '\0') if(d == nil || d[0] == '\0')
return; return;
colon = strchr(d, ':'); colon = strchr(d, ':');
if(colon == nil) if(colon == nil)
return; return;
if(colon > d){ if(colon > d)
n = colon - d; copyfield(host, hsz, d, colon - d);
if(n >= hsz) n = hsz - 1;
memcpy(host, d, n);
host[n] = '\0';
}
p = colon + 1; p = colon + 1;
dot = strchr(p, '.'); dot = strchr(p, '.');
n = dot ? dot - p : (int)strlen(p); copyfield(num, nsz, p, dot ? dot - p : (int)strlen(p));
if(n >= nsz) n = nsz - 1;
memcpy(num, p, n);
num[n] = '\0';
} }
/* The IBus address file lives where libibus looks: config/ibus/bus/. */ /* The IBus address file lives where libibus looks: config/ibus/bus/. */
static int static int
buildaddrpath(char *buf, int sz) buildaddrpath(char *buf, int sz)
{ {
char mid[64], host[64], num[8], dir[512]; char mid[64], host[64], num[64], dir[512];
char *cfg, *home, *explicit, *p; char *cfg, *home, *explicit, *p;
int n; int n;
@@ -155,8 +179,6 @@ buildaddrpath(char *buf, int sz)
return explicit[0] == '\0' || n < 0 || n >= sz ? -1 : 0; return explicit[0] == '\0' || n < 0 || n >= sz ? -1 : 0;
} }
machineid(mid, sizeof(mid)); machineid(mid, sizeof(mid));
if(mid[0] == '\0')
return -1;
xdisplay(host, sizeof(host), num, sizeof(num)); xdisplay(host, sizeof(host), num, sizeof(num));
cfg = getenv("XDG_CONFIG_HOME"); cfg = getenv("XDG_CONFIG_HOME");
if(cfg != nil && cfg[0] != '\0') if(cfg != nil && cfg[0] != '\0')
@@ -211,11 +233,10 @@ writeaddr(char *path, char *addr)
} }
static dbus_bool_t static dbus_bool_t
addwatch(DBusWatch *w, void *_) addwatch(DBusWatch *w, void*)
{ {
int i; int i;
USED(_);
for(i = 0; i < nwatches; i++) for(i = 0; i < nwatches; i++)
if(watches[i] == nil){ if(watches[i] == nil){
watches[i] = w; watches[i] = w;
@@ -228,11 +249,10 @@ addwatch(DBusWatch *w, void *_)
} }
static void static void
removewatch(DBusWatch *w, void *_) removewatch(DBusWatch *w, void*)
{ {
int i; int i;
USED(_);
for(i = 0; i < nwatches; i++) for(i = 0; i < nwatches; i++)
if(watches[i] == w){ if(watches[i] == w){
watches[i] = nil; watches[i] = nil;
@@ -240,13 +260,6 @@ removewatch(DBusWatch *w, void *_)
} }
} }
static void
togglewatch(DBusWatch *w, void *_)
{
USED(w);
USED(_);
}
static Ictx* static Ictx*
findcontext(DBusConnection *conn, const char *path) findcontext(DBusConnection *conn, const char *path)
{ {
@@ -287,7 +300,7 @@ sendrequest(Ictx *ctx, int op, u32int ks, u32int mod, Keyres *res)
memset(&kr, 0, sizeof kr); memset(&kr, 0, sizeof kr);
kr.owner = ctx; kr.owner = ctx;
kr.cap = clientpreedit(ctx) ? Cclientpreedit : 0; kr.clientpre = clientpreedit(ctx);
kr.op = op; kr.op = op;
kr.ks = ks; kr.ks = ks;
kr.mod = mod; kr.mod = mod;
@@ -384,16 +397,34 @@ newsignal(const char *path, const char *name)
return sig; return sig;
} }
static int
postpush(Ictx *ctx, char type, const char *text, const char *pos)
{
Post *p;
if(!ctx->postprocess || !ctx->keying)
return 0;
if(ctx->npost < Maxpost){
p = &ctx->post[ctx->npost++];
p->type = type;
strcpy(p->text, text);
strcpy(p->pos, pos);
}
return 1;
}
static void static void
emitcommit(DBusConnection *c, const char *path, const char *text) emitcommit(Ictx *ctx, const char *text)
{ {
DBusMessage *sig; DBusMessage *sig;
DBusMessageIter it; DBusMessageIter it;
sig = newsignal(path, "CommitText"); if(postpush(ctx, 'c', text, ""))
return;
sig = newsignal(ctx->path, "CommitText");
dbus_message_iter_init_append(sig, &it); dbus_message_iter_init_append(sig, &it);
appendibustext(&it, text, 0); appendibustext(&it, text, 0);
dbus_connection_send(c, sig, nil); dbus_connection_send(ctx->conn, sig, nil);
dbus_message_unref(sig); dbus_message_unref(sig);
} }
@@ -405,28 +436,33 @@ emitpreedit(Ictx *ctx, const char *text)
DBusMessageIter it; DBusMessageIter it;
dbus_uint32_t cursor, mode; dbus_uint32_t cursor, mode;
dbus_bool_t visible; dbus_bool_t visible;
char pos[32];
if(text[0] == '\0' && preowner != ctx) if(text[0] == '\0' && preowner != ctx)
return; return;
sig = newsignal(ctx->path, ctx->clientcommitpreedit ?
"UpdatePreeditTextWithMode" : "UpdatePreeditText");
dbus_message_iter_init_append(sig, &it);
appendibustext(&it, text, 1);
cursor = utflen((char*)text); cursor = utflen((char*)text);
visible = text[0] != '\0' ? TRUE : FALSE; visible = text[0] != '\0' ? TRUE : FALSE;
dbus_message_iter_append_basic(&it, DBUS_TYPE_UINT32, &cursor); mode = Ibuspreeditclear;
dbus_message_iter_append_basic(&it, DBUS_TYPE_BOOLEAN, &visible); if(ctx->clientcommitpreedit)
if(ctx->clientcommitpreedit){ snprintf(pos, sizeof pos, "%u,%u,%u", cursor, visible, mode);
mode = Ibuspreeditclear; else
dbus_message_iter_append_basic(&it, DBUS_TYPE_UINT32, &mode); snprintf(pos, sizeof pos, "%u,%u", cursor, visible);
if(!postpush(ctx, ctx->clientcommitpreedit ? 'm' : 'u', text, pos)){
sig = newsignal(ctx->path, ctx->clientcommitpreedit ?
"UpdatePreeditTextWithMode" : "UpdatePreeditText");
dbus_message_iter_init_append(sig, &it);
appendibustext(&it, text, 1);
dbus_message_iter_append_basic(&it, DBUS_TYPE_UINT32, &cursor);
dbus_message_iter_append_basic(&it, DBUS_TYPE_BOOLEAN, &visible);
if(ctx->clientcommitpreedit)
dbus_message_iter_append_basic(&it, DBUS_TYPE_UINT32, &mode);
dbus_connection_send(ctx->conn, sig, nil);
dbus_message_unref(sig);
} }
if(dbus_connection_send(ctx->conn, sig, nil)){ if(text[0] != '\0')
if(text[0] != '\0') preowner = ctx;
preowner = ctx; else if(preowner == ctx)
else if(preowner == ctx) preowner = nil;
preowner = nil;
}
dbus_message_unref(sig);
} }
/* Another frontend may have taken the engine; then our preedit is stale. */ /* Another frontend may have taken the engine; then our preedit is stale. */
@@ -456,7 +492,7 @@ flushcontext(Ictx *ctx, int op)
emitpreedit(ctx, ""); emitpreedit(ctx, "");
stoutf(&res.commit, commit, sizeof commit); stoutf(&res.commit, commit, sizeof commit);
if(commit[0] != '\0') if(commit[0] != '\0')
emitcommit(ctx->conn, ctx->path, commit); emitcommit(ctx, commit);
} }
/* Nobody is left to hand text to: the context or its connection is gone. */ /* Nobody is left to hand text to: the context or its connection is gone. */
@@ -490,13 +526,26 @@ dropconncontexts(DBusConnection *conn)
dropcontext(&contexts[i]); dropcontext(&contexts[i]);
} }
/*
* A key is proof of focus: some clients never send FocusIn, and the
* focus events of two applications can cross.
*/
static int static int
processkey(Ictx *ctx, u32int sym, u32int state, Keyres *res) processkey(Ictx *ctx, u32int sym, u32int state, char *text, int n, Keyres *res)
{ {
if(state & Relmask || !ctx->focused || hidden(ctx)) text[0] = '\0';
if(state & Relmask || hidden(ctx))
return 0; return 0;
sendrequest(ctx, Keypress, ipckeysym(sym, xkb_keysym_to_utf32(sym)), ctx->focused = 1;
ipcmod(state), res); /* An IBus client drops a dead key we do not take: compose it here. */
if(composekey(sym, text, n))
return -1;
if(text[0] != '\0')
/* Composed text follows whatever was pending. */
sendrequest(ctx, Keyreset, 0, 0, res);
else
sendrequest(ctx, Keypress, ipckeysym(sym, xkb_keysym_to_utf32(sym)),
ipcmod(state), res);
if(preowner != nil && preowner != ctx) if(preowner != nil && preowner != ctx)
checkpreowner(); checkpreowner();
return 1; return 1;
@@ -600,29 +649,66 @@ handlekey(DBusConnection *c, DBusMessage *m, Ictx *ctx)
{ {
dbus_uint32_t sym, code, state; dbus_uint32_t sym, code, state;
Keyres res; Keyres res;
char commit[Maxutf], preedit[Maxutf]; char commit[Maxutf], preedit[Maxutf], text[Maxutf];
int restart; int restart, rv;
if(!dbus_message_get_args(m, nil, dbus_message_get_args(m, nil, DBUS_TYPE_UINT32, &sym,
DBUS_TYPE_UINT32, &sym, DBUS_TYPE_UINT32, &code, DBUS_TYPE_UINT32, &state,
DBUS_TYPE_UINT32, &code, DBUS_TYPE_INVALID);
DBUS_TYPE_UINT32, &state, rv = processkey(ctx, sym, state, text, sizeof text, &res);
DBUS_TYPE_INVALID)) if(rv <= 0) /* below zero: a sequence in the making */
return handleerror(c, m, DBUS_ERROR_INVALID_ARGS, return replybool(c, m, rv < 0);
"ProcessKeyEvent expects three unsigned integers");
if(!processkey(ctx, sym, state, &res))
return replybool(c, m, 0);
stoutf(&res.commit, commit, sizeof commit); stoutf(&res.commit, commit, sizeof commit);
stoutf(&res.preedit, preedit, sizeof preedit); stoutf(&res.preedit, preedit, sizeof preedit);
/* A commit ends the preedit around it, so clients place it right. */ /* A commit ends the preedit around it, so clients place it right. */
restart = clientpreedit(ctx) && commit[0] != '\0' && preowner == ctx; restart = clientpreedit(ctx) && commit[0] != '\0' && preowner == ctx;
ctx->keying = 1;
if(restart) if(restart)
emitpreedit(ctx, ""); emitpreedit(ctx, "");
if(commit[0] != '\0') if(commit[0] != '\0')
emitcommit(c, dbus_message_get_path(m), commit); emitcommit(ctx, commit);
if(text[0] != '\0')
emitcommit(ctx, text);
if(clientpreedit(ctx) && (!restart || preedit[0] != '\0')) if(clientpreedit(ctx) && (!restart || preedit[0] != '\0'))
emitpreedit(ctx, preedit); emitpreedit(ctx, preedit);
return replybool(c, m, res.eaten); ctx->keying = 0;
return replybool(c, m, res.eaten || text[0] != '\0');
}
/* Properties.Get PostProcessKeyEvent: what the last key produced. */
static DBusHandlerResult
handlepost(DBusConnection *c, DBusMessage *m, Ictx *ctx)
{
DBusMessage *r;
DBusMessageIter it, v, a, st;
Post *p;
int i;
r = dbus_message_new_method_return(m);
if(r == nil)
die("ibus: out of memory");
dbus_message_iter_init_append(r, &it);
dbus_message_iter_open_container(&it, DBUS_TYPE_VARIANT, "a(yv)", &v);
dbus_message_iter_open_container(&v, DBUS_TYPE_ARRAY, "(yv)", &a);
for(i = 0; i < ctx->npost; i++){
p = &ctx->post[i];
dbus_message_iter_open_container(&a, DBUS_TYPE_STRUCT, nil, &st);
dbus_message_iter_append_basic(&st, DBUS_TYPE_BYTE, &p->type);
appendibustext(&st, p->text, p->type != 'c');
dbus_message_iter_close_container(&a, &st);
if(p->type == 'c')
continue;
dbus_message_iter_open_container(&a, DBUS_TYPE_STRUCT, nil, &st);
dbus_message_iter_append_basic(&st, DBUS_TYPE_BYTE, &p->type);
appendibustext(&st, p->pos, 0);
dbus_message_iter_close_container(&a, &st);
}
dbus_message_iter_close_container(&v, &a);
dbus_message_iter_close_container(&it, &v);
dbus_connection_send(c, r, nil);
dbus_message_unref(r);
ctx->npost = 0;
return DBUS_HANDLER_RESULT_HANDLED;
} }
static DBusHandlerResult static DBusHandlerResult
@@ -657,10 +743,7 @@ handlecap(DBusConnection *c, DBusMessage *m, Ictx *ctx)
char preedit[Maxutf]; char preedit[Maxutf];
u32int old; u32int old;
if(!dbus_message_get_args(m, nil, DBUS_TYPE_UINT32, &cap, dbus_message_get_args(m, nil, DBUS_TYPE_UINT32, &cap, DBUS_TYPE_INVALID);
DBUS_TYPE_INVALID))
return handleerror(c, m, DBUS_ERROR_INVALID_ARGS,
"SetCapabilities expects one unsigned integer");
old = ctx->cap; old = ctx->cap;
ctx->cap = cap; ctx->cap = cap;
if(ctx->focused){ if(ctx->focused){
@@ -715,7 +798,7 @@ getcontent(DBusMessageIter *value, u32int *purpose)
return 1; return 1;
} }
/* Only ContentType and ClientCommitPreedit are settable; none is readable. */ /* ContentType, ClientCommitPreedit, and EffectivePostProcessKeyEvent are settable. */
static DBusHandlerResult static DBusHandlerResult
handlepropertyset(DBusConnection *c, DBusMessage *m, Ictx *ctx) handlepropertyset(DBusConnection *c, DBusMessage *m, Ictx *ctx)
{ {
@@ -741,19 +824,23 @@ handlepropertyset(DBusConnection *c, DBusMessage *m, Ictx *ctx)
flushcontext(ctx, Keyreset); flushcontext(ctx, Keyreset);
return reply(c, m, DBUS_TYPE_INVALID, nil); return reply(c, m, DBUS_TYPE_INVALID, nil);
} }
if(strcmp(name, "ClientCommitPreedit") == 0){ if(strcmp(name, "ClientCommitPreedit") == 0 ||
strcmp(name, "EffectivePostProcessKeyEvent") == 0){
if(dbus_message_iter_get_arg_type(&value) != DBUS_TYPE_STRUCT) if(dbus_message_iter_get_arg_type(&value) != DBUS_TYPE_STRUCT)
return handleerror(c, m, DBUS_ERROR_INVALID_ARGS, return handleerror(c, m, DBUS_ERROR_INVALID_ARGS,
"ClientCommitPreedit expects (b)"); "the property expects (b)");
dbus_message_iter_recurse(&value, &st); dbus_message_iter_recurse(&value, &st);
if(dbus_message_iter_get_arg_type(&st) != DBUS_TYPE_BOOLEAN) if(dbus_message_iter_get_arg_type(&st) != DBUS_TYPE_BOOLEAN)
return handleerror(c, m, DBUS_ERROR_INVALID_ARGS, return handleerror(c, m, DBUS_ERROR_INVALID_ARGS,
"ClientCommitPreedit expects (b)"); "the property expects (b)");
dbus_message_iter_get_basic(&st, &b); dbus_message_iter_get_basic(&st, &b);
if(dbus_message_iter_next(&st)) if(dbus_message_iter_next(&st))
return handleerror(c, m, DBUS_ERROR_INVALID_ARGS, return handleerror(c, m, DBUS_ERROR_INVALID_ARGS,
"ClientCommitPreedit expects (b)"); "the property expects (b)");
ctx->clientcommitpreedit = b != FALSE; if(name[0] == 'C')
ctx->clientcommitpreedit = b != FALSE;
else
ctx->postprocess = b != FALSE;
return reply(c, m, DBUS_TYPE_INVALID, nil); return reply(c, m, DBUS_TYPE_INVALID, nil);
} }
return handleerror(c, m, DBUS_ERROR_UNKNOWN_PROPERTY, return handleerror(c, m, DBUS_ERROR_UNKNOWN_PROPERTY,
@@ -796,35 +883,35 @@ handlecursor(DBusConnection *c, DBusMessage *m, Ictx *ctx)
{ {
dbus_int32_t x, y, w, h; dbus_int32_t x, y, w, h;
if(!dbus_message_get_args(m, nil, dbus_message_get_args(m, nil, DBUS_TYPE_INT32, &x, DBUS_TYPE_INT32, &y,
DBUS_TYPE_INT32, &x, DBUS_TYPE_INT32, &y, DBUS_TYPE_INT32, &w, DBUS_TYPE_INT32, &h, DBUS_TYPE_INVALID);
DBUS_TYPE_INT32, &w, DBUS_TYPE_INT32, &h,
DBUS_TYPE_INVALID))
return handleerror(c, m, DBUS_ERROR_INVALID_ARGS,
"SetCursorLocation expects four integers");
if(w < 0 || h < 0)
return handleerror(c, m, DBUS_ERROR_INVALID_ARGS,
"cursor dimensions must not be negative");
setcursor(ctx, x, y, h); setcursor(ctx, x, y, h);
return reply(c, m, DBUS_TYPE_INVALID, nil); return reply(c, m, DBUS_TYPE_INVALID, nil);
} }
/* Argument-free InputContext calls, checked for an empty signature. */ /* SetEngine: there is one engine, so any name is fine. */
static DBusHandlerResult static DBusHandlerResult
handleplain(DBusConnection *c, DBusMessage *m, Ictx *ctx, const char *member) handleok(DBusConnection *c, DBusMessage *m, Ictx*)
{ {
if(!dbus_message_has_signature(m, "")) return reply(c, m, DBUS_TYPE_INVALID, nil);
return handleerror(c, m, DBUS_ERROR_INVALID_ARGS,
"this call takes no arguments");
if(strcmp(member, "FocusIn") == 0)
return handlefocusin(c, m, ctx);
if(strcmp(member, "FocusOut") == 0)
return handlefocusout(c, m, ctx);
if(strcmp(member, "Reset") == 0)
return handlereset(c, m, ctx);
return handledestroy(c, m, ctx);
} }
/* The InputContext interface: member, argument signature, handler. */
static struct {
char *member;
char *sig;
DBusHandlerResult (*fn)(DBusConnection*, DBusMessage*, Ictx*);
} ictab[] = {
{"ProcessKeyEvent", "uuu", handlekey},
{"FocusIn", "", handlefocusin},
{"FocusOut", "", handlefocusout},
{"Reset", "", handlereset},
{"Destroy", "", handledestroy},
{"SetCursorLocation", "iiii", handlecursor},
{"SetCapabilities", "u", handlecap},
{"SetEngine", "s", handleok},
};
/* /*
* One handler for everything a client sends: the tiny slice of the bus * One handler for everything a client sends: the tiny slice of the bus
* that libibus needs, then the InputContext interface. Unhandled calls * that libibus needs, then the InputContext interface. Unhandled calls
@@ -835,6 +922,7 @@ onmsg(DBusConnection *c, DBusMessage *m, void*)
{ {
const char *iface, *member, *path; const char *iface, *member, *path;
Ictx *ctx; Ictx *ctx;
int i;
if(dbus_message_get_type(m) != DBUS_MESSAGE_TYPE_METHOD_CALL) if(dbus_message_get_type(m) != DBUS_MESSAGE_TYPE_METHOD_CALL)
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
@@ -861,14 +949,20 @@ onmsg(DBusConnection *c, DBusMessage *m, void*)
return handlecreate(c, m); return handlecreate(c, m);
} }
if(strcmp(iface, "org.freedesktop.DBus.Properties") == 0){ if(strcmp(iface, "org.freedesktop.DBus.Properties") == 0){
if(findcontext(c, path) == nil) ctx = findcontext(c, path);
if(ctx == nil)
return handleerror(c, m, DBUS_ERROR_UNKNOWN_OBJECT, return handleerror(c, m, DBUS_ERROR_UNKNOWN_OBJECT,
"unknown input context"); "unknown input context");
if(strcmp(member, "Set") == 0) if(strcmp(member, "Set") == 0)
return handlepropertyset(c, m, findcontext(c, path)); return handlepropertyset(c, m, ctx);
if(strcmp(member, "Get") == 0) if(strcmp(member, "Get") == 0){
if(dbus_message_get_args(m, nil, DBUS_TYPE_STRING, &iface,
DBUS_TYPE_STRING, &member, DBUS_TYPE_INVALID) &&
strcmp(member, "PostProcessKeyEvent") == 0)
return handlepost(c, m, ctx);
return handleerror(c, m, DBUS_ERROR_UNKNOWN_PROPERTY, return handleerror(c, m, DBUS_ERROR_UNKNOWN_PROPERTY,
"input context properties are write-only"); "unknown input context property");
}
if(strcmp(member, "GetAll") == 0) if(strcmp(member, "GetAll") == 0)
return handlepropertygetall(c, m); return handlepropertygetall(c, m);
} }
@@ -881,50 +975,39 @@ onmsg(DBusConnection *c, DBusMessage *m, void*)
if(ctx == nil) if(ctx == nil)
return handleerror(c, m, DBUS_ERROR_UNKNOWN_OBJECT, return handleerror(c, m, DBUS_ERROR_UNKNOWN_OBJECT,
"unknown input context"); "unknown input context");
if(strcmp(member, "ProcessKeyEvent") == 0) for(i = 0; i < nelem(ictab); i++){
return handlekey(c, m, ctx); if(strcmp(member, ictab[i].member) != 0)
if(strcmp(member, "FocusIn") == 0 || continue;
strcmp(member, "FocusOut") == 0 || if(!dbus_message_has_signature(m, ictab[i].sig))
strcmp(member, "Reset") == 0 ||
strcmp(member, "Destroy") == 0)
return handleplain(c, m, ctx, member);
if(strcmp(member, "SetCursorLocation") == 0)
return handlecursor(c, m, ctx);
if(strcmp(member, "SetCapabilities") == 0)
return handlecap(c, m, ctx);
if(strcmp(member, "SetEngine") == 0){
if(!dbus_message_has_signature(m, "s"))
return handleerror(c, m, DBUS_ERROR_INVALID_ARGS, return handleerror(c, m, DBUS_ERROR_INVALID_ARGS,
"SetEngine expects one engine name"); "bad arguments");
return reply(c, m, DBUS_TYPE_INVALID, nil); return ictab[i].fn(c, m, ctx);
} }
} }
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
} }
static void static void
newconn(DBusServer *s, DBusConnection *c, void *_) newconn(DBusServer*, DBusConnection *c, void*)
{ {
DBusObjectPathVTable vt; DBusObjectPathVTable vt;
USED(s); if(nconns >= Maxclients){
USED(_); fprint(2, "strans: ibus: rejecting client: %d-connection limit reached\n",
if(nconns >= Maxconns){ Maxclients);
fprintf(stderr, "strans: ibus: rejecting client: %d-connection limit reached\n",
Maxconns);
dbus_connection_close(c); dbus_connection_close(c);
return; return;
} }
if(!dbus_connection_set_watch_functions(c, addwatch, removewatch, if(!dbus_connection_set_watch_functions(c, addwatch, removewatch,
togglewatch, nil, nil)){ nil, nil, nil)){
fprintf(stderr, "strans: ibus: cannot watch client connection\n"); fprint(2, "strans: ibus: cannot watch client connection\n");
dbus_connection_close(c); dbus_connection_close(c);
return; return;
} }
memset(&vt, 0, sizeof(vt)); memset(&vt, 0, sizeof(vt));
vt.message_function = onmsg; vt.message_function = onmsg;
if(!dbus_connection_register_fallback(c, "/", &vt, nil)){ if(!dbus_connection_register_fallback(c, "/", &vt, nil)){
fprintf(stderr, "strans: ibus: cannot register client handler\n"); fprint(2, "strans: ibus: cannot register client handler\n");
dbus_connection_set_watch_functions(c, nil, nil, nil, nil, nil); dbus_connection_set_watch_functions(c, nil, nil, nil, nil, nil);
dbus_connection_close(c); dbus_connection_close(c);
return; return;
@@ -950,70 +1033,35 @@ pruneconns(void)
nconns = j; nconns = j;
} }
static int static void
ibusinit(void) ibusinit(void)
{ {
DBusError err; DBusError err;
char addr[128]; char addr[64], *full;
char *full;
int cleanupregistered;
full = nil; if(buildaddrpath(addrfile, sizeof addrfile) < 0)
cleanupregistered = 0; die("ibus: cannot build address path");
if(buildaddrpath(addrfile, sizeof(addrfile)) < 0){ snprintf(addr, sizeof addr, "unix:abstract=strans-%d", (int)getpid());
fprintf(stderr, "strans: ibus: cannot build address path\n");
return -1;
}
if(snprintf(addr, sizeof(addr), "unix:abstract=strans-%d",
(int)getpid()) >= (int)sizeof addr){
fprintf(stderr, "strans: ibus: address is too long\n");
return -1;
}
dbus_error_init(&err); dbus_error_init(&err);
srv = dbus_server_listen(addr, &err); srv = dbus_server_listen(addr, &err);
if(srv == nil){ if(srv == nil)
fprintf(stderr, "strans: ibus: listen: %s\n", err.message); die("ibus: listen: %s", err.message);
dbus_error_free(&err);
addrfile[0] = '\0';
return -1;
}
dbus_server_set_new_connection_function(srv, newconn, nil, nil); dbus_server_set_new_connection_function(srv, newconn, nil, nil);
if(!dbus_server_set_watch_functions(srv, addwatch, removewatch, if(!dbus_server_set_watch_functions(srv, addwatch, removewatch,
togglewatch, nil, nil)){ nil, nil, nil))
fprintf(stderr, "strans: ibus: cannot watch server\n"); die("ibus: cannot watch server");
goto fail;
}
full = dbus_server_get_address(srv); full = dbus_server_get_address(srv);
if(full == nil){ if(full == nil)
fprintf(stderr, "strans: ibus: cannot get server address\n"); die("ibus: cannot get server address");
goto fail; atexit(unlinkaddr);
}
if(!atexit(unlinkaddr)){
fprintf(stderr, "strans: ibus: cannot register address cleanup\n");
goto fail;
}
cleanupregistered = 1;
threadnotify(addrnote, 1); threadnotify(addrnote, 1);
if(writeaddr(addrfile, full) < 0){ if(writeaddr(addrfile, full) < 0)
fprintf(stderr, "strans: ibus: cannot write %s\n", addrfile); die("ibus: cannot write %s", addrfile);
goto fail;
}
dbus_free(full); dbus_free(full);
return 0;
fail:
if(cleanupregistered)
atexitdont(unlinkaddr);
if(full != nil)
dbus_free(full);
dbus_server_disconnect(srv);
dbus_server_unref(srv);
srv = nil;
addrfile[0] = '\0';
return -1;
} }
void void
ibusthread(void *_) ibusthread(void*)
{ {
struct pollfd pfds[Maxwatches]; struct pollfd pfds[Maxwatches];
DBusWatch *polled[Maxwatches]; DBusWatch *polled[Maxwatches];
@@ -1021,13 +1069,9 @@ ibusthread(void *_)
int i, n, rv; int i, n, rv;
unsigned int f; unsigned int f;
USED(_);
threadsetname("ibus"); threadsetname("ibus");
if(ibusinit() < 0) ibusinit();
die("ibus: initialization failed");
replyc = chancreate(sizeof(Keyres), 0); replyc = chancreate(sizeof(Keyres), 0);
if(replyc == nil)
die("ibus: cannot create reply channel");
for(;;){ for(;;){
if(!dbus_server_get_is_connected(srv)) if(!dbus_server_get_is_connected(srv))
die("ibus: server disconnected"); die("ibus: server disconnected");
@@ -1050,7 +1094,7 @@ ibusthread(void *_)
if(rv < 0 && errno == EINTR) if(rv < 0 && errno == EINTR)
continue; continue;
if(rv < 0) if(rv < 0)
die("ibus: poll: %s", strerror(errno)); die("ibus: poll: %r");
for(i = 0; i < n; i++){ for(i = 0; i < n; i++){
if(pfds[i].revents == 0) if(pfds[i].revents == 0)
continue; continue;

15
ipc.c
View File

@@ -165,20 +165,31 @@ ipcconnect(void)
/* /*
* Engine key for an X keysym and its Unicode value: printable characters * Engine key for an X keysym and its Unicode value: printable characters
* as themselves, the function keysyms 0xff00-0xffff as Kspec+offset. * as themselves, the function keysyms 0xff00-0xffff as Kspec+offset, and
* The keypad and Shift+Tab (ISO_Left_Tab) variants fold onto Enter and Tab. * no key at all for a modifier. The keypad keys and Shift+Tab
* (ISO_Left_Tab) fold onto their plain counterparts.
*/ */
uint32_t uint32_t
ipckeysym(uint32_t sym, uint32_t unicode) ipckeysym(uint32_t sym, uint32_t unicode)
{ {
if(unicode >= ' ' && unicode != 0x7f) if(unicode >= ' ' && unicode != 0x7f)
return unicode; return unicode;
if(sym >= 0xffe1 && sym <= 0xffee)
return 0;
switch(sym){ switch(sym){
case 0xfe20: case 0xfe20:
case 0xff89: case 0xff89:
return Ktab; return Ktab;
case 0xff8d: case 0xff8d:
return Kret; return Kret;
case 0xff97:
return Kup;
case 0xff99:
return Kdown;
case 0xff9a:
return Kpgup;
case 0xff9b:
return Kpgdown;
} }
if(sym >= 0xff00 && sym <= 0xffff) if(sym >= 0xff00 && sym <= 0xffff)
return Kspec + (sym - 0xff00); return Kspec + (sym - 0xff00);

8
ipc.h
View File

@@ -39,8 +39,12 @@ enum
Kdown = Kspec|0x54, Kdown = Kspec|0x54,
Kpgup = Kspec|0x55, Kpgup = Kspec|0x55,
Kpgdown = Kspec|0x56, Kpgdown = Kspec|0x56,
Kmodfirst = Kspec|0xe1, Khangul = Kspec|0x31,
Kmodlast = Kspec|0xee, Khanja = Kspec|0x34,
Kmuhenkan = Kspec|0x22,
Khenkan = Kspec|0x23,
Kkana = Kspec|0x27,
Kzenkaku = Kspec|0x2a,
/* X core modifier bits; GDK and IBus also flag Super at bit 26. */ /* X core modifier bits; GDK and IBus also flag Super at bit 26. */
Mshift = 1<<0, Mshift = 1<<0,

79
ko.c
View File

@@ -4,8 +4,6 @@
enum enum
{ {
Sbase = 0xAC00, Sbase = 0xAC00,
Jbase = 0x3131,
Jrange = 51,
Ncho = 19, Ncho = 19,
Njung = 21, Njung = 21,
Njong = 28, Njong = 28,
@@ -35,41 +33,21 @@ static Rune jong[] = {
L'', L'', L'', L'', L'', L'',
}; };
static int choidx[Jrange] = { /* A jamo's index in cho, jung, or jong, or -1; jong[0] is no jong. */
[L''-Jbase] = 1, [L''-Jbase] = 2, [L''-Jbase] = 3, static int
[L''-Jbase] = 4, [L''-Jbase] = 5, [L''-Jbase] = 6, idx(Rune *t, int n, Rune r)
[L''-Jbase] = 7, [L''-Jbase] = 8, [L''-Jbase] = 9, {
[L''-Jbase] = 10, [L''-Jbase] = 11, [L''-Jbase] = 12, int i;
[L''-Jbase] = 13, [L''-Jbase] = 14, [L''-Jbase] = 15,
[L''-Jbase] = 16, [L''-Jbase] = 17, [L''-Jbase] = 18,
[L''-Jbase] = 19,
};
static int jungidx[Jrange] = { for(i = 0; i < n; i++)
[L''-Jbase] = 1, [L''-Jbase] = 2, [L''-Jbase] = 3, if(t[i] == r)
[L''-Jbase] = 4, [L''-Jbase] = 5, [L''-Jbase] = 6, return i;
[L''-Jbase] = 7, [L''-Jbase] = 8, [L''-Jbase] = 9, return -1;
[L''-Jbase] = 10, [L''-Jbase] = 11, [L''-Jbase] = 12, }
[L''-Jbase] = 13, [L''-Jbase] = 14, [L''-Jbase] = 15,
[L''-Jbase] = 16, [L''-Jbase] = 17, [L''-Jbase] = 18,
[L''-Jbase] = 19, [L''-Jbase] = 20, [L''-Jbase] = 21,
};
static int jongidx[Jrange] = { #define Choidx(r) idx(cho, Ncho, r)
[L''-Jbase] = 2, [L''-Jbase] = 3, [L''-Jbase] = 4, #define Jungidx(r) idx(jung, Njung, r)
[L''-Jbase] = 5, [L''-Jbase] = 6, [L''-Jbase] = 7, #define Jongidx(r) idx(jong, Njong, r)
[L''-Jbase] = 8, [L''-Jbase] = 9, [L''-Jbase] = 10,
[L''-Jbase] = 11, [L''-Jbase] = 12, [L''-Jbase] = 13,
[L''-Jbase] = 14, [L''-Jbase] = 15, [L''-Jbase] = 16,
[L''-Jbase] = 17, [L''-Jbase] = 18, [L''-Jbase] = 19,
[L''-Jbase] = 20, [L''-Jbase] = 21, [L''-Jbase] = 22,
[L''-Jbase] = 23, [L''-Jbase] = 24, [L''-Jbase] = 25,
[L''-Jbase] = 26, [L''-Jbase] = 27, [L''-Jbase] = 28,
};
#define Choidx(r) (choidx[(r) - Jbase] - 1)
#define Jungidx(r) (jungidx[(r) - Jbase] - 1)
#define Jongidx(r) (jongidx[(r) - Jbase] - 1)
static Rune jamomap[128] = { static Rune jamomap[128] = {
['r'] = L'', ['R'] = L'', ['r'] = L'', ['R'] = L'',
@@ -221,12 +199,23 @@ transko(Im *im, Rune c)
sputr(&e.next, compose(ci, ji, 0)); sputr(&e.next, compose(ci, ji, 0));
return e; return e;
} }
if(Jungidx(last) >= 0 && ji >= 0){ /* A vowel typed first is reordered under its consonant. */
comb = combine(cvow, nelem(cvow), last, jm); if(Jungidx(last) >= 0 && Choidx(jm) >= 0){
if(comb){ sputr(&e.next, compose(Choidx(jm), Jungidx(last), 0));
sputr(&e.next, comb); return e;
return e; }
} /* Two vowels or two consonants may be one jamo, ㅘ or ㄳ. */
comb = combine(cvow, nelem(cvow), last, jm);
if(comb == 0)
comb = combine(cjong, nelem(cjong), last, jm);
if(comb){
sputr(&e.next, comb);
return e;
}
if(ji >= 0 && splitpair(cjong, nelem(cjong), last, &stay, &next)){
sputr(&e.s, stay);
sputr(&e.next, compose(Choidx(next), ji, 0));
return e;
} }
e.s = im->pre; e.s = im->pre;
sputr(&e.next, jm); sputr(&e.next, jm);
@@ -294,12 +283,10 @@ backko(Im *im)
last = slastr(&im->pre); last = slastr(&im->pre);
if(!issyl(last)){ if(!issyl(last)){
if(splitpair(cvow, nelem(cvow), last, &stay, &next)){
spopr(&im->pre);
sputr(&im->pre, stay);
return;
}
spopr(&im->pre); spopr(&im->pre);
if(splitpair(cvow, nelem(cvow), last, &stay, &next) ||
splitpair(cjong, nelem(cjong), last, &stay, &next))
sputr(&im->pre, stay);
return; return;
} }
decompose(last, &c, &j, &jo); decompose(last, &c, &j, &jo);

1
main.c
View File

@@ -55,6 +55,7 @@ threadmain(int argc, char **argv)
drawc = chancreate(sizeof(Drawcmd), 4); drawc = chancreate(sizeof(Drawcmd), 4);
keyc = chancreate(sizeof(Keyreq), 0); keyc = chancreate(sizeof(Keyreq), 0);
langinit(argv[1]); langinit(argv[1]);
composeinit();
srvinit(); srvinit();
proccreate(drawthread, nil, 16384); proccreate(drawthread, nil, 16384);
proccreate(srvthread, nil, 16384); proccreate(srvthread, nil, 16384);

View File

@@ -3,18 +3,19 @@
## Korean Hanja data ## Korean Hanja data
`hanja.src` is the tracked, reviewable source for Korean Hanja conversion. `hanja.src` is the tracked, reviewable source for Korean Hanja conversion.
Every data row is exactly one Hanja character, a tab, and one modern Hangul Every data row is Hanja, a tab, and its modern Hangul reading, a character
syllable: or a word:
``` ```
漢 한 漢 한
漢字 한자
``` ```
It contains no word rows such as `견출지` or `방학`. `mkhanja` validates this `mkhanja` validates that contract and groups rows by their reading to
contract and groups rows by their Hangul reading to produce the existing produce the runtime dictionary. Candidate order follows source order. The
runtime dictionary format. Candidate order follows source order. The source source keeps all retained pairs for review; the generated dictionary stores
keeps all retained pairs for review; the generated dictionary stores the first the first 128 candidates per reading because that is the engine's lookup
128 candidates per reading because that is the engine's lookup limit. limit.
The source is derived from libhangul release tag `libhangul-0.2.0`. The The source is derived from libhangul release tag `libhangul-0.2.0`. The
annotated tag object is `20afc38922e3595ee3ed5b186f2ea05afe663763`, its annotated tag object is `20afc38922e3595ee3ed5b186f2ea05afe663763`, its
@@ -30,14 +31,50 @@ map/libhangul2hanja upstream >map/hanja.src
map/mkhanja >map/hanja.dict map/mkhanja >map/hanja.dict
``` ```
`libhangul2hanja` retains only rows whose reading is one modern Hangul `libhangul2hanja` retains only rows whose reading is modern Hangul syllables
syllable and whose value is one Hanja character supported by the current throughout and whose value is Hanja the popup can draw: U+3400U+4DBF,
popup: U+3400U+4DBF, U+4E00U+9FFF, or U+F900U+FAFF. Thus word readings, U+4E00U+9FFF, or U+F900U+FAFF. Thus jamo readings, mixed values, and
multi-character values, jamo readings, and supplementary-plane ideographs supplementary-plane ideographs are omitted. The import preserves the upstream license header and row order.
are omitted. The import preserves the upstream license header and row order.
The retained data is BSD 3-Clause licensed by Choe Hwanjin; the complete text The retained data is BSD 3-Clause licensed by Choe Hwanjin; the complete text
is in `LICENSES/BSD-3-Clause-libhangul-hanja.txt`. is in `LICENSES/BSD-3-Clause-libhangul-hanja.txt`.
## Emoji and symbol data
`symbol.src` is hand-written: a symbol, a tab, and its ASCII aliases, kept
so that a bare `1`-`9` picks the matching superscript or subscript from a
`^` or `_` prefix search. `emoji.src` is generated: every fully-qualified
emoji of Unicode's `emoji-test.txt` except the skin-tone variants, with
its CLDR names and keywords in English, Korean, and Japanese. `mkemoji`
folds the aliases, adds each Japanese one in the other kana so a query
typed in either mode finds it, and writes one row per alias to
`emoji.dict`; the engine searches that dictionary by prefix, so the rows
carry no prefixes.
The sources are Emoji 17.0 and CLDR 48.2.0. Regenerate them from the
repository root with:
```sh
curl -L https://www.unicode.org/Public/17.0.0/emoji/emoji-test.txt -o emoji-test.txt
printf '%s %s\n' 1d8a944f88d7952f7ef7c5167fef3c67995bcae24543949710231b03a201acda emoji-test.txt | sha256sum -c -
for l in en ko ja; do
curl -L https://raw.githubusercontent.com/unicode-org/cldr-json/48.2.0/cldr-json/cldr-annotations-full/annotations/$l/annotations.json -o $l.json
curl -L https://raw.githubusercontent.com/unicode-org/cldr-json/48.2.0/cldr-json/cldr-annotations-derived-full/annotationsDerived/$l/annotations.json -o $l-derived.json
done
map/cldr2emoji emoji-test.txt en.json en-derived.json ko.json ko-derived.json ja.json ja-derived.json >map/emoji.src
map/mkemoji >map/emoji.dict
```
The data is under the Unicode License v3; the complete text is in
`LICENSES/Unicode-3.0.txt`.
## Japanese romaji
`hira.map` and `kata.map` are Mozc's default romaji table, one row per key,
in gojūon order; `kata.map` is the same table in Katakana, and
`make verify-map` checks that the two have the same keys. A doubled
consonant, or `tch`, maps to っ and keeps the consonant pending; the engine
knows that rule, the table only lists the rows.
## Japanese dictionary data ## Japanese dictionary data
`kanji.dict` is the historical dictionary bundled with strans. Its own header `kanji.dict` is the historical dictionary bundled with strans. Its own header

52
map/cldr2emoji Executable file
View File

@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""Write emoji.src: every fully-qualified emoji of Unicode's emoji-test.txt
but the skin-tone variants, each with its CLDR names and keywords in the
languages given, as result-first TAB-separated rows for mkemoji."""
import json
import sys
from pathlib import Path
def emoji(path):
out = []
for line in path.open(encoding="utf-8"):
if "; fully-qualified" not in line:
continue
cps = [int(cp, 16) for cp in line.split(";")[0].split()]
if any(0x1F3FB <= cp <= 0x1F3FF for cp in cps):
continue
out.append("".join(map(chr, cps)))
return out
def annotations(path):
"""CLDR annotations.json, plain or derived: emoji -> names, keywords."""
root = json.load(path.open(encoding="utf-8"))
table = root[next(iter(root))]["annotations"]
return {e: a.get("tts", []) + a.get("default", []) for e, a in table.items()}
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
if len(sys.argv) < 3:
print(f"usage: {sys.argv[0]} emoji-test.txt annotations.json...",
file=sys.stderr)
return 2
tables = [annotations(Path(arg)) for arg in sys.argv[2:]]
print("# Result first, then its CLDR names and keywords; see map/README.")
for e in emoji(Path(sys.argv[1])):
aliases = []
for table in tables:
for alias in table.get(e.replace("", ""), table.get(e, [])):
alias = alias.strip()
if alias and alias not in aliases:
aliases.append(alias)
if aliases:
print(e + "\t" + "\t".join(aliases))
return 0
if __name__ == "__main__":
sys.exit(main())

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

238096
map/hanja.src

File diff suppressed because it is too large Load Diff

View File

@@ -1,152 +1,321 @@
a あ a あ
- ー
i い i い
u う u う
e え e え
o お o お
ka か
ga が
ki き
kya きゃ kya きゃ
kyi きぃ
kyu きゅ kyu きゅ
kye きぇ
kyo きょ kyo きょ
gi ぎ ka か
gya ぎゃ ki き
gyu ぎゅ
gyo ぎょ
ku く ku く
gu ぐ
ke け ke け
ge げ
ko こ ko こ
kwa くぁ
kwi くぃ
kwu くぅ
kwe くぇ
kwo くぉ
gya ぎゃ
gyi ぎぃ
gyu ぎゅ
gye ぎぇ
gyo ぎょ
ga が
gi ぎ
gu ぐ
ge げ
go ご go ご
sa さ gwa ぐぁ
za ざ gwi ぐぃ
si し gwu ぐぅ
shi し gwe ぐぇ
gwo ぐぉ
sya しゃ
syi しぃ
syu しゅ
sye しぇ
syo しょ
sha しゃ sha しゃ
shi し
shu しゅ shu しゅ
she しぇ she しぇ
sho しょ sho しょ
syo しょ sa さ
zi si
su す
se せ
so そ
swa すぁ
swi すぃ
swu すぅ
swe すぇ
swo すぉ
z/ ・
z. …
z, ‥
zh ←
zj ↓
zk ↑
zl →
z- 〜
z[ 『
z] 』
zya じゃ
zyi じぃ
zyu じゅ
zye じぇ
zyo じょ
zwa ずぁ
zwi ずぃ
zwu ずぅ
zwe ずぇ
zwo ずぉ
za ざ
zi じ
zu ず
ze ぜ
zo ぞ
ja じゃ ja じゃ
ji じ
ju じゅ ju じゅ
je じぇ je じぇ
jo じょ jo じょ
su す jya じゃ
zu ず jyi じぃ
se せ jyu じゅ
ze ぜ jye じぇ
so そ jyo じょ
zo ぞ tya ちゃ
ta た tyi ちぃ
da だ
di ぢ
ti ち
chi ち
cha ちゃ
chu ちゅ
che ちぇ
tyu ちゅ tyu ちゅ
cho tye
ji じ tyo ちょ
tsa つぁ
tsi つぃ
tse つぇ
tso つぉ
tha てゃ
thi てぃ
t'i てぃ
thu てゅ
the てぇ
tho てょ
t'yu てゅ
twa とぁ
twi とぃ
twu とぅ
twe とぇ
two とぉ
t'u とぅ
ta た
ti ち
tu つ tu つ
tsu つ tsu つ
du づ
te て te て
de で
to と to と
dya ぢゃ
dyi ぢぃ
dyu ぢゅ
dye ぢぇ
dyo ぢょ
dha でゃ
dhi でぃ
d'i でぃ
dhu でゅ
dhe でぇ
dho でょ
d'yu でゅ
dwa どぁ
dwi どぃ
dwu どぅ
dwe どぇ
dwo どぉ
d'u どぅ
da だ
di ぢ
du づ
de で
do ど do ど
cha ちゃ
chi ち
chu ちゅ
che ちぇ
cho ちょ
cya ちゃ
cyi ちぃ
cyu ちゅ
cye ちぇ
cyo ちょ
ca か
ci し
cu く
ce せ
co こ
nya にゃ
nyi にぃ
nyu にゅ
nye にぇ
nyo にょ
n' ん
nn ん
n ん
na な na な
ni に ni に
nya にゃ
nyu にゅ
nyo にょ
nu ぬ nu ぬ
ne ね ne ね
no の no の
ha は
ba ば
va ば
pa ぱ
hi ひ
hya ひゃ hya ひゃ
hyi ひぃ
hyu ひゅ hyu ひゅ
hye ひぇ
hyo ひょ hyo ひょ
bi び hwa ふぁ
bya びゃ hwi ふぃ
byu びゅ hwe ふぇ
byo びょ hwo ふぉ
vi び hwyu ふゅ
pi ぴ ha は
pya ぴゃ hi ひ
pyu ぴゅ
pyo ぴょ
hu ふ hu ふ
fu ふ he へ
ho ほ
fa ふぁ fa ふぁ
fi ふぃ fi ふぃ
fu ふ
fe ふぇ fe ふぇ
fo ふぉ fo ふぉ
fya ふゃ
fyu ふゅ
fyo ふょ
bya びゃ
byi びぃ
byu びゅ
bye びぇ
byo びょ
ba ば
bi び
bu ぶ bu ぶ
vu ぶ
pu ぷ
he へ
be べ be べ
ve べ
pe ぺ
ho ほ
bo ぼ bo ぼ
vo ぼ pya ぴゃ
pyi ぴぃ
pyu ぴゅ
pye ぴぇ
pyo ぴょ
pa ぱ
pi ぴ
pu ぷ
pe ぺ
po ぽ po ぽ
mya みゃ
myi みぃ
myu みゅ
mye みぇ
myo みょ
ma ま ma ま
mi み mi み
mya みゃ
myu みゅ
myo みょ
mu む mu む
me め me め
mo も mo も
ye いぇ
ya や ya や
yu ゆ yu ゆ
yo よ yo よ
rya りゃ
ryi りぃ
ryu りゅ
rye りぇ
ryo りょ
ra ら ra ら
ri り ri り
rya りゃ
ryu りゅ
ryo りょ
ru る ru る
re れ re れ
ro ろ ro ろ
wu う
wyi ゐ
wye ゑ
wa わ wa わ
wi wi うぃ
we we うぇ
wo を wo を
n ん wha うぁ
whi うぃ
whu う
whe うぇ
who うぉ
va ゔぁ
vi ゔぃ
vu ゔ
ve ゔぇ
vo ゔぉ
vya ゔゃ
vyi ゔぃ
vyu ゔゅ
vye ゔぇ
vyo ゔょ
qa くぁ
qi くぃ
qu く
qe くぇ
qo くぉ
xn ん xn ん
xa ぁ xa ぁ
xi ぃ xi ぃ
xu ぅ xu ぅ
xe ぇ xe ぇ
xo ぉ xo ぉ
xyi ぃ
xye ぇ
xka ヵ
xke ヶ
xtu っ
xtsu っ
xya ゃ
xyu ゅ
xyo ょ
xwa ゎ
la ぁ
li ぃ
lu ぅ
le ぇ
lo ぉ
lyi ぃ
lye ぇ
lka ヵ
lke ヶ
ltu っ
ltsu っ
lya ゃ
lyu ゅ
lyo ょ
lwa ゎ
qq っ
vv っ
ll っ
xx っ
kk っ kk っ
ss っ
tt っ
pp っ
cc っ
gg っ gg っ
ss っ
zz っ zz っ
jj っ jj っ
tt っ
tch っ
dd っ dd っ
bb っ
hh っ hh っ
ff っ ff っ
bb っ
pp っ
mm っ mm っ
yy っ yy っ
rr っ rr っ
ww っ ww っ
vv cc
tch っ - ー
~ 〜
. 。 . 。
, 、 , 、
[ 「
] 」

View File

@@ -1,153 +1,321 @@
a ア a ア
- ー
i イ i イ
u ウ u ウ
e エ e エ
o オ o オ
ka カ
ga ガ
ki キ
kya キャ kya キャ
kyi キィ
kyu キュ kyu キュ
kye キェ
kyo キョ kyo キョ
gi ギ ka カ
gya ギャ ki キ
gyu ギュ
gyo ギョ
ku ク ku ク
gu グ
ke ケ ke ケ
ge ゲ
ko コ ko コ
kwa クァ
kwi クィ
kwu クゥ
kwe クェ
kwo クォ
gya ギャ
gyi ギィ
gyu ギュ
gye ギェ
gyo ギョ
ga ガ
gi ギ
gu グ
ge ゲ
go ゴ go ゴ
sa サ gwa グァ
za ザ gwi グィ
si シ gwu グゥ
shi シ gwe グェ
ji ジ gwo グォ
sha シャ sya シャ
she syi
shu シュ syu シュ
je sye
sho ショ
syo ショ syo ショ
ja sha
ju ジュ shi シ
jo ジョ shu シュ
she シェ
sho ショ
sa サ
si シ
su ス su ス
zu ズ
se セ se セ
ze ゼ
so ソ so ソ
swa スァ
swi スィ
swu スゥ
swe スェ
swo スォ
z/ ・
z. …
z, ‥
zh ←
zj ↓
zk ↑
zl →
z- 〜
z[ 『
z] 』
zya ジャ
zyi ジィ
zyu ジュ
zye ジェ
zyo ジョ
zwa ズァ
zwi ズィ
zwu ズゥ
zwe ズェ
zwo ズォ
za ザ
zi ジ
zu ズ
ze ゼ
zo ゾ zo ゾ
ta ja ジャ
da ダ ji ジ
ti ティ ju ジュ
chi チ je ジェ
zi ヂ jo ジョ
cha jya
chu チュ jyi ジィ
jyu ジュ
jye ジェ
jyo ジョ
tya チャ
tyi チィ
tyu チュ tyu チュ
che チェ tye チェ
cho チョ tyo チョ
tsa ツァ
tsi ツィ
tse ツェ
tso ツォ
tha テャ
thi ティ
t'i ティ
thu テュ
the テェ
tho テョ
t'yu テュ
twa トァ
twi トィ
twu トゥ
twe トェ
two トォ
t'u トゥ
ta タ
ti チ
tu ツ tu ツ
tsu ツ tsu ツ
du ヅ
te テ te テ
de デ
to ト to ト
dya ヂャ
dyi ヂィ
dyu ヂュ
dye ヂェ
dyo ヂョ
dha デャ
dhi ディ
d'i ディ
dhu デュ
dhe デェ
dho デョ
d'yu デュ
dwa ドァ
dwi ドィ
dwu ドゥ
dwe ドェ
dwo ドォ
d'u ドゥ
da ダ
di ヂ
du ヅ
de デ
do ド do ド
cha チャ
chi チ
chu チュ
che チェ
cho チョ
cya チャ
cyi チィ
cyu チュ
cye チェ
cyo チョ
ca カ
ci シ
cu ク
ce セ
co コ
nya ニャ
nyi ニィ
nyu ニュ
nye ニェ
nyo ニョ
n' ン
nn ン
n ン
na ナ na ナ
ni ニ ni ニ
nya ニャ
nyu ニュ
nyo ニョ
nu ヌ nu ヌ
ne ネ ne ネ
no no
ha ハ
ba バ
pa パ
hi ヒ
hya ヒャ hya ヒャ
hyi ヒィ
hyu ヒュ hyu ヒュ
hye ヒェ
hyo ヒョ hyo ヒョ
bi ビ hwa ファ
bya ビャ hwi フィ
byu ビュ hwe フェ
byo ビョ hwo フォ
pi ピ hwyu フュ
pya ピャ ha ハ
pyu ピュ hi ヒ
pyo ピョ
hu フ hu フ
fu フ
bu ブ
pu プ
he ヘ he ヘ
be ベ
pe ペ
ho ホ ho ホ
fa ファ
fi フィ
fu フ
fe フェ
fo フォ
fya フャ
fyu フュ
fyo フョ
bya ビャ
byi ビィ
byu ビュ
bye ビェ
byo ビョ
ba バ
bi ビ
bu ブ
be ベ
bo ボ bo ボ
pya ピャ
pyi ピィ
pyu ピュ
pye ピェ
pyo ピョ
pa パ
pi ピ
pu プ
pe ペ
po ポ po ポ
mya ミャ
myi ミィ
myu ミュ
mye ミェ
myo ミョ
ma マ ma マ
mi ミ mi ミ
mya ミャ
myu ミュ
myo ミョ
mu ム mu ム
me メ me メ
mo モ mo モ
ye イェ
ya ヤ ya ヤ
yu ユ yu ユ
yo ヨ yo ヨ
rya リャ
ryi リィ
ryu リュ
rye リェ
ryo リョ
ra ラ ra ラ
ri リ ri リ
rya リャ
ryu リュ
ryo リョ
ru ル ru ル
re レ re レ
ro ロ ro ロ
wu ウ
wyi ヰ
wye ヱ
wa ワ wa ワ
wi wi ウィ
we we ウェ
wo ヲ wo ヲ
n ン wha ウァ
xn ン whi ウィ
v ヴ whu ウ
xa ァ whe ウェ
xi ィ who ウォ
xu ゥ
xe ェ
xo ォ
cc ッ
dd ッ
kk ッ
pp ッ
tt ッ
tch ッ
ss ッ
gg ッ
zz ッ
jj ッ
bb ッ
hh ッ
ff ッ
mm ッ
yy ッ
rr ッ
ww ッ
vv ッ
di ディ
fa ファ
fi フィ
fe フェ
fo フォ
va ヴァ va ヴァ
vi ヴィ vi ヴィ
vu ヴ vu ヴ
ve ヴェ ve ヴェ
vo ヴォ vo ヴォ
vya ヴャ
vyi ヴィ
vyu ヴュ
vye ヴェ
vyo ヴョ
qa クァ
qi クィ
qu ク
qe クェ
qo クォ
xn ン
xa ァ
xi ィ
xu ゥ
xe ェ
xo ォ
xyi ィ
xye ェ
xka ヵ
xke ヶ
xtu ッ
xtsu ッ
xya ャ
xyu ュ
xyo ョ
xwa ヮ
la ァ
li ィ
lu ゥ
le ェ
lo ォ
lyi ィ
lye ェ
lka ヵ
lke ヶ
ltu ッ
ltsu ッ
lya ャ
lyu ュ
lyo ョ
lwa ヮ
qq ッ
vv ッ
ll ッ
xx ッ
kk ッ
gg ッ
ss ッ
zz ッ
jj ッ
tt ッ
tch ッ
dd ッ
hh ッ
ff ッ
bb ッ
pp ッ
mm ッ
yy ッ
rr ッ
ww ッ
cc ッ
- ー
~ 〜
. 。 . 。
, 、 , 、
[ 「
] 」

View File

@@ -1,21 +1,18 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Extract single-character Hanja readings from libhangul data.""" """Extract Hanja readings from libhangul data."""
import sys import sys
from pathlib import Path from pathlib import Path
def ishangul(s): def ishangul(s):
return len(s) == 1 and 0xAC00 <= ord(s) <= 0xD7A3 return s != "" and all(0xAC00 <= ord(c) <= 0xD7A3 for c in s)
def ishanja(s): def ishanja(s):
if len(s) != 1: return s != "" and all(0x3400 <= ord(c) <= 0x4DBF
return False or 0x4E00 <= ord(c) <= 0x9FFF
c = ord(s) or 0xF900 <= ord(c) <= 0xFAFF for c in s)
return (0x3400 <= c <= 0x4DBF
or 0x4E00 <= c <= 0x9FFF
or 0xF900 <= c <= 0xFAFF)
def extract(src, name): def extract(src, name):
@@ -44,7 +41,7 @@ def extract(src, name):
seen.add(pair) seen.add(pair)
entries.append(pair) entries.append(pair)
if not entries: if not entries:
raise ValueError(f"{name}: no single-character Hanja readings") raise ValueError(f"{name}: no Hanja readings")
return comments, entries return comments, entries

View File

@@ -1,13 +1,19 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Write emoji.dict to stdout from a result-first UTF-8 TSV file.""" """Write emoji.dict to stdout from result-first UTF-8 TSV files."""
import sys import sys
import unicodedata import unicodedata
from pathlib import Path from pathlib import Path
SOURCE = Path(__file__).with_name("emoji.src") SOURCES = [Path(__file__).with_name(name)
for name in ("symbol.src", "emoji.src")]
MAXRUNES = 64 MAXRUNES = 64
MAXCANDIDATES = 128
HIRA = {c: c + 0x60 for c in range(0x3041, 0x3097)}
KATA = {c: c - 0x60 for c in range(0x30A1, 0x30F7)}
def fold(s): def fold(s):
@@ -15,6 +21,12 @@ def fold(s):
return unicodedata.normalize("NFC", s) return unicodedata.normalize("NFC", s)
def kana(alias):
"""The alias, and in the other kana where it has any: a query typed
in either Japanese mode finds it."""
return {alias, alias.translate(HIRA), alias.translate(KATA)}
def hascontrol(s): def hascontrol(s):
return any(unicodedata.category(c) == "Cc" for c in s) return any(unicodedata.category(c) == "Cc" for c in s)
@@ -40,35 +52,29 @@ def read(path):
if (not alias or len(alias) > MAXRUNES or hascontrol(alias) if (not alias or len(alias) > MAXRUNES or hascontrol(alias)
or alias != alias.strip() or alias.startswith(";")): or alias != alias.strip() or alias.startswith(";")):
raise ValueError(f"{path}:{lineno}: bad alias") raise ValueError(f"{path}:{lineno}: bad alias")
entries.append((result, alias)) entries.extend((result, a) for a in sorted(kana(alias)))
return entries return entries
def add(table, key, result):
values = table.setdefault(key, [])
if result not in values:
values.append(result)
def build(entries): def build(entries):
exact = {} """One row per alias, in order of first appearance; the engine searches
prefix = {} the dictionary by prefix, so no prefix rows are needed."""
table = {}
for result, alias in entries: for result, alias in entries:
for n in range(1, len(alias) + 1): values = table.setdefault(alias, [])
add(exact if n == len(alias) else prefix, alias[:n], result) if result not in values:
for key in sorted(exact.keys() | prefix.keys()): values.append(result)
values = exact.get(key, []) + prefix.get(key, []) for alias, values in table.items():
values = list(dict.fromkeys(values)) yield f"{alias}\t{' '.join(values[:MAXCANDIDATES])}"
yield f"{key}\t{' '.join(values)}"
def main(): def main():
if len(sys.argv) > 2: sys.stdout.reconfigure(encoding="utf-8")
print(f"usage: {sys.argv[0]} [emoji.src]", file=sys.stderr) sys.stderr.reconfigure(encoding="utf-8")
return 2 paths = [Path(arg) for arg in sys.argv[1:]] or SOURCES
path = Path(sys.argv[1]) if len(sys.argv) == 2 else SOURCE
try: try:
for line in build(read(path)): entries = [entry for path in paths for entry in read(path)]
for line in build(entries):
print(line) print(line)
except (OSError, UnicodeError, ValueError) as error: except (OSError, UnicodeError, ValueError) as error:
print(error, file=sys.stderr) print(error, file=sys.stderr)

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Write hanja.dict from a one-Hanja-per-row UTF-8 source.""" """Write hanja.dict from a Hanja-per-row UTF-8 source."""
import sys import sys
from pathlib import Path from pathlib import Path
@@ -10,16 +10,13 @@ MAXCANDIDATES = 128
def ishangul(s): def ishangul(s):
return len(s) == 1 and 0xAC00 <= ord(s) <= 0xD7A3 return s != "" and all(0xAC00 <= ord(c) <= 0xD7A3 for c in s)
def ishanja(s): def ishanja(s):
if len(s) != 1: return s != "" and all(0x3400 <= ord(c) <= 0x4DBF
return False or 0x4E00 <= ord(c) <= 0x9FFF
c = ord(s) or 0xF900 <= ord(c) <= 0xFAFF for c in s)
return (0x3400 <= c <= 0x4DBF
or 0x4E00 <= c <= 0x9FFF
or 0xF900 <= c <= 0xFAFF)
def read(path): def read(path):
@@ -42,9 +39,9 @@ def read(path):
raise ValueError(f"{path}:{lineno}: need Hanja<TAB>reading") raise ValueError(f"{path}:{lineno}: need Hanja<TAB>reading")
hanja, reading = fields hanja, reading = fields
if not ishanja(hanja): if not ishanja(hanja):
raise ValueError(f"{path}:{lineno}: need one BMP Hanja character") raise ValueError(f"{path}:{lineno}: need BMP Hanja")
if not ishangul(reading): if not ishangul(reading):
raise ValueError(f"{path}:{lineno}: need one Hangul syllable") raise ValueError(f"{path}:{lineno}: need Hangul syllables")
pair = (hanja, reading) pair = (hanja, reading)
if pair in seen: if pair in seen:
raise ValueError(f"{path}:{lineno}: duplicate Hanja reading") raise ValueError(f"{path}:{lineno}: duplicate Hanja reading")

View File

@@ -36,10 +36,16 @@ function add(k, v, id) {
row[k] = row[k] " " row[k] = row[k] " "
row[k] = row[k] v row[k] = row[k] v
} }
/^;;/ || /^[[:space:]]*$/ { /^;;/ {
if(!body) # the header, with its license notice, is kept
print
next
}
/^[[:space:]]*$/ {
next next
} }
{ {
body = 1
if(!match($0, /[[:space:]]+/)) if(!match($0, /[[:space:]]+/))
fail("missing candidate list") fail("missing candidate list")
key = substr($0, 1, RSTART-1) key = substr($0, 1, RSTART-1)

103
map/symbol.src Normal file
View File

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

View File

@@ -119,7 +119,7 @@ void
popuplayout(Drawcmd *dc, int areaw, int areah, Popup *p) popuplayout(Drawcmd *dc, int areaw, int areah, Popup *p)
{ {
char buf[32]; char buf[32];
int first, i, markrow, nall, nmark, npre, pad, total, width, y; int first, i, markrow, nall, nmark, npre, total, width, y;
memset(p, 0, sizeof *p); memset(p, 0, sizeof *p);
p->prey = -1; p->prey = -1;
@@ -127,14 +127,12 @@ popuplayout(Drawcmd *dc, int areaw, int areah, Popup *p)
p->rowsy = -1; p->rowsy = -1;
p->marky = -1; p->marky = -1;
p->sely = -1; p->sely = -1;
pad = PopupPad;
nall = min(max(dc->nkouho, 0), Maxdisp); nall = min(max(dc->nkouho, 0), Maxdisp);
npre = dc->pre.n != 0; npre = dc->pre.n != 0;
if((nall == 0 && !npre) || areaw <= 0 || areah <= 0) if((nall == 0 && !npre) || areaw <= 0 || areah <= 0)
return; return;
/* Rows first; a page marker when they do not all fit; and in a /* As many rows as fit, and a page marker when they do not all. */
* sliver of a work area the selected row alone, unpadded. */
total = max(dc->total, dc->first + nall); total = max(dc->total, dc->first + nall);
markrow = dc->first > 0 || nall < total; markrow = dc->first > 0 || nall < total;
p->n = fitrows(areah, npre, markrow, nall); p->n = fitrows(areah, npre, markrow, nall);
@@ -142,15 +140,8 @@ popuplayout(Drawcmd *dc, int areaw, int areah, Popup *p)
markrow = 1; markrow = 1;
p->n = fitrows(areah, npre, markrow, nall); p->n = fitrows(areah, npre, markrow, nall);
} }
if(p->n == 0 && markrow){ if(p->n == 0 && !npre)
markrow = 0; return;
p->n = fitrows(areah, npre, markrow, nall);
}
if(p->n == 0 && nall != 0){
npre = 0;
pad = 0;
p->n = max(min(nall, areah / Fontsz), 1);
}
if(p->n != 0 && dc->sel >= p->n) if(p->n != 0 && dc->sel >= p->n)
p->row0 = min(dc->sel - p->n + 1, nall - p->n); p->row0 = min(dc->sel - p->n + 1, nall - p->n);
first = dc->first + p->row0; first = dc->first + p->row0;
@@ -158,7 +149,8 @@ popuplayout(Drawcmd *dc, int areaw, int areah, Popup *p)
if(nmark == 0) if(nmark == 0)
markrow = 0; markrow = 0;
width = PopupBasew; /* Rows share one steady width; a preedit alone hugs its text. */
width = p->n != 0 ? PopupBasew : 2*PopupPad;
if(npre) if(npre)
width = max(width, textwidth(&dc->pre) + 2*PopupPad); width = max(width, textwidth(&dc->pre) + 2*PopupPad);
for(i = 0; i < p->n; i++) for(i = 0; i < p->n; i++)
@@ -171,7 +163,7 @@ popuplayout(Drawcmd *dc, int areaw, int areah, Popup *p)
} }
p->w = min(width, areaw); p->w = min(width, areaw);
y = pad; y = PopupPad;
if(npre){ if(npre){
p->prey = y; p->prey = y;
y += Fontsz; y += Fontsz;
@@ -188,7 +180,7 @@ popuplayout(Drawcmd *dc, int areaw, int areah, Popup *p)
p->marky = y; p->marky = y;
y += Fontsz; y += Fontsz;
} }
p->h = min(y + pad, areah); p->h = min(y + PopupPad, areah);
p->textw = max(p->w - 2*PopupPad - PopupNumw, 0); p->textw = max(p->w - 2*PopupPad - PopupNumw, 0);
if(markrow){ if(markrow){
p->markw = min(p->markw, max(p->w - 2*PopupPad, 0)); p->markw = min(p->markw, max(p->w - 2*PopupPad, 0));
@@ -206,10 +198,6 @@ popupdraw(u32int *img, Drawcmd *dc, Popup *p)
u32int color; u32int color;
int i, j, y; int i, j, y;
if(img == nil || p->n < 0 || p->n > Maxdisp ||
p->row0 < 0 || p->row0 + p->n > dc->nkouho ||
p->w <= 0 || p->h <= 0 || p->h > Imgh)
return;
fill(img, p->w*p->h, Colbg); fill(img, p->w*p->h, Colbg);
if(p->prey >= 0) if(p->prey >= 0)
textdraw(img, p->w, p->h, PopupPad, p->prey, textdraw(img, p->w, p->h, PopupPad, p->prey,

6
srv.c
View File

@@ -67,7 +67,7 @@ srvreadreq(int fd, Keyreq *kr, int *want)
} }
/* /*
* One Keyreq persists for the connection: the negotiated capability and * One Keyreq persists for the connection: the negotiated preedit and
* the last caret ride along with every request, and the socket closing * the last caret ride along with every request, and the socket closing
* releases the engine. * releases the engine.
*/ */
@@ -85,10 +85,10 @@ clientthread(void *arg)
memset(&kr, 0, sizeof kr); memset(&kr, 0, sizeof kr);
kr.reply = chancreate(sizeof(Keyres), 0); kr.reply = chancreate(sizeof(Keyres), 0);
kr.owner = &fd; kr.owner = &fd;
kr.cap = Cclientpreedit; kr.clientpre = 1;
while(srvreadreq(fd, &kr, &want) >= 0){ while(srvreadreq(fd, &kr, &want) >= 0){
if(kr.op != Keycaret) if(kr.op != Keycaret)
kr.cap = want ? Cclientpreedit : 0; kr.clientpre = want;
chansend(keyc, &kr); chansend(keyc, &kr);
chanrecv(kr.reply, &res); chanrecv(kr.reply, &res);
if(kr.op == Keycaret) if(kr.op == Keycaret)

550
strans.c
View File

@@ -3,9 +3,12 @@
static Im im; static Im im;
static void *activeowner; static void *activeowner;
static int activecap; static void *lostowner; /* the context the engine was taken from */
static Str lost;
static int clientpre; /* the owner draws its own preedit */
static Caret caret; static Caret caret;
static int candidatechosen; static Rune modemark; /* the mode the last key switched to */
static int okuriat = -1; /* where Shift marked the okurigana, or -1 */
static Drawcmd lastdraw; static Drawcmd lastdraw;
static Emit transjp(Im*, Rune); static Emit transjp(Im*, Rune);
static void backjp(Im*); static void backjp(Im*);
@@ -54,12 +57,12 @@ mapget(Trie *t, Str *key, Str *out)
return mapmatch(t, key, out) == TrieExact; return mapmatch(t, key, out) == TrieExact;
} }
/* sel is the chosen candidate; -1 while the user has not moved to one. */
static void static void
clearkouho(void) clearkouho(void)
{ {
im.nkouho = 0; im.nkouho = 0;
im.sel = -1; im.sel = -1;
candidatechosen = 0;
} }
static void static void
@@ -99,7 +102,28 @@ movekouho(int delta)
im.sel = 0; im.sel = 0;
if(im.sel >= im.nkouho) if(im.sel >= im.nkouho)
im.sel = im.nkouho - 1; im.sel = im.nkouho - 1;
candidatechosen = 1; }
/* Space and Tab step through the candidates and wrap around. */
static void
cyclekouho(int delta)
{
if(im.sel < 0)
im.sel = delta > 0 ? 0 : im.nkouho - 1;
else
im.sel = (im.sel + delta + im.nkouho) % im.nkouho;
}
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 int static int
@@ -122,10 +146,11 @@ isjp(Im *p)
} }
/* /*
* Japanese keeps completed kana in pre and an ambiguous romaji syllable in * Japanese keeps completed kana, and any romaji that never became kana,
* raw. A mapped raw suffix is part of the reading even while it remains * in pre, and the romaji still being typed in raw. A mapped raw suffix
* pending (notably n, which may still grow into nya). Returns whether * is part of the reading even while it remains pending (notably n,
* the reading is complete. * which may still grow into nya). Returns whether the reading is
* complete.
*/ */
static int static int
jpreading(Trie *map, Str *pre, Str *raw, Str *s) jpreading(Trie *map, Str *pre, Str *raw, Str *s)
@@ -170,8 +195,12 @@ snapshot(Drawcmd *dc)
pre = search.text; pre = search.text;
else else
impre(&im, &pre); impre(&im, &pre);
if(!(activecap & Cclientpreedit)) if(!clientpre)
dc->pre = pre; dc->pre = pre;
if(search.lang && pre.n == 0) /* a search just begun shows itself */
sputr(&dc->pre, search.lang == LangEMOJI ? L'' : L'');
else if(modemark != 0 && pre.n == 0 && im.nkouho == 0)
sputr(&dc->pre, modemark);
first = pagefirst(); first = pagefirst();
n = im.nkouho - first; n = im.nkouho - first;
if(n > Maxdisp) n = Maxdisp; if(n > Maxdisp) n = Maxdisp;
@@ -230,17 +259,107 @@ redraw(void)
channbsend(drawc, &dc); channbsend(drawc, &dc);
} }
/* Kanji candidates for a complete reading. */ /* SKK's letter for the okurigana a kana begins: its row, or its vowel. */
static Rune
okuriletter(Rune r)
{
static char *rows[] = {
"aあ", "iい", "uう", "eえ", "oお",
"kかきくけこ", "gがぎぐげご", "sさしすせそ", "zざじずぜぞ",
"tたちつてとっ", "dだぢづでど", "nなにぬねのん", "hはひふへほ",
"bばびぶべぼ", "pぱぴぷぺぽ", "mまみむめも", "yやゆよ",
"rらりるれろ", "wわ",
};
int i;
for(i = 0; i < nelem(rows); i++)
if(utfrune(rows[i]+1, r) != nil)
return rows[i][0];
return 0;
}
/*
* SKK keys a verb or adjective by its stem and the letter of the
* okurigana that follows (かk: 書, 描, ...). The split the user marked
* is tried alone; otherwise every split is, longest stem first. The
* okurigana is put back, and a っ ending the stem (いっt: 言, 行) is
* written in kana too.
*/
static void
dictqokuri(Str *reading, int split)
{
Str key, okuri, kouho[Maxkouho];
Rune c;
int i, j, n;
for(i = split >= 0 ? split : reading->n - 1; i > 0; i--){
c = okuriletter(reading->r[i]);
if(c == 0)
break;
key = *reading;
key.n = i;
sputr(&key, c);
n = dictlookup(im.l->dict, &key, kouho, Maxkouho);
sclear(&okuri);
for(j = i - (reading->r[i-1] == L''); j < reading->n; j++)
sputr(&okuri, reading->r[j]);
for(j = 0; j < n; j++){
sappend(&kouho[j], &okuri);
addkouho(&kouho[j]);
}
if(split >= 0)
break;
}
}
/* Kanji candidates for a complete Japanese reading; none is chosen yet. */
static void static void
dictqjp(void) dictqjp(void)
{ {
Str reading; Str reading, exact[Maxkouho];
int i, n;
clearkouho(); clearkouho();
if(!jpreading(im.l->map, &im.pre, &im.raw, &reading) || reading.n == 0) if(!isjp(&im) || !jpreading(im.l->map, &im.pre, &im.raw, &reading) ||
reading.n == 0)
return; return;
im.nkouho = dictlookup(im.l, &reading, im.kouho, Maxkouho); /* What Shift marked comes first; the dictionary's own readings next. */
selectfirst(); if(okuriat > 0 && okuriat < reading.n)
dictqokuri(&reading, okuriat);
n = dictlookup(im.l->dict, &reading, exact, Maxkouho);
for(i = 0; i < n; i++)
addkouho(&exact[i]);
dictqokuri(&reading, -1);
}
/* The reading in Katakana is the last candidate, as in every IME. */
static void
katakouho(void)
{
Str reading, k;
int i;
if(!jpreading(im.l->map, &im.pre, &im.raw, &reading))
return;
k = reading;
for(i = 0; i < k.n; i++)
if(k.r[i] >= L'' && k.r[i] <= L'')
k.r[i] += L'' - L'';
if(scmp(&k, &reading) != 0)
addkouho(&k);
}
/* What the popup shows for a moment when the mode changes. */
static Rune
langmark(int lang)
{
switch(lang){
case LangJP: return L'';
case LangJPK: return L'';
case LangKO: return L'';
case LangVI: return L'ă';
}
return L'A';
} }
static int static int
@@ -251,6 +370,8 @@ setlang(int c)
l = getlang(c); l = getlang(c);
if(l == nil) if(l == nil)
return 0; return 0;
if(l != im.l)
modemark = langmark(l->lang);
im.l = l; im.l = l;
return 1; return 1;
} }
@@ -260,34 +381,37 @@ commitim(Im *p, Str *com)
{ {
Str val; Str val;
if(isjp(p)) impre(p, &val);
jpreading(p->l->map, &p->pre, &p->raw, &val);
else if(!mapget(p->l->map, &p->pre, &val))
val = p->pre;
sappend(com, &val); sappend(com, &val);
sclear(&p->pre); sclear(&p->pre);
sclear(&p->raw); sclear(&p->raw);
} }
static void /* Deletes the last kana as shown, not the last key: kya loses ゃ. */
commit(Str *com)
{
commitim(&im, com);
}
static void static void
backjp(Im *p) backjp(Im *p)
{ {
if(p->raw.n != 0) Str v;
spopr(&p->raw);
else if(p->raw.n == 0)
spopr(&p->pre); spopr(&p->pre);
else if(mapget(p->l->map, &p->raw, &v)){
spopr(&v);
sappend(&p->pre, &v);
sclear(&p->raw);
}else
spopr(&p->raw);
} }
/*
* kk and the like are a sokuon: っ, and the consonant stays pending, as
* ch does after tch. xtu and ltu are っ itself.
*/
static int static int
issmalltsu(Str *s) issokuon(Str *key, Str *mapped)
{ {
return s->n == 1 && (s->r[0] == L'' || s->r[0] == L''); return mapped->n == 1 && (mapped->r[0] == L'' || mapped->r[0] == L'') &&
(key->r[0] == key->r[1] || (key->r[0] == 't' && key->r[1] == 'c'));
} }
static void static void
@@ -311,36 +435,31 @@ transjp(Im *p, Rune c)
e.eat = 1; e.eat = 1;
e.next = p->pre; e.next = p->pre;
e.raw = p->raw; e.raw = p->raw;
if(c == '\'' && e.raw.n == 1 && e.raw.r[0] == 'n'){
if(mapget(p->l->map, &e.raw, &mapped))
sappend(&e.next, &mapped);
sclear(&e.raw);
return e;
}
key = e.raw; key = e.raw;
sputr(&key, c); sputr(&key, c);
if(mapmatch(p->l->map, &key, &mapped) != TrieMiss){ if(mapmatch(p->l->map, &key, &mapped) != TrieMiss){
e.raw = key; e.raw = key;
if(issmalltsu(&mapped)){ if(issokuon(&key, &mapped)){
sappend(&e.next, &mapped); sappend(&e.next, &mapped);
keeptail(&e.raw); keeptail(&e.raw);
} }
return e; return e;
} }
if(e.raw.n != 0 && mapget(p->l->map, &e.raw, &mapped)){ /* The pending romaji is done: kana if it is complete, else the
sappend(&e.next, &mapped); * letters as typed, for Backspace to mend. */
sclear(&e.raw); if(!mapget(p->l->map, &e.raw, &mapped))
sclear(&key); mapped = e.raw;
sputr(&key, c); sappend(&e.next, &mapped);
if(mapmatch(p->l->map, &key, &mapped) != TrieMiss){ sclear(&e.raw);
e.raw = key; sclear(&key);
return e; sputr(&key, c);
} if(mapmatch(p->l->map, &key, &mapped) != TrieMiss){
e.raw = key;
return e;
} }
/* The key starts no syllable: flush the reading and pass it on. */ /* The key starts no syllable: flush the reading and pass it on. */
jpreading(p->l->map, &e.next, &e.raw, &e.s); e.s = e.next;
sclear(&e.next); sclear(&e.next);
sclear(&e.raw);
e.eat = 0; e.eat = 0;
return e; return e;
} }
@@ -354,10 +473,7 @@ dotrans(Rune c, Str *com)
sappend(com, &e.s); sappend(com, &e.s);
im.pre = e.next; im.pre = e.next;
im.raw = e.raw; im.raw = e.raw;
if(e.eat && isjp(&im)) dictqjp();
dictqjp();
else
clearkouho();
return e.eat; return e.eat;
} }
@@ -402,37 +518,39 @@ transmap(Im *p, Rune c)
return e; return e;
} }
/* ASCII only: what the maps and dictionaries fold too. */
static Rune static Rune
ctrlkey(Rune c) lower(Rune c)
{ {
if(c >= 'A' && c <= 'Z') if(c >= 'A' && c <= 'Z')
c += 'a' - 'A'; c += 'a' - 'A';
return c;
}
static Rune
ctrlkey(Rune c)
{
c = lower(c);
if(c >= 'a' && c <= 'z') if(c >= 'a' && c <= 'z')
return c - 'a' + 1; return c - 'a' + 1;
return c; return c;
} }
/* Caps Lock does not shift a Korean key; Shift does. */
static Rune static Rune
kokey(Rune c, u32int mod) kokey(Rune c, u32int mod)
{ {
if(c >= 'A' && c <= 'Z') c = lower(c);
c += 'a' - 'A';
if((mod & Mshift) && c >= 'a' && c <= 'z') if((mod & Mshift) && c >= 'a' && c <= 'z')
c -= 'a' - 'A'; c -= 'a' - 'A';
return c; return c;
} }
static int /* A modifier press or an unmapped key is key 0: it engages nothing. */
ismodkey(u32int ks)
{
return ks >= Kmodfirst && ks <= Kmodlast;
}
/* Modifier presses and unmapped keys never engage the engine. */
int int
keymeaningful(u32int ks) keymeaningful(u32int ks)
{ {
return ks != 0 && !ismodkey(ks); return ks != 0;
} }
static void static void
@@ -441,25 +559,30 @@ foldascii(Str *s)
int i; int i;
for(i = 0; i < s->n; i++) for(i = 0; i < s->n; i++)
if(s->r[i] >= 'A' && s->r[i] <= 'Z') s->r[i] = lower(s->r[i]);
s->r[i] += 'a' - 'A';
} }
/* What typing raw in language l would produce, composition included. */ /*
* What typing raw in language l would produce, composition included,
* after pre, which may be nil, was already pending.
*/
void void
transstr(Lang *l, Str *raw, Str *out) transstr(Lang *l, Str *pre, Str *raw, Str *out)
{ {
Im q; Im q;
Emit e; Emit e;
int i; int i;
sclear(out);
if(l->trans == nil){
*out = *raw;
return;
}
memset(&q, 0, sizeof q); memset(&q, 0, sizeof q);
q.l = l; q.l = l;
if(pre != nil)
q.pre = *pre;
sclear(out);
if(l->trans == nil){
sappend(out, &q.pre);
sappend(out, raw);
return;
}
for(i = 0; i < raw->n; i++){ for(i = 0; i < raw->n; i++){
e = l->trans(&q, raw->r[i]); e = l->trans(&q, raw->r[i]);
sappend(out, &e.s); sappend(out, &e.s);
@@ -471,56 +594,45 @@ transstr(Lang *l, Str *raw, Str *out)
commitim(&q, out); commitim(&q, out);
} }
static void /*
addkouho(Str *s) * The typed keys and their transliteration are both emoji queries, folded
{ * as the dictionary is; the query shown is the keys while they match
int i; * something, else the transliteration, as typed.
*/
for(i = 0; i < im.nkouho; i++)
if(scmp(&im.kouho[i], s) == 0)
return;
if(im.nkouho < Maxkouho)
im.kouho[im.nkouho++] = *s;
}
/* The typed keys and their transliteration are both emoji queries. */
static void static void
emojiquery(void) emojiquery(void)
{ {
Str kouho[Maxkouho], raw, local; Str kouho[Maxkouho], raw, local;
int i, n, rawhit, localhit; int i, n, rawhit;
raw = search.raw; raw = search.raw;
foldascii(&raw); foldascii(&raw);
transstr(im.l, &search.raw, &local); transstr(im.l, nil, &search.raw, &search.text);
local = search.text;
foldascii(&local); foldascii(&local);
clearkouho(); clearkouho();
n = dictlookup(getlang(LangEMOJI), &raw, kouho, Maxkouho); n = dictprefix(getlang(LangEMOJI)->dict, &raw, kouho, Maxkouho);
rawhit = n != 0; rawhit = n != 0;
for(i = 0; i < n; i++) for(i = 0; i < n; i++)
addkouho(&kouho[i]); addkouho(&kouho[i]);
localhit = 0;
if(scmp(&raw, &local) != 0){ if(scmp(&raw, &local) != 0){
n = dictlookup(getlang(LangEMOJI), &local, kouho, Maxkouho); n = dictprefix(getlang(LangEMOJI)->dict, &local, kouho, Maxkouho);
localhit = n != 0;
for(i = 0; i < n; i++) for(i = 0; i < n; i++)
addkouho(&kouho[i]); addkouho(&kouho[i]);
} }
search.text = rawhit || !localhit ? raw : local; if(rawhit)
search.text = search.raw;
selectfirst(); selectfirst();
} }
/* The keys typed since Ctrl+H go on composing from the seed. */
static void static void
hanjaquery(void) hanjaquery(void)
{ {
Str key; transstr(im.l, &search.seed, &search.raw, &search.text);
transstr(im.l, &search.raw, &key);
search.text = search.seed;
sappend(&search.text, &key);
clearkouho(); clearkouho();
im.nkouho = dictlookup(getlang(LangHANJA), &search.text, im.kouho, im.nkouho = dictlookup(getlang(LangHANJA)->dict, &search.text,
Maxkouho); im.kouho, Maxkouho);
selectfirst(); selectfirst();
} }
@@ -539,13 +651,20 @@ searchlang(Rune c)
return c == LangEMOJI || c == LangHANJA ? c : 0; return c == LangEMOJI || c == LangHANJA ? c : 0;
} }
/*
* A chord with Alt or Super, or Ctrl with Shift, is the application's
* (Ctrl+Shift+V pastes); only a plain Ctrl+letter is a strans command.
*/
static int
chord(u32int mod)
{
return (mod & ~Mshift) != 0 && mod != Mctrl;
}
static void static void
endsearch(void) endsearch(void)
{ {
search.lang = 0; memset(&search, 0, sizeof search);
sclear(&search.seed);
sclear(&search.raw);
sclear(&search.text);
clearkouho(); clearkouho();
} }
@@ -554,15 +673,39 @@ reset(void)
{ {
sclear(&im.pre); sclear(&im.pre);
sclear(&im.raw); sclear(&im.raw);
okuriat = -1;
endsearch(); endsearch();
memset(&caret, 0, sizeof caret);
} }
/*
* Whatever is pending becomes committed text: the search query, a chosen
* candidate, else the reading. Enter does this, and so do a reset and a
* lost focus.
*/
static void static void
commitsearch(Str *com) flush(Str *com)
{ {
sappend(com, &search.text); if(search.lang)
endsearch(); sappend(com, &search.text);
else if(im.sel >= 0)
sappend(com, &im.kouho[im.sel]);
else
commitim(&im, com);
reset();
}
/*
* Focus can be lost after the next context's keys arrive, so the engine
* changes hands with text still pending. The context it was taken from
* gets that text back when it is next heard from.
*/
static void
takelost(void *owner, Str *com)
{
if(owner != lostowner)
return;
sappend(com, &lost);
lostowner = nil;
} }
/* /*
@@ -572,36 +715,18 @@ commitsearch(Str *com)
static void static void
startsearch(int lang, Str *com) startsearch(int lang, Str *com)
{ {
Str pending; Str seed;
impre(&im, &pending); sclear(&seed);
if(lang == LangHANJA){ if(lang == LangHANJA && !search.lang && im.sel < 0)
sclear(&im.pre); impre(&im, &seed);
sclear(&im.raw);
}else
commit(com);
commitsearch(com);
search.lang = lang;
if(lang == LangHANJA){
search.seed = pending;
searchquery();
}
}
/*
* Whatever is pending becomes committed text, a moved-to candidate
* first: Enter does this, and so do a reset and a lost focus.
*/
static void
flush(Str *com)
{
if(search.lang)
commitsearch(com);
else if(candidatechosen && im.sel >= 0 && im.sel < im.nkouho)
sappend(com, &im.kouho[im.sel]);
else else
commit(com); flush(com);
reset(); reset();
search.lang = lang;
search.seed = seed;
if(lang == LangHANJA)
searchquery();
} }
static void static void
@@ -630,24 +755,15 @@ searchkey(u32int ks, u32int mod, Str *com)
return 1; return 1;
} }
if(ks == Ktab){ if(ks == Ktab){
if(im.nkouho == 0) if(im.nkouho != 0)
return 1; cyclekouho(mod & Mshift ? -1 : 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++;
return 1; return 1;
} }
if(ks == Kret){ if(ks == Kret || ks == ' '){
if(im.nkouho > 0) if(im.nkouho > 0)
picksearch(max(im.sel, 0), com); picksearch(max(im.sel, 0), com);
else else
commitsearch(com); flush(com);
return 1; return 1;
} }
if(ks == Kback){ if(ks == Kback){
@@ -663,26 +779,26 @@ searchkey(u32int ks, u32int mod, Str *com)
return 1; return 1;
} }
if(ks == Kesc){ if(ks == Kesc){
im.pre = search.seed; /* the syllable Ctrl+H took is pending again */
endsearch(); endsearch();
return 1; return 1;
} }
if(ks >= Kspec || (ks < ' ' && !(mod & Mctrl)) || if(ks >= Kspec || (ks < ' ' && !(mod & Mctrl)) || chord(mod)){
(mod & (Malt|Msuper))){ flush(com);
commitsearch(com);
return 0; return 0;
} }
if(mod & Mctrl){ if(mod == Mctrl){
c = ctrlkey(ks); c = ctrlkey(ks);
n = searchlang(c); n = searchlang(c);
if(n != 0 && n != search.lang){ if(n != 0 && n != search.lang){
startsearch(n, com); startsearch(n, com);
return 1; return 1;
} }
commitsearch(com); flush(com);
return n != 0 || setlang(c); return n != 0 || setlang(c);
} }
if(search.raw.n >= Maxrunes){ if(search.raw.n >= Maxrunes){
commitsearch(com); flush(com);
return 0; return 0;
} }
sputr(&search.raw, ks); sputr(&search.raw, ks);
@@ -693,15 +809,38 @@ searchkey(u32int ks, u32int mod, Str *com)
static int static int
transition(u32int ks, u32int mod, Str *com) transition(u32int ks, u32int mod, Str *com)
{ {
Str mark;
int n; int n;
Rune c; Rune c;
if(ismodkey(ks)) modemark = 0; /* any key after the switch takes its mark away */
return 0; /* Shift on a romaji letter marks the okurigana, as SKK's capital does. */
if(isjp(&im) && (mod & Mshift) && ks >= 'A' && ks <= 'Z' && haspre(&im)){
impre(&im, &mark);
okuriat = mark.n;
}
if(im.l->lang == LangKO) if(im.l->lang == LangKO)
ks = kokey(ks, mod); ks = kokey(ks, mod);
else if(isjp(&im) && ks >= 'A' && ks <= 'Z') else if(isjp(&im))
ks += 'a' - 'A'; /* romaji is case-blind, Caps Lock included */ ks = lower(ks); /* romaji is case-blind, Caps Lock included */
/* The Korean and Japanese keyboards' own keys, as the Ctrl chords
* and Space they stand for. */
switch(ks){
case Khangul: ks = im.l->lang == LangKO ? 't' : 's'; mod = Mctrl; break;
case Khanja: ks = 'h'; mod = Mctrl; break;
case Kzenkaku: ks = isjp(&im) ? 't' : 'n'; mod = Mctrl; break;
case Kkana: ks = im.l->lang == LangJP ? 'k' : 'n'; mod = Mctrl; break;
case Khenkan:
if(isjp(&im)){
ks = ' ';
mod &= Mshift;
}else{
ks = 'n';
mod = Mctrl;
}
break;
case Kmuhenkan: ks = 't'; mod = Mctrl; break;
}
if(search.lang) if(search.lang)
return searchkey(ks, mod, com); return searchkey(ks, mod, com);
n = movedelta(ks); n = movedelta(ks);
@@ -709,79 +848,77 @@ transition(u32int ks, u32int mod, Str *com)
movekouho(n); movekouho(n);
return 1; return 1;
} }
n = numberedkouho(ks, mod); /* Digits pick a candidate only once the list has been engaged. */
n = im.sel >= 0 ? numberedkouho(ks, mod) : -1;
if(n >= 0){ if(n >= 0){
sappend(com, &im.kouho[n]); sappend(com, &im.kouho[n]);
reset(); reset();
return 1; return 1;
} }
if(ks == Ktab || ks == Kret){ if((ks == ' ' || ks == Ktab) && isjp(&im) && haspre(&im) &&
!(mod & ~Mshift)){
/* Space and Tab convert: they step through the candidates,
* Katakana last; a reading with none is committed. */
katakouho();
if(im.nkouho != 0)
cyclekouho(mod & Mshift ? -1 : 1);
else
flush(com);
return 1;
}
if(ks == Ktab || ks == Kret || (ks == Kesc && !isjp(&im))){
if(!haspre(&im)) if(!haspre(&im))
return 0; return 0;
flush(com); flush(com);
/* Japanese confirms with the key; Korean and Telex let it on. */ /* Japanese confirms with the key; Korean and Telex let it on. */
return isjp(&im); return isjp(&im);
} }
if(ks == Kback){ if(ks == Kback || ks == Kesc){
if(!haspre(&im)) if(!haspre(&im))
return 0; return 0;
im.l->back(&im); if(im.sel >= 0)
if(!haspre(&im)){ im.sel = -1; /* back from the candidate to the reading */
else if(ks == Kesc)
reset(); reset();
return 1; else{
im.l->back(&im);
if(haspre(&im))
dictqjp();
else
reset();
} }
if(isjp(&im))
dictqjp();
else
clearkouho();
return 1; return 1;
} }
if(ks == Kesc){ if(ks >= Kspec || chord(mod)){
if(!haspre(&im)) flush(com);
return 0;
reset();
return 1;
}
if(ks >= Kspec || (mod & (Malt|Msuper))){
commit(com);
reset();
return 0; return 0;
} }
if(mod & Mctrl){ if(mod == Mctrl){
c = ctrlkey(ks); c = ctrlkey(ks);
n = searchlang(c); n = searchlang(c);
if(n != 0){ if(n != 0){
startsearch(n, com); startsearch(n, com);
return 1; return 1;
} }
commit(com); flush(com);
reset();
return setlang(c); return setlang(c);
} }
if(ks == '0' && im.nkouho > 0){ if(ks == '0' && isjp(&im) && haspre(&im)){
commit(com); im.sel = -1; /* 0 commits the reading, as 1-9 pick candidates */
reset(); flush(com);
return 1;
}
if(ks > 0x7f || ks == ' '){
if(!haspre(&im))
return 0;
commit(com);
reset();
if(com->n >= Maxrunes)
return 0;
sputr(com, ks);
return 1; return 1;
} }
if(im.l->trans == nil) if(im.l->trans == nil)
return 0; return 0;
if(im.sel >= 0)
flush(com); /* typing on takes the chosen candidate */
if(im.pre.n >= Maxrunes || if(im.pre.n >= Maxrunes ||
(isjp(&im) && im.pre.n + im.raw.n >= Maxrunes)){ (isjp(&im) && im.pre.n + im.raw.n >= Maxrunes)){
/* A full reading commits; a pending romaji syllable stays. */ /* A full reading commits; a pending romaji syllable stays. */
if(isjp(&im)) if(isjp(&im))
sappend(com, &im.pre); sappend(com, &im.pre);
else else
commit(com); commitim(&im, com);
sclear(&im.pre); sclear(&im.pre);
clearkouho(); clearkouho();
} }
@@ -796,9 +933,11 @@ init(void)
im.sel = -1; im.sel = -1;
memset(&search, 0, sizeof search); memset(&search, 0, sizeof search);
memset(&caret, 0, sizeof caret); memset(&caret, 0, sizeof caret);
modemark = 0;
okuriat = -1;
activeowner = nil; activeowner = nil;
activecap = 0; lostowner = nil;
candidatechosen = 0; clientpre = 0;
memset(&lastdraw, 0, sizeof lastdraw); memset(&lastdraw, 0, sizeof lastdraw);
} }
@@ -819,13 +958,15 @@ imhandlekey(Keyreq *kr)
res.eaten = 1; res.eaten = 1;
switch(kr->op){ switch(kr->op){
case Keyrelease: case Keyrelease:
takelost(kr->owner, &res.commit);
if(kr->owner == activeowner){ if(kr->owner == activeowner){
flush(&res.commit); flush(&res.commit);
activeowner = nil; activeowner = nil;
activecap = 0; clientpre = 0;
} }
break; break;
case Keyreset: case Keyreset:
takelost(kr->owner, &res.commit);
if(kr->owner == activeowner) if(kr->owner == activeowner)
flush(&res.commit); flush(&res.commit);
break; break;
@@ -834,22 +975,23 @@ imhandlekey(Keyreq *kr)
caret = kr->caret; caret = kr->caret;
break; break;
case Keycap: case Keycap:
res.eaten = activeowner != nil && kr->owner == activeowner; res.eaten = kr->owner == activeowner;
if(res.eaten) if(res.eaten)
activecap = kr->cap & Cclientpreedit; clientpre = kr->clientpre;
break; break;
case Keypress: case Keypress:
res.eaten = 0; res.eaten = 0;
if(activeowner != nil && kr->owner == activeowner) if(kr->owner == activeowner)
activecap = kr->cap & Cclientpreedit; clientpre = kr->clientpre;
if(keymeaningful(kr->ks)){ if(keymeaningful(kr->ks)){
if(kr->owner != activeowner){ if(kr->owner != activeowner){
reset(); sclear(&lost);
flush(&lost);
lostowner = lost.n > 0 ? activeowner : nil;
activeowner = kr->owner; activeowner = kr->owner;
activecap = kr->cap & Cclientpreedit; clientpre = kr->clientpre;
caret = kr->caret; }
}else if(kr->caret.valid) caret = kr->caret;
caret = kr->caret;
res.eaten = transition(kr->ks, kr->mod, &res.commit); res.eaten = transition(kr->ks, kr->mod, &res.commit);
} }
break; break;

View File

@@ -3,7 +3,7 @@ LD = 9l
HOSTCC = cc HOSTCC = cc
PKG_CONFIG ?= pkg-config PKG_CONFIG ?= pkg-config
CFLAGS ?= -O2 -g CFLAGS ?= -O2 -g
UNIT_CPPFLAGS = -I.. -I../cutest UNIT_CPPFLAGS = -I..
UNIT_CFLAGS = -std=c99 -Wall -Wextra UNIT_CFLAGS = -std=c99 -Wall -Wextra
HOST_CFLAGS = -std=c99 -Wall -Wextra HOST_CFLAGS = -std=c99 -Wall -Wextra
TEXT_CFLAGS = $(shell $(PKG_CONFIG) --cflags pangocairo cairo fontconfig) TEXT_CFLAGS = $(shell $(PKG_CONFIG) --cflags pangocairo cairo fontconfig)
@@ -18,8 +18,8 @@ IBUS_CLIENT_CFLAGS = $(shell $(PKG_CONFIG) --cflags ibus-1.0)
IBUS_CLIENT_LIBS = $(shell $(PKG_CONFIG) --libs ibus-1.0) IBUS_CLIENT_LIBS = $(shell $(PKG_CONFIG) --libs ibus-1.0)
X11_CFLAGS = $(shell $(PKG_CONFIG) --cflags x11) X11_CFLAGS = $(shell $(PKG_CONFIG) --cflags x11)
X11_LIBS = $(shell $(PKG_CONFIG) --libs x11) X11_LIBS = $(shell $(PKG_CONFIG) --libs x11)
XIM_CFLAGS = $(shell $(PKG_CONFIG) --cflags xcb-imdkit xcb-xkb xkbcommon-x11) XIM_CFLAGS = $(shell $(PKG_CONFIG) --cflags xcb-imdkit xcb-aux xcb-xkb xkbcommon-x11)
XIM_LIBS = $(shell $(PKG_CONFIG) --libs xcb-imdkit xcb-xkb xkbcommon-x11) XIM_LIBS = $(shell $(PKG_CONFIG) --libs xcb-imdkit xcb-aux xcb-xkb xkbcommon-x11)
UNIT_LDLIBS = -lthread -lbio $(TEXT_LIBS) $(IBUS_LIBS) $(XIM_LIBS) UNIT_LDLIBS = -lthread -lbio $(TEXT_LIBS) $(IBUS_LIBS) $(XIM_LIBS)
PROG = unit_test PROG = unit_test
@@ -32,14 +32,13 @@ FAULT = daemon_collision_test daemon_failure_test daemon_restart_test
LIVE = $(SMOKE) $(FAULT) LIVE = $(SMOKE) $(FAULT)
TESTSRC = test_util.c str_test.c trie_test.c \ TESTSRC = test_util.c str_test.c trie_test.c \
ko_test.c vi_test.c engine_test.c dict_test.c ipc_test.c \ ko_test.c vi_test.c engine_test.c dict_test.c ipc_test.c \
popup_test.c font_test.c ibus_test.c server_test.c keymap_test.c \ popup_test.c font_test.c ibus_test.c server_test.c compose_test.c \
xim_adapter_test.c xim_adapter_test.c
TESTOBJ = $(TESTSRC:.c=.o) TESTOBJ = $(TESTSRC:.c=.o)
PARENTSRC = str.c trie.c dict.c ko.c vi.c ipc.c popup_layout.c \ PARENTSRC = str.c trie.c dict.c ko.c vi.c ipc.c popup_layout.c \
font.c font.c compose.c
PARENTOBJ = $(PARENTSRC:%.c=unit_%.o) PARENTOBJ = $(PARENTSRC:%.c=unit_%.o)
XIMOBJ = unit_xim_keymap.o COMMONOBJ = $(TESTOBJ) $(PARENTOBJ)
COMMONOBJ = $(TESTOBJ) $(PARENTOBJ) $(XIMOBJ)
OBJS = unit_test.o stress_test.o $(COMMONOBJ) OBJS = unit_test.o stress_test.o $(COMMONOBJ)
all: $(PROG) $(STRESS) $(LIVE) all: $(PROG) $(STRESS) $(LIVE)
@@ -123,14 +122,11 @@ unit_test.o stress_test.o $(TESTOBJ): test.h ../dat.h ../fn.h ../ipc.h \
engine_test.o: ../strans.c engine_test.o: ../strans.c
ibus_test.o: UNIT_CPPFLAGS += $(IBUS_CFLAGS) ibus_test.o: UNIT_CPPFLAGS += $(IBUS_CFLAGS)
ibus_test.o: ../ibus.c ibus_test.o: ../ibus.c
keymap_test.o xim_adapter_test.o: UNIT_CPPFLAGS += $(XIM_CFLAGS) xim_adapter_test.o: UNIT_CPPFLAGS += $(XIM_CFLAGS)
xim_adapter_test.o: ../xim/xim.c xim_adapter_test.o: ../xim.c
server_test.o: ../srv.c server_test.o: ../srv.c
unit_font.o: UNIT_CPPFLAGS += $(TEXT_CFLAGS) unit_font.o: UNIT_CPPFLAGS += $(TEXT_CFLAGS)
unit_compose.o compose_test.o: UNIT_CPPFLAGS += $(IBUS_CFLAGS)
unit_xim_keymap.o: ../xim/keymap.c
$(CC) $(CPPFLAGS) $(UNIT_CPPFLAGS) $(XIM_CFLAGS) $(UNIT_CFLAGS) \
$(CFLAGS) -c -o $@ $<
unit_%.o: ../%.c ../dat.h ../fn.h ../ipc.h unit_%.o: ../%.c ../dat.h ../fn.h ../ipc.h
$(CC) $(CPPFLAGS) $(UNIT_CPPFLAGS) $(UNIT_CFLAGS) $(CFLAGS) -c -o $@ $< $(CC) $(CPPFLAGS) $(UNIT_CPPFLAGS) $(UNIT_CFLAGS) $(CFLAGS) -c -o $@ $<

37
tests/compose_test.c Normal file
View File

@@ -0,0 +1,37 @@
#include "dat.h"
#include "fn.h"
#include "test.h"
#include <xkbcommon/xkbcommon-keysyms.h>
/* A sequence swallows its keys and hands its text back at the end. */
void
compose_sequences(struct ct *t)
{
static const struct {
u32int sym;
int composing;
char *text;
} keys[] = {
{ XKB_KEY_a, 0, "" },
{ XKB_KEY_dead_acute, 1, "" },
{ XKB_KEY_e, 0, "é" },
{ XKB_KEY_Multi_key, 1, "" },
{ XKB_KEY_o, 1, "" },
{ XKB_KEY_c, 0, "©" },
{ XKB_KEY_dead_acute, 1, "" },
{ XKB_KEY_Escape, 1, "" },
{ XKB_KEY_a, 0, "" },
};
char buf[Maxutf];
int i;
if(!CT_CHECK(t, setenv("XCOMPOSEFILE", "data/compose", 1) == 0))
return;
composeinit();
for(i = 0; i < nelem(keys); i++){
CT_EQ_INT(t, keys[i].composing,
composekey(keys[i].sym, buf, sizeof buf));
CT_EQ_STR(t, keys[i].text, buf);
}
unsetenv("XCOMPOSEFILE");
}

2
tests/data/compose Normal file
View File

@@ -0,0 +1,2 @@
<dead_acute> <e> : "é"
<Multi_key> <o> <c> : "©"

View File

@@ -1,3 +1,4 @@
;; converter fixture; the test transcodes this file to EUC-JP
かんじ 漢字 幹事 感じ かんじ 漢字 幹事 感じ
えがお 笑顔 えがお 笑顔
きごう 記号 普通 きごう 記号 普通
1 かんじ ;; converter fixture; the test transcodes this file to EUC-JP 漢字 幹事 感じ
1 ;; converter fixture; the test transcodes this file to EUC-JP
2 かんじ かんじ 漢字 幹事 感じ
3 えがお えがお 笑顔
4 きごう きごう 記号 普通

View File

@@ -15,10 +15,10 @@ dictionary_candidates(struct ct *t)
lang = getlang(LangJP); lang = getlang(LangJP);
saved = lang->dict; saved = lang->dict;
lang->dict = trienew(); lang->dict = trienew();
trieput(lang->dict, "かな", strlen("かな"), " 候補1 かな 候補2 ", trieput(lang->dict, "かな", strlen("かな"), " 候補1 候補2 ",
strlen(" 候補1 かな 候補2 ")); strlen(" 候補1 候補2 "));
key = mkstr("かな"); key = mkstr("かな");
n = dictlookup(lang, &key, kouho, Maxkouho); n = dictlookup(lang->dict, &key, kouho, Maxkouho);
if(CT_EQ_INT(t, 2, n)){ if(CT_EQ_INT(t, 2, n)){
checkstr(t, "candidate 1", "候補1", &kouho[0]); checkstr(t, "candidate 1", "候補1", &kouho[0]);
checkstr(t, "candidate 2", "候補2", &kouho[1]); checkstr(t, "candidate 2", "候補2", &kouho[1]);
@@ -34,13 +34,13 @@ dictionary_candidates(struct ct *t)
*p = '\0'; *p = '\0';
trieput(lang->dict, "key", 3, many, strlen(many)); trieput(lang->dict, "key", 3, many, strlen(many));
key = mkstr("key"); key = mkstr("key");
n = dictlookup(lang, &key, kouho, Maxkouho); n = dictlookup(lang->dict, &key, kouho, Maxkouho);
if(CT_EQ_INT(t, Maxkouho, n)){ if(CT_EQ_INT(t, Maxkouho, n)){
checkstr(t, "first capped candidate", "c00", &kouho[0]); checkstr(t, "first capped candidate", "c00", &kouho[0]);
snprint(item, sizeof item, "c%02d", Maxkouho-1); snprint(item, sizeof item, "c%02d", Maxkouho-1);
checkstr(t, "last capped candidate", item, &kouho[Maxkouho-1]); checkstr(t, "last capped candidate", item, &kouho[Maxkouho-1]);
} }
CT_EQ_INT(t, 3, dictlookup(lang, &key, kouho, 3)); CT_EQ_INT(t, 3, dictlookup(lang->dict, &key, kouho, 3));
trieclose(lang->dict); trieclose(lang->dict);
lang->dict = saved; lang->dict = saved;
} }
@@ -56,28 +56,33 @@ dictionary_misses(struct ct *t)
saved = lang->dict; saved = lang->dict;
lang->dict = trienew(); lang->dict = trienew();
key = mkstr(""); key = mkstr("");
CT_EQ_INT(t, 0, dictlookup(lang, &key, kouho, Maxkouho)); CT_EQ_INT(t, 0, dictlookup(lang->dict, &key, kouho, Maxkouho));
key = mkstr("missing"); key = mkstr("missing");
CT_EQ_INT(t, 0, dictlookup(lang, &key, kouho, Maxkouho)); CT_EQ_INT(t, 0, dictlookup(lang->dict, &key, kouho, Maxkouho));
CT_EQ_INT(t, 0, dictlookup(getlang(LangKO), &key, kouho, Maxkouho)); CT_EQ_INT(t, 0, dictlookup(getlang(LangKO)->dict, &key, kouho, Maxkouho));
trieclose(lang->dict); trieclose(lang->dict);
lang->dict = saved; lang->dict = saved;
} }
void void
dictionary_emoji_identity(struct ct *t) dictionary_prefix(struct ct *t)
{ {
Str kouho[Maxkouho], key; Str kouho[Maxkouho], key;
Trie *saved; Trie *dict;
Lang *lang;
lang = getlang(LangEMOJI); dict = trienew();
saved = lang->dict; trieput(dict, "smile", 5, "A B", 3);
lang->dict = trienew(); trieput(dict, "smiley", 6, "B C", 3);
trieput(lang->dict, "é", strlen("é"), "é", strlen("é")); trieput(dict, "sad", 3, "D", 1);
key = mkstr("é"); key = mkstr("smile");
if(CT_EQ_INT(t, 1, dictlookup(lang, &key, kouho, Maxkouho))) if(CT_EQ_INT(t, 4, dictprefix(dict, &key, kouho, Maxkouho))){
checkstr(t, "emoji identity candidate", "é", &kouho[0]); checkstr(t, "own entry first", "A", &kouho[0]);
trieclose(lang->dict); checkstr(t, "entry below", "C", &kouho[3]);
lang->dict = saved; }
key = mkstr("s");
CT_EQ_INT(t, 5, dictprefix(dict, &key, kouho, Maxkouho));
CT_EQ_INT(t, 2, dictprefix(dict, &key, kouho, 2));
key = mkstr("x");
CT_EQ_INT(t, 0, dictprefix(dict, &key, kouho, Maxkouho));
trieclose(dict);
} }

View File

@@ -10,7 +10,7 @@ keystroke(u32int ks, u32int mod, Str *com)
{ {
int eaten; int eaten;
eaten = transition(ks, mod, com); eaten = keymeaningful(ks) && transition(ks, mod, com);
redraw(); redraw();
return eaten; return eaten;
} }
@@ -52,7 +52,7 @@ vietnamese_state_lifetime(struct ct *t)
Str com; Str com;
init(); init();
im.l = &testvi; im.l = getlang(LangVI);
im.pre = mkstr("as"); im.pre = mkstr("as");
im.raw = mkstr("as"); im.raw = mkstr("as");
sclear(&com); sclear(&com);
@@ -62,17 +62,17 @@ vietnamese_state_lifetime(struct ct *t)
CT_EQ_INT(t, 0, im.raw.n); CT_EQ_INT(t, 0, im.raw.n);
init(); init();
im.l = &testvi; im.l = getlang(LangVI);
im.pre = mkstr("as"); im.pre = mkstr("as");
im.raw = mkstr("as"); im.raw = mkstr("as");
sclear(&com); sclear(&com);
CT_CHECK(t, keystroke(Kesc, 0, &com)); CT_CHECK(t, !keystroke(Kesc, 0, &com));
CT_EQ_INT(t, 0, com.n); checkstr(t, "Escape commits Telex", "á", &com);
CT_EQ_INT(t, 0, im.pre.n); CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n); CT_EQ_INT(t, 0, im.raw.n);
init(); init();
im.l = &testvi; im.l = getlang(LangVI);
im.pre = mkstr("as"); im.pre = mkstr("as");
im.raw = mkstr("as"); im.raw = mkstr("as");
sclear(&com); sclear(&com);
@@ -116,10 +116,13 @@ engine_backspace_clears_candidates(struct ct *t)
im.nkouho = 1; im.nkouho = 1;
im.sel = 0; im.sel = 0;
CT_CHECK(t, keystroke(Kback, 0, &com)); CT_CHECK(t, keystroke(Kback, 0, &com));
CT_EQ_INT(t, -1, im.sel);
checkstr(t, "chosen candidate backspace", "na", &im.raw);
CT_CHECK(t, keystroke(Kback, 0, &com));
checkstr(t, "backspace commit", "", &com); checkstr(t, "backspace commit", "", &com);
checkstr(t, "pending romaji after backspace", "n", &im.raw); checkstr(t, "backspace deletes the kana shown", "", &im.raw);
impre(&im, &shown); impre(&im, &shown);
checkstr(t, "shown preedit after backspace", "", &shown); checkstr(t, "shown preedit after backspace", "", &shown);
CT_EQ_INT(t, 0, im.nkouho); CT_EQ_INT(t, 0, im.nkouho);
CT_EQ_INT(t, -1, im.sel); CT_EQ_INT(t, -1, im.sel);
} }
@@ -162,7 +165,7 @@ engine_candidate_shortcut_modifiers(struct ct *t)
init(); init();
im.kouho[9] = mkstr("must-not-select"); im.kouho[9] = mkstr("must-not-select");
im.nkouho = 10; im.nkouho = 10;
im.sel = 9; im.sel = -1;
selected = im.kouho[9]; selected = im.kouho[9];
sclear(&com); sclear(&com);
CT_CHECK(t, !keystroke('1', mods[i], &com)); CT_CHECK(t, !keystroke('1', mods[i], &com));
@@ -181,7 +184,7 @@ ownerrequestatcap(void *owner, int cap, int op, Rune key, u32int mod,
reply = chancreate(sizeof(Keyres), 1); reply = chancreate(sizeof(Keyres), 1);
memset(&req, 0, sizeof req); memset(&req, 0, sizeof req);
req.owner = owner; req.owner = owner;
req.cap = cap; req.clientpre = cap;
req.op = op; req.op = op;
req.ks = key; req.ks = key;
req.mod = mod; req.mod = mod;
@@ -231,6 +234,7 @@ engine_active_owner_lifecycle(struct ct *t)
checkstr(t, "takeover resets old preedit", "", &res.preedit); checkstr(t, "takeover resets old preedit", "", &res.preedit);
CT_EQ_PTR(t, &b, activeowner); CT_EQ_PTR(t, &b, activeowner);
res = ownerrequest(&a, Keyreset, 0, 0); res = ownerrequest(&a, Keyreset, 0, 0);
checkstr(t, "the taken owner gets its reading back", "", &res.commit);
CT_EQ_INT(t, 0, res.preedit.n); CT_EQ_INT(t, 0, res.preedit.n);
impre(&im, &shown); impre(&im, &shown);
checkstr(t, "stale reset preserves owner", "", &shown); checkstr(t, "stale reset preserves owner", "", &shown);
@@ -249,7 +253,7 @@ engine_active_owner_lifecycle(struct ct *t)
CT_EQ_PTR(t, nil, activeowner); CT_EQ_PTR(t, nil, activeowner);
CT_EQ_INT(t, 0, im.pre.n); CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n); CT_EQ_INT(t, 0, im.raw.n);
ownerrequest(&c, Keypress, Kmodfirst, 0); ownerrequest(&c, Keypress, 0, 0);
CT_EQ_PTR(t, nil, activeowner); CT_EQ_PTR(t, nil, activeowner);
res = ownerrequest(&c, Keypress, 'k', 0); res = ownerrequest(&c, Keypress, 'k', 0);
CT_EQ_PTR(t, &c, activeowner); CT_EQ_PTR(t, &c, activeowner);
@@ -277,7 +281,6 @@ engine_active_owner_reset(struct ct *t)
CT_EQ_PTR(t, &owner, activeowner); CT_EQ_PTR(t, &owner, activeowner);
checkstr(t, "reset hands the reading back", "", &res.commit); checkstr(t, "reset hands the reading back", "", &res.commit);
CT_EQ_INT(t, 0, res.preedit.n); CT_EQ_INT(t, 0, res.preedit.n);
CT_CHECK(t, !caret.valid);
res = ownerrequest(&owner, Keypress, 'n', 0); res = ownerrequest(&owner, Keypress, 'n', 0);
checkstr(t, "same owner after reset", "", &res.preedit); checkstr(t, "same owner after reset", "", &res.preedit);
} }
@@ -354,11 +357,11 @@ engine_vietnamese_client_preedit(struct ct *t)
for(i = 0; i < nelem(cases); i++){ for(i = 0; i < nelem(cases); i++){
init(); init();
im.l = &testvi; im.l = getlang(LangVI);
draindraw(nil); draindraw(nil);
sclear(&com); sclear(&com);
for(k = cases[i].keys; *k != '\0'; k++){ for(k = cases[i].keys; *k != '\0'; k++){
res = ownerrequestcap(&owner, Cclientpreedit, res = ownerrequestcap(&owner, 1,
Keypress, *k, 0); Keypress, *k, 0);
sappend(&com, &res.commit); sappend(&com, &res.commit);
if(!res.eaten) if(!res.eaten)
@@ -369,7 +372,7 @@ engine_vietnamese_client_preedit(struct ct *t)
shown = com; shown = com;
sappend(&shown, &res.preedit); sappend(&shown, &res.preedit);
checkstr(t, "client preedit", cases[i].shown, &shown); checkstr(t, "client preedit", cases[i].shown, &shown);
activecap = 0; clientpre = 0;
redraw(); redraw();
if(CT_CHECK(t, draindraw(&dc) > 0)) if(CT_CHECK(t, draindraw(&dc) > 0))
CT_EQ_INT(t, 0, scmp(&dc.pre, &res.preedit)); CT_EQ_INT(t, 0, scmp(&dc.pre, &res.preedit));
@@ -401,7 +404,7 @@ engine_commit_contract(struct ct *t)
im.l = getlang(LangKO); im.l = getlang(LangKO);
sclear(&all); sclear(&all);
for(i = 0; i < 3; i++){ for(i = 0; i < 3; i++){
res = ownerrequestcap(&owner, Cclientpreedit, res = ownerrequestcap(&owner, 1,
Keypress, cases[j].key, 0); Keypress, cases[j].key, 0);
CT_CHECK(t, res.eaten); CT_CHECK(t, res.eaten);
checkstr(t, "repeated jamo commit", checkstr(t, "repeated jamo commit",
@@ -449,7 +452,7 @@ engine_commit_contract(struct ct *t)
return; return;
im.nkouho = 1; im.nkouho = 1;
im.sel = -1; im.sel = -1;
CT_CHECK(t, !keystroke('q', 0, &com)); CT_CHECK(t, !keystroke('!', 0, &com));
checkstr(t, "uneaten-key commit", "", &com); checkstr(t, "uneaten-key commit", "", &com);
CT_EQ_INT(t, 0, im.pre.n); CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n); CT_EQ_INT(t, 0, im.raw.n);
@@ -471,10 +474,9 @@ engine_commit_contract(struct ct *t)
sclear(&com); sclear(&com);
if(!typekeys(t, "ka", &com)) if(!typekeys(t, "ka", &com))
return; return;
CT_CHECK(t, keystroke(0xf008, 0, &com)); /* A key outside the map commits the reading and passes on. */
CT_EQ_INT(t, 2, com.n); CT_CHECK(t, !keystroke(0xf008, 0, &com));
CT_EQ_UINT(t, 0x304b, com.r[0]); checkstr(t, "non-ASCII key commit", "", &com);
CT_EQ_UINT(t, 0xf008, com.r[1]);
CT_EQ_INT(t, 0, im.pre.n); CT_EQ_INT(t, 0, im.pre.n);
} }
@@ -486,7 +488,7 @@ engine_telex_history_bound(struct ct *t)
int i; int i;
init(); init();
im.l = &testvi; im.l = getlang(LangVI);
sclear(&com); sclear(&com);
CT_CHECK(t, keystroke('a', 0, &com)); CT_CHECK(t, keystroke('a', 0, &com));
for(i = 0; i < Maxrunes; i++){ for(i = 0; i < Maxrunes; i++){
@@ -563,11 +565,25 @@ engine_korean_modifiers_and_backspace(struct ct *t)
void void
engine_direct_language_modes(struct ct *t) engine_direct_language_modes(struct ct *t)
{ {
Drawcmd dc;
Str com; Str com;
init(); init();
draindraw(nil);
sclear(&com); sclear(&com);
CT_EQ_INT(t, LangEN, im.l->lang); CT_EQ_INT(t, LangEN, im.l->lang);
/* A switch shows the mode it switched to until the next key. */
CT_CHECK(t, keystroke('s', Mctrl, &com));
if(CT_CHECK(t, draindraw(&dc) > 0))
checkstr(t, "mode mark", "", &dc.pre);
CT_CHECK(t, keystroke('t', Mctrl, &com));
if(CT_CHECK(t, draindraw(&dc) > 0))
checkstr(t, "mode mark", "A", &dc.pre);
CT_CHECK(t, !keystroke('a', 0, &com));
if(CT_CHECK(t, draindraw(&dc) > 0))
CT_EQ_INT(t, 0, dc.pre.n);
CT_CHECK(t, !keystroke('a', 0, &com));
CT_EQ_INT(t, 0, draindraw(nil));
CT_EQ_PTR(t, nil, im.l->mapname); CT_EQ_PTR(t, nil, im.l->mapname);
CT_EQ_PTR(t, nil, im.l->map); CT_EQ_PTR(t, nil, im.l->map);
CT_EQ_PTR(t, nil, im.l->trans); CT_EQ_PTR(t, nil, im.l->trans);
@@ -618,22 +634,15 @@ engine_language_switch_state(struct ct *t)
{ "commit Vietnamese", 't', Mctrl, LangEN, "", "", "á", 0 }, { "commit Vietnamese", 't', Mctrl, LangEN, "", "", "á", 0 },
}; };
char where[80]; char where[80];
Lang *vi;
Trie *saved;
Str com; Str com;
int eaten, i; int eaten, i;
vi = getlang(LangVI);
if(!CT_CHECK(t, vi != nil))
return;
saved = vi->map;
vi->map = testvi.map;
init(); init();
for(i = 0; i < nelem(steps); i++){ for(i = 0; i < nelem(steps); i++){
if(steps[i].stale){ if(steps[i].stale){
im.kouho[0] = mkstr("stale"); im.kouho[0] = mkstr("stale");
im.nkouho = 1; im.nkouho = 1;
im.sel = 0; im.sel = -1;
} }
sclear(&com); sclear(&com);
eaten = keystroke(steps[i].key, steps[i].mod, &com); eaten = keystroke(steps[i].key, steps[i].mod, &com);
@@ -673,7 +682,7 @@ engine_language_switch_state(struct ct *t)
CT_EQ_INT(t, 0, im.pre.n); CT_EQ_INT(t, 0, im.pre.n);
} }
init(); init();
im.l = &testvi; im.l = getlang(LangVI);
sclear(&com); sclear(&com);
if(typekeys(t, "as", &com)){ if(typekeys(t, "as", &com)){
CT_CHECK(t, !keystroke('p', Mctrl, &com)); CT_CHECK(t, !keystroke('p', Mctrl, &com));
@@ -681,7 +690,6 @@ engine_language_switch_state(struct ct *t)
CT_EQ_INT(t, 0, im.pre.n); CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n); CT_EQ_INT(t, 0, im.raw.n);
} }
vi->map = saved;
} }
typedef struct Searchfix Searchfix; typedef struct Searchfix Searchfix;
@@ -691,9 +699,8 @@ struct Searchfix
Search search; Search search;
Lang *dictlang; Lang *dictlang;
Trie *dict; Trie *dict;
int activecap; int clientpre;
Drawcmd lastdraw; Drawcmd lastdraw;
int candidatechosen;
}; };
static int static int
@@ -723,7 +730,6 @@ setcandidates(int n)
im.kouho[i] = mkstr(name); im.kouho[i] = mkstr(name);
} }
im.nkouho = n; im.nkouho = n;
selectfirst();
} }
static void static void
@@ -853,26 +859,34 @@ engine_candidate_page_movement(struct ct *t)
void void
engine_candidate_completion(struct ct *t) engine_candidate_completion(struct ct *t)
{ {
static Rune keys[] = { Kret, Ktab };
Str com; Str com;
int i;
for(i = 0; i < nelem(keys); i++){ init();
init(); im.l = getlang(LangJP);
im.l = getlang(LangJP); im.pre = mkstr("reading");
im.pre = mkstr("reading"); setcandidates(1);
setcandidates(1); sclear(&com);
sclear(&com); CT_CHECK(t, keystroke(Kret, 0, &com));
CT_CHECK(t, keystroke(keys[i], 0, &com)); checkstr(t, "unselected candidate commits reading", "reading", &com);
checkstr(t, "unselected candidate commits reading", CT_EQ_INT(t, 0, im.nkouho);
"reading", &com);
CT_EQ_INT(t, 0, im.nkouho); init();
} im.l = getlang(LangJP);
im.pre = mkstr("reading");
setcandidates(2);
sclear(&com);
CT_CHECK(t, keystroke(Ktab, 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, "Tab steps through the candidates", "c2", &com);
candidatebegin(2); candidatebegin(2);
im.pre = mkstr("reading"); im.pre = mkstr("reading");
sclear(&com); sclear(&com);
CT_CHECK(t, keystroke(Kdown, 0, &com)); CT_CHECK(t, keystroke(Kdown, 0, &com));
CT_CHECK(t, keystroke(Kdown, 0, &com));
CT_CHECK(t, keystroke(Kret, 0, &com)); CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "explicit candidate", "c2", &com); checkstr(t, "explicit candidate", "c2", &com);
@@ -888,13 +902,28 @@ engine_candidate_completion(struct ct *t)
CT_CHECK(t, keystroke(Kret, 0, &com)); CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "Enter without candidates", "reading", &com); checkstr(t, "Enter without candidates", "reading", &com);
/* A digit with no such row on the page is an ordinary key. */ /* A digit types on until Space engages the list. */
candidatebegin(3);
im.pre = mkstr("reading");
sclear(&com);
CT_CHECK(t, !keystroke('2', 0, &com));
checkstr(t, "digit before the list is engaged", "reading", &com);
CT_EQ_INT(t, 0, im.nkouho);
candidatebegin(3);
im.pre = mkstr("reading");
sclear(&com);
CT_CHECK(t, keystroke(' ', 0, &com));
CT_CHECK(t, keystroke('2', 0, &com));
checkstr(t, "digit after the list is engaged", "c2", &com);
/* A digit with no such row on the page is an ordinary key: it
* confirms the candidate paged to and passes on. */
candidatebegin(32); candidatebegin(32);
im.pre = mkstr("reading"); im.pre = mkstr("reading");
im.sel = 27; im.sel = 27;
sclear(&com); sclear(&com);
CT_CHECK(t, !keystroke('9', 0, &com)); CT_CHECK(t, !keystroke('9', 0, &com));
checkstr(t, "unavailable short-page digit", "reading", &com); checkstr(t, "unavailable short-page digit", "c28", &com);
CT_EQ_INT(t, 0, im.nkouho); CT_EQ_INT(t, 0, im.nkouho);
} }
@@ -911,9 +940,8 @@ searchsave(Searchfix *f, int lang)
f->search = search; f->search = search;
f->dictlang = getlang(lang); f->dictlang = getlang(lang);
f->dict = f->dictlang->dict; f->dict = f->dictlang->dict;
f->activecap = activecap; f->clientpre = clientpre;
f->lastdraw = lastdraw; f->lastdraw = lastdraw;
f->candidatechosen = candidatechosen;
draindraw(nil); draindraw(nil);
} }
@@ -940,7 +968,7 @@ emojibegin(Searchfix *f, int showpre)
"c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11 c12"); "c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11 c12");
init(); init();
im.l = getlang(LangKO); im.l = getlang(LangKO);
activecap = showpre ? 0 : Cclientpreedit; clientpre = showpre ? 0 : 1;
} }
static void static void
@@ -954,9 +982,8 @@ searchend(Searchfix *f)
draindraw(nil); draindraw(nil);
im = f->im; im = f->im;
search = f->search; search = f->search;
activecap = f->activecap; clientpre = f->clientpre;
lastdraw = f->lastdraw; lastdraw = f->lastdraw;
candidatechosen = f->candidatechosen;
} }
void void
@@ -975,14 +1002,14 @@ engine_popup_preedit_capability(struct ct *t)
init(); init();
im.l = jp; im.l = jp;
draindraw(nil); draindraw(nil);
ownerrequestcap(&client, Cclientpreedit, Keypress, 'k', 0); ownerrequestcap(&client, 1, Keypress, 'k', 0);
ownerrequestcap(&client, Cclientpreedit, Keypress, 'a', 0); ownerrequestcap(&client, 1, Keypress, 'a', 0);
CT_EQ_INT(t, 1, im.nkouho); CT_EQ_INT(t, 1, im.nkouho);
redraw(); redraw();
if(CT_CHECK(t, draindraw(&dc) > 0)){ if(CT_CHECK(t, draindraw(&dc) > 0)){
CT_EQ_INT(t, 0, dc.pre.n); CT_EQ_INT(t, 0, dc.pre.n);
CT_EQ_INT(t, 1, dc.nkouho); CT_EQ_INT(t, 1, dc.nkouho);
CT_EQ_INT(t, 0, dc.sel); CT_EQ_INT(t, -1, dc.sel);
checkstr(t, "client-preedit candidate", "", &dc.kouho[0]); checkstr(t, "client-preedit candidate", "", &dc.kouho[0]);
} }
@@ -992,37 +1019,37 @@ engine_popup_preedit_capability(struct ct *t)
if(CT_CHECK(t, draindraw(&dc) > 0)){ if(CT_CHECK(t, draindraw(&dc) > 0)){
checkstr(t, "popup preedit", "", &dc.pre); checkstr(t, "popup preedit", "", &dc.pre);
CT_EQ_INT(t, 1, dc.nkouho); CT_EQ_INT(t, 1, dc.nkouho);
CT_EQ_INT(t, 0, dc.sel); CT_EQ_INT(t, -1, dc.sel);
checkstr(t, "popup candidate", "", &dc.kouho[0]); checkstr(t, "popup candidate", "", &dc.kouho[0]);
} }
CT_EQ_INT(t, 1, im.nkouho); CT_EQ_INT(t, 1, im.nkouho);
CT_EQ_INT(t, 0, im.sel); CT_EQ_INT(t, -1, im.sel);
checkstr(t, "retained popup candidate", "", &im.kouho[0]); checkstr(t, "retained popup candidate", "", &im.kouho[0]);
kres = ownerrequestcap(&client, 0, Keycap, 0, 0); kres = ownerrequestcap(&client, 0, Keycap, 0, 0);
CT_CHECK(t, kres.eaten); CT_CHECK(t, kres.eaten);
CT_EQ_INT(t, 0, draindraw(nil)); CT_EQ_INT(t, 0, draindraw(nil));
kres = ownerrequestcap(&inactive, Cclientpreedit, Keycap, 0, 0); kres = ownerrequestcap(&inactive, 1, Keycap, 0, 0);
CT_CHECK(t, !kres.eaten); CT_CHECK(t, !kres.eaten);
CT_EQ_INT(t, 0, kres.preedit.n); CT_EQ_INT(t, 0, kres.preedit.n);
CT_EQ_PTR(t, &client, activeowner); CT_EQ_PTR(t, &client, activeowner);
CT_EQ_INT(t, 0, activecap & Cclientpreedit); CT_EQ_INT(t, 0, clientpre);
CT_EQ_INT(t, 0, draindraw(nil)); CT_EQ_INT(t, 0, draindraw(nil));
kres = ownerrequestcap(&client, Cclientpreedit, Keycap, 0, 0); kres = ownerrequestcap(&client, 1, Keycap, 0, 0);
CT_CHECK(t, kres.eaten); CT_CHECK(t, kres.eaten);
checkstr(t, "restored client preedit", "", &kres.preedit); checkstr(t, "restored client preedit", "", &kres.preedit);
if(CT_CHECK(t, draindraw(&dc) > 0)){ if(CT_CHECK(t, draindraw(&dc) > 0)){
CT_EQ_INT(t, 0, dc.pre.n); CT_EQ_INT(t, 0, dc.pre.n);
CT_EQ_INT(t, 1, dc.nkouho); CT_EQ_INT(t, 1, dc.nkouho);
CT_EQ_INT(t, 0, dc.sel); CT_EQ_INT(t, -1, dc.sel);
checkstr(t, "restored client candidate", "", &dc.kouho[0]); checkstr(t, "restored client candidate", "", &dc.kouho[0]);
} }
CT_EQ_INT(t, 1, im.nkouho); CT_EQ_INT(t, 1, im.nkouho);
CT_EQ_INT(t, 0, im.sel); CT_EQ_INT(t, -1, im.sel);
checkstr(t, "retained client candidate", "", &im.kouho[0]); checkstr(t, "retained client candidate", "", &im.kouho[0]);
kres = ownerrequestcap(&client, Cclientpreedit, Keycap, 0, 0); kres = ownerrequestcap(&client, 1, Keycap, 0, 0);
CT_CHECK(t, kres.eaten); CT_CHECK(t, kres.eaten);
CT_EQ_INT(t, 0, draindraw(nil)); CT_EQ_INT(t, 0, draindraw(nil));
@@ -1061,7 +1088,17 @@ engine_japanese_readings(struct ct *t)
} cases[] = { } cases[] = {
{ "nya", "にゃ" }, { "nya", "にゃ" },
{ "n'ya", "んや" }, { "n'ya", "んや" },
{ "nnya", "にゃ" }, { "nnya", "" },
{ "nnnya", "んにゃ" },
{ "konnnichiha", "こんにちは" },
{ "wi", "うぃ" },
{ "vu", "" },
{ "xtu", "" },
{ "axtu", "あっ" },
{ "lla", "っぁ" },
{ "matcha", "まっちゃ" },
{ "zi", "" },
{ "[", "" },
{ "kin'youbi", "きんようび" }, { "kin'youbi", "きんようび" },
}; };
static const struct { static const struct {
@@ -1091,7 +1128,7 @@ engine_japanese_readings(struct ct *t)
} }
for(i = 0; i < nelem(replay); i++){ for(i = 0; i < nelem(replay); i++){
raw = mkstr(replay[i].raw); raw = mkstr(replay[i].raw);
transstr(getlang(LangJP), &raw, &shown); transstr(getlang(LangJP), nil, &raw, &shown);
checkstr(t, replay[i].raw, replay[i].want, &shown); checkstr(t, replay[i].raw, replay[i].want, &shown);
} }
} }
@@ -1116,21 +1153,143 @@ engine_japanese_candidates(struct ct *t)
checkstr(t, "complete Japanese reading", "かんじ", &shown); checkstr(t, "complete Japanese reading", "かんじ", &shown);
if(!CT_EQ_INT(t, 2, im.nkouho)) if(!CT_EQ_INT(t, 2, im.nkouho))
goto cleanup; goto cleanup;
CT_EQ_INT(t, 0, im.sel); CT_EQ_INT(t, -1, im.sel);
checkstr(t, "candidate one", "漢字", &im.kouho[0]); checkstr(t, "candidate one", "漢字", &im.kouho[0]);
checkstr(t, "candidate two", "幹事", &im.kouho[1]); checkstr(t, "candidate two", "幹事", &im.kouho[1]);
CT_CHECK(t, keystroke(Kdown, 0, &com)); CT_CHECK(t, keystroke(Kdown, 0, &com));
CT_EQ_INT(t, 0, im.sel);
CT_CHECK(t, keystroke(Kdown, 0, &com));
CT_EQ_INT(t, 1, im.sel); CT_EQ_INT(t, 1, im.sel);
CT_CHECK(t, keystroke(Kret, 0, &com)); CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "selected Kanji", "幹事", &com); checkstr(t, "selected Kanji", "幹事", &com);
CT_EQ_INT(t, 0, im.nkouho); CT_EQ_INT(t, 0, im.nkouho);
/* Space converts: it steps through the candidates and wraps;
* typing on takes the chosen one; Backspace goes back. */
if(!typekeys(t, "kanji", &com))
goto cleanup;
sclear(&com);
CT_CHECK(t, keystroke(' ', 0, &com));
CT_EQ_INT(t, 0, im.sel);
CT_CHECK(t, keystroke(' ', 0, &com));
CT_EQ_INT(t, 1, im.sel);
CT_CHECK(t, keystroke(' ', 0, &com));
CT_EQ_INT(t, 2, im.sel);
CT_CHECK(t, keystroke(' ', 0, &com));
CT_EQ_INT(t, 0, im.sel);
CT_CHECK(t, keystroke(' ', Mshift, &com));
CT_EQ_INT(t, 2, im.sel);
CT_CHECK(t, keystroke(Kback, 0, &com));
CT_EQ_INT(t, -1, im.sel);
CT_EQ_INT(t, 3, im.nkouho);
CT_CHECK(t, keystroke(' ', 0, &com));
CT_CHECK(t, keystroke('w', 0, &com));
CT_CHECK(t, keystroke('o', 0, &com));
checkstr(t, "typing on takes the candidate", "漢字", &com);
impre(&im, &shown);
checkstr(t, "reading after the candidate", "", &shown);
sclear(&com);
CT_CHECK(t, keystroke(' ', 0, &com));
if(CT_EQ_INT(t, 1, im.nkouho))
checkstr(t, "Space offers Katakana without candidates", "",
&im.kouho[0]);
CT_EQ_INT(t, 0, im.sel);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "Katakana committed", "", &com);
CT_CHECK(t, !keystroke(' ', 0, &com));
sclear(&com);
if(!typekeys(t, "kanji", &com))
goto cleanup;
CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke(' ', 0, &com));
if(CT_EQ_INT(t, 3, im.nkouho))
checkstr(t, "Katakana after the candidates", "カンジ",
&im.kouho[2]);
CT_CHECK(t, keystroke(Kesc, 0, &com));
CT_CHECK(t, keystroke(Kesc, 0, &com));
/* SKK's okuri-ari entries convert verbs: stem plus okurigana. */
setdict(jp->dict, "かk", "書 描");
setdict(jp->dict, "かく", "");
if(!typekeys(t, "kaku", &com))
goto cleanup;
if(CT_EQ_INT(t, 3, im.nkouho)){
checkstr(t, "exact candidate first", "", &im.kouho[0]);
checkstr(t, "okurigana put back", "書く", &im.kouho[1]);
checkstr(t, "second okuri candidate", "描く", &im.kouho[2]);
}
CT_CHECK(t, keystroke(Kesc, 0, &com));
setdict(jp->dict, "おおk", "");
if(!typekeys(t, "ookii", &com))
goto cleanup;
if(CT_EQ_INT(t, 1, im.nkouho))
checkstr(t, "two-kana okurigana", "大きい", &im.kouho[0]);
CT_CHECK(t, keystroke(Kesc, 0, &com));
/* Shift on the okurigana asks for that split alone, first. */
sclear(&com);
if(!typekeys(t, "ka", &com))
goto cleanup;
CT_CHECK(t, keystroke('K', Mshift, &com));
CT_CHECK(t, keystroke('u', 0, &com));
if(CT_EQ_INT(t, 3, im.nkouho)){
checkstr(t, "marked okurigana first", "書く", &im.kouho[0]);
checkstr(t, "then the second", "描く", &im.kouho[1]);
checkstr(t, "then the reading's own", "", &im.kouho[2]);
}
CT_CHECK(t, keystroke(Kesc, 0, &com));
setdict(jp->dict, "いt", "言 行");
setdict(jp->dict, "いっt", "言 行");
if(!typekeys(t, "itta", &com))
goto cleanup;
if(CT_EQ_INT(t, 2, im.nkouho)){
checkstr(t, "sokuon written in kana", "言った", &im.kouho[0]);
checkstr(t, "sokuon written in kana", "行った", &im.kouho[1]);
}
CT_CHECK(t, keystroke(Kesc, 0, &com));
/* A romaji typo stays in the reading for Backspace to mend. */
if(!typekeys(t, "kanjr", &com))
goto cleanup;
impre(&im, &shown);
checkstr(t, "typo kept", "かんjr", &shown);
CT_CHECK(t, keystroke(Kback, 0, &com));
impre(&im, &shown);
checkstr(t, "typo half mended", "かんj", &shown);
CT_CHECK(t, keystroke(Kback, 0, &com));
if(!typekeys(t, "ji", &com))
goto cleanup;
impre(&im, &shown);
checkstr(t, "typo mended", "かんじ", &shown);
CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke('0', 0, &com));
checkstr(t, "0 commits the reading", "かんじ", &com);
sclear(&com);
if(!typekeys(t, "wo", &com))
goto cleanup;
CT_CHECK(t, keystroke('0', 0, &com));
checkstr(t, "0 commits a reading without candidates", "", &com);
sclear(&com);
if(!typekeys(t, "kanji", &com))
goto cleanup;
CT_CHECK(t, keystroke(' ', 0, &com));
CT_CHECK(t, keystroke(Kesc, 0, &com));
CT_EQ_INT(t, -1, im.sel);
impre(&im, &shown);
checkstr(t, "Escape goes back to the reading", "かんじ", &shown);
CT_CHECK(t, keystroke(Kesc, 0, &com));
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
CT_EQ_INT(t, 0, im.nkouho);
sclear(&com);
if(!typekeys(t, "kanji", &com))
goto cleanup;
CT_CHECK(t, keystroke(Kdown, 0, &com));
CT_CHECK(t, keystroke('s', Mctrl, &com));
checkstr(t, "language switch takes the candidate", "漢字", &com);
CT_EQ_INT(t, LangKO, im.l->lang);
im.l = jp;
/* Backspace looks a complete shorter reading up again. */ /* Backspace looks a complete shorter reading up again. */
setdict(jp->dict, "かん", ""); setdict(jp->dict, "かん", "");
if(!typekeys(t, "kanji", &com)) if(!typekeys(t, "kanji", &com))
goto cleanup; goto cleanup;
CT_CHECK(t, keystroke(Kback, 0, &com)); CT_CHECK(t, keystroke(Kback, 0, &com));
CT_EQ_INT(t, 0, im.nkouho);
CT_CHECK(t, keystroke(Kback, 0, &com));
if(CT_EQ_INT(t, 1, im.nkouho)) if(CT_EQ_INT(t, 1, im.nkouho))
checkstr(t, "shorter reading candidate", "", &im.kouho[0]); checkstr(t, "shorter reading candidate", "", &im.kouho[0]);
cleanup: cleanup:
@@ -1149,13 +1308,18 @@ engine_japanese_backspace_and_boundaries(struct ct *t)
if(typekeys(t, "kanji", &com)){ if(typekeys(t, "kanji", &com)){
CT_CHECK(t, keystroke(Kback, 0, &com)); CT_CHECK(t, keystroke(Kback, 0, &com));
impre(&im, &shown); impre(&im, &shown);
checkstr(t, "backspace pending i", "かんj", &shown); checkstr(t, "backspace deletes じ", "かん", &shown);
CT_CHECK(t, keystroke(Kback, 0, &com)); CT_CHECK(t, keystroke(Kback, 0, &com));
impre(&im, &shown); impre(&im, &shown);
checkstr(t, "backspace pending j", "", &shown); checkstr(t, "backspace deletes ん", "", &shown);
CT_CHECK(t, keystroke(Kback, 0, &com)); if(typekeys(t, "kyu", &com)){
impre(&im, &shown); CT_CHECK(t, keystroke(Kback, 0, &com));
checkstr(t, "backspace accumulated kana", "", &shown); impre(&im, &shown);
checkstr(t, "backspace deletes ゅ", "かき", &shown);
CT_CHECK(t, keystroke(Kback, 0, &com));
CT_CHECK(t, keystroke(Kback, 0, &com));
CT_CHECK(t, !haspre(&im));
}
} }
init(); init();
@@ -1179,8 +1343,8 @@ engine_japanese_backspace_and_boundaries(struct ct *t)
im.l = getlang(LangJP); im.l = getlang(LangJP);
sclear(&com); sclear(&com);
if(typekeys(t, "kanji", &com)){ if(typekeys(t, "kanji", &com)){
CT_CHECK(t, keystroke(' ', 0, &com)); CT_CHECK(t, !keystroke(L'', 0, &com));
checkstr(t, "real input boundary", "かんじ ", &com); checkstr(t, "real input boundary", "かんじ", &com);
} }
init(); init();
@@ -1238,6 +1402,8 @@ engine_emoji_single_candidate(struct ct *t)
CT_CHECK(t, keystroke('e', Mctrl, &com)); CT_CHECK(t, keystroke('e', Mctrl, &com));
CT_CHECK(t, search.lang); CT_CHECK(t, search.lang);
CT_EQ_INT(t, LangKO, im.l->lang); CT_EQ_INT(t, LangKO, im.l->lang);
if(CT_CHECK(t, draindraw(&dc) > 0))
checkstr(t, "empty search shows itself", "", &dc.pre);
if(typekeys(t, "alpha", &com)){ if(typekeys(t, "alpha", &com)){
checkstr(t, "no automatic commit", "", &com); checkstr(t, "no automatic commit", "", &com);
checkstr(t, "raw English query", "alpha", &search.text); checkstr(t, "raw English query", "alpha", &search.text);
@@ -1271,13 +1437,14 @@ engine_emoji_queries(struct ct *t)
CT_CHECK(t, keystroke('e', Mctrl, &com)); CT_CHECK(t, keystroke('e', Mctrl, &com));
CT_CHECK(t, keystroke('a', 0, &com)); CT_CHECK(t, keystroke('a', 0, &com));
checkstr(t, "raw query wins", "a", &search.text); checkstr(t, "raw query wins", "a", &search.text);
CT_EQ_INT(t, 3, im.nkouho); CT_EQ_INT(t, 4, im.nkouho);
checkstr(t, "raw candidate", "A", &im.kouho[0]); checkstr(t, "raw candidate", "A", &im.kouho[0]);
checkstr(t, "deduplicated candidate", "B", &im.kouho[1]); checkstr(t, "deduplicated candidate", "B", &im.kouho[1]);
checkstr(t, "local candidate", "C", &im.kouho[2]); checkstr(t, "raw prefix candidate", "α", &im.kouho[2]);
checkstr(t, "local candidate", "C", &im.kouho[3]);
CT_CHECK(t, draindraw(&dc) > 0); CT_CHECK(t, draindraw(&dc) > 0);
checkstr(t, "prefix popup query", "a", &dc.pre); checkstr(t, "prefix popup query", "a", &dc.pre);
CT_EQ_INT(t, 3, dc.nkouho); CT_EQ_INT(t, 4, dc.nkouho);
CT_CHECK(t, keystroke(Kesc, 0, &com)); CT_CHECK(t, keystroke(Kesc, 0, &com));
CT_CHECK(t, keystroke('e', Mctrl, &com)); CT_CHECK(t, keystroke('e', Mctrl, &com));
@@ -1404,6 +1571,7 @@ engine_emoji_digit_aliases(struct ct *t)
checkstr(t, "zero alias", "", &com); checkstr(t, "zero alias", "", &com);
sclear(&com); sclear(&com);
im.l = getlang(LangEN);
CT_CHECK(t, keystroke('e', Mctrl, &com)); CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "smile", &com)){ if(typekeys(t, "smile", &com)){
CT_CHECK(t, keystroke('9', 0, &com)); CT_CHECK(t, keystroke('9', 0, &com));
@@ -1460,6 +1628,7 @@ engine_search_candidate_keys(struct ct *t)
Str com; Str com;
emojibegin(&f, 0); emojibegin(&f, 0);
im.l = getlang(LangEN);
sclear(&com); sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com)); CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "smile", &com)){ if(typekeys(t, "smile", &com)){
@@ -1510,22 +1679,26 @@ engine_emoji_preedit_languages(struct ct *t)
im.l = getlang(LangJP); im.l = getlang(LangJP);
sclear(&com); sclear(&com);
if(typekeys(t, "ka", &com)){ if(typekeys(t, "ka", &com)){
CT_CHECK(t, !keystroke(Kmodfirst+2, 0, &com)); CT_CHECK(t, !keystroke(0, 0, &com));
impre(&im, &shown); impre(&im, &shown);
checkstr(t, "modifier preserves Japanese", "", &shown); checkstr(t, "modifier preserves Japanese", "", &shown);
CT_CHECK(t, keystroke('E', Mctrl|Mshift, &com)); CT_CHECK(t, keystroke('E', Mctrl, &com));
checkstr(t, "committed Japanese preedit", "", &com); checkstr(t, "committed Japanese preedit", "", &com);
CT_CHECK(t, search.lang); CT_CHECK(t, search.lang);
CT_EQ_INT(t, LangJP, im.l->lang); CT_EQ_INT(t, LangJP, im.l->lang);
CT_CHECK(t, !keystroke('E', Mctrl|Malt, &com)); CT_CHECK(t, !keystroke('E', Mctrl|Malt, &com));
CT_EQ_INT(t, 0, search.lang); CT_EQ_INT(t, 0, search.lang);
CT_CHECK(t, keystroke('E', Mctrl|Mshift, &com)); CT_CHECK(t, keystroke('E', Mctrl, &com));
CT_CHECK(t, !keystroke('E', Mctrl|Msuper, &com)); CT_CHECK(t, !keystroke('E', Mctrl|Msuper, &com));
CT_EQ_INT(t, 0, search.lang); CT_EQ_INT(t, 0, search.lang);
CT_CHECK(t, keystroke('E', Mctrl|Mshift, &com)); CT_CHECK(t, keystroke('E', Mctrl, &com));
CT_CHECK(t, !keystroke('V', Mctrl|Mshift, &com));
CT_EQ_INT(t, 0, search.lang);
CT_EQ_INT(t, LangJP, im.l->lang);
CT_CHECK(t, keystroke('E', Mctrl, &com));
CT_EQ_INT(t, LangJP, im.l->lang); CT_EQ_INT(t, LangJP, im.l->lang);
CT_CHECK(t, keystroke('a', 0, &com)); CT_CHECK(t, keystroke('a', 0, &com));
CT_CHECK(t, !keystroke(Kmodfirst+2, 0, &com)); CT_CHECK(t, !keystroke(0, 0, &com));
CT_CHECK(t, search.lang); CT_CHECK(t, search.lang);
checkstr(t, "modifier preserves search", "a", &search.text); checkstr(t, "modifier preserves search", "a", &search.text);
CT_CHECK(t, !keystroke('P', Mctrl|Mshift, &com)); CT_CHECK(t, !keystroke('P', Mctrl|Mshift, &com));
@@ -1553,14 +1726,14 @@ engine_emoji_preedit_languages(struct ct *t)
} }
init(); init();
im.l = &testvi; im.l = getlang(LangVI);
sclear(&com); sclear(&com);
if(typekeys(t, "as", &com)){ if(typekeys(t, "as", &com)){
CT_CHECK(t, keystroke('e', Mctrl, &com)); CT_CHECK(t, keystroke('e', Mctrl, &com));
checkstr(t, "committed Telex preedit", "á", &com); checkstr(t, "committed Telex preedit", "á", &com);
CT_CHECK(t, keystroke(Kesc, 0, &com)); CT_CHECK(t, keystroke(Kesc, 0, &com));
CT_CHECK(t, !search.lang); CT_CHECK(t, !search.lang);
CT_EQ_PTR(t, &testvi, im.l); CT_EQ_PTR(t, getlang(LangVI), im.l);
sclear(&com); sclear(&com);
if(typekeys(t, "as", &com)){ if(typekeys(t, "as", &com)){
impre(&im, &shown); impre(&im, &shown);
@@ -1583,11 +1756,13 @@ engine_emoji_start_and_unknown(struct ct *t)
sclear(&com); sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com)); CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "xyz", &com)){ if(typekeys(t, "xyz", &com)){
/* Keys that match nothing show what they type in the language. */
CT_CHECK(t, !keystroke(Kspec|0x51, 0, &com)); CT_CHECK(t, !keystroke(Kspec|0x51, 0, &com));
checkstr(t, "special commits shown query", "xyz", &com); checkstr(t, "special commits shown query", "", &com);
CT_CHECK(t, !search.lang); CT_CHECK(t, !search.lang);
CT_EQ_INT(t, LangKO, im.l->lang); CT_EQ_INT(t, LangKO, im.l->lang);
} }
im.l = getlang(LangEN);
/* A language switch commits the query like any other pending text. */ /* A language switch commits the query like any other pending text. */
sclear(&com); sclear(&com);
@@ -1597,7 +1772,6 @@ engine_emoji_start_and_unknown(struct ct *t)
checkstr(t, "language switch commits query", "xy", &com); checkstr(t, "language switch commits query", "xy", &com);
CT_CHECK(t, !search.lang); CT_CHECK(t, !search.lang);
CT_EQ_INT(t, LangEN, im.l->lang); CT_EQ_INT(t, LangEN, im.l->lang);
im.l = getlang(LangKO);
} }
/* So do the search keys, ending or changing the search. */ /* So do the search keys, ending or changing the search. */
@@ -1768,6 +1942,25 @@ engine_hanja_backspace(struct ct *t)
CT_CHECK(t, keystroke(Kback, 0, &com)); CT_CHECK(t, keystroke(Kback, 0, &com));
CT_EQ_INT(t, 0, search.lang); CT_EQ_INT(t, 0, search.lang);
CT_EQ_INT(t, LangKO, im.l->lang); CT_EQ_INT(t, LangKO, im.l->lang);
/* Escape gives the seed back; keys typed compose from it. */
if(!typekeys(t, "gks", &com))
goto cleanup;
CT_CHECK(t, keystroke('h', Mctrl, &com));
CT_CHECK(t, keystroke(Kesc, 0, &com));
CT_EQ_INT(t, 0, search.lang);
checkstr(t, "seed pending again", "", &im.pre);
checkstr(t, "nothing committed", "", &com);
CT_CHECK(t, keystroke(Kback, 0, &com));
CT_CHECK(t, keystroke('h', Mctrl, &com));
checkstr(t, "seed 하", "", &search.text);
CT_CHECK(t, keystroke('s', 0, &com));
checkstr(t, "key composes with the seed", "", &search.text);
CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke(Kesc, 0, &com));
checkstr(t, "seed alone comes back", "", &im.pre);
CT_CHECK(t, !keystroke(Kesc, 0, &com));
checkstr(t, "Korean Escape commits", "", &com);
CT_EQ_INT(t, 0, im.pre.n);
cleanup: cleanup:
searchend(&f); searchend(&f);
} }
@@ -1805,6 +1998,40 @@ engine_hanja_input_languages(struct ct *t)
checkstr(t, "Japanese Hanja commit", "", &com); checkstr(t, "Japanese Hanja commit", "", &com);
CT_EQ_INT(t, LangJP, im.l->lang); CT_EQ_INT(t, LangJP, im.l->lang);
/* The keyboard's own keys: 한자 seeds like Ctrl+H, 한/영 toggles. */
init();
im.l = getlang(LangKO);
sclear(&com);
if(typekeys(t, "gks", &com)){
CT_CHECK(t, keystroke(Khanja, 0, &com));
checkstr(t, "Hanja key seed", "", &search.text);
CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke(Khangul, 0, &com));
checkstr(t, "Hangul key commits the query", "", &com);
CT_EQ_INT(t, 0, search.lang);
CT_EQ_INT(t, LangEN, im.l->lang);
CT_CHECK(t, keystroke(Khangul, 0, &com));
CT_EQ_INT(t, LangKO, im.l->lang);
CT_CHECK(t, keystroke(Kzenkaku, 0, &com));
CT_EQ_INT(t, LangJP, im.l->lang);
CT_CHECK(t, keystroke(Kkana, 0, &com));
CT_EQ_INT(t, LangJPK, im.l->lang);
CT_CHECK(t, keystroke(Kkana, 0, &com));
CT_EQ_INT(t, LangJP, im.l->lang);
CT_CHECK(t, keystroke(Kzenkaku, 0, &com));
CT_EQ_INT(t, LangEN, im.l->lang);
CT_CHECK(t, keystroke(Khenkan, 0, &com));
CT_EQ_INT(t, LangJP, im.l->lang);
sclear(&com);
if(typekeys(t, "ka", &com)){
CT_CHECK(t, keystroke(Khenkan, 0, &com));
CT_EQ_INT(t, 0, im.sel);
CT_CHECK(t, keystroke(Kmuhenkan, 0, &com));
checkstr(t, "無変換 commits", "", &com);
CT_EQ_INT(t, LangEN, im.l->lang);
}
}
init(); init();
im.l = getlang(LangEN); im.l = getlang(LangEN);
sclear(&com); sclear(&com);
@@ -1856,8 +2083,6 @@ engine_randomized_stress(struct ct *t)
}; };
static Rune modes[] = { 't', 'n', 'k', 's', 'v' }; static Rune modes[] = { 't', 'n', 'k', 's', 'v' };
Searchfix f; Searchfix f;
Lang *vi;
Trie *vimap;
Keyres res; Keyres res;
Caret pos; Caret pos;
u32int rnd, mod; u32int rnd, mod;
@@ -1865,11 +2090,6 @@ engine_randomized_stress(struct ct *t)
void *owner, *owners[3]; void *owner, *owners[3];
int i, j, op; int i, j, op;
vi = getlang(LangVI);
if(!CT_CHECK(t, vi != nil))
return;
vimap = vi->map;
vi->map = testvi.map;
emojibegin(&f, 0); emojibegin(&f, 0);
owners[0] = &contexts[0]; owners[0] = &contexts[0];
owners[1] = &contexts[1]; owners[1] = &contexts[1];
@@ -1914,7 +2134,7 @@ engine_randomized_stress(struct ct *t)
res = ownerrequestat(owner, Keycaret, 0, 0, &pos); res = ownerrequestat(owner, Keycaret, 0, 0, &pos);
else if(op == 24) else if(op == 24)
res = ownerrequest(owner, Keypress, res = ownerrequest(owner, Keypress,
Kmodfirst + ((rnd >> 20) & 7), 0); 0, 0);
else if(op == 25) else if(op == 25)
res = ownerrequestat(owner, Keypress, 'p', Mctrl, &pos); res = ownerrequestat(owner, Keypress, 'p', Mctrl, &pos);
else if(op == 26) else if(op == 26)
@@ -1945,7 +2165,6 @@ engine_randomized_stress(struct ct *t)
} }
} }
searchend(&f); searchend(&f);
vi->map = vimap;
} }
void void
@@ -1960,7 +2179,7 @@ engine_full_boundary_passthrough(struct ct *t)
im.pre.r[i] = L''; im.pre.r[i] = L'';
im.pre.n = Maxrunes; im.pre.n = Maxrunes;
sclear(&com); sclear(&com);
CT_CHECK(t, !keystroke(' ', 0, &com)); CT_CHECK(t, !keystroke(L'', 0, &com));
CT_EQ_INT(t, Maxrunes, com.n); CT_EQ_INT(t, Maxrunes, com.n);
for(i = 0; i < Maxrunes; i++) for(i = 0; i < Maxrunes; i++)
CT_EQ_UINT(t, L'', com.r[i]); CT_EQ_UINT(t, L'', com.r[i]);
@@ -1973,9 +2192,9 @@ engine_full_boundary_passthrough(struct ct *t)
im.pre.r[i] = L''; im.pre.r[i] = L'';
im.pre.n = Maxrunes-1; im.pre.n = Maxrunes-1;
sclear(&com); sclear(&com);
CT_CHECK(t, keystroke(' ', 0, &com)); CT_CHECK(t, !keystroke(L'', 0, &com));
CT_EQ_INT(t, Maxrunes, com.n); CT_EQ_INT(t, Maxrunes-1, com.n);
CT_EQ_UINT(t, ' ', com.r[Maxrunes-1]); CT_EQ_INT(t, 0, im.pre.n);
/* Typing on at the limit commits the kana and keeps the syllable. */ /* Typing on at the limit commits the kana and keeps the syllable. */
init(); init();

View File

@@ -189,10 +189,7 @@ font_render(struct ct *t)
buf = mem + Guard; buf = mem + Guard;
fillpixels(buf, Testn, Colbg); fillpixels(buf, Testn, Colbg);
a = mkstr("A"); a = mkstr("A");
if(!CT_CHECK(t, textinit())){ textinit();
free(mem);
return;
}
for(i = 0; i < nelem(plain); i++) for(i = 0; i < nelem(plain); i++)
checktext(t, buf, plain[i]); checktext(t, buf, plain[i]);
@@ -257,6 +254,4 @@ font_render(struct ct *t)
checkguards(t, mem); checkguards(t, mem);
free(mem); free(mem);
textclose(); textclose();
textclose();
CT_EQ_INT(t, 0, textwidth(&a));
} }

View File

@@ -437,73 +437,6 @@ runquery(char *module, char *cache)
return 1; return 1;
} }
static int
startxvfb(pid_t *pidp, char *display, size_t ndisplay)
{
struct pollfd pfd;
char fdarg[32], line[32];
int p[2], n, status;
pid_t pid;
if(pipe(p) < 0)
return fail("Xvfb display pipe: %s", strerror(errno));
pid = fork();
if(pid < 0){
close(p[0]);
close(p[1]);
return fail("fork Xvfb: %s", strerror(errno));
}
if(pid == 0){
close(p[0]);
snprintf(fdarg, sizeof fdarg, "%d", p[1]);
execlp("Xvfb", "Xvfb", "-displayfd", fdarg, "-screen", "0",
"1024x768x24", "-nolisten", "tcp", "-noreset", (char*)NULL);
_exit(127);
}
close(p[1]);
pfd.fd = p[0];
pfd.events = POLLIN;
if(poll(&pfd, 1, Timeout) <= 0){
kill(pid, SIGTERM);
waitpid(pid, NULL, 0);
close(p[0]);
return fail("Xvfb did not publish a display");
}
n = read(p[0], line, sizeof line - 1);
close(p[0]);
if(n <= 0){
waitpid(pid, &status, 0);
return fail("read Xvfb display number");
}
line[n] = '\0';
line[strcspn(line, "\r\n")] = '\0';
if(snprintf(display, ndisplay, ":%s", line) >= (int)ndisplay){
kill(pid, SIGTERM);
waitpid(pid, NULL, 0);
return fail("Xvfb display number is too long");
}
*pidp = pid;
return 1;
}
static void
stopxvfb(pid_t *pidp)
{
int status;
int64_t deadline;
if(*pidp <= 0)
return;
kill(*pidp, SIGTERM);
deadline = nowms() + Timeout;
while(waitpid(*pidp, &status, WNOHANG) == 0 && leftms(deadline) > 0)
g_usleep(10000);
if(waitpid(*pidp, &status, WNOHANG) == 0){
kill(*pidp, SIGKILL);
waitpid(*pidp, &status, 0);
}
*pidp = -1;
}
static void static void
pump(void) pump(void)
@@ -656,16 +589,17 @@ main(int argc, char **argv)
static const int badresponses[] = { Rbadcommit, Rbadpreedit }; static const int badresponses[] = { Rbadcommit, Rbadpreedit };
char root[] = "/tmp/strans-gtk.XXXXXX"; char root[] = "/tmp/strans-gtk.XXXXXX";
char runtime[320] = "", socket[384] = "", cache[384] = ""; char runtime[320] = "", socket[384] = "", cache[384] = "";
char display[32];
char *module; char *module;
pid_t xvfb; Daemon xvfb;
Live l;
int first, i, ox, oy, scale, closes, ok, rootmade, serverup; int first, i, ox, oy, scale, closes, ok, rootmade, serverup;
int64_t stalledms, started; int64_t stalledms, started;
testname = "gtk_live_test"; testname = "gtk_live_test";
ctx = oldctx = badctx = NULL; ctx = oldctx = badctx = NULL;
top = NULL; top = NULL;
xvfb = -1; memset(&l, 0, sizeof l);
daemoninit(&xvfb, "Xvfb");
rootmade = 0; rootmade = 0;
serverup = 0; serverup = 0;
ok = 0; ok = 0;
@@ -687,8 +621,8 @@ main(int argc, char **argv)
Check(snprintf(cache, sizeof cache, "%s/immodules.cache", root) < Check(snprintf(cache, sizeof cache, "%s/immodules.cache", root) <
(int)sizeof cache, "cache path is too long"); (int)sizeof cache, "cache path is too long");
Check(mkdir(runtime, 0700) == 0, "mkdir runtime: %s", strerror(errno)); Check(mkdir(runtime, 0700) == 0, "mkdir runtime: %s", strerror(errno));
Check(startxvfb(&xvfb, display, sizeof display), "start private Xvfb"); Check(startxvfb(&l, &xvfb, "Xvfb"), "start private Xvfb");
Check(setenv("DISPLAY", display, 1) == 0 && Check(setenv("DISPLAY", l.display, 1) == 0 &&
setenv("GDK_BACKEND", "x11", 1) == 0 && setenv("GDK_BACKEND", "x11", 1) == 0 &&
setenv("GDK_SCALE", "2", 1) == 0 && setenv("GDK_SCALE", "2", 1) == 0 &&
setenv("XDG_RUNTIME_DIR", runtime, 1) == 0, setenv("XDG_RUNTIME_DIR", runtime, 1) == 0,
@@ -764,6 +698,26 @@ main(int argc, char **argv)
Check(key(ctx, win, GDK_KEY_dead_acute) && key(ctx, win, GDK_KEY_e), Check(key(ctx, win, GDK_KEY_dead_acute) && key(ctx, win, GDK_KEY_e),
"dead-key sequence was not consumed"); "dead-key sequence was not consumed");
Check(strcmp(log.commit, "é") == 0, "dead key committed %s", log.commit); Check(strcmp(log.commit, "é") == 0, "dead key committed %s", log.commit);
/* A dead key over a preedit hands the pending text back first. */
setreply(&srv, 1, Rnormal, 0);
setpre(&srv, "");
clearlog(&log);
Check(key(ctx, win, GDK_KEY_h), "dead-key setup key was not eaten");
clearevent(&log);
first = eventcount(&srv);
Check(!key(ctx, win, GDK_KEY_Shift_L), "a modifier press was eaten");
Check(eventcount(&srv) == first && strcmp(log.pre, "") == 0,
"a modifier press disturbed the preedit");
setreply(&srv, 0, Rnormal, 0);
setpre(&srv, "");
Check(key(ctx, win, GDK_KEY_dead_acute) && key(ctx, win, GDK_KEY_e),
"dead key over a preedit was not consumed");
Check(getevent(&srv, first).type == Ereset,
"dead key did not hand the pending text back");
Check(strcmp(log.commit, "é") == 0 && log.pre[0] == '\0',
"dead key over a preedit committed %s (preedit %s)",
log.commit, log.pre);
clearlog(&log); clearlog(&log);
Check(keystate(ctx, win, GDK_KEY_U, GDK_CONTROL_MASK|GDK_SHIFT_MASK), Check(keystate(ctx, win, GDK_KEY_U, GDK_CONTROL_MASK|GDK_SHIFT_MASK),
"Unicode prefix was not consumed"); "Unicode prefix was not consumed");
@@ -931,7 +885,8 @@ out:
rmdir(runtime); rmdir(runtime);
if(rootmade) if(rootmade)
rmdir(root); rmdir(root);
stopxvfb(&xvfb); stopdaemon(&xvfb);
closeerrors(&xvfb);
free(module); free(module);
if(ok) if(ok)
printf("gtk_live_test: ok (scale %d, stalled peer %lld ms)\n", printf("gtk_live_test: ok (scale %d, stalled peer %lld ms)\n",

View File

@@ -3,7 +3,6 @@
#include <errno.h> #include <errno.h>
enum enum
{ {
Testctrlmask = 1<<2, Testctrlmask = 1<<2,
@@ -53,6 +52,9 @@ ibus_machine_id_fallback(struct ct *t)
path[1] = fallback; path[1] = fallback;
machineidfiles(got, sizeof got, path, nelem(path)); machineidfiles(got, sizeof got, path, nelem(path));
CT_EQ_STR(t, "fallback-machine-id", got); CT_EQ_STR(t, "fallback-machine-id", got);
path[1] = primary;
machineidfiles(got, sizeof got, path, nelem(path));
CT_EQ_STR(t, "", got);
old = getenv("IBUS_ADDRESS_FILE"); old = getenv("IBUS_ADDRESS_FILE");
saved = old == nil ? nil : strdup(old); saved = old == nil ? nil : strdup(old);
@@ -79,85 +81,6 @@ cleanup:
CT_CHECK(t, rmdir(root) == 0); CT_CHECK(t, rmdir(root) == 0);
} }
void
ibus_startup_requires_ownership(struct ct *t)
{
char root[] = "/tmp/strans-ibus-startup.XXXXXX";
char busdir[128], ibusdir[128], path[512], *old, *saved;
struct stat st;
int i, made, rv;
made = 0;
busdir[0] = '\0';
ibusdir[0] = '\0';
path[0] = '\0';
old = getenv("XDG_CONFIG_HOME");
saved = old == nil ? nil : strdup(old);
if(old != nil && !CT_CHECK(t, saved != nil))
return;
/* An IBus desktop names a live address file; the path must be ours. */
unsetenv("IBUS_ADDRESS_FILE");
if(!CT_CHECK(t, mkdtemp(root) != nil))
goto cleanup;
made = 1;
if(!CT_CHECK(t, snprintf(busdir, sizeof busdir, "%s/ibus/bus", root)
< (int)sizeof busdir) ||
!CT_CHECK(t, snprintf(ibusdir, sizeof ibusdir, "%s/ibus", root)
< (int)sizeof ibusdir) ||
!CT_CHECK(t, setenv("XDG_CONFIG_HOME", root, 1) == 0) ||
!CT_CHECK(t, buildaddrpath(path, sizeof path) == 0))
goto cleanup;
memset(watches, 0, sizeof watches);
for(i = 0; i < nelem(watches); i++)
watches[i] = (DBusWatch*)1;
nwatches = nelem(watches);
rv = ibusinit();
CT_EQ_INT(t, -1, rv);
CT_EQ_PTR(t, nil, srv);
CT_CHECK(t, lstat(path, &st) < 0 && errno == ENOENT);
if(rv == 0){
dbus_server_disconnect(srv);
dbus_server_unref(srv);
srv = nil;
unlinkaddr();
atexitdont(unlinkaddr);
}
memset(watches, 0, sizeof watches);
nwatches = 0;
while(atexit(unlinkaddr))
continue;
CT_EQ_INT(t, -1, ibusinit());
CT_EQ_PTR(t, nil, srv);
CT_CHECK(t, lstat(path, &st) < 0 && errno == ENOENT);
cleanup:
if(srv != nil){
dbus_server_disconnect(srv);
dbus_server_unref(srv);
srv = nil;
}
unlinkaddr();
atexitdont(unlinkaddr);
memset(watches, 0, sizeof watches);
nwatches = 0;
addrfile[0] = '\0';
if(saved != nil){
setenv("XDG_CONFIG_HOME", saved, 1);
free(saved);
}else
unsetenv("XDG_CONFIG_HOME");
if(made){
if(path[0] != '\0')
unlink(path);
if(busdir[0] != '\0')
CT_CHECK(t, rmdir(busdir) == 0 || errno == ENOENT);
if(ibusdir[0] != '\0')
CT_CHECK(t, rmdir(ibusdir) == 0 || errno == ENOENT);
CT_CHECK(t, rmdir(root) == 0);
}
}
static int static int
ibusbegin(struct ct *t, Ibusfix *f) ibusbegin(struct ct *t, Ibusfix *f)
{ {
@@ -213,7 +136,7 @@ nexttrace(struct ct *t, Ibusfix *f, int op, Ictx *ctx)
CT_EQ_INT(t, op, req.op); CT_EQ_INT(t, op, req.op);
CT_EQ_PTR(t, ctx, req.owner); CT_EQ_PTR(t, ctx, req.owner);
if(ctx != nil) if(ctx != nil)
CT_EQ_INT(t, ctx->cap & 1 ? Cclientpreedit : 0, req.cap); CT_EQ_INT(t, clientpreedit(ctx), req.clientpre);
CT_EQ_PTR(t, replyc, req.reply); CT_EQ_PTR(t, replyc, req.reply);
return req; return req;
} }
@@ -230,9 +153,10 @@ static Keyres
contextkey(struct ct *t, Ibusfix *f, Ictx *ctx, u32int sym, u32int state) contextkey(struct ct *t, Ibusfix *f, Ictx *ctx, u32int sym, u32int state)
{ {
Keyres res; Keyres res;
char text[Maxutf];
memset(&res, 0, sizeof res); memset(&res, 0, sizeof res);
CT_CHECK(t, processkey(ctx, sym, state, &res)); CT_CHECK(t, processkey(ctx, sym, state, text, sizeof text, &res));
nexttrace(t, f, Keypress, ctx); nexttrace(t, f, Keypress, ctx);
return res; return res;
} }
@@ -258,7 +182,7 @@ ibus_capability_policy(struct ct *t)
checkstr(t, "preedit", "k", &res.preedit); checkstr(t, "preedit", "k", &res.preedit);
CT_EQ_PTR(t, a, testengineowner()); CT_EQ_PTR(t, a, testengineowner());
a->cap = Cclientpreedit; a->cap = Ibuscappreedit;
memset(&res, 0, sizeof res); memset(&res, 0, sizeof res);
sendrequest(a, Keycap, 0, 0, &res); sendrequest(a, Keycap, 0, 0, &res);
nexttrace(t, &f, Keycap, a); nexttrace(t, &f, Keycap, a);
@@ -266,7 +190,7 @@ ibus_capability_policy(struct ct *t)
checkstr(t, "preedit", "k", &res.preedit); checkstr(t, "preedit", "k", &res.preedit);
b->focused = 1; b->focused = 1;
b->cap = Cclientpreedit; b->cap = Ibuscappreedit;
memset(&res, 0, sizeof res); memset(&res, 0, sizeof res);
sendrequest(b, Keycap, 0, 0, &res); sendrequest(b, Keycap, 0, 0, &res);
nexttrace(t, &f, Keycap, b); nexttrace(t, &f, Keycap, b);
@@ -296,6 +220,7 @@ ibus_private_input_policy(struct ct *t)
Ibusfix f; Ibusfix f;
Ictx *ctx; Ictx *ctx;
Keyres res; Keyres res;
char text[Maxutf];
int i; int i;
if(!ibusbegin(t, &f)) if(!ibusbegin(t, &f))
@@ -309,14 +234,14 @@ ibus_private_input_policy(struct ct *t)
} }
ctx->focused = 1; ctx->focused = 1;
ctx->cap = Cclientpreedit; ctx->cap = 1;
ctx->purpose = 0; ctx->purpose = 0;
contextkey(t, &f, ctx, 'n', Testctrlmask); contextkey(t, &f, ctx, 'n', Testctrlmask);
res = contextkey(t, &f, ctx, 'k', 0); res = contextkey(t, &f, ctx, 'k', 0);
checkstr(t, "preedit", "k", &res.preedit); checkstr(t, "preedit", "k", &res.preedit);
ctx->purpose = Ibuspurposepassword; ctx->purpose = Ibuspurposepassword;
memset(&res, 0, sizeof res); memset(&res, 0, sizeof res);
CT_CHECK(t, !processkey(ctx, 'x', 0, &res)); CT_CHECK(t, !processkey(ctx, 'x', 0, text, sizeof text, &res));
CT_CHECK(t, !res.eaten); CT_CHECK(t, !res.eaten);
notrace(t, &f); notrace(t, &f);
checkenginepreedit(t, "k"); checkenginepreedit(t, "k");
@@ -347,6 +272,7 @@ ibus_context_lifecycle(struct ct *t)
Keyreq req; Keyreq req;
Keyres res; Keyres res;
Caret at; Caret at;
char text[Maxutf];
if(!ibusbegin(t, &f)) if(!ibusbegin(t, &f))
goto cleanup; goto cleanup;
@@ -358,25 +284,21 @@ ibus_context_lifecycle(struct ct *t)
CT_EQ_PTR(t, b, findcontext(f.c2, "/context/one")); CT_EQ_PTR(t, b, findcontext(f.c2, "/context/one"));
CT_EQ_PTR(t, nil, findcontext(f.c1, "/context/missing")); CT_EQ_PTR(t, nil, findcontext(f.c1, "/context/missing"));
memset(&res, 0, sizeof res);
CT_CHECK(t, !processkey(a, 'x', 0, &res));
CT_CHECK(t, !res.eaten);
notrace(t, &f);
CT_EQ_PTR(t, nil, testengineowner());
setcursor(a, -10, -20, 14); setcursor(a, -10, -20, 14);
notrace(t, &f); notrace(t, &f);
CT_CHECK(t, a->caret.valid); CT_CHECK(t, a->caret.valid);
a->focused = 1;
memset(&res, 0, sizeof res); memset(&res, 0, sizeof res);
CT_CHECK(t, !processkey(a, 'x', Relmask, &res)); CT_CHECK(t, !processkey(a, 'x', Relmask, text, sizeof text, &res));
CT_CHECK(t, !res.eaten); CT_CHECK(t, !res.eaten);
notrace(t, &f); notrace(t, &f);
CT_CHECK(t, !a->focused);
CT_EQ_PTR(t, nil, testengineowner()); CT_EQ_PTR(t, nil, testengineowner());
/* A key stands for the FocusIn a client may never send. */
memset(&res, 0, sizeof res); memset(&res, 0, sizeof res);
CT_CHECK(t, processkey(a, 'x', 0, &res)); CT_CHECK(t, processkey(a, 'x', 0, text, sizeof text, &res));
req = nexttrace(t, &f, Keypress, a); req = nexttrace(t, &f, Keypress, a);
CT_CHECK(t, a->focused);
CT_CHECK(t, !res.eaten); CT_CHECK(t, !res.eaten);
CT_CHECK(t, req.caret.valid); CT_CHECK(t, req.caret.valid);
CT_EQ_INT(t, -10, req.caret.x); CT_EQ_INT(t, -10, req.caret.x);
@@ -450,7 +372,7 @@ ibus_context_lifecycle(struct ct *t)
notrace(t, &f); notrace(t, &f);
memset(&res, 0, sizeof res); memset(&res, 0, sizeof res);
CT_CHECK(t, !processkey(b, 'x', Relmask, &res)); CT_CHECK(t, !processkey(b, 'x', Relmask, text, sizeof text, &res));
CT_CHECK(t, !res.eaten); CT_CHECK(t, !res.eaten);
notrace(t, &f); notrace(t, &f);
CT_EQ_PTR(t, b, testengineowner()); CT_EQ_PTR(t, b, testengineowner());
@@ -507,12 +429,8 @@ ibus_active_release_lifecycle(struct ct *t)
reused = newcontext(f.c1, "/context/prune"); reused = newcontext(f.c1, "/context/prune");
CT_EQ_PTR(t, ctx, reused); CT_EQ_PTR(t, ctx, reused);
CT_EQ_PTR(t, nil, testengineowner()); CT_EQ_PTR(t, nil, testengineowner());
memset(&res, 0, sizeof res);
CT_CHECK(t, !processkey(reused, 'x', 0, &res));
CT_CHECK(t, !res.eaten);
notrace(t, &f);
reused->focused = 1;
contextkey(t, &f, reused, 'n', 0); contextkey(t, &f, reused, 'n', 0);
CT_CHECK(t, reused->focused);
CT_EQ_PTR(t, reused, testengineowner()); CT_EQ_PTR(t, reused, testengineowner());
dropconncontexts(f.c1); dropconncontexts(f.c1);
nexttrace(t, &f, Keyrelease, reused); nexttrace(t, &f, Keyrelease, reused);

View File

@@ -161,7 +161,7 @@ tryclient(char *path, int64_t deadline, int *client)
fd = connectsocket(path, deadline); fd = connectsocket(path, deadline);
if(fd < 0) if(fd < 0)
return errno == ETIMEDOUT ? -1 : 0; return errno == ETIMEDOUT ? -1 : 0;
if(!sendkey(fd, 1, 0, Kmodfirst)){ if(!sendkey(fd, 1, 0, 0)){
close(fd); close(fd);
return 0; return 0;
} }
@@ -208,7 +208,7 @@ overflowrejected(char *path)
fd = connectsocket(path, deadline); fd = connectsocket(path, deadline);
if(fd < 0) if(fd < 0)
return fail("overflow connect: %s", strerror(errno)); return fail("overflow connect: %s", strerror(errno));
if(!sendkey(fd, 1, 0, Kmodfirst)){ if(!sendkey(fd, 1, 0, 0)){
close(fd); close(fd);
return 1; return 1;
} }
@@ -286,7 +286,7 @@ runcapacity(char *path)
fail("connect capacity client %d: %s", i, strerror(errno)); fail("connect capacity client %d: %s", i, strerror(errno));
goto out; goto out;
} }
if(!requestkey(client[i], 1, 0, Kmodfirst, 0, "", "", if(!requestkey(client[i], 1, 0, 0, 0, "", "",
"capacity modifier")) "capacity modifier"))
goto out; goto out;
} }

View File

@@ -55,10 +55,12 @@ ipc_masks_modifiers(struct ct *t)
CT_EQ_UINT(t, Kret, ipckeysym(0xff0d, '\r')); CT_EQ_UINT(t, Kret, ipckeysym(0xff0d, '\r'));
CT_EQ_UINT(t, Kret, ipckeysym(0xff8d, '\r')); /* KP_Enter */ CT_EQ_UINT(t, Kret, ipckeysym(0xff8d, '\r')); /* KP_Enter */
CT_EQ_UINT(t, Ktab, ipckeysym(0xfe20, 0)); /* ISO_Left_Tab */ CT_EQ_UINT(t, Ktab, ipckeysym(0xfe20, 0)); /* ISO_Left_Tab */
CT_EQ_UINT(t, Kdown, ipckeysym(0xff99, 0)); /* KP_Down */
CT_EQ_UINT(t, Kback, ipckeysym(0xff08, 8)); CT_EQ_UINT(t, Kback, ipckeysym(0xff08, 8));
CT_EQ_UINT(t, Kspec + 0xff, ipckeysym(0xffff, 0x7f)); /* Delete */ CT_EQ_UINT(t, Kspec + 0xff, ipckeysym(0xffff, 0x7f)); /* Delete */
CT_EQ_UINT(t, 0x1f642, ipckeysym(0x101f642, 0x1f642)); CT_EQ_UINT(t, 0x1f642, ipckeysym(0x101f642, 0x1f642));
CT_EQ_UINT(t, 0, ipckeysym(0xfe03, 0)); /* ISO_Level3_Shift */ CT_EQ_UINT(t, 0, ipckeysym(0xfe03, 0)); /* ISO_Level3_Shift */
CT_EQ_UINT(t, 0, ipckeysym(0xffe1, 0)); /* Shift_L */
CT_EQ_UINT(t, Mshift|Mctrl, ipcmod(Mshift|Mctrl|(1<<1)|(1<<4))); CT_EQ_UINT(t, Mshift|Mctrl, ipcmod(Mshift|Mctrl|(1<<1)|(1<<4)));
CT_EQ_UINT(t, Msuper, ipcmod(1<<6)); CT_EQ_UINT(t, Msuper, ipcmod(1<<6));
CT_EQ_UINT(t, Msuper, ipcmod(1<<26)); CT_EQ_UINT(t, Msuper, ipcmod(1<<26));
@@ -68,7 +70,7 @@ void
ipc_control_and_caret_frames(struct ct *t) ipc_control_and_caret_frames(struct ct *t)
{ {
static const uchar capon[Ipcreqsz] = { static const uchar capon[Ipcreqsz] = {
Ipcext|Cclientpreedit, Ipcversion, Ipcopcap, 0, 0, 0, Ipcext|Ipcreqwant, Ipcversion, Ipcopcap, 0, 0, 0,
}; };
static const uchar capoff[Ipcreqsz] = { static const uchar capoff[Ipcreqsz] = {
Ipcext, Ipcversion, Ipcopcap, 0, 0, 0, Ipcext, Ipcversion, Ipcopcap, 0, 0, 0,
@@ -87,7 +89,7 @@ ipc_control_and_caret_frames(struct ct *t)
int valid, want; int valid, want;
int32_t x, y, h; int32_t x, y, h;
ipcpackcap(buf, Cclientpreedit); ipcpackcap(buf, 1);
CT_EQ_MEM(t, capon, buf, sizeof capon); CT_EQ_MEM(t, capon, buf, sizeof capon);
CT_EQ_INT(t, Ipccap, ipcreqtype(buf)); CT_EQ_INT(t, Ipccap, ipcreqtype(buf));
/* The probe remains a harmless key-zero request to an old daemon. */ /* The probe remains a harmless key-zero request to an old daemon. */

View File

@@ -1,73 +0,0 @@
#include "dat.h"
#include "fn.h"
#include "test.h"
#include <xcb-imdkit/encoding.h>
#include <xkbcommon/xkbcommon.h>
#include <xkbcommon/xkbcommon-keysyms.h>
uint32_t keymaplookup(struct xkb_state*, uint8_t, uint16_t);
enum
{
ShiftMask = 1<<0,
LockMask = 1<<1,
Mod2Mask = 1<<4,
Mod5Mask = 1<<7,
Group1 = 1<<13,
};
void
xim_keymap_lookup(struct ct *t)
{
struct xkb_context *context;
struct xkb_keymap *keymap;
struct xkb_rule_names names;
struct xkb_state *state;
memset(&names, 0, sizeof names);
names.layout = "de,ru";
context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
keymap = xkb_keymap_new_from_names(context, &names,
XKB_KEYMAP_COMPILE_NO_FLAGS);
if(!CT_CHECK(t, keymap != nil)){
xkb_context_unref(context);
return;
}
state = xkb_state_new(keymap);
CT_EQ_UINT(t, XKB_KEY_a, keymaplookup(state, 38, 0));
CT_EQ_UINT(t, XKB_KEY_A, keymaplookup(state, 38, ShiftMask));
CT_EQ_UINT(t, XKB_KEY_A, keymaplookup(state, 38, LockMask));
CT_EQ_UINT(t, XKB_KEY_a, keymaplookup(state, 38, LockMask|ShiftMask));
CT_EQ_UINT(t, XKB_KEY_EuroSign, keymaplookup(state, 26, Mod5Mask));
CT_EQ_UINT(t, XKB_KEY_Cyrillic_u, keymaplookup(state, 26, Group1));
CT_EQ_UINT(t, XKB_KEY_KP_End, keymaplookup(state, 87, 0));
CT_EQ_UINT(t, XKB_KEY_KP_1, keymaplookup(state, 87, Mod2Mask));
xkb_state_unref(state);
xkb_keymap_unref(keymap);
xkb_context_unref(context);
}
/* imdkit carries any UTF-8 text through COMPOUND_TEXT unchanged. */
void
xim_compound_text(struct ct *t)
{
static char *text[] = { "A😀한", "한글", "𠀋", "👩‍💻" };
char *ct, *utf8;
size_t clen, len, ulen;
int i;
xcb_compound_text_init();
for(i = 0; i < nelem(text); i++){
len = strlen(text[i]);
ct = xcb_utf8_to_compound_text(text[i], len, &clen);
if(!CT_CHECK(t, ct != nil))
continue;
utf8 = xcb_compound_text_to_utf8(ct, clen, &ulen);
if(CT_CHECK(t, utf8 != nil)){
CT_EQ_SIZE(t, len, ulen);
CT_EQ_MEM(t, text[i], utf8, len);
}
free(utf8);
free(ct);
}
}

View File

@@ -8,7 +8,7 @@ kotrans(char *keys)
Str out, raw; Str out, raw;
raw = mkstr(keys); raw = mkstr(keys);
transstr(getlang(LangKO), &raw, &out); transstr(getlang(LangKO), nil, &raw, &out);
return out; return out;
} }
@@ -25,6 +25,12 @@ korean_sequences(struct ct *t)
{ "rr", "ㄱㄱ" }, { "rr", "ㄱㄱ" },
{ "Rk", "" }, { "Rk", "" },
{ "rk1", "가1" }, { "rk1", "가1" },
{ "rt", "" },
{ "rtk", "ㄱ사" },
{ "rtt", "ㄳㅅ" },
{ "kr", "" },
{ "hkr", "" },
{ "kk", "ㅏㅏ" },
}; };
Str out; Str out;
int i; int i;
@@ -81,6 +87,7 @@ korean_backspace(struct ct *t)
{ {
static const struct { char *before, *after; } cases[] = { static const struct { char *before, *after; } cases[] = {
{ "", "" }, { "", "" },
{ "", "" },
{ "", "" }, { "", "" },
{ "", "" }, { "", "" }, { "", "" }, { "", "" }, { "", "" }, { "", "" },
{ "", "" }, { "", "" }, { "", "" }, { "", "" }, { "", "" }, { "", "" },

View File

@@ -240,6 +240,81 @@ childfds(int fd)
return dup2(fd, STDOUT_FILENO) >= 0 && dup2(fd, STDERR_FILENO) >= 0; return dup2(fd, STDOUT_FILENO) >= 0 && dup2(fd, STDERR_FILENO) >= 0;
} }
/*
* A private X server for the tests that need one; its display goes in
* Live, so the daemon and the clients started after it share it.
*/
int
startxvfb(Live *l, Daemon *d, char *program)
{
struct pollfd pfd;
char fdarg[16], line[32];
int errpipe[2], n, pipefd[2];
int64_t deadline;
pid_t pid;
ssize_t nr;
if(pipe2(pipefd, O_CLOEXEC) < 0)
return fail("Xvfb display pipe: %s", strerror(errno));
if(pipe2(errpipe, O_CLOEXEC|O_NONBLOCK) < 0){
close(pipefd[0]);
close(pipefd[1]);
return fail("pipe2: %s", strerror(errno));
}
pid = fork();
if(pid < 0){
close(pipefd[0]);
close(pipefd[1]);
close(errpipe[0]);
close(errpipe[1]);
return fail("fork Xvfb: %s", strerror(errno));
}
if(pid == 0){
close(pipefd[0]);
close(errpipe[0]);
n = fcntl(pipefd[1], F_GETFD);
if(n < 0 || fcntl(pipefd[1], F_SETFD, n & ~FD_CLOEXEC) < 0 ||
!childfds(errpipe[1]))
_exit(126);
snprintf(fdarg, sizeof fdarg, "%d", pipefd[1]);
execlp(program, program, "-displayfd", fdarg, "-screen", "0",
"1024x768x24", "-nolisten", "tcp", "-noreset", (char*)0);
_exit(127);
}
close(pipefd[1]);
close(errpipe[1]);
d->pid = pid;
d->errfd = errpipe[0];
pfd.fd = pipefd[0];
pfd.events = POLLIN|POLLHUP;
deadline = nowms() + Starttimeout;
n = 0;
while(n + 1 < (int)sizeof line){
pfd.revents = 0;
if(poll(&pfd, 1, leftms(deadline)) <= 0)
break;
nr = read(pipefd[0], line + n, sizeof line - n - 1);
if(nr < 0 && errno == EINTR)
continue;
if(nr <= 0)
break;
n += nr;
line[n] = '\0';
if(strchr(line, '\n') != NULL)
break;
}
close(pipefd[0]);
if(n == 0 || strchr(line, '\n') == NULL)
return fail("Xvfb did not report a private display");
line[strcspn(line, "\r\n")] = '\0';
if(strspn(line, "0123456789") != strlen(line))
return fail("invalid Xvfb display number %s", line);
if(snprintf(l->display, sizeof l->display, ":%s", line)
>= (int)sizeof l->display)
return fail("Xvfb display number is too long");
return 1;
}
int int
startdaemon(Live *l, Daemon *d, char *program, char *arg) startdaemon(Live *l, Daemon *d, char *program, char *arg)
{ {
@@ -268,7 +343,10 @@ startdaemon(Live *l, Daemon *d, char *program, char *arg)
} }
if(setenv("XDG_RUNTIME_DIR", l->runtime, 1) < 0 || if(setenv("XDG_RUNTIME_DIR", l->runtime, 1) < 0 ||
setenv("XDG_CONFIG_HOME", l->config, 1) < 0 || setenv("XDG_CONFIG_HOME", l->config, 1) < 0 ||
setenv("HOME", l->home, 1) < 0 || unsetenv("DISPLAY") < 0 || setenv("HOME", l->home, 1) < 0 ||
(l->display[0] != '\0' ?
setenv("DISPLAY", l->display, 1) :
unsetenv("DISPLAY")) < 0 ||
unsetenv("DBUS_SESSION_BUS_ADDRESS") < 0 || unsetenv("DBUS_SESSION_BUS_ADDRESS") < 0 ||
unsetenv("IBUS_ADDRESS") < 0 || unsetenv("IBUS_ADDRESS") < 0 ||
unsetenv("IBUS_ADDRESS_FILE") < 0){ unsetenv("IBUS_ADDRESS_FILE") < 0){
@@ -393,7 +471,7 @@ stopdaemon(Daemon *d)
} }
/* plan9port turns a caught termination note into exit status 1. */ /* plan9port turns a caught termination note into exit status 1. */
if(reaped && !((WIFSIGNALED(status) && WTERMSIG(status) == SIGTERM) || if(reaped && !((WIFSIGNALED(status) && WTERMSIG(status) == SIGTERM) ||
(WIFEXITED(status) && WEXITSTATUS(status) == 1))) (WIFEXITED(status) && WEXITSTATUS(status) <= 1)))
ok = fail("%s exited with unexpected wait status %#x", d->name, ok = fail("%s exited with unexpected wait status %#x", d->name,
status); status);
readerrors(d); readerrors(d);
@@ -691,5 +769,5 @@ ipcprobe(int fd, char *where)
{ {
unsigned char empty[] = {0, 0, 0, 0, 0}; unsigned char empty[] = {0, 0, 0, 0, 0};
return ipcrequest(fd, 0, Kmodfirst, empty, sizeof empty, where); return ipcrequest(fd, 0, 0, empty, sizeof empty, where);
} }

View File

@@ -39,6 +39,7 @@ struct Live
char home[320]; char home[320];
char socket[384]; char socket[384];
char addrfile[512]; char addrfile[512];
char display[32];
}; };
extern char *testname; extern char *testname;
@@ -54,6 +55,7 @@ int livesetup(Live*, char*);
int liveclean(Live*); int liveclean(Live*);
void daemoninit(Daemon*, char*); void daemoninit(Daemon*, char*);
int startxvfb(Live*, Daemon*, char*);
int startdaemon(Live*, Daemon*, char*, char*); int startdaemon(Live*, Daemon*, char*, char*);
int waitready(Live*, Daemon*, char*, size_t); int waitready(Live*, Daemon*, char*, size_t);
int daemonalive(Daemon*); int daemonalive(Daemon*);

View File

@@ -12,10 +12,8 @@ MKEMOJI = ROOT / "map" / "mkemoji"
TIMEOUT = 10 TIMEOUT = 10
def generate(source=None): def generate(*sources):
args = [sys.executable, "-B", str(MKEMOJI)] args = [sys.executable, "-B", str(MKEMOJI)] + [str(s) for s in sources]
if source is not None:
args.append(str(source))
return subprocess.run( return subprocess.run(
args, capture_output=True, text=True, check=False, timeout=TIMEOUT args, capture_output=True, text=True, check=False, timeout=TIMEOUT
) )
@@ -37,27 +35,32 @@ class MkemojiTest(unittest.TestCase):
result = generate() result = generate()
self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.returncode, 0, result.stderr)
data = table(result.stdout) data = table(result.stdout)
self.assertEqual(data["^"].split()[:9], list("¹²³⁴⁵⁶⁷⁸⁹")) keys = list(data)
self.assertEqual(data["_"].split()[:9], list("₁₂₃₄₅₆₇₈₉")) self.assertEqual([k for k in keys if k[:1] == "^"][:9],
self.assertEqual(data["<"].split()[2], "") [f"^{d}" for d in range(1, 10)])
self.assertEqual([k for k in keys if k[:1] == "_"][:9],
[f"_{d}" for d in range(1, 10)])
self.assertEqual([k for k in keys if k[:1] == "<"][2], "<3")
self.assertEqual(data["^1"], "¹") self.assertEqual(data["^1"], "¹")
self.assertEqual(data["_2"], "") self.assertEqual(data["_2"], "")
self.assertEqual(data["<3"], "") self.assertEqual(data["<3"].split()[0], "")
self.assertIn("😀", data["smile"].split())
self.assertIn("😀", data["웃음"].split())
def test_fold_normalize_and_exact_first(self): def test_fold_normalize_and_source_order(self):
source = self.source( source = self.source(
"β\tALPHABET\n" "β\tALPHABET\talpha\n"
"α\talpha\n" "α\talpha\n"
"e\u0301\tE\u0301\n" "é\t\n"
"#\thash\n" "#\thash\n"
) )
first = generate(source) first = generate(source)
second = generate(source) second = generate(source, source)
self.assertEqual(first.returncode, 0, first.stderr) self.assertEqual(first.returncode, 0, first.stderr)
self.assertEqual(first.stdout, second.stdout) self.assertEqual(first.stdout, second.stdout)
data = table(first.stdout) data = table(first.stdout)
self.assertEqual(data["alpha"].split(), ["α", "β"]) self.assertEqual(list(data), ["alphabet", "alpha", "é", "hash"])
self.assertEqual(data["al"].split(), ["β", "α"]) self.assertEqual(data["alpha"].split(), ["β", "α"])
self.assertEqual(data["é"], "é") self.assertEqual(data["é"], "é")
self.assertEqual(data["hash"], "#") self.assertEqual(data["hash"], "#")

View File

@@ -42,25 +42,27 @@ class MkhanjaTest(unittest.TestCase):
self.addCleanup(tmp.cleanup) self.addCleanup(tmp.cleanup)
return path return path
def test_imports_single_bmp_hanja_rows(self): def test_imports_bmp_hanja_rows(self):
result = run( result = run(
IMPORT, IMPORT,
self.source( self.source(
"# Copyright holder\n" "# Copyright holder\n"
"# BSD license\n" "# BSD license\n"
"한:漢:first\n" "\ud55c:\u6f22:first\n"
"가:㐀:extension A\n" "\uac00:\u3400:extension A\n"
"김:金:compatibility\n" "\uae40:\u91d1:compatibility\n"
"방학:放:word reading\n" "\ud55c\uc790:\u6f22\u5b57:word\n"
"학:學校:word value\n" "\ud55c:\U00020000:astral\n"
"한:𠀀:astral\n" "\u3131:\u52a0:jamo\n"
"ㄱ:加:jamo\n" "\ud55c\uae00:\u97d3glyph:mixed\n"
), ),
) )
self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.returncode, 0, result.stderr)
lines = result.stdout.splitlines() lines = result.stdout.splitlines()
self.assertEqual(lines[:2], ["# Copyright holder", "# BSD license"]) self.assertEqual(lines[:2], ["# Copyright holder", "# BSD license"])
self.assertEqual(lines[2:], ["", "\t", "\t", "\t"]) self.assertEqual(lines[2:], ["", "\u6f22\t\ud55c", "\u3400\t\uac00",
"\u91d1\t\uae40",
"\u6f22\u5b57\t\ud55c\uc790"])
def test_import_rejects_malformed_and_duplicate_rows(self): def test_import_rejects_malformed_and_duplicate_rows(self):
bad = [ bad = [
@@ -91,6 +93,7 @@ class MkhanjaTest(unittest.TestCase):
"\t\n" "\t\n"
"\t\n" "\t\n"
"\t\n" "\t\n"
"漢字\t한자\n"
), ),
) )
self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.returncode, 0, result.stderr)
@@ -103,13 +106,14 @@ class MkhanjaTest(unittest.TestCase):
"\t漢 韓", "\t漢 韓",
"\t㐀 家", "\t㐀 家",
"\t", "\t",
"한자\t漢字",
], ],
) )
def test_generator_rejects_bad_rows(self): def test_generator_rejects_bad_rows(self):
bad = [ bad = [
"\t\n", "a\t\n",
"\t\n", "\ta\n",
"𠀀\t\n", "𠀀\t\n",
"\t\n\t\n", "\t\n\t\n",
"漢 한\n", "漢 한\n",

View File

@@ -2,6 +2,9 @@
#include "fn.h" #include "fn.h"
#include "test.h" #include "test.h"
/* A popup with every section, tall enough for the tests. */
#define Imgh (2*PopupPad + (Maxdisp + 2)*Fontsz + PopupSep)
enum { enum {
Rgbmask = 0xffffff, Rgbmask = 0xffffff,
}; };
@@ -40,9 +43,6 @@ checkpadding(struct ct *t, u32int *img, Popup *p)
void void
popup_layout(struct ct *t) popup_layout(struct ct *t)
{ {
int tinyheight[] = {
1, PopupPad, 2*PopupPad + Fontsz - 1,
};
static const struct { static const struct {
int first; int first;
int shown; int shown;
@@ -132,8 +132,7 @@ popup_layout(struct ct *t)
CT_EQ_INT(t, 100, x); CT_EQ_INT(t, 100, x);
CT_EQ_INT(t, 200, y); CT_EQ_INT(t, 200, y);
if(!CT_CHECK(t, textinit())) textinit();
return;
/* Empty sections reserve nothing. */ /* Empty sections reserve nothing. */
memset(&dc, 0, sizeof dc); memset(&dc, 0, sizeof dc);
@@ -145,10 +144,10 @@ popup_layout(struct ct *t)
CT_EQ_INT(t, -1, p.rowsy); CT_EQ_INT(t, -1, p.rowsy);
CT_EQ_INT(t, -1, p.marky); CT_EQ_INT(t, -1, p.marky);
/* Preedit only. */ /* Preedit only: as wide as its text. */
dc.pre = mkstr("preedit"); dc.pre = mkstr("preedit");
popuplayout(&dc, 2*PopupBasew, Imgh, &p); popuplayout(&dc, 2*PopupBasew, Imgh, &p);
CT_EQ_INT(t, PopupBasew, p.w); CT_EQ_INT(t, textwidth(&dc.pre) + 2*PopupPad, p.w);
CT_EQ_INT(t, PopupPad, p.prey); CT_EQ_INT(t, PopupPad, p.prey);
CT_EQ_INT(t, -1, p.sepy); CT_EQ_INT(t, -1, p.sepy);
CT_EQ_INT(t, -1, p.rowsy); CT_EQ_INT(t, -1, p.rowsy);
@@ -320,24 +319,5 @@ popup_layout(struct ct *t)
CT_CHECK(t, numink > 0); /* full-width */ CT_CHECK(t, numink > 0); /* full-width */
CT_CHECK(t, markink > 0); /* 9-9/9 */ CT_CHECK(t, markink > 0); /* 9-9/9 */
/* The selected row wins over preedit and footer in one-row height. */
dc.pre = mkstr("preedit");
popuplayout(&dc, PopupBasew, 2*PopupPad + Fontsz, &p);
CT_EQ_INT(t, -1, p.prey);
CT_EQ_INT(t, 1, p.n);
CT_EQ_INT(t, Maxdisp - 1, p.row0);
CT_EQ_INT(t, -1, p.marky);
/* With less than one padded row, selected content starts at the top. */
for(i = 0; i < nelem(tinyheight); i++){
popuplayout(&dc, PopupBasew, tinyheight[i], &p);
CT_EQ_INT(t, min(tinyheight[i], Fontsz), p.h);
CT_EQ_INT(t, 0, p.rowsy);
CT_EQ_INT(t, 0, p.sely);
CT_EQ_INT(t, 1, p.n);
CT_EQ_INT(t, Maxdisp - 1, p.row0);
popupdraw(img, &dc, &p);
CT_EQ_UINT(t, Colsel, pixel(img, &p, p.w - PopupPad - 1, 0));
}
free(img); free(img);
} }

View File

@@ -143,14 +143,14 @@ nextrequestcap(struct ct *t, Pump *p, int op, int cap)
memset(&req, 0, sizeof req); memset(&req, 0, sizeof req);
chanrecv(p->trace, &req); chanrecv(p->trace, &req);
CT_EQ_INT(t, op, req.op); CT_EQ_INT(t, op, req.op);
CT_EQ_INT(t, cap, req.cap); CT_EQ_INT(t, cap, req.clientpre);
return req; return req;
} }
static Keyreq static Keyreq
nextrequest(struct ct *t, Pump *p, int op) nextrequest(struct ct *t, Pump *p, int op)
{ {
return nextrequestcap(t, p, op, Cclientpreedit); return nextrequestcap(t, p, op, 1);
} }
static void static void
@@ -284,10 +284,11 @@ server_connection_ownership(struct ct *t)
req = nextrequest(t, &f.pump, Keyreset); req = nextrequest(t, &f.pump, Keyreset);
CT_EQ_PTR(t, aowner, req.owner); CT_EQ_PTR(t, aowner, req.owner);
allowrequest(&f.pump); allowrequest(&f.pump);
CT_CHECK(t, readreply(t, &a, 1, "", preedit)); /* The reset hands back what the takeover took from this client. */
CT_CHECK(t, readreply(t, &a, 1, "", preedit));
CT_EQ_STR(t, "", preedit); CT_EQ_STR(t, "", preedit);
CT_EQ_PTR(t, bowner, testengineowner()); CT_EQ_PTR(t, bowner, testengineowner());
if(!sendkey(t, &b, 1, 0, Kmodfirst)) if(!sendkey(t, &b, 1, 0, 0))
goto cleanup; goto cleanup;
req = nextrequest(t, &f.pump, Keypress); req = nextrequest(t, &f.pump, Keypress);
allowrequest(&f.pump); allowrequest(&f.pump);
@@ -387,7 +388,7 @@ server_extension_stream(struct ct *t)
goto cleanup; goto cleanup;
/* Capability and caret frames may be split at any byte boundary. */ /* Capability and caret frames may be split at any byte boundary. */
ipcpackcap(frame, Cclientpreedit); ipcpackcap(frame, 1);
if(!sendframe(t, &a, frame, Ipcreqsz, 1)) if(!sendframe(t, &a, frame, Ipcreqsz, 1))
goto cleanup; goto cleanup;
req = nextrequest(t, &f.pump, Keycap); req = nextrequest(t, &f.pump, Keycap);
@@ -446,7 +447,7 @@ server_extension_stream(struct ct *t)
allowrequest(&f.pump); allowrequest(&f.pump);
CT_EQ_INT(t, 1, readreply(t, &a, 0, "k", preedit)); CT_EQ_INT(t, 1, readreply(t, &a, 0, "k", preedit));
ipcpackcap(frame, Cclientpreedit); ipcpackcap(frame, 1);
if(!sendframe(t, &a, frame, Ipcreqsz, 0)) if(!sendframe(t, &a, frame, Ipcreqsz, 0))
goto cleanup; goto cleanup;
req = nextrequest(t, &f.pump, Keycap); req = nextrequest(t, &f.pump, Keycap);

View File

@@ -1,8 +1,6 @@
/* Included after dat.h and fn.h (or after the source under test). */ /* Included after dat.h and fn.h (or after the source under test). */
#include "cutest/cutest.h" #include "cutest/cutest.h"
extern Lang testvi;
/* /*
* A test's stand-in for imthread: handles engine requests from keyc, * A test's stand-in for imthread: handles engine requests from keyc,
* tracing each one first. Requests whose op is holdop (every request * tracing each one first. Requests whose op is holdop (every request
@@ -95,7 +93,7 @@ void engine_randomized_stress(struct ct*);
void engine_full_boundary_passthrough(struct ct*); void engine_full_boundary_passthrough(struct ct*);
void dictionary_candidates(struct ct*); void dictionary_candidates(struct ct*);
void dictionary_misses(struct ct*); void dictionary_misses(struct ct*);
void dictionary_emoji_identity(struct ct*); void dictionary_prefix(struct ct*);
void ipc_masks_modifiers(struct ct*); void ipc_masks_modifiers(struct ct*);
void ipc_control_and_caret_frames(struct ct*); void ipc_control_and_caret_frames(struct ct*);
void ipc_runtime_path(struct ct*); void ipc_runtime_path(struct ct*);
@@ -108,11 +106,11 @@ void server_connection_ownership(struct ct*);
void server_extension_stream(struct ct*); void server_extension_stream(struct ct*);
void server_rejects_unknown_extension(struct ct*); void server_rejects_unknown_extension(struct ct*);
void ibus_machine_id_fallback(struct ct*); void ibus_machine_id_fallback(struct ct*);
void ibus_startup_requires_ownership(struct ct*);
void ibus_capability_policy(struct ct*); void ibus_capability_policy(struct ct*);
void ibus_private_input_policy(struct ct*); void ibus_private_input_policy(struct ct*);
void ibus_context_lifecycle(struct ct*); void ibus_context_lifecycle(struct ct*);
void ibus_active_release_lifecycle(struct ct*); void ibus_active_release_lifecycle(struct ct*);
void compose_sequences(struct ct*);
void xim_keymap_lookup(struct ct*); void xim_keymap_lookup(struct ct*);
void xim_compound_text(struct ct*); void xim_compound_text(struct ct*);
void xim_adapter_key_contract(struct ct*); void xim_adapter_key_contract(struct ct*);

View File

@@ -7,7 +7,6 @@
Channel *drawc; Channel *drawc;
Channel *keyc; Channel *keyc;
Lang testvi;
void void
die(char *fmt, ...) die(char *fmt, ...)
@@ -45,20 +44,16 @@ erealloc(void *p, ulong n)
static void static void
testmapinit(void) testmapinit(void)
{ {
Lang *jp, *kata; Lang *jp, *kata, *vi;
jp = getlang(LangJP); jp = getlang(LangJP);
kata = getlang(LangJPK); kata = getlang(LangJPK);
if(jp == nil || kata == nil) vi = getlang(LangVI);
if(jp == nil || kata == nil || vi == nil)
die("test language is not registered"); die("test language is not registered");
jp->map = trieopen("../map/hira.map"); jp->map = trieopen("../map/hira.map");
kata->map = trieopen("../map/kata.map"); kata->map = trieopen("../map/kata.map");
memset(&testvi, 0, sizeof testvi); vi->map = trieopen("../map/telex.map");
testvi.lang = LangVI;
testvi.mapname = "telex";
testvi.trans = transvi;
testvi.back = backvi;
testvi.map = trieopen("../map/telex.map");
} }
#ifndef STRESS #ifndef STRESS
@@ -118,7 +113,7 @@ static const struct ct_test tests[] = {
{ "engine/full-boundary-passthrough", engine_full_boundary_passthrough }, { "engine/full-boundary-passthrough", engine_full_boundary_passthrough },
{ "dict/candidates", dictionary_candidates }, { "dict/candidates", dictionary_candidates },
{ "dict/misses", dictionary_misses }, { "dict/misses", dictionary_misses },
{ "dict/emoji-identity", dictionary_emoji_identity }, { "dict/prefix", dictionary_prefix },
{ "ipc/masks-modifiers", ipc_masks_modifiers }, { "ipc/masks-modifiers", ipc_masks_modifiers },
{ "ipc/control-caret-frames", ipc_control_and_caret_frames }, { "ipc/control-caret-frames", ipc_control_and_caret_frames },
{ "ipc/runtime-path", ipc_runtime_path }, { "ipc/runtime-path", ipc_runtime_path },
@@ -131,11 +126,11 @@ static const struct ct_test tests[] = {
{ "server/extension-stream", server_extension_stream }, { "server/extension-stream", server_extension_stream },
{ "server/rejects-unknown-extension", server_rejects_unknown_extension }, { "server/rejects-unknown-extension", server_rejects_unknown_extension },
{ "ibus/machine-id-fallback", ibus_machine_id_fallback }, { "ibus/machine-id-fallback", ibus_machine_id_fallback },
{ "ibus/startup-requires-ownership", ibus_startup_requires_ownership },
{ "ibus/capability-policy", ibus_capability_policy }, { "ibus/capability-policy", ibus_capability_policy },
{ "ibus/private-input-policy", ibus_private_input_policy }, { "ibus/private-input-policy", ibus_private_input_policy },
{ "ibus/context-lifecycle", ibus_context_lifecycle }, { "ibus/context-lifecycle", ibus_context_lifecycle },
{ "ibus/active-release-lifecycle", ibus_active_release_lifecycle }, { "ibus/active-release-lifecycle", ibus_active_release_lifecycle },
{ "compose/sequences", compose_sequences },
{ "xim/keymap-lookup", xim_keymap_lookup }, { "xim/keymap-lookup", xim_keymap_lookup },
{ "xim/compound-text", xim_compound_text }, { "xim/compound-text", xim_compound_text },
{ "xim/adapter-key-contract", xim_adapter_key_contract }, { "xim/adapter-key-contract", xim_adapter_key_contract },
@@ -170,8 +165,6 @@ threadmain(int argc, char **argv)
trieclose(langs[i].map); trieclose(langs[i].map);
langs[i].map = nil; langs[i].map = nil;
} }
trieclose(testvi.map);
testvi.map = nil;
chanfree(drawc); chanfree(drawc);
chanfree(keyc); chanfree(keyc);
threadexitsall(status == 0 ? nil : "tests failed"); threadexitsall(status == 0 ? nil : "tests failed");

View File

@@ -8,7 +8,7 @@ typevi(struct ct *t, char *keys, char *want)
Str out, raw; Str out, raw;
raw = mkstr(keys); raw = mkstr(keys);
transstr(&testvi, &raw, &out); transstr(getlang(LangVI), nil, &raw, &out);
checkstr(t, keys, want, &out); checkstr(t, keys, want, &out);
} }
@@ -67,7 +67,7 @@ vietnamese_backspace(struct ct *t)
for(i = 0; i < nelem(cases); i++){ for(i = 0; i < nelem(cases); i++){
memset(&state, 0, sizeof state); memset(&state, 0, sizeof state);
state.l = &testvi; state.l = getlang(LangVI);
state.pre = mkstr(cases[i].pre); state.pre = mkstr(cases[i].pre);
state.raw = mkstr(cases[i].raw); state.raw = mkstr(cases[i].raw);
backvi(&state); backvi(&state);

View File

@@ -67,7 +67,7 @@ static int wireflush(xcb_connection_t*);
#define xcb_translate_coordinates wiretranslate #define xcb_translate_coordinates wiretranslate
#define xcb_translate_coordinates_reply wiretranslatereply #define xcb_translate_coordinates_reply wiretranslatereply
#define xcb_flush wireflush #define xcb_flush wireflush
#include "xim/xim.c" #include "xim.c"
#undef xcb_im_commit_string #undef xcb_im_commit_string
#undef xcb_im_preedit_start_callback #undef xcb_im_preedit_start_callback
#undef xcb_im_preedit_draw_callback #undef xcb_im_preedit_draw_callback
@@ -437,7 +437,7 @@ nexttracecap(struct ct *t, Ximfix *f, int op, Ic *owner, int cap)
return req; return req;
CT_EQ_INT(t, op, req.op); CT_EQ_INT(t, op, req.op);
CT_EQ_PTR(t, owner, req.owner); CT_EQ_PTR(t, owner, req.owner);
CT_EQ_INT(t, cap, req.cap); CT_EQ_INT(t, cap, req.clientpre);
CT_EQ_PTR(t, replyc, req.reply); CT_EQ_PTR(t, replyc, req.reply);
return req; return req;
} }
@@ -445,7 +445,7 @@ nexttracecap(struct ct *t, Ximfix *f, int op, Ic *owner, int cap)
static Keyreq static Keyreq
nexttrace(struct ct *t, Ximfix *f, int op, Ic *state) nexttrace(struct ct *t, Ximfix *f, int op, Ic *state)
{ {
return nexttracecap(t, f, op, state, state->cap); return nexttracecap(t, f, op, state, state->clientpre);
} }
static void static void
@@ -539,7 +539,7 @@ xim_adapter_release_lifecycle(struct ct *t)
keypress(dead, 'n', 0, &res); keypress(dead, 'n', 0, &res);
nexttrace(t, &f, Keypress, dead); nexttrace(t, &f, Keypress, dead);
CT_EQ_PTR(t, dead, testengineowner()); CT_EQ_PTR(t, dead, testengineowner());
deadcap = dead->cap; deadcap = dead->clientpre;
icfree(dead); icfree(dead);
nexttracecap(t, &f, Keyrelease, dead, deadcap); nexttracecap(t, &f, Keyrelease, dead, deadcap);
dead = nil; dead = nil;
@@ -632,6 +632,9 @@ xim_adapter_styles(struct ct *t)
XCB_IM_PreeditPosition | XCB_IM_StatusNothing, XCB_IM_PreeditPosition | XCB_IM_StatusNothing,
XCB_IM_PreeditCallbacks | XCB_IM_StatusNothing, XCB_IM_PreeditCallbacks | XCB_IM_StatusNothing,
XCB_IM_PreeditNothing | XCB_IM_StatusNothing, XCB_IM_PreeditNothing | XCB_IM_StatusNothing,
XCB_IM_PreeditPosition | XCB_IM_StatusNone,
XCB_IM_PreeditCallbacks | XCB_IM_StatusNone,
XCB_IM_PreeditNothing | XCB_IM_StatusNone,
}; };
xcb_im_input_context_t *ic; xcb_im_input_context_t *ic;
Ic *state; Ic *state;
@@ -651,7 +654,8 @@ xim_adapter_styles(struct ct *t)
state = icbindings[0].data; state = icbindings[0].data;
if(!CT_CHECK(t, state != nil && state == ics)) if(!CT_CHECK(t, state != nil && state == ics))
continue; continue;
CT_EQ_INT(t, i == 1 ? Cclientpreedit : 0, state->cap); CT_EQ_INT(t, want[i] & XCB_IM_PreeditCallbacks ? 1 : 0,
state->clientpre);
CT_EQ_PTR(t, ic, state->xic); CT_EQ_PTR(t, ic, state->xic);
icunlink(state); icunlink(state);
free(state); free(state);
@@ -663,7 +667,7 @@ xim_adapter_placement(struct ct *t)
{ {
/* Spot location first, then the bottom of the focus window, then of /* Spot location first, then the bottom of the focus window, then of
* the client window; an unset focus window is the client window. */ * the client window; an unset focus window is the client window. */
static const struct { const struct {
u32int mask; u32int mask;
int focuswin; int focuswin;
int fgeo; int fgeo;
@@ -673,14 +677,15 @@ xim_adapter_placement(struct ct *t)
int valid; int valid;
int x; int x;
int y; int y;
int h;
} cases[] = { } cases[] = {
{XCB_XIM_XNSpotLocation_MASK, 10, 1, 1, 1, 1, 1, 103, 204}, {XCB_XIM_XNSpotLocation_MASK, 10, 1, 1, 1, 1, 1, 103, 204-Fontsz, Fontsz},
{XCB_XIM_XNSpotLocation_MASK, 10, 1, 0, 1, 1, 1, 300, 440}, {XCB_XIM_XNSpotLocation_MASK, 10, 1, 0, 1, 1, 1, 300, 440, 0},
{XCB_XIM_XNSpotLocation_MASK, 0, 1, 1, 1, 1, 1, 303, 404}, {XCB_XIM_XNSpotLocation_MASK, 0, 1, 1, 1, 1, 1, 303, 404-Fontsz, Fontsz},
{0, 10, 1, 1, 1, 1, 1, 100, 230}, {0, 10, 1, 1, 1, 1, 1, 100, 230, 0},
{0, 0, 1, 1, 1, 1, 1, 300, 440}, {0, 0, 1, 1, 1, 1, 1, 300, 440, 0},
{0, 10, 0, 1, 1, 1, 1, 300, 440}, {0, 10, 0, 1, 1, 1, 1, 300, 440, 0},
{0, 10, 0, 1, 0, 1, 0, 0, 0}, {0, 10, 0, 1, 0, 1, 0, 0, 0, 0},
}; };
xcb_im_input_context_t *ic; xcb_im_input_context_t *ic;
Icbinding *b; Icbinding *b;
@@ -712,7 +717,7 @@ xim_adapter_placement(struct ct *t)
CT_EQ_INT(t, cases[i].valid, state.caret.valid); CT_EQ_INT(t, cases[i].valid, state.caret.valid);
CT_EQ_INT(t, cases[i].x, state.caret.x); CT_EQ_INT(t, cases[i].x, state.caret.x);
CT_EQ_INT(t, cases[i].y, state.caret.y); CT_EQ_INT(t, cases[i].y, state.caret.y);
CT_EQ_INT(t, 0, state.caret.h); CT_EQ_INT(t, cases[i].h, state.caret.h);
} }
} }
@@ -752,13 +757,13 @@ xim_adapter_placement_updates(struct ct *t)
req = nexttrace(t, &f, Keypress, &state); req = nexttrace(t, &f, Keypress, &state);
CT_CHECK(t, req.caret.valid); CT_CHECK(t, req.caret.valid);
CT_EQ_INT(t, 105, req.caret.x); CT_EQ_INT(t, 105, req.caret.x);
CT_EQ_INT(t, 206, req.caret.y); CT_EQ_INT(t, 206-Fontsz, req.caret.y);
wirewins[0].rootx = 150; wirewins[0].rootx = 150;
wirewins[0].rooty = 250; wirewins[0].rooty = 250;
kpress(&state, &ev); kpress(&state, &ev);
req = nexttrace(t, &f, Keypress, &state); req = nexttrace(t, &f, Keypress, &state);
CT_EQ_INT(t, 155, req.caret.x); CT_EQ_INT(t, 155, req.caret.x);
CT_EQ_INT(t, 256, req.caret.y); CT_EQ_INT(t, 256-Fontsz, req.caret.y);
b->preattr.spot_location.x = 15; b->preattr.spot_location.x = 15;
b->preattr.spot_location.y = 16; b->preattr.spot_location.y = 16;
@@ -768,7 +773,7 @@ xim_adapter_placement_updates(struct ct *t)
req = nexttrace(t, &f, Keycaret, &state); req = nexttrace(t, &f, Keycaret, &state);
CT_CHECK(t, req.caret.valid); CT_CHECK(t, req.caret.valid);
CT_EQ_INT(t, 165, req.caret.x); CT_EQ_INT(t, 165, req.caret.x);
CT_EQ_INT(t, 266, req.caret.y); CT_EQ_INT(t, 266-Fontsz, req.caret.y);
b->preattrmask = 0; b->preattrmask = 0;
wirewins[0].geometryok = 0; wirewins[0].geometryok = 0;
hdr.major_opcode = XCB_XIM_SET_IC_FOCUS; hdr.major_opcode = XCB_XIM_SET_IC_FOCUS;
@@ -794,7 +799,7 @@ xim_adapter_callback_replacement(struct ct *t)
ic = (xcb_im_input_context_t*)(uintptr)3; ic = (xcb_im_input_context_t*)(uintptr)3;
memset(&state, 0, sizeof state); memset(&state, 0, sizeof state);
state.xic = ic; state.xic = ic;
state.cap = Cclientpreedit; state.clientpre = 1;
wireclear(); wireclear();
sinit(&pre, "k", strlen("k")); sinit(&pre, "k", strlen("k"));
updatepreedit(&state, &pre); updatepreedit(&state, &pre);
@@ -843,7 +848,7 @@ xim_adapter_callback_unicode(struct ct *t)
for(i = 0; i < nelem(text); i++){ for(i = 0; i < nelem(text); i++){
memset(&state, 0, sizeof state); memset(&state, 0, sizeof state);
state.xic = (xcb_im_input_context_t*)(uintptr)(10+i); state.xic = (xcb_im_input_context_t*)(uintptr)(10+i);
state.cap = Cclientpreedit; state.clientpre = 1;
sinit(&pre, text[i], strlen(text[i])); sinit(&pre, text[i], strlen(text[i]));
wireclear(); wireclear();
updatepreedit(&state, &pre); updatepreedit(&state, &pre);
@@ -863,7 +868,7 @@ startstate(Ic *state, xcb_im_input_context_t *ic)
memset(state, 0, sizeof *state); memset(state, 0, sizeof *state);
state->xic = ic; state->xic = ic;
state->cap = Cclientpreedit; state->clientpre = 1;
sinit(&pre, "", strlen("")); sinit(&pre, "", strlen(""));
updatepreedit(state, &pre); updatepreedit(state, &pre);
} }
@@ -891,7 +896,7 @@ xim_adapter_callback_cleanup(struct ct *t)
wireclear(); wireclear();
wirebind(0, ic, &state); wirebind(0, ic, &state);
state.xic = ic; state.xic = ic;
state.cap = Cclientpreedit; state.clientpre = 1;
keypress(&state, 'k', 0, &res); keypress(&state, 'k', 0, &res);
nexttrace(t, &f, Keypress, &state); nexttrace(t, &f, Keypress, &state);
keypress(&state, 'a', 0, &res); keypress(&state, 'a', 0, &res);
@@ -979,7 +984,7 @@ xim_adapter_callback_transfer(struct ct *t)
goto cleanup; goto cleanup;
wireclear(); wireclear();
a.xic = aic; a.xic = aic;
a.cap = Cclientpreedit; a.clientpre = 1;
sinit(&pre, "", strlen("")); sinit(&pre, "", strlen(""));
updatepreedit(&a, &pre); updatepreedit(&a, &pre);
keypress(&b, 'x', 0, &res); keypress(&b, 'x', 0, &res);
@@ -1016,7 +1021,7 @@ xim_adapter_callback_owner_loss(struct ct *t)
wireclear(); wireclear();
wirebind(0, ic, &state); wirebind(0, ic, &state);
state.xic = ic; state.xic = ic;
state.cap = Cclientpreedit; state.clientpre = 1;
keypress(&state, 'k', 0, &res); keypress(&state, 'k', 0, &res);
nexttrace(t, &f, Keypress, &state); nexttrace(t, &f, Keypress, &state);
updatepreedit(&state, &res.preedit); updatepreedit(&state, &res.preedit);
@@ -1054,9 +1059,12 @@ xim_adapter_callback_owner_loss(struct ct *t)
checkpreowner(); checkpreowner();
notrace(t, &f); notrace(t, &f);
/* Late XIM lifecycle messages remain idempotent after owner cleanup. */ /* The release hands back the reading the takeover took. */
release(&state); release(&state);
nexttrace(t, &f, Keyrelease, &state); nexttrace(t, &f, Keyrelease, &state);
CT_EQ_INT(t, 5, nwirecalls);
CT_EQ_INT(t, Wcommit, wirecalls[4].op);
/* Late XIM lifecycle messages remain idempotent after owner cleanup. */
release(&state); release(&state);
notrace(t, &f); notrace(t, &f);
memset(&hdr, 0, sizeof hdr); memset(&hdr, 0, sizeof hdr);
@@ -1066,7 +1074,7 @@ xim_adapter_callback_owner_loss(struct ct *t)
hdr.major_opcode = XCB_XIM_DESTROY_IC; hdr.major_opcode = XCB_XIM_DESTROY_IC;
callback(nil, nil, ic, &hdr, nil, nil, nil); callback(nil, nil, ic, &hdr, nil, nil, nil);
notrace(t, &f); notrace(t, &f);
CT_EQ_INT(t, 4, nwirecalls); CT_EQ_INT(t, 5, nwirecalls);
memset(&req, 0, sizeof req); memset(&req, 0, sizeof req);
req.owner = &foreign; req.owner = &foreign;
@@ -1104,3 +1112,61 @@ xim_adapter_commit_encoding(struct ct *t)
commit(&state, text, 0); commit(&state, text, 0);
CT_EQ_INT(t, 0, nwirecalls); CT_EQ_INT(t, 0, nwirecalls);
} }
void
xim_keymap_lookup(struct ct *t)
{
enum { ShiftMask = 1<<0, LockMask = 1<<1, Mod2Mask = 1<<4,
Mod5Mask = 1<<7, Group1 = 1<<13 };
struct xkb_context *context;
struct xkb_keymap *keymap;
struct xkb_rule_names names;
struct xkb_state *state;
memset(&names, 0, sizeof names);
names.layout = "de,ru";
context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
keymap = xkb_keymap_new_from_names(context, &names,
XKB_KEYMAP_COMPILE_NO_FLAGS);
if(!CT_CHECK(t, keymap != nil)){
xkb_context_unref(context);
return;
}
state = xkb_state_new(keymap);
CT_EQ_UINT(t, XKB_KEY_a, keymaplookup(state, 38, 0));
CT_EQ_UINT(t, XKB_KEY_A, keymaplookup(state, 38, ShiftMask));
CT_EQ_UINT(t, XKB_KEY_A, keymaplookup(state, 38, LockMask));
CT_EQ_UINT(t, XKB_KEY_a, keymaplookup(state, 38, LockMask|ShiftMask));
CT_EQ_UINT(t, XKB_KEY_EuroSign, keymaplookup(state, 26, Mod5Mask));
CT_EQ_UINT(t, XKB_KEY_Cyrillic_u, keymaplookup(state, 26, Group1));
CT_EQ_UINT(t, XKB_KEY_KP_End, keymaplookup(state, 87, 0));
CT_EQ_UINT(t, XKB_KEY_KP_1, keymaplookup(state, 87, Mod2Mask));
xkb_state_unref(state);
xkb_keymap_unref(keymap);
xkb_context_unref(context);
}
/* imdkit carries any UTF-8 text through COMPOUND_TEXT unchanged. */
void
xim_compound_text(struct ct *t)
{
static char *text[] = { "A😀한", "한글", "𠀋", "👩‍💻" };
char *ct, *utf8;
size_t clen, len, ulen;
int i;
xcb_compound_text_init();
for(i = 0; i < nelem(text); i++){
len = strlen(text[i]);
ct = xcb_utf8_to_compound_text(text[i], len, &clen);
if(!CT_CHECK(t, ct != nil))
continue;
utf8 = xcb_compound_text_to_utf8(ct, clen, &ulen);
if(CT_CHECK(t, utf8 != nil)){
CT_EQ_SIZE(t, len, ulen);
CT_EQ_MEM(t, text[i], utf8, len);
}
free(utf8);
free(ct);
}
}

View File

@@ -24,23 +24,14 @@ enum
Eventtimeout = 4000, Eventtimeout = 4000,
}; };
typedef struct Child Child;
typedef struct Run Run; typedef struct Run Run;
typedef struct Prelog Prelog; typedef struct Prelog Prelog;
struct Child
{
pid_t pid;
pid_t pgid;
char log[320];
};
struct Run struct Run
{ {
Live l; Live l;
Child xvfb; Daemon xvfb;
Child daemon; Daemon daemon;
char display[32];
}; };
struct Prelog struct Prelog
@@ -55,144 +46,10 @@ struct Prelog
static int xerror; static int xerror;
static int
childlog(char *path)
{
int fd;
fd = open(path, O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0600);
return fd;
}
static int
spawnxvfb(Run *r, char *program)
{
char fdstr[16], line[32];
int displayfd[2], errfd, flags, n, status;
int64_t deadline;
pid_t pid;
struct pollfd pfd;
ssize_t nr;
if(pipe2(displayfd, O_CLOEXEC) < 0)
return fail("create Xvfb display pipe: %s", strerror(errno));
errfd = childlog(r->xvfb.log);
if(errfd < 0){
close(displayfd[0]);
close(displayfd[1]);
return fail("open Xvfb log: %s", strerror(errno));
}
pid = fork();
if(pid < 0){
close(displayfd[0]);
close(displayfd[1]);
close(errfd);
return fail("fork Xvfb: %s", strerror(errno));
}
if(pid == 0){
setpgid(0, 0);
close(displayfd[0]);
flags = fcntl(displayfd[1], F_GETFD);
if(flags < 0 || fcntl(displayfd[1], F_SETFD,
flags & ~FD_CLOEXEC) < 0)
_exit(126);
if(dup2(errfd, STDOUT_FILENO) < 0 ||
dup2(errfd, STDERR_FILENO) < 0)
_exit(126);
close(errfd);
snprintf(fdstr, sizeof fdstr, "%d", displayfd[1]);
execlp(program, program, "-displayfd", fdstr,
"-screen", "0", "800x600x24", "-nolisten", "tcp",
"-noreset", (char*)0);
_exit(127);
}
close(displayfd[1]);
close(errfd);
if((setpgid(pid, pid) < 0 && errno != EACCES) || getpgid(pid) != pid){
kill(pid, SIGKILL);
while(waitpid(pid, &status, 0) < 0 && errno == EINTR)
;
close(displayfd[0]);
return fail("isolate Xvfb process group: %s", strerror(errno));
}
r->xvfb.pid = pid;
r->xvfb.pgid = pid;
pfd.fd = displayfd[0];
pfd.events = POLLIN|POLLHUP;
deadline = nowms() + Starttimeout;
n = 0;
while(n + 1 < (int)sizeof line){
pfd.revents = 0;
status = poll(&pfd, 1, leftms(deadline));
if(status < 0 && errno == EINTR)
continue;
if(status <= 0)
break;
nr = read(displayfd[0], line + n, sizeof line - n - 1);
if(nr > 0){
n += nr;
line[n] = '\0';
if(strchr(line, '\n') != NULL)
break;
continue;
}
if(nr < 0 && errno == EINTR)
continue;
break;
}
close(displayfd[0]);
if(n == 0 || strchr(line, '\n') == NULL)
return fail("Xvfb did not report a private display");
line[strcspn(line, "\r\n")] = '\0';
for(n = 0; line[n] != '\0'; n++)
if(line[n] < '0' || line[n] > '9')
return fail("invalid Xvfb display number %s", line);
if(snprintf(r->display, sizeof r->display, ":%s", line)
>= (int)sizeof r->display)
return fail("Xvfb display number is too long");
return 1;
}
static int
spawndaemon(Run *r, char *program, char *mapdir)
{
int errfd;
pid_t pid;
errfd = childlog(r->daemon.log);
if(errfd < 0)
return fail("open daemon log: %s", strerror(errno));
pid = fork();
if(pid < 0){
close(errfd);
return fail("fork daemon: %s", strerror(errno));
}
if(pid == 0){
setpgid(0, 0);
if(dup2(errfd, STDOUT_FILENO) < 0 ||
dup2(errfd, STDERR_FILENO) < 0)
_exit(126);
close(errfd);
execl(program, program, mapdir, (char*)0);
_exit(127);
}
close(errfd);
if((setpgid(pid, pid) < 0 && errno != EACCES) || getpgid(pid) != pid){
kill(pid, SIGKILL);
while(waitpid(pid, NULL, 0) < 0 && errno == EINTR)
;
return fail("isolate daemon process group: %s", strerror(errno));
}
r->daemon.pid = pid;
r->daemon.pgid = pid;
return 1;
}
static int static int
waitsocket(Run *r) waitsocket(Run *r)
{ {
struct stat st; struct stat st;
int status;
int64_t deadline; int64_t deadline;
deadline = nowms() + Starttimeout; deadline = nowms() + Starttimeout;
@@ -200,10 +57,8 @@ waitsocket(Run *r)
if(lstat(r->l.socket, &st) == 0 && S_ISSOCK(st.st_mode) && if(lstat(r->l.socket, &st) == 0 && S_ISSOCK(st.st_mode) &&
(st.st_mode & 0777) == 0600) (st.st_mode & 0777) == 0600)
return 1; return 1;
if(waitpid(r->daemon.pid, &status, WNOHANG) == r->daemon.pid){ if(!daemonalive(&r->daemon))
r->daemon.pid = -1;
return fail("daemon exited before creating its IPC socket"); return fail("daemon exited before creating its IPC socket");
}
pausems(20); pausems(20);
} }
return fail("timed out waiting for daemon IPC socket"); return fail("timed out waiting for daemon IPC socket");
@@ -213,90 +68,29 @@ static int
start(Run *r, char *daemon, char *mapdir, char *xvfb) start(Run *r, char *daemon, char *mapdir, char *xvfb)
{ {
memset(r, 0, sizeof *r); memset(r, 0, sizeof *r);
r->xvfb.pid = r->daemon.pid = -1; daemoninit(&r->xvfb, "Xvfb");
r->xvfb.pgid = r->daemon.pgid = -1; daemoninit(&r->daemon, "daemon");
if(!livesetup(&r->l, "xim")) if(!livesetup(&r->l, "xim") || !startxvfb(&r->l, &r->xvfb, xvfb))
return 0; return 0;
if(snprintf(r->xvfb.log, sizeof r->xvfb.log, "%s/xvfb.log", r->l.root) if(setenv("DISPLAY", r->l.display, 1) < 0 ||
>= (int)sizeof r->xvfb.log || setenv("XMODIFIERS", "@im=strans", 1) < 0)
snprintf(r->daemon.log, sizeof r->daemon.log, "%s/daemon.log", r->l.root)
>= (int)sizeof r->daemon.log)
return fail("temporary path is too long");
if(!spawnxvfb(r, xvfb))
return 0;
if(setenv("DISPLAY", r->display, 1) < 0 ||
setenv("XMODIFIERS", "@im=strans", 1) < 0 ||
setenv("XDG_RUNTIME_DIR", r->l.runtime, 1) < 0 ||
setenv("XDG_CONFIG_HOME", r->l.config, 1) < 0 ||
setenv("HOME", r->l.home, 1) < 0 ||
unsetenv("DBUS_SESSION_BUS_ADDRESS") < 0 ||
unsetenv("IBUS_ADDRESS") < 0)
return fail("set private environment: %s", strerror(errno)); return fail("set private environment: %s", strerror(errno));
if(!spawndaemon(r, daemon, mapdir)) if(!startdaemon(&r->l, &r->daemon, daemon, mapdir))
return 0; return 0;
return waitsocket(r); return waitsocket(r);
} }
static void
showlog(char *name)
{
char buf[4096];
ssize_t n;
int fd;
fd = open(name, O_RDONLY|O_CLOEXEC);
if(fd < 0)
return;
while((n = read(fd, buf, sizeof buf)) > 0)
fwrite(buf, 1, n, stderr);
close(fd);
}
static void
stopchild(Child *c)
{
int i, status;
pid_t n;
if(c->pgid > 0 && kill(-c->pgid, SIGTERM) < 0 && errno != ESRCH)
fail("terminate process group %ld: %s", (long)c->pgid,
strerror(errno));
if(c->pid > 0)
for(i = 0; i < Stoptimeout / 20; i++){
n = waitpid(c->pid, &status, WNOHANG);
if(n == c->pid || (n < 0 && errno == ECHILD)){
c->pid = -1;
break;
}
if(n < 0 && errno != EINTR)
break;
pausems(20);
}
if(c->pgid > 0)
kill(-c->pgid, SIGKILL);
if(c->pid > 0){
while(waitpid(c->pid, &status, 0) < 0 && errno == EINTR)
;
c->pid = -1;
}
c->pgid = -1;
}
static void static void
cleanup(Run *r, int noisy) cleanup(Run *r, int noisy)
{ {
stopchild(&r->daemon); stopdaemon(&r->daemon);
stopchild(&r->xvfb); stopdaemon(&r->xvfb);
if(noisy){ if(noisy){
fprintf(stderr, "%s: daemon log:\n", testname); showerrors(&r->daemon);
showlog(r->daemon.log); showerrors(&r->xvfb);
fprintf(stderr, "%s: Xvfb log:\n", testname);
showlog(r->xvfb.log);
} }
if(r->daemon.log[0] != '\0') closeerrors(&r->daemon);
unlink(r->daemon.log); closeerrors(&r->xvfb);
if(r->xvfb.log[0] != '\0')
unlink(r->xvfb.log);
liveclean(&r->l); liveclean(&r->l);
} }
@@ -765,9 +559,6 @@ main(int argc, char **argv)
fprintf(stderr, "usage: xim_live_test daemon mapdir [Xvfb]\n"); fprintf(stderr, "usage: xim_live_test daemon mapdir [Xvfb]\n");
return 2; return 2;
} }
memset(&run, 0, sizeof run);
run.xvfb.pid = run.daemon.pid = -1;
run.xvfb.pgid = run.daemon.pgid = -1;
xvfb = argc == 4 ? argv[3] : "Xvfb"; xvfb = argc == 4 ? argv[3] : "Xvfb";
ok = start(&run, argv[1], argv[2], xvfb); ok = start(&run, argv[1], argv[2], xvfb);
if(!ok){ if(!ok){
@@ -781,15 +572,15 @@ main(int argc, char **argv)
return 1; return 1;
} }
XSetErrorHandler(xerr); XSetErrorHandler(xerr);
dpy = XOpenDisplay(run.display); dpy = XOpenDisplay(run.l.display);
if(dpy == NULL){ if(dpy == NULL){
fail("open private display %s", run.display); fail("open private display %s", run.l.display);
cleanup(&run, 1); cleanup(&run, 1);
return 1; return 1;
} }
im = openim(dpy); im = openim(dpy);
if(im == NULL){ if(im == NULL){
fail("open strans XIM on private display %s", run.display); fail("open strans XIM on private display %s", run.l.display);
XCloseDisplay(dpy); XCloseDisplay(dpy);
cleanup(&run, 1); cleanup(&run, 1);
return 1; return 1;

76
trie.c
View File

@@ -3,9 +3,9 @@
#include "fn.h" #include "fn.h"
/* /*
* A byte trie over UTF-8 keys. Maps need prefix matches while composing; * A trie over the runes of a key. Maps need prefix matches while
* dictionaries need exact matches; both are small enough for one structure. * composing; dictionaries need exact matches; both are small enough for
* Node 0 is the root. * one structure. Node 0 is the root.
*/ */
static int static int
@@ -24,26 +24,37 @@ newnode(Trie *t)
return i; return i;
} }
/* The children of a node are kept in rune order, so both end early. */
static int static int
find(Trie *t, int ni, char c) find(Trie *t, int ni, Rune c)
{ {
int pi; int pi;
for(pi = t->nodes[ni].child; pi >= 0; pi = t->nodes[pi].sibling) for(pi = t->nodes[ni].child; pi >= 0; pi = t->nodes[pi].sibling){
if(t->nodes[pi].c == c) if(t->nodes[pi].c == c)
return pi; return pi;
if(t->nodes[pi].c > c)
break;
}
return -1; return -1;
} }
static int static int
add(Trie *t, int ni, char c) add(Trie *t, int ni, Rune c)
{ {
int pi; int next, pi, prev;
prev = -1;
for(next = t->nodes[ni].child; next >= 0 && t->nodes[next].c < c;
next = t->nodes[next].sibling)
prev = next;
pi = newnode(t); pi = newnode(t);
t->nodes[pi].c = c; t->nodes[pi].c = c;
t->nodes[pi].sibling = t->nodes[ni].child; t->nodes[pi].sibling = next;
t->nodes[ni].child = pi; if(prev < 0)
t->nodes[ni].child = pi;
else
t->nodes[prev].sibling = pi;
return pi; return pi;
} }
@@ -63,15 +74,26 @@ trienew(void)
void void
trieput(Trie *t, char *key, int klen, char *val, int vlen) trieput(Trie *t, char *key, int klen, char *val, int vlen)
{ {
int ni, ci, i; Str k;
int ci, i, ni, same;
if(!sinit(&k, key, klen))
return;
ni = 0; ni = 0;
for(i = 0; i < klen; i++){ same = 1;
ci = find(t, ni, key[i]); for(i = 0; i < k.n; i++){
if(same && i < t->last.n && t->last.r[i] == k.r[i]){
ni = t->path[i];
continue;
}
same = 0;
ci = find(t, ni, k.r[i]);
if(ci < 0) if(ci < 0)
ci = add(t, ni, key[i]); ci = add(t, ni, k.r[i]);
ni = ci; ni = ci;
t->path[i] = ni;
} }
t->last = k;
free(t->nodes[ni].val); free(t->nodes[ni].val);
t->nodes[ni].val = emalloc(vlen + 1); t->nodes[ni].val = emalloc(vlen + 1);
memmove(t->nodes[ni].val, val, vlen); memmove(t->nodes[ni].val, val, vlen);
@@ -153,24 +175,30 @@ trieclose(Trie *t)
free(t); free(t);
} }
/* A nil trie is an unloaded map: every key misses. */ /* The node key leads to, or -1. A nil trie is an unloaded map: every key misses. */
int
trienode(Trie *t, Str *key)
{
int i, ni;
if(t == nil)
return -1;
ni = 0;
for(i = 0; i < key->n && ni >= 0; i++)
ni = find(t, ni, key->r[i]);
return ni;
}
int int
trielookup(Trie *t, Str *key, char **val, int *vlen) trielookup(Trie *t, Str *key, char **val, int *vlen)
{ {
char buf[Maxutf]; int ni;
int i, klen, ni;
*val = nil; *val = nil;
*vlen = 0; *vlen = 0;
if(t == nil) ni = trienode(t, key);
if(ni < 0)
return TrieMiss; return TrieMiss;
klen = stoutf(key, buf, sizeof buf);
ni = 0;
for(i = 0; i < klen; i++){
ni = find(t, ni, buf[i]);
if(ni < 0)
return TrieMiss;
}
if(t->nodes[ni].val == nil) if(t->nodes[ni].val == nil)
return TriePrefix; return TriePrefix;
*val = t->nodes[ni].val; *val = t->nodes[ni].val;

85
win.c
View File

@@ -1,4 +1,5 @@
#include <xcb/xcb.h> #include <xcb/xcb.h>
#include <xcb/xcb_aux.h>
#include <xcb/randr.h> #include <xcb/randr.h>
#include "dat.h" #include "dat.h"
#include "fn.h" #include "fn.h"
@@ -12,17 +13,7 @@ static u32int *img;
static xcb_atom_t currentdesktop; static xcb_atom_t currentdesktop;
static xcb_atom_t workarea; static xcb_atom_t workarea;
static int hasrandr, imgh, imgw; static int hasrandr, imgh, imgw;
static int shown, ptrx, ptry; /* the popup, and where the pointer was as it came up */
static xcb_screen_t*
getscr(xcb_connection_t *c, int n)
{
xcb_screen_iterator_t i;
for(i = xcb_setup_roots_iterator(xcb_get_setup(c)); i.rem; xcb_screen_next(&i))
if(n-- == 0)
return i.data;
return nil;
}
static xcb_visualtype_t* static xcb_visualtype_t*
getvisual(xcb_screen_t *s) getvisual(xcb_screen_t *s)
@@ -201,7 +192,7 @@ static int
wininit(void) wininit(void)
{ {
int n; int n;
u32int mask, vals[4]; u32int mask, vals[5];
xcb_atom_t type, tooltip; xcb_atom_t type, tooltip;
xcb_randr_query_version_cookie_t rc; xcb_randr_query_version_cookie_t rc;
xcb_randr_query_version_reply_t *rr; xcb_randr_query_version_reply_t *rr;
@@ -217,7 +208,7 @@ wininit(void)
wincleanup(); wincleanup();
return 0; return 0;
} }
scr = getscr(conn, n); scr = xcb_aux_get_screen(conn, n);
if(scr == nil || !validformat(conn, scr)){ if(scr == nil || !validformat(conn, scr)){
fprint(2, "strans: popup disabled: unsupported X root format\n"); fprint(2, "strans: popup disabled: unsupported X root format\n");
wincleanup(); wincleanup();
@@ -234,14 +225,17 @@ wininit(void)
currentdesktop = getatom("_NET_CURRENT_DESKTOP"); currentdesktop = getatom("_NET_CURRENT_DESKTOP");
workarea = getatom("_NET_WORKAREA"); workarea = getatom("_NET_WORKAREA");
win = xcb_generate_id(conn); win = xcb_generate_id(conn);
/* Clicks on the popup go nowhere, not to the root window under it. */
mask = XCB_CW_BACK_PIXEL | XCB_CW_BORDER_PIXEL | mask = XCB_CW_BACK_PIXEL | XCB_CW_BORDER_PIXEL |
XCB_CW_OVERRIDE_REDIRECT | XCB_CW_SAVE_UNDER; XCB_CW_OVERRIDE_REDIRECT | XCB_CW_SAVE_UNDER |
XCB_CW_DONT_PROPAGATE;
vals[0] = Colbg; vals[0] = Colbg;
vals[1] = 0; vals[1] = Colsep;
vals[2] = 1; vals[2] = 1;
vals[3] = 1; vals[3] = 1;
vals[4] = XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE;
xcb_create_window(conn, XCB_COPY_FROM_PARENT, win, scr->root, xcb_create_window(conn, XCB_COPY_FROM_PARENT, win, scr->root,
0, 0, 1, 1, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, 0, 0, 1, 1, PopupBorder, XCB_WINDOW_CLASS_INPUT_OUTPUT,
scr->root_visual, mask, vals); scr->root_visual, mask, vals);
type = getatom("_NET_WM_WINDOW_TYPE"); type = getatom("_NET_WM_WINDOW_TYPE");
tooltip = getatom("_NET_WM_WINDOW_TYPE_TOOLTIP"); tooltip = getatom("_NET_WM_WINDOW_TYPE_TOOLTIP");
@@ -250,11 +244,7 @@ wininit(void)
win, type, XCB_ATOM_ATOM, 32, 1, &tooltip); win, type, XCB_ATOM_ATOM, 32, 1, &tooltip);
gc = xcb_generate_id(conn); gc = xcb_generate_id(conn);
xcb_create_gc(conn, gc, win, 0, nil); xcb_create_gc(conn, gc, win, 0, nil);
if(!textinit()){ textinit();
fprint(2, "strans: popup disabled: no usable fonts\n");
wincleanup();
return 0;
}
return 1; return 1;
} }
@@ -306,43 +296,54 @@ putimage(int w, int h)
xcb_clear_area(conn, 0, win, 0, 0, w, h); xcb_clear_area(conn, 0, win, 0, 0, w, h);
} }
static int static void
winhide(void) winhide(void)
{ {
xcb_unmap_window(conn, win); xcb_unmap_window(conn, win);
return xcb_flush(conn) > 0; xcb_flush(conn);
shown = 0;
} }
/* Draws dc at the caret, or by the pointer when the caret is unknown. */ /*
static int * Draws dc at the caret, or by the pointer when the caret is unknown:
* where the pointer was as the popup came up, so that it does not
* follow the mouse from key to key.
*/
static void
winshow(Drawcmd *dc) winshow(Drawcmd *dc)
{ {
Area area; Area area;
Popup p; Popup p;
int ax, ay, px, py, x, y; int ax, ay, x, y;
u32int vals[5]; u32int vals[5];
xcb_query_pointer_reply_t *ptr; xcb_query_pointer_reply_t *ptr;
xcb_query_pointer_cookie_t cookie; xcb_query_pointer_cookie_t cookie;
if(dc->nkouho == 0 && dc->pre.n == 0) if(dc->nkouho == 0 && dc->pre.n == 0){
return winhide(); winhide();
px = py = 0; return;
if(!dc->caret.valid){ }
if(!dc->caret.valid && !shown){
cookie = xcb_query_pointer(conn, scr->root); cookie = xcb_query_pointer(conn, scr->root);
ptr = xcb_query_pointer_reply(conn, cookie, nil); ptr = xcb_query_pointer_reply(conn, cookie, nil);
if(ptr == nil) if(ptr == nil)
return 0; return;
px = ptr->root_x; ptrx = ptr->root_x;
py = ptr->root_y; ptry = ptr->root_y;
free(ptr); free(ptr);
} }
ax = dc->caret.valid ? dc->caret.x : px; ax = dc->caret.valid ? dc->caret.x : ptrx;
ay = dc->caret.valid ? dc->caret.y : py; ay = dc->caret.valid ? dc->caret.y : ptry;
popupwork(ax, ay, &area); popupwork(ax, ay, &area);
popuplayout(dc, area.w, area.h, &p); popuplayout(dc, area.w, area.h, &p);
if(p.w <= 0 || p.h <= 0 || !resizebacking(p.w, p.h)) if(p.w <= 0 || p.h <= 0){
return 0; winhide();
popupposition(&dc->caret, px, py, &area, p.w, p.h, &x, &y); return;
}
if(!resizebacking(p.w, p.h))
return;
popupposition(&dc->caret, ptrx, ptry, &area, p.w + 2*PopupBorder,
p.h + 2*PopupBorder, &x, &y);
/* Mapping does not restack: raise above windows opened since. */ /* Mapping does not restack: raise above windows opened since. */
vals[0] = x; vals[0] = x;
vals[1] = y; vals[1] = y;
@@ -356,7 +357,8 @@ winshow(Drawcmd *dc)
popupdraw(img, dc, &p); popupdraw(img, dc, &p);
putimage(p.w, p.h); putimage(p.w, p.h);
xcb_map_window(conn, win); xcb_map_window(conn, win);
return xcb_flush(conn) > 0; xcb_flush(conn);
shown = 1;
} }
void void
@@ -367,8 +369,7 @@ drawthread(void*)
threadsetname("draw"); threadsetname("draw");
if(!wininit()) if(!wininit())
return; return;
while(chanrecv(drawc, &dc) > 0) while(chanrecv(drawc, &dc) > 0 && !xcb_connection_has_error(conn))
if(!winshow(&dc)) winshow(&dc);
break;
wincleanup(); wincleanup();
} }

View File

@@ -5,18 +5,17 @@
#include <locale.h> #include <locale.h>
#include <poll.h> #include <poll.h>
#include <xcb/xcb.h> #include <xcb/xcb.h>
#include <xcb/xcb_aux.h>
#include <xcb/xkb.h> #include <xcb/xkb.h>
#include <xkbcommon/xkbcommon.h> #include <xkbcommon/xkbcommon.h>
#include <xkbcommon/xkbcommon-compose.h> #include <xkbcommon/xkbcommon-names.h>
#include <xkbcommon/xkbcommon-x11.h> #include <xkbcommon/xkbcommon-x11.h>
#include <xcb-imdkit/imdkit.h> #include <xcb-imdkit/imdkit.h>
#include <xcb-imdkit/encoding.h> #include <xcb-imdkit/encoding.h>
u32int keymaplookup(struct xkb_state*, uint8_t, u16int);
/* /*
* One XIM input context. Only PreeditCallbacks clients draw the preedit * One XIM input context. Only PreeditCallbacks clients draw the preedit
* themselves (cap); the popup shows it for the others. engaged means the * themselves (clientpre); the popup shows it for the others. engaged means the
* engine has seen a real key from this context and owes it a release. * engine has seen a real key from this context and owes it a release.
*/ */
typedef struct Ic Ic; typedef struct Ic Ic;
@@ -26,21 +25,15 @@ struct Ic
xcb_im_input_context_t *xic; xcb_im_input_context_t *xic;
xcb_im_client_t *client; xcb_im_client_t *client;
int engaged; int engaged;
int cap; int clientpre;
int prestarted; int prestarted;
int nprerune; int nprerune;
Caret caret; Caret caret;
}; };
enum
{
Ownerpoll = 200, /* ms between owner checks while a preedit shows */
};
static xcb_connection_t *conn; static xcb_connection_t *conn;
static xcb_im_t *xim; static xcb_im_t *xim;
static struct xkb_state *kstate; static struct xkb_state *kstate;
static struct xkb_compose_state *compose;
static uint8_t xkbevent; static uint8_t xkbevent;
static xcb_window_t rootwin; static xcb_window_t rootwin;
static Ic *ics; static Ic *ics;
@@ -52,6 +45,9 @@ static u32int styles[] = {
XCB_IM_PreeditPosition | XCB_IM_StatusNothing, XCB_IM_PreeditPosition | XCB_IM_StatusNothing,
XCB_IM_PreeditCallbacks | XCB_IM_StatusNothing, XCB_IM_PreeditCallbacks | XCB_IM_StatusNothing,
XCB_IM_PreeditNothing | XCB_IM_StatusNothing, XCB_IM_PreeditNothing | XCB_IM_StatusNothing,
XCB_IM_PreeditPosition | XCB_IM_StatusNone,
XCB_IM_PreeditCallbacks | XCB_IM_StatusNone,
XCB_IM_PreeditNothing | XCB_IM_StatusNone,
}; };
static void static void
@@ -99,26 +95,6 @@ out:
return ok; return ok;
} }
/* Dead keys and Compose: Xlib leaves them to the input method server. */
static void
cinit(void)
{
struct xkb_context *context;
struct xkb_compose_table *table;
char *locale;
locale = setlocale(LC_CTYPE, "");
context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
if(context == nil)
return;
table = xkb_compose_table_new_from_locale(context,
locale != nil ? locale : "C", XKB_COMPOSE_COMPILE_NO_FLAGS);
if(table != nil)
compose = xkb_compose_state_new(table, XKB_COMPOSE_STATE_NO_FLAGS);
xkb_compose_table_unref(table);
xkb_context_unref(context);
}
static void static void
kwatch(void) kwatch(void)
{ {
@@ -135,18 +111,6 @@ kwatch(void)
events, parts, parts, nil); events, parts, parts, nil);
} }
static xcb_screen_t*
getscreen(int scr)
{
xcb_screen_iterator_t iter;
iter = xcb_setup_roots_iterator(xcb_get_setup(conn));
for(; iter.rem; scr--, xcb_screen_next(&iter))
if(scr == 0)
return iter.data;
return nil;
}
static int static int
translate(xcb_window_t win, int x, int y, Caret *caret) translate(xcb_window_t win, int x, int y, Caret *caret)
{ {
@@ -210,8 +174,13 @@ place(Ic *state)
XCB_XIM_XNSpotLocation_MASK){ XCB_XIM_XNSpotLocation_MASK){
attr = xcb_im_input_context_get_preedit_attr(state->xic); attr = xcb_im_input_context_get_preedit_attr(state->xic);
if(translate(focuswin, attr->spot_location.x, if(translate(focuswin, attr->spot_location.x,
attr->spot_location.y, &state->caret)) attr->spot_location.y, &state->caret)){
/* A spot is a baseline: call the line one row above it,
* so that a popup flipped above it clears the text. */
state->caret.y -= Fontsz;
state->caret.h = Fontsz;
return; return;
}
} }
if(placebottom(state, focuswin)) if(placebottom(state, focuswin))
return; return;
@@ -264,7 +233,7 @@ updatepreedit(Ic *state, Str *pre)
size_t nwire; size_t nwire;
int i, nbyte; int i, nbyte;
if(!(state->cap & Cclientpreedit)) if(!state->clientpre)
return; return;
if(pre->n == 0){ if(pre->n == 0){
clearpreedit(state); clearpreedit(state);
@@ -300,7 +269,7 @@ sendrequest(Ic *state, int op, u32int key, u32int mod, Keyres *res)
memset(&kr, 0, sizeof kr); memset(&kr, 0, sizeof kr);
kr.owner = state; kr.owner = state;
kr.cap = state->cap; kr.clientpre = state->clientpre;
kr.op = op; kr.op = op;
kr.ks = key; kr.ks = key;
kr.mod = ipcmod(mod); kr.mod = ipcmod(mod);
@@ -379,47 +348,58 @@ resetic(Ic *state, xcb_im_reset_ic_reply_fr_t *reply)
reply->byte_length_of_committed_string = nwire; reply->byte_length_of_committed_string = nwire;
} }
/* The keysym for an X core key event, with its modifier and group state. */
static u32int
keymaplookup(struct xkb_state *state, uchar keycode, u16int corestate)
{
static char *modname[] = {
XKB_MOD_NAME_SHIFT, XKB_MOD_NAME_CAPS, XKB_MOD_NAME_CTRL,
XKB_MOD_NAME_MOD1, XKB_MOD_NAME_MOD2, XKB_MOD_NAME_MOD3,
XKB_MOD_NAME_MOD4, XKB_MOD_NAME_MOD5,
};
struct xkb_keymap *keymap;
xkb_mod_index_t index;
xkb_mod_mask_t mods;
int i;
keymap = xkb_state_get_keymap(state);
mods = 0;
for(i = 0; i < nelem(modname); i++){
if(!(corestate & (1 << i)))
continue;
index = xkb_keymap_mod_get_index(keymap, modname[i]);
if(index != XKB_MOD_INVALID)
mods |= (xkb_mod_mask_t)1 << index;
}
xkb_state_update_mask(state, mods, 0, 0, 0, 0, (corestate >> 13) & 3);
return xkb_state_key_get_one_sym(state, keycode);
}
static void static void
kpress(Ic *state, xcb_key_press_event_t *ev) kpress(Ic *state, xcb_key_press_event_t *ev)
{ {
Keyres res; Keyres res;
char buf[Maxutf]; char buf[Maxutf], text[Maxutf];
u32int key, sym; u32int key, sym;
int composed, n, wasvalid; int n;
sym = keymaplookup(kstate, ev->detail, ev->state); sym = keymaplookup(kstate, ev->detail, ev->state);
composed = 0; if(composekey(sym, text, sizeof text))
if(compose != nil && return;
xkb_compose_state_feed(compose, sym) == XKB_COMPOSE_FEED_ACCEPTED)
switch(xkb_compose_state_get_status(compose)){
case XKB_COMPOSE_COMPOSING:
case XKB_COMPOSE_CANCELLED:
return;
case XKB_COMPOSE_COMPOSED:
sym = xkb_compose_state_get_one_sym(compose);
composed = 1;
break;
default:
break;
}
key = ipckeysym(sym, xkb_keysym_to_utf32(sym)); key = ipckeysym(sym, xkb_keysym_to_utf32(sym));
if(keymeaningful(key)){ if(keymeaningful(key))
wasvalid = state->caret.valid;
place(state); place(state);
if(state->engaged && wasvalid && !state->caret.valid) if(text[0] != '\0')
sendrequest(state, Keycaret, 0, 0, &res); /* Composed text follows whatever was pending. */
} sendrequest(state, Keyreset, 0, 0, &res);
keypress(state, key, ev->state, &res); else
keypress(state, key, ev->state, &res);
n = stoutf(&res.commit, buf, sizeof buf); n = stoutf(&res.commit, buf, sizeof buf);
commit(state, buf, n); commit(state, buf, n);
updatepreedit(state, &res.preedit); updatepreedit(state, &res.preedit);
if(res.eaten) if(text[0] != '\0')
; commit(state, text, strlen(text));
else if(composed){ else if(!res.eaten)
/* The event holds the last key of the sequence, not its result. */
n = xkb_keysym_to_utf8(sym, buf, sizeof buf);
commit(state, buf, max(n - 1, 0));
}else
xcb_im_forward_event(xim, state->xic, ev); xcb_im_forward_event(xim, state->xic, ev);
xcb_flush(conn); xcb_flush(conn);
} }
@@ -456,9 +436,8 @@ iccreate(xcb_im_client_t *client, xcb_im_input_context_t *ic)
state = emalloc(sizeof(Ic)); state = emalloc(sizeof(Ic));
state->xic = ic; state->xic = ic;
state->client = client; state->client = client;
if(xcb_im_input_context_get_input_style(ic) == if(xcb_im_input_context_get_input_style(ic) & XCB_IM_PreeditCallbacks)
(XCB_IM_PreeditCallbacks | XCB_IM_StatusNothing)) state->clientpre = 1;
state->cap = Cclientpreedit;
state->next = ics; state->next = ics;
ics = state; ics = state;
xcb_im_input_context_set_data(ic, state, icfree); xcb_im_input_context_set_data(ic, state, icfree);
@@ -531,7 +510,7 @@ ximinit(void)
ximlog("cannot connect to X server"); ximlog("cannot connect to X server");
return -1; return -1;
} }
screen = getscreen(scr); screen = xcb_aux_get_screen(conn, scr);
if(screen == nil){ if(screen == nil){
ximlog("cannot find X screen"); ximlog("cannot find X screen");
return -1; return -1;
@@ -541,7 +520,6 @@ ximinit(void)
ximlog("cannot read keyboard mapping"); ximlog("cannot read keyboard mapping");
return -1; return -1;
} }
cinit();
kwatch(); kwatch();
win = xcb_generate_id(conn); win = xcb_generate_id(conn);
xcb_create_window(conn, XCB_COPY_FROM_PARENT, win, screen->root, xcb_create_window(conn, XCB_COPY_FROM_PARENT, win, screen->root,

View File

@@ -1,37 +0,0 @@
#include <stdint.h>
#include <xcb/xcb.h>
#include <xkbcommon/xkbcommon.h>
#include <xkbcommon/xkbcommon-names.h>
/* The keysym for an X core key event, with its modifier and group state. */
uint32_t
keymaplookup(struct xkb_state *state, uint8_t keycode, uint16_t corestate)
{
static const char *modname[] = {
XKB_MOD_NAME_SHIFT,
XKB_MOD_NAME_CAPS,
XKB_MOD_NAME_CTRL,
XKB_MOD_NAME_MOD1,
XKB_MOD_NAME_MOD2,
XKB_MOD_NAME_MOD3,
XKB_MOD_NAME_MOD4,
XKB_MOD_NAME_MOD5,
};
struct xkb_keymap *keymap;
xkb_mod_index_t index;
xkb_mod_mask_t mods;
int i;
keymap = xkb_state_get_keymap(state);
mods = 0;
for(i = 0; i < (int)(sizeof modname / sizeof modname[0]); i++){
if(!(corestate & (1U << i)))
continue;
index = xkb_keymap_mod_get_index(keymap, modname[i]);
if(index != XKB_MOD_INVALID)
mods |= (xkb_mod_mask_t)1 << index;
}
xkb_state_update_mask(state, mods, 0, 0, 0, 0,
(corestate >> 13) & 3);
return xkb_state_key_get_one_sym(state, keycode);
}