Compare commits

..

44 Commits

Author SHA1 Message Date
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
45 changed files with 3985 additions and 376 deletions

5
.gitignore vendored
View File

@@ -1,5 +1,9 @@
/*.o
/strans
/imv2.c
/imv2.h
/vkv1.c
/vkv1.h
/tests/*.o
/tests/unit_test
/tests/stress_test
@@ -14,3 +18,4 @@
/gtk/im-strans.so
/bench/bench
/bench/perf.data*
/ref-*/

View File

@@ -20,6 +20,7 @@ RUN pacman -Syu --noconfirm --needed \
ttf-dejavu \
ttf-jigmo \
valgrind \
wayland \
which \
xorg-server-xvfb \
xcb-imdkit \
@@ -41,4 +42,9 @@ RUN dbus-uuidgen --ensure=/etc/machine-id
ENV PLAN9=/usr/lib/plan9
ENV PATH=$PATH:/usr/lib/plan9/bin
# 9c and 9l filter their own output through $egrep, which plan9port never
# assigns, so the pipeline dies and takes every warning and error with it.
# Both source $PLAN9/config, which the distribution does not ship.
RUN echo 'egrep="grep -E"' > $PLAN9/config
WORKDIR /src

View File

@@ -1,8 +1,9 @@
# License inventory
- `BSD-3-Clause-libhangul-hanja.txt` contains the terms for the
single-character Hanja data in `map/hanja.src` and generated
`map/hanja.dict`, derived from libhangul's `data/hanja/hanja.txt`.
- `BSD-3-Clause-libhangul-hanja.txt` contains the terms for the Hanja data
in `map/hanja.src` and the symbol data in `map/mssymbol.src`, and for the
generated `map/hanja.dict` they share, derived from libhangul's
`data/hanja/hanja.txt` and `data/hanja/mssymbol.txt`.
- `GPL-2.0-or-later.txt` contains GNU GPL version 2. The header of
`map/kanji.dict` grants the option to use GPL version 2 or any later
version.
@@ -10,4 +11,7 @@
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.

View File

@@ -1,6 +1,7 @@
CC = 9c
LD = 9l
PKG_CONFIG ?= pkg-config
SCANNER ?= wayland-scanner
CFLAGS ?= -O2 -g
WARN_CFLAGS = -Wall -Wextra
IBUS_CFLAGS = $(shell $(PKG_CONFIG) --cflags dbus-1 xkbcommon)
@@ -11,34 +12,50 @@ 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)
$(XIM_LIBS) $(POPUP_LIBS) $(WL_LIBS)
PROG = strans
DOCKER_IMAGE = strans-build
DOCKER_RUN = docker run --rm --user "$$(id -u):$$(id -g)" \
-v "$(CURDIR):/src" $(DOCKER_IMAGE)
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 xim.c
srv.c str.c strans.c trie.c vi.c win.c wl.c xim.c
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) gtk
$(PROG): $(OBJS)
$(LD) $(LDFLAGS) -o $@ $(OBJS) $(PROJECT_LDLIBS) $(LDLIBS)
$(PROG): $(OBJS) $(PROTOOBJ)
$(LD) $(LDFLAGS) -o $@ $(OBJS) $(PROTOOBJ) $(PROJECT_LDLIBS) $(LDLIBS)
$(OBJS): dat.h fn.h ipc.h
wl.o: imv2.h vkv1.h
compose.o ibus.o: PROJECT_CPPFLAGS += $(IBUS_CFLAGS)
font.o: PROJECT_CPPFLAGS += $(TEXT_CFLAGS)
win.o: PROJECT_CPPFLAGS += $(POPUP_CFLAGS)
wl.o $(PROTOOBJ): PROJECT_CPPFLAGS += $(WL_CFLAGS)
xim.o: PROJECT_CPPFLAGS += $(XIM_CFLAGS)
%.o: %.c
$(CC) $(CPPFLAGS) $(PROJECT_CPPFLAGS) $(WARN_CFLAGS) $(CFLAGS) -c -o $@ $<
imv2.h: proto/input-method-unstable-v2.xml
$(SCANNER) client-header $< $@
imv2.c: proto/input-method-unstable-v2.xml
$(SCANNER) private-code $< $@
vkv1.h: proto/virtual-keyboard-unstable-v1.xml
$(SCANNER) client-header $< $@
vkv1.c: proto/virtual-keyboard-unstable-v1.xml
$(SCANNER) private-code $< $@
clean:
rm -f $(OBJS) $(PROG)
rm -f $(OBJS) $(PROG) $(PROTO) $(PROTOOBJ)
$(MAKE) -C tests clean
$(MAKE) -C gtk/ clean
$(MAKE) -C bench/ clean

286
README.md
View File

