Compare commits

...

204 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
95 changed files with 462467 additions and 10606 deletions

12
.gitignore vendored
View File

@@ -1,15 +1,21 @@
/*.o /*.o
/strans /strans
/imv2.c
/imv2.h
/vkv1.c
/vkv1.h
/tests/*.o /tests/*.o
/tests/unit_test /tests/unit_test
/tests/stress_test
/tests/ibus_live_test /tests/ibus_live_test
/tests/ibus_client_smoke
/tests/gtk_live_test
/tests/xim_live_test
/tests/ipc_live_test /tests/ipc_live_test
/tests/daemon_collision_test /tests/daemon_collision_test
/tests/daemon_failure_test /tests/daemon_failure_test
/tests/daemon_restart_test /tests/daemon_restart_test
/xim/*.o
/xim/strans-xim
/xim/xim_test
/gtk/im-strans.so /gtk/im-strans.so
/bench/bench /bench/bench
/bench/perf.data* /bench/perf.data*
/ref-*/

View File

@@ -1,27 +1,50 @@
FROM archlinux:base@sha256:b0deabeb3d283da2c7f7dbf0eea051b7b2cd0554e0b737cc457fd21683bdcdd1 FROM archlinux:base@sha256:b0deabeb3d283da2c7f7dbf0eea051b7b2cd0554e0b737cc457fd21683bdcdd1
RUN pacman -Syu --noconfirm --needed \ RUN pacman -Syu --noconfirm --needed \
cairo \
fontconfig \
diffutils \ diffutils \
freetype2 \
gcc \ gcc \
gtk3 \ gtk3 \
ibus \
harfbuzz \
libx11 \
libxcb \ libxcb \
libxkbcommon \ libxkbcommon \
make \ make \
noto-fonts-emoji \
pango \
pkgconf \ pkgconf \
plan9port \ plan9port \
python \ python \
ttf-dejavu \ ttf-dejavu \
ttf-jigmo \ ttf-jigmo \
valgrind \
wayland \
which \ which \
xorg-server-xvfb \
xcb-imdkit \ xcb-imdkit \
xcb-util \
dbus \ dbus \
&& pacman -Scc --noconfirm && pacman -Scc --noconfirm
RUN printf "egrep='grep -E'\n" > /usr/lib/plan9/config 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 RUN dbus-uuidgen --ensure=/etc/machine-id
ENV PLAN9=/usr/lib/plan9 ENV PLAN9=/usr/lib/plan9
ENV PATH=$PATH:/usr/lib/plan9/bin 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 WORKDIR /src

View File

@@ -1,12 +1,17 @@
# License inventory # License inventory
- `BSD-3-Clause-libhangul-hanja.txt` contains the terms for the - `BSD-3-Clause-libhangul-hanja.txt` contains the terms for the Hanja data
single-character Hanja data in `map/hanja.src` and generated in `map/hanja.src` and the symbol data in `map/mssymbol.src`, and for the
`map/hanja.dict`, derived from libhangul's `data/hanja/hanja.txt`. 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 - `GPL-2.0-or-later.txt` contains GNU GPL version 2. The header of
`map/kanji.dict` grants the option to use GPL version 2 or any later `map/kanji.dict` grants the option to use GPL version 2 or any later
version. version.
- License texts and provenance for the historically bundled fonts are retained - `Unicode-3.0.txt` contains the Unicode License v3, the terms for the
in `font/`; no font binary remains in the source tree. 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. 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.

108
Makefile
View File

@@ -1,37 +1,65 @@
CC = 9c CC = 9c
LD = 9l LD = 9l
PKG_CFLAGS = $(shell pkg-config --cflags dbus-1 xkbcommon) PKG_CONFIG ?= pkg-config
PKG_LIBS = $(shell pkg-config --libs dbus-1 xkbcommon) SCANNER ?= wayland-scanner
FT_CFLAGS = $(shell pkg-config --cflags freetype2) CFLAGS ?= -O2 -g
FT_LIBS = $(shell pkg-config --libs freetype2) WARN_CFLAGS = -Wall -Wextra
CFLAGS = -Wall -Wextra -O2 -g $(PKG_CFLAGS) IBUS_CFLAGS = $(shell $(PKG_CONFIG) --cflags dbus-1 xkbcommon)
IBUS_LIBS = $(shell $(PKG_CONFIG) --libs dbus-1 xkbcommon)
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)
TEXT_CFLAGS = $(shell $(PKG_CONFIG) --cflags pangocairo cairo fontconfig)
TEXT_LIBS = $(shell $(PKG_CONFIG) --libs pangocairo cairo fontconfig)
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_IMAGE = strans-build
DOCKER_RUN = docker run --rm --user "$$(id -u):$$(id -g)" \ DOCKER_RUN = docker run --rm --user "$$(id -u):$$(id -g)" \
-v "$(CURDIR):/src" $(DOCKER_IMAGE) -v "$(CURDIR):/src" $(DOCKER_IMAGE)
SRCS = dict.c font.c hash.c ibus.c ipc.c ko.c main.c popup_layout.c \ SRCS = compose.c dict.c font.c ibus.c ipc.c ko.c main.c popup_layout.c \
srv.c str.c strans.c trie.c vi.c win.c srv.c str.c strans.c trie.c vi.c win.c 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 gtk all: $(PROG) gtk
$(PROG): $(OBJS) $(PROG): $(OBJS) $(PROTOOBJ)
$(LD) -o $@ $(OBJS) -lthread -lbio -lxcb $(PKG_LIBS) $(FT_LIBS) $(LD) $(LDFLAGS) -o $@ $(OBJS) $(PROTOOBJ) $(PROJECT_LDLIBS) $(LDLIBS)
$(OBJS): dat.h fn.h ipc.h $(OBJS): dat.h fn.h ipc.h
font.o: CFLAGS += $(FT_CFLAGS) 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 gtk/ clean
$(MAKE) -C bench/ clean $(MAKE) -C bench/ clean
xim:
$(MAKE) -C xim/
gtk: gtk:
$(MAKE) -C gtk/ $(MAKE) -C gtk/
@@ -42,31 +70,49 @@ docker-image: Dockerfile
docker build -t $(DOCKER_IMAGE) - < Dockerfile docker build -t $(DOCKER_IMAGE) - < Dockerfile
docker-build: docker-build:
$(DOCKER_RUN) make -B all $(DOCKER_RUN) sh -c 'make clean && make all'
docker-check: docker-check:
$(DOCKER_RUN) make -B check TESTARGS="$(TESTARGS)" $(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-bench:
$(DOCKER_RUN) make -B 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 docker: docker-image
$(MAKE) docker-build $(MAKE) docker-build
check: verify-map test check: verify-map
$(MAKE) -C xim check $(MAKE) -C tests check UNITARGS="$(UNITARGS)"
test: $(PROG) check-live: $(PROG) gtk
$(MAKE) -C tests check TESTARGS="$(TESTARGS)" $(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 map/mkhanja | cmp - map/hanja.dict python3 map/mkhanja | cmp - map/hanja.dict
python3 -B map/verifymap.py map/*.map map/*.dict python3 map/verifymap.py map/*.map map/*.dict
python3 -B tests/mkemoji_test.py python3 tests/mkemoji_test.py
python3 -B tests/mkhanja_test.py python3 tests/mkhanja_test.py
python3 -B tests/skk2ktrans_test.py python3 tests/skk2ktrans_test.py
.PHONY: all check test verify-map clean xim gtk bench docker docker-image \ .PHONY: all check check-live check-stress test verify-map clean gtk bench \
docker-build docker-check docker-bench docker docker-image docker-build docker-check docker-check-live \
docker-check-stress docker-bench docker-valgrind

340
README.md
View File

@@ -1,210 +1,194 @@
# strans # strans
strans is a small, single-user input method for Korean, Japanese, English, strans is a small, single-user input method for Korean, Japanese, English,
and emoji. It provides one engine shared by IBus, XIM, and GTK 3 frontends. emoji and symbols, with Vietnamese Telex as a compatibility mode. One
The existing Vietnamese Telex mode remains available for compatibility, but engine serves four frontends: `zwp_input_method_v2` on Wayland — sway and
is not an actively developed language mode. any other compositor that offers it — IBus, which GTK 4 and Qt use, XIM,
and a GTK 3 module.
## Input behavior ## Modes
English is direct passthrough. Korean uses 2-beolsik composition. Japanese | Key | Mode |
Hiragana keeps the complete kana reading until it is committed, so `kanji` | --- | --- |
forms `かんじ` and offers whole-reading candidates such as `漢字` and `幹事`. | `Ctrl+S` | Korean Hangul (2-beolsik) |
An apostrophe resolves an ambiguous `n` without forwarding the apostrophe: | `Ctrl+N` | Japanese Hiragana, converting to Kanji |
for example, `n'ya` forms `んや`. Katakana is direct Katakana composition | `Ctrl+K` | Japanese Katakana |
and does not perform Kanji dictionary conversion. | `Ctrl+T` | English |
| `Ctrl+V` | Vietnamese Telex |
| `Ctrl+E` | Emoji and symbol search |
| `Ctrl+H` | One-shot Hanja and symbol search |
A Japanese composition is committed by Enter, candidate selection, a real Only a plain `Ctrl` chord is a strans key: with `Shift`, `Alt` or `Super`
input boundary, or an explicit mode change. Backspace first removes pending held it commits what is pending and goes to the application, so
romaji, then accumulated kana. The Japanese modes support doubled consonants `Ctrl+Shift+V` still pastes. `Backspace`, `Enter`, `Tab`, `Esc` and the
and combinations such as `nya`, `nnya`, and `matcha`. arrow and page keys do the same under `Ctrl`, so `Ctrl+Backspace` still
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.
Modes and temporary searches use Ctrl and one letter: ## Composing
```text Japanese composes a whole reading and then offers its Kanji with none
Ctrl+N Japanese Hiragana and Kanji conversion chosen. `Space` and `Tab` step through the candidates and wrap (`Shift`
Ctrl+K Japanese Katakana reverses), the reading in Katakana last, so a word the dictionary lacks
Ctrl+S Korean Hangul converts with one `Space`; `Up`/`Down` and `PageUp`/`PageDown` move without
Ctrl+T English passthrough wrapping. A chosen candidate takes the reading's place in the preedit, so
Ctrl+V Vietnamese Telex compatibility mode it stands where it will land, and `Enter` commits it, or the reading, as
Ctrl+E Emoji and symbol search `0` and typing on always do. Once a candidate is chosen `1`-`9` take that
Ctrl+H One-shot Hanja search row of the page shown; before that the digits type. `Backspace` deletes
Ctrl+P Toggle popup preedit 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.
While candidates are visible, Up and Down move the selection, Enter accepts Korean composes one syllable at a time. `Enter`, `Tab`, `Esc` and any key
the selected candidate, and an unmodified `1`-`9` selects the corresponding that is not a jamo commit the syllable and go on to the application, so
visible row. Tab cycles temporary search results. `0` commits an unconverted `Esc` still leaves insert mode. Two lone consonants join into the compound
language reading. Escape cancels the current composition. Ctrl, Alt, or final they make (rt → ㄳ), and a vowel typed before its consonant is
Super combined with a number does not select a candidate. reordered under it (kr → 가), as in libhangul.
Emoji search is temporary and returns to the previous mode. Queries may use A search takes the keys until `Enter`, `Space` or `1`-`9` picks a result or
English aliases or the active language's input rules, including Japanese `Esc` cancels, then returns to the previous mode. Emoji matches the typed
`egao` becoming `えがお` or `エガオ`: 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 ①②③.
```text Reaching back needs the application to hand over the text around its
Ctrl+E → smile → Enter → 😀 cursor and to take some of it away again. The Wayland input method, IBus
``` and the GTK 3 module all can; XIM has no such request, so there the
reading is what is still being composed and nothing else, and an IBus
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.
Hanja search is temporary and returns to the active language after one Dead keys and Compose sequences are composed by strans itself for the
conversion. Press Ctrl+H and type with that language's normal input rules, Wayland, XIM and IBus frontends, from `XCOMPOSEFILE` or the locale; the
then select with Up, Down, Tab, Enter, or `1`-`9`. The bundled dictionary GTK 3 module leaves them to GtkIMContextSimple.
currently indexes exactly one modern Hangul syllable. Incomplete jamo,
multiple syllables, and other languages therefore have no candidates; Enter ## Preedit and candidates
commits the displayed reading unchanged. Escape cancels the search. It does
not convert words or text already surrounding the cursor. | 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 ## Build and test
The supported build uses Docker so host and container toolchains are not
mixed. Only Docker and Make are needed on the host:
```sh ```sh
git submodule update --init git submodule update --init
make docker-image make docker-image
make docker-build make docker-build
make docker-check
``` ```
`docker-image` creates the Arch Linux dependency image, `docker-build` That leaves `strans`, the daemon, and `gtk/im-strans.so`, the GTK 3 module.
compiles the runtime components, and `docker-check` runs the map verifiers and Tests come in three tiers:
test suites. Docker targets force their own compilation, so newer host
objects cannot be reused. `docker` is a convenience alias for the image and
build steps.
The resulting runtime artifacts are:
```text
strans daemon and IBus frontend
xim/strans-xim XIM frontend
gtk/im-strans.so GTK 3 frontend
```
Use `make docker-check TESTARGS=hangul` to filter the C unit tests. A native
build is still possible with the dependencies and Plan 9 setup recorded in
`Dockerfile`; use `make all`, `make check`, and `make bench` inside that
environment.
After editing an input source, regenerate and verify its dictionary with:
```sh ```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
```sh
./run.sh # restart the daemon in the background
./strans map # or run it in the foreground
```
`./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.
strans shows one popup per session, so it picks a frontend at startup: a
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.
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.
```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
```
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.
## Dictionary data
After changing an input source, regenerate and verify:
```sh
python3 map/mktelex.py >map/telex.map
map/mkemoji >map/emoji.dict map/mkemoji >map/emoji.dict
map/mkhanja >map/hanja.dict map/mkhanja >map/hanja.dict
make verify-map make verify-map
``` ```
Hanja import and Japanese dictionary regeneration are documented in [`map/README`](map/README) says where the emoji, Hanja and Japanese data
[`map/README`](map/README). The converter output is deterministic UTF-8, comes from.
tab-separated data consumable by the runtime dictionary reader.
## Run and install ## Benchmark
Install one or more scalable outline fonts, then pass their files in fallback `make bench` builds `bench/bench`. With the daemon stopped, `./bench.sh`
order after the map directory. The daemon accepts at most four font files and starts one, warms it up and records the workload with Linux `perf` into
does not search the system font database. For example, the Docker-tested Arch `bench/perf.data`.
packages and paths are:
```sh ## Licensing
doas pacman -S ttf-dejavu ttf-jigmo
./strans map \
/usr/share/fonts/TTF/DejaVuSans.ttf \
/usr/share/fonts/TTF/Jigmo.ttf \
/usr/share/fonts/TTF/Jigmo2.ttf &
```
These fonts are test fixtures, not runtime requirements. Any suitable Third-party notices are in [`LICENSES`](LICENSES). The repository declares
scalable outline files may be supplied; their argument order is the glyph no license for the strans source as a whole.
fallback order. A collection file uses its first face. If any named file is
absent or invalid, the popup disables itself cleanly while input processing
continues. `run.sh` and `bench.sh` accept the same font-file arguments.
When `run.sh` is invoked without arguments, it uses the installed DejaVu,
Jigmo, or Noto CJK font files from the standard Arch Linux paths. Explicit
font-file arguments replace these defaults:
```sh
./run.sh
./run.sh /path/to/primary.ttf /path/to/fallback.ttc
```
`run.sh` remains in the foreground and owns only the daemon and XIM process
that it starts. It waits for that daemon's IPC socket and IBus address file
before reporting success. With `DISPLAY` set it then starts XIM; without a
display it keeps the headless daemon in the foreground and does not start
XIM. A signal or XIM exit stops and reaps the launcher's daemon. On repeated
invocation, it stops a previous live `run.sh` only after confirming that the
old launcher directly owns the daemon serving the same IPC and IBus
endpoints and that the daemon carries that launcher's PID and process-start
identity. A manually started or orphaned daemon is left untouched and
reported as a visible startup failure.
The IPC socket is placed at `$XDG_RUNTIME_DIR/strans.sock` when
`XDG_RUNTIME_DIR` is an absolute path. Otherwise strans uses the per-user
fallback `/tmp/strans.<uid>`. A second daemon will not replace a live
daemon's socket.
For XIM applications:
```sh
./xim/strans-xim &
export XMODIFIERS=@im=strans
xterm
```
For GTK 3 applications, install the module built by Docker. Installation
needs the GTK 3 runtime, but not its development package:
```sh
doas make -C gtk install
```
`LIBDIR` defaults to `/usr/lib` and may be overridden for another system.
Normal `DESTDIR` semantics are supported, and a staged install does not update
the host GTK module cache:
```sh
make -C gtk install DESTDIR="$pkgdir"
```
For IBus clients such as kitty:
```sh
GLFW_IM_MODULE=ibus kitty
```
strans provides its own IBus endpoint; neither ibus-daemon nor fcitx is
required. Starting without `DISPLAY` is supported: input processing and IBus
continue to run while the optional X popup is disabled.
## Ownership and rendering constraints
There is one global composition, not one independent engine per frontend
context. The first meaningful key from a new context resets the previous
composition and transfers ownership. Reset, focus loss, context destruction,
and disconnect affect the engine only when they come from its active owner;
stale events cannot clear a newer owner's preedit.
The popup is an optional X window. It uses the explicit font-file order and
fixed-width rune cells; U+FE0E and U+FE0F variation selectors use zero cells.
It does not perform OpenType shaping, color-emoji rendering, or general
grapheme clustering. Multi-rune emoji are committed intact but are drawn as
their constituent rune cells under this boundary. A supplied caret rectangle
is used when available, with the pointer as fallback. The X renderer accepts
only a 24-depth, 32-bpp TrueColor root format and disables itself cleanly on
other visuals.
## Data, third-party code, and licensing
Data and third-party provenance is recorded in
[`docs/PROVENANCE.md`](docs/PROVENANCE.md), with font-specific details in
[`font/PROVENANCE`](font/PROVENANCE). No font binaries are bundled; that file
retains the provenance of the faces formerly kept in the repository. The
historical SKK-derived Kanji
dictionary is distributed under GPL version 2 or later; its license text is
in [`LICENSES/GPL-2.0-or-later.txt`](LICENSES/GPL-2.0-or-later.txt).
The single-character Hanja data derived from libhangul is distributed under
the BSD 3-Clause license in
[`LICENSES/BSD-3-Clause-libhangul-hanja.txt`](LICENSES/BSD-3-Clause-libhangul-hanja.txt).
The repository does not currently declare a license for the strans project
source as a whole. The licenses of bundled data, historical font assets, and
third-party components do not by themselves license the original strans
source.

View File

@@ -4,18 +4,13 @@ set -eu
cd "$(dirname "$0")" || exit 1 cd "$(dirname "$0")" || exit 1
case ${XDG_RUNTIME_DIR-} in if test "$#" -ne 0; then
/) ipc=/strans.sock ;; echo "usage: bench.sh" >&2
/*/) ipc=${XDG_RUNTIME_DIR}strans.sock ;; exit 1
/*) ipc=${XDG_RUNTIME_DIR}/strans.sock ;; fi
*) ipc=/tmp/strans.$(id -u) ;;
esac if ! test -x ./bench/bench; then
if test -n "${XDG_CONFIG_HOME-}"; then echo "bench.sh: ./bench/bench is not executable; run make bench first" >&2
busdir=$XDG_CONFIG_HOME/ibus/bus
elif test "${HOME+x}" = x; then
busdir=$HOME/.config/ibus/bus
else
echo "bench.sh: HOME or XDG_CONFIG_HOME is required for IBus discovery" >&2
exit 1 exit 1
fi fi
@@ -29,70 +24,36 @@ cleanup()
if test -n "$perf_pid"; then if test -n "$perf_pid"; then
kill -INT "$perf_pid" 2>/dev/null || : kill -INT "$perf_pid" 2>/dev/null || :
wait "$perf_pid" 2>/dev/null || : wait "$perf_pid" 2>/dev/null || :
perf_pid=
fi fi
if test -n "$strans_pid"; then if test -n "$strans_pid"; then
kill "$strans_pid" 2>/dev/null || : kill "$strans_pid" 2>/dev/null || :
wait "$strans_pid" 2>/dev/null || : wait "$strans_pid" 2>/dev/null || :
strans_pid=
fi fi
exit "$status" exit "$status"
} }
ibusready()
{
for address in "$busdir"/*; do
test -f "$address" || continue
foundpid=
foundaddress=
while IFS= read -r line; do
case $line in
IBUS_DAEMON_PID="$strans_pid") foundpid=1 ;;
IBUS_ADDRESS=unix:abstract=strans-"$strans_pid",*) foundaddress=1 ;;
esac
done <"$address"
if test -n "$foundpid" && test -n "$foundaddress"; then
return 0
fi
done
return 1
}
trap cleanup 0 trap cleanup 0
trap 'exit 129' 1 trap 'exit 129' 1
trap 'exit 130' 2 trap 'exit 130' 2
trap 'exit 143' 15 trap 'exit 143' 15
./strans map "$@" & ./strans map &
strans_pid=$! strans_pid=$!
# The daemon is ready once the benchmark client can complete a round.
attempt=0 attempt=0
while test "$attempt" -lt 10; do until ./bench/bench bench/bench.keys 1 >/dev/null 2>&1; do
if test -S "$ipc" && ibusready; then
break
fi
if ! kill -0 "$strans_pid" 2>/dev/null; then if ! kill -0 "$strans_pid" 2>/dev/null; then
if wait "$strans_pid"; then echo "bench.sh: daemon exited before accepting IPC clients" >&2
status=0 exit 1
else
status=$?
fi
strans_pid=
if test "$status" -eq 0; then
status=1
fi
echo "bench.sh: daemon exited before publishing IPC and IBus endpoints" >&2
exit "$status"
fi fi
attempt=$((attempt + 1)) attempt=$((attempt + 1))
if test "$attempt" -lt 10; then if test "$attempt" -ge 50; then
sleep 1 echo "bench.sh: daemon did not accept IPC clients within 10 seconds" >&2
exit 1
fi fi
sleep 0.2
done done
if test "$attempt" -eq 10; then
echo "bench.sh: daemon did not publish IPC and IBus endpoints within 10 seconds" >&2
exit 1
fi
# warm up the renderer # warm up the renderer
./bench/bench bench/bench.keys 100 ./bench/bench bench/bench.keys 100

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

@@ -7,7 +7,7 @@
#include <string.h> #include <string.h>
#include <unistd.h> #include <unistd.h>
#include <time.h> #include <time.h>
#include "../ipc.h" #include "ipc.h"
typedef struct Key Key; typedef struct Key Key;
struct Key { struct Key {
@@ -99,7 +99,7 @@ iterations(char *s)
goto Bad; goto Bad;
errno = 0; errno = 0;
n = strtoumax(s, &end, 10); n = strtoumax(s, &end, 10);
if(errno != 0 || *end != '\0' || n == 0 || n > UINT64_MAX) if(errno != 0 || *end != '\0' || n == 0)
goto Bad; goto Bad;
return n; return n;
Bad: Bad:
@@ -128,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
@@ -157,7 +157,7 @@ main(int argc, char **argv)
loadkeys(argv[1]); loadkeys(argv[1]);
niter = argc == 3 ? iterations(argv[2]) : 1000; niter = argc == 3 ? iterations(argv[2]) : 1000;
if(nkeys > UINT64_MAX || niter > UINT64_MAX / nkeys){ if(niter > UINT64_MAX / nkeys){
fprintf(stderr, "iteration count overflows key total\n"); fprintf(stderr, "iteration count overflows key total\n");
exit(1); exit(1);
} }

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;
}

145
dat.h
View File

@@ -7,6 +7,14 @@
#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,
@@ -17,8 +25,6 @@ enum
LangEMOJI = 0x05, LangEMOJI = 0x05,
LangVI = 0x16, LangVI = 0x16,
Fontsz = 32,
Maxfonts = 4,
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,9 +125,8 @@ 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
@@ -119,6 +142,15 @@ struct Im
typedef struct Drawcmd Drawcmd; typedef struct Drawcmd Drawcmd;
typedef struct Caret Caret; typedef struct Caret Caret;
typedef struct Area Area;
struct Area
{
int x;
int y;
int w;
int h;
};
struct Caret struct Caret
{ {
int valid; int valid;
@@ -133,9 +165,35 @@ struct Drawcmd
Str kouho[Maxdisp]; Str kouho[Maxdisp];
int nkouho; int nkouho;
int sel; int sel;
int first;
int total;
Caret caret; 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 enum
@@ -144,10 +202,12 @@ enum
Keyreset, Keyreset,
Keyrelease, Keyrelease,
Keycaret, 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;
}; };
@@ -155,33 +215,16 @@ struct Keyres
struct Keyreq struct Keyreq
{ {
void *owner; /* stable until the context's release is acknowledged */ void *owner; /* stable until the context's release is acknowledged */
int clientpre; /* the client draws the preedit; the popup does not */
int op; int op;
u32int ks; u32int ks;
u32int mod; u32int mod;
Caret caret; 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;

156
dict.c
View File

@@ -1,122 +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(req->lang == LangEMOJI || scmp(&tmp, &req->key) != 0) out[n++] = tmp;
res->kouho[res->nkouho++] = tmp;
if(p < e)
p++;
} }
return n;
} }
void /* The entries at and below a node, the shortest keys first. */
dictthread(void*) static int
below(Trie *t, int ni, Str *out, int n, int max)
{ {
Dictreq req; Tnode *nd;
static Dictres res; int ci;
threadsetname("dict"); nd = &t->nodes[ni];
for(;;){ n = words(nd, out, n, max);
if(chanrecv(dictreqc, &req) < 0) for(ci = nd->child; ci >= 0 && n < max; ci = t->nodes[ci].sibling)
break; n = below(t, ci, out, n, max);
while(channbrecv(dictreqc, &req) > 0) return n;
;
dictlookup(&req, &res);
chansend(dictresc, &res);
}
} }
static Hmap* /* Fills out[] with up to max candidates for key: the words of its entry. */
dictopen(char *path) int
dictlookup(Trie *t, Str *key, Str *out, int max)
{ {
Hmap *h; int ni;
Biobuf *b;
Str key;
char *line, *tab, *p, *e;
int len, lineno;
b = Bopen(path, OREAD); ni = key->n == 0 ? -1 : trienode(t, key);
if(b == nil) if(ni < 0)
die("can't open: %s", path); return 0;
h = hmapalloc(4096); return words(&t->nodes[ni], out, 0, max);
lineno = 0;
while((line = Brdstr(b, '\n', 1)) != nil){
lineno++;
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 || tab >= line + len - 1 ||
strchr(tab+1, '\t') != nil)
die("malformed dictionary: %s:%d", path, lineno);
*tab = '\0';
if(utflen(line) > Maxrunes)
die("dictionary key too long: %s:%d", path, lineno);
for(p = tab+1; p < line+len; p = e+1){
e = memchr(p, ' ', line+len-p);
if(e == nil)
e = line+len;
if(utfnlen(p, e-p) > Maxrunes)
die("dictionary candidate too long: %s:%d", path, lineno);
if(e == line+len)
break;
}
sinit(&key, line, tab - line);
hmapset(&h, &key, tab+1, len - (tab - line) - 1);
free(line);
}
Bterm(b);
return h;
} }
void /* As dictlookup, for every entry key is a prefix of, key's own first. */
dictinit(char *dir) 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; char *path;
int i; Trie *t;
for(i = 0; i < nlang; i++){ path = smprint("%s/%s.%s", dir, name, ext);
if(langs[i].dictname == nil) if(path == nil)
continue; die("out of memory");
path = smprint("%s/%s.dict", dir, langs[i].dictname); t = trieopen(path);
if(path == nil) free(path);
die("out of memory"); return t;
langs[i].dict = dictopen(path); }
free(path);
void
langinit(char *dir)
{
Lang *l;
for(l = langs; l < langs + nlang; l++){
if(l->mapname != nil)
l->map = langopen(dir, l->mapname, "map");
if(l->dictname != nil)
l->dict = langopen(dir, l->dictname, "dict");
} }
} }

View File

@@ -1,79 +0,0 @@
# Data and third-party provenance
This file records what can be established from repository contents and
history. It does not assign a license to original strans source code.
## Input maps and dictionaries
`map/hira.map`, `map/kata.map`, and the original map data entered the history
in commit `cc157c0b3c33506d1cbf5bd251ddc6a9ca909081`, whose subject says the
maps came from 9front. That commit does not identify a 9front revision or
carry a separate data license. `map/telex.map` is generated by
`map/mktelex.py`; Vietnamese behavior is retained for compatibility.
`map/emoji.dict` is generated deterministically from the repository-maintained
`map/emoji.src` by `map/mkemoji`. No external dataset is required.
`map/hanja.src` is a single-character subset of libhangul's
`data/hanja/hanja.txt` from release tag `libhangul-0.2.0`. Its immutable
identifiers are:
- annotated tag object: `20afc38922e3595ee3ed5b186f2ea05afe663763`
- peeled commit: `41c702f5d3581325b646ef6249f1f641b0427ae0`
- input blob: `199cfd70c4b306257ac2e018714a1285f7cf0ed3`
- input SHA-256: `dd44dcc856cf542b1022d0f39c2e9b9f8805fdcc5923be80f04849ed97ce0996`
`map/libhangul2hanja` keeps only a one-syllable modern Hangul reading paired
with one BMP Hanja rune. It writes one
`Hanja<TAB>Hangul syllable` pair per row in upstream order; word entries are
not part of this data. `map/mkhanja` groups those rows by reading for the
runtime dictionary without changing candidate order, retaining the first 32
candidates supported by the engine. Exact import and regeneration commands
are in `map/README`.
The upstream data header is BSD 3-Clause licensed, copyright 2005, 2006 Choe
Hwanjin. Its complete license text is in
`LICENSES/BSD-3-Clause-libhangul-hanja.txt`.
`map/kanji.dict` identifies itself as SKK Medium dictionary version 8.1,
May 24, 1995, rearranged for Plan 9 ktrans by Kenji Okamoto on February 17,
2000. Its header names its copyright holders and grants GPL version 2 or
later. The complete GPL version 2 text is in
`LICENSES/GPL-2.0-or-later.txt`; the dictionary header supplies the
"or later" option. `map/README` records the exact repository history,
repairs made to this copy, and the reproducible procedure for importing a
new SKK revision.
## Fonts
No font binaries are bundled. The daemon receives one to four font file paths
on its command line and tries them in that order; it does not discover fonts by
family name or directory contents. `font/PROVENANCE` retains the exact
checksums, source revisions, and license records for the three Noto faces that
were formerly stored in the repository.
The Docker renderer tests obtain their fonts from signed Arch packages rather
than the source checkout. On August 13, 2026 the tested package contract was:
- `ttf-dejavu` `2.37+18+g9b5d1b2f-8`, providing
`/usr/share/fonts/TTF/DejaVuSans.ttf` (SHA-256
`6038a160b491e121c1f12c7bccb4a9c8730296e3adc1086a059404ed84b7451c`);
- `ttf-jigmo` `20250912-1`, providing
`/usr/share/fonts/TTF/Jigmo.ttf` (SHA-256
`c8f295b9bd8f9f117a76b3a454aaaa1bb5b4babc18e254978c6deb88464e40cf`)
and `/usr/share/fonts/TTF/Jigmo2.ttf` (SHA-256
`5da3582efe77e22073b86b3b86b556d7111148a76b957cfb53318a91da2efff0`).
Pacman validates the package checksums and repository signatures. DejaVu Sans
supplies ASCII and the monochrome U+1F600 outline in the tests; Jigmo supplies
Hangul and BMP CJK, and Jigmo2 supplies U+2000B. Their installed license files
are `/usr/share/licenses/ttf-dejavu/LICENSE` and
`/usr/share/licenses/ttf-jigmo/LICENSE.txt`; the packages identify their terms
as the DejaVu custom license and CC0 1.0, respectively. These packages are
test fixtures, not names or paths hard-coded into the daemon.
## Git submodules
`cutest` is pinned by the repository as the unit-test framework. XIM uses
the system xcb-imdkit and xcb-util libraries at build time; those system
libraries are not bundled by this repository.

43
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,28 +10,30 @@ 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*);
int popupcells(Rune*, int); void popuparea(Area*, int, Area*, int, int, Area*);
void popupposition(Caret*, int, int, int, int, int, int, int*, int*); 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);
@@ -40,9 +43,19 @@ void backvi(Im*);
void srvinit(void); void srvinit(void);
void srvthread(void*); void srvthread(void*);
void ibusthread(void*); void ibusthread(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);
int fontinit(char**, int); 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*);

280
font.c
View File

@@ -1,168 +1,142 @@
#include "dat.h" #include "dat.h"
#include <ft2build.h> #include <fontconfig/fontconfig.h>
#include FT_FREETYPE_H #include <pango/pangocairo.h>
#include "fn.h" #include "fn.h"
typedef struct Font Font; static PangoFontMap *fontmap;
struct Font static PangoContext *context;
static PangoLayout *layout;
void
textclose(void)
{ {
FT_Face face; if(layout != nil)
int base; g_object_unref(layout);
}; if(context != nil)
g_object_unref(context);
static FT_Library lib; if(fontmap != nil)
static Font fonts[Maxfonts]; g_object_unref(fontmap);
static int nfonts; layout = nil;
static u32int blendtab[2][256]; context = nil;
fontmap = nil;
static u32int
blend(u32int bg, u32int fg, int a)
{
int r, g, b, inv;
inv = 255 - a;
r = ((bg >> 16 & 0xff) * inv + (fg >> 16 & 0xff) * a) / 255;
g = ((bg >> 8 & 0xff) * inv + (fg >> 8 & 0xff) * a) / 255;
b = ((bg & 0xff) * inv + (fg & 0xff) * a) / 255;
return (r << 16) | (g << 8) | b;
}
static int
loadfont(char *path)
{
FT_Face face;
Font *f;
vlong height, pixels;
if(nfonts >= nelem(fonts))
return 0;
if(FT_New_Face(lib, path, 0, &face) != 0){
fprint(2, "strans: popup: can't load font: %s\n", path);
return 0;
}
height = (vlong)face->ascender - face->descender;
if(height <= 0 || face->units_per_EM == 0){
fprint(2, "strans: popup: invalid font metrics: %s\n", path);
FT_Done_Face(face);
return 0;
}
pixels = (vlong)Fontsz * face->units_per_EM / height;
if(pixels < 1)
pixels = 1;
if(FT_Set_Pixel_Sizes(face, 0, pixels) != 0){
fprint(2, "strans: popup: can't size font: %s\n", path);
FT_Done_Face(face);
return 0;
}
f = &fonts[nfonts];
f->face = face;
f->base = (vlong)Fontsz * face->ascender / height;
nfonts++;
return 1;
} }
static void static void
clearfonts(void) setfont(void)
{ {
int i; PangoFontDescription *font;
PangoRectangle r;
int size;
for(i = 0; i < nfonts; i++){ font = pango_font_description_new();
FT_Done_Face(fonts[i].face); pango_font_description_set_family(font, "sans");
fonts[i].face = nil; pango_font_description_set_absolute_size(font, Fontsz * PANGO_SCALE);
pango_layout_set_font_description(layout, font);
pango_layout_set_text(layout, "Mg", -1);
pango_layout_get_pixel_extents(layout, nil, &r);
if(r.height > 0){
/* Fontsz is the popup row height, not a point size. */
size = Fontsz * PANGO_SCALE * Fontsz / r.height;
pango_font_description_set_absolute_size(font, max(size, PANGO_SCALE));
pango_layout_set_font_description(layout, font);
} }
nfonts = 0; pango_font_description_free(font);
}
int
fontinit(char **path, int npath)
{
int i, a;
if(path == nil || npath < 1 || npath > nelem(fonts)){
fprint(2, "strans: popup: need 1-%d font files\n", nelem(fonts));
clearfonts();
return 0;
}
if(lib == nil && FT_Init_FreeType(&lib) != 0){
fprint(2, "strans: popup: can't initialize FreeType\n");
return 0;
}
clearfonts();
for(i = 0; i < npath; i++){
if(path[i] == nil || !loadfont(path[i])){
clearfonts();
return 0;
}
}
for(a = 0; a < 256; a++){
blendtab[0][a] = blend(Colbg, Colfg, a);
blendtab[1][a] = blend(Colsel, Colfg, a);
}
return 1;
}
static int
drawglyph(Font *f, u32int *buf, int w, int h, int px, int py)
{
FT_Bitmap *b;
FT_GlyphSlot g;
uchar *row;
vlong pitch, x0, x1, y0, y1;
int a, sel, x, xa, xb, y, ya, yb;
u32int *p;
g = f->face->glyph;
b = &g->bitmap;
if(b->pixel_mode != FT_PIXEL_MODE_GRAY || b->num_grays != 256)
return 0;
if(b->width == 0 || b->rows == 0)
return 1;
pitch = b->pitch;
if(pitch < 0)
pitch = -pitch;
if(b->buffer == nil || pitch < b->width)
return 0;
x0 = (vlong)px + g->bitmap_left;
y0 = (vlong)py + f->base - g->bitmap_top;
x1 = x0 + b->width;
y1 = y0 + b->rows;
if(x1 <= 0 || y1 <= 0 || x0 >= w || y0 >= h)
return 1;
xa = x0 < 0 ? 0 : x0;
xb = x1 > w ? w : x1;
ya = y0 < 0 ? 0 : y0;
yb = y1 > h ? h : y1;
for(y = ya; y < yb; y++){
if(b->pitch < 0)
row = b->buffer + ((vlong)b->rows - 1 - (y - y0)) * pitch;
else
row = b->buffer + ((vlong)y - y0) * pitch;
for(x = xa; x < xb; x++){
a = row[(vlong)x - x0];
if(a != 0){
p = &buf[y * w + x];
sel = *p == Colsel;
*p = blendtab[sel][a];
}
}
}
return 1;
} }
void void
putfont(u32int *buf, int w, int h, int px, int py, Rune r) textinit(void)
{ {
FT_UInt g; FcInit();
int f; fontmap = pango_cairo_font_map_new();
pango_cairo_font_map_set_resolution(PANGO_CAIRO_FONT_MAP(fontmap), 96);
if(buf == nil || w <= 0 || h <= 0) context = pango_font_map_create_context(fontmap);
return; layout = pango_layout_new(context);
for(f = 0; f < nfonts; f++){ pango_layout_set_single_paragraph_mode(layout, TRUE);
g = FT_Get_Char_Index(fonts[f].face, r); setfont();
if(g == 0 || FT_Load_Glyph(fonts[f].face, g, FT_LOAD_DEFAULT) != 0 || }
FT_Render_Glyph(fonts[f].face->glyph, FT_RENDER_MODE_NORMAL) != 0)
continue; static int
if(drawglyph(&fonts[f], buf, w, h, px, py)) settext(Str *s)
return; {
} char utf[Maxutf];
int n;
n = stoutf(s, utf, sizeof utf);
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;
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
textdraw(u32int *buf, int w, int h, int x, int y, int fit, u32int color,
Str *s)
{
PangoRectangle r;
cairo_surface_t *surface;
cairo_t *cr;
int b, g, red;
if(fit <= 0)
return;
pango_layout_set_width(layout, fit * PANGO_SCALE);
pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END);
if(w <= 0 || h <= 0 || !settext(s))
return;
surface = cairo_image_surface_create_for_data((uchar*)buf,
CAIRO_FORMAT_RGB24, w, h, w * sizeof buf[0]);
if(cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS){
cairo_surface_destroy(surface);
return;
}
cairo_surface_mark_dirty(surface);
cr = cairo_create(surface);
if(cairo_status(cr) == CAIRO_STATUS_SUCCESS){
pango_cairo_update_layout(cr, layout);
textextents(&r);
cairo_rectangle(cr, x, y, fit, Fontsz);
cairo_clip(cr);
cairo_move_to(cr, x - r.x, y + (Fontsz - r.height) / 2 - r.y);
red = color >> 16 & 0xff;
g = color >> 8 & 0xff;
b = color & 0xff;
cairo_set_source_rgb(cr, red / 255.0, g / 255.0, b / 255.0);
pango_cairo_show_layout(cr, layout);
}
cairo_destroy(cr);
cairo_surface_flush(surface);
cairo_surface_destroy(surface);
} }

