Compare commits

...

276 Commits

Author SHA1 Message Date
0f77e0e9ac engine: the chosen candidate is what the client shows
The preedit sent back to the owner was always impre(), the reading, so
かく stayed underlined in the application while the popup showed 書く and
Enter inserted 書く.  Nothing was hidden -- the popup had it -- but the
text did not stand where it was going to land, and mozc, fcitx5 and kime
all put the candidate inline.

Only the reply changes.  impre() still answers the reading everywhere it
is the reading that is wanted: commitim, the okurigana mark, the seed
Ctrl+H takes, and snapshot, so a popup that draws the preedit itself
keeps the reading above the list where it says something the highlighted
row does not.  Consulting im.sel here is safe where consulting it inside
impre() would not be: transstr memsets a local Im, whose sel is 0 and not
-1, and this reply only ever reads the engine's own.

Nothing in the suite pinned the old answer, so the test is new.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:25:55 +09:00
761428dbc4 engine: a Hanja word shows once, as every other candidate does
hanjaquery was the only query that assigned dictprefix's output straight
into im.kouho; emojiquery, dictqjp and katakouho all go through
addkouho, which is the only place a duplicate is dropped.  Two readings
under one prefix can carry the same word, and 198 of the 61,479 one- and
two-syllable prefixes in hanja.dict do: 게 listed 偈句 at rows 8 and 9,
개근 listed 皆勤狀 twice in five.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:20:18 +09:00
1bc0ba0491 build: the parent make knows what the daemon is built from
../strans listed no prerequisites, so once the file existed make held it
up to date whatever changed under it: touch strans.c, run
make -C tests check-live, and it planned no compile at all and smoked a
stale daemon.  The sibling rule for the GTK module answered it by naming
three of its sources, which is the parent Makefile's list copied and
already short of gtk/Makefile.  Neither list is needed -- ask the parent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:18:43 +09:00
74607afef8 test: the take-back byte is read back off the wire
ipc_response_fragmented_and_truncated already packed del = 2 and sent
the frame a byte at a time; it checked the commit and the preedit and
never looked at the field the v2 wire was widened for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:17:27 +09:00
591029237e test: a fixture that cannot fail needs no guard
ximbegin took a struct ct* only to USED() it and always returned 1, so
its seven `if(!ximbegin(...)) goto cleanup;` call sites tested nothing
and three of the cleanup labels they jumped to were unreachable.
ibusbegin malloc'd one byte twice so that its two fake DBusConnections
would differ by address, then CT_CHECKed the mallocs; two bytes in the
fixture are two addresses, and nothing has to be freed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:16:02 +09:00
bdf05e3c4a str, bench: three checks that guard nothing
sinit sets s->n = 0 as its first statement and no failure path restores
it, so stail's sclear after a failed sinit could never change anything;
say so in sinit's comment instead, where the contract belongs.  bench
compared a uintmax_t against UINT64_MAX and a size_t against UINT64_MAX,
both constant on any host this builds for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:14:16 +09:00
4802b6593e wl: counting bytes needs no buffer to put them in
backbytes copied the runes into a Str and encoded them into a 257-byte
stack buffer only to throw the bytes away and keep the length.  runelen
answers over the same runes with neither.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:13:13 +09:00
ba0a08abe2 docs: Backspace gives the reach back
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:05:30 +09:00
08d0e0fec8 engine: an okurigana mark does not outlive its kana
Shift on a romaji letter marks where the okurigana begins, and Backspace
shortened the reading through im.l->back without clearing the mark.
dictqjp only applies a mark that still falls inside the reading, so the
stale index lay dormant while the reading was short and fired again the
moment it grew back past it: kaKu, Backspace, then nzi offered
噛んじ 兼んじ 漢字 幹事 感じ, where kanzi typed plainly offers 漢字 first.
A Backspace edits the reading, so the split marked in it is gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:03:15 +09:00
d3d46c8cb1 engine: Backspace gives the reach back before what was typed
searchkey popped search.raw and then search.seed but never touched
search.back, so once the typed syllable was gone the query was nothing
but the client's own text: an empty preedit above a full candidate list
drawn from syllables the user had not selected, and Enter rewrote them.
With 상 written and 태 typed, Ctrl+H offered 狀態; one Backspace left
the query 상 and 128 candidates, and Enter replaced the 상 with 上 and
dropped the 태 altogether.

Undo now runs backwards through what happened -- the keys typed since
Ctrl+H, then the reach Ctrl+H made, then the syllable that seeded it --
so the same Backspace narrows 상태 to 태 and offers 太, which is also
the only way there was ever going to be to convert the syllable alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:02:52 +09:00
b08f3bb7a2 engine: nothing pending is no reading, and reaches into nothing
startsearch let reachback walk the client's text whenever Ctrl+H found
im.sel < 0, without asking whether anything was pending, so a Ctrl+H
pressed to begin a reading took the syllables already written instead.
With the cursor after 입니다 the query became 다, the preedit showed
nothing at all, and Space committed 多 over the 다 the user had written;
typing the reading the key was pressed for gave 다한, so 입니다漢 came
out 입니多恨.  A reading is what is pending, and there is none.

The seeded reach is untouched: 한 committed with 자 pending still
converts 한자 as a word and takes the 한 back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:02:27 +09:00
cf0eba6642 docs: the reading reaches back, and which frontends can
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 14:31:44 +09:00
5d9a2a614e wl: the text before the cursor, and the runes to take back
The surrounding_text event was taken and thrown away.  It carries the
text around the cursor with a byte offset into it, and belongs to the
activation like the content type, so it is pending until done and
starts empty at every activate.  delete_surrounding_text counts bytes
where the engine counts runes, and the frontend holds the text those
runes are in, so it measures them itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 14:31:44 +09:00
921e632eae ibus: the text before the cursor, and the runes to take back
RequireSurroundingText asks the client to send its text; it arrives
through SetSurroundingText as an IBusText and a cursor counted in
runes, and DeleteSurroundingText asks for runes back before the commit
that replaces them.  The official libibus client now proves the whole
turn: 한자 typed, the Hanja key, Enter, and 漢字 arrives with the 한
taken away.

A client that has set EffectivePostProcessKeyEvent is never asked for
its text.  It reads the key's commits back after ProcessKeyEvent
returns, and a DeleteSurroundingText signal is not one of the things
that reply can carry, so the deletion would land after the text it was
meant to make room for and eat the wrong runes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 14:31:44 +09:00
910bf51347 ipc, srv, gtk: the client's text goes over and a take-back comes back
The engine can reach a Hanja reading back into the text the client
already holds, but only if the frontend hands that text over and can
take some of it away again.  GTK 3 has both: retrieve-surrounding
brings the text around the cursor and delete-surrounding removes runes
before it, and a widget that answers neither leaves the text empty, so
nothing is ever reached into or taken from it.

The wire grows a control frame for the text, sent like the caret only
when it changes, and one byte in every response for the runes to take
back.  That byte moves the length fields along, so the version goes to
2: an old daemon and a new module, either way round, fail the handshake
and the module falls through to GtkIMContextSimple rather than misread
a frame.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 14:31:31 +09:00
f7277f54a3 engine: Ctrl+H reaches back into the text the client already has
Korean commits a syllable as the next one begins, so by the time the
한자 key is pressed only the last syllable is still ours: typing 한자 and
then Ctrl+H asked about 자 alone and answered 子, leaving 한子 in the
document -- the mixed Hangul and Hanja that was refused as dictionary
data, made by the interaction instead.  To get 漢字 the key had to be
pressed before typing, which no other Korean input method asks for.

A request now carries the client's own text just before the cursor, and
the reading reaches back through it as far as the dictionary still knows
the whole of it: 한 joins 자 and 대한민 joins 국.  Nothing but the
dictionary says where to stop, because a reading is syllables, so a key
holding a space or an already converted Hanja leads nowhere and the
reach ends there.  A pick answers with the count of runes to take back
first; Escape gives back only what was pending, and a query with no
candidate types only the part the client lacks.

A frontend that sends no surrounding text reaches back by nothing and
behaves exactly as before, which is what XIM will keep doing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 14:17:08 +09:00
997e4c8d93 srv, ibus: a note the daemon lives through keeps its endpoints
srvnote and addrnote unlinked on any note at all, and plan9port marks
SIGPIPE Ignore: notify.c:59 lists it, and signotify runs the handler
chain first and only then finds the Ignore flag and returns.  So one
broken pipe took the IPC socket and the IBus address file away from a
daemon that went on running -- measured on a private runtime dir, a
single kill -PIPE left the process in state Ssl with both files gone,
so every client that focused a widget afterwards silently had no input
method and only a restart brought it back.

libxcb writes with writev, so the note is a broken X connection away;
today xim.c's die() masks it by taking the daemon down on the same
event, which is exactly why the two must not depend on each other.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 13:59:13 +09:00
dab80b3a77 engine: a key that converts nothing is the application's
Space and Tab convert a Japanese reading, and a reading with no
candidate to step through was committed and the key eaten.  Katakana
mode converts nothing -- katakouho adds the Katakana form only when it
differs from the reading, and in Katakana mode it never does -- so
every Space between two Katakana words was swallowed and had to be
typed twice: カク<Space> committed カク and left no space, and the
second Space passed through only because nothing was pending any more.

Committing and passing the key on gives the space back.  Hiragana is
unchanged wherever the reading has kana, since the Katakana form is
always one candidate there; it changes only for a reading that made no
kana at all, かx<Space>, which now commits and spaces too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 13:56:19 +09:00
d9e27e8b01 engine: a Hanja reading answers with the words it begins
hanjaquery matched the reading exactly, so a word was reachable only
once every syllable of it had been typed, and the popup went blank on
the way there.  Measured over map/hanja.dict's 187,304 readings:
95,024 proper prefixes of a word answer with nothing today, and 66,731
of those are the keystroke just before the word completes -- 대한민 is
one, so 대한민국 looks absent until the last key lands.

dictprefix walks the entry at a node before its children, so the
reading's own conversions keep their place and the words follow: 34,441
readings gain candidates and none of the 187,304 has its existing order
changed.  A jamo reading is untouched, since the two keyspaces do not
meet and ㅁ has no children.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 13:54:42 +09:00
5abd885e11 data(symbol): a prefix owns its slots only through its own row
map/symbol.src:12 says "Bare 1-9 choose from a prefix search; keep digit
aliases in matching slots", map/README:55 repeats it, and both
tests/engine_test.c:1001 and tests/mkemoji_test.py were written to it.
None of it happened.  dictprefix ends in below(), which returns a node's
own words first and then its children in rune order (dict.c:29-39), and
under `^` the punctuation sorts ahead of the digits:

	^ gave  ⁽ ⁾ ⁺ ⁻ ⁰ ¹ ² ³ ⁴ ⁵ ...
	_ gave  ₍ ₎ ₊ ₋ ₀ ₁ ₂ ₃ ₄ ₅ ...
	< gave  ← ♥ 🫰 🫶 ≤ ≠

so the file's own aliases picked the wrong character every time:

	          before   after
	^ then 1  ⁽        ¹
	^ then 2  ⁾        ²
	_ then 1  ₍        ₁
	< then 3  🫰        ♥

mkemoji has no bare `^` row to emit, because no source row claims `^` as
an alias -- the prefix exists in the trie only as the parent of `^1`..`^9`
and `^(`..`^n`, and a parent has no words of its own.  Giving the nine
superscripts `^` as a second alias, the nine subscripts `_`, and ←≤♥≠ `<`,
makes build() group them in source order and emit three rows:

	<	← ≤ ♥ ≠
	^	¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹
	_	₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉

which is what engine_test.c:1001's fixture has said all along, and the
first thing below() now returns.  Nothing is lost: `^0`, `^(`, `^i` and
their kind still answer their own key, `0` is not a selection key so it
still extends the query, and addkouho drops the duplicate when a child
repeats what the parent already offered.  Three rows on 18953.

tests/mkemoji_test.py was asking the wrong question.  It checked that the
first nine `^` keys in the file are `^1`..`^9` -- true before and after,
and decided nothing, because the file's order is not the trie's.  It now
checks the row the engine actually reads, and fails without this change
with a bare KeyError on `^`.

What this does not cover: `+1` and `-1`.  They are not slot aliases, they
are words -- plus one, minus one -- and  and  are the right first
answers to `+` and `-`.  👍 is slot 3 of `+` and reachable there.
2026-08-18 09:26:46 +09:00
a351f50308 engine: じ is keyed j, the letter SKK types it with
okuriletter() puts じ in the z row, so dictqokuri can never build a key
ending in j.  map/kanji.dict has 17 of them, and 8 have no z twin at all:

	before                     after
	shinjiru  死んじる           信じる 死んじる
	tojiru    (nothing)        綴じる 閉じる
	tsuujiru  (nothing)        通じる
	shoujiru  (nothing)        生じる
	toujiru   問うじる           投じる 問うじる
	koujiru   乞うじる 請うじる    高じる 乞うじる 請うじる
	gozonji   御存知             御存知 御存じ
	majiru    混じる 先じる       交じる 混じる

信じる, 閉じる, 通じる, 生じる, 投じる, 交じる: the dictionary holds every
one and the engine could reach none.  What it offered instead was the
next split down -- しんじる falls back to しn, so 死んじる is what the
popup shows for 信じる, and it is the only candidate.

The letter is not ours to choose; it is the one the SKK dictionary was
keyed by, which is the one the user typed.  Real SKK carries both spellings
where both are typed, and here that is 9 of the 17 -- えんj/えんz, かんj/かんz
and their kin, identical values on both.  Those 8 z rows go unreachable and
lose nothing, because their j twin says the same word.  ま is the one pair
that differs, まj=交 混 against まz=混 先, and both stay reachable: じ takes
the j road and ぜ/ず keep the z one.

	mazeru    混ぜる 先ぜる      unchanged
	kanarazu  必ず ...           unchanged
	mizu      水 見ず            unchanged
	dekizu    出来ず             unchanged

The z row keeps ざずぜぞ, so nothing that is not じ moves.

What this does not cover, deliberately: ち.  kanji.dict has 7 keys ending
in c, from users who typed "chi", and okuriletter puts ち in the t row.
Giving c its own row would have to take ち out of t, and t is where the
larger entry lives -- おt is 落 折 負 追 against おc's 落 alone -- so the c
rows stay unreachable and lose nothing.  づ needed no such choice: it is
in the d row, もとd is 基, and もとz is its duplicate.
2026-08-18 09:20:15 +09:00
c9c01fdeb9 engine: a page key on an untouched list turns the page
movekouho() answers every one of Up, Down, PageUp and PageDown the same
way when nothing is chosen yet -- `if(im.sel < 0) im.sel = 0;` -- and
delta is thrown away.  For Up and Down that is right: the list is drawn
with no cursor, so the first move takes the first candidate.  For
PageDown it is not, because there is no page state to move; pagefirst()
derives the page from im.sel alone, so selecting candidate 1 leaves the
page exactly where it was.

dictqjp() leaves sel at -1 on every keystroke, so a Japanese list is
always untouched when it first appears.  Type かく, look at its thirty
candidates, and press PageDown for the next nine:

	                     before          after
	かく, PageDown        sel 0, page 0   sel 9, page 9
	かく, PageDown twice  sel 9, page 9   sel 18, page 18
	かく, PageUp          sel 0, page 0   unchanged
	かく, Down            sel 0, page 0   unchanged

Nothing on screen answers the first press but the highlight appearing on
row 1, and the page turns only on the second.  fcitx5 pages on the first,
because its candidate list carries a page of its own and
toPageable()->next() does not touch the cursor
(ref-fcitx5-hangul/src/engine.cpp:326-336).  strans has one number where
fcitx5 has two, which is the right trade for nine rows and 128
candidates -- but then the number has to move by a page when a page key
asks for one.

`delta == Maxdisp` rather than `delta > 0` is deliberate: Down must still
land on candidate 1, and writing it the loose way fails both this table
and engine/japanese-candidates at engine_test.c:1198, where the language
switch takes 漢字 and would take 幹事 instead.  PageUp from an untouched
list still clamps to 0; there is no page above the first.

searchkey shares movekouho, and is unaffected: emojiquery and hanjaquery
both end in selectfirst(), so a search list is never untouched while it
has candidates.
2026-08-18 09:14:39 +09:00
bfcd9f6786 engine: a modifier makes a special key the application's
Backspace, Enter, Tab, Escape and the arrow and page keys are matched by
keysym alone -- strans.c:852, 875 and 882 name them, and searchkey names
the same set at strans.c:746-786.  Every other key above Kspec falls
through to the catch-all at strans.c:898, `ks >= Kspec || chord(mod)`,
and goes to the application.  So the special keys strans knows by name
are the ones it takes under a modifier, and the ones it does not know are
the ones it hands over.  That is backwards: a named special key under a
modifier is exactly the one the application has a binding for.

In Korean a syllable is pending for nearly all the time anyone is typing,
ko.c holding one and no more, and the guards at strans.c:876 and 883
return 0 only when nothing is pending -- so the key is eaten precisely
when it is wanted.  Type 안녕하세요 and reach for Ctrl+Backspace to take
the word back: 요 loses ㅛ, then ㅇ, and the word itself goes on the third
press.  Ctrl+Enter in a chat box, Ctrl+Tab in a browser and Ctrl+PageDown
in either are the same key eaten by the same lines.

	                            before               after
	Korean 가, Ctrl+Backspace    eaten, pre ㄱ         passed, commit 가
	Korean 가, Alt+Backspace     eaten, pre ㄱ         passed, commit 가
	Korean 가, Super+Backspace   eaten, pre ㄱ         passed, commit 가
	Korean 가, Backspace         eaten, pre ㄱ         unchanged
	Korean 가, Shift+Backspace   eaten, pre ㄱ         unchanged
	かく, Ctrl+Enter             eaten, commit かく     passed, commit かく
	かく, Ctrl+Tab               eaten, commit かく     passed, commit かく
	かく Space, Ctrl+PageDown    eaten, sel 0 -> 9     passed, commit 確
	かく Space, PageDown         eaten, sel 0 -> 9     unchanged
	かく Space, Shift+Tab        eaten, sel 0 -> 30    unchanged
	Ctrl+E sm, Ctrl+Backspace    eaten, query s        passed, commit sm
	Ctrl+E sm, Backspace         eaten, query s        unchanged

chord() cannot be reused here.  It is `(mod & ~Mshift) != 0 && mod !=
Mctrl` and the exclusion is deliberate, since Ctrl+letter is strans's
whole command set and chord() has to let plain Ctrl through.  The rule
this needs is the other one -- any modifier that is not Shift -- and
Shift must stay in: Shift+Tab cycles the candidates backwards, pinned by
engine/candidate-completion and engine/emoji-navigation at
engine_test.c:914 and 1649, and Shift on a Korean key is what makes ㅃ.

One line serves both paths because it sits above the searchkey dispatch,
and it has to sit below the switch at strans.c:829: 한자, 한/영, 変換 and
無変換 arrive as keys above Kspec and are rewritten there into the Ctrl
chords they stand for.  Above the switch, 한자 would commit and pass
instead of opening the Hanja list; below it, pressing it with Ctrl held
still opens the list, because the switch sets the modifier itself.

What this does not cover: Space, which is below Kspec, so searchkey:763
still picks a candidate on Ctrl+Space inside a search.  transition tests
!(mod & ~Mshift) for its own Space at strans.c:865, so the two disagree
there.  Left alone: what Ctrl+Space should mean wants its own argument,
not a widened guard.
2026-08-18 09:01:53 +09:00
3f8f51adf4 engine: typing counts as being heard from
takelost's own comment says the context the engine was taken from "gets
that text back when it is next heard from", but it was wired to the
Keyrelease and Keyreset arms only.  The Keypress arm opens with
sclear(&lost), so the one way a context is most obviously heard from --
somebody typing into it -- is the way that threw its text away.

Compose in A, type in B before A's focus-out arrives, then go back to A
and type: A's reading is gone rather than handed back.  That interleave is
not exotic here, it is the premise takelost exists for.  Focus-out can
arrive after the next context's keys, which is why the engine changes
hands with text still pending; and a context whose focus-out is still in
flight can be typed into again just as easily as it can be reset.

	engine_test.c: the taken owner types and gets its reading back:
		want "か", got ""

The call has to go above the owner switch, because that switch's own
sclear(&lost) is what destroys the text -- putting it after the switch
instead fails the same line.  Above it, the order also comes out right:
the recovered reading is appended to the commit first and whatever the key
itself commits follows it.

What this does not cover: there is one lost slot.  A third context taking
over between the two still destroys the first one's text for good, and
widening that means an array where a single Str is now -- a new structure
for an interleave that needs three contexts and no focus-out from any of
them.  Left as it is.

ibus_client_smoke was pinning the defect rather than merely missing it: it
required exactly three commits on context a, and its handler rejected any
commit text but か, so the recovered reading tripped both.  Bumping the
count would have been a constant with nothing behind it, so the take-back
got a stage of its own and the text is asserted -- the official libibus
client now watches the whole thing, and without the fix reports

	retook=0

One assertion was written and then deleted.  takelost now runs on every
key rather than only on the two lifecycle ops, so re-committing the same
text on every keystroke looked like the hazard worth pinning, and "the
reading comes back once" went in beside the others.  Making takelost skip
its lostowner = nil left all 90 passing: the owner switch reassigns
lostowner to the context it just displaced, so lostowner is never the
owner of the next key, and the hazard is unreachable.  An assertion that
cannot fail is decoration, so it is not in the tree.

90 unit, check-live, check-stress and valgrind clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 08:27:21 +09:00
8ebe6b32df engine: a mode mark does not outlive its owner
The mark a mode switch leaves, from 6a6749e, is cleared at the top of
transition(), so the next key takes it away.  Nothing takes it away when
there is no next key.  reset() clears the pending text, the raw state, the okurigana split
and the search, and leaves modemark alone -- so a release or a lifecycle
reset flushes everything the popup was showing except the one thing that
is still drawn.

Then samedraw() sees a picture identical to the last one and redraw()
sends nothing, and win.c only ever unmaps from a Drawcmd (winshow, the
sole caller of both map and unmap).  So the popup stays.  Press the 한/영
key, type nothing, and alt-tab: 한 sits above every window, override
redirect and typed as a tooltip, until the next keystroke in any
strans-aware field -- which may be minutes.  Counted on a private X
server, viewable override-redirect windows:

			before	after
	at rest		0	0
	after Ctrl+S	1	1
	after letting go	1	0

reset() is where it belongs rather than the release arm, because the mark
is one more thing that is pending: it is drawn only when nothing else is
(snapshot), and it means the switch has not been typed on yet.  Clearing
it there is safe for the switch itself only because transition() zeroes it
at the top and setlang() runs after flush() -- reset() is reached through
that flush, before the new mark is set.  That ordering is not obvious, and
it is exactly what engine/direct-language-modes already asserts at
engine_test.c:584; breaking setlang so a switch marks nothing fails that
line and the new ones together.

Not a Wayland defect: wl.c's leave() calls hidepopup() unconditionally
after the release, so its popup was already coming down.  X11 was the
frontend that trusted the picture.

The new case goes in engine/direct-language-modes beside the mark
assertions that were already there, and drives imhandlekey rather than
transition, since the defect is on the owner-release path.  Without the
fix:

	engine_test.c:619: check failed: draindraw(&dc) > 0

that is, the engine published no picture at all.  Its second line needs
its own reason to exist, so it was checked by removing the fix and making
samedraw() answer 0 for everything: the picture then arrives and still
carries the mark,

	engine_test.c:620: want 0, got 1  (dc.pre.n)

which is the assertion that would otherwise have been decoration.  The two
setup lines are load-bearing too -- with setlang marking nothing they fail
rather than passing vacuously, since an empty picture equal to the last
one is never sent.

90 unit, check-live, check-stress and valgrind clean; the count is
unchanged because the case joined a test that already existed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 07:48:26 +09:00
b61c04b280 gtk: an entry hidden the older way is private too
isprivate() reads the GTK input purpose and nothing else, but the purpose
is not the only way an application marks a password field, and it is not
the older one.  GTK 3.24.52, measured:

	gtk_entry_set_visibility(entry, FALSE)	purpose stays FREE_FORM
	gtk_entry_set_input_purpose(PASSWORD)	purpose PASSWORD

so an entry hidden the first way looks like ordinary text to the module.
This machine runs one.  /usr/libexec/xfce-polkit, the XFCE authentication
dialog, is up right now under GTK_IM_MODULE=strans, and its binary calls
gtk_entry_set_visibility and never gtk_entry_set_input_purpose, so every
password typed into it goes through the engine.  Against the real daemon,
in Korean mode:

	typed hunter2		stored ㅗㅕ숟ㄱ2
	typed correcthorse	stored 책ㄱㄷㅊ쇅ㄴㄷ
	typed P4ssw0rd		stored ㅔ4ㄴㄵ0ㄱㅇ

The field draws bullets, so nothing on screen says why the authentication
failed -- except the pending syllable, which is drawn as itself: three
keys into such an entry the old module leaves 한 on screen where the
field should read ●●●.  With this it reads ●●●, and the entry holds gks.

set_visibility sends the input context no signal, so the question cannot
be answered where the purpose is, in init and notify::input-purpose.  It
has to be asked at the key, of the client window, which is the entry's
own: gdk_window_get_user_data on it returns the GtkEntry.  fcitx5-gtk asks
it the same way in all three of its GTK versions -- gtk3/fcitximcontext
.cpp:1155-1162, under the comment "seems visibility != PASSWORD hint".

It costs one field read and one type check per key, on a path that then
does a socket round trip anyway.  It does not cover XIM: that protocol has
no attribute for this, and no fcitx5 frontend answers it either -- only
its GTK and Qt client modules do -- so an X client reached over XIM still
composes in its password field.  Nor does it notice a "show password" box
switched off in the middle of a composition: there is no signal for
visibility, so the pending text stays in the daemon until the next focus
change.  Noticing that needs a signal connection on a widget the module
does not own, which is a bigger thing than the hole is.

The test is a live one and it earns its line: with the fix removed it says

	gtk_live_test: hidden-entry key reached daemon or did not commit

Both guards were checked by removing them.  Without GTK_IS_ENTRY the run
takes a Gtk-CRITICAL and fails; without the im->win test it exits 139, on
the stalled-peer context, which never gets a client window.  An ishidden
that always answers yes fails earlier still, at the initial protocol
frames.  Breaking the test's own helper so it hands back the toplevel's
window instead of the entry's fails too, which is what says the assertion
watches the right window rather than merely counting no events.

That count is the one thing that had to be made deterministic.  A new
client window invalidates the caret, and the fake daemon records that
frame on its own thread, so sampling the event count straight after
set_client_window raced it: 2 failures in 8 runs.  The block now waits for
that frame and names it, and there were 0 in 20 after.

90 unit, check-live, check-stress and valgrind clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 04:18:41 +09:00
dadda19b3a wl: a key the engine takes is ours to repeat
grabrepeat was an empty body, and under the grab the compositor feeds the
client nothing while a key is down: one press, then only repeat_info,
leaving the repeat to whoever holds the grab.  So a key strans ate acted
once however long it was held.  Hold Backspace over a syllable and one
jamo goes; hold a jamo key and you get one where every other window gives
four a second.  X11 has none of this -- the server auto-repeats and xim.c
dispatches every press -- so the same keyboard behaved differently
depending on which frontend the window went through.

Under headless sway, which advertises repeat_info(25, 600), holding
Backspace 1.5s over 라 with 가가가나다 committed behind it:

	before	preedit 라 -> ㄹ, and no key at the client at all
	after	preedit 라 -> ㄹ -> empty, then 9 BackSpace at the client

fcitx5 and kime both keep driving the repeat after the engine stops
wanting the key, and both then must send a release before every press, or
the client sees a key held down and starts a repeat of its own on top of
theirs.  That machinery exists only to undo the choice that created it.
strans does not need it: forward() already hands a key the engine did not
take to the client as a key, and the client repeats that correctly -- the
same hold with nothing pending, which takes that untouched path, delivers
24.  So the rule is one sentence.  strans repeats what the engine takes;
when the engine stops taking it, presskey has already passed the key on,
the deadline is dropped, and the client repeats it from there.  9 against
24 is that handover: driving every tick here would have made them one
number.

It costs one pause of the client's own delay, 600ms, where the preedit
empties.  It buys no release-and-press fiction anywhere in the file, no
second repeat engine, and no timestamp arithmetic -- exactly one key is
forwarded per hold, so there is no run of stale timestamps to mend.

nsec() is gettimeofday, which steps; a deadline wants the clock ipc.c
already uses.  The owner poll stands down while a key repeats, because the
repeat's own Keypress carries our owner and takes the engine back, so it
would have nothing to find.

Arming needs the engine and cannot be reached from the unit suite; ending
a repeat can, and a repeat outliving its key is the worst this could do.
wl/repeat-ends was checked by breaking it five ways -- a constant rate, a
constant delay, the empty body back again, the key's own release no longer
ending it, and a repeat into a dead context staying armed -- each failing
only its own line.

Phases A-G unchanged at both scales; 90 unit, check-live, check-stress and
valgrind clean.  One limit worth writing down: the GTK probe prints the
BackSpace it receives and never deletes on one, on this path or on the
plain pass-through, so the phase proves the keys arrive and says nothing
about the text behind them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 02:26:36 +09:00
62ddc2177b data(hanja): a lone consonant is a reading too
Every Korean keyboard's 한자 key answers a lone consonant with the KS X
1001 symbol palette, and has since 한글 워드프로세서: ㅁ for ※ ○ △ ㈜, ㄴ
for the brackets, ㄹ for the units, ㅇ for the circled numbers.  strans
sends that key to the same search as a syllable -- Khanja is Ctrl+H at
strans.c:830, and startsearch seeds the query with whatever ko.c left
pending -- but every one of hanja.dict's 187286 readings is a syllable, so
the popup came up with a query in it and nothing to pick:

	ㅁ: 0 candidates
	ㄴ: 0 candidates
	ㄹ: 0 candidates
	한: 99 candidates 韓 漢 寒 限 閑 恨 旱 汗 翰 邯 罕 悍 澣 閒 瀚

libhangul ships that palette beside the Hanja table already imported here:
data/hanja/mssymbol.txt, same commit, same author, same BSD-3 terms, same
key:value:comment format -- and keyed by the compatibility jamo ko.c
already holds, U+3141 for ㅁ.  So the engine does not change at all; the
same dictlookup on the same trie now finds something:

	ㅁ: 75 candidates # & * @ § ※ ☆ ★ ○ ● ◎ ◇ ◆ □ ■ △ ▲ ▽ ▼
	ㄴ: 23 candidates " ( ) [ ] { } ‘ ’ “ ” 〔 〕 〈 〉 《 》 「 」
	ㄹ: 94 candidates $ % ₩ F ′ ″ ℃ Å ¢ £ ¥ ¤ ℉ ‰ € ㎕ ㎖ ㎗ ℓ
	한: 99 candidates 韓 漢 寒 限 閑 恨 旱 汗 翰 邯 罕 悍 澣 閒 瀚

Both scripts widen by one rule -- a syllable reading gives Hanja, a jamo
reading gives a symbol -- and hanja.src regenerates byte for byte as it
was, because upstream's own non-syllable readings are words like ㄱ자집
whose values were never Hanja and still fall out.  985 of mssymbol.txt's
987 rows survive: its ideographic space and its soft hyphen do not, since
a candidate the popup cannot draw is not a candidate, and the row format
separates candidates with a space besides.

The two keyspaces cannot collide -- one is syllables, one is single jamo --
so the 187286 existing rows are unchanged, byte for byte, and 18 rows join
them.  mkhanja takes a source list as mkemoji already does, and keeps each
upstream header, which is why the licence text now appears twice.

89 unit, check-live, check-stress and valgrind all clean.  The five new
assertions were checked by breaking the change five ways: dropping
mssymbol.src from SOURCES, letting issymbol keep a formatting character,
letting a jamo reading keep Hanja, widening isjamo to the vowels, and
making mkhanja reject jamo readings.  Each fails only the tests that exist
for it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 01:56:23 +09:00
82078a60e7 build: the reference trees are not ours
fcitx5, fcitx5-hangul, fcitx5-gtk, kime and libhangul are cloned into the
work tree as ref-*/ to be read against, so git has to be told to leave them
alone.  One pattern rather than five names: whatever gets cloned next is
covered too, and none of it is ever ours to track.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 00:40:11 +09:00
46ab39ab95 test: the popup's stacking and its map state are one observation
popupabove() took the stacking order from XQueryTree and then the map
state of each child from a round trip of its own.  The daemon raises and
maps between those, in that order and in one flush, so the loop could pair
a stale order -- popup still below the client, where it was created -- with
a fresh IsViewable, and report the raise that had already happened as a
popup stacked below the client.  1 run in 25 under load, which is the worst
kind: often enough to teach you to re-run a red instead of reading it.

Widening the gap to 300ms shows it with no load at all, and shows the fix
is the right one.  Same test, same widener, 15 runs each:

	before	pass=11 fail=4
	after	pass=15 fail=0

XGrabServer is what makes the two one observation, and the widener is then
harmless because nothing can raise inside it.  Unwidened, 20 runs clean.

Nothing about win.c was wrong: the popup does go above, and the test now
only says so when it is looking at a single moment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 00:22:34 +09:00
65066eef53 wl: an unavailable seat still owes the engine a release
Another input method taking the seat made wlthread hide its popup and
return, leaving activeowner pointing at our context with its text still
pending, and whatever keys we had passed on still down at the client.
leave() is that cleanup and all of it: it gives the keys back, drops a
half-typed sequence, sends the Keyrelease and takes the popup down.

Nothing visible changes either way -- the compositor drops anything an
unavailable input method commits, and the next key from any other
frontend takes the engine regardless.  This is the engine left tidy, in
the one word that already means it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 23:26:08 +09:00
0f128a46b3 wl: another frontend's picture is not ours to show
e64e4ea took the picture out of the owner poll and argued a later take
could not show a stale one: "either our own key replaced it, or the
engine suppressed the send because our snapshot is that same picture".
The second half is the hole.  redraw() returns before it drains when the
picture has not changed, and under Wayland wl is drawc's only reader, so
every IBus interaction leaves its last picture sitting there -- the mode
mark after Ctrl+T, a hanja list, whatever was up.  Press a bare modifier
in a text-input-v3 window while IBus owns the engine: keymeaningful(0) is
false, the engine changes nothing, the send is suppressed, and grabkey's
takedraw puts the other window's candidates on our popup surface.

I could not make it visible.  checkowner() runs later in the same loop
iteration, finds we are not the owner and hides it, and both commits
leave in one wl_display_flush, so the compositor renders nothing between
them.  What is wrong is the rule: README says an IBus client under
Wayland "shows no candidates, because the popup belongs to the Wayland
frontend", and that is now true by construction rather than by luck.

leave() and flushpending() already take only while engaged, so the guard
costs nothing where it is and covers all three callers.  The drain still
happens either way, which is what keeps drawc and lastdraw in step.
Phases A-G under headless sway at both scales; the phase E hanja popup
is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 23:25:53 +09:00
650f00812d compose: a sequence belongs to one context
One xkb_compose_state served the whole session, so a dead key left half
typed in one window was completed by the first key in the next.  Two IBus
contexts through the real processkey(), before this:

	want length 0, got length 2; byte 0: want <end>, got 0xc3  (text)
	want 0, got 1                                              (req.op)

Context A pressed dead_acute; context B pressed e, got é, and went down
the composed-text branch instead of sending a key.  Only COMPOSING is
sticky -- xkbcommon starts over by itself after COMPOSED and CANCELLED,
which the table in compose_test already pins -- and nothing here ever
called xkb_compose_state_reset.

That static was also fed from three procs, two of them live at once, with
no lock, which xkbcommon forbids.  Reaching it needs two focused windows,
so it cannot be made to fail on demand and has no test: it goes because
the sharing goes, not because anything guards it.

So a state per frontend, each starting over when the key comes from
another context.  All are made in composeinit(), on threadmain, before
proccreate, because xkb_compose_state_new refs the table and that ref is
a plain increment --

	b160: mov (%rdi),%edx   b16a: add $0x1,%edx   b170: mov %edx,(%rdi)

-- so making a state from a shared table on two procs would race in place
of the feed.  Made before the procs exist they can still share the one
table.  An owner address that gets reused says so with composedrop: ibus
hands out a contexts[] slot again, xim can malloc an Ic at a freed one,
and wl's single context, which serves every client in turn, drops at
every activate and deactivate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 23:25:22 +09:00
e64e4ea6af wl: the owner poll does not need the picture
It took the engine's picture and drew it, then took the popup straight back
down: a buffer allocated, filled and attached for nothing, and for one
frame it was another context's candidates at our client's cursor.  The
drain it was doing is not needed either.  redraw() empties drawc before
every send, so the channel holds at most one picture and that picture is
always lastdraw -- what the engine wants shown now, whoever owns it.  A
later take therefore cannot show something stale: either our own key
replaced it, or the engine suppressed the send because our snapshot is
that same picture.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 22:18:31 +09:00
867c6ebe40 docs: the compositor list was six claims and one of them false
Mainline dwl has no input method: dwl.c creates a virtual keyboard manager
and never a wlr_input_method_manager_v2, so IME there is a third-party
patch and strans falls back to XIM through Xwayland -- not what the line
promised.  Being a wlroots or smithay compositor is the wrong criterion,
which is the general lesson: the compositor has to wire the protocol up.
river and labwc do, and were checked; Wayfire, niri and COSMIC never were.

So the opening says the criterion instead of a list to maintain, and the
Run section already says how the choice is made.  The README says it once.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 22:17:53 +09:00
8a08efe2bb wl: a deactivate cannot hand the pending text back
40fd4ab was half right and the sway run said which half.  The compositor
does relay the commit when a client merely disabled its text input while
holding the focus -- that much was read correctly out of sway -- but the
client has stopped listening by then.  On the wire, in that order:

	-> zwp_text_input_v3.disable()
	-> zwp_text_input_v3.commit()
	<- zwp_text_input_v3.preedit_string("")
	<- zwp_text_input_v3.commit_string("가")
	<- zwp_text_input_v3.done()

and the entry unchanged.  GTK3 drops global->current at its own
focus-out, and any client that follows text-input-v3 does the same, since
events between disable and the next enable are to be ignored.  So the
composition is unrecoverable there, by no fault of ours, and the commit
was a request nobody could take.

The code goes back to what it was; the comment does not.  The old one
said the compositor drops the commit, which is true only when the focus
moved, and that half-truth is what invited the change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 22:10:31 +09:00
5ff54237ff docs: the Wayland frontend composes too
It feeds the Compose table from the grab's keysyms like the other two; the
sentence was written when there were only two.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 21:26:59 +09:00
bbc0c97656 main: GDK_SCALE is read once
Both popups read it, each just before its own textinit, and the second
copy needed a comment to say why it was there at all.  One frontend runs
per session and popupscale is one global, so threadmain reads it before it
starts either -- which is before any setfont, the only ordering the two
copies were keeping.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 21:26:59 +09:00
aef577026d dat: one pair of content purposes
Two frontends read the same two numbers, and wl.c carried four lines of
comment to say why it kept its own copy of them.  IBus and text-input-v3
number the purposes alike because both took them from GTK, so the header
is where they belong and the explanation goes away with the copy.  hidden
stays in both files: one asks about a context, the other about the seat.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 21:26:48 +09:00
8472474332 wl: modmask's caller has already looked at the keymap
grabkey returns before it asks for the mask when there is no keyboard
state, so the guard inside modmask answered a question nobody put to it --
except the test, which was the only caller that could reach it.  A check
kept alive by the test that reaches it is one line of code and one line of
test to delete.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 21:22:36 +09:00
33710178d4 wl: no keyboard grab while inactive
An activate and a deactivate in one batch left the grab taken with nothing
active, which is the one state the grab must never be in: sway hands keys
to the grab holder without asking whether the input method is active.
sway cannot produce that batch -- relay_send_im_state sends a done after
each -- so this is the table being total, not a bug that bit.  The
deactivate arm was already right; only the activate arm forgot to look at
where the batch ended.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 21:22:10 +09:00
5cb9f69590 wl: every keycode evdev can send
The bitmap held 256, and forward dropped anything above it rather than
passing it on, so a key the engine does not want vanished instead of
reaching the client.  Keys do live up there: KEY_FN is 0x1d0,
KEY_VOICECOMMAND 0x246, KEY_MACRO1 0x290, and xkeyboard-config maps
keycodes to <I709>, so they carry a keysym and are nothing special to us.
sway matches its bindings before it hands a key to the grab, so an unbound
one of those was lost for as long as any text field had the focus.

The whole evdev range costs 96 bytes of bitmap instead of 32.  The test
already writes the bound rather than the number, so it still says what it
said.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 21:21:54 +09:00
6d511919e8 wl: a content purpose belongs to its activation
pendingpurpose was never cleared, and sway sends content_type only when
the text input asked for one: active_features is frozen at the enable
commit (wlr_text_input_v3.c) and relay_send_im_state tests it before
sending.  So a client that sets no content type leaves the last client's
purpose standing -- and after a GTK password entry that purpose is
password, so every key in the next window was forwarded raw and strans
looked dead there until a client that does declare one took the focus.

A purpose is per activation, as it is per context in ibus.c.  Clearing it
on activate is enough: the content type, if any, arrives after the
activate and before the done that applies both.

The sway runs could not have found this; the password client was GTK,
which always declares.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 21:21:24 +09:00
40fd4ab551 wl: a deactivate hands the pending text back
Two things arrive as a deactivate.  When the focus moved, the compositor
sent the text input its leave first, and sway's handle_im_commit finds no
focused text input and drops the whole commit -- which is what the old
comment described.  But a client that merely disabled its text input
still holds the focus: sway relays that deactivate only in that case
(handle_text_input_disable returns early once the surface is unfocused),
so the commit would have been delivered, and half a syllable was thrown
away instead.  A GtkEntry losing the focus to a button in its own window
is that case.

XIM and IBus both hand the text back there and drop it only when the
client itself is gone -- ibus.c keeps the two apart as flushcontext and
releasecontext.  This gives the Wayland frontend the same halves.  The
commit that goes nowhere costs one empty request.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 21:21:07 +09:00
841d93ac35 docs: the README says it once
It had grown to 256 lines, a third of them the engine's fine behaviour
told twice and packaging trivia that belongs in the Makefile.  Every
user-facing fact is kept; the prose around it is not.  The dependency
list is one sentence and a pointer at the Dockerfile that already names
the packages exactly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 20:45:04 +09:00
f6dd056ffb build: 9c and 9l can report again
Both filter the compiler's output through $egrep, and plan9port assigns
that variable nowhere -- six scripts read it, one unrelated developer
script sets it.  The pipeline dies on the empty command and takes the
whole diagnostic with it, so -Wall -Wextra was decorative and a build
that failed to compile printed nothing but an exit status.  That is how
the tests' own dialect bug hid until it was hunted with a patched 9c.

Both scripts source $PLAN9/config, which the distribution does not ship,
so one line in it is the fix the scripts themselves ask for; it needs no
patching of a packaged file and covers 9c and 9l together.  The daemon it
builds is byte-identical, and with the diagnostics visible the whole tree
and every test object compile without a warning.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 20:04:33 +09:00
ad2f2fc5eb test: the xkb modifier mask and the forwarded key bitmap
The two pure things in the Wayland frontend, in the shape the XIM adapter
test already uses: wl.c included behind one define that makes the virtual
keyboard inert, and a keymap built from a string, so no compositor and no
xkb data files are wanted.  The mask test holds Caps Lock apart from
Shift, which is what a Korean key turns on; the bitmap test holds a
release of a key we never passed on, a code the bitmap cannot hold, and
the release of everything still down when the context goes.

wayland-scanner's output is the parent Makefile's to write, as ../strans
already is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 19:51:37 +09:00
2e1e0b9645 docs: Wayland
One popup per session, so the frontend is picked at startup: a compositor
with zwp_input_method_v2 gets that frontend and neither XIM nor the X11
popup is started.  The answer for a user is one sentence -- on Wayland set
none of the module variables -- and the two exceptions the plan asked to
check turned out this way: GTK 4 binds text-input-v3 by itself, with
GTK_IM_MODULE unset (4.22 under sway 1.12, typed and composed), and
Chromium is beyond help either way, since it asks for text-input-v1 and
wlroots implements only v3.

The plan is deleted, as it said to be once the README described what
landed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 19:47:50 +09:00
5664a008de wl: the candidate popup
One wl_surface and one zwp_input_popup_surface_v2, made once; the
compositor makes them visible on activate, puts them at the text cursor
and takes them down again, so popuparea, popupposition and Caret stay
X11-only and popuplayout gets a constant area.  Two shm buffers are used
in turn, each busy from attach until the compositor releases it, because
win.c's single grow-only image would be redrawn while the compositor was
still reading it; a picture that arrives while both are busy waits for
the next release, and the engine will not send it twice.  drawthread no
longer runs, so wl.c reads GDK_SCALE itself and calls textinit after it,
since setfont reads Fontsz.  The buffer is rounded up to a multiple of
the scale: one that is not is an invalid_size error at attach, and a
preedit alone really does lay out to an odd width at GDK_SCALE=2.

drawc is taken where the engine's picture and our surface can part:
after every key, after the Keyrelease a deactivate sends, and after the
owner poll, hiding for the last two.  The poll is not an optimisation --
once another frontend takes the engine, its pictures go unconsumed and
no later send will ever say hide.

main.c now starts either the Wayland frontend or the X11 popup and XIM,
which is where the two popups would otherwise collide.

Checked by hand under headless sway 1.12, reading grim screenshots: the
hanja list for 가 with its selection and 1-9/125 marker, the emoji
search, the popup gone after Escape, and the same at GDK_SCALE=2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 19:35:19 +09:00
4ac0625c6b wl: the input-method frontend
zwp_input_method_v2 over one connection, in one process: activate,
deactivate and content type applied at done, whose count is the serial a
commit must carry.  The keyboard is grabbed at every activate and
released at every deactivate, because sway hands keys to the grab holder
without asking whether the input method is active, and a grab held while
inactive would take every key in the session.  Keys come as evdev codes,
go through xkb and the Compose table the other frontends already share,
and reach the engine as an ipckeysym and a modifier mask; the grab's
modifiers go on to the virtual keyboard as well, since wlroots derives no
state from a virtual keyboard's own keys.  A key the engine does not eat
goes back as a key, not as text, and whatever is still down is released
when the context goes.  A password or a PIN purpose never reaches the
engine at all.

No popup yet: the X11 one still owns drawc, and with no Keycaret from us
it follows the pointer.

Checked by hand under headless sway 1.12 with a GTK3 entry driven by
wtype: ascii passes through, Ctrl+s selects Korean, rk shows the preedit
and Enter commits 가 and lets the Return on, Ctrl+c arrives with its
modifier, focus moving between two clients re-grabs and keeps typing, a
password entry types literally, a second strans says so and stops, and
WAYLAND_DISPLAY without a compositor falls back to XIM.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 19:17:27 +09:00
233884e51f build: the input-method and virtual-keyboard protocols
zwp_input_method_v2 and zwp_virtual_keyboard_v1, vendored verbatim from
wlroots 012ca825 under proto/ and turned into imv2.[ch] and vkv1.[ch] by
wayland-scanner; only the XML is tracked, and nothing compiles them yet.
The Dockerfile gains wayland, which carries the scanner.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 18:55:41 +09:00
27180bc0fe test: a key passed through is not committed text
A Korean Enter commits the syllable and lets the key on, so xim.c sends
the commit and then forwards the key.  pumpinput took the text of either
and kept the last, so under load the Return's own carriage return
overwrote 가: 22 of 25 runs failed on a loaded machine and none on an
idle one.  An XIM commit arrives as XLookupChars; a forwarded key comes
back as XLookupBoth, and is not a commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 18:38:12 +09:00
f6477cb97d test: the unit build takes the dialect the daemon is built with
9c compiles with -std=gnu11 and UNIT_CFLAGS overrode that with -std=c99,
so the tests built the daemon's own sources under a dialect the daemon
never sees.  Under it glibc hides lstat, and server_test.c reaches
<sys/stat.h> before srv.c pulls in u.h -- it must, since thread.h
defines recv as chanrecv and the socket recv it calls has to be declared
first.  So make all was green while make check would not compile.
HOST_CFLAGS keeps -std=c99: those are host programs with no Plan 9
headers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 18:38:12 +09:00
059e8bbc75 ibus: SetCursorLocationRelative is not a caret
A GTK client on the Wayland backend sends this where an X11 one sends
SetCursorLocation, and ictab did not list it, so libdbus answered
UnknownMethod.  Accept it and throw it away: the absolute translation
sits inside GDK_IS_X11_DISPLAY, so the coordinates are surface local,
and ibus's own daemon will not hand them to an engine either.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 18:38:04 +09:00
e93ec43706 docs: the plan for a Wayland input method
zwp_input_method_v2 in one new file, with its popup, drawn from the reply
the engine already produced; the session picks the popup, so the engine,
the IPC and the X11 side are untouched.  Records what was measured, what
fcitx5 does differently and why, and what was refused.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:18:32 +09:00
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
ea363ca792 font: initialise fontconfig ourselves
fontconfig 2.16 warns on every start when Pango reaches it before
FcInit(); calling it first keeps stderr quiet.
2026-08-16 21:41:36 +09:00
bab6077c75 srv, ibus: remove the endpoints on a signal too
A stopped daemon left its socket and IBus address file behind, since
plan9port ends a process on SIGTERM without running exit handlers. Both
files now go on a note as well as on exit, and only while they are still
the ones this daemon made, so a successor is never robbed of its own.
2026-08-16 21:40:42 +09:00
48d3e165c3 xim: dead keys and Compose
Xlib does no local composing for a client on an input method server,
so under XIM a dead key was forwarded as a bare keysym and vanished.
The XIM frontend now feeds keys through xkbcommon's Compose table for
the daemon's locale and commits the composed character itself.
2026-08-16 21:39:22 +09:00
6b1de2df4f popup: flip above the pointer at the bottom edge
Without a caret the popup sat at pointer+10 and was clamped upward at
the bottom of the work area, covering the pointer; it now goes above the
pointer like it goes above a caret.
2026-08-16 21:36:31 +09:00
45c6be7e1b docs: rewrap the popup paragraph 2026-08-16 21:35:51 +09:00
4ff696d06c popup: follow GDK_SCALE on HiDPI
The popup was 32-pixel rows whatever the display; next to 2x application
text it was small. Its metrics now scale with the daemon's GDK_SCALE,
the setting the applications use.
2026-08-16 21:35:43 +09:00
30abd05e68 engine: room for 128 candidates
32 hid the tail of common readings: きょう has 41 kanji, 구 has 352
hanja. 128 covers every kanji entry and the hanja dictionary now keeps
that many per reading.
2026-08-16 21:33:18 +09:00
693a1210a6 engine: romaji is case-blind
Shift or Caps Lock made a Japanese key pass through as a Latin capital,
so nothing composed with Caps Lock on; Korean already folds case. Latin
text still has Ctrl+T.
2026-08-16 21:30:59 +09:00
91b3e538f1 engine: Ctrl+H converts the pending syllable; Backspace on an empty search stays put
Ctrl+H committed the syllable being composed and opened an empty search,
so converting 한 meant typing it twice; the pending syllable now seeds the
query. Backspace on an already empty search ended it and then reached the
application, deleting a character; it now only ends the search.
2026-08-16 21:30:16 +09:00
5e72a48c2c engine: Korean and Telex let Enter and Tab through after committing
Enter with a pending syllable committed it and stopped there, so a chat
message needed Enter twice; Korean input methods commit and pass the key
on. Japanese keeps consuming the key that confirms a reading or a chosen
candidate, as Japanese input methods do.
2026-08-16 21:28:11 +09:00
11bf379ad3 fix(engine): a reset or a lost focus commits the pending text
Clicking elsewhere, changing focus, or any client reset dropped the
composition in the GTK module and IBus (only XIM ResetIC handed it
back), so typing 안녕 and clicking Send lost 녕. The engine now returns
the pending text — a moved-to candidate first, as Enter would — on
Keyreset and Keyrelease; the GTK module asks for it before closing on
focus-out and commits it, IBus commits it on FocusOut, Reset and a
switch to a password field, and XIM commits it on focus loss and hands
it back on ResetIC without a separate capability probe.
2026-08-16 21:05:31 +09:00
4c8954b8ae test: keep the IBus startup check off a desktop's IBUS_ADDRESS_FILE
buildaddrpath honours IBUS_ADDRESS_FILE, so under an IBus session the
ownership check looked at the live address file and failed.
2026-08-16 20:32:20 +09:00
ae1bc51256 data(kana): one sokuon rule for every doubled consonant, and the missing hira rows
Hiragana spelled out 42 doubled syllables (kka, ssha, ttsu, ...) and so
knew none it did not list: matcha, baggu and beddo came out as ma-tc-ha,
ba-gg-u. Katakana already used the engine's small-tsu rule for a few
doubles. Both maps now list every doubled consonant but n, plus tch, and
hiragana gains di, che and fa/fi/fe/fo like katakana.
2026-08-16 20:31:48 +09:00
e860008d84 fix(telex): know the rimes Telex needs, and put the ưa tone on ư
The map is the syllable grammar: a key sequence stays pending only
while it is a prefix of some entry, so a rime the table lacked split
the syllable and a trailing tone key landed on the wrong letter (vowis
gave vơí, rooif rôì, thaays thâý), ua+tone was a valueless prefix that
committed the raw keys (cuar gave cuar, muaf muaf), uyê and ươ typed as
uwow took no coda (nguyễn, được), and ưa toned the a (cửa gave cưả).
Add âu ây êu ôi ơi eo ia ưi ưu uê ươu uây oeo uya uyu, the uyê, uwow,
uê and oe codas, and tone ưa on ư.
2026-08-16 20:30:21 +09:00
32a9723043 fix(engine): the search keys commit the query they end or replace
Ctrl+E during an emoji search, Ctrl+H during a hanja search, or either
during the other search dropped the typed query, while every other
Ctrl key committed it as the README says. Escape is the way to cancel;
the search keys now commit the shown text like a language switch.
2026-08-16 20:27:31 +09:00
6402c26830 fix(engine): movement keys without candidates commit the pending text
Up, Down, PageUp and PageDown with a composition pending and nothing to
move through returned the key uneaten but left the text pending, so the
client moved its cursor and reset the context: the pending Korean
syllable vanished, where Left or Home would have committed it. Let them
fall through to the special-key path that commits and passes the key.
2026-08-16 20:26:28 +09:00
9853d65d4c fix(engine): keep composing when a reading reaches Maxrunes
At the limit the key that did not fit was handed back to the client as
plain ASCII, so a long Japanese reading ended in a Latin letter, and a
pending romaji consonant was flushed as ASCII with it (…あka for か).
Commit the completed kana, keep the pending syllable, and transliterate
the key as usual; other languages commit and go on the same way.
2026-08-16 20:25:38 +09:00
cdd3e38195 fix(ibus): drop a context on the Service.Destroy libibus really sends
libibus destroys an input context with Destroy on
org.freedesktop.IBus.Service, not on the InputContext interface, so the
call went unanswered and the context slot stayed taken until the whole
connection closed. A long-lived GTK process that opens and closes text
widgets exhausted the 1024 slots and then got LimitsExceeded for every
new field. The live contract test now destroys a context that way and
checks the path is gone.
2026-08-16 20:24:26 +09:00
f6adb117c9 fix(popup): raise the popup whenever it is shown
The popup window is created once at startup and only mapped afterwards,
and MapWindow does not restack, so every application window (or WM
frame) opened after the daemon sat above it: candidates and preedit were
invisible in those windows until the daemon restarted. Configure the
window above its siblings on every show. The XIM live test now checks
the popup stacks above a client window created after the daemon.
2026-08-16 20:23:29 +09:00
333300b297 ipc: fold Shift+Tab and the keypad Enter and Tab onto Tab and Enter
Shift+Tab arrives from every frontend as ISO_Left_Tab (0xfe20), which
has no Unicode value and lies outside the function keysym range, so
ipckeysym gave key 0 and the documented Shift-Tab wrap through search
results never reached the engine; GTK moved focus instead. KP_Enter and
KP_Tab likewise became unknown special keys, so the keypad Enter
committed the reading and passed a newline instead of taking the
highlighted candidate.
2026-08-16 20:22:17 +09:00
4b6cc6fb2f test: name the release-then-drop check the two IBus lifecycle tests share 2026-08-16 18:37:29 +09:00
0aef3506b6 str: drop argument checks no caller can trip
sinit's nil and negative-length tests and stoutf's zero-size test only
had test callers; every real caller passes a buffer and its size. The
len > n check after fullrune succeeded could never fire.
2026-08-16 18:36:40 +09:00
723a271a9c build: name the Valgrind run the container is prepared for
The Dockerfile installs valgrind and the ld.so debug info it needs, but
no target used them; docker-valgrind runs the unit suite under it, so
the dependency is visible and the run is one command.
2026-08-16 18:29:03 +09:00
3f1ff03b94 popup: keep only computed geometry in Popup; one fallback for slivers
numx, textx and selx were compile-time constants stored per layout;
the number column starts at PopupPad and text at PopupPad+PopupNumw,
which dat.h now says once. The height fallback tried dropping the
marker, then the preedit, then the padding as three separate refits;
below one padded row it now shows as many bare rows as fit, at least
one, which is what those steps added up to.
2026-08-16 18:27:24 +09:00
68678283cd ibus: room for a thousand input contexts
Toolkits keep one IBus context per widget that ever took focus, so 64
across every client could run out in a long session, after which new
widgets silently got no input method. The table is static and small
(about 100 bytes a slot).
2026-08-16 18:25:55 +09:00
03c196a7f6 ibus: keep only the content purpose; nothing read the hints 2026-08-16 16:58:12 +09:00
8831b84ffc ipc: say what the want flag and the capability probe mean now 2026-08-16 16:57:15 +09:00
45c0290fd4 build: ignore a stale xim_test binary
The old xim/Makefile built xim/xim_test; nothing does now, but older
checkouts may still have it, so keep the name ignored.
2026-08-16 16:56:56 +09:00
dde9fb431b test: probe the dead IBus address in-process
rejectold forked and re-execed the test binary with --reject-address
just to call dbus_connection_open_private once against the crashed
daemon's abstract address, then waited on the helper, drained its
stderr pipe and checked its exit status. An abstract socket with no
listener refuses the connection immediately, so the call cannot block
and needs no separate process.

Open the address in-process and require a null connection with the
D-Bus error set, freeing the error afterwards. This drops the helper
process, its stderr capture, the Test.helper fields and the argv
dispatch in main. killdaemon stays; the daemons still need it.
2026-08-16 16:44:05 +09:00
7220286ab5 test: share one daemon and D-Bus harness across live tests
Six live tests carried private copies of the same daemon harness: the
private XDG_RUNTIME_DIR, the fork/exec with captured stderr, the
readiness waits, the SIGTERM and SIGKILL paths, the IBus address file
reader, the socket connect, and the IPC probe. Three of them also
carried the same libdbus helpers.

Extract one implementation into tests/live.c and, so that the binaries
that do not link dbus-1 keep not linking it, the D-Bus half into
tests/livebus.c. Both are compiled into each binary the way ../ipc.c
already is. gtk_live_test keeps its own fake-server harness and only
takes fail, nowms and leftms; xim_live_test keeps its own Xvfb and
process-group daemon spawn, which must inherit DISPLAY, and takes the
temporary directory, timing and cleanup halves.

The copies had drifted; the harness keeps the stricter behaviour.

  - nowms reports a broken clock (-1) instead of pretending it read
    zero, and leftms turns that into an expired deadline, so a loop
    ends in a timeout failure rather than spinning. livesetup checks
    the clock once up front, as daemon_restart_test did.
  - Timeouts compare with <= 0, not == 0.
  - readuntil keeps the three-way result (complete, peer closed, error)
    from ipc_live_test and daemon_restart_test rather than folding
    peer closure into ECONNRESET.
  - The daemon's stdout and stderr are both captured, and a failing
    setenv is reported, for every daemon; daemon_failure_test captured
    stderr only and said nothing about setenv.
  - The child keeps daemon_restart_test's careful redirect that also
    works when the pipe lands on fd 1 or 2.
  - parseaddress requires a positive declared PID.
  - killdaemon reports ECHILD after SIGKILL as a failure; one copy
    accepted it. It now reaps with a blocking waitpid, which SIGKILL
    guarantees will return, instead of daemon_restart_test's polled
    wait with its own timeout diagnostic.
  - stopdaemon tolerates ESRCH on SIGTERM, a benign race two copies
    reported as an error.
  - liveclean removes the socket and address files and then rmdirs
    each directory, reporting leftovers, rather than deleting the
    temporary root recursively.
  - ibus_live_test now waits for the IPC socket and the address file
    by polling, dropping its inotify variant; it asserted only the
    address file before.
2026-08-16 16:44:05 +09:00
e1c36af9ba gtk: drop two includes nothing uses 2026-08-16 16:32:37 +09:00
7c73e4a463 docs: describe XIM text and placement as they now are 2026-08-16 16:31:07 +09:00
795f11be22 test: one engine pump and one preedit check for the white-box suites
server_test, ibus_test and xim_adapter_test each ran their own copy of
the same alt loop that stands in for imthread, and two of them had
private checkstr/checkenginepreedit variants. test_util.c now provides
Pump (trace, optional hold on a chosen op, stop) in its own proc so a
test may block in socket I/O while the engine runs, and test.h declares
the engine hooks once; every .c includes dat.h and fn.h itself as the
rest of the tree does. server_test's three copies of fixture setup and
teardown became serverbegin/serverend.
2026-08-16 16:30:32 +09:00
826da31486 test: drive Korean and Vietnamese through the engine's own transstr
ko_test and vi_test replayed keys with private copies of transstr's
loop, and test_util reimplemented the shown preedit; the engine exports
impre(Im*, Str*) and transstr() instead, so a change to composition
rules is one edit.
2026-08-16 16:25:40 +09:00
849bf1278b popup: one text drawing call; drop what the draw thread never reaches
font.c exported three wrappers for one function, and the fit == -1 mode
existed only for the tests; textdraw() takes fit and colour. win.c
tested the empty picture in the thread and again in winshow, interned
two atoms by hand next to getatom(), kept a consumer-side drain that the
producer's already guarantees never finds anything, zeroed ten globals
before returning from the only thread that read them, and re-checked
sizes that resizebacking had just established. The page marker is laid
out once and drawn from the layout instead of being recomputed.
2026-08-16 16:22:37 +09:00
eaf31da0d1 ibus: one reply path, no ceremony around calls that cannot fail
Six handlers built method returns by hand and only two of them checked
for a nil message; reply() builds them all and dies on OOM like the
rest of the daemon. DBusError objects were initialised and freed but
never read: libdbus accepts nil. Properties.Get always errored, so it
is one line in onmsg; the introspection XML served nobody (libibus
never asks) and had to be kept in sync by hand. Ibushinthidden was not
an IBus hint, so that term of hidden() never fired. writeaddr uses the
syscalls directly and the address file's dev/ino is the ownership
proof; buildaddrpath makes the directory in place. An empty
UpdatePreeditText goes out only to the context that shows a preedit.
2026-08-16 16:20:10 +09:00
c2baa47026 fix(xim): follow keyboard layout changes
kinit() sets up the XKB extension, after which the server stops sending
core MappingNotify to this client, so the refresh branch never fired and
strans kept its startup keymap; setxkbmap after launch left every XIM
key resolved against the old layout. Select XKB NewKeyboardNotify and
MapNotify (with the map parts, or MapNotify is never delivered) and
rebuild the keymap on those events instead.
2026-08-16 16:16:44 +09:00
a70cfc42e3 xim: drop what no client uses; poll for owner loss like IBus
XIM text always went out through imdkit's COMPOUND_TEXT converter,
which already wraps every UTF-8 string in ESC%G; the UTF8_STRING
negotiation, the per-client encoding list, ximtext.c's fallback and its
wrapper never changed a byte on the wire. The spot location is honoured
for every style now (GTK's XIM module sends it with PreeditCallbacks)
and an unset focus window means the client window, as the spec says,
so those clients get the popup at the caret. readattrs/place kept four
transient fields to pass values between them; ximclose tore down state
right before die(); the OOM passthrough context was a third policy for
one small calloc where emalloc dies like everything else. Both frontends
now poll for engine-owner loss only while a preedit shows instead of
XIM waking on a pipe the engine had to know about; ibus stops waking
five times a second when idle. keymeaningful() is the engine's own
predicate. The standalone xim_test moved into the unit suite, so
xim/Makefile is gone.
2026-08-16 16:16:31 +09:00
9080bb833c engine: trim the key dispatch
mapget and maplookup were one trie probe with two predicates; mapmatch
returns the match kind and mapget is its exact case. movedelta is a
switch. searchkey's modifier test was unreachable behind transition's,
searchlang's Alt/Super test behind both callers' earlier returns, and
picksearch's range check behind numberedkouho. Korean key folding runs
once at the top of transition. commit() on an empty preedit is a no-op,
so the Ctrl branch and startsearch lose their haspre guards and repeated
commit/reset pairs. Ctrl+language inside a search now commits the shown
query as it does elsewhere instead of dropping it. Language ids are
documented as the Ctrl codes they are; Im parameters no longer shadow
the file's Im.
2026-08-16 16:09:05 +09:00
68869e2589 engine: redraw when the picture changes, not when the state might have
Every key copied the whole Im and Search rune by rune, compared them
after the transition, and combined the answer with 'eaten', a commit
count that is always zero, and a force flag for caret and owner changes,
while redraw() kept a separate 'visible' bit to know when to send an
empty popup. redraw() now compares the snapshot with the last one sent
and treats two empty popups as equal, so imhandlekey just ends in
redraw(). keystroke() was only a test entry point and moves there.
2026-08-16 16:06:42 +09:00
a557fb114a engine: one trans contract for every language
Japanese was never dispatched through the Lang table: dotrans and
transstr both special-cased it into transjp, which edited Im in place
and wrote commits through a side argument, while transko popped and
cleared a pre that every caller overwrote anyway, and Vietnamese kept
its key history in dotrans. Emit now carries the new raw state next to
the new pre; transjp, transko and transvi are pure functions of (Im,
key), and dotrans and transstr have a single path. Vietnamese history
bookkeeping lives in vi.c, where the full-history flush no longer
resets the caret as a side effect; istone was toneidx() >= 0.
2026-08-16 16:04:57 +09:00
abaea77248 engine: look dictionaries up synchronously
The dictionary thread ran in imthread's own proc, so a lookup could
only start once the engine blocked, and it was a trie probe anyway; the
emoji and Hanja searches already called dictlookup directly. The
request/result channels, sequence numbers, staleness checks and the
second draw per Japanese key are gone; dictqjp fills the candidates
in place. Emit.dict and Lang.dictq only ever triggered lookups for
Vietnamese, which has no dictionary. dictlookup(Lang*, key, out, max)
returns the count. The Hanja lookup no longer pre-checks for a single
syllable; a reading either has an entry or it does not.
2026-08-16 16:02:09 +09:00
ebcec3af6b fix(engine): send clients the composed Telex text, not the keys
For Vietnamese the preedit is the map key ('as', 'oong'); only the popup
and the commit path looked it up. Clients with inline preedit therefore
showed 'as' while Enter committed 'á'. impre() now maps once for every
reader, and snapshot() no longer maps a second time.
2026-08-16 15:57:55 +09:00
c7718ece52 data: one trie for maps and dictionaries, one loader
The hash map served a single exact-match lookup that the trie already
answers, at the price of a second container, a second file loader with
its own drift, and a Str-to-UTF-8 conversion on every chain probe. The
files are small (kanji.dict is 7.5k lines), so the trie holds both.
trieopen validates keys and each space-separated value word against Str
and reports path:line; trielookup takes the Str every caller holds and
treats a nil trie as an unloaded map; trienew/trieput exist for tests.
The overflow guards on growth, Trie.root (always 0) and the per-language
init loop written twice are gone.
2026-08-16 15:56:32 +09:00
eb0f88f764 ipc: strip machinery no peer uses; keep client state in one Keyreq
ipc.c: an AF_UNIX nonblocking connect completes at once or fails with
EAGAIN, so the EINPROGRESS/poll/SO_ERROR path and the fcntl juggling
were dead; the deadline plumbing checked a clock that cannot fail and
re-tested the deadline before every transfer although only waitfd
blocks; readfield's truncation and discard loop served the unit test,
since every caller owns char[Ipcfieldmax+1]; NULL-argument checks on
in-tree encoders are gone (peer validation stays). The primary key frame
is Ipckey, not 'legacy'; ipckeysym names the keysym mapping.

srv.c: the per-connection capability and caret already lived in the
persistent Keyreq; the mirror locals and 'negotiated' flag guarded a
protocol rule no client relied on. proccreate never fails in libthread.

gtk: focus-out no longer round-trips a reset before closing the socket
that releases the engine; the caret dedup compares the packed frame;
srvconnect's preedit no-ops and the insimple flag are gone.
2026-08-16 15:53:08 +09:00
43b469f70b ipc: share one keysym and modifier mapping across frontends
The keysym-to-engine-key rule lived in ibus.c, xim/xim.c and gtk/main.c,
and the copies disagreed: GTK sent keypad digits as special keys while
IBus and XIM sent '1'..'9'. Modifiers were rebuilt bit by bit in two
places although Mmask already is the X core layout; only the virtual
Super bit (26) that GDK and IBus set needs folding. ipckey/ipcmod in
ipc.c serve the daemon, the module and the tests.
2026-08-16 15:47:47 +09:00
66ea2892f8 bench, docs: let the client detect readiness; say what Enter commits
bench.sh copied ipcpath() and the IBus address-file discovery into shell,
although the benchmark client only speaks IPC; retrying the client itself
is the readiness test. It also forwarded arguments to a daemon that takes
exactly one. README claimed Enter confirms the selection, but Enter and Tab
commit the reading unless a candidate was moved to.
2026-08-16 15:46:28 +09:00
d3c9813f6e build: scope IBus flags, rebuild containers with clean, dedup gtk install
Only ibus.o needs the D-Bus and xkbcommon flags, so give them the same
per-object scoping as the popup and XIM objects. The docker targets used
-B, which recursive makes inherit: the daemon was linked three times and
the live tests built twice per docker-check. gtk/Makefile repeated the
module-directory check and cache refresh in install and uninstall.
2026-08-16 15:46:00 +09:00
61840fc108 build: ignore the stress test binary 2026-08-16 15:45:18 +09:00
c1971c8096 fix(popup): accept depth-24 roots regardless of colormap precision
bits_per_rgb_value is the colormap component precision, not the pixel
layout; the NVIDIA driver reports 11 for its ordinary x8r8g8b8 visuals,
which left the popup disabled there. Depth, class, masks, byte order and
the pixmap format already pin the layout that putimage writes.
2026-08-16 15:45:09 +09:00
7047c57d29 fix(gtk): unset cache override during install 2026-08-15 16:10:34 +09:00
a1ef8a4744 fix(frontends): restart preedit around commits 2026-08-15 15:42:35 +09:00
67ed2ef478 fix(engine): redraw only for visible caret changes 2026-08-15 15:07:13 +09:00
f489c56fb1 fix(ipc): retry nonblocking I/O until deadline 2026-08-15 15:07:07 +09:00
d29d43f443 fix(frontends): preserve preedit lifecycle 2026-08-15 15:05:49 +09:00
aeb6010f3c fix(runtime): restart daemon in background 2026-08-15 00:09:15 +09:00
f0abace40b fix(gtk): install prebuilt modules without development files 2026-08-14 23:54:41 +09:00
7468d573ec test: make container Valgrind runnable 2026-08-14 23:25:07 +09:00
133cc97e55 fix(runtime): make startup and GTK installation explicit 2026-08-14 23:17:18 +09:00
516ec98a70 test: separate quick, live, and stress checks 2026-08-14 23:13:32 +09:00
bfe83d0119 fix(data): reject partial and malformed map data 2026-08-14 23:09:55 +09:00
38475318db fix(popup): fit the current X11 monitor 2026-08-14 22:54:15 +09:00
59482d00f3 fix(engine): compare logical transition state
Avoid reading inactive Str storage when deciding whether to publish a redraw. Keep the focused engine suite compact by retaining one case for each page, navigation, and empty-result invariant.
2026-08-14 22:46:42 +09:00
8df43cecd8 fix(ipc): reject unconnected backlog clients 2026-08-14 22:46:29 +09:00
a637f457f7 fix(xim): honor reset and X11 keyboard state 2026-08-14 22:34:37 +09:00
9804c1f55c fix(gtk): restore compose fallback and clean lifecycle 2026-08-14 22:29:14 +09:00
534ddcd9bb fix(ipc): preserve legacy preedit and bound client waits 2026-08-14 22:17:59 +09:00
5dc2530951 fix(engine): make composition transitions deterministic 2026-08-14 22:13:16 +09:00
f2fb71cd89 fix(ibus): make the X11 frontend fail truthfully 2026-08-14 22:10:40 +09:00
77c6dd66ca test: make clean checks trustworthy 2026-08-14 22:06:54 +09:00
0471d58dec popup: stabilize the input panel width 2026-08-14 21:13:49 +09:00
f222b4d573 popup: refine the input panel 2026-08-14 20:54:09 +09:00
3576c4ea68 tests: cover frontend input-panel protocols 2026-08-14 20:09:49 +09:00
e4fc327ffd popup: show the candidate page 2026-08-14 20:06:53 +09:00
193ed009bd popup: place and bound the input panel 2026-08-14 20:00:36 +09:00
7619d2bd9d ibus: support modern client preedit 2026-08-14 19:59:52 +09:00
a841894015 ibus: honor preedit and private-input policy 2026-08-14 19:41:07 +09:00
4cac6f1a15 engine: use stable candidate pages 2026-08-14 19:27:22 +09:00
2e6d7d8e48 xim: clear callback preedit on owner loss 2026-08-14 19:17:37 +09:00
76f9e1a21f gtk: honor preedit policy and cursor placement 2026-08-14 19:12:53 +09:00
6a58b63223 ipc: negotiate preedit capability and caret messages 2026-08-14 18:53:14 +09:00
b536573622 engine: update active preedit capability 2026-08-14 18:44:52 +09:00
9a3a92170e engine: reject stale dictionary results 2026-08-14 18:43:12 +09:00
76af1a022f xim: refresh placement before each key 2026-08-14 17:43:56 +09:00
68dc1e6a18 docs: describe frontend preedit behavior 2026-08-14 17:35:57 +09:00
e6bf32b0f6 xim: place popup from input context 2026-08-14 17:35:33 +09:00
2b4c509d0d xim: add callback preedit and UTF-8 negotiation 2026-08-14 17:20:38 +09:00
2376035524 engine: route popup preedit by client capability 2026-08-14 17:02:46 +09:00
c1b1b7016e docs: remove historical provenance files 2026-08-14 16:20:26 +09:00
4eba8c2904 tests: cover in-process XIM lifecycle 2026-08-14 16:17:27 +09:00
f5dc552c08 build: use configured include paths 2026-08-14 16:17:07 +09:00
4e07699460 xim: run server inside strans 2026-08-14 16:16:27 +09:00
08369e718e popup: use system fonts exclusively 2026-08-14 15:17:00 +09:00
54355b8190 popup: replace FreeType renderer with PangoCairo 2026-08-14 14:37:30 +09:00
279b9230a4 docs: simplify README 2026-08-14 13:43:45 +09:00
c2cb18cbdd run: return after background startup 2026-08-14 13:39:07 +09:00
24e380651c run: replace only owned launcher instance 2026-08-14 03:30:29 +09:00
c4b0141413 build: avoid xkbcommon in D-Bus-only tests 2026-08-14 02:52:10 +09:00
46c8ccd779 tests: bound generator subprocesses 2026-08-14 02:50:58 +09:00
d7115c5eb5 tests: close inherited descriptors before exec 2026-08-14 02:49:57 +09:00
f395f29918 ibus: complete setup before publication 2026-08-14 02:48:49 +09:00
6a2b2bccc4 run: make launcher ownership explicit 2026-08-14 02:43:42 +09:00
05ff88c74f run: use installed default fonts 2026-08-14 02:10:45 +09:00
6c0a533c4e ibus: fall back from unreadable machine id 2026-08-14 02:10:16 +09:00
e44fc005da tests: cover stale daemon endpoint recovery 2026-08-14 01:54:25 +09:00
469278209f tests: verify failed startup publishes no endpoints 2026-08-14 01:29:00 +09:00
9569c2eaaf tests: cover daemon endpoint collision ownership 2026-08-14 01:03:13 +09:00
335783ebe5 daemon: claim ipc endpoint before ibus publication 2026-08-14 01:02:17 +09:00
0f8396915b tests: cover ibus connection capacity recovery 2026-08-14 00:44:49 +09:00
91b3042e66 ibus: provision connection watch capacity 2026-08-14 00:44:16 +09:00
b44663096f tests: verify live daemon cleanup 2026-08-14 00:11:03 +09:00
685d78bab5 tests: keep headless environments minimal 2026-08-14 00:04:32 +09:00
f8ebfcdc86 tests: cover ipc listener capacity and recovery 2026-08-14 00:02:23 +09:00
f399eb0af6 tests: cover live ibus transport lifecycle 2026-08-13 23:55:27 +09:00
88d26c598d renderer: remove obsolete candidate offset 2026-08-13 23:21:59 +09:00
9f4f9ceb14 ibus: cover context ownership through the engine 2026-08-13 23:21:12 +09:00
bb2a970db1 tests: cover ownership across ipc connections 2026-08-13 23:07:25 +09:00
21a94d447c server: remove client ownership wrapper 2026-08-13 22:09:54 +09:00
367cbb6cc3 ibus: make context release explicit 2026-08-13 22:03:27 +09:00
b6a9ccd9d9 cleanup: simplify frontend request ownership 2026-08-13 21:57:32 +09:00
13f0b436b8 headers: make server key reader private 2026-08-13 21:26:56 +09:00
c07673ad4c build: list daemon sources explicitly 2026-08-13 21:26:00 +09:00
e01a991de4 remove: drop Wayland frontend 2026-08-13 21:25:12 +09:00
fffbb9d8de fonts: remove bundled binary payloads 2026-08-13 20:40:50 +09:00
f46d51b539 renderer: use direct FreeType with explicit font files 2026-08-13 20:40:38 +09:00
125e9349c0 fix: pass empty search backspace through 2026-08-12 21:35:33 +09:00
768ac3de74 hanja: force UTF-8 filter streams 2026-08-12 21:23:59 +09:00
136695a900 hanja: separate temporary search mode 2026-08-12 21:23:10 +09:00
4e1c15dde4 hanja: add single-character dictionary data 2026-08-12 19:29:11 +09:00
0216cc3c8a hanja: add one-shot Korean search 2026-08-12 19:19:26 +09:00
e69883d0ad build: restore the single-instance launcher 2026-08-12 18:32:18 +09:00
9051c69b49 build: install GTK without development files 2026-08-12 18:01:39 +09:00
9566995c07 fix: separate Bash import streams 2026-08-12 17:42:25 +09:00
1f9af6ee3e fix: reject unsupported SKK expressions 2026-08-12 17:39:09 +09:00
bae39c0188 build: force Docker-owned compilation 2026-08-12 17:34:48 +09:00
4e90fdd5c7 fix: harden caret and IBus dispatch 2026-08-12 17:34:34 +09:00
de536da0bb refactor: trim popup and XIM helpers 2026-08-12 17:33:14 +09:00
8d68e00f2b data: simplify and validate Japanese imports 2026-08-12 17:32:28 +09:00
a8a6c3b455 fix: construct Plan 9 paths without truncation 2026-08-12 17:28:17 +09:00
0d06ba43cd refactor: remove conditional compilation layers 2026-08-12 17:27:42 +09:00
91931196e2 refactor: simplify the Plan 9 runtime loop 2026-08-12 17:26:55 +09:00
211a6917b2 refactor: remove guarded helper headers 2026-08-12 16:45:27 +09:00
4e8720a40d fix: terminate XIM UTF-8 text runs 2026-08-12 16:43:57 +09:00
71af269efe fix: preserve emoji commits through XIM 2026-08-12 16:40:58 +09:00
3f5e107230 docs: record data provenance and runtime constraints 2026-08-12 16:24:22 +09:00
c41a1b4227 build: remove generated artifacts and unsafe scripts 2026-08-12 16:23:45 +09:00
368e6adcd4 fix: keep the headless daemon running 2026-08-12 16:22:58 +09:00
5927f6a5e1 fix: forward input at the composition limit 2026-08-12 16:21:33 +09:00
9d60c4bbb0 fix: release XIM ownership on reset 2026-08-12 16:20:47 +09:00
c7d4f16d3e test: stress engine transitions 2026-08-12 16:16:25 +09:00
a03efcf6bf test: cover IPC response framing 2026-08-12 16:11:22 +09:00
432df3730c fix: harden the per-user IPC endpoint 2026-08-12 16:08:23 +09:00
4120f90736 fix: make popup rendering deterministic 2026-08-12 15:59:52 +09:00
9c43bcb75c fix: repair Wayland key lifecycle 2026-08-12 15:58:24 +09:00
1d8d711882 build: install the GTK module predictably 2026-08-12 15:57:18 +09:00
1295480858 fix: repair XIM context lifecycle 2026-08-12 15:56:43 +09:00
103f2ec448 fix: harden small core containers 2026-08-12 15:54:16 +09:00
cec62cc40f fix: repair IBus context lifecycle 2026-08-12 15:49:28 +09:00
bc5712b6ac fix: retain the active owner's caret 2026-08-12 15:44:04 +09:00
5ba5cf209c fix: assign one active engine owner 2026-08-12 15:41:46 +09:00
9c244d1dd1 fix: retain the newest dictionary request 2026-08-12 15:37:50 +09:00
fabce14933 fix: tighten emoji lookup contracts 2026-08-12 15:36:37 +09:00
d0def814ef refactor: remove empty language maps 2026-08-12 15:35:02 +09:00
d2fa51ceba data: validate and repair Japanese dictionaries 2026-08-12 15:34:00 +09:00
f0ba7fc5f6 fix: normalize Korean shifted input 2026-08-12 15:32:28 +09:00
6dfbc08704 fix: correct Katakana composition 2026-08-12 15:29:35 +09:00
ec7e6eb992 fix: preserve complete Japanese readings 2026-08-12 15:28:16 +09:00
5ae0945015 Simplify build system 2026-08-12 14:23:41 +09:00
234 changed files with 495339 additions and 171621 deletions

76
.gitignore vendored
View File

@@ -1,59 +1,21 @@
strans /*.o
strans-xim /strans
/imv2.c
/imv2.h
/vkv1.c
/vkv1.h
/tests/*.o
/tests/unit_test /tests/unit_test
/tests/stress_test
/tests/ibus_live_test
/tests/ibus_client_smoke
/tests/gtk_live_test
/tests/xim_live_test
/tests/ipc_live_test
/tests/daemon_collision_test
/tests/daemon_failure_test
/tests/daemon_restart_test
/gtk/im-strans.so
/bench/bench /bench/bench
/bench/perf.data*
# Prerequisites /ref-*/
*.d
# Object files
*.o
*.ko
*.obj
*.elf
# Linker output
*.ilk
*.exp
# Precompiled Headers
*.gch
*.pch
# Libraries
*.lib
*.a
*.la
*.lo
# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib
# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex
# Debug files
*.dSYM/
*.su
*.idb
*.pdb
# Kernel Module Compile Results
*.mod*
*.cmd
.tmp_versions/
modules.order
Module.symvers
Mkfile.old
dkms.conf
# debug information files
*.dwo

50
Dockerfile Normal file
View File

@@ -0,0 +1,50 @@
FROM archlinux:base@sha256:b0deabeb3d283da2c7f7dbf0eea051b7b2cd0554e0b737cc457fd21683bdcdd1
RUN pacman -Syu --noconfirm --needed \
cairo \
fontconfig \
diffutils \
gcc \
gtk3 \
ibus \
harfbuzz \
libx11 \
libxcb \
libxkbcommon \
make \
noto-fonts-emoji \
pango \
pkgconf \
plan9port \
python \
ttf-dejavu \
ttf-jigmo \
valgrind \
wayland \
which \
xorg-server-xvfb \
xcb-imdkit \
xcb-util \
dbus \
&& pacman -Scc --noconfirm
RUN export DEBUGINFOD_URLS=https://debuginfod.archlinux.org \
&& buildid="$(readelf -n /usr/lib/ld-linux-x86-64.so.2 | sed -n 's/.*Build ID: //p')" \
&& test -n "$buildid" \
&& debugdir="/usr/lib/debug/.build-id/$(printf '%.2s' "$buildid")" \
&& mkdir -p "$debugdir" \
&& install -m 0644 "$(debuginfod-find debuginfo /usr/lib/ld-linux-x86-64.so.2)" \
"$debugdir/${buildid#??}.debug" \
&& rm -rf /root/.cache/debuginfod_client
RUN dbus-uuidgen --ensure=/etc/machine-id
ENV PLAN9=/usr/lib/plan9
ENV PATH=$PATH:/usr/lib/plan9/bin
# 9c and 9l filter their own output through $egrep, which plan9port never
# assigns, so the pipeline dies and takes every warning and error with it.
# Both source $PLAN9/config, which the distribution does not ship.
RUN echo 'egrep="grep -E"' > $PLAN9/config
WORKDIR /src

View File

@@ -0,0 +1,26 @@
Copyright (c) 2005,2006 Choe Hwanjin
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the author nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

17
LICENSES/README.md Normal file
View File

@@ -0,0 +1,17 @@
# License inventory
- `BSD-3-Clause-libhangul-hanja.txt` contains the terms for the Hanja data
in `map/hanja.src` and the symbol data in `map/mssymbol.src`, and for the
generated `map/hanja.dict` they share, derived from libhangul's
`data/hanja/hanja.txt` and `data/hanja/mssymbol.txt`.
- `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
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.
- The two protocol descriptions in `proto/` are vendored verbatim from
wlroots `012ca825` and carry their own MIT terms inside the XML.
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.

120
Makefile
View File

@@ -1,48 +1,118 @@
CC = 9c CC = 9c
LD = 9l LD = 9l
PKGGOALS := $(filter-out check test verify-map clean xim bench,$(MAKECMDGOALS)) PKG_CONFIG ?= pkg-config
ifeq ($(MAKECMDGOALS),) SCANNER ?= wayland-scanner
PKGGOALS := all CFLAGS ?= -O2 -g
endif WARN_CFLAGS = -Wall -Wextra
ifneq ($(PKGGOALS),) IBUS_CFLAGS = $(shell $(PKG_CONFIG) --cflags dbus-1 xkbcommon)
DBUS_CFLAGS := $(shell pkg-config --cflags dbus-1) IBUS_LIBS = $(shell $(PKG_CONFIG) --libs dbus-1 xkbcommon)
DBUS_LIBS := $(shell pkg-config --libs dbus-1) XIM_CFLAGS = $(shell $(PKG_CONFIG) --cflags xcb-imdkit xcb-aux xcb-xkb xkbcommon-x11)
WL_CFLAGS := $(shell pkg-config --cflags wayland-client xkbcommon) XIM_LIBS = $(shell $(PKG_CONFIG) --libs xcb-imdkit xcb-aux xcb-xkb xkbcommon-x11)
WL_LIBS := $(shell pkg-config --libs wayland-client xkbcommon) TEXT_CFLAGS = $(shell $(PKG_CONFIG) --cflags pangocairo cairo fontconfig)
endif TEXT_LIBS = $(shell $(PKG_CONFIG) --libs pangocairo cairo fontconfig)
CFLAGS = -Wall -Wextra -O2 -g $(DBUS_CFLAGS) $(WL_CFLAGS) POPUP_CFLAGS = $(shell $(PKG_CONFIG) --cflags xcb-aux xcb-randr)
POPUP_LIBS = $(shell $(PKG_CONFIG) --libs xcb-aux xcb-randr)
WL_CFLAGS = $(shell $(PKG_CONFIG) --cflags wayland-client xkbcommon)
WL_LIBS = $(shell $(PKG_CONFIG) --libs wayland-client xkbcommon)
PROJECT_CPPFLAGS =
PROJECT_LDLIBS = -lthread -lbio -lxcb $(IBUS_LIBS) $(TEXT_LIBS) \
$(XIM_LIBS) $(POPUP_LIBS) $(WL_LIBS)
PROG = strans PROG = strans
DOCKER_IMAGE = strans-build
DOCKER_RUN = docker run --rm --user "$$(id -u):$$(id -g)" \
-v "$(CURDIR):/src" $(DOCKER_IMAGE)
SRCS = $(wildcard *.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 wl.c xim.c
OBJS = $(SRCS:.c=.o) OBJS = $(SRCS:.c=.o)
# wayland-scanner writes these from the XML in proto/; only the XML is tracked.
PROTO = imv2.c imv2.h vkv1.c vkv1.h
PROTOOBJ = imv2.o vkv1.o
all: $(PROG) xim bench all: $(PROG) gtk
$(PROG): $(OBJS) $(PROG): $(OBJS) $(PROTOOBJ)
$(LD) -o $@ $(OBJS) -lthread -lString -lbio -lxcb -lm $(DBUS_LIBS) $(WL_LIBS) $(LD) $(LDFLAGS) -o $@ $(OBJS) $(PROTOOBJ) $(PROJECT_LDLIBS) $(LDLIBS)
$(OBJS): dat.h fn.h ipc.h $(OBJS): dat.h fn.h ipc.h
wl.o: imv2.h vkv1.h
compose.o ibus.o: PROJECT_CPPFLAGS += $(IBUS_CFLAGS)
font.o: PROJECT_CPPFLAGS += $(TEXT_CFLAGS)
win.o: PROJECT_CPPFLAGS += $(POPUP_CFLAGS)
wl.o $(PROTOOBJ): PROJECT_CPPFLAGS += $(WL_CFLAGS)
xim.o: PROJECT_CPPFLAGS += $(XIM_CFLAGS)
%.o: %.c
$(CC) $(CPPFLAGS) $(PROJECT_CPPFLAGS) $(WARN_CFLAGS) $(CFLAGS) -c -o $@ $<
imv2.h: proto/input-method-unstable-v2.xml
$(SCANNER) client-header $< $@
imv2.c: proto/input-method-unstable-v2.xml
$(SCANNER) private-code $< $@
vkv1.h: proto/virtual-keyboard-unstable-v1.xml
$(SCANNER) client-header $< $@
vkv1.c: proto/virtual-keyboard-unstable-v1.xml
$(SCANNER) private-code $< $@
clean: clean:
rm -f $(OBJS) $(PROG) rm -f $(OBJS) $(PROG) $(PROTO) $(PROTOOBJ)
$(MAKE) -C tests clean $(MAKE) -C tests clean
$(MAKE) -C xim/ clean $(MAKE) -C gtk/ clean
$(MAKE) -C bench/ clean $(MAKE) -C bench/ clean
xim: gtk:
$(MAKE) -C xim/ $(MAKE) -C gtk/
bench: bench:
$(MAKE) -C bench/ $(MAKE) -C bench/
check: verify-map test docker-image: Dockerfile
docker build -t $(DOCKER_IMAGE) - < Dockerfile
test: docker-build:
$(MAKE) -C tests check TESTARGS="$(TESTARGS)" $(DOCKER_RUN) sh -c 'make clean && make all'
docker-check:
$(DOCKER_RUN) sh -c 'make clean && \
make check check-live check-stress UNITARGS="$(UNITARGS)"'
docker-check-live:
$(DOCKER_RUN) sh -c 'make clean && make check-live'
docker-check-stress:
$(DOCKER_RUN) sh -c 'make clean && make check-stress'
docker-bench:
$(DOCKER_RUN) sh -c 'make clean && make bench'
docker-valgrind:
$(DOCKER_RUN) sh -c 'make clean && make -C tests unit_test && \
cd tests && valgrind -q --error-exitcode=1 ./unit_test $(UNITARGS)'
docker: docker-image
$(MAKE) docker-build
check: verify-map
$(MAKE) -C tests check UNITARGS="$(UNITARGS)"
check-live: $(PROG) gtk
$(MAKE) -C tests check-live
check-stress: $(PROG)
$(MAKE) -C tests check-stress
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 tests/mkemoji_test.py python3 map/mkhanja | cmp - map/hanja.dict
python3 map/verifymap.py map/*.map map/*.dict
python3 tests/mkemoji_test.py
python3 tests/mkhanja_test.py
python3 tests/skk2ktrans_test.py
.PHONY: all check test verify-map clean xim 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-check-stress docker-bench docker-valgrind

277
README.md
View File

@@ -1,135 +1,194 @@
# strans # strans
An input method daemon for CJK text entry on X11 and Wayland. strans is a small, single-user input method for Korean, Japanese, English,
emoji and symbols, with Vietnamese Telex as a compatibility mode. One
engine serves four frontends: `zwp_input_method_v2` on Wayland — sway and
any other compositor that offers it — IBus, which GTK 4 and Qt use, XIM,
and a GTK 3 module.
Inspired by 9front's ktrans. Threads communicate via CSP channels. ## Modes
## Dependencies | Key | Mode |
| --- | --- |
| `Ctrl+S` | Korean Hangul (2-beolsik) |
| `Ctrl+N` | Japanese Hiragana, converting to Kanji |
| `Ctrl+K` | Japanese Katakana |
| `Ctrl+T` | English |
| `Ctrl+V` | Vietnamese Telex |
| `Ctrl+E` | Emoji and symbol search |
| `Ctrl+H` | One-shot Hanja and symbol search |
- plan9port Only a plain `Ctrl` chord is a strans key: with `Shift`, `Alt` or `Super`
- Python 3 (for generated-map checks) held it commits what is pending and goes to the application, so
- dbus-1 `Ctrl+Shift+V` still pastes. `Backspace`, `Enter`, `Tab`, `Esc` and the
- wayland-client, libxkbcommon (for Wayland support) arrow and page keys do the same under `Ctrl`, so `Ctrl+Backspace` still
- gtk+-3.0 (optional, for GTK IM module) deletes a word. The mode switched to — `한`, `あ`, `ア`, `ă`, `A` — shows
in the popup until the next key. A Korean keyboard's 한/영 and 한자 keys
stand for `Ctrl+S`/`Ctrl+T` and `Ctrl+H`; a Japanese one's 半角/全角,
ひらがな/カタカナ, 変換 and 無変換 for the kana modes, `Space` and English.
## Build ## Composing
make Japanese composes a whole reading and then offers its Kanji with none
cd xim && make # XIM adapter chosen. `Space` and `Tab` step through the candidates and wrap (`Shift`
cd gtk && make docker && make install # GTK IM module reverses), the reading in Katakana last, so a word the dictionary lacks
converts with one `Space`; `Up`/`Down` and `PageUp`/`PageDown` move without
wrapping. A chosen candidate takes the reading's place in the preedit, so
it stands where it will land, and `Enter` commits it, or the reading, as
`0` and typing on always do. Once a candidate is chosen `1`-`9` take that
row of the page shown; before that the digits type. `Backspace` deletes
the last kana shown, and romaji that made no kana stays as typed until it
is mended; `Esc` cancels the reading, and on a chosen candidate both go
back to it. The dictionary keys verbs and adjectives by stem and
okurigana, so `kaku` offers かく's readings and then 書く and its kin —
typing the okurigana with `Shift`, as SKK does, `kaKu`, asks for that
split first. Katakana mode does not convert, so there `Space` and `Tab`
commit the reading and go on to the application.
Unit tests use the sibling `cutest` Git repository as a submodule and do not Korean composes one syllable at a time. `Enter`, `Tab`, `Esc` and any key
require the Wayland, D-Bus, GTK, or X11 development packages. Repository that is not a jamo commit the syllable and go on to the application, so
mirrors must provide the same sibling; initialize it after cloning with `Esc` still leaves insert mode. Two lone consonants join into the compound
`git submodule update --init`: final they make (rt → ㄳ), and a vowel typed before its consonant is
reordered under it (kr → 가), as in libhangul.
make check # generated map + unit tests A search takes the keys until `Enter`, `Space` or `1`-`9` picks a result or
make test TESTARGS=hangul # filtered unit tests only `Esc` cancels, then returns to the previous mode. Emoji matches the typed
make verify-map # generated-map check only keys, and what they spell in the current mode, against a prefix of every
emoji's CLDR name and keywords in English, Korean and Japanese, and against
ASCII aliases such as `->` and `<=`. `Ctrl+H` takes the syllable being
composed as its query and composes on from it, converting a word as well as
a syllable — 한자 gives 漢字, 대한민국 gives 大韓民國 — and `Esc` gives the
syllable back. A word is committed a syllable at a time, so the reading
also reaches back into the text the application already holds, as far as
the dictionary knows the whole of it: type 한자 and then `Ctrl+H` and the
query is 한자, not 자, and picking 漢字 takes the 한 back. `Backspace`
gives that reach back before it deletes what was typed, so 상 written
with 태 typed answers 狀態, and one `Backspace` answers 態 for the
syllable alone. A reading is also the start of the longer words it
begins, whose conversions follow its own, so 대한 answers 大寒 first and
大韓民國 further down. A lone consonant is a reading too, and answers
with the symbol table a Korean keyboard's 한자 key has always offered:
ㅁ gives ※ ○ △ ㈜, ㄴ the brackets 「」『』, ㄹ the units ℃ ㎏ , ㅇ the
circled numbers ①②③.
The suite covers the fixed-size UTF-8 string, hash table, trie fixtures, Reaching back needs the application to hand over the text around its
Japanese/Hangul/Telex state transitions, dictionary candidates, engine cursor and to take some of it away again. The Wayland input method, IBus
selection state, IPC request fields, and production map loading. Tests use and the GTK 3 module all can; XIM has no such request, so there the
explicit case tables, boundary values, and small regressions for repaired core reading is what is still being composed and nothing else, and an IBus
contracts. Full IPC and GUI protocol stacks require separate integration checks. client that reads a key's effects only after the call returns is not
asked for its text either, since a deletion could not be ordered before
the commit.
Dead keys and Compose sequences are composed by strans itself for the
Wayland, XIM and IBus frontends, from `XCOMPOSEFILE` or the locale; the
GTK 3 module leaves them to GtkIMContextSimple.
## Preedit and candidates
| Frontend | Preedit | Candidates | Reaches back |
| --- | --- | --- | --- |
| Wayland input method | inline in the client | popup | yes |
| GTK 3 and IBus | inline in the client | popup | yes |
| XIM PreeditCallbacks | inline through XIM callbacks | popup | no |
| XIM PreeditPosition, PreeditNothing | popup | popup | no |
On Wayland the popup is a surface the compositor places at the text cursor,
flipping it above the line when there is no room below. Elsewhere it is an
X11 window, placed from the XIM spot or from the GTK caret, and XIM text
travels as `COMPOUND_TEXT`. Either way `GDK_SCALE` sizes it for HiDPI
displays.
## Build and test
```sh
git submodule update --init
make docker-image
make docker-build
```
That leaves `strans`, the daemon, and `gtk/im-strans.so`, the GTK 3 module.
Tests come in three tiers:
```sh
make check # generated-map validation and unit tests
make check-live # one IBus, GTK, XIM and IPC daemon smoke each
make check-stress # randomized, capacity, collision, failure and restart
```
`make docker-check` runs all three in the container, and
`make docker-valgrind` the unit suite under Valgrind; rerun
`make docker-image` after changing `Dockerfile`. `UNITARGS=hangul` filters
the unit suite alone, not the other tiers.
A native build wants a C toolchain, Make, pkg-config and Plan 9 port, plus
development files for D-Bus, XCB and xcb-imdkit, Wayland and
`wayland-scanner`, xkbcommon, Pango and Cairo, and GTK 3; the tests also
want Xvfb, Python 3 and fonts covering Latin, CJK and emoji.
[`Dockerfile`](Dockerfile) names the exact packages.
## Run ## Run
./strans map font & ```sh
./run.sh # restart the daemon in the background
./strans map # or run it in the foreground
```
For XIM apps: `./strans DIR` reads `hira.map`, `kata.map`, `telex.map`, `kanji.dict`,
`emoji.dict` and `hanja.dict` from `DIR` at runtime; nothing but the GTK
module is installed. A service supervisor wants the session's
`XDG_RUNTIME_DIR` and its `WAYLAND_DISPLAY` or `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.
./xim/strans-xim & strans shows one popup per session, so it picks a frontend at startup: a
XMODIFIERS=@im=strans xterm compositor offering `zwp_input_method_v2` gets the Wayland frontend, and
then neither XIM nor the X11 popup runs; every other session gets XIM. The
IBus endpoint and the GTK 3 module are served in both.
For GTK apps: On Wayland, then, set none of the variables below. GTK, Qt and Firefox
speak text-input-v3 themselves — GTK 4 binds it with `GTK_IM_MODULE` unset
— and setting these is what pushes them off the path that works: such a
client still composes inline but shows no candidates, because the popup
belongs to the Wayland frontend. Chromium is beyond help either way, since
it asks for `text-input-v1`, which wlroots does not implement.
GTK_IM_MODULE=strans gedit ```sh
export XMODIFIERS=@im=strans # XIM
doas make -C gtk install # GTK 3 module
export GTK_IM_MODULE=strans
GLFW_IM_MODULE=ibus kitty # IBus client example
```
For IBus apps (kitty, foot, etc.): strans provides its own IBus endpoint, so `ibus-daemon` and fcitx are not
needed. Building the GTK module wants GTK development files; installing
one built elsewhere does not. The install target takes `GTK_MODULE_DIR`
when set, else asks GTK where its modules live; `doas make -C gtk uninstall`
removes it. With neither `WAYLAND_DISPLAY` nor `DISPLAY` the daemon still
serves IBus and its socket, but nothing draws a popup.
GLFW_IM_MODULE=ibus kitty ## Dictionary data
For Wayland apps (text-input-v3 clients on wlroots compositors): After changing an input source, regenerate and verify:
# nothing to set; the compositor relays text-input-v3 to strans ```sh
python3 map/mktelex.py >map/telex.map
map/mkemoji >map/emoji.dict
map/mkhanja >map/hanja.dict
make verify-map
```
Strans itself is the IBus endpoint and the Wayland input-method-v2 client; [`map/README`](map/README) says where the emoji, Hanja and Japanese data
no ibus-daemon or fcitx5 needed. Start strans after the compositor. comes from.
## Usage ## Benchmark
Switch input modes with Ctrl + key: `make bench` builds `bench/bench`. With the daemon stopped, `./bench.sh`
starts one, warms it up and records the workload with Linux `perf` into
`bench/perf.data`.
N Hiragana ## Licensing
K Katakana
S Hangul
T English
V Vietnamese (Telex)
P Toggle preedit echo
Type romanized input. Select candidates with 1-9 or arrow keys. Third-party notices are in [`LICENSES`](LICENSES). The repository declares
Outside one-shot search, Tab or Enter commits. no license for the strans source as a whole.
### Emoji and symbols
Ctrl+E starts a one-shot symbol search; it is not a mode. Any current preedit
is committed, then the previous language is kept for the next input. Type an
English alias or the phonetic keys of the current layout (Korean aliases work,
for example). The candidate list is shown even when Ctrl+P has hidden normal
preedit echo.
안녕 → Ctrl+E → smile → Enter → 😀
There is no auto-insert. Enter commits the first candidate (or the current
one); arrows and Tab move, and Esc cancels. Bare 1 through 9 select an existing
numbered row; a bare digit with no such row and 0 remain query text. Digit
aliases are kept in matching slots, so `^1`, `_2`, and `<3` produce `¹`, `₂`,
and `♥`.
`map/emoji.src` is the source of truth: each UTF-8 TSV row is a result followed
by one or more tab-separated aliases. Aliases may be multilingual and ASCII
letters are case-insensitive. Results and aliases are limited to 64 Unicode
code points; results cannot contain whitespace. Run `map/mkemoji > map/emoji.dict`
after editing it; `make verify-map` checks the generated dictionary and the
Telex map.
😀 웃음 웃다 스마일 smile grin
## Architecture
Threads communicate via CSP channels:
- [imthread](strans.c): keystroke processing, transliteration
- [dictthread](dict.c): dictionary lookup
- [drawthread](win.c): preedit window rendering
- [srvthread](srv.c): IPC via unix socket
- [ibusthread](ibus.c): IBus D-Bus endpoint for GLFW/kitty
- [waylandthread](wayland.c): Wayland input-method-v2 + virtual-keyboard-v1
Adapters (strans-xim, im-strans.so) bridge X11/GTK events.
## Files
strans.c input method engine
dict.c dictionary queries
win.c xcb window management
font.c truetype rendering (stb_truetype)
wayland.c wayland input-method-v2 adapter
map/ transliteration tables
font/ bundled CJK fonts
input-method-unstable-v2-*.{c,h} wayland-scanner output, vendored
virtual-keyboard-unstable-v1-*.{c,h}
The two Wayland protocols (input-method-v2, virtual-keyboard-v1) are
wlroots-only and not in the upstream wayland-protocols package, so the
generated client code is vendored. Regenerate with wayland-scanner if
the XML upstream ever changes (it hasn't in years).
## References
- https://git.9front.org/plan9front/plan9front/HEAD/info.html
- https://np.mkv.li/kor/9front-ktrans-%ED%95%9C%EA%B8%80-%EB%98%91%EB%B0%94%EB%A1%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0/
- https://en.wikipedia.org/wiki/Telex_(input_method)
- https://gist.github.com/hieuthi/0f5adb7d3f79e7fb67e0e499004bf558

View File

@@ -1,23 +1,71 @@
#!/bin/sh #!/bin/sh
pkill strans set -eu
pkill strans-xim
sleep 1
./strans map font & cd "$(dirname "$0")" || exit 1
sleep 1
# warm up glyph cache if test "$#" -ne 0; then
echo "usage: bench.sh" >&2
exit 1
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=
perf_pid=
cleanup()
{
status=$?
trap - 0 1 2 15
if test -n "$perf_pid"; then
kill -INT "$perf_pid" 2>/dev/null || :
wait "$perf_pid" 2>/dev/null || :
fi
if test -n "$strans_pid"; then
kill "$strans_pid" 2>/dev/null || :
wait "$strans_pid" 2>/dev/null || :
fi
exit "$status"
}
trap cleanup 0
trap 'exit 129' 1
trap 'exit 130' 2
trap 'exit 143' 15
./strans map &
strans_pid=$!
# The daemon is ready once the benchmark client can complete a round.
attempt=0
until ./bench/bench bench/bench.keys 1 >/dev/null 2>&1; do
if ! kill -0 "$strans_pid" 2>/dev/null; then
echo "bench.sh: daemon exited before accepting IPC clients" >&2
exit 1
fi
attempt=$((attempt + 1))
if test "$attempt" -ge 50; then
echo "bench.sh: daemon did not accept IPC clients within 10 seconds" >&2
exit 1
fi
sleep 0.2
done
# warm up the renderer
./bench/bench bench/bench.keys 100 ./bench/bench bench/bench.keys 100
echo "cache warmed up" echo "renderer warmed up"
STRANS_PID=$(pgrep -x strans) perf record -g -o bench/perf.data -p "$strans_pid" &
perf record -g -o bench/perf.data -p "$STRANS_PID" & perf_pid=$!
PERF_PID=$!
sleep 1 sleep 1
kill -0 "$perf_pid"
./bench/bench bench/bench.keys 100 ./bench/bench bench/bench.keys 100
kill -INT $PERF_PID kill -INT "$perf_pid"
wait $PERF_PID 2>/dev/null wait "$perf_pid" 2>/dev/null || :
pkill strans perf_pid=

View File

@@ -1,8 +1,9 @@
CC = cc CC = cc
CFLAGS = -Wall -O2 CFLAGS ?= -O2
bench: main.c ../ipc.c ../ipc.h bench: main.c ../ipc.c ../ipc.h
$(CC) $(CFLAGS) -I.. -o $@ main.c ../ipc.c $(CC) $(CPPFLAGS) -I.. -Wall $(CFLAGS) $(LDFLAGS) -o $@ \
main.c ../ipc.c $(LDLIBS)
clean: clean:
rm -f bench rm -f bench

View File

@@ -1,12 +1,13 @@
#define _POSIX_C_SOURCE 200809L #define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <unistd.h> #include <unistd.h>
#include <time.h> #include <time.h>
#include <sys/socket.h> #include "ipc.h"
#include <sys/un.h>
#include "../ipc.h"
typedef struct Key Key; typedef struct Key Key;
struct Key { struct Key {
@@ -15,7 +16,7 @@ struct Key {
}; };
static Key *keys; static Key *keys;
static int nkeys; static size_t nkeys;
static int fd; static int fd;
static void static void
@@ -28,13 +29,19 @@ die(char *msg)
static void static void
addkey(int k, int mod) addkey(int k, int mod)
{ {
static int cap; static size_t cap;
Key *p;
if(nkeys >= cap){ if(nkeys >= cap){
if(cap > SIZE_MAX / 2 / sizeof(Key)){
fprintf(stderr, "too many keys\n");
exit(1);
}
cap = cap ? cap * 2 : 256; cap = cap ? cap * 2 : 256;
keys = realloc(keys, cap * sizeof(Key)); p = realloc(keys, cap * sizeof(Key));
if(!keys) if(!p)
die("realloc"); die("realloc");
keys = p;
} }
keys[nkeys].k = k; keys[nkeys].k = k;
keys[nkeys].mod = mod; keys[nkeys].mod = mod;
@@ -76,20 +83,35 @@ loadkeys(char *file)
} }
} }
fclose(f); fclose(f);
if(nkeys == 0){
fprintf(stderr, "%s: no keys\n", file);
exit(1);
}
}
static uint64_t
iterations(char *s)
{
uintmax_t n;
char *end;
if(*s == '\0' || *s == '-')
goto Bad;
errno = 0;
n = strtoumax(s, &end, 10);
if(errno != 0 || *end != '\0' || n == 0)
goto Bad;
return n;
Bad:
fprintf(stderr, "invalid iteration count: %s\n", s);
exit(1);
} }
static void static void
dial(void) dial(void)
{ {
struct sockaddr_un addr; fd = ipcconnect();
fd = socket(AF_UNIX, SOCK_STREAM, 0);
if(fd < 0) if(fd < 0)
die("socket");
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
snprintf(addr.sun_path, sizeof(addr.sun_path), IPCPATH, getuid());
if(connect(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0)
die("connect"); die("connect");
} }
@@ -106,10 +128,10 @@ sendkey(int key, int mod)
static int static int
readresp(void) readresp(void)
{ {
char buf[Ipcfieldmax+1]; char commit[Ipcfieldmax+1], preedit[Ipcfieldmax+1];
Ipcresp resp; Ipcresp resp;
return ipcreadresp(fd, 0, buf, sizeof buf, NULL, 0, &resp); return ipcreadresp(fd, 0, commit, preedit, &resp);
} }
static double static double
@@ -124,31 +146,38 @@ now(void)
int int
main(int argc, char **argv) main(int argc, char **argv)
{ {
int i, j, niter; size_t j;
uint64_t i, niter, total;
double t0, t1, dt; double t0, t1, dt;
if(argc < 2){ if(argc != 2 && argc != 3){
fprintf(stderr, "usage: bench file [niter]\n"); fprintf(stderr, "usage: bench file [niter]\n");
exit(1); exit(1);
} }
loadkeys(argv[1]); loadkeys(argv[1]);
niter = argc > 2 ? atoi(argv[2]) : 1000; niter = argc == 3 ? iterations(argv[2]) : 1000;
if(niter > UINT64_MAX / nkeys){
fprintf(stderr, "iteration count overflows key total\n");
exit(1);
}
total = niter * nkeys;
dial(); dial();
t0 = now(); t0 = now();
for(i = 0; i < niter; i++) for(i = 0; i < niter; i++)
for(j = 0; j < nkeys; j++){ for(j = 0; j < nkeys; j++){
sendkey(keys[j].k, keys[j].mod); sendkey(keys[j].k, keys[j].mod);
if(readresp() < 0){ if(readresp() < 0){
fprintf(stderr, "failed at iter %d key %d\n", i, j); fprintf(stderr, "failed at iter %" PRIu64 " key %zu\n", i, j);
exit(1); exit(1);
} }
} }
t1 = now(); t1 = now();
dt = t1 - t0; dt = t1 - t0;
printf("%d iters x %d keys = %d keys\n", niter, nkeys, niter * nkeys); printf("%" PRIu64 " iters x %zu keys = %" PRIu64 " keys\n",
niter, nkeys, total);
printf("%.3f ms total, %.3f us/key, %.3f us/iter\n", printf("%.3f ms total, %.3f us/key, %.3f us/iter\n",
dt * 1000, dt * 1e6 / (niter * nkeys), dt * 1e6 / niter); dt * 1000, dt * 1e6 / total, dt * 1e6 / niter);
close(fd); close(fd);
return 0; return 0;
} }

Binary file not shown.

Binary file not shown.

82
compose.c Normal file
View File

@@ -0,0 +1,82 @@
#include "dat.h"
#include "fn.h"
#include <locale.h>
#include <xkbcommon/xkbcommon.h>
#include <xkbcommon/xkbcommon-compose.h>
/*
* One state per frontend, all made before the procs are: xkbcommon locks
* nothing, and even its table refcount is a plain increment. cowner is
* the context whose sequence a state holds.
*/
static struct xkb_compose_state *cstate[Ncompose];
static void *cowner[Ncompose];
/* 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;
int i;
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)
for(i = 0; i < Ncompose; i++)
cstate[i] = xkb_compose_state_new(table,
XKB_COMPOSE_STATE_NO_FLAGS);
xkb_compose_table_unref(table);
xkb_context_unref(ctx);
}
/* The context is going, and the sequence it left half typed with it. */
void
composedrop(int who, void *owner)
{
if(cstate[who] == nil || owner != cowner[who])
return;
xkb_compose_state_reset(cstate[who]);
cowner[who] = nil;
}
/*
* 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. A sequence belongs to the
* context that started it: a key from another one starts over.
*/
int
composekey(int who, void *owner, u32int sym, char *buf, int n)
{
struct xkb_compose_state *cs;
buf[0] = '\0';
cs = cstate[who];
if(cs == nil)
return 0;
if(owner != cowner[who]){
xkb_compose_state_reset(cs);
cowner[who] = owner;
}
if(xkb_compose_state_feed(cs, sym) != XKB_COMPOSE_FEED_ACCEPTED)
return 0;
switch(xkb_compose_state_get_status(cs)){
case XKB_COMPOSE_COMPOSING:
case XKB_COMPOSE_CANCELLED:
return 1;
case XKB_COMPOSE_COMPOSED:
xkb_compose_state_get_utf8(cs, buf, n);
break;
default:
break;
}
return 0;
}

171
dat.h
View File

@@ -7,18 +7,24 @@
#define min(a, b) ((a) < (b) ? (a) : (b)) #define min(a, b) ((a) < (b) ? (a) : (b))
#define max(a, b) ((a) > (b) ? (a) : (b)) #define max(a, b) ((a) > (b) ? (a) : (b))
/*
* plan9port runs the note handlers for a note it goes on to ignore, and
* the process lives through it, so what strans announced must outlive
* such a note. A daemon that forks nothing gets only the one.
*/
#define notefatal(note) (strcmp(note, "sys: write on closed pipe") != 0)
/* A language id is the control code of the Ctrl key that selects it. */
enum enum
{ {
LangEN = 0x14, LangEN = 0x14,
LangJP = 0x0e, LangJP = 0x0e,
LangJPK = 0x0b, LangJPK = 0x0b,
LangKO = 0x13, LangKO = 0x13,
LangHANJA = 0x08,
LangEMOJI = 0x05, LangEMOJI = 0x05,
LangVI = 0x16, LangVI = 0x16,
Fontsz = 32,
Fontbase = 4,
Nglyphs = 0x20000,
Maxrunes = 64, Maxrunes = 64,
Maxutf = Maxrunes * UTFmax + 1, Maxutf = Maxrunes * UTFmax + 1,
}; };
@@ -26,16 +32,41 @@ enum
enum enum
{ {
Maxclients = 64, Maxclients = 64,
Maxkouho = 32, Ownerpoll = 200, /* ms between a frontend's owner checks while its preedit shows */
Maxkouho = 128,
Maxdisp = 9, Maxdisp = 9,
Imgw = (Maxrunes + 3) * Fontsz,
Imgh = (Maxdisp + 1) * Fontsz, /* Content purposes whose keys the engine never sees; IBus and
* text-input-v3 number them alike, both after GTK. */
Purposepassword = 8,
Purposepin = 9,
Colfg = 0x000000, Colfg = 0x000000,
Colbg = 0xffffff, Colbg = 0xffffff,
Colsel = 0xcccccc, Colsep = 0xd0d0d0,
Colsel = 0x333333,
Colselfg = 0xffffff,
}; };
/* Each frontend composes on its own: they run in different procs. */
enum
{
Composeibus,
Composexim,
Composewl,
Ncompose,
};
/* Popup metrics in pixels; popupscale is GDK_SCALE, for HiDPI. */
extern int popupscale;
#define Fontsz (32*popupscale)
#define PopupPad (4*popupscale)
#define PopupSep popupscale
#define PopupBorder popupscale
#define PopupNumw Fontsz
#define PopupTextw (12*Fontsz)
#define PopupBasew (2*PopupPad + PopupNumw + PopupTextw)
typedef struct Str Str; typedef struct Str Str;
struct Str struct Str
{ {
@@ -43,53 +74,46 @@ struct Str
int n; int n;
}; };
/*
* What one key does to a composition: text to commit, the new pending
* text, and the new raw state (Telex key history, or pending Japanese
* romaji), given the old ones in Im.
*/
typedef struct Emit Emit; typedef struct Emit Emit;
struct Emit struct Emit
{ {
int eat; int eat;
Str s; Str s;
Str next; Str next;
Str dict; Str raw;
};
typedef struct Hnode Hnode;
struct Hnode
{
int filled;
int next;
char *key;
int klen;
char *val;
int vlen;
}; };
typedef struct Tnode Tnode; typedef struct Tnode Tnode;
struct Tnode struct Tnode
{ {
int child;
int sibling;
char c;
char *val; char *val;
int vlen; int vlen;
int child;
int sibling;
Rune c;
}; };
typedef struct Trie Trie; typedef struct Trie Trie;
struct Trie struct Trie
{ {
int root;
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];
}; };
typedef struct Hmap Hmap; enum
struct Hmap
{ {
int nbs; TrieMiss,
int nsz; TriePrefix,
int len; TrieExact,
int cap;
uchar *nodes;
}; };
typedef struct Lang Lang; typedef struct Lang Lang;
@@ -101,67 +125,106 @@ struct Lang
char *dictname; char *dictname;
Emit (*trans)(Im*, Rune); Emit (*trans)(Im*, Rune);
void (*back)(Im*); void (*back)(Im*);
void (*dictq)(Im*);
Trie *map; Trie *map;
Hmap *dict; Trie *dict;
}; };
struct Im struct Im
{ {
Lang *l; Lang *l;
Str pre; Str pre;
Str raw; /* physical keys for the current Telex preedit */ /* Telex history, or the short pending romaji in a Japanese mode. */
Str raw;
Str kouho[Maxkouho]; Str kouho[Maxkouho];
int nkouho; int nkouho;
int sel; int sel;
}; };
typedef struct Drawcmd Drawcmd; typedef struct Drawcmd Drawcmd;
typedef struct Caret Caret;
typedef struct Area Area;
struct Area
{
int x;
int y;
int w;
int h;
};
struct Caret
{
int valid;
int x;
int y;
int h;
};
struct Drawcmd struct Drawcmd
{ {
Str pre; Str pre;
Str kouho[Maxdisp]; Str kouho[Maxdisp];
int nkouho; int nkouho;
int sel; int sel;
int first;
int total;
Caret caret;
};
/*
* Popup geometry from popuplayout: n rows from row0 of the candidates,
* each Fontsz tall; a section's y is -1 when it is absent. Numbers
* start at PopupPad, text at PopupPad+PopupNumw.
*/
typedef struct Popup Popup;
struct Popup
{
int w;
int h;
int n;
int row0;
int prey;
int sepy;
int rowsy;
int textw;
int marky;
int markx;
int markw;
Str mark;
int sely;
int selw;
}; };
typedef struct Keyreq Keyreq; typedef struct Keyreq Keyreq;
typedef struct Keyres Keyres; typedef struct Keyres Keyres;
enum
{
Keypress,
Keyreset,
Keyrelease,
Keycaret,
Keycap,
};
struct Keyres struct Keyres
{ {
int eaten; int eaten; /* Keycap: the caller is the owner and clientpre took. */
int del; /* runes of the client's own text to take back first */
Str commit; Str commit;
Str preedit; Str preedit;
}; };
struct Keyreq struct Keyreq
{ {
void *owner; /* stable until the context's release is acknowledged */
int clientpre; /* the client draws the preedit; the popup does not */
int op;
u32int ks; u32int ks;
u32int mod; u32int mod;
int want; /* nonzero: include preedit in reply */ Caret caret;
Str surround; /* the client's own text just before the cursor */
Channel *reply; Channel *reply;
}; };
typedef struct Dictreq Dictreq;
struct Dictreq
{
Str key;
Str pre;
int lang;
};
typedef struct Dictres Dictres;
struct Dictres
{
Str key;
Str kouho[Maxkouho];
int nkouho;
int lang;
};
extern Lang langs[]; extern Lang langs[];
extern int nlang; extern int nlang;
extern Channel *drawc; extern Channel *drawc;
extern Channel *keyc; extern Channel *keyc;
extern Channel *dictreqc;
extern Channel *dictresc;

157
dict.c
View File

@@ -1,107 +1,90 @@
#include "dat.h" #include "dat.h"
#include "fn.h" #include "fn.h"
void /* Appends the space-separated words of a node's entry to out[], up to max. */
dictlookup(Dictreq *req, Dictres *res) static int
words(Tnode *nd, Str *out, int n, int max)
{ {
Lang *l;
Hmap *dict;
Hnode *n;
char *p, *e, *sp; char *p, *e, *sp;
Str tmp; Str tmp;
res->key = req->pre; if(nd->val == nil)
res->nkouho = 0; return n;
res->lang = req->lang; p = nd->val;
if(req->key.n == 0) e = p + nd->vlen;
return; while(n < max && p < e){
l = getlang(req->lang);
dict = l ? l->dict : nil;
if(dict == nil)
return;
n = hmapget(dict, &req->key);
if(n == nil || n->vlen == 0)
return;
p = n->val;
e = p + n->vlen;
while(res->nkouho < Maxkouho && p < e){
while(p < e && *p == ' ') while(p < e && *p == ' ')
p++; p++;
if(p == e)
break;
sp = p; sp = p;
while(p < e && *p != ' ') while(p < e && *p != ' ')
p++; p++;
sinit(&tmp, sp, p - sp); if(sinit(&tmp, sp, p - sp) && tmp.n > 0)
if(scmp(&tmp, &req->key) != 0) out[n++] = tmp;
res->kouho[res->nkouho++] = tmp;
if(p < e)
p++;
} }
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*
langopen(char *dir, char *name, char *ext)
{
char *path;
Trie *t;
path = smprint("%s/%s.%s", dir, name, ext);
if(path == nil)
die("out of memory");
t = trieopen(path);
free(path);
return t;
} }
void void
dictthread(void*) langinit(char *dir)
{ {
Dictreq req; Lang *l;
Dictres res;
threadsetname("dict"); for(l = langs; l < langs + nlang; l++){
for(;;){ if(l->mapname != nil)
if(chanrecv(dictreqc, &req) < 0) l->map = langopen(dir, l->mapname, "map");
break; if(l->dictname != nil)
while(channbrecv(dictreqc, &req) > 0) l->dict = langopen(dir, l->dictname, "dict");
;
dictlookup(&req, &res);
chansend(dictresc, &res);
}
}
static Hmap*
dictopen(char *path)
{
Hmap *h;
Biobuf *b;
Str key;
char *line, *tab;
int len;
b = Bopen(path, OREAD);
if(b == nil)
die("can't open: %s", path);
h = hmapalloc(4096);
while((line = Brdstr(b, '\n', 1)) != nil){
len = strlen(line);
if(len > 0 && line[len-1] == '\r')
line[--len] = '\0';
if(len == 0 || line[0] == ';'){
free(line);
continue;
}
tab = strchr(line, '\t');
if(tab == nil || tab >= line + len - 1){
free(line);
continue;
}
*tab = '\0';
sinit(&key, line, tab - line);
hmapset(&h, &key, tab+1, len - (tab - line) - 1);
free(line);
}
Bterm(b);
return h;
}
void
dictinit(char *dir)
{
char path[1024];
int i;
for(i = 0; i < nlang; i++){
if(langs[i].dictname == nil)
continue;
snprint(path, sizeof(path), "%s/%s.dict", dir, langs[i].dictname);
langs[i].dict = dictopen(path);
} }
} }

45
fn.h
View File

@@ -1,6 +1,7 @@
void die(char*, ...); void die(char*, ...);
void sinit(Str*, char*, int); int sinit(Str*, char*, int);
void stail(Str*, char*, int);
void sclear(Str*); void sclear(Str*);
void sputr(Str*, Rune); void sputr(Str*, Rune);
void spopr(Str*); void spopr(Str*);
@@ -9,40 +10,52 @@ int scmp(Str*, Str*);
int stoutf(Str*, char*, int); int stoutf(Str*, char*, int);
Rune slastr(Str*); Rune slastr(Str*);
Hmap* hmapalloc(int);
void hmapfree(Hmap*);
void hmapset(Hmap**, Str*, const char*, int);
Hnode* hmapget(Hmap*, Str*);
int mapget(Trie*, Str*, Str*); int mapget(Trie*, Str*, Str*);
Trie* trienew(void);
void trieput(Trie*, char*, int, char*, int);
Trie* trieopen(char*); Trie* trieopen(char*);
void trieclose(Trie*); void trieclose(Trie*);
char* trieget(Trie*, char*, int, int*); int trienode(Trie*, Str*);
int trielookup(Trie*, char*, int, char**, int*); int trielookup(Trie*, Str*, char**, int*);
Lang* getlang(int); Lang* getlang(int);
void mapinit(char*); void langinit(char*);
void dictinit(char*);
void dictthread(void*); int dictlookup(Trie*, Str*, Str*, int);
void dictlookup(Dictreq*, Dictres*); int dictprefix(Trie*, Str*, Str*, int);
void drawthread(void*); void drawthread(void*);
void popuparea(Area*, int, Area*, int, int, Area*);
void popupposition(Caret*, int, int, Area*, int, int, int*, int*);
int pagemarker(char*, int, int, int, int);
void popuplayout(Drawcmd*, int, int, Popup*);
void popupdraw(u32int*, Drawcmd*, Popup*);
void imthread(void*); void imthread(void*);
void impre(Im*, 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);
void backko(Im*); void backko(Im*);
void backvi(Im*); void backvi(Im*);
void dictsend(Im*, Str*);
int srvreadkey(int, Keyreq*); void srvinit(void);
void srvthread(void*); void srvthread(void*);
void ibusthread(void*); void ibusthread(void*);
void waylandthread(void*); void ximthread(void*);
int wlinit(void);
void wlthread(void*);
int keymeaningful(u32int);
void composeinit(void);
void composedrop(int, void*);
int composekey(int, void*, u32int, char*, int);
void* emalloc(ulong); void* emalloc(ulong);
void* erealloc(void*, ulong); void* erealloc(void*, ulong);
void fontinit(char*); void textinit(void);
void putfont(u32int*, int, int, int, int, Rune); void textclose(void);
int textwidth(Str*);
void textdraw(u32int*, int, int, int, int, int, u32int, Str*);

229
font.c
View File

@@ -1,137 +1,142 @@
#define STB_TRUETYPE_IMPLEMENTATION
#include "stb_truetype.h"
#include "dat.h" #include "dat.h"
#include <fontconfig/fontconfig.h>
#include <pango/pangocairo.h>
#include "fn.h" #include "fn.h"
typedef struct Glyph Glyph; static PangoFontMap *fontmap;
struct Glyph static PangoContext *context;
static PangoLayout *layout;
void
textclose(void)
{ {
uchar *bmp; if(layout != nil)
int w, h, ox, oy; g_object_unref(layout);
}; if(context != nil)
g_object_unref(context);
enum { Maxfonts = 4 }; if(fontmap != nil)
g_object_unref(fontmap);
static stbtt_fontinfo fonts[Maxfonts]; layout = nil;
static uchar *fontdata[Maxfonts]; context = nil;
static float scale[Maxfonts]; fontmap = nil;
static int nfonts;
static Glyph cache[Nglyphs];
static u32int blendtab[2][256];
static u32int
blend(u32int bg, int a)
{
int r, g, b, inv;
inv = 255 - a;
r = (bg >> 16 & 0xff) * inv / 255;
g = (bg >> 8 & 0xff) * inv / 255;
b = (bg & 0xff) * inv / 255;
return (r << 16) | (g << 8) | b;
} }
static void static void
loadfont(char *path) setfont(void)
{ {
int fd; PangoFontDescription *font;
long sz, n; PangoRectangle r;
int size;
if(nfonts >= Maxfonts) font = pango_font_description_new();
die("too many fonts"); pango_font_description_set_family(font, "sans");
fd = open(path, OREAD); pango_font_description_set_absolute_size(font, Fontsz * PANGO_SCALE);
if(fd < 0) pango_layout_set_font_description(layout, font);
die("can't open font: %s", path); pango_layout_set_text(layout, "Mg", -1);
sz = seek(fd, 0, 2); pango_layout_get_pixel_extents(layout, nil, &r);
seek(fd, 0, 0); if(r.height > 0){
fontdata[nfonts] = emalloc(sz); /* Fontsz is the popup row height, not a point size. */
n = readn(fd, fontdata[nfonts], sz); size = Fontsz * PANGO_SCALE * Fontsz / r.height;
close(fd); pango_font_description_set_absolute_size(font, max(size, PANGO_SCALE));
if(n != sz) pango_layout_set_font_description(layout, font);
die("can't read font: %s", path); }
if(!stbtt_InitFont(&fonts[nfonts], fontdata[nfonts], stbtt_GetFontOffsetForIndex(fontdata[nfonts], 0))) pango_font_description_free(font);
die("can't init font: %s", path); }
scale[nfonts] = stbtt_ScaleForPixelHeight(&fonts[nfonts], Fontsz);
nfonts++; void
textinit(void)
{
FcInit();
fontmap = pango_cairo_font_map_new();
pango_cairo_font_map_set_resolution(PANGO_CAIRO_FONT_MAP(fontmap), 96);
context = pango_font_map_create_context(fontmap);
layout = pango_layout_new(context);
pango_layout_set_single_paragraph_mode(layout, TRUE);
setfont();
} }
static int static int
isfont(char *name) settext(Str *s)
{ {
char *p; char utf[Maxutf];
int n;
p = strrchr(name, '.'); n = stoutf(s, utf, sizeof utf);
if(p == nil) pango_layout_set_text(layout, utf, n);
return n > 0;
}
static void
textextents(PangoRectangle *r)
{
PangoRectangle ink, logical;
int x0, y0, x1, y1;
pango_layout_get_pixel_extents(layout, &ink, &logical);
x0 = min(ink.x, logical.x);
y0 = min(ink.y, logical.y);
x1 = max(ink.x + ink.width, logical.x + logical.width);
y1 = max(ink.y + ink.height, logical.y + logical.height);
r->x = x0;
r->y = y0;
r->width = max(x1 - x0, 0);
r->height = max(y1 - y0, 0);
}
int
textwidth(Str *s)
{
PangoRectangle r;
pango_layout_set_width(layout, -1);
pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_NONE);
if(!settext(s))
return 0; return 0;
return strcmp(p, ".ttf") == 0 || strcmp(p, ".otf") == 0; textextents(&r);
return r.width;
} }
/*
* Draws s at (x, y) into the w-by-h buffer, ellipsized to fit pixels and
* clipped to its own row so a tall fallback face cannot paint a
* neighbour.
*/
void void
fontinit(char *dir) textdraw(u32int *buf, int w, int h, int x, int y, int fit, u32int color,
Str *s)
{ {
int fd, n, i, a; PangoRectangle r;
Dir *d; cairo_surface_t *surface;
char path[256]; cairo_t *cr;
int b, g, red;
fd = open(dir, OREAD); if(fit <= 0)
if(fd < 0)
die("can't open font dir: %s", dir);
n = dirreadall(fd, &d);
close(fd);
if(n < 0)
die("can't read font dir: %s", dir);
for(i = 0; i < n; i++){
if(isfont(d[i].name)){
snprint(path, sizeof path, "%s/%s", dir, d[i].name);
loadfont(path);
}
}
free(d);
if(nfonts == 0)
die("no fonts in %s", dir);
for(a = 0; a < 256; a++){
blendtab[0][a] = blend(Colbg, a);
blendtab[1][a] = blend(Colsel, a);
}
}
void
putfont(u32int *buf, int w, int h, int px, int py, Rune r)
{
Glyph *g;
int i, j, a, sel, f;
int y0, j0, j1, x0, i0, i1;
u32int *p;
if(r >= Nglyphs)
return; return;
g = &cache[r]; pango_layout_set_width(layout, fit * PANGO_SCALE);
if(g->bmp == nil){ pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END);
for(f = 0; f < nfonts; f++){ if(w <= 0 || h <= 0 || !settext(s))
if(stbtt_FindGlyphIndex(&fonts[f], r) == 0) return;
continue; surface = cairo_image_surface_create_for_data((uchar*)buf,
g->bmp = stbtt_GetCodepointBitmap(&fonts[f], scale[f], scale[f], r, &g->w, &g->h, &g->ox, &g->oy); CAIRO_FORMAT_RGB24, w, h, w * sizeof buf[0]);
if(g->bmp != nil) if(cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS){
break; cairo_surface_destroy(surface);
}
if(g->bmp == nil)
return; return;
} }
cairo_surface_mark_dirty(surface);
y0 = py + g->oy + Fontsz - Fontbase; cr = cairo_create(surface);
j0 = y0 < 0 ? -y0 : 0; if(cairo_status(cr) == CAIRO_STATUS_SUCCESS){
j1 = y0 + g->h > h ? h - y0 : g->h; pango_cairo_update_layout(cr, layout);
x0 = px + g->ox; textextents(&r);
i0 = x0 < 0 ? -x0 : 0; cairo_rectangle(cr, x, y, fit, Fontsz);
i1 = x0 + g->w > w ? w - x0 : g->w; cairo_clip(cr);
for(j = j0; j < j1; j++){ cairo_move_to(cr, x - r.x, y + (Fontsz - r.height) / 2 - r.y);
for(i = i0; i < i1; i++){ red = color >> 16 & 0xff;
a = g->bmp[j * g->w + i]; g = color >> 8 & 0xff;
if(a > 0){ b = color & 0xff;
p = &buf[(y0 + j) * w + x0 + i]; cairo_set_source_rgb(cr, red / 255.0, g / 255.0, b / 255.0);
sel = (*p == Colsel) ? 1 : 0; pango_cairo_show_layout(cr, layout);
*p = blendtab[sel][a];
}
}
} }
cairo_destroy(cr);
cairo_surface_flush(surface);
cairo_surface_destroy(surface);
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,3 +0,0 @@
FROM debian:stable-slim
RUN apt-get update && apt-get install -y gcc libgtk-3-dev make
WORKDIR /src

View File

@@ -1,22 +1,43 @@
CFLAGS = -Wall -O2 -I.. `pkg-config --cflags gtk+-3.0` PROG = im-strans.so
LDFLAGS = `pkg-config --libs gtk+-3.0` PKG_CONFIG ?= pkg-config
DSTDIR ?= $(shell find /usr/lib* -type d -path "*/gtk-3.0/*/immodules" 2>/dev/null | head -1) CFLAGS ?= -O2
GTK_CFLAGS = $(shell $(PKG_CONFIG) --cflags gtk+-3.0)
GTK_LIBS = $(shell $(PKG_CONFIG) --libs gtk+-3.0)
GTK_QUERY_IMMODULES ?= gtk-query-immodules-3.0
GTK_LIBDIR ?= $(shell $(PKG_CONFIG) --variable=libdir gtk+-3.0 2>/dev/null)
GTK_BINARY_VERSION ?= $(shell $(PKG_CONFIG) \
--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_QUERY_IMMODULES) 2>/dev/null | \
awk -F '"' '/^"\// { sub("/[^/]*$$", "", $$2); print $$2; exit }')
GTK_MODULE_DIR ?= $(strip $(if \
$(and $(strip $(GTK_LIBDIR)),$(strip $(GTK_BINARY_VERSION))),\
$(GTK_LIBDIR)/gtk-3.0/$(GTK_BINARY_VERSION)/immodules,\
$(GTK_RUNTIME_MODULE_DIR)))
im-strans.so: main.c ../ipc.c ../ipc.h all: $(PROG)
$(CC) -shared -fPIC $(CFLAGS) -o $@ main.c ../ipc.c $(LDFLAGS)
docker: main.c Dockerfile $(PROG): main.c ../ipc.c ../ipc.h
docker build -t strans-gtk . $(CC) $(CPPFLAGS) -I.. $(GTK_CFLAGS) -Wall $(CFLAGS) $(LDFLAGS) \
docker run --rm -v $(CURDIR)/..:/src -w /src/gtk strans-gtk make im-strans.so -shared -fPIC -o $@ main.c ../ipc.c $(GTK_LIBS) $(LDLIBS)
install: CHECKDIR = test -n "$$module_dir" || { \
mkdir -p $(DSTDIR) echo "cannot find GTK 3 module directory; set GTK_MODULE_DIR" >&2; \
cp im-strans.so $(DSTDIR)/ exit 1; }
gtk-query-immodules-3.0 --update-cache REFRESH = test -n "$(DESTDIR)" || { \
unset GTK_PATH GTK_IM_MODULE_FILE; $(GTK_QUERY_IMMODULES) --update-cache; }
install: $(PROG)
module_dir="$(GTK_MODULE_DIR)"; $(CHECKDIR); \
mkdir -p "$(DESTDIR)$$module_dir" && \
cp "$(PROG)" "$(DESTDIR)$$module_dir/" && $(REFRESH)
uninstall: uninstall:
rm -f $(DSTDIR)/im-strans.so module_dir="$(GTK_MODULE_DIR)"; $(CHECKDIR); \
gtk-query-immodules-3.0 --update-cache rm -f "$(DESTDIR)$$module_dir/$(PROG)" && $(REFRESH)
clean: clean:
rm -f im-strans.so rm -f $(PROG)
.PHONY: all install uninstall clean

View File

@@ -1,145 +1,314 @@
#include <stdio.h> #include <errno.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include <unistd.h> #include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <gtk/gtk.h> #include <gtk/gtk.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "ipc.h" #include "ipc.h"
typedef struct Im Im; typedef struct Im Im;
struct Im struct Im
{ {
GtkIMContext parent; GtkIMContextSimple parent;
int fd; int fd;
int usepreedit;
int ext;
int private;
int simpleactive;
int simpledone;
char pre[Ipcfieldmax+1]; char pre[Ipcfieldmax+1];
int prelen; int prelen;
GdkWindow *win;
GdkRectangle cursor;
int cursorvalid;
int caretsent;
unsigned char sent[Ipccaretsz];
char surround[Ipcfieldmax];
int nsurround;
int surroundsent;
}; };
typedef struct ImClass ImClass; typedef struct ImClass ImClass;
struct ImClass struct ImClass
{ {
GtkIMContextClass parent; GtkIMContextSimpleClass parent;
}; };
static GType imtype; static GType imtype;
static GObjectClass *parentobject;
static GtkIMContextClass *parentim;
static void static void
srvconnect(Im *im) setpreedit(Im *im, const char *s, int n)
{ {
struct sockaddr_un addr; int was;
if(n < 0 || n > Ipcfieldmax)
n = 0;
if(n == 0 && im->prelen == 0)
return;
was = im->prelen;
if(n > 0)
memcpy(im->pre, s, n);
im->pre[n] = '\0';
im->prelen = n;
if(was == 0 && n > 0)
g_signal_emit_by_name(im, "preedit-start");
g_signal_emit_by_name(im, "preedit-changed");
if(was > 0 && n == 0)
g_signal_emit_by_name(im, "preedit-end");
}
static void
srvdrop(Im *im, int notify)
{
if(im->fd >= 0) if(im->fd >= 0)
return;
im->fd = socket(AF_UNIX, SOCK_STREAM, 0);
if(im->fd < 0)
return;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
snprintf(addr.sun_path, sizeof(addr.sun_path), IPCPATH, getuid());
if(connect(im->fd, (struct sockaddr*)&addr, sizeof(addr)) < 0){
close(im->fd); close(im->fd);
im->fd = -1; im->fd = -1;
im->ext = 0;
im->caretsent = 0;
im->surroundsent = 0;
if(notify)
setpreedit(im, "", 0);
else{
im->pre[0] = '\0';
im->prelen = 0;
} }
} }
static void static void
srvclose(Im *im) srvclose(Im *im)
{ {
int was; srvdrop(im, 1);
}
if(im->fd >= 0) /* Both buffers are char[Ipcfieldmax+1]. */
close(im->fd); static int
im->fd = -1; readresp(Im *im, int want, char *commit, char *pre, Ipcresp *resp)
was = im->prelen; {
im->pre[0] = '\0'; if(ipcreadresp(im->fd, want, commit, pre, resp) < 0)
im->prelen = 0; return -1;
if(was > 0){ if(!g_utf8_validate(commit, resp->commitlen, NULL) ||
g_signal_emit_by_name(im, "preedit-changed"); (want && !g_utf8_validate(pre, resp->preeditlen, NULL))){
g_signal_emit_by_name(im, "preedit-end"); errno = EPROTO;
return -1;
} }
return 0;
}
static void
dropwindow(Im *im)
{
if(im->win != NULL)
g_object_unref(im->win);
im->win = NULL;
im->cursorvalid = 0;
}
/* Root-relative caret in device pixels; returns whether it is known. */
static int
caretget(Im *im, int32_t *x, int32_t *y, int32_t *h)
{
gint rx, ry, scale;
int64_t sx, sy, sh;
*x = *y = *h = 0;
if(im->win != NULL && gdk_window_is_destroyed(im->win))
dropwindow(im);
if(!im->cursorvalid || im->win == NULL || im->cursor.height < 0)
return 0;
#ifdef GDK_WINDOWING_X11
if(!GDK_IS_X11_WINDOW(im->win))
return 0;
#else
return 0;
#endif
scale = gdk_window_get_scale_factor(im->win);
if(scale <= 0)
return 0;
/* GDK translates the client-relative point before device scaling. */
gdk_window_get_root_coords(im->win, 0, 0, &rx, &ry);
sx = ((int64_t)rx + im->cursor.x) * scale;
sy = ((int64_t)ry + im->cursor.y) * scale;
sh = (int64_t)im->cursor.height * scale;
if(sx < INT32_MIN || sx > INT32_MAX ||
sy < INT32_MIN || sy > INT32_MAX || sh > INT32_MAX)
return 0;
*x = sx;
*y = sy;
*h = sh;
return 1;
} }
static int static int
readresp(Im *im, char *buf, int bufsz) sendcaret(Im *im)
{ {
Ipcresp resp; unsigned char buf[Ipccaretsz];
int was; int32_t x, y, h;
int valid;
was = im->prelen; if(im->fd < 0 || !im->ext)
if(ipcreadresp(im->fd, 1, buf, bufsz, im->pre, return 0;
sizeof(im->pre), &resp) < 0) valid = caretget(im, &x, &y, &h);
ipcpackcaret(buf, valid, x, y, h);
if(im->caretsent && memcmp(buf, im->sent, sizeof buf) == 0)
return 0;
if(ipcsend(im->fd, buf, sizeof buf) < 0)
return -1; return -1;
im->prelen = resp.npreedit; memcpy(im->sent, buf, sizeof buf);
if(was == 0 && im->prelen > 0) im->caretsent = 1;
g_signal_emit_by_name(im, "preedit-start"); return 0;
if(was != 0 || im->prelen != 0)
g_signal_emit_by_name(im, "preedit-changed");
if(was > 0 && im->prelen == 0)
g_signal_emit_by_name(im, "preedit-end");
return resp.eaten;
} }
static uint32_t /*
kget(uint32_t gdk) * The text just before the cursor, as much of it as a reading can use,
* cut on a rune boundary. A widget that keeps none leaves it empty, and
* then the daemon never asks for any of it back.
*/
static void
readsurround(Im *im, GtkIMContext *ctx)
{ {
uint32_t u; char *text, *p;
gint cursor;
int start;
u = gdk_keyval_to_unicode(gdk); im->nsurround = 0;
if((gdk & 0xff000000) == 0x01000000 && u != 0) if(!gtk_im_context_get_surrounding(ctx, &text, &cursor))
return u; return;
if(gdk >= 0xff00 && gdk <= 0xffff) if(cursor < 0 || cursor > (gint)strlen(text)){
return Kspec + (gdk - 0xff00); g_free(text);
return u; return;
}
start = cursor > Ipcfieldmax ? cursor - Ipcfieldmax : 0;
for(p = text + start; p < text + cursor && (*p & 0xc0) == 0x80; p++)
;
im->nsurround = text + cursor - p;
memcpy(im->surround, p, im->nsurround);
g_free(text);
} }
static uint32_t /* Sent only when it changes, as the caret is. */
mget(uint32_t state) static int
sendsurround(Im *im, GtkIMContext *ctx)
{ {
uint32_t m; unsigned char hdr[Ipcreqsz];
char was[Ipcfieldmax];
int nwas;
m = 0; if(im->fd < 0 || !im->ext)
if(state & GDK_SHIFT_MASK) return 0;
m |= Mshift; nwas = im->nsurround;
if(state & GDK_CONTROL_MASK) memcpy(was, im->surround, nwas);
m |= Mctrl; readsurround(im, ctx);
if(state & GDK_MOD1_MASK) if(im->surroundsent && nwas == im->nsurround &&
m |= Malt; memcmp(was, im->surround, nwas) == 0)
if(state & GDK_SUPER_MASK) return 0;
m |= Msuper; ipcpacksurround(hdr, im->nsurround);
return m; if(ipcsend(im->fd, hdr, sizeof hdr) < 0 ||
(im->nsurround > 0 &&
ipcsend(im->fd, im->surround, im->nsurround) < 0))
return -1;
im->surroundsent = 1;
return 0;
} }
static int
srvconnect(Im *im)
{
unsigned char buf[Ipcreqsz];
char commit[Ipcfieldmax+1];
char pre[Ipcfieldmax+1];
Ipcresp resp;
if(im->fd >= 0)
return 0;
im->fd = ipcconnect();
if(im->fd < 0)
return -1;
/* Marker zero is an old daemon's harmless key-zero response. */
ipcpackcap(buf, im->usepreedit);
if(ipcsend(im->fd, buf, sizeof buf) < 0 ||
readresp(im, im->usepreedit, commit, pre, &resp) < 0){
srvclose(im);
return -1;
}
im->ext = resp.eaten != 0;
if(sendcaret(im) < 0){
srvclose(im);
return -1;
}
return 0;
}
static void
simplecommit(GtkIMContext *ctx, const char *s, Im *im)
{
(void)ctx;
(void)s;
im->simpledone = 1;
}
static void
simpleend(GtkIMContext *ctx, Im *im)
{
(void)ctx;
im->simpledone = 1;
}
/*
* GtkIMContextSimple composes dead keys and Compose sequences; it keeps
* the key stream until it commits, ends its preedit, or rejects a press.
*/
static gboolean
simplefilter(GtkIMContext *ctx, GdkEventKey *ev, int release)
{
Im *im;
gboolean r;
im = (Im*)ctx;
im->simpleactive = 1;
im->simpledone = 0;
r = parentim->filter_keypress(ctx, ev);
if((!r && !release) || im->simpledone)
im->simpleactive = 0;
return r;
}
/* The daemon hands the pending text back; it becomes committed text. */
static void static void
sendreset(Im *im) sendreset(Im *im)
{ {
unsigned char buf[Ipcreqsz]; unsigned char buf[Ipcreqsz];
char resp[Ipcfieldmax+1]; char commit[Ipcfieldmax+1];
char pre[Ipcfieldmax+1];
Ipcresp resp;
if(im->fd < 0){ if(im->fd < 0){
setpreedit(im, "", 0);
return;
}
ipcpackreset(buf, im->usepreedit);
if(ipcsend(im->fd, buf, sizeof buf) < 0 ||
readresp(im, im->usepreedit, commit, pre, &resp) < 0){
srvclose(im); srvclose(im);
return; return;
} }
ipcpackreq(buf, 1, 0, Kesc); setpreedit(im, "", 0);
if(ipcsend(im->fd, buf, sizeof buf) < 0 || if(commit[0] != '\0')
readresp(im, resp, sizeof(resp)) < 0) g_signal_emit_by_name(im, "commit", commit);
srvclose(im);
} }
static gboolean /* A password entry hidden the older way sets no purpose and signals nothing. */
plaincommit(GtkIMContext *ctx, uint32_t key, uint32_t mod) static int
ishidden(Im *im)
{ {
char u[8]; GtkWidget *w;
int n;
if(mod & (Mctrl|Malt|Msuper)) if(im->win == NULL)
return FALSE; return 0;
if(key < 0x20 || key >= Kspec) gdk_window_get_user_data(im->win, (gpointer*)&w);
return FALSE; return GTK_IS_ENTRY(w) && !gtk_entry_get_visibility(GTK_ENTRY(w));
n = g_unichar_to_utf8(key, u);
u[n] = '\0';
g_signal_emit_by_name(ctx, "commit", u);
return TRUE;
} }
static gboolean static gboolean
@@ -147,41 +316,56 @@ kpress(GtkIMContext *ctx, GdkEventKey *ev)
{ {
Im *im; Im *im;
unsigned char buf[Ipcreqsz]; unsigned char buf[Ipcreqsz];
char resp[Ipcfieldmax+1]; char commit[Ipcfieldmax+1];
char pre[Ipcfieldmax+1];
uint32_t key, mod; uint32_t key, mod;
int r; Ipcresp resp;
GtkInputPurpose purpose; gboolean r;
im = (Im*)ctx; im = (Im*)ctx;
if(ev->type != GDK_KEY_PRESS) if(ev->type != GDK_KEY_PRESS){
if(im->simpleactive)
return simplefilter(ctx, ev, 1);
return FALSE; return FALSE;
key = kget(ev->keyval);
if(key == 0)
return FALSE;
mod = mget(ev->state);
g_object_get(ctx, "input-purpose", &purpose, NULL);
if(purpose == GTK_INPUT_PURPOSE_PASSWORD || purpose == GTK_INPUT_PURPOSE_PIN)
return plaincommit(ctx, key, mod);
srvconnect(im);
if(im->fd < 0){
srvclose(im);
return plaincommit(ctx, key, mod);
} }
ipcpackreq(buf, 1, mod, key); if(im->simpleactive)
if(ipcsend(im->fd, buf, sizeof buf) < 0){ return simplefilter(ctx, ev, 0);
srvclose(im); key = ipckeysym(ev->keyval, gdk_keyval_to_unicode(ev->keyval));
return plaincommit(ctx, key, mod); mod = ipcmod(ev->state);
if(im->private || ishidden(im) || key == 0){
r = simplefilter(ctx, ev, 0);
/* A dead key starts a compose: pending text was typed first. */
if(im->simpleactive)
sendreset(im);
return r;
} }
r = readresp(im, resp, sizeof(resp)); if(srvconnect(im) < 0)
if(r < 0){ return simplefilter(ctx, ev, 0);
/* A retained GDK window may have moved since the last cursor report. */
if(sendcaret(im) < 0 || sendsurround(im, ctx) < 0){
srvclose(im); srvclose(im);
return plaincommit(ctx, key, mod); return simplefilter(ctx, ev, 0);
} }
if(resp[0] != '\0') ipcpackreq(buf, im->usepreedit, mod, key);
g_signal_emit_by_name(ctx, "commit", resp); if(ipcsend(im->fd, buf, sizeof buf) < 0 ||
if(r != 0) readresp(im, im->usepreedit, commit, pre, &resp) < 0){
srvclose(im);
return simplefilter(ctx, ev, 0);
}
if(im->usepreedit && commit[0] != '\0' && im->prelen > 0)
setpreedit(im, "", 0);
/* The reading reached into the widget's own text: give it back. */
if(resp.del > 0){
gtk_im_context_delete_surrounding(ctx, -resp.del, resp.del);
im->surroundsent = 0;
}
if(commit[0] != '\0')
g_signal_emit_by_name(ctx, "commit", commit);
if(im->usepreedit)
setpreedit(im, pre, resp.preeditlen);
if(resp.eaten)
return TRUE; return TRUE;
return plaincommit(ctx, key, mod); return simplefilter(ctx, ev, 0);
} }
static void static void
@@ -192,6 +376,10 @@ getpreedit(GtkIMContext *ctx, gchar **str, PangoAttrList **attrs,
PangoAttribute *u; PangoAttribute *u;
im = (Im*)ctx; im = (Im*)ctx;
if(im->simpleactive){
parentim->get_preedit_string(ctx, str, attrs, cursor_pos);
return;
}
if(str) if(str)
*str = g_strdup(im->pre); *str = g_strdup(im->pre);
if(attrs){ if(attrs){
@@ -210,32 +398,148 @@ getpreedit(GtkIMContext *ctx, gchar **str, PangoAttrList **attrs,
static void static void
reset(GtkIMContext *ctx) reset(GtkIMContext *ctx)
{ {
if(parentim->reset != NULL)
parentim->reset(ctx);
((Im*)ctx)->simpleactive = 0;
sendreset((Im*)ctx); sendreset((Im*)ctx);
} }
static void static void
focusout(GtkIMContext *ctx) focusout(GtkIMContext *ctx)
{ {
sendreset((Im*)ctx); Im *im;
im = (Im*)ctx;
if(parentim->focus_out != NULL)
parentim->focus_out(ctx);
im->simpleactive = 0;
/* Closing the connection releases the engine. */
sendreset(im);
srvclose(im);
} }
static void static void
finalize(GObject *obj) setusepreedit(GtkIMContext *ctx, gboolean use)
{
Im *im;
unsigned char buf[Ipcreqsz];
char commit[Ipcfieldmax+1];
char pre[Ipcfieldmax+1];
Ipcresp resp;
im = (Im*)ctx;
use = use != FALSE;
if(parentim->set_use_preedit != NULL)
parentim->set_use_preedit(ctx, use);
if(im->usepreedit == use)
return;
im->usepreedit = use;
if(!use)
setpreedit(im, "", 0);
if(im->fd < 0 || !im->ext)
return;
ipcpackcap(buf, use);
if(ipcsend(im->fd, buf, sizeof buf) < 0 ||
readresp(im, use, commit, pre, &resp) < 0 ||
!resp.eaten){
srvclose(im);
return;
}
if(use)
setpreedit(im, pre, resp.preeditlen);
}
static void
setclientwindow(GtkIMContext *ctx, GdkWindow *win)
{
Im *im;
im = (Im*)ctx;
if(win != NULL && gdk_window_is_destroyed(win))
win = NULL;
if(parentim->set_client_window != NULL)
parentim->set_client_window(ctx, win);
if(im->win == win)
return;
if(win != NULL)
g_object_ref(win);
dropwindow(im);
im->win = win;
if(sendcaret(im) < 0)
srvclose(im);
}
static void
setcursorlocation(GtkIMContext *ctx, GdkRectangle *area)
{
Im *im;
im = (Im*)ctx;
if(parentim->set_cursor_location != NULL)
parentim->set_cursor_location(ctx, area);
if(im->win != NULL && gdk_window_is_destroyed(im->win))
dropwindow(im);
if(area == NULL || im->win == NULL)
im->cursorvalid = 0;
else{
im->cursor = *area;
im->cursorvalid = 1;
}
if(sendcaret(im) < 0)
srvclose(im);
}
static int
isprivate(GtkInputPurpose purpose)
{
return purpose == GTK_INPUT_PURPOSE_PASSWORD ||
purpose == GTK_INPUT_PURPOSE_PIN;
}
static void
purposechanged(GObject *obj, GParamSpec *pspec, gpointer data)
{
Im *im;
GtkInputPurpose purpose;
int private;
(void)pspec;
(void)data;
im = (Im*)obj;
g_object_get(obj, "input-purpose", &purpose, NULL);
private = isprivate(purpose);
if(private == im->private)
return;
im->private = private;
if(private)
reset(GTK_IM_CONTEXT(im));
}
static void
dispose(GObject *obj)
{ {
Im *im; Im *im;
im = (Im*)obj; im = (Im*)obj;
if(im->fd >= 0) srvdrop(im, 0);
close(im->fd); dropwindow(im);
G_OBJECT_CLASS(g_type_class_peek_parent(G_OBJECT_GET_CLASS(obj)))->finalize(obj); parentobject->dispose(obj);
} }
static void static void
init(Im *im) init(Im *im)
{ {
GtkInputPurpose purpose;
im->fd = -1; im->fd = -1;
im->usepreedit = 1;
im->pre[0] = '\0'; im->pre[0] = '\0';
im->prelen = 0; g_object_get(im, "input-purpose", &purpose, NULL);
im->private = isprivate(purpose);
g_signal_connect(im, "notify::input-purpose",
G_CALLBACK(purposechanged), NULL);
g_signal_connect(im, "commit", G_CALLBACK(simplecommit), im);
g_signal_connect(im, "preedit-end", G_CALLBACK(simpleend), im);
} }
static void static void
@@ -246,11 +550,16 @@ classinit(ImClass *klass)
ic = GTK_IM_CONTEXT_CLASS(klass); ic = GTK_IM_CONTEXT_CLASS(klass);
oc = G_OBJECT_CLASS(klass); oc = G_OBJECT_CLASS(klass);
parentim = g_type_class_peek_parent(klass);
parentobject = G_OBJECT_CLASS(parentim);
ic->filter_keypress = kpress; ic->filter_keypress = kpress;
ic->get_preedit_string = getpreedit; ic->get_preedit_string = getpreedit;
ic->reset = reset; ic->reset = reset;
ic->focus_out = focusout; ic->focus_out = focusout;
oc->finalize = finalize; ic->set_use_preedit = setusepreedit;
ic->set_client_window = setclientwindow;
ic->set_cursor_location = setcursorlocation;
oc->dispose = dispose;
} }
static const GtkIMContextInfo info = { static const GtkIMContextInfo info = {
@@ -275,7 +584,8 @@ im_module_init(GTypeModule *mod)
0, 0,
(GInstanceInitFunc)init, (GInstanceInitFunc)init,
}; };
imtype = g_type_module_register_type(mod, GTK_TYPE_IM_CONTEXT, "strans-gtk", &ti, 0); imtype = g_type_module_register_type(mod, GTK_TYPE_IM_CONTEXT_SIMPLE,
"strans-gtk", &ti, 0);
} }
G_MODULE_EXPORT void G_MODULE_EXPORT void

162
hash.c
View File

@@ -1,162 +0,0 @@
#include "dat.h"
#include "fn.h"
enum {
Tagsize = sizeof(Hnode),
};
static uvlong
hash(Str *s)
{
uvlong h;
int i;
h = 7;
for(i = 0; i < s->n; i++)
h = h*31 + s->r[i];
return h;
}
Hmap*
hmapalloc(int nbuckets)
{
void *store;
Hmap *h;
int nsz;
if(nbuckets < 1)
die("hmapalloc: no buckets");
nsz = Tagsize;
store = emalloc(sizeof(*h) + nbuckets * nsz);
h = store;
h->nbs = nbuckets;
h->nsz = nsz;
h->len = h->cap = nbuckets;
h->nodes = (uchar*)store + sizeof(*h);
return h;
}
static int
keycmp(Hnode *n, Str *key)
{
char buf[Maxutf];
int len;
len = stoutf(key, buf, sizeof(buf));
if(n->klen != len)
return 1;
return memcmp(n->key, buf, len);
}
Hnode*
hmapget(Hmap *h, Str *key)
{
Hnode *n;
uchar *v;
v = h->nodes + (hash(key) % h->nbs) * h->nsz;
for(;;){
n = (Hnode*)v;
if(n->filled && keycmp(n, key) == 0)
return n;
if(n->next == 0)
break;
v = h->nodes + n->next * h->nsz;
}
return nil;
}
static char*
sdup(Str *s)
{
char buf[Maxutf];
char *p;
int n;
n = stoutf(s, buf, sizeof(buf));
p = emalloc(n + 1);
memmove(p, buf, n);
p[n] = '\0';
return p;
}
static char*
memdup(const char *src, int n)
{
char *p;
if(n == 0)
return nil;
p = emalloc(n + 1);
memmove(p, src, n);
p[n] = '\0';
return p;
}
void
hmapfree(Hmap *h)
{
Hnode *n;
int i;
if(h == nil)
return;
for(i = 0; i < h->len; i++){
n = (Hnode*)(h->nodes + i * h->nsz);
if(!n->filled)
continue;
free(n->key);
free(n->val);
}
free(h);
}
void
hmapset(Hmap **store, Str *key, const char *val, int vlen)
{
char *newval;
Hnode *n;
uchar *v;
Hmap *h;
int next;
vlong diff;
newval = memdup(val, vlen);
h = *store;
v = h->nodes + (hash(key) % h->nbs) * h->nsz;
for(;;){
n = (Hnode*)v;
next = n->next;
if(n->filled == 0)
goto replace;
if(keycmp(n, key) == 0)
goto replace;
if(next == 0)
break;
v = h->nodes + next * h->nsz;
}
if(h->cap == h->len){
diff = v - h->nodes;
h->cap *= 2;
*store = erealloc(*store, sizeof(*h) + h->cap * h->nsz);
h = *store;
h->nodes = (uchar*)*store + sizeof(*h);
v = h->nodes + diff;
n = (Hnode*)v;
}
n->next = h->len;
memset(h->nodes + h->len * h->nsz, 0, h->nsz);
h->len++;
v = h->nodes + n->next * h->nsz;
n = (Hnode*)v;
replace:
if(n->filled == 0){
n->key = sdup(key);
n->klen = strlen(n->key);
n->filled = 1;
}
n->next = next;
free(n->val);
n->val = newval;
n->vlen = vlen;
}

1169
ibus.c

File diff suppressed because it is too large Load Diff

View File

@@ -1,955 +0,0 @@
/* Generated by wayland-scanner 1.21.0 */
#ifndef INPUT_METHOD_UNSTABLE_V2_CLIENT_PROTOCOL_H
#define INPUT_METHOD_UNSTABLE_V2_CLIENT_PROTOCOL_H
#include <stdint.h>
#include <stddef.h>
#include "wayland-client.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @page page_input_method_unstable_v2 The input_method_unstable_v2 protocol
* Protocol for creating input methods
*
* @section page_desc_input_method_unstable_v2 Description
*
* This protocol allows applications to act as input methods for compositors.
*
* An input method context is used to manage the state of the input method.
*
* Text strings are UTF-8 encoded, their indices and lengths are in bytes.
*
* This document adheres to the RFC 2119 when using words like "must",
* "should", "may", etc.
*
* Warning! The protocol described in this file is experimental and
* backward incompatible changes may be made. Backward compatible changes
* may be added together with the corresponding interface version bump.
* Backward incompatible changes are done by bumping the version number in
* the protocol and interface names and resetting the interface version.
* Once the protocol is to be declared stable, the 'z' prefix and the
* version number in the protocol and interface names are removed and the
* interface version number is reset.
*
* @section page_ifaces_input_method_unstable_v2 Interfaces
* - @subpage page_iface_zwp_input_method_v2 - input method
* - @subpage page_iface_zwp_input_popup_surface_v2 - popup surface
* - @subpage page_iface_zwp_input_method_keyboard_grab_v2 - keyboard grab
* - @subpage page_iface_zwp_input_method_manager_v2 - input method manager
* @section page_copyright_input_method_unstable_v2 Copyright
* <pre>
*
* Copyright © 2008-2011 Kristian Høgsberg
* Copyright © 2010-2011 Intel Corporation
* Copyright © 2012-2013 Collabora, Ltd.
* Copyright © 2012, 2013 Intel Corporation
* Copyright © 2015, 2016 Jan Arne Petersen
* Copyright © 2017, 2018 Red Hat, Inc.
* Copyright © 2018 Purism SPC
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice (including the next
* paragraph) shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS 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. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
* </pre>
*/
struct wl_seat;
struct wl_surface;
struct zwp_input_method_keyboard_grab_v2;
struct zwp_input_method_manager_v2;
struct zwp_input_method_v2;
struct zwp_input_popup_surface_v2;
#ifndef ZWP_INPUT_METHOD_V2_INTERFACE
#define ZWP_INPUT_METHOD_V2_INTERFACE
/**
* @page page_iface_zwp_input_method_v2 zwp_input_method_v2
* @section page_iface_zwp_input_method_v2_desc Description
*
* An input method object allows for clients to compose text.
*
* The objects connects the client to a text input in an application, and
* lets the client to serve as an input method for a seat.
*
* The zwp_input_method_v2 object can occupy two distinct states: active and
* inactive. In the active state, the object is associated to and
* communicates with a text input. In the inactive state, there is no
* associated text input, and the only communication is with the compositor.
* Initially, the input method is in the inactive state.
*
* Requests issued in the inactive state must be accepted by the compositor.
* Because of the serial mechanism, and the state reset on activate event,
* they will not have any effect on the state of the next text input.
*
* There must be no more than one input method object per seat.
* @section page_iface_zwp_input_method_v2_api API
* See @ref iface_zwp_input_method_v2.
*/
/**
* @defgroup iface_zwp_input_method_v2 The zwp_input_method_v2 interface
*
* An input method object allows for clients to compose text.
*
* The objects connects the client to a text input in an application, and
* lets the client to serve as an input method for a seat.
*
* The zwp_input_method_v2 object can occupy two distinct states: active and
* inactive. In the active state, the object is associated to and
* communicates with a text input. In the inactive state, there is no
* associated text input, and the only communication is with the compositor.
* Initially, the input method is in the inactive state.
*
* Requests issued in the inactive state must be accepted by the compositor.
* Because of the serial mechanism, and the state reset on activate event,
* they will not have any effect on the state of the next text input.
*
* There must be no more than one input method object per seat.
*/
extern const struct wl_interface zwp_input_method_v2_interface;
#endif
#ifndef ZWP_INPUT_POPUP_SURFACE_V2_INTERFACE
#define ZWP_INPUT_POPUP_SURFACE_V2_INTERFACE
/**
* @page page_iface_zwp_input_popup_surface_v2 zwp_input_popup_surface_v2
* @section page_iface_zwp_input_popup_surface_v2_desc Description
*
* This interface marks a surface as a popup for interacting with an input
* method.
*
* The compositor should place it near the active text input area. It must
* be visible if and only if the input method is in the active state.
*
* The client must not destroy the underlying wl_surface while the
* zwp_input_popup_surface_v2 object exists.
* @section page_iface_zwp_input_popup_surface_v2_api API
* See @ref iface_zwp_input_popup_surface_v2.
*/
/**
* @defgroup iface_zwp_input_popup_surface_v2 The zwp_input_popup_surface_v2 interface
*
* This interface marks a surface as a popup for interacting with an input
* method.
*
* The compositor should place it near the active text input area. It must
* be visible if and only if the input method is in the active state.
*
* The client must not destroy the underlying wl_surface while the
* zwp_input_popup_surface_v2 object exists.
*/
extern const struct wl_interface zwp_input_popup_surface_v2_interface;
#endif
#ifndef ZWP_INPUT_METHOD_KEYBOARD_GRAB_V2_INTERFACE
#define ZWP_INPUT_METHOD_KEYBOARD_GRAB_V2_INTERFACE
/**
* @page page_iface_zwp_input_method_keyboard_grab_v2 zwp_input_method_keyboard_grab_v2
* @section page_iface_zwp_input_method_keyboard_grab_v2_desc Description
*
* The zwp_input_method_keyboard_grab_v2 interface represents an exclusive
* grab of the wl_keyboard interface associated with the seat.
* @section page_iface_zwp_input_method_keyboard_grab_v2_api API
* See @ref iface_zwp_input_method_keyboard_grab_v2.
*/
/**
* @defgroup iface_zwp_input_method_keyboard_grab_v2 The zwp_input_method_keyboard_grab_v2 interface
*
* The zwp_input_method_keyboard_grab_v2 interface represents an exclusive
* grab of the wl_keyboard interface associated with the seat.
*/
extern const struct wl_interface zwp_input_method_keyboard_grab_v2_interface;
#endif
#ifndef ZWP_INPUT_METHOD_MANAGER_V2_INTERFACE
#define ZWP_INPUT_METHOD_MANAGER_V2_INTERFACE
/**
* @page page_iface_zwp_input_method_manager_v2 zwp_input_method_manager_v2
* @section page_iface_zwp_input_method_manager_v2_desc Description
*
* The input method manager allows the client to become the input method on
* a chosen seat.
*
* No more than one input method must be associated with any seat at any
* given time.
* @section page_iface_zwp_input_method_manager_v2_api API
* See @ref iface_zwp_input_method_manager_v2.
*/
/**
* @defgroup iface_zwp_input_method_manager_v2 The zwp_input_method_manager_v2 interface
*
* The input method manager allows the client to become the input method on
* a chosen seat.
*
* No more than one input method must be associated with any seat at any
* given time.
*/
extern const struct wl_interface zwp_input_method_manager_v2_interface;
#endif
#ifndef ZWP_INPUT_METHOD_V2_ERROR_ENUM
#define ZWP_INPUT_METHOD_V2_ERROR_ENUM
enum zwp_input_method_v2_error {
/**
* wl_surface has another role
*/
ZWP_INPUT_METHOD_V2_ERROR_ROLE = 0,
};
#endif /* ZWP_INPUT_METHOD_V2_ERROR_ENUM */
/**
* @ingroup iface_zwp_input_method_v2
* @struct zwp_input_method_v2_listener
*/
struct zwp_input_method_v2_listener {
/**
* input method has been requested
*
* Notification that a text input focused on this seat requested
* the input method to be activated.
*
* This event serves the purpose of providing the compositor with
* an active input method.
*
* This event resets all state associated with previous
* surrounding_text, text_change_cause, and content_type events, as
* well as the state associated with set_preedit_string,
* commit_string, and delete_surrounding_text requests. In
* addition, it marks the zwp_input_method_v2 object as active, and
* makes any existing zwp_input_popup_surface_v2 objects visible.
*
* The surrounding_text, and content_type events must follow before
* the next done event if the text input supports the respective
* functionality.
*
* State set with this event is double-buffered. It will get
* applied on the next zwp_input_method_v2.done event, and stay
* valid until changed.
*/
void (*activate)(void *data,
struct zwp_input_method_v2 *zwp_input_method_v2);
/**
* deactivate event
*
* Notification that no focused text input currently needs an
* active input method on this seat.
*
* This event marks the zwp_input_method_v2 object as inactive. The
* compositor must make all existing zwp_input_popup_surface_v2
* objects invisible until the next activate event.
*
* State set with this event is double-buffered. It will get
* applied on the next zwp_input_method_v2.done event, and stay
* valid until changed.
*/
void (*deactivate)(void *data,
struct zwp_input_method_v2 *zwp_input_method_v2);
/**
* surrounding text event
*
* Updates the surrounding plain text around the cursor,
* excluding the preedit text.
*
* If any preedit text is present, it is replaced with the cursor
* for the purpose of this event.
*
* The argument text is a buffer containing the surrounding text,
* and must include the cursor position, and the complete
* selection. It should contain additional characters before and
* after these. There is a maximum length of wayland messages, so
* text can not be longer than 4000 bytes.
*
* cursor is the byte offset of the cursor within the text buffer.
*
* anchor is the byte offset of the selection anchor within the
* text buffer. If there is no selected text, anchor must be the
* same as cursor.
*
* If this event does not arrive before the first done event, the
* input method may assume that the text input does not support
* this functionality and ignore following surrounding_text events.
*
* Values set with this event are double-buffered. They will get
* applied and set to initial values on the next
* zwp_input_method_v2.done event.
*
* The initial state for affected fields is empty, meaning that the
* text input does not support sending surrounding text. If the
* empty values get applied, subsequent attempts to change them may
* have no effect.
*/
void (*surrounding_text)(void *data,
struct zwp_input_method_v2 *zwp_input_method_v2,
const char *text,
uint32_t cursor,
uint32_t anchor);
/**
* indicates the cause of surrounding text change
*
* Tells the input method why the text surrounding the cursor
* changed.
*
* Whenever the client detects an external change in text, cursor,
* or anchor position, it must issue this request to the
* compositor. This request is intended to give the input method a
* chance to update the preedit text in an appropriate way, e.g. by
* removing it when the user starts typing with a keyboard.
*
* cause describes the source of the change.
*
* The value set with this event is double-buffered. It will get
* applied and set to its initial value on the next
* zwp_input_method_v2.done event.
*
* The initial value of cause is input_method.
*/
void (*text_change_cause)(void *data,
struct zwp_input_method_v2 *zwp_input_method_v2,
uint32_t cause);
/**
* content purpose and hint
*
* Indicates the content type and hint for the current
* zwp_input_method_v2 instance.
*
* Values set with this event are double-buffered. They will get
* applied on the next zwp_input_method_v2.done event.
*
* The initial value for hint is none, and the initial value for
* purpose is normal.
*/
void (*content_type)(void *data,
struct zwp_input_method_v2 *zwp_input_method_v2,
uint32_t hint,
uint32_t purpose);
/**
* apply state
*
* Atomically applies state changes recently sent to the client.
*
* The done event establishes and updates the state of the client,
* and must be issued after any changes to apply them.
*
* Text input state (content purpose, content hint, surrounding
* text, and change cause) is conceptually double-buffered within
* an input method context.
*
* Events modify the pending state, as opposed to the current state
* in use by the input method. A done event atomically applies all
* pending state, replacing the current state. After done, the new
* pending state is as documented for each related request.
*
* Events must be applied in the order of arrival.
*
* Neither current nor pending state are modified unless noted
* otherwise.
*/
void (*done)(void *data,
struct zwp_input_method_v2 *zwp_input_method_v2);
/**
* input method unavailable
*
* The input method ceased to be available.
*
* The compositor must issue this event as the only event on the
* object if there was another input_method object associated with
* the same seat at the time of its creation.
*
* The compositor must issue this request when the object is no
* longer usable, e.g. due to seat removal.
*
* The input method context becomes inert and should be destroyed
* after deactivation is handled. Any further requests and events
* except for the destroy request must be ignored.
*/
void (*unavailable)(void *data,
struct zwp_input_method_v2 *zwp_input_method_v2);
};
/**
* @ingroup iface_zwp_input_method_v2
*/
static inline int
zwp_input_method_v2_add_listener(struct zwp_input_method_v2 *zwp_input_method_v2,
const struct zwp_input_method_v2_listener *listener, void *data)
{
return wl_proxy_add_listener((struct wl_proxy *) zwp_input_method_v2,
(void (**)(void)) listener, data);
}
#define ZWP_INPUT_METHOD_V2_COMMIT_STRING 0
#define ZWP_INPUT_METHOD_V2_SET_PREEDIT_STRING 1
#define ZWP_INPUT_METHOD_V2_DELETE_SURROUNDING_TEXT 2
#define ZWP_INPUT_METHOD_V2_COMMIT 3
#define ZWP_INPUT_METHOD_V2_GET_INPUT_POPUP_SURFACE 4
#define ZWP_INPUT_METHOD_V2_GRAB_KEYBOARD 5
#define ZWP_INPUT_METHOD_V2_DESTROY 6
/**
* @ingroup iface_zwp_input_method_v2
*/
#define ZWP_INPUT_METHOD_V2_ACTIVATE_SINCE_VERSION 1
/**
* @ingroup iface_zwp_input_method_v2
*/
#define ZWP_INPUT_METHOD_V2_DEACTIVATE_SINCE_VERSION 1
/**
* @ingroup iface_zwp_input_method_v2
*/
#define ZWP_INPUT_METHOD_V2_SURROUNDING_TEXT_SINCE_VERSION 1
/**
* @ingroup iface_zwp_input_method_v2
*/
#define ZWP_INPUT_METHOD_V2_TEXT_CHANGE_CAUSE_SINCE_VERSION 1
/**
* @ingroup iface_zwp_input_method_v2
*/
#define ZWP_INPUT_METHOD_V2_CONTENT_TYPE_SINCE_VERSION 1
/**
* @ingroup iface_zwp_input_method_v2
*/
#define ZWP_INPUT_METHOD_V2_DONE_SINCE_VERSION 1
/**
* @ingroup iface_zwp_input_method_v2
*/
#define ZWP_INPUT_METHOD_V2_UNAVAILABLE_SINCE_VERSION 1
/**
* @ingroup iface_zwp_input_method_v2
*/
#define ZWP_INPUT_METHOD_V2_COMMIT_STRING_SINCE_VERSION 1
/**
* @ingroup iface_zwp_input_method_v2
*/
#define ZWP_INPUT_METHOD_V2_SET_PREEDIT_STRING_SINCE_VERSION 1
/**
* @ingroup iface_zwp_input_method_v2
*/
#define ZWP_INPUT_METHOD_V2_DELETE_SURROUNDING_TEXT_SINCE_VERSION 1
/**
* @ingroup iface_zwp_input_method_v2
*/
#define ZWP_INPUT_METHOD_V2_COMMIT_SINCE_VERSION 1
/**
* @ingroup iface_zwp_input_method_v2
*/
#define ZWP_INPUT_METHOD_V2_GET_INPUT_POPUP_SURFACE_SINCE_VERSION 1
/**
* @ingroup iface_zwp_input_method_v2
*/
#define ZWP_INPUT_METHOD_V2_GRAB_KEYBOARD_SINCE_VERSION 1
/**
* @ingroup iface_zwp_input_method_v2
*/
#define ZWP_INPUT_METHOD_V2_DESTROY_SINCE_VERSION 1
/** @ingroup iface_zwp_input_method_v2 */
static inline void
zwp_input_method_v2_set_user_data(struct zwp_input_method_v2 *zwp_input_method_v2, void *user_data)
{
wl_proxy_set_user_data((struct wl_proxy *) zwp_input_method_v2, user_data);
}
/** @ingroup iface_zwp_input_method_v2 */
static inline void *
zwp_input_method_v2_get_user_data(struct zwp_input_method_v2 *zwp_input_method_v2)
{
return wl_proxy_get_user_data((struct wl_proxy *) zwp_input_method_v2);
}
static inline uint32_t
zwp_input_method_v2_get_version(struct zwp_input_method_v2 *zwp_input_method_v2)
{
return wl_proxy_get_version((struct wl_proxy *) zwp_input_method_v2);
}
/**
* @ingroup iface_zwp_input_method_v2
*
* Send the commit string text for insertion to the application.
*
* Inserts a string at current cursor position (see commit event
* sequence). The string to commit could be either just a single character
* after a key press or the result of some composing.
*
* The argument text is a buffer containing the string to insert. There is
* a maximum length of wayland messages, so text can not be longer than
* 4000 bytes.
*
* Values set with this event are double-buffered. They must be applied
* and reset to initial on the next zwp_text_input_v3.commit request.
*
* The initial value of text is an empty string.
*/
static inline void
zwp_input_method_v2_commit_string(struct zwp_input_method_v2 *zwp_input_method_v2, const char *text)
{
wl_proxy_marshal_flags((struct wl_proxy *) zwp_input_method_v2,
ZWP_INPUT_METHOD_V2_COMMIT_STRING, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_input_method_v2), 0, text);
}
/**
* @ingroup iface_zwp_input_method_v2
*
* Send the pre-edit string text to the application text input.
*
* Place a new composing text (pre-edit) at the current cursor position.
* Any previously set composing text must be removed. Any previously
* existing selected text must be removed. The cursor is moved to a new
* position within the preedit string.
*
* The argument text is a buffer containing the preedit string. There is
* a maximum length of wayland messages, so text can not be longer than
* 4000 bytes.
*
* The arguments cursor_begin and cursor_end are counted in bytes relative
* to the beginning of the submitted string buffer. Cursor should be
* hidden by the text input when both are equal to -1.
*
* cursor_begin indicates the beginning of the cursor. cursor_end
* indicates the end of the cursor. It may be equal or different than
* cursor_begin.
*
* Values set with this event are double-buffered. They must be applied on
* the next zwp_input_method_v2.commit event.
*
* The initial value of text is an empty string. The initial value of
* cursor_begin, and cursor_end are both 0.
*/
static inline void
zwp_input_method_v2_set_preedit_string(struct zwp_input_method_v2 *zwp_input_method_v2, const char *text, int32_t cursor_begin, int32_t cursor_end)
{
wl_proxy_marshal_flags((struct wl_proxy *) zwp_input_method_v2,
ZWP_INPUT_METHOD_V2_SET_PREEDIT_STRING, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_input_method_v2), 0, text, cursor_begin, cursor_end);
}
/**
* @ingroup iface_zwp_input_method_v2
*
* Remove the surrounding text.
*
* before_length and after_length are the number of bytes before and after
* the current cursor index (excluding the preedit text) to delete.
*
* If any preedit text is present, it is replaced with the cursor for the
* purpose of this event. In effect before_length is counted from the
* beginning of preedit text, and after_length from its end (see commit
* event sequence).
*
* Values set with this event are double-buffered. They must be applied
* and reset to initial on the next zwp_input_method_v2.commit request.
*
* The initial values of both before_length and after_length are 0.
*/
static inline void
zwp_input_method_v2_delete_surrounding_text(struct zwp_input_method_v2 *zwp_input_method_v2, uint32_t before_length, uint32_t after_length)
{
wl_proxy_marshal_flags((struct wl_proxy *) zwp_input_method_v2,
ZWP_INPUT_METHOD_V2_DELETE_SURROUNDING_TEXT, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_input_method_v2), 0, before_length, after_length);
}
/**
* @ingroup iface_zwp_input_method_v2
*
* Apply state changes from commit_string, set_preedit_string and
* delete_surrounding_text requests.
*
* The state relating to these events is double-buffered, and each one
* modifies the pending state. This request replaces the current state
* with the pending state.
*
* The connected text input is expected to proceed by evaluating the
* changes in the following order:
*
* 1. Replace existing preedit string with the cursor.
* 2. Delete requested surrounding text.
* 3. Insert commit string with the cursor at its end.
* 4. Calculate surrounding text to send.
* 5. Insert new preedit text in cursor position.
* 6. Place cursor inside preedit text.
*
* The serial number reflects the last state of the zwp_input_method_v2
* object known to the client. The value of the serial argument must be
* equal to the number of done events already issued by that object. When
* the compositor receives a commit request with a serial different than
* the number of past done events, it must proceed as normal, except it
* should not change the current state of the zwp_input_method_v2 object.
*/
static inline void
zwp_input_method_v2_commit(struct zwp_input_method_v2 *zwp_input_method_v2, uint32_t serial)
{
wl_proxy_marshal_flags((struct wl_proxy *) zwp_input_method_v2,
ZWP_INPUT_METHOD_V2_COMMIT, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_input_method_v2), 0, serial);
}
/**
* @ingroup iface_zwp_input_method_v2
*
* Creates a new zwp_input_popup_surface_v2 object wrapping a given
* surface.
*
* The surface gets assigned the "input_popup" role. If the surface
* already has an assigned role, the compositor must issue a protocol
* error.
*/
static inline struct zwp_input_popup_surface_v2 *
zwp_input_method_v2_get_input_popup_surface(struct zwp_input_method_v2 *zwp_input_method_v2, struct wl_surface *surface)
{
struct wl_proxy *id;
id = wl_proxy_marshal_flags((struct wl_proxy *) zwp_input_method_v2,
ZWP_INPUT_METHOD_V2_GET_INPUT_POPUP_SURFACE, &zwp_input_popup_surface_v2_interface, wl_proxy_get_version((struct wl_proxy *) zwp_input_method_v2), 0, NULL, surface);
return (struct zwp_input_popup_surface_v2 *) id;
}
/**
* @ingroup iface_zwp_input_method_v2
*
* Allow an input method to receive hardware keyboard input and process
* key events to generate text events (with pre-edit) over the wire. This
* allows input methods which compose multiple key events for inputting
* text like it is done for CJK languages.
*
* The compositor should send all keyboard events on the seat to the grab
* holder via the returned wl_keyboard object. Nevertheless, the
* compositor may decide not to forward any particular event. The
* compositor must not further process any event after it has been
* forwarded to the grab holder.
*
* Releasing the resulting wl_keyboard object releases the grab.
*/
static inline struct zwp_input_method_keyboard_grab_v2 *
zwp_input_method_v2_grab_keyboard(struct zwp_input_method_v2 *zwp_input_method_v2)
{
struct wl_proxy *keyboard;
keyboard = wl_proxy_marshal_flags((struct wl_proxy *) zwp_input_method_v2,
ZWP_INPUT_METHOD_V2_GRAB_KEYBOARD, &zwp_input_method_keyboard_grab_v2_interface, wl_proxy_get_version((struct wl_proxy *) zwp_input_method_v2), 0, NULL);
return (struct zwp_input_method_keyboard_grab_v2 *) keyboard;
}
/**
* @ingroup iface_zwp_input_method_v2
*
* Destroys the zwp_text_input_v2 object and any associated child
* objects, i.e. zwp_input_popup_surface_v2 and
* zwp_input_method_keyboard_grab_v2.
*/
static inline void
zwp_input_method_v2_destroy(struct zwp_input_method_v2 *zwp_input_method_v2)
{
wl_proxy_marshal_flags((struct wl_proxy *) zwp_input_method_v2,
ZWP_INPUT_METHOD_V2_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_input_method_v2), WL_MARSHAL_FLAG_DESTROY);
}
/**
* @ingroup iface_zwp_input_popup_surface_v2
* @struct zwp_input_popup_surface_v2_listener
*/
struct zwp_input_popup_surface_v2_listener {
/**
* set text input area position
*
* Notify about the position of the area of the text input
* expressed as a rectangle in surface local coordinates.
*
* This is a hint to the input method telling it the relative
* position of the text being entered.
*/
void (*text_input_rectangle)(void *data,
struct zwp_input_popup_surface_v2 *zwp_input_popup_surface_v2,
int32_t x,
int32_t y,
int32_t width,
int32_t height);
};
/**
* @ingroup iface_zwp_input_popup_surface_v2
*/
static inline int
zwp_input_popup_surface_v2_add_listener(struct zwp_input_popup_surface_v2 *zwp_input_popup_surface_v2,
const struct zwp_input_popup_surface_v2_listener *listener, void *data)
{
return wl_proxy_add_listener((struct wl_proxy *) zwp_input_popup_surface_v2,
(void (**)(void)) listener, data);
}
#define ZWP_INPUT_POPUP_SURFACE_V2_DESTROY 0
/**
* @ingroup iface_zwp_input_popup_surface_v2
*/
#define ZWP_INPUT_POPUP_SURFACE_V2_TEXT_INPUT_RECTANGLE_SINCE_VERSION 1
/**
* @ingroup iface_zwp_input_popup_surface_v2
*/
#define ZWP_INPUT_POPUP_SURFACE_V2_DESTROY_SINCE_VERSION 1
/** @ingroup iface_zwp_input_popup_surface_v2 */
static inline void
zwp_input_popup_surface_v2_set_user_data(struct zwp_input_popup_surface_v2 *zwp_input_popup_surface_v2, void *user_data)
{
wl_proxy_set_user_data((struct wl_proxy *) zwp_input_popup_surface_v2, user_data);
}
/** @ingroup iface_zwp_input_popup_surface_v2 */
static inline void *
zwp_input_popup_surface_v2_get_user_data(struct zwp_input_popup_surface_v2 *zwp_input_popup_surface_v2)
{
return wl_proxy_get_user_data((struct wl_proxy *) zwp_input_popup_surface_v2);
}
static inline uint32_t
zwp_input_popup_surface_v2_get_version(struct zwp_input_popup_surface_v2 *zwp_input_popup_surface_v2)
{
return wl_proxy_get_version((struct wl_proxy *) zwp_input_popup_surface_v2);
}
/**
* @ingroup iface_zwp_input_popup_surface_v2
*/
static inline void
zwp_input_popup_surface_v2_destroy(struct zwp_input_popup_surface_v2 *zwp_input_popup_surface_v2)
{
wl_proxy_marshal_flags((struct wl_proxy *) zwp_input_popup_surface_v2,
ZWP_INPUT_POPUP_SURFACE_V2_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_input_popup_surface_v2), WL_MARSHAL_FLAG_DESTROY);
}
/**
* @ingroup iface_zwp_input_method_keyboard_grab_v2
* @struct zwp_input_method_keyboard_grab_v2_listener
*/
struct zwp_input_method_keyboard_grab_v2_listener {
/**
* keyboard mapping
*
* This event provides a file descriptor to the client which can
* be memory-mapped to provide a keyboard mapping description.
* @param format keymap format
* @param fd keymap file descriptor
* @param size keymap size, in bytes
*/
void (*keymap)(void *data,
struct zwp_input_method_keyboard_grab_v2 *zwp_input_method_keyboard_grab_v2,
uint32_t format,
int32_t fd,
uint32_t size);
/**
* key event
*
* A key was pressed or released. The time argument is a
* timestamp with millisecond granularity, with an undefined base.
* @param serial serial number of the key event
* @param time timestamp with millisecond granularity
* @param key key that produced the event
* @param state physical state of the key
*/
void (*key)(void *data,
struct zwp_input_method_keyboard_grab_v2 *zwp_input_method_keyboard_grab_v2,
uint32_t serial,
uint32_t time,
uint32_t key,
uint32_t state);
/**
* modifier and group state
*
* Notifies clients that the modifier and/or group state has
* changed, and it should update its local state.
* @param serial serial number of the modifiers event
* @param mods_depressed depressed modifiers
* @param mods_latched latched modifiers
* @param mods_locked locked modifiers
* @param group keyboard layout
*/
void (*modifiers)(void *data,
struct zwp_input_method_keyboard_grab_v2 *zwp_input_method_keyboard_grab_v2,
uint32_t serial,
uint32_t mods_depressed,
uint32_t mods_latched,
uint32_t mods_locked,
uint32_t group);
/**
* repeat rate and delay
*
* Informs the client about the keyboard's repeat rate and delay.
*
* This event is sent as soon as the
* zwp_input_method_keyboard_grab_v2 object has been created, and
* is guaranteed to be received by the client before any key press
* event.
*
* Negative values for either rate or delay are illegal. A rate of
* zero will disable any repeating (regardless of the value of
* delay).
*
* This event can be sent later on as well with a new value if
* necessary, so clients should continue listening for the event
* past the creation of zwp_input_method_keyboard_grab_v2.
* @param rate the rate of repeating keys in characters per second
* @param delay delay in milliseconds since key down until repeating starts
*/
void (*repeat_info)(void *data,
struct zwp_input_method_keyboard_grab_v2 *zwp_input_method_keyboard_grab_v2,
int32_t rate,
int32_t delay);
};
/**
* @ingroup iface_zwp_input_method_keyboard_grab_v2
*/
static inline int
zwp_input_method_keyboard_grab_v2_add_listener(struct zwp_input_method_keyboard_grab_v2 *zwp_input_method_keyboard_grab_v2,
const struct zwp_input_method_keyboard_grab_v2_listener *listener, void *data)
{
return wl_proxy_add_listener((struct wl_proxy *) zwp_input_method_keyboard_grab_v2,
(void (**)(void)) listener, data);
}
#define ZWP_INPUT_METHOD_KEYBOARD_GRAB_V2_RELEASE 0
/**
* @ingroup iface_zwp_input_method_keyboard_grab_v2
*/
#define ZWP_INPUT_METHOD_KEYBOARD_GRAB_V2_KEYMAP_SINCE_VERSION 1
/**
* @ingroup iface_zwp_input_method_keyboard_grab_v2
*/
#define ZWP_INPUT_METHOD_KEYBOARD_GRAB_V2_KEY_SINCE_VERSION 1
/**
* @ingroup iface_zwp_input_method_keyboard_grab_v2
*/
#define ZWP_INPUT_METHOD_KEYBOARD_GRAB_V2_MODIFIERS_SINCE_VERSION 1
/**
* @ingroup iface_zwp_input_method_keyboard_grab_v2
*/
#define ZWP_INPUT_METHOD_KEYBOARD_GRAB_V2_REPEAT_INFO_SINCE_VERSION 1
/**
* @ingroup iface_zwp_input_method_keyboard_grab_v2
*/
#define ZWP_INPUT_METHOD_KEYBOARD_GRAB_V2_RELEASE_SINCE_VERSION 1
/** @ingroup iface_zwp_input_method_keyboard_grab_v2 */
static inline void
zwp_input_method_keyboard_grab_v2_set_user_data(struct zwp_input_method_keyboard_grab_v2 *zwp_input_method_keyboard_grab_v2, void *user_data)
{
wl_proxy_set_user_data((struct wl_proxy *) zwp_input_method_keyboard_grab_v2, user_data);
}
/** @ingroup iface_zwp_input_method_keyboard_grab_v2 */
static inline void *
zwp_input_method_keyboard_grab_v2_get_user_data(struct zwp_input_method_keyboard_grab_v2 *zwp_input_method_keyboard_grab_v2)
{
return wl_proxy_get_user_data((struct wl_proxy *) zwp_input_method_keyboard_grab_v2);
}
static inline uint32_t
zwp_input_method_keyboard_grab_v2_get_version(struct zwp_input_method_keyboard_grab_v2 *zwp_input_method_keyboard_grab_v2)
{
return wl_proxy_get_version((struct wl_proxy *) zwp_input_method_keyboard_grab_v2);
}
/** @ingroup iface_zwp_input_method_keyboard_grab_v2 */
static inline void
zwp_input_method_keyboard_grab_v2_destroy(struct zwp_input_method_keyboard_grab_v2 *zwp_input_method_keyboard_grab_v2)
{
wl_proxy_destroy((struct wl_proxy *) zwp_input_method_keyboard_grab_v2);
}
/**
* @ingroup iface_zwp_input_method_keyboard_grab_v2
*/
static inline void
zwp_input_method_keyboard_grab_v2_release(struct zwp_input_method_keyboard_grab_v2 *zwp_input_method_keyboard_grab_v2)
{
wl_proxy_marshal_flags((struct wl_proxy *) zwp_input_method_keyboard_grab_v2,
ZWP_INPUT_METHOD_KEYBOARD_GRAB_V2_RELEASE, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_input_method_keyboard_grab_v2), WL_MARSHAL_FLAG_DESTROY);
}
#define ZWP_INPUT_METHOD_MANAGER_V2_GET_INPUT_METHOD 0
#define ZWP_INPUT_METHOD_MANAGER_V2_DESTROY 1
/**
* @ingroup iface_zwp_input_method_manager_v2
*/
#define ZWP_INPUT_METHOD_MANAGER_V2_GET_INPUT_METHOD_SINCE_VERSION 1
/**
* @ingroup iface_zwp_input_method_manager_v2
*/
#define ZWP_INPUT_METHOD_MANAGER_V2_DESTROY_SINCE_VERSION 1
/** @ingroup iface_zwp_input_method_manager_v2 */
static inline void
zwp_input_method_manager_v2_set_user_data(struct zwp_input_method_manager_v2 *zwp_input_method_manager_v2, void *user_data)
{
wl_proxy_set_user_data((struct wl_proxy *) zwp_input_method_manager_v2, user_data);
}
/** @ingroup iface_zwp_input_method_manager_v2 */
static inline void *
zwp_input_method_manager_v2_get_user_data(struct zwp_input_method_manager_v2 *zwp_input_method_manager_v2)
{
return wl_proxy_get_user_data((struct wl_proxy *) zwp_input_method_manager_v2);
}
static inline uint32_t
zwp_input_method_manager_v2_get_version(struct zwp_input_method_manager_v2 *zwp_input_method_manager_v2)
{
return wl_proxy_get_version((struct wl_proxy *) zwp_input_method_manager_v2);
}
/**
* @ingroup iface_zwp_input_method_manager_v2
*
* Request a new input zwp_input_method_v2 object associated with a given
* seat.
*/
static inline struct zwp_input_method_v2 *
zwp_input_method_manager_v2_get_input_method(struct zwp_input_method_manager_v2 *zwp_input_method_manager_v2, struct wl_seat *seat)
{
struct wl_proxy *input_method;
input_method = wl_proxy_marshal_flags((struct wl_proxy *) zwp_input_method_manager_v2,
ZWP_INPUT_METHOD_MANAGER_V2_GET_INPUT_METHOD, &zwp_input_method_v2_interface, wl_proxy_get_version((struct wl_proxy *) zwp_input_method_manager_v2), 0, seat, NULL);
return (struct zwp_input_method_v2 *) input_method;
}
/**
* @ingroup iface_zwp_input_method_manager_v2
*
* Destroys the zwp_input_method_manager_v2 object.
*
* The zwp_input_method_v2 objects originating from it remain valid.
*/
static inline void
zwp_input_method_manager_v2_destroy(struct zwp_input_method_manager_v2 *zwp_input_method_manager_v2)
{
wl_proxy_marshal_flags((struct wl_proxy *) zwp_input_method_manager_v2,
ZWP_INPUT_METHOD_MANAGER_V2_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_input_method_manager_v2), WL_MARSHAL_FLAG_DESTROY);
}
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -1,132 +0,0 @@
/* Generated by wayland-scanner 1.21.0 */
/*
* Copyright © 2008-2011 Kristian Høgsberg
* Copyright © 2010-2011 Intel Corporation
* Copyright © 2012-2013 Collabora, Ltd.
* Copyright © 2012, 2013 Intel Corporation
* Copyright © 2015, 2016 Jan Arne Petersen
* Copyright © 2017, 2018 Red Hat, Inc.
* Copyright © 2018 Purism SPC
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice (including the next
* paragraph) shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS 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. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
#include <stdlib.h>
#include <stdint.h>
#include "wayland-util.h"
#ifndef __has_attribute
# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */
#endif
#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4)
#define WL_PRIVATE __attribute__ ((visibility("hidden")))
#else
#define WL_PRIVATE
#endif
extern const struct wl_interface wl_seat_interface;
extern const struct wl_interface wl_surface_interface;
extern const struct wl_interface zwp_input_method_keyboard_grab_v2_interface;
extern const struct wl_interface zwp_input_method_v2_interface;
extern const struct wl_interface zwp_input_popup_surface_v2_interface;
static const struct wl_interface *input_method_unstable_v2_types[] = {
NULL,
NULL,
NULL,
NULL,
NULL,
&zwp_input_popup_surface_v2_interface,
&wl_surface_interface,
&zwp_input_method_keyboard_grab_v2_interface,
&wl_seat_interface,
&zwp_input_method_v2_interface,
};
static const struct wl_message zwp_input_method_v2_requests[] = {
{ "commit_string", "s", input_method_unstable_v2_types + 0 },
{ "set_preedit_string", "sii", input_method_unstable_v2_types + 0 },
{ "delete_surrounding_text", "uu", input_method_unstable_v2_types + 0 },
{ "commit", "u", input_method_unstable_v2_types + 0 },
{ "get_input_popup_surface", "no", input_method_unstable_v2_types + 5 },
{ "grab_keyboard", "n", input_method_unstable_v2_types + 7 },
{ "destroy", "", input_method_unstable_v2_types + 0 },
};
static const struct wl_message zwp_input_method_v2_events[] = {
{ "activate", "", input_method_unstable_v2_types + 0 },
{ "deactivate", "", input_method_unstable_v2_types + 0 },
{ "surrounding_text", "suu", input_method_unstable_v2_types + 0 },
{ "text_change_cause", "u", input_method_unstable_v2_types + 0 },
{ "content_type", "uu", input_method_unstable_v2_types + 0 },
{ "done", "", input_method_unstable_v2_types + 0 },
{ "unavailable", "", input_method_unstable_v2_types + 0 },
};
WL_PRIVATE const struct wl_interface zwp_input_method_v2_interface = {
"zwp_input_method_v2", 1,
7, zwp_input_method_v2_requests,
7, zwp_input_method_v2_events,
};
static const struct wl_message zwp_input_popup_surface_v2_requests[] = {
{ "destroy", "", input_method_unstable_v2_types + 0 },
};
static const struct wl_message zwp_input_popup_surface_v2_events[] = {
{ "text_input_rectangle", "iiii", input_method_unstable_v2_types + 0 },
};
WL_PRIVATE const struct wl_interface zwp_input_popup_surface_v2_interface = {
"zwp_input_popup_surface_v2", 1,
1, zwp_input_popup_surface_v2_requests,
1, zwp_input_popup_surface_v2_events,
};
static const struct wl_message zwp_input_method_keyboard_grab_v2_requests[] = {
{ "release", "", input_method_unstable_v2_types + 0 },
};
static const struct wl_message zwp_input_method_keyboard_grab_v2_events[] = {
{ "keymap", "uhu", input_method_unstable_v2_types + 0 },
{ "key", "uuuu", input_method_unstable_v2_types + 0 },
{ "modifiers", "uuuuu", input_method_unstable_v2_types + 0 },
{ "repeat_info", "ii", input_method_unstable_v2_types + 0 },
};
WL_PRIVATE const struct wl_interface zwp_input_method_keyboard_grab_v2_interface = {
"zwp_input_method_keyboard_grab_v2", 1,
1, zwp_input_method_keyboard_grab_v2_requests,
4, zwp_input_method_keyboard_grab_v2_events,
};
static const struct wl_message zwp_input_method_manager_v2_requests[] = {
{ "get_input_method", "on", input_method_unstable_v2_types + 8 },
{ "destroy", "", input_method_unstable_v2_types + 0 },
};
WL_PRIVATE const struct wl_interface zwp_input_method_manager_v2_interface = {
"zwp_input_method_manager_v2", 1,
2, zwp_input_method_manager_v2_requests,
0, NULL,
};

423
ipc.c
View File

@@ -1,12 +1,86 @@
#define _POSIX_C_SOURCE 200809L
#include <errno.h> #include <errno.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include <time.h>
#include <unistd.h> #include <unistd.h>
#include <sys/socket.h> #include <sys/socket.h>
#include <sys/un.h>
#include "ipc.h" #include "ipc.h"
#if !defined(MSG_NOSIGNAL) && !defined(SO_NOSIGPIPE) static int64_t
#error "ipcsend requires MSG_NOSIGNAL or SO_NOSIGPIPE" nowms(void)
#endif {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
static int64_t
deadline(void)
{
return nowms() + Ipcwaitms;
}
static int
waitfd(int fd, short events, int64_t until)
{
struct pollfd pfd;
int64_t left;
int n;
pfd.fd = fd;
pfd.events = events;
for(;;){
left = until - nowms();
if(left <= 0){
errno = ETIMEDOUT;
return -1;
}
n = poll(&pfd, 1, left);
if(n < 0 && errno == EINTR)
continue;
if(n < 0)
return -1;
if(n == 0){
errno = ETIMEDOUT;
return -1;
}
return 0;
}
}
static int
readwait(int fd, void *buf, size_t n, int64_t until)
{
unsigned char *p;
ssize_t r;
p = buf;
while(n > 0){
r = recv(fd, p, n, MSG_DONTWAIT);
if(r < 0 && errno == EINTR)
continue;
if(r < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)){
if(waitfd(fd, POLLIN, until) < 0)
return -1;
continue;
}
if(r < 0)
return -1;
if(r == 0){
errno = ECONNRESET;
return -1;
}
p += r;
n -= r;
}
return 0;
}
static void static void
putlen(unsigned char p[Ipclensz], size_t n) putlen(unsigned char p[Ipclensz], size_t n)
@@ -21,11 +95,123 @@ getlen(const unsigned char p[Ipclensz])
return p[0] | (p[1] << 8); return p[0] | (p[1] << 8);
} }
void static void
ipcpackreq(unsigned char req[Ipcreqsz], int want, unsigned int mod, put32(unsigned char *p, int32_t v)
unsigned int key)
{ {
req[0] = want != 0; uint32_t u;
u = v;
p[0] = u;
p[1] = u >> 8;
p[2] = u >> 16;
p[3] = u >> 24;
}
static int32_t
get32(const unsigned char *p)
{
return (int32_t)((uint32_t)p[0] |
((uint32_t)p[1] << 8) |
((uint32_t)p[2] << 16) |
((uint32_t)p[3] << 24));
}
int
ipcpath(char *dst, size_t cap)
{
const char *dir, *sep;
int n;
dir = getenv("XDG_RUNTIME_DIR");
if(dir != NULL && dir[0] == '/'){
sep = dir[strlen(dir)-1] == '/' ? "" : "/";
n = snprintf(dst, cap, "%s%sstrans.sock", dir, sep);
}else
n = snprintf(dst, cap, "/tmp/strans.%lu",
(unsigned long)getuid());
if(n < 0 || (size_t)n >= cap)
return -1;
return 0;
}
/*
* A nonblocking AF_UNIX connect completes at once or fails with EAGAIN
* when the daemon's backlog is full; the socket stays nonblocking because
* every transfer below polls with a deadline.
*/
int
ipcconnect(void)
{
struct sockaddr_un addr;
int e, fd;
memset(&addr, 0, sizeof addr);
addr.sun_family = AF_UNIX;
if(ipcpath(addr.sun_path, sizeof addr.sun_path) < 0){
errno = ENAMETOOLONG;
return -1;
}
fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
if(fd < 0)
return -1;
if(connect(fd, (struct sockaddr*)&addr, sizeof addr) < 0){
e = errno;
close(fd);
errno = e;
return -1;
}
return fd;
}
/*
* Engine key for an X keysym and its Unicode value: printable characters
* as themselves, the function keysyms 0xff00-0xffff as Kspec+offset, and
* no key at all for a modifier. The keypad keys and Shift+Tab
* (ISO_Left_Tab) fold onto their plain counterparts.
*/
uint32_t
ipckeysym(uint32_t sym, uint32_t unicode)
{
if(unicode >= ' ' && unicode != 0x7f)
return unicode;
if(sym >= 0xffe1 && sym <= 0xffee)
return 0;
switch(sym){
case 0xfe20:
case 0xff89:
return Ktab;
case 0xff8d:
return Kret;
case 0xff97:
return Kup;
case 0xff99:
return Kdown;
case 0xff9a:
return Kpgup;
case 0xff9b:
return Kpgdown;
}
if(sym >= 0xff00 && sym <= 0xffff)
return Kspec + (sym - 0xff00);
return unicode;
}
uint32_t
ipcmod(uint32_t state)
{
uint32_t m;
m = state & Mmask;
if(state & Msupervirt)
m |= Msuper;
return m;
}
void
ipcpackreq(unsigned char req[Ipcreqsz], int want, uint32_t mod,
uint32_t key)
{
req[0] = want ? Ipcreqwant : 0;
req[1] = mod; req[1] = mod;
req[2] = key; req[2] = key;
req[3] = key >> 8; req[3] = key >> 8;
@@ -34,41 +220,131 @@ ipcpackreq(unsigned char req[Ipcreqsz], int want, unsigned int mod,
} }
void void
ipcunpackreq(const unsigned char req[Ipcreqsz], int *want, unsigned int *mod, ipcpackreset(unsigned char req[Ipcreqsz], int want)
unsigned int *key)
{ {
*want = req[0] != 0; memset(req, 0, Ipcreqsz);
req[0] = Ipcreqreset | (want ? Ipcreqwant : 0);
}
/* An old server sees this reserved frame as a key-zero request. */
void
ipcpackcap(unsigned char req[Ipcreqsz], int want)
{
memset(req, 0, Ipcreqsz);
req[0] = Ipcext | (want ? Ipcreqwant : 0);
req[1] = Ipcversion;
req[2] = Ipcopcap;
}
void
ipcpackcaret(unsigned char req[Ipccaretsz], int valid, int32_t x,
int32_t y, int32_t h)
{
memset(req, 0, Ipccaretsz);
req[0] = Ipcext;
req[1] = Ipcversion;
req[2] = Ipcopcaret;
req[3] = valid != 0;
if(valid){
put32(req + 4, x);
put32(req + 8, y);
put32(req + 12, h);
}
}
void
ipcpacksurround(unsigned char req[Ipcreqsz], size_t n)
{
memset(req, 0, Ipcreqsz);
req[0] = Ipcext;
req[1] = Ipcversion;
req[2] = Ipcopsurround;
putlen(req + 4, n);
}
size_t
ipcsurroundlen(const unsigned char req[Ipcreqsz])
{
return getlen(req + 4);
}
void
ipcunpackreq(const unsigned char req[Ipcreqsz], int *want, uint32_t *mod,
uint32_t *key)
{
*want = (req[0] & Ipcreqwant) != 0;
*mod = req[1] & Mmask; *mod = req[1] & Mmask;
*key = (unsigned int)req[2] | *key = (uint32_t)req[2] |
((unsigned int)req[3] << 8) | ((uint32_t)req[3] << 8) |
((unsigned int)req[4] << 16) | ((uint32_t)req[4] << 16) |
((unsigned int)req[5] << 24); ((uint32_t)req[5] << 24);
} }
int int
ipcpackresp(unsigned char *dst, size_t cap, int eaten, ipcreqtype(const unsigned char req[Ipcreqsz])
{
if((req[0] & Ipcext) == 0)
return Ipckey;
if(req[1] != Ipcversion)
return Ipcunknown;
switch(req[2]){
case Ipcopcap:
if((req[0] & ~(Ipcext|Ipcreqwant)) != 0 ||
req[3] != 0 || req[4] != 0 || req[5] != 0)
return Ipcunknown;
return Ipccap;
case Ipcopcaret:
if(req[0] != Ipcext || req[3] > 1)
return Ipcunknown;
return Ipccaret;
case Ipcopsurround:
if(req[0] != Ipcext || req[3] != 0 ||
getlen(req + 4) > Ipcfieldmax)
return Ipcunknown;
return Ipcsurround;
}
return Ipcunknown;
}
int
ipcunpackcaret(const unsigned char req[Ipccaretsz], int *valid,
int32_t *x, int32_t *y, int32_t *h)
{
if(ipcreqtype(req) != Ipccaret || get32(req + 12) < 0)
return -1;
*valid = req[3];
*x = *valid ? get32(req + 4) : 0;
*y = *valid ? get32(req + 8) : 0;
*h = *valid ? get32(req + 12) : 0;
return 0;
}
int
ipcreqreset(const unsigned char req[Ipcreqsz])
{
return (req[0] & Ipcreqreset) != 0;
}
int
ipcpackresp(unsigned char *dst, size_t cap, int eaten, int del,
const char *commit, size_t ncommit, const char *preedit, size_t npreedit, const char *commit, size_t ncommit, const char *preedit, size_t npreedit,
int want) int want)
{ {
size_t n; size_t n;
if(ncommit > Ipcfieldmax || npreedit > Ipcfieldmax) if(ncommit > Ipcfieldmax || npreedit > Ipcfieldmax ||
return -1; del < 0 || del > 0xff)
if((ncommit > 0 && commit == NULL) ||
(want && npreedit > 0 && preedit == NULL))
return -1; return -1;
n = Ipcresphdrsz + ncommit + (want ? Ipclensz + npreedit : 0); n = Ipcresphdrsz + ncommit + (want ? Ipclensz + npreedit : 0);
if(dst == NULL || cap < n) if(cap < n)
return -1; return -1;
dst[0] = eaten != 0; dst[0] = eaten != 0;
putlen(dst + 1, ncommit); dst[1] = del;
if(ncommit > 0) putlen(dst + 2, ncommit);
memcpy(dst + Ipcresphdrsz, commit, ncommit); memcpy(dst + Ipcresphdrsz, commit, ncommit);
if(want){ if(want){
putlen(dst + Ipcresphdrsz + ncommit, npreedit); putlen(dst + Ipcresphdrsz + ncommit, npreedit);
if(npreedit > 0) memcpy(dst + Ipcresphdrsz + ncommit + Ipclensz, preedit, npreedit);
memcpy(dst + Ipcresphdrsz + ncommit + Ipclensz,
preedit, npreedit);
} }
return n; return n;
} }
@@ -97,90 +373,79 @@ ipcsend(int fd, const void *buf, size_t n)
{ {
const unsigned char *p; const unsigned char *p;
ssize_t r; ssize_t r;
int flags; int64_t until;
#if !defined(MSG_NOSIGNAL) && defined(SO_NOSIGPIPE)
int one, sr;
#endif
p = buf; p = buf;
#ifdef MSG_NOSIGNAL until = deadline();
flags = MSG_NOSIGNAL;
#else
flags = 0;
#ifdef SO_NOSIGPIPE
one = 1;
do
sr = setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof one);
while(sr < 0 && errno == EINTR);
if(sr < 0)
return -1;
#endif
#endif
while(n > 0){ while(n > 0){
r = send(fd, p, n, flags); r = send(fd, p, n, MSG_NOSIGNAL|MSG_DONTWAIT);
if(r < 0 && errno == EINTR) if(r < 0 && errno == EINTR)
continue; continue;
if(r <= 0) if(r < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)){
if(waitfd(fd, POLLOUT, until) < 0)
return -1; return -1;
continue;
}
if(r < 0)
return -1;
if(r == 0){
errno = EPIPE;
return -1;
}
p += r; p += r;
n -= r; n -= r;
} }
return 0; return 0;
} }
/* Reads a length-prefixed field into dst[Ipcfieldmax+1]. */
static int static int
readfield(int fd, size_t n, char *dst, size_t cap) readfield(int fd, char *dst, int64_t until)
{ {
unsigned char discard[128]; unsigned char len[Ipclensz];
size_t keep, part; size_t n;
if(cap > 0 && dst == NULL) if(readwait(fd, len, sizeof len, until) < 0)
return -1; return -1;
keep = 0; n = getlen(len);
if(cap > 0) if(n > Ipcfieldmax){
keep = n >= cap ? cap - 1 : n; errno = EPROTO;
if(keep > 0 && ipcreadn(fd, dst, keep) < 0)
return -1; return -1;
if(cap > 0)
dst[keep] = '\0';
n -= keep;
while(n > 0){
part = n < sizeof discard ? n : sizeof discard;
if(ipcreadn(fd, discard, part) < 0)
return -1;
n -= part;
} }
return 0; if(readwait(fd, dst, n, until) < 0)
return -1;
dst[n] = '\0';
return n;
} }
int int
ipcreadresp(int fd, int want, char *commit, size_t ccap, ipcreadresp(int fd, int want, char *commit, char *preedit, Ipcresp *resp)
char *preedit, size_t pcap, Ipcresp *resp)
{ {
unsigned char hdr[Ipcresphdrsz], npreedit[Ipclensz]; unsigned char head[2];
int64_t until;
int n;
if(resp == NULL || (ccap > 0 && commit == NULL) ||
(pcap > 0 && preedit == NULL))
return -1;
if(ccap > 0)
commit[0] = '\0'; commit[0] = '\0';
if(pcap > 0)
preedit[0] = '\0'; preedit[0] = '\0';
memset(resp, 0, sizeof *resp); memset(resp, 0, sizeof *resp);
if(ipcreadn(fd, hdr, sizeof hdr) < 0) until = deadline();
if(readwait(fd, head, sizeof head, until) < 0)
return -1; return -1;
resp->eaten = hdr[0] != 0; if(head[0] > 1){
resp->ncommit = getlen(hdr + 1); errno = EPROTO;
if(resp->ncommit > Ipcfieldmax)
return -1; return -1;
if(readfield(fd, resp->ncommit, commit, ccap) < 0) }
resp->eaten = head[0];
resp->del = head[1];
n = readfield(fd, commit, until);
if(n < 0)
return -1; return -1;
resp->commitlen = n;
if(!want) if(!want)
return 0; return 0;
if(ipcreadn(fd, npreedit, sizeof npreedit) < 0) n = readfield(fd, preedit, until);
if(n < 0)
return -1; return -1;
resp->npreedit = getlen(npreedit); resp->preeditlen = n;
if(resp->npreedit > Ipcfieldmax) return 0;
return -1;
return readfield(fd, resp->npreedit, preedit, pcap);
} }

92
ipc.h
View File

@@ -1,21 +1,42 @@
#ifndef STRANS_IPC_H
#define STRANS_IPC_H
#include <stddef.h> #include <stddef.h>
#include <stdint.h>
#define IPCPATH "/tmp/strans.%d"
/* /*
* Request: [want-preedit, modifiers, key byte 0, ..., key byte 3]. * Key request: [flags, modifiers, key byte 0, ..., key byte 3].
* The want flag asks for the preedit text: the client then draws it and
* the popup does not. The reset flag is a lifecycle reset, not Escape.
* The server identifies the connection as the engine owner; that identity is
* not sent on the wire.
* Only Mmask modifier bits are retained. * Only Mmask modifier bits are retained.
* Response: [eaten, commit-length-low, commit-length-high, commit...], * Response: [eaten, take-back, commit-length-low, commit-length-high,
* followed, when requested, by * commit...], followed, when requested, by
* [preedit-length-low, preedit-length-high, preedit...]. * [preedit-length-low, preedit-length-high, preedit...].
* Lengths are little-endian byte counts; fields are at most Ipcfieldmax bytes. * Take-back is the count of runes of the client's own text, just before the
* cursor, to remove before inserting the commit.
* Lengths are little-endian byte counts; fields are at most Ipcfieldmax bytes,
* so callers read them into char[Ipcfieldmax+1].
*
* Capability control is six bytes: [0x80|want, version, 0, 0, 0, 0]; the
* server always answers it eaten, which tells the client that extension
* frames are understood.
* Caret control is sixteen bytes: [0x80, version, 1, valid, x, y, h],
* with the signed coordinates and height encoded as little-endian 32-bit words.
* Surrounding-text control is six bytes, [0x80, version, 2, 0, length-low,
* length-high], and then that many bytes of the text just before the cursor.
* It changes what rides along with the requests that follow and is not
* answered.
*/ */
enum enum
{ {
Ipcreqwant = 1<<0,
Ipcreqreset = 1<<1,
Ipcext = 1<<7,
Ipcversion = 2,
Ipcopcap = 0,
Ipcopcaret = 1,
Ipcopsurround = 2,
Kspec = 0x110000, Kspec = 0x110000,
Kback = Kspec|0x08, Kback = Kspec|0x08,
Ktab = Kspec|0x09, Ktab = Kspec|0x09,
@@ -23,35 +44,66 @@ enum
Kesc = Kspec|0x1b, Kesc = Kspec|0x1b,
Kup = Kspec|0x52, Kup = Kspec|0x52,
Kdown = Kspec|0x54, Kdown = Kspec|0x54,
Kmodfirst = Kspec|0xe1, Kpgup = Kspec|0x55,
Kmodlast = Kspec|0xee, Kpgdown = Kspec|0x56,
Khangul = Kspec|0x31,
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. */
Mshift = 1<<0, Mshift = 1<<0,
Mctrl = 1<<2, Mctrl = 1<<2,
Malt = 1<<3, Malt = 1<<3,
Msuper = 1<<6, Msuper = 1<<6,
Mmask = Mshift|Mctrl|Malt|Msuper, Mmask = Mshift|Mctrl|Malt|Msuper,
Msupervirt = 1<<26,
Ipcreqsz = 6, Ipcreqsz = 6,
Ipccaretsz = 16,
Ipclensz = 2, Ipclensz = 2,
Ipcresphdrsz = 1 + Ipclensz, Ipcresphdrsz = 2 + Ipclensz,
Ipcfieldmax = 256, Ipcfieldmax = 256,
Ipcmaxresp = Ipcresphdrsz + Ipcfieldmax + Ipclensz + Ipcfieldmax, Ipcmaxresp = Ipcresphdrsz + Ipcfieldmax + Ipclensz + Ipcfieldmax,
Ipcwaitms = 250,
};
enum
{
Ipcunknown = -1,
Ipckey,
Ipccap,
Ipccaret,
Ipcsurround,
}; };
typedef struct Ipcresp Ipcresp; typedef struct Ipcresp Ipcresp;
struct Ipcresp struct Ipcresp
{ {
int eaten; int eaten;
size_t ncommit; int del; /* runes of the client's own text to take back first */
size_t npreedit; /* Bytes copied to each caller buffer, excluding its trailing NUL. */
size_t commitlen;
size_t preeditlen;
}; };
void ipcpackreq(unsigned char[Ipcreqsz], int, unsigned int, unsigned int); uint32_t ipckeysym(uint32_t, uint32_t);
void ipcunpackreq(const unsigned char[Ipcreqsz], int*, unsigned int*, unsigned int*); uint32_t ipcmod(uint32_t);
int ipcpackresp(unsigned char*, size_t, int, const char*, size_t, const char*, size_t, int); void ipcpackreq(unsigned char[Ipcreqsz], int, uint32_t, uint32_t);
void ipcpackreset(unsigned char[Ipcreqsz], int);
void ipcpackcap(unsigned char[Ipcreqsz], int);
void ipcpackcaret(unsigned char[Ipccaretsz], int, int32_t, int32_t, int32_t);
void ipcpacksurround(unsigned char[Ipcreqsz], size_t);
size_t ipcsurroundlen(const unsigned char[Ipcreqsz]);
void ipcunpackreq(const unsigned char[Ipcreqsz], int*, uint32_t*, uint32_t*);
int ipcunpackcaret(const unsigned char[Ipccaretsz], int*, int32_t*, int32_t*, int32_t*);
int ipcreqtype(const unsigned char[Ipcreqsz]);
int ipcreqreset(const unsigned char[Ipcreqsz]);
int ipcpackresp(unsigned char*, size_t, int, int, const char*, size_t, const char*, size_t, int);
int ipcreadn(int, void*, size_t); int ipcreadn(int, void*, size_t);
int ipcsend(int, const void*, size_t); int ipcsend(int, const void*, size_t);
int ipcreadresp(int, int, char*, size_t, char*, size_t, Ipcresp*); int ipcreadresp(int, int, char*, char*, Ipcresp*);
int ipcpath(char*, size_t);
#endif int ipcconnect(void);

81
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'',
@@ -189,6 +167,10 @@ issyl(Rune r)
return r >= Sbase && r < Sbase + Ncho*Njung*Njong; return r >= Sbase && r < Sbase + Ncho*Njung*Njong;
} }
/*
* The pending text is at most one syllable; a key either extends it,
* commits it and starts another, or commits it and passes through.
*/
Emit Emit
transko(Im *im, Rune c) transko(Im *im, Rune c)
{ {
@@ -199,10 +181,7 @@ transko(Im *im, Rune c)
memset(&e, 0, sizeof e); memset(&e, 0, sizeof e);
jm = keytojamo(c); jm = keytojamo(c);
if(jm == 0){ if(jm == 0){
if(im->pre.n > 0){
e.s = im->pre; e.s = im->pre;
sclear(&im->pre);
}
return e; return e;
} }
@@ -217,17 +196,26 @@ transko(Im *im, Rune c)
ci = Choidx(last); ci = Choidx(last);
ji = Jungidx(jm); ji = Jungidx(jm);
if(ci >= 0 && ji >= 0){ if(ci >= 0 && ji >= 0){
spopr(&im->pre);
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. */
if(Jungidx(last) >= 0 && Choidx(jm) >= 0){
sputr(&e.next, compose(Choidx(jm), Jungidx(last), 0));
return e;
}
/* Two vowels or two consonants may be one jamo, ㅘ or ㄳ. */
comb = combine(cvow, nelem(cvow), last, jm); comb = combine(cvow, nelem(cvow), last, jm);
if(comb == 0)
comb = combine(cjong, nelem(cjong), last, jm);
if(comb){ if(comb){
spopr(&im->pre);
sputr(&e.next, comb); sputr(&e.next, comb);
return e; 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);
@@ -239,7 +227,6 @@ transko(Im *im, Rune c)
if(joi == 0){ if(joi == 0){
ni = Jongidx(jm); ni = Jongidx(jm);
if(ni > 0){ if(ni > 0){
spopr(&im->pre);
sputr(&e.next, compose(ci, ji, ni)); sputr(&e.next, compose(ci, ji, ni));
return e; return e;
} }
@@ -248,7 +235,6 @@ transko(Im *im, Rune c)
comb = combine(cvow, nelem(cvow), jung[ji], jm); comb = combine(cvow, nelem(cvow), jung[ji], jm);
if(comb){ if(comb){
ni = Jungidx(comb); ni = Jungidx(comb);
spopr(&im->pre);
sputr(&e.next, compose(ci, ni, 0)); sputr(&e.next, compose(ci, ni, 0));
return e; return e;
} }
@@ -260,7 +246,6 @@ transko(Im *im, Rune c)
if(comb){ if(comb){
ni = Jongidx(comb); ni = Jongidx(comb);
if(ni > 0){ if(ni > 0){
spopr(&im->pre);
sputr(&e.next, compose(ci, ji, ni)); sputr(&e.next, compose(ci, ji, ni));
return e; return e;
} }
@@ -271,14 +256,12 @@ transko(Im *im, Rune c)
if(splitpair(cjong, nelem(cjong), jc, &stay, &next)){ if(splitpair(cjong, nelem(cjong), jc, &stay, &next)){
si = Jongidx(stay); si = Jongidx(stay);
ni = Choidx(next); ni = Choidx(next);
spopr(&im->pre);
sputr(&e.s, compose(ci, ji, si)); sputr(&e.s, compose(ci, ji, si));
sputr(&e.next, compose(ni, vi, 0)); sputr(&e.next, compose(ni, vi, 0));
return e; return e;
} }
ni = Choidx(jc); ni = Choidx(jc);
if(ni >= 0){ if(ni >= 0){
spopr(&im->pre);
sputr(&e.s, compose(ci, ji, 0)); sputr(&e.s, compose(ci, ji, 0));
sputr(&e.next, compose(ni, vi, 0)); sputr(&e.next, compose(ni, vi, 0));
return e; return e;
@@ -300,14 +283,12 @@ 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); spopr(&im->pre);
if(splitpair(cvow, nelem(cvow), last, &stay, &next) ||
splitpair(cjong, nelem(cjong), last, &stay, &next))
sputr(&im->pre, stay); sputr(&im->pre, stay);
return; return;
} }
spopr(&im->pre);
return;
}
decompose(last, &c, &j, &jo); decompose(last, &c, &j, &jo);
spopr(&im->pre); spopr(&im->pre);
if(jo > 0 && splitpair(cjong, nelem(cjong), jong[jo], &stay, &next)){ if(jo > 0 && splitpair(cjong, nelem(cjong), jong[jo], &stay, &next)){

47
main.c
View File

@@ -3,20 +3,11 @@
Channel *drawc; Channel *drawc;
Channel *keyc; Channel *keyc;
Channel *dictreqc;
Channel *dictresc;
char *fontdir;
int
threadmaybackground(void)
{
return 1;
}
void void
usage(void) usage(void)
{ {
fprint(2, "usage: strans mapdir fontdir\n"); fprint(2, "usage: strans mapdir\n");
threadexitsall("usage"); threadexitsall("usage");
} }
@@ -38,10 +29,9 @@ emalloc(ulong n)
{ {
void *p; void *p;
p = malloc(n); p = mallocz(n, 1);
if(p == nil) if(p == nil)
die("out of memory"); die("out of memory");
memset(p, 0, n);
return p; return p;
} }
@@ -57,22 +47,31 @@ erealloc(void *p, ulong n)
void void
threadmain(int argc, char **argv) threadmain(int argc, char **argv)
{ {
if(argc != 3) char *display, *scale;
if(argc != 2)
usage(); usage();
fontdir = argv[2]; /* Whichever popup runs is sized from it, and setfont reads Fontsz. */
scale = getenv("GDK_SCALE");
if(scale != nil && atoi(scale) > 1)
popupscale = min(atoi(scale), 4);
drawc = chancreate(sizeof(Drawcmd), 4); drawc = chancreate(sizeof(Drawcmd), 4);
keyc = chancreate(sizeof(Keyreq), 0); keyc = chancreate(sizeof(Keyreq), 0);
dictreqc = chancreate(sizeof(Dictreq), 4); langinit(argv[1]);
dictresc = chancreate(sizeof(Dictres), 0); composeinit();
mapinit(argv[1]); srvinit();
dictinit(argv[1]);
proccreate(drawthread, nil, 16384);
proccreate(srvthread, nil, 16384); proccreate(srvthread, nil, 16384);
proccreate(ibusthread, nil, 32768); proccreate(ibusthread, nil, 32768);
proccreate(waylandthread, nil, 32768); /* One popup per session: the Wayland frontend draws its own, so the
threadcreate(dictthread, nil, 16384); * X11 one and the XIM it serves stand down. */
threadcreate(imthread, nil, 16384); if(getenv("WAYLAND_DISPLAY") != nil && wlinit())
proccreate(wlthread, nil, 32768);
threadexits(nil); else{
proccreate(drawthread, nil, 16384);
display = getenv("DISPLAY");
if(display != nil && display[0] != '\0')
proccreate(ximthread, nil, 32768);
}
imthread(nil);
} }

View File

@@ -1,12 +1,130 @@
# # Dictionary data
# The following are a set of tools to obtain and process dictionaries from the SKK project in order to use them with ktrans(1).
#
# grabskkdicts pulls the skk kana-kanji conversion dictionaries from the skk-dev/dict repo.
# skk2ktrans takes an skk dictionary and converts it into a kanji jisho suitable to be used with ktrans(1).
#
# You can fetch and convert all the dictionaries by running this file.
#
grabskkdicts ## Korean Hanja and symbol data
for(d in skkdicts/SKK-JISYO.*)
<$d skk2ktrans >$d.jisho `hanja.src` and `mssymbol.src` are the tracked, reviewable sources for what
the Hanja search converts. Every data row is a result, a tab, and its
Hangul reading. A Hanja reads as a syllable or a word; a symbol reads as
the lone consonant a Korean keyboard's 한자 key offers it under:
```
漢 한
漢字 한자
※ ㅁ
```
`mkhanja` validates that contract and groups the rows of both sources by
their reading to produce the runtime dictionary. The two keyspaces do not
meet: a reading is either syllables or one jamo. Candidate order follows
source order. The sources keep all retained pairs for review; the
generated dictionary stores the first 128 candidates per reading because
that is the engine's lookup limit.
The sources are derived from libhangul release tag `libhangul-0.2.0`. The
annotated tag object is `20afc38922e3595ee3ed5b186f2ea05afe663763`, its
peeled commit is `41c702f5d3581325b646ef6249f1f641b0427ae0`, and
`data/hanja/hanja.txt` and `data/hanja/mssymbol.txt` have blob IDs
`199cfd70c4b306257ac2e018714a1285f7cf0ed3` and
`31c4e63d74293b6a759d4a2005cd1e7333746631`. Import and regenerate them
from the repository root with:
```sh
curl -L https://raw.githubusercontent.com/libhangul/libhangul/41c702f5d3581325b646ef6249f1f641b0427ae0/data/hanja/hanja.txt -o upstream
printf '%s %s\n' dd44dcc856cf542b1022d0f39c2e9b9f8805fdcc5923be80f04849ed97ce0996 upstream | sha256sum -c -
map/libhangul2hanja upstream >map/hanja.src
curl -L https://raw.githubusercontent.com/libhangul/libhangul/41c702f5d3581325b646ef6249f1f641b0427ae0/data/hanja/mssymbol.txt -o upstream
printf '%s %s\n' b685a4ebe2716b25eb29c42f6da6493716ecd78b604b44a33420987d21948e99 upstream | sha256sum -c -
map/libhangul2hanja upstream >map/mssymbol.src
map/mkhanja >map/hanja.dict
```
`libhangul2hanja` keeps a syllable reading only when its value is Hanja the
popup can draw: U+3400U+4DBF, U+4E00U+9FFF, or U+F900U+FAFF. Thus mixed
values and supplementary-plane ideographs are omitted. It keeps a jamo
reading, U+3131U+314E, only when its value is one rune the popup can draw
and a candidate row can carry — so `mssymbol.txt`'s ideographic space and
soft hyphen are omitted, leaving 985 of its 987 rows, and a Hanja under a
jamo reading is omitted as noise. The import preserves each upstream
license header and row order. The retained data is BSD 3-Clause licensed
by Choe Hwanjin; the complete text 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
`kanji.dict` is the historical dictionary bundled with strans. Its own header
identifies it as the SKK Medium dictionary, version 8.1 of May 24, 1995,
rearranged for Plan 9 ktrans by Kenji Okamoto on February 17, 2000. It was
already present when this repository was created in commit
`dcd1147638f908eca7e4b7616815e215022cc99d` on December 23, 2025. No original
SKK input file or upstream revision was committed, so the current file cannot
honestly be regenerated byte-for-byte from repository artifacts. The license
notice in its header permits redistribution and modification under GPL version
2 or later.
The repository copy keeps that historical candidate order. Duplicate keys
were merged at their first occurrence, later unseen candidates were appended
in source order, duplicate candidates were removed, and the empty
`ようたつ` row was removed.
## Importing a current SKK dictionary
From the repository root, fetch a known upstream revision and convert it with:
```
git clone https://github.com/skk-dev/dict.git map/skkdicts
git -C map/skkdicts checkout --detach REVISION
map/skk2ktrans map/skkdicts/SKK-JISYO.M >map/kanji.dict.new
python3 map/verifymap.py map/kanji.dict.new
```
Use a full skk-dev/dict commit ID for `REVISION` and record it when replacing
the bundled data. Git is needed only to fetch upstream data; it is not part of
the normal build image.
`skk2ktrans` accepts one or more EUC-JP SKK files (or standard input), writes
UTF-8 tab-separated rows, and merges input in command-line and source order.
It strips annotations, deduplicates candidates, and omits candidates containing
whitespace. Rows containing expressions or escaped candidate delimiters are
rejected; rewrite or omit those rows before import.
`verifymap.py` checks UTF-8, row structure, unique keys, 64-rune keys and
values, canonical candidate spacing, and duplicate dictionary candidates.
Pass it the exact `.map` and `.dict` files used for runtime or validation.
The normal build does not install map data.

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

View File

View File

@@ -1,2 +0,0 @@
#!/bin/rc
git/clone https://github.com/skk-dev/dict skkdicts

View File

187357
map/hanja.dict Normal file

File diff suppressed because it is too large Load Diff

266539
map/hanja.src Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,170 +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 だ
ti ち
chi ち
cha ちゃ
chu ちゅ
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 ふ
bu ぶ
vu ぶ
pu ぷ
he へ he へ
be べ
ve べ
pe ぺ
ho ほ ho ほ
fa ふぁ
fi ふぃ
fu ふ
fe ふぇ
fo ふぉ
fya ふゃ
fyu ふゅ
fyo ふょ
bya びゃ
byi びぃ
byu びゅ
bye びぇ
byo びょ
ba ば
bi び
bu ぶ
be べ
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 ぉ
kka っか xyi ぃ
kki っき xye ぇ
kkya っきゃ xka ヵ
kkyu っきゅ xke ヶ
kkyo っきょ xtu っ
kku っ xtsu っ
kke っけ xya ゃ
kko っこ xyu ゅ
ssa っさ xyo ょ
ssi っし xwa ゎ
sshi っし la ぁ
ssha っしゃ li ぃ
sshu っしゅ lu ぅ
sshe っし le
ssho っしょ lo ぉ
ssyo っしょ lyi ぃ
ssu っす lye ぇ
sse っせ lka ヵ
sso っそ lke ヶ
tta ltu
dda ltsu
tti っち lya ゃ
cchi っち lyu ゅ
ccha っちゃ lyo ょ
cchu っちゅ lwa ゎ
ttyu っちゅ qq っ
ccho っちょ vv っ
ttu ll
ttsu xx
ddu kk
tte gg
dde ss
tto zz
ddo jj
ppa tt
ppi tch
ppya っぴゃ dd っ
ppyu っぴゅ hh っ
ppyo っぴょ ff っ
ppu bb
ppe pp っ
ppo mm
yy っ
rr っ
ww っ
cc っ
- ー
~ 〜
. 。 . 。
, 、 , 、
[ 「
] 」

View File

@@ -1,28 +0,0 @@
 
,
. 。
< 《
> 》
/
?
;
:
\ 、
| ・
`
~ 〜
!
@
#
$ ¥
&
*
(
)
-
+
=
[ 「
] 」
{ 『
} 』

View File

@@ -60,7 +60,6 @@
あたらs 新 あたらs 新
あつi 熱 暑 厚 あつi 熱 暑 厚
あつk 厚 熱 暑 あつk 厚 熱 暑
あつk 厚 熱 暑
あつs 暑 あつs 暑
あつm 集 厚 あつm 集 厚
あつかe 扱 あつかe 扱
@@ -350,7 +349,6 @@
かわいr 可愛 かわいr 可愛
かんj 感 かんj 感
かんs 関 かんs 関
かんj 感
かんz 感 かんz 感
かんがe 考 かんがe 考
かんしゃs 感謝 かんしゃs 感謝
@@ -455,7 +453,7 @@
こu 乞 請 こu 乞 請
こy 来 こy 来
こうj 高 こうj 高
こうしょう 鉱床 高尚 こうしょう 鉱床 高尚 交渉
こうぶつ 鉱物 好物 こうぶつ 鉱物 好物
こうりょs 考慮 こうりょs 考慮
こえt 超 こえt 超
@@ -665,8 +663,6 @@
ただc 直 ただc 直
ただt 直 ただt 直
たとe 例 たとe 例
たおr 倒
たおr 倒
たのm 頼 たのm 頼
たのn 頼 たのn 頼
たのs 楽 たのs 楽
@@ -742,7 +738,6 @@
つみあげr 積み上げ つみあげr 積み上げ
つみあげt 積み上げ つみあげt 積み上げ
つめt 冷 つめt 冷
つづr 綴
つよi 強 つよi 強
つよk 強 つよk 強
つらi 辛 つらi 辛
@@ -793,7 +788,6 @@
とt 取 撮 採 とt 取 撮 採
とu 問 とu 問
とw 問 とw 問
とj 閉
といあw 問い合 といあw 問い合
とうj 投 とうj 投
とおi 遠 とおi 遠
@@ -913,13 +907,12 @@
ねらt 狙 ねらt 狙
ねらu 狙 ねらu 狙
のb 述 伸 延 のb 述 伸 延
のk 退 のk 退
のm 飲 呑 のm 飲 呑
のn 飲 のn 飲
のr 乗 載 のr 乗 載
のs 載 乗 のs 載 乗
のt 乗 載 のt 乗 載
のk 乗
のこr 残 のこr 残
のこs 残 のこs 残
のこt 残 のこt 残
@@ -1059,12 +1052,11 @@
ぼうえいs 防衛 ぼうえいs 防衛
まc 待 まc 待
まi 舞 まi 舞
まj 交 まj 交
まk 負 巻 まk 負 巻
まs 増 まs 増
まt 待 まt 待
まu 舞 まu 舞
まj 混
まz 混 先 まz 混 先
まいr 参 詣 まいr 参 詣
まいt 参 詣 まいt 参 詣
@@ -1241,8 +1233,6 @@
よわm 弱 よわm 弱
よわs 弱 よわs 弱
よわt 弱 よわt 弱
よろこb 喜 慶
よろこb 喜 慶
ろんj 論 ろんj 論
ろんz 論 ろんz 論
わk 分 湧 わk 分 湧
@@ -2267,7 +2257,6 @@
がくわり 学割 がくわり 学割
がけ 崖 がけ 崖
がし 樫 がし 樫
かせい 火星
がそ 画素 がそ 画素
がそかん 画素間 がそかん 画素間
がそごと 画素毎 がそごと 画素毎
@@ -2993,7 +2982,6 @@
こうしゃ 後者 公社 校舎 こうしゃ 後者 公社 校舎
こうしゅう 講習 公衆 こうしゅう 講習 公衆
こうしゅうかい 講習会 こうしゅうかい 講習会
こうしょう 交渉 鉱床
こうしん 更新 交信 後身 こうしん 更新 交信 後身
こうじ 麹 公示 高次 工事 孝二 こうじ 麹 公示 高次 工事 孝二
こうじつ 口実 こうじつ 口実
@@ -4802,7 +4790,7 @@
たいきゅうりょく 耐久力 たいきゅうりょく 耐久力
たいきょく 対極 たいきょく 対極
たいきん 大金 たいきん 大金
たいく 体躯 たいく 体躯 体育
たいくつ 退屈 たいくつ 退屈
たいけい 体系 体型 たいけい 体系 体型
たいけん 体験 たいけん 体験
@@ -4949,8 +4937,6 @@
たんらく 短絡 たんらく 短絡
だ 騨 駄 陀 楕 舵 柁 打 惰 妥 堕 唾 田 朶 だ 騨 駄 陀 楕 舵 柁 打 惰 妥 堕 唾 田 朶
だい 大 第 内 代 台 題 醍 だい 大 第 内 代 台 題 醍
たいく 体育
たいいく 体育
だいいち 第一 だいいち 第一
だいがく 大学 だいがく 大学
だいがくいん 大学院 だいがくいん 大学院
@@ -7314,7 +7300,6 @@
よこう 予行 予稿 よこう 予行 予稿
よこうち 横内 よこうち 横内
よこく 予告 よこく 予告
ようたつ
よこて 横手 よこて 横手
よこはま 横浜 よこはま 横浜
よこもじ 横文字 よこもじ 横文字
@@ -7426,7 +7411,7 @@
りゅうど 粒度 りゅうど 粒度
りゆう 理由 りゆう 理由
りょ 虜 旅 慮 侶 りょ 虜 旅 慮 侶
りょう 量 寮 両 領 陵 遼 諒 良 糧 稜 瞭 療 猟 涼 梁 料 凌 僚 亮 了 漁 粮 霊 りょう 量 寮 両 領 陵 遼 諒 良 糧 稜 瞭 療 猟 涼 梁 料 凌 僚 亮 了 漁 粮 霊
りょうあし 両足 りょうあし 両足
りょういき 領域 りょういき 領域
りょうか 量化 りょうか 量化

View File

@@ -1,142 +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 ショ
sha シャ
shi シ
shu シュ
she シェ
sho ショ
sa サ
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 ダ
ti ティ
chi チ
zi ヂ
cha チャ
chu チュ
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 ウォ
xe ェ
xo ォ
cc ッ
dd ッ
kk ッ
pp ッ
tt ッ
tch ッ
ss ッ
xn ン
di ディ
fa ファ
fi フィ
fe フェ
fo フォ
va ヴァ va ヴァ
vi ヴィ vi ヴィ
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 ッ
- ー
~ 〜
. 。 . 。
, 、 , 、
[ 「
] 」

98
map/libhangul2hanja Executable file
View File

@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""Extract Hanja and symbol readings from libhangul data."""
import sys
import unicodedata
from pathlib import Path
def ishangul(s):
return s != "" and all(0xAC00 <= ord(c) <= 0xD7A3 for c in s)
def isjamo(s):
return len(s) == 1 and 0x3131 <= ord(s) <= 0x314E
def ishanja(s):
return s != "" and all(0x3400 <= ord(c) <= 0x4DBF
or 0x4E00 <= ord(c) <= 0x9FFF
or 0xF900 <= ord(c) <= 0xFAFF for c in s)
def issymbol(s):
"""One rune the popup can draw and a candidate row can carry: not a
space, which the row separates candidates with, and not a formatting
character, which would leave a blank candidate to pick."""
return (len(s) == 1 and not s.isspace() and not ishanja(s)
and not unicodedata.category(s).startswith("C"))
def keeps(reading, value):
"""A syllable reading gives Hanja; a jamo reading gives a symbol."""
if ishangul(reading):
return ishanja(value)
return isjamo(reading) and issymbol(value)
def extract(src, name):
comments = []
entries = []
seen = set()
leading = True
for lineno, raw in enumerate(src, 1):
line = raw.rstrip("\r\n")
if not line:
continue
if line.startswith("#"):
if leading:
comments.append(line.rstrip())
continue
leading = False
fields = line.split(":")
if len(fields) != 3:
raise ValueError(f"{name}:{lineno}: need key:value:comment")
reading, value, _ = fields
if not keeps(reading, value):
continue
pair = (value, reading)
if pair in seen:
raise ValueError(f"{name}:{lineno}: duplicate reading")
seen.add(pair)
entries.append(pair)
if not entries:
raise ValueError(f"{name}: no readings")
return comments, entries
def main():
sys.stdin.reconfigure(encoding="utf-8")
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
if len(sys.argv) > 2:
print(f"usage: {sys.argv[0]} [libhangul-hanja.txt]", file=sys.stderr)
return 2
src = sys.stdin
name = "<stdin>"
try:
if len(sys.argv) == 2:
name = sys.argv[1]
src = Path(name).open(encoding="utf-8")
comments, entries = extract(src, name)
for line in comments:
print(line)
if comments:
print()
for value, reading in entries:
print(f"{value}\t{reading}")
except (OSError, UnicodeError, ValueError) as error:
print(error, file=sys.stderr)
return 1
finally:
if src is not sys.stdin:
src.close()
return 0
if __name__ == "__main__":
sys.exit(main())

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)
@@ -38,37 +50,31 @@ def read(path):
for field in fields[1:]: for field in fields[1:]:
alias = fold(field) alias = fold(field)
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 != 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): def build(entries):
values = table.setdefault(key, []) """One row per alias, in order of first appearance; the engine searches
the dictionary by prefix, so no prefix rows are needed."""
table = {}
for result, alias in entries:
values = table.setdefault(alias, [])
if result not in values: if result not in values:
values.append(result) values.append(result)
for alias, values in table.items():
yield f"{alias}\t{' '.join(values[:MAXCANDIDATES])}"
def build(entries):
exact = {}
prefix = {}
for result, alias in entries:
for n in range(1, len(alias) + 1):
add(exact if n == len(alias) else prefix, alias[:n], result)
for key in sorted(exact.keys() | prefix.keys()):
values = exact.get(key, []) + prefix.get(key, [])
values = list(dict.fromkeys(values))
yield f"{key}\t{' '.join(values)}"
def main(): 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)

96
map/mkhanja Executable file
View File

@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""Write hanja.dict from result-per-row UTF-8 sources."""
import sys
import unicodedata
from pathlib import Path
SOURCES = [Path(__file__).with_name(name)
for name in ("hanja.src", "mssymbol.src")]
MAXCANDIDATES = 128
def ishangul(s):
return s != "" and all(0xAC00 <= ord(c) <= 0xD7A3 for c in s)
def isjamo(s):
return len(s) == 1 and 0x3131 <= ord(s) <= 0x314E
def ishanja(s):
return s != "" and all(0x3400 <= ord(c) <= 0x4DBF
or 0x4E00 <= ord(c) <= 0x9FFF
or 0xF900 <= ord(c) <= 0xFAFF for c in s)
def issymbol(s):
"""One rune the popup can draw and a candidate row can carry: not a
space, which the row separates candidates with, and not a formatting
character, which would leave a blank candidate to pick."""
return (len(s) == 1 and not s.isspace() and not ishanja(s)
and not unicodedata.category(s).startswith("C"))
def read(path, table, seen):
"""Adds path's rows to table, keyed by reading, and returns its header."""
comments = []
leading = True
found = False
with path.open(encoding="utf-8") as src:
for lineno, raw in enumerate(src, 1):
line = raw.rstrip("\r\n")
if not line:
continue
if line.startswith("#"):
if leading:
comments.append(";;" + line[1:])
continue
leading = False
fields = line.split("\t")
if len(fields) != 2:
raise ValueError(f"{path}:{lineno}: need result<TAB>reading")
value, reading = fields
if ishangul(reading):
if not ishanja(value):
raise ValueError(f"{path}:{lineno}: need BMP Hanja")
elif isjamo(reading):
if not issymbol(value):
raise ValueError(f"{path}:{lineno}: need one symbol rune")
else:
raise ValueError(f"{path}:{lineno}: need a syllable or jamo "
"reading")
pair = (value, reading)
if pair in seen:
raise ValueError(f"{path}:{lineno}: duplicate reading")
seen.add(pair)
found = True
table.setdefault(reading, []).append(value)
if not found:
raise ValueError(f"{path}: no readings")
return comments
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
paths = [Path(arg) for arg in sys.argv[1:]] or SOURCES
table = {}
seen = set()
try:
comments = [line for path in paths for line in read(path, table, seen)]
for line in comments:
print(line)
if comments:
print()
for reading, candidates in table.items():
print(f"{reading}\t{' '.join(candidates[:MAXCANDIDATES])}")
except (OSError, UnicodeError, ValueError) as error:
print(error, file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -55,11 +55,15 @@ def vowel2():
tab = [ tab = [
("oa", "oa", "a"), ("oe", "oe", "e"), ("ai", "ai", "a"), ("oa", "oa", "a"), ("oe", "oe", "e"), ("ai", "ai", "a"),
("ao", "ao", "a"), ("au", "au", "a"), ("ay", "ay", "a"), ("ao", "ao", "a"), ("au", "au", "a"), ("ay", "ay", "a"),
("eu", "eu", "e"), ("iu", "iu", "i"), ("oi", "oi", "o"), ("eo", "eo", "e"), ("eu", "eu", "e"), ("ia", "ia", "i"),
("iu", "iu", "i"), ("oi", "oi", "o"), ("ua", "ua", "u"),
("ui", "ui", "u"), ("uy", "uy", "y"), ("ui", "ui", "u"), ("uy", "uy", "y"),
("aau", "âu", "â"), ("aay", "ây", "â"), ("eeu", "êu", "ê"),
("ooi", "ôi", "ô"), ("owi", "ơi", "ơ"), ("uwi", "ưi", "ư"),
("uwu", "ưu", "ư"), ("uwa", "ưa", "ư"), ("uee", "", "ê"),
("iee", "", "ê"), ("yee", "", "ê"), ("uoo", "", "ô"), ("iee", "", "ê"), ("yee", "", "ê"), ("uoo", "", "ô"),
("uow", "ươ", "ơ"), ("uaa", "", "â"), ("oaw", "", "ă"), ("uow", "ươ", "ơ"), ("uaa", "", "â"), ("oaw", "", "ă"),
("uwa", "ưa", "a"), ("uwow", "ươ", "ơ"), ("uwow", "ươ", "ơ"),
] ]
for i, o, v in tab: for i, o, v in tab:
emit(i, o) emit(i, o)
@@ -68,21 +72,21 @@ def vowel2():
emit("ie", "ie") emit("ie", "ie")
emit("ye", "ye") emit("ye", "ye")
emit("uo", "uo") emit("uo", "uo")
emit("ua", "ua")
def vowel3(): def vowel3():
# input, output, vowel # input, output, vowel
tab = [ tab = [
("ieeu", "iêu", "ê"), ("yeeu", "yêu", "ê"), ("ieeu", "iêu", "ê"), ("yeeu", "yêu", "ê"),
("uooi", "uôi", "ô"), ("uowi", "ươi", "ơ"), ("uooi", "uôi", "ô"), ("uowi", "ươi", "ơ"),
("oai", "oai", "a"), ("oay", "oay", "a"), ("uowu", "ươu", "ơ"), ("uwowi", "ươi", "ơ"),
("uyee", "uyê", "ê"), ("uwowu", "ươu", "ơ"), ("uaay", "uây", "â"),
("oai", "oai", "a"), ("oay", "oay", "a"), ("oeo", "oeo", "e"),
("uya", "uya", "y"), ("uyu", "uyu", "y"), ("uyee", "uyê", "ê"),
] ]
for i, o, v in tab: for i, o, v in tab:
emit(i, o) emit(i, o)
for t in tone: for t in tone:
emit(i+t, o.replace(v, addtone(v, t), 1)) emit(i+t, o.replace(v, addtone(v, t), 1))
emit("uya", "uya")
def modvowels(): def modvowels():
for i, o in modvowel: for i, o in modvowel:
@@ -132,10 +136,11 @@ def final():
("aw", "ă", "ă"), ("aa", "â", "â"), ("ee", "ê", "ê"), ("aw", "ă", "ă"), ("aa", "â", "â"), ("ee", "ê", "ê"),
("oo", "ô", "ô"), ("ow", "ơ", "ơ"), ("uw", "ư", "ư"), ("oo", "ô", "ô"), ("ow", "ơ", "ơ"), ("uw", "ư", "ư"),
("iee", "", "ê"), ("yee", "", "ê"), ("uoo", "", "ô"), ("iee", "", "ê"), ("yee", "", "ê"), ("uoo", "", "ô"),
("uow", "ươ", "ơ"), ("uaa", "", "â"), ("oaw", "", "ă"), ("uow", "ươ", "ơ"), ("uwow", "ươ", "ơ"), ("uyee", "uyê", "ê"),
("uaa", "", "â"), ("uee", "", "ê"), ("oaw", "", "ă"),
("ai", "ai", "a"), ("ao", "ao", "a"), ("au", "au", "a"), ("ai", "ai", "a"), ("ao", "ao", "a"), ("au", "au", "a"),
("ay", "ay", "a"), ("oa", "oa", "a"), ("oi", "oi", "o"), ("ay", "ay", "a"), ("oa", "oa", "a"), ("oe", "oe", "e"),
("ui", "ui", "u"), ("uy", "uy", "y"), ("oi", "oi", "o"), ("ui", "ui", "u"), ("uy", "uy", "y"),
] ]
for i, o, v in tab: for i, o, v in tab:
for c in coda: for c in coda:

1012
map/mssymbol.src Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +1,69 @@
#!/bin/rc #!/bin/bash
tcs -sf ujis | awk '$1 !~ /;;/ {gsub("(^\/|\/$)", "", $2); gsub(" ", " "); gsub("\/", " ", $2);} {print}'
set -euo pipefail
transcode()
{
local file
if (( $# == 0 )); then
iconv -f EUC-JP -t UTF-8
return
fi
for file in "$@"; do
iconv -f EUC-JP -t UTF-8 "$file"
printf '\n'
done
}
transcode "$@" | awk '
function fail(s) {
print "skk2ktrans: " FILENAME ":" FNR ": " s >"/dev/stderr"
bad = 1
exit 1
}
function add(k, v, id) {
sub(/;.*/, "", v)
if(v == "" || v ~ /^[([#]/ || v ~ /[[:space:]]/)
return
id = k SUBSEP v
if(seen[id])
return
seen[id] = 1
if(!(k in row))
order[++nkey] = k
else
row[k] = row[k] " "
row[k] = row[k] v
}
/^;;/ {
if(!body) # the header, with its license notice, is kept
print
next
}
/^[[:space:]]*$/ {
next
}
{
body = 1
if(!match($0, /[[:space:]]+/))
fail("missing candidate list")
key = substr($0, 1, RSTART-1)
field = substr($0, RSTART+RLENGTH)
if(key == "" || key ~ /^;/ || field !~ /^\/.*\/$/)
fail("invalid row")
if(field ~ /\\/)
fail("escaped candidates are unsupported")
if(field ~ /\/[([#]/)
fail("expression candidates are unsupported")
n = split(substr(field, 2, length(field)-2), value, "/")
for(i = 1; i <= n; i++)
add(key, value[i])
}
END {
if(bad)
exit 1
for(i = 1; i <= nkey; i++)
print order[i] "\t" row[order[i]]
}
'

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

@@ -142,6 +142,18 @@ ayx ãy
Ayx Ãy Ayx Ãy
ayj ạy ayj ạy
Ayj Ạy Ayj Ạy
eo eo
Eo Eo
eos éo
Eos Éo
eof èo
Eof Èo
eor ẻo
Eor Ẻo
eox ẽo
Eox Ẽo
eoj ẹo
Eoj Ẹo
eu eu eu eu
Eu Eu Eu Eu
eus éu eus éu
@@ -154,6 +166,18 @@ eux ẽu
Eux Ẽu Eux Ẽu
euj ẹu euj ẹu
Euj Ẹu Euj Ẹu
ia ia
Ia Ia
ias ía
Ias Ía
iaf ìa
Iaf Ìa
iar ỉa
Iar Ỉa
iax ĩa
Iax Ĩa
iaj ịa
Iaj Ịa
iu iu iu iu
Iu Iu Iu Iu
ius íu ius íu
@@ -178,6 +202,18 @@ oix õi
Oix Õi Oix Õi
oij ọi oij ọi
Oij Ọi Oij Ọi
ua ua
Ua Ua
uas úa
Uas Úa
uaf ùa
Uaf Ùa
uar ủa
Uar Ủa
uax ũa
Uax Ũa
uaj ụa
Uaj Ụa
ui ui ui ui
Ui Ui Ui Ui
uis úi uis úi
@@ -202,6 +238,114 @@ uyx uỹ
Uyx Uỹ Uyx Uỹ
uyj uỵ uyj uỵ
Uyj Uỵ Uyj Uỵ
aau âu
Aau Âu
aaus ấu
Aaus Ấu
aauf ầu
Aauf Ầu
aaur ẩu
Aaur Ẩu
aaux ẫu
Aaux Ẫu
aauj ậu
Aauj Ậu
aay ây
Aay Ây
aays ấy
Aays Ấy
aayf ầy
Aayf Ầy
aayr ẩy
Aayr Ẩy
aayx ẫy
Aayx Ẫy
aayj ậy
Aayj Ậy
eeu êu
Eeu Êu
eeus ếu
Eeus Ếu
eeuf ều
Eeuf Ều
eeur ểu
Eeur Ểu
eeux ễu
Eeux Ễu
eeuj ệu
Eeuj Ệu
ooi ôi
Ooi Ôi
oois ối
Oois Ối
ooif ồi
Ooif Ồi
ooir ổi
Ooir Ổi
ooix ỗi
Ooix Ỗi
ooij ội
Ooij Ội
owi ơi
Owi Ơi
owis ới
Owis Ới
owif ời
Owif Ời
owir ởi
Owir Ởi
owix ỡi
Owix Ỡi
owij ợi
Owij Ợi
uwi ưi
Uwi Ưi
uwis ứi
Uwis Ứi
uwif ừi
Uwif Ừi
uwir ửi
Uwir Ửi
uwix ữi
Uwix Ữi
uwij ựi
Uwij Ựi
uwu ưu
Uwu Ưu
uwus ứu
Uwus Ứu
uwuf ừu
Uwuf Ừu
uwur ửu
Uwur Ửu
uwux ữu
Uwux Ữu
uwuj ựu
Uwuj Ựu
uwa ưa
Uwa Ưa
uwas ứa
Uwas Ứa
uwaf ừa
Uwaf Ừa
uwar ửa
Uwar Ửa
uwax ữa
Uwax Ữa
uwaj ựa
Uwaj Ựa
uee uê
Uee Uê
uees uế
Uees Uế
ueef uề
Ueef Uề
ueer uể
Ueer Uể
ueex uễ
Ueex Uễ
ueej uệ
Ueej Uệ
iee iê iee iê
Iee Iê Iee Iê
iees iế iees iế
@@ -274,18 +418,6 @@ oawx oẵ
Oawx Oẵ Oawx Oẵ
oawj oặ oawj oặ
Oawj Oặ Oawj Oặ
uwa ưa
Uwa Ưa
uwas ưá
Uwas Ưá
uwaf ưà
Uwaf Ưà
uwar ưả
Uwar Ưả
uwax ưã
Uwax Ưã
uwaj ưạ
Uwaj Ưạ
uwow ươ uwow ươ
Uwow Ươ Uwow Ươ
uwows ướ uwows ướ
@@ -304,8 +436,6 @@ ye ye
Ye Ye Ye Ye
uo uo uo uo
Uo Uo Uo Uo
ua ua
Ua Ua
ieeu iêu ieeu iêu
Ieeu Iêu Ieeu Iêu
ieeus iếu ieeus iếu
@@ -354,6 +484,54 @@ uowix ưỡi
Uowix Ưỡi Uowix Ưỡi
uowij ượi uowij ượi
Uowij Ượi Uowij Ượi
uowu ươu
Uowu Ươu
uowus ướu
Uowus Ướu
uowuf ườu
Uowuf Ườu
uowur ưởu
Uowur Ưởu
uowux ưỡu
Uowux Ưỡu
uowuj ượu
Uowuj Ượu
uwowi ươi
Uwowi Ươi
uwowis ưới
Uwowis Ưới
uwowif ười
Uwowif Ười
uwowir ưởi
Uwowir Ưởi
uwowix ưỡi
Uwowix Ưỡi
uwowij ượi
Uwowij Ượi
uwowu ươu
Uwowu Ươu
uwowus ướu
Uwowus Ướu
uwowuf ườu
Uwowuf Ườu
uwowur ưởu
Uwowur Ưởu
uwowux ưỡu
Uwowux Ưỡu
uwowuj ượu
Uwowuj Ượu
uaay uây
Uaay Uây
uaays uấy
Uaays Uấy
uaayf uầy
Uaayf Uầy
uaayr uẩy
Uaayr Uẩy
uaayx uẫy
Uaayx Uẫy
uaayj uậy
Uaayj Uậy
oai oai oai oai
Oai Oai Oai Oai
oais oái oais oái
@@ -378,6 +556,42 @@ oayx oãy
Oayx Oãy Oayx Oãy
oayj oạy oayj oạy
Oayj Oạy Oayj Oạy
oeo oeo
Oeo Oeo
oeos oéo
Oeos Oéo
oeof oèo
Oeof Oèo
oeor oẻo
Oeor Oẻo
oeox oẽo
Oeox Oẽo
oeoj oẹo
Oeoj Oẹo
uya uya
Uya Uya
uyas uýa
Uyas Uýa
uyaf uỳa
Uyaf Uỳa
uyar uỷa
Uyar Uỷa
uyax uỹa
Uyax Uỹa
uyaj uỵa
Uyaj Uỵa
uyu uyu
Uyu Uyu
uyus uýu
Uyus Uýu
uyuf uỳu
Uyuf Uỳu
uyur uỷu
Uyur Uỷu
uyux uỹu
Uyux Uỹu
uyuj uỵu
Uyuj Uỵu
uyee uyê uyee uyê
Uyee Uyê Uyee Uyê
uyees uyế uyees uyế
@@ -390,8 +604,6 @@ uyeex uyễ
Uyeex Uyễ Uyeex Uyễ
uyeej uyệ uyeej uyệ
Uyeej Uyệ Uyeej Uyệ
uya uya
Uya Uya
aw ă aw ă
Aw Ă Aw Ă
aa â aa â
@@ -2146,6 +2358,198 @@ uownhx ưỡnh
Uownhx Ưỡnh Uownhx Ưỡnh
uownhj ượnh uownhj ượnh
Uownhj Ượnh Uownhj Ượnh
uwowc ươc
Uwowc Ươc
uwowcs ước
Uwowcs Ước
uwowcf ườc
Uwowcf Ườc
uwowcr ưởc
Uwowcr Ưởc
uwowcx ưỡc
Uwowcx Ưỡc
uwowcj ược
Uwowcj Ược
uwowm ươm
Uwowm Ươm
uwowms ướm
Uwowms Ướm
uwowmf ườm
Uwowmf Ườm
uwowmr ưởm
Uwowmr Ưởm
uwowmx ưỡm
Uwowmx Ưỡm
uwowmj ượm
Uwowmj Ượm
uwown ươn
Uwown Ươn
uwowns ướn
Uwowns Ướn
uwownf ườn
Uwownf Ườn
uwownr ưởn
Uwownr Ưởn
uwownx ưỡn
Uwownx Ưỡn
uwownj ượn
Uwownj Ượn
uwowp ươp
Uwowp Ươp
uwowps ướp
Uwowps Ướp
uwowpf ườp
Uwowpf Ườp
uwowpr ưởp
Uwowpr Ưởp
uwowpx ưỡp
Uwowpx Ưỡp
uwowpj ượp
Uwowpj Ượp
uwowt ươt
Uwowt Ươt
uwowts ướt
Uwowts Ướt
uwowtf ườt
Uwowtf Ườt
uwowtr ưởt
Uwowtr Ưởt
uwowtx ưỡt
Uwowtx Ưỡt
uwowtj ượt
Uwowtj Ượt
uwowch ươch
Uwowch Ươch
uwowchs ướch
Uwowchs Ướch
uwowchf ườch
Uwowchf Ườch
uwowchr ưởch
Uwowchr Ưởch
uwowchx ưỡch
Uwowchx Ưỡch
uwowchj ượch
Uwowchj Ượch
uwowng ương
Uwowng Ương
uwowngs ướng
Uwowngs Ướng
uwowngf ường
Uwowngf Ường
uwowngr ưởng
Uwowngr Ưởng
uwowngx ưỡng
Uwowngx Ưỡng
uwowngj ượng
Uwowngj Ượng
uwownh ươnh
Uwownh Ươnh
uwownhs ướnh
Uwownhs Ướnh
uwownhf ườnh
Uwownhf Ườnh
uwownhr ưởnh
Uwownhr Ưởnh
uwownhx ưỡnh
Uwownhx Ưỡnh
uwownhj ượnh
Uwownhj Ượnh
uyeec uyêc
Uyeec Uyêc
uyeecs uyếc
Uyeecs Uyếc
uyeecf uyềc
Uyeecf Uyềc
uyeecr uyểc
Uyeecr Uyểc
uyeecx uyễc
Uyeecx Uyễc
uyeecj uyệc
Uyeecj Uyệc
uyeem uyêm
Uyeem Uyêm
uyeems uyếm
Uyeems Uyếm
uyeemf uyềm
Uyeemf Uyềm
uyeemr uyểm
Uyeemr Uyểm
uyeemx uyễm
Uyeemx Uyễm
uyeemj uyệm
Uyeemj Uyệm
uyeen uyên
Uyeen Uyên
uyeens uyến
Uyeens Uyến
uyeenf uyền
Uyeenf Uyền
uyeenr uyển
Uyeenr Uyển
uyeenx uyễn
Uyeenx Uyễn
uyeenj uyện
Uyeenj Uyện
uyeep uyêp
Uyeep Uyêp
uyeeps uyếp
Uyeeps Uyếp
uyeepf uyềp
Uyeepf Uyềp
uyeepr uyểp
Uyeepr Uyểp
uyeepx uyễp
Uyeepx Uyễp
uyeepj uyệp
Uyeepj Uyệp
uyeet uyêt
Uyeet Uyêt
uyeets uyết
Uyeets Uyết
uyeetf uyềt
Uyeetf Uyềt
uyeetr uyểt
Uyeetr Uyểt
uyeetx uyễt
Uyeetx Uyễt
uyeetj uyệt
Uyeetj Uyệt
uyeech uyêch
Uyeech Uyêch
uyeechs uyếch
Uyeechs Uyếch
uyeechf uyềch
Uyeechf Uyềch
uyeechr uyểch
Uyeechr Uyểch
uyeechx uyễch
Uyeechx Uyễch
uyeechj uyệch
Uyeechj Uyệch
uyeeng uyêng
Uyeeng Uyêng
uyeengs uyếng
Uyeengs Uyếng
uyeengf uyềng
Uyeengf Uyềng
uyeengr uyểng
Uyeengr Uyểng
uyeengx uyễng
Uyeengx Uyễng
uyeengj uyệng
Uyeengj Uyệng
uyeenh uyênh
Uyeenh Uyênh
uyeenhs uyếnh
Uyeenhs Uyếnh
uyeenhf uyềnh
Uyeenhf Uyềnh
uyeenhr uyểnh
Uyeenhr Uyểnh
uyeenhx uyễnh
Uyeenhx Uyễnh
uyeenhj uyệnh
Uyeenhj Uyệnh
uaac uâc uaac uâc
Uaac Uâc Uaac Uâc
uaacs uấc uaacs uấc
@@ -2242,6 +2646,102 @@ uaanhx uẫnh
Uaanhx Uẫnh Uaanhx Uẫnh
uaanhj uậnh uaanhj uậnh
Uaanhj Uậnh Uaanhj Uậnh
ueec uêc
Ueec Uêc
ueecs uếc
Ueecs Uếc
ueecf uềc
Ueecf Uềc
ueecr uểc
Ueecr Uểc
ueecx uễc
Ueecx Uễc
ueecj uệc
Ueecj Uệc
ueem uêm
Ueem Uêm
ueems uếm
Ueems Uếm
ueemf uềm
Ueemf Uềm
ueemr uểm
Ueemr Uểm
ueemx uễm
Ueemx Uễm
ueemj uệm
Ueemj Uệm
ueen uên
Ueen Uên
ueens uến
Ueens Uến
ueenf uền
Ueenf Uền
ueenr uển
Ueenr Uển
ueenx uễn
Ueenx Uễn
ueenj uện
Ueenj Uện
ueep uêp
Ueep Uêp
ueeps uếp
Ueeps Uếp
ueepf uềp
Ueepf Uềp
ueepr uểp
Ueepr Uểp
ueepx uễp
Ueepx Uễp
ueepj uệp
Ueepj Uệp
ueet uêt
Ueet Uêt
ueets uết
Ueets Uết
ueetf uềt
Ueetf Uềt
ueetr uểt
Ueetr Uểt
ueetx uễt
Ueetx Uễt
ueetj uệt
Ueetj Uệt
ueech uêch
Ueech Uêch
ueechs uếch
Ueechs Uếch
ueechf uềch
Ueechf Uềch
ueechr uểch
Ueechr Uểch
ueechx uễch
Ueechx Uễch
ueechj uệch
Ueechj Uệch
ueeng uêng
Ueeng Uêng
ueengs uếng
Ueengs Uếng
ueengf uềng
Ueengf Uềng
ueengr uểng
Ueengr Uểng
ueengx uễng
Ueengx Uễng
ueengj uệng
Ueengj Uệng
ueenh uênh
Ueenh Uênh
ueenhs uếnh
Ueenhs Uếnh
ueenhf uềnh
Ueenhf Uềnh
ueenhr uểnh
Ueenhr Uểnh
ueenhx uễnh
Ueenhx Uễnh
ueenhj uệnh
Ueenhj Uệnh
oawc oăc oawc oăc
Oawc Oăc Oawc Oăc
oawcs oắc oawcs oắc
@@ -2818,6 +3318,102 @@ oanhx oãnh
Oanhx Oãnh Oanhx Oãnh
oanhj oạnh oanhj oạnh
Oanhj Oạnh Oanhj Oạnh
oec oec
Oec Oec
oecs oéc
Oecs Oéc
oecf oèc
Oecf Oèc
oecr oẻc
Oecr Oẻc
oecx oẽc
Oecx Oẽc
oecj oẹc
Oecj Oẹc
oem oem
Oem Oem
oems oém
Oems Oém
oemf oèm
Oemf Oèm
oemr oẻm
Oemr Oẻm
oemx oẽm
Oemx Oẽm
oemj oẹm
Oemj Oẹm
oen oen
Oen Oen
oens oén
Oens Oén
oenf oèn
Oenf Oèn
oenr oẻn
Oenr Oẻn
oenx oẽn
Oenx Oẽn
oenj oẹn
Oenj Oẹn
oep oep
Oep Oep
oeps oép
Oeps Oép
oepf oèp
Oepf Oèp
oepr oẻp
Oepr Oẻp
oepx oẽp
Oepx Oẽp
oepj oẹp
Oepj Oẹp
oet oet
Oet Oet
oets oét
Oets Oét
oetf oèt
Oetf Oèt
oetr oẻt
Oetr Oẻt
oetx oẽt
Oetx Oẽt
oetj oẹt
Oetj Oẹt
oech oech
Oech Oech
oechs oéch
Oechs Oéch
oechf oèch
Oechf Oèch
oechr oẻch
Oechr Oẻch
oechx oẽch
Oechx Oẽch
oechj oẹch
Oechj Oẹch
oeng oeng
Oeng Oeng
oengs oéng
Oengs Oéng
oengf oèng
Oengf Oèng
oengr oẻng
Oengr Oẻng
oengx oẽng
Oengx Oẽng
oengj oẹng
Oengj Oẹng
oenh oenh
Oenh Oenh
oenhs oénh
Oenhs Oénh
oenhf oènh
Oenhf Oènh
oenhr oẻnh
Oenhr Oẻnh
oenhx oẽnh
Oenhx Oẽnh
oenhj oẹnh
Oenhj Oẹnh
oic oic oic oic
Oic Oic Oic Oic
oics óic oics óic
@@ -3153,10 +3749,18 @@ quay quay
Quay Quay Quay Quay
giay giay giay giay
Giay Giay Giay Giay
queo queo
Queo Queo
gieo gieo
Gieo Gieo
queu queu queu queu
Queu Queu Queu Queu
gieu gieu gieu gieu
Gieu Gieu Gieu Gieu
quia quia
Quia Quia
giia giia
Giia Giia
quiu quiu quiu quiu
Quiu Quiu Quiu Quiu
giiu giiu giiu giiu
@@ -3165,10 +3769,40 @@ quoi quoi
Quoi Quoi Quoi Quoi
gioi gioi gioi gioi
Gioi Gioi Gioi Gioi
giua giua
Giua Giua
giui giui giui giui
Giui Giui Giui Giui
giuy giuy giuy giuy
Giuy Giuy Giuy Giuy
quaau quâu
Quaau Quâu
giaau giâu
Giaau Giâu
quaay quây
Quaay Quây
giaay giây
Giaay Giây
queeu quêu
Queeu Quêu
gieeu giêu
Gieeu Giêu
quooi quôi
Quooi Quôi
giooi giôi
Giooi Giôi
quowi quơi
Quowi Quơi
giowi giơi
Giowi Giơi
giuwi giưi
Giuwi Giưi
giuwu giưu
Giuwu Giưu
giuwa giưa
Giuwa Giưa
giuee giuê
Giuee Giuê
quiee quiê quiee quiê
Quiee Quiê Quiee Quiê
giiee giiê giiee giiê
@@ -3187,8 +3821,6 @@ quoaw quoă
Quoaw Quoă Quoaw Quoă
gioaw gioă gioaw gioă
Gioaw Gioă Gioaw Gioă
giuwa giưa
Giuwa Giưa
giuwow giươ giuwow giươ
Giuwow Giươ Giuwow Giươ
quie quie quie quie
@@ -3201,8 +3833,6 @@ giye giye
Giye Giye Giye Giye
giuo giuo giuo giuo
Giuo Giuo Giuo Giuo
giua giua
Giua Giua
quieeu quiêu quieeu quiêu
Quieeu Quiêu Quieeu Quiêu
giieeu giiêu giieeu giiêu
@@ -3215,6 +3845,14 @@ giuooi giuôi
Giuooi Giuôi Giuooi Giuôi
giuowi giươi giuowi giươi
Giuowi Giươi Giuowi Giươi
giuowu giươu
Giuowu Giươu
giuwowi giươi
Giuwowi Giươi
giuwowu giươu
Giuwowu Giươu
giuaay giuây
Giuaay Giuây
quoai quoai quoai quoai
Quoai Quoai Quoai Quoai
gioai gioai gioai gioai
@@ -3223,10 +3861,16 @@ quoay quoay
Quoay Quoay Quoay Quoay
gioay gioay gioay gioay
Gioay Gioay Gioay Gioay
giuyee giuyê quoeo quoeo
Giuyee Giuyê Quoeo Quoeo
gioeo gioeo
Gioeo Gioeo
giuya giuya giuya giuya
Giuya Giuya Giuya Giuya
giuyu giuyu
Giuyu Giuyu
giuyee giuyê
Giuyee Giuyê
quaw quă quaw quă
Quaw Quă Quaw Quă
giaw giă giaw giă
@@ -3719,6 +4363,38 @@ giuowng giương
Giuowng Giương Giuowng Giương
giuownh giươnh giuownh giươnh
Giuownh Giươnh Giuownh Giươnh
giuwowc giươc
Giuwowc Giươc
giuwowm giươm
Giuwowm Giươm
giuwown giươn
Giuwown Giươn
giuwowp giươp
Giuwowp Giươp
giuwowt giươt
Giuwowt Giươt
giuwowch giươch
Giuwowch Giươch
giuwowng giương
Giuwowng Giương
giuwownh giươnh
Giuwownh Giươnh
giuyeec giuyêc
Giuyeec Giuyêc
giuyeem giuyêm
Giuyeem Giuyêm
giuyeen giuyên
Giuyeen Giuyên
giuyeep giuyêp
Giuyeep Giuyêp
giuyeet giuyêt
Giuyeet Giuyêt
giuyeech giuyêch
Giuyeech Giuyêch
giuyeeng giuyêng
Giuyeeng Giuyêng
giuyeenh giuyênh
Giuyeenh Giuyênh
giuaac giuâc giuaac giuâc
Giuaac Giuâc Giuaac Giuâc
giuaam giuâm giuaam giuâm
@@ -3735,6 +4411,22 @@ giuaang giuâng
Giuaang Giuâng Giuaang Giuâng
giuaanh giuânh giuaanh giuânh
Giuaanh Giuânh Giuaanh Giuânh
giueec giuêc
Giueec Giuêc
giueem giuêm
Giueem Giuêm
giueen giuên
Giueen Giuên
giueep giuêp
Giueep Giuêp
giueet giuêt
Giueet Giuêt
giueech giuêch
Giueech Giuêch
giueeng giuêng
Giueeng Giuêng
giueenh giuênh
Giueenh Giuênh
quoawc quoăc quoawc quoăc
Quoawc Quoăc Quoawc Quoăc
gioawc gioăc gioawc gioăc
@@ -3927,6 +4619,38 @@ quoanh quoanh
Quoanh Quoanh Quoanh Quoanh
gioanh gioanh gioanh gioanh
Gioanh Gioanh Gioanh Gioanh
quoec quoec
Quoec Quoec
gioec gioec
Gioec Gioec
quoem quoem
Quoem Quoem
gioem gioem
Gioem Gioem
quoen quoen
Quoen Quoen
gioen gioen
Gioen Gioen
quoep quoep
Quoep Quoep
gioep gioep
Gioep Gioep
quoet quoet
Quoet Quoet
gioet gioet
Gioet Gioet
quoech quoech
Quoech Quoech
gioech gioech
Gioech Gioech
quoeng quoeng
Quoeng Quoeng
gioeng gioeng
Gioeng Gioeng
quoenh quoenh
Quoenh Quoenh
gioenh gioenh
Gioenh Gioenh
quoic quoic quoic quoic
Quoic Quoic Quoic Quoic
gioic gioic gioic gioic

73
map/verifymap.py Executable file
View File

@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""Verify the text maps and dictionaries consumed by strans."""
import argparse
import sys
from pathlib import Path
def verify(path):
errors = []
try:
data = path.read_bytes()
except OSError as error:
return [f"{path}: {error.strerror}"]
try:
text = data.decode("utf-8")
except UnicodeDecodeError as error:
return [f"{path}: invalid UTF-8: {error}"]
keys = {}
for lineno, line in enumerate(text.split("\n"), 1):
where = f"{path}:{lineno}"
if "\r" in line:
errors.append(f"{where}: carriage return is not canonical")
line = line.replace("\r", "")
if not line or line.startswith(";"):
continue
if "\0" in line:
errors.append(f"{where}: embedded NUL")
if line.count("\t") != 1:
errors.append(f"{where}: expected exactly one tab")
continue
key, value = line.split("\t")
if not key:
errors.append(f"{where}: empty key")
if len(key) > 64:
errors.append(f"{where}: key has {len(key)} runes; maximum is 64")
if key in keys:
errors.append(f"{where}: duplicate key; first defined on line {keys[key]}")
else:
keys[key] = lineno
if not value:
errors.append(f"{where}: empty value")
continue
if value != " ".join(value.split(" ")):
errors.append(f"{where}: noncanonical candidate spacing")
values = value.split(" ")
if path.name.endswith(".map"):
if len(value) > 64:
errors.append(f"{where}: value exceeds 64 runes")
else:
for candidate in values:
if len(candidate) > 64:
errors.append(f"{where}: candidate exceeds 64 runes")
if len(values) != len(set(values)):
errors.append(f"{where}: duplicate candidate")
return errors
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("file", nargs="+", type=Path)
args = parser.parse_args()
errors = []
for path in args.file:
errors.extend(verify(path))
for error in errors:
print(error, file=sys.stderr)
return bool(errors)
if __name__ == "__main__":
raise SystemExit(main())

File diff suppressed because it is too large Load Diff

257
popup_layout.c Normal file
View File

@@ -0,0 +1,257 @@
#include "dat.h"
#include "fn.h"
enum {
Asciitofull = 0xFEE0,
};
int popupscale = 1;
static void
fill(u32int *buf, int n, u32int color)
{
int i;
for(i = 0; i < n; i++)
buf[i] = color;
}
static void
fillrect(u32int *buf, int w, int h, int x, int y, int rw, int rh,
u32int color)
{
int x0, x1, y0, y1;
x0 = min(max(x, 0), w);
x1 = min(max(x + max(rw, 0), 0), w);
y0 = min(max(y, 0), h);
y1 = min(max(y + max(rh, 0), 0), h);
for(; y0 < y1; y0++)
fill(buf + y0*w + x0, max(x1 - x0, 0), color);
}
static int
validarea(Area *a)
{
return a != nil && a->w > 0 && a->h > 0;
}
static vlong
areadistance(Area *a, int x, int y)
{
vlong dx, dy, right, bottom;
right = (vlong)a->x + a->w;
bottom = (vlong)a->y + a->h;
dx = x < a->x ? (vlong)a->x - x : x >= right ? x - right : 0;
dy = y < a->y ? (vlong)a->y - y : y >= bottom ? y - bottom : 0;
return dx + dy;
}
void
popuparea(Area *mon, int nmon, Area *work, int x, int y, Area *out)
{
vlong best, bottom, d, right, x0, x1, y0, y1;
int i, pick;
memset(out, 0, sizeof *out);
pick = -1;
best = (vlong)1 << 62;
for(i = 0; i < nmon; i++){
if(!validarea(&mon[i]))
continue;
right = (vlong)mon[i].x + mon[i].w;
bottom = (vlong)mon[i].y + mon[i].h;
if(x >= mon[i].x && x < right &&
y >= mon[i].y && y < bottom){
pick = i;
break;
}
d = areadistance(&mon[i], x, y);
if(d < best){
best = d;
pick = i;
}
}
if(pick < 0)
return;
*out = mon[pick];
if(!validarea(work))
return;
x0 = max((vlong)out->x, work->x);
y0 = max((vlong)out->y, work->y);
x1 = min((vlong)out->x + out->w, (vlong)work->x + work->w);
y1 = min((vlong)out->y + out->h, (vlong)work->y + work->h);
if(x1 <= x0 || y1 <= y0)
return;
out->x = x0;
out->y = y0;
out->w = x1 - x0;
out->h = y1 - y0;
}
int
pagemarker(char *buf, int nbuf, int first, int shown, int total)
{
if(nbuf <= 0)
return 0;
buf[0] = 0;
if(shown <= 0 || (first == 0 && shown >= total))
return 0;
return snprint(buf, nbuf, "%d-%d/%d", first + 1,
first + shown, total);
}
static int
fitrows(int h, int pre, int mark, int n)
{
int fixed;
fixed = 2*PopupPad + mark*Fontsz;
if(pre)
fixed += Fontsz + (n != 0 ? PopupSep : 0);
if(h <= fixed)
return 0;
return min(n, (h - fixed) / Fontsz);
}
void
popuplayout(Drawcmd *dc, int areaw, int areah, Popup *p)
{
char buf[32];
int first, i, markrow, nall, nmark, npre, total, width, y;
memset(p, 0, sizeof *p);
p->prey = -1;
p->sepy = -1;
p->rowsy = -1;
p->marky = -1;
p->sely = -1;
nall = min(max(dc->nkouho, 0), Maxdisp);
npre = dc->pre.n != 0;
if((nall == 0 && !npre) || areaw <= 0 || areah <= 0)
return;
/* As many rows as fit, and a page marker when they do not all. */
total = max(dc->total, dc->first + nall);
markrow = dc->first > 0 || nall < total;
p->n = fitrows(areah, npre, markrow, nall);
if(p->n < nall && !markrow){
markrow = 1;
p->n = fitrows(areah, npre, markrow, nall);
}
if(p->n == 0 && !npre)
return;
if(p->n != 0 && dc->sel >= p->n)
p->row0 = min(dc->sel - p->n + 1, nall - p->n);
first = dc->first + p->row0;
nmark = markrow ? pagemarker(buf, sizeof buf, first, p->n, total) : 0;
if(nmark == 0)
markrow = 0;
/* Rows share one steady width; a preedit alone hugs its text. */
width = p->n != 0 ? PopupBasew : 2*PopupPad;
if(npre)
width = max(width, textwidth(&dc->pre) + 2*PopupPad);
for(i = 0; i < p->n; i++)
width = max(width, textwidth(&dc->kouho[p->row0+i]) +
PopupNumw + 2*PopupPad);
if(markrow){
sinit(&p->mark, buf, nmark);
p->markw = textwidth(&p->mark);
width = max(width, p->markw + 2*PopupPad);
}
p->w = min(width, areaw);
y = PopupPad;
if(npre){
p->prey = y;
y += Fontsz;
}
if(npre && p->n != 0){
p->sepy = y;
y += PopupSep;
}
if(p->n != 0){
p->rowsy = y;
y += p->n*Fontsz;
}
if(markrow){
p->marky = y;
y += Fontsz;
}
p->h = min(y + PopupPad, areah);
p->textw = max(p->w - 2*PopupPad - PopupNumw, 0);
if(markrow){
p->markw = min(p->markw, max(p->w - 2*PopupPad, 0));
p->markx = p->w - PopupPad - p->markw;
}
p->selw = max(p->w - 2*PopupPad, 0);
if(dc->sel >= p->row0 && dc->sel < p->row0 + p->n)
p->sely = p->rowsy + (dc->sel - p->row0)*Fontsz;
}
void
popupdraw(u32int *img, Drawcmd *dc, Popup *p)
{
Str num;
u32int color;
int i, j, y;
fill(img, p->w*p->h, Colbg);
if(p->prey >= 0)
textdraw(img, p->w, p->h, PopupPad, p->prey,
max(p->w - 2*PopupPad, 0), Colfg, &dc->pre);
if(p->sepy >= 0)
fillrect(img, p->w, p->h, PopupPad, p->sepy,
p->w - 2*PopupPad, PopupSep, Colsep);
if(p->sely >= 0)
fillrect(img, p->w, p->h, PopupPad, p->sely,
p->selw, Fontsz, Colsel);
for(i = 0, y = p->rowsy; i < p->n; i++, y += Fontsz){
j = p->row0 + i;
color = j == dc->sel ? Colselfg : Colfg;
sclear(&num);
sputr(&num, '1' + j + Asciitofull);
textdraw(img, p->w, p->h, PopupPad, y,
min(PopupNumw, max(p->w - 2*PopupPad, 0)), color, &num);
textdraw(img, p->w, p->h, PopupPad + PopupNumw, y, p->textw,
color, &dc->kouho[j]);
}
if(p->marky >= 0)
textdraw(img, p->w, p->h, p->markx, p->marky, p->markw,
Colfg, &p->mark);
}
void
popupposition(Caret *caret, int pointerx, int pointery, Area *area,
int w, int h, int *x, int *y)
{
vlong below, bottom, px, py, right, xmax, ymax;
if(!validarea(area)){
*x = 0;
*y = 0;
return;
}
right = (vlong)area->x + area->w;
bottom = (vlong)area->y + area->h;
if(caret->valid){
px = caret->x;
below = (vlong)caret->y + max(caret->h, 0);
if(below >= area->y && below + h <= bottom)
py = below;
else
py = (vlong)caret->y - h;
}else{
px = (vlong)pointerx + 10;
py = (vlong)pointery + 10;
if(py + h > bottom)
py = (vlong)pointery - 10 - h;
}
xmax = max(right - w, area->x);
ymax = max(bottom - h, area->y);
*x = max((vlong)area->x, min(px, xmax));
*y = max((vlong)area->y, min(py, ymax));
}

View File

@@ -0,0 +1,494 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="input_method_unstable_v2">
<copyright>
Copyright © 2008-2011 Kristian Høgsberg
Copyright © 2010-2011 Intel Corporation
Copyright © 2012-2013 Collabora, Ltd.
Copyright © 2012, 2013 Intel Corporation
Copyright © 2015, 2016 Jan Arne Petersen
Copyright © 2017, 2018 Red Hat, Inc.
Copyright © 2018 Purism SPC
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS 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. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
</copyright>
<description summary="Protocol for creating input methods">
This protocol allows applications to act as input methods for compositors.
An input method context is used to manage the state of the input method.
Text strings are UTF-8 encoded, their indices and lengths are in bytes.
This document adheres to the RFC 2119 when using words like "must",
"should", "may", etc.
Warning! The protocol described in this file is experimental and
backward incompatible changes may be made. Backward compatible changes
may be added together with the corresponding interface version bump.
Backward incompatible changes are done by bumping the version number in
the protocol and interface names and resetting the interface version.
Once the protocol is to be declared stable, the 'z' prefix and the
version number in the protocol and interface names are removed and the
interface version number is reset.
</description>
<interface name="zwp_input_method_v2" version="1">
<description summary="input method">
An input method object allows for clients to compose text.
The objects connects the client to a text input in an application, and
lets the client to serve as an input method for a seat.
The zwp_input_method_v2 object can occupy two distinct states: active and
inactive. In the active state, the object is associated to and
communicates with a text input. In the inactive state, there is no
associated text input, and the only communication is with the compositor.
Initially, the input method is in the inactive state.
Requests issued in the inactive state must be accepted by the compositor.
Because of the serial mechanism, and the state reset on activate event,
they will not have any effect on the state of the next text input.
There must be no more than one input method object per seat.
</description>
<enum name="error">
<entry name="role" value="0" summary="wl_surface has another role"/>
</enum>
<event name="activate">
<description summary="input method has been requested">
Notification that a text input focused on this seat requested the input
method to be activated.
This event serves the purpose of providing the compositor with an
active input method.
This event resets all state associated with previous
surrounding_text, text_change_cause, and content_type events, as well
as the state associated with set_preedit_string, commit_string, and
delete_surrounding_text requests. In addition, it marks the
zwp_input_method_v2 object as active, and makes any existing
zwp_input_popup_surface_v2 objects visible.
The surrounding_text, and content_type events must follow before the
next done event if the text input supports the respective
functionality.
State set with this event is double-buffered. It will get applied on
the next zwp_input_method_v2.done event, and stay valid until changed.
</description>
</event>
<event name="deactivate">
<description summary="deactivate event">
Notification that no focused text input currently needs an active
input method on this seat.
This event marks the zwp_input_method_v2 object as inactive. The
compositor must make all existing zwp_input_popup_surface_v2 objects
invisible until the next activate event.
State set with this event is double-buffered. It will get applied on
the next zwp_input_method_v2.done event, and stay valid until changed.
</description>
</event>
<event name="surrounding_text">
<description summary="surrounding text event">
Updates the surrounding plain text around the cursor, excluding the
preedit text.
If any preedit text is present, it is replaced with the cursor for the
purpose of this event.
The argument text is a buffer containing the surrounding text, and must
include the cursor position, and the complete selection. It should
contain additional characters before and after these. There is a
maximum length of wayland messages, so text can not be longer than 4000
bytes.
cursor is the byte offset of the cursor within the text buffer.
anchor is the byte offset of the selection anchor within the text
buffer. If there is no selected text, anchor must be the same as
cursor.
If this event does not arrive before the first done event, the input
method may assume that the text input does not support this
functionality and ignore following surrounding_text events.
Values set with this event are double-buffered. They will get applied
and set to initial values on the next zwp_input_method_v2.done
event.
The initial state for affected fields is empty, meaning that the text
input does not support sending surrounding text. If the empty values
get applied, subsequent attempts to change them may have no effect.
</description>
<arg name="text" type="string"/>
<arg name="cursor" type="uint"/>
<arg name="anchor" type="uint"/>
</event>
<event name="text_change_cause">
<description summary="indicates the cause of surrounding text change">
Tells the input method why the text surrounding the cursor changed.
Whenever the client detects an external change in text, cursor, or
anchor position, it must issue this request to the compositor. This
request is intended to give the input method a chance to update the
preedit text in an appropriate way, e.g. by removing it when the user
starts typing with a keyboard.
cause describes the source of the change.
The value set with this event is double-buffered. It will get applied
and set to its initial value on the next zwp_input_method_v2.done
event.
The initial value of cause is input_method.
</description>
<arg name="cause" type="uint" enum="zwp_text_input_v3.change_cause"/>
</event>
<event name="content_type">
<description summary="content purpose and hint">
Indicates the content type and hint for the current
zwp_input_method_v2 instance.
Values set with this event are double-buffered. They will get applied
on the next zwp_input_method_v2.done event.
The initial value for hint is none, and the initial value for purpose
is normal.
</description>
<arg name="hint" type="uint" enum="zwp_text_input_v3.content_hint"/>
<arg name="purpose" type="uint" enum="zwp_text_input_v3.content_purpose"/>
</event>
<event name="done">
<description summary="apply state">
Atomically applies state changes recently sent to the client.
The done event establishes and updates the state of the client, and
must be issued after any changes to apply them.
Text input state (content purpose, content hint, surrounding text, and
change cause) is conceptually double-buffered within an input method
context.
Events modify the pending state, as opposed to the current state in use
by the input method. A done event atomically applies all pending state,
replacing the current state. After done, the new pending state is as
documented for each related request.
Events must be applied in the order of arrival.
Neither current nor pending state are modified unless noted otherwise.
</description>
</event>
<request name="commit_string">
<description summary="commit string">
Send the commit string text for insertion to the application.
Inserts a string at current cursor position (see commit event
sequence). The string to commit could be either just a single character
after a key press or the result of some composing.
The argument text is a buffer containing the string to insert. There is
a maximum length of wayland messages, so text can not be longer than
4000 bytes.
Values set with this event are double-buffered. They must be applied
and reset to initial on the next zwp_text_input_v3.commit request.
The initial value of text is an empty string.
</description>
<arg name="text" type="string"/>
</request>
<request name="set_preedit_string">
<description summary="pre-edit string">
Send the pre-edit string text to the application text input.
Place a new composing text (pre-edit) at the current cursor position.
Any previously set composing text must be removed. Any previously
existing selected text must be removed. The cursor is moved to a new
position within the preedit string.
The argument text is a buffer containing the preedit string. There is
a maximum length of wayland messages, so text can not be longer than
4000 bytes.
The arguments cursor_begin and cursor_end are counted in bytes relative
to the beginning of the submitted string buffer. Cursor should be
hidden by the text input when both are equal to -1.
cursor_begin indicates the beginning of the cursor. cursor_end
indicates the end of the cursor. It may be equal or different than
cursor_begin.
Values set with this event are double-buffered. They must be applied on
the next zwp_input_method_v2.commit event.
The initial value of text is an empty string. The initial value of
cursor_begin, and cursor_end are both 0.
</description>
<arg name="text" type="string"/>
<arg name="cursor_begin" type="int"/>
<arg name="cursor_end" type="int"/>
</request>
<request name="delete_surrounding_text">
<description summary="delete text">
Remove the surrounding text.
before_length and after_length are the number of bytes before and after
the current cursor index (excluding the preedit text) to delete.
If any preedit text is present, it is replaced with the cursor for the
purpose of this event. In effect before_length is counted from the
beginning of preedit text, and after_length from its end (see commit
event sequence).
Values set with this event are double-buffered. They must be applied
and reset to initial on the next zwp_input_method_v2.commit request.
The initial values of both before_length and after_length are 0.
</description>
<arg name="before_length" type="uint"/>
<arg name="after_length" type="uint"/>
</request>
<request name="commit">
<description summary="apply state">
Apply state changes from commit_string, set_preedit_string and
delete_surrounding_text requests.
The state relating to these events is double-buffered, and each one
modifies the pending state. This request replaces the current state
with the pending state.
The connected text input is expected to proceed by evaluating the
changes in the following order:
1. Replace existing preedit string with the cursor.
2. Delete requested surrounding text.
3. Insert commit string with the cursor at its end.
4. Calculate surrounding text to send.
5. Insert new preedit text in cursor position.
6. Place cursor inside preedit text.
The serial number reflects the last state of the zwp_input_method_v2
object known to the client. The value of the serial argument must be
equal to the number of done events already issued by that object. When
the compositor receives a commit request with a serial different than
the number of past done events, it must proceed as normal, except it
should not change the current state of the zwp_input_method_v2 object.
</description>
<arg name="serial" type="uint"/>
</request>
<request name="get_input_popup_surface">
<description summary="create popup surface">
Creates a new zwp_input_popup_surface_v2 object wrapping a given
surface.
The surface gets assigned the "input_popup" role. If the surface
already has an assigned role, the compositor must issue a protocol
error.
</description>
<arg name="id" type="new_id" interface="zwp_input_popup_surface_v2"/>
<arg name="surface" type="object" interface="wl_surface"/>
</request>
<request name="grab_keyboard">
<description summary="grab hardware keyboard">
Allow an input method to receive hardware keyboard input and process
key events to generate text events (with pre-edit) over the wire. This
allows input methods which compose multiple key events for inputting
text like it is done for CJK languages.
The compositor should send all keyboard events on the seat to the grab
holder via the returned wl_keyboard object. Nevertheless, the
compositor may decide not to forward any particular event. The
compositor must not further process any event after it has been
forwarded to the grab holder.
Releasing the resulting wl_keyboard object releases the grab.
</description>
<arg name="keyboard" type="new_id"
interface="zwp_input_method_keyboard_grab_v2"/>
</request>
<event name="unavailable">
<description summary="input method unavailable">
The input method ceased to be available.
The compositor must issue this event as the only event on the object if
there was another input_method object associated with the same seat at
the time of its creation.
The compositor must issue this request when the object is no longer
usable, e.g. due to seat removal.
The input method context becomes inert and should be destroyed after
deactivation is handled. Any further requests and events except for the
destroy request must be ignored.
</description>
</event>
<request name="destroy" type="destructor">
<description summary="destroy the text input">
Destroys the zwp_text_input_v2 object and any associated child
objects, i.e. zwp_input_popup_surface_v2 and
zwp_input_method_keyboard_grab_v2.
</description>
</request>
</interface>
<interface name="zwp_input_popup_surface_v2" version="1">
<description summary="popup surface">
This interface marks a surface as a popup for interacting with an input
method.
The compositor should place it near the active text input area. It must
be visible if and only if the input method is in the active state.
The client must not destroy the underlying wl_surface while the
zwp_input_popup_surface_v2 object exists.
</description>
<event name="text_input_rectangle">
<description summary="set text input area position">
Notify about the position of the area of the text input expressed as a
rectangle in surface local coordinates.
This is a hint to the input method telling it the relative position of
the text being entered.
</description>
<arg name="x" type="int"/>
<arg name="y" type="int"/>
<arg name="width" type="int"/>
<arg name="height" type="int"/>
</event>
<request name="destroy" type="destructor"/>
</interface>
<interface name="zwp_input_method_keyboard_grab_v2" version="1">
<!-- Closely follows wl_keyboard version 6 -->
<description summary="keyboard grab">
The zwp_input_method_keyboard_grab_v2 interface represents an exclusive
grab of the wl_keyboard interface associated with the seat.
</description>
<event name="keymap">
<description summary="keyboard mapping">
This event provides a file descriptor to the client which can be
memory-mapped to provide a keyboard mapping description.
</description>
<arg name="format" type="uint" enum="wl_keyboard.keymap_format"
summary="keymap format"/>
<arg name="fd" type="fd" summary="keymap file descriptor"/>
<arg name="size" type="uint" summary="keymap size, in bytes"/>
</event>
<event name="key">
<description summary="key event">
A key was pressed or released.
The time argument is a timestamp with millisecond granularity, with an
undefined base.
</description>
<arg name="serial" type="uint" summary="serial number of the key event"/>
<arg name="time" type="uint" summary="timestamp with millisecond granularity"/>
<arg name="key" type="uint" summary="key that produced the event"/>
<arg name="state" type="uint" enum="wl_keyboard.key_state"
summary="physical state of the key"/>
</event>
<event name="modifiers">
<description summary="modifier and group state">
Notifies clients that the modifier and/or group state has changed, and
it should update its local state.
</description>
<arg name="serial" type="uint" summary="serial number of the modifiers event"/>
<arg name="mods_depressed" type="uint" summary="depressed modifiers"/>
<arg name="mods_latched" type="uint" summary="latched modifiers"/>
<arg name="mods_locked" type="uint" summary="locked modifiers"/>
<arg name="group" type="uint" summary="keyboard layout"/>
</event>
<request name="release" type="destructor">
<description summary="release the grab object"/>
</request>
<event name="repeat_info">
<description summary="repeat rate and delay">
Informs the client about the keyboard's repeat rate and delay.
This event is sent as soon as the zwp_input_method_keyboard_grab_v2
object has been created, and is guaranteed to be received by the
client before any key press event.
Negative values for either rate or delay are illegal. A rate of zero
will disable any repeating (regardless of the value of delay).
This event can be sent later on as well with a new value if necessary,
so clients should continue listening for the event past the creation
of zwp_input_method_keyboard_grab_v2.
</description>
<arg name="rate" type="int"
summary="the rate of repeating keys in characters per second"/>
<arg name="delay" type="int"
summary="delay in milliseconds since key down until repeating starts"/>
</event>
</interface>
<interface name="zwp_input_method_manager_v2" version="1">
<description summary="input method manager">
The input method manager allows the client to become the input method on
a chosen seat.
No more than one input method must be associated with any seat at any
given time.
</description>
<request name="get_input_method">
<description summary="request an input method object">
Request a new input zwp_input_method_v2 object associated with a given
seat.
</description>
<arg name="seat" type="object" interface="wl_seat"/>
<arg name="input_method" type="new_id" interface="zwp_input_method_v2"/>
</request>
<request name="destroy" type="destructor">
<description summary="destroy the input method manager">
Destroys the zwp_input_method_manager_v2 object.
The zwp_input_method_v2 objects originating from it remain valid.
</description>
</request>
</interface>
</protocol>

View File

@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="virtual_keyboard_unstable_v1">
<copyright>
Copyright © 2008-2011 Kristian Høgsberg
Copyright © 2010-2013 Intel Corporation
Copyright © 2012-2013 Collabora, Ltd.
Copyright © 2018 Purism SPC
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS 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. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
</copyright>
<interface name="zwp_virtual_keyboard_v1" version="1">
<description summary="virtual keyboard">
The virtual keyboard provides an application with requests which emulate
the behaviour of a physical keyboard.
This interface can be used by clients on its own to provide raw input
events, or it can accompany the input method protocol.
</description>
<request name="keymap">
<description summary="keyboard mapping">
Provide a file descriptor to the compositor which can be
memory-mapped to provide a keyboard mapping description.
</description>
<arg name="format" type="uint" enum="wl_keyboard.keymap_format" summary="keymap format"/>
<arg name="fd" type="fd" summary="keymap file descriptor"/>
<arg name="size" type="uint" summary="keymap size, in bytes"/>
</request>
<enum name="error">
<entry name="no_keymap" value="0" summary="No keymap was set"/>
<entry name="invalid_keymap_format" value="1" summary="Invalid keymap format"/>
</enum>
<request name="key">
<description summary="key event">
A key was pressed or released.
The time argument is a timestamp with millisecond granularity, with an
undefined base. All requests regarding a single object must share the
same clock.
Keymap must be set before issuing this request.
State carries a value from the key_state enumeration.
</description>
<arg name="time" type="uint" summary="timestamp with millisecond granularity"/>
<arg name="key" type="uint" summary="key that produced the event"/>
<arg name="state" type="uint" summary="physical state of the key"/>
</request>
<request name="modifiers">
<description summary="modifier and group state">
Notifies the compositor that the modifier and/or group state has
changed, and it should update state.
The client should use wl_keyboard.modifiers event to synchronize its
internal state with seat state.
Keymap must be set before issuing this request.
</description>
<arg name="mods_depressed" type="uint" summary="depressed modifiers"/>
<arg name="mods_latched" type="uint" summary="latched modifiers"/>
<arg name="mods_locked" type="uint" summary="locked modifiers"/>
<arg name="group" type="uint" summary="keyboard layout"/>
</request>
<request name="destroy" type="destructor" since="1">
<description summary="destroy the virtual keyboard keyboard object"/>
</request>
</interface>
<interface name="zwp_virtual_keyboard_manager_v1" version="1">
<description summary="virtual keyboard manager">
A virtual keyboard manager allows an application to provide keyboard
input events as if they came from a physical keyboard.
</description>
<enum name="error">
<entry name="unauthorized" value="0" summary="client not authorized to use the interface"/>
</enum>
<request name="create_virtual_keyboard">
<description summary="Create a new virtual keyboard">
Creates a new virtual keyboard associated to a seat.
If the compositor enables a keyboard to perform arbitrary actions, it
should present an error when an untrusted client requests a new
keyboard.
</description>
<arg name="seat" type="object" interface="wl_seat"/>
<arg name="id" type="new_id" interface="zwp_virtual_keyboard_v1"/>
</request>
</interface>
</protocol>

28
run.sh
View File

@@ -1,8 +1,24 @@
#!/bin/sh #!/bin/sh
cd "$(dirname "$0")" cd "$(dirname "$0")" || exit 1
pkill strans
sleep 1 if test "$#" -ne 0; then
./strans map font & echo "usage: run.sh" >&2
sleep 1 exit 1
xim/strans-xim & fi
if ! test -x ./strans; then
echo "run.sh: ./strans is not executable; run make first" >&2
exit 1
fi
if ! command -v pkill >/dev/null 2>&1; then
echo "run.sh: pkill is required" >&2
exit 1
fi
uid=$(id -u) || exit 1
pkill -TERM -u "$uid" -x strans 2>/dev/null || :
sleep 0.1
pkill -KILL -u "$uid" -x strans 2>/dev/null || :
./strans map </dev/null &

149
srv.c
View File

@@ -1,65 +1,150 @@
#include "dat.h" #include "dat.h"
#include "fn.h" #include "fn.h"
static char adir[40]; #include <sys/stat.h>
#include <sys/un.h>
static char adir[256];
static char sockpath[sizeof(((struct sockaddr_un*)0)->sun_path)];
static dev_t sockdev;
static ino_t sockino;
static Channel *clientc; static Channel *clientc;
int /* Removes the socket only while it is still the one we announced. */
srvreadkey(int fd, Keyreq *kr) static void
srvunlink(void)
{ {
uchar req[Ipcreqsz]; struct stat st;
u32int ks, mod;
int want;
if(ipcreadn(fd, req, sizeof req) < 0) if(sockpath[0] != '\0' && lstat(sockpath, &st) == 0 &&
return -1; st.st_dev == sockdev && st.st_ino == sockino)
ipcunpackreq(req, &want, &mod, &ks); unlink(sockpath);
kr->ks = ks; sockpath[0] = '\0';
kr->mod = mod; }
kr->want = want;
static int
srvnote(void *v, char *note)
{
USED(v);
if(notefatal(note))
srvunlink();
return 0; return 0;
} }
/*
* 0 for a request to make, 1 for a frame that only changes what rides
* along with the requests after it, -1 for anything else.
*/
static int
srvreadreq(int fd, Keyreq *kr, int *want)
{
uchar req[Ipccaretsz];
char text[Ipcfieldmax];
int valid;
size_t n;
int32_t x, y, h;
if(ipcreadn(fd, req, Ipcreqsz) < 0)
return -1;
*want = 0;
kr->ks = 0;
kr->mod = 0;
switch(ipcreqtype(req)){
case Ipckey:
ipcunpackreq(req, want, &kr->mod, &kr->ks);
kr->op = ipcreqreset(req) ? Keyreset : Keypress;
return 0;
case Ipccap:
*want = (req[0] & Ipcreqwant) != 0;
kr->op = Keycap;
return 0;
case Ipccaret:
if(ipcreadn(fd, req + Ipcreqsz, Ipccaretsz - Ipcreqsz) < 0 ||
ipcunpackcaret(req, &valid, &x, &y, &h) < 0)
return -1;
kr->op = Keycaret;
kr->caret.valid = valid;
kr->caret.x = x;
kr->caret.y = y;
kr->caret.h = h;
return 0;
case Ipcsurround:
n = ipcsurroundlen(req);
if(n > 0 && ipcreadn(fd, text, n) < 0)
return -1;
stail(&kr->surround, text, n);
return 1;
}
return -1;
}
/*
* One Keyreq persists for the connection: the negotiated preedit and
* the last caret ride along with every request, and the socket closing
* releases the engine.
*/
static void static void
clientthread(void *arg) clientthread(void *arg)
{ {
Channel *reply;
int fd;
Keyreq kr; Keyreq kr;
Keyres res; Keyres res;
uchar out[Ipcmaxresp]; uchar out[Ipcmaxresp], token;
char commit[Maxutf], preedit[Maxutf]; char commit[Maxutf], preedit[Maxutf];
int n, ncommit, npreedit; int fd, n, ncommit, npreedit, rv, want;
uchar token;
fd = (int)(uintptr)arg; fd = (int)(uintptr)arg;
threadsetname("client %d", fd); threadsetname("client %d", fd);
reply = chancreate(sizeof(Keyres), 0); memset(&kr, 0, sizeof kr);
kr.reply = reply; kr.reply = chancreate(sizeof(Keyres), 0);
while(srvreadkey(fd, &kr) == 0){ kr.owner = &fd;
kr.clientpre = 1;
while((rv = srvreadreq(fd, &kr, &want)) >= 0){
if(rv != 0)
continue;
if(kr.op != Keycaret)
kr.clientpre = want;
chansend(keyc, &kr); chansend(keyc, &kr);
chanrecv(reply, &res); chanrecv(kr.reply, &res);
if(kr.op == Keycaret)
continue;
ncommit = stoutf(&res.commit, commit, sizeof commit); ncommit = stoutf(&res.commit, commit, sizeof commit);
npreedit = stoutf(&res.preedit, preedit, sizeof preedit); npreedit = stoutf(&res.preedit, preedit, sizeof preedit);
n = ipcpackresp(out, sizeof out, res.eaten, /* A capability reply is always eaten: it marks the extension. */
commit, ncommit, preedit, npreedit, kr.want); n = ipcpackresp(out, sizeof out, kr.op == Keycap || res.eaten,
res.del, commit, ncommit, preedit, npreedit, want);
if(n < 0 || ipcsend(fd, out, n) < 0) if(n < 0 || ipcsend(fd, out, n) < 0)
break; break;
} }
chanfree(reply); kr.op = Keyrelease;
kr.ks = 0;
kr.mod = 0;
chansend(keyc, &kr);
chanrecv(kr.reply, &res);
chanfree(kr.reply);
close(fd); close(fd);
chanrecv(clientc, &token); chanrecv(clientc, &token);
} }
static void void
srvinit(void) srvinit(void)
{ {
char addr[64]; struct stat st;
char *addr;
snprint(addr, sizeof(addr), "unix!" IPCPATH, getuid()); if(ipcpath(sockpath, sizeof sockpath) < 0)
remove(addr + 5); die("IPC path is too long");
addr = smprint("unix!%s", sockpath);
if(addr == nil)
die("out of memory");
if(announce(addr, adir) < 0) if(announce(addr, adir) < 0)
die("announce: %r"); die("IPC endpoint is already in use: %r");
free(addr);
if(chmod(sockpath, 0600) < 0 || lstat(sockpath, &st) < 0)
die("can't protect IPC endpoint: %s", sockpath);
sockdev = st.st_dev;
sockino = st.st_ino;
atexit(srvunlink);
threadnotify(srvnote, 1);
} }
void void
@@ -70,7 +155,6 @@ srvthread(void*)
uchar token; uchar token;
threadsetname("srv"); threadsetname("srv");
srvinit();
token = 0; token = 0;
clientc = chancreate(sizeof token, Maxclients); clientc = chancreate(sizeof token, Maxclients);
for(;;){ for(;;){
@@ -81,9 +165,6 @@ srvthread(void*)
close(fd); close(fd);
continue; continue;
} }
if(proccreate(clientthread, (void*)(uintptr)fd, 8192) < 0){ proccreate(clientthread, (void*)(uintptr)fd, 8192);
chanrecv(clientc, &token);
close(fd);
}
} }
} }

File diff suppressed because it is too large Load Diff

42
str.c
View File

@@ -1,22 +1,42 @@
#include "dat.h" #include "dat.h"
#include "fn.h" #include "fn.h"
void /* Fills s from n bytes of UTF-8; whole, valid, and at most Maxrunes.
* Anything else leaves s empty. */
int
sinit(Str *s, char *src, int n) sinit(Str *s, char *src, int n)
{ {
Str tmp = {0};
Rune r;
int len; int len;
s->n = 0; s->n = 0;
while(n > 0 && s->n < Maxrunes){ while(n > 0){
if(!fullrune(src, n)) if(tmp.n >= Maxrunes || !fullrune(src, n))
break; return 0;
len = chartorune(&s->r[s->n], src); len = chartorune(&r, src);
if(len > n) if((r == Runeerror && len == 1) || (r >= 0xd800 && r <= 0xdfff))
break; return 0;
s->n++; tmp.r[tmp.n++] = r;
src += len; src += len;
n -= len; n -= len;
} }
*s = tmp;
return 1;
}
/* The last runes of n bytes of UTF-8, as many as a Str holds. */
void
stail(Str *s, char *src, int n)
{
Rune r;
char *p;
int nr;
nr = utfnlen(src, n);
for(p = src; nr > Maxrunes; nr--)
p += chartorune(&r, p);
sinit(s, p, n - (p - src));
} }
void void
@@ -28,8 +48,9 @@ sclear(Str *s)
void void
sputr(Str *s, Rune r) sputr(Str *s, Rune r)
{ {
/* Str is a capped value; appends at capacity leave it unchanged. */
if(s->n >= Maxrunes) if(s->n >= Maxrunes)
die("sputr overflow"); return;
s->r[s->n++] = r; s->r[s->n++] = r;
} }
@@ -63,14 +84,13 @@ scmp(Str *a, Str *b)
return 0; return 0;
} }
/* UTF-8 of s into buf[sz], NUL-terminated, whole runes only. */
int int
stoutf(Str *s, char *buf, int sz) stoutf(Str *s, char *buf, int sz)
{ {
char tmp[UTFmax]; char tmp[UTFmax];
int i, n, len; int i, n, len;
if(sz <= 0)
return 0;
n = 0; n = 0;
for(i = 0; i < s->n; i++){ for(i = 0; i < s->n; i++){
len = runetochar(tmp, &s->r[i]); len = runetochar(tmp, &s->r[i]);

1399
strans.c

File diff suppressed because it is too large Load Diff

View File

@@ -1,34 +1,150 @@
CC = 9c CC = 9c
LD = 9l LD = 9l
CFLAGS = -std=c99 -Wall -Wextra -O2 -g -I.. -I../cutest HOSTCC = cc
LIBS = -lthread -lbio PKG_CONFIG ?= pkg-config
CFLAGS ?= -O2 -g
UNIT_CPPFLAGS = -I..
UNIT_CFLAGS = -Wall -Wextra
HOST_CFLAGS = -std=c99 -Wall -Wextra
TEXT_CFLAGS = $(shell $(PKG_CONFIG) --cflags pangocairo cairo fontconfig)
TEXT_LIBS = $(shell $(PKG_CONFIG) --libs pangocairo cairo fontconfig)
DBUS_CFLAGS = $(shell $(PKG_CONFIG) --cflags dbus-1)
DBUS_LIBS = $(shell $(PKG_CONFIG) --libs dbus-1)
GTK_CFLAGS = $(shell $(PKG_CONFIG) --cflags gtk+-3.0)
GTK_LIBS = $(shell $(PKG_CONFIG) --libs gtk+-3.0)
IBUS_CFLAGS = $(shell $(PKG_CONFIG) --cflags dbus-1 xkbcommon)
IBUS_LIBS = $(shell $(PKG_CONFIG) --libs dbus-1 xkbcommon)
IBUS_CLIENT_CFLAGS = $(shell $(PKG_CONFIG) --cflags ibus-1.0)
IBUS_CLIENT_LIBS = $(shell $(PKG_CONFIG) --libs ibus-1.0)
X11_CFLAGS = $(shell $(PKG_CONFIG) --cflags x11)
X11_LIBS = $(shell $(PKG_CONFIG) --libs x11)
XIM_CFLAGS = $(shell $(PKG_CONFIG) --cflags xcb-imdkit xcb-aux xcb-xkb xkbcommon-x11)
XIM_LIBS = $(shell $(PKG_CONFIG) --libs xcb-imdkit xcb-aux xcb-xkb xkbcommon-x11)
WL_CFLAGS = $(shell $(PKG_CONFIG) --cflags wayland-client xkbcommon)
WL_LIBS = $(shell $(PKG_CONFIG) --libs wayland-client xkbcommon)
UNIT_LDLIBS = -lthread -lbio $(TEXT_LIBS) $(IBUS_LIBS) $(XIM_LIBS) $(WL_LIBS)
PROG = unit_test PROG = unit_test
TESTSRC = unit_test.c test_util.c str_test.c hash_test.c trie_test.c \ STRESS = stress_test
ko_test.c vi_test.c engine_test.c dict_test.c ipc_test.c LIVESRC = live.c live.h
LIVEBUSSRC = livebus.c livebus.h
SMOKE = ibus_live_test ibus_client_smoke gtk_live_test xim_live_test \
ipc_live_test
FAULT = daemon_collision_test daemon_failure_test daemon_restart_test
LIVE = $(SMOKE) $(FAULT)
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 \
popup_test.c font_test.c ibus_test.c server_test.c compose_test.c \
xim_adapter_test.c wl_adapter_test.c
TESTOBJ = $(TESTSRC:.c=.o) TESTOBJ = $(TESTSRC:.c=.o)
PARENTSRC = str.c hash.c trie.c dict.c ko.c vi.c ipc.c # imv2.c and vkv1.c are wayland-scanner's; the parent Makefile writes them.
PARENTSRC = str.c trie.c dict.c ko.c vi.c ipc.c popup_layout.c \
font.c compose.c imv2.c vkv1.c
PARENTOBJ = $(PARENTSRC:%.c=unit_%.o) PARENTOBJ = $(PARENTSRC:%.c=unit_%.o)
OBJS = $(TESTOBJ) $(PARENTOBJ) COMMONOBJ = $(TESTOBJ) $(PARENTOBJ)
OBJS = unit_test.o stress_test.o $(COMMONOBJ)
all: $(PROG) all: $(PROG) $(STRESS) $(LIVE)
check test: $(PROG) check test: $(PROG)
./$(PROG) $(TESTARGS) ./$(PROG) $(UNITARGS)
$(PROG): $(OBJS) check-live: $(SMOKE) ../strans ../gtk/im-strans.so
$(LD) -o $@ $(OBJS) $(LIBS) ./ibus_live_test ../strans ../map ./ibus_client_smoke
./gtk_live_test ../gtk/im-strans.so
./xim_live_test ../strans ../map
./ipc_live_test ../strans ../map
$(TESTOBJ): test.h ../dat.h ../fn.h ../ipc.h ../cutest/cutest.h check-stress: $(STRESS) ibus_live_test ipc_live_test $(FAULT) ../strans
./$(STRESS)
./ibus_live_test --capacity ../strans ../map
./ipc_live_test --capacity ../strans ../map
./daemon_collision_test ../strans ../map
./daemon_failure_test ../strans
./daemon_restart_test ../strans ../map
$(PROG): unit_test.o $(COMMONOBJ)
$(LD) $(LDFLAGS) -o $@ unit_test.o $(COMMONOBJ) $(UNIT_LDLIBS) \
$(LDLIBS)
$(STRESS): stress_test.o $(COMMONOBJ)
$(LD) $(LDFLAGS) -o $@ stress_test.o $(COMMONOBJ) $(UNIT_LDLIBS) \
$(LDLIBS)
stress_test.o: unit_test.c
$(CC) $(CPPFLAGS) $(UNIT_CPPFLAGS) $(UNIT_CFLAGS) $(CFLAGS) -DSTRESS \
-c -o $@ $<
ibus_live_test: ibus_live_test.c $(LIVESRC) $(LIVEBUSSRC) ../ipc.c ../ipc.h
$(HOSTCC) $(CPPFLAGS) -I.. $(DBUS_CFLAGS) $(HOST_CFLAGS) $(CFLAGS) \
$(LDFLAGS) -o $@ ibus_live_test.c live.c livebus.c ../ipc.c \
$(DBUS_LIBS) $(LDLIBS)
ibus_client_smoke: ibus_client_smoke.c
$(HOSTCC) $(CPPFLAGS) $(IBUS_CLIENT_CFLAGS) $(HOST_CFLAGS) $(CFLAGS) \
$(LDFLAGS) -o $@ $< $(IBUS_CLIENT_LIBS) $(LDLIBS)
gtk_live_test: gtk_live_test.c $(LIVESRC) ../ipc.c ../ipc.h
$(HOSTCC) $(CPPFLAGS) -I.. $(GTK_CFLAGS) $(HOST_CFLAGS) $(CFLAGS) \
$(LDFLAGS) -o $@ gtk_live_test.c live.c ../ipc.c $(GTK_LIBS) \
-pthread $(LDLIBS)
# The parent make knows what these are built from; always ask it.
../gtk/im-strans.so:
$(MAKE) -C ../gtk
../strans:
$(MAKE) -C .. strans
../imv2.c ../imv2.h ../vkv1.c ../vkv1.h:
$(MAKE) -C .. $(@F)
xim_live_test: xim_live_test.c $(LIVESRC) ../ipc.c ../ipc.h
$(HOSTCC) $(CPPFLAGS) -I.. $(X11_CFLAGS) $(HOST_CFLAGS) $(CFLAGS) \
$(LDFLAGS) -o $@ xim_live_test.c live.c ../ipc.c $(X11_LIBS) \
$(LDLIBS)
ipc_live_test: ipc_live_test.c $(LIVESRC) ../ipc.c ../ipc.h
$(HOSTCC) $(CPPFLAGS) -I.. $(HOST_CFLAGS) $(CFLAGS) $(LDFLAGS) \
-o $@ ipc_live_test.c live.c ../ipc.c $(LDLIBS)
daemon_collision_test: daemon_collision_test.c $(LIVESRC) $(LIVEBUSSRC) \
../ipc.c ../ipc.h
$(HOSTCC) $(CPPFLAGS) -I.. $(DBUS_CFLAGS) $(HOST_CFLAGS) $(CFLAGS) \
$(LDFLAGS) -o $@ daemon_collision_test.c live.c livebus.c \
../ipc.c $(DBUS_LIBS) $(LDLIBS)
daemon_failure_test: daemon_failure_test.c $(LIVESRC) ../ipc.c ../ipc.h
$(HOSTCC) $(CPPFLAGS) -I.. $(HOST_CFLAGS) $(CFLAGS) $(LDFLAGS) -o $@ \
daemon_failure_test.c live.c ../ipc.c $(LDLIBS)
daemon_restart_test: daemon_restart_test.c $(LIVESRC) $(LIVEBUSSRC) \
../ipc.c ../ipc.h
$(HOSTCC) $(CPPFLAGS) -I.. $(DBUS_CFLAGS) $(HOST_CFLAGS) $(CFLAGS) \
$(LDFLAGS) -o $@ daemon_restart_test.c live.c livebus.c ../ipc.c \
$(DBUS_LIBS) $(LDLIBS)
unit_test.o stress_test.o $(TESTOBJ): test.h ../dat.h ../fn.h ../ipc.h \
../cutest/cutest.h
engine_test.o: ../strans.c engine_test.o: ../strans.c
ibus_test.o: UNIT_CPPFLAGS += $(IBUS_CFLAGS)
ibus_test.o: ../ibus.c
xim_adapter_test.o: UNIT_CPPFLAGS += $(XIM_CFLAGS)
xim_adapter_test.o: ../xim.c
wl_adapter_test.o unit_imv2.o unit_vkv1.o: UNIT_CPPFLAGS += $(WL_CFLAGS)
wl_adapter_test.o: ../wl.c ../imv2.h ../vkv1.h
server_test.o: ../srv.c
unit_font.o: UNIT_CPPFLAGS += $(TEXT_CFLAGS)
unit_compose.o compose_test.o: UNIT_CPPFLAGS += $(IBUS_CFLAGS)
unit_%.o: ../%.c ../dat.h ../fn.h ../ipc.h unit_%.o: ../%.c ../dat.h ../fn.h ../ipc.h
$(CC) $(CFLAGS) -c -o $@ $< $(CC) $(CPPFLAGS) $(UNIT_CPPFLAGS) $(UNIT_CFLAGS) $(CFLAGS) -c -o $@ $<
%.o: %.c %.o: %.c
$(CC) $(CFLAGS) -c -o $@ $< $(CC) $(CPPFLAGS) $(UNIT_CPPFLAGS) $(UNIT_CFLAGS) $(CFLAGS) -c -o $@ $<
clean: clean:
rm -f $(OBJS) $(PROG) rm -f $(OBJS) $(PROG) $(STRESS) $(LIVE)
.PHONY: all check test clean .PHONY: all check check-live check-stress test clean \
../strans ../gtk/im-strans.so

56
tests/compose_test.c Normal file
View File

@@ -0,0 +1,56 @@
#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, and
* belongs to the one context, in the one frontend, that started it.
*/
void
compose_sequences(struct ct *t)
{
static int a, b; /* two input contexts, told apart by address */
static const struct {
int who;
void *owner;
u32int sym;
int composing;
char *text;
} keys[] = {
{ Composexim, &a, XKB_KEY_a, 0, "" },
{ Composexim, &a, XKB_KEY_dead_acute, 1, "" },
{ Composexim, &a, XKB_KEY_e, 0, "é" },
{ Composexim, &a, XKB_KEY_Multi_key, 1, "" },
{ Composexim, &a, XKB_KEY_o, 1, "" },
{ Composexim, &a, XKB_KEY_c, 0, "©" },
{ Composexim, &a, XKB_KEY_dead_acute, 1, "" },
{ Composexim, &a, XKB_KEY_Escape, 1, "" },
{ Composexim, &a, XKB_KEY_a, 0, "" },
/* another context of the same frontend starts over */
{ Composexim, &a, XKB_KEY_dead_acute, 1, "" },
{ Composexim, &b, XKB_KEY_e, 0, "" },
/* another frontend does not touch this one's sequence */
{ Composexim, &a, XKB_KEY_dead_acute, 1, "" },
{ Composewl, &a, XKB_KEY_o, 0, "" },
{ Composexim, &a, XKB_KEY_e, 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].who,
keys[i].owner, keys[i].sym, buf, sizeof buf));
CT_EQ_STR(t, keys[i].text, buf);
}
/* a context that goes takes its half-typed sequence with it */
CT_EQ_INT(t, 1,
composekey(Composewl, &a, XKB_KEY_dead_acute, buf, sizeof buf));
composedrop(Composewl, &a);
CT_EQ_INT(t, 0, composekey(Composewl, &a, XKB_KEY_e, buf, sizeof buf));
CT_EQ_STR(t, "", buf);
unsetenv("XCOMPOSEFILE");
}

View File

@@ -0,0 +1,214 @@
#define _GNU_SOURCE
#include <errno.h>
#include <poll.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "live.h"
#include "livebus.h"
typedef struct Test Test;
struct Test
{
Live l;
Daemon first;
Daemon second;
int ipcfirst;
int ipcnew;
DBusConnection *busfirst;
DBusConnection *busnew;
char address[512];
};
static int
setup(Test *t)
{
memset(t, 0, sizeof *t);
daemoninit(&t->first, "first daemon");
daemoninit(&t->second, "second daemon");
t->ipcfirst = -1;
t->ipcnew = -1;
return livesetup(&t->l, "collision");
}
static int
addressunchanged(Test *t)
{
char contents[2048], address[512];
size_t ncontents;
pid_t declared;
if(!readfile(t->l.addrfile, contents, sizeof contents, &ncontents) ||
!parseaddress(contents, ncontents, address, sizeof address, &declared))
return 0;
if(declared != t->first.pid)
return fail("IBus address PID %ld, expected %ld", (long)declared,
(long)t->first.pid);
return strcmp(address, t->address) == 0 ||
fail("first private IBus address changed after collision");
}
static int
waitsecond(Test *t)
{
struct pollfd pfd;
int n, status, timeout;
int64_t deadline;
deadline = nowms() + Starttimeout;
for(;;){
do
n = waitpid(t->second.pid, &status, WNOHANG);
while(n < 0 && errno == EINTR);
if(n == t->second.pid){
t->second.pid = -1;
readerrors(&t->second);
if(!WIFEXITED(status) || WEXITSTATUS(status) == 0)
return fail("second daemon did not exit unsuccessfully: %#x",
status);
if(strstr(t->second.err, "IPC endpoint is already in use") == NULL)
return fail("second daemon did not report the IPC collision");
return 1;
}
if(n < 0){
fail("waitpid second daemon %ld: %s", (long)t->second.pid,
strerror(errno));
if(errno == ECHILD)
t->second.pid = -1;
return 0;
}
if(!daemonalive(&t->first))
return 0;
timeout = leftms(deadline);
if(timeout == 0){
fail("second daemon did not exit after the endpoint collision");
killdaemon(&t->second, NULL);
return 0;
}
pfd.fd = t->second.errfd;
pfd.events = POLLIN|POLLHUP;
pfd.revents = 0;
n = poll(&pfd, 1, timeout > 20 ? 20 : timeout);
if(n < 0 && errno == EINTR)
continue;
if(n < 0)
return fail("poll second daemon: %s", strerror(errno));
if(pfd.revents != 0)
readerrors(&t->second);
}
}
static int
runcollision(Test *t, char *program, char *mapdir)
{
if(!startdaemon(&t->l, &t->first, program, mapdir) ||
!waitready(&t->l, &t->first, t->address, sizeof t->address))
return 0;
t->ipcfirst = connectsocket(t->l.socket, nowms() + Calltimeout);
if(t->ipcfirst < 0)
return fail("connect persistent IPC client: %s", strerror(errno));
if(!ipcprobe(t->ipcfirst, "persistent pre-collision"))
return 0;
t->busfirst = openbus(t->address, "persistent pre-collision");
if(t->busfirst == NULL ||
!hello(t->busfirst, NULL, 0, "persistent pre-collision Hello"))
return 0;
if(!startdaemon(&t->l, &t->second, program, mapdir) ||
!waitsecond(t) || !daemonalive(&t->first) || !addressunchanged(t))
return 0;
if(!ipcprobe(t->ipcfirst, "persistent post-collision"))
return 0;
if(!createcontext(t->busfirst, NULL, 0,
"persistent post-collision context"))
return 0;
t->ipcnew = connectsocket(t->l.socket, nowms() + Calltimeout);
if(t->ipcnew < 0)
return fail("connect new IPC client after collision: %s",
strerror(errno));
if(!ipcprobe(t->ipcnew, "new post-collision"))
return 0;
t->busnew = openbus(t->address, "new post-collision");
if(t->busnew == NULL ||
!hello(t->busnew, NULL, 0, "new post-collision Hello") ||
!createcontext(t->busnew, NULL, 0, "new post-collision context"))
return 0;
return daemonalive(&t->first);
}
static int
cleanup(Test *t)
{
int n, ok, status;
ok = 1;
closebus(&t->busnew);
closebus(&t->busfirst);
if(t->ipcnew >= 0){
if(close(t->ipcnew) < 0)
ok = fail("close new IPC client: %s", strerror(errno));
t->ipcnew = -1;
}
if(t->ipcfirst >= 0){
if(close(t->ipcfirst) < 0)
ok = fail("close persistent IPC client: %s", strerror(errno));
t->ipcfirst = -1;
}
if(t->second.pid > 0){
do
n = waitpid(t->second.pid, &status, WNOHANG);
while(n < 0 && errno == EINTR);
if(n == t->second.pid)
t->second.pid = -1;
else if(n == 0){
ok = fail("second daemon still running during cleanup");
if(!killdaemon(&t->second, NULL))
ok = 0;
}else{
fail("check second daemon during cleanup: %s", strerror(errno));
ok = 0;
if(errno == ECHILD)
t->second.pid = -1;
else if(!killdaemon(&t->second, NULL))
ok = 0;
}
}
if(!stopdaemon(&t->first))
ok = 0;
if(!closeerrors(&t->first))
ok = 0;
if(!closeerrors(&t->second))
ok = 0;
if(!liveclean(&t->l))
ok = 0;
return ok;
}
int
main(int argc, char **argv)
{
Test test;
int ok;
testname = "daemon_collision_test";
if(argc != 3){
fprintf(stderr, "usage: daemon_collision_test strans mapdir\n");
return 2;
}
ok = setup(&test);
if(ok)
ok = runcollision(&test, argv[1], argv[2]);
if(!cleanup(&test))
ok = 0;
if(!ok){
showerrors(&test.first);
showerrors(&test.second);
return 1;
}
printf("daemon endpoint collision ownership: ok\n");
return 0;
}

138
tests/daemon_failure_test.c Normal file
View File

@@ -0,0 +1,138 @@
#define _GNU_SOURCE
#include <dirent.h>
#include <errno.h>
#include <poll.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "live.h"
enum
{
Failtimeout = 4000,
};
static int
waitfailed(Daemon *d, char *badmap)
{
struct pollfd pfd;
int n, status;
int64_t deadline;
status = 0;
deadline = nowms() + Failtimeout;
for(;;){
n = waitpid(d->pid, &status, WNOHANG);
if(n == d->pid){
d->pid = -1;
readerrors(d);
break;
}
if(n < 0 && errno == EINTR)
continue;
if(n < 0){
if(errno == ECHILD)
d->pid = -1;
return fail("waitpid: %s", strerror(errno));
}
n = leftms(deadline);
if(n == 0)
return fail("daemon did not exit after map initialization failure");
pfd.fd = d->errfd;
pfd.events = POLLIN|POLLHUP;
pfd.revents = 0;
n = poll(&pfd, 1, n);
if(n < 0 && errno == EINTR)
continue;
if(n < 0)
return fail("poll daemon: %s", strerror(errno));
if(n > 0)
readerrors(d);
}
if(!WIFEXITED(status) || WEXITSTATUS(status) == 0)
return fail("daemon map failure had wait status %#x", status);
if(strstr(d->err, "can't open") == NULL || strstr(d->err, badmap) == NULL)
return fail("daemon did not report the missing map directory");
return 1;
}
static int
emptydir(char *path)
{
DIR *dir;
struct dirent *de;
int empty;
dir = opendir(path);
if(dir == NULL)
return fail("open %s: %s", path, strerror(errno));
empty = 1;
errno = 0;
while((de = readdir(dir)) != NULL)
if(strcmp(de->d_name, ".") != 0 && strcmp(de->d_name, "..") != 0){
fail("unexpected endpoint %s/%s", path, de->d_name);
empty = 0;
}
if(errno != 0){
fail("read %s: %s", path, strerror(errno));
empty = 0;
}
if(closedir(dir) < 0){
fail("close %s: %s", path, strerror(errno));
empty = 0;
}
return empty;
}
static int
checkendpoints(Live *l)
{
struct stat st;
if(lstat(l->socket, &st) == 0)
return fail("IPC endpoint exists after failed startup: %s", l->socket);
if(errno != ENOENT)
return fail("lstat IPC endpoint: %s", strerror(errno));
return emptydir(l->runtime) && emptydir(l->bus);
}
int
main(int argc, char **argv)
{
Daemon daemon;
Live live;
char badmap[320];
int ok;
testname = "daemon_failure_test";
if(argc != 2){
fprintf(stderr, "usage: daemon_failure_test strans\n");
return 2;
}
daemoninit(&daemon, "daemon");
ok = livesetup(&live, "failure");
if(ok && snprintf(badmap, sizeof badmap, "%s/missing-map", live.root)
>= (int)sizeof badmap)
ok = fail("temporary path is too long");
if(ok)
ok = startdaemon(&live, &daemon, argv[1], badmap);
if(ok)
ok = waitfailed(&daemon, badmap) && checkendpoints(&live);
if(!killdaemon(&daemon, NULL))
ok = 0;
if(!closeerrors(&daemon))
ok = 0;
if(!liveclean(&live))
ok = 0;
if(!ok){
showerrors(&daemon);
return 1;
}
printf("failed daemon startup leaves no endpoints: ok\n");
return 0;
}

406
tests/daemon_restart_test.c Normal file
View File

@@ -0,0 +1,406 @@
#define _GNU_SOURCE
#include <errno.h>
#include <poll.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "ipc.h"
#include "live.h"
#include "livebus.h"
enum
{
Ctrlmask = 1<<2,
};
typedef struct Test Test;
struct Test
{
Live l;
Daemon first;
Daemon second;
pid_t firstpid;
int ipcfirst;
int ipcnew;
int busfirstfd;
DBusConnection *busfirst;
DBusConnection *busnew;
char firstaddress[512];
char secondaddress[512];
};
static int
setup(Test *t)
{
struct sigaction sa;
memset(t, 0, sizeof *t);
daemoninit(&t->first, "first daemon");
daemoninit(&t->second, "second daemon");
t->firstpid = -1;
t->ipcfirst = -1;
t->ipcnew = -1;
t->busfirstfd = -1;
memset(&sa, 0, sizeof sa);
sa.sa_handler = SIG_DFL;
if(sigemptyset(&sa.sa_mask) < 0 || sigaction(SIGCHLD, &sa, NULL) < 0)
return fail("establish exact child ownership: %s", strerror(errno));
return livesetup(&t->l, "restart");
}
static int
privateaddress(char *address, pid_t pid)
{
char prefix[96];
size_t n;
if(snprintf(prefix, sizeof prefix, "unix:abstract=strans-%ld",
(long)pid) >= (int)sizeof prefix)
return fail("private IBus prefix is too long");
n = strlen(prefix);
return (strncmp(address, prefix, n) == 0 && address[n] == ',') ||
fail("IBus address is not the daemon's private abstract address");
}
static int
focusin(DBusConnection *conn, char *path)
{
DBusMessage *reply;
reply = callret(conn, contextcall(path, "FocusIn"),
"replacement FocusIn");
if(reply == NULL)
return 0;
if(!dbus_message_has_signature(reply, "")){
dbus_message_unref(reply);
return fail("replacement FocusIn returned a nonempty reply");
}
dbus_message_unref(reply);
return 1;
}
static int
keycall(DBusConnection *conn, char *path, dbus_uint32_t sym,
dbus_uint32_t state, int expected, char *where)
{
DBusMessage *m, *reply;
DBusError err;
dbus_uint32_t code;
dbus_bool_t eaten;
code = 0;
m = contextcall(path, "ProcessKeyEvent");
if(m == NULL || !dbus_message_append_args(m,
DBUS_TYPE_UINT32, &sym, DBUS_TYPE_UINT32, &code,
DBUS_TYPE_UINT32, &state, DBUS_TYPE_INVALID)){
if(m != NULL)
dbus_message_unref(m);
return fail("build %s ProcessKeyEvent call", where);
}
reply = callret(conn, m, where);
if(reply == NULL)
return 0;
dbus_error_init(&err);
if(!dbus_message_has_signature(reply, "b") ||
!dbus_message_get_args(reply, &err, DBUS_TYPE_BOOLEAN, &eaten,
DBUS_TYPE_INVALID)){
dbus_message_unref(reply);
dbus_error_free(&err);
return fail("%s returned an invalid ProcessKeyEvent reply", where);
}
dbus_error_free(&err);
dbus_message_unref(reply);
return (eaten != FALSE) == (expected != 0) ||
fail("%s eaten=%d, expected %d", where, eaten != FALSE, expected);
}
static int
openpersistent(Test *t)
{
t->ipcfirst = connectsocket(t->l.socket, nowms() + Calltimeout);
if(t->ipcfirst < 0)
return fail("connect persistent IPC client: %s", strerror(errno));
if(!ipcprobe(t->ipcfirst, "persistent pre-crash"))
return 0;
t->busfirst = openbus(t->firstaddress, "persistent pre-crash");
if(t->busfirst == NULL ||
!hello(t->busfirst, NULL, 0, "persistent pre-crash Hello") ||
!createcontext(t->busfirst, NULL, 0, "persistent pre-crash context"))
return 0;
if(!dbus_connection_get_unix_fd(t->busfirst, &t->busfirstfd))
return fail("persistent IBus connection has no Unix descriptor");
return 1;
}
/*
* plan9port ignores a broken pipe: the note handlers run and the daemon
* goes on serving, so the endpoints it announced must still be there.
*/
static int
survivenote(Test *t)
{
struct stat st;
int fd, ok;
if(kill(t->firstpid, SIGPIPE) < 0)
return fail("send a broken pipe to the first daemon: %s",
strerror(errno));
pausems(200);
if(!daemonalive(&t->first))
return fail("first daemon died of a broken pipe");
if(lstat(t->l.socket, &st) < 0)
return fail("IPC socket taken by a note the daemon survived: %s",
strerror(errno));
if(lstat(t->l.addrfile, &st) < 0)
return fail("IBus address taken by a note the daemon survived: %s",
strerror(errno));
fd = connectsocket(t->l.socket, nowms() + Calltimeout);
if(fd < 0)
return fail("connect after a broken pipe: %s", strerror(errno));
ok = ipcprobe(fd, "after a broken pipe");
if(close(fd) < 0)
return fail("close the client opened after a broken pipe: %s",
strerror(errno));
return ok;
}
static int
hardcrash(Test *t)
{
int status;
status = 0;
if(!daemonalive(&t->first) || !killdaemon(&t->first, &status))
return 0;
readerrors(&t->first);
if(!WIFSIGNALED(status) || WTERMSIG(status) != SIGKILL)
return fail("first daemon hard crash had wait status %#x", status);
return 1;
}
static int
waitipcclosed(Test *t)
{
struct pollfd pfd;
unsigned char byte;
ssize_t n;
int timeout;
int64_t deadline;
deadline = nowms() + Calltimeout;
for(;;){
timeout = leftms(deadline);
if(timeout == 0)
return fail("persistent IPC connection did not disconnect promptly");
pfd.fd = t->ipcfirst;
pfd.events = POLLIN|POLLHUP|POLLERR;
pfd.revents = 0;
n = poll(&pfd, 1, timeout);
if(n < 0 && errno == EINTR)
continue;
if(n <= 0)
return fail("poll persistent IPC disconnection: %s",
n == 0 ? "timed out" : strerror(errno));
n = recv(t->ipcfirst, &byte, 1, MSG_PEEK);
if(n == 0 || (n < 0 && (errno == ECONNRESET || errno == ENOTCONN ||
errno == EPIPE)))
return 1;
if(n < 0 && (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK))
continue;
if(n < 0)
return fail("read persistent IPC disconnection: %s",
strerror(errno));
return fail("persistent IPC connection retained unread data after crash");
}
}
static int
waitbusclosed(Test *t)
{
struct pollfd pfd;
int n, timeout;
int64_t deadline;
deadline = nowms() + Calltimeout;
for(;;){
if(!dbus_connection_get_is_connected(t->busfirst))
return 1;
timeout = leftms(deadline);
if(timeout == 0)
return fail("persistent IBus connection did not disconnect promptly");
pfd.fd = t->busfirstfd;
pfd.events = POLLIN|POLLHUP|POLLERR;
pfd.revents = 0;
n = poll(&pfd, 1, timeout);
if(n < 0 && errno == EINTR)
continue;
if(n <= 0)
return fail("poll persistent IBus disconnection: %s",
n == 0 ? "timed out" : strerror(errno));
if(pfd.revents & POLLNVAL)
return fail("persistent IBus descriptor became invalid");
dbus_connection_read_write_dispatch(t->busfirst, 0);
}
}
static int
checkstale(Test *t)
{
struct stat st;
char contents[2048], address[512];
size_t ncontents;
pid_t declared;
if(lstat(t->l.socket, &st) < 0)
return fail("stale IPC socket disappeared: %s", strerror(errno));
if(!S_ISSOCK(st.st_mode))
return fail("stale IPC endpoint is not a socket");
if(lstat(t->l.addrfile, &st) < 0)
return fail("stale IBus address file disappeared: %s", strerror(errno));
if(!readfile(t->l.addrfile, contents, sizeof contents, &ncontents) ||
!parseaddress(contents, ncontents, address, sizeof address, &declared))
return 0;
if(declared != t->firstpid || strcmp(address, t->firstaddress) != 0)
return fail("stale IBus address no longer names the dead first daemon");
return 1;
}
/* The dead daemon's abstract socket has no listener, so this must fail. */
static int
rejectold(Test *t, char *where)
{
DBusConnection *conn;
DBusError err;
int ok;
dbus_error_init(&err);
conn = dbus_connection_open_private(t->firstaddress, &err);
if(conn != NULL){
dbus_connection_set_exit_on_disconnect(conn, FALSE);
closebus(&conn);
ok = fail("old private address accepted a connection %s", where);
}else
ok = dbus_error_is_set(&err) ||
fail("old private address failed without a D-Bus error %s", where);
dbus_error_free(&err);
return ok;
}
static int
openreplacement(Test *t)
{
unsigned char selectjp[] = {1, 0, 0, 0, 0, 0};
unsigned char keyk[] = {1, 0, 0, 0, 1, 0, 'k'};
char path[96];
t->ipcnew = connectsocket(t->l.socket, nowms() + Calltimeout);
if(t->ipcnew < 0)
return fail("connect replacement IPC client: %s", strerror(errno));
if(!ipcrequest(t->ipcnew, Mctrl, 'n', selectjp, sizeof selectjp,
"replacement select Japanese") ||
!ipcrequest(t->ipcnew, 0, 'k', keyk, sizeof keyk,
"replacement real key"))
return 0;
t->busnew = openbus(t->secondaddress, "replacement");
if(t->busnew == NULL ||
!hello(t->busnew, NULL, 0, "replacement Hello") ||
!createcontext(t->busnew, path, sizeof path, "replacement context") ||
!focusin(t->busnew, path) ||
!keycall(t->busnew, path, 'n', Ctrlmask, 1,
"replacement select Japanese") ||
!keycall(t->busnew, path, 'k', 0, 1, "replacement real key"))
return 0;
return daemonalive(&t->second);
}
static int
cleanup(Test *t)
{
int ok;
ok = 1;
closebus(&t->busnew);
closebus(&t->busfirst);
if(t->ipcnew >= 0){
if(close(t->ipcnew) < 0)
ok = fail("close replacement IPC client: %s", strerror(errno));
t->ipcnew = -1;
}
if(t->ipcfirst >= 0){
if(close(t->ipcfirst) < 0)
ok = fail("close persistent IPC client: %s", strerror(errno));
t->ipcfirst = -1;
}
if(!killdaemon(&t->first, NULL))
ok = 0;
if(!stopdaemon(&t->second))
ok = 0;
if(!closeerrors(&t->first))
ok = 0;
if(!closeerrors(&t->second))
ok = 0;
if(!liveclean(&t->l))
ok = 0;
return ok;
}
static int
runrestart(Test *t, char *program, char *mapdir)
{
if(!startdaemon(&t->l, &t->first, program, mapdir))
return 0;
t->firstpid = t->first.pid;
if(!waitready(&t->l, &t->first, t->firstaddress, sizeof t->firstaddress) ||
!privateaddress(t->firstaddress, t->firstpid) || !openpersistent(t))
return 0;
if(!survivenote(t))
return 0;
if(!hardcrash(t) || !waitipcclosed(t) || !waitbusclosed(t) ||
!checkstale(t) || !rejectold(t, "after hard crash"))
return 0;
closebus(&t->busfirst);
if(close(t->ipcfirst) < 0)
return fail("close disconnected persistent IPC client: %s",
strerror(errno));
t->ipcfirst = -1;
if(!startdaemon(&t->l, &t->second, program, mapdir) ||
!waitready(&t->l, &t->second, t->secondaddress,
sizeof t->secondaddress) ||
!privateaddress(t->secondaddress, t->second.pid) ||
!openreplacement(t))
return 0;
return daemonalive(&t->second);
}
int
main(int argc, char **argv)
{
Test test;
int ok;
testname = "daemon_restart_test";
if(argc != 3){
fprintf(stderr, "usage: daemon_restart_test strans mapdir\n");
return 2;
}
ok = setup(&test);
if(ok)
ok = runrestart(&test, argv[1], argv[2]);
if(!cleanup(&test))
ok = 0;
if(!ok){
showerrors(&test.first);
showerrors(&test.second);
return 1;
}
printf("daemon endpoints across a note and a hard crash: ok\n");
return 0;
}

2
tests/data/compose Normal file
View File

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

View File

@@ -0,0 +1,4 @@
;; 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

@@ -0,0 +1,6 @@
;; converter fixture; the test transcodes this file to EUC-JP
かんじ /漢字;common/幹事/
えがお /笑顔;face/
かんじ /感じ/漢字;duplicate/
きごう /記号;symbol/普通/
むこう /候補 with space/

View File

@@ -3,4 +3,3 @@ ab beta
한 값 한 값
duplicate first duplicate first
duplicate second duplicate second
tabs one two

View File

@@ -1,35 +1,28 @@
#include "dat.h"
#include "fn.h"
#include "test.h" #include "test.h"
void void
dictionary_candidates(struct ct *t) dictionary_candidates(struct ct *t)
{ {
char many[512], item[8]; char many[8*(Maxkouho+1)], item[8];
char *p; char *p;
Dictreq req; Str kouho[Maxkouho], key;
Dictres res; Trie *saved;
Hmap *saved;
Lang *lang; Lang *lang;
Str key; int i, n;
int i;
lang = getlang(LangJP); lang = getlang(LangJP);
saved = lang->dict; saved = lang->dict;
lang->dict = hmapalloc(1); lang->dict = trienew();
trieput(lang->dict, "かな", strlen("かな"), " 候補1 候補2 ",
strlen(" 候補1 候補2 "));
key = mkstr("かな"); key = mkstr("かな");
hmapset(&lang->dict, &key, " 候補1 かな 候補2 ", n = dictlookup(lang->dict, &key, kouho, Maxkouho);
strlen(" 候補1 かな 候補2 ")); if(CT_EQ_INT(t, 2, n)){
memset(&req, 0, sizeof req); checkstr(t, "candidate 1", "候補1", &kouho[0]);
req.key = key; checkstr(t, "candidate 2", "候補2", &kouho[1]);
req.pre = mkstr("preedit-one"); }
req.lang = LangJP;
dictlookup(&req, &res);
if(!CT_EQ_INT(t, 2, res.nkouho))
goto cleanup;
CT_EQ_INT(t, LangJP, res.lang);
CT_EQ_INT(t, 0, scmp(&req.pre, &res.key));
checkstr(t, "candidate 1", "候補1", &res.kouho[0]);
checkstr(t, "candidate 2", "候補2", &res.kouho[1]);
key = mkstr("key");
p = many; p = many;
for(i = 0; i < Maxkouho+1; i++){ for(i = 0; i < Maxkouho+1; i++){
snprint(item, sizeof item, "c%02d", i); snprint(item, sizeof item, "c%02d", i);
@@ -39,53 +32,57 @@ dictionary_candidates(struct ct *t)
p += strlen(item); p += strlen(item);
} }
*p = '\0'; *p = '\0';
hmapset(&lang->dict, &key, many, strlen(many)); trieput(lang->dict, "key", 3, many, strlen(many));
req.key = key; key = mkstr("key");
req.pre = mkstr("preedit-two"); n = dictlookup(lang->dict, &key, kouho, Maxkouho);
dictlookup(&req, &res); if(CT_EQ_INT(t, Maxkouho, n)){
if(!CT_EQ_INT(t, Maxkouho, res.nkouho)) checkstr(t, "first capped candidate", "c00", &kouho[0]);
goto cleanup; snprint(item, sizeof item, "c%02d", Maxkouho-1);
CT_EQ_INT(t, LangJP, res.lang); checkstr(t, "last capped candidate", item, &kouho[Maxkouho-1]);
CT_EQ_INT(t, 0, scmp(&req.pre, &res.key)); }
checkstr(t, "first capped candidate", "c00", &res.kouho[0]); CT_EQ_INT(t, 3, dictlookup(lang->dict, &key, kouho, 3));
checkstr(t, "last capped candidate", "c31", &res.kouho[31]); trieclose(lang->dict);
cleanup:
hmapfree(lang->dict);
lang->dict = saved; lang->dict = saved;
} }
void void
dictionary_misses_clear_result(struct ct *t) dictionary_misses(struct ct *t)
{ {
static const struct { Str kouho[Maxkouho], key;
char *name; Trie *saved;
char *key;
char *pre;
} cases[] = {
{ "empty key", "", "empty-preedit" },
{ "missing key", "missing", "missing-preedit" },
};
Dictreq req;
Dictres res;
Hmap *saved;
Lang *lang; Lang *lang;
int i;
lang = getlang(LangJP); lang = getlang(LangJP);
saved = lang->dict; saved = lang->dict;
lang->dict = hmapalloc(1); lang->dict = trienew();
for(i = 0; i < nelem(cases); i++){ key = mkstr("");
memset(&res, 0xa5, sizeof res); CT_EQ_INT(t, 0, dictlookup(lang->dict, &key, kouho, Maxkouho));
req.key = mkstr(cases[i].key); key = mkstr("missing");
req.pre = mkstr(cases[i].pre); CT_EQ_INT(t, 0, dictlookup(lang->dict, &key, kouho, Maxkouho));
req.lang = LangJP; CT_EQ_INT(t, 0, dictlookup(getlang(LangKO)->dict, &key, kouho, Maxkouho));
dictlookup(&req, &res); trieclose(lang->dict);
if(res.nkouho != 0)
CT_ERRORF(t, "%s: want 0 candidates, got %d",
cases[i].name, res.nkouho);
CT_EQ_INT(t, LangJP, res.lang);
checkstr(t, cases[i].name, cases[i].pre, &res.key);
}
hmapfree(lang->dict);
lang->dict = saved; lang->dict = saved;
} }
void
dictionary_prefix(struct ct *t)
{
Str kouho[Maxkouho], key;
Trie *dict;
dict = trienew();
trieput(dict, "smile", 5, "A B", 3);
trieput(dict, "smiley", 6, "B C", 3);
trieput(dict, "sad", 3, "D", 1);
key = mkstr("smile");
if(CT_EQ_INT(t, 4, dictprefix(dict, &key, kouho, Maxkouho))){
checkstr(t, "own entry first", "A", &kouho[0]);
checkstr(t, "entry below", "C", &kouho[3]);
}
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);
}

File diff suppressed because it is too large Load Diff

257
tests/font_test.c Normal file
View File

@@ -0,0 +1,257 @@
#include "dat.h"
#include "fn.h"
#include "test.h"
enum
{
Guard = 8,
Rgbmask = 0xffffff,
};
#define Testw (6 * Fontsz)
#define Testh (3 * Fontsz)
#define Testn (Testw * Testh)
static u32int guardcolor = 0x5a5a5a5a;
static void
fillpixels(u32int *p, int n, u32int color)
{
int i;
for(i = 0; i < n; i++)
p[i] = color;
}
static int
hasink(u32int *p, int n, u32int bg)
{
int i;
bg &= Rgbmask;
for(i = 0; i < n; i++)
if((p[i] & Rgbmask) != bg)
return 1;
return 0;
}
static int
hascolors(u32int *p, int n, u32int bg)
{
u32int c, first;
int b, found, g, i, r;
bg &= Rgbmask;
first = 0;
found = 0;
for(i = 0; i < n; i++){
c = p[i] & Rgbmask;
if(c == bg)
continue;
r = c >> 16 & 0xff;
g = c >> 8 & 0xff;
b = c & 0xff;
if(r == g && g == b)
continue;
if(!found){
first = c;
found = 1;
}else if(c != first)
return 1;
}
return 0;
}
static void
checkguards(struct ct *t, u32int *mem)
{
int i;
for(i = 0; i < Guard; i++){
CT_EQ_UINT(t, guardcolor, mem[i]);
CT_EQ_UINT(t, guardcolor, mem[Guard + Testn + i]);
}
}
static void
checkoutside(struct ct *t, u32int *buf, u32int bg, int row)
{
int x, y, y0, y1;
bg &= Rgbmask;
y0 = max(row, 0);
y1 = min(row + Fontsz, Testh);
for(y = 0; y < Testh; y++){
if(y >= y0 && y < y1)
continue;
for(x = 0; x < Testw; x++)
CT_EQ_UINT(t, bg, buf[y * Testw + x] & Rgbmask);
}
}
static int
inkbounds(u32int *p, u32int bg, int *minx, int *miny, int *maxx, int *maxy)
{
int x, y;
bg &= Rgbmask;
*minx = Testw;
*miny = Testh;
*maxx = -1;
*maxy = -1;
for(y = 0; y < Testh; y++){
for(x = 0; x < Testw; x++){
if((p[y * Testw + x] & Rgbmask) == bg)
continue;
*minx = min(*minx, x);
*miny = min(*miny, y);
*maxx = max(*maxx, x);
*maxy = max(*maxy, y);
}
}
return *maxx >= 0;
}
static void
checktext(struct ct *t, u32int *buf, char *utf)
{
Str s;
int maxx, maxy, minx, miny, w;
s = mkstr(utf);
w = textwidth(&s);
CT_CHECK(t, w > 0);
CT_CHECK(t, w < Testw - Fontsz);
fillpixels(buf, Testn, Colbg);
textdraw(buf, Testw, Testh, Fontsz, Fontsz, Testw, Colfg, &s);
if(!CT_CHECK(t, inkbounds(buf, Colbg, &minx, &miny, &maxx, &maxy)))
return;
CT_CHECK(t, minx >= 0 && miny >= 0);
CT_CHECK(t, maxx < Testw && maxy < Testh);
CT_CHECK(t, maxx - minx + 1 <= w);
}
static void
checkcolor(struct ct *t, u32int *buf, char *utf)
{
Str s;
int w;
s = mkstr(utf);
w = textwidth(&s);
CT_CHECK(t, w > 0);
if(s.n > 1)
CT_CHECK(t, w < s.n * Fontsz);
fillpixels(buf, Testn, Colbg);
textdraw(buf, Testw, Testh, Fontsz, Fontsz, Testw, Colfg, &s);
CT_CHECK(t, hasink(buf, Testn, Colbg));
CT_CHECK(t, hascolors(buf, Testn, Colbg));
}
static void
checkshape(struct ct *t, u32int *buf, char *utf)
{
Str s;
int w;
s = mkstr(utf);
w = textwidth(&s);
CT_CHECK(t, w > 0);
CT_CHECK(t, w < s.n * Fontsz);
fillpixels(buf, Testn, Colbg);
textdraw(buf, Testw, Testh, Fontsz, Fontsz, Testw, Colfg, &s);
CT_CHECK(t, hasink(buf, Testn, Colbg));
}
static void
checkclip(struct ct *t, u32int *mem, int x, int y, Str *s)
{
u32int *buf;
buf = mem + Guard;
fillpixels(buf, Testn, Colbg);
textdraw(buf, Testw, Testh, x, y, Testw, Colfg, s);
CT_CHECK(t, hasink(buf, Testn, Colbg));
checkoutside(t, buf, Colbg, y);
checkguards(t, mem);
}
void
font_render(struct ct *t)
{
static char *plain[] = { "A", "", "", "", "", "𠀋" };
u32int *buf, *mem;
Str a, heart, longrow, missing;
int i, maxx, maxy, minx, miny, natural, w, x, y;
mem = emalloc((Testn + 2 * Guard) * sizeof mem[0]);
fillpixels(mem, Testn + 2 * Guard, guardcolor);
buf = mem + Guard;
fillpixels(buf, Testn, Colbg);
a = mkstr("A");
textinit();
for(i = 0; i < nelem(plain); i++)
checktext(t, buf, plain[i]);
checkcolor(t, buf, "😀");
checkcolor(t, buf, "❤️");
checkcolor(t, buf, "☕️");
checkshape(t, buf, "👩‍💻");
checkshape(t, buf, "👍🏽");
checkshape(t, buf, "🇰🇷");
checkshape(t, buf, "☕︎");
heart = mkstr("❤️");
fillpixels(buf, Testn, Colsel);
textdraw(buf, Testw, Testh, Fontsz, Fontsz, Testw, Colfg, &heart);
CT_CHECK(t, hascolors(buf, Testn, Colsel));
CT_EQ_UINT(t, Colsel,
buf[(Fontsz + Fontsz/2) * Testw + Testw - 1] & Rgbmask);
checkoutside(t, buf, Colsel, Fontsz);
checkguards(t, mem);
w = textwidth(&a);
checkclip(t, mem, -w / 2, Fontsz, &a);
checkclip(t, mem, Testw - w / 2, Fontsz, &a);
checkclip(t, mem, Fontsz, -Fontsz / 2, &a);
checkclip(t, mem, Fontsz, Testh - Fontsz / 2, &a);
longrow = mkstr("abcdefghijklmnopqrstuvwxyz");
natural = textwidth(&longrow);
CT_CHECK(t, natural > 2*Fontsz);
fillpixels(buf, Testn, Colbg);
textdraw(buf, Testw, Testh, 2*Fontsz, Fontsz, 2*Fontsz, Colfg,
&longrow);
CT_CHECK(t, hasink(buf, Testn, Colbg));
for(y = Fontsz; y < 2*Fontsz; y++){
for(x = 0; x < 2*Fontsz; x++)
CT_EQ_UINT(t, Colbg, buf[y * Testw + x] & Rgbmask);
for(x = 4*Fontsz; x < Testw; x++)
CT_EQ_UINT(t, Colbg, buf[y * Testw + x] & Rgbmask);
}
CT_EQ_INT(t, natural, textwidth(&longrow));
fillpixels(buf, Testn, Colsel);
textdraw(buf, Testw, Testh, 2*Fontsz, Fontsz, 2*Fontsz, Colselfg,
&longrow);
CT_CHECK(t, (Colselfg & Rgbmask) != (Colsel & Rgbmask));
if(CT_CHECK(t, inkbounds(buf, Colsel,
&minx, &miny, &maxx, &maxy))){
CT_CHECK(t, minx >= 2*Fontsz);
CT_CHECK(t, maxx < 4*Fontsz);
CT_CHECK(t, miny >= Fontsz);
CT_CHECK(t, maxy < 2*Fontsz);
}
checkoutside(t, buf, Colsel, Fontsz);
checkguards(t, mem);
CT_EQ_INT(t, natural, textwidth(&longrow));
missing = mkstr("\xF4\x8F\xBF\xBF");
for(i = 0; i < 32; i++){
textdraw(buf, Testw, Testh, Fontsz, Fontsz, Testw, Colfg, &heart);
textdraw(buf, Testw, Testh, 0, 0, Testw, Colfg, &missing);
}
checkguards(t, mem);
free(mem);
textclose();
}

1015
tests/gtk_live_test.c Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,78 +0,0 @@
#include "test.h"
void
hmap_set_replace_and_grow(struct ct *t)
{
char keybuf[16], valbuf[16], source[] = "copied";
Hmap *h;
Hnode *n;
Str key;
int i;
h = hmapalloc(1);
for(i = 0; i < 4; i++){
snprint(keybuf, sizeof keybuf, "key%d", i);
snprint(valbuf, sizeof valbuf, "value%d", i);
key = mkstr(keybuf);
hmapset(&h, &key, valbuf, strlen(valbuf));
}
for(i = 0; i < 4; i++){
snprint(keybuf, sizeof keybuf, "key%d", i);
snprint(valbuf, sizeof valbuf, "value%d", i);
key = mkstr(keybuf);
n = hmapget(h, &key);
if(n == nil || strcmp(n->val, valbuf) != 0)
CT_ERRORF(t, "%s: collision chain lost value", keybuf);
}
key = mkstr("key1");
hmapset(&h, &key, source, strlen(source));
source[0] = 'X';
n = hmapget(h, &key);
if(!CT_CHECK(t, n != nil))
goto cleanup;
CT_EQ_STR(t, "copied", n->val);
key = mkstr("key2");
n = hmapget(h, &key);
if(!CT_CHECK(t, n != nil))
goto cleanup;
CT_EQ_STR(t, "value2", n->val);
key = mkstr("key1");
hmapset(&h, &key, nil, 0);
n = hmapget(h, &key);
if(!CT_CHECK(t, n != nil))
goto cleanup;
CT_EQ_INT(t, 0, n->vlen);
CT_EQ_PTR(t, nil, n->val);
key = mkstr("missing");
CT_EQ_PTR(t, nil, hmapget(h, &key));
cleanup:
hmapfree(h);
}
void
hmap_long_utf8_keys(struct ct *t)
{
Hmap *h;
Hnode *n;
Str a, b;
int i;
for(i = 0; i < Maxrunes-1; i++)
a.r[i] = b.r[i] = 0x1f600;
a.r[Maxrunes-1] = 0x1f601;
b.r[Maxrunes-1] = 0x1f602;
a.n = b.n = Maxrunes;
h = hmapalloc(1);
hmapset(&h, &a, "first", 5);
hmapset(&h, &b, "second", 6);
n = hmapget(h, &a);
if(!CT_CHECK(t, n != nil))
goto cleanup;
CT_EQ_STR(t, "first", n != nil ? n->val : nil);
n = hmapget(h, &b);
if(!CT_CHECK(t, n != nil))
goto cleanup;
CT_EQ_STR(t, "second", n != nil ? n->val : nil);
cleanup:
hmapfree(h);
}

386
tests/ibus_client_smoke.c Normal file
View File

@@ -0,0 +1,386 @@
#define _POSIX_C_SOURCE 200809L
#include <ibus.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Log Log;
struct Log
{
GMainLoop *loop;
int *waiting;
IBusInputContext *a;
IBusInputContext *clearctx;
int legacy;
int modern;
int commit;
int invalid;
int sawpreedit;
int sawcommit;
int done;
int transfer;
int atext;
int aclear;
int retake;
int retook;
char revent[16];
int rn;
int repeat;
int rfirst;
int rcommit;
int rdone;
int flushed;
int hanja;
int required;
int deleted;
int delbefore;
int delcount;
int converted;
};
static void
revent(Log *log, char c)
{
if(log->rn + 1 < (int)sizeof log->revent){
log->revent[log->rn++] = c;
log->revent[log->rn] = '\0';
}
}
static void
legacy(IBusInputContext *ctx, IBusText *text, guint cursor,
gboolean visible, void *arg)
{
IBusAttrList *attrs;
IBusAttribute *a;
Log *log;
const char *s;
(void)ctx;
log = arg;
log->legacy++;
s = ibus_text_get_text(text);
if(log->repeat == 3){
if(s[0] != '\0' || cursor != 0 || visible)
log->invalid = 1;
return;
}
if(log->repeat != 0){
revent(log, 'P');
if(log->repeat == 1){
if(s[0] == '\0'){
if(cursor != 0 || visible)
log->invalid = 1;
}else if(strcmp(s, "") != 0 || cursor != 1 || !visible)
log->invalid = 1;
else
log->rfirst = 1;
}else if(s[0] == '\0'){
if(cursor != 0 || visible || log->rcommit)
log->invalid = 1;
}else if(strcmp(s, "") != 0 || cursor != 1 || !visible ||
!log->rcommit)
log->invalid = 1;
else
log->rdone = 1;
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
g_main_loop_quit(log->loop);
return;
}
if(strcmp(s, "k") == 0){
attrs = ibus_text_get_attributes(text);
a = attrs == NULL ? NULL : ibus_attr_list_get(attrs, 0);
if(cursor != 1 || !visible || attrs == NULL || a == NULL ||
a->type != IBUS_ATTR_TYPE_UNDERLINE ||
a->value != IBUS_ATTR_UNDERLINE_SINGLE ||
a->start_index != 0 || a->end_index != 1 ||
ibus_attr_list_get(attrs, 1) != NULL)
log->invalid = 1;
else{
log->sawpreedit = 1;
if(log->transfer && ctx == log->a)
log->atext = 1;
}
}
if(log->transfer && log->atext && !log->aclear && s[0] == '\0'){
if(cursor != 0 || visible)
log->invalid = 1;
else{
log->clearctx = ctx;
if(ctx == log->a)
log->aclear = 1;
else
log->invalid = 1;
}
}
log->done = log->sawpreedit && log->sawcommit;
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
g_main_loop_quit(log->loop);
}
static void
modern(IBusInputContext *ctx, IBusText *text, guint cursor,
gboolean visible, guint mode, void *arg)
{
Log *log;
(void)ctx;
(void)text;
(void)cursor;
(void)visible;
(void)mode;
log = arg;
log->modern++;
log->invalid = 1;
if(log->loop != NULL)
g_main_loop_quit(log->loop);
}
static void
commit(IBusInputContext *ctx, IBusText *text, void *arg)
{
IBusAttrList *attrs;
Log *log;
const char *s;
(void)ctx;
log = arg;
log->commit++;
s = ibus_text_get_text(text);
attrs = ibus_text_get_attributes(text);
if(log->hanja){
/* The word converted, with the client\'s own syllable taken back. */
if(strcmp(s, "漢字") == 0 && log->deleted && log->delbefore == -1 &&
log->delcount == 1)
log->converted = 1;
else
log->invalid = 1;
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
g_main_loop_quit(log->loop);
return;
}
if(log->repeat == 3){
/* Focus loss hands the pending syllable back as a commit. */
if(strcmp(s, "") == 0)
log->flushed = 1;
else
log->invalid = 1;
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
g_main_loop_quit(log->loop);
return;
}
if(log->repeat != 0){
revent(log, 'K');
if(log->repeat != 2 || strcmp(s, "") != 0 || attrs == NULL ||
ibus_attr_list_get(attrs, 0) != NULL)
log->invalid = 1;
else
log->rcommit = 1;
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
g_main_loop_quit(log->loop);
return;
}
if(log->retake){
/* Typing again takes back the reading the other context took. */
if(strcmp(s, "k") == 0)
log->retook = 1;
else
log->invalid = 1;
log->retake = 0;
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
g_main_loop_quit(log->loop);
return;
}
if(strcmp(s, "") != 0 || attrs == NULL ||
ibus_attr_list_get(attrs, 0) != NULL)
log->invalid = 1;
else
log->sawcommit = 1;
log->done = log->sawpreedit && log->sawcommit;
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
g_main_loop_quit(log->loop);
}
static void
required(IBusInputContext *ctx, void *arg)
{
Log *log;
(void)ctx;
log = arg;
log->required = 1;
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
g_main_loop_quit(log->loop);
}
static void
deleted(IBusInputContext *ctx, gint offset, guint n, void *arg)
{
Log *log;
(void)ctx;
log = arg;
log->deleted = 1;
log->delbefore = offset;
log->delcount = n;
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
g_main_loop_quit(log->loop);
}
static gboolean
timeout(void *arg)
{
g_main_loop_quit(arg);
return G_SOURCE_REMOVE;
}
static int
waitflag(Log *log, int *flag)
{
guint timer;
if(*flag)
return 1;
log->waiting = flag;
log->loop = g_main_loop_new(NULL, FALSE);
timer = g_timeout_add_seconds(4, timeout, log->loop);
g_main_loop_run(log->loop);
if(g_main_context_find_source_by_id(NULL, timer) != NULL)
g_source_remove(timer);
g_main_loop_unref(log->loop);
log->loop = NULL;
log->waiting = NULL;
return *flag;
}
int
main(int argc, char **argv)
{
IBusBus *bus;
IBusInputContext *a, *b, *c;
IBusText *around;
Log log;
int ok;
if(argc != 2){
fprintf(stderr, "usage: ibus_client_smoke address\n");
return 2;
}
if(setenv("IBUS_ADDRESS", argv[1], 1) < 0 ||
unsetenv("IBUS_ADDRESS_FILE") < 0 ||
unsetenv("DBUS_SESSION_BUS_ADDRESS") < 0 || unsetenv("DISPLAY") < 0){
perror("ibus_client_smoke: environment");
return 1;
}
memset(&log, 0, sizeof log);
b = c = NULL;
ibus_init();
bus = ibus_bus_new();
if(bus == NULL || !ibus_bus_is_connected(bus)){
fprintf(stderr, "ibus_client_smoke: cannot connect to private bus\n");
if(bus != NULL) g_object_unref(bus);
return 1;
}
a = ibus_bus_create_input_context(bus, "strans-libibus-smoke-a");
if(a == NULL){
fprintf(stderr, "ibus_client_smoke: cannot create input context\n");
g_object_unref(bus);
return 1;
}
log.a = a;
g_signal_connect(a, "update-preedit-text", G_CALLBACK(legacy), &log);
g_signal_connect(a, "update-preedit-text-with-mode", G_CALLBACK(modern),
&log);
g_signal_connect(a, "commit-text", G_CALLBACK(commit), &log);
ibus_input_context_set_capabilities(a, IBUS_CAP_PREEDIT_TEXT);
ibus_input_context_focus_in(a);
ok = ibus_input_context_process_key_event(a, 'n', 0, IBUS_CONTROL_MASK) &&
ibus_input_context_process_key_event(a, 'k', 0, 0) &&
ibus_input_context_process_key_event(a, 'a', 0, 0) &&
ibus_input_context_process_key_event(a, '0', 0, 0) &&
waitflag(&log, &log.done);
b = ibus_bus_create_input_context(bus, "strans-libibus-smoke-b");
if(b == NULL)
ok = 0;
else{
g_signal_connect(b, "update-preedit-text", G_CALLBACK(legacy), &log);
g_signal_connect(b, "update-preedit-text-with-mode",
G_CALLBACK(modern), &log);
ibus_input_context_set_capabilities(b, IBUS_CAP_PREEDIT_TEXT);
log.transfer = 1;
ok = ok && ibus_input_context_process_key_event(a, 'k', 0, 0) &&
waitflag(&log, &log.atext);
ibus_input_context_focus_in(b);
ok = ok && ibus_input_context_process_key_event(b, 'n', 0, 0) &&
waitflag(&log, &log.aclear);
ibus_input_context_focus_out(b);
}
ibus_input_context_focus_in(a);
log.retake = 1;
ok = ok && ibus_input_context_process_key_event(a, 's', 0,
IBUS_CONTROL_MASK) && waitflag(&log, &log.retook);
log.repeat = 1;
ok = ok && ibus_input_context_process_key_event(a, 'z', 0, 0) &&
waitflag(&log, &log.rfirst);
log.repeat = 2;
log.revent[0] = '\0';
log.rn = 0;
ok = ok && ibus_input_context_process_key_event(a, 'z', 0, 0) &&
waitflag(&log, &log.rdone) && strcmp(log.revent, "PKP") == 0;
log.repeat = 3;
ibus_input_context_focus_out(a);
ok = ok && waitflag(&log, &log.flushed);
/*
* A word is committed a syllable at a time, so the Hanja key must
* reach into what the client already holds and ask for it back.
*/
c = ibus_bus_create_input_context(bus, "strans-libibus-smoke-c");
if(c == NULL)
ok = 0;
else{
g_signal_connect(c, "commit-text", G_CALLBACK(commit), &log);
g_signal_connect(c, "require-surrounding-text",
G_CALLBACK(required), &log);
g_signal_connect(c, "delete-surrounding-text",
G_CALLBACK(deleted), &log);
log.repeat = 0;
ibus_input_context_set_capabilities(c,
IBUS_CAP_PREEDIT_TEXT|IBUS_CAP_SURROUNDING_TEXT);
ibus_input_context_focus_in(c);
ok = ok && waitflag(&log, &log.required);
around = ibus_text_new_from_string("저는 한");
ibus_input_context_set_surrounding_text(c, around, 4, 4);
log.hanja = 1;
ok = ok && ibus_input_context_process_key_event(c, 's', 0,
IBUS_CONTROL_MASK) &&
ibus_input_context_process_key_event(c, 'w', 0, 0) &&
ibus_input_context_process_key_event(c, 'k', 0, 0) &&
ibus_input_context_process_key_event(c, 'h', 0,
IBUS_CONTROL_MASK) &&
ibus_input_context_process_key_event(c, IBUS_KEY_Return,
0, 0) && waitflag(&log, &log.converted);
log.hanja = 0;
}
if(log.legacy == 0 || log.modern != 0 || log.commit != 5 ||
log.invalid || !log.done || !log.atext || !log.aclear ||
!log.retook || log.clearctx != a || !log.rdone || !log.converted)
ok = 0;
if(c != NULL)
g_object_unref(c);
if(b != NULL)
g_object_unref(b);
g_object_unref(a);
g_object_unref(bus);
if(!ok){
fprintf(stderr,
"ibus_client_smoke: legacy=%d modern=%d commit=%d invalid=%d preedit=%d committed=%d transfer-text=%d a-clear=%d retook=%d repeat=%s flushed=%d required=%d deleted=%d,%d converted=%d\n",
log.legacy, log.modern, log.commit, log.invalid,
log.sawpreedit, log.sawcommit, log.atext, log.aclear,
log.retook, log.revent, log.flushed, log.required,
log.delbefore, log.delcount, log.converted);
return 1;
}
printf("official libibus client preedit, commit, owner clear, take-back and focus-out hand-back: ok\n");
return 0;
}

262
tests/ibus_live_test.c Normal file
View File

@@ -0,0 +1,262 @@
#define _GNU_SOURCE
#include <errno.h>
#include <poll.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "live.h"
#include "livebus.h"
enum
{
Maxconnections = 64,
};
static int
invalidcall(DBusConnection *conn, char *path, char *member)
{
DBusMessage *reply;
const char *name;
int ok;
reply = sendcall(conn, contextcall(path, member), member);
if(reply == NULL)
return 0;
name = dbus_message_get_error_name(reply);
ok = dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR &&
name != NULL && strcmp(name, DBUS_ERROR_INVALID_ARGS) == 0;
if(!ok)
fail("malformed %s returned %s, expected %s", member,
name != NULL ? name : "a non-error reply",
DBUS_ERROR_INVALID_ARGS);
dbus_message_unref(reply);
return ok;
}
/* libibus destroys through the Service interface; the path must go away. */
static int
destroyed(DBusConnection *conn, char *path)
{
DBusMessage *m, *reply;
const char *name;
int ok;
m = dbus_message_new_method_call("org.freedesktop.IBus", path,
"org.freedesktop.IBus.Service", "Destroy");
reply = callret(conn, m, "Service.Destroy");
if(reply == NULL)
return 0;
dbus_message_unref(reply);
reply = sendcall(conn, contextcall(path, "FocusIn"), "FocusIn");
if(reply == NULL)
return 0;
name = dbus_message_get_error_name(reply);
ok = name != NULL && strcmp(name, DBUS_ERROR_UNKNOWN_OBJECT) == 0;
if(!ok)
fail("context survived Service.Destroy");
dbus_message_unref(reply);
return ok;
}
static int
overflowrejected(char *address)
{
DBusConnection *conn;
DBusError err;
struct pollfd pfd;
int fd, n, ok, timeout;
int64_t deadline;
dbus_error_init(&err);
conn = dbus_connection_open_private(address, &err);
if(conn == NULL){
ok = dbus_error_is_set(&err);
dbus_error_free(&err);
return ok || fail("overflow connection failed without a D-Bus error");
}
dbus_error_free(&err);
dbus_connection_set_exit_on_disconnect(conn, FALSE);
if(!dbus_connection_get_unix_fd(conn, &fd)){
closebus(&conn);
return fail("overflow connection has no Unix descriptor");
}
ok = 0;
deadline = nowms() + Calltimeout;
for(;;){
if(!dbus_connection_get_is_connected(conn)){
ok = 1;
break;
}
timeout = leftms(deadline);
if(timeout == 0){
fail("65th IBus connection was not rejected");
break;
}
pfd.fd = fd;
pfd.events = POLLIN;
pfd.revents = 0;
n = poll(&pfd, 1, timeout);
if(n < 0 && errno == EINTR)
continue;
if(n < 0){
fail("poll overflow IBus connection: %s", strerror(errno));
break;
}
if(n == 0)
continue;
if(pfd.revents & POLLNVAL){
fail("overflow IBus descriptor became invalid");
break;
}
dbus_connection_read_write_dispatch(conn, 0);
}
closebus(&conn);
return ok;
}
static int
runcontract(char *address)
{
static char *bad[] = {
"ProcessKeyEvent",
"SetEngine",
"SetCapabilities",
"SetCursorLocation",
"SetCursorLocationRelative",
"SetSurroundingText",
};
DBusConnection *conn;
char path[96];
int i, ok;
conn = openbus(address, "contract");
if(conn == NULL)
return 0;
ok = hello(conn, NULL, 0, "contract Hello") &&
createcontext(conn, path, sizeof path, "contract context");
for(i = 0; ok && i < (int)(sizeof bad / sizeof bad[0]); i++)
ok = invalidcall(conn, path, bad[i]);
ok = ok && destroyed(conn, path);
closebus(&conn);
return ok;
}
static int
runcapacity(char *address)
{
DBusConnection *conn[Maxconnections];
int i, ok;
for(i = 0; i < Maxconnections; i++)
conn[i] = NULL;
ok = 0;
for(i = 0; i < Maxconnections; i++){
conn[i] = openbus(address, "capacity");
if(conn[i] == NULL || !hello(conn[i], NULL, 0, "capacity Hello"))
goto out;
}
if(!overflowrejected(address))
goto out;
closebus(&conn[Maxconnections-1]);
if(!createcontext(conn[0], NULL, 0, "capacity context"))
goto out;
conn[Maxconnections-1] = openbus(address, "replacement");
if(conn[Maxconnections-1] == NULL ||
!hello(conn[Maxconnections-1], NULL, 0, "replacement Hello") ||
!createcontext(conn[Maxconnections-1], NULL, 0, "replacement context"))
goto out;
ok = 1;
out:
for(i = 0; i < Maxconnections; i++)
closebus(&conn[i]);
return ok;
}
static int
runclient(Live *l, char *address, char *program)
{
pid_t pid, n;
int status;
int64_t deadline;
pid = fork();
if(pid < 0)
return fail("fork official libibus client: %s", strerror(errno));
if(pid == 0){
if(setenv("XDG_RUNTIME_DIR", l->runtime, 1) < 0 ||
setenv("XDG_CONFIG_HOME", l->config, 1) < 0 ||
setenv("HOME", l->home, 1) < 0 ||
unsetenv("IBUS_ADDRESS_FILE") < 0)
_exit(126);
execl(program, program, address, (char*)0);
dprintf(STDERR_FILENO, "exec %s: %s\n", program, strerror(errno));
_exit(127);
}
deadline = nowms() + Starttimeout;
for(;;){
n = waitpid(pid, &status, WNOHANG);
if(n == pid)
break;
if(n < 0 && errno == EINTR)
continue;
if(n < 0)
return fail("wait official libibus client: %s", strerror(errno));
if(leftms(deadline) == 0){
fail("official libibus client timed out");
kill(pid, SIGKILL);
do
n = waitpid(pid, &status, 0);
while(n < 0 && errno == EINTR);
return 0;
}
pausems(10);
}
if(!WIFEXITED(status) || WEXITSTATUS(status) != 0)
return fail("official libibus client exited with wait status %#x",
status);
return 1;
}
int
main(int argc, char **argv)
{
Daemon daemon;
Live live;
char address[512];
int capacity, ok;
testname = "ibus_live_test";
capacity = argc == 4 && strcmp(argv[1], "--capacity") == 0;
if(argc != 4){
fprintf(stderr, "usage: ibus_live_test strans mapdir libibus-client\n");
fprintf(stderr, " ibus_live_test --capacity strans mapdir\n");
return 2;
}
daemoninit(&daemon, "daemon");
ok = livesetup(&live, "ibus") &&
startdaemon(&live, &daemon, argv[capacity ? 2 : 1],
argv[capacity ? 3 : 2]) &&
waitready(&live, &daemon, address, sizeof address);
if(ok)
ok = capacity ? runcapacity(address) :
runclient(&live, address, argv[3]) && runcontract(address);
if(!ok)
showerrors(&daemon);
if(!stopdaemon(&daemon))
ok = 0;
if(!closeerrors(&daemon))
ok = 0;
if(!liveclean(&live))
ok = 0;
if(!ok)
return 1;
printf(capacity ? "ibus connection capacity: ok\n" :
"ibus official client and malformed call: ok\n");
return 0;
}

439
tests/ibus_test.c Normal file
View File

@@ -0,0 +1,439 @@
#include "ibus.c"
#include "test.h"
#include <errno.h>
enum
{
Testctrlmask = 1<<2,
};
typedef struct Ibusfix Ibusfix;
struct Ibusfix
{
Channel *oldreply;
Pump pump;
/* A fake connection is nothing but an address to tell two apart. */
char fake[2];
DBusConnection *c1;
DBusConnection *c2;
};
void
ibus_machine_id_fallback(struct ct *t)
{
char root[] = "/tmp/strans-machine-id.XXXXXX";
char primary[128], fallback[128], override[128], got[512];
char *path[2], *old, *saved;
FILE *fp;
int madeprimary, madefallback;
madeprimary = 0;
madefallback = 0;
if(!CT_CHECK(t, mkdtemp(root) != nil))
return;
if(!CT_CHECK(t, snprintf(primary, sizeof primary, "%s/primary", root)
< (int)sizeof primary) ||
!CT_CHECK(t, snprintf(fallback, sizeof fallback, "%s/fallback", root)
< (int)sizeof fallback))
goto cleanup;
if(!CT_CHECK(t, mkdir(primary, 0700) == 0))
goto cleanup;
madeprimary = 1;
fp = fopen(fallback, "w");
if(!CT_CHECK(t, fp != nil))
goto cleanup;
madefallback = 1;
if(!CT_CHECK(t, fputs("fallback-machine-id\n", fp) >= 0)){
fclose(fp);
goto cleanup;
}
if(!CT_CHECK(t, fclose(fp) == 0))
goto cleanup;
path[0] = primary;
path[1] = fallback;
machineidfiles(got, sizeof got, path, nelem(path));
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");
saved = old == nil ? nil : strdup(old);
if(old == nil || CT_CHECK(t, saved != nil)){
if(CT_CHECK(t, snprintf(override, sizeof override, "%s/address", root)
< (int)sizeof override) &&
CT_CHECK(t, setenv("IBUS_ADDRESS_FILE", override, 1) == 0)){
CT_EQ_INT(t, 0, buildaddrpath(got, sizeof got));
CT_EQ_STR(t, override, got);
CT_CHECK(t, setenv("IBUS_ADDRESS_FILE", "", 1) == 0);
CT_EQ_INT(t, -1, buildaddrpath(got, sizeof got));
}
if(saved != nil){
setenv("IBUS_ADDRESS_FILE", saved, 1);
free(saved);
}else
unsetenv("IBUS_ADDRESS_FILE");
}
cleanup:
if(madefallback)
CT_CHECK(t, unlink(fallback) == 0);
if(madeprimary)
CT_CHECK(t, rmdir(primary) == 0);
CT_CHECK(t, rmdir(root) == 0);
}
static void
ibusbegin(Ibusfix *f)
{
Drawcmd dc;
memset(f, 0, sizeof *f);
memset(contexts, 0, sizeof contexts);
memset(conns, 0, sizeof conns);
nconns = 0;
preowner = nil;
while(channbrecv(drawc, &dc) > 0)
;
testengineinit(LangEN);
f->oldreply = replyc;
replyc = chancreate(sizeof(Keyres), 0);
pumpstart(&f->pump, Maxcontexts);
f->c1 = (DBusConnection*)&f->fake[0];
f->c2 = (DBusConnection*)&f->fake[1];
}
static void
ibusend(Ibusfix *f)
{
Drawcmd dc;
int i;
for(i = 0; i < nelem(contexts); i++)
if(contexts[i].conn != nil)
dropcontext(&contexts[i]);
pumpstop(&f->pump);
memset(contexts, 0, sizeof contexts);
memset(conns, 0, sizeof conns);
nconns = 0;
preowner = nil;
testengineinit(LangEN);
while(channbrecv(drawc, &dc) > 0)
;
chanfree(replyc);
replyc = f->oldreply;
}
static Keyreq
nexttrace(struct ct *t, Ibusfix *f, int op, Ictx *ctx)
{
Keyreq req;
memset(&req, 0, sizeof req);
if(!CT_CHECK(t, channbrecv(f->pump.trace, &req) > 0))
return req;
CT_EQ_INT(t, op, req.op);
CT_EQ_PTR(t, ctx, req.owner);
if(ctx != nil)
CT_EQ_INT(t, clientpreedit(ctx), req.clientpre);
CT_EQ_PTR(t, replyc, req.reply);
return req;
}
static void
notrace(struct ct *t, Ibusfix *f)
{
Keyreq req;
CT_CHECK(t, channbrecv(f->pump.trace, &req) <= 0);
}
static Keyres
contextkey(struct ct *t, Ibusfix *f, Ictx *ctx, u32int sym, u32int state)
{
Keyres res;
char text[Maxutf];
memset(&res, 0, sizeof res);
CT_CHECK(t, processkey(ctx, sym, state, text, sizeof text, &res));
nexttrace(t, f, Keypress, ctx);
return res;
}
void
ibus_capability_policy(struct ct *t)
{
Ibusfix f;
Ictx *a, *b;
Keyres res;
ibusbegin(&f);
a = newcontext(f.c1, "/context/cap-a");
b = newcontext(f.c2, "/context/cap-b");
if(!CT_CHECK(t, a != nil && b != nil))
goto cleanup;
CT_EQ_INT(t, 0, a->cap);
a->focused = 1;
res = contextkey(t, &f, a, 'n', Testctrlmask);
CT_CHECK(t, res.eaten);
res = contextkey(t, &f, a, 'k', 0);
checkstr(t, "preedit", "k", &res.preedit);
CT_EQ_PTR(t, a, testengineowner());
a->cap = Ibuscappreedit;
memset(&res, 0, sizeof res);
sendrequest(a, Keycap, 0, 0, &res);
nexttrace(t, &f, Keycap, a);
CT_CHECK(t, res.eaten);
checkstr(t, "preedit", "k", &res.preedit);
b->focused = 1;
b->cap = Ibuscappreedit;
memset(&res, 0, sizeof res);
sendrequest(b, Keycap, 0, 0, &res);
nexttrace(t, &f, Keycap, b);
CT_CHECK(t, !res.eaten);
CT_EQ_PTR(t, a, testengineowner());
checkenginepreedit(t, "k");
a->cap = 0;
memset(&res, 0, sizeof res);
sendrequest(a, Keycap, 0, 0, &res);
nexttrace(t, &f, Keycap, a);
CT_CHECK(t, res.eaten);
checkstr(t, "preedit", "k", &res.preedit);
cleanup:
ibusend(&f);
}
void
ibus_private_input_policy(struct ct *t)
{
static const struct { u32int purpose; int want; } cases[] = {
{ 0, 0 },
{ Purposepassword, 1 },
{ Purposepin, 1 },
{ 37, 0 },
};
Ibusfix f;
Ictx *ctx;
Keyres res;
char text[Maxutf];
int i;
ibusbegin(&f);
ctx = newcontext(f.c1, "/context/private");
if(!CT_CHECK(t, ctx != nil))
goto cleanup;
for(i = 0; i < nelem(cases); i++){
ctx->purpose = cases[i].purpose;
CT_EQ_INT(t, cases[i].want, hidden(ctx));
}
ctx->focused = 1;
ctx->cap = 1;
ctx->purpose = 0;
contextkey(t, &f, ctx, 'n', Testctrlmask);
res = contextkey(t, &f, ctx, 'k', 0);
checkstr(t, "preedit", "k", &res.preedit);
ctx->purpose = Purposepassword;
memset(&res, 0, sizeof res);
CT_CHECK(t, !processkey(ctx, 'x', 0, text, sizeof text, &res));
CT_CHECK(t, !res.eaten);
notrace(t, &f);
checkenginepreedit(t, "k");
ctx->purpose = 0;
res = contextkey(t, &f, ctx, 'a', 0);
checkstr(t, "preedit", "", &res.preedit);
cleanup:
ibusend(&f);
}
/* A second release is silent, and dropping frees the slot. */
static void
checkdrop(struct ct *t, Ibusfix *f, Ictx *ctx, char *path)
{
releasecontext(ctx);
notrace(t, f);
dropcontext(ctx);
notrace(t, f);
CT_EQ_PTR(t, nil, findcontext(f->c1, path));
}
void
ibus_context_lifecycle(struct ct *t)
{
Ibusfix f;
Ictx *a, *b, *reused;
Keyreq req;
Keyres res;
Caret at;
char text[Maxutf];
ibusbegin(&f);
a = newcontext(f.c1, "/context/one");
b = newcontext(f.c2, "/context/one");
if(!CT_CHECK(t, a != nil && b != nil && a != b))
goto cleanup;
CT_EQ_PTR(t, a, findcontext(f.c1, "/context/one"));
CT_EQ_PTR(t, b, findcontext(f.c2, "/context/one"));
CT_EQ_PTR(t, nil, findcontext(f.c1, "/context/missing"));
setcursor(a, -10, -20, 14);
notrace(t, &f);
CT_CHECK(t, a->caret.valid);
memset(&res, 0, sizeof res);
CT_CHECK(t, !processkey(a, 'x', Relmask, text, sizeof text, &res));
CT_CHECK(t, !res.eaten);
notrace(t, &f);
CT_CHECK(t, !a->focused);
CT_EQ_PTR(t, nil, testengineowner());
/* A key stands for the FocusIn a client may never send. */
memset(&res, 0, sizeof res);
CT_CHECK(t, processkey(a, 'x', 0, text, sizeof text, &res));
req = nexttrace(t, &f, Keypress, a);
CT_CHECK(t, a->focused);
CT_CHECK(t, !res.eaten);
CT_CHECK(t, req.caret.valid);
CT_EQ_INT(t, -10, req.caret.x);
CT_EQ_INT(t, -20, req.caret.y);
CT_EQ_INT(t, 14, req.caret.h);
CT_EQ_PTR(t, a, testengineowner());
testenginecaret(&at);
CT_CHECK(t, at.valid);
CT_EQ_INT(t, -10, at.x);
CT_EQ_INT(t, -20, at.y);
res = contextkey(t, &f, a, 'n', Testctrlmask);
CT_CHECK(t, res.eaten);
contextkey(t, &f, a, 'k', 0);
res = contextkey(t, &f, a, 'a', 0);
checkstr(t, "preedit", "", &res.preedit);
b->focused = 1;
res = contextkey(t, &f, b, 'n', 0);
checkstr(t, "preedit", "", &res.preedit);
CT_EQ_PTR(t, b, testengineowner());
setcursor(b, 50, 60, 12);
req = nexttrace(t, &f, Keycaret, b);
CT_CHECK(t, req.caret.valid);
CT_EQ_INT(t, 50, req.caret.x);
testenginecaret(&at);
CT_CHECK(t, at.valid);
CT_EQ_INT(t, 50, at.x);
memset(&res, 0, sizeof res);
sendrequest(a, Keyreset, 0, 0, &res);
nexttrace(t, &f, Keyreset, a);
checkstr(t, "preedit", "", &res.preedit);
checkenginepreedit(t, "");
CT_EQ_PTR(t, b, testengineowner());
setcursor(a, 90, 100, 20);
nexttrace(t, &f, Keycaret, a);
testenginecaret(&at);
CT_EQ_INT(t, 50, at.x);
CT_EQ_INT(t, 60, at.y);
releasecontext(a);
nexttrace(t, &f, Keyrelease, a);
CT_CHECK(t, !a->focused);
CT_CHECK(t, !a->caret.valid);
checkenginepreedit(t, "");
CT_EQ_PTR(t, b, testengineowner());
checkdrop(t, &f, a, "/context/one");
reused = newcontext(f.c1, "/context/stale-destroy");
CT_EQ_PTR(t, a, reused);
reused->focused = 1;
dropcontext(reused);
nexttrace(t, &f, Keyrelease, reused);
notrace(t, &f);
CT_EQ_PTR(t, nil, findcontext(f.c1, "/context/stale-destroy"));
checkenginepreedit(t, "");
CT_EQ_PTR(t, b, testengineowner());
reused = newcontext(f.c1, "/context/reused");
CT_EQ_PTR(t, a, reused);
reused->focused = 1;
dropconncontexts(f.c1);
nexttrace(t, &f, Keyrelease, reused);
notrace(t, &f);
CT_EQ_PTR(t, nil, findcontext(f.c1, "/context/reused"));
checkenginepreedit(t, "");
CT_EQ_PTR(t, b, testengineowner());
dropconncontexts(f.c1);
notrace(t, &f);
memset(&res, 0, sizeof res);
CT_CHECK(t, !processkey(b, 'x', Relmask, text, sizeof text, &res));
CT_CHECK(t, !res.eaten);
notrace(t, &f);
CT_EQ_PTR(t, b, testengineowner());
cleanup:
ibusend(&f);
}
void
ibus_active_release_lifecycle(struct ct *t)
{
Ibusfix f;
Ictx *ctx, *reused;
Keyres res;
ibusbegin(&f);
ctx = newcontext(f.c1, "/context/reset");
if(!CT_CHECK(t, ctx != nil))
goto cleanup;
ctx->focused = 1;
contextkey(t, &f, ctx, 'n', Testctrlmask);
res = contextkey(t, &f, ctx, 'n', 0);
checkstr(t, "preedit", "", &res.preedit);
memset(&res, 0, sizeof res);
sendrequest(ctx, Keyreset, 0, 0, &res);
nexttrace(t, &f, Keyreset, ctx);
checkstr(t, "preedit", "", &res.preedit);
checkenginepreedit(t, "");
CT_CHECK(t, ctx->focused);
CT_EQ_PTR(t, ctx, testengineowner());
res = contextkey(t, &f, ctx, 'k', 0);
checkstr(t, "preedit", "k", &res.preedit);
CT_EQ_PTR(t, ctx, testengineowner());
releasecontext(ctx);
nexttrace(t, &f, Keyrelease, ctx);
CT_CHECK(t, !ctx->focused);
checkenginepreedit(t, "");
CT_EQ_PTR(t, nil, testengineowner());
checkdrop(t, &f, ctx, "/context/reset");
reused = newcontext(f.c1, "/context/destroy");
CT_EQ_PTR(t, ctx, reused);
reused->focused = 1;
contextkey(t, &f, reused, 'n', 0);
CT_EQ_PTR(t, reused, testengineowner());
dropcontext(reused);
nexttrace(t, &f, Keyrelease, reused);
CT_EQ_PTR(t, nil, testengineowner());
notrace(t, &f);
CT_EQ_PTR(t, nil, findcontext(f.c1, "/context/destroy"));
reused = newcontext(f.c1, "/context/prune");
CT_EQ_PTR(t, ctx, reused);
CT_EQ_PTR(t, nil, testengineowner());
contextkey(t, &f, reused, 'n', 0);
CT_CHECK(t, reused->focused);
CT_EQ_PTR(t, reused, testengineowner());
dropconncontexts(f.c1);
nexttrace(t, &f, Keyrelease, reused);
CT_EQ_PTR(t, nil, testengineowner());
notrace(t, &f);
CT_EQ_PTR(t, nil, findcontext(f.c1, "/context/prune"));
dropconncontexts(f.c1);
notrace(t, &f);
cleanup:
ibusend(&f);
}

390
tests/ipc_live_test.c Normal file
View File

@@ -0,0 +1,390 @@
#define _GNU_SOURCE
#include <errno.h>
#include <poll.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include "ipc.h"
#include "live.h"
enum
{
Maxclients = 64,
Cappreedit = 1,
};
typedef struct Response Response;
struct Response
{
int eaten;
int del;
char commit[Ipcfieldmax+1];
char preedit[Ipcfieldmax+1];
};
static size_t
getlen(unsigned char p[Ipclensz])
{
return p[0] | (p[1] << 8);
}
static int
readresponseuntil(int fd, int want, Response *res, int64_t deadline,
int quiet)
{
unsigned char hdr[Ipcresphdrsz], len[Ipclensz];
size_t n;
int rv;
memset(res, 0, sizeof *res);
rv = readuntil(fd, hdr, sizeof hdr, deadline);
if(rv <= 0){
if(!quiet)
fail("read response header: %s",
rv == 0 ? "peer closed" : strerror(errno));
return rv;
}
res->eaten = hdr[0] != 0;
res->del = hdr[1];
n = getlen(hdr + 2);
if(n > Ipcfieldmax){
fail("invalid commit length %zu", n);
return -1;
}
if(n != 0){
rv = readuntil(fd, res->commit, n, deadline);
if(rv <= 0){
if(!quiet)
fail("read commit: %s",
rv == 0 ? "peer closed" : strerror(errno));
return rv;
}
}
res->commit[n] = '\0';
if(!want)
return 1;
rv = readuntil(fd, len, sizeof len, deadline);
if(rv <= 0){
if(!quiet)
fail("read preedit length: %s",
rv == 0 ? "peer closed" : strerror(errno));
return rv;
}
n = getlen(len);
if(n > Ipcfieldmax){
fail("invalid preedit length %zu", n);
return -1;
}
if(n != 0){
rv = readuntil(fd, res->preedit, n, deadline);
if(rv <= 0){
if(!quiet)
fail("read preedit: %s",
rv == 0 ? "peer closed" : strerror(errno));
return rv;
}
}
res->preedit[n] = '\0';
return 1;
}
static int
sendkey(int fd, int want, uint32_t mod, uint32_t key)
{
unsigned char req[Ipcreqsz];
ipcpackreq(req, want, mod, key);
return ipcsend(fd, req, sizeof req) == 0;
}
static int
sendreset(int fd, int want)
{
unsigned char req[Ipcreqsz];
ipcpackreset(req, want);
return ipcsend(fd, req, sizeof req) == 0;
}
static int
sendcap(int fd, int cap)
{
unsigned char req[Ipcreqsz];
ipcpackcap(req, cap);
return ipcsend(fd, req, sizeof req) == 0;
}
static int
sendsurround(int fd, char *text)
{
unsigned char req[Ipcreqsz];
size_t n;
n = strlen(text);
ipcpacksurround(req, n);
return ipcsend(fd, req, sizeof req) == 0 &&
(n == 0 || ipcsend(fd, text, n) == 0);
}
static int
expectresponse(int fd, int want, int eaten, char *commit, char *preedit,
char *where)
{
Response res;
if(readresponseuntil(fd, want, &res, nowms() + Calltimeout, 0) != 1)
return 0;
if(res.eaten != eaten || strcmp(res.commit, commit) != 0 ||
(want && strcmp(res.preedit, preedit) != 0))
return fail("%s: eaten=%d commit=%s preedit=%s",
where, res.eaten, res.commit, res.preedit);
return 1;
}
static int
requestreset(int fd, int want, int eaten, char *commit, char *preedit,
char *where)
{
if(!sendreset(fd, want))
return fail("send %s: %s", where, strerror(errno));
return expectresponse(fd, want, eaten, commit, preedit, where);
}
static int
requestkey(int fd, int want, uint32_t mod, uint32_t key, int eaten,
char *commit, char *preedit, char *where)
{
if(!sendkey(fd, want, mod, key))
return fail("send %s: %s", where, strerror(errno));
return expectresponse(fd, want, eaten, commit, preedit, where);
}
/*
* The Korean habit: 한자 typed, then the Hanja key. The 한 is the
* client's by then, so the daemon must ask for it back and answer with
* the whole word.
*/
static int
requesthanja(int fd)
{
Response res;
if(!sendsurround(fd, "저는 한"))
return fail("send surrounding text: %s", strerror(errno));
if(!requestkey(fd, 1, Mctrl, 's', 1, "", "", "select Korean") ||
!requestkey(fd, 1, 0, 'w', 1, "", "", "jamo") ||
!requestkey(fd, 1, 0, 'k', 1, "", "", "syllable") ||
!requestkey(fd, 1, Mctrl, 'h', 1, "", "", "Hanja search"))
return 0;
if(!sendkey(fd, 1, 0, Kret))
return fail("send Hanja pick: %s", strerror(errno));
if(readresponseuntil(fd, 1, &res, nowms() + Calltimeout, 0) != 1)
return 0;
if(strcmp(res.commit, "漢字") != 0 || res.del != 1)
return fail("Hanja pick committed %s and took back %d",
res.commit, res.del);
return 1;
}
static int
tryclient(char *path, int64_t deadline, int *client)
{
Response res;
int fd, rv;
fd = connectsocket(path, deadline);
if(fd < 0)
return errno == ETIMEDOUT ? -1 : 0;
if(!sendkey(fd, 1, 0, 0)){
close(fd);
return 0;
}
rv = readresponseuntil(fd, 1, &res, deadline, 1);
if(rv != 1){
close(fd);
return rv < 0 && errno == ETIMEDOUT ? -1 : 0;
}
if(res.eaten || res.commit[0] != '\0' || res.preedit[0] != '\0'){
close(fd);
fail("replacement client returned invalid modifier response");
return -1;
}
*client = fd;
return 1;
}
static int
waitslot(char *path, int *client, char *where)
{
int rv;
int64_t deadline;
deadline = nowms() + Calltimeout;
for(;;){
rv = tryclient(path, deadline, client);
if(rv > 0)
return 1;
if(rv < 0 || leftms(deadline) == 0)
return fail("%s: timed out waiting for a worker slot", where);
}
}
static int
overflowrejected(char *path)
{
struct pollfd pfd;
unsigned char byte;
ssize_t n;
int fd, timeout;
int64_t deadline;
deadline = nowms() + Calltimeout;
fd = connectsocket(path, deadline);
if(fd < 0)
return fail("overflow connect: %s", strerror(errno));
if(!sendkey(fd, 1, 0, 0)){
close(fd);
return 1;
}
for(;;){
timeout = leftms(deadline);
if(timeout == 0){
close(fd);
return fail("overflow client was not rejected");
}
pfd.fd = fd;
pfd.events = POLLIN;
pfd.revents = 0;
n = poll(&pfd, 1, timeout);
if(n < 0 && errno == EINTR)
continue;
if(n <= 0){
close(fd);
return fail("poll overflow client: %s",
n == 0 ? "timed out" : strerror(errno));
}
n = recv(fd, &byte, 1, 0);
if(n < 0 && errno == EINTR)
continue;
if(n == 0 || (n < 0 && (errno == ECONNRESET || errno == EPIPE))){
close(fd);
return 1;
}
if(n < 0){
close(fd);
return fail("read overflow rejection: %s", strerror(errno));
}
close(fd);
return fail("overflow client received a response");
}
}
static int
runsmoke(char *path)
{
int client, ok;
client = connectsocket(path, nowms() + Calltimeout);
if(client < 0)
return fail("connect client: %s", strerror(errno));
ok = sendcap(client, Cappreedit) &&
expectresponse(client, 1, 1, "", "", "negotiate preedit") &&
requestkey(client, 1, Mctrl, 'n', 1, "", "",
"select Japanese") &&
requestkey(client, 1, 0, 'k', 1, "", "k", "preedit") &&
requestreset(client, 1, 1, "k", "", "reset") &&
requesthanja(client);
close(client);
return ok;
}
static int
runcapacity(char *path)
{
int client[Maxclients];
int i, ok;
for(i = 0; i < Maxclients; i++)
client[i] = -1;
ok = 0;
client[0] = connectsocket(path, nowms() + Calltimeout);
if(client[0] < 0){
fail("connect first client: %s", strerror(errno));
goto out;
}
if(!requestkey(client[0], 1, Mctrl, 'n', 1, "", "",
"select Japanese"))
goto out;
for(i = 1; i < Maxclients; i++){
client[i] = connectsocket(path, nowms() + Calltimeout);
if(client[i] < 0){
fail("connect capacity client %d: %s", i, strerror(errno));
goto out;
}
if(!requestkey(client[i], 1, 0, 0, 0, "", "",
"capacity modifier"))
goto out;
}
if(!overflowrejected(path))
goto out;
close(client[Maxclients-1]);
client[Maxclients-1] = -1;
if(!waitslot(path, &client[Maxclients-1], "inactive slot recovery"))
goto out;
close(client[0]);
client[0] = -1;
if(!waitslot(path, &client[0], "active slot recovery") ||
!requestkey(client[0], 1, 0, 'a', 1, "", "",
"post-disconnect composition"))
goto out;
ok = 1;
out:
for(i = 0; i < Maxclients; i++)
if(client[i] >= 0)
close(client[i]);
return ok;
}
int
main(int argc, char **argv)
{
Daemon daemon;
Live live;
int capacity, ok;
testname = "ipc_live_test";
capacity = argc == 4 && strcmp(argv[1], "--capacity") == 0;
if((!capacity && argc != 3) || (capacity && argc != 4)){
fprintf(stderr,
"usage: ipc_live_test [--capacity] strans mapdir\n");
return 2;
}
daemoninit(&daemon, "daemon");
ok = livesetup(&live, "ipc") &&
startdaemon(&live, &daemon, argv[1+capacity], argv[2+capacity]) &&
waitready(&live, &daemon, NULL, 0);
if(ok)
ok = capacity ? runcapacity(live.socket) : runsmoke(live.socket);
if(!ok)
showerrors(&daemon);
if(!stopdaemon(&daemon))
ok = 0;
/* SIGTERM must leave no socket behind. */
if(access(live.socket, F_OK) == 0)
ok = fail("socket %s survived the daemon", live.socket);
if(!closeerrors(&daemon))
ok = 0;
if(!liveclean(&live))
ok = 0;
if(!ok)
return 1;
printf("ipc %s: ok\n", capacity ? "connection capacity" : "live smoke");
return 0;
}

View File

@@ -1,5 +1,33 @@
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <time.h>
#include <unistd.h>
#include "dat.h"
#include "fn.h"
#include "test.h" #include "test.h"
#undef accept
#undef listen
#undef send
static int64_t
nowms(void)
{
struct timespec ts;
if(clock_gettime(CLOCK_MONOTONIC, &ts) < 0)
return 0;
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
static void ipcclientdeadlines(struct ct*);
static void ipcconnectcloexec(struct ct*);
void void
ipc_masks_modifiers(struct ct *t) ipc_masks_modifiers(struct ct *t)
{ {
@@ -12,4 +40,408 @@ ipc_masks_modifiers(struct ct *t)
CT_EQ_INT(t, 1, want); CT_EQ_INT(t, 1, want);
CT_EQ_UINT(t, Mctrl|Malt, mod); CT_EQ_UINT(t, Mctrl|Malt, mod);
CT_EQ_UINT(t, 0x1f642, key); CT_EQ_UINT(t, 0x1f642, key);
CT_CHECK(t, !ipcreqreset(buf));
ipcpackreset(buf, 1);
ipcunpackreq(buf, &want, &mod, &key);
CT_EQ_INT(t, 1, want);
CT_EQ_UINT(t, 0, mod);
CT_EQ_UINT(t, 0, key);
CT_CHECK(t, ipcreqreset(buf));
/* Frontends share one keysym and modifier mapping. */
CT_EQ_UINT(t, 'a', ipckeysym('a', 'a'));
CT_EQ_UINT(t, '1', ipckeysym(0xffb1, '1')); /* KP_1 */
CT_EQ_UINT(t, Kret, ipckeysym(0xff0d, '\r'));
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, Kdown, ipckeysym(0xff99, 0)); /* KP_Down */
CT_EQ_UINT(t, Kback, ipckeysym(0xff08, 8));
CT_EQ_UINT(t, Kspec + 0xff, ipckeysym(0xffff, 0x7f)); /* Delete */
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(0xffe1, 0)); /* Shift_L */
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<<26));
}
void
ipc_control_and_caret_frames(struct ct *t)
{
static const uchar capon[Ipcreqsz] = {
Ipcext|Ipcreqwant, Ipcversion, Ipcopcap, 0, 0, 0,
};
static const uchar capoff[Ipcreqsz] = {
Ipcext, Ipcversion, Ipcopcap, 0, 0, 0,
};
static const uchar caret[Ipccaretsz] = {
Ipcext, Ipcversion, Ipcopcaret, 1,
0xfe, 0xff, 0xff, 0xff,
0x04, 0x03, 0x02, 0x01,
0x06, 0x05, 0x00, 0x00,
};
static const uchar nocaret[Ipccaretsz] = {
Ipcext, Ipcversion, Ipcopcaret, 0,
};
uchar buf[Ipccaretsz], bad[Ipccaretsz];
u32int key, mod;
int valid, want;
int32_t x, y, h;
ipcpackcap(buf, 1);
CT_EQ_MEM(t, capon, buf, sizeof capon);
CT_EQ_INT(t, Ipccap, ipcreqtype(buf));
/* The probe remains a harmless key-zero request to an old daemon. */
ipcunpackreq(buf, &want, &mod, &key);
CT_EQ_INT(t, 1, want);
CT_EQ_UINT(t, 0, key);
ipcpackcap(buf, 0);
CT_EQ_MEM(t, capoff, buf, sizeof capoff);
ipcunpackreq(buf, &want, &mod, &key);
CT_EQ_INT(t, 0, want);
CT_EQ_UINT(t, 0, key);
ipcpackcaret(buf, 1, -2, 0x01020304, 0x506);
CT_EQ_MEM(t, caret, buf, sizeof caret);
CT_EQ_INT(t, Ipccaret, ipcreqtype(buf));
if(CT_EQ_INT(t, 0, ipcunpackcaret(buf, &valid, &x, &y, &h))){
CT_EQ_INT(t, 1, valid);
CT_EQ_INT(t, -2, x);
CT_EQ_INT(t, 0x01020304, y);
CT_EQ_INT(t, 0x506, h);
}
ipcpackcaret(buf, 1, INT32_MIN, INT32_MAX, 0);
if(CT_EQ_INT(t, 0, ipcunpackcaret(buf, &valid, &x, &y, &h))){
CT_EQ_INT(t, INT32_MIN, x);
CT_EQ_INT(t, INT32_MAX, y);
CT_EQ_INT(t, 0, h);
}
ipcpackcaret(buf, 0, -2, 3, 4);
CT_EQ_MEM(t, nocaret, buf, sizeof nocaret);
if(CT_EQ_INT(t, 0, ipcunpackcaret(buf, &valid, &x, &y, &h)))
CT_EQ_INT(t, 0, valid);
memcpy(bad, caret, sizeof bad);
bad[1]++;
CT_EQ_INT(t, Ipcunknown, ipcreqtype(bad));
CT_EQ_INT(t, -1, ipcunpackcaret(bad, &valid, &x, &y, &h));
memcpy(bad, caret, sizeof bad);
bad[2]++;
CT_EQ_INT(t, Ipcunknown, ipcreqtype(bad));
CT_EQ_INT(t, -1, ipcunpackcaret(bad, &valid, &x, &y, &h));
memcpy(bad, caret, sizeof bad);
bad[3] = 2;
CT_EQ_INT(t, -1, ipcunpackcaret(bad, &valid, &x, &y, &h));
memcpy(bad, caret, sizeof bad);
bad[0] |= Ipcreqwant;
CT_EQ_INT(t, Ipcunknown, ipcreqtype(bad));
memcpy(bad, caret, sizeof bad);
bad[15] = 0x80;
CT_EQ_INT(t, -1, ipcunpackcaret(bad, &valid, &x, &y, &h));
memcpy(bad, capon, sizeof capon);
bad[3] = 1;
CT_EQ_INT(t, Ipcunknown, ipcreqtype(bad));
ipcpackreq(buf, 1, 0, 'a');
CT_EQ_INT(t, Ipckey, ipcreqtype(buf));
ipcpackreset(buf, 1);
CT_EQ_INT(t, Ipckey, ipcreqtype(buf));
}
void
ipc_runtime_path(struct ct *t)
{
char buf[128], fallback[128], *old, *saved;
old = getenv("XDG_RUNTIME_DIR");
saved = old == nil ? nil : strdup(old);
setenv("XDG_RUNTIME_DIR", "/tmp/strans-runtime-test", 1);
CT_EQ_INT(t, 0, ipcpath(buf, sizeof buf));
CT_EQ_STR(t, "/tmp/strans-runtime-test/strans.sock", buf);
setenv("XDG_RUNTIME_DIR", "/tmp/strans-runtime-test/", 1);
CT_EQ_INT(t, 0, ipcpath(buf, sizeof buf));
CT_EQ_STR(t, "/tmp/strans-runtime-test/strans.sock", buf);
setenv("XDG_RUNTIME_DIR", "relative", 1);
CT_EQ_INT(t, 0, ipcpath(buf, sizeof buf));
snprint(fallback, sizeof fallback, "/tmp/strans.%d", getuid());
CT_EQ_STR(t, fallback, buf);
CT_EQ_INT(t, -1, ipcpath(buf, 4));
if(saved != nil){
setenv("XDG_RUNTIME_DIR", saved, 1);
free(saved);
}else
unsetenv("XDG_RUNTIME_DIR");
}
void
ipc_response_pack_boundaries(struct ct *t)
{
static const uchar withpre[] = { 1, 3, 1, 0, 'A', 2, 0, 'x', 'y' };
static const uchar withoutpre[] = { 1, 0, 1, 0, 'A' };
uchar out[Ipcmaxresp+1], field[Ipcfieldmax];
int n;
n = ipcpackresp(out, sizeof out, 7, 3, "A", 1, "xy", 2, 1);
CT_EQ_INT(t, sizeof withpre, n);
CT_EQ_MEM(t, withpre, out, sizeof withpre);
n = ipcpackresp(out, sizeof out, 1, 0, "A", 1, "xy", 2, 0);
CT_EQ_INT(t, sizeof withoutpre, n);
CT_EQ_MEM(t, withoutpre, out, sizeof withoutpre);
n = ipcpackresp(out, sizeof out, 0, 0, nil, 0, nil, 0, 1);
CT_EQ_INT(t, Ipcresphdrsz + Ipclensz, n);
memset(field, 'x', sizeof field);
memset(out, 0xa5, sizeof out);
n = ipcpackresp(out, Ipcmaxresp, 1, 0,
(char*)field, sizeof field, (char*)field, sizeof field, 1);
CT_EQ_INT(t, Ipcmaxresp, n);
CT_EQ_INT(t, 0, out[2]);
CT_EQ_INT(t, 1, out[3]);
CT_EQ_INT(t, 0, out[4+Ipcfieldmax]);
CT_EQ_INT(t, 1, out[5+Ipcfieldmax]);
CT_EQ_INT(t, 0xa5, out[Ipcmaxresp]);
CT_EQ_INT(t, -1, ipcpackresp(out, Ipcmaxresp-1, 1, 0,
(char*)field, sizeof field, (char*)field, sizeof field, 1));
CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0, 0,
(char*)field, Ipcfieldmax+1, "", 0, 0));
/* A take-back is a rune count and never leaves its byte. */
CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0, -1, "", 0, "", 0, 0));
CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0, 256, "", 0, "", 0, 0));
}
void
ipc_response_empty_and_preedit(struct ct *t)
{
uchar first[Ipcmaxresp], second[Ipcmaxresp];
char commit[Ipcfieldmax+1], preedit[Ipcfieldmax+1];
Ipcresp resp;
int fd[2], nfirst, nsecond;
nfirst = ipcpackresp(first, sizeof first, 0, 0, "", 0, "", 0, 0);
nsecond = ipcpackresp(second, sizeof second, 1, 0,
"go", 2, "kana", 4, 1);
if(!CT_CHECK(t, nfirst > 0 && nsecond > 0))
return;
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
CT_ERRORF(t, "socketpair failed");
return;
}
if(CT_EQ_INT(t, 0, ipcsend(fd[0], first, nfirst)) &&
CT_EQ_INT(t, 0, ipcsend(fd[0], second, nsecond)) &&
CT_EQ_INT(t, 0, ipcreadresp(fd[1], 0, commit, preedit, &resp))){
CT_EQ_INT(t, 0, resp.eaten);
CT_EQ_SIZE(t, 0, resp.commitlen);
CT_EQ_SIZE(t, 0, resp.preeditlen);
CT_EQ_STR(t, "", commit);
}
if(CT_EQ_INT(t, 0, ipcreadresp(fd[1], 1, commit, preedit, &resp))){
CT_EQ_INT(t, 1, resp.eaten);
CT_EQ_SIZE(t, 2, resp.commitlen);
CT_EQ_SIZE(t, 4, resp.preeditlen);
CT_EQ_STR(t, "go", commit);
CT_EQ_STR(t, "kana", preedit);
}
close(fd[0]);
close(fd[1]);
}
void
ipc_response_max_and_drain(struct ct *t)
{
uchar first[Ipcmaxresp], second[Ipcmaxresp], field[Ipcfieldmax];
char commit[Ipcfieldmax+1], preedit[Ipcfieldmax+1];
Ipcresp resp;
int fd[2], nfirst, nsecond;
memset(field, 'x', sizeof field);
nfirst = ipcpackresp(first, sizeof first, 1, 0,
(char*)field, sizeof field, (char*)field, sizeof field, 1);
nsecond = ipcpackresp(second, sizeof second, 0, 0, "ok", 2, "", 0, 0);
if(!CT_CHECK(t, nfirst == Ipcmaxresp && nsecond > 0))
return;
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
CT_ERRORF(t, "socketpair failed");
return;
}
if(CT_EQ_INT(t, 0, ipcsend(fd[0], first, nfirst)) &&
CT_EQ_INT(t, 0, ipcsend(fd[0], second, nsecond)) &&
CT_EQ_INT(t, 0, ipcreadresp(fd[1], 1, commit, preedit, &resp))){
CT_EQ_SIZE(t, Ipcfieldmax, resp.commitlen);
CT_EQ_SIZE(t, Ipcfieldmax, resp.preeditlen);
CT_EQ_MEM(t, field, commit, Ipcfieldmax);
CT_EQ_MEM(t, field, preedit, Ipcfieldmax);
CT_EQ_INT(t, 0, commit[Ipcfieldmax]);
CT_EQ_INT(t, 0, preedit[Ipcfieldmax]);
}
if(CT_EQ_INT(t, 0, ipcreadresp(fd[1], 0, commit, preedit, &resp)))
CT_EQ_STR(t, "ok", commit);
close(fd[0]);
close(fd[1]);
}
void
ipc_response_fragmented_and_truncated(struct ct *t)
{
uchar frame[Ipcmaxresp];
char commit[Ipcfieldmax+1], preedit[Ipcfieldmax+1];
Ipcresp resp;
int fd[2], i, n;
n = ipcpackresp(frame, sizeof frame, 1, 2, "abc", 3, "xy", 2, 1);
if(!CT_CHECK(t, n > 0))
return;
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
CT_ERRORF(t, "socketpair failed");
return;
}
for(i = 0; i < n; i++)
if(ipcsend(fd[0], frame+i, 1) < 0){
CT_ERRORF(t, "fragment send failed");
break;
}
if(i == n && CT_EQ_INT(t, 0, ipcreadresp(fd[1], 1, commit, preedit,
&resp))){
CT_EQ_INT(t, 2, resp.del);
CT_EQ_STR(t, "abc", commit);
CT_EQ_STR(t, "xy", preedit);
}
close(fd[0]);
close(fd[1]);
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
CT_ERRORF(t, "socketpair failed");
return;
}
CT_EQ_INT(t, 0, ipcsend(fd[0], frame, n-1));
shutdown(fd[0], SHUT_WR);
CT_EQ_INT(t, -1, ipcreadresp(fd[1], 1, commit, preedit, &resp));
close(fd[0]);
close(fd[1]);
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
CT_ERRORF(t, "socketpair failed");
return;
}
frame[0] = 2;
CT_EQ_INT(t, 0, ipcsend(fd[0], frame, n));
errno = 0;
CT_EQ_INT(t, -1, ipcreadresp(fd[1], 1, commit, preedit, &resp));
CT_EQ_INT(t, EPROTO, errno);
close(fd[0]);
close(fd[1]);
}
void
ipc_broken_peer_send(struct ct *t)
{
int fd[2];
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
CT_ERRORF(t, "socketpair failed");
return;
}
close(fd[1]);
CT_EQ_INT(t, -1, ipcsend(fd[0], "x", 1));
close(fd[0]);
ipcclientdeadlines(t);
ipcconnectcloexec(t);
}
static void
ipcclientdeadlines(struct ct *t)
{
char fill[4096], commit[Ipcfieldmax+1], preedit[Ipcfieldmax+1];
Ipcresp resp;
int fd[2];
int64_t start, elapsed;
ssize_t n;
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
CT_ERRORF(t, "socketpair failed");
return;
}
start = nowms();
errno = 0;
CT_EQ_INT(t, -1, ipcreadresp(fd[0], 0, commit, preedit, &resp));
elapsed = nowms() - start;
CT_EQ_INT(t, ETIMEDOUT, errno);
CT_CHECK(t, elapsed >= 0 && elapsed < 4*Ipcwaitms);
memset(fill, 'x', sizeof fill);
for(;;){
n = send(fd[0], fill, sizeof fill, MSG_DONTWAIT|MSG_NOSIGNAL);
if(n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
break;
if(n <= 0){
CT_ERRORF(t, "fill send failed: %s", strerror(errno));
close(fd[0]);
close(fd[1]);
return;
}
}
start = nowms();
errno = 0;
CT_EQ_INT(t, -1, ipcsend(fd[0], "x", 1));
elapsed = nowms() - start;
CT_EQ_INT(t, ETIMEDOUT, errno);
CT_CHECK(t, elapsed >= 0 && elapsed < 4*Ipcwaitms);
close(fd[0]);
close(fd[1]);
}
static void
ipcconnectcloexec(struct ct *t)
{
struct sockaddr_un addr;
char root[] = "/tmp/strans-ipc.XXXXXX";
char *old, *saved;
int accepted, client, flags, full, listener;
accepted = client = full = listener = -1;
old = getenv("XDG_RUNTIME_DIR");
saved = old == nil ? nil : strdup(old);
if(mkdtemp(root) == nil){
CT_ERRORF(t, "mkdtemp failed: %s", strerror(errno));
free(saved);
return;
}
setenv("XDG_RUNTIME_DIR", root, 1);
memset(&addr, 0, sizeof addr);
addr.sun_family = AF_UNIX;
if(ipcpath(addr.sun_path, sizeof addr.sun_path) < 0 ||
(listener = socket(AF_UNIX, SOCK_STREAM, 0)) < 0 ||
bind(listener, (struct sockaddr*)&addr, sizeof addr) < 0 ||
listen(listener, 0) < 0){
CT_ERRORF(t, "listen failed: %s", strerror(errno));
goto Out;
}
client = ipcconnect();
if(!CT_CHECK(t, client >= 0))
goto Out;
flags = fcntl(client, F_GETFD);
CT_CHECK(t, flags >= 0 && (flags & FD_CLOEXEC) != 0);
errno = 0;
full = ipcconnect();
CT_EQ_INT(t, -1, full);
CT_CHECK(t, errno == EAGAIN || errno == EWOULDBLOCK);
accepted = accept(listener, nil, nil);
if(!CT_CHECK(t, accepted >= 0))
goto Out;
Out:
if(full >= 0)
close(full);
if(accepted >= 0)
close(accepted);
if(client >= 0)
close(client);
if(listener >= 0)
close(listener);
unlink(addr.sun_path);
rmdir(root);
if(saved != nil){
setenv("XDG_RUNTIME_DIR", saved, 1);
free(saved);
}else
unsetenv("XDG_RUNTIME_DIR");
} }

View File

@@ -1,5 +1,17 @@
#include "dat.h"
#include "fn.h"
#include "test.h" #include "test.h"
static Str
kotrans(char *keys)
{
Str out, raw;
raw = mkstr(keys);
transstr(getlang(LangKO), nil, &raw, &out);
return out;
}
void void
korean_sequences(struct ct *t) korean_sequences(struct ct *t)
{ {
@@ -13,40 +25,78 @@ korean_sequences(struct ct *t)
{ "rr", "ㄱㄱ" }, { "rr", "ㄱㄱ" },
{ "Rk", "" }, { "Rk", "" },
{ "rk1", "가1" }, { "rk1", "가1" },
{ "rt", "" },
{ "rtk", "ㄱ사" },
{ "rtt", "ㄳㅅ" },
{ "kr", "" },
{ "hkr", "" },
{ "kk", "ㅏㅏ" },
}; };
Emit e;
Im state;
Str out; Str out;
Rune key;
char *p;
int i; int i;
for(i = 0; i < nelem(cases); i++){ for(i = 0; i < nelem(cases); i++){
memset(&state, 0, sizeof state); out = kotrans(cases[i].keys);
state.l = getlang(LangKO);
sclear(&out);
for(p = cases[i].keys; *p != '\0'; p++){
key = (uchar)*p;
e = transko(&state, key);
sappend(&out, &e.s);
state.pre = e.next;
if(!e.eat)
sputr(&out, key);
}
sappend(&out, &state.pre);
checkstr(t, cases[i].keys, cases[i].want, &out); checkstr(t, cases[i].keys, cases[i].want, &out);
} }
} }
void
korean_compound_vowels(struct ct *t)
{
static const struct { char *keys, *want; } cases[] = {
{ "hk", "" }, { "ho", "" }, { "hl", "" },
{ "nj", "" }, { "np", "" }, { "nl", "" },
{ "ml", "" },
};
Str out;
int i;
for(i = 0; i < nelem(cases); i++){
out = kotrans(cases[i].keys);
checkstr(t, cases[i].keys, cases[i].want, &out);
}
}
void
korean_compound_finals(struct ct *t)
{
static const struct { char *keys, *want, *split; } cases[] = {
{ "rkrt", "", "각사" }, { "rksw", "", "간자" },
{ "rksg", "", "간하" }, { "rkfr", "", "갈가" },
{ "rkfa", "", "갈마" }, { "rkfq", "", "갈바" },
{ "rkft", "", "갈사" }, { "rkfx", "", "갈타" },
{ "rkfv", "", "갈파" }, { "rkfg", "", "갈하" },
{ "rkqt", "", "갑사" },
};
char keys[16];
Str out;
int i;
for(i = 0; i < nelem(cases); i++){
out = kotrans(cases[i].keys);
checkstr(t, cases[i].keys, cases[i].want, &out);
snprint(keys, sizeof keys, "%sk", cases[i].keys);
out = kotrans(keys);
checkstr(t, keys, cases[i].split, &out);
}
}
void void
korean_backspace(struct ct *t) korean_backspace(struct ct *t)
{ {
static const struct { char *before, *after; } cases[] = { static const struct { char *before, *after; } cases[] = {
{ "", "" }, { "", "" },
{ "", "" },
{ "", "" }, { "", "" },
{ "", "" }, { "", "" }, { "", "" }, { "", "" },
{ "", "" }, { "", "" }, { "", "" },
{ "", "" },
{ "", "" }, { "", "" },
{ "", "" }, { "", "" }, { "", "" }, { "", "" },
{ "", "" }, { "", "" }, { "", "" },
{ "", "" }, { "", "" }, { "", "" },
{ "", "" }, { "", "" },
}; };
Im state; Im state;
int i; int i;

774
tests/live.c Normal file
View File

@@ -0,0 +1,774 @@
#define _GNU_SOURCE
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <poll.h>
#include <signal.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include "ipc.h"
#include "live.h"
char *testname = "live_test";
int
fail(char *fmt, ...)
{
va_list ap;
fprintf(stderr, "%s: ", testname);
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fputc('\n', stderr);
return 0;
}
int64_t
nowms(void)
{
struct timespec ts;
if(clock_gettime(CLOCK_MONOTONIC, &ts) < 0)
return -1;
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
/* A clock that cannot be read counts as an expired deadline. */
int
leftms(int64_t deadline)
{
int64_t n;
n = nowms();
if(n < 0)
return 0;
n = deadline - n;
if(n <= 0)
return 0;
return n > INT_MAX ? INT_MAX : (int)n;
}
void
pausems(int ms)
{
struct timespec ts;
ts.tv_sec = ms / 1000;
ts.tv_nsec = (ms % 1000) * 1000000L;
while(nanosleep(&ts, &ts) < 0 && errno == EINTR)
;
}
int
makedir(char *path)
{
if(mkdir(path, 0700) == 0)
return 1;
return fail("mkdir %s: %s", path, strerror(errno));
}
int
cleardir(char *path)
{
DIR *dir;
struct dirent *de;
char name[576];
int ok;
if(path[0] == '\0')
return 1;
dir = opendir(path);
if(dir == NULL)
return errno == ENOENT ||
fail("open %s: %s", path, strerror(errno));
ok = 1;
errno = 0;
while((de = readdir(dir)) != NULL){
if(strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0)
continue;
if(snprintf(name, sizeof name, "%s/%s", path, de->d_name)
>= (int)sizeof name)
ok = fail("cleanup path is too long: %s", de->d_name);
else if(unlink(name) < 0 && errno != ENOENT)
ok = fail("remove %s: %s", name, strerror(errno));
errno = 0;
}
if(errno != 0)
ok = fail("read %s: %s", path, strerror(errno));
if(closedir(dir) < 0)
ok = fail("close %s: %s", path, strerror(errno));
return ok;
}
static int
rmdirknown(char *path)
{
if(path[0] == '\0' || rmdir(path) == 0 || errno == ENOENT)
return 1;
return fail("rmdir %s: %s", path, strerror(errno));
}
int
livesetup(Live *l, char *prefix)
{
memset(l, 0, sizeof *l);
if(nowms() < 0)
return fail("read monotonic clock: %s", strerror(errno));
if(snprintf(l->root, sizeof l->root, "/tmp/strans-%s.XXXXXX", prefix)
>= (int)sizeof l->root){
l->root[0] = '\0';
return fail("temporary root name is too long");
}
if(mkdtemp(l->root) == NULL){
l->root[0] = '\0';
return fail("mkdtemp: %s", strerror(errno));
}
if(snprintf(l->runtime, sizeof l->runtime, "%s/runtime", l->root)
>= (int)sizeof l->runtime ||
snprintf(l->config, sizeof l->config, "%s/config", l->root)
>= (int)sizeof l->config ||
snprintf(l->ibus, sizeof l->ibus, "%s/ibus", l->config)
>= (int)sizeof l->ibus ||
snprintf(l->bus, sizeof l->bus, "%s/bus", l->ibus)
>= (int)sizeof l->bus ||
snprintf(l->home, sizeof l->home, "%s/home", l->root)
>= (int)sizeof l->home ||
snprintf(l->socket, sizeof l->socket, "%s/strans.sock", l->runtime)
>= (int)sizeof l->socket)
return fail("temporary path is too long");
return makedir(l->runtime) && makedir(l->config) && makedir(l->ibus) &&
makedir(l->bus) && makedir(l->home);
}
/* The daemon owns its endpoints, so leftovers elsewhere are a failure. */
int
liveclean(Live *l)
{
int ok;
ok = 1;
if(l->socket[0] != '\0' && unlink(l->socket) < 0 && errno != ENOENT)
ok = fail("remove IPC socket %s: %s", l->socket, strerror(errno));
if(!cleardir(l->bus)) ok = 0;
if(!rmdirknown(l->bus)) ok = 0;
if(!rmdirknown(l->ibus)) ok = 0;
if(!rmdirknown(l->config)) ok = 0;
if(!rmdirknown(l->runtime)) ok = 0;
if(!rmdirknown(l->home)) ok = 0;
if(!rmdirknown(l->root)) ok = 0;
return ok;
}
void
daemoninit(Daemon *d, char *name)
{
memset(d, 0, sizeof *d);
d->pid = -1;
d->errfd = -1;
d->name = name;
}
void
readerrors(Daemon *d)
{
ssize_t n;
if(d->errfd < 0)
return;
while(d->nerr + 1 < sizeof d->err){
n = read(d->errfd, d->err + d->nerr, sizeof d->err - d->nerr - 1);
if(n > 0){
d->nerr += n;
continue;
}
if(n < 0 && errno == EINTR)
continue;
break;
}
d->err[d->nerr] = '\0';
}
void
showerrors(Daemon *d)
{
readerrors(d);
if(d->nerr != 0)
fprintf(stderr, "%s: %s stderr:\n%s", testname, d->name, d->err);
}
int
closeerrors(Daemon *d)
{
int fd;
readerrors(d);
fd = d->errfd;
d->errfd = -1;
if(fd >= 0 && close(fd) < 0)
return fail("close %s stderr: %s", d->name, strerror(errno));
return 1;
}
/* Point both standard streams at the capture pipe, even if it is 1 or 2. */
static int
childfds(int fd)
{
int flags;
if(fd > STDERR_FILENO){
if(dup2(fd, STDOUT_FILENO) < 0 || dup2(fd, STDERR_FILENO) < 0)
return 0;
close(fd);
return 1;
}
flags = fcntl(fd, F_GETFD);
if(flags < 0 || fcntl(fd, F_SETFD, flags & ~FD_CLOEXEC) < 0)
return 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
startdaemon(Live *l, Daemon *d, char *program, char *arg)
{
int errpipe[2], fd;
long maxfd;
pid_t pid;
if(pipe2(errpipe, O_CLOEXEC|O_NONBLOCK) < 0)
return fail("pipe2: %s", strerror(errno));
pid = fork();
if(pid < 0){
close(errpipe[0]);
close(errpipe[1]);
return fail("fork: %s", strerror(errno));
}
if(pid == 0){
close(errpipe[0]);
if(!childfds(errpipe[1]))
_exit(126);
if(close_range(3, UINT_MAX, 0) < 0){
maxfd = sysconf(_SC_OPEN_MAX);
if(maxfd < 0)
maxfd = 1024;
for(fd = 3; fd < maxfd; fd++)
close(fd);
}
if(setenv("XDG_RUNTIME_DIR", l->runtime, 1) < 0 ||
setenv("XDG_CONFIG_HOME", l->config, 1) < 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("IBUS_ADDRESS") < 0 ||
unsetenv("IBUS_ADDRESS_FILE") < 0){
dprintf(STDERR_FILENO, "set daemon environment: %s\n",
strerror(errno));
_exit(126);
}
execl(program, program, arg, (char*)0);
dprintf(STDERR_FILENO, "exec %s: %s\n", program, strerror(errno));
_exit(127);
}
close(errpipe[1]);
d->pid = pid;
d->errfd = errpipe[0];
return 1;
}
int
daemonalive(Daemon *d)
{
int n, status;
if(d->pid <= 0)
return fail("%s is no longer running", d->name);
do
n = waitpid(d->pid, &status, WNOHANG);
while(n < 0 && errno == EINTR);
if(n == 0)
return 1;
if(n == d->pid){
d->pid = -1;
return fail("%s exited unexpectedly with wait status %#x", d->name,
status);
}
if(n < 0){
fail("check %s %ld: %s", d->name, (long)d->pid, strerror(errno));
if(errno == ECHILD)
d->pid = -1;
return 0;
}
return fail("waitpid returned the wrong child for %s", d->name);
}
int
killdaemon(Daemon *d, int *status)
{
int n, ok, wait;
if(d->pid <= 0)
return 1;
ok = 1;
if(kill(d->pid, SIGKILL) < 0 && errno != ESRCH)
ok = fail("kill -9 %s %ld: %s", d->name, (long)d->pid,
strerror(errno));
do
n = waitpid(d->pid, &wait, 0);
while(n < 0 && errno == EINTR);
if(n != d->pid)
ok = fail("reap %s %ld after SIGKILL: %s", d->name, (long)d->pid,
n < 0 ? strerror(errno) : "wrong child");
else if(status != NULL)
*status = wait;
d->pid = -1;
return ok;
}
int
stopdaemon(Daemon *d)
{
struct pollfd pfd;
int n, ok, reaped, status;
int64_t deadline;
if(d->pid <= 0)
return 1;
if(!daemonalive(d))
return 0;
ok = 1;
status = 0;
reaped = 0;
if(kill(d->pid, SIGTERM) < 0 && errno != ESRCH)
ok = fail("kill %s %ld: %s", d->name, (long)d->pid, strerror(errno));
deadline = nowms() + Stoptimeout;
while(d->pid > 0){
do
n = waitpid(d->pid, &status, WNOHANG);
while(n < 0 && errno == EINTR);
if(n == d->pid){
reaped = 1;
d->pid = -1;
break;
}
if(n < 0){
fail("waitpid %s %ld: %s", d->name, (long)d->pid,
strerror(errno));
ok = 0;
if(errno == ECHILD)
d->pid = -1;
else if(!killdaemon(d, NULL))
ok = 0;
break;
}
n = leftms(deadline);
if(n == 0){
fail("%s %ld did not stop after SIGTERM", d->name, (long)d->pid);
ok = 0;
if(!killdaemon(d, NULL))
ok = 0;
break;
}
pfd.fd = d->errfd;
pfd.events = POLLIN|POLLHUP;
pfd.revents = 0;
if(poll(&pfd, 1, n) < 0 && errno != EINTR){
fail("poll %s %ld: %s", d->name, (long)d->pid, strerror(errno));
ok = 0;
if(!killdaemon(d, NULL))
ok = 0;
break;
}
readerrors(d);
}
/* plan9port turns a caught termination note into exit status 1. */
if(reaped && !((WIFSIGNALED(status) && WTERMSIG(status) == SIGTERM) ||
(WIFEXITED(status) && WEXITSTATUS(status) <= 1)))
ok = fail("%s exited with unexpected wait status %#x", d->name,
status);
readerrors(d);
return ok;
}
int
findaddress(Live *l)
{
DIR *dir;
struct dirent *de;
int count, ok;
dir = opendir(l->bus);
if(dir == NULL){
fail("open IBus directory %s: %s", l->bus, strerror(errno));
return -1;
}
count = 0;
ok = 1;
errno = 0;
while((de = readdir(dir)) != NULL){
if(strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0 ||
strstr(de->d_name, ".tmp.") != NULL)
continue;
count++;
if(snprintf(l->addrfile, sizeof l->addrfile, "%s/%s", l->bus,
de->d_name) >= (int)sizeof l->addrfile){
ok = fail("IBus address path is too long");
break;
}
}
if(errno != 0)
ok = fail("read IBus directory %s: %s", l->bus, strerror(errno));
if(closedir(dir) < 0)
ok = fail("close IBus directory %s: %s", l->bus, strerror(errno));
if(!ok)
return -1;
if(count > 1){
fail("found %d IBus address files", count);
return -1;
}
return count;
}
int
readfile(char *path, char *buf, size_t cap, size_t *nread)
{
struct stat st;
ssize_t n;
size_t off;
char extra;
int fd, ok;
fd = open(path, O_RDONLY|O_CLOEXEC);
if(fd < 0)
return fail("open %s: %s", path, strerror(errno));
ok = 0;
if(fstat(fd, &st) < 0){
fail("stat open file %s: %s", path, strerror(errno));
goto out;
}
if(st.st_size < 0 || (uintmax_t)st.st_size >= cap){
fail("file %s is too large", path);
goto out;
}
off = 0;
while(off < (size_t)st.st_size){
n = read(fd, buf + off, (size_t)st.st_size - off);
if(n < 0 && errno == EINTR)
continue;
if(n <= 0){
fail("read %s: %s", path,
n == 0 ? "unexpected EOF" : strerror(errno));
goto out;
}
off += n;
}
do
n = read(fd, &extra, 1);
while(n < 0 && errno == EINTR);
if(n != 0){
fail("file %s changed while being read", path);
goto out;
}
buf[off] = '\0';
*nread = off;
ok = 1;
out:
if(close(fd) < 0){
fail("close %s: %s", path, strerror(errno));
ok = 0;
}
return ok;
}
int
parseaddress(char *contents, size_t ncontents, char *address, size_t naddress,
pid_t *declared)
{
char found[512];
long pid;
int consumed;
consumed = -1;
if(strlen(contents) != ncontents ||
sscanf(contents, "IBUS_ADDRESS=%511[^\n]\nIBUS_DAEMON_PID=%ld\n%n",
found, &pid, &consumed) != 2 || consumed != (int)ncontents || pid <= 0)
return fail("invalid IBus address file contents");
if(snprintf(address, naddress, "%s", found) >= (int)naddress)
return fail("private IBus address is too long");
*declared = (pid_t)pid;
return 1;
}
/*
* Wait for the protected IPC socket and, when addr is not null, for the
* IBus address file the daemon publishes for itself.
*/
int
waitready(Live *l, Daemon *d, char *addr, size_t naddr)
{
struct pollfd pfd;
struct stat st;
char contents[2048];
size_t ncontents;
pid_t declared;
int count, n, timeout;
int64_t deadline;
deadline = nowms() + Starttimeout;
for(;;){
if(lstat(l->socket, &st) == 0 && S_ISSOCK(st.st_mode) &&
(st.st_mode & 0777) == 0600){
if(addr == NULL)
return 1;
count = findaddress(l);
if(count < 0)
return 0;
if(count == 1 &&
readfile(l->addrfile, contents, sizeof contents, &ncontents) &&
parseaddress(contents, ncontents, addr, naddr, &declared) &&
declared == d->pid)
return 1;
}
if(!daemonalive(d))
return 0;
timeout = leftms(deadline);
if(timeout == 0)
return fail("timed out waiting for the %s endpoints", d->name);
pfd.fd = d->errfd;
pfd.events = POLLIN|POLLHUP;
pfd.revents = 0;
n = poll(&pfd, 1, timeout > 20 ? 20 : timeout);
if(n < 0 && errno == EINTR)
continue;
if(n < 0)
return fail("poll %s readiness: %s", d->name, strerror(errno));
if(pfd.revents & (POLLERR|POLLNVAL))
return fail("%s stderr pipe became unusable: %#x", d->name,
pfd.revents);
if(pfd.revents & (POLLIN|POLLHUP))
readerrors(d);
}
}
int
connectsocket(char *path, int64_t deadline)
{
struct sockaddr_un addr;
struct pollfd pfd;
socklen_t nerr;
int err, fd, flags, n;
memset(&addr, 0, sizeof addr);
addr.sun_family = AF_UNIX;
if(snprintf(addr.sun_path, sizeof addr.sun_path, "%s", path)
>= (int)sizeof addr.sun_path){
errno = ENAMETOOLONG;
return -1;
}
fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
if(fd < 0)
return -1;
if(connect(fd, (struct sockaddr*)&addr, sizeof addr) < 0 &&
errno != EINPROGRESS){
err = errno;
close(fd);
errno = err;
return -1;
}
for(;;){
n = leftms(deadline);
if(n == 0){
close(fd);
errno = ETIMEDOUT;
return -1;
}
pfd.fd = fd;
pfd.events = POLLOUT;
pfd.revents = 0;
n = poll(&pfd, 1, n);
if(n < 0 && errno == EINTR)
continue;
if(n <= 0){
err = n == 0 ? ETIMEDOUT : errno;
close(fd);
errno = err;
return -1;
}
break;
}
err = 0;
nerr = sizeof err;
if(getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &nerr) < 0 || err != 0){
if(err == 0)
err = errno;
close(fd);
errno = err;
return -1;
}
flags = fcntl(fd, F_GETFL);
if(flags < 0 || fcntl(fd, F_SETFL, flags & ~O_NONBLOCK) < 0){
err = errno;
close(fd);
errno = err;
return -1;
}
return fd;
}
/* 1 is complete, 0 is peer closure, and -1 is a timeout or I/O error. */
int
readuntil(int fd, void *buf, size_t n, int64_t deadline)
{
struct pollfd pfd;
unsigned char *p;
ssize_t r;
int timeout;
p = buf;
while(n > 0){
timeout = leftms(deadline);
if(timeout == 0){
errno = ETIMEDOUT;
return -1;
}
pfd.fd = fd;
pfd.events = POLLIN;
pfd.revents = 0;
r = poll(&pfd, 1, timeout);
if(r < 0 && errno == EINTR)
continue;
if(r <= 0){
if(r == 0)
errno = ETIMEDOUT;
return -1;
}
r = recv(fd, p, n, 0);
if(r < 0 && errno == EINTR)
continue;
if(r < 0)
return -1;
if(r == 0)
return 0;
p += r;
n -= r;
}
return 1;
}
int
ipcrequest(int fd, uint32_t mod, uint32_t key, unsigned char *want,
size_t nwant, char *where)
{
unsigned char req[Ipcreqsz], got[16];
int rv;
if(nwant > sizeof got)
return fail("%s expected IPC response is too large", where);
ipcpackreq(req, 1, mod, key);
if(ipcsend(fd, req, sizeof req) < 0)
return fail("send %s IPC request: %s", where, strerror(errno));
rv = readuntil(fd, got, nwant, nowms() + Calltimeout);
if(rv != 1)
return fail("read %s IPC response: %s", where,
rv == 0 ? "peer closed" : strerror(errno));
if(memcmp(got, want, nwant) != 0)
return fail("%s IPC response did not match", where);
return 1;
}
int
ipcprobe(int fd, char *where)
{
/* eaten, take-back, no commit, no preedit */
unsigned char empty[] = {0, 0, 0, 0, 0, 0};
return ipcrequest(fd, 0, 0, empty, sizeof empty, where);
}

75
tests/live.h Normal file
View File

@@ -0,0 +1,75 @@
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
/*
* Harness shared by the live tests: a strans daemon started in a private
* XDG_RUNTIME_DIR with its output captured, the endpoints it publishes,
* and the IPC probe used to prove the daemon answers.
* Set testname before anything else; fail prefixes it to every message.
*/
typedef struct Daemon Daemon;
typedef struct Live Live;
enum
{
Calltimeout = 4000,
Starttimeout = 8000,
Stoptimeout = 3000,
Errmax = 8192,
};
struct Daemon
{
pid_t pid;
int errfd;
char *name;
char err[Errmax];
size_t nerr;
};
struct Live
{
char root[256];
char runtime[320];
char config[320];
char ibus[384];
char bus[448];
char home[320];
char socket[384];
char addrfile[512];
char display[32];
};
extern char *testname;
int fail(char*, ...);
int64_t nowms(void);
int leftms(int64_t);
void pausems(int);
int makedir(char*);
int cleardir(char*);
int livesetup(Live*, char*);
int liveclean(Live*);
void daemoninit(Daemon*, char*);
int startxvfb(Live*, Daemon*, char*);
int startdaemon(Live*, Daemon*, char*, char*);
int waitready(Live*, Daemon*, char*, size_t);
int daemonalive(Daemon*);
int stopdaemon(Daemon*);
int killdaemon(Daemon*, int*);
void readerrors(Daemon*);
void showerrors(Daemon*);
int closeerrors(Daemon*);
int findaddress(Live*);
int readfile(char*, char*, size_t, size_t*);
int parseaddress(char*, size_t, char*, size_t, pid_t*);
int connectsocket(char*, int64_t);
int readuntil(int, void*, size_t, int64_t);
int ipcrequest(int, uint32_t, uint32_t, unsigned char*, size_t, char*);
int ipcprobe(int, char*);

168
tests/livebus.c Normal file
View File

@@ -0,0 +1,168 @@
#define _GNU_SOURCE
#include <dbus/dbus.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "live.h"
#include "livebus.h"
DBusConnection*
openbus(char *address, char *where)
{
DBusConnection *conn;
DBusError err;
dbus_error_init(&err);
conn = dbus_connection_open_private(address, &err);
if(conn == NULL){
fail("open %s private IBus connection: %s", where,
err.message != NULL ? err.message : "D-Bus error");
dbus_error_free(&err);
return NULL;
}
dbus_error_free(&err);
dbus_connection_set_exit_on_disconnect(conn, FALSE);
return conn;
}
void
closebus(DBusConnection **conn)
{
if(*conn == NULL)
return;
dbus_connection_close(*conn);
dbus_connection_unref(*conn);
*conn = NULL;
}
DBusMessage*
contextcall(char *path, char *member)
{
return dbus_message_new_method_call("org.freedesktop.IBus", path,
"org.freedesktop.IBus.InputContext", member);
}
DBusMessage*
sendcall(DBusConnection *conn, DBusMessage *m, char *where)
{
DBusMessage *reply;
DBusPendingCall *pending;
int timeout;
int64_t deadline;
if(m == NULL){
fail("allocate %s call", where);
return NULL;
}
pending = NULL;
if(!dbus_connection_send_with_reply(conn, m, &pending, Calltimeout) ||
pending == NULL){
dbus_message_unref(m);
fail("queue %s call", where);
return NULL;
}
dbus_message_unref(m);
deadline = nowms() + Calltimeout;
while(!dbus_pending_call_get_completed(pending)){
timeout = leftms(deadline);
if(timeout == 0){
fail("timed out waiting for %s", where);
goto out;
}
if(!dbus_connection_read_write_dispatch(conn, timeout)){
fail("connection closed waiting for %s", where);
goto out;
}
}
reply = dbus_pending_call_steal_reply(pending);
dbus_pending_call_unref(pending);
if(reply == NULL)
fail("%s completed without a reply", where);
return reply;
out:
dbus_pending_call_cancel(pending);
dbus_pending_call_unref(pending);
return NULL;
}
DBusMessage*
callret(DBusConnection *conn, DBusMessage *m, char *where)
{
DBusMessage *reply;
reply = sendcall(conn, m, where);
if(reply == NULL)
return NULL;
if(dbus_message_get_type(reply) != DBUS_MESSAGE_TYPE_METHOD_RETURN){
fail("%s returned D-Bus message type %d", where,
dbus_message_get_type(reply));
dbus_message_unref(reply);
return NULL;
}
return reply;
}
int
hello(DBusConnection *conn, char *name, size_t nname, char *where)
{
DBusMessage *m, *reply;
DBusError err;
const char *s;
int ok;
m = dbus_message_new_method_call("org.freedesktop.DBus",
"/org/freedesktop/DBus", "org.freedesktop.DBus", "Hello");
reply = callret(conn, m, where);
if(reply == NULL)
return 0;
dbus_error_init(&err);
ok = dbus_message_has_signature(reply, "s") &&
dbus_message_get_args(reply, &err, DBUS_TYPE_STRING, &s,
DBUS_TYPE_INVALID);
if(!ok)
fail("%s returned an invalid Hello reply", where);
else if(s[0] != ':')
ok = fail("%s returned invalid Hello name %s", where, s);
else if(name != NULL && snprintf(name, nname, "%s", s) >= (int)nname)
ok = fail("%s Hello name is too long", where);
dbus_error_free(&err);
dbus_message_unref(reply);
return ok;
}
int
createcontext(DBusConnection *conn, char *path, size_t npath, char *where)
{
DBusMessage *m, *reply;
DBusError err;
const char *client, *p;
int ok;
client = testname;
m = dbus_message_new_method_call("org.freedesktop.IBus",
"/org/freedesktop/IBus", "org.freedesktop.IBus",
"CreateInputContext");
if(m == NULL || !dbus_message_append_args(m, DBUS_TYPE_STRING, &client,
DBUS_TYPE_INVALID)){
if(m != NULL)
dbus_message_unref(m);
return fail("build %s CreateInputContext call", where);
}
reply = callret(conn, m, where);
if(reply == NULL)
return 0;
dbus_error_init(&err);
ok = dbus_message_has_signature(reply, "o") &&
dbus_message_get_args(reply, &err, DBUS_TYPE_OBJECT_PATH, &p,
DBUS_TYPE_INVALID);
if(!ok)
fail("%s returned an invalid context reply", where);
else if(p[0] != '/')
ok = fail("%s returned invalid context path %s", where, p);
else if(path != NULL && snprintf(path, npath, "%s", p) >= (int)npath)
ok = fail("%s context path is too long", where);
dbus_error_free(&err);
dbus_message_unref(reply);
return ok;
}

17
tests/livebus.h Normal file
View File

@@ -0,0 +1,17 @@
#include <stddef.h>
#include <dbus/dbus.h>
/*
* D-Bus half of the live harness: private connections to the IBus address
* a strans daemon publishes, and the calls every IBus client makes first.
* sendcall returns any reply, callret only a method return.
*/
DBusConnection* openbus(char*, char*);
void closebus(DBusConnection**);
DBusMessage* contextcall(char*, char*);
DBusMessage* sendcall(DBusConnection*, DBusMessage*, char*);
DBusMessage* callret(DBusConnection*, DBusMessage*, char*);
int hello(DBusConnection*, char*, size_t, char*);
int createcontext(DBusConnection*, char*, size_t, char*);

View File

@@ -9,13 +9,14 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
MKEMOJI = ROOT / "map" / "mkemoji" MKEMOJI = ROOT / "map" / "mkemoji"
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: return subprocess.run(
args.append(str(source)) args, capture_output=True, text=True, check=False, timeout=TIMEOUT
return subprocess.run(args, capture_output=True, text=True, check=False) )
def table(output): def table(output):
@@ -34,27 +35,31 @@ 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("¹²³⁴⁵⁶⁷⁸⁹")) # The engine walks a prefix's children in rune order, so only the
self.assertEqual(data["_"].split()[:9], list("₁₂₃₄₅₆₇₈₉")) # prefix's own row decides which candidate a digit picks.
self.assertEqual(data["^"].split(), list("¹²³⁴⁵⁶⁷⁸⁹"))
self.assertEqual(data["_"].split(), list("₁₂₃₄₅₆₇₈₉"))
self.assertEqual(data["<"].split()[2], "") self.assertEqual(data["<"].split()[2], "")
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"], "#")
@@ -66,6 +71,7 @@ class MkemojiTest(unittest.TestCase):
"x\t a\n", "x\t a\n",
"\0\ta\n", "\0\ta\n",
"x\ta\0b\n", "x\ta\0b\n",
"x\t;hidden\n",
"x" * 65 + "\ta\n", "x" * 65 + "\ta\n",
"x\t" + "a" * 65 + "\n", "x\t" + "a" * 65 + "\n",
] ]

190
tests/mkhanja_test.py Normal file
View File

@@ -0,0 +1,190 @@
#!/usr/bin/env python3
import os
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
IMPORT = ROOT / "map" / "libhangul2hanja"
GENERATE = ROOT / "map" / "mkhanja"
TIMEOUT = 10
def run(program, source=None, stdin=None, hostile=False):
command = [sys.executable, "-B", str(program)]
if source is not None:
command.append(str(source))
env = None
if hostile:
env = os.environ.copy()
env.update(LC_ALL="C", LANG="C", PYTHONCOERCECLOCALE="0", PYTHONUTF8="0")
env.pop("PYTHONIOENCODING", None)
return subprocess.run(
command,
input=stdin,
capture_output=True,
encoding="utf-8",
env=env,
check=False,
timeout=TIMEOUT,
)
class MkhanjaTest(unittest.TestCase):
def source(self, text):
tmp = tempfile.TemporaryDirectory()
path = Path(tmp.name) / "hanja.txt"
path.write_text(text, encoding="utf-8")
self.addCleanup(tmp.cleanup)
return path
def test_imports_bmp_hanja_rows(self):
result = run(
IMPORT,
self.source(
"# Copyright holder\n"
"# BSD license\n"
"\ud55c:\u6f22:first\n"
"\uac00:\u3400:extension A\n"
"\uae40:\u91d1:compatibility\n"
"\ud55c\uc790:\u6f22\u5b57:word\n"
"\ud55c:\U00020000:astral\n"
"\u3131:\u52a0:jamo\n"
"\ud55c\uae00:\u97d3glyph:mixed\n"
),
)
self.assertEqual(result.returncode, 0, result.stderr)
lines = result.stdout.splitlines()
self.assertEqual(lines[:2], ["# Copyright holder", "# BSD license"])
self.assertEqual(lines[2:], ["", "\u6f22\t\ud55c", "\u3400\t\uac00",
"\u91d1\t\uae40",
"\u6f22\u5b57\t\ud55c\uc790"])
def test_imports_symbol_rows_under_a_jamo(self):
result = run(
IMPORT,
self.source(
"\u3141:\u203b:reference mark\n"
"\u3134:\u300c:bracket\n"
"\u3131:\u3000:ideographic space\n"
"\u3131:\u00ad:soft hyphen\n"
"\u3131:\u52a0:Hanja under a jamo\n"
"\u3141:\u203b\u203b:two runes\n"
"\u3141\u3134:\u203b:two jamo\n"
"\u314f:\u203b:a vowel\n"
),
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout.splitlines(),
["\u203b\t\u3141", "\u300c\t\u3134"])
def test_import_rejects_malformed_and_duplicate_rows(self):
bad = [
"한 漢\n",
"한:漢:first\n한:漢:again\n",
]
for text in bad:
with self.subTest(text=repr(text)):
self.assertEqual(run(IMPORT, self.source(text)).returncode, 1)
def test_import_uses_utf8_without_locale(self):
text = "# 저작권\n한:漢:first\n"
expected = "# 저작권\n\n\t\n"
for source, stdin in ((self.source(text), None), (None, text)):
with self.subTest(source=source):
result = run(IMPORT, source, stdin, hostile=True)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout, expected)
def test_groups_readings_and_preserves_order(self):
result = run(
GENERATE,
self.source(
"# Copyright holder\n"
"# BSD license\n"
"\t\n"
"\t\n"
"\t\n"
"\t\n"
"\t\n"
"漢字\t한자\n"
),
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(
result.stdout.splitlines(),
[
";; Copyright holder",
";; BSD license",
"",
"\t漢 韓",
"\t㐀 家",
"\t",
"한자\t漢字",
],
)
def test_groups_symbols_under_their_jamo(self):
result = run(
GENERATE,
self.source(
"\t\n"
"\t\n"
"\t\n"
),
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout.splitlines(),
["\t※ ○", "\t"])
def test_generator_rejects_bad_rows(self):
bad = [
"漢a\t\n",
"\t한a\n",
"𠀀\t\n",
"\t\n\t\n",
"漢 한\n",
"\t\n",
"\t\n",
"\tㅁㄴ\n",
"※※\t\n",
"\u3000\t\n",
"\u00ad\t\n",
]
for text in bad:
with self.subTest(text=repr(text)):
self.assertEqual(run(GENERATE, self.source(text)).returncode, 1)
def test_generator_uses_utf8_without_locale(self):
result = run(GENERATE, self.source("# 저작권\n\t\n"), hostile=True)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout, ";; 저작권\n\n\t\n")
result = run(GENERATE, hostile=True)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("\t", result.stdout)
def test_generator_reads_both_sources(self):
result = run(GENERATE)
self.assertEqual(result.returncode, 0, result.stderr)
lines = result.stdout.splitlines()
self.assertEqual(lines.count(";; All rights reserved."), 2)
rows = dict(line.split("\t") for line in lines if "\t" in line)
self.assertIn("", rows[""].split())
self.assertIn("", rows[""].split())
def test_generator_keeps_runtime_candidate_limit(self):
rows = "".join(f"{chr(0x4E00 + n)}\t\n" for n in range(129))
result = run(GENERATE, self.source(rows))
self.assertEqual(result.returncode, 0, result.stderr)
candidates = result.stdout.rstrip().split("\t", 1)[1].split()
self.assertEqual(len(candidates), 128)
self.assertEqual(candidates[-1], chr(0x4E00 + 127))
if __name__ == "__main__":
unittest.main()

323
tests/popup_test.c Normal file
View File

@@ -0,0 +1,323 @@
#include "dat.h"
#include "fn.h"
#include "test.h"
/* A popup with every section, tall enough for the tests. */
#define Imgh (2*PopupPad + (Maxdisp + 2)*Fontsz + PopupSep)
enum {
Rgbmask = 0xffffff,
};
static u32int
pixel(u32int *img, Popup *p, int x, int y)
{
return img[y*p->w + x] & Rgbmask;
}
static int
rectcolor(u32int *img, Popup *p, int x0, int y0, int x1, int y1,
u32int color)
{
int x, y;
color &= Rgbmask;
for(y = y0; y < y1; y++)
for(x = x0; x < x1; x++)
if(pixel(img, p, x, y) != color)
return 0;
return 1;
}
static void
checkpadding(struct ct *t, u32int *img, Popup *p)
{
CT_CHECK(t, rectcolor(img, p, 0, 0, p->w, PopupPad, Colbg));
CT_CHECK(t, rectcolor(img, p, 0, p->h - PopupPad,
p->w, p->h, Colbg));
CT_CHECK(t, rectcolor(img, p, 0, 0, PopupPad, p->h, Colbg));
CT_CHECK(t, rectcolor(img, p, p->w - PopupPad, 0,
p->w, p->h, Colbg));
}
void
popup_layout(struct ct *t)
{
static const struct {
int first;
int shown;
int total;
char *want;
} markers[] = {
{ 0, 0, 0, "" },
{ 0, 9, 9, "" },
{ 0, 3, 9, "1-3/9" },
{ 8, 1, 9, "9-9/9" },
{ 0, 9, 10, "1-9/10" },
{ 9, 1, 10, "10-10/10" },
{ 0, 9, 18, "1-9/18" },
{ 9, 9, 18, "10-18/18" },
{ 9, 9, 32, "10-18/32" },
{ 18, 9, 32, "19-27/32" },
{ 27, 5, 32, "28-32/32" },
};
char buf[32];
Area area, mon[2], out, work;
Caret caret;
Drawcmd dc;
Popup p;
Str longrow;
u32int c, *img;
int i, ink, markink, n, numink, selbg, textink, x, y;
for(i = 0; i < nelem(markers); i++){
n = pagemarker(buf, sizeof buf, markers[i].first,
markers[i].shown, markers[i].total);
CT_EQ_INT(t, strlen(markers[i].want), n);
CT_CHECK(t, strcmp(markers[i].want, buf) == 0);
}
/* Monitor selection retains origins and intersects the EWMH work area. */
mon[0] = (Area){0, 0, 1920, 1080};
mon[1] = (Area){1920, 200, 1280, 1024};
work = (Area){0, 30, 3200, 1070};
popuparea(mon, 2, &work, 2500, 500, &out);
CT_EQ_INT(t, 1920, out.x);
CT_EQ_INT(t, 200, out.y);
CT_EQ_INT(t, 1280, out.w);
CT_EQ_INT(t, 900, out.h);
work = (Area){60, 0, 3140, 1224};
popuparea(mon, 2, &work, 100, 100, &out);
CT_EQ_INT(t, 60, out.x);
CT_EQ_INT(t, 0, out.y);
CT_EQ_INT(t, 1860, out.w);
CT_EQ_INT(t, 1080, out.h);
work = (Area){5000, 0, 100, 100};
popuparea(mon, 2, &work, 2500, 500, &out);
CT_EQ_INT(t, mon[1].x, out.x);
CT_EQ_INT(t, mon[1].y, out.y);
CT_EQ_INT(t, mon[1].w, out.w);
CT_EQ_INT(t, mon[1].h, out.h);
mon[0] = (Area){0, 0, 100, 100};
mon[1] = (Area){200, 0, 100, 100};
popuparea(mon, 2, nil, 150, 50, &out);
CT_EQ_INT(t, 0, out.x);
memset(&caret, 0, sizeof caret);
area = (Area){1920, 30, 1280, 900};
popupposition(&caret, 2000, 50, &area, 100, 60, &x, &y);
CT_EQ_INT(t, 2010, x);
CT_EQ_INT(t, 60, y);
popupposition(&caret, 2000, 900, &area, 100, 60, &x, &y);
CT_EQ_INT(t, 830, y); /* above the pointer, not over it */
caret.valid = 1;
caret.x = 2100;
caret.y = 80;
caret.h = 20;
popupposition(&caret, 0, 0, &area, 100, 60, &x, &y);
CT_EQ_INT(t, 2100, x);
CT_EQ_INT(t, 100, y);
caret.x = 3150;
caret.y = 850;
popupposition(&caret, 0, 0, &area, 200, 100, &x, &y);
CT_EQ_INT(t, 3000, x);
CT_EQ_INT(t, 750, y);
/* A popup no larger than a small work area stays inside its origin. */
area = (Area){100, 200, 80, 80};
caret.x = 120;
caret.y = 230;
caret.h = 20;
popupposition(&caret, 0, 0, &area, 80, 80, &x, &y);
CT_EQ_INT(t, 100, x);
CT_EQ_INT(t, 200, y);
textinit();
/* Empty sections reserve nothing. */
memset(&dc, 0, sizeof dc);
dc.sel = -1;
popuplayout(&dc, PopupBasew, Imgh, &p);
CT_EQ_INT(t, 0, p.w);
CT_EQ_INT(t, 0, p.h);
CT_EQ_INT(t, -1, p.prey);
CT_EQ_INT(t, -1, p.rowsy);
CT_EQ_INT(t, -1, p.marky);
/* Preedit only: as wide as its text. */
dc.pre = mkstr("preedit");
popuplayout(&dc, 2*PopupBasew, Imgh, &p);
CT_EQ_INT(t, textwidth(&dc.pre) + 2*PopupPad, p.w);
CT_EQ_INT(t, PopupPad, p.prey);
CT_EQ_INT(t, -1, p.sepy);
CT_EQ_INT(t, -1, p.rowsy);
CT_EQ_INT(t, -1, p.marky);
CT_EQ_INT(t, 2*PopupPad + Fontsz, p.h);
/* Candidates only. */
memset(&dc, 0, sizeof dc);
dc.sel = -1;
dc.nkouho = 2;
dc.kouho[0] = mkstr("short");
dc.kouho[1] = mkstr("candidate");
popuplayout(&dc, 2*PopupBasew, Imgh, &p);
CT_EQ_INT(t, PopupBasew, p.w);
CT_EQ_INT(t, -1, p.prey);
CT_EQ_INT(t, -1, p.sepy);
CT_EQ_INT(t, PopupPad, p.rowsy);
CT_EQ_INT(t, -1, p.marky);
CT_EQ_INT(t, 2*PopupPad + 2*Fontsz, p.h);
CT_EQ_INT(t, PopupTextw, p.textw);
CT_EQ_INT(t, p.w - PopupPad, PopupPad + PopupNumw + p.textw);
/* Preedit and candidates meet at one separator, with no empty rows. */
dc.pre = mkstr("preedit");
popuplayout(&dc, 2*PopupBasew, Imgh, &p);
CT_EQ_INT(t, PopupBasew, p.w);
CT_EQ_INT(t, PopupPad, p.prey);
CT_EQ_INT(t, p.prey + Fontsz, p.sepy);
CT_EQ_INT(t, p.sepy + PopupSep, p.rowsy);
CT_EQ_INT(t, -1, p.marky);
CT_EQ_INT(t, p.rowsy + 2*Fontsz + PopupPad, p.h);
CT_EQ_INT(t, 2*PopupPad + 3*Fontsz + PopupSep, p.h);
/* Long preedit expands to the monitor cap just like a candidate. */
sclear(&longrow);
for(i = 0; i < Maxrunes; i++)
sputr(&longrow, 'W');
memset(&dc, 0, sizeof dc);
dc.pre = longrow;
popuplayout(&dc, 2*PopupBasew, Imgh, &p);
CT_EQ_INT(t, 2*PopupBasew, p.w);
/* The largest panel includes all sections and fits its backing image. */
memset(&dc, 0, sizeof dc);
dc.pre = mkstr("preedit");
dc.nkouho = Maxdisp;
dc.sel = 4;
dc.first = 0;
dc.total = Maxdisp + 1;
for(i = 0; i < Maxdisp; i++)
dc.kouho[i] = mkstr(i == dc.sel ? "M" : "candidate");
dc.kouho[0] = longrow;
popuplayout(&dc, 2*PopupBasew, Imgh, &p);
CT_EQ_INT(t, Maxdisp, p.n);
CT_EQ_INT(t, PopupPad, p.prey);
CT_EQ_INT(t, p.prey + Fontsz, p.sepy);
CT_EQ_INT(t, p.sepy + PopupSep, p.rowsy);
CT_EQ_INT(t, p.rowsy + Maxdisp*Fontsz, p.marky);
CT_EQ_INT(t, p.marky + Fontsz + PopupPad, p.h);
CT_EQ_INT(t, Imgh, p.h);
CT_EQ_INT(t, 2*PopupBasew, p.w);
CT_CHECK(t, p.h <= Imgh);
CT_EQ_INT(t, p.w - PopupPad, p.markx + p.markw);
CT_EQ_INT(t, p.w - 2*PopupPad, p.selw);
CT_EQ_INT(t, p.rowsy + dc.sel*Fontsz, p.sely);
CT_EQ_INT(t, p.w - PopupPad, PopupPad + PopupNumw + p.textw);
img = emalloc(2*PopupBasew*Imgh*sizeof img[0]);
popupdraw(img, &dc, &p);
checkpadding(t, img, &p);
for(x = 0; x < p.w; x++)
CT_EQ_UINT(t,
x >= PopupPad && x < p.w - PopupPad ? Colsep : Colbg,
pixel(img, &p, x, p.sepy));
/* Selection paint and both foreground columns stay inside the row. */
selbg = numink = textink = 0;
for(y = p.sely; y < p.sely + Fontsz; y++){
for(x = 0; x < p.w; x++){
c = pixel(img, &p, x, y);
if(x < PopupPad || x >= PopupPad + p.selw){
CT_EQ_UINT(t, Colbg, c);
continue;
}
if(c == (Colsel & Rgbmask)){
selbg++;
continue;
}
CT_CHECK(t,
(x >= PopupPad && x < PopupPad + PopupNumw) ||
(x >= (PopupPad + PopupNumw) && x < (PopupPad + PopupNumw) + p.textw));
if(x < PopupPad + PopupNumw)
numink++;
else
textink++;
}
}
CT_CHECK(t, Colsel != Colselfg);
CT_CHECK(t, selbg > p.selw*Fontsz/2);
CT_CHECK(t, numink > 0);
CT_CHECK(t, textink > 0);
/* The marker occupies only its compact, right-aligned footer box. */
markink = 0;
for(y = p.marky; y < p.marky + Fontsz; y++){
for(x = 0; x < p.w; x++){
if(pixel(img, &p, x, y) == (Colbg & Rgbmask))
continue;
markink++;
CT_CHECK(t, x >= p.markx && x < p.markx + p.markw);
}
}
CT_CHECK(t, markink > 0);
/* A long candidate grows from the stable base to the monitor cap. */
memset(&dc, 0, sizeof dc);
dc.sel = -1;
dc.nkouho = 1;
dc.kouho[0] = longrow;
n = textwidth(&longrow);
popuplayout(&dc, 2*PopupBasew, Imgh, &p);
CT_EQ_INT(t, 2*PopupBasew, p.w);
CT_CHECK(t, p.textw > PopupTextw);
CT_CHECK(t, n > p.textw);
CT_EQ_INT(t, PopupPad, p.rowsy);
CT_EQ_INT(t, p.w - PopupPad, PopupPad + PopupNumw + p.textw);
popupdraw(img, &dc, &p);
ink = 0;
for(y = p.rowsy; y < p.rowsy + Fontsz; y++){
for(x = 0; x < p.w; x++){
c = pixel(img, &p, x, y);
if(x < PopupPad || x >= p.w - PopupPad)
CT_EQ_UINT(t, Colbg, c);
if(x >= (PopupPad + PopupNumw) && x < (PopupPad + PopupNumw) + p.textw &&
c != (Colbg & Rgbmask))
ink++;
}
}
CT_CHECK(t, ink > 0);
CT_EQ_INT(t, n, textwidth(&longrow));
popuplayout(&dc, PopupBasew - 1, Imgh, &p);
CT_EQ_INT(t, PopupBasew - 1, p.w);
/* Height truncation scrolls the row window to the selected candidate. */
memset(&dc, 0, sizeof dc);
dc.nkouho = Maxdisp;
dc.sel = Maxdisp - 1;
dc.total = Maxdisp;
for(i = 0; i < Maxdisp; i++)
dc.kouho[i] = mkstr("candidate");
popuplayout(&dc, PopupBasew, 2*PopupPad + 2*Fontsz, &p);
CT_EQ_INT(t, 1, p.n);
CT_EQ_INT(t, Maxdisp - 1, p.row0);
CT_EQ_INT(t, PopupPad, p.sely);
CT_CHECK(t, p.marky >= 0);
CT_CHECK(t, p.h <= 2*PopupPad + 2*Fontsz);
popupdraw(img, &dc, &p);
numink = 0;
for(y = p.rowsy; y < p.rowsy + Fontsz; y++)
for(x = PopupPad; x < PopupPad + PopupNumw; x++)
if(pixel(img, &p, x, y) != (Colsel & Rgbmask))
numink++;
markink = 0;
for(y = p.marky; y < p.marky + Fontsz; y++)
for(x = p.markx; x < p.markx + p.markw; x++)
if(pixel(img, &p, x, y) != (Colbg & Rgbmask))
markink++;
CT_CHECK(t, numink > 0); /* full-width */
CT_CHECK(t, markink > 0); /* 9-9/9 */
free(img);
}

549
tests/server_test.c Normal file
View File

@@ -0,0 +1,549 @@
#include <errno.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <unistd.h>
#include "srv.c"
#include "test.h"
#undef recv
typedef struct Serverfix Serverfix;
typedef struct Testclient Testclient;
/* A held pump plus a private client-slot channel of the wanted depth. */
struct Serverfix
{
Pump pump;
Channel *oldclientc;
};
struct Testclient
{
int fd;
int peer;
dev_t dev;
ino_t ino;
Channel *done;
};
static void
serverbegin(Serverfix *f, int nclients)
{
Drawcmd dc;
uchar token;
f->oldclientc = clientc;
while(channbrecv(drawc, &dc) > 0)
;
clientc = chancreate(sizeof token, nclients);
testengineinit(LangJP);
pumpstart(&f->pump, 0);
pumphold(&f->pump, Pumpall);
}
static void
serverend(Serverfix *f)
{
Drawcmd dc;
pumpstop(&f->pump);
chanfree(clientc);
clientc = f->oldclientc;
while(channbrecv(drawc, &dc) > 0)
;
}
static void
clientproc(void *arg)
{
Testclient *client;
uchar token;
client = arg;
clientthread((void*)(uintptr)client->fd);
token = 0;
chansend(client->done, &token);
}
static int
startclient(struct ct *t, Testclient *client)
{
struct stat st;
int fd[2];
uchar token;
memset(client, 0, sizeof *client);
client->fd = -1;
client->peer = -1;
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0)
return CT_ERRORF(t, "socketpair failed: %s", strerror(errno));
client->fd = fd[0];
client->peer = fd[1];
if(fstat(client->fd, &st) < 0){
close(client->fd);
close(client->peer);
client->fd = -1;
client->peer = -1;
return CT_ERRORF(t, "socket stat failed: %s", strerror(errno));
}
client->dev = st.st_dev;
client->ino = st.st_ino;
client->done = chancreate(sizeof token, 0);
token = 0;
if(channbsend(clientc, &token) <= 0){
close(client->fd);
close(client->peer);
chanfree(client->done);
client->fd = -1;
client->peer = -1;
client->done = nil;
return CT_ERRORF(t, "client slot unavailable");
}
if(proccreate(clientproc, client, 8192) < 0){
chanrecv(clientc, &token);
close(client->fd);
close(client->peer);
chanfree(client->done);
client->fd = -1;
client->peer = -1;
client->done = nil;
return CT_ERRORF(t, "client worker creation failed");
}
return 1;
}
static int
sendkey(struct ct *t, Testclient *client, int want, u32int mod, u32int key)
{
uchar req[Ipcreqsz];
ipcpackreq(req, want, mod, key);
if(ipcsend(client->peer, req, sizeof req) == 0)
return 1;
return CT_ERRORF(t, "request send failed: %s", strerror(errno));
}
static int
sendreset(struct ct *t, Testclient *client, int want)
{
uchar req[Ipcreqsz];
ipcpackreset(req, want);
if(ipcsend(client->peer, req, sizeof req) == 0)
return 1;
return CT_ERRORF(t, "reset send failed: %s", strerror(errno));
}
static Keyreq
nextrequestcap(struct ct *t, Pump *p, int op, int cap)
{
Keyreq req;
memset(&req, 0, sizeof req);
chanrecv(p->trace, &req);
CT_EQ_INT(t, op, req.op);
CT_EQ_INT(t, cap, req.clientpre);
return req;
}
static Keyreq
nextrequest(struct ct *t, Pump *p, int op)
{
return nextrequestcap(t, p, op, 1);
}
static void
allowrequest(Pump *p)
{
uchar token;
token = 0;
chansend(p->go, &token);
}
static int
readreply(struct ct *t, Testclient *client, int want, char *wantcommit,
char *preedit)
{
char commit[Ipcfieldmax+1];
Ipcresp resp;
if(ipcreadresp(client->peer, want, commit, preedit, &resp) < 0)
return CT_ERRORF(t, "response read failed: %s", strerror(errno));
CT_EQ_STR(t, wantcommit, commit);
return resp.eaten;
}
static int
sendframe(struct ct *t, Testclient *client, uchar *frame, int n, int fragment)
{
int i, m;
for(i = 0; i < n; i += m){
m = fragment && n-i > 1 ? 1 : n-i;
if(ipcsend(client->peer, frame+i, m) < 0)
return CT_ERRORF(t, "frame send failed: %s", strerror(errno));
}
return 1;
}
static void
checknoreply(struct ct *t, Testclient *client)
{
uchar byte;
ssize_t n;
errno = 0;
n = recv(client->peer, &byte, 1, MSG_PEEK|MSG_DONTWAIT);
CT_EQ_INT(t, -1, n);
CT_CHECK(t, errno == EAGAIN || errno == EWOULDBLOCK);
}
static void
waitclient(struct ct *t, Testclient *client)
{
struct stat st;
uchar token;
chanrecv(client->done, &token);
errno = 0;
if(fstat(client->fd, &st) == 0)
CT_CHECK(t, st.st_dev != client->dev || st.st_ino != client->ino);
else
CT_EQ_INT(t, EBADF, errno);
chanfree(client->done);
client->done = nil;
}
static void
disconnectclient(struct ct *t, Pump *p, Testclient *client, void *owner)
{
Keyreq req;
uchar token;
close(client->peer);
client->peer = -1;
req = nextrequest(t, p, Keyrelease);
if(owner != nil)
CT_EQ_PTR(t, owner, req.owner);
CT_CHECK(t, channbrecv(client->done, &token) <= 0);
allowrequest(p);
waitclient(t, client);
}
void
server_connection_ownership(struct ct *t)
{
Serverfix f;
Testclient a, b, c;
Keyreq req;
Str shown;
char preedit[Ipcfieldmax+1];
void *aowner, *bowner, *cowner;
uchar byte, token;
ssize_t n;
memset(&a, 0, sizeof a);
memset(&b, 0, sizeof b);
memset(&c, 0, sizeof c);
a.fd = a.peer = b.fd = b.peer = c.fd = c.peer = -1;
aowner = bowner = cowner = nil;
serverbegin(&f, 3);
if(!startclient(t, &a) || !startclient(t, &b))
goto cleanup;
if(!sendkey(t, &a, 1, 0, 'k'))
goto cleanup;
req = nextrequest(t, &f.pump, Keypress);
aowner = req.owner;
allowrequest(&f.pump);
CT_CHECK(t, readreply(t, &a, 1, "", preedit));
CT_EQ_STR(t, "k", preedit);
if(!sendkey(t, &a, 1, 0, 'a'))
goto cleanup;
req = nextrequest(t, &f.pump, Keypress);
CT_EQ_PTR(t, aowner, req.owner);
allowrequest(&f.pump);
CT_CHECK(t, readreply(t, &a, 1, "", preedit));
CT_EQ_STR(t, "", preedit);
CT_EQ_PTR(t, aowner, testengineowner());
if(!sendkey(t, &b, 1, 0, 'n'))
goto cleanup;
req = nextrequest(t, &f.pump, Keypress);
bowner = req.owner;
CT_CHECK(t, bowner != aowner);
allowrequest(&f.pump);
CT_CHECK(t, readreply(t, &b, 1, "", preedit));
CT_EQ_STR(t, "", preedit);
CT_EQ_PTR(t, bowner, testengineowner());
if(!sendreset(t, &a, 1))
goto cleanup;
req = nextrequest(t, &f.pump, Keyreset);
CT_EQ_PTR(t, aowner, req.owner);
allowrequest(&f.pump);
/* 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_PTR(t, bowner, testengineowner());
if(!sendkey(t, &b, 1, 0, 0))
goto cleanup;
req = nextrequest(t, &f.pump, Keypress);
allowrequest(&f.pump);
CT_CHECK(t, !readreply(t, &b, 1, "", preedit));
CT_EQ_STR(t, "", preedit);
disconnectclient(t, &f.pump, &a, aowner);
CT_EQ_PTR(t, bowner, testengineowner());
if(!sendkey(t, &b, 1, 0, 'y'))
goto cleanup;
req = nextrequest(t, &f.pump, Keypress);
allowrequest(&f.pump);
CT_CHECK(t, readreply(t, &b, 1, "", preedit));
if(!sendkey(t, &b, 1, 0, 'a'))
goto cleanup;
req = nextrequest(t, &f.pump, Keypress);
allowrequest(&f.pump);
CT_CHECK(t, readreply(t, &b, 1, "", preedit));
CT_EQ_STR(t, "にゃ", preedit);
if(!sendreset(t, &b, 1))
goto cleanup;
req = nextrequest(t, &f.pump, Keyreset);
CT_EQ_PTR(t, bowner, req.owner);
allowrequest(&f.pump);
CT_CHECK(t, readreply(t, &b, 1, "にゃ", preedit));
CT_EQ_STR(t, "", preedit);
CT_EQ_PTR(t, bowner, testengineowner());
if(!sendkey(t, &b, 0, 0, 'k'))
goto cleanup;
req = nextrequestcap(t, &f.pump, Keypress, 0);
CT_EQ_PTR(t, bowner, req.owner);
allowrequest(&f.pump);
CT_CHECK(t, readreply(t, &b, 0, "", preedit));
errno = 0;
n = recv(b.peer, &byte, 1, MSG_PEEK|MSG_DONTWAIT);
CT_EQ_INT(t, -1, n);
CT_CHECK(t, errno == EAGAIN || errno == EWOULDBLOCK);
CT_EQ_PTR(t, bowner, testengineowner());
if(!sendkey(t, &b, 1, 0, 'a'))
goto cleanup;
req = nextrequest(t, &f.pump, Keypress);
allowrequest(&f.pump);
CT_CHECK(t, readreply(t, &b, 1, "", preedit));
CT_EQ_STR(t, "", preedit);
disconnectclient(t, &f.pump, &b, bowner);
CT_EQ_PTR(t, nil, testengineowner());
testenginepreedit(&shown);
CT_EQ_INT(t, 0, shown.n);
if(!startclient(t, &c))
goto cleanup;
if(!sendkey(t, &c, 1, 0, 'k'))
goto cleanup;
req = nextrequest(t, &f.pump, Keypress);
cowner = req.owner;
close(c.peer);
c.peer = -1;
allowrequest(&f.pump);
req = nextrequest(t, &f.pump, Keyrelease);
CT_EQ_PTR(t, cowner, req.owner);
CT_EQ_PTR(t, cowner, testengineowner());
CT_CHECK(t, channbrecv(c.done, &token) <= 0);
allowrequest(&f.pump);
waitclient(t, &c);
CT_EQ_PTR(t, nil, testengineowner());
CT_CHECK(t, channbrecv(clientc, &token) <= 0);
cleanup:
if(a.peer >= 0)
disconnectclient(t, &f.pump, &a, aowner);
if(b.peer >= 0)
disconnectclient(t, &f.pump, &b, bowner);
if(c.peer >= 0)
disconnectclient(t, &f.pump, &c, cowner);
serverend(&f);
}
void
server_extension_stream(struct ct *t)
{
Serverfix f;
Testclient a, b;
Keyreq req;
uchar frame[Ipccaretsz];
char preedit[Ipcfieldmax+1];
void *aowner, *bowner;
memset(&a, 0, sizeof a);
memset(&b, 0, sizeof b);
a.fd = a.peer = b.fd = b.peer = -1;
aowner = bowner = nil;
serverbegin(&f, 2);
if(!startclient(t, &a))
goto cleanup;
/* Capability and caret frames may be split at any byte boundary. */
ipcpackcap(frame, 1);
if(!sendframe(t, &a, frame, Ipcreqsz, 1))
goto cleanup;
req = nextrequest(t, &f.pump, Keycap);
aowner = req.owner;
CT_EQ_PTR(t, nil, testengineowner());
allowrequest(&f.pump);
CT_EQ_INT(t, 1, readreply(t, &a, 1, "", preedit));
CT_EQ_STR(t, "", preedit);
ipcpackcaret(frame, 1, -101, -7, 23);
if(!sendframe(t, &a, frame, Ipccaretsz, 1))
goto cleanup;
req = nextrequest(t, &f.pump, Keycaret);
CT_EQ_PTR(t, aowner, req.owner);
CT_EQ_INT(t, 1, req.caret.valid);
CT_EQ_INT(t, -101, req.caret.x);
CT_EQ_INT(t, -7, req.caret.y);
CT_EQ_INT(t, 23, req.caret.h);
CT_EQ_PTR(t, nil, testengineowner());
allowrequest(&f.pump);
checknoreply(t, &a);
ipcpackreq(frame, 1, 0, 'k');
if(!sendframe(t, &a, frame, Ipcreqsz, 1))
goto cleanup;
req = nextrequest(t, &f.pump, Keypress);
CT_EQ_PTR(t, aowner, req.owner);
CT_EQ_INT(t, 1, req.caret.valid);
CT_EQ_INT(t, -101, req.caret.x);
CT_EQ_INT(t, -7, req.caret.y);
CT_EQ_INT(t, 23, req.caret.h);
allowrequest(&f.pump);
CT_EQ_INT(t, 1, readreply(t, &a, 1, "", preedit));
CT_EQ_STR(t, "k", preedit);
CT_EQ_PTR(t, aowner, testengineowner());
ipcpackcap(frame, 0);
if(!sendframe(t, &a, frame, Ipcreqsz, 0))
goto cleanup;
req = nextrequestcap(t, &f.pump, Keycap, 0);
CT_EQ_PTR(t, aowner, req.owner);
CT_EQ_INT(t, 1, req.caret.valid);
allowrequest(&f.pump);
CT_EQ_INT(t, 1, readreply(t, &a, 0, "", preedit));
/* Reset is still the old six-byte frame and does not discard caret. */
ipcpackreset(frame, 0);
if(!sendframe(t, &a, frame, Ipcreqsz, 1))
goto cleanup;
req = nextrequestcap(t, &f.pump, Keyreset, 0);
CT_EQ_PTR(t, aowner, req.owner);
CT_EQ_INT(t, 1, req.caret.valid);
CT_EQ_INT(t, -101, req.caret.x);
CT_EQ_INT(t, -7, req.caret.y);
CT_EQ_INT(t, 23, req.caret.h);
allowrequest(&f.pump);
CT_EQ_INT(t, 1, readreply(t, &a, 0, "k", preedit));
ipcpackcap(frame, 1);
if(!sendframe(t, &a, frame, Ipcreqsz, 0))
goto cleanup;
req = nextrequest(t, &f.pump, Keycap);
allowrequest(&f.pump);
CT_EQ_INT(t, 1, readreply(t, &a, 1, "", preedit));
CT_EQ_STR(t, "", preedit);
ipcpackreq(frame, 1, 0, 'n');
if(!sendframe(t, &a, frame, Ipcreqsz, 0))
goto cleanup;
req = nextrequest(t, &f.pump, Keypress);
CT_EQ_INT(t, 1, req.caret.valid);
CT_EQ_INT(t, -101, req.caret.x);
allowrequest(&f.pump);
CT_EQ_INT(t, 1, readreply(t, &a, 1, "", preedit));
CT_EQ_STR(t, "", preedit);
disconnectclient(t, &f.pump, &a, aowner);
/* Disconnect drops state; a legacy client gets the historical defaults. */
if(!startclient(t, &b) || !sendkey(t, &b, 1, 0, 'k'))
goto cleanup;
req = nextrequest(t, &f.pump, Keypress);
bowner = req.owner;
CT_EQ_INT(t, 0, req.caret.valid);
allowrequest(&f.pump);
CT_EQ_INT(t, 1, readreply(t, &b, 1, "", preedit));
CT_EQ_STR(t, "k", preedit);
disconnectclient(t, &f.pump, &b, bowner);
cleanup:
if(a.peer >= 0)
disconnectclient(t, &f.pump, &a, aowner);
if(b.peer >= 0)
disconnectclient(t, &f.pump, &b, bowner);
serverend(&f);
}
void
server_rejects_unknown_extension(struct ct *t)
{
Serverfix f;
Testclient badver, badop, good;
Keyreq req;
uchar frame[Ipccaretsz];
char preedit[Ipcfieldmax+1];
void *owner;
memset(&badver, 0, sizeof badver);
memset(&badop, 0, sizeof badop);
memset(&good, 0, sizeof good);
badver.fd = badver.peer = badop.fd = badop.peer = -1;
good.fd = good.peer = -1;
owner = nil;
serverbegin(&f, 3);
if(!startclient(t, &badver))
goto cleanup;
ipcpackcaret(frame, 1, 3, 4, 5);
frame[1]++;
if(!sendframe(t, &badver, frame, sizeof frame, 0))
goto cleanup;
req = nextrequest(t, &f.pump, Keyrelease);
allowrequest(&f.pump);
waitclient(t, &badver);
close(badver.peer);
badver.peer = -1;
if(!startclient(t, &badop))
goto cleanup;
ipcpackcaret(frame, 1, 3, 4, 5);
frame[2]++;
if(!sendframe(t, &badop, frame, sizeof frame, 1))
goto cleanup;
req = nextrequest(t, &f.pump, Keyrelease);
allowrequest(&f.pump);
waitclient(t, &badop);
close(badop.peer);
badop.peer = -1;
/* A malformed peer must not disturb another client connection. */
if(!startclient(t, &good) || !sendkey(t, &good, 1, 0, 'k'))
goto cleanup;
req = nextrequest(t, &f.pump, Keypress);
owner = req.owner;
allowrequest(&f.pump);
CT_EQ_INT(t, 1, readreply(t, &good, 1, "", preedit));
CT_EQ_STR(t, "k", preedit);
disconnectclient(t, &f.pump, &good, owner);
cleanup:
if(badver.peer >= 0)
disconnectclient(t, &f.pump, &badver, nil);
if(badop.peer >= 0)
disconnectclient(t, &f.pump, &badop, nil);
if(good.peer >= 0)
disconnectclient(t, &f.pump, &good, owner);
serverend(&f);
}

64
tests/skk2ktrans_test.py Executable file
View File

@@ -0,0 +1,64 @@
#!/usr/bin/env python3
import subprocess
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
CONVERTER = ROOT / "map" / "skk2ktrans"
FIXTURES = Path(__file__).parent / "data" / "skk"
TIMEOUT = 10
class Skk2KtransTest(unittest.TestCase):
def test_euc_jp_conversion_is_stable_and_literal(self):
source = (FIXTURES / "source.skk.utf8").read_text(encoding="utf-8")
expected = (FIXTURES / "expected.tsv").read_bytes()
with tempfile.TemporaryDirectory() as directory:
input_path = Path(directory) / "source.skk"
input_path.write_bytes(source.encode("euc_jp"))
result = subprocess.run(
[CONVERTER, input_path], capture_output=True, check=False,
timeout=TIMEOUT)
self.assertEqual(result.returncode, 0, result.stderr.decode())
self.assertEqual(result.stdout, expected)
def test_invalid_euc_jp_is_rejected(self):
result = subprocess.run(
[CONVERTER], input=b"\xff\xff\n", capture_output=True, check=False,
timeout=TIMEOUT)
self.assertNotEqual(result.returncode, 0)
self.assertTrue(result.stderr)
def test_escaped_candidate_is_rejected(self):
source = b"key /escaped\\/slash/\n"
result = subprocess.run(
[CONVERTER], input=source, capture_output=True, check=False,
timeout=TIMEOUT)
self.assertNotEqual(result.returncode, 0)
self.assertIn(b"escaped candidates are unsupported", result.stderr)
def test_expression_candidate_is_rejected(self):
source = b'key /(concat "a/b")/literal/\n'
result = subprocess.run(
[CONVERTER], input=source, capture_output=True, check=False,
timeout=TIMEOUT)
self.assertNotEqual(result.returncode, 0)
self.assertIn(b"expression candidates are unsupported", result.stderr)
def test_files_have_distinct_row_boundaries(self):
with tempfile.TemporaryDirectory() as directory:
first = Path(directory) / "first.skk"
second = Path(directory) / "second.skk"
first.write_bytes(b"a /A/")
second.write_bytes(b"b /B/\n")
result = subprocess.run(
[CONVERTER, first, second], capture_output=True, check=False,
timeout=TIMEOUT)
self.assertEqual(result.returncode, 0, result.stderr.decode())
self.assertEqual(result.stdout, b"a\tA\nb\tB\n")
if __name__ == "__main__":
unittest.main()

View File

@@ -1,21 +1,52 @@
#include "dat.h"
#include "fn.h"
#include "test.h" #include "test.h"
void void
str_init_utf8(struct ct *t) str_init_utf8(struct ct *t)
{ {
static char badtail[] = { 'a', (char)0x80 };
static char incomplete[] = { (char)0xea, (char)0xb0 };
static char overlong[] = { (char)0xc0, (char)0xaf };
static char outofrange[] = {
(char)0xf4, (char)0x90, (char)0x80, (char)0x80,
};
static char surrogate[] = {
(char)0xed, (char)0xa0, (char)0x80,
};
static char unexpected[] = { (char)0x80 };
static const struct {
char *name;
char *src;
int n;
int ok;
char *want;
} cases[] = {
{ "canonical Runeerror", "\xef\xbf\xbd", 3, 1, "\xef\xbf\xbd" },
{ "incomplete", incomplete, sizeof incomplete, 0, "" },
{ "unexpected continuation", unexpected, sizeof unexpected, 0, "" },
{ "overlong", overlong, sizeof overlong, 0, "" },
{ "surrogate", surrogate, sizeof surrogate, 0, "" },
{ "out of range", outofrange, sizeof outofrange, 0, "" },
{ "bad tail", badtail, sizeof badtail, 0, "" },
};
char full[Maxrunes+2]; char full[Maxrunes+2];
char incomplete[] = { (char)0xea, (char)0xb0 };
Str s; Str s;
int i;
s = mkstr("A한😀"); s = mkstr("A한😀");
CT_EQ_INT(t, 3, s.n); CT_EQ_INT(t, 3, s.n);
checkstr(t, "round trip", "A한😀", &s); checkstr(t, "round trip", "A한😀", &s);
sinit(&s, incomplete, sizeof incomplete); for(i = 0; i < nelem(cases); i++){
CT_EQ_INT(t, 0, s.n); memset(&s, 0xa5, sizeof s);
if(sinit(&s, cases[i].src, cases[i].n) != cases[i].ok)
CT_ERRORF(t, "%s: wrong validity", cases[i].name);
checkstr(t, cases[i].name, cases[i].want, &s);
}
memset(full, 'a', sizeof full); memset(full, 'a', sizeof full);
full[sizeof full-1] = '\0'; full[sizeof full-1] = '\0';
sinit(&s, full, strlen(full)); CT_CHECK(t, !sinit(&s, full, strlen(full)));
CT_EQ_INT(t, Maxrunes, s.n); CT_EQ_INT(t, 0, s.n);
} }
void void
@@ -45,7 +76,6 @@ str_utf8_capacity(struct ct *t)
int n; int n;
char *want; char *want;
} cases[] = { } cases[] = {
{ 0, 0, nil },
{ 1, 0, "" }, { 1, 0, "" },
{ 2, 1, "a" }, { 2, 1, "a" },
{ 3, 1, "a" }, { 3, 1, "a" },
@@ -64,10 +94,6 @@ str_utf8_capacity(struct ct *t)
if(n != cases[i].n) if(n != cases[i].n)
CT_ERRORF(t, "size %d: want length %d, got %d", CT_ERRORF(t, "size %d: want length %d, got %d",
cases[i].size, cases[i].n, n); cases[i].size, cases[i].n, n);
if(cases[i].size == 0){
CT_EQ_INT(t, 'Z', buf[0]);
continue;
}
if(strcmp(cases[i].want, buf) != 0) if(strcmp(cases[i].want, buf) != 0)
CT_ERRORF(t, "size %d: want \"%s\", got \"%s\"", CT_ERRORF(t, "size %d: want \"%s\", got \"%s\"",
cases[i].size, cases[i].want, buf); cases[i].size, cases[i].want, buf);
@@ -82,3 +108,17 @@ str_utf8_capacity(struct ct *t)
CT_EQ_STR(t, "😀", buf); CT_EQ_STR(t, "😀", buf);
CT_EQ_INT(t, 'Z', buf[5]); CT_EQ_INT(t, 'Z', buf[5]);
} }
void
str_invalid_and_full_appends(struct ct *t)
{
Str s;
int i;
for(i = 0; i < Maxrunes; i++)
s.r[i] = 'a';
s.n = Maxrunes;
sputr(&s, 'z');
CT_EQ_INT(t, Maxrunes, s.n);
CT_EQ_INT(t, 'a', s.r[Maxrunes-1]);
}

View File

@@ -1,28 +1,55 @@
#ifndef STRANS_TEST_H /* Included after dat.h and fn.h (or after the source under test). */
#define STRANS_TEST_H #include "cutest/cutest.h"
#include "../cutest/cutest.h" /*
* A test's stand-in for imthread: handles engine requests from keyc,
* tracing each one first. Requests whose op is holdop (every request
* when holdop is Pumpall) wait on go before the engine sees them.
*/
typedef struct Pump Pump;
struct Pump
{
Channel *trace;
Channel *go;
Channel *stop;
Channel *done;
int holdop;
int active;
};
#ifndef STRANS_TEST_ENGINE enum
#include "../dat.h" {
#include "../fn.h" Pumpnone = -1,
#endif Pumpall = -2,
};
extern Lang testvi;
Str mkstr(char*); Str mkstr(char*);
int checkstr(struct ct*, char*, char*, Str*); int checkstr(struct ct*, char*, char*, Str*);
Str shownpre(Im*); void checkenginepreedit(struct ct*, char*);
void pumpstart(Pump*, int);
void pumphold(Pump*, int);
void pumpstop(Pump*);
/* Engine internals reached through engine_test.c. */
void testengineinit(int);
void testenginehandle(Keyreq*);
void* testengineowner(void);
void testenginepreedit(Str*);
void testenginecaret(Caret*);
void str_init_utf8(struct ct*); void str_init_utf8(struct ct*);
void str_edit_and_alias(struct ct*); void str_edit_and_alias(struct ct*);
void str_utf8_capacity(struct ct*); void str_utf8_capacity(struct ct*);
void hmap_set_replace_and_grow(struct ct*); void str_invalid_and_full_appends(struct ct*);
void hmap_long_utf8_keys(struct ct*);
void trie_exact_prefix_and_duplicate(struct ct*); void trie_exact_prefix_and_duplicate(struct ct*);
void trie_put_and_unloaded(struct ct*);
void popup_layout(struct ct*);
void font_render(struct ct*);
void production_maps_load(struct ct*); void production_maps_load(struct ct*);
void transmap_states(struct ct*); void transmap_states(struct ct*);
void korean_sequences(struct ct*); void korean_sequences(struct ct*);
void korean_compound_vowels(struct ct*);
void korean_compound_finals(struct ct*);
void korean_backspace(struct ct*); void korean_backspace(struct ct*);
void vietnamese_transitions(struct ct*); void vietnamese_transitions(struct ct*);
void vietnamese_backspace(struct ct*); void vietnamese_backspace(struct ct*);
@@ -30,18 +57,77 @@ void vietnamese_state_lifetime(struct ct*);
void engine_clears_candidates(struct ct*); void engine_clears_candidates(struct ct*);
void engine_backspace_clears_candidates(struct ct*); void engine_backspace_clears_candidates(struct ct*);
void engine_selects_visible_candidate(struct ct*); void engine_selects_visible_candidate(struct ct*);
void engine_candidate_shortcut_modifiers(struct ct*);
void engine_candidate_page_metadata(struct ct*);
void engine_candidate_page_snapshots(struct ct*);
void engine_candidate_page_movement(struct ct*);
void engine_candidate_completion(struct ct*);
void engine_active_owner_lifecycle(struct ct*);
void engine_active_owner_reset(struct ct*);
void engine_active_owner_caret(struct ct*);
void engine_popup_preedit_capability(struct ct*);
void engine_vietnamese_client_preedit(struct ct*);
void engine_commit_contract(struct ct*); void engine_commit_contract(struct ct*);
void engine_language_switch_state(struct ct*); void engine_language_switch_state(struct ct*);
void engine_telex_history_bound(struct ct*); void engine_telex_history_bound(struct ct*);
void engine_korean_modifiers_and_backspace(struct ct*);
void engine_direct_language_modes(struct ct*);
void engine_japanese_readings(struct ct*);
void engine_japanese_candidates(struct ct*);
void engine_japanese_backspace_and_boundaries(struct ct*);
void engine_katakana_sequences(struct ct*);
void engine_emoji_single_candidate(struct ct*); void engine_emoji_single_candidate(struct ct*);
void engine_emoji_queries(struct ct*); void engine_emoji_queries(struct ct*);
void engine_emoji_japanese_and_multirune(struct ct*);
void engine_emoji_digit_aliases(struct ct*); void engine_emoji_digit_aliases(struct ct*);
void engine_emoji_navigation(struct ct*); void engine_emoji_navigation(struct ct*);
void engine_search_candidate_keys(struct ct*);
void engine_emoji_preedit_languages(struct ct*); void engine_emoji_preedit_languages(struct ct*);
void engine_emoji_start_and_unknown(struct ct*); void engine_emoji_start_and_unknown(struct ct*);
void engine_emoji_dictionary_identity(struct ct*); void engine_hanja_search(struct ct*);
void engine_hanja_unknown_and_cancel(struct ct*);
void engine_hanja_reaches_back(struct ct*);
void engine_hanja_word_prefix(struct ct*);
void engine_hanja_korean_keys(struct ct*);
void engine_hanja_backspace(struct ct*);
void engine_hanja_input_languages(struct ct*);
void engine_randomized_stress(struct ct*);
void engine_full_boundary_passthrough(struct ct*);
void dictionary_candidates(struct ct*); void dictionary_candidates(struct ct*);
void dictionary_misses_clear_result(struct ct*); void dictionary_misses(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*);
#endif void ipc_runtime_path(struct ct*);
void ipc_response_pack_boundaries(struct ct*);
void ipc_response_empty_and_preedit(struct ct*);
void ipc_response_max_and_drain(struct ct*);
void ipc_response_fragmented_and_truncated(struct ct*);
void ipc_broken_peer_send(struct ct*);
void server_connection_ownership(struct ct*);
void server_extension_stream(struct ct*);
void server_rejects_unknown_extension(struct ct*);
void ibus_machine_id_fallback(struct ct*);
void ibus_capability_policy(struct ct*);
void ibus_private_input_policy(struct ct*);
void ibus_context_lifecycle(struct ct*);
void ibus_active_release_lifecycle(struct ct*);
void compose_sequences(struct ct*);
void xim_keymap_lookup(struct ct*);
void xim_compound_text(struct ct*);
void xim_adapter_key_contract(struct ct*);
void xim_adapter_release_lifecycle(struct ct*);
void xim_adapter_free_waits_for_release(struct ct*);
void xim_adapter_styles(struct ct*);
void xim_adapter_placement(struct ct*);
void xim_adapter_placement_updates(struct ct*);
void xim_adapter_callback_replacement(struct ct*);
void xim_adapter_callback_unicode(struct ct*);
void xim_adapter_callback_cleanup(struct ct*);
void xim_adapter_callback_transfer(struct ct*);
void xim_adapter_callback_owner_loss(struct ct*);
void xim_adapter_commit_encoding(struct ct*);
void wl_modifier_mask(struct ct*);
void wl_forwarded_keys(struct ct*);
void wl_surrounding_text(struct ct*);
void wl_repeat_ends(struct ct*);

View File

@@ -1,3 +1,5 @@
#include "dat.h"
#include "fn.h"
#include "test.h" #include "test.h"
Str Str
@@ -21,12 +23,82 @@ checkstr(struct ct *t, char *where, char *want, Str *got)
where, want, buf); where, want, buf);
} }
Str void
shownpre(Im *state) checkenginepreedit(struct ct *t, char *want)
{ {
Str shown; Str preedit;
if(state->l->map != nil && mapget(state->l->map, &state->pre, &shown)) testenginepreedit(&preedit);
return shown; checkstr(t, "engine preedit", want, &preedit);
return state->pre; }
static void
pumpthread(void *arg)
{
Pump *p;
Keyreq req;
uchar token;
Alt alts[] = {
{nil, &req, CHANRCV, nil},
{nil, &token, CHANRCV, nil},
{nil, nil, CHANEND, nil},
};
p = arg;
alts[0].c = keyc;
alts[1].c = p->stop;
for(;;)
switch(alt(alts)){
case 0:
chansend(p->trace, &req);
if(p->holdop == Pumpall || p->holdop == req.op)
chanrecv(p->go, &token);
testenginehandle(&req);
break;
case 1:
chansend(p->done, &token);
return;
}
}
/* ntrace is the trace channel's depth: 0 makes every trace a rendezvous. */
void
pumpstart(Pump *p, int ntrace)
{
memset(p, 0, sizeof *p);
p->trace = chancreate(sizeof(Keyreq), ntrace);
p->go = chancreate(sizeof(uchar), 0);
p->stop = chancreate(sizeof(uchar), 0);
p->done = chancreate(sizeof(uchar), 0);
p->holdop = Pumpnone;
/* Its own proc: tests block in socket I/O while the engine runs. */
proccreate(pumpthread, p, 8192);
p->active = 1;
}
void
pumphold(Pump *p, int op)
{
p->holdop = op;
}
/* Every held request must have been let go before stopping. */
void
pumpstop(Pump *p)
{
Keyreq req;
uchar token;
if(!p->active)
return;
while(channbrecv(p->trace, &req) > 0)
;
token = 0;
chansend(p->stop, &token);
chanrecv(p->done, &token);
chanfree(p->trace);
chanfree(p->go);
chanfree(p->stop);
chanfree(p->done);
p->active = 0;
} }

View File

@@ -1,36 +1,71 @@
#include "dat.h"
#include "fn.h"
#include "test.h" #include "test.h"
void void
trie_exact_prefix_and_duplicate(struct ct *t) trie_exact_prefix_and_duplicate(struct ct *t)
{ {
char *v, *sentinel; static const struct {
char *key;
int match;
char *want;
} cases[] = {
{ "a", TrieExact, "alpha" },
{ "ab", TrieExact, "beta" },
{ "", TrieExact, "" },
{ "dupli", TriePrefix, nil },
{ "duplicate", TrieExact, "second" },
{ "missing", TrieMiss, nil },
{ "", TriePrefix, nil },
};
char *v;
Trie *trie; Trie *trie;
int n; Str key;
int i, match, n;
trie = trieopen("data/trie.map"); trie = trieopen("data/trie.map");
v = trieget(trie, "a", 1, &n); for(i = 0; i < nelem(cases); i++){
if(!CT_CHECK(t, v != nil)) key = mkstr(cases[i].key);
goto cleanup; match = trielookup(trie, &key, &v, &n);
CT_EQ_INT(t, 5, n); if(match != cases[i].match){
CT_EQ_MEM(t, "alpha", v, n); CT_ERRORF(t, "case %d: want match %d, got %d",
sentinel = "unchanged"; i, cases[i].match, match);
v = sentinel; continue;
n = 77; }
CT_CHECK(t, trielookup(trie, "dupli", 5, &v, &n)); if(cases[i].want == nil){
CT_EQ_PTR(t, sentinel, v); CT_EQ_PTR(t, nil, v);
CT_EQ_INT(t, 77, n); CT_EQ_INT(t, 0, n);
v = trieget(trie, "duplicate", 9, &n); }else{
if(!CT_CHECK(t, v != nil)) CT_EQ_INT(t, strlen(cases[i].want), n);
goto cleanup; CT_EQ_MEM(t, cases[i].want, v, n);
CT_EQ_INT(t, 6, n); }
CT_EQ_MEM(t, "second", v, n); }
v = trieget(trie, "tabs", 4, &n); trieclose(trie);
if(!CT_CHECK(t, v != nil)) }
goto cleanup;
CT_EQ_INT(t, 7, n); void
CT_EQ_MEM(t, "one\ttwo", v, n); trie_put_and_unloaded(struct ct *t)
CT_EQ_PTR(t, nil, trieget(trie, "missing", 7, &n)); {
cleanup: char *v;
Trie *trie;
Str key;
int n;
key = mkstr("k");
CT_EQ_INT(t, TrieMiss, trielookup(nil, &key, &v, &n));
trie = trienew();
CT_EQ_INT(t, TrieMiss, trielookup(trie, &key, &v, &n));
trieput(trie, "k", 1, "one two", 7);
trieput(trie, "ka", 2, "", 0);
if(CT_EQ_INT(t, TrieExact, trielookup(trie, &key, &v, &n)))
CT_EQ_MEM(t, "one two", v, 7);
key = mkstr("ka");
if(CT_EQ_INT(t, TrieExact, trielookup(trie, &key, &v, &n)))
CT_EQ_INT(t, 0, n);
trieput(trie, "k", 1, "three", 5);
key = mkstr("k");
if(CT_EQ_INT(t, TrieExact, trielookup(trie, &key, &v, &n)))
CT_EQ_MEM(t, "three", v, 5);
trieclose(trie); trieclose(trie);
} }
@@ -66,20 +101,23 @@ transmap_states(struct ct *t)
int eat; int eat;
char *emit; char *emit;
char *next; char *next;
char *mapped;
} cases[] = { } cases[] = {
{ "", 'k', 1, "", "k", "" }, { "", 'k', 1, "", "k" },
{ "k", 'a', 1, "", "ka", "" }, { "k", 'a', 1, "", "ka" },
{ "ka", 's', 1, "", "s", "" }, { "ka", 's', 1, "", "s" },
{ "ka", 'q', 0, "", "", "" }, { "ka", 'q', 0, "", "" },
{ "k", 'q', 0, "k", "", "" }, { "k", 'q', 0, "k", "" },
}; };
Emit e; Emit e;
Im state; Im state;
Trie *fixture, *saved;
int i; int i;
fixture = trieopen("data/hira.map");
memset(&state, 0, sizeof state); memset(&state, 0, sizeof state);
state.l = getlang(LangJP); state.l = getlang(LangJP);
saved = state.l->map;
state.l->map = fixture;
for(i = 0; i < nelem(cases); i++){ for(i = 0; i < nelem(cases); i++){
state.pre = mkstr(cases[i].pre); state.pre = mkstr(cases[i].pre);
e = transmap(&state, cases[i].key); e = transmap(&state, cases[i].key);
@@ -88,6 +126,7 @@ transmap_states(struct ct *t)
i, cases[i].eat, e.eat); i, cases[i].eat, e.eat);
checkstr(t, "emit", cases[i].emit, &e.s); checkstr(t, "emit", cases[i].emit, &e.s);
checkstr(t, "next", cases[i].next, &e.next); checkstr(t, "next", cases[i].next, &e.next);
checkstr(t, "mapped", cases[i].mapped, &e.dict);
} }
trieclose(fixture);
state.l->map = saved;
} }

View File

@@ -1,13 +1,12 @@
#define CT_IMPLEMENTATION #define CT_IMPLEMENTATION
#include "dat.h"
#include "fn.h"
#include "test.h" #include "test.h"
#include <stdarg.h> #include <stdarg.h>
Channel *drawc; Channel *drawc;
Channel *keyc; Channel *keyc;
Channel *dictreqc;
Channel *dictresc;
Lang testvi;
void void
die(char *fmt, ...) die(char *fmt, ...)
@@ -27,10 +26,9 @@ emalloc(ulong n)
{ {
void *p; void *p;
p = malloc(n); p = mallocz(n, 1);
if(p == nil) if(p == nil)
die("out of memory"); die("out of memory");
memset(p, 0, n);
return p; return p;
} }
@@ -46,30 +44,33 @@ erealloc(void *p, ulong n)
static void static void
testmapinit(void) testmapinit(void)
{ {
Lang *jp; Lang *jp, *kata, *vi;
jp = getlang(LangJP); jp = getlang(LangJP);
if(jp == nil) kata = getlang(LangJPK);
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("data/hira.map"); jp->map = trieopen("../map/hira.map");
memset(&testvi, 0, sizeof testvi); kata->map = trieopen("../map/kata.map");
testvi.lang = LangVI; vi->map = trieopen("../map/telex.map");
testvi.mapname = "telex";
testvi.trans = transvi;
testvi.back = backvi;
testvi.map = trieopen("../map/telex.map");
} }
#ifndef STRESS
static const struct ct_test tests[] = { static const struct ct_test tests[] = {
{ "str/init-utf8", str_init_utf8 }, { "str/init-utf8", str_init_utf8 },
{ "str/edit-and-alias", str_edit_and_alias }, { "str/edit-and-alias", str_edit_and_alias },
{ "str/utf8-capacity", str_utf8_capacity }, { "str/utf8-capacity", str_utf8_capacity },
{ "hmap/set-replace-grow", hmap_set_replace_and_grow }, { "str/invalid-full-appends", str_invalid_and_full_appends },
{ "hmap/long-utf8-keys", hmap_long_utf8_keys },
{ "trie/exact-prefix-duplicate", trie_exact_prefix_and_duplicate }, { "trie/exact-prefix-duplicate", trie_exact_prefix_and_duplicate },
{ "trie/put-and-unloaded", trie_put_and_unloaded },
{ "popup/layout", popup_layout },
{ "font/render", font_render },
{ "map/production-lifecycle", production_maps_load }, { "map/production-lifecycle", production_maps_load },
{ "transmap/states", transmap_states }, { "transmap/states", transmap_states },
{ "hangul/sequences", korean_sequences }, { "hangul/sequences", korean_sequences },
{ "hangul/compound-vowels", korean_compound_vowels },
{ "hangul/compound-finals", korean_compound_finals },
{ "hangul/backspace", korean_backspace }, { "hangul/backspace", korean_backspace },
{ "telex/transitions", vietnamese_transitions }, { "telex/transitions", vietnamese_transitions },
{ "telex/backspace", vietnamese_backspace }, { "telex/backspace", vietnamese_backspace },
@@ -77,20 +78,85 @@ static const struct ct_test tests[] = {
{ "engine/clears-candidates", engine_clears_candidates }, { "engine/clears-candidates", engine_clears_candidates },
{ "engine/backspace-clears-candidates", engine_backspace_clears_candidates }, { "engine/backspace-clears-candidates", engine_backspace_clears_candidates },
{ "engine/selects-visible-candidate", engine_selects_visible_candidate }, { "engine/selects-visible-candidate", engine_selects_visible_candidate },
{ "engine/candidate-shortcut-modifiers", engine_candidate_shortcut_modifiers },
{ "engine/candidate-page-metadata", engine_candidate_page_metadata },
{ "engine/candidate-page-snapshots", engine_candidate_page_snapshots },
{ "engine/candidate-page-movement", engine_candidate_page_movement },
{ "engine/candidate-completion", engine_candidate_completion },
{ "engine/active-owner-lifecycle", engine_active_owner_lifecycle },
{ "engine/active-owner-reset", engine_active_owner_reset },
{ "engine/active-owner-caret", engine_active_owner_caret },
{ "engine/popup-preedit-capability", engine_popup_preedit_capability },
{ "engine/vietnamese-client-preedit", engine_vietnamese_client_preedit },
{ "engine/commit-contract", engine_commit_contract }, { "engine/commit-contract", engine_commit_contract },
{ "engine/language-switch-state", engine_language_switch_state }, { "engine/language-switch-state", engine_language_switch_state },
{ "engine/telex-history-bound", engine_telex_history_bound }, { "engine/telex-history-bound", engine_telex_history_bound },
{ "engine/korean-modifiers-backspace", engine_korean_modifiers_and_backspace },
{ "engine/direct-language-modes", engine_direct_language_modes },
{ "engine/japanese-readings", engine_japanese_readings },
{ "engine/japanese-candidates", engine_japanese_candidates },
{ "engine/japanese-backspace-boundaries", engine_japanese_backspace_and_boundaries },
{ "engine/katakana-sequences", engine_katakana_sequences },
{ "engine/emoji-single-candidate", engine_emoji_single_candidate }, { "engine/emoji-single-candidate", engine_emoji_single_candidate },
{ "engine/emoji-queries", engine_emoji_queries }, { "engine/emoji-queries", engine_emoji_queries },
{ "engine/emoji-japanese-multirune", engine_emoji_japanese_and_multirune },
{ "engine/emoji-digit-aliases", engine_emoji_digit_aliases }, { "engine/emoji-digit-aliases", engine_emoji_digit_aliases },
{ "engine/emoji-navigation", engine_emoji_navigation }, { "engine/emoji-navigation", engine_emoji_navigation },
{ "engine/search-candidate-keys", engine_search_candidate_keys },
{ "engine/emoji-preedit-languages", engine_emoji_preedit_languages }, { "engine/emoji-preedit-languages", engine_emoji_preedit_languages },
{ "engine/emoji-start-and-unknown", engine_emoji_start_and_unknown }, { "engine/emoji-start-and-unknown", engine_emoji_start_and_unknown },
{ "engine/emoji-dictionary-identity", engine_emoji_dictionary_identity }, { "engine/hanja-search", engine_hanja_search },
{ "engine/hanja-unknown-cancel", engine_hanja_unknown_and_cancel },
{ "engine/hanja-reaches-back", engine_hanja_reaches_back },
{ "engine/hanja-word-prefix", engine_hanja_word_prefix },
{ "engine/hanja-korean-keys", engine_hanja_korean_keys },
{ "engine/hanja-backspace", engine_hanja_backspace },
{ "engine/hanja-input-languages", engine_hanja_input_languages },
{ "engine/full-boundary-passthrough", engine_full_boundary_passthrough },
{ "dict/candidates", dictionary_candidates }, { "dict/candidates", dictionary_candidates },
{ "dict/misses-clear-result", dictionary_misses_clear_result }, { "dict/misses", dictionary_misses },
{ "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/runtime-path", ipc_runtime_path },
{ "ipc/response-pack-boundaries", ipc_response_pack_boundaries },
{ "ipc/response-empty-preedit", ipc_response_empty_and_preedit },
{ "ipc/response-max-drain", ipc_response_max_and_drain },
{ "ipc/response-fragmented-truncated", ipc_response_fragmented_and_truncated },
{ "ipc/broken-peer-send", ipc_broken_peer_send },
{ "server/connection-ownership", server_connection_ownership },
{ "server/extension-stream", server_extension_stream },
{ "server/rejects-unknown-extension", server_rejects_unknown_extension },
{ "ibus/machine-id-fallback", ibus_machine_id_fallback },
{ "ibus/capability-policy", ibus_capability_policy },
{ "ibus/private-input-policy", ibus_private_input_policy },
{ "ibus/context-lifecycle", ibus_context_lifecycle },
{ "ibus/active-release-lifecycle", ibus_active_release_lifecycle },
{ "compose/sequences", compose_sequences },
{ "xim/keymap-lookup", xim_keymap_lookup },
{ "xim/compound-text", xim_compound_text },
{ "xim/adapter-key-contract", xim_adapter_key_contract },
{ "xim/adapter-release-lifecycle", xim_adapter_release_lifecycle },
{ "xim/adapter-free-waits-release", xim_adapter_free_waits_for_release },
{ "xim/styles", xim_adapter_styles },
{ "xim/placement", xim_adapter_placement },
{ "xim/placement-updates", xim_adapter_placement_updates },
{ "xim/callback-replacement", xim_adapter_callback_replacement },
{ "xim/callback-unicode", xim_adapter_callback_unicode },
{ "xim/callback-cleanup", xim_adapter_callback_cleanup },
{ "xim/callback-transfer", xim_adapter_callback_transfer },
{ "xim/callback-owner-loss", xim_adapter_callback_owner_loss },
{ "xim/commit-encoding", xim_adapter_commit_encoding },
{ "wl/modifier-mask", wl_modifier_mask },
{ "wl/forwarded-keys", wl_forwarded_keys },
{ "wl/surrounding-text", wl_surrounding_text },
{ "wl/repeat-ends", wl_repeat_ends },
}; };
#else
static const struct ct_test tests[] = {
{ "engine/randomized-stress", engine_randomized_stress },
};
#endif
void void
threadmain(int argc, char **argv) threadmain(int argc, char **argv)
@@ -99,19 +165,13 @@ 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);
dictreqc = chancreate(sizeof(Dictreq), 64);
dictresc = chancreate(sizeof(Dictres), 0);
testmapinit(); testmapinit();
status = CT_RUN_ARGS(tests, argc, argv); status = CT_RUN_ARGS(tests, argc, argv);
for(i = 0; i < nlang; i++){ for(i = 0; i < nlang; i++){
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);
chanfree(dictreqc);
chanfree(dictresc);
threadexitsall(status == 0 ? nil : "tests failed"); threadexitsall(status == 0 ? nil : "tests failed");
} }

View File

@@ -1,25 +1,14 @@
#include "dat.h"
#include "fn.h"
#include "test.h" #include "test.h"
static void static void
typevi(struct ct *t, char *keys, char *want) typevi(struct ct *t, char *keys, char *want)
{ {
Emit e; Str out, raw;
Im state;
Str out, shown;
char *p;
memset(&state, 0, sizeof state); raw = mkstr(keys);
state.l = &testvi; transstr(getlang(LangVI), nil, &raw, &out);
sclear(&out);
for(p = keys; *p != '\0'; p++){
e = transvi(&state, (uchar)*p);
sappend(&out, &e.s);
state.pre = e.next;
if(!e.eat)
sputr(&out, (uchar)*p);
}
shown = shownpre(&state);
sappend(&out, &shown);
checkstr(t, keys, want, &out); checkstr(t, keys, want, &out);
} }
@@ -46,6 +35,11 @@ vietnamese_transitions(struct ct *t)
{ "gias", "giá" }, { "gias", "giá" },
{ "Gias", "Giá" }, { "Gias", "Giá" },
{ "gif", "" }, { "gif", "" },
{ "cuar", "của" },
{ "vowis", "với" },
{ "cuwar", "cửa" },
{ "nguyeenx", "nguyễn" },
{ "dduwowngf", "đường" },
{ "\\s", "s" }, { "\\s", "s" },
{ "a\\s", "as" }, { "a\\s", "as" },
}; };
@@ -73,12 +67,12 @@ 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);
checkstr(t, cases[i].raw, cases[i].rawafter, &state.raw); checkstr(t, cases[i].raw, cases[i].rawafter, &state.raw);
shown = shownpre(&state); impre(&state, &shown);
checkstr(t, cases[i].raw, cases[i].after, &shown); checkstr(t, cases[i].raw, cases[i].after, &shown);
} }
} }

165
tests/wl_adapter_test.c Normal file
View File

@@ -0,0 +1,165 @@
#include <wayland-client.h>
#include <xkbcommon/xkbcommon.h>
#include "vkv1.h"
/* The virtual keyboard is the only wire call the tested code makes. */
static int nkeysent;
static void wirekey(struct zwp_virtual_keyboard_v1*, uint32_t, uint32_t,
uint32_t);
#define zwp_virtual_keyboard_v1_key wirekey
#include "wl.c"
#include "test.h"
static void
wirekey(struct zwp_virtual_keyboard_v1 *vkb, uint32_t time, uint32_t code,
uint32_t state)
{
USED(vkb);
USED(time);
USED(code);
USED(state);
nkeysent++;
}
/* Only the modifiers are read from it, so nothing else need be here. */
static char keymaptext[] =
"xkb_keymap {\n"
"xkb_keycodes { <AD01> = 24; };\n"
"xkb_types { type \"ONE_LEVEL\" {\n"
" modifiers = none; map[none] = 1; level_name[1] = \"Any\"; }; };\n"
"xkb_compatibility { };\n"
"xkb_symbols { key <AD01> { [ q ] }; };\n"
"};\n";
void
wl_modifier_mask(struct ct *t)
{
struct xkb_context *ctx;
struct xkb_keymap *km;
xkb_mod_mask_t caps, ctrl, shift;
ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
km = xkb_keymap_new_from_string(ctx, keymaptext,
XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS);
CT_CHECK(t, km != nil);
if(km != nil){
kstate = xkb_state_new(km);
shift = (xkb_mod_mask_t)1 <<
xkb_keymap_mod_get_index(km, XKB_MOD_NAME_SHIFT);
ctrl = (xkb_mod_mask_t)1 <<
xkb_keymap_mod_get_index(km, XKB_MOD_NAME_CTRL);
caps = (xkb_mod_mask_t)1 <<
xkb_keymap_mod_get_index(km, XKB_MOD_NAME_CAPS);
xkb_state_update_mask(kstate, shift, 0, 0, 0, 0, 0);
CT_EQ_UINT(t, Mshift, modmask());
xkb_state_update_mask(kstate, shift|ctrl, 0, 0, 0, 0, 0);
CT_EQ_UINT(t, Mshift|Mctrl, modmask());
/* Caps Lock is not Shift; a Korean key must not shift under it. */
xkb_state_update_mask(kstate, 0, 0, caps, 0, 0, 0);
CT_EQ_UINT(t, 0, modmask());
xkb_state_unref(kstate);
kstate = nil;
xkb_keymap_unref(km);
}
xkb_context_unref(ctx);
}
void
wl_forwarded_keys(struct ct *t)
{
haskeymap = 1;
memset(sent, 0, sizeof sent);
nkeysent = 0;
forward(30, WL_KEYBOARD_KEY_STATE_PRESSED);
CT_CHECK(t, issent(30));
forward(30, WL_KEYBOARD_KEY_STATE_RELEASED);
CT_CHECK(t, !issent(30));
CT_EQ_INT(t, 2, nkeysent);
/* A code the bitmap cannot hold is not passed on at all. */
nkeysent = 0;
forward(Maxcode, WL_KEYBOARD_KEY_STATE_PRESSED);
CT_EQ_INT(t, 0, nkeysent);
/* Whatever is still down is released, once each and no more. */
forward(1, WL_KEYBOARD_KEY_STATE_PRESSED);
forward(200, WL_KEYBOARD_KEY_STATE_PRESSED);
nkeysent = 0;
releasekeys();
CT_EQ_INT(t, 2, nkeysent);
CT_CHECK(t, !issent(1));
CT_CHECK(t, !issent(200));
releasekeys();
CT_EQ_INT(t, 2, nkeysent);
/* Before a keymap the virtual keyboard must not be touched. */
haskeymap = 0;
nkeysent = 0;
forward(30, WL_KEYBOARD_KEY_STATE_PRESSED);
CT_EQ_INT(t, 0, nkeysent);
CT_CHECK(t, !issent(30));
}
/*
* Arming a repeat needs the engine, which is not here; ending one does
* not, and a repeat that outlives its key is the worst this can do.
*/
/*
* The text before the cursor is what a reading may reach into, and a
* take-back is measured in the bytes those runes hold.
*/
void
wl_surrounding_text(struct ct *t)
{
sclear(&pendingsurround);
imsurrounding(nil, nil, "가나다라", 9, 9);
checkstr(t, "the text before the cursor", "가나다", &pendingsurround);
surround = pendingsurround;
CT_EQ_INT(t, 3, backbytes(1));
CT_EQ_INT(t, 6, backbytes(2));
CT_EQ_INT(t, 9, backbytes(9));
CT_EQ_INT(t, 0, backbytes(0));
/* A cursor past the end of the buffer is the whole of it. */
imsurrounding(nil, nil, "ab", 99, 99);
checkstr(t, "a cursor past the end", "ab", &pendingsurround);
/* An activation starts with none, whatever the last one had. */
imactivate(nil, nil);
CT_EQ_INT(t, 0, pendingsurround.n);
sclear(&surround);
}
void
wl_repeat_ends(struct ct *t)
{
haskeymap = 1;
memset(sent, 0, sizeof sent);
/* Not a rate or a delay any seat would pick: the body was empty
* once, and a stored constant would pass for the real thing. */
grabrepeat(nil, nil, 33, 410);
CT_EQ_INT(t, 33, reprate);
CT_EQ_INT(t, 410, repdelay);
/* The key's own release ends it; another key's release does not. */
repcode = 14;
repdue = 1;
grabkey(nil, nil, 0, 0, 15, WL_KEYBOARD_KEY_STATE_RELEASED);
CT_CHECK(t, repdue != 0);
grabkey(nil, nil, 0, 0, 14, WL_KEYBOARD_KEY_STATE_RELEASED);
CT_EQ_INT(t, 0, repdue);
/* So does a seat that has stopped repeating anything. */
repdue = 1;
grabrepeat(nil, nil, 0, 410);
CT_EQ_INT(t, 0, repdue);
/* A due repeat with no text input to serve is dropped, not sent. */
grabrepeat(nil, nil, 33, 410);
active = 0;
repdue = 1;
repeat();
CT_EQ_INT(t, 0, repdue);
}

Some files were not shown because too many files have changed in this diff Show More