Compare commits
108 Commits
ea363ca792
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f77e0e9ac | |||
| 761428dbc4 | |||
| 1bc0ba0491 | |||
| 74607afef8 | |||
| 591029237e | |||
| bdf05e3c4a | |||
| 4802b6593e | |||
| ba0a08abe2 | |||
| 08d0e0fec8 | |||
| d3d46c8cb1 | |||
| b08f3bb7a2 | |||
| cf0eba6642 | |||
| 5d9a2a614e | |||
| 921e632eae | |||
| 910bf51347 | |||
| f7277f54a3 | |||
| 997e4c8d93 | |||
| dab80b3a77 | |||
| d9e27e8b01 | |||
| 5abd885e11 | |||
| a351f50308 | |||
| c9c01fdeb9 | |||
| bfcd9f6786 | |||
| 3f8f51adf4 | |||
| 8ebe6b32df | |||
| b61c04b280 | |||
| dadda19b3a | |||
| 62ddc2177b | |||
| 82078a60e7 | |||
| 46ab39ab95 | |||
| 65066eef53 | |||
| 0f128a46b3 | |||
| 650f00812d | |||
| e64e4ea6af | |||
| 867c6ebe40 | |||
| 8a08efe2bb | |||
| 5ff54237ff | |||
| bbc0c97656 | |||
| aef577026d | |||
| 8472474332 | |||
| 33710178d4 | |||
| 5cb9f69590 | |||
| 6d511919e8 | |||
| 40fd4ab551 | |||
| 841d93ac35 | |||
| f6dd056ffb | |||
| ad2f2fc5eb | |||
| 2e1e0b9645 | |||
| 5664a008de | |||
| 4ac0625c6b | |||
| 233884e51f | |||
| 27180bc0fe | |||
| f6477cb97d | |||
| 059e8bbc75 | |||
| e93ec43706 | |||
| 544ed835b7 | |||
| ae7dbae993 | |||
| 2e53627b7d | |||
| 3527bcd489 | |||
| 6a6749e824 | |||
| c2aec44aa9 | |||
| cf8eca2ff1 | |||
| d129f406d5 | |||
| e54783dc92 | |||
| 3a90826238 | |||
| 0ba7dc7f50 | |||
| ef7fb627c6 | |||
| a0e83f98c6 | |||
| 34c0c501aa | |||
| 0f684ae6d3 | |||
| c102c87d55 | |||
| bfa919f623 | |||
| 5a6f352d72 | |||
| 88ad5f7630 | |||
| 33023d7f51 | |||
| 46bb8b1d46 | |||
| 82cb07eba2 | |||
| b979afc3f7 | |||
| dd389edc4f | |||
| 268b687ee6 | |||
| e32cea3296 | |||
| a03fb05324 | |||
| 42568ab020 | |||
| 7c9e736996 | |||
| 0cb395579f | |||
| 33848709a5 | |||
| 47f9801ad8 | |||
| fcdce7dc16 | |||
| 0c4271b8b5 | |||
| bab40c95a7 | |||
| 87cb03fdf8 | |||
| cd637be4ca | |||
| 5faefd9a87 | |||
| 3d862bdc0d | |||
| 97a838eda1 | |||
| 1c8d9fac07 | |||
| 97e6f2cf1a | |||
| 852e129f62 | |||
| 1285476b41 | |||
| f0498868eb | |||
| ab0366b7f7 | |||
| f61f7ab5f8 | |||
| 95dff0b82b | |||
| abff5ed122 | |||
| 221117f93d | |||
| b77400fa86 | |||
| 63fc60dc06 | |||
| e8519b4e06 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,5 +1,9 @@
|
||||
/*.o
|
||||
/strans
|
||||
/imv2.c
|
||||
/imv2.h
|
||||
/vkv1.c
|
||||
/vkv1.h
|
||||
/tests/*.o
|
||||
/tests/unit_test
|
||||
/tests/stress_test
|
||||
@@ -11,8 +15,7 @@
|
||||
/tests/daemon_collision_test
|
||||
/tests/daemon_failure_test
|
||||
/tests/daemon_restart_test
|
||||
/xim/*.o
|
||||
/xim/xim_test
|
||||
/gtk/im-strans.so
|
||||
/bench/bench
|
||||
/bench/perf.data*
|
||||
/ref-*/
|
||||
|
||||
@@ -20,9 +20,11 @@ RUN pacman -Syu --noconfirm --needed \
|
||||
ttf-dejavu \
|
||||
ttf-jigmo \
|
||||
valgrind \
|
||||
wayland \
|
||||
which \
|
||||
xorg-server-xvfb \
|
||||
xcb-imdkit \
|
||||
xcb-util \
|
||||
dbus \
|
||||
&& pacman -Scc --noconfirm
|
||||
|
||||
@@ -35,10 +37,14 @@ RUN export DEBUGINFOD_URLS=https://debuginfod.archlinux.org \
|
||||
"$debugdir/${buildid#??}.debug" \
|
||||
&& rm -rf /root/.cache/debuginfod_client
|
||||
|
||||
RUN printf "egrep='grep -E'\n" > /usr/lib/plan9/config
|
||||
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
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
# 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.
|
||||
- `Unicode-3.0.txt` contains the Unicode License v3, the terms for the
|
||||
emoji names and keywords in `map/emoji.src` and generated
|
||||
`map/emoji.dict`, derived from Unicode's `emoji-test.txt` and CLDR's
|
||||
annotations.
|
||||
- The two protocol descriptions in `proto/` are vendored verbatim from
|
||||
wlroots `012ca825` and carry their own MIT terms inside the XML.
|
||||
|
||||
No repository-wide license has been declared for original strans source.
|
||||
|
||||
39
LICENSES/Unicode-3.0.txt
Normal file
39
LICENSES/Unicode-3.0.txt
Normal file
@@ -0,0 +1,39 @@
|
||||
UNICODE LICENSE V3
|
||||
|
||||
COPYRIGHT AND PERMISSION NOTICE
|
||||
|
||||
Copyright © 1991-2026 Unicode, Inc.
|
||||
|
||||
NOTICE TO USER: Carefully read the following legal agreement. BY
|
||||
DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
|
||||
SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
|
||||
TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
|
||||
DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of data files and any associated documentation (the "Data Files") or
|
||||
software and any associated documentation (the "Software") to deal in the
|
||||
Data Files or Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, and/or sell
|
||||
copies of the Data Files or Software, and to permit persons to whom the
|
||||
Data Files or Software are furnished to do so, provided that either (a)
|
||||
this copyright and permission notice appear with all copies of the Data
|
||||
Files or Software, or (b) this copyright and permission notice appear in
|
||||
associated Documentation.
|
||||
|
||||
THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
||||
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
|
||||
THIRD PARTY RIGHTS.
|
||||
|
||||
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
|
||||
BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
|
||||
OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA
|
||||
FILES OR SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the name of a copyright holder shall
|
||||
not be used in advertising or otherwise to promote the sale, use or other
|
||||
dealings in these Data Files or Software without prior written
|
||||
authorization of the copyright holder.
|
||||
58
Makefile
58
Makefile
@@ -1,46 +1,61 @@
|
||||
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)
|
||||
IBUS_LIBS = $(shell $(PKG_CONFIG) --libs dbus-1 xkbcommon)
|
||||
XIM_CFLAGS = $(shell $(PKG_CONFIG) --cflags xcb-imdkit xcb-xkb xkbcommon-x11)
|
||||
XIM_LIBS = $(shell $(PKG_CONFIG) --libs xcb-imdkit xcb-xkb xkbcommon-x11)
|
||||
XIM_CFLAGS = $(shell $(PKG_CONFIG) --cflags xcb-imdkit xcb-aux xcb-xkb xkbcommon-x11)
|
||||
XIM_LIBS = $(shell $(PKG_CONFIG) --libs xcb-imdkit xcb-aux xcb-xkb xkbcommon-x11)
|
||||
TEXT_CFLAGS = $(shell $(PKG_CONFIG) --cflags pangocairo cairo fontconfig)
|
||||
TEXT_LIBS = $(shell $(PKG_CONFIG) --libs pangocairo cairo fontconfig)
|
||||
POPUP_CFLAGS = $(shell $(PKG_CONFIG) --cflags xcb-randr)
|
||||
POPUP_LIBS = $(shell $(PKG_CONFIG) --libs xcb-randr)
|
||||
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 = 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
|
||||
XIMSRCS = xim/xim.c xim/keymap.c
|
||||
XIMOBJS = $(XIMSRCS:.c=.o)
|
||||
OBJS = $(SRCS:.c=.o) $(XIMOBJS)
|
||||
SRCS = compose.c dict.c font.c ibus.c ipc.c ko.c main.c popup_layout.c \
|
||||
srv.c str.c strans.c trie.c vi.c win.c wl.c xim.c
|
||||
OBJS = $(SRCS:.c=.o)
|
||||
# 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
|
||||
ibus.o: PROJECT_CPPFLAGS += $(IBUS_CFLAGS)
|
||||
wl.o: imv2.h vkv1.h
|
||||
compose.o ibus.o: PROJECT_CPPFLAGS += $(IBUS_CFLAGS)
|
||||
font.o: PROJECT_CPPFLAGS += $(TEXT_CFLAGS)
|
||||
win.o: PROJECT_CPPFLAGS += $(POPUP_CFLAGS)
|
||||
$(XIMOBJS): PROJECT_CPPFLAGS += -I. $(XIM_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
|
||||
@@ -89,13 +104,14 @@ check-stress: $(PROG)
|
||||
test: check
|
||||
|
||||
verify-map:
|
||||
test "$$(cut -f1 map/hira.map)" = "$$(cut -f1 map/kata.map)"
|
||||
python3 map/mktelex.py | cmp - map/telex.map
|
||||
python3 -B map/mkemoji | cmp - map/emoji.dict
|
||||
python3 -B map/mkhanja | cmp - map/hanja.dict
|
||||
python3 -B map/verifymap.py map/*.map map/*.dict
|
||||
python3 -B tests/mkemoji_test.py
|
||||
python3 -B tests/mkhanja_test.py
|
||||
python3 -B tests/skk2ktrans_test.py
|
||||
python3 map/mkemoji | cmp - map/emoji.dict
|
||||
python3 map/mkhanja | cmp - map/hanja.dict
|
||||
python3 map/verifymap.py map/*.map map/*.dict
|
||||
python3 tests/mkemoji_test.py
|
||||
python3 tests/mkhanja_test.py
|
||||
python3 tests/skk2ktrans_test.py
|
||||
|
||||
.PHONY: all check check-live check-stress test verify-map clean gtk bench \
|
||||
docker docker-image docker-build docker-check docker-check-live \
|
||||
|
||||
258
README.md
258
README.md
@@ -1,186 +1,194 @@
|
||||
# strans
|
||||
|
||||
strans is a small, single-user input method for Korean, Japanese, English,
|
||||
emoji, and symbols. It provides one engine for IBus, XIM, and GTK 3.
|
||||
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 |
|
||||
|
||||
Use `Up`/`Down` to move through candidates and `PageUp`/`PageDown` to move by
|
||||
nine without wrapping. An unmodified `1`-`9` selects that row on the current
|
||||
page. `Enter` and `Tab` commit a candidate you have moved to; otherwise they
|
||||
commit the reading, as `0` always does. Japanese consumes the confirming key;
|
||||
Korean and Vietnamese pass it on to the application. In a temporary Emoji or
|
||||
Hanja search, `Enter` commits the highlighted result and `Tab`/`Shift-Tab`
|
||||
wrap through the results. Leaving the field or clicking elsewhere commits
|
||||
what is pending; `Esc` cancels it.
|
||||
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.
|
||||
|
||||
Hiragana mode composes a complete reading before offering Kanji candidates.
|
||||
Katakana mode does not perform Kanji conversion. Emoji and Hanja searches
|
||||
return to the previous mode after use; switching language during a search
|
||||
commits the shown query. `Ctrl+H` takes the syllable being composed as its
|
||||
query; the Hanja dictionary lists one modern Hangul syllable at a time.
|
||||
## Composing
|
||||
|
||||
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. A chosen candidate takes the reading's place in the preedit, so
|
||||
it stands where it will land, and `Enter` commits it, or the reading, as
|
||||
`0` and typing on always do. Once a candidate is chosen `1`-`9` take that
|
||||
row of the page shown; before that the digits type. `Backspace` deletes
|
||||
the last kana shown, and romaji that made no kana stays as typed until it
|
||||
is mended; `Esc` cancels the reading, and on a chosen candidate both go
|
||||
back to it. The dictionary keys verbs and adjectives by stem and
|
||||
okurigana, so `kaku` offers かく's readings and then 書く and its kin —
|
||||
typing the okurigana with `Shift`, as SKK does, `kaKu`, asks for that
|
||||
split first. Katakana mode does not convert, so there `Space` and `Tab`
|
||||
commit the reading and go on to the application.
|
||||
|
||||
Korean composes one syllable at a time. `Enter`, `Tab`, `Esc` and any key
|
||||
that is not a jamo commit the syllable and go on to the application, so
|
||||
`Esc` still leaves insert mode. Two lone consonants join into the compound
|
||||
final they make (rt → ㄳ), and a vowel typed before its consonant is
|
||||
reordered under it (kr → 가), as in libhangul.
|
||||
|
||||
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. `Backspace`
|
||||
gives that reach back before it deletes what was typed, so 상 written
|
||||
with 태 typed answers 狀態, and one `Backspace` answers 態 for the
|
||||
syllable alone. A reading is also the start of the longer words it
|
||||
begins, whose conversions follow its own, so 대한 answers 大寒 first and
|
||||
大韓民國 further down. A lone consonant is a reading too, and answers
|
||||
with the symbol table a Korean keyboard's 한자 key has always offered:
|
||||
ㅁ gives ※ ○ △ ㈜, ㄴ the brackets 「」『』, ㄹ the units ℃ ㎏ ℓ, ㅇ the
|
||||
circled numbers ①②③.
|
||||
|
||||
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 |
|
||||
|
||||
XIM text travels as `COMPOUND_TEXT`, which carries any UTF-8. A client that
|
||||
sends `XNSpotLocation` gets the popup at that spot whatever its preedit
|
||||
style; 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 use the pinned Docker environment
|
||||
for a reproducible build:
|
||||
|
||||
```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, pkg-config, Python 3,
|
||||
and cmp/diff, plus development files for:
|
||||
|
||||
- Plan 9 port (`9c`, `9l`, `libthread`, and `libbio`);
|
||||
- D-Bus development files;
|
||||
- XCB, XCB RandR, XCB XKB, 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;
|
||||
- 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; no font
|
||||
file argument or system-wide strans installation is required. 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.
|
||||
`./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.
|
||||
|
||||
Pango shapes the complete string and selects glyph fallback from the system
|
||||
font set.
|
||||
|
||||
The popup renderer uses Pango/PangoCairo and Cairo. When `DISPLAY` is set,
|
||||
`strans` starts its XIM worker in the same process.
|
||||
|
||||
Configure clients as needed:
|
||||
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 an
|
||||
already-built, up-to-date module from this build tree does not: the install
|
||||
target first uses an explicit `GTK_MODULE_DIR`, then GTK's pkg-config
|
||||
variables, and finally the module directory reported by the GTK runtime's
|
||||
`gtk-query-immodules-3.0`. Packagers and cross builds should set
|
||||
`GTK_MODULE_DIR` explicitly. 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. 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 dictionaries:
|
||||
After changing an input source, regenerate and verify:
|
||||
|
||||
```sh
|
||||
python3 map/mktelex.py >map/telex.map
|
||||
map/mkemoji >map/emoji.dict
|
||||
map/mkhanja >map/hanja.dict
|
||||
make verify-map
|
||||
```
|
||||
|
||||
See [`map/README`](map/README) for Hanja import and Japanese dictionary
|
||||
generation.
|
||||
[`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.
|
||||
|
||||
5
bench.sh
5
bench.sh
@@ -9,6 +9,11 @@ if test "$#" -ne 0; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! test -x ./bench/bench; then
|
||||
echo "bench.sh: ./bench/bench is not executable; run make bench first" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
strans_pid=
|
||||
perf_pid=
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ iterations(char *s)
|
||||
goto Bad;
|
||||
errno = 0;
|
||||
n = strtoumax(s, &end, 10);
|
||||
if(errno != 0 || *end != '\0' || n == 0 || n > UINT64_MAX)
|
||||
if(errno != 0 || *end != '\0' || n == 0)
|
||||
goto Bad;
|
||||
return n;
|
||||
Bad:
|
||||
@@ -157,7 +157,7 @@ main(int argc, char **argv)
|
||||
|
||||
loadkeys(argv[1]);
|
||||
niter = argc == 3 ? iterations(argv[2]) : 1000;
|
||||
if(nkeys > UINT64_MAX || niter > UINT64_MAX / nkeys){
|
||||
if(niter > UINT64_MAX / nkeys){
|
||||
fprintf(stderr, "iteration count overflows key total\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
82
compose.c
Normal file
82
compose.c
Normal file
@@ -0,0 +1,82 @@
|
||||
#include "dat.h"
|
||||
#include "fn.h"
|
||||
|
||||
#include <locale.h>
|
||||
#include <xkbcommon/xkbcommon.h>
|
||||
#include <xkbcommon/xkbcommon-compose.h>
|
||||
|
||||
/*
|
||||
* One state per frontend, all made before the procs are: xkbcommon locks
|
||||
* nothing, and even its table refcount is a plain increment. cowner is
|
||||
* the context whose sequence a state holds.
|
||||
*/
|
||||
static struct xkb_compose_state *cstate[Ncompose];
|
||||
static void *cowner[Ncompose];
|
||||
|
||||
/* Dead keys and Compose: no frontend's client does them for us. */
|
||||
void
|
||||
composeinit(void)
|
||||
{
|
||||
struct xkb_context *ctx;
|
||||
struct xkb_compose_table *table;
|
||||
char *locale;
|
||||
int i;
|
||||
|
||||
locale = setlocale(LC_CTYPE, "");
|
||||
ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
|
||||
if(ctx == nil)
|
||||
return;
|
||||
table = xkb_compose_table_new_from_locale(ctx,
|
||||
locale != nil ? locale : "C", XKB_COMPOSE_COMPILE_NO_FLAGS);
|
||||
if(table != nil)
|
||||
for(i = 0; i < Ncompose; i++)
|
||||
cstate[i] = xkb_compose_state_new(table,
|
||||
XKB_COMPOSE_STATE_NO_FLAGS);
|
||||
xkb_compose_table_unref(table);
|
||||
xkb_context_unref(ctx);
|
||||
}
|
||||
|
||||
/* The context is going, and the sequence it left half typed with it. */
|
||||
void
|
||||
composedrop(int who, void *owner)
|
||||
{
|
||||
if(cstate[who] == nil || owner != cowner[who])
|
||||
return;
|
||||
xkb_compose_state_reset(cstate[who]);
|
||||
cowner[who] = nil;
|
||||
}
|
||||
|
||||
/*
|
||||
* Feeds sym to the Compose table. Returns 1 while a sequence is
|
||||
* unfinished: the key belongs to the sequence, not to the engine. A
|
||||
* finished sequence leaves its text in buf, which the caller commits
|
||||
* after whatever the engine had pending. A sequence belongs to the
|
||||
* context that started it: a key from another one starts over.
|
||||
*/
|
||||
int
|
||||
composekey(int who, void *owner, u32int sym, char *buf, int n)
|
||||
{
|
||||
struct xkb_compose_state *cs;
|
||||
|
||||
buf[0] = '\0';
|
||||
cs = cstate[who];
|
||||
if(cs == nil)
|
||||
return 0;
|
||||
if(owner != cowner[who]){
|
||||
xkb_compose_state_reset(cs);
|
||||
cowner[who] = owner;
|
||||
}
|
||||
if(xkb_compose_state_feed(cs, sym) != XKB_COMPOSE_FEED_ACCEPTED)
|
||||
return 0;
|
||||
switch(xkb_compose_state_get_status(cs)){
|
||||
case XKB_COMPOSE_COMPOSING:
|
||||
case XKB_COMPOSE_CANCELLED:
|
||||
return 1;
|
||||
case XKB_COMPOSE_COMPOSED:
|
||||
xkb_compose_state_get_utf8(cs, buf, n);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
39
dat.h
39
dat.h
@@ -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
|
||||
{
|
||||
@@ -25,9 +32,15 @@ enum
|
||||
enum
|
||||
{
|
||||
Maxclients = 64,
|
||||
Ownerpoll = 200, /* ms between a frontend's owner checks while its preedit shows */
|
||||
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,
|
||||
@@ -35,15 +48,24 @@ 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)
|
||||
#define PopupPad (4*popupscale)
|
||||
#define PopupSep popupscale
|
||||
#define PopupBorder popupscale
|
||||
#define PopupNumw Fontsz
|
||||
#define PopupTextw (12*Fontsz)
|
||||
#define PopupBasew (2*PopupPad + PopupNumw + PopupTextw)
|
||||
#define Imgh (2*PopupPad + (Maxdisp + 2)*Fontsz + PopupSep)
|
||||
|
||||
typedef struct Str Str;
|
||||
struct Str
|
||||
@@ -73,7 +95,7 @@ struct Tnode
|
||||
int vlen;
|
||||
int child;
|
||||
int sibling;
|
||||
char c;
|
||||
Rune c;
|
||||
};
|
||||
|
||||
typedef struct Trie Trie;
|
||||
@@ -82,6 +104,9 @@ struct Trie
|
||||
Tnode *nodes;
|
||||
int n;
|
||||
int cap;
|
||||
/* the last key put and its path, so a sorted file walks it once */
|
||||
Str last;
|
||||
int path[Maxrunes];
|
||||
};
|
||||
|
||||
enum
|
||||
@@ -172,10 +197,6 @@ struct Popup
|
||||
typedef struct Keyreq Keyreq;
|
||||
typedef struct Keyres Keyres;
|
||||
enum
|
||||
{
|
||||
Cclientpreedit = 1<<0,
|
||||
};
|
||||
enum
|
||||
{
|
||||
Keypress,
|
||||
Keyreset,
|
||||
@@ -185,7 +206,8 @@ enum
|
||||
};
|
||||
struct Keyres
|
||||
{
|
||||
int eaten; /* Keycap: caller is active and capability was applied. */
|
||||
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;
|
||||
};
|
||||
@@ -193,11 +215,12 @@ struct Keyres
|
||||
struct Keyreq
|
||||
{
|
||||
void *owner; /* stable until the context's release is acknowledged */
|
||||
int cap;
|
||||
int clientpre; /* the client draws the preedit; the popup does not */
|
||||
int op;
|
||||
u32int ks;
|
||||
u32int mod;
|
||||
Caret caret;
|
||||
Str surround; /* the client's own text just before the cursor */
|
||||
Channel *reply;
|
||||
};
|
||||
|
||||
|
||||
61
dict.c
61
dict.c
@@ -1,36 +1,67 @@
|
||||
#include "dat.h"
|
||||
#include "fn.h"
|
||||
|
||||
/*
|
||||
* Fills out[] with up to max candidates for key: the space-separated words
|
||||
* of the entry, minus the key itself except in the emoji dictionary, where
|
||||
* a query may name its own answer.
|
||||
*/
|
||||
int
|
||||
dictlookup(Lang *l, Str *key, Str *out, int max)
|
||||
/* Appends the space-separated words of a node's entry to out[], up to max. */
|
||||
static int
|
||||
words(Tnode *nd, Str *out, int n, int max)
|
||||
{
|
||||
char *p, *e, *sp;
|
||||
Str tmp;
|
||||
int n, vlen;
|
||||
|
||||
if(l == nil || key->n == 0 ||
|
||||
trielookup(l->dict, key, &p, &vlen) != TrieExact)
|
||||
return 0;
|
||||
n = 0;
|
||||
e = p + vlen;
|
||||
if(nd->val == nil)
|
||||
return n;
|
||||
p = nd->val;
|
||||
e = p + nd->vlen;
|
||||
while(n < max && p < e){
|
||||
while(p < e && *p == ' ')
|
||||
p++;
|
||||
sp = p;
|
||||
while(p < e && *p != ' ')
|
||||
p++;
|
||||
if(sinit(&tmp, sp, p - sp) && tmp.n > 0 &&
|
||||
(l->lang == LangEMOJI || scmp(&tmp, key) != 0))
|
||||
if(sinit(&tmp, sp, p - sp) && tmp.n > 0)
|
||||
out[n++] = tmp;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/* The entries at and below a node, the shortest keys first. */
|
||||
static int
|
||||
below(Trie *t, int ni, Str *out, int n, int max)
|
||||
{
|
||||
Tnode *nd;
|
||||
int ci;
|
||||
|
||||
nd = &t->nodes[ni];
|
||||
n = words(nd, out, n, max);
|
||||
for(ci = nd->child; ci >= 0 && n < max; ci = t->nodes[ci].sibling)
|
||||
n = below(t, ci, out, n, max);
|
||||
return n;
|
||||
}
|
||||
|
||||
/* Fills out[] with up to max candidates for key: the words of its entry. */
|
||||
int
|
||||
dictlookup(Trie *t, Str *key, Str *out, int max)
|
||||
{
|
||||
int ni;
|
||||
|
||||
ni = key->n == 0 ? -1 : trienode(t, key);
|
||||
if(ni < 0)
|
||||
return 0;
|
||||
return words(&t->nodes[ni], out, 0, max);
|
||||
}
|
||||
|
||||
/* As dictlookup, for every entry key is a prefix of, key's own first. */
|
||||
int
|
||||
dictprefix(Trie *t, Str *key, Str *out, int max)
|
||||
{
|
||||
int ni;
|
||||
|
||||
ni = key->n == 0 ? -1 : trienode(t, key);
|
||||
if(ni < 0)
|
||||
return 0;
|
||||
return below(t, ni, out, 0, max);
|
||||
}
|
||||
|
||||
static Trie*
|
||||
langopen(char *dir, char *name, char *ext)
|
||||
{
|
||||
|
||||
15
fn.h
15
fn.h
@@ -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*);
|
||||
@@ -15,12 +16,14 @@ Trie* trienew(void);
|
||||
void trieput(Trie*, char*, int, char*, int);
|
||||
Trie* trieopen(char*);
|
||||
void trieclose(Trie*);
|
||||
int trienode(Trie*, Str*);
|
||||
int trielookup(Trie*, Str*, char**, int*);
|
||||
|
||||
Lang* getlang(int);
|
||||
void langinit(char*);
|
||||
|
||||
int dictlookup(Lang*, Str*, Str*, int);
|
||||
int dictlookup(Trie*, Str*, Str*, int);
|
||||
int dictprefix(Trie*, Str*, Str*, int);
|
||||
|
||||
void drawthread(void*);
|
||||
void popuparea(Area*, int, Area*, int, int, Area*);
|
||||
@@ -30,7 +33,7 @@ void popuplayout(Drawcmd*, int, int, Popup*);
|
||||
void popupdraw(u32int*, Drawcmd*, Popup*);
|
||||
void imthread(void*);
|
||||
void impre(Im*, Str*);
|
||||
void transstr(Lang*, Str*, Str*);
|
||||
void transstr(Lang*, Str*, Str*, Str*);
|
||||
Emit transmap(Im*, Rune);
|
||||
Emit transko(Im*, Rune);
|
||||
Emit transvi(Im*, Rune);
|
||||
@@ -41,12 +44,18 @@ void srvinit(void);
|
||||
void srvthread(void*);
|
||||
void ibusthread(void*);
|
||||
void ximthread(void*);
|
||||
int wlinit(void);
|
||||
void wlthread(void*);
|
||||
int keymeaningful(u32int);
|
||||
|
||||
void composeinit(void);
|
||||
void composedrop(int, void*);
|
||||
int composekey(int, void*, u32int, char*, int);
|
||||
|
||||
void* emalloc(ulong);
|
||||
void* erealloc(void*, ulong);
|
||||
|
||||
int textinit(void);
|
||||
void textinit(void);
|
||||
void textclose(void);
|
||||
int textwidth(Str*);
|
||||
void textdraw(u32int*, int, int, int, int, int, u32int, Str*);
|
||||
|
||||
40
font.c
40
font.c
@@ -7,13 +7,6 @@ static PangoFontMap *fontmap;
|
||||
static PangoContext *context;
|
||||
static PangoLayout *layout;
|
||||
|
||||
static void
|
||||
resetlayout(void)
|
||||
{
|
||||
pango_layout_set_width(layout, -1);
|
||||
pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_NONE);
|
||||
}
|
||||
|
||||
void
|
||||
textclose(void)
|
||||
{
|
||||
@@ -50,28 +43,16 @@ setfont(void)
|
||||
pango_font_description_free(font);
|
||||
}
|
||||
|
||||
int
|
||||
void
|
||||
textinit(void)
|
||||
{
|
||||
textclose();
|
||||
FcInit();
|
||||
fontmap = pango_cairo_font_map_new();
|
||||
if(fontmap == nil){
|
||||
fprint(2, "strans: popup: can't initialize PangoCairo\n");
|
||||
return 0;
|
||||
}
|
||||
pango_cairo_font_map_set_resolution(PANGO_CAIRO_FONT_MAP(fontmap), 96);
|
||||
context = pango_font_map_create_context(fontmap);
|
||||
if(context != nil)
|
||||
layout = pango_layout_new(context);
|
||||
if(context == nil || layout == nil){
|
||||
fprint(2, "strans: popup: can't create text layout\n");
|
||||
textclose();
|
||||
return 0;
|
||||
}
|
||||
layout = pango_layout_new(context);
|
||||
pango_layout_set_single_paragraph_mode(layout, TRUE);
|
||||
setfont();
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
@@ -80,8 +61,6 @@ settext(Str *s)
|
||||
char utf[Maxutf];
|
||||
int n;
|
||||
|
||||
if(layout == nil || s == nil || s->n <= 0)
|
||||
return 0;
|
||||
n = stoutf(s, utf, sizeof utf);
|
||||
pango_layout_set_text(layout, utf, n);
|
||||
return n > 0;
|
||||
@@ -109,9 +88,8 @@ textwidth(Str *s)
|
||||
{
|
||||
PangoRectangle r;
|
||||
|
||||
if(layout == nil)
|
||||
return 0;
|
||||
resetlayout();
|
||||
pango_layout_set_width(layout, -1);
|
||||
pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_NONE);
|
||||
if(!settext(s))
|
||||
return 0;
|
||||
textextents(&r);
|
||||
@@ -130,20 +108,16 @@ textdraw(u32int *buf, int w, int h, int x, int y, int fit, u32int color,
|
||||
PangoRectangle r;
|
||||
cairo_surface_t *surface;
|
||||
cairo_t *cr;
|
||||
int b, g, red, stride;
|
||||
int b, g, red;
|
||||
|
||||
if(layout == nil || fit <= 0)
|
||||
if(fit <= 0)
|
||||
return;
|
||||
resetlayout();
|
||||
pango_layout_set_width(layout, fit * PANGO_SCALE);
|
||||
pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END);
|
||||
if(w <= 0 || h <= 0 || !settext(s))
|
||||
return;
|
||||
stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, w);
|
||||
if(stride < 0 || (vlong)stride != (vlong)w * sizeof buf[0])
|
||||
return;
|
||||
surface = cairo_image_surface_create_for_data((uchar*)buf,
|
||||
CAIRO_FORMAT_RGB24, w, h, stride);
|
||||
CAIRO_FORMAT_RGB24, w, h, w * sizeof buf[0]);
|
||||
if(cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS){
|
||||
cairo_surface_destroy(surface);
|
||||
return;
|
||||
|
||||
@@ -7,6 +7,7 @@ GTK_QUERY_IMMODULES ?= gtk-query-immodules-3.0
|
||||
GTK_LIBDIR ?= $(shell $(PKG_CONFIG) --variable=libdir gtk+-3.0 2>/dev/null)
|
||||
GTK_BINARY_VERSION ?= $(shell $(PKG_CONFIG) \
|
||||
--variable=gtk_binary_version gtk+-3.0 2>/dev/null)
|
||||
# Without GTK development files, ask the runtime where its modules live.
|
||||
GTK_RUNTIME_MODULE_DIR = $(shell GTK_PATH= GTK_IM_MODULE_FILE= \
|
||||
$(GTK_QUERY_IMMODULES) 2>/dev/null | \
|
||||
awk -F '"' '/^"\// { sub("/[^/]*$$", "", $$2); print $$2; exit }')
|
||||
|
||||
85
gtk/main.c
85
gtk/main.c
@@ -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)
|
||||
{
|
||||
@@ -252,6 +320,7 @@ kpress(GtkIMContext *ctx, GdkEventKey *ev)
|
||||
char pre[Ipcfieldmax+1];
|
||||
uint32_t key, mod;
|
||||
Ipcresp resp;
|
||||
gboolean r;
|
||||
|
||||
im = (Im*)ctx;
|
||||
if(ev->type != GDK_KEY_PRESS){
|
||||
@@ -263,12 +332,17 @@ 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)
|
||||
return simplefilter(ctx, ev, 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)
|
||||
sendreset(im);
|
||||
return r;
|
||||
}
|
||||
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);
|
||||
}
|
||||
@@ -280,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)
|
||||
|
||||
534
ibus.c
534
ibus.c
@@ -2,10 +2,7 @@
|
||||
#include "fn.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <poll.h>
|
||||
#include <dbus/dbus.h>
|
||||
@@ -13,19 +10,32 @@
|
||||
|
||||
enum
|
||||
{
|
||||
Maxconns = Maxclients,
|
||||
Maxwatches = 2*Maxconns + 1,
|
||||
Maxwatches = 2*Maxclients + 1,
|
||||
/* Toolkits keep one context per widget that ever took focus. */
|
||||
Maxcontexts = 1024,
|
||||
Relmask = 1<<30,
|
||||
Ownerpoll = 200, /* ms between owner checks while a preedit shows */
|
||||
/* 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,
|
||||
Maxpost = 3,
|
||||
};
|
||||
|
||||
/*
|
||||
* A client in IBus's synchronous mode (GTK 4) cannot take signals while
|
||||
* it waits for its ProcessKeyEvent reply: it asks afterwards, through the
|
||||
* PostProcessKeyEvent property, for what the key produced, as typed
|
||||
* (yv) pairs — 'c' a commit, 'u' or 'm' a preedit and then its
|
||||
* "cursor,visible[,mode]".
|
||||
*/
|
||||
typedef struct Post Post;
|
||||
struct Post
|
||||
{
|
||||
char type;
|
||||
char text[Maxutf];
|
||||
char pos[32];
|
||||
};
|
||||
|
||||
typedef struct Ictx Ictx;
|
||||
@@ -37,12 +47,17 @@ struct Ictx
|
||||
u32int cap;
|
||||
u32int purpose;
|
||||
int clientcommitpreedit;
|
||||
int postprocess;
|
||||
int keying; /* inside ProcessKeyEvent: hold signals for the post */
|
||||
int npost;
|
||||
Post post[Maxpost];
|
||||
Caret caret;
|
||||
Str surround; /* the client's own text just before the cursor */
|
||||
};
|
||||
|
||||
static DBusWatch *watches[Maxwatches];
|
||||
static int nwatches;
|
||||
static DBusConnection *conns[Maxconns];
|
||||
static DBusConnection *conns[Maxclients];
|
||||
static int nconns;
|
||||
static DBusServer *srv;
|
||||
static Ictx contexts[Maxcontexts];
|
||||
@@ -70,11 +85,10 @@ unlinkaddr(void)
|
||||
}
|
||||
|
||||
static int
|
||||
addrnote(void *v, char *note)
|
||||
addrnote(void*, char *note)
|
||||
{
|
||||
USED(v);
|
||||
USED(note);
|
||||
unlinkaddr();
|
||||
if(notefatal(note))
|
||||
unlinkaddr();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -100,52 +114,63 @@ machineidfiles(char *buf, int sz, char **path, int npath)
|
||||
buf[0] = '\0';
|
||||
}
|
||||
|
||||
/* libibus reads the D-Bus file first and calls it "machine-id" if neither is there. */
|
||||
static void
|
||||
machineid(char *buf, int sz)
|
||||
{
|
||||
char *path[] = {
|
||||
"/etc/machine-id",
|
||||
"/var/lib/dbus/machine-id",
|
||||
"/etc/machine-id",
|
||||
};
|
||||
|
||||
machineidfiles(buf, sz, path, nelem(path));
|
||||
if(buf[0] == '\0')
|
||||
snprintf(buf, sz, "machine-id");
|
||||
}
|
||||
|
||||
static void
|
||||
copyfield(char *dst, int sz, char *src, int n)
|
||||
{
|
||||
if(n >= sz)
|
||||
n = sz - 1;
|
||||
memcpy(dst, src, n);
|
||||
dst[n] = '\0';
|
||||
}
|
||||
|
||||
/*
|
||||
* The display libibus names the file after: the whole WAYLAND_DISPLAY,
|
||||
* else DISPLAY's host and number, else unix and 0.
|
||||
*/
|
||||
static void
|
||||
xdisplay(char *host, int hsz, char *num, int nsz)
|
||||
{
|
||||
char *d, *colon, *dot, *p;
|
||||
int n;
|
||||
|
||||
strncpy(host, "unix", hsz);
|
||||
host[hsz-1] = '\0';
|
||||
strncpy(num, "0", nsz);
|
||||
num[nsz-1] = '\0';
|
||||
copyfield(host, hsz, "unix", 4);
|
||||
copyfield(num, nsz, "0", 1);
|
||||
d = getenv("WAYLAND_DISPLAY");
|
||||
if(d != nil && d[0] != '\0'){
|
||||
copyfield(num, nsz, d, strlen(d));
|
||||
return;
|
||||
}
|
||||
d = getenv("DISPLAY");
|
||||
if(d == nil || d[0] == '\0')
|
||||
return;
|
||||
colon = strchr(d, ':');
|
||||
if(colon == nil)
|
||||
return;
|
||||
if(colon > d){
|
||||
n = colon - d;
|
||||
if(n >= hsz) n = hsz - 1;
|
||||
memcpy(host, d, n);
|
||||
host[n] = '\0';
|
||||
}
|
||||
if(colon > d)
|
||||
copyfield(host, hsz, d, colon - d);
|
||||
p = colon + 1;
|
||||
dot = strchr(p, '.');
|
||||
n = dot ? dot - p : (int)strlen(p);
|
||||
if(n >= nsz) n = nsz - 1;
|
||||
memcpy(num, p, n);
|
||||
num[n] = '\0';
|
||||
copyfield(num, nsz, p, dot ? dot - p : (int)strlen(p));
|
||||
}
|
||||
|
||||
/* The IBus address file lives where libibus looks: config/ibus/bus/. */
|
||||
static int
|
||||
buildaddrpath(char *buf, int sz)
|
||||
{
|
||||
char mid[64], host[64], num[8], dir[512];
|
||||
char mid[64], host[64], num[64], dir[512];
|
||||
char *cfg, *home, *explicit, *p;
|
||||
int n;
|
||||
|
||||
@@ -155,8 +180,6 @@ buildaddrpath(char *buf, int sz)
|
||||
return explicit[0] == '\0' || n < 0 || n >= sz ? -1 : 0;
|
||||
}
|
||||
machineid(mid, sizeof(mid));
|
||||
if(mid[0] == '\0')
|
||||
return -1;
|
||||
xdisplay(host, sizeof(host), num, sizeof(num));
|
||||
cfg = getenv("XDG_CONFIG_HOME");
|
||||
if(cfg != nil && cfg[0] != '\0')
|
||||
@@ -211,11 +234,10 @@ writeaddr(char *path, char *addr)
|
||||
}
|
||||
|
||||
static dbus_bool_t
|
||||
addwatch(DBusWatch *w, void *_)
|
||||
addwatch(DBusWatch *w, void*)
|
||||
{
|
||||
int i;
|
||||
|
||||
USED(_);
|
||||
for(i = 0; i < nwatches; i++)
|
||||
if(watches[i] == nil){
|
||||
watches[i] = w;
|
||||
@@ -228,11 +250,10 @@ addwatch(DBusWatch *w, void *_)
|
||||
}
|
||||
|
||||
static void
|
||||
removewatch(DBusWatch *w, void *_)
|
||||
removewatch(DBusWatch *w, void*)
|
||||
{
|
||||
int i;
|
||||
|
||||
USED(_);
|
||||
for(i = 0; i < nwatches; i++)
|
||||
if(watches[i] == w){
|
||||
watches[i] = nil;
|
||||
@@ -240,13 +261,6 @@ removewatch(DBusWatch *w, void *_)
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
togglewatch(DBusWatch *w, void *_)
|
||||
{
|
||||
USED(w);
|
||||
USED(_);
|
||||
}
|
||||
|
||||
static Ictx*
|
||||
findcontext(DBusConnection *conn, const char *path)
|
||||
{
|
||||
@@ -287,11 +301,16 @@ sendrequest(Ictx *ctx, int op, u32int ks, u32int mod, Keyres *res)
|
||||
|
||||
memset(&kr, 0, sizeof kr);
|
||||
kr.owner = ctx;
|
||||
kr.cap = clientpreedit(ctx) ? Cclientpreedit : 0;
|
||||
kr.clientpre = clientpreedit(ctx);
|
||||
kr.op = op;
|
||||
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);
|
||||
@@ -300,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
|
||||
@@ -384,16 +403,34 @@ newsignal(const char *path, const char *name)
|
||||
return sig;
|
||||
}
|
||||
|
||||
static int
|
||||
postpush(Ictx *ctx, char type, const char *text, const char *pos)
|
||||
{
|
||||
Post *p;
|
||||
|
||||
if(!ctx->postprocess || !ctx->keying)
|
||||
return 0;
|
||||
if(ctx->npost < Maxpost){
|
||||
p = &ctx->post[ctx->npost++];
|
||||
p->type = type;
|
||||
strcpy(p->text, text);
|
||||
strcpy(p->pos, pos);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void
|
||||
emitcommit(DBusConnection *c, const char *path, const char *text)
|
||||
emitcommit(Ictx *ctx, const char *text)
|
||||
{
|
||||
DBusMessage *sig;
|
||||
DBusMessageIter it;
|
||||
|
||||
sig = newsignal(path, "CommitText");
|
||||
if(postpush(ctx, 'c', text, ""))
|
||||
return;
|
||||
sig = newsignal(ctx->path, "CommitText");
|
||||
dbus_message_iter_init_append(sig, &it);
|
||||
appendibustext(&it, text, 0);
|
||||
dbus_connection_send(c, sig, nil);
|
||||
dbus_connection_send(ctx->conn, sig, nil);
|
||||
dbus_message_unref(sig);
|
||||
}
|
||||
|
||||
@@ -405,27 +442,60 @@ emitpreedit(Ictx *ctx, const char *text)
|
||||
DBusMessageIter it;
|
||||
dbus_uint32_t cursor, mode;
|
||||
dbus_bool_t visible;
|
||||
char pos[32];
|
||||
|
||||
if(text[0] == '\0' && preowner != ctx)
|
||||
return;
|
||||
sig = newsignal(ctx->path, ctx->clientcommitpreedit ?
|
||||
"UpdatePreeditTextWithMode" : "UpdatePreeditText");
|
||||
dbus_message_iter_init_append(sig, &it);
|
||||
appendibustext(&it, text, 1);
|
||||
cursor = utflen((char*)text);
|
||||
visible = text[0] != '\0' ? TRUE : FALSE;
|
||||
dbus_message_iter_append_basic(&it, DBUS_TYPE_UINT32, &cursor);
|
||||
dbus_message_iter_append_basic(&it, DBUS_TYPE_BOOLEAN, &visible);
|
||||
if(ctx->clientcommitpreedit){
|
||||
mode = Ibuspreeditclear;
|
||||
dbus_message_iter_append_basic(&it, DBUS_TYPE_UINT32, &mode);
|
||||
}
|
||||
if(dbus_connection_send(ctx->conn, sig, nil)){
|
||||
if(text[0] != '\0')
|
||||
preowner = ctx;
|
||||
else if(preowner == ctx)
|
||||
preowner = nil;
|
||||
mode = Ibuspreeditclear;
|
||||
if(ctx->clientcommitpreedit)
|
||||
snprintf(pos, sizeof pos, "%u,%u,%u", cursor, visible, mode);
|
||||
else
|
||||
snprintf(pos, sizeof pos, "%u,%u", cursor, visible);
|
||||
if(!postpush(ctx, ctx->clientcommitpreedit ? 'm' : 'u', text, pos)){
|
||||
sig = newsignal(ctx->path, ctx->clientcommitpreedit ?
|
||||
"UpdatePreeditTextWithMode" : "UpdatePreeditText");
|
||||
dbus_message_iter_init_append(sig, &it);
|
||||
appendibustext(&it, text, 1);
|
||||
dbus_message_iter_append_basic(&it, DBUS_TYPE_UINT32, &cursor);
|
||||
dbus_message_iter_append_basic(&it, DBUS_TYPE_BOOLEAN, &visible);
|
||||
if(ctx->clientcommitpreedit)
|
||||
dbus_message_iter_append_basic(&it, DBUS_TYPE_UINT32, &mode);
|
||||
dbus_connection_send(ctx->conn, sig, nil);
|
||||
dbus_message_unref(sig);
|
||||
}
|
||||
if(text[0] != '\0')
|
||||
preowner = ctx;
|
||||
else if(preowner == ctx)
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -456,7 +526,7 @@ flushcontext(Ictx *ctx, int op)
|
||||
emitpreedit(ctx, "");
|
||||
stoutf(&res.commit, commit, sizeof commit);
|
||||
if(commit[0] != '\0')
|
||||
emitcommit(ctx->conn, ctx->path, commit);
|
||||
emitcommit(ctx, commit);
|
||||
}
|
||||
|
||||
/* Nobody is left to hand text to: the context or its connection is gone. */
|
||||
@@ -475,6 +545,7 @@ static void
|
||||
dropcontext(Ictx *ctx)
|
||||
{
|
||||
releasecontext(ctx);
|
||||
composedrop(Composeibus, ctx);
|
||||
if(preowner == ctx)
|
||||
preowner = nil;
|
||||
memset(ctx, 0, sizeof *ctx);
|
||||
@@ -490,13 +561,26 @@ dropconncontexts(DBusConnection *conn)
|
||||
dropcontext(&contexts[i]);
|
||||
}
|
||||
|
||||
/*
|
||||
* A key is proof of focus: some clients never send FocusIn, and the
|
||||
* focus events of two applications can cross.
|
||||
*/
|
||||
static int
|
||||
processkey(Ictx *ctx, u32int sym, u32int state, Keyres *res)
|
||||
processkey(Ictx *ctx, u32int sym, u32int state, char *text, int n, Keyres *res)
|
||||
{
|
||||
if(state & Relmask || !ctx->focused || hidden(ctx))
|
||||
text[0] = '\0';
|
||||
if(state & Relmask || hidden(ctx))
|
||||
return 0;
|
||||
sendrequest(ctx, Keypress, ipckeysym(sym, xkb_keysym_to_utf32(sym)),
|
||||
ipcmod(state), res);
|
||||
ctx->focused = 1;
|
||||
/* An IBus client drops a dead key we do not take: compose it here. */
|
||||
if(composekey(Composeibus, ctx, sym, text, n))
|
||||
return -1;
|
||||
if(text[0] != '\0')
|
||||
/* Composed text follows whatever was pending. */
|
||||
sendrequest(ctx, Keyreset, 0, 0, res);
|
||||
else
|
||||
sendrequest(ctx, Keypress, ipckeysym(sym, xkb_keysym_to_utf32(sym)),
|
||||
ipcmod(state), res);
|
||||
if(preowner != nil && preowner != ctx)
|
||||
checkpreowner();
|
||||
return 1;
|
||||
@@ -600,29 +684,68 @@ handlekey(DBusConnection *c, DBusMessage *m, Ictx *ctx)
|
||||
{
|
||||
dbus_uint32_t sym, code, state;
|
||||
Keyres res;
|
||||
char commit[Maxutf], preedit[Maxutf];
|
||||
int restart;
|
||||
char commit[Maxutf], preedit[Maxutf], text[Maxutf];
|
||||
int restart, rv;
|
||||
|
||||
if(!dbus_message_get_args(m, nil,
|
||||
DBUS_TYPE_UINT32, &sym,
|
||||
DBUS_TYPE_UINT32, &code,
|
||||
DBUS_TYPE_UINT32, &state,
|
||||
DBUS_TYPE_INVALID))
|
||||
return handleerror(c, m, DBUS_ERROR_INVALID_ARGS,
|
||||
"ProcessKeyEvent expects three unsigned integers");
|
||||
if(!processkey(ctx, sym, state, &res))
|
||||
return replybool(c, m, 0);
|
||||
dbus_message_get_args(m, nil, DBUS_TYPE_UINT32, &sym,
|
||||
DBUS_TYPE_UINT32, &code, DBUS_TYPE_UINT32, &state,
|
||||
DBUS_TYPE_INVALID);
|
||||
rv = processkey(ctx, sym, state, text, sizeof text, &res);
|
||||
if(rv <= 0) /* below zero: a sequence in the making */
|
||||
return replybool(c, m, rv < 0);
|
||||
stoutf(&res.commit, commit, sizeof commit);
|
||||
stoutf(&res.preedit, preedit, sizeof preedit);
|
||||
/* A commit ends the preedit around it, so clients place it right. */
|
||||
restart = clientpreedit(ctx) && commit[0] != '\0' && preowner == ctx;
|
||||
ctx->keying = 1;
|
||||
if(restart)
|
||||
emitpreedit(ctx, "");
|
||||
if(res.del > 0)
|
||||
emitdelete(ctx, res.del);
|
||||
if(commit[0] != '\0')
|
||||
emitcommit(c, dbus_message_get_path(m), commit);
|
||||
emitcommit(ctx, commit);
|
||||
if(text[0] != '\0')
|
||||
emitcommit(ctx, text);
|
||||
if(clientpreedit(ctx) && (!restart || preedit[0] != '\0'))
|
||||
emitpreedit(ctx, preedit);
|
||||
return replybool(c, m, res.eaten);
|
||||
ctx->keying = 0;
|
||||
return replybool(c, m, res.eaten || text[0] != '\0');
|
||||
}
|
||||
|
||||
/* Properties.Get PostProcessKeyEvent: what the last key produced. */
|
||||
static DBusHandlerResult
|
||||
handlepost(DBusConnection *c, DBusMessage *m, Ictx *ctx)
|
||||
{
|
||||
DBusMessage *r;
|
||||
DBusMessageIter it, v, a, st;
|
||||
Post *p;
|
||||
int i;
|
||||
|
||||
r = dbus_message_new_method_return(m);
|
||||
if(r == nil)
|
||||
die("ibus: out of memory");
|
||||
dbus_message_iter_init_append(r, &it);
|
||||
dbus_message_iter_open_container(&it, DBUS_TYPE_VARIANT, "a(yv)", &v);
|
||||
dbus_message_iter_open_container(&v, DBUS_TYPE_ARRAY, "(yv)", &a);
|
||||
for(i = 0; i < ctx->npost; i++){
|
||||
p = &ctx->post[i];
|
||||
dbus_message_iter_open_container(&a, DBUS_TYPE_STRUCT, nil, &st);
|
||||
dbus_message_iter_append_basic(&st, DBUS_TYPE_BYTE, &p->type);
|
||||
appendibustext(&st, p->text, p->type != 'c');
|
||||
dbus_message_iter_close_container(&a, &st);
|
||||
if(p->type == 'c')
|
||||
continue;
|
||||
dbus_message_iter_open_container(&a, DBUS_TYPE_STRUCT, nil, &st);
|
||||
dbus_message_iter_append_basic(&st, DBUS_TYPE_BYTE, &p->type);
|
||||
appendibustext(&st, p->pos, 0);
|
||||
dbus_message_iter_close_container(&a, &st);
|
||||
}
|
||||
dbus_message_iter_close_container(&v, &a);
|
||||
dbus_message_iter_close_container(&it, &v);
|
||||
dbus_connection_send(c, r, nil);
|
||||
dbus_message_unref(r);
|
||||
ctx->npost = 0;
|
||||
return DBUS_HANDLER_RESULT_HANDLED;
|
||||
}
|
||||
|
||||
static DBusHandlerResult
|
||||
@@ -646,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);
|
||||
}
|
||||
|
||||
@@ -657,12 +781,11 @@ handlecap(DBusConnection *c, DBusMessage *m, Ictx *ctx)
|
||||
char preedit[Maxutf];
|
||||
u32int old;
|
||||
|
||||
if(!dbus_message_get_args(m, nil, DBUS_TYPE_UINT32, &cap,
|
||||
DBUS_TYPE_INVALID))
|
||||
return handleerror(c, m, DBUS_ERROR_INVALID_ARGS,
|
||||
"SetCapabilities expects one unsigned integer");
|
||||
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)){
|
||||
@@ -676,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)
|
||||
@@ -715,7 +889,7 @@ getcontent(DBusMessageIter *value, u32int *purpose)
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Only ContentType and ClientCommitPreedit are settable; none is readable. */
|
||||
/* ContentType, ClientCommitPreedit, and EffectivePostProcessKeyEvent are settable. */
|
||||
static DBusHandlerResult
|
||||
handlepropertyset(DBusConnection *c, DBusMessage *m, Ictx *ctx)
|
||||
{
|
||||
@@ -741,19 +915,23 @@ handlepropertyset(DBusConnection *c, DBusMessage *m, Ictx *ctx)
|
||||
flushcontext(ctx, Keyreset);
|
||||
return reply(c, m, DBUS_TYPE_INVALID, nil);
|
||||
}
|
||||
if(strcmp(name, "ClientCommitPreedit") == 0){
|
||||
if(strcmp(name, "ClientCommitPreedit") == 0 ||
|
||||
strcmp(name, "EffectivePostProcessKeyEvent") == 0){
|
||||
if(dbus_message_iter_get_arg_type(&value) != DBUS_TYPE_STRUCT)
|
||||
return handleerror(c, m, DBUS_ERROR_INVALID_ARGS,
|
||||
"ClientCommitPreedit expects (b)");
|
||||
"the property expects (b)");
|
||||
dbus_message_iter_recurse(&value, &st);
|
||||
if(dbus_message_iter_get_arg_type(&st) != DBUS_TYPE_BOOLEAN)
|
||||
return handleerror(c, m, DBUS_ERROR_INVALID_ARGS,
|
||||
"ClientCommitPreedit expects (b)");
|
||||
"the property expects (b)");
|
||||
dbus_message_iter_get_basic(&st, &b);
|
||||
if(dbus_message_iter_next(&st))
|
||||
return handleerror(c, m, DBUS_ERROR_INVALID_ARGS,
|
||||
"ClientCommitPreedit expects (b)");
|
||||
ctx->clientcommitpreedit = b != FALSE;
|
||||
"the property expects (b)");
|
||||
if(name[0] == 'C')
|
||||
ctx->clientcommitpreedit = b != FALSE;
|
||||
else
|
||||
ctx->postprocess = b != FALSE;
|
||||
return reply(c, m, DBUS_TYPE_INVALID, nil);
|
||||
}
|
||||
return handleerror(c, m, DBUS_ERROR_UNKNOWN_PROPERTY,
|
||||
@@ -796,35 +974,40 @@ handlecursor(DBusConnection *c, DBusMessage *m, Ictx *ctx)
|
||||
{
|
||||
dbus_int32_t x, y, w, h;
|
||||
|
||||
if(!dbus_message_get_args(m, nil,
|
||||
DBUS_TYPE_INT32, &x, DBUS_TYPE_INT32, &y,
|
||||
DBUS_TYPE_INT32, &w, DBUS_TYPE_INT32, &h,
|
||||
DBUS_TYPE_INVALID))
|
||||
return handleerror(c, m, DBUS_ERROR_INVALID_ARGS,
|
||||
"SetCursorLocation expects four integers");
|
||||
if(w < 0 || h < 0)
|
||||
return handleerror(c, m, DBUS_ERROR_INVALID_ARGS,
|
||||
"cursor dimensions must not be negative");
|
||||
dbus_message_get_args(m, nil, DBUS_TYPE_INT32, &x, DBUS_TYPE_INT32, &y,
|
||||
DBUS_TYPE_INT32, &w, DBUS_TYPE_INT32, &h, DBUS_TYPE_INVALID);
|
||||
setcursor(ctx, x, y, h);
|
||||
return reply(c, m, DBUS_TYPE_INVALID, nil);
|
||||
}
|
||||
|
||||
/* Argument-free InputContext calls, checked for an empty signature. */
|
||||
/*
|
||||
* 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
|
||||
handleplain(DBusConnection *c, DBusMessage *m, Ictx *ctx, const char *member)
|
||||
handleok(DBusConnection *c, DBusMessage *m, Ictx*)
|
||||
{
|
||||
if(!dbus_message_has_signature(m, ""))
|
||||
return handleerror(c, m, DBUS_ERROR_INVALID_ARGS,
|
||||
"this call takes no arguments");
|
||||
if(strcmp(member, "FocusIn") == 0)
|
||||
return handlefocusin(c, m, ctx);
|
||||
if(strcmp(member, "FocusOut") == 0)
|
||||
return handlefocusout(c, m, ctx);
|
||||
if(strcmp(member, "Reset") == 0)
|
||||
return handlereset(c, m, ctx);
|
||||
return handledestroy(c, m, ctx);
|
||||
return reply(c, m, DBUS_TYPE_INVALID, nil);
|
||||
}
|
||||
|
||||
/* The InputContext interface: member, argument signature, handler. */
|
||||
static struct {
|
||||
char *member;
|
||||
char *sig;
|
||||
DBusHandlerResult (*fn)(DBusConnection*, DBusMessage*, Ictx*);
|
||||
} ictab[] = {
|
||||
{"ProcessKeyEvent", "uuu", handlekey},
|
||||
{"FocusIn", "", handlefocusin},
|
||||
{"FocusOut", "", handlefocusout},
|
||||
{"Reset", "", handlereset},
|
||||
{"Destroy", "", handledestroy},
|
||||
{"SetCursorLocation", "iiii", handlecursor},
|
||||
{"SetCursorLocationRelative", "iiii", handleok},
|
||||
{"SetCapabilities", "u", handlecap},
|
||||
{"SetSurroundingText", "vuu", handlesurround},
|
||||
{"SetEngine", "s", handleok},
|
||||
};
|
||||
|
||||
/*
|
||||
* One handler for everything a client sends: the tiny slice of the bus
|
||||
* that libibus needs, then the InputContext interface. Unhandled calls
|
||||
@@ -835,6 +1018,7 @@ onmsg(DBusConnection *c, DBusMessage *m, void*)
|
||||
{
|
||||
const char *iface, *member, *path;
|
||||
Ictx *ctx;
|
||||
int i;
|
||||
|
||||
if(dbus_message_get_type(m) != DBUS_MESSAGE_TYPE_METHOD_CALL)
|
||||
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
|
||||
@@ -861,14 +1045,20 @@ onmsg(DBusConnection *c, DBusMessage *m, void*)
|
||||
return handlecreate(c, m);
|
||||
}
|
||||
if(strcmp(iface, "org.freedesktop.DBus.Properties") == 0){
|
||||
if(findcontext(c, path) == nil)
|
||||
ctx = findcontext(c, path);
|
||||
if(ctx == nil)
|
||||
return handleerror(c, m, DBUS_ERROR_UNKNOWN_OBJECT,
|
||||
"unknown input context");
|
||||
if(strcmp(member, "Set") == 0)
|
||||
return handlepropertyset(c, m, findcontext(c, path));
|
||||
if(strcmp(member, "Get") == 0)
|
||||
return handlepropertyset(c, m, ctx);
|
||||
if(strcmp(member, "Get") == 0){
|
||||
if(dbus_message_get_args(m, nil, DBUS_TYPE_STRING, &iface,
|
||||
DBUS_TYPE_STRING, &member, DBUS_TYPE_INVALID) &&
|
||||
strcmp(member, "PostProcessKeyEvent") == 0)
|
||||
return handlepost(c, m, ctx);
|
||||
return handleerror(c, m, DBUS_ERROR_UNKNOWN_PROPERTY,
|
||||
"input context properties are write-only");
|
||||
"unknown input context property");
|
||||
}
|
||||
if(strcmp(member, "GetAll") == 0)
|
||||
return handlepropertygetall(c, m);
|
||||
}
|
||||
@@ -881,50 +1071,39 @@ onmsg(DBusConnection *c, DBusMessage *m, void*)
|
||||
if(ctx == nil)
|
||||
return handleerror(c, m, DBUS_ERROR_UNKNOWN_OBJECT,
|
||||
"unknown input context");
|
||||
if(strcmp(member, "ProcessKeyEvent") == 0)
|
||||
return handlekey(c, m, ctx);
|
||||
if(strcmp(member, "FocusIn") == 0 ||
|
||||
strcmp(member, "FocusOut") == 0 ||
|
||||
strcmp(member, "Reset") == 0 ||
|
||||
strcmp(member, "Destroy") == 0)
|
||||
return handleplain(c, m, ctx, member);
|
||||
if(strcmp(member, "SetCursorLocation") == 0)
|
||||
return handlecursor(c, m, ctx);
|
||||
if(strcmp(member, "SetCapabilities") == 0)
|
||||
return handlecap(c, m, ctx);
|
||||
if(strcmp(member, "SetEngine") == 0){
|
||||
if(!dbus_message_has_signature(m, "s"))
|
||||
for(i = 0; i < nelem(ictab); i++){
|
||||
if(strcmp(member, ictab[i].member) != 0)
|
||||
continue;
|
||||
if(!dbus_message_has_signature(m, ictab[i].sig))
|
||||
return handleerror(c, m, DBUS_ERROR_INVALID_ARGS,
|
||||
"SetEngine expects one engine name");
|
||||
return reply(c, m, DBUS_TYPE_INVALID, nil);
|
||||
"bad arguments");
|
||||
return ictab[i].fn(c, m, ctx);
|
||||
}
|
||||
}
|
||||
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
|
||||
}
|
||||
|
||||
static void
|
||||
newconn(DBusServer *s, DBusConnection *c, void *_)
|
||||
newconn(DBusServer*, DBusConnection *c, void*)
|
||||
{
|
||||
DBusObjectPathVTable vt;
|
||||
|
||||
USED(s);
|
||||
USED(_);
|
||||
if(nconns >= Maxconns){
|
||||
fprintf(stderr, "strans: ibus: rejecting client: %d-connection limit reached\n",
|
||||
Maxconns);
|
||||
if(nconns >= Maxclients){
|
||||
fprint(2, "strans: ibus: rejecting client: %d-connection limit reached\n",
|
||||
Maxclients);
|
||||
dbus_connection_close(c);
|
||||
return;
|
||||
}
|
||||
if(!dbus_connection_set_watch_functions(c, addwatch, removewatch,
|
||||
togglewatch, nil, nil)){
|
||||
fprintf(stderr, "strans: ibus: cannot watch client connection\n");
|
||||
nil, nil, nil)){
|
||||
fprint(2, "strans: ibus: cannot watch client connection\n");
|
||||
dbus_connection_close(c);
|
||||
return;
|
||||
}
|
||||
memset(&vt, 0, sizeof(vt));
|
||||
vt.message_function = onmsg;
|
||||
if(!dbus_connection_register_fallback(c, "/", &vt, nil)){
|
||||
fprintf(stderr, "strans: ibus: cannot register client handler\n");
|
||||
fprint(2, "strans: ibus: cannot register client handler\n");
|
||||
dbus_connection_set_watch_functions(c, nil, nil, nil, nil, nil);
|
||||
dbus_connection_close(c);
|
||||
return;
|
||||
@@ -950,70 +1129,35 @@ pruneconns(void)
|
||||
nconns = j;
|
||||
}
|
||||
|
||||
static int
|
||||
static void
|
||||
ibusinit(void)
|
||||
{
|
||||
DBusError err;
|
||||
char addr[128];
|
||||
char *full;
|
||||
int cleanupregistered;
|
||||
char addr[64], *full;
|
||||
|
||||
full = nil;
|
||||
cleanupregistered = 0;
|
||||
if(buildaddrpath(addrfile, sizeof(addrfile)) < 0){
|
||||
fprintf(stderr, "strans: ibus: cannot build address path\n");
|
||||
return -1;
|
||||
}
|
||||
if(snprintf(addr, sizeof(addr), "unix:abstract=strans-%d",
|
||||
(int)getpid()) >= (int)sizeof addr){
|
||||
fprintf(stderr, "strans: ibus: address is too long\n");
|
||||
return -1;
|
||||
}
|
||||
if(buildaddrpath(addrfile, sizeof addrfile) < 0)
|
||||
die("ibus: cannot build address path");
|
||||
snprintf(addr, sizeof addr, "unix:abstract=strans-%d", (int)getpid());
|
||||
dbus_error_init(&err);
|
||||
srv = dbus_server_listen(addr, &err);
|
||||
if(srv == nil){
|
||||
fprintf(stderr, "strans: ibus: listen: %s\n", err.message);
|
||||
dbus_error_free(&err);
|
||||
addrfile[0] = '\0';
|
||||
return -1;
|
||||
}
|
||||
if(srv == nil)
|
||||
die("ibus: listen: %s", err.message);
|
||||
dbus_server_set_new_connection_function(srv, newconn, nil, nil);
|
||||
if(!dbus_server_set_watch_functions(srv, addwatch, removewatch,
|
||||
togglewatch, nil, nil)){
|
||||
fprintf(stderr, "strans: ibus: cannot watch server\n");
|
||||
goto fail;
|
||||
}
|
||||
nil, nil, nil))
|
||||
die("ibus: cannot watch server");
|
||||
full = dbus_server_get_address(srv);
|
||||
if(full == nil){
|
||||
fprintf(stderr, "strans: ibus: cannot get server address\n");
|
||||
goto fail;
|
||||
}
|
||||
if(!atexit(unlinkaddr)){
|
||||
fprintf(stderr, "strans: ibus: cannot register address cleanup\n");
|
||||
goto fail;
|
||||
}
|
||||
cleanupregistered = 1;
|
||||
if(full == nil)
|
||||
die("ibus: cannot get server address");
|
||||
atexit(unlinkaddr);
|
||||
threadnotify(addrnote, 1);
|
||||
if(writeaddr(addrfile, full) < 0){
|
||||
fprintf(stderr, "strans: ibus: cannot write %s\n", addrfile);
|
||||
goto fail;
|
||||
}
|
||||
if(writeaddr(addrfile, full) < 0)
|
||||
die("ibus: cannot write %s", addrfile);
|
||||
dbus_free(full);
|
||||
return 0;
|
||||
fail:
|
||||
if(cleanupregistered)
|
||||
atexitdont(unlinkaddr);
|
||||
if(full != nil)
|
||||
dbus_free(full);
|
||||
dbus_server_disconnect(srv);
|
||||
dbus_server_unref(srv);
|
||||
srv = nil;
|
||||
addrfile[0] = '\0';
|
||||
return -1;
|
||||
}
|
||||
|
||||
void
|
||||
ibusthread(void *_)
|
||||
ibusthread(void*)
|
||||
{
|
||||
struct pollfd pfds[Maxwatches];
|
||||
DBusWatch *polled[Maxwatches];
|
||||
@@ -1021,13 +1165,9 @@ ibusthread(void *_)
|
||||
int i, n, rv;
|
||||
unsigned int f;
|
||||
|
||||
USED(_);
|
||||
threadsetname("ibus");
|
||||
if(ibusinit() < 0)
|
||||
die("ibus: initialization failed");
|
||||
ibusinit();
|
||||
replyc = chancreate(sizeof(Keyres), 0);
|
||||
if(replyc == nil)
|
||||
die("ibus: cannot create reply channel");
|
||||
for(;;){
|
||||
if(!dbus_server_get_is_connected(srv))
|
||||
die("ibus: server disconnected");
|
||||
@@ -1050,7 +1190,7 @@ ibusthread(void *_)
|
||||
if(rv < 0 && errno == EINTR)
|
||||
continue;
|
||||
if(rv < 0)
|
||||
die("ibus: poll: %s", strerror(errno));
|
||||
die("ibus: poll: %r");
|
||||
for(i = 0; i < n; i++){
|
||||
if(pfds[i].revents == 0)
|
||||
continue;
|
||||
|
||||
53
ipc.c
53
ipc.c
@@ -165,20 +165,31 @@ ipcconnect(void)
|
||||
|
||||
/*
|
||||
* Engine key for an X keysym and its Unicode value: printable characters
|
||||
* as themselves, the function keysyms 0xff00-0xffff as Kspec+offset.
|
||||
* The keypad and Shift+Tab (ISO_Left_Tab) variants fold onto Enter and Tab.
|
||||
* as themselves, the function keysyms 0xff00-0xffff as Kspec+offset, and
|
||||
* no key at all for a modifier. The keypad keys and Shift+Tab
|
||||
* (ISO_Left_Tab) fold onto their plain counterparts.
|
||||
*/
|
||||
uint32_t
|
||||
ipckeysym(uint32_t sym, uint32_t unicode)
|
||||
{
|
||||
if(unicode >= ' ' && unicode != 0x7f)
|
||||
return unicode;
|
||||
if(sym >= 0xffe1 && sym <= 0xffee)
|
||||
return 0;
|
||||
switch(sym){
|
||||
case 0xfe20:
|
||||
case 0xff89:
|
||||
return Ktab;
|
||||
case 0xff8d:
|
||||
return Kret;
|
||||
case 0xff97:
|
||||
return Kup;
|
||||
case 0xff99:
|
||||
return Kdown;
|
||||
case 0xff9a:
|
||||
return Kpgup;
|
||||
case 0xff9b:
|
||||
return Kpgdown;
|
||||
}
|
||||
if(sym >= 0xff00 && sym <= 0xffff)
|
||||
return Kspec + (sym - 0xff00);
|
||||
@@ -241,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)
|
||||
@@ -270,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;
|
||||
}
|
||||
@@ -294,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);
|
||||
@@ -387,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;
|
||||
|
||||
@@ -395,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;
|
||||
|
||||
29
ipc.h
29
ipc.h
@@ -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,
|
||||
@@ -39,8 +46,12 @@ enum
|
||||
Kdown = Kspec|0x54,
|
||||
Kpgup = Kspec|0x55,
|
||||
Kpgdown = Kspec|0x56,
|
||||
Kmodfirst = Kspec|0xe1,
|
||||
Kmodlast = Kspec|0xee,
|
||||
Khangul = Kspec|0x31,
|
||||
Khanja = Kspec|0x34,
|
||||
Kmuhenkan = Kspec|0x22,
|
||||
Khenkan = Kspec|0x23,
|
||||
Kkana = Kspec|0x27,
|
||||
Kzenkaku = Kspec|0x2a,
|
||||
|
||||
/* X core modifier bits; GDK and IBus also flag Super at bit 26. */
|
||||
Mshift = 1<<0,
|
||||
@@ -53,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,
|
||||
@@ -65,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;
|
||||
@@ -82,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*);
|
||||
|
||||
79
ko.c
79
ko.c
@@ -4,8 +4,6 @@
|
||||
enum
|
||||
{
|
||||
Sbase = 0xAC00,
|
||||
Jbase = 0x3131,
|
||||
Jrange = 51,
|
||||
Ncho = 19,
|
||||
Njung = 21,
|
||||
Njong = 28,
|
||||
@@ -35,41 +33,21 @@ static Rune jong[] = {
|
||||
L'ㅌ', L'ㅍ', L'ㅎ',
|
||||
};
|
||||
|
||||
static int choidx[Jrange] = {
|
||||
[L'ㄱ'-Jbase] = 1, [L'ㄲ'-Jbase] = 2, [L'ㄴ'-Jbase] = 3,
|
||||
[L'ㄷ'-Jbase] = 4, [L'ㄸ'-Jbase] = 5, [L'ㄹ'-Jbase] = 6,
|
||||
[L'ㅁ'-Jbase] = 7, [L'ㅂ'-Jbase] = 8, [L'ㅃ'-Jbase] = 9,
|
||||
[L'ㅅ'-Jbase] = 10, [L'ㅆ'-Jbase] = 11, [L'ㅇ'-Jbase] = 12,
|
||||
[L'ㅈ'-Jbase] = 13, [L'ㅉ'-Jbase] = 14, [L'ㅊ'-Jbase] = 15,
|
||||
[L'ㅋ'-Jbase] = 16, [L'ㅌ'-Jbase] = 17, [L'ㅍ'-Jbase] = 18,
|
||||
[L'ㅎ'-Jbase] = 19,
|
||||
};
|
||||
/* A jamo's index in cho, jung, or jong, or -1; jong[0] is no jong. */
|
||||
static int
|
||||
idx(Rune *t, int n, Rune r)
|
||||
{
|
||||
int i;
|
||||
|
||||
static int jungidx[Jrange] = {
|
||||
[L'ㅏ'-Jbase] = 1, [L'ㅐ'-Jbase] = 2, [L'ㅑ'-Jbase] = 3,
|
||||
[L'ㅒ'-Jbase] = 4, [L'ㅓ'-Jbase] = 5, [L'ㅔ'-Jbase] = 6,
|
||||
[L'ㅕ'-Jbase] = 7, [L'ㅖ'-Jbase] = 8, [L'ㅗ'-Jbase] = 9,
|
||||
[L'ㅘ'-Jbase] = 10, [L'ㅙ'-Jbase] = 11, [L'ㅚ'-Jbase] = 12,
|
||||
[L'ㅛ'-Jbase] = 13, [L'ㅜ'-Jbase] = 14, [L'ㅝ'-Jbase] = 15,
|
||||
[L'ㅞ'-Jbase] = 16, [L'ㅟ'-Jbase] = 17, [L'ㅠ'-Jbase] = 18,
|
||||
[L'ㅡ'-Jbase] = 19, [L'ㅢ'-Jbase] = 20, [L'ㅣ'-Jbase] = 21,
|
||||
};
|
||||
for(i = 0; i < n; i++)
|
||||
if(t[i] == r)
|
||||
return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int jongidx[Jrange] = {
|
||||
[L'ㄱ'-Jbase] = 2, [L'ㄲ'-Jbase] = 3, [L'ㄳ'-Jbase] = 4,
|
||||
[L'ㄴ'-Jbase] = 5, [L'ㄵ'-Jbase] = 6, [L'ㄶ'-Jbase] = 7,
|
||||
[L'ㄷ'-Jbase] = 8, [L'ㄹ'-Jbase] = 9, [L'ㄺ'-Jbase] = 10,
|
||||
[L'ㄻ'-Jbase] = 11, [L'ㄼ'-Jbase] = 12, [L'ㄽ'-Jbase] = 13,
|
||||
[L'ㄾ'-Jbase] = 14, [L'ㄿ'-Jbase] = 15, [L'ㅀ'-Jbase] = 16,
|
||||
[L'ㅁ'-Jbase] = 17, [L'ㅂ'-Jbase] = 18, [L'ㅄ'-Jbase] = 19,
|
||||
[L'ㅅ'-Jbase] = 20, [L'ㅆ'-Jbase] = 21, [L'ㅇ'-Jbase] = 22,
|
||||
[L'ㅈ'-Jbase] = 23, [L'ㅊ'-Jbase] = 24, [L'ㅋ'-Jbase] = 25,
|
||||
[L'ㅌ'-Jbase] = 26, [L'ㅍ'-Jbase] = 27, [L'ㅎ'-Jbase] = 28,
|
||||
};
|
||||
|
||||
#define Choidx(r) (choidx[(r) - Jbase] - 1)
|
||||
#define Jungidx(r) (jungidx[(r) - Jbase] - 1)
|
||||
#define Jongidx(r) (jongidx[(r) - Jbase] - 1)
|
||||
#define Choidx(r) idx(cho, Ncho, r)
|
||||
#define Jungidx(r) idx(jung, Njung, r)
|
||||
#define Jongidx(r) idx(jong, Njong, r)
|
||||
|
||||
static Rune jamomap[128] = {
|
||||
['r'] = L'ㄱ', ['R'] = L'ㄲ',
|
||||
@@ -221,12 +199,23 @@ transko(Im *im, Rune c)
|
||||
sputr(&e.next, compose(ci, ji, 0));
|
||||
return e;
|
||||
}
|
||||
if(Jungidx(last) >= 0 && ji >= 0){
|
||||
comb = combine(cvow, nelem(cvow), last, jm);
|
||||
if(comb){
|
||||
sputr(&e.next, comb);
|
||||
return e;
|
||||
}
|
||||
/* A vowel typed first is reordered under its consonant. */
|
||||
if(Jungidx(last) >= 0 && Choidx(jm) >= 0){
|
||||
sputr(&e.next, compose(Choidx(jm), Jungidx(last), 0));
|
||||
return e;
|
||||
}
|
||||
/* Two vowels or two consonants may be one jamo, ㅘ or ㄳ. */
|
||||
comb = combine(cvow, nelem(cvow), last, jm);
|
||||
if(comb == 0)
|
||||
comb = combine(cjong, nelem(cjong), last, jm);
|
||||
if(comb){
|
||||
sputr(&e.next, comb);
|
||||
return e;
|
||||
}
|
||||
if(ji >= 0 && splitpair(cjong, nelem(cjong), last, &stay, &next)){
|
||||
sputr(&e.s, stay);
|
||||
sputr(&e.next, compose(Choidx(next), ji, 0));
|
||||
return e;
|
||||
}
|
||||
e.s = im->pre;
|
||||
sputr(&e.next, jm);
|
||||
@@ -294,12 +283,10 @@ backko(Im *im)
|
||||
|
||||
last = slastr(&im->pre);
|
||||
if(!issyl(last)){
|
||||
if(splitpair(cvow, nelem(cvow), last, &stay, &next)){
|
||||
spopr(&im->pre);
|
||||
sputr(&im->pre, stay);
|
||||
return;
|
||||
}
|
||||
spopr(&im->pre);
|
||||
if(splitpair(cvow, nelem(cvow), last, &stay, &next) ||
|
||||
splitpair(cjong, nelem(cjong), last, &stay, &next))
|
||||
sputr(&im->pre, stay);
|
||||
return;
|
||||
}
|
||||
decompose(last, &c, &j, &jo);
|
||||
|
||||
21
main.c
21
main.c
@@ -47,20 +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);
|
||||
}
|
||||
|
||||
88
map/README
88
map/README
@@ -1,42 +1,90 @@
|
||||
# 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 exactly one Hanja character, a tab, and one modern Hangul
|
||||
syllable:
|
||||
`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:
|
||||
|
||||
```
|
||||
漢 한
|
||||
漢字 한자
|
||||
※ ㅁ
|
||||
```
|
||||
|
||||
It contains no word rows such as `견출지` or `방학`. `mkhanja` validates this
|
||||
contract and groups rows by their Hangul reading to produce the existing
|
||||
runtime dictionary format. 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 one modern Hangul
|
||||
syllable and whose value is one Hanja character supported by the current
|
||||
popup: U+3400–U+4DBF, U+4E00–U+9FFF, or U+F900–U+FAFF. Thus word readings,
|
||||
multi-character values, jamo readings, 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+3400–U+4DBF, U+4E00–U+9FFF, or U+F900–U+FAFF. Thus mixed
|
||||
values and supplementary-plane ideographs are omitted. It keeps a jamo
|
||||
reading, U+3131–U+314E, only when its value is one rune the popup can draw
|
||||
and a candidate row can carry — so `mssymbol.txt`'s ideographic space and
|
||||
soft hyphen are omitted, leaving 985 of its 987 rows, and a Hanja under a
|
||||
jamo reading is omitted as noise. The import preserves each upstream
|
||||
license header and row order. The retained data is BSD 3-Clause licensed
|
||||
by Choe Hwanjin; the complete text is in
|
||||
`LICENSES/BSD-3-Clause-libhangul-hanja.txt`.
|
||||
|
||||
## Emoji and symbol data
|
||||
|
||||
`symbol.src` is hand-written: a symbol, a tab, and its ASCII aliases, kept
|
||||
so that a bare `1`-`9` picks the matching superscript or subscript from a
|
||||
`^` or `_` prefix search. `emoji.src` is generated: every fully-qualified
|
||||
emoji of Unicode's `emoji-test.txt` except the skin-tone variants, with
|
||||
its CLDR names and keywords in English, Korean, and Japanese. `mkemoji`
|
||||
folds the aliases, adds each Japanese one in the other kana so a query
|
||||
typed in either mode finds it, and writes one row per alias to
|
||||
`emoji.dict`; the engine searches that dictionary by prefix, so the rows
|
||||
carry no prefixes.
|
||||
|
||||
The sources are Emoji 17.0 and CLDR 48.2.0. Regenerate them from the
|
||||
repository root with:
|
||||
|
||||
```sh
|
||||
curl -L https://www.unicode.org/Public/17.0.0/emoji/emoji-test.txt -o emoji-test.txt
|
||||
printf '%s %s\n' 1d8a944f88d7952f7ef7c5167fef3c67995bcae24543949710231b03a201acda emoji-test.txt | sha256sum -c -
|
||||
for l in en ko ja; do
|
||||
curl -L https://raw.githubusercontent.com/unicode-org/cldr-json/48.2.0/cldr-json/cldr-annotations-full/annotations/$l/annotations.json -o $l.json
|
||||
curl -L https://raw.githubusercontent.com/unicode-org/cldr-json/48.2.0/cldr-json/cldr-annotations-derived-full/annotationsDerived/$l/annotations.json -o $l-derived.json
|
||||
done
|
||||
map/cldr2emoji emoji-test.txt en.json en-derived.json ko.json ko-derived.json ja.json ja-derived.json >map/emoji.src
|
||||
map/mkemoji >map/emoji.dict
|
||||
```
|
||||
|
||||
The data is under the Unicode License v3; the complete text is in
|
||||
`LICENSES/Unicode-3.0.txt`.
|
||||
|
||||
## Japanese romaji
|
||||
|
||||
`hira.map` and `kata.map` are Mozc's default romaji table, one row per key,
|
||||
in gojūon order; `kata.map` is the same table in Katakana, and
|
||||
`make verify-map` checks that the two have the same keys. A doubled
|
||||
consonant, or `tch`, maps to っ and keeps the consonant pending; the engine
|
||||
knows that rule, the table only lists the rows.
|
||||
|
||||
## Japanese dictionary data
|
||||
|
||||
|
||||
52
map/cldr2emoji
Executable file
52
map/cldr2emoji
Executable file
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Write emoji.src: every fully-qualified emoji of Unicode's emoji-test.txt
|
||||
but the skin-tone variants, each with its CLDR names and keywords in the
|
||||
languages given, as result-first TAB-separated rows for mkemoji."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def emoji(path):
|
||||
out = []
|
||||
for line in path.open(encoding="utf-8"):
|
||||
if "; fully-qualified" not in line:
|
||||
continue
|
||||
cps = [int(cp, 16) for cp in line.split(";")[0].split()]
|
||||
if any(0x1F3FB <= cp <= 0x1F3FF for cp in cps):
|
||||
continue
|
||||
out.append("".join(map(chr, cps)))
|
||||
return out
|
||||
|
||||
|
||||
def annotations(path):
|
||||
"""CLDR annotations.json, plain or derived: emoji -> names, keywords."""
|
||||
root = json.load(path.open(encoding="utf-8"))
|
||||
table = root[next(iter(root))]["annotations"]
|
||||
return {e: a.get("tts", []) + a.get("default", []) for e, a in table.items()}
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
if len(sys.argv) < 3:
|
||||
print(f"usage: {sys.argv[0]} emoji-test.txt annotations.json...",
|
||||
file=sys.stderr)
|
||||
return 2
|
||||
tables = [annotations(Path(arg)) for arg in sys.argv[2:]]
|
||||
print("# Result first, then its CLDR names and keywords; see map/README.")
|
||||
for e in emoji(Path(sys.argv[1])):
|
||||
aliases = []
|
||||
for table in tables:
|
||||
for alias in table.get(e.replace("️", ""), table.get(e, [])):
|
||||
alias = alias.strip()
|
||||
if alias and alias not in aliases:
|
||||
aliases.append(alias)
|
||||
if aliases:
|
||||
print(e + "\t" + "\t".join(aliases))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
19134
map/emoji.dict
19134
map/emoji.dict
File diff suppressed because it is too large
Load Diff
2027
map/emoji.src
2027
map/emoji.src
File diff suppressed because it is too large
Load Diff
186777
map/hanja.dict
186777
map/hanja.dict
File diff suppressed because it is too large
Load Diff
238096
map/hanja.src
238096
map/hanja.src
File diff suppressed because it is too large
Load Diff
319
map/hira.map
319
map/hira.map
@@ -1,152 +1,321 @@
|
||||
a あ
|
||||
- ー
|
||||
i い
|
||||
u う
|
||||
e え
|
||||
o お
|
||||
ka か
|
||||
ga が
|
||||
ki き
|
||||
kya きゃ
|
||||
kyi きぃ
|
||||
kyu きゅ
|
||||
kye きぇ
|
||||
kyo きょ
|
||||
gi ぎ
|
||||
gya ぎゃ
|
||||
gyu ぎゅ
|
||||
gyo ぎょ
|
||||
ka か
|
||||
ki き
|
||||
ku く
|
||||
gu ぐ
|
||||
ke け
|
||||
ge げ
|
||||
ko こ
|
||||
kwa くぁ
|
||||
kwi くぃ
|
||||
kwu くぅ
|
||||
kwe くぇ
|
||||
kwo くぉ
|
||||
gya ぎゃ
|
||||
gyi ぎぃ
|
||||
gyu ぎゅ
|
||||
gye ぎぇ
|
||||
gyo ぎょ
|
||||
ga が
|
||||
gi ぎ
|
||||
gu ぐ
|
||||
ge げ
|
||||
go ご
|
||||
sa さ
|
||||
za ざ
|
||||
si し
|
||||
shi し
|
||||
gwa ぐぁ
|
||||
gwi ぐぃ
|
||||
gwu ぐぅ
|
||||
gwe ぐぇ
|
||||
gwo ぐぉ
|
||||
sya しゃ
|
||||
syi しぃ
|
||||
syu しゅ
|
||||
sye しぇ
|
||||
syo しょ
|
||||
sha しゃ
|
||||
shi し
|
||||
shu しゅ
|
||||
she しぇ
|
||||
sho しょ
|
||||
syo しょ
|
||||
zi ぢ
|
||||
sa さ
|
||||
si し
|
||||
su す
|
||||
se せ
|
||||
so そ
|
||||
swa すぁ
|
||||
swi すぃ
|
||||
swu すぅ
|
||||
swe すぇ
|
||||
swo すぉ
|
||||
z/ ・
|
||||
z. …
|
||||
z, ‥
|
||||
zh ←
|
||||
zj ↓
|
||||
zk ↑
|
||||
zl →
|
||||
z- 〜
|
||||
z[ 『
|
||||
z] 』
|
||||
zya じゃ
|
||||
zyi じぃ
|
||||
zyu じゅ
|
||||
zye じぇ
|
||||
zyo じょ
|
||||
zwa ずぁ
|
||||
zwi ずぃ
|
||||
zwu ずぅ
|
||||
zwe ずぇ
|
||||
zwo ずぉ
|
||||
za ざ
|
||||
zi じ
|
||||
zu ず
|
||||
ze ぜ
|
||||
zo ぞ
|
||||
ja じゃ
|
||||
ji じ
|
||||
ju じゅ
|
||||
je じぇ
|
||||
jo じょ
|
||||
su す
|
||||
zu ず
|
||||
se せ
|
||||
ze ぜ
|
||||
so そ
|
||||
zo ぞ
|
||||
ta た
|
||||
da だ
|
||||
di ぢ
|
||||
ti ち
|
||||
chi ち
|
||||
cha ちゃ
|
||||
chu ちゅ
|
||||
che ちぇ
|
||||
jya じゃ
|
||||
jyi じぃ
|
||||
jyu じゅ
|
||||
jye じぇ
|
||||
jyo じょ
|
||||
tya ちゃ
|
||||
tyi ちぃ
|
||||
tyu ちゅ
|
||||
cho ちょ
|
||||
ji じ
|
||||
tye ちぇ
|
||||
tyo ちょ
|
||||
tsa つぁ
|
||||
tsi つぃ
|
||||
tse つぇ
|
||||
tso つぉ
|
||||
tha てゃ
|
||||
thi てぃ
|
||||
t'i てぃ
|
||||
thu てゅ
|
||||
the てぇ
|
||||
tho てょ
|
||||
t'yu てゅ
|
||||
twa とぁ
|
||||
twi とぃ
|
||||
twu とぅ
|
||||
twe とぇ
|
||||
two とぉ
|
||||
t'u とぅ
|
||||
ta た
|
||||
ti ち
|
||||
tu つ
|
||||
tsu つ
|
||||
du づ
|
||||
te て
|
||||
de で
|
||||
to と
|
||||
dya ぢゃ
|
||||
dyi ぢぃ
|
||||
dyu ぢゅ
|
||||
dye ぢぇ
|
||||
dyo ぢょ
|
||||
dha でゃ
|
||||
dhi でぃ
|
||||
d'i でぃ
|
||||
dhu でゅ
|
||||
dhe でぇ
|
||||
dho でょ
|
||||
d'yu でゅ
|
||||
dwa どぁ
|
||||
dwi どぃ
|
||||
dwu どぅ
|
||||
dwe どぇ
|
||||
dwo どぉ
|
||||
d'u どぅ
|
||||
da だ
|
||||
di ぢ
|
||||
du づ
|
||||
de で
|
||||
do ど
|
||||
cha ちゃ
|
||||
chi ち
|
||||
chu ちゅ
|
||||
che ちぇ
|
||||
cho ちょ
|
||||
cya ちゃ
|
||||
cyi ちぃ
|
||||
cyu ちゅ
|
||||
cye ちぇ
|
||||
cyo ちょ
|
||||
ca か
|
||||
ci し
|
||||
cu く
|
||||
ce せ
|
||||
co こ
|
||||
nya にゃ
|
||||
nyi にぃ
|
||||
nyu にゅ
|
||||
nye にぇ
|
||||
nyo にょ
|
||||
n' ん
|
||||
nn ん
|
||||
n ん
|
||||
na な
|
||||
ni に
|
||||
nya にゃ
|
||||
nyu にゅ
|
||||
nyo にょ
|
||||
nu ぬ
|
||||
ne ね
|
||||
no の
|
||||
ha は
|
||||
ba ば
|
||||
va ば
|
||||
pa ぱ
|
||||
hi ひ
|
||||
hya ひゃ
|
||||
hyi ひぃ
|
||||
hyu ひゅ
|
||||
hye ひぇ
|
||||
hyo ひょ
|
||||
bi び
|
||||
bya びゃ
|
||||
byu びゅ
|
||||
byo びょ
|
||||
vi び
|
||||
pi ぴ
|
||||
pya ぴゃ
|
||||
pyu ぴゅ
|
||||
pyo ぴょ
|
||||
hwa ふぁ
|
||||
hwi ふぃ
|
||||
hwe ふぇ
|
||||
hwo ふぉ
|
||||
hwyu ふゅ
|
||||
ha は
|
||||
hi ひ
|
||||
hu ふ
|
||||
fu ふ
|
||||
he へ
|
||||
ho ほ
|
||||
fa ふぁ
|
||||
fi ふぃ
|
||||
fu ふ
|
||||
fe ふぇ
|
||||
fo ふぉ
|
||||
fya ふゃ
|
||||
fyu ふゅ
|
||||
fyo ふょ
|
||||
bya びゃ
|
||||
byi びぃ
|
||||
byu びゅ
|
||||
bye びぇ
|
||||
byo びょ
|
||||
ba ば
|
||||
bi び
|
||||
bu ぶ
|
||||
vu ぶ
|
||||
pu ぷ
|
||||
he へ
|
||||
be べ
|
||||
ve べ
|
||||
pe ぺ
|
||||
ho ほ
|
||||
bo ぼ
|
||||
vo ぼ
|
||||
pya ぴゃ
|
||||
pyi ぴぃ
|
||||
pyu ぴゅ
|
||||
pye ぴぇ
|
||||
pyo ぴょ
|
||||
pa ぱ
|
||||
pi ぴ
|
||||
pu ぷ
|
||||
pe ぺ
|
||||
po ぽ
|
||||
mya みゃ
|
||||
myi みぃ
|
||||
myu みゅ
|
||||
mye みぇ
|
||||
myo みょ
|
||||
ma ま
|
||||
mi み
|
||||
mya みゃ
|
||||
myu みゅ
|
||||
myo みょ
|
||||
mu む
|
||||
me め
|
||||
mo も
|
||||
ye いぇ
|
||||
ya や
|
||||
yu ゆ
|
||||
yo よ
|
||||
rya りゃ
|
||||
ryi りぃ
|
||||
ryu りゅ
|
||||
rye りぇ
|
||||
ryo りょ
|
||||
ra ら
|
||||
ri り
|
||||
rya りゃ
|
||||
ryu りゅ
|
||||
ryo りょ
|
||||
ru る
|
||||
re れ
|
||||
ro ろ
|
||||
wu う
|
||||
wyi ゐ
|
||||
wye ゑ
|
||||
wa わ
|
||||
wi ゐ
|
||||
we ゑ
|
||||
wi うぃ
|
||||
we うぇ
|
||||
wo を
|
||||
n ん
|
||||
wha うぁ
|
||||
whi うぃ
|
||||
whu う
|
||||
whe うぇ
|
||||
who うぉ
|
||||
va ゔぁ
|
||||
vi ゔぃ
|
||||
vu ゔ
|
||||
ve ゔぇ
|
||||
vo ゔぉ
|
||||
vya ゔゃ
|
||||
vyi ゔぃ
|
||||
vyu ゔゅ
|
||||
vye ゔぇ
|
||||
vyo ゔょ
|
||||
qa くぁ
|
||||
qi くぃ
|
||||
qu く
|
||||
qe くぇ
|
||||
qo くぉ
|
||||
xn ん
|
||||
xa ぁ
|
||||
xi ぃ
|
||||
xu ぅ
|
||||
xe ぇ
|
||||
xo ぉ
|
||||
xyi ぃ
|
||||
xye ぇ
|
||||
xka ヵ
|
||||
xke ヶ
|
||||
xtu っ
|
||||
xtsu っ
|
||||
xya ゃ
|
||||
xyu ゅ
|
||||
xyo ょ
|
||||
xwa ゎ
|
||||
la ぁ
|
||||
li ぃ
|
||||
lu ぅ
|
||||
le ぇ
|
||||
lo ぉ
|
||||
lyi ぃ
|
||||
lye ぇ
|
||||
lka ヵ
|
||||
lke ヶ
|
||||
ltu っ
|
||||
ltsu っ
|
||||
lya ゃ
|
||||
lyu ゅ
|
||||
lyo ょ
|
||||
lwa ゎ
|
||||
qq っ
|
||||
vv っ
|
||||
ll っ
|
||||
xx っ
|
||||
kk っ
|
||||
ss っ
|
||||
tt っ
|
||||
pp っ
|
||||
cc っ
|
||||
gg っ
|
||||
ss っ
|
||||
zz っ
|
||||
jj っ
|
||||
tt っ
|
||||
tch っ
|
||||
dd っ
|
||||
bb っ
|
||||
hh っ
|
||||
ff っ
|
||||
bb っ
|
||||
pp っ
|
||||
mm っ
|
||||
yy っ
|
||||
rr っ
|
||||
ww っ
|
||||
vv っ
|
||||
tch っ
|
||||
cc っ
|
||||
- ー
|
||||
~ 〜
|
||||
. 。
|
||||
, 、
|
||||
[ 「
|
||||
] 」
|
||||
|
||||
358
map/kata.map
358
map/kata.map
@@ -1,153 +1,321 @@
|
||||
a ア
|
||||
- ー
|
||||
i イ
|
||||
u ウ
|
||||
e エ
|
||||
o オ
|
||||
ka カ
|
||||
ga ガ
|
||||
ki キ
|
||||
kya キャ
|
||||
kyi キィ
|
||||
kyu キュ
|
||||
kye キェ
|
||||
kyo キョ
|
||||
gi ギ
|
||||
gya ギャ
|
||||
gyu ギュ
|
||||
gyo ギョ
|
||||
ka カ
|
||||
ki キ
|
||||
ku ク
|
||||
gu グ
|
||||
ke ケ
|
||||
ge ゲ
|
||||
ko コ
|
||||
kwa クァ
|
||||
kwi クィ
|
||||
kwu クゥ
|
||||
kwe クェ
|
||||
kwo クォ
|
||||
gya ギャ
|
||||
gyi ギィ
|
||||
gyu ギュ
|
||||
gye ギェ
|
||||
gyo ギョ
|
||||
ga ガ
|
||||
gi ギ
|
||||
gu グ
|
||||
ge ゲ
|
||||
go ゴ
|
||||
sa サ
|
||||
za ザ
|
||||
si シ
|
||||
shi シ
|
||||
ji ジ
|
||||
sha シャ
|
||||
she シェ
|
||||
shu シュ
|
||||
je ジェ
|
||||
sho ショ
|
||||
gwa グァ
|
||||
gwi グィ
|
||||
gwu グゥ
|
||||
gwe グェ
|
||||
gwo グォ
|
||||
sya シャ
|
||||
syi シィ
|
||||
syu シュ
|
||||
sye シェ
|
||||
syo ショ
|
||||
ja ジャ
|
||||
ju ジュ
|
||||
jo ジョ
|
||||
sha シャ
|
||||
shi シ
|
||||
shu シュ
|
||||
she シェ
|
||||
sho ショ
|
||||
sa サ
|
||||
si シ
|
||||
su ス
|
||||
zu ズ
|
||||
se セ
|
||||
ze ゼ
|
||||
so ソ
|
||||
swa スァ
|
||||
swi スィ
|
||||
swu スゥ
|
||||
swe スェ
|
||||
swo スォ
|
||||
z/ ・
|
||||
z. …
|
||||
z, ‥
|
||||
zh ←
|
||||
zj ↓
|
||||
zk ↑
|
||||
zl →
|
||||
z- 〜
|
||||
z[ 『
|
||||
z] 』
|
||||
zya ジャ
|
||||
zyi ジィ
|
||||
zyu ジュ
|
||||
zye ジェ
|
||||
zyo ジョ
|
||||
zwa ズァ
|
||||
zwi ズィ
|
||||
zwu ズゥ
|
||||
zwe ズェ
|
||||
zwo ズォ
|
||||
za ザ
|
||||
zi ジ
|
||||
zu ズ
|
||||
ze ゼ
|
||||
zo ゾ
|
||||
ta タ
|
||||
da ダ
|
||||
ti ティ
|
||||
chi チ
|
||||
zi ヂ
|
||||
cha チャ
|
||||
chu チュ
|
||||
ja ジャ
|
||||
ji ジ
|
||||
ju ジュ
|
||||
je ジェ
|
||||
jo ジョ
|
||||
jya ジャ
|
||||
jyi ジィ
|
||||
jyu ジュ
|
||||
jye ジェ
|
||||
jyo ジョ
|
||||
tya チャ
|
||||
tyi チィ
|
||||
tyu チュ
|
||||
che チェ
|
||||
cho チョ
|
||||
tye チェ
|
||||
tyo チョ
|
||||
tsa ツァ
|
||||
tsi ツィ
|
||||
tse ツェ
|
||||
tso ツォ
|
||||
tha テャ
|
||||
thi ティ
|
||||
t'i ティ
|
||||
thu テュ
|
||||
the テェ
|
||||
tho テョ
|
||||
t'yu テュ
|
||||
twa トァ
|
||||
twi トィ
|
||||
twu トゥ
|
||||
twe トェ
|
||||
two トォ
|
||||
t'u トゥ
|
||||
ta タ
|
||||
ti チ
|
||||
tu ツ
|
||||
tsu ツ
|
||||
du ヅ
|
||||
te テ
|
||||
de デ
|
||||
to ト
|
||||
dya ヂャ
|
||||
dyi ヂィ
|
||||
dyu ヂュ
|
||||
dye ヂェ
|
||||
dyo ヂョ
|
||||
dha デャ
|
||||
dhi ディ
|
||||
d'i ディ
|
||||
dhu デュ
|
||||
dhe デェ
|
||||
dho デョ
|
||||
d'yu デュ
|
||||
dwa ドァ
|
||||
dwi ドィ
|
||||
dwu ドゥ
|
||||
dwe ドェ
|
||||
dwo ドォ
|
||||
d'u ドゥ
|
||||
da ダ
|
||||
di ヂ
|
||||
du ヅ
|
||||
de デ
|
||||
do ド
|
||||
cha チャ
|
||||
chi チ
|
||||
chu チュ
|
||||
che チェ
|
||||
cho チョ
|
||||
cya チャ
|
||||
cyi チィ
|
||||
cyu チュ
|
||||
cye チェ
|
||||
cyo チョ
|
||||
ca カ
|
||||
ci シ
|
||||
cu ク
|
||||
ce セ
|
||||
co コ
|
||||
nya ニャ
|
||||
nyi ニィ
|
||||
nyu ニュ
|
||||
nye ニェ
|
||||
nyo ニョ
|
||||
n' ン
|
||||
nn ン
|
||||
n ン
|
||||
na ナ
|
||||
ni ニ
|
||||
nya ニャ
|
||||
nyu ニュ
|
||||
nyo ニョ
|
||||
nu ヌ
|
||||
ne ネ
|
||||
no ノ
|
||||
ha ハ
|
||||
ba バ
|
||||
pa パ
|
||||
hi ヒ
|
||||
hya ヒャ
|
||||
hyi ヒィ
|
||||
hyu ヒュ
|
||||
hye ヒェ
|
||||
hyo ヒョ
|
||||
bi ビ
|
||||
bya ビャ
|
||||
byu ビュ
|
||||
byo ビョ
|
||||
pi ピ
|
||||
pya ピャ
|
||||
pyu ピュ
|
||||
pyo ピョ
|
||||
hwa ファ
|
||||
hwi フィ
|
||||
hwe フェ
|
||||
hwo フォ
|
||||
hwyu フュ
|
||||
ha ハ
|
||||
hi ヒ
|
||||
hu フ
|
||||
fu フ
|
||||
bu ブ
|
||||
pu プ
|
||||
he ヘ
|
||||
be ベ
|
||||
pe ペ
|
||||
ho ホ
|
||||
fa ファ
|
||||
fi フィ
|
||||
fu フ
|
||||
fe フェ
|
||||
fo フォ
|
||||
fya フャ
|
||||
fyu フュ
|
||||
fyo フョ
|
||||
bya ビャ
|
||||
byi ビィ
|
||||
byu ビュ
|
||||
bye ビェ
|
||||
byo ビョ
|
||||
ba バ
|
||||
bi ビ
|
||||
bu ブ
|
||||
be ベ
|
||||
bo ボ
|
||||
pya ピャ
|
||||
pyi ピィ
|
||||
pyu ピュ
|
||||
pye ピェ
|
||||
pyo ピョ
|
||||
pa パ
|
||||
pi ピ
|
||||
pu プ
|
||||
pe ペ
|
||||
po ポ
|
||||
mya ミャ
|
||||
myi ミィ
|
||||
myu ミュ
|
||||
mye ミェ
|
||||
myo ミョ
|
||||
ma マ
|
||||
mi ミ
|
||||
mya ミャ
|
||||
myu ミュ
|
||||
myo ミョ
|
||||
mu ム
|
||||
me メ
|
||||
mo モ
|
||||
ye イェ
|
||||
ya ヤ
|
||||
yu ユ
|
||||
yo ヨ
|
||||
rya リャ
|
||||
ryi リィ
|
||||
ryu リュ
|
||||
rye リェ
|
||||
ryo リョ
|
||||
ra ラ
|
||||
ri リ
|
||||
rya リャ
|
||||
ryu リュ
|
||||
ryo リョ
|
||||
ru ル
|
||||
re レ
|
||||
ro ロ
|
||||
wu ウ
|
||||
wyi ヰ
|
||||
wye ヱ
|
||||
wa ワ
|
||||
wi ヰ
|
||||
we ヱ
|
||||
wi ウィ
|
||||
we ウェ
|
||||
wo ヲ
|
||||
n ン
|
||||
xn ン
|
||||
v ヴ
|
||||
xa ァ
|
||||
xi ィ
|
||||
xu ゥ
|
||||
xe ェ
|
||||
xo ォ
|
||||
cc ッ
|
||||
dd ッ
|
||||
kk ッ
|
||||
pp ッ
|
||||
tt ッ
|
||||
tch ッ
|
||||
ss ッ
|
||||
gg ッ
|
||||
zz ッ
|
||||
jj ッ
|
||||
bb ッ
|
||||
hh ッ
|
||||
ff ッ
|
||||
mm ッ
|
||||
yy ッ
|
||||
rr ッ
|
||||
ww ッ
|
||||
vv ッ
|
||||
di ディ
|
||||
fa ファ
|
||||
fi フィ
|
||||
fe フェ
|
||||
fo フォ
|
||||
wha ウァ
|
||||
whi ウィ
|
||||
whu ウ
|
||||
whe ウェ
|
||||
who ウォ
|
||||
va ヴァ
|
||||
vi ヴィ
|
||||
vu ヴ
|
||||
ve ヴェ
|
||||
vo ヴォ
|
||||
vya ヴャ
|
||||
vyi ヴィ
|
||||
vyu ヴュ
|
||||
vye ヴェ
|
||||
vyo ヴョ
|
||||
qa クァ
|
||||
qi クィ
|
||||
qu ク
|
||||
qe クェ
|
||||
qo クォ
|
||||
xn ン
|
||||
xa ァ
|
||||
xi ィ
|
||||
xu ゥ
|
||||
xe ェ
|
||||
xo ォ
|
||||
xyi ィ
|
||||
xye ェ
|
||||
xka ヵ
|
||||
xke ヶ
|
||||
xtu ッ
|
||||
xtsu ッ
|
||||
xya ャ
|
||||
xyu ュ
|
||||
xyo ョ
|
||||
xwa ヮ
|
||||
la ァ
|
||||
li ィ
|
||||
lu ゥ
|
||||
le ェ
|
||||
lo ォ
|
||||
lyi ィ
|
||||
lye ェ
|
||||
lka ヵ
|
||||
lke ヶ
|
||||
ltu ッ
|
||||
ltsu ッ
|
||||
lya ャ
|
||||
lyu ュ
|
||||
lyo ョ
|
||||
lwa ヮ
|
||||
qq ッ
|
||||
vv ッ
|
||||
ll ッ
|
||||
xx ッ
|
||||
kk ッ
|
||||
gg ッ
|
||||
ss ッ
|
||||
zz ッ
|
||||
jj ッ
|
||||
tt ッ
|
||||
tch ッ
|
||||
dd ッ
|
||||
hh ッ
|
||||
ff ッ
|
||||
bb ッ
|
||||
pp ッ
|
||||
mm ッ
|
||||
yy ッ
|
||||
rr ッ
|
||||
ww ッ
|
||||
cc ッ
|
||||
- ー
|
||||
~ 〜
|
||||
. 。
|
||||
, 、
|
||||
[ 「
|
||||
] 」
|
||||
|
||||
@@ -1,21 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract single-character Hanja readings from libhangul data."""
|
||||
"""Extract Hanja and symbol readings from libhangul data."""
|
||||
|
||||
import sys
|
||||
import unicodedata
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def ishangul(s):
|
||||
return len(s) == 1 and 0xAC00 <= ord(s) <= 0xD7A3
|
||||
return s != "" and all(0xAC00 <= ord(c) <= 0xD7A3 for c in s)
|
||||
|
||||
|
||||
def isjamo(s):
|
||||
return len(s) == 1 and 0x3131 <= ord(s) <= 0x314E
|
||||
|
||||
|
||||
def ishanja(s):
|
||||
if len(s) != 1:
|
||||
return False
|
||||
c = ord(s)
|
||||
return (0x3400 <= c <= 0x4DBF
|
||||
or 0x4E00 <= c <= 0x9FFF
|
||||
or 0xF900 <= c <= 0xFAFF)
|
||||
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):
|
||||
@@ -35,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 single-character Hanja readings")
|
||||
raise ValueError(f"{name}: no readings")
|
||||
return comments, entries
|
||||
|
||||
|
||||
@@ -66,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
|
||||
|
||||
50
map/mkemoji
50
map/mkemoji
@@ -1,13 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Write emoji.dict to stdout from a result-first UTF-8 TSV file."""
|
||||
"""Write emoji.dict to stdout from result-first UTF-8 TSV files."""
|
||||
|
||||
import sys
|
||||
import unicodedata
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SOURCE = Path(__file__).with_name("emoji.src")
|
||||
SOURCES = [Path(__file__).with_name(name)
|
||||
for name in ("symbol.src", "emoji.src")]
|
||||
MAXRUNES = 64
|
||||
MAXCANDIDATES = 128
|
||||
|
||||
|
||||
HIRA = {c: c + 0x60 for c in range(0x3041, 0x3097)}
|
||||
KATA = {c: c - 0x60 for c in range(0x30A1, 0x30F7)}
|
||||
|
||||
|
||||
def fold(s):
|
||||
@@ -15,6 +21,12 @@ def fold(s):
|
||||
return unicodedata.normalize("NFC", s)
|
||||
|
||||
|
||||
def kana(alias):
|
||||
"""The alias, and in the other kana where it has any: a query typed
|
||||
in either Japanese mode finds it."""
|
||||
return {alias, alias.translate(HIRA), alias.translate(KATA)}
|
||||
|
||||
|
||||
def hascontrol(s):
|
||||
return any(unicodedata.category(c) == "Cc" for c in s)
|
||||
|
||||
@@ -40,35 +52,29 @@ def read(path):
|
||||
if (not alias or len(alias) > MAXRUNES or hascontrol(alias)
|
||||
or alias != alias.strip() or alias.startswith(";")):
|
||||
raise ValueError(f"{path}:{lineno}: bad alias")
|
||||
entries.append((result, alias))
|
||||
entries.extend((result, a) for a in sorted(kana(alias)))
|
||||
return entries
|
||||
|
||||
|
||||
def add(table, key, result):
|
||||
values = table.setdefault(key, [])
|
||||
if result not in values:
|
||||
values.append(result)
|
||||
|
||||
|
||||
def build(entries):
|
||||
exact = {}
|
||||
prefix = {}
|
||||
"""One row per alias, in order of first appearance; the engine searches
|
||||
the dictionary by prefix, so no prefix rows are needed."""
|
||||
table = {}
|
||||
for result, alias in entries:
|
||||
for n in range(1, len(alias) + 1):
|
||||
add(exact if n == len(alias) else prefix, alias[:n], result)
|
||||
for key in sorted(exact.keys() | prefix.keys()):
|
||||
values = exact.get(key, []) + prefix.get(key, [])
|
||||
values = list(dict.fromkeys(values))
|
||||
yield f"{key}\t{' '.join(values)}"
|
||||
values = table.setdefault(alias, [])
|
||||
if result not in values:
|
||||
values.append(result)
|
||||
for alias, values in table.items():
|
||||
yield f"{alias}\t{' '.join(values[:MAXCANDIDATES])}"
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) > 2:
|
||||
print(f"usage: {sys.argv[0]} [emoji.src]", file=sys.stderr)
|
||||
return 2
|
||||
path = Path(sys.argv[1]) if len(sys.argv) == 2 else SOURCE
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
paths = [Path(arg) for arg in sys.argv[1:]] or SOURCES
|
||||
try:
|
||||
for line in build(read(path)):
|
||||
entries = [entry for path in paths for entry in read(path)]
|
||||
for line in build(entries):
|
||||
print(line)
|
||||
except (OSError, UnicodeError, ValueError) as error:
|
||||
print(error, file=sys.stderr)
|
||||
|
||||
74
map/mkhanja
74
map/mkhanja
@@ -1,32 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Write hanja.dict from a one-Hanja-per-row UTF-8 source."""
|
||||
"""Write hanja.dict from result-per-row UTF-8 sources."""
|
||||
|
||||
import sys
|
||||
import 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
|
||||
|
||||
|
||||
def ishangul(s):
|
||||
return len(s) == 1 and 0xAC00 <= ord(s) <= 0xD7A3
|
||||
return s != "" and all(0xAC00 <= ord(c) <= 0xD7A3 for c in s)
|
||||
|
||||
|
||||
def isjamo(s):
|
||||
return len(s) == 1 and 0x3131 <= ord(s) <= 0x314E
|
||||
|
||||
|
||||
def ishanja(s):
|
||||
if len(s) != 1:
|
||||
return False
|
||||
c = ord(s)
|
||||
return (0x3400 <= c <= 0x4DBF
|
||||
or 0x4E00 <= c <= 0x9FFF
|
||||
or 0xF900 <= c <= 0xFAFF)
|
||||
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")
|
||||
@@ -39,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 one BMP Hanja character")
|
||||
if not ishangul(reading):
|
||||
raise ValueError(f"{path}:{lineno}: need one Hangul syllable")
|
||||
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
1012
map/mssymbol.src
Normal file
File diff suppressed because it is too large
Load Diff
@@ -36,10 +36,16 @@ function add(k, v, id) {
|
||||
row[k] = row[k] " "
|
||||
row[k] = row[k] v
|
||||
}
|
||||
/^;;/ || /^[[:space:]]*$/ {
|
||||
/^;;/ {
|
||||
if(!body) # the header, with its license notice, is kept
|
||||
print
|
||||
next
|
||||
}
|
||||
/^[[:space:]]*$/ {
|
||||
next
|
||||
}
|
||||
{
|
||||
body = 1
|
||||
if(!match($0, /[[:space:]]+/))
|
||||
fail("missing candidate list")
|
||||
key = substr($0, 1, RSTART-1)
|
||||
|
||||
103
map/symbol.src
Normal file
103
map/symbol.src
Normal file
@@ -0,0 +1,103 @@
|
||||
# Result first, followed by one or more TAB-separated aliases.
|
||||
⚠ !!
|
||||
★ **
|
||||
± +-
|
||||
→ ->
|
||||
· ..
|
||||
… ...
|
||||
÷ ./
|
||||
☹ :(
|
||||
☺ :)
|
||||
|
||||
# Bare 1-9 choose from a prefix search; keep digit aliases in matching slots.
|
||||
← <- <
|
||||
≤ <= <
|
||||
♥ <3 <
|
||||
≠ <> != <
|
||||
≡ ==
|
||||
⇒ =>
|
||||
≥ >=
|
||||
¹ ^1 ^
|
||||
² ^2 ^
|
||||
³ ^3 ^
|
||||
⁴ ^4 ^
|
||||
⁵ ^5 ^
|
||||
⁶ ^6 ^
|
||||
⁷ ^7 ^
|
||||
⁸ ^8 ^
|
||||
⁹ ^9 ^
|
||||
₁ _1 _
|
||||
₂ _2 _
|
||||
₃ _3 _
|
||||
₄ _4 _
|
||||
₅ _5 _
|
||||
₆ _6 _
|
||||
₇ _7 _
|
||||
₈ _8 _
|
||||
₉ _9 _
|
||||
|
||||
⁽ ^(
|
||||
⁾ ^)
|
||||
⁺ ^+
|
||||
⁻ ^-
|
||||
⁼ ^=
|
||||
₍ _(
|
||||
₎ _)
|
||||
₊ _+
|
||||
₋ _-
|
||||
₌ _=
|
||||
≈ ~=
|
||||
⁰ ^0
|
||||
₀ _0
|
||||
ₐ _a
|
||||
α alpha
|
||||
β beta
|
||||
χ chi
|
||||
° deg degree
|
||||
δ delta
|
||||
Δ De Delta
|
||||
↓ dn down
|
||||
ₑ _e
|
||||
ε eps
|
||||
η eta
|
||||
凸 fuck
|
||||
γ gamma
|
||||
Γ Ga Gamma
|
||||
ⁱ ^i
|
||||
∫ II integral
|
||||
∞ inf infinity
|
||||
ι iota
|
||||
κ kappa
|
||||
λ lambda
|
||||
Λ La Lambda
|
||||
× mul times
|
||||
μ mu
|
||||
ⁿ ^n
|
||||
ν nu
|
||||
ω omega
|
||||
Ω Om Omega
|
||||
● oo circle
|
||||
ₒ _o
|
||||
φ phi
|
||||
Φ Ph Phi
|
||||
π pi
|
||||
Π Pi
|
||||
∏ PP prod
|
||||
ψ psi
|
||||
Ψ Ps Psi
|
||||
ρ rho
|
||||
σ sigma
|
||||
Σ Si Sigma
|
||||
√ sq sqrt
|
||||
∑ SS sum
|
||||
τ tau
|
||||
θ theta
|
||||
Θ Th Theta
|
||||
↑ up
|
||||
υ ups
|
||||
✓ vv check
|
||||
ξ xi
|
||||
Ξ Xi
|
||||
✗ xx cross
|
||||
ₓ _x
|
||||
ζ zeta
|
||||
@@ -119,7 +119,7 @@ void
|
||||
popuplayout(Drawcmd *dc, int areaw, int areah, Popup *p)
|
||||
{
|
||||
char buf[32];
|
||||
int first, i, markrow, nall, nmark, npre, pad, total, width, y;
|
||||
int first, i, markrow, nall, nmark, npre, total, width, y;
|
||||
|
||||
memset(p, 0, sizeof *p);
|
||||
p->prey = -1;
|
||||
@@ -127,14 +127,12 @@ popuplayout(Drawcmd *dc, int areaw, int areah, Popup *p)
|
||||
p->rowsy = -1;
|
||||
p->marky = -1;
|
||||
p->sely = -1;
|
||||
pad = PopupPad;
|
||||
nall = min(max(dc->nkouho, 0), Maxdisp);
|
||||
npre = dc->pre.n != 0;
|
||||
if((nall == 0 && !npre) || areaw <= 0 || areah <= 0)
|
||||
return;
|
||||
|
||||
/* Rows first; a page marker when they do not all fit; and in a
|
||||
* sliver of a work area the selected row alone, unpadded. */
|
||||
/* As many rows as fit, and a page marker when they do not all. */
|
||||
total = max(dc->total, dc->first + nall);
|
||||
markrow = dc->first > 0 || nall < total;
|
||||
p->n = fitrows(areah, npre, markrow, nall);
|
||||
@@ -142,15 +140,8 @@ popuplayout(Drawcmd *dc, int areaw, int areah, Popup *p)
|
||||
markrow = 1;
|
||||
p->n = fitrows(areah, npre, markrow, nall);
|
||||
}
|
||||
if(p->n == 0 && markrow){
|
||||
markrow = 0;
|
||||
p->n = fitrows(areah, npre, markrow, nall);
|
||||
}
|
||||
if(p->n == 0 && nall != 0){
|
||||
npre = 0;
|
||||
pad = 0;
|
||||
p->n = max(min(nall, areah / Fontsz), 1);
|
||||
}
|
||||
if(p->n == 0 && !npre)
|
||||
return;
|
||||
if(p->n != 0 && dc->sel >= p->n)
|
||||
p->row0 = min(dc->sel - p->n + 1, nall - p->n);
|
||||
first = dc->first + p->row0;
|
||||
@@ -158,7 +149,8 @@ popuplayout(Drawcmd *dc, int areaw, int areah, Popup *p)
|
||||
if(nmark == 0)
|
||||
markrow = 0;
|
||||
|
||||
width = PopupBasew;
|
||||
/* Rows share one steady width; a preedit alone hugs its text. */
|
||||
width = p->n != 0 ? PopupBasew : 2*PopupPad;
|
||||
if(npre)
|
||||
width = max(width, textwidth(&dc->pre) + 2*PopupPad);
|
||||
for(i = 0; i < p->n; i++)
|
||||
@@ -171,7 +163,7 @@ popuplayout(Drawcmd *dc, int areaw, int areah, Popup *p)
|
||||
}
|
||||
p->w = min(width, areaw);
|
||||
|
||||
y = pad;
|
||||
y = PopupPad;
|
||||
if(npre){
|
||||
p->prey = y;
|
||||
y += Fontsz;
|
||||
@@ -188,7 +180,7 @@ popuplayout(Drawcmd *dc, int areaw, int areah, Popup *p)
|
||||
p->marky = y;
|
||||
y += Fontsz;
|
||||
}
|
||||
p->h = min(y + pad, areah);
|
||||
p->h = min(y + PopupPad, areah);
|
||||
p->textw = max(p->w - 2*PopupPad - PopupNumw, 0);
|
||||
if(markrow){
|
||||
p->markw = min(p->markw, max(p->w - 2*PopupPad, 0));
|
||||
@@ -206,10 +198,6 @@ popupdraw(u32int *img, Drawcmd *dc, Popup *p)
|
||||
u32int color;
|
||||
int i, j, y;
|
||||
|
||||
if(img == nil || p->n < 0 || p->n > Maxdisp ||
|
||||
p->row0 < 0 || p->row0 + p->n > dc->nkouho ||
|
||||
p->w <= 0 || p->h <= 0 || p->h > Imgh)
|
||||
return;
|
||||
fill(img, p->w*p->h, Colbg);
|
||||
if(p->prey >= 0)
|
||||
textdraw(img, p->w, p->h, PopupPad, p->prey,
|
||||
|
||||
494
proto/input-method-unstable-v2.xml
Normal file
494
proto/input-method-unstable-v2.xml
Normal 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>
|
||||
112
proto/virtual-keyboard-unstable-v1.xml
Normal file
112
proto/virtual-keyboard-unstable-v1.xml
Normal 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>
|
||||
30
srv.c
30
srv.c
@@ -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,12 +68,18 @@ 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;
|
||||
}
|
||||
|
||||
/*
|
||||
* One Keyreq persists for the connection: the negotiated capability and
|
||||
* One Keyreq persists for the connection: the negotiated preedit and
|
||||
* the last caret ride along with every request, and the socket closing
|
||||
* releases the engine.
|
||||
*/
|
||||
@@ -78,17 +90,19 @@ 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);
|
||||
memset(&kr, 0, sizeof kr);
|
||||
kr.reply = chancreate(sizeof(Keyres), 0);
|
||||
kr.owner = &fd;
|
||||
kr.cap = Cclientpreedit;
|
||||
while(srvreadreq(fd, &kr, &want) >= 0){
|
||||
kr.clientpre = 1;
|
||||
while((rv = srvreadreq(fd, &kr, &want)) >= 0){
|
||||
if(rv != 0)
|
||||
continue;
|
||||
if(kr.op != Keycaret)
|
||||
kr.cap = want ? Cclientpreedit : 0;
|
||||
kr.clientpre = want;
|
||||
chansend(keyc, &kr);
|
||||
chanrecv(kr.reply, &res);
|
||||
if(kr.op == Keycaret)
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
17
str.c
17
str.c
@@ -1,7 +1,8 @@
|
||||
#include "dat.h"
|
||||
#include "fn.h"
|
||||
|
||||
/* Fills s from n bytes of UTF-8; whole, valid, and at most Maxrunes. */
|
||||
/* Fills s from n bytes of UTF-8; whole, valid, and at most Maxrunes.
|
||||
* Anything else leaves s empty. */
|
||||
int
|
||||
sinit(Str *s, char *src, int n)
|
||||
{
|
||||
@@ -24,6 +25,20 @@ 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);
|
||||
sinit(s, p, n - (p - src));
|
||||
}
|
||||
|
||||
void
|
||||
sclear(Str *s)
|
||||
{
|
||||
|
||||
@@ -3,8 +3,8 @@ LD = 9l
|
||||
HOSTCC = cc
|
||||
PKG_CONFIG ?= pkg-config
|
||||
CFLAGS ?= -O2 -g
|
||||
UNIT_CPPFLAGS = -I.. -I../cutest
|
||||
UNIT_CFLAGS = -std=c99 -Wall -Wextra
|
||||
UNIT_CPPFLAGS = -I..
|
||||
UNIT_CFLAGS = -Wall -Wextra
|
||||
HOST_CFLAGS = -std=c99 -Wall -Wextra
|
||||
TEXT_CFLAGS = $(shell $(PKG_CONFIG) --cflags pangocairo cairo fontconfig)
|
||||
TEXT_LIBS = $(shell $(PKG_CONFIG) --libs pangocairo cairo fontconfig)
|
||||
@@ -18,9 +18,11 @@ IBUS_CLIENT_CFLAGS = $(shell $(PKG_CONFIG) --cflags ibus-1.0)
|
||||
IBUS_CLIENT_LIBS = $(shell $(PKG_CONFIG) --libs ibus-1.0)
|
||||
X11_CFLAGS = $(shell $(PKG_CONFIG) --cflags x11)
|
||||
X11_LIBS = $(shell $(PKG_CONFIG) --libs x11)
|
||||
XIM_CFLAGS = $(shell $(PKG_CONFIG) --cflags xcb-imdkit xcb-xkb xkbcommon-x11)
|
||||
XIM_LIBS = $(shell $(PKG_CONFIG) --libs xcb-imdkit xcb-xkb xkbcommon-x11)
|
||||
UNIT_LDLIBS = -lthread -lbio $(TEXT_LIBS) $(IBUS_LIBS) $(XIM_LIBS)
|
||||
XIM_CFLAGS = $(shell $(PKG_CONFIG) --cflags xcb-imdkit xcb-aux xcb-xkb xkbcommon-x11)
|
||||
XIM_LIBS = $(shell $(PKG_CONFIG) --libs xcb-imdkit xcb-aux xcb-xkb xkbcommon-x11)
|
||||
WL_CFLAGS = $(shell $(PKG_CONFIG) --cflags wayland-client xkbcommon)
|
||||
WL_LIBS = $(shell $(PKG_CONFIG) --libs wayland-client xkbcommon)
|
||||
UNIT_LDLIBS = -lthread -lbio $(TEXT_LIBS) $(IBUS_LIBS) $(XIM_LIBS) $(WL_LIBS)
|
||||
|
||||
PROG = unit_test
|
||||
STRESS = stress_test
|
||||
@@ -32,14 +34,14 @@ FAULT = daemon_collision_test daemon_failure_test daemon_restart_test
|
||||
LIVE = $(SMOKE) $(FAULT)
|
||||
TESTSRC = test_util.c str_test.c trie_test.c \
|
||||
ko_test.c vi_test.c engine_test.c dict_test.c ipc_test.c \
|
||||
popup_test.c font_test.c ibus_test.c server_test.c keymap_test.c \
|
||||
xim_adapter_test.c
|
||||
popup_test.c font_test.c ibus_test.c server_test.c compose_test.c \
|
||||
xim_adapter_test.c wl_adapter_test.c
|
||||
TESTOBJ = $(TESTSRC:.c=.o)
|
||||
# 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
|
||||
font.c compose.c imv2.c vkv1.c
|
||||
PARENTOBJ = $(PARENTSRC:%.c=unit_%.o)
|
||||
XIMOBJ = unit_xim_keymap.o
|
||||
COMMONOBJ = $(TESTOBJ) $(PARENTOBJ) $(XIMOBJ)
|
||||
COMMONOBJ = $(TESTOBJ) $(PARENTOBJ)
|
||||
OBJS = unit_test.o stress_test.o $(COMMONOBJ)
|
||||
|
||||
all: $(PROG) $(STRESS) $(LIVE)
|
||||
@@ -87,12 +89,16 @@ gtk_live_test: gtk_live_test.c $(LIVESRC) ../ipc.c ../ipc.h
|
||||
$(LDFLAGS) -o $@ gtk_live_test.c live.c ../ipc.c $(GTK_LIBS) \
|
||||
-pthread $(LDLIBS)
|
||||
|
||||
../gtk/im-strans.so: ../gtk/main.c ../ipc.c ../ipc.h
|
||||
# The parent make knows what these are built from; always ask it.
|
||||
../gtk/im-strans.so:
|
||||
$(MAKE) -C ../gtk
|
||||
|
||||
../strans:
|
||||
$(MAKE) -C .. strans
|
||||
|
||||
../imv2.c ../imv2.h ../vkv1.c ../vkv1.h:
|
||||
$(MAKE) -C .. $(@F)
|
||||
|
||||
xim_live_test: xim_live_test.c $(LIVESRC) ../ipc.c ../ipc.h
|
||||
$(HOSTCC) $(CPPFLAGS) -I.. $(X11_CFLAGS) $(HOST_CFLAGS) $(CFLAGS) \
|
||||
$(LDFLAGS) -o $@ xim_live_test.c live.c ../ipc.c $(X11_LIBS) \
|
||||
@@ -123,14 +129,13 @@ unit_test.o stress_test.o $(TESTOBJ): test.h ../dat.h ../fn.h ../ipc.h \
|
||||
engine_test.o: ../strans.c
|
||||
ibus_test.o: UNIT_CPPFLAGS += $(IBUS_CFLAGS)
|
||||
ibus_test.o: ../ibus.c
|
||||
keymap_test.o xim_adapter_test.o: UNIT_CPPFLAGS += $(XIM_CFLAGS)
|
||||
xim_adapter_test.o: ../xim/xim.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_xim_keymap.o: ../xim/keymap.c
|
||||
$(CC) $(CPPFLAGS) $(UNIT_CPPFLAGS) $(XIM_CFLAGS) $(UNIT_CFLAGS) \
|
||||
$(CFLAGS) -c -o $@ $<
|
||||
unit_compose.o compose_test.o: UNIT_CPPFLAGS += $(IBUS_CFLAGS)
|
||||
|
||||
unit_%.o: ../%.c ../dat.h ../fn.h ../ipc.h
|
||||
$(CC) $(CPPFLAGS) $(UNIT_CPPFLAGS) $(UNIT_CFLAGS) $(CFLAGS) -c -o $@ $<
|
||||
@@ -141,4 +146,5 @@ unit_%.o: ../%.c ../dat.h ../fn.h ../ipc.h
|
||||
clean:
|
||||
rm -f $(OBJS) $(PROG) $(STRESS) $(LIVE)
|
||||
|
||||
.PHONY: all check check-live check-stress test clean
|
||||
.PHONY: all check check-live check-stress test clean \
|
||||
../strans ../gtk/im-strans.so
|
||||
|
||||
56
tests/compose_test.c
Normal file
56
tests/compose_test.c
Normal file
@@ -0,0 +1,56 @@
|
||||
#include "dat.h"
|
||||
#include "fn.h"
|
||||
#include "test.h"
|
||||
#include <xkbcommon/xkbcommon-keysyms.h>
|
||||
|
||||
/*
|
||||
* A sequence swallows its keys and hands its text back at the end, and
|
||||
* belongs to the one context, in the one frontend, that started it.
|
||||
*/
|
||||
void
|
||||
compose_sequences(struct ct *t)
|
||||
{
|
||||
static int a, b; /* two input contexts, told apart by address */
|
||||
static const struct {
|
||||
int who;
|
||||
void *owner;
|
||||
u32int sym;
|
||||
int composing;
|
||||
char *text;
|
||||
} keys[] = {
|
||||
{ Composexim, &a, XKB_KEY_a, 0, "" },
|
||||
{ Composexim, &a, XKB_KEY_dead_acute, 1, "" },
|
||||
{ Composexim, &a, XKB_KEY_e, 0, "é" },
|
||||
{ Composexim, &a, XKB_KEY_Multi_key, 1, "" },
|
||||
{ Composexim, &a, XKB_KEY_o, 1, "" },
|
||||
{ Composexim, &a, XKB_KEY_c, 0, "©" },
|
||||
{ Composexim, &a, XKB_KEY_dead_acute, 1, "" },
|
||||
{ Composexim, &a, XKB_KEY_Escape, 1, "" },
|
||||
{ Composexim, &a, XKB_KEY_a, 0, "" },
|
||||
/* another context of the same frontend starts over */
|
||||
{ Composexim, &a, XKB_KEY_dead_acute, 1, "" },
|
||||
{ Composexim, &b, XKB_KEY_e, 0, "" },
|
||||
/* another frontend does not touch this one's sequence */
|
||||
{ Composexim, &a, XKB_KEY_dead_acute, 1, "" },
|
||||
{ Composewl, &a, XKB_KEY_o, 0, "" },
|
||||
{ Composexim, &a, XKB_KEY_e, 0, "é" },
|
||||
};
|
||||
char buf[Maxutf];
|
||||
int i;
|
||||
|
||||
if(!CT_CHECK(t, setenv("XCOMPOSEFILE", "data/compose", 1) == 0))
|
||||
return;
|
||||
composeinit();
|
||||
for(i = 0; i < nelem(keys); i++){
|
||||
CT_EQ_INT(t, keys[i].composing, composekey(keys[i].who,
|
||||
keys[i].owner, keys[i].sym, buf, sizeof buf));
|
||||
CT_EQ_STR(t, keys[i].text, buf);
|
||||
}
|
||||
/* a context that goes takes its half-typed sequence with it */
|
||||
CT_EQ_INT(t, 1,
|
||||
composekey(Composewl, &a, XKB_KEY_dead_acute, buf, sizeof buf));
|
||||
composedrop(Composewl, &a);
|
||||
CT_EQ_INT(t, 0, composekey(Composewl, &a, XKB_KEY_e, buf, sizeof buf));
|
||||
CT_EQ_STR(t, "", buf);
|
||||
unsetenv("XCOMPOSEFILE");
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
2
tests/data/compose
Normal file
2
tests/data/compose
Normal file
@@ -0,0 +1,2 @@
|
||||
<dead_acute> <e> : "é"
|
||||
<Multi_key> <o> <c> : "©"
|
||||
@@ -1,3 +1,4 @@
|
||||
;; converter fixture; the test transcodes this file to EUC-JP
|
||||
かんじ 漢字 幹事 感じ
|
||||
えがお 笑顔
|
||||
きごう 記号 普通
|
||||
|
||||
|
@@ -15,10 +15,10 @@ dictionary_candidates(struct ct *t)
|
||||
lang = getlang(LangJP);
|
||||
saved = lang->dict;
|
||||
lang->dict = trienew();
|
||||
trieput(lang->dict, "かな", strlen("かな"), " 候補1 かな 候補2 ",
|
||||
strlen(" 候補1 かな 候補2 "));
|
||||
trieput(lang->dict, "かな", strlen("かな"), " 候補1 候補2 ",
|
||||
strlen(" 候補1 候補2 "));
|
||||
key = mkstr("かな");
|
||||
n = dictlookup(lang, &key, kouho, Maxkouho);
|
||||
n = dictlookup(lang->dict, &key, kouho, Maxkouho);
|
||||
if(CT_EQ_INT(t, 2, n)){
|
||||
checkstr(t, "candidate 1", "候補1", &kouho[0]);
|
||||
checkstr(t, "candidate 2", "候補2", &kouho[1]);
|
||||
@@ -34,13 +34,13 @@ dictionary_candidates(struct ct *t)
|
||||
*p = '\0';
|
||||
trieput(lang->dict, "key", 3, many, strlen(many));
|
||||
key = mkstr("key");
|
||||
n = dictlookup(lang, &key, kouho, Maxkouho);
|
||||
n = dictlookup(lang->dict, &key, kouho, Maxkouho);
|
||||
if(CT_EQ_INT(t, Maxkouho, n)){
|
||||
checkstr(t, "first capped candidate", "c00", &kouho[0]);
|
||||
snprint(item, sizeof item, "c%02d", Maxkouho-1);
|
||||
checkstr(t, "last capped candidate", item, &kouho[Maxkouho-1]);
|
||||
}
|
||||
CT_EQ_INT(t, 3, dictlookup(lang, &key, kouho, 3));
|
||||
CT_EQ_INT(t, 3, dictlookup(lang->dict, &key, kouho, 3));
|
||||
trieclose(lang->dict);
|
||||
lang->dict = saved;
|
||||
}
|
||||
@@ -56,28 +56,33 @@ dictionary_misses(struct ct *t)
|
||||
saved = lang->dict;
|
||||
lang->dict = trienew();
|
||||
key = mkstr("");
|
||||
CT_EQ_INT(t, 0, dictlookup(lang, &key, kouho, Maxkouho));
|
||||
CT_EQ_INT(t, 0, dictlookup(lang->dict, &key, kouho, Maxkouho));
|
||||
key = mkstr("missing");
|
||||
CT_EQ_INT(t, 0, dictlookup(lang, &key, kouho, Maxkouho));
|
||||
CT_EQ_INT(t, 0, dictlookup(getlang(LangKO), &key, kouho, Maxkouho));
|
||||
CT_EQ_INT(t, 0, dictlookup(lang->dict, &key, kouho, Maxkouho));
|
||||
CT_EQ_INT(t, 0, dictlookup(getlang(LangKO)->dict, &key, kouho, Maxkouho));
|
||||
trieclose(lang->dict);
|
||||
lang->dict = saved;
|
||||
}
|
||||
|
||||
void
|
||||
dictionary_emoji_identity(struct ct *t)
|
||||
dictionary_prefix(struct ct *t)
|
||||
{
|
||||
Str kouho[Maxkouho], key;
|
||||
Trie *saved;
|
||||
Lang *lang;
|
||||
Trie *dict;
|
||||
|
||||
lang = getlang(LangEMOJI);
|
||||
saved = lang->dict;
|
||||
lang->dict = trienew();
|
||||
trieput(lang->dict, "é", strlen("é"), "é", strlen("é"));
|
||||
key = mkstr("é");
|
||||
if(CT_EQ_INT(t, 1, dictlookup(lang, &key, kouho, Maxkouho)))
|
||||
checkstr(t, "emoji identity candidate", "é", &kouho[0]);
|
||||
trieclose(lang->dict);
|
||||
lang->dict = saved;
|
||||
dict = trienew();
|
||||
trieput(dict, "smile", 5, "A B", 3);
|
||||
trieput(dict, "smiley", 6, "B C", 3);
|
||||
trieput(dict, "sad", 3, "D", 1);
|
||||
key = mkstr("smile");
|
||||
if(CT_EQ_INT(t, 4, dictprefix(dict, &key, kouho, Maxkouho))){
|
||||
checkstr(t, "own entry first", "A", &kouho[0]);
|
||||
checkstr(t, "entry below", "C", &kouho[3]);
|
||||
}
|
||||
key = mkstr("s");
|
||||
CT_EQ_INT(t, 5, dictprefix(dict, &key, kouho, Maxkouho));
|
||||
CT_EQ_INT(t, 2, dictprefix(dict, &key, kouho, 2));
|
||||
key = mkstr("x");
|
||||
CT_EQ_INT(t, 0, dictprefix(dict, &key, kouho, Maxkouho));
|
||||
trieclose(dict);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -189,10 +189,7 @@ font_render(struct ct *t)
|
||||
buf = mem + Guard;
|
||||
fillpixels(buf, Testn, Colbg);
|
||||
a = mkstr("A");
|
||||
if(!CT_CHECK(t, textinit())){
|
||||
free(mem);
|
||||
return;
|
||||
}
|
||||
textinit();
|
||||
for(i = 0; i < nelem(plain); i++)
|
||||
checktext(t, buf, plain[i]);
|
||||
|
||||
@@ -257,6 +254,4 @@ font_render(struct ct *t)
|
||||
checkguards(t, mem);
|
||||
free(mem);
|
||||
textclose();
|
||||
textclose();
|
||||
CT_EQ_INT(t, 0, textwidth(&a));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -437,73 +454,6 @@ runquery(char *module, char *cache)
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
startxvfb(pid_t *pidp, char *display, size_t ndisplay)
|
||||
{
|
||||
struct pollfd pfd;
|
||||
char fdarg[32], line[32];
|
||||
int p[2], n, status;
|
||||
pid_t pid;
|
||||
|
||||
if(pipe(p) < 0)
|
||||
return fail("Xvfb display pipe: %s", strerror(errno));
|
||||
pid = fork();
|
||||
if(pid < 0){
|
||||
close(p[0]);
|
||||
close(p[1]);
|
||||
return fail("fork Xvfb: %s", strerror(errno));
|
||||
}
|
||||
if(pid == 0){
|
||||
close(p[0]);
|
||||
snprintf(fdarg, sizeof fdarg, "%d", p[1]);
|
||||
execlp("Xvfb", "Xvfb", "-displayfd", fdarg, "-screen", "0",
|
||||
"1024x768x24", "-nolisten", "tcp", "-noreset", (char*)NULL);
|
||||
_exit(127);
|
||||
}
|
||||
close(p[1]);
|
||||
pfd.fd = p[0];
|
||||
pfd.events = POLLIN;
|
||||
if(poll(&pfd, 1, Timeout) <= 0){
|
||||
kill(pid, SIGTERM);
|
||||
waitpid(pid, NULL, 0);
|
||||
close(p[0]);
|
||||
return fail("Xvfb did not publish a display");
|
||||
}
|
||||
n = read(p[0], line, sizeof line - 1);
|
||||
close(p[0]);
|
||||
if(n <= 0){
|
||||
waitpid(pid, &status, 0);
|
||||
return fail("read Xvfb display number");
|
||||
}
|
||||
line[n] = '\0';
|
||||
line[strcspn(line, "\r\n")] = '\0';
|
||||
if(snprintf(display, ndisplay, ":%s", line) >= (int)ndisplay){
|
||||
kill(pid, SIGTERM);
|
||||
waitpid(pid, NULL, 0);
|
||||
return fail("Xvfb display number is too long");
|
||||
}
|
||||
*pidp = pid;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void
|
||||
stopxvfb(pid_t *pidp)
|
||||
{
|
||||
int status;
|
||||
int64_t deadline;
|
||||
|
||||
if(*pidp <= 0)
|
||||
return;
|
||||
kill(*pidp, SIGTERM);
|
||||
deadline = nowms() + Timeout;
|
||||
while(waitpid(*pidp, &status, WNOHANG) == 0 && leftms(deadline) > 0)
|
||||
g_usleep(10000);
|
||||
if(waitpid(*pidp, &status, WNOHANG) == 0){
|
||||
kill(*pidp, SIGKILL);
|
||||
waitpid(*pidp, &status, 0);
|
||||
}
|
||||
*pidp = -1;
|
||||
}
|
||||
|
||||
static void
|
||||
pump(void)
|
||||
@@ -580,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)
|
||||
{
|
||||
@@ -587,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);
|
||||
@@ -637,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
|
||||
@@ -645,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[] = {
|
||||
@@ -656,16 +646,17 @@ main(int argc, char **argv)
|
||||
static const int badresponses[] = { Rbadcommit, Rbadpreedit };
|
||||
char root[] = "/tmp/strans-gtk.XXXXXX";
|
||||
char runtime[320] = "", socket[384] = "", cache[384] = "";
|
||||
char display[32];
|
||||
char *module;
|
||||
pid_t xvfb;
|
||||
Daemon xvfb;
|
||||
Live l;
|
||||
int first, i, ox, oy, scale, closes, ok, rootmade, serverup;
|
||||
int64_t stalledms, started;
|
||||
|
||||
testname = "gtk_live_test";
|
||||
ctx = oldctx = badctx = NULL;
|
||||
top = NULL;
|
||||
xvfb = -1;
|
||||
memset(&l, 0, sizeof l);
|
||||
daemoninit(&xvfb, "Xvfb");
|
||||
rootmade = 0;
|
||||
serverup = 0;
|
||||
ok = 0;
|
||||
@@ -687,8 +678,8 @@ main(int argc, char **argv)
|
||||
Check(snprintf(cache, sizeof cache, "%s/immodules.cache", root) <
|
||||
(int)sizeof cache, "cache path is too long");
|
||||
Check(mkdir(runtime, 0700) == 0, "mkdir runtime: %s", strerror(errno));
|
||||
Check(startxvfb(&xvfb, display, sizeof display), "start private Xvfb");
|
||||
Check(setenv("DISPLAY", display, 1) == 0 &&
|
||||
Check(startxvfb(&l, &xvfb, "Xvfb"), "start private Xvfb");
|
||||
Check(setenv("DISPLAY", l.display, 1) == 0 &&
|
||||
setenv("GDK_BACKEND", "x11", 1) == 0 &&
|
||||
setenv("GDK_SCALE", "2", 1) == 0 &&
|
||||
setenv("XDG_RUNTIME_DIR", runtime, 1) == 0,
|
||||
@@ -714,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);
|
||||
@@ -729,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, "ㅋ");
|
||||
@@ -764,6 +758,26 @@ main(int argc, char **argv)
|
||||
Check(key(ctx, win, GDK_KEY_dead_acute) && key(ctx, win, GDK_KEY_e),
|
||||
"dead-key sequence was not consumed");
|
||||
Check(strcmp(log.commit, "é") == 0, "dead key committed %s", log.commit);
|
||||
|
||||
/* A dead key over a preedit hands the pending text back first. */
|
||||
setreply(&srv, 1, Rnormal, 0);
|
||||
setpre(&srv, "하");
|
||||
clearlog(&log);
|
||||
Check(key(ctx, win, GDK_KEY_h), "dead-key setup key was not eaten");
|
||||
clearevent(&log);
|
||||
first = eventcount(&srv);
|
||||
Check(!key(ctx, win, GDK_KEY_Shift_L), "a modifier press was eaten");
|
||||
Check(eventcount(&srv) == first && strcmp(log.pre, "하") == 0,
|
||||
"a modifier press disturbed the preedit");
|
||||
setreply(&srv, 0, Rnormal, 0);
|
||||
setpre(&srv, "");
|
||||
Check(key(ctx, win, GDK_KEY_dead_acute) && key(ctx, win, GDK_KEY_e),
|
||||
"dead key over a preedit was not consumed");
|
||||
Check(getevent(&srv, first).type == Ereset,
|
||||
"dead key did not hand the pending text back");
|
||||
Check(strcmp(log.commit, "é") == 0 && log.pre[0] == '\0',
|
||||
"dead key over a preedit committed %s (preedit %s)",
|
||||
log.commit, log.pre);
|
||||
clearlog(&log);
|
||||
Check(keystate(ctx, win, GDK_KEY_U, GDK_CONTROL_MASK|GDK_SHIFT_MASK),
|
||||
"Unicode prefix was not consumed");
|
||||
@@ -803,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");
|
||||
@@ -931,7 +1005,8 @@ out:
|
||||
rmdir(runtime);
|
||||
if(rootmade)
|
||||
rmdir(root);
|
||||
stopxvfb(&xvfb);
|
||||
stopdaemon(&xvfb);
|
||||
closeerrors(&xvfb);
|
||||
free(module);
|
||||
if(ok)
|
||||
printf("gtk_live_test: ok (scale %d, stalled peer %lld ms)\n",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -128,6 +128,8 @@ runcontract(char *address)
|
||||
"SetEngine",
|
||||
"SetCapabilities",
|
||||
"SetCursorLocation",
|
||||
"SetCursorLocationRelative",
|
||||
"SetSurroundingText",
|
||||
};
|
||||
DBusConnection *conn;
|
||||
char path[96];
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
#include <errno.h>
|
||||
|
||||
|
||||
enum
|
||||
{
|
||||
Testctrlmask = 1<<2,
|
||||
@@ -14,6 +13,8 @@ struct Ibusfix
|
||||
{
|
||||
Channel *oldreply;
|
||||
Pump pump;
|
||||
/* A fake connection is nothing but an address to tell two apart. */
|
||||
char fake[2];
|
||||
DBusConnection *c1;
|
||||
DBusConnection *c2;
|
||||
};
|
||||
@@ -53,6 +54,9 @@ ibus_machine_id_fallback(struct ct *t)
|
||||
path[1] = fallback;
|
||||
machineidfiles(got, sizeof got, path, nelem(path));
|
||||
CT_EQ_STR(t, "fallback-machine-id", got);
|
||||
path[1] = primary;
|
||||
machineidfiles(got, sizeof got, path, nelem(path));
|
||||
CT_EQ_STR(t, "", got);
|
||||
|
||||
old = getenv("IBUS_ADDRESS_FILE");
|
||||
saved = old == nil ? nil : strdup(old);
|
||||
@@ -79,87 +83,8 @@ cleanup:
|
||||
CT_CHECK(t, rmdir(root) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
ibus_startup_requires_ownership(struct ct *t)
|
||||
{
|
||||
char root[] = "/tmp/strans-ibus-startup.XXXXXX";
|
||||
char busdir[128], ibusdir[128], path[512], *old, *saved;
|
||||
struct stat st;
|
||||
int i, made, rv;
|
||||
|
||||
made = 0;
|
||||
busdir[0] = '\0';
|
||||
ibusdir[0] = '\0';
|
||||
path[0] = '\0';
|
||||
old = getenv("XDG_CONFIG_HOME");
|
||||
saved = old == nil ? nil : strdup(old);
|
||||
if(old != nil && !CT_CHECK(t, saved != nil))
|
||||
return;
|
||||
/* An IBus desktop names a live address file; the path must be ours. */
|
||||
unsetenv("IBUS_ADDRESS_FILE");
|
||||
if(!CT_CHECK(t, mkdtemp(root) != nil))
|
||||
goto cleanup;
|
||||
made = 1;
|
||||
if(!CT_CHECK(t, snprintf(busdir, sizeof busdir, "%s/ibus/bus", root)
|
||||
< (int)sizeof busdir) ||
|
||||
!CT_CHECK(t, snprintf(ibusdir, sizeof ibusdir, "%s/ibus", root)
|
||||
< (int)sizeof ibusdir) ||
|
||||
!CT_CHECK(t, setenv("XDG_CONFIG_HOME", root, 1) == 0) ||
|
||||
!CT_CHECK(t, buildaddrpath(path, sizeof path) == 0))
|
||||
goto cleanup;
|
||||
|
||||
memset(watches, 0, sizeof watches);
|
||||
for(i = 0; i < nelem(watches); i++)
|
||||
watches[i] = (DBusWatch*)1;
|
||||
nwatches = nelem(watches);
|
||||
rv = ibusinit();
|
||||
CT_EQ_INT(t, -1, rv);
|
||||
CT_EQ_PTR(t, nil, srv);
|
||||
CT_CHECK(t, lstat(path, &st) < 0 && errno == ENOENT);
|
||||
if(rv == 0){
|
||||
dbus_server_disconnect(srv);
|
||||
dbus_server_unref(srv);
|
||||
srv = nil;
|
||||
unlinkaddr();
|
||||
atexitdont(unlinkaddr);
|
||||
}
|
||||
|
||||
memset(watches, 0, sizeof watches);
|
||||
nwatches = 0;
|
||||
while(atexit(unlinkaddr))
|
||||
continue;
|
||||
CT_EQ_INT(t, -1, ibusinit());
|
||||
CT_EQ_PTR(t, nil, srv);
|
||||
CT_CHECK(t, lstat(path, &st) < 0 && errno == ENOENT);
|
||||
cleanup:
|
||||
if(srv != nil){
|
||||
dbus_server_disconnect(srv);
|
||||
dbus_server_unref(srv);
|
||||
srv = nil;
|
||||
}
|
||||
unlinkaddr();
|
||||
atexitdont(unlinkaddr);
|
||||
memset(watches, 0, sizeof watches);
|
||||
nwatches = 0;
|
||||
addrfile[0] = '\0';
|
||||
if(saved != nil){
|
||||
setenv("XDG_CONFIG_HOME", saved, 1);
|
||||
free(saved);
|
||||
}else
|
||||
unsetenv("XDG_CONFIG_HOME");
|
||||
if(made){
|
||||
if(path[0] != '\0')
|
||||
unlink(path);
|
||||
if(busdir[0] != '\0')
|
||||
CT_CHECK(t, rmdir(busdir) == 0 || errno == ENOENT);
|
||||
if(ibusdir[0] != '\0')
|
||||
CT_CHECK(t, rmdir(ibusdir) == 0 || errno == ENOENT);
|
||||
CT_CHECK(t, rmdir(root) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
ibusbegin(struct ct *t, Ibusfix *f)
|
||||
static void
|
||||
ibusbegin(Ibusfix *f)
|
||||
{
|
||||
Drawcmd dc;
|
||||
|
||||
@@ -174,9 +99,8 @@ ibusbegin(struct ct *t, Ibusfix *f)
|
||||
f->oldreply = replyc;
|
||||
replyc = chancreate(sizeof(Keyres), 0);
|
||||
pumpstart(&f->pump, Maxcontexts);
|
||||
f->c1 = malloc(1);
|
||||
f->c2 = malloc(1);
|
||||
return CT_CHECK(t, f->c1 != nil && f->c2 != nil);
|
||||
f->c1 = (DBusConnection*)&f->fake[0];
|
||||
f->c2 = (DBusConnection*)&f->fake[1];
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -196,8 +120,6 @@ ibusend(Ibusfix *f)
|
||||
testengineinit(LangEN);
|
||||
while(channbrecv(drawc, &dc) > 0)
|
||||
;
|
||||
free(f->c1);
|
||||
free(f->c2);
|
||||
chanfree(replyc);
|
||||
replyc = f->oldreply;
|
||||
}
|
||||
@@ -213,7 +135,7 @@ nexttrace(struct ct *t, Ibusfix *f, int op, Ictx *ctx)
|
||||
CT_EQ_INT(t, op, req.op);
|
||||
CT_EQ_PTR(t, ctx, req.owner);
|
||||
if(ctx != nil)
|
||||
CT_EQ_INT(t, ctx->cap & 1 ? Cclientpreedit : 0, req.cap);
|
||||
CT_EQ_INT(t, clientpreedit(ctx), req.clientpre);
|
||||
CT_EQ_PTR(t, replyc, req.reply);
|
||||
return req;
|
||||
}
|
||||
@@ -230,9 +152,10 @@ static Keyres
|
||||
contextkey(struct ct *t, Ibusfix *f, Ictx *ctx, u32int sym, u32int state)
|
||||
{
|
||||
Keyres res;
|
||||
char text[Maxutf];
|
||||
|
||||
memset(&res, 0, sizeof res);
|
||||
CT_CHECK(t, processkey(ctx, sym, state, &res));
|
||||
CT_CHECK(t, processkey(ctx, sym, state, text, sizeof text, &res));
|
||||
nexttrace(t, f, Keypress, ctx);
|
||||
return res;
|
||||
}
|
||||
@@ -244,8 +167,7 @@ ibus_capability_policy(struct ct *t)
|
||||
Ictx *a, *b;
|
||||
Keyres res;
|
||||
|
||||
if(!ibusbegin(t, &f))
|
||||
goto cleanup;
|
||||
ibusbegin(&f);
|
||||
a = newcontext(f.c1, "/context/cap-a");
|
||||
b = newcontext(f.c2, "/context/cap-b");
|
||||
if(!CT_CHECK(t, a != nil && b != nil))
|
||||
@@ -258,7 +180,7 @@ ibus_capability_policy(struct ct *t)
|
||||
checkstr(t, "preedit", "k", &res.preedit);
|
||||
CT_EQ_PTR(t, a, testengineowner());
|
||||
|
||||
a->cap = Cclientpreedit;
|
||||
a->cap = Ibuscappreedit;
|
||||
memset(&res, 0, sizeof res);
|
||||
sendrequest(a, Keycap, 0, 0, &res);
|
||||
nexttrace(t, &f, Keycap, a);
|
||||
@@ -266,7 +188,7 @@ ibus_capability_policy(struct ct *t)
|
||||
checkstr(t, "preedit", "k", &res.preedit);
|
||||
|
||||
b->focused = 1;
|
||||
b->cap = Cclientpreedit;
|
||||
b->cap = Ibuscappreedit;
|
||||
memset(&res, 0, sizeof res);
|
||||
sendrequest(b, Keycap, 0, 0, &res);
|
||||
nexttrace(t, &f, Keycap, b);
|
||||
@@ -289,17 +211,17 @@ 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;
|
||||
Ictx *ctx;
|
||||
Keyres res;
|
||||
char text[Maxutf];
|
||||
int i;
|
||||
|
||||
if(!ibusbegin(t, &f))
|
||||
goto cleanup;
|
||||
ibusbegin(&f);
|
||||
ctx = newcontext(f.c1, "/context/private");
|
||||
if(!CT_CHECK(t, ctx != nil))
|
||||
goto cleanup;
|
||||
@@ -309,14 +231,14 @@ ibus_private_input_policy(struct ct *t)
|
||||
}
|
||||
|
||||
ctx->focused = 1;
|
||||
ctx->cap = Cclientpreedit;
|
||||
ctx->cap = 1;
|
||||
ctx->purpose = 0;
|
||||
contextkey(t, &f, ctx, 'n', Testctrlmask);
|
||||
res = contextkey(t, &f, ctx, 'k', 0);
|
||||
checkstr(t, "preedit", "k", &res.preedit);
|
||||
ctx->purpose = Ibuspurposepassword;
|
||||
ctx->purpose = Purposepassword;
|
||||
memset(&res, 0, sizeof res);
|
||||
CT_CHECK(t, !processkey(ctx, 'x', 0, &res));
|
||||
CT_CHECK(t, !processkey(ctx, 'x', 0, text, sizeof text, &res));
|
||||
CT_CHECK(t, !res.eaten);
|
||||
notrace(t, &f);
|
||||
checkenginepreedit(t, "k");
|
||||
@@ -347,9 +269,9 @@ ibus_context_lifecycle(struct ct *t)
|
||||
Keyreq req;
|
||||
Keyres res;
|
||||
Caret at;
|
||||
char text[Maxutf];
|
||||
|
||||
if(!ibusbegin(t, &f))
|
||||
goto cleanup;
|
||||
ibusbegin(&f);
|
||||
a = newcontext(f.c1, "/context/one");
|
||||
b = newcontext(f.c2, "/context/one");
|
||||
if(!CT_CHECK(t, a != nil && b != nil && a != b))
|
||||
@@ -358,25 +280,21 @@ ibus_context_lifecycle(struct ct *t)
|
||||
CT_EQ_PTR(t, b, findcontext(f.c2, "/context/one"));
|
||||
CT_EQ_PTR(t, nil, findcontext(f.c1, "/context/missing"));
|
||||
|
||||
memset(&res, 0, sizeof res);
|
||||
CT_CHECK(t, !processkey(a, 'x', 0, &res));
|
||||
CT_CHECK(t, !res.eaten);
|
||||
notrace(t, &f);
|
||||
CT_EQ_PTR(t, nil, testengineowner());
|
||||
|
||||
setcursor(a, -10, -20, 14);
|
||||
notrace(t, &f);
|
||||
CT_CHECK(t, a->caret.valid);
|
||||
a->focused = 1;
|
||||
memset(&res, 0, sizeof res);
|
||||
CT_CHECK(t, !processkey(a, 'x', Relmask, &res));
|
||||
CT_CHECK(t, !processkey(a, 'x', Relmask, text, sizeof text, &res));
|
||||
CT_CHECK(t, !res.eaten);
|
||||
notrace(t, &f);
|
||||
CT_CHECK(t, !a->focused);
|
||||
CT_EQ_PTR(t, nil, testengineowner());
|
||||
|
||||
/* A key stands for the FocusIn a client may never send. */
|
||||
memset(&res, 0, sizeof res);
|
||||
CT_CHECK(t, processkey(a, 'x', 0, &res));
|
||||
CT_CHECK(t, processkey(a, 'x', 0, text, sizeof text, &res));
|
||||
req = nexttrace(t, &f, Keypress, a);
|
||||
CT_CHECK(t, a->focused);
|
||||
CT_CHECK(t, !res.eaten);
|
||||
CT_CHECK(t, req.caret.valid);
|
||||
CT_EQ_INT(t, -10, req.caret.x);
|
||||
@@ -450,7 +368,7 @@ ibus_context_lifecycle(struct ct *t)
|
||||
notrace(t, &f);
|
||||
|
||||
memset(&res, 0, sizeof res);
|
||||
CT_CHECK(t, !processkey(b, 'x', Relmask, &res));
|
||||
CT_CHECK(t, !processkey(b, 'x', Relmask, text, sizeof text, &res));
|
||||
CT_CHECK(t, !res.eaten);
|
||||
notrace(t, &f);
|
||||
CT_EQ_PTR(t, b, testengineowner());
|
||||
@@ -465,8 +383,7 @@ ibus_active_release_lifecycle(struct ct *t)
|
||||
Ictx *ctx, *reused;
|
||||
Keyres res;
|
||||
|
||||
if(!ibusbegin(t, &f))
|
||||
goto cleanup;
|
||||
ibusbegin(&f);
|
||||
ctx = newcontext(f.c1, "/context/reset");
|
||||
if(!CT_CHECK(t, ctx != nil))
|
||||
goto cleanup;
|
||||
@@ -507,12 +424,8 @@ ibus_active_release_lifecycle(struct ct *t)
|
||||
reused = newcontext(f.c1, "/context/prune");
|
||||
CT_EQ_PTR(t, ctx, reused);
|
||||
CT_EQ_PTR(t, nil, testengineowner());
|
||||
memset(&res, 0, sizeof res);
|
||||
CT_CHECK(t, !processkey(reused, 'x', 0, &res));
|
||||
CT_CHECK(t, !res.eaten);
|
||||
notrace(t, &f);
|
||||
reused->focused = 1;
|
||||
contextkey(t, &f, reused, 'n', 0);
|
||||
CT_CHECK(t, reused->focused);
|
||||
CT_EQ_PTR(t, reused, testengineowner());
|
||||
dropconncontexts(f.c1);
|
||||
nexttrace(t, &f, Keyrelease, reused);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
@@ -161,7 +202,7 @@ tryclient(char *path, int64_t deadline, int *client)
|
||||
fd = connectsocket(path, deadline);
|
||||
if(fd < 0)
|
||||
return errno == ETIMEDOUT ? -1 : 0;
|
||||
if(!sendkey(fd, 1, 0, Kmodfirst)){
|
||||
if(!sendkey(fd, 1, 0, 0)){
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
@@ -208,7 +249,7 @@ overflowrejected(char *path)
|
||||
fd = connectsocket(path, deadline);
|
||||
if(fd < 0)
|
||||
return fail("overflow connect: %s", strerror(errno));
|
||||
if(!sendkey(fd, 1, 0, Kmodfirst)){
|
||||
if(!sendkey(fd, 1, 0, 0)){
|
||||
close(fd);
|
||||
return 1;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -286,7 +328,7 @@ runcapacity(char *path)
|
||||
fail("connect capacity client %d: %s", i, strerror(errno));
|
||||
goto out;
|
||||
}
|
||||
if(!requestkey(client[i], 1, 0, Kmodfirst, 0, "", "",
|
||||
if(!requestkey(client[i], 1, 0, 0, 0, "", "",
|
||||
"capacity modifier"))
|
||||
goto out;
|
||||
}
|
||||
|
||||
@@ -55,10 +55,12 @@ ipc_masks_modifiers(struct ct *t)
|
||||
CT_EQ_UINT(t, Kret, ipckeysym(0xff0d, '\r'));
|
||||
CT_EQ_UINT(t, Kret, ipckeysym(0xff8d, '\r')); /* KP_Enter */
|
||||
CT_EQ_UINT(t, Ktab, ipckeysym(0xfe20, 0)); /* ISO_Left_Tab */
|
||||
CT_EQ_UINT(t, Kdown, ipckeysym(0xff99, 0)); /* KP_Down */
|
||||
CT_EQ_UINT(t, Kback, ipckeysym(0xff08, 8));
|
||||
CT_EQ_UINT(t, Kspec + 0xff, ipckeysym(0xffff, 0x7f)); /* Delete */
|
||||
CT_EQ_UINT(t, 0x1f642, ipckeysym(0x101f642, 0x1f642));
|
||||
CT_EQ_UINT(t, 0, ipckeysym(0xfe03, 0)); /* ISO_Level3_Shift */
|
||||
CT_EQ_UINT(t, 0, ipckeysym(0xffe1, 0)); /* Shift_L */
|
||||
CT_EQ_UINT(t, Mshift|Mctrl, ipcmod(Mshift|Mctrl|(1<<1)|(1<<4)));
|
||||
CT_EQ_UINT(t, Msuper, ipcmod(1<<6));
|
||||
CT_EQ_UINT(t, Msuper, ipcmod(1<<26));
|
||||
@@ -68,7 +70,7 @@ void
|
||||
ipc_control_and_caret_frames(struct ct *t)
|
||||
{
|
||||
static const uchar capon[Ipcreqsz] = {
|
||||
Ipcext|Cclientpreedit, Ipcversion, Ipcopcap, 0, 0, 0,
|
||||
Ipcext|Ipcreqwant, Ipcversion, Ipcopcap, 0, 0, 0,
|
||||
};
|
||||
static const uchar capoff[Ipcreqsz] = {
|
||||
Ipcext, Ipcversion, Ipcopcap, 0, 0, 0,
|
||||
@@ -87,7 +89,7 @@ ipc_control_and_caret_frames(struct ct *t)
|
||||
int valid, want;
|
||||
int32_t x, y, h;
|
||||
|
||||
ipcpackcap(buf, Cclientpreedit);
|
||||
ipcpackcap(buf, 1);
|
||||
CT_EQ_MEM(t, capon, buf, sizeof capon);
|
||||
CT_EQ_INT(t, Ipccap, ipcreqtype(buf));
|
||||
/* The probe remains a harmless key-zero request to an old daemon. */
|
||||
@@ -175,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
|
||||
@@ -213,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;
|
||||
@@ -250,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){
|
||||
@@ -283,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){
|
||||
@@ -297,6 +302,7 @@ ipc_response_fragmented_and_truncated(struct ct *t)
|
||||
}
|
||||
if(i == n && CT_EQ_INT(t, 0, ipcreadresp(fd[1], 1, commit, preedit,
|
||||
&resp))){
|
||||
CT_EQ_INT(t, 2, resp.del);
|
||||
CT_EQ_STR(t, "abc", commit);
|
||||
CT_EQ_STR(t, "xy", preedit);
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
#include "dat.h"
|
||||
#include "fn.h"
|
||||
#include "test.h"
|
||||
#include <xcb-imdkit/encoding.h>
|
||||
#include <xkbcommon/xkbcommon.h>
|
||||
#include <xkbcommon/xkbcommon-keysyms.h>
|
||||
|
||||
uint32_t keymaplookup(struct xkb_state*, uint8_t, uint16_t);
|
||||
|
||||
enum
|
||||
{
|
||||
ShiftMask = 1<<0,
|
||||
LockMask = 1<<1,
|
||||
Mod2Mask = 1<<4,
|
||||
Mod5Mask = 1<<7,
|
||||
Group1 = 1<<13,
|
||||
};
|
||||
|
||||
void
|
||||
xim_keymap_lookup(struct ct *t)
|
||||
{
|
||||
struct xkb_context *context;
|
||||
struct xkb_keymap *keymap;
|
||||
struct xkb_rule_names names;
|
||||
struct xkb_state *state;
|
||||
|
||||
memset(&names, 0, sizeof names);
|
||||
names.layout = "de,ru";
|
||||
context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
|
||||
keymap = xkb_keymap_new_from_names(context, &names,
|
||||
XKB_KEYMAP_COMPILE_NO_FLAGS);
|
||||
if(!CT_CHECK(t, keymap != nil)){
|
||||
xkb_context_unref(context);
|
||||
return;
|
||||
}
|
||||
state = xkb_state_new(keymap);
|
||||
CT_EQ_UINT(t, XKB_KEY_a, keymaplookup(state, 38, 0));
|
||||
CT_EQ_UINT(t, XKB_KEY_A, keymaplookup(state, 38, ShiftMask));
|
||||
CT_EQ_UINT(t, XKB_KEY_A, keymaplookup(state, 38, LockMask));
|
||||
CT_EQ_UINT(t, XKB_KEY_a, keymaplookup(state, 38, LockMask|ShiftMask));
|
||||
CT_EQ_UINT(t, XKB_KEY_EuroSign, keymaplookup(state, 26, Mod5Mask));
|
||||
CT_EQ_UINT(t, XKB_KEY_Cyrillic_u, keymaplookup(state, 26, Group1));
|
||||
CT_EQ_UINT(t, XKB_KEY_KP_End, keymaplookup(state, 87, 0));
|
||||
CT_EQ_UINT(t, XKB_KEY_KP_1, keymaplookup(state, 87, Mod2Mask));
|
||||
xkb_state_unref(state);
|
||||
xkb_keymap_unref(keymap);
|
||||
xkb_context_unref(context);
|
||||
}
|
||||
|
||||
/* imdkit carries any UTF-8 text through COMPOUND_TEXT unchanged. */
|
||||
void
|
||||
xim_compound_text(struct ct *t)
|
||||
{
|
||||
static char *text[] = { "A😀한", "한글", "𠀋", "👩💻" };
|
||||
char *ct, *utf8;
|
||||
size_t clen, len, ulen;
|
||||
int i;
|
||||
|
||||
xcb_compound_text_init();
|
||||
for(i = 0; i < nelem(text); i++){
|
||||
len = strlen(text[i]);
|
||||
ct = xcb_utf8_to_compound_text(text[i], len, &clen);
|
||||
if(!CT_CHECK(t, ct != nil))
|
||||
continue;
|
||||
utf8 = xcb_compound_text_to_utf8(ct, clen, &ulen);
|
||||
if(CT_CHECK(t, utf8 != nil)){
|
||||
CT_EQ_SIZE(t, len, ulen);
|
||||
CT_EQ_MEM(t, text[i], utf8, len);
|
||||
}
|
||||
free(utf8);
|
||||
free(ct);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ kotrans(char *keys)
|
||||
Str out, raw;
|
||||
|
||||
raw = mkstr(keys);
|
||||
transstr(getlang(LangKO), &raw, &out);
|
||||
transstr(getlang(LangKO), nil, &raw, &out);
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,12 @@ korean_sequences(struct ct *t)
|
||||
{ "rr", "ㄱㄱ" },
|
||||
{ "Rk", "까" },
|
||||
{ "rk1", "가1" },
|
||||
{ "rt", "ㄳ" },
|
||||
{ "rtk", "ㄱ사" },
|
||||
{ "rtt", "ㄳㅅ" },
|
||||
{ "kr", "가" },
|
||||
{ "hkr", "과" },
|
||||
{ "kk", "ㅏㅏ" },
|
||||
};
|
||||
Str out;
|
||||
int i;
|
||||
@@ -81,6 +87,7 @@ korean_backspace(struct ct *t)
|
||||
{
|
||||
static const struct { char *before, *after; } cases[] = {
|
||||
{ "ㄱ", "" },
|
||||
{ "ㄳ", "ㄱ" },
|
||||
{ "가", "ㄱ" },
|
||||
{ "과", "고" }, { "괘", "고" }, { "괴", "고" },
|
||||
{ "궈", "구" }, { "궤", "구" }, { "귀", "구" },
|
||||
|
||||
87
tests/live.c
87
tests/live.c
@@ -240,6 +240,81 @@ childfds(int fd)
|
||||
return dup2(fd, STDOUT_FILENO) >= 0 && dup2(fd, STDERR_FILENO) >= 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* A private X server for the tests that need one; its display goes in
|
||||
* Live, so the daemon and the clients started after it share it.
|
||||
*/
|
||||
int
|
||||
startxvfb(Live *l, Daemon *d, char *program)
|
||||
{
|
||||
struct pollfd pfd;
|
||||
char fdarg[16], line[32];
|
||||
int errpipe[2], n, pipefd[2];
|
||||
int64_t deadline;
|
||||
pid_t pid;
|
||||
ssize_t nr;
|
||||
|
||||
if(pipe2(pipefd, O_CLOEXEC) < 0)
|
||||
return fail("Xvfb display pipe: %s", strerror(errno));
|
||||
if(pipe2(errpipe, O_CLOEXEC|O_NONBLOCK) < 0){
|
||||
close(pipefd[0]);
|
||||
close(pipefd[1]);
|
||||
return fail("pipe2: %s", strerror(errno));
|
||||
}
|
||||
pid = fork();
|
||||
if(pid < 0){
|
||||
close(pipefd[0]);
|
||||
close(pipefd[1]);
|
||||
close(errpipe[0]);
|
||||
close(errpipe[1]);
|
||||
return fail("fork Xvfb: %s", strerror(errno));
|
||||
}
|
||||
if(pid == 0){
|
||||
close(pipefd[0]);
|
||||
close(errpipe[0]);
|
||||
n = fcntl(pipefd[1], F_GETFD);
|
||||
if(n < 0 || fcntl(pipefd[1], F_SETFD, n & ~FD_CLOEXEC) < 0 ||
|
||||
!childfds(errpipe[1]))
|
||||
_exit(126);
|
||||
snprintf(fdarg, sizeof fdarg, "%d", pipefd[1]);
|
||||
execlp(program, program, "-displayfd", fdarg, "-screen", "0",
|
||||
"1024x768x24", "-nolisten", "tcp", "-noreset", (char*)0);
|
||||
_exit(127);
|
||||
}
|
||||
close(pipefd[1]);
|
||||
close(errpipe[1]);
|
||||
d->pid = pid;
|
||||
d->errfd = errpipe[0];
|
||||
pfd.fd = pipefd[0];
|
||||
pfd.events = POLLIN|POLLHUP;
|
||||
deadline = nowms() + Starttimeout;
|
||||
n = 0;
|
||||
while(n + 1 < (int)sizeof line){
|
||||
pfd.revents = 0;
|
||||
if(poll(&pfd, 1, leftms(deadline)) <= 0)
|
||||
break;
|
||||
nr = read(pipefd[0], line + n, sizeof line - n - 1);
|
||||
if(nr < 0 && errno == EINTR)
|
||||
continue;
|
||||
if(nr <= 0)
|
||||
break;
|
||||
n += nr;
|
||||
line[n] = '\0';
|
||||
if(strchr(line, '\n') != NULL)
|
||||
break;
|
||||
}
|
||||
close(pipefd[0]);
|
||||
if(n == 0 || strchr(line, '\n') == NULL)
|
||||
return fail("Xvfb did not report a private display");
|
||||
line[strcspn(line, "\r\n")] = '\0';
|
||||
if(strspn(line, "0123456789") != strlen(line))
|
||||
return fail("invalid Xvfb display number %s", line);
|
||||
if(snprintf(l->display, sizeof l->display, ":%s", line)
|
||||
>= (int)sizeof l->display)
|
||||
return fail("Xvfb display number is too long");
|
||||
return 1;
|
||||
}
|
||||
|
||||
int
|
||||
startdaemon(Live *l, Daemon *d, char *program, char *arg)
|
||||
{
|
||||
@@ -268,7 +343,10 @@ startdaemon(Live *l, Daemon *d, char *program, char *arg)
|
||||
}
|
||||
if(setenv("XDG_RUNTIME_DIR", l->runtime, 1) < 0 ||
|
||||
setenv("XDG_CONFIG_HOME", l->config, 1) < 0 ||
|
||||
setenv("HOME", l->home, 1) < 0 || unsetenv("DISPLAY") < 0 ||
|
||||
setenv("HOME", l->home, 1) < 0 ||
|
||||
(l->display[0] != '\0' ?
|
||||
setenv("DISPLAY", l->display, 1) :
|
||||
unsetenv("DISPLAY")) < 0 ||
|
||||
unsetenv("DBUS_SESSION_BUS_ADDRESS") < 0 ||
|
||||
unsetenv("IBUS_ADDRESS") < 0 ||
|
||||
unsetenv("IBUS_ADDRESS_FILE") < 0){
|
||||
@@ -393,7 +471,7 @@ stopdaemon(Daemon *d)
|
||||
}
|
||||
/* plan9port turns a caught termination note into exit status 1. */
|
||||
if(reaped && !((WIFSIGNALED(status) && WTERMSIG(status) == SIGTERM) ||
|
||||
(WIFEXITED(status) && WEXITSTATUS(status) == 1)))
|
||||
(WIFEXITED(status) && WEXITSTATUS(status) <= 1)))
|
||||
ok = fail("%s exited with unexpected wait status %#x", d->name,
|
||||
status);
|
||||
readerrors(d);
|
||||
@@ -689,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, Kmodfirst, empty, sizeof empty, where);
|
||||
return ipcrequest(fd, 0, 0, empty, sizeof empty, where);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ struct Live
|
||||
char home[320];
|
||||
char socket[384];
|
||||
char addrfile[512];
|
||||
char display[32];
|
||||
};
|
||||
|
||||
extern char *testname;
|
||||
@@ -54,6 +55,7 @@ int livesetup(Live*, char*);
|
||||
int liveclean(Live*);
|
||||
|
||||
void daemoninit(Daemon*, char*);
|
||||
int startxvfb(Live*, Daemon*, char*);
|
||||
int startdaemon(Live*, Daemon*, char*, char*);
|
||||
int waitready(Live*, Daemon*, char*, size_t);
|
||||
int daemonalive(Daemon*);
|
||||
|
||||
@@ -12,10 +12,8 @@ MKEMOJI = ROOT / "map" / "mkemoji"
|
||||
TIMEOUT = 10
|
||||
|
||||
|
||||
def generate(source=None):
|
||||
args = [sys.executable, "-B", str(MKEMOJI)]
|
||||
if source is not None:
|
||||
args.append(str(source))
|
||||
def generate(*sources):
|
||||
args = [sys.executable, "-B", str(MKEMOJI)] + [str(s) for s in sources]
|
||||
return subprocess.run(
|
||||
args, capture_output=True, text=True, check=False, timeout=TIMEOUT
|
||||
)
|
||||
@@ -37,27 +35,31 @@ class MkemojiTest(unittest.TestCase):
|
||||
result = generate()
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
data = table(result.stdout)
|
||||
self.assertEqual(data["^"].split()[:9], list("¹²³⁴⁵⁶⁷⁸⁹"))
|
||||
self.assertEqual(data["_"].split()[:9], list("₁₂₃₄₅₆₇₈₉"))
|
||||
# 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"], "♥")
|
||||
self.assertEqual(data["<3"].split()[0], "♥")
|
||||
self.assertIn("😀", data["smile"].split())
|
||||
self.assertIn("😀", data["웃음"].split())
|
||||
|
||||
def test_fold_normalize_and_exact_first(self):
|
||||
def test_fold_normalize_and_source_order(self):
|
||||
source = self.source(
|
||||
"β\tALPHABET\n"
|
||||
"β\tALPHABET\talpha\n"
|
||||
"α\talpha\n"
|
||||
"e\u0301\tE\u0301\n"
|
||||
"é\tÉ\n"
|
||||
"#\thash\n"
|
||||
)
|
||||
first = generate(source)
|
||||
second = generate(source)
|
||||
second = generate(source, source)
|
||||
self.assertEqual(first.returncode, 0, first.stderr)
|
||||
self.assertEqual(first.stdout, second.stdout)
|
||||
data = table(first.stdout)
|
||||
self.assertEqual(data["alpha"].split(), ["α", "β"])
|
||||
self.assertEqual(data["al"].split(), ["β", "α"])
|
||||
self.assertEqual(list(data), ["alphabet", "alpha", "é", "hash"])
|
||||
self.assertEqual(data["alpha"].split(), ["β", "α"])
|
||||
self.assertEqual(data["é"], "é")
|
||||
self.assertEqual(data["hash"], "#")
|
||||
|
||||
|
||||
@@ -42,25 +42,45 @@ class MkhanjaTest(unittest.TestCase):
|
||||
self.addCleanup(tmp.cleanup)
|
||||
return path
|
||||
|
||||
def test_imports_single_bmp_hanja_rows(self):
|
||||
def test_imports_bmp_hanja_rows(self):
|
||||
result = run(
|
||||
IMPORT,
|
||||
self.source(
|
||||
"# Copyright holder\n"
|
||||
"# BSD license\n"
|
||||
"한:漢:first\n"
|
||||
"가:㐀:extension A\n"
|
||||
"김:金:compatibility\n"
|
||||
"방학:放:word reading\n"
|
||||
"학:學校:word value\n"
|
||||
"한:𠀀:astral\n"
|
||||
"ㄱ:加:jamo\n"
|
||||
"\ud55c:\u6f22:first\n"
|
||||
"\uac00:\u3400:extension A\n"
|
||||
"\uae40:\u91d1:compatibility\n"
|
||||
"\ud55c\uc790:\u6f22\u5b57:word\n"
|
||||
"\ud55c:\U00020000:astral\n"
|
||||
"\u3131:\u52a0:jamo\n"
|
||||
"\ud55c\uae00:\u97d3glyph:mixed\n"
|
||||
),
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
lines = result.stdout.splitlines()
|
||||
self.assertEqual(lines[:2], ["# Copyright holder", "# BSD license"])
|
||||
self.assertEqual(lines[2:], ["", "漢\t한", "㐀\t가", "金\t김"])
|
||||
self.assertEqual(lines[2:], ["", "\u6f22\t\ud55c", "\u3400\t\uac00",
|
||||
"\u91d1\t\uae40",
|
||||
"\u6f22\u5b57\t\ud55c\uc790"])
|
||||
|
||||
def test_imports_symbol_rows_under_a_jamo(self):
|
||||
result = run(
|
||||
IMPORT,
|
||||
self.source(
|
||||
"\u3141:\u203b:reference mark\n"
|
||||
"\u3134:\u300c:bracket\n"
|
||||
"\u3131:\u3000:ideographic space\n"
|
||||
"\u3131:\u00ad:soft hyphen\n"
|
||||
"\u3131:\u52a0:Hanja under a jamo\n"
|
||||
"\u3141:\u203b\u203b:two runes\n"
|
||||
"\u3141\u3134:\u203b:two jamo\n"
|
||||
"\u314f:\u203b:a vowel\n"
|
||||
),
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(result.stdout.splitlines(),
|
||||
["\u203b\t\u3141", "\u300c\t\u3134"])
|
||||
|
||||
def test_import_rejects_malformed_and_duplicate_rows(self):
|
||||
bad = [
|
||||
@@ -91,6 +111,7 @@ class MkhanjaTest(unittest.TestCase):
|
||||
"㐀\t가\n"
|
||||
"家\t가\n"
|
||||
"金\t금\n"
|
||||
"漢字\t한자\n"
|
||||
),
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
@@ -103,16 +124,36 @@ class MkhanjaTest(unittest.TestCase):
|
||||
"한\t漢 韓",
|
||||
"가\t㐀 家",
|
||||
"금\t金",
|
||||
"한자\t漢字",
|
||||
],
|
||||
)
|
||||
|
||||
def test_groups_symbols_under_their_jamo(self):
|
||||
result = run(
|
||||
GENERATE,
|
||||
self.source(
|
||||
"※\tㅁ\n"
|
||||
"○\tㅁ\n"
|
||||
"「\tㄴ\n"
|
||||
),
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(result.stdout.splitlines(),
|
||||
["ㅁ\t※ ○", "ㄴ\t「"])
|
||||
|
||||
def test_generator_rejects_bad_rows(self):
|
||||
bad = [
|
||||
"漢韓\t한\n",
|
||||
"漢\t한자\n",
|
||||
"漢a\t한\n",
|
||||
"漢\t한a\n",
|
||||
"𠀀\t한\n",
|
||||
"漢\t한\n漢\t한\n",
|
||||
"漢 한\n",
|
||||
"加\tㅁ\n",
|
||||
"※\t한\n",
|
||||
"※\tㅁㄴ\n",
|
||||
"※※\tㅁ\n",
|
||||
"\u3000\tㄱ\n",
|
||||
"\u00ad\tㄱ\n",
|
||||
]
|
||||
for text in bad:
|
||||
with self.subTest(text=repr(text)):
|
||||
@@ -127,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))
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
#include "fn.h"
|
||||
#include "test.h"
|
||||
|
||||
/* A popup with every section, tall enough for the tests. */
|
||||
#define Imgh (2*PopupPad + (Maxdisp + 2)*Fontsz + PopupSep)
|
||||
|
||||
enum {
|
||||
Rgbmask = 0xffffff,
|
||||
};
|
||||
@@ -40,9 +43,6 @@ checkpadding(struct ct *t, u32int *img, Popup *p)
|
||||
void
|
||||
popup_layout(struct ct *t)
|
||||
{
|
||||
int tinyheight[] = {
|
||||
1, PopupPad, 2*PopupPad + Fontsz - 1,
|
||||
};
|
||||
static const struct {
|
||||
int first;
|
||||
int shown;
|
||||
@@ -132,8 +132,7 @@ popup_layout(struct ct *t)
|
||||
CT_EQ_INT(t, 100, x);
|
||||
CT_EQ_INT(t, 200, y);
|
||||
|
||||
if(!CT_CHECK(t, textinit()))
|
||||
return;
|
||||
textinit();
|
||||
|
||||
/* Empty sections reserve nothing. */
|
||||
memset(&dc, 0, sizeof dc);
|
||||
@@ -145,10 +144,10 @@ popup_layout(struct ct *t)
|
||||
CT_EQ_INT(t, -1, p.rowsy);
|
||||
CT_EQ_INT(t, -1, p.marky);
|
||||
|
||||
/* Preedit only. */
|
||||
/* Preedit only: as wide as its text. */
|
||||
dc.pre = mkstr("preedit");
|
||||
popuplayout(&dc, 2*PopupBasew, Imgh, &p);
|
||||
CT_EQ_INT(t, PopupBasew, p.w);
|
||||
CT_EQ_INT(t, textwidth(&dc.pre) + 2*PopupPad, p.w);
|
||||
CT_EQ_INT(t, PopupPad, p.prey);
|
||||
CT_EQ_INT(t, -1, p.sepy);
|
||||
CT_EQ_INT(t, -1, p.rowsy);
|
||||
@@ -320,24 +319,5 @@ popup_layout(struct ct *t)
|
||||
CT_CHECK(t, numink > 0); /* full-width 9 */
|
||||
CT_CHECK(t, markink > 0); /* 9-9/9 */
|
||||
|
||||
/* The selected row wins over preedit and footer in one-row height. */
|
||||
dc.pre = mkstr("preedit");
|
||||
popuplayout(&dc, PopupBasew, 2*PopupPad + Fontsz, &p);
|
||||
CT_EQ_INT(t, -1, p.prey);
|
||||
CT_EQ_INT(t, 1, p.n);
|
||||
CT_EQ_INT(t, Maxdisp - 1, p.row0);
|
||||
CT_EQ_INT(t, -1, p.marky);
|
||||
|
||||
/* With less than one padded row, selected content starts at the top. */
|
||||
for(i = 0; i < nelem(tinyheight); i++){
|
||||
popuplayout(&dc, PopupBasew, tinyheight[i], &p);
|
||||
CT_EQ_INT(t, min(tinyheight[i], Fontsz), p.h);
|
||||
CT_EQ_INT(t, 0, p.rowsy);
|
||||
CT_EQ_INT(t, 0, p.sely);
|
||||
CT_EQ_INT(t, 1, p.n);
|
||||
CT_EQ_INT(t, Maxdisp - 1, p.row0);
|
||||
popupdraw(img, &dc, &p);
|
||||
CT_EQ_UINT(t, Colsel, pixel(img, &p, p.w - PopupPad - 1, 0));
|
||||
}
|
||||
free(img);
|
||||
}
|
||||
|
||||
@@ -143,14 +143,14 @@ nextrequestcap(struct ct *t, Pump *p, int op, int cap)
|
||||
memset(&req, 0, sizeof req);
|
||||
chanrecv(p->trace, &req);
|
||||
CT_EQ_INT(t, op, req.op);
|
||||
CT_EQ_INT(t, cap, req.cap);
|
||||
CT_EQ_INT(t, cap, req.clientpre);
|
||||
return req;
|
||||
}
|
||||
|
||||
static Keyreq
|
||||
nextrequest(struct ct *t, Pump *p, int op)
|
||||
{
|
||||
return nextrequestcap(t, p, op, Cclientpreedit);
|
||||
return nextrequestcap(t, p, op, 1);
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -284,10 +284,11 @@ server_connection_ownership(struct ct *t)
|
||||
req = nextrequest(t, &f.pump, Keyreset);
|
||||
CT_EQ_PTR(t, aowner, req.owner);
|
||||
allowrequest(&f.pump);
|
||||
CT_CHECK(t, readreply(t, &a, 1, "", preedit));
|
||||
/* The reset hands back what the takeover took from this client. */
|
||||
CT_CHECK(t, readreply(t, &a, 1, "か", preedit));
|
||||
CT_EQ_STR(t, "", preedit);
|
||||
CT_EQ_PTR(t, bowner, testengineowner());
|
||||
if(!sendkey(t, &b, 1, 0, Kmodfirst))
|
||||
if(!sendkey(t, &b, 1, 0, 0))
|
||||
goto cleanup;
|
||||
req = nextrequest(t, &f.pump, Keypress);
|
||||
allowrequest(&f.pump);
|
||||
@@ -387,7 +388,7 @@ server_extension_stream(struct ct *t)
|
||||
goto cleanup;
|
||||
|
||||
/* Capability and caret frames may be split at any byte boundary. */
|
||||
ipcpackcap(frame, Cclientpreedit);
|
||||
ipcpackcap(frame, 1);
|
||||
if(!sendframe(t, &a, frame, Ipcreqsz, 1))
|
||||
goto cleanup;
|
||||
req = nextrequest(t, &f.pump, Keycap);
|
||||
@@ -446,7 +447,7 @@ server_extension_stream(struct ct *t)
|
||||
allowrequest(&f.pump);
|
||||
CT_EQ_INT(t, 1, readreply(t, &a, 0, "k", preedit));
|
||||
|
||||
ipcpackcap(frame, Cclientpreedit);
|
||||
ipcpackcap(frame, 1);
|
||||
if(!sendframe(t, &a, frame, Ipcreqsz, 0))
|
||||
goto cleanup;
|
||||
req = nextrequest(t, &f.pump, Keycap);
|
||||
|
||||
12
tests/test.h
12
tests/test.h
@@ -1,8 +1,6 @@
|
||||
/* Included after dat.h and fn.h (or after the source under test). */
|
||||
#include "cutest/cutest.h"
|
||||
|
||||
extern Lang testvi;
|
||||
|
||||
/*
|
||||
* A test's stand-in for imthread: handles engine requests from keyc,
|
||||
* tracing each one first. Requests whose op is holdop (every request
|
||||
@@ -88,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*);
|
||||
@@ -95,7 +95,7 @@ void engine_randomized_stress(struct ct*);
|
||||
void engine_full_boundary_passthrough(struct ct*);
|
||||
void dictionary_candidates(struct ct*);
|
||||
void dictionary_misses(struct ct*);
|
||||
void dictionary_emoji_identity(struct ct*);
|
||||
void dictionary_prefix(struct ct*);
|
||||
void ipc_masks_modifiers(struct ct*);
|
||||
void ipc_control_and_caret_frames(struct ct*);
|
||||
void ipc_runtime_path(struct ct*);
|
||||
@@ -108,11 +108,11 @@ void server_connection_ownership(struct ct*);
|
||||
void server_extension_stream(struct ct*);
|
||||
void server_rejects_unknown_extension(struct ct*);
|
||||
void ibus_machine_id_fallback(struct ct*);
|
||||
void ibus_startup_requires_ownership(struct ct*);
|
||||
void ibus_capability_policy(struct ct*);
|
||||
void ibus_private_input_policy(struct ct*);
|
||||
void ibus_context_lifecycle(struct ct*);
|
||||
void ibus_active_release_lifecycle(struct ct*);
|
||||
void compose_sequences(struct ct*);
|
||||
void xim_keymap_lookup(struct ct*);
|
||||
void xim_compound_text(struct ct*);
|
||||
void xim_adapter_key_contract(struct ct*);
|
||||
@@ -127,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*);
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
Channel *drawc;
|
||||
Channel *keyc;
|
||||
Lang testvi;
|
||||
|
||||
void
|
||||
die(char *fmt, ...)
|
||||
@@ -45,20 +44,16 @@ erealloc(void *p, ulong n)
|
||||
static void
|
||||
testmapinit(void)
|
||||
{
|
||||
Lang *jp, *kata;
|
||||
Lang *jp, *kata, *vi;
|
||||
|
||||
jp = getlang(LangJP);
|
||||
kata = getlang(LangJPK);
|
||||
if(jp == nil || kata == nil)
|
||||
vi = getlang(LangVI);
|
||||
if(jp == nil || kata == nil || vi == nil)
|
||||
die("test language is not registered");
|
||||
jp->map = trieopen("../map/hira.map");
|
||||
kata->map = trieopen("../map/kata.map");
|
||||
memset(&testvi, 0, sizeof testvi);
|
||||
testvi.lang = LangVI;
|
||||
testvi.mapname = "telex";
|
||||
testvi.trans = transvi;
|
||||
testvi.back = backvi;
|
||||
testvi.map = trieopen("../map/telex.map");
|
||||
vi->map = trieopen("../map/telex.map");
|
||||
}
|
||||
|
||||
#ifndef STRESS
|
||||
@@ -112,13 +107,15 @@ 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 },
|
||||
{ "engine/full-boundary-passthrough", engine_full_boundary_passthrough },
|
||||
{ "dict/candidates", dictionary_candidates },
|
||||
{ "dict/misses", dictionary_misses },
|
||||
{ "dict/emoji-identity", dictionary_emoji_identity },
|
||||
{ "dict/prefix", dictionary_prefix },
|
||||
{ "ipc/masks-modifiers", ipc_masks_modifiers },
|
||||
{ "ipc/control-caret-frames", ipc_control_and_caret_frames },
|
||||
{ "ipc/runtime-path", ipc_runtime_path },
|
||||
@@ -131,11 +128,11 @@ static const struct ct_test tests[] = {
|
||||
{ "server/extension-stream", server_extension_stream },
|
||||
{ "server/rejects-unknown-extension", server_rejects_unknown_extension },
|
||||
{ "ibus/machine-id-fallback", ibus_machine_id_fallback },
|
||||
{ "ibus/startup-requires-ownership", ibus_startup_requires_ownership },
|
||||
{ "ibus/capability-policy", ibus_capability_policy },
|
||||
{ "ibus/private-input-policy", ibus_private_input_policy },
|
||||
{ "ibus/context-lifecycle", ibus_context_lifecycle },
|
||||
{ "ibus/active-release-lifecycle", ibus_active_release_lifecycle },
|
||||
{ "compose/sequences", compose_sequences },
|
||||
{ "xim/keymap-lookup", xim_keymap_lookup },
|
||||
{ "xim/compound-text", xim_compound_text },
|
||||
{ "xim/adapter-key-contract", xim_adapter_key_contract },
|
||||
@@ -150,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[] = {
|
||||
@@ -170,8 +171,6 @@ threadmain(int argc, char **argv)
|
||||
trieclose(langs[i].map);
|
||||
langs[i].map = nil;
|
||||
}
|
||||
trieclose(testvi.map);
|
||||
testvi.map = nil;
|
||||
chanfree(drawc);
|
||||
chanfree(keyc);
|
||||
threadexitsall(status == 0 ? nil : "tests failed");
|
||||
|
||||
@@ -8,7 +8,7 @@ typevi(struct ct *t, char *keys, char *want)
|
||||
Str out, raw;
|
||||
|
||||
raw = mkstr(keys);
|
||||
transstr(&testvi, &raw, &out);
|
||||
transstr(getlang(LangVI), nil, &raw, &out);
|
||||
checkstr(t, keys, want, &out);
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ vietnamese_backspace(struct ct *t)
|
||||
|
||||
for(i = 0; i < nelem(cases); i++){
|
||||
memset(&state, 0, sizeof state);
|
||||
state.l = &testvi;
|
||||
state.l = getlang(LangVI);
|
||||
state.pre = mkstr(cases[i].pre);
|
||||
state.raw = mkstr(cases[i].raw);
|
||||
backvi(&state);
|
||||
|
||||
165
tests/wl_adapter_test.c
Normal file
165
tests/wl_adapter_test.c
Normal 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);
|
||||
}
|
||||
@@ -67,7 +67,7 @@ static int wireflush(xcb_connection_t*);
|
||||
#define xcb_translate_coordinates wiretranslate
|
||||
#define xcb_translate_coordinates_reply wiretranslatereply
|
||||
#define xcb_flush wireflush
|
||||
#include "xim/xim.c"
|
||||
#include "xim.c"
|
||||
#undef xcb_im_commit_string
|
||||
#undef xcb_im_preedit_start_callback
|
||||
#undef xcb_im_preedit_draw_callback
|
||||
@@ -398,8 +398,8 @@ struct Freejob
|
||||
Channel *done;
|
||||
};
|
||||
|
||||
static int
|
||||
ximbegin(struct ct *t, Ximfix *f, int lang)
|
||||
static void
|
||||
ximbegin(Ximfix *f, int lang)
|
||||
{
|
||||
Drawcmd dc;
|
||||
|
||||
@@ -410,8 +410,6 @@ ximbegin(struct ct *t, Ximfix *f, int lang)
|
||||
f->oldreply = replyc;
|
||||
replyc = chancreate(sizeof(Keyres), 0);
|
||||
pumpstart(&f->pump, 16);
|
||||
USED(t);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -437,7 +435,7 @@ nexttracecap(struct ct *t, Ximfix *f, int op, Ic *owner, int cap)
|
||||
return req;
|
||||
CT_EQ_INT(t, op, req.op);
|
||||
CT_EQ_PTR(t, owner, req.owner);
|
||||
CT_EQ_INT(t, cap, req.cap);
|
||||
CT_EQ_INT(t, cap, req.clientpre);
|
||||
CT_EQ_PTR(t, replyc, req.reply);
|
||||
return req;
|
||||
}
|
||||
@@ -445,7 +443,7 @@ nexttracecap(struct ct *t, Ximfix *f, int op, Ic *owner, int cap)
|
||||
static Keyreq
|
||||
nexttrace(struct ct *t, Ximfix *f, int op, Ic *state)
|
||||
{
|
||||
return nexttracecap(t, f, op, state, state->cap);
|
||||
return nexttracecap(t, f, op, state, state->clientpre);
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -466,8 +464,7 @@ xim_adapter_key_contract(struct ct *t)
|
||||
u32int allmod;
|
||||
|
||||
memset(&state, 0, sizeof state);
|
||||
if(!ximbegin(t, &f, LangJP))
|
||||
goto cleanup;
|
||||
ximbegin(&f, LangJP);
|
||||
allmod = ~0;
|
||||
keypress(&state, 'x', allmod, &res);
|
||||
req = nexttrace(t, &f, Keypress, &state);
|
||||
@@ -487,7 +484,6 @@ xim_adapter_key_contract(struct ct *t)
|
||||
CT_CHECK(t, !res.eaten);
|
||||
checkstr(t, "preedit", "か", &res.commit);
|
||||
checkstr(t, "preedit", "", &res.preedit);
|
||||
cleanup:
|
||||
ximend(&f);
|
||||
}
|
||||
|
||||
@@ -502,8 +498,7 @@ xim_adapter_release_lifecycle(struct ct *t)
|
||||
memset(&a, 0, sizeof a);
|
||||
memset(&b, 0, sizeof b);
|
||||
dead = nil;
|
||||
if(!ximbegin(t, &f, LangJP))
|
||||
goto cleanup;
|
||||
ximbegin(&f, LangJP);
|
||||
keypress(&a, 'k', 0, &res);
|
||||
nexttrace(t, &f, Keypress, &a);
|
||||
keypress(&a, 'a', 0, &res);
|
||||
@@ -539,7 +534,7 @@ xim_adapter_release_lifecycle(struct ct *t)
|
||||
keypress(dead, 'n', 0, &res);
|
||||
nexttrace(t, &f, Keypress, dead);
|
||||
CT_EQ_PTR(t, dead, testengineowner());
|
||||
deadcap = dead->cap;
|
||||
deadcap = dead->clientpre;
|
||||
icfree(dead);
|
||||
nexttracecap(t, &f, Keyrelease, dead, deadcap);
|
||||
dead = nil;
|
||||
@@ -580,8 +575,7 @@ xim_adapter_free_waits_for_release(struct ct *t)
|
||||
state = nil;
|
||||
freed = nil;
|
||||
workeractive = 0;
|
||||
if(!ximbegin(t, &f, LangJP))
|
||||
goto cleanup;
|
||||
ximbegin(&f, LangJP);
|
||||
state = calloc(1, sizeof *state);
|
||||
freed = chancreate(sizeof(uchar), 0);
|
||||
if(!CT_CHECK(t, state != nil && freed != nil))
|
||||
@@ -632,6 +626,9 @@ xim_adapter_styles(struct ct *t)
|
||||
XCB_IM_PreeditPosition | XCB_IM_StatusNothing,
|
||||
XCB_IM_PreeditCallbacks | XCB_IM_StatusNothing,
|
||||
XCB_IM_PreeditNothing | XCB_IM_StatusNothing,
|
||||
XCB_IM_PreeditPosition | XCB_IM_StatusNone,
|
||||
XCB_IM_PreeditCallbacks | XCB_IM_StatusNone,
|
||||
XCB_IM_PreeditNothing | XCB_IM_StatusNone,
|
||||
};
|
||||
xcb_im_input_context_t *ic;
|
||||
Ic *state;
|
||||
@@ -651,7 +648,8 @@ xim_adapter_styles(struct ct *t)
|
||||
state = icbindings[0].data;
|
||||
if(!CT_CHECK(t, state != nil && state == ics))
|
||||
continue;
|
||||
CT_EQ_INT(t, i == 1 ? Cclientpreedit : 0, state->cap);
|
||||
CT_EQ_INT(t, want[i] & XCB_IM_PreeditCallbacks ? 1 : 0,
|
||||
state->clientpre);
|
||||
CT_EQ_PTR(t, ic, state->xic);
|
||||
icunlink(state);
|
||||
free(state);
|
||||
@@ -663,7 +661,7 @@ xim_adapter_placement(struct ct *t)
|
||||
{
|
||||
/* Spot location first, then the bottom of the focus window, then of
|
||||
* the client window; an unset focus window is the client window. */
|
||||
static const struct {
|
||||
const struct {
|
||||
u32int mask;
|
||||
int focuswin;
|
||||
int fgeo;
|
||||
@@ -673,14 +671,15 @@ xim_adapter_placement(struct ct *t)
|
||||
int valid;
|
||||
int x;
|
||||
int y;
|
||||
int h;
|
||||
} cases[] = {
|
||||
{XCB_XIM_XNSpotLocation_MASK, 10, 1, 1, 1, 1, 1, 103, 204},
|
||||
{XCB_XIM_XNSpotLocation_MASK, 10, 1, 0, 1, 1, 1, 300, 440},
|
||||
{XCB_XIM_XNSpotLocation_MASK, 0, 1, 1, 1, 1, 1, 303, 404},
|
||||
{0, 10, 1, 1, 1, 1, 1, 100, 230},
|
||||
{0, 0, 1, 1, 1, 1, 1, 300, 440},
|
||||
{0, 10, 0, 1, 1, 1, 1, 300, 440},
|
||||
{0, 10, 0, 1, 0, 1, 0, 0, 0},
|
||||
{XCB_XIM_XNSpotLocation_MASK, 10, 1, 1, 1, 1, 1, 103, 204-Fontsz, Fontsz},
|
||||
{XCB_XIM_XNSpotLocation_MASK, 10, 1, 0, 1, 1, 1, 300, 440, 0},
|
||||
{XCB_XIM_XNSpotLocation_MASK, 0, 1, 1, 1, 1, 1, 303, 404-Fontsz, Fontsz},
|
||||
{0, 10, 1, 1, 1, 1, 1, 100, 230, 0},
|
||||
{0, 0, 1, 1, 1, 1, 1, 300, 440, 0},
|
||||
{0, 10, 0, 1, 1, 1, 1, 300, 440, 0},
|
||||
{0, 10, 0, 1, 0, 1, 0, 0, 0, 0},
|
||||
};
|
||||
xcb_im_input_context_t *ic;
|
||||
Icbinding *b;
|
||||
@@ -712,7 +711,7 @@ xim_adapter_placement(struct ct *t)
|
||||
CT_EQ_INT(t, cases[i].valid, state.caret.valid);
|
||||
CT_EQ_INT(t, cases[i].x, state.caret.x);
|
||||
CT_EQ_INT(t, cases[i].y, state.caret.y);
|
||||
CT_EQ_INT(t, 0, state.caret.h);
|
||||
CT_EQ_INT(t, cases[i].h, state.caret.h);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -731,8 +730,7 @@ xim_adapter_placement_updates(struct ct *t)
|
||||
ic = (xcb_im_input_context_t*)(uintptr)71;
|
||||
memset(&state, 0, sizeof state);
|
||||
oldstate = kstate;
|
||||
if(!ximbegin(t, &f, LangJP))
|
||||
goto cleanup;
|
||||
ximbegin(&f, LangJP);
|
||||
kstate = testkeystate("us");
|
||||
if(!CT_CHECK(t, kstate != nil))
|
||||
goto cleanup;
|
||||
@@ -752,13 +750,13 @@ xim_adapter_placement_updates(struct ct *t)
|
||||
req = nexttrace(t, &f, Keypress, &state);
|
||||
CT_CHECK(t, req.caret.valid);
|
||||
CT_EQ_INT(t, 105, req.caret.x);
|
||||
CT_EQ_INT(t, 206, req.caret.y);
|
||||
CT_EQ_INT(t, 206-Fontsz, req.caret.y);
|
||||
wirewins[0].rootx = 150;
|
||||
wirewins[0].rooty = 250;
|
||||
kpress(&state, &ev);
|
||||
req = nexttrace(t, &f, Keypress, &state);
|
||||
CT_EQ_INT(t, 155, req.caret.x);
|
||||
CT_EQ_INT(t, 256, req.caret.y);
|
||||
CT_EQ_INT(t, 256-Fontsz, req.caret.y);
|
||||
|
||||
b->preattr.spot_location.x = 15;
|
||||
b->preattr.spot_location.y = 16;
|
||||
@@ -768,7 +766,7 @@ xim_adapter_placement_updates(struct ct *t)
|
||||
req = nexttrace(t, &f, Keycaret, &state);
|
||||
CT_CHECK(t, req.caret.valid);
|
||||
CT_EQ_INT(t, 165, req.caret.x);
|
||||
CT_EQ_INT(t, 266, req.caret.y);
|
||||
CT_EQ_INT(t, 266-Fontsz, req.caret.y);
|
||||
b->preattrmask = 0;
|
||||
wirewins[0].geometryok = 0;
|
||||
hdr.major_opcode = XCB_XIM_SET_IC_FOCUS;
|
||||
@@ -794,7 +792,7 @@ xim_adapter_callback_replacement(struct ct *t)
|
||||
ic = (xcb_im_input_context_t*)(uintptr)3;
|
||||
memset(&state, 0, sizeof state);
|
||||
state.xic = ic;
|
||||
state.cap = Cclientpreedit;
|
||||
state.clientpre = 1;
|
||||
wireclear();
|
||||
sinit(&pre, "k", strlen("k"));
|
||||
updatepreedit(&state, &pre);
|
||||
@@ -843,7 +841,7 @@ xim_adapter_callback_unicode(struct ct *t)
|
||||
for(i = 0; i < nelem(text); i++){
|
||||
memset(&state, 0, sizeof state);
|
||||
state.xic = (xcb_im_input_context_t*)(uintptr)(10+i);
|
||||
state.cap = Cclientpreedit;
|
||||
state.clientpre = 1;
|
||||
sinit(&pre, text[i], strlen(text[i]));
|
||||
wireclear();
|
||||
updatepreedit(&state, &pre);
|
||||
@@ -863,7 +861,7 @@ startstate(Ic *state, xcb_im_input_context_t *ic)
|
||||
|
||||
memset(state, 0, sizeof *state);
|
||||
state->xic = ic;
|
||||
state->cap = Cclientpreedit;
|
||||
state->clientpre = 1;
|
||||
sinit(&pre, "あ", strlen("あ"));
|
||||
updatepreedit(state, &pre);
|
||||
}
|
||||
@@ -886,12 +884,11 @@ xim_adapter_callback_cleanup(struct ct *t)
|
||||
|
||||
ic = (xcb_im_input_context_t*)(uintptr)29;
|
||||
memset(&state, 0, sizeof state);
|
||||
if(!ximbegin(t, &f, LangJP))
|
||||
return;
|
||||
ximbegin(&f, LangJP);
|
||||
wireclear();
|
||||
wirebind(0, ic, &state);
|
||||
state.xic = ic;
|
||||
state.cap = Cclientpreedit;
|
||||
state.clientpre = 1;
|
||||
keypress(&state, 'k', 0, &res);
|
||||
nexttrace(t, &f, Keypress, &state);
|
||||
keypress(&state, 'a', 0, &res);
|
||||
@@ -975,11 +972,10 @@ xim_adapter_callback_transfer(struct ct *t)
|
||||
aic = (xcb_im_input_context_t*)(uintptr)50;
|
||||
memset(&a, 0, sizeof a);
|
||||
memset(&b, 0, sizeof b);
|
||||
if(!ximbegin(t, &f, LangJP))
|
||||
goto cleanup;
|
||||
ximbegin(&f, LangJP);
|
||||
wireclear();
|
||||
a.xic = aic;
|
||||
a.cap = Cclientpreedit;
|
||||
a.clientpre = 1;
|
||||
sinit(&pre, "あ", strlen("あ"));
|
||||
updatepreedit(&a, &pre);
|
||||
keypress(&b, 'x', 0, &res);
|
||||
@@ -991,7 +987,6 @@ xim_adapter_callback_transfer(struct ct *t)
|
||||
CT_EQ_PTR(t, nil, preowner);
|
||||
release(&b);
|
||||
nexttrace(t, &f, Keyrelease, &b);
|
||||
cleanup:
|
||||
preowner = nil;
|
||||
ximend(&f);
|
||||
}
|
||||
@@ -1011,12 +1006,11 @@ xim_adapter_callback_owner_loss(struct ct *t)
|
||||
ic = (xcb_im_input_context_t*)(uintptr)51;
|
||||
memset(&state, 0, sizeof state);
|
||||
foreign = 0;
|
||||
if(!ximbegin(t, &f, LangJP))
|
||||
goto cleanup;
|
||||
ximbegin(&f, LangJP);
|
||||
wireclear();
|
||||
wirebind(0, ic, &state);
|
||||
state.xic = ic;
|
||||
state.cap = Cclientpreedit;
|
||||
state.clientpre = 1;
|
||||
keypress(&state, 'k', 0, &res);
|
||||
nexttrace(t, &f, Keypress, &state);
|
||||
updatepreedit(&state, &res.preedit);
|
||||
@@ -1054,9 +1048,12 @@ xim_adapter_callback_owner_loss(struct ct *t)
|
||||
checkpreowner();
|
||||
notrace(t, &f);
|
||||
|
||||
/* Late XIM lifecycle messages remain idempotent after owner cleanup. */
|
||||
/* The release hands back the reading the takeover took. */
|
||||
release(&state);
|
||||
nexttrace(t, &f, Keyrelease, &state);
|
||||
CT_EQ_INT(t, 5, nwirecalls);
|
||||
CT_EQ_INT(t, Wcommit, wirecalls[4].op);
|
||||
/* Late XIM lifecycle messages remain idempotent after owner cleanup. */
|
||||
release(&state);
|
||||
notrace(t, &f);
|
||||
memset(&hdr, 0, sizeof hdr);
|
||||
@@ -1066,7 +1063,7 @@ xim_adapter_callback_owner_loss(struct ct *t)
|
||||
hdr.major_opcode = XCB_XIM_DESTROY_IC;
|
||||
callback(nil, nil, ic, &hdr, nil, nil, nil);
|
||||
notrace(t, &f);
|
||||
CT_EQ_INT(t, 4, nwirecalls);
|
||||
CT_EQ_INT(t, 5, nwirecalls);
|
||||
|
||||
memset(&req, 0, sizeof req);
|
||||
req.owner = &foreign;
|
||||
@@ -1078,7 +1075,6 @@ xim_adapter_callback_owner_loss(struct ct *t)
|
||||
CT_CHECK(t, channbrecv(f.pump.trace, &trace) > 0);
|
||||
CT_EQ_INT(t, Keyrelease, trace.op);
|
||||
CT_EQ_PTR(t, &foreign, trace.owner);
|
||||
cleanup:
|
||||
preowner = nil;
|
||||
ximend(&f);
|
||||
}
|
||||
@@ -1104,3 +1100,61 @@ xim_adapter_commit_encoding(struct ct *t)
|
||||
commit(&state, text, 0);
|
||||
CT_EQ_INT(t, 0, nwirecalls);
|
||||
}
|
||||
|
||||
void
|
||||
xim_keymap_lookup(struct ct *t)
|
||||
{
|
||||
enum { ShiftMask = 1<<0, LockMask = 1<<1, Mod2Mask = 1<<4,
|
||||
Mod5Mask = 1<<7, Group1 = 1<<13 };
|
||||
struct xkb_context *context;
|
||||
struct xkb_keymap *keymap;
|
||||
struct xkb_rule_names names;
|
||||
struct xkb_state *state;
|
||||
|
||||
memset(&names, 0, sizeof names);
|
||||
names.layout = "de,ru";
|
||||
context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
|
||||
keymap = xkb_keymap_new_from_names(context, &names,
|
||||
XKB_KEYMAP_COMPILE_NO_FLAGS);
|
||||
if(!CT_CHECK(t, keymap != nil)){
|
||||
xkb_context_unref(context);
|
||||
return;
|
||||
}
|
||||
state = xkb_state_new(keymap);
|
||||
CT_EQ_UINT(t, XKB_KEY_a, keymaplookup(state, 38, 0));
|
||||
CT_EQ_UINT(t, XKB_KEY_A, keymaplookup(state, 38, ShiftMask));
|
||||
CT_EQ_UINT(t, XKB_KEY_A, keymaplookup(state, 38, LockMask));
|
||||
CT_EQ_UINT(t, XKB_KEY_a, keymaplookup(state, 38, LockMask|ShiftMask));
|
||||
CT_EQ_UINT(t, XKB_KEY_EuroSign, keymaplookup(state, 26, Mod5Mask));
|
||||
CT_EQ_UINT(t, XKB_KEY_Cyrillic_u, keymaplookup(state, 26, Group1));
|
||||
CT_EQ_UINT(t, XKB_KEY_KP_End, keymaplookup(state, 87, 0));
|
||||
CT_EQ_UINT(t, XKB_KEY_KP_1, keymaplookup(state, 87, Mod2Mask));
|
||||
xkb_state_unref(state);
|
||||
xkb_keymap_unref(keymap);
|
||||
xkb_context_unref(context);
|
||||
}
|
||||
|
||||
/* imdkit carries any UTF-8 text through COMPOUND_TEXT unchanged. */
|
||||
void
|
||||
xim_compound_text(struct ct *t)
|
||||
{
|
||||
static char *text[] = { "A😀한", "한글", "𠀋", "👩💻" };
|
||||
char *ct, *utf8;
|
||||
size_t clen, len, ulen;
|
||||
int i;
|
||||
|
||||
xcb_compound_text_init();
|
||||
for(i = 0; i < nelem(text); i++){
|
||||
len = strlen(text[i]);
|
||||
ct = xcb_utf8_to_compound_text(text[i], len, &clen);
|
||||
if(!CT_CHECK(t, ct != nil))
|
||||
continue;
|
||||
utf8 = xcb_compound_text_to_utf8(ct, clen, &ulen);
|
||||
if(CT_CHECK(t, utf8 != nil)){
|
||||
CT_EQ_SIZE(t, len, ulen);
|
||||
CT_EQ_MEM(t, text[i], utf8, len);
|
||||
}
|
||||
free(utf8);
|
||||
free(ct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,23 +24,14 @@ enum
|
||||
Eventtimeout = 4000,
|
||||
};
|
||||
|
||||
typedef struct Child Child;
|
||||
typedef struct Run Run;
|
||||
typedef struct Prelog Prelog;
|
||||
|
||||
struct Child
|
||||
{
|
||||
pid_t pid;
|
||||
pid_t pgid;
|
||||
char log[320];
|
||||
};
|
||||
|
||||
struct Run
|
||||
{
|
||||
Live l;
|
||||
Child xvfb;
|
||||
Child daemon;
|
||||
char display[32];
|
||||
Live l;
|
||||
Daemon xvfb;
|
||||
Daemon daemon;
|
||||
};
|
||||
|
||||
struct Prelog
|
||||
@@ -55,144 +46,10 @@ struct Prelog
|
||||
|
||||
static int xerror;
|
||||
|
||||
static int
|
||||
childlog(char *path)
|
||||
{
|
||||
int fd;
|
||||
|
||||
fd = open(path, O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0600);
|
||||
return fd;
|
||||
}
|
||||
|
||||
static int
|
||||
spawnxvfb(Run *r, char *program)
|
||||
{
|
||||
char fdstr[16], line[32];
|
||||
int displayfd[2], errfd, flags, n, status;
|
||||
int64_t deadline;
|
||||
pid_t pid;
|
||||
struct pollfd pfd;
|
||||
ssize_t nr;
|
||||
|
||||
if(pipe2(displayfd, O_CLOEXEC) < 0)
|
||||
return fail("create Xvfb display pipe: %s", strerror(errno));
|
||||
errfd = childlog(r->xvfb.log);
|
||||
if(errfd < 0){
|
||||
close(displayfd[0]);
|
||||
close(displayfd[1]);
|
||||
return fail("open Xvfb log: %s", strerror(errno));
|
||||
}
|
||||
pid = fork();
|
||||
if(pid < 0){
|
||||
close(displayfd[0]);
|
||||
close(displayfd[1]);
|
||||
close(errfd);
|
||||
return fail("fork Xvfb: %s", strerror(errno));
|
||||
}
|
||||
if(pid == 0){
|
||||
setpgid(0, 0);
|
||||
close(displayfd[0]);
|
||||
flags = fcntl(displayfd[1], F_GETFD);
|
||||
if(flags < 0 || fcntl(displayfd[1], F_SETFD,
|
||||
flags & ~FD_CLOEXEC) < 0)
|
||||
_exit(126);
|
||||
if(dup2(errfd, STDOUT_FILENO) < 0 ||
|
||||
dup2(errfd, STDERR_FILENO) < 0)
|
||||
_exit(126);
|
||||
close(errfd);
|
||||
snprintf(fdstr, sizeof fdstr, "%d", displayfd[1]);
|
||||
execlp(program, program, "-displayfd", fdstr,
|
||||
"-screen", "0", "800x600x24", "-nolisten", "tcp",
|
||||
"-noreset", (char*)0);
|
||||
_exit(127);
|
||||
}
|
||||
close(displayfd[1]);
|
||||
close(errfd);
|
||||
if((setpgid(pid, pid) < 0 && errno != EACCES) || getpgid(pid) != pid){
|
||||
kill(pid, SIGKILL);
|
||||
while(waitpid(pid, &status, 0) < 0 && errno == EINTR)
|
||||
;
|
||||
close(displayfd[0]);
|
||||
return fail("isolate Xvfb process group: %s", strerror(errno));
|
||||
}
|
||||
r->xvfb.pid = pid;
|
||||
r->xvfb.pgid = pid;
|
||||
pfd.fd = displayfd[0];
|
||||
pfd.events = POLLIN|POLLHUP;
|
||||
deadline = nowms() + Starttimeout;
|
||||
n = 0;
|
||||
while(n + 1 < (int)sizeof line){
|
||||
pfd.revents = 0;
|
||||
status = poll(&pfd, 1, leftms(deadline));
|
||||
if(status < 0 && errno == EINTR)
|
||||
continue;
|
||||
if(status <= 0)
|
||||
break;
|
||||
nr = read(displayfd[0], line + n, sizeof line - n - 1);
|
||||
if(nr > 0){
|
||||
n += nr;
|
||||
line[n] = '\0';
|
||||
if(strchr(line, '\n') != NULL)
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
if(nr < 0 && errno == EINTR)
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
close(displayfd[0]);
|
||||
if(n == 0 || strchr(line, '\n') == NULL)
|
||||
return fail("Xvfb did not report a private display");
|
||||
line[strcspn(line, "\r\n")] = '\0';
|
||||
for(n = 0; line[n] != '\0'; n++)
|
||||
if(line[n] < '0' || line[n] > '9')
|
||||
return fail("invalid Xvfb display number %s", line);
|
||||
if(snprintf(r->display, sizeof r->display, ":%s", line)
|
||||
>= (int)sizeof r->display)
|
||||
return fail("Xvfb display number is too long");
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
spawndaemon(Run *r, char *program, char *mapdir)
|
||||
{
|
||||
int errfd;
|
||||
pid_t pid;
|
||||
|
||||
errfd = childlog(r->daemon.log);
|
||||
if(errfd < 0)
|
||||
return fail("open daemon log: %s", strerror(errno));
|
||||
pid = fork();
|
||||
if(pid < 0){
|
||||
close(errfd);
|
||||
return fail("fork daemon: %s", strerror(errno));
|
||||
}
|
||||
if(pid == 0){
|
||||
setpgid(0, 0);
|
||||
if(dup2(errfd, STDOUT_FILENO) < 0 ||
|
||||
dup2(errfd, STDERR_FILENO) < 0)
|
||||
_exit(126);
|
||||
close(errfd);
|
||||
execl(program, program, mapdir, (char*)0);
|
||||
_exit(127);
|
||||
}
|
||||
close(errfd);
|
||||
if((setpgid(pid, pid) < 0 && errno != EACCES) || getpgid(pid) != pid){
|
||||
kill(pid, SIGKILL);
|
||||
while(waitpid(pid, NULL, 0) < 0 && errno == EINTR)
|
||||
;
|
||||
return fail("isolate daemon process group: %s", strerror(errno));
|
||||
}
|
||||
r->daemon.pid = pid;
|
||||
r->daemon.pgid = pid;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
waitsocket(Run *r)
|
||||
{
|
||||
struct stat st;
|
||||
int status;
|
||||
int64_t deadline;
|
||||
|
||||
deadline = nowms() + Starttimeout;
|
||||
@@ -200,10 +57,8 @@ waitsocket(Run *r)
|
||||
if(lstat(r->l.socket, &st) == 0 && S_ISSOCK(st.st_mode) &&
|
||||
(st.st_mode & 0777) == 0600)
|
||||
return 1;
|
||||
if(waitpid(r->daemon.pid, &status, WNOHANG) == r->daemon.pid){
|
||||
r->daemon.pid = -1;
|
||||
if(!daemonalive(&r->daemon))
|
||||
return fail("daemon exited before creating its IPC socket");
|
||||
}
|
||||
pausems(20);
|
||||
}
|
||||
return fail("timed out waiting for daemon IPC socket");
|
||||
@@ -213,90 +68,29 @@ static int
|
||||
start(Run *r, char *daemon, char *mapdir, char *xvfb)
|
||||
{
|
||||
memset(r, 0, sizeof *r);
|
||||
r->xvfb.pid = r->daemon.pid = -1;
|
||||
r->xvfb.pgid = r->daemon.pgid = -1;
|
||||
if(!livesetup(&r->l, "xim"))
|
||||
daemoninit(&r->xvfb, "Xvfb");
|
||||
daemoninit(&r->daemon, "daemon");
|
||||
if(!livesetup(&r->l, "xim") || !startxvfb(&r->l, &r->xvfb, xvfb))
|
||||
return 0;
|
||||
if(snprintf(r->xvfb.log, sizeof r->xvfb.log, "%s/xvfb.log", r->l.root)
|
||||
>= (int)sizeof r->xvfb.log ||
|
||||
snprintf(r->daemon.log, sizeof r->daemon.log, "%s/daemon.log", r->l.root)
|
||||
>= (int)sizeof r->daemon.log)
|
||||
return fail("temporary path is too long");
|
||||
if(!spawnxvfb(r, xvfb))
|
||||
return 0;
|
||||
if(setenv("DISPLAY", r->display, 1) < 0 ||
|
||||
setenv("XMODIFIERS", "@im=strans", 1) < 0 ||
|
||||
setenv("XDG_RUNTIME_DIR", r->l.runtime, 1) < 0 ||
|
||||
setenv("XDG_CONFIG_HOME", r->l.config, 1) < 0 ||
|
||||
setenv("HOME", r->l.home, 1) < 0 ||
|
||||
unsetenv("DBUS_SESSION_BUS_ADDRESS") < 0 ||
|
||||
unsetenv("IBUS_ADDRESS") < 0)
|
||||
if(setenv("DISPLAY", r->l.display, 1) < 0 ||
|
||||
setenv("XMODIFIERS", "@im=strans", 1) < 0)
|
||||
return fail("set private environment: %s", strerror(errno));
|
||||
if(!spawndaemon(r, daemon, mapdir))
|
||||
if(!startdaemon(&r->l, &r->daemon, daemon, mapdir))
|
||||
return 0;
|
||||
return waitsocket(r);
|
||||
}
|
||||
|
||||
static void
|
||||
showlog(char *name)
|
||||
{
|
||||
char buf[4096];
|
||||
ssize_t n;
|
||||
int fd;
|
||||
|
||||
fd = open(name, O_RDONLY|O_CLOEXEC);
|
||||
if(fd < 0)
|
||||
return;
|
||||
while((n = read(fd, buf, sizeof buf)) > 0)
|
||||
fwrite(buf, 1, n, stderr);
|
||||
close(fd);
|
||||
}
|
||||
|
||||
static void
|
||||
stopchild(Child *c)
|
||||
{
|
||||
int i, status;
|
||||
pid_t n;
|
||||
|
||||
if(c->pgid > 0 && kill(-c->pgid, SIGTERM) < 0 && errno != ESRCH)
|
||||
fail("terminate process group %ld: %s", (long)c->pgid,
|
||||
strerror(errno));
|
||||
if(c->pid > 0)
|
||||
for(i = 0; i < Stoptimeout / 20; i++){
|
||||
n = waitpid(c->pid, &status, WNOHANG);
|
||||
if(n == c->pid || (n < 0 && errno == ECHILD)){
|
||||
c->pid = -1;
|
||||
break;
|
||||
}
|
||||
if(n < 0 && errno != EINTR)
|
||||
break;
|
||||
pausems(20);
|
||||
}
|
||||
if(c->pgid > 0)
|
||||
kill(-c->pgid, SIGKILL);
|
||||
if(c->pid > 0){
|
||||
while(waitpid(c->pid, &status, 0) < 0 && errno == EINTR)
|
||||
;
|
||||
c->pid = -1;
|
||||
}
|
||||
c->pgid = -1;
|
||||
}
|
||||
|
||||
static void
|
||||
cleanup(Run *r, int noisy)
|
||||
{
|
||||
stopchild(&r->daemon);
|
||||
stopchild(&r->xvfb);
|
||||
stopdaemon(&r->daemon);
|
||||
stopdaemon(&r->xvfb);
|
||||
if(noisy){
|
||||
fprintf(stderr, "%s: daemon log:\n", testname);
|
||||
showlog(r->daemon.log);
|
||||
fprintf(stderr, "%s: Xvfb log:\n", testname);
|
||||
showlog(r->xvfb.log);
|
||||
showerrors(&r->daemon);
|
||||
showerrors(&r->xvfb);
|
||||
}
|
||||
if(r->daemon.log[0] != '\0')
|
||||
unlink(r->daemon.log);
|
||||
if(r->xvfb.log[0] != '\0')
|
||||
unlink(r->xvfb.log);
|
||||
closeerrors(&r->daemon);
|
||||
closeerrors(&r->xvfb);
|
||||
liveclean(&r->l);
|
||||
}
|
||||
|
||||
@@ -335,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';
|
||||
}
|
||||
@@ -479,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);
|
||||
@@ -611,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)
|
||||
@@ -625,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;
|
||||
@@ -635,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)
|
||||
@@ -765,9 +566,6 @@ main(int argc, char **argv)
|
||||
fprintf(stderr, "usage: xim_live_test daemon mapdir [Xvfb]\n");
|
||||
return 2;
|
||||
}
|
||||
memset(&run, 0, sizeof run);
|
||||
run.xvfb.pid = run.daemon.pid = -1;
|
||||
run.xvfb.pgid = run.daemon.pgid = -1;
|
||||
xvfb = argc == 4 ? argv[3] : "Xvfb";
|
||||
ok = start(&run, argv[1], argv[2], xvfb);
|
||||
if(!ok){
|
||||
@@ -781,15 +579,15 @@ main(int argc, char **argv)
|
||||
return 1;
|
||||
}
|
||||
XSetErrorHandler(xerr);
|
||||
dpy = XOpenDisplay(run.display);
|
||||
dpy = XOpenDisplay(run.l.display);
|
||||
if(dpy == NULL){
|
||||
fail("open private display %s", run.display);
|
||||
fail("open private display %s", run.l.display);
|
||||
cleanup(&run, 1);
|
||||
return 1;
|
||||
}
|
||||
im = openim(dpy);
|
||||
if(im == NULL){
|
||||
fail("open strans XIM on private display %s", run.display);
|
||||
fail("open strans XIM on private display %s", run.l.display);
|
||||
XCloseDisplay(dpy);
|
||||
cleanup(&run, 1);
|
||||
return 1;
|
||||
|
||||
76
trie.c
76
trie.c
@@ -3,9 +3,9 @@
|
||||
#include "fn.h"
|
||||
|
||||
/*
|
||||
* A byte trie over UTF-8 keys. Maps need prefix matches while composing;
|
||||
* dictionaries need exact matches; both are small enough for one structure.
|
||||
* Node 0 is the root.
|
||||
* A trie over the runes of a key. Maps need prefix matches while
|
||||
* composing; dictionaries need exact matches; both are small enough for
|
||||
* one structure. Node 0 is the root.
|
||||
*/
|
||||
|
||||
static int
|
||||
@@ -24,26 +24,37 @@ newnode(Trie *t)
|
||||
return i;
|
||||
}
|
||||
|
||||
/* The children of a node are kept in rune order, so both end early. */
|
||||
static int
|
||||
find(Trie *t, int ni, char c)
|
||||
find(Trie *t, int ni, Rune c)
|
||||
{
|
||||
int pi;
|
||||
|
||||
for(pi = t->nodes[ni].child; pi >= 0; pi = t->nodes[pi].sibling)
|
||||
for(pi = t->nodes[ni].child; pi >= 0; pi = t->nodes[pi].sibling){
|
||||
if(t->nodes[pi].c == c)
|
||||
return pi;
|
||||
if(t->nodes[pi].c > c)
|
||||
break;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int
|
||||
add(Trie *t, int ni, char c)
|
||||
add(Trie *t, int ni, Rune c)
|
||||
{
|
||||
int pi;
|
||||
int next, pi, prev;
|
||||
|
||||
prev = -1;
|
||||
for(next = t->nodes[ni].child; next >= 0 && t->nodes[next].c < c;
|
||||
next = t->nodes[next].sibling)
|
||||
prev = next;
|
||||
pi = newnode(t);
|
||||
t->nodes[pi].c = c;
|
||||
t->nodes[pi].sibling = t->nodes[ni].child;
|
||||
t->nodes[ni].child = pi;
|
||||
t->nodes[pi].sibling = next;
|
||||
if(prev < 0)
|
||||
t->nodes[ni].child = pi;
|
||||
else
|
||||
t->nodes[prev].sibling = pi;
|
||||
return pi;
|
||||
}
|
||||
|
||||
@@ -63,15 +74,26 @@ trienew(void)
|
||||
void
|
||||
trieput(Trie *t, char *key, int klen, char *val, int vlen)
|
||||
{
|
||||
int ni, ci, i;
|
||||
Str k;
|
||||
int ci, i, ni, same;
|
||||
|
||||
if(!sinit(&k, key, klen))
|
||||
return;
|
||||
ni = 0;
|
||||
for(i = 0; i < klen; i++){
|
||||
ci = find(t, ni, key[i]);
|
||||
same = 1;
|
||||
for(i = 0; i < k.n; i++){
|
||||
if(same && i < t->last.n && t->last.r[i] == k.r[i]){
|
||||
ni = t->path[i];
|
||||
continue;
|
||||
}
|
||||
same = 0;
|
||||
ci = find(t, ni, k.r[i]);
|
||||
if(ci < 0)
|
||||
ci = add(t, ni, key[i]);
|
||||
ci = add(t, ni, k.r[i]);
|
||||
ni = ci;
|
||||
t->path[i] = ni;
|
||||
}
|
||||
t->last = k;
|
||||
free(t->nodes[ni].val);
|
||||
t->nodes[ni].val = emalloc(vlen + 1);
|
||||
memmove(t->nodes[ni].val, val, vlen);
|
||||
@@ -153,24 +175,30 @@ trieclose(Trie *t)
|
||||
free(t);
|
||||
}
|
||||
|
||||
/* A nil trie is an unloaded map: every key misses. */
|
||||
/* The node key leads to, or -1. A nil trie is an unloaded map: every key misses. */
|
||||
int
|
||||
trienode(Trie *t, Str *key)
|
||||
{
|
||||
int i, ni;
|
||||
|
||||
if(t == nil)
|
||||
return -1;
|
||||
ni = 0;
|
||||
for(i = 0; i < key->n && ni >= 0; i++)
|
||||
ni = find(t, ni, key->r[i]);
|
||||
return ni;
|
||||
}
|
||||
|
||||
int
|
||||
trielookup(Trie *t, Str *key, char **val, int *vlen)
|
||||
{
|
||||
char buf[Maxutf];
|
||||
int i, klen, ni;
|
||||
int ni;
|
||||
|
||||
*val = nil;
|
||||
*vlen = 0;
|
||||
if(t == nil)
|
||||
ni = trienode(t, key);
|
||||
if(ni < 0)
|
||||
return TrieMiss;
|
||||
klen = stoutf(key, buf, sizeof buf);
|
||||
ni = 0;
|
||||
for(i = 0; i < klen; i++){
|
||||
ni = find(t, ni, buf[i]);
|
||||
if(ni < 0)
|
||||
return TrieMiss;
|
||||
}
|
||||
if(t->nodes[ni].val == nil)
|
||||
return TriePrefix;
|
||||
*val = t->nodes[ni].val;
|
||||
|
||||
89
win.c
89
win.c
@@ -1,4 +1,5 @@
|
||||
#include <xcb/xcb.h>
|
||||
#include <xcb/xcb_aux.h>
|
||||
#include <xcb/randr.h>
|
||||
#include "dat.h"
|
||||
#include "fn.h"
|
||||
@@ -12,17 +13,7 @@ static u32int *img;
|
||||
static xcb_atom_t currentdesktop;
|
||||
static xcb_atom_t workarea;
|
||||
static int hasrandr, imgh, imgw;
|
||||
|
||||
static xcb_screen_t*
|
||||
getscr(xcb_connection_t *c, int n)
|
||||
{
|
||||
xcb_screen_iterator_t i;
|
||||
|
||||
for(i = xcb_setup_roots_iterator(xcb_get_setup(c)); i.rem; xcb_screen_next(&i))
|
||||
if(n-- == 0)
|
||||
return i.data;
|
||||
return nil;
|
||||
}
|
||||
static int shown, ptrx, ptry; /* the popup, and where the pointer was as it came up */
|
||||
|
||||
static xcb_visualtype_t*
|
||||
getvisual(xcb_screen_t *s)
|
||||
@@ -201,23 +192,19 @@ static int
|
||||
wininit(void)
|
||||
{
|
||||
int n;
|
||||
u32int mask, vals[4];
|
||||
u32int mask, vals[5];
|
||||
xcb_atom_t type, tooltip;
|
||||
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");
|
||||
wincleanup();
|
||||
return 0;
|
||||
}
|
||||
scr = getscr(conn, n);
|
||||
scr = xcb_aux_get_screen(conn, n);
|
||||
if(scr == nil || !validformat(conn, scr)){
|
||||
fprint(2, "strans: popup disabled: unsupported X root format\n");
|
||||
wincleanup();
|
||||
@@ -234,14 +221,17 @@ wininit(void)
|
||||
currentdesktop = getatom("_NET_CURRENT_DESKTOP");
|
||||
workarea = getatom("_NET_WORKAREA");
|
||||
win = xcb_generate_id(conn);
|
||||
/* Clicks on the popup go nowhere, not to the root window under it. */
|
||||
mask = XCB_CW_BACK_PIXEL | XCB_CW_BORDER_PIXEL |
|
||||
XCB_CW_OVERRIDE_REDIRECT | XCB_CW_SAVE_UNDER;
|
||||
XCB_CW_OVERRIDE_REDIRECT | XCB_CW_SAVE_UNDER |
|
||||
XCB_CW_DONT_PROPAGATE;
|
||||
vals[0] = Colbg;
|
||||
vals[1] = 0;
|
||||
vals[1] = Colsep;
|
||||
vals[2] = 1;
|
||||
vals[3] = 1;
|
||||
vals[4] = XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE;
|
||||
xcb_create_window(conn, XCB_COPY_FROM_PARENT, win, scr->root,
|
||||
0, 0, 1, 1, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT,
|
||||
0, 0, 1, 1, PopupBorder, XCB_WINDOW_CLASS_INPUT_OUTPUT,
|
||||
scr->root_visual, mask, vals);
|
||||
type = getatom("_NET_WM_WINDOW_TYPE");
|
||||
tooltip = getatom("_NET_WM_WINDOW_TYPE_TOOLTIP");
|
||||
@@ -250,11 +240,7 @@ wininit(void)
|
||||
win, type, XCB_ATOM_ATOM, 32, 1, &tooltip);
|
||||
gc = xcb_generate_id(conn);
|
||||
xcb_create_gc(conn, gc, win, 0, nil);
|
||||
if(!textinit()){
|
||||
fprint(2, "strans: popup disabled: no usable fonts\n");
|
||||
wincleanup();
|
||||
return 0;
|
||||
}
|
||||
textinit();
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -306,43 +292,54 @@ putimage(int w, int h)
|
||||
xcb_clear_area(conn, 0, win, 0, 0, w, h);
|
||||
}
|
||||
|
||||
static int
|
||||
static void
|
||||
winhide(void)
|
||||
{
|
||||
xcb_unmap_window(conn, win);
|
||||
return xcb_flush(conn) > 0;
|
||||
xcb_flush(conn);
|
||||
shown = 0;
|
||||
}
|
||||
|
||||
/* Draws dc at the caret, or by the pointer when the caret is unknown. */
|
||||
static int
|
||||
/*
|
||||
* Draws dc at the caret, or by the pointer when the caret is unknown:
|
||||
* where the pointer was as the popup came up, so that it does not
|
||||
* follow the mouse from key to key.
|
||||
*/
|
||||
static void
|
||||
winshow(Drawcmd *dc)
|
||||
{
|
||||
Area area;
|
||||
Popup p;
|
||||
int ax, ay, px, py, x, y;
|
||||
int ax, ay, x, y;
|
||||
u32int vals[5];
|
||||
xcb_query_pointer_reply_t *ptr;
|
||||
xcb_query_pointer_cookie_t cookie;
|
||||
|
||||
if(dc->nkouho == 0 && dc->pre.n == 0)
|
||||
return winhide();
|
||||
px = py = 0;
|
||||
if(!dc->caret.valid){
|
||||
if(dc->nkouho == 0 && dc->pre.n == 0){
|
||||
winhide();
|
||||
return;
|
||||
}
|
||||
if(!dc->caret.valid && !shown){
|
||||
cookie = xcb_query_pointer(conn, scr->root);
|
||||
ptr = xcb_query_pointer_reply(conn, cookie, nil);
|
||||
if(ptr == nil)
|
||||
return 0;
|
||||
px = ptr->root_x;
|
||||
py = ptr->root_y;
|
||||
return;
|
||||
ptrx = ptr->root_x;
|
||||
ptry = ptr->root_y;
|
||||
free(ptr);
|
||||
}
|
||||
ax = dc->caret.valid ? dc->caret.x : px;
|
||||
ay = dc->caret.valid ? dc->caret.y : py;
|
||||
ax = dc->caret.valid ? dc->caret.x : ptrx;
|
||||
ay = dc->caret.valid ? dc->caret.y : ptry;
|
||||
popupwork(ax, ay, &area);
|
||||
popuplayout(dc, area.w, area.h, &p);
|
||||
if(p.w <= 0 || p.h <= 0 || !resizebacking(p.w, p.h))
|
||||
return 0;
|
||||
popupposition(&dc->caret, px, py, &area, p.w, p.h, &x, &y);
|
||||
if(p.w <= 0 || p.h <= 0){
|
||||
winhide();
|
||||
return;
|
||||
}
|
||||
if(!resizebacking(p.w, p.h))
|
||||
return;
|
||||
popupposition(&dc->caret, ptrx, ptry, &area, p.w + 2*PopupBorder,
|
||||
p.h + 2*PopupBorder, &x, &y);
|
||||
/* Mapping does not restack: raise above windows opened since. */
|
||||
vals[0] = x;
|
||||
vals[1] = y;
|
||||
@@ -356,7 +353,8 @@ winshow(Drawcmd *dc)
|
||||
popupdraw(img, dc, &p);
|
||||
putimage(p.w, p.h);
|
||||
xcb_map_window(conn, win);
|
||||
return xcb_flush(conn) > 0;
|
||||
xcb_flush(conn);
|
||||
shown = 1;
|
||||
}
|
||||
|
||||
void
|
||||
@@ -367,8 +365,7 @@ drawthread(void*)
|
||||
threadsetname("draw");
|
||||
if(!wininit())
|
||||
return;
|
||||
while(chanrecv(drawc, &dc) > 0)
|
||||
if(!winshow(&dc))
|
||||
break;
|
||||
while(chanrecv(drawc, &dc) > 0 && !xcb_connection_has_error(conn))
|
||||
winshow(&dc);
|
||||
wincleanup();
|
||||
}
|
||||
|
||||
798
wl.c
Normal file
798
wl.c
Normal file
@@ -0,0 +1,798 @@
|
||||
#include "dat.h"
|
||||
#include "fn.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <poll.h>
|
||||
#include <sys/mman.h>
|
||||
#include <time.h>
|
||||
#include <wayland-client.h>
|
||||
#include <xkbcommon/xkbcommon.h>
|
||||
#include "imv2.h"
|
||||
#include "vkv1.h"
|
||||
|
||||
enum
|
||||
{
|
||||
Maxcode = 0x300, /* KEY_MAX+1: what evdev can send */
|
||||
};
|
||||
|
||||
/*
|
||||
* What popuplayout may use. The compositor puts the popup at the text
|
||||
* cursor and constrains it there, so this need only be wide enough for
|
||||
* the layout's own width and tall enough for every row it can show.
|
||||
*/
|
||||
#define Popupw PopupBasew
|
||||
#define Popuph ((Maxdisp + 2)*Fontsz + 2*PopupPad + PopupSep)
|
||||
|
||||
/*
|
||||
* One of two shm buffers, busy from attach until the compositor releases
|
||||
* it. win.c's single grow-only image would be redrawn while the
|
||||
* compositor was still reading it.
|
||||
*/
|
||||
typedef struct Buf Buf;
|
||||
struct Buf
|
||||
{
|
||||
struct wl_buffer *b;
|
||||
u32int *img;
|
||||
int size;
|
||||
int w;
|
||||
int h;
|
||||
int busy;
|
||||
};
|
||||
|
||||
static struct wl_display *display;
|
||||
static struct wl_seat *seat;
|
||||
static struct wl_compositor *comp;
|
||||
static struct wl_shm *shm;
|
||||
static struct wl_surface *surface;
|
||||
static struct zwp_input_popup_surface_v2 *popsurf;
|
||||
static struct zwp_input_method_manager_v2 *immgr;
|
||||
static struct zwp_virtual_keyboard_manager_v1 *vkmgr;
|
||||
static struct zwp_input_method_v2 *im;
|
||||
static struct zwp_virtual_keyboard_v1 *vk;
|
||||
static struct zwp_input_method_keyboard_grab_v2 *grab;
|
||||
static struct xkb_context *xkbctx;
|
||||
static struct xkb_state *kstate;
|
||||
static Channel *replyc;
|
||||
|
||||
/*
|
||||
* The protocol gives one input context per seat, so the engine knows us
|
||||
* by one address; nil would mean no owner at all.
|
||||
*/
|
||||
static int context;
|
||||
|
||||
static u32int serial; /* done events counted; the serial commit takes */
|
||||
static int active; /* a text input is being served */
|
||||
static int pendingactive;
|
||||
static int activated; /* an activate arrived since the last done */
|
||||
static u32int purpose, pendingpurpose;
|
||||
static Str surround, pendingsurround; /* the client's text before the cursor */
|
||||
static u32int sent[Maxcode/32]; /* keycodes passed on and still down */
|
||||
static u32int lasttime; /* the compositor's clock, for a release of our own */
|
||||
static int reprate; /* repeats a second the seat asks for; 0 is none */
|
||||
static int repdelay; /* ms a key is held before the first repeat */
|
||||
static u32int repcode; /* the key repeating */
|
||||
static vlong repdue; /* when its next repeat falls due; 0 if none */
|
||||
static int engaged; /* the engine owes this context a release */
|
||||
static int preshown; /* our preedit is on the client's screen */
|
||||
static int haskeymap;
|
||||
static int gone; /* unavailable: the seat is another input method's */
|
||||
static Buf bufs[2];
|
||||
static int shown; /* a buffer is attached and the popup is up */
|
||||
static Drawcmd held; /* a picture that came while both buffers were busy */
|
||||
static int helddraw;
|
||||
|
||||
static void grabkeyboard(void);
|
||||
static void popup(Drawcmd*);
|
||||
|
||||
static void
|
||||
wllog(char *msg)
|
||||
{
|
||||
fprint(2, "strans: wl: %s\n", msg);
|
||||
}
|
||||
|
||||
/* A repeat deadline needs a clock that cannot step; nsec() is not one. */
|
||||
static vlong
|
||||
nowms(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (vlong)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||||
}
|
||||
|
||||
static int
|
||||
hidden(void)
|
||||
{
|
||||
return purpose == Purposepassword || purpose == Purposepin;
|
||||
}
|
||||
|
||||
static void
|
||||
sendrequest(int op, u32int ks, u32int mod, Keyres *res)
|
||||
{
|
||||
Keyreq kr;
|
||||
|
||||
memset(&kr, 0, sizeof kr);
|
||||
kr.owner = &context;
|
||||
kr.clientpre = 1; /* set_preedit_string draws it; the popup does not */
|
||||
kr.op = op;
|
||||
kr.ks = ks;
|
||||
kr.mod = mod;
|
||||
kr.surround = surround;
|
||||
kr.reply = replyc;
|
||||
chansend(keyc, &kr);
|
||||
chanrecv(replyc, res);
|
||||
}
|
||||
|
||||
/* The bytes the last n runes of the client's text take. */
|
||||
static int
|
||||
backbytes(int n)
|
||||
{
|
||||
int i, nb;
|
||||
|
||||
nb = 0;
|
||||
for(i = max(surround.n - n, 0); i < surround.n; i++)
|
||||
nb += runelen(surround.r[i]);
|
||||
return nb;
|
||||
}
|
||||
|
||||
/* The engine's modifier bits, read from the grab's own keyboard state. */
|
||||
static u32int
|
||||
modmask(void)
|
||||
{
|
||||
static char *name[] = {
|
||||
XKB_MOD_NAME_SHIFT, XKB_MOD_NAME_CTRL,
|
||||
XKB_MOD_NAME_ALT, XKB_MOD_NAME_LOGO,
|
||||
};
|
||||
static u32int bit[] = {Mshift, Mctrl, Malt, Msuper};
|
||||
u32int m;
|
||||
int i;
|
||||
|
||||
m = 0;
|
||||
for(i = 0; i < nelem(name); i++)
|
||||
if(xkb_state_mod_name_is_active(kstate, name[i],
|
||||
XKB_STATE_MODS_EFFECTIVE) > 0)
|
||||
m |= bit[i];
|
||||
return m;
|
||||
}
|
||||
|
||||
static int
|
||||
issent(u32int code)
|
||||
{
|
||||
return code < Maxcode && (sent[code/32] & 1<<(code%32)) != 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* A key the engine did not take goes to the client as a key, not as
|
||||
* text: eaten = 0 means it is not ours, and committing it would lie to a
|
||||
* terminal and to anything with a keybinding.
|
||||
*/
|
||||
static void
|
||||
forward(u32int code, u32int state)
|
||||
{
|
||||
if(!haskeymap || code >= Maxcode)
|
||||
return;
|
||||
zwp_virtual_keyboard_v1_key(vk, lasttime, code, state);
|
||||
if(state == WL_KEYBOARD_KEY_STATE_PRESSED)
|
||||
sent[code/32] |= 1<<(code%32);
|
||||
else
|
||||
sent[code/32] &= ~(1<<(code%32));
|
||||
}
|
||||
|
||||
/* Whatever we passed on is still down; the client must not keep it. */
|
||||
static void
|
||||
releasekeys(void)
|
||||
{
|
||||
u32int code;
|
||||
|
||||
for(code = 0; code < Maxcode; code++)
|
||||
if(issent(code))
|
||||
forward(code, WL_KEYBOARD_KEY_STATE_RELEASED);
|
||||
}
|
||||
|
||||
/*
|
||||
* The engine's text, then the composed text, in one commit_string: the
|
||||
* compositor keeps only the last one sent before a commit. A commit is
|
||||
* an event and goes only when there is something, while a preedit is
|
||||
* state and goes every time, empty to take the last one down.
|
||||
*/
|
||||
static void
|
||||
answer(Keyres *res, char *tail)
|
||||
{
|
||||
char utf[2*Maxutf];
|
||||
int n;
|
||||
|
||||
/* The compositor takes the text back before it inserts ours. */
|
||||
if(res->del > 0)
|
||||
zwp_input_method_v2_delete_surrounding_text(im,
|
||||
backbytes(res->del), 0);
|
||||
n = stoutf(&res->commit, utf, sizeof utf);
|
||||
if(tail[0] != '\0')
|
||||
n += snprint(utf+n, sizeof utf - n, "%s", tail);
|
||||
if(n > 0)
|
||||
zwp_input_method_v2_commit_string(im, utf);
|
||||
n = stoutf(&res->preedit, utf, sizeof utf);
|
||||
if(n > 0 || preshown) /* an empty one only to take the last down */
|
||||
zwp_input_method_v2_set_preedit_string(im, utf, n, n);
|
||||
zwp_input_method_v2_commit(im, serial);
|
||||
preshown = n > 0;
|
||||
}
|
||||
|
||||
static void
|
||||
bufrelease(void *p, struct wl_buffer*)
|
||||
{
|
||||
((Buf*)p)->busy = 0;
|
||||
if(helddraw){
|
||||
helddraw = 0;
|
||||
popup(&held);
|
||||
}
|
||||
}
|
||||
|
||||
static const struct wl_buffer_listener buflisten = {bufrelease};
|
||||
|
||||
static void
|
||||
bufclear(Buf *p)
|
||||
{
|
||||
if(p->b != nil)
|
||||
wl_buffer_destroy(p->b);
|
||||
if(p->img != nil)
|
||||
munmap(p->img, p->size);
|
||||
memset(p, 0, sizeof *p);
|
||||
}
|
||||
|
||||
/* An anonymous file of pixels, shared with the compositor. */
|
||||
static int
|
||||
shmfd(int size)
|
||||
{
|
||||
char path[] = "/dev/shm/strans-XXXXXX";
|
||||
int fd;
|
||||
|
||||
fd = mkstemp(path);
|
||||
if(fd < 0)
|
||||
return -1;
|
||||
unlink(path);
|
||||
if(ftruncate(fd, size) < 0){
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
/*
|
||||
* A free buffer of the wanted size. Sizes differ from draw to draw, so
|
||||
* a free one of the wrong size is remade; both busy means the compositor
|
||||
* is still reading them and the picture has to wait.
|
||||
*/
|
||||
static Buf*
|
||||
getbuf(int w, int h)
|
||||
{
|
||||
struct wl_shm_pool *pool;
|
||||
Buf *p;
|
||||
int fd, i, size;
|
||||
|
||||
for(i = 0; i < nelem(bufs); i++)
|
||||
if(!bufs[i].busy && bufs[i].w == w && bufs[i].h == h)
|
||||
return &bufs[i];
|
||||
for(i = 0; i < nelem(bufs); i++)
|
||||
if(!bufs[i].busy)
|
||||
break;
|
||||
if(i == nelem(bufs))
|
||||
return nil;
|
||||
p = &bufs[i];
|
||||
bufclear(p);
|
||||
size = w * h * sizeof p->img[0];
|
||||
fd = shmfd(size);
|
||||
if(fd < 0)
|
||||
return nil;
|
||||
p->img = mmap(nil, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
|
||||
if(p->img == MAP_FAILED){
|
||||
p->img = nil;
|
||||
close(fd);
|
||||
return nil;
|
||||
}
|
||||
pool = wl_shm_create_pool(shm, fd, size);
|
||||
p->b = wl_shm_pool_create_buffer(pool, 0, w, h,
|
||||
w * sizeof p->img[0], WL_SHM_FORMAT_XRGB8888);
|
||||
wl_shm_pool_destroy(pool);
|
||||
close(fd);
|
||||
wl_buffer_add_listener(p->b, &buflisten, p);
|
||||
p->size = size;
|
||||
p->w = w;
|
||||
p->h = h;
|
||||
return p;
|
||||
}
|
||||
|
||||
/* Unmapping takes the popup down; the compositor puts it back at the
|
||||
* next activate with whatever buffer is still attached. */
|
||||
static void
|
||||
hidepopup(void)
|
||||
{
|
||||
helddraw = 0; /* a picture still waiting for a buffer is stale now */
|
||||
if(!shown)
|
||||
return;
|
||||
wl_surface_attach(surface, nil, 0, 0);
|
||||
wl_surface_commit(surface);
|
||||
shown = 0;
|
||||
}
|
||||
|
||||
static void
|
||||
popup(Drawcmd *dc)
|
||||
{
|
||||
Popup p;
|
||||
Buf *b;
|
||||
|
||||
popuplayout(dc, Popupw, Popuph, &p);
|
||||
if(p.w <= 0 || p.h <= 0){
|
||||
hidepopup();
|
||||
return;
|
||||
}
|
||||
/* A buffer that is not a multiple of the scale is an invalid_size
|
||||
* error at attach, which would kill the connection. */
|
||||
p.w = (p.w + popupscale - 1) / popupscale * popupscale;
|
||||
p.h = (p.h + popupscale - 1) / popupscale * popupscale;
|
||||
b = getbuf(p.w, p.h);
|
||||
if(b == nil){
|
||||
held = *dc; /* the engine will not send it twice */
|
||||
helddraw = 1;
|
||||
return;
|
||||
}
|
||||
popupdraw(b->img, dc, &p);
|
||||
wl_surface_attach(surface, b->b, 0, 0);
|
||||
wl_surface_set_buffer_scale(surface, popupscale);
|
||||
wl_surface_damage_buffer(surface, 0, 0, p.w, p.h);
|
||||
wl_surface_commit(surface);
|
||||
b->busy = 1;
|
||||
shown = 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* drawc is empty or holds exactly what the engine last published, so one
|
||||
* take is enough. lastdraw follows the engine and not our surface, so
|
||||
* take it wherever the two can part -- but show it only while the engine
|
||||
* is ours: what it drew for another frontend is not ours to put up.
|
||||
*/
|
||||
static void
|
||||
takedraw(void)
|
||||
{
|
||||
Drawcmd dc;
|
||||
|
||||
if(channbrecv(drawc, &dc) > 0 && engaged)
|
||||
popup(&dc);
|
||||
}
|
||||
|
||||
/* The compositor places the popup; where the text is does not concern us. */
|
||||
static void
|
||||
poprect(void*, struct zwp_input_popup_surface_v2*, int, int, int, int)
|
||||
{
|
||||
}
|
||||
|
||||
static const struct zwp_input_popup_surface_v2_listener poplisten = {poprect};
|
||||
|
||||
static void
|
||||
releasegrab(void)
|
||||
{
|
||||
if(grab == nil)
|
||||
return;
|
||||
zwp_input_method_keyboard_grab_v2_release(grab);
|
||||
grab = nil;
|
||||
}
|
||||
|
||||
/*
|
||||
* The context is going: give the client its keys back. The pending text
|
||||
* goes nowhere either way -- the compositor drops a commit once the text
|
||||
* input has its leave, and a client that disabled its own text input has
|
||||
* stopped listening, so it throws a relayed commit away. XIM and IBus
|
||||
* hand the text back here because their focus-out is a round trip the
|
||||
* client still waits on; a deactivate is not.
|
||||
*/
|
||||
static void
|
||||
leave(void)
|
||||
{
|
||||
Keyres res;
|
||||
|
||||
repdue = 0;
|
||||
releasekeys();
|
||||
composedrop(Composewl, &context);
|
||||
if(engaged){
|
||||
sendrequest(Keyrelease, 0, 0, &res);
|
||||
engaged = 0;
|
||||
takedraw();
|
||||
}
|
||||
hidepopup();
|
||||
preshown = 0;
|
||||
}
|
||||
|
||||
/* A purpose that turned secret with text pending gets the text first. */
|
||||
static void
|
||||
flushpending(void)
|
||||
{
|
||||
Keyres res;
|
||||
|
||||
if(!engaged)
|
||||
return;
|
||||
sendrequest(Keyreset, 0, 0, &res);
|
||||
answer(&res, "");
|
||||
takedraw();
|
||||
}
|
||||
|
||||
/*
|
||||
* A purpose belongs to the activation: the compositor sends a content type
|
||||
* only for a client that asked for one, so a client that asks for none has
|
||||
* none, whatever the last one wanted.
|
||||
*/
|
||||
static void
|
||||
imactivate(void*, struct zwp_input_method_v2*)
|
||||
{
|
||||
pendingactive = 1;
|
||||
activated = 1;
|
||||
pendingpurpose = 0;
|
||||
sclear(&pendingsurround);
|
||||
}
|
||||
|
||||
static void
|
||||
imdeactivate(void*, struct zwp_input_method_v2*)
|
||||
{
|
||||
pendingactive = 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* The text around the cursor, of which a reading can use what comes
|
||||
* before it. A text input that sends none leaves this empty, and then
|
||||
* nothing is ever reached back into or taken away.
|
||||
*/
|
||||
static void
|
||||
imsurrounding(void*, struct zwp_input_method_v2*, const char *text,
|
||||
u32int cursor, u32int)
|
||||
{
|
||||
u32int n;
|
||||
|
||||
n = strlen(text);
|
||||
stail(&pendingsurround, (char*)text, cursor < n ? cursor : n);
|
||||
}
|
||||
|
||||
static void
|
||||
imcause(void*, struct zwp_input_method_v2*, u32int)
|
||||
{
|
||||
}
|
||||
|
||||
static void
|
||||
imcontent(void*, struct zwp_input_method_v2*, u32int, u32int what)
|
||||
{
|
||||
pendingpurpose = what;
|
||||
}
|
||||
|
||||
/*
|
||||
* activate, deactivate and content_type are pending until done, and the
|
||||
* count of done events is the serial a commit must carry: wlroots throws
|
||||
* away a whole commit whose serial does not match, without a word.
|
||||
*/
|
||||
static void
|
||||
imdone(void*, struct zwp_input_method_v2*)
|
||||
{
|
||||
int washidden;
|
||||
|
||||
serial++;
|
||||
washidden = hidden();
|
||||
purpose = pendingpurpose;
|
||||
surround = pendingsurround;
|
||||
if(activated || (active && !pendingactive))
|
||||
leave();
|
||||
else if(active && !washidden && hidden())
|
||||
flushpending();
|
||||
if(activated && pendingactive)
|
||||
grabkeyboard();
|
||||
else if(!pendingactive)
|
||||
releasegrab();
|
||||
active = pendingactive;
|
||||
activated = 0;
|
||||
}
|
||||
|
||||
static void
|
||||
imgone(void*, struct zwp_input_method_v2*)
|
||||
{
|
||||
gone = 1;
|
||||
}
|
||||
|
||||
static const struct zwp_input_method_v2_listener imlisten = {
|
||||
imactivate, imdeactivate, imsurrounding, imcause, imcontent, imdone,
|
||||
imgone,
|
||||
};
|
||||
|
||||
/*
|
||||
* The same fd goes to the virtual keyboard, so a key we pass on means
|
||||
* there what it meant here.
|
||||
*/
|
||||
static void
|
||||
grabkeymap(void*, struct zwp_input_method_keyboard_grab_v2*, u32int format,
|
||||
int fd, u32int size)
|
||||
{
|
||||
struct xkb_keymap *keymap;
|
||||
struct xkb_state *state;
|
||||
char *s;
|
||||
|
||||
if(format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1){
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
s = mmap(nil, size, PROT_READ, MAP_PRIVATE, fd, 0);
|
||||
if(s == MAP_FAILED){
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
keymap = xkb_keymap_new_from_string(xkbctx, s,
|
||||
XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS);
|
||||
munmap(s, size);
|
||||
state = keymap != nil ? xkb_state_new(keymap) : nil;
|
||||
xkb_keymap_unref(keymap);
|
||||
if(state == nil){
|
||||
wllog("cannot read the keyboard mapping");
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
xkb_state_unref(kstate);
|
||||
kstate = state;
|
||||
zwp_virtual_keyboard_v1_keymap(vk, format, fd, size);
|
||||
close(fd);
|
||||
haskeymap = 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* A press, from the keyboard or from the repeat deadline. Returns whether
|
||||
* the engine took it: what it did not take has gone on to the client as a
|
||||
* key, and a key at the client is the client's own to repeat.
|
||||
*/
|
||||
static int
|
||||
presskey(u32int code)
|
||||
{
|
||||
Keyres res;
|
||||
char text[Maxutf];
|
||||
u32int key, sym;
|
||||
|
||||
sym = xkb_state_key_get_one_sym(kstate, code + 8);
|
||||
if(composekey(Composewl, &context, sym, text, sizeof text))
|
||||
return 0; /* a sequence in the making */
|
||||
key = ipckeysym(sym, xkb_keysym_to_utf32(sym));
|
||||
if(keymeaningful(key))
|
||||
engaged = 1;
|
||||
if(text[0] != '\0')
|
||||
/* Composed text follows whatever was pending. */
|
||||
sendrequest(Keyreset, 0, 0, &res);
|
||||
else
|
||||
sendrequest(Keypress, key, modmask(), &res);
|
||||
answer(&res, text);
|
||||
takedraw();
|
||||
if(text[0] == '\0' && !res.eaten)
|
||||
forward(code, WL_KEYBOARD_KEY_STATE_PRESSED);
|
||||
/* A finished sequence is one act; only a key is held down. */
|
||||
return text[0] == '\0' && res.eaten;
|
||||
}
|
||||
|
||||
static void
|
||||
grabkey(void*, struct zwp_input_method_keyboard_grab_v2*, u32int,
|
||||
u32int time, u32int code, u32int state)
|
||||
{
|
||||
lasttime = time;
|
||||
if(state != WL_KEYBOARD_KEY_STATE_PRESSED){
|
||||
if(code == repcode)
|
||||
repdue = 0;
|
||||
if(issent(code))
|
||||
forward(code, state);
|
||||
return;
|
||||
}
|
||||
repdue = 0; /* one key repeats at a time, and this is the newest */
|
||||
if(!active || hidden() || kstate == nil){
|
||||
forward(code, state);
|
||||
return;
|
||||
}
|
||||
if(!presskey(code) || reprate <= 0 ||
|
||||
!xkb_keymap_key_repeats(xkb_state_get_keymap(kstate), code + 8))
|
||||
return;
|
||||
repcode = code;
|
||||
repdue = nowms() + repdelay;
|
||||
}
|
||||
|
||||
/*
|
||||
* Our own lookup must see Shift, and so must the client: wlroots derives
|
||||
* no state from a virtual keyboard's keys, so a modifier forwarded as a
|
||||
* keycode arrives bare and Ctrl+C reaches the client as c.
|
||||
*/
|
||||
static void
|
||||
grabmodifiers(void*, struct zwp_input_method_keyboard_grab_v2*, u32int,
|
||||
u32int depressed, u32int latched, u32int locked, u32int group)
|
||||
{
|
||||
if(kstate != nil)
|
||||
xkb_state_update_mask(kstate, depressed, latched, locked,
|
||||
0, 0, group);
|
||||
if(haskeymap)
|
||||
zwp_virtual_keyboard_v1_modifiers(vk, depressed, latched,
|
||||
locked, group);
|
||||
}
|
||||
|
||||
static void
|
||||
grabrepeat(void*, struct zwp_input_method_keyboard_grab_v2*, int rate,
|
||||
int delay)
|
||||
{
|
||||
reprate = rate;
|
||||
repdelay = delay;
|
||||
if(rate <= 0)
|
||||
repdue = 0;
|
||||
}
|
||||
|
||||
static const struct zwp_input_method_keyboard_grab_v2_listener grablisten = {
|
||||
grabkeymap, grabkey, grabmodifiers, grabrepeat,
|
||||
};
|
||||
|
||||
/*
|
||||
* sway hands keys to the grab holder whenever the grab object exists,
|
||||
* without asking whether the input method is active, so a grab held
|
||||
* while inactive takes every key in the session. Two activates can also
|
||||
* arrive without a deactivate between them, and a second grab_keyboard
|
||||
* then fails and leaves a dead object: release before grabbing again.
|
||||
*/
|
||||
static void
|
||||
grabkeyboard(void)
|
||||
{
|
||||
releasegrab();
|
||||
grab = zwp_input_method_v2_grab_keyboard(im);
|
||||
zwp_input_method_keyboard_grab_v2_add_listener(grab, &grablisten, nil);
|
||||
}
|
||||
|
||||
static void
|
||||
regglobal(void*, struct wl_registry *r, u32int name, const char *iface, u32int)
|
||||
{
|
||||
if(strcmp(iface, wl_seat_interface.name) == 0){
|
||||
if(seat == nil) /* the first seat is ours */
|
||||
seat = wl_registry_bind(r, name, &wl_seat_interface, 1);
|
||||
}else if(strcmp(iface, wl_compositor_interface.name) == 0)
|
||||
/* 4 for set_buffer_scale and damage_buffer */
|
||||
comp = wl_registry_bind(r, name, &wl_compositor_interface, 4);
|
||||
else if(strcmp(iface, wl_shm_interface.name) == 0)
|
||||
shm = wl_registry_bind(r, name, &wl_shm_interface, 1);
|
||||
else if(strcmp(iface, zwp_input_method_manager_v2_interface.name) == 0)
|
||||
immgr = wl_registry_bind(r, name,
|
||||
&zwp_input_method_manager_v2_interface, 1);
|
||||
else if(strcmp(iface, zwp_virtual_keyboard_manager_v1_interface.name) == 0)
|
||||
vkmgr = wl_registry_bind(r, name,
|
||||
&zwp_virtual_keyboard_manager_v1_interface, 1);
|
||||
}
|
||||
|
||||
static void
|
||||
regremove(void*, struct wl_registry*, u32int)
|
||||
{
|
||||
}
|
||||
|
||||
static const struct wl_registry_listener reglisten = {regglobal, regremove};
|
||||
|
||||
/*
|
||||
* Another frontend may have taken the engine; then our preedit and our
|
||||
* popup are both stale. Nothing later will say so: the engine's own
|
||||
* pictures go to whoever took it, and it settles back at blank.
|
||||
*/
|
||||
static void
|
||||
checkowner(void)
|
||||
{
|
||||
Keyres res;
|
||||
|
||||
if(!preshown && !shown)
|
||||
return;
|
||||
sendrequest(Keycap, 0, 0, &res);
|
||||
if(res.eaten)
|
||||
return;
|
||||
hidepopup();
|
||||
if(preshown){
|
||||
zwp_input_method_v2_set_preedit_string(im, "", 0, 0);
|
||||
zwp_input_method_v2_commit(im, serial);
|
||||
preshown = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* One repeat, once the key has been held long enough. Under the grab the
|
||||
* compositor feeds the client nothing while the key is down, so what it
|
||||
* would have repeated there is ours to make -- but only while the engine
|
||||
* wants the key. Backspace stops being ours once the preedit is empty,
|
||||
* and by then presskey has passed it on, so the key is held down at the
|
||||
* client and the client repeats it from there.
|
||||
*/
|
||||
static void
|
||||
repeat(void)
|
||||
{
|
||||
if(nowms() < repdue)
|
||||
return;
|
||||
if(!active || hidden() || kstate == nil || !presskey(repcode)){
|
||||
repdue = 0;
|
||||
return;
|
||||
}
|
||||
repdue = nowms() + 1000/reprate;
|
||||
}
|
||||
|
||||
/*
|
||||
* Whether this session has the protocol, not whether it is Wayland:
|
||||
* GNOME and KWin set WAYLAND_DISPLAY and have no manager, and XIM is the
|
||||
* only path left to them.
|
||||
*/
|
||||
int
|
||||
wlinit(void)
|
||||
{
|
||||
struct wl_registry *reg;
|
||||
|
||||
display = wl_display_connect(nil);
|
||||
if(display == nil)
|
||||
return 0;
|
||||
reg = wl_display_get_registry(display);
|
||||
wl_registry_add_listener(reg, ®listen, 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");
|
||||
}
|
||||
@@ -5,18 +5,17 @@
|
||||
#include <locale.h>
|
||||
#include <poll.h>
|
||||
#include <xcb/xcb.h>
|
||||
#include <xcb/xcb_aux.h>
|
||||
#include <xcb/xkb.h>
|
||||
#include <xkbcommon/xkbcommon.h>
|
||||
#include <xkbcommon/xkbcommon-compose.h>
|
||||
#include <xkbcommon/xkbcommon-names.h>
|
||||
#include <xkbcommon/xkbcommon-x11.h>
|
||||
#include <xcb-imdkit/imdkit.h>
|
||||
#include <xcb-imdkit/encoding.h>
|
||||
|
||||
u32int keymaplookup(struct xkb_state*, uint8_t, u16int);
|
||||
|
||||
/*
|
||||
* One XIM input context. Only PreeditCallbacks clients draw the preedit
|
||||
* themselves (cap); the popup shows it for the others. engaged means the
|
||||
* themselves (clientpre); the popup shows it for the others. engaged means the
|
||||
* engine has seen a real key from this context and owes it a release.
|
||||
*/
|
||||
typedef struct Ic Ic;
|
||||
@@ -26,21 +25,15 @@ struct Ic
|
||||
xcb_im_input_context_t *xic;
|
||||
xcb_im_client_t *client;
|
||||
int engaged;
|
||||
int cap;
|
||||
int clientpre;
|
||||
int prestarted;
|
||||
int nprerune;
|
||||
Caret caret;
|
||||
};
|
||||
|
||||
enum
|
||||
{
|
||||
Ownerpoll = 200, /* ms between owner checks while a preedit shows */
|
||||
};
|
||||
|
||||
static xcb_connection_t *conn;
|
||||
static xcb_im_t *xim;
|
||||
static struct xkb_state *kstate;
|
||||
static struct xkb_compose_state *compose;
|
||||
static uint8_t xkbevent;
|
||||
static xcb_window_t rootwin;
|
||||
static Ic *ics;
|
||||
@@ -52,6 +45,9 @@ static u32int styles[] = {
|
||||
XCB_IM_PreeditPosition | XCB_IM_StatusNothing,
|
||||
XCB_IM_PreeditCallbacks | XCB_IM_StatusNothing,
|
||||
XCB_IM_PreeditNothing | XCB_IM_StatusNothing,
|
||||
XCB_IM_PreeditPosition | XCB_IM_StatusNone,
|
||||
XCB_IM_PreeditCallbacks | XCB_IM_StatusNone,
|
||||
XCB_IM_PreeditNothing | XCB_IM_StatusNone,
|
||||
};
|
||||
|
||||
static void
|
||||
@@ -99,26 +95,6 @@ out:
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* Dead keys and Compose: Xlib leaves them to the input method server. */
|
||||
static void
|
||||
cinit(void)
|
||||
{
|
||||
struct xkb_context *context;
|
||||
struct xkb_compose_table *table;
|
||||
char *locale;
|
||||
|
||||
locale = setlocale(LC_CTYPE, "");
|
||||
context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
|
||||
if(context == nil)
|
||||
return;
|
||||
table = xkb_compose_table_new_from_locale(context,
|
||||
locale != nil ? locale : "C", XKB_COMPOSE_COMPILE_NO_FLAGS);
|
||||
if(table != nil)
|
||||
compose = xkb_compose_state_new(table, XKB_COMPOSE_STATE_NO_FLAGS);
|
||||
xkb_compose_table_unref(table);
|
||||
xkb_context_unref(context);
|
||||
}
|
||||
|
||||
static void
|
||||
kwatch(void)
|
||||
{
|
||||
@@ -135,18 +111,6 @@ kwatch(void)
|
||||
events, parts, parts, nil);
|
||||
}
|
||||
|
||||
static xcb_screen_t*
|
||||
getscreen(int scr)
|
||||
{
|
||||
xcb_screen_iterator_t iter;
|
||||
|
||||
iter = xcb_setup_roots_iterator(xcb_get_setup(conn));
|
||||
for(; iter.rem; scr--, xcb_screen_next(&iter))
|
||||
if(scr == 0)
|
||||
return iter.data;
|
||||
return nil;
|
||||
}
|
||||
|
||||
static int
|
||||
translate(xcb_window_t win, int x, int y, Caret *caret)
|
||||
{
|
||||
@@ -210,8 +174,13 @@ place(Ic *state)
|
||||
XCB_XIM_XNSpotLocation_MASK){
|
||||
attr = xcb_im_input_context_get_preedit_attr(state->xic);
|
||||
if(translate(focuswin, attr->spot_location.x,
|
||||
attr->spot_location.y, &state->caret))
|
||||
attr->spot_location.y, &state->caret)){
|
||||
/* A spot is a baseline: call the line one row above it,
|
||||
* so that a popup flipped above it clears the text. */
|
||||
state->caret.y -= Fontsz;
|
||||
state->caret.h = Fontsz;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(placebottom(state, focuswin))
|
||||
return;
|
||||
@@ -264,7 +233,7 @@ updatepreedit(Ic *state, Str *pre)
|
||||
size_t nwire;
|
||||
int i, nbyte;
|
||||
|
||||
if(!(state->cap & Cclientpreedit))
|
||||
if(!state->clientpre)
|
||||
return;
|
||||
if(pre->n == 0){
|
||||
clearpreedit(state);
|
||||
@@ -300,7 +269,7 @@ sendrequest(Ic *state, int op, u32int key, u32int mod, Keyres *res)
|
||||
|
||||
memset(&kr, 0, sizeof kr);
|
||||
kr.owner = state;
|
||||
kr.cap = state->cap;
|
||||
kr.clientpre = state->clientpre;
|
||||
kr.op = op;
|
||||
kr.ks = key;
|
||||
kr.mod = ipcmod(mod);
|
||||
@@ -379,47 +348,58 @@ resetic(Ic *state, xcb_im_reset_ic_reply_fr_t *reply)
|
||||
reply->byte_length_of_committed_string = nwire;
|
||||
}
|
||||
|
||||
/* The keysym for an X core key event, with its modifier and group state. */
|
||||
static u32int
|
||||
keymaplookup(struct xkb_state *state, uchar keycode, u16int corestate)
|
||||
{
|
||||
static char *modname[] = {
|
||||
XKB_MOD_NAME_SHIFT, XKB_MOD_NAME_CAPS, XKB_MOD_NAME_CTRL,
|
||||
XKB_MOD_NAME_MOD1, XKB_MOD_NAME_MOD2, XKB_MOD_NAME_MOD3,
|
||||
XKB_MOD_NAME_MOD4, XKB_MOD_NAME_MOD5,
|
||||
};
|
||||
struct xkb_keymap *keymap;
|
||||
xkb_mod_index_t index;
|
||||
xkb_mod_mask_t mods;
|
||||
int i;
|
||||
|
||||
keymap = xkb_state_get_keymap(state);
|
||||
mods = 0;
|
||||
for(i = 0; i < nelem(modname); i++){
|
||||
if(!(corestate & (1 << i)))
|
||||
continue;
|
||||
index = xkb_keymap_mod_get_index(keymap, modname[i]);
|
||||
if(index != XKB_MOD_INVALID)
|
||||
mods |= (xkb_mod_mask_t)1 << index;
|
||||
}
|
||||
xkb_state_update_mask(state, mods, 0, 0, 0, 0, (corestate >> 13) & 3);
|
||||
return xkb_state_key_get_one_sym(state, keycode);
|
||||
}
|
||||
|
||||
static void
|
||||
kpress(Ic *state, xcb_key_press_event_t *ev)
|
||||
{
|
||||
Keyres res;
|
||||
char buf[Maxutf];
|
||||
char buf[Maxutf], text[Maxutf];
|
||||
u32int key, sym;
|
||||
int composed, n, wasvalid;
|
||||
int n;
|
||||
|
||||
sym = keymaplookup(kstate, ev->detail, ev->state);
|
||||
composed = 0;
|
||||
if(compose != nil &&
|
||||
xkb_compose_state_feed(compose, sym) == XKB_COMPOSE_FEED_ACCEPTED)
|
||||
switch(xkb_compose_state_get_status(compose)){
|
||||
case XKB_COMPOSE_COMPOSING:
|
||||
case XKB_COMPOSE_CANCELLED:
|
||||
return;
|
||||
case XKB_COMPOSE_COMPOSED:
|
||||
sym = xkb_compose_state_get_one_sym(compose);
|
||||
composed = 1;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if(composekey(Composexim, state, sym, text, sizeof text))
|
||||
return;
|
||||
key = ipckeysym(sym, xkb_keysym_to_utf32(sym));
|
||||
if(keymeaningful(key)){
|
||||
wasvalid = state->caret.valid;
|
||||
if(keymeaningful(key))
|
||||
place(state);
|
||||
if(state->engaged && wasvalid && !state->caret.valid)
|
||||
sendrequest(state, Keycaret, 0, 0, &res);
|
||||
}
|
||||
keypress(state, key, ev->state, &res);
|
||||
if(text[0] != '\0')
|
||||
/* Composed text follows whatever was pending. */
|
||||
sendrequest(state, Keyreset, 0, 0, &res);
|
||||
else
|
||||
keypress(state, key, ev->state, &res);
|
||||
n = stoutf(&res.commit, buf, sizeof buf);
|
||||
commit(state, buf, n);
|
||||
updatepreedit(state, &res.preedit);
|
||||
if(res.eaten)
|
||||
;
|
||||
else if(composed){
|
||||
/* The event holds the last key of the sequence, not its result. */
|
||||
n = xkb_keysym_to_utf8(sym, buf, sizeof buf);
|
||||
commit(state, buf, max(n - 1, 0));
|
||||
}else
|
||||
if(text[0] != '\0')
|
||||
commit(state, text, strlen(text));
|
||||
else if(!res.eaten)
|
||||
xcb_im_forward_event(xim, state->xic, ev);
|
||||
xcb_flush(conn);
|
||||
}
|
||||
@@ -444,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);
|
||||
}
|
||||
@@ -456,9 +437,8 @@ iccreate(xcb_im_client_t *client, xcb_im_input_context_t *ic)
|
||||
state = emalloc(sizeof(Ic));
|
||||
state->xic = ic;
|
||||
state->client = client;
|
||||
if(xcb_im_input_context_get_input_style(ic) ==
|
||||
(XCB_IM_PreeditCallbacks | XCB_IM_StatusNothing))
|
||||
state->cap = Cclientpreedit;
|
||||
if(xcb_im_input_context_get_input_style(ic) & XCB_IM_PreeditCallbacks)
|
||||
state->clientpre = 1;
|
||||
state->next = ics;
|
||||
ics = state;
|
||||
xcb_im_input_context_set_data(ic, state, icfree);
|
||||
@@ -531,7 +511,7 @@ ximinit(void)
|
||||
ximlog("cannot connect to X server");
|
||||
return -1;
|
||||
}
|
||||
screen = getscreen(scr);
|
||||
screen = xcb_aux_get_screen(conn, scr);
|
||||
if(screen == nil){
|
||||
ximlog("cannot find X screen");
|
||||
return -1;
|
||||
@@ -541,7 +521,6 @@ ximinit(void)
|
||||
ximlog("cannot read keyboard mapping");
|
||||
return -1;
|
||||
}
|
||||
cinit();
|
||||
kwatch();
|
||||
win = xcb_generate_id(conn);
|
||||
xcb_create_window(conn, XCB_COPY_FROM_PARENT, win, screen->root,
|
||||
37
xim/keymap.c
37
xim/keymap.c
@@ -1,37 +0,0 @@
|
||||
#include <stdint.h>
|
||||
#include <xcb/xcb.h>
|
||||
#include <xkbcommon/xkbcommon.h>
|
||||
#include <xkbcommon/xkbcommon-names.h>
|
||||
|
||||
/* The keysym for an X core key event, with its modifier and group state. */
|
||||
uint32_t
|
||||
keymaplookup(struct xkb_state *state, uint8_t keycode, uint16_t corestate)
|
||||
{
|
||||
static const char *modname[] = {
|
||||
XKB_MOD_NAME_SHIFT,
|
||||
XKB_MOD_NAME_CAPS,
|
||||
XKB_MOD_NAME_CTRL,
|
||||
XKB_MOD_NAME_MOD1,
|
||||
XKB_MOD_NAME_MOD2,
|
||||
XKB_MOD_NAME_MOD3,
|
||||
XKB_MOD_NAME_MOD4,
|
||||
XKB_MOD_NAME_MOD5,
|
||||
};
|
||||
struct xkb_keymap *keymap;
|
||||
xkb_mod_index_t index;
|
||||
xkb_mod_mask_t mods;
|
||||
int i;
|
||||
|
||||
keymap = xkb_state_get_keymap(state);
|
||||
mods = 0;
|
||||
for(i = 0; i < (int)(sizeof modname / sizeof modname[0]); i++){
|
||||
if(!(corestate & (1U << i)))
|
||||
continue;
|
||||
index = xkb_keymap_mod_get_index(keymap, modname[i]);
|
||||
if(index != XKB_MOD_INVALID)
|
||||
mods |= (xkb_mod_mask_t)1 << index;
|
||||
}
|
||||
xkb_state_update_mask(state, mods, 0, 0, 0, 0,
|
||||
(corestate >> 13) & 3);
|
||||
return xkb_state_key_get_one_sym(state, keycode);
|
||||
}
|
||||
Reference in New Issue
Block a user