View File

@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1,94 +0,0 @@
Copyright 2018 The Noto Project Authors (github.com/googlei18n/noto-fonts)
This Font Software is licensed under the SIL Open Font License,
Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to
provide a free and open framework in which fonts may be shared and
improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software
components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to,
deleting, or substituting -- in part or in whole -- any of the
components of the Original Version, by changing formats or by porting
the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed,
modify, redistribute, and sell modified and unmodified copies of the
Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created using
the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -1,92 +0,0 @@
This Font Software is licensed under the SIL Open Font License,
Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to
provide a free and open framework in which fonts may be shared and
improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software
components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to,
deleting, or substituting -- in part or in whole -- any of the
components of the Original Version, by changing formats or by porting
the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed,
modify, redistribute, and sell modified and unmodified copies of the
Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created using
the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -1,49 +0,0 @@
Historical popup fonts
======================
These binaries are no longer stored in the strans source tree. Before font
selection became an ordered list of command-line file paths, the renderer used
these repository files in this fixed order:
1. NotoSans-Regular.ttf
2. NotoSansMonoCJKjp-Regular.otf
3. NotoEmoji-Regular.ttf
NotoSans-Regular.ttf
--------------------
SHA-256: 9cb49a54e520423033f9727be2e53e4805a60656deb09c219740d8e5f3e033ac
Internal family/version: Noto Sans Regular, version 2.005
License: SIL Open Font License 1.1 (OFL-NotoSans.txt)
Source revision: 7697007fcb3563290d73f41f56a70d5d559d828c
Source: https://raw.githubusercontent.com/notofonts/noto-fonts/7697007fcb3563290d73f41f56a70d5d559d828c/hinted/ttf/NotoSans/NotoSans-Regular.ttf
This file entered the strans repository in commit 874b941. Its source URL was
not recorded then; the immutable revision above was recovered later and is
byte-identical to the historical file.
NotoSansMonoCJKjp-Regular.otf
--------------------------------
SHA-256: 4d01725be822d144cf9a56ade981e6fb920cd7a610b8fc24cc601a920beea5b9
Release: Noto Sans CJK 2.004, tag Sans2.004
Source revision: 523d033d6cb47f4a80c58a35753646f5c3608a78
Source: https://raw.githubusercontent.com/notofonts/noto-cjk/523d033d6cb47f4a80c58a35753646f5c3608a78/Sans/Mono/NotoSansMonoCJKjp-Regular.otf
License: SIL Open Font License 1.1 (OFL-NotoSansCJK.txt)
License source: https://raw.githubusercontent.com/notofonts/noto-cjk/523d033d6cb47f4a80c58a35753646f5c3608a78/LICENSE
NotoEmoji-Regular.ttf
---------------------
SHA-256: 415dc6290378574135b64c808dc640c1df7531973290c4970c51fdeb849cb0c5
Internal family/version: Noto Emoji Regular, version 1.05
Source revision: 2f1ffdd6fbbd05d6f382138a3d3adcd89c5ce800
Source: https://raw.githubusercontent.com/googlei18n/noto-emoji/2f1ffdd6fbbd05d6f382138a3d3adcd89c5ce800/fonts/NotoEmoji-Regular.ttf
Font license: SIL Open Font License 1.1
Font license source: https://raw.githubusercontent.com/googlei18n/noto-emoji/2f1ffdd6fbbd05d6f382138a3d3adcd89c5ce800/fonts/LICENSE
Font license SHA-256: 6a73f9541c2de74158c0e7cf6b0a58ef774f5a780bf191f2d7ec9cc53efe2bf2
The font-specific license is byte-identical to `OFL-NotoSansCJK.txt`. The
previously recorded `Apache-2.0-NotoEmoji.txt` is retained as the license of
the upstream project at that revision; it is not the license declared by this
font binary.

View File

@@ -1,24 +1,41 @@
PROG = im-strans.so PROG = im-strans.so
CFLAGS = -Wall -O2 -I.. $(shell pkg-config --cflags gtk+-3.0) PKG_CONFIG ?= pkg-config
LDFLAGS = $(shell pkg-config --libs gtk+-3.0) CFLAGS ?= -O2
LIBDIR ?= /usr/lib GTK_CFLAGS = $(shell $(PKG_CONFIG) --cflags gtk+-3.0)
GTK_MODULE_DIR = $(LIBDIR)/gtk-3.0/3.0.0/immodules 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)))
all: $(PROG) all: $(PROG)
$(PROG): main.c ../ipc.c ../ipc.h $(PROG): main.c ../ipc.c ../ipc.h
$(CC) -shared -fPIC $(CFLAGS) -o $@ main.c ../ipc.c $(LDFLAGS) $(CC) $(CPPFLAGS) -I.. $(GTK_CFLAGS) -Wall $(CFLAGS) $(LDFLAGS) \
-shared -fPIC -o $@ main.c ../ipc.c $(GTK_LIBS) $(LDLIBS)
CHECKDIR = test -n "$$module_dir" || { \
echo "cannot find GTK 3 module directory; set GTK_MODULE_DIR" >&2; \
exit 1; }
REFRESH = test -n "$(DESTDIR)" || { \
unset GTK_PATH GTK_IM_MODULE_FILE; $(GTK_QUERY_IMMODULES) --update-cache; }
install: $(PROG) install: $(PROG)
test -n "$(LIBDIR)" module_dir="$(GTK_MODULE_DIR)"; $(CHECKDIR); \
mkdir -p "$(DESTDIR)$(GTK_MODULE_DIR)" mkdir -p "$(DESTDIR)$$module_dir" && \
cp $(PROG) "$(DESTDIR)$(GTK_MODULE_DIR)/" cp "$(PROG)" "$(DESTDIR)$$module_dir/" && $(REFRESH)
if test -z "$(DESTDIR)"; then gtk-query-immodules-3.0 --update-cache; fi
uninstall: uninstall:
test -n "$(LIBDIR)" module_dir="$(GTK_MODULE_DIR)"; $(CHECKDIR); \
rm -f "$(DESTDIR)$(GTK_MODULE_DIR)/$(PROG)" rm -f "$(DESTDIR)$$module_dir/$(PROG)" && $(REFRESH)
if test -z "$(DESTDIR)"; then gtk-query-immodules-3.0 --update-cache; fi
clean: clean:
rm -f $(PROG) rm -f $(PROG)

View File

@@ -1,132 +1,314 @@
#include <stdio.h> #include <errno.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include <unistd.h> #include <unistd.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)
{
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; close(im->fd);
im->fd = ipcconnect(); 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;
} }
ipcpackreset(buf, 1); 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
@@ -134,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
@@ -179,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){
@@ -197,6 +398,9 @@ 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);
} }
@@ -206,28 +410,136 @@ focusout(GtkIMContext *ctx)
Im *im; Im *im;
im = (Im*)ctx; im = (Im*)ctx;
if(parentim->focus_out != NULL)
parentim->focus_out(ctx);
im->simpleactive = 0;
/* Closing the connection releases the engine. */
sendreset(im); sendreset(im);
/* Closing the context connection releases engine ownership. */
srvclose(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
@@ -238,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 = {
@@ -267,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

168
hash.c
View File

@@ -1,168 +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)
return nil;
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;
if(h == nil || key == nil || key->n < 0 || key->n > Maxrunes)
return nil;
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, int *len)
{
char buf[Maxutf];
char *p;
int n;
n = stoutf(s, buf, sizeof(buf));
p = emalloc(n + 1);
memmove(p, buf, n);
p[n] = '\0';
*len = n;
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;
if(store == nil || *store == nil || key == nil ||
key->n < 0 || key->n > Maxrunes || vlen < 0 ||
(vlen > 0 && val == nil))
return;
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);
n->filled = 1;
}
n->next = next;
free(n->val);
n->val = newval;
n->vlen = vlen;
}

1121
ibus.c

File diff suppressed because it is too large Load Diff

347
ipc.c
View File

@@ -1,12 +1,87 @@
#define _POSIX_C_SOURCE 200809L
#include <errno.h> #include <errno.h>
#include <poll.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.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 <sys/un.h>
#include "ipc.h" #include "ipc.h"
static int64_t
nowms(void)
{
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)
{ {
@@ -20,14 +95,33 @@ getlen(const unsigned char p[Ipclensz])
return p[0] | (p[1] << 8); return p[0] | (p[1] << 8);
} }
static void
put32(unsigned char *p, int32_t v)
{
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 int
ipcpath(char *dst, size_t cap) ipcpath(char *dst, size_t cap)
{ {
const char *dir, *sep; const char *dir, *sep;
int n; int n;
if(dst == NULL || cap == 0)
return -1;
dir = getenv("XDG_RUNTIME_DIR"); dir = getenv("XDG_RUNTIME_DIR");
if(dir != NULL && dir[0] == '/'){ if(dir != NULL && dir[0] == '/'){
sep = dir[strlen(dir)-1] == '/' ? "" : "/"; sep = dir[strlen(dir)-1] == '/' ? "" : "/";
@@ -40,6 +134,11 @@ ipcpath(char *dst, size_t cap)
return 0; 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 int
ipcconnect(void) ipcconnect(void)
{ {
@@ -52,15 +151,60 @@ ipcconnect(void)
errno = ENAMETOOLONG; errno = ENAMETOOLONG;
return -1; return -1;
} }
fd = socket(AF_UNIX, SOCK_STREAM, 0); fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
if(fd < 0) if(fd < 0)
return -1; return -1;
if(connect(fd, (struct sockaddr*)&addr, sizeof addr) == 0) if(connect(fd, (struct sockaddr*)&addr, sizeof addr) < 0){
return fd; e = errno;
e = errno; close(fd);
close(fd); errno = e;
errno = e; return -1;
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 void
@@ -82,6 +226,48 @@ ipcpackreset(unsigned char req[Ipcreqsz], int want)
req[0] = Ipcreqreset | (want ? Ipcreqwant : 0); 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 void
ipcunpackreq(const unsigned char req[Ipcreqsz], int *want, uint32_t *mod, ipcunpackreq(const unsigned char req[Ipcreqsz], int *want, uint32_t *mod,
uint32_t *key) uint32_t *key)
@@ -94,6 +280,45 @@ ipcunpackreq(const unsigned char req[Ipcreqsz], int *want, uint32_t *mod,
((uint32_t)req[5] << 24); ((uint32_t)req[5] << 24);
} }
int
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 int
ipcreqreset(const unsigned char req[Ipcreqsz]) ipcreqreset(const unsigned char req[Ipcreqsz])
{ {
@@ -101,29 +326,25 @@ ipcreqreset(const unsigned char req[Ipcreqsz])
} }
int int
ipcpackresp(unsigned char *dst, size_t cap, int eaten, 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;
} }
@@ -152,73 +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;
int64_t until;
p = buf; p = buf;
until = deadline();
while(n > 0){ while(n > 0){
r = send(fd, p, n, MSG_NOSIGNAL); 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;
continue;
}
if(r < 0)
return -1; 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) || commit[0] = '\0';
(pcap > 0 && preedit == NULL)) preedit[0] = '\0';
return -1;
if(ccap > 0)
commit[0] = '\0';
if(pcap > 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);
} }

72
ipc.h
View File

@@ -2,21 +2,40 @@
#include <stdint.h> #include <stdint.h>
/* /*
* Request: [flags, modifiers, key byte 0, ..., key byte 3]. * Key request: [flags, modifiers, key byte 0, ..., key byte 3].
* Flags request preedit and distinguish lifecycle reset from physical Escape. * 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 * The server identifies the connection as the engine owner; that identity is
* not sent on the wire. * 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, Ipcreqwant = 1<<0,
Ipcreqreset = 1<<1, Ipcreqreset = 1<<1,
Ipcext = 1<<7,
Ipcversion = 2,
Ipcopcap = 0,
Ipcopcaret = 1,
Ipcopsurround = 2,
Kspec = 0x110000, Kspec = 0x110000,
Kback = Kspec|0x08, Kback = Kspec|0x08,
@@ -25,37 +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;
}; };
uint32_t ipckeysym(uint32_t, uint32_t);
uint32_t ipcmod(uint32_t);
void ipcpackreq(unsigned char[Ipcreqsz], int, uint32_t, uint32_t); void ipcpackreq(unsigned char[Ipcreqsz], int, uint32_t, uint32_t);
void ipcpackreset(unsigned char[Ipcreqsz], int); 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*); 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 ipcreqreset(const unsigned char[Ipcreqsz]);
int ipcpackresp(unsigned char*, size_t, int, const char*, size_t, const char*, size_t, int); 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); int ipcpath(char*, size_t);
int ipcconnect(void); int ipcconnect(void);

95
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. */
comb = combine(cvow, nelem(cvow), last, jm); if(Jungidx(last) >= 0 && Choidx(jm) >= 0){
if(comb){ sputr(&e.next, compose(Choidx(jm), Jungidx(last), 0));
spopr(&im->pre); return e;
sputr(&e.next, comb); }
return e; /* Two vowels or two consonants may be one jamo, ㅘ or ㄳ. */
} comb = combine(cvow, nelem(cvow), last, jm);
if(comb == 0)
comb = combine(cjong, nelem(cjong), last, jm);
if(comb){
sputr(&e.next, comb);
return e;
}
if(ji >= 0 && splitpair(cjong, nelem(cjong), last, &stay, &next)){
sputr(&e.s, stay);
sputr(&e.next, compose(Choidx(next), ji, 0));
return e;
} }
e.s = im->pre; e.s = im->pre;
sputr(&e.next, jm); sputr(&e.next, jm);
@@ -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,12 +283,10 @@ backko(Im *im)
last = slastr(&im->pre); last = slastr(&im->pre);
if(!issyl(last)){ if(!issyl(last)){
if(splitpair(cvow, nelem(cvow), last, &stay, &next)){
spopr(&im->pre);
sputr(&im->pre, stay);
return;
}
spopr(&im->pre); spopr(&im->pre);
if(splitpair(cvow, nelem(cvow), last, &stay, &next) ||
splitpair(cjong, nelem(cjong), last, &stay, &next))
sputr(&im->pre, stay);
return; return;
} }
decompose(last, &c, &j, &jo); decompose(last, &c, &j, &jo);

42
main.c
View File

@@ -3,15 +3,11 @@
Channel *drawc; Channel *drawc;
Channel *keyc; Channel *keyc;
Channel *dictreqc;
Channel *dictresc;
char **fontfiles;
int nfontfiles;
void void
usage(void) usage(void)
{ {
fprint(2, "usage: strans mapdir fontfile [fontfile ...]\n"); fprint(2, "usage: strans mapdir\n");
threadexitsall("usage"); threadexitsall("usage");
} }
@@ -51,25 +47,31 @@ erealloc(void *p, ulong n)
void void
threadmain(int argc, char **argv) threadmain(int argc, char **argv)
{ {
if(argc < 3 || argc > Maxfonts + 2) char *display, *scale;
if(argc != 2)
usage(); usage();
fontfiles = argv + 2; /* Whichever popup runs is sized from it, and setfont reads Fontsz. */
nfontfiles = argc - 2; 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]);
dictinit(argv[1]);
srvinit(); srvinit();
if(proccreate(drawthread, nil, 16384) < 0) proccreate(srvthread, nil, 16384);
die("can't create draw worker"); proccreate(ibusthread, nil, 32768);
if(proccreate(srvthread, nil, 16384) < 0) /* One popup per session: the Wayland frontend draws its own, so the
die("can't create server worker"); * X11 one and the XIM it serves stand down. */
if(proccreate(ibusthread, nil, 32768) < 0) if(getenv("WAYLAND_DISPLAY") != nil && wlinit())
die("can't create IBus worker"); proccreate(wlthread, nil, 32768);
if(threadcreate(dictthread, nil, 16384) < 0) else{
die("can't create dictionary worker"); proccreate(drawthread, nil, 16384);
display = getenv("DISPLAY");
if(display != nil && display[0] != '\0')
proccreate(ximthread, nil, 32768);
}
imthread(nil); imthread(nil);
} }

View File

@@ -1,42 +1,90 @@
# Dictionary data # Dictionary data
## Korean Hanja data ## Korean Hanja and symbol data
`hanja.src` is the tracked, reviewable source for Korean Hanja conversion. `hanja.src` and `mssymbol.src` are the tracked, reviewable sources for what
Every data row is exactly one Hanja character, a tab, and one modern Hangul the Hanja search converts. Every data row is a result, a tab, and its
syllable: 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:
``` ```
漢 한 漢 한
漢字 한자
※ ㅁ
``` ```
It contains no word rows such as `견출지` or `방학`. `mkhanja` validates this `mkhanja` validates that contract and groups the rows of both sources by
contract and groups rows by their Hangul reading to produce the existing their reading to produce the runtime dictionary. The two keyspaces do not
runtime dictionary format. Candidate order follows source order. The source meet: a reading is either syllables or one jamo. Candidate order follows
keeps all retained pairs for review; the generated dictionary stores the first source order. The sources keep all retained pairs for review; the
32 candidates per reading because that is the engine's lookup limit. generated dictionary stores the first 128 candidates per reading because
that is the engine's lookup limit.
The source is derived from libhangul release tag `libhangul-0.2.0`. The The sources are derived from libhangul release tag `libhangul-0.2.0`. The
annotated tag object is `20afc38922e3595ee3ed5b186f2ea05afe663763`, its annotated tag object is `20afc38922e3595ee3ed5b186f2ea05afe663763`, its
peeled commit is `41c702f5d3581325b646ef6249f1f641b0427ae0`, and peeled commit is `41c702f5d3581325b646ef6249f1f641b0427ae0`, and
`data/hanja/hanja.txt` has blob ID `data/hanja/hanja.txt` and `data/hanja/mssymbol.txt` have blob IDs
`199cfd70c4b306257ac2e018714a1285f7cf0ed3`. Import and regenerate it from `199cfd70c4b306257ac2e018714a1285f7cf0ed3` and
the repository root with: `31c4e63d74293b6a759d4a2005cd1e7333746631`. Import and regenerate them
from the repository root with:
```sh ```sh
curl -L https://raw.githubusercontent.com/libhangul/libhangul/41c702f5d3581325b646ef6249f1f641b0427ae0/data/hanja/hanja.txt -o upstream curl -L https://raw.githubusercontent.com/libhangul/libhangul/41c702f5d3581325b646ef6249f1f641b0427ae0/data/hanja/hanja.txt -o upstream
printf '%s %s\n' dd44dcc856cf542b1022d0f39c2e9b9f8805fdcc5923be80f04849ed97ce0996 upstream | sha256sum -c - printf '%s %s\n' dd44dcc856cf542b1022d0f39c2e9b9f8805fdcc5923be80f04849ed97ce0996 upstream | sha256sum -c -
map/libhangul2hanja upstream >map/hanja.src 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 map/mkhanja >map/hanja.dict
``` ```
`libhangul2hanja` retains only rows whose reading is one modern Hangul `libhangul2hanja` keeps a syllable reading only when its value is Hanja the
syllable and whose value is one Hanja character supported by the current popup can draw: U+3400U+4DBF, U+4E00U+9FFF, or U+F900U+FAFF. Thus mixed
popup: U+3400U+4DBF, U+4E00U+9FFF, or U+F900U+FAFF. Thus word readings, values and supplementary-plane ideographs are omitted. It keeps a jamo
multi-character values, jamo readings, and supplementary-plane ideographs reading, U+3131U+314E, only when its value is one rune the popup can draw
are omitted. The import preserves the upstream license header and row order. and a candidate row can carry — so `mssymbol.txt`'s ideographic space and
The retained data is BSD 3-Clause licensed by Choe Hwanjin; the complete text soft hyphen are omitted, leaving 985 of its 987 rows, and a Hanja under a
is in `LICENSES/BSD-3-Clause-libhangul-hanja.txt`. 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 ## Japanese dictionary data
@@ -78,4 +126,5 @@ rejected; rewrite or omit those rows before import.
`verifymap.py` checks UTF-8, row structure, unique keys, 64-rune keys and `verifymap.py` checks UTF-8, row structure, unique keys, 64-rune keys and
values, canonical candidate spacing, and duplicate dictionary candidates. values, canonical candidate spacing, and duplicate dictionary candidates.
Pass it the exact `.map` and `.dict` files installed by the build. 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

File diff suppressed because it is too large Load Diff