@@ -1,213 +1,172 @@
# strans
strans is a small, single-user input method for Korean, Japanese, English,
emoji, and symbols. It provides one engine for IBus (which GTK 4 and
Qt use), XIM, and GTK 3. Vietnamese Telex is also available as a
compatibility mode.
emoji and symbols, with Vietnamese Telex as a compatibility mode. One
engine serves four frontends: `zwp_input_method_v2` on Wayland — sway and
any other compositor that offers it — IBus, which GTK 4 and Qt use, XIM,
and a GTK 3 module.
## Input modes
## Modes
| Key | Mode |
| --- | --- |
| `Ctrl+N` | Japanese Hiragana and Kanji conversion |
| `Ctrl+K` | Japanese Katakana |
| `Ctrl+S` | Korean Hangul (2-beolsik) |
| `Ctrl+N` | Japanese Hiragana, converting to Kanji |
| `Ctrl+K` | Japanese Katakana |
| `Ctrl+T` | English |
| `Ctrl+V` | Vietnamese Telex |
| `Ctrl+E` | Emoji and symbol search |
| `Ctrl+H` | One-shot Hanja search |
| `Ctrl+H` | One-shot Hanja and symbol search |
Switching mode shows the mode switched to — `A`, `한`, `あ`, `ア`, `ă` — in
the popup until the next key.
Only a plain `Ctrl` chord is a strans key: with `Shift`, `Alt` or `Super`
held it commits what is pending and goes to the application, so
`Ctrl+Shift+V` still pastes. `Backspace`, `Enter`, `Tab`, `Esc` and the
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.
A Korean keyboard's 한/영 key toggles Korean and English, and its 한자 key
is `Ctrl+H`; a Japanese keyboard's 半角/全角 toggles Japanese and English,
ひらがな/カタカナ switches between the two kana modes, 変換 turns Japanese
on and then converts like `Space`, and 無変換 turns it off. Only a plain
`Ctrl` chord is a strans key: with `Shift`, `Alt`, or `Super` held it
commits what is pending and goes to the application, so `Ctrl+Shift+V`
still pastes.
## Composing
Hiragana mode composes a complete reading, then shows its Kanji candidates
with none chosen: the exact readings of the dictionary, then the verbs and
adjectives SKK keys by stem and okurigana (かく offers 確 and 書く), and
under `Space` the reading in Katakana last, so a word the dictionary lacks
converts to Katakana with one `Space`. `Space` or `Tab` chooses the first
candidate and then steps on, wrapping (with `Shift`, back); `Up`/`Down` and
`PageUp`/`PageDown` move without wrapping. Once a candidate is chosen, an unmodified `1`-`9` commits that row of the
current page; before that the digits type. `Enter` commits the chosen candidate, or the
reading when none is chosen, as `0` always does; so does typing on, or any
key that ends the composition. `Backspace` deletes the last kana shown;
romaji that made no kana stays in the reading as typed until it is mended.
The dictionary keys a verb or adjective by its stem, so `kaku` offers the
readings of かく and then 書く and its kin; typing the okurigana with
`Shift`, as SKK does — `kaKu` — asks for that split first. Caps Lock is
not `Shift` and marks nothing.
`Backspace` and `Esc` on a chosen candidate go back to the reading, and
`Esc` on the reading cancels it. Katakana mode does not perform Kanji
conversion.
Japanese composes a whole reading and then offers its Kanji with none
chosen. `Space` and `Tab` step through the candidates and wrap (`Shift`
reverses), the reading in Katakana last, so a word the dictionary lacks
converts with one `Space`; `Up`/`Down` and `PageUp`/`PageDown` move without
wrapping. `Enter` commits the chosen candidate, or the reading, as `0` and
typing on always do. Once a candidate is chosen `1`-`9` take that row of
the page shown; before that the digits type. `Backspace` deletes the last
kana shown, and romaji that made no kana stays as typed until it is mended;
`Esc` cancels the reading, and on a chosen candidate both go back to it.
The dictionary keys verbs and adjectives by stem and okurigana, so `kaku`
offers かく's readings and then 書く and its kin — typing the okurigana
with `Shift`, as SKK does, `kaKu`, asks for that split first. Katakana
mode does not convert, so there `Space` and `Tab` commit the reading and
go on to the application.
Korean composes one syllable at a time; `Enter`, `Tab`, `Esc`, and any key
Korean composes one syllable at a time. `Enter`, `Tab`, `Esc` and any key
that is not a jamo commit the syllable and go on to the application, so
`Esc` still leaves insert mode. Two lone consonants join into the compound
final they make (rt → ㄳ) and a vowel typed before its consonant is
final they make (rt → ㄳ), and a vowel typed before its consonant is
reordered under it (kr → 가), as in libhangul.
Emoji and Hanja searches take over the keys until `Enter`, `Space`, or
`1`-`9` picks a result or `Esc` cancels; they return to the previous mode
after use, and switching language during a search commits the shown
query. `Tab`/`Shift-Tab` wrap through the results, and a search with
nothing typed yet shows ☺ or 漢 in the popup. The Emoji search matches the
typed keys, and what they spell in the current language, against a prefix
of every emoji's CLDR name and keywords in English, Korean, and Japanese,
and against ASCII symbol aliases such as `->` and `<=`. `Ctrl+H` takes the
syllable being composed as its query and composes on from it; `Esc` gives
the syllable back. The Hanja dictionary converts a word as well as a
syllable, so `Ctrl+H` and then자 gives 漢字, and 대한민국 gives 大韓民國;
a syllable already committed is the application's text, not the engine's,
and cannot be converted. Leaving the field or
clicking elsewhere commits what is pending.
A search takes the keys until `Enter`, `Space` or `1`-`9` picks a result or
`Esc` cancels, then returns to the previous mode. Emoji matches the typed
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. 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 ①②③.
Reaching back needs the application to hand over the text around its
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.
Dead keys and Compose sequences are composed by strans itself for the
Wayland, XIM and IBus frontends, from `XCOMPOSEFILE` or the locale; the
GTK 3 module leaves them to GtkIMContextSimple.
## Preedit and candidates
| Frontend | Preedit | Candidates |
| --- | --- | --- |
| GTK 3 and IBus | inline in the client | strans popup |
| XIM PreeditCallbacks | inline through XIM callbacks | strans popup |
| XIM PreeditPosition | strans popup | strans popup |
| XIM PreeditNothing | strans popup | strans popup |
| 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 |
Each XIM style is offered with StatusNothing and with StatusNone.
Dead keys and Compose sequences are composed by strans itself for the XIM
and IBus frontends, from the table `XCOMPOSEFILE` or the locale names; the
GTK 3 module leaves them to GtkIMContextSimple. Their text follows
whatever was pending.
XIM text travels as `COMPOUND_TEXT`, which carries any UTF-8. A client that
sends `XNSpotLocation` gets the popup under that spot whatever its preedit
style, and above the line when there is no room below; otherwise the popup
sits under the focus window, or under the client window when no focus
window is set.
The popup is an X11 window sized by `GDK_SCALE` on HiDPI displays, XIM is
X11-only, and GTK popup placement converts the GTK caret to X11 root
coordinates. There is no native Wayland popup surface or Wayland caret
positioning. GTK and IBus clients can still use inline preedit where their
display environment supports it.
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
Initialize the bundled test library, then build in the Docker environment:
```sh
git submodule update --init
make docker-image
make docker-build
```
Build output:
```text
strans daemon with IBus and XIM frontends
gtk/im-strans.so GTK 3 frontend module
```
Tests have three tiers:
That leaves `strans`, the daemon, and `gtk/im-strans.so`, the GTK 3 module.
Tests come in three tiers:
```sh
make check # generated-map validation and focused unit tests
make check-live # one IBus, GTK, XIM, and IPC daemon smoke
make check-stress # randomized, capacity, collision, failure, and restart tests
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
```
`UNITARGS` filters only the focused C unit suite, for example
`make check UNITARGS=hangul`. It does not select or skip live and stress
tests. `make docker-check` rebuilds and runs all three tiers in the
container, and `make docker-valgrind` runs the unit suite under Valgrind
there; run `make docker-image` first after changing `Dockerfile`.
`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.
Native Linux source builds require a C toolchain, Make, and pkg-config, plus
development files for:
- Plan 9 port (`9c`, `9l`, `libthread`, and `libbio`);
- D-Bus development files;
- XCB, XCB RandR, XCB XKB, xcb-util, xcb-imdkit, xkbcommon, and xkbcommon-x11;
- Pango, PangoCairo, Cairo, and Fontconfig;
- GTK 3, plus libibus and Xlib for the live clients;
- Xvfb for the X11 live tests;
- Python 3 and `cmp` for `make check` and the map generators;
- a working `C.UTF-8` locale and fonts covering Latin, CJK, and emoji.
The package names used by the canonical Arch Linux image are listed in
[`Dockerfile`](Dockerfile).
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
The popup uses the normal system fonts found by Pango and Fontconfig. Run
it directly from the build tree:
```sh
./run.sh
# equivalently
./strans map
./run.sh # restart the daemon in the background
./strans map # or run it in the foreground
```
`run.sh` stops every same-user process whose exact name is `strans`, starts one
build-tree daemon in the background, and returns to the shell. It requires
`pkill` from procps. Startup and frontend failures remain visible on the
caller's standard error. A service supervisor should instead run
`./strans map` directly in the foreground, with the session's
`XDG_RUNTIME_DIR` and `DISPLAY`: the GTK module finds the daemon at
`$XDG_RUNTIME_DIR/strans.sock` (else `/tmp/strans.UID`), and IBus clients
through the address file libibus expects under `~/.config/ibus/bus/`,
which strans writes as `ibus-daemon` would, named after `WAYLAND_DISPLAY`
when there is one and `DISPLAY` otherwise.
`./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.
The build does not copy or install the daemon or its data. `./strans DIR`
opens `hira.map`, `kata.map`, `telex.map`, `kanji.dict`, `emoji.dict`, and
`hanja.dict` from `DIR` at runtime; `run.sh` passes the tracked `map/`
directory. Only the GTK subdirectory has install and uninstall targets.
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.
Configure clients as needed:
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
# XIM
export XMODIFIERS=@im=strans
# GTK 3 module
doas make -C gtk install
export XMODIFIERS=@im=strans # XIM
doas make -C gtk install # GTK 3 module
export GTK_IM_MODULE=strans
# IBus client example
GLFW_IM_MODULE=ibus kitty
GLFW_IM_MODULE=ibus kitty # IBus client example
```
Building `gtk/im-strans.so` requires GTK development files; installing one
built elsewhere, in the container for instance, does not. The install
target uses `GTK_MODULE_DIR` when it is set, else GTK's pkg-config
variables, else the directory the installed `gtk-query-immodules-3.0`
reports. Packagers and cross builds should set `GTK_MODULE_DIR`, and a
prebuilt module must still match the target architecture and library ABI.
`DESTDIR` staging is supported and deliberately skips the host module-cache
update. A native install needs the GTK runtime query utility so it can refresh
that cache. Remove a native installation with:
```sh
doas make -C gtk uninstall
```
strans provides its own IBus endpoint; `ibus-daemon` and fcitx are not
required. IBus clients that process keys synchronously — GTK 4, or GTK 3
with `IBUS_ENABLE_SYNC_MODE=1` — get their commits and preedits through
`PostProcessKeyEvent`, in order with the key. Without `DISPLAY`, the daemon and IBus frontend still work, but XIM
is not started and the popup is disabled.
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 the maps and
dictionaries:
After changing an input source, regenerate and verify:
```sh
python3 map/mktelex.py >map/telex.map
@@ -216,17 +175,16 @@ map/mkhanja >map/hanja.dict
make verify-map
```
See [`map/README`](map/README) for the emoji, Hanja, and Japanese data and
where it comes from.
[`map/README`](map/README) says where the emoji, Hanja and Japanese data
comes from.
## Benchmark
`make bench` builds `bench/bench`. With the daemon stopped, `./bench.sh` starts
one build-tree instance, warms it up, and records the workload with Linux
`perf`. It requires `perf` and permission to profile the daemon, and writes
`make bench` builds `bench/bench`. With the daemon stopped, `./bench.sh`
starts one, warms it up and records the workload with Linux `perf` into
`bench/perf.data`.
## Licensing
Third-party license notices are in [`LICENSES`](LICENSES). The repository does
not currently declare a license for the strans source as a whole.
Third-party notices are in [`LICENSES`](LICENSES). The repository declares
no license for the strans source as a whole.

View File

@@ -5,7 +5,13 @@
#include <xkbcommon/xkbcommon.h>
#include <xkbcommon/xkbcommon-compose.h>
static struct xkb_compose_state *cstate;
/*
* 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
@@ -14,6 +20,7 @@ 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);
@@ -22,30 +29,51 @@ composeinit(void)
table = xkb_compose_table_new_from_locale(ctx,
locale != nil ? locale : "C", XKB_COMPOSE_COMPILE_NO_FLAGS);
if(table != nil)
cstate = xkb_compose_state_new(table, XKB_COMPOSE_STATE_NO_FLAGS);
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.
* after whatever the engine had pending. A sequence belongs to the
* context that started it: a key from another one starts over.
*/
int
composekey(u32int sym, char *buf, int n)
composekey(int who, void *owner, u32int sym, char *buf, int n)
{
struct xkb_compose_state *cs;
buf[0] = '\0';
if(cstate == nil ||
xkb_compose_state_feed(cstate, sym) != XKB_COMPOSE_FEED_ACCEPTED)
cs = cstate[who];
if(cs == nil)
return 0;
switch(xkb_compose_state_get_status(cstate)){
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(cstate, buf, n);
xkb_compose_state_get_utf8(cs, buf, n);
break;
default:
break;

23
dat.h
View File

@@ -7,6 +7,13 @@
#define min(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
{
@@ -29,6 +36,11 @@ enum
Maxkouho = 128,
Maxdisp = 9,
/* 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,
Colbg = 0xffffff,
Colsep = 0xd0d0d0,
@@ -36,6 +48,15 @@ enum
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)
@@ -186,6 +207,7 @@ enum
struct Keyres
{
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 preedit;
};
@@ -198,6 +220,7 @@ struct Keyreq
u32int ks;
u32int mod;
Caret caret;
Str surround; /* the client's own text just before the cursor */
Channel *reply;
};

6
fn.h
View File

@@ -1,6 +1,7 @@
void die(char*, ...);
int sinit(Str*, char*, int);
void stail(Str*, char*, int);
void sclear(Str*);
void sputr(Str*, Rune);
void spopr(Str*);
@@ -43,10 +44,13 @@ void srvinit(void);
void srvthread(void*);
void ibusthread(void*);
void ximthread(void*);
int wlinit(void);
void wlthread(void*);
int keymeaningful(u32int);
void composeinit(void);
int composekey(u32int, char*, int);
void composedrop(int, void*);
int composekey(int, void*, u32int, char*, int);
void* emalloc(ulong);
void* erealloc(void*, ulong);

View File

@@ -24,6 +24,9 @@ struct Im
int cursorvalid;
int caretsent;
unsigned char sent[Ipccaretsz];
char surround[Ipcfieldmax];
int nsurround;
int surroundsent;
};
typedef struct ImClass ImClass;
@@ -65,6 +68,7 @@ srvdrop(Im *im, int notify)
im->fd = -1;
im->ext = 0;
im->caretsent = 0;
im->surroundsent = 0;
if(notify)
setpreedit(im, "", 0);
else{
@@ -157,6 +161,58 @@ sendcaret(Im *im)
return 0;
}
/*
* 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)
{
char *text, *p;
gint cursor;
int start;
im->nsurround = 0;
if(!gtk_im_context_get_surrounding(ctx, &text, &cursor))
return;
if(cursor < 0 || cursor > (gint)strlen(text)){
g_free(text);
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);
}
/* Sent only when it changes, as the caret is. */
static int
sendsurround(Im *im, GtkIMContext *ctx)
{
unsigned char hdr[Ipcreqsz];
char was[Ipcfieldmax];
int nwas;
if(im->fd < 0 || !im->ext)
return 0;
nwas = im->nsurround;
memcpy(was, im->surround, nwas);
readsurround(im, ctx);
if(im->surroundsent && nwas == im->nsurround &&
memcmp(was, im->surround, nwas) == 0)
return 0;
ipcpacksurround(hdr, im->nsurround);
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)
{
@@ -243,6 +299,18 @@ sendreset(Im *im)
g_signal_emit_by_name(im, "commit", commit);
}
/* A password entry hidden the older way sets no purpose and signals nothing. */
static int
ishidden(Im *im)
{
GtkWidget *w;
if(im->win == NULL)
return 0;
gdk_window_get_user_data(im->win, (gpointer*)&w);
return GTK_IS_ENTRY(w) && !gtk_entry_get_visibility(GTK_ENTRY(w));
}
static gboolean
kpress(GtkIMContext *ctx, GdkEventKey *ev)
{
@@ -264,7 +332,7 @@ kpress(GtkIMContext *ctx, GdkEventKey *ev)
return simplefilter(ctx, ev, 0);
key = ipckeysym(ev->keyval, gdk_keyval_to_unicode(ev->keyval));
mod = ipcmod(ev->state);
if(im->private || key == 0){
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)
@@ -274,7 +342,7 @@ kpress(GtkIMContext *ctx, GdkEventKey *ev)
if(srvconnect(im) < 0)
return simplefilter(ctx, ev, 0);
/* A retained GDK window may have moved since the last cursor report. */
if(sendcaret(im) < 0){
if(sendcaret(im) < 0 || sendsurround(im, ctx) < 0){
srvclose(im);
return simplefilter(ctx, ev, 0);
}
@@ -286,6 +354,11 @@ kpress(GtkIMContext *ctx, GdkEventKey *ev)
}
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)

112
ibus.c
View File

@@ -16,8 +16,7 @@ enum
Relmask = 1<<30,
/* IBus wire constants; the daemon does not link libibus. */
Ibuscappreedit = 1<<0,
Ibuspurposepassword = 8,
Ibuspurposepin = 9,
Ibuscapsurround = 1<<5,
Ibusattrunderline = 1,
Ibusunderlinesingle = 1,
Ibuspreeditclear = 0,
@@ -53,6 +52,7 @@ struct Ictx
int npost;
Post post[Maxpost];
Caret caret;
Str surround; /* the client's own text just before the cursor */
};
static DBusWatch *watches[Maxwatches];
@@ -85,9 +85,10 @@ unlinkaddr(void)
}
static int
addrnote(void*, char*)
addrnote(void*, char *note)
{
unlinkaddr();
if(notefatal(note))
unlinkaddr();
return 0;
}
@@ -305,6 +306,11 @@ sendrequest(Ictx *ctx, int op, u32int ks, u32int mod, Keyres *res)
kr.ks = ks;
kr.mod = mod;
kr.caret = ctx->caret;
/* A client that reads the key's effects back afterwards takes a
* DeleteSurroundingText signal after them, which is too late to be
* a taking back: it is never asked for text it cannot lose. */
if(!ctx->postprocess)
kr.surround = ctx->surround;
kr.reply = replyc;
chansend(keyc, &kr);
chanrecv(replyc, res);
@@ -313,8 +319,8 @@ sendrequest(Ictx *ctx, int op, u32int ks, u32int mod, Keyres *res)
static int
hidden(Ictx *ctx)
{
return ctx->purpose == Ibuspurposepassword ||
ctx->purpose == Ibuspurposepin;
return ctx->purpose == Purposepassword ||
ctx->purpose == Purposepin;
}
static void
@@ -465,6 +471,34 @@ emitpreedit(Ictx *ctx, const char *text)
preowner = nil;
}
/* The client is to take back the runes the reading reached into. */
static void
emitdelete(Ictx *ctx, int n)
{
DBusMessage *sig;
dbus_int32_t off;
dbus_uint32_t nrune;
off = -n;
nrune = n;
sig = newsignal(ctx->path, "DeleteSurroundingText");
dbus_message_append_args(sig, DBUS_TYPE_INT32, &off,
DBUS_TYPE_UINT32, &nrune, DBUS_TYPE_INVALID);
dbus_connection_send(ctx->conn, sig, nil);
dbus_message_unref(sig);
}
/* Nothing arrives until the client is asked to send it. */
static void
emitrequiresurround(Ictx *ctx)
{
DBusMessage *sig;
sig = newsignal(ctx->path, "RequireSurroundingText");
dbus_connection_send(ctx->conn, sig, nil);
dbus_message_unref(sig);
}
/* Another frontend may have taken the engine; then our preedit is stale. */
static void
checkpreowner(void)
@@ -511,6 +545,7 @@ static void
dropcontext(Ictx *ctx)
{
releasecontext(ctx);
composedrop(Composeibus, ctx);
if(preowner == ctx)
preowner = nil;
memset(ctx, 0, sizeof *ctx);
@@ -538,7 +573,7 @@ processkey(Ictx *ctx, u32int sym, u32int state, char *text, int n, Keyres *res)
return 0;
ctx->focused = 1;
/* An IBus client drops a dead key we do not take: compose it here. */
if(composekey(sym, text, n))
if(composekey(Composeibus, ctx, sym, text, n))
return -1;
if(text[0] != '\0')
/* Composed text follows whatever was pending. */
@@ -665,6 +700,8 @@ handlekey(DBusConnection *c, DBusMessage *m, Ictx *ctx)
ctx->keying = 1;
if(restart)
emitpreedit(ctx, "");
if(res.del > 0)
emitdelete(ctx, res.del);
if(commit[0] != '\0')
emitcommit(ctx, commit);
if(text[0] != '\0')
@@ -732,6 +769,7 @@ handlefocusout(DBusConnection *c, DBusMessage *m, Ictx *ctx)
flushcontext(ctx, Keyrelease);
ctx->focused = 0;
memset(&ctx->caret, 0, sizeof ctx->caret);
sclear(&ctx->surround);
return reply(c, m, DBUS_TYPE_INVALID, nil);
}
@@ -746,6 +784,8 @@ handlecap(DBusConnection *c, DBusMessage *m, Ictx *ctx)
dbus_message_get_args(m, nil, DBUS_TYPE_UINT32, &cap, DBUS_TYPE_INVALID);
old = ctx->cap;
ctx->cap = cap;
if((cap & ~old) & Ibuscapsurround)
emitrequiresurround(ctx);
if(ctx->focused){
sendrequest(ctx, Keycap, 0, 0, &res);
if((old & Ibuscappreedit) != (ctx->cap & Ibuscappreedit)){
@@ -759,6 +799,57 @@ handlecap(DBusConnection *c, DBusMessage *m, Ictx *ctx)
return reply(c, m, DBUS_TYPE_INVALID, nil);
}
/* IBusText is (sa{sv}sv); the string in it is all we want. */
static int
getibustext(DBusMessageIter *value, const char **s)
{
DBusMessageIter st;
int i;
if(dbus_message_iter_get_arg_type(value) != DBUS_TYPE_STRUCT)
return 0;
dbus_message_iter_recurse(value, &st);
for(i = 0; i < 2; i++)
if(!dbus_message_iter_next(&st))
return 0;
if(dbus_message_iter_get_arg_type(&st) != DBUS_TYPE_STRING)
return 0;
dbus_message_iter_get_basic(&st, s);
return 1;
}
/* The bytes the first n runes of s take; IBus counts a cursor in runes. */
static int
runebytes(const char *s, int n)
{
Rune r;
char *p;
for(p = (char*)s; n > 0 && *p != '\0'; n--)
p += chartorune(&r, p);
return p - s;
}
/* The text around the cursor; a reading can use what comes before it. */
static DBusHandlerResult
handlesurround(DBusConnection *c, DBusMessage *m, Ictx *ctx)
{
DBusMessageIter it, v;
dbus_uint32_t cursor;
const char *s;
if(!dbus_message_iter_init(m, &it))
return handleerror(c, m, DBUS_ERROR_INVALID_ARGS,
"SetSurroundingText expects text, cursor, and anchor");
dbus_message_iter_recurse(&it, &v);
if(!getibustext(&v, &s) || !dbus_message_iter_next(&it))
return handleerror(c, m, DBUS_ERROR_INVALID_ARGS,
"SetSurroundingText expects an IBusText");
dbus_message_iter_get_basic(&it, &cursor);
stail(&ctx->surround, (char*)s, runebytes(s, cursor));
return reply(c, m, DBUS_TYPE_INVALID, nil);
}
static int
getproperty(DBusMessage *m, const char **iface, const char **name,
DBusMessageIter *value)
@@ -889,7 +980,10 @@ handlecursor(DBusConnection *c, DBusMessage *m, Ictx *ctx)
return reply(c, m, DBUS_TYPE_INVALID, nil);
}
/* SetEngine: there is one engine, so any name is fine. */
/*
* Accepted and thrown away: there is one engine, so any name is fine,
* and a relative cursor location is surface local, not a caret.
*/
static DBusHandlerResult
handleok(DBusConnection *c, DBusMessage *m, Ictx*)
{
@@ -908,7 +1002,9 @@ static struct {
{"Reset", "", handlereset},
{"Destroy", "", handledestroy},
{"SetCursorLocation", "iiii", handlecursor},
{"SetCursorLocationRelative", "iiii", handleok},
{"SetCapabilities", "u", handlecap},
{"SetSurroundingText", "vuu", handlesurround},
{"SetEngine", "s", handleok},
};

38
ipc.c
View File

@@ -252,6 +252,22 @@ ipcpackcaret(unsigned char req[Ipccaretsz], int valid, int32_t x,
}
}
void
ipcpacksurround(unsigned char req[Ipcreqsz], size_t n)
{
memset(req, 0, Ipcreqsz);
req[0] = Ipcext;
req[1] = Ipcversion;
req[2] = Ipcopsurround;
putlen(req + 4, n);
}
size_t
ipcsurroundlen(const unsigned char req[Ipcreqsz])
{
return getlen(req + 4);
}
void
ipcunpackreq(const unsigned char req[Ipcreqsz], int *want, uint32_t *mod,
uint32_t *key)
@@ -281,6 +297,11 @@ ipcreqtype(const unsigned char req[Ipcreqsz])
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;
}
@@ -305,19 +326,21 @@ ipcreqreset(const unsigned char req[Ipcreqsz])
}
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,
int want)
{
size_t n;
if(ncommit > Ipcfieldmax || npreedit > Ipcfieldmax)
if(ncommit > Ipcfieldmax || npreedit > Ipcfieldmax ||
del < 0 || del > 0xff)
return -1;
n = Ipcresphdrsz + ncommit + (want ? Ipclensz + npreedit : 0);
if(cap < n)
return -1;
dst[0] = eaten != 0;
putlen(dst + 1, ncommit);
dst[1] = del;
putlen(dst + 2, ncommit);
memcpy(dst + Ipcresphdrsz, commit, ncommit);
if(want){
putlen(dst + Ipcresphdrsz + ncommit, npreedit);
@@ -398,7 +421,7 @@ readfield(int fd, char *dst, int64_t until)
int
ipcreadresp(int fd, int want, char *commit, char *preedit, Ipcresp *resp)
{
unsigned char eaten;
unsigned char head[2];
int64_t until;
int n;
@@ -406,13 +429,14 @@ ipcreadresp(int fd, int want, char *commit, char *preedit, Ipcresp *resp)
preedit[0] = '\0';
memset(resp, 0, sizeof *resp);
until = deadline();
if(readwait(fd, &eaten, 1, until) < 0)
if(readwait(fd, head, sizeof head, until) < 0)
return -1;
if(eaten > 1){
if(head[0] > 1){
errno = EPROTO;
return -1;
}
resp->eaten = eaten;
resp->eaten = head[0];
resp->del = head[1];
n = readfield(fd, commit, until);
if(n < 0)
return -1;

21
ipc.h
View File

@@ -8,9 +8,11 @@
* The server identifies the connection as the engine owner; that identity is
* not sent on the wire.
* Only Mmask modifier bits are retained.
* Response: [eaten, commit-length-low, commit-length-high, commit...],
* followed, when requested, by
* Response: [eaten, take-back, commit-length-low, commit-length-high,
* commit...], followed, when requested, by
* [preedit-length-low, preedit-length-high, preedit...].
* 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].
*
@@ -19,6 +21,10 @@
* 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
@@ -26,9 +32,10 @@ enum
Ipcreqwant = 1<<0,
Ipcreqreset = 1<<1,
Ipcext = 1<<7,
Ipcversion = 1,
Ipcversion = 2,
Ipcopcap = 0,
Ipcopcaret = 1,
Ipcopsurround = 2,
Kspec = 0x110000,
Kback = Kspec|0x08,
@@ -57,7 +64,7 @@ enum
Ipcreqsz = 6,
Ipccaretsz = 16,
Ipclensz = 2,
Ipcresphdrsz = 1 + Ipclensz,
Ipcresphdrsz = 2 + Ipclensz,
Ipcfieldmax = 256,
Ipcmaxresp = Ipcresphdrsz + Ipcfieldmax + Ipclensz + Ipcfieldmax,
Ipcwaitms = 250,
@@ -69,12 +76,14 @@ enum
Ipckey,
Ipccap,
Ipccaret,
Ipcsurround,
};
typedef struct Ipcresp Ipcresp;
struct Ipcresp
{
int eaten;
int del; /* runes of the client's own text to take back first */
/* Bytes copied to each caller buffer, excluding its trailing NUL. */
size_t commitlen;
size_t preeditlen;
@@ -86,11 +95,13 @@ void ipcpackreq(unsigned char[Ipcreqsz], int, uint32_t, uint32_t);
void ipcpackreset(unsigned char[Ipcreqsz], int);
void ipcpackcap(unsigned char[Ipcreqsz], int);
void ipcpackcaret(unsigned char[Ipccaretsz], int, int32_t, int32_t, int32_t);
void ipcpacksurround(unsigned char[Ipcreqsz], size_t);
size_t ipcsurroundlen(const unsigned char[Ipcreqsz]);
void ipcunpackreq(const unsigned char[Ipcreqsz], int*, uint32_t*, uint32_t*);
int ipcunpackcaret(const unsigned char[Ipccaretsz], int*, int32_t*, int32_t*, int32_t*);
int ipcreqtype(const unsigned char[Ipcreqsz]);
int ipcreqreset(const unsigned char[Ipcreqsz]);
int ipcpackresp(unsigned char*, size_t, int, 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 ipcsend(int, const void*, size_t);
int ipcreadresp(int, int, char*, char*, Ipcresp*);

20
main.c
View File

@@ -47,21 +47,31 @@ erealloc(void *p, ulong n)
void
threadmain(int argc, char **argv)
{
char *display;
char *display, *scale;
if(argc != 2)
usage();
/* Whichever popup runs is sized from it, and setfont reads Fontsz. */
scale = getenv("GDK_SCALE");
if(scale != nil && atoi(scale) > 1)
popupscale = min(atoi(scale), 4);
drawc = chancreate(sizeof(Drawcmd), 4);
keyc = chancreate(sizeof(Keyreq), 0);
langinit(argv[1]);
composeinit();
srvinit();
proccreate(drawthread, nil, 16384);
proccreate(srvthread, nil, 16384);
proccreate(ibusthread, nil, 32768);
display = getenv("DISPLAY");
if(display != nil && display[0] != '\0')
proccreate(ximthread, nil, 32768);
/* One popup per session: the Wayland frontend draws its own, so the
* X11 one and the XIM it serves stand down. */
if(getenv("WAYLAND_DISPLAY") != nil && wlinit())
proccreate(wlthread, nil, 32768);
else{
proccreate(drawthread, nil, 16384);
display = getenv("DISPLAY");
if(display != nil && display[0] != '\0')
proccreate(ximthread, nil, 32768);
}
imthread(nil);
}

View File

@@ -1,42 +1,53 @@
# Dictionary data
## Korean Hanja data
## Korean Hanja and symbol data
`hanja.src` is the tracked, reviewable source for Korean Hanja conversion.
Every data row is Hanja, a tab, and its modern Hangul reading, a character
or a word:
`hanja.src` and `mssymbol.src` are the tracked, reviewable sources for what
the Hanja search converts. Every data row is a result, a tab, and its
Hangul reading. A Hanja reads as a syllable or a word; a symbol reads as
the lone consonant a Korean keyboard's 한자 key offers it under:
```
漢 한
漢字 한자
※ ㅁ
```
`mkhanja` validates that contract and groups rows by their reading to
produce the runtime dictionary. Candidate order follows source order. The
source keeps all retained pairs for review; the generated dictionary stores
the first 128 candidates per reading because that is the engine's lookup
limit.
`mkhanja` validates that contract and groups the rows of both sources by
their reading to produce the runtime dictionary. The two keyspaces do not
meet: a reading is either syllables or one jamo. Candidate order follows
source order. The sources keep all retained pairs for review; the
generated dictionary stores the first 128 candidates per reading because
that is the engine's lookup limit.
The 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
peeled commit is `41c702f5d3581325b646ef6249f1f641b0427ae0`, and
`data/hanja/hanja.txt` has blob ID
`199cfd70c4b306257ac2e018714a1285f7cf0ed3`. Import and regenerate it from
the repository root with:
`data/hanja/hanja.txt` and `data/hanja/mssymbol.txt` have blob IDs
`199cfd70c4b306257ac2e018714a1285f7cf0ed3` and
`31c4e63d74293b6a759d4a2005cd1e7333746631`. Import and regenerate them
from the repository root with:
```sh
curl -L https://raw.githubusercontent.com/libhangul/libhangul/41c702f5d3581325b646ef6249f1f641b0427ae0/data/hanja/hanja.txt -o upstream
printf '%s %s\n' dd44dcc856cf542b1022d0f39c2e9b9f8805fdcc5923be80f04849ed97ce0996 upstream | sha256sum -c -
map/libhangul2hanja upstream >map/hanja.src
curl -L https://raw.githubusercontent.com/libhangul/libhangul/41c702f5d3581325b646ef6249f1f641b0427ae0/data/hanja/mssymbol.txt -o upstream
printf '%s %s\n' b685a4ebe2716b25eb29c42f6da6493716ecd78b604b44a33420987d21948e99 upstream | sha256sum -c -
map/libhangul2hanja upstream >map/mssymbol.src
map/mkhanja >map/hanja.dict
```
`libhangul2hanja` retains only rows whose reading is modern Hangul syllables
throughout and whose value is Hanja the popup can draw: U+3400U+4DBF,
U+4E00U+9FFF, or U+F900U+FAFF. Thus jamo readings, mixed values, and
supplementary-plane ideographs are omitted. The import preserves the upstream license header and row order.
The retained data is BSD 3-Clause licensed by Choe Hwanjin; the complete text
is in `LICENSES/BSD-3-Clause-libhangul-hanja.txt`.
`libhangul2hanja` keeps a syllable reading only when its value is Hanja the
popup can draw: U+3400U+4DBF, U+4E00U+9FFF, or U+F900U+FAFF. Thus mixed
values and supplementary-plane ideographs are omitted. It keeps a jamo
reading, U+3131U+314E, only when its value is one rune the popup can draw
and a candidate row can carry — so `mssymbol.txt`'s ideographic space and
soft hyphen are omitted, leaving 985 of its 987 rows, and a Hanja under a
jamo reading is omitted as noise. The import preserves each upstream
license header and row order. The retained data is BSD 3-Clause licensed
by Choe Hwanjin; the complete text is in
`LICENSES/BSD-3-Clause-libhangul-hanja.txt`.
## Emoji and symbol data

View File

@@ -8,6 +8,7 @@
:( ☹
:) ☺
<- ←
< ← ≤ ♥ ≠
<= ≤
<3 ♥ 🫰 🫶
<> ≠
@@ -16,6 +17,7 @@
=> ⇒
>= ≥
^1 ¹
^ ¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹
^2 ²
^3 ³
^4 ⁴
@@ -25,6 +27,7 @@
^8 ⁸
^9 ⁹
_1 ₁
_ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉
_2 ₂
_3 ₃
_4 ₄

View File

@@ -24,6 +24,32 @@
;; CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
;; ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
;; POSSIBILITY OF SUCH DAMAGE.
;; Copyright (c) 2005-2014 Choe Hwanjin
;; All rights reserved.
;;
;; Redistribution and use in source and binary forms, with or without
;; modification, are permitted provided that the following conditions are met:
;;
;; 1. Redistributions of source code must retain the above copyright notice,
;; this list of conditions and the following disclaimer.
;; 2. Redistributions in binary form must reproduce the above copyright notice,
;; this list of conditions and the following disclaimer in the documentation
;; and/or other materials provided with the distribution.
;; 3. Neither the name of the author nor the names of its contributors
;; may be used to endorse or promote products derived from this software
;; without specific prior written permission.
;;
;; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
;; AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
;; IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
;; ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
;; LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
;; CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
;; SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
;; INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
;; CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
;; ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
;; POSSIBILITY OF SUCH DAMAGE.
가 可 家 加 歌 價 街 假 佳 暇 架 价 賈 伽 柯 迦 軻 嘉 駕 嫁 稼 哥 呵 苛 袈 訶 枷 珂 茄 痂 跏 斝 葭 舸 笳 哿 坷 檟 謌 珈 砢 佉 呿 榎 仮 傢 咖 瘕 耞 宊 椵 叚 価 猳 髂 瘸 鴐 䴥 拁 㤎 榢 毠 溊 牁 豭 槚 诃 贾 轲 镓 驾 鲄 婽 尜 岢 幏 彁 徦 愘 戓 戨 斚 樖 泇 渮 滒 炣 牫 牱 犌 玍 癿 笴 糘 胢 腵 蚵 貑 跒 酠 鉫 鉲 鎵 鎶 魺 鴚 麚 㗎 㚙 㞹 㢦 㤉 㧝 㪃 㪼 㱒 㹢 䂟 䈔 䑝 䔅 䕒 䪪 䯊 䶗 嘏
가가 假家 可呵 可嘉 家家 呵呵
@@ -187311,3 +187337,21 @@
힐책 詰責
힐척 詰斥
힐항 詰抗
_  ̄ 、 。 · ‥ … ¨ 〃 ― ∥ ´ ˇ ˘ ˝ ˚ ˙ ¸ ˛ ¡ ¿ ː
ㄲ Æ Ð Ħ IJ Ŀ Ł Ø Œ Þ Ŧ Ŋ æ đ ð ħ ı ij ĸ ŀ ł ø œ ß þ ŧ ŋ ʼn
“ ” 〈 〉 《 》 「 」 『 』 【 】
± × ÷ ≠ ≤ ≥ ∞ ∴ ♂ ♀ ∠ ⊥ ⌒ ∂ ∇ ≡ ≒ ≪ ≫ √ ∽ ∝ ∵ ∫ ∬ ∈ ∋ ⊆ ⊇ ⊂ ⊃ ∩ ∧ ¬ ⇒ ⇔ ∀ ∃ ∮ ∑ ∏
ㄸ ぁ あ ぃ い ぅ う ぇ え ぉ お か が き ぎ く ぐ け げ こ ご さ ざ し じ す ず せ ぜ そ ぞ た だ ち ぢ っ つ づ て で と ど な に ぬ ね の は ば ぱ ひ び ぴ ふ ぶ ぷ へ べ ぺ ほ ぼ ぽ ま み む め も ゃ や ゅ ゆ ょ よ ら り る れ ろ ゎ わ ゐ ゑ を ん
″ ℃ Å ¢ £ ¥ ¤ ℉ ‰ € ㎕ ㎖ ㎗ ㎘ ㏄ ㎣ ㎤ ㎥ ㎦ ㎙ ㎚ ㎛ ㎜ ㎝ ㎞ ㎟ ㎠ ㎡ ㎢ ㏊ ㎍ ㎎ ㎏ ㏏ ㎈ ㎉ ㏈ ㎧ ㎨ ㎰ ㎱ ㎲ ㎳ ㎴ ㎵ ㎶ ㎷ ㎸ ㎹ ㎀ ㎁ ㎂ ㎃ ㎄ ㎺ ㎻ ㎼ ㎽ ㎾ ㎿ ㎐ ㎑ ㎒ ㎓ ㎔ Ω ㏀ ㏁ ㎊ ㎋ ㎌ ㏖ ㏅ ㎭ ㎮ ㎯ ㏛ ㎩ ㎪ ㎫ ㎬ ㏝ ㏐ ㏓ ㏃ ㏉ ㏜ ㏆
§ ※ ☆ ★ ○ ● ◎ ◇ ◆ □ ■ △ ▲ ▽ ▼ → ← ↑ ↓ ↔ 〓 ◁ ◀ ▷ ▶ ♤ ♠ ♡ ♥ ♧ ♣ ⊙ ◈ ▣ ◐ ◑ ▒ ▤ ▥ ▨ ▧ ▦ ▩ ♨ ☏ ☎ ☜ ☞ ¶ † ‡ ↕ ↗ ↙ ↖ ↘ ♭ ♩ ♪ ♬ ㉿ ㈜ № ㏇ ™ ㏂ ㏘ ℡ ® ª º
ㅂ ─ │ ┌ ┐ ┘ └ ├ ┬ ┤ ┴ ┼ ━ ┃ ┏ ┓ ┛ ┗ ┣ ┳ ┫ ┻ ╋ ┠ ┯ ┨ ┷ ┿ ┝ ┰ ┥ ┸ ╂ ┒ ┑ ┚ ┙ ┖ ┕ ┎ ┍ ┞ ┟ ┡ ┢ ┦ ┧ ┩ ┪ ┭ ┮ ┱ ┲ ┵ ┶ ┹ ┺ ┽ ┾ ╀ ╁ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊
ㅃ ァ ア ィ イ ゥ ウ ェ エ ォ オ カ ガ キ ギ ク グ ケ ゲ コ ゴ サ ザ シ ジ ス ズ セ ゼ ソ ゾ タ ダ チ ヂ ッ ツ ヅ テ デ ト ド ナ ニ ヌ ネ ハ バ パ ヒ ビ ピ フ ブ プ ヘ ベ ペ ホ ボ ポ マ ミ ム メ モ ャ ヤ ュ ユ ョ ヨ ラ リ ル レ ロ ヮ ワ ヰ ヱ ヲ ン ヴ ヵ ヶ
ㅅ ㉠ ㉡ ㉢ ㉣ ㉤ ㉥ ㉦ ㉧ ㉨ ㉩ ㉪ ㉫ ㉬ ㉭ ㉮ ㉯ ㉰ ㉱ ㉲ ㉳ ㉴ ㉵ ㉶ ㉷ ㉸ ㉹ ㉺ ㉻ ㈀ ㈁ ㈂ ㈃ ㈄ ㈅ ㈆ ㈇ ㈈ ㈉ ㈊ ㈋ ㈌ ㈍ ㈎ ㈏ ㈐ ㈑ ㈒ ㈓ ㈔ ㈕ ㈖ ㈗ ㈘ ㈙ ㈚ ㈛
А Б В Г Д Е Ё Ж З И Й К Л М Н О П Р С Т У Ф Х Ц Ч Ш Щ Ъ Ы Ь Э Ю Я а б в г д е ё ж з и й к л м н о п р с т у ф х ц ч ш щ ъ ы ь э ю я
ㅇ ⓐ ⓑ ⓒ ⓓ ⓔ ⓕ ⓖ ⓗ ⓘ ⓙ ⓚ ⓛ ⓜ ⓝ ⓞ ⓟ ⓠ ⓡ ⓢ ⓣ ⓤ ⓥ ⓦ ⓧ ⓨ ⓩ ① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ ⑪ ⑫ ⑬ ⑭ ⑮ ⒜ ⒝ ⒞ ⒟ ⒠ ⒡ ⒢ ⒣ ⒤ ⒥ ⒦ ⒧ ⒨ ⒩ ⒪ ⒫ ⒬ ⒭ ⒮ ⒯ ⒰ ⒱ ⒲ ⒳ ⒴ ⒵ ⑴ ⑵ ⑶ ⑷ ⑸ ⑹ ⑺ ⑻ ⑼ ⑽ ⑾ ⑿ ⒀ ⒁ ⒂
ⅱ ⅲ ⅳ ⅵ ⅶ ⅷ ⅸ Ⅱ Ⅲ Ⅳ Ⅵ Ⅶ Ⅷ Ⅸ
ㅊ ½ ⅓ ⅔ ¼ ¾ ⅛ ⅜ ⅝ ⅞ ¹ ² ³ ⁴ ⁿ ₁ ₂ ₃ ₄
ㅋ ㄱ ㄲ ㄳ ㄴ ㄵ ㄶ ㄷ ㄸ ㄹ ㄺ ㄻ ㄼ ㄽ ㄾ ㄿ ㅀ ㅁ ㅂ ㅃ ㅄ ㅅ ㅆ ㅇ ㅈ ㅉ ㅊ ㅋ ㅌ ㅍ ㅎ ㅏ ㅐ ㅑ ㅒ ㅓ ㅔ ㅕ ㅖ ㅗ ㅘ ㅙ ㅚ ㅛ ㅜ ㅝ ㅞ ㅟ ㅠ ㅡ ㅢ ㅣ
ㅌ ㅥ ㅦ ㅧ ㅨ ㅩ ㅪ ㅫ ㅬ ㅭ ㅮ ㅯ ㅰ ㅱ ㅲ ㅳ ㅴ ㅵ ㅶ ㅷ ㅸ ㅹ ㅺ ㅻ ㅼ ㅽ ㅾ ㅿ ㆀ ㆁ ㆂ ㆃ ㆄ ㆅ ㆆ ㆇ ㆈ ㆉ ㆊ ㆋ ㆌ ㆍ ㆎ
Α Β Γ Δ Ε Ζ Η Θ Ι Κ Λ Μ Ν Ξ Ο Π Ρ Σ Τ Υ Φ Χ Ψ Ω α β γ δ ε ζ η θ ι κ λ μ ν ξ ο π ρ σ τ υ φ χ ψ ω

View File

@@ -1,7 +1,8 @@
#!/usr/bin/env python3
"""Extract Hanja readings from libhangul data."""
"""Extract Hanja and symbol readings from libhangul data."""
import sys
import unicodedata
from pathlib import Path
@@ -9,12 +10,31 @@ def ishangul(s):
return s != "" and all(0xAC00 <= ord(c) <= 0xD7A3 for c in s)
def isjamo(s):
return len(s) == 1 and 0x3131 <= ord(s) <= 0x314E
def ishanja(s):
return s != "" and all(0x3400 <= ord(c) <= 0x4DBF
or 0x4E00 <= ord(c) <= 0x9FFF
or 0xF900 <= ord(c) <= 0xFAFF for c in s)
def issymbol(s):
"""One rune the popup can draw and a candidate row can carry: not a
space, which the row separates candidates with, and not a formatting
character, which would leave a blank candidate to pick."""
return (len(s) == 1 and not s.isspace() and not ishanja(s)
and not unicodedata.category(s).startswith("C"))
def keeps(reading, value):
"""A syllable reading gives Hanja; a jamo reading gives a symbol."""
if ishangul(reading):
return ishanja(value)
return isjamo(reading) and issymbol(value)
def extract(src, name):
comments = []
entries = []
@@ -32,16 +52,16 @@ def extract(src, name):
fields = line.split(":")
if len(fields) != 3:
raise ValueError(f"{name}:{lineno}: need key:value:comment")
reading, hanja, _ = fields
if not ishangul(reading) or not ishanja(hanja):
reading, value, _ = fields
if not keeps(reading, value):
continue
pair = (hanja, reading)
pair = (value, reading)
if pair in seen:
raise ValueError(f"{name}:{lineno}: duplicate Hanja reading")
raise ValueError(f"{name}:{lineno}: duplicate reading")
seen.add(pair)
entries.append(pair)
if not entries:
raise ValueError(f"{name}: no Hanja readings")
raise ValueError(f"{name}: no readings")
return comments, entries
@@ -63,8 +83,8 @@ def main():
print(line)
if comments:
print()
for hanja, reading in entries:
print(f"{hanja}\t{reading}")
for value, reading in entries:
print(f"{value}\t{reading}")
except (OSError, UnicodeError, ValueError) as error:
print(error, file=sys.stderr)
return 1

View File

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

1012
map/mssymbol.src Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -10,31 +10,31 @@
☺ :)
# Bare 1-9 choose from a prefix search; keep digit aliases in matching slots.
← <-
≤ <=
♥ <3
≠ <> !=
← <- <
≤ <= <
♥ <3 <
≠ <> != <
≡ ==
⇒ =>
≥ >=
¹ ^1
² ^2
³ ^3
⁴ ^4
⁵ ^5
⁶ ^6
⁷ ^7
⁸ ^8
⁹ ^9
₁ _1
₂ _2
₃ _3
₄ _4
₅ _5
₆ _6
₇ _7
₈ _8
₉ _9
¹ ^1 ^
² ^2 ^
³ ^3 ^
⁴ ^4 ^
⁵ ^5 ^
⁶ ^6 ^
⁷ ^7 ^
⁸ ^8 ^
⁹ ^9 ^
₁ _1 _
₂ _2 _
₃ _3 _
₄ _4 _
₅ _5 _
₆ _6 _
₇ _7 _
₈ _8 _
₉ _9 _
⁽ ^(
⁾ ^)

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>

24
srv.c
View File

@@ -26,16 +26,22 @@ static int
srvnote(void *v, char *note)
{
USED(v);
USED(note);
srvunlink();
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)
@@ -62,6 +68,12 @@ srvreadreq(int fd, Keyreq *kr, int *want)
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;
}
@@ -78,7 +90,7 @@ clientthread(void *arg)
Keyres res;
uchar out[Ipcmaxresp], token;
char commit[Maxutf], preedit[Maxutf];
int fd, n, ncommit, npreedit, want;
int fd, n, ncommit, npreedit, rv, want;
fd = (int)(uintptr)arg;
threadsetname("client %d", fd);
@@ -86,7 +98,9 @@ clientthread(void *arg)
kr.reply = chancreate(sizeof(Keyres), 0);
kr.owner = &fd;
kr.clientpre = 1;
while(srvreadreq(fd, &kr, &want) >= 0){
while((rv = srvreadreq(fd, &kr, &want)) >= 0){
if(rv != 0)
continue;
if(kr.op != Keycaret)
kr.clientpre = want;
chansend(keyc, &kr);
@@ -97,7 +111,7 @@ clientthread(void *arg)
npreedit = stoutf(&res.preedit, preedit, sizeof preedit);
/* A capability reply is always eaten: it marks the extension. */
n = ipcpackresp(out, sizeof out, kr.op == Keycap || res.eaten,
commit, ncommit, preedit, npreedit, want);
res.del, commit, ncommit, preedit, npreedit, want);
if(n < 0 || ipcsend(fd, out, n) < 0)
break;
}

15
str.c
View File

@@ -24,6 +24,21 @@ sinit(Str *s, char *src, int n)
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);
if(!sinit(s, p, n - (p - src)))
sclear(s);
}
void
sclear(Str *s)
{

127
strans.c
View File

@@ -7,17 +7,23 @@ static void *lostowner; /* the context the engine was taken from */
static Str lost;
static int clientpre; /* the owner draws its own preedit */
static Caret caret;
static Str surround; /* the owner's own text just before the cursor */
static int takeback; /* runes of it a pick takes back before committing */
static Rune modemark; /* the mode the last key switched to */
static int okuriat = -1; /* where Shift marked the okurigana, or -1 */
static Drawcmd lastdraw;
static Emit transjp(Im*, Rune);
static void backjp(Im*);
/* seed is what Ctrl+H found pending; raw the keys typed since; text both. */
/*
* back is the client's own text the reading reached into; seed what
* Ctrl+H found pending; raw the keys typed since; text all three.
*/
typedef struct Search Search;
struct Search
{
int lang;
Str back;
Str seed;
Str raw;
Str text;
@@ -91,11 +97,12 @@ movedelta(u32int ks)
return 0;
}
/* Nothing chosen yet: Down takes the first candidate, PageDown pages. */
static void
movekouho(int delta)
{
if(im.sel < 0)
im.sel = 0;
im.sel = delta == Maxdisp ? Maxdisp : 0;
else
im.sel += delta;
if(im.sel < 0)
@@ -184,6 +191,17 @@ impre(Im *p, Str *s)
*s = p->pre;
}
/* The query as it is still ours to show: the client has the rest. */
static void
searchpre(Str *s)
{
int i;
sclear(s);
for(i = search.back.n; i < search.text.n; i++)
sputr(s, search.text.r[i]);
}
static void
snapshot(Drawcmd *dc)
{
@@ -192,7 +210,7 @@ snapshot(Drawcmd *dc)
memset(dc, 0, sizeof *dc);
if(search.lang)
pre = search.text;
searchpre(&pre);
else
impre(&im, &pre);
if(!clientpre)
@@ -265,7 +283,8 @@ okuriletter(Rune r)
{
static char *rows[] = {
"aあ", "iい", "uう", "eえ", "oお",
"kかきくけこ", "gがぎぐげご", "sさしすせそ", "zざずぜぞ",
"kかきくけこ", "gがぎぐげご", "sさしすせそ", "zざずぜぞ",
"jじ",
"tたちつてとっ", "dだぢづでど", "nなにぬねのん", "hはひふへほ",
"bばびぶべぼ", "pぱぴぷぺぽ", "mまみむめも", "yやゆよ",
"rらりるれろ", "wわ",
@@ -625,13 +644,22 @@ emojiquery(void)
selectfirst();
}
/* The keys typed since Ctrl+H go on composing from the seed. */
/*
* The keys typed since Ctrl+H go on composing from the seed. A reading
* is also the start of the longer words it begins, so its own
* conversions come first and theirs follow: 대한 answers 大寒 and, past
* them, 大韓民國.
*/
static void
hanjaquery(void)
{
transstr(im.l, &search.seed, &search.raw, &search.text);
Str tail;
transstr(im.l, &search.seed, &search.raw, &tail);
search.text = search.back;
sappend(&search.text, &tail);
clearkouho();
im.nkouho = dictlookup(getlang(LangHANJA)->dict, &search.text,
im.nkouho = dictprefix(getlang(LangHANJA)->dict, &search.text,
im.kouho, Maxkouho);
selectfirst();
}
@@ -673,6 +701,7 @@ reset(void)
{
sclear(&im.pre);
sclear(&im.raw);
modemark = 0;
okuriat = -1;
endsearch();
}
@@ -685,9 +714,12 @@ reset(void)
static void
flush(Str *com)
{
if(search.lang)
sappend(com, &search.text);
else if(im.sel >= 0)
Str pre;
if(search.lang){
searchpre(&pre);
sappend(com, &pre);
}else if(im.sel >= 0)
sappend(com, &im.kouho[im.sel]);
else
commitim(&im, com);
@@ -708,6 +740,46 @@ takelost(void *owner, Str *com)
lostowner = nil;
}
/* The syllables from i to the end of the client's text, then seed. */
static void
reachkey(int i, Str *seed, Str *key)
{
sclear(key);
for(; i < surround.n; i++)
sputr(key, surround.r[i]);
sappend(key, seed);
}
/*
* Ctrl+H converts a word, and a word is committed one syllable at a
* time: only its last syllable is still pending. So the reading reaches
* back into the text the client already has, as far as the dictionary
* still knows the whole of it -- 한자 converts as a word where the 한 is
* the client's and only the 자 is ours. Nothing but the dictionary says
* where to stop: a reading is syllables, so a key holding a space or a
* Hanja leads nowhere and the reach ends there.
*/
static void
reachback(Str *seed, Str *back)
{
Str key;
Trie *t;
int i, best;
t = getlang(LangHANJA)->dict;
best = surround.n;
for(i = surround.n - 1; i >= 0; i--){
if(surround.n - i + seed->n > Maxrunes)
break;
reachkey(i, seed, &key);
if(trienode(t, &key) >= 0)
best = i;
}
sclear(back);
for(i = best; i < surround.n; i++)
sputr(back, surround.r[i]);
}
/*
* A search takes over the keys. Hanja converts the pending syllable, so
* it becomes the query; anything else pending is committed.
@@ -715,23 +787,28 @@ takelost(void *owner, Str *com)
static void
startsearch(int lang, Str *com)
{
Str seed;
Str back, seed;
sclear(&back);
sclear(&seed);
if(lang == LangHANJA && !search.lang && im.sel < 0)
if(lang == LangHANJA && !search.lang && im.sel < 0){
impre(&im, &seed);
else
reachback(&seed, &back);
}else
flush(com);
reset();
search.lang = lang;
search.back = back;
search.seed = seed;
if(lang == LangHANJA)
searchquery();
}
/* The client keeps what the reading reached back into; we take it back. */
static void
picksearch(int n, Str *com)
{
takeback = search.back.n;
sappend(com, &im.kouho[n]);
endsearch();
}
@@ -841,6 +918,11 @@ transition(u32int ks, u32int mod, Str *com)
break;
case Kmuhenkan: ks = 't'; mod = Mctrl; break;
}
/* Ctrl+Backspace deletes a word, Ctrl+Tab changes tab: not ours. */
if(ks >= Kspec && (mod & ~Mshift)){
flush(com);
return 0;
}
if(search.lang)
return searchkey(ks, mod, com);
n = movedelta(ks);
@@ -858,12 +940,15 @@ transition(u32int ks, u32int mod, Str *com)
if((ks == ' ' || ks == Ktab) && isjp(&im) && haspre(&im) &&
!(mod & ~Mshift)){
/* Space and Tab convert: they step through the candidates,
* Katakana last; a reading with none is committed. */
* Katakana last. Katakana mode converts nothing, so there
* the key only commits, and then it is the application's:
* the space between two Katakana words is a space. */
katakouho();
if(im.nkouho != 0)
cyclekouho(mod & Mshift ? -1 : 1);
else
if(im.nkouho == 0){
flush(com);
return 0;
}
cyclekouho(mod & Mshift ? -1 : 1);
return 1;
}
if(ks == Ktab || ks == Kret || (ks == Kesc && !isjp(&im))){
@@ -938,6 +1023,8 @@ init(void)
activeowner = nil;
lostowner = nil;
clientpre = 0;
sclear(&surround);
takeback = 0;
memset(&lastdraw, 0, sizeof lastdraw);
}
@@ -956,6 +1043,7 @@ imhandlekey(Keyreq *kr)
sclear(&res.commit);
sclear(&res.preedit);
res.eaten = 1;
takeback = 0;
switch(kr->op){
case Keyrelease:
takelost(kr->owner, &res.commit);
@@ -981,6 +1069,7 @@ imhandlekey(Keyreq *kr)
break;
case Keypress:
res.eaten = 0;
takelost(kr->owner, &res.commit);
if(kr->owner == activeowner)
clientpre = kr->clientpre;
if(keymeaningful(kr->ks)){
@@ -992,6 +1081,7 @@ imhandlekey(Keyreq *kr)
clientpre = kr->clientpre;
}
caret = kr->caret;
surround = kr->surround;
res.eaten = transition(kr->ks, kr->mod, &res.commit);
}
break;
@@ -999,10 +1089,11 @@ imhandlekey(Keyreq *kr)
redraw();
if(kr->owner == activeowner){
if(search.lang)
res.preedit = search.text;
searchpre(&res.preedit);
else
impre(&im, &res.preedit);
}
res.del = takeback;
chansend(kr->reply, &res);
}

View File

@@ -4,7 +4,7 @@ HOSTCC = cc
PKG_CONFIG ?= pkg-config
CFLAGS ?= -O2 -g
UNIT_CPPFLAGS = -I..
UNIT_CFLAGS = -std=c99 -Wall -Wextra
UNIT_CFLAGS = -Wall -Wextra
HOST_CFLAGS = -std=c99 -Wall -Wextra
TEXT_CFLAGS = $(shell $(PKG_CONFIG) --cflags pangocairo cairo fontconfig)
TEXT_LIBS = $(shell $(PKG_CONFIG) --libs pangocairo cairo fontconfig)
@@ -20,7 +20,9 @@ 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)
UNIT_LDLIBS = -lthread -lbio $(TEXT_LIBS) $(IBUS_LIBS) $(XIM_LIBS)
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
STRESS = stress_test
@@ -33,10 +35,11 @@ LIVE = $(SMOKE) $(FAULT)
TESTSRC = test_util.c str_test.c trie_test.c \
ko_test.c vi_test.c engine_test.c dict_test.c ipc_test.c \
popup_test.c font_test.c ibus_test.c server_test.c compose_test.c \
xim_adapter_test.c
xim_adapter_test.c wl_adapter_test.c
TESTOBJ = $(TESTSRC:.c=.o)
# imv2.c and vkv1.c are wayland-scanner's; the parent Makefile writes them.
PARENTSRC = str.c trie.c dict.c ko.c vi.c ipc.c popup_layout.c \
font.c compose.c
font.c compose.c imv2.c vkv1.c
PARENTOBJ = $(PARENTSRC:%.c=unit_%.o)
COMMONOBJ = $(TESTOBJ) $(PARENTOBJ)
OBJS = unit_test.o stress_test.o $(COMMONOBJ)
@@ -92,6 +95,9 @@ gtk_live_test: gtk_live_test.c $(LIVESRC) ../ipc.c ../ipc.h
../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) \
@@ -124,6 +130,8 @@ ibus_test.o: UNIT_CPPFLAGS += $(IBUS_CFLAGS)
ibus_test.o: ../ibus.c
xim_adapter_test.o: UNIT_CPPFLAGS += $(XIM_CFLAGS)
xim_adapter_test.o: ../xim.c
wl_adapter_test.o unit_imv2.o unit_vkv1.o: UNIT_CPPFLAGS += $(WL_CFLAGS)
wl_adapter_test.o: ../wl.c ../imv2.h ../vkv1.h
server_test.o: ../srv.c
unit_font.o: UNIT_CPPFLAGS += $(TEXT_CFLAGS)
unit_compose.o compose_test.o: UNIT_CPPFLAGS += $(IBUS_CFLAGS)

View File

@@ -3,24 +3,37 @@
#include "test.h"
#include <xkbcommon/xkbcommon-keysyms.h>
/* A sequence swallows its keys and hands its text back at the end. */
/*
* 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[] = {
{ XKB_KEY_a, 0, "" },
{ XKB_KEY_dead_acute, 1, "" },
{ XKB_KEY_e, 0, "é" },
{ XKB_KEY_Multi_key, 1, "" },
{ XKB_KEY_o, 1, "" },
{ XKB_KEY_c, 0, "©" },
{ XKB_KEY_dead_acute, 1, "" },
{ XKB_KEY_Escape, 1, "" },
{ XKB_KEY_a, 0, "" },
{ 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;
@@ -29,9 +42,15 @@ compose_sequences(struct ct *t)
return;
composeinit();
for(i = 0; i < nelem(keys); i++){
CT_EQ_INT(t, keys[i].composing,
composekey(keys[i].sym, buf, sizeof buf));
CT_EQ_INT(t, keys[i].composing, composekey(keys[i].who,
keys[i].owner, keys[i].sym, buf, sizeof buf));
CT_EQ_STR(t, keys[i].text, buf);
}
/* a context that goes takes its half-typed sequence with it */
CT_EQ_INT(t, 1,
composekey(Composewl, &a, XKB_KEY_dead_acute, buf, sizeof buf));
composedrop(Composewl, &a);
CT_EQ_INT(t, 0, composekey(Composewl, &a, XKB_KEY_e, buf, sizeof buf));
CT_EQ_STR(t, "", buf);
unsetenv("XCOMPOSEFILE");
}

View File

@@ -140,6 +140,38 @@ openpersistent(Test *t)
return 1;
}
/*
* plan9port ignores a broken pipe: the note handlers run and the daemon
* goes on serving, so the endpoints it announced must still be there.
*/
static int
survivenote(Test *t)
{
struct stat st;
int fd, ok;
if(kill(t->firstpid, SIGPIPE) < 0)
return fail("send a broken pipe to the first daemon: %s",
strerror(errno));
pausems(200);
if(!daemonalive(&t->first))
return fail("first daemon died of a broken pipe");
if(lstat(t->l.socket, &st) < 0)
return fail("IPC socket taken by a note the daemon survived: %s",
strerror(errno));
if(lstat(t->l.addrfile, &st) < 0)
return fail("IBus address taken by a note the daemon survived: %s",
strerror(errno));
fd = connectsocket(t->l.socket, nowms() + Calltimeout);
if(fd < 0)
return fail("connect after a broken pipe: %s", strerror(errno));
ok = ipcprobe(fd, "after a broken pipe");
if(close(fd) < 0)
return fail("close the client opened after a broken pipe: %s",
strerror(errno));
return ok;
}
static int
hardcrash(Test *t)
{
@@ -265,8 +297,8 @@ rejectold(Test *t, char *where)
static int
openreplacement(Test *t)
{
unsigned char selectjp[] = {1, 0, 0, 0, 0};
unsigned char keyk[] = {1, 0, 0, 1, 0, 'k'};
unsigned char selectjp[] = {1, 0, 0, 0, 0, 0};
unsigned char keyk[] = {1, 0, 0, 0, 1, 0, 'k'};
char path[96];
t->ipcnew = connectsocket(t->l.socket, nowms() + Calltimeout);
@@ -329,6 +361,8 @@ runrestart(Test *t, char *program, char *mapdir)
if(!waitready(&t->l, &t->first, t->firstaddress, sizeof t->firstaddress) ||
!privateaddress(t->firstaddress, t->firstpid) || !openpersistent(t))
return 0;
if(!survivenote(t))
return 0;
if(!hardcrash(t) || !waitipcclosed(t) || !waitbusclosed(t) ||
!checkstale(t) || !rejectold(t, "after hard crash"))
return 0;
@@ -367,6 +401,6 @@ main(int argc, char **argv)
showerrors(&test.second);
return 1;
}
printf("daemon hard-crash endpoint recovery: ok\n");
printf("daemon endpoints across a note and a hard crash: ok\n");
return 0;
}

View File

@@ -10,6 +10,7 @@ keystroke(u32int ks, u32int mod, Str *com)
{
int eaten;
takeback = 0;
eaten = keymeaningful(ks) && transition(ks, mod, com);
redraw();
return eaten;
@@ -175,7 +176,7 @@ engine_candidate_shortcut_modifiers(struct ct *t)
static Keyres
ownerrequestatcap(void *owner, int cap, int op, Rune key, u32int mod,
Caret *caret)
Caret *caret, char *before)
{
Channel *reply;
Keyreq req;
@@ -190,6 +191,8 @@ ownerrequestatcap(void *owner, int cap, int op, Rune key, u32int mod,
req.mod = mod;
if(caret != nil)
req.caret = *caret;
if(before != nil)
req.surround = mkstr(before);
req.reply = reply;
imhandlekey(&req);
chanrecv(reply, &res);
@@ -200,7 +203,7 @@ ownerrequestatcap(void *owner, int cap, int op, Rune key, u32int mod,
static Keyres
ownerrequestat(void *owner, int op, Rune key, u32int mod, Caret *caret)
{
return ownerrequestatcap(owner, 0, op, key, mod, caret);
return ownerrequestatcap(owner, 0, op, key, mod, caret, nil);
}
static Keyres
@@ -212,7 +215,7 @@ ownerrequest(void *owner, int op, Rune key, u32int mod)
static Keyres
ownerrequestcap(void *owner, int cap, int op, Rune key, u32int mod)
{
return ownerrequestatcap(owner, cap, op, key, mod, nil);
return ownerrequestatcap(owner, cap, op, key, mod, nil, nil);
}
void
@@ -258,6 +261,20 @@ engine_active_owner_lifecycle(struct ct *t)
res = ownerrequest(&c, Keypress, 'k', 0);
CT_EQ_PTR(t, &c, activeowner);
checkstr(t, "next owner first key", "k", &res.preedit);
/* Typing is being heard from, as much as a reset or a release is. */
init();
im.l = getlang(LangJP);
ownerrequest(&a, Keypress, 'k', 0);
ownerrequest(&a, Keypress, 'a', 0);
ownerrequest(&b, Keypress, 'n', 0);
CT_EQ_PTR(t, &b, activeowner);
res = ownerrequest(&a, Keypress, 'k', 0);
checkstr(t, "the taken owner types and gets its reading back", "",
&res.commit);
CT_EQ_PTR(t, &a, activeowner);
res = ownerrequest(&b, Keyreset, 0, 0);
checkstr(t, "the owner it took from keeps its own", "", &res.commit);
}
void
@@ -560,6 +577,15 @@ engine_korean_modifiers_and_backspace(struct ct *t)
CT_EQ_INT(t, 0, im.pre.n);
CT_CHECK(t, !keystroke(Kback, 0, &com));
}
init();
im.l = getlang(LangKO);
sclear(&com);
if(typekeys(t, "rk", &com)){
CT_CHECK(t, !keystroke(Kback, Mctrl, &com));
checkstr(t, "Ctrl+Backspace commits and passes", "", &com);
CT_EQ_INT(t, 0, im.pre.n);
}
}
void
@@ -567,6 +593,7 @@ engine_direct_language_modes(struct ct *t)
{
Drawcmd dc;
Str com;
char owner;
init();
draindraw(nil);
@@ -607,6 +634,16 @@ engine_direct_language_modes(struct ct *t)
checkstr(t, "Korean without map", "", &im.pre);
CT_CHECK(t, keystroke(Kback, 0, &com));
CT_EQ_INT(t, 0, im.pre.n);
/* Leaving takes the mark away as surely as typing does. */
init();
draindraw(nil);
ownerrequest(&owner, Keypress, 's', Mctrl);
if(CT_CHECK(t, draindraw(&dc) > 0))
checkstr(t, "mode mark", "", &dc.pre);
ownerrequest(&owner, Keyrelease, 0, 0);
if(CT_CHECK(t, draindraw(&dc) > 0))
CT_EQ_INT(t, 0, dc.pre.n);
}
void
@@ -837,6 +874,10 @@ engine_candidate_page_movement(struct ct *t)
{ 32, 22, Kpgdown, 31 },
{ 32, 31, Kpgup, 22 },
{ 32, 31, Kpgdown, 31 },
{ 32, -1, Kdown, 0 },
{ 32, -1, Kup, 0 },
{ 32, -1, Kpgdown, 9 },
{ 32, -1, Kpgup, 0 },
};
int first, i, n;
Str com;
@@ -1066,6 +1107,8 @@ hanjabegin(Searchfix *f, int lang)
f->dictlang->dict = trienew();
setdict(f->dictlang->dict, "", "漢 韓");
setdict(f->dictlang->dict, "", "");
setdict(f->dictlang->dict, "", "");
setdict(f->dictlang->dict, "민국", "民國");
init();
im.l = getlang(lang);
}
@@ -1244,6 +1287,19 @@ engine_japanese_candidates(struct ct *t)
checkstr(t, "sokuon written in kana", "行った", &im.kouho[1]);
}
CT_CHECK(t, keystroke(Kesc, 0, &com));
/* じ is keyed j, as SKK types it; ざずぜぞ keep z. */
setdict(jp->dict, "しんj", "");
setdict(jp->dict, "まz", "");
if(!typekeys(t, "shinjiru", &com))
goto cleanup;
if(CT_EQ_INT(t, 1, im.nkouho))
checkstr(t, "じ okurigana", "信じる", &im.kouho[0]);
CT_CHECK(t, keystroke(Kesc, 0, &com));
if(!typekeys(t, "mazeru", &com))
goto cleanup;
if(CT_EQ_INT(t, 1, im.nkouho))
checkstr(t, "ぜ okurigana", "混ぜる", &im.kouho[0]);
CT_CHECK(t, keystroke(Kesc, 0, &com));
/* A romaji typo stays in the reading for Backspace to mend. */
if(!typekeys(t, "kanjr", &com))
goto cleanup;
@@ -1388,6 +1444,16 @@ engine_katakana_sequences(struct ct *t)
checkstr(t, "committed Katakana", cases[i].want, &com);
}
CT_EQ_PTR(t, nil, getlang(LangJPK)->dictname);
/* Nothing to convert to: the key commits and goes on to the client. */
init();
im.l = getlang(LangJPK);
sclear(&com);
if(typekeys(t, "kaku", &com)){
CT_CHECK(t, !keystroke(' ', 0, &com));
checkstr(t, "Space commits the Katakana", "カク", &com);
CT_EQ_INT(t, 0, im.nkouho);
}
}
void
@@ -1882,6 +1948,107 @@ cleanup:
searchend(&f);
}
/*
* A word is committed one syllable at a time, so Ctrl+H reaches back
* into the text the client already has, and a pick takes it back.
*/
void
engine_hanja_reaches_back(struct ct *t)
{
Searchfix f;
Keyres res;
Str com;
char owner;
hanjabegin(&f, LangKO);
surround = mkstr("");
im.pre = mkstr("");
sclear(&com);
if(!CT_CHECK(t, keystroke('h', Mctrl, &com)))
goto cleanup;
checkstr(t, "the client's syllable joined the reading", "민국",
&search.text);
checkstr(t, "what the client already has", "", &search.back);
CT_EQ_INT(t, 1, im.nkouho);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "the word converted", "民國", &com);
CT_EQ_INT(t, 1, takeback);
/* Escape gives back what was pending, and nothing of the client's. */
surround = mkstr("");
im.pre = mkstr("");
sclear(&com);
CT_CHECK(t, keystroke('h', Mctrl, &com));
CT_CHECK(t, keystroke(Kesc, 0, &com));
checkstr(t, "only the pending syllable comes back", "", &im.pre);
CT_EQ_INT(t, 0, takeback);
/* A reading nothing is keyed by stops the reach where it stands. */
surround = mkstr("");
im.pre = mkstr("");
sclear(&com);
CT_CHECK(t, keystroke('h', Mctrl, &com));
checkstr(t, "no reach without a reading", "", &search.text);
CT_EQ_INT(t, 0, search.back.n);
CT_CHECK(t, keystroke(Kesc, 0, &com));
/* A query without candidates types only what the client lacks. */
surround = mkstr("");
im.pre = mkstr("");
sclear(&com);
CT_CHECK(t, keystroke('h', Mctrl, &com));
if(typekeys(t, "r", &com)){
CT_EQ_INT(t, 0, im.nkouho);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "the client's syllable is not typed again", "국ㄱ",
&com);
CT_EQ_INT(t, 0, takeback);
}
/* The frontend's own request carries the text and is told to take it. */
init();
im.l = getlang(LangKO);
ownerrequestatcap(&owner, 1, Keypress, 'r', 0, nil, "");
ownerrequestatcap(&owner, 1, Keypress, 'n', 0, nil, "");
res = ownerrequestatcap(&owner, 1, Keypress, 'r', 0, nil, "");
checkstr(t, "the syllable the request composed", "", &res.preedit);
CT_EQ_INT(t, 0, res.del);
ownerrequestatcap(&owner, 1, Keypress, 'h', Mctrl, nil, "");
checkstr(t, "the request's own text joined the reading", "민국",
&search.text);
res = ownerrequestatcap(&owner, 1, Keypress, Kret, 0, nil, "");
checkstr(t, "the word the request converted", "民國", &res.commit);
CT_EQ_INT(t, 1, res.del);
cleanup:
searchend(&f);
}
/*
* A reading is the start of the longer words it begins: its own
* conversions come first, and past them theirs.
*/
void
engine_hanja_word_prefix(struct ct *t)
{
Searchfix f;
Str com;
hanjabegin(&f, LangKO);
sclear(&com);
if(!CT_CHECK(t, keystroke('h', Mctrl, &com)) ||
!typekeys(t, "als", &com))
goto cleanup;
checkstr(t, "the reading typed so far", "", &search.text);
CT_EQ_INT(t, 2, im.nkouho);
checkstr(t, "the reading's own conversion", "", &im.kouho[0]);
checkstr(t, "the word it begins", "民國", &im.kouho[1]);
CT_CHECK(t, keystroke(Kdown, 0, &com));
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "a word longer than the reading", "民國", &com);
cleanup:
searchend(&f);
}
void
engine_hanja_korean_keys(struct ct *t)
{

View File

@@ -32,6 +32,7 @@ enum
Ecaret,
Ekey,
Ereset,
Esurround,
};
enum
@@ -55,6 +56,7 @@ struct Event
int32_t x;
int32_t y;
int32_t h;
char text[Ipcfieldmax+1];
};
struct Server
@@ -69,6 +71,7 @@ struct Server
int eaten;
int stall;
int response;
int del;
int nclose;
char pre[Ipcfieldmax+1];
Event ev[Maxevent];
@@ -83,6 +86,10 @@ struct Siglog
char shown[Ipcfieldmax+1];
char commit[Ipcfieldmax+1];
int prechanged;
char around[64]; /* what the widget answers retrieve-surrounding */
int aroundcursor;
int deloffset;
int deln;
};
static void
@@ -110,13 +117,14 @@ sendresponse(Server *s, int fd, int eaten, int want, int key)
unsigned char buf[Ipcmaxresp];
const char *commit;
char pre[Ipcfieldmax+1];
int n, response, stall;
int del, n, response, stall;
size_t ncommit, npreedit;
pthread_mutex_lock(&s->lock);
memcpy(pre, s->pre, sizeof pre);
response = s->response;
stall = s->stall;
del = key ? s->del : 0;
pthread_mutex_unlock(&s->lock);
if(stall)
return 1;
@@ -135,7 +143,7 @@ sendresponse(Server *s, int fd, int eaten, int want, int key)
pre[2] = 'y';
npreedit = 3;
}
n = ipcpackresp(buf, sizeof buf, eaten, commit, ncommit,
n = ipcpackresp(buf, sizeof buf, eaten, del, commit, ncommit,
pre, npreedit, want);
return n >= 0 && ipcsend(fd, buf, n) == 0;
}
@@ -146,6 +154,7 @@ request(Server *s, int fd)
unsigned char buf[Ipccaretsz];
uint32_t mod, key;
Event e;
size_t n;
int old, type, want;
if(ipcreadn(fd, buf, Ipcreqsz) < 0)
@@ -168,6 +177,14 @@ request(Server *s, int fd)
e.type = Ecaret;
record(s, &e);
return 1;
case Ipcsurround:
n = ipcsurroundlen(buf);
if(n > 0 && ipcreadn(fd, e.text, n) < 0)
return 0;
e.text[n] = '\0';
e.type = Esurround;
record(s, &e);
return 1;
case Ipckey:
ipcunpackreq(buf, &want, &mod, &key);
e.want = want;
@@ -513,6 +530,25 @@ clearlog(Siglog *l)
memset(l, 0, sizeof *l);
}
/* The widget's half of the surrounding-text contract. */
static gboolean
retrievearound(GtkIMContext *ctx, Siglog *l)
{
if(l->around[0] == '\0')
return FALSE;
gtk_im_context_set_surrounding(ctx, l->around, -1, l->aroundcursor);
return TRUE;
}
static gboolean
deletearound(GtkIMContext *ctx, gint offset, gint n, Siglog *l)
{
(void)ctx;
l->deloffset = offset;
l->deln = n;
return TRUE;
}
static GtkIMContext*
newcontext(Siglog *l)
{
@@ -520,6 +556,8 @@ newcontext(Siglog *l)
ctx = gtk_im_multicontext_new();
gtk_im_multicontext_set_context_id(GTK_IM_MULTICONTEXT(ctx), "strans");
g_signal_connect(ctx, "retrieve-surrounding", G_CALLBACK(retrievearound), l);
g_signal_connect(ctx, "delete-surrounding", G_CALLBACK(deletearound), l);
g_signal_connect(ctx, "preedit-start", G_CALLBACK(prestart), l);
g_signal_connect(ctx, "preedit-changed", G_CALLBACK(prechange), l);
g_signal_connect(ctx, "preedit-end", G_CALLBACK(preend), l);
@@ -570,6 +608,25 @@ newwindow(int x, int y)
return w;
}
/* A GtkEntry gives its context a window of its own, under the toplevel's. */
static GdkWindow*
entrywindow(GtkWidget *entry)
{
GdkWindow *w;
GList *kids, *l;
gpointer u;
w = NULL;
kids = gdk_window_get_children(gtk_widget_get_window(entry));
for(l = kids; l != NULL; l = l->next){
gdk_window_get_user_data(l->data, &u);
if(u == entry)
w = l->data;
}
g_list_free(kids);
return w;
}
#define Check(c, ...) do{ if(!(c)){ fail(__VA_ARGS__); goto out; } }while(0)
int
@@ -578,8 +635,8 @@ main(int argc, char **argv)
Server srv;
Siglog log;
GtkIMContext *ctx, *oldctx, *badctx;
GtkWidget *top;
GdkWindow *win;
GtkWidget *top, *entry;
GdkWindow *win, *entrywin;
GdkRectangle rect;
Event e;
static const GtkInputPurpose purposes[] = {
@@ -648,7 +705,7 @@ main(int argc, char **argv)
Check(eventcount(&srv) == 0, "cursor reporting opened the socket");
setpre(&srv, "");
Check(key(ctx, win, GDK_KEY_a), "initial key was not eaten");
Check(waitcount(&srv, 3), "initial protocol frames timed out");
Check(waitcount(&srv, 4), "initial protocol frames timed out");
e = getevent(&srv, 0);
Check(e.type == Ecap && e.want == 1, "default preedit capability missing");
e = getevent(&srv, 1);
@@ -663,6 +720,9 @@ main(int argc, char **argv)
e.x, e.y, e.h, (ox + rect.x) * scale,
(oy + rect.y) * scale, rect.height * scale);
e = getevent(&srv, 2);
Check(e.type == Esurround && e.text[0] == '\0',
"a widget that keeps no text sent some");
e = getevent(&srv, 3);
Check(e.type == Ekey && e.want == 1, "initial key framing changed");
Check(log.n == 0, "initial empty preedit emitted %s", log.event);
setpre(&srv, "");
@@ -757,6 +817,66 @@ main(int argc, char **argv)
}
g_object_set(ctx, "input-purpose", GTK_INPUT_PURPOSE_FREE_FORM, NULL);
/* An entry hidden the older way is private too, purpose or no purpose. */
entry = gtk_entry_new();
gtk_container_add(GTK_CONTAINER(top), entry);
gtk_widget_show(entry);
pump();
entrywin = entrywindow(entry);
Check(entrywin != NULL, "the test GtkEntry has no window of its own");
first = eventcount(&srv);
gtk_im_context_set_client_window(ctx, entrywin);
/* The caret the new window invalidates must land before keys are counted. */
Check(waitcount(&srv, first + 1) && getevent(&srv, first).type == Ecaret,
"the entry window did not renegotiate the caret");
setpre(&srv, "");
gtk_entry_set_visibility(GTK_ENTRY(entry), FALSE);
clearlog(&log);
first = eventcount(&srv);
Check(key(ctx, entrywin, GDK_KEY_y), "hidden-entry fallback did not commit");
Check(eventcount(&srv) == first && strcmp(log.commit, "y") == 0,
"hidden-entry key reached daemon or did not commit");
gtk_entry_set_visibility(GTK_ENTRY(entry), TRUE);
clearlog(&log);
first = eventcount(&srv);
Check(key(ctx, entrywin, GDK_KEY_y), "visible-entry key was not eaten");
Check(waitcount(&srv, first + 1) && log.commit[0] == '\0',
"visible-entry key did not reach the daemon");
gtk_im_context_set_client_window(ctx, win);
/* The widget's own text goes over, and a take-back comes back. */
setpre(&srv, "");
clearlog(&log);
snprintf(log.around, sizeof log.around, "%s", "가나한");
log.aroundcursor = 9;
first = eventcount(&srv);
Check(key(ctx, win, GDK_KEY_j), "surrounding-text key was not eaten");
Check(waitcount(&srv, first + 2), "surrounding text did not reach the daemon");
e = getevent(&srv, first);
Check(e.type == Esurround && strcmp(e.text, "가나한") == 0,
"the daemon was sent %s, not the text before the cursor", e.text);
Check(getevent(&srv, first + 1).type == Ekey,
"the text did not ride ahead of the key");
/* Sent again only when it changes. */
first = eventcount(&srv);
Check(key(ctx, win, GDK_KEY_j), "second surrounding-text key was not eaten");
Check(waitcount(&srv, first + 1) && getevent(&srv, first).type == Ekey,
"unchanged surrounding text was sent again");
setreply(&srv, 1, Rnormal, 0);
pthread_mutex_lock(&srv.lock);
srv.del = 2;
pthread_mutex_unlock(&srv.lock);
log.deln = 0;
Check(key(ctx, win, GDK_KEY_j), "take-back key was not eaten");
Check(log.deloffset == -2 && log.deln == 2,
"the widget was told to take back %d runes at %d", log.deln,
log.deloffset);
pthread_mutex_lock(&srv.lock);
srv.del = 0;
pthread_mutex_unlock(&srv.lock);
log.around[0] = '\0';
setpre(&srv, "reset");
clearlog(&log);
Check(key(ctx, win, GDK_KEY_e), "reset setup key was not eaten");

View File

@@ -21,6 +21,8 @@ struct Log
int transfer;
int atext;
int aclear;
int retake;
int retook;
char revent[16];
int rn;
int repeat;
@@ -28,6 +30,12 @@ struct Log
int rcommit;
int rdone;
int flushed;
int hanja;
int required;
int deleted;
int delbefore;
int delcount;
int converted;
};
static void
@@ -140,6 +148,17 @@ commit(IBusInputContext *ctx, IBusText *text, void *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)
@@ -161,6 +180,17 @@ commit(IBusInputContext *ctx, IBusText *text, void *arg)
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;
@@ -171,6 +201,32 @@ commit(IBusInputContext *ctx, IBusText *text, void *arg)
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)
{
@@ -201,7 +257,8 @@ int
main(int argc, char **argv)
{
IBusBus *bus;
IBusInputContext *a, *b;
IBusInputContext *a, *b, *c;
IBusText *around;
Log log;
int ok;
@@ -216,7 +273,7 @@ main(int argc, char **argv)
return 1;
}
memset(&log, 0, sizeof log);
b = NULL;
b = c = NULL;
ibus_init();
bus = ibus_bus_new();
if(bus == NULL || !ibus_bus_is_connected(bus)){
@@ -259,8 +316,9 @@ main(int argc, char **argv)
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);
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);
@@ -272,22 +330,57 @@ main(int argc, char **argv)
log.repeat = 3;
ibus_input_context_focus_out(a);
ok = ok && waitflag(&log, &log.flushed);
if(log.legacy == 0 || log.modern != 0 || log.commit != 3 ||
log.invalid || !log.done || !log.atext || !log.aclear ||
log.clearctx != a || !log.rdone)
/*
* 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 repeat=%s flushed=%d\n",
"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.revent, log.flushed);
log.retook, log.revent, log.flushed, log.required,
log.delbefore, log.delcount, log.converted);
return 1;
}
printf("official libibus client preedit, commit, owner clear, and focus-out hand-back: ok\n");
printf("official libibus client preedit, commit, owner clear, take-back and focus-out hand-back: ok\n");
return 0;
}

View File

@@ -128,6 +128,8 @@ runcontract(char *address)
"SetEngine",
"SetCapabilities",
"SetCursorLocation",
"SetCursorLocationRelative",
"SetSurroundingText",
};
DBusConnection *conn;
char path[96];

View File

@@ -213,8 +213,8 @@ ibus_private_input_policy(struct ct *t)
{
static const struct { u32int purpose; int want; } cases[] = {
{ 0, 0 },
{ Ibuspurposepassword, 1 },
{ Ibuspurposepin, 1 },
{ Purposepassword, 1 },
{ Purposepin, 1 },
{ 37, 0 },
};
Ibusfix f;
@@ -239,7 +239,7 @@ ibus_private_input_policy(struct ct *t)
contextkey(t, &f, ctx, 'n', Testctrlmask);
res = contextkey(t, &f, ctx, 'k', 0);
checkstr(t, "preedit", "k", &res.preedit);
ctx->purpose = Ibuspurposepassword;
ctx->purpose = Purposepassword;
memset(&res, 0, sizeof res);
CT_CHECK(t, !processkey(ctx, 'x', 0, text, sizeof text, &res));
CT_CHECK(t, !res.eaten);

View File

@@ -23,6 +23,7 @@ typedef struct Response Response;
struct Response
{
int eaten;
int del;
char commit[Ipcfieldmax+1];
char preedit[Ipcfieldmax+1];
};
@@ -50,7 +51,8 @@ readresponseuntil(int fd, int want, Response *res, int64_t deadline,
return rv;
}
res->eaten = hdr[0] != 0;
n = getlen(hdr + 1);
res->del = hdr[1];
n = getlen(hdr + 2);
if(n > Ipcfieldmax){
fail("invalid commit length %zu", n);
return -1;
@@ -119,6 +121,18 @@ sendcap(int fd, int cap)
return ipcsend(fd, req, sizeof req) == 0;
}
static int
sendsurround(int fd, char *text)
{
unsigned char req[Ipcreqsz];
size_t n;
n = strlen(text);
ipcpacksurround(req, n);
return ipcsend(fd, req, sizeof req) == 0 &&
(n == 0 || ipcsend(fd, text, n) == 0);
}
static int
expectresponse(int fd, int want, int eaten, char *commit, char *preedit,
char *where)
@@ -152,6 +166,33 @@ requestkey(int fd, int want, uint32_t mod, uint32_t key, int eaten,
return expectresponse(fd, want, eaten, commit, preedit, where);
}
/*
* The Korean habit: 한자 typed, then the Hanja key. The 한 is the
* client's by then, so the daemon must ask for it back and answer with
* the whole word.
*/
static int
requesthanja(int fd)
{
Response res;
if(!sendsurround(fd, "저는 한"))
return fail("send surrounding text: %s", strerror(errno));
if(!requestkey(fd, 1, Mctrl, 's', 1, "", "", "select Korean") ||
!requestkey(fd, 1, 0, 'w', 1, "", "", "jamo") ||
!requestkey(fd, 1, 0, 'k', 1, "", "", "syllable") ||
!requestkey(fd, 1, Mctrl, 'h', 1, "", "", "Hanja search"))
return 0;
if(!sendkey(fd, 1, 0, Kret))
return fail("send Hanja pick: %s", strerror(errno));
if(readresponseuntil(fd, 1, &res, nowms() + Calltimeout, 0) != 1)
return 0;
if(strcmp(res.commit, "漢字") != 0 || res.del != 1)
return fail("Hanja pick committed %s and took back %d",
res.commit, res.del);
return 1;
}
static int
tryclient(char *path, int64_t deadline, int *client)
{
@@ -258,7 +299,8 @@ runsmoke(char *path)
requestkey(client, 1, Mctrl, 'n', 1, "", "",
"select Japanese") &&
requestkey(client, 1, 0, 'k', 1, "", "k", "preedit") &&
requestreset(client, 1, 1, "k", "", "reset");
requestreset(client, 1, 1, "k", "", "reset") &&
requesthanja(client);
close(client);
return ok;
}

View File

@@ -177,34 +177,37 @@ ipc_runtime_path(struct ct *t)
void
ipc_response_pack_boundaries(struct ct *t)
{
static const uchar withpre[] = { 1, 1, 0, 'A', 2, 0, 'x', 'y' };
static const uchar withoutpre[] = { 1, 1, 0, 'A' };
static const uchar withpre[] = { 1, 3, 1, 0, 'A', 2, 0, 'x', 'y' };
static const uchar withoutpre[] = { 1, 0, 1, 0, 'A' };
uchar out[Ipcmaxresp+1], field[Ipcfieldmax];
int n;
n = ipcpackresp(out, sizeof out, 7, "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_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_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);
memset(field, 'x', sizeof field);
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);
CT_EQ_INT(t, Ipcmaxresp, n);
CT_EQ_INT(t, 0, out[1]);
CT_EQ_INT(t, 1, out[2]);
CT_EQ_INT(t, 0, out[3+Ipcfieldmax]);
CT_EQ_INT(t, 1, out[4+Ipcfieldmax]);
CT_EQ_INT(t, 0, out[2]);
CT_EQ_INT(t, 1, out[3]);
CT_EQ_INT(t, 0, out[4+Ipcfieldmax]);
CT_EQ_INT(t, 1, out[5+Ipcfieldmax]);
CT_EQ_INT(t, 0xa5, out[Ipcmaxresp]);
CT_EQ_INT(t, -1, ipcpackresp(out, Ipcmaxresp-1, 1,
CT_EQ_INT(t, -1, ipcpackresp(out, Ipcmaxresp-1, 1, 0,
(char*)field, sizeof field, (char*)field, sizeof field, 1));
CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0,
CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0, 0,
(char*)field, Ipcfieldmax+1, "", 0, 0));
/* A take-back is a rune count and never leaves its byte. */
CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0, -1, "", 0, "", 0, 0));
CT_EQ_INT(t, -1, ipcpackresp(out, sizeof out, 0, 256, "", 0, "", 0, 0));
}
void
@@ -215,8 +218,8 @@ ipc_response_empty_and_preedit(struct ct *t)
Ipcresp resp;
int fd[2], nfirst, nsecond;
nfirst = ipcpackresp(first, sizeof first, 0, "", 0, "", 0, 0);
nsecond = ipcpackresp(second, sizeof second, 1,
nfirst = ipcpackresp(first, sizeof first, 0, 0, "", 0, "", 0, 0);
nsecond = ipcpackresp(second, sizeof second, 1, 0,
"go", 2, "kana", 4, 1);
if(!CT_CHECK(t, nfirst > 0 && nsecond > 0))
return;
@@ -252,9 +255,9 @@ ipc_response_max_and_drain(struct ct *t)
int fd[2], nfirst, nsecond;
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);
nsecond = ipcpackresp(second, sizeof second, 0, "ok", 2, "", 0, 0);
nsecond = ipcpackresp(second, sizeof second, 0, 0, "ok", 2, "", 0, 0);
if(!CT_CHECK(t, nfirst == Ipcmaxresp && nsecond > 0))
return;
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
@@ -285,7 +288,7 @@ ipc_response_fragmented_and_truncated(struct ct *t)
Ipcresp resp;
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))
return;
if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){

View File

@@ -767,7 +767,8 @@ ipcrequest(int fd, uint32_t mod, uint32_t key, unsigned char *want,
int
ipcprobe(int fd, char *where)
{
unsigned char empty[] = {0, 0, 0, 0, 0};
/* 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);
}

View File

@@ -35,12 +35,11 @@ class MkemojiTest(unittest.TestCase):
result = generate()
self.assertEqual(result.returncode, 0, result.stderr)
data = table(result.stdout)
keys = list(data)
self.assertEqual([k for k in keys if k[:1] == "^"][:9],
[f"^{d}" for d in range(1, 10)])
self.assertEqual([k for k in keys if k[:1] == "_"][:9],
[f"_{d}" for d in range(1, 10)])
self.assertEqual([k for k in keys if k[:1] == "<"][2], "<3")
# The engine walks a prefix's children in rune order, so only the
# 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["^1"], "¹")
self.assertEqual(data["_2"], "")
self.assertEqual(data["<3"].split()[0], "")

View File

@@ -64,6 +64,24 @@ class MkhanjaTest(unittest.TestCase):
"\u91d1\t\uae40",
"\u6f22\u5b57\t\ud55c\uc790"])
def test_imports_symbol_rows_under_a_jamo(self):
result = run(
IMPORT,
self.source(
"\u3141:\u203b:reference mark\n"
"\u3134:\u300c:bracket\n"
"\u3131:\u3000:ideographic space\n"
"\u3131:\u00ad:soft hyphen\n"
"\u3131:\u52a0:Hanja under a jamo\n"
"\u3141:\u203b\u203b:two runes\n"
"\u3141\u3134:\u203b:two jamo\n"
"\u314f:\u203b:a vowel\n"
),
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout.splitlines(),
["\u203b\t\u3141", "\u300c\t\u3134"])
def test_import_rejects_malformed_and_duplicate_rows(self):
bad = [
"한 漢\n",
@@ -110,6 +128,19 @@ class MkhanjaTest(unittest.TestCase):
],
)
def test_groups_symbols_under_their_jamo(self):
result = run(
GENERATE,
self.source(
"\t\n"
"\t\n"
"\t\n"
),
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout.splitlines(),
["\t※ ○", "\t"])
def test_generator_rejects_bad_rows(self):
bad = [
"漢a\t\n",
@@ -117,6 +148,12 @@ class MkhanjaTest(unittest.TestCase):
"𠀀\t\n",
"\t\n\t\n",
"漢 한\n",
"\t\n",
"\t\n",
"\tㅁㄴ\n",
"※※\t\n",
"\u3000\t\n",
"\u00ad\t\n",
]
for text in bad:
with self.subTest(text=repr(text)):
@@ -131,6 +168,15 @@ class MkhanjaTest(unittest.TestCase):
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("\t", result.stdout)
def test_generator_reads_both_sources(self):
result = run(GENERATE)
self.assertEqual(result.returncode, 0, result.stderr)
lines = result.stdout.splitlines()
self.assertEqual(lines.count(";; All rights reserved."), 2)
rows = dict(line.split("\t") for line in lines if "\t" in line)
self.assertIn("", rows[""].split())
self.assertIn("", rows[""].split())
def test_generator_keeps_runtime_candidate_limit(self):
rows = "".join(f"{chr(0x4E00 + n)}\t\n" for n in range(129))
result = run(GENERATE, self.source(rows))

View File

@@ -86,6 +86,8 @@ void engine_emoji_preedit_languages(struct ct*);
void engine_emoji_start_and_unknown(struct ct*);
void engine_hanja_search(struct ct*);
void engine_hanja_unknown_and_cancel(struct ct*);
void engine_hanja_reaches_back(struct ct*);
void engine_hanja_word_prefix(struct ct*);
void engine_hanja_korean_keys(struct ct*);
void engine_hanja_backspace(struct ct*);
void engine_hanja_input_languages(struct ct*);
@@ -125,3 +127,7 @@ 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

@@ -107,6 +107,8 @@ static const struct ct_test tests[] = {
{ "engine/emoji-start-and-unknown", engine_emoji_start_and_unknown },
{ "engine/hanja-search", engine_hanja_search },
{ "engine/hanja-unknown-cancel", engine_hanja_unknown_and_cancel },
{ "engine/hanja-reaches-back", engine_hanja_reaches_back },
{ "engine/hanja-word-prefix", engine_hanja_word_prefix },
{ "engine/hanja-korean-keys", engine_hanja_korean_keys },
{ "engine/hanja-backspace", engine_hanja_backspace },
{ "engine/hanja-input-languages", engine_hanja_input_languages },
@@ -145,6 +147,10 @@ static const struct ct_test tests[] = {
{ "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[] = {

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

View File

@@ -129,10 +129,10 @@ pumpinput(Display *dpy, XIC ic, char *commit, size_t cap, int timeout)
continue;
n = Xutf8LookupString(ic, &ev.xkey, buf, sizeof buf - 1,
&sym, &status);
if(n < 0 || ((status == XLookupChars || status == XLookupBoth) &&
if(n < 0 || (status == XLookupChars &&
((size_t)n >= cap || commit == NULL)))
return 0;
if((status == XLookupChars || status == XLookupBoth) && n != 0){
if(status == XLookupChars && n != 0){
memcpy(commit, buf, n);
commit[n] = '\0';
}
@@ -273,7 +273,7 @@ sendkey(Display *dpy, XIC ic, Window win, KeySym sym, unsigned int state,
if(n < 0)
return fail("Xutf8LookupString failed for keysym %#lx",
(unsigned long)sym);
if((status == XLookupChars || status == XLookupBoth) && n != 0){
if(status == XLookupChars && n != 0){
if((size_t)n >= cap)
return fail("commit buffer overflow");
memcpy(commit, buf, n);
@@ -405,6 +405,9 @@ testcallbacks(Display *dpy, XIM im, Window win)
/*
* 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)
@@ -419,8 +422,11 @@ popupabove(Display *dpy, Window client)
for(;;){
above = 0;
root = DefaultRootWindow(dpy);
if(!XQueryTree(dpy, root, &root, &parent, &kids, &n))
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;
@@ -429,6 +435,7 @@ popupabove(Display *dpy, Window client)
break;
}
XFree(kids);
XUngrabServer(dpy);
if(i < n)
return above || fail("popup is stacked below the client");
if(leftms(deadline) == 0)

4
win.c
View File

@@ -197,11 +197,7 @@ wininit(void)
xcb_randr_query_version_cookie_t rc;
xcb_randr_query_version_reply_t *rr;
const xcb_query_extension_reply_t *rext;
char *scale;
scale = getenv("GDK_SCALE");
if(scale != nil && atoi(scale) > 1)
popupscale = min(atoi(scale), 4);
conn = xcb_connect(nil, &n);
if(xcb_connection_has_error(conn)){
fprint(2, "strans: popup disabled: cannot connect to X display\n");

800
wl.c Normal file
View File

@@ -0,0 +1,800 @@
#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)
{
char utf[Maxutf];
Str s;
int i;
sclear(&s);
for(i = max(surround.n - n, 0); i < surround.n; i++)
sputr(&s, surround.r[i]);
return stoutf(&s, utf, sizeof utf);
}
/* 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");
}

3
xim.c
View File

@@ -384,7 +384,7 @@ kpress(Ic *state, xcb_key_press_event_t *ev)
int n;
sym = keymaplookup(kstate, ev->detail, ev->state);
if(composekey(sym, text, sizeof text))
if(composekey(Composexim, state, sym, text, sizeof text))
return;
key = ipckeysym(sym, xkb_keysym_to_utf32(sym));
if(keymeaningful(key))
@@ -424,6 +424,7 @@ icfree(void *p)
state = p;
/* The owner remains valid until imthread acknowledges its release. */
release(state);
composedrop(Composexim, state);
icunlink(state);
free(state);
}