238096
map/hanja.src

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,21 +1,38 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Extract single-character Hanja readings from libhangul data.""" """Extract Hanja and symbol readings from libhangul data."""
import sys import sys
import unicodedata
from pathlib import Path from pathlib import Path
def ishangul(s): def ishangul(s):
return len(s) == 1 and 0xAC00 <= ord(s) <= 0xD7A3 return s != "" and all(0xAC00 <= ord(c) <= 0xD7A3 for c in s)
def isjamo(s):
return len(s) == 1 and 0x3131 <= ord(s) <= 0x314E
def ishanja(s): def ishanja(s):
if len(s) != 1: return s != "" and all(0x3400 <= ord(c) <= 0x4DBF
return False or 0x4E00 <= ord(c) <= 0x9FFF
c = ord(s) or 0xF900 <= ord(c) <= 0xFAFF for c in s)
return (0x3400 <= c <= 0x4DBF
or 0x4E00 <= c <= 0x9FFF
or 0xF900 <= c <= 0xFAFF) def 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): def extract(src, name):
@@ -35,16 +52,16 @@ def extract(src, name):
fields = line.split(":") fields = line.split(":")
if len(fields) != 3: if len(fields) != 3:
raise ValueError(f"{name}:{lineno}: need key:value:comment") raise ValueError(f"{name}:{lineno}: need key:value:comment")
reading, hanja, _ = fields reading, value, _ = fields
if not ishangul(reading) or not ishanja(hanja): if not keeps(reading, value):
continue continue
pair = (hanja, reading) pair = (value, reading)
if pair in seen: if pair in seen:
raise ValueError(f"{name}:{lineno}: duplicate Hanja reading") raise ValueError(f"{name}:{lineno}: duplicate reading")
seen.add(pair) seen.add(pair)
entries.append(pair) entries.append(pair)
if not entries: if not entries:
raise ValueError(f"{name}: no single-character Hanja readings") raise ValueError(f"{name}: no readings")
return comments, entries return comments, entries
@@ -66,8 +83,8 @@ def main():
print(line) print(line)
if comments: if comments:
print() print()
for hanja, reading in entries: for value, reading in entries:
print(f"{hanja}\t{reading}") print(f"{value}\t{reading}")
except (OSError, UnicodeError, ValueError) as error: except (OSError, UnicodeError, ValueError) as error:
print(error, file=sys.stderr) print(error, file=sys.stderr)
return 1 return 1

View File

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

View File

@@ -1,32 +1,43 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Write hanja.dict from a one-Hanja-per-row UTF-8 source.""" """Write hanja.dict from result-per-row UTF-8 sources."""
import sys import sys
import unicodedata
from pathlib import Path from pathlib import Path
SOURCE = Path(__file__).with_name("hanja.src") SOURCES = [Path(__file__).with_name(name)
MAXCANDIDATES = 32 for name in ("hanja.src", "mssymbol.src")]
MAXCANDIDATES = 128
def ishangul(s): def ishangul(s):
return len(s) == 1 and 0xAC00 <= ord(s) <= 0xD7A3 return s != "" and all(0xAC00 <= ord(c) <= 0xD7A3 for c in s)
def isjamo(s):
return len(s) == 1 and 0x3131 <= ord(s) <= 0x314E
def ishanja(s): def ishanja(s):
if len(s) != 1: return s != "" and all(0x3400 <= ord(c) <= 0x4DBF
return False or 0x4E00 <= ord(c) <= 0x9FFF
c = ord(s) or 0xF900 <= ord(c) <= 0xFAFF for c in s)
return (0x3400 <= c <= 0x4DBF
or 0x4E00 <= c <= 0x9FFF
or 0xF900 <= c <= 0xFAFF)
def read(path): def 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 = [] comments = []
table = {}
seen = set()
leading = True leading = True
found = False
with path.open(encoding="utf-8") as src: with path.open(encoding="utf-8") as src:
for lineno, raw in enumerate(src, 1): for lineno, raw in enumerate(src, 1):
line = raw.rstrip("\r\n") line = raw.rstrip("\r\n")
@@ -39,31 +50,36 @@ def read(path):
leading = False leading = False
fields = line.split("\t") fields = line.split("\t")
if len(fields) != 2: if len(fields) != 2:
raise ValueError(f"{path}:{lineno}: need Hanja<TAB>reading") raise ValueError(f"{path}:{lineno}: need result<TAB>reading")
hanja, reading = fields value, reading = fields
if not ishanja(hanja): if ishangul(reading):
raise ValueError(f"{path}:{lineno}: need one BMP Hanja character") if not ishanja(value):
if not ishangul(reading): raise ValueError(f"{path}:{lineno}: need BMP Hanja")
raise ValueError(f"{path}:{lineno}: need one Hangul syllable") elif isjamo(reading):
pair = (hanja, 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: if pair in seen:
raise ValueError(f"{path}:{lineno}: duplicate Hanja reading") raise ValueError(f"{path}:{lineno}: duplicate reading")
seen.add(pair) seen.add(pair)
table.setdefault(reading, []).append(hanja) found = True
if not seen: table.setdefault(reading, []).append(value)
raise ValueError(f"{path}: no Hanja readings") if not found:
return comments, table raise ValueError(f"{path}: no readings")
return comments
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
if len(sys.argv) > 2: paths = [Path(arg) for arg in sys.argv[1:]] or SOURCES
print(f"usage: {sys.argv[0]} [hanja.src]", file=sys.stderr) table = {}
return 2 seen = set()
path = Path(sys.argv[1]) if len(sys.argv) == 2 else SOURCE
try: try:
comments, table = read(path) comments = [line for path in paths for line in read(path, table, seen)]
for line in comments: for line in comments:
print(line) print(line)
if comments: if comments:

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

View File

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

103
map/symbol.src Normal file
View File

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

View File

@@ -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

View File

@@ -1,33 +1,257 @@
#include "dat.h" #include "dat.h"
#include "fn.h" #include "fn.h"
int enum {
popupcells(Rune *r, int n) Asciitofull = 0xFEE0,
{ };
int i, w;
int popupscale = 1;
static void
fill(u32int *buf, int n, u32int color)
{
int i;
w = 0;
for(i = 0; i < n; i++) for(i = 0; i < n; i++)
if(r[i] != 0xfe0e && r[i] != 0xfe0f) buf[i] = color;
w++; }
return w;
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 void
popupposition(Caret *caret, int pointerx, int pointery, int screenw, popuparea(Area *mon, int nmon, Area *work, int x, int y, Area *out)
int screenh, int w, int h, int *x, int *y)
{ {
vlong px, py, xmax, ymax; 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){ if(caret->valid){
px = caret->x; px = caret->x;
py = (vlong)caret->y + max(caret->h, 0); below = (vlong)caret->y + max(caret->h, 0);
if(below >= area->y && below + h <= bottom)
py = below;
else
py = (vlong)caret->y - h;
}else{ }else{
px = (vlong)pointerx + 10; px = (vlong)pointerx + 10;
py = (vlong)pointery + 10; py = (vlong)pointery + 10;
if(py + h > bottom)
py = (vlong)pointery - 10 - h;
} }
xmax = max((vlong)screenw - w, 0); xmax = max(right - w, area->x);
ymax = max((vlong)screenh - h, 0); ymax = max(bottom - h, area->y);
*x = max(0, min(px, xmax)); *x = max((vlong)area->x, min(px, xmax));
*y = max(0, min(py, ymax)); *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>

322
run.sh
View File

@@ -1,326 +1,24 @@
#!/bin/sh #!/bin/sh
cd "$(dirname "$0")" || exit 1 cd "$(dirname "$0")" || exit 1
root=$(pwd -P) || exit 1
uid=$(id -u) || exit 1
if test "$#" -eq 0; then if test "$#" -ne 0; then
set -- echo "usage: run.sh" >&2
for font in \ exit 1
/usr/share/fonts/TTF/DejaVuSans.ttf \
/usr/share/fonts/TTF/Jigmo.ttf \
/usr/share/fonts/TTF/Jigmo2.ttf \
/usr/share/fonts/noto/NotoSansCJK-Regular.ttc
do
if test -f "$font"; then
set -- "$@" "$font"
fi
done
if test "$#" -eq 0; then
echo "run.sh: no default font found; pass a font file" >&2
exit 1
fi
fi fi
if ! test -x ./strans; then if ! test -x ./strans; then
echo "run.sh: ./strans is not executable; run make first" >&2 echo "run.sh: ./strans is not executable; run make first" >&2
exit 1 exit 1
fi fi
if test -n "${DISPLAY-}" && ! test -x xim/strans-xim; then if ! command -v pkill >/dev/null 2>&1; then
echo "run.sh: xim/strans-xim is not executable; run make first" >&2 echo "run.sh: pkill is required" >&2
exit 1 exit 1
fi fi
case ${XDG_RUNTIME_DIR-} in uid=$(id -u) || exit 1
/) ipc=/strans.sock ;; pkill -TERM -u "$uid" -x strans 2>/dev/null || :
/*/) ipc=${XDG_RUNTIME_DIR}strans.sock ;; sleep 0.1
/*) ipc=${XDG_RUNTIME_DIR}/strans.sock ;; pkill -KILL -u "$uid" -x strans 2>/dev/null || :
*) ipc=/tmp/strans.$(id -u) ;;
esac
if test -n "${XDG_CONFIG_HOME-}"; then
busdir=$XDG_CONFIG_HOME/ibus/bus
elif test "${HOME+x}" = x; then
busdir=$HOME/.config/ibus/bus
else
echo "run.sh: HOME or XDG_CONFIG_HOME is required for IBus discovery" >&2
exit 1
fi
strans_pid= ./strans map </dev/null &
xim_pid=
old_daemon_pid=
old_daemon_start=
old_launcher_pid=
old_launcher_start=
cleanup()
{
status=$?
trap - 0 1 2 15
if test -n "$xim_pid"; then
kill "$xim_pid" 2>/dev/null || :
wait "$xim_pid" 2>/dev/null || :
xim_pid=
fi
if test -n "$strans_pid"; then
kill "$strans_pid" 2>/dev/null || :
wait "$strans_pid" 2>/dev/null || :
strans_pid=
fi
exit "$status"
}
ibusready()
{
for address in "$busdir"/*; do
test -f "$address" || continue
foundpid=
foundaddress=
while IFS= read -r line; do
case $line in
IBUS_DAEMON_PID="$strans_pid") foundpid=1 ;;
IBUS_ADDRESS=unix:abstract=strans-"$strans_pid",*) foundaddress=1 ;;
esac
done <"$address"
if test -n "$foundpid" && test -n "$foundaddress"; then
return 0
fi
done
return 1
}
procuid()
{
{
while IFS=' ' read -r key real effective saved filesystem rest; do
case $key in
Uid:)
test "$real" = "$effective" &&
test "$real" = "$saved" &&
test "$real" = "$filesystem" || return 1
printf '%s\n' "$real"
return 0
;;
esac
done <"/proc/$1/status"
} 2>/dev/null
return 1
}
procparent()
{
{
while IFS=' ' read -r key value rest; do
case $key in
PPid:) printf '%s\n' "$value"; return 0 ;;
esac
done <"/proc/$1/status"
} 2>/dev/null
return 1
}
procstart()
{
proc_stat=
{ IFS= read -r proc_stat <"/proc/$1/stat"; } 2>/dev/null || return 1
proc_stat=${proc_stat##*) }
set -- $proc_stat
test "$#" -ge 20 || return 1
test "$1" != Z || return 1
printf '%s\n' "${20}"
}
sameproc()
{
test -n "$2" || return 1
proc_now=$(procstart "$1") || return 1
test "$proc_now" = "$2"
}
listeninginode()
{
listener_inode=
listener_count=0
{
while IFS=' ' read -r num ref proto flags type state inode path; do
if test "$flags" = 00010000 && test "$type" = 0001 &&
test "$state" = 01 && test "$path" = "$1"; then
listener_inode=$inode
listener_count=$((listener_count + 1))
fi
done </proc/net/unix
} 2>/dev/null
test "$listener_count" -eq 1 || return 1
printf '%s\n' "$listener_inode"
}
socketowned()
{
socket_inode=$(listeninginode "$2") || return 1
for socket_fd in "/proc/$1/fd"/*; do
test "$(readlink "$socket_fd" 2>/dev/null)" = \
"socket:[$socket_inode]" && return 0
done
return 1
}
validdaemon()
{
test "$(procuid "$1")" = "$uid" || return 1
test "$(readlink "/proc/$1/cwd" 2>/dev/null)" = "$root" || return 1
daemon_exe=$(readlink "/proc/$1/exe" 2>/dev/null) || return 1
case $daemon_exe in
"$root/strans"|"$root/strans (deleted)") ;;
*) return 1 ;;
esac
test -S "$ipc" || return 1
socketowned "$1" "$ipc" || return 1
socketowned "$1" "@strans-$1"
}
finddaemon()
{
old_daemon_pid=
old_daemon_start=
for address in "$busdir"/*; do
test -f "$address" || continue
address_pid=
address_ok=
while IFS= read -r line; do
case $line in
IBUS_DAEMON_PID=*) address_pid=${line#IBUS_DAEMON_PID=} ;;
esac
done <"$address"
case $address_pid in
''|*[!0-9]*) continue ;;
esac
while IFS= read -r line; do
case $line in
IBUS_ADDRESS=unix:abstract=strans-"$address_pid",*)
address_ok=1 ;;
esac
done <"$address"
test -n "$address_ok" || continue
validdaemon "$address_pid" || continue
address_start=$(procstart "$address_pid") || continue
validdaemon "$address_pid" || continue
old_daemon_pid=$address_pid
old_daemon_start=$address_start
return 0
done
return 1
}
findlauncher()
{
launcher_env=$({ tr '\000' '\n' <"/proc/$old_daemon_pid/environ"; } \
2>/dev/null) || return 1
old_launcher_pid=$(printf '%s\n' "$launcher_env" |
sed -n 's/^STRANS_RUN_OWNER_PID=//p')
old_launcher_start=$(printf '%s\n' "$launcher_env" |
sed -n 's/^STRANS_RUN_OWNER_START=//p')
case $old_launcher_pid in
''|*[!0-9]*|0|1|"$$") return 1 ;;
esac
case $old_launcher_start in
''|*[!0-9]*) return 1 ;;
esac
test "$(procuid "$old_launcher_pid")" = "$uid" || return 1
test "$(readlink "/proc/$old_launcher_pid/cwd" 2>/dev/null)" = "$root" ||
return 1
sameproc "$old_launcher_pid" "$old_launcher_start" || return 1
sameproc "$old_daemon_pid" "$old_daemon_start" || return 1
test "$(procparent "$old_daemon_pid")" = "$old_launcher_pid" || return 1
return 0
}
stoplauncher()
{
sameproc "$old_daemon_pid" "$old_daemon_start" || return 1
test "$(procparent "$old_daemon_pid")" = "$old_launcher_pid" || return 1
sameproc "$old_launcher_pid" "$old_launcher_start" || return 1
kill "$old_launcher_pid" 2>/dev/null || return 1
echo "run.sh: stopping previous launcher $old_launcher_pid (daemon $old_daemon_pid)"
stop_attempt=0
while test "$stop_attempt" -lt 10; do
if ! sameproc "$old_launcher_pid" "$old_launcher_start" &&
! sameproc "$old_daemon_pid" "$old_daemon_start"; then
return 0
fi
stop_attempt=$((stop_attempt + 1))
if test "$stop_attempt" -lt 10; then
sleep 1
fi
done
echo "run.sh: previous launcher did not stop within 10 seconds" >&2
return 1
}
trap cleanup 0
trap 'exit 129' 1
trap 'exit 130' 2
trap 'exit 143' 15
launcher_start=$(procstart "$$") || {
echo "run.sh: cannot read launcher process identity from /proc" >&2
exit 1
}
if finddaemon; then
if ! findlauncher; then
echo "run.sh: daemon $old_daemon_pid owns the current endpoints but has no" >&2
echo "run.sh: live run.sh owner; leaving it untouched (stop it once, then retry)" >&2
exit 1
fi
if ! stoplauncher; then
echo "run.sh: previous launcher did not stop cleanly; refusing to start another daemon" >&2
exit 1
fi
fi
STRANS_RUN_OWNER_PID=$$ STRANS_RUN_OWNER_START=$launcher_start \
./strans map "$@" &
strans_pid=$!
attempt=0
while test "$attempt" -lt 10; do
if test -S "$ipc" && ibusready; then
break
fi
if ! kill -0 "$strans_pid" 2>/dev/null; then
wait "$strans_pid"
status=$?
strans_pid=
if test "$status" -eq 0; then
status=1
fi
echo "run.sh: daemon exited before publishing IPC and IBus endpoints" >&2
exit "$status"
fi
attempt=$((attempt + 1))
if test "$attempt" -lt 10; then
sleep 1
fi
done
if test "$attempt" -eq 10; then
echo "run.sh: daemon did not publish IPC and IBus endpoints within 10 seconds" >&2
exit 1
fi
echo "run.sh: daemon $strans_pid is ready"
if test -z "${DISPLAY-}"; then
echo "run.sh: DISPLAY is unset; XIM will not be started"
wait "$strans_pid"
status=$?
strans_pid=
exit "$status"
fi
xim/strans-xim &
xim_pid=$!
echo "run.sh: started XIM $xim_pid; press Ctrl-C to stop both processes"
wait "$xim_pid"
status=$?
xim_pid=
exit "$status"

138
srv.c
View File

@@ -5,56 +5,122 @@
#include <sys/un.h> #include <sys/un.h>
static char adir[256]; 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;
static 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->op = ipcreqreset(req) ? Keyreset : Keypress; sockpath[0] = '\0';
kr->ks = ks;
kr->mod = mod;
memset(&kr->caret, 0, sizeof kr->caret);
return want;
} }
static int
srvnote(void *v, char *note)
{
USED(v);
if(notefatal(note))
srvunlink();
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, want; 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);
kr.owner = &fd; kr.owner = &fd;
while((want = srvreadkey(fd, &kr)) >= 0){ 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, 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;
} }
kr.op = Keyrelease; kr.op = Keyrelease;
kr.ks = 0;
kr.mod = 0;
chansend(keyc, &kr); chansend(keyc, &kr);
chanrecv(reply, &res); chanrecv(kr.reply, &res);
chanfree(reply); chanfree(kr.reply);
close(fd); close(fd);
chanrecv(clientc, &token); chanrecv(clientc, &token);
} }
@@ -62,18 +128,23 @@ clientthread(void *arg)
void void
srvinit(void) srvinit(void)
{ {
char *addr, path[sizeof(((struct sockaddr_un*)0)->sun_path)]; struct stat st;
char *addr;
if(ipcpath(path, sizeof path) < 0) if(ipcpath(sockpath, sizeof sockpath) < 0)
die("IPC path is too long"); die("IPC path is too long");
addr = smprint("unix!%s", path); addr = smprint("unix!%s", sockpath);
if(addr == nil) if(addr == nil)
die("out of memory"); die("out of memory");
if(announce(addr, adir) < 0) if(announce(addr, adir) < 0)
die("IPC endpoint is already in use: %r"); die("IPC endpoint is already in use: %r");
free(addr); free(addr);
if(chmod(path, 0600) < 0) if(chmod(sockpath, 0600) < 0 || lstat(sockpath, &st) < 0)
die("can't protect IPC endpoint: %s", path); die("can't protect IPC endpoint: %s", sockpath);
sockdev = st.st_dev;
sockino = st.st_ino;
atexit(srvunlink);
threadnotify(srvnote, 1);
} }
void void
@@ -94,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);
}
} }
} }

41
str.c
View File

@@ -1,24 +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;
if(n < 0 || (n > 0 && src == nil)) while(n > 0){
return; if(tmp.n >= Maxrunes || !fullrune(src, n))
while(n > 0 && s->n < Maxrunes){ return 0;
if(!fullrune(src, n)) len = chartorune(&r, src);
break; if((r == Runeerror && len == 1) || (r >= 0xd800 && r <= 0xdfff))
len = chartorune(&s->r[s->n], src); return 0;
if(len > n) tmp.r[tmp.n++] = r;
break;
s->n++;
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
@@ -66,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]);

1531
strans.c

File diff suppressed because it is too large Load Diff

View File

@@ -1,71 +1,150 @@
CC = 9c CC = 9c
LD = 9l LD = 9l
HOSTCC = cc HOSTCC = cc
CFLAGS = -std=c99 -Wall -Wextra -O2 -g -I.. -I../cutest PKG_CONFIG ?= pkg-config
FT_CFLAGS = $(shell pkg-config --cflags freetype2) CFLAGS ?= -O2 -g
FT_LIBS = $(shell pkg-config --libs freetype2) UNIT_CPPFLAGS = -I..
DBUS_CFLAGS = $(shell pkg-config --cflags dbus-1) UNIT_CFLAGS = -Wall -Wextra
DBUS_LIBS = $(shell pkg-config --libs dbus-1) HOST_CFLAGS = -std=c99 -Wall -Wextra
IBUS_CFLAGS = $(shell pkg-config --cflags dbus-1 xkbcommon) TEXT_CFLAGS = $(shell $(PKG_CONFIG) --cflags pangocairo cairo fontconfig)
IBUS_LIBS = $(shell pkg-config --libs dbus-1 xkbcommon) TEXT_LIBS = $(shell $(PKG_CONFIG) --libs pangocairo cairo fontconfig)
LIBS = -lthread -lbio $(FT_LIBS) $(IBUS_LIBS) 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
LIVE = ibus_live_test ipc_live_test daemon_collision_test daemon_failure_test \ STRESS = stress_test
daemon_restart_test LIVESRC = live.c live.h
TESTSRC = unit_test.c test_util.c str_test.c hash_test.c trie_test.c \ 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 \ 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 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 popup_layout.c \ # imv2.c and vkv1.c are wayland-scanner's; the parent Makefile writes them.
font.c 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) $(LIVE) all: $(PROG) $(STRESS) $(LIVE)
check test: $(PROG) $(LIVE) check test: $(PROG)
./$(PROG) $(TESTARGS) ./$(PROG) $(UNITARGS)
./ibus_live_test ../strans ../map
check-live: $(SMOKE) ../strans ../gtk/im-strans.so
./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 ./ipc_live_test ../strans ../map
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_collision_test ../strans ../map
./daemon_failure_test ../strans ./daemon_failure_test ../strans
./daemon_restart_test ../strans ../map ./daemon_restart_test ../strans ../map
$(PROG): $(OBJS) $(PROG): unit_test.o $(COMMONOBJ)
$(LD) -o $@ $(OBJS) $(LIBS) $(LD) $(LDFLAGS) -o $@ unit_test.o $(COMMONOBJ) $(UNIT_LDLIBS) \
$(LDLIBS)
ibus_live_test: ibus_live_test.c $(STRESS): stress_test.o $(COMMONOBJ)
$(HOSTCC) -std=c99 -Wall -Wextra -O2 -g $(DBUS_CFLAGS) -o $@ $< $(DBUS_LIBS) $(LD) $(LDFLAGS) -o $@ stress_test.o $(COMMONOBJ) $(UNIT_LDLIBS) \
$(LDLIBS)
ipc_live_test: ipc_live_test.c ../ipc.c ../ipc.h stress_test.o: unit_test.c
$(HOSTCC) -std=c99 -Wall -Wextra -O2 -g -I.. -o $@ ipc_live_test.c ../ipc.c $(CC) $(CPPFLAGS) $(UNIT_CPPFLAGS) $(UNIT_CFLAGS) $(CFLAGS) -DSTRESS \
-c -o $@ $<
daemon_collision_test: daemon_collision_test.c ../ipc.c ../ipc.h ibus_live_test: ibus_live_test.c $(LIVESRC) $(LIVEBUSSRC) ../ipc.c ../ipc.h
$(HOSTCC) -std=c99 -Wall -Wextra -O2 -g -I.. $(DBUS_CFLAGS) -o $@ \ $(HOSTCC) $(CPPFLAGS) -I.. $(DBUS_CFLAGS) $(HOST_CFLAGS) $(CFLAGS) \
daemon_collision_test.c ../ipc.c $(DBUS_LIBS) $(LDFLAGS) -o $@ ibus_live_test.c live.c livebus.c ../ipc.c \
$(DBUS_LIBS) $(LDLIBS)
daemon_failure_test: daemon_failure_test.c ibus_client_smoke: ibus_client_smoke.c
$(HOSTCC) -std=c99 -Wall -Wextra -O2 -g -o $@ $< $(HOSTCC) $(CPPFLAGS) $(IBUS_CLIENT_CFLAGS) $(HOST_CFLAGS) $(CFLAGS) \
$(LDFLAGS) -o $@ $< $(IBUS_CLIENT_LIBS) $(LDLIBS)
daemon_restart_test: daemon_restart_test.c ../ipc.c ../ipc.h gtk_live_test: gtk_live_test.c $(LIVESRC) ../ipc.c ../ipc.h
$(HOSTCC) -std=c99 -Wall -Wextra -O2 -g -I.. $(DBUS_CFLAGS) -o $@ \ $(HOSTCC) $(CPPFLAGS) -I.. $(GTK_CFLAGS) $(HOST_CFLAGS) $(CFLAGS) \
daemon_restart_test.c ../ipc.c $(DBUS_LIBS) $(LDFLAGS) -o $@ gtk_live_test.c live.c ../ipc.c $(GTK_LIBS) \
-pthread $(LDLIBS)
$(TESTOBJ): test.h ../dat.h ../fn.h ../ipc.h ../cutest/cutest.h # 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: CFLAGS += $(IBUS_CFLAGS) ibus_test.o: UNIT_CPPFLAGS += $(IBUS_CFLAGS)
ibus_test.o: ../ibus.c 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 server_test.o: ../srv.c
unit_font.o: CFLAGS += $(FT_CFLAGS) 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) $(LIVE) 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");
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,635 +1,138 @@
#define _GNU_SOURCE #define _GNU_SOURCE
#include <dirent.h> #include <dirent.h>
#include <errno.h> #include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <poll.h> #include <poll.h>
#include <signal.h>
#include <stdarg.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/inotify.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <sys/types.h> #include <sys/types.h>
#include <sys/wait.h> #include <sys/wait.h>
#include <time.h>
#include <unistd.h> #include <unistd.h>
#include "live.h"
enum enum
{ {
Failtimeout = 8000, Failtimeout = 4000,
}; };
typedef struct Test Test;
struct Test
{
pid_t child;
int errfd;
int notifyfd;
int runtimewd;
int buswd;
int endpointclean;
char root[256];
char runtime[320];
char config[320];
char ibus[384];
char bus[448];
char home[320];
char socket[384];
char badmap[320];
char missingfont[384];
char expected[768];
char err[4096];
size_t nerr;
};
static uint32_t endpointmask = IN_CREATE|IN_MOVED_TO|IN_MOVED_FROM|IN_ATTRIB|
IN_DELETE|IN_MODIFY|IN_CLOSE_WRITE|IN_DELETE_SELF|IN_MOVE_SELF;
static int static int
fail(char *fmt, ...) waitfailed(Daemon *d, char *badmap)
{ {
va_list ap; struct pollfd pfd;
int n, status;
fprintf(stderr, "daemon_failure_test: ");
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fputc('\n', stderr);
return 0;
}
static 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;
}
static int
leftms(int64_t deadline)
{
int64_t n;
n = nowms();
if(n < 0)
return -1;
n = deadline - n;
if(n <= 0)
return 0;
if(n > 0x7fffffff)
return 0x7fffffff;
return n;
}
static int
makedir(char *path)
{
if(mkdir(path, 0700) == 0)
return 1;
return fail("mkdir %s: %s", path, strerror(errno));
}
static void
readerrors(Test *t)
{
ssize_t n;
if(t->errfd < 0)
return;
while(t->nerr + 1 < sizeof t->err){
n = read(t->errfd, t->err + t->nerr,
sizeof t->err - t->nerr - 1);
if(n > 0){
t->nerr += n;
continue;
}
if(n < 0 && errno == EINTR)
continue;
break;
}
t->err[t->nerr] = '\0';
}
static void
showerrors(Test *t)
{
readerrors(t);
if(t->nerr != 0)
fprintf(stderr, "daemon_failure_test: daemon stderr:\n%s", t->err);
}
static int
setup(Test *t)
{
memset(t, 0, sizeof *t);
t->child = -1;
t->errfd = -1;
t->notifyfd = -1;
t->runtimewd = -1;
t->buswd = -1;
t->endpointclean = 1;
if(nowms() < 0)
return fail("read monotonic clock: %s", strerror(errno));
snprintf(t->root, sizeof t->root, "/tmp/strans-failure.XXXXXX");
if(mkdtemp(t->root) == NULL){
t->root[0] = '\0';
return fail("mkdtemp: %s", strerror(errno));
}
if(snprintf(t->runtime, sizeof t->runtime, "%s/runtime", t->root)
>= (int)sizeof t->runtime ||
snprintf(t->config, sizeof t->config, "%s/config", t->root)
>= (int)sizeof t->config ||
snprintf(t->ibus, sizeof t->ibus, "%s/ibus", t->config)
>= (int)sizeof t->ibus ||
snprintf(t->bus, sizeof t->bus, "%s/bus", t->ibus)
>= (int)sizeof t->bus ||
snprintf(t->home, sizeof t->home, "%s/home", t->root)
>= (int)sizeof t->home ||
snprintf(t->socket, sizeof t->socket, "%s/strans.sock", t->runtime)
>= (int)sizeof t->socket ||
snprintf(t->badmap, sizeof t->badmap, "%s/missing-map", t->root)
>= (int)sizeof t->badmap ||
snprintf(t->missingfont, sizeof t->missingfont,
"%s/missing-font.ttf", t->root) >= (int)sizeof t->missingfont ||
snprintf(t->expected, sizeof t->expected,
"strans: can't open: %s/hira.map\n", t->badmap)
>= (int)sizeof t->expected)
return fail("temporary path is too long");
if(!makedir(t->runtime) || !makedir(t->config) || !makedir(t->ibus) ||
!makedir(t->bus) || !makedir(t->home))
return 0;
t->notifyfd = inotify_init1(IN_CLOEXEC|IN_NONBLOCK);
if(t->notifyfd < 0)
return fail("inotify_init1: %s", strerror(errno));
t->runtimewd = inotify_add_watch(t->notifyfd, t->runtime, endpointmask);
if(t->runtimewd < 0)
return fail("watch runtime directory: %s", strerror(errno));
t->buswd = inotify_add_watch(t->notifyfd, t->bus, endpointmask);
if(t->buswd < 0)
return fail("watch IBus directory: %s", strerror(errno));
return 1;
}
static int
startchild(Test *t, char *program)
{
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(dup2(errpipe[1], STDERR_FILENO) < 0)
_exit(126);
close(errpipe[1]);
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", t->runtime, 1) < 0 ||
setenv("XDG_CONFIG_HOME", t->config, 1) < 0 ||
setenv("HOME", t->home, 1) < 0 || unsetenv("DISPLAY") < 0 ||
unsetenv("DBUS_SESSION_BUS_ADDRESS") < 0 ||
unsetenv("IBUS_ADDRESS") < 0){
dprintf(STDERR_FILENO, "set daemon environment: %s\n",
strerror(errno));
_exit(126);
}
execl(program, program, t->badmap, t->missingfont, (char*)0);
dprintf(STDERR_FILENO, "exec %s: %s\n", program, strerror(errno));
_exit(127);
}
close(errpipe[1]);
t->child = pid;
t->errfd = errpipe[0];
return 1;
}
static int
drainnotify(Test *t)
{
char buf[4096];
char *where;
struct inotify_event *ev;
ssize_t n;
size_t off;
int ok;
ok = 1;
for(;;){
n = read(t->notifyfd, buf, sizeof buf);
if(n < 0 && errno == EINTR)
continue;
if(n < 0 && errno == EAGAIN)
return ok;
if(n < 0)
return fail("read endpoint watches: %s", strerror(errno));
if(n == 0)
return ok;
for(off = 0; off + sizeof *ev <= (size_t)n;
off += sizeof *ev + ev->len){
ev = (struct inotify_event*)(buf + off);
if(off + sizeof *ev + ev->len > (size_t)n)
return fail("truncated inotify event");
if(ev->mask & IN_Q_OVERFLOW){
t->endpointclean = 0;
fail("endpoint watch queue overflowed");
ok = 0;
continue;
}
if(ev->mask & (IN_IGNORED|IN_UNMOUNT)){
t->endpointclean = 0;
fail("endpoint watch became invalid: %#x", ev->mask);
ok = 0;
continue;
}
if(ev->wd != t->runtimewd && ev->wd != t->buswd)
continue;
if(!(ev->mask & endpointmask))
continue;
where = ev->wd == t->runtimewd ? "runtime" : "IBus";
t->endpointclean = 0;
fail("unexpected %s endpoint event %#x for %s", where,
ev->mask, ev->len != 0 ? ev->name : "directory");
ok = 0;
}
if(off != (size_t)n)
return fail("truncated inotify event buffer");
}
}
static int
killowned(Test *t, int *status)
{
pid_t pid;
int n, ok;
if(t->child <= 0)
return 1;
pid = t->child;
ok = 1;
if(kill(pid, SIGKILL) < 0 && errno != ESRCH){
fail("kill -9 %ld: %s", (long)pid, strerror(errno));
ok = 0;
}
do
n = waitpid(pid, status, 0);
while(n < 0 && errno == EINTR);
if(n != pid){
fail("reap %ld after SIGKILL: %s", (long)pid,
n < 0 ? strerror(errno) : "wrong child");
ok = 0;
}else
t->child = -1;
return ok;
}
static int
waitfailed(Test *t)
{
struct pollfd pfd[2];
pid_t pid;
int n, ok, status, timeout;
int64_t deadline; int64_t deadline;
pid = t->child; status = 0;
ok = 1; deadline = nowms() + Failtimeout;
deadline = nowms();
if(deadline < 0){
fail("read monotonic clock before waiting for daemon: %s",
strerror(errno));
killowned(t, &status);
readerrors(t);
drainnotify(t);
return 0;
}
deadline += Failtimeout;
for(;;){ for(;;){
do n = waitpid(d->pid, &status, WNOHANG);
n = waitpid(pid, &status, WNOHANG); if(n == d->pid){
while(n < 0 && errno == EINTR); d->pid = -1;
if(n == pid){ readerrors(d);
t->child = -1;
readerrors(t);
if(!drainnotify(t))
ok = 0;
break; break;
} }
if(n < 0){
fail("waitpid failed daemon %ld: %s", (long)pid,
strerror(errno));
if(errno == ECHILD){
t->child = -1;
readerrors(t);
drainnotify(t);
}else{
killowned(t, &status);
readerrors(t);
drainnotify(t);
}
return 0;
}
timeout = leftms(deadline);
if(timeout < 0){
fail("read monotonic clock while waiting for daemon: %s",
strerror(errno));
killowned(t, &status);
readerrors(t);
drainnotify(t);
return 0;
}
if(timeout == 0){
fail("daemon %ld did not exit after map initialization failure",
(long)pid);
killowned(t, &status);
return 0;
}
pfd[0].fd = t->notifyfd;
pfd[0].events = POLLIN;
pfd[0].revents = 0;
pfd[1].fd = t->errfd;
pfd[1].events = POLLIN|POLLHUP;
pfd[1].revents = 0;
n = poll(pfd, 2, timeout);
if(n < 0 && errno == EINTR) if(n < 0 && errno == EINTR)
continue; continue;
if(n < 0){ if(n < 0){
fail("poll failed daemon: %s", strerror(errno)); if(errno == ECHILD)
killowned(t, &status); d->pid = -1;
readerrors(t); return fail("waitpid: %s", strerror(errno));
drainnotify(t);
return 0;
} }
if(pfd[0].revents & (POLLERR|POLLHUP|POLLNVAL)){ n = leftms(deadline);
fail("endpoint watch became unusable: %#x", pfd[0].revents); if(n == 0)
killowned(t, &status); return fail("daemon did not exit after map initialization failure");
readerrors(t); pfd.fd = d->errfd;
drainnotify(t); pfd.events = POLLIN|POLLHUP;
return 0; pfd.revents = 0;
} n = poll(&pfd, 1, n);
if(pfd[1].revents & (POLLERR|POLLNVAL)){ if(n < 0 && errno == EINTR)
fail("daemon stderr pipe became unusable: %#x", continue;
pfd[1].revents); if(n < 0)
killowned(t, &status); return fail("poll daemon: %s", strerror(errno));
readerrors(t); if(n > 0)
drainnotify(t); readerrors(d);
return 0;
}
if((pfd[0].revents & POLLIN) && !drainnotify(t))
ok = 0;
if(pfd[1].revents & (POLLIN|POLLHUP))
readerrors(t);
} }
if(!WIFEXITED(status) || WEXITSTATUS(status) != 1) if(!WIFEXITED(status) || WEXITSTATUS(status) == 0)
return fail("daemon map failure had wait status %#x", status); return fail("daemon map failure had wait status %#x", status);
if(strcmp(t->err, t->expected) != 0) if(strstr(d->err, "can't open") == NULL || strstr(d->err, badmap) == NULL)
return fail("daemon did not report the expected map initialization failure"); return fail("daemon did not report the missing map directory");
return ok && t->endpointclean; return 1;
} }
static int static int
dirempty(char *path, char *which) emptydir(char *path)
{ {
DIR *dir; DIR *dir;
struct dirent *de; struct dirent *de;
int count, ok; int empty;
dir = opendir(path); dir = opendir(path);
if(dir == NULL) if(dir == NULL)
return fail("open %s directory %s: %s", which, path, strerror(errno)); return fail("open %s: %s", path, strerror(errno));
count = 0; empty = 1;
ok = 1;
errno = 0; errno = 0;
while((de = readdir(dir)) != NULL){ while((de = readdir(dir)) != NULL)
if(strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) if(strcmp(de->d_name, ".") != 0 && strcmp(de->d_name, "..") != 0){
continue; fail("unexpected endpoint %s/%s", path, de->d_name);
count++; empty = 0;
fail("unexpected %s directory entry %s", which, de->d_name); }
ok = 0;
errno = 0;
}
if(errno != 0){ if(errno != 0){
fail("read %s directory %s: %s", which, path, strerror(errno)); fail("read %s: %s", path, strerror(errno));
ok = 0; empty = 0;
} }
if(closedir(dir) < 0){ if(closedir(dir) < 0){
fail("close %s directory %s: %s", which, path, strerror(errno)); fail("close %s: %s", path, strerror(errno));
ok = 0; empty = 0;
} }
if(count != 0) return empty;
ok = 0;
return ok;
} }
static int static int
checkendpoints(Test *t) checkendpoints(Live *l)
{ {
struct stat st; struct stat st;
int ok;
ok = 1; if(lstat(l->socket, &st) == 0)
errno = 0; return fail("IPC endpoint exists after failed startup: %s", l->socket);
if(lstat(t->socket, &st) == 0){ if(errno != ENOENT)
fail("IPC endpoint exists after failed startup: %s", t->socket); return fail("lstat IPC endpoint: %s", strerror(errno));
ok = 0; return emptydir(l->runtime) && emptydir(l->bus);
}else if(errno != ENOENT){
fail("lstat failed IPC endpoint: %s", strerror(errno));
ok = 0;
}
if(!dirempty(t->runtime, "runtime"))
ok = 0;
if(!dirempty(t->bus, "IBus"))
ok = 0;
return ok && t->endpointclean;
}
static int clearfd(int);
static int
removeentry(int dirfd, char *name)
{
struct stat st;
int fd, ok;
if(fstatat(dirfd, name, &st, AT_SYMLINK_NOFOLLOW) < 0)
return fail("stat cleanup entry %s: %s", name, strerror(errno));
if(!S_ISDIR(st.st_mode)){
if(unlinkat(dirfd, name, 0) == 0 || errno == ENOENT)
return 1;
return fail("remove cleanup entry %s: %s", name, strerror(errno));
}
fd = openat(dirfd, name, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW);
if(fd < 0)
return fail("open cleanup directory %s: %s", name, strerror(errno));
ok = clearfd(fd);
if(close(fd) < 0){
fail("close cleanup directory %s: %s", name, strerror(errno));
ok = 0;
}
if(unlinkat(dirfd, name, AT_REMOVEDIR) < 0 && errno != ENOENT){
fail("remove cleanup directory %s: %s", name, strerror(errno));
ok = 0;
}
return ok;
}
static int
clearfd(int fd)
{
DIR *dir;
struct dirent *de;
int copy, ok;
copy = dup(fd);
if(copy < 0)
return fail("duplicate cleanup directory: %s", strerror(errno));
dir = fdopendir(copy);
if(dir == NULL){
close(copy);
return fail("open cleanup directory stream: %s", strerror(errno));
}
ok = 1;
errno = 0;
while((de = readdir(dir)) != NULL){
if(strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0)
continue;
if(!removeentry(fd, de->d_name))
ok = 0;
errno = 0;
}
if(errno != 0){
fail("read cleanup directory: %s", strerror(errno));
ok = 0;
}
if(closedir(dir) < 0){
fail("close cleanup directory stream: %s", strerror(errno));
ok = 0;
}
return ok;
}
static int
removeroot(Test *t)
{
int fd, ok;
if(t->root[0] == '\0')
return 1;
fd = open(t->root, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW);
if(fd < 0)
return errno == ENOENT || fail("open cleanup root %s: %s", t->root,
strerror(errno));
ok = clearfd(fd);
if(close(fd) < 0){
fail("close cleanup root %s: %s", t->root, strerror(errno));
ok = 0;
}
if(rmdir(t->root) < 0 && errno != ENOENT){
fail("rmdir %s: %s", t->root, strerror(errno));
ok = 0;
}
return ok;
}
static int
cleanup(Test *t)
{
int n, ok, status;
ok = 1;
if(t->child > 0){
do
n = waitpid(t->child, &status, WNOHANG);
while(n < 0 && errno == EINTR);
if(n == t->child)
t->child = -1;
else if(n == 0){
fail("failed-start daemon still running during cleanup");
ok = 0;
if(!killowned(t, &status))
ok = 0;
}else{
fail("check failed-start daemon during cleanup: %s",
strerror(errno));
ok = 0;
if(errno == ECHILD)
t->child = -1;
else if(!killowned(t, &status))
ok = 0;
}
}
readerrors(t);
if(t->errfd >= 0){
if(close(t->errfd) < 0){
fail("close daemon stderr: %s", strerror(errno));
ok = 0;
}
t->errfd = -1;
}
if(t->notifyfd >= 0){
if(t->runtimewd >= 0 &&
inotify_rm_watch(t->notifyfd, t->runtimewd) < 0 && errno != EINVAL){
fail("remove runtime watch: %s", strerror(errno));
ok = 0;
}
if(t->buswd >= 0 &&
inotify_rm_watch(t->notifyfd, t->buswd) < 0 && errno != EINVAL){
fail("remove IBus watch: %s", strerror(errno));
ok = 0;
}
if(close(t->notifyfd) < 0){
fail("close endpoint watches: %s", strerror(errno));
ok = 0;
}
t->notifyfd = -1;
}
if(!removeroot(t))
ok = 0;
return ok;
} }
int int
main(int argc, char **argv) main(int argc, char **argv)
{ {
Test test; Daemon daemon;
Live live;
char badmap[320];
int ok; int ok;
testname = "daemon_failure_test";
if(argc != 2){ if(argc != 2){
fprintf(stderr, "usage: daemon_failure_test strans\n"); fprintf(stderr, "usage: daemon_failure_test strans\n");
return 2; return 2;
} }
ok = setup(&test); 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) if(ok)
ok = startchild(&test, argv[1]); ok = startdaemon(&live, &daemon, argv[1], badmap);
if(ok){ if(ok)
ok = waitfailed(&test); ok = waitfailed(&daemon, badmap) && checkendpoints(&live);
if(test.child <= 0 && !checkendpoints(&test)) if(!killdaemon(&daemon, NULL))
ok = 0; ok = 0;
} if(!closeerrors(&daemon))
if(!cleanup(&test)) ok = 0;
if(!liveclean(&live))
ok = 0; ok = 0;
if(!ok){ if(!ok){
showerrors(&test); showerrors(&daemon);
return 1; return 1;
} }
printf("failed daemon startup published no endpoints: ok\n"); printf("failed daemon startup leaves no endpoints: ok\n");
return 0; return 0;
} }

File diff suppressed because it is too large Load Diff

2
tests/data/compose Normal file
View File

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

View File

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

View File

@@ -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,78 +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 void
dictionary_emoji_identity(struct ct *t) dictionary_prefix(struct ct *t)
{ {
Dictreq req; Str kouho[Maxkouho], key;
Dictres res; Trie *dict;
Hmap *saved;
Lang *lang;
Str key;
lang = getlang(LangEMOJI); dict = trienew();
saved = lang->dict; trieput(dict, "smile", 5, "A B", 3);
lang->dict = hmapalloc(2); trieput(dict, "smiley", 6, "B C", 3);
key = mkstr("é"); trieput(dict, "sad", 3, "D", 1);
hmapset(&lang->dict, &key, "é", strlen("é")); key = mkstr("smile");
memset(&req, 0, sizeof req); if(CT_EQ_INT(t, 4, dictprefix(dict, &key, kouho, Maxkouho))){
req.key = key; checkstr(t, "own entry first", "A", &kouho[0]);
req.pre = key; checkstr(t, "entry below", "C", &kouho[3]);
req.lang = LangEMOJI; }
dictlookup(&req, &res); key = mkstr("s");
if(CT_EQ_INT(t, 1, res.nkouho)) CT_EQ_INT(t, 5, dictprefix(dict, &key, kouho, Maxkouho));
checkstr(t, "emoji identity candidate", "é", &res.kouho[0]); CT_EQ_INT(t, 2, dictprefix(dict, &key, kouho, 2));
hmapfree(lang->dict); key = mkstr("x");
lang->dict = saved; CT_EQ_INT(t, 0, dictprefix(dict, &key, kouho, Maxkouho));
trieclose(dict);
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,18 @@
#include "dat.h"
#include "fn.h"
#include "test.h" #include "test.h"
enum enum
{ {
Guard = 8, Guard = 8,
Testw = 3 * Fontsz, Rgbmask = 0xffffff,
Testh = 3 * Fontsz,
Testn = Testw * Testh,
Missingn = Fontsz * Fontsz,
}; };
#define Testw (6 * Fontsz)
#define Testh (3 * Fontsz)
#define Testn (Testw * Testh)
static u32int guardcolor = 0x5a5a5a5a; static u32int guardcolor = 0x5a5a5a5a;
static char *testfonts[] = {
"/usr/share/fonts/TTF/DejaVuSans.ttf",
"/usr/share/fonts/TTF/Jigmo.ttf",
"/usr/share/fonts/TTF/Jigmo2.ttf",
};
static char *missingfonts[] = {
"/strans-test-no-such-font",
};
static void static void
fillpixels(u32int *p, int n, u32int color) fillpixels(u32int *p, int n, u32int color)
@@ -29,38 +24,84 @@ fillpixels(u32int *p, int n, u32int color)
} }
static int static int
hasink(u32int *p, int n) hasink(u32int *p, int n, u32int bg)
{ {
int i; int i;
bg &= Rgbmask;
for(i = 0; i < n; i++) for(i = 0; i < n; i++)
if(p[i] != Colbg) if((p[i] & Rgbmask) != bg)
return 1; return 1;
return 0; return 0;
} }
static int static int
renders(Rune r) hascolors(u32int *p, int n, u32int bg)
{ {
u32int buf[Fontsz * Fontsz]; u32int c, first;
int b, found, g, i, r;
fillpixels(buf, nelem(buf), Colbg); bg &= Rgbmask;
putfont(buf, Fontsz, Fontsz, 0, 0, r); first = 0;
return hasink(buf, nelem(buf)); 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 static int
inkbounds(u32int *p, int *minx, int *miny, int *maxx, int *maxy) inkbounds(u32int *p, u32int bg, int *minx, int *miny, int *maxx, int *maxy)
{ {
int x, y; int x, y;
bg &= Rgbmask;
*minx = Testw; *minx = Testw;
*miny = Testh; *miny = Testh;
*maxx = -1; *maxx = -1;
*maxy = -1; *maxy = -1;
for(y = 0; y < Testh; y++){ for(y = 0; y < Testh; y++){
for(x = 0; x < Testw; x++){ for(x = 0; x < Testw; x++){
if(p[y * Testw + x] == Colbg) if((p[y * Testw + x] & Rgbmask) == bg)
continue; continue;
*minx = min(*minx, x); *minx = min(*minx, x);
*miny = min(*miny, y); *miny = min(*miny, y);
@@ -72,86 +113,145 @@ inkbounds(u32int *p, int *minx, int *miny, int *maxx, int *maxy)
} }
static void static void
checkclip(struct ct *t, u32int *ref, int dx, int dy) checktext(struct ct *t, u32int *buf, char *utf)
{ {
u32int *buf, *mem, *want; Str s;
int i, sx, sy, x, y; 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;
mem = emalloc((Testn + 2 * Guard) * sizeof mem[0]);
want = emalloc(Testn * sizeof want[0]);
fillpixels(mem, Testn + 2 * Guard, guardcolor);
buf = mem + Guard; buf = mem + Guard;
fillpixels(buf, Testn, Colbg); fillpixels(buf, Testn, Colbg);
fillpixels(want, Testn, Colbg); textdraw(buf, Testw, Testh, x, y, Testw, Colfg, s);
for(y = 0; y < Testh; y++){ CT_CHECK(t, hasink(buf, Testn, Colbg));
for(x = 0; x < Testw; x++){ checkoutside(t, buf, Colbg, y);
sx = x - dx; checkguards(t, mem);
sy = y - dy;
if(sx >= 0 && sx < Testw && sy >= 0 && sy < Testh)
want[y * Testw + x] = ref[sy * Testw + sx];
}
}
putfont(buf, Testw, Testh, Fontsz + dx, Fontsz + dy, 'A');
CT_CHECK(t, hasink(buf, Testn));
CT_EQ_MEM(t, want, buf, Testn * sizeof buf[0]);
for(i = 0; i < Guard; i++){
CT_EQ_UINT(t, guardcolor, mem[i]);
CT_EQ_UINT(t, guardcolor, mem[Guard + Testn + i]);
}
free(want);
free(mem);
} }
void void
font_render(struct ct *t) font_render(struct ct *t)
{ {
u32int *buf, *missing, *ref; static char *plain[] = { "A", "", "", "", "", "𠀋" };
int i, minx, miny, maxx, maxy; u32int *buf, *mem;
Str a, heart, longrow, missing;
int i, maxx, maxy, minx, miny, natural, w, x, y;
missing = emalloc((Missingn + 2 * Guard) * sizeof missing[0]); mem = emalloc((Testn + 2 * Guard) * sizeof mem[0]);
fillpixels(missing, Missingn + 2 * Guard, guardcolor); fillpixels(mem, Testn + 2 * Guard, guardcolor);
buf = missing + Guard; buf = mem + Guard;
fillpixels(buf, Missingn, Colbg); fillpixels(buf, Testn, Colbg);
CT_CHECK(t, !fontinit(missingfonts, nelem(missingfonts))); a = mkstr("A");
putfont(buf, Fontsz, Fontsz, 0, 0, 'A'); textinit();
for(i = 0; i < Missingn; i++) for(i = 0; i < nelem(plain); i++)
CT_EQ_UINT(t, Colbg, buf[i]); checktext(t, buf, plain[i]);
for(i = 0; i < Guard; i++){
CT_EQ_UINT(t, guardcolor, missing[i]);
CT_EQ_UINT(t, guardcolor, missing[Guard + Missingn + i]);
}
if(!CT_CHECK(t, fontinit(testfonts, nelem(testfonts)))){ checkcolor(t, buf, "😀");
free(missing); checkcolor(t, buf, "❤️");
return; checkcolor(t, buf, "☕️");
} checkshape(t, buf, "👩‍💻");
CT_CHECK(t, renders('A')); checkshape(t, buf, "👍🏽");
CT_CHECK(t, renders(0xac00)); checkshape(t, buf, "🇰🇷");
CT_CHECK(t, renders(0x4e00)); checkshape(t, buf, "☕︎");
CT_CHECK(t, renders(0x1f600));
CT_CHECK(t, renders(0x2000b));
fillpixels(missing, Missingn + 2 * Guard, guardcolor);
buf = missing + Guard;
fillpixels(buf, Missingn, Colbg);
putfont(buf, Fontsz, Fontsz, 0, 0, 0x10ffff);
for(i = 0; i < Missingn; i++)
CT_EQ_UINT(t, Colbg, buf[i]);
for(i = 0; i < Guard; i++){
CT_EQ_UINT(t, guardcolor, missing[i]);
CT_EQ_UINT(t, guardcolor, missing[Guard + Missingn + i]);
}
free(missing);
ref = emalloc(Testn * sizeof ref[0]); heart = mkstr("❤️");
fillpixels(ref, Testn, Colbg); fillpixels(buf, Testn, Colsel);
putfont(ref, Testw, Testh, Fontsz, Fontsz, 'A'); textdraw(buf, Testw, Testh, Fontsz, Fontsz, Testw, Colfg, &heart);
if(!CT_CHECK(t, inkbounds(ref, &minx, &miny, &maxx, &maxy))){ CT_CHECK(t, hascolors(buf, Testn, Colsel));
free(ref); CT_EQ_UINT(t, Colsel,
return; 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);
} }
checkclip(t, ref, -minx - 1, 0); CT_EQ_INT(t, natural, textwidth(&longrow));
checkclip(t, ref, Testw - maxx, 0);
checkclip(t, ref, 0, -miny - 1); fillpixels(buf, Testn, Colsel);
checkclip(t, ref, 0, Testh - maxy); textdraw(buf, Testw, Testh, 2*Fontsz, Fontsz, 2*Fontsz, Colselfg,
free(ref); &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,112 +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);
}
void
hmap_binary_keys_and_invalid_lengths(struct ct *t)
{
Hmap *h;
Hnode *n;
Str key, other;
h = hmapalloc(1);
CT_CHECK(t, h != nil);
key.n = 3;
key.r[0] = 'a';
key.r[1] = 0;
key.r[2] = 'b';
other = key;
other.r[2] = 'c';
hmapset(&h, &key, "one", 3);
hmapset(&h, &other, "two", 3);
n = hmapget(h, &key);
if(CT_CHECK(t, n != nil)){
CT_EQ_INT(t, 3, n->klen);
CT_EQ_MEM(t, "one", n->val, n->vlen);
}
n = hmapget(h, &other);
if(CT_CHECK(t, n != nil))
CT_EQ_MEM(t, "two", n->val, n->vlen);
hmapset(&h, &key, "bad", -1);
hmapset(&h, &key, nil, 1);
n = hmapget(h, &key);
if(CT_CHECK(t, n != nil))
CT_EQ_MEM(t, "one", n->val, n->vlen);
CT_EQ_PTR(t, nil, hmapalloc(0));
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;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,8 @@
#include "../ibus.c" #include "ibus.c"
#include "../cutest/cutest.h" #include "test.h"
#include <errno.h> #include <errno.h>
void testengineinit(int);
void testenginehandle(Keyreq*);
void* testengineowner(void);
void testenginepreedit(Str*);
void testenginecaret(Caret*);
enum enum
{ {
Testctrlmask = 1<<2, Testctrlmask = 1<<2,
@@ -18,20 +12,19 @@ typedef struct Ibusfix Ibusfix;
struct Ibusfix struct Ibusfix
{ {
Channel *oldreply; Channel *oldreply;
Channel *trace; Pump pump;
Channel *stop; /* A fake connection is nothing but an address to tell two apart. */
Channel *done; char fake[2];
DBusConnection *c1; DBusConnection *c1;
DBusConnection *c2; DBusConnection *c2;
int pumpactive;
}; };
void void
ibus_machine_id_fallback(struct ct *t) ibus_machine_id_fallback(struct ct *t)
{ {
char root[] = "/tmp/strans-machine-id.XXXXXX"; char root[] = "/tmp/strans-machine-id.XXXXXX";
char primary[128], fallback[128], got[64]; char primary[128], fallback[128], override[128], got[512];
char *path[2]; char *path[2], *old, *saved;
FILE *fp; FILE *fp;
int madeprimary, madefallback; int madeprimary, madefallback;
@@ -61,6 +54,27 @@ ibus_machine_id_fallback(struct ct *t)
path[1] = fallback; path[1] = fallback;
machineidfiles(got, sizeof got, path, nelem(path)); machineidfiles(got, sizeof got, path, nelem(path));
CT_EQ_STR(t, "fallback-machine-id", got); CT_EQ_STR(t, "fallback-machine-id", got);
path[1] = primary;
machineidfiles(got, sizeof got, path, nelem(path));
CT_EQ_STR(t, "", got);
old = getenv("IBUS_ADDRESS_FILE");
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: cleanup:
if(madefallback) if(madefallback)
CT_CHECK(t, unlink(fallback) == 0); CT_CHECK(t, unlink(fallback) == 0);
@@ -69,111 +83,8 @@ cleanup:
CT_CHECK(t, rmdir(root) == 0); CT_CHECK(t, rmdir(root) == 0);
} }
void
ibus_startup_requires_ownership(struct ct *t)
{
char root[] = "/tmp/strans-ibus-startup.XXXXXX";
char busdir[128], ibusdir[128], path[512], *old, *saved;
struct stat st;
int i, made, rv;
made = 0;
busdir[0] = '\0';
ibusdir[0] = '\0';
path[0] = '\0';
old = getenv("XDG_CONFIG_HOME");
saved = old == nil ? nil : strdup(old);
if(old != nil && !CT_CHECK(t, saved != nil))
return;
if(!CT_CHECK(t, mkdtemp(root) != nil))
goto cleanup;
made = 1;
if(!CT_CHECK(t, snprintf(busdir, sizeof busdir, "%s/ibus/bus", root)
< (int)sizeof busdir) ||
!CT_CHECK(t, snprintf(ibusdir, sizeof ibusdir, "%s/ibus", root)
< (int)sizeof ibusdir) ||
!CT_CHECK(t, setenv("XDG_CONFIG_HOME", root, 1) == 0) ||
!CT_CHECK(t, buildaddrpath(path, sizeof path) == 0))
goto cleanup;
memset(watches, 0, sizeof watches);
for(i = 0; i < nelem(watches); i++)
watches[i] = (DBusWatch*)1;
nwatches = nelem(watches);
rv = ibusinit();
CT_EQ_INT(t, -1, rv);
CT_EQ_PTR(t, nil, srv);
CT_CHECK(t, lstat(path, &st) < 0 && errno == ENOENT);
if(rv == 0){
dbus_server_disconnect(srv);
dbus_server_unref(srv);
srv = nil;
unlinkaddr();
atexitdont(unlinkaddr);
}
memset(watches, 0, sizeof watches);
nwatches = 0;
while(atexit(unlinkaddr))
continue;
CT_EQ_INT(t, -1, ibusinit());
CT_EQ_PTR(t, nil, srv);
CT_CHECK(t, lstat(path, &st) < 0 && errno == ENOENT);
cleanup:
if(srv != nil){
dbus_server_disconnect(srv);
dbus_server_unref(srv);
srv = nil;
}
unlinkaddr();
atexitdont(unlinkaddr);
memset(watches, 0, sizeof watches);
nwatches = 0;
addrfile[0] = '\0';
if(saved != nil){
setenv("XDG_CONFIG_HOME", saved, 1);
free(saved);
}else
unsetenv("XDG_CONFIG_HOME");
if(made){
if(path[0] != '\0')
unlink(path);
if(busdir[0] != '\0')
CT_CHECK(t, rmdir(busdir) == 0 || errno == ENOENT);
if(ibusdir[0] != '\0')
CT_CHECK(t, rmdir(ibusdir) == 0 || errno == ENOENT);
CT_CHECK(t, rmdir(root) == 0);
}
}
static void static void
enginepump(void *arg) ibusbegin(Ibusfix *f)
{
Ibusfix *f;
Keyreq req;
uchar token;
Alt alts[] = {
{keyc, &req, CHANRCV, nil},
{nil, &token, CHANRCV, nil},
{nil, nil, CHANEND, nil},
};
f = arg;
alts[1].c = f->stop;
for(;;)
switch(alt(alts)){
case 0:
chansend(f->trace, &req);
testenginehandle(&req);
break;
case 1:
chansend(f->done, &token);
return;
}
}
static int
ibusbegin(struct ct *t, Ibusfix *f)
{ {
Drawcmd dc; Drawcmd dc;
@@ -181,51 +92,36 @@ ibusbegin(struct ct *t, Ibusfix *f)
memset(contexts, 0, sizeof contexts); memset(contexts, 0, sizeof contexts);
memset(conns, 0, sizeof conns); memset(conns, 0, sizeof conns);
nconns = 0; nconns = 0;
preowner = nil;
while(channbrecv(drawc, &dc) > 0) while(channbrecv(drawc, &dc) > 0)
; ;
testengineinit(LangEN); testengineinit(LangEN);
f->oldreply = replyc; f->oldreply = replyc;
replyc = chancreate(sizeof(Keyres), 0); replyc = chancreate(sizeof(Keyres), 0);
f->trace = chancreate(sizeof(Keyreq), Maxcontexts); pumpstart(&f->pump, Maxcontexts);
f->stop = chancreate(sizeof(uchar), 0); f->c1 = (DBusConnection*)&f->fake[0];
f->done = chancreate(sizeof(uchar), 0); f->c2 = (DBusConnection*)&f->fake[1];
f->pumpactive = threadcreate(enginepump, f, 8192) >= 0;
f->c1 = malloc(1);
f->c2 = malloc(1);
return CT_CHECK(t, f->pumpactive && f->c1 != nil && f->c2 != nil);
} }
static void static void
ibusend(Ibusfix *f) ibusend(Ibusfix *f)
{ {
Drawcmd dc; Drawcmd dc;
Keyreq req;
uchar token;
int i; int i;
if(f->pumpactive){ for(i = 0; i < nelem(contexts); i++)
for(i = 0; i < nelem(contexts); i++) if(contexts[i].conn != nil)
if(contexts[i].conn != nil) dropcontext(&contexts[i]);
dropcontext(&contexts[i]); pumpstop(&f->pump);
while(channbrecv(f->trace, &req) > 0)
;
token = 0;
chansend(f->stop, &token);
chanrecv(f->done, &token);
}
memset(contexts, 0, sizeof contexts); memset(contexts, 0, sizeof contexts);
memset(conns, 0, sizeof conns); memset(conns, 0, sizeof conns);
nconns = 0; nconns = 0;
preowner = nil;
testengineinit(LangEN); testengineinit(LangEN);
while(channbrecv(drawc, &dc) > 0) while(channbrecv(drawc, &dc) > 0)
; ;
free(f->c1);
free(f->c2);
chanfree(replyc); chanfree(replyc);
replyc = f->oldreply; replyc = f->oldreply;
chanfree(f->trace);
chanfree(f->stop);
chanfree(f->done);
} }
static Keyreq static Keyreq
@@ -234,10 +130,12 @@ nexttrace(struct ct *t, Ibusfix *f, int op, Ictx *ctx)
Keyreq req; Keyreq req;
memset(&req, 0, sizeof req); memset(&req, 0, sizeof req);
if(!CT_CHECK(t, channbrecv(f->trace, &req) > 0)) if(!CT_CHECK(t, channbrecv(f->pump.trace, &req) > 0))
return req; return req;
CT_EQ_INT(t, op, req.op); CT_EQ_INT(t, op, req.op);
CT_EQ_PTR(t, ctx, req.owner); CT_EQ_PTR(t, ctx, req.owner);
if(ctx != nil)
CT_EQ_INT(t, clientpreedit(ctx), req.clientpre);
CT_EQ_PTR(t, replyc, req.reply); CT_EQ_PTR(t, replyc, req.reply);
return req; return req;
} }
@@ -247,40 +145,122 @@ notrace(struct ct *t, Ibusfix *f)
{ {
Keyreq req; Keyreq req;
CT_CHECK(t, channbrecv(f->trace, &req) <= 0); CT_CHECK(t, channbrecv(f->pump.trace, &req) <= 0);
}
static void
checkpreedit(struct ct *t, char *want, Keyres *res)
{
char got[Maxutf];
stoutf(&res->preedit, got, sizeof got);
CT_EQ_STR(t, want, got);
}
static void
checkenginepreedit(struct ct *t, char *want)
{
char got[Maxutf];
Str preedit;
testenginepreedit(&preedit);
stoutf(&preedit, got, sizeof got);
CT_EQ_STR(t, want, got);
} }
static Keyres static Keyres
contextkey(struct ct *t, Ibusfix *f, Ictx *ctx, u32int sym, u32int state) contextkey(struct ct *t, Ibusfix *f, Ictx *ctx, u32int sym, u32int state)
{ {
Keyres res; Keyres res;
char text[Maxutf];
memset(&res, 0, sizeof res); memset(&res, 0, sizeof res);
CT_CHECK(t, processkey(ctx, sym, state, &res)); CT_CHECK(t, processkey(ctx, sym, state, text, sizeof text, &res));
nexttrace(t, f, Keypress, ctx); nexttrace(t, f, Keypress, ctx);
return res; return res;
} }
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 void
ibus_context_lifecycle(struct ct *t) ibus_context_lifecycle(struct ct *t)
{ {
@@ -289,9 +269,9 @@ ibus_context_lifecycle(struct ct *t)
Keyreq req; Keyreq req;
Keyres res; Keyres res;
Caret at; Caret at;
char text[Maxutf];
if(!ibusbegin(t, &f)) ibusbegin(&f);
goto cleanup;
a = newcontext(f.c1, "/context/one"); a = newcontext(f.c1, "/context/one");
b = newcontext(f.c2, "/context/one"); b = newcontext(f.c2, "/context/one");
if(!CT_CHECK(t, a != nil && b != nil && a != b)) if(!CT_CHECK(t, a != nil && b != nil && a != b))
@@ -300,44 +280,40 @@ ibus_context_lifecycle(struct ct *t)
CT_EQ_PTR(t, b, findcontext(f.c2, "/context/one")); CT_EQ_PTR(t, b, findcontext(f.c2, "/context/one"));
CT_EQ_PTR(t, nil, findcontext(f.c1, "/context/missing")); CT_EQ_PTR(t, nil, findcontext(f.c1, "/context/missing"));
memset(&res, 0, sizeof res); setcursor(a, -10, -20, 14);
CT_CHECK(t, !processkey(a, 'x', 0, &res));
CT_CHECK(t, !res.eaten);
notrace(t, &f);
CT_EQ_PTR(t, nil, testengineowner());
setcursor(a, 10, 20, 14);
notrace(t, &f); notrace(t, &f);
CT_CHECK(t, a->caret.valid); CT_CHECK(t, a->caret.valid);
a->focused = 1;
memset(&res, 0, sizeof res); memset(&res, 0, sizeof res);
CT_CHECK(t, !processkey(a, 'x', Relmask, &res)); CT_CHECK(t, !processkey(a, 'x', Relmask, text, sizeof text, &res));
CT_CHECK(t, !res.eaten); CT_CHECK(t, !res.eaten);
notrace(t, &f); notrace(t, &f);
CT_CHECK(t, !a->focused);
CT_EQ_PTR(t, nil, testengineowner()); CT_EQ_PTR(t, nil, testengineowner());
/* A key stands for the FocusIn a client may never send. */
memset(&res, 0, sizeof res); memset(&res, 0, sizeof res);
CT_CHECK(t, processkey(a, 'x', 0, &res)); CT_CHECK(t, processkey(a, 'x', 0, text, sizeof text, &res));
req = nexttrace(t, &f, Keypress, a); req = nexttrace(t, &f, Keypress, a);
CT_CHECK(t, a->focused);
CT_CHECK(t, !res.eaten); CT_CHECK(t, !res.eaten);
CT_CHECK(t, req.caret.valid); CT_CHECK(t, req.caret.valid);
CT_EQ_INT(t, 10, req.caret.x); CT_EQ_INT(t, -10, req.caret.x);
CT_EQ_INT(t, 20, req.caret.y); CT_EQ_INT(t, -20, req.caret.y);
CT_EQ_INT(t, 14, req.caret.h); CT_EQ_INT(t, 14, req.caret.h);
CT_EQ_PTR(t, a, testengineowner()); CT_EQ_PTR(t, a, testengineowner());
testenginecaret(&at); testenginecaret(&at);
CT_CHECK(t, at.valid); CT_CHECK(t, at.valid);
CT_EQ_INT(t, 10, at.x); CT_EQ_INT(t, -10, at.x);
CT_EQ_INT(t, 20, at.y); CT_EQ_INT(t, -20, at.y);
res = contextkey(t, &f, a, 'n', Testctrlmask); res = contextkey(t, &f, a, 'n', Testctrlmask);
CT_CHECK(t, res.eaten); CT_CHECK(t, res.eaten);
contextkey(t, &f, a, 'k', 0); contextkey(t, &f, a, 'k', 0);
res = contextkey(t, &f, a, 'a', 0); res = contextkey(t, &f, a, 'a', 0);
checkpreedit(t, "", &res); checkstr(t, "preedit", "", &res.preedit);
b->focused = 1; b->focused = 1;
res = contextkey(t, &f, b, 'n', 0); res = contextkey(t, &f, b, 'n', 0);
checkpreedit(t, "", &res); checkstr(t, "preedit", "", &res.preedit);
CT_EQ_PTR(t, b, testengineowner()); CT_EQ_PTR(t, b, testengineowner());
setcursor(b, 50, 60, 12); setcursor(b, 50, 60, 12);
@@ -351,7 +327,7 @@ ibus_context_lifecycle(struct ct *t)
memset(&res, 0, sizeof res); memset(&res, 0, sizeof res);
sendrequest(a, Keyreset, 0, 0, &res); sendrequest(a, Keyreset, 0, 0, &res);
nexttrace(t, &f, Keyreset, a); nexttrace(t, &f, Keyreset, a);
checkpreedit(t, "", &res); checkstr(t, "preedit", "", &res.preedit);
checkenginepreedit(t, ""); checkenginepreedit(t, "");
CT_EQ_PTR(t, b, testengineowner()); CT_EQ_PTR(t, b, testengineowner());
@@ -367,11 +343,7 @@ ibus_context_lifecycle(struct ct *t)
CT_CHECK(t, !a->caret.valid); CT_CHECK(t, !a->caret.valid);
checkenginepreedit(t, ""); checkenginepreedit(t, "");
CT_EQ_PTR(t, b, testengineowner()); CT_EQ_PTR(t, b, testengineowner());
releasecontext(a); checkdrop(t, &f, a, "/context/one");
notrace(t, &f);
dropcontext(a);
notrace(t, &f);
CT_EQ_PTR(t, nil, findcontext(f.c1, "/context/one"));
reused = newcontext(f.c1, "/context/stale-destroy"); reused = newcontext(f.c1, "/context/stale-destroy");
CT_EQ_PTR(t, a, reused); CT_EQ_PTR(t, a, reused);
@@ -396,7 +368,7 @@ ibus_context_lifecycle(struct ct *t)
notrace(t, &f); notrace(t, &f);
memset(&res, 0, sizeof res); memset(&res, 0, sizeof res);
CT_CHECK(t, !processkey(b, 'x', Relmask, &res)); CT_CHECK(t, !processkey(b, 'x', Relmask, text, sizeof text, &res));
CT_CHECK(t, !res.eaten); CT_CHECK(t, !res.eaten);
notrace(t, &f); notrace(t, &f);
CT_EQ_PTR(t, b, testengineowner()); CT_EQ_PTR(t, b, testengineowner());
@@ -411,25 +383,24 @@ ibus_active_release_lifecycle(struct ct *t)
Ictx *ctx, *reused; Ictx *ctx, *reused;
Keyres res; Keyres res;
if(!ibusbegin(t, &f)) ibusbegin(&f);
goto cleanup;
ctx = newcontext(f.c1, "/context/reset"); ctx = newcontext(f.c1, "/context/reset");
if(!CT_CHECK(t, ctx != nil)) if(!CT_CHECK(t, ctx != nil))
goto cleanup; goto cleanup;
ctx->focused = 1; ctx->focused = 1;
contextkey(t, &f, ctx, 'n', Testctrlmask); contextkey(t, &f, ctx, 'n', Testctrlmask);
res = contextkey(t, &f, ctx, 'n', 0); res = contextkey(t, &f, ctx, 'n', 0);
checkpreedit(t, "", &res); checkstr(t, "preedit", "", &res.preedit);
memset(&res, 0, sizeof res); memset(&res, 0, sizeof res);
sendrequest(ctx, Keyreset, 0, 0, &res); sendrequest(ctx, Keyreset, 0, 0, &res);
nexttrace(t, &f, Keyreset, ctx); nexttrace(t, &f, Keyreset, ctx);
checkpreedit(t, "", &res); checkstr(t, "preedit", "", &res.preedit);
checkenginepreedit(t, ""); checkenginepreedit(t, "");
CT_CHECK(t, ctx->focused); CT_CHECK(t, ctx->focused);
CT_EQ_PTR(t, ctx, testengineowner()); CT_EQ_PTR(t, ctx, testengineowner());
res = contextkey(t, &f, ctx, 'k', 0); res = contextkey(t, &f, ctx, 'k', 0);
checkpreedit(t, "k", &res); checkstr(t, "preedit", "k", &res.preedit);
CT_EQ_PTR(t, ctx, testengineowner()); CT_EQ_PTR(t, ctx, testengineowner());
releasecontext(ctx); releasecontext(ctx);
@@ -437,11 +408,7 @@ ibus_active_release_lifecycle(struct ct *t)
CT_CHECK(t, !ctx->focused); CT_CHECK(t, !ctx->focused);
checkenginepreedit(t, ""); checkenginepreedit(t, "");
CT_EQ_PTR(t, nil, testengineowner()); CT_EQ_PTR(t, nil, testengineowner());
releasecontext(ctx); checkdrop(t, &f, ctx, "/context/reset");
notrace(t, &f);
dropcontext(ctx);
notrace(t, &f);
CT_EQ_PTR(t, nil, findcontext(f.c1, "/context/reset"));
reused = newcontext(f.c1, "/context/destroy"); reused = newcontext(f.c1, "/context/destroy");
CT_EQ_PTR(t, ctx, reused); CT_EQ_PTR(t, ctx, reused);
@@ -457,12 +424,8 @@ ibus_active_release_lifecycle(struct ct *t)
reused = newcontext(f.c1, "/context/prune"); reused = newcontext(f.c1, "/context/prune");
CT_EQ_PTR(t, ctx, reused); CT_EQ_PTR(t, ctx, reused);
CT_EQ_PTR(t, nil, testengineowner()); CT_EQ_PTR(t, nil, testengineowner());
memset(&res, 0, sizeof res);
CT_CHECK(t, !processkey(reused, 'x', 0, &res));
CT_CHECK(t, !res.eaten);
notrace(t, &f);
reused->focused = 1;
contextkey(t, &f, reused, 'n', 0); contextkey(t, &f, reused, 'n', 0);
CT_CHECK(t, reused->focused);
CT_EQ_PTR(t, reused, testengineowner()); CT_EQ_PTR(t, reused, testengineowner());
dropconncontexts(f.c1); dropconncontexts(f.c1);
nexttrace(t, &f, Keyrelease, reused); nexttrace(t, &f, Keyrelease, reused);

View File

@@ -1,538 +1,33 @@
#define _GNU_SOURCE #define _GNU_SOURCE
#include <dirent.h>
#include <errno.h> #include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <poll.h> #include <poll.h>
#include <signal.h>
#include <stdarg.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/inotify.h>
#include <sys/socket.h> #include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h> #include <sys/types.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h> #include <unistd.h>
#include "../ipc.h" #include "ipc.h"
#include "live.h"
enum enum
{ {
Maxclients = 64, Maxclients = 64,
Calltimeout = 4000, Cappreedit = 1,
Starttimeout = 8000,
Stoptimeout = 3000,
}; };
typedef struct Daemon Daemon;
typedef struct Response Response; typedef struct Response Response;
struct Daemon
{
pid_t pid;
int errfd;
char root[256];
char runtime[320];
char config[320];
char ibus[384];
char bus[448];
char home[320];
char socket[384];
char err[4096];
size_t nerr;
};
struct Response struct Response
{ {
int eaten; int eaten;
int del;
char commit[Ipcfieldmax+1]; char commit[Ipcfieldmax+1];
char preedit[Ipcfieldmax+1]; char preedit[Ipcfieldmax+1];
}; };
static int
fail(char *fmt, ...)
{
va_list ap;
fprintf(stderr, "ipc_live_test: ");
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fputc('\n', stderr);
return 0;
}
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 int
leftms(int64_t deadline)
{
int64_t n;
n = deadline - nowms();
if(n <= 0)
return 0;
if(n > 0x7fffffff)
return 0x7fffffff;
return n;
}
static int
makedir(char *path)
{
if(mkdir(path, 0700) == 0)
return 1;
return fail("mkdir %s: %s", path, strerror(errno));
}
static 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';
}
static void
showerrors(Daemon *d)
{
readerrors(d);
if(d->nerr != 0)
fprintf(stderr, "ipc_live_test: daemon stderr:\n%s", d->err);
}
static int
waitsocket(Daemon *d, int notifyfd)
{
struct pollfd pfd[2];
struct stat st;
char buf[4096];
int n, status, timeout;
int64_t deadline;
deadline = nowms() + Starttimeout;
for(;;){
if(lstat(d->socket, &st) == 0 && S_ISSOCK(st.st_mode) &&
(st.st_mode & 0777) == 0600)
return 1;
if(waitpid(d->pid, &status, WNOHANG) == d->pid){
d->pid = -1;
readerrors(d);
return fail("daemon exited before creating IPC socket");
}
timeout = leftms(deadline);
if(timeout == 0)
return fail("timed out waiting for protected IPC socket %s",
d->socket);
pfd[0].fd = notifyfd;
pfd[0].events = POLLIN;
pfd[0].revents = 0;
pfd[1].fd = d->errfd;
pfd[1].events = POLLIN;
pfd[1].revents = 0;
n = poll(pfd, 2, timeout);
if(n < 0 && errno == EINTR)
continue;
if(n < 0)
return fail("poll for IPC socket: %s", strerror(errno));
if(n == 0)
continue;
if(pfd[1].revents != 0)
readerrors(d);
if(pfd[0].revents & POLLIN)
while(read(notifyfd, buf, sizeof buf) < 0 && errno == EINTR)
;
}
}
static int
startdaemon(Daemon *d, char *program, char *mapdir)
{
char missing[384];
int fd, notifyfd, watch, errpipe[2];
long maxfd;
pid_t pid;
memset(d, 0, sizeof *d);
d->pid = -1;
d->errfd = -1;
snprintf(d->root, sizeof d->root, "/tmp/strans-ipc.XXXXXX");
if(mkdtemp(d->root) == NULL)
return fail("mkdtemp: %s", strerror(errno));
if(snprintf(d->runtime, sizeof d->runtime, "%s/runtime", d->root)
>= (int)sizeof d->runtime ||
snprintf(d->config, sizeof d->config, "%s/config", d->root)
>= (int)sizeof d->config ||
snprintf(d->ibus, sizeof d->ibus, "%s/ibus", d->config)
>= (int)sizeof d->ibus ||
snprintf(d->bus, sizeof d->bus, "%s/bus", d->ibus)
>= (int)sizeof d->bus ||
snprintf(d->home, sizeof d->home, "%s/home", d->root)
>= (int)sizeof d->home ||
snprintf(d->socket, sizeof d->socket, "%s/strans.sock", d->runtime)
>= (int)sizeof d->socket ||
snprintf(missing, sizeof missing, "%s/no-such-font.ttf", d->root)
>= (int)sizeof missing)
return fail("temporary path is too long");
if(!makedir(d->runtime) || !makedir(d->config) || !makedir(d->ibus) ||
!makedir(d->bus) || !makedir(d->home))
return 0;
notifyfd = inotify_init1(IN_CLOEXEC|IN_NONBLOCK);
if(notifyfd < 0)
return fail("inotify_init1: %s", strerror(errno));
watch = inotify_add_watch(notifyfd, d->runtime,
IN_CREATE|IN_MOVED_TO|IN_ATTRIB);
if(watch < 0){
close(notifyfd);
return fail("inotify_add_watch: %s", strerror(errno));
}
if(pipe2(errpipe, O_CLOEXEC|O_NONBLOCK) < 0){
close(notifyfd);
return fail("pipe2: %s", strerror(errno));
}
pid = fork();
if(pid < 0){
close(errpipe[0]);
close(errpipe[1]);
close(notifyfd);
return fail("fork: %s", strerror(errno));
}
if(pid == 0){
close(errpipe[0]);
close(notifyfd);
if(dup2(errpipe[1], STDOUT_FILENO) < 0 ||
dup2(errpipe[1], STDERR_FILENO) < 0)
_exit(126);
close(errpipe[1]);
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", d->runtime, 1) < 0 ||
setenv("XDG_CONFIG_HOME", d->config, 1) < 0 ||
setenv("HOME", d->home, 1) < 0 || unsetenv("DISPLAY") < 0 ||
unsetenv("DBUS_SESSION_BUS_ADDRESS") < 0 ||
unsetenv("IBUS_ADDRESS") < 0){
dprintf(STDERR_FILENO, "set daemon environment: %s\n",
strerror(errno));
_exit(126);
}
execl(program, program, mapdir, missing, (char*)0);
dprintf(STDERR_FILENO, "exec %s: %s\n", program, strerror(errno));
_exit(127);
}
close(errpipe[1]);
d->pid = pid;
d->errfd = errpipe[0];
if(!waitsocket(d, notifyfd)){
inotify_rm_watch(notifyfd, watch);
close(notifyfd);
return 0;
}
inotify_rm_watch(notifyfd, watch);
close(notifyfd);
return 1;
}
static int
clearbus(Daemon *d)
{
DIR *dir;
struct dirent *de;
char path[576];
int ok;
dir = opendir(d->bus);
if(dir == NULL)
return errno == ENOENT || fail("open cleanup directory %s: %s",
d->bus, 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(path, sizeof path, "%s/%s", d->bus, de->d_name)
>= (int)sizeof path){
fail("cleanup path is too long: %s", de->d_name);
ok = 0;
continue;
}
if(unlink(path) < 0 && errno != ENOENT){
fail("remove IBus address file %s: %s", de->d_name,
strerror(errno));
ok = 0;
}
errno = 0;
}
if(errno != 0){
fail("read cleanup directory %s: %s", d->bus, strerror(errno));
ok = 0;
}
closedir(dir);
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));
}
static int
killdaemon(Daemon *d, int *status)
{
int n, ok;
ok = 1;
if(kill(d->pid, SIGKILL) < 0 && errno != ESRCH){
fail("kill -9 %ld: %s", (long)d->pid, strerror(errno));
ok = 0;
}
do
n = waitpid(d->pid, status, 0);
while(n < 0 && errno == EINTR);
if(n != d->pid){
fail("reap %ld after SIGKILL: %s", (long)d->pid,
n < 0 ? strerror(errno) : "wrong child");
ok = 0;
}else
d->pid = -1;
return ok;
}
static int
stopdaemon(Daemon *d)
{
struct pollfd pfd;
int ok, status, n, reaped;
int64_t deadline;
ok = 1;
if(d->pid > 0){
do
n = waitpid(d->pid, &status, WNOHANG);
while(n < 0 && errno == EINTR);
if(n == d->pid){
fail("daemon exited before test termination with wait status %#x",
status);
d->pid = -1;
ok = 0;
}else if(n < 0){
fail("check daemon %ld before termination: %s",
(long)d->pid, strerror(errno));
if(errno == ECHILD)
d->pid = -1;
ok = 0;
}
}
if(d->pid > 0){
if(kill(d->pid, SIGTERM) < 0){
fail("kill %ld: %s", (long)d->pid, strerror(errno));
ok = 0;
}
reaped = 0;
deadline = nowms() + Stoptimeout;
for(;;){
n = waitpid(d->pid, &status, WNOHANG);
if(n == d->pid){
reaped = 1;
d->pid = -1;
break;
}
if(n < 0 && errno == EINTR)
continue;
if(n < 0){
fail("waitpid %ld: %s", (long)d->pid, strerror(errno));
ok = 0;
if(errno == ECHILD)
d->pid = -1;
break;
}
n = leftms(deadline);
if(n == 0){
fail("daemon %ld did not stop after SIGTERM", (long)d->pid);
ok = 0;
if(!killdaemon(d, &status))
ok = 0;
break;
}
pfd.fd = d->errfd;
pfd.events = POLLIN|POLLHUP;
pfd.revents = 0;
if(poll(&pfd, 1, n) < 0 && errno != EINTR){
fail("poll daemon %ld: %s", (long)d->pid, strerror(errno));
ok = 0;
if(!killdaemon(d, &status))
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))){
fail("daemon exited with unexpected wait status %#x", status);
ok = 0;
}
}
readerrors(d);
if(d->errfd >= 0){
close(d->errfd);
d->errfd = -1;
}
if(d->socket[0] != '\0' && unlink(d->socket) < 0 && errno != ENOENT){
fail("remove IPC socket %s: %s", d->socket, strerror(errno));
ok = 0;
}
if(!clearbus(d)) ok = 0;
if(!rmdirknown(d->bus)) ok = 0;
if(!rmdirknown(d->ibus)) ok = 0;
if(!rmdirknown(d->config)) ok = 0;
if(!rmdirknown(d->runtime)) ok = 0;
if(!rmdirknown(d->home)) ok = 0;
if(!rmdirknown(d->root)) ok = 0;
return ok;
}
static int
connectuntil(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;
}
pfd.fd = fd;
pfd.events = POLLOUT;
pfd.revents = 0;
for(;;){
n = leftms(deadline);
if(n == 0){
close(fd);
errno = ETIMEDOUT;
return -1;
}
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. */
static 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){
if(errno == EINTR)
continue;
return -1;
}
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;
}
static size_t static size_t
getlen(unsigned char p[Ipclensz]) getlen(unsigned char p[Ipclensz])
{ {
@@ -556,7 +51,8 @@ readresponseuntil(int fd, int want, Response *res, int64_t deadline,
return rv; return rv;
} }
res->eaten = hdr[0] != 0; res->eaten = hdr[0] != 0;
n = getlen(hdr + 1); res->del = hdr[1];
n = getlen(hdr + 2);
if(n > Ipcfieldmax){ if(n > Ipcfieldmax){
fail("invalid commit length %zu", n); fail("invalid commit length %zu", n);
return -1; return -1;
@@ -616,6 +112,27 @@ sendreset(int fd, int want)
return ipcsend(fd, req, sizeof req) == 0; 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 static int
expectresponse(int fd, int want, int eaten, char *commit, char *preedit, expectresponse(int fd, int want, int eaten, char *commit, char *preedit,
char *where) char *where)
@@ -632,11 +149,12 @@ expectresponse(int fd, int want, int eaten, char *commit, char *preedit,
} }
static int static int
requestreset(int fd, int want, int eaten, char *preedit, char *where) requestreset(int fd, int want, int eaten, char *commit, char *preedit,
char *where)
{ {
if(!sendreset(fd, want)) if(!sendreset(fd, want))
return fail("send %s: %s", where, strerror(errno)); return fail("send %s: %s", where, strerror(errno));
return expectresponse(fd, want, eaten, "", preedit, where); return expectresponse(fd, want, eaten, commit, preedit, where);
} }
static int static int
@@ -648,16 +166,43 @@ requestkey(int fd, int want, uint32_t mod, uint32_t key, int eaten,
return expectresponse(fd, want, eaten, commit, preedit, where); 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 static int
tryclient(char *path, int64_t deadline, int *client) tryclient(char *path, int64_t deadline, int *client)
{ {
Response res; Response res;
int fd, rv; int fd, rv;
fd = connectuntil(path, deadline); fd = connectsocket(path, deadline);
if(fd < 0) if(fd < 0)
return errno == ETIMEDOUT ? -1 : 0; return errno == ETIMEDOUT ? -1 : 0;
if(!sendkey(fd, 1, 0, Kmodfirst)){ if(!sendkey(fd, 1, 0, 0)){
close(fd); close(fd);
return 0; return 0;
} }
@@ -701,10 +246,10 @@ overflowrejected(char *path)
int64_t deadline; int64_t deadline;
deadline = nowms() + Calltimeout; deadline = nowms() + Calltimeout;
fd = connectuntil(path, deadline); fd = connectsocket(path, deadline);
if(fd < 0) if(fd < 0)
return fail("overflow connect: %s", strerror(errno)); return fail("overflow connect: %s", strerror(errno));
if(!sendkey(fd, 1, 0, Kmodfirst)){ if(!sendkey(fd, 1, 0, 0)){
close(fd); close(fd);
return 1; return 1;
} }
@@ -742,7 +287,26 @@ overflowrejected(char *path)
} }
static int static int
runlistener(Daemon *d) 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 client[Maxclients];
int i, ok; int i, ok;
@@ -750,42 +314,33 @@ runlistener(Daemon *d)
for(i = 0; i < Maxclients; i++) for(i = 0; i < Maxclients; i++)
client[i] = -1; client[i] = -1;
ok = 0; ok = 0;
client[0] = connectuntil(d->socket, nowms() + Calltimeout); client[0] = connectsocket(path, nowms() + Calltimeout);
if(client[0] < 0){ if(client[0] < 0){
fail("connect first client: %s", strerror(errno)); fail("connect first client: %s", strerror(errno));
goto out; goto out;
} }
if(!requestkey(client[0], 1, Mctrl, 'n', 1, "", "", "select Japanese") || if(!requestkey(client[0], 1, Mctrl, 'n', 1, "", "",
!requestkey(client[0], 1, 0, 'k', 1, "", "k", "GTK preedit") || "select Japanese"))
!requestreset(client[0], 1, 1, "", "active reset"))
goto out;
if(!sendkey(client[0], 0, 0, 'k') ||
!sendkey(client[0], 1, 0, Kmodfirst)){
fail("send pipelined XIM/GTK requests: %s", strerror(errno));
goto out;
}
if(!expectresponse(client[0], 0, 1, "", "", "XIM framing") ||
!expectresponse(client[0], 1, 0, "", "k", "GTK after XIM framing"))
goto out; goto out;
for(i = 1; i < Maxclients; i++){ for(i = 1; i < Maxclients; i++){
client[i] = connectuntil(d->socket, nowms() + Calltimeout); client[i] = connectsocket(path, nowms() + Calltimeout);
if(client[i] < 0){ if(client[i] < 0){
fail("connect capacity client %d: %s", i, strerror(errno)); fail("connect capacity client %d: %s", i, strerror(errno));
goto out; goto out;
} }
if(!requestkey(client[i], 1, 0, Kmodfirst, 0, "", "", if(!requestkey(client[i], 1, 0, 0, 0, "", "",
"capacity modifier")) "capacity modifier"))
goto out; goto out;
} }
if(!overflowrejected(d->socket)) if(!overflowrejected(path))
goto out; goto out;
close(client[Maxclients-1]); close(client[Maxclients-1]);
client[Maxclients-1] = -1; client[Maxclients-1] = -1;
if(!waitslot(d->socket, &client[Maxclients-1], "inactive slot recovery")) if(!waitslot(path, &client[Maxclients-1], "inactive slot recovery"))
goto out; goto out;
close(client[0]); close(client[0]);
client[0] = -1; client[0] = -1;
if(!waitslot(d->socket, &client[0], "active slot recovery") || if(!waitslot(path, &client[0], "active slot recovery") ||
!requestkey(client[0], 1, 0, 'a', 1, "", "", !requestkey(client[0], 1, 0, 'a', 1, "", "",
"post-disconnect composition")) "post-disconnect composition"))
goto out; goto out;
@@ -801,21 +356,35 @@ int
main(int argc, char **argv) main(int argc, char **argv)
{ {
Daemon daemon; Daemon daemon;
int ok; Live live;
int capacity, ok;
if(argc != 3){ testname = "ipc_live_test";
fprintf(stderr, "usage: ipc_live_test strans mapdir\n"); 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; return 2;
} }
ok = startdaemon(&daemon, argv[1], argv[2]); daemoninit(&daemon, "daemon");
ok = livesetup(&live, "ipc") &&
startdaemon(&live, &daemon, argv[1+capacity], argv[2+capacity]) &&
waitready(&live, &daemon, NULL, 0);
if(ok) if(ok)
ok = runlistener(&daemon); ok = capacity ? runcapacity(live.socket) : runsmoke(live.socket);
if(!ok) if(!ok)
showerrors(&daemon); showerrors(&daemon);
if(!stopdaemon(&daemon)) if(!stopdaemon(&daemon))
ok = 0; 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) if(!ok)
return 1; return 1;
printf("ipc live listener: ok\n"); printf("ipc %s: ok\n", capacity ? "connection capacity" : "live smoke");
return 0; return 0;
} }

View File

@@ -1,10 +1,33 @@
#define _POSIX_C_SOURCE 200809L #define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h> #include <stdlib.h>
#include <sys/socket.h> #include <sys/socket.h>
#include <sys/un.h>
#include <time.h>
#include <unistd.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)
{ {
@@ -25,6 +48,105 @@ ipc_masks_modifiers(struct ct *t)
CT_EQ_UINT(t, 0, mod); CT_EQ_UINT(t, 0, mod);
CT_EQ_UINT(t, 0, key); CT_EQ_UINT(t, 0, key);
CT_CHECK(t, ipcreqreset(buf)); 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 void
@@ -45,7 +167,6 @@ ipc_runtime_path(struct ct *t)
snprint(fallback, sizeof fallback, "/tmp/strans.%d", getuid()); snprint(fallback, sizeof fallback, "/tmp/strans.%d", getuid());
CT_EQ_STR(t, fallback, buf); CT_EQ_STR(t, fallback, buf);
CT_EQ_INT(t, -1, ipcpath(buf, 4)); CT_EQ_INT(t, -1, ipcpath(buf, 4));
CT_EQ_INT(t, -1, ipcpath(nil, sizeof buf));
if(saved != nil){ if(saved != nil){
setenv("XDG_RUNTIME_DIR", saved, 1); setenv("XDG_RUNTIME_DIR", saved, 1);
free(saved); free(saved);
@@ -56,50 +177,49 @@ ipc_runtime_path(struct ct *t)
void void
ipc_response_pack_boundaries(struct ct *t) ipc_response_pack_boundaries(struct ct *t)
{ {
static const uchar withpre[] = { 1, 1, 0, 'A', 2, 0, 'x', 'y' }; static const uchar withpre[] = { 1, 3, 1, 0, 'A', 2, 0, 'x', 'y' };
static const uchar withoutpre[] = { 1, 1, 0, 'A' }; static const uchar withoutpre[] = { 1, 0, 1, 0, 'A' };
uchar out[Ipcmaxresp+1], field[Ipcfieldmax]; uchar out[Ipcmaxresp+1], field[Ipcfieldmax];
int n; int n;
n = ipcpackresp(out, sizeof out, 7, "A", 1, "xy", 2, 1); n = ipcpackresp(out, sizeof out, 7, 3, "A", 1, "xy", 2, 1);
CT_EQ_INT(t, sizeof withpre, n); CT_EQ_INT(t, sizeof withpre, n);
CT_EQ_MEM(t, withpre, out, sizeof withpre); CT_EQ_MEM(t, withpre, out, sizeof withpre);
n = ipcpackresp(out, sizeof out, 1, "A", 1, "xy", 2, 0); n = ipcpackresp(out, sizeof out, 1, 0, "A", 1, "xy", 2, 0);
CT_EQ_INT(t, sizeof withoutpre, n); CT_EQ_INT(t, sizeof withoutpre, n);
CT_EQ_MEM(t, withoutpre, out, sizeof withoutpre); CT_EQ_MEM(t, withoutpre, out, sizeof withoutpre);
n = ipcpackresp(out, sizeof out, 0, nil, 0, nil, 0, 1); n = ipcpackresp(out, sizeof out, 0, 0, nil, 0, nil, 0, 1);
CT_EQ_INT(t, Ipcresphdrsz + Ipclensz, n); CT_EQ_INT(t, Ipcresphdrsz + Ipclensz, n);
memset(field, 'x', sizeof field); memset(field, 'x', sizeof field);
memset(out, 0xa5, sizeof out); memset(out, 0xa5, sizeof out);
n = ipcpackresp(out, Ipcmaxresp, 1, n = ipcpackresp(out, Ipcmaxresp, 1, 0,
(char*)field, sizeof field, (char*)field, sizeof field, 1); (char*)field, sizeof field, (char*)field, sizeof field, 1);
CT_EQ_INT(t, Ipcmaxresp, n); CT_EQ_INT(t, Ipcmaxresp, n);
CT_EQ_INT(t, 0, out[1]); CT_EQ_INT(t, 0, out[2]);
CT_EQ_INT(t, 1, out[2]); CT_EQ_INT(t, 1, out[3]);
CT_EQ_INT(t, 0, out[3+Ipcfieldmax]); CT_EQ_INT(t, 0, out[4+Ipcfieldmax]);
CT_EQ_INT(t, 1, out[4+Ipcfieldmax]); CT_EQ_INT(t, 1, out[5+Ipcfieldmax]);
CT_EQ_INT(t, 0xa5, out[Ipcmaxresp]); CT_EQ_INT(t, 0xa5, out[Ipcmaxresp]);
CT_EQ_INT(t, -1, ipcpackresp(out, Ipcmaxresp-1, 1, CT_EQ_INT(t, -1, ipcpackresp(out, Ipcmaxresp-1, 1, 0,
(char*)field, sizeof field, (char*)field, sizeof field, 1)); (char*)field, sizeof field, (char*)field, sizeof field, 1));
CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0, CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0, 0,
nil, 1, nil, 0, 0)); (char*)field, Ipcfieldmax+1, "", 0, 0));
CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0, /* A take-back is a rune count and never leaves its byte. */
nil, 0, nil, 1, 1)); CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0, -1, "", 0, "", 0, 0));
CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0, CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0, 256, "", 0, "", 0, 0));
(char*)field, Ipcfieldmax+1, nil, 0, 0));
} }
void void
ipc_response_empty_and_preedit(struct ct *t) ipc_response_empty_and_preedit(struct ct *t)
{ {
uchar first[Ipcmaxresp], second[Ipcmaxresp]; uchar first[Ipcmaxresp], second[Ipcmaxresp];
char commit[16], preedit[16]; char commit[Ipcfieldmax+1], preedit[Ipcfieldmax+1];
Ipcresp resp; Ipcresp resp;
int fd[2], nfirst, nsecond; int fd[2], nfirst, nsecond;
nfirst = ipcpackresp(first, sizeof first, 0, nil, 0, nil, 0, 0); nfirst = ipcpackresp(first, sizeof first, 0, 0, "", 0, "", 0, 0);
nsecond = ipcpackresp(second, sizeof second, 1, nsecond = ipcpackresp(second, sizeof second, 1, 0,
"go", 2, "kana", 4, 1); "go", 2, "kana", 4, 1);
if(!CT_CHECK(t, nfirst > 0 && nsecond > 0)) if(!CT_CHECK(t, nfirst > 0 && nsecond > 0))
return; return;
@@ -109,18 +229,16 @@ ipc_response_empty_and_preedit(struct ct *t)
} }
if(CT_EQ_INT(t, 0, ipcsend(fd[0], first, nfirst)) && 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, ipcsend(fd[0], second, nsecond)) &&
CT_EQ_INT(t, 0, ipcreadresp(fd[1], 0, commit, sizeof commit, CT_EQ_INT(t, 0, ipcreadresp(fd[1], 0, commit, preedit, &resp))){
nil, 0, &resp))){
CT_EQ_INT(t, 0, resp.eaten); CT_EQ_INT(t, 0, resp.eaten);
CT_EQ_SIZE(t, 0, resp.ncommit); CT_EQ_SIZE(t, 0, resp.commitlen);
CT_EQ_SIZE(t, 0, resp.npreedit); CT_EQ_SIZE(t, 0, resp.preeditlen);
CT_EQ_STR(t, "", commit); CT_EQ_STR(t, "", commit);
} }
if(CT_EQ_INT(t, 0, ipcreadresp(fd[1], 1, commit, sizeof commit, if(CT_EQ_INT(t, 0, ipcreadresp(fd[1], 1, commit, preedit, &resp))){
preedit, sizeof preedit, &resp))){
CT_EQ_INT(t, 1, resp.eaten); CT_EQ_INT(t, 1, resp.eaten);
CT_EQ_SIZE(t, 2, resp.ncommit); CT_EQ_SIZE(t, 2, resp.commitlen);
CT_EQ_SIZE(t, 4, resp.npreedit); CT_EQ_SIZE(t, 4, resp.preeditlen);
CT_EQ_STR(t, "go", commit); CT_EQ_STR(t, "go", commit);
CT_EQ_STR(t, "kana", preedit); CT_EQ_STR(t, "kana", preedit);
} }
@@ -132,14 +250,14 @@ void
ipc_response_max_and_drain(struct ct *t) ipc_response_max_and_drain(struct ct *t)
{ {
uchar first[Ipcmaxresp], second[Ipcmaxresp], field[Ipcfieldmax]; uchar first[Ipcmaxresp], second[Ipcmaxresp], field[Ipcfieldmax];
char commit[8], preedit[8]; char commit[Ipcfieldmax+1], preedit[Ipcfieldmax+1];
Ipcresp resp; Ipcresp resp;
int fd[2], nfirst, nsecond; int fd[2], nfirst, nsecond;
memset(field, 'x', sizeof field); memset(field, 'x', sizeof field);
nfirst = ipcpackresp(first, sizeof first, 1, nfirst = ipcpackresp(first, sizeof first, 1, 0,
(char*)field, sizeof field, (char*)field, sizeof field, 1); (char*)field, sizeof field, (char*)field, sizeof field, 1);
nsecond = ipcpackresp(second, sizeof second, 0, "ok", 2, nil, 0, 0); nsecond = ipcpackresp(second, sizeof second, 0, 0, "ok", 2, "", 0, 0);
if(!CT_CHECK(t, nfirst == Ipcmaxresp && nsecond > 0)) if(!CT_CHECK(t, nfirst == Ipcmaxresp && nsecond > 0))
return; return;
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){ if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
@@ -148,15 +266,15 @@ ipc_response_max_and_drain(struct ct *t)
} }
if(CT_EQ_INT(t, 0, ipcsend(fd[0], first, nfirst)) && 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, ipcsend(fd[0], second, nsecond)) &&
CT_EQ_INT(t, 0, ipcreadresp(fd[1], 1, commit, sizeof commit, CT_EQ_INT(t, 0, ipcreadresp(fd[1], 1, commit, preedit, &resp))){
preedit, sizeof preedit, &resp))){ CT_EQ_SIZE(t, Ipcfieldmax, resp.commitlen);
CT_EQ_SIZE(t, Ipcfieldmax, resp.ncommit); CT_EQ_SIZE(t, Ipcfieldmax, resp.preeditlen);
CT_EQ_SIZE(t, Ipcfieldmax, resp.npreedit); CT_EQ_MEM(t, field, commit, Ipcfieldmax);
CT_EQ_STR(t, "xxxxxxx", commit); CT_EQ_MEM(t, field, preedit, Ipcfieldmax);
CT_EQ_STR(t, "xxxxxxx", preedit); 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, sizeof commit, if(CT_EQ_INT(t, 0, ipcreadresp(fd[1], 0, commit, preedit, &resp)))
nil, 0, &resp)))
CT_EQ_STR(t, "ok", commit); CT_EQ_STR(t, "ok", commit);
close(fd[0]); close(fd[0]);
close(fd[1]); close(fd[1]);
@@ -166,11 +284,11 @@ void
ipc_response_fragmented_and_truncated(struct ct *t) ipc_response_fragmented_and_truncated(struct ct *t)
{ {
uchar frame[Ipcmaxresp]; uchar frame[Ipcmaxresp];
char commit[8], preedit[8]; char commit[Ipcfieldmax+1], preedit[Ipcfieldmax+1];
Ipcresp resp; Ipcresp resp;
int fd[2], i, n; int fd[2], i, n;
n = ipcpackresp(frame, sizeof frame, 1, "abc", 3, "xy", 2, 1); n = ipcpackresp(frame, sizeof frame, 1, 2, "abc", 3, "xy", 2, 1);
if(!CT_CHECK(t, n > 0)) if(!CT_CHECK(t, n > 0))
return; return;
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){ if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
@@ -182,8 +300,9 @@ ipc_response_fragmented_and_truncated(struct ct *t)
CT_ERRORF(t, "fragment send failed"); CT_ERRORF(t, "fragment send failed");
break; break;
} }
if(i == n && CT_EQ_INT(t, 0, ipcreadresp(fd[1], 1, if(i == n && CT_EQ_INT(t, 0, ipcreadresp(fd[1], 1, commit, preedit,
commit, sizeof commit, preedit, sizeof preedit, &resp))){ &resp))){
CT_EQ_INT(t, 2, resp.del);
CT_EQ_STR(t, "abc", commit); CT_EQ_STR(t, "abc", commit);
CT_EQ_STR(t, "xy", preedit); CT_EQ_STR(t, "xy", preedit);
} }
@@ -196,8 +315,19 @@ ipc_response_fragmented_and_truncated(struct ct *t)
} }
CT_EQ_INT(t, 0, ipcsend(fd[0], frame, n-1)); CT_EQ_INT(t, 0, ipcsend(fd[0], frame, n-1));
shutdown(fd[0], SHUT_WR); shutdown(fd[0], SHUT_WR);
CT_EQ_INT(t, -1, ipcreadresp(fd[1], 1, commit, sizeof commit, CT_EQ_INT(t, -1, ipcreadresp(fd[1], 1, commit, preedit, &resp));
preedit, sizeof 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[0]);
close(fd[1]); close(fd[1]);
} }
@@ -214,4 +344,104 @@ ipc_broken_peer_send(struct ct *t)
close(fd[1]); close(fd[1]);
CT_EQ_INT(t, -1, ipcsend(fd[0], "x", 1)); CT_EQ_INT(t, -1, ipcsend(fd[0], "x", 1));
close(fd[0]); 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,24 +1,14 @@
#include "dat.h"
#include "fn.h"
#include "test.h" #include "test.h"
static Str static Str
kotrans(char *keys) kotrans(char *keys)
{ {
Emit e; Str out, raw;
Im state;
Str out;
char *p;
memset(&state, 0, sizeof state); raw = mkstr(keys);
state.l = getlang(LangKO); transstr(getlang(LangKO), nil, &raw, &out);
sclear(&out);
for(p = keys; *p != '\0'; p++){
e = transko(&state, (uchar)*p);
sappend(&out, &e.s);
state.pre = e.next;
if(!e.eat)
sputr(&out, (uchar)*p);
}
sappend(&out, &state.pre);
return out; return out;
} }
@@ -35,6 +25,12 @@ korean_sequences(struct ct *t)
{ "rr", "ㄱㄱ" }, { "rr", "ㄱㄱ" },
{ "Rk", "" }, { "Rk", "" },
{ "rk1", "가1" }, { "rk1", "가1" },
{ "rt", "" },
{ "rtk", "ㄱ사" },
{ "rtt", "ㄳㅅ" },
{ "kr", "" },
{ "hkr", "" },
{ "kk", "ㅏㅏ" },
}; };
Str out; Str out;
int i; int i;
@@ -91,6 +87,7 @@ korean_backspace(struct ct *t)
{ {
static const struct { char *before, *after; } cases[] = { static const struct { char *before, *after; } cases[] = {
{ "", "" }, { "", "" },
{ "", "" },
{ "", "" }, { "", "" },
{ "", "" }, { "", "" }, { "", "" }, { "", "" }, { "", "" }, { "", "" },
{ "", "" }, { "", "" }, { "", "" }, { "", "" }, { "", "" }, { "", "" },

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

@@ -12,10 +12,8 @@ MKEMOJI = ROOT / "map" / "mkemoji"
TIMEOUT = 10 TIMEOUT = 10
def generate(source=None): def generate(*sources):
args = [sys.executable, "-B", str(MKEMOJI)] args = [sys.executable, "-B", str(MKEMOJI)] + [str(s) for s in sources]
if source is not None:
args.append(str(source))
return subprocess.run( return subprocess.run(
args, capture_output=True, text=True, check=False, timeout=TIMEOUT args, capture_output=True, text=True, check=False, timeout=TIMEOUT
) )
@@ -37,27 +35,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"], "#")

View File

@@ -42,25 +42,45 @@ class MkhanjaTest(unittest.TestCase):
self.addCleanup(tmp.cleanup) self.addCleanup(tmp.cleanup)
return path return path
def test_imports_single_bmp_hanja_rows(self): def test_imports_bmp_hanja_rows(self):
result = run( result = run(
IMPORT, IMPORT,
self.source( self.source(
"# Copyright holder\n" "# Copyright holder\n"
"# BSD license\n" "# BSD license\n"
"한:漢:first\n" "\ud55c:\u6f22:first\n"
"가:㐀:extension A\n" "\uac00:\u3400:extension A\n"
"김:金:compatibility\n" "\uae40:\u91d1:compatibility\n"
"방학:放:word reading\n" "\ud55c\uc790:\u6f22\u5b57:word\n"
"학:學校:word value\n" "\ud55c:\U00020000:astral\n"
"한:𠀀:astral\n" "\u3131:\u52a0:jamo\n"
"ㄱ:加:jamo\n" "\ud55c\uae00:\u97d3glyph:mixed\n"
), ),
) )
self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.returncode, 0, result.stderr)
lines = result.stdout.splitlines() lines = result.stdout.splitlines()
self.assertEqual(lines[:2], ["# Copyright holder", "# BSD license"]) self.assertEqual(lines[:2], ["# Copyright holder", "# BSD license"])
self.assertEqual(lines[2:], ["", "\t", "\t", "\t"]) self.assertEqual(lines[2:], ["", "\u6f22\t\ud55c", "\u3400\t\uac00",
"\u91d1\t\uae40",
"\u6f22\u5b57\t\ud55c\uc790"])
def test_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): def test_import_rejects_malformed_and_duplicate_rows(self):
bad = [ bad = [
@@ -91,6 +111,7 @@ class MkhanjaTest(unittest.TestCase):
"\t\n" "\t\n"
"\t\n" "\t\n"
"\t\n" "\t\n"
"漢字\t한자\n"
), ),
) )
self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.returncode, 0, result.stderr)
@@ -103,16 +124,36 @@ class MkhanjaTest(unittest.TestCase):
"\t漢 韓", "\t漢 韓",
"\t㐀 家", "\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): def test_generator_rejects_bad_rows(self):
bad = [ bad = [
"\t\n", "a\t\n",
"\t\n", "\ta\n",
"𠀀\t\n", "𠀀\t\n",
"\t\n\t\n", "\t\n\t\n",
"漢 한\n", "漢 한\n",
"\t\n",
"\t\n",
"\tㅁㄴ\n",
"※※\t\n",
"\u3000\t\n",
"\u00ad\t\n",
] ]
for text in bad: for text in bad:
with self.subTest(text=repr(text)): with self.subTest(text=repr(text)):
@@ -127,13 +168,22 @@ class MkhanjaTest(unittest.TestCase):
self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("\t", result.stdout) 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): def test_generator_keeps_runtime_candidate_limit(self):
rows = "".join(f"{chr(0x4E00 + n)}\t\n" for n in range(33)) rows = "".join(f"{chr(0x4E00 + n)}\t\n" for n in range(129))
result = run(GENERATE, self.source(rows)) result = run(GENERATE, self.source(rows))
self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.returncode, 0, result.stderr)
candidates = result.stdout.rstrip().split("\t", 1)[1].split() candidates = result.stdout.rstrip().split("\t", 1)[1].split()
self.assertEqual(len(candidates), 32) self.assertEqual(len(candidates), 128)
self.assertEqual(candidates[-1], chr(0x4E00 + 31)) self.assertEqual(candidates[-1], chr(0x4E00 + 127))
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -1,34 +1,323 @@
#include "dat.h"
#include "fn.h"
#include "test.h" #include "test.h"
#include <limits.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 void
popup_layout(struct ct *t) popup_layout(struct ct *t)
{ {
Rune heart[] = { 0x2764, 0xfe0f }; 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; Caret caret;
int x, y; 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);
CT_EQ_INT(t, 1, popupcells(heart, nelem(heart)));
memset(&caret, 0, sizeof caret); memset(&caret, 0, sizeof caret);
popupposition(&caret, 40, 50, 800, 600, 100, 60, &x, &y); area = (Area){1920, 30, 1280, 900};
CT_EQ_INT(t, 50, x); popupposition(&caret, 2000, 50, &area, 100, 60, &x, &y);
CT_EQ_INT(t, 2010, x);
CT_EQ_INT(t, 60, y); 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.valid = 1;
caret.x = 70; caret.x = 2100;
caret.y = 80; caret.y = 80;
caret.h = 20; caret.h = 20;
popupposition(&caret, 40, 50, 800, 600, 100, 60, &x, &y); popupposition(&caret, 0, 0, &area, 100, 60, &x, &y);
CT_EQ_INT(t, 70, x); CT_EQ_INT(t, 2100, x);
CT_EQ_INT(t, 100, y); CT_EQ_INT(t, 100, y);
caret.x = 790; caret.x = 3150;
caret.y = 590; caret.y = 850;
popupposition(&caret, 0, 0, 800, 600, 100, 60, &x, &y); popupposition(&caret, 0, 0, &area, 200, 100, &x, &y);
CT_EQ_INT(t, 700, x); CT_EQ_INT(t, 3000, x);
CT_EQ_INT(t, 540, y); CT_EQ_INT(t, 750, y);
caret.x = INT_MIN;
caret.y = INT_MAX; /* A popup no larger than a small work area stays inside its origin. */
caret.h = INT_MAX; area = (Area){100, 200, 80, 80};
popupposition(&caret, 0, 0, 800, 600, 100, 60, &x, &y); caret.x = 120;
CT_EQ_INT(t, 0, x); caret.y = 230;
CT_EQ_INT(t, 540, y); 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);
} }

View File

@@ -3,25 +3,19 @@
#include <sys/stat.h> #include <sys/stat.h>
#include <unistd.h> #include <unistd.h>
#include "../srv.c" #include "srv.c"
#include "../cutest/cutest.h" #include "test.h"
void testengineinit(int);
void testenginehandle(Keyreq*);
void* testengineowner(void);
void testenginepreedit(Str*);
#undef recv #undef recv
typedef struct Enginegate Enginegate; typedef struct Serverfix Serverfix;
typedef struct Testclient Testclient; typedef struct Testclient Testclient;
struct Enginegate /* A held pump plus a private client-slot channel of the wanted depth. */
struct Serverfix
{ {
Channel *seen; Pump pump;
Channel *go; Channel *oldclientc;
Channel *stop;
Channel *done;
}; };
struct Testclient struct Testclient
@@ -34,30 +28,30 @@ struct Testclient
}; };
static void static void
enginegate(void *arg) serverbegin(Serverfix *f, int nclients)
{ {
Enginegate *g; Drawcmd dc;
Keyreq req;
uchar token; uchar token;
Alt alts[] = {
{keyc, &req, CHANRCV, nil},
{nil, &token, CHANRCV, nil},
{nil, nil, CHANEND, nil},
};
g = arg; f->oldclientc = clientc;
alts[1].c = g->stop; while(channbrecv(drawc, &dc) > 0)
for(;;) ;
switch(alt(alts)){ clientc = chancreate(sizeof token, nclients);
case 0: testengineinit(LangJP);
chansend(g->seen, &req); pumpstart(&f->pump, 0);
chanrecv(g->go, &token); pumphold(&f->pump, Pumpall);
testenginehandle(&req); }
break;
case 1: static void
chansend(g->done, &token); serverend(Serverfix *f)
return; {
} Drawcmd dc;
pumpstop(&f->pump);
chanfree(clientc);
clientc = f->oldclientc;
while(channbrecv(drawc, &dc) > 0)
;
} }
static void static void
@@ -142,38 +136,70 @@ sendreset(struct ct *t, Testclient *client, int want)
} }
static Keyreq static Keyreq
nextrequest(struct ct *t, Enginegate *g, int op) nextrequestcap(struct ct *t, Pump *p, int op, int cap)
{ {
Keyreq req; Keyreq req;
memset(&req, 0, sizeof req); memset(&req, 0, sizeof req);
chanrecv(g->seen, &req); chanrecv(p->trace, &req);
CT_EQ_INT(t, op, req.op); CT_EQ_INT(t, op, req.op);
CT_EQ_INT(t, cap, req.clientpre);
return req; return req;
} }
static Keyreq
nextrequest(struct ct *t, Pump *p, int op)
{
return nextrequestcap(t, p, op, 1);
}
static void static void
allowrequest(Enginegate *g) allowrequest(Pump *p)
{ {
uchar token; uchar token;
token = 0; token = 0;
chansend(g->go, &token); chansend(p->go, &token);
} }
static int static int
readreply(struct ct *t, Testclient *client, int want, char *preedit, int npreedit) readreply(struct ct *t, Testclient *client, int want, char *wantcommit,
char *preedit)
{ {
char commit[Maxutf]; char commit[Ipcfieldmax+1];
Ipcresp resp; Ipcresp resp;
if(ipcreadresp(client->peer, want, commit, sizeof commit, if(ipcreadresp(client->peer, want, commit, preedit, &resp) < 0)
preedit, npreedit, &resp) < 0)
return CT_ERRORF(t, "response read failed: %s", strerror(errno)); return CT_ERRORF(t, "response read failed: %s", strerror(errno));
CT_EQ_STR(t, "", commit); CT_EQ_STR(t, wantcommit, commit);
return resp.eaten; 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 static void
waitclient(struct ct *t, Testclient *client) waitclient(struct ct *t, Testclient *client)
{ {
@@ -191,127 +217,113 @@ waitclient(struct ct *t, Testclient *client)
} }
static void static void
disconnectclient(struct ct *t, Enginegate *g, Testclient *client, void *owner) disconnectclient(struct ct *t, Pump *p, Testclient *client, void *owner)
{ {
Keyreq req; Keyreq req;
uchar token; uchar token;
close(client->peer); close(client->peer);
client->peer = -1; client->peer = -1;
req = nextrequest(t, g, Keyrelease); req = nextrequest(t, p, Keyrelease);
if(owner != nil) if(owner != nil)
CT_EQ_PTR(t, owner, req.owner); CT_EQ_PTR(t, owner, req.owner);
CT_CHECK(t, channbrecv(client->done, &token) <= 0); CT_CHECK(t, channbrecv(client->done, &token) <= 0);
allowrequest(g); allowrequest(p);
waitclient(t, client); waitclient(t, client);
} }
void void
server_connection_ownership(struct ct *t) server_connection_ownership(struct ct *t)
{ {
Channel *oldclientc; Serverfix f;
Enginegate gate;
Testclient a, b, c; Testclient a, b, c;
Keyreq req; Keyreq req;
Drawcmd dc;
Str shown; Str shown;
char preedit[Maxutf]; char preedit[Ipcfieldmax+1];
void *aowner, *bowner, *cowner; void *aowner, *bowner, *cowner;
uchar byte, token; uchar byte, token;
ssize_t n; ssize_t n;
int gateactive;
memset(&gate, 0, sizeof gate);
memset(&a, 0, sizeof a); memset(&a, 0, sizeof a);
memset(&b, 0, sizeof b); memset(&b, 0, sizeof b);
memset(&c, 0, sizeof c); memset(&c, 0, sizeof c);
a.fd = a.peer = b.fd = b.peer = c.fd = c.peer = -1; a.fd = a.peer = b.fd = b.peer = c.fd = c.peer = -1;
aowner = bowner = cowner = nil; aowner = bowner = cowner = nil;
oldclientc = clientc; serverbegin(&f, 3);
while(channbrecv(drawc, &dc) > 0)
;
clientc = chancreate(sizeof token, 3);
gate.seen = chancreate(sizeof(Keyreq), 0);
gate.go = chancreate(sizeof token, 0);
gate.stop = chancreate(sizeof token, 0);
gate.done = chancreate(sizeof token, 0);
testengineinit(LangJP);
gateactive = proccreate(enginegate, &gate, 8192) >= 0;
if(!CT_CHECK(t, gateactive))
goto cleanup;
if(!startclient(t, &a) || !startclient(t, &b)) if(!startclient(t, &a) || !startclient(t, &b))
goto cleanup; goto cleanup;
if(!sendkey(t, &a, 1, 0, 'k')) if(!sendkey(t, &a, 1, 0, 'k'))
goto cleanup; goto cleanup;
req = nextrequest(t, &gate, Keypress); req = nextrequest(t, &f.pump, Keypress);
aowner = req.owner; aowner = req.owner;
allowrequest(&gate); allowrequest(&f.pump);
CT_CHECK(t, readreply(t, &a, 1, preedit, sizeof preedit)); CT_CHECK(t, readreply(t, &a, 1, "", preedit));
CT_EQ_STR(t, "k", preedit); CT_EQ_STR(t, "k", preedit);
if(!sendkey(t, &a, 1, 0, 'a')) if(!sendkey(t, &a, 1, 0, 'a'))
goto cleanup; goto cleanup;
req = nextrequest(t, &gate, Keypress); req = nextrequest(t, &f.pump, Keypress);
CT_EQ_PTR(t, aowner, req.owner); CT_EQ_PTR(t, aowner, req.owner);
allowrequest(&gate); allowrequest(&f.pump);
CT_CHECK(t, readreply(t, &a, 1, preedit, sizeof preedit)); CT_CHECK(t, readreply(t, &a, 1, "", preedit));
CT_EQ_STR(t, "", preedit); CT_EQ_STR(t, "", preedit);
CT_EQ_PTR(t, aowner, testengineowner()); CT_EQ_PTR(t, aowner, testengineowner());
if(!sendkey(t, &b, 1, 0, 'n')) if(!sendkey(t, &b, 1, 0, 'n'))
goto cleanup; goto cleanup;
req = nextrequest(t, &gate, Keypress); req = nextrequest(t, &f.pump, Keypress);
bowner = req.owner; bowner = req.owner;
CT_CHECK(t, bowner != aowner); CT_CHECK(t, bowner != aowner);
allowrequest(&gate); allowrequest(&f.pump);
CT_CHECK(t, readreply(t, &b, 1, preedit, sizeof preedit)); CT_CHECK(t, readreply(t, &b, 1, "", preedit));
CT_EQ_STR(t, "", preedit); CT_EQ_STR(t, "", preedit);
CT_EQ_PTR(t, bowner, testengineowner()); CT_EQ_PTR(t, bowner, testengineowner());
if(!sendreset(t, &a, 1)) if(!sendreset(t, &a, 1))
goto cleanup; goto cleanup;
req = nextrequest(t, &gate, Keyreset); req = nextrequest(t, &f.pump, Keyreset);
CT_EQ_PTR(t, aowner, req.owner); CT_EQ_PTR(t, aowner, req.owner);
allowrequest(&gate); allowrequest(&f.pump);
CT_CHECK(t, readreply(t, &a, 1, preedit, sizeof preedit)); /* The reset hands back what the takeover took from this client. */
CT_CHECK(t, readreply(t, &a, 1, "", preedit));
CT_EQ_STR(t, "", preedit); CT_EQ_STR(t, "", preedit);
CT_EQ_PTR(t, bowner, testengineowner()); CT_EQ_PTR(t, bowner, testengineowner());
if(!sendkey(t, &b, 1, 0, Kmodfirst)) if(!sendkey(t, &b, 1, 0, 0))
goto cleanup; goto cleanup;
req = nextrequest(t, &gate, Keypress); req = nextrequest(t, &f.pump, Keypress);
allowrequest(&gate); allowrequest(&f.pump);
CT_CHECK(t, !readreply(t, &b, 1, preedit, sizeof preedit)); CT_CHECK(t, !readreply(t, &b, 1, "", preedit));
CT_EQ_STR(t, "", preedit); CT_EQ_STR(t, "", preedit);
disconnectclient(t, &gate, &a, aowner); disconnectclient(t, &f.pump, &a, aowner);
CT_EQ_PTR(t, bowner, testengineowner()); CT_EQ_PTR(t, bowner, testengineowner());
if(!sendkey(t, &b, 1, 0, 'y')) if(!sendkey(t, &b, 1, 0, 'y'))
goto cleanup; goto cleanup;
req = nextrequest(t, &gate, Keypress); req = nextrequest(t, &f.pump, Keypress);
allowrequest(&gate); allowrequest(&f.pump);
CT_CHECK(t, readreply(t, &b, 1, preedit, sizeof preedit)); CT_CHECK(t, readreply(t, &b, 1, "", preedit));
if(!sendkey(t, &b, 1, 0, 'a')) if(!sendkey(t, &b, 1, 0, 'a'))
goto cleanup; goto cleanup;
req = nextrequest(t, &gate, Keypress); req = nextrequest(t, &f.pump, Keypress);
allowrequest(&gate); allowrequest(&f.pump);
CT_CHECK(t, readreply(t, &b, 1, preedit, sizeof preedit)); CT_CHECK(t, readreply(t, &b, 1, "", preedit));
CT_EQ_STR(t, "にゃ", preedit); CT_EQ_STR(t, "にゃ", preedit);
if(!sendreset(t, &b, 1)) if(!sendreset(t, &b, 1))
goto cleanup; goto cleanup;
req = nextrequest(t, &gate, Keyreset); req = nextrequest(t, &f.pump, Keyreset);
CT_EQ_PTR(t, bowner, req.owner); CT_EQ_PTR(t, bowner, req.owner);
allowrequest(&gate); allowrequest(&f.pump);
CT_CHECK(t, readreply(t, &b, 1, preedit, sizeof preedit)); CT_CHECK(t, readreply(t, &b, 1, "にゃ", preedit));
CT_EQ_STR(t, "", preedit); CT_EQ_STR(t, "", preedit);
CT_EQ_PTR(t, bowner, testengineowner()); CT_EQ_PTR(t, bowner, testengineowner());
if(!sendkey(t, &b, 0, 0, 'k')) if(!sendkey(t, &b, 0, 0, 'k'))
goto cleanup; goto cleanup;
req = nextrequest(t, &gate, Keypress); req = nextrequestcap(t, &f.pump, Keypress, 0);
CT_EQ_PTR(t, bowner, req.owner); CT_EQ_PTR(t, bowner, req.owner);
allowrequest(&gate); allowrequest(&f.pump);
CT_CHECK(t, readreply(t, &b, 0, nil, 0)); CT_CHECK(t, readreply(t, &b, 0, "", preedit));
errno = 0; errno = 0;
n = recv(b.peer, &byte, 1, MSG_PEEK|MSG_DONTWAIT); n = recv(b.peer, &byte, 1, MSG_PEEK|MSG_DONTWAIT);
CT_EQ_INT(t, -1, n); CT_EQ_INT(t, -1, n);
@@ -319,12 +331,12 @@ server_connection_ownership(struct ct *t)
CT_EQ_PTR(t, bowner, testengineowner()); CT_EQ_PTR(t, bowner, testengineowner());
if(!sendkey(t, &b, 1, 0, 'a')) if(!sendkey(t, &b, 1, 0, 'a'))
goto cleanup; goto cleanup;
req = nextrequest(t, &gate, Keypress); req = nextrequest(t, &f.pump, Keypress);
allowrequest(&gate); allowrequest(&f.pump);
CT_CHECK(t, readreply(t, &b, 1, preedit, sizeof preedit)); CT_CHECK(t, readreply(t, &b, 1, "", preedit));
CT_EQ_STR(t, "", preedit); CT_EQ_STR(t, "", preedit);
disconnectclient(t, &gate, &b, bowner); disconnectclient(t, &f.pump, &b, bowner);
CT_EQ_PTR(t, nil, testengineowner()); CT_EQ_PTR(t, nil, testengineowner());
testenginepreedit(&shown); testenginepreedit(&shown);
CT_EQ_INT(t, 0, shown.n); CT_EQ_INT(t, 0, shown.n);
@@ -333,38 +345,205 @@ server_connection_ownership(struct ct *t)
goto cleanup; goto cleanup;
if(!sendkey(t, &c, 1, 0, 'k')) if(!sendkey(t, &c, 1, 0, 'k'))
goto cleanup; goto cleanup;
req = nextrequest(t, &gate, Keypress); req = nextrequest(t, &f.pump, Keypress);
cowner = req.owner; cowner = req.owner;
close(c.peer); close(c.peer);
c.peer = -1; c.peer = -1;
allowrequest(&gate); allowrequest(&f.pump);
req = nextrequest(t, &gate, Keyrelease); req = nextrequest(t, &f.pump, Keyrelease);
CT_EQ_PTR(t, cowner, req.owner); CT_EQ_PTR(t, cowner, req.owner);
CT_EQ_PTR(t, cowner, testengineowner()); CT_EQ_PTR(t, cowner, testengineowner());
CT_CHECK(t, channbrecv(c.done, &token) <= 0); CT_CHECK(t, channbrecv(c.done, &token) <= 0);
allowrequest(&gate); allowrequest(&f.pump);
waitclient(t, &c); waitclient(t, &c);
CT_EQ_PTR(t, nil, testengineowner()); CT_EQ_PTR(t, nil, testengineowner());
CT_CHECK(t, channbrecv(clientc, &token) <= 0); CT_CHECK(t, channbrecv(clientc, &token) <= 0);
cleanup: cleanup:
if(a.peer >= 0) if(a.peer >= 0)
disconnectclient(t, &gate, &a, aowner); disconnectclient(t, &f.pump, &a, aowner);
if(b.peer >= 0) if(b.peer >= 0)
disconnectclient(t, &gate, &b, bowner); disconnectclient(t, &f.pump, &b, bowner);
if(c.peer >= 0) if(c.peer >= 0)
disconnectclient(t, &gate, &c, cowner); disconnectclient(t, &f.pump, &c, cowner);
if(gateactive){ serverend(&f);
token = 0; }
chansend(gate.stop, &token);
chanrecv(gate.done, &token); void
} server_extension_stream(struct ct *t)
chanfree(gate.seen); {
chanfree(gate.go); Serverfix f;
chanfree(gate.stop); Testclient a, b;
chanfree(gate.done); Keyreq req;
chanfree(clientc); uchar frame[Ipccaretsz];
clientc = oldclientc; char preedit[Ipcfieldmax+1];
while(channbrecv(drawc, &dc) > 0) 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);
} }

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);
@@ -89,10 +115,6 @@ str_invalid_and_full_appends(struct ct *t)
Str s; Str s;
int i; int i;
sinit(&s, nil, 1);
CT_EQ_INT(t, 0, s.n);
sinit(&s, "x", -1);
CT_EQ_INT(t, 0, s.n);
for(i = 0; i < Maxrunes; i++) for(i = 0; i < Maxrunes; i++)
s.r[i] = 'a'; s.r[i] = 'a';
s.n = Maxrunes; s.n = Maxrunes;

View File

@@ -1,22 +1,48 @@
#include "../cutest/cutest.h" /* Included after dat.h and fn.h (or after the source under test). */
#include "../dat.h" #include "cutest/cutest.h"
#include "../fn.h"
extern Lang testvi; /*
* 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;
};
enum
{
Pumpnone = -1,
Pumpall = -2,
};
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 str_invalid_and_full_appends(struct ct*); void str_invalid_and_full_appends(struct ct*);
void hmap_set_replace_and_grow(struct ct*);
void hmap_long_utf8_keys(struct ct*);
void hmap_binary_keys_and_invalid_lengths(struct ct*);
void trie_exact_prefix_and_duplicate(struct ct*); void trie_exact_prefix_and_duplicate(struct ct*);
void trie_optional_outputs_and_invalid_lengths(struct ct*); void trie_put_and_unloaded(struct ct*);
void popup_layout(struct ct*); void popup_layout(struct ct*);
void font_render(struct ct*); void font_render(struct ct*);
void production_maps_load(struct ct*); void production_maps_load(struct ct*);
@@ -32,10 +58,15 @@ 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_shortcut_modifiers(struct ct*);
void engine_dictionary_queue_latest_wins(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_lifecycle(struct ct*);
void engine_active_owner_reset(struct ct*); void engine_active_owner_reset(struct ct*);
void engine_active_owner_caret(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*);
@@ -50,20 +81,23 @@ void engine_emoji_queries(struct ct*);
void engine_emoji_japanese_and_multirune(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_search(struct ct*);
void engine_hanja_unknown_and_cancel(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_korean_keys(struct ct*);
void engine_hanja_backspace(struct ct*); void engine_hanja_backspace(struct ct*);
void engine_hanja_input_languages(struct ct*); void engine_hanja_input_languages(struct ct*);
void engine_randomized_stress(struct ct*); void engine_randomized_stress(struct ct*);
void engine_full_boundary_passthrough(struct ct*); void engine_full_boundary_passthrough(struct ct*);
void dictionary_candidates(struct ct*); void dictionary_candidates(struct ct*);
void dictionary_misses_clear_result(struct ct*); void dictionary_misses(struct ct*);
void dictionary_emoji_identity(struct ct*); void dictionary_prefix(struct ct*);
void ipc_masks_modifiers(struct ct*); void ipc_masks_modifiers(struct ct*);
void ipc_control_and_caret_frames(struct ct*);
void ipc_runtime_path(struct ct*); void ipc_runtime_path(struct ct*);
void ipc_response_pack_boundaries(struct ct*); void ipc_response_pack_boundaries(struct ct*);
void ipc_response_empty_and_preedit(struct ct*); void ipc_response_empty_and_preedit(struct ct*);
@@ -71,7 +105,29 @@ void ipc_response_max_and_drain(struct ct*);
void ipc_response_fragmented_and_truncated(struct ct*); void ipc_response_fragmented_and_truncated(struct ct*);
void ipc_broken_peer_send(struct ct*); void ipc_broken_peer_send(struct ct*);
void server_connection_ownership(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_machine_id_fallback(struct ct*);
void ibus_startup_requires_ownership(struct ct*); void ibus_capability_policy(struct ct*);
void ibus_private_input_policy(struct ct*);
void ibus_context_lifecycle(struct ct*); void ibus_context_lifecycle(struct ct*);
void ibus_active_release_lifecycle(struct ct*); void ibus_active_release_lifecycle(struct ct*);
void compose_sequences(struct ct*);
void xim_keymap_lookup(struct ct*);
void xim_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,22 +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 mapped, shown; Str preedit;
if(state->l->lang == LangJP || state->l->lang == LangJPK){ testenginepreedit(&preedit);
shown = state->pre; checkstr(t, "engine preedit", want, &preedit);
if(state->raw.n == 0) }
return shown;
if(mapget(state->l->map, &state->raw, &mapped)) static void
sappend(&shown, &mapped); pumpthread(void *arg)
else {
sappend(&shown, &state->raw); Pump *p;
return shown; Keyreq req;
} uchar token;
if(state->l->map != nil && mapget(state->l->map, &state->pre, &shown)) Alt alts[] = {
return shown; {nil, &req, CHANRCV, nil},
return state->pre; {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,54 +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); }
CT_EQ_PTR(t, nil, trieget(trie, "missing", 7, &n));
cleanup:
trieclose(trie); trieclose(trie);
} }
void void
trie_optional_outputs_and_invalid_lengths(struct ct *t) trie_put_and_unloaded(struct ct *t)
{ {
char *v; char *v;
Trie *trie; Trie *trie;
Str key;
int n; int n;
trie = trieopen("data/trie.map"); key = mkstr("k");
CT_CHECK(t, trieget(trie, "a", 1, nil) != nil); CT_EQ_INT(t, TrieMiss, trielookup(nil, &key, &v, &n));
v = nil; trie = trienew();
CT_CHECK(t, trielookup(trie, "a", 1, &v, nil)); CT_EQ_INT(t, TrieMiss, trielookup(trie, &key, &v, &n));
CT_CHECK(t, v != nil); trieput(trie, "k", 1, "one two", 7);
n = -1; trieput(trie, "ka", 2, "", 0);
CT_CHECK(t, trielookup(trie, "a", 1, nil, &n)); if(CT_EQ_INT(t, TrieExact, trielookup(trie, &key, &v, &n)))
CT_EQ_INT(t, 5, n); CT_EQ_MEM(t, "one two", v, 7);
CT_CHECK(t, trielookup(trie, "dupli", 5, nil, nil)); key = mkstr("ka");
CT_EQ_PTR(t, nil, trieget(trie, nil, 1, &n)); if(CT_EQ_INT(t, TrieExact, trielookup(trie, &key, &v, &n)))
CT_EQ_PTR(t, nil, trieget(trie, "a", -1, &n)); CT_EQ_INT(t, 0, n);
CT_CHECK(t, !trielookup(trie, nil, 1, &v, &n)); trieput(trie, "k", 1, "three", 5);
CT_CHECK(t, !trielookup(trie, "a", -1, &v, &n)); 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);
} }
@@ -84,13 +101,12 @@ 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;
@@ -110,7 +126,6 @@ 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); trieclose(fixture);
state.l->map = saved; 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, ...)
@@ -45,32 +44,26 @@ erealloc(void *p, ulong n)
static void static void
testmapinit(void) testmapinit(void)
{ {
Lang *jp, *kata; Lang *jp, *kata, *vi;
jp = getlang(LangJP); jp = getlang(LangJP);
kata = getlang(LangJPK); kata = getlang(LangJPK);
if(jp == nil || kata == nil) vi = getlang(LangVI);
if(jp == nil || kata == nil || vi == nil)
die("test language is not registered"); die("test language is not registered");
jp->map = trieopen("../map/hira.map"); jp->map = trieopen("../map/hira.map");
kata->map = trieopen("../map/kata.map"); kata->map = trieopen("../map/kata.map");
memset(&testvi, 0, sizeof testvi); vi->map = trieopen("../map/telex.map");
testvi.lang = LangVI;
testvi.mapname = "telex";
testvi.trans = transvi;
testvi.back = backvi;
testvi.map = trieopen("../map/telex.map");
} }
#ifndef STRESS
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 },
{ "str/invalid-full-appends", str_invalid_and_full_appends }, { "str/invalid-full-appends", str_invalid_and_full_appends },
{ "hmap/set-replace-grow", hmap_set_replace_and_grow },
{ "hmap/long-utf8-keys", hmap_long_utf8_keys },
{ "hmap/binary-invalid-lengths", hmap_binary_keys_and_invalid_lengths },
{ "trie/exact-prefix-duplicate", trie_exact_prefix_and_duplicate }, { "trie/exact-prefix-duplicate", trie_exact_prefix_and_duplicate },
{ "trie/optional-invalid-lengths", trie_optional_outputs_and_invalid_lengths }, { "trie/put-and-unloaded", trie_put_and_unloaded },
{ "popup/layout", popup_layout }, { "popup/layout", popup_layout },
{ "font/render", font_render }, { "font/render", font_render },
{ "map/production-lifecycle", production_maps_load }, { "map/production-lifecycle", production_maps_load },
@@ -86,10 +79,15 @@ static const struct ct_test tests[] = {
{ "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-shortcut-modifiers", engine_candidate_shortcut_modifiers },
{ "engine/dictionary-queue-latest", engine_dictionary_queue_latest_wins }, { "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-lifecycle", engine_active_owner_lifecycle },
{ "engine/active-owner-reset", engine_active_owner_reset }, { "engine/active-owner-reset", engine_active_owner_reset },
{ "engine/active-owner-caret", engine_active_owner_caret }, { "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 },
@@ -104,20 +102,22 @@ static const struct ct_test tests[] = {
{ "engine/emoji-japanese-multirune", engine_emoji_japanese_and_multirune }, { "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-search", engine_hanja_search },
{ "engine/hanja-unknown-cancel", engine_hanja_unknown_and_cancel }, { "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-korean-keys", engine_hanja_korean_keys },
{ "engine/hanja-backspace", engine_hanja_backspace }, { "engine/hanja-backspace", engine_hanja_backspace },
{ "engine/hanja-input-languages", engine_hanja_input_languages }, { "engine/hanja-input-languages", engine_hanja_input_languages },
{ "engine/randomized-stress", engine_randomized_stress },
{ "engine/full-boundary-passthrough", engine_full_boundary_passthrough }, { "engine/full-boundary-passthrough", engine_full_boundary_passthrough },
{ "dict/candidates", dictionary_candidates }, { "dict/candidates", dictionary_candidates },
{ "dict/misses-clear-result", dictionary_misses_clear_result }, { "dict/misses", dictionary_misses },
{ "dict/emoji-identity", dictionary_emoji_identity }, { "dict/prefix", dictionary_prefix },
{ "ipc/masks-modifiers", ipc_masks_modifiers }, { "ipc/masks-modifiers", ipc_masks_modifiers },
{ "ipc/control-caret-frames", ipc_control_and_caret_frames },
{ "ipc/runtime-path", ipc_runtime_path }, { "ipc/runtime-path", ipc_runtime_path },
{ "ipc/response-pack-boundaries", ipc_response_pack_boundaries }, { "ipc/response-pack-boundaries", ipc_response_pack_boundaries },
{ "ipc/response-empty-preedit", ipc_response_empty_and_preedit }, { "ipc/response-empty-preedit", ipc_response_empty_and_preedit },
@@ -125,11 +125,38 @@ static const struct ct_test tests[] = {
{ "ipc/response-fragmented-truncated", ipc_response_fragmented_and_truncated }, { "ipc/response-fragmented-truncated", ipc_response_fragmented_and_truncated },
{ "ipc/broken-peer-send", ipc_broken_peer_send }, { "ipc/broken-peer-send", ipc_broken_peer_send },
{ "server/connection-ownership", server_connection_ownership }, { "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/machine-id-fallback", ibus_machine_id_fallback },
{ "ibus/startup-requires-ownership", ibus_startup_requires_ownership }, { "ibus/capability-policy", ibus_capability_policy },
{ "ibus/private-input-policy", ibus_private_input_policy },
{ "ibus/context-lifecycle", ibus_context_lifecycle }, { "ibus/context-lifecycle", ibus_context_lifecycle },
{ "ibus/active-release-lifecycle", ibus_active_release_lifecycle }, { "ibus/active-release-lifecycle", ibus_active_release_lifecycle },
{ "compose/sequences", compose_sequences },
{ "xim/keymap-lookup", xim_keymap_lookup },
{ "xim/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)
@@ -138,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);
}

1160
tests/xim_adapter_test.c Normal file

File diff suppressed because it is too large Load Diff

620
tests/xim_live_test.c Normal file
View File

@@ -0,0 +1,620 @@
#define _GNU_SOURCE
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
#include <errno.h>
#include <fcntl.h>
#include <locale.h>
#include <poll.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "ipc.h"
#include "live.h"
enum
{
Eventtimeout = 4000,
};
typedef struct Run Run;
typedef struct Prelog Prelog;
struct Run
{
Live l;
Daemon xvfb;
Daemon daemon;
};
struct Prelog
{
int start;
int nonempty;
int empty;
int done;
int doneafterempty;
int badorder;
};
static int xerror;
static int
waitsocket(Run *r)
{
struct stat st;
int64_t deadline;
deadline = nowms() + Starttimeout;
while(leftms(deadline) > 0){
if(lstat(r->l.socket, &st) == 0 && S_ISSOCK(st.st_mode) &&
(st.st_mode & 0777) == 0600)
return 1;
if(!daemonalive(&r->daemon))
return fail("daemon exited before creating its IPC socket");
pausems(20);
}
return fail("timed out waiting for daemon IPC socket");
}
static int
start(Run *r, char *daemon, char *mapdir, char *xvfb)
{
memset(r, 0, sizeof *r);
daemoninit(&r->xvfb, "Xvfb");
daemoninit(&r->daemon, "daemon");
if(!livesetup(&r->l, "xim") || !startxvfb(&r->l, &r->xvfb, xvfb))
return 0;
if(setenv("DISPLAY", r->l.display, 1) < 0 ||
setenv("XMODIFIERS", "@im=strans", 1) < 0)
return fail("set private environment: %s", strerror(errno));
if(!startdaemon(&r->l, &r->daemon, daemon, mapdir))
return 0;
return waitsocket(r);
}
static void
cleanup(Run *r, int noisy)
{
stopdaemon(&r->daemon);
stopdaemon(&r->xvfb);
if(noisy){
showerrors(&r->daemon);
showerrors(&r->xvfb);
}
closeerrors(&r->daemon);
closeerrors(&r->xvfb);
liveclean(&r->l);
}
static int
xerr(Display *dpy, XErrorEvent *e)
{
char msg[128];
xerror++;
XGetErrorText(dpy, e->error_code, msg, sizeof msg);
fail("X error: %s (request %d.%d)", msg, e->request_code, e->minor_code);
return 0;
}
static int
pumpinput(Display *dpy, XIC ic, char *commit, size_t cap, int timeout)
{
struct pollfd pfd;
XEvent ev;
KeySym sym;
Status status;
char buf[Ipcfieldmax+1];
int n;
XFlush(dpy);
if(XPending(dpy) == 0){
pfd.fd = ConnectionNumber(dpy);
pfd.events = POLLIN;
pfd.revents = 0;
while(poll(&pfd, 1, timeout) < 0 && errno == EINTR)
;
}
while(XPending(dpy) != 0){
XNextEvent(dpy, &ev);
if(XFilterEvent(&ev, None) || ic == NULL || ev.type != KeyPress)
continue;
n = Xutf8LookupString(ic, &ev.xkey, buf, sizeof buf - 1,
&sym, &status);
if(n < 0 || (status == XLookupChars &&
((size_t)n >= cap || commit == NULL)))
return 0;
if(status == XLookupChars && n != 0){
memcpy(commit, buf, n);
commit[n] = '\0';
}
}
return 1;
}
static void
pump(Display *dpy, int timeout)
{
pumpinput(dpy, NULL, NULL, 0, timeout);
}
static int
prestart(XIC ic, XPointer data, XPointer call)
{
Prelog *p;
(void)ic;
(void)call;
p = (Prelog*)data;
if(p->start != p->done)
p->badorder++;
p->start++;
return Ipcfieldmax;
}
static void
predraw(XIM im, XPointer data, XPointer call)
{
Prelog *p;
XIMPreeditDrawCallbackStruct *draw;
(void)im;
p = (Prelog*)data;
draw = (XIMPreeditDrawCallbackStruct*)call;
if(draw != NULL && draw->text != NULL && draw->text->length != 0){
if(p->start != p->done + 1)
p->badorder++;
p->nonempty++;
}else{
if(p->start != p->done + 1)
p->badorder++;
p->empty++;
}
}
static void
predone(XIM im, XPointer data, XPointer call)
{
Prelog *p;
(void)im;
(void)call;
p = (Prelog*)data;
if(p->empty > p->done)
p->doneafterempty++;
else
p->badorder++;
p->done++;
}
static void
precaret(XIM im, XPointer data, XPointer call)
{
(void)im;
(void)data;
(void)call;
}
static XIC
callbackic(XIM im, Window win, Prelog *log)
{
union {
int (*start)(XIC, XPointer, XPointer);
XIMProc xim;
} fn;
XIMCallback start, draw, done, caret;
XVaNestedList pre;
XIC ic;
start.client_data = (XPointer)log;
fn.start = prestart;
start.callback = fn.xim;
draw.client_data = (XPointer)log;
draw.callback = predraw;
done.client_data = (XPointer)log;
done.callback = predone;
caret.client_data = (XPointer)log;
caret.callback = precaret;
pre = XVaCreateNestedList(0,
XNPreeditStartCallback, &start,
XNPreeditDrawCallback, &draw,
XNPreeditDoneCallback, &done,
XNPreeditCaretCallback, &caret,
NULL);
if(pre == NULL)
return NULL;
ic = XCreateIC(im,
XNInputStyle, XIMPreeditCallbacks|XIMStatusNothing,
XNClientWindow, win,
XNFocusWindow, win,
XNPreeditAttributes, pre,
NULL);
XFree(pre);
return ic;
}
static int
sendkey(Display *dpy, XIC ic, Window win, KeySym sym, unsigned int state,
char *commit, size_t cap)
{
XKeyPressedEvent key;
XEvent ev;
KeySym got;
Status status;
char buf[Ipcfieldmax+1];
int n;
if(cap != 0)
commit[0] = '\0';
memset(&key, 0, sizeof key);
key.type = KeyPress;
key.display = dpy;
key.window = win;
key.root = DefaultRootWindow(dpy);
key.time = CurrentTime;
key.x = key.y = key.x_root = key.y_root = 1;
key.same_screen = True;
key.keycode = XKeysymToKeycode(dpy, sym);
key.state = state;
memset(&ev, 0, sizeof ev);
ev.xkey = key;
if(XFilterEvent(&ev, None)){
return pumpinput(dpy, ic, commit, cap, 20);
}
n = Xutf8LookupString(ic, &key, buf, sizeof buf - 1, &got, &status);
if(n < 0)
return fail("Xutf8LookupString failed for keysym %#lx",
(unsigned long)sym);
if(status == XLookupChars && n != 0){
if((size_t)n >= cap)
return fail("commit buffer overflow");
memcpy(commit, buf, n);
commit[n] = '\0';
}
return pumpinput(dpy, ic, commit, cap, 20);
}
static int
waitcommit(Display *dpy, XIC ic, char *commit, size_t cap)
{
int64_t deadline;
deadline = nowms() + Eventtimeout;
while(leftms(deadline) > 0){
if(!pumpinput(dpy, ic, commit, cap, leftms(deadline)))
return fail("read committed XIM text");
if(commit[0] != '\0')
return 1;
}
return 0;
}
static int
waitpreedit(Display *dpy, Prelog *p)
{
int64_t deadline;
deadline = nowms() + Eventtimeout;
while(leftms(deadline) > 0){
pump(dpy, leftms(deadline));
if(p->empty != 0 || p->done != 0 || p->badorder != 0)
return -1;
if(p->start != 0 && p->nonempty != 0)
return 1;
}
return 0;
}
static XIM
openim(Display *dpy)
{
XIM im;
int64_t deadline;
deadline = nowms() + Starttimeout;
do{
im = XOpenIM(dpy, NULL, "strans", "Strans");
if(im != NULL)
return im;
pump(dpy, 20);
}while(leftms(deadline) > 0);
return NULL;
}
static int
testcallbacks(Display *dpy, XIM im, Window win)
{
char commit[Ipcfieldmax+1], *reset;
Prelog log;
XIC ic;
memset(&log, 0, sizeof log);
ic = callbackic(im, win, &log);
if(ic == NULL)
return fail("create PreeditCallbacks input context");
XSetICFocus(ic);
if(!sendkey(dpy, ic, win, XK_s, ControlMask, commit, sizeof commit) ||
!sendkey(dpy, ic, win, XK_r, 0, commit, sizeof commit)){
XDestroyIC(ic);
return 0;
}
if(waitpreedit(dpy, &log) != 1){
int start, draw, empty, done;
start = log.start;
draw = log.nonempty;
empty = log.empty;
done = log.done;
XDestroyIC(ic);
return fail("invalid callback preedit: start=%d draw=%d empty=%d done=%d",
start, draw, empty, done);
}
pump(dpy, 50);
if(log.start != 1 || log.nonempty < 1 || log.done != 0 ||
log.empty != 0 || log.badorder != 0){
int start, draw, empty, done, ordered, bad;
start = log.start;
draw = log.nonempty;
empty = log.empty;
done = log.done;
ordered = log.doneafterempty;
bad = log.badorder;
XDestroyIC(ic);
return fail("bad initial callback sequence: start=%d draw=%d empty=%d done=%d ordered=%d bad=%d",
start, draw, empty, done, ordered, bad);
}
reset = Xutf8ResetIC(ic);
if(reset == NULL || strcmp(reset, "") != 0){
if(reset != NULL)
XFree(reset);
XDestroyIC(ic);
return fail("ResetIC did not return pending preedit");
}
XFree(reset);
if(!sendkey(dpy, ic, win, XK_r, 0, commit, sizeof commit) ||
!sendkey(dpy, ic, win, XK_k, 0, commit, sizeof commit) ||
!sendkey(dpy, ic, win, XK_Return, 0, commit, sizeof commit)){
XDestroyIC(ic);
return 0;
}
if(commit[0] == '\0' && !waitcommit(dpy, ic, commit, sizeof commit)){
XDestroyIC(ic);
return fail("input context did not compose after ResetIC");
}
if(strcmp(commit, "") != 0){
XDestroyIC(ic);
return fail("post-reset composition did not contain 가");
}
XUnsetICFocus(ic);
XDestroyIC(ic);
pump(dpy, 100);
if(log.badorder != 0)
return fail("bad callback order after ResetIC");
return 1;
}
/*
* The popup is the one viewable override-redirect window on the private
* display; created before the client window, it must still stack above it.
* The order and the map states have to be one observation: the daemon
* raises and maps between two ungrabbed round trips, and a stale order read
* against a fresh map state says the popup is below when it is not.
*/
static int
popupabove(Display *dpy, Window client)
{
XWindowAttributes wa;
Window root, parent, *kids;
unsigned i, n;
int64_t deadline;
int above;
deadline = nowms() + Eventtimeout;
for(;;){
above = 0;
root = DefaultRootWindow(dpy);
XGrabServer(dpy);
if(!XQueryTree(dpy, root, &root, &parent, &kids, &n)){
XUngrabServer(dpy);
return fail("query the window tree");
}
for(i = 0; i < n; i++){
if(kids[i] == client)
above = 1;
else if(XGetWindowAttributes(dpy, kids[i], &wa) &&
wa.override_redirect && wa.map_state == IsViewable)
break;
}
XFree(kids);
XUngrabServer(dpy);
if(i < n)
return above || fail("popup is stacked below the client");
if(leftms(deadline) == 0)
return fail("popup did not appear");
pump(dpy, 20);
}
}
static int
testnothing(Display *dpy, XIM im, Window win)
{
char commit[Ipcfieldmax+1];
XIC ic;
ic = XCreateIC(im,
XNInputStyle, XIMPreeditNothing|XIMStatusNothing,
XNClientWindow, win,
XNFocusWindow, win,
NULL);
if(ic == NULL)
return fail("create PreeditNothing input context");
XSetICFocus(ic);
if(!sendkey(dpy, ic, win, XK_s, ControlMask, commit, sizeof commit)){
XDestroyIC(ic);
return 0;
}
pump(dpy, 100);
if(!sendkey(dpy, ic, win, XK_r, 0, commit, sizeof commit) ||
!popupabove(dpy, win) ||
!sendkey(dpy, ic, win, XK_k, 0, commit, sizeof commit) ||
!sendkey(dpy, ic, win, XK_Return, 0, commit, sizeof commit)){
XDestroyIC(ic);
return 0;
}
if(commit[0] == '\0' &&
!waitcommit(dpy, ic, commit, sizeof commit)){
XDestroyIC(ic);
return fail("PreeditNothing composition produced no commit");
}
if(strcmp(commit, "\352\260\200") != 0){
XDestroyIC(ic);
return fail("PreeditNothing commit did not contain 가");
}
XUnsetICFocus(ic);
XDestroyIC(ic);
return 1;
}
static int
testposition(Display *dpy, XIM im, Window client, Window focus)
{
char commit[Ipcfieldmax+1];
char **missing, *def;
int nmissing;
XPoint spot;
XVaNestedList pre;
XFontSet fontset;
XIC ic;
missing = NULL;
nmissing = 0;
def = NULL;
fontset = XCreateFontSet(dpy, "fixed", &missing, &nmissing, &def);
if(missing != NULL)
XFreeStringList(missing);
if(fontset == NULL)
return fail("create XIM position font set");
spot.x = 7;
spot.y = 11;
pre = XVaCreateNestedList(0, XNSpotLocation, &spot,
XNFontSet, fontset, NULL);
if(pre == NULL){
XFreeFontSet(dpy, fontset);
return fail("create XNSpotLocation list");
}
ic = XCreateIC(im,
XNInputStyle, XIMPreeditPosition|XIMStatusNothing,
XNClientWindow, client,
XNFocusWindow, focus,
XNPreeditAttributes, pre,
NULL);
XFree(pre);
if(ic == NULL){
XFreeFontSet(dpy, fontset);
return fail("create nested PreeditPosition input context");
}
XSetICFocus(ic);
if(!sendkey(dpy, ic, focus, XK_s, ControlMask, commit, sizeof commit) ||
!sendkey(dpy, ic, focus, XK_r, 0, commit, sizeof commit)){
XDestroyIC(ic);
XFreeFontSet(dpy, fontset);
return 0;
}
spot.x = 13;
spot.y = 17;
pre = XVaCreateNestedList(0, XNSpotLocation, &spot, NULL);
if(pre == NULL || XSetICValues(ic, XNPreeditAttributes, pre, NULL) != NULL){
if(pre != NULL)
XFree(pre);
XDestroyIC(ic);
XFreeFontSet(dpy, fontset);
return fail("update nested XNSpotLocation");
}
XFree(pre);
if(!sendkey(dpy, ic, focus, XK_Escape, 0, commit, sizeof commit)){
XDestroyIC(ic);
XFreeFontSet(dpy, fontset);
return 0;
}
XUnsetICFocus(ic);
XDestroyIC(ic);
XFreeFontSet(dpy, fontset);
return 1;
}
int
main(int argc, char **argv)
{
Display *dpy;
Window client, focus, root;
XIM im;
Run run;
int ok;
char *xvfb;
testname = "xim_live_test";
if(argc != 3 && argc != 4){
fprintf(stderr, "usage: xim_live_test daemon mapdir [Xvfb]\n");
return 2;
}
xvfb = argc == 4 ? argv[3] : "Xvfb";
ok = start(&run, argv[1], argv[2], xvfb);
if(!ok){
cleanup(&run, 1);
return 1;
}
if(setlocale(LC_CTYPE, "C.UTF-8") == NULL || !XSupportsLocale() ||
XSetLocaleModifiers("@im=strans") == NULL){
fail("initialize UTF-8 X locale for strans");
cleanup(&run, 1);
return 1;
}
XSetErrorHandler(xerr);
dpy = XOpenDisplay(run.l.display);
if(dpy == NULL){
fail("open private display %s", run.l.display);
cleanup(&run, 1);
return 1;
}
im = openim(dpy);
if(im == NULL){
fail("open strans XIM on private display %s", run.l.display);
XCloseDisplay(dpy);
cleanup(&run, 1);
return 1;
}
root = DefaultRootWindow(dpy);
client = XCreateSimpleWindow(dpy, root, 41, 53, 240, 100, 0, 0, 0);
focus = XCreateSimpleWindow(dpy, client, 17, 19, 160, 50, 0, 0, 0);
XSelectInput(dpy, client, KeyPressMask|StructureNotifyMask);
XSelectInput(dpy, focus, KeyPressMask|StructureNotifyMask);
XMapWindow(dpy, client);
XMapWindow(dpy, focus);
XSync(dpy, False);
ok = testcallbacks(dpy, im, client) &&
testnothing(dpy, im, client) &&
testposition(dpy, im, client, focus);
XDestroyWindow(dpy, focus);
XDestroyWindow(dpy, client);
XCloseIM(im);
XSync(dpy, False);
XCloseDisplay(dpy);
if(xerror != 0){
fail("observed %d X protocol errors", xerror);
ok = 0;
}
cleanup(&run, !ok);
if(!ok)
return 1;
printf("xim_live_test: ok\n");
return 0;
}

View File

@@ -1,82 +0,0 @@
#include <assert.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <xcb-imdkit/encoding.h>
#include <xkbcommon/xkbcommon-keysyms.h>
uint32_t keymaplookup(const uint32_t*, int, uint16_t);
char *ximcompound(const char*, size_t, size_t*);
enum
{
ShiftMask = 1<<0,
LockMask = 1<<1,
Group1 = 1<<13,
};
static void
checktext(const char *s)
{
char *ct, *utf8;
size_t clen, len, ulen;
len = strlen(s);
ct = ximcompound(s, len, &clen);
assert(ct != NULL);
utf8 = xcb_compound_text_to_utf8(ct, clen, &ulen);
assert(utf8 != NULL);
assert(ulen == len);
assert(memcmp(utf8, s, len) == 0);
free(utf8);
free(ct);
}
static void
checkutf8run(void)
{
static const char s[] = "😀";
char *ct;
size_t n;
ct = ximcompound(s, sizeof s - 1, &n);
assert(ct != NULL);
assert(n >= 6);
assert(memcmp(ct, "\033%G", 3) == 0);
assert(memcmp(ct+n-3, "\033%@", 3) == 0);
free(ct);
}
int
main(void)
{
static const uint32_t letters[] = {
XKB_KEY_a, XKB_KEY_A, XKB_KEY_Cyrillic_ef, XKB_KEY_Cyrillic_EF,
};
static const uint32_t inferred[] = { XKB_KEY_a, XKB_KEY_NoSymbol };
static const uint32_t punctuation[] = { XKB_KEY_1, XKB_KEY_exclam };
static const uint32_t missing[] = {
XKB_KEY_a, XKB_KEY_A, XKB_KEY_NoSymbol, XKB_KEY_NoSymbol,
};
xcb_compound_text_init();
assert(keymaplookup(letters, 4, 0) == XKB_KEY_a);
assert(keymaplookup(letters, 4, ShiftMask) == XKB_KEY_A);
assert(keymaplookup(letters, 4, LockMask) == XKB_KEY_A);
assert(keymaplookup(letters, 4, LockMask|ShiftMask) == XKB_KEY_a);
assert(keymaplookup(letters, 4, Group1) == XKB_KEY_Cyrillic_ef);
assert(keymaplookup(letters, 4, Group1|ShiftMask) == XKB_KEY_Cyrillic_EF);
assert(keymaplookup(inferred, 2, ShiftMask) == XKB_KEY_A);
assert(keymaplookup(punctuation, 2, LockMask) == XKB_KEY_1);
assert(keymaplookup(punctuation, 2, LockMask|ShiftMask) == XKB_KEY_exclam);
assert(keymaplookup(missing, 4, Group1) == XKB_KEY_a);
assert(keymaplookup(NULL, 0, 0) == XKB_KEY_NoSymbol);
checktext("한글");
checktext("😀");
checktext("❤️");
checktext("A😀한");
checkutf8run();
return 0;
}

199
trie.c
View File

@@ -1,12 +1,19 @@
#include <errno.h>
#include "dat.h" #include "dat.h"
#include "fn.h" #include "fn.h"
/*
* A trie over the runes of a key. Maps need prefix matches while
* composing; dictionaries need exact matches; both are small enough for
* one structure. Node 0 is the root.
*/
static int static int
newnode(Trie *t) newnode(Trie *t)
{ {
int i; int i;
if(t->n >= t->cap){ if(t->n == t->cap){
t->cap *= 2; t->cap *= 2;
t->nodes = erealloc(t->nodes, t->cap * sizeof(Tnode)); t->nodes = erealloc(t->nodes, t->cap * sizeof(Tnode));
} }
@@ -17,42 +24,76 @@ newnode(Trie *t)
return i; return i;
} }
/* The children of a node are kept in rune order, so both end early. */
static int static int
find(Trie *t, int ni, char c) find(Trie *t, int ni, Rune c)
{ {
int pi; int pi;
for(pi = t->nodes[ni].child; pi >= 0; pi = t->nodes[pi].sibling) for(pi = t->nodes[ni].child; pi >= 0; pi = t->nodes[pi].sibling){
if(t->nodes[pi].c == c) if(t->nodes[pi].c == c)
return pi; return pi;
if(t->nodes[pi].c > c)
break;
}
return -1; return -1;
} }
static int static int
add(Trie *t, int ni, char c) add(Trie *t, int ni, Rune c)
{ {
int pi; int next, pi, prev;
prev = -1;
for(next = t->nodes[ni].child; next >= 0 && t->nodes[next].c < c;
next = t->nodes[next].sibling)
prev = next;
pi = newnode(t); pi = newnode(t);
t->nodes[pi].c = c; t->nodes[pi].c = c;
t->nodes[pi].sibling = t->nodes[ni].child; t->nodes[pi].sibling = next;
t->nodes[ni].child = pi; if(prev < 0)
t->nodes[ni].child = pi;
else
t->nodes[prev].sibling = pi;
return pi; return pi;
} }
static void Trie*
insert(Trie *t, char *key, int klen, char *val, int vlen) trienew(void)
{ {
int ni, ci; Trie *t;
int i;
ni = t->root; t = emalloc(sizeof(*t));
for(i = 0; i < klen; i++){ t->cap = 1024;
ci = find(t, ni, key[i]); t->nodes = emalloc(t->cap * sizeof(Tnode));
t->n = 0;
newnode(t);
return t;
}
void
trieput(Trie *t, char *key, int klen, char *val, int vlen)
{
Str k;
int ci, i, ni, same;
if(!sinit(&k, key, klen))
return;
ni = 0;
same = 1;
for(i = 0; i < k.n; i++){
if(same && i < t->last.n && t->last.r[i] == k.r[i]){
ni = t->path[i];
continue;
}
same = 0;
ci = find(t, ni, k.r[i]);
if(ci < 0) if(ci < 0)
ci = add(t, ni, key[i]); ci = add(t, ni, k.r[i]);
ni = ci; ni = ci;
t->path[i] = ni;
} }
t->last = k;
free(t->nodes[ni].val); free(t->nodes[ni].val);
t->nodes[ni].val = emalloc(vlen + 1); t->nodes[ni].val = emalloc(vlen + 1);
memmove(t->nodes[ni].val, val, vlen); memmove(t->nodes[ni].val, val, vlen);
@@ -60,47 +101,64 @@ insert(Trie *t, char *key, int klen, char *val, int vlen)
t->nodes[ni].vlen = vlen; t->nodes[ni].vlen = vlen;
} }
static char*
readline(Biobuf *b, char *path)
{
char *line;
errno = 0;
line = Brdstr(b, '\n', 1);
if(errno != 0)
die("can't read %s: %r", path);
return line;
}
/*
* A file holds "key<tab>value" lines; ';' starts a comment. Keys and each
* space-separated word of a value must fit a Str, which is what lookups
* hand back.
*/
Trie* Trie*
trieopen(char *path) trieopen(char *path)
{ {
Trie *t; Trie *t;
Biobuf *b; Biobuf *b;
char *line, *tab, *key, *val; Str s;
int klen, vlen; char *e, *line, *p, *tab;
int len, lineno;
b = Bopen(path, OREAD); b = Bopen(path, OREAD);
if(b == nil) if(b == nil)
die("can't open: %s", path); die("can't open %s: %r", path);
t = emalloc(sizeof(*t)); t = trienew();
t->cap = 1024; for(lineno = 1; (line = readline(b, path)) != nil; lineno++){
t->nodes = emalloc(t->cap * sizeof(Tnode)); len = Blinelen(b);
t->n = 0; if(memchr(line, '\0', len) != nil)
t->root = newnode(t); die("%s:%d: NUL byte", path, lineno);
while((line = Brdstr(b, '\n', 1)) != nil){ if(len > 0 && line[len-1] == '\r')
vlen = strlen(line); line[--len] = '\0';
if(vlen > 0 && line[vlen-1] == '\r') if(len == 0 || line[0] == ';'){
line[--vlen] = '\0';
if(line[0] == '\0' || line[0] == ';'){
free(line); free(line);
continue; continue;
} }
tab = strchr(line, '\t'); tab = memchr(line, '\t', len);
if(tab == nil || tab == line || tab[1] == '\0' || if(tab == nil || tab == line || tab == line+len-1 ||
strchr(tab+1, '\t') != nil) memchr(tab+1, '\t', line+len-(tab+1)) != nil)
die("malformed map: %s", path); die("%s:%d: expected key, tab, value", path, lineno);
*tab = '\0'; if(!sinit(&s, line, tab-line))
key = line; die("%s:%d: invalid or oversized key", path, lineno);
klen = tab - line; for(p = tab+1; p < line+len; p = e+1){
if(utflen(key) > Maxrunes) e = memchr(p, ' ', line+len-p);
die("map key too long: %s", path); if(e == nil)
val = tab + 1; e = line+len;
vlen = strlen(val); if(!sinit(&s, p, e-p))
if(utflen(val) > Maxrunes) die("%s:%d: invalid or oversized value", path, lineno);
die("map value too long: %s", path); }
insert(t, key, klen, val, vlen); trieput(t, line, tab-line, tab+1, line+len-(tab+1));
free(line); free(line);
} }
Bterm(b); if(Bterm(b) < 0)
die("can't close %s: %r", path);
return t; return t;
} }
@@ -117,46 +175,33 @@ trieclose(Trie *t)
free(t); free(t);
} }
char* /* The node key leads to, or -1. A nil trie is an unloaded map: every key misses. */
trieget(Trie *t, char *key, int klen, int *vlen) int
trienode(Trie *t, Str *key)
{ {
int ni; int i, ni;
int i;
if(t == nil || klen < 0 || (klen > 0 && key == nil)) if(t == nil)
return nil; return -1;
ni = t->root; ni = 0;
for(i = 0; i < klen; i++){ for(i = 0; i < key->n && ni >= 0; i++)
ni = find(t, ni, key[i]); ni = find(t, ni, key->r[i]);
if(ni < 0) return ni;
return nil;
}
if(t->nodes[ni].val == nil)
return nil;
if(vlen != nil)
*vlen = t->nodes[ni].vlen;
return t->nodes[ni].val;
} }
int int
trielookup(Trie *t, char *key, int klen, char **val, int *vlen) trielookup(Trie *t, Str *key, char **val, int *vlen)
{ {
int ni; int ni;
int i;
if(t == nil || klen < 0 || (klen > 0 && key == nil)) *val = nil;
return 0; *vlen = 0;
ni = t->root; ni = trienode(t, key);
for(i = 0; i < klen; i++){ if(ni < 0)
ni = find(t, ni, key[i]); return TrieMiss;
if(ni < 0) if(t->nodes[ni].val == nil)
return 0; return TriePrefix;
} *val = t->nodes[ni].val;
if(t->nodes[ni].val != nil){ *vlen = t->nodes[ni].vlen;
if(val != nil) return TrieExact;
*val = t->nodes[ni].val;
if(vlen != nil)
*vlen = t->nodes[ni].vlen;
}
return 1;
} }

69
vi.c
View File

@@ -31,12 +31,6 @@ static struct {
{L'Y', {L'Ý', L'', L'', L'', L''}}, {L'Y', {L'Ý', L'', L'', L'', L''}},
}; };
static int
istone(Rune c)
{
return c == 's' || c == 'f' || c == 'r' || c == 'x' || c == 'j';
}
static int static int
toneidx(Rune c) toneidx(Rune c)
{ {
@@ -114,20 +108,44 @@ onsetglide(Str *m)
return -1; return -1;
} }
Emit /*
transvi(Im *im, Rune c) * The pending text is the key sequence itself, read through the map for
* display; raw keeps every key of it so Backspace can replay one fewer.
*/
static Emit
history(Im *im, Rune c, Emit e)
{ {
Emit e, mappedkey; Str mapped;
e.raw = im->raw;
if(e.s.n > 0 || !e.eat)
sclear(&e.raw);
if(!e.eat || e.next.n == 0)
return e;
if(e.raw.n < Maxrunes){
sputr(&e.raw, c);
return e;
}
/* The history is full: flush the composed text as it stands. */
if(!mapget(im->l->map, &e.next, &mapped))
mapped = e.next;
sappend(&e.s, &mapped);
sclear(&e.next);
sclear(&e.raw);
return e;
}
static Emit
tone(Im *im, Rune c)
{
Emit e;
Str mapped, pre; Str mapped, pre;
int i, tidx, vi, last, penult, glide; int i, tidx, vi, last, penult, glide;
Rune v, b1, b2; Rune v, b1, b2;
if(!istone(c) && c != 'z') e = transmap(im, c);
return transmap(im, c); if(e.eat)
mappedkey = transmap(im, c); return e;
if(mappedkey.eat)
return mappedkey;
memset(&e, 0, sizeof e); memset(&e, 0, sizeof e);
if(im->pre.n == 0) if(im->pre.n == 0)
return e; return e;
@@ -175,17 +193,24 @@ transvi(Im *im, Rune c)
return e; return e;
} }
if(c == 'z') tidx = toneidx(c);
if(tidx < 0)
mapped.r[vi] = removetone(mapped.r[vi]); mapped.r[vi] = removetone(mapped.r[vi]);
else{ else
tidx = toneidx(c);
mapped.r[vi] = applytone(mapped.r[vi], tidx); mapped.r[vi] = applytone(mapped.r[vi], tidx);
}
e.eat = 1; e.eat = 1;
e.next = mapped; e.next = mapped;
return e; return e;
} }
Emit
transvi(Im *im, Rune c)
{
if(toneidx(c) < 0 && c != 'z')
return history(im, c, transmap(im, c));
return history(im, c, tone(im, c));
}
void void
backvi(Im *im) backvi(Im *im)
{ {
@@ -200,10 +225,10 @@ backvi(Im *im)
raw = im->raw; raw = im->raw;
spopr(&raw); spopr(&raw);
sclear(&im->pre); sclear(&im->pre);
sclear(&im->raw);
for(i = 0; i < raw.n; i++){ for(i = 0; i < raw.n; i++){
e = transvi(im, raw.r[i]); e = transvi(im, raw.r[i]);
sclear(&im->pre); im->pre = e.next;
sappend(&im->pre, &e.next); im->raw = e.raw;
} }
im->raw = raw;
} }

371
win.c
View File

@@ -1,32 +1,19 @@
#include <xcb/xcb.h> #include <xcb/xcb.h>
#include <xcb/xcb_aux.h>
#include <xcb/randr.h>
#include "dat.h" #include "dat.h"
#include "fn.h" #include "fn.h"
enum {
Asciitofull = 0xFEE0,
};
extern char **fontfiles;
extern int nfontfiles;
static xcb_connection_t *conn; static xcb_connection_t *conn;
static xcb_screen_t *scr; static xcb_screen_t *scr;
static xcb_window_t win; static xcb_window_t win;
static xcb_gcontext_t gc; static xcb_gcontext_t gc;
static xcb_pixmap_t pix; static xcb_pixmap_t pix;
static u32int *img; static u32int *img;
static int depth; static xcb_atom_t currentdesktop;
static xcb_atom_t workarea;
static xcb_screen_t* static int hasrandr, imgh, imgw;
getscr(xcb_connection_t *c, int n) static int shown, ptrx, ptry; /* the popup, and where the pointer was as it came up */
{
xcb_screen_iterator_t i;
for(i = xcb_setup_roots_iterator(xcb_get_setup(c)); i.rem; xcb_screen_next(&i))
if(n-- == 0)
return i.data;
return nil;
}
static xcb_visualtype_t* static xcb_visualtype_t*
getvisual(xcb_screen_t *s) getvisual(xcb_screen_t *s)
@@ -46,6 +33,7 @@ getvisual(xcb_screen_t *s)
return nil; return nil;
} }
/* putimage writes native-endian x8r8g8b8; the root must take that. */
static int static int
validformat(xcb_connection_t *c, xcb_screen_t *s) validformat(xcb_connection_t *c, xcb_screen_t *s)
{ {
@@ -57,9 +45,8 @@ validformat(xcb_connection_t *c, xcb_screen_t *s)
setup = xcb_get_setup(c); setup = xcb_get_setup(c);
v = getvisual(s); v = getvisual(s);
if(setup == nil || v == nil || s->root_depth != 24 || if(v == nil || s->root_depth != 24 ||
v->_class != XCB_VISUAL_CLASS_TRUE_COLOR || v->_class != XCB_VISUAL_CLASS_TRUE_COLOR ||
v->bits_per_rgb_value != 8 ||
v->red_mask != 0xff0000 || v->green_mask != 0x00ff00 || v->red_mask != 0xff0000 || v->green_mask != 0x00ff00 ||
v->blue_mask != 0x0000ff) v->blue_mask != 0x0000ff)
return 0; return 0;
@@ -76,177 +63,298 @@ validformat(xcb_connection_t *c, xcb_screen_t *s)
return 0; return 0;
} }
static xcb_atom_t
getatom(char *name)
{
xcb_intern_atom_cookie_t cookie;
xcb_intern_atom_reply_t *reply;
xcb_atom_t atom;
cookie = xcb_intern_atom(conn, 0, strlen(name), name);
reply = xcb_intern_atom_reply(conn, cookie, nil);
atom = reply == nil ? XCB_ATOM_NONE : reply->atom;
free(reply);
return atom;
}
static int
getcardinals(xcb_atom_t atom, u32int off, int n, u32int *v)
{
xcb_get_property_cookie_t cookie;
xcb_get_property_reply_t *reply;
int ok;
if(atom == XCB_ATOM_NONE)
return 0;
cookie = xcb_get_property(conn, 0, scr->root, atom,
XCB_ATOM_CARDINAL, off, n);
reply = xcb_get_property_reply(conn, cookie, nil);
ok = reply != nil && reply->type == XCB_ATOM_CARDINAL &&
reply->format == 32 &&
xcb_get_property_value_length(reply) == n*(int)sizeof v[0];
if(ok)
memmove(v, xcb_get_property_value(reply), n*sizeof v[0]);
free(reply);
return ok;
}
static int
getworkarea(Area *a)
{
u32int desktop, v[4];
if(!getcardinals(currentdesktop, 0, 1, &desktop) ||
desktop > 0x3fffffff ||
!getcardinals(workarea, desktop*4, 4, v) ||
v[0] > 0x7fffffff || v[1] > 0x7fffffff ||
v[2] == 0 || v[2] > 0x7fffffff ||
v[3] == 0 || v[3] > 0x7fffffff)
return 0;
a->x = v[0];
a->y = v[1];
a->w = v[2];
a->h = v[3];
return 1;
}
static void
getrootarea(Area *a)
{
xcb_get_geometry_cookie_t cookie;
xcb_get_geometry_reply_t *reply;
a->x = 0;
a->y = 0;
a->w = scr->width_in_pixels;
a->h = scr->height_in_pixels;
cookie = xcb_get_geometry(conn, scr->root);
reply = xcb_get_geometry_reply(conn, cookie, nil);
if(reply != nil && reply->width > 0 && reply->height > 0){
a->w = reply->width;
a->h = reply->height;
}
free(reply);
}
/* The area the popup may use: the RandR monitor under (x, y), or the
* root, cut down to the EWMH work area. */
static void
popupwork(int x, int y, Area *out)
{
xcb_randr_get_monitors_cookie_t cookie;
xcb_randr_get_monitors_reply_t *reply;
xcb_randr_monitor_info_iterator_t it;
Area root, net, *mon;
int n;
mon = nil;
n = 0;
reply = nil;
if(hasrandr){
cookie = xcb_randr_get_monitors(conn, scr->root, 1);
reply = xcb_randr_get_monitors_reply(conn, cookie, nil);
}
if(reply != nil && xcb_randr_get_monitors_monitors_length(reply) > 0){
mon = emalloc(xcb_randr_get_monitors_monitors_length(reply) *
sizeof mon[0]);
it = xcb_randr_get_monitors_monitors_iterator(reply);
for(; it.rem; xcb_randr_monitor_info_next(&it)){
if(it.data->width == 0 || it.data->height == 0)
continue;
mon[n].x = it.data->x;
mon[n].y = it.data->y;
mon[n].w = it.data->width;
mon[n].h = it.data->height;
n++;
}
}
if(n == 0){
free(mon);
getrootarea(&root);
mon = &root;
n = 1;
}
popuparea(mon, n, getworkarea(&net) ? &net : nil, x, y, out);
if(mon != &root)
free(mon);
free(reply);
}
static void static void
wincleanup(void) wincleanup(void)
{ {
textclose();
free(img); free(img);
img = nil; xcb_disconnect(conn);
if(conn != nil){
xcb_disconnect(conn);
conn = nil;
}
scr = nil;
win = 0;
gc = 0;
pix = 0;
} }
static int static int
wininit(void) wininit(void)
{ {
int n; int n;
u32int mask, vals[4]; u32int mask, vals[5];
xcb_intern_atom_cookie_t c1, c2; xcb_atom_t type, tooltip;
xcb_intern_atom_reply_t *r1, *r2; xcb_randr_query_version_cookie_t rc;
xcb_randr_query_version_reply_t *rr;
const xcb_query_extension_reply_t *rext;
conn = xcb_connect(nil, &n); conn = xcb_connect(nil, &n);
if(conn == nil || xcb_connection_has_error(conn)){ if(xcb_connection_has_error(conn)){
fprint(2, "strans: popup disabled: cannot connect to X display\n"); fprint(2, "strans: popup disabled: cannot connect to X display\n");
wincleanup(); wincleanup();
return 0; return 0;
} }
scr = getscr(conn, n); scr = xcb_aux_get_screen(conn, n);
if(scr == nil || !validformat(conn, scr)){ if(scr == nil || !validformat(conn, scr)){
fprint(2, "strans: popup disabled: unsupported X root format\n"); fprint(2, "strans: popup disabled: unsupported X root format\n");
wincleanup(); wincleanup();
return 0; return 0;
} }
depth = scr->root_depth; rext = xcb_get_extension_data(conn, &xcb_randr_id);
if(rext != nil && rext->present){
rc = xcb_randr_query_version(conn, 1, 5);
rr = xcb_randr_query_version_reply(conn, rc, nil);
hasrandr = rr != nil && (rr->major_version > 1 ||
(rr->major_version == 1 && rr->minor_version >= 5));
free(rr);
}
currentdesktop = getatom("_NET_CURRENT_DESKTOP");
workarea = getatom("_NET_WORKAREA");
win = xcb_generate_id(conn); win = xcb_generate_id(conn);
/* Clicks on the popup go nowhere, not to the root window under it. */
mask = XCB_CW_BACK_PIXEL | XCB_CW_BORDER_PIXEL | mask = XCB_CW_BACK_PIXEL | XCB_CW_BORDER_PIXEL |
XCB_CW_OVERRIDE_REDIRECT | XCB_CW_SAVE_UNDER; XCB_CW_OVERRIDE_REDIRECT | XCB_CW_SAVE_UNDER |
XCB_CW_DONT_PROPAGATE;
vals[0] = Colbg; vals[0] = Colbg;
vals[1] = 0; vals[1] = Colsep;
vals[2] = 1; vals[2] = 1;
vals[3] = 1; vals[3] = 1;
vals[4] = XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE;
xcb_create_window(conn, XCB_COPY_FROM_PARENT, win, scr->root, xcb_create_window(conn, XCB_COPY_FROM_PARENT, win, scr->root,
0, 0, 1, 1, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, 0, 0, 1, 1, PopupBorder, XCB_WINDOW_CLASS_INPUT_OUTPUT,
scr->root_visual, mask, vals); scr->root_visual, mask, vals);
c1 = xcb_intern_atom(conn, 0, type = getatom("_NET_WM_WINDOW_TYPE");
strlen("_NET_WM_WINDOW_TYPE"), "_NET_WM_WINDOW_TYPE"); tooltip = getatom("_NET_WM_WINDOW_TYPE_TOOLTIP");
c2 = xcb_intern_atom(conn, 0, if(type != XCB_ATOM_NONE && tooltip != XCB_ATOM_NONE)
strlen("_NET_WM_WINDOW_TYPE_TOOLTIP"),
"_NET_WM_WINDOW_TYPE_TOOLTIP");
r1 = xcb_intern_atom_reply(conn, c1, nil);
r2 = xcb_intern_atom_reply(conn, c2, nil);
if(r1 != nil && r2 != nil)
xcb_change_property(conn, XCB_PROP_MODE_REPLACE, xcb_change_property(conn, XCB_PROP_MODE_REPLACE,
win, r1->atom, XCB_ATOM_ATOM, 32, 1, &r2->atom); win, type, XCB_ATOM_ATOM, 32, 1, &tooltip);
free(r1);
free(r2);
gc = xcb_generate_id(conn); gc = xcb_generate_id(conn);
xcb_create_gc(conn, gc, win, 0, nil); xcb_create_gc(conn, gc, win, 0, nil);
pix = xcb_generate_id(conn); textinit();
xcb_create_pixmap(conn, depth, pix, win, Imgw, Imgh);
mask = XCB_CW_BACK_PIXMAP;
xcb_change_window_attributes(conn, win, mask, &pix);
img = emalloc(Imgw * Imgh * sizeof(img[0]));
if(!fontinit(fontfiles, nfontfiles)){
fprint(2, "strans: popup disabled: no usable fonts\n");
wincleanup();
return 0;
}
return 1; return 1;
} }
static void /* The image and its retained pixmap only ever grow. */
drawstr(u32int *buf, int x, int y, Rune *r, int n, int maxw, int maxh) static int
resizebacking(int w, int h)
{ {
while(n-- > 0){ xcb_generic_error_t *err;
if(*r != 0xfe0e && *r != 0xfe0f){ xcb_pixmap_t old, new;
putfont(buf, maxw, maxh, x, y, *r); xcb_void_cookie_t cookie;
x += Fontsz; int nh, nw;
}
r++; if(w <= imgw && h <= imgh)
return 1;
nw = max(w, imgw);
nh = max(h, imgh);
img = erealloc(img, (ulong)nw * nh * sizeof img[0]);
new = xcb_generate_id(conn);
cookie = xcb_create_pixmap_checked(conn, scr->root_depth, new, win,
nw, nh);
err = xcb_request_check(conn, cookie);
if(err != nil){
free(err);
return 0;
} }
} cookie = xcb_change_window_attributes_checked(conn, win,
XCB_CW_BACK_PIXMAP, &new);
static void err = xcb_request_check(conn, cookie);
fill(u32int *buf, int n, u32int color) if(err != nil){
{ free(err);
int i; xcb_free_pixmap(conn, new);
return 0;
for(i = 0; i < n; i++)
buf[i] = color;
}
static void
drawkouho(Drawcmd *dc, int n, int w, int h)
{
int npre, sely, y, i;
Str *s;
fill(img, w * h, Colbg);
npre = dc->pre.n != 0;
if(npre)
drawstr(img, 0, 0, dc->pre.r, dc->pre.n, w, h);
if(dc->sel >= 0 && dc->sel < n){
sely = (npre + dc->sel) * Fontsz;
fill(img + sely * w, Fontsz * w, Colsel);
}
for(i = 0, y = npre * Fontsz; i < n; i++, y += Fontsz){
s = &dc->kouho[i];
putfont(img, w, h, 0, y, '1' + i + Asciitofull);
drawstr(img, 2*Fontsz, y, s->r, s->n, w, h);
} }
old = pix;
pix = new;
imgw = nw;
imgh = nh;
if(old != 0)
xcb_free_pixmap(conn, old);
return 1;
} }
static void static void
putimage(int w, int h) putimage(int w, int h)
{ {
xcb_put_image(conn, XCB_IMAGE_FORMAT_Z_PIXMAP, pix, gc, xcb_put_image(conn, XCB_IMAGE_FORMAT_Z_PIXMAP, pix, gc,
w, h, 0, 0, 0, depth, w * h * 4, (u8int*)img); w, h, 0, 0, 0, scr->root_depth, w * h * 4, (u8int*)img);
/* The retained background pixmap lets the server repaint exposures. */ /* The retained background pixmap lets the server repaint exposures. */
xcb_clear_area(conn, 0, win, 0, 0, w, h); xcb_clear_area(conn, 0, win, 0, 0, w, h);
} }
static int static void
winhide(void) winhide(void)
{ {
xcb_unmap_window(conn, win); xcb_unmap_window(conn, win);
return xcb_flush(conn) > 0; xcb_flush(conn);
shown = 0;
} }
static int /*
* Draws dc at the caret, or by the pointer when the caret is unknown:
* where the pointer was as the popup came up, so that it does not
* follow the mouse from key to key.
*/
static void
winshow(Drawcmd *dc) winshow(Drawcmd *dc)
{ {
int npre, px, py, w, h, i, n, maxw; Area area;
u32int vals[4]; Popup p;
int ax, ay, x, y;
u32int vals[5];
xcb_query_pointer_reply_t *ptr; xcb_query_pointer_reply_t *ptr;
xcb_query_pointer_cookie_t cookie; xcb_query_pointer_cookie_t cookie;
n = min(max(dc->nkouho, 0), Maxdisp); if(dc->nkouho == 0 && dc->pre.n == 0){
npre = dc->pre.n != 0; winhide();
if(n == 0 && npre == 0){ return;
return winhide();
} }
maxw = popupcells(dc->pre.r, dc->pre.n); if(!dc->caret.valid && !shown){
for(i = 0; i < n; i++)
maxw = max(maxw, popupcells(dc->kouho[i].r, dc->kouho[i].n));
vals[3] = h = (n + npre) * Fontsz;
vals[2] = w = (maxw + 3) * Fontsz;
px = py = 0;
if(!dc->caret.valid){
cookie = xcb_query_pointer(conn, scr->root); cookie = xcb_query_pointer(conn, scr->root);
ptr = xcb_query_pointer_reply(conn, cookie, nil); ptr = xcb_query_pointer_reply(conn, cookie, nil);
if(ptr == nil) if(ptr == nil)
return 0; return;
px = ptr->root_x; ptrx = ptr->root_x;
py = ptr->root_y; ptry = ptr->root_y;
free(ptr); free(ptr);
} }
popupposition(&dc->caret, px, py, scr->width_in_pixels, ax = dc->caret.valid ? dc->caret.x : ptrx;
scr->height_in_pixels, w, h, &px, &py); ay = dc->caret.valid ? dc->caret.y : ptry;
vals[1] = py; popupwork(ax, ay, &area);
vals[0] = px; popuplayout(dc, area.w, area.h, &p);
if(p.w <= 0 || p.h <= 0){
winhide();
return;
}
if(!resizebacking(p.w, p.h))
return;
popupposition(&dc->caret, ptrx, ptry, &area, p.w + 2*PopupBorder,
p.h + 2*PopupBorder, &x, &y);
/* Mapping does not restack: raise above windows opened since. */
vals[0] = x;
vals[1] = y;
vals[2] = p.w;
vals[3] = p.h;
vals[4] = XCB_STACK_MODE_ABOVE;
xcb_configure_window(conn, win, xcb_configure_window(conn, win,
XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y | XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y |
XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT, XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT |
vals); XCB_CONFIG_WINDOW_STACK_MODE, vals);
popupdraw(img, dc, &p);
putimage(p.w, p.h);
xcb_map_window(conn, win); xcb_map_window(conn, win);
drawkouho(dc, n, w, h); xcb_flush(conn);
putimage(w, h); shown = 1;
return xcb_flush(conn) > 0;
} }
void void
@@ -257,14 +365,7 @@ drawthread(void*)
threadsetname("draw"); threadsetname("draw");
if(!wininit()) if(!wininit())
return; return;
while(chanrecv(drawc, &dc) > 0){ while(chanrecv(drawc, &dc) > 0 && !xcb_connection_has_error(conn))
while(channbrecv(drawc, &dc) > 0) winshow(&dc);
;
if(dc.nkouho == 0 && dc.pre.n == 0){
if(!winhide())
break;
}else if(!winshow(&dc))
break;
}
wincleanup(); wincleanup();
} }

798
wl.c Normal file
View File

@@ -0,0 +1,798 @@
#include "dat.h"
#include "fn.h"
#include <errno.h>
#include <poll.h>
#include <sys/mman.h>
#include <time.h>
#include <wayland-client.h>
#include <xkbcommon/xkbcommon.h>
#include "imv2.h"
#include "vkv1.h"
enum
{
Maxcode = 0x300, /* KEY_MAX+1: what evdev can send */
};
/*
* What popuplayout may use. The compositor puts the popup at the text
* cursor and constrains it there, so this need only be wide enough for
* the layout's own width and tall enough for every row it can show.
*/
#define Popupw PopupBasew
#define Popuph ((Maxdisp + 2)*Fontsz + 2*PopupPad + PopupSep)
/*
* One of two shm buffers, busy from attach until the compositor releases
* it. win.c's single grow-only image would be redrawn while the
* compositor was still reading it.
*/
typedef struct Buf Buf;
struct Buf
{
struct wl_buffer *b;
u32int *img;
int size;
int w;
int h;
int busy;
};
static struct wl_display *display;
static struct wl_seat *seat;
static struct wl_compositor *comp;
static struct wl_shm *shm;
static struct wl_surface *surface;
static struct zwp_input_popup_surface_v2 *popsurf;
static struct zwp_input_method_manager_v2 *immgr;
static struct zwp_virtual_keyboard_manager_v1 *vkmgr;
static struct zwp_input_method_v2 *im;
static struct zwp_virtual_keyboard_v1 *vk;
static struct zwp_input_method_keyboard_grab_v2 *grab;
static struct xkb_context *xkbctx;
static struct xkb_state *kstate;
static Channel *replyc;
/*
* The protocol gives one input context per seat, so the engine knows us
* by one address; nil would mean no owner at all.
*/
static int context;
static u32int serial; /* done events counted; the serial commit takes */
static int active; /* a text input is being served */
static int pendingactive;
static int activated; /* an activate arrived since the last done */
static u32int purpose, pendingpurpose;
static Str surround, pendingsurround; /* the client's text before the cursor */
static u32int sent[Maxcode/32]; /* keycodes passed on and still down */
static u32int lasttime; /* the compositor's clock, for a release of our own */
static int reprate; /* repeats a second the seat asks for; 0 is none */
static int repdelay; /* ms a key is held before the first repeat */
static u32int repcode; /* the key repeating */
static vlong repdue; /* when its next repeat falls due; 0 if none */
static int engaged; /* the engine owes this context a release */
static int preshown; /* our preedit is on the client's screen */
static int haskeymap;
static int gone; /* unavailable: the seat is another input method's */
static Buf bufs[2];
static int shown; /* a buffer is attached and the popup is up */
static Drawcmd held; /* a picture that came while both buffers were busy */
static int helddraw;
static void grabkeyboard(void);
static void popup(Drawcmd*);
static void
wllog(char *msg)
{
fprint(2, "strans: wl: %s\n", msg);
}
/* A repeat deadline needs a clock that cannot step; nsec() is not one. */
static vlong
nowms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (vlong)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
static int
hidden(void)
{
return purpose == Purposepassword || purpose == Purposepin;
}
static void
sendrequest(int op, u32int ks, u32int mod, Keyres *res)
{
Keyreq kr;
memset(&kr, 0, sizeof kr);
kr.owner = &context;
kr.clientpre = 1; /* set_preedit_string draws it; the popup does not */
kr.op = op;
kr.ks = ks;
kr.mod = mod;
kr.surround = surround;
kr.reply = replyc;
chansend(keyc, &kr);
chanrecv(replyc, res);
}
/* The bytes the last n runes of the client's text take. */
static int
backbytes(int n)
{
int i, nb;
nb = 0;
for(i = max(surround.n - n, 0); i < surround.n; i++)
nb += runelen(surround.r[i]);
return nb;
}
/* The engine's modifier bits, read from the grab's own keyboard state. */
static u32int
modmask(void)
{
static char *name[] = {
XKB_MOD_NAME_SHIFT, XKB_MOD_NAME_CTRL,
XKB_MOD_NAME_ALT, XKB_MOD_NAME_LOGO,
};
static u32int bit[] = {Mshift, Mctrl, Malt, Msuper};
u32int m;
int i;
m = 0;
for(i = 0; i < nelem(name); i++)
if(xkb_state_mod_name_is_active(kstate, name[i],
XKB_STATE_MODS_EFFECTIVE) > 0)
m |= bit[i];
return m;
}
static int
issent(u32int code)
{
return code < Maxcode && (sent[code/32] & 1<<(code%32)) != 0;
}
/*
* A key the engine did not take goes to the client as a key, not as
* text: eaten = 0 means it is not ours, and committing it would lie to a
* terminal and to anything with a keybinding.
*/
static void
forward(u32int code, u32int state)
{
if(!haskeymap || code >= Maxcode)
return;
zwp_virtual_keyboard_v1_key(vk, lasttime, code, state);
if(state == WL_KEYBOARD_KEY_STATE_PRESSED)
sent[code/32] |= 1<<(code%32);
else
sent[code/32] &= ~(1<<(code%32));
}
/* Whatever we passed on is still down; the client must not keep it. */
static void
releasekeys(void)
{
u32int code;
for(code = 0; code < Maxcode; code++)
if(issent(code))
forward(code, WL_KEYBOARD_KEY_STATE_RELEASED);
}
/*
* The engine's text, then the composed text, in one commit_string: the
* compositor keeps only the last one sent before a commit. A commit is
* an event and goes only when there is something, while a preedit is
* state and goes every time, empty to take the last one down.
*/
static void
answer(Keyres *res, char *tail)
{
char utf[2*Maxutf];
int n;
/* The compositor takes the text back before it inserts ours. */
if(res->del > 0)
zwp_input_method_v2_delete_surrounding_text(im,
backbytes(res->del), 0);
n = stoutf(&res->commit, utf, sizeof utf);
if(tail[0] != '\0')
n += snprint(utf+n, sizeof utf - n, "%s", tail);
if(n > 0)
zwp_input_method_v2_commit_string(im, utf);
n = stoutf(&res->preedit, utf, sizeof utf);
if(n > 0 || preshown) /* an empty one only to take the last down */
zwp_input_method_v2_set_preedit_string(im, utf, n, n);
zwp_input_method_v2_commit(im, serial);
preshown = n > 0;
}
static void
bufrelease(void *p, struct wl_buffer*)
{
((Buf*)p)->busy = 0;
if(helddraw){
helddraw = 0;
popup(&held);
}
}
static const struct wl_buffer_listener buflisten = {bufrelease};
static void
bufclear(Buf *p)
{
if(p->b != nil)
wl_buffer_destroy(p->b);
if(p->img != nil)
munmap(p->img, p->size);
memset(p, 0, sizeof *p);
}
/* An anonymous file of pixels, shared with the compositor. */
static int
shmfd(int size)
{
char path[] = "/dev/shm/strans-XXXXXX";
int fd;
fd = mkstemp(path);
if(fd < 0)
return -1;
unlink(path);
if(ftruncate(fd, size) < 0){
close(fd);
return -1;
}
return fd;
}
/*
* A free buffer of the wanted size. Sizes differ from draw to draw, so
* a free one of the wrong size is remade; both busy means the compositor
* is still reading them and the picture has to wait.
*/
static Buf*
getbuf(int w, int h)
{
struct wl_shm_pool *pool;
Buf *p;
int fd, i, size;
for(i = 0; i < nelem(bufs); i++)
if(!bufs[i].busy && bufs[i].w == w && bufs[i].h == h)
return &bufs[i];
for(i = 0; i < nelem(bufs); i++)
if(!bufs[i].busy)
break;
if(i == nelem(bufs))
return nil;
p = &bufs[i];
bufclear(p);
size = w * h * sizeof p->img[0];
fd = shmfd(size);
if(fd < 0)
return nil;
p->img = mmap(nil, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(p->img == MAP_FAILED){
p->img = nil;
close(fd);
return nil;
}
pool = wl_shm_create_pool(shm, fd, size);
p->b = wl_shm_pool_create_buffer(pool, 0, w, h,
w * sizeof p->img[0], WL_SHM_FORMAT_XRGB8888);
wl_shm_pool_destroy(pool);
close(fd);
wl_buffer_add_listener(p->b, &buflisten, p);
p->size = size;
p->w = w;
p->h = h;
return p;
}
/* Unmapping takes the popup down; the compositor puts it back at the
* next activate with whatever buffer is still attached. */
static void
hidepopup(void)
{
helddraw = 0; /* a picture still waiting for a buffer is stale now */
if(!shown)
return;
wl_surface_attach(surface, nil, 0, 0);
wl_surface_commit(surface);
shown = 0;
}
static void
popup(Drawcmd *dc)
{
Popup p;
Buf *b;
popuplayout(dc, Popupw, Popuph, &p);
if(p.w <= 0 || p.h <= 0){
hidepopup();
return;
}
/* A buffer that is not a multiple of the scale is an invalid_size
* error at attach, which would kill the connection. */
p.w = (p.w + popupscale - 1) / popupscale * popupscale;
p.h = (p.h + popupscale - 1) / popupscale * popupscale;
b = getbuf(p.w, p.h);
if(b == nil){
held = *dc; /* the engine will not send it twice */
helddraw = 1;
return;
}
popupdraw(b->img, dc, &p);
wl_surface_attach(surface, b->b, 0, 0);
wl_surface_set_buffer_scale(surface, popupscale);
wl_surface_damage_buffer(surface, 0, 0, p.w, p.h);
wl_surface_commit(surface);
b->busy = 1;
shown = 1;
}
/*
* drawc is empty or holds exactly what the engine last published, so one
* take is enough. lastdraw follows the engine and not our surface, so
* take it wherever the two can part -- but show it only while the engine
* is ours: what it drew for another frontend is not ours to put up.
*/
static void
takedraw(void)
{
Drawcmd dc;
if(channbrecv(drawc, &dc) > 0 && engaged)
popup(&dc);
}
/* The compositor places the popup; where the text is does not concern us. */
static void
poprect(void*, struct zwp_input_popup_surface_v2*, int, int, int, int)
{
}
static const struct zwp_input_popup_surface_v2_listener poplisten = {poprect};
static void
releasegrab(void)
{
if(grab == nil)
return;
zwp_input_method_keyboard_grab_v2_release(grab);
grab = nil;
}
/*
* The context is going: give the client its keys back. The pending text
* goes nowhere either way -- the compositor drops a commit once the text
* input has its leave, and a client that disabled its own text input has
* stopped listening, so it throws a relayed commit away. XIM and IBus
* hand the text back here because their focus-out is a round trip the
* client still waits on; a deactivate is not.
*/
static void
leave(void)
{
Keyres res;
repdue = 0;
releasekeys();
composedrop(Composewl, &context);
if(engaged){
sendrequest(Keyrelease, 0, 0, &res);
engaged = 0;
takedraw();
}
hidepopup();
preshown = 0;
}
/* A purpose that turned secret with text pending gets the text first. */
static void
flushpending(void)
{
Keyres res;
if(!engaged)
return;
sendrequest(Keyreset, 0, 0, &res);
answer(&res, "");
takedraw();
}
/*
* A purpose belongs to the activation: the compositor sends a content type
* only for a client that asked for one, so a client that asks for none has
* none, whatever the last one wanted.
*/
static void
imactivate(void*, struct zwp_input_method_v2*)
{
pendingactive = 1;
activated = 1;
pendingpurpose = 0;
sclear(&pendingsurround);
}
static void
imdeactivate(void*, struct zwp_input_method_v2*)
{
pendingactive = 0;
}
/*
* The text around the cursor, of which a reading can use what comes
* before it. A text input that sends none leaves this empty, and then
* nothing is ever reached back into or taken away.
*/
static void
imsurrounding(void*, struct zwp_input_method_v2*, const char *text,
u32int cursor, u32int)
{
u32int n;
n = strlen(text);
stail(&pendingsurround, (char*)text, cursor < n ? cursor : n);
}
static void
imcause(void*, struct zwp_input_method_v2*, u32int)
{
}
static void
imcontent(void*, struct zwp_input_method_v2*, u32int, u32int what)
{
pendingpurpose = what;
}
/*
* activate, deactivate and content_type are pending until done, and the
* count of done events is the serial a commit must carry: wlroots throws
* away a whole commit whose serial does not match, without a word.
*/
static void
imdone(void*, struct zwp_input_method_v2*)
{
int washidden;
serial++;
washidden = hidden();
purpose = pendingpurpose;
surround = pendingsurround;
if(activated || (active && !pendingactive))
leave();
else if(active && !washidden && hidden())
flushpending();
if(activated && pendingactive)
grabkeyboard();
else if(!pendingactive)
releasegrab();
active = pendingactive;
activated = 0;
}
static void
imgone(void*, struct zwp_input_method_v2*)
{
gone = 1;
}
static const struct zwp_input_method_v2_listener imlisten = {
imactivate, imdeactivate, imsurrounding, imcause, imcontent, imdone,
imgone,
};
/*
* The same fd goes to the virtual keyboard, so a key we pass on means
* there what it meant here.
*/
static void
grabkeymap(void*, struct zwp_input_method_keyboard_grab_v2*, u32int format,
int fd, u32int size)
{
struct xkb_keymap *keymap;
struct xkb_state *state;
char *s;
if(format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1){
close(fd);
return;
}
s = mmap(nil, size, PROT_READ, MAP_PRIVATE, fd, 0);
if(s == MAP_FAILED){
close(fd);
return;
}
keymap = xkb_keymap_new_from_string(xkbctx, s,
XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS);
munmap(s, size);
state = keymap != nil ? xkb_state_new(keymap) : nil;
xkb_keymap_unref(keymap);
if(state == nil){
wllog("cannot read the keyboard mapping");
close(fd);
return;
}
xkb_state_unref(kstate);
kstate = state;
zwp_virtual_keyboard_v1_keymap(vk, format, fd, size);
close(fd);
haskeymap = 1;
}
/*
* A press, from the keyboard or from the repeat deadline. Returns whether
* the engine took it: what it did not take has gone on to the client as a
* key, and a key at the client is the client's own to repeat.
*/
static int
presskey(u32int code)
{
Keyres res;
char text[Maxutf];
u32int key, sym;
sym = xkb_state_key_get_one_sym(kstate, code + 8);
if(composekey(Composewl, &context, sym, text, sizeof text))
return 0; /* a sequence in the making */
key = ipckeysym(sym, xkb_keysym_to_utf32(sym));
if(keymeaningful(key))
engaged = 1;
if(text[0] != '\0')
/* Composed text follows whatever was pending. */
sendrequest(Keyreset, 0, 0, &res);
else
sendrequest(Keypress, key, modmask(), &res);
answer(&res, text);
takedraw();
if(text[0] == '\0' && !res.eaten)
forward(code, WL_KEYBOARD_KEY_STATE_PRESSED);
/* A finished sequence is one act; only a key is held down. */
return text[0] == '\0' && res.eaten;
}
static void
grabkey(void*, struct zwp_input_method_keyboard_grab_v2*, u32int,
u32int time, u32int code, u32int state)
{
lasttime = time;
if(state != WL_KEYBOARD_KEY_STATE_PRESSED){
if(code == repcode)
repdue = 0;
if(issent(code))
forward(code, state);
return;
}
repdue = 0; /* one key repeats at a time, and this is the newest */
if(!active || hidden() || kstate == nil){
forward(code, state);
return;
}
if(!presskey(code) || reprate <= 0 ||
!xkb_keymap_key_repeats(xkb_state_get_keymap(kstate), code + 8))
return;
repcode = code;
repdue = nowms() + repdelay;
}
/*
* Our own lookup must see Shift, and so must the client: wlroots derives
* no state from a virtual keyboard's keys, so a modifier forwarded as a
* keycode arrives bare and Ctrl+C reaches the client as c.
*/
static void
grabmodifiers(void*, struct zwp_input_method_keyboard_grab_v2*, u32int,
u32int depressed, u32int latched, u32int locked, u32int group)
{
if(kstate != nil)
xkb_state_update_mask(kstate, depressed, latched, locked,
0, 0, group);
if(haskeymap)
zwp_virtual_keyboard_v1_modifiers(vk, depressed, latched,
locked, group);
}
static void
grabrepeat(void*, struct zwp_input_method_keyboard_grab_v2*, int rate,
int delay)
{
reprate = rate;
repdelay = delay;
if(rate <= 0)
repdue = 0;
}
static const struct zwp_input_method_keyboard_grab_v2_listener grablisten = {
grabkeymap, grabkey, grabmodifiers, grabrepeat,
};
/*
* sway hands keys to the grab holder whenever the grab object exists,
* without asking whether the input method is active, so a grab held
* while inactive takes every key in the session. Two activates can also
* arrive without a deactivate between them, and a second grab_keyboard
* then fails and leaves a dead object: release before grabbing again.
*/
static void
grabkeyboard(void)
{
releasegrab();
grab = zwp_input_method_v2_grab_keyboard(im);
zwp_input_method_keyboard_grab_v2_add_listener(grab, &grablisten, nil);
}
static void
regglobal(void*, struct wl_registry *r, u32int name, const char *iface, u32int)
{
if(strcmp(iface, wl_seat_interface.name) == 0){
if(seat == nil) /* the first seat is ours */
seat = wl_registry_bind(r, name, &wl_seat_interface, 1);
}else if(strcmp(iface, wl_compositor_interface.name) == 0)
/* 4 for set_buffer_scale and damage_buffer */
comp = wl_registry_bind(r, name, &wl_compositor_interface, 4);
else if(strcmp(iface, wl_shm_interface.name) == 0)
shm = wl_registry_bind(r, name, &wl_shm_interface, 1);
else if(strcmp(iface, zwp_input_method_manager_v2_interface.name) == 0)
immgr = wl_registry_bind(r, name,
&zwp_input_method_manager_v2_interface, 1);
else if(strcmp(iface, zwp_virtual_keyboard_manager_v1_interface.name) == 0)
vkmgr = wl_registry_bind(r, name,
&zwp_virtual_keyboard_manager_v1_interface, 1);
}
static void
regremove(void*, struct wl_registry*, u32int)
{
}
static const struct wl_registry_listener reglisten = {regglobal, regremove};
/*
* Another frontend may have taken the engine; then our preedit and our
* popup are both stale. Nothing later will say so: the engine's own
* pictures go to whoever took it, and it settles back at blank.
*/
static void
checkowner(void)
{
Keyres res;
if(!preshown && !shown)
return;
sendrequest(Keycap, 0, 0, &res);
if(res.eaten)
return;
hidepopup();
if(preshown){
zwp_input_method_v2_set_preedit_string(im, "", 0, 0);
zwp_input_method_v2_commit(im, serial);
preshown = 0;
}
}
/*
* One repeat, once the key has been held long enough. Under the grab the
* compositor feeds the client nothing while the key is down, so what it
* would have repeated there is ours to make -- but only while the engine
* wants the key. Backspace stops being ours once the preedit is empty,
* and by then presskey has passed it on, so the key is held down at the
* client and the client repeats it from there.
*/
static void
repeat(void)
{
if(nowms() < repdue)
return;
if(!active || hidden() || kstate == nil || !presskey(repcode)){
repdue = 0;
return;
}
repdue = nowms() + 1000/reprate;
}
/*
* Whether this session has the protocol, not whether it is Wayland:
* GNOME and KWin set WAYLAND_DISPLAY and have no manager, and XIM is the
* only path left to them.
*/
int
wlinit(void)
{
struct wl_registry *reg;
display = wl_display_connect(nil);
if(display == nil)
return 0;
reg = wl_display_get_registry(display);
wl_registry_add_listener(reg, &reglisten, nil);
wl_display_roundtrip(display);
/* Without the virtual keyboard every key we do not eat is lost. */
if(seat != nil && immgr != nil && vkmgr != nil && comp != nil &&
shm != nil)
return 1;
wl_display_disconnect(display);
display = nil;
return 0;
}
/*
* One process touches libwayland: it reads events, dispatches them and
* answers the engine, in order. dispatch reads only when poll says
* there is something, so it never blocks and the owner check still runs.
*/
void
wlthread(void*)
{
struct pollfd pfd;
int ms, n;
threadsetname("wl");
replyc = chancreate(sizeof(Keyres), 0);
xkbctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
if(xkbctx == nil){
wllog("cannot make an xkb context");
return;
}
textinit();
im = zwp_input_method_manager_v2_get_input_method(immgr, seat);
zwp_input_method_v2_add_listener(im, &imlisten, nil);
vk = zwp_virtual_keyboard_manager_v1_create_virtual_keyboard(vkmgr, seat);
surface = wl_compositor_create_surface(comp);
popsurf = zwp_input_method_v2_get_input_popup_surface(im, surface);
zwp_input_popup_surface_v2_add_listener(popsurf, &poplisten, nil);
pfd.fd = wl_display_get_fd(display);
pfd.events = POLLIN;
for(;;){
if(wl_display_flush(display) < 0 && errno != EAGAIN)
break;
pfd.revents = 0;
if(repdue != 0){
ms = repdue - nowms();
if(ms < 0)
ms = 0;
}else
ms = preshown || shown ? Ownerpoll : -1;
n = poll(&pfd, 1, ms);
if(n < 0 && errno != EINTR)
break;
if(pfd.revents & POLLIN){
if(wl_display_dispatch(display) < 0)
break;
}else if(wl_display_dispatch_pending(display) < 0)
break;
if(gone){
wllog("another input method already has the seat");
releasegrab();
leave();
wl_display_flush(display);
bufclear(&bufs[0]);
bufclear(&bufs[1]);
textclose();
return;
}
/* A repeating key's own press keeps the engine ours, so
* while one repeats the owner poll has nothing to find. */
if(repdue != 0)
repeat();
else
checkowner();
}
if(wl_display_get_error(display) != 0)
die("wl: compositor disconnected");
die("wl: frontend stopped");
}

578
xim.c Normal file
View File

@@ -0,0 +1,578 @@
#include "dat.h"
#include "fn.h"
#include <errno.h>
#include <locale.h>
#include <poll.h>
#include <xcb/xcb.h>
#include <xcb/xcb_aux.h>
#include <xcb/xkb.h>
#include <xkbcommon/xkbcommon.h>
#include <xkbcommon/xkbcommon-names.h>
#include <xkbcommon/xkbcommon-x11.h>
#include <xcb-imdkit/imdkit.h>
#include <xcb-imdkit/encoding.h>
/*
* One XIM input context. Only PreeditCallbacks clients draw the preedit
* themselves (clientpre); the popup shows it for the others. engaged means the
* engine has seen a real key from this context and owes it a release.
*/
typedef struct Ic Ic;
struct Ic
{
Ic *next;
xcb_im_input_context_t *xic;
xcb_im_client_t *client;
int engaged;
int clientpre;
int prestarted;
int nprerune;
Caret caret;
};
static xcb_connection_t *conn;
static xcb_im_t *xim;
static struct xkb_state *kstate;
static uint8_t xkbevent;
static xcb_window_t rootwin;
static Ic *ics;
static Ic *preowner;
static Channel *replyc;
static char *encs[] = {"COMPOUND_TEXT"};
static u32int styles[] = {
XCB_IM_PreeditPosition | XCB_IM_StatusNothing,
XCB_IM_PreeditCallbacks | XCB_IM_StatusNothing,
XCB_IM_PreeditNothing | XCB_IM_StatusNothing,
XCB_IM_PreeditPosition | XCB_IM_StatusNone,
XCB_IM_PreeditCallbacks | XCB_IM_StatusNone,
XCB_IM_PreeditNothing | XCB_IM_StatusNone,
};
static void
ximlog(char *msg)
{
fprint(2, "strans: xim: %s\n", msg);
}
/* Reads the server's keymap; XKB-aware clients get XKB events, not MappingNotify. */
static int
kinit(void)
{
struct xkb_context *context;
struct xkb_keymap *keymap;
struct xkb_state *state;
int32_t device;
int ok;
keymap = nil;
state = nil;
ok = -1;
context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
if(context == nil || !xkb_x11_setup_xkb_extension(conn,
XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION,
XKB_X11_SETUP_XKB_EXTENSION_NO_FLAGS, nil, nil, &xkbevent, nil))
goto out;
device = xkb_x11_get_core_keyboard_device_id(conn);
if(device < 0)
goto out;
keymap = xkb_x11_keymap_new_from_device(context, conn, device,
XKB_KEYMAP_COMPILE_NO_FLAGS);
if(keymap == nil)
goto out;
state = xkb_state_new(keymap);
if(state == nil)
goto out;
xkb_state_unref(kstate);
kstate = state;
state = nil;
ok = 0;
out:
xkb_state_unref(state);
xkb_keymap_unref(keymap);
xkb_context_unref(context);
return ok;
}
static void
kwatch(void)
{
u16int events, parts;
events = XCB_XKB_EVENT_TYPE_NEW_KEYBOARD_NOTIFY |
XCB_XKB_EVENT_TYPE_MAP_NOTIFY;
parts = XCB_XKB_MAP_PART_KEY_TYPES | XCB_XKB_MAP_PART_KEY_SYMS |
XCB_XKB_MAP_PART_MODIFIER_MAP |
XCB_XKB_MAP_PART_EXPLICIT_COMPONENTS |
XCB_XKB_MAP_PART_KEY_ACTIONS | XCB_XKB_MAP_PART_VIRTUAL_MODS |
XCB_XKB_MAP_PART_VIRTUAL_MOD_MAP;
xcb_xkb_select_events(conn, XCB_XKB_ID_USE_CORE_KBD, events, 0,
events, parts, parts, nil);
}
static int
translate(xcb_window_t win, int x, int y, Caret *caret)
{
xcb_translate_coordinates_cookie_t cookie;
xcb_translate_coordinates_reply_t *reply;
if(win == XCB_NONE)
return 0;
cookie = xcb_translate_coordinates(conn, win, rootwin, x, y);
reply = xcb_translate_coordinates_reply(conn, cookie, nil);
if(reply == nil || !reply->same_screen){
free(reply);
return 0;
}
caret->valid = 1;
caret->x = reply->dst_x;
caret->y = reply->dst_y;
caret->h = 0;
free(reply);
return 1;
}
static int
placebottom(Ic *state, xcb_window_t win)
{
xcb_get_geometry_cookie_t gcookie;
xcb_get_geometry_reply_t *geometry;
int height;
if(win == XCB_NONE)
return 0;
gcookie = xcb_get_geometry(conn, win);
geometry = xcb_get_geometry_reply(conn, gcookie, nil);
if(geometry == nil)
return 0;
height = geometry->height;
free(geometry);
if(!translate(win, 0, 0, &state->caret))
return 0;
state->caret.y += height;
return 1;
}
/*
* The popup goes at the client's spot location when it sends one, else
* under the focus window, else under the client window. An unset focus
* window means the client window, as the XIM spec says.
*/
static void
place(Ic *state)
{
const xcb_im_preedit_attr_t *attr;
xcb_window_t clientwin, focuswin;
memset(&state->caret, 0, sizeof state->caret);
clientwin = xcb_im_input_context_get_client_window(state->xic);
focuswin = xcb_im_input_context_get_focus_window(state->xic);
if(focuswin == XCB_NONE)
focuswin = clientwin;
if(xcb_im_input_context_get_preedit_attr_mask(state->xic) &
XCB_XIM_XNSpotLocation_MASK){
attr = xcb_im_input_context_get_preedit_attr(state->xic);
if(translate(focuswin, attr->spot_location.x,
attr->spot_location.y, &state->caret)){
/* A spot is a baseline: call the line one row above it,
* so that a popup flipped above it clears the text. */
state->caret.y -= Fontsz;
state->caret.h = Fontsz;
return;
}
}
if(placebottom(state, focuswin))
return;
if(clientwin != focuswin)
placebottom(state, clientwin);
}
/* XIM text goes out as COMPOUND_TEXT; imdkit wraps UTF-8 in ESC%G. */
static void
commit(Ic *state, char *s, int nbyte)
{
char *wire;
size_t nwire;
if(nbyte == 0)
return;
wire = xcb_utf8_to_compound_text(s, nbyte, &nwire);
if(wire == nil)
return;
xcb_im_commit_string(xim, state->xic, XCB_XIM_LOOKUP_CHARS,
wire, (u32int)nwire, 0);
free(wire);
}
static void
clearpreedit(Ic *state)
{
xcb_im_preedit_draw_fr_t frame;
if(!state->prestarted)
return;
memset(&frame, 0, sizeof frame);
frame.chg_length = state->nprerune;
frame.status = 1;
xcb_im_preedit_draw_callback(xim, state->xic, &frame);
xcb_im_preedit_done_callback(xim, state->xic);
state->prestarted = 0;
state->nprerune = 0;
if(preowner == state)
preowner = nil;
xcb_flush(conn);
}
static void
updatepreedit(Ic *state, Str *pre)
{
xcb_im_preedit_draw_fr_t frame;
u32int feedback[Maxrunes];
char utf[Maxutf], *wire;
size_t nwire;
int i, nbyte;
if(!state->clientpre)
return;
if(pre->n == 0){
clearpreedit(state);
return;
}
nbyte = stoutf(pre, utf, sizeof utf);
wire = xcb_utf8_to_compound_text(utf, nbyte, &nwire);
if(wire == nil)
return;
for(i = 0; i < pre->n; i++)
feedback[i] = XCB_XIM_UNDERLINE;
if(!state->prestarted){
xcb_im_preedit_start_callback(xim, state->xic);
state->prestarted = 1;
}
memset(&frame, 0, sizeof frame);
frame.caret = pre->n;
frame.chg_length = state->nprerune;
frame.length_of_preedit_string = (u16int)nwire;
frame.preedit_string = (uchar*)wire;
frame.feedback_array.size = pre->n;
frame.feedback_array.items = feedback;
xcb_im_preedit_draw_callback(xim, state->xic, &frame);
state->nprerune = pre->n;
preowner = state;
free(wire);
}
static void
sendrequest(Ic *state, int op, u32int key, u32int mod, Keyres *res)
{
Keyreq kr;
memset(&kr, 0, sizeof kr);
kr.owner = state;
kr.clientpre = state->clientpre;
kr.op = op;
kr.ks = key;
kr.mod = ipcmod(mod);
kr.caret = state->caret;
kr.reply = replyc;
chansend(keyc, &kr);
chanrecv(replyc, res);
}
/* Another frontend may have taken the engine; then our preedit is stale. */
static void
checkpreowner(void)
{
Keyres res;
if(preowner == nil)
return;
sendrequest(preowner, Keycap, 0, 0, &res);
if(!res.eaten)
clearpreedit(preowner);
}
static void
keypress(Ic *state, u32int key, u32int mod, Keyres *res)
{
if(keymeaningful(key)){
if(preowner != nil && preowner != state)
clearpreedit(preowner);
state->engaged = 1;
}
sendrequest(state, Keypress, key, mod, res);
}
/* Losing the engine commits the pending text; the release hands it back. */
static void
release(Ic *state)
{
Keyres res;
char buf[Maxutf];
int n;
if(!state->engaged){
clearpreedit(state);
return;
}
sendrequest(state, Keyrelease, 0, 0, &res);
state->engaged = 0;
n = stoutf(&res.commit, buf, sizeof buf);
commit(state, buf, n);
clearpreedit(state);
xcb_flush(conn);
}
/* XIM reset hands the pending text back to the client as committed text. */
static void
resetic(Ic *state, xcb_im_reset_ic_reply_fr_t *reply)
{
Keyres res;
char utf[Maxutf], *wire;
size_t nwire;
int nbyte;
if(!state->engaged){
clearpreedit(state);
return;
}
sendrequest(state, Keyreset, 0, 0, &res);
clearpreedit(state);
if(reply == nil || res.commit.n == 0)
return;
nbyte = stoutf(&res.commit, utf, sizeof utf);
wire = xcb_utf8_to_compound_text(utf, nbyte, &nwire);
if(wire == nil)
return;
reply->committed_string = (uchar*)wire;
reply->byte_length_of_committed_string = nwire;
}
/* The keysym for an X core key event, with its modifier and group state. */
static u32int
keymaplookup(struct xkb_state *state, uchar keycode, u16int corestate)
{
static char *modname[] = {
XKB_MOD_NAME_SHIFT, XKB_MOD_NAME_CAPS, XKB_MOD_NAME_CTRL,
XKB_MOD_NAME_MOD1, XKB_MOD_NAME_MOD2, XKB_MOD_NAME_MOD3,
XKB_MOD_NAME_MOD4, XKB_MOD_NAME_MOD5,
};
struct xkb_keymap *keymap;
xkb_mod_index_t index;
xkb_mod_mask_t mods;
int i;
keymap = xkb_state_get_keymap(state);
mods = 0;
for(i = 0; i < nelem(modname); i++){
if(!(corestate & (1 << i)))
continue;
index = xkb_keymap_mod_get_index(keymap, modname[i]);
if(index != XKB_MOD_INVALID)
mods |= (xkb_mod_mask_t)1 << index;
}
xkb_state_update_mask(state, mods, 0, 0, 0, 0, (corestate >> 13) & 3);
return xkb_state_key_get_one_sym(state, keycode);
}
static void
kpress(Ic *state, xcb_key_press_event_t *ev)
{
Keyres res;
char buf[Maxutf], text[Maxutf];
u32int key, sym;
int n;
sym = keymaplookup(kstate, ev->detail, ev->state);
if(composekey(Composexim, state, sym, text, sizeof text))
return;
key = ipckeysym(sym, xkb_keysym_to_utf32(sym));
if(keymeaningful(key))
place(state);
if(text[0] != '\0')
/* Composed text follows whatever was pending. */
sendrequest(state, Keyreset, 0, 0, &res);
else
keypress(state, key, ev->state, &res);
n = stoutf(&res.commit, buf, sizeof buf);
commit(state, buf, n);
updatepreedit(state, &res.preedit);
if(text[0] != '\0')
commit(state, text, strlen(text));
else if(!res.eaten)
xcb_im_forward_event(xim, state->xic, ev);
xcb_flush(conn);
}
static void
icunlink(Ic *state)
{
Ic **p;
for(p = &ics; *p != nil; p = &(*p)->next)
if(*p == state){
*p = state->next;
return;
}
}
static void
icfree(void *p)
{
Ic *state;
state = p;
/* The owner remains valid until imthread acknowledges its release. */
release(state);
composedrop(Composexim, state);
icunlink(state);
free(state);
}
static void
iccreate(xcb_im_client_t *client, xcb_im_input_context_t *ic)
{
Ic *state;
state = emalloc(sizeof(Ic));
state->xic = ic;
state->client = client;
if(xcb_im_input_context_get_input_style(ic) & XCB_IM_PreeditCallbacks)
state->clientpre = 1;
state->next = ics;
ics = state;
xcb_im_input_context_set_data(ic, state, icfree);
}
static void
callback(xcb_im_t *im, xcb_im_client_t *client, xcb_im_input_context_t *ic,
const xcb_im_packet_header_fr_t *hdr, void *frame, void *arg, void *user)
{
xcb_key_press_event_t *ev;
Keyres res;
Ic *state;
USED(im);
USED(frame);
USED(user);
if(hdr->major_opcode == XCB_XIM_DISCONNECT ||
hdr->major_opcode == XCB_XIM_CLOSE){
for(state = ics; state != nil; state = state->next)
if(state->client == client)
release(state);
return;
}
if(hdr->major_opcode == XCB_XIM_CREATE_IC){
iccreate(client, ic);
return;
}
if(ic == nil)
return;
state = xcb_im_input_context_get_data(ic);
switch(hdr->major_opcode){
case XCB_XIM_FORWARD_EVENT:
ev = arg;
if(ev != nil && (ev->response_type & ~0x80) == XCB_KEY_PRESS)
kpress(state, ev);
break;
case XCB_XIM_SET_IC_VALUES:
case XCB_XIM_SET_IC_FOCUS:
place(state);
if(state->engaged)
sendrequest(state, Keycaret, 0, 0, &res);
break;
case XCB_XIM_DESTROY_IC:
case XCB_XIM_UNSET_IC_FOCUS:
release(state);
break;
case XCB_XIM_RESET_IC:
resetic(state, arg);
break;
}
}
static int
ximinit(void)
{
xcb_screen_t *screen;
xcb_window_t win;
xcb_im_styles_t st;
xcb_im_encodings_t enc;
int scr;
replyc = chancreate(sizeof(Keyres), 0);
st.nStyles = nelem(styles);
st.styles = styles;
enc.nEncodings = nelem(encs);
enc.encodings = encs;
xcb_compound_text_init();
conn = xcb_connect(nil, &scr);
if(xcb_connection_has_error(conn)){
ximlog("cannot connect to X server");
return -1;
}
screen = xcb_aux_get_screen(conn, scr);
if(screen == nil){
ximlog("cannot find X screen");
return -1;
}
rootwin = screen->root;
if(kinit() < 0){
ximlog("cannot read keyboard mapping");
return -1;
}
kwatch();
win = xcb_generate_id(conn);
xcb_create_window(conn, XCB_COPY_FROM_PARENT, win, screen->root,
0, 0, 1, 1, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT,
screen->root_visual, 0, nil);
xim = xcb_im_create(conn, scr, win, "strans",
XCB_IM_ALL_LOCALES, &st, nil, nil, &enc,
XCB_EVENT_MASK_KEY_PRESS, callback, nil);
if(xim == nil){
ximlog("cannot create XIM server");
return -1;
}
if(!xcb_im_open_im(xim)){
ximlog("cannot claim XIM selection");
return -1;
}
return 0;
}
void
ximthread(void *arg)
{
struct pollfd pfd;
xcb_generic_event_t *ev;
uint8_t type;
int n;
USED(arg);
threadsetname("xim");
if(ximinit() < 0)
die("xim: initialization failed");
pfd.fd = xcb_get_file_descriptor(conn);
pfd.events = POLLIN;
for(;;){
pfd.revents = 0;
n = poll(&pfd, 1, preowner != nil ? Ownerpoll : -1);
if(n < 0 && errno != EINTR)
break;
while((ev = xcb_poll_for_event(conn)) != nil){
type = ev->response_type & ~0x80;
if(type == xkbevent){
if(kinit() < 0)
ximlog("cannot refresh keyboard mapping");
}else
xcb_im_filter_event(xim, ev);
free(ev);
}
checkpreowner();
if(pfd.revents & (POLLERR|POLLHUP|POLLNVAL))
break;
}
if(xcb_connection_has_error(conn))
die("xim: X server disconnected");
die("xim: frontend stopped");
}

View File

@@ -1,27 +0,0 @@
CC = cc
XIM_CFLAGS = $(shell pkg-config --cflags xcb-imdkit xkbcommon)
XIM_LIBS = $(shell pkg-config --libs xcb-imdkit xkbcommon)
CFLAGS = -Wall -Wextra -O2 -I.. $(XIM_CFLAGS)
PROG = strans-xim
SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o)
TEST = xim_test
all: $(PROG)
$(PROG): $(OBJS) ../ipc.c ../ipc.h
$(CC) $(CFLAGS) -o $@ $(OBJS) ../ipc.c $(XIM_LIBS)
$(OBJS): ../ipc.h
$(TEST): ../tests/xim_test.c keymap.c ximtext.c
$(CC) $(CFLAGS) -o $@ ../tests/xim_test.c keymap.c ximtext.c $(XIM_LIBS)
check: $(TEST)
./$(TEST)
clean:
rm -f $(OBJS) $(PROG) $(TEST)
.PHONY: all check clean

View File

@@ -1,37 +0,0 @@
#include <stdint.h>
#include <xkbcommon/xkbcommon.h>
enum
{
ShiftMask = 1<<0,
LockMask = 1<<1,
GroupShift = 13,
GroupMask = 3,
};
uint32_t
keymaplookup(const uint32_t *syms, int nsyms, uint16_t state)
{
uint32_t lo, hi, lower, upper;
int col, group, shift;
if(syms == NULL || nsyms <= 0)
return XKB_KEY_NoSymbol;
group = (state >> GroupShift) & GroupMask;
col = group * 2;
if(col >= nsyms || syms[col] == XKB_KEY_NoSymbol)
col = 0;
lo = syms[col];
if(lo == XKB_KEY_NoSymbol)
return XKB_KEY_NoSymbol;
hi = col + 1 < nsyms ? syms[col + 1] : XKB_KEY_NoSymbol;
lower = xkb_keysym_to_lower(lo);
upper = xkb_keysym_to_upper(lo);
if(hi == XKB_KEY_NoSymbol)
hi = lower != upper ? upper : lo;
shift = (state & ShiftMask) != 0;
/* Lock reverses Shift only for keys with an alphabetic case pair. */
if((state & LockMask) && lower != upper)
shift = !shift;
return shift ? hi : lo;
}

View File

@@ -1,298 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <xcb/xcb.h>
#include <xkbcommon/xkbcommon.h>
#include <xcb-imdkit/imdkit.h>
#include <xcb-imdkit/encoding.h>
#include "ipc.h"
uint32_t keymaplookup(const uint32_t*, int, uint16_t);
char *ximcompound(const char*, size_t, size_t*);
typedef struct Ic Ic;
struct Ic
{
int fd;
};
static xcb_connection_t *conn;
static xcb_im_t *xim;
static xcb_keysym_t *kmap;
static uint8_t minkc, maxkc;
static uint8_t symsper;
static char *encs[] = {"COMPOUND_TEXT"};
static uint32_t styles[] = {
XCB_IM_PreeditNothing | XCB_IM_StatusNothing,
XCB_IM_PreeditNone | XCB_IM_StatusNone,
};
static void
die(char *msg)
{
fprintf(stderr, "strans-xim: %s\n", msg);
exit(1);
}
static void
kinit(void)
{
xcb_get_keyboard_mapping_cookie_t c;
xcb_get_keyboard_mapping_reply_t *r;
const xcb_setup_t *setup;
xcb_keysym_t *syms;
int n;
setup = xcb_get_setup(conn);
if(setup == NULL)
die("xcb_get_setup failed");
minkc = setup->min_keycode;
maxkc = setup->max_keycode;
c = xcb_get_keyboard_mapping(conn, minkc, maxkc - minkc + 1);
r = xcb_get_keyboard_mapping_reply(conn, c, NULL);
if(r == NULL)
die("keyboard mapping failed");
symsper = r->keysyms_per_keycode;
n = xcb_get_keyboard_mapping_keysyms_length(r);
syms = malloc(n * sizeof(xcb_keysym_t));
if(syms == NULL)
die("malloc failed");
memcpy(syms, xcb_get_keyboard_mapping_keysyms(r),
n * sizeof(xcb_keysym_t));
free(kmap);
kmap = syms;
free(r);
}
static uint32_t
kget(uint8_t kc, uint16_t state)
{
if(kmap == NULL || kc < minkc || kc > maxkc)
return 0;
return keymaplookup(kmap + (kc - minkc) * symsper, symsper, state);
}
static xcb_screen_t*
getscreen(int scr)
{
xcb_screen_iterator_t iter;
const xcb_setup_t *setup;
setup = xcb_get_setup(conn);
if(setup == NULL)
die("xcb_get_setup failed");
iter = xcb_setup_roots_iterator(setup);
for(; iter.rem; scr--, xcb_screen_next(&iter))
if(scr == 0)
return iter.data;
die("no screen");
return NULL;
}
static void
commit(xcb_im_input_context_t *ic, char *s, int len)
{
char *ct;
size_t clen;
if(len == 0)
return;
ct = ximcompound(s, len, &clen);
if(ct == NULL)
return;
xcb_im_commit_string(xim, ic, XCB_XIM_LOOKUP_CHARS, ct, clen, 0);
xcb_flush(conn);
free(ct);
}
static void
srvclose(Ic *state)
{
if(state == NULL || state->fd < 0)
return;
close(state->fd);
state->fd = -1;
}
static int
srvconnect(Ic *state)
{
if(state->fd >= 0)
return 0;
state->fd = ipcconnect();
if(state->fd < 0)
return -1;
return 0;
}
static int
readresp(Ic *state, xcb_im_input_context_t *ic)
{
char buf[Ipcfieldmax+1];
Ipcresp resp;
if(ipcreadresp(state->fd, 0, buf, sizeof buf, NULL, 0, &resp) < 0)
return -1;
if(resp.ncommit > 0)
commit(ic, buf, resp.ncommit);
return resp.eaten;
}
static void
kpress(Ic *state, xcb_im_input_context_t *ic, xcb_key_press_event_t *ev)
{
unsigned char buf[Ipcreqsz];
uint32_t key, rune;
int eaten;
key = kget(ev->detail, ev->state);
rune = xkb_keysym_to_utf32(key);
if(rune >= ' ' && rune != 0x7f)
key = rune;
else if(key >= 0xff00 && key <= 0xffff)
key = Kspec + (key - 0xff00);
else
key = rune;
ipcpackreq(buf, 0, ev->state, key);
eaten = 0;
if(srvconnect(state) == 0){
if(ipcsend(state->fd, buf, sizeof buf) < 0 ||
(eaten = readresp(state, ic)) < 0){
srvclose(state);
eaten = 0;
}
}
if(eaten == 0)
xcb_im_forward_event(xim, ic, ev);
xcb_flush(conn);
}
static void
reset(Ic *state, xcb_im_input_context_t *ic, int release)
{
unsigned char buf[Ipcreqsz];
if(state == NULL || state->fd < 0)
return;
ipcpackreset(buf, 0);
if(ipcsend(state->fd, buf, sizeof buf) < 0 ||
readresp(state, ic) < 0)
release = 1;
if(release)
srvclose(state);
}
static void
icfree(void *p)
{
Ic *state;
state = p;
/* imdkit frees this data on context destruction and client loss. */
srvclose(state);
free(state);
}
static void
iccreate(xcb_im_input_context_t *ic)
{
Ic *state;
if(ic == NULL || xcb_im_input_context_get_data(ic) != NULL)
return;
state = calloc(1, sizeof *state);
if(state == NULL)
return;
state->fd = -1;
xcb_im_input_context_set_data(ic, state, icfree);
}
static void
callback(xcb_im_t *im, xcb_im_client_t *client, xcb_im_input_context_t *ic,
const xcb_im_packet_header_fr_t *hdr, void *frame, void *arg, void *user)
{
xcb_key_press_event_t *ev;
Ic *state;
(void)im;
(void)client;
(void)frame;
(void)user;
if(hdr->major_opcode == XCB_XIM_CREATE_IC){
iccreate(ic);
return;
}
if(ic == NULL)
return;
state = xcb_im_input_context_get_data(ic);
if(state == NULL)
return;
switch(hdr->major_opcode){
case XCB_XIM_FORWARD_EVENT:
ev = arg;
if(ev != NULL && (ev->response_type & ~0x80) == XCB_KEY_PRESS)
kpress(state, ic, ev);
break;
case XCB_XIM_RESET_IC:
/* ResetIC ends this context's ownership, like focus loss. */
reset(state, ic, 1);
break;
case XCB_XIM_UNSET_IC_FOCUS:
reset(state, ic, 1);
break;
}
}
static void
ximinit(void)
{
xcb_screen_t *screen;
xcb_window_t win;
xcb_im_styles_t st;
xcb_im_encodings_t enc;
int scr;
st.nStyles = 2;
st.styles = styles;
enc.nEncodings = 1;
enc.encodings = encs;
xcb_compound_text_init();
conn = xcb_connect(NULL, &scr);
if(conn == NULL || xcb_connection_has_error(conn))
die("xcb_connect failed");
screen = getscreen(scr);
kinit();
win = xcb_generate_id(conn);
xcb_create_window(conn, XCB_COPY_FROM_PARENT, win, screen->root,
0, 0, 1, 1, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT,
screen->root_visual, 0, NULL);
xim = xcb_im_create(conn, scr, win, "strans",
XCB_IM_ALL_LOCALES, &st, NULL, NULL, &enc,
XCB_EVENT_MASK_KEY_PRESS, callback, NULL);
if(xim == NULL || !xcb_im_open_im(xim))
die("xcb_im failed");
}
int
main(void)
{
xcb_generic_event_t *ev;
uint8_t type;
ximinit();
for(;;){
ev = xcb_wait_for_event(conn);
if(ev == NULL)
break;
type = ev->response_type & ~0x80;
if(type == XCB_MAPPING_NOTIFY)
kinit();
else
xcb_im_filter_event(xim, ev);
free(ev);
}
return 0;
}

View File

@@ -1,41 +0,0 @@
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <xcb-imdkit/encoding.h>
static char*
ximutf8compound(const char *s, size_t len, size_t *outlen)
{
static const char begin[] = "\033%G";
static const char end[] = "\033%@";
char *ct;
size_t n;
if(s == NULL || len > SIZE_MAX - (sizeof begin + sizeof end - 1))
return NULL;
n = len + sizeof begin + sizeof end - 2;
ct = malloc(n + 1);
if(ct == NULL)
return NULL;
memcpy(ct, begin, sizeof begin - 1);
memcpy(ct + sizeof begin - 1, s, len);
memcpy(ct + sizeof begin - 1 + len, end, sizeof end - 1);
ct[n] = '\0';
if(outlen != NULL)
*outlen = n;
return ct;
}
char*
ximcompound(const char *s, size_t len, size_t *outlen)
{
char *ct;
if(s == NULL)
return NULL;
ct = xcb_utf8_to_compound_text(s, len, outlen);
if(ct != NULL)
return ct;
/* X.Org Compound Text extension for characters in no legacy charset. */
return ximutf8compound(s, len, outlen);
}